From 28e20752806a492f5a6a5d343c02f9556f39b1cd Mon Sep 17 00:00:00 2001 From: "henrike@webrtc.org" Date: Wed, 10 Jul 2013 00:45:36 +0000 Subject: [PATCH] Adds trunk/talk folder of revision 359 from libjingles google code to trunk/talk git-svn-id: http://webrtc.googlecode.com/svn/trunk@4318 4adac7df-926f-26a2-2b94-8c16560cd09d --- talk/OWNERS | 1 + talk/app/webrtc/audiotrack.cc | 53 + talk/app/webrtc/audiotrack.h | 66 + talk/app/webrtc/audiotrackrenderer.cc | 48 + talk/app/webrtc/audiotrackrenderer.h | 55 + talk/app/webrtc/datachannel.cc | 295 + talk/app/webrtc/datachannel.h | 154 + talk/app/webrtc/datachannelinterface.h | 127 + talk/app/webrtc/dtmfsender.cc | 257 + talk/app/webrtc/dtmfsender.h | 138 + talk/app/webrtc/dtmfsender_unittest.cc | 356 ++ talk/app/webrtc/dtmfsenderinterface.h | 105 + talk/app/webrtc/fakeportallocatorfactory.h | 74 + talk/app/webrtc/java/README | 23 + .../app/webrtc/java/jni/peerconnection_jni.cc | 1359 +++++ .../java/src/org/webrtc/AudioSource.java | 38 + .../java/src/org/webrtc/AudioTrack.java | 35 + .../java/src/org/webrtc/IceCandidate.java | 48 + .../java/src/org/webrtc/MediaConstraints.java | 85 + .../java/src/org/webrtc/MediaSource.java | 55 + .../java/src/org/webrtc/MediaStream.java | 114 + .../java/src/org/webrtc/MediaStreamTrack.java | 86 + .../java/src/org/webrtc/PeerConnection.java | 194 + .../src/org/webrtc/PeerConnectionFactory.java | 119 + .../java/src/org/webrtc/SdpObserver.java | 43 + .../src/org/webrtc/SessionDescription.java | 57 + .../java/src/org/webrtc/StatsObserver.java | 34 + .../java/src/org/webrtc/StatsReport.java | 72 + .../java/src/org/webrtc/VideoCapturer.java | 53 + .../java/src/org/webrtc/VideoRenderer.java | 136 + .../java/src/org/webrtc/VideoSource.java | 36 + .../java/src/org/webrtc/VideoTrack.java | 65 + .../libjingle_peerconnection_java_unittest.sh | 47 + .../src/org/webrtc/PeerConnectionTest.java | 532 ++ talk/app/webrtc/jsep.h | 164 + talk/app/webrtc/jsepicecandidate.cc | 105 + talk/app/webrtc/jsepicecandidate.h | 92 + talk/app/webrtc/jsepsessiondescription.cc | 193 + talk/app/webrtc/jsepsessiondescription.h | 106 + .../webrtc/jsepsessiondescription_unittest.cc | 223 + talk/app/webrtc/localaudiosource.cc | 127 + talk/app/webrtc/localaudiosource.h | 69 + talk/app/webrtc/localaudiosource_unittest.cc | 118 + talk/app/webrtc/localvideosource.cc | 442 ++ talk/app/webrtc/localvideosource.h | 100 + talk/app/webrtc/localvideosource_unittest.cc | 523 ++ talk/app/webrtc/mediaconstraintsinterface.cc | 78 + talk/app/webrtc/mediaconstraintsinterface.h | 129 + talk/app/webrtc/mediastream.cc | 112 + talk/app/webrtc/mediastream.h | 75 + talk/app/webrtc/mediastream_unittest.cc | 162 + talk/app/webrtc/mediastreamhandler.cc | 440 ++ talk/app/webrtc/mediastreamhandler.h | 264 + .../app/webrtc/mediastreamhandler_unittest.cc | 297 + talk/app/webrtc/mediastreaminterface.h | 196 + talk/app/webrtc/mediastreamprovider.h | 81 + talk/app/webrtc/mediastreamproxy.h | 54 + talk/app/webrtc/mediastreamsignaling.cc | 883 +++ talk/app/webrtc/mediastreamsignaling.h | 385 ++ .../webrtc/mediastreamsignaling_unittest.cc | 949 ++++ talk/app/webrtc/mediastreamtrack.h | 81 + talk/app/webrtc/mediastreamtrackproxy.h | 73 + talk/app/webrtc/notifier.h | 77 + talk/app/webrtc/objc/README | 45 + talk/app/webrtc/objc/RTCAudioTrack+Internal.h | 37 + talk/app/webrtc/objc/RTCAudioTrack.mm | 45 + talk/app/webrtc/objc/RTCEnumConverter.h | 54 + talk/app/webrtc/objc/RTCEnumConverter.mm | 126 + talk/app/webrtc/objc/RTCI420Frame.mm | 34 + .../webrtc/objc/RTCIceCandidate+Internal.h | 39 + talk/app/webrtc/objc/RTCIceCandidate.mm | 86 + talk/app/webrtc/objc/RTCIceServer+Internal.h | 37 + talk/app/webrtc/objc/RTCIceServer.mm | 65 + .../objc/RTCMediaConstraints+Internal.h | 40 + talk/app/webrtc/objc/RTCMediaConstraints.mm | 76 + .../webrtc/objc/RTCMediaConstraintsNative.cc | 51 + .../webrtc/objc/RTCMediaConstraintsNative.h | 50 + .../app/webrtc/objc/RTCMediaSource+Internal.h | 40 + talk/app/webrtc/objc/RTCMediaSource.mm | 65 + .../app/webrtc/objc/RTCMediaStream+Internal.h | 40 + talk/app/webrtc/objc/RTCMediaStream.mm | 145 + .../objc/RTCMediaStreamTrack+Internal.h | 40 + talk/app/webrtc/objc/RTCMediaStreamTrack.mm | 103 + talk/app/webrtc/objc/RTCPair.m | 40 + .../webrtc/objc/RTCPeerConnection+Internal.h | 44 + talk/app/webrtc/objc/RTCPeerConnection.mm | 247 + .../webrtc/objc/RTCPeerConnectionFactory.mm | 127 + .../webrtc/objc/RTCPeerConnectionObserver.h | 79 + .../webrtc/objc/RTCPeerConnectionObserver.mm | 103 + .../objc/RTCSessionDescription+Internal.h | 41 + talk/app/webrtc/objc/RTCSessionDescription.mm | 81 + .../webrtc/objc/RTCVideoCapturer+Internal.h | 38 + talk/app/webrtc/objc/RTCVideoCapturer.mm | 76 + .../webrtc/objc/RTCVideoRenderer+Internal.h | 40 + talk/app/webrtc/objc/RTCVideoRenderer.mm | 72 + .../app/webrtc/objc/RTCVideoSource+Internal.h | 37 + talk/app/webrtc/objc/RTCVideoSource.mm | 44 + talk/app/webrtc/objc/RTCVideoTrack+Internal.h | 40 + talk/app/webrtc/objc/RTCVideoTrack.mm | 77 + talk/app/webrtc/objc/public/RTCAudioSource.h | 40 + talk/app/webrtc/objc/public/RTCAudioTrack.h | 39 + talk/app/webrtc/objc/public/RTCI420Frame.h | 36 + talk/app/webrtc/objc/public/RTCIceCandidate.h | 56 + talk/app/webrtc/objc/public/RTCIceServer.h | 48 + .../webrtc/objc/public/RTCMediaConstraints.h | 39 + talk/app/webrtc/objc/public/RTCMediaSource.h | 44 + talk/app/webrtc/objc/public/RTCMediaStream.h | 51 + .../webrtc/objc/public/RTCMediaStreamTrack.h | 51 + talk/app/webrtc/objc/public/RTCPair.h | 45 + .../webrtc/objc/public/RTCPeerConnection.h | 110 + .../objc/public/RTCPeerConnectionDelegate.h | 70 + .../objc/public/RTCPeerConnectionFactory.h | 67 + .../objc/public/RTCSessionDescription.h | 50 + .../public/RTCSessionDescriptonDelegate.h | 49 + talk/app/webrtc/objc/public/RTCTypes.h | 72 + .../app/webrtc/objc/public/RTCVideoCapturer.h | 42 + .../app/webrtc/objc/public/RTCVideoRenderer.h | 52 + .../objc/public/RTCVideoRendererDelegate.h | 44 + talk/app/webrtc/objc/public/RTCVideoSource.h | 39 + talk/app/webrtc/objc/public/RTCVideoTrack.h | 50 + talk/app/webrtc/objctests/Info.plist | 38 + .../objctests/RTCPeerConnectionSyncObserver.h | 53 + .../objctests/RTCPeerConnectionSyncObserver.m | 190 + .../webrtc/objctests/RTCPeerConnectionTest.mm | 235 + .../RTCSessionDescriptionSyncObserver.h | 49 + .../RTCSessionDescriptionSyncObserver.m | 97 + talk/app/webrtc/objctests/mac/main.mm | 33 + talk/app/webrtc/peerconnection.cc | 755 +++ talk/app/webrtc/peerconnection.h | 192 + talk/app/webrtc/peerconnection_unittest.cc | 1374 +++++ talk/app/webrtc/peerconnectionfactory.cc | 369 ++ talk/app/webrtc/peerconnectionfactory.h | 127 + .../webrtc/peerconnectionfactory_unittest.cc | 270 + talk/app/webrtc/peerconnectioninterface.h | 451 ++ .../peerconnectioninterface_unittest.cc | 1220 ++++ talk/app/webrtc/peerconnectionproxy.h | 72 + talk/app/webrtc/portallocatorfactory.cc | 92 + talk/app/webrtc/portallocatorfactory.h | 70 + talk/app/webrtc/proxy.h | 287 + talk/app/webrtc/proxy_unittest.cc | 170 + talk/app/webrtc/statscollector.cc | 571 ++ talk/app/webrtc/statscollector.h | 95 + talk/app/webrtc/statscollector_unittest.cc | 442 ++ talk/app/webrtc/statstypes.h | 160 + talk/app/webrtc/streamcollection.h | 125 + .../app/webrtc/test/fakeaudiocapturemodule.cc | 716 +++ talk/app/webrtc/test/fakeaudiocapturemodule.h | 280 + .../test/fakeaudiocapturemodule_unittest.cc | 212 + talk/app/webrtc/test/fakeconstraints.h | 118 + .../webrtc/test/fakeperiodicvideocapturer.h | 89 + talk/app/webrtc/test/fakevideotrackrenderer.h | 70 + .../webrtc/test/mockpeerconnectionobservers.h | 172 + talk/app/webrtc/test/testsdpstrings.h | 144 + talk/app/webrtc/videosourceinterface.h | 57 + talk/app/webrtc/videosourceproxy.h | 51 + talk/app/webrtc/videotrack.cc | 78 + talk/app/webrtc/videotrack.h | 65 + talk/app/webrtc/videotrack_unittest.cc | 84 + talk/app/webrtc/videotrackrenderers.cc | 94 + talk/app/webrtc/videotrackrenderers.h | 77 + talk/app/webrtc/webrtc.scons | 88 + talk/app/webrtc/webrtcsdp.cc | 2885 ++++++++++ talk/app/webrtc/webrtcsdp.h | 81 + talk/app/webrtc/webrtcsdp_unittest.cc | 1961 +++++++ talk/app/webrtc/webrtcsession.cc | 1440 +++++ talk/app/webrtc/webrtcsession.h | 295 + talk/app/webrtc/webrtcsession_unittest.cc | 2473 +++++++++ talk/base/asyncfile.cc | 38 + talk/base/asyncfile.h | 57 + talk/base/asynchttprequest.cc | 133 + talk/base/asynchttprequest.h | 121 + talk/base/asynchttprequest_unittest.cc | 250 + talk/base/asyncpacketsocket.h | 108 + talk/base/asyncsocket.cc | 61 + talk/base/asyncsocket.h | 133 + talk/base/asynctcpsocket.cc | 313 ++ talk/base/asynctcpsocket.h | 114 + talk/base/asynctcpsocket_unittest.cc | 70 + talk/base/asyncudpsocket.cc | 136 + talk/base/asyncudpsocket.h | 78 + talk/base/asyncudpsocket_unittest.cc | 70 + talk/base/atomicops.h | 166 + talk/base/atomicops_unittest.cc | 96 + talk/base/autodetectproxy.cc | 290 + talk/base/autodetectproxy.h | 106 + talk/base/autodetectproxy_unittest.cc | 141 + talk/base/bandwidthsmoother.cc | 101 + talk/base/bandwidthsmoother.h | 76 + talk/base/bandwidthsmoother_unittest.cc | 133 + talk/base/base64.cc | 259 + talk/base/base64.h | 104 + talk/base/base64_unittest.cc | 1018 ++++ talk/base/basicdefs.h | 37 + talk/base/basictypes.h | 171 + talk/base/basictypes_unittest.cc | 92 + talk/base/bind.h | 397 ++ talk/base/bind.h.pump | 125 + talk/base/bind_unittest.cc | 74 + talk/base/buffer.h | 119 + talk/base/buffer_unittest.cc | 160 + talk/base/bytebuffer.cc | 250 + talk/base/bytebuffer.h | 136 + talk/base/bytebuffer_unittest.cc | 228 + talk/base/byteorder.h | 185 + talk/base/byteorder_unittest.cc | 100 + talk/base/checks.cc | 47 + talk/base/checks.h | 44 + talk/base/common.cc | 80 + talk/base/common.h | 188 + talk/base/constructormagic.h | 55 + talk/base/cpumonitor.cc | 423 ++ talk/base/cpumonitor.h | 140 + talk/base/cpumonitor_unittest.cc | 402 ++ talk/base/crc32.cc | 69 + talk/base/crc32.h | 51 + talk/base/crc32_unittest.cc | 52 + talk/base/criticalsection.h | 196 + talk/base/cryptstring.h | 198 + talk/base/dbus.cc | 409 ++ talk/base/dbus.h | 185 + talk/base/dbus_unittest.cc | 249 + talk/base/diskcache.cc | 364 ++ talk/base/diskcache.h | 142 + talk/base/diskcache_win32.cc | 103 + talk/base/diskcache_win32.h | 46 + talk/base/event.cc | 139 + talk/base/event.h | 68 + talk/base/event_unittest.cc | 59 + talk/base/fakecpumonitor.h | 49 + talk/base/fakenetwork.h | 136 + talk/base/fakesslidentity.h | 69 + talk/base/faketaskrunner.h | 55 + talk/base/filelock.cc | 79 + talk/base/filelock.h | 70 + talk/base/filelock_unittest.cc | 104 + talk/base/fileutils.cc | 297 + talk/base/fileutils.h | 457 ++ talk/base/fileutils_mock.h | 270 + talk/base/fileutils_unittest.cc | 148 + talk/base/firewallsocketserver.cc | 254 + talk/base/firewallsocketserver.h | 137 + talk/base/flags.cc | 315 ++ talk/base/flags.h | 284 + talk/base/gunit.h | 112 + talk/base/gunit_prod.h | 37 + talk/base/helpers.cc | 289 + talk/base/helpers.h | 73 + talk/base/helpers_unittest.cc | 83 + talk/base/host.cc | 49 + talk/base/host.h | 40 + talk/base/host_unittest.cc | 33 + talk/base/httpbase.cc | 893 +++ talk/base/httpbase.h | 201 + talk/base/httpbase_unittest.cc | 536 ++ talk/base/httpclient.cc | 847 +++ talk/base/httpclient.h | 219 + talk/base/httpcommon-inl.h | 148 + talk/base/httpcommon.cc | 1055 ++++ talk/base/httpcommon.h | 463 ++ talk/base/httpcommon_unittest.cc | 182 + talk/base/httprequest.cc | 127 + talk/base/httprequest.h | 132 + talk/base/httpserver.cc | 305 + talk/base/httpserver.h | 154 + talk/base/httpserver_unittest.cc | 130 + talk/base/ifaddrs-android.cc | 234 + talk/base/ifaddrs-android.h | 50 + talk/base/ipaddress.cc | 466 ++ talk/base/ipaddress.h | 158 + talk/base/ipaddress_unittest.cc | 888 +++ talk/base/json.cc | 313 ++ talk/base/json.h | 106 + talk/base/json_unittest.cc | 294 + talk/base/latebindingsymboltable.cc | 157 + talk/base/latebindingsymboltable.cc.def | 85 + talk/base/latebindingsymboltable.h | 83 + talk/base/latebindingsymboltable.h.def | 99 + talk/base/latebindingsymboltable_unittest.cc | 72 + talk/base/libdbusglibsymboltable.cc | 41 + talk/base/libdbusglibsymboltable.h | 73 + talk/base/linked_ptr.h | 142 + talk/base/linux.cc | 282 + talk/base/linux.h | 135 + talk/base/linux_unittest.cc | 113 + talk/base/linuxfdwalk.c | 98 + talk/base/linuxfdwalk.h | 51 + talk/base/linuxfdwalk_unittest.cc | 92 + talk/base/linuxwindowpicker.cc | 835 +++ talk/base/linuxwindowpicker.h | 68 + talk/base/linuxwindowpicker_unittest.cc | 54 + talk/base/logging.cc | 635 +++ talk/base/logging.h | 389 ++ talk/base/logging_unittest.cc | 149 + talk/base/macasyncsocket.cc | 472 ++ talk/base/macasyncsocket.h | 91 + talk/base/maccocoasocketserver.h | 63 + talk/base/maccocoasocketserver.mm | 134 + talk/base/maccocoasocketserver_unittest.mm | 64 + talk/base/maccocoathreadhelper.h | 44 + talk/base/maccocoathreadhelper.mm | 61 + talk/base/macconversion.cc | 176 + talk/base/macconversion.h | 56 + talk/base/macsocketserver.cc | 369 ++ talk/base/macsocketserver.h | 130 + talk/base/macsocketserver_unittest.cc | 250 + talk/base/macutils.cc | 231 + talk/base/macutils.h | 75 + talk/base/macutils_unittest.cc | 58 + talk/base/macwindowpicker.cc | 250 + talk/base/macwindowpicker.h | 31 + talk/base/macwindowpicker_unittest.cc | 39 + talk/base/mathutils.h | 37 + talk/base/md5.cc | 218 + talk/base/md5.h | 40 + talk/base/md5digest.h | 63 + talk/base/md5digest_unittest.cc | 96 + talk/base/messagedigest.cc | 184 + talk/base/messagedigest.h | 123 + talk/base/messagedigest_unittest.cc | 168 + talk/base/messagehandler.cc | 37 + talk/base/messagehandler.h | 53 + talk/base/messagequeue.cc | 388 ++ talk/base/messagequeue.h | 264 + talk/base/messagequeue_unittest.cc | 132 + talk/base/multipart.cc | 268 + talk/base/multipart.h | 94 + talk/base/multipart_unittest.cc | 142 + talk/base/nat_unittest.cc | 359 ++ talk/base/natserver.cc | 190 + talk/base/natserver.h | 121 + talk/base/natserver_main.cc | 57 + talk/base/natsocketfactory.cc | 505 ++ talk/base/natsocketfactory.h | 183 + talk/base/nattypes.cc | 72 + talk/base/nattypes.h | 64 + talk/base/nethelpers.cc | 142 + talk/base/nethelpers.h | 77 + talk/base/network.cc | 542 ++ talk/base/network.h | 227 + talk/base/network_unittest.cc | 512 ++ talk/base/nssidentity.cc | 428 ++ talk/base/nssidentity.h | 128 + talk/base/nssstreamadapter.cc | 1007 ++++ talk/base/nssstreamadapter.h | 130 + talk/base/nullsocketserver.h | 78 + talk/base/nullsocketserver_unittest.cc | 64 + talk/base/openssladapter.cc | 908 +++ talk/base/openssladapter.h | 105 + talk/base/openssldigest.cc | 114 + talk/base/openssldigest.h | 64 + talk/base/opensslidentity.cc | 342 ++ talk/base/opensslidentity.h | 151 + talk/base/opensslstreamadapter.cc | 940 ++++ talk/base/opensslstreamadapter.h | 215 + talk/base/optionsfile.cc | 201 + talk/base/optionsfile.h | 66 + talk/base/optionsfile_unittest.cc | 178 + talk/base/pathutils.cc | 268 + talk/base/pathutils.h | 180 + talk/base/pathutils_unittest.cc | 65 + talk/base/physicalsocketserver.cc | 1673 ++++++ talk/base/physicalsocketserver.h | 139 + talk/base/physicalsocketserver_unittest.cc | 299 + talk/base/posix.cc | 148 + talk/base/posix.h | 42 + talk/base/profiler.cc | 171 + talk/base/profiler.h | 169 + talk/base/profiler_unittest.cc | 126 + talk/base/proxy_unittest.cc | 152 + talk/base/proxydetect.cc | 1263 +++++ talk/base/proxydetect.h | 48 + talk/base/proxydetect_unittest.cc | 182 + talk/base/proxyinfo.cc | 37 + talk/base/proxyinfo.h | 59 + talk/base/proxyserver.cc | 161 + talk/base/proxyserver.h | 113 + talk/base/ratelimiter.cc | 46 + talk/base/ratelimiter.h | 80 + talk/base/ratelimiter_unittest.cc | 76 + talk/base/ratetracker.cc | 80 + talk/base/ratetracker.h | 59 + talk/base/ratetracker_unittest.cc | 91 + talk/base/refcount.h | 95 + talk/base/referencecountedsingletonfactory.h | 174 + ...ferencecountedsingletonfactory_unittest.cc | 149 + talk/base/rollingaccumulator.h | 137 + talk/base/rollingaccumulator_unittest.cc | 102 + talk/base/schanneladapter.cc | 719 +++ talk/base/schanneladapter.h | 94 + talk/base/scoped_autorelease_pool.h | 76 + talk/base/scoped_autorelease_pool.mm | 42 + talk/base/scoped_ptr.h | 277 + talk/base/scoped_ref_ptr.h | 162 + talk/base/sec_buffer.h | 173 + talk/base/sha1.cc | 282 + talk/base/sha1.h | 28 + talk/base/sha1digest.h | 64 + talk/base/sha1digest_unittest.cc | 99 + talk/base/sharedexclusivelock.cc | 61 + talk/base/sharedexclusivelock.h | 93 + talk/base/sharedexclusivelock_unittest.cc | 234 + talk/base/signalthread.cc | 166 + talk/base/signalthread.h | 172 + talk/base/signalthread_unittest.cc | 209 + talk/base/sigslot.h | 2850 ++++++++++ talk/base/sigslot_unittest.cc | 267 + talk/base/sigslotrepeater.h | 111 + talk/base/socket.h | 201 + talk/base/socket_unittest.cc | 1018 ++++ talk/base/socket_unittest.h | 105 + talk/base/socketadapters.cc | 910 +++ talk/base/socketadapters.h | 261 + talk/base/socketaddress.cc | 398 ++ talk/base/socketaddress.h | 231 + talk/base/socketaddress_unittest.cc | 352 ++ talk/base/socketaddresspair.cc | 58 + talk/base/socketaddresspair.h | 58 + talk/base/socketfactory.h | 55 + talk/base/socketpool.cc | 297 + talk/base/socketpool.h | 160 + talk/base/socketserver.h | 61 + talk/base/socketstream.cc | 138 + talk/base/socketstream.h | 74 + talk/base/ssladapter.cc | 113 + talk/base/ssladapter.h | 76 + talk/base/sslconfig.h | 50 + talk/base/sslfingerprint.h | 109 + talk/base/sslidentity.cc | 104 + talk/base/sslidentity.h | 100 + talk/base/sslidentity_unittest.cc | 190 + talk/base/sslroots.h | 4930 +++++++++++++++++ talk/base/sslsocketfactory.cc | 192 + talk/base/sslsocketfactory.h | 98 + talk/base/sslstreamadapter.cc | 94 + talk/base/sslstreamadapter.h | 185 + talk/base/sslstreamadapter_unittest.cc | 886 +++ talk/base/sslstreamadapterhelper.cc | 147 + talk/base/sslstreamadapterhelper.h | 137 + talk/base/stream.cc | 1252 +++++ talk/base/stream.h | 807 +++ talk/base/stream_unittest.cc | 509 ++ talk/base/stringdigest.h | 34 + talk/base/stringencode.cc | 674 +++ talk/base/stringencode.h | 227 + talk/base/stringencode_unittest.cc | 402 ++ talk/base/stringutils.cc | 150 + talk/base/stringutils.h | 335 ++ talk/base/stringutils_unittest.cc | 126 + talk/base/systeminfo.cc | 533 ++ talk/base/systeminfo.h | 98 + talk/base/systeminfo_unittest.cc | 211 + talk/base/task.cc | 289 + talk/base/task.h | 194 + talk/base/task_unittest.cc | 562 ++ talk/base/taskparent.cc | 112 + talk/base/taskparent.h | 79 + talk/base/taskrunner.cc | 241 + talk/base/taskrunner.h | 117 + talk/base/testbase64.h | 5 + talk/base/testclient.cc | 155 + talk/base/testclient.h | 109 + talk/base/testclient_unittest.cc | 95 + talk/base/testechoserver.h | 88 + talk/base/testutils.h | 570 ++ talk/base/thread.cc | 582 ++ talk/base/thread.h | 323 ++ talk/base/thread_unittest.cc | 329 ++ talk/base/timeutils.cc | 201 + talk/base/timeutils.h | 98 + talk/base/timeutils_unittest.cc | 163 + talk/base/timing.cc | 129 + talk/base/timing.h | 76 + talk/base/transformadapter.cc | 202 + talk/base/transformadapter.h | 97 + talk/base/unittest_main.cc | 118 + talk/base/unixfilesystem.cc | 546 ++ talk/base/unixfilesystem.h | 139 + talk/base/urlencode.cc | 196 + talk/base/urlencode.h | 60 + talk/base/urlencode_unittest.cc | 98 + talk/base/versionparsing.cc | 74 + talk/base/versionparsing.h | 52 + talk/base/versionparsing_unittest.cc | 91 + talk/base/virtualsocket_unittest.cc | 1016 ++++ talk/base/virtualsocketserver.cc | 1117 ++++ talk/base/virtualsocketserver.h | 250 + talk/base/win32.cc | 473 ++ talk/base/win32.h | 146 + talk/base/win32_unittest.cc | 79 + talk/base/win32filesystem.cc | 477 ++ talk/base/win32filesystem.h | 118 + talk/base/win32regkey.cc | 1119 ++++ talk/base/win32regkey.h | 354 ++ talk/base/win32regkey_unittest.cc | 607 ++ talk/base/win32securityerrors.cc | 66 + talk/base/win32socketinit.cc | 63 + talk/base/win32socketinit.h | 37 + talk/base/win32socketserver.cc | 864 +++ talk/base/win32socketserver.h | 180 + talk/base/win32socketserver_unittest.cc | 151 + talk/base/win32toolhelp.h | 166 + talk/base/win32toolhelp_unittest.cc | 296 + talk/base/win32window.cc | 138 + talk/base/win32window.h | 77 + talk/base/win32window_unittest.cc | 83 + talk/base/win32windowpicker.cc | 137 + talk/base/win32windowpicker.h | 33 + talk/base/win32windowpicker_unittest.cc | 93 + talk/base/window.h | 141 + talk/base/windowpicker.h | 78 + talk/base/windowpicker_unittest.cc | 55 + talk/base/windowpickerfactory.h | 76 + talk/base/winfirewall.cc | 172 + talk/base/winfirewall.h | 73 + talk/base/winfirewall_unittest.cc | 57 + talk/base/winping.cc | 376 ++ talk/base/winping.h | 120 + talk/base/worker.cc | 92 + talk/base/worker.h | 89 + talk/build/build_jar.sh | 52 + talk/build/common.gypi | 117 + talk/examples/android/AndroidManifest.xml | 40 + talk/examples/android/README | 43 + talk/examples/android/ant.properties | 17 + talk/examples/android/assets/channel.html | 54 + talk/examples/android/build.xml | 92 + talk/examples/android/jni/Android.mk | 2 + talk/examples/android/project.properties | 14 + .../android/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 7503 bytes .../android/res/drawable-ldpi/ic_launcher.png | Bin 0 -> 2502 bytes .../android/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 3835 bytes .../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 12056 bytes talk/examples/android/res/values/strings.xml | 4 + .../src/org/appspot/apprtc/AppRTCClient.java | 432 ++ .../appspot/apprtc/AppRTCDemoActivity.java | 499 ++ .../src/org/appspot/apprtc/FramePool.java | 104 + .../org/appspot/apprtc/GAEChannelClient.java | 164 + .../org/appspot/apprtc/VideoStreamsView.java | 295 + talk/examples/call/Info.plist | 11 + talk/examples/call/call_main.cc | 499 ++ talk/examples/call/call_unittest.cc | 37 + talk/examples/call/callclient.cc | 1615 ++++++ talk/examples/call/callclient.h | 352 ++ talk/examples/call/callclient_unittest.cc | 47 + talk/examples/call/console.cc | 165 + talk/examples/call/console.h | 69 + talk/examples/call/friendinvitesendtask.cc | 76 + talk/examples/call/friendinvitesendtask.h | 49 + talk/examples/call/mediaenginefactory.cc | 81 + talk/examples/call/mediaenginefactory.h | 40 + talk/examples/call/muc.h | 66 + talk/examples/call/mucinviterecvtask.cc | 124 + talk/examples/call/mucinviterecvtask.h | 82 + talk/examples/call/mucinvitesendtask.cc | 63 + talk/examples/call/mucinvitesendtask.h | 50 + talk/examples/call/presencepushtask.cc | 222 + talk/examples/call/presencepushtask.h | 70 + talk/examples/chat/Info.plist | 11 + talk/examples/chat/chat_main.cc | 159 + talk/examples/chat/chatapp.cc | 251 + talk/examples/chat/chatapp.h | 171 + talk/examples/chat/consoletask.cc | 177 + talk/examples/chat/consoletask.h | 92 + talk/examples/chat/textchatreceivetask.cc | 66 + talk/examples/chat/textchatreceivetask.h | 63 + talk/examples/chat/textchatsendtask.cc | 81 + talk/examples/chat/textchatsendtask.h | 56 + .../ios/AppRTCDemo.xcodeproj/project.pbxproj | 570 ++ .../examples/ios/AppRTCDemo/APPRTCAppClient.h | 54 + .../examples/ios/AppRTCDemo/APPRTCAppClient.m | 333 ++ .../ios/AppRTCDemo/APPRTCAppDelegate.h | 53 + .../ios/AppRTCDemo/APPRTCAppDelegate.m | 370 ++ .../ios/AppRTCDemo/APPRTCViewController.h | 40 + .../ios/AppRTCDemo/APPRTCViewController.m | 83 + .../ios/AppRTCDemo/AppRTCDemo-Info.plist | 71 + .../ios/AppRTCDemo/AppRTCDemo-Prefix.pch | 40 + talk/examples/ios/AppRTCDemo/Default.png | Bin 0 -> 6540 bytes .../ios/AppRTCDemo/GAEChannelClient.h | 49 + .../ios/AppRTCDemo/GAEChannelClient.m | 104 + .../en.lproj/APPRTCViewController.xib | 529 ++ talk/examples/ios/AppRTCDemo/ios_channel.html | 88 + talk/examples/ios/AppRTCDemo/main.m | 37 + talk/examples/ios/Icon.png | Bin 0 -> 62469 bytes talk/examples/ios/README | 28 + talk/examples/ios/makeLibs.sh | 30 + talk/examples/login/login_main.cc | 66 + talk/examples/pcp/pcp_main.cc | 715 +++ .../peerconnection/client/conductor.cc | 494 ++ .../peerconnection/client/conductor.h | 144 + .../peerconnection/client/defaults.cc | 75 + .../examples/peerconnection/client/defaults.h | 47 + .../examples/peerconnection/client/flagdefs.h | 50 + .../peerconnection/client/linux/main.cc | 117 + .../peerconnection/client/linux/main_wnd.cc | 520 ++ .../peerconnection/client/linux/main_wnd.h | 138 + talk/examples/peerconnection/client/main.cc | 72 + .../peerconnection/client/main_wnd.cc | 603 ++ .../examples/peerconnection/client/main_wnd.h | 213 + .../client/peer_connection_client.cc | 531 ++ .../client/peer_connection_client.h | 140 + .../peerconnection/peerconnection.scons | 64 + .../peerconnection/server/data_socket.cc | 306 + .../peerconnection/server/data_socket.h | 168 + talk/examples/peerconnection/server/main.cc | 190 + .../peerconnection/server/peer_channel.cc | 369 ++ .../peerconnection/server/peer_channel.h | 137 + .../peerconnection/server/server_test.html | 253 + talk/examples/peerconnection/server/utils.cc | 47 + talk/examples/peerconnection/server/utils.h | 53 + talk/examples/plus/libjingleplus.cc | 736 +++ talk/examples/plus/libjingleplus.h | 154 + talk/examples/plus/presencepushtask.cc | 201 + talk/examples/plus/presencepushtask.h | 53 + talk/examples/plus/rostertask.cc | 218 + talk/examples/plus/rostertask.h | 72 + .../plus/testutil/libjingleplus_main.cc | 119 + .../testutil/libjingleplus_test_notifier.h | 101 + .../plus/testutil/libjingleplus_unittest.cc | 52 + talk/libjingle.gyp | 1174 ++++ talk/libjingle.scons | 788 +++ talk/libjingle_all.gyp | 40 + talk/libjingle_examples.gyp | 274 + talk/libjingle_tests.gyp | 529 ++ talk/main.scons | 889 +++ talk/media/base/audioframe.h | 63 + talk/media/base/audiorenderer.h | 45 + talk/media/base/capturemanager.cc | 389 ++ talk/media/base/capturemanager.h | 114 + talk/media/base/capturemanager_unittest.cc | 251 + talk/media/base/capturerenderadapter.cc | 137 + talk/media/base/capturerenderadapter.h | 91 + talk/media/base/codec.cc | 169 + talk/media/base/codec.h | 301 + talk/media/base/codec_unittest.cc | 294 + talk/media/base/constants.cc | 95 + talk/media/base/constants.h | 118 + talk/media/base/cpuid.cc | 88 + talk/media/base/cpuid.h | 76 + talk/media/base/cpuid_unittest.cc | 74 + talk/media/base/cryptoparams.h | 54 + talk/media/base/fakecapturemanager.h | 52 + talk/media/base/fakemediaengine.h | 971 ++++ talk/media/base/fakemediaprocessor.h | 79 + talk/media/base/fakenetworkinterface.h | 249 + talk/media/base/fakertp.h | 104 + talk/media/base/fakevideocapturer.h | 154 + talk/media/base/fakevideorenderer.h | 142 + talk/media/base/filemediaengine.cc | 342 ++ talk/media/base/filemediaengine.h | 316 ++ talk/media/base/filemediaengine_unittest.cc | 463 ++ talk/media/base/hybriddataengine.h | 76 + talk/media/base/hybridvideoengine.cc | 350 ++ talk/media/base/hybridvideoengine.h | 275 + talk/media/base/mediachannel.h | 933 ++++ talk/media/base/mediacommon.h | 44 + talk/media/base/mediaengine.cc | 74 + talk/media/base/mediaengine.h | 400 ++ talk/media/base/mutedvideocapturer.cc | 135 + talk/media/base/mutedvideocapturer.h | 60 + .../media/base/mutedvideocapturer_unittest.cc | 96 + talk/media/base/nullvideoframe.h | 93 + talk/media/base/nullvideorenderer.h | 48 + talk/media/base/rtpdataengine.cc | 381 ++ talk/media/base/rtpdataengine.h | 142 + talk/media/base/rtpdataengine_unittest.cc | 463 ++ talk/media/base/rtpdump.cc | 424 ++ talk/media/base/rtpdump.h | 232 + talk/media/base/rtpdump_unittest.cc | 297 + talk/media/base/rtputils.cc | 226 + talk/media/base/rtputils.h | 79 + talk/media/base/rtputils_unittest.cc | 194 + talk/media/base/screencastid.h | 88 + talk/media/base/streamparams.cc | 182 + talk/media/base/streamparams.h | 209 + talk/media/base/streamparams_unittest.cc | 165 + talk/media/base/testutils.cc | 338 ++ talk/media/base/testutils.h | 242 + talk/media/base/videoadapter.cc | 588 ++ talk/media/base/videoadapter.h | 213 + talk/media/base/videocapturer.cc | 580 ++ talk/media/base/videocapturer.h | 327 ++ talk/media/base/videocapturer_unittest.cc | 683 +++ talk/media/base/videocommon.cc | 237 + talk/media/base/videocommon.h | 242 + talk/media/base/videocommon_unittest.cc | 290 + talk/media/base/videoengine_unittest.h | 1644 ++++++ talk/media/base/videoframe.cc | 385 ++ talk/media/base/videoframe.h | 188 + talk/media/base/videoframe_unittest.h | 2102 +++++++ talk/media/base/videoprocessor.h | 50 + talk/media/base/videorenderer.h | 58 + talk/media/base/voiceprocessor.h | 56 + talk/media/devices/carbonvideorenderer.cc | 182 + talk/media/devices/carbonvideorenderer.h | 72 + talk/media/devices/deviceinfo.h | 42 + talk/media/devices/devicemanager.cc | 389 ++ talk/media/devices/devicemanager.h | 214 + talk/media/devices/devicemanager_unittest.cc | 452 ++ talk/media/devices/dummydevicemanager.cc | 38 + talk/media/devices/dummydevicemanager.h | 51 + .../devices/dummydevicemanager_unittest.cc | 104 + talk/media/devices/fakedevicemanager.h | 219 + talk/media/devices/filevideocapturer.cc | 366 ++ talk/media/devices/filevideocapturer.h | 156 + .../devices/filevideocapturer_unittest.cc | 203 + talk/media/devices/gdivideorenderer.cc | 266 + talk/media/devices/gdivideorenderer.h | 60 + talk/media/devices/gtkvideorenderer.cc | 156 + talk/media/devices/gtkvideorenderer.h | 69 + talk/media/devices/iosdeviceinfo.cc | 40 + talk/media/devices/libudevsymboltable.cc | 40 + talk/media/devices/libudevsymboltable.h | 71 + talk/media/devices/linuxdeviceinfo.cc | 173 + talk/media/devices/linuxdevicemanager.cc | 406 ++ talk/media/devices/linuxdevicemanager.h | 55 + talk/media/devices/macdeviceinfo.cc | 56 + talk/media/devices/macdevicemanager.cc | 197 + talk/media/devices/macdevicemanager.h | 56 + talk/media/devices/macdevicemanagermm.mm | 140 + talk/media/devices/mobiledevicemanager.cc | 76 + talk/media/devices/v4llookup.cc | 67 + talk/media/devices/v4llookup.h | 44 + talk/media/devices/videorendererfactory.h | 66 + talk/media/devices/win32deviceinfo.cc | 62 + talk/media/devices/win32devicemanager.cc | 404 ++ talk/media/devices/win32devicemanager.h | 60 + talk/media/other/linphonemediaengine.cc | 276 + talk/media/other/linphonemediaengine.h | 173 + talk/media/sctp/sctpdataengine.cc | 684 +++ talk/media/sctp/sctpdataengine.h | 220 + talk/media/sctp/sctpdataengine_unittest.cc | 260 + talk/media/testdata/1.frame_plus_1.byte | Bin 0 -> 153641 bytes .../testdata/captured-320x240-2s-48.frames | Bin 0 -> 7374720 bytes .../testdata/h264-svc-99-640x360.rtpdump | Bin 0 -> 1058252 bytes talk/media/testdata/video.rtpdump | Bin 0 -> 134998 bytes talk/media/testdata/voice.rtpdump | Bin 0 -> 78339 bytes talk/media/webrtc/fakewebrtccommon.h | 66 + talk/media/webrtc/fakewebrtcdeviceinfo.h | 123 + talk/media/webrtc/fakewebrtcvcmfactory.h | 63 + .../webrtc/fakewebrtcvideocapturemodule.h | 159 + talk/media/webrtc/fakewebrtcvideoengine.h | 1100 ++++ talk/media/webrtc/fakewebrtcvoiceengine.h | 1010 ++++ talk/media/webrtc/webrtccommon.h | 76 + talk/media/webrtc/webrtcexport.h | 79 + talk/media/webrtc/webrtcmediaengine.h | 203 + talk/media/webrtc/webrtcpassthroughrender.cc | 176 + talk/media/webrtc/webrtcpassthroughrender.h | 211 + .../webrtcpassthroughrender_unittest.cc | 147 + talk/media/webrtc/webrtcvideocapturer.cc | 366 ++ talk/media/webrtc/webrtcvideocapturer.h | 103 + .../webrtc/webrtcvideocapturer_unittest.cc | 145 + talk/media/webrtc/webrtcvideodecoderfactory.h | 53 + talk/media/webrtc/webrtcvideoencoderfactory.h | 89 + talk/media/webrtc/webrtcvideoengine.cc | 3626 ++++++++++++ talk/media/webrtc/webrtcvideoengine.h | 456 ++ .../webrtc/webrtcvideoengine_unittest.cc | 1882 +++++++ talk/media/webrtc/webrtcvideoframe.cc | 355 ++ talk/media/webrtc/webrtcvideoframe.h | 149 + .../media/webrtc/webrtcvideoframe_unittest.cc | 313 ++ talk/media/webrtc/webrtcvie.h | 151 + talk/media/webrtc/webrtcvoe.h | 179 + talk/media/webrtc/webrtcvoiceengine.cc | 2777 ++++++++++ talk/media/webrtc/webrtcvoiceengine.h | 428 ++ .../webrtc/webrtcvoiceengine_unittest.cc | 2584 +++++++++ talk/p2p/base/asyncstuntcpsocket.cc | 168 + talk/p2p/base/asyncstuntcpsocket.h | 66 + talk/p2p/base/asyncstuntcpsocket_unittest.cc | 277 + talk/p2p/base/basicpacketsocketfactory.cc | 187 + talk/p2p/base/basicpacketsocketfactory.h | 66 + talk/p2p/base/candidate.h | 203 + talk/p2p/base/common.h | 37 + talk/p2p/base/constants.cc | 263 + talk/p2p/base/constants.h | 264 + talk/p2p/base/dtlstransport.h | 144 + talk/p2p/base/dtlstransportchannel.cc | 563 ++ talk/p2p/base/dtlstransportchannel.h | 249 + .../p2p/base/dtlstransportchannel_unittest.cc | 564 ++ talk/p2p/base/fakesession.h | 445 ++ talk/p2p/base/p2ptransport.cc | 263 + talk/p2p/base/p2ptransport.h | 103 + talk/p2p/base/p2ptransportchannel.cc | 1222 ++++ talk/p2p/base/p2ptransportchannel.h | 196 + talk/p2p/base/p2ptransportchannel_unittest.cc | 1461 +++++ talk/p2p/base/packetsocketfactory.h | 65 + talk/p2p/base/parsing.cc | 158 + talk/p2p/base/parsing.h | 157 + talk/p2p/base/port.cc | 1400 +++++ talk/p2p/base/port.h | 585 ++ talk/p2p/base/port_unittest.cc | 2261 ++++++++ talk/p2p/base/portallocator.cc | 108 + talk/p2p/base/portallocator.h | 184 + talk/p2p/base/portallocatorsessionproxy.cc | 239 + talk/p2p/base/portallocatorsessionproxy.h | 123 + .../portallocatorsessionproxy_unittest.cc | 160 + talk/p2p/base/portinterface.h | 143 + talk/p2p/base/portproxy.cc | 180 + talk/p2p/base/portproxy.h | 102 + talk/p2p/base/pseudotcp.cc | 1296 +++++ talk/p2p/base/pseudotcp.h | 258 + talk/p2p/base/pseudotcp_unittest.cc | 857 +++ talk/p2p/base/rawtransport.cc | 132 + talk/p2p/base/rawtransport.h | 81 + talk/p2p/base/rawtransportchannel.cc | 275 + talk/p2p/base/rawtransportchannel.h | 143 + talk/p2p/base/relayport.cc | 820 +++ talk/p2p/base/relayport.h | 115 + talk/p2p/base/relayport_unittest.cc | 292 + talk/p2p/base/relayserver.cc | 756 +++ talk/p2p/base/relayserver.h | 249 + talk/p2p/base/relayserver_main.cc | 80 + talk/p2p/base/relayserver_unittest.cc | 539 ++ talk/p2p/base/session.cc | 1659 ++++++ talk/p2p/base/session.h | 723 +++ talk/p2p/base/session_unittest.cc | 2464 ++++++++ talk/p2p/base/sessionclient.h | 95 + talk/p2p/base/sessiondescription.cc | 239 + talk/p2p/base/sessiondescription.h | 202 + talk/p2p/base/sessionid.h | 37 + talk/p2p/base/sessionmanager.cc | 313 ++ talk/p2p/base/sessionmanager.h | 207 + talk/p2p/base/sessionmessages.cc | 1147 ++++ talk/p2p/base/sessionmessages.h | 243 + talk/p2p/base/stun.cc | 928 ++++ talk/p2p/base/stun.h | 648 +++ talk/p2p/base/stun_unittest.cc | 1468 +++++ talk/p2p/base/stunport.cc | 353 ++ talk/p2p/base/stunport.h | 208 + talk/p2p/base/stunport_unittest.cc | 166 + talk/p2p/base/stunrequest.cc | 210 + talk/p2p/base/stunrequest.h | 133 + talk/p2p/base/stunrequest_unittest.cc | 222 + talk/p2p/base/stunserver.cc | 109 + talk/p2p/base/stunserver.h | 77 + talk/p2p/base/stunserver_main.cc | 69 + talk/p2p/base/stunserver_unittest.cc | 120 + talk/p2p/base/tcpport.cc | 307 + talk/p2p/base/tcpport.h | 148 + talk/p2p/base/testrelayserver.h | 118 + talk/p2p/base/teststunserver.h | 55 + talk/p2p/base/testturnserver.h | 78 + talk/p2p/base/transport.cc | 838 +++ talk/p2p/base/transport.h | 481 ++ talk/p2p/base/transport_unittest.cc | 309 ++ talk/p2p/base/transportchannel.cc | 60 + talk/p2p/base/transportchannel.h | 164 + talk/p2p/base/transportchannelimpl.h | 120 + talk/p2p/base/transportchannelproxy.cc | 223 + talk/p2p/base/transportchannelproxy.h | 106 + talk/p2p/base/transportdescription.h | 150 + talk/p2p/base/transportdescriptionfactory.cc | 161 + talk/p2p/base/transportdescriptionfactory.h | 84 + .../transportdescriptionfactory_unittest.cc | 388 ++ talk/p2p/base/transportinfo.h | 60 + talk/p2p/base/turnport.cc | 953 ++++ talk/p2p/base/turnport.h | 179 + talk/p2p/base/turnport_unittest.cc | 331 ++ talk/p2p/base/turnserver.cc | 1006 ++++ talk/p2p/base/turnserver.h | 186 + talk/p2p/base/turnserver_main.cc | 102 + talk/p2p/base/udpport.h | 34 + talk/p2p/client/autoportallocator.h | 69 + talk/p2p/client/basicportallocator.cc | 1072 ++++ talk/p2p/client/basicportallocator.h | 249 + talk/p2p/client/connectivitychecker.cc | 516 ++ talk/p2p/client/connectivitychecker.h | 274 + .../client/connectivitychecker_unittest.cc | 353 ++ talk/p2p/client/fakeportallocator.h | 107 + talk/p2p/client/httpportallocator.cc | 334 ++ talk/p2p/client/httpportallocator.h | 192 + talk/p2p/client/portallocator_unittest.cc | 822 +++ talk/p2p/client/sessionmanagertask.h | 93 + talk/p2p/client/sessionsendtask.h | 145 + talk/p2p/client/socketmonitor.cc | 114 + talk/p2p/client/socketmonitor.h | 71 + talk/session/media/audiomonitor.cc | 121 + talk/session/media/audiomonitor.h | 75 + talk/session/media/call.cc | 1027 ++++ talk/session/media/call.h | 275 + talk/session/media/channel.cc | 2704 +++++++++ talk/session/media/channel.h | 688 +++ talk/session/media/channel_unittest.cc | 2895 ++++++++++ talk/session/media/channelmanager.cc | 931 ++++ talk/session/media/channelmanager.h | 310 ++ talk/session/media/channelmanager_unittest.cc | 610 ++ talk/session/media/currentspeakermonitor.cc | 208 + talk/session/media/currentspeakermonitor.h | 100 + .../media/currentspeakermonitor_unittest.cc | 232 + talk/session/media/mediamessages.cc | 394 ++ talk/session/media/mediamessages.h | 169 + talk/session/media/mediamessages_unittest.cc | 352 ++ talk/session/media/mediamonitor.cc | 108 + talk/session/media/mediamonitor.h | 98 + talk/session/media/mediarecorder.cc | 224 + talk/session/media/mediarecorder.h | 119 + talk/session/media/mediarecorder_unittest.cc | 358 ++ talk/session/media/mediasession.cc | 1657 ++++++ talk/session/media/mediasession.h | 497 ++ talk/session/media/mediasession_unittest.cc | 1905 +++++++ talk/session/media/mediasessionclient.cc | 1085 ++++ talk/session/media/mediasessionclient.h | 174 + .../media/mediasessionclient_unittest.cc | 3310 +++++++++++ talk/session/media/mediasink.h | 48 + talk/session/media/rtcpmuxfilter.cc | 132 + talk/session/media/rtcpmuxfilter.h | 86 + talk/session/media/rtcpmuxfilter_unittest.cc | 212 + talk/session/media/soundclip.cc | 82 + talk/session/media/soundclip.h | 70 + talk/session/media/srtpfilter.cc | 805 +++ talk/session/media/srtpfilter.h | 304 + talk/session/media/srtpfilter_unittest.cc | 863 +++ talk/session/media/ssrcmuxfilter.cc | 93 + talk/session/media/ssrcmuxfilter.h | 67 + talk/session/media/ssrcmuxfilter_unittest.cc | 184 + talk/session/media/typewrapping.h.pump | 297 + talk/session/media/typingmonitor.cc | 123 + talk/session/media/typingmonitor.h | 84 + talk/session/media/typingmonitor_unittest.cc | 92 + talk/session/media/voicechannel.h | 33 + talk/session/tunnel/pseudotcpchannel.cc | 600 ++ talk/session/tunnel/pseudotcpchannel.h | 140 + .../tunnel/securetunnelsessionclient.cc | 387 ++ .../tunnel/securetunnelsessionclient.h | 165 + talk/session/tunnel/tunnelsessionclient.cc | 432 ++ talk/session/tunnel/tunnelsessionclient.h | 182 + .../tunnel/tunnelsessionclient_unittest.cc | 226 + talk/site_scons/site_tools/talk_linux.py | 313 ++ talk/site_scons/site_tools/talk_noops.py | 20 + talk/site_scons/talk.py | 635 +++ talk/sound/alsasoundsystem.cc | 761 +++ talk/sound/alsasoundsystem.h | 120 + talk/sound/alsasymboltable.cc | 37 + talk/sound/alsasymboltable.h | 66 + talk/sound/automaticallychosensoundsystem.h | 105 + ...automaticallychosensoundsystem_unittest.cc | 214 + talk/sound/linuxsoundsystem.cc | 42 + talk/sound/linuxsoundsystem.h | 58 + talk/sound/nullsoundsystem.cc | 174 + talk/sound/nullsoundsystem.h | 70 + talk/sound/nullsoundsystemfactory.cc | 49 + talk/sound/nullsoundsystemfactory.h | 50 + talk/sound/platformsoundsystem.cc | 48 + talk/sound/platformsoundsystem.h | 40 + talk/sound/platformsoundsystemfactory.cc | 57 + talk/sound/platformsoundsystemfactory.h | 52 + talk/sound/pulseaudiosoundsystem.cc | 1559 ++++++ talk/sound/pulseaudiosoundsystem.h | 194 + talk/sound/pulseaudiosymboltable.cc | 41 + talk/sound/pulseaudiosymboltable.h | 104 + talk/sound/sounddevicelocator.h | 71 + talk/sound/soundinputstreaminterface.h | 85 + talk/sound/soundoutputstreaminterface.h | 89 + talk/sound/soundsystemfactory.h | 44 + talk/sound/soundsysteminterface.cc | 46 + talk/sound/soundsysteminterface.h | 129 + talk/sound/soundsystemproxy.cc | 64 + talk/sound/soundsystemproxy.h | 64 + talk/third_party/libudev/libudev.h | 175 + talk/xmllite/qname.cc | 95 + talk/xmllite/qname.h | 100 + talk/xmllite/qname_unittest.cc | 131 + talk/xmllite/xmlbuilder.cc | 147 + talk/xmllite/xmlbuilder.h | 78 + talk/xmllite/xmlbuilder_unittest.cc | 194 + talk/xmllite/xmlconstants.cc | 42 + talk/xmllite/xmlconstants.h | 47 + talk/xmllite/xmlelement.cc | 513 ++ talk/xmllite/xmlelement.h | 251 + talk/xmllite/xmlelement_unittest.cc | 271 + talk/xmllite/xmlnsstack.cc | 195 + talk/xmllite/xmlnsstack.h | 62 + talk/xmllite/xmlnsstack_unittest.cc | 258 + talk/xmllite/xmlparser.cc | 279 + talk/xmllite/xmlparser.h | 121 + talk/xmllite/xmlparser_unittest.cc | 302 + talk/xmllite/xmlprinter.cc | 191 + talk/xmllite/xmlprinter.h | 49 + talk/xmllite/xmlprinter_unittest.cc | 62 + talk/xmpp/asyncsocket.h | 87 + talk/xmpp/chatroommodule.h | 270 + talk/xmpp/chatroommodule_unittest.cc | 297 + talk/xmpp/chatroommoduleimpl.cc | 755 +++ talk/xmpp/constants.cc | 608 ++ talk/xmpp/constants.h | 551 ++ talk/xmpp/discoitemsquerytask.cc | 79 + talk/xmpp/discoitemsquerytask.h | 82 + talk/xmpp/fakexmppclient.h | 123 + talk/xmpp/hangoutpubsubclient.cc | 643 +++ talk/xmpp/hangoutpubsubclient.h | 218 + talk/xmpp/hangoutpubsubclient_unittest.cc | 740 +++ talk/xmpp/iqtask.cc | 86 + talk/xmpp/iqtask.h | 65 + talk/xmpp/jid.cc | 396 ++ talk/xmpp/jid.h | 98 + talk/xmpp/jid_unittest.cc | 115 + talk/xmpp/jingleinfotask.cc | 138 + talk/xmpp/jingleinfotask.h | 61 + talk/xmpp/module.h | 51 + talk/xmpp/moduleimpl.cc | 65 + talk/xmpp/moduleimpl.h | 93 + talk/xmpp/mucroomconfigtask.cc | 91 + talk/xmpp/mucroomconfigtask.h | 64 + talk/xmpp/mucroomconfigtask_unittest.cc | 144 + talk/xmpp/mucroomdiscoverytask.cc | 75 + talk/xmpp/mucroomdiscoverytask.h | 57 + talk/xmpp/mucroomdiscoverytask_unittest.cc | 152 + talk/xmpp/mucroomlookuptask.cc | 176 + talk/xmpp/mucroomlookuptask.h | 88 + talk/xmpp/mucroomlookuptask_unittest.cc | 204 + talk/xmpp/mucroomuniquehangoutidtask.cc | 44 + talk/xmpp/mucroomuniquehangoutidtask.h | 31 + .../mucroomuniquehangoutidtask_unittest.cc | 116 + talk/xmpp/pingtask.cc | 85 + talk/xmpp/pingtask.h | 71 + talk/xmpp/pingtask_unittest.cc | 118 + talk/xmpp/plainsaslhandler.h | 80 + talk/xmpp/presenceouttask.cc | 157 + talk/xmpp/presenceouttask.h | 54 + talk/xmpp/presencereceivetask.cc | 158 + talk/xmpp/presencereceivetask.h | 73 + talk/xmpp/presencestatus.cc | 62 + talk/xmpp/presencestatus.h | 205 + talk/xmpp/prexmppauth.h | 88 + talk/xmpp/pubsub_task.cc | 217 + talk/xmpp/pubsub_task.h | 75 + talk/xmpp/pubsubclient.cc | 137 + talk/xmpp/pubsubclient.h | 125 + talk/xmpp/pubsubclient_unittest.cc | 271 + talk/xmpp/pubsubtasks.cc | 214 + talk/xmpp/pubsubtasks.h | 130 + talk/xmpp/pubsubtasks_unittest.cc | 273 + talk/xmpp/receivetask.cc | 51 + talk/xmpp/receivetask.h | 58 + talk/xmpp/rostermodule.h | 343 ++ talk/xmpp/rostermodule_unittest.cc | 849 +++ talk/xmpp/rostermoduleimpl.cc | 1080 ++++ talk/xmpp/rostermoduleimpl.h | 302 + talk/xmpp/saslcookiemechanism.h | 86 + talk/xmpp/saslhandler.h | 59 + talk/xmpp/saslmechanism.cc | 72 + talk/xmpp/saslmechanism.h | 74 + talk/xmpp/saslplainmechanism.h | 65 + talk/xmpp/util_unittest.cc | 102 + talk/xmpp/util_unittest.h | 75 + talk/xmpp/xmppauth.cc | 105 + talk/xmpp/xmppauth.h | 78 + talk/xmpp/xmppclient.cc | 440 ++ talk/xmpp/xmppclient.h | 165 + talk/xmpp/xmppclientsettings.h | 128 + talk/xmpp/xmppengine.h | 349 ++ talk/xmpp/xmppengine_unittest.cc | 318 ++ talk/xmpp/xmppengineimpl.cc | 464 ++ talk/xmpp/xmppengineimpl.h | 284 + talk/xmpp/xmppengineimpl_iq.cc | 277 + talk/xmpp/xmpplogintask.cc | 397 ++ talk/xmpp/xmpplogintask.h | 104 + talk/xmpp/xmpplogintask_unittest.cc | 614 ++ talk/xmpp/xmpppump.cc | 84 + talk/xmpp/xmpppump.h | 79 + talk/xmpp/xmppsocket.cc | 262 + talk/xmpp/xmppsocket.h | 89 + talk/xmpp/xmppstanzaparser.cc | 106 + talk/xmpp/xmppstanzaparser.h | 97 + talk/xmpp/xmppstanzaparser_unittest.cc | 168 + talk/xmpp/xmpptask.cc | 175 + talk/xmpp/xmpptask.h | 189 + talk/xmpp/xmppthread.cc | 85 + talk/xmpp/xmppthread.h | 62 + 1067 files changed, 275209 insertions(+) create mode 100644 talk/OWNERS create mode 100644 talk/app/webrtc/audiotrack.cc create mode 100644 talk/app/webrtc/audiotrack.h create mode 100644 talk/app/webrtc/audiotrackrenderer.cc create mode 100644 talk/app/webrtc/audiotrackrenderer.h create mode 100644 talk/app/webrtc/datachannel.cc create mode 100644 talk/app/webrtc/datachannel.h create mode 100644 talk/app/webrtc/datachannelinterface.h create mode 100644 talk/app/webrtc/dtmfsender.cc create mode 100644 talk/app/webrtc/dtmfsender.h create mode 100644 talk/app/webrtc/dtmfsender_unittest.cc create mode 100644 talk/app/webrtc/dtmfsenderinterface.h create mode 100644 talk/app/webrtc/fakeportallocatorfactory.h create mode 100644 talk/app/webrtc/java/README create mode 100644 talk/app/webrtc/java/jni/peerconnection_jni.cc create mode 100644 talk/app/webrtc/java/src/org/webrtc/AudioSource.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/AudioTrack.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/IceCandidate.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/MediaConstraints.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/MediaSource.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/MediaStream.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/MediaStreamTrack.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/PeerConnection.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/PeerConnectionFactory.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/SdpObserver.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/SessionDescription.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/StatsObserver.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/StatsReport.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/VideoCapturer.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/VideoRenderer.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/VideoSource.java create mode 100644 talk/app/webrtc/java/src/org/webrtc/VideoTrack.java create mode 100644 talk/app/webrtc/javatests/libjingle_peerconnection_java_unittest.sh create mode 100644 talk/app/webrtc/javatests/src/org/webrtc/PeerConnectionTest.java create mode 100644 talk/app/webrtc/jsep.h create mode 100644 talk/app/webrtc/jsepicecandidate.cc create mode 100644 talk/app/webrtc/jsepicecandidate.h create mode 100644 talk/app/webrtc/jsepsessiondescription.cc create mode 100644 talk/app/webrtc/jsepsessiondescription.h create mode 100644 talk/app/webrtc/jsepsessiondescription_unittest.cc create mode 100644 talk/app/webrtc/localaudiosource.cc create mode 100644 talk/app/webrtc/localaudiosource.h create mode 100644 talk/app/webrtc/localaudiosource_unittest.cc create mode 100644 talk/app/webrtc/localvideosource.cc create mode 100644 talk/app/webrtc/localvideosource.h create mode 100644 talk/app/webrtc/localvideosource_unittest.cc create mode 100644 talk/app/webrtc/mediaconstraintsinterface.cc create mode 100644 talk/app/webrtc/mediaconstraintsinterface.h create mode 100644 talk/app/webrtc/mediastream.cc create mode 100644 talk/app/webrtc/mediastream.h create mode 100644 talk/app/webrtc/mediastream_unittest.cc create mode 100644 talk/app/webrtc/mediastreamhandler.cc create mode 100644 talk/app/webrtc/mediastreamhandler.h create mode 100644 talk/app/webrtc/mediastreamhandler_unittest.cc create mode 100644 talk/app/webrtc/mediastreaminterface.h create mode 100644 talk/app/webrtc/mediastreamprovider.h create mode 100644 talk/app/webrtc/mediastreamproxy.h create mode 100644 talk/app/webrtc/mediastreamsignaling.cc create mode 100644 talk/app/webrtc/mediastreamsignaling.h create mode 100644 talk/app/webrtc/mediastreamsignaling_unittest.cc create mode 100644 talk/app/webrtc/mediastreamtrack.h create mode 100644 talk/app/webrtc/mediastreamtrackproxy.h create mode 100644 talk/app/webrtc/notifier.h create mode 100644 talk/app/webrtc/objc/README create mode 100644 talk/app/webrtc/objc/RTCAudioTrack+Internal.h create mode 100644 talk/app/webrtc/objc/RTCAudioTrack.mm create mode 100644 talk/app/webrtc/objc/RTCEnumConverter.h create mode 100644 talk/app/webrtc/objc/RTCEnumConverter.mm create mode 100644 talk/app/webrtc/objc/RTCI420Frame.mm create mode 100644 talk/app/webrtc/objc/RTCIceCandidate+Internal.h create mode 100644 talk/app/webrtc/objc/RTCIceCandidate.mm create mode 100644 talk/app/webrtc/objc/RTCIceServer+Internal.h create mode 100644 talk/app/webrtc/objc/RTCIceServer.mm create mode 100644 talk/app/webrtc/objc/RTCMediaConstraints+Internal.h create mode 100644 talk/app/webrtc/objc/RTCMediaConstraints.mm create mode 100644 talk/app/webrtc/objc/RTCMediaConstraintsNative.cc create mode 100644 talk/app/webrtc/objc/RTCMediaConstraintsNative.h create mode 100644 talk/app/webrtc/objc/RTCMediaSource+Internal.h create mode 100644 talk/app/webrtc/objc/RTCMediaSource.mm create mode 100644 talk/app/webrtc/objc/RTCMediaStream+Internal.h create mode 100644 talk/app/webrtc/objc/RTCMediaStream.mm create mode 100644 talk/app/webrtc/objc/RTCMediaStreamTrack+Internal.h create mode 100644 talk/app/webrtc/objc/RTCMediaStreamTrack.mm create mode 100644 talk/app/webrtc/objc/RTCPair.m create mode 100644 talk/app/webrtc/objc/RTCPeerConnection+Internal.h create mode 100644 talk/app/webrtc/objc/RTCPeerConnection.mm create mode 100644 talk/app/webrtc/objc/RTCPeerConnectionFactory.mm create mode 100644 talk/app/webrtc/objc/RTCPeerConnectionObserver.h create mode 100644 talk/app/webrtc/objc/RTCPeerConnectionObserver.mm create mode 100644 talk/app/webrtc/objc/RTCSessionDescription+Internal.h create mode 100644 talk/app/webrtc/objc/RTCSessionDescription.mm create mode 100644 talk/app/webrtc/objc/RTCVideoCapturer+Internal.h create mode 100644 talk/app/webrtc/objc/RTCVideoCapturer.mm create mode 100644 talk/app/webrtc/objc/RTCVideoRenderer+Internal.h create mode 100644 talk/app/webrtc/objc/RTCVideoRenderer.mm create mode 100644 talk/app/webrtc/objc/RTCVideoSource+Internal.h create mode 100644 talk/app/webrtc/objc/RTCVideoSource.mm create mode 100644 talk/app/webrtc/objc/RTCVideoTrack+Internal.h create mode 100644 talk/app/webrtc/objc/RTCVideoTrack.mm create mode 100644 talk/app/webrtc/objc/public/RTCAudioSource.h create mode 100644 talk/app/webrtc/objc/public/RTCAudioTrack.h create mode 100644 talk/app/webrtc/objc/public/RTCI420Frame.h create mode 100644 talk/app/webrtc/objc/public/RTCIceCandidate.h create mode 100644 talk/app/webrtc/objc/public/RTCIceServer.h create mode 100644 talk/app/webrtc/objc/public/RTCMediaConstraints.h create mode 100644 talk/app/webrtc/objc/public/RTCMediaSource.h create mode 100644 talk/app/webrtc/objc/public/RTCMediaStream.h create mode 100644 talk/app/webrtc/objc/public/RTCMediaStreamTrack.h create mode 100644 talk/app/webrtc/objc/public/RTCPair.h create mode 100644 talk/app/webrtc/objc/public/RTCPeerConnection.h create mode 100644 talk/app/webrtc/objc/public/RTCPeerConnectionDelegate.h create mode 100644 talk/app/webrtc/objc/public/RTCPeerConnectionFactory.h create mode 100644 talk/app/webrtc/objc/public/RTCSessionDescription.h create mode 100644 talk/app/webrtc/objc/public/RTCSessionDescriptonDelegate.h create mode 100644 talk/app/webrtc/objc/public/RTCTypes.h create mode 100644 talk/app/webrtc/objc/public/RTCVideoCapturer.h create mode 100644 talk/app/webrtc/objc/public/RTCVideoRenderer.h create mode 100644 talk/app/webrtc/objc/public/RTCVideoRendererDelegate.h create mode 100644 talk/app/webrtc/objc/public/RTCVideoSource.h create mode 100644 talk/app/webrtc/objc/public/RTCVideoTrack.h create mode 100644 talk/app/webrtc/objctests/Info.plist create mode 100644 talk/app/webrtc/objctests/RTCPeerConnectionSyncObserver.h create mode 100644 talk/app/webrtc/objctests/RTCPeerConnectionSyncObserver.m create mode 100644 talk/app/webrtc/objctests/RTCPeerConnectionTest.mm create mode 100644 talk/app/webrtc/objctests/RTCSessionDescriptionSyncObserver.h create mode 100644 talk/app/webrtc/objctests/RTCSessionDescriptionSyncObserver.m create mode 100644 talk/app/webrtc/objctests/mac/main.mm create mode 100644 talk/app/webrtc/peerconnection.cc create mode 100644 talk/app/webrtc/peerconnection.h create mode 100644 talk/app/webrtc/peerconnection_unittest.cc create mode 100644 talk/app/webrtc/peerconnectionfactory.cc create mode 100644 talk/app/webrtc/peerconnectionfactory.h create mode 100644 talk/app/webrtc/peerconnectionfactory_unittest.cc create mode 100644 talk/app/webrtc/peerconnectioninterface.h create mode 100644 talk/app/webrtc/peerconnectioninterface_unittest.cc create mode 100644 talk/app/webrtc/peerconnectionproxy.h create mode 100644 talk/app/webrtc/portallocatorfactory.cc create mode 100644 talk/app/webrtc/portallocatorfactory.h create mode 100644 talk/app/webrtc/proxy.h create mode 100644 talk/app/webrtc/proxy_unittest.cc create mode 100644 talk/app/webrtc/statscollector.cc create mode 100644 talk/app/webrtc/statscollector.h create mode 100644 talk/app/webrtc/statscollector_unittest.cc create mode 100644 talk/app/webrtc/statstypes.h create mode 100644 talk/app/webrtc/streamcollection.h create mode 100644 talk/app/webrtc/test/fakeaudiocapturemodule.cc create mode 100644 talk/app/webrtc/test/fakeaudiocapturemodule.h create mode 100644 talk/app/webrtc/test/fakeaudiocapturemodule_unittest.cc create mode 100644 talk/app/webrtc/test/fakeconstraints.h create mode 100644 talk/app/webrtc/test/fakeperiodicvideocapturer.h create mode 100644 talk/app/webrtc/test/fakevideotrackrenderer.h create mode 100644 talk/app/webrtc/test/mockpeerconnectionobservers.h create mode 100644 talk/app/webrtc/test/testsdpstrings.h create mode 100644 talk/app/webrtc/videosourceinterface.h create mode 100644 talk/app/webrtc/videosourceproxy.h create mode 100644 talk/app/webrtc/videotrack.cc create mode 100644 talk/app/webrtc/videotrack.h create mode 100644 talk/app/webrtc/videotrack_unittest.cc create mode 100644 talk/app/webrtc/videotrackrenderers.cc create mode 100644 talk/app/webrtc/videotrackrenderers.h create mode 100644 talk/app/webrtc/webrtc.scons create mode 100644 talk/app/webrtc/webrtcsdp.cc create mode 100644 talk/app/webrtc/webrtcsdp.h create mode 100644 talk/app/webrtc/webrtcsdp_unittest.cc create mode 100644 talk/app/webrtc/webrtcsession.cc create mode 100644 talk/app/webrtc/webrtcsession.h create mode 100644 talk/app/webrtc/webrtcsession_unittest.cc create mode 100644 talk/base/asyncfile.cc create mode 100644 talk/base/asyncfile.h create mode 100644 talk/base/asynchttprequest.cc create mode 100644 talk/base/asynchttprequest.h create mode 100644 talk/base/asynchttprequest_unittest.cc create mode 100644 talk/base/asyncpacketsocket.h create mode 100644 talk/base/asyncsocket.cc create mode 100644 talk/base/asyncsocket.h create mode 100644 talk/base/asynctcpsocket.cc create mode 100644 talk/base/asynctcpsocket.h create mode 100644 talk/base/asynctcpsocket_unittest.cc create mode 100644 talk/base/asyncudpsocket.cc create mode 100644 talk/base/asyncudpsocket.h create mode 100644 talk/base/asyncudpsocket_unittest.cc create mode 100644 talk/base/atomicops.h create mode 100644 talk/base/atomicops_unittest.cc create mode 100644 talk/base/autodetectproxy.cc create mode 100644 talk/base/autodetectproxy.h create mode 100644 talk/base/autodetectproxy_unittest.cc create mode 100644 talk/base/bandwidthsmoother.cc create mode 100644 talk/base/bandwidthsmoother.h create mode 100644 talk/base/bandwidthsmoother_unittest.cc create mode 100644 talk/base/base64.cc create mode 100644 talk/base/base64.h create mode 100644 talk/base/base64_unittest.cc create mode 100644 talk/base/basicdefs.h create mode 100644 talk/base/basictypes.h create mode 100644 talk/base/basictypes_unittest.cc create mode 100644 talk/base/bind.h create mode 100644 talk/base/bind.h.pump create mode 100644 talk/base/bind_unittest.cc create mode 100644 talk/base/buffer.h create mode 100644 talk/base/buffer_unittest.cc create mode 100644 talk/base/bytebuffer.cc create mode 100644 talk/base/bytebuffer.h create mode 100644 talk/base/bytebuffer_unittest.cc create mode 100644 talk/base/byteorder.h create mode 100644 talk/base/byteorder_unittest.cc create mode 100644 talk/base/checks.cc create mode 100644 talk/base/checks.h create mode 100644 talk/base/common.cc create mode 100644 talk/base/common.h create mode 100644 talk/base/constructormagic.h create mode 100644 talk/base/cpumonitor.cc create mode 100644 talk/base/cpumonitor.h create mode 100644 talk/base/cpumonitor_unittest.cc create mode 100644 talk/base/crc32.cc create mode 100644 talk/base/crc32.h create mode 100644 talk/base/crc32_unittest.cc create mode 100644 talk/base/criticalsection.h create mode 100644 talk/base/cryptstring.h create mode 100644 talk/base/dbus.cc create mode 100644 talk/base/dbus.h create mode 100644 talk/base/dbus_unittest.cc create mode 100644 talk/base/diskcache.cc create mode 100644 talk/base/diskcache.h create mode 100644 talk/base/diskcache_win32.cc create mode 100644 talk/base/diskcache_win32.h create mode 100644 talk/base/event.cc create mode 100644 talk/base/event.h create mode 100644 talk/base/event_unittest.cc create mode 100644 talk/base/fakecpumonitor.h create mode 100644 talk/base/fakenetwork.h create mode 100644 talk/base/fakesslidentity.h create mode 100644 talk/base/faketaskrunner.h create mode 100644 talk/base/filelock.cc create mode 100644 talk/base/filelock.h create mode 100644 talk/base/filelock_unittest.cc create mode 100644 talk/base/fileutils.cc create mode 100644 talk/base/fileutils.h create mode 100644 talk/base/fileutils_mock.h create mode 100644 talk/base/fileutils_unittest.cc create mode 100644 talk/base/firewallsocketserver.cc create mode 100644 talk/base/firewallsocketserver.h create mode 100644 talk/base/flags.cc create mode 100644 talk/base/flags.h create mode 100644 talk/base/gunit.h create mode 100644 talk/base/gunit_prod.h create mode 100644 talk/base/helpers.cc create mode 100644 talk/base/helpers.h create mode 100644 talk/base/helpers_unittest.cc create mode 100644 talk/base/host.cc create mode 100644 talk/base/host.h create mode 100644 talk/base/host_unittest.cc create mode 100644 talk/base/httpbase.cc create mode 100644 talk/base/httpbase.h create mode 100644 talk/base/httpbase_unittest.cc create mode 100644 talk/base/httpclient.cc create mode 100644 talk/base/httpclient.h create mode 100644 talk/base/httpcommon-inl.h create mode 100644 talk/base/httpcommon.cc create mode 100644 talk/base/httpcommon.h create mode 100644 talk/base/httpcommon_unittest.cc create mode 100644 talk/base/httprequest.cc create mode 100644 talk/base/httprequest.h create mode 100644 talk/base/httpserver.cc create mode 100644 talk/base/httpserver.h create mode 100644 talk/base/httpserver_unittest.cc create mode 100644 talk/base/ifaddrs-android.cc create mode 100644 talk/base/ifaddrs-android.h create mode 100644 talk/base/ipaddress.cc create mode 100644 talk/base/ipaddress.h create mode 100644 talk/base/ipaddress_unittest.cc create mode 100644 talk/base/json.cc create mode 100644 talk/base/json.h create mode 100644 talk/base/json_unittest.cc create mode 100644 talk/base/latebindingsymboltable.cc create mode 100644 talk/base/latebindingsymboltable.cc.def create mode 100644 talk/base/latebindingsymboltable.h create mode 100644 talk/base/latebindingsymboltable.h.def create mode 100644 talk/base/latebindingsymboltable_unittest.cc create mode 100644 talk/base/libdbusglibsymboltable.cc create mode 100644 talk/base/libdbusglibsymboltable.h create mode 100644 talk/base/linked_ptr.h create mode 100644 talk/base/linux.cc create mode 100644 talk/base/linux.h create mode 100644 talk/base/linux_unittest.cc create mode 100644 talk/base/linuxfdwalk.c create mode 100644 talk/base/linuxfdwalk.h create mode 100644 talk/base/linuxfdwalk_unittest.cc create mode 100644 talk/base/linuxwindowpicker.cc create mode 100644 talk/base/linuxwindowpicker.h create mode 100644 talk/base/linuxwindowpicker_unittest.cc create mode 100644 talk/base/logging.cc create mode 100644 talk/base/logging.h create mode 100644 talk/base/logging_unittest.cc create mode 100644 talk/base/macasyncsocket.cc create mode 100644 talk/base/macasyncsocket.h create mode 100644 talk/base/maccocoasocketserver.h create mode 100644 talk/base/maccocoasocketserver.mm create mode 100644 talk/base/maccocoasocketserver_unittest.mm create mode 100644 talk/base/maccocoathreadhelper.h create mode 100644 talk/base/maccocoathreadhelper.mm create mode 100644 talk/base/macconversion.cc create mode 100644 talk/base/macconversion.h create mode 100644 talk/base/macsocketserver.cc create mode 100644 talk/base/macsocketserver.h create mode 100644 talk/base/macsocketserver_unittest.cc create mode 100644 talk/base/macutils.cc create mode 100644 talk/base/macutils.h create mode 100644 talk/base/macutils_unittest.cc create mode 100644 talk/base/macwindowpicker.cc create mode 100644 talk/base/macwindowpicker.h create mode 100644 talk/base/macwindowpicker_unittest.cc create mode 100644 talk/base/mathutils.h create mode 100644 talk/base/md5.cc create mode 100644 talk/base/md5.h create mode 100644 talk/base/md5digest.h create mode 100644 talk/base/md5digest_unittest.cc create mode 100644 talk/base/messagedigest.cc create mode 100644 talk/base/messagedigest.h create mode 100644 talk/base/messagedigest_unittest.cc create mode 100644 talk/base/messagehandler.cc create mode 100644 talk/base/messagehandler.h create mode 100644 talk/base/messagequeue.cc create mode 100644 talk/base/messagequeue.h create mode 100644 talk/base/messagequeue_unittest.cc create mode 100644 talk/base/multipart.cc create mode 100644 talk/base/multipart.h create mode 100644 talk/base/multipart_unittest.cc create mode 100644 talk/base/nat_unittest.cc create mode 100644 talk/base/natserver.cc create mode 100644 talk/base/natserver.h create mode 100644 talk/base/natserver_main.cc create mode 100644 talk/base/natsocketfactory.cc create mode 100644 talk/base/natsocketfactory.h create mode 100644 talk/base/nattypes.cc create mode 100644 talk/base/nattypes.h create mode 100644 talk/base/nethelpers.cc create mode 100644 talk/base/nethelpers.h create mode 100644 talk/base/network.cc create mode 100644 talk/base/network.h create mode 100644 talk/base/network_unittest.cc create mode 100644 talk/base/nssidentity.cc create mode 100644 talk/base/nssidentity.h create mode 100644 talk/base/nssstreamadapter.cc create mode 100644 talk/base/nssstreamadapter.h create mode 100644 talk/base/nullsocketserver.h create mode 100644 talk/base/nullsocketserver_unittest.cc create mode 100644 talk/base/openssladapter.cc create mode 100644 talk/base/openssladapter.h create mode 100644 talk/base/openssldigest.cc create mode 100644 talk/base/openssldigest.h create mode 100644 talk/base/opensslidentity.cc create mode 100644 talk/base/opensslidentity.h create mode 100644 talk/base/opensslstreamadapter.cc create mode 100644 talk/base/opensslstreamadapter.h create mode 100644 talk/base/optionsfile.cc create mode 100644 talk/base/optionsfile.h create mode 100644 talk/base/optionsfile_unittest.cc create mode 100644 talk/base/pathutils.cc create mode 100644 talk/base/pathutils.h create mode 100644 talk/base/pathutils_unittest.cc create mode 100644 talk/base/physicalsocketserver.cc create mode 100644 talk/base/physicalsocketserver.h create mode 100644 talk/base/physicalsocketserver_unittest.cc create mode 100644 talk/base/posix.cc create mode 100644 talk/base/posix.h create mode 100644 talk/base/profiler.cc create mode 100644 talk/base/profiler.h create mode 100644 talk/base/profiler_unittest.cc create mode 100644 talk/base/proxy_unittest.cc create mode 100644 talk/base/proxydetect.cc create mode 100644 talk/base/proxydetect.h create mode 100644 talk/base/proxydetect_unittest.cc create mode 100644 talk/base/proxyinfo.cc create mode 100644 talk/base/proxyinfo.h create mode 100644 talk/base/proxyserver.cc create mode 100644 talk/base/proxyserver.h create mode 100644 talk/base/ratelimiter.cc create mode 100644 talk/base/ratelimiter.h create mode 100644 talk/base/ratelimiter_unittest.cc create mode 100644 talk/base/ratetracker.cc create mode 100644 talk/base/ratetracker.h create mode 100644 talk/base/ratetracker_unittest.cc create mode 100644 talk/base/refcount.h create mode 100644 talk/base/referencecountedsingletonfactory.h create mode 100644 talk/base/referencecountedsingletonfactory_unittest.cc create mode 100644 talk/base/rollingaccumulator.h create mode 100644 talk/base/rollingaccumulator_unittest.cc create mode 100644 talk/base/schanneladapter.cc create mode 100644 talk/base/schanneladapter.h create mode 100644 talk/base/scoped_autorelease_pool.h create mode 100644 talk/base/scoped_autorelease_pool.mm create mode 100644 talk/base/scoped_ptr.h create mode 100644 talk/base/scoped_ref_ptr.h create mode 100644 talk/base/sec_buffer.h create mode 100644 talk/base/sha1.cc create mode 100644 talk/base/sha1.h create mode 100644 talk/base/sha1digest.h create mode 100644 talk/base/sha1digest_unittest.cc create mode 100644 talk/base/sharedexclusivelock.cc create mode 100644 talk/base/sharedexclusivelock.h create mode 100644 talk/base/sharedexclusivelock_unittest.cc create mode 100644 talk/base/signalthread.cc create mode 100644 talk/base/signalthread.h create mode 100644 talk/base/signalthread_unittest.cc create mode 100644 talk/base/sigslot.h create mode 100644 talk/base/sigslot_unittest.cc create mode 100644 talk/base/sigslotrepeater.h create mode 100644 talk/base/socket.h create mode 100644 talk/base/socket_unittest.cc create mode 100644 talk/base/socket_unittest.h create mode 100644 talk/base/socketadapters.cc create mode 100644 talk/base/socketadapters.h create mode 100644 talk/base/socketaddress.cc create mode 100644 talk/base/socketaddress.h create mode 100644 talk/base/socketaddress_unittest.cc create mode 100644 talk/base/socketaddresspair.cc create mode 100644 talk/base/socketaddresspair.h create mode 100644 talk/base/socketfactory.h create mode 100644 talk/base/socketpool.cc create mode 100644 talk/base/socketpool.h create mode 100644 talk/base/socketserver.h create mode 100644 talk/base/socketstream.cc create mode 100644 talk/base/socketstream.h create mode 100644 talk/base/ssladapter.cc create mode 100644 talk/base/ssladapter.h create mode 100644 talk/base/sslconfig.h create mode 100644 talk/base/sslfingerprint.h create mode 100644 talk/base/sslidentity.cc create mode 100644 talk/base/sslidentity.h create mode 100644 talk/base/sslidentity_unittest.cc create mode 100644 talk/base/sslroots.h create mode 100644 talk/base/sslsocketfactory.cc create mode 100644 talk/base/sslsocketfactory.h create mode 100644 talk/base/sslstreamadapter.cc create mode 100644 talk/base/sslstreamadapter.h create mode 100644 talk/base/sslstreamadapter_unittest.cc create mode 100644 talk/base/sslstreamadapterhelper.cc create mode 100644 talk/base/sslstreamadapterhelper.h create mode 100644 talk/base/stream.cc create mode 100644 talk/base/stream.h create mode 100644 talk/base/stream_unittest.cc create mode 100644 talk/base/stringdigest.h create mode 100644 talk/base/stringencode.cc create mode 100644 talk/base/stringencode.h create mode 100644 talk/base/stringencode_unittest.cc create mode 100644 talk/base/stringutils.cc create mode 100644 talk/base/stringutils.h create mode 100644 talk/base/stringutils_unittest.cc create mode 100644 talk/base/systeminfo.cc create mode 100644 talk/base/systeminfo.h create mode 100644 talk/base/systeminfo_unittest.cc create mode 100644 talk/base/task.cc create mode 100644 talk/base/task.h create mode 100644 talk/base/task_unittest.cc create mode 100644 talk/base/taskparent.cc create mode 100644 talk/base/taskparent.h create mode 100644 talk/base/taskrunner.cc create mode 100644 talk/base/taskrunner.h create mode 100644 talk/base/testbase64.h create mode 100644 talk/base/testclient.cc create mode 100644 talk/base/testclient.h create mode 100644 talk/base/testclient_unittest.cc create mode 100644 talk/base/testechoserver.h create mode 100644 talk/base/testutils.h create mode 100644 talk/base/thread.cc create mode 100644 talk/base/thread.h create mode 100644 talk/base/thread_unittest.cc create mode 100644 talk/base/timeutils.cc create mode 100644 talk/base/timeutils.h create mode 100644 talk/base/timeutils_unittest.cc create mode 100644 talk/base/timing.cc create mode 100644 talk/base/timing.h create mode 100644 talk/base/transformadapter.cc create mode 100644 talk/base/transformadapter.h create mode 100644 talk/base/unittest_main.cc create mode 100644 talk/base/unixfilesystem.cc create mode 100644 talk/base/unixfilesystem.h create mode 100644 talk/base/urlencode.cc create mode 100644 talk/base/urlencode.h create mode 100644 talk/base/urlencode_unittest.cc create mode 100644 talk/base/versionparsing.cc create mode 100644 talk/base/versionparsing.h create mode 100644 talk/base/versionparsing_unittest.cc create mode 100644 talk/base/virtualsocket_unittest.cc create mode 100644 talk/base/virtualsocketserver.cc create mode 100644 talk/base/virtualsocketserver.h create mode 100644 talk/base/win32.cc create mode 100644 talk/base/win32.h create mode 100644 talk/base/win32_unittest.cc create mode 100644 talk/base/win32filesystem.cc create mode 100644 talk/base/win32filesystem.h create mode 100644 talk/base/win32regkey.cc create mode 100644 talk/base/win32regkey.h create mode 100644 talk/base/win32regkey_unittest.cc create mode 100644 talk/base/win32securityerrors.cc create mode 100644 talk/base/win32socketinit.cc create mode 100644 talk/base/win32socketinit.h create mode 100644 talk/base/win32socketserver.cc create mode 100644 talk/base/win32socketserver.h create mode 100644 talk/base/win32socketserver_unittest.cc create mode 100644 talk/base/win32toolhelp.h create mode 100644 talk/base/win32toolhelp_unittest.cc create mode 100644 talk/base/win32window.cc create mode 100644 talk/base/win32window.h create mode 100644 talk/base/win32window_unittest.cc create mode 100644 talk/base/win32windowpicker.cc create mode 100644 talk/base/win32windowpicker.h create mode 100644 talk/base/win32windowpicker_unittest.cc create mode 100644 talk/base/window.h create mode 100644 talk/base/windowpicker.h create mode 100644 talk/base/windowpicker_unittest.cc create mode 100644 talk/base/windowpickerfactory.h create mode 100644 talk/base/winfirewall.cc create mode 100644 talk/base/winfirewall.h create mode 100644 talk/base/winfirewall_unittest.cc create mode 100644 talk/base/winping.cc create mode 100644 talk/base/winping.h create mode 100644 talk/base/worker.cc create mode 100644 talk/base/worker.h create mode 100755 talk/build/build_jar.sh create mode 100644 talk/build/common.gypi create mode 100644 talk/examples/android/AndroidManifest.xml create mode 100644 talk/examples/android/README create mode 100644 talk/examples/android/ant.properties create mode 100644 talk/examples/android/assets/channel.html create mode 100644 talk/examples/android/build.xml create mode 100644 talk/examples/android/jni/Android.mk create mode 100644 talk/examples/android/project.properties create mode 100644 talk/examples/android/res/drawable-hdpi/ic_launcher.png create mode 100644 talk/examples/android/res/drawable-ldpi/ic_launcher.png create mode 100644 talk/examples/android/res/drawable-mdpi/ic_launcher.png create mode 100644 talk/examples/android/res/drawable-xhdpi/ic_launcher.png create mode 100644 talk/examples/android/res/values/strings.xml create mode 100644 talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java create mode 100644 talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java create mode 100644 talk/examples/android/src/org/appspot/apprtc/FramePool.java create mode 100644 talk/examples/android/src/org/appspot/apprtc/GAEChannelClient.java create mode 100644 talk/examples/android/src/org/appspot/apprtc/VideoStreamsView.java create mode 100644 talk/examples/call/Info.plist create mode 100644 talk/examples/call/call_main.cc create mode 100644 talk/examples/call/call_unittest.cc create mode 100644 talk/examples/call/callclient.cc create mode 100644 talk/examples/call/callclient.h create mode 100644 talk/examples/call/callclient_unittest.cc create mode 100644 talk/examples/call/console.cc create mode 100644 talk/examples/call/console.h create mode 100644 talk/examples/call/friendinvitesendtask.cc create mode 100644 talk/examples/call/friendinvitesendtask.h create mode 100644 talk/examples/call/mediaenginefactory.cc create mode 100644 talk/examples/call/mediaenginefactory.h create mode 100644 talk/examples/call/muc.h create mode 100644 talk/examples/call/mucinviterecvtask.cc create mode 100644 talk/examples/call/mucinviterecvtask.h create mode 100644 talk/examples/call/mucinvitesendtask.cc create mode 100644 talk/examples/call/mucinvitesendtask.h create mode 100644 talk/examples/call/presencepushtask.cc create mode 100644 talk/examples/call/presencepushtask.h create mode 100644 talk/examples/chat/Info.plist create mode 100644 talk/examples/chat/chat_main.cc create mode 100644 talk/examples/chat/chatapp.cc create mode 100644 talk/examples/chat/chatapp.h create mode 100644 talk/examples/chat/consoletask.cc create mode 100644 talk/examples/chat/consoletask.h create mode 100644 talk/examples/chat/textchatreceivetask.cc create mode 100644 talk/examples/chat/textchatreceivetask.h create mode 100644 talk/examples/chat/textchatsendtask.cc create mode 100644 talk/examples/chat/textchatsendtask.h create mode 100644 talk/examples/ios/AppRTCDemo.xcodeproj/project.pbxproj create mode 100644 talk/examples/ios/AppRTCDemo/APPRTCAppClient.h create mode 100644 talk/examples/ios/AppRTCDemo/APPRTCAppClient.m create mode 100644 talk/examples/ios/AppRTCDemo/APPRTCAppDelegate.h create mode 100644 talk/examples/ios/AppRTCDemo/APPRTCAppDelegate.m create mode 100644 talk/examples/ios/AppRTCDemo/APPRTCViewController.h create mode 100644 talk/examples/ios/AppRTCDemo/APPRTCViewController.m create mode 100644 talk/examples/ios/AppRTCDemo/AppRTCDemo-Info.plist create mode 100644 talk/examples/ios/AppRTCDemo/AppRTCDemo-Prefix.pch create mode 100644 talk/examples/ios/AppRTCDemo/Default.png create mode 100644 talk/examples/ios/AppRTCDemo/GAEChannelClient.h create mode 100644 talk/examples/ios/AppRTCDemo/GAEChannelClient.m create mode 100644 talk/examples/ios/AppRTCDemo/en.lproj/APPRTCViewController.xib create mode 100644 talk/examples/ios/AppRTCDemo/ios_channel.html create mode 100644 talk/examples/ios/AppRTCDemo/main.m create mode 100644 talk/examples/ios/Icon.png create mode 100644 talk/examples/ios/README create mode 100755 talk/examples/ios/makeLibs.sh create mode 100644 talk/examples/login/login_main.cc create mode 100644 talk/examples/pcp/pcp_main.cc create mode 100644 talk/examples/peerconnection/client/conductor.cc create mode 100644 talk/examples/peerconnection/client/conductor.h create mode 100644 talk/examples/peerconnection/client/defaults.cc create mode 100644 talk/examples/peerconnection/client/defaults.h create mode 100644 talk/examples/peerconnection/client/flagdefs.h create mode 100644 talk/examples/peerconnection/client/linux/main.cc create mode 100644 talk/examples/peerconnection/client/linux/main_wnd.cc create mode 100644 talk/examples/peerconnection/client/linux/main_wnd.h create mode 100644 talk/examples/peerconnection/client/main.cc create mode 100644 talk/examples/peerconnection/client/main_wnd.cc create mode 100644 talk/examples/peerconnection/client/main_wnd.h create mode 100644 talk/examples/peerconnection/client/peer_connection_client.cc create mode 100644 talk/examples/peerconnection/client/peer_connection_client.h create mode 100644 talk/examples/peerconnection/peerconnection.scons create mode 100644 talk/examples/peerconnection/server/data_socket.cc create mode 100644 talk/examples/peerconnection/server/data_socket.h create mode 100644 talk/examples/peerconnection/server/main.cc create mode 100644 talk/examples/peerconnection/server/peer_channel.cc create mode 100644 talk/examples/peerconnection/server/peer_channel.h create mode 100644 talk/examples/peerconnection/server/server_test.html create mode 100644 talk/examples/peerconnection/server/utils.cc create mode 100644 talk/examples/peerconnection/server/utils.h create mode 100644 talk/examples/plus/libjingleplus.cc create mode 100644 talk/examples/plus/libjingleplus.h create mode 100644 talk/examples/plus/presencepushtask.cc create mode 100644 talk/examples/plus/presencepushtask.h create mode 100644 talk/examples/plus/rostertask.cc create mode 100644 talk/examples/plus/rostertask.h create mode 100644 talk/examples/plus/testutil/libjingleplus_main.cc create mode 100644 talk/examples/plus/testutil/libjingleplus_test_notifier.h create mode 100644 talk/examples/plus/testutil/libjingleplus_unittest.cc create mode 100755 talk/libjingle.gyp create mode 100644 talk/libjingle.scons create mode 100644 talk/libjingle_all.gyp create mode 100755 talk/libjingle_examples.gyp create mode 100755 talk/libjingle_tests.gyp create mode 100644 talk/main.scons create mode 100755 talk/media/base/audioframe.h create mode 100644 talk/media/base/audiorenderer.h create mode 100644 talk/media/base/capturemanager.cc create mode 100644 talk/media/base/capturemanager.h create mode 100644 talk/media/base/capturemanager_unittest.cc create mode 100644 talk/media/base/capturerenderadapter.cc create mode 100644 talk/media/base/capturerenderadapter.h create mode 100644 talk/media/base/codec.cc create mode 100644 talk/media/base/codec.h create mode 100644 talk/media/base/codec_unittest.cc create mode 100644 talk/media/base/constants.cc create mode 100644 talk/media/base/constants.h create mode 100644 talk/media/base/cpuid.cc create mode 100644 talk/media/base/cpuid.h create mode 100644 talk/media/base/cpuid_unittest.cc create mode 100644 talk/media/base/cryptoparams.h create mode 100644 talk/media/base/fakecapturemanager.h create mode 100644 talk/media/base/fakemediaengine.h create mode 100644 talk/media/base/fakemediaprocessor.h create mode 100644 talk/media/base/fakenetworkinterface.h create mode 100644 talk/media/base/fakertp.h create mode 100644 talk/media/base/fakevideocapturer.h create mode 100644 talk/media/base/fakevideorenderer.h create mode 100644 talk/media/base/filemediaengine.cc create mode 100644 talk/media/base/filemediaengine.h create mode 100644 talk/media/base/filemediaengine_unittest.cc create mode 100644 talk/media/base/hybriddataengine.h create mode 100644 talk/media/base/hybridvideoengine.cc create mode 100644 talk/media/base/hybridvideoengine.h create mode 100644 talk/media/base/mediachannel.h create mode 100644 talk/media/base/mediacommon.h create mode 100644 talk/media/base/mediaengine.cc create mode 100644 talk/media/base/mediaengine.h create mode 100644 talk/media/base/mutedvideocapturer.cc create mode 100644 talk/media/base/mutedvideocapturer.h create mode 100644 talk/media/base/mutedvideocapturer_unittest.cc create mode 100644 talk/media/base/nullvideoframe.h create mode 100644 talk/media/base/nullvideorenderer.h create mode 100644 talk/media/base/rtpdataengine.cc create mode 100644 talk/media/base/rtpdataengine.h create mode 100644 talk/media/base/rtpdataengine_unittest.cc create mode 100644 talk/media/base/rtpdump.cc create mode 100644 talk/media/base/rtpdump.h create mode 100644 talk/media/base/rtpdump_unittest.cc create mode 100644 talk/media/base/rtputils.cc create mode 100644 talk/media/base/rtputils.h create mode 100644 talk/media/base/rtputils_unittest.cc create mode 100644 talk/media/base/screencastid.h create mode 100644 talk/media/base/streamparams.cc create mode 100644 talk/media/base/streamparams.h create mode 100644 talk/media/base/streamparams_unittest.cc create mode 100644 talk/media/base/testutils.cc create mode 100644 talk/media/base/testutils.h create mode 100644 talk/media/base/videoadapter.cc create mode 100644 talk/media/base/videoadapter.h create mode 100644 talk/media/base/videocapturer.cc create mode 100644 talk/media/base/videocapturer.h create mode 100644 talk/media/base/videocapturer_unittest.cc create mode 100644 talk/media/base/videocommon.cc create mode 100644 talk/media/base/videocommon.h create mode 100644 talk/media/base/videocommon_unittest.cc create mode 100644 talk/media/base/videoengine_unittest.h create mode 100644 talk/media/base/videoframe.cc create mode 100644 talk/media/base/videoframe.h create mode 100644 talk/media/base/videoframe_unittest.h create mode 100755 talk/media/base/videoprocessor.h create mode 100644 talk/media/base/videorenderer.h create mode 100755 talk/media/base/voiceprocessor.h create mode 100644 talk/media/devices/carbonvideorenderer.cc create mode 100644 talk/media/devices/carbonvideorenderer.h create mode 100644 talk/media/devices/deviceinfo.h create mode 100644 talk/media/devices/devicemanager.cc create mode 100644 talk/media/devices/devicemanager.h create mode 100644 talk/media/devices/devicemanager_unittest.cc create mode 100644 talk/media/devices/dummydevicemanager.cc create mode 100644 talk/media/devices/dummydevicemanager.h create mode 100644 talk/media/devices/dummydevicemanager_unittest.cc create mode 100644 talk/media/devices/fakedevicemanager.h create mode 100644 talk/media/devices/filevideocapturer.cc create mode 100644 talk/media/devices/filevideocapturer.h create mode 100644 talk/media/devices/filevideocapturer_unittest.cc create mode 100755 talk/media/devices/gdivideorenderer.cc create mode 100755 talk/media/devices/gdivideorenderer.h create mode 100755 talk/media/devices/gtkvideorenderer.cc create mode 100755 talk/media/devices/gtkvideorenderer.h create mode 100644 talk/media/devices/iosdeviceinfo.cc create mode 100644 talk/media/devices/libudevsymboltable.cc create mode 100644 talk/media/devices/libudevsymboltable.h create mode 100644 talk/media/devices/linuxdeviceinfo.cc create mode 100644 talk/media/devices/linuxdevicemanager.cc create mode 100644 talk/media/devices/linuxdevicemanager.h create mode 100644 talk/media/devices/macdeviceinfo.cc create mode 100644 talk/media/devices/macdevicemanager.cc create mode 100644 talk/media/devices/macdevicemanager.h create mode 100644 talk/media/devices/macdevicemanagermm.mm create mode 100644 talk/media/devices/mobiledevicemanager.cc create mode 100644 talk/media/devices/v4llookup.cc create mode 100644 talk/media/devices/v4llookup.h create mode 100644 talk/media/devices/videorendererfactory.h create mode 100644 talk/media/devices/win32deviceinfo.cc create mode 100644 talk/media/devices/win32devicemanager.cc create mode 100644 talk/media/devices/win32devicemanager.h create mode 100644 talk/media/other/linphonemediaengine.cc create mode 100644 talk/media/other/linphonemediaengine.h create mode 100644 talk/media/sctp/sctpdataengine.cc create mode 100644 talk/media/sctp/sctpdataengine.h create mode 100644 talk/media/sctp/sctpdataengine_unittest.cc create mode 100644 talk/media/testdata/1.frame_plus_1.byte create mode 100644 talk/media/testdata/captured-320x240-2s-48.frames create mode 100644 talk/media/testdata/h264-svc-99-640x360.rtpdump create mode 100644 talk/media/testdata/video.rtpdump create mode 100644 talk/media/testdata/voice.rtpdump create mode 100644 talk/media/webrtc/fakewebrtccommon.h create mode 100644 talk/media/webrtc/fakewebrtcdeviceinfo.h create mode 100644 talk/media/webrtc/fakewebrtcvcmfactory.h create mode 100644 talk/media/webrtc/fakewebrtcvideocapturemodule.h create mode 100644 talk/media/webrtc/fakewebrtcvideoengine.h create mode 100644 talk/media/webrtc/fakewebrtcvoiceengine.h create mode 100644 talk/media/webrtc/webrtccommon.h create mode 100644 talk/media/webrtc/webrtcexport.h create mode 100644 talk/media/webrtc/webrtcmediaengine.h create mode 100644 talk/media/webrtc/webrtcpassthroughrender.cc create mode 100644 talk/media/webrtc/webrtcpassthroughrender.h create mode 100644 talk/media/webrtc/webrtcpassthroughrender_unittest.cc create mode 100644 talk/media/webrtc/webrtcvideocapturer.cc create mode 100644 talk/media/webrtc/webrtcvideocapturer.h create mode 100644 talk/media/webrtc/webrtcvideocapturer_unittest.cc create mode 100644 talk/media/webrtc/webrtcvideodecoderfactory.h create mode 100644 talk/media/webrtc/webrtcvideoencoderfactory.h create mode 100644 talk/media/webrtc/webrtcvideoengine.cc create mode 100644 talk/media/webrtc/webrtcvideoengine.h create mode 100644 talk/media/webrtc/webrtcvideoengine_unittest.cc create mode 100644 talk/media/webrtc/webrtcvideoframe.cc create mode 100644 talk/media/webrtc/webrtcvideoframe.h create mode 100644 talk/media/webrtc/webrtcvideoframe_unittest.cc create mode 100644 talk/media/webrtc/webrtcvie.h create mode 100644 talk/media/webrtc/webrtcvoe.h create mode 100644 talk/media/webrtc/webrtcvoiceengine.cc create mode 100644 talk/media/webrtc/webrtcvoiceengine.h create mode 100644 talk/media/webrtc/webrtcvoiceengine_unittest.cc create mode 100644 talk/p2p/base/asyncstuntcpsocket.cc create mode 100644 talk/p2p/base/asyncstuntcpsocket.h create mode 100644 talk/p2p/base/asyncstuntcpsocket_unittest.cc create mode 100644 talk/p2p/base/basicpacketsocketfactory.cc create mode 100644 talk/p2p/base/basicpacketsocketfactory.h create mode 100644 talk/p2p/base/candidate.h create mode 100644 talk/p2p/base/common.h create mode 100644 talk/p2p/base/constants.cc create mode 100644 talk/p2p/base/constants.h create mode 100644 talk/p2p/base/dtlstransport.h create mode 100644 talk/p2p/base/dtlstransportchannel.cc create mode 100644 talk/p2p/base/dtlstransportchannel.h create mode 100644 talk/p2p/base/dtlstransportchannel_unittest.cc create mode 100644 talk/p2p/base/fakesession.h create mode 100644 talk/p2p/base/p2ptransport.cc create mode 100644 talk/p2p/base/p2ptransport.h create mode 100644 talk/p2p/base/p2ptransportchannel.cc create mode 100644 talk/p2p/base/p2ptransportchannel.h create mode 100644 talk/p2p/base/p2ptransportchannel_unittest.cc create mode 100644 talk/p2p/base/packetsocketfactory.h create mode 100644 talk/p2p/base/parsing.cc create mode 100644 talk/p2p/base/parsing.h create mode 100644 talk/p2p/base/port.cc create mode 100644 talk/p2p/base/port.h create mode 100644 talk/p2p/base/port_unittest.cc create mode 100644 talk/p2p/base/portallocator.cc create mode 100644 talk/p2p/base/portallocator.h create mode 100644 talk/p2p/base/portallocatorsessionproxy.cc create mode 100644 talk/p2p/base/portallocatorsessionproxy.h create mode 100644 talk/p2p/base/portallocatorsessionproxy_unittest.cc create mode 100644 talk/p2p/base/portinterface.h create mode 100644 talk/p2p/base/portproxy.cc create mode 100644 talk/p2p/base/portproxy.h create mode 100644 talk/p2p/base/pseudotcp.cc create mode 100644 talk/p2p/base/pseudotcp.h create mode 100644 talk/p2p/base/pseudotcp_unittest.cc create mode 100644 talk/p2p/base/rawtransport.cc create mode 100644 talk/p2p/base/rawtransport.h create mode 100644 talk/p2p/base/rawtransportchannel.cc create mode 100644 talk/p2p/base/rawtransportchannel.h create mode 100644 talk/p2p/base/relayport.cc create mode 100644 talk/p2p/base/relayport.h create mode 100644 talk/p2p/base/relayport_unittest.cc create mode 100644 talk/p2p/base/relayserver.cc create mode 100644 talk/p2p/base/relayserver.h create mode 100644 talk/p2p/base/relayserver_main.cc create mode 100644 talk/p2p/base/relayserver_unittest.cc create mode 100644 talk/p2p/base/session.cc create mode 100644 talk/p2p/base/session.h create mode 100644 talk/p2p/base/session_unittest.cc create mode 100644 talk/p2p/base/sessionclient.h create mode 100644 talk/p2p/base/sessiondescription.cc create mode 100644 talk/p2p/base/sessiondescription.h create mode 100644 talk/p2p/base/sessionid.h create mode 100644 talk/p2p/base/sessionmanager.cc create mode 100644 talk/p2p/base/sessionmanager.h create mode 100644 talk/p2p/base/sessionmessages.cc create mode 100644 talk/p2p/base/sessionmessages.h create mode 100644 talk/p2p/base/stun.cc create mode 100644 talk/p2p/base/stun.h create mode 100644 talk/p2p/base/stun_unittest.cc create mode 100644 talk/p2p/base/stunport.cc create mode 100644 talk/p2p/base/stunport.h create mode 100644 talk/p2p/base/stunport_unittest.cc create mode 100644 talk/p2p/base/stunrequest.cc create mode 100644 talk/p2p/base/stunrequest.h create mode 100644 talk/p2p/base/stunrequest_unittest.cc create mode 100644 talk/p2p/base/stunserver.cc create mode 100644 talk/p2p/base/stunserver.h create mode 100644 talk/p2p/base/stunserver_main.cc create mode 100644 talk/p2p/base/stunserver_unittest.cc create mode 100644 talk/p2p/base/tcpport.cc create mode 100644 talk/p2p/base/tcpport.h create mode 100644 talk/p2p/base/testrelayserver.h create mode 100644 talk/p2p/base/teststunserver.h create mode 100644 talk/p2p/base/testturnserver.h create mode 100644 talk/p2p/base/transport.cc create mode 100644 talk/p2p/base/transport.h create mode 100644 talk/p2p/base/transport_unittest.cc create mode 100644 talk/p2p/base/transportchannel.cc create mode 100644 talk/p2p/base/transportchannel.h create mode 100644 talk/p2p/base/transportchannelimpl.h create mode 100644 talk/p2p/base/transportchannelproxy.cc create mode 100644 talk/p2p/base/transportchannelproxy.h create mode 100644 talk/p2p/base/transportdescription.h create mode 100644 talk/p2p/base/transportdescriptionfactory.cc create mode 100644 talk/p2p/base/transportdescriptionfactory.h create mode 100644 talk/p2p/base/transportdescriptionfactory_unittest.cc create mode 100644 talk/p2p/base/transportinfo.h create mode 100644 talk/p2p/base/turnport.cc create mode 100644 talk/p2p/base/turnport.h create mode 100644 talk/p2p/base/turnport_unittest.cc create mode 100644 talk/p2p/base/turnserver.cc create mode 100644 talk/p2p/base/turnserver.h create mode 100644 talk/p2p/base/turnserver_main.cc create mode 100644 talk/p2p/base/udpport.h create mode 100644 talk/p2p/client/autoportallocator.h create mode 100644 talk/p2p/client/basicportallocator.cc create mode 100644 talk/p2p/client/basicportallocator.h create mode 100644 talk/p2p/client/connectivitychecker.cc create mode 100644 talk/p2p/client/connectivitychecker.h create mode 100644 talk/p2p/client/connectivitychecker_unittest.cc create mode 100644 talk/p2p/client/fakeportallocator.h create mode 100644 talk/p2p/client/httpportallocator.cc create mode 100644 talk/p2p/client/httpportallocator.h create mode 100644 talk/p2p/client/portallocator_unittest.cc create mode 100644 talk/p2p/client/sessionmanagertask.h create mode 100644 talk/p2p/client/sessionsendtask.h create mode 100644 talk/p2p/client/socketmonitor.cc create mode 100644 talk/p2p/client/socketmonitor.h create mode 100644 talk/session/media/audiomonitor.cc create mode 100644 talk/session/media/audiomonitor.h create mode 100644 talk/session/media/call.cc create mode 100644 talk/session/media/call.h create mode 100644 talk/session/media/channel.cc create mode 100644 talk/session/media/channel.h create mode 100644 talk/session/media/channel_unittest.cc create mode 100644 talk/session/media/channelmanager.cc create mode 100644 talk/session/media/channelmanager.h create mode 100644 talk/session/media/channelmanager_unittest.cc create mode 100644 talk/session/media/currentspeakermonitor.cc create mode 100644 talk/session/media/currentspeakermonitor.h create mode 100644 talk/session/media/currentspeakermonitor_unittest.cc create mode 100644 talk/session/media/mediamessages.cc create mode 100644 talk/session/media/mediamessages.h create mode 100644 talk/session/media/mediamessages_unittest.cc create mode 100644 talk/session/media/mediamonitor.cc create mode 100644 talk/session/media/mediamonitor.h create mode 100644 talk/session/media/mediarecorder.cc create mode 100644 talk/session/media/mediarecorder.h create mode 100644 talk/session/media/mediarecorder_unittest.cc create mode 100644 talk/session/media/mediasession.cc create mode 100644 talk/session/media/mediasession.h create mode 100644 talk/session/media/mediasession_unittest.cc create mode 100644 talk/session/media/mediasessionclient.cc create mode 100644 talk/session/media/mediasessionclient.h create mode 100644 talk/session/media/mediasessionclient_unittest.cc create mode 100644 talk/session/media/mediasink.h create mode 100644 talk/session/media/rtcpmuxfilter.cc create mode 100644 talk/session/media/rtcpmuxfilter.h create mode 100644 talk/session/media/rtcpmuxfilter_unittest.cc create mode 100644 talk/session/media/soundclip.cc create mode 100644 talk/session/media/soundclip.h create mode 100644 talk/session/media/srtpfilter.cc create mode 100644 talk/session/media/srtpfilter.h create mode 100644 talk/session/media/srtpfilter_unittest.cc create mode 100644 talk/session/media/ssrcmuxfilter.cc create mode 100644 talk/session/media/ssrcmuxfilter.h create mode 100644 talk/session/media/ssrcmuxfilter_unittest.cc create mode 100644 talk/session/media/typewrapping.h.pump create mode 100644 talk/session/media/typingmonitor.cc create mode 100644 talk/session/media/typingmonitor.h create mode 100644 talk/session/media/typingmonitor_unittest.cc create mode 100644 talk/session/media/voicechannel.h create mode 100644 talk/session/tunnel/pseudotcpchannel.cc create mode 100644 talk/session/tunnel/pseudotcpchannel.h create mode 100644 talk/session/tunnel/securetunnelsessionclient.cc create mode 100644 talk/session/tunnel/securetunnelsessionclient.h create mode 100644 talk/session/tunnel/tunnelsessionclient.cc create mode 100644 talk/session/tunnel/tunnelsessionclient.h create mode 100644 talk/session/tunnel/tunnelsessionclient_unittest.cc create mode 100644 talk/site_scons/site_tools/talk_linux.py create mode 100644 talk/site_scons/site_tools/talk_noops.py create mode 100644 talk/site_scons/talk.py create mode 100644 talk/sound/alsasoundsystem.cc create mode 100644 talk/sound/alsasoundsystem.h create mode 100644 talk/sound/alsasymboltable.cc create mode 100644 talk/sound/alsasymboltable.h create mode 100644 talk/sound/automaticallychosensoundsystem.h create mode 100644 talk/sound/automaticallychosensoundsystem_unittest.cc create mode 100644 talk/sound/linuxsoundsystem.cc create mode 100644 talk/sound/linuxsoundsystem.h create mode 100644 talk/sound/nullsoundsystem.cc create mode 100644 talk/sound/nullsoundsystem.h create mode 100644 talk/sound/nullsoundsystemfactory.cc create mode 100644 talk/sound/nullsoundsystemfactory.h create mode 100644 talk/sound/platformsoundsystem.cc create mode 100644 talk/sound/platformsoundsystem.h create mode 100644 talk/sound/platformsoundsystemfactory.cc create mode 100644 talk/sound/platformsoundsystemfactory.h create mode 100644 talk/sound/pulseaudiosoundsystem.cc create mode 100644 talk/sound/pulseaudiosoundsystem.h create mode 100644 talk/sound/pulseaudiosymboltable.cc create mode 100644 talk/sound/pulseaudiosymboltable.h create mode 100644 talk/sound/sounddevicelocator.h create mode 100644 talk/sound/soundinputstreaminterface.h create mode 100644 talk/sound/soundoutputstreaminterface.h create mode 100644 talk/sound/soundsystemfactory.h create mode 100644 talk/sound/soundsysteminterface.cc create mode 100644 talk/sound/soundsysteminterface.h create mode 100644 talk/sound/soundsystemproxy.cc create mode 100644 talk/sound/soundsystemproxy.h create mode 100644 talk/third_party/libudev/libudev.h create mode 100644 talk/xmllite/qname.cc create mode 100644 talk/xmllite/qname.h create mode 100644 talk/xmllite/qname_unittest.cc create mode 100644 talk/xmllite/xmlbuilder.cc create mode 100644 talk/xmllite/xmlbuilder.h create mode 100644 talk/xmllite/xmlbuilder_unittest.cc create mode 100644 talk/xmllite/xmlconstants.cc create mode 100644 talk/xmllite/xmlconstants.h create mode 100644 talk/xmllite/xmlelement.cc create mode 100644 talk/xmllite/xmlelement.h create mode 100644 talk/xmllite/xmlelement_unittest.cc create mode 100644 talk/xmllite/xmlnsstack.cc create mode 100644 talk/xmllite/xmlnsstack.h create mode 100644 talk/xmllite/xmlnsstack_unittest.cc create mode 100644 talk/xmllite/xmlparser.cc create mode 100644 talk/xmllite/xmlparser.h create mode 100644 talk/xmllite/xmlparser_unittest.cc create mode 100644 talk/xmllite/xmlprinter.cc create mode 100644 talk/xmllite/xmlprinter.h create mode 100644 talk/xmllite/xmlprinter_unittest.cc create mode 100644 talk/xmpp/asyncsocket.h create mode 100644 talk/xmpp/chatroommodule.h create mode 100644 talk/xmpp/chatroommodule_unittest.cc create mode 100644 talk/xmpp/chatroommoduleimpl.cc create mode 100644 talk/xmpp/constants.cc create mode 100644 talk/xmpp/constants.h create mode 100644 talk/xmpp/discoitemsquerytask.cc create mode 100644 talk/xmpp/discoitemsquerytask.h create mode 100644 talk/xmpp/fakexmppclient.h create mode 100644 talk/xmpp/hangoutpubsubclient.cc create mode 100644 talk/xmpp/hangoutpubsubclient.h create mode 100644 talk/xmpp/hangoutpubsubclient_unittest.cc create mode 100644 talk/xmpp/iqtask.cc create mode 100644 talk/xmpp/iqtask.h create mode 100644 talk/xmpp/jid.cc create mode 100644 talk/xmpp/jid.h create mode 100644 talk/xmpp/jid_unittest.cc create mode 100644 talk/xmpp/jingleinfotask.cc create mode 100644 talk/xmpp/jingleinfotask.h create mode 100644 talk/xmpp/module.h create mode 100644 talk/xmpp/moduleimpl.cc create mode 100644 talk/xmpp/moduleimpl.h create mode 100644 talk/xmpp/mucroomconfigtask.cc create mode 100644 talk/xmpp/mucroomconfigtask.h create mode 100644 talk/xmpp/mucroomconfigtask_unittest.cc create mode 100644 talk/xmpp/mucroomdiscoverytask.cc create mode 100644 talk/xmpp/mucroomdiscoverytask.h create mode 100644 talk/xmpp/mucroomdiscoverytask_unittest.cc create mode 100644 talk/xmpp/mucroomlookuptask.cc create mode 100644 talk/xmpp/mucroomlookuptask.h create mode 100644 talk/xmpp/mucroomlookuptask_unittest.cc create mode 100644 talk/xmpp/mucroomuniquehangoutidtask.cc create mode 100644 talk/xmpp/mucroomuniquehangoutidtask.h create mode 100644 talk/xmpp/mucroomuniquehangoutidtask_unittest.cc create mode 100644 talk/xmpp/pingtask.cc create mode 100644 talk/xmpp/pingtask.h create mode 100644 talk/xmpp/pingtask_unittest.cc create mode 100644 talk/xmpp/plainsaslhandler.h create mode 100644 talk/xmpp/presenceouttask.cc create mode 100644 talk/xmpp/presenceouttask.h create mode 100644 talk/xmpp/presencereceivetask.cc create mode 100644 talk/xmpp/presencereceivetask.h create mode 100644 talk/xmpp/presencestatus.cc create mode 100644 talk/xmpp/presencestatus.h create mode 100644 talk/xmpp/prexmppauth.h create mode 100644 talk/xmpp/pubsub_task.cc create mode 100644 talk/xmpp/pubsub_task.h create mode 100644 talk/xmpp/pubsubclient.cc create mode 100644 talk/xmpp/pubsubclient.h create mode 100644 talk/xmpp/pubsubclient_unittest.cc create mode 100644 talk/xmpp/pubsubtasks.cc create mode 100644 talk/xmpp/pubsubtasks.h create mode 100644 talk/xmpp/pubsubtasks_unittest.cc create mode 100644 talk/xmpp/receivetask.cc create mode 100644 talk/xmpp/receivetask.h create mode 100644 talk/xmpp/rostermodule.h create mode 100644 talk/xmpp/rostermodule_unittest.cc create mode 100644 talk/xmpp/rostermoduleimpl.cc create mode 100644 talk/xmpp/rostermoduleimpl.h create mode 100644 talk/xmpp/saslcookiemechanism.h create mode 100644 talk/xmpp/saslhandler.h create mode 100644 talk/xmpp/saslmechanism.cc create mode 100644 talk/xmpp/saslmechanism.h create mode 100644 talk/xmpp/saslplainmechanism.h create mode 100644 talk/xmpp/util_unittest.cc create mode 100644 talk/xmpp/util_unittest.h create mode 100644 talk/xmpp/xmppauth.cc create mode 100644 talk/xmpp/xmppauth.h create mode 100644 talk/xmpp/xmppclient.cc create mode 100644 talk/xmpp/xmppclient.h create mode 100644 talk/xmpp/xmppclientsettings.h create mode 100644 talk/xmpp/xmppengine.h create mode 100644 talk/xmpp/xmppengine_unittest.cc create mode 100644 talk/xmpp/xmppengineimpl.cc create mode 100644 talk/xmpp/xmppengineimpl.h create mode 100644 talk/xmpp/xmppengineimpl_iq.cc create mode 100644 talk/xmpp/xmpplogintask.cc create mode 100644 talk/xmpp/xmpplogintask.h create mode 100644 talk/xmpp/xmpplogintask_unittest.cc create mode 100644 talk/xmpp/xmpppump.cc create mode 100644 talk/xmpp/xmpppump.h create mode 100644 talk/xmpp/xmppsocket.cc create mode 100644 talk/xmpp/xmppsocket.h create mode 100644 talk/xmpp/xmppstanzaparser.cc create mode 100644 talk/xmpp/xmppstanzaparser.h create mode 100644 talk/xmpp/xmppstanzaparser_unittest.cc create mode 100644 talk/xmpp/xmpptask.cc create mode 100644 talk/xmpp/xmpptask.h create mode 100644 talk/xmpp/xmppthread.cc create mode 100644 talk/xmpp/xmppthread.h diff --git a/talk/OWNERS b/talk/OWNERS new file mode 100644 index 000000000..f124b6850 --- /dev/null +++ b/talk/OWNERS @@ -0,0 +1 @@ +henrike@webrtc.org \ No newline at end of file diff --git a/talk/app/webrtc/audiotrack.cc b/talk/app/webrtc/audiotrack.cc new file mode 100644 index 000000000..5bfca4268 --- /dev/null +++ b/talk/app/webrtc/audiotrack.cc @@ -0,0 +1,53 @@ +/* + * libjingle + * Copyright 2004--2011, 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/app/webrtc/audiotrack.h" + +#include + +namespace webrtc { + +static const char kAudioTrackKind[] = "audio"; + +AudioTrack::AudioTrack(const std::string& label, + AudioSourceInterface* audio_source) + : MediaStreamTrack(label), + audio_source_(audio_source), + renderer_(new AudioTrackRenderer()) { +} + +std::string AudioTrack::kind() const { + return kAudioTrackKind; +} + +talk_base::scoped_refptr AudioTrack::Create( + const std::string& id, AudioSourceInterface* source) { + talk_base::RefCountedObject* track = + new talk_base::RefCountedObject(id, source); + return track; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/audiotrack.h b/talk/app/webrtc/audiotrack.h new file mode 100644 index 000000000..48098f5d8 --- /dev/null +++ b/talk/app/webrtc/audiotrack.h @@ -0,0 +1,66 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_APP_WEBRTC_AUDIOTRACK_H_ +#define TALK_APP_WEBRTC_AUDIOTRACK_H_ + +#include "talk/app/webrtc/audiotrackrenderer.h" +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/mediastreamtrack.h" +#include "talk/app/webrtc/notifier.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/scoped_ref_ptr.h" + +namespace webrtc { + +class AudioTrack : public MediaStreamTrack { + public: + static talk_base::scoped_refptr Create( + const std::string& id, AudioSourceInterface* source); + + virtual AudioSourceInterface* GetSource() const { + return audio_source_.get(); + } + + virtual cricket::AudioRenderer* FrameInput() { + return renderer_.get(); + } + + // Implement MediaStreamTrack + virtual std::string kind() const; + + protected: + AudioTrack(const std::string& label, AudioSourceInterface* audio_source); + + private: + talk_base::scoped_refptr audio_source_; + talk_base::scoped_ptr renderer_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_AUDIOTRACK_H_ diff --git a/talk/app/webrtc/audiotrackrenderer.cc b/talk/app/webrtc/audiotrackrenderer.cc new file mode 100644 index 000000000..c8ad52252 --- /dev/null +++ b/talk/app/webrtc/audiotrackrenderer.cc @@ -0,0 +1,48 @@ +/* + * libjingle + * Copyright 2013, 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/app/webrtc/audiotrackrenderer.h" +#include "talk/base/common.h" + +namespace webrtc { + +AudioTrackRenderer::AudioTrackRenderer() : channel_id_(-1) { +} + +AudioTrackRenderer::~AudioTrackRenderer() { +} + +void AudioTrackRenderer::SetChannelId(int channel_id) { + ASSERT(channel_id_ == -1); + channel_id_ = channel_id; +} + +int AudioTrackRenderer::GetChannelId() const { + return channel_id_; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/audiotrackrenderer.h b/talk/app/webrtc/audiotrackrenderer.h new file mode 100644 index 000000000..55de04ea9 --- /dev/null +++ b/talk/app/webrtc/audiotrackrenderer.h @@ -0,0 +1,55 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#ifndef TALK_APP_WEBRTC_AUDIOTRACKRENDERER_H_ +#define TALK_APP_WEBRTC_AUDIOTRACKRENDERER_H_ + +#include "talk/media/base/audiorenderer.h" + +namespace webrtc { + +// Class used for AudioTrack to get the ID of WebRtc voice channel that +// the AudioTrack is connecting to. +// Each AudioTrack owns a AudioTrackRenderer instance. +// SetChannelID() should be called only when a AudioTrack is added to a +// MediaStream and should not be changed afterwards. +class AudioTrackRenderer : public cricket::AudioRenderer { + public: + AudioTrackRenderer(); + ~AudioTrackRenderer(); + + // Implements cricket::AudioRenderer. + virtual void SetChannelId(int channel_id); + virtual int GetChannelId() const; + + private: + int channel_id_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_AUDIOTRACKRENDERER_H_ diff --git a/talk/app/webrtc/datachannel.cc b/talk/app/webrtc/datachannel.cc new file mode 100644 index 000000000..345cd5f10 --- /dev/null +++ b/talk/app/webrtc/datachannel.cc @@ -0,0 +1,295 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/datachannel.h" + +#include + +#include "talk/app/webrtc/webrtcsession.h" +#include "talk/base/logging.h" +#include "talk/base/refcount.h" + +namespace webrtc { + +static size_t kMaxQueuedDataPackets = 100; + +talk_base::scoped_refptr DataChannel::Create( + WebRtcSession* session, + const std::string& label, + const DataChannelInit* config) { + talk_base::scoped_refptr channel( + new talk_base::RefCountedObject(session, label)); + if (!channel->Init(config)) { + return NULL; + } + return channel; +} + +DataChannel::DataChannel(WebRtcSession* session, const std::string& label) + : label_(label), + observer_(NULL), + state_(kConnecting), + was_ever_writable_(false), + session_(session), + data_session_(NULL), + send_ssrc_set_(false), + send_ssrc_(0), + receive_ssrc_set_(false), + receive_ssrc_(0) { +} + +bool DataChannel::Init(const DataChannelInit* config) { + if (config) { + if (session_->data_channel_type() == cricket::DCT_RTP && + (config->reliable || + config->id != -1 || + config->maxRetransmits != -1 || + config->maxRetransmitTime != -1)) { + LOG(LS_ERROR) << "Failed to initialize the RTP data channel due to " + << "invalid DataChannelInit."; + return false; + } else if (session_->data_channel_type() == cricket::DCT_SCTP) { + if (config->id < -1 || + config->maxRetransmits < -1 || + config->maxRetransmitTime < -1) { + LOG(LS_ERROR) << "Failed to initialize the SCTP data channel due to " + << "invalid DataChannelInit."; + return false; + } + if (config->maxRetransmits != -1 && config->maxRetransmitTime != -1) { + LOG(LS_ERROR) << + "maxRetransmits and maxRetransmitTime should not be both set."; + return false; + } + } + config_ = *config; + } + return true; +} + +bool DataChannel::HasNegotiationCompleted() { + return send_ssrc_set_ == receive_ssrc_set_; +} + +DataChannel::~DataChannel() { + ClearQueuedData(); +} + +void DataChannel::RegisterObserver(DataChannelObserver* observer) { + observer_ = observer; + DeliverQueuedData(); +} + +void DataChannel::UnregisterObserver() { + observer_ = NULL; +} + +bool DataChannel::reliable() const { + if (session_->data_channel_type() == cricket::DCT_RTP) { + return false; + } else { + return config_.maxRetransmits == -1 && + config_.maxRetransmitTime == -1; + } +} + +uint64 DataChannel::buffered_amount() const { + return 0; +} + +void DataChannel::Close() { + if (state_ == kClosed) + return; + send_ssrc_ = 0; + send_ssrc_set_ = false; + SetState(kClosing); + UpdateState(); +} + +bool DataChannel::Send(const DataBuffer& buffer) { + if (state_ != kOpen) { + return false; + } + cricket::SendDataParams send_params; + + send_params.ssrc = send_ssrc_; + if (session_->data_channel_type() == cricket::DCT_SCTP) { + send_params.ordered = config_.ordered; + send_params.max_rtx_count = config_.maxRetransmits; + send_params.max_rtx_ms = config_.maxRetransmitTime; + } + send_params.type = buffer.binary ? cricket::DMT_BINARY : cricket::DMT_TEXT; + + cricket::SendDataResult send_result; + // TODO(pthatcher): Use send_result.would_block for buffering. + return session_->data_channel()->SendData( + send_params, buffer.data, &send_result); +} + +void DataChannel::SetReceiveSsrc(uint32 receive_ssrc) { + if (receive_ssrc_set_) { + ASSERT(session_->data_channel_type() == cricket::DCT_RTP || + receive_ssrc_ == send_ssrc_); + return; + } + receive_ssrc_ = receive_ssrc; + receive_ssrc_set_ = true; + UpdateState(); +} + +// The remote peer request that this channel shall be closed. +void DataChannel::RemotePeerRequestClose() { + DoClose(); +} + +void DataChannel::SetSendSsrc(uint32 send_ssrc) { + if (send_ssrc_set_) { + ASSERT(session_->data_channel_type() == cricket::DCT_RTP || + receive_ssrc_ == send_ssrc_); + return; + } + send_ssrc_ = send_ssrc; + send_ssrc_set_ = true; + UpdateState(); +} + +// The underlaying data engine is closing. +// This function make sure the DataChannel is disconneced and change state to +// kClosed. +void DataChannel::OnDataEngineClose() { + DoClose(); +} + +void DataChannel::DoClose() { + receive_ssrc_set_ = false; + send_ssrc_set_ = false; + SetState(kClosing); + UpdateState(); +} + +void DataChannel::UpdateState() { + switch (state_) { + case kConnecting: { + if (HasNegotiationCompleted()) { + if (!IsConnectedToDataSession()) { + ConnectToDataSession(); + } + if (was_ever_writable_) { + SetState(kOpen); + // If we have received buffers before the channel got writable. + // Deliver them now. + DeliverQueuedData(); + } + } + break; + } + case kOpen: { + break; + } + case kClosing: { + if (IsConnectedToDataSession()) { + DisconnectFromDataSession(); + } + if (HasNegotiationCompleted()) { + SetState(kClosed); + } + break; + } + case kClosed: + break; + } +} + +void DataChannel::SetState(DataState state) { + state_ = state; + if (observer_) { + observer_->OnStateChange(); + } +} + +void DataChannel::ConnectToDataSession() { + ASSERT(session_->data_channel() != NULL); + if (!session_->data_channel()) { + LOG(LS_ERROR) << "The DataEngine does not exist."; + return; + } + + data_session_ = session_->data_channel(); + data_session_->SignalReadyToSendData.connect(this, + &DataChannel::OnChannelReady); + data_session_->SignalDataReceived.connect(this, &DataChannel::OnDataReceived); +} + +void DataChannel::DisconnectFromDataSession() { + data_session_->SignalReadyToSendData.disconnect(this); + data_session_->SignalDataReceived.disconnect(this); + data_session_ = NULL; +} + +void DataChannel::DeliverQueuedData() { + if (was_ever_writable_ && observer_) { + while (!queued_data_.empty()) { + DataBuffer* buffer = queued_data_.front(); + observer_->OnMessage(*buffer); + queued_data_.pop(); + delete buffer; + } + } +} + +void DataChannel::ClearQueuedData() { + while (!queued_data_.empty()) { + DataBuffer* buffer = queued_data_.front(); + queued_data_.pop(); + delete buffer; + } +} + +void DataChannel::OnDataReceived(cricket::DataChannel* channel, + const cricket::ReceiveDataParams& params, + const talk_base::Buffer& payload) { + if (params.ssrc == receive_ssrc_) { + bool binary = false; + talk_base::scoped_ptr buffer(new DataBuffer(payload, binary)); + if (was_ever_writable_ && observer_) { + observer_->OnMessage(*buffer.get()); + } else { + if (queued_data_.size() > kMaxQueuedDataPackets) { + ClearQueuedData(); + } + queued_data_.push(buffer.release()); + } + } +} + +void DataChannel::OnChannelReady(bool writable) { + if (!was_ever_writable_ && writable) { + was_ever_writable_ = true; + UpdateState(); + } +} + +} // namespace webrtc diff --git a/talk/app/webrtc/datachannel.h b/talk/app/webrtc/datachannel.h new file mode 100644 index 000000000..c79c491c9 --- /dev/null +++ b/talk/app/webrtc/datachannel.h @@ -0,0 +1,154 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_DATACHANNEL_H_ +#define TALK_APP_WEBRTC_DATACHANNEL_H_ + +#include +#include + +#include "talk/app/webrtc/datachannelinterface.h" +#include "talk/app/webrtc/proxy.h" +#include "talk/base/scoped_ref_ptr.h" +#include "talk/base/sigslot.h" +#include "talk/session/media/channel.h" + +namespace webrtc { + +class WebRtcSession; + +// DataChannel is a an implementation of the DataChannelInterface based on +// libjingle's data engine. It provides an implementation of unreliable data +// channels. Currently this class is specifically designed to use RtpDataEngine, +// and will changed to use SCTP in the future. + +// DataChannel states: +// kConnecting: The channel has been created but SSRC for sending and receiving +// has not yet been set and the transport might not yet be ready. +// kOpen: The channel have a local SSRC set by a call to UpdateSendSsrc +// and a remote SSRC set by call to UpdateReceiveSsrc and the transport +// has been writable once. +// kClosing: DataChannelInterface::Close has been called or UpdateReceiveSsrc +// has been called with SSRC==0 +// kClosed: Both UpdateReceiveSsrc and UpdateSendSsrc has been called with +// SSRC==0. +class DataChannel : public DataChannelInterface, + public sigslot::has_slots<> { + public: + static talk_base::scoped_refptr Create( + WebRtcSession* session, + const std::string& label, + const DataChannelInit* config); + + virtual void RegisterObserver(DataChannelObserver* observer); + virtual void UnregisterObserver(); + + virtual std::string label() const { return label_; } + virtual bool reliable() const; + virtual int id() const { return config_.id; } + virtual uint64 buffered_amount() const; + virtual void Close(); + virtual DataState state() const { return state_; } + virtual bool Send(const DataBuffer& buffer); + + // Set the SSRC this channel should use to receive data from the + // underlying data engine. + void SetReceiveSsrc(uint32 receive_ssrc); + // The remote peer request that this channel should be closed. + void RemotePeerRequestClose(); + + // Set the SSRC this channel should use to send data on the + // underlying data engine. |send_ssrc| == 0 means that the channel is no + // longer part of the session negotiation. + void SetSendSsrc(uint32 send_ssrc); + + // Called if the underlying data engine is closing. + void OnDataEngineClose(); + + protected: + DataChannel(WebRtcSession* session, const std::string& label); + virtual ~DataChannel(); + + bool Init(const DataChannelInit* config); + bool HasNegotiationCompleted(); + + // Sigslots from cricket::DataChannel + void OnDataReceived(cricket::DataChannel* channel, + const cricket::ReceiveDataParams& params, + const talk_base::Buffer& payload); + void OnChannelReady(bool writable); + + private: + void DoClose(); + void UpdateState(); + void SetState(DataState state); + void ConnectToDataSession(); + void DisconnectFromDataSession(); + bool IsConnectedToDataSession() { return data_session_ != NULL; } + void DeliverQueuedData(); + void ClearQueuedData(); + + std::string label_; + DataChannelInit config_; + DataChannelObserver* observer_; + DataState state_; + bool was_ever_writable_; + WebRtcSession* session_; + cricket::DataChannel* data_session_; + bool send_ssrc_set_; + uint32 send_ssrc_; + bool receive_ssrc_set_; + uint32 receive_ssrc_; + std::queue queued_data_; +}; + +class DataChannelFactory { + public: + virtual talk_base::scoped_refptr CreateDataChannel( + const std::string& label, + const DataChannelInit* config) = 0; + + protected: + virtual ~DataChannelFactory() {} +}; + +// Define proxy for DataChannelInterface. +BEGIN_PROXY_MAP(DataChannel) + PROXY_METHOD1(void, RegisterObserver, DataChannelObserver*) + PROXY_METHOD0(void, UnregisterObserver) + PROXY_CONSTMETHOD0(std::string, label) + PROXY_CONSTMETHOD0(bool, reliable) + PROXY_CONSTMETHOD0(int, id) + PROXY_CONSTMETHOD0(DataState, state) + PROXY_CONSTMETHOD0(uint64, buffered_amount) + PROXY_METHOD0(void, Close) + PROXY_METHOD1(bool, Send, const DataBuffer&) +END_PROXY() + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_DATACHANNEL_H_ diff --git a/talk/app/webrtc/datachannelinterface.h b/talk/app/webrtc/datachannelinterface.h new file mode 100644 index 000000000..9c66a50fb --- /dev/null +++ b/talk/app/webrtc/datachannelinterface.h @@ -0,0 +1,127 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// This file contains interfaces for DataChannels +// http://dev.w3.org/2011/webrtc/editor/webrtc.html#rtcdatachannel + +#ifndef TALK_APP_WEBRTC_DATACHANNELINTERFACE_H_ +#define TALK_APP_WEBRTC_DATACHANNELINTERFACE_H_ + +#include + +#include "talk/base/basictypes.h" +#include "talk/base/buffer.h" +#include "talk/base/refcount.h" + + +namespace webrtc { + +struct DataChannelInit { + DataChannelInit() + : reliable(false), + ordered(true), + maxRetransmitTime(-1), + maxRetransmits(-1), + negotiated(false), + id(-1) { + } + + bool reliable; // Deprecated. + bool ordered; // True if ordered delivery is required. + int maxRetransmitTime; // The max period of time in milliseconds in which + // retransmissions will be sent. After this time, no + // more retransmissions will be sent. -1 if unset. + int maxRetransmits; // The max number of retransmissions. -1 if unset. + std::string protocol; // This is set by the application and opaque to the + // WebRTC implementation. + bool negotiated; // True if the channel has been externally negotiated + // and we do not send an in-band signalling in the + // form of an "open" message. + int id; // The stream id, or SID, for SCTP data channels. -1 + // if unset. +}; + +struct DataBuffer { + DataBuffer(const talk_base::Buffer& data, bool binary) + : data(data), + binary(binary) { + } + // For convenience for unit tests. + explicit DataBuffer(const std::string& text) + : data(text.data(), text.length()), + binary(false) { + } + talk_base::Buffer data; + // Indicates if the received data contains UTF-8 or binary data. + // Note that the upper layers are left to verify the UTF-8 encoding. + // TODO(jiayl): prefer to use an enum instead of a bool. + bool binary; +}; + +class DataChannelObserver { + public: + // The data channel state have changed. + virtual void OnStateChange() = 0; + // A data buffer was successfully received. + virtual void OnMessage(const DataBuffer& buffer) = 0; + + protected: + virtual ~DataChannelObserver() {} +}; + +class DataChannelInterface : public talk_base::RefCountInterface { + public: + enum DataState { + kConnecting, + kOpen, // The DataChannel is ready to send data. + kClosing, + kClosed + }; + + virtual void RegisterObserver(DataChannelObserver* observer) = 0; + virtual void UnregisterObserver() = 0; + // The label attribute represents a label that can be used to distinguish this + // DataChannel object from other DataChannel objects. + virtual std::string label() const = 0; + virtual bool reliable() const = 0; + virtual int id() const = 0; + virtual DataState state() const = 0; + // The buffered_amount returns the number of bytes of application data + // (UTF-8 text and binary data) that have been queued using SendBuffer but + // have not yet been transmitted to the network. + virtual uint64 buffered_amount() const = 0; + virtual void Close() = 0; + // Sends |data| to the remote peer. + virtual bool Send(const DataBuffer& buffer) = 0; + + protected: + virtual ~DataChannelInterface() {} +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_DATACHANNELINTERFACE_H_ diff --git a/talk/app/webrtc/dtmfsender.cc b/talk/app/webrtc/dtmfsender.cc new file mode 100644 index 000000000..6556acdbb --- /dev/null +++ b/talk/app/webrtc/dtmfsender.cc @@ -0,0 +1,257 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/dtmfsender.h" + +#include + +#include + +#include "talk/base/logging.h" +#include "talk/base/thread.h" + +namespace webrtc { + +enum { + MSG_DO_INSERT_DTMF = 0, +}; + +// RFC4733 +// +-------+--------+------+---------+ +// | Event | Code | Type | Volume? | +// +-------+--------+------+---------+ +// | 0--9 | 0--9 | tone | yes | +// | * | 10 | tone | yes | +// | # | 11 | tone | yes | +// | A--D | 12--15 | tone | yes | +// +-------+--------+------+---------+ +// The "," is a special event defined by the WebRTC spec. It means to delay for +// 2 seconds before processing the next tone. We use -1 as its code. +static const int kDtmfCodeTwoSecondDelay = -1; +static const int kDtmfTwoSecondInMs = 2000; +static const char kDtmfValidTones[] = ",0123456789*#ABCDabcd"; +static const char kDtmfTonesTable[] = ",0123456789*#ABCD"; +// The duration cannot be more than 6000ms or less than 70ms. The gap between +// tones must be at least 50 ms. +static const int kDtmfDefaultDurationMs = 100; +static const int kDtmfMinDurationMs = 70; +static const int kDtmfMaxDurationMs = 6000; +static const int kDtmfDefaultGapMs = 50; +static const int kDtmfMinGapMs = 50; + +// Get DTMF code from the DTMF event character. +bool GetDtmfCode(char tone, int* code) { + // Convert a-d to A-D. + char event = toupper(tone); + const char* p = strchr(kDtmfTonesTable, event); + if (!p) { + return false; + } + *code = p - kDtmfTonesTable - 1; + return true; +} + +talk_base::scoped_refptr DtmfSender::Create( + AudioTrackInterface* track, + talk_base::Thread* signaling_thread, + DtmfProviderInterface* provider) { + if (!track || !signaling_thread) { + return NULL; + } + talk_base::scoped_refptr dtmf_sender( + new talk_base::RefCountedObject(track, signaling_thread, + provider)); + return dtmf_sender; +} + +DtmfSender::DtmfSender(AudioTrackInterface* track, + talk_base::Thread* signaling_thread, + DtmfProviderInterface* provider) + : track_(track), + observer_(NULL), + signaling_thread_(signaling_thread), + provider_(provider), + duration_(kDtmfDefaultDurationMs), + inter_tone_gap_(kDtmfDefaultGapMs) { + ASSERT(track_ != NULL); + ASSERT(signaling_thread_ != NULL); + if (provider_) { + ASSERT(provider_->GetOnDestroyedSignal() != NULL); + provider_->GetOnDestroyedSignal()->connect( + this, &DtmfSender::OnProviderDestroyed); + } +} + +DtmfSender::~DtmfSender() { + if (provider_) { + ASSERT(provider_->GetOnDestroyedSignal() != NULL); + provider_->GetOnDestroyedSignal()->disconnect(this); + } + StopSending(); +} + +void DtmfSender::RegisterObserver(DtmfSenderObserverInterface* observer) { + observer_ = observer; +} + +void DtmfSender::UnregisterObserver() { + observer_ = NULL; +} + +bool DtmfSender::CanInsertDtmf() { + ASSERT(signaling_thread_->IsCurrent()); + if (!provider_) { + return false; + } + return provider_->CanInsertDtmf(track_->id()); +} + +bool DtmfSender::InsertDtmf(const std::string& tones, int duration, + int inter_tone_gap) { + ASSERT(signaling_thread_->IsCurrent()); + + if (duration > kDtmfMaxDurationMs || + duration < kDtmfMinDurationMs || + inter_tone_gap < kDtmfMinGapMs) { + LOG(LS_ERROR) << "InsertDtmf is called with invalid duration or tones gap. " + << "The duration cannot be more than " << kDtmfMaxDurationMs + << "ms or less than " << kDtmfMinDurationMs << "ms. " + << "The gap between tones must be at least " << kDtmfMinGapMs << "ms."; + return false; + } + + if (!CanInsertDtmf()) { + LOG(LS_ERROR) + << "InsertDtmf is called on DtmfSender that can't send DTMF."; + return false; + } + + tones_ = tones; + duration_ = duration; + inter_tone_gap_ = inter_tone_gap; + // Clear the previous queue. + signaling_thread_->Clear(this, MSG_DO_INSERT_DTMF); + // Kick off a new DTMF task queue. + signaling_thread_->Post(this, MSG_DO_INSERT_DTMF); + return true; +} + +const AudioTrackInterface* DtmfSender::track() const { + return track_; +} + +std::string DtmfSender::tones() const { + return tones_; +} + +int DtmfSender::duration() const { + return duration_; +} + +int DtmfSender::inter_tone_gap() const { + return inter_tone_gap_; +} + +void DtmfSender::OnMessage(talk_base::Message* msg) { + switch (msg->message_id) { + case MSG_DO_INSERT_DTMF: { + DoInsertDtmf(); + break; + } + default: { + ASSERT(false); + break; + } + } +} + +void DtmfSender::DoInsertDtmf() { + ASSERT(signaling_thread_->IsCurrent()); + + // Get the first DTMF tone from the tone buffer. Unrecognized characters will + // be ignored and skipped. + size_t first_tone_pos = tones_.find_first_of(kDtmfValidTones); + int code = 0; + if (first_tone_pos == std::string::npos) { + tones_.clear(); + // Fire a “OnToneChange” event with an empty string and stop. + if (observer_) { + observer_->OnToneChange(std::string()); + } + return; + } else { + char tone = tones_[first_tone_pos]; + if (!GetDtmfCode(tone, &code)) { + // The find_first_of(kDtmfValidTones) should have guarantee |tone| is + // a valid DTMF tone. + ASSERT(false); + } + } + + int tone_gap = inter_tone_gap_; + if (code == kDtmfCodeTwoSecondDelay) { + // Special case defined by WebRTC - The character',' indicates a delay of 2 + // seconds before processing the next character in the tones parameter. + tone_gap = kDtmfTwoSecondInMs; + } else { + if (!provider_) { + LOG(LS_ERROR) << "The DtmfProvider has been destroyed."; + return; + } + // The provider starts playout of the given tone on the + // associated RTP media stream, using the appropriate codec. + if (!provider_->InsertDtmf(track_->id(), code, duration_)) { + LOG(LS_ERROR) << "The DtmfProvider can no longer send DTMF."; + return; + } + // Wait for the number of milliseconds specified by |duration_|. + tone_gap += duration_; + } + + // Fire a “OnToneChange” event with the tone that's just processed. + if (observer_) { + observer_->OnToneChange(tones_.substr(first_tone_pos, 1)); + } + + // Erase the unrecognized characters plus the tone that's just processed. + tones_.erase(0, first_tone_pos + 1); + + // Continue with the next tone. + signaling_thread_->PostDelayed(tone_gap, this, MSG_DO_INSERT_DTMF); +} + +void DtmfSender::OnProviderDestroyed() { + LOG(LS_INFO) << "The Dtmf provider is deleted. Clear the sending queue."; + StopSending(); + provider_ = NULL; +} + +void DtmfSender::StopSending() { + signaling_thread_->Clear(this); +} + +} // namespace webrtc diff --git a/talk/app/webrtc/dtmfsender.h b/talk/app/webrtc/dtmfsender.h new file mode 100644 index 000000000..f2bebdeb5 --- /dev/null +++ b/talk/app/webrtc/dtmfsender.h @@ -0,0 +1,138 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_DTMFSENDER_H_ +#define TALK_APP_WEBRTC_DTMFSENDER_H_ + +#include + +#include "talk/app/webrtc/dtmfsenderinterface.h" +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/proxy.h" +#include "talk/base/common.h" +#include "talk/base/messagehandler.h" +#include "talk/base/refcount.h" + +// DtmfSender is the native implementation of the RTCDTMFSender defined by +// the WebRTC W3C Editor's Draft. +// http://dev.w3.org/2011/webrtc/editor/webrtc.html + +namespace talk_base { +class Thread; +} + +namespace webrtc { + +// This interface is called by DtmfSender to talk to the actual audio channel +// to send DTMF. +class DtmfProviderInterface { + public: + // Returns true if the audio track with given id (|track_id|) is capable + // of sending DTMF. Otherwise returns false. + virtual bool CanInsertDtmf(const std::string& track_id) = 0; + // Sends DTMF |code| via the audio track with given id (|track_id|). + // The |duration| indicates the length of the DTMF tone in ms. + // Returns true on success and false on failure. + virtual bool InsertDtmf(const std::string& track_id, + int code, int duration) = 0; + // Returns a |sigslot::signal0<>| signal. The signal should fire before + // the provider is destroyed. + virtual sigslot::signal0<>* GetOnDestroyedSignal() = 0; + + protected: + virtual ~DtmfProviderInterface() {} +}; + +class DtmfSender + : public DtmfSenderInterface, + public sigslot::has_slots<>, + public talk_base::MessageHandler { + public: + static talk_base::scoped_refptr Create( + AudioTrackInterface* track, + talk_base::Thread* signaling_thread, + DtmfProviderInterface* provider); + + // Implements DtmfSenderInterface. + virtual void RegisterObserver(DtmfSenderObserverInterface* observer) OVERRIDE; + virtual void UnregisterObserver() OVERRIDE; + virtual bool CanInsertDtmf() OVERRIDE; + virtual bool InsertDtmf(const std::string& tones, int duration, + int inter_tone_gap) OVERRIDE; + virtual const AudioTrackInterface* track() const OVERRIDE; + virtual std::string tones() const OVERRIDE; + virtual int duration() const OVERRIDE; + virtual int inter_tone_gap() const OVERRIDE; + + protected: + DtmfSender(AudioTrackInterface* track, + talk_base::Thread* signaling_thread, + DtmfProviderInterface* provider); + virtual ~DtmfSender(); + + private: + DtmfSender(); + + // Implements MessageHandler. + virtual void OnMessage(talk_base::Message* msg); + + // The DTMF sending task. + void DoInsertDtmf(); + + void OnProviderDestroyed(); + + void StopSending(); + + talk_base::scoped_refptr track_; + DtmfSenderObserverInterface* observer_; + talk_base::Thread* signaling_thread_; + DtmfProviderInterface* provider_; + std::string tones_; + int duration_; + int inter_tone_gap_; + + DISALLOW_COPY_AND_ASSIGN(DtmfSender); +}; + +// Define proxy for DtmfSenderInterface. +BEGIN_PROXY_MAP(DtmfSender) + PROXY_METHOD1(void, RegisterObserver, DtmfSenderObserverInterface*) + PROXY_METHOD0(void, UnregisterObserver) + PROXY_METHOD0(bool, CanInsertDtmf) + PROXY_METHOD3(bool, InsertDtmf, const std::string&, int, int) + PROXY_CONSTMETHOD0(const AudioTrackInterface*, track) + PROXY_CONSTMETHOD0(std::string, tones) + PROXY_CONSTMETHOD0(int, duration) + PROXY_CONSTMETHOD0(int, inter_tone_gap) +END_PROXY() + +// Get DTMF code from the DTMF event character. +bool GetDtmfCode(char tone, int* code); + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_DTMFSENDER_H_ diff --git a/talk/app/webrtc/dtmfsender_unittest.cc b/talk/app/webrtc/dtmfsender_unittest.cc new file mode 100644 index 000000000..e1c3be9b1 --- /dev/null +++ b/talk/app/webrtc/dtmfsender_unittest.cc @@ -0,0 +1,356 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/dtmfsender.h" + +#include +#include +#include + +#include "talk/app/webrtc/audiotrack.h" +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/timeutils.h" + +using webrtc::AudioTrackInterface; +using webrtc::AudioTrack; +using webrtc::DtmfProviderInterface; +using webrtc::DtmfSender; +using webrtc::DtmfSenderObserverInterface; + +static const char kTestAudioLabel[] = "test_audio_track"; +static const int kMaxWaitMs = 3000; + +class FakeDtmfObserver : public DtmfSenderObserverInterface { + public: + FakeDtmfObserver() : completed_(false) {} + + // Implements DtmfSenderObserverInterface. + virtual void OnToneChange(const std::string& tone) OVERRIDE { + LOG(LS_VERBOSE) << "FakeDtmfObserver::OnToneChange '" << tone << "'."; + tones_.push_back(tone); + if (tone.empty()) { + completed_ = true; + } + } + + // getters + const std::vector& tones() const { + return tones_; + } + bool completed() const { + return completed_; + } + + private: + std::vector tones_; + bool completed_; +}; + +class FakeDtmfProvider : public DtmfProviderInterface { + public: + struct DtmfInfo { + DtmfInfo(int code, int duration, int gap) + : code(code), + duration(duration), + gap(gap) {} + int code; + int duration; + int gap; + }; + + FakeDtmfProvider() : last_insert_dtmf_call_(0) {} + + ~FakeDtmfProvider() { + SignalDestroyed(); + } + + // Implements DtmfProviderInterface. + virtual bool CanInsertDtmf(const std::string& track_label) OVERRIDE { + return (can_insert_dtmf_tracks_.count(track_label) != 0); + } + + virtual bool InsertDtmf(const std::string& track_label, + int code, int duration) OVERRIDE { + int gap = 0; + // TODO(ronghuawu): Make the timer (basically the talk_base::TimeNanos) + // mockable and use a fake timer in the unit tests. + if (last_insert_dtmf_call_ > 0) { + gap = static_cast(talk_base::Time() - last_insert_dtmf_call_); + } + last_insert_dtmf_call_ = talk_base::Time(); + + LOG(LS_VERBOSE) << "FakeDtmfProvider::InsertDtmf code=" << code + << " duration=" << duration + << " gap=" << gap << "."; + dtmf_info_queue_.push_back(DtmfInfo(code, duration, gap)); + return true; + } + + virtual sigslot::signal0<>* GetOnDestroyedSignal() { + return &SignalDestroyed; + } + + // getter and setter + const std::vector& dtmf_info_queue() const { + return dtmf_info_queue_; + } + + // helper functions + void AddCanInsertDtmfTrack(const std::string& label) { + can_insert_dtmf_tracks_.insert(label); + } + void RemoveCanInsertDtmfTrack(const std::string& label) { + can_insert_dtmf_tracks_.erase(label); + } + + private: + std::set can_insert_dtmf_tracks_; + std::vector dtmf_info_queue_; + int64 last_insert_dtmf_call_; + sigslot::signal0<> SignalDestroyed; +}; + +class DtmfSenderTest : public testing::Test { + protected: + DtmfSenderTest() + : track_(AudioTrack::Create(kTestAudioLabel, NULL)), + observer_(new talk_base::RefCountedObject()), + provider_(new FakeDtmfProvider()) { + provider_->AddCanInsertDtmfTrack(kTestAudioLabel); + dtmf_ = DtmfSender::Create(track_, talk_base::Thread::Current(), + provider_.get()); + dtmf_->RegisterObserver(observer_.get()); + } + + ~DtmfSenderTest() { + if (dtmf_.get()) { + dtmf_->UnregisterObserver(); + } + } + + // Constructs a list of DtmfInfo from |tones|, |duration| and + // |inter_tone_gap|. + void GetDtmfInfoFromString(const std::string& tones, int duration, + int inter_tone_gap, + std::vector* dtmfs) { + // Init extra_delay as -inter_tone_gap - duration to ensure the first + // DtmfInfo's gap field will be 0. + int extra_delay = -1 * (inter_tone_gap + duration); + + std::string::const_iterator it = tones.begin(); + for (; it != tones.end(); ++it) { + char tone = *it; + int code = 0; + webrtc::GetDtmfCode(tone, &code); + if (tone == ',') { + extra_delay = 2000; // 2 seconds + } else { + dtmfs->push_back(FakeDtmfProvider::DtmfInfo(code, duration, + duration + inter_tone_gap + extra_delay)); + extra_delay = 0; + } + } + } + + void VerifyExpectedState(AudioTrackInterface* track, + const std::string& tones, + int duration, int inter_tone_gap) { + EXPECT_EQ(track, dtmf_->track()); + EXPECT_EQ(tones, dtmf_->tones()); + EXPECT_EQ(duration, dtmf_->duration()); + EXPECT_EQ(inter_tone_gap, dtmf_->inter_tone_gap()); + } + + // Verify the provider got all the expected calls. + void VerifyOnProvider(const std::string& tones, int duration, + int inter_tone_gap) { + std::vector dtmf_queue_ref; + GetDtmfInfoFromString(tones, duration, inter_tone_gap, &dtmf_queue_ref); + VerifyOnProvider(dtmf_queue_ref); + } + + void VerifyOnProvider( + const std::vector& dtmf_queue_ref) { + const std::vector& dtmf_queue = + provider_->dtmf_info_queue(); + ASSERT_EQ(dtmf_queue_ref.size(), dtmf_queue.size()); + std::vector::const_iterator it_ref = + dtmf_queue_ref.begin(); + std::vector::const_iterator it = + dtmf_queue.begin(); + while (it_ref != dtmf_queue_ref.end() && it != dtmf_queue.end()) { + EXPECT_EQ(it_ref->code, it->code); + EXPECT_EQ(it_ref->duration, it->duration); + // Allow ~20ms error. + EXPECT_GE(it_ref->gap, it->gap - 20); + EXPECT_LE(it_ref->gap, it->gap + 20); + ++it_ref; + ++it; + } + } + + // Verify the observer got all the expected callbacks. + void VerifyOnObserver(const std::string& tones_ref) { + const std::vector& tones = observer_->tones(); + // The observer will get an empty string at the end. + EXPECT_EQ(tones_ref.size() + 1, tones.size()); + EXPECT_TRUE(tones.back().empty()); + std::string::const_iterator it_ref = tones_ref.begin(); + std::vector::const_iterator it = tones.begin(); + while (it_ref != tones_ref.end() && it != tones.end()) { + EXPECT_EQ(*it_ref, it->at(0)); + ++it_ref; + ++it; + } + } + + talk_base::scoped_refptr track_; + talk_base::scoped_ptr observer_; + talk_base::scoped_ptr provider_; + talk_base::scoped_refptr dtmf_; +}; + +TEST_F(DtmfSenderTest, CanInsertDtmf) { + EXPECT_TRUE(dtmf_->CanInsertDtmf()); + provider_->RemoveCanInsertDtmfTrack(kTestAudioLabel); + EXPECT_FALSE(dtmf_->CanInsertDtmf()); +} + +TEST_F(DtmfSenderTest, InsertDtmf) { + std::string tones = "@1%a&*$"; + int duration = 100; + int inter_tone_gap = 50; + EXPECT_TRUE(dtmf_->InsertDtmf(tones, duration, inter_tone_gap)); + EXPECT_TRUE_WAIT(observer_->completed(), kMaxWaitMs); + + // The unrecognized characters should be ignored. + std::string known_tones = "1a*"; + VerifyOnProvider(known_tones, duration, inter_tone_gap); + VerifyOnObserver(known_tones); +} + +TEST_F(DtmfSenderTest, InsertDtmfTwice) { + std::string tones1 = "12"; + std::string tones2 = "ab"; + int duration = 100; + int inter_tone_gap = 50; + EXPECT_TRUE(dtmf_->InsertDtmf(tones1, duration, inter_tone_gap)); + VerifyExpectedState(track_, tones1, duration, inter_tone_gap); + // Wait until the first tone got sent. + EXPECT_TRUE_WAIT(observer_->tones().size() == 1, kMaxWaitMs); + VerifyExpectedState(track_, "2", duration, inter_tone_gap); + // Insert with another tone buffer. + EXPECT_TRUE(dtmf_->InsertDtmf(tones2, duration, inter_tone_gap)); + VerifyExpectedState(track_, tones2, duration, inter_tone_gap); + // Wait until it's completed. + EXPECT_TRUE_WAIT(observer_->completed(), kMaxWaitMs); + + std::vector dtmf_queue_ref; + GetDtmfInfoFromString("1", duration, inter_tone_gap, &dtmf_queue_ref); + GetDtmfInfoFromString("ab", duration, inter_tone_gap, &dtmf_queue_ref); + VerifyOnProvider(dtmf_queue_ref); + VerifyOnObserver("1ab"); +} + +TEST_F(DtmfSenderTest, InsertDtmfWhileProviderIsDeleted) { + std::string tones = "@1%a&*$"; + int duration = 100; + int inter_tone_gap = 50; + EXPECT_TRUE(dtmf_->InsertDtmf(tones, duration, inter_tone_gap)); + // Wait until the first tone got sent. + EXPECT_TRUE_WAIT(observer_->tones().size() == 1, kMaxWaitMs); + // Delete provider. + provider_.reset(); + // The queue should be discontinued so no more tone callbacks. + WAIT(false, 200); + EXPECT_EQ(1U, observer_->tones().size()); +} + +TEST_F(DtmfSenderTest, InsertDtmfWhileSenderIsDeleted) { + std::string tones = "@1%a&*$"; + int duration = 100; + int inter_tone_gap = 50; + EXPECT_TRUE(dtmf_->InsertDtmf(tones, duration, inter_tone_gap)); + // Wait until the first tone got sent. + EXPECT_TRUE_WAIT(observer_->tones().size() == 1, kMaxWaitMs); + // Delete the sender. + dtmf_ = NULL; + // The queue should be discontinued so no more tone callbacks. + WAIT(false, 200); + EXPECT_EQ(1U, observer_->tones().size()); +} + +TEST_F(DtmfSenderTest, InsertEmptyTonesToCancelPreviousTask) { + std::string tones1 = "12"; + std::string tones2 = ""; + int duration = 100; + int inter_tone_gap = 50; + EXPECT_TRUE(dtmf_->InsertDtmf(tones1, duration, inter_tone_gap)); + // Wait until the first tone got sent. + EXPECT_TRUE_WAIT(observer_->tones().size() == 1, kMaxWaitMs); + // Insert with another tone buffer. + EXPECT_TRUE(dtmf_->InsertDtmf(tones2, duration, inter_tone_gap)); + // Wait until it's completed. + EXPECT_TRUE_WAIT(observer_->completed(), kMaxWaitMs); + + std::vector dtmf_queue_ref; + GetDtmfInfoFromString("1", duration, inter_tone_gap, &dtmf_queue_ref); + VerifyOnProvider(dtmf_queue_ref); + VerifyOnObserver("1"); +} + +TEST_F(DtmfSenderTest, InsertDtmfWithCommaAsDelay) { + std::string tones = "3,4"; + int duration = 100; + int inter_tone_gap = 50; + EXPECT_TRUE(dtmf_->InsertDtmf(tones, duration, inter_tone_gap)); + EXPECT_TRUE_WAIT(observer_->completed(), kMaxWaitMs); + + VerifyOnProvider(tones, duration, inter_tone_gap); + VerifyOnObserver(tones); +} + +TEST_F(DtmfSenderTest, TryInsertDtmfWhenItDoesNotWork) { + std::string tones = "3,4"; + int duration = 100; + int inter_tone_gap = 50; + provider_->RemoveCanInsertDtmfTrack(kTestAudioLabel); + EXPECT_FALSE(dtmf_->InsertDtmf(tones, duration, inter_tone_gap)); +} + +TEST_F(DtmfSenderTest, InsertDtmfWithInvalidDurationOrGap) { + std::string tones = "3,4"; + int duration = 100; + int inter_tone_gap = 50; + + EXPECT_FALSE(dtmf_->InsertDtmf(tones, 6001, inter_tone_gap)); + EXPECT_FALSE(dtmf_->InsertDtmf(tones, 69, inter_tone_gap)); + EXPECT_FALSE(dtmf_->InsertDtmf(tones, duration, 49)); + + EXPECT_TRUE(dtmf_->InsertDtmf(tones, duration, inter_tone_gap)); +} diff --git a/talk/app/webrtc/dtmfsenderinterface.h b/talk/app/webrtc/dtmfsenderinterface.h new file mode 100644 index 000000000..46f39245d --- /dev/null +++ b/talk/app/webrtc/dtmfsenderinterface.h @@ -0,0 +1,105 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_DTMFSENDERINTERFACE_H_ +#define TALK_APP_WEBRTC_DTMFSENDERINTERFACE_H_ + +#include + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/base/common.h" +#include "talk/base/refcount.h" + +// This file contains interfaces for DtmfSender. + +namespace webrtc { + +// DtmfSender callback interface. Application should implement this interface +// to get notifications from the DtmfSender. +class DtmfSenderObserverInterface { + public: + // Triggered when DTMF |tone| is sent. + // If |tone| is empty that means the DtmfSender has sent out all the given + // tones. + virtual void OnToneChange(const std::string& tone) = 0; + + protected: + virtual ~DtmfSenderObserverInterface() {} +}; + +// The interface of native implementation of the RTCDTMFSender defined by the +// WebRTC W3C Editor's Draft. +class DtmfSenderInterface : public talk_base::RefCountInterface { + public: + virtual void RegisterObserver(DtmfSenderObserverInterface* observer) = 0; + virtual void UnregisterObserver() = 0; + + // Returns true if this DtmfSender is capable of sending DTMF. + // Otherwise returns false. + virtual bool CanInsertDtmf() = 0; + + // Queues a task that sends the DTMF |tones|. The |tones| parameter is treated + // as a series of characters. The characters 0 through 9, A through D, #, and + // * generate the associated DTMF tones. The characters a to d are equivalent + // to A to D. The character ',' indicates a delay of 2 seconds before + // processing the next character in the tones parameter. + // Unrecognized characters are ignored. + // The |duration| parameter indicates the duration in ms to use for each + // character passed in the |tones| parameter. + // The duration cannot be more than 6000 or less than 70. + // The |inter_tone_gap| parameter indicates the gap between tones in ms. + // The |inter_tone_gap| must be at least 50 ms but should be as short as + // possible. + // If InsertDtmf is called on the same object while an existing task for this + // object to generate DTMF is still running, the previous task is canceled. + // Returns true on success and false on failure. + virtual bool InsertDtmf(const std::string& tones, int duration, + int inter_tone_gap) = 0; + + // Returns the track given as argument to the constructor. + virtual const AudioTrackInterface* track() const = 0; + + // Returns the tones remaining to be played out. + virtual std::string tones() const = 0; + + // Returns the current tone duration value in ms. + // This value will be the value last set via the InsertDtmf() method, or the + // default value of 100 ms if InsertDtmf() was never called. + virtual int duration() const = 0; + + // Returns the current value of the between-tone gap in ms. + // This value will be the value last set via the InsertDtmf() method, or the + // default value of 50 ms if InsertDtmf() was never called. + virtual int inter_tone_gap() const = 0; + + protected: + virtual ~DtmfSenderInterface() {} +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_DTMFSENDERINTERFACE_H_ diff --git a/talk/app/webrtc/fakeportallocatorfactory.h b/talk/app/webrtc/fakeportallocatorfactory.h new file mode 100644 index 000000000..c1727ae5c --- /dev/null +++ b/talk/app/webrtc/fakeportallocatorfactory.h @@ -0,0 +1,74 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +// This file defines a fake port allocator factory used for testing. +// This implementation creates instances of cricket::FakePortAllocator. + +#ifndef TALK_APP_WEBRTC_FAKEPORTALLOCATORFACTORY_H_ +#define TALK_APP_WEBRTC_FAKEPORTALLOCATORFACTORY_H_ + +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/p2p/client/fakeportallocator.h" + +namespace webrtc { + +class FakePortAllocatorFactory : public PortAllocatorFactoryInterface { + public: + static FakePortAllocatorFactory* Create() { + talk_base::RefCountedObject* allocator = + new talk_base::RefCountedObject(); + return allocator; + } + + virtual cricket::PortAllocator* CreatePortAllocator( + const std::vector& stun_configurations, + const std::vector& turn_configurations) { + stun_configs_ = stun_configurations; + turn_configs_ = turn_configurations; + return new cricket::FakePortAllocator(talk_base::Thread::Current(), NULL); + } + + const std::vector& stun_configs() const { + return stun_configs_; + } + + const std::vector& turn_configs() const { + return turn_configs_; + } + + protected: + FakePortAllocatorFactory() {} + ~FakePortAllocatorFactory() {} + + private: + std::vector stun_configs_; + std::vector turn_configs_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_FAKEPORTALLOCATORFACTORY_H_ diff --git a/talk/app/webrtc/java/README b/talk/app/webrtc/java/README new file mode 100644 index 000000000..454046cb1 --- /dev/null +++ b/talk/app/webrtc/java/README @@ -0,0 +1,23 @@ +This directory holds a Java implementation of the webrtc::PeerConnection API, as +well as the JNI glue C++ code that lets the Java implementation reuse the C++ +implementation of the same API. + +To build the Java API and related tests, build with +OS=linux or OS=android and include +build_with_libjingle=1 build_with_chromium=0 +in $GYP_DEFINES. + +To use the Java API, start by looking at the public interface of +org.webrtc.PeerConnection{,Factory} and the org.webrtc.PeerConnectionTest. + +To understand the implementation of the API, see the native code in jni/. + +An example command-line to build & run the unittest: +cd path/to/trunk +GYP_DEFINES="build_with_libjingle=1 build_with_chromium=0 java_home=path/to/JDK" gclient runhooks && \ + ninja -C out/Debug libjingle_peerconnection_java_unittest && \ + ./out/Debug/libjingle_peerconnection_java_unittest +(where path/to/JDK should contain include/jni.h) + +During development it can be helpful to run the JVM with the -Xcheck:jni flag. + diff --git a/talk/app/webrtc/java/jni/peerconnection_jni.cc b/talk/app/webrtc/java/jni/peerconnection_jni.cc new file mode 100644 index 000000000..6b5a6a48c --- /dev/null +++ b/talk/app/webrtc/java/jni/peerconnection_jni.cc @@ -0,0 +1,1359 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +// Hints for future visitors: +// This entire file is an implementation detail of the org.webrtc Java package, +// the most interesting bits of which are org.webrtc.PeerConnection{,Factory}. +// The layout of this file is roughly: +// - various helper C++ functions & classes that wrap Java counterparts and +// expose a C++ interface that can be passed to the C++ PeerConnection APIs +// - implementations of methods declared "static" in the Java package (named +// things like Java_org_webrtc_OMG_Can_This_Name_Be_Any_Longer, prescribed by +// the JNI spec). +// +// Lifecycle notes: objects are owned where they will be called; in other words +// FooObservers are owned by C++-land, and user-callable objects (e.g. +// PeerConnection and VideoTrack) are owned by Java-land. +// When this file allocates C++ RefCountInterfaces it AddRef()s an artificial +// ref simulating the jlong held in Java-land, and then Release()s the ref in +// the respective free call. Sometimes this AddRef is implicit in the +// construction of a scoped_refptr<> which is then .release()d. +// Any persistent (non-local) references from C++ to Java must be global or weak +// (in which case they must be checked before use)! +// +// Exception notes: pretty much all JNI calls can throw Java exceptions, so each +// call through a JNIEnv* pointer needs to be followed by an ExceptionCheck() +// call. In this file this is done in CHECK_EXCEPTION, making for much easier +// debugging in case of failure (the alternative is to wait for control to +// return to the Java frame that called code in this file, at which point it's +// impossible to tell which JNI call broke). + +#include +#undef JNIEXPORT +#define JNIEXPORT __attribute__((visibility("default"))) + +#include + +#include "talk/app/webrtc/mediaconstraintsinterface.h" +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/app/webrtc/videosourceinterface.h" +#include "talk/base/logging.h" +#include "talk/base/ssladapter.h" +#include "talk/media/base/videocapturer.h" +#include "talk/media/base/videorenderer.h" +#include "talk/media/devices/videorendererfactory.h" +#include "talk/media/webrtc/webrtcvideocapturer.h" +#include "third_party/icu/public/common/unicode/unistr.h" +#include "third_party/webrtc/system_wrappers/interface/trace.h" +#include "third_party/webrtc/video_engine/include/vie_base.h" +#include "third_party/webrtc/voice_engine/include/voe_base.h" + +using icu::UnicodeString; +using webrtc::AudioSourceInterface; +using webrtc::AudioTrackInterface; +using webrtc::AudioTrackVector; +using webrtc::CreateSessionDescriptionObserver; +using webrtc::IceCandidateInterface; +using webrtc::MediaConstraintsInterface; +using webrtc::MediaSourceInterface; +using webrtc::MediaStreamInterface; +using webrtc::MediaStreamTrackInterface; +using webrtc::PeerConnectionFactoryInterface; +using webrtc::PeerConnectionInterface; +using webrtc::PeerConnectionObserver; +using webrtc::SessionDescriptionInterface; +using webrtc::SetSessionDescriptionObserver; +using webrtc::StatsObserver; +using webrtc::StatsReport; +using webrtc::VideoRendererInterface; +using webrtc::VideoSourceInterface; +using webrtc::VideoTrackInterface; +using webrtc::VideoTrackVector; +using webrtc::VideoRendererInterface; + +// Abort the process if |x| is false, emitting |msg|. +#define CHECK(x, msg) \ + if (x) {} else { \ + LOG(LS_ERROR) << __FILE__ << ":" << __LINE__ << ": " << msg; \ + abort(); \ + } +// Abort the process if |jni| has a Java exception pending, emitting |msg|. +#define CHECK_EXCEPTION(jni, msg) \ + if (0) {} else { \ + if (jni->ExceptionCheck()) { \ + jni->ExceptionDescribe(); \ + jni->ExceptionClear(); \ + CHECK(0, msg); \ + } \ + } + +namespace { + +static JavaVM* g_jvm = NULL; // Set in JNI_OnLoad(). + +static pthread_once_t g_jni_ptr_once = PTHREAD_ONCE_INIT; +static pthread_key_t g_jni_ptr; // Key for per-thread JNIEnv* data. + +static void ThreadDestructor(void* unused) { + jint status = g_jvm->DetachCurrentThread(); + CHECK(status == JNI_OK, "Failed to detach thread: " << status); +} + +static void CreateJNIPtrKey() { + CHECK(!pthread_key_create(&g_jni_ptr, &ThreadDestructor), + "pthread_key_create"); +} + +// Deal with difference in signatures between Oracle's jni.h and Android's. +static JNIEnv* AttachCurrentThreadIfNeeded() { + CHECK(!pthread_once(&g_jni_ptr_once, &CreateJNIPtrKey), + "pthread_once"); + JNIEnv* jni = reinterpret_cast(pthread_getspecific(g_jni_ptr)); + if (jni == NULL) { +#ifdef _JAVASOFT_JNI_H_ // Oracle's jni.h violates the JNI spec! + void* env; +#else + JNIEnv* env; +#endif + CHECK(!g_jvm->AttachCurrentThread(&env, NULL), "Failed to attach thread"); + CHECK(env, "AttachCurrentThread handed back NULL!"); + jni = reinterpret_cast(env); + CHECK(!pthread_setspecific(g_jni_ptr, jni), "pthread_setspecific"); + } + return jni; +} + +// Android's FindClass() is trickier than usual because the app-specific +// ClassLoader is not consulted when there is no app-specific frame on the +// stack. Consequently, we only look up classes once in JNI_OnLoad. +// http://developer.android.com/training/articles/perf-jni.html#faq_FindClass +class ClassReferenceHolder { + public: + explicit ClassReferenceHolder(JNIEnv* jni) { + LoadClass(jni, "java/nio/ByteBuffer"); + LoadClass(jni, "org/webrtc/AudioTrack"); + LoadClass(jni, "org/webrtc/IceCandidate"); + LoadClass(jni, "org/webrtc/MediaSource$State"); + LoadClass(jni, "org/webrtc/MediaStream"); + LoadClass(jni, "org/webrtc/MediaStreamTrack$State"); + LoadClass(jni, "org/webrtc/PeerConnection$SignalingState"); + LoadClass(jni, "org/webrtc/PeerConnection$IceConnectionState"); + LoadClass(jni, "org/webrtc/PeerConnection$IceGatheringState"); + LoadClass(jni, "org/webrtc/SessionDescription"); + LoadClass(jni, "org/webrtc/SessionDescription$Type"); + LoadClass(jni, "org/webrtc/StatsReport"); + LoadClass(jni, "org/webrtc/StatsReport$Value"); + LoadClass(jni, "org/webrtc/VideoRenderer$I420Frame"); + LoadClass(jni, "org/webrtc/VideoTrack"); + } + + ~ClassReferenceHolder() { + CHECK(classes_.empty(), "Must call FreeReferences() before dtor!"); + } + + void FreeReferences(JNIEnv* jni) { + for (std::map::const_iterator it = classes_.begin(); + it != classes_.end(); ++it) { + jni->DeleteGlobalRef(it->second); + } + classes_.clear(); + } + + jclass GetClass(const std::string& name) { + std::map::iterator it = classes_.find(name); + CHECK(it != classes_.end(), "Unexpected GetClass() call for: " << name); + return it->second; + } + + private: + void LoadClass(JNIEnv* jni, const std::string& name) { + jclass localRef = jni->FindClass(name.c_str()); + CHECK_EXCEPTION(jni, "error during FindClass: " << name); + CHECK(localRef, name); + jclass globalRef = reinterpret_cast(jni->NewGlobalRef(localRef)); + CHECK_EXCEPTION(jni, "error during NewGlobalRef: " << name); + CHECK(globalRef, name); + bool inserted = classes_.insert(std::make_pair(name, globalRef)).second; + CHECK(inserted, "Duplicate class name: " << name); + } + + std::map classes_; +}; + +// Allocated in JNI_OnLoad(), freed in JNI_OnUnLoad(). +static ClassReferenceHolder* g_class_reference_holder = NULL; + +// JNIEnv-helper methods that CHECK success: no Java exception thrown and found +// object/class/method/field is non-null. +jmethodID GetMethodID( + JNIEnv* jni, jclass c, const std::string& name, const char* signature) { + jmethodID m = jni->GetMethodID(c, name.c_str(), signature); + CHECK_EXCEPTION(jni, + "error during GetMethodID: " << name << ", " << signature); + CHECK(m, name << ", " << signature); + return m; +} + +jmethodID GetStaticMethodID( + JNIEnv* jni, jclass c, const char* name, const char* signature) { + jmethodID m = jni->GetStaticMethodID(c, name, signature); + CHECK_EXCEPTION(jni, + "error during GetStaticMethodID: " + << name << ", " << signature); + CHECK(m, name << ", " << signature); + return m; +} + +jfieldID GetFieldID( + JNIEnv* jni, jclass c, const char* name, const char* signature) { + jfieldID f = jni->GetFieldID(c, name, signature); + CHECK_EXCEPTION(jni, "error during GetFieldID"); + CHECK(f, name << ", " << signature); + return f; +} + +jclass FindClass(JNIEnv* jni, const char* name) { + return g_class_reference_holder->GetClass(name); +} + +jclass GetObjectClass(JNIEnv* jni, jobject object) { + jclass c = jni->GetObjectClass(object); + CHECK_EXCEPTION(jni, "error during GetObjectClass"); + CHECK(c, ""); + return c; +} + +jobject GetObjectField(JNIEnv* jni, jobject object, jfieldID id) { + jobject o = jni->GetObjectField(object, id); + CHECK_EXCEPTION(jni, "error during GetObjectField"); + CHECK(o, ""); + return o; +} + +jlong GetLongField(JNIEnv* jni, jobject object, jfieldID id) { + jlong l = jni->GetLongField(object, id); + CHECK_EXCEPTION(jni, "error during GetLongField"); + CHECK(l, ""); + return l; +} + +jobject NewGlobalRef(JNIEnv* jni, jobject o) { + jobject ret = jni->NewGlobalRef(o); + CHECK_EXCEPTION(jni, "error during NewGlobalRef"); + CHECK(ret, ""); + return ret; +} + +void DeleteGlobalRef(JNIEnv* jni, jobject o) { + jni->DeleteGlobalRef(o); + CHECK_EXCEPTION(jni, "error during DeleteGlobalRef"); +} + +// Given a jweak reference, allocate a (strong) local reference scoped to the +// lifetime of this object if the weak reference is still valid, or NULL +// otherwise. +class WeakRef { + public: + WeakRef(JNIEnv* jni, jweak ref) + : jni_(jni), obj_(jni_->NewLocalRef(ref)) { + CHECK_EXCEPTION(jni, "error during NewLocalRef"); + } + ~WeakRef() { + if (obj_) { + jni_->DeleteLocalRef(obj_); + CHECK_EXCEPTION(jni_, "error during DeleteLocalRef"); + } + } + jobject obj() { return obj_; } + + private: + JNIEnv* const jni_; + jobject const obj_; +}; + +// Given a local ref, take ownership of it and delete the ref when this goes out +// of scope. +template // T is jclass, jobject, jintArray, etc. +class ScopedLocalRef { + public: + ScopedLocalRef(JNIEnv* jni, T obj) + : jni_(jni), obj_(obj) {} + ~ScopedLocalRef() { + jni_->DeleteLocalRef(obj_); + } + T operator*() const { + return obj_; + } + private: + JNIEnv* jni_; + T obj_; +}; + +// Scoped holder for global Java refs. +class ScopedGlobalRef { + public: + explicit ScopedGlobalRef(JNIEnv* jni, jobject obj) + : obj_(jni->NewGlobalRef(obj)) {} + ~ScopedGlobalRef() { + DeleteGlobalRef(AttachCurrentThreadIfNeeded(), obj_); + } + jobject operator*() const { + return obj_; + } + private: + jobject obj_; +}; + +// Return the (singleton) Java Enum object corresponding to |index|; +// |state_class_fragment| is something like "MediaSource$State". +jobject JavaEnumFromIndex( + JNIEnv* jni, const std::string& state_class_fragment, int index) { + std::string state_class_name = "org/webrtc/" + state_class_fragment; + jclass state_class = FindClass(jni, state_class_name.c_str()); + jmethodID state_values_id = GetStaticMethodID( + jni, state_class, "values", ("()[L" + state_class_name + ";").c_str()); + ScopedLocalRef state_values( + jni, + (jobjectArray)jni->CallStaticObjectMethod(state_class, state_values_id)); + CHECK_EXCEPTION(jni, "error during CallStaticObjectMethod"); + jobject ret = jni->GetObjectArrayElement(*state_values, index); + CHECK_EXCEPTION(jni, "error during GetObjectArrayElement"); + return ret; +} + +// Given a UTF-8 encoded |native| string return a new (UTF-16) jstring. +static jstring JavaStringFromStdString(JNIEnv* jni, const std::string& native) { + UnicodeString ustr(UnicodeString::fromUTF8(native)); + jstring jstr = jni->NewString(ustr.getBuffer(), ustr.length()); + CHECK_EXCEPTION(jni, "error during NewString"); + return jstr; +} + +// Given a (UTF-16) jstring return a new UTF-8 native string. +static std::string JavaToStdString(JNIEnv* jni, const jstring& j_string) { + const jchar* jchars = jni->GetStringChars(j_string, NULL); + CHECK_EXCEPTION(jni, "Error during GetStringChars"); + UnicodeString ustr(jchars, jni->GetStringLength(j_string)); + CHECK_EXCEPTION(jni, "Error during GetStringLength"); + jni->ReleaseStringChars(j_string, jchars); + CHECK_EXCEPTION(jni, "Error during ReleaseStringChars"); + std::string ret; + return ustr.toUTF8String(ret); +} + +class ConstraintsWrapper; + +// Adapter between the C++ PeerConnectionObserver interface and the Java +// PeerConnection.Observer interface. Wraps an instance of the Java interface +// and dispatches C++ callbacks to Java. +class PCOJava : public PeerConnectionObserver { + public: + PCOJava(JNIEnv* jni, jobject j_observer) + : j_observer_global_(jni, j_observer), + j_observer_class_((jclass)NewGlobalRef( + jni, GetObjectClass(jni, *j_observer_global_))), + j_media_stream_class_((jclass)NewGlobalRef( + jni, FindClass(jni, "org/webrtc/MediaStream"))), + j_media_stream_ctor_(GetMethodID(jni, + j_media_stream_class_, "", "(J)V")), + j_audio_track_class_((jclass)NewGlobalRef( + jni, FindClass(jni, "org/webrtc/AudioTrack"))), + j_audio_track_ctor_(GetMethodID( + jni, j_audio_track_class_, "", "(J)V")), + j_video_track_class_((jclass)NewGlobalRef( + jni, FindClass(jni, "org/webrtc/VideoTrack"))), + j_video_track_ctor_(GetMethodID(jni, + j_video_track_class_, "", "(J)V")) { + } + + virtual ~PCOJava() {} + + virtual void OnIceCandidate(const IceCandidateInterface* candidate) { + std::string sdp; + CHECK(candidate->ToString(&sdp), "got so far: " << sdp); + jclass candidate_class = FindClass(jni(), "org/webrtc/IceCandidate"); + jmethodID ctor = GetMethodID(jni(), candidate_class, + "", "(Ljava/lang/String;ILjava/lang/String;)V"); + ScopedLocalRef j_mid( + jni(), JavaStringFromStdString(jni(), candidate->sdp_mid())); + ScopedLocalRef j_sdp(jni(), JavaStringFromStdString(jni(), sdp)); + ScopedLocalRef j_candidate(jni(), jni()->NewObject( + candidate_class, ctor, *j_mid, candidate->sdp_mline_index(), *j_sdp)); + CHECK_EXCEPTION(jni(), "error during NewObject"); + jmethodID m = GetMethodID(jni(), j_observer_class_, + "onIceCandidate", "(Lorg/webrtc/IceCandidate;)V"); + jni()->CallVoidMethod(*j_observer_global_, m, *j_candidate); + CHECK_EXCEPTION(jni(), "error during CallVoidMethod"); + } + + virtual void OnError() { + jmethodID m = GetMethodID(jni(), j_observer_class_, "onError", "(V)V"); + jni()->CallVoidMethod(*j_observer_global_, m); + CHECK_EXCEPTION(jni(), "error during CallVoidMethod"); + } + + virtual void OnSignalingChange( + PeerConnectionInterface::SignalingState new_state) { + jmethodID m = GetMethodID( + jni(), j_observer_class_, "onSignalingChange", + "(Lorg/webrtc/PeerConnection$SignalingState;)V"); + ScopedLocalRef new_state_enum(jni(), JavaEnumFromIndex( + jni(), "PeerConnection$SignalingState", new_state)); + jni()->CallVoidMethod(*j_observer_global_, m, *new_state_enum); + CHECK_EXCEPTION(jni(), "error during CallVoidMethod"); + } + + virtual void OnIceConnectionChange( + PeerConnectionInterface::IceConnectionState new_state) { + jmethodID m = GetMethodID( + jni(), j_observer_class_, "onIceConnectionChange", + "(Lorg/webrtc/PeerConnection$IceConnectionState;)V"); + ScopedLocalRef new_state_enum(jni(), JavaEnumFromIndex( + jni(), "PeerConnection$IceConnectionState", new_state)); + jni()->CallVoidMethod(*j_observer_global_, m, *new_state_enum); + CHECK_EXCEPTION(jni(), "error during CallVoidMethod"); + } + + virtual void OnIceGatheringChange( + PeerConnectionInterface::IceGatheringState new_state) { + jmethodID m = GetMethodID( + jni(), j_observer_class_, "onIceGatheringChange", + "(Lorg/webrtc/PeerConnection$IceGatheringState;)V"); + ScopedLocalRef new_state_enum(jni(), JavaEnumFromIndex( + jni(), "PeerConnection$IceGatheringState", new_state)); + jni()->CallVoidMethod(*j_observer_global_, m, *new_state_enum); + CHECK_EXCEPTION(jni(), "error during CallVoidMethod"); + } + + virtual void OnAddStream(MediaStreamInterface* stream) { + ScopedLocalRef j_stream(jni(), jni()->NewObject( + j_media_stream_class_, j_media_stream_ctor_, (jlong)stream)); + CHECK_EXCEPTION(jni(), "error during NewObject"); + + AudioTrackVector audio_tracks = stream->GetAudioTracks(); + for (size_t i = 0; i < audio_tracks.size(); ++i) { + AudioTrackInterface* track = audio_tracks[i]; + ScopedLocalRef id( + jni(), JavaStringFromStdString(jni(), track->id())); + ScopedLocalRef j_track(jni(), jni()->NewObject( + j_audio_track_class_, j_audio_track_ctor_, (jlong)track, *id)); + CHECK_EXCEPTION(jni(), "error during NewObject"); + jfieldID audio_tracks_id = GetFieldID(jni(), + j_media_stream_class_, "audioTracks", "Ljava/util/List;"); + ScopedLocalRef audio_tracks(jni(), GetObjectField( + jni(), *j_stream, audio_tracks_id)); + jmethodID add = GetMethodID(jni(), + GetObjectClass(jni(), *audio_tracks), "add", "(Ljava/lang/Object;)Z"); + jboolean added = jni()->CallBooleanMethod(*audio_tracks, add, *j_track); + CHECK_EXCEPTION(jni(), "error during CallBooleanMethod"); + CHECK(added, ""); + } + + VideoTrackVector video_tracks = stream->GetVideoTracks(); + for (size_t i = 0; i < video_tracks.size(); ++i) { + VideoTrackInterface* track = video_tracks[i]; + ScopedLocalRef id( + jni(), JavaStringFromStdString(jni(), track->id())); + ScopedLocalRef j_track(jni(), jni()->NewObject( + j_video_track_class_, j_video_track_ctor_, (jlong)track, *id)); + CHECK_EXCEPTION(jni(), "error during NewObject"); + jfieldID video_tracks_id = GetFieldID(jni(), + j_media_stream_class_, "videoTracks", "Ljava/util/List;"); + ScopedLocalRef video_tracks(jni(), GetObjectField( + jni(), *j_stream, video_tracks_id)); + jmethodID add = GetMethodID(jni(), + GetObjectClass(jni(), *video_tracks), "add", "(Ljava/lang/Object;)Z"); + jboolean added = jni()->CallBooleanMethod(*video_tracks, add, *j_track); + CHECK_EXCEPTION(jni(), "error during CallBooleanMethod"); + CHECK(added, ""); + } + streams_[stream] = jni()->NewWeakGlobalRef(*j_stream); + CHECK_EXCEPTION(jni(), "error during NewWeakGlobalRef"); + + jmethodID m = GetMethodID(jni(), + j_observer_class_, "onAddStream", "(Lorg/webrtc/MediaStream;)V"); + jni()->CallVoidMethod(*j_observer_global_, m, *j_stream); + CHECK_EXCEPTION(jni(), "error during CallVoidMethod"); + } + + virtual void OnRemoveStream(MediaStreamInterface* stream) { + NativeToJavaStreamsMap::iterator it = streams_.find(stream); + CHECK(it != streams_.end(), "unexpected stream: " << std::hex << stream); + + WeakRef s(jni(), it->second); + streams_.erase(it); + if (!s.obj()) + return; + + jmethodID m = GetMethodID(jni(), + j_observer_class_, "onRemoveStream", "(Lorg/webrtc/MediaStream;)V"); + jni()->CallVoidMethod(*j_observer_global_, m, s.obj()); + CHECK_EXCEPTION(jni(), "error during CallVoidMethod"); + } + + void SetConstraints(ConstraintsWrapper* constraints) { + CHECK(!constraints_.get(), "constraints already set!"); + constraints_.reset(constraints); + } + + const ConstraintsWrapper* constraints() { return constraints_.get(); } + + private: + JNIEnv* jni() { + return AttachCurrentThreadIfNeeded(); + } + + const ScopedGlobalRef j_observer_global_; + const jclass j_observer_class_; + const jclass j_media_stream_class_; + const jmethodID j_media_stream_ctor_; + const jclass j_audio_track_class_; + const jmethodID j_audio_track_ctor_; + const jclass j_video_track_class_; + const jmethodID j_video_track_ctor_; + typedef std::map NativeToJavaStreamsMap; + NativeToJavaStreamsMap streams_; // C++ -> Java streams. + talk_base::scoped_ptr constraints_; +}; + +// Wrapper for a Java MediaConstraints object. Copies all needed data so when +// the constructor returns the Java object is no longer needed. +class ConstraintsWrapper : public MediaConstraintsInterface { + public: + ConstraintsWrapper(JNIEnv* jni, jobject j_constraints) { + PopulateConstraintsFromJavaPairList( + jni, j_constraints, "mandatory", &mandatory_); + PopulateConstraintsFromJavaPairList( + jni, j_constraints, "optional", &optional_); + } + + virtual ~ConstraintsWrapper() {} + + // MediaConstraintsInterface. + virtual const Constraints& GetMandatory() const { return mandatory_; } + virtual const Constraints& GetOptional() const { return optional_; } + + private: + // Helper for translating a List> to a Constraints. + static void PopulateConstraintsFromJavaPairList( + JNIEnv* jni, jobject j_constraints, + const char* field_name, Constraints* field) { + jfieldID j_id = GetFieldID(jni, + GetObjectClass(jni, j_constraints), field_name, "Ljava/util/List;"); + jobject j_list = GetObjectField(jni, j_constraints, j_id); + jmethodID j_iterator_id = GetMethodID(jni, + GetObjectClass(jni, j_list), "iterator", "()Ljava/util/Iterator;"); + jobject j_iterator = jni->CallObjectMethod(j_list, j_iterator_id); + CHECK_EXCEPTION(jni, "error during CallObjectMethod"); + jmethodID j_has_next = GetMethodID(jni, + GetObjectClass(jni, j_iterator), "hasNext", "()Z"); + jmethodID j_next = GetMethodID(jni, + GetObjectClass(jni, j_iterator), "next", "()Ljava/lang/Object;"); + while (jni->CallBooleanMethod(j_iterator, j_has_next)) { + CHECK_EXCEPTION(jni, "error during CallBooleanMethod"); + jobject entry = jni->CallObjectMethod(j_iterator, j_next); + CHECK_EXCEPTION(jni, "error during CallObjectMethod"); + jmethodID get_key = GetMethodID(jni, + GetObjectClass(jni, entry), "getKey", "()Ljava/lang/String;"); + jstring j_key = reinterpret_cast( + jni->CallObjectMethod(entry, get_key)); + CHECK_EXCEPTION(jni, "error during CallObjectMethod"); + jmethodID get_value = GetMethodID(jni, + GetObjectClass(jni, entry), "getValue", "()Ljava/lang/String;"); + jstring j_value = reinterpret_cast( + jni->CallObjectMethod(entry, get_value)); + CHECK_EXCEPTION(jni, "error during CallObjectMethod"); + field->push_back(Constraint(JavaToStdString(jni, j_key), + JavaToStdString(jni, j_value))); + } + CHECK_EXCEPTION(jni, "error during CallBooleanMethod"); + } + + Constraints mandatory_; + Constraints optional_; +}; + +static jobject JavaSdpFromNativeSdp( + JNIEnv* jni, const SessionDescriptionInterface* desc) { + std::string sdp; + CHECK(desc->ToString(&sdp), "got so far: " << sdp); + ScopedLocalRef j_description(jni, JavaStringFromStdString(jni, sdp)); + + jclass j_type_class = FindClass( + jni, "org/webrtc/SessionDescription$Type"); + jmethodID j_type_from_canonical = GetStaticMethodID( + jni, j_type_class, "fromCanonicalForm", + "(Ljava/lang/String;)Lorg/webrtc/SessionDescription$Type;"); + ScopedLocalRef j_type_string( + jni, JavaStringFromStdString(jni, desc->type())); + jobject j_type = jni->CallStaticObjectMethod( + j_type_class, j_type_from_canonical, *j_type_string); + CHECK_EXCEPTION(jni, "error during CallObjectMethod"); + + jclass j_sdp_class = FindClass(jni, "org/webrtc/SessionDescription"); + jmethodID j_sdp_ctor = GetMethodID( + jni, j_sdp_class, "", + "(Lorg/webrtc/SessionDescription$Type;Ljava/lang/String;)V"); + jobject j_sdp = jni->NewObject( + j_sdp_class, j_sdp_ctor, j_type, *j_description); + CHECK_EXCEPTION(jni, "error during NewObject"); + return j_sdp; +} + +template // T is one of {Create,Set}SessionDescriptionObserver. +class SdpObserverWrapper : public T { + public: + SdpObserverWrapper(JNIEnv* jni, jobject j_observer, + ConstraintsWrapper* constraints) + : constraints_(constraints), + j_observer_global_(NewGlobalRef(jni, j_observer)), + j_observer_class_((jclass)NewGlobalRef( + jni, GetObjectClass(jni, j_observer))) { + } + + virtual ~SdpObserverWrapper() { + DeleteGlobalRef(jni(), j_observer_global_); + DeleteGlobalRef(jni(), j_observer_class_); + } + + virtual void OnSuccess() { + jmethodID m = GetMethodID(jni(), j_observer_class_, "onSetSuccess", "()V"); + jni()->CallVoidMethod(j_observer_global_, m); + CHECK_EXCEPTION(jni(), "error during CallVoidMethod"); + } + + virtual void OnSuccess(SessionDescriptionInterface* desc) { + jmethodID m = GetMethodID( + jni(), j_observer_class_, "onCreateSuccess", + "(Lorg/webrtc/SessionDescription;)V"); + ScopedLocalRef j_sdp(jni(), JavaSdpFromNativeSdp(jni(), desc)); + jni()->CallVoidMethod(j_observer_global_, m, *j_sdp); + CHECK_EXCEPTION(jni(), "error during CallVoidMethod"); + } + + protected: + // Common implementation for failure of Set & Create types, distinguished by + // |op| being "Set" or "Create". + void OnFailure(const std::string& op, const std::string& error) { + jmethodID m = GetMethodID(jni(), + j_observer_class_, "on" + op + "Failure", "(Ljava/lang/String;)V"); + ScopedLocalRef j_error_string( + jni(), JavaStringFromStdString(jni(), error)); + jni()->CallVoidMethod(j_observer_global_, m, *j_error_string); + CHECK_EXCEPTION(jni(), "error during CallVoidMethod"); + } + + private: + JNIEnv* jni() { + return AttachCurrentThreadIfNeeded(); + } + + talk_base::scoped_ptr constraints_; + const jobject j_observer_global_; + const jclass j_observer_class_; +}; + +class CreateSdpObserverWrapper + : public SdpObserverWrapper { + public: + CreateSdpObserverWrapper(JNIEnv* jni, jobject j_observer, + ConstraintsWrapper* constraints) + : SdpObserverWrapper(jni, j_observer, constraints) {} + + virtual void OnFailure(const std::string& error) { + SdpObserverWrapper::OnFailure(std::string("Create"), error); + } +}; + +class SetSdpObserverWrapper + : public SdpObserverWrapper { + public: + SetSdpObserverWrapper(JNIEnv* jni, jobject j_observer, + ConstraintsWrapper* constraints) + : SdpObserverWrapper(jni, j_observer, constraints) {} + + virtual void OnFailure(const std::string& error) { + SdpObserverWrapper::OnFailure(std::string("Set"), error); + } +}; + +// Adapter for a Java StatsObserver presenting a C++ StatsObserver and +// dispatching the callback from C++ back to Java. +class StatsObserverWrapper : public StatsObserver { + public: + StatsObserverWrapper(JNIEnv* jni, jobject j_observer) + : j_observer_global_(NewGlobalRef(jni, j_observer)), + j_observer_class_((jclass)NewGlobalRef( + jni, GetObjectClass(jni, j_observer))), + j_stats_report_class_(FindClass(jni, "org/webrtc/StatsReport")), + j_stats_report_ctor_(GetMethodID( + jni, j_stats_report_class_, "", + "(Ljava/lang/String;Ljava/lang/String;D" + "[Lorg/webrtc/StatsReport$Value;)V")), + j_value_class_(FindClass( + jni, "org/webrtc/StatsReport$Value")), + j_value_ctor_(GetMethodID( + jni, j_value_class_, "", + "(Ljava/lang/String;Ljava/lang/String;)V")) { + } + + virtual ~StatsObserverWrapper() { + DeleteGlobalRef(jni(), j_observer_global_); + DeleteGlobalRef(jni(), j_observer_class_); + } + + virtual void OnComplete(const std::vector& reports) { + ScopedLocalRef j_reports(jni(), + ReportsToJava(jni(), reports)); + jmethodID m = GetMethodID( + jni(), j_observer_class_, "onComplete", "([Lorg/webrtc/StatsReport;)V"); + jni()->CallVoidMethod(j_observer_global_, m, *j_reports); + CHECK_EXCEPTION(jni(), "error during CallVoidMethod"); + } + + private: + jobjectArray ReportsToJava( + JNIEnv* jni, const std::vector& reports) { + jobjectArray reports_array = jni->NewObjectArray( + reports.size(), j_stats_report_class_, NULL); + for (int i = 0; i < reports.size(); ++i) { + const StatsReport& report = reports[i]; + ScopedLocalRef j_id( + jni, JavaStringFromStdString(jni, report.id)); + ScopedLocalRef j_type( + jni, JavaStringFromStdString(jni, report.type)); + ScopedLocalRef j_values( + jni, ValuesToJava(jni, report.values)); + ScopedLocalRef j_report(jni, jni->NewObject( + j_stats_report_class_, j_stats_report_ctor_, *j_id, *j_type, + report.timestamp, *j_values)); + jni->SetObjectArrayElement(reports_array, i, *j_report); + } + return reports_array; + } + + jobjectArray ValuesToJava(JNIEnv* jni, const StatsReport::Values& values) { + jobjectArray j_values = jni->NewObjectArray( + values.size(), j_value_class_, NULL); + for (int i = 0; i < values.size(); ++i) { + const StatsReport::Value& value = values[i]; + ScopedLocalRef j_name( + jni, JavaStringFromStdString(jni, value.name)); + ScopedLocalRef j_value( + jni, JavaStringFromStdString(jni, value.value)); + ScopedLocalRef j_element_value(jni, jni->NewObject( + j_value_class_, j_value_ctor_, *j_name, *j_value)); + jni->SetObjectArrayElement(j_values, i, *j_element_value); + } + return j_values; + } + + JNIEnv* jni() { + return AttachCurrentThreadIfNeeded(); + } + + const jobject j_observer_global_; + const jclass j_observer_class_; + const jclass j_stats_report_class_; + const jmethodID j_stats_report_ctor_; + const jclass j_value_class_; + const jmethodID j_value_ctor_; +}; + +// Adapter presenting a cricket::VideoRenderer as a +// webrtc::VideoRendererInterface. +class VideoRendererWrapper : public VideoRendererInterface { + public: + static VideoRendererWrapper* Create(cricket::VideoRenderer* renderer) { + if (renderer) + return new VideoRendererWrapper(renderer); + return NULL; + } + + virtual ~VideoRendererWrapper() {} + + virtual void SetSize(int width, int height) { + const bool kNotReserved = false; // What does this param mean?? + renderer_->SetSize(width, height, kNotReserved); + } + + virtual void RenderFrame(const cricket::VideoFrame* frame) { + renderer_->RenderFrame(frame); + } + + private: + explicit VideoRendererWrapper(cricket::VideoRenderer* renderer) + : renderer_(renderer) {} + + talk_base::scoped_ptr renderer_; +}; + +// Wrapper dispatching webrtc::VideoRendererInterface to a Java VideoRenderer +// instance. +class JavaVideoRendererWrapper : public VideoRendererInterface { + public: + JavaVideoRendererWrapper(JNIEnv* jni, jobject j_callbacks) + : j_callbacks_(jni, j_callbacks) { + j_set_size_id_ = GetMethodID( + jni, GetObjectClass(jni, j_callbacks), "setSize", "(II)V"); + j_render_frame_id_ = GetMethodID( + jni, GetObjectClass(jni, j_callbacks), "renderFrame", + "(Lorg/webrtc/VideoRenderer$I420Frame;)V"); + j_frame_class_ = FindClass(jni, "org/webrtc/VideoRenderer$I420Frame"); + j_frame_ctor_id_ = GetMethodID( + jni, j_frame_class_, "", "(II[I[Ljava/nio/ByteBuffer;)V"); + j_byte_buffer_class_ = FindClass(jni, "java/nio/ByteBuffer"); + CHECK_EXCEPTION(jni, ""); + } + + virtual void SetSize(int width, int height) { + jni()->CallVoidMethod(*j_callbacks_, j_set_size_id_, width, height); + CHECK_EXCEPTION(jni(), ""); + } + + virtual void RenderFrame(const cricket::VideoFrame* frame) { + ScopedLocalRef j_frame(jni(), CricketToJavaFrame(frame)); + jni()->CallVoidMethod(*j_callbacks_, j_render_frame_id_, *j_frame); + CHECK_EXCEPTION(jni(), ""); + } + + private: + // Return a VideoRenderer.I420Frame referring to the data in |frame|. + jobject CricketToJavaFrame(const cricket::VideoFrame* frame) { + ScopedLocalRef strides(jni(), jni()->NewIntArray(3)); + jint* strides_array = jni()->GetIntArrayElements(*strides, NULL); + strides_array[0] = frame->GetYPitch(); + strides_array[1] = frame->GetUPitch(); + strides_array[2] = frame->GetVPitch(); + jni()->ReleaseIntArrayElements(*strides, strides_array, 0); + ScopedLocalRef planes( + jni(), jni()->NewObjectArray(3, j_byte_buffer_class_, NULL)); + ScopedLocalRef y_buffer(jni(), jni()->NewDirectByteBuffer( + const_cast(frame->GetYPlane()), + frame->GetYPitch() * frame->GetHeight())); + ScopedLocalRef u_buffer(jni(), jni()->NewDirectByteBuffer( + const_cast(frame->GetUPlane()), frame->GetChromaSize())); + ScopedLocalRef v_buffer(jni(), jni()->NewDirectByteBuffer( + const_cast(frame->GetVPlane()), frame->GetChromaSize())); + jni()->SetObjectArrayElement(*planes, 0, *y_buffer); + jni()->SetObjectArrayElement(*planes, 1, *u_buffer); + jni()->SetObjectArrayElement(*planes, 2, *v_buffer); + return jni()->NewObject( + j_frame_class_, j_frame_ctor_id_, + frame->GetWidth(), frame->GetHeight(), *strides, *planes); + } + + JNIEnv* jni() { + return AttachCurrentThreadIfNeeded(); + } + + ScopedGlobalRef j_callbacks_; + jmethodID j_set_size_id_; + jmethodID j_render_frame_id_; + jclass j_frame_class_; + jmethodID j_frame_ctor_id_; + jclass j_byte_buffer_class_; +}; + +} // anonymous namespace + + +// Convenience macro defining JNI-accessible methods in the org.webrtc package. +// Eliminates unnecessary boilerplate and line-wraps, reducing visual clutter. +#define JOW(rettype, name) extern "C" rettype JNIEXPORT JNICALL \ + Java_org_webrtc_##name + +extern "C" jint JNIEXPORT JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) { + CHECK(!g_jvm, "JNI_OnLoad called more than once!"); + g_jvm = jvm; + CHECK(g_jvm, "JNI_OnLoad handed NULL?"); + + CHECK(talk_base::InitializeSSL(), "Failed to InitializeSSL()"); + + JNIEnv* jni; + if (jvm->GetEnv(reinterpret_cast(&jni), JNI_VERSION_1_6) != JNI_OK) + return -1; + g_class_reference_holder = new ClassReferenceHolder(jni); + +#ifdef ANDROID + webrtc::Trace::CreateTrace(); + CHECK(!webrtc::Trace::SetTraceFile("/sdcard/trace.txt", false), + "SetTraceFile failed"); + CHECK(!webrtc::Trace::SetLevelFilter(webrtc::kTraceAll), + "SetLevelFilter failed"); +#endif // ANDROID + + // Uncomment to get sensitive logs emitted (to stderr or logcat). + // talk_base::LogMessage::LogToDebug(talk_base::LS_SENSITIVE); + + return JNI_VERSION_1_6; +} + +extern "C" void JNIEXPORT JNICALL JNI_OnUnLoad(JavaVM *jvm, void *reserved) { + webrtc::Trace::ReturnTrace(); + delete g_class_reference_holder; + g_class_reference_holder = NULL; + CHECK(talk_base::CleanupSSL(), "Failed to CleanupSSL()"); +} + +JOW(void, PeerConnection_freePeerConnection)(JNIEnv*, jclass, jlong j_p) { + reinterpret_cast(j_p)->Release(); +} + +JOW(void, PeerConnection_freeObserver)(JNIEnv*, jclass, jlong j_p) { + PCOJava* p = reinterpret_cast(j_p); + delete p; +} + +JOW(void, MediaSource_free)(JNIEnv*, jclass, jlong j_p) { + reinterpret_cast(j_p)->Release(); +} + +JOW(void, VideoCapturer_free)(JNIEnv*, jclass, jlong j_p) { + delete reinterpret_cast(j_p); +} + +JOW(void, VideoRenderer_free)(JNIEnv*, jclass, jlong j_p) { + delete reinterpret_cast(j_p); +} + +JOW(void, MediaStreamTrack_free)(JNIEnv*, jclass, jlong j_p) { + reinterpret_cast(j_p)->Release(); +} + +JOW(jboolean, MediaStream_nativeAddAudioTrack)( + JNIEnv* jni, jclass, jlong pointer, jlong j_audio_track_pointer) { + talk_base::scoped_refptr stream( + reinterpret_cast(pointer)); + talk_base::scoped_refptr track( + reinterpret_cast(j_audio_track_pointer)); + return stream->AddTrack(track); +} + +JOW(jboolean, MediaStream_nativeAddVideoTrack)( + JNIEnv* jni, jclass, jlong pointer, jlong j_video_track_pointer) { + talk_base::scoped_refptr stream( + reinterpret_cast(pointer)); + talk_base::scoped_refptr track( + reinterpret_cast(j_video_track_pointer)); + return stream->AddTrack(track); +} + +JOW(jboolean, MediaStream_nativeRemoveAudioTrack)( + JNIEnv* jni, jclass, jlong pointer, jlong j_audio_track_pointer) { + talk_base::scoped_refptr stream( + reinterpret_cast(pointer)); + talk_base::scoped_refptr track( + reinterpret_cast(j_audio_track_pointer)); + return stream->RemoveTrack(track); +} + +JOW(jboolean, MediaStream_nativeRemoveVideoTrack)( + JNIEnv* jni, jclass, jlong pointer, jlong j_video_track_pointer) { + talk_base::scoped_refptr stream( + reinterpret_cast(pointer)); + talk_base::scoped_refptr track( + reinterpret_cast(j_video_track_pointer)); + return stream->RemoveTrack(track); +} + +JOW(jstring, MediaStream_nativeLabel)(JNIEnv* jni, jclass, jlong j_p) { + return JavaStringFromStdString( + jni, reinterpret_cast(j_p)->label()); +} + +JOW(void, MediaStream_free)(JNIEnv*, jclass, jlong j_p) { + reinterpret_cast(j_p)->Release(); +} + +JOW(jlong, PeerConnectionFactory_nativeCreateObserver)( + JNIEnv * jni, jclass, jobject j_observer) { + return (jlong)new PCOJava(jni, j_observer); +} + +#ifdef ANDROID +JOW(jboolean, PeerConnectionFactory_initializeAndroidGlobals)( + JNIEnv* jni, jclass, jobject context) { + CHECK(g_jvm, "JNI_OnLoad failed to run?"); + bool failure = false; + failure |= webrtc::VideoEngine::SetAndroidObjects(g_jvm, context); + failure |= webrtc::VoiceEngine::SetAndroidObjects(g_jvm, jni, context); + return !failure; +} +#endif // ANDROID + +JOW(jlong, PeerConnectionFactory_nativeCreatePeerConnectionFactory)( + JNIEnv* jni, jclass) { + talk_base::scoped_refptr factory( + webrtc::CreatePeerConnectionFactory()); + return (jlong)factory.release(); +} + +JOW(void, PeerConnectionFactory_freeFactory)(JNIEnv*, jclass, jlong j_p) { + reinterpret_cast(j_p)->Release(); +} + +JOW(jlong, PeerConnectionFactory_nativeCreateLocalMediaStream)( + JNIEnv* jni, jclass, jlong native_factory, jstring label) { + talk_base::scoped_refptr factory( + reinterpret_cast(native_factory)); + talk_base::scoped_refptr stream( + factory->CreateLocalMediaStream(JavaToStdString(jni, label))); + return (jlong)stream.release(); +} + +JOW(jlong, PeerConnectionFactory_nativeCreateVideoSource)( + JNIEnv* jni, jclass, jlong native_factory, jlong native_capturer, + jobject j_constraints) { + talk_base::scoped_ptr constraints( + new ConstraintsWrapper(jni, j_constraints)); + talk_base::scoped_refptr factory( + reinterpret_cast(native_factory)); + talk_base::scoped_refptr source( + factory->CreateVideoSource( + reinterpret_cast(native_capturer), + constraints.get())); + return (jlong)source.release(); +} + +JOW(jlong, PeerConnectionFactory_nativeCreateVideoTrack)( + JNIEnv* jni, jclass, jlong native_factory, jstring id, + jlong native_source) { + talk_base::scoped_refptr factory( + reinterpret_cast(native_factory)); + talk_base::scoped_refptr track( + factory->CreateVideoTrack( + JavaToStdString(jni, id), + reinterpret_cast(native_source))); + return (jlong)track.release(); +} + +JOW(jlong, PeerConnectionFactory_nativeCreateAudioTrack)( + JNIEnv* jni, jclass, jlong native_factory, jstring id) { + talk_base::scoped_refptr factory( + reinterpret_cast(native_factory)); + talk_base::scoped_refptr track( + factory->CreateAudioTrack(JavaToStdString(jni, id), NULL)); + return (jlong)track.release(); +} + +static void JavaIceServersToJsepIceServers( + JNIEnv* jni, jobject j_ice_servers, + PeerConnectionInterface::IceServers* ice_servers) { + jclass list_class = GetObjectClass(jni, j_ice_servers); + jmethodID iterator_id = GetMethodID( + jni, list_class, "iterator", "()Ljava/util/Iterator;"); + jobject iterator = jni->CallObjectMethod(j_ice_servers, iterator_id); + CHECK_EXCEPTION(jni, "error during CallObjectMethod"); + jmethodID iterator_has_next = GetMethodID( + jni, GetObjectClass(jni, iterator), "hasNext", "()Z"); + jmethodID iterator_next = GetMethodID( + jni, GetObjectClass(jni, iterator), "next", "()Ljava/lang/Object;"); + while (jni->CallBooleanMethod(iterator, iterator_has_next)) { + CHECK_EXCEPTION(jni, "error during CallBooleanMethod"); + jobject j_ice_server = jni->CallObjectMethod(iterator, iterator_next); + CHECK_EXCEPTION(jni, "error during CallObjectMethod"); + jclass j_ice_server_class = GetObjectClass(jni, j_ice_server); + jfieldID j_ice_server_uri_id = + GetFieldID(jni, j_ice_server_class, "uri", "Ljava/lang/String;"); + jfieldID j_ice_server_username_id = + GetFieldID(jni, j_ice_server_class, "username", "Ljava/lang/String;"); + jfieldID j_ice_server_password_id = + GetFieldID(jni, j_ice_server_class, "password", "Ljava/lang/String;"); + jstring uri = reinterpret_cast( + GetObjectField(jni, j_ice_server, j_ice_server_uri_id)); + jstring username = reinterpret_cast( + GetObjectField(jni, j_ice_server, j_ice_server_username_id)); + jstring password = reinterpret_cast( + GetObjectField(jni, j_ice_server, j_ice_server_password_id)); + PeerConnectionInterface::IceServer server; + server.uri = JavaToStdString(jni, uri); + server.username = JavaToStdString(jni, username); + server.password = JavaToStdString(jni, password); + ice_servers->push_back(server); + } + CHECK_EXCEPTION(jni, "error during CallBooleanMethod"); +} + +JOW(jlong, PeerConnectionFactory_nativeCreatePeerConnection)( + JNIEnv *jni, jclass, jlong factory, jobject j_ice_servers, + jobject j_constraints, jlong observer_p) { + talk_base::scoped_refptr f( + reinterpret_cast(factory)); + PeerConnectionInterface::IceServers servers; + JavaIceServersToJsepIceServers(jni, j_ice_servers, &servers); + PCOJava* observer = reinterpret_cast(observer_p); + observer->SetConstraints(new ConstraintsWrapper(jni, j_constraints)); + talk_base::scoped_refptr pc(f->CreatePeerConnection( + servers, observer->constraints(), NULL, observer)); + return (jlong)pc.release(); +} + +static talk_base::scoped_refptr ExtractNativePC( + JNIEnv* jni, jobject j_pc) { + jfieldID native_pc_id = GetFieldID(jni, + GetObjectClass(jni, j_pc), "nativePeerConnection", "J"); + jlong j_p = GetLongField(jni, j_pc, native_pc_id); + return talk_base::scoped_refptr( + reinterpret_cast(j_p)); +} + +JOW(jobject, PeerConnection_getLocalDescription)(JNIEnv* jni, jobject j_pc) { + const SessionDescriptionInterface* sdp = + ExtractNativePC(jni, j_pc)->local_description(); + return sdp ? JavaSdpFromNativeSdp(jni, sdp) : NULL; +} + +JOW(jobject, PeerConnection_getRemoteDescription)(JNIEnv* jni, jobject j_pc) { + const SessionDescriptionInterface* sdp = + ExtractNativePC(jni, j_pc)->remote_description(); + return sdp ? JavaSdpFromNativeSdp(jni, sdp) : NULL; +} + +JOW(void, PeerConnection_createOffer)( + JNIEnv* jni, jobject j_pc, jobject j_observer, jobject j_constraints) { + ConstraintsWrapper* constraints = + new ConstraintsWrapper(jni, j_constraints); + talk_base::scoped_refptr observer( + new talk_base::RefCountedObject( + jni, j_observer, constraints)); + ExtractNativePC(jni, j_pc)->CreateOffer(observer, constraints); +} + +JOW(void, PeerConnection_createAnswer)( + JNIEnv* jni, jobject j_pc, jobject j_observer, jobject j_constraints) { + ConstraintsWrapper* constraints = + new ConstraintsWrapper(jni, j_constraints); + talk_base::scoped_refptr observer( + new talk_base::RefCountedObject( + jni, j_observer, constraints)); + ExtractNativePC(jni, j_pc)->CreateAnswer(observer, constraints); +} + +// Helper to create a SessionDescriptionInterface from a SessionDescription. +static SessionDescriptionInterface* JavaSdpToNativeSdp( + JNIEnv* jni, jobject j_sdp) { + jfieldID j_type_id = GetFieldID( + jni, GetObjectClass(jni, j_sdp), "type", + "Lorg/webrtc/SessionDescription$Type;"); + jobject j_type = GetObjectField(jni, j_sdp, j_type_id); + jmethodID j_canonical_form_id = GetMethodID( + jni, GetObjectClass(jni, j_type), "canonicalForm", + "()Ljava/lang/String;"); + jstring j_type_string = (jstring)jni->CallObjectMethod( + j_type, j_canonical_form_id); + CHECK_EXCEPTION(jni, "error during CallObjectMethod"); + std::string std_type = JavaToStdString(jni, j_type_string); + + jfieldID j_description_id = GetFieldID( + jni, GetObjectClass(jni, j_sdp), "description", "Ljava/lang/String;"); + jstring j_description = (jstring)GetObjectField(jni, j_sdp, j_description_id); + std::string std_description = JavaToStdString(jni, j_description); + + return webrtc::CreateSessionDescription( + std_type, std_description, NULL); +} + +JOW(void, PeerConnection_setLocalDescription)( + JNIEnv* jni, jobject j_pc, + jobject j_observer, jobject j_sdp) { + talk_base::scoped_refptr observer( + new talk_base::RefCountedObject( + jni, j_observer, reinterpret_cast(NULL))); + ExtractNativePC(jni, j_pc)->SetLocalDescription( + observer, JavaSdpToNativeSdp(jni, j_sdp)); +} + +JOW(void, PeerConnection_setRemoteDescription)( + JNIEnv* jni, jobject j_pc, + jobject j_observer, jobject j_sdp) { + talk_base::scoped_refptr observer( + new talk_base::RefCountedObject( + jni, j_observer, reinterpret_cast(NULL))); + ExtractNativePC(jni, j_pc)->SetRemoteDescription( + observer, JavaSdpToNativeSdp(jni, j_sdp)); +} + +JOW(jboolean, PeerConnection_updateIce)( + JNIEnv* jni, jobject j_pc, jobject j_ice_servers, jobject j_constraints) { + PeerConnectionInterface::IceServers ice_servers; + JavaIceServersToJsepIceServers(jni, j_ice_servers, &ice_servers); + talk_base::scoped_ptr constraints( + new ConstraintsWrapper(jni, j_constraints)); + return ExtractNativePC(jni, j_pc)->UpdateIce(ice_servers, constraints.get()); +} + +JOW(jboolean, PeerConnection_nativeAddIceCandidate)( + JNIEnv* jni, jobject j_pc, jstring j_sdp_mid, + jint j_sdp_mline_index, jstring j_candidate_sdp) { + std::string sdp_mid = JavaToStdString(jni, j_sdp_mid); + std::string sdp = JavaToStdString(jni, j_candidate_sdp); + talk_base::scoped_ptr candidate( + webrtc::CreateIceCandidate(sdp_mid, j_sdp_mline_index, sdp, NULL)); + return ExtractNativePC(jni, j_pc)->AddIceCandidate(candidate.get()); +} + +JOW(jboolean, PeerConnection_nativeAddLocalStream)( + JNIEnv* jni, jobject j_pc, jlong native_stream, jobject j_constraints) { + talk_base::scoped_ptr constraints( + new ConstraintsWrapper(jni, j_constraints)); + return ExtractNativePC(jni, j_pc)->AddStream( + reinterpret_cast(native_stream), + constraints.get()); +} + +JOW(void, PeerConnection_nativeRemoveLocalStream)( + JNIEnv* jni, jobject j_pc, jlong native_stream) { + ExtractNativePC(jni, j_pc)->RemoveStream( + reinterpret_cast(native_stream)); +} + +JOW(bool, PeerConnection_nativeGetStats)( + JNIEnv* jni, jobject j_pc, jobject j_observer, jlong native_track) { + talk_base::scoped_refptr observer( + new talk_base::RefCountedObject(jni, j_observer)); + return ExtractNativePC(jni, j_pc)->GetStats( + observer, reinterpret_cast(native_track)); +} + +JOW(jobject, PeerConnection_signalingState)(JNIEnv* jni, jobject j_pc) { + PeerConnectionInterface::SignalingState state = + ExtractNativePC(jni, j_pc)->signaling_state(); + return JavaEnumFromIndex(jni, "PeerConnection$SignalingState", state); +} + +JOW(jobject, PeerConnection_iceConnectionState)(JNIEnv* jni, jobject j_pc) { + PeerConnectionInterface::IceConnectionState state = + ExtractNativePC(jni, j_pc)->ice_connection_state(); + return JavaEnumFromIndex(jni, "PeerConnection$IceConnectionState", state); +} + +JOW(jobject, PeerGathering_iceGatheringState)(JNIEnv* jni, jobject j_pc) { + PeerConnectionInterface::IceGatheringState state = + ExtractNativePC(jni, j_pc)->ice_gathering_state(); + return JavaEnumFromIndex(jni, "PeerGathering$IceGatheringState", state); +} + +JOW(void, PeerConnection_close)(JNIEnv* jni, jobject j_pc) { + ExtractNativePC(jni, j_pc)->Close(); + return; +} + +JOW(jobject, MediaSource_nativeState)(JNIEnv* jni, jclass, jlong j_p) { + talk_base::scoped_refptr p( + reinterpret_cast(j_p)); + return JavaEnumFromIndex(jni, "MediaSource$State", p->state()); +} + +JOW(jlong, VideoCapturer_nativeCreateVideoCapturer)( + JNIEnv* jni, jclass, jstring j_device_name) { + std::string device_name = JavaToStdString(jni, j_device_name); + talk_base::scoped_ptr device_manager( + cricket::DeviceManagerFactory::Create()); + CHECK(device_manager->Init(), "DeviceManager::Init() failed"); + cricket::Device device; + if (!device_manager->GetVideoCaptureDevice(device_name, &device)) { + LOG(LS_ERROR) << "GetVideoCaptureDevice failed"; + return 0; + } + talk_base::scoped_ptr capturer( + device_manager->CreateVideoCapturer(device)); + return (jlong)capturer.release(); +} + +JOW(jlong, VideoRenderer_nativeCreateGuiVideoRenderer)( + JNIEnv* jni, jclass, int x, int y) { + talk_base::scoped_ptr renderer( + VideoRendererWrapper::Create( + cricket::VideoRendererFactory::CreateGuiVideoRenderer(x, y))); + return (jlong)renderer.release(); +} + +JOW(jlong, VideoRenderer_nativeWrapVideoRenderer)( + JNIEnv* jni, jclass, jobject j_callbacks) { + talk_base::scoped_ptr renderer( + new JavaVideoRendererWrapper(jni, j_callbacks)); + return (jlong)renderer.release(); +} + +JOW(jstring, MediaStreamTrack_nativeId)(JNIEnv* jni, jclass, jlong j_p) { + talk_base::scoped_refptr p( + reinterpret_cast(j_p)); + return JavaStringFromStdString(jni, p->id()); +} + +JOW(jstring, MediaStreamTrack_nativeKind)(JNIEnv* jni, jclass, jlong j_p) { + talk_base::scoped_refptr p( + reinterpret_cast(j_p)); + return JavaStringFromStdString(jni, p->kind()); +} + +JOW(jboolean, MediaStreamTrack_nativeEnabled)(JNIEnv* jni, jclass, jlong j_p) { + talk_base::scoped_refptr p( + reinterpret_cast(j_p)); + return p->enabled(); +} + +JOW(jobject, MediaStreamTrack_nativeState)(JNIEnv* jni, jclass, jlong j_p) { + talk_base::scoped_refptr p( + reinterpret_cast(j_p)); + return JavaEnumFromIndex(jni, "MediaStreamTrack$State", p->state()); +} + +JOW(jboolean, MediaStreamTrack_nativeSetState)( + JNIEnv* jni, jclass, jlong j_p, jint j_new_state) { + talk_base::scoped_refptr p( + reinterpret_cast(j_p)); + MediaStreamTrackInterface::TrackState new_state = + (MediaStreamTrackInterface::TrackState)j_new_state; + return p->set_state(new_state); +} + +JOW(jboolean, MediaStreamTrack_nativeSetEnabled)( + JNIEnv* jni, jclass, jlong j_p, jboolean enabled) { + talk_base::scoped_refptr p( + reinterpret_cast(j_p)); + return p->set_enabled(enabled); +} + +JOW(void, VideoTrack_nativeAddRenderer)( + JNIEnv* jni, jclass, + jlong j_video_track_pointer, jlong j_renderer_pointer) { + talk_base::scoped_refptr track( + reinterpret_cast(j_video_track_pointer)); + track->AddRenderer( + reinterpret_cast(j_renderer_pointer)); +} + +JOW(void, VideoTrack_nativeRemoveRenderer)( + JNIEnv* jni, jclass, + jlong j_video_track_pointer, jlong j_renderer_pointer) { + talk_base::scoped_refptr track( + reinterpret_cast(j_video_track_pointer)); + track->RemoveRenderer( + reinterpret_cast(j_renderer_pointer)); +} diff --git a/talk/app/webrtc/java/src/org/webrtc/AudioSource.java b/talk/app/webrtc/java/src/org/webrtc/AudioSource.java new file mode 100644 index 000000000..8b7a8f7dc --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/AudioSource.java @@ -0,0 +1,38 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.webrtc; + +/** + * Java wrapper for a C++ AudioSourceInterface. Used as the source for one or + * more {@code AudioTrack} objects. + */ +public class AudioSource extends MediaSource { + public AudioSource(long nativeSource) { + super(nativeSource); + } +} diff --git a/talk/app/webrtc/java/src/org/webrtc/AudioTrack.java b/talk/app/webrtc/java/src/org/webrtc/AudioTrack.java new file mode 100644 index 000000000..35d7c41f2 --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/AudioTrack.java @@ -0,0 +1,35 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.webrtc; + +/** Java wrapper for a C++ AudioTrackInterface */ +public class AudioTrack extends MediaStreamTrack { + public AudioTrack(long nativeTrack) { + super(nativeTrack); + } +} diff --git a/talk/app/webrtc/java/src/org/webrtc/IceCandidate.java b/talk/app/webrtc/java/src/org/webrtc/IceCandidate.java new file mode 100644 index 000000000..b5d2dc9da --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/IceCandidate.java @@ -0,0 +1,48 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.webrtc; + +/** + * Representation of a single ICE Candidate, mirroring + * {@code IceCandidateInterface} in the C++ API. + */ +public class IceCandidate { + public final String sdpMid; + public final int sdpMLineIndex; + public final String sdp; + + public IceCandidate(String sdpMid, int sdpMLineIndex, String sdp) { + this.sdpMid = sdpMid; + this.sdpMLineIndex = sdpMLineIndex; + this.sdp = sdp; + } + + public String toString() { + return sdpMid + ":" + sdpMLineIndex + ":" + sdp; + } +} diff --git a/talk/app/webrtc/java/src/org/webrtc/MediaConstraints.java b/talk/app/webrtc/java/src/org/webrtc/MediaConstraints.java new file mode 100644 index 000000000..ef303019c --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/MediaConstraints.java @@ -0,0 +1,85 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.webrtc; + +import java.util.LinkedList; +import java.util.List; + +/** + * Description of media constraints for {@code MediaStream} and + * {@code PeerConnection}. + */ +public class MediaConstraints { + /** Simple String key/value pair. */ + public static class KeyValuePair { + private final String key; + private final String value; + + public KeyValuePair(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + public String toString() { + return key + ": " + value; + } + } + + + public final List mandatory; + public final List optional; + + public MediaConstraints() { + mandatory = new LinkedList(); + optional = new LinkedList(); + } + + private static String stringifyKeyValuePairList(List list) { + StringBuilder builder = new StringBuilder("["); + for (KeyValuePair pair : list) { + if (builder.length() > 1) { + builder.append(", "); + } + builder.append(pair.toString()); + } + return builder.append("]").toString(); + } + + public String toString() { + return "mandatory: " + stringifyKeyValuePairList(mandatory) + + ", optional: " + stringifyKeyValuePairList(optional); + } +} diff --git a/talk/app/webrtc/java/src/org/webrtc/MediaSource.java b/talk/app/webrtc/java/src/org/webrtc/MediaSource.java new file mode 100644 index 000000000..294904900 --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/MediaSource.java @@ -0,0 +1,55 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + + +package org.webrtc; + +/** Java wrapper for a C++ MediaSourceInterface. */ +public class MediaSource { + /** Tracks MediaSourceInterface.SourceState */ + public enum State { + INITIALIZING, LIVE, ENDED, MUTED + } + + final long nativeSource; // Package-protected for PeerConnectionFactory. + + public MediaSource(long nativeSource) { + this.nativeSource = nativeSource; + } + + public State state() { + return nativeState(nativeSource); + } + + void dispose() { + free(nativeSource); + } + + private static native State nativeState(long pointer); + + private static native void free(long nativeSource); +} diff --git a/talk/app/webrtc/java/src/org/webrtc/MediaStream.java b/talk/app/webrtc/java/src/org/webrtc/MediaStream.java new file mode 100644 index 000000000..431c56158 --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/MediaStream.java @@ -0,0 +1,114 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.webrtc; + +import java.util.LinkedList; +import java.util.List; + +/** Java wrapper for a C++ MediaStreamInterface. */ +public class MediaStream { + public final List audioTracks; + public final List videoTracks; + // Package-protected for LocalMediaStream and PeerConnection. + final long nativeStream; + + public MediaStream(long nativeStream) { + audioTracks = new LinkedList(); + videoTracks = new LinkedList(); + this.nativeStream = nativeStream; + } + + public boolean addTrack(AudioTrack track) { + if (nativeAddAudioTrack(nativeStream, track.nativeTrack)) { + audioTracks.add(track); + return true; + } + return false; + } + + public boolean addTrack(VideoTrack track) { + if (nativeAddVideoTrack(nativeStream, track.nativeTrack)) { + videoTracks.add(track); + return true; + } + return false; + } + + public boolean removeTrack(AudioTrack track) { + if (nativeRemoveAudioTrack(nativeStream, track.nativeTrack)) { + audioTracks.remove(track); + return true; + } + return false; + } + + public boolean removeTrack(VideoTrack track) { + if (nativeRemoveVideoTrack(nativeStream, track.nativeTrack)) { + videoTracks.remove(track); + return true; + } + return false; + } + + public void dispose() { + for (AudioTrack track : audioTracks) { + track.dispose(); + } + audioTracks.clear(); + for (VideoTrack track : videoTracks) { + track.dispose(); + } + videoTracks.clear(); + free(nativeStream); + } + + public String label() { + return nativeLabel(nativeStream); + } + + public String toString() { + return "[" + label() + ":A=" + audioTracks.size() + + ":V=" + videoTracks.size() + "]"; + } + + private static native boolean nativeAddAudioTrack( + long nativeStream, long nativeAudioTrack); + + private static native boolean nativeAddVideoTrack( + long nativeStream, long nativeVideoTrack); + + private static native boolean nativeRemoveAudioTrack( + long nativeStream, long nativeAudioTrack); + + private static native boolean nativeRemoveVideoTrack( + long nativeStream, long nativeVideoTrack); + + private static native String nativeLabel(long nativeStream); + + private static native void free(long nativeStream); +} diff --git a/talk/app/webrtc/java/src/org/webrtc/MediaStreamTrack.java b/talk/app/webrtc/java/src/org/webrtc/MediaStreamTrack.java new file mode 100644 index 000000000..5cd2f4c11 --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/MediaStreamTrack.java @@ -0,0 +1,86 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.webrtc; + +/** Java wrapper for a C++ MediaStreamTrackInterface. */ +public class MediaStreamTrack { + /** Tracks MediaStreamTrackInterface.TrackState */ + public enum State { + INITIALIZING, LIVE, ENDED, FAILED + } + + final long nativeTrack; + + public MediaStreamTrack(long nativeTrack) { + this.nativeTrack = nativeTrack; + } + + public String id() { + return nativeId(nativeTrack); + } + + public String kind() { + return nativeKind(nativeTrack); + } + + public boolean enabled() { + return nativeEnabled(nativeTrack); + } + + public boolean setEnabled(boolean enable) { + return nativeSetEnabled(nativeTrack, enable); + } + + public State state() { + return nativeState(nativeTrack); + } + + public boolean setState(State newState) { + return nativeSetState(nativeTrack, newState.ordinal()); + } + + public void dispose() { + free(nativeTrack); + } + + private static native String nativeId(long nativeTrack); + + private static native String nativeKind(long nativeTrack); + + private static native boolean nativeEnabled(long nativeTrack); + + private static native boolean nativeSetEnabled( + long nativeTrack, boolean enabled); + + private static native State nativeState(long nativeTrack); + + private static native boolean nativeSetState( + long nativeTrack, int newState); + + private static native void free(long nativeTrack); +} diff --git a/talk/app/webrtc/java/src/org/webrtc/PeerConnection.java b/talk/app/webrtc/java/src/org/webrtc/PeerConnection.java new file mode 100644 index 000000000..5d08c0457 --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/PeerConnection.java @@ -0,0 +1,194 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + + +package org.webrtc; + +import java.util.LinkedList; +import java.util.List; + +/** + * Java-land version of the PeerConnection APIs; wraps the C++ API + * http://www.webrtc.org/reference/native-apis, which in turn is inspired by the + * JS APIs: http://dev.w3.org/2011/webrtc/editor/webrtc.html and + * http://www.w3.org/TR/mediacapture-streams/ + */ +public class PeerConnection { + static { + System.loadLibrary("jingle_peerconnection_so"); + } + + /** Tracks PeerConnectionInterface::IceGatheringState */ + public enum IceGatheringState { NEW, GATHERING, COMPLETE }; + + + /** Tracks PeerConnectionInterface::IceConnectionState */ + public enum IceConnectionState { + NEW, CHECKING, CONNECTED, COMPLETED, FAILED, DISCONNECTED, CLOSED + }; + + /** Tracks PeerConnectionInterface::SignalingState */ + public enum SignalingState { + STABLE, HAVE_LOCAL_OFFER, HAVE_LOCAL_PRANSWER, HAVE_REMOTE_OFFER, + HAVE_REMOTE_PRANSWER, CLOSED + }; + + /** Java version of PeerConnectionObserver. */ + public static interface Observer { + /** Triggered when the SignalingState changes. */ + public void onSignalingChange(SignalingState newState); + + /** Triggered when the IceConnectionState changes. */ + public void onIceConnectionChange(IceConnectionState newState); + + /** Triggered when the IceGatheringState changes. */ + public void onIceGatheringChange(IceGatheringState newState); + + /** Triggered when a new ICE candidate has been found. */ + public void onIceCandidate(IceCandidate candidate); + + /** Triggered on any error. */ + public void onError(); + + /** Triggered when media is received on a new stream from remote peer. */ + public void onAddStream(MediaStream stream); + + /** Triggered when a remote peer close a stream. */ + public void onRemoveStream(MediaStream stream); + } + + /** Java version of PeerConnectionInterface.IceServer. */ + public static class IceServer { + public final String uri; + public final String username; + public final String password; + + /** Convenience constructor for STUN servers. */ + public IceServer(String uri) { + this(uri, "", ""); + } + + public IceServer(String uri, String username, String password) { + this.uri = uri; + this.username = username; + this.password = password; + } + + public String toString() { + return uri + "[" + username + ":" + password + "]"; + } + } + + private final List localStreams; + private final long nativePeerConnection; + private final long nativeObserver; + + PeerConnection(long nativePeerConnection, long nativeObserver) { + this.nativePeerConnection = nativePeerConnection; + this.nativeObserver = nativeObserver; + localStreams = new LinkedList(); + } + + // JsepInterface. + public native SessionDescription getLocalDescription(); + + public native SessionDescription getRemoteDescription(); + + public native void createOffer( + SdpObserver observer, MediaConstraints constraints); + + public native void createAnswer( + SdpObserver observer, MediaConstraints constraints); + + public native void setLocalDescription( + SdpObserver observer, SessionDescription sdp); + + public native void setRemoteDescription( + SdpObserver observer, SessionDescription sdp); + + public native boolean updateIce( + List iceServers, MediaConstraints constraints); + + public boolean addIceCandidate(IceCandidate candidate) { + return nativeAddIceCandidate( + candidate.sdpMid, candidate.sdpMLineIndex, candidate.sdp); + } + + public boolean addStream( + MediaStream stream, MediaConstraints constraints) { + boolean ret = nativeAddLocalStream(stream.nativeStream, constraints); + if (!ret) { + return false; + } + localStreams.add(stream); + return true; + } + + public void removeStream(MediaStream stream) { + nativeRemoveLocalStream(stream.nativeStream); + localStreams.remove(stream); + } + + public boolean getStats(StatsObserver observer, MediaStreamTrack track) { + return nativeGetStats(observer, (track == null) ? 0 : track.nativeTrack); + } + + // TODO(fischman): add support for DTMF-related methods once that API + // stabilizes. + public native SignalingState signalingState(); + + public native IceConnectionState iceConnectionState(); + + public native IceGatheringState iceGatheringState(); + + public native void close(); + + public void dispose() { + close(); + for (MediaStream stream : localStreams) { + stream.dispose(); + } + localStreams.clear(); + freePeerConnection(nativePeerConnection); + freeObserver(nativeObserver); + } + + private static native void freePeerConnection(long nativePeerConnection); + + private static native void freeObserver(long nativeObserver); + + private native boolean nativeAddIceCandidate( + String sdpMid, int sdpMLineIndex, String iceCandidateSdp); + + private native boolean nativeAddLocalStream( + long nativeStream, MediaConstraints constraints); + + private native void nativeRemoveLocalStream(long nativeStream); + + private native boolean nativeGetStats( + StatsObserver observer, long nativeTrack); +} diff --git a/talk/app/webrtc/java/src/org/webrtc/PeerConnectionFactory.java b/talk/app/webrtc/java/src/org/webrtc/PeerConnectionFactory.java new file mode 100644 index 000000000..03ed03f8e --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/PeerConnectionFactory.java @@ -0,0 +1,119 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + + +package org.webrtc; + +import java.util.List; + +/** + * Java wrapper for a C++ PeerConnectionFactoryInterface. Main entry point to + * the PeerConnection API for clients. + */ +public class PeerConnectionFactory { + static { + System.loadLibrary("jingle_peerconnection_so"); + } + + private final long nativeFactory; + + // |context| is an android.content.Context object, but we keep it untyped here + // to allow building on non-Android platforms. + public static native boolean initializeAndroidGlobals(Object context); + + public PeerConnectionFactory() { + nativeFactory = nativeCreatePeerConnectionFactory(); + if (nativeFactory == 0) { + throw new RuntimeException("Failed to initialize PeerConnectionFactory!"); + } + } + + + public PeerConnection createPeerConnection( + List iceServers, + MediaConstraints constraints, + PeerConnection.Observer observer) { + long nativeObserver = nativeCreateObserver(observer); + if (nativeObserver == 0) { + return null; + } + long nativePeerConnection = nativeCreatePeerConnection( + nativeFactory, iceServers, constraints, nativeObserver); + if (nativePeerConnection == 0) { + return null; + } + return new PeerConnection(nativePeerConnection, nativeObserver); + } + + public MediaStream createLocalMediaStream(String label) { + return new MediaStream( + nativeCreateLocalMediaStream(nativeFactory, label)); + } + + public VideoSource createVideoSource( + VideoCapturer capturer, MediaConstraints constraints) { + return new VideoSource(nativeCreateVideoSource( + nativeFactory, capturer.nativeVideoCapturer, constraints)); + } + + public VideoTrack createVideoTrack(String id, VideoSource source) { + return new VideoTrack(nativeCreateVideoTrack( + nativeFactory, id, source.nativeSource)); + } + + public AudioTrack createAudioTrack(String id) { + return new AudioTrack(nativeCreateAudioTrack(nativeFactory, id)); + } + + public void dispose() { + freeFactory(nativeFactory); + } + + private static native long nativeCreatePeerConnectionFactory(); + + private static native long nativeCreateObserver( + PeerConnection.Observer observer); + + private static native long nativeCreatePeerConnection( + long nativeFactory, List iceServers, + MediaConstraints constraints, long nativeObserver); + + private static native long nativeCreateLocalMediaStream( + long nativeFactory, String label); + + private static native long nativeCreateVideoSource( + long nativeFactory, long nativeVideoCapturer, + MediaConstraints constraints); + + private static native long nativeCreateVideoTrack( + long nativeFactory, String id, long nativeVideoSource); + + private static native long nativeCreateAudioTrack( + long nativeFactory, String id); + + private static native void freeFactory(long nativeFactory); +} diff --git a/talk/app/webrtc/java/src/org/webrtc/SdpObserver.java b/talk/app/webrtc/java/src/org/webrtc/SdpObserver.java new file mode 100644 index 000000000..c9eb14a02 --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/SdpObserver.java @@ -0,0 +1,43 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.webrtc; + +/** Interface for observing SDP-related events. */ +public interface SdpObserver { + /** Called on success of Create{Offer,Answer}(). */ + public void onCreateSuccess(SessionDescription sdp); + + /** Called on success of Set{Local,Remote}Description(). */ + public void onSetSuccess(); + + /** Called on error of Create{Offer,Answer}(). */ + public void onCreateFailure(String error); + + /** Called on error of Set{Local,Remote}Description(). */ + public void onSetFailure(String error); +} diff --git a/talk/app/webrtc/java/src/org/webrtc/SessionDescription.java b/talk/app/webrtc/java/src/org/webrtc/SessionDescription.java new file mode 100644 index 000000000..982db8fc2 --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/SessionDescription.java @@ -0,0 +1,57 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + + +package org.webrtc; + +/** + * Description of an RFC 4566 Session. + * SDPs are passed as serialized Strings in Java-land and are materialized + * to SessionDescriptionInterface as appropriate in the JNI layer. + */ +public class SessionDescription { + /** Java-land enum version of SessionDescriptionInterface's type() string. */ + public static enum Type { + OFFER, PRANSWER, ANSWER; + + public String canonicalForm() { + return name().toLowerCase(); + } + + public static Type fromCanonicalForm(String canonical) { + return Type.valueOf(Type.class, canonical.toUpperCase()); + } + } + + public final Type type; + public final String description; + + public SessionDescription(Type type, String description) { + this.type = type; + this.description = description; + } +} diff --git a/talk/app/webrtc/java/src/org/webrtc/StatsObserver.java b/talk/app/webrtc/java/src/org/webrtc/StatsObserver.java new file mode 100644 index 000000000..e61d8f74d --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/StatsObserver.java @@ -0,0 +1,34 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.webrtc; + +/** Interface for observing Stats reports (see webrtc::StatsObservers). */ +public interface StatsObserver { + /** Called when the reports are ready.*/ + public void onComplete(StatsReport[] reports); +} diff --git a/talk/app/webrtc/java/src/org/webrtc/StatsReport.java b/talk/app/webrtc/java/src/org/webrtc/StatsReport.java new file mode 100644 index 000000000..8285ba232 --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/StatsReport.java @@ -0,0 +1,72 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.webrtc; + +/** Java version of webrtc::StatsReport. */ +public class StatsReport { + + /** Java version of webrtc::StatsReport::Value. */ + public static class Value { + public final String name; + public final String value; + + public Value(String name, String value) { + this.name = name; + this.value = value; + } + + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("[").append(name).append(": ").append(value).append("]"); + return builder.toString(); + } + } + + public final String id; + public final String type; + // Time since 1970-01-01T00:00:00Z in milliseconds. + public final double timestamp; + public final Value[] values; + + public StatsReport(String id, String type, double timestamp, Value[] values) { + this.id = id; + this.type = type; + this.timestamp = timestamp; + this.values = values; + } + + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("id: ").append(id).append(", type: ").append(type) + .append(", timestamp: ").append(timestamp).append(", values: "); + for (int i = 0; i < values.length; ++i) { + builder.append(values[i].toString()).append(", "); + } + return builder.toString(); + } +} diff --git a/talk/app/webrtc/java/src/org/webrtc/VideoCapturer.java b/talk/app/webrtc/java/src/org/webrtc/VideoCapturer.java new file mode 100644 index 000000000..eab5797bf --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/VideoCapturer.java @@ -0,0 +1,53 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.webrtc; + +/** Java version of VideoCapturerInterface. */ +public class VideoCapturer { + final long nativeVideoCapturer; + + private VideoCapturer(long nativeVideoCapturer) { + this.nativeVideoCapturer = nativeVideoCapturer; + } + + public static VideoCapturer create(String deviceName) { + long nativeVideoCapturer = nativeCreateVideoCapturer(deviceName); + if (nativeVideoCapturer == 0) { + return null; + } + return new VideoCapturer(nativeVideoCapturer); + } + + public void dispose() { + free(nativeVideoCapturer); + } + + private static native long nativeCreateVideoCapturer(String deviceName); + + private static native void free(long nativeVideoCapturer); +} diff --git a/talk/app/webrtc/java/src/org/webrtc/VideoRenderer.java b/talk/app/webrtc/java/src/org/webrtc/VideoRenderer.java new file mode 100644 index 000000000..4cc341a48 --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/VideoRenderer.java @@ -0,0 +1,136 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.webrtc; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * Java version of VideoRendererInterface. In addition to allowing clients to + * define their own rendering behavior (by passing in a Callbacks object), this + * class also provides a createGui() method for creating a GUI-rendering window + * on various platforms. + */ +public class VideoRenderer { + + /** Java version of cricket::VideoFrame. */ + public static class I420Frame { + public final int width; + public final int height; + public final int[] yuvStrides; + public final ByteBuffer[] yuvPlanes; + + /** + * Construct a frame of the given dimensions with the specified planar + * data. If |yuvPlanes| is null, new planes of the appropriate sizes are + * allocated. + */ + public I420Frame( + int width, int height, int[] yuvStrides, ByteBuffer[] yuvPlanes) { + this.width = width; + this.height = height; + this.yuvStrides = yuvStrides; + if (yuvPlanes == null) { + yuvPlanes = new ByteBuffer[3]; + yuvPlanes[0] = ByteBuffer.allocateDirect(yuvStrides[0] * height); + yuvPlanes[1] = ByteBuffer.allocateDirect(yuvStrides[1] * height); + yuvPlanes[2] = ByteBuffer.allocateDirect(yuvStrides[2] * height); + } + this.yuvPlanes = yuvPlanes; + } + + /** + * Copy the planes out of |source| into |this| and return |this|. Calling + * this with mismatched frame dimensions is a programming error and will + * likely crash. + */ + public I420Frame copyFrom(I420Frame source) { + if (!Arrays.equals(yuvStrides, source.yuvStrides) || + width != source.width || height != source.height) { + throw new RuntimeException("Mismatched dimensions! Source: " + + source.toString() + ", destination: " + toString()); + } + copyPlane(source.yuvPlanes[0], yuvPlanes[0]); + copyPlane(source.yuvPlanes[1], yuvPlanes[1]); + copyPlane(source.yuvPlanes[2], yuvPlanes[2]); + return this; + } + + @Override + public String toString() { + return width + "x" + height + ":" + yuvStrides[0] + ":" + yuvStrides[1] + + ":" + yuvStrides[2]; + } + + // Copy the bytes out of |src| and into |dst|, ignoring and overwriting + // positon & limit in both buffers. + private void copyPlane(ByteBuffer src, ByteBuffer dst) { + src.position(0).limit(src.capacity()); + dst.put(src); + dst.position(0).limit(dst.capacity()); + } +} + + /** The real meat of VideoRendererInterface. */ + public static interface Callbacks { + public void setSize(int width, int height); + public void renderFrame(I420Frame frame); + } + + // |this| either wraps a native (GUI) renderer or a client-supplied Callbacks + // (Java) implementation; so exactly one of these will be non-0/null. + final long nativeVideoRenderer; + private final Callbacks callbacks; + + public static VideoRenderer createGui(int x, int y) { + long nativeVideoRenderer = nativeCreateGuiVideoRenderer(x, y); + if (nativeVideoRenderer == 0) { + return null; + } + return new VideoRenderer(nativeVideoRenderer); + } + + public VideoRenderer(Callbacks callbacks) { + nativeVideoRenderer = nativeWrapVideoRenderer(callbacks); + this.callbacks = callbacks; + } + + private VideoRenderer(long nativeVideoRenderer) { + this.nativeVideoRenderer = nativeVideoRenderer; + callbacks = null; + } + + public void dispose() { + free(nativeVideoRenderer); + } + + private static native long nativeCreateGuiVideoRenderer(int x, int y); + private static native long nativeWrapVideoRenderer(Callbacks callbacks); + + private static native void free(long nativeVideoRenderer); +} diff --git a/talk/app/webrtc/java/src/org/webrtc/VideoSource.java b/talk/app/webrtc/java/src/org/webrtc/VideoSource.java new file mode 100644 index 000000000..f29f312c6 --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/VideoSource.java @@ -0,0 +1,36 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + + +package org.webrtc; + +/** Java version of VideoSourceInterface. */ +public class VideoSource extends MediaSource { + public VideoSource(long nativeSource) { + super(nativeSource); + } +} diff --git a/talk/app/webrtc/java/src/org/webrtc/VideoTrack.java b/talk/app/webrtc/java/src/org/webrtc/VideoTrack.java new file mode 100644 index 000000000..90e5c9565 --- /dev/null +++ b/talk/app/webrtc/java/src/org/webrtc/VideoTrack.java @@ -0,0 +1,65 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.webrtc; + +import java.util.LinkedList; + +/** Java version of VideoTrackInterface. */ +public class VideoTrack extends MediaStreamTrack { + private final LinkedList renderers; + + public VideoTrack(long nativeTrack) { + super(nativeTrack); + renderers = new LinkedList(); + } + + public void addRenderer(VideoRenderer renderer) { + renderers.add(renderer); + nativeAddRenderer(nativeTrack, renderer.nativeVideoRenderer); + } + + public void removeRenderer(VideoRenderer renderer) { + if (!renderers.remove(renderer)) { + return; + } + nativeRemoveRenderer(nativeTrack, renderer.nativeVideoRenderer); + renderer.dispose(); + } + + public void dispose() { + while (!renderers.isEmpty()) { + removeRenderer(renderers.getFirst()); + } + } + + private static native void nativeAddRenderer( + long nativeTrack, long nativeRenderer); + + private static native void nativeRemoveRenderer( + long nativeTrack, long nativeRenderer); +} diff --git a/talk/app/webrtc/javatests/libjingle_peerconnection_java_unittest.sh b/talk/app/webrtc/javatests/libjingle_peerconnection_java_unittest.sh new file mode 100644 index 000000000..0ecb7309d --- /dev/null +++ b/talk/app/webrtc/javatests/libjingle_peerconnection_java_unittest.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# +# libjingle +# Copyright 2013, 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. + +# Wrapper script for running the Java tests under this directory. + +# Exit with error immediately if any subcommand fails. +set -e + +# Change directory to the PRODUCT_DIR (e.g. out/Debug). +cd -P $(dirname $0) + +export CLASSPATH=`pwd`/junit-4.11.jar +CLASSPATH=$CLASSPATH:`pwd`/libjingle_peerconnection_test.jar +CLASSPATH=$CLASSPATH:`pwd`/libjingle_peerconnection.jar + +export LD_LIBRARY_PATH=`pwd` + +# The RHS value is replaced by the build action that copies this script to +# <(PRODUCT_DIR). +export JAVA_HOME=GYP_JAVA_HOME + +${JAVA_HOME}/bin/java -Xcheck:jni -classpath $CLASSPATH \ + junit.textui.TestRunner org.webrtc.PeerConnectionTest diff --git a/talk/app/webrtc/javatests/src/org/webrtc/PeerConnectionTest.java b/talk/app/webrtc/javatests/src/org/webrtc/PeerConnectionTest.java new file mode 100644 index 000000000..cdd8c734f --- /dev/null +++ b/talk/app/webrtc/javatests/src/org/webrtc/PeerConnectionTest.java @@ -0,0 +1,532 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.webrtc; + +import junit.framework.TestCase; + +import org.junit.Test; +import org.webrtc.PeerConnection.IceConnectionState; +import org.webrtc.PeerConnection.IceGatheringState; +import org.webrtc.PeerConnection.SignalingState; + +import java.lang.ref.WeakReference; +import java.util.IdentityHashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** End-to-end tests for PeerConnection.java. */ +public class PeerConnectionTest extends TestCase { + // Set to true to render video. + private static final boolean RENDER_TO_GUI = false; + + private static class ObserverExpectations implements PeerConnection.Observer, + VideoRenderer.Callbacks, + StatsObserver { + private int expectedIceCandidates = 0; + private int expectedErrors = 0; + private LinkedList expectedSetSizeDimensions = + new LinkedList(); // Alternating width/height. + private int expectedFramesDelivered = 0; + private LinkedList expectedSignalingChanges = + new LinkedList(); + private LinkedList expectedIceConnectionChanges = + new LinkedList(); + private LinkedList expectedIceGatheringChanges = + new LinkedList(); + private LinkedList expectedAddStreamLabels = + new LinkedList(); + private LinkedList expectedRemoveStreamLabels = + new LinkedList(); + public LinkedList gotIceCandidates = + new LinkedList(); + private Map> renderers = + new IdentityHashMap>(); + private int expectedStatsCallbacks = 0; + private LinkedList gotStatsReports = + new LinkedList(); + + public synchronized void expectIceCandidates(int count) { + expectedIceCandidates += count; + } + + public synchronized void onIceCandidate(IceCandidate candidate) { + --expectedIceCandidates; + // We don't assert expectedIceCandidates >= 0 because it's hard to know + // how many to expect, in general. We only use expectIceCandidates to + // assert a minimal count. + gotIceCandidates.add(candidate); + } + + public synchronized void expectError() { + ++expectedErrors; + } + + public synchronized void onError() { + assertTrue(--expectedErrors >= 0); + } + + public synchronized void expectSetSize(int width, int height) { + expectedSetSizeDimensions.add(width); + expectedSetSizeDimensions.add(height); + } + + @Override + public synchronized void setSize(int width, int height) { + assertEquals(width, expectedSetSizeDimensions.removeFirst().intValue()); + assertEquals(height, expectedSetSizeDimensions.removeFirst().intValue()); + } + + public synchronized void expectFramesDelivered(int count) { + expectedFramesDelivered += count; + } + + @Override + public synchronized void renderFrame(VideoRenderer.I420Frame frame) { + --expectedFramesDelivered; + } + + public synchronized void expectSignalingChange(SignalingState newState) { + expectedSignalingChanges.add(newState); + } + + @Override + public synchronized void onSignalingChange(SignalingState newState) { + assertEquals(expectedSignalingChanges.removeFirst(), newState); + } + + public synchronized void expectIceConnectionChange( + IceConnectionState newState) { + expectedIceConnectionChanges.add(newState); + } + + @Override + public void onIceConnectionChange(IceConnectionState newState) { + assertEquals(expectedIceConnectionChanges.removeFirst(), newState); + } + + public synchronized void expectIceGatheringChange( + IceGatheringState newState) { + expectedIceGatheringChanges.add(newState); + } + + @Override + public void onIceGatheringChange(IceGatheringState newState) { + // It's fine to get a variable number of GATHERING messages before + // COMPLETE fires (depending on how long the test runs) so we don't assert + // any particular count. + if (newState == IceGatheringState.GATHERING) { + return; + } + assertEquals(expectedIceGatheringChanges.removeFirst(), newState); + } + + public synchronized void expectAddStream(String label) { + expectedAddStreamLabels.add(label); + } + + public synchronized void onAddStream(MediaStream stream) { + assertEquals(expectedAddStreamLabels.removeFirst(), stream.label()); + assertEquals(1, stream.videoTracks.size()); + assertEquals(1, stream.audioTracks.size()); + assertTrue(stream.videoTracks.get(0).id().endsWith("LMSv0")); + assertTrue(stream.audioTracks.get(0).id().endsWith("LMSa0")); + assertEquals("video", stream.videoTracks.get(0).kind()); + assertEquals("audio", stream.audioTracks.get(0).kind()); + VideoRenderer renderer = createVideoRenderer(this); + stream.videoTracks.get(0).addRenderer(renderer); + assertNull(renderers.put( + stream, new WeakReference(renderer))); + } + + public synchronized void expectRemoveStream(String label) { + expectedRemoveStreamLabels.add(label); + } + + public synchronized void onRemoveStream(MediaStream stream) { + assertEquals(expectedRemoveStreamLabels.removeFirst(), stream.label()); + WeakReference renderer = renderers.remove(stream); + assertNotNull(renderer); + assertNotNull(renderer.get()); + assertEquals(1, stream.videoTracks.size()); + stream.videoTracks.get(0).removeRenderer(renderer.get()); + } + + @Override + public synchronized void onComplete(StatsReport[] reports) { + if (--expectedStatsCallbacks < 0) { + throw new RuntimeException("Unexpected stats report: " + reports); + } + gotStatsReports.add(reports); + } + + public synchronized void expectStatsCallback() { + ++expectedStatsCallbacks; + } + + public synchronized LinkedList takeStatsReports() { + LinkedList got = gotStatsReports; + gotStatsReports = new LinkedList(); + return got; + } + + public synchronized boolean areAllExpectationsSatisfied() { + return expectedIceCandidates <= 0 && // See comment in onIceCandidate. + expectedErrors == 0 && + expectedSignalingChanges.size() == 0 && + expectedIceConnectionChanges.size() == 0 && + expectedIceGatheringChanges.size() == 0 && + expectedAddStreamLabels.size() == 0 && + expectedRemoveStreamLabels.size() == 0 && + expectedSetSizeDimensions.isEmpty() && + expectedFramesDelivered <= 0 && + expectedStatsCallbacks == 0; + } + + public void waitForAllExpectationsToBeSatisfied() { + // TODO(fischman): problems with this approach: + // - come up with something better than a poll loop + // - avoid serializing expectations explicitly; the test is not as robust + // as it could be because it must place expectations between wait + // statements very precisely (e.g. frame must not arrive before its + // expectation, and expectation must not be registered so early as to + // stall a wait). Use callbacks to fire off dependent steps instead of + // explicitly waiting, so there can be just a single wait at the end of + // the test. + while (!areAllExpectationsSatisfied()) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + } + + private static class SdpObserverLatch implements SdpObserver { + private boolean success = false; + private SessionDescription sdp = null; + private String error = null; + private CountDownLatch latch = new CountDownLatch(1); + + public SdpObserverLatch() {} + + public void onCreateSuccess(SessionDescription sdp) { + this.sdp = sdp; + onSetSuccess(); + } + + public void onSetSuccess() { + success = true; + latch.countDown(); + } + + public void onCreateFailure(String error) { + onSetFailure(error); + } + + public void onSetFailure(String error) { + this.error = error; + latch.countDown(); + } + + public boolean await() { + try { + assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); + return getSuccess(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public boolean getSuccess() { + return success; + } + + public SessionDescription getSdp() { + return sdp; + } + + public String getError() { + return error; + } + } + + static int videoWindowsMapped = -1; + + private static class TestRenderer implements VideoRenderer.Callbacks { + public int width = -1; + public int height = -1; + public int numFramesDelivered = 0; + + public void setSize(int width, int height) { + assertEquals(this.width, -1); + assertEquals(this.height, -1); + this.width = width; + this.height = height; + } + + public void renderFrame(VideoRenderer.I420Frame frame) { + ++numFramesDelivered; + } + } + + private static VideoRenderer createVideoRenderer( + ObserverExpectations observer) { + if (!RENDER_TO_GUI) { + return new VideoRenderer(observer); + } + ++videoWindowsMapped; + assertTrue(videoWindowsMapped < 4); + int x = videoWindowsMapped % 2 != 0 ? 700 : 0; + int y = videoWindowsMapped >= 2 ? 0 : 500; + return VideoRenderer.createGui(x, y); + } + + // Return a weak reference to test that ownership is correctly held by + // PeerConnection, not by test code. + private static WeakReference addTracksToPC( + PeerConnectionFactory factory, PeerConnection pc, + VideoSource videoSource, + String streamLabel, String videoTrackId, String audioTrackId, + ObserverExpectations observer) { + MediaStream lMS = factory.createLocalMediaStream(streamLabel); + VideoTrack videoTrack = + factory.createVideoTrack(videoTrackId, videoSource); + assertNotNull(videoTrack); + VideoRenderer videoRenderer = createVideoRenderer(observer); + assertNotNull(videoRenderer); + videoTrack.addRenderer(videoRenderer); + lMS.addTrack(videoTrack); + // Just for fun, let's remove and re-add the track. + lMS.removeTrack(videoTrack); + lMS.addTrack(videoTrack); + lMS.addTrack(factory.createAudioTrack(audioTrackId)); + pc.addStream(lMS, new MediaConstraints()); + return new WeakReference(lMS); + } + + private static void assertEquals( + SessionDescription lhs, SessionDescription rhs) { + assertEquals(lhs.type, rhs.type); + assertEquals(lhs.description, rhs.description); + } + + @Test + public void testCompleteSession() throws Exception { + CountDownLatch testDone = new CountDownLatch(1); + + PeerConnectionFactory factory = new PeerConnectionFactory(); + MediaConstraints constraints = new MediaConstraints(); + + LinkedList iceServers = + new LinkedList(); + iceServers.add(new PeerConnection.IceServer( + "stun:stun.l.google.com:19302")); + iceServers.add(new PeerConnection.IceServer( + "turn:fake.example.com", "fakeUsername", "fakePassword")); + ObserverExpectations offeringExpectations = new ObserverExpectations(); + PeerConnection offeringPC = factory.createPeerConnection( + iceServers, constraints, offeringExpectations); + assertNotNull(offeringPC); + + ObserverExpectations answeringExpectations = new ObserverExpectations(); + PeerConnection answeringPC = factory.createPeerConnection( + iceServers, constraints, answeringExpectations); + assertNotNull(answeringPC); + + // We want to use the same camera for offerer & answerer, so create it here + // instead of in addTracksToPC. + VideoSource videoSource = factory.createVideoSource( + VideoCapturer.create(""), new MediaConstraints()); + + // TODO(fischman): the track ids here and in the addTracksToPC() call + // below hard-code the [av] scheme used in the + // serialized SDP, because the C++ API doesn't auto-translate. + // Drop |label| params from {Audio,Video}Track-related APIs once + // https://code.google.com/p/webrtc/issues/detail?id=1253 is fixed. + WeakReference oLMS = addTracksToPC( + factory, offeringPC, videoSource, "oLMS", "oLMSv0", "oLMSa0", + offeringExpectations); + + SdpObserverLatch sdpLatch = new SdpObserverLatch(); + offeringPC.createOffer(sdpLatch, constraints); + assertTrue(sdpLatch.await()); + SessionDescription offerSdp = sdpLatch.getSdp(); + assertEquals(offerSdp.type, SessionDescription.Type.OFFER); + assertFalse(offerSdp.description.isEmpty()); + + sdpLatch = new SdpObserverLatch(); + answeringExpectations.expectSignalingChange( + SignalingState.HAVE_REMOTE_OFFER); + answeringExpectations.expectAddStream("oLMS"); + answeringPC.setRemoteDescription(sdpLatch, offerSdp); + answeringExpectations.waitForAllExpectationsToBeSatisfied(); + assertEquals( + PeerConnection.SignalingState.STABLE, offeringPC.signalingState()); + assertTrue(sdpLatch.await()); + assertNull(sdpLatch.getSdp()); + + WeakReference aLMS = addTracksToPC( + factory, answeringPC, videoSource, "aLMS", "aLMSv0", "aLMSa0", + answeringExpectations); + + sdpLatch = new SdpObserverLatch(); + answeringPC.createAnswer(sdpLatch, constraints); + assertTrue(sdpLatch.await()); + SessionDescription answerSdp = sdpLatch.getSdp(); + assertEquals(answerSdp.type, SessionDescription.Type.ANSWER); + assertFalse(answerSdp.description.isEmpty()); + + offeringExpectations.expectIceCandidates(2); + answeringExpectations.expectIceCandidates(2); + + sdpLatch = new SdpObserverLatch(); + answeringExpectations.expectSignalingChange(SignalingState.STABLE); + answeringPC.setLocalDescription(sdpLatch, answerSdp); + assertTrue(sdpLatch.await()); + assertNull(sdpLatch.getSdp()); + + sdpLatch = new SdpObserverLatch(); + offeringExpectations.expectSignalingChange(SignalingState.HAVE_LOCAL_OFFER); + offeringPC.setLocalDescription(sdpLatch, offerSdp); + assertTrue(sdpLatch.await()); + assertNull(sdpLatch.getSdp()); + sdpLatch = new SdpObserverLatch(); + offeringExpectations.expectSignalingChange(SignalingState.STABLE); + offeringExpectations.expectAddStream("aLMS"); + offeringPC.setRemoteDescription(sdpLatch, answerSdp); + assertTrue(sdpLatch.await()); + assertNull(sdpLatch.getSdp()); + + offeringExpectations.waitForAllExpectationsToBeSatisfied(); + answeringExpectations.waitForAllExpectationsToBeSatisfied(); + + assertEquals(offeringPC.getLocalDescription().type, offerSdp.type); + assertEquals(offeringPC.getRemoteDescription().type, answerSdp.type); + assertEquals(answeringPC.getLocalDescription().type, answerSdp.type); + assertEquals(answeringPC.getRemoteDescription().type, offerSdp.type); + + if (!RENDER_TO_GUI) { + offeringExpectations.expectSetSize(640, 480); + offeringExpectations.expectSetSize(640, 480); + answeringExpectations.expectSetSize(640, 480); + answeringExpectations.expectSetSize(640, 480); + // Wait for at least some frames to be delivered at each end (number + // chosen arbitrarily). + offeringExpectations.expectFramesDelivered(10); + answeringExpectations.expectFramesDelivered(10); + } + + offeringExpectations.expectIceConnectionChange( + IceConnectionState.CHECKING); + offeringExpectations.expectIceConnectionChange( + IceConnectionState.CONNECTED); + answeringExpectations.expectIceConnectionChange( + IceConnectionState.CHECKING); + answeringExpectations.expectIceConnectionChange( + IceConnectionState.CONNECTED); + + offeringExpectations.expectIceGatheringChange(IceGatheringState.COMPLETE); + answeringExpectations.expectIceGatheringChange(IceGatheringState.COMPLETE); + + for (IceCandidate candidate : offeringExpectations.gotIceCandidates) { + answeringPC.addIceCandidate(candidate); + } + offeringExpectations.gotIceCandidates.clear(); + for (IceCandidate candidate : answeringExpectations.gotIceCandidates) { + offeringPC.addIceCandidate(candidate); + } + answeringExpectations.gotIceCandidates.clear(); + + offeringExpectations.waitForAllExpectationsToBeSatisfied(); + answeringExpectations.waitForAllExpectationsToBeSatisfied(); + + assertEquals( + PeerConnection.SignalingState.STABLE, offeringPC.signalingState()); + assertEquals( + PeerConnection.SignalingState.STABLE, answeringPC.signalingState()); + + if (RENDER_TO_GUI) { + try { + Thread.sleep(3000); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + // TODO(fischman) MOAR test ideas: + // - Test that PC.removeStream() works; requires a second + // createOffer/createAnswer dance. + // - audit each place that uses |constraints| for specifying non-trivial + // constraints (and ensure they're honored). + // - test error cases + // - ensure reasonable coverage of _jni.cc is achieved. Coverage is + // extra-important because of all the free-text (class/method names, etc) + // in JNI-style programming; make sure no typos! + // - Test that shutdown mid-interaction is crash-free. + + // Free the Java-land objects, collect them, and sleep a bit to make sure we + // don't get late-arrival crashes after the Java-land objects have been + // freed. + shutdownPC(offeringPC, offeringExpectations); + offeringPC = null; + shutdownPC(answeringPC, answeringExpectations); + answeringPC = null; + System.gc(); + Thread.sleep(100); + } + + private static void shutdownPC( + PeerConnection pc, ObserverExpectations expectations) { + expectations.expectStatsCallback(); + assertTrue(pc.getStats(expectations, null)); + expectations.waitForAllExpectationsToBeSatisfied(); + expectations.expectIceConnectionChange(IceConnectionState.CLOSED); + expectations.expectSignalingChange(SignalingState.CLOSED); + pc.close(); + expectations.waitForAllExpectationsToBeSatisfied(); + expectations.expectStatsCallback(); + assertTrue(pc.getStats(expectations, null)); + expectations.waitForAllExpectationsToBeSatisfied(); + + System.out.println("FYI stats: "); + int reportIndex = -1; + for (StatsReport[] reports : expectations.takeStatsReports()) { + System.out.println(" Report #" + (++reportIndex)); + for (int i = 0; i < reports.length; ++i) { + System.out.println(" " + reports[i].toString()); + } + } + assertEquals(1, reportIndex); + System.out.println("End stats."); + + pc.dispose(); + } +} diff --git a/talk/app/webrtc/jsep.h b/talk/app/webrtc/jsep.h new file mode 100644 index 000000000..5f28fc885 --- /dev/null +++ b/talk/app/webrtc/jsep.h @@ -0,0 +1,164 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// Interfaces matching the draft-ietf-rtcweb-jsep-01. + +#ifndef TALK_APP_WEBRTC_JSEP_H_ +#define TALK_APP_WEBRTC_JSEP_H_ + +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/refcount.h" + +namespace cricket { +class SessionDescription; +class Candidate; +} // namespace cricket + +namespace webrtc { + +struct SdpParseError { + public: + // The sdp line that causes the error. + std::string line; + // Explains the error. + std::string description; +}; + +// Class representation of an ICE candidate. +// An instance of this interface is supposed to be owned by one class at +// a time and is therefore not expected to be thread safe. +class IceCandidateInterface { + public: + virtual ~IceCandidateInterface() {} + /// If present, this contains the identierfier of the "media stream + // identification" as defined in [RFC 3388] for m-line this candidate is + // assocated with. + virtual std::string sdp_mid() const = 0; + // This indeicates the index (starting at zero) of m-line in the SDP this + // candidate is assocated with. + virtual int sdp_mline_index() const = 0; + virtual const cricket::Candidate& candidate() const = 0; + // Creates a SDP-ized form of this candidate. + virtual bool ToString(std::string* out) const = 0; +}; + +// Creates a IceCandidateInterface based on SDP string. +// Returns NULL if the sdp string can't be parsed. +// TODO(ronghuawu): Deprecated. +IceCandidateInterface* CreateIceCandidate(const std::string& sdp_mid, + int sdp_mline_index, + const std::string& sdp); + +// |error| can be NULL if doesn't care about the failure reason. +IceCandidateInterface* CreateIceCandidate(const std::string& sdp_mid, + int sdp_mline_index, + const std::string& sdp, + SdpParseError* error); + +// This class represents a collection of candidates for a specific m-line. +// This class is used in SessionDescriptionInterface to represent all known +// candidates for a certain m-line. +class IceCandidateCollection { + public: + virtual ~IceCandidateCollection() {} + virtual size_t count() const = 0; + // Returns true if an equivalent |candidate| exist in the collection. + virtual bool HasCandidate(const IceCandidateInterface* candidate) const = 0; + virtual const IceCandidateInterface* at(size_t index) const = 0; +}; + +// Class representation of a Session description. +// An instance of this interface is supposed to be owned by one class at +// a time and is therefore not expected to be thread safe. +class SessionDescriptionInterface { + public: + // Supported types: + static const char kOffer[]; + static const char kPrAnswer[]; + static const char kAnswer[]; + + virtual ~SessionDescriptionInterface() {} + virtual cricket::SessionDescription* description() = 0; + virtual const cricket::SessionDescription* description() const = 0; + // Get the session id and session version, which are defined based on + // RFC 4566 for the SDP o= line. + virtual std::string session_id() const = 0; + virtual std::string session_version() const = 0; + virtual std::string type() const = 0; + // Adds the specified candidate to the description. + // Ownership is not transferred. + // Returns false if the session description does not have a media section that + // corresponds to the |candidate| label. + virtual bool AddCandidate(const IceCandidateInterface* candidate) = 0; + // Returns the number of m- lines in the session description. + virtual size_t number_of_mediasections() const = 0; + // Returns a collection of all candidates that belong to a certain m-line + virtual const IceCandidateCollection* candidates( + size_t mediasection_index) const = 0; + // Serializes the description to SDP. + virtual bool ToString(std::string* out) const = 0; +}; + +// Creates a SessionDescriptionInterface based on SDP string and the type. +// Returns NULL if the sdp string can't be parsed or the type is unsupported. +// TODO(ronghuawu): Deprecated. +SessionDescriptionInterface* CreateSessionDescription(const std::string& type, + const std::string& sdp); + +// |error| can be NULL if doesn't care about the failure reason. +SessionDescriptionInterface* CreateSessionDescription(const std::string& type, + const std::string& sdp, + SdpParseError* error); + +// Jsep CreateOffer and CreateAnswer callback interface. +class CreateSessionDescriptionObserver : public talk_base::RefCountInterface { + public: + // The implementation of the CreateSessionDescriptionObserver takes + // the ownership of the |desc|. + virtual void OnSuccess(SessionDescriptionInterface* desc) = 0; + virtual void OnFailure(const std::string& error) = 0; + + protected: + ~CreateSessionDescriptionObserver() {} +}; + +// Jsep SetLocalDescription and SetRemoteDescription callback interface. +class SetSessionDescriptionObserver : public talk_base::RefCountInterface { + public: + virtual void OnSuccess() = 0; + virtual void OnFailure(const std::string& error) = 0; + + protected: + ~SetSessionDescriptionObserver() {} +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_JSEP_H_ diff --git a/talk/app/webrtc/jsepicecandidate.cc b/talk/app/webrtc/jsepicecandidate.cc new file mode 100644 index 000000000..13cc81273 --- /dev/null +++ b/talk/app/webrtc/jsepicecandidate.cc @@ -0,0 +1,105 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/jsepicecandidate.h" + +#include + +#include "talk/app/webrtc/webrtcsdp.h" +#include "talk/base/stringencode.h" + +namespace webrtc { + +IceCandidateInterface* CreateIceCandidate(const std::string& sdp_mid, + int sdp_mline_index, + const std::string& sdp) { + return CreateIceCandidate(sdp_mid, sdp_mline_index, sdp, NULL); +} + +IceCandidateInterface* CreateIceCandidate(const std::string& sdp_mid, + int sdp_mline_index, + const std::string& sdp, + SdpParseError* error) { + JsepIceCandidate* jsep_ice = new JsepIceCandidate(sdp_mid, sdp_mline_index); + if (!jsep_ice->Initialize(sdp, error)) { + delete jsep_ice; + return NULL; + } + return jsep_ice; +} + +JsepIceCandidate::JsepIceCandidate(const std::string& sdp_mid, + int sdp_mline_index) + : sdp_mid_(sdp_mid), + sdp_mline_index_(sdp_mline_index) { +} + +JsepIceCandidate::JsepIceCandidate(const std::string& sdp_mid, + int sdp_mline_index, + const cricket::Candidate& candidate) + : sdp_mid_(sdp_mid), + sdp_mline_index_(sdp_mline_index), + candidate_(candidate) { +} + +JsepIceCandidate::~JsepIceCandidate() { +} + +bool JsepIceCandidate::Initialize(const std::string& sdp, SdpParseError* err) { + return SdpDeserializeCandidate(sdp, this, err); +} + +bool JsepIceCandidate::ToString(std::string* out) const { + if (!out) + return false; + *out = SdpSerializeCandidate(*this); + return !out->empty(); +} + +JsepCandidateCollection::~JsepCandidateCollection() { + for (std::vector::iterator it = candidates_.begin(); + it != candidates_.end(); ++it) { + delete *it; + } +} + +bool JsepCandidateCollection::HasCandidate( + const IceCandidateInterface* candidate) const { + bool ret = false; + for (std::vector::const_iterator it = candidates_.begin(); + it != candidates_.end(); ++it) { + if ((*it)->sdp_mid() == candidate->sdp_mid() && + (*it)->sdp_mline_index() == candidate->sdp_mline_index() && + (*it)->candidate().IsEquivalent(candidate->candidate())) { + ret = true; + break; + } + } + return ret; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/jsepicecandidate.h b/talk/app/webrtc/jsepicecandidate.h new file mode 100644 index 000000000..54de950e9 --- /dev/null +++ b/talk/app/webrtc/jsepicecandidate.h @@ -0,0 +1,92 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// Implements the IceCandidateInterface. + +#ifndef TALK_APP_WEBRTC_JSEPICECANDIDATE_H_ +#define TALK_APP_WEBRTC_JSEPICECANDIDATE_H_ + +#include + +#include "talk/app/webrtc/jsep.h" +#include "talk/base/constructormagic.h" +#include "talk/p2p/base/candidate.h" + +namespace webrtc { + +class JsepIceCandidate : public IceCandidateInterface { + public: + JsepIceCandidate(const std::string& sdp_mid, int sdp_mline_index); + JsepIceCandidate(const std::string& sdp_mid, int sdp_mline_index, + const cricket::Candidate& candidate); + ~JsepIceCandidate(); + // |error| can be NULL if don't care about the failure reason. + bool Initialize(const std::string& sdp, SdpParseError* err); + void SetCandidate(const cricket::Candidate& candidate) { + candidate_ = candidate; + } + + virtual std::string sdp_mid() const { return sdp_mid_; } + virtual int sdp_mline_index() const { return sdp_mline_index_; } + virtual const cricket::Candidate& candidate() const { + return candidate_; + } + + virtual bool ToString(std::string* out) const; + + private: + std::string sdp_mid_; + int sdp_mline_index_; + cricket::Candidate candidate_; + + DISALLOW_COPY_AND_ASSIGN(JsepIceCandidate); +}; + +// Implementation of IceCandidateCollection. +// This implementation stores JsepIceCandidates. +class JsepCandidateCollection : public IceCandidateCollection { + public: + ~JsepCandidateCollection(); + virtual size_t count() const { + return candidates_.size(); + } + virtual bool HasCandidate(const IceCandidateInterface* candidate) const; + // Adds and takes ownership of the JsepIceCandidate. + virtual void add(JsepIceCandidate* candidate) { + candidates_.push_back(candidate); + } + virtual const IceCandidateInterface* at(size_t index) const { + return candidates_[index]; + } + + private: + std::vector candidates_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_JSEPICECANDIDATE_H_ diff --git a/talk/app/webrtc/jsepsessiondescription.cc b/talk/app/webrtc/jsepsessiondescription.cc new file mode 100644 index 000000000..bc65ca5a1 --- /dev/null +++ b/talk/app/webrtc/jsepsessiondescription.cc @@ -0,0 +1,193 @@ +/* libjingle + * Copyright 2012, 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/app/webrtc/jsepsessiondescription.h" + +#include "talk/app/webrtc/webrtcsdp.h" +#include "talk/base/stringencode.h" +#include "talk/session/media/mediasession.h" + +using talk_base::scoped_ptr; +using cricket::SessionDescription; + +namespace webrtc { + +static const char* kSupportedTypes[] = { + JsepSessionDescription::kOffer, + JsepSessionDescription::kPrAnswer, + JsepSessionDescription::kAnswer +}; + +static bool IsTypeSupported(const std::string& type) { + bool type_supported = false; + for (size_t i = 0; i < ARRAY_SIZE(kSupportedTypes); ++i) { + if (kSupportedTypes[i] == type) { + type_supported = true; + break; + } + } + return type_supported; +} + +const char SessionDescriptionInterface::kOffer[] = "offer"; +const char SessionDescriptionInterface::kPrAnswer[] = "pranswer"; +const char SessionDescriptionInterface::kAnswer[] = "answer"; + +const int JsepSessionDescription::kDefaultVideoCodecId = 100; +const int JsepSessionDescription::kDefaultVideoCodecFramerate = 30; +const char JsepSessionDescription::kDefaultVideoCodecName[] = "VP8"; +const int JsepSessionDescription::kMaxVideoCodecWidth = 1280; +const int JsepSessionDescription::kMaxVideoCodecHeight = 720; +const int JsepSessionDescription::kDefaultVideoCodecPreference = 1; + +SessionDescriptionInterface* CreateSessionDescription(const std::string& type, + const std::string& sdp) { + return CreateSessionDescription(type, sdp, NULL); +} + +SessionDescriptionInterface* CreateSessionDescription(const std::string& type, + const std::string& sdp, + SdpParseError* error) { + if (!IsTypeSupported(type)) { + return NULL; + } + + JsepSessionDescription* jsep_desc = new JsepSessionDescription(type); + if (!jsep_desc->Initialize(sdp, error)) { + delete jsep_desc; + return NULL; + } + return jsep_desc; +} + +JsepSessionDescription::JsepSessionDescription(const std::string& type) + : type_(type) { +} + +JsepSessionDescription::~JsepSessionDescription() {} + +bool JsepSessionDescription::Initialize( + cricket::SessionDescription* description, + const std::string& session_id, + const std::string& session_version) { + if (!description) + return false; + + session_id_ = session_id; + session_version_ = session_version; + description_.reset(description); + candidate_collection_.resize(number_of_mediasections()); + return true; +} + +bool JsepSessionDescription::Initialize(const std::string& sdp, + SdpParseError* error) { + return SdpDeserialize(sdp, this, error); +} + +bool JsepSessionDescription::AddCandidate( + const IceCandidateInterface* candidate) { + if (!candidate || candidate->sdp_mline_index() < 0) + return false; + size_t mediasection_index = 0; + if (!GetMediasectionIndex(candidate, &mediasection_index)) { + return false; + } + if (mediasection_index >= number_of_mediasections()) + return false; + if (candidate_collection_[mediasection_index].HasCandidate(candidate)) { + return true; // Silently ignore this candidate if we already have it. + } + const std::string content_name = + description_->contents()[mediasection_index].name; + const cricket::TransportInfo* transport_info = + description_->GetTransportInfoByName(content_name); + if (!transport_info) { + return false; + } + + cricket::Candidate updated_candidate = candidate->candidate(); + if (updated_candidate.username().empty()) { + updated_candidate.set_username(transport_info->description.ice_ufrag); + } + if (updated_candidate.password().empty()) { + updated_candidate.set_password(transport_info->description.ice_pwd); + } + + candidate_collection_[mediasection_index].add( + new JsepIceCandidate(candidate->sdp_mid(), + mediasection_index, + updated_candidate)); + return true; +} + +size_t JsepSessionDescription::number_of_mediasections() const { + if (!description_) + return 0; + return description_->contents().size(); +} + +const IceCandidateCollection* JsepSessionDescription::candidates( + size_t mediasection_index) const { + if (mediasection_index >= candidate_collection_.size()) + return NULL; + return &candidate_collection_[mediasection_index]; +} + +bool JsepSessionDescription::ToString(std::string* out) const { + if (!description_ || !out) + return false; + *out = SdpSerialize(*this); + return !out->empty(); +} + +bool JsepSessionDescription::GetMediasectionIndex( + const IceCandidateInterface* candidate, + size_t* index) { + if (!candidate || !index) { + return false; + } + *index = static_cast(candidate->sdp_mline_index()); + if (description_ && !candidate->sdp_mid().empty()) { + bool found = false; + // Try to match the sdp_mid with content name. + for (size_t i = 0; i < description_->contents().size(); ++i) { + if (candidate->sdp_mid() == description_->contents().at(i).name) { + *index = i; + found = true; + break; + } + } + if (!found) { + // If the sdp_mid is presented but we can't find a match, we consider + // this as an error. + return false; + } + } + return true; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/jsepsessiondescription.h b/talk/app/webrtc/jsepsessiondescription.h new file mode 100644 index 000000000..7ca7a2242 --- /dev/null +++ b/talk/app/webrtc/jsepsessiondescription.h @@ -0,0 +1,106 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// Implements the SessionDescriptionInterface. + +#ifndef TALK_APP_WEBRTC_JSEPSESSIONDESCRIPTION_H_ +#define TALK_APP_WEBRTC_JSEPSESSIONDESCRIPTION_H_ + +#include +#include + +#include "talk/app/webrtc/jsep.h" +#include "talk/app/webrtc/jsepicecandidate.h" +#include "talk/base/scoped_ptr.h" + +namespace cricket { +class SessionDescription; +} + +namespace webrtc { + +class JsepSessionDescription : public SessionDescriptionInterface { + public: + explicit JsepSessionDescription(const std::string& type); + virtual ~JsepSessionDescription(); + + // |error| can be NULL if don't care about the failure reason. + bool Initialize(const std::string& sdp, SdpParseError* error); + + // Takes ownership of |description|. + bool Initialize(cricket::SessionDescription* description, + const std::string& session_id, + const std::string& session_version); + + virtual cricket::SessionDescription* description() { + return description_.get(); + } + virtual const cricket::SessionDescription* description() const { + return description_.get(); + } + virtual std::string session_id() const { + return session_id_; + } + virtual std::string session_version() const { + return session_version_; + } + virtual std::string type() const { + return type_; + } + // Allow changing the type. Used for testing. + void set_type(const std::string& type) { type_ = type; } + virtual bool AddCandidate(const IceCandidateInterface* candidate); + virtual size_t number_of_mediasections() const; + virtual const IceCandidateCollection* candidates( + size_t mediasection_index) const; + virtual bool ToString(std::string* out) const; + + // Default video encoder settings. The resolution is the max resolution. + // TODO(perkj): Implement proper negotiation of video resolution. + static const int kDefaultVideoCodecId; + static const int kDefaultVideoCodecFramerate; + static const char kDefaultVideoCodecName[]; + static const int kMaxVideoCodecWidth; + static const int kMaxVideoCodecHeight; + static const int kDefaultVideoCodecPreference; + + private: + talk_base::scoped_ptr description_; + std::string session_id_; + std::string session_version_; + std::string type_; + std::vector candidate_collection_; + + bool GetMediasectionIndex(const IceCandidateInterface* candidate, + size_t* index); + + DISALLOW_COPY_AND_ASSIGN(JsepSessionDescription); +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_JSEPSESSIONDESCRIPTION_H_ diff --git a/talk/app/webrtc/jsepsessiondescription_unittest.cc b/talk/app/webrtc/jsepsessiondescription_unittest.cc new file mode 100644 index 000000000..83f67cb3a --- /dev/null +++ b/talk/app/webrtc/jsepsessiondescription_unittest.cc @@ -0,0 +1,223 @@ +/* + * libjingle + * Copyright 2012, 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 + +#include "talk/app/webrtc/jsepicecandidate.h" +#include "talk/app/webrtc/jsepsessiondescription.h" +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringencode.h" +#include "talk/p2p/base/candidate.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/sessiondescription.h" +#include "talk/session/media/mediasession.h" + +using webrtc::IceCandidateCollection; +using webrtc::IceCandidateInterface; +using webrtc::JsepIceCandidate; +using webrtc::JsepSessionDescription; +using webrtc::SessionDescriptionInterface; +using talk_base::scoped_ptr; + +static const char kCandidateUfrag[] = "ufrag"; +static const char kCandidatePwd[] = "pwd"; +static const char kCandidateUfragVoice[] = "ufrag_voice"; +static const char kCandidatePwdVoice[] = "pwd_voice"; +static const char kCandidateUfragVideo[] = "ufrag_video"; +static const char kCandidatePwdVideo[] = "pwd_video"; + +// This creates a session description with both audio and video media contents. +// In SDP this is described by two m lines, one audio and one video. +static cricket::SessionDescription* CreateCricketSessionDescription() { + cricket::SessionDescription* desc(new cricket::SessionDescription()); + // AudioContentDescription + scoped_ptr audio( + new cricket::AudioContentDescription()); + + // VideoContentDescription + scoped_ptr video( + new cricket::VideoContentDescription()); + + audio->AddCodec(cricket::AudioCodec(103, "ISAC", 16000, 0, 0, 0)); + desc->AddContent(cricket::CN_AUDIO, cricket::NS_JINGLE_RTP, + audio.release()); + + video->AddCodec(cricket::VideoCodec(120, "VP8", 640, 480, 30, 0)); + desc->AddContent(cricket::CN_VIDEO, cricket::NS_JINGLE_RTP, + video.release()); + + EXPECT_TRUE(desc->AddTransportInfo( + cricket::TransportInfo( + cricket::CN_AUDIO, + cricket::TransportDescription( + cricket::NS_GINGLE_P2P, + std::vector(), + kCandidateUfragVoice, kCandidatePwdVoice, + cricket::ICEMODE_FULL, NULL, + cricket::Candidates())))); + EXPECT_TRUE(desc->AddTransportInfo( + cricket::TransportInfo(cricket::CN_VIDEO, + cricket::TransportDescription( + cricket::NS_GINGLE_P2P, + std::vector(), + kCandidateUfragVideo, kCandidatePwdVideo, + cricket::ICEMODE_FULL, NULL, + cricket::Candidates())))); + return desc; +} + +class JsepSessionDescriptionTest : public testing::Test { + protected: + virtual void SetUp() { + int port = 1234; + talk_base::SocketAddress address("127.0.0.1", port++); + cricket::Candidate candidate("rtp", cricket::ICE_CANDIDATE_COMPONENT_RTP, + "udp", address, 1, "", + "", "local", "eth0", 0, "1"); + candidate_ = candidate; + const std::string session_id = + talk_base::ToString(talk_base::CreateRandomId64()); + const std::string session_version = + talk_base::ToString(talk_base::CreateRandomId()); + jsep_desc_.reset(new JsepSessionDescription("dummy")); + ASSERT_TRUE(jsep_desc_->Initialize(CreateCricketSessionDescription(), + session_id, session_version)); + } + + std::string Serialize(const SessionDescriptionInterface* desc) { + std::string sdp; + EXPECT_TRUE(desc->ToString(&sdp)); + EXPECT_FALSE(sdp.empty()); + return sdp; + } + + SessionDescriptionInterface* DeSerialize(const std::string& sdp) { + JsepSessionDescription* desc(new JsepSessionDescription("dummy")); + EXPECT_TRUE(desc->Initialize(sdp, NULL)); + return desc; + } + + cricket::Candidate candidate_; + talk_base::scoped_ptr jsep_desc_; +}; + +// Test that number_of_mediasections() returns the number of media contents in +// a session description. +TEST_F(JsepSessionDescriptionTest, CheckSessionDescription) { + EXPECT_EQ(2u, jsep_desc_->number_of_mediasections()); +} + +// Test that we can add a candidate to a session description. +TEST_F(JsepSessionDescriptionTest, AddCandidateWithoutMid) { + JsepIceCandidate jsep_candidate("", 0, candidate_); + EXPECT_TRUE(jsep_desc_->AddCandidate(&jsep_candidate)); + const IceCandidateCollection* ice_candidates = jsep_desc_->candidates(0); + ASSERT_TRUE(ice_candidates != NULL); + EXPECT_EQ(1u, ice_candidates->count()); + const IceCandidateInterface* ice_candidate = ice_candidates->at(0); + ASSERT_TRUE(ice_candidate != NULL); + candidate_.set_username(kCandidateUfragVoice); + candidate_.set_password(kCandidatePwdVoice); + EXPECT_TRUE(ice_candidate->candidate().IsEquivalent(candidate_)); + EXPECT_EQ(0, ice_candidate->sdp_mline_index()); + EXPECT_EQ(0u, jsep_desc_->candidates(1)->count()); +} + +TEST_F(JsepSessionDescriptionTest, AddCandidateWithMid) { + // mid and m-line index don't match, in this case mid is preferred. + JsepIceCandidate jsep_candidate("video", 0, candidate_); + EXPECT_TRUE(jsep_desc_->AddCandidate(&jsep_candidate)); + EXPECT_EQ(0u, jsep_desc_->candidates(0)->count()); + const IceCandidateCollection* ice_candidates = jsep_desc_->candidates(1); + ASSERT_TRUE(ice_candidates != NULL); + EXPECT_EQ(1u, ice_candidates->count()); + const IceCandidateInterface* ice_candidate = ice_candidates->at(0); + ASSERT_TRUE(ice_candidate != NULL); + candidate_.set_username(kCandidateUfragVideo); + candidate_.set_password(kCandidatePwdVideo); + EXPECT_TRUE(ice_candidate->candidate().IsEquivalent(candidate_)); + // The mline index should have been updated according to mid. + EXPECT_EQ(1, ice_candidate->sdp_mline_index()); +} + +TEST_F(JsepSessionDescriptionTest, AddCandidateAlreadyHasUfrag) { + candidate_.set_username(kCandidateUfrag); + candidate_.set_password(kCandidatePwd); + JsepIceCandidate jsep_candidate("audio", 0, candidate_); + EXPECT_TRUE(jsep_desc_->AddCandidate(&jsep_candidate)); + const IceCandidateCollection* ice_candidates = jsep_desc_->candidates(0); + ASSERT_TRUE(ice_candidates != NULL); + EXPECT_EQ(1u, ice_candidates->count()); + const IceCandidateInterface* ice_candidate = ice_candidates->at(0); + ASSERT_TRUE(ice_candidate != NULL); + candidate_.set_username(kCandidateUfrag); + candidate_.set_password(kCandidatePwd); + EXPECT_TRUE(ice_candidate->candidate().IsEquivalent(candidate_)); + + EXPECT_EQ(0u, jsep_desc_->candidates(1)->count()); +} + +// Test that we can not add a candidate if there is no corresponding media +// content in the session description. +TEST_F(JsepSessionDescriptionTest, AddBadCandidate) { + JsepIceCandidate bad_candidate1("", 55, candidate_); + EXPECT_FALSE(jsep_desc_->AddCandidate(&bad_candidate1)); + + JsepIceCandidate bad_candidate2("some weird mid", 0, candidate_); + EXPECT_FALSE(jsep_desc_->AddCandidate(&bad_candidate2)); +} + +// Test that we can serialize a JsepSessionDescription and deserialize it again. +TEST_F(JsepSessionDescriptionTest, SerializeDeserialize) { + std::string sdp = Serialize(jsep_desc_.get()); + + scoped_ptr parsed_jsep_desc(DeSerialize(sdp)); + EXPECT_EQ(2u, parsed_jsep_desc->number_of_mediasections()); + + std::string parsed_sdp = Serialize(parsed_jsep_desc.get()); + EXPECT_EQ(sdp, parsed_sdp); +} + +// Tests that we can serialize and deserialize a JsepSesssionDescription +// with candidates. +TEST_F(JsepSessionDescriptionTest, SerializeDeserializeWithCandidates) { + std::string sdp = Serialize(jsep_desc_.get()); + + // Add a candidate and check that the serialized result is different. + JsepIceCandidate jsep_candidate("audio", 0, candidate_); + EXPECT_TRUE(jsep_desc_->AddCandidate(&jsep_candidate)); + std::string sdp_with_candidate = Serialize(jsep_desc_.get()); + EXPECT_NE(sdp, sdp_with_candidate); + + scoped_ptr parsed_jsep_desc( + DeSerialize(sdp_with_candidate)); + std::string parsed_sdp_with_candidate = Serialize(parsed_jsep_desc.get()); + + EXPECT_EQ(sdp_with_candidate, parsed_sdp_with_candidate); +} diff --git a/talk/app/webrtc/localaudiosource.cc b/talk/app/webrtc/localaudiosource.cc new file mode 100644 index 000000000..9706c0767 --- /dev/null +++ b/talk/app/webrtc/localaudiosource.cc @@ -0,0 +1,127 @@ +/* + * libjingle + * Copyright 2013, 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/app/webrtc/localaudiosource.h" + +#include + +#include "talk/media/base/mediaengine.h" +#include "talk/app/webrtc/mediaconstraintsinterface.h" + +using webrtc::MediaConstraintsInterface; +using webrtc::MediaSourceInterface; + +namespace webrtc { + +// Constraint keys. +// They are declared as static members in mediaconstraintsinterface.h +const char MediaConstraintsInterface::kEchoCancellation[] = + "googEchoCancellation"; +const char MediaConstraintsInterface::kExperimentalEchoCancellation[] = + "googEchoCancellation2"; +const char MediaConstraintsInterface::kAutoGainControl[] = + "googAutoGainControl"; +const char MediaConstraintsInterface::kExperimentalAutoGainControl[] = + "googAutoGainControl2"; +const char MediaConstraintsInterface::kNoiseSuppression[] = + "googNoiseSuppression"; +const char MediaConstraintsInterface::kHighpassFilter[] = + "googHighpassFilter"; +const char MediaConstraintsInterface::kInternalAecDump[] = "internalAecDump"; + +namespace { + +// Convert constraints to audio options. Return false if constraints are +// invalid. +bool FromConstraints(const MediaConstraintsInterface::Constraints& constraints, + cricket::AudioOptions* options) { + bool success = true; + MediaConstraintsInterface::Constraints::const_iterator iter; + + // This design relies on the fact that all the audio constraints are actually + // "options", i.e. boolean-valued and always satisfiable. If the constraints + // are extended to include non-boolean values or actual format constraints, + // a different algorithm will be required. + for (iter = constraints.begin(); iter != constraints.end(); ++iter) { + bool value = false; + + if (!talk_base::FromString(iter->value, &value)) { + success = false; + continue; + } + + if (iter->key == MediaConstraintsInterface::kEchoCancellation) + options->echo_cancellation.Set(value); + else if (iter->key == + MediaConstraintsInterface::kExperimentalEchoCancellation) + options->experimental_aec.Set(value); + else if (iter->key == MediaConstraintsInterface::kAutoGainControl) + options->auto_gain_control.Set(value); + else if (iter->key == + MediaConstraintsInterface::kExperimentalAutoGainControl) + options->experimental_agc.Set(value); + else if (iter->key == MediaConstraintsInterface::kNoiseSuppression) + options->noise_suppression.Set(value); + else if (iter->key == MediaConstraintsInterface::kHighpassFilter) + options->highpass_filter.Set(value); + else if (iter->key == MediaConstraintsInterface::kInternalAecDump) + options->aec_dump.Set(value); + else + success = false; + } + return success; +} + +} // namespace + +talk_base::scoped_refptr LocalAudioSource::Create( + const MediaConstraintsInterface* constraints) { + talk_base::scoped_refptr source( + new talk_base::RefCountedObject()); + source->Initialize(constraints); + return source; +} + +void LocalAudioSource::Initialize( + const MediaConstraintsInterface* constraints) { + if (!constraints) + return; + + // Apply optional constraints first, they will be overwritten by mandatory + // constraints. + FromConstraints(constraints->GetOptional(), &options_); + + cricket::AudioOptions options; + if (!FromConstraints(constraints->GetMandatory(), &options)) { + source_state_ = kEnded; + return; + } + options_.SetAll(options); + source_state_ = kLive; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/localaudiosource.h b/talk/app/webrtc/localaudiosource.h new file mode 100644 index 000000000..e0fda03d7 --- /dev/null +++ b/talk/app/webrtc/localaudiosource.h @@ -0,0 +1,69 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_LOCALAUDIOSOURCE_H_ +#define TALK_APP_WEBRTC_LOCALAUDIOSOURCE_H_ + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/notifier.h" +#include "talk/base/scoped_ptr.h" +#include "talk/media/base/mediachannel.h" + +// LocalAudioSource implements AudioSourceInterface. +// This contains settings for switching audio processing on and off. + +namespace webrtc { + +class MediaConstraintsInterface; + +class LocalAudioSource : public Notifier { + public: + // Creates an instance of LocalAudioSource. + static talk_base::scoped_refptr Create( + const MediaConstraintsInterface* constraints); + + virtual SourceState state() const { return source_state_; } + virtual const cricket::AudioOptions& options() const { return options_; } + + protected: + LocalAudioSource() + : source_state_(kInitializing) { + } + + ~LocalAudioSource() { + } + + private: + void Initialize(const MediaConstraintsInterface* constraints); + + cricket::AudioOptions options_; + SourceState source_state_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_LOCALAUDIOSOURCE_H_ diff --git a/talk/app/webrtc/localaudiosource_unittest.cc b/talk/app/webrtc/localaudiosource_unittest.cc new file mode 100644 index 000000000..ae077878d --- /dev/null +++ b/talk/app/webrtc/localaudiosource_unittest.cc @@ -0,0 +1,118 @@ +/* + * libjingle + * Copyright 2013, 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/app/webrtc/localaudiosource.h" + +#include +#include + +#include "talk/app/webrtc/test/fakeconstraints.h" +#include "talk/base/gunit.h" +#include "talk/media/base/fakemediaengine.h" +#include "talk/media/base/fakevideorenderer.h" +#include "talk/media/devices/fakedevicemanager.h" + +using webrtc::LocalAudioSource; +using webrtc::MediaConstraintsInterface; +using webrtc::MediaSourceInterface; + +TEST(LocalAudioSourceTest, SetValidOptions) { + webrtc::FakeConstraints constraints; + constraints.AddMandatory(MediaConstraintsInterface::kEchoCancellation, false); + constraints.AddOptional( + MediaConstraintsInterface::kExperimentalEchoCancellation, true); + constraints.AddOptional(MediaConstraintsInterface::kAutoGainControl, true); + constraints.AddOptional( + MediaConstraintsInterface::kExperimentalAutoGainControl, true); + constraints.AddMandatory(MediaConstraintsInterface::kNoiseSuppression, false); + constraints.AddOptional(MediaConstraintsInterface::kHighpassFilter, true); + + talk_base::scoped_refptr source = + LocalAudioSource::Create(&constraints); + + bool value; + EXPECT_TRUE(source->options().echo_cancellation.Get(&value)); + EXPECT_FALSE(value); + EXPECT_TRUE(source->options().experimental_aec.Get(&value)); + EXPECT_TRUE(value); + EXPECT_TRUE(source->options().auto_gain_control.Get(&value)); + EXPECT_TRUE(value); + EXPECT_TRUE(source->options().experimental_agc.Get(&value)); + EXPECT_TRUE(value); + EXPECT_TRUE(source->options().noise_suppression.Get(&value)); + EXPECT_FALSE(value); + EXPECT_TRUE(source->options().highpass_filter.Get(&value)); + EXPECT_TRUE(value); +} + +TEST(LocalAudioSourceTest, OptionNotSet) { + webrtc::FakeConstraints constraints; + talk_base::scoped_refptr source = + LocalAudioSource::Create(&constraints); + bool value; + EXPECT_FALSE(source->options().highpass_filter.Get(&value)); +} + +TEST(LocalAudioSourceTest, MandatoryOverridesOptional) { + webrtc::FakeConstraints constraints; + constraints.AddMandatory(MediaConstraintsInterface::kEchoCancellation, false); + constraints.AddOptional(MediaConstraintsInterface::kEchoCancellation, true); + + talk_base::scoped_refptr source = + LocalAudioSource::Create(&constraints); + + bool value; + EXPECT_TRUE(source->options().echo_cancellation.Get(&value)); + EXPECT_FALSE(value); +} + +TEST(LocalAudioSourceTest, InvalidOptional) { + webrtc::FakeConstraints constraints; + constraints.AddOptional(MediaConstraintsInterface::kHighpassFilter, false); + constraints.AddOptional("invalidKey", false); + + talk_base::scoped_refptr source = + LocalAudioSource::Create(&constraints); + + EXPECT_EQ(MediaSourceInterface::kLive, source->state()); + bool value; + EXPECT_TRUE(source->options().highpass_filter.Get(&value)); + EXPECT_FALSE(value); +} + +TEST(LocalAudioSourceTest, InvalidMandatory) { + webrtc::FakeConstraints constraints; + constraints.AddMandatory(MediaConstraintsInterface::kHighpassFilter, false); + constraints.AddMandatory("invalidKey", false); + + talk_base::scoped_refptr source = + LocalAudioSource::Create(&constraints); + + EXPECT_EQ(MediaSourceInterface::kEnded, source->state()); + bool value; + EXPECT_FALSE(source->options().highpass_filter.Get(&value)); +} diff --git a/talk/app/webrtc/localvideosource.cc b/talk/app/webrtc/localvideosource.cc new file mode 100644 index 000000000..2d43885fc --- /dev/null +++ b/talk/app/webrtc/localvideosource.cc @@ -0,0 +1,442 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/localvideosource.h" + +#include + +#include "talk/app/webrtc/mediaconstraintsinterface.h" +#include "talk/session/media/channelmanager.h" + +using cricket::CaptureState; +using webrtc::MediaConstraintsInterface; +using webrtc::MediaSourceInterface; + +namespace webrtc { + +// Constraint keys. Specified by draft-alvestrand-constraints-resolution-00b +// They are declared as static members in mediastreaminterface.h +const char MediaConstraintsInterface::kMinAspectRatio[] = "minAspectRatio"; +const char MediaConstraintsInterface::kMaxAspectRatio[] = "maxAspectRatio"; +const char MediaConstraintsInterface::kMaxWidth[] = "maxWidth"; +const char MediaConstraintsInterface::kMinWidth[] = "minWidth"; +const char MediaConstraintsInterface::kMaxHeight[] = "maxHeight"; +const char MediaConstraintsInterface::kMinHeight[] = "minHeight"; +const char MediaConstraintsInterface::kMaxFrameRate[] = "maxFrameRate"; +const char MediaConstraintsInterface::kMinFrameRate[] = "minFrameRate"; + +// Google-specific keys +const char MediaConstraintsInterface::kNoiseReduction[] = "googNoiseReduction"; +const char MediaConstraintsInterface::kLeakyBucket[] = "googLeakyBucket"; +const char MediaConstraintsInterface::kTemporalLayeredScreencast[] = + "googTemporalLayeredScreencast"; + +} // namespace webrtc + +namespace { + +const double kRoundingTruncation = 0.0005; + +enum { + MSG_VIDEOCAPTURESTATECONNECT, + MSG_VIDEOCAPTURESTATEDISCONNECT, + MSG_VIDEOCAPTURESTATECHANGE, +}; + +// Default resolution. If no constraint is specified, this is the resolution we +// will use. +static const cricket::VideoFormatPod kDefaultResolution = + {640, 480, FPS_TO_INTERVAL(30), cricket::FOURCC_ANY}; + +// List of formats used if the camera doesn't support capability enumeration. +static const cricket::VideoFormatPod kVideoFormats[] = { + {1920, 1080, FPS_TO_INTERVAL(30), cricket::FOURCC_ANY}, + {1280, 720, FPS_TO_INTERVAL(30), cricket::FOURCC_ANY}, + {960, 720, FPS_TO_INTERVAL(30), cricket::FOURCC_ANY}, + {640, 360, FPS_TO_INTERVAL(30), cricket::FOURCC_ANY}, + {640, 480, FPS_TO_INTERVAL(30), cricket::FOURCC_ANY}, + {320, 240, FPS_TO_INTERVAL(30), cricket::FOURCC_ANY}, + {320, 180, FPS_TO_INTERVAL(30), cricket::FOURCC_ANY} +}; + +MediaSourceInterface::SourceState +GetReadyState(cricket::CaptureState state) { + switch (state) { + case cricket::CS_STARTING: + return MediaSourceInterface::kInitializing; + case cricket::CS_RUNNING: + return MediaSourceInterface::kLive; + case cricket::CS_FAILED: + case cricket::CS_NO_DEVICE: + case cricket::CS_STOPPED: + return MediaSourceInterface::kEnded; + case cricket::CS_PAUSED: + return MediaSourceInterface::kMuted; + default: + ASSERT(false && "GetReadyState unknown state"); + } + return MediaSourceInterface::kEnded; +} + +void SetUpperLimit(int new_limit, int* original_limit) { + if (*original_limit < 0 || new_limit < *original_limit) + *original_limit = new_limit; +} + +// Updates |format_upper_limit| from |constraint|. +// If constraint.maxFoo is smaller than format_upper_limit.foo, +// set format_upper_limit.foo to constraint.maxFoo. +void SetUpperLimitFromConstraint( + const MediaConstraintsInterface::Constraint& constraint, + cricket::VideoFormat* format_upper_limit) { + if (constraint.key == MediaConstraintsInterface::kMaxWidth) { + int value = talk_base::FromString(constraint.value); + SetUpperLimit(value, &(format_upper_limit->width)); + } else if (constraint.key == MediaConstraintsInterface::kMaxHeight) { + int value = talk_base::FromString(constraint.value); + SetUpperLimit(value, &(format_upper_limit->height)); + } +} + +// Fills |format_out| with the max width and height allowed by |constraints|. +void FromConstraintsForScreencast( + const MediaConstraintsInterface::Constraints& constraints, + cricket::VideoFormat* format_out) { + typedef MediaConstraintsInterface::Constraints::const_iterator + ConstraintsIterator; + + cricket::VideoFormat upper_limit(-1, -1, 0, 0); + for (ConstraintsIterator constraints_it = constraints.begin(); + constraints_it != constraints.end(); ++constraints_it) + SetUpperLimitFromConstraint(*constraints_it, &upper_limit); + + if (upper_limit.width >= 0) + format_out->width = upper_limit.width; + if (upper_limit.height >= 0) + format_out->height = upper_limit.height; +} + +// Returns true if |constraint| is fulfilled. |format_out| can differ from +// |format_in| if the format is changed by the constraint. Ie - the frame rate +// can be changed by setting maxFrameRate. +bool NewFormatWithConstraints( + const MediaConstraintsInterface::Constraint& constraint, + const cricket::VideoFormat& format_in, + bool mandatory, + cricket::VideoFormat* format_out) { + ASSERT(format_out != NULL); + *format_out = format_in; + + if (constraint.key == MediaConstraintsInterface::kMinWidth) { + int value = talk_base::FromString(constraint.value); + return (value <= format_in.width); + } else if (constraint.key == MediaConstraintsInterface::kMaxWidth) { + int value = talk_base::FromString(constraint.value); + return (value >= format_in.width); + } else if (constraint.key == MediaConstraintsInterface::kMinHeight) { + int value = talk_base::FromString(constraint.value); + return (value <= format_in.height); + } else if (constraint.key == MediaConstraintsInterface::kMaxHeight) { + int value = talk_base::FromString(constraint.value); + return (value >= format_in.height); + } else if (constraint.key == MediaConstraintsInterface::kMinFrameRate) { + int value = talk_base::FromString(constraint.value); + return (value <= cricket::VideoFormat::IntervalToFps(format_in.interval)); + } else if (constraint.key == MediaConstraintsInterface::kMaxFrameRate) { + int value = talk_base::FromString(constraint.value); + if (value == 0) { + if (mandatory) { + // TODO(ronghuawu): Convert the constraint value to float when sub-1fps + // is supported by the capturer. + return false; + } else { + value = 1; + } + } + if (value <= cricket::VideoFormat::IntervalToFps(format_in.interval)) { + format_out->interval = cricket::VideoFormat::FpsToInterval(value); + return true; + } else { + return false; + } + } else if (constraint.key == MediaConstraintsInterface::kMinAspectRatio) { + double value = talk_base::FromString(constraint.value); + // The aspect ratio in |constraint.value| has been converted to a string and + // back to a double, so it may have a rounding error. + // E.g if the value 1/3 is converted to a string, the string will not have + // infinite length. + // We add a margin of 0.0005 which is high enough to detect the same aspect + // ratio but small enough to avoid matching wrong aspect ratios. + double ratio = static_cast(format_in.width) / format_in.height; + return (value <= ratio + kRoundingTruncation); + } else if (constraint.key == MediaConstraintsInterface::kMaxAspectRatio) { + double value = talk_base::FromString(constraint.value); + double ratio = static_cast(format_in.width) / format_in.height; + // Subtract 0.0005 to avoid rounding problems. Same as above. + const double kRoundingTruncation = 0.0005; + return (value >= ratio - kRoundingTruncation); + } else if (constraint.key == MediaConstraintsInterface::kNoiseReduction || + constraint.key == MediaConstraintsInterface::kLeakyBucket || + constraint.key == + MediaConstraintsInterface::kTemporalLayeredScreencast) { + // These are actually options, not constraints, so they can be satisfied + // regardless of the format. + return true; + } + LOG(LS_WARNING) << "Found unknown MediaStream constraint. Name:" + << constraint.key << " Value:" << constraint.value; + return false; +} + +// Removes cricket::VideoFormats from |formats| that don't meet |constraint|. +void FilterFormatsByConstraint( + const MediaConstraintsInterface::Constraint& constraint, + bool mandatory, + std::vector* formats) { + std::vector::iterator format_it = + formats->begin(); + while (format_it != formats->end()) { + // Modify the format_it to fulfill the constraint if possible. + // Delete it otherwise. + if (!NewFormatWithConstraints(constraint, (*format_it), + mandatory, &(*format_it))) { + format_it = formats->erase(format_it); + } else { + ++format_it; + } + } +} + +// Returns a vector of cricket::VideoFormat that best match |constraints|. +std::vector FilterFormats( + const MediaConstraintsInterface::Constraints& mandatory, + const MediaConstraintsInterface::Constraints& optional, + const std::vector& supported_formats) { + typedef MediaConstraintsInterface::Constraints::const_iterator + ConstraintsIterator; + std::vector candidates = supported_formats; + + for (ConstraintsIterator constraints_it = mandatory.begin(); + constraints_it != mandatory.end(); ++constraints_it) + FilterFormatsByConstraint(*constraints_it, true, &candidates); + + if (candidates.size() == 0) + return candidates; + + // Ok - all mandatory checked and we still have a candidate. + // Let's try filtering using the optional constraints. + for (ConstraintsIterator constraints_it = optional.begin(); + constraints_it != optional.end(); ++constraints_it) { + std::vector current_candidates = candidates; + FilterFormatsByConstraint(*constraints_it, false, ¤t_candidates); + if (current_candidates.size() > 0) { + candidates = current_candidates; + } + } + + // We have done as good as we can to filter the supported resolutions. + return candidates; +} + +// Find the format that best matches the default video size. +// Constraints are optional and since the performance of a video call +// might be bad due to bitrate limitations, CPU, and camera performance, +// it is better to select a resolution that is as close as possible to our +// default and still meets the contraints. +const cricket::VideoFormat& GetBestCaptureFormat( + const std::vector& formats) { + ASSERT(formats.size() > 0); + + int default_area = kDefaultResolution.width * kDefaultResolution.height; + + std::vector::const_iterator it = formats.begin(); + std::vector::const_iterator best_it = formats.begin(); + int best_diff = abs(default_area - it->width* it->height); + for (; it != formats.end(); ++it) { + int diff = abs(default_area - it->width* it->height); + if (diff < best_diff) { + best_diff = diff; + best_it = it; + } + } + return *best_it; +} + +// Set |option| to the highest-priority value of |key| in the constraints. +// Return false if the key is mandatory, and the value is invalid. +bool ExtractOption(const MediaConstraintsInterface* all_constraints, + const std::string& key, cricket::Settable* option) { + size_t mandatory = 0; + bool value; + if (FindConstraint(all_constraints, key, &value, &mandatory)) { + option->Set(value); + return true; + } + + return mandatory == 0; +} + +// Search |all_constraints| for known video options. Apply all options that are +// found with valid values, and return false if any mandatory video option was +// found with an invalid value. +bool ExtractVideoOptions(const MediaConstraintsInterface* all_constraints, + cricket::VideoOptions* options) { + bool all_valid = true; + + all_valid &= ExtractOption(all_constraints, + MediaConstraintsInterface::kNoiseReduction, + &(options->video_noise_reduction)); + all_valid &= ExtractOption(all_constraints, + MediaConstraintsInterface::kLeakyBucket, + &(options->video_leaky_bucket)); + all_valid &= ExtractOption(all_constraints, + MediaConstraintsInterface::kTemporalLayeredScreencast, + &(options->video_temporal_layer_screencast)); + + return all_valid; +} + +} // anonymous namespace + +namespace webrtc { + +talk_base::scoped_refptr LocalVideoSource::Create( + cricket::ChannelManager* channel_manager, + cricket::VideoCapturer* capturer, + const webrtc::MediaConstraintsInterface* constraints) { + ASSERT(channel_manager != NULL); + ASSERT(capturer != NULL); + talk_base::scoped_refptr source( + new talk_base::RefCountedObject(channel_manager, + capturer)); + source->Initialize(constraints); + return source; +} + +LocalVideoSource::LocalVideoSource(cricket::ChannelManager* channel_manager, + cricket::VideoCapturer* capturer) + : channel_manager_(channel_manager), + video_capturer_(capturer), + state_(kInitializing) { + channel_manager_->SignalVideoCaptureStateChange.connect( + this, &LocalVideoSource::OnStateChange); +} + +LocalVideoSource::~LocalVideoSource() { + channel_manager_->StopVideoCapture(video_capturer_.get(), format_); + channel_manager_->SignalVideoCaptureStateChange.disconnect(this); +} + +void LocalVideoSource::Initialize( + const webrtc::MediaConstraintsInterface* constraints) { + + std::vector formats; + if (video_capturer_->GetSupportedFormats() && + video_capturer_->GetSupportedFormats()->size() > 0) { + formats = *video_capturer_->GetSupportedFormats(); + } else if (video_capturer_->IsScreencast()) { + // The screen capturer can accept any resolution and we will derive the + // format from the constraints if any. + // Note that this only affects tab capturing, not desktop capturing, + // since desktop capturer does not respect the VideoFormat passed in. + formats.push_back(cricket::VideoFormat(kDefaultResolution)); + } else { + // The VideoCapturer implementation doesn't support capability enumeration. + // We need to guess what the camera support. + for (int i = 0; i < ARRAY_SIZE(kVideoFormats); ++i) { + formats.push_back(cricket::VideoFormat(kVideoFormats[i])); + } + } + + if (constraints) { + MediaConstraintsInterface::Constraints mandatory_constraints = + constraints->GetMandatory(); + MediaConstraintsInterface::Constraints optional_constraints; + optional_constraints = constraints->GetOptional(); + + if (video_capturer_->IsScreencast()) { + // Use the maxWidth and maxHeight allowed by constraints for screencast. + FromConstraintsForScreencast(mandatory_constraints, &(formats[0])); + } + + formats = FilterFormats(mandatory_constraints, optional_constraints, + formats); + } + + if (formats.size() == 0) { + LOG(LS_WARNING) << "Failed to find a suitable video format."; + SetState(kEnded); + return; + } + + cricket::VideoOptions options; + if (!ExtractVideoOptions(constraints, &options)) { + LOG(LS_WARNING) << "Could not satisfy mandatory options."; + SetState(kEnded); + return; + } + options_.SetAll(options); + + format_ = GetBestCaptureFormat(formats); + // Start the camera with our best guess. + // TODO(perkj): Should we try again with another format it it turns out that + // the camera doesn't produce frames with the correct format? Or will + // cricket::VideCapturer be able to re-scale / crop to the requested + // resolution? + if (!channel_manager_->StartVideoCapture(video_capturer_.get(), format_)) { + SetState(kEnded); + return; + } + // Initialize hasn't succeeded until a successful state change has occurred. +} + +void LocalVideoSource::AddSink(cricket::VideoRenderer* output) { + channel_manager_->AddVideoRenderer(video_capturer_.get(), output); +} + +void LocalVideoSource::RemoveSink(cricket::VideoRenderer* output) { + channel_manager_->RemoveVideoRenderer(video_capturer_.get(), output); +} + +// OnStateChange listens to the ChannelManager::SignalVideoCaptureStateChange. +// This signal is triggered for all video capturers. Not only the one we are +// interested in. +void LocalVideoSource::OnStateChange(cricket::VideoCapturer* capturer, + cricket::CaptureState capture_state) { + if (capturer == video_capturer_.get()) { + SetState(GetReadyState(capture_state)); + } +} + +void LocalVideoSource::SetState(SourceState new_state) { + if (VERIFY(state_ != new_state)) { + state_ = new_state; + FireOnChanged(); + } +} + +} // namespace webrtc diff --git a/talk/app/webrtc/localvideosource.h b/talk/app/webrtc/localvideosource.h new file mode 100644 index 000000000..0a3bac091 --- /dev/null +++ b/talk/app/webrtc/localvideosource.h @@ -0,0 +1,100 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_LOCALVIDEOSOURCE_H_ +#define TALK_APP_WEBRTC_LOCALVIDEOSOURCE_H_ + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/notifier.h" +#include "talk/app/webrtc/videosourceinterface.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" +#include "talk/media/base/videocapturer.h" +#include "talk/media/base/videocommon.h" + +// LocalVideoSource implements VideoSourceInterface. It owns a +// cricket::VideoCapturer and make sure the camera is started at a resolution +// that honors the constraints. +// The state is set depending on the result of starting the capturer. +// If the constraint can't be met or the capturer fails to start, the state +// transition to kEnded, otherwise it transitions to kLive. + +namespace cricket { + +class ChannelManager; + +} // namespace cricket + +namespace webrtc { + +class MediaConstraintsInterface; + +class LocalVideoSource : public Notifier, + public sigslot::has_slots<> { + public: + // Creates an instance of LocalVideoSource. + // LocalVideoSource take ownership of |capturer|. + // |constraints| can be NULL and in that case the camera is opened using a + // default resolution. + static talk_base::scoped_refptr Create( + cricket::ChannelManager* channel_manager, + cricket::VideoCapturer* capturer, + const webrtc::MediaConstraintsInterface* constraints); + + virtual SourceState state() const { return state_; } + virtual const cricket::VideoOptions* options() const { return &options_; } + + virtual cricket::VideoCapturer* GetVideoCapturer() { + return video_capturer_.get(); + } + // |output| will be served video frames as long as the underlying capturer + // is running video frames. + virtual void AddSink(cricket::VideoRenderer* output); + virtual void RemoveSink(cricket::VideoRenderer* output); + + protected: + LocalVideoSource(cricket::ChannelManager* channel_manager, + cricket::VideoCapturer* capturer); + ~LocalVideoSource(); + + private: + void Initialize(const webrtc::MediaConstraintsInterface* constraints); + void OnStateChange(cricket::VideoCapturer* capturer, + cricket::CaptureState capture_state); + void SetState(SourceState new_state); + + cricket::ChannelManager* channel_manager_; + talk_base::scoped_ptr video_capturer_; + + cricket::VideoFormat format_; + cricket::VideoOptions options_; + SourceState state_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_LOCALVIDEOSOURCE_H_ diff --git a/talk/app/webrtc/localvideosource_unittest.cc b/talk/app/webrtc/localvideosource_unittest.cc new file mode 100644 index 000000000..24a858886 --- /dev/null +++ b/talk/app/webrtc/localvideosource_unittest.cc @@ -0,0 +1,523 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/localvideosource.h" + +#include +#include + +#include "talk/app/webrtc/test/fakeconstraints.h" +#include "talk/base/gunit.h" +#include "talk/media/base/fakemediaengine.h" +#include "talk/media/base/fakevideorenderer.h" +#include "talk/media/devices/fakedevicemanager.h" +#include "talk/session/media/channelmanager.h" + +using webrtc::FakeConstraints; +using webrtc::LocalVideoSource; +using webrtc::MediaConstraintsInterface; +using webrtc::MediaSourceInterface; +using webrtc::ObserverInterface; +using webrtc::VideoSourceInterface; + +namespace { + +// Max wait time for a test. +const int kMaxWaitMs = 100; + +} // anonymous namespace + + +// TestVideoCapturer extends cricket::FakeVideoCapturer so it can be used for +// testing without known camera formats. +// It keeps its own lists of cricket::VideoFormats for the unit tests in this +// file. +class TestVideoCapturer : public cricket::FakeVideoCapturer { + public: + TestVideoCapturer() : test_without_formats_(false) { + std::vector formats; + formats.push_back(cricket::VideoFormat(1280, 720, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(640, 400, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(320, 240, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(352, 288, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + ResetSupportedFormats(formats); + } + + // This function is used for resetting the supported capture formats and + // simulating a cricket::VideoCapturer implementation that don't support + // capture format enumeration. This is used to simulate the current + // Chrome implementation. + void TestWithoutCameraFormats() { + test_without_formats_ = true; + std::vector formats; + ResetSupportedFormats(formats); + } + + virtual cricket::CaptureState Start( + const cricket::VideoFormat& capture_format) { + if (test_without_formats_) { + std::vector formats; + formats.push_back(capture_format); + ResetSupportedFormats(formats); + } + return FakeVideoCapturer::Start(capture_format); + } + + virtual bool GetBestCaptureFormat(const cricket::VideoFormat& desired, + cricket::VideoFormat* best_format) { + if (test_without_formats_) { + *best_format = desired; + return true; + } + return FakeVideoCapturer::GetBestCaptureFormat(desired, + best_format); + } + + private: + bool test_without_formats_; +}; + +class StateObserver : public ObserverInterface { + public: + explicit StateObserver(VideoSourceInterface* source) + : state_(source->state()), + source_(source) { + } + virtual void OnChanged() { + state_ = source_->state(); + } + MediaSourceInterface::SourceState state() const { return state_; } + + private: + MediaSourceInterface::SourceState state_; + talk_base::scoped_refptr source_; +}; + +class LocalVideoSourceTest : public testing::Test { + protected: + LocalVideoSourceTest() + : channel_manager_(new cricket::ChannelManager( + new cricket::FakeMediaEngine(), + new cricket::FakeDeviceManager(), talk_base::Thread::Current())) { + } + + void SetUp() { + ASSERT_TRUE(channel_manager_->Init()); + capturer_ = new TestVideoCapturer(); + } + + void CreateLocalVideoSource() { + CreateLocalVideoSource(NULL); + } + + void CreateLocalVideoSource( + const webrtc::MediaConstraintsInterface* constraints) { + // VideoSource take ownership of |capturer_| + local_source_ = LocalVideoSource::Create(channel_manager_.get(), + capturer_, + constraints); + + ASSERT_TRUE(local_source_.get() != NULL); + EXPECT_EQ(capturer_, local_source_->GetVideoCapturer()); + + state_observer_.reset(new StateObserver(local_source_)); + local_source_->RegisterObserver(state_observer_.get()); + local_source_->AddSink(&renderer_); + } + + TestVideoCapturer* capturer_; // Raw pointer. Owned by local_source_. + cricket::FakeVideoRenderer renderer_; + talk_base::scoped_ptr channel_manager_; + talk_base::scoped_ptr state_observer_; + talk_base::scoped_refptr local_source_; +}; + + +// Test that a LocalVideoSource transition to kLive state when the capture +// device have started and kEnded if it is stopped. +// It also test that an output can receive video frames. +TEST_F(LocalVideoSourceTest, StartStop) { + // Initialize without constraints. + CreateLocalVideoSource(); + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + + ASSERT_TRUE(capturer_->CaptureFrame()); + EXPECT_EQ(1, renderer_.num_rendered_frames()); + + capturer_->Stop(); + EXPECT_EQ_WAIT(MediaSourceInterface::kEnded, state_observer_->state(), + kMaxWaitMs); +} + +// Test that a LocalVideoSource transition to kEnded if the capture device +// fails. +TEST_F(LocalVideoSourceTest, CameraFailed) { + CreateLocalVideoSource(); + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + + capturer_->SignalStateChange(capturer_, cricket::CS_FAILED); + EXPECT_EQ_WAIT(MediaSourceInterface::kEnded, state_observer_->state(), + kMaxWaitMs); +} + +// Test that the capture output is CIF if we set max constraints to CIF. +// and the capture device support CIF. +TEST_F(LocalVideoSourceTest, MandatoryConstraintCif5Fps) { + FakeConstraints constraints; + constraints.AddMandatory(MediaConstraintsInterface::kMaxWidth, 352); + constraints.AddMandatory(MediaConstraintsInterface::kMaxHeight, 288); + constraints.AddMandatory(MediaConstraintsInterface::kMaxFrameRate, 5); + + CreateLocalVideoSource(&constraints); + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + const cricket::VideoFormat* format = capturer_->GetCaptureFormat(); + ASSERT_TRUE(format != NULL); + EXPECT_EQ(352, format->width); + EXPECT_EQ(288, format->height); + EXPECT_EQ(5, format->framerate()); +} + +// Test that the capture output is 720P if the camera support it and the +// optional constraint is set to 720P. +TEST_F(LocalVideoSourceTest, MandatoryMinVgaOptional720P) { + FakeConstraints constraints; + constraints.AddMandatory(MediaConstraintsInterface::kMinWidth, 640); + constraints.AddMandatory(MediaConstraintsInterface::kMinHeight, 480); + constraints.AddOptional(MediaConstraintsInterface::kMinWidth, 1280); + constraints.AddOptional(MediaConstraintsInterface::kMinAspectRatio, + 1280.0 / 720); + + CreateLocalVideoSource(&constraints); + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + const cricket::VideoFormat* format = capturer_->GetCaptureFormat(); + ASSERT_TRUE(format != NULL); + EXPECT_EQ(1280, format->width); + EXPECT_EQ(720, format->height); + EXPECT_EQ(30, format->framerate()); +} + +// Test that the capture output have aspect ratio 4:3 if a mandatory constraint +// require it even if an optional constraint request a higher resolution +// that don't have this aspect ratio. +TEST_F(LocalVideoSourceTest, MandatoryAspectRatio4To3) { + FakeConstraints constraints; + constraints.AddMandatory(MediaConstraintsInterface::kMinWidth, 640); + constraints.AddMandatory(MediaConstraintsInterface::kMinHeight, 480); + constraints.AddMandatory(MediaConstraintsInterface::kMaxAspectRatio, + 640.0 / 480); + constraints.AddOptional(MediaConstraintsInterface::kMinWidth, 1280); + + CreateLocalVideoSource(&constraints); + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + const cricket::VideoFormat* format = capturer_->GetCaptureFormat(); + ASSERT_TRUE(format != NULL); + EXPECT_EQ(640, format->width); + EXPECT_EQ(480, format->height); + EXPECT_EQ(30, format->framerate()); +} + + +// Test that the source state transition to kEnded if the mandatory aspect ratio +// is set higher than supported. +TEST_F(LocalVideoSourceTest, MandatoryAspectRatioTooHigh) { + FakeConstraints constraints; + constraints.AddMandatory(MediaConstraintsInterface::kMinAspectRatio, 2); + CreateLocalVideoSource(&constraints); + EXPECT_EQ_WAIT(MediaSourceInterface::kEnded, state_observer_->state(), + kMaxWaitMs); +} + +// Test that the source ignores an optional aspect ratio that is higher than +// supported. +TEST_F(LocalVideoSourceTest, OptionalAspectRatioTooHigh) { + FakeConstraints constraints; + constraints.AddOptional(MediaConstraintsInterface::kMinAspectRatio, 2); + CreateLocalVideoSource(&constraints); + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + const cricket::VideoFormat* format = capturer_->GetCaptureFormat(); + ASSERT_TRUE(format != NULL); + double aspect_ratio = static_cast(format->width) / format->height; + EXPECT_LT(aspect_ratio, 2); +} + +// Test that the source starts video with the default resolution if the +// camera doesn't support capability enumeration and there are no constraints. +TEST_F(LocalVideoSourceTest, NoCameraCapability) { + capturer_->TestWithoutCameraFormats(); + + CreateLocalVideoSource(); + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + const cricket::VideoFormat* format = capturer_->GetCaptureFormat(); + ASSERT_TRUE(format != NULL); + EXPECT_EQ(640, format->width); + EXPECT_EQ(480, format->height); + EXPECT_EQ(30, format->framerate()); +} + +// Test that the source can start the video and get the requested aspect ratio +// if the camera doesn't support capability enumeration and the aspect ratio is +// set. +TEST_F(LocalVideoSourceTest, NoCameraCapability16To9Ratio) { + capturer_->TestWithoutCameraFormats(); + + FakeConstraints constraints; + double requested_aspect_ratio = 640.0 / 360; + constraints.AddMandatory(MediaConstraintsInterface::kMinWidth, 640); + constraints.AddMandatory(MediaConstraintsInterface::kMinAspectRatio, + requested_aspect_ratio); + + CreateLocalVideoSource(&constraints); + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + const cricket::VideoFormat* format = capturer_->GetCaptureFormat(); + double aspect_ratio = static_cast(format->width) / format->height; + EXPECT_LE(requested_aspect_ratio, aspect_ratio); +} + +// Test that the source state transitions to kEnded if an unknown mandatory +// constraint is found. +TEST_F(LocalVideoSourceTest, InvalidMandatoryConstraint) { + FakeConstraints constraints; + constraints.AddMandatory("weird key", 640); + + CreateLocalVideoSource(&constraints); + EXPECT_EQ_WAIT(MediaSourceInterface::kEnded, state_observer_->state(), + kMaxWaitMs); +} + +// Test that the source ignores an unknown optional constraint. +TEST_F(LocalVideoSourceTest, InvalidOptionalConstraint) { + FakeConstraints constraints; + constraints.AddOptional("weird key", 640); + + CreateLocalVideoSource(&constraints); + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); +} + +TEST_F(LocalVideoSourceTest, SetValidOptionValues) { + FakeConstraints constraints; + constraints.AddMandatory(MediaConstraintsInterface::kNoiseReduction, "false"); + constraints.AddMandatory( + MediaConstraintsInterface::kTemporalLayeredScreencast, "false"); + constraints.AddOptional( + MediaConstraintsInterface::kLeakyBucket, "true"); + + CreateLocalVideoSource(&constraints); + + bool value = true; + EXPECT_TRUE(local_source_->options()->video_noise_reduction.Get(&value)); + EXPECT_FALSE(value); + EXPECT_TRUE(local_source_->options()-> + video_temporal_layer_screencast.Get(&value)); + EXPECT_FALSE(value); + EXPECT_TRUE(local_source_->options()->video_leaky_bucket.Get(&value)); + EXPECT_TRUE(value); +} + +TEST_F(LocalVideoSourceTest, OptionNotSet) { + FakeConstraints constraints; + CreateLocalVideoSource(&constraints); + bool value; + EXPECT_FALSE(local_source_->options()->video_noise_reduction.Get(&value)); +} + +TEST_F(LocalVideoSourceTest, MandatoryOptionOverridesOptional) { + FakeConstraints constraints; + constraints.AddMandatory( + MediaConstraintsInterface::kNoiseReduction, true); + constraints.AddOptional( + MediaConstraintsInterface::kNoiseReduction, false); + + CreateLocalVideoSource(&constraints); + + bool value = false; + EXPECT_TRUE(local_source_->options()->video_noise_reduction.Get(&value)); + EXPECT_TRUE(value); + EXPECT_FALSE(local_source_->options()->video_leaky_bucket.Get(&value)); +} + +TEST_F(LocalVideoSourceTest, InvalidOptionKeyOptional) { + FakeConstraints constraints; + constraints.AddOptional( + MediaConstraintsInterface::kNoiseReduction, false); + constraints.AddOptional("invalidKey", false); + + CreateLocalVideoSource(&constraints); + + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + bool value = true; + EXPECT_TRUE(local_source_->options()->video_noise_reduction.Get(&value)); + EXPECT_FALSE(value); +} + +TEST_F(LocalVideoSourceTest, InvalidOptionKeyMandatory) { + FakeConstraints constraints; + constraints.AddMandatory( + MediaConstraintsInterface::kNoiseReduction, false); + constraints.AddMandatory("invalidKey", false); + + CreateLocalVideoSource(&constraints); + + EXPECT_EQ_WAIT(MediaSourceInterface::kEnded, state_observer_->state(), + kMaxWaitMs); + bool value; + EXPECT_FALSE(local_source_->options()->video_noise_reduction.Get(&value)); +} + +TEST_F(LocalVideoSourceTest, InvalidOptionValueOptional) { + FakeConstraints constraints; + constraints.AddOptional( + MediaConstraintsInterface::kNoiseReduction, "true"); + constraints.AddOptional( + MediaConstraintsInterface::kLeakyBucket, "not boolean"); + + CreateLocalVideoSource(&constraints); + + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + bool value = false; + EXPECT_TRUE(local_source_->options()->video_noise_reduction.Get(&value)); + EXPECT_TRUE(value); + EXPECT_FALSE(local_source_->options()->video_leaky_bucket.Get(&value)); +} + +TEST_F(LocalVideoSourceTest, InvalidOptionValueMandatory) { + FakeConstraints constraints; + // Optional constraints should be ignored if the mandatory constraints fail. + constraints.AddOptional( + MediaConstraintsInterface::kNoiseReduction, "false"); + // Values are case-sensitive and must be all lower-case. + constraints.AddMandatory( + MediaConstraintsInterface::kLeakyBucket, "True"); + + CreateLocalVideoSource(&constraints); + + EXPECT_EQ_WAIT(MediaSourceInterface::kEnded, state_observer_->state(), + kMaxWaitMs); + bool value; + EXPECT_FALSE(local_source_->options()->video_noise_reduction.Get(&value)); +} + +TEST_F(LocalVideoSourceTest, MixedOptionsAndConstraints) { + FakeConstraints constraints; + constraints.AddMandatory(MediaConstraintsInterface::kMaxWidth, 352); + constraints.AddMandatory(MediaConstraintsInterface::kMaxHeight, 288); + constraints.AddOptional(MediaConstraintsInterface::kMaxFrameRate, 5); + + constraints.AddMandatory( + MediaConstraintsInterface::kNoiseReduction, false); + constraints.AddOptional( + MediaConstraintsInterface::kNoiseReduction, true); + + CreateLocalVideoSource(&constraints); + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + const cricket::VideoFormat* format = capturer_->GetCaptureFormat(); + ASSERT_TRUE(format != NULL); + EXPECT_EQ(352, format->width); + EXPECT_EQ(288, format->height); + EXPECT_EQ(5, format->framerate()); + + bool value = true; + EXPECT_TRUE(local_source_->options()->video_noise_reduction.Get(&value)); + EXPECT_FALSE(value); + EXPECT_FALSE(local_source_->options()->video_leaky_bucket.Get(&value)); +} + +// Tests that the source starts video with the default resolution for +// screencast if no constraint is set. +TEST_F(LocalVideoSourceTest, ScreencastResolutionNoConstraint) { + capturer_->TestWithoutCameraFormats(); + capturer_->SetScreencast(true); + + CreateLocalVideoSource(); + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + const cricket::VideoFormat* format = capturer_->GetCaptureFormat(); + ASSERT_TRUE(format != NULL); + EXPECT_EQ(640, format->width); + EXPECT_EQ(480, format->height); + EXPECT_EQ(30, format->framerate()); +} + +// Tests that the source starts video with the max width and height set by +// constraints for screencast. +TEST_F(LocalVideoSourceTest, ScreencastResolutionWithConstraint) { + FakeConstraints constraints; + constraints.AddMandatory(MediaConstraintsInterface::kMaxWidth, 480); + constraints.AddMandatory(MediaConstraintsInterface::kMaxHeight, 270); + + capturer_->TestWithoutCameraFormats(); + capturer_->SetScreencast(true); + + CreateLocalVideoSource(&constraints); + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + const cricket::VideoFormat* format = capturer_->GetCaptureFormat(); + ASSERT_TRUE(format != NULL); + EXPECT_EQ(480, format->width); + EXPECT_EQ(270, format->height); + EXPECT_EQ(30, format->framerate()); +} + +TEST_F(LocalVideoSourceTest, MandatorySubOneFpsConstraints) { + FakeConstraints constraints; + constraints.AddMandatory(MediaConstraintsInterface::kMaxFrameRate, 0.5); + + CreateLocalVideoSource(&constraints); + EXPECT_EQ_WAIT(MediaSourceInterface::kEnded, state_observer_->state(), + kMaxWaitMs); + ASSERT_TRUE(capturer_->GetCaptureFormat() == NULL); +} + +TEST_F(LocalVideoSourceTest, OptionalSubOneFpsConstraints) { + FakeConstraints constraints; + constraints.AddOptional(MediaConstraintsInterface::kMaxFrameRate, 0.5); + + CreateLocalVideoSource(&constraints); + EXPECT_EQ_WAIT(MediaSourceInterface::kLive, state_observer_->state(), + kMaxWaitMs); + const cricket::VideoFormat* format = capturer_->GetCaptureFormat(); + ASSERT_TRUE(format != NULL); + EXPECT_EQ(1, format->framerate()); +} + diff --git a/talk/app/webrtc/mediaconstraintsinterface.cc b/talk/app/webrtc/mediaconstraintsinterface.cc new file mode 100644 index 000000000..2e6af7728 --- /dev/null +++ b/talk/app/webrtc/mediaconstraintsinterface.cc @@ -0,0 +1,78 @@ +/* + * libjingle + * Copyright 2013, 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/app/webrtc/mediaconstraintsinterface.h" + +#include "talk/base/stringencode.h" + +namespace webrtc { + +const char MediaConstraintsInterface::kValueTrue[] = "true"; +const char MediaConstraintsInterface::kValueFalse[] = "false"; + +// Set |value| to the value associated with the first appearance of |key|, or +// return false if |key| is not found. +bool MediaConstraintsInterface::Constraints::FindFirst( + const std::string& key, std::string* value) const { + for (Constraints::const_iterator iter = begin(); iter != end(); ++iter) { + if (iter->key == key) { + *value = iter->value; + return true; + } + } + return false; +} + +// Find the highest-priority instance of the boolean-valued constraint) named by +// |key| and return its value as |value|. |constraints| can be null. +// If |mandatory_constraints| is non-null, it is incremented if the key appears +// among the mandatory constraints. +// Returns true if the key was found and has a valid boolean value. +// If the key appears multiple times as an optional constraint, appearances +// after the first are ignored. +// Note: Because this uses FindFirst, repeated optional constraints whose +// first instance has an unrecognized value are not handled precisely in +// accordance with the specification. +bool FindConstraint(const MediaConstraintsInterface* constraints, + const std::string& key, bool* value, + size_t* mandatory_constraints) { + std::string string_value; + if (!constraints) { + return false; + } + if (constraints->GetMandatory().FindFirst(key, &string_value)) { + if (mandatory_constraints) + ++*mandatory_constraints; + return talk_base::FromString(string_value, value); + } + if (constraints->GetOptional().FindFirst(key, &string_value)) { + return talk_base::FromString(string_value, value); + } + return false; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/mediaconstraintsinterface.h b/talk/app/webrtc/mediaconstraintsinterface.h new file mode 100644 index 000000000..a6b23c63a --- /dev/null +++ b/talk/app/webrtc/mediaconstraintsinterface.h @@ -0,0 +1,129 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +// This file contains the interface for MediaConstraints, corresponding to +// the definition at +// http://www.w3.org/TR/mediacapture-streams/#mediastreamconstraints and also +// used in WebRTC: http://dev.w3.org/2011/webrtc/editor/webrtc.html#constraints. + +#ifndef TALK_APP_WEBRTC_MEDIACONSTRAINTSINTERFACE_H_ +#define TALK_APP_WEBRTC_MEDIACONSTRAINTSINTERFACE_H_ + +#include +#include + +namespace webrtc { + +// MediaConstraintsInterface +// Interface used for passing arguments about media constraints +// to the MediaStream and PeerConnection implementation. +class MediaConstraintsInterface { + public: + struct Constraint { + Constraint() {} + Constraint(const std::string& key, const std::string value) + : key(key), value(value) { + } + std::string key; + std::string value; + }; + + class Constraints : public std::vector { + public: + bool FindFirst(const std::string& key, std::string* value) const; + }; + + virtual const Constraints& GetMandatory() const = 0; + virtual const Constraints& GetOptional() const = 0; + + + // Constraint keys used by a local video source. + // Specified by draft-alvestrand-constraints-resolution-00b + static const char kMinAspectRatio[]; // minAspectRatio + static const char kMaxAspectRatio[]; // maxAspectRatio + static const char kMaxWidth[]; // maxWidth + static const char kMinWidth[]; // minWidth + static const char kMaxHeight[]; // maxHeight + static const char kMinHeight[]; // minHeight + static const char kMaxFrameRate[]; // maxFrameRate + static const char kMinFrameRate[]; // minFrameRate + + // Constraint keys used by a local audio source. + // These keys are google specific. + static const char kEchoCancellation[]; // googEchoCancellation + static const char kExperimentalEchoCancellation[]; // googEchoCancellation2 + static const char kAutoGainControl[]; // googAutoGainControl + static const char kExperimentalAutoGainControl[]; // googAutoGainControl2 + static const char kNoiseSuppression[]; // googNoiseSuppression + static const char kHighpassFilter[]; // googHighpassFilter + + // Google-specific constraint keys for a local video source + static const char kNoiseReduction[]; // googNoiseReduction + static const char kLeakyBucket[]; // googLeakyBucket + // googTemporalLayeredScreencast + static const char kTemporalLayeredScreencast[]; + + // Constraint keys for CreateOffer / CreateAnswer + // Specified by the W3C PeerConnection spec + static const char kOfferToReceiveVideo[]; // OfferToReceiveVideo + static const char kOfferToReceiveAudio[]; // OfferToReceiveAudio + static const char kVoiceActivityDetection[]; // VoiceActivityDetection + static const char kIceRestart[]; // IceRestart + // These keys are google specific. + static const char kUseRtpMux[]; // googUseRtpMUX + + // Constraints values. + static const char kValueTrue[]; // true + static const char kValueFalse[]; // false + + // Temporary pseudo-constraints used to enable DTLS-SRTP + static const char kEnableDtlsSrtp[]; // Enable DTLS-SRTP + // Temporary pseudo-constraints used to enable DataChannels + static const char kEnableRtpDataChannels[]; // Enable RTP DataChannels + static const char kEnableSctpDataChannels[]; // Enable SCTP DataChannels + + // The prefix of internal-only constraints whose JS set values should be + // stripped by Chrome before passed down to Libjingle. + static const char kInternalConstraintPrefix[]; + + // This constraint is for internal use only, representing the Chrome command + // line flag. So it is prefixed with "internal" so JS values will be removed. + // Used by a local audio source. + static const char kInternalAecDump[]; // internalAecDump + + protected: + // Dtor protected as objects shouldn't be deleted via this interface + virtual ~MediaConstraintsInterface() {} +}; + +bool FindConstraint(const MediaConstraintsInterface* constraints, + const std::string& key, bool* value, + size_t* mandatory_constraints); + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_MEDIACONSTRAINTSINTERFACE_H_ diff --git a/talk/app/webrtc/mediastream.cc b/talk/app/webrtc/mediastream.cc new file mode 100644 index 000000000..aad8e85f8 --- /dev/null +++ b/talk/app/webrtc/mediastream.cc @@ -0,0 +1,112 @@ +/* + * libjingle + * Copyright 2011, 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/app/webrtc/mediastream.h" +#include "talk/base/logging.h" + +namespace webrtc { + +template +static typename V::iterator FindTrack(V* vector, + const std::string& track_id) { + typename V::iterator it = vector->begin(); + for (; it != vector->end(); ++it) { + if ((*it)->id() == track_id) { + break; + } + } + return it; +}; + +talk_base::scoped_refptr MediaStream::Create( + const std::string& label) { + talk_base::RefCountedObject* stream = + new talk_base::RefCountedObject(label); + return stream; +} + +MediaStream::MediaStream(const std::string& label) + : label_(label) { +} + +bool MediaStream::AddTrack(AudioTrackInterface* track) { + return AddTrack(&audio_tracks_, track); +} + +bool MediaStream::AddTrack(VideoTrackInterface* track) { + return AddTrack(&video_tracks_, track); +} + +bool MediaStream::RemoveTrack(AudioTrackInterface* track) { + return RemoveTrack(&audio_tracks_, track); +} + +bool MediaStream::RemoveTrack(VideoTrackInterface* track) { + return RemoveTrack(&video_tracks_, track); +} + +talk_base::scoped_refptr +MediaStream::FindAudioTrack(const std::string& track_id) { + AudioTrackVector::iterator it = FindTrack(&audio_tracks_, track_id); + if (it == audio_tracks_.end()) + return NULL; + return *it; +} + +talk_base::scoped_refptr +MediaStream::FindVideoTrack(const std::string& track_id) { + VideoTrackVector::iterator it = FindTrack(&video_tracks_, track_id); + if (it == video_tracks_.end()) + return NULL; + return *it; +} + +template +bool MediaStream::AddTrack(TrackVector* tracks, Track* track) { + typename TrackVector::iterator it = FindTrack(tracks, track->id()); + if (it != tracks->end()) + return false; + tracks->push_back(track); + FireOnChanged(); + return true; +} + +template +bool MediaStream::RemoveTrack(TrackVector* tracks, + MediaStreamTrackInterface* track) { + ASSERT(tracks != NULL); + if (!track) + return false; + typename TrackVector::iterator it = FindTrack(tracks, track->id()); + if (it == tracks->end()) + return false; + tracks->erase(it); + FireOnChanged(); + return true; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/mediastream.h b/talk/app/webrtc/mediastream.h new file mode 100644 index 000000000..e5ac6ebee --- /dev/null +++ b/talk/app/webrtc/mediastream.h @@ -0,0 +1,75 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +// This file contains the implementation of MediaStreamInterface interface. + +#ifndef TALK_APP_WEBRTC_MEDIASTREAM_H_ +#define TALK_APP_WEBRTC_MEDIASTREAM_H_ + +#include +#include + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/notifier.h" + +namespace webrtc { + +class MediaStream : public Notifier { + public: + static talk_base::scoped_refptr Create(const std::string& label); + + virtual std::string label() const OVERRIDE { return label_; } + + virtual bool AddTrack(AudioTrackInterface* track) OVERRIDE; + virtual bool AddTrack(VideoTrackInterface* track) OVERRIDE; + virtual bool RemoveTrack(AudioTrackInterface* track) OVERRIDE; + virtual bool RemoveTrack(VideoTrackInterface* track) OVERRIDE; + virtual talk_base::scoped_refptr + FindAudioTrack(const std::string& track_id); + virtual talk_base::scoped_refptr + FindVideoTrack(const std::string& track_id); + + virtual AudioTrackVector GetAudioTracks() OVERRIDE { return audio_tracks_; } + virtual VideoTrackVector GetVideoTracks() OVERRIDE { return video_tracks_; } + + protected: + explicit MediaStream(const std::string& label); + + private: + template + bool AddTrack(TrackVector* Tracks, Track* track); + template + bool RemoveTrack(TrackVector* Tracks, MediaStreamTrackInterface* track); + + std::string label_; + AudioTrackVector audio_tracks_; + VideoTrackVector video_tracks_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_MEDIASTREAM_H_ diff --git a/talk/app/webrtc/mediastream_unittest.cc b/talk/app/webrtc/mediastream_unittest.cc new file mode 100644 index 000000000..bb2d50e6a --- /dev/null +++ b/talk/app/webrtc/mediastream_unittest.cc @@ -0,0 +1,162 @@ +/* + * libjingle + * Copyright 2011, 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 + +#include "talk/app/webrtc/audiotrack.h" +#include "talk/app/webrtc/mediastream.h" +#include "talk/app/webrtc/videotrack.h" +#include "talk/base/refcount.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/gunit.h" +#include "testing/base/public/gmock.h" + +static const char kStreamLabel1[] = "local_stream_1"; +static const char kVideoTrackId[] = "dummy_video_cam_1"; +static const char kAudioTrackId[] = "dummy_microphone_1"; + +using talk_base::scoped_refptr; +using ::testing::Exactly; + +namespace webrtc { + +// Helper class to test Observer. +class MockObserver : public ObserverInterface { + public: + MockObserver() {} + + MOCK_METHOD0(OnChanged, void()); +}; + +class MediaStreamTest: public testing::Test { + protected: + virtual void SetUp() { + stream_ = MediaStream::Create(kStreamLabel1); + ASSERT_TRUE(stream_.get() != NULL); + + video_track_ = VideoTrack::Create(kVideoTrackId, NULL); + ASSERT_TRUE(video_track_.get() != NULL); + EXPECT_EQ(MediaStreamTrackInterface::kInitializing, video_track_->state()); + + audio_track_ = AudioTrack::Create(kAudioTrackId, NULL); + + ASSERT_TRUE(audio_track_.get() != NULL); + EXPECT_EQ(MediaStreamTrackInterface::kInitializing, audio_track_->state()); + + EXPECT_TRUE(stream_->AddTrack(video_track_)); + EXPECT_FALSE(stream_->AddTrack(video_track_)); + EXPECT_TRUE(stream_->AddTrack(audio_track_)); + EXPECT_FALSE(stream_->AddTrack(audio_track_)); + } + + void ChangeTrack(MediaStreamTrackInterface* track) { + MockObserver observer; + track->RegisterObserver(&observer); + + EXPECT_CALL(observer, OnChanged()) + .Times(Exactly(1)); + track->set_enabled(false); + EXPECT_FALSE(track->enabled()); + + EXPECT_CALL(observer, OnChanged()) + .Times(Exactly(1)); + track->set_state(MediaStreamTrackInterface::kLive); + EXPECT_EQ(MediaStreamTrackInterface::kLive, track->state()); + } + + scoped_refptr stream_; + scoped_refptr audio_track_; + scoped_refptr video_track_; +}; + +TEST_F(MediaStreamTest, GetTrackInfo) { + ASSERT_EQ(1u, stream_->GetVideoTracks().size()); + ASSERT_EQ(1u, stream_->GetAudioTracks().size()); + + // Verify the video track. + scoped_refptr video_track( + stream_->GetVideoTracks()[0]); + EXPECT_EQ(0, video_track->id().compare(kVideoTrackId)); + EXPECT_TRUE(video_track->enabled()); + + ASSERT_EQ(1u, stream_->GetVideoTracks().size()); + EXPECT_TRUE(stream_->GetVideoTracks()[0].get() == video_track.get()); + EXPECT_TRUE(stream_->FindVideoTrack(video_track->id()).get() + == video_track.get()); + video_track = stream_->GetVideoTracks()[0]; + EXPECT_EQ(0, video_track->id().compare(kVideoTrackId)); + EXPECT_TRUE(video_track->enabled()); + + // Verify the audio track. + scoped_refptr audio_track( + stream_->GetAudioTracks()[0]); + EXPECT_EQ(0, audio_track->id().compare(kAudioTrackId)); + EXPECT_TRUE(audio_track->enabled()); + ASSERT_EQ(1u, stream_->GetAudioTracks().size()); + EXPECT_TRUE(stream_->GetAudioTracks()[0].get() == audio_track.get()); + EXPECT_TRUE(stream_->FindAudioTrack(audio_track->id()).get() + == audio_track.get()); + audio_track = stream_->GetAudioTracks()[0]; + EXPECT_EQ(0, audio_track->id().compare(kAudioTrackId)); + EXPECT_TRUE(audio_track->enabled()); +} + +TEST_F(MediaStreamTest, RemoveTrack) { + MockObserver observer; + stream_->RegisterObserver(&observer); + + EXPECT_CALL(observer, OnChanged()) + .Times(Exactly(2)); + + EXPECT_TRUE(stream_->RemoveTrack(audio_track_)); + EXPECT_FALSE(stream_->RemoveTrack(audio_track_)); + EXPECT_EQ(0u, stream_->GetAudioTracks().size()); + EXPECT_EQ(0u, stream_->GetAudioTracks().size()); + + EXPECT_TRUE(stream_->RemoveTrack(video_track_)); + EXPECT_FALSE(stream_->RemoveTrack(video_track_)); + + EXPECT_EQ(0u, stream_->GetVideoTracks().size()); + EXPECT_EQ(0u, stream_->GetVideoTracks().size()); + + EXPECT_FALSE(stream_->RemoveTrack(static_cast(NULL))); + EXPECT_FALSE(stream_->RemoveTrack(static_cast(NULL))); +} + +TEST_F(MediaStreamTest, ChangeVideoTrack) { + scoped_refptr video_track( + stream_->GetVideoTracks()[0]); + ChangeTrack(video_track.get()); +} + +TEST_F(MediaStreamTest, ChangeAudioTrack) { + scoped_refptr audio_track( + stream_->GetAudioTracks()[0]); + ChangeTrack(audio_track.get()); +} + +} // namespace webrtc diff --git a/talk/app/webrtc/mediastreamhandler.cc b/talk/app/webrtc/mediastreamhandler.cc new file mode 100644 index 000000000..a6a45b214 --- /dev/null +++ b/talk/app/webrtc/mediastreamhandler.cc @@ -0,0 +1,440 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/mediastreamhandler.h" + +#include "talk/app/webrtc/localaudiosource.h" +#include "talk/app/webrtc/localvideosource.h" +#include "talk/app/webrtc/videosourceinterface.h" + +namespace webrtc { + +TrackHandler::TrackHandler(MediaStreamTrackInterface* track, uint32 ssrc) + : track_(track), + ssrc_(ssrc), + state_(track->state()), + enabled_(track->enabled()) { + track_->RegisterObserver(this); +} + +TrackHandler::~TrackHandler() { + track_->UnregisterObserver(this); +} + +void TrackHandler::OnChanged() { + if (state_ != track_->state()) { + state_ = track_->state(); + OnStateChanged(); + } + if (enabled_ != track_->enabled()) { + enabled_ = track_->enabled(); + OnEnabledChanged(); + } +} + +LocalAudioTrackHandler::LocalAudioTrackHandler( + AudioTrackInterface* track, + uint32 ssrc, + AudioProviderInterface* provider) + : TrackHandler(track, ssrc), + audio_track_(track), + provider_(provider) { + OnEnabledChanged(); +} + +LocalAudioTrackHandler::~LocalAudioTrackHandler() { +} + +void LocalAudioTrackHandler::OnStateChanged() { + // TODO(perkj): What should happen when the state change? +} + +void LocalAudioTrackHandler::Stop() { + cricket::AudioOptions options; + provider_->SetAudioSend(ssrc(), false, options); +} + +void LocalAudioTrackHandler::OnEnabledChanged() { + cricket::AudioOptions options; + if (audio_track_->enabled() && audio_track_->GetSource()) { + options = static_cast( + audio_track_->GetSource())->options(); + } + provider_->SetAudioSend(ssrc(), audio_track_->enabled(), options); +} + +RemoteAudioTrackHandler::RemoteAudioTrackHandler( + AudioTrackInterface* track, + uint32 ssrc, + AudioProviderInterface* provider) + : TrackHandler(track, ssrc), + audio_track_(track), + provider_(provider) { + OnEnabledChanged(); + provider_->SetAudioRenderer(ssrc, audio_track_->FrameInput()); +} + +RemoteAudioTrackHandler::~RemoteAudioTrackHandler() { +} + +void RemoteAudioTrackHandler::Stop() { + provider_->SetAudioPlayout(ssrc(), false); +} + +void RemoteAudioTrackHandler::OnStateChanged() { +} + +void RemoteAudioTrackHandler::OnEnabledChanged() { + provider_->SetAudioPlayout(ssrc(), audio_track_->enabled()); +} + +LocalVideoTrackHandler::LocalVideoTrackHandler( + VideoTrackInterface* track, + uint32 ssrc, + VideoProviderInterface* provider) + : TrackHandler(track, ssrc), + local_video_track_(track), + provider_(provider) { + VideoSourceInterface* source = local_video_track_->GetSource(); + if (source) + provider_->SetCaptureDevice(ssrc, source->GetVideoCapturer()); + OnEnabledChanged(); +} + +LocalVideoTrackHandler::~LocalVideoTrackHandler() { +} + +void LocalVideoTrackHandler::OnStateChanged() { +} + +void LocalVideoTrackHandler::Stop() { + provider_->SetCaptureDevice(ssrc(), NULL); + provider_->SetVideoSend(ssrc(), false, NULL); +} + +void LocalVideoTrackHandler::OnEnabledChanged() { + const cricket::VideoOptions* options = NULL; + VideoSourceInterface* source = local_video_track_->GetSource(); + if (local_video_track_->enabled() && source) { + options = source->options(); + } + provider_->SetVideoSend(ssrc(), local_video_track_->enabled(), options); +} + +RemoteVideoTrackHandler::RemoteVideoTrackHandler( + VideoTrackInterface* track, + uint32 ssrc, + VideoProviderInterface* provider) + : TrackHandler(track, ssrc), + remote_video_track_(track), + provider_(provider) { + OnEnabledChanged(); +} + +RemoteVideoTrackHandler::~RemoteVideoTrackHandler() { +} + +void RemoteVideoTrackHandler::Stop() { + // Since cricket::VideoRenderer is not reference counted + // we need to remove the renderer before we are deleted. + provider_->SetVideoPlayout(ssrc(), false, NULL); +} + +void RemoteVideoTrackHandler::OnStateChanged() { +} + +void RemoteVideoTrackHandler::OnEnabledChanged() { + provider_->SetVideoPlayout(ssrc(), + remote_video_track_->enabled(), + remote_video_track_->FrameInput()); +} + +MediaStreamHandler::MediaStreamHandler(MediaStreamInterface* stream, + AudioProviderInterface* audio_provider, + VideoProviderInterface* video_provider) + : stream_(stream), + audio_provider_(audio_provider), + video_provider_(video_provider) { +} + +MediaStreamHandler::~MediaStreamHandler() { + for (TrackHandlers::iterator it = track_handlers_.begin(); + it != track_handlers_.end(); ++it) { + delete *it; + } +} + +void MediaStreamHandler::RemoveTrack(MediaStreamTrackInterface* track) { + for (TrackHandlers::iterator it = track_handlers_.begin(); + it != track_handlers_.end(); ++it) { + if ((*it)->track() == track) { + TrackHandler* track = *it; + track->Stop(); + delete track; + track_handlers_.erase(it); + break; + } + } +} + +TrackHandler* MediaStreamHandler::FindTrackHandler( + MediaStreamTrackInterface* track) { + TrackHandlers::iterator it = track_handlers_.begin(); + for (; it != track_handlers_.end(); ++it) { + if ((*it)->track() == track) { + return *it; + break; + } + } + return NULL; +} + +MediaStreamInterface* MediaStreamHandler::stream() { + return stream_.get(); +} + +void MediaStreamHandler::OnChanged() { +} + +void MediaStreamHandler::Stop() { + for (TrackHandlers::const_iterator it = track_handlers_.begin(); + it != track_handlers_.end(); ++it) { + (*it)->Stop(); + } +} + +LocalMediaStreamHandler::LocalMediaStreamHandler( + MediaStreamInterface* stream, + AudioProviderInterface* audio_provider, + VideoProviderInterface* video_provider) + : MediaStreamHandler(stream, audio_provider, video_provider) { +} + +LocalMediaStreamHandler::~LocalMediaStreamHandler() { +} + +void LocalMediaStreamHandler::AddAudioTrack(AudioTrackInterface* audio_track, + uint32 ssrc) { + ASSERT(!FindTrackHandler(audio_track)); + + TrackHandler* handler(new LocalAudioTrackHandler(audio_track, ssrc, + audio_provider_)); + track_handlers_.push_back(handler); +} + +void LocalMediaStreamHandler::AddVideoTrack(VideoTrackInterface* video_track, + uint32 ssrc) { + ASSERT(!FindTrackHandler(video_track)); + + TrackHandler* handler(new LocalVideoTrackHandler(video_track, ssrc, + video_provider_)); + track_handlers_.push_back(handler); +} + +RemoteMediaStreamHandler::RemoteMediaStreamHandler( + MediaStreamInterface* stream, + AudioProviderInterface* audio_provider, + VideoProviderInterface* video_provider) + : MediaStreamHandler(stream, audio_provider, video_provider) { +} + +RemoteMediaStreamHandler::~RemoteMediaStreamHandler() { +} + +void RemoteMediaStreamHandler::AddAudioTrack(AudioTrackInterface* audio_track, + uint32 ssrc) { + ASSERT(!FindTrackHandler(audio_track)); + TrackHandler* handler( + new RemoteAudioTrackHandler(audio_track, ssrc, audio_provider_)); + track_handlers_.push_back(handler); +} + +void RemoteMediaStreamHandler::AddVideoTrack(VideoTrackInterface* video_track, + uint32 ssrc) { + ASSERT(!FindTrackHandler(video_track)); + TrackHandler* handler( + new RemoteVideoTrackHandler(video_track, ssrc, video_provider_)); + track_handlers_.push_back(handler); +} + +MediaStreamHandlerContainer::MediaStreamHandlerContainer( + AudioProviderInterface* audio_provider, + VideoProviderInterface* video_provider) + : audio_provider_(audio_provider), + video_provider_(video_provider) { +} + +MediaStreamHandlerContainer::~MediaStreamHandlerContainer() { + ASSERT(remote_streams_handlers_.empty()); + ASSERT(local_streams_handlers_.empty()); +} + +void MediaStreamHandlerContainer::TearDown() { + for (StreamHandlerList::iterator it = remote_streams_handlers_.begin(); + it != remote_streams_handlers_.end(); ++it) { + (*it)->Stop(); + delete *it; + } + remote_streams_handlers_.clear(); + for (StreamHandlerList::iterator it = local_streams_handlers_.begin(); + it != local_streams_handlers_.end(); ++it) { + (*it)->Stop(); + delete *it; + } + local_streams_handlers_.clear(); +} + +void MediaStreamHandlerContainer::RemoveRemoteStream( + MediaStreamInterface* stream) { + DeleteStreamHandler(&remote_streams_handlers_, stream); +} + +void MediaStreamHandlerContainer::AddRemoteAudioTrack( + MediaStreamInterface* stream, + AudioTrackInterface* audio_track, + uint32 ssrc) { + MediaStreamHandler* handler = FindStreamHandler(remote_streams_handlers_, + stream); + if (handler == NULL) { + handler = CreateRemoteStreamHandler(stream); + } + handler->AddAudioTrack(audio_track, ssrc); +} + +void MediaStreamHandlerContainer::AddRemoteVideoTrack( + MediaStreamInterface* stream, + VideoTrackInterface* video_track, + uint32 ssrc) { + MediaStreamHandler* handler = FindStreamHandler(remote_streams_handlers_, + stream); + if (handler == NULL) { + handler = CreateRemoteStreamHandler(stream); + } + handler->AddVideoTrack(video_track, ssrc); +} + +void MediaStreamHandlerContainer::RemoveRemoteTrack( + MediaStreamInterface* stream, + MediaStreamTrackInterface* track) { + MediaStreamHandler* handler = FindStreamHandler(remote_streams_handlers_, + stream); + if (!VERIFY(handler != NULL)) { + LOG(LS_WARNING) << "Local MediaStreamHandler for stream with id " + << stream->label() << "doesnt't exist."; + return; + } + handler->RemoveTrack(track); +} + +void MediaStreamHandlerContainer::RemoveLocalStream( + MediaStreamInterface* stream) { + DeleteStreamHandler(&local_streams_handlers_, stream); +} + +void MediaStreamHandlerContainer::AddLocalAudioTrack( + MediaStreamInterface* stream, + AudioTrackInterface* audio_track, + uint32 ssrc) { + MediaStreamHandler* handler = FindStreamHandler(local_streams_handlers_, + stream); + if (handler == NULL) { + handler = CreateLocalStreamHandler(stream); + } + handler->AddAudioTrack(audio_track, ssrc); +} + +void MediaStreamHandlerContainer::AddLocalVideoTrack( + MediaStreamInterface* stream, + VideoTrackInterface* video_track, + uint32 ssrc) { + MediaStreamHandler* handler = FindStreamHandler(local_streams_handlers_, + stream); + if (handler == NULL) { + handler = CreateLocalStreamHandler(stream); + } + handler->AddVideoTrack(video_track, ssrc); +} + +void MediaStreamHandlerContainer::RemoveLocalTrack( + MediaStreamInterface* stream, + MediaStreamTrackInterface* track) { + MediaStreamHandler* handler = FindStreamHandler(local_streams_handlers_, + stream); + if (!VERIFY(handler != NULL)) { + LOG(LS_WARNING) << "Remote MediaStreamHandler for stream with id " + << stream->label() << "doesnt't exist."; + return; + } + handler->RemoveTrack(track); +} + +MediaStreamHandler* MediaStreamHandlerContainer::CreateRemoteStreamHandler( + MediaStreamInterface* stream) { + ASSERT(!FindStreamHandler(remote_streams_handlers_, stream)); + + RemoteMediaStreamHandler* handler = + new RemoteMediaStreamHandler(stream, audio_provider_, video_provider_); + remote_streams_handlers_.push_back(handler); + return handler; +} + +MediaStreamHandler* MediaStreamHandlerContainer::CreateLocalStreamHandler( + MediaStreamInterface* stream) { + ASSERT(!FindStreamHandler(local_streams_handlers_, stream)); + + LocalMediaStreamHandler* handler = + new LocalMediaStreamHandler(stream, audio_provider_, video_provider_); + local_streams_handlers_.push_back(handler); + return handler; +} + +MediaStreamHandler* MediaStreamHandlerContainer::FindStreamHandler( + const StreamHandlerList& handlers, + MediaStreamInterface* stream) { + StreamHandlerList::const_iterator it = handlers.begin(); + for (; it != handlers.end(); ++it) { + if ((*it)->stream() == stream) { + return *it; + } + } + return NULL; +} + +void MediaStreamHandlerContainer::DeleteStreamHandler( + StreamHandlerList* streamhandlers, MediaStreamInterface* stream) { + StreamHandlerList::iterator it = streamhandlers->begin(); + for (; it != streamhandlers->end(); ++it) { + if ((*it)->stream() == stream) { + (*it)->Stop(); + delete *it; + streamhandlers->erase(it); + break; + } + } +} + +} // namespace webrtc diff --git a/talk/app/webrtc/mediastreamhandler.h b/talk/app/webrtc/mediastreamhandler.h new file mode 100644 index 000000000..0cd34d615 --- /dev/null +++ b/talk/app/webrtc/mediastreamhandler.h @@ -0,0 +1,264 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// This file contains classes for listening on changes on MediaStreams and +// MediaTracks that are connected to a certain PeerConnection. +// Example: If a user sets a rendererer on a remote video track the renderer is +// connected to the appropriate remote video stream. + +#ifndef TALK_APP_WEBRTC_MEDIASTREAMHANDLER_H_ +#define TALK_APP_WEBRTC_MEDIASTREAMHANDLER_H_ + +#include +#include + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/mediastreamprovider.h" +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/base/thread.h" + +namespace webrtc { + +// TrackHandler listen to events on a MediaStreamTrackInterface that is +// connected to a certain PeerConnection. +class TrackHandler : public ObserverInterface { + public: + TrackHandler(MediaStreamTrackInterface* track, uint32 ssrc); + virtual ~TrackHandler(); + virtual void OnChanged(); + // Stop using |track_| on this PeerConnection. + virtual void Stop() = 0; + + MediaStreamTrackInterface* track() { return track_; } + uint32 ssrc() const { return ssrc_; } + + protected: + virtual void OnStateChanged() = 0; + virtual void OnEnabledChanged() = 0; + + private: + talk_base::scoped_refptr track_; + uint32 ssrc_; + MediaStreamTrackInterface::TrackState state_; + bool enabled_; +}; + +// LocalAudioTrackHandler listen to events on a local AudioTrack instance +// connected to a PeerConnection and orders the |provider| to executes the +// requested change. +class LocalAudioTrackHandler : public TrackHandler { + public: + LocalAudioTrackHandler(AudioTrackInterface* track, + uint32 ssrc, + AudioProviderInterface* provider); + virtual ~LocalAudioTrackHandler(); + + virtual void Stop() OVERRIDE; + + protected: + virtual void OnStateChanged() OVERRIDE; + virtual void OnEnabledChanged() OVERRIDE; + + private: + AudioTrackInterface* audio_track_; + AudioProviderInterface* provider_; +}; + +// RemoteAudioTrackHandler listen to events on a remote AudioTrack instance +// connected to a PeerConnection and orders the |provider| to executes the +// requested change. +class RemoteAudioTrackHandler : public TrackHandler { + public: + RemoteAudioTrackHandler(AudioTrackInterface* track, + uint32 ssrc, + AudioProviderInterface* provider); + virtual ~RemoteAudioTrackHandler(); + virtual void Stop() OVERRIDE; + + protected: + virtual void OnStateChanged() OVERRIDE; + virtual void OnEnabledChanged() OVERRIDE; + + private: + AudioTrackInterface* audio_track_; + AudioProviderInterface* provider_; +}; + +// LocalVideoTrackHandler listen to events on a local VideoTrack instance +// connected to a PeerConnection and orders the |provider| to executes the +// requested change. +class LocalVideoTrackHandler : public TrackHandler { + public: + LocalVideoTrackHandler(VideoTrackInterface* track, + uint32 ssrc, + VideoProviderInterface* provider); + virtual ~LocalVideoTrackHandler(); + virtual void Stop() OVERRIDE; + + protected: + virtual void OnStateChanged() OVERRIDE; + virtual void OnEnabledChanged() OVERRIDE; + + private: + VideoTrackInterface* local_video_track_; + VideoProviderInterface* provider_; +}; + +// RemoteVideoTrackHandler listen to events on a remote VideoTrack instance +// connected to a PeerConnection and orders the |provider| to execute +// requested changes. +class RemoteVideoTrackHandler : public TrackHandler { + public: + RemoteVideoTrackHandler(VideoTrackInterface* track, + uint32 ssrc, + VideoProviderInterface* provider); + virtual ~RemoteVideoTrackHandler(); + virtual void Stop() OVERRIDE; + + protected: + virtual void OnStateChanged() OVERRIDE; + virtual void OnEnabledChanged() OVERRIDE; + + private: + VideoTrackInterface* remote_video_track_; + VideoProviderInterface* provider_; +}; + +class MediaStreamHandler : public ObserverInterface { + public: + MediaStreamHandler(MediaStreamInterface* stream, + AudioProviderInterface* audio_provider, + VideoProviderInterface* video_provider); + ~MediaStreamHandler(); + MediaStreamInterface* stream(); + void Stop(); + + virtual void AddAudioTrack(AudioTrackInterface* audio_track, uint32 ssrc) = 0; + virtual void AddVideoTrack(VideoTrackInterface* video_track, uint32 ssrc) = 0; + + virtual void RemoveTrack(MediaStreamTrackInterface* track); + virtual void OnChanged() OVERRIDE; + + protected: + TrackHandler* FindTrackHandler(MediaStreamTrackInterface* track); + talk_base::scoped_refptr stream_; + AudioProviderInterface* audio_provider_; + VideoProviderInterface* video_provider_; + typedef std::vector TrackHandlers; + TrackHandlers track_handlers_; +}; + +class LocalMediaStreamHandler : public MediaStreamHandler { + public: + LocalMediaStreamHandler(MediaStreamInterface* stream, + AudioProviderInterface* audio_provider, + VideoProviderInterface* video_provider); + ~LocalMediaStreamHandler(); + + virtual void AddAudioTrack(AudioTrackInterface* audio_track, + uint32 ssrc) OVERRIDE; + virtual void AddVideoTrack(VideoTrackInterface* video_track, + uint32 ssrc) OVERRIDE; +}; + +class RemoteMediaStreamHandler : public MediaStreamHandler { + public: + RemoteMediaStreamHandler(MediaStreamInterface* stream, + AudioProviderInterface* audio_provider, + VideoProviderInterface* video_provider); + ~RemoteMediaStreamHandler(); + virtual void AddAudioTrack(AudioTrackInterface* audio_track, + uint32 ssrc) OVERRIDE; + virtual void AddVideoTrack(VideoTrackInterface* video_track, + uint32 ssrc) OVERRIDE; +}; + +// Container for MediaStreamHandlers of currently known local and remote +// MediaStreams. +class MediaStreamHandlerContainer { + public: + MediaStreamHandlerContainer(AudioProviderInterface* audio_provider, + VideoProviderInterface* video_provider); + ~MediaStreamHandlerContainer(); + + // Notify all referenced objects that MediaStreamHandlerContainer will be + // destroyed. This method must be called prior to the dtor and prior to the + // |audio_provider| and |video_provider| is destroyed. + void TearDown(); + + // Remove all TrackHandlers for tracks in |stream| and make sure + // the audio_provider and video_provider is notified that the tracks has been + // removed. + void RemoveRemoteStream(MediaStreamInterface* stream); + + // Create a RemoteAudioTrackHandler and associate |audio_track| with |ssrc|. + void AddRemoteAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track, + uint32 ssrc); + // Create a RemoteVideoTrackHandler and associate |video_track| with |ssrc|. + void AddRemoteVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track, + uint32 ssrc); + // Remove the TrackHandler for |track|. + void RemoveRemoteTrack(MediaStreamInterface* stream, + MediaStreamTrackInterface* track); + + // Remove all TrackHandlers for tracks in |stream| and make sure + // the audio_provider and video_provider is notified that the tracks has been + // removed. + void RemoveLocalStream(MediaStreamInterface* stream); + + // Create a LocalAudioTrackHandler and associate |audio_track| with |ssrc|. + void AddLocalAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track, + uint32 ssrc); + // Create a LocalVideoTrackHandler and associate |video_track| with |ssrc|. + void AddLocalVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track, + uint32 ssrc); + // Remove the TrackHandler for |track|. + void RemoveLocalTrack(MediaStreamInterface* stream, + MediaStreamTrackInterface* track); + + private: + typedef std::list StreamHandlerList; + MediaStreamHandler* FindStreamHandler(const StreamHandlerList& handlers, + MediaStreamInterface* stream); + MediaStreamHandler* CreateRemoteStreamHandler(MediaStreamInterface* stream); + MediaStreamHandler* CreateLocalStreamHandler(MediaStreamInterface* stream); + void DeleteStreamHandler(StreamHandlerList* streamhandlers, + MediaStreamInterface* stream); + + StreamHandlerList local_streams_handlers_; + StreamHandlerList remote_streams_handlers_; + AudioProviderInterface* audio_provider_; + VideoProviderInterface* video_provider_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_MEDIASTREAMHANDLER_H_ diff --git a/talk/app/webrtc/mediastreamhandler_unittest.cc b/talk/app/webrtc/mediastreamhandler_unittest.cc new file mode 100644 index 000000000..bc4189bf9 --- /dev/null +++ b/talk/app/webrtc/mediastreamhandler_unittest.cc @@ -0,0 +1,297 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/mediastreamhandler.h" + +#include + +#include "talk/app/webrtc/audiotrack.h" +#include "talk/app/webrtc/localvideosource.h" +#include "talk/app/webrtc/mediastream.h" +#include "talk/app/webrtc/streamcollection.h" +#include "talk/app/webrtc/videotrack.h" +#include "talk/base/gunit.h" +#include "talk/media/base/fakevideocapturer.h" +#include "talk/media/base/mediachannel.h" +#include "testing/base/public/gmock.h" + +using ::testing::_; +using ::testing::Exactly; + +static const char kStreamLabel1[] = "local_stream_1"; +static const char kVideoTrackId[] = "video_1"; +static const char kAudioTrackId[] = "audio_1"; +static const uint32 kVideoSsrc = 98; +static const uint32 kAudioSsrc = 99; + +namespace webrtc { + +// Helper class to test MediaStreamHandler. +class MockAudioProvider : public AudioProviderInterface { + public: + virtual ~MockAudioProvider() {} + MOCK_METHOD2(SetAudioPlayout, void(uint32 ssrc, bool enable)); + MOCK_METHOD3(SetAudioSend, void(uint32 ssrc, bool enable, + const cricket::AudioOptions& options)); + MOCK_METHOD2(SetAudioRenderer, bool(uint32, cricket::AudioRenderer*)); +}; + +// Helper class to test MediaStreamHandler. +class MockVideoProvider : public VideoProviderInterface { + public: + virtual ~MockVideoProvider() {} + MOCK_METHOD2(SetCaptureDevice, bool(uint32 ssrc, + cricket::VideoCapturer* camera)); + MOCK_METHOD3(SetVideoPlayout, void(uint32 ssrc, + bool enable, + cricket::VideoRenderer* renderer)); + MOCK_METHOD3(SetVideoSend, void(uint32 ssrc, bool enable, + const cricket::VideoOptions* options)); +}; + +class FakeVideoSource : public Notifier { + public: + static talk_base::scoped_refptr Create() { + return new talk_base::RefCountedObject(); + } + virtual cricket::VideoCapturer* GetVideoCapturer() { + return &fake_capturer_; + } + virtual void AddSink(cricket::VideoRenderer* output) {} + virtual void RemoveSink(cricket::VideoRenderer* output) {} + virtual SourceState state() const { return state_; } + virtual const cricket::VideoOptions* options() const { return &options_; } + + protected: + FakeVideoSource() : state_(kLive) {} + ~FakeVideoSource() {} + + private: + cricket::FakeVideoCapturer fake_capturer_; + SourceState state_; + cricket::VideoOptions options_; +}; + +class MediaStreamHandlerTest : public testing::Test { + public: + MediaStreamHandlerTest() + : handlers_(&audio_provider_, &video_provider_) { + } + + virtual void SetUp() { + stream_ = MediaStream::Create(kStreamLabel1); + talk_base::scoped_refptr source( + FakeVideoSource::Create()); + video_track_ = VideoTrack::Create(kVideoTrackId, source); + EXPECT_TRUE(stream_->AddTrack(video_track_)); + audio_track_ = AudioTrack::Create(kAudioTrackId, + NULL); + EXPECT_TRUE(stream_->AddTrack(audio_track_)); + } + + void AddLocalAudioTrack() { + EXPECT_CALL(audio_provider_, SetAudioSend(kAudioSsrc, true, _)); + handlers_.AddLocalAudioTrack(stream_, stream_->GetAudioTracks()[0], + kAudioSsrc); + } + + void AddLocalVideoTrack() { + EXPECT_CALL(video_provider_, SetCaptureDevice( + kVideoSsrc, video_track_->GetSource()->GetVideoCapturer())); + EXPECT_CALL(video_provider_, SetVideoSend(kVideoSsrc, true, _)); + handlers_.AddLocalVideoTrack(stream_, stream_->GetVideoTracks()[0], + kVideoSsrc); + } + + void RemoveLocalAudioTrack() { + EXPECT_CALL(audio_provider_, SetAudioSend(kAudioSsrc, false, _)) + .Times(1); + handlers_.RemoveLocalTrack(stream_, audio_track_); + } + + void RemoveLocalVideoTrack() { + EXPECT_CALL(video_provider_, SetCaptureDevice(kVideoSsrc, NULL)) + .Times(1); + EXPECT_CALL(video_provider_, SetVideoSend(kVideoSsrc, false, _)) + .Times(1); + handlers_.RemoveLocalTrack(stream_, video_track_); + } + + void AddRemoteAudioTrack() { + EXPECT_CALL(audio_provider_, SetAudioRenderer(kAudioSsrc, _)); + EXPECT_CALL(audio_provider_, SetAudioPlayout(kAudioSsrc, true)); + handlers_.AddRemoteAudioTrack(stream_, stream_->GetAudioTracks()[0], + kAudioSsrc); + } + + void AddRemoteVideoTrack() { + EXPECT_CALL(video_provider_, SetVideoPlayout(kVideoSsrc, true, + video_track_->FrameInput())); + handlers_.AddRemoteVideoTrack(stream_, stream_->GetVideoTracks()[0], + kVideoSsrc); + } + + void RemoveRemoteAudioTrack() { + EXPECT_CALL(audio_provider_, SetAudioPlayout(kAudioSsrc, false)); + handlers_.RemoveRemoteTrack(stream_, stream_->GetAudioTracks()[0]); + } + + void RemoveRemoteVideoTrack() { + EXPECT_CALL(video_provider_, SetVideoPlayout(kVideoSsrc, false, NULL)); + handlers_.RemoveRemoteTrack(stream_, stream_->GetVideoTracks()[0]); + } + + protected: + MockAudioProvider audio_provider_; + MockVideoProvider video_provider_; + MediaStreamHandlerContainer handlers_; + talk_base::scoped_refptr stream_; + talk_base::scoped_refptr video_track_; + talk_base::scoped_refptr audio_track_; +}; + +// Test that |audio_provider_| is notified when an audio track is associated +// and disassociated with a MediaStreamHandler. +TEST_F(MediaStreamHandlerTest, AddAndRemoveLocalAudioTrack) { + AddLocalAudioTrack(); + RemoveLocalAudioTrack(); + + handlers_.RemoveLocalStream(stream_); +} + +// Test that |video_provider_| is notified when a video track is associated and +// disassociated with a MediaStreamHandler. +TEST_F(MediaStreamHandlerTest, AddAndRemoveLocalVideoTrack) { + AddLocalVideoTrack(); + RemoveLocalVideoTrack(); + + handlers_.RemoveLocalStream(stream_); +} + +// Test that |video_provider_| and |audio_provider_| is notified when an audio +// and video track is disassociated with a MediaStreamHandler by calling +// RemoveLocalStream. +TEST_F(MediaStreamHandlerTest, RemoveLocalStream) { + AddLocalAudioTrack(); + AddLocalVideoTrack(); + + EXPECT_CALL(video_provider_, SetCaptureDevice(kVideoSsrc, NULL)) + .Times(1); + EXPECT_CALL(video_provider_, SetVideoSend(kVideoSsrc, false, _)) + .Times(1); + EXPECT_CALL(audio_provider_, SetAudioSend(kAudioSsrc, false, _)) + .Times(1); + handlers_.RemoveLocalStream(stream_); +} + + +// Test that |audio_provider_| is notified when a remote audio and track is +// associated and disassociated with a MediaStreamHandler. +TEST_F(MediaStreamHandlerTest, AddAndRemoveRemoteAudioTrack) { + AddRemoteAudioTrack(); + RemoveRemoteAudioTrack(); + + handlers_.RemoveRemoteStream(stream_); +} + +// Test that |video_provider_| is notified when a remote +// video track is associated and disassociated with a MediaStreamHandler. +TEST_F(MediaStreamHandlerTest, AddAndRemoveRemoteVideoTrack) { + AddRemoteVideoTrack(); + RemoveRemoteVideoTrack(); + + handlers_.RemoveRemoteStream(stream_); +} + +// Test that |audio_provider_| and |video_provider_| is notified when an audio +// and video track is disassociated with a MediaStreamHandler by calling +// RemoveRemoveStream. +TEST_F(MediaStreamHandlerTest, RemoveRemoteStream) { + AddRemoteAudioTrack(); + AddRemoteVideoTrack(); + + EXPECT_CALL(video_provider_, SetVideoPlayout(kVideoSsrc, false, NULL)) + .Times(1); + EXPECT_CALL(audio_provider_, SetAudioPlayout(kAudioSsrc, false)) + .Times(1); + handlers_.RemoveRemoteStream(stream_); +} + +TEST_F(MediaStreamHandlerTest, LocalAudioTrackDisable) { + AddLocalAudioTrack(); + + EXPECT_CALL(audio_provider_, SetAudioSend(kAudioSsrc, false, _)); + audio_track_->set_enabled(false); + + EXPECT_CALL(audio_provider_, SetAudioSend(kAudioSsrc, true, _)); + audio_track_->set_enabled(true); + + RemoveLocalAudioTrack(); + handlers_.TearDown(); +} + +TEST_F(MediaStreamHandlerTest, RemoteAudioTrackDisable) { + AddRemoteAudioTrack(); + + EXPECT_CALL(audio_provider_, SetAudioPlayout(kAudioSsrc, false)); + audio_track_->set_enabled(false); + + EXPECT_CALL(audio_provider_, SetAudioPlayout(kAudioSsrc, true)); + audio_track_->set_enabled(true); + + RemoveRemoteAudioTrack(); + handlers_.TearDown(); +} + +TEST_F(MediaStreamHandlerTest, LocalVideoTrackDisable) { + AddLocalVideoTrack(); + + EXPECT_CALL(video_provider_, SetVideoSend(kVideoSsrc, false, _)); + video_track_->set_enabled(false); + + EXPECT_CALL(video_provider_, SetVideoSend(kVideoSsrc, true, _)); + video_track_->set_enabled(true); + + RemoveLocalVideoTrack(); + handlers_.TearDown(); +} + +TEST_F(MediaStreamHandlerTest, RemoteVideoTrackDisable) { + AddRemoteVideoTrack(); + + EXPECT_CALL(video_provider_, SetVideoPlayout(kVideoSsrc, false, _)); + video_track_->set_enabled(false); + + EXPECT_CALL(video_provider_, SetVideoPlayout(kVideoSsrc, true, + video_track_->FrameInput())); + video_track_->set_enabled(true); + + RemoveRemoteVideoTrack(); + handlers_.TearDown(); +} + +} // namespace webrtc diff --git a/talk/app/webrtc/mediastreaminterface.h b/talk/app/webrtc/mediastreaminterface.h new file mode 100644 index 000000000..6f834d274 --- /dev/null +++ b/talk/app/webrtc/mediastreaminterface.h @@ -0,0 +1,196 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// This file contains interfaces for MediaStream, MediaTrack and MediaSource. +// These interfaces are used for implementing MediaStream and MediaTrack as +// defined in http://dev.w3.org/2011/webrtc/editor/webrtc.html#stream-api. These +// interfaces must be used only with PeerConnection. PeerConnectionManager +// interface provides the factory methods to create MediaStream and MediaTracks. + +#ifndef TALK_APP_WEBRTC_MEDIASTREAMINTERFACE_H_ +#define TALK_APP_WEBRTC_MEDIASTREAMINTERFACE_H_ + +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/refcount.h" +#include "talk/base/scoped_ref_ptr.h" + +namespace cricket { + +class AudioRenderer; +class VideoCapturer; +class VideoRenderer; +class VideoFrame; + +} // namespace cricket + +namespace webrtc { + +// Generic observer interface. +class ObserverInterface { + public: + virtual void OnChanged() = 0; + + protected: + virtual ~ObserverInterface() {} +}; + +class NotifierInterface { + public: + virtual void RegisterObserver(ObserverInterface* observer) = 0; + virtual void UnregisterObserver(ObserverInterface* observer) = 0; + + virtual ~NotifierInterface() {} +}; + +// Base class for sources. A MediaStreamTrack have an underlying source that +// provide media. A source can be shared with multiple tracks. +// TODO(perkj): Implement sources for local and remote audio tracks and +// remote video tracks. +class MediaSourceInterface : public talk_base::RefCountInterface, + public NotifierInterface { + public: + enum SourceState { + kInitializing, + kLive, + kEnded, + kMuted + }; + + virtual SourceState state() const = 0; + + protected: + virtual ~MediaSourceInterface() {} +}; + +// Information about a track. +class MediaStreamTrackInterface : public talk_base::RefCountInterface, + public NotifierInterface { + public: + enum TrackState { + kInitializing, // Track is beeing negotiated. + kLive = 1, // Track alive + kEnded = 2, // Track have ended + kFailed = 3, // Track negotiation failed. + }; + + virtual std::string kind() const = 0; + virtual std::string id() const = 0; + virtual bool enabled() const = 0; + virtual TrackState state() const = 0; + virtual bool set_enabled(bool enable) = 0; + // These methods should be called by implementation only. + virtual bool set_state(TrackState new_state) = 0; +}; + +// Interface for rendering VideoFrames from a VideoTrack +class VideoRendererInterface { + public: + virtual void SetSize(int width, int height) = 0; + virtual void RenderFrame(const cricket::VideoFrame* frame) = 0; + + protected: + // The destructor is protected to prevent deletion via the interface. + // This is so that we allow reference counted classes, where the destructor + // should never be public, to implement the interface. + virtual ~VideoRendererInterface() {} +}; + +class VideoSourceInterface; + +class VideoTrackInterface : public MediaStreamTrackInterface { + public: + // Register a renderer that will render all frames received on this track. + virtual void AddRenderer(VideoRendererInterface* renderer) = 0; + // Deregister a renderer. + virtual void RemoveRenderer(VideoRendererInterface* renderer) = 0; + + // Gets a pointer to the frame input of this VideoTrack. + // The pointer is valid for the lifetime of this VideoTrack. + // VideoFrames rendered to the cricket::VideoRenderer will be rendered on all + // registered renderers. + virtual cricket::VideoRenderer* FrameInput() = 0; + + virtual VideoSourceInterface* GetSource() const = 0; + + protected: + virtual ~VideoTrackInterface() {} +}; + +// AudioSourceInterface is a reference counted source used for AudioTracks. +// The same source can be used in multiple AudioTracks. +// TODO(perkj): Extend this class with necessary methods to allow separate +// sources for each audio track. +class AudioSourceInterface : public MediaSourceInterface { +}; + +class AudioTrackInterface : public MediaStreamTrackInterface { + public: + // TODO(xians): Figure out if the following interface should be const or not. + virtual AudioSourceInterface* GetSource() const = 0; + + // Gets a pointer to the frame input of this AudioTrack. + // The pointer is valid for the lifetime of this AudioTrack. + // TODO(xians): Make the following interface pure virtual once Chrome has its + // implementation. + virtual cricket::AudioRenderer* FrameInput() { return NULL; } + + protected: + virtual ~AudioTrackInterface() {} +}; + +typedef std::vector > + AudioTrackVector; +typedef std::vector > + VideoTrackVector; + +class MediaStreamInterface : public talk_base::RefCountInterface, + public NotifierInterface { + public: + virtual std::string label() const = 0; + + virtual AudioTrackVector GetAudioTracks() = 0; + virtual VideoTrackVector GetVideoTracks() = 0; + virtual talk_base::scoped_refptr + FindAudioTrack(const std::string& track_id) = 0; + virtual talk_base::scoped_refptr + FindVideoTrack(const std::string& track_id) = 0; + + virtual bool AddTrack(AudioTrackInterface* track) = 0; + virtual bool AddTrack(VideoTrackInterface* track) = 0; + virtual bool RemoveTrack(AudioTrackInterface* track) = 0; + virtual bool RemoveTrack(VideoTrackInterface* track) = 0; + + protected: + virtual ~MediaStreamInterface() {} +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_MEDIASTREAMINTERFACE_H_ diff --git a/talk/app/webrtc/mediastreamprovider.h b/talk/app/webrtc/mediastreamprovider.h new file mode 100644 index 000000000..4c77fd076 --- /dev/null +++ b/talk/app/webrtc/mediastreamprovider.h @@ -0,0 +1,81 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_MEDIASTREAMPROVIDER_H_ +#define TALK_APP_WEBRTC_MEDIASTREAMPROVIDER_H_ + +namespace cricket { + +class AudioRenderer; +class VideoCapturer; +class VideoRenderer; +struct AudioOptions; +struct VideoOptions; + +} // namespace cricket + +namespace webrtc { + +// This interface is called by AudioTrackHandler classes in mediastreamhandler.h +// to change the settings of an audio track connected to certain PeerConnection. +class AudioProviderInterface { + public: + // Enable/disable the audio playout of a remote audio track with |ssrc|. + virtual void SetAudioPlayout(uint32 ssrc, bool enable) = 0; + // Enable/disable sending audio on the local audio track with |ssrc|. + // When |enable| is true |options| should be applied to the audio track. + virtual void SetAudioSend(uint32 ssrc, bool enable, + const cricket::AudioOptions& options) = 0; + // Sets the renderer to be used for the specified |ssrc|. + virtual bool SetAudioRenderer(uint32 ssrc, + cricket::AudioRenderer* renderer) = 0; + + protected: + virtual ~AudioProviderInterface() {} +}; + +// This interface is called by VideoTrackHandler classes in mediastreamhandler.h +// to change the settings of a video track connected to a certain +// PeerConnection. +class VideoProviderInterface { + public: + virtual bool SetCaptureDevice(uint32 ssrc, + cricket::VideoCapturer* camera) = 0; + // Enable/disable the video playout of a remote video track with |ssrc|. + virtual void SetVideoPlayout(uint32 ssrc, bool enable, + cricket::VideoRenderer* renderer) = 0; + // Enable sending video on the local video track with |ssrc|. + virtual void SetVideoSend(uint32 ssrc, bool enable, + const cricket::VideoOptions* options) = 0; + + protected: + virtual ~VideoProviderInterface() {} +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_MEDIASTREAMPROVIDER_H_ diff --git a/talk/app/webrtc/mediastreamproxy.h b/talk/app/webrtc/mediastreamproxy.h new file mode 100644 index 000000000..7d018d5ea --- /dev/null +++ b/talk/app/webrtc/mediastreamproxy.h @@ -0,0 +1,54 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_APP_WEBRTC_MEDIASTREAMPROXY_H_ +#define TALK_APP_WEBRTC_MEDIASTREAMPROXY_H_ + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/proxy.h" + +namespace webrtc { + +BEGIN_PROXY_MAP(MediaStream) + PROXY_CONSTMETHOD0(std::string, label) + PROXY_METHOD0(AudioTrackVector, GetAudioTracks) + PROXY_METHOD0(VideoTrackVector, GetVideoTracks) + PROXY_METHOD1(talk_base::scoped_refptr, + FindAudioTrack, const std::string&) + PROXY_METHOD1(talk_base::scoped_refptr, + FindVideoTrack, const std::string&) + PROXY_METHOD1(bool, AddTrack, AudioTrackInterface*) + PROXY_METHOD1(bool, AddTrack, VideoTrackInterface*) + PROXY_METHOD1(bool, RemoveTrack, AudioTrackInterface*) + PROXY_METHOD1(bool, RemoveTrack, VideoTrackInterface*) + PROXY_METHOD1(void, RegisterObserver, ObserverInterface*) + PROXY_METHOD1(void, UnregisterObserver, ObserverInterface*) +END_PROXY() + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_MEDIASTREAMPROXY_H_ diff --git a/talk/app/webrtc/mediastreamsignaling.cc b/talk/app/webrtc/mediastreamsignaling.cc new file mode 100644 index 000000000..1397a7fc0 --- /dev/null +++ b/talk/app/webrtc/mediastreamsignaling.cc @@ -0,0 +1,883 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/mediastreamsignaling.h" + +#include + +#include "talk/app/webrtc/audiotrack.h" +#include "talk/app/webrtc/mediastreamproxy.h" +#include "talk/app/webrtc/mediaconstraintsinterface.h" +#include "talk/app/webrtc/mediastreamtrackproxy.h" +#include "talk/app/webrtc/videotrack.h" + +static const char kDefaultStreamLabel[] = "default"; +static const char kDefaultAudioTrackLabel[] = "defaulta0"; +static const char kDefaultVideoTrackLabel[] = "defaultv0"; + +namespace webrtc { + +using talk_base::scoped_ptr; +using talk_base::scoped_refptr; + +// Supported MediaConstraints. +const char MediaConstraintsInterface::kOfferToReceiveAudio[] = + "OfferToReceiveAudio"; +const char MediaConstraintsInterface::kOfferToReceiveVideo[] = + "OfferToReceiveVideo"; +const char MediaConstraintsInterface::kIceRestart[] = + "IceRestart"; +const char MediaConstraintsInterface::kUseRtpMux[] = + "googUseRtpMUX"; +const char MediaConstraintsInterface::kVoiceActivityDetection[] = + "VoiceActivityDetection"; + +static bool ParseConstraints( + const MediaConstraintsInterface* constraints, + cricket::MediaSessionOptions* options, bool is_answer) { + bool value; + size_t mandatory_constraints_satisfied = 0; + + if (FindConstraint(constraints, + MediaConstraintsInterface::kOfferToReceiveAudio, + &value, &mandatory_constraints_satisfied)) { + // |options-|has_audio| can only change from false to + // true, but never change from true to false. This is to make sure + // CreateOffer / CreateAnswer doesn't remove a media content + // description that has been created. + options->has_audio |= value; + } else { + // kOfferToReceiveAudio defaults to true according to spec. + options->has_audio = true; + } + + if (FindConstraint(constraints, + MediaConstraintsInterface::kOfferToReceiveVideo, + &value, &mandatory_constraints_satisfied)) { + // |options->has_video| can only change from false to + // true, but never change from true to false. This is to make sure + // CreateOffer / CreateAnswer doesn't remove a media content + // description that has been created. + options->has_video |= value; + } else { + // kOfferToReceiveVideo defaults to false according to spec. But + // if it is an answer and video is offered, we should still accept video + // per default. + options->has_video |= is_answer; + } + + if (FindConstraint(constraints, + MediaConstraintsInterface::kVoiceActivityDetection, + &value, &mandatory_constraints_satisfied)) { + options->vad_enabled = value; + } + + if (FindConstraint(constraints, + MediaConstraintsInterface::kUseRtpMux, + &value, &mandatory_constraints_satisfied)) { + options->bundle_enabled = value; + } else { + // kUseRtpMux defaults to true according to spec. + options->bundle_enabled = true; + } + if (FindConstraint(constraints, + MediaConstraintsInterface::kIceRestart, + &value, &mandatory_constraints_satisfied)) { + options->transport_options.ice_restart = value; + } else { + // kIceRestart defaults to false according to spec. + options->transport_options.ice_restart = false; + } + + if (!constraints) { + return true; + } + return mandatory_constraints_satisfied == constraints->GetMandatory().size(); +} + +// Returns true if if at least one media content is present and +// |options.bundle_enabled| is true. +// Bundle will be enabled by default if at least one media content is present +// and the constraint kUseRtpMux has not disabled bundle. +static bool EvaluateNeedForBundle(const cricket::MediaSessionOptions& options) { + return options.bundle_enabled && + (options.has_audio || options.has_video || options.has_data()); +} + +// Factory class for creating remote MediaStreams and MediaStreamTracks. +class RemoteMediaStreamFactory { + public: + explicit RemoteMediaStreamFactory(talk_base::Thread* signaling_thread) + : signaling_thread_(signaling_thread) { + } + + talk_base::scoped_refptr CreateMediaStream( + const std::string& stream_label) { + return MediaStreamProxy::Create( + signaling_thread_, MediaStream::Create(stream_label)); + } + + AudioTrackInterface* AddAudioTrack(webrtc::MediaStreamInterface* stream, + const std::string& track_id) { + return AddTrack(stream, + track_id); + } + + VideoTrackInterface* AddVideoTrack(webrtc::MediaStreamInterface* stream, + const std::string& track_id) { + return AddTrack(stream, + track_id); + } + + private: + template + TI* AddTrack(MediaStreamInterface* stream, const std::string& track_id) { + talk_base::scoped_refptr track( + TP::Create(signaling_thread_, T::Create(track_id, NULL))); + track->set_state(webrtc::MediaStreamTrackInterface::kLive); + if (stream->AddTrack(track)) { + return track; + } + return NULL; + } + + talk_base::Thread* signaling_thread_; +}; + +MediaStreamSignaling::MediaStreamSignaling( + talk_base::Thread* signaling_thread, + MediaStreamSignalingObserver* stream_observer) + : signaling_thread_(signaling_thread), + data_channel_factory_(NULL), + stream_observer_(stream_observer), + local_streams_(StreamCollection::Create()), + remote_streams_(StreamCollection::Create()), + remote_stream_factory_(new RemoteMediaStreamFactory(signaling_thread)), + last_allocated_sctp_id_(0) { + options_.has_video = false; + options_.has_audio = false; +} + +MediaStreamSignaling::~MediaStreamSignaling() { +} + +void MediaStreamSignaling::TearDown() { + OnAudioChannelClose(); + OnVideoChannelClose(); + OnDataChannelClose(); +} + +bool MediaStreamSignaling::IsSctpIdAvailable(int id) const { + if (id < 0 || id > static_cast(cricket::kMaxSctpSid)) + return false; + for (DataChannels::const_iterator iter = data_channels_.begin(); + iter != data_channels_.end(); + ++iter) { + if (iter->second->id() == id) { + return false; + } + } + return true; +} + +// Gets the first id that has not been taken by existing data +// channels. Starting from 1. +// Returns false if no id can be allocated. +// TODO(jiayl): Update to some kind of even/odd random number selection when the +// rules are fully standardized. +bool MediaStreamSignaling::AllocateSctpId(int* id) { + do { + last_allocated_sctp_id_++; + } while (last_allocated_sctp_id_ <= static_cast(cricket::kMaxSctpSid) && + !IsSctpIdAvailable(last_allocated_sctp_id_)); + + if (last_allocated_sctp_id_ > static_cast(cricket::kMaxSctpSid)) { + last_allocated_sctp_id_ = cricket::kMaxSctpSid; + return false; + } + + *id = last_allocated_sctp_id_; + return true; +} + +bool MediaStreamSignaling::AddDataChannel(DataChannel* data_channel) { + ASSERT(data_channel != NULL); + if (data_channels_.find(data_channel->label()) != data_channels_.end()) { + LOG(LS_ERROR) << "DataChannel with label " << data_channel->label() + << " already exists."; + return false; + } + data_channels_[data_channel->label()] = data_channel; + return true; +} + +bool MediaStreamSignaling::AddLocalStream(MediaStreamInterface* local_stream) { + if (local_streams_->find(local_stream->label()) != NULL) { + LOG(LS_WARNING) << "MediaStream with label " << local_stream->label() + << "already exist."; + return false; + } + local_streams_->AddStream(local_stream); + + // Find tracks that has already been configured in SDP. This can occur if a + // local session description that contains the MSID of these tracks is set + // before AddLocalStream is called. It can also occur if the local session + // description is not changed and RemoveLocalStream + // is called and later AddLocalStream is called again with the same stream. + AudioTrackVector audio_tracks = local_stream->GetAudioTracks(); + for (AudioTrackVector::const_iterator it = audio_tracks.begin(); + it != audio_tracks.end(); ++it) { + TrackInfos::const_iterator track_info_it = + local_audio_tracks_.find((*it)->id()); + if (track_info_it != local_audio_tracks_.end()) { + const TrackInfo& info = track_info_it->second; + OnLocalTrackSeen(info.stream_label, info.track_id, info.ssrc, + cricket::MEDIA_TYPE_AUDIO); + } + } + + VideoTrackVector video_tracks = local_stream->GetVideoTracks(); + for (VideoTrackVector::const_iterator it = video_tracks.begin(); + it != video_tracks.end(); ++it) { + TrackInfos::const_iterator track_info_it = + local_video_tracks_.find((*it)->id()); + if (track_info_it != local_video_tracks_.end()) { + const TrackInfo& info = track_info_it->second; + OnLocalTrackSeen(info.stream_label, info.track_id, info.ssrc, + cricket::MEDIA_TYPE_VIDEO); + } + } + return true; +} + +void MediaStreamSignaling::RemoveLocalStream( + MediaStreamInterface* local_stream) { + local_streams_->RemoveStream(local_stream); + stream_observer_->OnRemoveLocalStream(local_stream); +} + +bool MediaStreamSignaling::GetOptionsForOffer( + const MediaConstraintsInterface* constraints, + cricket::MediaSessionOptions* options) { + UpdateSessionOptions(); + if (!ParseConstraints(constraints, &options_, false)) { + return false; + } + options_.bundle_enabled = EvaluateNeedForBundle(options_); + *options = options_; + return true; +} + +bool MediaStreamSignaling::GetOptionsForAnswer( + const MediaConstraintsInterface* constraints, + cricket::MediaSessionOptions* options) { + UpdateSessionOptions(); + + // Copy the |options_| to not let the flag MediaSessionOptions::has_audio and + // MediaSessionOptions::has_video affect subsequent offers. + cricket::MediaSessionOptions current_options = options_; + if (!ParseConstraints(constraints, ¤t_options, true)) { + return false; + } + current_options.bundle_enabled = EvaluateNeedForBundle(current_options); + *options = current_options; + return true; +} + +// Updates or creates remote MediaStream objects given a +// remote SessionDesription. +// If the remote SessionDesription contains new remote MediaStreams +// the observer OnAddStream method is called. If a remote MediaStream is missing +// from the remote SessionDescription OnRemoveStream is called. +void MediaStreamSignaling::OnRemoteDescriptionChanged( + const SessionDescriptionInterface* desc) { + const cricket::SessionDescription* remote_desc = desc->description(); + talk_base::scoped_refptr new_streams( + StreamCollection::Create()); + + // Find all audio rtp streams and create corresponding remote AudioTracks + // and MediaStreams. + const cricket::ContentInfo* audio_content = GetFirstAudioContent(remote_desc); + if (audio_content) { + const cricket::AudioContentDescription* desc = + static_cast( + audio_content->description); + UpdateRemoteStreamsList(desc->streams(), desc->type(), new_streams); + remote_info_.default_audio_track_needed = + desc->direction() == cricket::MD_SENDRECV && desc->streams().empty(); + } + + // Find all video rtp streams and create corresponding remote VideoTracks + // and MediaStreams. + const cricket::ContentInfo* video_content = GetFirstVideoContent(remote_desc); + if (video_content) { + const cricket::VideoContentDescription* desc = + static_cast( + video_content->description); + UpdateRemoteStreamsList(desc->streams(), desc->type(), new_streams); + remote_info_.default_video_track_needed = + desc->direction() == cricket::MD_SENDRECV && desc->streams().empty(); + } + + // Update the DataChannels with the information from the remote peer. + const cricket::ContentInfo* data_content = GetFirstDataContent(remote_desc); + if (data_content) { + const cricket::DataContentDescription* data_desc = + static_cast( + data_content->description); + if (data_desc->protocol() == cricket::kMediaProtocolDtlsSctp) { + UpdateRemoteSctpDataChannels(); + } else { + UpdateRemoteRtpDataChannels(data_desc->streams()); + } + } + + // Iterate new_streams and notify the observer about new MediaStreams. + for (size_t i = 0; i < new_streams->count(); ++i) { + MediaStreamInterface* new_stream = new_streams->at(i); + stream_observer_->OnAddRemoteStream(new_stream); + } + + // Find removed MediaStreams. + if (remote_info_.IsDefaultMediaStreamNeeded() && + remote_streams_->find(kDefaultStreamLabel) != NULL) { + // The default media stream already exists. No need to do anything. + } else { + UpdateEndedRemoteMediaStreams(); + remote_info_.msid_supported |= remote_streams_->count() > 0; + } + MaybeCreateDefaultStream(); +} + +void MediaStreamSignaling::OnLocalDescriptionChanged( + const SessionDescriptionInterface* desc) { + const cricket::ContentInfo* audio_content = + GetFirstAudioContent(desc->description()); + if (audio_content) { + if (audio_content->rejected) { + RejectRemoteTracks(cricket::MEDIA_TYPE_AUDIO); + } + const cricket::AudioContentDescription* audio_desc = + static_cast( + audio_content->description); + UpdateLocalTracks(audio_desc->streams(), audio_desc->type()); + } + + const cricket::ContentInfo* video_content = + GetFirstVideoContent(desc->description()); + if (video_content) { + if (video_content->rejected) { + RejectRemoteTracks(cricket::MEDIA_TYPE_VIDEO); + } + const cricket::VideoContentDescription* video_desc = + static_cast( + video_content->description); + UpdateLocalTracks(video_desc->streams(), video_desc->type()); + } + + const cricket::ContentInfo* data_content = + GetFirstDataContent(desc->description()); + if (data_content) { + const cricket::DataContentDescription* data_desc = + static_cast( + data_content->description); + if (data_desc->protocol() == cricket::kMediaProtocolDtlsSctp) { + UpdateLocalSctpDataChannels(); + } else { + UpdateLocalRtpDataChannels(data_desc->streams()); + } + } +} + +void MediaStreamSignaling::OnAudioChannelClose() { + RejectRemoteTracks(cricket::MEDIA_TYPE_AUDIO); +} + +void MediaStreamSignaling::OnVideoChannelClose() { + RejectRemoteTracks(cricket::MEDIA_TYPE_VIDEO); +} + +void MediaStreamSignaling::OnDataChannelClose() { + DataChannels::iterator it = data_channels_.begin(); + for (; it != data_channels_.end(); ++it) { + DataChannel* data_channel = it->second; + data_channel->OnDataEngineClose(); + } +} + +bool MediaStreamSignaling::GetRemoteAudioTrackSsrc( + const std::string& track_id, uint32* ssrc) const { + TrackInfos::const_iterator it = remote_audio_tracks_.find(track_id); + if (it == remote_audio_tracks_.end()) { + return false; + } + + *ssrc = it->second.ssrc; + return true; +} + +bool MediaStreamSignaling::GetRemoteVideoTrackSsrc( + const std::string& track_id, uint32* ssrc) const { + TrackInfos::const_iterator it = remote_video_tracks_.find(track_id); + if (it == remote_video_tracks_.end()) { + return false; + } + + *ssrc = it->second.ssrc; + return true; +} + +void MediaStreamSignaling::UpdateSessionOptions() { + options_.streams.clear(); + if (local_streams_ != NULL) { + for (size_t i = 0; i < local_streams_->count(); ++i) { + MediaStreamInterface* stream = local_streams_->at(i); + + AudioTrackVector audio_tracks(stream->GetAudioTracks()); + if (!audio_tracks.empty()) { + options_.has_audio = true; + } + + // For each audio track in the stream, add it to the MediaSessionOptions. + for (size_t j = 0; j < audio_tracks.size(); ++j) { + scoped_refptr track(audio_tracks[j]); + options_.AddStream(cricket::MEDIA_TYPE_AUDIO, track->id(), + stream->label()); + } + + VideoTrackVector video_tracks(stream->GetVideoTracks()); + if (!video_tracks.empty()) { + options_.has_video = true; + } + // For each video track in the stream, add it to the MediaSessionOptions. + for (size_t j = 0; j < video_tracks.size(); ++j) { + scoped_refptr track(video_tracks[j]); + options_.AddStream(cricket::MEDIA_TYPE_VIDEO, track->id(), + stream->label()); + } + } + } + + // Check for data channels. + DataChannels::const_iterator data_channel_it = data_channels_.begin(); + for (; data_channel_it != data_channels_.end(); ++data_channel_it) { + const DataChannel* channel = data_channel_it->second; + if (channel->state() == DataChannel::kConnecting || + channel->state() == DataChannel::kOpen) { + // |streamid| and |sync_label| are both set to the DataChannel label + // here so they can be signaled the same way as MediaStreams and Tracks. + // For MediaStreams, the sync_label is the MediaStream label and the + // track label is the same as |streamid|. + const std::string& streamid = channel->label(); + const std::string& sync_label = channel->label(); + options_.AddStream(cricket::MEDIA_TYPE_DATA, streamid, sync_label); + } + } +} + +void MediaStreamSignaling::UpdateRemoteStreamsList( + const cricket::StreamParamsVec& streams, + cricket::MediaType media_type, + StreamCollection* new_streams) { + TrackInfos* current_tracks = GetRemoteTracks(media_type); + + // Find removed tracks. Ie tracks where the track id or ssrc don't match the + // new StreamParam. + TrackInfos::iterator track_it = current_tracks->begin(); + while (track_it != current_tracks->end()) { + TrackInfo info = track_it->second; + cricket::StreamParams params; + if (!cricket::GetStreamBySsrc(streams, info.ssrc, ¶ms) || + params.id != info.track_id) { + OnRemoteTrackRemoved(info.stream_label, info.track_id, media_type); + current_tracks->erase(track_it++); + } else { + ++track_it; + } + } + + // Find new and active tracks. + for (cricket::StreamParamsVec::const_iterator it = streams.begin(); + it != streams.end(); ++it) { + // The sync_label is the MediaStream label and the |stream.id| is the + // track id. + const std::string& stream_label = it->sync_label; + const std::string& track_id = it->id; + uint32 ssrc = it->first_ssrc(); + + talk_base::scoped_refptr stream = + remote_streams_->find(stream_label); + if (!stream) { + // This is a new MediaStream. Create a new remote MediaStream. + stream = remote_stream_factory_->CreateMediaStream(stream_label); + remote_streams_->AddStream(stream); + new_streams->AddStream(stream); + } + + TrackInfos::iterator track_it = current_tracks->find(track_id); + if (track_it == current_tracks->end()) { + (*current_tracks)[track_id] = + TrackInfo(stream_label, track_id, ssrc); + OnRemoteTrackSeen(stream_label, track_id, it->first_ssrc(), media_type); + } + } +} + +void MediaStreamSignaling::OnRemoteTrackSeen(const std::string& stream_label, + const std::string& track_id, + uint32 ssrc, + cricket::MediaType media_type) { + MediaStreamInterface* stream = remote_streams_->find(stream_label); + + if (media_type == cricket::MEDIA_TYPE_AUDIO) { + AudioTrackInterface* audio_track = + remote_stream_factory_->AddAudioTrack(stream, track_id); + stream_observer_->OnAddRemoteAudioTrack(stream, audio_track, ssrc); + } else if (media_type == cricket::MEDIA_TYPE_VIDEO) { + VideoTrackInterface* video_track = + remote_stream_factory_->AddVideoTrack(stream, track_id); + stream_observer_->OnAddRemoteVideoTrack(stream, video_track, ssrc); + } else { + ASSERT(false && "Invalid media type"); + } +} + +void MediaStreamSignaling::OnRemoteTrackRemoved( + const std::string& stream_label, + const std::string& track_id, + cricket::MediaType media_type) { + MediaStreamInterface* stream = remote_streams_->find(stream_label); + + if (media_type == cricket::MEDIA_TYPE_AUDIO) { + talk_base::scoped_refptr audio_track = + stream->FindAudioTrack(track_id); + audio_track->set_state(webrtc::MediaStreamTrackInterface::kEnded); + stream->RemoveTrack(audio_track); + stream_observer_->OnRemoveRemoteAudioTrack(stream, audio_track); + } else if (media_type == cricket::MEDIA_TYPE_VIDEO) { + talk_base::scoped_refptr video_track = + stream->FindVideoTrack(track_id); + video_track->set_state(webrtc::MediaStreamTrackInterface::kEnded); + stream->RemoveTrack(video_track); + stream_observer_->OnRemoveRemoteVideoTrack(stream, video_track); + } else { + ASSERT(false && "Invalid media type"); + } +} + +void MediaStreamSignaling::RejectRemoteTracks(cricket::MediaType media_type) { + TrackInfos* current_tracks = GetRemoteTracks(media_type); + for (TrackInfos::iterator track_it = current_tracks->begin(); + track_it != current_tracks->end(); ++track_it) { + TrackInfo info = track_it->second; + MediaStreamInterface* stream = remote_streams_->find(info.stream_label); + if (media_type == cricket::MEDIA_TYPE_AUDIO) { + AudioTrackInterface* track = stream->FindAudioTrack(info.track_id); + track->set_state(webrtc::MediaStreamTrackInterface::kEnded); + } + if (media_type == cricket::MEDIA_TYPE_VIDEO) { + VideoTrackInterface* track = stream->FindVideoTrack(info.track_id); + track->set_state(webrtc::MediaStreamTrackInterface::kEnded); + } + } +} + +void MediaStreamSignaling::UpdateEndedRemoteMediaStreams() { + std::vector > streams_to_remove; + for (size_t i = 0; i < remote_streams_->count(); ++i) { + MediaStreamInterface*stream = remote_streams_->at(i); + if (stream->GetAudioTracks().empty() && stream->GetVideoTracks().empty()) { + streams_to_remove.push_back(stream); + } + } + + std::vector >::const_iterator it; + for (it = streams_to_remove.begin(); it != streams_to_remove.end(); ++it) { + remote_streams_->RemoveStream(*it); + stream_observer_->OnRemoveRemoteStream(*it); + } +} + +void MediaStreamSignaling::MaybeCreateDefaultStream() { + if (!remote_info_.IsDefaultMediaStreamNeeded()) + return; + + bool default_created = false; + + scoped_refptr default_remote_stream = + remote_streams_->find(kDefaultStreamLabel); + if (default_remote_stream == NULL) { + default_created = true; + default_remote_stream = + remote_stream_factory_->CreateMediaStream(kDefaultStreamLabel); + remote_streams_->AddStream(default_remote_stream); + } + if (remote_info_.default_audio_track_needed && + default_remote_stream->GetAudioTracks().size() == 0) { + remote_audio_tracks_[kDefaultAudioTrackLabel] = + TrackInfo(kDefaultStreamLabel, kDefaultAudioTrackLabel, 0); + OnRemoteTrackSeen(kDefaultStreamLabel, kDefaultAudioTrackLabel, 0, + cricket::MEDIA_TYPE_AUDIO); + } + if (remote_info_.default_video_track_needed && + default_remote_stream->GetVideoTracks().size() == 0) { + remote_video_tracks_[kDefaultVideoTrackLabel] = + TrackInfo(kDefaultStreamLabel, kDefaultVideoTrackLabel, 0); + OnRemoteTrackSeen(kDefaultStreamLabel, kDefaultVideoTrackLabel, 0, + cricket::MEDIA_TYPE_VIDEO); + } + if (default_created) { + stream_observer_->OnAddRemoteStream(default_remote_stream); + } +} + +MediaStreamSignaling::TrackInfos* MediaStreamSignaling::GetRemoteTracks( + cricket::MediaType type) { + if (type == cricket::MEDIA_TYPE_AUDIO) + return &remote_audio_tracks_; + else if (type == cricket::MEDIA_TYPE_VIDEO) + return &remote_video_tracks_; + ASSERT(false && "Unknown MediaType"); + return NULL; +} + +MediaStreamSignaling::TrackInfos* MediaStreamSignaling::GetLocalTracks( + cricket::MediaType media_type) { + ASSERT(media_type == cricket::MEDIA_TYPE_AUDIO || + media_type == cricket::MEDIA_TYPE_VIDEO); + + return (media_type == cricket::MEDIA_TYPE_AUDIO) ? + &local_audio_tracks_ : &local_video_tracks_; +} + +void MediaStreamSignaling::UpdateLocalTracks( + const std::vector& streams, + cricket::MediaType media_type) { + TrackInfos* current_tracks = GetLocalTracks(media_type); + + // Find removed tracks. Ie tracks where the track id or ssrc don't match the + // new StreamParam. + TrackInfos::iterator track_it = current_tracks->begin(); + while (track_it != current_tracks->end()) { + TrackInfo info = track_it->second; + cricket::StreamParams params; + if (!cricket::GetStreamBySsrc(streams, info.ssrc, ¶ms) || + params.id != info.track_id) { + OnLocalTrackRemoved(info.stream_label, info.track_id, media_type); + current_tracks->erase(track_it++); + } else { + ++track_it; + } + } + + // Find new and active tracks. + for (cricket::StreamParamsVec::const_iterator it = streams.begin(); + it != streams.end(); ++it) { + // The sync_label is the MediaStream label and the |stream.id| is the + // track id. + const std::string& stream_label = it->sync_label; + const std::string& track_id = it->id; + uint32 ssrc = it->first_ssrc(); + TrackInfos::iterator track_it = current_tracks->find(track_id); + if (track_it == current_tracks->end()) { + (*current_tracks)[track_id] = + TrackInfo(stream_label, track_id, ssrc); + OnLocalTrackSeen(stream_label, track_id, it->first_ssrc(), + media_type); + } + } +} + +void MediaStreamSignaling::OnLocalTrackSeen( + const std::string& stream_label, + const std::string& track_id, + uint32 ssrc, + cricket::MediaType media_type) { + MediaStreamInterface* stream = local_streams_->find(stream_label); + if (!stream) { + LOG(LS_WARNING) << "An unknown local MediaStream with label " + << stream_label << " has been configured."; + return; + } + + if (media_type == cricket::MEDIA_TYPE_AUDIO) { + AudioTrackInterface* audio_track = stream->FindAudioTrack(track_id); + if (!audio_track) { + LOG(LS_WARNING) << "An unknown local AudioTrack with id , " + << track_id << " has been configured."; + return; + } + stream_observer_->OnAddLocalAudioTrack(stream, audio_track, ssrc); + } else if (media_type == cricket::MEDIA_TYPE_VIDEO) { + VideoTrackInterface* video_track = stream->FindVideoTrack(track_id); + if (!video_track) { + LOG(LS_WARNING) << "An unknown local VideoTrack with id , " + << track_id << " has been configured."; + return; + } + stream_observer_->OnAddLocalVideoTrack(stream, video_track, ssrc); + } else { + ASSERT(false && "Invalid media type"); + } +} + +void MediaStreamSignaling::OnLocalTrackRemoved( + const std::string& stream_label, + const std::string& track_id, + cricket::MediaType media_type) { + MediaStreamInterface* stream = local_streams_->find(stream_label); + if (!stream) { + // This is the normal case. Ie RemoveLocalStream has been called and the + // SessionDescriptions has been renegotiated. + return; + } + // A track has been removed from the SessionDescription but the MediaStream + // is still associated with MediaStreamSignaling. This only occurs if the SDP + // doesn't match with the calls to AddLocalStream and RemoveLocalStream. + + if (media_type == cricket::MEDIA_TYPE_AUDIO) { + AudioTrackInterface* audio_track = stream->FindAudioTrack(track_id); + if (!audio_track) { + return; + } + stream_observer_->OnRemoveLocalAudioTrack(stream, audio_track); + } else if (media_type == cricket::MEDIA_TYPE_VIDEO) { + VideoTrackInterface* video_track = stream->FindVideoTrack(track_id); + if (!video_track) { + return; + } + stream_observer_->OnRemoveLocalVideoTrack(stream, video_track); + } else { + ASSERT(false && "Invalid media type."); + } +} + +void MediaStreamSignaling::UpdateLocalRtpDataChannels( + const cricket::StreamParamsVec& streams) { + std::vector existing_channels; + + // Find new and active data channels. + for (cricket::StreamParamsVec::const_iterator it =streams.begin(); + it != streams.end(); ++it) { + // |it->sync_label| is actually the data channel label. The reason is that + // we use the same naming of data channels as we do for + // MediaStreams and Tracks. + // For MediaStreams, the sync_label is the MediaStream label and the + // track label is the same as |streamid|. + const std::string& channel_label = it->sync_label; + DataChannels::iterator data_channel_it = data_channels_.find(channel_label); + if (!VERIFY(data_channel_it != data_channels_.end())) { + continue; + } + // Set the SSRC the data channel should use for sending. + data_channel_it->second->SetSendSsrc(it->first_ssrc()); + existing_channels.push_back(data_channel_it->first); + } + + UpdateClosingDataChannels(existing_channels, true); +} + +void MediaStreamSignaling::UpdateRemoteRtpDataChannels( + const cricket::StreamParamsVec& streams) { + std::vector existing_channels; + + // Find new and active data channels. + for (cricket::StreamParamsVec::const_iterator it = streams.begin(); + it != streams.end(); ++it) { + // The data channel label is either the mslabel or the SSRC if the mslabel + // does not exist. Ex a=ssrc:444330170 mslabel:test1. + std::string label = it->sync_label.empty() ? + talk_base::ToString(it->first_ssrc()) : it->sync_label; + DataChannels::iterator data_channel_it = + data_channels_.find(label); + if (data_channel_it == data_channels_.end()) { + // This is a new data channel. + CreateRemoteDataChannel(label, it->first_ssrc()); + } else { + data_channel_it->second->SetReceiveSsrc(it->first_ssrc()); + } + existing_channels.push_back(label); + } + + UpdateClosingDataChannels(existing_channels, false); +} + +void MediaStreamSignaling::UpdateClosingDataChannels( + const std::vector& active_channels, bool is_local_update) { + DataChannels::iterator it = data_channels_.begin(); + while (it != data_channels_.end()) { + DataChannel* data_channel = it->second; + if (std::find(active_channels.begin(), active_channels.end(), + data_channel->label()) != active_channels.end()) { + ++it; + continue; + } + + if (is_local_update) + data_channel->SetSendSsrc(0); + else + data_channel->RemotePeerRequestClose(); + + if (data_channel->state() == DataChannel::kClosed) { + data_channels_.erase(it); + it = data_channels_.begin(); + } else { + ++it; + } + } +} + +void MediaStreamSignaling::CreateRemoteDataChannel(const std::string& label, + uint32 remote_ssrc) { + if (!data_channel_factory_) { + LOG(LS_WARNING) << "Remote peer requested a DataChannel but DataChannels " + << "are not supported."; + return; + } + scoped_refptr channel( + data_channel_factory_->CreateDataChannel(label, NULL)); + channel->SetReceiveSsrc(remote_ssrc); + stream_observer_->OnAddDataChannel(channel); +} + +void MediaStreamSignaling::UpdateLocalSctpDataChannels() { + DataChannels::iterator it = data_channels_.begin(); + for (; it != data_channels_.end(); ++it) { + DataChannel* data_channel = it->second; + data_channel->SetSendSsrc(data_channel->id()); + } +} + +void MediaStreamSignaling::UpdateRemoteSctpDataChannels() { + DataChannels::iterator it = data_channels_.begin(); + for (; it != data_channels_.end(); ++it) { + DataChannel* data_channel = it->second; + data_channel->SetReceiveSsrc(data_channel->id()); + } +} + +} // namespace webrtc diff --git a/talk/app/webrtc/mediastreamsignaling.h b/talk/app/webrtc/mediastreamsignaling.h new file mode 100644 index 000000000..9ead8b096 --- /dev/null +++ b/talk/app/webrtc/mediastreamsignaling.h @@ -0,0 +1,385 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_MEDIASTREAMSIGNALING_H_ +#define TALK_APP_WEBRTC_MEDIASTREAMSIGNALING_H_ + +#include +#include +#include + +#include "talk/app/webrtc/datachannel.h" +#include "talk/app/webrtc/mediastream.h" +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/app/webrtc/streamcollection.h" +#include "talk/base/scoped_ref_ptr.h" +#include "talk/session/media/mediasession.h" + +namespace talk_base { +class Thread; +} // namespace talk_base + +namespace webrtc { + +class RemoteMediaStreamFactory; + +// A MediaStreamSignalingObserver is notified when events happen to +// MediaStreams, MediaStreamTracks or DataChannels associated with the observed +// MediaStreamSignaling object. The notifications identify the stream, track or +// channel. +class MediaStreamSignalingObserver { + public: + // Triggered when the remote SessionDescription has a new stream. + virtual void OnAddRemoteStream(MediaStreamInterface* stream) = 0; + + // Triggered when the remote SessionDescription removes a stream. + virtual void OnRemoveRemoteStream(MediaStreamInterface* stream) = 0; + + // Triggered when the remote SessionDescription has a new data channel. + virtual void OnAddDataChannel(DataChannelInterface* data_channel) = 0; + + // Triggered when the remote SessionDescription has a new audio track. + virtual void OnAddRemoteAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track, + uint32 ssrc) = 0; + + // Triggered when the remote SessionDescription has a new video track. + virtual void OnAddRemoteVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track, + uint32 ssrc) = 0; + + // Triggered when the remote SessionDescription has removed an audio track. + virtual void OnRemoveRemoteAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track) = 0; + + // Triggered when the remote SessionDescription has removed a video track. + virtual void OnRemoveRemoteVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track) = 0; + + // Triggered when the local SessionDescription has a new audio track. + virtual void OnAddLocalAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track, + uint32 ssrc) = 0; + + // Triggered when the local SessionDescription has a new video track. + virtual void OnAddLocalVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track, + uint32 ssrc) = 0; + + // Triggered when the local SessionDescription has removed an audio track. + virtual void OnRemoveLocalAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track) = 0; + + // Triggered when the local SessionDescription has removed a video track. + virtual void OnRemoveLocalVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track) = 0; + + // Triggered when RemoveLocalStream is called. |stream| is no longer used + // when negotiating and all tracks in |stream| should stop providing data to + // this PeerConnection. This doesn't mean that the local session description + // has changed and OnRemoveLocalAudioTrack and OnRemoveLocalVideoTrack is not + // called for each individual track. + virtual void OnRemoveLocalStream(MediaStreamInterface* stream) = 0; + + protected: + ~MediaStreamSignalingObserver() {} +}; + +// MediaStreamSignaling works as a glue between MediaStreams and a cricket +// classes for SessionDescriptions. +// It is used for creating cricket::MediaSessionOptions given the local +// MediaStreams and data channels. +// +// It is responsible for creating remote MediaStreams given a remote +// SessionDescription and creating cricket::MediaSessionOptions given +// local MediaStreams. +// +// To signal that a DataChannel should be established: +// 1. Call AddDataChannel with the new DataChannel. Next time +// GetMediaSessionOptions will include the description of the DataChannel. +// 2. When a local session description is set, call UpdateLocalStreams with the +// session description. This will set the SSRC used for sending data on +// this DataChannel. +// 3. When remote session description is set, call UpdateRemoteStream with the +// session description. If the DataChannel label and a SSRC is included in +// the description, the DataChannel is updated with SSRC that will be used +// for receiving data. +// 4. When both the local and remote SSRC of a DataChannel is set the state of +// the DataChannel change to kOpen. +// +// To setup a DataChannel initialized by the remote end. +// 1. When remote session description is set, call UpdateRemoteStream with the +// session description. If a label and a SSRC of a new DataChannel is found +// MediaStreamSignalingObserver::OnAddDataChannel with the label and SSRC is +// triggered. +// 2. Create a DataChannel instance with the label and set the remote SSRC. +// 3. Call AddDataChannel with this new DataChannel. GetMediaSessionOptions +// will include the description of the DataChannel. +// 4. Create a local session description and call UpdateLocalStreams. This will +// set the local SSRC used by the DataChannel. +// 5. When both the local and remote SSRC of a DataChannel is set the state of +// the DataChannel change to kOpen. +// +// To close a DataChannel: +// 1. Call DataChannel::Close. This will change the state of the DataChannel to +// kClosing. GetMediaSessionOptions will not +// include the description of the DataChannel. +// 2. When a local session description is set, call UpdateLocalStreams with the +// session description. The description will no longer contain the +// DataChannel label or SSRC. +// 3. When remote session description is set, call UpdateRemoteStream with the +// session description. The description will no longer contain the +// DataChannel label or SSRC. The DataChannel SSRC is updated with SSRC=0. +// The DataChannel change state to kClosed. + +class MediaStreamSignaling { + public: + MediaStreamSignaling(talk_base::Thread* signaling_thread, + MediaStreamSignalingObserver* stream_observer); + virtual ~MediaStreamSignaling(); + + // Notify all referenced objects that MediaStreamSignaling will be teared + // down. This method must be called prior to the dtor. + void TearDown(); + + // Set a factory for creating data channels that are initiated by the remote + // peer. + void SetDataChannelFactory(DataChannelFactory* data_channel_factory) { + data_channel_factory_ = data_channel_factory; + } + + // Checks if |id| is available to be assigned to a new SCTP data channel. + bool IsSctpIdAvailable(int id) const; + + // Gets the first available SCTP id that is not assigned to any existing + // data channels. + bool AllocateSctpId(int* id); + + // Adds |local_stream| to the collection of known MediaStreams that will be + // offered in a SessionDescription. + bool AddLocalStream(MediaStreamInterface* local_stream); + + // Removes |local_stream| from the collection of known MediaStreams that will + // be offered in a SessionDescription. + void RemoveLocalStream(MediaStreamInterface* local_stream); + + // Adds |data_channel| to the collection of DataChannels that will be + // be offered in a SessionDescription. + bool AddDataChannel(DataChannel* data_channel); + + // Returns a MediaSessionOptions struct with options decided by |constraints|, + // the local MediaStreams and DataChannels. + virtual bool GetOptionsForOffer( + const MediaConstraintsInterface* constraints, + cricket::MediaSessionOptions* options); + + // Returns a MediaSessionOptions struct with options decided by + // |constraints|, the local MediaStreams and DataChannels. + virtual bool GetOptionsForAnswer( + const MediaConstraintsInterface* constraints, + cricket::MediaSessionOptions* options); + + // Called when the remote session description has changed. The purpose is to + // update remote MediaStreams and DataChannels with the current + // session state. + // If the remote SessionDescription contain information about a new remote + // MediaStreams a new remote MediaStream is created and + // MediaStreamSignalingObserver::OnAddStream is called. + // If a remote MediaStream is missing from + // the remote SessionDescription MediaStreamSignalingObserver::OnRemoveStream + // is called. + // If the SessionDescription contains information about a new DataChannel, + // MediaStreamSignalingObserver::OnAddDataChannel is called with the + // DataChannel. + void OnRemoteDescriptionChanged(const SessionDescriptionInterface* desc); + + // Called when the local session description has changed. The purpose is to + // update local and remote MediaStreams and DataChannels with the current + // session state. + // If |desc| indicates that the media type should be rejected, the method + // ends the remote MediaStreamTracks. + // It also updates local DataChannels with information about its local SSRC. + void OnLocalDescriptionChanged(const SessionDescriptionInterface* desc); + + // Called when the audio channel closes. + void OnAudioChannelClose(); + // Called when the video channel closes. + void OnVideoChannelClose(); + // Called when the data channel closes. + void OnDataChannelClose(); + + // Returns the SSRC for a given track. + bool GetRemoteAudioTrackSsrc(const std::string& track_id, uint32* ssrc) const; + bool GetRemoteVideoTrackSsrc(const std::string& track_id, uint32* ssrc) const; + + // Returns all current known local MediaStreams. + StreamCollectionInterface* local_streams() const { return local_streams_;} + + // Returns all current remote MediaStreams. + StreamCollectionInterface* remote_streams() const { + return remote_streams_.get(); + } + + private: + struct RemotePeerInfo { + RemotePeerInfo() + : msid_supported(false), + default_audio_track_needed(false), + default_video_track_needed(false) { + } + // True if it has been discovered that the remote peer support MSID. + bool msid_supported; + // The remote peer indicates in the session description that audio will be + // sent but no MSID is given. + bool default_audio_track_needed; + // The remote peer indicates in the session description that video will be + // sent but no MSID is given. + bool default_video_track_needed; + + bool IsDefaultMediaStreamNeeded() { + return !msid_supported && (default_audio_track_needed || + default_video_track_needed); + } + }; + + struct TrackInfo { + TrackInfo() : ssrc(0) {} + TrackInfo(const std::string& stream_label, + const std::string track_id, + uint32 ssrc) + : stream_label(stream_label), + track_id(track_id), + ssrc(ssrc) { + } + std::string stream_label; + std::string track_id; + uint32 ssrc; + }; + typedef std::map TrackInfos; + + void UpdateSessionOptions(); + + // Makes sure a MediaStream Track is created for each StreamParam in + // |streams|. |media_type| is the type of the |streams| and can be either + // audio or video. + // If a new MediaStream is created it is added to |new_streams|. + void UpdateRemoteStreamsList( + const std::vector& streams, + cricket::MediaType media_type, + StreamCollection* new_streams); + + // Triggered when a remote track has been seen for the first time in a remote + // session description. It creates a remote MediaStreamTrackInterface + // implementation and triggers MediaStreamSignaling::OnAddRemoteAudioTrack or + // MediaStreamSignaling::OnAddRemoteVideoTrack. + void OnRemoteTrackSeen(const std::string& stream_label, + const std::string& track_id, + uint32 ssrc, + cricket::MediaType media_type); + + // Triggered when a remote track has been removed from a remote session + // description. It removes the remote track with id |track_id| from a remote + // MediaStream and triggers MediaStreamSignaling::OnRemoveRemoteAudioTrack or + // MediaStreamSignaling::OnRemoveRemoteVideoTrack. + void OnRemoteTrackRemoved(const std::string& stream_label, + const std::string& track_id, + cricket::MediaType media_type); + + // Set the MediaStreamTrackInterface::TrackState to |kEnded| on all remote + // tracks of type |media_type|. + void RejectRemoteTracks(cricket::MediaType media_type); + + // Finds remote MediaStreams without any tracks and removes them from + // |remote_streams_| and notifies the observer that the MediaStream no longer + // exist. + void UpdateEndedRemoteMediaStreams(); + void MaybeCreateDefaultStream(); + TrackInfos* GetRemoteTracks(cricket::MediaType type); + + // Returns a map of currently negotiated LocalTrackInfo of type |type|. + TrackInfos* GetLocalTracks(cricket::MediaType type); + bool FindLocalTrack(const std::string& track_id, cricket::MediaType type); + + // Loops through the vector of |streams| and finds added and removed + // StreamParams since last time this method was called. + // For each new or removed StreamParam NotifyLocalTrackAdded or + // NotifyLocalTrackRemoved in invoked. + void UpdateLocalTracks(const std::vector& streams, + cricket::MediaType media_type); + + // Triggered when a local track has been seen for the first time in a local + // session description. + // This method triggers MediaStreamSignaling::OnAddLocalAudioTrack or + // MediaStreamSignaling::OnAddLocalVideoTrack if the rtp streams in the local + // SessionDescription can be mapped to a MediaStreamTrack in a MediaStream in + // |local_streams_| + void OnLocalTrackSeen(const std::string& stream_label, + const std::string& track_id, + uint32 ssrc, + cricket::MediaType media_type); + + // Triggered when a local track has been removed from a local session + // description. + // This method triggers MediaStreamSignaling::OnRemoveLocalAudioTrack or + // MediaStreamSignaling::OnRemoveLocalVideoTrack if a stream has been removed + // from the local SessionDescription and the stream can be mapped to a + // MediaStreamTrack in a MediaStream in |local_streams_|. + void OnLocalTrackRemoved(const std::string& stream_label, + const std::string& track_id, + cricket::MediaType media_type); + + void UpdateLocalRtpDataChannels(const cricket::StreamParamsVec& streams); + void UpdateRemoteRtpDataChannels(const cricket::StreamParamsVec& streams); + void UpdateClosingDataChannels( + const std::vector& active_channels, bool is_local_update); + void CreateRemoteDataChannel(const std::string& label, uint32 remote_ssrc); + void UpdateLocalSctpDataChannels(); + void UpdateRemoteSctpDataChannels(); + + RemotePeerInfo remote_info_; + talk_base::Thread* signaling_thread_; + DataChannelFactory* data_channel_factory_; + cricket::MediaSessionOptions options_; + MediaStreamSignalingObserver* stream_observer_; + talk_base::scoped_refptr local_streams_; + talk_base::scoped_refptr remote_streams_; + talk_base::scoped_ptr remote_stream_factory_; + + TrackInfos remote_audio_tracks_; + TrackInfos remote_video_tracks_; + TrackInfos local_audio_tracks_; + TrackInfos local_video_tracks_; + + int last_allocated_sctp_id_; + typedef std::map > + DataChannels; + DataChannels data_channels_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_MEDIASTREAMSIGNALING_H_ diff --git a/talk/app/webrtc/mediastreamsignaling_unittest.cc b/talk/app/webrtc/mediastreamsignaling_unittest.cc new file mode 100644 index 000000000..7f8745403 --- /dev/null +++ b/talk/app/webrtc/mediastreamsignaling_unittest.cc @@ -0,0 +1,949 @@ +/* + * libjingle + * Copyright 2012, 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 + +#include "talk/app/webrtc/audiotrack.h" +#include "talk/app/webrtc/mediastream.h" +#include "talk/app/webrtc/mediastreamsignaling.h" +#include "talk/app/webrtc/streamcollection.h" +#include "talk/app/webrtc/test/fakeconstraints.h" +#include "talk/app/webrtc/videotrack.h" +#include "talk/base/gunit.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/sessiondescription.h" + +static const char kStreams[][8] = {"stream1", "stream2"}; +static const char kAudioTracks[][32] = {"audiotrack0", "audiotrack1"}; +static const char kVideoTracks[][32] = {"videotrack0", "videotrack1"}; + +using webrtc::AudioTrack; +using webrtc::AudioTrackInterface; +using webrtc::AudioTrackVector; +using webrtc::VideoTrack; +using webrtc::VideoTrackInterface; +using webrtc::VideoTrackVector; +using webrtc::DataChannelInterface; +using webrtc::FakeConstraints; +using webrtc::IceCandidateInterface; +using webrtc::MediaConstraintsInterface; +using webrtc::MediaStreamInterface; +using webrtc::MediaStreamTrackInterface; +using webrtc::SdpParseError; +using webrtc::SessionDescriptionInterface; +using webrtc::StreamCollection; +using webrtc::StreamCollectionInterface; + +// Reference SDP with a MediaStream with label "stream1" and audio track with +// id "audio_1" and a video track with id "video_1; +static const char kSdpStringWithStream1[] = + "v=0\r\n" + "o=- 0 0 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "m=audio 1 RTP/AVPF 103\r\n" + "a=mid:audio\r\n" + "a=rtpmap:103 ISAC/16000\r\n" + "a=ssrc:1 cname:stream1\r\n" + "a=ssrc:1 mslabel:stream1\r\n" + "a=ssrc:1 label:audiotrack0\r\n" + "m=video 1 RTP/AVPF 120\r\n" + "a=mid:video\r\n" + "a=rtpmap:120 VP8/90000\r\n" + "a=ssrc:2 cname:stream1\r\n" + "a=ssrc:2 mslabel:stream1\r\n" + "a=ssrc:2 label:videotrack0\r\n"; + +// Reference SDP with two MediaStreams with label "stream1" and "stream2. Each +// MediaStreams have one audio track and one video track. +// This uses MSID. +static const char kSdpStringWith2Stream[] = + "v=0\r\n" + "o=- 0 0 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "a=msid-semantic: WMS stream1 stream2\r\n" + "m=audio 1 RTP/AVPF 103\r\n" + "a=mid:audio\r\n" + "a=rtpmap:103 ISAC/16000\r\n" + "a=ssrc:1 cname:stream1\r\n" + "a=ssrc:1 msid:stream1 audiotrack0\r\n" + "a=ssrc:3 cname:stream2\r\n" + "a=ssrc:3 msid:stream2 audiotrack1\r\n" + "m=video 1 RTP/AVPF 120\r\n" + "a=mid:video\r\n" + "a=rtpmap:120 VP8/0\r\n" + "a=ssrc:2 cname:stream1\r\n" + "a=ssrc:2 msid:stream1 videotrack0\r\n" + "a=ssrc:4 cname:stream2\r\n" + "a=ssrc:4 msid:stream2 videotrack1\r\n"; + +// Reference SDP without MediaStreams. Msid is not supported. +static const char kSdpStringWithoutStreams[] = + "v=0\r\n" + "o=- 0 0 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "m=audio 1 RTP/AVPF 103\r\n" + "a=mid:audio\r\n" + "a=rtpmap:103 ISAC/16000\r\n" + "m=video 1 RTP/AVPF 120\r\n" + "a=mid:video\r\n" + "a=rtpmap:120 VP8/90000\r\n"; + +// Reference SDP without MediaStreams. Msid is supported. +static const char kSdpStringWithMsidWithoutStreams[] = + "v=0\r\n" + "o=- 0 0 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "a:msid-semantic: WMS\r\n" + "m=audio 1 RTP/AVPF 103\r\n" + "a=mid:audio\r\n" + "a=rtpmap:103 ISAC/16000\r\n" + "m=video 1 RTP/AVPF 120\r\n" + "a=mid:video\r\n" + "a=rtpmap:120 VP8/90000\r\n"; + +// Reference SDP without MediaStreams and audio only. +static const char kSdpStringWithoutStreamsAudioOnly[] = + "v=0\r\n" + "o=- 0 0 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "m=audio 1 RTP/AVPF 103\r\n" + "a=mid:audio\r\n" + "a=rtpmap:103 ISAC/16000\r\n"; + +static const char kSdpStringInit[] = + "v=0\r\n" + "o=- 0 0 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "a=msid-semantic: WMS\r\n"; + +static const char kSdpStringAudio[] = + "m=audio 1 RTP/AVPF 103\r\n" + "a=mid:audio\r\n" + "a=rtpmap:103 ISAC/16000\r\n"; + +static const char kSdpStringVideo[] = + "m=video 1 RTP/AVPF 120\r\n" + "a=mid:video\r\n" + "a=rtpmap:120 VP8/90000\r\n"; + +static const char kSdpStringMs1Audio0[] = + "a=ssrc:1 cname:stream1\r\n" + "a=ssrc:1 msid:stream1 audiotrack0\r\n"; + +static const char kSdpStringMs1Video0[] = + "a=ssrc:2 cname:stream1\r\n" + "a=ssrc:2 msid:stream1 videotrack0\r\n"; + +static const char kSdpStringMs1Audio1[] = + "a=ssrc:3 cname:stream1\r\n" + "a=ssrc:3 msid:stream1 audiotrack1\r\n"; + +static const char kSdpStringMs1Video1[] = + "a=ssrc:4 cname:stream1\r\n" + "a=ssrc:4 msid:stream1 videotrack1\r\n"; + +// Verifies that |options| contain all tracks in |collection| and that +// the |options| has set the the has_audio and has_video flags correct. +static void VerifyMediaOptions(StreamCollectionInterface* collection, + const cricket::MediaSessionOptions& options) { + if (!collection) { + return; + } + + size_t stream_index = 0; + for (size_t i = 0; i < collection->count(); ++i) { + MediaStreamInterface* stream = collection->at(i); + AudioTrackVector audio_tracks = stream->GetAudioTracks(); + ASSERT_GE(options.streams.size(), stream_index + audio_tracks.size()); + for (size_t j = 0; j < audio_tracks.size(); ++j) { + webrtc::AudioTrackInterface* audio = audio_tracks[j]; + EXPECT_EQ(options.streams[stream_index].sync_label, stream->label()); + EXPECT_EQ(options.streams[stream_index++].id, audio->id()); + EXPECT_TRUE(options.has_audio); + } + VideoTrackVector video_tracks = stream->GetVideoTracks(); + ASSERT_GE(options.streams.size(), stream_index + video_tracks.size()); + for (size_t j = 0; j < video_tracks.size(); ++j) { + webrtc::VideoTrackInterface* video = video_tracks[j]; + EXPECT_EQ(options.streams[stream_index].sync_label, stream->label()); + EXPECT_EQ(options.streams[stream_index++].id, video->id()); + EXPECT_TRUE(options.has_video); + } + } +} + +static bool CompareStreamCollections(StreamCollectionInterface* s1, + StreamCollectionInterface* s2) { + if (s1 == NULL || s2 == NULL || s1->count() != s2->count()) + return false; + + for (size_t i = 0; i != s1->count(); ++i) { + if (s1->at(i)->label() != s2->at(i)->label()) + return false; + webrtc::AudioTrackVector audio_tracks1 = s1->at(i)->GetAudioTracks(); + webrtc::AudioTrackVector audio_tracks2 = s2->at(i)->GetAudioTracks(); + webrtc::VideoTrackVector video_tracks1 = s1->at(i)->GetVideoTracks(); + webrtc::VideoTrackVector video_tracks2 = s2->at(i)->GetVideoTracks(); + + if (audio_tracks1.size() != audio_tracks2.size()) + return false; + for (size_t j = 0; j != audio_tracks1.size(); ++j) { + if (audio_tracks1[j]->id() != audio_tracks2[j]->id()) + return false; + } + if (video_tracks1.size() != video_tracks2.size()) + return false; + for (size_t j = 0; j != video_tracks1.size(); ++j) { + if (video_tracks1[j]->id() != video_tracks2[j]->id()) + return false; + } + } + return true; +} + +class MockSignalingObserver : public webrtc::MediaStreamSignalingObserver { + public: + MockSignalingObserver() + : remote_media_streams_(StreamCollection::Create()) { + } + + virtual ~MockSignalingObserver() { + } + + // New remote stream have been discovered. + virtual void OnAddRemoteStream(MediaStreamInterface* remote_stream) { + remote_media_streams_->AddStream(remote_stream); + } + + // Remote stream is no longer available. + virtual void OnRemoveRemoteStream(MediaStreamInterface* remote_stream) { + remote_media_streams_->RemoveStream(remote_stream); + } + + virtual void OnAddDataChannel(DataChannelInterface* data_channel) { + } + + virtual void OnAddLocalAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track, + uint32 ssrc) { + AddTrack(&local_audio_tracks_, stream, audio_track, ssrc); + } + + virtual void OnAddLocalVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track, + uint32 ssrc) { + AddTrack(&local_video_tracks_, stream, video_track, ssrc); + } + + virtual void OnRemoveLocalAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track) { + RemoveTrack(&local_audio_tracks_, stream, audio_track); + } + + virtual void OnRemoveLocalVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track) { + RemoveTrack(&local_video_tracks_, stream, video_track); + } + + virtual void OnAddRemoteAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track, + uint32 ssrc) { + AddTrack(&remote_audio_tracks_, stream, audio_track, ssrc); + } + + virtual void OnAddRemoteVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track, + uint32 ssrc) { + AddTrack(&remote_video_tracks_, stream, video_track, ssrc); + } + + virtual void OnRemoveRemoteAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track) { + RemoveTrack(&remote_audio_tracks_, stream, audio_track); + } + + virtual void OnRemoveRemoteVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track) { + RemoveTrack(&remote_video_tracks_, stream, video_track); + } + + virtual void OnRemoveLocalStream(MediaStreamInterface* stream) { + } + + MediaStreamInterface* RemoteStream(const std::string& label) { + return remote_media_streams_->find(label); + } + + StreamCollectionInterface* remote_streams() const { + return remote_media_streams_; + } + + size_t NumberOfRemoteAudioTracks() { return remote_audio_tracks_.size(); } + + void VerifyRemoteAudioTrack(const std::string& stream_label, + const std::string& track_id, + uint32 ssrc) { + VerifyTrack(remote_audio_tracks_, stream_label, track_id, ssrc); + } + + size_t NumberOfRemoteVideoTracks() { return remote_video_tracks_.size(); } + + void VerifyRemoteVideoTrack(const std::string& stream_label, + const std::string& track_id, + uint32 ssrc) { + VerifyTrack(remote_video_tracks_, stream_label, track_id, ssrc); + } + + size_t NumberOfLocalAudioTracks() { return local_audio_tracks_.size(); } + void VerifyLocalAudioTrack(const std::string& stream_label, + const std::string& track_id, + uint32 ssrc) { + VerifyTrack(local_audio_tracks_, stream_label, track_id, ssrc); + } + + size_t NumberOfLocalVideoTracks() { return local_video_tracks_.size(); } + + void VerifyLocalVideoTrack(const std::string& stream_label, + const std::string& track_id, + uint32 ssrc) { + VerifyTrack(local_video_tracks_, stream_label, track_id, ssrc); + } + + private: + struct TrackInfo { + TrackInfo() {} + TrackInfo(const std::string& stream_label, const std::string track_id, + uint32 ssrc) + : stream_label(stream_label), + track_id(track_id), + ssrc(ssrc) { + } + std::string stream_label; + std::string track_id; + uint32 ssrc; + }; + typedef std::map TrackInfos; + + void AddTrack(TrackInfos* track_infos, MediaStreamInterface* stream, + MediaStreamTrackInterface* track, + uint32 ssrc) { + (*track_infos)[track->id()] = TrackInfo(stream->label(), track->id(), + ssrc); + } + + void RemoveTrack(TrackInfos* track_infos, MediaStreamInterface* stream, + MediaStreamTrackInterface* track) { + TrackInfos::iterator it = track_infos->find(track->id()); + ASSERT_TRUE(it != track_infos->end()); + ASSERT_EQ(it->second.stream_label, stream->label()); + track_infos->erase(it); + } + + void VerifyTrack(const TrackInfos& track_infos, + const std::string& stream_label, + const std::string& track_id, + uint32 ssrc) { + TrackInfos::const_iterator it = track_infos.find(track_id); + ASSERT_TRUE(it != track_infos.end()); + EXPECT_EQ(stream_label, it->second.stream_label); + EXPECT_EQ(ssrc, it->second.ssrc); + } + + TrackInfos remote_audio_tracks_; + TrackInfos remote_video_tracks_; + TrackInfos local_audio_tracks_; + TrackInfos local_video_tracks_; + + talk_base::scoped_refptr remote_media_streams_; +}; + +class MediaStreamSignalingForTest : public webrtc::MediaStreamSignaling { + public: + explicit MediaStreamSignalingForTest(MockSignalingObserver* observer) + : webrtc::MediaStreamSignaling(talk_base::Thread::Current(), observer) { + }; + + using webrtc::MediaStreamSignaling::GetOptionsForOffer; + using webrtc::MediaStreamSignaling::GetOptionsForAnswer; + using webrtc::MediaStreamSignaling::OnRemoteDescriptionChanged; + using webrtc::MediaStreamSignaling::remote_streams; +}; + +class MediaStreamSignalingTest: public testing::Test { + protected: + virtual void SetUp() { + observer_.reset(new MockSignalingObserver()); + signaling_.reset(new MediaStreamSignalingForTest(observer_.get())); + } + + // Create a collection of streams. + // CreateStreamCollection(1) creates a collection that + // correspond to kSdpString1. + // CreateStreamCollection(2) correspond to kSdpString2. + talk_base::scoped_refptr + CreateStreamCollection(int number_of_streams) { + talk_base::scoped_refptr local_collection( + StreamCollection::Create()); + + for (int i = 0; i < number_of_streams; ++i) { + talk_base::scoped_refptr stream( + webrtc::MediaStream::Create(kStreams[i])); + + // Add a local audio track. + talk_base::scoped_refptr audio_track( + webrtc::AudioTrack::Create(kAudioTracks[i], NULL)); + stream->AddTrack(audio_track); + + // Add a local video track. + talk_base::scoped_refptr video_track( + webrtc::VideoTrack::Create(kVideoTracks[i], NULL)); + stream->AddTrack(video_track); + + local_collection->AddStream(stream); + } + return local_collection; + } + + // This functions Creates a MediaStream with label kStreams[0] and + // |number_of_audio_tracks| and |number_of_video_tracks| tracks and the + // corresponding SessionDescriptionInterface. The SessionDescriptionInterface + // is returned in |desc| and the MediaStream is stored in + // |reference_collection_| + void CreateSessionDescriptionAndReference( + size_t number_of_audio_tracks, + size_t number_of_video_tracks, + SessionDescriptionInterface** desc) { + ASSERT_TRUE(desc != NULL); + ASSERT_LE(number_of_audio_tracks, 2u); + ASSERT_LE(number_of_video_tracks, 2u); + + reference_collection_ = StreamCollection::Create(); + std::string sdp_ms1 = std::string(kSdpStringInit); + + std::string mediastream_label = kStreams[0]; + + talk_base::scoped_refptr stream( + webrtc::MediaStream::Create(mediastream_label)); + reference_collection_->AddStream(stream); + + if (number_of_audio_tracks > 0) { + sdp_ms1 += std::string(kSdpStringAudio); + sdp_ms1 += std::string(kSdpStringMs1Audio0); + AddAudioTrack(kAudioTracks[0], stream); + } + if (number_of_audio_tracks > 1) { + sdp_ms1 += kSdpStringMs1Audio1; + AddAudioTrack(kAudioTracks[1], stream); + } + + if (number_of_video_tracks > 0) { + sdp_ms1 += std::string(kSdpStringVideo); + sdp_ms1 += std::string(kSdpStringMs1Video0); + AddVideoTrack(kVideoTracks[0], stream); + } + if (number_of_video_tracks > 1) { + sdp_ms1 += kSdpStringMs1Video1; + AddVideoTrack(kVideoTracks[1], stream); + } + + *desc = webrtc::CreateSessionDescription( + SessionDescriptionInterface::kOffer, sdp_ms1, NULL); + } + + void AddAudioTrack(const std::string& track_id, + MediaStreamInterface* stream) { + talk_base::scoped_refptr audio_track( + webrtc::AudioTrack::Create(track_id, NULL)); + ASSERT_TRUE(stream->AddTrack(audio_track)); + } + + void AddVideoTrack(const std::string& track_id, + MediaStreamInterface* stream) { + talk_base::scoped_refptr video_track( + webrtc::VideoTrack::Create(track_id, NULL)); + ASSERT_TRUE(stream->AddTrack(video_track)); + } + + talk_base::scoped_refptr reference_collection_; + talk_base::scoped_ptr observer_; + talk_base::scoped_ptr signaling_; +}; + +// Test that a MediaSessionOptions is created for an offer if +// kOfferToReceiveAudio and kOfferToReceiveVideo constraints are set but no +// MediaStreams are sent. +TEST_F(MediaStreamSignalingTest, GetMediaSessionOptionsForOfferWithAudioVideo) { + FakeConstraints constraints; + constraints.SetMandatoryReceiveAudio(true); + constraints.SetMandatoryReceiveVideo(true); + cricket::MediaSessionOptions options; + EXPECT_TRUE(signaling_->GetOptionsForOffer(&constraints, &options)); + EXPECT_TRUE(options.has_audio); + EXPECT_TRUE(options.has_video); + EXPECT_TRUE(options.bundle_enabled); +} + +// Test that a correct MediaSessionOptions is created for an offer if +// kOfferToReceiveAudio constraints is set but no MediaStreams are sent. +TEST_F(MediaStreamSignalingTest, GetMediaSessionOptionsForOfferWithAudio) { + FakeConstraints constraints; + constraints.SetMandatoryReceiveAudio(true); + cricket::MediaSessionOptions options; + EXPECT_TRUE(signaling_->GetOptionsForOffer(&constraints, &options)); + EXPECT_TRUE(options.has_audio); + EXPECT_FALSE(options.has_video); + EXPECT_TRUE(options.bundle_enabled); +} + +// Test that a correct MediaSessionOptions is created for an offer if +// no constraints or MediaStreams are sent. +TEST_F(MediaStreamSignalingTest, GetDefaultMediaSessionOptionsForOffer) { + cricket::MediaSessionOptions options; + EXPECT_TRUE(signaling_->GetOptionsForOffer(NULL, &options)); + EXPECT_TRUE(options.has_audio); + EXPECT_FALSE(options.has_video); + EXPECT_TRUE(options.bundle_enabled); +} + +// Test that a correct MediaSessionOptions is created for an offer if +// kOfferToReceiveVideo constraints is set but no MediaStreams are sent. +TEST_F(MediaStreamSignalingTest, GetMediaSessionOptionsForOfferWithVideo) { + FakeConstraints constraints; + constraints.SetMandatoryReceiveAudio(false); + constraints.SetMandatoryReceiveVideo(true); + cricket::MediaSessionOptions options; + EXPECT_TRUE(signaling_->GetOptionsForOffer(&constraints, &options)); + EXPECT_FALSE(options.has_audio); + EXPECT_TRUE(options.has_video); + EXPECT_TRUE(options.bundle_enabled); +} + +// Test that a correct MediaSessionOptions is created for an offer if +// kUseRtpMux constraints is set to false. +TEST_F(MediaStreamSignalingTest, + GetMediaSessionOptionsForOfferWithBundleDisabled) { + FakeConstraints constraints; + constraints.SetMandatoryReceiveAudio(true); + constraints.SetMandatoryReceiveVideo(true); + constraints.SetMandatoryUseRtpMux(false); + cricket::MediaSessionOptions options; + EXPECT_TRUE(signaling_->GetOptionsForOffer(&constraints, &options)); + EXPECT_TRUE(options.has_audio); + EXPECT_TRUE(options.has_video); + EXPECT_FALSE(options.bundle_enabled); +} + +// Test that a correct MediaSessionOptions is created to restart ice if +// kIceRestart constraints is set. It also tests that subsequent +// MediaSessionOptions don't have |transport_options.ice_restart| set. +TEST_F(MediaStreamSignalingTest, + GetMediaSessionOptionsForOfferWithIceRestart) { + FakeConstraints constraints; + constraints.SetMandatoryIceRestart(true); + cricket::MediaSessionOptions options; + EXPECT_TRUE(signaling_->GetOptionsForOffer(&constraints, &options)); + EXPECT_TRUE(options.transport_options.ice_restart); + + EXPECT_TRUE(signaling_->GetOptionsForOffer(NULL, &options)); + EXPECT_FALSE(options.transport_options.ice_restart); +} + +// Test that GetMediaSessionOptionsForOffer and GetOptionsForAnswer work as +// expected if unknown constraints are used. +TEST_F(MediaStreamSignalingTest, GetMediaSessionOptionsWithBadConstraints) { + FakeConstraints mandatory; + mandatory.AddMandatory("bad_key", "bad_value"); + cricket::MediaSessionOptions options; + EXPECT_FALSE(signaling_->GetOptionsForOffer(&mandatory, &options)); + EXPECT_FALSE(signaling_->GetOptionsForAnswer(&mandatory, &options)); + + FakeConstraints optional; + optional.AddOptional("bad_key", "bad_value"); + EXPECT_TRUE(signaling_->GetOptionsForOffer(&optional, &options)); + EXPECT_TRUE(signaling_->GetOptionsForAnswer(&optional, &options)); +} + +// Test that a correct MediaSessionOptions are created for an offer if +// a MediaStream is sent and later updated with a new track. +// MediaConstraints are not used. +TEST_F(MediaStreamSignalingTest, AddTrackToLocalMediaStream) { + talk_base::scoped_refptr local_streams( + CreateStreamCollection(1)); + MediaStreamInterface* local_stream = local_streams->at(0); + EXPECT_TRUE(signaling_->AddLocalStream(local_stream)); + cricket::MediaSessionOptions options; + EXPECT_TRUE(signaling_->GetOptionsForOffer(NULL, &options)); + VerifyMediaOptions(local_streams, options); + + cricket::MediaSessionOptions updated_options; + local_stream->AddTrack(AudioTrack::Create(kAudioTracks[1], NULL)); + EXPECT_TRUE(signaling_->GetOptionsForOffer(NULL, &options)); + VerifyMediaOptions(local_streams, options); +} + +// Test that the MediaConstraints in an answer don't affect if audio and video +// is offered in an offer but that if kOfferToReceiveAudio or +// kOfferToReceiveVideo constraints are true in an offer, the media type will be +// included in subsequent answers. +TEST_F(MediaStreamSignalingTest, MediaConstraintsInAnswer) { + FakeConstraints answer_c; + answer_c.SetMandatoryReceiveAudio(true); + answer_c.SetMandatoryReceiveVideo(true); + + cricket::MediaSessionOptions answer_options; + EXPECT_TRUE(signaling_->GetOptionsForAnswer(&answer_c, &answer_options)); + EXPECT_TRUE(answer_options.has_audio); + EXPECT_TRUE(answer_options.has_video); + + FakeConstraints offer_c; + offer_c.SetMandatoryReceiveAudio(false); + offer_c.SetMandatoryReceiveVideo(false); + + cricket::MediaSessionOptions offer_options; + EXPECT_TRUE(signaling_->GetOptionsForOffer(&offer_c, &offer_options)); + EXPECT_FALSE(offer_options.has_audio); + EXPECT_FALSE(offer_options.has_video); + + FakeConstraints updated_offer_c; + updated_offer_c.SetMandatoryReceiveAudio(true); + updated_offer_c.SetMandatoryReceiveVideo(true); + + cricket::MediaSessionOptions updated_offer_options; + EXPECT_TRUE(signaling_->GetOptionsForOffer(&updated_offer_c, + &updated_offer_options)); + EXPECT_TRUE(updated_offer_options.has_audio); + EXPECT_TRUE(updated_offer_options.has_video); + + // Since an offer has been created with both audio and video, subsequent + // offers and answers should contain both audio and video. + // Answers will only contain the media types that exist in the offer + // regardless of the value of |updated_answer_options.has_audio| and + // |updated_answer_options.has_video|. + FakeConstraints updated_answer_c; + answer_c.SetMandatoryReceiveAudio(false); + answer_c.SetMandatoryReceiveVideo(false); + + cricket::MediaSessionOptions updated_answer_options; + EXPECT_TRUE(signaling_->GetOptionsForAnswer(&updated_answer_c, + &updated_answer_options)); + EXPECT_TRUE(updated_answer_options.has_audio); + EXPECT_TRUE(updated_answer_options.has_video); + + EXPECT_TRUE(signaling_->GetOptionsForOffer(NULL, + &updated_offer_options)); + EXPECT_TRUE(updated_offer_options.has_audio); + EXPECT_TRUE(updated_offer_options.has_video); +} + +// This test verifies that the remote MediaStreams corresponding to a received +// SDP string is created. In this test the two separate MediaStreams are +// signaled. +TEST_F(MediaStreamSignalingTest, UpdateRemoteStreams) { + talk_base::scoped_ptr desc( + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + kSdpStringWithStream1, NULL)); + EXPECT_TRUE(desc != NULL); + signaling_->OnRemoteDescriptionChanged(desc.get()); + + talk_base::scoped_refptr reference( + CreateStreamCollection(1)); + EXPECT_TRUE(CompareStreamCollections(signaling_->remote_streams(), + reference.get())); + EXPECT_TRUE(CompareStreamCollections(observer_->remote_streams(), + reference.get())); + EXPECT_EQ(1u, observer_->NumberOfRemoteAudioTracks()); + observer_->VerifyRemoteAudioTrack(kStreams[0], kAudioTracks[0], 1); + EXPECT_EQ(1u, observer_->NumberOfRemoteVideoTracks()); + observer_->VerifyRemoteVideoTrack(kStreams[0], kVideoTracks[0], 2); + + // Create a session description based on another SDP with another + // MediaStream. + talk_base::scoped_ptr update_desc( + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + kSdpStringWith2Stream, NULL)); + EXPECT_TRUE(update_desc != NULL); + signaling_->OnRemoteDescriptionChanged(update_desc.get()); + + talk_base::scoped_refptr reference2( + CreateStreamCollection(2)); + EXPECT_TRUE(CompareStreamCollections(signaling_->remote_streams(), + reference2.get())); + EXPECT_TRUE(CompareStreamCollections(observer_->remote_streams(), + reference2.get())); + + EXPECT_EQ(2u, observer_->NumberOfRemoteAudioTracks()); + observer_->VerifyRemoteAudioTrack(kStreams[0], kAudioTracks[0], 1); + observer_->VerifyRemoteAudioTrack(kStreams[1], kAudioTracks[1], 3); + EXPECT_EQ(2u, observer_->NumberOfRemoteVideoTracks()); + observer_->VerifyRemoteVideoTrack(kStreams[0], kVideoTracks[0], 2); + observer_->VerifyRemoteVideoTrack(kStreams[1], kVideoTracks[1], 4); +} + +// This test verifies that the remote MediaStreams corresponding to a received +// SDP string is created. In this test the same remote MediaStream is signaled +// but MediaStream tracks are added and removed. +TEST_F(MediaStreamSignalingTest, AddRemoveTrackFromExistingRemoteMediaStream) { + talk_base::scoped_ptr desc_ms1; + CreateSessionDescriptionAndReference(1, 1, desc_ms1.use()); + signaling_->OnRemoteDescriptionChanged(desc_ms1.get()); + EXPECT_TRUE(CompareStreamCollections(signaling_->remote_streams(), + reference_collection_)); + + // Add extra audio and video tracks to the same MediaStream. + talk_base::scoped_ptr desc_ms1_two_tracks; + CreateSessionDescriptionAndReference(2, 2, desc_ms1_two_tracks.use()); + signaling_->OnRemoteDescriptionChanged(desc_ms1_two_tracks.get()); + EXPECT_TRUE(CompareStreamCollections(signaling_->remote_streams(), + reference_collection_)); + EXPECT_TRUE(CompareStreamCollections(observer_->remote_streams(), + reference_collection_)); + + // Remove the extra audio and video tracks again. + CreateSessionDescriptionAndReference(1, 1, desc_ms1.use()); + signaling_->OnRemoteDescriptionChanged(desc_ms1.get()); + EXPECT_TRUE(CompareStreamCollections(signaling_->remote_streams(), + reference_collection_)); + EXPECT_TRUE(CompareStreamCollections(observer_->remote_streams(), + reference_collection_)); +} + +// This test that remote tracks are ended if a +// local session description is set that rejects the media content type. +TEST_F(MediaStreamSignalingTest, RejectMediaContent) { + talk_base::scoped_ptr desc( + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + kSdpStringWithStream1, NULL)); + EXPECT_TRUE(desc != NULL); + signaling_->OnRemoteDescriptionChanged(desc.get()); + + ASSERT_EQ(1u, observer_->remote_streams()->count()); + MediaStreamInterface* remote_stream = observer_->remote_streams()->at(0); + ASSERT_EQ(1u, remote_stream->GetVideoTracks().size()); + ASSERT_EQ(1u, remote_stream->GetAudioTracks().size()); + + talk_base::scoped_refptr remote_video = + remote_stream->GetVideoTracks()[0]; + EXPECT_EQ(webrtc::MediaStreamTrackInterface::kLive, remote_video->state()); + talk_base::scoped_refptr remote_audio = + remote_stream->GetAudioTracks()[0]; + EXPECT_EQ(webrtc::MediaStreamTrackInterface::kLive, remote_audio->state()); + + cricket::ContentInfo* video_info = + desc->description()->GetContentByName("video"); + ASSERT_TRUE(video_info != NULL); + video_info->rejected = true; + signaling_->OnLocalDescriptionChanged(desc.get()); + EXPECT_EQ(webrtc::MediaStreamTrackInterface::kEnded, remote_video->state()); + EXPECT_EQ(webrtc::MediaStreamTrackInterface::kLive, remote_audio->state()); + + cricket::ContentInfo* audio_info = + desc->description()->GetContentByName("audio"); + ASSERT_TRUE(audio_info != NULL); + audio_info->rejected = true; + signaling_->OnLocalDescriptionChanged(desc.get()); + EXPECT_EQ(webrtc::MediaStreamTrackInterface::kEnded, remote_audio->state()); +} + +// This tests that a default MediaStream is created if a remote session +// description doesn't contain any streams and no MSID support. +// It also tests that the default stream is updated if a video m-line is added +// in a subsequent session description. +TEST_F(MediaStreamSignalingTest, SdpWithoutMsidCreatesDefaultStream) { + talk_base::scoped_ptr desc_audio_only( + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + kSdpStringWithoutStreamsAudioOnly, + NULL)); + ASSERT_TRUE(desc_audio_only != NULL); + signaling_->OnRemoteDescriptionChanged(desc_audio_only.get()); + + EXPECT_EQ(1u, signaling_->remote_streams()->count()); + ASSERT_EQ(1u, observer_->remote_streams()->count()); + MediaStreamInterface* remote_stream = observer_->remote_streams()->at(0); + + EXPECT_EQ(1u, remote_stream->GetAudioTracks().size()); + EXPECT_EQ(0u, remote_stream->GetVideoTracks().size()); + EXPECT_EQ("default", remote_stream->label()); + + talk_base::scoped_ptr desc( + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + kSdpStringWithoutStreams, NULL)); + ASSERT_TRUE(desc != NULL); + signaling_->OnRemoteDescriptionChanged(desc.get()); + EXPECT_EQ(1u, signaling_->remote_streams()->count()); + ASSERT_EQ(1u, remote_stream->GetAudioTracks().size()); + EXPECT_EQ("defaulta0", remote_stream->GetAudioTracks()[0]->id()); + ASSERT_EQ(1u, remote_stream->GetVideoTracks().size()); + EXPECT_EQ("defaultv0", remote_stream->GetVideoTracks()[0]->id()); + observer_->VerifyRemoteAudioTrack("default", "defaulta0", 0); + observer_->VerifyRemoteVideoTrack("default", "defaultv0", 0); +} + +// This tests that a default MediaStream is created if the remote session +// description doesn't contain any streams and don't contain an indication if +// MSID is supported. +TEST_F(MediaStreamSignalingTest, + SdpWithoutMsidAndStreamsCreatesDefaultStream) { + talk_base::scoped_ptr desc( + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + kSdpStringWithoutStreams, + NULL)); + ASSERT_TRUE(desc != NULL); + signaling_->OnRemoteDescriptionChanged(desc.get()); + + ASSERT_EQ(1u, observer_->remote_streams()->count()); + MediaStreamInterface* remote_stream = observer_->remote_streams()->at(0); + EXPECT_EQ(1u, remote_stream->GetAudioTracks().size()); + EXPECT_EQ(1u, remote_stream->GetVideoTracks().size()); +} + +// This tests that a default MediaStream is not created if the remote session +// description doesn't contain any streams but does support MSID. +TEST_F(MediaStreamSignalingTest, SdpWitMsidDontCreatesDefaultStream) { + talk_base::scoped_ptr desc_msid_without_streams( + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + kSdpStringWithMsidWithoutStreams, + NULL)); + signaling_->OnRemoteDescriptionChanged(desc_msid_without_streams.get()); + EXPECT_EQ(0u, observer_->remote_streams()->count()); +} + +// This test that a default MediaStream is not created if a remote session +// description is updated to not have any MediaStreams. +TEST_F(MediaStreamSignalingTest, VerifyDefaultStreamIsNotCreated) { + talk_base::scoped_ptr desc( + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + kSdpStringWithStream1, + NULL)); + ASSERT_TRUE(desc != NULL); + signaling_->OnRemoteDescriptionChanged(desc.get()); + talk_base::scoped_refptr reference( + CreateStreamCollection(1)); + EXPECT_TRUE(CompareStreamCollections(observer_->remote_streams(), + reference.get())); + + talk_base::scoped_ptr desc_without_streams( + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + kSdpStringWithoutStreams, + NULL)); + signaling_->OnRemoteDescriptionChanged(desc_without_streams.get()); + EXPECT_EQ(0u, observer_->remote_streams()->count()); +} + +// This test that the correct MediaStreamSignalingObserver methods are called +// when MediaStreamSignaling::OnLocalDescriptionChanged is called with an +// updated local session description. +TEST_F(MediaStreamSignalingTest, LocalDescriptionChanged) { + talk_base::scoped_ptr desc_1; + CreateSessionDescriptionAndReference(2, 2, desc_1.use()); + + signaling_->AddLocalStream(reference_collection_->at(0)); + signaling_->OnLocalDescriptionChanged(desc_1.get()); + EXPECT_EQ(2u, observer_->NumberOfLocalAudioTracks()); + EXPECT_EQ(2u, observer_->NumberOfLocalVideoTracks()); + observer_->VerifyLocalAudioTrack(kStreams[0], kAudioTracks[0], 1); + observer_->VerifyLocalVideoTrack(kStreams[0], kVideoTracks[0], 2); + observer_->VerifyLocalAudioTrack(kStreams[0], kAudioTracks[1], 3); + observer_->VerifyLocalVideoTrack(kStreams[0], kVideoTracks[1], 4); + + // Remove an audio and video track. + talk_base::scoped_ptr desc_2; + CreateSessionDescriptionAndReference(1, 1, desc_2.use()); + signaling_->OnLocalDescriptionChanged(desc_2.get()); + EXPECT_EQ(1u, observer_->NumberOfLocalAudioTracks()); + EXPECT_EQ(1u, observer_->NumberOfLocalVideoTracks()); + observer_->VerifyLocalAudioTrack(kStreams[0], kAudioTracks[0], 1); + observer_->VerifyLocalVideoTrack(kStreams[0], kVideoTracks[0], 2); +} + +// This test that the correct MediaStreamSignalingObserver methods are called +// when MediaStreamSignaling::AddLocalStream is called after +// MediaStreamSignaling::OnLocalDescriptionChanged is called. +TEST_F(MediaStreamSignalingTest, AddLocalStreamAfterLocalDescriptionChanged) { + talk_base::scoped_ptr desc_1; + CreateSessionDescriptionAndReference(2, 2, desc_1.use()); + + signaling_->OnLocalDescriptionChanged(desc_1.get()); + EXPECT_EQ(0u, observer_->NumberOfLocalAudioTracks()); + EXPECT_EQ(0u, observer_->NumberOfLocalVideoTracks()); + + signaling_->AddLocalStream(reference_collection_->at(0)); + EXPECT_EQ(2u, observer_->NumberOfLocalAudioTracks()); + EXPECT_EQ(2u, observer_->NumberOfLocalVideoTracks()); + observer_->VerifyLocalAudioTrack(kStreams[0], kAudioTracks[0], 1); + observer_->VerifyLocalVideoTrack(kStreams[0], kVideoTracks[0], 2); + observer_->VerifyLocalAudioTrack(kStreams[0], kAudioTracks[1], 3); + observer_->VerifyLocalVideoTrack(kStreams[0], kVideoTracks[1], 4); +} + +// This test that the correct MediaStreamSignalingObserver methods are called +// if the ssrc on a local track is changed when +// MediaStreamSignaling::OnLocalDescriptionChanged is called. +TEST_F(MediaStreamSignalingTest, ChangeSsrcOnTrackInLocalSessionDescription) { + talk_base::scoped_ptr desc; + CreateSessionDescriptionAndReference(1, 1, desc.use()); + + signaling_->AddLocalStream(reference_collection_->at(0)); + signaling_->OnLocalDescriptionChanged(desc.get()); + EXPECT_EQ(1u, observer_->NumberOfLocalAudioTracks()); + EXPECT_EQ(1u, observer_->NumberOfLocalVideoTracks()); + observer_->VerifyLocalAudioTrack(kStreams[0], kAudioTracks[0], 1); + observer_->VerifyLocalVideoTrack(kStreams[0], kVideoTracks[0], 2); + + // Change the ssrc of the audio and video track. + std::string sdp; + desc->ToString(&sdp); + std::string ssrc_org = "a=ssrc:1"; + std::string ssrc_to = "a=ssrc:97"; + talk_base::replace_substrs(ssrc_org.c_str(), ssrc_org.length(), + ssrc_to.c_str(), ssrc_to.length(), + &sdp); + ssrc_org = "a=ssrc:2"; + ssrc_to = "a=ssrc:98"; + talk_base::replace_substrs(ssrc_org.c_str(), ssrc_org.length(), + ssrc_to.c_str(), ssrc_to.length(), + &sdp); + talk_base::scoped_ptr updated_desc( + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + sdp, NULL)); + + signaling_->OnLocalDescriptionChanged(updated_desc.get()); + EXPECT_EQ(1u, observer_->NumberOfLocalAudioTracks()); + EXPECT_EQ(1u, observer_->NumberOfLocalVideoTracks()); + observer_->VerifyLocalAudioTrack(kStreams[0], kAudioTracks[0], 97); + observer_->VerifyLocalVideoTrack(kStreams[0], kVideoTracks[0], 98); +} + + diff --git a/talk/app/webrtc/mediastreamtrack.h b/talk/app/webrtc/mediastreamtrack.h new file mode 100644 index 000000000..6055e51d8 --- /dev/null +++ b/talk/app/webrtc/mediastreamtrack.h @@ -0,0 +1,81 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_APP_WEBRTC_MEDIASTREAMTRACK_H_ +#define TALK_APP_WEBRTC_MEDIASTREAMTRACK_H_ + +#include + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/notifier.h" + +namespace webrtc { + +// MediaTrack implements the interface common to AudioTrackInterface and +// VideoTrackInterface. +template +class MediaStreamTrack : public Notifier { + public: + typedef typename T::TrackState TypedTrackState; + + virtual std::string id() const { return id_; } + virtual MediaStreamTrackInterface::TrackState state() const { + return state_; + } + virtual bool enabled() const { return enabled_; } + virtual bool set_enabled(bool enable) { + bool fire_on_change = (enable != enabled_); + enabled_ = enable; + if (fire_on_change) { + Notifier::FireOnChanged(); + } + return fire_on_change; + } + virtual bool set_state(MediaStreamTrackInterface::TrackState new_state) { + bool fire_on_change = (state_ != new_state); + state_ = new_state; + if (fire_on_change) + Notifier::FireOnChanged(); + return true; + } + + protected: + explicit MediaStreamTrack(const std::string& id) + : enabled_(true), + id_(id), + state_(MediaStreamTrackInterface::kInitializing) { + } + + private: + bool enabled_; + std::string id_; + MediaStreamTrackInterface::TrackState state_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_MEDIASTREAMTRACK_H_ diff --git a/talk/app/webrtc/mediastreamtrackproxy.h b/talk/app/webrtc/mediastreamtrackproxy.h new file mode 100644 index 000000000..954874bf3 --- /dev/null +++ b/talk/app/webrtc/mediastreamtrackproxy.h @@ -0,0 +1,73 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +// This file includes proxy classes for tracks. The purpose is +// to make sure tracks are only accessed from the signaling thread. + +#ifndef TALK_APP_WEBRTC_MEDIASTREAMTRACKPROXY_H_ +#define TALK_APP_WEBRTC_MEDIASTREAMTRACKPROXY_H_ + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/proxy.h" + +namespace webrtc { + +BEGIN_PROXY_MAP(AudioTrack) + PROXY_CONSTMETHOD0(std::string, kind) + PROXY_CONSTMETHOD0(std::string, id) + PROXY_CONSTMETHOD0(TrackState, state) + PROXY_CONSTMETHOD0(bool, enabled) + PROXY_CONSTMETHOD0(AudioSourceInterface*, GetSource) + PROXY_METHOD0(cricket::AudioRenderer*, FrameInput) + + PROXY_METHOD1(bool, set_enabled, bool) + PROXY_METHOD1(bool, set_state, TrackState) + + PROXY_METHOD1(void, RegisterObserver, ObserverInterface*) + PROXY_METHOD1(void, UnregisterObserver, ObserverInterface*) +END_PROXY() + +BEGIN_PROXY_MAP(VideoTrack) + PROXY_CONSTMETHOD0(std::string, kind) + PROXY_CONSTMETHOD0(std::string, id) + PROXY_CONSTMETHOD0(TrackState, state) + PROXY_CONSTMETHOD0(bool, enabled) + PROXY_METHOD1(bool, set_enabled, bool) + PROXY_METHOD1(bool, set_state, TrackState) + + PROXY_METHOD1(void, AddRenderer, VideoRendererInterface*) + PROXY_METHOD1(void, RemoveRenderer, VideoRendererInterface*) + PROXY_METHOD0(cricket::VideoRenderer*, FrameInput) + PROXY_CONSTMETHOD0(VideoSourceInterface*, GetSource) + + PROXY_METHOD1(void, RegisterObserver, ObserverInterface*) + PROXY_METHOD1(void, UnregisterObserver, ObserverInterface*) +END_PROXY() + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_MEDIASTREAMTRACKPROXY_H_ diff --git a/talk/app/webrtc/notifier.h b/talk/app/webrtc/notifier.h new file mode 100644 index 000000000..eaa006387 --- /dev/null +++ b/talk/app/webrtc/notifier.h @@ -0,0 +1,77 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_APP_WEBRTC_NOTIFIER_H_ +#define TALK_APP_WEBRTC_NOTIFIER_H_ + +#include + +#include "talk/base/common.h" +#include "talk/app/webrtc/mediastreaminterface.h" + +namespace webrtc { + +// Implement a template version of a notifier. +template +class Notifier : public T { + public: + Notifier() { + } + + virtual void RegisterObserver(ObserverInterface* observer) { + ASSERT(observer != NULL); + observers_.push_back(observer); + } + + virtual void UnregisterObserver(ObserverInterface* observer) { + for (std::list::iterator it = observers_.begin(); + it != observers_.end(); it++) { + if (*it == observer) { + observers_.erase(it); + break; + } + } + } + + void FireOnChanged() { + // Copy the list of observers to avoid a crash if the observer object + // unregisters as a result of the OnChanged() call. If the same list is used + // UnregisterObserver will affect the list make the iterator invalid. + std::list observers = observers_; + for (std::list::iterator it = observers.begin(); + it != observers.end(); ++it) { + (*it)->OnChanged(); + } + } + + protected: + std::list observers_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_NOTIFIER_H_ diff --git a/talk/app/webrtc/objc/README b/talk/app/webrtc/objc/README new file mode 100644 index 000000000..cea2aae6a --- /dev/null +++ b/talk/app/webrtc/objc/README @@ -0,0 +1,45 @@ +This directory contains the ObjectiveC implementation of the +webrtc::PeerConnection API. This can be built for Mac or iOS. + +Prerequisites: +- Make sure gclient is checking out tools necessary to target iOS: your + .gclient file should contain a line like: + target_os = ['ios', 'mac'] + Make sure to re-run gclient sync after adding this to download the tools. +- Set up webrtc-related GYP variables: +- For Mac: + export GYP_DEFINES="build_with_libjingle=1 build_with_chromium=0 OS=mac + target_arch=x64 libjingle_objc=1 libpeer_target_type=static_library + $GYP_DEFINES" +- For iOS: + export GYP_DEFINES="build_with_libjingle=1 build_with_chromium=0 OS=ios + libjingle_enable_video=0 libjingle_objc=1 enable_video=0 $GYP_DEFINES" +- Finally, run "gclient runhooks" to generate iOS or Mac targeting Xcode + projects. + +Example of building & using the app: + +cd /trunk/talk +- Open libjingle.xcproj. Select iPhone or iPad simulator and build everything. + Then switch to iOS device and build everything. This creates x86 and ARM + archives. +cd examples/ios +./makeLibs.sh +- This will generate fat archives containing both targets and copy them to + ./libs. +- This step must be rerun every time you run gclient sync or build the API + libraries. +- Open AppRTCDemo.xcodeproj, select your device or simulator and run. +- If you have any problems deploying for the first time, check the project + properties to ensure that the Bundle Identifier matches your phone + provisioning profile. Or use the simulator as it doesn't require a profile. + +- In desktop chrome, navigate to http://apprtc.appspot.com and note the r= + room number in the resulting URL. + +- Enter that number into the text field on the phone. + +- Alternatively, you can background the app and launch Safari. In Safari, open + the url apprtc://apprtc.appspot.com/?r= where is the room name. + Other options are to put the link in an email and send it to your self. + Clicking on it will launch AppRTCDemo and navigate to the room. diff --git a/talk/app/webrtc/objc/RTCAudioTrack+Internal.h b/talk/app/webrtc/objc/RTCAudioTrack+Internal.h new file mode 100644 index 000000000..17d272324 --- /dev/null +++ b/talk/app/webrtc/objc/RTCAudioTrack+Internal.h @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCAudioTrack.h" + +#include "talk/app/webrtc/mediastreaminterface.h" + +@interface RTCAudioTrack (Internal) + +@property(nonatomic, assign, readonly) + talk_base::scoped_refptr audioTrack; + +@end diff --git a/talk/app/webrtc/objc/RTCAudioTrack.mm b/talk/app/webrtc/objc/RTCAudioTrack.mm new file mode 100644 index 000000000..8a5698653 --- /dev/null +++ b/talk/app/webrtc/objc/RTCAudioTrack.mm @@ -0,0 +1,45 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCAudioTrack+internal.h" + +#import "RTCMediaStreamTrack+internal.h" + +@implementation RTCAudioTrack +@end + +@implementation RTCAudioTrack (Internal) + +- (talk_base::scoped_refptr)audioTrack { + return static_cast(self.mediaTrack.get()); +} + +@end diff --git a/talk/app/webrtc/objc/RTCEnumConverter.h b/talk/app/webrtc/objc/RTCEnumConverter.h new file mode 100644 index 000000000..0e83719d5 --- /dev/null +++ b/talk/app/webrtc/objc/RTCEnumConverter.h @@ -0,0 +1,54 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +#import "RTCTypes.h" + +#include "talk/app/webrtc/peerconnectioninterface.h" + +@interface RTCEnumConverter : NSObject + ++ (RTCICEConnectionState)convertIceConnectionStateToObjC: + (webrtc::PeerConnectionInterface::IceConnectionState)nativeState; + ++ (RTCICEGatheringState)convertIceGatheringStateToObjC: + (webrtc::PeerConnectionInterface::IceGatheringState)nativeState; + ++ (RTCSignalingState)convertSignalingStateToObjC: + (webrtc::PeerConnectionInterface::SignalingState)nativeState; + ++ (RTCSourceState)convertSourceStateToObjC: + (webrtc::MediaSourceInterface::SourceState)nativeState; + ++ (webrtc::MediaStreamTrackInterface::TrackState)convertTrackStateToNative: + (RTCTrackState)state; + ++ (RTCTrackState)convertTrackStateToObjC: + (webrtc::MediaStreamTrackInterface::TrackState)nativeState; + +@end diff --git a/talk/app/webrtc/objc/RTCEnumConverter.mm b/talk/app/webrtc/objc/RTCEnumConverter.mm new file mode 100644 index 000000000..7c81c8d85 --- /dev/null +++ b/talk/app/webrtc/objc/RTCEnumConverter.mm @@ -0,0 +1,126 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCEnumConverter.h" + +#include "talk/app/webrtc/peerconnectioninterface.h" + +@implementation RTCEnumConverter + ++ (RTCICEConnectionState)convertIceConnectionStateToObjC: + (webrtc::PeerConnectionInterface::IceConnectionState)nativeState { + switch (nativeState) { + case webrtc::PeerConnectionInterface::kIceConnectionNew: + return RTCICEConnectionNew; + case webrtc::PeerConnectionInterface::kIceConnectionChecking: + return RTCICEConnectionChecking; + case webrtc::PeerConnectionInterface::kIceConnectionConnected: + return RTCICEConnectionConnected; + case webrtc::PeerConnectionInterface::kIceConnectionCompleted: + return RTCICEConnectionCompleted; + case webrtc::PeerConnectionInterface::kIceConnectionFailed: + return RTCICEConnectionFailed; + case webrtc::PeerConnectionInterface::kIceConnectionDisconnected: + return RTCICEConnectionDisconnected; + case webrtc::PeerConnectionInterface::kIceConnectionClosed: + return RTCICEConnectionClosed; + } +} + ++ (RTCICEGatheringState)convertIceGatheringStateToObjC: + (webrtc::PeerConnectionInterface::IceGatheringState)nativeState { + switch (nativeState) { + case webrtc::PeerConnectionInterface::kIceGatheringNew: + return RTCICEGatheringNew; + case webrtc::PeerConnectionInterface::kIceGatheringGathering: + return RTCICEGatheringGathering; + case webrtc::PeerConnectionInterface::kIceGatheringComplete: + return RTCICEGatheringComplete; + } +} + ++ (RTCSignalingState)convertSignalingStateToObjC: + (webrtc::PeerConnectionInterface::SignalingState)nativeState { + switch (nativeState) { + case webrtc::PeerConnectionInterface::kStable: + return RTCSignalingStable; + case webrtc::PeerConnectionInterface::kHaveLocalOffer: + return RTCSignalingHaveLocalOffer; + case webrtc::PeerConnectionInterface::kHaveLocalPrAnswer: + return RTCSignalingHaveLocalPrAnswer; + case webrtc::PeerConnectionInterface::kHaveRemoteOffer: + return RTCSignalingHaveRemoteOffer; + case webrtc::PeerConnectionInterface::kHaveRemotePrAnswer: + return RTCSignalingHaveRemotePrAnswer; + case webrtc::PeerConnectionInterface::kClosed: + return RTCSignalingClosed; + } +} + ++ (RTCSourceState)convertSourceStateToObjC: + (webrtc::MediaSourceInterface::SourceState)nativeState { + switch (nativeState) { + case webrtc::MediaSourceInterface::kInitializing: + return RTCSourceStateInitializing; + case webrtc::MediaSourceInterface::kLive: + return RTCSourceStateLive; + case webrtc::MediaSourceInterface::kEnded: + return RTCSourceStateEnded; + case webrtc::MediaSourceInterface::kMuted: + return RTCSourceStateMuted; + } +} + ++ (webrtc::MediaStreamTrackInterface::TrackState) + convertTrackStateToNative:(RTCTrackState)state { + switch (state) { + case RTCTrackStateInitializing: + return webrtc::MediaStreamTrackInterface::kInitializing; + case RTCTrackStateLive: + return webrtc::MediaStreamTrackInterface::kLive; + case RTCTrackStateEnded: + return webrtc::MediaStreamTrackInterface::kEnded; + case RTCTrackStateFailed: + return webrtc::MediaStreamTrackInterface::kFailed; + } +} + ++ (RTCTrackState)convertTrackStateToObjC: + (webrtc::MediaStreamTrackInterface::TrackState)nativeState { + switch (nativeState) { + case webrtc::MediaStreamTrackInterface::kInitializing: + return RTCTrackStateInitializing; + case webrtc::MediaStreamTrackInterface::kLive: + return RTCTrackStateLive; + case webrtc::MediaStreamTrackInterface::kEnded: + return RTCTrackStateEnded; + case webrtc::MediaStreamTrackInterface::kFailed: + return RTCTrackStateFailed; + } +} + +@end diff --git a/talk/app/webrtc/objc/RTCI420Frame.mm b/talk/app/webrtc/objc/RTCI420Frame.mm new file mode 100644 index 000000000..df84fc15e --- /dev/null +++ b/talk/app/webrtc/objc/RTCI420Frame.mm @@ -0,0 +1,34 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCI420Frame.h" + +@implementation RTCI420Frame + +// TODO(hughv): Should this just be a cricket::VideoFrame wrapper object? + +@end diff --git a/talk/app/webrtc/objc/RTCIceCandidate+Internal.h b/talk/app/webrtc/objc/RTCIceCandidate+Internal.h new file mode 100644 index 000000000..e4964d4d1 --- /dev/null +++ b/talk/app/webrtc/objc/RTCIceCandidate+Internal.h @@ -0,0 +1,39 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCICECandidate.h" + +#include "talk/app/webrtc/peerconnectioninterface.h" + +@interface RTCICECandidate (Internal) + +@property(nonatomic, assign, readonly) const + webrtc::IceCandidateInterface *candidate; + +- (id)initWithCandidate:(const webrtc::IceCandidateInterface *)candidate; + +@end diff --git a/talk/app/webrtc/objc/RTCIceCandidate.mm b/talk/app/webrtc/objc/RTCIceCandidate.mm new file mode 100644 index 000000000..63eac1dc0 --- /dev/null +++ b/talk/app/webrtc/objc/RTCIceCandidate.mm @@ -0,0 +1,86 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCICECandidate+internal.h" + +@implementation RTCICECandidate { + NSString *_sdpMid; + NSInteger _sdpMLineIndex; + NSString *_sdp; +} + +- (id)initWithMid:(NSString *)sdpMid + index:(NSInteger)sdpMLineIndex + sdp:(NSString *)sdp { + if (!sdpMid || !sdp) { + NSAssert(NO, @"nil arguments not allowed"); + return nil; + } + if ((self = [super init])) { + _sdpMid = [sdpMid copy]; + _sdpMLineIndex = sdpMLineIndex; + _sdp = [sdp copy]; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@:%ld:%@", + self.sdpMid, + (long)self.sdpMLineIndex, + self.sdp]; +} + +@end + +@implementation RTCICECandidate (Internal) + +- (id)initWithCandidate:(const webrtc::IceCandidateInterface *)candidate { + if ((self = [super init])) { + std::string sdp; + if (candidate->ToString(&sdp)) { + _sdpMid = @(candidate->sdp_mid().c_str()); + _sdpMLineIndex = candidate->sdp_mline_index(); + _sdp = @(sdp.c_str()); + } else { + self = nil; + NSAssert(NO, @"ICECandidateInterface->ToString failed"); + } + } + return self; +} + +- (const webrtc::IceCandidateInterface *)candidate { + return webrtc::CreateIceCandidate( + [self.sdpMid UTF8String], self.sdpMLineIndex, [self.sdp UTF8String]); +} + +@end diff --git a/talk/app/webrtc/objc/RTCIceServer+Internal.h b/talk/app/webrtc/objc/RTCIceServer+Internal.h new file mode 100644 index 000000000..c074294b4 --- /dev/null +++ b/talk/app/webrtc/objc/RTCIceServer+Internal.h @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCICEServer.h" + +#include "talk/app/webrtc/peerconnectioninterface.h" + +@interface RTCICEServer (Internal) + +@property(nonatomic, assign, readonly) + webrtc::PeerConnectionInterface::IceServer iceServer; + +@end diff --git a/talk/app/webrtc/objc/RTCIceServer.mm b/talk/app/webrtc/objc/RTCIceServer.mm new file mode 100644 index 000000000..cb32673aa --- /dev/null +++ b/talk/app/webrtc/objc/RTCIceServer.mm @@ -0,0 +1,65 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCICEServer+internal.h" + +@implementation RTCICEServer + +- (id)initWithURI:(NSURL *)URI password:(NSString *)password { + if (!URI || !password) { + NSAssert(NO, @"nil arguments not allowed"); + self = nil; + return nil; + } + if ((self = [super init])) { + _URI = URI; + _password = [password copy]; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"Server: [%@]\nPassword: [%@]", + [self.URI absoluteString], self.password]; +} + +@end + +@implementation RTCICEServer (Internal) + +- (webrtc::PeerConnectionInterface::IceServer)iceServer { + webrtc::PeerConnectionInterface::IceServer iceServer; + iceServer.uri = [[self.URI absoluteString] UTF8String]; + iceServer.password = [self.password UTF8String]; + return iceServer; +} + +@end diff --git a/talk/app/webrtc/objc/RTCMediaConstraints+Internal.h b/talk/app/webrtc/objc/RTCMediaConstraints+Internal.h new file mode 100644 index 000000000..71a10c7b4 --- /dev/null +++ b/talk/app/webrtc/objc/RTCMediaConstraints+Internal.h @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCMediaConstraints.h" + +#import "RTCMediaConstraintsNative.h" + +#include "talk/app/webrtc/mediastreaminterface.h" + +@interface RTCMediaConstraints (Internal) + +// Ownership is retained for the lifetime of this object. +@property(nonatomic, assign, readonly) const + webrtc::RTCMediaConstraintsNative *constraints; + +@end diff --git a/talk/app/webrtc/objc/RTCMediaConstraints.mm b/talk/app/webrtc/objc/RTCMediaConstraints.mm new file mode 100644 index 000000000..fcb3b52dc --- /dev/null +++ b/talk/app/webrtc/objc/RTCMediaConstraints.mm @@ -0,0 +1,76 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCMediaConstraints+internal.h" + +#import "RTCPair.h" + +#include "talk/base/scoped_ptr.h" + +// TODO(hughv): Add accessors for mandatory and optional constraints. +// TODO(hughv): Add description. + +@implementation RTCMediaConstraints { + talk_base::scoped_ptr _constraints; + webrtc::MediaConstraintsInterface::Constraints _mandatory; + webrtc::MediaConstraintsInterface::Constraints _optional; +} + +- (id)initWithMandatoryConstraints:(NSArray *)mandatory + optionalConstraints:(NSArray *)optional { + if ((self = [super init])) { + _mandatory = [[self class] constraintsFromArray:mandatory]; + _optional = [[self class] constraintsFromArray:optional]; + _constraints.reset( + new webrtc::RTCMediaConstraintsNative(_mandatory, _optional)); + } + return self; +} + ++ (webrtc::MediaConstraintsInterface::Constraints) + constraintsFromArray:(NSArray *)array { + webrtc::MediaConstraintsInterface::Constraints constraints; + for (RTCPair *pair in array) { + constraints.push_back(webrtc::MediaConstraintsInterface::Constraint( + [pair.key UTF8String], [pair.value UTF8String])); + } + return constraints; +} + +@end + +@implementation RTCMediaConstraints (internal) + +- (const webrtc::RTCMediaConstraintsNative *)constraints { + return _constraints.get(); +} + +@end diff --git a/talk/app/webrtc/objc/RTCMediaConstraintsNative.cc b/talk/app/webrtc/objc/RTCMediaConstraintsNative.cc new file mode 100644 index 000000000..ed06d1850 --- /dev/null +++ b/talk/app/webrtc/objc/RTCMediaConstraintsNative.cc @@ -0,0 +1,51 @@ +/* + * libjingle + * Copyright 2013, 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/app/webrtc/objc/RTCMediaConstraintsNative.h" + +namespace webrtc { + +RTCMediaConstraintsNative::~RTCMediaConstraintsNative() {} + +RTCMediaConstraintsNative::RTCMediaConstraintsNative() {} + +RTCMediaConstraintsNative::RTCMediaConstraintsNative( + const MediaConstraintsInterface::Constraints& mandatory, + const MediaConstraintsInterface::Constraints& optional) + : mandatory_(mandatory), optional_(optional) {} + +const MediaConstraintsInterface::Constraints& +RTCMediaConstraintsNative::GetMandatory() const { + return mandatory_; +} + +const MediaConstraintsInterface::Constraints& +RTCMediaConstraintsNative::GetOptional() const { + return optional_; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/objc/RTCMediaConstraintsNative.h b/talk/app/webrtc/objc/RTCMediaConstraintsNative.h new file mode 100644 index 000000000..a5cd26624 --- /dev/null +++ b/talk/app/webrtc/objc/RTCMediaConstraintsNative.h @@ -0,0 +1,50 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#ifndef TALK_APP_WEBRTC_OBJC_RTCMEDIACONSTRAINTSNATIVE_H_ +#define TALK_APP_WEBRTC_OBJC_RTCMEDIACONSTRAINTSNATIVE_H_ + +#include "talk/app/webrtc/mediaconstraintsinterface.h" + +namespace webrtc { +class RTCMediaConstraintsNative : public MediaConstraintsInterface { + public: + virtual ~RTCMediaConstraintsNative(); + RTCMediaConstraintsNative(); + RTCMediaConstraintsNative( + const MediaConstraintsInterface::Constraints& mandatory, + const MediaConstraintsInterface::Constraints& optional); + virtual const Constraints& GetMandatory() const; + virtual const Constraints& GetOptional() const; + + private: + MediaConstraintsInterface::Constraints mandatory_; + MediaConstraintsInterface::Constraints optional_; +}; +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_OBJC_RTCMEDIACONSTRAINTSNATIVE_H_ diff --git a/talk/app/webrtc/objc/RTCMediaSource+Internal.h b/talk/app/webrtc/objc/RTCMediaSource+Internal.h new file mode 100644 index 000000000..98f8e9cc2 --- /dev/null +++ b/talk/app/webrtc/objc/RTCMediaSource+Internal.h @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCMediaSource.h" + +#include "talk/app/webrtc/mediastreaminterface.h" + +@interface RTCMediaSource (Internal) + +@property(nonatomic, assign, readonly) + talk_base::scoped_refptr mediaSource; + +- (id)initWithMediaSource: + (talk_base::scoped_refptr)mediaSource; + +@end diff --git a/talk/app/webrtc/objc/RTCMediaSource.mm b/talk/app/webrtc/objc/RTCMediaSource.mm new file mode 100644 index 000000000..9331fd729 --- /dev/null +++ b/talk/app/webrtc/objc/RTCMediaSource.mm @@ -0,0 +1,65 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCMediaSource+internal.h" + +#import "RTCEnumConverter.h" + +@implementation RTCMediaSource { + talk_base::scoped_refptr _mediaSource; +} + +- (RTCSourceState)state { + return [RTCEnumConverter convertSourceStateToObjC:self.mediaSource->state()]; +} + +@end + +@implementation RTCMediaSource (Internal) + +- (id)initWithMediaSource: + (talk_base::scoped_refptr)mediaSource { + if (!mediaSource) { + NSAssert(NO, @"nil arguments not allowed"); + self = nil; + return nil; + } + if ((self = [super init])) { + _mediaSource = mediaSource; + } + return self; +} + +- (talk_base::scoped_refptr)mediaSource { + return _mediaSource; +} + +@end diff --git a/talk/app/webrtc/objc/RTCMediaStream+Internal.h b/talk/app/webrtc/objc/RTCMediaStream+Internal.h new file mode 100644 index 000000000..2123c2d8b --- /dev/null +++ b/talk/app/webrtc/objc/RTCMediaStream+Internal.h @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCMediaStream.h" + +#include "talk/app/webrtc/mediastreamtrack.h" + +@interface RTCMediaStream (Internal) + +@property(nonatomic, assign, readonly) + talk_base::scoped_refptr mediaStream; + +- (id)initWithMediaStream: + (talk_base::scoped_refptr)mediaStream; + +@end diff --git a/talk/app/webrtc/objc/RTCMediaStream.mm b/talk/app/webrtc/objc/RTCMediaStream.mm new file mode 100644 index 000000000..dd4aab690 --- /dev/null +++ b/talk/app/webrtc/objc/RTCMediaStream.mm @@ -0,0 +1,145 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCMediaStream+internal.h" + +#import "RTCAudioTrack+internal.h" +#import "RTCMediaStreamTrack+internal.h" +#import "RTCVideoTrack+internal.h" + +#include "talk/app/webrtc/mediastreaminterface.h" + +@implementation RTCMediaStream { + NSMutableArray *_audioTracks; + NSMutableArray *_videoTracks; + talk_base::scoped_refptr _mediaStream; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"[%@:A=%lu:V=%lu]", + [self label], + (unsigned long)[self.audioTracks count], + (unsigned long)[self.videoTracks count]]; +} + +- (NSArray *)audioTracks { + return [_audioTracks copy]; +} + +- (NSArray *)videoTracks { + return [_videoTracks copy]; +} + +- (NSString *)label { + return @(self.mediaStream->label().c_str()); +} + +- (BOOL)addAudioTrack:(RTCAudioTrack *)track { + if (self.mediaStream->AddTrack(track.audioTrack)) { + [_audioTracks addObject:track]; + return YES; + } + return NO; +} + +- (BOOL)addVideoTrack:(RTCVideoTrack *)track { + if (self.mediaStream->AddTrack(track.videoTrack)) { + [_videoTracks addObject:track]; + return YES; + } + return NO; +} + +- (BOOL)removeAudioTrack:(RTCAudioTrack *)track { + NSUInteger index = [_audioTracks indexOfObjectIdenticalTo:track]; + NSAssert(index != NSNotFound, + @"|removeAudioTrack| called on unexpected RTCAudioTrack"); + if (index != NSNotFound && self.mediaStream->RemoveTrack(track.audioTrack)) { + [_audioTracks removeObjectAtIndex:index]; + return YES; + } + return NO; +} + +- (BOOL)removeVideoTrack:(RTCVideoTrack *)track { + NSUInteger index = [_videoTracks indexOfObjectIdenticalTo:track]; + NSAssert(index != NSNotFound, + @"|removeAudioTrack| called on unexpected RTCVideoTrack"); + if (index != NSNotFound && self.mediaStream->RemoveTrack(track.videoTrack)) { + [_videoTracks removeObjectAtIndex:index]; + return YES; + } + return NO; +} + +@end + +@implementation RTCMediaStream (Internal) + +- (id)initWithMediaStream: + (talk_base::scoped_refptr)mediaStream { + if (!mediaStream) { + NSAssert(NO, @"nil arguments not allowed"); + self = nil; + return nil; + } + if ((self = [super init])) { + webrtc::AudioTrackVector audio_tracks = mediaStream->GetAudioTracks(); + webrtc::VideoTrackVector video_tracks = mediaStream->GetVideoTracks(); + + _audioTracks = [NSMutableArray arrayWithCapacity:audio_tracks.size()]; + _videoTracks = [NSMutableArray arrayWithCapacity:video_tracks.size()]; + _mediaStream = mediaStream; + + for (size_t i = 0; i < audio_tracks.size(); ++i) { + talk_base::scoped_refptr track = + audio_tracks[i]; + RTCAudioTrack *audioTrack = + [[RTCAudioTrack alloc] initWithMediaTrack:track]; + [_audioTracks addObject:audioTrack]; + } + // TODO(hughv): Add video. +// for (size_t i = 0; i < video_tracks.size(); ++i) { +// talk_base::scoped_refptr track = +// video_tracks[i]; +// RTCVideoTrack *videoTrack = +// [[RTCVideoTrack alloc] initWithMediaTrack:track]; +// [_videoTracks addObject:videoTrack]; +// } + } + return self; +} + +- (talk_base::scoped_refptr)mediaStream { + return _mediaStream; +} + +@end diff --git a/talk/app/webrtc/objc/RTCMediaStreamTrack+Internal.h b/talk/app/webrtc/objc/RTCMediaStreamTrack+Internal.h new file mode 100644 index 000000000..9a0cab39b --- /dev/null +++ b/talk/app/webrtc/objc/RTCMediaStreamTrack+Internal.h @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCMediaStreamTrack.h" + +#include "talk/app/webrtc/mediastreaminterface.h" + +@interface RTCMediaStreamTrack (Internal) + +@property(nonatomic, assign, readonly) + talk_base::scoped_refptr mediaTrack; + +- (id)initWithMediaTrack: + (talk_base::scoped_refptr)mediaTrack; + +@end diff --git a/talk/app/webrtc/objc/RTCMediaStreamTrack.mm b/talk/app/webrtc/objc/RTCMediaStreamTrack.mm new file mode 100644 index 000000000..6c8f71542 --- /dev/null +++ b/talk/app/webrtc/objc/RTCMediaStreamTrack.mm @@ -0,0 +1,103 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCMediaStreamTrack+internal.h" +#import "RTCEnumConverter.h" + +@implementation RTCMediaStreamTrack { + talk_base::scoped_refptr _mediaTrack; +} + +@synthesize label; + +- (BOOL)isEqual:(id)other { + // Equality is purely based on the label just like the C++ implementation. + if (self == other) return YES; + if (![other isKindOfClass:[self class]] || + ![self isKindOfClass:[other class]]) { + return NO; + } + RTCMediaStreamTrack *otherMediaStream = (RTCMediaStreamTrack *)other; + return [self.label isEqual:otherMediaStream.label]; +} + +- (NSUInteger)hash { + return [self.label hash]; +} + +- (NSString *)kind { + return @(self.mediaTrack->kind().c_str()); +} + +- (NSString *)label { + return @(self.mediaTrack->id().c_str()); +} + +- (BOOL)isEnabled { + return self.mediaTrack->enabled(); +} + +- (BOOL)setEnabled:(BOOL)enabled { + return self.mediaTrack->set_enabled(enabled); +} + +- (RTCTrackState)state { + return [RTCEnumConverter convertTrackStateToObjC:self.mediaTrack->state()]; +} + +- (BOOL)setState:(RTCTrackState)state { + return self.mediaTrack->set_state( + [RTCEnumConverter convertTrackStateToNative:state]); +} + +@end + +@implementation RTCMediaStreamTrack (Internal) + +- (id)initWithMediaTrack:( + talk_base::scoped_refptr)mediaTrack { + if (!mediaTrack) { + NSAssert(NO, @"nil arguments not allowed"); + self = nil; + return nil; + } + if ((self = [super init])) { + _mediaTrack = mediaTrack; + label = @(mediaTrack->id().c_str()); + } + return self; +} + +- (talk_base::scoped_refptr)mediaTrack { + return _mediaTrack; +} + +@end diff --git a/talk/app/webrtc/objc/RTCPair.m b/talk/app/webrtc/objc/RTCPair.m new file mode 100644 index 000000000..ee2ba1b1e --- /dev/null +++ b/talk/app/webrtc/objc/RTCPair.m @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCPair.h" + +@implementation RTCPair + +- (id)initWithKey:(NSString *)key value:(NSString *)value { + if ((self = [super init])) { + _key = [key copy]; + _value = [value copy]; + } + return self; +} + +@end diff --git a/talk/app/webrtc/objc/RTCPeerConnection+Internal.h b/talk/app/webrtc/objc/RTCPeerConnection+Internal.h new file mode 100644 index 000000000..d1b4639f8 --- /dev/null +++ b/talk/app/webrtc/objc/RTCPeerConnection+Internal.h @@ -0,0 +1,44 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCPeerConnection.h" + +#import "RTCPeerConnectionDelegate.h" +#import "RTCPeerConnectionObserver.h" + +#include "talk/app/webrtc/peerconnectioninterface.h" + +@interface RTCPeerConnection (Internal) + +@property(nonatomic, assign, readonly) + talk_base::scoped_refptr peerConnection; + +- (id)initWithPeerConnection:( + talk_base::scoped_refptr)peerConnection + observer:(webrtc::RTCPeerConnectionObserver *)observer; + +@end diff --git a/talk/app/webrtc/objc/RTCPeerConnection.mm b/talk/app/webrtc/objc/RTCPeerConnection.mm new file mode 100644 index 000000000..73dce36f0 --- /dev/null +++ b/talk/app/webrtc/objc/RTCPeerConnection.mm @@ -0,0 +1,247 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCPeerConnection+internal.h" + +#import "RTCEnumConverter.h" +#import "RTCICECandidate+internal.h" +#import "RTCICEServer+internal.h" +#import "RTCMediaConstraints+internal.h" +#import "RTCMediaStream+internal.h" +#import "RTCSessionDescription+internal.h" +#import "RTCSessionDescriptonDelegate.h" +#import "RTCSessionDescription.h" + +#include "talk/app/webrtc/jsep.h" + +NSString* const kRTCSessionDescriptionDelegateErrorDomain = @"RTCSDPError"; +int const kRTCSessionDescriptionDelegateErrorCode = -1; + +namespace webrtc { + +class RTCCreateSessionDescriptionObserver + : public CreateSessionDescriptionObserver { + public: + RTCCreateSessionDescriptionObserver(id delegate, + RTCPeerConnection *peerConnection) { + _delegate = delegate; + _peerConnection = peerConnection; + } + + virtual void OnSuccess(SessionDescriptionInterface *desc) OVERRIDE { + RTCSessionDescription *session = + [[RTCSessionDescription alloc] initWithSessionDescription:desc]; + [_delegate peerConnection:_peerConnection + didCreateSessionDescription:session + error:nil]; + } + + virtual void OnFailure(const std::string &error) OVERRIDE { + NSString *str = @(error.c_str()); + NSError *err = + [NSError errorWithDomain:kRTCSessionDescriptionDelegateErrorDomain + code:kRTCSessionDescriptionDelegateErrorCode + userInfo:@{ @"error" : str }]; + [_delegate peerConnection:_peerConnection + didCreateSessionDescription:nil + error:err]; + } + + private: + id _delegate; + RTCPeerConnection *_peerConnection; +}; + +class RTCSetSessionDescriptionObserver : public SetSessionDescriptionObserver { + public: + RTCSetSessionDescriptionObserver(id delegate, + RTCPeerConnection *peerConnection) { + _delegate = delegate; + _peerConnection = peerConnection; + } + + virtual void OnSuccess() OVERRIDE { + [_delegate peerConnection:_peerConnection + didSetSessionDescriptionWithError:nil]; + } + + virtual void OnFailure(const std::string &error) OVERRIDE { + NSString *str = @(error.c_str()); + NSError *err = + [NSError errorWithDomain:kRTCSessionDescriptionDelegateErrorDomain + code:kRTCSessionDescriptionDelegateErrorCode + userInfo:@{ @"error" : str }]; + [_delegate peerConnection:_peerConnection + didSetSessionDescriptionWithError:err]; + } + + private: + id _delegate; + RTCPeerConnection *_peerConnection; +}; + +} + +@implementation RTCPeerConnection { + NSMutableArray *_localStreams; + talk_base::scoped_ptr_observer; + talk_base::scoped_refptr _peerConnection; +} + +- (BOOL)addICECandidate:(RTCICECandidate *)candidate { + const webrtc::IceCandidateInterface *iceCandidate = candidate.candidate; + return self.peerConnection->AddIceCandidate(iceCandidate); + delete iceCandidate; +} + +- (BOOL)addStream:(RTCMediaStream *)stream + constraints:(RTCMediaConstraints *)constraints { + BOOL ret = self.peerConnection->AddStream(stream.mediaStream, + constraints.constraints); + if (!ret) { + return NO; + } + [_localStreams addObject:stream]; + return YES; +} + +- (void)createAnswerWithDelegate:(id)delegate + constraints:(RTCMediaConstraints *)constraints { + talk_base::scoped_refptr + observer(new talk_base::RefCountedObject< + webrtc::RTCCreateSessionDescriptionObserver>(delegate, self)); + self.peerConnection->CreateAnswer(observer, constraints.constraints); +} + +- (void)createOfferWithDelegate:(id)delegate + constraints:(RTCMediaConstraints *)constraints { + talk_base::scoped_refptr + observer(new talk_base::RefCountedObject< + webrtc::RTCCreateSessionDescriptionObserver>(delegate, self)); + self.peerConnection->CreateOffer(observer, constraints.constraints); +} + +- (void)removeStream:(RTCMediaStream *)stream { + self.peerConnection->RemoveStream(stream.mediaStream); + [_localStreams removeObject:stream]; +} + +- (void) + setLocalDescriptionWithDelegate:(id)delegate + sessionDescription:(RTCSessionDescription *)sdp { + talk_base::scoped_refptr observer( + new talk_base::RefCountedObject( + delegate, self)); + self.peerConnection->SetLocalDescription(observer, sdp.sessionDescription); +} + +- (void) + setRemoteDescriptionWithDelegate:(id)delegate + sessionDescription:(RTCSessionDescription *)sdp { + talk_base::scoped_refptr observer( + new talk_base::RefCountedObject( + delegate, self)); + self.peerConnection->SetRemoteDescription(observer, sdp.sessionDescription); +} + +- (BOOL)updateICEServers:(NSArray *)servers + constraints:(RTCMediaConstraints *)constraints { + webrtc::PeerConnectionInterface::IceServers iceServers; + for (RTCICEServer *server in servers) { + iceServers.push_back(server.iceServer); + } + return self.peerConnection->UpdateIce(iceServers, constraints.constraints); +} + +- (RTCSessionDescription *)localDescription { + const webrtc::SessionDescriptionInterface *sdi = + self.peerConnection->local_description(); + return sdi ? + [[RTCSessionDescription alloc] initWithSessionDescription:sdi] : + nil; +} + +- (NSArray *)localStreams { + return [_localStreams copy]; +} + +- (RTCSessionDescription *)remoteDescription { + const webrtc::SessionDescriptionInterface *sdi = + self.peerConnection->remote_description(); + return sdi ? + [[RTCSessionDescription alloc] initWithSessionDescription:sdi] : + nil; +} + +- (RTCICEConnectionState)iceConnectionState { + return [RTCEnumConverter convertIceConnectionStateToObjC: + self.peerConnection->ice_connection_state()]; +} + +- (RTCICEGatheringState)iceGatheringState { + return [RTCEnumConverter convertIceGatheringStateToObjC: + self.peerConnection->ice_gathering_state()]; +} + +- (RTCSignalingState)signalingState { + return [RTCEnumConverter + convertSignalingStateToObjC:self.peerConnection->signaling_state()]; +} + +- (void)close { + self.peerConnection->Close(); +} + +@end + +@implementation RTCPeerConnection (Internal) + +- (id)initWithPeerConnection:( + talk_base::scoped_refptr)peerConnection + observer:(webrtc::RTCPeerConnectionObserver *)observer { + if (!peerConnection || !observer) { + NSAssert(NO, @"nil arguments not allowed"); + self = nil; + return nil; + } + if ((self = [super init])) { + _peerConnection = peerConnection; + _localStreams = [[NSMutableArray alloc] init]; + _observer.reset(observer); + } + return self; +} + +- (talk_base::scoped_refptr)peerConnection { + return _peerConnection; +} + +@end diff --git a/talk/app/webrtc/objc/RTCPeerConnectionFactory.mm b/talk/app/webrtc/objc/RTCPeerConnectionFactory.mm new file mode 100644 index 000000000..b12af9dfa --- /dev/null +++ b/talk/app/webrtc/objc/RTCPeerConnectionFactory.mm @@ -0,0 +1,127 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCPeerConnectionFactory.h" + +#include + +#import "RTCAudioTrack+internal.h" +#import "RTCICEServer+internal.h" +#import "RTCMediaConstraints+internal.h" +#import "RTCMediaSource+internal.h" +#import "RTCMediaStream+internal.h" +#import "RTCMediaStreamTrack+internal.h" +#import "RTCPeerConnection+internal.h" +#import "RTCPeerConnectionDelegate.h" +#import "RTCPeerConnectionObserver.h" +#import "RTCVideoCapturer+internal.h" +#import "RTCVideoSource+internal.h" +#import "RTCVideoTrack+internal.h" + +#include "talk/app/webrtc/audiotrack.h" +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/peerconnectionfactory.h" +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/app/webrtc/videosourceinterface.h" +#include "talk/app/webrtc/videotrack.h" +#include "talk/base/logging.h" + +@interface RTCPeerConnectionFactory () + +@property(nonatomic, assign) talk_base::scoped_refptr< + webrtc::PeerConnectionFactoryInterface> nativeFactory; + +@end + +@implementation RTCPeerConnectionFactory + +- (id)init { + if ((self = [super init])) { + _nativeFactory = webrtc::CreatePeerConnectionFactory(); + NSAssert(_nativeFactory, @"Failed to initialize PeerConnectionFactory!"); + // Uncomment to get sensitive logs emitted (to stderr or logcat). + // talk_base::LogMessage::LogToDebug(talk_base::LS_SENSITIVE); + } + return self; +} + +- (RTCPeerConnection *) + peerConnectionWithICEServers:(NSArray *)servers + constraints:(RTCMediaConstraints *)constraints + delegate:(id)delegate { + webrtc::PeerConnectionInterface::IceServers iceServers; + for (RTCICEServer *server in servers) { + iceServers.push_back(server.iceServer); + } + webrtc::RTCPeerConnectionObserver *observer = + new webrtc::RTCPeerConnectionObserver(delegate); + talk_base::scoped_refptr peerConnection = + self.nativeFactory->CreatePeerConnection( + iceServers, constraints.constraints, observer); + RTCPeerConnection *pc = + [[RTCPeerConnection alloc] initWithPeerConnection:peerConnection + observer:observer]; + observer->SetPeerConnection(pc); + return pc; +} + +- (RTCMediaStream *)mediaStreamWithLabel:(NSString *)label { + talk_base::scoped_refptr nativeMediaStream = + self.nativeFactory->CreateLocalMediaStream([label UTF8String]); + return [[RTCMediaStream alloc] initWithMediaStream:nativeMediaStream]; +} + +- (RTCVideoSource *)videoSourceWithCapturer:(RTCVideoCapturer *)capturer + constraints:(RTCMediaConstraints *)constraints { + if (!capturer) { + return nil; + } + talk_base::scoped_refptr source = + self.nativeFactory->CreateVideoSource(capturer.capturer.get(), + constraints.constraints); + return [[RTCVideoSource alloc] initWithMediaSource:source]; +} + +- (RTCVideoTrack *)videoTrackWithID:(NSString *)videoId + source:(RTCVideoSource *)source { + talk_base::scoped_refptr track = + self.nativeFactory->CreateVideoTrack([videoId UTF8String], + source.videoSource); + return [[RTCVideoTrack alloc] initWithMediaTrack:track]; +} + +- (RTCAudioTrack *)audioTrackWithID:(NSString *)audioId { + talk_base::scoped_refptr track = + self.nativeFactory->CreateAudioTrack([audioId UTF8String], NULL); + return [[RTCAudioTrack alloc] initWithMediaTrack:track]; +} + +@end diff --git a/talk/app/webrtc/objc/RTCPeerConnectionObserver.h b/talk/app/webrtc/objc/RTCPeerConnectionObserver.h new file mode 100644 index 000000000..c7d1ef8b8 --- /dev/null +++ b/talk/app/webrtc/objc/RTCPeerConnectionObserver.h @@ -0,0 +1,79 @@ +/* + * libjingle + * Copyright 2013, 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/app/webrtc/peerconnectioninterface.h" + +#import "RTCPeerConnection.h" +#import "RTCPeerConnectionDelegate.h" + +// These objects are created by RTCPeerConnectionFactory to wrap an +// id and call methods on that interface. + +namespace webrtc { + +class RTCPeerConnectionObserver : public PeerConnectionObserver { + + public: + explicit RTCPeerConnectionObserver(id delegate); + + void SetPeerConnection(RTCPeerConnection *peerConnection); + + virtual void OnError() OVERRIDE; + + // Triggered when the SignalingState changed. + virtual void OnSignalingChange( + PeerConnectionInterface::SignalingState new_state) OVERRIDE; + + // Triggered when media is received on a new stream from remote peer. + virtual void OnAddStream(MediaStreamInterface* stream) OVERRIDE; + + // Triggered when a remote peer close a stream. + virtual void OnRemoveStream(MediaStreamInterface* stream) OVERRIDE; + + // Triggered when a remote peer open a data channel. + virtual void OnDataChannel(DataChannelInterface* data_channel) OVERRIDE; + + // Triggered when renegotation is needed, for example the ICE has restarted. + virtual void OnRenegotiationNeeded() OVERRIDE; + + // Called any time the ICEConnectionState changes + virtual void OnIceConnectionChange( + PeerConnectionInterface::IceConnectionState new_state) OVERRIDE; + + // Called any time the ICEGatheringState changes + virtual void OnIceGatheringChange( + PeerConnectionInterface::IceGatheringState new_state) OVERRIDE; + + // New Ice candidate have been found. + virtual void OnIceCandidate(const IceCandidateInterface* candidate) OVERRIDE; + + private: + id _delegate; + RTCPeerConnection *_peerConnection; +}; + +} // namespace webrtc diff --git a/talk/app/webrtc/objc/RTCPeerConnectionObserver.mm b/talk/app/webrtc/objc/RTCPeerConnectionObserver.mm new file mode 100644 index 000000000..e102bb974 --- /dev/null +++ b/talk/app/webrtc/objc/RTCPeerConnectionObserver.mm @@ -0,0 +1,103 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCPeerConnectionObserver.h" + +#import "RTCICECandidate+internal.h" +#import "RTCMediaStream+internal.h" +#import "RTCEnumConverter.h" + +namespace webrtc { + +RTCPeerConnectionObserver::RTCPeerConnectionObserver( + id delegate) { + _delegate = delegate; +} + +void RTCPeerConnectionObserver::SetPeerConnection( + RTCPeerConnection *peerConnection) { + _peerConnection = peerConnection; +} + +void RTCPeerConnectionObserver::OnError() { + [_delegate peerConnectionOnError:_peerConnection]; +} + +void RTCPeerConnectionObserver::OnSignalingChange( + PeerConnectionInterface::SignalingState new_state) { + [_delegate peerConnection:_peerConnection + signalingStateChanged: + [RTCEnumConverter convertSignalingStateToObjC:new_state]]; +} + +void RTCPeerConnectionObserver::OnAddStream(MediaStreamInterface* stream) { + RTCMediaStream* mediaStream = + [[RTCMediaStream alloc] initWithMediaStream:stream]; + [_delegate peerConnection:_peerConnection addedStream:mediaStream]; +} + +void RTCPeerConnectionObserver::OnRemoveStream(MediaStreamInterface* stream) { + RTCMediaStream* mediaStream = + [[RTCMediaStream alloc] initWithMediaStream:stream]; + [_delegate peerConnection:_peerConnection removedStream:mediaStream]; +} + +void RTCPeerConnectionObserver::OnDataChannel( + DataChannelInterface* data_channel) { + // TODO(hughv): Implement for future version. +} + +void RTCPeerConnectionObserver::OnRenegotiationNeeded() { + [_delegate peerConnectionOnRenegotiationNeeded:_peerConnection]; +} + +void RTCPeerConnectionObserver::OnIceConnectionChange( + PeerConnectionInterface::IceConnectionState new_state) { + [_delegate peerConnection:_peerConnection + iceConnectionChanged: + [RTCEnumConverter convertIceConnectionStateToObjC:new_state]]; +} + +void RTCPeerConnectionObserver::OnIceGatheringChange( + PeerConnectionInterface::IceGatheringState new_state) { + [_delegate peerConnection:_peerConnection + iceGatheringChanged: + [RTCEnumConverter convertIceGatheringStateToObjC:new_state]]; +} + +void RTCPeerConnectionObserver::OnIceCandidate( + const IceCandidateInterface* candidate) { + RTCICECandidate* iceCandidate = + [[RTCICECandidate alloc] initWithCandidate:candidate]; + [_delegate peerConnection:_peerConnection gotICECandidate:iceCandidate]; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/objc/RTCSessionDescription+Internal.h b/talk/app/webrtc/objc/RTCSessionDescription+Internal.h new file mode 100644 index 000000000..261a176c6 --- /dev/null +++ b/talk/app/webrtc/objc/RTCSessionDescription+Internal.h @@ -0,0 +1,41 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCSessionDescription.h" + +#include "talk/app/webrtc/jsep.h" +#include "talk/app/webrtc/webrtcsession.h" + +@interface RTCSessionDescription (Internal) + +// Caller assumes ownership of this object! +- (webrtc::SessionDescriptionInterface *)sessionDescription; + +- (id)initWithSessionDescription: + (const webrtc::SessionDescriptionInterface*)sessionDescription; + +@end diff --git a/talk/app/webrtc/objc/RTCSessionDescription.mm b/talk/app/webrtc/objc/RTCSessionDescription.mm new file mode 100644 index 000000000..4bd9b1447 --- /dev/null +++ b/talk/app/webrtc/objc/RTCSessionDescription.mm @@ -0,0 +1,81 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCSessionDescription+internal.h" + +@implementation RTCSessionDescription { + NSString *_description; + NSString *_type; +} + +- (id)initWithType:(NSString *)type sdp:(NSString *)sdp { + if (!type || !sdp) { + NSAssert(NO, @"nil arguments not allowed"); + return nil; + } + if ((self = [super init])) { + _description = sdp; + _type = type; + } + return self; +} + +@end + +@implementation RTCSessionDescription (Internal) + +- (id)initWithSessionDescription: + (const webrtc::SessionDescriptionInterface *)sessionDescription { + if (!sessionDescription) { + NSAssert(NO, @"nil arguments not allowed"); + self = nil; + return nil; + } + if ((self = [super init])) { + const std::string &type = sessionDescription->type(); + std::string sdp; + if (!sessionDescription->ToString(&sdp)) { + NSAssert(NO, @"Invalid SessionDescriptionInterface."); + self = nil; + } else { + _description = @(sdp.c_str()); + _type = @(type.c_str()); + } + } + return self; +} + +- (webrtc::SessionDescriptionInterface *)sessionDescription { + return webrtc::CreateSessionDescription( + [self.type UTF8String], [self.description UTF8String], NULL); +} + +@end diff --git a/talk/app/webrtc/objc/RTCVideoCapturer+Internal.h b/talk/app/webrtc/objc/RTCVideoCapturer+Internal.h new file mode 100644 index 000000000..d0d685b2c --- /dev/null +++ b/talk/app/webrtc/objc/RTCVideoCapturer+Internal.h @@ -0,0 +1,38 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCVideoCapturer.h" + +#include "talk/app/webrtc/videosourceinterface.h" + +@interface RTCVideoCapturer (Internal) + +@property(nonatomic, assign, readonly) const talk_base::scoped_ptr &capturer; + +- (id)initWithCapturer:(cricket::VideoCapturer*)capturer; + +@end diff --git a/talk/app/webrtc/objc/RTCVideoCapturer.mm b/talk/app/webrtc/objc/RTCVideoCapturer.mm new file mode 100644 index 000000000..f7282c55d --- /dev/null +++ b/talk/app/webrtc/objc/RTCVideoCapturer.mm @@ -0,0 +1,76 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCVideoCapturer+internal.h" + +#include "talk/media/base/videocapturer.h" +#include "talk/media/devices/devicemanager.h" + +@implementation RTCVideoCapturer { + talk_base::scoped_ptr_capturer; +} + ++ (RTCVideoCapturer *)capturerWithDeviceName:(NSString *)deviceName { + const std::string &device_name = std::string([deviceName UTF8String]); + talk_base::scoped_ptr device_manager( + cricket::DeviceManagerFactory::Create()); + bool initialized = device_manager->Init(); + NSAssert(initialized, @"DeviceManager::Init() failed"); + cricket::Device device; + if (!device_manager->GetVideoCaptureDevice(device_name, &device)) { + LOG(LS_ERROR) << "GetVideoCaptureDevice failed"; + return 0; + } + talk_base::scoped_ptr capturer( + device_manager->CreateVideoCapturer(device)); + RTCVideoCapturer *rtcCapturer = + [[RTCVideoCapturer alloc] initWithCapturer:capturer.release()]; + return rtcCapturer; +} + +@end + +@implementation RTCVideoCapturer (Internal) + +- (id)initWithCapturer:(cricket::VideoCapturer *)capturer { + if ((self = [super init])) { + _capturer.reset(capturer); + } + return self; +} + +// TODO(hughv): When capturer is implemented, this needs to return +// _capturer.release() instead. For now, this isn't used. +- (const talk_base::scoped_ptr &)capturer { + return _capturer; +} + +@end diff --git a/talk/app/webrtc/objc/RTCVideoRenderer+Internal.h b/talk/app/webrtc/objc/RTCVideoRenderer+Internal.h new file mode 100644 index 000000000..8854ed71f --- /dev/null +++ b/talk/app/webrtc/objc/RTCVideoRenderer+Internal.h @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCVideoRenderer.h" + +#include "talk/app/webrtc/mediastreaminterface.h" + +@interface RTCVideoRenderer (Internal) + +// TODO(hughv): Use smart pointer. +@property(nonatomic, assign, readonly) + webrtc::VideoRendererInterface *videoRenderer; + +- (id)initWithVideoRenderer:(webrtc::VideoRendererInterface *)videoRenderer; + +@end diff --git a/talk/app/webrtc/objc/RTCVideoRenderer.mm b/talk/app/webrtc/objc/RTCVideoRenderer.mm new file mode 100644 index 000000000..3d3b10e2b --- /dev/null +++ b/talk/app/webrtc/objc/RTCVideoRenderer.mm @@ -0,0 +1,72 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCVideoRenderer+internal.h" + +#if TARGET_OS_IPHONE +#import +#endif + +#import "RTCI420Frame.h" +#import "RTCVideoRendererDelegate.h" + +@implementation RTCVideoRenderer + ++ (RTCVideoRenderer *)videoRenderGUIWithFrame:(CGRect)frame { + // TODO (hughv): Implement. + return nil; +} + +- (id)initWithDelegate:(id)delegate { + if ((self = [super init])) { + _delegate = delegate; + // TODO (hughv): Create video renderer. + } + return self; +} + +@end + +@implementation RTCVideoRenderer (Internal) + +- (id)initWithVideoRenderer:(webrtc::VideoRendererInterface *)videoRenderer { + if ((self = [super init])) { + // TODO (hughv): Implement. + } + return self; +} + +- (webrtc::VideoRendererInterface *)videoRenderer { + // TODO (hughv): Implement. + return NULL; +} + +@end diff --git a/talk/app/webrtc/objc/RTCVideoSource+Internal.h b/talk/app/webrtc/objc/RTCVideoSource+Internal.h new file mode 100644 index 000000000..1d3c4c9f1 --- /dev/null +++ b/talk/app/webrtc/objc/RTCVideoSource+Internal.h @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCVideoSource.h" + +#include "talk/app/webrtc/videosourceinterface.h" + +@interface RTCVideoSource (Internal) + +@property(nonatomic, assign, readonly) + talk_base::scoped_refptrvideoSource; + +@end diff --git a/talk/app/webrtc/objc/RTCVideoSource.mm b/talk/app/webrtc/objc/RTCVideoSource.mm new file mode 100644 index 000000000..c28fa9bcf --- /dev/null +++ b/talk/app/webrtc/objc/RTCVideoSource.mm @@ -0,0 +1,44 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCVideoSource+internal.h" +#import "RTCMediaSource+internal.h" + +@implementation RTCVideoSource +@end + +@implementation RTCVideoSource (Internal) + +- (talk_base::scoped_refptr)videoSource { + return static_cast(self.mediaSource.get()); +} + +@end diff --git a/talk/app/webrtc/objc/RTCVideoTrack+Internal.h b/talk/app/webrtc/objc/RTCVideoTrack+Internal.h new file mode 100644 index 000000000..b5da54bda --- /dev/null +++ b/talk/app/webrtc/objc/RTCVideoTrack+Internal.h @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCVideoTrack.h" + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/peerconnectioninterface.h" + +@class RTCVideoRenderer; + +@interface RTCVideoTrack (Internal) + +@property(nonatomic, assign, readonly) + talk_base::scoped_refptr videoTrack; + +@end diff --git a/talk/app/webrtc/objc/RTCVideoTrack.mm b/talk/app/webrtc/objc/RTCVideoTrack.mm new file mode 100644 index 000000000..88f7226a1 --- /dev/null +++ b/talk/app/webrtc/objc/RTCVideoTrack.mm @@ -0,0 +1,77 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCVideoTrack+internal.h" + +#import "RTCMediaStreamTrack+internal.h" +#import "RTCVideoRenderer+internal.h" + +@implementation RTCVideoTrack { + NSMutableArray *_rendererArray; +} + +- (id)initWithMediaTrack:( + talk_base::scoped_refptr)mediaTrack { + if (self = [super initWithMediaTrack:mediaTrack]) { + _rendererArray = [NSMutableArray array]; + } + return self; +} + +- (void)addRenderer:(RTCVideoRenderer *)renderer { + NSAssert1(![self.renderers containsObject:renderer], + @"renderers already contains object [%@]", + [renderer description]); + [_rendererArray addObject:renderer]; + self.videoTrack->AddRenderer(renderer.videoRenderer); +} + +- (void)removeRenderer:(RTCVideoRenderer *)renderer { + NSUInteger index = [self.renderers indexOfObjectIdenticalTo:renderer]; + if (index != NSNotFound) { + [_rendererArray removeObjectAtIndex:index]; + self.videoTrack->RemoveRenderer(renderer.videoRenderer); + } +} + +- (NSArray *)renderers { + return [_rendererArray copy]; +} + +@end + +@implementation RTCVideoTrack (Internal) + +- (talk_base::scoped_refptr)videoTrack { + return static_cast(self.mediaTrack.get()); +} + +@end diff --git a/talk/app/webrtc/objc/public/RTCAudioSource.h b/talk/app/webrtc/objc/public/RTCAudioSource.h new file mode 100644 index 000000000..e357620dd --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCAudioSource.h @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCMediaSource.h" + +// RTCAudioSource is an ObjectiveC wrapper for AudioSourceInterface. It is +// used as the source for one or more RTCAudioTrack objects. +@interface RTCAudioSource : RTCMediaSource + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end diff --git a/talk/app/webrtc/objc/public/RTCAudioTrack.h b/talk/app/webrtc/objc/public/RTCAudioTrack.h new file mode 100644 index 000000000..e6aae133b --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCAudioTrack.h @@ -0,0 +1,39 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCMediaStreamTrack.h" + +// RTCAudioTrack is an ObjectiveC wrapper for AudioTrackInterface. +@interface RTCAudioTrack : RTCMediaStreamTrack + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end diff --git a/talk/app/webrtc/objc/public/RTCI420Frame.h b/talk/app/webrtc/objc/public/RTCI420Frame.h new file mode 100644 index 000000000..bf58085da --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCI420Frame.h @@ -0,0 +1,36 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +// RTCI420Frame is an ObjectiveC version of cricket::VideoFrame. +@interface RTCI420Frame : NSObject + +// TODO(hughv): Implement this when iOS VP8 is ready. + +@end + diff --git a/talk/app/webrtc/objc/public/RTCIceCandidate.h b/talk/app/webrtc/objc/public/RTCIceCandidate.h new file mode 100644 index 000000000..f3f2c161c --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCIceCandidate.h @@ -0,0 +1,56 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +// RTCICECandidate contains an instance of ICECandidateInterface. +@interface RTCICECandidate : NSObject + +// If present, this contains the identifier of the "media stream +// identification" as defined in [RFC 3388] for m-line this candidate is +// associated with. +@property(nonatomic, copy, readonly) NSString *sdpMid; + +// This indicates the index (starting at zero) of m-line in the SDP this +// candidate is associated with. +@property(nonatomic, assign, readonly) NSInteger sdpMLineIndex; + +// Creates an SDP-ized form of this candidate. +@property(nonatomic, copy, readonly) NSString *sdp; + +// Creates an ICECandidateInterface based on SDP string. +- (id)initWithMid:(NSString *)sdpMid + index:(NSInteger)sdpMLineIndex + sdp:(NSString *)sdp; + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end diff --git a/talk/app/webrtc/objc/public/RTCIceServer.h b/talk/app/webrtc/objc/public/RTCIceServer.h new file mode 100644 index 000000000..01ad9b54d --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCIceServer.h @@ -0,0 +1,48 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +// RTCICEServer allows for the creation of ICEServer structs. +@interface RTCICEServer : NSObject + +// The server URI. +@property(nonatomic, strong, readonly) NSURL *URI; + +// The server password. +@property(nonatomic, copy, readonly) NSString *password; + +// Initializer for RTCICEServer taking uri and password. +- (id)initWithURI:(NSString *)URI password:(NSString *)password; + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end diff --git a/talk/app/webrtc/objc/public/RTCMediaConstraints.h b/talk/app/webrtc/objc/public/RTCMediaConstraints.h new file mode 100644 index 000000000..89d2c3bf8 --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCMediaConstraints.h @@ -0,0 +1,39 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +// RTCMediaConstraints contains the media constraints to be used in +// RTCPeerConnection and RTCMediaStream. +@interface RTCMediaConstraints : NSObject + +// Initializer for RTCMediaConstraints. The parameters mandatory and optional +// contain RTCPair objects with key/value for each constrant. +- (id)initWithMandatoryConstraints:(NSArray *)mandatory + optionalConstraints:(NSArray *)optional; + +@end diff --git a/talk/app/webrtc/objc/public/RTCMediaSource.h b/talk/app/webrtc/objc/public/RTCMediaSource.h new file mode 100644 index 000000000..be3ad3291 --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCMediaSource.h @@ -0,0 +1,44 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +#import "RTCTypes.h" + +// RTCMediaSource is an ObjectiveC wrapper for MediaSourceInterface +@interface RTCMediaSource : NSObject + +// The current state of the RTCMediaSource. +@property (nonatomic, assign, readonly)RTCSourceState state; + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end diff --git a/talk/app/webrtc/objc/public/RTCMediaStream.h b/talk/app/webrtc/objc/public/RTCMediaStream.h new file mode 100644 index 000000000..cd1032118 --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCMediaStream.h @@ -0,0 +1,51 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +@class RTCAudioTrack; +@class RTCVideoTrack; + +// RTCMediaStream is an ObjectiveC wrapper for MediaStreamInterface. +@interface RTCMediaStream : NSObject + +@property(nonatomic, strong, readonly) NSArray *audioTracks; +@property(nonatomic, strong, readonly) NSArray *videoTracks; +@property(nonatomic, strong, readonly) NSString *label; + +- (BOOL)addAudioTrack:(RTCAudioTrack *)track; +- (BOOL)addVideoTrack:(RTCVideoTrack *)track; +- (BOOL)removeAudioTrack:(RTCAudioTrack *)track; +- (BOOL)removeVideoTrack:(RTCVideoTrack *)track; + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end diff --git a/talk/app/webrtc/objc/public/RTCMediaStreamTrack.h b/talk/app/webrtc/objc/public/RTCMediaStreamTrack.h new file mode 100644 index 000000000..f8f9369b5 --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCMediaStreamTrack.h @@ -0,0 +1,51 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +#import "RTCTypes.h" + +// RTCMediaStreamTrack implements the interface common to RTCAudioTrack and +// RTCVideoTrack. Do not create an instance of this class, rather create one +// of the derived classes. +@interface RTCMediaStreamTrack : NSObject + +@property(nonatomic, assign, readonly) NSString *kind; +@property(nonatomic, assign, readonly) NSString *label; + +- (BOOL)isEnabled; +- (BOOL)setEnabled:(BOOL)enabled; +- (RTCTrackState)state; +- (BOOL)setState:(RTCTrackState)state; + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end diff --git a/talk/app/webrtc/objc/public/RTCPair.h b/talk/app/webrtc/objc/public/RTCPair.h new file mode 100644 index 000000000..bb57e0293 --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCPair.h @@ -0,0 +1,45 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +// A class to hold a key and value. +@interface RTCPair : NSObject + +@property(nonatomic, strong, readonly) NSString *key; +@property(nonatomic, strong, readonly) NSString *value; + +// Initialize a RTCPair object with a key and value. +- (id)initWithKey:(NSString *)key value:(NSString *)value; + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end diff --git a/talk/app/webrtc/objc/public/RTCPeerConnection.h b/talk/app/webrtc/objc/public/RTCPeerConnection.h new file mode 100644 index 000000000..c66bac8b4 --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCPeerConnection.h @@ -0,0 +1,110 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCPeerConnectionDelegate.h" + +@class RTCICECandidate; +@class RTCICEServers; +@class RTCMediaConstraints; +@class RTCMediaStream; +@class RTCSessionDescription; +@protocol RTCSessionDescriptonDelegate; + +// RTCPeerConnection is an ObjectiveC friendly wrapper around a PeerConnection +// object. See the documentation in talk/app/webrtc/peerconnectioninterface.h. +// or http://www.webrtc.org/reference/native-apis, which in turn is inspired by +// the JS APIs: http://dev.w3.org/2011/webrtc/editor/webrtc.html and +// http://www.w3.org/TR/mediacapture-streams/ +@interface RTCPeerConnection : NSObject + +// Accessor methods to active local streams. +@property(nonatomic, strong, readonly) NSArray *localStreams; + +// The local description. +@property(nonatomic, assign, readonly) RTCSessionDescription *localDescription; + +// The remote description. +@property(nonatomic, assign, readonly) RTCSessionDescription *remoteDescription; + +// The current signaling state. +@property(nonatomic, assign, readonly) RTCSignalingState signalingState; +@property(nonatomic, assign, readonly) RTCICEConnectionState iceConnectionState; +@property(nonatomic, assign, readonly) RTCICEGatheringState iceGatheringState; + +// Add a new MediaStream to be sent on this PeerConnection. +// Note that a SessionDescription negotiation is needed before the +// remote peer can receive the stream. +- (BOOL)addStream:(RTCMediaStream *)stream + constraints:(RTCMediaConstraints *)constraints; + +// Remove a MediaStream from this PeerConnection. +// Note that a SessionDescription negotiation is need before the +// remote peer is notified. +- (void)removeStream:(RTCMediaStream *)stream; + +// Create a new offer. +// Success or failure will be reported via RTCSessionDescriptonDelegate. +- (void)createOfferWithDelegate:(id)delegate + constraints:(RTCMediaConstraints *)constraints; + +// Create an answer to an offer. +// Success or failure will be reported via RTCSessionDescriptonDelegate. +- (void)createAnswerWithDelegate:(id)delegate + constraints:(RTCMediaConstraints *)constraints; + +// Sets the local session description. +// Success or failure will be reported via RTCSessionDescriptonDelegate. +- (void) + setLocalDescriptionWithDelegate:(id)delegate + sessionDescription:(RTCSessionDescription *)sdp; + +// Sets the remote session description. +// Success or failure will be reported via RTCSessionDescriptonDelegate. +- (void) + setRemoteDescriptionWithDelegate:(id)delegate + sessionDescription:(RTCSessionDescription *)sdp; + +// Restarts or updates the ICE Agent process of gathering local candidates +// and pinging remote candidates. +- (BOOL)updateICEServers:(NSArray *)servers + constraints:(RTCMediaConstraints *)constraints; + +// Provides a remote candidate to the ICE Agent. +- (BOOL)addICECandidate:(RTCICECandidate *)candidate; + +// Terminates all media and closes the transport. +- (void)close; + +// TODO(hughv): Implement GetStats. + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end diff --git a/talk/app/webrtc/objc/public/RTCPeerConnectionDelegate.h b/talk/app/webrtc/objc/public/RTCPeerConnectionDelegate.h new file mode 100644 index 000000000..b3bb881da --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCPeerConnectionDelegate.h @@ -0,0 +1,70 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +#import "RTCTypes.h" + +@class RTCICECandidate; +@class RTCMediaStream; +@class RTCPeerConnection; + +// RTCPeerConnectionDelegate is a protocol for an object that must be +// implemented to get messages from PeerConnection. +@protocol RTCPeerConnectionDelegate + +// Triggered when there is an error. +- (void)peerConnectionOnError:(RTCPeerConnection *)peerConnection; + +// Triggered when the SignalingState changed. +- (void)peerConnection:(RTCPeerConnection *)peerConnection + signalingStateChanged:(RTCSignalingState)stateChanged; + +// Triggered when media is received on a new stream from remote peer. +- (void)peerConnection:(RTCPeerConnection *)peerConnection + addedStream:(RTCMediaStream *)stream; + +// Triggered when a remote peer close a stream. +- (void)peerConnection:(RTCPeerConnection *)peerConnection + removedStream:(RTCMediaStream *)stream; + +// Triggered when renegotation is needed, for example the ICE has restarted. +- (void)peerConnectionOnRenegotiationNeeded:(RTCPeerConnection *)peerConnection; + +// Called any time the ICEConnectionState changes. +- (void)peerConnection:(RTCPeerConnection *)peerConnection + iceConnectionChanged:(RTCICEConnectionState)newState; + +// Called any time the ICEGatheringState changes. +- (void)peerConnection:(RTCPeerConnection *)peerConnection + iceGatheringChanged:(RTCICEGatheringState)newState; + +// New Ice candidate have been found. +- (void)peerConnection:(RTCPeerConnection *)peerConnection + gotICECandidate:(RTCICECandidate *)candidate; + +@end diff --git a/talk/app/webrtc/objc/public/RTCPeerConnectionFactory.h b/talk/app/webrtc/objc/public/RTCPeerConnectionFactory.h new file mode 100644 index 000000000..0f48299b0 --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCPeerConnectionFactory.h @@ -0,0 +1,67 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +@class RTCAudioTrack; +@class RTCMediaConstraints; +@class RTCMediaStream; +@class RTCPeerConnection; +@class RTCVideoCapturer; +@class RTCVideoSource; +@class RTCVideoTrack; +@protocol RTCPeerConnectionDelegate; + +// RTCPeerConnectionFactory is an ObjectiveC wrapper for PeerConnectionFactory. +// It is the main entry point to the PeerConnection API for clients. +@interface RTCPeerConnectionFactory : NSObject + +// Create an RTCPeerConnection object. RTCPeerConnectionFactory will create +// required libjingle threads, socket and network manager factory classes for +// networking. +- (RTCPeerConnection *) + peerConnectionWithICEServers:(NSArray *)servers + constraints:(RTCMediaConstraints *)constraints + delegate:(id)delegate; + +// Create an RTCMediaStream named |label|. +- (RTCMediaStream *)mediaStreamWithLabel:(NSString *)label; + +// Creates a RTCVideoSource. The new source takes ownership of |capturer|. +// |constraints| decides video resolution and frame rate but can be NULL. +- (RTCVideoSource *)videoSourceWithCapturer:(RTCVideoCapturer *)capturer + constraints:(RTCMediaConstraints *)constraints; + +// Creates a new local VideoTrack. The same |source| can be used in several +// tracks. +- (RTCVideoTrack *)videoTrackWithID:(NSString *)videoId + source:(RTCVideoSource *)source; + +// Creates an new AudioTrack. +- (RTCAudioTrack *)audioTrackWithID:(NSString *)audioId; + +@end diff --git a/talk/app/webrtc/objc/public/RTCSessionDescription.h b/talk/app/webrtc/objc/public/RTCSessionDescription.h new file mode 100644 index 000000000..ffe8fbe52 --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCSessionDescription.h @@ -0,0 +1,50 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +// Description of an RFC 4566 Session. +// RTCSessionDescription is an ObjectiveC wrapper for +// SessionDescriptionInterface. +@interface RTCSessionDescription : NSObject + +// The SDP description. +@property(nonatomic, copy, readonly) NSString *description; + +// The session type. +@property(nonatomic, copy, readonly) NSString *type; + +- (id)initWithType:(NSString *)type sdp:(NSString *)sdp; + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end + diff --git a/talk/app/webrtc/objc/public/RTCSessionDescriptonDelegate.h b/talk/app/webrtc/objc/public/RTCSessionDescriptonDelegate.h new file mode 100644 index 000000000..409aaee5d --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCSessionDescriptonDelegate.h @@ -0,0 +1,49 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +@class RTCPeerConnection; +@class RTCSessionDescription; + +extern NSString* const kRTCSessionDescriptionDelegateErrorDomain; +extern int const kRTCSessionDescriptionDelegateErrorCode; + +// RTCSessionDescriptonDelegate is a protocol for listening to callback messages +// when RTCSessionDescriptions are created or set. +@protocol RTCSessionDescriptonDelegate + +// Called when creating a session. +- (void)peerConnection:(RTCPeerConnection *)peerConnection + didCreateSessionDescription:(RTCSessionDescription *)sdp + error:(NSError *)error; + +// Called when setting a local or remote description. +- (void)peerConnection:(RTCPeerConnection *)peerConnection + didSetSessionDescriptionWithError:(NSError *)error; + +@end diff --git a/talk/app/webrtc/objc/public/RTCTypes.h b/talk/app/webrtc/objc/public/RTCTypes.h new file mode 100644 index 000000000..8ff8bf485 --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCTypes.h @@ -0,0 +1,72 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +// Enums that are common to the ObjectiveC version of the PeerConnection API. + +// RTCICEConnectionState correspond to the states in webrtc::ICEConnectionState. +typedef enum { + RTCICEConnectionNew, + RTCICEConnectionChecking, + RTCICEConnectionConnected, + RTCICEConnectionCompleted, + RTCICEConnectionFailed, + RTCICEConnectionDisconnected, + RTCICEConnectionClosed, +} RTCICEConnectionState; + +// RTCICEGatheringState the states in webrtc::ICEGatheringState. +typedef enum { + RTCICEGatheringNew, + RTCICEGatheringGathering, + RTCICEGatheringComplete, +} RTCICEGatheringState; + +// RTCSignalingState correspond to the states in webrtc::SignalingState. +typedef enum { + RTCSignalingStable, + RTCSignalingHaveLocalOffer, + RTCSignalingHaveLocalPrAnswer, + RTCSignalingHaveRemoteOffer, + RTCSignalingHaveRemotePrAnswer, + RTCSignalingClosed, +} RTCSignalingState; + +// RTCSourceState corresponds to the states in webrtc::SourceState. +typedef enum { + RTCSourceStateInitializing, + RTCSourceStateLive, + RTCSourceStateEnded, + RTCSourceStateMuted, +} RTCSourceState; + +// RTCTrackState corresponds to the states in webrtc::TrackState. +typedef enum { + RTCTrackStateInitializing, + RTCTrackStateLive, + RTCTrackStateEnded, + RTCTrackStateFailed, +} RTCTrackState; diff --git a/talk/app/webrtc/objc/public/RTCVideoCapturer.h b/talk/app/webrtc/objc/public/RTCVideoCapturer.h new file mode 100644 index 000000000..7321d575b --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCVideoCapturer.h @@ -0,0 +1,42 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +// RTCVideoCapturer is an ObjectiveC wrapper for VideoCapturerInterface. +@interface RTCVideoCapturer : NSObject + +// Create a new video capturer using the specified device. ++ (RTCVideoCapturer *)capturerWithDeviceName:(NSString *)deviceName; + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end diff --git a/talk/app/webrtc/objc/public/RTCVideoRenderer.h b/talk/app/webrtc/objc/public/RTCVideoRenderer.h new file mode 100644 index 000000000..cc7ba7184 --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCVideoRenderer.h @@ -0,0 +1,52 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +@protocol RTCVideoRendererDelegate; +struct CGRect; + +// Interface for rendering VideoFrames from a VideoTrack +@interface RTCVideoRenderer : NSObject + +@property(nonatomic, strong) id delegate; + +// A convenience method to create a renderer and window and render frames into +// that window. ++ (RTCVideoRenderer *)videoRenderGUIWithFrame:(CGRect)frame; + +// Initialize the renderer. Requires a delegate which does the actual drawing +// of frames. +- (id)initWithDelegate:(id)delegate; + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end diff --git a/talk/app/webrtc/objc/public/RTCVideoRendererDelegate.h b/talk/app/webrtc/objc/public/RTCVideoRendererDelegate.h new file mode 100644 index 000000000..af72bdeb9 --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCVideoRendererDelegate.h @@ -0,0 +1,44 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +@class RTCI420Frame; +@class RTCVideoRenderer; + +// RTCVideoRendererDelegate is a protocol for an object that must be +// implemented to get messages when rendering. +@protocol RTCVideoRendererDelegate + +// The size of the frame. +- (void)videoRenderer:(RTCVideoRenderer *)videoRenderer setSize:(CGSize)size; + +// The frame to be displayed. +- (void)videoRenderer:(RTCVideoRenderer *)videoRenderer + renderFrame:(RTCI420Frame *)frame; + +@end diff --git a/talk/app/webrtc/objc/public/RTCVideoSource.h b/talk/app/webrtc/objc/public/RTCVideoSource.h new file mode 100644 index 000000000..8de806884 --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCVideoSource.h @@ -0,0 +1,39 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCMediaSource.h" + +// RTCVideoSource is an ObjectiveC wrapper for VideoSourceInterface. +@interface RTCVideoSource : RTCMediaSource + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end diff --git a/talk/app/webrtc/objc/public/RTCVideoTrack.h b/talk/app/webrtc/objc/public/RTCVideoTrack.h new file mode 100644 index 000000000..291c92338 --- /dev/null +++ b/talk/app/webrtc/objc/public/RTCVideoTrack.h @@ -0,0 +1,50 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "RTCMediaStreamTrack.h" + +@class RTCVideoRenderer; + +// RTCVideoTrack is an ObjectiveC wrapper for VideoTrackInterface. +@interface RTCVideoTrack : RTCMediaStreamTrack + +// The currently registered renderers. +@property(nonatomic, strong, readonly) NSArray *renderers; + +// Register a renderer that will render all frames received on this track. +- (void)addRenderer:(RTCVideoRenderer *)renderer; + +// Deregister a renderer. +- (void)removeRenderer:(RTCVideoRenderer *)renderer; + +#ifndef DOXYGEN_SHOULD_SKIP_THIS +// Disallow init and don't add to documentation +- (id)init __attribute__( + (unavailable("init is not a supported initializer for this class."))); +#endif /* DOXYGEN_SHOULD_SKIP_THIS */ + +@end diff --git a/talk/app/webrtc/objctests/Info.plist b/talk/app/webrtc/objctests/Info.plist new file mode 100644 index 000000000..0b1583e6e --- /dev/null +++ b/talk/app/webrtc/objctests/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.Google.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/talk/app/webrtc/objctests/RTCPeerConnectionSyncObserver.h b/talk/app/webrtc/objctests/RTCPeerConnectionSyncObserver.h new file mode 100644 index 000000000..db97816fc --- /dev/null +++ b/talk/app/webrtc/objctests/RTCPeerConnectionSyncObserver.h @@ -0,0 +1,53 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +#import "RTCPeerConnectionDelegate.h" + +// Observer of PeerConnection events, used by RTCPeerConnectionTest to check +// expectations. +@interface RTCPeerConnectionSyncObserver : NSObject +// TODO(hughv): Add support for RTCVideoRendererDelegate when Video is enabled. + +// Transfer received ICE candidates to the caller. +- (NSArray*)releaseReceivedICECandidates; + +// Register expectations for events that this observer should see before it can +// be considered satisfied (see below). +- (void)expectError; +- (void)expectSignalingChange:(RTCSignalingState)state; +- (void)expectAddStream:(NSString *)label; +- (void)expectRemoveStream:(NSString *)label; +- (void)expectICECandidates:(int)count; +- (void)expectICEConnectionChange:(RTCICEConnectionState)state; +- (void)expectICEGatheringChange:(RTCICEGatheringState)state; + +// Wait until all registered expectations above have been observed. +- (void)waitForAllExpectationsToBeSatisfied; + +@end diff --git a/talk/app/webrtc/objctests/RTCPeerConnectionSyncObserver.m b/talk/app/webrtc/objctests/RTCPeerConnectionSyncObserver.m new file mode 100644 index 000000000..0f33bac5c --- /dev/null +++ b/talk/app/webrtc/objctests/RTCPeerConnectionSyncObserver.m @@ -0,0 +1,190 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCPeerConnectionSyncObserver.h" + +#import "RTCMediaStream.h" + +@implementation RTCPeerConnectionSyncObserver { + int _expectedErrors; + NSMutableArray *_expectedSignalingChanges; + NSMutableArray *_expectedAddStreamLabels; + NSMutableArray *_expectedRemoveStreamLabels; + int _expectedICECandidates; + NSMutableArray *_receivedICECandidates; + NSMutableArray *_expectedICEConnectionChanges; + NSMutableArray *_expectedICEGatheringChanges; +} + +- (id)init { + self = [super init]; + if (self) { + _expectedSignalingChanges = [NSMutableArray array]; + _expectedSignalingChanges = [NSMutableArray array]; + _expectedAddStreamLabels = [NSMutableArray array]; + _expectedRemoveStreamLabels = [NSMutableArray array]; + _receivedICECandidates = [NSMutableArray array]; + _expectedICEConnectionChanges = [NSMutableArray array]; + _expectedICEGatheringChanges = [NSMutableArray array]; + } + return self; +} + +- (int)popFirstElementAsInt:(NSMutableArray *)array { + NSAssert([array count] > 0, @"Empty array"); + NSNumber *boxedState = [array objectAtIndex:0]; + [array removeObjectAtIndex:0]; + return [boxedState intValue]; +} + +- (NSString *)popFirstElementAsNSString:(NSMutableArray *)array { + NSAssert([array count] > 0, @"Empty expectation array"); + NSString *string = [array objectAtIndex:0]; + [array removeObjectAtIndex:0]; + return string; +} + +- (BOOL)areAllExpectationsSatisfied { + return _expectedICECandidates <= 0 && // See comment in gotICECandidate. + _expectedErrors == 0 && + [_expectedSignalingChanges count] == 0 && + [_expectedICEConnectionChanges count] == 0 && + [_expectedICEGatheringChanges count] == 0 && + [_expectedAddStreamLabels count] == 0 && + [_expectedRemoveStreamLabels count] == 0; + // TODO(hughv): Test video state here too. +} + +- (NSArray *)releaseReceivedICECandidates { + NSArray* ret = _receivedICECandidates; + _receivedICECandidates = [NSMutableArray array]; + return ret; +} + +- (void)expectError { + ++_expectedErrors; +} + +- (void)expectSignalingChange:(RTCSignalingState)state { + [_expectedSignalingChanges addObject:@((int)state)]; +} + +- (void)expectAddStream:(NSString *)label { + [_expectedAddStreamLabels addObject:label]; +} + +- (void)expectRemoveStream:(NSString *)label { + [_expectedRemoveStreamLabels addObject:label]; +} + +- (void)expectICECandidates:(int)count { + _expectedICECandidates += count; +} + +- (void)expectICEConnectionChange:(RTCICEConnectionState)state { + [_expectedICEConnectionChanges addObject:@((int)state)]; +} + +- (void)expectICEGatheringChange:(RTCICEGatheringState)state { + [_expectedICEGatheringChanges addObject:@((int)state)]; +} + +- (void)waitForAllExpectationsToBeSatisfied { + // TODO (fischman): Revisit. Keeping in sync with the Java version, but + // polling is not optimal. + // https://code.google.com/p/libjingle/source/browse/trunk/talk/app/webrtc/javatests/src/org/webrtc/PeerConnectionTest.java?line=212#212 + while (![self areAllExpectationsSatisfied]) { + [[NSRunLoop currentRunLoop] + runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; + } +} + +#pragma mark - RTCPeerConnectionDelegate methods + +- (void)peerConnectionOnError:(RTCPeerConnection *)peerConnection { + NSLog(@"RTCPeerConnectionDelegate::onError"); + NSAssert(--_expectedErrors >= 0, @"Unexpected error"); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + signalingStateChanged:(RTCSignalingState)stateChanged { + int expectedState = [self popFirstElementAsInt:_expectedSignalingChanges]; + NSString *message = [NSString stringWithFormat: @"RTCPeerConnectionDelegate::" + @"onSignalingStateChange [%d] expected[%d]", stateChanged, expectedState]; + NSAssert(expectedState == (int) stateChanged, message); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + addedStream:(RTCMediaStream *)stream { + NSString *expectedLabel = + [self popFirstElementAsNSString:_expectedAddStreamLabels]; + NSAssert([expectedLabel isEqual:stream.label], @"Stream not expected"); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + removedStream:(RTCMediaStream *)stream { + NSString *expectedLabel = + [self popFirstElementAsNSString:_expectedRemoveStreamLabels]; + NSAssert([expectedLabel isEqual:stream.label], @"Stream not expected"); +} + +- (void)peerConnectionOnRenegotiationNeeded: + (RTCPeerConnection *)peerConnection { +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + gotICECandidate:(RTCICECandidate *)candidate { + --_expectedICECandidates; + // We don't assert expectedICECandidates >= 0 because it's hard to know + // how many to expect, in general. We only use expectICECandidates to + // assert a minimal count. + [_receivedICECandidates addObject:candidate]; +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + iceGatheringChanged:(RTCICEGatheringState)newState { + // It's fine to get a variable number of GATHERING messages before + // COMPLETE fires (depending on how long the test runs) so we don't assert + // any particular count. + if (newState == RTCICEGatheringGathering) { + return; + } + int expectedState = [self popFirstElementAsInt:_expectedICEGatheringChanges]; + NSAssert(expectedState == (int)newState, @"Empty expectation array"); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + iceConnectionChanged:(RTCICEConnectionState)newState { + int expectedState = [self popFirstElementAsInt:_expectedICEConnectionChanges]; + NSAssert(expectedState == (int)newState, @"Empty expectation array"); +} + +@end diff --git a/talk/app/webrtc/objctests/RTCPeerConnectionTest.mm b/talk/app/webrtc/objctests/RTCPeerConnectionTest.mm new file mode 100644 index 000000000..826409f56 --- /dev/null +++ b/talk/app/webrtc/objctests/RTCPeerConnectionTest.mm @@ -0,0 +1,235 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +#import "RTCICEServer.h" +#import "RTCMediaConstraints.h" +#import "RTCMediaStream.h" +#import "RTCPeerConnection.h" +#import "RTCPeerConnectionFactory.h" +#import "RTCPeerConnectionSyncObserver.h" +#import "RTCSessionDescription.h" +#import "RTCSessionDescriptionSyncObserver.h" +#import "RTCVideoRenderer.h" +#import "RTCVideoTrack.h" + +#include "talk/base/gunit.h" + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +@interface RTCPeerConnectionTest : NSObject + +// Returns whether the two sessions are of the same type. ++ (BOOL)isSession:(RTCSessionDescription *)session1 + ofSameTypeAsSession:(RTCSessionDescription *)session2; + +// Create and add tracks to pc, with the given source, label, and IDs +- (RTCMediaStream *) + addTracksToPeerConnection:(RTCPeerConnection *)pc + withFactory:(RTCPeerConnectionFactory *)factory + videoSource:(RTCVideoSource *)videoSource + streamLabel:(NSString *)streamLabel + videoTrackID:(NSString *)videoTrackID + audioTrackID:(NSString *)audioTrackID; + +- (void)testCompleteSession; + +@end + +@implementation RTCPeerConnectionTest + ++ (BOOL)isSession:(RTCSessionDescription *)session1 + ofSameTypeAsSession:(RTCSessionDescription *)session2 { + return [session1.type isEqual:session2.type]; +} + +- (RTCMediaStream *) + addTracksToPeerConnection:(RTCPeerConnection *)pc + withFactory:(RTCPeerConnectionFactory *)factory + videoSource:(RTCVideoSource *)videoSource + streamLabel:(NSString *)streamLabel + videoTrackID:(NSString *)videoTrackID + audioTrackID:(NSString *)audioTrackID { + RTCMediaStream *localMediaStream = [factory mediaStreamWithLabel:streamLabel]; + RTCVideoTrack *videoTrack = + [factory videoTrackWithID:videoTrackID source:videoSource]; + RTCVideoRenderer *videoRenderer = + [[RTCVideoRenderer alloc] initWithDelegate:nil]; + [videoTrack addRenderer:videoRenderer]; + [localMediaStream addVideoTrack:videoTrack]; + // Test that removal/re-add works. + [localMediaStream removeVideoTrack:videoTrack]; + [localMediaStream addVideoTrack:videoTrack]; + RTCAudioTrack *audioTrack = [factory audioTrackWithID:audioTrackID]; + [localMediaStream addAudioTrack:audioTrack]; + RTCMediaConstraints *constraints = [[RTCMediaConstraints alloc] init]; + [pc addStream:localMediaStream constraints:constraints]; + return localMediaStream; +} + +- (void)testCompleteSession { + RTCPeerConnectionFactory *factory = [[RTCPeerConnectionFactory alloc] init]; + NSString *stunURL = @"stun:stun.l.google.com:19302"; + RTCICEServer *stunServer = + [[RTCICEServer alloc] initWithURI:[NSURL URLWithString:stunURL] + password:@""]; + NSArray *iceServers = @[stunServer]; + + RTCMediaConstraints *constraints = [[RTCMediaConstraints alloc] init]; + RTCPeerConnectionSyncObserver *offeringExpectations = + [[RTCPeerConnectionSyncObserver alloc] init]; + RTCPeerConnection *pcOffer = + [factory peerConnectionWithICEServers:iceServers + constraints:constraints + delegate:offeringExpectations]; + + RTCPeerConnectionSyncObserver *answeringExpectations = + [[RTCPeerConnectionSyncObserver alloc] init]; + RTCPeerConnection *pcAnswer = + [factory peerConnectionWithICEServers:iceServers + constraints:constraints + delegate:answeringExpectations]; + + // TODO(hughv): Create video capturer + RTCVideoCapturer *capturer = nil; + RTCVideoSource *videoSource = + [factory videoSourceWithCapturer:capturer constraints:constraints]; + + // Here and below, "oLMS" refers to offerer's local media stream, and "aLMS" + // refers to the answerer's local media stream, with suffixes of "a0" and "v0" + // for audio and video tracks, resp. These mirror chrome historical naming. + RTCMediaStream *oLMSUnused = + [self addTracksToPeerConnection:pcOffer + withFactory:factory + videoSource:videoSource + streamLabel:@"oLMS" + videoTrackID:@"oLMSv0" + audioTrackID:@"oLMSa0"]; + RTCSessionDescriptionSyncObserver *sdpObserver = + [[RTCSessionDescriptionSyncObserver alloc] init]; + [pcOffer createOfferWithDelegate:sdpObserver constraints:constraints]; + [sdpObserver wait]; + EXPECT_TRUE(sdpObserver.success); + RTCSessionDescription *offerSDP = sdpObserver.sessionDescription; + EXPECT_EQ([@"offer" compare:offerSDP.type options:NSCaseInsensitiveSearch], + NSOrderedSame); + EXPECT_GT([offerSDP.description length], 0); + + sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init]; + [answeringExpectations + expectSignalingChange:RTCSignalingHaveRemoteOffer]; + [answeringExpectations expectAddStream:@"oLMS"]; + [pcAnswer setRemoteDescriptionWithDelegate:sdpObserver + sessionDescription:offerSDP]; + [sdpObserver wait]; + + RTCMediaStream *aLMSUnused = + [self addTracksToPeerConnection:pcAnswer + withFactory:factory + videoSource:videoSource + streamLabel:@"aLMS" + videoTrackID:@"aLMSv0" + audioTrackID:@"aLMSa0"]; + + sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init]; + [pcAnswer createAnswerWithDelegate:sdpObserver constraints:constraints]; + [sdpObserver wait]; + EXPECT_TRUE(sdpObserver.success); + RTCSessionDescription *answerSDP = sdpObserver.sessionDescription; + EXPECT_EQ([@"answer" compare:answerSDP.type options:NSCaseInsensitiveSearch], + NSOrderedSame); + EXPECT_GT([answerSDP.description length], 0); + + [offeringExpectations expectICECandidates:2]; + [answeringExpectations expectICECandidates:2]; + + sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init]; + [answeringExpectations expectSignalingChange:RTCSignalingStable]; + [pcAnswer setLocalDescriptionWithDelegate:sdpObserver + sessionDescription:answerSDP]; + [sdpObserver wait]; + EXPECT_TRUE(sdpObserver.sessionDescription == NULL); + + sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init]; + [offeringExpectations expectSignalingChange:RTCSignalingHaveLocalOffer]; + [pcOffer setLocalDescriptionWithDelegate:sdpObserver + sessionDescription:offerSDP]; + [sdpObserver wait]; + EXPECT_TRUE(sdpObserver.sessionDescription == NULL); + + [offeringExpectations expectICEConnectionChange:RTCICEConnectionChecking]; + [offeringExpectations expectICEConnectionChange:RTCICEConnectionConnected]; + [answeringExpectations expectICEConnectionChange:RTCICEConnectionChecking]; + [answeringExpectations expectICEConnectionChange:RTCICEConnectionConnected]; + + [offeringExpectations expectICEGatheringChange:RTCICEGatheringComplete]; + [answeringExpectations expectICEGatheringChange:RTCICEGatheringComplete]; + + sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init]; + [offeringExpectations expectSignalingChange:RTCSignalingStable]; + [offeringExpectations expectAddStream:@"aLMS"]; + [pcOffer setRemoteDescriptionWithDelegate:sdpObserver + sessionDescription:answerSDP]; + [sdpObserver wait]; + EXPECT_TRUE(sdpObserver.sessionDescription == NULL); + + EXPECT_TRUE([offerSDP.type isEqual:pcOffer.localDescription.type]); + EXPECT_TRUE([answerSDP.type isEqual:pcOffer.remoteDescription.type]); + EXPECT_TRUE([offerSDP.type isEqual:pcAnswer.remoteDescription.type]); + EXPECT_TRUE([answerSDP.type isEqual:pcAnswer.localDescription.type]); + + for (RTCICECandidate *candidate in + offeringExpectations.releaseReceivedICECandidates) { + [pcAnswer addICECandidate:candidate]; + } + for (RTCICECandidate *candidate in + answeringExpectations.releaseReceivedICECandidates) { + [pcOffer addICECandidate:candidate]; + } + + [offeringExpectations waitForAllExpectationsToBeSatisfied]; + [answeringExpectations waitForAllExpectationsToBeSatisfied]; + + // Let the audio feedback run for 10s to allow human testing and to ensure + // things stabilize. TODO(fischman): replace seconds with # of video frames, + // when we have video flowing. + [[NSRunLoop currentRunLoop] + runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]]; + + // TODO(hughv): Implement orderly shutdown. +} + +@end + + +TEST(RTCPeerConnectionTest, SessionTest) { + RTCPeerConnectionTest *pcTest = [[RTCPeerConnectionTest alloc] init]; + [pcTest testCompleteSession]; +} diff --git a/talk/app/webrtc/objctests/RTCSessionDescriptionSyncObserver.h b/talk/app/webrtc/objctests/RTCSessionDescriptionSyncObserver.h new file mode 100644 index 000000000..18d790288 --- /dev/null +++ b/talk/app/webrtc/objctests/RTCSessionDescriptionSyncObserver.h @@ -0,0 +1,49 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +#import "RTCSessionDescriptonDelegate.h" + +@class RTCSessionDescription; + +// Observer of SDP-related events, used by RTCPeerConnectionTest to check +// expectations. +@interface RTCSessionDescriptionSyncObserver : NSObject< + RTCSessionDescriptonDelegate> + +// Error string. May be nil. +@property(atomic, copy) NSString *error; +// Created session description. May be nil. +@property(atomic, strong) RTCSessionDescription *sessionDescription; +// Whether an SDP-related callback reported success. +@property(atomic, assign) BOOL success; + +// Wait for an SDP-related callback to fire. +- (void)wait; + +@end diff --git a/talk/app/webrtc/objctests/RTCSessionDescriptionSyncObserver.m b/talk/app/webrtc/objctests/RTCSessionDescriptionSyncObserver.m new file mode 100644 index 000000000..c04c1c39c --- /dev/null +++ b/talk/app/webrtc/objctests/RTCSessionDescriptionSyncObserver.m @@ -0,0 +1,97 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "RTCSessionDescriptionSyncObserver.h" + +#import "RTCSessionDescription.h" + +@interface RTCSessionDescriptionSyncObserver() + +// CondVar used to wait for, and signal arrival of, an SDP-related callback. +@property(nonatomic, strong) NSCondition *condition; +// Whether an SDP-related callback has fired; cleared before wait returns. +@property(atomic, assign) BOOL signaled; + +@end + +@implementation RTCSessionDescriptionSyncObserver + +- (id)init { + if ((self = [super init])) { + if (!(_condition = [[NSCondition alloc] init])) + self = nil; + } + return self; +} + +- (void)signal { + self.signaled = YES; + [self.condition signal]; +} + +- (void)wait { + [self.condition lock]; + if (!self.signaled) + [self.condition wait]; + self.signaled = NO; + [self.condition unlock]; +} + +#pragma mark - RTCSessionDescriptonDelegate methods +- (void)peerConnection:(RTCPeerConnection *)peerConnection + didCreateSessionDescription:(RTCSessionDescription *)sdp + error:(NSError *)error { + [self.condition lock]; + if (error) { + self.success = NO; + self.error = error.description; + } else { + self.success = YES; + self.sessionDescription = sdp; + } + [self signal]; + [self.condition unlock]; +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + didSetSessionDescriptionWithError:(NSError *)error { + [self.condition lock]; + if (error) { + self.success = NO; + self.error = error.description; + } else { + self.success = YES; + } + [self signal]; + [self.condition unlock]; +} + +@end diff --git a/talk/app/webrtc/objctests/mac/main.mm b/talk/app/webrtc/objctests/mac/main.mm new file mode 100644 index 000000000..3fb24f37b --- /dev/null +++ b/talk/app/webrtc/objctests/mac/main.mm @@ -0,0 +1,33 @@ +/* + * libjingle + * Copyright 2013, 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/base/gunit.h" + +int main(int argc, char *argv[]) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/talk/app/webrtc/peerconnection.cc b/talk/app/webrtc/peerconnection.cc new file mode 100644 index 000000000..6d3417aab --- /dev/null +++ b/talk/app/webrtc/peerconnection.cc @@ -0,0 +1,755 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/peerconnection.h" + +#include + +#include "talk/app/webrtc/dtmfsender.h" +#include "talk/app/webrtc/jsepicecandidate.h" +#include "talk/app/webrtc/jsepsessiondescription.h" +#include "talk/app/webrtc/mediastreamhandler.h" +#include "talk/app/webrtc/streamcollection.h" +#include "talk/base/logging.h" +#include "talk/base/stringencode.h" +#include "talk/session/media/channelmanager.h" + +namespace { + +using webrtc::PeerConnectionInterface; + +// The min number of tokens in the ice uri. +static const size_t kMinIceUriTokens = 2; +// The min number of tokens must present in Turn host uri. +// e.g. user@turn.example.org +static const size_t kTurnHostTokensNum = 2; +// Number of tokens must be preset when TURN uri has transport param. +static const size_t kTurnTransportTokensNum = 2; +// The default stun port. +static const int kDefaultPort = 3478; +static const char kTransport[] = "transport"; +static const char kDefaultTransportType[] = "udp"; + +// NOTE: Must be in the same order as the ServiceType enum. +static const char* kValidIceServiceTypes[] = { + "stun", "stuns", "turn", "turns", "invalid" }; + +enum ServiceType { + STUN, // Indicates a STUN server. + STUNS, // Indicates a STUN server used with a TLS session. + TURN, // Indicates a TURN server + TURNS, // Indicates a TURN server used with a TLS session. + INVALID, // Unknown. +}; + +enum { + MSG_CREATE_SESSIONDESCRIPTION_SUCCESS = 0, + MSG_CREATE_SESSIONDESCRIPTION_FAILED, + MSG_SET_SESSIONDESCRIPTION_SUCCESS, + MSG_SET_SESSIONDESCRIPTION_FAILED, + MSG_GETSTATS, + MSG_ICECONNECTIONCHANGE, + MSG_ICEGATHERINGCHANGE, + MSG_ICECANDIDATE, + MSG_ICECOMPLETE, +}; + +struct CandidateMsg : public talk_base::MessageData { + explicit CandidateMsg(const webrtc::JsepIceCandidate* candidate) + : candidate(candidate) { + } + talk_base::scoped_ptr candidate; +}; + +struct CreateSessionDescriptionMsg : public talk_base::MessageData { + explicit CreateSessionDescriptionMsg( + webrtc::CreateSessionDescriptionObserver* observer) + : observer(observer) { + } + + talk_base::scoped_refptr observer; + std::string error; + talk_base::scoped_ptr description; +}; + +struct SetSessionDescriptionMsg : public talk_base::MessageData { + explicit SetSessionDescriptionMsg( + webrtc::SetSessionDescriptionObserver* observer) + : observer(observer) { + } + + talk_base::scoped_refptr observer; + std::string error; +}; + +struct GetStatsMsg : public talk_base::MessageData { + explicit GetStatsMsg(webrtc::StatsObserver* observer) + : observer(observer) { + } + webrtc::StatsReports reports; + talk_base::scoped_refptr observer; +}; + +typedef webrtc::PortAllocatorFactoryInterface::StunConfiguration + StunConfiguration; +typedef webrtc::PortAllocatorFactoryInterface::TurnConfiguration + TurnConfiguration; + +bool ParseIceServers(const PeerConnectionInterface::IceServers& configuration, + std::vector* stun_config, + std::vector* turn_config) { + // draft-nandakumar-rtcweb-stun-uri-01 + // stunURI = scheme ":" stun-host [ ":" stun-port ] + // scheme = "stun" / "stuns" + // stun-host = IP-literal / IPv4address / reg-name + // stun-port = *DIGIT + + // draft-petithuguenin-behave-turn-uris-01 + // turnURI = scheme ":" turn-host [ ":" turn-port ] + // [ "?transport=" transport ] + // scheme = "turn" / "turns" + // transport = "udp" / "tcp" / transport-ext + // transport-ext = 1*unreserved + // turn-host = IP-literal / IPv4address / reg-name + // turn-port = *DIGIT + + // TODO(ronghuawu): Handle IPV6 address + for (size_t i = 0; i < configuration.size(); ++i) { + webrtc::PeerConnectionInterface::IceServer server = configuration[i]; + if (server.uri.empty()) { + LOG(WARNING) << "Empty uri."; + continue; + } + std::vector tokens; + std::string turn_transport_type = kDefaultTransportType; + talk_base::tokenize(server.uri, '?', &tokens); + std::string uri_without_transport = tokens[0]; + // Let's look into transport= param, if it exists. + if (tokens.size() == kTurnTransportTokensNum) { // ?transport= is present. + std::string uri_transport_param = tokens[1]; + talk_base::tokenize(uri_transport_param, '=', &tokens); + if (tokens[0] == kTransport) { + turn_transport_type = tokens[1]; + } + } + + tokens.clear(); + talk_base::tokenize(uri_without_transport, ':', &tokens); + if (tokens.size() < kMinIceUriTokens) { + LOG(WARNING) << "Invalid uri: " << server.uri; + continue; + } + ServiceType service_type = INVALID; + const std::string& type = tokens[0]; + for (size_t i = 0; i < ARRAY_SIZE(kValidIceServiceTypes); ++i) { + if (type.compare(kValidIceServiceTypes[i]) == 0) { + service_type = static_cast(i); + break; + } + } + if (service_type == INVALID) { + LOG(WARNING) << "Invalid service type: " << type; + continue; + } + std::string address = tokens[1]; + int port = kDefaultPort; + if (tokens.size() > kMinIceUriTokens) { + if (!talk_base::FromString(tokens[2], &port)) { + LOG(LS_WARNING) << "Failed to parse port string: " << tokens[2]; + continue; + } + + if (port <= 0 || port > 0xffff) { + LOG(WARNING) << "Invalid port: " << port; + continue; + } + } + + switch (service_type) { + case STUN: + case STUNS: + stun_config->push_back(StunConfiguration(address, port)); + break; + case TURN: { + if (server.username.empty()) { + // Turn url example from the spec |url:"turn:user@turn.example.org"|. + std::vector turn_tokens; + talk_base::tokenize(address, '@', &turn_tokens); + if (turn_tokens.size() == kTurnHostTokensNum) { + server.username = talk_base::s_url_decode(turn_tokens[0]); + address = turn_tokens[1]; + } + } + turn_config->push_back(TurnConfiguration(address, port, + server.username, + server.password, + turn_transport_type)); + // STUN functionality is part of TURN. + stun_config->push_back(StunConfiguration(address, port)); + break; + } + case TURNS: + case INVALID: + default: + LOG(WARNING) << "Configuration not supported: " << server.uri; + return false; + } + } + return true; +} + +// Check if we can send |new_stream| on a PeerConnection. +// Currently only one audio but multiple video track is supported per +// PeerConnection. +bool CanAddLocalMediaStream(webrtc::StreamCollectionInterface* current_streams, + webrtc::MediaStreamInterface* new_stream) { + if (!new_stream || !current_streams) + return false; + if (current_streams->find(new_stream->label()) != NULL) { + LOG(LS_ERROR) << "MediaStream with label " << new_stream->label() + << " is already added."; + return false; + } + + bool audio_track_exist = false; + for (size_t j = 0; j < current_streams->count(); ++j) { + if (!audio_track_exist) { + audio_track_exist = current_streams->at(j)->GetAudioTracks().size() > 0; + } + } + if (audio_track_exist && (new_stream->GetAudioTracks().size() > 0)) { + LOG(LS_ERROR) << "AddStream - Currently only one audio track is supported" + << "per PeerConnection."; + return false; + } + return true; +} + +} // namespace + +namespace webrtc { + +PeerConnection::PeerConnection(PeerConnectionFactory* factory) + : factory_(factory), + observer_(NULL), + signaling_state_(kStable), + ice_state_(kIceNew), + ice_connection_state_(kIceConnectionNew), + ice_gathering_state_(kIceGatheringNew) { +} + +PeerConnection::~PeerConnection() { + if (mediastream_signaling_) + mediastream_signaling_->TearDown(); + if (stream_handler_container_) + stream_handler_container_->TearDown(); +} + +bool PeerConnection::Initialize( + const PeerConnectionInterface::IceServers& configuration, + const MediaConstraintsInterface* constraints, + webrtc::PortAllocatorFactoryInterface* allocator_factory, + PeerConnectionObserver* observer) { + std::vector stun_config; + std::vector turn_config; + if (!ParseIceServers(configuration, &stun_config, &turn_config)) { + return false; + } + + return DoInitialize(stun_config, turn_config, constraints, + allocator_factory, observer); +} + +bool PeerConnection::DoInitialize( + const StunConfigurations& stun_config, + const TurnConfigurations& turn_config, + const MediaConstraintsInterface* constraints, + webrtc::PortAllocatorFactoryInterface* allocator_factory, + PeerConnectionObserver* observer) { + ASSERT(observer != NULL); + if (!observer) + return false; + observer_ = observer; + port_allocator_.reset( + allocator_factory->CreatePortAllocator(stun_config, turn_config)); + // To handle both internal and externally created port allocator, we will + // enable BUNDLE here. Also enabling TURN and disable legacy relay service. + port_allocator_->set_flags(cricket::PORTALLOCATOR_ENABLE_BUNDLE | + cricket::PORTALLOCATOR_ENABLE_SHARED_UFRAG | + cricket::PORTALLOCATOR_ENABLE_SHARED_SOCKET); + // No step delay is used while allocating ports. + port_allocator_->set_step_delay(cricket::kMinimumStepDelay); + + mediastream_signaling_.reset(new MediaStreamSignaling( + factory_->signaling_thread(), this)); + + session_.reset(new WebRtcSession(factory_->channel_manager(), + factory_->signaling_thread(), + factory_->worker_thread(), + port_allocator_.get(), + mediastream_signaling_.get())); + stream_handler_container_.reset(new MediaStreamHandlerContainer( + session_.get(), session_.get())); + stats_.set_session(session_.get()); + + // Initialize the WebRtcSession. It creates transport channels etc. + if (!session_->Initialize(constraints)) + return false; + + + // Register PeerConnection as receiver of local ice candidates. + // All the callbacks will be posted to the application from PeerConnection. + session_->RegisterIceObserver(this); + session_->SignalState.connect(this, &PeerConnection::OnSessionStateChange); + return true; +} + +talk_base::scoped_refptr +PeerConnection::local_streams() { + return mediastream_signaling_->local_streams(); +} + +talk_base::scoped_refptr +PeerConnection::remote_streams() { + return mediastream_signaling_->remote_streams(); +} + +bool PeerConnection::AddStream(MediaStreamInterface* local_stream, + const MediaConstraintsInterface* constraints) { + if (IsClosed()) { + return false; + } + if (!CanAddLocalMediaStream(mediastream_signaling_->local_streams(), + local_stream)) + return false; + + // TODO(perkj): Implement support for MediaConstraints in AddStream. + if (!mediastream_signaling_->AddLocalStream(local_stream)) { + return false; + } + stats_.AddStream(local_stream); + observer_->OnRenegotiationNeeded(); + return true; +} + +void PeerConnection::RemoveStream(MediaStreamInterface* local_stream) { + if (IsClosed()) { + return; + } + mediastream_signaling_->RemoveLocalStream(local_stream); + observer_->OnRenegotiationNeeded(); +} + +talk_base::scoped_refptr PeerConnection::CreateDtmfSender( + AudioTrackInterface* track) { + if (!track) { + LOG(LS_ERROR) << "CreateDtmfSender - track is NULL."; + return NULL; + } + if (!mediastream_signaling_->local_streams()->FindAudioTrack(track->id())) { + LOG(LS_ERROR) << "CreateDtmfSender is called with a non local audio track."; + return NULL; + } + + talk_base::scoped_refptr sender( + DtmfSender::Create(track, signaling_thread(), session_.get())); + if (!sender.get()) { + LOG(LS_ERROR) << "CreateDtmfSender failed on DtmfSender::Create."; + return NULL; + } + return DtmfSenderProxy::Create(signaling_thread(), sender.get()); +} + +bool PeerConnection::GetStats(StatsObserver* observer, + MediaStreamTrackInterface* track) { + if (!VERIFY(observer != NULL)) { + LOG(LS_ERROR) << "GetStats - observer is NULL."; + return false; + } + + stats_.UpdateStats(); + talk_base::scoped_ptr msg(new GetStatsMsg(observer)); + if (!stats_.GetStats(track, &(msg->reports))) { + return false; + } + signaling_thread()->Post(this, MSG_GETSTATS, msg.release()); + return true; +} + +PeerConnectionInterface::SignalingState PeerConnection::signaling_state() { + return signaling_state_; +} + +PeerConnectionInterface::IceState PeerConnection::ice_state() { + return ice_state_; +} + +PeerConnectionInterface::IceConnectionState +PeerConnection::ice_connection_state() { + return ice_connection_state_; +} + +PeerConnectionInterface::IceGatheringState +PeerConnection::ice_gathering_state() { + return ice_gathering_state_; +} + +talk_base::scoped_refptr +PeerConnection::CreateDataChannel( + const std::string& label, + const DataChannelInit* config) { + talk_base::scoped_refptr channel( + session_->CreateDataChannel(label, config)); + if (!channel.get()) + return NULL; + + observer_->OnRenegotiationNeeded(); + return DataChannelProxy::Create(signaling_thread(), channel.get()); +} + +void PeerConnection::CreateOffer(CreateSessionDescriptionObserver* observer, + const MediaConstraintsInterface* constraints) { + if (!VERIFY(observer != NULL)) { + LOG(LS_ERROR) << "CreateOffer - observer is NULL."; + return; + } + CreateSessionDescriptionMsg* msg = new CreateSessionDescriptionMsg(observer); + msg->description.reset( + session_->CreateOffer(constraints)); + + if (!msg->description) { + msg->error = "CreateOffer failed."; + signaling_thread()->Post(this, MSG_CREATE_SESSIONDESCRIPTION_FAILED, msg); + return; + } + + signaling_thread()->Post(this, MSG_CREATE_SESSIONDESCRIPTION_SUCCESS, msg); +} + +void PeerConnection::CreateAnswer( + CreateSessionDescriptionObserver* observer, + const MediaConstraintsInterface* constraints) { + if (!VERIFY(observer != NULL)) { + LOG(LS_ERROR) << "CreateAnswer - observer is NULL."; + return; + } + CreateSessionDescriptionMsg* msg = new CreateSessionDescriptionMsg(observer); + msg->description.reset(session_->CreateAnswer(constraints)); + if (!msg->description) { + msg->error = "CreateAnswer failed."; + signaling_thread()->Post(this, MSG_CREATE_SESSIONDESCRIPTION_FAILED, msg); + return; + } + + signaling_thread()->Post(this, MSG_CREATE_SESSIONDESCRIPTION_SUCCESS, msg); +} + +void PeerConnection::SetLocalDescription( + SetSessionDescriptionObserver* observer, + SessionDescriptionInterface* desc) { + if (!VERIFY(observer != NULL)) { + LOG(LS_ERROR) << "SetLocalDescription - observer is NULL."; + return; + } + if (!desc) { + PostSetSessionDescriptionFailure(observer, "SessionDescription is NULL."); + return; + } + + // Update stats here so that we have the most recent stats for tracks and + // streams that might be removed by updating the session description. + stats_.UpdateStats(); + std::string error; + if (!session_->SetLocalDescription(desc, &error)) { + PostSetSessionDescriptionFailure(observer, error); + return; + } + SetSessionDescriptionMsg* msg = new SetSessionDescriptionMsg(observer); + signaling_thread()->Post(this, MSG_SET_SESSIONDESCRIPTION_SUCCESS, msg); +} + +void PeerConnection::SetRemoteDescription( + SetSessionDescriptionObserver* observer, + SessionDescriptionInterface* desc) { + if (!VERIFY(observer != NULL)) { + LOG(LS_ERROR) << "SetRemoteDescription - observer is NULL."; + return; + } + + if (!desc) { + PostSetSessionDescriptionFailure(observer, "SessionDescription is NULL."); + return; + } + // Update stats here so that we have the most recent stats for tracks and + // streams that might be removed by updating the session description. + stats_.UpdateStats(); + std::string error; + if (!session_->SetRemoteDescription(desc, &error)) { + PostSetSessionDescriptionFailure(observer, error); + return; + } + SetSessionDescriptionMsg* msg = new SetSessionDescriptionMsg(observer); + signaling_thread()->Post(this, MSG_SET_SESSIONDESCRIPTION_SUCCESS, msg); +} + +void PeerConnection::PostSetSessionDescriptionFailure( + SetSessionDescriptionObserver* observer, + const std::string& error) { + SetSessionDescriptionMsg* msg = new SetSessionDescriptionMsg(observer); + msg->error = error; + signaling_thread()->Post(this, MSG_SET_SESSIONDESCRIPTION_FAILED, msg); +} + +bool PeerConnection::UpdateIce(const IceServers& configuration, + const MediaConstraintsInterface* constraints) { + // TODO(ronghuawu): Implement UpdateIce. + LOG(LS_ERROR) << "UpdateIce is not implemented."; + return false; +} + +bool PeerConnection::AddIceCandidate( + const IceCandidateInterface* ice_candidate) { + return session_->ProcessIceMessage(ice_candidate); +} + +const SessionDescriptionInterface* PeerConnection::local_description() const { + return session_->local_description(); +} + +const SessionDescriptionInterface* PeerConnection::remote_description() const { + return session_->remote_description(); +} + +void PeerConnection::Close() { + // Update stats here so that we have the most recent stats for tracks and + // streams before the channels are closed. + stats_.UpdateStats(); + + session_->Terminate(); +} + +void PeerConnection::OnSessionStateChange(cricket::BaseSession* /*session*/, + cricket::BaseSession::State state) { + switch (state) { + case cricket::BaseSession::STATE_INIT: + ChangeSignalingState(PeerConnectionInterface::kStable); + case cricket::BaseSession::STATE_SENTINITIATE: + ChangeSignalingState(PeerConnectionInterface::kHaveLocalOffer); + break; + case cricket::BaseSession::STATE_SENTPRACCEPT: + ChangeSignalingState(PeerConnectionInterface::kHaveLocalPrAnswer); + break; + case cricket::BaseSession::STATE_RECEIVEDINITIATE: + ChangeSignalingState(PeerConnectionInterface::kHaveRemoteOffer); + break; + case cricket::BaseSession::STATE_RECEIVEDPRACCEPT: + ChangeSignalingState(PeerConnectionInterface::kHaveRemotePrAnswer); + break; + case cricket::BaseSession::STATE_SENTACCEPT: + case cricket::BaseSession::STATE_RECEIVEDACCEPT: + ChangeSignalingState(PeerConnectionInterface::kStable); + break; + case cricket::BaseSession::STATE_RECEIVEDTERMINATE: + ChangeSignalingState(PeerConnectionInterface::kClosed); + break; + default: + break; + } +} + +void PeerConnection::OnMessage(talk_base::Message* msg) { + switch (msg->message_id) { + case MSG_CREATE_SESSIONDESCRIPTION_SUCCESS: { + CreateSessionDescriptionMsg* param = + static_cast(msg->pdata); + param->observer->OnSuccess(param->description.release()); + delete param; + break; + } + case MSG_CREATE_SESSIONDESCRIPTION_FAILED: { + CreateSessionDescriptionMsg* param = + static_cast(msg->pdata); + param->observer->OnFailure(param->error); + delete param; + break; + } + case MSG_SET_SESSIONDESCRIPTION_SUCCESS: { + SetSessionDescriptionMsg* param = + static_cast(msg->pdata); + param->observer->OnSuccess(); + delete param; + break; + } + case MSG_SET_SESSIONDESCRIPTION_FAILED: { + SetSessionDescriptionMsg* param = + static_cast(msg->pdata); + param->observer->OnFailure(param->error); + delete param; + break; + } + case MSG_GETSTATS: { + GetStatsMsg* param = static_cast(msg->pdata); + param->observer->OnComplete(param->reports); + delete param; + break; + } + case MSG_ICECONNECTIONCHANGE: { + observer_->OnIceConnectionChange(ice_connection_state_); + break; + } + case MSG_ICEGATHERINGCHANGE: { + observer_->OnIceGatheringChange(ice_gathering_state_); + break; + } + case MSG_ICECANDIDATE: { + CandidateMsg* data = static_cast(msg->pdata); + observer_->OnIceCandidate(data->candidate.get()); + delete data; + break; + } + case MSG_ICECOMPLETE: { + observer_->OnIceComplete(); + break; + } + default: + ASSERT(false && "Not implemented"); + break; + } +} + +void PeerConnection::OnAddRemoteStream(MediaStreamInterface* stream) { + stats_.AddStream(stream); + observer_->OnAddStream(stream); +} + +void PeerConnection::OnRemoveRemoteStream(MediaStreamInterface* stream) { + stream_handler_container_->RemoveRemoteStream(stream); + observer_->OnRemoveStream(stream); +} + +void PeerConnection::OnAddDataChannel(DataChannelInterface* data_channel) { + observer_->OnDataChannel(DataChannelProxy::Create(signaling_thread(), + data_channel)); +} + +void PeerConnection::OnAddRemoteAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track, + uint32 ssrc) { + stream_handler_container_->AddRemoteAudioTrack(stream, audio_track, ssrc); +} + +void PeerConnection::OnAddRemoteVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track, + uint32 ssrc) { + stream_handler_container_->AddRemoteVideoTrack(stream, video_track, ssrc); +} + +void PeerConnection::OnRemoveRemoteAudioTrack( + MediaStreamInterface* stream, + AudioTrackInterface* audio_track) { + stream_handler_container_->RemoveRemoteTrack(stream, audio_track); +} + +void PeerConnection::OnRemoveRemoteVideoTrack( + MediaStreamInterface* stream, + VideoTrackInterface* video_track) { + stream_handler_container_->RemoveRemoteTrack(stream, video_track); +} +void PeerConnection::OnAddLocalAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track, + uint32 ssrc) { + stream_handler_container_->AddLocalAudioTrack(stream, audio_track, ssrc); +} +void PeerConnection::OnAddLocalVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track, + uint32 ssrc) { + stream_handler_container_->AddLocalVideoTrack(stream, video_track, ssrc); +} + +void PeerConnection::OnRemoveLocalAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track) { + stream_handler_container_->RemoveLocalTrack(stream, audio_track); +} + +void PeerConnection::OnRemoveLocalVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track) { + stream_handler_container_->RemoveLocalTrack(stream, video_track); +} + +void PeerConnection::OnRemoveLocalStream(MediaStreamInterface* stream) { + stream_handler_container_->RemoveLocalStream(stream); +} + +void PeerConnection::OnIceConnectionChange( + PeerConnectionInterface::IceConnectionState new_state) { + ice_connection_state_ = new_state; + signaling_thread()->Post(this, MSG_ICECONNECTIONCHANGE); +} + +void PeerConnection::OnIceGatheringChange( + PeerConnectionInterface::IceGatheringState new_state) { + if (IsClosed()) { + return; + } + ice_gathering_state_ = new_state; + signaling_thread()->Post(this, MSG_ICEGATHERINGCHANGE); +} + +void PeerConnection::OnIceCandidate(const IceCandidateInterface* candidate) { + JsepIceCandidate* candidate_copy = NULL; + if (candidate) { + // TODO(ronghuawu): Make IceCandidateInterface reference counted instead + // of making a copy. + candidate_copy = new JsepIceCandidate(candidate->sdp_mid(), + candidate->sdp_mline_index(), + candidate->candidate()); + } + // The Post takes the ownership of the |candidate_copy|. + signaling_thread()->Post(this, MSG_ICECANDIDATE, + new CandidateMsg(candidate_copy)); +} + +void PeerConnection::OnIceComplete() { + signaling_thread()->Post(this, MSG_ICECOMPLETE); +} + +void PeerConnection::ChangeSignalingState( + PeerConnectionInterface::SignalingState signaling_state) { + signaling_state_ = signaling_state; + if (signaling_state == kClosed) { + ice_connection_state_ = kIceConnectionClosed; + observer_->OnIceConnectionChange(ice_connection_state_); + if (ice_gathering_state_ != kIceGatheringComplete) { + ice_gathering_state_ = kIceGatheringComplete; + observer_->OnIceGatheringChange(ice_gathering_state_); + } + } + observer_->OnSignalingChange(signaling_state_); + observer_->OnStateChange(PeerConnectionObserver::kSignalingState); +} + +} // namespace webrtc diff --git a/talk/app/webrtc/peerconnection.h b/talk/app/webrtc/peerconnection.h new file mode 100644 index 000000000..28aa9d82c --- /dev/null +++ b/talk/app/webrtc/peerconnection.h @@ -0,0 +1,192 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_PEERCONNECTION_H_ +#define TALK_APP_WEBRTC_PEERCONNECTION_H_ + +#include + +#include "talk/app/webrtc/mediastreamsignaling.h" +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/app/webrtc/peerconnectionfactory.h" +#include "talk/app/webrtc/statscollector.h" +#include "talk/app/webrtc/streamcollection.h" +#include "talk/app/webrtc/webrtcsession.h" +#include "talk/base/scoped_ptr.h" + +namespace webrtc { +class MediaStreamHandlerContainer; + +typedef std::vector + StunConfigurations; +typedef std::vector + TurnConfigurations; + +// PeerConnectionImpl implements the PeerConnection interface. +// It uses MediaStreamSignaling and WebRtcSession to implement +// the PeerConnection functionality. +class PeerConnection : public PeerConnectionInterface, + public MediaStreamSignalingObserver, + public IceObserver, + public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + explicit PeerConnection(PeerConnectionFactory* factory); + + bool Initialize(const PeerConnectionInterface::IceServers& configuration, + const MediaConstraintsInterface* constraints, + webrtc::PortAllocatorFactoryInterface* allocator_factory, + PeerConnectionObserver* observer); + virtual talk_base::scoped_refptr local_streams(); + virtual talk_base::scoped_refptr remote_streams(); + virtual bool AddStream(MediaStreamInterface* local_stream, + const MediaConstraintsInterface* constraints); + virtual void RemoveStream(MediaStreamInterface* local_stream); + + virtual talk_base::scoped_refptr CreateDtmfSender( + AudioTrackInterface* track); + + virtual talk_base::scoped_refptr CreateDataChannel( + const std::string& label, + const DataChannelInit* config); + virtual bool GetStats(StatsObserver* observer, + webrtc::MediaStreamTrackInterface* track); + + virtual SignalingState signaling_state(); + + // TODO(bemasc): Remove ice_state() when callers are removed. + virtual IceState ice_state(); + virtual IceConnectionState ice_connection_state(); + virtual IceGatheringState ice_gathering_state(); + + virtual const SessionDescriptionInterface* local_description() const; + virtual const SessionDescriptionInterface* remote_description() const; + + // JSEP01 + virtual void CreateOffer(CreateSessionDescriptionObserver* observer, + const MediaConstraintsInterface* constraints); + virtual void CreateAnswer(CreateSessionDescriptionObserver* observer, + const MediaConstraintsInterface* constraints); + virtual void SetLocalDescription(SetSessionDescriptionObserver* observer, + SessionDescriptionInterface* desc); + virtual void SetRemoteDescription(SetSessionDescriptionObserver* observer, + SessionDescriptionInterface* desc); + virtual bool UpdateIce(const IceServers& configuration, + const MediaConstraintsInterface* constraints); + virtual bool AddIceCandidate(const IceCandidateInterface* candidate); + + virtual void Close(); + + protected: + virtual ~PeerConnection(); + + private: + // Implements MessageHandler. + virtual void OnMessage(talk_base::Message* msg); + + // Implements MediaStreamSignalingObserver. + virtual void OnAddRemoteStream(MediaStreamInterface* stream) OVERRIDE; + virtual void OnRemoveRemoteStream(MediaStreamInterface* stream) OVERRIDE; + virtual void OnAddDataChannel(DataChannelInterface* data_channel) OVERRIDE; + virtual void OnAddRemoteAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track, + uint32 ssrc) OVERRIDE; + virtual void OnAddRemoteVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track, + uint32 ssrc) OVERRIDE; + virtual void OnRemoveRemoteAudioTrack( + MediaStreamInterface* stream, + AudioTrackInterface* audio_track) OVERRIDE; + virtual void OnRemoveRemoteVideoTrack( + MediaStreamInterface* stream, + VideoTrackInterface* video_track) OVERRIDE; + virtual void OnAddLocalAudioTrack(MediaStreamInterface* stream, + AudioTrackInterface* audio_track, + uint32 ssrc) OVERRIDE; + virtual void OnAddLocalVideoTrack(MediaStreamInterface* stream, + VideoTrackInterface* video_track, + uint32 ssrc) OVERRIDE; + virtual void OnRemoveLocalAudioTrack( + MediaStreamInterface* stream, + AudioTrackInterface* audio_track) OVERRIDE; + virtual void OnRemoveLocalVideoTrack( + MediaStreamInterface* stream, + VideoTrackInterface* video_track) OVERRIDE; + virtual void OnRemoveLocalStream(MediaStreamInterface* stream); + + // Implements IceObserver + virtual void OnIceConnectionChange(IceConnectionState new_state); + virtual void OnIceGatheringChange(IceGatheringState new_state); + virtual void OnIceCandidate(const IceCandidateInterface* candidate); + virtual void OnIceComplete(); + + // Signals from WebRtcSession. + void OnSessionStateChange(cricket::BaseSession* session, + cricket::BaseSession::State state); + void ChangeSignalingState(SignalingState signaling_state); + + bool DoInitialize(const StunConfigurations& stun_config, + const TurnConfigurations& turn_config, + const MediaConstraintsInterface* constraints, + webrtc::PortAllocatorFactoryInterface* allocator_factory, + PeerConnectionObserver* observer); + + talk_base::Thread* signaling_thread() const { + return factory_->signaling_thread(); + } + + void PostSetSessionDescriptionFailure(SetSessionDescriptionObserver* observer, + const std::string& error); + + bool IsClosed() const { + return signaling_state_ == PeerConnectionInterface::kClosed; + } + + // Storing the factory as a scoped reference pointer ensures that the memory + // in the PeerConnectionFactoryImpl remains available as long as the + // PeerConnection is running. It is passed to PeerConnection as a raw pointer. + // However, since the reference counting is done in the + // PeerConnectionFactoryInteface all instances created using the raw pointer + // will refer to the same reference count. + talk_base::scoped_refptr factory_; + PeerConnectionObserver* observer_; + SignalingState signaling_state_; + // TODO(bemasc): Remove ice_state_. + IceState ice_state_; + IceConnectionState ice_connection_state_; + IceGatheringState ice_gathering_state_; + + talk_base::scoped_ptr port_allocator_; + talk_base::scoped_ptr session_; + talk_base::scoped_ptr mediastream_signaling_; + talk_base::scoped_ptr stream_handler_container_; + StatsCollector stats_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_PEERCONNECTION_H_ diff --git a/talk/app/webrtc/peerconnection_unittest.cc b/talk/app/webrtc/peerconnection_unittest.cc new file mode 100644 index 000000000..96a9c1cd4 --- /dev/null +++ b/talk/app/webrtc/peerconnection_unittest.cc @@ -0,0 +1,1374 @@ +/* + * libjingle + * Copyright 2012, 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 + +#include +#include +#include +#include + +#include "talk/app/webrtc/dtmfsender.h" +#include "talk/app/webrtc/fakeportallocatorfactory.h" +#include "talk/app/webrtc/localaudiosource.h" +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/peerconnectionfactory.h" +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/app/webrtc/test/fakeaudiocapturemodule.h" +#include "talk/app/webrtc/test/fakeconstraints.h" +#include "talk/app/webrtc/test/fakevideotrackrenderer.h" +#include "talk/app/webrtc/test/fakeperiodicvideocapturer.h" +#include "talk/app/webrtc/test/mockpeerconnectionobservers.h" +#include "talk/app/webrtc/videosourceinterface.h" +#include "talk/base/gunit.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/ssladapter.h" +#include "talk/base/sslstreamadapter.h" +#include "talk/base/thread.h" +#include "talk/media/webrtc/fakewebrtcvideoengine.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/sessiondescription.h" +#include "talk/session/media/mediasession.h" + +#define MAYBE_SKIP_TEST(feature) \ + if (!(feature())) { \ + LOG(LS_INFO) << "Feature disabled... skipping"; \ + return; \ + } + +using cricket::ContentInfo; +using cricket::FakeWebRtcVideoDecoder; +using cricket::FakeWebRtcVideoDecoderFactory; +using cricket::FakeWebRtcVideoEncoder; +using cricket::FakeWebRtcVideoEncoderFactory; +using cricket::MediaContentDescription; +using webrtc::DataBuffer; +using webrtc::DataChannelInterface; +using webrtc::DtmfSender; +using webrtc::DtmfSenderInterface; +using webrtc::DtmfSenderObserverInterface; +using webrtc::FakeConstraints; +using webrtc::MediaConstraintsInterface; +using webrtc::MediaStreamTrackInterface; +using webrtc::MockCreateSessionDescriptionObserver; +using webrtc::MockDataChannelObserver; +using webrtc::MockSetSessionDescriptionObserver; +using webrtc::MockStatsObserver; +using webrtc::SessionDescriptionInterface; +using webrtc::StreamCollectionInterface; + +static const int kMaxWaitMs = 1000; +static const int kMaxWaitForStatsMs = 3000; +static const int kMaxWaitForFramesMs = 5000; +static const int kEndAudioFrameCount = 3; +static const int kEndVideoFrameCount = 3; + +static const char kStreamLabelBase[] = "stream_label"; +static const char kVideoTrackLabelBase[] = "video_track"; +static const char kAudioTrackLabelBase[] = "audio_track"; +static const char kDataChannelLabel[] = "data_channel"; + +static void RemoveLinesFromSdp(const std::string& line_start, + std::string* sdp) { + const char kSdpLineEnd[] = "\r\n"; + size_t ssrc_pos = 0; + while ((ssrc_pos = sdp->find(line_start, ssrc_pos)) != + std::string::npos) { + size_t end_ssrc = sdp->find(kSdpLineEnd, ssrc_pos); + sdp->erase(ssrc_pos, end_ssrc - ssrc_pos + strlen(kSdpLineEnd)); + } +} + +class SignalingMessageReceiver { + public: + protected: + SignalingMessageReceiver() {} + virtual ~SignalingMessageReceiver() {} +}; + +class JsepMessageReceiver : public SignalingMessageReceiver { + public: + virtual void ReceiveSdpMessage(const std::string& type, + std::string& msg) = 0; + virtual void ReceiveIceMessage(const std::string& sdp_mid, + int sdp_mline_index, + const std::string& msg) = 0; + + protected: + JsepMessageReceiver() {} + virtual ~JsepMessageReceiver() {} +}; + +template +class PeerConnectionTestClientBase + : public webrtc::PeerConnectionObserver, + public MessageReceiver { + public: + ~PeerConnectionTestClientBase() { + while (!fake_video_renderers_.empty()) { + RenderMap::iterator it = fake_video_renderers_.begin(); + delete it->second; + fake_video_renderers_.erase(it); + } + } + + virtual void Negotiate() = 0; + + virtual void Negotiate(bool audio, bool video) = 0; + + virtual void SetVideoConstraints( + const webrtc::FakeConstraints& video_constraint) { + video_constraints_ = video_constraint; + } + + void AddMediaStream(bool audio, bool video) { + std::string label = kStreamLabelBase + + talk_base::ToString(peer_connection_->local_streams()->count()); + talk_base::scoped_refptr stream = + peer_connection_factory_->CreateLocalMediaStream(label); + + if (audio && can_receive_audio()) { + FakeConstraints constraints; + // Disable highpass filter so that we can get all the test audio frames. + constraints.AddMandatory( + MediaConstraintsInterface::kHighpassFilter, false); + talk_base::scoped_refptr source = + webrtc::LocalAudioSource::Create(&constraints); + // TODO(perkj): Test audio source when it is implemented. Currently audio + // always use the default input. + talk_base::scoped_refptr audio_track( + peer_connection_factory_->CreateAudioTrack(kAudioTrackLabelBase, + source)); + stream->AddTrack(audio_track); + } + if (video && can_receive_video()) { + stream->AddTrack(CreateLocalVideoTrack(label)); + } + + EXPECT_TRUE(peer_connection_->AddStream(stream, NULL)); + } + + size_t NumberOfLocalMediaStreams() { + return peer_connection_->local_streams()->count(); + } + + bool SessionActive() { + return peer_connection_->signaling_state() == + webrtc::PeerConnectionInterface::kStable; + } + + void set_signaling_message_receiver( + MessageReceiver* signaling_message_receiver) { + signaling_message_receiver_ = signaling_message_receiver; + } + + void EnableVideoDecoderFactory() { + video_decoder_factory_enabled_ = true; + fake_video_decoder_factory_->AddSupportedVideoCodecType( + webrtc::kVideoCodecVP8); + } + + bool AudioFramesReceivedCheck(int number_of_frames) const { + return number_of_frames <= fake_audio_capture_module_->frames_received(); + } + + bool VideoFramesReceivedCheck(int number_of_frames) { + if (video_decoder_factory_enabled_) { + const std::vector& decoders + = fake_video_decoder_factory_->decoders(); + if (decoders.empty()) { + return number_of_frames <= 0; + } + + for (std::vector::const_iterator + it = decoders.begin(); it != decoders.end(); ++it) { + if (number_of_frames > (*it)->GetNumFramesReceived()) { + return false; + } + } + return true; + } else { + if (fake_video_renderers_.empty()) { + return number_of_frames <= 0; + } + + for (RenderMap::const_iterator it = fake_video_renderers_.begin(); + it != fake_video_renderers_.end(); ++it) { + if (number_of_frames > it->second->num_rendered_frames()) { + return false; + } + } + return true; + } + } + // Verify the CreateDtmfSender interface + void VerifyDtmf() { + talk_base::scoped_ptr observer(new DummyDtmfObserver()); + talk_base::scoped_refptr dtmf_sender; + + // We can't create a DTMF sender with an invalid audio track or a non local + // track. + EXPECT_TRUE(peer_connection_->CreateDtmfSender(NULL) == NULL); + talk_base::scoped_refptr non_localtrack( + peer_connection_factory_->CreateAudioTrack("dummy_track", + NULL)); + EXPECT_TRUE(peer_connection_->CreateDtmfSender(non_localtrack) == NULL); + + // We should be able to create a DTMF sender from a local track. + webrtc::AudioTrackInterface* localtrack = + peer_connection_->local_streams()->at(0)->GetAudioTracks()[0]; + dtmf_sender = peer_connection_->CreateDtmfSender(localtrack); + EXPECT_TRUE(dtmf_sender.get() != NULL); + dtmf_sender->RegisterObserver(observer.get()); + + // Test the DtmfSender object just created. + EXPECT_TRUE(dtmf_sender->CanInsertDtmf()); + EXPECT_TRUE(dtmf_sender->InsertDtmf("1a", 100, 50)); + + // We don't need to verify that the DTMF tones are actually sent out because + // that is already covered by the tests of the lower level components. + + EXPECT_TRUE_WAIT(observer->completed(), kMaxWaitMs); + std::vector tones; + tones.push_back("1"); + tones.push_back("a"); + tones.push_back(""); + observer->Verify(tones); + + dtmf_sender->UnregisterObserver(); + } + + // Verifies that the SessionDescription have rejected the appropriate media + // content. + void VerifyRejectedMediaInSessionDescription() { + ASSERT_TRUE(peer_connection_->remote_description() != NULL); + ASSERT_TRUE(peer_connection_->local_description() != NULL); + const cricket::SessionDescription* remote_desc = + peer_connection_->remote_description()->description(); + const cricket::SessionDescription* local_desc = + peer_connection_->local_description()->description(); + + const ContentInfo* remote_audio_content = GetFirstAudioContent(remote_desc); + if (remote_audio_content) { + const ContentInfo* audio_content = + GetFirstAudioContent(local_desc); + EXPECT_EQ(can_receive_audio(), !audio_content->rejected); + } + + const ContentInfo* remote_video_content = GetFirstVideoContent(remote_desc); + if (remote_video_content) { + const ContentInfo* video_content = + GetFirstVideoContent(local_desc); + EXPECT_EQ(can_receive_video(), !video_content->rejected); + } + } + + void SetExpectIceRestart(bool expect_restart) { + expect_ice_restart_ = expect_restart; + } + + bool ExpectIceRestart() const { return expect_ice_restart_; } + + void VerifyLocalIceUfragAndPassword() { + ASSERT_TRUE(peer_connection_->local_description() != NULL); + const cricket::SessionDescription* desc = + peer_connection_->local_description()->description(); + const cricket::ContentInfos& contents = desc->contents(); + + for (size_t index = 0; index < contents.size(); ++index) { + if (contents[index].rejected) + continue; + const cricket::TransportDescription* transport_desc = + desc->GetTransportDescriptionByName(contents[index].name); + + std::map::const_iterator ufragpair_it = + ice_ufrag_pwd_.find(index); + if (ufragpair_it == ice_ufrag_pwd_.end()) { + ASSERT_FALSE(ExpectIceRestart()); + ice_ufrag_pwd_[index] = IceUfragPwdPair(transport_desc->ice_ufrag, + transport_desc->ice_pwd); + } else if (ExpectIceRestart()) { + const IceUfragPwdPair& ufrag_pwd = ufragpair_it->second; + EXPECT_NE(ufrag_pwd.first, transport_desc->ice_ufrag); + EXPECT_NE(ufrag_pwd.second, transport_desc->ice_pwd); + } else { + const IceUfragPwdPair& ufrag_pwd = ufragpair_it->second; + EXPECT_EQ(ufrag_pwd.first, transport_desc->ice_ufrag); + EXPECT_EQ(ufrag_pwd.second, transport_desc->ice_pwd); + } + } + } + + int GetAudioOutputLevelStats(webrtc::MediaStreamTrackInterface* track) { + talk_base::scoped_refptr + observer(new talk_base::RefCountedObject()); + EXPECT_TRUE(peer_connection_->GetStats(observer, track)); + EXPECT_TRUE_WAIT(observer->called(), kMaxWaitMs); + return observer->AudioOutputLevel(); + } + + int GetAudioInputLevelStats() { + talk_base::scoped_refptr + observer(new talk_base::RefCountedObject()); + EXPECT_TRUE(peer_connection_->GetStats(observer, NULL)); + EXPECT_TRUE_WAIT(observer->called(), kMaxWaitMs); + return observer->AudioInputLevel(); + } + + int GetBytesReceivedStats(webrtc::MediaStreamTrackInterface* track) { + talk_base::scoped_refptr + observer(new talk_base::RefCountedObject()); + EXPECT_TRUE(peer_connection_->GetStats(observer, track)); + EXPECT_TRUE_WAIT(observer->called(), kMaxWaitMs); + return observer->BytesReceived(); + } + + int GetBytesSentStats(webrtc::MediaStreamTrackInterface* track) { + talk_base::scoped_refptr + observer(new talk_base::RefCountedObject()); + EXPECT_TRUE(peer_connection_->GetStats(observer, track)); + EXPECT_TRUE_WAIT(observer->called(), kMaxWaitMs); + return observer->BytesSent(); + } + + int rendered_width() { + EXPECT_FALSE(fake_video_renderers_.empty()); + return fake_video_renderers_.empty() ? 1 : + fake_video_renderers_.begin()->second->width(); + } + + int rendered_height() { + EXPECT_FALSE(fake_video_renderers_.empty()); + return fake_video_renderers_.empty() ? 1 : + fake_video_renderers_.begin()->second->height(); + } + + size_t number_of_remote_streams() { + if (!pc()) + return 0; + return pc()->remote_streams()->count(); + } + + StreamCollectionInterface* remote_streams() { + if (!pc()) { + ADD_FAILURE(); + return NULL; + } + return pc()->remote_streams(); + } + + StreamCollectionInterface* local_streams() { + if (!pc()) { + ADD_FAILURE(); + return NULL; + } + return pc()->local_streams(); + } + + webrtc::PeerConnectionInterface::SignalingState signaling_state() { + return pc()->signaling_state(); + } + + webrtc::PeerConnectionInterface::IceConnectionState ice_connection_state() { + return pc()->ice_connection_state(); + } + + webrtc::PeerConnectionInterface::IceGatheringState ice_gathering_state() { + return pc()->ice_gathering_state(); + } + + // PeerConnectionObserver callbacks. + virtual void OnError() {} + virtual void OnMessage(const std::string&) {} + virtual void OnSignalingMessage(const std::string& /*msg*/) {} + virtual void OnSignalingChange( + webrtc::PeerConnectionInterface::SignalingState new_state) { + EXPECT_EQ(peer_connection_->signaling_state(), new_state); + } + virtual void OnAddStream(webrtc::MediaStreamInterface* media_stream) { + for (size_t i = 0; i < media_stream->GetVideoTracks().size(); ++i) { + const std::string id = media_stream->GetVideoTracks()[i]->id(); + ASSERT_TRUE(fake_video_renderers_.find(id) == + fake_video_renderers_.end()); + fake_video_renderers_[id] = new webrtc::FakeVideoTrackRenderer( + media_stream->GetVideoTracks()[i]); + } + } + virtual void OnRemoveStream(webrtc::MediaStreamInterface* media_stream) {} + virtual void OnRenegotiationNeeded() {} + virtual void OnIceConnectionChange( + webrtc::PeerConnectionInterface::IceConnectionState new_state) { + EXPECT_EQ(peer_connection_->ice_connection_state(), new_state); + } + virtual void OnIceGatheringChange( + webrtc::PeerConnectionInterface::IceGatheringState new_state) { + EXPECT_EQ(peer_connection_->ice_gathering_state(), new_state); + } + virtual void OnIceCandidate( + const webrtc::IceCandidateInterface* /*candidate*/) {} + + webrtc::PeerConnectionInterface* pc() { + return peer_connection_.get(); + } + + protected: + explicit PeerConnectionTestClientBase(const std::string& id) + : id_(id), + expect_ice_restart_(false), + fake_video_decoder_factory_(NULL), + fake_video_encoder_factory_(NULL), + video_decoder_factory_enabled_(false), + signaling_message_receiver_(NULL) { + } + bool Init(const MediaConstraintsInterface* constraints) { + EXPECT_TRUE(!peer_connection_); + EXPECT_TRUE(!peer_connection_factory_); + allocator_factory_ = webrtc::FakePortAllocatorFactory::Create(); + if (!allocator_factory_) { + return false; + } + audio_thread_.Start(); + fake_audio_capture_module_ = FakeAudioCaptureModule::Create( + &audio_thread_); + + if (fake_audio_capture_module_ == NULL) { + return false; + } + fake_video_decoder_factory_ = new FakeWebRtcVideoDecoderFactory(); + fake_video_encoder_factory_ = new FakeWebRtcVideoEncoderFactory(); + peer_connection_factory_ = webrtc::CreatePeerConnectionFactory( + talk_base::Thread::Current(), talk_base::Thread::Current(), + fake_audio_capture_module_, fake_video_encoder_factory_, + fake_video_decoder_factory_); + if (!peer_connection_factory_) { + return false; + } + peer_connection_ = CreatePeerConnection(allocator_factory_.get(), + constraints); + return peer_connection_.get() != NULL; + } + virtual talk_base::scoped_refptr + CreatePeerConnection(webrtc::PortAllocatorFactoryInterface* factory, + const MediaConstraintsInterface* constraints) = 0; + MessageReceiver* signaling_message_receiver() { + return signaling_message_receiver_; + } + webrtc::PeerConnectionFactoryInterface* peer_connection_factory() { + return peer_connection_factory_.get(); + } + + virtual bool can_receive_audio() = 0; + virtual bool can_receive_video() = 0; + const std::string& id() const { return id_; } + + private: + class DummyDtmfObserver : public DtmfSenderObserverInterface { + public: + DummyDtmfObserver() : completed_(false) {} + + // Implements DtmfSenderObserverInterface. + void OnToneChange(const std::string& tone) { + tones_.push_back(tone); + if (tone.empty()) { + completed_ = true; + } + } + + void Verify(const std::vector& tones) const { + ASSERT_TRUE(tones_.size() == tones.size()); + EXPECT_TRUE(std::equal(tones.begin(), tones.end(), tones_.begin())); + } + + bool completed() const { return completed_; } + + private: + bool completed_; + std::vector tones_; + }; + + talk_base::scoped_refptr + CreateLocalVideoTrack(const std::string stream_label) { + // Set max frame rate to 10fps to reduce the risk of the tests to be flaky. + FakeConstraints source_constraints = video_constraints_; + source_constraints.SetMandatoryMaxFrameRate(10); + + talk_base::scoped_refptr source = + peer_connection_factory_->CreateVideoSource( + new webrtc::FakePeriodicVideoCapturer(), + &source_constraints); + std::string label = stream_label + kVideoTrackLabelBase; + return peer_connection_factory_->CreateVideoTrack(label, source); + } + + std::string id_; + // Separate thread for executing |fake_audio_capture_module_| tasks. Audio + // processing must not be performed on the same thread as signaling due to + // signaling time constraints and relative complexity of the audio pipeline. + // This is consistent with the video pipeline that us a a separate thread for + // encoding and decoding. + talk_base::Thread audio_thread_; + + talk_base::scoped_refptr + allocator_factory_; + talk_base::scoped_refptr peer_connection_; + talk_base::scoped_refptr + peer_connection_factory_; + + typedef std::pair IceUfragPwdPair; + std::map ice_ufrag_pwd_; + bool expect_ice_restart_; + + // Needed to keep track of number of frames send. + talk_base::scoped_refptr fake_audio_capture_module_; + // Needed to keep track of number of frames received. + typedef std::map RenderMap; + RenderMap fake_video_renderers_; + // Needed to keep track of number of frames received when external decoder + // used. + FakeWebRtcVideoDecoderFactory* fake_video_decoder_factory_; + FakeWebRtcVideoEncoderFactory* fake_video_encoder_factory_; + bool video_decoder_factory_enabled_; + webrtc::FakeConstraints video_constraints_; + + // For remote peer communication. + MessageReceiver* signaling_message_receiver_; +}; + +class JsepTestClient + : public PeerConnectionTestClientBase { + public: + static JsepTestClient* CreateClient( + const std::string& id, + const MediaConstraintsInterface* constraints) { + JsepTestClient* client(new JsepTestClient(id)); + if (!client->Init(constraints)) { + delete client; + return NULL; + } + return client; + } + ~JsepTestClient() {} + + virtual void Negotiate() { + Negotiate(true, true); + } + virtual void Negotiate(bool audio, bool video) { + talk_base::scoped_ptr offer; + EXPECT_TRUE(DoCreateOffer(offer.use())); + + if (offer->description()->GetContentByName("audio")) { + offer->description()->GetContentByName("audio")->rejected = !audio; + } + if (offer->description()->GetContentByName("video")) { + offer->description()->GetContentByName("video")->rejected = !video; + } + + std::string sdp; + EXPECT_TRUE(offer->ToString(&sdp)); + EXPECT_TRUE(DoSetLocalDescription(offer.release())); + signaling_message_receiver()->ReceiveSdpMessage( + webrtc::SessionDescriptionInterface::kOffer, sdp); + } + // JsepMessageReceiver callback. + virtual void ReceiveSdpMessage(const std::string& type, + std::string& msg) { + FilterIncomingSdpMessage(&msg); + if (type == webrtc::SessionDescriptionInterface::kOffer) { + HandleIncomingOffer(msg); + } else { + HandleIncomingAnswer(msg); + } + } + // JsepMessageReceiver callback. + virtual void ReceiveIceMessage(const std::string& sdp_mid, + int sdp_mline_index, + const std::string& msg) { + LOG(INFO) << id() << "ReceiveIceMessage"; + talk_base::scoped_ptr candidate( + webrtc::CreateIceCandidate(sdp_mid, sdp_mline_index, msg, NULL)); + EXPECT_TRUE(pc()->AddIceCandidate(candidate.get())); + } + // Implements PeerConnectionObserver functions needed by Jsep. + virtual void OnIceCandidate(const webrtc::IceCandidateInterface* candidate) { + LOG(INFO) << id() << "OnIceCandidate"; + + std::string ice_sdp; + EXPECT_TRUE(candidate->ToString(&ice_sdp)); + if (signaling_message_receiver() == NULL) { + // Remote party may be deleted. + return; + } + signaling_message_receiver()->ReceiveIceMessage(candidate->sdp_mid(), + candidate->sdp_mline_index(), ice_sdp); + } + + void IceRestart() { + session_description_constraints_.SetMandatoryIceRestart(true); + SetExpectIceRestart(true); + } + + void SetReceiveAudioVideo(bool audio, bool video) { + session_description_constraints_.SetMandatoryReceiveAudio(audio); + session_description_constraints_.SetMandatoryReceiveVideo(video); + ASSERT_EQ(audio, can_receive_audio()); + ASSERT_EQ(video, can_receive_video()); + } + + void RemoveMsidFromReceivedSdp(bool remove) { + remove_msid_ = remove; + } + + void RemoveSdesCryptoFromReceivedSdp(bool remove) { + remove_sdes_ = remove; + } + + void RemoveBundleFromReceivedSdp(bool remove) { + remove_bundle_ = remove; + } + + virtual bool can_receive_audio() { + bool value; + if (webrtc::FindConstraint(&session_description_constraints_, + MediaConstraintsInterface::kOfferToReceiveAudio, &value, NULL)) { + return value; + } + return true; + } + + virtual bool can_receive_video() { + bool value; + if (webrtc::FindConstraint(&session_description_constraints_, + MediaConstraintsInterface::kOfferToReceiveVideo, &value, NULL)) { + return value; + } + return true; + } + + virtual void OnIceComplete() { + LOG(INFO) << id() << "OnIceComplete"; + } + + virtual void OnDataChannel(DataChannelInterface* data_channel) { + LOG(INFO) << id() << "OnDataChannel"; + data_channel_ = data_channel; + data_observer_.reset(new MockDataChannelObserver(data_channel)); + } + + void CreateDataChannel() { + data_channel_ = pc()->CreateDataChannel(kDataChannelLabel, + NULL); + ASSERT_TRUE(data_channel_.get() != NULL); + data_observer_.reset(new MockDataChannelObserver(data_channel_)); + } + + DataChannelInterface* data_channel() { return data_channel_; } + const MockDataChannelObserver* data_observer() const { + return data_observer_.get(); + } + + protected: + explicit JsepTestClient(const std::string& id) + : PeerConnectionTestClientBase(id), + remove_msid_(false), + remove_bundle_(false), + remove_sdes_(false) { + } + + virtual talk_base::scoped_refptr + CreatePeerConnection(webrtc::PortAllocatorFactoryInterface* factory, + const MediaConstraintsInterface* constraints) { + // CreatePeerConnection with IceServers. + webrtc::PeerConnectionInterface::IceServers ice_servers; + webrtc::PeerConnectionInterface::IceServer ice_server; + ice_server.uri = "stun:stun.l.google.com:19302"; + ice_servers.push_back(ice_server); + return peer_connection_factory()->CreatePeerConnection( + ice_servers, constraints, factory, NULL, this); + } + + void HandleIncomingOffer(const std::string& msg) { + LOG(INFO) << id() << "HandleIncomingOffer "; + if (NumberOfLocalMediaStreams() == 0) { + // If we are not sending any streams ourselves it is time to add some. + AddMediaStream(true, true); + } + talk_base::scoped_ptr desc( + webrtc::CreateSessionDescription("offer", msg, NULL)); + EXPECT_TRUE(DoSetRemoteDescription(desc.release())); + talk_base::scoped_ptr answer; + EXPECT_TRUE(DoCreateAnswer(answer.use())); + std::string sdp; + EXPECT_TRUE(answer->ToString(&sdp)); + EXPECT_TRUE(DoSetLocalDescription(answer.release())); + if (signaling_message_receiver()) { + signaling_message_receiver()->ReceiveSdpMessage( + webrtc::SessionDescriptionInterface::kAnswer, sdp); + } + } + + void HandleIncomingAnswer(const std::string& msg) { + LOG(INFO) << id() << "HandleIncomingAnswer"; + talk_base::scoped_ptr desc( + webrtc::CreateSessionDescription("answer", msg, NULL)); + EXPECT_TRUE(DoSetRemoteDescription(desc.release())); + } + + bool DoCreateOfferAnswer(SessionDescriptionInterface** desc, + bool offer) { + talk_base::scoped_refptr + observer(new talk_base::RefCountedObject< + MockCreateSessionDescriptionObserver>()); + if (offer) { + pc()->CreateOffer(observer, &session_description_constraints_); + } else { + pc()->CreateAnswer(observer, &session_description_constraints_); + } + EXPECT_EQ_WAIT(true, observer->called(), kMaxWaitMs); + *desc = observer->release_desc(); + if (observer->result() && ExpectIceRestart()) { + EXPECT_EQ(0u, (*desc)->candidates(0)->count()); + } + return observer->result(); + } + + bool DoCreateOffer(SessionDescriptionInterface** desc) { + return DoCreateOfferAnswer(desc, true); + } + + bool DoCreateAnswer(SessionDescriptionInterface** desc) { + return DoCreateOfferAnswer(desc, false); + } + + bool DoSetLocalDescription(SessionDescriptionInterface* desc) { + talk_base::scoped_refptr + observer(new talk_base::RefCountedObject< + MockSetSessionDescriptionObserver>()); + LOG(INFO) << id() << "SetLocalDescription "; + pc()->SetLocalDescription(observer, desc); + // Ignore the observer result. If we wait for the result with + // EXPECT_TRUE_WAIT, local ice candidates might be sent to the remote peer + // before the offer which is an error. + // The reason is that EXPECT_TRUE_WAIT uses + // talk_base::Thread::Current()->ProcessMessages(1); + // ProcessMessages waits at least 1ms but processes all messages before + // returning. Since this test is synchronous and send messages to the remote + // peer whenever a callback is invoked, this can lead to messages being + // sent to the remote peer in the wrong order. + // TODO(perkj): Find a way to check the result without risking that the + // order of sent messages are changed. Ex- by posting all messages that are + // sent to the remote peer. + return true; + } + + bool DoSetRemoteDescription(SessionDescriptionInterface* desc) { + talk_base::scoped_refptr + observer(new talk_base::RefCountedObject< + MockSetSessionDescriptionObserver>()); + LOG(INFO) << id() << "SetRemoteDescription "; + pc()->SetRemoteDescription(observer, desc); + EXPECT_TRUE_WAIT(observer->called(), kMaxWaitMs); + return observer->result(); + } + + // This modifies all received SDP messages before they are processed. + void FilterIncomingSdpMessage(std::string* sdp) { + if (remove_msid_) { + const char kSdpSsrcAttribute[] = "a=ssrc:"; + RemoveLinesFromSdp(kSdpSsrcAttribute, sdp); + const char kSdpMsidSupportedAttribute[] = "a=msid-semantic:"; + RemoveLinesFromSdp(kSdpMsidSupportedAttribute, sdp); + } + if (remove_bundle_) { + const char kSdpBundleAttribute[] = "a=group:BUNDLE"; + RemoveLinesFromSdp(kSdpBundleAttribute, sdp); + } + if (remove_sdes_) { + const char kSdpSdesCryptoAttribute[] = "a=crypto"; + RemoveLinesFromSdp(kSdpSdesCryptoAttribute, sdp); + } + } + + private: + webrtc::FakeConstraints session_description_constraints_; + bool remove_msid_; // True if MSID should be removed in received SDP. + bool remove_bundle_; // True if bundle should be removed in received SDP. + bool remove_sdes_; // True if a=crypto should be removed in received SDP. + + talk_base::scoped_refptr data_channel_; + talk_base::scoped_ptr data_observer_; +}; + +template +class P2PTestConductor : public testing::Test { + public: + bool SessionActive() { + return initiating_client_->SessionActive() && + receiving_client_->SessionActive(); + } + // Return true if the number of frames provided have been received or it is + // known that that will never occur (e.g. no frames will be sent or + // captured). + bool FramesNotPending(int audio_frames_to_receive, + int video_frames_to_receive) { + return VideoFramesReceivedCheck(video_frames_to_receive) && + AudioFramesReceivedCheck(audio_frames_to_receive); + } + bool AudioFramesReceivedCheck(int frames_received) { + return initiating_client_->AudioFramesReceivedCheck(frames_received) && + receiving_client_->AudioFramesReceivedCheck(frames_received); + } + bool VideoFramesReceivedCheck(int frames_received) { + return initiating_client_->VideoFramesReceivedCheck(frames_received) && + receiving_client_->VideoFramesReceivedCheck(frames_received); + } + void VerifyDtmf() { + initiating_client_->VerifyDtmf(); + receiving_client_->VerifyDtmf(); + } + + void TestUpdateOfferWithRejectedContent() { + initiating_client_->Negotiate(true, false); + EXPECT_TRUE_WAIT( + FramesNotPending(kEndAudioFrameCount * 2, kEndVideoFrameCount), + kMaxWaitForFramesMs); + // There shouldn't be any more video frame after the new offer is + // negotiated. + EXPECT_FALSE(VideoFramesReceivedCheck(kEndVideoFrameCount + 1)); + } + + void VerifyRenderedSize(int width, int height) { + EXPECT_EQ(width, receiving_client()->rendered_width()); + EXPECT_EQ(height, receiving_client()->rendered_height()); + EXPECT_EQ(width, initializing_client()->rendered_width()); + EXPECT_EQ(height, initializing_client()->rendered_height()); + } + + void VerifySessionDescriptions() { + initiating_client_->VerifyRejectedMediaInSessionDescription(); + receiving_client_->VerifyRejectedMediaInSessionDescription(); + initiating_client_->VerifyLocalIceUfragAndPassword(); + receiving_client_->VerifyLocalIceUfragAndPassword(); + } + + P2PTestConductor() { + talk_base::InitializeSSL(NULL); + } + ~P2PTestConductor() { + if (initiating_client_) { + initiating_client_->set_signaling_message_receiver(NULL); + } + if (receiving_client_) { + receiving_client_->set_signaling_message_receiver(NULL); + } + } + + bool CreateTestClients() { + return CreateTestClients(NULL, NULL); + } + + bool CreateTestClients(MediaConstraintsInterface* init_constraints, + MediaConstraintsInterface* recv_constraints) { + initiating_client_.reset(SignalingClass::CreateClient("Caller: ", + init_constraints)); + receiving_client_.reset(SignalingClass::CreateClient("Callee: ", + recv_constraints)); + if (!initiating_client_ || !receiving_client_) { + return false; + } + initiating_client_->set_signaling_message_receiver(receiving_client_.get()); + receiving_client_->set_signaling_message_receiver(initiating_client_.get()); + return true; + } + + void SetVideoConstraints(const webrtc::FakeConstraints& init_constraints, + const webrtc::FakeConstraints& recv_constraints) { + initiating_client_->SetVideoConstraints(init_constraints); + receiving_client_->SetVideoConstraints(recv_constraints); + } + + void EnableVideoDecoderFactory() { + initiating_client_->EnableVideoDecoderFactory(); + receiving_client_->EnableVideoDecoderFactory(); + } + + // This test sets up a call between two parties. Both parties send static + // frames to each other. Once the test is finished the number of sent frames + // is compared to the number of received frames. + void LocalP2PTest() { + if (initiating_client_->NumberOfLocalMediaStreams() == 0) { + initiating_client_->AddMediaStream(true, true); + } + initiating_client_->Negotiate(); + const int kMaxWaitForActivationMs = 5000; + // Assert true is used here since next tests are guaranteed to fail and + // would eat up 5 seconds. + ASSERT_TRUE_WAIT(SessionActive(), kMaxWaitForActivationMs); + VerifySessionDescriptions(); + + + int audio_frame_count = kEndAudioFrameCount; + // TODO(ronghuawu): Add test to cover the case of sendonly and recvonly. + if (!initiating_client_->can_receive_audio() || + !receiving_client_->can_receive_audio()) { + audio_frame_count = -1; + } + int video_frame_count = kEndVideoFrameCount; + if (!initiating_client_->can_receive_video() || + !receiving_client_->can_receive_video()) { + video_frame_count = -1; + } + + if (audio_frame_count != -1 || video_frame_count != -1) { + // Audio or video is expected to flow, so both sides should get to the + // Connected state. + // Note: These tests have been observed to fail under heavy load at + // shorter timeouts, so they may be flaky. + EXPECT_EQ_WAIT( + webrtc::PeerConnectionInterface::kIceConnectionConnected, + initiating_client_->ice_connection_state(), + kMaxWaitForFramesMs); + EXPECT_EQ_WAIT( + webrtc::PeerConnectionInterface::kIceConnectionConnected, + receiving_client_->ice_connection_state(), + kMaxWaitForFramesMs); + } + + if (initiating_client_->can_receive_audio() || + initiating_client_->can_receive_video()) { + // The initiating client can receive media, so it must produce candidates + // that will serve as destinations for that media. + // TODO(bemasc): Understand why the state is not already Complete here, as + // seems to be the case for the receiving client. This may indicate a bug + // in the ICE gathering system. + EXPECT_NE(webrtc::PeerConnectionInterface::kIceGatheringNew, + initiating_client_->ice_gathering_state()); + } + if (receiving_client_->can_receive_audio() || + receiving_client_->can_receive_video()) { + EXPECT_EQ_WAIT(webrtc::PeerConnectionInterface::kIceGatheringComplete, + receiving_client_->ice_gathering_state(), + kMaxWaitForFramesMs); + } + + EXPECT_TRUE_WAIT(FramesNotPending(audio_frame_count, video_frame_count), + kMaxWaitForFramesMs); + } + + SignalingClass* initializing_client() { return initiating_client_.get(); } + SignalingClass* receiving_client() { return receiving_client_.get(); } + + private: + talk_base::scoped_ptr initiating_client_; + talk_base::scoped_ptr receiving_client_; +}; +typedef P2PTestConductor JsepPeerConnectionP2PTestClient; + +// This test sets up a Jsep call between two parties and test Dtmf. +TEST_F(JsepPeerConnectionP2PTestClient, LocalP2PTestDtmf) { + ASSERT_TRUE(CreateTestClients()); + LocalP2PTest(); + VerifyDtmf(); +} + +// This test sets up a Jsep call between two parties and test that we can get a +// video aspect ratio of 16:9. +TEST_F(JsepPeerConnectionP2PTestClient, LocalP2PTest16To9) { + ASSERT_TRUE(CreateTestClients()); + FakeConstraints constraint; + double requested_ratio = 640.0/360; + constraint.SetMandatoryMinAspectRatio(requested_ratio); + SetVideoConstraints(constraint, constraint); + LocalP2PTest(); + + ASSERT_LE(0, initializing_client()->rendered_height()); + double initiating_video_ratio = + static_cast (initializing_client()->rendered_width()) / + initializing_client()->rendered_height(); + EXPECT_LE(requested_ratio, initiating_video_ratio); + + ASSERT_LE(0, receiving_client()->rendered_height()); + double receiving_video_ratio = + static_cast (receiving_client()->rendered_width()) / + receiving_client()->rendered_height(); + EXPECT_LE(requested_ratio, receiving_video_ratio); +} + +// This test sets up a Jsep call between two parties and test that the +// received video has a resolution of 1280*720. +// TODO(mallinath): Enable when +// http://code.google.com/p/webrtc/issues/detail?id=981 is fixed. +TEST_F(JsepPeerConnectionP2PTestClient, DISABLED_LocalP2PTest1280By720) { + ASSERT_TRUE(CreateTestClients()); + FakeConstraints constraint; + constraint.SetMandatoryMinWidth(1280); + constraint.SetMandatoryMinHeight(720); + SetVideoConstraints(constraint, constraint); + LocalP2PTest(); + VerifyRenderedSize(1280, 720); +} + +// This test sets up a call between two endpoints that are configured to use +// DTLS key agreement. As a result, DTLS is negotiated and used for transport. +TEST_F(JsepPeerConnectionP2PTestClient, LocalP2PTestDtls) { + MAYBE_SKIP_TEST(talk_base::SSLStreamAdapter::HaveDtlsSrtp); + FakeConstraints setup_constraints; + setup_constraints.AddMandatory(MediaConstraintsInterface::kEnableDtlsSrtp, + true); + ASSERT_TRUE(CreateTestClients(&setup_constraints, &setup_constraints)); + LocalP2PTest(); + VerifyRenderedSize(640, 480); +} + +// This test sets up a call between an endpoint configured to use either SDES or +// DTLS (the offerer) and just SDES (the answerer). As a result, SDES is used +// instead of DTLS. +TEST_F(JsepPeerConnectionP2PTestClient, LocalP2PTestOfferDtlsToSdes) { + MAYBE_SKIP_TEST(talk_base::SSLStreamAdapter::HaveDtlsSrtp); + FakeConstraints setup_constraints; + setup_constraints.AddMandatory(MediaConstraintsInterface::kEnableDtlsSrtp, + true); + ASSERT_TRUE(CreateTestClients(&setup_constraints, NULL)); + LocalP2PTest(); + VerifyRenderedSize(640, 480); +} + +// This test sets up a call between an endpoint configured to use SDES +// (the offerer) and either SDES or DTLS (the answerer). As a result, SDES is +// used instead of DTLS. +TEST_F(JsepPeerConnectionP2PTestClient, LocalP2PTestOfferSdesToDtls) { + MAYBE_SKIP_TEST(talk_base::SSLStreamAdapter::HaveDtlsSrtp); + FakeConstraints setup_constraints; + setup_constraints.AddMandatory(MediaConstraintsInterface::kEnableDtlsSrtp, + true); + ASSERT_TRUE(CreateTestClients(NULL, &setup_constraints)); + LocalP2PTest(); + VerifyRenderedSize(640, 480); +} + +// This test sets up a call between two endpoints that are configured to use +// DTLS key agreement. The offerer don't support SDES. As a result, DTLS is +// negotiated and used for transport. +TEST_F(JsepPeerConnectionP2PTestClient, LocalP2PTestOfferDtlsButNotSdes) { + MAYBE_SKIP_TEST(talk_base::SSLStreamAdapter::HaveDtlsSrtp); + FakeConstraints setup_constraints; + setup_constraints.AddMandatory(MediaConstraintsInterface::kEnableDtlsSrtp, + true); + ASSERT_TRUE(CreateTestClients(&setup_constraints, &setup_constraints)); + receiving_client()->RemoveSdesCryptoFromReceivedSdp(true); + LocalP2PTest(); + VerifyRenderedSize(640, 480); +} + +// This test sets up a Jsep call between two parties, and the callee only +// accept to receive video. +TEST_F(JsepPeerConnectionP2PTestClient, LocalP2PTestAnswerVideo) { + ASSERT_TRUE(CreateTestClients()); + receiving_client()->SetReceiveAudioVideo(false, true); + LocalP2PTest(); +} + +// This test sets up a Jsep call between two parties, and the callee only +// accept to receive audio. +TEST_F(JsepPeerConnectionP2PTestClient, LocalP2PTestAnswerAudio) { + ASSERT_TRUE(CreateTestClients()); + receiving_client()->SetReceiveAudioVideo(true, false); + LocalP2PTest(); +} + +// This test sets up a Jsep call between two parties, and the callee reject both +// audio and video. +TEST_F(JsepPeerConnectionP2PTestClient, LocalP2PTestAnswerNone) { + ASSERT_TRUE(CreateTestClients()); + receiving_client()->SetReceiveAudioVideo(false, false); + LocalP2PTest(); +} + +// This test sets up an audio and video call between two parties. After the call +// runs for a while (10 frames), the caller sends an update offer with video +// being rejected. Once the re-negotiation is done, the video flow should stop +// and the audio flow should continue. +TEST_F(JsepPeerConnectionP2PTestClient, UpdateOfferWithRejectedContent) { + ASSERT_TRUE(CreateTestClients()); + LocalP2PTest(); + TestUpdateOfferWithRejectedContent(); +} + +// This test sets up a Jsep call between two parties. The MSID is removed from +// the SDP strings from the caller. +TEST_F(JsepPeerConnectionP2PTestClient, LocalP2PTestWithoutMsid) { + ASSERT_TRUE(CreateTestClients()); + receiving_client()->RemoveMsidFromReceivedSdp(true); + // TODO(perkj): Currently there is a bug that cause audio to stop playing if + // audio and video is muxed when MSID is disabled. Remove + // SetRemoveBundleFromSdp once + // https://code.google.com/p/webrtc/issues/detail?id=1193 is fixed. + receiving_client()->RemoveBundleFromReceivedSdp(true); + LocalP2PTest(); +} + +// This test sets up a Jsep call between two parties and the initiating peer +// sends two steams. +// TODO(perkj): Disabled due to +// https://code.google.com/p/webrtc/issues/detail?id=1454 +TEST_F(JsepPeerConnectionP2PTestClient, DISABLED_LocalP2PTestTwoStreams) { + ASSERT_TRUE(CreateTestClients()); + // Set optional video constraint to max 320pixels to decrease CPU usage. + FakeConstraints constraint; + constraint.SetOptionalMaxWidth(320); + SetVideoConstraints(constraint, constraint); + initializing_client()->AddMediaStream(true, true); + initializing_client()->AddMediaStream(false, true); + ASSERT_EQ(2u, initializing_client()->NumberOfLocalMediaStreams()); + LocalP2PTest(); + EXPECT_EQ(2u, receiving_client()->number_of_remote_streams()); +} + +// Test that we can receive the audio output level from a remote audio track. +TEST_F(JsepPeerConnectionP2PTestClient, GetAudioOutputLevelStats) { + ASSERT_TRUE(CreateTestClients()); + LocalP2PTest(); + + StreamCollectionInterface* remote_streams = + initializing_client()->remote_streams(); + ASSERT_GT(remote_streams->count(), 0u); + ASSERT_GT(remote_streams->at(0)->GetAudioTracks().size(), 0u); + MediaStreamTrackInterface* remote_audio_track = + remote_streams->at(0)->GetAudioTracks()[0]; + + // Get the audio output level stats. Note that the level is not available + // until a RTCP packet has been received. + EXPECT_TRUE_WAIT( + initializing_client()->GetAudioOutputLevelStats(remote_audio_track) > 0, + kMaxWaitForStatsMs); +} + +// Test that an audio input level is reported. +TEST_F(JsepPeerConnectionP2PTestClient, GetAudioInputLevelStats) { + ASSERT_TRUE(CreateTestClients()); + LocalP2PTest(); + + // Get the audio input level stats. The level should be available very + // soon after the test starts. + EXPECT_TRUE_WAIT(initializing_client()->GetAudioInputLevelStats() > 0, + kMaxWaitForStatsMs); +} + +// Test that we can get incoming byte counts from both audio and video tracks. +TEST_F(JsepPeerConnectionP2PTestClient, GetBytesReceivedStats) { + ASSERT_TRUE(CreateTestClients()); + LocalP2PTest(); + + StreamCollectionInterface* remote_streams = + initializing_client()->remote_streams(); + ASSERT_GT(remote_streams->count(), 0u); + ASSERT_GT(remote_streams->at(0)->GetAudioTracks().size(), 0u); + MediaStreamTrackInterface* remote_audio_track = + remote_streams->at(0)->GetAudioTracks()[0]; + EXPECT_TRUE_WAIT( + initializing_client()->GetBytesReceivedStats(remote_audio_track) > 0, + kMaxWaitForStatsMs); + + MediaStreamTrackInterface* remote_video_track = + remote_streams->at(0)->GetVideoTracks()[0]; + EXPECT_TRUE_WAIT( + initializing_client()->GetBytesReceivedStats(remote_video_track) > 0, + kMaxWaitForStatsMs); +} + +// Test that we can get outgoing byte counts from both audio and video tracks. +TEST_F(JsepPeerConnectionP2PTestClient, GetBytesSentStats) { + ASSERT_TRUE(CreateTestClients()); + LocalP2PTest(); + + StreamCollectionInterface* local_streams = + initializing_client()->local_streams(); + ASSERT_GT(local_streams->count(), 0u); + ASSERT_GT(local_streams->at(0)->GetAudioTracks().size(), 0u); + MediaStreamTrackInterface* local_audio_track = + local_streams->at(0)->GetAudioTracks()[0]; + EXPECT_TRUE_WAIT( + initializing_client()->GetBytesSentStats(local_audio_track) > 0, + kMaxWaitForStatsMs); + + MediaStreamTrackInterface* local_video_track = + local_streams->at(0)->GetVideoTracks()[0]; + EXPECT_TRUE_WAIT( + initializing_client()->GetBytesSentStats(local_video_track) > 0, + kMaxWaitForStatsMs); +} + +// This test sets up a call between two parties with audio, video and data. +TEST_F(JsepPeerConnectionP2PTestClient, LocalP2PTestDataChannel) { + FakeConstraints setup_constraints; + setup_constraints.SetAllowRtpDataChannels(); + ASSERT_TRUE(CreateTestClients(&setup_constraints, &setup_constraints)); + initializing_client()->CreateDataChannel(); + LocalP2PTest(); + ASSERT_TRUE(initializing_client()->data_channel() != NULL); + ASSERT_TRUE(receiving_client()->data_channel() != NULL); + EXPECT_TRUE_WAIT(initializing_client()->data_observer()->IsOpen(), + kMaxWaitMs); + EXPECT_TRUE_WAIT(receiving_client()->data_observer()->IsOpen(), + kMaxWaitMs); + + std::string data = "hello world"; + initializing_client()->data_channel()->Send(DataBuffer(data)); + EXPECT_EQ_WAIT(data, receiving_client()->data_observer()->last_message(), + kMaxWaitMs); + receiving_client()->data_channel()->Send(DataBuffer(data)); + EXPECT_EQ_WAIT(data, initializing_client()->data_observer()->last_message(), + kMaxWaitMs); + + receiving_client()->data_channel()->Close(); + // Send new offer and answer. + receiving_client()->Negotiate(); + EXPECT_FALSE(initializing_client()->data_observer()->IsOpen()); + EXPECT_FALSE(receiving_client()->data_observer()->IsOpen()); +} + +// This test sets up a call between two parties and creates a data channel. +// The test tests that received data is buffered unless an observer has been +// registered. +// Rtp data channels can receive data before the underlying +// transport has detected that a channel is writable and thus data can be +// received before the data channel state changes to open. That is hard to test +// but the same buffering is used in that case. +TEST_F(JsepPeerConnectionP2PTestClient, RegisterDataChannelObserver) { + FakeConstraints setup_constraints; + setup_constraints.SetAllowRtpDataChannels(); + ASSERT_TRUE(CreateTestClients(&setup_constraints, &setup_constraints)); + initializing_client()->CreateDataChannel(); + initializing_client()->Negotiate(); + + ASSERT_TRUE(initializing_client()->data_channel() != NULL); + ASSERT_TRUE(receiving_client()->data_channel() != NULL); + EXPECT_TRUE_WAIT(initializing_client()->data_observer()->IsOpen(), + kMaxWaitMs); + EXPECT_EQ_WAIT(DataChannelInterface::kOpen, + receiving_client()->data_channel()->state(), kMaxWaitMs); + + // Unregister the existing observer. + receiving_client()->data_channel()->UnregisterObserver(); + std::string data = "hello world"; + initializing_client()->data_channel()->Send(DataBuffer(data)); + // Wait a while to allow the sent data to arrive before an observer is + // registered.. + talk_base::Thread::Current()->ProcessMessages(100); + + MockDataChannelObserver new_observer(receiving_client()->data_channel()); + EXPECT_EQ_WAIT(data, new_observer.last_message(), kMaxWaitMs); +} + +// This test sets up a call between two parties with audio, video and but only +// the initiating client support data. +TEST_F(JsepPeerConnectionP2PTestClient, LocalP2PTestReceiverDoesntSupportData) { + FakeConstraints setup_constraints; + setup_constraints.SetAllowRtpDataChannels(); + ASSERT_TRUE(CreateTestClients(&setup_constraints, NULL)); + initializing_client()->CreateDataChannel(); + LocalP2PTest(); + EXPECT_TRUE(initializing_client()->data_channel() != NULL); + EXPECT_FALSE(receiving_client()->data_channel()); + EXPECT_FALSE(initializing_client()->data_observer()->IsOpen()); +} + +// This test sets up a call between two parties with audio, video. When audio +// and video is setup and flowing and data channel is negotiated. +TEST_F(JsepPeerConnectionP2PTestClient, AddDataChannelAfterRenegotiation) { + FakeConstraints setup_constraints; + setup_constraints.SetAllowRtpDataChannels(); + ASSERT_TRUE(CreateTestClients(&setup_constraints, &setup_constraints)); + LocalP2PTest(); + initializing_client()->CreateDataChannel(); + // Send new offer and answer. + initializing_client()->Negotiate(); + ASSERT_TRUE(initializing_client()->data_channel() != NULL); + ASSERT_TRUE(receiving_client()->data_channel() != NULL); + EXPECT_TRUE_WAIT(initializing_client()->data_observer()->IsOpen(), + kMaxWaitMs); + EXPECT_TRUE_WAIT(receiving_client()->data_observer()->IsOpen(), + kMaxWaitMs); +} + +// This test sets up a call between two parties with audio, and video. +// During the call, the initializing side restart ice and the test verifies that +// new ice candidates are generated and audio and video still can flow. +TEST_F(JsepPeerConnectionP2PTestClient, IceRestart) { + ASSERT_TRUE(CreateTestClients()); + + // Negotiate and wait for ice completion and make sure audio and video plays. + LocalP2PTest(); + + // Create a SDP string of the first audio candidate for both clients. + const webrtc::IceCandidateCollection* audio_candidates_initiator = + initializing_client()->pc()->local_description()->candidates(0); + const webrtc::IceCandidateCollection* audio_candidates_receiver = + receiving_client()->pc()->local_description()->candidates(0); + ASSERT_GT(audio_candidates_initiator->count(), 0u); + ASSERT_GT(audio_candidates_receiver->count(), 0u); + std::string initiator_candidate; + EXPECT_TRUE( + audio_candidates_initiator->at(0)->ToString(&initiator_candidate)); + std::string receiver_candidate; + EXPECT_TRUE(audio_candidates_receiver->at(0)->ToString(&receiver_candidate)); + + // Restart ice on the initializing client. + receiving_client()->SetExpectIceRestart(true); + initializing_client()->IceRestart(); + + // Negotiate and wait for ice completion again and make sure audio and video + // plays. + LocalP2PTest(); + + // Create a SDP string of the first audio candidate for both clients again. + const webrtc::IceCandidateCollection* audio_candidates_initiator_restart = + initializing_client()->pc()->local_description()->candidates(0); + const webrtc::IceCandidateCollection* audio_candidates_reciever_restart = + receiving_client()->pc()->local_description()->candidates(0); + ASSERT_GT(audio_candidates_initiator_restart->count(), 0u); + ASSERT_GT(audio_candidates_reciever_restart->count(), 0u); + std::string initiator_candidate_restart; + EXPECT_TRUE(audio_candidates_initiator_restart->at(0)->ToString( + &initiator_candidate_restart)); + std::string receiver_candidate_restart; + EXPECT_TRUE(audio_candidates_reciever_restart->at(0)->ToString( + &receiver_candidate_restart)); + + // Verify that the first candidates in the local session descriptions has + // changed. + EXPECT_NE(initiator_candidate, initiator_candidate_restart); + EXPECT_NE(receiver_candidate, receiver_candidate_restart); +} + + +// This test sets up a Jsep call between two parties with external +// VideoDecoderFactory. +TEST_F(JsepPeerConnectionP2PTestClient, LocalP2PTestWithVideoDecoderFactory) { + ASSERT_TRUE(CreateTestClients()); + EnableVideoDecoderFactory(); + LocalP2PTest(); +} diff --git a/talk/app/webrtc/peerconnectionfactory.cc b/talk/app/webrtc/peerconnectionfactory.cc new file mode 100644 index 000000000..7ae5a3b7a --- /dev/null +++ b/talk/app/webrtc/peerconnectionfactory.cc @@ -0,0 +1,369 @@ +/* + * libjingle + * Copyright 2004--2011, 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/app/webrtc/peerconnectionfactory.h" + +#include "talk/app/webrtc/audiotrack.h" +#include "talk/app/webrtc/localaudiosource.h" +#include "talk/app/webrtc/localvideosource.h" +#include "talk/app/webrtc/mediastreamproxy.h" +#include "talk/app/webrtc/mediastreamtrackproxy.h" +#include "talk/app/webrtc/peerconnection.h" +#include "talk/app/webrtc/peerconnectionproxy.h" +#include "talk/app/webrtc/portallocatorfactory.h" +#include "talk/app/webrtc/videosourceproxy.h" +#include "talk/app/webrtc/videotrack.h" +#include "talk/media/devices/dummydevicemanager.h" +#include "talk/media/webrtc/webrtcmediaengine.h" +#include "talk/media/webrtc/webrtcvideodecoderfactory.h" +#include "talk/media/webrtc/webrtcvideoencoderfactory.h" +#include "webrtc/modules/audio_device/include/audio_device.h" + +using talk_base::scoped_refptr; + +namespace { + +typedef talk_base::TypedMessageData InitMessageData; + +struct CreatePeerConnectionParams : public talk_base::MessageData { + CreatePeerConnectionParams( + const webrtc::PeerConnectionInterface::IceServers& configuration, + const webrtc::MediaConstraintsInterface* constraints, + webrtc::PortAllocatorFactoryInterface* allocator_factory, + webrtc::PeerConnectionObserver* observer) + : configuration(configuration), + constraints(constraints), + allocator_factory(allocator_factory), + observer(observer) { + } + scoped_refptr peerconnection; + const webrtc::PeerConnectionInterface::IceServers& configuration; + const webrtc::MediaConstraintsInterface* constraints; + scoped_refptr allocator_factory; + webrtc::PeerConnectionObserver* observer; +}; + +struct CreatePeerConnectionParamsDeprecated : public talk_base::MessageData { + CreatePeerConnectionParamsDeprecated( + const std::string& configuration, + webrtc::PortAllocatorFactoryInterface* allocator_factory, + webrtc::PeerConnectionObserver* observer) + : configuration(configuration), + allocator_factory(allocator_factory), + observer(observer) { + } + scoped_refptr peerconnection; + const std::string& configuration; + scoped_refptr allocator_factory; + webrtc::PeerConnectionObserver* observer; +}; + +struct CreateAudioSourceParams : public talk_base::MessageData { + explicit CreateAudioSourceParams( + const webrtc::MediaConstraintsInterface* constraints) + : constraints(constraints) { + } + const webrtc::MediaConstraintsInterface* constraints; + scoped_refptr source; +}; + +struct CreateVideoSourceParams : public talk_base::MessageData { + CreateVideoSourceParams(cricket::VideoCapturer* capturer, + const webrtc::MediaConstraintsInterface* constraints) + : capturer(capturer), + constraints(constraints) { + } + cricket::VideoCapturer* capturer; + const webrtc::MediaConstraintsInterface* constraints; + scoped_refptr source; +}; + +enum { + MSG_INIT_FACTORY = 1, + MSG_TERMINATE_FACTORY, + MSG_CREATE_PEERCONNECTION, + MSG_CREATE_AUDIOSOURCE, + MSG_CREATE_VIDEOSOURCE, +}; + +} // namespace + +namespace webrtc { + +scoped_refptr +CreatePeerConnectionFactory() { + scoped_refptr pc_factory( + new talk_base::RefCountedObject()); + + if (!pc_factory->Initialize()) { + return NULL; + } + return pc_factory; +} + +scoped_refptr +CreatePeerConnectionFactory( + talk_base::Thread* worker_thread, + talk_base::Thread* signaling_thread, + AudioDeviceModule* default_adm, + cricket::WebRtcVideoEncoderFactory* encoder_factory, + cricket::WebRtcVideoDecoderFactory* decoder_factory) { + scoped_refptr pc_factory( + new talk_base::RefCountedObject( + worker_thread, signaling_thread, default_adm, + encoder_factory, decoder_factory)); + if (!pc_factory->Initialize()) { + return NULL; + } + return pc_factory; +} + +PeerConnectionFactory::PeerConnectionFactory() + : owns_ptrs_(true), + signaling_thread_(new talk_base::Thread), + worker_thread_(new talk_base::Thread) { + bool result = signaling_thread_->Start(); + ASSERT(result); + result = worker_thread_->Start(); + ASSERT(result); +} + +PeerConnectionFactory::PeerConnectionFactory( + talk_base::Thread* worker_thread, + talk_base::Thread* signaling_thread, + AudioDeviceModule* default_adm, + cricket::WebRtcVideoEncoderFactory* video_encoder_factory, + cricket::WebRtcVideoDecoderFactory* video_decoder_factory) + : owns_ptrs_(false), + signaling_thread_(signaling_thread), + worker_thread_(worker_thread), + default_adm_(default_adm), + video_encoder_factory_(video_encoder_factory), + video_decoder_factory_(video_decoder_factory) { + ASSERT(worker_thread != NULL); + ASSERT(signaling_thread != NULL); + // TODO: Currently there is no way creating an external adm in + // libjingle source tree. So we can 't currently assert if this is NULL. + // ASSERT(default_adm != NULL); +} + +PeerConnectionFactory::~PeerConnectionFactory() { + signaling_thread_->Clear(this); + signaling_thread_->Send(this, MSG_TERMINATE_FACTORY); + if (owns_ptrs_) { + delete signaling_thread_; + delete worker_thread_; + } +} + +bool PeerConnectionFactory::Initialize() { + InitMessageData result(false); + signaling_thread_->Send(this, MSG_INIT_FACTORY, &result); + return result.data(); +} + +void PeerConnectionFactory::OnMessage(talk_base::Message* msg) { + switch (msg->message_id) { + case MSG_INIT_FACTORY: { + InitMessageData* pdata = static_cast (msg->pdata); + pdata->data() = Initialize_s(); + break; + } + case MSG_TERMINATE_FACTORY: { + Terminate_s(); + break; + } + case MSG_CREATE_PEERCONNECTION: { + CreatePeerConnectionParams* pdata = + static_cast (msg->pdata); + pdata->peerconnection = CreatePeerConnection_s(pdata->configuration, + pdata->constraints, + pdata->allocator_factory, + pdata->observer); + break; + } + case MSG_CREATE_AUDIOSOURCE: { + CreateAudioSourceParams* pdata = + static_cast(msg->pdata); + pdata->source = CreateAudioSource_s(pdata->constraints); + break; + } + case MSG_CREATE_VIDEOSOURCE: { + CreateVideoSourceParams* pdata = + static_cast (msg->pdata); + pdata->source = CreateVideoSource_s(pdata->capturer, pdata->constraints); + break; + } + } +} + +bool PeerConnectionFactory::Initialize_s() { + talk_base::InitRandom(talk_base::Time()); + + allocator_factory_ = PortAllocatorFactory::Create(worker_thread_); + if (!allocator_factory_) + return false; + + cricket::DummyDeviceManager* device_manager( + new cricket::DummyDeviceManager()); + // TODO: Need to make sure only one VoE is created inside + // WebRtcMediaEngine. + cricket::WebRtcMediaEngine* webrtc_media_engine( + new cricket::WebRtcMediaEngine(default_adm_.get(), + NULL, // No secondary adm. + video_encoder_factory_.get(), + video_decoder_factory_.get())); + + channel_manager_.reset(new cricket::ChannelManager( + webrtc_media_engine, device_manager, worker_thread_)); + if (!channel_manager_->Init()) { + return false; + } + return true; +} + +// Terminate what we created on the signaling thread. +void PeerConnectionFactory::Terminate_s() { + channel_manager_.reset(NULL); + allocator_factory_ = NULL; +} + +talk_base::scoped_refptr +PeerConnectionFactory::CreateAudioSource_s( + const MediaConstraintsInterface* constraints) { + talk_base::scoped_refptr source( + LocalAudioSource::Create(constraints)); + return source; +} + +talk_base::scoped_refptr +PeerConnectionFactory::CreateVideoSource_s( + cricket::VideoCapturer* capturer, + const MediaConstraintsInterface* constraints) { + talk_base::scoped_refptr source( + LocalVideoSource::Create(channel_manager_.get(), capturer, + constraints)); + return VideoSourceProxy::Create(signaling_thread_, source); +} + +scoped_refptr +PeerConnectionFactory::CreatePeerConnection( + const PeerConnectionInterface::IceServers& configuration, + const MediaConstraintsInterface* constraints, + PortAllocatorFactoryInterface* allocator_factory, + DTLSIdentityServiceInterface* dtls_identity_service, + PeerConnectionObserver* observer) { + CreatePeerConnectionParams params(configuration, constraints, + allocator_factory, observer); + signaling_thread_->Send(this, MSG_CREATE_PEERCONNECTION, ¶ms); + return params.peerconnection; +} + +scoped_refptr +PeerConnectionFactory::CreatePeerConnection( + const PeerConnectionInterface::IceServers& configuration, + const MediaConstraintsInterface* constraints, + DTLSIdentityServiceInterface* dtls_identity_service, + PeerConnectionObserver* observer) { + return CreatePeerConnection( + configuration, constraints, NULL, dtls_identity_service, observer); +} + +talk_base::scoped_refptr +PeerConnectionFactory::CreatePeerConnection_s( + const PeerConnectionInterface::IceServers& configuration, + const MediaConstraintsInterface* constraints, + PortAllocatorFactoryInterface* allocator_factory, + PeerConnectionObserver* observer) { + ASSERT(allocator_factory || allocator_factory_); + talk_base::scoped_refptr pc( + new talk_base::RefCountedObject(this)); + if (!pc->Initialize( + configuration, + constraints, + allocator_factory ? allocator_factory : allocator_factory_.get(), + observer)) { + return NULL; + } + return PeerConnectionProxy::Create(signaling_thread(), pc); +} + +scoped_refptr +PeerConnectionFactory::CreateLocalMediaStream(const std::string& label) { + return MediaStreamProxy::Create(signaling_thread_, + MediaStream::Create(label)); +} + +talk_base::scoped_refptr +PeerConnectionFactory::CreateAudioSource( + const MediaConstraintsInterface* constraints) { + CreateAudioSourceParams params(constraints); + signaling_thread_->Send(this, MSG_CREATE_AUDIOSOURCE, ¶ms); + return params.source; +} + +talk_base::scoped_refptr +PeerConnectionFactory::CreateVideoSource( + cricket::VideoCapturer* capturer, + const MediaConstraintsInterface* constraints) { + + CreateVideoSourceParams params(capturer, + constraints); + signaling_thread_->Send(this, MSG_CREATE_VIDEOSOURCE, ¶ms); + return params.source; +} + +talk_base::scoped_refptr +PeerConnectionFactory::CreateVideoTrack( + const std::string& id, + VideoSourceInterface* source) { + talk_base::scoped_refptr track( + VideoTrack::Create(id, source)); + return VideoTrackProxy::Create(signaling_thread_, track); +} + +scoped_refptr PeerConnectionFactory::CreateAudioTrack( + const std::string& id, + AudioSourceInterface* source) { + talk_base::scoped_refptr track( + AudioTrack::Create(id, source)); + return AudioTrackProxy::Create(signaling_thread_, track); +} + +cricket::ChannelManager* PeerConnectionFactory::channel_manager() { + return channel_manager_.get(); +} + +talk_base::Thread* PeerConnectionFactory::signaling_thread() { + return signaling_thread_; +} + +talk_base::Thread* PeerConnectionFactory::worker_thread() { + return worker_thread_; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/peerconnectionfactory.h b/talk/app/webrtc/peerconnectionfactory.h new file mode 100644 index 000000000..c0e15e375 --- /dev/null +++ b/talk/app/webrtc/peerconnectionfactory.h @@ -0,0 +1,127 @@ +/* + * libjingle + * Copyright 2011, 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. + */ +#ifndef TALK_APP_WEBRTC_PEERCONNECTIONFACTORY_H_ +#define TALK_APP_WEBRTC_PEERCONNECTIONFACTORY_H_ + +#include + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/thread.h" +#include "talk/session/media/channelmanager.h" + +namespace webrtc { + +class PeerConnectionFactory : public PeerConnectionFactoryInterface, + public talk_base::MessageHandler { + public: + virtual talk_base::scoped_refptr + CreatePeerConnection( + const PeerConnectionInterface::IceServers& configuration, + const MediaConstraintsInterface* constraints, + DTLSIdentityServiceInterface* dtls_identity_service, + PeerConnectionObserver* observer); + + virtual talk_base::scoped_refptr + CreatePeerConnection( + const PeerConnectionInterface::IceServers& configuration, + const MediaConstraintsInterface* constraints, + PortAllocatorFactoryInterface* allocator_factory, + DTLSIdentityServiceInterface* dtls_identity_service, + PeerConnectionObserver* observer); + bool Initialize(); + + virtual talk_base::scoped_refptr + CreateLocalMediaStream(const std::string& label); + + virtual talk_base::scoped_refptr CreateAudioSource( + const MediaConstraintsInterface* constraints); + + virtual talk_base::scoped_refptr CreateVideoSource( + cricket::VideoCapturer* capturer, + const MediaConstraintsInterface* constraints); + + virtual talk_base::scoped_refptr + CreateVideoTrack(const std::string& id, + VideoSourceInterface* video_source); + + virtual talk_base::scoped_refptr + CreateAudioTrack(const std::string& id, + AudioSourceInterface* audio_source); + + virtual cricket::ChannelManager* channel_manager(); + virtual talk_base::Thread* signaling_thread(); + virtual talk_base::Thread* worker_thread(); + + protected: + PeerConnectionFactory(); + PeerConnectionFactory( + talk_base::Thread* worker_thread, + talk_base::Thread* signaling_thread, + AudioDeviceModule* default_adm, + cricket::WebRtcVideoEncoderFactory* video_encoder_factory, + cricket::WebRtcVideoDecoderFactory* video_decoder_factory); + virtual ~PeerConnectionFactory(); + + + private: + bool Initialize_s(); + void Terminate_s(); + talk_base::scoped_refptr CreateAudioSource_s( + const MediaConstraintsInterface* constraints); + talk_base::scoped_refptr CreateVideoSource_s( + cricket::VideoCapturer* capturer, + const MediaConstraintsInterface* constraints); + talk_base::scoped_refptr CreatePeerConnection_s( + const PeerConnectionInterface::IceServers& configuration, + const MediaConstraintsInterface* constraints, + PortAllocatorFactoryInterface* allocator_factory, + PeerConnectionObserver* observer); + // Implements talk_base::MessageHandler. + void OnMessage(talk_base::Message* msg); + + bool owns_ptrs_; + talk_base::Thread* signaling_thread_; + talk_base::Thread* worker_thread_; + talk_base::scoped_refptr allocator_factory_; + // External Audio device used for audio playback. + talk_base::scoped_refptr default_adm_; + talk_base::scoped_ptr channel_manager_; + // External Video encoder factory. This can be NULL if the client has not + // injected any. In that case, video engine will use the internal SW encoder. + talk_base::scoped_ptr + video_encoder_factory_; + // External Video decoder factory. This can be NULL if the client has not + // injected any. In that case, video engine will use the internal SW decoder. + talk_base::scoped_ptr + video_decoder_factory_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_PEERCONNECTIONFACTORY_H_ diff --git a/talk/app/webrtc/peerconnectionfactory_unittest.cc b/talk/app/webrtc/peerconnectionfactory_unittest.cc new file mode 100644 index 000000000..6d5420428 --- /dev/null +++ b/talk/app/webrtc/peerconnectionfactory_unittest.cc @@ -0,0 +1,270 @@ +/* + * libjingle + * Copyright 2012, 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 + * 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 + +#include "talk/app/webrtc/fakeportallocatorfactory.h" +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/peerconnectionfactory.h" +#include "talk/app/webrtc/videosourceinterface.h" +#include "talk/app/webrtc/test/fakevideotrackrenderer.h" +#include "talk/base/gunit.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/thread.h" +#include "talk/media/base/fakevideocapturer.h" +#include "talk/media/webrtc/webrtccommon.h" +#include "talk/media/webrtc/webrtcvoe.h" + +using webrtc::FakeVideoTrackRenderer; +using webrtc::MediaStreamInterface; +using webrtc::PeerConnectionFactoryInterface; +using webrtc::PeerConnectionInterface; +using webrtc::PeerConnectionObserver; +using webrtc::PortAllocatorFactoryInterface; +using webrtc::VideoSourceInterface; +using webrtc::VideoTrackInterface; + +namespace { + +typedef std::vector + StunConfigurations; +typedef std::vector + TurnConfigurations; + +static const char kStunIceServer[] = "stun:stun.l.google.com:19302"; +static const char kTurnIceServer[] = "turn:test%40hello.com@test.com:1234"; +static const char kTurnIceServerWithTransport[] = + "turn:test@hello.com?transport=tcp"; +static const char kSecureTurnIceServer[] = + "turns:test@hello.com?transport=tcp"; +static const char kTurnIceServerWithNoUsernameInUri[] = + "turn:test.com:1234"; +static const char kTurnPassword[] = "turnpassword"; +static const int kDefaultPort = 3478; +static const char kTurnUsername[] = "test"; + +class NullPeerConnectionObserver : public PeerConnectionObserver { + public: + virtual void OnError() {} + virtual void OnMessage(const std::string& msg) {} + virtual void OnSignalingMessage(const std::string& msg) {} + virtual void OnSignalingChange( + PeerConnectionInterface::SignalingState new_state) {} + virtual void OnAddStream(MediaStreamInterface* stream) {} + virtual void OnRemoveStream(MediaStreamInterface* stream) {} + virtual void OnRenegotiationNeeded() {} + virtual void OnIceConnectionChange( + PeerConnectionInterface::IceConnectionState new_state) {} + virtual void OnIceGatheringChange( + PeerConnectionInterface::IceGatheringState new_state) {} + virtual void OnIceCandidate(const webrtc::IceCandidateInterface* candidate) {} +}; + +} // namespace + +class PeerConnectionFactoryTest : public testing::Test { + void SetUp() { + factory_ = webrtc::CreatePeerConnectionFactory(talk_base::Thread::Current(), + talk_base::Thread::Current(), + NULL, + NULL, + NULL); + + ASSERT_TRUE(factory_.get() != NULL); + allocator_factory_ = webrtc::FakePortAllocatorFactory::Create(); + } + + protected: + void VerifyStunConfigurations(StunConfigurations stun_config) { + webrtc::FakePortAllocatorFactory* allocator = + static_cast( + allocator_factory_.get()); + ASSERT_TRUE(allocator != NULL); + EXPECT_EQ(stun_config.size(), allocator->stun_configs().size()); + for (size_t i = 0; i < stun_config.size(); ++i) { + EXPECT_EQ(stun_config[i].server.ToString(), + allocator->stun_configs()[i].server.ToString()); + } + } + + void VerifyTurnConfigurations(TurnConfigurations turn_config) { + webrtc::FakePortAllocatorFactory* allocator = + static_cast( + allocator_factory_.get()); + ASSERT_TRUE(allocator != NULL); + EXPECT_EQ(turn_config.size(), allocator->turn_configs().size()); + for (size_t i = 0; i < turn_config.size(); ++i) { + EXPECT_EQ(turn_config[i].server.ToString(), + allocator->turn_configs()[i].server.ToString()); + EXPECT_EQ(turn_config[i].username, allocator->turn_configs()[i].username); + EXPECT_EQ(turn_config[i].password, allocator->turn_configs()[i].password); + EXPECT_EQ(turn_config[i].transport_type, + allocator->turn_configs()[i].transport_type); + } + } + + talk_base::scoped_refptr factory_; + NullPeerConnectionObserver observer_; + talk_base::scoped_refptr allocator_factory_; +}; + +// Verify creation of PeerConnection using internal ADM, video factory and +// internal libjingle threads. +TEST(PeerConnectionFactoryTestInternal, CreatePCUsingInternalModules) { + talk_base::scoped_refptr factory( + webrtc::CreatePeerConnectionFactory()); + + NullPeerConnectionObserver observer; + webrtc::PeerConnectionInterface::IceServers servers; + + talk_base::scoped_refptr pc( + factory->CreatePeerConnection(servers, NULL, NULL, &observer)); + + EXPECT_TRUE(pc.get() != NULL); +} + +// This test verifies creation of PeerConnection with valid STUN and TURN +// configuration. Also verifies the URL's parsed correctly as expected. +TEST_F(PeerConnectionFactoryTest, CreatePCUsingIceServers) { + webrtc::PeerConnectionInterface::IceServers ice_servers; + webrtc::PeerConnectionInterface::IceServer ice_server; + ice_server.uri = kStunIceServer; + ice_servers.push_back(ice_server); + ice_server.uri = kTurnIceServer; + ice_server.password = kTurnPassword; + ice_servers.push_back(ice_server); + talk_base::scoped_refptr pc( + factory_->CreatePeerConnection(ice_servers, NULL, + allocator_factory_.get(), + NULL, + &observer_)); + EXPECT_TRUE(pc.get() != NULL); + StunConfigurations stun_configs; + webrtc::PortAllocatorFactoryInterface::StunConfiguration stun( + "stun.l.google.com", 19302); + stun_configs.push_back(stun); + webrtc::PortAllocatorFactoryInterface::StunConfiguration stun1( + "test.com", 1234); + stun_configs.push_back(stun1); + VerifyStunConfigurations(stun_configs); + TurnConfigurations turn_configs; + webrtc::PortAllocatorFactoryInterface::TurnConfiguration turn( + "test.com", 1234, "test@hello.com", kTurnPassword, "udp"); + turn_configs.push_back(turn); + VerifyTurnConfigurations(turn_configs); +} + +TEST_F(PeerConnectionFactoryTest, CreatePCUsingNoUsernameInUri) { + webrtc::PeerConnectionInterface::IceServers ice_servers; + webrtc::PeerConnectionInterface::IceServer ice_server; + ice_server.uri = kStunIceServer; + ice_servers.push_back(ice_server); + ice_server.uri = kTurnIceServerWithNoUsernameInUri; + ice_server.username = kTurnUsername; + ice_server.password = kTurnPassword; + ice_servers.push_back(ice_server); + talk_base::scoped_refptr pc( + factory_->CreatePeerConnection(ice_servers, NULL, + allocator_factory_.get(), + NULL, + &observer_)); + EXPECT_TRUE(pc.get() != NULL); + TurnConfigurations turn_configs; + webrtc::PortAllocatorFactoryInterface::TurnConfiguration turn( + "test.com", 1234, kTurnUsername, kTurnPassword, "udp"); + turn_configs.push_back(turn); + VerifyTurnConfigurations(turn_configs); +} + +// This test verifies the PeerConnection created properly with TURN url which +// has transport parameter in it. +TEST_F(PeerConnectionFactoryTest, CreatePCUsingTurnUrlWithTransportParam) { + webrtc::PeerConnectionInterface::IceServers ice_servers; + webrtc::PeerConnectionInterface::IceServer ice_server; + ice_server.uri = kTurnIceServerWithTransport; + ice_server.password = kTurnPassword; + ice_servers.push_back(ice_server); + talk_base::scoped_refptr pc( + factory_->CreatePeerConnection(ice_servers, NULL, + allocator_factory_.get(), + NULL, + &observer_)); + EXPECT_TRUE(pc.get() != NULL); + TurnConfigurations turn_configs; + webrtc::PortAllocatorFactoryInterface::TurnConfiguration turn( + "hello.com", kDefaultPort, "test", kTurnPassword, "tcp"); + turn_configs.push_back(turn); + VerifyTurnConfigurations(turn_configs); + StunConfigurations stun_configs; + webrtc::PortAllocatorFactoryInterface::StunConfiguration stun( + "hello.com", kDefaultPort); + stun_configs.push_back(stun); + VerifyStunConfigurations(stun_configs); +} + +// This test verifies factory failed to create a peerconneciton object when +// a valid secure TURN url passed. Connecting to a secure TURN server is not +// supported currently. +TEST_F(PeerConnectionFactoryTest, CreatePCUsingSecureTurnUrl) { + webrtc::PeerConnectionInterface::IceServers ice_servers; + webrtc::PeerConnectionInterface::IceServer ice_server; + ice_server.uri = kSecureTurnIceServer; + ice_server.password = kTurnPassword; + ice_servers.push_back(ice_server); + talk_base::scoped_refptr pc( + factory_->CreatePeerConnection(ice_servers, NULL, + allocator_factory_.get(), + NULL, + &observer_)); + EXPECT_TRUE(pc.get() == NULL); + TurnConfigurations turn_configs; + VerifyTurnConfigurations(turn_configs); +} + +// This test verifies the captured stream is rendered locally using a +// local video track. +TEST_F(PeerConnectionFactoryTest, LocalRendering) { + cricket::FakeVideoCapturer* capturer = new cricket::FakeVideoCapturer(); + // The source take ownership of |capturer|. + talk_base::scoped_refptr source( + factory_->CreateVideoSource(capturer, NULL)); + ASSERT_TRUE(source.get() != NULL); + talk_base::scoped_refptr track( + factory_->CreateVideoTrack("testlabel", source)); + ASSERT_TRUE(track.get() != NULL); + FakeVideoTrackRenderer local_renderer(track); + + EXPECT_EQ(0, local_renderer.num_rendered_frames()); + EXPECT_TRUE(capturer->CaptureFrame()); + EXPECT_EQ(1, local_renderer.num_rendered_frames()); + + track->set_enabled(false); + EXPECT_TRUE(capturer->CaptureFrame()); + EXPECT_EQ(1, local_renderer.num_rendered_frames()); + + track->set_enabled(true); + EXPECT_TRUE(capturer->CaptureFrame()); + EXPECT_EQ(2, local_renderer.num_rendered_frames()); +} diff --git a/talk/app/webrtc/peerconnectioninterface.h b/talk/app/webrtc/peerconnectioninterface.h new file mode 100644 index 000000000..9a7cdd0c8 --- /dev/null +++ b/talk/app/webrtc/peerconnectioninterface.h @@ -0,0 +1,451 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// This file contains the PeerConnection interface as defined in +// http://dev.w3.org/2011/webrtc/editor/webrtc.html#peer-to-peer-connections. +// Applications must use this interface to implement peerconnection. +// PeerConnectionFactory class provides factory methods to create +// peerconnection, mediastream and media tracks objects. +// +// The Following steps are needed to setup a typical call using Jsep. +// 1. Create a PeerConnectionFactoryInterface. Check constructors for more +// information about input parameters. +// 2. Create a PeerConnection object. Provide a configuration string which +// points either to stun or turn server to generate ICE candidates and provide +// an object that implements the PeerConnectionObserver interface. +// 3. Create local MediaStream and MediaTracks using the PeerConnectionFactory +// and add it to PeerConnection by calling AddStream. +// 4. Create an offer and serialize it and send it to the remote peer. +// 5. Once an ice candidate have been found PeerConnection will call the +// observer function OnIceCandidate. The candidates must also be serialized and +// sent to the remote peer. +// 6. Once an answer is received from the remote peer, call +// SetLocalSessionDescription with the offer and SetRemoteSessionDescription +// with the remote answer. +// 7. Once a remote candidate is received from the remote peer, provide it to +// the peerconnection by calling AddIceCandidate. + + +// The Receiver of a call can decide to accept or reject the call. +// This decision will be taken by the application not peerconnection. +// If application decides to accept the call +// 1. Create PeerConnectionFactoryInterface if it doesn't exist. +// 2. Create a new PeerConnection. +// 3. Provide the remote offer to the new PeerConnection object by calling +// SetRemoteSessionDescription. +// 4. Generate an answer to the remote offer by calling CreateAnswer and send it +// back to the remote peer. +// 5. Provide the local answer to the new PeerConnection by calling +// SetLocalSessionDescription with the answer. +// 6. Provide the remote ice candidates by calling AddIceCandidate. +// 7. Once a candidate have been found PeerConnection will call the observer +// function OnIceCandidate. Send these candidates to the remote peer. + +#ifndef TALK_APP_WEBRTC_PEERCONNECTIONINTERFACE_H_ +#define TALK_APP_WEBRTC_PEERCONNECTIONINTERFACE_H_ + +#include +#include + +#include "talk/app/webrtc/datachannelinterface.h" +#include "talk/app/webrtc/dtmfsenderinterface.h" +#include "talk/app/webrtc/jsep.h" +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/statstypes.h" +#include "talk/base/socketaddress.h" + +namespace talk_base { +class Thread; +} + +namespace cricket { +class PortAllocator; +class WebRtcVideoDecoderFactory; +class WebRtcVideoEncoderFactory; +} + +namespace webrtc { +class AudioDeviceModule; +class MediaConstraintsInterface; + +// MediaStream container interface. +class StreamCollectionInterface : public talk_base::RefCountInterface { + public: + // TODO(ronghuawu): Update the function names to c++ style, e.g. find -> Find. + virtual size_t count() = 0; + virtual MediaStreamInterface* at(size_t index) = 0; + virtual MediaStreamInterface* find(const std::string& label) = 0; + virtual MediaStreamTrackInterface* FindAudioTrack( + const std::string& id) = 0; + virtual MediaStreamTrackInterface* FindVideoTrack( + const std::string& id) = 0; + + protected: + // Dtor protected as objects shouldn't be deleted via this interface. + ~StreamCollectionInterface() {} +}; + +class StatsObserver : public talk_base::RefCountInterface { + public: + virtual void OnComplete(const std::vector& reports) = 0; + + protected: + virtual ~StatsObserver() {} +}; + +class PeerConnectionInterface : public talk_base::RefCountInterface { + public: + // See http://dev.w3.org/2011/webrtc/editor/webrtc.html#state-definitions . + enum SignalingState { + kStable, + kHaveLocalOffer, + kHaveLocalPrAnswer, + kHaveRemoteOffer, + kHaveRemotePrAnswer, + kClosed, + }; + + // TODO(bemasc): Remove IceState when callers are changed to + // IceConnection/GatheringState. + enum IceState { + kIceNew, + kIceGathering, + kIceWaiting, + kIceChecking, + kIceConnected, + kIceCompleted, + kIceFailed, + kIceClosed, + }; + + enum IceGatheringState { + kIceGatheringNew, + kIceGatheringGathering, + kIceGatheringComplete + }; + + enum IceConnectionState { + kIceConnectionNew, + kIceConnectionChecking, + kIceConnectionConnected, + kIceConnectionCompleted, + kIceConnectionFailed, + kIceConnectionDisconnected, + kIceConnectionClosed, + }; + + struct IceServer { + std::string uri; + std::string username; + std::string password; + }; + typedef std::vector IceServers; + + // Accessor methods to active local streams. + virtual talk_base::scoped_refptr + local_streams() = 0; + + // Accessor methods to remote streams. + virtual talk_base::scoped_refptr + remote_streams() = 0; + + // Add a new MediaStream to be sent on this PeerConnection. + // Note that a SessionDescription negotiation is needed before the + // remote peer can receive the stream. + virtual bool AddStream(MediaStreamInterface* stream, + const MediaConstraintsInterface* constraints) = 0; + + // Remove a MediaStream from this PeerConnection. + // Note that a SessionDescription negotiation is need before the + // remote peer is notified. + virtual void RemoveStream(MediaStreamInterface* stream) = 0; + + // Returns pointer to the created DtmfSender on success. + // Otherwise returns NULL. + virtual talk_base::scoped_refptr CreateDtmfSender( + AudioTrackInterface* track) = 0; + + virtual bool GetStats(StatsObserver* observer, + MediaStreamTrackInterface* track) = 0; + + virtual talk_base::scoped_refptr CreateDataChannel( + const std::string& label, + const DataChannelInit* config) = 0; + + virtual const SessionDescriptionInterface* local_description() const = 0; + virtual const SessionDescriptionInterface* remote_description() const = 0; + + // Create a new offer. + // The CreateSessionDescriptionObserver callback will be called when done. + virtual void CreateOffer(CreateSessionDescriptionObserver* observer, + const MediaConstraintsInterface* constraints) = 0; + // Create an answer to an offer. + // The CreateSessionDescriptionObserver callback will be called when done. + virtual void CreateAnswer(CreateSessionDescriptionObserver* observer, + const MediaConstraintsInterface* constraints) = 0; + // Sets the local session description. + // JsepInterface takes the ownership of |desc| even if it fails. + // The |observer| callback will be called when done. + virtual void SetLocalDescription(SetSessionDescriptionObserver* observer, + SessionDescriptionInterface* desc) = 0; + // Sets the remote session description. + // JsepInterface takes the ownership of |desc| even if it fails. + // The |observer| callback will be called when done. + virtual void SetRemoteDescription(SetSessionDescriptionObserver* observer, + SessionDescriptionInterface* desc) = 0; + // Restarts or updates the ICE Agent process of gathering local candidates + // and pinging remote candidates. + virtual bool UpdateIce(const IceServers& configuration, + const MediaConstraintsInterface* constraints) = 0; + // Provides a remote candidate to the ICE Agent. + // A copy of the |candidate| will be created and added to the remote + // description. So the caller of this method still has the ownership of the + // |candidate|. + // TODO(ronghuawu): Consider to change this so that the AddIceCandidate will + // take the ownership of the |candidate|. + virtual bool AddIceCandidate(const IceCandidateInterface* candidate) = 0; + + // Returns the current SignalingState. + virtual SignalingState signaling_state() = 0; + + // TODO(bemasc): Remove ice_state when callers are changed to + // IceConnection/GatheringState. + // Returns the current IceState. + virtual IceState ice_state() = 0; + virtual IceConnectionState ice_connection_state() = 0; + virtual IceGatheringState ice_gathering_state() = 0; + + // Terminates all media and closes the transport. + virtual void Close() = 0; + + protected: + // Dtor protected as objects shouldn't be deleted via this interface. + ~PeerConnectionInterface() {} +}; + +// PeerConnection callback interface. Application should implement these +// methods. +class PeerConnectionObserver { + public: + enum StateType { + kSignalingState, + kIceState, + }; + + virtual void OnError() = 0; + + // Triggered when the SignalingState changed. + virtual void OnSignalingChange( + PeerConnectionInterface::SignalingState new_state) {} + + // Triggered when SignalingState or IceState have changed. + // TODO(bemasc): Remove once callers transition to OnSignalingChange. + virtual void OnStateChange(StateType state_changed) {} + + // Triggered when media is received on a new stream from remote peer. + virtual void OnAddStream(MediaStreamInterface* stream) = 0; + + // Triggered when a remote peer close a stream. + virtual void OnRemoveStream(MediaStreamInterface* stream) = 0; + + // Triggered when a remote peer open a data channel. + // TODO(perkj): Make pure virtual. + virtual void OnDataChannel(DataChannelInterface* data_channel) {} + + // Triggered when renegotation is needed, for example the ICE has restarted. + virtual void OnRenegotiationNeeded() {} + + // Called any time the IceConnectionState changes + virtual void OnIceConnectionChange( + PeerConnectionInterface::IceConnectionState new_state) {} + + // Called any time the IceGatheringState changes + virtual void OnIceGatheringChange( + PeerConnectionInterface::IceGatheringState new_state) {} + + // New Ice candidate have been found. + virtual void OnIceCandidate(const IceCandidateInterface* candidate) = 0; + + // TODO(bemasc): Remove this once callers transition to OnIceGatheringChange. + // All Ice candidates have been found. + virtual void OnIceComplete() {} + + protected: + // Dtor protected as objects shouldn't be deleted via this interface. + ~PeerConnectionObserver() {} +}; + +// Factory class used for creating cricket::PortAllocator that is used +// for ICE negotiation. +class PortAllocatorFactoryInterface : public talk_base::RefCountInterface { + public: + struct StunConfiguration { + StunConfiguration(const std::string& address, int port) + : server(address, port) {} + // STUN server address and port. + talk_base::SocketAddress server; + }; + + struct TurnConfiguration { + TurnConfiguration(const std::string& address, + int port, + const std::string& username, + const std::string& password, + const std::string& transport_type) + : server(address, port), + username(username), + password(password), + transport_type(transport_type) {} + talk_base::SocketAddress server; + std::string username; + std::string password; + std::string transport_type; + }; + + virtual cricket::PortAllocator* CreatePortAllocator( + const std::vector& stun_servers, + const std::vector& turn_configurations) = 0; + + protected: + PortAllocatorFactoryInterface() {} + ~PortAllocatorFactoryInterface() {} +}; + +// Used to receive callbacks of DTLS identity requests. +class DTLSIdentityRequestObserver : public talk_base::RefCountInterface { + public: + virtual void OnFailure(int error) = 0; + virtual void OnSuccess(const std::string& certificate, + const std::string& private_key) = 0; + protected: + virtual ~DTLSIdentityRequestObserver() {} +}; + +class DTLSIdentityServiceInterface { + public: + // Asynchronously request a DTLS identity, including a self-signed certificate + // and the private key used to sign the certificate, from the identity store + // for the given identity name. + // DTLSIdentityRequestObserver::OnSuccess will be called with the identity if + // the request succeeded; DTLSIdentityRequestObserver::OnFailure will be + // called with an error code if the request failed. + // + // Only one request can be made at a time. If a second request is called + // before the first one completes, RequestIdentity will abort and return + // false. + // + // |identity_name| is an internal name selected by the client to identify an + // identity within an origin. E.g. an web site may cache the certificates used + // to communicate with differnent peers under different identity names. + // + // |common_name| is the common name used to generate the certificate. If the + // certificate already exists in the store, |common_name| is ignored. + // + // |observer| is the object to receive success or failure callbacks. + // + // Returns true if either OnFailure or OnSuccess will be called. + virtual bool RequestIdentity( + const std::string& identity_name, + const std::string& common_name, + DTLSIdentityRequestObserver* observer) = 0; +}; + +// PeerConnectionFactoryInterface is the factory interface use for creating +// PeerConnection, MediaStream and media tracks. +// PeerConnectionFactoryInterface will create required libjingle threads, +// socket and network manager factory classes for networking. +// If an application decides to provide its own threads and network +// implementation of these classes it should use the alternate +// CreatePeerConnectionFactory method which accepts threads as input and use the +// CreatePeerConnection version that takes a PortAllocatorFactoryInterface as +// argument. +class PeerConnectionFactoryInterface : public talk_base::RefCountInterface { + public: + virtual talk_base::scoped_refptr + CreatePeerConnection( + const PeerConnectionInterface::IceServers& configuration, + const MediaConstraintsInterface* constraints, + DTLSIdentityServiceInterface* dtls_identity_service, + PeerConnectionObserver* observer) = 0; + virtual talk_base::scoped_refptr + CreatePeerConnection( + const PeerConnectionInterface::IceServers& configuration, + const MediaConstraintsInterface* constraints, + PortAllocatorFactoryInterface* allocator_factory, + DTLSIdentityServiceInterface* dtls_identity_service, + PeerConnectionObserver* observer) = 0; + virtual talk_base::scoped_refptr + CreateLocalMediaStream(const std::string& label) = 0; + + // Creates a AudioSourceInterface. + // |constraints| decides audio processing settings but can be NULL. + virtual talk_base::scoped_refptr CreateAudioSource( + const MediaConstraintsInterface* constraints) = 0; + + // Creates a VideoSourceInterface. The new source take ownership of + // |capturer|. |constraints| decides video resolution and frame rate but can + // be NULL. + virtual talk_base::scoped_refptr CreateVideoSource( + cricket::VideoCapturer* capturer, + const MediaConstraintsInterface* constraints) = 0; + + // Creates a new local VideoTrack. The same |source| can be used in several + // tracks. + virtual talk_base::scoped_refptr + CreateVideoTrack(const std::string& label, + VideoSourceInterface* source) = 0; + + // Creates an new AudioTrack. At the moment |source| can be NULL. + virtual talk_base::scoped_refptr + CreateAudioTrack(const std::string& label, + AudioSourceInterface* source) = 0; + + protected: + // Dtor and ctor protected as objects shouldn't be created or deleted via + // this interface. + PeerConnectionFactoryInterface() {} + ~PeerConnectionFactoryInterface() {} // NOLINT +}; + +// Create a new instance of PeerConnectionFactoryInterface. +talk_base::scoped_refptr +CreatePeerConnectionFactory(); + +// Create a new instance of PeerConnectionFactoryInterface. +// Ownership of |factory|, |default_adm|, and optionally |encoder_factory| and +// |decoder_factory| transferred to the returned factory. +talk_base::scoped_refptr +CreatePeerConnectionFactory( + talk_base::Thread* worker_thread, + talk_base::Thread* signaling_thread, + AudioDeviceModule* default_adm, + cricket::WebRtcVideoEncoderFactory* encoder_factory, + cricket::WebRtcVideoDecoderFactory* decoder_factory); + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_PEERCONNECTIONINTERFACE_H_ diff --git a/talk/app/webrtc/peerconnectioninterface_unittest.cc b/talk/app/webrtc/peerconnectioninterface_unittest.cc new file mode 100644 index 000000000..782bba160 --- /dev/null +++ b/talk/app/webrtc/peerconnectioninterface_unittest.cc @@ -0,0 +1,1220 @@ +/* + * libjingle + * Copyright 2012, 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 + +#include "talk/app/webrtc/fakeportallocatorfactory.h" +#include "talk/app/webrtc/jsepsessiondescription.h" +#include "talk/app/webrtc/localvideosource.h" +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/app/webrtc/test/fakeconstraints.h" +#include "talk/app/webrtc/test/mockpeerconnectionobservers.h" +#include "talk/app/webrtc/test/testsdpstrings.h" +#include "talk/base/gunit.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sslstreamadapter.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/media/base/fakevideocapturer.h" +#include "talk/session/media/mediasession.h" + +static const char kStreamLabel1[] = "local_stream_1"; +static const char kStreamLabel2[] = "local_stream_2"; +static const char kStreamLabel3[] = "local_stream_3"; +static const int kDefaultStunPort = 3478; +static const char kStunAddressOnly[] = "stun:address"; +static const char kStunInvalidPort[] = "stun:address:-1"; +static const char kStunAddressPortAndMore1[] = "stun:address:port:more"; +static const char kStunAddressPortAndMore2[] = "stun:address:port more"; +static const char kTurnIceServerUri[] = "turn:user@turn.example.org"; +static const char kTurnUsername[] = "user"; +static const char kTurnPassword[] = "password"; +static const char kTurnHostname[] = "turn.example.org"; +static const uint32 kTimeout = 5000U; + +#define MAYBE_SKIP_TEST(feature) \ + if (!(feature())) { \ + LOG(LS_INFO) << "Feature disabled... skipping"; \ + return; \ + } + +using talk_base::scoped_ptr; +using talk_base::scoped_refptr; +using webrtc::AudioSourceInterface; +using webrtc::AudioTrackInterface; +using webrtc::DataBuffer; +using webrtc::DataChannelInterface; +using webrtc::FakeConstraints; +using webrtc::FakePortAllocatorFactory; +using webrtc::IceCandidateInterface; +using webrtc::MediaStreamInterface; +using webrtc::MediaStreamTrackInterface; +using webrtc::MockCreateSessionDescriptionObserver; +using webrtc::MockDataChannelObserver; +using webrtc::MockSetSessionDescriptionObserver; +using webrtc::MockStatsObserver; +using webrtc::PeerConnectionInterface; +using webrtc::PeerConnectionObserver; +using webrtc::PortAllocatorFactoryInterface; +using webrtc::SdpParseError; +using webrtc::SessionDescriptionInterface; +using webrtc::VideoSourceInterface; +using webrtc::VideoTrackInterface; + +namespace { + +// Gets the first ssrc of given content type from the ContentInfo. +bool GetFirstSsrc(const cricket::ContentInfo* content_info, int* ssrc) { + if (!content_info || !ssrc) { + return false; + } + const cricket::MediaContentDescription* media_desc = + static_cast ( + content_info->description); + if (!media_desc || media_desc->streams().empty()) { + return false; + } + *ssrc = media_desc->streams().begin()->first_ssrc(); + return true; +} + +void SetSsrcToZero(std::string* sdp) { + const char kSdpSsrcAtribute[] = "a=ssrc:"; + const char kSdpSsrcAtributeZero[] = "a=ssrc:0"; + size_t ssrc_pos = 0; + while ((ssrc_pos = sdp->find(kSdpSsrcAtribute, ssrc_pos)) != + std::string::npos) { + size_t end_ssrc = sdp->find(" ", ssrc_pos); + sdp->replace(ssrc_pos, end_ssrc - ssrc_pos, kSdpSsrcAtributeZero); + ssrc_pos = end_ssrc; + } +} + +class MockPeerConnectionObserver : public PeerConnectionObserver { + public: + MockPeerConnectionObserver() + : renegotiation_needed_(false), + ice_complete_(false) { + } + ~MockPeerConnectionObserver() { + } + void SetPeerConnectionInterface(PeerConnectionInterface* pc) { + pc_ = pc; + if (pc) { + state_ = pc_->signaling_state(); + } + } + virtual void OnError() {} + virtual void OnSignalingChange( + PeerConnectionInterface::SignalingState new_state) { + EXPECT_EQ(pc_->signaling_state(), new_state); + state_ = new_state; + } + // TODO(bemasc): Remove this once callers transition to OnIceGatheringChange. + virtual void OnStateChange(StateType state_changed) { + if (pc_.get() == NULL) + return; + switch (state_changed) { + case kSignalingState: + // OnSignalingChange and OnStateChange(kSignalingState) should always + // be called approximately simultaneously. To ease testing, we require + // that they always be called in that order. This check verifies + // that OnSignalingChange has just been called. + EXPECT_EQ(pc_->signaling_state(), state_); + break; + case kIceState: + ADD_FAILURE(); + break; + default: + ADD_FAILURE(); + break; + } + } + virtual void OnAddStream(MediaStreamInterface* stream) { + last_added_stream_ = stream; + } + virtual void OnRemoveStream(MediaStreamInterface* stream) { + last_removed_stream_ = stream; + } + virtual void OnRenegotiationNeeded() { + renegotiation_needed_ = true; + } + virtual void OnDataChannel(DataChannelInterface* data_channel) { + last_datachannel_ = data_channel; + } + + virtual void OnIceConnectionChange( + PeerConnectionInterface::IceConnectionState new_state) { + EXPECT_EQ(pc_->ice_connection_state(), new_state); + } + virtual void OnIceGatheringChange( + PeerConnectionInterface::IceGatheringState new_state) { + EXPECT_EQ(pc_->ice_gathering_state(), new_state); + } + virtual void OnIceCandidate(const webrtc::IceCandidateInterface* candidate) { + EXPECT_NE(PeerConnectionInterface::kIceGatheringNew, + pc_->ice_gathering_state()); + + std::string sdp; + EXPECT_TRUE(candidate->ToString(&sdp)); + EXPECT_LT(0u, sdp.size()); + last_candidate_.reset(webrtc::CreateIceCandidate(candidate->sdp_mid(), + candidate->sdp_mline_index(), sdp, NULL)); + EXPECT_TRUE(last_candidate_.get() != NULL); + } + // TODO(bemasc): Remove this once callers transition to OnSignalingChange. + virtual void OnIceComplete() { + ice_complete_ = true; + // OnIceGatheringChange(IceGatheringCompleted) and OnIceComplete() should + // be called approximately simultaneously. For ease of testing, this + // check additionally requires that they be called in the above order. + EXPECT_EQ(PeerConnectionInterface::kIceGatheringComplete, + pc_->ice_gathering_state()); + } + + // Returns the label of the last added stream. + // Empty string if no stream have been added. + std::string GetLastAddedStreamLabel() { + if (last_added_stream_.get()) + return last_added_stream_->label(); + return ""; + } + std::string GetLastRemovedStreamLabel() { + if (last_removed_stream_.get()) + return last_removed_stream_->label(); + return ""; + } + + scoped_refptr pc_; + PeerConnectionInterface::SignalingState state_; + scoped_ptr last_candidate_; + scoped_refptr last_datachannel_; + bool renegotiation_needed_; + bool ice_complete_; + + private: + scoped_refptr last_added_stream_; + scoped_refptr last_removed_stream_; +}; + +} // namespace +class PeerConnectionInterfaceTest : public testing::Test { + protected: + virtual void SetUp() { + pc_factory_ = webrtc::CreatePeerConnectionFactory( + talk_base::Thread::Current(), talk_base::Thread::Current(), NULL, NULL, + NULL); + ASSERT_TRUE(pc_factory_.get() != NULL); + } + + void CreatePeerConnection() { + CreatePeerConnection("", "", NULL); + } + + void CreatePeerConnection(webrtc::MediaConstraintsInterface* constraints) { + CreatePeerConnection("", "", constraints); + } + + void CreatePeerConnection(const std::string& uri, + const std::string& password, + webrtc::MediaConstraintsInterface* constraints) { + PeerConnectionInterface::IceServer server; + PeerConnectionInterface::IceServers servers; + server.uri = uri; + server.password = password; + servers.push_back(server); + + port_allocator_factory_ = FakePortAllocatorFactory::Create(); + pc_ = pc_factory_->CreatePeerConnection(servers, constraints, + port_allocator_factory_.get(), + NULL, + &observer_); + ASSERT_TRUE(pc_.get() != NULL); + observer_.SetPeerConnectionInterface(pc_.get()); + EXPECT_EQ(PeerConnectionInterface::kStable, observer_.state_); + } + + void CreatePeerConnectionWithDifferentConfigurations() { + CreatePeerConnection(kStunAddressOnly, "", NULL); + EXPECT_EQ(1u, port_allocator_factory_->stun_configs().size()); + EXPECT_EQ(0u, port_allocator_factory_->turn_configs().size()); + EXPECT_EQ("address", + port_allocator_factory_->stun_configs()[0].server.hostname()); + EXPECT_EQ(kDefaultStunPort, + port_allocator_factory_->stun_configs()[0].server.port()); + + CreatePeerConnection(kStunInvalidPort, "", NULL); + EXPECT_EQ(0u, port_allocator_factory_->stun_configs().size()); + EXPECT_EQ(0u, port_allocator_factory_->turn_configs().size()); + + CreatePeerConnection(kStunAddressPortAndMore1, "", NULL); + EXPECT_EQ(0u, port_allocator_factory_->stun_configs().size()); + EXPECT_EQ(0u, port_allocator_factory_->turn_configs().size()); + + CreatePeerConnection(kStunAddressPortAndMore2, "", NULL); + EXPECT_EQ(0u, port_allocator_factory_->stun_configs().size()); + EXPECT_EQ(0u, port_allocator_factory_->turn_configs().size()); + + CreatePeerConnection(kTurnIceServerUri, kTurnPassword, NULL); + EXPECT_EQ(1u, port_allocator_factory_->stun_configs().size()); + EXPECT_EQ(1u, port_allocator_factory_->turn_configs().size()); + EXPECT_EQ(kTurnUsername, + port_allocator_factory_->turn_configs()[0].username); + EXPECT_EQ(kTurnPassword, + port_allocator_factory_->turn_configs()[0].password); + EXPECT_EQ(kTurnHostname, + port_allocator_factory_->turn_configs()[0].server.hostname()); + EXPECT_EQ(kTurnHostname, + port_allocator_factory_->stun_configs()[0].server.hostname()); + } + + void ReleasePeerConnection() { + pc_ = NULL; + observer_.SetPeerConnectionInterface(NULL); + } + + void AddStream(const std::string& label) { + // Create a local stream. + scoped_refptr stream( + pc_factory_->CreateLocalMediaStream(label)); + scoped_refptr video_source( + pc_factory_->CreateVideoSource(new cricket::FakeVideoCapturer(), NULL)); + scoped_refptr video_track( + pc_factory_->CreateVideoTrack(label + "v0", video_source)); + stream->AddTrack(video_track.get()); + EXPECT_TRUE(pc_->AddStream(stream, NULL)); + EXPECT_TRUE_WAIT(observer_.renegotiation_needed_, kTimeout); + observer_.renegotiation_needed_ = false; + } + + void AddVoiceStream(const std::string& label) { + // Create a local stream. + scoped_refptr stream( + pc_factory_->CreateLocalMediaStream(label)); + scoped_refptr audio_track( + pc_factory_->CreateAudioTrack(label + "a0", NULL)); + stream->AddTrack(audio_track.get()); + EXPECT_TRUE(pc_->AddStream(stream, NULL)); + EXPECT_TRUE_WAIT(observer_.renegotiation_needed_, kTimeout); + observer_.renegotiation_needed_ = false; + } + + void AddAudioVideoStream(const std::string& stream_label, + const std::string& audio_track_label, + const std::string& video_track_label) { + // Create a local stream. + scoped_refptr stream( + pc_factory_->CreateLocalMediaStream(stream_label)); + scoped_refptr audio_track( + pc_factory_->CreateAudioTrack( + audio_track_label, static_cast(NULL))); + stream->AddTrack(audio_track.get()); + scoped_refptr video_track( + pc_factory_->CreateVideoTrack(video_track_label, NULL)); + stream->AddTrack(video_track.get()); + EXPECT_TRUE(pc_->AddStream(stream, NULL)); + EXPECT_TRUE_WAIT(observer_.renegotiation_needed_, kTimeout); + observer_.renegotiation_needed_ = false; + } + + bool DoCreateOfferAnswer(SessionDescriptionInterface** desc, bool offer) { + talk_base::scoped_refptr + observer(new talk_base::RefCountedObject< + MockCreateSessionDescriptionObserver>()); + if (offer) { + pc_->CreateOffer(observer, NULL); + } else { + pc_->CreateAnswer(observer, NULL); + } + EXPECT_EQ_WAIT(true, observer->called(), kTimeout); + *desc = observer->release_desc(); + return observer->result(); + } + + bool DoCreateOffer(SessionDescriptionInterface** desc) { + return DoCreateOfferAnswer(desc, true); + } + + bool DoCreateAnswer(SessionDescriptionInterface** desc) { + return DoCreateOfferAnswer(desc, false); + } + + bool DoSetSessionDescription(SessionDescriptionInterface* desc, bool local) { + talk_base::scoped_refptr + observer(new talk_base::RefCountedObject< + MockSetSessionDescriptionObserver>()); + if (local) { + pc_->SetLocalDescription(observer, desc); + } else { + pc_->SetRemoteDescription(observer, desc); + } + EXPECT_EQ_WAIT(true, observer->called(), kTimeout); + return observer->result(); + } + + bool DoSetLocalDescription(SessionDescriptionInterface* desc) { + return DoSetSessionDescription(desc, true); + } + + bool DoSetRemoteDescription(SessionDescriptionInterface* desc) { + return DoSetSessionDescription(desc, false); + } + + // Calls PeerConnection::GetStats and check the return value. + // It does not verify the values in the StatReports since a RTCP packet might + // be required. + bool DoGetStats(MediaStreamTrackInterface* track) { + talk_base::scoped_refptr observer( + new talk_base::RefCountedObject()); + if (!pc_->GetStats(observer, track)) + return false; + EXPECT_TRUE_WAIT(observer->called(), kTimeout); + return observer->called(); + } + + void InitiateCall() { + CreatePeerConnection(); + // Create a local stream with audio&video tracks. + AddAudioVideoStream(kStreamLabel1, "audio_label", "video_label"); + CreateOfferReceiveAnswer(); + } + + // Verify that RTP Header extensions has been negotiated for audio and video. + void VerifyRemoteRtpHeaderExtensions() { + const cricket::MediaContentDescription* desc = + cricket::GetFirstAudioContentDescription( + pc_->remote_description()->description()); + ASSERT_TRUE(desc != NULL); + EXPECT_GT(desc->rtp_header_extensions().size(), 0u); + + desc = cricket::GetFirstVideoContentDescription( + pc_->remote_description()->description()); + ASSERT_TRUE(desc != NULL); + EXPECT_GT(desc->rtp_header_extensions().size(), 0u); + } + + void CreateOfferAsRemoteDescription() { + talk_base::scoped_ptr offer; + EXPECT_TRUE(DoCreateOffer(offer.use())); + std::string sdp; + EXPECT_TRUE(offer->ToString(&sdp)); + SessionDescriptionInterface* remote_offer = + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + sdp, NULL); + EXPECT_TRUE(DoSetRemoteDescription(remote_offer)); + EXPECT_EQ(PeerConnectionInterface::kHaveRemoteOffer, observer_.state_); + } + + void CreateAnswerAsLocalDescription() { + scoped_ptr answer; + EXPECT_TRUE(DoCreateAnswer(answer.use())); + + // TODO(perkj): Currently SetLocalDescription fails if any parameters in an + // audio codec change, even if the parameter has nothing to do with + // receiving. Not all parameters are serialized to SDP. + // Since CreatePrAnswerAsLocalDescription serialize/deserialize + // the SessionDescription, it is necessary to do that here to in order to + // get ReceiveOfferCreatePrAnswerAndAnswer and RenegotiateAudioOnly to pass. + // https://code.google.com/p/webrtc/issues/detail?id=1356 + std::string sdp; + EXPECT_TRUE(answer->ToString(&sdp)); + SessionDescriptionInterface* new_answer = + webrtc::CreateSessionDescription(SessionDescriptionInterface::kAnswer, + sdp, NULL); + EXPECT_TRUE(DoSetLocalDescription(new_answer)); + EXPECT_EQ(PeerConnectionInterface::kStable, observer_.state_); + } + + void CreatePrAnswerAsLocalDescription() { + scoped_ptr answer; + EXPECT_TRUE(DoCreateAnswer(answer.use())); + + std::string sdp; + EXPECT_TRUE(answer->ToString(&sdp)); + SessionDescriptionInterface* pr_answer = + webrtc::CreateSessionDescription(SessionDescriptionInterface::kPrAnswer, + sdp, NULL); + EXPECT_TRUE(DoSetLocalDescription(pr_answer)); + EXPECT_EQ(PeerConnectionInterface::kHaveLocalPrAnswer, observer_.state_); + } + + void CreateOfferReceiveAnswer() { + CreateOfferAsLocalDescription(); + std::string sdp; + EXPECT_TRUE(pc_->local_description()->ToString(&sdp)); + CreateAnswerAsRemoteDescription(sdp); + } + + void CreateOfferAsLocalDescription() { + talk_base::scoped_ptr offer; + ASSERT_TRUE(DoCreateOffer(offer.use())); + // TODO(perkj): Currently SetLocalDescription fails if any parameters in an + // audio codec change, even if the parameter has nothing to do with + // receiving. Not all parameters are serialized to SDP. + // Since CreatePrAnswerAsLocalDescription serialize/deserialize + // the SessionDescription, it is necessary to do that here to in order to + // get ReceiveOfferCreatePrAnswerAndAnswer and RenegotiateAudioOnly to pass. + // https://code.google.com/p/webrtc/issues/detail?id=1356 + std::string sdp; + EXPECT_TRUE(offer->ToString(&sdp)); + SessionDescriptionInterface* new_offer = + webrtc::CreateSessionDescription( + SessionDescriptionInterface::kOffer, + sdp, NULL); + + EXPECT_TRUE(DoSetLocalDescription(new_offer)); + EXPECT_EQ(PeerConnectionInterface::kHaveLocalOffer, observer_.state_); + } + + void CreateAnswerAsRemoteDescription(const std::string& offer) { + webrtc::JsepSessionDescription* answer = new webrtc::JsepSessionDescription( + SessionDescriptionInterface::kAnswer); + EXPECT_TRUE(answer->Initialize(offer, NULL)); + EXPECT_TRUE(DoSetRemoteDescription(answer)); + EXPECT_EQ(PeerConnectionInterface::kStable, observer_.state_); + } + + void CreatePrAnswerAndAnswerAsRemoteDescription(const std::string& offer) { + webrtc::JsepSessionDescription* pr_answer = + new webrtc::JsepSessionDescription( + SessionDescriptionInterface::kPrAnswer); + EXPECT_TRUE(pr_answer->Initialize(offer, NULL)); + EXPECT_TRUE(DoSetRemoteDescription(pr_answer)); + EXPECT_EQ(PeerConnectionInterface::kHaveRemotePrAnswer, observer_.state_); + webrtc::JsepSessionDescription* answer = + new webrtc::JsepSessionDescription( + SessionDescriptionInterface::kAnswer); + EXPECT_TRUE(answer->Initialize(offer, NULL)); + EXPECT_TRUE(DoSetRemoteDescription(answer)); + EXPECT_EQ(PeerConnectionInterface::kStable, observer_.state_); + } + + // Help function used for waiting until a the last signaled remote stream has + // the same label as |stream_label|. In a few of the tests in this file we + // answer with the same session description as we offer and thus we can + // check if OnAddStream have been called with the same stream as we offer to + // send. + void WaitAndVerifyOnAddStream(const std::string& stream_label) { + EXPECT_EQ_WAIT(stream_label, observer_.GetLastAddedStreamLabel(), kTimeout); + } + + // Creates an offer and applies it as a local session description. + // Creates an answer with the same SDP an the offer but removes all lines + // that start with a:ssrc" + void CreateOfferReceiveAnswerWithoutSsrc() { + CreateOfferAsLocalDescription(); + std::string sdp; + EXPECT_TRUE(pc_->local_description()->ToString(&sdp)); + SetSsrcToZero(&sdp); + CreateAnswerAsRemoteDescription(sdp); + } + + scoped_refptr port_allocator_factory_; + scoped_refptr pc_factory_; + scoped_refptr pc_; + MockPeerConnectionObserver observer_; +}; + +TEST_F(PeerConnectionInterfaceTest, + CreatePeerConnectionWithDifferentConfigurations) { + CreatePeerConnectionWithDifferentConfigurations(); +} + +TEST_F(PeerConnectionInterfaceTest, AddStreams) { + CreatePeerConnection(); + AddStream(kStreamLabel1); + AddVoiceStream(kStreamLabel2); + ASSERT_EQ(2u, pc_->local_streams()->count()); + + // Fail to add another stream with audio since we already have an audio track. + scoped_refptr stream( + pc_factory_->CreateLocalMediaStream(kStreamLabel3)); + scoped_refptr audio_track( + pc_factory_->CreateAudioTrack( + kStreamLabel3, static_cast(NULL))); + stream->AddTrack(audio_track.get()); + EXPECT_FALSE(pc_->AddStream(stream, NULL)); + + // Remove the stream with the audio track. + pc_->RemoveStream(pc_->local_streams()->at(1)); + + // Test that we now can add the stream with the audio track. + EXPECT_TRUE(pc_->AddStream(stream, NULL)); +} + +TEST_F(PeerConnectionInterfaceTest, RemoveStream) { + CreatePeerConnection(); + AddStream(kStreamLabel1); + ASSERT_EQ(1u, pc_->local_streams()->count()); + pc_->RemoveStream(pc_->local_streams()->at(0)); + EXPECT_EQ(0u, pc_->local_streams()->count()); +} + +TEST_F(PeerConnectionInterfaceTest, CreateOfferReceiveAnswer) { + InitiateCall(); + WaitAndVerifyOnAddStream(kStreamLabel1); + VerifyRemoteRtpHeaderExtensions(); +} + +TEST_F(PeerConnectionInterfaceTest, CreateOfferReceivePrAnswerAndAnswer) { + CreatePeerConnection(); + AddStream(kStreamLabel1); + CreateOfferAsLocalDescription(); + std::string offer; + EXPECT_TRUE(pc_->local_description()->ToString(&offer)); + CreatePrAnswerAndAnswerAsRemoteDescription(offer); + WaitAndVerifyOnAddStream(kStreamLabel1); +} + +TEST_F(PeerConnectionInterfaceTest, ReceiveOfferCreateAnswer) { + CreatePeerConnection(); + AddStream(kStreamLabel1); + + CreateOfferAsRemoteDescription(); + CreateAnswerAsLocalDescription(); + + WaitAndVerifyOnAddStream(kStreamLabel1); +} + +TEST_F(PeerConnectionInterfaceTest, ReceiveOfferCreatePrAnswerAndAnswer) { + CreatePeerConnection(); + AddStream(kStreamLabel1); + + CreateOfferAsRemoteDescription(); + CreatePrAnswerAsLocalDescription(); + CreateAnswerAsLocalDescription(); + + WaitAndVerifyOnAddStream(kStreamLabel1); +} + +TEST_F(PeerConnectionInterfaceTest, Renegotiate) { + InitiateCall(); + ASSERT_EQ(1u, pc_->remote_streams()->count()); + pc_->RemoveStream(pc_->local_streams()->at(0)); + CreateOfferReceiveAnswer(); + EXPECT_EQ(0u, pc_->remote_streams()->count()); + AddStream(kStreamLabel1); + CreateOfferReceiveAnswer(); +} + +// Tests that after negotiating an audio only call, the respondent can perform a +// renegotiation that removes the audio stream. +TEST_F(PeerConnectionInterfaceTest, RenegotiateAudioOnly) { + CreatePeerConnection(); + AddVoiceStream(kStreamLabel1); + CreateOfferAsRemoteDescription(); + CreateAnswerAsLocalDescription(); + + ASSERT_EQ(1u, pc_->remote_streams()->count()); + pc_->RemoveStream(pc_->local_streams()->at(0)); + CreateOfferReceiveAnswer(); + EXPECT_EQ(0u, pc_->remote_streams()->count()); +} + +// Test that candidates are generated and that we can parse our own candidates. +TEST_F(PeerConnectionInterfaceTest, IceCandidates) { + CreatePeerConnection(); + + EXPECT_FALSE(pc_->AddIceCandidate(observer_.last_candidate_.get())); + // SetRemoteDescription takes ownership of offer. + SessionDescriptionInterface* offer = NULL; + AddStream(kStreamLabel1); + EXPECT_TRUE(DoCreateOffer(&offer)); + EXPECT_TRUE(DoSetRemoteDescription(offer)); + + // SetLocalDescription takes ownership of answer. + SessionDescriptionInterface* answer = NULL; + EXPECT_TRUE(DoCreateAnswer(&answer)); + EXPECT_TRUE(DoSetLocalDescription(answer)); + + EXPECT_TRUE_WAIT(observer_.last_candidate_.get() != NULL, kTimeout); + EXPECT_TRUE_WAIT(observer_.ice_complete_, kTimeout); + + EXPECT_TRUE(pc_->AddIceCandidate(observer_.last_candidate_.get())); +} + +// Test that the CreateOffer and CreatAnswer will fail if the track labels are +// not unique. +TEST_F(PeerConnectionInterfaceTest, CreateOfferAnswerWithInvalidStream) { + CreatePeerConnection(); + // Create a regular offer for the CreateAnswer test later. + SessionDescriptionInterface* offer = NULL; + EXPECT_TRUE(DoCreateOffer(&offer)); + EXPECT_TRUE(offer != NULL); + delete offer; + offer = NULL; + + // Create a local stream with audio&video tracks having same label. + AddAudioVideoStream(kStreamLabel1, "track_label", "track_label"); + + // Test CreateOffer + EXPECT_FALSE(DoCreateOffer(&offer)); + + // Test CreateAnswer + SessionDescriptionInterface* answer = NULL; + EXPECT_FALSE(DoCreateAnswer(&answer)); +} + +// Test that we will get different SSRCs for each tracks in the offer and answer +// we created. +TEST_F(PeerConnectionInterfaceTest, SsrcInOfferAnswer) { + CreatePeerConnection(); + // Create a local stream with audio&video tracks having different labels. + AddAudioVideoStream(kStreamLabel1, "audio_label", "video_label"); + + // Test CreateOffer + scoped_ptr offer; + EXPECT_TRUE(DoCreateOffer(offer.use())); + int audio_ssrc = 0; + int video_ssrc = 0; + EXPECT_TRUE(GetFirstSsrc(GetFirstAudioContent(offer->description()), + &audio_ssrc)); + EXPECT_TRUE(GetFirstSsrc(GetFirstVideoContent(offer->description()), + &video_ssrc)); + EXPECT_NE(audio_ssrc, video_ssrc); + + // Test CreateAnswer + EXPECT_TRUE(DoSetRemoteDescription(offer.release())); + scoped_ptr answer; + EXPECT_TRUE(DoCreateAnswer(answer.use())); + audio_ssrc = 0; + video_ssrc = 0; + EXPECT_TRUE(GetFirstSsrc(GetFirstAudioContent(answer->description()), + &audio_ssrc)); + EXPECT_TRUE(GetFirstSsrc(GetFirstVideoContent(answer->description()), + &video_ssrc)); + EXPECT_NE(audio_ssrc, video_ssrc); +} + +// Test that we can specify a certain track that we want statistics about. +TEST_F(PeerConnectionInterfaceTest, GetStatsForSpecificTrack) { + InitiateCall(); + ASSERT_LT(0u, pc_->remote_streams()->count()); + ASSERT_LT(0u, pc_->remote_streams()->at(0)->GetAudioTracks().size()); + scoped_refptr remote_audio = + pc_->remote_streams()->at(0)->GetAudioTracks()[0]; + EXPECT_TRUE(DoGetStats(remote_audio)); + + // Remove the stream. Since we are sending to our selves the local + // and the remote stream is the same. + pc_->RemoveStream(pc_->local_streams()->at(0)); + // Do a re-negotiation. + CreateOfferReceiveAnswer(); + + ASSERT_EQ(0u, pc_->remote_streams()->count()); + + // Test that we still can get statistics for the old track. Even if it is not + // sent any longer. + EXPECT_TRUE(DoGetStats(remote_audio)); +} + +// Test that we can get stats on a video track. +TEST_F(PeerConnectionInterfaceTest, GetStatsForVideoTrack) { + InitiateCall(); + ASSERT_LT(0u, pc_->remote_streams()->count()); + ASSERT_LT(0u, pc_->remote_streams()->at(0)->GetVideoTracks().size()); + scoped_refptr remote_video = + pc_->remote_streams()->at(0)->GetVideoTracks()[0]; + EXPECT_TRUE(DoGetStats(remote_video)); +} + +// Test that we don't get statistics for an invalid track. +TEST_F(PeerConnectionInterfaceTest, GetStatsForInvalidTrack) { + InitiateCall(); + scoped_refptr unknown_audio_track( + pc_factory_->CreateAudioTrack("unknown track", NULL)); + EXPECT_FALSE(DoGetStats(unknown_audio_track)); +} + +// This test setup two RTP data channels in loop back. +#ifdef WIN32 +// TODO(perkj): Investigate why the transport channel sometimes don't become +// writable on Windows when we try to connect in loop back. +TEST_F(PeerConnectionInterfaceTest, DISABLED_TestDataChannel) { +#else +TEST_F(PeerConnectionInterfaceTest, TestDataChannel) { +#endif + FakeConstraints constraints; + constraints.SetAllowRtpDataChannels(); + CreatePeerConnection(&constraints); + scoped_refptr data1 = + pc_->CreateDataChannel("test1", NULL); + scoped_refptr data2 = + pc_->CreateDataChannel("test2", NULL); + ASSERT_TRUE(data1 != NULL); + talk_base::scoped_ptr observer1( + new MockDataChannelObserver(data1)); + talk_base::scoped_ptr observer2( + new MockDataChannelObserver(data2)); + + EXPECT_EQ(DataChannelInterface::kConnecting, data1->state()); + EXPECT_EQ(DataChannelInterface::kConnecting, data2->state()); + std::string data_to_send1 = "testing testing"; + std::string data_to_send2 = "testing something else"; + EXPECT_FALSE(data1->Send(DataBuffer(data_to_send1))); + + CreateOfferReceiveAnswer(); + EXPECT_TRUE_WAIT(observer1->IsOpen(), kTimeout); + EXPECT_TRUE_WAIT(observer2->IsOpen(), kTimeout); + + EXPECT_EQ(DataChannelInterface::kOpen, data1->state()); + EXPECT_EQ(DataChannelInterface::kOpen, data2->state()); + EXPECT_TRUE(data1->Send(DataBuffer(data_to_send1))); + EXPECT_TRUE(data2->Send(DataBuffer(data_to_send2))); + + EXPECT_EQ_WAIT(data_to_send1, observer1->last_message(), kTimeout); + EXPECT_EQ_WAIT(data_to_send2, observer2->last_message(), kTimeout); + + data1->Close(); + EXPECT_EQ(DataChannelInterface::kClosing, data1->state()); + CreateOfferReceiveAnswer(); + EXPECT_FALSE(observer1->IsOpen()); + EXPECT_EQ(DataChannelInterface::kClosed, data1->state()); + EXPECT_TRUE(observer2->IsOpen()); + + data_to_send2 = "testing something else again"; + EXPECT_TRUE(data2->Send(DataBuffer(data_to_send2))); + + EXPECT_EQ_WAIT(data_to_send2, observer2->last_message(), kTimeout); +} + +// This test verifies that sendnig binary data over RTP data channels should +// fail. +#ifdef WIN32 +// TODO(perkj): Investigate why the transport channel sometimes don't become +// writable on Windows when we try to connect in loop back. +TEST_F(PeerConnectionInterfaceTest, DISABLED_TestSendBinaryOnRtpDataChannel) { +#else +TEST_F(PeerConnectionInterfaceTest, TestSendBinaryOnRtpDataChannel) { +#endif + FakeConstraints constraints; + constraints.SetAllowRtpDataChannels(); + CreatePeerConnection(&constraints); + scoped_refptr data1 = + pc_->CreateDataChannel("test1", NULL); + scoped_refptr data2 = + pc_->CreateDataChannel("test2", NULL); + ASSERT_TRUE(data1 != NULL); + talk_base::scoped_ptr observer1( + new MockDataChannelObserver(data1)); + talk_base::scoped_ptr observer2( + new MockDataChannelObserver(data2)); + + EXPECT_EQ(DataChannelInterface::kConnecting, data1->state()); + EXPECT_EQ(DataChannelInterface::kConnecting, data2->state()); + + CreateOfferReceiveAnswer(); + EXPECT_TRUE_WAIT(observer1->IsOpen(), kTimeout); + EXPECT_TRUE_WAIT(observer2->IsOpen(), kTimeout); + + EXPECT_EQ(DataChannelInterface::kOpen, data1->state()); + EXPECT_EQ(DataChannelInterface::kOpen, data2->state()); + + talk_base::Buffer buffer("test", 4); + EXPECT_FALSE(data1->Send(DataBuffer(buffer, true))); +} + +// This test setup a RTP data channels in loop back and test that a channel is +// opened even if the remote end answer with a zero SSRC. +#ifdef WIN32 +// TODO(perkj): Investigate why the transport channel sometimes don't become +// writable on Windows when we try to connect in loop back. +TEST_F(PeerConnectionInterfaceTest, DISABLED_TestSendOnlyDataChannel) { +#else +TEST_F(PeerConnectionInterfaceTest, TestSendOnlyDataChannel) { +#endif + FakeConstraints constraints; + constraints.SetAllowRtpDataChannels(); + CreatePeerConnection(&constraints); + scoped_refptr data1 = + pc_->CreateDataChannel("test1", NULL); + talk_base::scoped_ptr observer1( + new MockDataChannelObserver(data1)); + + CreateOfferReceiveAnswerWithoutSsrc(); + + EXPECT_TRUE_WAIT(observer1->IsOpen(), kTimeout); + + data1->Close(); + EXPECT_EQ(DataChannelInterface::kClosing, data1->state()); + CreateOfferReceiveAnswerWithoutSsrc(); + EXPECT_EQ(DataChannelInterface::kClosed, data1->state()); + EXPECT_FALSE(observer1->IsOpen()); +} + +// This test that if a data channel is added in an answer a receive only channel +// channel is created. +TEST_F(PeerConnectionInterfaceTest, TestReceiveOnlyDataChannel) { + FakeConstraints constraints; + constraints.SetAllowRtpDataChannels(); + CreatePeerConnection(&constraints); + + std::string offer_label = "offer_channel"; + scoped_refptr offer_channel = + pc_->CreateDataChannel(offer_label, NULL); + + CreateOfferAsLocalDescription(); + + // Replace the data channel label in the offer and apply it as an answer. + std::string receive_label = "answer_channel"; + std::string sdp; + EXPECT_TRUE(pc_->local_description()->ToString(&sdp)); + talk_base::replace_substrs(offer_label.c_str(), offer_label.length(), + receive_label.c_str(), receive_label.length(), + &sdp); + CreateAnswerAsRemoteDescription(sdp); + + // Verify that a new incoming data channel has been created and that + // it is open but can't we written to. + ASSERT_TRUE(observer_.last_datachannel_ != NULL); + DataChannelInterface* received_channel = observer_.last_datachannel_; + EXPECT_EQ(DataChannelInterface::kConnecting, received_channel->state()); + EXPECT_EQ(receive_label, received_channel->label()); + EXPECT_FALSE(received_channel->Send(DataBuffer("something"))); + + // Verify that the channel we initially offered has been rejected. + EXPECT_EQ(DataChannelInterface::kClosed, offer_channel->state()); + + // Do another offer / answer exchange and verify that the data channel is + // opened. + CreateOfferReceiveAnswer(); + EXPECT_EQ_WAIT(DataChannelInterface::kOpen, received_channel->state(), + kTimeout); +} + +// This test that no data channel is returned if a reliable channel is +// requested. +// TODO(perkj): Remove this test once reliable channels are implemented. +TEST_F(PeerConnectionInterfaceTest, CreateReliableRtpDataChannelShouldFail) { + FakeConstraints constraints; + constraints.SetAllowRtpDataChannels(); + CreatePeerConnection(&constraints); + + std::string label = "test"; + webrtc::DataChannelInit config; + config.reliable = true; + scoped_refptr channel = + pc_->CreateDataChannel(label, &config); + EXPECT_TRUE(channel == NULL); +} + +// This tests that a SCTP data channel is returned using different +// DataChannelInit configurations. +TEST_F(PeerConnectionInterfaceTest, CreateSctpDataChannel) { + FakeConstraints constraints; + constraints.SetAllowDtlsSctpDataChannels(); + CreatePeerConnection(&constraints); + + webrtc::DataChannelInit config; + + scoped_refptr channel = + pc_->CreateDataChannel("1", &config); + EXPECT_TRUE(channel != NULL); + EXPECT_TRUE(channel->reliable()); + + config.ordered = false; + channel = pc_->CreateDataChannel("2", &config); + EXPECT_TRUE(channel != NULL); + EXPECT_TRUE(channel->reliable()); + + config.ordered = true; + config.maxRetransmits = 0; + channel = pc_->CreateDataChannel("3", &config); + EXPECT_TRUE(channel != NULL); + EXPECT_FALSE(channel->reliable()); + + config.maxRetransmits = -1; + config.maxRetransmitTime = 0; + channel = pc_->CreateDataChannel("4", &config); + EXPECT_TRUE(channel != NULL); + EXPECT_FALSE(channel->reliable()); +} + +// This tests that no data channel is returned if both maxRetransmits and +// maxRetransmitTime are set for SCTP data channels. +TEST_F(PeerConnectionInterfaceTest, + CreateSctpDataChannelShouldFailForInvalidConfig) { + FakeConstraints constraints; + constraints.SetAllowDtlsSctpDataChannels(); + CreatePeerConnection(&constraints); + + std::string label = "test"; + webrtc::DataChannelInit config; + config.maxRetransmits = 0; + config.maxRetransmitTime = 0; + + scoped_refptr channel = + pc_->CreateDataChannel(label, &config); + EXPECT_TRUE(channel == NULL); +} + +// The test verifies that the first id not used by existing data channels is +// assigned to a new data channel if no id is specified. +TEST_F(PeerConnectionInterfaceTest, AssignSctpDataChannelId) { + FakeConstraints constraints; + constraints.SetAllowDtlsSctpDataChannels(); + CreatePeerConnection(&constraints); + + webrtc::DataChannelInit config; + + scoped_refptr channel = + pc_->CreateDataChannel("1", &config); + EXPECT_TRUE(channel != NULL); + EXPECT_EQ(1, channel->id()); + + config.id = 4; + channel = pc_->CreateDataChannel("4", &config); + EXPECT_TRUE(channel != NULL); + EXPECT_EQ(config.id, channel->id()); + + config.id = -1; + channel = pc_->CreateDataChannel("2", &config); + EXPECT_TRUE(channel != NULL); + EXPECT_EQ(2, channel->id()); +} + +// The test verifies that creating a SCTP data channel with an id already in use +// or out of range should fail. +TEST_F(PeerConnectionInterfaceTest, + CreateSctpDataChannelWithInvalidIdShouldFail) { + FakeConstraints constraints; + constraints.SetAllowDtlsSctpDataChannels(); + CreatePeerConnection(&constraints); + + webrtc::DataChannelInit config; + + scoped_refptr channel = + pc_->CreateDataChannel("1", &config); + EXPECT_TRUE(channel != NULL); + EXPECT_EQ(1, channel->id()); + + config.id = 1; + channel = pc_->CreateDataChannel("x", &config); + EXPECT_TRUE(channel == NULL); + + config.id = cricket::kMaxSctpSid; + channel = pc_->CreateDataChannel("max", &config); + EXPECT_TRUE(channel != NULL); + EXPECT_EQ(config.id, channel->id()); + + config.id = cricket::kMaxSctpSid + 1; + channel = pc_->CreateDataChannel("x", &config); + EXPECT_TRUE(channel == NULL); +} + +// This test that a data channel closes when a PeerConnection is deleted/closed. +#ifdef WIN32 +// TODO(perkj): Investigate why the transport channel sometimes don't become +// writable on Windows when we try to connect in loop back. +TEST_F(PeerConnectionInterfaceTest, + DISABLED_DataChannelCloseWhenPeerConnectionClose) { +#else +TEST_F(PeerConnectionInterfaceTest, DataChannelCloseWhenPeerConnectionClose) { +#endif + FakeConstraints constraints; + constraints.SetAllowRtpDataChannels(); + CreatePeerConnection(&constraints); + + scoped_refptr data1 = + pc_->CreateDataChannel("test1", NULL); + scoped_refptr data2 = + pc_->CreateDataChannel("test2", NULL); + ASSERT_TRUE(data1 != NULL); + talk_base::scoped_ptr observer1( + new MockDataChannelObserver(data1)); + talk_base::scoped_ptr observer2( + new MockDataChannelObserver(data2)); + + CreateOfferReceiveAnswer(); + EXPECT_TRUE_WAIT(observer1->IsOpen(), kTimeout); + EXPECT_TRUE_WAIT(observer2->IsOpen(), kTimeout); + + ReleasePeerConnection(); + EXPECT_EQ(DataChannelInterface::kClosed, data1->state()); + EXPECT_EQ(DataChannelInterface::kClosed, data2->state()); +} + +// This test that data channels can be rejected in an answer. +TEST_F(PeerConnectionInterfaceTest, TestRejectDataChannelInAnswer) { + FakeConstraints constraints; + constraints.SetAllowRtpDataChannels(); + CreatePeerConnection(&constraints); + + scoped_refptr offer_channel( + pc_->CreateDataChannel("offer_channel", NULL)); + + CreateOfferAsLocalDescription(); + + // Create an answer where the m-line for data channels are rejected. + std::string sdp; + EXPECT_TRUE(pc_->local_description()->ToString(&sdp)); + webrtc::JsepSessionDescription* answer = new webrtc::JsepSessionDescription( + SessionDescriptionInterface::kAnswer); + EXPECT_TRUE(answer->Initialize(sdp, NULL)); + cricket::ContentInfo* data_info = + answer->description()->GetContentByName("data"); + data_info->rejected = true; + + DoSetRemoteDescription(answer); + EXPECT_EQ(DataChannelInterface::kClosed, offer_channel->state()); +} + +// Test that we can create a session description from an SDP string from +// FireFox, use it as a remote session description, generate an answer and use +// the answer as a local description. +TEST_F(PeerConnectionInterfaceTest, ReceiveFireFoxOffer) { + MAYBE_SKIP_TEST(talk_base::SSLStreamAdapter::HaveDtlsSrtp); + FakeConstraints constraints; + constraints.AddMandatory(webrtc::MediaConstraintsInterface::kEnableDtlsSrtp, + true); + CreatePeerConnection(&constraints); + AddAudioVideoStream(kStreamLabel1, "audio_label", "video_label"); + SessionDescriptionInterface* desc = + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + webrtc::kFireFoxSdpOffer); + EXPECT_TRUE(DoSetSessionDescription(desc, false)); + CreateAnswerAsLocalDescription(); + ASSERT_TRUE(pc_->local_description() != NULL); + ASSERT_TRUE(pc_->remote_description() != NULL); + + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(pc_->local_description()->description()); + ASSERT_TRUE(content != NULL); + EXPECT_FALSE(content->rejected); + + content = + cricket::GetFirstVideoContent(pc_->local_description()->description()); + ASSERT_TRUE(content != NULL); + EXPECT_FALSE(content->rejected); + + content = + cricket::GetFirstDataContent(pc_->local_description()->description()); + ASSERT_TRUE(content != NULL); + EXPECT_TRUE(content->rejected); +} + +// Test that we can create an audio only offer and receive an answer with a +// limited set of audio codecs and receive an updated offer with more audio +// codecs, where the added codecs are not supported. +TEST_F(PeerConnectionInterfaceTest, ReceiveUpdatedAudioOfferWithBadCodecs) { + CreatePeerConnection(); + AddVoiceStream("audio_label"); + CreateOfferAsLocalDescription(); + + SessionDescriptionInterface* answer = + webrtc::CreateSessionDescription(SessionDescriptionInterface::kAnswer, + webrtc::kAudioSdp); + EXPECT_TRUE(DoSetSessionDescription(answer, false)); + + SessionDescriptionInterface* updated_offer = + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + webrtc::kAudioSdpWithUnsupportedCodecs); + EXPECT_TRUE(DoSetSessionDescription(updated_offer, false)); + CreateAnswerAsLocalDescription(); +} + +// Test that PeerConnection::Close changes the states to closed and all remote +// tracks change state to ended. +TEST_F(PeerConnectionInterfaceTest, CloseAndTestStreamsAndStates) { + // Initialize a PeerConnection and negotiate local and remote session + // description. + InitiateCall(); + ASSERT_EQ(1u, pc_->local_streams()->count()); + ASSERT_EQ(1u, pc_->remote_streams()->count()); + + pc_->Close(); + + EXPECT_EQ(PeerConnectionInterface::kClosed, pc_->signaling_state()); + EXPECT_EQ(PeerConnectionInterface::kIceConnectionClosed, + pc_->ice_connection_state()); + EXPECT_EQ(PeerConnectionInterface::kIceGatheringComplete, + pc_->ice_gathering_state()); + + EXPECT_EQ(1u, pc_->local_streams()->count()); + EXPECT_EQ(1u, pc_->remote_streams()->count()); + + scoped_refptr remote_stream = + pc_->remote_streams()->at(0); + EXPECT_EQ(MediaStreamTrackInterface::kEnded, + remote_stream->GetVideoTracks()[0]->state()); + EXPECT_EQ(MediaStreamTrackInterface::kEnded, + remote_stream->GetAudioTracks()[0]->state()); +} + +// Test that PeerConnection methods fails gracefully after +// PeerConnection::Close has been called. +TEST_F(PeerConnectionInterfaceTest, CloseAndTestMethods) { + CreatePeerConnection(); + AddAudioVideoStream(kStreamLabel1, "audio_label", "video_label"); + CreateOfferAsRemoteDescription(); + CreateAnswerAsLocalDescription(); + + ASSERT_EQ(1u, pc_->local_streams()->count()); + scoped_refptr local_stream = + pc_->local_streams()->at(0); + + pc_->Close(); + + pc_->RemoveStream(local_stream); + EXPECT_FALSE(pc_->AddStream(local_stream, NULL)); + + ASSERT_FALSE(local_stream->GetAudioTracks().empty()); + talk_base::scoped_refptr dtmf_sender( + pc_->CreateDtmfSender(local_stream->GetAudioTracks()[0])); + EXPECT_FALSE(dtmf_sender->CanInsertDtmf()); + + EXPECT_TRUE(pc_->CreateDataChannel("test", NULL) == NULL); + + EXPECT_TRUE(pc_->local_description() != NULL); + EXPECT_TRUE(pc_->remote_description() != NULL); + + talk_base::scoped_ptr offer; + EXPECT_TRUE(DoCreateOffer(offer.use())); + talk_base::scoped_ptr answer; + EXPECT_TRUE(DoCreateAnswer(answer.use())); + + std::string sdp; + ASSERT_TRUE(pc_->remote_description()->ToString(&sdp)); + SessionDescriptionInterface* remote_offer = + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + sdp, NULL); + EXPECT_FALSE(DoSetRemoteDescription(remote_offer)); + + ASSERT_TRUE(pc_->local_description()->ToString(&sdp)); + SessionDescriptionInterface* local_offer = + webrtc::CreateSessionDescription(SessionDescriptionInterface::kOffer, + sdp, NULL); + EXPECT_FALSE(DoSetLocalDescription(local_offer)); +} + +// Test that GetStats can still be called after PeerConnection::Close. +TEST_F(PeerConnectionInterfaceTest, CloseAndGetStats) { + InitiateCall(); + pc_->Close(); + DoGetStats(NULL); +} diff --git a/talk/app/webrtc/peerconnectionproxy.h b/talk/app/webrtc/peerconnectionproxy.h new file mode 100644 index 000000000..f07416d65 --- /dev/null +++ b/talk/app/webrtc/peerconnectionproxy.h @@ -0,0 +1,72 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_PEERCONNECTIONPROXY_H_ +#define TALK_APP_WEBRTC_PEERCONNECTIONPROXY_H_ + +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/app/webrtc/proxy.h" + +namespace webrtc { + +// Define proxy for PeerConnectionInterface. +BEGIN_PROXY_MAP(PeerConnection) + PROXY_METHOD0(talk_base::scoped_refptr, + local_streams) + PROXY_METHOD0(talk_base::scoped_refptr, + remote_streams) + PROXY_METHOD2(bool, AddStream, MediaStreamInterface*, + const MediaConstraintsInterface*) + PROXY_METHOD1(void, RemoveStream, MediaStreamInterface*) + PROXY_METHOD1(talk_base::scoped_refptr, + CreateDtmfSender, AudioTrackInterface*) + PROXY_METHOD2(bool, GetStats, StatsObserver*, MediaStreamTrackInterface*) + PROXY_METHOD2(talk_base::scoped_refptr, + CreateDataChannel, const std::string&, const DataChannelInit*) + PROXY_CONSTMETHOD0(const SessionDescriptionInterface*, local_description) + PROXY_CONSTMETHOD0(const SessionDescriptionInterface*, remote_description) + PROXY_METHOD2(void, CreateOffer, CreateSessionDescriptionObserver*, + const MediaConstraintsInterface*) + PROXY_METHOD2(void, CreateAnswer, CreateSessionDescriptionObserver*, + const MediaConstraintsInterface*) + PROXY_METHOD2(void, SetLocalDescription, SetSessionDescriptionObserver*, + SessionDescriptionInterface*) + PROXY_METHOD2(void, SetRemoteDescription, SetSessionDescriptionObserver*, + SessionDescriptionInterface*) + PROXY_METHOD2(bool, UpdateIce, const IceServers&, + const MediaConstraintsInterface*) + PROXY_METHOD1(bool, AddIceCandidate, const IceCandidateInterface*) + PROXY_METHOD0(SignalingState, signaling_state) + PROXY_METHOD0(IceState, ice_state) + PROXY_METHOD0(IceConnectionState, ice_connection_state) + PROXY_METHOD0(IceGatheringState, ice_gathering_state) + PROXY_METHOD0(void, Close) +END_PROXY() + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_PEERCONNECTIONPROXY_H_ diff --git a/talk/app/webrtc/portallocatorfactory.cc b/talk/app/webrtc/portallocatorfactory.cc new file mode 100644 index 000000000..59ac9fb3f --- /dev/null +++ b/talk/app/webrtc/portallocatorfactory.cc @@ -0,0 +1,92 @@ +/* + * libjingle + * Copyright 2004--2011, 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/app/webrtc/portallocatorfactory.h" + +#include "talk/base/logging.h" +#include "talk/base/network.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/client/httpportallocator.h" + +static const char kUserAgent[] = "PeerConnection User Agent"; + +namespace webrtc { + +using talk_base::scoped_ptr; + +talk_base::scoped_refptr +PortAllocatorFactory::Create( + talk_base::Thread* worker_thread) { + talk_base::RefCountedObject* allocator = + new talk_base::RefCountedObject(worker_thread); + return allocator; +} + +PortAllocatorFactory::PortAllocatorFactory(talk_base::Thread* worker_thread) + : network_manager_(new talk_base::BasicNetworkManager()), + socket_factory_(new talk_base::BasicPacketSocketFactory(worker_thread)) { +} + +PortAllocatorFactory::~PortAllocatorFactory() {} + +cricket::PortAllocator* PortAllocatorFactory::CreatePortAllocator( + const std::vector& stun, + const std::vector& turn) { + std::vector stun_hosts; + typedef std::vector::const_iterator StunIt; + for (StunIt stun_it = stun.begin(); stun_it != stun.end(); ++stun_it) { + stun_hosts.push_back(stun_it->server); + } + + talk_base::SocketAddress stun_addr; + if (!stun_hosts.empty()) { + stun_addr = stun_hosts.front(); + } + scoped_ptr allocator( + new cricket::BasicPortAllocator( + network_manager_.get(), socket_factory_.get(), stun_addr)); + + if (turn.size() > 0) { + cricket::RelayCredentials credentials(turn[0].username, turn[0].password); + cricket::RelayServerConfig relay_server(cricket::RELAY_TURN); + cricket::ProtocolType protocol; + if (cricket::StringToProto(turn[0].transport_type.c_str(), &protocol)) { + relay_server.ports.push_back(cricket::ProtocolAddress( + turn[0].server, protocol)); + relay_server.credentials = credentials; + allocator->AddRelay(relay_server); + } else { + LOG(LS_WARNING) << "Ignoring TURN server " << turn[0].server << ". " + << "Reason= Incorrect " << turn[0].transport_type + << " transport parameter."; + } + } + return allocator.release(); +} + +} // namespace webrtc diff --git a/talk/app/webrtc/portallocatorfactory.h b/talk/app/webrtc/portallocatorfactory.h new file mode 100644 index 000000000..e30024cb0 --- /dev/null +++ b/talk/app/webrtc/portallocatorfactory.h @@ -0,0 +1,70 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +// This file defines the default implementation of +// PortAllocatorFactoryInterface. +// This implementation creates instances of cricket::HTTPPortAllocator and uses +// the BasicNetworkManager and BasicPacketSocketFactory. + +#ifndef TALK_APP_WEBRTC_PORTALLOCATORFACTORY_H_ +#define TALK_APP_WEBRTC_PORTALLOCATORFACTORY_H_ + +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/base/scoped_ptr.h" + +namespace cricket { +class PortAllocator; +} + +namespace talk_base { +class BasicNetworkManager; +class BasicPacketSocketFactory; +} + +namespace webrtc { + +class PortAllocatorFactory : public PortAllocatorFactoryInterface { + public: + static talk_base::scoped_refptr Create( + talk_base::Thread* worker_thread); + + virtual cricket::PortAllocator* CreatePortAllocator( + const std::vector& stun, + const std::vector& turn); + + protected: + explicit PortAllocatorFactory(talk_base::Thread* worker_thread); + ~PortAllocatorFactory(); + + private: + talk_base::scoped_ptr network_manager_; + talk_base::scoped_ptr socket_factory_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_PORTALLOCATORFACTORY_H_ diff --git a/talk/app/webrtc/proxy.h b/talk/app/webrtc/proxy.h new file mode 100644 index 000000000..4db4befa6 --- /dev/null +++ b/talk/app/webrtc/proxy.h @@ -0,0 +1,287 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +// This file contains Macros for creating proxies for webrtc MediaStream and +// PeerConnection classes. + +// +// Example usage: +// +// class TestInterface : public talk_base::RefCountInterface { +// public: +// std::string FooA() = 0; +// std::string FooB(bool arg1) const = 0; +// std::string FooC(bool arg1)= 0; +// }; +// +// Note that return types can not be a const reference. +// +// class Test : public TestInterface { +// ... implementation of the interface. +// }; +// +// BEGIN_PROXY_MAP(Test) +// PROXY_METHOD0(std::string, FooA) +// PROXY_CONSTMETHOD1(std::string, FooB, arg1) +// PROXY_METHOD1(std::string, FooC, arg1) +// END_PROXY() +// +// The proxy can be created using TestProxy::Create(Thread*, TestInterface*). + +#ifndef TALK_APP_WEBRTC_PROXY_H_ +#define TALK_APP_WEBRTC_PROXY_H_ + +#include "talk/base/thread.h" + +namespace webrtc { + +template +class ReturnType { + public: + template + void Invoke(C* c, M m) { r_ = (c->*m)(); } + template + void Invoke(C* c, M m, T1 a1) { r_ = (c->*m)(a1); } + template + void Invoke(C* c, M m, T1 a1, T2 a2) { r_ = (c->*m)(a1, a2); } + template + void Invoke(C* c, M m, T1 a1, T2 a2, T3 a3) { r_ = (c->*m)(a1, a2, a3); } + + R value() { return r_; } + + private: + R r_; +}; + +template <> +class ReturnType { + public: + template + void Invoke(C* c, M m) { (c->*m)(); } + template + void Invoke(C* c, M m, T1 a1) { (c->*m)(a1); } + template + void Invoke(C* c, M m, T1 a1, T2 a2) { (c->*m)(a1, a2); } + template + void Invoke(C* c, M m, T1 a1, T2 a2, T3 a3) { (c->*m)(a1, a2, a3); } + + void value() {} +}; + +template +class MethodCall0 : public talk_base::Message, + public talk_base::MessageHandler { + public: + typedef R (C::*Method)(); + MethodCall0(C* c, Method m) : c_(c), m_(m) {} + + R Marshal(talk_base::Thread* t) { + t->Send(this, 0); + return r_.value(); + } + + private: + void OnMessage(talk_base::Message*) { r_.Invoke(c_, m_);} + + C* c_; + Method m_; + ReturnType r_; +}; + +template +class ConstMethodCall0 : public talk_base::Message, + public talk_base::MessageHandler { + public: + typedef R (C::*Method)() const; + ConstMethodCall0(C* c, Method m) : c_(c), m_(m) {} + + R Marshal(talk_base::Thread* t) { + t->Send(this, 0); + return r_.value(); + } + + private: + void OnMessage(talk_base::Message*) { r_.Invoke(c_, m_); } + + C* c_; + Method m_; + ReturnType r_; +}; + +template +class MethodCall1 : public talk_base::Message, + public talk_base::MessageHandler { + public: + typedef R (C::*Method)(T1 a1); + MethodCall1(C* c, Method m, T1 a1) : c_(c), m_(m), a1_(a1) {} + + R Marshal(talk_base::Thread* t) { + t->Send(this, 0); + return r_.value(); + } + + private: + void OnMessage(talk_base::Message*) { r_.Invoke(c_, m_, a1_); } + + C* c_; + Method m_; + ReturnType r_; + T1 a1_; +}; + +template +class ConstMethodCall1 : public talk_base::Message, + public talk_base::MessageHandler { + public: + typedef R (C::*Method)(T1 a1) const; + ConstMethodCall1(C* c, Method m, T1 a1) : c_(c), m_(m), a1_(a1) {} + + R Marshal(talk_base::Thread* t) { + t->Send(this, 0); + return r_.value(); + } + + private: + void OnMessage(talk_base::Message*) { r_.Invoke(c_, m_, a1_); } + + C* c_; + Method m_; + ReturnType r_; + T1 a1_; +}; + +template +class MethodCall2 : public talk_base::Message, + public talk_base::MessageHandler { + public: + typedef R (C::*Method)(T1 a1, T2 a2); + MethodCall2(C* c, Method m, T1 a1, T2 a2) : c_(c), m_(m), a1_(a1), a2_(a2) {} + + R Marshal(talk_base::Thread* t) { + t->Send(this, 0); + return r_.value(); + } + + private: + void OnMessage(talk_base::Message*) { r_.Invoke(c_, m_, a1_, a2_); } + + C* c_; + Method m_; + ReturnType r_; + T1 a1_; + T2 a2_; +}; + +template +class MethodCall3 : public talk_base::Message, + public talk_base::MessageHandler { + public: + typedef R (C::*Method)(T1 a1, T2 a2, T3 a3); + MethodCall3(C* c, Method m, T1 a1, T2 a2, T3 a3) + : c_(c), m_(m), a1_(a1), a2_(a2), a3_(a3) {} + + R Marshal(talk_base::Thread* t) { + t->Send(this, 0); + return r_.value(); + } + + private: + void OnMessage(talk_base::Message*) { r_.Invoke(c_, m_, a1_, a2_, a3_); } + + C* c_; + Method m_; + ReturnType r_; + T1 a1_; + T2 a2_; + T3 a3_; +}; + +#define BEGIN_PROXY_MAP(c) \ + class c##Proxy : public c##Interface {\ + protected:\ + typedef c##Interface C;\ + c##Proxy(talk_base::Thread* thread, C* c)\ + : owner_thread_(thread), \ + c_(c) {}\ + ~c##Proxy() {\ + MethodCall0 call(this, &c##Proxy::Release_s);\ + call.Marshal(owner_thread_);\ + }\ + public:\ + static talk_base::scoped_refptr Create(talk_base::Thread* thread, \ + C* c) {\ + return new talk_base::RefCountedObject(thread, c);\ + }\ + +#define PROXY_METHOD0(r, method)\ + r method() OVERRIDE {\ + MethodCall0 call(c_.get(), &C::method);\ + return call.Marshal(owner_thread_);\ + }\ + +#define PROXY_CONSTMETHOD0(r, method)\ + r method() const OVERRIDE {\ + ConstMethodCall0 call(c_.get(), &C::method);\ + return call.Marshal(owner_thread_);\ + }\ + +#define PROXY_METHOD1(r, method, t1)\ + r method(t1 a1) OVERRIDE {\ + MethodCall1 call(c_.get(), &C::method, a1);\ + return call.Marshal(owner_thread_);\ + }\ + +#define PROXY_CONSTMETHOD1(r, method, t1)\ + r method(t1 a1) const OVERRIDE {\ + ConstMethodCall1 call(c_.get(), &C::method, a1);\ + return call.Marshal(owner_thread_);\ + }\ + +#define PROXY_METHOD2(r, method, t1, t2)\ + r method(t1 a1, t2 a2) OVERRIDE {\ + MethodCall2 call(c_.get(), &C::method, a1, a2);\ + return call.Marshal(owner_thread_);\ + }\ + +#define PROXY_METHOD3(r, method, t1, t2, t3)\ + r method(t1 a1, t2 a2, t3 a3) OVERRIDE {\ + MethodCall3 call(c_.get(), &C::method, a1, a2, a3);\ + return call.Marshal(owner_thread_);\ + }\ + +#define END_PROXY() \ + private:\ + void Release_s() {\ + c_ = NULL;\ + }\ + mutable talk_base::Thread* owner_thread_;\ + talk_base::scoped_refptr c_;\ + };\ + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_PROXY_H_ diff --git a/talk/app/webrtc/proxy_unittest.cc b/talk/app/webrtc/proxy_unittest.cc new file mode 100644 index 000000000..71a583c67 --- /dev/null +++ b/talk/app/webrtc/proxy_unittest.cc @@ -0,0 +1,170 @@ +/* + * libjingle + * Copyright 2013, 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/app/webrtc/proxy.h" + +#include + +#include "talk/base/refcount.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/thread.h" +#include "talk/base/gunit.h" +#include "testing/base/public/gmock.h" + +using ::testing::_; +using ::testing::DoAll; +using ::testing::Exactly; +using ::testing::InvokeWithoutArgs; +using ::testing::Return; + +namespace webrtc { + +// Interface used for testing here. +class FakeInterface : public talk_base::RefCountInterface { + public: + virtual void VoidMethod0() = 0; + virtual std::string Method0() = 0; + virtual std::string ConstMethod0() const = 0; + virtual std::string Method1(std::string s) = 0; + virtual std::string ConstMethod1(std::string s) const = 0; + virtual std::string Method2(std::string s1, std::string s2) = 0; + + protected: + ~FakeInterface() {} +}; + +// Proxy for the test interface. +BEGIN_PROXY_MAP(Fake) + PROXY_METHOD0(void, VoidMethod0) + PROXY_METHOD0(std::string, Method0) + PROXY_CONSTMETHOD0(std::string, ConstMethod0) + PROXY_METHOD1(std::string, Method1, std::string) + PROXY_CONSTMETHOD1(std::string, ConstMethod1, std::string) + PROXY_METHOD2(std::string, Method2, std::string, std::string) +END_PROXY() + +// Implementation of the test interface. +class Fake : public FakeInterface { + public: + static talk_base::scoped_refptr Create() { + return new talk_base::RefCountedObject(); + } + + MOCK_METHOD0(VoidMethod0, void()); + MOCK_METHOD0(Method0, std::string()); + MOCK_CONST_METHOD0(ConstMethod0, std::string()); + + MOCK_METHOD1(Method1, std::string(std::string)); + MOCK_CONST_METHOD1(ConstMethod1, std::string(std::string)); + + MOCK_METHOD2(Method2, std::string(std::string, std::string)); + + protected: + Fake() {} + ~Fake() {} +}; + +class ProxyTest: public testing::Test { + public: + // Checks that the functions is called on the |signaling_thread_|. + void CheckThread() { + EXPECT_EQ(talk_base::Thread::Current(), signaling_thread_.get()); + } + + protected: + virtual void SetUp() { + signaling_thread_.reset(new talk_base::Thread()); + ASSERT_TRUE(signaling_thread_->Start()); + fake_ = Fake::Create(); + fake_proxy_ = FakeProxy::Create(signaling_thread_.get(), fake_.get()); + } + + protected: + talk_base::scoped_ptr signaling_thread_; + talk_base::scoped_refptr fake_proxy_; + talk_base::scoped_refptr fake_; +}; + +TEST_F(ProxyTest, VoidMethod0) { + EXPECT_CALL(*fake_, VoidMethod0()) + .Times(Exactly(1)) + .WillOnce(InvokeWithoutArgs(this, &ProxyTest::CheckThread)); + fake_proxy_->VoidMethod0(); +} + +TEST_F(ProxyTest, Method0) { + EXPECT_CALL(*fake_, Method0()) + .Times(Exactly(1)) + .WillOnce( + DoAll(InvokeWithoutArgs(this, &ProxyTest::CheckThread), + Return("Method0"))); + EXPECT_EQ("Method0", + fake_proxy_->Method0()); +} + +TEST_F(ProxyTest, ConstMethod0) { + EXPECT_CALL(*fake_, ConstMethod0()) + .Times(Exactly(1)) + .WillOnce( + DoAll(InvokeWithoutArgs(this, &ProxyTest::CheckThread), + Return("ConstMethod0"))); + EXPECT_EQ("ConstMethod0", + fake_proxy_->ConstMethod0()); +} + +TEST_F(ProxyTest, Method1) { + const std::string arg1 = "arg1"; + EXPECT_CALL(*fake_, Method1(arg1)) + .Times(Exactly(1)) + .WillOnce( + DoAll(InvokeWithoutArgs(this, &ProxyTest::CheckThread), + Return("Method1"))); + EXPECT_EQ("Method1", fake_proxy_->Method1(arg1)); +} + +TEST_F(ProxyTest, ConstMethod1) { + const std::string arg1 = "arg1"; + EXPECT_CALL(*fake_, ConstMethod1(arg1)) + .Times(Exactly(1)) + .WillOnce( + DoAll(InvokeWithoutArgs(this, &ProxyTest::CheckThread), + Return("ConstMethod1"))); + EXPECT_EQ("ConstMethod1", fake_proxy_->ConstMethod1(arg1)); +} + +TEST_F(ProxyTest, Method2) { + const std::string arg1 = "arg1"; + const std::string arg2 = "arg2"; + EXPECT_CALL(*fake_, Method2(arg1, arg2)) + .Times(Exactly(1)) + .WillOnce( + DoAll(InvokeWithoutArgs(this, &ProxyTest::CheckThread), + Return("Method2"))); + EXPECT_EQ("Method2", fake_proxy_->Method2(arg1, arg2)); +} + +} // namespace webrtc diff --git a/talk/app/webrtc/statscollector.cc b/talk/app/webrtc/statscollector.cc new file mode 100644 index 000000000..b994f2faa --- /dev/null +++ b/talk/app/webrtc/statscollector.cc @@ -0,0 +1,571 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/statscollector.h" + +#include +#include + +#include "talk/session/media/channel.h" + +namespace webrtc { + +// The items below are in alphabetical order. +const char StatsReport::kStatsValueNameActiveConnection[] = + "googActiveConnection"; +const char StatsReport::kStatsValueNameActualEncBitrate[] = + "googActualEncBitrate"; +const char StatsReport::kStatsValueNameAudioOutputLevel[] = "audioOutputLevel"; +const char StatsReport::kStatsValueNameAudioInputLevel[] = "audioInputLevel"; +const char StatsReport::kStatsValueNameAvailableReceiveBandwidth[] = + "googAvailableReceiveBandwidth"; +const char StatsReport::kStatsValueNameAvailableSendBandwidth[] = + "googAvailableSendBandwidth"; +const char StatsReport::kStatsValueNameBucketDelay[] = "googBucketDelay"; +const char StatsReport::kStatsValueNameBytesReceived[] = "bytesReceived"; +const char StatsReport::kStatsValueNameBytesSent[] = "bytesSent"; +const char StatsReport::kStatsValueNameChannelId[] = "googChannelId"; +const char StatsReport::kStatsValueNameCodecName[] = "googCodecName"; +const char StatsReport::kStatsValueNameComponent[] = "googComponent"; +const char StatsReport::kStatsValueNameContentName[] = "googContentName"; +// Echo metrics from the audio processing module. +const char StatsReport::kStatsValueNameEchoCancellationQualityMin[] = + "googEchoCancellationQualityMin"; +const char StatsReport::kStatsValueNameEchoDelayMedian[] = + "googEchoCancellationEchoDelayMedian"; +const char StatsReport::kStatsValueNameEchoDelayStdDev[] = + "googEchoCancellationEchoDelayStdDev"; +const char StatsReport::kStatsValueNameEchoReturnLoss[] = + "googEchoCancellationReturnLoss"; +const char StatsReport::kStatsValueNameEchoReturnLossEnhancement[] = + "googEchoCancellationReturnLossEnhancement"; + +const char StatsReport::kStatsValueNameFirsReceived[] = "googFirsReceived"; +const char StatsReport::kStatsValueNameFirsSent[] = "googFirsSent"; +const char StatsReport::kStatsValueNameFrameHeightReceived[] = + "googFrameHeightReceived"; +const char StatsReport::kStatsValueNameFrameHeightSent[] = + "googFrameHeightSent"; +const char StatsReport::kStatsValueNameFrameRateReceived[] = + "googFrameRateReceived"; +const char StatsReport::kStatsValueNameFrameRateDecoded[] = + "googFrameRateDecoded"; +const char StatsReport::kStatsValueNameFrameRateOutput[] = + "googFrameRateOutput"; +const char StatsReport::kStatsValueNameFrameRateInput[] = "googFrameRateInput"; +const char StatsReport::kStatsValueNameFrameRateSent[] = "googFrameRateSent"; +const char StatsReport::kStatsValueNameFrameWidthReceived[] = + "googFrameWidthReceived"; +const char StatsReport::kStatsValueNameFrameWidthSent[] = "googFrameWidthSent"; +const char StatsReport::kStatsValueNameInitiator[] = "googInitiator"; +const char StatsReport::kStatsValueNameJitterReceived[] = "googJitterReceived"; +const char StatsReport::kStatsValueNameLocalAddress[] = "googLocalAddress"; +const char StatsReport::kStatsValueNameNacksReceived[] = "googNacksReceived"; +const char StatsReport::kStatsValueNameNacksSent[] = "googNacksSent"; +const char StatsReport::kStatsValueNamePacketsReceived[] = "packetsReceived"; +const char StatsReport::kStatsValueNamePacketsSent[] = "packetsSent"; +const char StatsReport::kStatsValueNamePacketsLost[] = "packetsLost"; +const char StatsReport::kStatsValueNameReadable[] = "googReadable"; +const char StatsReport::kStatsValueNameRemoteAddress[] = "googRemoteAddress"; +const char StatsReport::kStatsValueNameRetransmitBitrate[] = + "googRetransmitBitrate"; +const char StatsReport::kStatsValueNameRtt[] = "googRtt"; +const char StatsReport::kStatsValueNameTargetEncBitrate[] = + "googTargetEncBitrate"; +const char StatsReport::kStatsValueNameTransmitBitrate[] = + "googTransmitBitrate"; +const char StatsReport::kStatsValueNameTransportId[] = "transportId"; +const char StatsReport::kStatsValueNameTransportType[] = "googTransportType"; +const char StatsReport::kStatsValueNameTrackId[] = "googTrackId"; +const char StatsReport::kStatsValueNameSsrc[] = "ssrc"; +const char StatsReport::kStatsValueNameWritable[] = "googWritable"; + +const char StatsReport::kStatsReportTypeSession[] = "googLibjingleSession"; +const char StatsReport::kStatsReportTypeBwe[] = "VideoBwe"; +const char StatsReport::kStatsReportTypeSsrc[] = "ssrc"; +const char StatsReport::kStatsReportTypeTrack[] = "googTrack"; +const char StatsReport::kStatsReportTypeIceCandidate[] = "iceCandidate"; +const char StatsReport::kStatsReportTypeTransport[] = "googTransport"; +const char StatsReport::kStatsReportTypeComponent[] = "googComponent"; +const char StatsReport::kStatsReportTypeCandidatePair[] = "googCandidatePair"; + +const char StatsReport::kStatsReportVideoBweId[] = "bweforvideo"; + +// Implementations of functions in statstypes.h +void StatsReport::AddValue(const std::string& name, const std::string& value) { + Value temp; + temp.name = name; + temp.value = value; + values.push_back(temp); +} + +void StatsReport::AddValue(const std::string& name, int64 value) { + AddValue(name, talk_base::ToString(value)); +} + +void StatsReport::AddBoolean(const std::string& name, bool value) { + AddValue(name, value ? "true" : "false"); +} + +namespace { +typedef std::map StatsMap; + +std::string StatsId(const std::string& type, const std::string& id) { + return type + "_" + id; +} + +bool ExtractValueFromReport( + const StatsReport& report, + const std::string& name, + std::string* value) { + StatsReport::Values::const_iterator it = report.values.begin(); + for (; it != report.values.end(); ++it) { + if (it->name == name) { + *value = it->value; + return true; + } + } + return false; +} + +template +void CreateTrackReports(const TrackVector& tracks, StatsMap* reports) { + for (size_t j = 0; j < tracks.size(); ++j) { + webrtc::MediaStreamTrackInterface* track = tracks[j]; + // Adds an empty track report. + StatsReport report; + report.type = StatsReport::kStatsReportTypeTrack; + report.id = StatsId(StatsReport::kStatsReportTypeTrack, track->id()); + report.AddValue(StatsReport::kStatsValueNameTrackId, + track->id()); + (*reports)[report.id] = report; + } +} + +void ExtractStats(const cricket::VoiceReceiverInfo& info, StatsReport* report) { + report->AddValue(StatsReport::kStatsValueNameAudioOutputLevel, + info.audio_level); + report->AddValue(StatsReport::kStatsValueNameBytesReceived, + info.bytes_rcvd); + report->AddValue(StatsReport::kStatsValueNameJitterReceived, + info.jitter_ms); + report->AddValue(StatsReport::kStatsValueNamePacketsReceived, + info.packets_rcvd); + report->AddValue(StatsReport::kStatsValueNamePacketsLost, + info.packets_lost); +} + +void ExtractStats(const cricket::VoiceSenderInfo& info, StatsReport* report) { + report->AddValue(StatsReport::kStatsValueNameAudioInputLevel, + info.audio_level); + report->AddValue(StatsReport::kStatsValueNameBytesSent, + info.bytes_sent); + report->AddValue(StatsReport::kStatsValueNamePacketsSent, + info.packets_sent); + report->AddValue(StatsReport::kStatsValueNameJitterReceived, + info.jitter_ms); + report->AddValue(StatsReport::kStatsValueNameRtt, info.rtt_ms); + report->AddValue(StatsReport::kStatsValueNameEchoCancellationQualityMin, + talk_base::ToString(info.aec_quality_min)); + report->AddValue(StatsReport::kStatsValueNameEchoDelayMedian, + info.echo_delay_median_ms); + report->AddValue(StatsReport::kStatsValueNameEchoDelayStdDev, + info.echo_delay_std_ms); + report->AddValue(StatsReport::kStatsValueNameEchoReturnLoss, + info.echo_return_loss); + report->AddValue(StatsReport::kStatsValueNameEchoReturnLossEnhancement, + info.echo_return_loss_enhancement); + report->AddValue(StatsReport::kStatsValueNameCodecName, info.codec_name); +} + +void ExtractStats(const cricket::VideoReceiverInfo& info, StatsReport* report) { + report->AddValue(StatsReport::kStatsValueNameBytesReceived, + info.bytes_rcvd); + report->AddValue(StatsReport::kStatsValueNamePacketsReceived, + info.packets_rcvd); + report->AddValue(StatsReport::kStatsValueNamePacketsLost, + info.packets_lost); + + report->AddValue(StatsReport::kStatsValueNameFirsSent, + info.firs_sent); + report->AddValue(StatsReport::kStatsValueNameNacksSent, + info.nacks_sent); + report->AddValue(StatsReport::kStatsValueNameFrameWidthReceived, + info.frame_width); + report->AddValue(StatsReport::kStatsValueNameFrameHeightReceived, + info.frame_height); + report->AddValue(StatsReport::kStatsValueNameFrameRateReceived, + info.framerate_rcvd); + report->AddValue(StatsReport::kStatsValueNameFrameRateDecoded, + info.framerate_decoded); + report->AddValue(StatsReport::kStatsValueNameFrameRateOutput, + info.framerate_output); +} + +void ExtractStats(const cricket::VideoSenderInfo& info, StatsReport* report) { + report->AddValue(StatsReport::kStatsValueNameBytesSent, + info.bytes_sent); + report->AddValue(StatsReport::kStatsValueNamePacketsSent, + info.packets_sent); + + report->AddValue(StatsReport::kStatsValueNameFirsReceived, + info.firs_rcvd); + report->AddValue(StatsReport::kStatsValueNameNacksReceived, + info.nacks_rcvd); + report->AddValue(StatsReport::kStatsValueNameFrameWidthSent, + info.frame_width); + report->AddValue(StatsReport::kStatsValueNameFrameHeightSent, + info.frame_height); + report->AddValue(StatsReport::kStatsValueNameFrameRateInput, + info.framerate_input); + report->AddValue(StatsReport::kStatsValueNameFrameRateSent, + info.framerate_sent); + report->AddValue(StatsReport::kStatsValueNameRtt, info.rtt_ms); + report->AddValue(StatsReport::kStatsValueNameCodecName, info.codec_name); +} + +void ExtractStats(const cricket::BandwidthEstimationInfo& info, + double stats_gathering_started, + StatsReport* report) { + report->id = StatsReport::kStatsReportVideoBweId; + report->type = StatsReport::kStatsReportTypeBwe; + + // Clear out stats from previous GatherStats calls if any. + if (report->timestamp != stats_gathering_started) { + report->values.clear(); + report->timestamp = stats_gathering_started; + } + + report->AddValue(StatsReport::kStatsValueNameAvailableSendBandwidth, + info.available_send_bandwidth); + report->AddValue(StatsReport::kStatsValueNameAvailableReceiveBandwidth, + info.available_recv_bandwidth); + report->AddValue(StatsReport::kStatsValueNameTargetEncBitrate, + info.target_enc_bitrate); + report->AddValue(StatsReport::kStatsValueNameActualEncBitrate, + info.actual_enc_bitrate); + report->AddValue(StatsReport::kStatsValueNameRetransmitBitrate, + info.retransmit_bitrate); + report->AddValue(StatsReport::kStatsValueNameTransmitBitrate, + info.transmit_bitrate); + report->AddValue(StatsReport::kStatsValueNameBucketDelay, + info.bucket_delay); +} + +uint32 ExtractSsrc(const cricket::VoiceReceiverInfo& info) { + return info.ssrc; +} + +uint32 ExtractSsrc(const cricket::VoiceSenderInfo& info) { + return info.ssrc; +} + +uint32 ExtractSsrc(const cricket::VideoReceiverInfo& info) { + return info.ssrcs[0]; +} + +uint32 ExtractSsrc(const cricket::VideoSenderInfo& info) { + return info.ssrcs[0]; +} + +// Template to extract stats from a data vector. +// ExtractSsrc and ExtractStats must be defined and overloaded for each type. +template +void ExtractStatsFromList(const std::vector& data, + const std::string& transport_id, + StatsCollector* collector) { + typename std::vector::const_iterator it = data.begin(); + for (; it != data.end(); ++it) { + std::string id; + uint32 ssrc = ExtractSsrc(*it); + StatsReport* report = collector->PrepareReport(ssrc, transport_id); + if (!report) { + continue; + } + ExtractStats(*it, report); + } +}; + +} // namespace + +StatsCollector::StatsCollector() + : session_(NULL), stats_gathering_started_(0) { +} + +// Adds a MediaStream with tracks that can be used as a |selector| in a call +// to GetStats. +void StatsCollector::AddStream(MediaStreamInterface* stream) { + ASSERT(stream != NULL); + + CreateTrackReports(stream->GetAudioTracks(), + &reports_); + CreateTrackReports(stream->GetVideoTracks(), + &reports_); +} + +bool StatsCollector::GetStats(MediaStreamTrackInterface* track, + StatsReports* reports) { + ASSERT(reports != NULL); + reports->clear(); + + StatsMap::iterator it; + if (!track) { + for (it = reports_.begin(); it != reports_.end(); ++it) { + reports->push_back(it->second); + } + return true; + } + + it = reports_.find(StatsId(StatsReport::kStatsReportTypeSession, + session_->id())); + if (it != reports_.end()) { + reports->push_back(it->second); + } + + it = reports_.find(StatsId(StatsReport::kStatsReportTypeTrack, track->id())); + + if (it == reports_.end()) { + LOG(LS_WARNING) << "No StatsReport is available for "<< track->id(); + return false; + } + + reports->push_back(it->second); + + std::string track_id; + for (it = reports_.begin(); it != reports_.end(); ++it) { + if (it->second.type != StatsReport::kStatsReportTypeSsrc) { + continue; + } + if (ExtractValueFromReport(it->second, + StatsReport::kStatsValueNameTrackId, + &track_id)) { + if (track_id == track->id()) { + reports->push_back(it->second); + } + } + } + + return true; +} + +void StatsCollector::UpdateStats() { + double time_now = GetTimeNow(); + // Calls to UpdateStats() that occur less than kMinGatherStatsPeriod number of + // ms apart will be ignored. + const double kMinGatherStatsPeriod = 50; + if (stats_gathering_started_ + kMinGatherStatsPeriod > time_now) { + return; + } + stats_gathering_started_ = time_now; + + if (session_) { + ExtractSessionInfo(); + ExtractVoiceInfo(); + ExtractVideoInfo(); + } +} + +StatsReport* StatsCollector::PrepareReport(uint32 ssrc, + const std::string& transport_id) { + std::string ssrc_id = talk_base::ToString(ssrc); + StatsMap::iterator it = reports_.find(StatsId( + StatsReport::kStatsReportTypeSsrc, ssrc_id)); + + std::string track_id; + if (it == reports_.end()) { + if (!session()->GetTrackIdBySsrc(ssrc, &track_id)) { + LOG(LS_ERROR) << "The SSRC " << ssrc + << " is not associated with a track"; + return NULL; + } + } else { + // Keeps the old track id since we want to report the stats for inactive + // tracks. + ExtractValueFromReport(it->second, + StatsReport::kStatsValueNameTrackId, + &track_id); + } + + StatsReport* report = &reports_[ + StatsId(StatsReport::kStatsReportTypeSsrc, ssrc_id)]; + report->id = StatsId(StatsReport::kStatsReportTypeSsrc, ssrc_id); + report->type = StatsReport::kStatsReportTypeSsrc; + + // Clear out stats from previous GatherStats calls if any. + if (report->timestamp != stats_gathering_started_) { + report->values.clear(); + report->timestamp = stats_gathering_started_; + } + + report->AddValue(StatsReport::kStatsValueNameSsrc, ssrc_id); + report->AddValue(StatsReport::kStatsValueNameTrackId, track_id); + // Add the mapping of SSRC to transport. + report->AddValue(StatsReport::kStatsValueNameTransportId, + transport_id); + return report; +} + +void StatsCollector::ExtractSessionInfo() { + // Extract information from the base session. + StatsReport report; + report.id = StatsId(StatsReport::kStatsReportTypeSession, session_->id()); + report.type = StatsReport::kStatsReportTypeSession; + report.timestamp = stats_gathering_started_; + report.values.clear(); + report.AddBoolean(StatsReport::kStatsValueNameInitiator, + session_->initiator()); + + reports_[report.id] = report; + + cricket::SessionStats stats; + if (session_->GetStats(&stats)) { + // Store the proxy map away for use in SSRC reporting. + proxy_to_transport_ = stats.proxy_to_transport; + + for (cricket::TransportStatsMap::iterator transport_iter + = stats.transport_stats.begin(); + transport_iter != stats.transport_stats.end(); ++transport_iter) { + for (cricket::TransportChannelStatsList::iterator channel_iter + = transport_iter->second.channel_stats.begin(); + channel_iter != transport_iter->second.channel_stats.end(); + ++channel_iter) { + StatsReport channel_report; + std::ostringstream ostc; + ostc << "Channel-" << transport_iter->second.content_name + << "-" << channel_iter->component; + channel_report.id = ostc.str(); + channel_report.type = StatsReport::kStatsReportTypeComponent; + channel_report.timestamp = stats_gathering_started_; + channel_report.AddValue(StatsReport::kStatsValueNameComponent, + channel_iter->component); + reports_[channel_report.id] = channel_report; + for (size_t i = 0; + i < channel_iter->connection_infos.size(); + ++i) { + StatsReport report; + const cricket::ConnectionInfo& info + = channel_iter->connection_infos[i]; + std::ostringstream ost; + ost << "Conn-" << transport_iter->first << "-" + << channel_iter->component << "-" << i; + report.id = ost.str(); + report.type = StatsReport::kStatsReportTypeCandidatePair; + report.timestamp = stats_gathering_started_; + // Link from connection to its containing channel. + report.AddValue(StatsReport::kStatsValueNameChannelId, + channel_report.id); + report.AddValue(StatsReport::kStatsValueNameBytesSent, + info.sent_total_bytes); + report.AddValue(StatsReport::kStatsValueNameBytesReceived, + info.recv_total_bytes); + report.AddBoolean(StatsReport::kStatsValueNameWritable, + info.writable); + report.AddBoolean(StatsReport::kStatsValueNameReadable, + info.readable); + report.AddBoolean(StatsReport::kStatsValueNameActiveConnection, + info.best_connection); + report.AddValue(StatsReport::kStatsValueNameLocalAddress, + info.local_candidate.address().ToString()); + report.AddValue(StatsReport::kStatsValueNameRemoteAddress, + info.remote_candidate.address().ToString()); + reports_[report.id] = report; + } + } + } + } +} + +void StatsCollector::ExtractVoiceInfo() { + if (!session_->voice_channel()) { + return; + } + cricket::VoiceMediaInfo voice_info; + if (!session_->voice_channel()->GetStats(&voice_info)) { + LOG(LS_ERROR) << "Failed to get voice channel stats."; + return; + } + std::string transport_id; + if (!GetTransportIdFromProxy(session_->voice_channel()->content_name(), + &transport_id)) { + LOG(LS_ERROR) << "Failed to get transport name for proxy " + << session_->voice_channel()->content_name(); + return; + } + ExtractStatsFromList(voice_info.receivers, transport_id, this); + ExtractStatsFromList(voice_info.senders, transport_id, this); +} + +void StatsCollector::ExtractVideoInfo() { + if (!session_->video_channel()) { + return; + } + cricket::VideoMediaInfo video_info; + if (!session_->video_channel()->GetStats(&video_info)) { + LOG(LS_ERROR) << "Failed to get video channel stats."; + return; + } + std::string transport_id; + if (!GetTransportIdFromProxy(session_->video_channel()->content_name(), + &transport_id)) { + LOG(LS_ERROR) << "Failed to get transport name for proxy " + << session_->video_channel()->content_name(); + return; + } + ExtractStatsFromList(video_info.receivers, transport_id, this); + ExtractStatsFromList(video_info.senders, transport_id, this); + if (video_info.bw_estimations.size() != 1) { + LOG(LS_ERROR) << "BWEs count: " << video_info.bw_estimations.size(); + } else { + StatsReport* report = &reports_[StatsReport::kStatsReportVideoBweId]; + ExtractStats( + video_info.bw_estimations[0], stats_gathering_started_, report); + } +} + +double StatsCollector::GetTimeNow() { + return timing_.WallTimeNow() * talk_base::kNumMillisecsPerSec; +} + +bool StatsCollector::GetTransportIdFromProxy(const std::string& proxy, + std::string* transport) { + // TODO(hta): Remove handling of empty proxy name once tests do not use it. + if (proxy.empty()) { + transport->clear(); + return true; + } + if (proxy_to_transport_.find(proxy) == proxy_to_transport_.end()) { + LOG(LS_ERROR) << "No transport ID mapping for " << proxy; + return false; + } + std::ostringstream ost; + // Component 1 is always used for RTP. + ost << "Channel-" << proxy_to_transport_[proxy] << "-1"; + *transport = ost.str(); + return true; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/statscollector.h b/talk/app/webrtc/statscollector.h new file mode 100644 index 000000000..03a32c493 --- /dev/null +++ b/talk/app/webrtc/statscollector.h @@ -0,0 +1,95 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// This file contains a class used for gathering statistics from an ongoing +// libjingle PeerConnection. + +#ifndef TALK_APP_WEBRTC_STATSCOLLECTOR_H_ +#define TALK_APP_WEBRTC_STATSCOLLECTOR_H_ + +#include +#include + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/statstypes.h" +#include "talk/app/webrtc/webrtcsession.h" + +#include "talk/base/timing.h" + +namespace webrtc { + +class StatsCollector { + public: + StatsCollector(); + + // Register the session Stats should operate on. + // Set to NULL if the session has ended. + void set_session(WebRtcSession* session) { + session_ = session; + } + + // Adds a MediaStream with tracks that can be used as a |selector| in a call + // to GetStats. + void AddStream(MediaStreamInterface* stream); + + // Gather statistics from the session and store them for future use. + void UpdateStats(); + + // Gets a StatsReports of the last collected stats. Note that UpdateStats must + // be called before this function to get the most recent stats. |selector| is + // a track label or empty string. The most recent reports are stored in + // |reports|. + bool GetStats(MediaStreamTrackInterface* track, StatsReports* reports); + + WebRtcSession* session() { return session_; } + // Prepare an SSRC report for the given ssrc. Used internally. + StatsReport* PrepareReport(uint32 ssrc, const std::string& transport); + // Extracts the ID of a Transport belonging to an SSRC. Used internally. + bool GetTransportIdFromProxy(const std::string& proxy, + std::string* transport_id); + + private: + bool CopySelectedReports(const std::string& selector, StatsReports* reports); + + void ExtractSessionInfo(); + void ExtractVoiceInfo(); + void ExtractVideoInfo(); + double GetTimeNow(); + void BuildSsrcToTransportId(); + + // A map from the report id to the report. + std::map reports_; + // Raw pointer to the session the statistics are gathered from. + WebRtcSession* session_; + double stats_gathering_started_; + talk_base::Timing timing_; + cricket::ProxyTransportMap proxy_to_transport_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_STATSCOLLECTOR_H_ diff --git a/talk/app/webrtc/statscollector_unittest.cc b/talk/app/webrtc/statscollector_unittest.cc new file mode 100644 index 000000000..cce1645bc --- /dev/null +++ b/talk/app/webrtc/statscollector_unittest.cc @@ -0,0 +1,442 @@ +/* + * libjingle + * + * 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 + +#include "talk/app/webrtc/statscollector.h" + +#include "talk/app/webrtc/mediastream.h" +#include "talk/app/webrtc/videotrack.h" +#include "talk/base/gunit.h" +#include "talk/media/base/fakemediaengine.h" +#include "talk/media/devices/fakedevicemanager.h" +#include "talk/p2p/base/fakesession.h" +#include "talk/session/media/channelmanager.h" +#include "testing/base/public/gmock.h" + +using testing::_; +using testing::DoAll; +using testing::Return; +using testing::ReturnNull; +using testing::SetArgPointee; + +namespace cricket { + +class ChannelManager; +class FakeDeviceManager; + +} // namespace cricket + +namespace { + +// Error return values +const char kNotFound[] = "NOT FOUND"; +const char kNoReports[] = "NO REPORTS"; + +class MockWebRtcSession : public webrtc::WebRtcSession { + public: + explicit MockWebRtcSession(cricket::ChannelManager* channel_manager) + : WebRtcSession(channel_manager, talk_base::Thread::Current(), + NULL, NULL, NULL) { + } + MOCK_METHOD0(video_channel, cricket::VideoChannel*()); + MOCK_METHOD2(GetTrackIdBySsrc, bool(uint32, std::string*)); + MOCK_METHOD1(GetStats, bool(cricket::SessionStats*)); +}; + +class MockVideoMediaChannel : public cricket::FakeVideoMediaChannel { + public: + MockVideoMediaChannel() + : cricket::FakeVideoMediaChannel(NULL) { + } + // MOCK_METHOD0(transport_channel, cricket::TransportChannel*()); + MOCK_METHOD1(GetStats, bool(cricket::VideoMediaInfo*)); +}; + +std::string ExtractStatsValue(const std::string& type, + webrtc::StatsReports reports, + const std::string name) { + if (reports.empty()) { + return kNoReports; + } + for (size_t i = 0; i < reports.size(); ++i) { + if (reports[i].type != type) + continue; + webrtc::StatsReport::Values::const_iterator it = + reports[i].values.begin(); + for (; it != reports[i].values.end(); ++it) { + if (it->name == name) { + return it->value; + } + } + } + + return kNotFound; +} + +// Finds the |n|-th report of type |type| in |reports|. +// |n| starts from 1 for finding the first report. +const webrtc::StatsReport* FindNthReportByType(webrtc::StatsReports reports, + const std::string& type, + int n) { + for (size_t i = 0; i < reports.size(); ++i) { + if (reports[i].type == type) { + n--; + if (n == 0) + return &reports[i]; + } + } + return NULL; +} + +const webrtc::StatsReport* FindReportById(webrtc::StatsReports reports, + const std::string& id) { + for (size_t i = 0; i < reports.size(); ++i) { + if (reports[i].id == id) { + return &reports[i]; + } + } + return NULL; +} + +std::string ExtractSsrcStatsValue(webrtc::StatsReports reports, + const std::string& name) { + return ExtractStatsValue( + webrtc::StatsReport::kStatsReportTypeSsrc, reports, name); +} + +std::string ExtractBweStatsValue(webrtc::StatsReports reports, + const std::string& name) { + return ExtractStatsValue( + webrtc::StatsReport::kStatsReportTypeBwe, reports, name); +} + +class StatsCollectorTest : public testing::Test { + protected: + StatsCollectorTest() + : media_engine_(new cricket::FakeMediaEngine), + channel_manager_( + new cricket::ChannelManager(media_engine_, + new cricket::FakeDeviceManager(), + talk_base::Thread::Current())), + session_(channel_manager_.get()) { + // By default, we ignore session GetStats calls. + EXPECT_CALL(session_, GetStats(_)).WillRepeatedly(Return(false)); + } + + cricket::FakeMediaEngine* media_engine_; + talk_base::scoped_ptr channel_manager_; + MockWebRtcSession session_; +}; + +// This test verifies that 64-bit counters are passed successfully. +TEST_F(StatsCollectorTest, BytesCounterHandles64Bits) { + webrtc::StatsCollector stats; // Implementation under test. + MockVideoMediaChannel* media_channel = new MockVideoMediaChannel; + cricket::VideoChannel video_channel(talk_base::Thread::Current(), + media_engine_, media_channel, &session_, "", false, NULL); + webrtc::StatsReports reports; // returned values. + cricket::VideoSenderInfo video_sender_info; + cricket::VideoMediaInfo stats_read; + const uint32 kSsrcOfTrack = 1234; + const std::string kNameOfTrack("somename"); + // The number of bytes must be larger than 0xFFFFFFFF for this test. + const int64 kBytesSent = 12345678901234LL; + const std::string kBytesSentString("12345678901234"); + + stats.set_session(&session_); + talk_base::scoped_refptr stream( + webrtc::MediaStream::Create("streamlabel")); + stream->AddTrack(webrtc::VideoTrack::Create(kNameOfTrack, NULL)); + stats.AddStream(stream); + + // Construct a stats value to read. + video_sender_info.ssrcs.push_back(1234); + video_sender_info.bytes_sent = kBytesSent; + stats_read.senders.push_back(video_sender_info); + + EXPECT_CALL(session_, video_channel()) + .WillRepeatedly(Return(&video_channel)); + EXPECT_CALL(*media_channel, GetStats(_)) + .WillOnce(DoAll(SetArgPointee<0>(stats_read), + Return(true))); + EXPECT_CALL(session_, GetTrackIdBySsrc(kSsrcOfTrack, _)) + .WillOnce(DoAll(SetArgPointee<1>(kNameOfTrack), + Return(true))); + stats.UpdateStats(); + stats.GetStats(NULL, &reports); + std::string result = ExtractSsrcStatsValue(reports, "bytesSent"); + EXPECT_EQ(kBytesSentString, result); +} + +// Test that BWE information is reported via stats. +TEST_F(StatsCollectorTest, BandwidthEstimationInfoIsReported) { + webrtc::StatsCollector stats; // Implementation under test. + MockVideoMediaChannel* media_channel = new MockVideoMediaChannel; + cricket::VideoChannel video_channel(talk_base::Thread::Current(), + media_engine_, media_channel, &session_, "", false, NULL); + webrtc::StatsReports reports; // returned values. + cricket::VideoSenderInfo video_sender_info; + cricket::VideoMediaInfo stats_read; + // Set up an SSRC just to test that we get both kinds of stats back: SSRC and + // BWE. + const uint32 kSsrcOfTrack = 1234; + const std::string kNameOfTrack("somename"); + const int64 kBytesSent = 12345678901234LL; + const std::string kBytesSentString("12345678901234"); + + stats.set_session(&session_); + talk_base::scoped_refptr stream( + webrtc::MediaStream::Create("streamlabel")); + stream->AddTrack(webrtc::VideoTrack::Create(kNameOfTrack, NULL)); + stats.AddStream(stream); + + // Construct a stats value to read. + video_sender_info.ssrcs.push_back(1234); + video_sender_info.bytes_sent = kBytesSent; + stats_read.senders.push_back(video_sender_info); + cricket::BandwidthEstimationInfo bwe; + const int kTargetEncBitrate = 123456; + const std::string kTargetEncBitrateString("123456"); + bwe.target_enc_bitrate = kTargetEncBitrate; + stats_read.bw_estimations.push_back(bwe); + + EXPECT_CALL(session_, video_channel()) + .WillRepeatedly(Return(&video_channel)); + EXPECT_CALL(*media_channel, GetStats(_)) + .WillOnce(DoAll(SetArgPointee<0>(stats_read), + Return(true))); + EXPECT_CALL(session_, GetTrackIdBySsrc(kSsrcOfTrack, _)) + .WillOnce(DoAll(SetArgPointee<1>(kNameOfTrack), + Return(true))); + stats.UpdateStats(); + stats.GetStats(NULL, &reports); + std::string result = ExtractSsrcStatsValue(reports, "bytesSent"); + EXPECT_EQ(kBytesSentString, result); + result = ExtractBweStatsValue(reports, "googTargetEncBitrate"); + EXPECT_EQ(kTargetEncBitrateString, result); +} + +// This test verifies that an object of type "googSession" always +// exists in the returned stats. +TEST_F(StatsCollectorTest, SessionObjectExists) { + webrtc::StatsCollector stats; // Implementation under test. + webrtc::StatsReports reports; // returned values. + stats.set_session(&session_); + EXPECT_CALL(session_, video_channel()) + .WillRepeatedly(ReturnNull()); + stats.UpdateStats(); + stats.GetStats(NULL, &reports); + const webrtc::StatsReport* session_report = FindNthReportByType( + reports, webrtc::StatsReport::kStatsReportTypeSession, 1); + EXPECT_FALSE(session_report == NULL); +} + +// This test verifies that only one object of type "googSession" exists +// in the returned stats. +TEST_F(StatsCollectorTest, OnlyOneSessionObjectExists) { + webrtc::StatsCollector stats; // Implementation under test. + webrtc::StatsReports reports; // returned values. + stats.set_session(&session_); + EXPECT_CALL(session_, video_channel()) + .WillRepeatedly(ReturnNull()); + stats.UpdateStats(); + stats.UpdateStats(); + stats.GetStats(NULL, &reports); + const webrtc::StatsReport* session_report = FindNthReportByType( + reports, webrtc::StatsReport::kStatsReportTypeSession, 1); + EXPECT_FALSE(session_report == NULL); + session_report = FindNthReportByType( + reports, webrtc::StatsReport::kStatsReportTypeSession, 2); + EXPECT_EQ(NULL, session_report); +} + +// This test verifies that the empty track report exists in the returned stats +// without calling StatsCollector::UpdateStats. +TEST_F(StatsCollectorTest, TrackObjectExistsWithoutUpdateStats) { + webrtc::StatsCollector stats; // Implementation under test. + MockVideoMediaChannel* media_channel = new MockVideoMediaChannel; + cricket::VideoChannel video_channel(talk_base::Thread::Current(), + media_engine_, media_channel, &session_, "", false, NULL); + const std::string kTrackId("somename"); + talk_base::scoped_refptr stream( + webrtc::MediaStream::Create("streamlabel")); + talk_base::scoped_refptr track = + webrtc::VideoTrack::Create(kTrackId, NULL); + stream->AddTrack(track); + stats.AddStream(stream); + + stats.set_session(&session_); + + webrtc::StatsReports reports; + + // Verfies the existence of the track report. + stats.GetStats(NULL, &reports); + EXPECT_EQ((size_t)1, reports.size()); + EXPECT_EQ(std::string(webrtc::StatsReport::kStatsReportTypeTrack), + reports[0].type); + + std::string trackValue = + ExtractStatsValue(webrtc::StatsReport::kStatsReportTypeTrack, + reports, + webrtc::StatsReport::kStatsValueNameTrackId); + EXPECT_EQ(kTrackId, trackValue); +} + +// This test verifies that the empty track report exists in the returned stats +// when StatsCollector::UpdateStats is called with ssrc stats. +TEST_F(StatsCollectorTest, TrackAndSsrcObjectExistAfterUpdateSsrcStats) { + webrtc::StatsCollector stats; // Implementation under test. + MockVideoMediaChannel* media_channel = new MockVideoMediaChannel; + cricket::VideoChannel video_channel(talk_base::Thread::Current(), + media_engine_, media_channel, &session_, "", false, NULL); + const std::string kTrackId("somename"); + talk_base::scoped_refptr stream( + webrtc::MediaStream::Create("streamlabel")); + talk_base::scoped_refptr track = + webrtc::VideoTrack::Create(kTrackId, NULL); + stream->AddTrack(track); + stats.AddStream(stream); + + stats.set_session(&session_); + + webrtc::StatsReports reports; + + // Constructs an ssrc stats update. + cricket::VideoSenderInfo video_sender_info; + cricket::VideoMediaInfo stats_read; + const uint32 kSsrcOfTrack = 1234; + const int64 kBytesSent = 12345678901234LL; + + // Construct a stats value to read. + video_sender_info.ssrcs.push_back(1234); + video_sender_info.bytes_sent = kBytesSent; + stats_read.senders.push_back(video_sender_info); + + EXPECT_CALL(session_, video_channel()) + .WillRepeatedly(Return(&video_channel)); + EXPECT_CALL(*media_channel, GetStats(_)) + .WillOnce(DoAll(SetArgPointee<0>(stats_read), + Return(true))); + EXPECT_CALL(session_, GetTrackIdBySsrc(kSsrcOfTrack, _)) + .WillOnce(DoAll(SetArgPointee<1>(kTrackId), + Return(true))); + + stats.UpdateStats(); + stats.GetStats(NULL, &reports); + // |reports| should contain one session report, one track report, and one ssrc + // report. + EXPECT_EQ((size_t)3, reports.size()); + const webrtc::StatsReport* track_report = FindNthReportByType( + reports, webrtc::StatsReport::kStatsReportTypeTrack, 1); + EXPECT_FALSE(track_report == NULL); + + stats.GetStats(track, &reports); + // |reports| should contain one session report, one track report, and one ssrc + // report. + EXPECT_EQ((size_t)3, reports.size()); + track_report = FindNthReportByType( + reports, webrtc::StatsReport::kStatsReportTypeTrack, 1); + EXPECT_FALSE(track_report == NULL); + + std::string ssrc_id = ExtractSsrcStatsValue( + reports, webrtc::StatsReport::kStatsValueNameSsrc); + EXPECT_EQ(talk_base::ToString(kSsrcOfTrack), ssrc_id); + + std::string track_id = ExtractSsrcStatsValue( + reports, webrtc::StatsReport::kStatsValueNameTrackId); + EXPECT_EQ(kTrackId, track_id); +} + +// This test verifies that an SSRC object has the identifier of a Transport +// stats object, and that this transport stats object exists in stats. +TEST_F(StatsCollectorTest, TransportObjectLinkedFromSsrcObject) { + webrtc::StatsCollector stats; // Implementation under test. + MockVideoMediaChannel* media_channel = new MockVideoMediaChannel; + // The content_name known by the video channel. + const std::string kVcName("vcname"); + cricket::VideoChannel video_channel(talk_base::Thread::Current(), + media_engine_, media_channel, &session_, kVcName, false, NULL); + const std::string kTrackId("somename"); + talk_base::scoped_refptr stream( + webrtc::MediaStream::Create("streamlabel")); + talk_base::scoped_refptr track = + webrtc::VideoTrack::Create(kTrackId, NULL); + stream->AddTrack(track); + stats.AddStream(stream); + + stats.set_session(&session_); + + webrtc::StatsReports reports; + + // Constructs an ssrc stats update. + cricket::VideoSenderInfo video_sender_info; + cricket::VideoMediaInfo stats_read; + const uint32 kSsrcOfTrack = 1234; + const int64 kBytesSent = 12345678901234LL; + + // Construct a stats value to read. + video_sender_info.ssrcs.push_back(1234); + video_sender_info.bytes_sent = kBytesSent; + stats_read.senders.push_back(video_sender_info); + + EXPECT_CALL(session_, video_channel()) + .WillRepeatedly(Return(&video_channel)); + EXPECT_CALL(*media_channel, GetStats(_)) + .WillRepeatedly(DoAll(SetArgPointee<0>(stats_read), + Return(true))); + EXPECT_CALL(session_, GetTrackIdBySsrc(kSsrcOfTrack, _)) + .WillOnce(DoAll(SetArgPointee<1>(kTrackId), + Return(true))); + + // Instruct the session to return stats containing the transport channel. + const std::string kTransportName("trspname"); + cricket::SessionStats session_stats; + cricket::TransportStats transport_stats; + cricket::TransportChannelStats channel_stats; + channel_stats.component = 1; + transport_stats.content_name = kTransportName; + transport_stats.channel_stats.push_back(channel_stats); + + session_stats.transport_stats[kTransportName] = transport_stats; + session_stats.proxy_to_transport[kVcName] = kTransportName; + EXPECT_CALL(session_, GetStats(_)) + .WillRepeatedly(DoAll(SetArgPointee<0>(session_stats), + Return(true))); + + stats.UpdateStats(); + stats.GetStats(NULL, &reports); + std::string transport_id = ExtractStatsValue( + webrtc::StatsReport::kStatsReportTypeSsrc, + reports, + webrtc::StatsReport::kStatsValueNameTransportId); + ASSERT_NE(kNotFound, transport_id); + const webrtc::StatsReport* transport_report = FindReportById(reports, + transport_id); + ASSERT_FALSE(transport_report == NULL); +} + +} // namespace diff --git a/talk/app/webrtc/statstypes.h b/talk/app/webrtc/statstypes.h new file mode 100644 index 000000000..62f878161 --- /dev/null +++ b/talk/app/webrtc/statstypes.h @@ -0,0 +1,160 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// This file contains structures used for retrieving statistics from an ongoing +// libjingle session. + +#ifndef TALK_APP_WEBRTC_STATSTYPES_H_ +#define TALK_APP_WEBRTC_STATSTYPES_H_ + +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/stringencode.h" + +namespace webrtc { + +class StatsReport { + public: + StatsReport() : timestamp(0) { } + + std::string id; // See below for contents. + std::string type; // See below for contents. + + struct Value { + std::string name; + std::string value; + }; + + void AddValue(const std::string& name, const std::string& value); + void AddValue(const std::string& name, int64 value); + void AddBoolean(const std::string& name, bool value); + + double timestamp; // Time since 1970-01-01T00:00:00Z in milliseconds. + typedef std::vector Values; + Values values; + + // StatsReport types. + // A StatsReport of |type| = "googSession" contains overall information + // about the thing libjingle calls a session (which may contain one + // or more RTP sessions. + static const char kStatsReportTypeSession[]; + + // A StatsReport of |type| = "googTransport" contains information + // about a libjingle "transport". + static const char kStatsReportTypeTransport[]; + + // A StatsReport of |type| = "googComponent" contains information + // about a libjingle "channel" (typically, RTP or RTCP for a transport). + // This is intended to be the same thing as an ICE "Component". + static const char kStatsReportTypeComponent[]; + + // A StatsReport of |type| = "googCandidatePair" contains information + // about a libjingle "connection" - a single source/destination port pair. + // This is intended to be the same thing as an ICE "candidate pair". + static const char kStatsReportTypeCandidatePair[]; + + // StatsReport of |type| = "VideoBWE" is statistics for video Bandwidth + // Estimation, which is global per-session. The |id| field is "bweforvideo" + // (will probably change in the future). + static const char kStatsReportTypeBwe[]; + + // StatsReport of |type| = "ssrc" is statistics for a specific rtp stream. + // The |id| field is the SSRC in decimal form of the rtp stream. + static const char kStatsReportTypeSsrc[]; + + // StatsReport of |type| = "googTrack" is statistics for a specific media + // track. The |id| field is the track id. + static const char kStatsReportTypeTrack[]; + + // StatsReport of |type| = "iceCandidate" is statistics on a specific + // ICE Candidate. It links to its transport. + static const char kStatsReportTypeIceCandidate[]; + + // The id of StatsReport of type VideoBWE. + static const char kStatsReportVideoBweId[]; + + // StatsValue names + static const char kStatsValueNameAudioOutputLevel[]; + static const char kStatsValueNameAudioInputLevel[]; + static const char kStatsValueNameBytesSent[]; + static const char kStatsValueNamePacketsSent[]; + static const char kStatsValueNameBytesReceived[]; + static const char kStatsValueNamePacketsReceived[]; + static const char kStatsValueNamePacketsLost[]; + static const char kStatsValueNameTransportId[]; + static const char kStatsValueNameLocalAddress[]; + static const char kStatsValueNameRemoteAddress[]; + static const char kStatsValueNameWritable[]; + static const char kStatsValueNameReadable[]; + static const char kStatsValueNameActiveConnection[]; + + + // Internal StatsValue names + static const char kStatsValueNameCodecName[]; + static const char kStatsValueNameEchoCancellationQualityMin[]; + static const char kStatsValueNameEchoDelayMedian[]; + static const char kStatsValueNameEchoDelayStdDev[]; + static const char kStatsValueNameEchoReturnLoss[]; + static const char kStatsValueNameEchoReturnLossEnhancement[]; + static const char kStatsValueNameFirsReceived[]; + static const char kStatsValueNameFirsSent[]; + static const char kStatsValueNameFrameHeightReceived[]; + static const char kStatsValueNameFrameHeightSent[]; + static const char kStatsValueNameFrameRateReceived[]; + static const char kStatsValueNameFrameRateDecoded[]; + static const char kStatsValueNameFrameRateOutput[]; + static const char kStatsValueNameFrameRateInput[]; + static const char kStatsValueNameFrameRateSent[]; + static const char kStatsValueNameFrameWidthReceived[]; + static const char kStatsValueNameFrameWidthSent[]; + static const char kStatsValueNameJitterReceived[]; + static const char kStatsValueNameNacksReceived[]; + static const char kStatsValueNameNacksSent[]; + static const char kStatsValueNameRtt[]; + static const char kStatsValueNameAvailableSendBandwidth[]; + static const char kStatsValueNameAvailableReceiveBandwidth[]; + static const char kStatsValueNameTargetEncBitrate[]; + static const char kStatsValueNameActualEncBitrate[]; + static const char kStatsValueNameRetransmitBitrate[]; + static const char kStatsValueNameTransmitBitrate[]; + static const char kStatsValueNameBucketDelay[]; + static const char kStatsValueNameInitiator[]; + static const char kStatsValueNameTransportType[]; + static const char kStatsValueNameContentName[]; + static const char kStatsValueNameComponent[]; + static const char kStatsValueNameChannelId[]; + static const char kStatsValueNameTrackId[]; + static const char kStatsValueNameSsrc[]; +}; + +typedef std::vector StatsReports; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_STATSTYPES_H_ diff --git a/talk/app/webrtc/streamcollection.h b/talk/app/webrtc/streamcollection.h new file mode 100644 index 000000000..7796b4266 --- /dev/null +++ b/talk/app/webrtc/streamcollection.h @@ -0,0 +1,125 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_APP_WEBRTC_STREAMCOLLECTION_H_ +#define TALK_APP_WEBRTC_STREAMCOLLECTION_H_ + +#include +#include + +#include "talk/app/webrtc/peerconnectioninterface.h" + +namespace webrtc { + +// Implementation of StreamCollection. +class StreamCollection : public StreamCollectionInterface { + public: + static talk_base::scoped_refptr Create() { + talk_base::RefCountedObject* implementation = + new talk_base::RefCountedObject(); + return implementation; + } + + static talk_base::scoped_refptr Create( + StreamCollection* streams) { + talk_base::RefCountedObject* implementation = + new talk_base::RefCountedObject(streams); + return implementation; + } + + virtual size_t count() { + return media_streams_.size(); + } + + virtual MediaStreamInterface* at(size_t index) { + return media_streams_.at(index); + } + + virtual MediaStreamInterface* find(const std::string& label) { + for (StreamVector::iterator it = media_streams_.begin(); + it != media_streams_.end(); ++it) { + if ((*it)->label().compare(label) == 0) { + return (*it); + } + } + return NULL; + } + + virtual MediaStreamTrackInterface* FindAudioTrack( + const std::string& id) { + for (size_t i = 0; i < media_streams_.size(); ++i) { + MediaStreamTrackInterface* track = media_streams_[i]->FindAudioTrack(id); + if (track) { + return track; + } + } + return NULL; + } + + virtual MediaStreamTrackInterface* FindVideoTrack( + const std::string& id) { + for (size_t i = 0; i < media_streams_.size(); ++i) { + MediaStreamTrackInterface* track = media_streams_[i]->FindVideoTrack(id); + if (track) { + return track; + } + } + return NULL; + } + + void AddStream(MediaStreamInterface* stream) { + for (StreamVector::iterator it = media_streams_.begin(); + it != media_streams_.end(); ++it) { + if ((*it)->label().compare(stream->label()) == 0) + return; + } + media_streams_.push_back(stream); + } + + void RemoveStream(MediaStreamInterface* remove_stream) { + for (StreamVector::iterator it = media_streams_.begin(); + it != media_streams_.end(); ++it) { + if ((*it)->label().compare(remove_stream->label()) == 0) { + media_streams_.erase(it); + break; + } + } + } + + protected: + StreamCollection() {} + explicit StreamCollection(StreamCollection* original) + : media_streams_(original->media_streams_) { + } + typedef std::vector > + StreamVector; + StreamVector media_streams_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_STREAMCOLLECTION_H_ diff --git a/talk/app/webrtc/test/fakeaudiocapturemodule.cc b/talk/app/webrtc/test/fakeaudiocapturemodule.cc new file mode 100644 index 000000000..4bdaf89f0 --- /dev/null +++ b/talk/app/webrtc/test/fakeaudiocapturemodule.cc @@ -0,0 +1,716 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/test/fakeaudiocapturemodule.h" + +#include "talk/base/common.h" +#include "talk/base/refcount.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" + +// Audio sample value that is high enough that it doesn't occur naturally when +// frames are being faked. E.g. NetEq will not generate this large sample value +// unless it has received an audio frame containing a sample of this value. +// Even simpler buffers would likely just contain audio sample values of 0. +static const int kHighSampleValue = 10000; + +// Same value as src/modules/audio_device/main/source/audio_device_config.h in +// https://code.google.com/p/webrtc/ +static const uint32 kAdmMaxIdleTimeProcess = 1000; + +// Constants here are derived by running VoE using a real ADM. +// The constants correspond to 10ms of mono audio at 44kHz. +static const int kTimePerFrameMs = 10; +static const int kNumberOfChannels = 1; +static const int kSamplesPerSecond = 44000; +static const int kTotalDelayMs = 0; +static const int kClockDriftMs = 0; +static const uint32_t kMaxVolume = 14392; + +enum { + MSG_RUN_PROCESS, + MSG_STOP_PROCESS, +}; + +FakeAudioCaptureModule::FakeAudioCaptureModule( + talk_base::Thread* process_thread) + : last_process_time_ms_(0), + audio_callback_(NULL), + recording_(false), + playing_(false), + play_is_initialized_(false), + rec_is_initialized_(false), + current_mic_level_(kMaxVolume), + started_(false), + next_frame_time_(0), + process_thread_(process_thread), + frames_received_(0) { +} + +FakeAudioCaptureModule::~FakeAudioCaptureModule() { + // Ensure that thread stops calling ProcessFrame(). + process_thread_->Send(this, MSG_STOP_PROCESS); +} + +talk_base::scoped_refptr FakeAudioCaptureModule::Create( + talk_base::Thread* process_thread) { + if (process_thread == NULL) return NULL; + + talk_base::scoped_refptr capture_module( + new talk_base::RefCountedObject(process_thread)); + if (!capture_module->Initialize()) { + return NULL; + } + return capture_module; +} + +int FakeAudioCaptureModule::frames_received() const { + return frames_received_; +} + +int32_t FakeAudioCaptureModule::Version(char* /*version*/, + uint32_t& /*remaining_buffer_in_bytes*/, + uint32_t& /*position*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::TimeUntilNextProcess() { + const uint32 current_time = talk_base::Time(); + if (current_time < last_process_time_ms_) { + // TODO: wraparound could be handled more gracefully. + return 0; + } + const uint32 elapsed_time = current_time - last_process_time_ms_; + if (kAdmMaxIdleTimeProcess < elapsed_time) { + return 0; + } + return kAdmMaxIdleTimeProcess - elapsed_time; +} + +int32_t FakeAudioCaptureModule::Process() { + last_process_time_ms_ = talk_base::Time(); + return 0; +} + +int32_t FakeAudioCaptureModule::ChangeUniqueId(const int32_t /*id*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::ActiveAudioLayer( + AudioLayer* /*audio_layer*/) const { + ASSERT(false); + return 0; +} + +webrtc::AudioDeviceModule::ErrorCode FakeAudioCaptureModule::LastError() const { + ASSERT(false); + return webrtc::AudioDeviceModule::kAdmErrNone; +} + +int32_t FakeAudioCaptureModule::RegisterEventObserver( + webrtc::AudioDeviceObserver* /*event_callback*/) { + // Only used to report warnings and errors. This fake implementation won't + // generate any so discard this callback. + return 0; +} + +int32_t FakeAudioCaptureModule::RegisterAudioCallback( + webrtc::AudioTransport* audio_callback) { + audio_callback_ = audio_callback; + return 0; +} + +int32_t FakeAudioCaptureModule::Init() { + // Initialize is called by the factory method. Safe to ignore this Init call. + return 0; +} + +int32_t FakeAudioCaptureModule::Terminate() { + // Clean up in the destructor. No action here, just success. + return 0; +} + +bool FakeAudioCaptureModule::Initialized() const { + ASSERT(false); + return 0; +} + +int16_t FakeAudioCaptureModule::PlayoutDevices() { + ASSERT(false); + return 0; +} + +int16_t FakeAudioCaptureModule::RecordingDevices() { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::PlayoutDeviceName( + uint16_t /*index*/, + char /*name*/[webrtc::kAdmMaxDeviceNameSize], + char /*guid*/[webrtc::kAdmMaxGuidSize]) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::RecordingDeviceName( + uint16_t /*index*/, + char /*name*/[webrtc::kAdmMaxDeviceNameSize], + char /*guid*/[webrtc::kAdmMaxGuidSize]) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SetPlayoutDevice(uint16_t /*index*/) { + // No playout device, just playing from file. Return success. + return 0; +} + +int32_t FakeAudioCaptureModule::SetPlayoutDevice(WindowsDeviceType /*device*/) { + if (play_is_initialized_) { + return -1; + } + return 0; +} + +int32_t FakeAudioCaptureModule::SetRecordingDevice(uint16_t /*index*/) { + // No recording device, just dropping audio. Return success. + return 0; +} + +int32_t FakeAudioCaptureModule::SetRecordingDevice( + WindowsDeviceType /*device*/) { + if (rec_is_initialized_) { + return -1; + } + return 0; +} + +int32_t FakeAudioCaptureModule::PlayoutIsAvailable(bool* /*available*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::InitPlayout() { + play_is_initialized_ = true; + return 0; +} + +bool FakeAudioCaptureModule::PlayoutIsInitialized() const { + return play_is_initialized_; +} + +int32_t FakeAudioCaptureModule::RecordingIsAvailable(bool* /*available*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::InitRecording() { + rec_is_initialized_ = true; + return 0; +} + +bool FakeAudioCaptureModule::RecordingIsInitialized() const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::StartPlayout() { + if (!play_is_initialized_) { + return -1; + } + playing_ = true; + UpdateProcessing(); + return 0; +} + +int32_t FakeAudioCaptureModule::StopPlayout() { + playing_ = false; + UpdateProcessing(); + return 0; +} + +bool FakeAudioCaptureModule::Playing() const { + return playing_; +} + +int32_t FakeAudioCaptureModule::StartRecording() { + if (!rec_is_initialized_) { + return -1; + } + recording_ = true; + UpdateProcessing(); + return 0; +} + +int32_t FakeAudioCaptureModule::StopRecording() { + recording_ = false; + UpdateProcessing(); + return 0; +} + +bool FakeAudioCaptureModule::Recording() const { + return recording_; +} + +int32_t FakeAudioCaptureModule::SetAGC(bool /*enable*/) { + // No AGC but not needed since audio is pregenerated. Return success. + return 0; +} + +bool FakeAudioCaptureModule::AGC() const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SetWaveOutVolume(uint16_t /*volume_left*/, + uint16_t /*volume_right*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::WaveOutVolume( + uint16_t* /*volume_left*/, + uint16_t* /*volume_right*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SpeakerIsAvailable(bool* available) { + // No speaker, just dropping audio. Return success. + *available = true; + return 0; +} + +int32_t FakeAudioCaptureModule::InitSpeaker() { + // No speaker, just playing from file. Return success. + return 0; +} + +bool FakeAudioCaptureModule::SpeakerIsInitialized() const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::MicrophoneIsAvailable(bool* available) { + // No microphone, just playing from file. Return success. + *available = true; + return 0; +} + +int32_t FakeAudioCaptureModule::InitMicrophone() { + // No microphone, just playing from file. Return success. + return 0; +} + +bool FakeAudioCaptureModule::MicrophoneIsInitialized() const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SpeakerVolumeIsAvailable(bool* /*available*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SetSpeakerVolume(uint32_t /*volume*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SpeakerVolume(uint32_t* /*volume*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::MaxSpeakerVolume( + uint32_t* /*max_volume*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::MinSpeakerVolume( + uint32_t* /*min_volume*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SpeakerVolumeStepSize( + uint16_t* /*step_size*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::MicrophoneVolumeIsAvailable( + bool* /*available*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SetMicrophoneVolume(uint32_t /*volume*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::MicrophoneVolume(uint32_t* volume) const { + *volume = current_mic_level_; + return 0; +} + +int32_t FakeAudioCaptureModule::MaxMicrophoneVolume( + uint32_t* max_volume) const { + *max_volume = kMaxVolume; + return 0; +} + +int32_t FakeAudioCaptureModule::MinMicrophoneVolume( + uint32_t* /*min_volume*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::MicrophoneVolumeStepSize( + uint16_t* /*step_size*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SpeakerMuteIsAvailable(bool* /*available*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SetSpeakerMute(bool /*enable*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SpeakerMute(bool* /*enabled*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::MicrophoneMuteIsAvailable(bool* /*available*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SetMicrophoneMute(bool /*enable*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::MicrophoneMute(bool* /*enabled*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::MicrophoneBoostIsAvailable( + bool* /*available*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SetMicrophoneBoost(bool /*enable*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::MicrophoneBoost(bool* /*enabled*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::StereoPlayoutIsAvailable( + bool* available) const { + // No recording device, just dropping audio. Stereo can be dropped just + // as easily as mono. + *available = true; + return 0; +} + +int32_t FakeAudioCaptureModule::SetStereoPlayout(bool /*enable*/) { + // No recording device, just dropping audio. Stereo can be dropped just + // as easily as mono. + return 0; +} + +int32_t FakeAudioCaptureModule::StereoPlayout(bool* /*enabled*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::StereoRecordingIsAvailable( + bool* available) const { + // Keep thing simple. No stereo recording. + *available = false; + return 0; +} + +int32_t FakeAudioCaptureModule::SetStereoRecording(bool enable) { + if (!enable) { + return 0; + } + return -1; +} + +int32_t FakeAudioCaptureModule::StereoRecording(bool* /*enabled*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SetRecordingChannel( + const ChannelType channel) { + if (channel != AudioDeviceModule::kChannelBoth) { + // There is no right or left in mono. I.e. kChannelBoth should be used for + // mono. + ASSERT(false); + return -1; + } + return 0; +} + +int32_t FakeAudioCaptureModule::RecordingChannel(ChannelType* channel) const { + // Stereo recording not supported. However, WebRTC ADM returns kChannelBoth + // in that case. Do the same here. + *channel = AudioDeviceModule::kChannelBoth; + return 0; +} + +int32_t FakeAudioCaptureModule::SetPlayoutBuffer(const BufferType /*type*/, + uint16_t /*size_ms*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::PlayoutBuffer(BufferType* /*type*/, + uint16_t* /*size_ms*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::PlayoutDelay(uint16_t* delay_ms) const { + // No delay since audio frames are dropped. + *delay_ms = 0; + return 0; +} + +int32_t FakeAudioCaptureModule::RecordingDelay(uint16_t* /*delay_ms*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::CPULoad(uint16_t* /*load*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::StartRawOutputFileRecording( + const char /*pcm_file_name_utf8*/[webrtc::kAdmMaxFileNameSize]) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::StopRawOutputFileRecording() { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::StartRawInputFileRecording( + const char /*pcm_file_name_utf8*/[webrtc::kAdmMaxFileNameSize]) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::StopRawInputFileRecording() { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SetRecordingSampleRate( + const uint32_t /*samples_per_sec*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::RecordingSampleRate( + uint32_t* /*samples_per_sec*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SetPlayoutSampleRate( + const uint32_t /*samples_per_sec*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::PlayoutSampleRate( + uint32_t* /*samples_per_sec*/) const { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::ResetAudioDevice() { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::SetLoudspeakerStatus(bool /*enable*/) { + ASSERT(false); + return 0; +} + +int32_t FakeAudioCaptureModule::GetLoudspeakerStatus(bool* /*enabled*/) const { + ASSERT(false); + return 0; +} + +void FakeAudioCaptureModule::OnMessage(talk_base::Message* msg) { + switch (msg->message_id) { + case MSG_RUN_PROCESS: + ProcessFrameP(); + break; + case MSG_STOP_PROCESS: + StopProcessP(); + break; + default: + // All existing messages should be caught. Getting here should never + // happen. + ASSERT(false); + } +} + +bool FakeAudioCaptureModule::Initialize() { + // Set the send buffer samples high enough that it would not occur on the + // remote side unless a packet containing a sample of that magnitude has been + // sent to it. Note that the audio processing pipeline will likely distort the + // original signal. + SetSendBuffer(kHighSampleValue); + last_process_time_ms_ = talk_base::Time(); + return true; +} + +void FakeAudioCaptureModule::SetSendBuffer(int value) { + Sample* buffer_ptr = reinterpret_cast(send_buffer_); + const int buffer_size_in_samples = sizeof(send_buffer_) / + kNumberBytesPerSample; + for (int i = 0; i < buffer_size_in_samples; ++i) { + buffer_ptr[i] = value; + } +} + +void FakeAudioCaptureModule::ResetRecBuffer() { + memset(rec_buffer_, 0, sizeof(rec_buffer_)); +} + +bool FakeAudioCaptureModule::CheckRecBuffer(int value) { + const Sample* buffer_ptr = reinterpret_cast(rec_buffer_); + const int buffer_size_in_samples = sizeof(rec_buffer_) / + kNumberBytesPerSample; + for (int i = 0; i < buffer_size_in_samples; ++i) { + if (buffer_ptr[i] >= value) return true; + } + return false; +} + +void FakeAudioCaptureModule::UpdateProcessing() { + const bool process = recording_ || playing_; + if (process) { + if (started_) { + // Already started. + return; + } + process_thread_->Post(this, MSG_RUN_PROCESS); + } else { + process_thread_->Send(this, MSG_STOP_PROCESS); + } +} + +void FakeAudioCaptureModule::ProcessFrameP() { + ASSERT(talk_base::Thread::Current() == process_thread_); + if (!started_) { + next_frame_time_ = talk_base::Time(); + started_ = true; + } + // Receive and send frames every kTimePerFrameMs. + if (audio_callback_ != NULL) { + if (playing_) { + ReceiveFrameP(); + } + if (recording_) { + SendFrameP(); + } + } + + next_frame_time_ += kTimePerFrameMs; + const uint32 current_time = talk_base::Time(); + const uint32 wait_time = (next_frame_time_ > current_time) ? + next_frame_time_ - current_time : 0; + process_thread_->PostDelayed(wait_time, this, MSG_RUN_PROCESS); +} + +void FakeAudioCaptureModule::ReceiveFrameP() { + ASSERT(talk_base::Thread::Current() == process_thread_); + ResetRecBuffer(); + uint32_t nSamplesOut = 0; + if (audio_callback_->NeedMorePlayData(kNumberSamples, kNumberBytesPerSample, + kNumberOfChannels, kSamplesPerSecond, + rec_buffer_, nSamplesOut) != 0) { + ASSERT(false); + } + ASSERT(nSamplesOut == kNumberSamples); + // The SetBuffer() function ensures that after decoding, the audio buffer + // should contain samples of similar magnitude (there is likely to be some + // distortion due to the audio pipeline). If one sample is detected to + // have the same or greater magnitude somewhere in the frame, an actual frame + // has been received from the remote side (i.e. faked frames are not being + // pulled). + if (CheckRecBuffer(kHighSampleValue)) ++frames_received_; +} + +void FakeAudioCaptureModule::SendFrameP() { + ASSERT(talk_base::Thread::Current() == process_thread_); + bool key_pressed = false; + if (audio_callback_->RecordedDataIsAvailable(send_buffer_, kNumberSamples, + kNumberBytesPerSample, + kNumberOfChannels, + kSamplesPerSecond, kTotalDelayMs, + kClockDriftMs, current_mic_level_, + key_pressed, + current_mic_level_) != 0) { + ASSERT(false); + } +} + +void FakeAudioCaptureModule::StopProcessP() { + ASSERT(talk_base::Thread::Current() == process_thread_); + started_ = false; + process_thread_->Clear(this); +} diff --git a/talk/app/webrtc/test/fakeaudiocapturemodule.h b/talk/app/webrtc/test/fakeaudiocapturemodule.h new file mode 100644 index 000000000..c32fa1f74 --- /dev/null +++ b/talk/app/webrtc/test/fakeaudiocapturemodule.h @@ -0,0 +1,280 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// This class implements an AudioCaptureModule that can be used to detect if +// audio is being received properly if it is fed by another AudioCaptureModule +// in some arbitrary audio pipeline where they are connected. It does not play +// out or record any audio so it does not need access to any hardware and can +// therefore be used in the gtest testing framework. + +// Note P postfix of a function indicates that it should only be called by the +// processing thread. + +#ifndef TALK_APP_WEBRTC_TEST_FAKEAUDIOCAPTUREMODULE_H_ +#define TALK_APP_WEBRTC_TEST_FAKEAUDIOCAPTUREMODULE_H_ + +#include "talk/base/basictypes.h" +#include "talk/base/messagehandler.h" +#include "talk/base/scoped_ref_ptr.h" +#include "webrtc/common_types.h" +#include "webrtc/modules/audio_device/include/audio_device.h" + +namespace talk_base { + +class Thread; + +} // namespace talk_base + +class FakeAudioCaptureModule + : public webrtc::AudioDeviceModule, + public talk_base::MessageHandler { + public: + typedef uint16 Sample; + + // The value for the following constants have been derived by running VoE + // using a real ADM. The constants correspond to 10ms of mono audio at 44kHz. + enum{kNumberSamples = 440}; + enum{kNumberBytesPerSample = sizeof(Sample)}; + + // Creates a FakeAudioCaptureModule or returns NULL on failure. + // |process_thread| is used to push and pull audio frames to and from the + // returned instance. Note: ownership of |process_thread| is not handed over. + static talk_base::scoped_refptr Create( + talk_base::Thread* process_thread); + + // Returns the number of frames that have been successfully pulled by the + // instance. Note that correctly detecting success can only be done if the + // pulled frame was generated/pushed from a FakeAudioCaptureModule. + int frames_received() const; + + // Following functions are inherited from webrtc::AudioDeviceModule. + // Only functions called by PeerConnection are implemented, the rest do + // nothing and return success. If a function is not expected to be called by + // PeerConnection an assertion is triggered if it is in fact called. + virtual int32_t Version(char* version, + uint32_t& remaining_buffer_in_bytes, + uint32_t& position) const; + virtual int32_t TimeUntilNextProcess(); + virtual int32_t Process(); + virtual int32_t ChangeUniqueId(const int32_t id); + + virtual int32_t ActiveAudioLayer(AudioLayer* audio_layer) const; + + virtual ErrorCode LastError() const; + virtual int32_t RegisterEventObserver( + webrtc::AudioDeviceObserver* event_callback); + + virtual int32_t RegisterAudioCallback(webrtc::AudioTransport* audio_callback); + + virtual int32_t Init(); + virtual int32_t Terminate(); + virtual bool Initialized() const; + + virtual int16_t PlayoutDevices(); + virtual int16_t RecordingDevices(); + virtual int32_t PlayoutDeviceName(uint16_t index, + char name[webrtc::kAdmMaxDeviceNameSize], + char guid[webrtc::kAdmMaxGuidSize]); + virtual int32_t RecordingDeviceName(uint16_t index, + char name[webrtc::kAdmMaxDeviceNameSize], + char guid[webrtc::kAdmMaxGuidSize]); + + virtual int32_t SetPlayoutDevice(uint16_t index); + virtual int32_t SetPlayoutDevice(WindowsDeviceType device); + virtual int32_t SetRecordingDevice(uint16_t index); + virtual int32_t SetRecordingDevice(WindowsDeviceType device); + + virtual int32_t PlayoutIsAvailable(bool* available); + virtual int32_t InitPlayout(); + virtual bool PlayoutIsInitialized() const; + virtual int32_t RecordingIsAvailable(bool* available); + virtual int32_t InitRecording(); + virtual bool RecordingIsInitialized() const; + + virtual int32_t StartPlayout(); + virtual int32_t StopPlayout(); + virtual bool Playing() const; + virtual int32_t StartRecording(); + virtual int32_t StopRecording(); + virtual bool Recording() const; + + virtual int32_t SetAGC(bool enable); + virtual bool AGC() const; + + virtual int32_t SetWaveOutVolume(uint16_t volume_left, + uint16_t volume_right); + virtual int32_t WaveOutVolume(uint16_t* volume_left, + uint16_t* volume_right) const; + + virtual int32_t SpeakerIsAvailable(bool* available); + virtual int32_t InitSpeaker(); + virtual bool SpeakerIsInitialized() const; + virtual int32_t MicrophoneIsAvailable(bool* available); + virtual int32_t InitMicrophone(); + virtual bool MicrophoneIsInitialized() const; + + virtual int32_t SpeakerVolumeIsAvailable(bool* available); + virtual int32_t SetSpeakerVolume(uint32_t volume); + virtual int32_t SpeakerVolume(uint32_t* volume) const; + virtual int32_t MaxSpeakerVolume(uint32_t* max_volume) const; + virtual int32_t MinSpeakerVolume(uint32_t* min_volume) const; + virtual int32_t SpeakerVolumeStepSize(uint16_t* step_size) const; + + virtual int32_t MicrophoneVolumeIsAvailable(bool* available); + virtual int32_t SetMicrophoneVolume(uint32_t volume); + virtual int32_t MicrophoneVolume(uint32_t* volume) const; + virtual int32_t MaxMicrophoneVolume(uint32_t* max_volume) const; + + virtual int32_t MinMicrophoneVolume(uint32_t* min_volume) const; + virtual int32_t MicrophoneVolumeStepSize(uint16_t* step_size) const; + + virtual int32_t SpeakerMuteIsAvailable(bool* available); + virtual int32_t SetSpeakerMute(bool enable); + virtual int32_t SpeakerMute(bool* enabled) const; + + virtual int32_t MicrophoneMuteIsAvailable(bool* available); + virtual int32_t SetMicrophoneMute(bool enable); + virtual int32_t MicrophoneMute(bool* enabled) const; + + virtual int32_t MicrophoneBoostIsAvailable(bool* available); + virtual int32_t SetMicrophoneBoost(bool enable); + virtual int32_t MicrophoneBoost(bool* enabled) const; + + virtual int32_t StereoPlayoutIsAvailable(bool* available) const; + virtual int32_t SetStereoPlayout(bool enable); + virtual int32_t StereoPlayout(bool* enabled) const; + virtual int32_t StereoRecordingIsAvailable(bool* available) const; + virtual int32_t SetStereoRecording(bool enable); + virtual int32_t StereoRecording(bool* enabled) const; + virtual int32_t SetRecordingChannel(const ChannelType channel); + virtual int32_t RecordingChannel(ChannelType* channel) const; + + virtual int32_t SetPlayoutBuffer(const BufferType type, + uint16_t size_ms = 0); + virtual int32_t PlayoutBuffer(BufferType* type, + uint16_t* size_ms) const; + virtual int32_t PlayoutDelay(uint16_t* delay_ms) const; + virtual int32_t RecordingDelay(uint16_t* delay_ms) const; + + virtual int32_t CPULoad(uint16_t* load) const; + + virtual int32_t StartRawOutputFileRecording( + const char pcm_file_name_utf8[webrtc::kAdmMaxFileNameSize]); + virtual int32_t StopRawOutputFileRecording(); + virtual int32_t StartRawInputFileRecording( + const char pcm_file_name_utf8[webrtc::kAdmMaxFileNameSize]); + virtual int32_t StopRawInputFileRecording(); + + virtual int32_t SetRecordingSampleRate(const uint32_t samples_per_sec); + virtual int32_t RecordingSampleRate(uint32_t* samples_per_sec) const; + virtual int32_t SetPlayoutSampleRate(const uint32_t samples_per_sec); + virtual int32_t PlayoutSampleRate(uint32_t* samples_per_sec) const; + + virtual int32_t ResetAudioDevice(); + virtual int32_t SetLoudspeakerStatus(bool enable); + virtual int32_t GetLoudspeakerStatus(bool* enabled) const; + // End of functions inherited from webrtc::AudioDeviceModule. + + // The following function is inherited from talk_base::MessageHandler. + virtual void OnMessage(talk_base::Message* msg); + + protected: + // The constructor is protected because the class needs to be created as a + // reference counted object (for memory managment reasons). It could be + // exposed in which case the burden of proper instantiation would be put on + // the creator of a FakeAudioCaptureModule instance. To create an instance of + // this class use the Create(..) API. + explicit FakeAudioCaptureModule(talk_base::Thread* process_thread); + // The destructor is protected because it is reference counted and should not + // be deleted directly. + virtual ~FakeAudioCaptureModule(); + + private: + // Initializes the state of the FakeAudioCaptureModule. This API is called on + // creation by the Create() API. + bool Initialize(); + // SetBuffer() sets all samples in send_buffer_ to |value|. + void SetSendBuffer(int value); + // Resets rec_buffer_. I.e., sets all rec_buffer_ samples to 0. + void ResetRecBuffer(); + // Returns true if rec_buffer_ contains one or more sample greater than or + // equal to |value|. + bool CheckRecBuffer(int value); + + // Starts or stops the pushing and pulling of audio frames depending on if + // recording or playback has been enabled/started. + void UpdateProcessing(); + + // Periodcally called function that ensures that frames are pulled and pushed + // periodically if enabled/started. + void ProcessFrameP(); + // Pulls frames from the registered webrtc::AudioTransport. + void ReceiveFrameP(); + // Pushes frames to the registered webrtc::AudioTransport. + void SendFrameP(); + // Stops the periodic calling of ProcessFrame() in a thread safe way. + void StopProcessP(); + + // The time in milliseconds when Process() was last called or 0 if no call + // has been made. + uint32 last_process_time_ms_; + + // Callback for playout and recording. + webrtc::AudioTransport* audio_callback_; + + bool recording_; // True when audio is being pushed from the instance. + bool playing_; // True when audio is being pulled by the instance. + + bool play_is_initialized_; // True when the instance is ready to pull audio. + bool rec_is_initialized_; // True when the instance is ready to push audio. + + // Input to and output from RecordedDataIsAvailable(..) makes it possible to + // modify the current mic level. The implementation does not care about the + // mic level so it just feeds back what it receives. + uint32_t current_mic_level_; + + // next_frame_time_ is updated in a non-drifting manner to indicate the next + // wall clock time the next frame should be generated and received. started_ + // ensures that next_frame_time_ can be initialized properly on first call. + bool started_; + uint32 next_frame_time_; + + // User provided thread context. + talk_base::Thread* process_thread_; + + // Buffer for storing samples received from the webrtc::AudioTransport. + char rec_buffer_[kNumberSamples * kNumberBytesPerSample]; + // Buffer for samples to send to the webrtc::AudioTransport. + char send_buffer_[kNumberSamples * kNumberBytesPerSample]; + + // Counter of frames received that have samples of high enough amplitude to + // indicate that the frames are not faked somewhere in the audio pipeline + // (e.g. by a jitter buffer). + int frames_received_; +}; + +#endif // TALK_APP_WEBRTC_TEST_FAKEAUDIOCAPTUREMODULE_H_ diff --git a/talk/app/webrtc/test/fakeaudiocapturemodule_unittest.cc b/talk/app/webrtc/test/fakeaudiocapturemodule_unittest.cc new file mode 100644 index 000000000..5738955ec --- /dev/null +++ b/talk/app/webrtc/test/fakeaudiocapturemodule_unittest.cc @@ -0,0 +1,212 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/test/fakeaudiocapturemodule.h" + +#include + +#include "talk/base/gunit.h" +#include "talk/base/scoped_ref_ptr.h" +#include "talk/base/thread.h" + +using std::min; + +class FakeAdmTest : public testing::Test, + public webrtc::AudioTransport { + protected: + static const int kMsInSecond = 1000; + + FakeAdmTest() + : push_iterations_(0), + pull_iterations_(0), + rec_buffer_bytes_(0) { + memset(rec_buffer_, 0, sizeof(rec_buffer_)); + } + + virtual void SetUp() { + fake_audio_capture_module_ = FakeAudioCaptureModule::Create( + talk_base::Thread::Current()); + EXPECT_TRUE(fake_audio_capture_module_.get() != NULL); + } + + // Callbacks inherited from webrtc::AudioTransport. + // ADM is pushing data. + virtual int32_t RecordedDataIsAvailable(const void* audioSamples, + const uint32_t nSamples, + const uint8_t nBytesPerSample, + const uint8_t nChannels, + const uint32_t samplesPerSec, + const uint32_t totalDelayMS, + const int32_t clockDrift, + const uint32_t currentMicLevel, + const bool keyPressed, + uint32_t& newMicLevel) { + rec_buffer_bytes_ = nSamples * nBytesPerSample; + if ((rec_buffer_bytes_ <= 0) || + (rec_buffer_bytes_ > FakeAudioCaptureModule::kNumberSamples * + FakeAudioCaptureModule::kNumberBytesPerSample)) { + ADD_FAILURE(); + return -1; + } + memcpy(rec_buffer_, audioSamples, rec_buffer_bytes_); + ++push_iterations_; + newMicLevel = currentMicLevel; + return 0; + } + + // ADM is pulling data. + virtual int32_t NeedMorePlayData(const uint32_t nSamples, + const uint8_t nBytesPerSample, + const uint8_t nChannels, + const uint32_t samplesPerSec, + void* audioSamples, + uint32_t& nSamplesOut) { + ++pull_iterations_; + const uint32_t audio_buffer_size = nSamples * nBytesPerSample; + const uint32_t bytes_out = RecordedDataReceived() ? + CopyFromRecBuffer(audioSamples, audio_buffer_size): + GenerateZeroBuffer(audioSamples, audio_buffer_size); + nSamplesOut = bytes_out / nBytesPerSample; + return 0; + } + + int push_iterations() const { return push_iterations_; } + int pull_iterations() const { return pull_iterations_; } + + talk_base::scoped_refptr fake_audio_capture_module_; + + private: + bool RecordedDataReceived() const { + return rec_buffer_bytes_ != 0; + } + int32_t GenerateZeroBuffer(void* audio_buffer, uint32_t audio_buffer_size) { + memset(audio_buffer, 0, audio_buffer_size); + return audio_buffer_size; + } + int32_t CopyFromRecBuffer(void* audio_buffer, uint32_t audio_buffer_size) { + EXPECT_EQ(audio_buffer_size, rec_buffer_bytes_); + const uint32_t min_buffer_size = min(audio_buffer_size, rec_buffer_bytes_); + memcpy(audio_buffer, rec_buffer_, min_buffer_size); + return min_buffer_size; + } + + int push_iterations_; + int pull_iterations_; + + char rec_buffer_[FakeAudioCaptureModule::kNumberSamples * + FakeAudioCaptureModule::kNumberBytesPerSample]; + uint32_t rec_buffer_bytes_; +}; + +TEST_F(FakeAdmTest, TestProccess) { + // Next process call must be some time in the future (or now). + EXPECT_LE(0, fake_audio_capture_module_->TimeUntilNextProcess()); + // Process call updates TimeUntilNextProcess() but there are no guarantees on + // timing so just check that Process can ba called successfully. + EXPECT_LE(0, fake_audio_capture_module_->Process()); +} + +TEST_F(FakeAdmTest, PlayoutTest) { + EXPECT_EQ(0, fake_audio_capture_module_->RegisterAudioCallback(this)); + + bool speaker_available = false; + EXPECT_EQ(0, fake_audio_capture_module_->SpeakerIsAvailable( + &speaker_available)); + EXPECT_TRUE(speaker_available); + + bool stereo_available = false; + EXPECT_EQ(0, + fake_audio_capture_module_->StereoPlayoutIsAvailable( + &stereo_available)); + EXPECT_TRUE(stereo_available); + + EXPECT_NE(0, fake_audio_capture_module_->StartPlayout()); + EXPECT_FALSE(fake_audio_capture_module_->PlayoutIsInitialized()); + EXPECT_FALSE(fake_audio_capture_module_->Playing()); + EXPECT_EQ(0, fake_audio_capture_module_->StopPlayout()); + + EXPECT_EQ(0, fake_audio_capture_module_->InitPlayout()); + EXPECT_TRUE(fake_audio_capture_module_->PlayoutIsInitialized()); + EXPECT_FALSE(fake_audio_capture_module_->Playing()); + + EXPECT_EQ(0, fake_audio_capture_module_->StartPlayout()); + EXPECT_TRUE(fake_audio_capture_module_->Playing()); + + uint16_t delay_ms = 10; + EXPECT_EQ(0, fake_audio_capture_module_->PlayoutDelay(&delay_ms)); + EXPECT_EQ(0, delay_ms); + + EXPECT_TRUE_WAIT(pull_iterations() > 0, kMsInSecond); + EXPECT_GE(0, push_iterations()); + + EXPECT_EQ(0, fake_audio_capture_module_->StopPlayout()); + EXPECT_FALSE(fake_audio_capture_module_->Playing()); +} + +TEST_F(FakeAdmTest, RecordTest) { + EXPECT_EQ(0, fake_audio_capture_module_->RegisterAudioCallback(this)); + + bool microphone_available = false; + EXPECT_EQ(0, fake_audio_capture_module_->MicrophoneIsAvailable( + µphone_available)); + EXPECT_TRUE(microphone_available); + + bool stereo_available = false; + EXPECT_EQ(0, fake_audio_capture_module_->StereoRecordingIsAvailable( + &stereo_available)); + EXPECT_FALSE(stereo_available); + + EXPECT_NE(0, fake_audio_capture_module_->StartRecording()); + EXPECT_FALSE(fake_audio_capture_module_->Recording()); + EXPECT_EQ(0, fake_audio_capture_module_->StopRecording()); + + EXPECT_EQ(0, fake_audio_capture_module_->InitRecording()); + EXPECT_EQ(0, fake_audio_capture_module_->StartRecording()); + EXPECT_TRUE(fake_audio_capture_module_->Recording()); + + EXPECT_TRUE_WAIT(push_iterations() > 0, kMsInSecond); + EXPECT_GE(0, pull_iterations()); + + EXPECT_EQ(0, fake_audio_capture_module_->StopRecording()); + EXPECT_FALSE(fake_audio_capture_module_->Recording()); +} + +TEST_F(FakeAdmTest, DuplexTest) { + EXPECT_EQ(0, fake_audio_capture_module_->RegisterAudioCallback(this)); + + EXPECT_EQ(0, fake_audio_capture_module_->InitPlayout()); + EXPECT_EQ(0, fake_audio_capture_module_->StartPlayout()); + + EXPECT_EQ(0, fake_audio_capture_module_->InitRecording()); + EXPECT_EQ(0, fake_audio_capture_module_->StartRecording()); + + EXPECT_TRUE_WAIT(push_iterations() > 0, kMsInSecond); + EXPECT_TRUE_WAIT(pull_iterations() > 0, kMsInSecond); + + EXPECT_EQ(0, fake_audio_capture_module_->StopPlayout()); + EXPECT_EQ(0, fake_audio_capture_module_->StopRecording()); +} diff --git a/talk/app/webrtc/test/fakeconstraints.h b/talk/app/webrtc/test/fakeconstraints.h new file mode 100644 index 000000000..0299afa83 --- /dev/null +++ b/talk/app/webrtc/test/fakeconstraints.h @@ -0,0 +1,118 @@ +/* + * libjingle + * Copyright 2012, 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. + * + */ +#ifndef TALK_APP_WEBRTC_TEST_FAKECONSTRAINTS_H_ +#define TALK_APP_WEBRTC_TEST_FAKECONSTRAINTS_H_ + +#include +#include + +#include "talk/app/webrtc/mediaconstraintsinterface.h" +#include "talk/base/stringencode.h" + +namespace webrtc { + +class FakeConstraints : public webrtc::MediaConstraintsInterface { + public: + FakeConstraints() { } + virtual ~FakeConstraints() { } + + virtual const Constraints& GetMandatory() const { + return mandatory_; + } + + virtual const Constraints& GetOptional() const { + return optional_; + } + + template + void AddMandatory(const std::string& key, const T& value) { + mandatory_.push_back(Constraint(key, talk_base::ToString(value))); + } + + template + void AddOptional(const std::string& key, const T& value) { + optional_.push_back(Constraint(key, talk_base::ToString(value))); + } + + void SetMandatoryMinAspectRatio(double ratio) { + AddMandatory(MediaConstraintsInterface::kMinAspectRatio, ratio); + } + + void SetMandatoryMinWidth(int width) { + AddMandatory(MediaConstraintsInterface::kMinWidth, width); + } + + void SetMandatoryMinHeight(int height) { + AddMandatory(MediaConstraintsInterface::kMinHeight, height); + } + + void SetOptionalMaxWidth(int width) { + AddOptional(MediaConstraintsInterface::kMaxWidth, width); + } + + void SetMandatoryMaxFrameRate(int frame_rate) { + AddMandatory(MediaConstraintsInterface::kMaxFrameRate, frame_rate); + } + + void SetMandatoryReceiveAudio(bool enable) { + AddMandatory(MediaConstraintsInterface::kOfferToReceiveAudio, enable); + } + + void SetMandatoryReceiveVideo(bool enable) { + AddMandatory(MediaConstraintsInterface::kOfferToReceiveVideo, enable); + } + + void SetMandatoryUseRtpMux(bool enable) { + AddMandatory(MediaConstraintsInterface::kUseRtpMux, enable); + } + + void SetMandatoryIceRestart(bool enable) { + AddMandatory(MediaConstraintsInterface::kIceRestart, enable); + } + + void SetAllowRtpDataChannels() { + AddMandatory(MediaConstraintsInterface::kEnableRtpDataChannels, true); + } + + void SetOptionalVAD(bool enable) { + AddOptional(MediaConstraintsInterface::kVoiceActivityDetection, enable); + } + + void SetAllowDtlsSctpDataChannels() { + AddMandatory(MediaConstraintsInterface::kEnableSctpDataChannels, true); + AddMandatory(MediaConstraintsInterface::kEnableDtlsSrtp, true); + } + + private: + Constraints mandatory_; + Constraints optional_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_TEST_FAKECONSTRAINTS_H_ diff --git a/talk/app/webrtc/test/fakeperiodicvideocapturer.h b/talk/app/webrtc/test/fakeperiodicvideocapturer.h new file mode 100644 index 000000000..7f70ae2ed --- /dev/null +++ b/talk/app/webrtc/test/fakeperiodicvideocapturer.h @@ -0,0 +1,89 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// FakePeriodicVideoCapturer implements a fake cricket::VideoCapturer that +// creates video frames periodically after it has been started. + +#ifndef TALK_APP_WEBRTC_TEST_FAKEPERIODICVIDEOCAPTURER_H_ +#define TALK_APP_WEBRTC_TEST_FAKEPERIODICVIDEOCAPTURER_H_ + +#include "talk/base/thread.h" +#include "talk/media/base/fakevideocapturer.h" + +namespace webrtc { + +class FakePeriodicVideoCapturer : public cricket::FakeVideoCapturer { + public: + FakePeriodicVideoCapturer() { + std::vector formats; + formats.push_back(cricket::VideoFormat(1280, 720, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(640, 360, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(320, 240, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(160, 120, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + ResetSupportedFormats(formats); + }; + + virtual cricket::CaptureState Start(const cricket::VideoFormat& format) { + cricket::CaptureState state = FakeVideoCapturer::Start(format); + if (state != cricket::CS_FAILED) { + talk_base::Thread::Current()->Post(this, MSG_CREATEFRAME); + } + return state; + } + virtual void Stop() { + talk_base::Thread::Current()->Clear(this); + } + // Inherited from MesageHandler. + virtual void OnMessage(talk_base::Message* msg) { + if (msg->message_id == MSG_CREATEFRAME) { + if (IsRunning()) { + CaptureFrame(); + talk_base::Thread::Current()->PostDelayed(static_cast( + GetCaptureFormat()->interval / talk_base::kNumNanosecsPerMillisec), + this, MSG_CREATEFRAME); + } + } else { + FakeVideoCapturer::OnMessage(msg); + } + } + + private: + enum { + // Offset 0xFF to make sure this don't collide with base class messages. + MSG_CREATEFRAME = 0xFF + }; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_TEST_FAKEPERIODICVIDEOCAPTURER_H_ diff --git a/talk/app/webrtc/test/fakevideotrackrenderer.h b/talk/app/webrtc/test/fakevideotrackrenderer.h new file mode 100644 index 000000000..0030a0c39 --- /dev/null +++ b/talk/app/webrtc/test/fakevideotrackrenderer.h @@ -0,0 +1,70 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_TEST_FAKEVIDEOTRACKRENDERER_H_ +#define TALK_APP_WEBRTC_TEST_FAKEVIDEOTRACKRENDERER_H_ + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/media/base/fakevideorenderer.h" + +namespace webrtc { + +class FakeVideoTrackRenderer : public VideoRendererInterface { + public: + explicit FakeVideoTrackRenderer(VideoTrackInterface* video_track) + : video_track_(video_track) { + video_track_->AddRenderer(this); + } + ~FakeVideoTrackRenderer() { + video_track_->RemoveRenderer(this); + } + + // Implements VideoRendererInterface + virtual void SetSize(int width, int height) { + fake_renderer_.SetSize(width, height, 0); + } + + virtual void RenderFrame(const cricket::VideoFrame* frame) { + fake_renderer_.RenderFrame(frame); + } + + int errors() const { return fake_renderer_.errors(); } + int width() const { return fake_renderer_.width(); } + int height() const { return fake_renderer_.height(); } + int num_set_sizes() const { return fake_renderer_.num_set_sizes(); } + int num_rendered_frames() const { + return fake_renderer_.num_rendered_frames(); + } + + private: + cricket::FakeVideoRenderer fake_renderer_; + talk_base::scoped_refptr video_track_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_TEST_FAKEVIDEOTRACKRENDERER_H_ diff --git a/talk/app/webrtc/test/mockpeerconnectionobservers.h b/talk/app/webrtc/test/mockpeerconnectionobservers.h new file mode 100644 index 000000000..e2de37930 --- /dev/null +++ b/talk/app/webrtc/test/mockpeerconnectionobservers.h @@ -0,0 +1,172 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// This file contains mock implementations of observers used in PeerConnection. + +#ifndef TALK_APP_WEBRTC_TEST_MOCKPEERCONNECTIONOBSERVERS_H_ +#define TALK_APP_WEBRTC_TEST_MOCKPEERCONNECTIONOBSERVERS_H_ + +#include + +#include "talk/app/webrtc/datachannelinterface.h" + +namespace webrtc { + +class MockCreateSessionDescriptionObserver + : public webrtc::CreateSessionDescriptionObserver { + public: + MockCreateSessionDescriptionObserver() + : called_(false), + result_(false) {} + virtual ~MockCreateSessionDescriptionObserver() {} + virtual void OnSuccess(SessionDescriptionInterface* desc) { + called_ = true; + result_ = true; + desc_.reset(desc); + } + virtual void OnFailure(const std::string& error) { + called_ = true; + result_ = false; + } + bool called() const { return called_; } + bool result() const { return result_; } + SessionDescriptionInterface* release_desc() { + return desc_.release(); + } + + private: + bool called_; + bool result_; + talk_base::scoped_ptr desc_; +}; + +class MockSetSessionDescriptionObserver + : public webrtc::SetSessionDescriptionObserver { + public: + MockSetSessionDescriptionObserver() + : called_(false), + result_(false) {} + virtual ~MockSetSessionDescriptionObserver() {} + virtual void OnSuccess() { + called_ = true; + result_ = true; + } + virtual void OnFailure(const std::string& error) { + called_ = true; + result_ = false; + } + bool called() const { return called_; } + bool result() const { return result_; } + + private: + bool called_; + bool result_; +}; + +class MockDataChannelObserver : public webrtc::DataChannelObserver { + public: + explicit MockDataChannelObserver(webrtc::DataChannelInterface* channel) + : channel_(channel) { + channel_->RegisterObserver(this); + state_ = channel_->state(); + } + virtual ~MockDataChannelObserver() { + channel_->UnregisterObserver(); + } + + virtual void OnStateChange() { state_ = channel_->state(); } + virtual void OnMessage(const DataBuffer& buffer) { + last_message_.assign(buffer.data.data(), buffer.data.length()); + } + + bool IsOpen() const { return state_ == DataChannelInterface::kOpen; } + const std::string& last_message() const { return last_message_; } + + private: + talk_base::scoped_refptr channel_; + DataChannelInterface::DataState state_; + std::string last_message_; +}; + +class MockStatsObserver : public webrtc::StatsObserver { + public: + MockStatsObserver() + : called_(false) {} + virtual ~MockStatsObserver() {} + virtual void OnComplete(const std::vector& reports) { + called_ = true; + reports_ = reports; + } + + bool called() const { return called_; } + size_t number_of_reports() const { return reports_.size(); } + + int AudioOutputLevel() { + return GetSsrcStatsValue( + webrtc::StatsReport::kStatsValueNameAudioOutputLevel); + } + + int AudioInputLevel() { + return GetSsrcStatsValue( + webrtc::StatsReport::kStatsValueNameAudioInputLevel); + } + + int BytesReceived() { + return GetSsrcStatsValue( + webrtc::StatsReport::kStatsValueNameBytesReceived); + } + + int BytesSent() { + return GetSsrcStatsValue(webrtc::StatsReport::kStatsValueNameBytesSent); + } + + private: + int GetSsrcStatsValue(const std::string name) { + if (reports_.empty()) { + return 0; + } + for (size_t i = 0; i < reports_.size(); ++i) { + if (reports_[i].type != StatsReport::kStatsReportTypeSsrc) + continue; + webrtc::StatsReport::Values::const_iterator it = + reports_[i].values.begin(); + for (; it != reports_[i].values.end(); ++it) { + if (it->name == name) { + return talk_base::FromString(it->value); + } + } + } + return 0; + } + + bool called_; + std::vector reports_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_TEST_MOCKPEERCONNECTIONOBSERVERS_H_ diff --git a/talk/app/webrtc/test/testsdpstrings.h b/talk/app/webrtc/test/testsdpstrings.h new file mode 100644 index 000000000..9f95d361f --- /dev/null +++ b/talk/app/webrtc/test/testsdpstrings.h @@ -0,0 +1,144 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// This file contain SDP strings used for testing. + +#ifndef TALK_APP_WEBRTC_TEST_TESTSDPSTRINGS_H_ +#define TALK_APP_WEBRTC_TEST_TESTSDPSTRINGS_H_ + +namespace webrtc { + +// SDP offer string from a Nightly FireFox build. +static const char kFireFoxSdpOffer[] = + "v=0\r\n" + "o=Mozilla-SIPUA 23551 0 IN IP4 0.0.0.0\r\n" + "s=SIP Call\r\n" + "t=0 0\r\n" + "a=ice-ufrag:e5785931\r\n" + "a=ice-pwd:36fb7878390db89481c1d46daa4278d8\r\n" + "a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:" + "BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93\r\n" + "m=audio 36993 RTP/SAVPF 109 0 8 101\r\n" + "c=IN IP4 74.95.2.170\r\n" + "a=rtpmap:109 opus/48000/2\r\n" + "a=ptime:20\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:8 PCMA/8000\r\n" + "a=rtpmap:101 telephone-event/8000\r\n" + "a=fmtp:101 0-15\r\n" + "a=sendrecv\r\n" + "a=candidate:0 1 UDP 2112946431 172.16.191.1 61725 typ host\r\n" + "a=candidate:2 1 UDP 2112487679 172.16.131.1 58798 typ host\r\n" + "a=candidate:4 1 UDP 2113667327 10.0.254.2 58122 typ host\r\n" + "a=candidate:5 1 UDP 1694302207 74.95.2.170 36993 typ srflx raddr " + "10.0.254.2 rport 58122\r\n" + "a=candidate:0 2 UDP 2112946430 172.16.191.1 55025 typ host\r\n" + "a=candidate:2 2 UDP 2112487678 172.16.131.1 63576 typ host\r\n" + "a=candidate:4 2 UDP 2113667326 10.0.254.2 50962 typ host\r\n" + "a=candidate:5 2 UDP 1694302206 74.95.2.170 41028 typ srflx raddr" + " 10.0.254.2 rport 50962\r\n" + "m=video 38826 RTP/SAVPF 120\r\n" + "c=IN IP4 74.95.2.170\r\n" + "a=rtpmap:120 VP8/90000\r\n" + "a=sendrecv\r\n" + "a=candidate:0 1 UDP 2112946431 172.16.191.1 62017 typ host\r\n" + "a=candidate:2 1 UDP 2112487679 172.16.131.1 59741 typ host\r\n" + "a=candidate:4 1 UDP 2113667327 10.0.254.2 62652 typ host\r\n" + "a=candidate:5 1 UDP 1694302207 74.95.2.170 38826 typ srflx raddr" + " 10.0.254.2 rport 62652\r\n" + "a=candidate:0 2 UDP 2112946430 172.16.191.1 63440 typ host\r\n" + "a=candidate:2 2 UDP 2112487678 172.16.131.1 51847 typ host\r\n" + "a=candidate:4 2 UDP 2113667326 10.0.254.2 58890 typ host\r\n" + "a=candidate:5 2 UDP 1694302206 74.95.2.170 33611 typ srflx raddr" + " 10.0.254.2 rport 58890\r\n" + "m=application 45536 SCTP/DTLS 5000\r\n" + "c=IN IP4 74.95.2.170\r\n" + "a=fmtp:5000 protocol=webrtc-datachannel;streams=16\r\n" + "a=sendrecv\r\n" + "a=candidate:0 1 UDP 2112946431 172.16.191.1 60248 typ host\r\n" + "a=candidate:2 1 UDP 2112487679 172.16.131.1 55925 typ host\r\n" + "a=candidate:4 1 UDP 2113667327 10.0.254.2 65268 typ host\r\n" + "a=candidate:5 1 UDP 1694302207 74.95.2.170 45536 typ srflx raddr" + " 10.0.254.2 rport 65268\r\n" + "a=candidate:0 2 UDP 2112946430 172.16.191.1 49162 typ host\r\n" + "a=candidate:2 2 UDP 2112487678 172.16.131.1 59635 typ host\r\n" + "a=candidate:4 2 UDP 2113667326 10.0.254.2 61232 typ host\r\n" + "a=candidate:5 2 UDP 1694302206 74.95.2.170 45468 typ srflx raddr" + " 10.0.254.2 rport 61232\r\n"; + +// Audio SDP with a limited set of audio codecs. +static const char kAudioSdp[] = + "v=0\r\n" + "o=- 7859371131 2 IN IP4 192.168.30.208\r\n" + "s=-\r\n" + "c=IN IP4 192.168.30.208\r\n" + "t=0 0\r\n" + "m=audio 16000 RTP/SAVPF 0 8 126\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:8 PCMA/8000\r\n" + "a=rtpmap:126 telephone-event/8000\r\n" + "a=sendrecv\r\n" + "a=rtcp:16000 IN IP4 192.168.30.208\r\n" + "a=rtcp-mux\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:tvKIFjbMQ7W0/C2RzhwN0oQglj/7GJg+frdsNRxt\r\n" + "a=ice-ufrag:AI2sRT3r\r\n" + "a=ice-pwd:lByS9z2RSQlSE9XurlvjYmEm\r\n" + "a=ssrc:4227871655 cname:GeAAgb6XCPNLVMX5\r\n" + "a=ssrc:4227871655 msid:1NFAV3iD08ioO2339rQS9pfOI9mDf6GeG9F4 a0\r\n" + "a=ssrc:4227871655 mslabel:1NFAV3iD08ioO2339rQS9pfOI9mDf6GeG9F4\r\n" + "a=ssrc:4227871655 label:1NFAV3iD08ioO2339rQS9pfOI9mDf6GeG9F4a0\r\n" + "a=mid:audio\r\n"; + +static const char kAudioSdpWithUnsupportedCodecs[] = + "v=0\r\n" + "o=- 6858750541 2 IN IP4 192.168.30.208\r\n" + "s=-\r\n" + "c=IN IP4 192.168.30.208\r\n" + "t=0 0\r\n" + "m=audio 16000 RTP/SAVPF 0 8 18 110 126\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:8 PCMA/8000\r\n" + "a=rtpmap:18 WeirdCodec1/8000\r\n" + "a=rtpmap:110 WeirdCodec2/8000\r\n" + "a=rtpmap:126 telephone-event/8000\r\n" + "a=sendonly\r\n" + "a=rtcp:16000 IN IP4 192.168.30.208\r\n" + "a=rtcp-mux\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:tvKIFjbMQ7W0/C2RzhwN0oQglj/7GJg+frdsNRxt\r\n" + "a=ice-ufrag:AI2sRT3r\r\n" + "a=ice-pwd:lByS9z2RSQlSE9XurlvjYmEm\r\n" + "a=ssrc:4227871655 cname:TsmD02HRfhkJBm4m\r\n" + "a=ssrc:4227871655 msid:7nU0TApbB-n4dfPlCplWT9QTEsbBDS1IlpW3 a0\r\n" + "a=ssrc:4227871655 mslabel:7nU0TApbB-n4dfPlCplWT9QTEsbBDS1IlpW3\r\n" + "a=ssrc:4227871655 label:7nU0TApbB-n4dfPlCplWT9QTEsbBDS1IlpW3a0\r\n" + "a=mid:audio\r\n"; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_TEST_TESTSDPSTRINGS_H_ diff --git a/talk/app/webrtc/videosourceinterface.h b/talk/app/webrtc/videosourceinterface.h new file mode 100644 index 000000000..ae968f728 --- /dev/null +++ b/talk/app/webrtc/videosourceinterface.h @@ -0,0 +1,57 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_VIDEOSOURCEINTERFACE_H_ +#define TALK_APP_WEBRTC_VIDEOSOURCEINTERFACE_H_ + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/media/base/mediachannel.h" + +namespace webrtc { + +// VideoSourceInterface is a reference counted source used for VideoTracks. +// The same source can be used in multiple VideoTracks. +// The methods are only supposed to be called by the PeerConnection +// implementation. +class VideoSourceInterface : public MediaSourceInterface { + public: + // Get access to the source implementation of cricket::VideoCapturer. + // This can be used for receiving frames and state notifications. + // But it should not be used for starting or stopping capturing. + virtual cricket::VideoCapturer* GetVideoCapturer() = 0; + // Adds |output| to the source to receive frames. + virtual void AddSink(cricket::VideoRenderer* output) = 0; + virtual void RemoveSink(cricket::VideoRenderer* output) = 0; + virtual const cricket::VideoOptions* options() const = 0; + + protected: + virtual ~VideoSourceInterface() {} +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_VIDEOSOURCEINTERFACE_H_ diff --git a/talk/app/webrtc/videosourceproxy.h b/talk/app/webrtc/videosourceproxy.h new file mode 100644 index 000000000..be800777c --- /dev/null +++ b/talk/app/webrtc/videosourceproxy.h @@ -0,0 +1,51 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_VIDEOSOURCEPROXY_H_ +#define TALK_APP_WEBRTC_VIDEOSOURCEPROXY_H_ + +#include "talk/app/webrtc/proxy.h" +#include "talk/app/webrtc/videosourceinterface.h" + +namespace webrtc { + +// VideoSourceProxy makes sure the real VideoSourceInterface implementation is +// destroyed on the signaling thread and marshals all method calls to the +// signaling thread. +BEGIN_PROXY_MAP(VideoSource) + PROXY_CONSTMETHOD0(SourceState, state) + PROXY_METHOD0(cricket::VideoCapturer*, GetVideoCapturer) + PROXY_METHOD1(void, AddSink, cricket::VideoRenderer*) + PROXY_METHOD1(void, RemoveSink, cricket::VideoRenderer*) + PROXY_CONSTMETHOD0(const cricket::VideoOptions*, options) + PROXY_METHOD1(void, RegisterObserver, ObserverInterface*) + PROXY_METHOD1(void, UnregisterObserver, ObserverInterface*) +END_PROXY() + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_VIDEOSOURCEPROXY_H_ diff --git a/talk/app/webrtc/videotrack.cc b/talk/app/webrtc/videotrack.cc new file mode 100644 index 000000000..ec17ec7e7 --- /dev/null +++ b/talk/app/webrtc/videotrack.cc @@ -0,0 +1,78 @@ +/* + * libjingle + * Copyright 2011, 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/app/webrtc/videotrack.h" + +#include + +#include "talk/media/webrtc/webrtcvideocapturer.h" + +namespace webrtc { + +static const char kVideoTrackKind[] = "video"; + +VideoTrack::VideoTrack(const std::string& label, + VideoSourceInterface* video_source) + : MediaStreamTrack(label), + video_source_(video_source) { + if (video_source_) + video_source_->AddSink(FrameInput()); +} + +VideoTrack::~VideoTrack() { + if (video_source_) + video_source_->RemoveSink(FrameInput()); +} + +std::string VideoTrack::kind() const { + return kVideoTrackKind; +} + +void VideoTrack::AddRenderer(VideoRendererInterface* renderer) { + renderers_.AddRenderer(renderer); +} + +void VideoTrack::RemoveRenderer(VideoRendererInterface* renderer) { + renderers_.RemoveRenderer(renderer); +} + +cricket::VideoRenderer* VideoTrack::FrameInput() { + return &renderers_; +} + +bool VideoTrack::set_enabled(bool enable) { + renderers_.SetEnabled(enable); + return MediaStreamTrack::set_enabled(enable); +} + +talk_base::scoped_refptr VideoTrack::Create( + const std::string& id, VideoSourceInterface* source) { + talk_base::RefCountedObject* track = + new talk_base::RefCountedObject(id, source); + return track; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/videotrack.h b/talk/app/webrtc/videotrack.h new file mode 100644 index 000000000..aefeb502c --- /dev/null +++ b/talk/app/webrtc/videotrack.h @@ -0,0 +1,65 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_VIDEOTRACK_H_ +#define TALK_APP_WEBRTC_VIDEOTRACK_H_ + +#include + +#include "talk/app/webrtc/mediastreamtrack.h" +#include "talk/app/webrtc/videosourceinterface.h" +#include "talk/app/webrtc/videotrackrenderers.h" +#include "talk/base/scoped_ref_ptr.h" + +namespace webrtc { + +class VideoTrack : public MediaStreamTrack { + public: + static talk_base::scoped_refptr Create( + const std::string& label, VideoSourceInterface* source); + + virtual void AddRenderer(VideoRendererInterface* renderer); + virtual void RemoveRenderer(VideoRendererInterface* renderer); + virtual cricket::VideoRenderer* FrameInput(); + virtual VideoSourceInterface* GetSource() const { + return video_source_.get(); + } + virtual bool set_enabled(bool enable); + virtual std::string kind() const; + + protected: + VideoTrack(const std::string& id, VideoSourceInterface* video_source); + ~VideoTrack(); + + private: + VideoTrackRenderers renderers_; + talk_base::scoped_refptr video_source_; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_VIDEOTRACK_H_ diff --git a/talk/app/webrtc/videotrack_unittest.cc b/talk/app/webrtc/videotrack_unittest.cc new file mode 100644 index 000000000..671e360f2 --- /dev/null +++ b/talk/app/webrtc/videotrack_unittest.cc @@ -0,0 +1,84 @@ +/* + * libjingle + * Copyright 2012, 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 + +#include "talk/app/webrtc/test/fakevideotrackrenderer.h" +#include "talk/app/webrtc/videotrack.h" +#include "talk/base/gunit.h" +#include "talk/base/scoped_ptr.h" +#include "talk/media/webrtc/webrtcvideoframe.h" + +using webrtc::FakeVideoTrackRenderer; +using webrtc::VideoTrack; +using webrtc::VideoTrackInterface; + +// Test adding renderers to a video track and render to them by providing +// VideoFrames to the track frame input. +TEST(VideoTrack, RenderVideo) { + static const char kVideoTrackId[] = "track_id"; + talk_base::scoped_refptr video_track( + VideoTrack::Create(kVideoTrackId, NULL)); + // FakeVideoTrackRenderer register itself to |video_track| + talk_base::scoped_ptr renderer_1( + new FakeVideoTrackRenderer(video_track.get())); + + cricket::VideoRenderer* render_input = video_track->FrameInput(); + ASSERT_FALSE(render_input == NULL); + + render_input->SetSize(123, 123, 0); + EXPECT_EQ(1, renderer_1->num_set_sizes()); + EXPECT_EQ(123, renderer_1->width()); + EXPECT_EQ(123, renderer_1->height()); + + cricket::WebRtcVideoFrame frame; + frame.InitToBlack(123, 123, 1, 1, 0, 0); + render_input->RenderFrame(&frame); + EXPECT_EQ(1, renderer_1->num_rendered_frames()); + + // FakeVideoTrackRenderer register itself to |video_track| + talk_base::scoped_ptr renderer_2( + new FakeVideoTrackRenderer(video_track.get())); + + render_input->RenderFrame(&frame); + + EXPECT_EQ(1, renderer_1->num_set_sizes()); + EXPECT_EQ(123, renderer_1->width()); + EXPECT_EQ(123, renderer_1->height()); + EXPECT_EQ(1, renderer_2->num_set_sizes()); + EXPECT_EQ(123, renderer_2->width()); + EXPECT_EQ(123, renderer_2->height()); + + EXPECT_EQ(2, renderer_1->num_rendered_frames()); + EXPECT_EQ(1, renderer_2->num_rendered_frames()); + + video_track->RemoveRenderer(renderer_1.get()); + render_input->RenderFrame(&frame); + + EXPECT_EQ(2, renderer_1->num_rendered_frames()); + EXPECT_EQ(2, renderer_2->num_rendered_frames()); +} diff --git a/talk/app/webrtc/videotrackrenderers.cc b/talk/app/webrtc/videotrackrenderers.cc new file mode 100644 index 000000000..b0e0c1f54 --- /dev/null +++ b/talk/app/webrtc/videotrackrenderers.cc @@ -0,0 +1,94 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/videotrackrenderers.h" + +namespace webrtc { + +VideoTrackRenderers::VideoTrackRenderers() + : width_(0), + height_(0), + enabled_(true) { +} + +VideoTrackRenderers::~VideoTrackRenderers() { +} + +void VideoTrackRenderers::AddRenderer(VideoRendererInterface* renderer) { + talk_base::CritScope cs(&critical_section_); + std::vector::iterator it = renderers_.begin(); + for (; it != renderers_.end(); ++it) { + if (it->renderer_ == renderer) + return; + } + renderers_.push_back(RenderObserver(renderer)); +} + +void VideoTrackRenderers::RemoveRenderer(VideoRendererInterface* renderer) { + talk_base::CritScope cs(&critical_section_); + std::vector::iterator it = renderers_.begin(); + for (; it != renderers_.end(); ++it) { + if (it->renderer_ == renderer) { + renderers_.erase(it); + return; + } + } +} + +void VideoTrackRenderers::SetEnabled(bool enable) { + talk_base::CritScope cs(&critical_section_); + enabled_ = enable; +} + +bool VideoTrackRenderers::SetSize(int width, int height, int reserved) { + talk_base::CritScope cs(&critical_section_); + width_ = width; + height_ = height; + std::vector::iterator it = renderers_.begin(); + for (; it != renderers_.end(); ++it) { + it->renderer_->SetSize(width, height); + it->size_set_ = true; + } + return true; +} + +bool VideoTrackRenderers::RenderFrame(const cricket::VideoFrame* frame) { + talk_base::CritScope cs(&critical_section_); + if (!enabled_) { + return true; + } + std::vector::iterator it = renderers_.begin(); + for (; it != renderers_.end(); ++it) { + if (!it->size_set_) { + it->renderer_->SetSize(width_, height_); + it->size_set_ = true; + } + it->renderer_->RenderFrame(frame); + } + return true; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/videotrackrenderers.h b/talk/app/webrtc/videotrackrenderers.h new file mode 100644 index 000000000..4bcf6a3a1 --- /dev/null +++ b/talk/app/webrtc/videotrackrenderers.h @@ -0,0 +1,77 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_VIDEOTRACKRENDERERS_H_ +#define TALK_APP_WEBRTC_VIDEOTRACKRENDERERS_H_ + +#include + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/base/criticalsection.h" +#include "talk/media/base/videorenderer.h" + +namespace webrtc { + +// Class used for rendering cricket::VideoFrames to multiple renderers of type +// VideoRendererInterface. +// Each VideoTrack owns a VideoTrackRenderers instance. +// The class is thread safe. Rendering to the added VideoRendererInterfaces is +// done on the same thread as the cricket::VideoRenderer. +class VideoTrackRenderers : public cricket::VideoRenderer { + public: + VideoTrackRenderers(); + ~VideoTrackRenderers(); + + // Implements cricket::VideoRenderer + virtual bool SetSize(int width, int height, int reserved); + virtual bool RenderFrame(const cricket::VideoFrame* frame); + + void AddRenderer(VideoRendererInterface* renderer); + void RemoveRenderer(VideoRendererInterface* renderer); + void SetEnabled(bool enable); + + private: + struct RenderObserver { + explicit RenderObserver(VideoRendererInterface* renderer) + : renderer_(renderer), + size_set_(false) { + } + VideoRendererInterface* renderer_; + bool size_set_; + }; + + int width_; + int height_; + bool enabled_; + std::vector renderers_; + + talk_base::CriticalSection critical_section_; // Protects the above variables +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_VIDEOTRACKRENDERERS_H_ diff --git a/talk/app/webrtc/webrtc.scons b/talk/app/webrtc/webrtc.scons new file mode 100644 index 000000000..0cbe756c3 --- /dev/null +++ b/talk/app/webrtc/webrtc.scons @@ -0,0 +1,88 @@ +# -*- Python -*- +import talk + +Import('env') + +# For peerconnection, we need additional flags only for GCC 4.6+. +peerconnection_lin_ccflags = [] + +if env.Bit('linux'): + # Detect the GCC version and update peerconnection flags. + (major, minor, rev) = env.GetGccVersion() + if major > 4 or (major == 4 and minor >= 6): + peerconnection_lin_ccflags = ['-Wno-error=unused-but-set-variable'] + + +if env.Bit('have_webrtc_voice') and env.Bit('have_webrtc_video'): + # local sources + talk.Library( + env, + name = 'peerconnection', + srcs = [ + 'audiotrack.cc', + 'jsepicecandidate.cc', + 'jsepsessiondescription.cc', + 'mediaconstraintsinterface.cc', + 'mediastream.cc', + 'mediastreamhandler.cc', + 'mediastreamproxy.cc', + 'mediastreamsignaling.cc', + 'mediastreamtrackproxy.cc', + 'peerconnectionfactory.cc', + 'peerconnection.cc', + 'portallocatorfactory.cc', + 'roapmessages.cc', + 'roapsession.cc', + 'roapsignaling.cc', + 'videorendererimpl.cc', + 'videotrack.cc', + 'webrtcsdp.cc', + 'webrtcsession.cc', + ], + lin_ccflags = peerconnection_lin_ccflags + ) + + talk.Unittest( + env, + name = 'peerconnection', + srcs = [ + 'test/fakeaudiocapturemodule.cc', + 'test/fakeaudiocapturemodule_unittest.cc', + 'test/fakevideocapturemodule.cc', + 'test/fileframesource.cc', + 'test/i420framesource.cc', + 'test/staticframesource.cc', + 'jsepsessiondescription_unittest.cc', + 'mediastream_unittest.cc', + 'mediastreamhandler_unittest.cc', + 'mediastreamsignaling_unittest.cc', + 'peerconnectioninterface_unittest.cc', + 'peerconnection_unittest.cc', + 'peerconnectionfactory_unittest.cc', + 'roapmessages_unittest.cc', + 'roapsession_unittest.cc', + 'roapsignaling_unittest.cc', + 'webrtcsdp_unittest.cc', + 'webrtcsession_unittest.cc', + ], + libs = [ + 'base', + 'expat', + 'json', + 'p2p', + 'phone', + 'srtp', + 'xmllite', + 'xmpp', + 'yuvscaler', + 'peerconnection', + ], + win_link_flags = [('', '/nodefaultlib:libcmt')[env.Bit('debug')]], + lin_libs = [ + 'sound', + ], + mac_libs = [ + 'crypto', + 'ssl', + ], + ) diff --git a/talk/app/webrtc/webrtcsdp.cc b/talk/app/webrtc/webrtcsdp.cc new file mode 100644 index 000000000..f91db8d50 --- /dev/null +++ b/talk/app/webrtc/webrtcsdp.cc @@ -0,0 +1,2885 @@ +/* + * libjingle + * Copyright 2011, 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/app/webrtc/webrtcsdp.h" + +#include +#include +#include +#include +#include + +#include "talk/app/webrtc/jsepicecandidate.h" +#include "talk/app/webrtc/jsepsessiondescription.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/messagedigest.h" +#include "talk/base/stringutils.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/cryptoparams.h" +#include "talk/p2p/base/candidate.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/port.h" +#include "talk/session/media/mediasession.h" +#include "talk/session/media/mediasessionclient.h" + +using cricket::AudioContentDescription; +using cricket::Candidate; +using cricket::Candidates; +using cricket::ContentDescription; +using cricket::ContentInfo; +using cricket::CryptoParams; +using cricket::DataContentDescription; +using cricket::ICE_CANDIDATE_COMPONENT_RTP; +using cricket::ICE_CANDIDATE_COMPONENT_RTCP; +using cricket::kCodecParamMaxBitrate; +using cricket::kCodecParamMaxPTime; +using cricket::kCodecParamMaxQuantization; +using cricket::kCodecParamMinBitrate; +using cricket::kCodecParamMinPTime; +using cricket::kCodecParamPTime; +using cricket::kCodecParamSPropStereo; +using cricket::kCodecParamStereo; +using cricket::kCodecParamUseInbandFec; +using cricket::kCodecParamSctpProtocol; +using cricket::kCodecParamSctpStreams; +using cricket::kWildcardPayloadType; +using cricket::MediaContentDescription; +using cricket::MediaType; +using cricket::NS_JINGLE_ICE_UDP; +using cricket::RtpHeaderExtension; +using cricket::SsrcGroup; +using cricket::StreamParams; +using cricket::StreamParamsVec; +using cricket::TransportDescription; +using cricket::TransportInfo; +using cricket::VideoContentDescription; +using talk_base::SocketAddress; + +typedef std::vector RtpHeaderExtensions; + +namespace cricket { +class SessionDescription; +} + +namespace webrtc { + +// Line type +// RFC 4566 +// An SDP session description consists of a number of lines of text of +// the form: +// = +// where MUST be exactly one case-significant character. +static const int kLinePrefixLength = 2; // Lenght of = +static const char kLineTypeVersion = 'v'; +static const char kLineTypeOrigin = 'o'; +static const char kLineTypeSessionName = 's'; +static const char kLineTypeSessionInfo = 'i'; +static const char kLineTypeSessionUri = 'u'; +static const char kLineTypeSessionEmail = 'e'; +static const char kLineTypeSessionPhone = 'p'; +static const char kLineTypeSessionBandwidth = 'b'; +static const char kLineTypeTiming = 't'; +static const char kLineTypeRepeatTimes = 'r'; +static const char kLineTypeTimeZone = 'z'; +static const char kLineTypeEncryptionKey = 'k'; +static const char kLineTypeMedia = 'm'; +static const char kLineTypeConnection = 'c'; +static const char kLineTypeAttributes = 'a'; + +// Attributes +static const char kAttributeGroup[] = "group"; +static const char kAttributeMid[] = "mid"; +static const char kAttributeRtcpMux[] = "rtcp-mux"; +static const char kAttributeSsrc[] = "ssrc"; +static const char kSsrcAttributeCname[] = "cname"; +static const char kAttributeExtmap[] = "extmap"; +// draft-alvestrand-mmusic-msid-01 +// a=msid-semantic: WMS +static const char kAttributeMsidSemantics[] = "msid-semantic"; +static const char kMediaStreamSemantic[] = "WMS"; +static const char kSsrcAttributeMsid[] = "msid"; +static const char kDefaultMsid[] = "default"; +static const char kMsidAppdataAudio[] = "a"; +static const char kMsidAppdataVideo[] = "v"; +static const char kMsidAppdataData[] = "d"; +static const char kSsrcAttributeMslabel[] = "mslabel"; +static const char kSSrcAttributeLabel[] = "label"; +static const char kAttributeSsrcGroup[] = "ssrc-group"; +static const char kAttributeCrypto[] = "crypto"; +static const char kAttributeCandidate[] = "candidate"; +static const char kAttributeCandidateTyp[] = "typ"; +static const char kAttributeCandidateRaddr[] = "raddr"; +static const char kAttributeCandidateRport[] = "rport"; +static const char kAttributeCandidateUsername[] = "username"; +static const char kAttributeCandidatePassword[] = "password"; +static const char kAttributeCandidateGeneration[] = "generation"; +static const char kAttributeFingerprint[] = "fingerprint"; +static const char kAttributeFmtp[] = "fmtp"; +static const char kAttributeRtpmap[] = "rtpmap"; +static const char kAttributeRtcp[] = "rtcp"; +static const char kAttributeIceUfrag[] = "ice-ufrag"; +static const char kAttributeIcePwd[] = "ice-pwd"; +static const char kAttributeIceLite[] = "ice-lite"; +static const char kAttributeIceOption[] = "ice-options"; +static const char kAttributeSendOnly[] = "sendonly"; +static const char kAttributeRecvOnly[] = "recvonly"; +static const char kAttributeRtcpFb[] = "rtcp-fb"; +static const char kAttributeSendRecv[] = "sendrecv"; +static const char kAttributeInactive[] = "inactive"; + +// Experimental flags +static const char kAttributeXGoogleFlag[] = "x-google-flag"; +static const char kValueConference[] = "conference"; +static const char kAttributeXGoogleBufferLatency[] = + "x-google-buffer-latency"; + +// Candidate +static const char kCandidateHost[] = "host"; +static const char kCandidateSrflx[] = "srflx"; +// TODO: How to map the prflx with circket candidate type +// static const char kCandidatePrflx[] = "prflx"; +static const char kCandidateRelay[] = "relay"; + +static const char kSdpDelimiterEqual = '='; +static const char kSdpDelimiterSpace = ' '; +static const char kSdpDelimiterColon = ':'; +static const char kSdpDelimiterSemicolon = ';'; +static const char kSdpDelimiterSlash = '/'; +static const char kNewLine = '\n'; +static const char kReturn = '\r'; +static const char kLineBreak[] = "\r\n"; + +// TODO: Generate the Session and Time description +// instead of hardcoding. +static const char kSessionVersion[] = "v=0"; +// RFC 4566 +static const char kSessionOriginUsername[] = "-"; +static const char kSessionOriginSessionId[] = "0"; +static const char kSessionOriginSessionVersion[] = "0"; +static const char kSessionOriginNettype[] = "IN"; +static const char kSessionOriginAddrtype[] = "IP4"; +static const char kSessionOriginAddress[] = "127.0.0.1"; +static const char kSessionName[] = "s=-"; +static const char kTimeDescription[] = "t=0 0"; +static const char kAttrGroup[] = "a=group:BUNDLE"; +static const char kConnectionNettype[] = "IN"; +static const char kConnectionAddrtype[] = "IP4"; +static const char kMediaTypeVideo[] = "video"; +static const char kMediaTypeAudio[] = "audio"; +static const char kMediaTypeData[] = "application"; +static const char kMediaPortRejected[] = "0"; +static const char kDefaultAddress[] = "0.0.0.0"; +static const char kDefaultPort[] = "1"; +// RFC 3556 +static const char kApplicationSpecificMaximum[] = "AS"; + +static const int kDefaultVideoClockrate = 90000; + +// ISAC special-case. +static const char kIsacCodecName[] = "ISAC"; // From webrtcvoiceengine.cc +static const int kIsacWbDefaultRate = 32000; // From acm_common_defs.h +static const int kIsacSwbDefaultRate = 56000; // From acm_common_defs.h + +static const int kDefaultSctpFmt = 5000; +static const char kDefaultSctpFmtProtocol[] = "webrtc-datachannel"; + +struct SsrcInfo { + SsrcInfo() + : msid_identifier(kDefaultMsid), + // TODO(ronghuawu): What should we do if the appdata doesn't appear? + // Create random string (which will be used as track label later)? + msid_appdata(talk_base::CreateRandomString(8)) { + } + uint32 ssrc_id; + std::string cname; + std::string msid_identifier; + std::string msid_appdata; + + // For backward compatibility. + // TODO(ronghuawu): Remove below 2 fields once all the clients support msid. + std::string label; + std::string mslabel; +}; +typedef std::vector SsrcInfoVec; +typedef std::vector SsrcGroupVec; + +// Serializes the passed in SessionDescription to a SDP string. +// desc - The SessionDescription object to be serialized. +static std::string SdpSerializeSessionDescription( + const JsepSessionDescription& jdesc); +template +static void AddFmtpLine(const T& codec, std::string* message); +static void BuildMediaDescription(const ContentInfo* content_info, + const TransportInfo* transport_info, + const MediaType media_type, + std::string* message); +static void BuildSctpContentAttributes(std::string* message); +static void BuildRtpContentAttributes( + const MediaContentDescription* media_desc, + const MediaType media_type, + std::string* message); +static void BuildRtpMap(const MediaContentDescription* media_desc, + const MediaType media_type, + std::string* message); +static void BuildCandidate(const std::vector& candidates, + std::string* message); +static void BuildIceOptions(const std::vector& transport_options, + std::string* message); + +static bool ParseSessionDescription(const std::string& message, size_t* pos, + std::string* session_id, + std::string* session_version, + bool* supports_msid, + TransportDescription* session_td, + RtpHeaderExtensions* session_extmaps, + cricket::SessionDescription* desc, + SdpParseError* error); +static bool ParseGroupAttribute(const std::string& line, + cricket::SessionDescription* desc, + SdpParseError* error); +static bool ParseMediaDescription( + const std::string& message, + const TransportDescription& session_td, + const RtpHeaderExtensions& session_extmaps, + bool supports_msid, + size_t* pos, cricket::SessionDescription* desc, + std::vector* candidates, + SdpParseError* error); +static bool ParseContent(const std::string& message, + const MediaType media_type, + int mline_index, + const std::string& protocol, + const std::vector& codec_preference, + size_t* pos, + std::string* content_name, + MediaContentDescription* media_desc, + TransportDescription* transport, + std::vector* candidates, + SdpParseError* error); +static bool ParseSsrcAttribute(const std::string& line, + SsrcInfoVec* ssrc_infos, + SdpParseError* error); +static bool ParseSsrcGroupAttribute(const std::string& line, + SsrcGroupVec* ssrc_groups, + SdpParseError* error); +static bool ParseCryptoAttribute(const std::string& line, + MediaContentDescription* media_desc, + SdpParseError* error); +static bool ParseRtpmapAttribute(const std::string& line, + const MediaType media_type, + const std::vector& codec_preference, + MediaContentDescription* media_desc, + SdpParseError* error); +static bool ParseFmtpAttributes(const std::string& line, + const MediaType media_type, + MediaContentDescription* media_desc, + SdpParseError* error); +static bool ParseFmtpParam(const std::string& line, std::string* parameter, + std::string* value, SdpParseError* error); +static bool ParseCandidate(const std::string& message, Candidate* candidate, + SdpParseError* error, bool is_raw); +static bool ParseRtcpFbAttribute(const std::string& line, + const MediaType media_type, + MediaContentDescription* media_desc, + SdpParseError* error); +static bool ParseIceOptions(const std::string& line, + std::vector* transport_options, + SdpParseError* error); +static bool ParseExtmap(const std::string& line, + RtpHeaderExtension* extmap, + SdpParseError* error); +static bool ParseFingerprintAttribute(const std::string& line, + talk_base::SSLFingerprint** fingerprint, + SdpParseError* error); + +// Helper functions + +// Below ParseFailed*** functions output the line that caused the parsing +// failure and the detailed reason (|description|) of the failure to |error|. +// The functions always return false so that they can be used directly in the +// following way when error happens: +// "return ParseFailed***(...);" + +// The line starting at |line_start| of |message| is the failing line. +// The reason for the failure should be provided in the |description|. +// An example of a description could be "unknown character". +static bool ParseFailed(const std::string& message, + size_t line_start, + const std::string& description, + SdpParseError* error) { + // Get the first line of |message| from |line_start|. + std::string first_line = message; + size_t line_end = message.find(kNewLine, line_start); + if (line_end != std::string::npos) { + if (line_end > 0 && (message.at(line_end - 1) == kReturn)) { + --line_end; + } + first_line = message.substr(line_start, (line_end - line_start)); + } + + if (error) { + error->line = first_line; + error->description = description; + } + LOG(LS_ERROR) << "Failed to parse: \"" << first_line + << "\". Reason: " << description; + return false; +} + +// |line| is the failing line. The reason for the failure should be +// provided in the |description|. +static bool ParseFailed(const std::string& line, + const std::string& description, + SdpParseError* error) { + return ParseFailed(line, 0, description, error); +} + +// Parses failure where the failing SDP line isn't know or there are multiple +// failing lines. +static bool ParseFailed(const std::string& description, + SdpParseError* error) { + return ParseFailed("", description, error); +} + +// |line| is the failing line. The failure is due to the fact that |line| +// doesn't have |expected_fields| fields. +static bool ParseFailedExpectFieldNum(const std::string& line, + int expected_fields, + SdpParseError* error) { + std::ostringstream description; + description << "Expects " << expected_fields << " fields."; + return ParseFailed(line, description.str(), error); +} + +// |line| is the failing line. The failure is due to the fact that |line| has +// less than |expected_min_fields| fields. +static bool ParseFailedExpectMinFieldNum(const std::string& line, + int expected_min_fields, + SdpParseError* error) { + std::ostringstream description; + description << "Expects at least " << expected_min_fields << " fields."; + return ParseFailed(line, description.str(), error); +} + +// |line| is the failing line. The failure is due to the fact that it failed to +// get the value of |attribute|. +static bool ParseFailedGetValue(const std::string& line, + const std::string& attribute, + SdpParseError* error) { + std::ostringstream description; + description << "Failed to get the value of attribute: " << attribute; + return ParseFailed(line, description.str(), error); +} + +// The line starting at |line_start| of |message| is the failing line. The +// failure is due to the line type (e.g. the "m" part of the "m-line") +// not matching what is expected. The expected line type should be +// provided as |line_type|. +static bool ParseFailedExpectLine(const std::string& message, + size_t line_start, + const char line_type, + const std::string& line_value, + SdpParseError* error) { + std::ostringstream description; + description << "Expect line: " << line_type << "=" << line_value; + return ParseFailed(message, line_start, description.str(), error); +} + +static bool AddLine(const std::string& line, std::string* message) { + if (!message) + return false; + + message->append(line); + message->append(kLineBreak); + return true; +} + +static bool GetLine(const std::string& message, + size_t* pos, + std::string* line) { + size_t line_begin = *pos; + size_t line_end = message.find(kNewLine, line_begin); + if (line_end == std::string::npos) { + return false; + } + // Update the new start position + *pos = line_end + 1; + if (line_end > 0 && (message.at(line_end - 1) == kReturn)) { + --line_end; + } + *line = message.substr(line_begin, (line_end - line_begin)); + const char* cline = line->c_str(); + // RFC 4566 + // An SDP session description consists of a number of lines of text of + // the form: + // = + // where MUST be exactly one case-significant character and + // is structured text whose format depends on . + // Whitespace MUST NOT be used on either side of the "=" sign. + if (cline[0] == kSdpDelimiterSpace || + cline[1] != kSdpDelimiterEqual || + cline[2] == kSdpDelimiterSpace) { + *pos = line_begin; + return false; + } + return true; +} + +// Init |os| to "|type|=|value|". +static void InitLine(const char type, + const std::string& value, + std::ostringstream* os) { + os->str(""); + *os << type << kSdpDelimiterEqual << value; +} + +// Init |os| to "a=|attribute|". +static void InitAttrLine(const std::string& attribute, std::ostringstream* os) { + InitLine(kLineTypeAttributes, attribute, os); +} + +// Writes a SDP attribute line based on |attribute| and |value| to |message|. +static void AddAttributeLine(const std::string& attribute, int value, + std::string* message) { + std::ostringstream os; + InitAttrLine(attribute, &os); + os << kSdpDelimiterColon << value; + AddLine(os.str(), message); +} + +// Returns the first line of the message without the line breaker. +static bool GetFirstLine(const std::string& message, std::string* line) { + size_t pos = 0; + if (!GetLine(message, &pos, line)) { + // If GetLine failed, just return the full |message|. + *line = message; + } + return true; +} + +static bool IsLineType(const std::string& message, + const char type, + size_t line_start) { + if (message.size() < line_start + kLinePrefixLength) { + return false; + } + const char* cmessage = message.c_str(); + return (cmessage[line_start] == type && + cmessage[line_start + 1] == kSdpDelimiterEqual); +} + +static bool IsLineType(const std::string& line, + const char type) { + return IsLineType(line, type, 0); +} + +static bool GetLineWithType(const std::string& message, size_t* pos, + std::string* line, const char type) { + if (!IsLineType(message, type, *pos)) { + return false; + } + + if (!GetLine(message, pos, line)) + return false; + + return true; +} + +static bool HasAttribute(const std::string& line, + const std::string& attribute) { + return (line.compare(kLinePrefixLength, attribute.size(), attribute) == 0); +} + +// Verifies the candiate to be of the format candidate: +static bool IsRawCandidate(const std::string& line) { + // Checking candiadte-attribute is starting with "candidate" str. + if (line.compare(0, strlen(kAttributeCandidate), kAttributeCandidate) != 0) { + return false; + } + const size_t first_candidate = line.find(kSdpDelimiterColon); + if (first_candidate == std::string::npos) + return false; + // In this format we only expecting one candiate. If any additional + // candidates present, whole string will be discared. + const size_t any_other = line.find(kSdpDelimiterColon, first_candidate + 1); + return (any_other == std::string::npos); +} + +static bool AddSsrcLine(uint32 ssrc_id, const std::string& attribute, + const std::string& value, std::string* message) { + // RFC 5576 + // a=ssrc: : + std::ostringstream os; + InitAttrLine(kAttributeSsrc, &os); + os << kSdpDelimiterColon << ssrc_id << kSdpDelimiterSpace + << attribute << kSdpDelimiterColon << value; + return AddLine(os.str(), message); +} + +// Split the message into two parts by the first delimiter. +static bool SplitByDelimiter(const std::string& message, + const char delimiter, + std::string* field1, + std::string* field2) { + // Find the first delimiter + size_t pos = message.find(delimiter); + if (pos == std::string::npos) { + return false; + } + *field1 = message.substr(0, pos); + // The rest is the value. + *field2 = message.substr(pos + 1); + return true; +} + +// Get value only from :. +static bool GetValue(const std::string& message, const std::string& attribute, + std::string* value, SdpParseError* error) { + std::string leftpart; + if (!SplitByDelimiter(message, kSdpDelimiterColon, &leftpart, value)) { + return ParseFailedGetValue(message, attribute, error); + } + // The left part should end with the expected attribute. + if (leftpart.length() < attribute.length() || + leftpart.compare(leftpart.length() - attribute.length(), + attribute.length(), attribute) != 0) { + return ParseFailedGetValue(message, attribute, error); + } + return true; +} + +static bool CaseInsensitiveFind(std::string str1, std::string str2) { + std::transform(str1.begin(), str1.end(), str1.begin(), + ::tolower); + std::transform(str2.begin(), str2.end(), str2.begin(), + ::tolower); + return str1.find(str2) != std::string::npos; +} + +void CreateTracksFromSsrcInfos(const SsrcInfoVec& ssrc_infos, + StreamParamsVec* tracks) { + ASSERT(tracks != NULL); + for (SsrcInfoVec::const_iterator ssrc_info = ssrc_infos.begin(); + ssrc_info != ssrc_infos.end(); ++ssrc_info) { + if (ssrc_info->cname.empty()) { + continue; + } + + std::string sync_label; + std::string track_id; + if (ssrc_info->msid_identifier == kDefaultMsid && + !ssrc_info->mslabel.empty()) { + // If there's no msid and there's mslabel, we consider this is a sdp from + // a older version of client that doesn't support msid. + // In that case, we use the mslabel and label to construct the track. + sync_label = ssrc_info->mslabel; + track_id = ssrc_info->label; + } else { + sync_label = ssrc_info->msid_identifier; + // The appdata consists of the "id" attribute of a MediaStreamTrack, which + // is corresponding to the "id" attribute of StreamParams. + track_id = ssrc_info->msid_appdata; + } + if (sync_label.empty() || track_id.empty()) { + ASSERT(false); + continue; + } + + StreamParamsVec::iterator track = tracks->begin(); + for (; track != tracks->end(); ++track) { + if (track->id == track_id) { + break; + } + } + if (track == tracks->end()) { + // If we don't find an existing track, create a new one. + tracks->push_back(StreamParams()); + track = tracks->end() - 1; + } + track->add_ssrc(ssrc_info->ssrc_id); + track->cname = ssrc_info->cname; + track->sync_label = sync_label; + track->id = track_id; + } +} + +void GetMediaStreamLabels(const ContentInfo* content, + std::set* labels) { + const MediaContentDescription* media_desc = + static_cast ( + content->description); + const cricket::StreamParamsVec& streams = media_desc->streams(); + for (cricket::StreamParamsVec::const_iterator it = streams.begin(); + it != streams.end(); ++it) { + labels->insert(it->sync_label); + } +} + +// RFC 5245 +// It is RECOMMENDED that default candidates be chosen based on the +// likelihood of those candidates to work with the peer that is being +// contacted. It is RECOMMENDED that relayed > reflexive > host. +static const int kPreferenceUnknown = 0; +static const int kPreferenceHost = 1; +static const int kPreferenceReflexive = 2; +static const int kPreferenceRelayed = 3; + +static int GetCandidatePreferenceFromType(const std::string& type) { + int preference = kPreferenceUnknown; + if (type == cricket::LOCAL_PORT_TYPE) { + preference = kPreferenceHost; + } else if (type == cricket::STUN_PORT_TYPE) { + preference = kPreferenceReflexive; + } else if (type == cricket::RELAY_PORT_TYPE) { + preference = kPreferenceRelayed; + } else { + ASSERT(false); + } + return preference; +} + +// Get ip and port of the default destination from the |candidates| with +// the given value of |component_id|. +// RFC 5245 +// The value of |component_id| currently supported are 1 (RTP) and 2 (RTCP). +// TODO: Decide the default destination in webrtcsession and +// pass it down via SessionDescription. +static bool GetDefaultDestination(const std::vector& candidates, + int component_id, std::string* port, std::string* ip) { + *port = kDefaultPort; + *ip = kDefaultAddress; + int current_preference = kPreferenceUnknown; + for (std::vector::const_iterator it = candidates.begin(); + it != candidates.end(); ++it) { + if (it->component() != component_id) { + continue; + } + const int preference = GetCandidatePreferenceFromType(it->type()); + // See if this candidate is more preferable then the current one. + if (preference <= current_preference) { + continue; + } + current_preference = preference; + *port = it->address().PortAsString(); + *ip = it->address().ipaddr().ToString(); + } + return true; +} + +// Update the media default destination. +static void UpdateMediaDefaultDestination( + const std::vector& candidates, std::string* mline) { + // RFC 4566 + // m= ... + std::vector fields; + talk_base::split(*mline, kSdpDelimiterSpace, &fields); + if (fields.size() < 3) { + return; + } + + bool is_rtp = + fields[2].empty() || + talk_base::starts_with(fields[2].data(), + cricket::kMediaProtocolRtpPrefix); + + std::ostringstream os; + std::string rtp_port, rtp_ip; + if (GetDefaultDestination(candidates, ICE_CANDIDATE_COMPONENT_RTP, + &rtp_port, &rtp_ip)) { + // Found default RTP candidate. + // RFC 5245 + // The default candidates are added to the SDP as the default + // destination for media. For streams based on RTP, this is done by + // placing the IP address and port of the RTP candidate into the c and m + // lines, respectively. + + // Update the port in the m line. + // If this is a m-line with port equal to 0, we don't change it. + if (fields[1] != kMediaPortRejected) { + mline->replace(fields[0].size() + 1, + fields[1].size(), + rtp_port); + } + // Add the c line. + // RFC 4566 + // c= + InitLine(kLineTypeConnection, kConnectionNettype, &os); + os << " " << kConnectionAddrtype << " " << rtp_ip; + AddLine(os.str(), mline); + } + + if (is_rtp) { + std::string rtcp_port, rtcp_ip; + if (GetDefaultDestination(candidates, ICE_CANDIDATE_COMPONENT_RTCP, + &rtcp_port, &rtcp_ip)) { + // Found default RTCP candidate. + // RFC 5245 + // If the agent is utilizing RTCP, it MUST encode the RTCP candidate + // using the a=rtcp attribute as defined in RFC 3605. + + // RFC 3605 + // rtcp-attribute = "a=rtcp:" port [nettype space addrtype space + // connection-address] CRLF + InitAttrLine(kAttributeRtcp, &os); + os << kSdpDelimiterColon + << rtcp_port << " " + << kConnectionNettype << " " + << kConnectionAddrtype << " " + << rtcp_ip; + AddLine(os.str(), mline); + } + } +} + +// Get candidates according to the mline index from SessionDescriptionInterface. +static void GetCandidatesByMindex(const SessionDescriptionInterface& desci, + int mline_index, + std::vector* candidates) { + if (!candidates) { + return; + } + const IceCandidateCollection* cc = desci.candidates(mline_index); + for (size_t i = 0; i < cc->count(); ++i) { + const IceCandidateInterface* candidate = cc->at(i); + candidates->push_back(candidate->candidate()); + } +} + +std::string SdpSerialize(const JsepSessionDescription& jdesc) { + std::string sdp = SdpSerializeSessionDescription(jdesc); + + std::string sdp_with_candidates; + size_t pos = 0; + std::string line; + int mline_index = -1; + while (GetLine(sdp, &pos, &line)) { + if (IsLineType(line, kLineTypeMedia)) { + ++mline_index; + std::vector candidates; + GetCandidatesByMindex(jdesc, mline_index, &candidates); + // Media line may append other lines inside the + // UpdateMediaDefaultDestination call, so add the kLineBreak here first. + line.append(kLineBreak); + UpdateMediaDefaultDestination(candidates, &line); + sdp_with_candidates.append(line); + // Build the a=candidate lines. + BuildCandidate(candidates, &sdp_with_candidates); + } else { + // Copy old line to new sdp without change. + AddLine(line, &sdp_with_candidates); + } + } + sdp = sdp_with_candidates; + + return sdp; +} + +std::string SdpSerializeSessionDescription( + const JsepSessionDescription& jdesc) { + const cricket::SessionDescription* desc = jdesc.description(); + if (!desc) { + return ""; + } + + std::string message; + + // Session Description. + AddLine(kSessionVersion, &message); + // Session Origin + // RFC 4566 + // o= + // + std::ostringstream os; + InitLine(kLineTypeOrigin, kSessionOriginUsername, &os); + const std::string session_id = jdesc.session_id().empty() ? + kSessionOriginSessionId : jdesc.session_id(); + const std::string session_version = jdesc.session_version().empty() ? + kSessionOriginSessionVersion : jdesc.session_version(); + os << " " << session_id << " " << session_version << " " + << kSessionOriginNettype << " " << kSessionOriginAddrtype << " " + << kSessionOriginAddress; + AddLine(os.str(), &message); + AddLine(kSessionName, &message); + + // Time Description. + AddLine(kTimeDescription, &message); + + // Group + if (desc->HasGroup(cricket::GROUP_TYPE_BUNDLE)) { + std::string group_line = kAttrGroup; + const cricket::ContentGroup* group = + desc->GetGroupByName(cricket::GROUP_TYPE_BUNDLE); + ASSERT(group != NULL); + const cricket::ContentNames& content_names = group->content_names(); + for (cricket::ContentNames::const_iterator it = content_names.begin(); + it != content_names.end(); ++it) { + group_line.append(" "); + group_line.append(*it); + } + AddLine(group_line, &message); + } + + // MediaStream semantics + InitAttrLine(kAttributeMsidSemantics, &os); + os << kSdpDelimiterColon << " " << kMediaStreamSemantic; + std::set media_stream_labels; + const ContentInfo* audio_content = GetFirstAudioContent(desc); + if (audio_content) + GetMediaStreamLabels(audio_content, &media_stream_labels); + const ContentInfo* video_content = GetFirstVideoContent(desc); + if (video_content) + GetMediaStreamLabels(video_content, &media_stream_labels); + for (std::set::const_iterator it = + media_stream_labels.begin(); it != media_stream_labels.end(); ++it) { + os << " " << *it; + } + AddLine(os.str(), &message); + + if (audio_content) { + BuildMediaDescription(audio_content, + desc->GetTransportInfoByName(audio_content->name), + cricket::MEDIA_TYPE_AUDIO, &message); + } + + + if (video_content) { + BuildMediaDescription(video_content, + desc->GetTransportInfoByName(video_content->name), + cricket::MEDIA_TYPE_VIDEO, &message); + } + + const ContentInfo* data_content = GetFirstDataContent(desc); + if (data_content) { + BuildMediaDescription(data_content, + desc->GetTransportInfoByName(data_content->name), + cricket::MEDIA_TYPE_DATA, &message); + } + + + return message; +} + +// Serializes the passed in IceCandidateInterface to a SDP string. +// candidate - The candidate to be serialized. +std::string SdpSerializeCandidate( + const IceCandidateInterface& candidate) { + std::string message; + std::vector candidates; + candidates.push_back(candidate.candidate()); + BuildCandidate(candidates, &message); + return message; +} + +bool SdpDeserialize(const std::string& message, + JsepSessionDescription* jdesc, + SdpParseError* error) { + std::string session_id; + std::string session_version; + TransportDescription session_td(NS_JINGLE_ICE_UDP, Candidates()); + RtpHeaderExtensions session_extmaps; + cricket::SessionDescription* desc = new cricket::SessionDescription(); + std::vector candidates; + size_t current_pos = 0; + bool supports_msid = false; + + // Session Description + if (!ParseSessionDescription(message, ¤t_pos, &session_id, + &session_version, &supports_msid, &session_td, + &session_extmaps, desc, error)) { + delete desc; + return false; + } + + // Media Description + if (!ParseMediaDescription(message, session_td, session_extmaps, + supports_msid, ¤t_pos, desc, &candidates, + error)) { + delete desc; + for (std::vector::const_iterator + it = candidates.begin(); it != candidates.end(); ++it) { + delete *it; + } + return false; + } + + jdesc->Initialize(desc, session_id, session_version); + + for (std::vector::const_iterator + it = candidates.begin(); it != candidates.end(); ++it) { + jdesc->AddCandidate(*it); + delete *it; + } + return true; +} + +bool SdpDeserializeCandidate(const std::string& message, + JsepIceCandidate* jcandidate, + SdpParseError* error) { + ASSERT(jcandidate != NULL); + Candidate candidate; + if (!ParseCandidate(message, &candidate, error, true)) { + return false; + } + jcandidate->SetCandidate(candidate); + return true; +} + +bool ParseCandidate(const std::string& message, Candidate* candidate, + SdpParseError* error, bool is_raw) { + ASSERT(candidate != NULL); + + // Get the first line from |message|. + std::string first_line; + GetFirstLine(message, &first_line); + + size_t start_pos = kLinePrefixLength; // Starting position to parse. + if (IsRawCandidate(first_line)) { + // From WebRTC draft section 4.8.1.1 candidate-attribute will be + // just candidate: not a=candidate:CRLF + start_pos = 0; + } else if (!IsLineType(first_line, kLineTypeAttributes) || + !HasAttribute(first_line, kAttributeCandidate)) { + // Must start with a=candidate line. + // Expecting to be of the format a=candidate:CRLF. + if (is_raw) { + std::ostringstream description; + description << "Expect line: " + << kAttributeCandidate + << ":" << ""; + return ParseFailed(first_line, 0, description.str(), error); + } else { + return ParseFailedExpectLine(first_line, 0, kLineTypeAttributes, + kAttributeCandidate, error); + } + } + + std::vector fields; + talk_base::split(first_line.substr(start_pos), + kSdpDelimiterSpace, &fields); + // RFC 5245 + // a=candidate: + // typ + // [raddr ] [rport ] + // *(SP extension-att-name SP extension-att-value) + const size_t expected_min_fields = 8; + if (fields.size() < expected_min_fields || + (fields[6] != kAttributeCandidateTyp)) { + return ParseFailedExpectMinFieldNum(first_line, expected_min_fields, error); + } + std::string foundation; + if (!GetValue(fields[0], kAttributeCandidate, &foundation, error)) { + return false; + } + const int component_id = talk_base::FromString(fields[1]); + const std::string transport = fields[2]; + const uint32 priority = talk_base::FromString(fields[3]); + const std::string connection_address = fields[4]; + const int port = talk_base::FromString(fields[5]); + SocketAddress address(connection_address, port); + + cricket::ProtocolType protocol; + if (!StringToProto(transport.c_str(), &protocol)) { + return ParseFailed(first_line, "Unsupported transport type.", error); + } + + std::string candidate_type; + const std::string type = fields[7]; + if (type == kCandidateHost) { + candidate_type = cricket::LOCAL_PORT_TYPE; + } else if (type == kCandidateSrflx) { + candidate_type = cricket::STUN_PORT_TYPE; + } else if (type == kCandidateRelay) { + candidate_type = cricket::RELAY_PORT_TYPE; + } else { + return ParseFailed(first_line, "Unsupported candidate type.", error); + } + + size_t current_position = expected_min_fields; + SocketAddress related_address; + // The 2 optional fields for related address + // [raddr ] [rport ] + if (fields.size() >= (current_position + 2) && + fields[current_position] == kAttributeCandidateRaddr) { + related_address.SetIP(fields[++current_position]); + ++current_position; + } + if (fields.size() >= (current_position + 2) && + fields[current_position] == kAttributeCandidateRport) { + related_address.SetPort( + talk_base::FromString(fields[++current_position])); + ++current_position; + } + + // Extension + // Empty string as the candidate username and password. + // Will be updated later with the ice-ufrag and ice-pwd. + // TODO: Remove the username/password extension, which is currently + // kept for backwards compatibility. + std::string username; + std::string password; + uint32 generation = 0; + for (size_t i = current_position; i + 1 < fields.size(); ++i) { + // RFC 5245 + // *(SP extension-att-name SP extension-att-value) + if (fields[i] == kAttributeCandidateGeneration) { + generation = talk_base::FromString(fields[++i]); + } else if (fields[i] == kAttributeCandidateUsername) { + username = fields[++i]; + } else if (fields[i] == kAttributeCandidatePassword) { + password = fields[++i]; + } else { + // Skip the unknown extension. + ++i; + } + } + + // Empty string as the candidate id and network name. + const std::string id; + const std::string network_name; + *candidate = Candidate(id, component_id, cricket::ProtoToString(protocol), + address, priority, username, password, candidate_type, network_name, + generation, foundation); + candidate->set_related_address(related_address); + return true; +} + +bool ParseIceOptions(const std::string& line, + std::vector* transport_options, + SdpParseError* error) { + std::string ice_options; + if (!GetValue(line, kAttributeIceOption, &ice_options, error)) { + return false; + } + std::vector fields; + talk_base::split(ice_options, kSdpDelimiterSpace, &fields); + for (size_t i = 0; i < fields.size(); ++i) { + transport_options->push_back(fields[i]); + } + return true; +} + +bool ParseExtmap(const std::string& line, RtpHeaderExtension* extmap, + SdpParseError* error) { + // RFC 5285 + // a=extmap:["/"] + std::vector fields; + talk_base::split(line.substr(kLinePrefixLength), + kSdpDelimiterSpace, &fields); + const size_t expected_min_fields = 2; + if (fields.size() < expected_min_fields) { + return ParseFailedExpectMinFieldNum(line, expected_min_fields, error); + } + std::string uri = fields[1]; + + std::string value_direction; + if (!GetValue(fields[0], kAttributeExtmap, &value_direction, error)) { + return false; + } + std::vector sub_fields; + talk_base::split(value_direction, kSdpDelimiterSlash, &sub_fields); + int value = talk_base::FromString(sub_fields[0]); + + *extmap = RtpHeaderExtension(uri, value); + return true; +} + +void BuildMediaDescription(const ContentInfo* content_info, + const TransportInfo* transport_info, + const MediaType media_type, + std::string* message) { + ASSERT(message != NULL); + if (content_info == NULL || message == NULL) { + return; + } + // TODO: Rethink if we should use sprintfn instead of stringstream. + // According to the style guide, streams should only be used for logging. + // http://google-styleguide.googlecode.com/svn/ + // trunk/cppguide.xml?showone=Streams#Streams + std::ostringstream os; + const MediaContentDescription* media_desc = + static_cast ( + content_info->description); + ASSERT(media_desc != NULL); + + bool is_sctp = (media_desc->protocol() == cricket::kMediaProtocolDtlsSctp); + + // RFC 4566 + // m= + // fmt is a list of payload type numbers that MAY be used in the session. + const char* type = NULL; + if (media_type == cricket::MEDIA_TYPE_AUDIO) + type = kMediaTypeAudio; + else if (media_type == cricket::MEDIA_TYPE_VIDEO) + type = kMediaTypeVideo; + else if (media_type == cricket::MEDIA_TYPE_DATA) + type = kMediaTypeData; + else + ASSERT(false); + + std::string fmt; + if (media_type == cricket::MEDIA_TYPE_VIDEO) { + const VideoContentDescription* video_desc = + static_cast(media_desc); + for (std::vector::const_iterator it = + video_desc->codecs().begin(); + it != video_desc->codecs().end(); ++it) { + fmt.append(" "); + fmt.append(talk_base::ToString(it->id)); + } + } else if (media_type == cricket::MEDIA_TYPE_AUDIO) { + const AudioContentDescription* audio_desc = + static_cast(media_desc); + for (std::vector::const_iterator it = + audio_desc->codecs().begin(); + it != audio_desc->codecs().end(); ++it) { + fmt.append(" "); + fmt.append(talk_base::ToString(it->id)); + } + } else if (media_type == cricket::MEDIA_TYPE_DATA) { + if (is_sctp) { + fmt.append(" "); + // TODO(jiayl): Replace the hard-coded string with the fmt read out of the + // ContentDescription. + fmt.append(talk_base::ToString(kDefaultSctpFmt)); + } else { + const DataContentDescription* data_desc = + static_cast(media_desc); + for (std::vector::const_iterator it = + data_desc->codecs().begin(); + it != data_desc->codecs().end(); ++it) { + fmt.append(" "); + fmt.append(talk_base::ToString(it->id)); + } + } + } + // The fmt must never be empty. If no codecs are found, set the fmt attribute + // to 0. + if (fmt.empty()) { + fmt = " 0"; + } + + // The port number in the m line will be updated later when associate with + // the candidates. + // RFC 3264 + // To reject an offered stream, the port number in the corresponding stream in + // the answer MUST be set to zero. + const std::string port = content_info->rejected ? + kMediaPortRejected : kDefaultPort; + + talk_base::SSLFingerprint* fp = (transport_info) ? + transport_info->description.identity_fingerprint.get() : NULL; + + InitLine(kLineTypeMedia, type, &os); + os << " " << port << " " << media_desc->protocol() << fmt; + AddLine(os.str(), message); + + // Use the transport_info to build the media level ice-ufrag and ice-pwd. + if (transport_info) { + // RFC 5245 + // ice-pwd-att = "ice-pwd" ":" password + // ice-ufrag-att = "ice-ufrag" ":" ufrag + // ice-ufrag + InitAttrLine(kAttributeIceUfrag, &os); + os << kSdpDelimiterColon << transport_info->description.ice_ufrag; + AddLine(os.str(), message); + // ice-pwd + InitAttrLine(kAttributeIcePwd, &os); + os << kSdpDelimiterColon << transport_info->description.ice_pwd; + AddLine(os.str(), message); + + // draft-petithuguenin-mmusic-ice-attributes-level-03 + BuildIceOptions(transport_info->description.transport_options, message); + + // RFC 4572 + // fingerprint-attribute = + // "fingerprint" ":" hash-func SP fingerprint + if (fp) { + // Insert the fingerprint attribute. + InitAttrLine(kAttributeFingerprint, &os); + os << kSdpDelimiterColon + << fp->algorithm << kSdpDelimiterSpace + << fp->GetRfc4572Fingerprint(); + + AddLine(os.str(), message); + } + } + + // RFC 3388 + // mid-attribute = "a=mid:" identification-tag + // identification-tag = token + // Use the content name as the mid identification-tag. + InitAttrLine(kAttributeMid, &os); + os << kSdpDelimiterColon << content_info->name; + AddLine(os.str(), message); + + if (is_sctp) { + BuildSctpContentAttributes(message); + } else { + BuildRtpContentAttributes(media_desc, media_type, message); + } +} + +void BuildSctpContentAttributes(std::string* message) { + cricket::DataCodec sctp_codec(kDefaultSctpFmt, kDefaultSctpFmtProtocol, 0); + sctp_codec.SetParam(kCodecParamSctpProtocol, kDefaultSctpFmtProtocol); + sctp_codec.SetParam(kCodecParamSctpStreams, cricket::kMaxSctpSid + 1); + AddFmtpLine(sctp_codec, message); +} + +void BuildRtpContentAttributes( + const MediaContentDescription* media_desc, + const MediaType media_type, + std::string* message) { + std::ostringstream os; + // RFC 5285 + // a=extmap:["/"] + // The definitions MUST be either all session level or all media level. This + // implementation uses all media level. + for (size_t i = 0; i < media_desc->rtp_header_extensions().size(); ++i) { + InitAttrLine(kAttributeExtmap, &os); + os << kSdpDelimiterColon << media_desc->rtp_header_extensions()[i].id + << kSdpDelimiterSpace << media_desc->rtp_header_extensions()[i].uri; + AddLine(os.str(), message); + } + + // RFC 3264 + // a=sendrecv || a=sendonly || a=sendrecv || a=inactive + + cricket::MediaContentDirection direction = media_desc->direction(); + if (media_desc->streams().empty() && direction == cricket::MD_SENDRECV) { + direction = cricket::MD_RECVONLY; + } + + switch (direction) { + case cricket::MD_INACTIVE: + InitAttrLine(kAttributeInactive, &os); + break; + case cricket::MD_SENDONLY: + InitAttrLine(kAttributeSendOnly, &os); + break; + case cricket::MD_RECVONLY: + InitAttrLine(kAttributeRecvOnly, &os); + break; + case cricket::MD_SENDRECV: + default: + InitAttrLine(kAttributeSendRecv, &os); + break; + } + AddLine(os.str(), message); + + // RFC 4566 + // b=AS: + if (media_desc->bandwidth() >= 1000) { + InitLine(kLineTypeSessionBandwidth, kApplicationSpecificMaximum, &os); + os << kSdpDelimiterColon << (media_desc->bandwidth() / 1000); + AddLine(os.str(), message); + } + + // RFC 5761 + // a=rtcp-mux + if (media_desc->rtcp_mux()) { + InitAttrLine(kAttributeRtcpMux, &os); + AddLine(os.str(), message); + } + + // RFC 4568 + // a=crypto: [] + for (std::vector::const_iterator it = + media_desc->cryptos().begin(); + it != media_desc->cryptos().end(); ++it) { + InitAttrLine(kAttributeCrypto, &os); + os << kSdpDelimiterColon << it->tag << " " << it->cipher_suite << " " + << it->key_params; + if (!it->session_params.empty()) { + os << " " << it->session_params; + } + AddLine(os.str(), message); + } + + // RFC 4566 + // a=rtpmap: / + // [/] + BuildRtpMap(media_desc, media_type, message); + + // Specify latency for buffered mode. + // a=x-google-buffer-latency: + if (media_desc->buffered_mode_latency() != cricket::kBufferedModeDisabled) { + std::ostringstream os; + InitAttrLine(kAttributeXGoogleBufferLatency, &os); + os << kSdpDelimiterColon << media_desc->buffered_mode_latency(); + AddLine(os.str(), message); + } + + for (StreamParamsVec::const_iterator track = media_desc->streams().begin(); + track != media_desc->streams().end(); ++track) { + // Require that the track belongs to a media stream, + // ie the sync_label is set. This extra check is necessary since the + // MediaContentDescription always contains a streamparam with an ssrc even + // if no track or media stream have been created. + if (track->sync_label.empty()) continue; + + // Build the ssrc-group lines. + for (size_t i = 0; i < track->ssrc_groups.size(); ++i) { + // RFC 5576 + // a=ssrc-group: ... + if (track->ssrc_groups[i].ssrcs.empty()) { + continue; + } + std::ostringstream os; + InitAttrLine(kAttributeSsrcGroup, &os); + os << kSdpDelimiterColon << track->ssrc_groups[i].semantics; + std::vector::const_iterator ssrc = + track->ssrc_groups[i].ssrcs.begin(); + for (; ssrc != track->ssrc_groups[i].ssrcs.end(); ++ssrc) { + os << kSdpDelimiterSpace << talk_base::ToString(*ssrc); + } + AddLine(os.str(), message); + } + // Build the ssrc lines for each ssrc. + for (size_t i = 0; i < track->ssrcs.size(); ++i) { + uint32 ssrc = track->ssrcs[i]; + // RFC 5576 + // a=ssrc: cname: + AddSsrcLine(ssrc, kSsrcAttributeCname, + track->cname, message); + + // draft-alvestrand-mmusic-msid-00 + // a=ssrc: msid:identifier [appdata] + // The appdata consists of the "id" attribute of a MediaStreamTrack, which + // is corresponding to the "name" attribute of StreamParams. + std::string appdata = track->id; + std::ostringstream os; + InitAttrLine(kAttributeSsrc, &os); + os << kSdpDelimiterColon << ssrc << kSdpDelimiterSpace + << kSsrcAttributeMsid << kSdpDelimiterColon << track->sync_label + << kSdpDelimiterSpace << appdata; + AddLine(os.str(), message); + + // TODO(ronghuawu): Remove below code which is for backward compatibility. + // draft-alvestrand-rtcweb-mid-01 + // a=ssrc: mslabel: + // The label isn't yet defined. + // a=ssrc: label: + AddSsrcLine(ssrc, kSsrcAttributeMslabel, track->sync_label, message); + AddSsrcLine(ssrc, kSSrcAttributeLabel, track->id, message); + } + } +} + +void WriteFmtpHeader(int payload_type, std::ostringstream* os) { + // fmtp header: a=fmtp:|payload_type| + // Add a=fmtp + InitAttrLine(kAttributeFmtp, os); + // Add :|payload_type| + *os << kSdpDelimiterColon << payload_type; +} + +void WriteRtcpFbHeader(int payload_type, std::ostringstream* os) { + // rtcp-fb header: a=rtcp-fb:|payload_type| + // /> + // Add a=rtcp-fb + InitAttrLine(kAttributeRtcpFb, os); + // Add : + *os << kSdpDelimiterColon; + if (payload_type == kWildcardPayloadType) { + *os << "*"; + } else { + *os << payload_type; + } +} + +void WriteFmtpParameter(const std::string& parameter_name, + const std::string& parameter_value, + std::ostringstream* os) { + // fmtp parameters: |parameter_name|=|parameter_value| + *os << parameter_name << kSdpDelimiterEqual << parameter_value; +} + +void WriteFmtpParameters(const cricket::CodecParameterMap& parameters, + std::ostringstream* os) { + for (cricket::CodecParameterMap::const_iterator fmtp = parameters.begin(); + fmtp != parameters.end(); ++fmtp) { + // Each new parameter, except the first one starts with ";" and " ". + if (fmtp != parameters.begin()) { + *os << kSdpDelimiterSemicolon; + } + *os << kSdpDelimiterSpace; + WriteFmtpParameter(fmtp->first, fmtp->second, os); + } +} + +bool IsFmtpParam(const std::string& name) { + const char* kFmtpParams[] = { + kCodecParamMinPTime, kCodecParamSPropStereo, + kCodecParamStereo, kCodecParamUseInbandFec, + kCodecParamMaxBitrate, kCodecParamMinBitrate, kCodecParamMaxQuantization, + kCodecParamSctpProtocol, kCodecParamSctpStreams + }; + for (size_t i = 0; i < ARRAY_SIZE(kFmtpParams); ++i) { + if (_stricmp(name.c_str(), kFmtpParams[i]) == 0) { + return true; + } + } + return false; +} + +// Retreives fmtp parameters from |params|, which may contain other parameters +// as well, and puts them in |fmtp_parameters|. +void GetFmtpParams(const cricket::CodecParameterMap& params, + cricket::CodecParameterMap* fmtp_parameters) { + for (cricket::CodecParameterMap::const_iterator iter = params.begin(); + iter != params.end(); ++iter) { + if (IsFmtpParam(iter->first)) { + (*fmtp_parameters)[iter->first] = iter->second; + } + } +} + +template +void AddFmtpLine(const T& codec, std::string* message) { + cricket::CodecParameterMap fmtp_parameters; + GetFmtpParams(codec.params, &fmtp_parameters); + if (fmtp_parameters.empty()) { + // No need to add an fmtp if it will have no (optional) parameters. + return; + } + std::ostringstream os; + WriteFmtpHeader(codec.id, &os); + WriteFmtpParameters(fmtp_parameters, &os); + AddLine(os.str(), message); + return; +} + +template +void AddRtcpFbLines(const T& codec, std::string* message) { + for (std::vector::const_iterator iter = + codec.feedback_params.params().begin(); + iter != codec.feedback_params.params().end(); ++iter) { + std::ostringstream os; + WriteRtcpFbHeader(codec.id, &os); + os << " " << iter->id(); + if (!iter->param().empty()) { + os << " " << iter->param(); + } + AddLine(os.str(), message); + } +} + +bool GetMinValue(const std::vector& values, int* value) { + if (values.empty()) { + return false; + } + std::vector::const_iterator found = + std::min_element(values.begin(), values.end()); + *value = *found; + return true; +} + +bool GetParameter(const std::string& name, + const cricket::CodecParameterMap& params, int* value) { + std::map::const_iterator found = + params.find(name); + if (found == params.end()) { + return false; + } + *value = talk_base::FromString(found->second); + return true; +} + +void BuildRtpMap(const MediaContentDescription* media_desc, + const MediaType media_type, + std::string* message) { + ASSERT(message != NULL); + ASSERT(media_desc != NULL); + std::ostringstream os; + if (media_type == cricket::MEDIA_TYPE_VIDEO) { + const VideoContentDescription* video_desc = + static_cast(media_desc); + for (std::vector::const_iterator it = + video_desc->codecs().begin(); + it != video_desc->codecs().end(); ++it) { + // RFC 4566 + // a=rtpmap: / + // [/] + if (it->id != kWildcardPayloadType) { + InitAttrLine(kAttributeRtpmap, &os); + os << kSdpDelimiterColon << it->id << " " << it->name + << "/" << kDefaultVideoClockrate; + AddLine(os.str(), message); + } + AddRtcpFbLines(*it, message); + AddFmtpLine(*it, message); + } + } else if (media_type == cricket::MEDIA_TYPE_AUDIO) { + const AudioContentDescription* audio_desc = + static_cast(media_desc); + std::vector ptimes; + std::vector maxptimes; + int max_minptime = 0; + for (std::vector::const_iterator it = + audio_desc->codecs().begin(); + it != audio_desc->codecs().end(); ++it) { + ASSERT(!it->name.empty()); + // RFC 4566 + // a=rtpmap: / + // [/] + InitAttrLine(kAttributeRtpmap, &os); + os << kSdpDelimiterColon << it->id << " "; + os << it->name << "/" << it->clockrate; + if (it->channels != 1) { + os << "/" << it->channels; + } + AddLine(os.str(), message); + AddRtcpFbLines(*it, message); + AddFmtpLine(*it, message); + int minptime = 0; + if (GetParameter(kCodecParamMinPTime, it->params, &minptime)) { + max_minptime = std::max(minptime, max_minptime); + } + int ptime; + if (GetParameter(kCodecParamPTime, it->params, &ptime)) { + ptimes.push_back(ptime); + } + int maxptime; + if (GetParameter(kCodecParamMaxPTime, it->params, &maxptime)) { + maxptimes.push_back(maxptime); + } + } + // Populate the maxptime attribute with the smallest maxptime of all codecs + // under the same m-line. + int min_maxptime = INT_MAX; + if (GetMinValue(maxptimes, &min_maxptime)) { + AddAttributeLine(kCodecParamMaxPTime, min_maxptime, message); + } + ASSERT(min_maxptime > max_minptime); + // Populate the ptime attribute with the smallest ptime or the largest + // minptime, whichever is the largest, for all codecs under the same m-line. + int ptime = INT_MAX; + if (GetMinValue(ptimes, &ptime)) { + ptime = std::min(ptime, min_maxptime); + ptime = std::max(ptime, max_minptime); + AddAttributeLine(kCodecParamPTime, ptime, message); + } + } else if (media_type == cricket::MEDIA_TYPE_DATA) { + const DataContentDescription* data_desc = + static_cast(media_desc); + for (std::vector::const_iterator it = + data_desc->codecs().begin(); + it != data_desc->codecs().end(); ++it) { + // RFC 4566 + // a=rtpmap: / + // [/] + InitAttrLine(kAttributeRtpmap, &os); + os << kSdpDelimiterColon << it->id << " " + << it->name << "/" << it->clockrate; + AddLine(os.str(), message); + } + } +} + +void BuildCandidate(const std::vector& candidates, + std::string* message) { + std::ostringstream os; + + for (std::vector::const_iterator it = candidates.begin(); + it != candidates.end(); ++it) { + // RFC 5245 + // a=candidate: + // typ + // [raddr ] [rport ] + // *(SP extension-att-name SP extension-att-value) + std::string type; + // Map the cricket candidate type to "host" / "srflx" / "prflx" / "relay" + if (it->type() == cricket::LOCAL_PORT_TYPE) { + type = kCandidateHost; + } else if (it->type() == cricket::STUN_PORT_TYPE) { + type = kCandidateSrflx; + } else if (it->type() == cricket::RELAY_PORT_TYPE) { + type = kCandidateRelay; + } else { + ASSERT(false); + } + + InitAttrLine(kAttributeCandidate, &os); + os << kSdpDelimiterColon + << it->foundation() << " " << it->component() << " " + << it->protocol() << " " << it->priority() << " " + << it->address().ipaddr().ToString() << " " + << it->address().PortAsString() << " " + << kAttributeCandidateTyp << " " << type << " "; + + // Related address + if (!it->related_address().IsNil()) { + os << kAttributeCandidateRaddr << " " + << it->related_address().ipaddr().ToString() << " " + << kAttributeCandidateRport << " " + << it->related_address().PortAsString() << " "; + } + + // Extensions + os << kAttributeCandidateGeneration << " " << it->generation(); + + AddLine(os.str(), message); + } +} + +void BuildIceOptions(const std::vector& transport_options, + std::string* message) { + if (!transport_options.empty()) { + std::ostringstream os; + InitAttrLine(kAttributeIceOption, &os); + os << kSdpDelimiterColon << transport_options[0]; + for (size_t i = 1; i < transport_options.size(); ++i) { + os << kSdpDelimiterSpace << transport_options[i]; + } + AddLine(os.str(), message); + } +} + +bool ParseSessionDescription(const std::string& message, size_t* pos, + std::string* session_id, + std::string* session_version, + bool* supports_msid, + TransportDescription* session_td, + RtpHeaderExtensions* session_extmaps, + cricket::SessionDescription* desc, + SdpParseError* error) { + std::string line; + + // RFC 4566 + // v= (protocol version) + if (!GetLineWithType(message, pos, &line, kLineTypeVersion)) { + return ParseFailedExpectLine(message, *pos, kLineTypeVersion, + std::string(), error); + } + // RFC 4566 + // o= + // + if (!GetLineWithType(message, pos, &line, kLineTypeOrigin)) { + return ParseFailedExpectLine(message, *pos, kLineTypeOrigin, + std::string(), error); + } + std::vector fields; + talk_base::split(line.substr(kLinePrefixLength), + kSdpDelimiterSpace, &fields); + const size_t expected_fields = 6; + if (fields.size() != expected_fields) { + return ParseFailedExpectFieldNum(line, expected_fields, error); + } + *session_id = fields[1]; + *session_version = fields[2]; + + // RFC 4566 + // s= (session name) + if (!GetLineWithType(message, pos, &line, kLineTypeSessionName)) { + return ParseFailedExpectLine(message, *pos, kLineTypeSessionName, + std::string(), error); + } + + // Optional lines + // Those are the optional lines, so shouldn't return false if not present. + // RFC 4566 + // i=* (session information) + GetLineWithType(message, pos, &line, kLineTypeSessionInfo); + + // RFC 4566 + // u=* (URI of description) + GetLineWithType(message, pos, &line, kLineTypeSessionUri); + + // RFC 4566 + // e=* (email address) + GetLineWithType(message, pos, &line, kLineTypeSessionEmail); + + // RFC 4566 + // p=* (phone number) + GetLineWithType(message, pos, &line, kLineTypeSessionPhone); + + // RFC 4566 + // c=* (connection information -- not required if included in + // all media) + GetLineWithType(message, pos, &line, kLineTypeConnection); + + // RFC 4566 + // b=* (zero or more bandwidth information lines) + while (GetLineWithType(message, pos, &line, kLineTypeSessionBandwidth)) { + // By pass zero or more b lines. + } + + // RFC 4566 + // One or more time descriptions ("t=" and "r=" lines; see below) + // t= (time the session is active) + // r=* (zero or more repeat times) + // Ensure there's at least one time description + if (!GetLineWithType(message, pos, &line, kLineTypeTiming)) { + return ParseFailedExpectLine(message, *pos, kLineTypeTiming, std::string(), + error); + } + + while (GetLineWithType(message, pos, &line, kLineTypeRepeatTimes)) { + // By pass zero or more r lines. + } + + // Go through the rest of the time descriptions + while (GetLineWithType(message, pos, &line, kLineTypeTiming)) { + while (GetLineWithType(message, pos, &line, kLineTypeRepeatTimes)) { + // By pass zero or more r lines. + } + } + + // RFC 4566 + // z=* (time zone adjustments) + GetLineWithType(message, pos, &line, kLineTypeTimeZone); + + // RFC 4566 + // k=* (encryption key) + GetLineWithType(message, pos, &line, kLineTypeEncryptionKey); + + // RFC 4566 + // a=* (zero or more session attribute lines) + while (GetLineWithType(message, pos, &line, kLineTypeAttributes)) { + if (HasAttribute(line, kAttributeGroup)) { + if (!ParseGroupAttribute(line, desc, error)) { + return false; + } + } else if (HasAttribute(line, kAttributeIceUfrag)) { + if (!GetValue(line, kAttributeIceUfrag, + &(session_td->ice_ufrag), error)) { + return false; + } + } else if (HasAttribute(line, kAttributeIcePwd)) { + if (!GetValue(line, kAttributeIcePwd, &(session_td->ice_pwd), error)) { + return false; + } + } else if (HasAttribute(line, kAttributeIceLite)) { + session_td->ice_mode = cricket::ICEMODE_LITE; + } else if (HasAttribute(line, kAttributeIceOption)) { + if (!ParseIceOptions(line, &(session_td->transport_options), error)) { + return false; + } + } else if (HasAttribute(line, kAttributeFingerprint)) { + if (session_td->identity_fingerprint.get()) { + return ParseFailed( + line, + "Can't have multiple fingerprint attributes at the same level.", + error); + } + talk_base::SSLFingerprint* fingerprint = NULL; + if (!ParseFingerprintAttribute(line, &fingerprint, error)) { + return false; + } + session_td->identity_fingerprint.reset(fingerprint); + } else if (HasAttribute(line, kAttributeMsidSemantics)) { + std::string semantics; + if (!GetValue(line, kAttributeMsidSemantics, &semantics, error)) { + return false; + } + *supports_msid = CaseInsensitiveFind(semantics, kMediaStreamSemantic); + } else if (HasAttribute(line, kAttributeExtmap)) { + RtpHeaderExtension extmap; + if (!ParseExtmap(line, &extmap, error)) { + return false; + } + session_extmaps->push_back(extmap); + } + } + + return true; +} + +bool ParseGroupAttribute(const std::string& line, + cricket::SessionDescription* desc, + SdpParseError* error) { + ASSERT(desc != NULL); + + // RFC 5888 and draft-holmberg-mmusic-sdp-bundle-negotiation-00 + // a=group:BUNDLE video voice + std::vector fields; + talk_base::split(line.substr(kLinePrefixLength), + kSdpDelimiterSpace, &fields); + std::string semantics; + if (!GetValue(fields[0], kAttributeGroup, &semantics, error)) { + return false; + } + cricket::ContentGroup group(semantics); + for (size_t i = 1; i < fields.size(); ++i) { + group.AddContentName(fields[i]); + } + desc->AddGroup(group); + return true; +} + +static bool ParseFingerprintAttribute(const std::string& line, + talk_base::SSLFingerprint** fingerprint, + SdpParseError* error) { + if (!IsLineType(line, kLineTypeAttributes) || + !HasAttribute(line, kAttributeFingerprint)) { + return ParseFailedExpectLine(line, 0, kLineTypeAttributes, + kAttributeFingerprint, error); + } + + std::vector fields; + talk_base::split(line.substr(kLinePrefixLength), + kSdpDelimiterSpace, &fields); + const size_t expected_fields = 2; + if (fields.size() != expected_fields) { + return ParseFailedExpectFieldNum(line, expected_fields, error); + } + + // The first field here is "fingerprint:. + std::string algorithm; + if (!GetValue(fields[0], kAttributeFingerprint, &algorithm, error)) { + return false; + } + + // Downcase the algorithm. Note that we don't need to downcase the + // fingerprint because hex_decode can handle upper-case. + std::transform(algorithm.begin(), algorithm.end(), algorithm.begin(), + ::tolower); + + // The second field is the digest value. De-hexify it. + *fingerprint = talk_base::SSLFingerprint::CreateFromRfc4572( + algorithm, fields[1]); + if (!*fingerprint) { + return ParseFailed(line, + "Failed to create fingerprint from the digest.", + error); + } + + return true; +} + +// RFC 3551 +// PT encoding media type clock rate channels +// name (Hz) +// 0 PCMU A 8,000 1 +// 1 reserved A +// 2 reserved A +// 3 GSM A 8,000 1 +// 4 G723 A 8,000 1 +// 5 DVI4 A 8,000 1 +// 6 DVI4 A 16,000 1 +// 7 LPC A 8,000 1 +// 8 PCMA A 8,000 1 +// 9 G722 A 8,000 1 +// 10 L16 A 44,100 2 +// 11 L16 A 44,100 1 +// 12 QCELP A 8,000 1 +// 13 CN A 8,000 1 +// 14 MPA A 90,000 (see text) +// 15 G728 A 8,000 1 +// 16 DVI4 A 11,025 1 +// 17 DVI4 A 22,050 1 +// 18 G729 A 8,000 1 +struct StaticPayloadAudioCodec { + const char* name; + int clockrate; + int channels; +}; +static const StaticPayloadAudioCodec kStaticPayloadAudioCodecs[] = { + { "PCMU", 8000, 1 }, + { "reserved", 0, 0 }, + { "reserved", 0, 0 }, + { "GSM", 8000, 1 }, + { "G723", 8000, 1 }, + { "DVI4", 8000, 1 }, + { "DVI4", 16000, 1 }, + { "LPC", 8000, 1 }, + { "PCMA", 8000, 1 }, + { "G722", 8000, 1 }, + { "L16", 44100, 2 }, + { "L16", 44100, 1 }, + { "QCELP", 8000, 1 }, + { "CN", 8000, 1 }, + { "MPA", 90000, 1 }, + { "G728", 8000, 1 }, + { "DVI4", 11025, 1 }, + { "DVI4", 22050, 1 }, + { "G729", 8000, 1 }, +}; + +void MaybeCreateStaticPayloadAudioCodecs( + const std::vector& fmts, AudioContentDescription* media_desc) { + if (!media_desc) { + return; + } + int preference = fmts.size(); + std::vector::const_iterator it = fmts.begin(); + bool add_new_codec = false; + for (; it != fmts.end(); ++it) { + int payload_type = *it; + if (!media_desc->HasCodec(payload_type) && + payload_type >= 0 && + payload_type < ARRAY_SIZE(kStaticPayloadAudioCodecs)) { + std::string encoding_name = kStaticPayloadAudioCodecs[payload_type].name; + int clock_rate = kStaticPayloadAudioCodecs[payload_type].clockrate; + int channels = kStaticPayloadAudioCodecs[payload_type].channels; + media_desc->AddCodec(cricket::AudioCodec(payload_type, encoding_name, + clock_rate, 0, channels, + preference)); + add_new_codec = true; + } + --preference; + } + if (add_new_codec) { + media_desc->SortCodecs(); + } +} + +template +static C* ParseContentDescription(const std::string& message, + const MediaType media_type, + int mline_index, + const std::string& protocol, + const std::vector& codec_preference, + size_t* pos, + std::string* content_name, + TransportDescription* transport, + std::vector* candidates, + webrtc::SdpParseError* error) { + C* media_desc = new C(); + switch (media_type) { + case cricket::MEDIA_TYPE_AUDIO: + *content_name = cricket::CN_AUDIO; + break; + case cricket::MEDIA_TYPE_VIDEO: + *content_name = cricket::CN_VIDEO; + break; + case cricket::MEDIA_TYPE_DATA: + *content_name = cricket::CN_DATA; + break; + default: + ASSERT(false); + break; + } + if (!ParseContent(message, media_type, mline_index, protocol, + codec_preference, pos, content_name, + media_desc, transport, candidates, error)) { + delete media_desc; + return NULL; + } + // Sort the codecs according to the m-line fmt list. + media_desc->SortCodecs(); + return media_desc; +} + +bool ParseMediaDescription(const std::string& message, + const TransportDescription& session_td, + const RtpHeaderExtensions& session_extmaps, + bool supports_msid, + size_t* pos, + cricket::SessionDescription* desc, + std::vector* candidates, + SdpParseError* error) { + ASSERT(desc != NULL); + std::string line; + int mline_index = -1; + + // Zero or more media descriptions + // RFC 4566 + // m= + while (GetLineWithType(message, pos, &line, kLineTypeMedia)) { + ++mline_index; + + std::vector fields; + talk_base::split(line.substr(kLinePrefixLength), + kSdpDelimiterSpace, &fields); + const size_t expected_min_fields = 4; + if (fields.size() < expected_min_fields) { + return ParseFailedExpectMinFieldNum(line, expected_min_fields, error); + } + bool rejected = false; + // RFC 3264 + // To reject an offered stream, the port number in the corresponding stream + // in the answer MUST be set to zero. + if (fields[1] == kMediaPortRejected) { + rejected = true; + } + + std::string protocol = fields[2]; + bool is_sctp = (protocol == cricket::kMediaProtocolDtlsSctp); + + // + std::vector codec_preference; + for (size_t j = 3 ; j < fields.size(); ++j) { + codec_preference.push_back(talk_base::FromString(fields[j])); + } + + // Make a temporary TransportDescription based on |session_td|. + // Some of this gets overwritten by ParseContent. + TransportDescription transport(NS_JINGLE_ICE_UDP, + session_td.transport_options, + session_td.ice_ufrag, + session_td.ice_pwd, + session_td.ice_mode, + session_td.identity_fingerprint.get(), + Candidates()); + + talk_base::scoped_ptr content; + std::string content_name; + if (HasAttribute(line, kMediaTypeVideo)) { + content.reset(ParseContentDescription( + message, cricket::MEDIA_TYPE_VIDEO, mline_index, protocol, + codec_preference, pos, &content_name, + &transport, candidates, error)); + } else if (HasAttribute(line, kMediaTypeAudio)) { + content.reset(ParseContentDescription( + message, cricket::MEDIA_TYPE_AUDIO, mline_index, protocol, + codec_preference, pos, &content_name, + &transport, candidates, error)); + MaybeCreateStaticPayloadAudioCodecs( + codec_preference, + static_cast(content.get())); + } else if (HasAttribute(line, kMediaTypeData)) { + content.reset(ParseContentDescription( + message, cricket::MEDIA_TYPE_DATA, mline_index, protocol, + codec_preference, pos, &content_name, + &transport, candidates, error)); + } else { + LOG(LS_WARNING) << "Unsupported media type: " << line; + continue; + } + if (!content.get()) { + // ParseContentDescription returns NULL if failed. + return false; + } + + if (!is_sctp) { + // Make sure to set the media direction correctly. If the direction is not + // MD_RECVONLY or Inactive and no streams are parsed, + // a default MediaStream will be created to prepare for receiving media. + if (supports_msid && content->streams().empty() && + content->direction() == cricket::MD_SENDRECV) { + content->set_direction(cricket::MD_RECVONLY); + } + + // Set the extmap. + if (!session_extmaps.empty() && + !content->rtp_header_extensions().empty()) { + return ParseFailed("", + "The a=extmap MUST be either all session level or " + "all media level.", + error); + } + for (size_t i = 0; i < session_extmaps.size(); ++i) { + content->AddRtpHeaderExtension(session_extmaps[i]); + } + } + content->set_protocol(protocol); + desc->AddContent(content_name, + is_sctp ? cricket::NS_JINGLE_DRAFT_SCTP : + cricket::NS_JINGLE_RTP, + rejected, + content.release()); + // Create TransportInfo with the media level "ice-pwd" and "ice-ufrag". + TransportInfo transport_info(content_name, transport); + + if (!desc->AddTransportInfo(transport_info)) { + std::ostringstream description; + description << "Failed to AddTransportInfo with content name: " + << content_name; + return ParseFailed("", description.str(), error); + } + } + return true; +} + +bool VerifyCodec(const cricket::Codec& codec) { + // Codec has not been populated correctly unless the name has been set. This + // can happen if an SDP has an fmtp or rtcp-fb with a payload type but doesn't + // have a corresponding "rtpmap" line. + cricket::Codec default_codec; + return default_codec.name != codec.name; +} + +bool VerifyAudioCodecs(const AudioContentDescription* audio_desc) { + const std::vector& codecs = audio_desc->codecs(); + for (std::vector::const_iterator iter = codecs.begin(); + iter != codecs.end(); ++iter) { + if (!VerifyCodec(*iter)) { + return false; + } + } + return true; +} + +bool VerifyVideoCodecs(const VideoContentDescription* video_desc) { + const std::vector& codecs = video_desc->codecs(); + for (std::vector::const_iterator iter = codecs.begin(); + iter != codecs.end(); ++iter) { + if (!VerifyCodec(*iter)) { + return false; + } + } + return true; +} + +void AddParameters(const cricket::CodecParameterMap& parameters, + cricket::Codec* codec) { + for (cricket::CodecParameterMap::const_iterator iter = + parameters.begin(); iter != parameters.end(); ++iter) { + codec->SetParam(iter->first, iter->second); + } +} + +void AddFeedbackParameter(const cricket::FeedbackParam& feedback_param, + cricket::Codec* codec) { + codec->AddFeedbackParam(feedback_param); +} + +void AddFeedbackParameters(const cricket::FeedbackParams& feedback_params, + cricket::Codec* codec) { + for (std::vector::const_iterator iter = + feedback_params.params().begin(); + iter != feedback_params.params().end(); ++iter) { + codec->AddFeedbackParam(*iter); + } +} + +// Gets the current codec setting associated with |payload_type|. If there +// is no AudioCodec associated with that payload type it returns an empty codec +// with that payload type. +template +T GetCodec(const std::vector& codecs, int payload_type) { + for (typename std::vector::const_iterator codec = codecs.begin(); + codec != codecs.end(); ++codec) { + if (codec->id == payload_type) { + return *codec; + } + } + T ret_val = T(); + ret_val.id = payload_type; + return ret_val; +} + +// Updates or creates a new codec entry in the audio description. +template +void AddOrReplaceCodec(MediaContentDescription* content_desc, const U& codec) { + T* desc = static_cast(content_desc); + std::vector codecs = desc->codecs(); + bool found = false; + + typename std::vector::iterator iter; + for (iter = codecs.begin(); iter != codecs.end(); ++iter) { + if (iter->id == codec.id) { + *iter = codec; + found = true; + break; + } + } + if (!found) { + desc->AddCodec(codec); + return; + } + desc->set_codecs(codecs); +} + +// Adds or updates existing codec corresponding to |payload_type| according +// to |parameters|. +template +void UpdateCodec(MediaContentDescription* content_desc, int payload_type, + const cricket::CodecParameterMap& parameters) { + // Codec might already have been populated (from rtpmap). + U new_codec = GetCodec(static_cast(content_desc)->codecs(), payload_type); + AddParameters(parameters, &new_codec); + AddOrReplaceCodec(content_desc, new_codec); +} + +// Adds or updates existing codec corresponding to |payload_type| according +// to |feedback_param|. +template +void UpdateCodec(MediaContentDescription* content_desc, int payload_type, + const cricket::FeedbackParam& feedback_param) { + // Codec might already have been populated (from rtpmap). + U new_codec = GetCodec(static_cast(content_desc)->codecs(), payload_type); + AddFeedbackParameter(feedback_param, &new_codec); + AddOrReplaceCodec(content_desc, new_codec); +} + +bool PopWildcardCodec(std::vector* codecs, + cricket::VideoCodec* wildcard_codec) { + for (std::vector::iterator iter = codecs->begin(); + iter != codecs->end(); ++iter) { + if (iter->id == kWildcardPayloadType) { + *wildcard_codec = *iter; + codecs->erase(iter); + return true; + } + } + return false; +} + +void UpdateFromWildcardVideoCodecs(VideoContentDescription* video_desc) { + std::vector codecs = video_desc->codecs(); + cricket::VideoCodec wildcard_codec; + if (!PopWildcardCodec(&codecs, &wildcard_codec)) { + return; + } + for (std::vector::iterator iter = codecs.begin(); + iter != codecs.end(); ++iter) { + cricket::VideoCodec& codec = *iter; + AddFeedbackParameters(wildcard_codec.feedback_params, &codec); + } + video_desc->set_codecs(codecs); +} + +void AddAudioAttribute(const std::string& name, const std::string& value, + AudioContentDescription* audio_desc) { + if (value.empty()) { + return; + } + std::vector codecs = audio_desc->codecs(); + for (std::vector::iterator iter = codecs.begin(); + iter != codecs.end(); ++iter) { + iter->params[name] = value; + } + audio_desc->set_codecs(codecs); +} + +bool ParseContent(const std::string& message, + const MediaType media_type, + int mline_index, + const std::string& protocol, + const std::vector& codec_preference, + size_t* pos, + std::string* content_name, + MediaContentDescription* media_desc, + TransportDescription* transport, + std::vector* candidates, + SdpParseError* error) { + ASSERT(media_desc != NULL); + ASSERT(content_name != NULL); + ASSERT(transport != NULL); + + // The media level "ice-ufrag" and "ice-pwd". + // The candidates before update the media level "ice-pwd" and "ice-ufrag". + Candidates candidates_orig; + std::string line; + std::string mline_id; + // Tracks created out of the ssrc attributes. + StreamParamsVec tracks; + SsrcInfoVec ssrc_infos; + SsrcGroupVec ssrc_groups; + std::string maxptime_as_string; + std::string ptime_as_string; + + bool is_rtp = + protocol.empty() || + talk_base::starts_with(protocol.data(), + cricket::kMediaProtocolRtpPrefix); + + // Loop until the next m line + while (!IsLineType(message, kLineTypeMedia, *pos)) { + if (!GetLine(message, pos, &line)) { + if (*pos >= message.size()) { + break; // Done parsing + } else { + return ParseFailed(message, *pos, "Can't find valid SDP line.", error); + } + } + + if (IsLineType(line, kLineTypeSessionBandwidth)) { + std::string bandwidth; + if (HasAttribute(line, kApplicationSpecificMaximum)) { + if (!GetValue(line, kApplicationSpecificMaximum, &bandwidth, error)) { + return false; + } else { + media_desc->set_bandwidth( + talk_base::FromString(bandwidth) * 1000); + } + } + continue; + } + + // RFC 4566 + // b=* (zero or more bandwidth information lines) + if (IsLineType(line, kLineTypeSessionBandwidth)) { + std::string bandwidth; + if (HasAttribute(line, kApplicationSpecificMaximum)) { + if (!GetValue(line, kApplicationSpecificMaximum, &bandwidth, error)) { + return false; + } else { + media_desc->set_bandwidth( + talk_base::FromString(bandwidth) * 1000); + } + } + continue; + } + + if (!IsLineType(line, kLineTypeAttributes)) { + // TODO: Handle other lines if needed. + LOG(LS_INFO) << "Ignored line: " << line; + continue; + } + + // Handle attributes common to SCTP and RTP. + if (HasAttribute(line, kAttributeMid)) { + // RFC 3388 + // mid-attribute = "a=mid:" identification-tag + // identification-tag = token + // Use the mid identification-tag as the content name. + if (!GetValue(line, kAttributeMid, &mline_id, error)) { + return false; + } + *content_name = mline_id; + } else if (HasAttribute(line, kAttributeCandidate)) { + Candidate candidate; + if (!ParseCandidate(line, &candidate, error, false)) { + return false; + } + candidates_orig.push_back(candidate); + } else if (HasAttribute(line, kAttributeIceUfrag)) { + if (!GetValue(line, kAttributeIceUfrag, &transport->ice_ufrag, error)) { + return false; + } + } else if (HasAttribute(line, kAttributeIcePwd)) { + if (!GetValue(line, kAttributeIcePwd, &transport->ice_pwd, error)) { + return false; + } + } else if (HasAttribute(line, kAttributeIceOption)) { + if (!ParseIceOptions(line, &transport->transport_options, error)) { + return false; + } + } else if (HasAttribute(line, kAttributeFmtp)) { + if (!ParseFmtpAttributes(line, media_type, media_desc, error)) { + return false; + } + } else if (HasAttribute(line, kAttributeFingerprint)) { + talk_base::SSLFingerprint* fingerprint = NULL; + + if (!ParseFingerprintAttribute(line, &fingerprint, error)) { + return false; + } + transport->identity_fingerprint.reset(fingerprint); + } else if (is_rtp) { + // + // RTP specific attrubtes + // + if (HasAttribute(line, kAttributeRtcpMux)) { + media_desc->set_rtcp_mux(true); + } else if (HasAttribute(line, kAttributeSsrcGroup)) { + if (!ParseSsrcGroupAttribute(line, &ssrc_groups, error)) { + return false; + } + } else if (HasAttribute(line, kAttributeSsrc)) { + if (!ParseSsrcAttribute(line, &ssrc_infos, error)) { + return false; + } + } else if (HasAttribute(line, kAttributeCrypto)) { + if (!ParseCryptoAttribute(line, media_desc, error)) { + return false; + } + } else if (HasAttribute(line, kAttributeRtpmap)) { + if (!ParseRtpmapAttribute(line, media_type, codec_preference, + media_desc, error)) { + return false; + } + } else if (HasAttribute(line, kCodecParamMaxPTime)) { + if (!GetValue(line, kCodecParamMaxPTime, &maxptime_as_string, error)) { + return false; + } + } else if (HasAttribute(line, kAttributeRtcpFb)) { + if (!ParseRtcpFbAttribute(line, media_type, media_desc, error)) { + return false; + } + } else if (HasAttribute(line, kCodecParamPTime)) { + if (!GetValue(line, kCodecParamPTime, &ptime_as_string, error)) { + return false; + } + } else if (HasAttribute(line, kAttributeSendOnly)) { + media_desc->set_direction(cricket::MD_SENDONLY); + } else if (HasAttribute(line, kAttributeRecvOnly)) { + media_desc->set_direction(cricket::MD_RECVONLY); + } else if (HasAttribute(line, kAttributeInactive)) { + media_desc->set_direction(cricket::MD_INACTIVE); + } else if (HasAttribute(line, kAttributeSendRecv)) { + media_desc->set_direction(cricket::MD_SENDRECV); + } else if (HasAttribute(line, kAttributeExtmap)) { + RtpHeaderExtension extmap; + if (!ParseExtmap(line, &extmap, error)) { + return false; + } + media_desc->AddRtpHeaderExtension(extmap); + } else if (HasAttribute(line, kAttributeXGoogleFlag)) { + // Experimental attribute. Conference mode activates more aggressive + // AEC and NS settings. + // TODO: expose API to set these directly. + std::string flag_value; + if (!GetValue(line, kAttributeXGoogleFlag, &flag_value, error)) { + return false; + } + if (flag_value.compare(kValueConference) == 0) + media_desc->set_conference_mode(true); + } else if (HasAttribute(line, kAttributeXGoogleBufferLatency)) { + // Experimental attribute. + // TODO: expose API to set this directly. + std::string flag_value; + if (!GetValue(line, kAttributeXGoogleBufferLatency, &flag_value, + error)) { + return false; + } + int buffer_latency = 0; + if (!talk_base::FromString(flag_value, &buffer_latency) || + buffer_latency < 0) { + return ParseFailed(message, "Invalid buffer latency.", error); + } + media_desc->set_buffered_mode_latency(buffer_latency); + } + } else { + // Only parse lines that we are interested of. + LOG(LS_INFO) << "Ignored line: " << line; + continue; + } + } + + // Create tracks from the |ssrc_infos|. + CreateTracksFromSsrcInfos(ssrc_infos, &tracks); + + // Add the ssrc group to the track. + for (SsrcGroupVec::iterator ssrc_group = ssrc_groups.begin(); + ssrc_group != ssrc_groups.end(); ++ssrc_group) { + if (ssrc_group->ssrcs.empty()) { + continue; + } + uint32 ssrc = ssrc_group->ssrcs.front(); + for (StreamParamsVec::iterator track = tracks.begin(); + track != tracks.end(); ++track) { + if (track->has_ssrc(ssrc)) { + track->ssrc_groups.push_back(*ssrc_group); + } + } + } + + // Add the new tracks to the |media_desc|. + for (StreamParamsVec::iterator track = tracks.begin(); + track != tracks.end(); ++track) { + media_desc->AddStream(*track); + } + + if (media_type == cricket::MEDIA_TYPE_AUDIO) { + AudioContentDescription* audio_desc = + static_cast(media_desc); + // Verify audio codec ensures that no audio codec has been populated with + // only fmtp. + if (!VerifyAudioCodecs(audio_desc)) { + return ParseFailed("Failed to parse audio codecs correctly.", error); + } + AddAudioAttribute(kCodecParamMaxPTime, maxptime_as_string, audio_desc); + AddAudioAttribute(kCodecParamPTime, ptime_as_string, audio_desc); + } + + if (media_type == cricket::MEDIA_TYPE_VIDEO) { + VideoContentDescription* video_desc = + static_cast(media_desc); + UpdateFromWildcardVideoCodecs(video_desc); + // Verify video codec ensures that no video codec has been populated with + // only rtcp-fb. + if (!VerifyVideoCodecs(video_desc)) { + return ParseFailed("Failed to parse video codecs correctly.", error); + } + } + + // RFC 5245 + // Update the candidates with the media level "ice-pwd" and "ice-ufrag". + for (Candidates::iterator it = candidates_orig.begin(); + it != candidates_orig.end(); ++it) { + ASSERT((*it).username().empty()); + (*it).set_username(transport->ice_ufrag); + ASSERT((*it).password().empty()); + (*it).set_password(transport->ice_pwd); + candidates->push_back( + new JsepIceCandidate(mline_id, mline_index, *it)); + } + return true; +} + +bool ParseSsrcAttribute(const std::string& line, SsrcInfoVec* ssrc_infos, + SdpParseError* error) { + ASSERT(ssrc_infos != NULL); + // RFC 5576 + // a=ssrc: + // a=ssrc: : + std::string field1, field2; + if (!SplitByDelimiter(line.substr(kLinePrefixLength), + kSdpDelimiterSpace, + &field1, + &field2)) { + const size_t expected_fields = 2; + return ParseFailedExpectFieldNum(line, expected_fields, error); + } + + // ssrc: + std::string ssrc_id_s; + if (!GetValue(field1, kAttributeSsrc, &ssrc_id_s, error)) { + return false; + } + uint32 ssrc_id = talk_base::FromString(ssrc_id_s); + + std::string attribute; + std::string value; + if (!SplitByDelimiter(field2, kSdpDelimiterColon, + &attribute, &value)) { + std::ostringstream description; + description << "Failed to get the ssrc attribute value from " << field2 + << ". Expected format :."; + return ParseFailed(line, description.str(), error); + } + + // Check if there's already an item for this |ssrc_id|. Create a new one if + // there isn't. + SsrcInfoVec::iterator ssrc_info = ssrc_infos->begin(); + for (; ssrc_info != ssrc_infos->end(); ++ssrc_info) { + if (ssrc_info->ssrc_id == ssrc_id) { + break; + } + } + if (ssrc_info == ssrc_infos->end()) { + SsrcInfo info; + info.ssrc_id = ssrc_id; + ssrc_infos->push_back(info); + ssrc_info = ssrc_infos->end() - 1; + } + + // Store the info to the |ssrc_info|. + if (attribute == kSsrcAttributeCname) { + // RFC 5576 + // cname: + ssrc_info->cname = value; + } else if (attribute == kSsrcAttributeMsid) { + // draft-alvestrand-mmusic-msid-00 + // "msid:" identifier [ " " appdata ] + std::vector fields; + talk_base::split(value, kSdpDelimiterSpace, &fields); + if (fields.size() < 1 || fields.size() > 2) { + return ParseFailed(line, + "Expected format \"msid:[ ]\".", + error); + } + ssrc_info->msid_identifier = fields[0]; + if (fields.size() == 2) { + ssrc_info->msid_appdata = fields[1]; + } + } else if (attribute == kSsrcAttributeMslabel) { + // draft-alvestrand-rtcweb-mid-01 + // mslabel: + ssrc_info->mslabel = value; + } else if (attribute == kSSrcAttributeLabel) { + // The label isn't defined. + // label: + ssrc_info->label = value; + } + return true; +} + +bool ParseSsrcGroupAttribute(const std::string& line, + SsrcGroupVec* ssrc_groups, + SdpParseError* error) { + ASSERT(ssrc_groups != NULL); + // RFC 5576 + // a=ssrc-group: ... + std::vector fields; + talk_base::split(line.substr(kLinePrefixLength), + kSdpDelimiterSpace, &fields); + const size_t expected_min_fields = 2; + if (fields.size() < expected_min_fields) { + return ParseFailedExpectMinFieldNum(line, expected_min_fields, error); + } + std::string semantics; + if (!GetValue(fields[0], kAttributeSsrcGroup, &semantics, error)) { + return false; + } + std::vector ssrcs; + for (size_t i = 1; i < fields.size(); ++i) { + uint32 ssrc = talk_base::FromString(fields[i]); + ssrcs.push_back(ssrc); + } + ssrc_groups->push_back(SsrcGroup(semantics, ssrcs)); + return true; +} + +bool ParseCryptoAttribute(const std::string& line, + MediaContentDescription* media_desc, + SdpParseError* error) { + std::vector fields; + talk_base::split(line.substr(kLinePrefixLength), + kSdpDelimiterSpace, &fields); + // RFC 4568 + // a=crypto: [] + const size_t expected_min_fields = 3; + if (fields.size() < expected_min_fields) { + return ParseFailedExpectMinFieldNum(line, expected_min_fields, error); + } + std::string tag_value; + if (!GetValue(fields[0], kAttributeCrypto, &tag_value, error)) { + return false; + } + int tag = talk_base::FromString(tag_value); + const std::string crypto_suite = fields[1]; + const std::string key_params = fields[2]; + std::string session_params; + if (fields.size() > 3) { + session_params = fields[3]; + } + media_desc->AddCrypto(CryptoParams(tag, crypto_suite, key_params, + session_params)); + return true; +} + +// Updates or creates a new codec entry in the audio description with according +// to |name|, |clockrate|, |bitrate|, |channels| and |preference|. +void UpdateCodec(int payload_type, const std::string& name, int clockrate, + int bitrate, int channels, int preference, + AudioContentDescription* audio_desc) { + // Codec may already be populated with (only) optional parameters + // (from an fmtp). + cricket::AudioCodec codec = GetCodec(audio_desc->codecs(), payload_type); + codec.name = name; + codec.clockrate = clockrate; + codec.bitrate = bitrate; + codec.channels = channels; + codec.preference = preference; + AddOrReplaceCodec(audio_desc, + codec); +} + +// Updates or creates a new codec entry in the video description according to +// |name|, |width|, |height|, |framerate| and |preference|. +void UpdateCodec(int payload_type, const std::string& name, int width, + int height, int framerate, int preference, + VideoContentDescription* video_desc) { + // Codec may already be populated with (only) optional parameters + // (from an fmtp). + cricket::VideoCodec codec = GetCodec(video_desc->codecs(), payload_type); + codec.name = name; + codec.width = width; + codec.height = height; + codec.framerate = framerate; + codec.preference = preference; + AddOrReplaceCodec(video_desc, + codec); +} + +bool ParseRtpmapAttribute(const std::string& line, + const MediaType media_type, + const std::vector& codec_preference, + MediaContentDescription* media_desc, + SdpParseError* error) { + std::vector fields; + talk_base::split(line.substr(kLinePrefixLength), + kSdpDelimiterSpace, &fields); + // RFC 4566 + // a=rtpmap: /[/] + const size_t expected_min_fields = 2; + if (fields.size() < expected_min_fields) { + return ParseFailedExpectMinFieldNum(line, expected_min_fields, error); + } + std::string payload_type_value; + if (!GetValue(fields[0], kAttributeRtpmap, &payload_type_value, error)) { + return false; + } + const int payload_type = talk_base::FromString(payload_type_value); + + // Set the preference order depending on the order of the pl type in the + // of the m-line. + const int preference = codec_preference.end() - + std::find(codec_preference.begin(), codec_preference.end(), + payload_type); + if (preference == 0) { + LOG(LS_WARNING) << "Ignore rtpmap line that did not appear in the " + << " of the m-line: " << line; + return true; + } + const std::string encoder = fields[1]; + std::vector codec_params; + talk_base::split(encoder, '/', &codec_params); + // /[/] + // 2 mandatory fields + if (codec_params.size() < 2 || codec_params.size() > 3) { + return ParseFailed(line, + "Expected format \"/" + "[/]\".", + error); + } + const std::string encoding_name = codec_params[0]; + const int clock_rate = talk_base::FromString(codec_params[1]); + if (media_type == cricket::MEDIA_TYPE_VIDEO) { + VideoContentDescription* video_desc = + static_cast(media_desc); + // TODO: We will send resolution in SDP. For now use + // JsepSessionDescription::kMaxVideoCodecWidth and kMaxVideoCodecHeight. + UpdateCodec(payload_type, encoding_name, + JsepSessionDescription::kMaxVideoCodecWidth, + JsepSessionDescription::kMaxVideoCodecHeight, + JsepSessionDescription::kDefaultVideoCodecFramerate, + preference, video_desc); + } else if (media_type == cricket::MEDIA_TYPE_AUDIO) { + // RFC 4566 + // For audio streams, indicates the number + // of audio channels. This parameter is OPTIONAL and may be + // omitted if the number of channels is one, provided that no + // additional parameters are needed. + int channels = 1; + if (codec_params.size() == 3) { + channels = talk_base::FromString(codec_params[2]); + } + int bitrate = 0; + // The default behavior for ISAC (bitrate == 0) in webrtcvoiceengine.cc + // (specifically FindWebRtcCodec) is bandwidth-adaptive variable bitrate. + // The bandwidth adaptation doesn't always work well, so this code + // sets a fixed target bitrate instead. + if (_stricmp(encoding_name.c_str(), kIsacCodecName) == 0) { + if (clock_rate <= 16000) { + bitrate = kIsacWbDefaultRate; + } else { + bitrate = kIsacSwbDefaultRate; + } + } + AudioContentDescription* audio_desc = + static_cast(media_desc); + UpdateCodec(payload_type, encoding_name, clock_rate, bitrate, channels, + preference, audio_desc); + } else if (media_type == cricket::MEDIA_TYPE_DATA) { + DataContentDescription* data_desc = + static_cast(media_desc); + data_desc->AddCodec(cricket::DataCodec(payload_type, encoding_name, + preference)); + } + return true; +} + +void PruneRight(const char delimiter, std::string* message) { + size_t trailing = message->find(delimiter); + if (trailing != std::string::npos) { + *message = message->substr(0, trailing); + } +} + +bool ParseFmtpParam(const std::string& line, std::string* parameter, + std::string* value, SdpParseError* error) { + if (!SplitByDelimiter(line, kSdpDelimiterEqual, parameter, value)) { + ParseFailed(line, "Unable to parse fmtp parameter. \'=\' missing.", error); + return false; + } + // a=fmtp: =; =; ... + // When parsing the values the trailing ";" gets picked up. Remove them. + PruneRight(kSdpDelimiterSemicolon, value); + return true; +} + +bool ParseFmtpAttributes(const std::string& line, const MediaType media_type, + MediaContentDescription* media_desc, + SdpParseError* error) { + if (media_type != cricket::MEDIA_TYPE_AUDIO && + media_type != cricket::MEDIA_TYPE_VIDEO) { + return true; + } + std::vector fields; + talk_base::split(line.substr(kLinePrefixLength), + kSdpDelimiterSpace, &fields); + + // RFC 5576 + // a=fmtp: + // At least two fields, whereas the second one is any of the optional + // parameters. + if (fields.size() < 2) { + ParseFailedExpectMinFieldNum(line, 2, error); + return false; + } + + std::string payload_type; + if (!GetValue(fields[0], kAttributeFmtp, &payload_type, error)) { + return false; + } + + cricket::CodecParameterMap codec_params; + for (std::vector::const_iterator iter = fields.begin() + 1; + iter != fields.end(); ++iter) { + std::string name; + std::string value; + if (iter->find(kSdpDelimiterEqual) == std::string::npos) { + // Only fmtps with equals are currently supported. Other fmtp types + // should be ignored. Unknown fmtps do not constitute an error. + continue; + } + if (!ParseFmtpParam(*iter, &name, &value, error)) { + return false; + } + codec_params[name] = value; + } + + int int_payload_type = talk_base::FromString(payload_type); + if (media_type == cricket::MEDIA_TYPE_AUDIO) { + UpdateCodec( + media_desc, int_payload_type, codec_params); + } else if (media_type == cricket::MEDIA_TYPE_VIDEO) { + UpdateCodec( + media_desc, int_payload_type, codec_params); + } + return true; +} + +bool ParseRtcpFbAttribute(const std::string& line, const MediaType media_type, + MediaContentDescription* media_desc, + SdpParseError* error) { + if (media_type != cricket::MEDIA_TYPE_AUDIO && + media_type != cricket::MEDIA_TYPE_VIDEO) { + return true; + } + std::vector rtcp_fb_fields; + talk_base::split(line.c_str(), kSdpDelimiterSpace, &rtcp_fb_fields); + if (rtcp_fb_fields.size() < 2) { + return ParseFailedGetValue(line, kAttributeRtcpFb, error); + } + std::string payload_type_string; + if (!GetValue(rtcp_fb_fields[0], kAttributeRtcpFb, &payload_type_string, + error)) { + return false; + } + int payload_type = (payload_type_string == "*") ? + kWildcardPayloadType : talk_base::FromString(payload_type_string); + std::string id = rtcp_fb_fields[1]; + std::string param = ""; + for (std::vector::iterator iter = rtcp_fb_fields.begin() + 2; + iter != rtcp_fb_fields.end(); ++iter) { + param.append(*iter); + } + const cricket::FeedbackParam feedback_param(id, param); + + if (media_type == cricket::MEDIA_TYPE_AUDIO) { + UpdateCodec(media_desc, + payload_type, + feedback_param); + } else if (media_type == cricket::MEDIA_TYPE_VIDEO) { + UpdateCodec(media_desc, + payload_type, + feedback_param); + } + return true; +} + +} // namespace webrtc diff --git a/talk/app/webrtc/webrtcsdp.h b/talk/app/webrtc/webrtcsdp.h new file mode 100644 index 000000000..c2f93a052 --- /dev/null +++ b/talk/app/webrtc/webrtcsdp.h @@ -0,0 +1,81 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +// This file contain functions for parsing and serializing SDP messages. +// Related RFC/draft including: +// * RFC 4566 - SDP +// * RFC 5245 - ICE +// * RFC 3388 - Grouping of Media Lines in SDP +// * RFC 4568 - SDP Security Descriptions for Media Streams +// * draft-lennox-mmusic-sdp-source-selection-02 - +// Mechanisms for Media Source Selection in SDP + +#ifndef TALK_APP_WEBRTC_WEBRTCSDP_H_ +#define TALK_APP_WEBRTC_WEBRTCSDP_H_ + +#include + +namespace webrtc { + +class IceCandidateInterface; +class JsepIceCandidate; +class JsepSessionDescription; +struct SdpParseError; + +// Serializes the passed in JsepSessionDescription. +// Serialize SessionDescription including candidates if +// JsepSessionDescription has candidates. +// jdesc - The JsepSessionDescription object to be serialized. +// return - SDP string serialized from the arguments. +std::string SdpSerialize(const JsepSessionDescription& jdesc); + +// Serializes the passed in IceCandidateInterface to a SDP string. +// candidate - The candidate to be serialized. +std::string SdpSerializeCandidate(const IceCandidateInterface& candidate); + +// Deserializes the passed in SDP string to a JsepSessionDescription. +// message - SDP string to be Deserialized. +// jdesc - The JsepSessionDescription deserialized from the SDP string. +// error - The detail error information when parsing fails. +// return - true on success, false on failure. +bool SdpDeserialize(const std::string& message, + JsepSessionDescription* jdesc, + SdpParseError* error); + +// Deserializes the passed in SDP string to one JsepIceCandidate. +// The first line must be a=candidate line and only the first line will be +// parsed. +// message - The SDP string to be Deserialized. +// candidates - The JsepIceCandidate from the SDP string. +// error - The detail error information when parsing fails. +// return - true on success, false on failure. +bool SdpDeserializeCandidate(const std::string& message, + JsepIceCandidate* candidate, + SdpParseError* error); +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_WEBRTCSDP_H_ diff --git a/talk/app/webrtc/webrtcsdp_unittest.cc b/talk/app/webrtc/webrtcsdp_unittest.cc new file mode 100644 index 000000000..9c3debd01 --- /dev/null +++ b/talk/app/webrtc/webrtcsdp_unittest.cc @@ -0,0 +1,1961 @@ +/* + * libjingle + * Copyright 2011, 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 +#include +#include + +#include "talk/app/webrtc/jsepsessiondescription.h" +#include "talk/app/webrtc/webrtcsdp.h" +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/messagedigest.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sslfingerprint.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" +#include "talk/media/base/constants.h" +#include "talk/p2p/base/constants.h" +#include "talk/session/media/mediasession.h" + +using cricket::AudioCodec; +using cricket::AudioContentDescription; +using cricket::Candidate; +using cricket::ContentInfo; +using cricket::CryptoParams; +using cricket::ContentGroup; +using cricket::DataCodec; +using cricket::DataContentDescription; +using cricket::ICE_CANDIDATE_COMPONENT_RTCP; +using cricket::ICE_CANDIDATE_COMPONENT_RTP; +using cricket::kFecSsrcGroupSemantics; +using cricket::LOCAL_PORT_TYPE; +using cricket::NS_JINGLE_DRAFT_SCTP; +using cricket::NS_JINGLE_ICE_UDP; +using cricket::NS_JINGLE_RTP; +using cricket::RtpHeaderExtension; +using cricket::RELAY_PORT_TYPE; +using cricket::SessionDescription; +using cricket::StreamParams; +using cricket::STUN_PORT_TYPE; +using cricket::TransportDescription; +using cricket::TransportInfo; +using cricket::VideoCodec; +using cricket::VideoContentDescription; +using webrtc::IceCandidateCollection; +using webrtc::IceCandidateInterface; +using webrtc::JsepIceCandidate; +using webrtc::JsepSessionDescription; +using webrtc::SdpParseError; +using webrtc::SessionDescriptionInterface; + +typedef std::vector AudioCodecs; +typedef std::vector Candidates; + +static const char kSessionTime[] = "t=0 0\r\n"; +static const uint32 kCandidatePriority = 2130706432U; // pref = 1.0 +static const char kCandidateUfragVoice[] = "ufrag_voice"; +static const char kCandidatePwdVoice[] = "pwd_voice"; +static const char kAttributeIcePwdVoice[] = "a=ice-pwd:pwd_voice\r\n"; +static const char kCandidateUfragVideo[] = "ufrag_video"; +static const char kCandidatePwdVideo[] = "pwd_video"; +static const char kCandidateUfragData[] = "ufrag_data"; +static const char kCandidatePwdData[] = "pwd_data"; +static const char kAttributeIcePwdVideo[] = "a=ice-pwd:pwd_video\r\n"; +static const uint32 kCandidateGeneration = 2; +static const char kCandidateFoundation1[] = "a0+B/1"; +static const char kCandidateFoundation2[] = "a0+B/2"; +static const char kCandidateFoundation3[] = "a0+B/3"; +static const char kCandidateFoundation4[] = "a0+B/4"; +static const char kAttributeCryptoVoice[] = + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n"; +static const char kAttributeCryptoVideo[] = + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n"; +static const char kFingerprint[] = "a=fingerprint:sha-1 " + "4A:AD:B9:B1:3F:82:18:3B:54:02:12:DF:3E:5D:49:6B:19:E5:7C:AB\r\n"; +static const int kExtmapId = 1; +static const char kExtmapUri[] = "http://example.com/082005/ext.htm#ttime"; +static const char kExtmap[] = + "a=extmap:1 http://example.com/082005/ext.htm#ttime\r\n"; +static const char kExtmapWithDirectionAndAttribute[] = + "a=extmap:1/sendrecv http://example.com/082005/ext.htm#ttime a1 a2\r\n"; + +static const uint8 kIdentityDigest[] = {0x4A, 0xAD, 0xB9, 0xB1, + 0x3F, 0x82, 0x18, 0x3B, + 0x54, 0x02, 0x12, 0xDF, + 0x3E, 0x5D, 0x49, 0x6B, + 0x19, 0xE5, 0x7C, 0xAB}; + +struct CodecParams { + int max_ptime; + int ptime; + int min_ptime; + int sprop_stereo; + int stereo; + int useinband; +}; + +// Reference sdp string +static const char kSdpFullString[] = + "v=0\r\n" + "o=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "a=msid-semantic: WMS local_stream_1 local_stream_2\r\n" + "m=audio 2345 RTP/SAVPF 111 103 104\r\n" + "c=IN IP4 74.125.127.126\r\n" + "a=rtcp:2347 IN IP4 74.125.127.126\r\n" + "a=candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1234 typ host " + "generation 2\r\n" + "a=candidate:a0+B/1 2 udp 2130706432 192.168.1.5 1235 typ host " + "generation 2\r\n" + "a=candidate:a0+B/2 1 udp 2130706432 ::1 1238 typ host " + "generation 2\r\n" + "a=candidate:a0+B/2 2 udp 2130706432 ::1 1239 typ host " + "generation 2\r\n" + "a=candidate:a0+B/3 1 udp 2130706432 74.125.127.126 2345 typ srflx " + "raddr 192.168.1.5 rport 2346 " + "generation 2\r\n" + "a=candidate:a0+B/3 2 udp 2130706432 74.125.127.126 2347 typ srflx " + "raddr 192.168.1.5 rport 2348 " + "generation 2\r\n" + "a=ice-ufrag:ufrag_voice\r\na=ice-pwd:pwd_voice\r\n" + "a=mid:audio_content_name\r\n" + "a=sendrecv\r\n" + "a=rtcp-mux\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=rtpmap:103 ISAC/16000\r\n" + "a=rtpmap:104 CELT/32000/2\r\n" + "a=ssrc:1 cname:stream_1_cname\r\n" + "a=ssrc:1 msid:local_stream_1 audio_track_id_1\r\n" + "a=ssrc:1 mslabel:local_stream_1\r\n" + "a=ssrc:1 label:audio_track_id_1\r\n" + "a=ssrc:4 cname:stream_2_cname\r\n" + "a=ssrc:4 msid:local_stream_2 audio_track_id_2\r\n" + "a=ssrc:4 mslabel:local_stream_2\r\n" + "a=ssrc:4 label:audio_track_id_2\r\n" + "m=video 3457 RTP/SAVPF 120\r\n" + "c=IN IP4 74.125.224.39\r\n" + "a=rtcp:3456 IN IP4 74.125.224.39\r\n" + "a=candidate:a0+B/1 2 udp 2130706432 192.168.1.5 1236 typ host " + "generation 2\r\n" + "a=candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1237 typ host " + "generation 2\r\n" + "a=candidate:a0+B/2 2 udp 2130706432 ::1 1240 typ host " + "generation 2\r\n" + "a=candidate:a0+B/2 1 udp 2130706432 ::1 1241 typ host " + "generation 2\r\n" + "a=candidate:a0+B/4 2 udp 2130706432 74.125.224.39 3456 typ relay " + "generation 2\r\n" + "a=candidate:a0+B/4 1 udp 2130706432 74.125.224.39 3457 typ relay " + "generation 2\r\n" + "a=ice-ufrag:ufrag_video\r\na=ice-pwd:pwd_video\r\n" + "a=mid:video_content_name\r\n" + "a=sendrecv\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n" + "a=rtpmap:120 VP8/90000\r\n" + "a=ssrc:2 cname:stream_1_cname\r\n" + "a=ssrc:2 msid:local_stream_1 video_track_id_1\r\n" + "a=ssrc:2 mslabel:local_stream_1\r\n" + "a=ssrc:2 label:video_track_id_1\r\n" + "a=ssrc:3 cname:stream_1_cname\r\n" + "a=ssrc:3 msid:local_stream_1 video_track_id_2\r\n" + "a=ssrc:3 mslabel:local_stream_1\r\n" + "a=ssrc:3 label:video_track_id_2\r\n" + "a=ssrc-group:FEC 5 6\r\n" + "a=ssrc:5 cname:stream_2_cname\r\n" + "a=ssrc:5 msid:local_stream_2 video_track_id_3\r\n" + "a=ssrc:5 mslabel:local_stream_2\r\n" + "a=ssrc:5 label:video_track_id_3\r\n" + "a=ssrc:6 cname:stream_2_cname\r\n" + "a=ssrc:6 msid:local_stream_2 video_track_id_3\r\n" + "a=ssrc:6 mslabel:local_stream_2\r\n" + "a=ssrc:6 label:video_track_id_3\r\n"; + +// SDP reference string without the candidates. +static const char kSdpString[] = + "v=0\r\n" + "o=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "a=msid-semantic: WMS local_stream_1 local_stream_2\r\n" + "m=audio 1 RTP/SAVPF 111 103 104\r\n" + "c=IN IP4 0.0.0.0\r\n" + "a=rtcp:1 IN IP4 0.0.0.0\r\n" + "a=ice-ufrag:ufrag_voice\r\na=ice-pwd:pwd_voice\r\n" + "a=mid:audio_content_name\r\n" + "a=sendrecv\r\n" + "a=rtcp-mux\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=rtpmap:103 ISAC/16000\r\n" + "a=rtpmap:104 CELT/32000/2\r\n" + "a=ssrc:1 cname:stream_1_cname\r\n" + "a=ssrc:1 msid:local_stream_1 audio_track_id_1\r\n" + "a=ssrc:1 mslabel:local_stream_1\r\n" + "a=ssrc:1 label:audio_track_id_1\r\n" + "a=ssrc:4 cname:stream_2_cname\r\n" + "a=ssrc:4 msid:local_stream_2 audio_track_id_2\r\n" + "a=ssrc:4 mslabel:local_stream_2\r\n" + "a=ssrc:4 label:audio_track_id_2\r\n" + "m=video 1 RTP/SAVPF 120\r\n" + "c=IN IP4 0.0.0.0\r\n" + "a=rtcp:1 IN IP4 0.0.0.0\r\n" + "a=ice-ufrag:ufrag_video\r\na=ice-pwd:pwd_video\r\n" + "a=mid:video_content_name\r\n" + "a=sendrecv\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n" + "a=rtpmap:120 VP8/90000\r\n" + "a=ssrc:2 cname:stream_1_cname\r\n" + "a=ssrc:2 msid:local_stream_1 video_track_id_1\r\n" + "a=ssrc:2 mslabel:local_stream_1\r\n" + "a=ssrc:2 label:video_track_id_1\r\n" + "a=ssrc:3 cname:stream_1_cname\r\n" + "a=ssrc:3 msid:local_stream_1 video_track_id_2\r\n" + "a=ssrc:3 mslabel:local_stream_1\r\n" + "a=ssrc:3 label:video_track_id_2\r\n" + "a=ssrc-group:FEC 5 6\r\n" + "a=ssrc:5 cname:stream_2_cname\r\n" + "a=ssrc:5 msid:local_stream_2 video_track_id_3\r\n" + "a=ssrc:5 mslabel:local_stream_2\r\n" + "a=ssrc:5 label:video_track_id_3\r\n" + "a=ssrc:6 cname:stream_2_cname\r\n" + "a=ssrc:6 msid:local_stream_2 video_track_id_3\r\n" + "a=ssrc:6 mslabel:local_stream_2\r\n" + "a=ssrc:6 label:video_track_id_3\r\n"; + +static const char kSdpRtpDataChannelString[] = + "m=application 1 RTP/SAVPF 101\r\n" + "c=IN IP4 0.0.0.0\r\n" + "a=rtcp:1 IN IP4 0.0.0.0\r\n" + "a=ice-ufrag:ufrag_data\r\n" + "a=ice-pwd:pwd_data\r\n" + "a=mid:data_content_name\r\n" + "a=sendrecv\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:FvLcvU2P3ZWmQxgPAgcDu7Zl9vftYElFOjEzhWs5\r\n" + "a=rtpmap:101 google-data/90000\r\n" + "a=ssrc:10 cname:data_channel_cname\r\n" + "a=ssrc:10 msid:data_channel data_channeld0\r\n" + "a=ssrc:10 mslabel:data_channel\r\n" + "a=ssrc:10 label:data_channeld0\r\n"; + +static const char kSdpSctpDataChannelString[] = + "m=application 1 DTLS/SCTP 5000\r\n" + "c=IN IP4 0.0.0.0\r\n" + "a=ice-ufrag:ufrag_data\r\n" + "a=ice-pwd:pwd_data\r\n" + "a=mid:data_content_name\r\n" + "a=fmtp:5000 protocol=webrtc-datachannel; streams=10\r\n"; + +static const char kSdpSctpDataChannelWithCandidatesString[] = + "m=application 2345 DTLS/SCTP 5000\r\n" + "c=IN IP4 74.125.127.126\r\n" + "a=candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1234 typ host " + "generation 2\r\n" + "a=candidate:a0+B/2 1 udp 2130706432 ::1 1238 typ host " + "generation 2\r\n" + "a=candidate:a0+B/3 1 udp 2130706432 74.125.127.126 2345 typ srflx " + "raddr 192.168.1.5 rport 2346 " + "generation 2\r\n" + "a=ice-ufrag:ufrag_data\r\n" + "a=ice-pwd:pwd_data\r\n" + "a=mid:data_content_name\r\n" + "a=fmtp:5000 protocol=webrtc-datachannel; streams=10\r\n"; + + +// One candidate reference string as per W3c spec. +// candidate: not a=candidate:CRLF +static const char kRawCandidate[] = + "candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1234 typ host generation 2"; +// One candidate reference string. +static const char kSdpOneCandidate[] = + "a=candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1234 typ host " + "generation 2\r\n"; + +// One candidate reference string. +static const char kSdpOneCandidateOldFormat[] = + "a=candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1234 typ host network_name" + " eth0 username user_rtp password password_rtp generation 2\r\n"; + +// Session id and version +static const char kSessionId[] = "18446744069414584320"; +static const char kSessionVersion[] = "18446462598732840960"; + +// Ice options +static const char kIceOption1[] = "iceoption1"; +static const char kIceOption2[] = "iceoption2"; +static const char kIceOption3[] = "iceoption3"; + +// Content name +static const char kAudioContentName[] = "audio_content_name"; +static const char kVideoContentName[] = "video_content_name"; +static const char kDataContentName[] = "data_content_name"; + +// MediaStream 1 +static const char kStreamLabel1[] = "local_stream_1"; +static const char kStream1Cname[] = "stream_1_cname"; +static const char kAudioTrackId1[] = "audio_track_id_1"; +static const uint32 kAudioTrack1Ssrc = 1; +static const char kVideoTrackId1[] = "video_track_id_1"; +static const uint32 kVideoTrack1Ssrc = 2; +static const char kVideoTrackId2[] = "video_track_id_2"; +static const uint32 kVideoTrack2Ssrc = 3; + +// MediaStream 2 +static const char kStreamLabel2[] = "local_stream_2"; +static const char kStream2Cname[] = "stream_2_cname"; +static const char kAudioTrackId2[] = "audio_track_id_2"; +static const uint32 kAudioTrack2Ssrc = 4; +static const char kVideoTrackId3[] = "video_track_id_3"; +static const uint32 kVideoTrack3Ssrc = 5; +static const uint32 kVideoTrack4Ssrc = 6; + +// DataChannel +static const char kDataChannelLabel[] = "data_channel"; +static const char kDataChannelMsid[] = "data_channeld0"; +static const char kDataChannelCname[] = "data_channel_cname"; +static const uint32 kDataChannelSsrc = 10; + +// Candidate +static const char kDummyMid[] = "dummy_mid"; +static const int kDummyIndex = 123; + +// Misc +static const char kDummyString[] = "dummy"; + +// Helper functions + +static bool SdpDeserialize(const std::string& message, + JsepSessionDescription* jdesc) { + return webrtc::SdpDeserialize(message, jdesc, NULL); +} + +static bool SdpDeserializeCandidate(const std::string& message, + JsepIceCandidate* candidate) { + return webrtc::SdpDeserializeCandidate(message, candidate, NULL); +} + +// Add some extra |newlines| to the |message| after |line|. +static void InjectAfter(const std::string& line, + const std::string& newlines, + std::string* message) { + const std::string tmp = line + newlines; + talk_base::replace_substrs(line.c_str(), line.length(), + tmp.c_str(), tmp.length(), message); +} + +static void Replace(const std::string& line, + const std::string& newlines, + std::string* message) { + talk_base::replace_substrs(line.c_str(), line.length(), + newlines.c_str(), newlines.length(), message); +} + +static void ReplaceAndTryToParse(const char* search, const char* replace) { + JsepSessionDescription desc(kDummyString); + std::string sdp = kSdpFullString; + Replace(search, replace, &sdp); + SdpParseError error; + bool ret = webrtc::SdpDeserialize(sdp, &desc, &error); + EXPECT_FALSE(ret); + EXPECT_NE(std::string::npos, error.line.find(replace)); +} + +static void ReplaceDirection(cricket::MediaContentDirection direction, + std::string* message) { + std::string new_direction; + switch (direction) { + case cricket::MD_INACTIVE: + new_direction = "a=inactive"; + break; + case cricket::MD_SENDONLY: + new_direction = "a=sendonly"; + break; + case cricket::MD_RECVONLY: + new_direction = "a=recvonly"; + break; + case cricket::MD_SENDRECV: + default: + new_direction = "a=sendrecv"; + break; + } + Replace("a=sendrecv", new_direction, message); +} + +static void ReplaceRejected(bool audio_rejected, bool video_rejected, + std::string* message) { + if (audio_rejected) { + Replace("m=audio 2345", "m=audio 0", message); + } + if (video_rejected) { + Replace("m=video 3457", "m=video 0", message); + } +} + +// WebRtcSdpTest + +class WebRtcSdpTest : public testing::Test { + public: + WebRtcSdpTest() + : jdesc_(kDummyString) { + // AudioContentDescription + audio_desc_ = CreateAudioContentDescription(); + AudioCodec opus(111, "opus", 48000, 0, 2, 3); + audio_desc_->AddCodec(opus); + audio_desc_->AddCodec(AudioCodec(103, "ISAC", 16000, 32000, 1, 2)); + audio_desc_->AddCodec(AudioCodec(104, "CELT", 32000, 0, 2, 1)); + desc_.AddContent(kAudioContentName, NS_JINGLE_RTP, audio_desc_); + + // VideoContentDescription + talk_base::scoped_ptr video( + new VideoContentDescription()); + video_desc_ = video.get(); + StreamParams video_stream1; + video_stream1.id = kVideoTrackId1; + video_stream1.cname = kStream1Cname; + video_stream1.sync_label = kStreamLabel1; + video_stream1.ssrcs.push_back(kVideoTrack1Ssrc); + video->AddStream(video_stream1); + StreamParams video_stream2; + video_stream2.id = kVideoTrackId2; + video_stream2.cname = kStream1Cname; + video_stream2.sync_label = kStreamLabel1; + video_stream2.ssrcs.push_back(kVideoTrack2Ssrc); + video->AddStream(video_stream2); + StreamParams video_stream3; + video_stream3.id = kVideoTrackId3; + video_stream3.cname = kStream2Cname; + video_stream3.sync_label = kStreamLabel2; + video_stream3.ssrcs.push_back(kVideoTrack3Ssrc); + video_stream3.ssrcs.push_back(kVideoTrack4Ssrc); + cricket::SsrcGroup ssrc_group(kFecSsrcGroupSemantics, video_stream3.ssrcs); + video_stream3.ssrc_groups.push_back(ssrc_group); + video->AddStream(video_stream3); + video->AddCrypto(CryptoParams(1, "AES_CM_128_HMAC_SHA1_80", + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32", "")); + video->set_protocol(cricket::kMediaProtocolSavpf); + video->AddCodec(VideoCodec( + 120, + JsepSessionDescription::kDefaultVideoCodecName, + JsepSessionDescription::kMaxVideoCodecWidth, + JsepSessionDescription::kMaxVideoCodecHeight, + JsepSessionDescription::kDefaultVideoCodecFramerate, + JsepSessionDescription::kDefaultVideoCodecPreference)); + + desc_.AddContent(kVideoContentName, NS_JINGLE_RTP, + video.release()); + + // TransportInfo + EXPECT_TRUE(desc_.AddTransportInfo( + TransportInfo(kAudioContentName, + TransportDescription(NS_JINGLE_ICE_UDP, + std::vector(), + kCandidateUfragVoice, + kCandidatePwdVoice, + cricket::ICEMODE_FULL, + NULL, Candidates())))); + EXPECT_TRUE(desc_.AddTransportInfo( + TransportInfo(kVideoContentName, + TransportDescription(NS_JINGLE_ICE_UDP, + std::vector(), + kCandidateUfragVideo, + kCandidatePwdVideo, + cricket::ICEMODE_FULL, + NULL, Candidates())))); + + // v4 host + int port = 1234; + talk_base::SocketAddress address("192.168.1.5", port++); + Candidate candidate1( + "", ICE_CANDIDATE_COMPONENT_RTP, "udp", address, kCandidatePriority, + "", "", LOCAL_PORT_TYPE, + "", kCandidateGeneration, kCandidateFoundation1); + address.SetPort(port++); + Candidate candidate2( + "", ICE_CANDIDATE_COMPONENT_RTCP, "udp", address, kCandidatePriority, + "", "", LOCAL_PORT_TYPE, + "", kCandidateGeneration, kCandidateFoundation1); + address.SetPort(port++); + Candidate candidate3( + "", ICE_CANDIDATE_COMPONENT_RTCP, "udp", address, kCandidatePriority, + "", "", LOCAL_PORT_TYPE, + "", kCandidateGeneration, kCandidateFoundation1); + address.SetPort(port++); + Candidate candidate4( + "", ICE_CANDIDATE_COMPONENT_RTP, "udp", address, kCandidatePriority, + "", "", LOCAL_PORT_TYPE, + "", kCandidateGeneration, kCandidateFoundation1); + + // v6 host + talk_base::SocketAddress v6_address("::1", port++); + cricket::Candidate candidate5( + "", cricket::ICE_CANDIDATE_COMPONENT_RTP, + "udp", v6_address, kCandidatePriority, + "", "", cricket::LOCAL_PORT_TYPE, + "", kCandidateGeneration, kCandidateFoundation2); + v6_address.SetPort(port++); + cricket::Candidate candidate6( + "", cricket::ICE_CANDIDATE_COMPONENT_RTCP, + "udp", v6_address, kCandidatePriority, + "", "", cricket::LOCAL_PORT_TYPE, + "", kCandidateGeneration, kCandidateFoundation2); + v6_address.SetPort(port++); + cricket::Candidate candidate7( + "", cricket::ICE_CANDIDATE_COMPONENT_RTCP, + "udp", v6_address, kCandidatePriority, + "", "", cricket::LOCAL_PORT_TYPE, + "", kCandidateGeneration, kCandidateFoundation2); + v6_address.SetPort(port++); + cricket::Candidate candidate8( + "", cricket::ICE_CANDIDATE_COMPONENT_RTP, + "udp", v6_address, kCandidatePriority, + "", "", cricket::LOCAL_PORT_TYPE, + "", kCandidateGeneration, kCandidateFoundation2); + + // stun + int port_stun = 2345; + talk_base::SocketAddress address_stun("74.125.127.126", port_stun++); + talk_base::SocketAddress rel_address_stun("192.168.1.5", port_stun++); + cricket::Candidate candidate9 + ("", cricket::ICE_CANDIDATE_COMPONENT_RTP, + "udp", address_stun, kCandidatePriority, + "", "", STUN_PORT_TYPE, + "", kCandidateGeneration, kCandidateFoundation3); + candidate9.set_related_address(rel_address_stun); + + address_stun.SetPort(port_stun++); + rel_address_stun.SetPort(port_stun++); + cricket::Candidate candidate10( + "", cricket::ICE_CANDIDATE_COMPONENT_RTCP, + "udp", address_stun, kCandidatePriority, + "", "", STUN_PORT_TYPE, + "", kCandidateGeneration, kCandidateFoundation3); + candidate10.set_related_address(rel_address_stun); + + // relay + int port_relay = 3456; + talk_base::SocketAddress address_relay("74.125.224.39", port_relay++); + cricket::Candidate candidate11( + "", cricket::ICE_CANDIDATE_COMPONENT_RTCP, + "udp", address_relay, kCandidatePriority, + "", "", + cricket::RELAY_PORT_TYPE, "", + kCandidateGeneration, kCandidateFoundation4); + address_relay.SetPort(port_relay++); + cricket::Candidate candidate12( + "", cricket::ICE_CANDIDATE_COMPONENT_RTP, + "udp", address_relay, kCandidatePriority, + "", "", + RELAY_PORT_TYPE, "", + kCandidateGeneration, kCandidateFoundation4); + + // voice + candidates_.push_back(candidate1); + candidates_.push_back(candidate2); + candidates_.push_back(candidate5); + candidates_.push_back(candidate6); + candidates_.push_back(candidate9); + candidates_.push_back(candidate10); + + // video + candidates_.push_back(candidate3); + candidates_.push_back(candidate4); + candidates_.push_back(candidate7); + candidates_.push_back(candidate8); + candidates_.push_back(candidate11); + candidates_.push_back(candidate12); + + jcandidate_.reset(new JsepIceCandidate(std::string("audio_content_name"), + 0, candidate1)); + + // Set up JsepSessionDescription. + jdesc_.Initialize(desc_.Copy(), kSessionId, kSessionVersion); + std::string mline_id; + int mline_index = 0; + for (size_t i = 0; i< candidates_.size(); ++i) { + // In this test, the audio m line index will be 0, and the video m line + // will be 1. + bool is_video = (i > 5); + mline_id = is_video ? "video_content_name" : "audio_content_name"; + mline_index = is_video ? 1 : 0; + JsepIceCandidate jice(mline_id, + mline_index, + candidates_.at(i)); + jdesc_.AddCandidate(&jice); + } + } + + AudioContentDescription* CreateAudioContentDescription() { + AudioContentDescription* audio = new AudioContentDescription(); + audio->set_rtcp_mux(true); + StreamParams audio_stream1; + audio_stream1.id = kAudioTrackId1; + audio_stream1.cname = kStream1Cname; + audio_stream1.sync_label = kStreamLabel1; + audio_stream1.ssrcs.push_back(kAudioTrack1Ssrc); + audio->AddStream(audio_stream1); + StreamParams audio_stream2; + audio_stream2.id = kAudioTrackId2; + audio_stream2.cname = kStream2Cname; + audio_stream2.sync_label = kStreamLabel2; + audio_stream2.ssrcs.push_back(kAudioTrack2Ssrc); + audio->AddStream(audio_stream2); + audio->AddCrypto(CryptoParams(1, "AES_CM_128_HMAC_SHA1_32", + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32", + "dummy_session_params")); + audio->set_protocol(cricket::kMediaProtocolSavpf); + return audio; + } + + template + void CompareMediaContentDescription(const MCD* cd1, + const MCD* cd2) { + // type + EXPECT_EQ(cd1->type(), cd1->type()); + + // content direction + EXPECT_EQ(cd1->direction(), cd2->direction()); + + // rtcp_mux + EXPECT_EQ(cd1->rtcp_mux(), cd2->rtcp_mux()); + + // cryptos + EXPECT_EQ(cd1->cryptos().size(), cd2->cryptos().size()); + if (cd1->cryptos().size() != cd2->cryptos().size()) { + ADD_FAILURE(); + return; + } + for (size_t i = 0; i< cd1->cryptos().size(); ++i) { + const CryptoParams c1 = cd1->cryptos().at(i); + const CryptoParams c2 = cd2->cryptos().at(i); + EXPECT_TRUE(c1.Matches(c2)); + EXPECT_EQ(c1.key_params, c2.key_params); + EXPECT_EQ(c1.session_params, c2.session_params); + } + // protocol + EXPECT_EQ(cd1->protocol(), cd2->protocol()); + + // codecs + EXPECT_EQ(cd1->codecs(), cd2->codecs()); + + // bandwidth + EXPECT_EQ(cd1->bandwidth(), cd2->bandwidth()); + + // streams + EXPECT_EQ(cd1->streams(), cd2->streams()); + + // extmap + ASSERT_EQ(cd1->rtp_header_extensions().size(), + cd2->rtp_header_extensions().size()); + for (size_t i = 0; i< cd1->rtp_header_extensions().size(); ++i) { + const RtpHeaderExtension ext1 = cd1->rtp_header_extensions().at(i); + const RtpHeaderExtension ext2 = cd2->rtp_header_extensions().at(i); + EXPECT_EQ(ext1.uri, ext2.uri); + EXPECT_EQ(ext1.id, ext2.id); + } + + // buffered mode latency + EXPECT_EQ(cd1->buffered_mode_latency(), cd2->buffered_mode_latency()); + } + + + void CompareSessionDescription(const SessionDescription& desc1, + const SessionDescription& desc2) { + // Compare content descriptions. + if (desc1.contents().size() != desc2.contents().size()) { + ADD_FAILURE(); + return; + } + for (size_t i = 0 ; i < desc1.contents().size(); ++i) { + const cricket::ContentInfo& c1 = desc1.contents().at(i); + const cricket::ContentInfo& c2 = desc2.contents().at(i); + // content name + EXPECT_EQ(c1.name, c2.name); + // content type + // Note, ASSERT will return from the function, but will not stop the test. + ASSERT_EQ(c1.type, c2.type); + + ASSERT_EQ(IsAudioContent(&c1), IsAudioContent(&c2)); + if (IsAudioContent(&c1)) { + const AudioContentDescription* acd1 = + static_cast(c1.description); + const AudioContentDescription* acd2 = + static_cast(c2.description); + CompareMediaContentDescription(acd1, acd2); + } + + ASSERT_EQ(IsVideoContent(&c1), IsVideoContent(&c2)); + if (IsVideoContent(&c1)) { + const VideoContentDescription* vcd1 = + static_cast(c1.description); + const VideoContentDescription* vcd2 = + static_cast(c2.description); + CompareMediaContentDescription(vcd1, vcd2); + } + + ASSERT_EQ(IsDataContent(&c1), IsDataContent(&c2)); + if (IsDataContent(&c1)) { + const DataContentDescription* dcd1 = + static_cast(c1.description); + const DataContentDescription* dcd2 = + static_cast(c2.description); + CompareMediaContentDescription(dcd1, dcd2); + } + } + + // group + const cricket::ContentGroups groups1 = desc1.groups(); + const cricket::ContentGroups groups2 = desc2.groups(); + EXPECT_EQ(groups1.size(), groups1.size()); + if (groups1.size() != groups2.size()) { + ADD_FAILURE(); + return; + } + for (size_t i = 0; i < groups1.size(); ++i) { + const cricket::ContentGroup group1 = groups1.at(i); + const cricket::ContentGroup group2 = groups2.at(i); + EXPECT_EQ(group1.semantics(), group2.semantics()); + const cricket::ContentNames names1 = group1.content_names(); + const cricket::ContentNames names2 = group2.content_names(); + EXPECT_EQ(names1.size(), names2.size()); + if (names1.size() != names2.size()) { + ADD_FAILURE(); + return; + } + cricket::ContentNames::const_iterator iter1 = names1.begin(); + cricket::ContentNames::const_iterator iter2 = names2.begin(); + while (iter1 != names1.end()) { + EXPECT_EQ(*iter1++, *iter2++); + } + } + + // transport info + const cricket::TransportInfos transports1 = desc1.transport_infos(); + const cricket::TransportInfos transports2 = desc2.transport_infos(); + EXPECT_EQ(transports1.size(), transports2.size()); + if (transports1.size() != transports2.size()) { + ADD_FAILURE(); + return; + } + for (size_t i = 0; i < transports1.size(); ++i) { + const cricket::TransportInfo transport1 = transports1.at(i); + const cricket::TransportInfo transport2 = transports2.at(i); + EXPECT_EQ(transport1.content_name, transport2.content_name); + EXPECT_EQ(transport1.description.transport_type, + transport2.description.transport_type); + EXPECT_EQ(transport1.description.ice_ufrag, + transport2.description.ice_ufrag); + EXPECT_EQ(transport1.description.ice_pwd, + transport2.description.ice_pwd); + if (transport1.description.identity_fingerprint) { + EXPECT_EQ(*transport1.description.identity_fingerprint, + *transport2.description.identity_fingerprint); + } else { + EXPECT_EQ(transport1.description.identity_fingerprint.get(), + transport2.description.identity_fingerprint.get()); + } + EXPECT_EQ(transport1.description.transport_options, + transport2.description.transport_options); + EXPECT_TRUE(CompareCandidates(transport1.description.candidates, + transport2.description.candidates)); + } + } + + bool CompareCandidates(const Candidates& cs1, const Candidates& cs2) { + EXPECT_EQ(cs1.size(), cs2.size()); + if (cs1.size() != cs2.size()) + return false; + for (size_t i = 0; i< cs1.size(); ++i) { + const Candidate c1 = cs1.at(i); + const Candidate c2 = cs2.at(i); + EXPECT_TRUE(c1.IsEquivalent(c2)); + } + return true; + } + + bool CompareSessionDescription( + const JsepSessionDescription& desc1, + const JsepSessionDescription& desc2) { + EXPECT_EQ(desc1.session_id(), desc2.session_id()); + EXPECT_EQ(desc1.session_version(), desc2.session_version()); + CompareSessionDescription(*desc1.description(), *desc2.description()); + if (desc1.number_of_mediasections() != desc2.number_of_mediasections()) + return false; + for (size_t i = 0; i < desc1.number_of_mediasections(); ++i) { + const IceCandidateCollection* cc1 = desc1.candidates(i); + const IceCandidateCollection* cc2 = desc2.candidates(i); + if (cc1->count() != cc2->count()) + return false; + for (size_t j = 0; j < cc1->count(); ++j) { + const IceCandidateInterface* c1 = cc1->at(j); + const IceCandidateInterface* c2 = cc2->at(j); + EXPECT_EQ(c1->sdp_mid(), c2->sdp_mid()); + EXPECT_EQ(c1->sdp_mline_index(), c2->sdp_mline_index()); + EXPECT_TRUE(c1->candidate().IsEquivalent(c2->candidate())); + } + } + return true; + } + + // Disable the ice-ufrag and ice-pwd in given |sdp| message by replacing + // them with invalid keywords so that the parser will just ignore them. + bool RemoveCandidateUfragPwd(std::string* sdp) { + const char ice_ufrag[] = "a=ice-ufrag"; + const char ice_ufragx[] = "a=xice-ufrag"; + const char ice_pwd[] = "a=ice-pwd"; + const char ice_pwdx[] = "a=xice-pwd"; + talk_base::replace_substrs(ice_ufrag, strlen(ice_ufrag), + ice_ufragx, strlen(ice_ufragx), sdp); + talk_base::replace_substrs(ice_pwd, strlen(ice_pwd), + ice_pwdx, strlen(ice_pwdx), sdp); + return true; + } + + // Update the candidates in |jdesc| to use the given |ufrag| and |pwd|. + bool UpdateCandidateUfragPwd(JsepSessionDescription* jdesc, int mline_index, + const std::string& ufrag, const std::string& pwd) { + std::string content_name; + if (mline_index == 0) { + content_name = kAudioContentName; + } else if (mline_index == 1) { + content_name = kVideoContentName; + } else { + ASSERT(false); + } + TransportInfo transport_info( + content_name, TransportDescription(NS_JINGLE_ICE_UDP, + std::vector(), + ufrag, pwd, cricket::ICEMODE_FULL, + NULL, Candidates())); + SessionDescription* desc = + const_cast(jdesc->description()); + desc->RemoveTransportInfoByName(content_name); + EXPECT_TRUE(desc->AddTransportInfo(transport_info)); + for (size_t i = 0; i < jdesc_.number_of_mediasections(); ++i) { + const IceCandidateCollection* cc = jdesc_.candidates(i); + for (size_t j = 0; j < cc->count(); ++j) { + if (cc->at(j)->sdp_mline_index() == mline_index) { + const_cast(cc->at(j)->candidate()).set_username( + ufrag); + const_cast(cc->at(j)->candidate()).set_password( + pwd); + } + } + } + return true; + } + + void AddIceOptions(const std::string& content_name, + const std::vector& transport_options) { + ASSERT_TRUE(desc_.GetTransportInfoByName(content_name) != NULL); + cricket::TransportInfo transport_info = + *(desc_.GetTransportInfoByName(content_name)); + desc_.RemoveTransportInfoByName(content_name); + transport_info.description.transport_options = transport_options; + desc_.AddTransportInfo(transport_info); + } + + void AddFingerprint() { + desc_.RemoveTransportInfoByName(kAudioContentName); + desc_.RemoveTransportInfoByName(kVideoContentName); + talk_base::SSLFingerprint fingerprint(talk_base::DIGEST_SHA_1, + kIdentityDigest, + sizeof(kIdentityDigest)); + EXPECT_TRUE(desc_.AddTransportInfo( + TransportInfo(kAudioContentName, + TransportDescription(NS_JINGLE_ICE_UDP, + std::vector(), + kCandidateUfragVoice, + kCandidatePwdVoice, + cricket::ICEMODE_FULL, &fingerprint, + Candidates())))); + EXPECT_TRUE(desc_.AddTransportInfo( + TransportInfo(kVideoContentName, + TransportDescription(NS_JINGLE_ICE_UDP, + std::vector(), + kCandidateUfragVideo, + kCandidatePwdVideo, + cricket::ICEMODE_FULL, &fingerprint, + Candidates())))); + } + + void AddExtmap() { + audio_desc_ = static_cast( + audio_desc_->Copy()); + video_desc_ = static_cast( + video_desc_->Copy()); + audio_desc_->AddRtpHeaderExtension( + RtpHeaderExtension(kExtmapUri, kExtmapId)); + video_desc_->AddRtpHeaderExtension( + RtpHeaderExtension(kExtmapUri, kExtmapId)); + desc_.RemoveContentByName(kAudioContentName); + desc_.RemoveContentByName(kVideoContentName); + desc_.AddContent(kAudioContentName, NS_JINGLE_RTP, audio_desc_); + desc_.AddContent(kVideoContentName, NS_JINGLE_RTP, video_desc_); + } + + void RemoveCryptos() { + audio_desc_->set_cryptos(std::vector()); + video_desc_->set_cryptos(std::vector()); + } + + bool TestSerializeDirection(cricket::MediaContentDirection direction) { + audio_desc_->set_direction(direction); + video_desc_->set_direction(direction); + std::string new_sdp = kSdpFullString; + ReplaceDirection(direction, &new_sdp); + + if (!jdesc_.Initialize(desc_.Copy(), + jdesc_.session_id(), + jdesc_.session_version())) { + return false; + } + std::string message = webrtc::SdpSerialize(jdesc_); + EXPECT_EQ(new_sdp, message); + return true; + } + + bool TestSerializeRejected(bool audio_rejected, bool video_rejected) { + audio_desc_ = static_cast( + audio_desc_->Copy()); + video_desc_ = static_cast( + video_desc_->Copy()); + desc_.RemoveContentByName(kAudioContentName); + desc_.RemoveContentByName(kVideoContentName); + desc_.AddContent(kAudioContentName, NS_JINGLE_RTP, audio_rejected, + audio_desc_); + desc_.AddContent(kVideoContentName, NS_JINGLE_RTP, video_rejected, + video_desc_); + std::string new_sdp = kSdpFullString; + ReplaceRejected(audio_rejected, video_rejected, &new_sdp); + + if (!jdesc_.Initialize(desc_.Copy(), + jdesc_.session_id(), + jdesc_.session_version())) { + return false; + } + std::string message = webrtc::SdpSerialize(jdesc_); + EXPECT_EQ(new_sdp, message); + return true; + } + + void AddSctpDataChannel() { + talk_base::scoped_ptr data( + new DataContentDescription()); + data_desc_ = data.get(); + data_desc_->set_protocol(cricket::kMediaProtocolDtlsSctp); + desc_.AddContent(kDataContentName, NS_JINGLE_DRAFT_SCTP, data.release()); + EXPECT_TRUE(desc_.AddTransportInfo( + TransportInfo(kDataContentName, + TransportDescription(NS_JINGLE_ICE_UDP, + std::vector(), + kCandidateUfragData, + kCandidatePwdData, + cricket::ICEMODE_FULL, + NULL, Candidates())))); + } + + void AddRtpDataChannel() { + talk_base::scoped_ptr data( + new DataContentDescription()); + data_desc_ = data.get(); + + data_desc_->AddCodec(DataCodec(101, "google-data", 1)); + StreamParams data_stream; + data_stream.id = kDataChannelMsid; + data_stream.cname = kDataChannelCname; + data_stream.sync_label = kDataChannelLabel; + data_stream.ssrcs.push_back(kDataChannelSsrc); + data_desc_->AddStream(data_stream); + data_desc_->AddCrypto(CryptoParams( + 1, "AES_CM_128_HMAC_SHA1_80", + "inline:FvLcvU2P3ZWmQxgPAgcDu7Zl9vftYElFOjEzhWs5", "")); + data_desc_->set_protocol(cricket::kMediaProtocolSavpf); + desc_.AddContent(kDataContentName, NS_JINGLE_RTP, data.release()); + EXPECT_TRUE(desc_.AddTransportInfo( + TransportInfo(kDataContentName, + TransportDescription(NS_JINGLE_ICE_UDP, + std::vector(), + kCandidateUfragData, + kCandidatePwdData, + cricket::ICEMODE_FULL, + NULL, Candidates())))); + } + + bool TestDeserializeDirection(cricket::MediaContentDirection direction) { + std::string new_sdp = kSdpFullString; + ReplaceDirection(direction, &new_sdp); + JsepSessionDescription new_jdesc(kDummyString); + + EXPECT_TRUE(SdpDeserialize(new_sdp, &new_jdesc)); + + audio_desc_->set_direction(direction); + video_desc_->set_direction(direction); + if (!jdesc_.Initialize(desc_.Copy(), + jdesc_.session_id(), + jdesc_.session_version())) { + return false; + } + EXPECT_TRUE(CompareSessionDescription(jdesc_, new_jdesc)); + return true; + } + + bool TestDeserializeRejected(bool audio_rejected, bool video_rejected) { + std::string new_sdp = kSdpFullString; + ReplaceRejected(audio_rejected, video_rejected, &new_sdp); + JsepSessionDescription new_jdesc(JsepSessionDescription::kOffer); + + EXPECT_TRUE(SdpDeserialize(new_sdp, &new_jdesc)); + audio_desc_ = static_cast( + audio_desc_->Copy()); + video_desc_ = static_cast( + video_desc_->Copy()); + desc_.RemoveContentByName(kAudioContentName); + desc_.RemoveContentByName(kVideoContentName); + desc_.AddContent(kAudioContentName, NS_JINGLE_RTP, audio_rejected, + audio_desc_); + desc_.AddContent(kVideoContentName, NS_JINGLE_RTP, video_rejected, + video_desc_); + if (!jdesc_.Initialize(desc_.Copy(), + jdesc_.session_id(), + jdesc_.session_version())) { + return false; + } + EXPECT_TRUE(CompareSessionDescription(jdesc_, new_jdesc)); + return true; + } + + void TestDeserializeExtmap(bool session_level, bool media_level) { + AddExtmap(); + JsepSessionDescription new_jdesc("dummy"); + ASSERT_TRUE(new_jdesc.Initialize(desc_.Copy(), + jdesc_.session_id(), + jdesc_.session_version())); + JsepSessionDescription jdesc_with_extmap("dummy"); + std::string sdp_with_extmap = kSdpString; + if (session_level) { + InjectAfter(kSessionTime, kExtmapWithDirectionAndAttribute, + &sdp_with_extmap); + } + if (media_level) { + InjectAfter(kAttributeIcePwdVoice, kExtmapWithDirectionAndAttribute, + &sdp_with_extmap); + InjectAfter(kAttributeIcePwdVideo, kExtmapWithDirectionAndAttribute, + &sdp_with_extmap); + } + // The extmap can't be present at the same time in both session level and + // media level. + if (session_level && media_level) { + SdpParseError error; + EXPECT_FALSE(webrtc::SdpDeserialize(sdp_with_extmap, + &jdesc_with_extmap, &error)); + EXPECT_NE(std::string::npos, error.description.find("a=extmap")); + } else { + EXPECT_TRUE(SdpDeserialize(sdp_with_extmap, &jdesc_with_extmap)); + EXPECT_TRUE(CompareSessionDescription(jdesc_with_extmap, new_jdesc)); + } + } + + void VerifyCodecParameter(const cricket::CodecParameterMap& params, + const std::string& name, int expected_value) { + cricket::CodecParameterMap::const_iterator found = params.find(name); + ASSERT_TRUE(found != params.end()); + EXPECT_EQ(found->second, talk_base::ToString(expected_value)); + } + + void TestDeserializeCodecParams(const CodecParams& params, + JsepSessionDescription* jdesc_output) { + std::string sdp = + "v=0\r\n" + "o=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + // Include semantics for WebRTC Media Streams since it is supported by + // this parser, and will be added to the SDP when serializing a session + // description. + "a=msid-semantic: WMS\r\n" + // Pl type 111 preferred. + "m=audio 1 RTP/SAVPF 111 104 103 102\r\n" + // Pltype 111 listed before 103 and 104 in the map. + "a=rtpmap:111 opus/48000/2\r\n" + // Pltype 103 listed before 104. + "a=rtpmap:103 ISAC/16000\r\n" + "a=rtpmap:104 CELT/32000/2\r\n" + "a=rtpmap:102 ISAC/32000/1\r\n" + "a=fmtp:111 0-15,66,70 "; + std::ostringstream os; + os << "minptime=" << params.min_ptime << " stereo=" << params.stereo + << " sprop-stereo=" << params.sprop_stereo + << " useinbandfec=" << params.useinband << "\r\n" + << "a=ptime:" << params.ptime << "\r\n" + << "a=maxptime:" << params.max_ptime << "\r\n"; + sdp += os.str(); + + // Deserialize + SdpParseError error; + EXPECT_TRUE(webrtc::SdpDeserialize(sdp, jdesc_output, &error)); + + const ContentInfo* ac = GetFirstAudioContent(jdesc_output->description()); + ASSERT_TRUE(ac != NULL); + const AudioContentDescription* acd = + static_cast(ac->description); + ASSERT_FALSE(acd->codecs().empty()); + cricket::AudioCodec opus = acd->codecs()[0]; + EXPECT_EQ("opus", opus.name); + EXPECT_EQ(111, opus.id); + VerifyCodecParameter(opus.params, "minptime", params.min_ptime); + VerifyCodecParameter(opus.params, "stereo", params.stereo); + VerifyCodecParameter(opus.params, "sprop-stereo", params.sprop_stereo); + VerifyCodecParameter(opus.params, "useinbandfec", params.useinband); + for (size_t i = 0; i < acd->codecs().size(); ++i) { + cricket::AudioCodec codec = acd->codecs()[i]; + VerifyCodecParameter(codec.params, "ptime", params.ptime); + VerifyCodecParameter(codec.params, "maxptime", params.max_ptime); + if (codec.name == "ISAC") { + if (codec.clockrate == 16000) { + EXPECT_EQ(32000, codec.bitrate); + } else { + EXPECT_EQ(56000, codec.bitrate); + } + } + } + } + + void TestDeserializeRtcpFb(JsepSessionDescription* jdesc_output, + bool use_wildcard) { + std::string sdp = + "v=0\r\n" + "o=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + // Include semantics for WebRTC Media Streams since it is supported by + // this parser, and will be added to the SDP when serializing a session + // description. + "a=msid-semantic: WMS\r\n" + "m=audio 1 RTP/SAVPF 111\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=rtcp-fb:111 nack\r\n" + "m=video 3457 RTP/SAVPF 101\r\n" + "a=rtpmap:101 VP8/90000\r\n" + "a=rtcp-fb:101 nack\r\n" + "a=rtcp-fb:101 goog-remb\r\n" + "a=rtcp-fb:101 ccm fir\r\n"; + std::ostringstream os; + os << "a=rtcp-fb:" << (use_wildcard ? "*" : "101") << " ccm fir\r\n"; + sdp += os.str(); + // Deserialize + SdpParseError error; + EXPECT_TRUE(webrtc::SdpDeserialize(sdp, jdesc_output, &error)); + const ContentInfo* ac = GetFirstAudioContent(jdesc_output->description()); + ASSERT_TRUE(ac != NULL); + const AudioContentDescription* acd = + static_cast(ac->description); + ASSERT_FALSE(acd->codecs().empty()); + cricket::AudioCodec opus = acd->codecs()[0]; + EXPECT_EQ(111, opus.id); + EXPECT_TRUE(opus.HasFeedbackParam( + cricket::FeedbackParam(cricket::kRtcpFbParamNack, + cricket::kParamValueEmpty))); + + const ContentInfo* vc = GetFirstVideoContent(jdesc_output->description()); + ASSERT_TRUE(vc != NULL); + const VideoContentDescription* vcd = + static_cast(vc->description); + ASSERT_FALSE(vcd->codecs().empty()); + cricket::VideoCodec vp8 = vcd->codecs()[0]; + EXPECT_STREQ(webrtc::JsepSessionDescription::kDefaultVideoCodecName, + vp8.name.c_str()); + EXPECT_EQ(101, vp8.id); + EXPECT_TRUE(vp8.HasFeedbackParam( + cricket::FeedbackParam(cricket::kRtcpFbParamNack, + cricket::kParamValueEmpty))); + EXPECT_TRUE(vp8.HasFeedbackParam( + cricket::FeedbackParam(cricket::kRtcpFbParamRemb, + cricket::kParamValueEmpty))); + EXPECT_TRUE(vp8.HasFeedbackParam( + cricket::FeedbackParam(cricket::kRtcpFbParamCcm, + cricket::kRtcpFbCcmParamFir))); + } + + // Two SDP messages can mean the same thing but be different strings, e.g. + // some of the lines can be serialized in different order. + // However, a deserialized description can be compared field by field and has + // no order. If deserializer has already been tested, serializing then + // deserializing and comparing JsepSessionDescription will test + // the serializer sufficiently. + void TestSerialize(const JsepSessionDescription& jdesc) { + std::string message = webrtc::SdpSerialize(jdesc); + JsepSessionDescription jdesc_output_des(kDummyString); + SdpParseError error; + EXPECT_TRUE(webrtc::SdpDeserialize(message, &jdesc_output_des, &error)); + EXPECT_TRUE(CompareSessionDescription(jdesc, jdesc_output_des)); + } + + protected: + SessionDescription desc_; + AudioContentDescription* audio_desc_; + VideoContentDescription* video_desc_; + DataContentDescription* data_desc_; + Candidates candidates_; + talk_base::scoped_ptr jcandidate_; + JsepSessionDescription jdesc_; +}; + +void TestMismatch(const std::string& string1, const std::string& string2) { + int position = 0; + for (size_t i = 0; i < string1.length() && i < string2.length(); ++i) { + if (string1.c_str()[i] != string2.c_str()[i]) { + position = i; + break; + } + } + EXPECT_EQ(0, position) << "Strings mismatch at the " << position + << " character\n" + << " 1: " << string1.substr(position, 20) << "\n" + << " 2: " << string2.substr(position, 20) << "\n"; +} + +std::string GetLine(const std::string& message, + const std::string& session_description_name) { + size_t start = message.find(session_description_name); + if (std::string::npos == start) { + return ""; + } + size_t stop = message.find("\r\n", start); + if (std::string::npos == stop) { + return ""; + } + if (stop <= start) { + return ""; + } + return message.substr(start, stop - start); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescription) { + // SessionDescription with desc and candidates. + std::string message = webrtc::SdpSerialize(jdesc_); + TestMismatch(std::string(kSdpFullString), message); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionEmpty) { + JsepSessionDescription jdesc_empty(kDummyString); + EXPECT_EQ("", webrtc::SdpSerialize(jdesc_empty)); +} + +// This tests serialization of SDP with a=crypto and a=fingerprint, as would be +// the case in a DTLS offer. +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithFingerprint) { + AddFingerprint(); + JsepSessionDescription jdesc_with_fingerprint(kDummyString); + ASSERT_TRUE(jdesc_with_fingerprint.Initialize(desc_.Copy(), + kSessionId, kSessionVersion)); + std::string message = webrtc::SdpSerialize(jdesc_with_fingerprint); + + std::string sdp_with_fingerprint = kSdpString; + InjectAfter(kAttributeIcePwdVoice, + kFingerprint, &sdp_with_fingerprint); + InjectAfter(kAttributeIcePwdVideo, + kFingerprint, &sdp_with_fingerprint); + + EXPECT_EQ(sdp_with_fingerprint, message); +} + +// This tests serialization of SDP with a=fingerprint with no a=crypto, as would +// be the case in a DTLS answer. +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithFingerprintNoCryptos) { + AddFingerprint(); + RemoveCryptos(); + JsepSessionDescription jdesc_with_fingerprint(kDummyString); + ASSERT_TRUE(jdesc_with_fingerprint.Initialize(desc_.Copy(), + kSessionId, kSessionVersion)); + std::string message = webrtc::SdpSerialize(jdesc_with_fingerprint); + + std::string sdp_with_fingerprint = kSdpString; + Replace(kAttributeCryptoVoice, "", &sdp_with_fingerprint); + Replace(kAttributeCryptoVideo, "", &sdp_with_fingerprint); + InjectAfter(kAttributeIcePwdVoice, + kFingerprint, &sdp_with_fingerprint); + InjectAfter(kAttributeIcePwdVideo, + kFingerprint, &sdp_with_fingerprint); + + EXPECT_EQ(sdp_with_fingerprint, message); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithoutCandidates) { + // JsepSessionDescription with desc but without candidates. + JsepSessionDescription jdesc_no_candidates(kDummyString); + ASSERT_TRUE(jdesc_no_candidates.Initialize(desc_.Copy(), + kSessionId, kSessionVersion)); + std::string message = webrtc::SdpSerialize(jdesc_no_candidates); + EXPECT_EQ(std::string(kSdpString), message); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithBundle) { + ContentGroup group(cricket::GROUP_TYPE_BUNDLE); + group.AddContentName(kAudioContentName); + group.AddContentName(kVideoContentName); + desc_.AddGroup(group); + ASSERT_TRUE(jdesc_.Initialize(desc_.Copy(), + jdesc_.session_id(), + jdesc_.session_version())); + std::string message = webrtc::SdpSerialize(jdesc_); + std::string sdp_with_bundle = kSdpFullString; + InjectAfter(kSessionTime, + "a=group:BUNDLE audio_content_name video_content_name\r\n", + &sdp_with_bundle); + EXPECT_EQ(sdp_with_bundle, message); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithBandwidth) { + VideoContentDescription* vcd = static_cast( + GetFirstVideoContent(&desc_)->description); + vcd->set_bandwidth(100 * 1000); + AudioContentDescription* acd = static_cast( + GetFirstAudioContent(&desc_)->description); + acd->set_bandwidth(50 * 1000); + ASSERT_TRUE(jdesc_.Initialize(desc_.Copy(), + jdesc_.session_id(), + jdesc_.session_version())); + std::string message = webrtc::SdpSerialize(jdesc_); + std::string sdp_with_bandwidth = kSdpFullString; + InjectAfter("a=mid:video_content_name\r\na=sendrecv\r\n", + "b=AS:100\r\n", + &sdp_with_bandwidth); + InjectAfter("a=mid:audio_content_name\r\na=sendrecv\r\n", + "b=AS:50\r\n", + &sdp_with_bandwidth); + EXPECT_EQ(sdp_with_bandwidth, message); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithIceOptions) { + std::vector transport_options; + transport_options.push_back(kIceOption1); + transport_options.push_back(kIceOption3); + AddIceOptions(kAudioContentName, transport_options); + transport_options.clear(); + transport_options.push_back(kIceOption2); + transport_options.push_back(kIceOption3); + AddIceOptions(kVideoContentName, transport_options); + ASSERT_TRUE(jdesc_.Initialize(desc_.Copy(), + jdesc_.session_id(), + jdesc_.session_version())); + std::string message = webrtc::SdpSerialize(jdesc_); + std::string sdp_with_ice_options = kSdpFullString; + InjectAfter(kAttributeIcePwdVoice, + "a=ice-options:iceoption1 iceoption3\r\n", + &sdp_with_ice_options); + InjectAfter(kAttributeIcePwdVideo, + "a=ice-options:iceoption2 iceoption3\r\n", + &sdp_with_ice_options); + EXPECT_EQ(sdp_with_ice_options, message); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithRecvOnlyContent) { + EXPECT_TRUE(TestSerializeDirection(cricket::MD_RECVONLY)); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithSendOnlyContent) { + EXPECT_TRUE(TestSerializeDirection(cricket::MD_SENDONLY)); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithInactiveContent) { + EXPECT_TRUE(TestSerializeDirection(cricket::MD_INACTIVE)); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithAudioRejected) { + EXPECT_TRUE(TestSerializeRejected(true, false)); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithVideoRejected) { + EXPECT_TRUE(TestSerializeRejected(false, true)); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithAudioVideoRejected) { + EXPECT_TRUE(TestSerializeRejected(true, true)); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithRtpDataChannel) { + AddRtpDataChannel(); + JsepSessionDescription jsep_desc(kDummyString); + + ASSERT_TRUE(jsep_desc.Initialize(desc_.Copy(), kSessionId, kSessionVersion)); + std::string message = webrtc::SdpSerialize(jsep_desc); + + std::string expected_sdp = kSdpString; + expected_sdp.append(kSdpRtpDataChannelString); + EXPECT_EQ(expected_sdp, message); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithSctpDataChannel) { + AddSctpDataChannel(); + JsepSessionDescription jsep_desc(kDummyString); + + ASSERT_TRUE(jsep_desc.Initialize(desc_.Copy(), kSessionId, kSessionVersion)); + std::string message = webrtc::SdpSerialize(jsep_desc); + + std::string expected_sdp = kSdpString; + expected_sdp.append(kSdpSctpDataChannelString); + EXPECT_EQ(message, expected_sdp); +} + +TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithExtmap) { + AddExtmap(); + JsepSessionDescription desc_with_extmap("dummy"); + ASSERT_TRUE(desc_with_extmap.Initialize(desc_.Copy(), + kSessionId, kSessionVersion)); + std::string message = webrtc::SdpSerialize(desc_with_extmap); + + std::string sdp_with_extmap = kSdpString; + InjectAfter("a=mid:audio_content_name\r\n", + kExtmap, &sdp_with_extmap); + InjectAfter("a=mid:video_content_name\r\n", + kExtmap, &sdp_with_extmap); + + EXPECT_EQ(sdp_with_extmap, message); +} + + +TEST_F(WebRtcSdpTest, SerializeCandidates) { + std::string message = webrtc::SdpSerializeCandidate(*jcandidate_); + EXPECT_EQ(std::string(kSdpOneCandidate), message); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescription) { + JsepSessionDescription jdesc(kDummyString); + // Deserialize + EXPECT_TRUE(SdpDeserialize(kSdpFullString, &jdesc)); + // Verify + EXPECT_TRUE(CompareSessionDescription(jdesc_, jdesc)); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithoutCarriageReturn) { + JsepSessionDescription jdesc(kDummyString); + std::string sdp_without_carriage_return = kSdpFullString; + Replace("\r\n", "\n", &sdp_without_carriage_return); + // Deserialize + EXPECT_TRUE(SdpDeserialize(sdp_without_carriage_return, &jdesc)); + // Verify + EXPECT_TRUE(CompareSessionDescription(jdesc_, jdesc)); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithoutCandidates) { + // SessionDescription with desc but without candidates. + JsepSessionDescription jdesc_no_candidates(kDummyString); + ASSERT_TRUE(jdesc_no_candidates.Initialize(desc_.Copy(), + kSessionId, kSessionVersion)); + JsepSessionDescription new_jdesc(kDummyString); + EXPECT_TRUE(SdpDeserialize(kSdpString, &new_jdesc)); + EXPECT_TRUE(CompareSessionDescription(jdesc_no_candidates, new_jdesc)); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithoutRtpmap) { + static const char kSdpNoRtpmapString[] = + "v=0\r\n" + "o=- 11 22 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "m=audio 49232 RTP/AVP 0 18 103\r\n" + // Codec that doesn't appear in the m= line will be ignored. + "a=rtpmap:104 CELT/32000/2\r\n" + // The rtpmap line for static payload codec is optional. + "a=rtpmap:18 G729/16000\r\n" + "a=rtpmap:103 ISAC/16000\r\n"; + + JsepSessionDescription jdesc(kDummyString); + EXPECT_TRUE(SdpDeserialize(kSdpNoRtpmapString, &jdesc)); + cricket::AudioContentDescription* audio = + static_cast( + jdesc.description()->GetContentDescriptionByName(cricket::CN_AUDIO)); + AudioCodecs ref_codecs; + // The codecs in the AudioContentDescription will be sorted by preference. + ref_codecs.push_back(AudioCodec(0, "PCMU", 8000, 0, 1, 3)); + ref_codecs.push_back(AudioCodec(18, "G729", 16000, 0, 1, 2)); + ref_codecs.push_back(AudioCodec(103, "ISAC", 16000, 32000, 1, 1)); + EXPECT_EQ(ref_codecs, audio->codecs()); +} + +// Ensure that we can deserialize SDP with a=fingerprint properly. +TEST_F(WebRtcSdpTest, DeserializeJsepSessionDescriptionWithFingerprint) { + // Add a DTLS a=fingerprint attribute to our session description. + AddFingerprint(); + JsepSessionDescription new_jdesc(kDummyString); + ASSERT_TRUE(new_jdesc.Initialize(desc_.Copy(), + jdesc_.session_id(), + jdesc_.session_version())); + + JsepSessionDescription jdesc_with_fingerprint(kDummyString); + std::string sdp_with_fingerprint = kSdpString; + InjectAfter(kAttributeIcePwdVoice, kFingerprint, &sdp_with_fingerprint); + InjectAfter(kAttributeIcePwdVideo, kFingerprint, &sdp_with_fingerprint); + EXPECT_TRUE(SdpDeserialize(sdp_with_fingerprint, &jdesc_with_fingerprint)); + EXPECT_TRUE(CompareSessionDescription(jdesc_with_fingerprint, new_jdesc)); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithBundle) { + JsepSessionDescription jdesc_with_bundle(kDummyString); + std::string sdp_with_bundle = kSdpFullString; + InjectAfter(kSessionTime, + "a=group:BUNDLE audio_content_name video_content_name\r\n", + &sdp_with_bundle); + EXPECT_TRUE(SdpDeserialize(sdp_with_bundle, &jdesc_with_bundle)); + ContentGroup group(cricket::GROUP_TYPE_BUNDLE); + group.AddContentName(kAudioContentName); + group.AddContentName(kVideoContentName); + desc_.AddGroup(group); + ASSERT_TRUE(jdesc_.Initialize(desc_.Copy(), + jdesc_.session_id(), + jdesc_.session_version())); + EXPECT_TRUE(CompareSessionDescription(jdesc_, jdesc_with_bundle)); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithBandwidth) { + JsepSessionDescription jdesc_with_bandwidth(kDummyString); + std::string sdp_with_bandwidth = kSdpFullString; + InjectAfter("a=mid:video_content_name\r\na=sendrecv\r\n", + "b=AS:100\r\n", + &sdp_with_bandwidth); + InjectAfter("a=mid:audio_content_name\r\na=sendrecv\r\n", + "b=AS:50\r\n", + &sdp_with_bandwidth); + EXPECT_TRUE( + SdpDeserialize(sdp_with_bandwidth, &jdesc_with_bandwidth)); + VideoContentDescription* vcd = static_cast( + GetFirstVideoContent(&desc_)->description); + vcd->set_bandwidth(100 * 1000); + AudioContentDescription* acd = static_cast( + GetFirstAudioContent(&desc_)->description); + acd->set_bandwidth(50 * 1000); + ASSERT_TRUE(jdesc_.Initialize(desc_.Copy(), + jdesc_.session_id(), + jdesc_.session_version())); + EXPECT_TRUE(CompareSessionDescription(jdesc_, jdesc_with_bandwidth)); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithIceOptions) { + JsepSessionDescription jdesc_with_ice_options(kDummyString); + std::string sdp_with_ice_options = kSdpFullString; + InjectAfter(kSessionTime, + "a=ice-options:iceoption3\r\n", + &sdp_with_ice_options); + InjectAfter(kAttributeIcePwdVoice, + "a=ice-options:iceoption1\r\n", + &sdp_with_ice_options); + InjectAfter(kAttributeIcePwdVideo, + "a=ice-options:iceoption2\r\n", + &sdp_with_ice_options); + EXPECT_TRUE(SdpDeserialize(sdp_with_ice_options, &jdesc_with_ice_options)); + std::vector transport_options; + transport_options.push_back(kIceOption3); + transport_options.push_back(kIceOption1); + AddIceOptions(kAudioContentName, transport_options); + transport_options.clear(); + transport_options.push_back(kIceOption3); + transport_options.push_back(kIceOption2); + AddIceOptions(kVideoContentName, transport_options); + ASSERT_TRUE(jdesc_.Initialize(desc_.Copy(), + jdesc_.session_id(), + jdesc_.session_version())); + EXPECT_TRUE(CompareSessionDescription(jdesc_, jdesc_with_ice_options)); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithUfragPwd) { + // Remove the original ice-ufrag and ice-pwd + JsepSessionDescription jdesc_with_ufrag_pwd(kDummyString); + std::string sdp_with_ufrag_pwd = kSdpFullString; + EXPECT_TRUE(RemoveCandidateUfragPwd(&sdp_with_ufrag_pwd)); + // Add session level ufrag and pwd + InjectAfter(kSessionTime, + "a=ice-pwd:session+level+icepwd\r\n" + "a=ice-ufrag:session+level+iceufrag\r\n", + &sdp_with_ufrag_pwd); + // Add media level ufrag and pwd for audio + InjectAfter("a=mid:audio_content_name\r\n", + "a=ice-pwd:media+level+icepwd\r\na=ice-ufrag:media+level+iceufrag\r\n", + &sdp_with_ufrag_pwd); + // Update the candidate ufrag and pwd to the expected ones. + EXPECT_TRUE(UpdateCandidateUfragPwd(&jdesc_, 0, + "media+level+iceufrag", "media+level+icepwd")); + EXPECT_TRUE(UpdateCandidateUfragPwd(&jdesc_, 1, + "session+level+iceufrag", "session+level+icepwd")); + EXPECT_TRUE(SdpDeserialize(sdp_with_ufrag_pwd, &jdesc_with_ufrag_pwd)); + EXPECT_TRUE(CompareSessionDescription(jdesc_, jdesc_with_ufrag_pwd)); +} + + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithRecvOnlyContent) { + EXPECT_TRUE(TestDeserializeDirection(cricket::MD_RECVONLY)); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithSendOnlyContent) { + EXPECT_TRUE(TestDeserializeDirection(cricket::MD_SENDONLY)); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithInactiveContent) { + EXPECT_TRUE(TestDeserializeDirection(cricket::MD_INACTIVE)); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithRejectedAudio) { + EXPECT_TRUE(TestDeserializeRejected(true, false)); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithRejectedVideo) { + EXPECT_TRUE(TestDeserializeRejected(false, true)); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithRejectedAudioVideo) { + EXPECT_TRUE(TestDeserializeRejected(true, true)); +} + +// Tests that we can still handle the sdp uses mslabel and label instead of +// msid for backward compatibility. +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithoutMsid) { + JsepSessionDescription jdesc(kDummyString); + std::string sdp_without_msid = kSdpFullString; + Replace("msid", "xmsid", &sdp_without_msid); + // Deserialize + EXPECT_TRUE(SdpDeserialize(sdp_without_msid, &jdesc)); + // Verify + EXPECT_TRUE(CompareSessionDescription(jdesc_, jdesc)); +} + +TEST_F(WebRtcSdpTest, DeserializeCandidate) { + JsepIceCandidate jcandidate(kDummyMid, kDummyIndex); + + std::string sdp = kSdpOneCandidate; + EXPECT_TRUE(SdpDeserializeCandidate(sdp, &jcandidate)); + EXPECT_EQ(kDummyMid, jcandidate.sdp_mid()); + EXPECT_EQ(kDummyIndex, jcandidate.sdp_mline_index()); + EXPECT_TRUE(jcandidate.candidate().IsEquivalent(jcandidate_->candidate())); + + // Candidate line without generation extension. + sdp = kSdpOneCandidate; + Replace(" generation 2", "", &sdp); + EXPECT_TRUE(SdpDeserializeCandidate(sdp, &jcandidate)); + EXPECT_EQ(kDummyMid, jcandidate.sdp_mid()); + EXPECT_EQ(kDummyIndex, jcandidate.sdp_mline_index()); + Candidate expected = jcandidate_->candidate(); + expected.set_generation(0); + EXPECT_TRUE(jcandidate.candidate().IsEquivalent(expected)); + + // Multiple candidate lines. + // Only the first line will be deserialized. The rest will be ignored. + sdp = kSdpOneCandidate; + sdp.append("a=candidate:1 2 tcp 1234 192.168.1.100 5678 typ host\r\n"); + EXPECT_TRUE(SdpDeserializeCandidate(sdp, &jcandidate)); + EXPECT_EQ(kDummyMid, jcandidate.sdp_mid()); + EXPECT_EQ(kDummyIndex, jcandidate.sdp_mline_index()); + EXPECT_TRUE(jcandidate.candidate().IsEquivalent(jcandidate_->candidate())); +} + +// This test verifies the deserialization of candidate-attribute +// as per RFC 5245. Candiate-attribute will be of the format +// candidate:. This format will be used when candidates +// are trickled. +TEST_F(WebRtcSdpTest, DeserializeRawCandidateAttribute) { + JsepIceCandidate jcandidate(kDummyMid, kDummyIndex); + + std::string candidate_attribute = kRawCandidate; + EXPECT_TRUE(SdpDeserializeCandidate(candidate_attribute, &jcandidate)); + EXPECT_EQ(kDummyMid, jcandidate.sdp_mid()); + EXPECT_EQ(kDummyIndex, jcandidate.sdp_mline_index()); + EXPECT_TRUE(jcandidate.candidate().IsEquivalent(jcandidate_->candidate())); + EXPECT_EQ(2u, jcandidate.candidate().generation()); + + // Candidate line without generation extension. + candidate_attribute = kRawCandidate; + Replace(" generation 2", "", &candidate_attribute); + EXPECT_TRUE(SdpDeserializeCandidate(candidate_attribute, &jcandidate)); + EXPECT_EQ(kDummyMid, jcandidate.sdp_mid()); + EXPECT_EQ(kDummyIndex, jcandidate.sdp_mline_index()); + Candidate expected = jcandidate_->candidate(); + expected.set_generation(0); + EXPECT_TRUE(jcandidate.candidate().IsEquivalent(expected)); + + // Candidate line without candidate: + candidate_attribute = kRawCandidate; + Replace("candidate:", "", &candidate_attribute); + EXPECT_FALSE(SdpDeserializeCandidate(candidate_attribute, &jcandidate)); + + // Concatenating additional candidate. Expecting deserialization to fail. + candidate_attribute = kRawCandidate; + candidate_attribute.append("candidate:1 2 udp 1234 192.168.1.1 typ host"); + EXPECT_FALSE(SdpDeserializeCandidate(candidate_attribute, &jcandidate)); +} + +TEST_F(WebRtcSdpTest, DeserializeSdpWithRtpDataChannels) { + AddRtpDataChannel(); + JsepSessionDescription jdesc(kDummyString); + ASSERT_TRUE(jdesc.Initialize(desc_.Copy(), kSessionId, kSessionVersion)); + + std::string sdp_with_data = kSdpString; + sdp_with_data.append(kSdpRtpDataChannelString); + JsepSessionDescription jdesc_output(kDummyString); + + // Deserialize + EXPECT_TRUE(SdpDeserialize(sdp_with_data, &jdesc_output)); + // Verify + EXPECT_TRUE(CompareSessionDescription(jdesc, jdesc_output)); +} + +TEST_F(WebRtcSdpTest, DeserializeSdpWithSctpDataChannels) { + AddSctpDataChannel(); + JsepSessionDescription jdesc(kDummyString); + ASSERT_TRUE(jdesc.Initialize(desc_.Copy(), kSessionId, kSessionVersion)); + + std::string sdp_with_data = kSdpString; + sdp_with_data.append(kSdpSctpDataChannelString); + JsepSessionDescription jdesc_output(kDummyString); + + EXPECT_TRUE(SdpDeserialize(sdp_with_data, &jdesc_output)); + EXPECT_TRUE(CompareSessionDescription(jdesc, jdesc_output)); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithSessionLevelExtmap) { + TestDeserializeExtmap(true, false); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithMediaLevelExtmap) { + TestDeserializeExtmap(false, true); +} + +TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithInvalidExtmap) { + TestDeserializeExtmap(true, true); +} + +TEST_F(WebRtcSdpTest, DeserializeCandidateWithDifferentTransport) { + JsepIceCandidate jcandidate(kDummyMid, kDummyIndex); + std::string new_sdp = kSdpOneCandidate; + Replace("udp", "unsupported_transport", &new_sdp); + EXPECT_FALSE(SdpDeserializeCandidate(new_sdp, &jcandidate)); + new_sdp = kSdpOneCandidate; + Replace("udp", "uDP", &new_sdp); + EXPECT_TRUE(SdpDeserializeCandidate(new_sdp, &jcandidate)); + EXPECT_EQ(kDummyMid, jcandidate.sdp_mid()); + EXPECT_EQ(kDummyIndex, jcandidate.sdp_mline_index()); + EXPECT_TRUE(jcandidate.candidate().IsEquivalent(jcandidate_->candidate())); +} + +TEST_F(WebRtcSdpTest, DeserializeCandidateOldFormat) { + JsepIceCandidate jcandidate(kDummyMid, kDummyIndex); + EXPECT_TRUE(SdpDeserializeCandidate(kSdpOneCandidateOldFormat,&jcandidate)); + EXPECT_EQ(kDummyMid, jcandidate.sdp_mid()); + EXPECT_EQ(kDummyIndex, jcandidate.sdp_mline_index()); + Candidate ref_candidate = jcandidate_->candidate(); + ref_candidate.set_username("user_rtp"); + ref_candidate.set_password("password_rtp"); + EXPECT_TRUE(jcandidate.candidate().IsEquivalent(ref_candidate)); +} + +TEST_F(WebRtcSdpTest, DeserializeBrokenSdp) { + const char kSdpDestroyer[] = "!@#$%^&"; + const char kSdpInvalidLine1[] = " =candidate"; + const char kSdpInvalidLine2[] = "a+candidate"; + const char kSdpInvalidLine3[] = "a= candidate"; + // Broken fingerprint. + const char kSdpInvalidLine4[] = "a=fingerprint:sha-1 " + "4AAD:B9:B1:3F:82:18:3B:54:02:12:DF:3E:5D:49:6B:19:E5:7C:AB"; + // Extra field. + const char kSdpInvalidLine5[] = "a=fingerprint:sha-1 " + "4A:AD:B9:B1:3F:82:18:3B:54:02:12:DF:3E:5D:49:6B:19:E5:7C:AB XXX"; + // Missing space. + const char kSdpInvalidLine6[] = "a=fingerprint:sha-1" + "4A:AD:B9:B1:3F:82:18:3B:54:02:12:DF:3E:5D:49:6B:19:E5:7C:AB"; + + // Broken session description + ReplaceAndTryToParse("v=", kSdpDestroyer); + ReplaceAndTryToParse("o=", kSdpDestroyer); + ReplaceAndTryToParse("s=-", kSdpDestroyer); + // Broken time description + ReplaceAndTryToParse("t=", kSdpDestroyer); + + // Broken media description + ReplaceAndTryToParse("m=video", kSdpDestroyer); + + // Invalid lines + ReplaceAndTryToParse("a=candidate", kSdpInvalidLine1); + ReplaceAndTryToParse("a=candidate", kSdpInvalidLine2); + ReplaceAndTryToParse("a=candidate", kSdpInvalidLine3); + + // Bogus fingerprint replacing a=sendrev. We selected this attribute + // because it's orthogonal to what we are replacing and hence + // safe. + ReplaceAndTryToParse("a=sendrecv", kSdpInvalidLine4); + ReplaceAndTryToParse("a=sendrecv", kSdpInvalidLine5); + ReplaceAndTryToParse("a=sendrecv", kSdpInvalidLine6); +} + +TEST_F(WebRtcSdpTest, DeserializeSdpWithReorderedPltypes) { + JsepSessionDescription jdesc_output(kDummyString); + + const char kSdpWithReorderedPlTypesString[] = + "v=0\r\n" + "o=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "m=audio 1 RTP/SAVPF 104 103\r\n" // Pl type 104 preferred. + "a=rtpmap:111 opus/48000/2\r\n" // Pltype 111 listed before 103 and 104 + // in the map. + "a=rtpmap:103 ISAC/16000\r\n" // Pltype 103 listed before 104 in the map. + "a=rtpmap:104 CELT/32000/2\r\n"; + + // Deserialize + EXPECT_TRUE(SdpDeserialize(kSdpWithReorderedPlTypesString, &jdesc_output)); + + const ContentInfo* ac = GetFirstAudioContent(jdesc_output.description()); + ASSERT_TRUE(ac != NULL); + const AudioContentDescription* acd = + static_cast(ac->description); + ASSERT_FALSE(acd->codecs().empty()); + EXPECT_EQ("CELT", acd->codecs()[0].name); + EXPECT_EQ(104, acd->codecs()[0].id); +} + +TEST_F(WebRtcSdpTest, DeserializeSerializeCodecParams) { + JsepSessionDescription jdesc_output(kDummyString); + CodecParams params; + params.max_ptime = 40; + params.ptime = 30; + params.min_ptime = 10; + params.sprop_stereo = 1; + params.stereo = 1; + params.useinband = 1; + TestDeserializeCodecParams(params, &jdesc_output); + TestSerialize(jdesc_output); +} + +TEST_F(WebRtcSdpTest, DeserializeSerializeRtcpFb) { + const bool kUseWildcard = false; + JsepSessionDescription jdesc_output(kDummyString); + TestDeserializeRtcpFb(&jdesc_output, kUseWildcard); + TestSerialize(jdesc_output); +} + +TEST_F(WebRtcSdpTest, DeserializeSerializeRtcpFbWildcard) { + const bool kUseWildcard = true; + JsepSessionDescription jdesc_output(kDummyString); + TestDeserializeRtcpFb(&jdesc_output, kUseWildcard); + TestSerialize(jdesc_output); +} + +TEST_F(WebRtcSdpTest, DeserializeVideoFmtp) { + JsepSessionDescription jdesc_output(kDummyString); + + const char kSdpWithFmtpString[] = + "v=0\r\n" + "o=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "m=video 3457 RTP/SAVPF 120\r\n" + "a=rtpmap:120 VP8/90000\r\n" + "a=fmtp:120 x-google-min-bitrate=10; x-google-max-quantization=40\r\n"; + + // Deserialize + SdpParseError error; + EXPECT_TRUE(webrtc::SdpDeserialize(kSdpWithFmtpString, &jdesc_output, + &error)); + + const ContentInfo* vc = GetFirstVideoContent(jdesc_output.description()); + ASSERT_TRUE(vc != NULL); + const VideoContentDescription* vcd = + static_cast(vc->description); + ASSERT_FALSE(vcd->codecs().empty()); + cricket::VideoCodec vp8 = vcd->codecs()[0]; + EXPECT_EQ("VP8", vp8.name); + EXPECT_EQ(120, vp8.id); + cricket::CodecParameterMap::iterator found = + vp8.params.find("x-google-min-bitrate"); + ASSERT_TRUE(found != vp8.params.end()); + EXPECT_EQ(found->second, "10"); + found = vp8.params.find("x-google-max-quantization"); + ASSERT_TRUE(found != vp8.params.end()); + EXPECT_EQ(found->second, "40"); +} + +TEST_F(WebRtcSdpTest, SerializeVideoFmtp) { + VideoContentDescription* vcd = static_cast( + GetFirstVideoContent(&desc_)->description); + + cricket::VideoCodecs codecs = vcd->codecs(); + codecs[0].params["x-google-min-bitrate"] = "10"; + vcd->set_codecs(codecs); + + ASSERT_TRUE(jdesc_.Initialize(desc_.Copy(), + jdesc_.session_id(), + jdesc_.session_version())); + std::string message = webrtc::SdpSerialize(jdesc_); + std::string sdp_with_fmtp = kSdpFullString; + InjectAfter("a=rtpmap:120 VP8/90000\r\n", + "a=fmtp:120 x-google-min-bitrate=10\r\n", + &sdp_with_fmtp); + EXPECT_EQ(sdp_with_fmtp, message); +} + +TEST_F(WebRtcSdpTest, DeserializeSdpWithIceLite) { + JsepSessionDescription jdesc_with_icelite(kDummyString); + std::string sdp_with_icelite = kSdpFullString; + EXPECT_TRUE(SdpDeserialize(sdp_with_icelite, &jdesc_with_icelite)); + cricket::SessionDescription* desc = jdesc_with_icelite.description(); + const cricket::TransportInfo* tinfo1 = + desc->GetTransportInfoByName("audio_content_name"); + EXPECT_EQ(cricket::ICEMODE_FULL, tinfo1->description.ice_mode); + const cricket::TransportInfo* tinfo2 = + desc->GetTransportInfoByName("video_content_name"); + EXPECT_EQ(cricket::ICEMODE_FULL, tinfo2->description.ice_mode); + InjectAfter(kSessionTime, + "a=ice-lite\r\n", + &sdp_with_icelite); + EXPECT_TRUE(SdpDeserialize(sdp_with_icelite, &jdesc_with_icelite)); + desc = jdesc_with_icelite.description(); + const cricket::TransportInfo* atinfo = + desc->GetTransportInfoByName("audio_content_name"); + EXPECT_EQ(cricket::ICEMODE_LITE, atinfo->description.ice_mode); + const cricket::TransportInfo* vtinfo = + desc->GetTransportInfoByName("video_content_name"); + EXPECT_EQ(cricket::ICEMODE_LITE, vtinfo->description.ice_mode); +} + +// Verifies that the candidates in the input SDP are parsed and serialized +// correctly in the output SDP. +TEST_F(WebRtcSdpTest, RoundTripSdpWithSctpDataChannelsWithCandidates) { + std::string sdp_with_data = kSdpString; + sdp_with_data.append(kSdpSctpDataChannelWithCandidatesString); + JsepSessionDescription jdesc_output(kDummyString); + + EXPECT_TRUE(SdpDeserialize(sdp_with_data, &jdesc_output)); + EXPECT_EQ(sdp_with_data, webrtc::SdpSerialize(jdesc_output)); +} diff --git a/talk/app/webrtc/webrtcsession.cc b/talk/app/webrtc/webrtcsession.cc new file mode 100644 index 000000000..fee8d42fc --- /dev/null +++ b/talk/app/webrtc/webrtcsession.cc @@ -0,0 +1,1440 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/webrtcsession.h" + +#include +#include +#include + +#include "talk/app/webrtc/jsepicecandidate.h" +#include "talk/app/webrtc/jsepsessiondescription.h" +#include "talk/app/webrtc/mediaconstraintsinterface.h" +#include "talk/app/webrtc/mediastreamsignaling.h" +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/stringencode.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/videocapturer.h" +#include "talk/session/media/channel.h" +#include "talk/session/media/channelmanager.h" +#include "talk/session/media/mediasession.h" + +using cricket::ContentInfo; +using cricket::ContentInfos; +using cricket::MediaContentDescription; +using cricket::SessionDescription; +using cricket::TransportInfo; + +typedef cricket::MediaSessionOptions::Stream Stream; +typedef cricket::MediaSessionOptions::Streams Streams; + +namespace webrtc { + +static const uint64 kInitSessionVersion = 2; + +const char kInternalConstraintPrefix[] = "internal"; + +// Supported MediaConstraints. +// DTLS-SRTP pseudo-constraints. +const char MediaConstraintsInterface::kEnableDtlsSrtp[] = + "DtlsSrtpKeyAgreement"; +// DataChannel pseudo constraints. +const char MediaConstraintsInterface::kEnableRtpDataChannels[] = + "RtpDataChannels"; +// This constraint is for internal use only, representing the Chrome command +// line flag. So it is prefixed with kInternalConstraintPrefix so JS values +// will be removed. +const char MediaConstraintsInterface::kEnableSctpDataChannels[] = + "internalSctpDataChannels"; + +// Arbitrary constant used as prefix for the identity. +// Chosen to make the certificates more readable. +const char kWebRTCIdentityPrefix[] = "WebRTC"; + +// Error messages +const char kSetLocalSdpFailed[] = "SetLocalDescription failed: "; +const char kSetRemoteSdpFailed[] = "SetRemoteDescription failed: "; +const char kCreateChannelFailed[] = "Failed to create channels."; +const char kInvalidCandidates[] = "Description contains invalid candidates."; +const char kInvalidSdp[] = "Invalid session description."; +const char kMlineMismatch[] = + "Offer and answer descriptions m-lines are not matching. " + "Rejecting answer."; +const char kSdpWithoutCrypto[] = "Called with a SDP without crypto enabled."; +const char kSessionError[] = "Session error code: "; +const char kUpdateStateFailed[] = "Failed to update session state: "; +const char kPushDownOfferTDFailed[] = + "Failed to push down offer transport description."; +const char kPushDownPranswerTDFailed[] = + "Failed to push down pranswer transport description."; +const char kPushDownAnswerTDFailed[] = + "Failed to push down answer transport description."; + +// Compares |answer| against |offer|. Comparision is done +// for number of m-lines in answer against offer. If matches true will be +// returned otherwise false. +static bool VerifyMediaDescriptions( + const SessionDescription* answer, const SessionDescription* offer) { + if (offer->contents().size() != answer->contents().size()) + return false; + + for (size_t i = 0; i < offer->contents().size(); ++i) { + if ((offer->contents()[i].name) != answer->contents()[i].name) { + return false; + } + } + return true; +} + +static void CopyCandidatesFromSessionDescription( + const SessionDescriptionInterface* source_desc, + SessionDescriptionInterface* dest_desc) { + if (!source_desc) + return; + for (size_t m = 0; m < source_desc->number_of_mediasections() && + m < dest_desc->number_of_mediasections(); ++m) { + const IceCandidateCollection* source_candidates = + source_desc->candidates(m); + const IceCandidateCollection* dest_candidates = dest_desc->candidates(m); + for (size_t n = 0; n < source_candidates->count(); ++n) { + const IceCandidateInterface* new_candidate = source_candidates->at(n); + if (!dest_candidates->HasCandidate(new_candidate)) + dest_desc->AddCandidate(source_candidates->at(n)); + } + } +} + +// Checks that each non-rejected content has SDES crypto keys or a DTLS +// fingerprint. Mismatches, such as replying with a DTLS fingerprint to SDES +// keys, will be caught in Transport negotiation, and backstopped by Channel's +// |secure_required| check. +static bool VerifyCrypto(const SessionDescription* desc) { + if (!desc) { + return false; + } + const ContentInfos& contents = desc->contents(); + for (size_t index = 0; index < contents.size(); ++index) { + const ContentInfo* cinfo = &contents[index]; + if (cinfo->rejected) { + continue; + } + + // If the content isn't rejected, crypto must be present. + const MediaContentDescription* media = + static_cast(cinfo->description); + const TransportInfo* tinfo = desc->GetTransportInfoByName(cinfo->name); + if (!media || !tinfo) { + // Something is not right. + LOG(LS_ERROR) << kInvalidSdp; + return false; + } + if (media->cryptos().empty() && + !tinfo->description.identity_fingerprint) { + // Crypto must be supplied. + LOG(LS_WARNING) << "Session description must have SDES or DTLS-SRTP."; + return false; + } + } + + return true; +} + +static bool CompareStream(const Stream& stream1, const Stream& stream2) { + return (stream1.id < stream2.id); +} + +static bool SameId(const Stream& stream1, const Stream& stream2) { + return (stream1.id == stream2.id); +} + +// Checks if each Stream within the |streams| has unique id. +static bool ValidStreams(const Streams& streams) { + Streams sorted_streams = streams; + std::sort(sorted_streams.begin(), sorted_streams.end(), CompareStream); + Streams::iterator it = + std::adjacent_find(sorted_streams.begin(), sorted_streams.end(), + SameId); + return (it == sorted_streams.end()); +} + +static bool GetAudioSsrcByTrackId( + const SessionDescription* session_description, + const std::string& track_id, uint32 *ssrc) { + const cricket::ContentInfo* audio_info = + cricket::GetFirstAudioContent(session_description); + if (!audio_info) { + LOG(LS_ERROR) << "Audio not used in this call"; + return false; + } + + const cricket::MediaContentDescription* audio_content = + static_cast( + audio_info->description); + cricket::StreamParams stream; + if (!cricket::GetStreamByIds(audio_content->streams(), "", track_id, + &stream)) { + return false; + } + *ssrc = stream.first_ssrc(); + return true; +} + +static bool GetTrackIdBySsrc(const SessionDescription* session_description, + uint32 ssrc, std::string* track_id) { + ASSERT(track_id != NULL); + + cricket::StreamParams stream_out; + const cricket::ContentInfo* audio_info = + cricket::GetFirstAudioContent(session_description); + if (!audio_info) { + return false; + } + const cricket::MediaContentDescription* audio_content = + static_cast( + audio_info->description); + + if (cricket::GetStreamBySsrc(audio_content->streams(), ssrc, &stream_out)) { + *track_id = stream_out.id; + return true; + } + + const cricket::ContentInfo* video_info = + cricket::GetFirstVideoContent(session_description); + if (!video_info) { + return false; + } + const cricket::MediaContentDescription* video_content = + static_cast( + video_info->description); + + if (cricket::GetStreamBySsrc(video_content->streams(), ssrc, &stream_out)) { + *track_id = stream_out.id; + return true; + } + return false; +} + +static bool BadSdp(const std::string& desc, std::string* err_desc) { + if (err_desc) { + *err_desc = desc; + } + LOG(LS_ERROR) << desc; + return false; +} + +static bool BadLocalSdp(const std::string& desc, std::string* err_desc) { + std::string set_local_sdp_failed = kSetLocalSdpFailed; + set_local_sdp_failed.append(desc); + return BadSdp(set_local_sdp_failed, err_desc); +} + +static bool BadRemoteSdp(const std::string& desc, std::string* err_desc) { + std::string set_remote_sdp_failed = kSetRemoteSdpFailed; + set_remote_sdp_failed.append(desc); + return BadSdp(set_remote_sdp_failed, err_desc); +} + +static bool BadSdp(cricket::ContentSource source, + const std::string& desc, std::string* err_desc) { + if (source == cricket::CS_LOCAL) { + return BadLocalSdp(desc, err_desc); + } else { + return BadRemoteSdp(desc, err_desc); + } +} + +static std::string SessionErrorMsg(cricket::BaseSession::Error error) { + std::ostringstream desc; + desc << kSessionError << error; + return desc.str(); +} + +#define GET_STRING_OF_STATE(state) \ + case cricket::BaseSession::state: \ + result = #state; \ + break; + +static std::string GetStateString(cricket::BaseSession::State state) { + std::string result; + switch (state) { + GET_STRING_OF_STATE(STATE_INIT) + GET_STRING_OF_STATE(STATE_SENTINITIATE) + GET_STRING_OF_STATE(STATE_RECEIVEDINITIATE) + GET_STRING_OF_STATE(STATE_SENTPRACCEPT) + GET_STRING_OF_STATE(STATE_SENTACCEPT) + GET_STRING_OF_STATE(STATE_RECEIVEDPRACCEPT) + GET_STRING_OF_STATE(STATE_RECEIVEDACCEPT) + GET_STRING_OF_STATE(STATE_SENTMODIFY) + GET_STRING_OF_STATE(STATE_RECEIVEDMODIFY) + GET_STRING_OF_STATE(STATE_SENTREJECT) + GET_STRING_OF_STATE(STATE_RECEIVEDREJECT) + GET_STRING_OF_STATE(STATE_SENTREDIRECT) + GET_STRING_OF_STATE(STATE_SENTTERMINATE) + GET_STRING_OF_STATE(STATE_RECEIVEDTERMINATE) + GET_STRING_OF_STATE(STATE_INPROGRESS) + GET_STRING_OF_STATE(STATE_DEINIT) + default: + ASSERT(false); + break; + } + return result; +} + +#define GET_STRING_OF_ERROR(err) \ + case cricket::BaseSession::err: \ + result = #err; \ + break; + +static std::string GetErrorString(cricket::BaseSession::Error err) { + std::string result; + switch (err) { + GET_STRING_OF_ERROR(ERROR_NONE) + GET_STRING_OF_ERROR(ERROR_TIME) + GET_STRING_OF_ERROR(ERROR_RESPONSE) + GET_STRING_OF_ERROR(ERROR_NETWORK) + GET_STRING_OF_ERROR(ERROR_CONTENT) + GET_STRING_OF_ERROR(ERROR_TRANSPORT) + default: + ASSERT(false); + break; + } + return result; +} + +static bool SetSessionStateFailed(cricket::ContentSource source, + cricket::BaseSession::Error err, + std::string* err_desc) { + std::string set_state_err = kUpdateStateFailed; + set_state_err.append(GetErrorString(err)); + return BadSdp(source, set_state_err, err_desc); +} + +// Help class used to remember if a a remote peer has requested ice restart by +// by sending a description with new ice ufrag and password. +class IceRestartAnswerLatch { + public: + IceRestartAnswerLatch() : ice_restart_(false) { } + + // Returns true if CheckForRemoteIceRestart has been called since last + // time this method was called with a new session description where + // ice password and ufrag has changed. + bool AnswerWithIceRestartLatch() { + if (ice_restart_) { + ice_restart_ = false; + return true; + } + return false; + } + + void CheckForRemoteIceRestart( + const SessionDescriptionInterface* old_desc, + const SessionDescriptionInterface* new_desc) { + if (!old_desc || new_desc->type() != SessionDescriptionInterface::kOffer) { + return; + } + const SessionDescription* new_sd = new_desc->description(); + const SessionDescription* old_sd = old_desc->description(); + const ContentInfos& contents = new_sd->contents(); + for (size_t index = 0; index < contents.size(); ++index) { + const ContentInfo* cinfo = &contents[index]; + if (cinfo->rejected) { + continue; + } + // If the content isn't rejected, check if ufrag and password has + // changed. + const cricket::TransportDescription* new_transport_desc = + new_sd->GetTransportDescriptionByName(cinfo->name); + const cricket::TransportDescription* old_transport_desc = + old_sd->GetTransportDescriptionByName(cinfo->name); + if (!new_transport_desc || !old_transport_desc) { + // No transport description exist. This is not an ice restart. + continue; + } + if (new_transport_desc->ice_pwd != old_transport_desc->ice_pwd && + new_transport_desc->ice_ufrag != old_transport_desc->ice_ufrag) { + LOG(LS_INFO) << "Remote peer request ice restart."; + ice_restart_ = true; + break; + } + } + } + + private: + bool ice_restart_; +}; + +WebRtcSession::WebRtcSession(cricket::ChannelManager* channel_manager, + talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread, + cricket::PortAllocator* port_allocator, + MediaStreamSignaling* mediastream_signaling) + : cricket::BaseSession(signaling_thread, worker_thread, port_allocator, + talk_base::ToString(talk_base::CreateRandomId64() & + LLONG_MAX), + cricket::NS_JINGLE_RTP, false), + // RFC 3264: The numeric value of the session id and version in the + // o line MUST be representable with a "64 bit signed integer". + // Due to this constraint session id |sid_| is max limited to LLONG_MAX. + channel_manager_(channel_manager), + session_desc_factory_(channel_manager, &transport_desc_factory_), + mediastream_signaling_(mediastream_signaling), + ice_observer_(NULL), + ice_connection_state_(PeerConnectionInterface::kIceConnectionNew), + // RFC 4566 suggested a Network Time Protocol (NTP) format timestamp + // as the session id and session version. To simplify, it should be fine + // to just use a random number as session id and start version from + // |kInitSessionVersion|. + session_version_(kInitSessionVersion), + older_version_remote_peer_(false), + data_channel_type_(cricket::DCT_NONE), + ice_restart_latch_(new IceRestartAnswerLatch) { + transport_desc_factory_.set_protocol(cricket::ICEPROTO_HYBRID); +} + +WebRtcSession::~WebRtcSession() { + if (voice_channel_.get()) { + SignalVoiceChannelDestroyed(); + channel_manager_->DestroyVoiceChannel(voice_channel_.release()); + } + if (video_channel_.get()) { + SignalVideoChannelDestroyed(); + channel_manager_->DestroyVideoChannel(video_channel_.release()); + } + if (data_channel_.get()) { + SignalDataChannelDestroyed(); + channel_manager_->DestroyDataChannel(data_channel_.release()); + } + for (size_t i = 0; i < saved_candidates_.size(); ++i) { + delete saved_candidates_[i]; + } + delete identity(); + set_identity(NULL); + transport_desc_factory_.set_identity(NULL); +} + +bool WebRtcSession::Initialize(const MediaConstraintsInterface* constraints) { + // TODO(perkj): Take |constraints| into consideration. Return false if not all + // mandatory constraints can be fulfilled. Note that |constraints| + // can be null. + + // By default SRTP-SDES is enabled in WebRtc. + set_secure_policy(cricket::SEC_REQUIRED); + + // Enable DTLS-SRTP if the constraint is set. + bool value; + if (FindConstraint(constraints, MediaConstraintsInterface::kEnableDtlsSrtp, + &value, NULL) && value) { + LOG(LS_INFO) << "DTLS-SRTP enabled; generating identity"; + std::string identity_name = kWebRTCIdentityPrefix + + talk_base::ToString(talk_base::CreateRandomId()); + transport_desc_factory_.set_identity(talk_base::SSLIdentity::Generate( + identity_name)); + LOG(LS_INFO) << "Finished generating identity"; + set_identity(transport_desc_factory_.identity()); + transport_desc_factory_.set_digest_algorithm(talk_base::DIGEST_SHA_256); + + transport_desc_factory_.set_secure(cricket::SEC_ENABLED); + } + + // Enable creation of RTP data channels if the kEnableRtpDataChannels is set. + // It takes precendence over the kEnableSctpDataChannels constraint. + if (FindConstraint( + constraints, MediaConstraintsInterface::kEnableRtpDataChannels, + &value, NULL) && value) { + LOG(LS_INFO) << "Allowing RTP data engine."; + data_channel_type_ = cricket::DCT_RTP; + } else if ( + FindConstraint( + constraints, + MediaConstraintsInterface::kEnableSctpDataChannels, + &value, NULL) && value && + // DTLS has to be enabled to use SCTP. + (transport_desc_factory_.secure() == cricket::SEC_ENABLED)) { + LOG(LS_INFO) << "Allowing SCTP data engine."; + data_channel_type_ = cricket::DCT_SCTP; + } + if (data_channel_type_ != cricket::DCT_NONE) { + mediastream_signaling_->SetDataChannelFactory(this); + } + + // Make sure SessionDescriptions only contains the StreamParams we negotiate. + session_desc_factory_.set_add_legacy_streams(false); + + const cricket::VideoCodec default_codec( + JsepSessionDescription::kDefaultVideoCodecId, + JsepSessionDescription::kDefaultVideoCodecName, + JsepSessionDescription::kMaxVideoCodecWidth, + JsepSessionDescription::kMaxVideoCodecHeight, + JsepSessionDescription::kDefaultVideoCodecFramerate, + JsepSessionDescription::kDefaultVideoCodecPreference); + channel_manager_->SetDefaultVideoEncoderConfig( + cricket::VideoEncoderConfig(default_codec)); + return true; +} + +void WebRtcSession::Terminate() { + SetState(STATE_RECEIVEDTERMINATE); + RemoveUnusedChannelsAndTransports(NULL); + ASSERT(voice_channel_.get() == NULL); + ASSERT(video_channel_.get() == NULL); + ASSERT(data_channel_.get() == NULL); +} + +bool WebRtcSession::StartCandidatesAllocation() { + // SpeculativelyConnectTransportChannels, will call ConnectChannels method + // from TransportProxy to start gathering ice candidates. + SpeculativelyConnectAllTransportChannels(); + if (!saved_candidates_.empty()) { + // If there are saved candidates which arrived before local description is + // set, copy those to remote description. + CopySavedCandidates(remote_desc_.get()); + } + // Push remote candidates present in remote description to transport channels. + UseCandidatesInSessionDescription(remote_desc_.get()); + return true; +} + +void WebRtcSession::set_secure_policy( + cricket::SecureMediaPolicy secure_policy) { + session_desc_factory_.set_secure(secure_policy); +} + +SessionDescriptionInterface* WebRtcSession::CreateOffer( + const MediaConstraintsInterface* constraints) { + cricket::MediaSessionOptions options; + + if (!mediastream_signaling_->GetOptionsForOffer(constraints, &options)) { + LOG(LS_ERROR) << "CreateOffer called with invalid constraints."; + return NULL; + } + + if (!ValidStreams(options.streams)) { + LOG(LS_ERROR) << "CreateOffer called with invalid media streams."; + return NULL; + } + + if (data_channel_type_ == cricket::DCT_SCTP) { + options.data_channel_type = cricket::DCT_SCTP; + } + SessionDescription* desc( + session_desc_factory_.CreateOffer(options, + BaseSession::local_description())); + // RFC 3264 + // When issuing an offer that modifies the session, + // the "o=" line of the new SDP MUST be identical to that in the + // previous SDP, except that the version in the origin field MUST + // increment by one from the previous SDP. + + // Just increase the version number by one each time when a new offer + // is created regardless if it's identical to the previous one or not. + // The |session_version_| is a uint64, the wrap around should not happen. + ASSERT(session_version_ + 1 > session_version_); + JsepSessionDescription* offer(new JsepSessionDescription( + JsepSessionDescription::kOffer)); + if (!offer->Initialize(desc, id(), + talk_base::ToString(session_version_++))) { + delete offer; + return NULL; + } + if (local_description() && !options.transport_options.ice_restart) { + // Include all local ice candidates in the SessionDescription unless + // the an ice restart has been requested. + CopyCandidatesFromSessionDescription(local_description(), offer); + } + return offer; +} + +SessionDescriptionInterface* WebRtcSession::CreateAnswer( + const MediaConstraintsInterface* constraints) { + if (!remote_description()) { + LOG(LS_ERROR) << "CreateAnswer can't be called before" + << " SetRemoteDescription."; + return NULL; + } + if (remote_description()->type() != JsepSessionDescription::kOffer) { + LOG(LS_ERROR) << "CreateAnswer failed because remote_description is not an" + << " offer."; + return NULL; + } + + cricket::MediaSessionOptions options; + if (!mediastream_signaling_->GetOptionsForAnswer(constraints, &options)) { + LOG(LS_ERROR) << "CreateAnswer called with invalid constraints."; + return NULL; + } + if (!ValidStreams(options.streams)) { + LOG(LS_ERROR) << "CreateAnswer called with invalid media streams."; + return NULL; + } + if (data_channel_type_ == cricket::DCT_SCTP) { + options.data_channel_type = cricket::DCT_SCTP; + } + // According to http://tools.ietf.org/html/rfc5245#section-9.2.1.1 + // an answer should also contain new ice ufrag and password if an offer has + // been received with new ufrag and password. + options.transport_options.ice_restart = + ice_restart_latch_->AnswerWithIceRestartLatch(); + SessionDescription* desc( + session_desc_factory_.CreateAnswer(BaseSession::remote_description(), + options, + BaseSession::local_description())); + // RFC 3264 + // If the answer is different from the offer in any way (different IP + // addresses, ports, etc.), the origin line MUST be different in the answer. + // In that case, the version number in the "o=" line of the answer is + // unrelated to the version number in the o line of the offer. + // Get a new version number by increasing the |session_version_answer_|. + // The |session_version_| is a uint64, the wrap around should not happen. + ASSERT(session_version_ + 1 > session_version_); + JsepSessionDescription* answer(new JsepSessionDescription( + JsepSessionDescription::kAnswer)); + if (!answer->Initialize(desc, id(), + talk_base::ToString(session_version_++))) { + delete answer; + return NULL; + } + if (local_description() && !options.transport_options.ice_restart) { + // Include all local ice candidates in the SessionDescription unless + // the remote peer has requested an ice restart. + CopyCandidatesFromSessionDescription(local_description(), answer); + } + return answer; +} + +bool WebRtcSession::SetLocalDescription(SessionDescriptionInterface* desc, + std::string* err_desc) { + if (error() != cricket::BaseSession::ERROR_NONE) { + delete desc; + return BadLocalSdp(SessionErrorMsg(error()), err_desc); + } + + if (!desc || !desc->description()) { + delete desc; + return BadLocalSdp(kInvalidSdp, err_desc); + } + Action action = GetAction(desc->type()); + if (!ExpectSetLocalDescription(action)) { + std::string type = desc->type(); + delete desc; + return BadLocalSdp(BadStateErrMsg(type, state()), err_desc); + } + + if (session_desc_factory_.secure() == cricket::SEC_REQUIRED && + !VerifyCrypto(desc->description())) { + delete desc; + return BadLocalSdp(kSdpWithoutCrypto, err_desc); + } + + if (action == kAnswer && !VerifyMediaDescriptions( + desc->description(), remote_description()->description())) { + return BadLocalSdp(kMlineMismatch, err_desc); + } + + // Update the initiator flag if this session is the initiator. + if (state() == STATE_INIT && action == kOffer) { + set_initiator(true); + } + + // Update the MediaContentDescription crypto settings as per the policy set. + UpdateSessionDescriptionSecurePolicy(desc->description()); + + set_local_description(desc->description()->Copy()); + local_desc_.reset(desc); + + // Transport and Media channels will be created only when offer is set. + if (action == kOffer && !CreateChannels(desc->description())) { + // TODO(mallinath) - Handle CreateChannel failure, as new local description + // is applied. Restore back to old description. + return BadLocalSdp(kCreateChannelFailed, err_desc); + } + + // Remove channel and transport proxies, if MediaContentDescription is + // rejected. + RemoveUnusedChannelsAndTransports(desc->description()); + + if (!UpdateSessionState(action, cricket::CS_LOCAL, + desc->description(), err_desc)) { + return false; + } + // Kick starting the ice candidates allocation. + StartCandidatesAllocation(); + + // Update state and SSRC of local MediaStreams and DataChannels based on the + // local session description. + mediastream_signaling_->OnLocalDescriptionChanged(local_desc_.get()); + + if (error() != cricket::BaseSession::ERROR_NONE) { + return BadLocalSdp(SessionErrorMsg(error()), err_desc); + } + return true; +} + +bool WebRtcSession::SetRemoteDescription(SessionDescriptionInterface* desc, + std::string* err_desc) { + if (error() != cricket::BaseSession::ERROR_NONE) { + delete desc; + return BadRemoteSdp(SessionErrorMsg(error()), err_desc); + } + + if (!desc || !desc->description()) { + delete desc; + return BadRemoteSdp(kInvalidSdp, err_desc); + } + Action action = GetAction(desc->type()); + if (!ExpectSetRemoteDescription(action)) { + std::string type = desc->type(); + delete desc; + return BadRemoteSdp(BadStateErrMsg(type, state()), err_desc); + } + + if (action == kAnswer && !VerifyMediaDescriptions( + desc->description(), local_description()->description())) { + return BadRemoteSdp(kMlineMismatch, err_desc); + } + + if (session_desc_factory_.secure() == cricket::SEC_REQUIRED && + !VerifyCrypto(desc->description())) { + delete desc; + return BadRemoteSdp(kSdpWithoutCrypto, err_desc); + } + + // Transport and Media channels will be created only when offer is set. + if (action == kOffer && !CreateChannels(desc->description())) { + // TODO(mallinath) - Handle CreateChannel failure, as new local description + // is applied. Restore back to old description. + return BadRemoteSdp(kCreateChannelFailed, err_desc); + } + + // Remove channel and transport proxies, if MediaContentDescription is + // rejected. + RemoveUnusedChannelsAndTransports(desc->description()); + + // NOTE: Candidates allocation will be initiated only when SetLocalDescription + // is called. + set_remote_description(desc->description()->Copy()); + if (!UpdateSessionState(action, cricket::CS_REMOTE, + desc->description(), err_desc)) { + return false; + } + + // Update remote MediaStreams. + mediastream_signaling_->OnRemoteDescriptionChanged(desc); + if (local_description() && !UseCandidatesInSessionDescription(desc)) { + delete desc; + return BadRemoteSdp(kInvalidCandidates, err_desc); + } + + // Copy all saved candidates. + CopySavedCandidates(desc); + // We retain all received candidates. + CopyCandidatesFromSessionDescription(remote_desc_.get(), desc); + // Check if this new SessionDescription contains new ice ufrag and password + // that indicates the remote peer requests ice restart. + ice_restart_latch_->CheckForRemoteIceRestart(remote_desc_.get(), + desc); + remote_desc_.reset(desc); + if (error() != cricket::BaseSession::ERROR_NONE) { + return BadRemoteSdp(SessionErrorMsg(error()), err_desc); + } + return true; +} + +bool WebRtcSession::UpdateSessionState( + Action action, cricket::ContentSource source, + const cricket::SessionDescription* desc, + std::string* err_desc) { + // If there's already a pending error then no state transition should happen. + // But all call-sites should be verifying this before calling us! + ASSERT(error() == cricket::BaseSession::ERROR_NONE); + if (action == kOffer) { + if (!PushdownTransportDescription(source, cricket::CA_OFFER)) { + return BadSdp(source, kPushDownOfferTDFailed, err_desc); + } + SetState(source == cricket::CS_LOCAL ? + STATE_SENTINITIATE : STATE_RECEIVEDINITIATE); + if (error() != cricket::BaseSession::ERROR_NONE) { + return SetSessionStateFailed(source, error(), err_desc); + } + } else if (action == kPrAnswer) { + if (!PushdownTransportDescription(source, cricket::CA_PRANSWER)) { + return BadSdp(source, kPushDownPranswerTDFailed, err_desc); + } + EnableChannels(); + SetState(source == cricket::CS_LOCAL ? + STATE_SENTPRACCEPT : STATE_RECEIVEDPRACCEPT); + if (error() != cricket::BaseSession::ERROR_NONE) { + return SetSessionStateFailed(source, error(), err_desc); + } + } else if (action == kAnswer) { + if (!PushdownTransportDescription(source, cricket::CA_ANSWER)) { + return BadSdp(source, kPushDownAnswerTDFailed, err_desc); + } + MaybeEnableMuxingSupport(); + EnableChannels(); + SetState(source == cricket::CS_LOCAL ? + STATE_SENTACCEPT : STATE_RECEIVEDACCEPT); + if (error() != cricket::BaseSession::ERROR_NONE) { + return SetSessionStateFailed(source, error(), err_desc); + } + } + return true; +} + +WebRtcSession::Action WebRtcSession::GetAction(const std::string& type) { + if (type == SessionDescriptionInterface::kOffer) { + return WebRtcSession::kOffer; + } else if (type == SessionDescriptionInterface::kPrAnswer) { + return WebRtcSession::kPrAnswer; + } else if (type == SessionDescriptionInterface::kAnswer) { + return WebRtcSession::kAnswer; + } + ASSERT(false && "unknown action type"); + return WebRtcSession::kOffer; +} + +bool WebRtcSession::ProcessIceMessage(const IceCandidateInterface* candidate) { + if (state() == STATE_INIT) { + LOG(LS_ERROR) << "ProcessIceMessage: ICE candidates can't be added " + << "without any offer (local or remote) " + << "session description."; + return false; + } + + if (!candidate) { + LOG(LS_ERROR) << "ProcessIceMessage: Candidate is NULL"; + return false; + } + + if (!local_description() || !remote_description()) { + LOG(LS_INFO) << "ProcessIceMessage: Remote description not set, " + << "save the candidate for later use."; + saved_candidates_.push_back( + new JsepIceCandidate(candidate->sdp_mid(), candidate->sdp_mline_index(), + candidate->candidate())); + return true; + } + + // Add this candidate to the remote session description. + if (!remote_desc_->AddCandidate(candidate)) { + LOG(LS_ERROR) << "ProcessIceMessage: Candidate cannot be used"; + return false; + } + + return UseCandidatesInSessionDescription(remote_desc_.get()); +} + +bool WebRtcSession::GetTrackIdBySsrc(uint32 ssrc, std::string* id) { + if (GetLocalTrackId(ssrc, id)) { + if (GetRemoteTrackId(ssrc, id)) { + LOG(LS_WARNING) << "SSRC " << ssrc + << " exists in both local and remote descriptions"; + return true; // We return the remote track id. + } + return true; + } else { + return GetRemoteTrackId(ssrc, id); + } +} + +bool WebRtcSession::GetLocalTrackId(uint32 ssrc, std::string* track_id) { + if (!BaseSession::local_description()) + return false; + return webrtc::GetTrackIdBySsrc( + BaseSession::local_description(), ssrc, track_id); +} + +bool WebRtcSession::GetRemoteTrackId(uint32 ssrc, std::string* track_id) { + if (!BaseSession::remote_description()) + return false; + return webrtc::GetTrackIdBySsrc( + BaseSession::remote_description(), ssrc, track_id); +} + +std::string WebRtcSession::BadStateErrMsg( + const std::string& type, State state) { + std::ostringstream desc; + desc << "Called with type in wrong state, " + << "type: " << type << " state: " << GetStateString(state); + return desc.str(); +} + +void WebRtcSession::SetAudioPlayout(uint32 ssrc, bool enable) { + ASSERT(signaling_thread()->IsCurrent()); + if (!voice_channel_) { + LOG(LS_ERROR) << "SetAudioPlayout: No audio channel exists."; + return; + } + if (!voice_channel_->SetOutputScaling(ssrc, enable ? 1 : 0, enable ? 1 : 0)) { + // Allow that SetOutputScaling fail if |enable| is false but assert + // otherwise. This in the normal case when the underlying media channel has + // already been deleted. + ASSERT(enable == false); + } +} + +void WebRtcSession::SetAudioSend(uint32 ssrc, bool enable, + const cricket::AudioOptions& options) { + ASSERT(signaling_thread()->IsCurrent()); + if (!voice_channel_) { + LOG(LS_ERROR) << "SetAudioSend: No audio channel exists."; + return; + } + if (!voice_channel_->MuteStream(ssrc, !enable)) { + // Allow that MuteStream fail if |enable| is false but assert otherwise. + // This in the normal case when the underlying media channel has already + // been deleted. + ASSERT(enable == false); + return; + } + if (enable) + voice_channel_->SetChannelOptions(options); +} + +bool WebRtcSession::SetAudioRenderer(uint32 ssrc, + cricket::AudioRenderer* renderer) { + if (!voice_channel_) { + LOG(LS_ERROR) << "SetAudioRenderer: No audio channel exists."; + return false; + } + + if (!voice_channel_->SetRenderer(ssrc, renderer)) { + // SetRenderer() can fail if the ssrc is not mapping to the playout channel. + LOG(LS_ERROR) << "SetAudioRenderer: ssrc is incorrect: " << ssrc; + return false; + } + + return true; +} + +bool WebRtcSession::SetCaptureDevice(uint32 ssrc, + cricket::VideoCapturer* camera) { + ASSERT(signaling_thread()->IsCurrent()); + + if (!video_channel_.get()) { + // |video_channel_| doesnt't exist. Probably because the remote end doesnt't + // support video. + LOG(LS_WARNING) << "Video not used in this call."; + return false; + } + if (!video_channel_->SetCapturer(ssrc, camera)) { + // Allow that SetCapturer fail if |camera| is NULL but assert otherwise. + // This in the normal case when the underlying media channel has already + // been deleted. + ASSERT(camera == NULL); + return false; + } + return true; +} + +void WebRtcSession::SetVideoPlayout(uint32 ssrc, + bool enable, + cricket::VideoRenderer* renderer) { + ASSERT(signaling_thread()->IsCurrent()); + if (!video_channel_) { + LOG(LS_WARNING) << "SetVideoPlayout: No video channel exists."; + return; + } + if (!video_channel_->SetRenderer(ssrc, enable ? renderer : NULL)) { + // Allow that SetRenderer fail if |renderer| is NULL but assert otherwise. + // This in the normal case when the underlying media channel has already + // been deleted. + ASSERT(renderer == NULL); + } +} + +void WebRtcSession::SetVideoSend(uint32 ssrc, bool enable, + const cricket::VideoOptions* options) { + ASSERT(signaling_thread()->IsCurrent()); + if (!video_channel_) { + LOG(LS_WARNING) << "SetVideoSend: No video channel exists."; + return; + } + if (!video_channel_->MuteStream(ssrc, !enable)) { + // Allow that MuteStream fail if |enable| is false but assert otherwise. + // This in the normal case when the underlying media channel has already + // been deleted. + ASSERT(enable == false); + return; + } + if (enable && options) + video_channel_->SetChannelOptions(*options); +} + +bool WebRtcSession::CanInsertDtmf(const std::string& track_id) { + ASSERT(signaling_thread()->IsCurrent()); + if (!voice_channel_) { + LOG(LS_ERROR) << "CanInsertDtmf: No audio channel exists."; + return false; + } + uint32 send_ssrc = 0; + // The Dtmf is negotiated per channel not ssrc, so we only check if the ssrc + // exists. + if (!GetAudioSsrcByTrackId(BaseSession::local_description(), track_id, + &send_ssrc)) { + LOG(LS_ERROR) << "CanInsertDtmf: Track does not exist: " << track_id; + return false; + } + return voice_channel_->CanInsertDtmf(); +} + +bool WebRtcSession::InsertDtmf(const std::string& track_id, + int code, int duration) { + ASSERT(signaling_thread()->IsCurrent()); + if (!voice_channel_) { + LOG(LS_ERROR) << "InsertDtmf: No audio channel exists."; + return false; + } + uint32 send_ssrc = 0; + if (!VERIFY(GetAudioSsrcByTrackId(BaseSession::local_description(), + track_id, &send_ssrc))) { + LOG(LS_ERROR) << "InsertDtmf: Track does not exist: " << track_id; + return false; + } + if (!voice_channel_->InsertDtmf(send_ssrc, code, duration, + cricket::DF_SEND)) { + LOG(LS_ERROR) << "Failed to insert DTMF to channel."; + return false; + } + return true; +} + +sigslot::signal0<>* WebRtcSession::GetOnDestroyedSignal() { + return &SignalVoiceChannelDestroyed; +} + +talk_base::scoped_refptr WebRtcSession::CreateDataChannel( + const std::string& label, + const DataChannelInit* config) { + if (state() == STATE_RECEIVEDTERMINATE) { + return NULL; + } + if (data_channel_type_ == cricket::DCT_NONE) { + LOG(LS_ERROR) << "CreateDataChannel: Data is not supported in this call."; + return NULL; + } + DataChannelInit new_config = config ? (*config) : DataChannelInit(); + + if (data_channel_type_ == cricket::DCT_SCTP) { + if (new_config.id < 0) { + if (!mediastream_signaling_->AllocateSctpId(&new_config.id)) { + LOG(LS_ERROR) << "No id can be allocated for the SCTP data channel."; + return NULL; + } + } else if (!mediastream_signaling_->IsSctpIdAvailable(new_config.id)) { + LOG(LS_ERROR) << "Failed to create a SCTP data channel " + << "because the id is already in use or out of range."; + return NULL; + } + } + talk_base::scoped_refptr channel( + DataChannel::Create(this, label, &new_config)); + if (channel == NULL) + return NULL; + if (!mediastream_signaling_->AddDataChannel(channel)) + return NULL; + return channel; +} + +cricket::DataChannelType WebRtcSession::data_channel_type() const { + return data_channel_type_; +} + +void WebRtcSession::SetIceConnectionState( + PeerConnectionInterface::IceConnectionState state) { + if (ice_connection_state_ == state) { + return; + } + + // ASSERT that the requested transition is allowed. Note that + // WebRtcSession does not implement "kIceConnectionClosed" (that is handled + // within PeerConnection). This switch statement should compile away when + // ASSERTs are disabled. + switch (ice_connection_state_) { + case PeerConnectionInterface::kIceConnectionNew: + ASSERT(state == PeerConnectionInterface::kIceConnectionChecking); + break; + case PeerConnectionInterface::kIceConnectionChecking: + ASSERT(state == PeerConnectionInterface::kIceConnectionFailed || + state == PeerConnectionInterface::kIceConnectionConnected); + break; + case PeerConnectionInterface::kIceConnectionConnected: + ASSERT(state == PeerConnectionInterface::kIceConnectionDisconnected || + state == PeerConnectionInterface::kIceConnectionChecking || + state == PeerConnectionInterface::kIceConnectionCompleted); + break; + case PeerConnectionInterface::kIceConnectionCompleted: + ASSERT(state == PeerConnectionInterface::kIceConnectionConnected || + state == PeerConnectionInterface::kIceConnectionDisconnected); + break; + case PeerConnectionInterface::kIceConnectionFailed: + ASSERT(state == PeerConnectionInterface::kIceConnectionNew); + break; + case PeerConnectionInterface::kIceConnectionDisconnected: + ASSERT(state == PeerConnectionInterface::kIceConnectionChecking || + state == PeerConnectionInterface::kIceConnectionConnected || + state == PeerConnectionInterface::kIceConnectionCompleted || + state == PeerConnectionInterface::kIceConnectionFailed); + break; + case PeerConnectionInterface::kIceConnectionClosed: + ASSERT(false); + break; + default: + ASSERT(false); + break; + } + + ice_connection_state_ = state; + if (ice_observer_) { + ice_observer_->OnIceConnectionChange(ice_connection_state_); + } +} + +void WebRtcSession::OnTransportRequestSignaling( + cricket::Transport* transport) { + ASSERT(signaling_thread()->IsCurrent()); + transport->OnSignalingReady(); + if (ice_observer_) { + ice_observer_->OnIceGatheringChange( + PeerConnectionInterface::kIceGatheringGathering); + } +} + +void WebRtcSession::OnTransportConnecting(cricket::Transport* transport) { + ASSERT(signaling_thread()->IsCurrent()); + // start monitoring for the write state of the transport. + OnTransportWritable(transport); +} + +void WebRtcSession::OnTransportWritable(cricket::Transport* transport) { + ASSERT(signaling_thread()->IsCurrent()); + // TODO(bemasc): Expose more API from Transport to detect when + // candidate selection starts or stops, due to success or failure. + if (transport->all_channels_writable()) { + if (ice_connection_state_ == + PeerConnectionInterface::kIceConnectionChecking || + ice_connection_state_ == + PeerConnectionInterface::kIceConnectionDisconnected) { + SetIceConnectionState(PeerConnectionInterface::kIceConnectionConnected); + } + } else if (transport->HasChannels()) { + // If the current state is Connected or Completed, then there were writable + // channels but now there are not, so the next state must be Disconnected. + if (ice_connection_state_ == + PeerConnectionInterface::kIceConnectionConnected || + ice_connection_state_ == + PeerConnectionInterface::kIceConnectionCompleted) { + SetIceConnectionState( + PeerConnectionInterface::kIceConnectionDisconnected); + } + } +} + +void WebRtcSession::OnTransportProxyCandidatesReady( + cricket::TransportProxy* proxy, const cricket::Candidates& candidates) { + ASSERT(signaling_thread()->IsCurrent()); + ProcessNewLocalCandidate(proxy->content_name(), candidates); +} + +bool WebRtcSession::ExpectSetLocalDescription(Action action) { + return ((action == kOffer && state() == STATE_INIT) || + // update local offer + (action == kOffer && state() == STATE_SENTINITIATE) || + // update the current ongoing session. + (action == kOffer && state() == STATE_RECEIVEDACCEPT) || + (action == kOffer && state() == STATE_SENTACCEPT) || + (action == kOffer && state() == STATE_INPROGRESS) || + // accept remote offer + (action == kAnswer && state() == STATE_RECEIVEDINITIATE) || + (action == kAnswer && state() == STATE_SENTPRACCEPT) || + (action == kPrAnswer && state() == STATE_RECEIVEDINITIATE) || + (action == kPrAnswer && state() == STATE_SENTPRACCEPT)); +} + +bool WebRtcSession::ExpectSetRemoteDescription(Action action) { + return ((action == kOffer && state() == STATE_INIT) || + // update remote offer + (action == kOffer && state() == STATE_RECEIVEDINITIATE) || + // update the current ongoing session + (action == kOffer && state() == STATE_RECEIVEDACCEPT) || + (action == kOffer && state() == STATE_SENTACCEPT) || + (action == kOffer && state() == STATE_INPROGRESS) || + // accept local offer + (action == kAnswer && state() == STATE_SENTINITIATE) || + (action == kAnswer && state() == STATE_RECEIVEDPRACCEPT) || + (action == kPrAnswer && state() == STATE_SENTINITIATE) || + (action == kPrAnswer && state() == STATE_RECEIVEDPRACCEPT)); +} + +void WebRtcSession::OnCandidatesAllocationDone() { + ASSERT(signaling_thread()->IsCurrent()); + if (ice_observer_) { + ice_observer_->OnIceGatheringChange( + PeerConnectionInterface::kIceGatheringComplete); + ice_observer_->OnIceComplete(); + } +} + +// Enabling voice and video channel. +void WebRtcSession::EnableChannels() { + if (voice_channel_ && !voice_channel_->enabled()) + voice_channel_->Enable(true); + + if (video_channel_ && !video_channel_->enabled()) + video_channel_->Enable(true); + + if (data_channel_.get() && !data_channel_->enabled()) + data_channel_->Enable(true); +} + +void WebRtcSession::ProcessNewLocalCandidate( + const std::string& content_name, + const cricket::Candidates& candidates) { + int sdp_mline_index; + if (!GetLocalCandidateMediaIndex(content_name, &sdp_mline_index)) { + LOG(LS_ERROR) << "ProcessNewLocalCandidate: content name " + << content_name << " not found"; + return; + } + + for (cricket::Candidates::const_iterator citer = candidates.begin(); + citer != candidates.end(); ++citer) { + // Use content_name as the candidate media id. + JsepIceCandidate candidate(content_name, sdp_mline_index, *citer); + if (ice_observer_) { + ice_observer_->OnIceCandidate(&candidate); + } + if (local_desc_) { + local_desc_->AddCandidate(&candidate); + } + } +} + +// Returns the media index for a local ice candidate given the content name. +bool WebRtcSession::GetLocalCandidateMediaIndex(const std::string& content_name, + int* sdp_mline_index) { + if (!BaseSession::local_description() || !sdp_mline_index) + return false; + + bool content_found = false; + const ContentInfos& contents = BaseSession::local_description()->contents(); + for (size_t index = 0; index < contents.size(); ++index) { + if (contents[index].name == content_name) { + *sdp_mline_index = index; + content_found = true; + break; + } + } + return content_found; +} + +bool WebRtcSession::UseCandidatesInSessionDescription( + const SessionDescriptionInterface* remote_desc) { + if (!remote_desc) + return true; + bool ret = true; + for (size_t m = 0; m < remote_desc->number_of_mediasections(); ++m) { + const IceCandidateCollection* candidates = remote_desc->candidates(m); + for (size_t n = 0; n < candidates->count(); ++n) { + ret = UseCandidate(candidates->at(n)); + if (!ret) + break; + } + } + return ret; +} + +bool WebRtcSession::UseCandidate( + const IceCandidateInterface* candidate) { + + size_t mediacontent_index = static_cast(candidate->sdp_mline_index()); + size_t remote_content_size = + BaseSession::remote_description()->contents().size(); + if (mediacontent_index >= remote_content_size) { + LOG(LS_ERROR) + << "UseRemoteCandidateInSession: Invalid candidate media index."; + return false; + } + + cricket::ContentInfo content = + BaseSession::remote_description()->contents()[mediacontent_index]; + std::vector candidates; + candidates.push_back(candidate->candidate()); + // Invoking BaseSession method to handle remote candidates. + std::string error; + if (OnRemoteCandidates(content.name, candidates, &error)) { + // Candidates successfully submitted for checking. + if (ice_connection_state_ == PeerConnectionInterface::kIceConnectionNew || + ice_connection_state_ == + PeerConnectionInterface::kIceConnectionDisconnected) { + // If state is New, then the session has just gotten its first remote ICE + // candidates, so go to Checking. + // If state is Disconnected, the session is re-using old candidates or + // receiving additional ones, so go to Checking. + // If state is Connected, stay Connected. + // TODO(bemasc): If state is Connected, and the new candidates are for a + // newly added transport, then the state actually _should_ move to + // checking. Add a way to distinguish that case. + SetIceConnectionState(PeerConnectionInterface::kIceConnectionChecking); + } + // TODO(bemasc): If state is Completed, go back to Connected. + } else { + LOG(LS_WARNING) << error; + } + return true; +} + +void WebRtcSession::RemoveUnusedChannelsAndTransports( + const SessionDescription* desc) { + const cricket::ContentInfo* voice_info = + cricket::GetFirstAudioContent(desc); + if ((!voice_info || voice_info->rejected) && voice_channel_) { + mediastream_signaling_->OnAudioChannelClose(); + SignalVoiceChannelDestroyed(); + const std::string content_name = voice_channel_->content_name(); + channel_manager_->DestroyVoiceChannel(voice_channel_.release()); + DestroyTransportProxy(content_name); + } + + const cricket::ContentInfo* video_info = + cricket::GetFirstVideoContent(desc); + if ((!video_info || video_info->rejected) && video_channel_) { + mediastream_signaling_->OnVideoChannelClose(); + SignalVideoChannelDestroyed(); + const std::string content_name = video_channel_->content_name(); + channel_manager_->DestroyVideoChannel(video_channel_.release()); + DestroyTransportProxy(content_name); + } + + const cricket::ContentInfo* data_info = + cricket::GetFirstDataContent(desc); + if ((!data_info || data_info->rejected) && data_channel_) { + mediastream_signaling_->OnDataChannelClose(); + SignalDataChannelDestroyed(); + const std::string content_name = data_channel_->content_name(); + channel_manager_->DestroyDataChannel(data_channel_.release()); + DestroyTransportProxy(content_name); + } +} + +bool WebRtcSession::CreateChannels(const SessionDescription* desc) { + // Disabling the BUNDLE flag in PortAllocator if offer disabled it. + if (state() == STATE_INIT && !desc->HasGroup(cricket::GROUP_TYPE_BUNDLE)) { + port_allocator()->set_flags(port_allocator()->flags() & + ~cricket::PORTALLOCATOR_ENABLE_BUNDLE); + } + + // Creating the media channels and transport proxies. + const cricket::ContentInfo* voice = cricket::GetFirstAudioContent(desc); + if (voice && !voice->rejected && !voice_channel_) { + if (!CreateVoiceChannel(desc)) { + LOG(LS_ERROR) << "Failed to create voice channel."; + return false; + } + } + + const cricket::ContentInfo* video = cricket::GetFirstVideoContent(desc); + if (video && !video->rejected && !video_channel_) { + if (!CreateVideoChannel(desc)) { + LOG(LS_ERROR) << "Failed to create video channel."; + return false; + } + } + + const cricket::ContentInfo* data = cricket::GetFirstDataContent(desc); + if (data_channel_type_ != cricket::DCT_NONE && + data && !data->rejected && !data_channel_.get()) { + if (!CreateDataChannel(desc)) { + LOG(LS_ERROR) << "Failed to create data channel."; + return false; + } + } + + return true; +} + +bool WebRtcSession::CreateVoiceChannel(const SessionDescription* desc) { + const cricket::ContentInfo* voice = cricket::GetFirstAudioContent(desc); + voice_channel_.reset(channel_manager_->CreateVoiceChannel( + this, voice->name, true)); + return voice_channel_ ? true : false; +} + +bool WebRtcSession::CreateVideoChannel(const SessionDescription* desc) { + const cricket::ContentInfo* video = cricket::GetFirstVideoContent(desc); + video_channel_.reset(channel_manager_->CreateVideoChannel( + this, video->name, true, voice_channel_.get())); + return video_channel_ ? true : false; +} + +bool WebRtcSession::CreateDataChannel(const SessionDescription* desc) { + const cricket::ContentInfo* data = cricket::GetFirstDataContent(desc); + bool rtcp = (data_channel_type_ == cricket::DCT_RTP); + data_channel_.reset(channel_manager_->CreateDataChannel( + this, data->name, rtcp, data_channel_type_)); + if (!data_channel_.get()) { + return false; + } + return true; +} + +void WebRtcSession::CopySavedCandidates( + SessionDescriptionInterface* dest_desc) { + if (!dest_desc) { + ASSERT(false); + return; + } + for (size_t i = 0; i < saved_candidates_.size(); ++i) { + dest_desc->AddCandidate(saved_candidates_[i]); + delete saved_candidates_[i]; + } + saved_candidates_.clear(); +} + +void WebRtcSession::UpdateSessionDescriptionSecurePolicy( + SessionDescription* sdesc) { + if (!sdesc) { + return; + } + + // Updating the |crypto_required_| in MediaContentDescription to the + // appropriate state based on the current security policy. + for (cricket::ContentInfos::iterator iter = sdesc->contents().begin(); + iter != sdesc->contents().end(); ++iter) { + if (cricket::IsMediaContent(&*iter)) { + MediaContentDescription* mdesc = + static_cast (iter->description); + if (mdesc) { + mdesc->set_crypto_required( + session_desc_factory_.secure() == cricket::SEC_REQUIRED); + } + } + } +} + +} // namespace webrtc diff --git a/talk/app/webrtc/webrtcsession.h b/talk/app/webrtc/webrtcsession.h new file mode 100644 index 000000000..045d34773 --- /dev/null +++ b/talk/app/webrtc/webrtcsession.h @@ -0,0 +1,295 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_APP_WEBRTC_WEBRTCSESSION_H_ +#define TALK_APP_WEBRTC_WEBRTCSESSION_H_ + +#include + +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/app/webrtc/dtmfsender.h" +#include "talk/app/webrtc/mediastreamprovider.h" +#include "talk/app/webrtc/datachannel.h" +#include "talk/app/webrtc/statstypes.h" +#include "talk/base/sigslot.h" +#include "talk/base/thread.h" +#include "talk/media/base/mediachannel.h" +#include "talk/p2p/base/session.h" +#include "talk/p2p/base/transportdescriptionfactory.h" +#include "talk/session/media/mediasession.h" + +namespace cricket { + +class ChannelManager; +class DataChannel; +class StatsReport; +class Transport; +class VideoCapturer; +class BaseChannel; +class VideoChannel; +class VoiceChannel; + +} // namespace cricket + +namespace webrtc { + +class IceRestartAnswerLatch; +class MediaStreamSignaling; + +extern const char kSetLocalSdpFailed[]; +extern const char kSetRemoteSdpFailed[]; +extern const char kCreateChannelFailed[]; +extern const char kInvalidCandidates[]; +extern const char kInvalidSdp[]; +extern const char kMlineMismatch[]; +extern const char kSdpWithoutCrypto[]; +extern const char kSessionError[]; +extern const char kUpdateStateFailed[]; +extern const char kPushDownOfferTDFailed[]; +extern const char kPushDownPranswerTDFailed[]; +extern const char kPushDownAnswerTDFailed[]; + +// ICE state callback interface. +class IceObserver { + public: + // Called any time the IceConnectionState changes + virtual void OnIceConnectionChange( + PeerConnectionInterface::IceConnectionState new_state) {} + // Called any time the IceGatheringState changes + virtual void OnIceGatheringChange( + PeerConnectionInterface::IceGatheringState new_state) {} + // New Ice candidate have been found. + virtual void OnIceCandidate(const IceCandidateInterface* candidate) = 0; + // All Ice candidates have been found. + // TODO(bemasc): Remove this once callers transition to OnIceGatheringChange. + // (via PeerConnectionObserver) + virtual void OnIceComplete() {} + + protected: + ~IceObserver() {} +}; + +class WebRtcSession : public cricket::BaseSession, + public AudioProviderInterface, + public DataChannelFactory, + public VideoProviderInterface, + public DtmfProviderInterface { + public: + WebRtcSession(cricket::ChannelManager* channel_manager, + talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread, + cricket::PortAllocator* port_allocator, + MediaStreamSignaling* mediastream_signaling); + virtual ~WebRtcSession(); + + bool Initialize(const MediaConstraintsInterface* constraints); + // Deletes the voice, video and data channel and changes the session state + // to STATE_RECEIVEDTERMINATE. + void Terminate(); + + void RegisterIceObserver(IceObserver* observer) { + ice_observer_ = observer; + } + + virtual cricket::VoiceChannel* voice_channel() { + return voice_channel_.get(); + } + virtual cricket::VideoChannel* video_channel() { + return video_channel_.get(); + } + virtual cricket::DataChannel* data_channel() { + return data_channel_.get(); + } + + void set_secure_policy(cricket::SecureMediaPolicy secure_policy); + cricket::SecureMediaPolicy secure_policy() const { + return session_desc_factory_.secure(); + } + + // Generic error message callback from WebRtcSession. + // TODO - It may be necessary to supply error code as well. + sigslot::signal0<> SignalError; + + SessionDescriptionInterface* CreateOffer( + const MediaConstraintsInterface* constraints); + + SessionDescriptionInterface* CreateAnswer( + const MediaConstraintsInterface* constraints); + + bool SetLocalDescription(SessionDescriptionInterface* desc, + std::string* err_desc); + bool SetRemoteDescription(SessionDescriptionInterface* desc, + std::string* err_desc); + bool ProcessIceMessage(const IceCandidateInterface* ice_candidate); + const SessionDescriptionInterface* local_description() const { + return local_desc_.get(); + } + const SessionDescriptionInterface* remote_description() const { + return remote_desc_.get(); + } + + // Get the id used as a media stream track's "id" field from ssrc. + virtual bool GetTrackIdBySsrc(uint32 ssrc, std::string* id); + + // AudioMediaProviderInterface implementation. + virtual void SetAudioPlayout(uint32 ssrc, bool enable) OVERRIDE; + virtual void SetAudioSend(uint32 ssrc, bool enable, + const cricket::AudioOptions& options) OVERRIDE; + virtual bool SetAudioRenderer(uint32 ssrc, + cricket::AudioRenderer* renderer) OVERRIDE; + + // Implements VideoMediaProviderInterface. + virtual bool SetCaptureDevice(uint32 ssrc, + cricket::VideoCapturer* camera) OVERRIDE; + virtual void SetVideoPlayout(uint32 ssrc, + bool enable, + cricket::VideoRenderer* renderer) OVERRIDE; + virtual void SetVideoSend(uint32 ssrc, bool enable, + const cricket::VideoOptions* options) OVERRIDE; + + // Implements DtmfProviderInterface. + virtual bool CanInsertDtmf(const std::string& track_id); + virtual bool InsertDtmf(const std::string& track_id, + int code, int duration); + virtual sigslot::signal0<>* GetOnDestroyedSignal(); + + talk_base::scoped_refptr CreateDataChannel( + const std::string& label, + const DataChannelInit* config); + + cricket::DataChannelType data_channel_type() const; + + private: + // Indicates the type of SessionDescription in a call to SetLocalDescription + // and SetRemoteDescription. + enum Action { + kOffer, + kPrAnswer, + kAnswer, + }; + // Invokes ConnectChannels() on transport proxies, which initiates ice + // candidates allocation. + bool StartCandidatesAllocation(); + bool UpdateSessionState(Action action, cricket::ContentSource source, + const cricket::SessionDescription* desc, + std::string* err_desc); + static Action GetAction(const std::string& type); + + // Transport related callbacks, override from cricket::BaseSession. + virtual void OnTransportRequestSignaling(cricket::Transport* transport); + virtual void OnTransportConnecting(cricket::Transport* transport); + virtual void OnTransportWritable(cricket::Transport* transport); + virtual void OnTransportProxyCandidatesReady( + cricket::TransportProxy* proxy, + const cricket::Candidates& candidates); + virtual void OnCandidatesAllocationDone(); + + // Check if a call to SetLocalDescription is acceptable with |action|. + bool ExpectSetLocalDescription(Action action); + // Check if a call to SetRemoteDescription is acceptable with |action|. + bool ExpectSetRemoteDescription(Action action); + // Creates local session description with audio and video contents. + bool CreateDefaultLocalDescription(); + // Enables media channels to allow sending of media. + void EnableChannels(); + // Creates a JsepIceCandidate and adds it to the local session description + // and notify observers. Called when a new local candidate have been found. + void ProcessNewLocalCandidate(const std::string& content_name, + const cricket::Candidates& candidates); + // Returns the media index for a local ice candidate given the content name. + // Returns false if the local session description does not have a media + // content called |content_name|. + bool GetLocalCandidateMediaIndex(const std::string& content_name, + int* sdp_mline_index); + // Uses all remote candidates in |remote_desc| in this session. + bool UseCandidatesInSessionDescription( + const SessionDescriptionInterface* remote_desc); + // Uses |candidate| in this session. + bool UseCandidate(const IceCandidateInterface* candidate); + // Deletes the corresponding channel of contents that don't exist in |desc|. + // |desc| can be null. This means that all channels are deleted. + void RemoveUnusedChannelsAndTransports( + const cricket::SessionDescription* desc); + + // Allocates media channels based on the |desc|. If |desc| doesn't have + // the BUNDLE option, this method will disable BUNDLE in PortAllocator. + // This method will also delete any existing media channels before creating. + bool CreateChannels(const cricket::SessionDescription* desc); + + // Helper methods to create media channels. + bool CreateVoiceChannel(const cricket::SessionDescription* desc); + bool CreateVideoChannel(const cricket::SessionDescription* desc); + bool CreateDataChannel(const cricket::SessionDescription* desc); + // Copy the candidates from |saved_candidates_| to |dest_desc|. + // The |saved_candidates_| will be cleared after this function call. + void CopySavedCandidates(SessionDescriptionInterface* dest_desc); + + // Forces |desc->crypto_required| to the appropriate state based on the + // current security policy, to ensure a failure occurs if there is an error + // in crypto negotiation. + // Called when processing the local session description. + void UpdateSessionDescriptionSecurePolicy(cricket::SessionDescription* desc); + + bool GetLocalTrackId(uint32 ssrc, std::string* track_id); + bool GetRemoteTrackId(uint32 ssrc, std::string* track_id); + + std::string BadStateErrMsg(const std::string& type, State state); + void SetIceConnectionState(PeerConnectionInterface::IceConnectionState state); + + talk_base::scoped_ptr voice_channel_; + talk_base::scoped_ptr video_channel_; + talk_base::scoped_ptr data_channel_; + cricket::ChannelManager* channel_manager_; + cricket::TransportDescriptionFactory transport_desc_factory_; + cricket::MediaSessionDescriptionFactory session_desc_factory_; + MediaStreamSignaling* mediastream_signaling_; + IceObserver* ice_observer_; + PeerConnectionInterface::IceConnectionState ice_connection_state_; + talk_base::scoped_ptr local_desc_; + talk_base::scoped_ptr remote_desc_; + // Candidates that arrived before the remote description was set. + std::vector saved_candidates_; + uint64 session_version_; + // If the remote peer is using a older version of implementation. + bool older_version_remote_peer_; + // Specifies which kind of data channel is allowed. This is controlled + // by the chrome command-line flag and constraints: + // 1. If chrome command-line switch 'enable-sctp-data-channels' is enabled, + // constraint kEnableDtlsSrtp is true, and constaint kEnableRtpDataChannels is + // not set or false, SCTP is allowed (DCT_SCTP); + // 2. If constraint kEnableRtpDataChannels is true, RTP is allowed (DCT_RTP); + // 3. If both 1&2 are false, data channel is not allowed (DCT_NONE). + cricket::DataChannelType data_channel_type_; + talk_base::scoped_ptr ice_restart_latch_; + sigslot::signal0<> SignalVoiceChannelDestroyed; + sigslot::signal0<> SignalVideoChannelDestroyed; + sigslot::signal0<> SignalDataChannelDestroyed; +}; + +} // namespace webrtc + +#endif // TALK_APP_WEBRTC_WEBRTCSESSION_H_ diff --git a/talk/app/webrtc/webrtcsession_unittest.cc b/talk/app/webrtc/webrtcsession_unittest.cc new file mode 100644 index 000000000..55b295076 --- /dev/null +++ b/talk/app/webrtc/webrtcsession_unittest.cc @@ -0,0 +1,2473 @@ +/* + * libjingle + * Copyright 2012, 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/app/webrtc/audiotrack.h" +#include "talk/app/webrtc/jsepicecandidate.h" +#include "talk/app/webrtc/jsepsessiondescription.h" +#include "talk/app/webrtc/mediastreamsignaling.h" +#include "talk/app/webrtc/streamcollection.h" +#include "talk/app/webrtc/videotrack.h" +#include "talk/app/webrtc/test/fakeconstraints.h" +#include "talk/app/webrtc/webrtcsession.h" +#include "talk/base/fakenetwork.h" +#include "talk/base/firewallsocketserver.h" +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/network.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/sslstreamadapter.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/base/virtualsocketserver.h" +#include "talk/media/base/fakemediaengine.h" +#include "talk/media/base/fakevideorenderer.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/devices/fakedevicemanager.h" +#include "talk/p2p/base/stunserver.h" +#include "talk/p2p/base/teststunserver.h" +#include "talk/p2p/client/basicportallocator.h" +#include "talk/session/media/channelmanager.h" +#include "talk/session/media/mediasession.h" + +#define MAYBE_SKIP_TEST(feature) \ + if (!(feature())) { \ + LOG(LS_INFO) << "Feature disabled... skipping"; \ + return; \ + } + +using cricket::BaseSession; +using cricket::DF_PLAY; +using cricket::DF_SEND; +using cricket::FakeVoiceMediaChannel; +using cricket::NS_GINGLE_P2P; +using cricket::NS_JINGLE_ICE_UDP; +using cricket::TransportInfo; +using cricket::kDtmfDelay; +using cricket::kDtmfReset; +using talk_base::SocketAddress; +using talk_base::scoped_ptr; +using webrtc::CreateSessionDescription; +using webrtc::FakeConstraints; +using webrtc::IceCandidateCollection; +using webrtc::JsepIceCandidate; +using webrtc::JsepSessionDescription; +using webrtc::PeerConnectionInterface; +using webrtc::SessionDescriptionInterface; +using webrtc::StreamCollection; +using webrtc::kMlineMismatch; +using webrtc::kSdpWithoutCrypto; +using webrtc::kSessionError; +using webrtc::kSetLocalSdpFailed; +using webrtc::kSetRemoteSdpFailed; +using webrtc::kPushDownAnswerTDFailed; +using webrtc::kPushDownPranswerTDFailed; + +static const SocketAddress kClientAddr1("11.11.11.11", 0); +static const SocketAddress kClientAddr2("22.22.22.22", 0); +static const SocketAddress kStunAddr("99.99.99.1", cricket::STUN_SERVER_PORT); + +static const char kSessionVersion[] = "1"; + +static const char kStream1[] = "stream1"; +static const char kVideoTrack1[] = "video1"; +static const char kAudioTrack1[] = "audio1"; + +static const char kStream2[] = "stream2"; +static const char kVideoTrack2[] = "video2"; +static const char kAudioTrack2[] = "audio2"; + +// Media index of candidates belonging to the first media content. +static const int kMediaContentIndex0 = 0; +static const char kMediaContentName0[] = "audio"; + +// Media index of candidates belonging to the second media content. +static const int kMediaContentIndex1 = 1; +static const char kMediaContentName1[] = "video"; + +static const int kIceCandidatesTimeout = 10000; + +static const cricket::AudioCodec + kTelephoneEventCodec(106, "telephone-event", 8000, 0, 1, 0); +static const cricket::AudioCodec kCNCodec1(102, "CN", 8000, 0, 1, 0); +static const cricket::AudioCodec kCNCodec2(103, "CN", 16000, 0, 1, 0); + +// Add some extra |newlines| to the |message| after |line|. +static void InjectAfter(const std::string& line, + const std::string& newlines, + std::string* message) { + const std::string tmp = line + newlines; + talk_base::replace_substrs(line.c_str(), line.length(), + tmp.c_str(), tmp.length(), message); +} + +class MockIceObserver : public webrtc::IceObserver { + public: + MockIceObserver() + : oncandidatesready_(false), + ice_connection_state_(PeerConnectionInterface::kIceConnectionNew), + ice_gathering_state_(PeerConnectionInterface::kIceGatheringNew) { + } + + virtual void OnIceConnectionChange( + PeerConnectionInterface::IceConnectionState new_state) { + ice_connection_state_ = new_state; + } + virtual void OnIceGatheringChange( + PeerConnectionInterface::IceGatheringState new_state) { + // We can never transition back to "new". + EXPECT_NE(PeerConnectionInterface::kIceGatheringNew, new_state); + ice_gathering_state_ = new_state; + + // oncandidatesready_ really means "ICE gathering is complete". + // This if statement ensures that this value remains correct when we + // transition from kIceGatheringComplete to kIceGatheringGathering. + if (new_state == PeerConnectionInterface::kIceGatheringGathering) { + oncandidatesready_ = false; + } + } + + // Found a new candidate. + virtual void OnIceCandidate(const webrtc::IceCandidateInterface* candidate) { + if (candidate->sdp_mline_index() == kMediaContentIndex0) { + mline_0_candidates_.push_back(candidate->candidate()); + } else if (candidate->sdp_mline_index() == kMediaContentIndex1) { + mline_1_candidates_.push_back(candidate->candidate()); + } + // The ICE gathering state should always be Gathering when a candidate is + // received (or possibly Completed in the case of the final candidate). + EXPECT_NE(PeerConnectionInterface::kIceGatheringNew, ice_gathering_state_); + } + + // TODO(bemasc): Remove this once callers transition to OnIceGatheringChange. + virtual void OnIceComplete() { + EXPECT_FALSE(oncandidatesready_); + oncandidatesready_ = true; + + // OnIceGatheringChange(IceGatheringCompleted) and OnIceComplete() should + // be called approximately simultaneously. For ease of testing, this + // check additionally requires that they be called in the above order. + EXPECT_EQ(PeerConnectionInterface::kIceGatheringComplete, + ice_gathering_state_); + } + + bool oncandidatesready_; + std::vector mline_0_candidates_; + std::vector mline_1_candidates_; + PeerConnectionInterface::IceConnectionState ice_connection_state_; + PeerConnectionInterface::IceGatheringState ice_gathering_state_; +}; + +class WebRtcSessionForTest : public webrtc::WebRtcSession { + public: + WebRtcSessionForTest(cricket::ChannelManager* cmgr, + talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread, + cricket::PortAllocator* port_allocator, + webrtc::IceObserver* ice_observer, + webrtc::MediaStreamSignaling* mediastream_signaling) + : WebRtcSession(cmgr, signaling_thread, worker_thread, port_allocator, + mediastream_signaling) { + RegisterIceObserver(ice_observer); + } + virtual ~WebRtcSessionForTest() {} + + using cricket::BaseSession::GetTransportProxy; + using webrtc::WebRtcSession::SetAudioPlayout; + using webrtc::WebRtcSession::SetAudioSend; + using webrtc::WebRtcSession::SetCaptureDevice; + using webrtc::WebRtcSession::SetVideoPlayout; + using webrtc::WebRtcSession::SetVideoSend; +}; + +class FakeMediaStreamSignaling : public webrtc::MediaStreamSignaling, + public webrtc::MediaStreamSignalingObserver { + public: + FakeMediaStreamSignaling() : + webrtc::MediaStreamSignaling(talk_base::Thread::Current(), this) { + } + + void SendAudioVideoStream1() { + ClearLocalStreams(); + AddLocalStream(CreateStream(kStream1, kAudioTrack1, kVideoTrack1)); + } + + void SendAudioVideoStream2() { + ClearLocalStreams(); + AddLocalStream(CreateStream(kStream2, kAudioTrack2, kVideoTrack2)); + } + + void SendAudioVideoStream1And2() { + ClearLocalStreams(); + AddLocalStream(CreateStream(kStream1, kAudioTrack1, kVideoTrack1)); + AddLocalStream(CreateStream(kStream2, kAudioTrack2, kVideoTrack2)); + } + + void SendNothing() { + ClearLocalStreams(); + } + + void UseOptionsAudioOnly() { + ClearLocalStreams(); + AddLocalStream(CreateStream(kStream2, kAudioTrack2, "")); + } + + void UseOptionsVideoOnly() { + ClearLocalStreams(); + AddLocalStream(CreateStream(kStream2, "", kVideoTrack2)); + } + + void ClearLocalStreams() { + while (local_streams()->count() != 0) { + RemoveLocalStream(local_streams()->at(0)); + } + } + + // Implements MediaStreamSignalingObserver. + virtual void OnAddRemoteStream(webrtc::MediaStreamInterface* stream) { + } + virtual void OnRemoveRemoteStream(webrtc::MediaStreamInterface* stream) { + } + virtual void OnAddDataChannel(webrtc::DataChannelInterface* data_channel) { + } + virtual void OnAddLocalAudioTrack(webrtc::MediaStreamInterface* stream, + webrtc::AudioTrackInterface* audio_track, + uint32 ssrc) { + } + virtual void OnAddLocalVideoTrack(webrtc::MediaStreamInterface* stream, + webrtc::VideoTrackInterface* video_track, + uint32 ssrc) { + } + virtual void OnAddRemoteAudioTrack(webrtc::MediaStreamInterface* stream, + webrtc::AudioTrackInterface* audio_track, + uint32 ssrc) { + } + + virtual void OnAddRemoteVideoTrack(webrtc::MediaStreamInterface* stream, + webrtc::VideoTrackInterface* video_track, + uint32 ssrc) { + } + + virtual void OnRemoveRemoteAudioTrack( + webrtc::MediaStreamInterface* stream, + webrtc::AudioTrackInterface* audio_track) { + } + + virtual void OnRemoveRemoteVideoTrack( + webrtc::MediaStreamInterface* stream, + webrtc::VideoTrackInterface* video_track) { + } + + virtual void OnRemoveLocalAudioTrack( + webrtc::MediaStreamInterface* stream, + webrtc::AudioTrackInterface* audio_track) { + } + virtual void OnRemoveLocalVideoTrack( + webrtc::MediaStreamInterface* stream, + webrtc::VideoTrackInterface* video_track) { + } + virtual void OnRemoveLocalStream(webrtc::MediaStreamInterface* stream) { + } + + private: + talk_base::scoped_refptr CreateStream( + const std::string& stream_label, + const std::string& audio_track_id, + const std::string& video_track_id) { + talk_base::scoped_refptr stream( + webrtc::MediaStream::Create(stream_label)); + + if (!audio_track_id.empty()) { + talk_base::scoped_refptr audio_track( + webrtc::AudioTrack::Create(audio_track_id, NULL)); + stream->AddTrack(audio_track); + } + + if (!video_track_id.empty()) { + talk_base::scoped_refptr video_track( + webrtc::VideoTrack::Create(video_track_id, NULL)); + stream->AddTrack(video_track); + } + return stream; + } + + cricket::MediaSessionOptions options_; +}; + +class WebRtcSessionTest : public testing::Test { + protected: + // TODO Investigate why ChannelManager crashes, if it's created + // after stun_server. + WebRtcSessionTest() + : media_engine_(new cricket::FakeMediaEngine()), + data_engine_(new cricket::FakeDataEngine()), + device_manager_(new cricket::FakeDeviceManager()), + channel_manager_(new cricket::ChannelManager( + media_engine_, data_engine_, device_manager_, + new cricket::CaptureManager(), talk_base::Thread::Current())), + tdesc_factory_(new cricket::TransportDescriptionFactory()), + desc_factory_(new cricket::MediaSessionDescriptionFactory( + channel_manager_.get(), tdesc_factory_.get())), + pss_(new talk_base::PhysicalSocketServer), + vss_(new talk_base::VirtualSocketServer(pss_.get())), + fss_(new talk_base::FirewallSocketServer(vss_.get())), + ss_scope_(fss_.get()), + stun_server_(talk_base::Thread::Current(), kStunAddr), + allocator_(&network_manager_, kStunAddr, + SocketAddress(), SocketAddress(), SocketAddress()) { + tdesc_factory_->set_protocol(cricket::ICEPROTO_HYBRID); + allocator_.set_flags(cricket::PORTALLOCATOR_DISABLE_TCP | + cricket::PORTALLOCATOR_DISABLE_RELAY | + cricket::PORTALLOCATOR_ENABLE_BUNDLE); + EXPECT_TRUE(channel_manager_->Init()); + desc_factory_->set_add_legacy_streams(false); + } + + void AddInterface(const SocketAddress& addr) { + network_manager_.AddInterface(addr); + } + + void Init() { + ASSERT_TRUE(session_.get() == NULL); + session_.reset(new WebRtcSessionForTest( + channel_manager_.get(), talk_base::Thread::Current(), + talk_base::Thread::Current(), &allocator_, + &observer_, + &mediastream_signaling_)); + + EXPECT_EQ(PeerConnectionInterface::kIceConnectionNew, + observer_.ice_connection_state_); + EXPECT_EQ(PeerConnectionInterface::kIceGatheringNew, + observer_.ice_gathering_state_); + + EXPECT_TRUE(session_->Initialize(constraints_.get())); + } + + void InitWithDtmfCodec() { + // Add kTelephoneEventCodec for dtmf test. + std::vector codecs; + codecs.push_back(kTelephoneEventCodec); + media_engine_->SetAudioCodecs(codecs); + desc_factory_->set_audio_codecs(codecs); + Init(); + } + + void InitWithDtls() { + constraints_.reset(new FakeConstraints()); + constraints_->AddOptional( + webrtc::MediaConstraintsInterface::kEnableDtlsSrtp, true); + + Init(); + } + + // Creates a local offer and applies it. Starts ice. + // Call mediastream_signaling_.UseOptionsWithStreamX() before this function + // to decide which streams to create. + void InitiateCall() { + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SetLocalDescriptionWithoutError(offer); + EXPECT_TRUE_WAIT(PeerConnectionInterface::kIceGatheringNew != + observer_.ice_gathering_state_, + kIceCandidatesTimeout); + } + + bool ChannelsExist() { + return (session_->voice_channel() != NULL && + session_->video_channel() != NULL); + } + + void CheckTransportChannels() { + EXPECT_TRUE(session_->GetChannel(cricket::CN_AUDIO, 1) != NULL); + EXPECT_TRUE(session_->GetChannel(cricket::CN_AUDIO, 2) != NULL); + EXPECT_TRUE(session_->GetChannel(cricket::CN_VIDEO, 1) != NULL); + EXPECT_TRUE(session_->GetChannel(cricket::CN_VIDEO, 2) != NULL); + } + + void VerifyCryptoParams(const cricket::SessionDescription* sdp) { + ASSERT_TRUE(session_.get() != NULL); + const cricket::ContentInfo* content = cricket::GetFirstAudioContent(sdp); + ASSERT_TRUE(content != NULL); + const cricket::AudioContentDescription* audio_content = + static_cast( + content->description); + ASSERT_TRUE(audio_content != NULL); + ASSERT_EQ(1U, audio_content->cryptos().size()); + ASSERT_EQ(47U, audio_content->cryptos()[0].key_params.size()); + ASSERT_EQ("AES_CM_128_HMAC_SHA1_80", + audio_content->cryptos()[0].cipher_suite); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), + audio_content->protocol()); + + content = cricket::GetFirstVideoContent(sdp); + ASSERT_TRUE(content != NULL); + const cricket::VideoContentDescription* video_content = + static_cast( + content->description); + ASSERT_TRUE(video_content != NULL); + ASSERT_EQ(1U, video_content->cryptos().size()); + ASSERT_EQ("AES_CM_128_HMAC_SHA1_80", + video_content->cryptos()[0].cipher_suite); + ASSERT_EQ(47U, video_content->cryptos()[0].key_params.size()); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), + video_content->protocol()); + } + + void VerifyNoCryptoParams(const cricket::SessionDescription* sdp, bool dtls) { + const cricket::ContentInfo* content = cricket::GetFirstAudioContent(sdp); + ASSERT_TRUE(content != NULL); + const cricket::AudioContentDescription* audio_content = + static_cast( + content->description); + ASSERT_TRUE(audio_content != NULL); + ASSERT_EQ(0U, audio_content->cryptos().size()); + + content = cricket::GetFirstVideoContent(sdp); + ASSERT_TRUE(content != NULL); + const cricket::VideoContentDescription* video_content = + static_cast( + content->description); + ASSERT_TRUE(video_content != NULL); + ASSERT_EQ(0U, video_content->cryptos().size()); + + if (dtls) { + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), + audio_content->protocol()); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), + video_content->protocol()); + } else { + EXPECT_EQ(std::string(cricket::kMediaProtocolAvpf), + audio_content->protocol()); + EXPECT_EQ(std::string(cricket::kMediaProtocolAvpf), + video_content->protocol()); + } + } + + // Set the internal fake description factories to do DTLS-SRTP. + void SetFactoryDtlsSrtp() { + desc_factory_->set_secure(cricket::SEC_ENABLED); + std::string identity_name = "WebRTC" + + talk_base::ToString(talk_base::CreateRandomId()); + tdesc_factory_->set_identity(talk_base::SSLIdentity::Generate( + identity_name)); + tdesc_factory_->set_digest_algorithm(talk_base::DIGEST_SHA_256); + tdesc_factory_->set_secure(cricket::SEC_REQUIRED); + } + + void VerifyFingerprintStatus(const cricket::SessionDescription* sdp, + bool expected) { + const TransportInfo* audio = sdp->GetTransportInfoByName("audio"); + ASSERT_TRUE(audio != NULL); + ASSERT_EQ(expected, audio->description.identity_fingerprint.get() != NULL); + if (expected) { + ASSERT_EQ(std::string(talk_base::DIGEST_SHA_256), audio->description. + identity_fingerprint->algorithm); + } + const TransportInfo* video = sdp->GetTransportInfoByName("video"); + ASSERT_TRUE(video != NULL); + ASSERT_EQ(expected, video->description.identity_fingerprint.get() != NULL); + if (expected) { + ASSERT_EQ(std::string(talk_base::DIGEST_SHA_256), video->description. + identity_fingerprint->algorithm); + } + } + + void VerifyAnswerFromNonCryptoOffer() { + // Create a SDP without Crypto. + cricket::MediaSessionOptions options; + options.has_video = true; + scoped_ptr offer( + CreateRemoteOffer(options, cricket::SEC_DISABLED)); + ASSERT_TRUE(offer.get() != NULL); + VerifyNoCryptoParams(offer->description(), false); + SetRemoteDescriptionExpectError("Called with a SDP without crypto enabled", + offer.release()); + const webrtc::SessionDescriptionInterface* answer = + session_->CreateAnswer(NULL); + // Answer should be NULL as no crypto params in offer. + ASSERT_TRUE(answer == NULL); + } + + void VerifyAnswerFromCryptoOffer() { + cricket::MediaSessionOptions options; + options.has_video = true; + options.bundle_enabled = true; + scoped_ptr offer( + CreateRemoteOffer(options, cricket::SEC_REQUIRED)); + ASSERT_TRUE(offer.get() != NULL); + VerifyCryptoParams(offer->description()); + SetRemoteDescriptionWithoutError(offer.release()); + scoped_ptr answer( + session_->CreateAnswer(NULL)); + ASSERT_TRUE(answer.get() != NULL); + VerifyCryptoParams(answer->description()); + } + + void CompareIceUfragAndPassword(const cricket::SessionDescription* desc1, + const cricket::SessionDescription* desc2, + bool expect_equal) { + if (desc1->contents().size() != desc2->contents().size()) { + EXPECT_FALSE(expect_equal); + return; + } + + const cricket::ContentInfos& contents = desc1->contents(); + cricket::ContentInfos::const_iterator it = contents.begin(); + + for (; it != contents.end(); ++it) { + const cricket::TransportDescription* transport_desc1 = + desc1->GetTransportDescriptionByName(it->name); + const cricket::TransportDescription* transport_desc2 = + desc2->GetTransportDescriptionByName(it->name); + if (!transport_desc1 || !transport_desc2) { + EXPECT_FALSE(expect_equal); + return; + } + if (transport_desc1->ice_pwd != transport_desc2->ice_pwd || + transport_desc1->ice_ufrag != transport_desc2->ice_ufrag) { + EXPECT_FALSE(expect_equal); + return; + } + } + EXPECT_TRUE(expect_equal); + } + // Creates a remote offer and and applies it as a remote description, + // creates a local answer and applies is as a local description. + // Call mediastream_signaling_.UseOptionsWithStreamX() before this function + // to decide which local and remote streams to create. + void CreateAndSetRemoteOfferAndLocalAnswer() { + SessionDescriptionInterface* offer = CreateRemoteOffer(); + SetRemoteDescriptionWithoutError(offer); + SessionDescriptionInterface* answer = session_->CreateAnswer(NULL); + SetLocalDescriptionWithoutError(answer); + } + void SetLocalDescriptionWithoutError(SessionDescriptionInterface* desc) { + EXPECT_TRUE(session_->SetLocalDescription(desc, NULL)); + } + void SetLocalDescriptionExpectState(SessionDescriptionInterface* desc, + BaseSession::State expected_state) { + SetLocalDescriptionWithoutError(desc); + EXPECT_EQ(expected_state, session_->state()); + } + void SetLocalDescriptionExpectError(const std::string& expected_error, + SessionDescriptionInterface* desc) { + std::string error; + EXPECT_FALSE(session_->SetLocalDescription(desc, &error)); + EXPECT_NE(std::string::npos, error.find(kSetLocalSdpFailed)); + EXPECT_NE(std::string::npos, error.find(expected_error)); + } + void SetRemoteDescriptionWithoutError(SessionDescriptionInterface* desc) { + EXPECT_TRUE(session_->SetRemoteDescription(desc, NULL)); + } + void SetRemoteDescriptionExpectState(SessionDescriptionInterface* desc, + BaseSession::State expected_state) { + SetRemoteDescriptionWithoutError(desc); + EXPECT_EQ(expected_state, session_->state()); + } + void SetRemoteDescriptionExpectError(const std::string& expected_error, + SessionDescriptionInterface* desc) { + std::string error; + EXPECT_FALSE(session_->SetRemoteDescription(desc, &error)); + EXPECT_NE(std::string::npos, error.find(kSetRemoteSdpFailed)); + EXPECT_NE(std::string::npos, error.find(expected_error)); + } + + void CreateCryptoOfferAndNonCryptoAnswer(SessionDescriptionInterface** offer, + SessionDescriptionInterface** nocrypto_answer) { + // Create a SDP without Crypto. + cricket::MediaSessionOptions options; + options.has_video = true; + options.bundle_enabled = true; + *offer = CreateRemoteOffer(options, cricket::SEC_ENABLED); + ASSERT_TRUE(*offer != NULL); + VerifyCryptoParams((*offer)->description()); + + *nocrypto_answer = CreateRemoteAnswer(*offer, options, + cricket::SEC_DISABLED); + EXPECT_TRUE(*nocrypto_answer != NULL); + } + + JsepSessionDescription* CreateRemoteOfferWithVersion( + cricket::MediaSessionOptions options, + cricket::SecurePolicy secure_policy, + const std::string& session_version, + const SessionDescriptionInterface* current_desc) { + std::string session_id = talk_base::ToString(talk_base::CreateRandomId64()); + const cricket::SessionDescription* cricket_desc = NULL; + if (current_desc) { + cricket_desc = current_desc->description(); + session_id = current_desc->session_id(); + } + + desc_factory_->set_secure(secure_policy); + JsepSessionDescription* offer( + new JsepSessionDescription(JsepSessionDescription::kOffer)); + if (!offer->Initialize(desc_factory_->CreateOffer(options, cricket_desc), + session_id, session_version)) { + delete offer; + offer = NULL; + } + return offer; + } + JsepSessionDescription* CreateRemoteOffer( + cricket::MediaSessionOptions options) { + return CreateRemoteOfferWithVersion(options, cricket::SEC_ENABLED, + kSessionVersion, NULL); + } + JsepSessionDescription* CreateRemoteOffer( + cricket::MediaSessionOptions options, cricket::SecurePolicy policy) { + return CreateRemoteOfferWithVersion(options, policy, kSessionVersion, NULL); + } + JsepSessionDescription* CreateRemoteOffer( + cricket::MediaSessionOptions options, + const SessionDescriptionInterface* current_desc) { + return CreateRemoteOfferWithVersion(options, cricket::SEC_ENABLED, + kSessionVersion, current_desc); + } + + // Create a remote offer. Call mediastream_signaling_.UseOptionsWithStreamX() + // before this function to decide which streams to create. + JsepSessionDescription* CreateRemoteOffer() { + cricket::MediaSessionOptions options; + mediastream_signaling_.GetOptionsForAnswer(NULL, &options); + return CreateRemoteOffer(options, session_->remote_description()); + } + + JsepSessionDescription* CreateRemoteAnswer( + const SessionDescriptionInterface* offer, + cricket::MediaSessionOptions options, + cricket::SecurePolicy policy) { + desc_factory_->set_secure(policy); + const std::string session_id = + talk_base::ToString(talk_base::CreateRandomId64()); + JsepSessionDescription* answer( + new JsepSessionDescription(JsepSessionDescription::kAnswer)); + if (!answer->Initialize(desc_factory_->CreateAnswer(offer->description(), + options, NULL), + session_id, kSessionVersion)) { + delete answer; + answer = NULL; + } + return answer; + } + + JsepSessionDescription* CreateRemoteAnswer( + const SessionDescriptionInterface* offer, + cricket::MediaSessionOptions options) { + return CreateRemoteAnswer(offer, options, cricket::SEC_REQUIRED); + } + + // Creates an answer session description with streams based on + // |mediastream_signaling_|. Call + // mediastream_signaling_.UseOptionsWithStreamX() before this function + // to decide which streams to create. + JsepSessionDescription* CreateRemoteAnswer( + const SessionDescriptionInterface* offer) { + cricket::MediaSessionOptions options; + mediastream_signaling_.GetOptionsForAnswer(NULL, &options); + return CreateRemoteAnswer(offer, options, cricket::SEC_REQUIRED); + } + + void TestSessionCandidatesWithBundleRtcpMux(bool bundle, bool rtcp_mux) { + AddInterface(kClientAddr1); + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + FakeConstraints constraints; + constraints.SetMandatoryUseRtpMux(bundle); + SessionDescriptionInterface* offer = session_->CreateOffer(&constraints); + // SetLocalDescription and SetRemoteDescriptions takes ownership of offer + // and answer. + SetLocalDescriptionWithoutError(offer); + + SessionDescriptionInterface* answer = CreateRemoteAnswer( + session_->local_description()); + std::string sdp; + EXPECT_TRUE(answer->ToString(&sdp)); + + size_t expected_candidate_num = 2; + if (!rtcp_mux) { + // If rtcp_mux is enabled we should expect 4 candidates - host and srflex + // for rtp and rtcp. + expected_candidate_num = 4; + // Disable rtcp-mux from the answer + + const std::string kRtcpMux = "a=rtcp-mux"; + const std::string kXRtcpMux = "a=xrtcp-mux"; + talk_base::replace_substrs(kRtcpMux.c_str(), kRtcpMux.length(), + kXRtcpMux.c_str(), kXRtcpMux.length(), + &sdp); + } + + SessionDescriptionInterface* new_answer = CreateSessionDescription( + JsepSessionDescription::kAnswer, sdp, NULL); + delete answer; + answer = new_answer; + + // SetRemoteDescription to enable rtcp mux. + SetRemoteDescriptionWithoutError(answer); + EXPECT_TRUE_WAIT(observer_.oncandidatesready_, kIceCandidatesTimeout); + EXPECT_EQ(expected_candidate_num, observer_.mline_0_candidates_.size()); + EXPECT_EQ(expected_candidate_num, observer_.mline_1_candidates_.size()); + for (size_t i = 0; i < observer_.mline_0_candidates_.size(); ++i) { + cricket::Candidate c0 = observer_.mline_0_candidates_[i]; + cricket::Candidate c1 = observer_.mline_1_candidates_[i]; + if (bundle) { + EXPECT_TRUE(c0.IsEquivalent(c1)); + } else { + EXPECT_FALSE(c0.IsEquivalent(c1)); + } + } + } + // Tests that we can only send DTMF when the dtmf codec is supported. + void TestCanInsertDtmf(bool can) { + if (can) { + InitWithDtmfCodec(); + } else { + Init(); + } + mediastream_signaling_.SendAudioVideoStream1(); + CreateAndSetRemoteOfferAndLocalAnswer(); + EXPECT_FALSE(session_->CanInsertDtmf("")); + EXPECT_EQ(can, session_->CanInsertDtmf(kAudioTrack1)); + } + + // The method sets up a call from the session to itself, in a loopback + // arrangement. It also uses a firewall rule to create a temporary + // disconnection. This code is placed as a method so that it can be invoked + // by multiple tests with different allocators (e.g. with and without BUNDLE). + // While running the call, this method also checks if the session goes through + // the correct sequence of ICE states when a connection is established, + // broken, and re-established. + // The Connection state should go: + // New -> Checking -> Connected -> Disconnected -> Connected. + // The Gathering state should go: New -> Gathering -> Completed. + void TestLoopbackCall() { + AddInterface(kClientAddr1); + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + + EXPECT_EQ(PeerConnectionInterface::kIceGatheringNew, + observer_.ice_gathering_state_); + SetLocalDescriptionWithoutError(offer); + EXPECT_EQ(PeerConnectionInterface::kIceConnectionNew, + observer_.ice_connection_state_); + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceGatheringGathering, + observer_.ice_gathering_state_, + kIceCandidatesTimeout); + EXPECT_TRUE_WAIT(observer_.oncandidatesready_, kIceCandidatesTimeout); + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceGatheringComplete, + observer_.ice_gathering_state_, + kIceCandidatesTimeout); + + std::string sdp; + offer->ToString(&sdp); + SessionDescriptionInterface* desc = + webrtc::CreateSessionDescription(JsepSessionDescription::kAnswer, sdp); + ASSERT_TRUE(desc != NULL); + SetRemoteDescriptionWithoutError(desc); + + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionChecking, + observer_.ice_connection_state_, + kIceCandidatesTimeout); + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionConnected, + observer_.ice_connection_state_, + kIceCandidatesTimeout); + // TODO(bemasc): EXPECT(Completed) once the details are standardized. + + // Adding firewall rule to block ping requests, which should cause + // transport channel failure. + fss_->AddRule(false, talk_base::FP_ANY, talk_base::FD_ANY, kClientAddr1); + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionDisconnected, + observer_.ice_connection_state_, + kIceCandidatesTimeout); + + // Clearing the rules, session should move back to completed state. + fss_->ClearRules(); + // Session is automatically calling OnSignalingReady after creation of + // new portallocator session which will allocate new set of candidates. + + // TODO(bemasc): Change this to Completed once the details are standardized. + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionConnected, + observer_.ice_connection_state_, + kIceCandidatesTimeout); + } + + void VerifyTransportType(const std::string& content_name, + cricket::TransportProtocol protocol) { + const cricket::Transport* transport = session_->GetTransport(content_name); + ASSERT_TRUE(transport != NULL); + EXPECT_EQ(protocol, transport->protocol()); + } + + // Adds CN codecs to FakeMediaEngine and MediaDescriptionFactory. + void AddCNCodecs() { + // Add kTelephoneEventCodec for dtmf test. + std::vector codecs = media_engine_->audio_codecs();; + codecs.push_back(kCNCodec1); + codecs.push_back(kCNCodec2); + media_engine_->SetAudioCodecs(codecs); + desc_factory_->set_audio_codecs(codecs); + } + + bool VerifyNoCNCodecs(const cricket::ContentInfo* content) { + const cricket::ContentDescription* description = content->description; + ASSERT(description != NULL); + const cricket::AudioContentDescription* audio_content_desc = + static_cast (description); + ASSERT(audio_content_desc != NULL); + for (size_t i = 0; i < audio_content_desc->codecs().size(); ++i) { + if (audio_content_desc->codecs()[i].name == "CN") + return false; + } + return true; + } + + void SetLocalDescriptionWithDataChannel() { + webrtc::DataChannelInit dci; + dci.reliable = false; + session_->CreateDataChannel("datachannel", &dci); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SetLocalDescriptionWithoutError(offer); + } + + cricket::FakeMediaEngine* media_engine_; + cricket::FakeDataEngine* data_engine_; + cricket::FakeDeviceManager* device_manager_; + talk_base::scoped_ptr channel_manager_; + talk_base::scoped_ptr tdesc_factory_; + talk_base::scoped_ptr desc_factory_; + talk_base::scoped_ptr pss_; + talk_base::scoped_ptr vss_; + talk_base::scoped_ptr fss_; + talk_base::SocketServerScope ss_scope_; + cricket::TestStunServer stun_server_; + talk_base::FakeNetworkManager network_manager_; + cricket::BasicPortAllocator allocator_; + talk_base::scoped_ptr constraints_; + FakeMediaStreamSignaling mediastream_signaling_; + talk_base::scoped_ptr session_; + MockIceObserver observer_; + cricket::FakeVideoMediaChannel* video_channel_; + cricket::FakeVoiceMediaChannel* voice_channel_; +}; + +TEST_F(WebRtcSessionTest, TestInitialize) { + Init(); +} + +TEST_F(WebRtcSessionTest, TestInitializeWithDtls) { + InitWithDtls(); +} + +TEST_F(WebRtcSessionTest, TestSessionCandidates) { + TestSessionCandidatesWithBundleRtcpMux(false, false); +} + +// Below test cases (TestSessionCandidatesWith*) verify the candidates gathered +// with rtcp-mux and/or bundle. +TEST_F(WebRtcSessionTest, TestSessionCandidatesWithRtcpMux) { + TestSessionCandidatesWithBundleRtcpMux(false, true); +} + +TEST_F(WebRtcSessionTest, TestSessionCandidatesWithBundle) { + TestSessionCandidatesWithBundleRtcpMux(true, false); +} + +TEST_F(WebRtcSessionTest, TestSessionCandidatesWithBundleRtcpMux) { + TestSessionCandidatesWithBundleRtcpMux(true, true); +} + +TEST_F(WebRtcSessionTest, TestMultihomeCandidates) { + AddInterface(kClientAddr1); + AddInterface(kClientAddr2); + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + InitiateCall(); + EXPECT_TRUE_WAIT(observer_.oncandidatesready_, kIceCandidatesTimeout); + EXPECT_EQ(8u, observer_.mline_0_candidates_.size()); + EXPECT_EQ(8u, observer_.mline_1_candidates_.size()); +} + +TEST_F(WebRtcSessionTest, TestStunError) { + AddInterface(kClientAddr1); + AddInterface(kClientAddr2); + fss_->AddRule(false, talk_base::FP_UDP, talk_base::FD_ANY, kClientAddr1); + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + InitiateCall(); + // Since kClientAddr1 is blocked, not expecting stun candidates for it. + EXPECT_TRUE_WAIT(observer_.oncandidatesready_, kIceCandidatesTimeout); + EXPECT_EQ(6u, observer_.mline_0_candidates_.size()); + EXPECT_EQ(6u, observer_.mline_1_candidates_.size()); +} + +// Test creating offers and receive answers and make sure the +// media engine creates the expected send and receive streams. +TEST_F(WebRtcSessionTest, TestCreateOfferReceiveAnswer) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + const std::string session_id_orig = offer->session_id(); + const std::string session_version_orig = offer->session_version(); + SetLocalDescriptionWithoutError(offer); + + mediastream_signaling_.SendAudioVideoStream2(); + SessionDescriptionInterface* answer = + CreateRemoteAnswer(session_->local_description()); + SetRemoteDescriptionWithoutError(answer); + + video_channel_ = media_engine_->GetVideoChannel(0); + voice_channel_ = media_engine_->GetVoiceChannel(0); + + ASSERT_EQ(1u, video_channel_->recv_streams().size()); + EXPECT_TRUE(kVideoTrack2 == video_channel_->recv_streams()[0].id); + + ASSERT_EQ(1u, voice_channel_->recv_streams().size()); + EXPECT_TRUE(kAudioTrack2 == voice_channel_->recv_streams()[0].id); + + ASSERT_EQ(1u, video_channel_->send_streams().size()); + EXPECT_TRUE(kVideoTrack1 == video_channel_->send_streams()[0].id); + ASSERT_EQ(1u, voice_channel_->send_streams().size()); + EXPECT_TRUE(kAudioTrack1 == voice_channel_->send_streams()[0].id); + + // Create new offer without send streams. + mediastream_signaling_.SendNothing(); + offer = session_->CreateOffer(NULL); + + // Verify the session id is the same and the session version is + // increased. + EXPECT_EQ(session_id_orig, offer->session_id()); + EXPECT_LT(talk_base::FromString(session_version_orig), + talk_base::FromString(offer->session_version())); + + SetLocalDescriptionWithoutError(offer); + + mediastream_signaling_.SendAudioVideoStream2(); + answer = CreateRemoteAnswer(session_->local_description()); + SetRemoteDescriptionWithoutError(answer); + + EXPECT_EQ(0u, video_channel_->send_streams().size()); + EXPECT_EQ(0u, voice_channel_->send_streams().size()); + + // Make sure the receive streams have not changed. + ASSERT_EQ(1u, video_channel_->recv_streams().size()); + EXPECT_TRUE(kVideoTrack2 == video_channel_->recv_streams()[0].id); + ASSERT_EQ(1u, voice_channel_->recv_streams().size()); + EXPECT_TRUE(kAudioTrack2 == voice_channel_->recv_streams()[0].id); +} + +// Test receiving offers and creating answers and make sure the +// media engine creates the expected send and receive streams. +TEST_F(WebRtcSessionTest, TestReceiveOfferCreateAnswer) { + Init(); + mediastream_signaling_.SendAudioVideoStream2(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SetRemoteDescriptionWithoutError(offer); + + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* answer = session_->CreateAnswer(NULL); + SetLocalDescriptionWithoutError(answer); + + const std::string session_id_orig = answer->session_id(); + const std::string session_version_orig = answer->session_version(); + + video_channel_ = media_engine_->GetVideoChannel(0); + voice_channel_ = media_engine_->GetVoiceChannel(0); + + ASSERT_EQ(1u, video_channel_->recv_streams().size()); + EXPECT_TRUE(kVideoTrack2 == video_channel_->recv_streams()[0].id); + + ASSERT_EQ(1u, voice_channel_->recv_streams().size()); + EXPECT_TRUE(kAudioTrack2 == voice_channel_->recv_streams()[0].id); + + ASSERT_EQ(1u, video_channel_->send_streams().size()); + EXPECT_TRUE(kVideoTrack1 == video_channel_->send_streams()[0].id); + ASSERT_EQ(1u, voice_channel_->send_streams().size()); + EXPECT_TRUE(kAudioTrack1 == voice_channel_->send_streams()[0].id); + + mediastream_signaling_.SendAudioVideoStream1And2(); + offer = session_->CreateOffer(NULL); + SetRemoteDescriptionWithoutError(offer); + + // Answer by turning off all send streams. + mediastream_signaling_.SendNothing(); + answer = session_->CreateAnswer(NULL); + + // Verify the session id is the same and the session version is + // increased. + EXPECT_EQ(session_id_orig, answer->session_id()); + EXPECT_LT(talk_base::FromString(session_version_orig), + talk_base::FromString(answer->session_version())); + SetLocalDescriptionWithoutError(answer); + + ASSERT_EQ(2u, video_channel_->recv_streams().size()); + EXPECT_TRUE(kVideoTrack1 == video_channel_->recv_streams()[0].id); + EXPECT_TRUE(kVideoTrack2 == video_channel_->recv_streams()[1].id); + ASSERT_EQ(2u, voice_channel_->recv_streams().size()); + EXPECT_TRUE(kAudioTrack1 == voice_channel_->recv_streams()[0].id); + EXPECT_TRUE(kAudioTrack2 == voice_channel_->recv_streams()[1].id); + + // Make sure we have no send streams. + EXPECT_EQ(0u, video_channel_->send_streams().size()); + EXPECT_EQ(0u, voice_channel_->send_streams().size()); +} + +// Test we will return fail when apply an offer that doesn't have +// crypto enabled. +TEST_F(WebRtcSessionTest, SetNonCryptoOffer) { + Init(); + cricket::MediaSessionOptions options; + options.has_video = true; + JsepSessionDescription* offer = CreateRemoteOffer( + options, cricket::SEC_DISABLED); + ASSERT_TRUE(offer != NULL); + VerifyNoCryptoParams(offer->description(), false); + // SetRemoteDescription and SetLocalDescription will take the ownership of + // the offer. + SetRemoteDescriptionExpectError(kSdpWithoutCrypto, offer); + offer = CreateRemoteOffer(options, cricket::SEC_DISABLED); + ASSERT_TRUE(offer != NULL); + SetLocalDescriptionExpectError(kSdpWithoutCrypto, offer); +} + +// Test we will return fail when apply an answer that doesn't have +// crypto enabled. +TEST_F(WebRtcSessionTest, SetLocalNonCryptoAnswer) { + Init(); + SessionDescriptionInterface* offer = NULL; + SessionDescriptionInterface* answer = NULL; + CreateCryptoOfferAndNonCryptoAnswer(&offer, &answer); + // SetRemoteDescription and SetLocalDescription will take the ownership of + // the offer. + SetRemoteDescriptionWithoutError(offer); + SetLocalDescriptionExpectError(kSdpWithoutCrypto, answer); +} + +// Test we will return fail when apply an answer that doesn't have +// crypto enabled. +TEST_F(WebRtcSessionTest, SetRemoteNonCryptoAnswer) { + Init(); + SessionDescriptionInterface* offer = NULL; + SessionDescriptionInterface* answer = NULL; + CreateCryptoOfferAndNonCryptoAnswer(&offer, &answer); + // SetRemoteDescription and SetLocalDescription will take the ownership of + // the offer. + SetLocalDescriptionWithoutError(offer); + SetRemoteDescriptionExpectError(kSdpWithoutCrypto, answer); +} + +// Test that we can create and set an offer with a DTLS fingerprint. +TEST_F(WebRtcSessionTest, CreateSetDtlsOffer) { + MAYBE_SKIP_TEST(talk_base::SSLStreamAdapter::HaveDtlsSrtp); + InitWithDtls(); + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + ASSERT_TRUE(offer != NULL); + VerifyFingerprintStatus(offer->description(), true); + // SetLocalDescription will take the ownership of the offer. + SetLocalDescriptionWithoutError(offer); +} + +// Test that we can process an offer with a DTLS fingerprint +// and that we return an answer with a fingerprint. +TEST_F(WebRtcSessionTest, ReceiveDtlsOfferCreateAnswer) { + MAYBE_SKIP_TEST(talk_base::SSLStreamAdapter::HaveDtlsSrtp); + InitWithDtls(); + SetFactoryDtlsSrtp(); + cricket::MediaSessionOptions options; + options.has_video = true; + JsepSessionDescription* offer = CreateRemoteOffer(options); + ASSERT_TRUE(offer != NULL); + VerifyFingerprintStatus(offer->description(), true); + + // SetRemoteDescription will take the ownership of the offer. + SetRemoteDescriptionWithoutError(offer); + + // Verify that we get a crypto fingerprint in the answer. + SessionDescriptionInterface* answer = session_->CreateAnswer(NULL); + ASSERT_TRUE(answer != NULL); + VerifyFingerprintStatus(answer->description(), true); + // Check that we don't have an a=crypto line in the answer. + VerifyNoCryptoParams(answer->description(), true); + + // Now set the local description, which should work, even without a=crypto. + SetLocalDescriptionWithoutError(answer); +} + +// Test that even if we support DTLS, if the other side didn't offer a +// fingerprint, we don't either. +TEST_F(WebRtcSessionTest, ReceiveNoDtlsOfferCreateAnswer) { + MAYBE_SKIP_TEST(talk_base::SSLStreamAdapter::HaveDtlsSrtp); + InitWithDtls(); + cricket::MediaSessionOptions options; + options.has_video = true; + JsepSessionDescription* offer = CreateRemoteOffer( + options, cricket::SEC_REQUIRED); + ASSERT_TRUE(offer != NULL); + VerifyFingerprintStatus(offer->description(), false); + + // SetRemoteDescription will take the ownership of + // the offer. + SetRemoteDescriptionWithoutError(offer); + + // Verify that we don't get a crypto fingerprint in the answer. + SessionDescriptionInterface* answer = session_->CreateAnswer(NULL); + ASSERT_TRUE(answer != NULL); + VerifyFingerprintStatus(answer->description(), false); + + // Now set the local description. + SetLocalDescriptionWithoutError(answer); +} + +TEST_F(WebRtcSessionTest, TestSetLocalOfferTwice) { + Init(); + mediastream_signaling_.SendNothing(); + // SetLocalDescription take ownership of offer. + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SetLocalDescriptionWithoutError(offer); + + // SetLocalDescription take ownership of offer. + SessionDescriptionInterface* offer2 = session_->CreateOffer(NULL); + SetLocalDescriptionWithoutError(offer2); +} + +TEST_F(WebRtcSessionTest, TestSetRemoteOfferTwice) { + Init(); + mediastream_signaling_.SendNothing(); + // SetLocalDescription take ownership of offer. + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SetRemoteDescriptionWithoutError(offer); + + SessionDescriptionInterface* offer2 = session_->CreateOffer(NULL); + SetRemoteDescriptionWithoutError(offer2); +} + +TEST_F(WebRtcSessionTest, TestSetLocalAndRemoteOffer) { + Init(); + mediastream_signaling_.SendNothing(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SetLocalDescriptionWithoutError(offer); + offer = session_->CreateOffer(NULL); + SetRemoteDescriptionExpectError( + "Called with type in wrong state, type: offer state: STATE_SENTINITIATE", + offer); +} + +TEST_F(WebRtcSessionTest, TestSetRemoteAndLocalOffer) { + Init(); + mediastream_signaling_.SendNothing(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SetRemoteDescriptionWithoutError(offer); + offer = session_->CreateOffer(NULL); + SetLocalDescriptionExpectError( + "Called with type in wrong state, type: " + "offer state: STATE_RECEIVEDINITIATE", + offer); +} + +TEST_F(WebRtcSessionTest, TestSetLocalPrAnswer) { + Init(); + mediastream_signaling_.SendNothing(); + SessionDescriptionInterface* offer = CreateRemoteOffer(); + SetRemoteDescriptionExpectState(offer, BaseSession::STATE_RECEIVEDINITIATE); + + JsepSessionDescription* pranswer = static_cast( + session_->CreateAnswer(NULL)); + pranswer->set_type(SessionDescriptionInterface::kPrAnswer); + SetLocalDescriptionExpectState(pranswer, BaseSession::STATE_SENTPRACCEPT); + + mediastream_signaling_.SendAudioVideoStream1(); + JsepSessionDescription* pranswer2 = static_cast( + session_->CreateAnswer(NULL)); + pranswer2->set_type(SessionDescriptionInterface::kPrAnswer); + + SetLocalDescriptionExpectState(pranswer2, BaseSession::STATE_SENTPRACCEPT); + + mediastream_signaling_.SendAudioVideoStream2(); + SessionDescriptionInterface* answer = session_->CreateAnswer(NULL); + SetLocalDescriptionExpectState(answer, BaseSession::STATE_SENTACCEPT); +} + +TEST_F(WebRtcSessionTest, TestSetRemotePrAnswer) { + Init(); + mediastream_signaling_.SendNothing(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SetLocalDescriptionExpectState(offer, BaseSession::STATE_SENTINITIATE); + + JsepSessionDescription* pranswer = + CreateRemoteAnswer(session_->local_description()); + pranswer->set_type(SessionDescriptionInterface::kPrAnswer); + + SetRemoteDescriptionExpectState(pranswer, + BaseSession::STATE_RECEIVEDPRACCEPT); + + mediastream_signaling_.SendAudioVideoStream1(); + JsepSessionDescription* pranswer2 = + CreateRemoteAnswer(session_->local_description()); + pranswer2->set_type(SessionDescriptionInterface::kPrAnswer); + + SetRemoteDescriptionExpectState(pranswer2, + BaseSession::STATE_RECEIVEDPRACCEPT); + + mediastream_signaling_.SendAudioVideoStream2(); + SessionDescriptionInterface* answer = + CreateRemoteAnswer(session_->local_description()); + SetRemoteDescriptionExpectState(answer, BaseSession::STATE_RECEIVEDACCEPT); +} + +TEST_F(WebRtcSessionTest, TestSetLocalAnswerWithoutOffer) { + Init(); + mediastream_signaling_.SendNothing(); + talk_base::scoped_ptr offer( + session_->CreateOffer(NULL)); + SessionDescriptionInterface* answer = + CreateRemoteAnswer(offer.get()); + SetLocalDescriptionExpectError( + "Called with type in wrong state, type: answer state: STATE_INIT", + answer); +} + +TEST_F(WebRtcSessionTest, TestSetRemoteAnswerWithoutOffer) { + Init(); + mediastream_signaling_.SendNothing(); + talk_base::scoped_ptr offer( + session_->CreateOffer(NULL)); + SessionDescriptionInterface* answer = + CreateRemoteAnswer(offer.get()); + SetRemoteDescriptionExpectError( + "Called with type in wrong state, type: answer state: STATE_INIT", + answer); +} + +TEST_F(WebRtcSessionTest, TestAddRemoteCandidate) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + + cricket::Candidate candidate; + candidate.set_component(1); + JsepIceCandidate ice_candidate1(kMediaContentName0, 0, candidate); + + // Fail since we have not set a offer description. + EXPECT_FALSE(session_->ProcessIceMessage(&ice_candidate1)); + + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SetLocalDescriptionWithoutError(offer); + // Candidate should be allowed to add before remote description. + EXPECT_TRUE(session_->ProcessIceMessage(&ice_candidate1)); + candidate.set_component(2); + JsepIceCandidate ice_candidate2(kMediaContentName0, 0, candidate); + EXPECT_TRUE(session_->ProcessIceMessage(&ice_candidate2)); + + SessionDescriptionInterface* answer = CreateRemoteAnswer( + session_->local_description()); + SetRemoteDescriptionWithoutError(answer); + + // Verifying the candidates are copied properly from internal vector. + const SessionDescriptionInterface* remote_desc = + session_->remote_description(); + ASSERT_TRUE(remote_desc != NULL); + ASSERT_EQ(2u, remote_desc->number_of_mediasections()); + const IceCandidateCollection* candidates = + remote_desc->candidates(kMediaContentIndex0); + ASSERT_EQ(2u, candidates->count()); + EXPECT_EQ(kMediaContentIndex0, candidates->at(0)->sdp_mline_index()); + EXPECT_EQ(kMediaContentName0, candidates->at(0)->sdp_mid()); + EXPECT_EQ(1, candidates->at(0)->candidate().component()); + EXPECT_EQ(2, candidates->at(1)->candidate().component()); + + candidate.set_component(2); + JsepIceCandidate ice_candidate3(kMediaContentName0, 0, candidate); + EXPECT_TRUE(session_->ProcessIceMessage(&ice_candidate3)); + ASSERT_EQ(3u, candidates->count()); + + JsepIceCandidate bad_ice_candidate("bad content name", 99, candidate); + EXPECT_FALSE(session_->ProcessIceMessage(&bad_ice_candidate)); +} + +// Test that a remote candidate is added to the remote session description and +// that it is retained if the remote session description is changed. +TEST_F(WebRtcSessionTest, TestRemoteCandidatesAddedToSessionDescription) { + Init(); + cricket::Candidate candidate1; + candidate1.set_component(1); + JsepIceCandidate ice_candidate1(kMediaContentName0, kMediaContentIndex0, + candidate1); + mediastream_signaling_.SendAudioVideoStream1(); + CreateAndSetRemoteOfferAndLocalAnswer(); + + EXPECT_TRUE(session_->ProcessIceMessage(&ice_candidate1)); + const SessionDescriptionInterface* remote_desc = + session_->remote_description(); + ASSERT_TRUE(remote_desc != NULL); + ASSERT_EQ(2u, remote_desc->number_of_mediasections()); + const IceCandidateCollection* candidates = + remote_desc->candidates(kMediaContentIndex0); + ASSERT_EQ(1u, candidates->count()); + EXPECT_EQ(kMediaContentIndex0, candidates->at(0)->sdp_mline_index()); + + // Update the RemoteSessionDescription with a new session description and + // a candidate and check that the new remote session description contains both + // candidates. + SessionDescriptionInterface* offer = CreateRemoteOffer(); + cricket::Candidate candidate2; + JsepIceCandidate ice_candidate2(kMediaContentName0, kMediaContentIndex0, + candidate2); + EXPECT_TRUE(offer->AddCandidate(&ice_candidate2)); + SetRemoteDescriptionWithoutError(offer); + + remote_desc = session_->remote_description(); + ASSERT_TRUE(remote_desc != NULL); + ASSERT_EQ(2u, remote_desc->number_of_mediasections()); + candidates = remote_desc->candidates(kMediaContentIndex0); + ASSERT_EQ(2u, candidates->count()); + EXPECT_EQ(kMediaContentIndex0, candidates->at(0)->sdp_mline_index()); + // Username and password have be updated with the TransportInfo of the + // SessionDescription, won't be equal to the original one. + candidate2.set_username(candidates->at(0)->candidate().username()); + candidate2.set_password(candidates->at(0)->candidate().password()); + EXPECT_TRUE(candidate2.IsEquivalent(candidates->at(0)->candidate())); + EXPECT_EQ(kMediaContentIndex0, candidates->at(1)->sdp_mline_index()); + // No need to verify the username and password. + candidate1.set_username(candidates->at(1)->candidate().username()); + candidate1.set_password(candidates->at(1)->candidate().password()); + EXPECT_TRUE(candidate1.IsEquivalent(candidates->at(1)->candidate())); + + // Test that the candidate is ignored if we can add the same candidate again. + EXPECT_TRUE(session_->ProcessIceMessage(&ice_candidate2)); +} + +// Test that local candidates are added to the local session description and +// that they are retained if the local session description is changed. +TEST_F(WebRtcSessionTest, TestLocalCandidatesAddedToSessionDescription) { + AddInterface(kClientAddr1); + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + CreateAndSetRemoteOfferAndLocalAnswer(); + + const SessionDescriptionInterface* local_desc = session_->local_description(); + const IceCandidateCollection* candidates = + local_desc->candidates(kMediaContentIndex0); + ASSERT_TRUE(candidates != NULL); + EXPECT_EQ(0u, candidates->count()); + + EXPECT_TRUE_WAIT(observer_.oncandidatesready_, kIceCandidatesTimeout); + + local_desc = session_->local_description(); + candidates = local_desc->candidates(kMediaContentIndex0); + ASSERT_TRUE(candidates != NULL); + EXPECT_LT(0u, candidates->count()); + candidates = local_desc->candidates(1); + ASSERT_TRUE(candidates != NULL); + EXPECT_LT(0u, candidates->count()); + + // Update the session descriptions. + mediastream_signaling_.SendAudioVideoStream1(); + CreateAndSetRemoteOfferAndLocalAnswer(); + + local_desc = session_->local_description(); + candidates = local_desc->candidates(kMediaContentIndex0); + ASSERT_TRUE(candidates != NULL); + EXPECT_LT(0u, candidates->count()); + candidates = local_desc->candidates(1); + ASSERT_TRUE(candidates != NULL); + EXPECT_LT(0u, candidates->count()); +} + +// Test that we can set a remote session description with remote candidates. +TEST_F(WebRtcSessionTest, TestSetRemoteSessionDescriptionWithCandidates) { + Init(); + + cricket::Candidate candidate1; + candidate1.set_component(1); + JsepIceCandidate ice_candidate(kMediaContentName0, kMediaContentIndex0, + candidate1); + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + + EXPECT_TRUE(offer->AddCandidate(&ice_candidate)); + SetRemoteDescriptionWithoutError(offer); + + const SessionDescriptionInterface* remote_desc = + session_->remote_description(); + ASSERT_TRUE(remote_desc != NULL); + ASSERT_EQ(2u, remote_desc->number_of_mediasections()); + const IceCandidateCollection* candidates = + remote_desc->candidates(kMediaContentIndex0); + ASSERT_EQ(1u, candidates->count()); + EXPECT_EQ(kMediaContentIndex0, candidates->at(0)->sdp_mline_index()); + + SessionDescriptionInterface* answer = session_->CreateAnswer(NULL); + SetLocalDescriptionWithoutError(answer); +} + +// Test that offers and answers contains ice candidates when Ice candidates have +// been gathered. +TEST_F(WebRtcSessionTest, TestSetLocalAndRemoteDescriptionWithCandidates) { + AddInterface(kClientAddr1); + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + // Ice is started but candidates are not provided until SetLocalDescription + // is called. + EXPECT_EQ(0u, observer_.mline_0_candidates_.size()); + EXPECT_EQ(0u, observer_.mline_1_candidates_.size()); + CreateAndSetRemoteOfferAndLocalAnswer(); + // Wait until at least one local candidate has been collected. + EXPECT_TRUE_WAIT(0u < observer_.mline_0_candidates_.size(), + kIceCandidatesTimeout); + EXPECT_TRUE_WAIT(0u < observer_.mline_1_candidates_.size(), + kIceCandidatesTimeout); + + talk_base::scoped_ptr local_offer( + session_->CreateOffer(NULL)); + ASSERT_TRUE(local_offer->candidates(kMediaContentIndex0) != NULL); + EXPECT_LT(0u, local_offer->candidates(kMediaContentIndex0)->count()); + ASSERT_TRUE(local_offer->candidates(kMediaContentIndex1) != NULL); + EXPECT_LT(0u, local_offer->candidates(kMediaContentIndex1)->count()); + + SessionDescriptionInterface* remote_offer(CreateRemoteOffer()); + SetRemoteDescriptionWithoutError(remote_offer); + SessionDescriptionInterface* answer = session_->CreateAnswer(NULL); + ASSERT_TRUE(answer->candidates(kMediaContentIndex0) != NULL); + EXPECT_LT(0u, answer->candidates(kMediaContentIndex0)->count()); + ASSERT_TRUE(answer->candidates(kMediaContentIndex1) != NULL); + EXPECT_LT(0u, answer->candidates(kMediaContentIndex1)->count()); + SetLocalDescriptionWithoutError(answer); +} + +// Verifies TransportProxy and media channels are created with content names +// present in the SessionDescription. +TEST_F(WebRtcSessionTest, TestChannelCreationsWithContentNames) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + talk_base::scoped_ptr offer( + session_->CreateOffer(NULL)); + + // CreateOffer creates session description with the content names "audio" and + // "video". Goal is to modify these content names and verify transport channel + // proxy in the BaseSession, as proxies are created with the content names + // present in SDP. + std::string sdp; + EXPECT_TRUE(offer->ToString(&sdp)); + const std::string kAudioMid = "a=mid:audio"; + const std::string kAudioMidReplaceStr = "a=mid:audio_content_name"; + const std::string kVideoMid = "a=mid:video"; + const std::string kVideoMidReplaceStr = "a=mid:video_content_name"; + + // Replacing |audio| with |audio_content_name|. + talk_base::replace_substrs(kAudioMid.c_str(), kAudioMid.length(), + kAudioMidReplaceStr.c_str(), + kAudioMidReplaceStr.length(), + &sdp); + // Replacing |video| with |video_content_name|. + talk_base::replace_substrs(kVideoMid.c_str(), kVideoMid.length(), + kVideoMidReplaceStr.c_str(), + kVideoMidReplaceStr.length(), + &sdp); + + SessionDescriptionInterface* modified_offer = + CreateSessionDescription(JsepSessionDescription::kOffer, sdp, NULL); + + SetRemoteDescriptionWithoutError(modified_offer); + + SessionDescriptionInterface* answer = + session_->CreateAnswer(NULL); + SetLocalDescriptionWithoutError(answer); + + EXPECT_TRUE(session_->GetTransportProxy("audio_content_name") != NULL); + EXPECT_TRUE(session_->GetTransportProxy("video_content_name") != NULL); + EXPECT_TRUE((video_channel_ = media_engine_->GetVideoChannel(0)) != NULL); + EXPECT_TRUE((voice_channel_ = media_engine_->GetVoiceChannel(0)) != NULL); +} + +// Test that an offer contains the correct media content descriptions based on +// the send streams when no constraints have been set. +TEST_F(WebRtcSessionTest, CreateOfferWithoutConstraintsOrStreams) { + Init(); + talk_base::scoped_ptr offer( + session_->CreateOffer(NULL)); + ASSERT_TRUE(offer != NULL); + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(offer->description()); + EXPECT_TRUE(content != NULL); + content = cricket::GetFirstVideoContent(offer->description()); + EXPECT_TRUE(content == NULL); +} + +// Test that an offer contains the correct media content descriptions based on +// the send streams when no constraints have been set. +TEST_F(WebRtcSessionTest, CreateOfferWithoutConstraints) { + Init(); + // Test Audio only offer. + mediastream_signaling_.UseOptionsAudioOnly(); + talk_base::scoped_ptr offer( + session_->CreateOffer(NULL)); + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(offer->description()); + EXPECT_TRUE(content != NULL); + content = cricket::GetFirstVideoContent(offer->description()); + EXPECT_TRUE(content == NULL); + + // Test Audio / Video offer. + mediastream_signaling_.SendAudioVideoStream1(); + offer.reset(session_->CreateOffer(NULL)); + content = cricket::GetFirstAudioContent(offer->description()); + EXPECT_TRUE(content != NULL); + content = cricket::GetFirstVideoContent(offer->description()); + EXPECT_TRUE(content != NULL); +} + +// Test that an offer contains no media content descriptions if +// kOfferToReceiveVideo and kOfferToReceiveAudio constraints are set to false. +TEST_F(WebRtcSessionTest, CreateOfferWithConstraintsWithoutStreams) { + Init(); + webrtc::FakeConstraints constraints_no_receive; + constraints_no_receive.SetMandatoryReceiveAudio(false); + constraints_no_receive.SetMandatoryReceiveVideo(false); + + talk_base::scoped_ptr offer( + session_->CreateOffer(&constraints_no_receive)); + ASSERT_TRUE(offer != NULL); + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(offer->description()); + EXPECT_TRUE(content == NULL); + content = cricket::GetFirstVideoContent(offer->description()); + EXPECT_TRUE(content == NULL); +} + +// Test that an offer contains only audio media content descriptions if +// kOfferToReceiveAudio constraints are set to true. +TEST_F(WebRtcSessionTest, CreateAudioOnlyOfferWithConstraints) { + Init(); + webrtc::FakeConstraints constraints_audio_only; + constraints_audio_only.SetMandatoryReceiveAudio(true); + talk_base::scoped_ptr offer( + session_->CreateOffer(&constraints_audio_only)); + + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(offer->description()); + EXPECT_TRUE(content != NULL); + content = cricket::GetFirstVideoContent(offer->description()); + EXPECT_TRUE(content == NULL); +} + +// Test that an offer contains audio and video media content descriptions if +// kOfferToReceiveAudio and kOfferToReceiveVideo constraints are set to true. +TEST_F(WebRtcSessionTest, CreateOfferWithConstraints) { + Init(); + // Test Audio / Video offer. + webrtc::FakeConstraints constraints_audio_video; + constraints_audio_video.SetMandatoryReceiveAudio(true); + constraints_audio_video.SetMandatoryReceiveVideo(true); + talk_base::scoped_ptr offer( + session_->CreateOffer(&constraints_audio_video)); + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(offer->description()); + + EXPECT_TRUE(content != NULL); + content = cricket::GetFirstVideoContent(offer->description()); + EXPECT_TRUE(content != NULL); + + // TODO(perkj): Should the direction be set to SEND_ONLY if + // The constraints is set to not receive audio or video but a track is added? +} + +// Test that an answer can not be created if the last remote description is not +// an offer. +TEST_F(WebRtcSessionTest, CreateAnswerWithoutAnOffer) { + Init(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SetLocalDescriptionWithoutError(offer); + SessionDescriptionInterface* answer = CreateRemoteAnswer(offer); + SetRemoteDescriptionWithoutError(answer); + EXPECT_TRUE(session_->CreateAnswer(NULL) == NULL); +} + +// Test that an answer contains the correct media content descriptions when no +// constraints have been set. +TEST_F(WebRtcSessionTest, CreateAnswerWithoutConstraintsOrStreams) { + Init(); + // Create a remote offer with audio and video content. + talk_base::scoped_ptr offer(CreateRemoteOffer()); + SetRemoteDescriptionWithoutError(offer.release()); + talk_base::scoped_ptr answer( + session_->CreateAnswer(NULL)); + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(answer->description()); + ASSERT_TRUE(content != NULL); + EXPECT_FALSE(content->rejected); + + content = cricket::GetFirstVideoContent(answer->description()); + ASSERT_TRUE(content != NULL); + EXPECT_FALSE(content->rejected); +} + +// Test that an answer contains the correct media content descriptions when no +// constraints have been set and the offer only contain audio. +TEST_F(WebRtcSessionTest, CreateAudioAnswerWithoutConstraintsOrStreams) { + Init(); + // Create a remote offer with audio only. + cricket::MediaSessionOptions options; + options.has_audio = true; + options.has_video = false; + talk_base::scoped_ptr offer( + CreateRemoteOffer(options)); + ASSERT_TRUE(cricket::GetFirstVideoContent(offer->description()) == NULL); + ASSERT_TRUE(cricket::GetFirstAudioContent(offer->description()) != NULL); + + SetRemoteDescriptionWithoutError(offer.release()); + talk_base::scoped_ptr answer( + session_->CreateAnswer(NULL)); + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(answer->description()); + ASSERT_TRUE(content != NULL); + EXPECT_FALSE(content->rejected); + + EXPECT_TRUE(cricket::GetFirstVideoContent(answer->description()) == NULL); +} + +// Test that an answer contains the correct media content descriptions when no +// constraints have been set. +TEST_F(WebRtcSessionTest, CreateAnswerWithoutConstraints) { + Init(); + // Create a remote offer with audio and video content. + talk_base::scoped_ptr offer(CreateRemoteOffer()); + SetRemoteDescriptionWithoutError(offer.release()); + // Test with a stream with tracks. + mediastream_signaling_.SendAudioVideoStream1(); + talk_base::scoped_ptr answer( + session_->CreateAnswer(NULL)); + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(answer->description()); + ASSERT_TRUE(content != NULL); + EXPECT_FALSE(content->rejected); + + content = cricket::GetFirstVideoContent(answer->description()); + ASSERT_TRUE(content != NULL); + EXPECT_FALSE(content->rejected); +} + +// Test that an answer contains the correct media content descriptions when +// constraints have been set but no stream is sent. +TEST_F(WebRtcSessionTest, CreateAnswerWithConstraintsWithoutStreams) { + Init(); + // Create a remote offer with audio and video content. + talk_base::scoped_ptr offer(CreateRemoteOffer()); + SetRemoteDescriptionWithoutError(offer.release()); + + webrtc::FakeConstraints constraints_no_receive; + constraints_no_receive.SetMandatoryReceiveAudio(false); + constraints_no_receive.SetMandatoryReceiveVideo(false); + + talk_base::scoped_ptr answer( + session_->CreateAnswer(&constraints_no_receive)); + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(answer->description()); + ASSERT_TRUE(content != NULL); + EXPECT_TRUE(content->rejected); + + content = cricket::GetFirstVideoContent(answer->description()); + ASSERT_TRUE(content != NULL); + EXPECT_TRUE(content->rejected); +} + +// Test that an answer contains the correct media content descriptions when +// constraints have been set and streams are sent. +TEST_F(WebRtcSessionTest, CreateAnswerWithConstraints) { + Init(); + // Create a remote offer with audio and video content. + talk_base::scoped_ptr offer(CreateRemoteOffer()); + SetRemoteDescriptionWithoutError(offer.release()); + + webrtc::FakeConstraints constraints_no_receive; + constraints_no_receive.SetMandatoryReceiveAudio(false); + constraints_no_receive.SetMandatoryReceiveVideo(false); + + // Test with a stream with tracks. + mediastream_signaling_.SendAudioVideoStream1(); + talk_base::scoped_ptr answer( + session_->CreateAnswer(&constraints_no_receive)); + + // TODO(perkj): Should the direction be set to SEND_ONLY? + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(answer->description()); + ASSERT_TRUE(content != NULL); + EXPECT_FALSE(content->rejected); + + // TODO(perkj): Should the direction be set to SEND_ONLY? + content = cricket::GetFirstVideoContent(answer->description()); + ASSERT_TRUE(content != NULL); + EXPECT_FALSE(content->rejected); +} + +TEST_F(WebRtcSessionTest, CreateOfferWithoutCNCodecs) { + AddCNCodecs(); + Init(); + webrtc::FakeConstraints constraints; + constraints.SetOptionalVAD(false); + talk_base::scoped_ptr offer( + session_->CreateOffer(&constraints)); + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(offer->description()); + EXPECT_TRUE(content != NULL); + EXPECT_TRUE(VerifyNoCNCodecs(content)); +} + +TEST_F(WebRtcSessionTest, CreateAnswerWithoutCNCodecs) { + AddCNCodecs(); + Init(); + // Create a remote offer with audio and video content. + talk_base::scoped_ptr offer(CreateRemoteOffer()); + SetRemoteDescriptionWithoutError(offer.release()); + + webrtc::FakeConstraints constraints; + constraints.SetOptionalVAD(false); + talk_base::scoped_ptr answer( + session_->CreateAnswer(&constraints)); + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(answer->description()); + ASSERT_TRUE(content != NULL); + EXPECT_TRUE(VerifyNoCNCodecs(content)); +} + +// This test verifies the call setup when remote answer with audio only and +// later updates with video. +TEST_F(WebRtcSessionTest, TestAVOfferWithAudioOnlyAnswer) { + Init(); + EXPECT_TRUE(media_engine_->GetVideoChannel(0) == NULL); + EXPECT_TRUE(media_engine_->GetVoiceChannel(0) == NULL); + + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + + cricket::MediaSessionOptions options; + options.has_video = false; + SessionDescriptionInterface* answer = CreateRemoteAnswer(offer, options); + + // SetLocalDescription and SetRemoteDescriptions takes ownership of offer + // and answer; + SetLocalDescriptionWithoutError(offer); + SetRemoteDescriptionWithoutError(answer); + + video_channel_ = media_engine_->GetVideoChannel(0); + voice_channel_ = media_engine_->GetVoiceChannel(0); + + ASSERT_TRUE(video_channel_ == NULL); + + ASSERT_EQ(0u, voice_channel_->recv_streams().size()); + ASSERT_EQ(1u, voice_channel_->send_streams().size()); + EXPECT_EQ(kAudioTrack1, voice_channel_->send_streams()[0].id); + + // Let the remote end update the session descriptions, with Audio and Video. + mediastream_signaling_.SendAudioVideoStream2(); + CreateAndSetRemoteOfferAndLocalAnswer(); + + video_channel_ = media_engine_->GetVideoChannel(0); + voice_channel_ = media_engine_->GetVoiceChannel(0); + + ASSERT_TRUE(video_channel_ != NULL); + ASSERT_TRUE(voice_channel_ != NULL); + + ASSERT_EQ(1u, video_channel_->recv_streams().size()); + ASSERT_EQ(1u, video_channel_->send_streams().size()); + EXPECT_EQ(kVideoTrack2, video_channel_->recv_streams()[0].id); + EXPECT_EQ(kVideoTrack2, video_channel_->send_streams()[0].id); + ASSERT_EQ(1u, voice_channel_->recv_streams().size()); + ASSERT_EQ(1u, voice_channel_->send_streams().size()); + EXPECT_EQ(kAudioTrack2, voice_channel_->recv_streams()[0].id); + EXPECT_EQ(kAudioTrack2, voice_channel_->send_streams()[0].id); + + // Change session back to audio only. + mediastream_signaling_.UseOptionsAudioOnly(); + CreateAndSetRemoteOfferAndLocalAnswer(); + + EXPECT_EQ(0u, video_channel_->recv_streams().size()); + ASSERT_EQ(1u, voice_channel_->recv_streams().size()); + EXPECT_EQ(kAudioTrack2, voice_channel_->recv_streams()[0].id); + ASSERT_EQ(1u, voice_channel_->send_streams().size()); + EXPECT_EQ(kAudioTrack2, voice_channel_->send_streams()[0].id); +} + +// This test verifies the call setup when remote answer with video only and +// later updates with audio. +TEST_F(WebRtcSessionTest, TestAVOfferWithVideoOnlyAnswer) { + Init(); + EXPECT_TRUE(media_engine_->GetVideoChannel(0) == NULL); + EXPECT_TRUE(media_engine_->GetVoiceChannel(0) == NULL); + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + + cricket::MediaSessionOptions options; + options.has_audio = false; + options.has_video = true; + SessionDescriptionInterface* answer = CreateRemoteAnswer( + offer, options, cricket::SEC_ENABLED); + + // SetLocalDescription and SetRemoteDescriptions takes ownership of offer + // and answer. + SetLocalDescriptionWithoutError(offer); + SetRemoteDescriptionWithoutError(answer); + + video_channel_ = media_engine_->GetVideoChannel(0); + voice_channel_ = media_engine_->GetVoiceChannel(0); + + ASSERT_TRUE(voice_channel_ == NULL); + ASSERT_TRUE(video_channel_ != NULL); + + EXPECT_EQ(0u, video_channel_->recv_streams().size()); + ASSERT_EQ(1u, video_channel_->send_streams().size()); + EXPECT_EQ(kVideoTrack1, video_channel_->send_streams()[0].id); + + // Update the session descriptions, with Audio and Video. + mediastream_signaling_.SendAudioVideoStream2(); + CreateAndSetRemoteOfferAndLocalAnswer(); + + voice_channel_ = media_engine_->GetVoiceChannel(0); + ASSERT_TRUE(voice_channel_ != NULL); + + ASSERT_EQ(1u, voice_channel_->recv_streams().size()); + ASSERT_EQ(1u, voice_channel_->send_streams().size()); + EXPECT_EQ(kAudioTrack2, voice_channel_->recv_streams()[0].id); + EXPECT_EQ(kAudioTrack2, voice_channel_->send_streams()[0].id); + + // Change session back to video only. + mediastream_signaling_.UseOptionsVideoOnly(); + CreateAndSetRemoteOfferAndLocalAnswer(); + + video_channel_ = media_engine_->GetVideoChannel(0); + voice_channel_ = media_engine_->GetVoiceChannel(0); + + ASSERT_EQ(1u, video_channel_->recv_streams().size()); + EXPECT_EQ(kVideoTrack2, video_channel_->recv_streams()[0].id); + ASSERT_EQ(1u, video_channel_->send_streams().size()); + EXPECT_EQ(kVideoTrack2, video_channel_->send_streams()[0].id); +} + +TEST_F(WebRtcSessionTest, TestDefaultSetSecurePolicy) { + Init(); + EXPECT_EQ(cricket::SEC_REQUIRED, session_->secure_policy()); +} + +TEST_F(WebRtcSessionTest, VerifyCryptoParamsInSDP) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + scoped_ptr offer( + session_->CreateOffer(NULL)); + VerifyCryptoParams(offer->description()); + SetRemoteDescriptionWithoutError(offer.release()); + const webrtc::SessionDescriptionInterface* answer = + session_->CreateAnswer(NULL); + VerifyCryptoParams(answer->description()); +} + +TEST_F(WebRtcSessionTest, VerifyNoCryptoParamsInSDP) { + Init(); + session_->set_secure_policy(cricket::SEC_DISABLED); + mediastream_signaling_.SendAudioVideoStream1(); + scoped_ptr offer( + session_->CreateOffer(NULL)); + VerifyNoCryptoParams(offer->description(), false); +} + +TEST_F(WebRtcSessionTest, VerifyAnswerFromNonCryptoOffer) { + Init(); + VerifyAnswerFromNonCryptoOffer(); +} + +TEST_F(WebRtcSessionTest, VerifyAnswerFromCryptoOffer) { + Init(); + VerifyAnswerFromCryptoOffer(); +} + +TEST_F(WebRtcSessionTest, VerifyBundleFlagInPA) { + // This test verifies BUNDLE flag in PortAllocator, if BUNDLE information in + // local description is removed by the application, BUNDLE flag should be + // disabled in PortAllocator. By default BUNDLE is enabled in the WebRtc. + Init(); + EXPECT_TRUE((cricket::PORTALLOCATOR_ENABLE_BUNDLE & allocator_.flags()) == + cricket::PORTALLOCATOR_ENABLE_BUNDLE); + talk_base::scoped_ptr offer( + session_->CreateOffer(NULL)); + cricket::SessionDescription* offer_copy = + offer->description()->Copy(); + offer_copy->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE); + JsepSessionDescription* modified_offer = + new JsepSessionDescription(JsepSessionDescription::kOffer); + modified_offer->Initialize(offer_copy, "1", "1"); + + SetLocalDescriptionWithoutError(modified_offer); + EXPECT_FALSE(allocator_.flags() & cricket::PORTALLOCATOR_ENABLE_BUNDLE); +} + +TEST_F(WebRtcSessionTest, TestDisabledBundleInAnswer) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + EXPECT_TRUE((cricket::PORTALLOCATOR_ENABLE_BUNDLE & allocator_.flags()) == + cricket::PORTALLOCATOR_ENABLE_BUNDLE); + FakeConstraints constraints; + constraints.SetMandatoryUseRtpMux(true); + SessionDescriptionInterface* offer = session_->CreateOffer(&constraints); + SetLocalDescriptionWithoutError(offer); + mediastream_signaling_.SendAudioVideoStream2(); + talk_base::scoped_ptr answer( + CreateRemoteAnswer(session_->local_description())); + cricket::SessionDescription* answer_copy = answer->description()->Copy(); + answer_copy->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE); + JsepSessionDescription* modified_answer = + new JsepSessionDescription(JsepSessionDescription::kAnswer); + modified_answer->Initialize(answer_copy, "1", "1"); + SetRemoteDescriptionWithoutError(modified_answer); + EXPECT_TRUE((cricket::PORTALLOCATOR_ENABLE_BUNDLE & allocator_.flags()) == + cricket::PORTALLOCATOR_ENABLE_BUNDLE); + + video_channel_ = media_engine_->GetVideoChannel(0); + voice_channel_ = media_engine_->GetVoiceChannel(0); + + ASSERT_EQ(1u, video_channel_->recv_streams().size()); + EXPECT_TRUE(kVideoTrack2 == video_channel_->recv_streams()[0].id); + + ASSERT_EQ(1u, voice_channel_->recv_streams().size()); + EXPECT_TRUE(kAudioTrack2 == voice_channel_->recv_streams()[0].id); + + ASSERT_EQ(1u, video_channel_->send_streams().size()); + EXPECT_TRUE(kVideoTrack1 == video_channel_->send_streams()[0].id); + ASSERT_EQ(1u, voice_channel_->send_streams().size()); + EXPECT_TRUE(kAudioTrack1 == voice_channel_->send_streams()[0].id); +} + +TEST_F(WebRtcSessionTest, SetAudioPlayout) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + CreateAndSetRemoteOfferAndLocalAnswer(); + cricket::FakeVoiceMediaChannel* channel = media_engine_->GetVoiceChannel(0); + ASSERT_TRUE(channel != NULL); + ASSERT_EQ(1u, channel->recv_streams().size()); + uint32 receive_ssrc = channel->recv_streams()[0].first_ssrc(); + double left_vol, right_vol; + EXPECT_TRUE(channel->GetOutputScaling(receive_ssrc, &left_vol, &right_vol)); + EXPECT_EQ(1, left_vol); + EXPECT_EQ(1, right_vol); + session_->SetAudioPlayout(receive_ssrc, false); + EXPECT_TRUE(channel->GetOutputScaling(receive_ssrc, &left_vol, &right_vol)); + EXPECT_EQ(0, left_vol); + EXPECT_EQ(0, right_vol); + session_->SetAudioPlayout(receive_ssrc, true); + EXPECT_TRUE(channel->GetOutputScaling(receive_ssrc, &left_vol, &right_vol)); + EXPECT_EQ(1, left_vol); + EXPECT_EQ(1, right_vol); +} + +TEST_F(WebRtcSessionTest, SetAudioSend) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + CreateAndSetRemoteOfferAndLocalAnswer(); + cricket::FakeVoiceMediaChannel* channel = media_engine_->GetVoiceChannel(0); + ASSERT_TRUE(channel != NULL); + ASSERT_EQ(1u, channel->send_streams().size()); + uint32 send_ssrc = channel->send_streams()[0].first_ssrc(); + EXPECT_FALSE(channel->IsStreamMuted(send_ssrc)); + + cricket::AudioOptions options; + options.echo_cancellation.Set(true); + + session_->SetAudioSend(send_ssrc, false, options); + EXPECT_TRUE(channel->IsStreamMuted(send_ssrc)); + EXPECT_FALSE(channel->options().echo_cancellation.IsSet()); + + session_->SetAudioSend(send_ssrc, true, options); + EXPECT_FALSE(channel->IsStreamMuted(send_ssrc)); + bool value; + EXPECT_TRUE(channel->options().echo_cancellation.Get(&value)); + EXPECT_TRUE(value); +} + +TEST_F(WebRtcSessionTest, SetVideoPlayout) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + CreateAndSetRemoteOfferAndLocalAnswer(); + cricket::FakeVideoMediaChannel* channel = media_engine_->GetVideoChannel(0); + ASSERT_TRUE(channel != NULL); + ASSERT_LT(0u, channel->renderers().size()); + EXPECT_TRUE(channel->renderers().begin()->second == NULL); + ASSERT_EQ(1u, channel->recv_streams().size()); + uint32 receive_ssrc = channel->recv_streams()[0].first_ssrc(); + cricket::FakeVideoRenderer renderer; + session_->SetVideoPlayout(receive_ssrc, true, &renderer); + EXPECT_TRUE(channel->renderers().begin()->second == &renderer); + session_->SetVideoPlayout(receive_ssrc, false, &renderer); + EXPECT_TRUE(channel->renderers().begin()->second == NULL); +} + +TEST_F(WebRtcSessionTest, SetVideoSend) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + CreateAndSetRemoteOfferAndLocalAnswer(); + cricket::FakeVideoMediaChannel* channel = media_engine_->GetVideoChannel(0); + ASSERT_TRUE(channel != NULL); + ASSERT_EQ(1u, channel->send_streams().size()); + uint32 send_ssrc = channel->send_streams()[0].first_ssrc(); + EXPECT_FALSE(channel->IsStreamMuted(send_ssrc)); + cricket::VideoOptions* options = NULL; + session_->SetVideoSend(send_ssrc, false, options); + EXPECT_TRUE(channel->IsStreamMuted(send_ssrc)); + session_->SetVideoSend(send_ssrc, true, options); + EXPECT_FALSE(channel->IsStreamMuted(send_ssrc)); +} + +TEST_F(WebRtcSessionTest, CanNotInsertDtmf) { + TestCanInsertDtmf(false); +} + +TEST_F(WebRtcSessionTest, CanInsertDtmf) { + TestCanInsertDtmf(true); +} + +TEST_F(WebRtcSessionTest, InsertDtmf) { + // Setup + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + CreateAndSetRemoteOfferAndLocalAnswer(); + FakeVoiceMediaChannel* channel = media_engine_->GetVoiceChannel(0); + EXPECT_EQ(0U, channel->dtmf_info_queue().size()); + + // Insert DTMF + const int expected_flags = DF_SEND; + const int expected_duration = 90; + session_->InsertDtmf(kAudioTrack1, 0, expected_duration); + session_->InsertDtmf(kAudioTrack1, 1, expected_duration); + session_->InsertDtmf(kAudioTrack1, 2, expected_duration); + + // Verify + ASSERT_EQ(3U, channel->dtmf_info_queue().size()); + const uint32 send_ssrc = channel->send_streams()[0].first_ssrc(); + EXPECT_TRUE(CompareDtmfInfo(channel->dtmf_info_queue()[0], send_ssrc, 0, + expected_duration, expected_flags)); + EXPECT_TRUE(CompareDtmfInfo(channel->dtmf_info_queue()[1], send_ssrc, 1, + expected_duration, expected_flags)); + EXPECT_TRUE(CompareDtmfInfo(channel->dtmf_info_queue()[2], send_ssrc, 2, + expected_duration, expected_flags)); +} + +// This test verifies the |initiator| flag when session initiates the call. +TEST_F(WebRtcSessionTest, TestInitiatorFlagAsOriginator) { + Init(); + EXPECT_FALSE(session_->initiator()); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SessionDescriptionInterface* answer = CreateRemoteAnswer(offer); + SetLocalDescriptionWithoutError(offer); + EXPECT_TRUE(session_->initiator()); + SetRemoteDescriptionWithoutError(answer); + EXPECT_TRUE(session_->initiator()); +} + +// This test verifies the |initiator| flag when session receives the call. +TEST_F(WebRtcSessionTest, TestInitiatorFlagAsReceiver) { + Init(); + EXPECT_FALSE(session_->initiator()); + SessionDescriptionInterface* offer = CreateRemoteOffer(); + SetRemoteDescriptionWithoutError(offer); + SessionDescriptionInterface* answer = session_->CreateAnswer(NULL); + + EXPECT_FALSE(session_->initiator()); + SetLocalDescriptionWithoutError(answer); + EXPECT_FALSE(session_->initiator()); +} + +// This test verifies the ice protocol type at initiator of the call +// if |a=ice-options:google-ice| is present in answer. +TEST_F(WebRtcSessionTest, TestInitiatorGIceInAnswer) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SessionDescriptionInterface* answer = CreateRemoteAnswer(offer); + SetLocalDescriptionWithoutError(offer); + std::string sdp; + EXPECT_TRUE(answer->ToString(&sdp)); + // Adding ice-options to the session level. + InjectAfter("t=0 0\r\n", + "a=ice-options:google-ice\r\n", + &sdp); + SessionDescriptionInterface* answer_with_gice = + CreateSessionDescription(JsepSessionDescription::kAnswer, sdp, NULL); + SetRemoteDescriptionWithoutError(answer_with_gice); + VerifyTransportType("audio", cricket::ICEPROTO_GOOGLE); + VerifyTransportType("video", cricket::ICEPROTO_GOOGLE); +} + +// This test verifies the ice protocol type at initiator of the call +// if ICE RFC5245 is supported in answer. +TEST_F(WebRtcSessionTest, TestInitiatorIceInAnswer) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SessionDescriptionInterface* answer = CreateRemoteAnswer(offer); + SetLocalDescriptionWithoutError(offer); + + SetRemoteDescriptionWithoutError(answer); + VerifyTransportType("audio", cricket::ICEPROTO_RFC5245); + VerifyTransportType("video", cricket::ICEPROTO_RFC5245); +} + +// This test verifies the ice protocol type at receiver side of the call if +// receiver decides to use google-ice. +TEST_F(WebRtcSessionTest, TestReceiverGIceInOffer) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SetRemoteDescriptionWithoutError(offer); + SessionDescriptionInterface* answer = session_->CreateAnswer(NULL); + std::string sdp; + EXPECT_TRUE(answer->ToString(&sdp)); + // Adding ice-options to the session level. + InjectAfter("t=0 0\r\n", + "a=ice-options:google-ice\r\n", + &sdp); + SessionDescriptionInterface* answer_with_gice = + CreateSessionDescription(JsepSessionDescription::kAnswer, sdp, NULL); + SetLocalDescriptionWithoutError(answer_with_gice); + VerifyTransportType("audio", cricket::ICEPROTO_GOOGLE); + VerifyTransportType("video", cricket::ICEPROTO_GOOGLE); +} + +// This test verifies the ice protocol type at receiver side of the call if +// receiver decides to use ice RFC 5245. +TEST_F(WebRtcSessionTest, TestReceiverIceInOffer) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SetRemoteDescriptionWithoutError(offer); + SessionDescriptionInterface* answer = session_->CreateAnswer(NULL); + SetLocalDescriptionWithoutError(answer); + VerifyTransportType("audio", cricket::ICEPROTO_RFC5245); + VerifyTransportType("video", cricket::ICEPROTO_RFC5245); +} + +// This test verifies the session state when ICE RFC5245 in offer and +// ICE google-ice in answer. +TEST_F(WebRtcSessionTest, TestIceOfferGIceOnlyAnswer) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + talk_base::scoped_ptr offer( + session_->CreateOffer(NULL)); + std::string offer_str; + offer->ToString(&offer_str); + // Disable google-ice + const std::string gice_option = "google-ice"; + const std::string xgoogle_xice = "xgoogle-xice"; + talk_base::replace_substrs(gice_option.c_str(), gice_option.length(), + xgoogle_xice.c_str(), xgoogle_xice.length(), + &offer_str); + JsepSessionDescription *ice_only_offer = + new JsepSessionDescription(JsepSessionDescription::kOffer); + EXPECT_TRUE((ice_only_offer)->Initialize(offer_str, NULL)); + SetLocalDescriptionWithoutError(ice_only_offer); + std::string original_offer_sdp; + EXPECT_TRUE(offer->ToString(&original_offer_sdp)); + SessionDescriptionInterface* pranswer_with_gice = + CreateSessionDescription(JsepSessionDescription::kPrAnswer, + original_offer_sdp, NULL); + SetRemoteDescriptionExpectError(kPushDownPranswerTDFailed, + pranswer_with_gice); + SessionDescriptionInterface* answer_with_gice = + CreateSessionDescription(JsepSessionDescription::kAnswer, + original_offer_sdp, NULL); + SetRemoteDescriptionExpectError(kPushDownAnswerTDFailed, answer_with_gice); +} + +// Verifing local offer and remote answer have matching m-lines as per RFC 3264. +TEST_F(WebRtcSessionTest, TestIncorrectMLinesInRemoteAnswer) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + SetLocalDescriptionWithoutError(offer); + talk_base::scoped_ptr answer( + CreateRemoteAnswer(session_->local_description())); + + cricket::SessionDescription* answer_copy = answer->description()->Copy(); + answer_copy->RemoveContentByName("video"); + JsepSessionDescription* modified_answer = + new JsepSessionDescription(JsepSessionDescription::kAnswer); + + EXPECT_TRUE(modified_answer->Initialize(answer_copy, + answer->session_id(), + answer->session_version())); + SetRemoteDescriptionExpectError(kMlineMismatch, modified_answer); + + // Modifying content names. + std::string sdp; + EXPECT_TRUE(answer->ToString(&sdp)); + const std::string kAudioMid = "a=mid:audio"; + const std::string kAudioMidReplaceStr = "a=mid:audio_content_name"; + + // Replacing |audio| with |audio_content_name|. + talk_base::replace_substrs(kAudioMid.c_str(), kAudioMid.length(), + kAudioMidReplaceStr.c_str(), + kAudioMidReplaceStr.length(), + &sdp); + + SessionDescriptionInterface* modified_answer1 = + CreateSessionDescription(JsepSessionDescription::kAnswer, sdp, NULL); + SetRemoteDescriptionExpectError(kMlineMismatch, modified_answer1); + + SetRemoteDescriptionWithoutError(answer.release()); +} + +// Verifying remote offer and local answer have matching m-lines as per +// RFC 3264. +TEST_F(WebRtcSessionTest, TestIncorrectMLinesInLocalAnswer) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = CreateRemoteOffer(); + SetRemoteDescriptionWithoutError(offer); + SessionDescriptionInterface* answer = session_->CreateAnswer(NULL); + + cricket::SessionDescription* answer_copy = answer->description()->Copy(); + answer_copy->RemoveContentByName("video"); + JsepSessionDescription* modified_answer = + new JsepSessionDescription(JsepSessionDescription::kAnswer); + + EXPECT_TRUE(modified_answer->Initialize(answer_copy, + answer->session_id(), + answer->session_version())); + SetLocalDescriptionExpectError(kMlineMismatch, modified_answer); + SetLocalDescriptionWithoutError(answer); +} + +// This test verifies that WebRtcSession does not start candidate allocation +// before SetLocalDescription is called. +TEST_F(WebRtcSessionTest, TestIceStartAfterSetLocalDescriptionOnly) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = CreateRemoteOffer(); + cricket::Candidate candidate; + candidate.set_component(1); + JsepIceCandidate ice_candidate(kMediaContentName0, kMediaContentIndex0, + candidate); + EXPECT_TRUE(offer->AddCandidate(&ice_candidate)); + cricket::Candidate candidate1; + candidate1.set_component(1); + JsepIceCandidate ice_candidate1(kMediaContentName1, kMediaContentIndex1, + candidate1); + EXPECT_TRUE(offer->AddCandidate(&ice_candidate1)); + SetRemoteDescriptionWithoutError(offer); + ASSERT_TRUE(session_->GetTransportProxy("audio") != NULL); + ASSERT_TRUE(session_->GetTransportProxy("video") != NULL); + + // Pump for 1 second and verify that no candidates are generated. + talk_base::Thread::Current()->ProcessMessages(1000); + EXPECT_TRUE(observer_.mline_0_candidates_.empty()); + EXPECT_TRUE(observer_.mline_1_candidates_.empty()); + + SessionDescriptionInterface* answer = session_->CreateAnswer(NULL); + SetLocalDescriptionWithoutError(answer); + EXPECT_TRUE(session_->GetTransportProxy("audio")->negotiated()); + EXPECT_TRUE(session_->GetTransportProxy("video")->negotiated()); + EXPECT_TRUE_WAIT(observer_.oncandidatesready_, kIceCandidatesTimeout); +} + +// This test verifies that crypto parameter is updated in local session +// description as per security policy set in MediaSessionDescriptionFactory. +TEST_F(WebRtcSessionTest, TestCryptoAfterSetLocalDescription) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + talk_base::scoped_ptr offer( + session_->CreateOffer(NULL)); + + // Making sure SetLocalDescription correctly sets crypto value in + // SessionDescription object after de-serialization of sdp string. The value + // will be set as per MediaSessionDescriptionFactory. + std::string offer_str; + offer->ToString(&offer_str); + SessionDescriptionInterface* jsep_offer_str = + CreateSessionDescription(JsepSessionDescription::kOffer, offer_str, NULL); + SetLocalDescriptionWithoutError(jsep_offer_str); + EXPECT_TRUE(session_->voice_channel()->secure_required()); + EXPECT_TRUE(session_->video_channel()->secure_required()); +} + +// This test verifies the crypto parameter when security is disabled. +TEST_F(WebRtcSessionTest, TestCryptoAfterSetLocalDescriptionWithDisabled) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + session_->set_secure_policy(cricket::SEC_DISABLED); + talk_base::scoped_ptr offer( + session_->CreateOffer(NULL)); + + // Making sure SetLocalDescription correctly sets crypto value in + // SessionDescription object after de-serialization of sdp string. The value + // will be set as per MediaSessionDescriptionFactory. + std::string offer_str; + offer->ToString(&offer_str); + SessionDescriptionInterface *jsep_offer_str = + CreateSessionDescription(JsepSessionDescription::kOffer, offer_str, NULL); + SetLocalDescriptionWithoutError(jsep_offer_str); + EXPECT_FALSE(session_->voice_channel()->secure_required()); + EXPECT_FALSE(session_->video_channel()->secure_required()); +} + +// This test verifies that an answer contains new ufrag and password if an offer +// with new ufrag and password is received. +TEST_F(WebRtcSessionTest, TestCreateAnswerWithNewUfragAndPassword) { + Init(); + cricket::MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + talk_base::scoped_ptr offer( + CreateRemoteOffer(options)); + SetRemoteDescriptionWithoutError(offer.release()); + + mediastream_signaling_.SendAudioVideoStream1(); + talk_base::scoped_ptr answer( + session_->CreateAnswer(NULL)); + SetLocalDescriptionWithoutError(answer.release()); + + // Receive an offer with new ufrag and password. + options.transport_options.ice_restart = true; + talk_base::scoped_ptr updated_offer1( + CreateRemoteOffer(options, + session_->remote_description())); + SetRemoteDescriptionWithoutError(updated_offer1.release()); + + talk_base::scoped_ptr updated_answer1( + session_->CreateAnswer(NULL)); + + CompareIceUfragAndPassword(updated_answer1->description(), + session_->local_description()->description(), + false); + + SetLocalDescriptionWithoutError(updated_answer1.release()); + + // Receive yet an offer without changed ufrag or password. + options.transport_options.ice_restart = false; + talk_base::scoped_ptr updated_offer2( + CreateRemoteOffer(options, + session_->remote_description())); + SetRemoteDescriptionWithoutError(updated_offer2.release()); + + talk_base::scoped_ptr updated_answer2( + session_->CreateAnswer(NULL)); + + CompareIceUfragAndPassword(updated_answer2->description(), + session_->local_description()->description(), + true); + + SetLocalDescriptionWithoutError(updated_answer2.release()); +} + +TEST_F(WebRtcSessionTest, TestSessionContentError) { + Init(); + mediastream_signaling_.SendAudioVideoStream1(); + SessionDescriptionInterface* offer = session_->CreateOffer(NULL); + const std::string session_id_orig = offer->session_id(); + const std::string session_version_orig = offer->session_version(); + SetLocalDescriptionWithoutError(offer); + + video_channel_ = media_engine_->GetVideoChannel(0); + video_channel_->set_fail_set_send_codecs(true); + + mediastream_signaling_.SendAudioVideoStream2(); + SessionDescriptionInterface* answer = + CreateRemoteAnswer(session_->local_description()); + SetRemoteDescriptionExpectError("ERROR_CONTENT", answer); +} + +// Runs the loopback call test with BUNDLE and STUN disabled. +TEST_F(WebRtcSessionTest, TestIceStatesBasic) { + // Lets try with only UDP ports. + allocator_.set_flags(cricket::PORTALLOCATOR_ENABLE_SHARED_UFRAG | + cricket::PORTALLOCATOR_DISABLE_TCP | + cricket::PORTALLOCATOR_DISABLE_STUN | + cricket::PORTALLOCATOR_DISABLE_RELAY); + TestLoopbackCall(); +} + +// Regression-test for a crash which should have been an error. +TEST_F(WebRtcSessionTest, TestNoStateTransitionPendingError) { + Init(); + cricket::MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + + session_->SetError(cricket::BaseSession::ERROR_CONTENT); + SessionDescriptionInterface* offer = CreateRemoteOffer(options); + SessionDescriptionInterface* answer = + CreateRemoteAnswer(offer, options); + SetRemoteDescriptionExpectError(kSessionError, offer); + SetLocalDescriptionExpectError(kSessionError, answer); + // Not crashing is our success. +} + +TEST_F(WebRtcSessionTest, TestRtpDataChannel) { + constraints_.reset(new FakeConstraints()); + constraints_->AddOptional( + webrtc::MediaConstraintsInterface::kEnableRtpDataChannels, true); + Init(); + + SetLocalDescriptionWithDataChannel(); + EXPECT_EQ(cricket::DCT_RTP, data_engine_->last_channel_type()); +} + +TEST_F(WebRtcSessionTest, TestRtpDataChannelConstraintTakesPrecedence) { + MAYBE_SKIP_TEST(talk_base::SSLStreamAdapter::HaveDtlsSrtp); + + constraints_.reset(new FakeConstraints()); + constraints_->AddOptional( + webrtc::MediaConstraintsInterface::kEnableRtpDataChannels, true); + constraints_->AddOptional( + webrtc::MediaConstraintsInterface::kEnableSctpDataChannels, true); + constraints_->AddOptional( + webrtc::MediaConstraintsInterface::kEnableDtlsSrtp, true); + Init(); + + SetLocalDescriptionWithDataChannel(); + EXPECT_EQ(cricket::DCT_RTP, data_engine_->last_channel_type()); +} + +TEST_F(WebRtcSessionTest, TestSctpDataChannelWithoutDtls) { + constraints_.reset(new FakeConstraints()); + constraints_->AddOptional( + webrtc::MediaConstraintsInterface::kEnableSctpDataChannels, true); + Init(); + + SetLocalDescriptionWithDataChannel(); + EXPECT_EQ(cricket::DCT_NONE, data_engine_->last_channel_type()); +} + +TEST_F(WebRtcSessionTest, TestSctpDataChannelWithDtls) { + MAYBE_SKIP_TEST(talk_base::SSLStreamAdapter::HaveDtlsSrtp); + + constraints_.reset(new FakeConstraints()); + constraints_->AddOptional( + webrtc::MediaConstraintsInterface::kEnableSctpDataChannels, true); + constraints_->AddOptional( + webrtc::MediaConstraintsInterface::kEnableDtlsSrtp, true); + Init(); + + SetLocalDescriptionWithDataChannel(); + EXPECT_EQ(cricket::DCT_SCTP, data_engine_->last_channel_type()); +} +// TODO(bemasc): Add a TestIceStatesBundle with BUNDLE enabled. That test +// currently fails because upon disconnection and reconnection OnIceComplete is +// called more than once without returning to IceGatheringGathering. diff --git a/talk/base/asyncfile.cc b/talk/base/asyncfile.cc new file mode 100644 index 000000000..5c6e11dda --- /dev/null +++ b/talk/base/asyncfile.cc @@ -0,0 +1,38 @@ +/* + * libjingle + * Copyright 2010, 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/base/asyncfile.h" + +namespace talk_base { + +AsyncFile::AsyncFile() { +} + +AsyncFile::~AsyncFile() { +} + +} // namespace talk_base diff --git a/talk/base/asyncfile.h b/talk/base/asyncfile.h new file mode 100644 index 000000000..8af52be43 --- /dev/null +++ b/talk/base/asyncfile.h @@ -0,0 +1,57 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_BASE_ASYNCFILE_H__ +#define TALK_BASE_ASYNCFILE_H__ + +#include "talk/base/sigslot.h" + +namespace talk_base { + +// Provides the ability to perform file I/O asynchronously. +// TODO: Create a common base class with AsyncSocket. +class AsyncFile { + public: + AsyncFile(); + virtual ~AsyncFile(); + + // Determines whether the file will receive read events. + virtual bool readable() = 0; + virtual void set_readable(bool value) = 0; + + // Determines whether the file will receive write events. + virtual bool writable() = 0; + virtual void set_writable(bool value) = 0; + + sigslot::signal1 SignalReadEvent; + sigslot::signal1 SignalWriteEvent; + sigslot::signal2 SignalCloseEvent; +}; + +} // namespace talk_base + +#endif // TALK_BASE_ASYNCFILE_H__ diff --git a/talk/base/asynchttprequest.cc b/talk/base/asynchttprequest.cc new file mode 100644 index 000000000..68f61005b --- /dev/null +++ b/talk/base/asynchttprequest.cc @@ -0,0 +1,133 @@ +/* + * libjingle + * Copyright 2004--2010, 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/base/asynchttprequest.h" + +namespace talk_base { + +enum { + MSG_TIMEOUT = SignalThread::ST_MSG_FIRST_AVAILABLE, + MSG_LAUNCH_REQUEST +}; +static const int kDefaultHTTPTimeout = 30 * 1000; // 30 sec + +/////////////////////////////////////////////////////////////////////////////// +// AsyncHttpRequest +/////////////////////////////////////////////////////////////////////////////// + +AsyncHttpRequest::AsyncHttpRequest(const std::string &user_agent) + : start_delay_(0), + firewall_(NULL), + port_(80), + secure_(false), + timeout_(kDefaultHTTPTimeout), + fail_redirect_(false), + factory_(Thread::Current()->socketserver(), user_agent), + pool_(&factory_), + client_(user_agent.c_str(), &pool_), + error_(HE_NONE) { + client_.SignalHttpClientComplete.connect(this, + &AsyncHttpRequest::OnComplete); +} + +AsyncHttpRequest::~AsyncHttpRequest() { +} + +void AsyncHttpRequest::OnWorkStart() { + if (start_delay_ <= 0) { + LaunchRequest(); + } else { + Thread::Current()->PostDelayed(start_delay_, this, MSG_LAUNCH_REQUEST); + } +} + +void AsyncHttpRequest::OnWorkStop() { + // worker is already quitting, no need to explicitly quit + LOG(LS_INFO) << "HttpRequest cancelled"; +} + +void AsyncHttpRequest::OnComplete(HttpClient* client, HttpErrorType error) { + Thread::Current()->Clear(this, MSG_TIMEOUT); + + set_error(error); + if (!error) { + LOG(LS_INFO) << "HttpRequest completed successfully"; + + std::string value; + if (client_.response().hasHeader(HH_LOCATION, &value)) { + response_redirect_ = value.c_str(); + } + } else { + LOG(LS_INFO) << "HttpRequest completed with error: " << error; + } + + worker()->Quit(); +} + +void AsyncHttpRequest::OnMessage(Message* message) { + switch (message->message_id) { + case MSG_TIMEOUT: + LOG(LS_INFO) << "HttpRequest timed out"; + client_.reset(); + worker()->Quit(); + break; + case MSG_LAUNCH_REQUEST: + LaunchRequest(); + break; + default: + SignalThread::OnMessage(message); + break; + } +} + +void AsyncHttpRequest::DoWork() { + // Do nothing while we wait for the request to finish. We only do this so + // that we can be a SignalThread; in the future this class should not be + // a SignalThread, since it does not need to spawn a new thread. + Thread::Current()->ProcessMessages(kForever); +} + +void AsyncHttpRequest::LaunchRequest() { + factory_.SetProxy(proxy_); + if (secure_) + factory_.UseSSL(host_.c_str()); + + bool transparent_proxy = (port_ == 80) && + ((proxy_.type == PROXY_HTTPS) || (proxy_.type == PROXY_UNKNOWN)); + if (transparent_proxy) { + client_.set_proxy(proxy_); + } + client_.set_fail_redirect(fail_redirect_); + client_.set_server(SocketAddress(host_, port_)); + + LOG(LS_INFO) << "HttpRequest start: " << host_ + client_.request().path; + + Thread::Current()->PostDelayed(timeout_, this, MSG_TIMEOUT); + client_.start(); +} + +} // namespace talk_base diff --git a/talk/base/asynchttprequest.h b/talk/base/asynchttprequest.h new file mode 100644 index 000000000..13edf6188 --- /dev/null +++ b/talk/base/asynchttprequest.h @@ -0,0 +1,121 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_BASE_ASYNCHTTPREQUEST_H_ +#define TALK_BASE_ASYNCHTTPREQUEST_H_ + +#include +#include "talk/base/event.h" +#include "talk/base/httpclient.h" +#include "talk/base/signalthread.h" +#include "talk/base/socketpool.h" +#include "talk/base/sslsocketfactory.h" + +namespace talk_base { + +class FirewallManager; + +/////////////////////////////////////////////////////////////////////////////// +// AsyncHttpRequest +// Performs an HTTP request on a background thread. Notifies on the foreground +// thread once the request is done (successfully or unsuccessfully). +/////////////////////////////////////////////////////////////////////////////// + +class AsyncHttpRequest : public SignalThread { + public: + explicit AsyncHttpRequest(const std::string &user_agent); + ~AsyncHttpRequest(); + + // If start_delay is less than or equal to zero, this starts immediately. + // Start_delay defaults to zero. + int start_delay() const { return start_delay_; } + void set_start_delay(int delay) { start_delay_ = delay; } + + const ProxyInfo& proxy() const { return proxy_; } + void set_proxy(const ProxyInfo& proxy) { + proxy_ = proxy; + } + void set_firewall(FirewallManager * firewall) { + firewall_ = firewall; + } + + // The DNS name of the host to connect to. + const std::string& host() { return host_; } + void set_host(const std::string& host) { host_ = host; } + + // The port to connect to on the target host. + int port() { return port_; } + void set_port(int port) { port_ = port; } + + // Whether the request should use SSL. + bool secure() { return secure_; } + void set_secure(bool secure) { secure_ = secure; } + + // Time to wait on the download, in ms. + int timeout() { return timeout_; } + void set_timeout(int timeout) { timeout_ = timeout; } + + // Fail redirects to allow analysis of redirect urls, etc. + bool fail_redirect() const { return fail_redirect_; } + void set_fail_redirect(bool redirect) { fail_redirect_ = redirect; } + + // Returns the redirect when redirection occurs + const std::string& response_redirect() { return response_redirect_; } + + HttpRequestData& request() { return client_.request(); } + HttpResponseData& response() { return client_.response(); } + HttpErrorType error() { return error_; } + + protected: + void set_error(HttpErrorType error) { error_ = error; } + virtual void OnWorkStart(); + virtual void OnWorkStop(); + void OnComplete(HttpClient* client, HttpErrorType error); + virtual void OnMessage(Message* message); + virtual void DoWork(); + + private: + void LaunchRequest(); + + int start_delay_; + ProxyInfo proxy_; + FirewallManager* firewall_; + std::string host_; + int port_; + bool secure_; + int timeout_; + bool fail_redirect_; + SslSocketFactory factory_; + ReuseSocketPool pool_; + HttpClient client_; + HttpErrorType error_; + std::string response_redirect_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_ASYNCHTTPREQUEST_H_ diff --git a/talk/base/asynchttprequest_unittest.cc b/talk/base/asynchttprequest_unittest.cc new file mode 100644 index 000000000..13842da71 --- /dev/null +++ b/talk/base/asynchttprequest_unittest.cc @@ -0,0 +1,250 @@ +/* + * libjingle + * Copyright 2004--2011, 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 +#include "talk/base/asynchttprequest.h" +#include "talk/base/gunit.h" +#include "talk/base/httpserver.h" +#include "talk/base/socketstream.h" +#include "talk/base/thread.h" + +namespace talk_base { + +static const SocketAddress kServerAddr("127.0.0.1", 0); +static const SocketAddress kServerHostnameAddr("localhost", 0); +static const char kServerGetPath[] = "/get"; +static const char kServerPostPath[] = "/post"; +static const char kServerResponse[] = "This is a test"; + +class TestHttpServer : public HttpServer, public sigslot::has_slots<> { + public: + TestHttpServer(Thread* thread, const SocketAddress& addr) : + socket_(thread->socketserver()->CreateAsyncSocket(addr.family(), + SOCK_STREAM)) { + socket_->Bind(addr); + socket_->Listen(5); + socket_->SignalReadEvent.connect(this, &TestHttpServer::OnAccept); + } + + SocketAddress address() const { return socket_->GetLocalAddress(); } + void Close() const { socket_->Close(); } + + private: + void OnAccept(AsyncSocket* socket) { + AsyncSocket* new_socket = socket_->Accept(NULL); + if (new_socket) { + HandleConnection(new SocketStream(new_socket)); + } + } + talk_base::scoped_ptr socket_; +}; + +class AsyncHttpRequestTest : public testing::Test, + public sigslot::has_slots<> { + public: + AsyncHttpRequestTest() + : started_(false), + done_(false), + server_(Thread::Current(), kServerAddr) { + server_.SignalHttpRequest.connect(this, &AsyncHttpRequestTest::OnRequest); + } + + bool started() const { return started_; } + bool done() const { return done_; } + + AsyncHttpRequest* CreateGetRequest(const std::string& host, int port, + const std::string& path) { + talk_base::AsyncHttpRequest* request = + new talk_base::AsyncHttpRequest("unittest"); + request->SignalWorkDone.connect(this, + &AsyncHttpRequestTest::OnRequestDone); + request->request().verb = talk_base::HV_GET; + request->set_host(host); + request->set_port(port); + request->request().path = path; + request->response().document.reset(new MemoryStream()); + return request; + } + AsyncHttpRequest* CreatePostRequest(const std::string& host, int port, + const std::string& path, + const std::string content_type, + StreamInterface* content) { + talk_base::AsyncHttpRequest* request = + new talk_base::AsyncHttpRequest("unittest"); + request->SignalWorkDone.connect(this, + &AsyncHttpRequestTest::OnRequestDone); + request->request().verb = talk_base::HV_POST; + request->set_host(host); + request->set_port(port); + request->request().path = path; + request->request().setContent(content_type, content); + request->response().document.reset(new MemoryStream()); + return request; + } + + const TestHttpServer& server() const { return server_; } + + protected: + void OnRequest(HttpServer* server, HttpServerTransaction* t) { + started_ = true; + + if (t->request.path == kServerGetPath) { + t->response.set_success("text/plain", new MemoryStream(kServerResponse)); + } else if (t->request.path == kServerPostPath) { + // reverse the data and reply + size_t size; + StreamInterface* in = t->request.document.get(); + StreamInterface* out = new MemoryStream(); + in->GetSize(&size); + for (size_t i = 0; i < size; ++i) { + char ch; + in->SetPosition(size - i - 1); + in->Read(&ch, 1, NULL, NULL); + out->Write(&ch, 1, NULL, NULL); + } + out->Rewind(); + t->response.set_success("text/plain", out); + } else { + t->response.set_error(404); + } + server_.Respond(t); + } + void OnRequestDone(SignalThread* thread) { + done_ = true; + } + + private: + bool started_; + bool done_; + TestHttpServer server_; +}; + +TEST_F(AsyncHttpRequestTest, TestGetSuccess) { + AsyncHttpRequest* req = CreateGetRequest( + kServerHostnameAddr.hostname(), server().address().port(), + kServerGetPath); + EXPECT_FALSE(started()); + req->Start(); + EXPECT_TRUE_WAIT(started(), 5000); // Should have started by now. + EXPECT_TRUE_WAIT(done(), 5000); + std::string response; + EXPECT_EQ(200U, req->response().scode); + ASSERT_TRUE(req->response().document); + req->response().document->Rewind(); + req->response().document->ReadLine(&response); + EXPECT_EQ(kServerResponse, response); + req->Release(); +} + +TEST_F(AsyncHttpRequestTest, TestGetNotFound) { + AsyncHttpRequest* req = CreateGetRequest( + kServerHostnameAddr.hostname(), server().address().port(), + "/bad"); + req->Start(); + EXPECT_TRUE_WAIT(done(), 5000); + size_t size; + EXPECT_EQ(404U, req->response().scode); + ASSERT_TRUE(req->response().document); + req->response().document->GetSize(&size); + EXPECT_EQ(0U, size); + req->Release(); +} + +TEST_F(AsyncHttpRequestTest, TestGetToNonServer) { + AsyncHttpRequest* req = CreateGetRequest( + "127.0.0.1", server().address().port(), + kServerGetPath); + // Stop the server before we send the request. + server().Close(); + req->Start(); + EXPECT_TRUE_WAIT(done(), 10000); + size_t size; + EXPECT_EQ(500U, req->response().scode); + ASSERT_TRUE(req->response().document); + req->response().document->GetSize(&size); + EXPECT_EQ(0U, size); + req->Release(); +} + +TEST_F(AsyncHttpRequestTest, DISABLED_TestGetToInvalidHostname) { + AsyncHttpRequest* req = CreateGetRequest( + "invalid", server().address().port(), + kServerGetPath); + req->Start(); + EXPECT_TRUE_WAIT(done(), 5000); + size_t size; + EXPECT_EQ(500U, req->response().scode); + ASSERT_TRUE(req->response().document); + req->response().document->GetSize(&size); + EXPECT_EQ(0U, size); + req->Release(); +} + +TEST_F(AsyncHttpRequestTest, TestPostSuccess) { + AsyncHttpRequest* req = CreatePostRequest( + kServerHostnameAddr.hostname(), server().address().port(), + kServerPostPath, "text/plain", new MemoryStream("abcd1234")); + req->Start(); + EXPECT_TRUE_WAIT(done(), 5000); + std::string response; + EXPECT_EQ(200U, req->response().scode); + ASSERT_TRUE(req->response().document); + req->response().document->Rewind(); + req->response().document->ReadLine(&response); + EXPECT_EQ("4321dcba", response); + req->Release(); +} + +// Ensure that we shut down properly even if work is outstanding. +TEST_F(AsyncHttpRequestTest, TestCancel) { + AsyncHttpRequest* req = CreateGetRequest( + kServerHostnameAddr.hostname(), server().address().port(), + kServerGetPath); + req->Start(); + req->Destroy(true); +} + +TEST_F(AsyncHttpRequestTest, TestGetSuccessDelay) { + AsyncHttpRequest* req = CreateGetRequest( + kServerHostnameAddr.hostname(), server().address().port(), + kServerGetPath); + req->set_start_delay(10); // Delay 10ms. + req->Start(); + Thread::SleepMs(5); + EXPECT_FALSE(started()); // Should not have started immediately. + EXPECT_TRUE_WAIT(started(), 5000); // Should have started by now. + EXPECT_TRUE_WAIT(done(), 5000); + std::string response; + EXPECT_EQ(200U, req->response().scode); + ASSERT_TRUE(req->response().document); + req->response().document->Rewind(); + req->response().document->ReadLine(&response); + EXPECT_EQ(kServerResponse, response); + req->Release(); +} + +} // namespace talk_base diff --git a/talk/base/asyncpacketsocket.h b/talk/base/asyncpacketsocket.h new file mode 100644 index 000000000..a88f770ca --- /dev/null +++ b/talk/base/asyncpacketsocket.h @@ -0,0 +1,108 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_ASYNCPACKETSOCKET_H_ +#define TALK_BASE_ASYNCPACKETSOCKET_H_ + +#include "talk/base/sigslot.h" +#include "talk/base/socket.h" + +namespace talk_base { + +// Provides the ability to receive packets asynchronously. Sends are not +// buffered since it is acceptable to drop packets under high load. +class AsyncPacketSocket : public sigslot::has_slots<> { + public: + enum State { + STATE_CLOSED, + STATE_BINDING, + STATE_BOUND, + STATE_CONNECTING, + STATE_CONNECTED + }; + + AsyncPacketSocket() { } + virtual ~AsyncPacketSocket() { } + + // Returns current local address. Address may be set to NULL if the + // socket is not bound yet (GetState() returns STATE_BINDING). + virtual SocketAddress GetLocalAddress() const = 0; + + // Returns remote address. Returns zeroes if this is not a client TCP socket. + virtual SocketAddress GetRemoteAddress() const = 0; + + // Send a packet. + virtual int Send(const void *pv, size_t cb) = 0; + virtual int SendTo(const void *pv, size_t cb, const SocketAddress& addr) = 0; + + // Close the socket. + virtual int Close() = 0; + + // Returns current state of the socket. + virtual State GetState() const = 0; + + // Get/set options. + virtual int GetOption(Socket::Option opt, int* value) = 0; + virtual int SetOption(Socket::Option opt, int value) = 0; + + // Get/Set current error. + // TODO: Remove SetError(). + virtual int GetError() const = 0; + virtual void SetError(int error) = 0; + + // Emitted each time a packet is read. Used only for UDP and + // connected TCP sockets. + sigslot::signal4 SignalReadPacket; + + // Emitted when the socket is currently able to send. + sigslot::signal1 SignalReadyToSend; + + // Emitted after address for the socket is allocated, i.e. binding + // is finished. State of the socket is changed from BINDING to BOUND + // (for UDP and server TCP sockets) or CONNECTING (for client TCP + // sockets). + sigslot::signal2 SignalAddressReady; + + // Emitted for client TCP sockets when state is changed from + // CONNECTING to CONNECTED. + sigslot::signal1 SignalConnect; + + // Emitted for client TCP sockets when state is changed from + // CONNECTED to CLOSED. + sigslot::signal2 SignalClose; + + // Used only for listening TCP sockets. + sigslot::signal2 SignalNewConnection; + + private: + DISALLOW_EVIL_CONSTRUCTORS(AsyncPacketSocket); +}; + +} // namespace talk_base + +#endif // TALK_BASE_ASYNCPACKETSOCKET_H_ diff --git a/talk/base/asyncsocket.cc b/talk/base/asyncsocket.cc new file mode 100644 index 000000000..d9ed94c13 --- /dev/null +++ b/talk/base/asyncsocket.cc @@ -0,0 +1,61 @@ +/* + * libjingle + * Copyright 2010, 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/base/asyncsocket.h" + +namespace talk_base { + +AsyncSocket::AsyncSocket() { +} + +AsyncSocket::~AsyncSocket() { +} + +AsyncSocketAdapter::AsyncSocketAdapter(AsyncSocket* socket) : socket_(NULL) { + Attach(socket); +} + +AsyncSocketAdapter::~AsyncSocketAdapter() { + delete socket_; +} + +void AsyncSocketAdapter::Attach(AsyncSocket* socket) { + ASSERT(!socket_); + socket_ = socket; + if (socket_) { + socket_->SignalConnectEvent.connect(this, + &AsyncSocketAdapter::OnConnectEvent); + socket_->SignalReadEvent.connect(this, + &AsyncSocketAdapter::OnReadEvent); + socket_->SignalWriteEvent.connect(this, + &AsyncSocketAdapter::OnWriteEvent); + socket_->SignalCloseEvent.connect(this, + &AsyncSocketAdapter::OnCloseEvent); + } +} + +} // namespace talk_base diff --git a/talk/base/asyncsocket.h b/talk/base/asyncsocket.h new file mode 100644 index 000000000..3d12984b7 --- /dev/null +++ b/talk/base/asyncsocket.h @@ -0,0 +1,133 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_BASE_ASYNCSOCKET_H_ +#define TALK_BASE_ASYNCSOCKET_H_ + +#include "talk/base/common.h" +#include "talk/base/sigslot.h" +#include "talk/base/socket.h" + +namespace talk_base { + +// TODO: Remove Socket and rename AsyncSocket to Socket. + +// Provides the ability to perform socket I/O asynchronously. +class AsyncSocket : public Socket { + public: + AsyncSocket(); + virtual ~AsyncSocket(); + + virtual AsyncSocket* Accept(SocketAddress* paddr) = 0; + + sigslot::signal1 SignalReadEvent; // ready to read + sigslot::signal1 SignalWriteEvent; // ready to write + sigslot::signal1 SignalConnectEvent; // connected + sigslot::signal2 SignalCloseEvent; // closed +}; + +class AsyncSocketAdapter : public AsyncSocket, public sigslot::has_slots<> { + public: + // The adapted socket may explicitly be NULL, and later assigned using Attach. + // However, subclasses which support detached mode must override any methods + // that will be called during the detached period (usually GetState()), to + // avoid dereferencing a null pointer. + explicit AsyncSocketAdapter(AsyncSocket* socket); + virtual ~AsyncSocketAdapter(); + void Attach(AsyncSocket* socket); + virtual SocketAddress GetLocalAddress() const { + return socket_->GetLocalAddress(); + } + virtual SocketAddress GetRemoteAddress() const { + return socket_->GetRemoteAddress(); + } + virtual int Bind(const SocketAddress& addr) { + return socket_->Bind(addr); + } + virtual int Connect(const SocketAddress& addr) { + return socket_->Connect(addr); + } + virtual int Send(const void* pv, size_t cb) { + return socket_->Send(pv, cb); + } + virtual int SendTo(const void* pv, size_t cb, const SocketAddress& addr) { + return socket_->SendTo(pv, cb, addr); + } + virtual int Recv(void* pv, size_t cb) { + return socket_->Recv(pv, cb); + } + virtual int RecvFrom(void* pv, size_t cb, SocketAddress* paddr) { + return socket_->RecvFrom(pv, cb, paddr); + } + virtual int Listen(int backlog) { + return socket_->Listen(backlog); + } + virtual AsyncSocket* Accept(SocketAddress* paddr) { + return socket_->Accept(paddr); + } + virtual int Close() { + return socket_->Close(); + } + virtual int GetError() const { + return socket_->GetError(); + } + virtual void SetError(int error) { + return socket_->SetError(error); + } + virtual ConnState GetState() const { + return socket_->GetState(); + } + virtual int EstimateMTU(uint16* mtu) { + return socket_->EstimateMTU(mtu); + } + virtual int GetOption(Option opt, int* value) { + return socket_->GetOption(opt, value); + } + virtual int SetOption(Option opt, int value) { + return socket_->SetOption(opt, value); + } + + protected: + virtual void OnConnectEvent(AsyncSocket* socket) { + SignalConnectEvent(this); + } + virtual void OnReadEvent(AsyncSocket* socket) { + SignalReadEvent(this); + } + virtual void OnWriteEvent(AsyncSocket* socket) { + SignalWriteEvent(this); + } + virtual void OnCloseEvent(AsyncSocket* socket, int err) { + SignalCloseEvent(this, err); + } + + AsyncSocket* socket_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_ASYNCSOCKET_H_ diff --git a/talk/base/asynctcpsocket.cc b/talk/base/asynctcpsocket.cc new file mode 100644 index 000000000..095413d3e --- /dev/null +++ b/talk/base/asynctcpsocket.cc @@ -0,0 +1,313 @@ +/* + * libjingle + * Copyright 2004--2010, 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/base/asynctcpsocket.h" + +#include + +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" + +#ifdef POSIX +#include +#endif // POSIX + +namespace talk_base { + +static const size_t kMaxPacketSize = 64 * 1024; + +typedef uint16 PacketLength; +static const size_t kPacketLenSize = sizeof(PacketLength); + +static const size_t kBufSize = kMaxPacketSize + kPacketLenSize; + +static const int kListenBacklog = 5; + +// Binds and connects |socket| +AsyncSocket* AsyncTCPSocketBase::ConnectSocket( + talk_base::AsyncSocket* socket, + const talk_base::SocketAddress& bind_address, + const talk_base::SocketAddress& remote_address) { + talk_base::scoped_ptr owned_socket(socket); + if (socket->Bind(bind_address) < 0) { + LOG(LS_ERROR) << "Bind() failed with error " << socket->GetError(); + return NULL; + } + if (socket->Connect(remote_address) < 0) { + LOG(LS_ERROR) << "Connect() failed with error " << socket->GetError(); + return NULL; + } + return owned_socket.release(); +} + +AsyncTCPSocketBase::AsyncTCPSocketBase(AsyncSocket* socket, bool listen, + size_t max_packet_size) + : socket_(socket), + listen_(listen), + insize_(max_packet_size), + inpos_(0), + outsize_(max_packet_size), + outpos_(0) { + inbuf_ = new char[insize_]; + outbuf_ = new char[outsize_]; + + ASSERT(socket_.get() != NULL); + socket_->SignalConnectEvent.connect( + this, &AsyncTCPSocketBase::OnConnectEvent); + socket_->SignalReadEvent.connect(this, &AsyncTCPSocketBase::OnReadEvent); + socket_->SignalWriteEvent.connect(this, &AsyncTCPSocketBase::OnWriteEvent); + socket_->SignalCloseEvent.connect(this, &AsyncTCPSocketBase::OnCloseEvent); + + if (listen_) { + if (socket_->Listen(kListenBacklog) < 0) { + LOG(LS_ERROR) << "Listen() failed with error " << socket_->GetError(); + } + } +} + +AsyncTCPSocketBase::~AsyncTCPSocketBase() { + delete [] inbuf_; + delete [] outbuf_; +} + +SocketAddress AsyncTCPSocketBase::GetLocalAddress() const { + return socket_->GetLocalAddress(); +} + +SocketAddress AsyncTCPSocketBase::GetRemoteAddress() const { + return socket_->GetRemoteAddress(); +} + +int AsyncTCPSocketBase::Close() { + return socket_->Close(); +} + +AsyncTCPSocket::State AsyncTCPSocketBase::GetState() const { + switch (socket_->GetState()) { + case Socket::CS_CLOSED: + return STATE_CLOSED; + case Socket::CS_CONNECTING: + if (listen_) { + return STATE_BOUND; + } else { + return STATE_CONNECTING; + } + case Socket::CS_CONNECTED: + return STATE_CONNECTED; + default: + ASSERT(false); + return STATE_CLOSED; + } +} + +int AsyncTCPSocketBase::GetOption(Socket::Option opt, int* value) { + return socket_->GetOption(opt, value); +} + +int AsyncTCPSocketBase::SetOption(Socket::Option opt, int value) { + return socket_->SetOption(opt, value); +} + +int AsyncTCPSocketBase::GetError() const { + return socket_->GetError(); +} + +void AsyncTCPSocketBase::SetError(int error) { + return socket_->SetError(error); +} + +int AsyncTCPSocketBase::SendTo(const void *pv, size_t cb, + const SocketAddress& addr) { + if (addr == GetRemoteAddress()) + return Send(pv, cb); + + ASSERT(false); + socket_->SetError(ENOTCONN); + return -1; +} + +int AsyncTCPSocketBase::SendRaw(const void * pv, size_t cb) { + if (outpos_ + cb > outsize_) { + socket_->SetError(EMSGSIZE); + return -1; + } + + memcpy(outbuf_ + outpos_, pv, cb); + outpos_ += cb; + + return FlushOutBuffer(); +} + +int AsyncTCPSocketBase::FlushOutBuffer() { + int res = socket_->Send(outbuf_, outpos_); + if (res <= 0) { + return res; + } + if (static_cast(res) <= outpos_) { + outpos_ -= res; + } else { + ASSERT(false); + return -1; + } + if (outpos_ > 0) { + memmove(outbuf_, outbuf_ + res, outpos_); + } + return res; +} + +void AsyncTCPSocketBase::AppendToOutBuffer(const void* pv, size_t cb) { + ASSERT(outpos_ + cb < outsize_); + memcpy(outbuf_ + outpos_, pv, cb); + outpos_ += cb; +} + +void AsyncTCPSocketBase::OnConnectEvent(AsyncSocket* socket) { + SignalConnect(this); +} + +void AsyncTCPSocketBase::OnReadEvent(AsyncSocket* socket) { + ASSERT(socket_.get() == socket); + + if (listen_) { + talk_base::SocketAddress address; + talk_base::AsyncSocket* new_socket = socket->Accept(&address); + if (!new_socket) { + // TODO: Do something better like forwarding the error + // to the user. + LOG(LS_ERROR) << "TCP accept failed with error " << socket_->GetError(); + return; + } + + HandleIncomingConnection(new_socket); + + // Prime a read event in case data is waiting. + new_socket->SignalReadEvent(new_socket); + } else { + int len = socket_->Recv(inbuf_ + inpos_, insize_ - inpos_); + if (len < 0) { + // TODO: Do something better like forwarding the error to the user. + if (!socket_->IsBlocking()) { + LOG(LS_ERROR) << "Recv() returned error: " << socket_->GetError(); + } + return; + } + + inpos_ += len; + + ProcessInput(inbuf_, &inpos_); + + if (inpos_ >= insize_) { + LOG(LS_ERROR) << "input buffer overflow"; + ASSERT(false); + inpos_ = 0; + } + } +} + +void AsyncTCPSocketBase::OnWriteEvent(AsyncSocket* socket) { + ASSERT(socket_.get() == socket); + + if (outpos_ > 0) { + FlushOutBuffer(); + } + + if (outpos_ == 0) { + SignalReadyToSend(this); + } +} + +void AsyncTCPSocketBase::OnCloseEvent(AsyncSocket* socket, int error) { + SignalClose(this, error); +} + +// AsyncTCPSocket +// Binds and connects |socket| and creates AsyncTCPSocket for +// it. Takes ownership of |socket|. Returns NULL if bind() or +// connect() fail (|socket| is destroyed in that case). +AsyncTCPSocket* AsyncTCPSocket::Create( + AsyncSocket* socket, + const SocketAddress& bind_address, + const SocketAddress& remote_address) { + return new AsyncTCPSocket(AsyncTCPSocketBase::ConnectSocket( + socket, bind_address, remote_address), false); +} + +AsyncTCPSocket::AsyncTCPSocket(AsyncSocket* socket, bool listen) + : AsyncTCPSocketBase(socket, listen, kBufSize) { +} + +int AsyncTCPSocket::Send(const void *pv, size_t cb) { + if (cb > kBufSize) { + SetError(EMSGSIZE); + return -1; + } + + // If we are blocking on send, then silently drop this packet + if (!IsOutBufferEmpty()) + return static_cast(cb); + + PacketLength pkt_len = HostToNetwork16(static_cast(cb)); + AppendToOutBuffer(&pkt_len, kPacketLenSize); + AppendToOutBuffer(pv, cb); + + int res = FlushOutBuffer(); + if (res <= 0) { + // drop packet if we made no progress + ClearOutBuffer(); + return res; + } + + // We claim to have sent the whole thing, even if we only sent partial + return static_cast(cb); +} + +void AsyncTCPSocket::ProcessInput(char * data, size_t* len) { + SocketAddress remote_addr(GetRemoteAddress()); + + while (true) { + if (*len < kPacketLenSize) + return; + + PacketLength pkt_len = talk_base::GetBE16(data); + if (*len < kPacketLenSize + pkt_len) + return; + + SignalReadPacket(this, data + kPacketLenSize, pkt_len, remote_addr); + + *len -= kPacketLenSize + pkt_len; + if (*len > 0) { + memmove(data, data + kPacketLenSize + pkt_len, *len); + } + } +} + +void AsyncTCPSocket::HandleIncomingConnection(AsyncSocket* socket) { + SignalNewConnection(this, new AsyncTCPSocket(socket, false)); +} + +} // namespace talk_base diff --git a/talk/base/asynctcpsocket.h b/talk/base/asynctcpsocket.h new file mode 100644 index 000000000..b34ce188e --- /dev/null +++ b/talk/base/asynctcpsocket.h @@ -0,0 +1,114 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_BASE_ASYNCTCPSOCKET_H_ +#define TALK_BASE_ASYNCTCPSOCKET_H_ + +#include "talk/base/asyncpacketsocket.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/socketfactory.h" + +namespace talk_base { + +// Simulates UDP semantics over TCP. Send and Recv packet sizes +// are preserved, and drops packets silently on Send, rather than +// buffer them in user space. +class AsyncTCPSocketBase : public AsyncPacketSocket { + public: + AsyncTCPSocketBase(AsyncSocket* socket, bool listen, size_t max_packet_size); + virtual ~AsyncTCPSocketBase(); + + // Pure virtual methods to send and recv data. + virtual int Send(const void *pv, size_t cb) = 0; + virtual void ProcessInput(char* data, size_t* len) = 0; + // Signals incoming connection. + virtual void HandleIncomingConnection(AsyncSocket* socket) = 0; + + virtual SocketAddress GetLocalAddress() const; + virtual SocketAddress GetRemoteAddress() const; + virtual int SendTo(const void *pv, size_t cb, const SocketAddress& addr); + virtual int Close(); + + virtual State GetState() const; + virtual int GetOption(Socket::Option opt, int* value); + virtual int SetOption(Socket::Option opt, int value); + virtual int GetError() const; + virtual void SetError(int error); + + protected: + // Binds and connects |socket| and creates AsyncTCPSocket for + // it. Takes ownership of |socket|. Returns NULL if bind() or + // connect() fail (|socket| is destroyed in that case). + static AsyncSocket* ConnectSocket(AsyncSocket* socket, + const SocketAddress& bind_address, + const SocketAddress& remote_address); + virtual int SendRaw(const void* pv, size_t cb); + int FlushOutBuffer(); + // Add data to |outbuf_|. + void AppendToOutBuffer(const void* pv, size_t cb); + + // Helper methods for |outpos_|. + bool IsOutBufferEmpty() const { return outpos_ == 0; } + void ClearOutBuffer() { outpos_ = 0; } + + private: + // Called by the underlying socket + void OnConnectEvent(AsyncSocket* socket); + void OnReadEvent(AsyncSocket* socket); + void OnWriteEvent(AsyncSocket* socket); + void OnCloseEvent(AsyncSocket* socket, int error); + + scoped_ptr socket_; + bool listen_; + char* inbuf_, * outbuf_; + size_t insize_, inpos_, outsize_, outpos_; + + DISALLOW_EVIL_CONSTRUCTORS(AsyncTCPSocketBase); +}; + +class AsyncTCPSocket : public AsyncTCPSocketBase { + public: + // Binds and connects |socket| and creates AsyncTCPSocket for + // it. Takes ownership of |socket|. Returns NULL if bind() or + // connect() fail (|socket| is destroyed in that case). + static AsyncTCPSocket* Create(AsyncSocket* socket, + const SocketAddress& bind_address, + const SocketAddress& remote_address); + AsyncTCPSocket(AsyncSocket* socket, bool listen); + virtual ~AsyncTCPSocket() {} + + virtual int Send(const void* pv, size_t cb); + virtual void ProcessInput(char* data, size_t* len); + virtual void HandleIncomingConnection(AsyncSocket* socket); + + private: + DISALLOW_EVIL_CONSTRUCTORS(AsyncTCPSocket); +}; + +} // namespace talk_base + +#endif // TALK_BASE_ASYNCTCPSOCKET_H_ diff --git a/talk/base/asynctcpsocket_unittest.cc b/talk/base/asynctcpsocket_unittest.cc new file mode 100644 index 000000000..4f87dbdac --- /dev/null +++ b/talk/base/asynctcpsocket_unittest.cc @@ -0,0 +1,70 @@ +/* + * libjingle + * Copyright 2004--2013, 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 + +#include "talk/base/asynctcpsocket.h" +#include "talk/base/gunit.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/virtualsocketserver.h" + +namespace talk_base { + +class AsyncTCPSocketTest + : public testing::Test, + public sigslot::has_slots<> { + public: + AsyncTCPSocketTest() + : pss_(new talk_base::PhysicalSocketServer), + vss_(new talk_base::VirtualSocketServer(pss_.get())), + socket_(vss_->CreateAsyncSocket(SOCK_STREAM)), + tcp_socket_(new AsyncTCPSocket(socket_, true)), + ready_to_send_(false) { + tcp_socket_->SignalReadyToSend.connect(this, + &AsyncTCPSocketTest::OnReadyToSend); + } + + void OnReadyToSend(talk_base::AsyncPacketSocket* socket) { + ready_to_send_ = true; + } + + protected: + scoped_ptr pss_; + scoped_ptr vss_; + AsyncSocket* socket_; + scoped_ptr tcp_socket_; + bool ready_to_send_; +}; + +TEST_F(AsyncTCPSocketTest, OnWriteEvent) { + EXPECT_FALSE(ready_to_send_); + socket_->SignalWriteEvent(socket_); + EXPECT_TRUE(ready_to_send_); +} + +} // namespace talk_base diff --git a/talk/base/asyncudpsocket.cc b/talk/base/asyncudpsocket.cc new file mode 100644 index 000000000..6388ce7ce --- /dev/null +++ b/talk/base/asyncudpsocket.cc @@ -0,0 +1,136 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/asyncudpsocket.h" +#include "talk/base/logging.h" + +namespace talk_base { + +static const int BUF_SIZE = 64 * 1024; + +AsyncUDPSocket* AsyncUDPSocket::Create( + AsyncSocket* socket, + const SocketAddress& bind_address) { + scoped_ptr owned_socket(socket); + if (socket->Bind(bind_address) < 0) { + LOG(LS_ERROR) << "Bind() failed with error " << socket->GetError(); + return NULL; + } + return new AsyncUDPSocket(owned_socket.release()); +} + +AsyncUDPSocket* AsyncUDPSocket::Create(SocketFactory* factory, + const SocketAddress& bind_address) { + AsyncSocket* socket = + factory->CreateAsyncSocket(bind_address.family(), SOCK_DGRAM); + if (!socket) + return NULL; + return Create(socket, bind_address); +} + +AsyncUDPSocket::AsyncUDPSocket(AsyncSocket* socket) + : socket_(socket) { + ASSERT(socket_); + size_ = BUF_SIZE; + buf_ = new char[size_]; + + // The socket should start out readable but not writable. + socket_->SignalReadEvent.connect(this, &AsyncUDPSocket::OnReadEvent); + socket_->SignalWriteEvent.connect(this, &AsyncUDPSocket::OnWriteEvent); +} + +AsyncUDPSocket::~AsyncUDPSocket() { + delete [] buf_; +} + +SocketAddress AsyncUDPSocket::GetLocalAddress() const { + return socket_->GetLocalAddress(); +} + +SocketAddress AsyncUDPSocket::GetRemoteAddress() const { + return socket_->GetRemoteAddress(); +} + +int AsyncUDPSocket::Send(const void *pv, size_t cb) { + return socket_->Send(pv, cb); +} + +int AsyncUDPSocket::SendTo( + const void *pv, size_t cb, const SocketAddress& addr) { + return socket_->SendTo(pv, cb, addr); +} + +int AsyncUDPSocket::Close() { + return socket_->Close(); +} + +AsyncUDPSocket::State AsyncUDPSocket::GetState() const { + return STATE_BOUND; +} + +int AsyncUDPSocket::GetOption(Socket::Option opt, int* value) { + return socket_->GetOption(opt, value); +} + +int AsyncUDPSocket::SetOption(Socket::Option opt, int value) { + return socket_->SetOption(opt, value); +} + +int AsyncUDPSocket::GetError() const { + return socket_->GetError(); +} + +void AsyncUDPSocket::SetError(int error) { + return socket_->SetError(error); +} + +void AsyncUDPSocket::OnReadEvent(AsyncSocket* socket) { + ASSERT(socket_.get() == socket); + + SocketAddress remote_addr; + int len = socket_->RecvFrom(buf_, size_, &remote_addr); + if (len < 0) { + // An error here typically means we got an ICMP error in response to our + // send datagram, indicating the remote address was unreachable. + // When doing ICE, this kind of thing will often happen. + // TODO: Do something better like forwarding the error to the user. + SocketAddress local_addr = socket_->GetLocalAddress(); + LOG(LS_INFO) << "AsyncUDPSocket[" << local_addr.ToSensitiveString() << "] " + << "receive failed with error " << socket_->GetError(); + return; + } + + // TODO: Make sure that we got all of the packet. + // If we did not, then we should resize our buffer to be large enough. + SignalReadPacket(this, buf_, (size_t)len, remote_addr); +} + +void AsyncUDPSocket::OnWriteEvent(AsyncSocket* socket) { + SignalReadyToSend(this); +} + +} // namespace talk_base diff --git a/talk/base/asyncudpsocket.h b/talk/base/asyncudpsocket.h new file mode 100644 index 000000000..1bf2ad229 --- /dev/null +++ b/talk/base/asyncudpsocket.h @@ -0,0 +1,78 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_ASYNCUDPSOCKET_H_ +#define TALK_BASE_ASYNCUDPSOCKET_H_ + +#include "talk/base/asyncpacketsocket.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/socketfactory.h" + +namespace talk_base { + +// Provides the ability to receive packets asynchronously. Sends are not +// buffered since it is acceptable to drop packets under high load. +class AsyncUDPSocket : public AsyncPacketSocket { + public: + // Binds |socket| and creates AsyncUDPSocket for it. Takes ownership + // of |socket|. Returns NULL if bind() fails (|socket| is destroyed + // in that case). + static AsyncUDPSocket* Create(AsyncSocket* socket, + const SocketAddress& bind_address); + // Creates a new socket for sending asynchronous UDP packets using an + // asynchronous socket from the given factory. + static AsyncUDPSocket* Create(SocketFactory* factory, + const SocketAddress& bind_address); + explicit AsyncUDPSocket(AsyncSocket* socket); + virtual ~AsyncUDPSocket(); + + virtual SocketAddress GetLocalAddress() const; + virtual SocketAddress GetRemoteAddress() const; + virtual int Send(const void *pv, size_t cb); + virtual int SendTo(const void *pv, size_t cb, const SocketAddress& addr); + virtual int Close(); + + virtual State GetState() const; + virtual int GetOption(Socket::Option opt, int* value); + virtual int SetOption(Socket::Option opt, int value); + virtual int GetError() const; + virtual void SetError(int error); + + private: + // Called when the underlying socket is ready to be read from. + void OnReadEvent(AsyncSocket* socket); + // Called when the underlying socket is ready to send. + void OnWriteEvent(AsyncSocket* socket); + + scoped_ptr socket_; + char* buf_; + size_t size_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_ASYNCUDPSOCKET_H_ diff --git a/talk/base/asyncudpsocket_unittest.cc b/talk/base/asyncudpsocket_unittest.cc new file mode 100644 index 000000000..562577a22 --- /dev/null +++ b/talk/base/asyncudpsocket_unittest.cc @@ -0,0 +1,70 @@ +/* + * libjingle + * Copyright 2004--2013, 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 + +#include "talk/base/asyncudpsocket.h" +#include "talk/base/gunit.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/virtualsocketserver.h" + +namespace talk_base { + +class AsyncUdpSocketTest + : public testing::Test, + public sigslot::has_slots<> { + public: + AsyncUdpSocketTest() + : pss_(new talk_base::PhysicalSocketServer), + vss_(new talk_base::VirtualSocketServer(pss_.get())), + socket_(vss_->CreateAsyncSocket(SOCK_DGRAM)), + udp_socket_(new AsyncUDPSocket(socket_)), + ready_to_send_(false) { + udp_socket_->SignalReadyToSend.connect(this, + &AsyncUdpSocketTest::OnReadyToSend); + } + + void OnReadyToSend(talk_base::AsyncPacketSocket* socket) { + ready_to_send_ = true; + } + + protected: + scoped_ptr pss_; + scoped_ptr vss_; + AsyncSocket* socket_; + scoped_ptr udp_socket_; + bool ready_to_send_; +}; + +TEST_F(AsyncUdpSocketTest, OnWriteEvent) { + EXPECT_FALSE(ready_to_send_); + socket_->SignalWriteEvent(socket_); + EXPECT_TRUE(ready_to_send_); +} + +} // namespace talk_base diff --git a/talk/base/atomicops.h b/talk/base/atomicops.h new file mode 100644 index 000000000..94ade6973 --- /dev/null +++ b/talk/base/atomicops.h @@ -0,0 +1,166 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_BASE_ATOMICOPS_H_ +#define TALK_BASE_ATOMICOPS_H_ + +#include + +#include "talk/base/basictypes.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" + +namespace talk_base { + +// A single-producer, single-consumer, fixed-size queue. +// All methods not ending in Unsafe can be safely called without locking, +// provided that calls to consumer methods (Peek/Pop) or producer methods (Push) +// only happen on a single thread per method type. If multiple threads need to +// read simultaneously or write simultaneously, other synchronization is +// necessary. Synchronization is also required if a call into any Unsafe method +// could happen at the same time as a call to any other method. +template +class FixedSizeLockFreeQueue { + private: +// Atomic primitives and memory barrier +#if defined(__arm__) + typedef uint32 Atomic32; + + // Copied from google3/base/atomicops-internals-arm-v6plus.h + static inline void MemoryBarrier() { + asm volatile("dmb":::"memory"); + } + + // Adapted from google3/base/atomicops-internals-arm-v6plus.h + static inline void AtomicIncrement(volatile Atomic32* ptr) { + Atomic32 str_success, value; + asm volatile ( + "1:\n" + "ldrex %1, [%2]\n" + "add %1, %1, #1\n" + "strex %0, %1, [%2]\n" + "teq %0, #0\n" + "bne 1b" + : "=&r"(str_success), "=&r"(value) + : "r" (ptr) + : "cc", "memory"); + } +#elif !defined(SKIP_ATOMIC_CHECK) +#error "No atomic operations defined for the given architecture." +#endif + + public: + // Constructs an empty queue, with capacity 0. + FixedSizeLockFreeQueue() : pushed_count_(0), + popped_count_(0), + capacity_(0), + data_(NULL) {} + // Constructs an empty queue with the given capacity. + FixedSizeLockFreeQueue(size_t capacity) : pushed_count_(0), + popped_count_(0), + capacity_(capacity), + data_(new T[capacity]) {} + + // Pushes a value onto the queue. Returns true if the value was successfully + // pushed (there was space in the queue). This method can be safely called at + // the same time as PeekFront/PopFront. + bool PushBack(T value) { + if (capacity_ == 0) { + LOG(LS_WARNING) << "Queue capacity is 0."; + return false; + } + if (IsFull()) { + return false; + } + + data_[pushed_count_ % capacity_] = value; + // Make sure the data is written before the count is incremented, so other + // threads can't see the value exists before being able to read it. + MemoryBarrier(); + AtomicIncrement(&pushed_count_); + return true; + } + + // Retrieves the oldest value pushed onto the queue. Returns true if there was + // an item to peek (the queue was non-empty). This method can be safely called + // at the same time as PushBack. + bool PeekFront(T* value_out) { + if (capacity_ == 0) { + LOG(LS_WARNING) << "Queue capacity is 0."; + return false; + } + if (IsEmpty()) { + return false; + } + + *value_out = data_[popped_count_ % capacity_]; + return true; + } + + // Retrieves the oldest value pushed onto the queue and removes it from the + // queue. Returns true if there was an item to pop (the queue was non-empty). + // This method can be safely called at the same time as PushBack. + bool PopFront(T* value_out) { + if (PeekFront(value_out)) { + AtomicIncrement(&popped_count_); + return true; + } + return false; + } + + // Clears the current items in the queue and sets the new (fixed) size. This + // method cannot be called at the same time as any other method. + void ClearAndResizeUnsafe(int new_capacity) { + capacity_ = new_capacity; + data_.reset(new T[new_capacity]); + pushed_count_ = 0; + popped_count_ = 0; + } + + // Returns true if there is no space left in the queue for new elements. + int IsFull() const { return pushed_count_ == popped_count_ + capacity_; } + // Returns true if there are no elements in the queue. + int IsEmpty() const { return pushed_count_ == popped_count_; } + // Returns the current number of elements in the queue. This is always in the + // range [0, capacity] + size_t Size() const { return pushed_count_ - popped_count_; } + + // Returns the capacity of the queue (max size). + size_t capacity() const { return capacity_; } + + private: + volatile Atomic32 pushed_count_; + volatile Atomic32 popped_count_; + size_t capacity_; + talk_base::scoped_array data_; + DISALLOW_COPY_AND_ASSIGN(FixedSizeLockFreeQueue); +}; + +} + +#endif // TALK_BASE_ATOMICOPS_H_ diff --git a/talk/base/atomicops_unittest.cc b/talk/base/atomicops_unittest.cc new file mode 100644 index 000000000..24804c678 --- /dev/null +++ b/talk/base/atomicops_unittest.cc @@ -0,0 +1,96 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#if !defined(__arm__) +// For testing purposes, define faked versions of the atomic operations +#include "talk/base/basictypes.h" +namespace talk_base { +typedef uint32 Atomic32; +static inline void MemoryBarrier() { } +static inline void AtomicIncrement(volatile Atomic32* ptr) { + *ptr = *ptr + 1; +} +} +#define SKIP_ATOMIC_CHECK +#endif + +#include "talk/base/atomicops.h" +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" + +TEST(FixedSizeLockFreeQueueTest, TestDefaultConstruct) { + talk_base::FixedSizeLockFreeQueue queue; + EXPECT_EQ(0u, queue.capacity()); + EXPECT_EQ(0u, queue.Size()); + EXPECT_FALSE(queue.PushBack(1)); + int val; + EXPECT_FALSE(queue.PopFront(&val)); +} + +TEST(FixedSizeLockFreeQueueTest, TestConstruct) { + talk_base::FixedSizeLockFreeQueue queue(5); + EXPECT_EQ(5u, queue.capacity()); + EXPECT_EQ(0u, queue.Size()); + int val; + EXPECT_FALSE(queue.PopFront(&val)); +} + +TEST(FixedSizeLockFreeQueueTest, TestPushPop) { + talk_base::FixedSizeLockFreeQueue queue(2); + EXPECT_EQ(2u, queue.capacity()); + EXPECT_EQ(0u, queue.Size()); + EXPECT_TRUE(queue.PushBack(1)); + EXPECT_EQ(1u, queue.Size()); + EXPECT_TRUE(queue.PushBack(2)); + EXPECT_EQ(2u, queue.Size()); + EXPECT_FALSE(queue.PushBack(3)); + EXPECT_EQ(2u, queue.Size()); + int val; + EXPECT_TRUE(queue.PopFront(&val)); + EXPECT_EQ(1, val); + EXPECT_EQ(1u, queue.Size()); + EXPECT_TRUE(queue.PopFront(&val)); + EXPECT_EQ(2, val); + EXPECT_EQ(0u, queue.Size()); + EXPECT_FALSE(queue.PopFront(&val)); + EXPECT_EQ(0u, queue.Size()); +} + +TEST(FixedSizeLockFreeQueueTest, TestResize) { + talk_base::FixedSizeLockFreeQueue queue(2); + EXPECT_EQ(2u, queue.capacity()); + EXPECT_EQ(0u, queue.Size()); + EXPECT_TRUE(queue.PushBack(1)); + EXPECT_EQ(1u, queue.Size()); + + queue.ClearAndResizeUnsafe(5); + EXPECT_EQ(5u, queue.capacity()); + EXPECT_EQ(0u, queue.Size()); + int val; + EXPECT_FALSE(queue.PopFront(&val)); +} diff --git a/talk/base/autodetectproxy.cc b/talk/base/autodetectproxy.cc new file mode 100644 index 000000000..02cbaade5 --- /dev/null +++ b/talk/base/autodetectproxy.cc @@ -0,0 +1,290 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/autodetectproxy.h" +#include "talk/base/httpcommon.h" +#include "talk/base/httpcommon-inl.h" +#include "talk/base/nethelpers.h" + +namespace talk_base { + +static const ProxyType TEST_ORDER[] = { + PROXY_HTTPS, PROXY_SOCKS5, PROXY_UNKNOWN +}; + +static const int kSavedStringLimit = 128; + +static void SaveStringToStack(char *dst, + const std::string &src, + size_t dst_size) { + strncpy(dst, src.c_str(), dst_size - 1); + dst[dst_size - 1] = '\0'; +} + +AutoDetectProxy::AutoDetectProxy(const std::string& user_agent) + : agent_(user_agent), resolver_(NULL), socket_(NULL), next_(0) { +} + +AutoDetectProxy::~AutoDetectProxy() { + if (resolver_) { + resolver_->Destroy(false); + } +} + +void AutoDetectProxy::DoWork() { + // TODO: Try connecting to server_url without proxy first here? + if (!server_url_.empty()) { + LOG(LS_INFO) << "GetProxySettingsForUrl(" << server_url_ << ") - start"; + GetProxyForUrl(agent_.c_str(), server_url_.c_str(), &proxy_); + LOG(LS_INFO) << "GetProxySettingsForUrl - stop"; + } + Url url(proxy_.address.HostAsURIString()); + if (url.valid()) { + LOG(LS_WARNING) << "AutoDetectProxy removing http prefix on proxy host"; + proxy_.address.SetIP(url.host()); + } + LOG(LS_INFO) << "AutoDetectProxy found proxy at " << proxy_.address; + if (proxy_.type == PROXY_UNKNOWN) { + LOG(LS_INFO) << "AutoDetectProxy initiating proxy classification"; + Next(); + // Process I/O until Stop() + Thread::Current()->ProcessMessages(kForever); + // Clean up the autodetect socket, from the thread that created it + delete socket_; + } + // TODO: If we found a proxy, try to use it to verify that it + // works by sending a request to server_url. This could either be + // done here or by the HttpPortAllocator. +} + +void AutoDetectProxy::OnMessage(Message *msg) { + if (MSG_TIMEOUT == msg->message_id) { + OnCloseEvent(socket_, ETIMEDOUT); + } else { + // This must be the ST_MSG_WORKER_DONE message that deletes the + // AutoDetectProxy object. We have observed crashes within this stack that + // seem to be highly reproducible for a small subset of users and thus are + // probably correlated with a specific proxy setting, so copy potentially + // relevant information onto the stack to make it available in Windows + // minidumps. + + // Save the user agent and the number of auto-detection passes that we + // needed. + char agent[kSavedStringLimit]; + SaveStringToStack(agent, agent_, sizeof agent); + + int next = next_; + + // Now the detected proxy config (minus the password field, which could be + // sensitive). + ProxyType type = proxy().type; + + char address_hostname[kSavedStringLimit]; + SaveStringToStack(address_hostname, + proxy().address.hostname(), + sizeof address_hostname); + + IPAddress address_ip = proxy().address.ipaddr(); + + uint16 address_port = proxy().address.port(); + + char autoconfig_url[kSavedStringLimit]; + SaveStringToStack(autoconfig_url, + proxy().autoconfig_url, + sizeof autoconfig_url); + + bool autodetect = proxy().autodetect; + + char bypass_list[kSavedStringLimit]; + SaveStringToStack(bypass_list, proxy().bypass_list, sizeof bypass_list); + + char username[kSavedStringLimit]; + SaveStringToStack(username, proxy().username, sizeof username); + + SignalThread::OnMessage(msg); + + // Log the gathered data at a log level that will never actually be enabled + // so that the compiler is forced to retain the data on the stack. + LOG(LS_SENSITIVE) << agent << " " << next << " " << type << " " + << address_hostname << " " << address_ip << " " + << address_port << " " << autoconfig_url << " " + << autodetect << " " << bypass_list << " " << username; + } +} + +void AutoDetectProxy::OnResolveResult(SignalThread* thread) { + if (thread != resolver_) { + return; + } + int error = resolver_->error(); + if (error == 0) { + LOG(LS_VERBOSE) << "Resolved " << proxy_.address << " to " + << resolver_->address(); + proxy_.address = resolver_->address(); + DoConnect(); + } else { + LOG(LS_INFO) << "Failed to resolve " << resolver_->address(); + resolver_->Destroy(false); + resolver_ = NULL; + proxy_.address = SocketAddress(); + Thread::Current()->Post(this, MSG_TIMEOUT); + } +} + +void AutoDetectProxy::Next() { + if (TEST_ORDER[next_] >= PROXY_UNKNOWN) { + Complete(PROXY_UNKNOWN); + return; + } + + LOG(LS_VERBOSE) << "AutoDetectProxy connecting to " + << proxy_.address.ToSensitiveString(); + + if (socket_) { + Thread::Current()->Clear(this, MSG_TIMEOUT); + socket_->Close(); + Thread::Current()->Dispose(socket_); + socket_ = NULL; + } + int timeout = 2000; + if (proxy_.address.IsUnresolvedIP()) { + // Launch an asyncresolver. This thread will spin waiting for it. + timeout += 2000; + if (!resolver_) { + resolver_ = new AsyncResolver(); + } + resolver_->set_address(proxy_.address); + resolver_->SignalWorkDone.connect(this, + &AutoDetectProxy::OnResolveResult); + resolver_->Start(); + } else { + DoConnect(); + } + Thread::Current()->PostDelayed(timeout, this, MSG_TIMEOUT); +} + +void AutoDetectProxy::DoConnect() { + if (resolver_) { + resolver_->Destroy(false); + resolver_ = NULL; + } + socket_ = + Thread::Current()->socketserver()->CreateAsyncSocket( + proxy_.address.family(), SOCK_STREAM); + if (!socket_) { + LOG(LS_VERBOSE) << "Unable to create socket for " << proxy_.address; + return; + } + socket_->SignalConnectEvent.connect(this, &AutoDetectProxy::OnConnectEvent); + socket_->SignalReadEvent.connect(this, &AutoDetectProxy::OnReadEvent); + socket_->SignalCloseEvent.connect(this, &AutoDetectProxy::OnCloseEvent); + socket_->Connect(proxy_.address); +} + +void AutoDetectProxy::Complete(ProxyType type) { + Thread::Current()->Clear(this, MSG_TIMEOUT); + if (socket_) { + socket_->Close(); + } + + proxy_.type = type; + LoggingSeverity sev = (proxy_.type == PROXY_UNKNOWN) ? LS_ERROR : LS_INFO; + LOG_V(sev) << "AutoDetectProxy detected " + << proxy_.address.ToSensitiveString() + << " as type " << proxy_.type; + + Thread::Current()->Quit(); +} + +void AutoDetectProxy::OnConnectEvent(AsyncSocket * socket) { + std::string probe; + + switch (TEST_ORDER[next_]) { + case PROXY_HTTPS: + probe.assign("CONNECT www.google.com:443 HTTP/1.0\r\n" + "User-Agent: "); + probe.append(agent_); + probe.append("\r\n" + "Host: www.google.com\r\n" + "Content-Length: 0\r\n" + "Proxy-Connection: Keep-Alive\r\n" + "\r\n"); + break; + case PROXY_SOCKS5: + probe.assign("\005\001\000", 3); + break; + default: + ASSERT(false); + return; + } + + LOG(LS_VERBOSE) << "AutoDetectProxy probing type " << TEST_ORDER[next_] + << " sending " << probe.size() << " bytes"; + socket_->Send(probe.data(), probe.size()); +} + +void AutoDetectProxy::OnReadEvent(AsyncSocket * socket) { + char data[257]; + int len = socket_->Recv(data, 256); + if (len > 0) { + data[len] = 0; + LOG(LS_VERBOSE) << "AutoDetectProxy read " << len << " bytes"; + } + + switch (TEST_ORDER[next_]) { + case PROXY_HTTPS: + if ((len >= 2) && (data[0] == '\x05')) { + Complete(PROXY_SOCKS5); + return; + } + if ((len >= 5) && (strncmp(data, "HTTP/", 5) == 0)) { + Complete(PROXY_HTTPS); + return; + } + break; + case PROXY_SOCKS5: + if ((len >= 2) && (data[0] == '\x05')) { + Complete(PROXY_SOCKS5); + return; + } + break; + default: + ASSERT(false); + return; + } + + ++next_; + Next(); +} + +void AutoDetectProxy::OnCloseEvent(AsyncSocket * socket, int error) { + LOG(LS_VERBOSE) << "AutoDetectProxy closed with error: " << error; + ++next_; + Next(); +} + +} // namespace talk_base diff --git a/talk/base/autodetectproxy.h b/talk/base/autodetectproxy.h new file mode 100644 index 000000000..a6ad3d113 --- /dev/null +++ b/talk/base/autodetectproxy.h @@ -0,0 +1,106 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_AUTODETECTPROXY_H_ +#define TALK_BASE_AUTODETECTPROXY_H_ + +#include + +#include "talk/base/constructormagic.h" +#include "talk/base/cryptstring.h" +#include "talk/base/proxydetect.h" +#include "talk/base/proxyinfo.h" +#include "talk/base/signalthread.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// AutoDetectProxy +/////////////////////////////////////////////////////////////////////////////// + +class AsyncResolver; +class AsyncSocket; + +class AutoDetectProxy : public SignalThread { + public: + explicit AutoDetectProxy(const std::string& user_agent); + + const ProxyInfo& proxy() const { return proxy_; } + + void set_server_url(const std::string& url) { + server_url_ = url; + } + void set_proxy(const SocketAddress& proxy) { + proxy_.type = PROXY_UNKNOWN; + proxy_.address = proxy; + } + void set_auth_info(bool use_auth, const std::string& username, + const CryptString& password) { + if (use_auth) { + proxy_.username = username; + proxy_.password = password; + } + } + // Default implementation of GetProxySettingsForUrl. Override for special + // implementation. + virtual bool GetProxyForUrl(const char* agent, const char* url, + talk_base::ProxyInfo* proxy) { + return GetProxySettingsForUrl(agent, url, proxy, true); + } + enum { MSG_TIMEOUT = SignalThread::ST_MSG_FIRST_AVAILABLE, + ADP_MSG_FIRST_AVAILABLE}; + + protected: + virtual ~AutoDetectProxy(); + + // SignalThread Interface + virtual void DoWork(); + virtual void OnMessage(Message *msg); + + void Next(); + void Complete(ProxyType type); + + void OnConnectEvent(AsyncSocket * socket); + void OnReadEvent(AsyncSocket * socket); + void OnCloseEvent(AsyncSocket * socket, int error); + void OnResolveResult(SignalThread* thread); + void DoConnect(); + + private: + std::string agent_; + std::string server_url_; + ProxyInfo proxy_; + AsyncResolver* resolver_; + AsyncSocket* socket_; + int next_; + + DISALLOW_IMPLICIT_CONSTRUCTORS(AutoDetectProxy); +}; + +} // namespace talk_base + +#endif // TALK_BASE_AUTODETECTPROXY_H_ diff --git a/talk/base/autodetectproxy_unittest.cc b/talk/base/autodetectproxy_unittest.cc new file mode 100644 index 000000000..3fca4c6b4 --- /dev/null +++ b/talk/base/autodetectproxy_unittest.cc @@ -0,0 +1,141 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/autodetectproxy.h" +#include "talk/base/gunit.h" +#include "talk/base/httpcommon.h" +#include "talk/base/httpcommon-inl.h" + +namespace talk_base { + +static const char kUserAgent[] = ""; +static const char kPath[] = "/"; +static const char kHost[] = "relay.google.com"; +static const uint16 kPort = 443; +static const bool kSecure = true; +// Each of the two stages in AutoDetectProxy has a 2-second time-out, so 5 +// seconds total should be enough. +static const int kTimeoutMs = 5000; + +class AutoDetectProxyTest : public testing::Test, public sigslot::has_slots<> { + public: + AutoDetectProxyTest() : auto_detect_proxy_(NULL), done_(false) {} + + protected: + bool Create(const std::string &user_agent, + const std::string &path, + const std::string &host, + uint16 port, + bool secure, + bool startnow) { + auto_detect_proxy_ = new AutoDetectProxy(user_agent); + EXPECT_TRUE(auto_detect_proxy_ != NULL); + if (!auto_detect_proxy_) { + return false; + } + Url host_url(path, host, port); + host_url.set_secure(secure); + auto_detect_proxy_->set_server_url(host_url.url()); + auto_detect_proxy_->SignalWorkDone.connect( + this, + &AutoDetectProxyTest::OnWorkDone); + if (startnow) { + auto_detect_proxy_->Start(); + } + return true; + } + + bool Run(int timeout_ms) { + EXPECT_TRUE_WAIT(done_, timeout_ms); + return done_; + } + + void SetProxy(const SocketAddress& proxy) { + auto_detect_proxy_->set_proxy(proxy); + } + + void Start() { + auto_detect_proxy_->Start(); + } + + void TestCopesWithProxy(const SocketAddress& proxy) { + // Tests that at least autodetect doesn't crash for a given proxy address. + ASSERT_TRUE(Create(kUserAgent, + kPath, + kHost, + kPort, + kSecure, + false)); + SetProxy(proxy); + Start(); + ASSERT_TRUE(Run(kTimeoutMs)); + } + + private: + void OnWorkDone(talk_base::SignalThread *thread) { + AutoDetectProxy *auto_detect_proxy = + static_cast(thread); + EXPECT_TRUE(auto_detect_proxy == auto_detect_proxy_); + auto_detect_proxy_ = NULL; + auto_detect_proxy->Release(); + done_ = true; + } + + AutoDetectProxy *auto_detect_proxy_; + bool done_; +}; + +TEST_F(AutoDetectProxyTest, TestDetectUnresolvedProxy) { + TestCopesWithProxy(talk_base::SocketAddress("localhost", 9999)); +} + +TEST_F(AutoDetectProxyTest, TestDetectUnresolvableProxy) { + TestCopesWithProxy(talk_base::SocketAddress("invalid", 9999)); +} + +TEST_F(AutoDetectProxyTest, TestDetectIPv6Proxy) { + TestCopesWithProxy(talk_base::SocketAddress("::1", 9999)); +} + +TEST_F(AutoDetectProxyTest, TestDetectIPv4Proxy) { + TestCopesWithProxy(talk_base::SocketAddress("127.0.0.1", 9999)); +} + +// Test that proxy detection completes successfully. (Does not actually verify +// the correct detection result since we don't know what proxy to expect on an +// arbitrary machine.) +TEST_F(AutoDetectProxyTest, TestProxyDetection) { + ASSERT_TRUE(Create(kUserAgent, + kPath, + kHost, + kPort, + kSecure, + true)); + ASSERT_TRUE(Run(kTimeoutMs)); +} + +} // namespace talk_base diff --git a/talk/base/bandwidthsmoother.cc b/talk/base/bandwidthsmoother.cc new file mode 100644 index 000000000..39164884d --- /dev/null +++ b/talk/base/bandwidthsmoother.cc @@ -0,0 +1,101 @@ +/* + * libjingle + * Copyright 2011, 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/base/bandwidthsmoother.h" + +#include + +namespace talk_base { + +BandwidthSmoother::BandwidthSmoother(int initial_bandwidth_guess, + uint32 time_between_increase, + double percent_increase, + size_t samples_count_to_average, + double min_sample_count_percent) + : time_between_increase_(time_between_increase), + percent_increase_(talk_base::_max(1.0, percent_increase)), + time_at_last_change_(0), + bandwidth_estimation_(initial_bandwidth_guess), + accumulator_(samples_count_to_average), + min_sample_count_percent_( + talk_base::_min(1.0, + talk_base::_max(0.0, min_sample_count_percent))) { +} + +// Samples a new bandwidth measurement +// returns true if the bandwidth estimation changed +bool BandwidthSmoother::Sample(uint32 sample_time, int bandwidth) { + if (bandwidth < 0) { + return false; + } + + accumulator_.AddSample(bandwidth); + + if (accumulator_.count() < static_cast( + accumulator_.max_count() * min_sample_count_percent_)) { + // We have not collected enough samples yet. + return false; + } + + // Replace bandwidth with the mean of sampled bandwidths. + const int mean_bandwidth = accumulator_.ComputeMean(); + + if (mean_bandwidth < bandwidth_estimation_) { + time_at_last_change_ = sample_time; + bandwidth_estimation_ = mean_bandwidth; + return true; + } + + const int old_bandwidth_estimation = bandwidth_estimation_; + const double increase_threshold_d = percent_increase_ * bandwidth_estimation_; + if (increase_threshold_d > INT_MAX) { + // If bandwidth goes any higher we would overflow. + return false; + } + + const int increase_threshold = static_cast(increase_threshold_d); + if (mean_bandwidth < increase_threshold) { + time_at_last_change_ = sample_time; + // The value of bandwidth_estimation remains the same if we don't exceed + // percent_increase_ * bandwidth_estimation_ for at least + // time_between_increase_ time. + } else if (sample_time >= time_at_last_change_ + time_between_increase_) { + time_at_last_change_ = sample_time; + if (increase_threshold == 0) { + // Bandwidth_estimation_ must be zero. Assume a jump from zero to a + // positive bandwidth means we have regained connectivity. + bandwidth_estimation_ = mean_bandwidth; + } else { + bandwidth_estimation_ = increase_threshold; + } + } + // Else don't make a change. + + return old_bandwidth_estimation != bandwidth_estimation_; +} + +} // namespace talk_base diff --git a/talk/base/bandwidthsmoother.h b/talk/base/bandwidthsmoother.h new file mode 100644 index 000000000..f63a0f549 --- /dev/null +++ b/talk/base/bandwidthsmoother.h @@ -0,0 +1,76 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_BASE_BANDWIDTHSMOOTHER_H_ +#define TALK_BASE_BANDWIDTHSMOOTHER_H_ + +#include "talk/base/rollingaccumulator.h" +#include "talk/base/timeutils.h" + +namespace talk_base { + +// The purpose of BandwidthSmoother is to smooth out bandwidth +// estimations so that 'trstate' messages can be triggered when we +// are "sure" there is sufficient bandwidth. To avoid frequent fluctuations, +// we take a slightly pessimistic view of our bandwidth. We only increase +// our estimation when we have sampled bandwidth measurements of values +// at least as large as the current estimation * percent_increase +// for at least time_between_increase time. If a sampled bandwidth +// is less than our current estimation we immediately decrease our estimation +// to that sampled value. +// We retain the initial bandwidth guess as our current bandwidth estimation +// until we have received (min_sample_count_percent * samples_count_to_average) +// number of samples. Min_sample_count_percent must be in range [0, 1]. +class BandwidthSmoother { + public: + BandwidthSmoother(int initial_bandwidth_guess, + uint32 time_between_increase, + double percent_increase, + size_t samples_count_to_average, + double min_sample_count_percent); + + // Samples a new bandwidth measurement. + // bandwidth is expected to be non-negative. + // returns true if the bandwidth estimation changed + bool Sample(uint32 sample_time, int bandwidth); + + int get_bandwidth_estimation() const { + return bandwidth_estimation_; + } + + private: + uint32 time_between_increase_; + double percent_increase_; + uint32 time_at_last_change_; + int bandwidth_estimation_; + RollingAccumulator accumulator_; + double min_sample_count_percent_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_BANDWIDTHSMOOTHER_H_ diff --git a/talk/base/bandwidthsmoother_unittest.cc b/talk/base/bandwidthsmoother_unittest.cc new file mode 100644 index 000000000..3ba846fde --- /dev/null +++ b/talk/base/bandwidthsmoother_unittest.cc @@ -0,0 +1,133 @@ +/* + * libjingle + * Copyright 2011, 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 + +#include "talk/base/bandwidthsmoother.h" +#include "talk/base/gunit.h" + +namespace talk_base { + +static const int kTimeBetweenIncrease = 10; +static const double kPercentIncrease = 1.1; +static const size_t kSamplesCountToAverage = 2; +static const double kMinSampleCountPercent = 1.0; + +TEST(BandwidthSmootherTest, TestSampleIncrease) { + BandwidthSmoother mon(1000, // initial_bandwidth_guess + kTimeBetweenIncrease, + kPercentIncrease, + kSamplesCountToAverage, + kMinSampleCountPercent); + + int bandwidth_sample = 1000; + EXPECT_EQ(bandwidth_sample, mon.get_bandwidth_estimation()); + bandwidth_sample = + static_cast(bandwidth_sample * kPercentIncrease); + EXPECT_FALSE(mon.Sample(9, bandwidth_sample)); + EXPECT_TRUE(mon.Sample(10, bandwidth_sample)); + EXPECT_EQ(bandwidth_sample, mon.get_bandwidth_estimation()); + int next_expected_est = + static_cast(bandwidth_sample * kPercentIncrease); + bandwidth_sample *= 2; + EXPECT_TRUE(mon.Sample(20, bandwidth_sample)); + EXPECT_EQ(next_expected_est, mon.get_bandwidth_estimation()); +} + +TEST(BandwidthSmootherTest, TestSampleIncreaseFromZero) { + BandwidthSmoother mon(0, // initial_bandwidth_guess + kTimeBetweenIncrease, + kPercentIncrease, + kSamplesCountToAverage, + kMinSampleCountPercent); + + const int kBandwidthSample = 1000; + EXPECT_EQ(0, mon.get_bandwidth_estimation()); + EXPECT_FALSE(mon.Sample(9, kBandwidthSample)); + EXPECT_TRUE(mon.Sample(10, kBandwidthSample)); + EXPECT_EQ(kBandwidthSample, mon.get_bandwidth_estimation()); +} + +TEST(BandwidthSmootherTest, TestSampleDecrease) { + BandwidthSmoother mon(1000, // initial_bandwidth_guess + kTimeBetweenIncrease, + kPercentIncrease, + kSamplesCountToAverage, + kMinSampleCountPercent); + + const int kBandwidthSample = 999; + EXPECT_EQ(1000, mon.get_bandwidth_estimation()); + EXPECT_FALSE(mon.Sample(1, kBandwidthSample)); + EXPECT_EQ(1000, mon.get_bandwidth_estimation()); + EXPECT_TRUE(mon.Sample(2, kBandwidthSample)); + EXPECT_EQ(kBandwidthSample, mon.get_bandwidth_estimation()); +} + +TEST(BandwidthSmootherTest, TestSampleTooFewSamples) { + BandwidthSmoother mon(1000, // initial_bandwidth_guess + kTimeBetweenIncrease, + kPercentIncrease, + 10, // 10 samples. + 0.5); // 5 min samples. + + const int kBandwidthSample = 500; + EXPECT_EQ(1000, mon.get_bandwidth_estimation()); + EXPECT_FALSE(mon.Sample(1, kBandwidthSample)); + EXPECT_FALSE(mon.Sample(2, kBandwidthSample)); + EXPECT_FALSE(mon.Sample(3, kBandwidthSample)); + EXPECT_FALSE(mon.Sample(4, kBandwidthSample)); + EXPECT_EQ(1000, mon.get_bandwidth_estimation()); + EXPECT_TRUE(mon.Sample(5, kBandwidthSample)); + EXPECT_EQ(kBandwidthSample, mon.get_bandwidth_estimation()); +} + +TEST(BandwidthSmootherTest, TestSampleRollover) { + const int kHugeBandwidth = 2000000000; // > INT_MAX/1.1 + BandwidthSmoother mon(kHugeBandwidth, + kTimeBetweenIncrease, + kPercentIncrease, + kSamplesCountToAverage, + kMinSampleCountPercent); + + EXPECT_FALSE(mon.Sample(10, INT_MAX)); + EXPECT_FALSE(mon.Sample(11, INT_MAX)); + EXPECT_EQ(kHugeBandwidth, mon.get_bandwidth_estimation()); +} + +TEST(BandwidthSmootherTest, TestSampleNegative) { + BandwidthSmoother mon(1000, // initial_bandwidth_guess + kTimeBetweenIncrease, + kPercentIncrease, + kSamplesCountToAverage, + kMinSampleCountPercent); + + EXPECT_FALSE(mon.Sample(10, -1)); + EXPECT_FALSE(mon.Sample(11, -1)); + EXPECT_EQ(1000, mon.get_bandwidth_estimation()); +} + +} // namespace talk_base diff --git a/talk/base/base64.cc b/talk/base/base64.cc new file mode 100644 index 000000000..7765f107e --- /dev/null +++ b/talk/base/base64.cc @@ -0,0 +1,259 @@ + +//********************************************************************* +//* Base64 - a simple base64 encoder and decoder. +//* +//* Copyright (c) 1999, Bob Withers - bwit@pobox.com +//* +//* This code may be freely used for any purpose, either personal +//* or commercial, provided the authors copyright notice remains +//* intact. +//* +//* Enhancements by Stanley Yamane: +//* o reverse lookup table for the decode function +//* o reserve string buffer space in advance +//* +//********************************************************************* + +#include "talk/base/base64.h" + +#include + +#include "talk/base/common.h" + +using std::string; +using std::vector; + +namespace talk_base { + +static const char kPad = '='; +static const unsigned char pd = 0xFD; // Padding +static const unsigned char sp = 0xFE; // Whitespace +static const unsigned char il = 0xFF; // Illegal base64 character + +const char Base64::Base64Table[] = +// 0000000000111111111122222222223333333333444444444455555555556666 +// 0123456789012345678901234567890123456789012345678901234567890123 + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +// Decode Table gives the index of any valid base64 character in the +// Base64 table +// 65 == A, 97 == a, 48 == 0, 43 == +, 47 == / + +const unsigned char Base64::DecodeTable[] = { +// 0 1 2 3 4 5 6 7 8 9 + il,il,il,il,il,il,il,il,il,sp, // 0 - 9 + sp,sp,sp,sp,il,il,il,il,il,il, // 10 - 19 + il,il,il,il,il,il,il,il,il,il, // 20 - 29 + il,il,sp,il,il,il,il,il,il,il, // 30 - 39 + il,il,il,62,il,il,il,63,52,53, // 40 - 49 + 54,55,56,57,58,59,60,61,il,il, // 50 - 59 + il,pd,il,il,il, 0, 1, 2, 3, 4, // 60 - 69 + 5, 6, 7, 8, 9,10,11,12,13,14, // 70 - 79 + 15,16,17,18,19,20,21,22,23,24, // 80 - 89 + 25,il,il,il,il,il,il,26,27,28, // 90 - 99 + 29,30,31,32,33,34,35,36,37,38, // 100 - 109 + 39,40,41,42,43,44,45,46,47,48, // 110 - 119 + 49,50,51,il,il,il,il,il,il,il, // 120 - 129 + il,il,il,il,il,il,il,il,il,il, // 130 - 139 + il,il,il,il,il,il,il,il,il,il, // 140 - 149 + il,il,il,il,il,il,il,il,il,il, // 150 - 159 + il,il,il,il,il,il,il,il,il,il, // 160 - 169 + il,il,il,il,il,il,il,il,il,il, // 170 - 179 + il,il,il,il,il,il,il,il,il,il, // 180 - 189 + il,il,il,il,il,il,il,il,il,il, // 190 - 199 + il,il,il,il,il,il,il,il,il,il, // 200 - 209 + il,il,il,il,il,il,il,il,il,il, // 210 - 219 + il,il,il,il,il,il,il,il,il,il, // 220 - 229 + il,il,il,il,il,il,il,il,il,il, // 230 - 239 + il,il,il,il,il,il,il,il,il,il, // 240 - 249 + il,il,il,il,il,il // 250 - 255 +}; + +bool Base64::IsBase64Char(char ch) { + return (('A' <= ch) && (ch <= 'Z')) || + (('a' <= ch) && (ch <= 'z')) || + (('0' <= ch) && (ch <= '9')) || + (ch == '+') || (ch == '/'); +} + +bool Base64::GetNextBase64Char(char ch, char* next_ch) { + if (next_ch == NULL) { + return false; + } + const char* p = strchr(Base64Table, ch); + if (!p) + return false; + ++p; + *next_ch = (*p) ? *p : Base64Table[0]; + return true; +} + +bool Base64::IsBase64Encoded(const std::string& str) { + for (size_t i = 0; i < str.size(); ++i) { + if (!IsBase64Char(str.at(i))) + return false; + } + return true; +} + +void Base64::EncodeFromArray(const void* data, size_t len, string* result) { + ASSERT(NULL != result); + result->clear(); + result->resize(((len + 2) / 3) * 4); + const unsigned char* byte_data = static_cast(data); + + unsigned char c; + size_t i = 0; + size_t dest_ix = 0; + while (i < len) { + c = (byte_data[i] >> 2) & 0x3f; + (*result)[dest_ix++] = Base64Table[c]; + + c = (byte_data[i] << 4) & 0x3f; + if (++i < len) { + c |= (byte_data[i] >> 4) & 0x0f; + } + (*result)[dest_ix++] = Base64Table[c]; + + if (i < len) { + c = (byte_data[i] << 2) & 0x3f; + if (++i < len) { + c |= (byte_data[i] >> 6) & 0x03; + } + (*result)[dest_ix++] = Base64Table[c]; + } else { + (*result)[dest_ix++] = kPad; + } + + if (i < len) { + c = byte_data[i] & 0x3f; + (*result)[dest_ix++] = Base64Table[c]; + ++i; + } else { + (*result)[dest_ix++] = kPad; + } + } +} + +size_t Base64::GetNextQuantum(DecodeFlags parse_flags, bool illegal_pads, + const char* data, size_t len, size_t* dpos, + unsigned char qbuf[4], bool* padded) +{ + size_t byte_len = 0, pad_len = 0, pad_start = 0; + for (; (byte_len < 4) && (*dpos < len); ++*dpos) { + qbuf[byte_len] = DecodeTable[static_cast(data[*dpos])]; + if ((il == qbuf[byte_len]) || (illegal_pads && (pd == qbuf[byte_len]))) { + if (parse_flags != DO_PARSE_ANY) + break; + // Ignore illegal characters + } else if (sp == qbuf[byte_len]) { + if (parse_flags == DO_PARSE_STRICT) + break; + // Ignore spaces + } else if (pd == qbuf[byte_len]) { + if (byte_len < 2) { + if (parse_flags != DO_PARSE_ANY) + break; + // Ignore unexpected padding + } else if (byte_len + pad_len >= 4) { + if (parse_flags != DO_PARSE_ANY) + break; + // Ignore extra pads + } else { + if (1 == ++pad_len) { + pad_start = *dpos; + } + } + } else { + if (pad_len > 0) { + if (parse_flags != DO_PARSE_ANY) + break; + // Ignore pads which are followed by data + pad_len = 0; + } + ++byte_len; + } + } + for (size_t i = byte_len; i < 4; ++i) { + qbuf[i] = 0; + } + if (4 == byte_len + pad_len) { + *padded = true; + } else { + *padded = false; + if (pad_len) { + // Roll back illegal padding + *dpos = pad_start; + } + } + return byte_len; +} + +bool Base64::DecodeFromArray(const char* data, size_t len, DecodeFlags flags, + string* result, size_t* data_used) { + return DecodeFromArrayTemplate(data, len, flags, result, data_used); +} + +bool Base64::DecodeFromArray(const char* data, size_t len, DecodeFlags flags, + vector* result, size_t* data_used) { + return DecodeFromArrayTemplate >(data, len, flags, result, + data_used); +} + +template +bool Base64::DecodeFromArrayTemplate(const char* data, size_t len, + DecodeFlags flags, T* result, + size_t* data_used) +{ + ASSERT(NULL != result); + ASSERT(flags <= (DO_PARSE_MASK | DO_PAD_MASK | DO_TERM_MASK)); + + const DecodeFlags parse_flags = flags & DO_PARSE_MASK; + const DecodeFlags pad_flags = flags & DO_PAD_MASK; + const DecodeFlags term_flags = flags & DO_TERM_MASK; + ASSERT(0 != parse_flags); + ASSERT(0 != pad_flags); + ASSERT(0 != term_flags); + + result->clear(); + result->reserve(len); + + size_t dpos = 0; + bool success = true, padded; + unsigned char c, qbuf[4]; + while (dpos < len) { + size_t qlen = GetNextQuantum(parse_flags, (DO_PAD_NO == pad_flags), + data, len, &dpos, qbuf, &padded); + c = (qbuf[0] << 2) | ((qbuf[1] >> 4) & 0x3); + if (qlen >= 2) { + result->push_back(c); + c = ((qbuf[1] << 4) & 0xf0) | ((qbuf[2] >> 2) & 0xf); + if (qlen >= 3) { + result->push_back(c); + c = ((qbuf[2] << 6) & 0xc0) | qbuf[3]; + if (qlen >= 4) { + result->push_back(c); + c = 0; + } + } + } + if (qlen < 4) { + if ((DO_TERM_ANY != term_flags) && (0 != c)) { + success = false; // unused bits + } + if ((DO_PAD_YES == pad_flags) && !padded) { + success = false; // expected padding + } + break; + } + } + if ((DO_TERM_BUFFER == term_flags) && (dpos != len)) { + success = false; // unused chars + } + if (data_used) { + *data_used = dpos; + } + return success; +} + +} // namespace talk_base diff --git a/talk/base/base64.h b/talk/base/base64.h new file mode 100644 index 000000000..a963515d8 --- /dev/null +++ b/talk/base/base64.h @@ -0,0 +1,104 @@ + +//********************************************************************* +//* C_Base64 - a simple base64 encoder and decoder. +//* +//* Copyright (c) 1999, Bob Withers - bwit@pobox.com +//* +//* This code may be freely used for any purpose, either personal +//* or commercial, provided the authors copyright notice remains +//* intact. +//********************************************************************* + +#ifndef TALK_BASE_BASE64_H__ +#define TALK_BASE_BASE64_H__ + +#include +#include + +namespace talk_base { + +class Base64 +{ +public: + enum DecodeOption { + DO_PARSE_STRICT = 1, // Parse only base64 characters + DO_PARSE_WHITE = 2, // Parse only base64 and whitespace characters + DO_PARSE_ANY = 3, // Parse all characters + DO_PARSE_MASK = 3, + + DO_PAD_YES = 4, // Padding is required + DO_PAD_ANY = 8, // Padding is optional + DO_PAD_NO = 12, // Padding is disallowed + DO_PAD_MASK = 12, + + DO_TERM_BUFFER = 16, // Must termiante at end of buffer + DO_TERM_CHAR = 32, // May terminate at any character boundary + DO_TERM_ANY = 48, // May terminate at a sub-character bit offset + DO_TERM_MASK = 48, + + // Strictest interpretation + DO_STRICT = DO_PARSE_STRICT | DO_PAD_YES | DO_TERM_BUFFER, + + DO_LAX = DO_PARSE_ANY | DO_PAD_ANY | DO_TERM_CHAR, + }; + typedef int DecodeFlags; + + static bool IsBase64Char(char ch); + + // Get the char next to the |ch| from the Base64Table. + // If the |ch| is the last one in the Base64Table then returns + // the first one from the table. + // Expects the |ch| be a base64 char. + // The result will be saved in |next_ch|. + // Returns true on success. + static bool GetNextBase64Char(char ch, char* next_ch); + + // Determines whether the given string consists entirely of valid base64 + // encoded characters. + static bool IsBase64Encoded(const std::string& str); + + static void EncodeFromArray(const void* data, size_t len, + std::string* result); + static bool DecodeFromArray(const char* data, size_t len, DecodeFlags flags, + std::string* result, size_t* data_used); + static bool DecodeFromArray(const char* data, size_t len, DecodeFlags flags, + std::vector* result, size_t* data_used); + + // Convenience Methods + static inline std::string Encode(const std::string& data) { + std::string result; + EncodeFromArray(data.data(), data.size(), &result); + return result; + } + static inline std::string Decode(const std::string& data, DecodeFlags flags) { + std::string result; + DecodeFromArray(data.data(), data.size(), flags, &result, NULL); + return result; + } + static inline bool Decode(const std::string& data, DecodeFlags flags, + std::string* result, size_t* data_used) + { + return DecodeFromArray(data.data(), data.size(), flags, result, data_used); + } + static inline bool Decode(const std::string& data, DecodeFlags flags, + std::vector* result, size_t* data_used) + { + return DecodeFromArray(data.data(), data.size(), flags, result, data_used); + } + +private: + static const char Base64Table[]; + static const unsigned char DecodeTable[]; + + static size_t GetNextQuantum(DecodeFlags parse_flags, bool illegal_pads, + const char* data, size_t len, size_t* dpos, + unsigned char qbuf[4], bool* padded); + template + static bool DecodeFromArrayTemplate(const char* data, size_t len, + DecodeFlags flags, T* result, + size_t* data_used); +}; + +} // namespace talk_base + +#endif // TALK_BASE_BASE64_H__ diff --git a/talk/base/base64_unittest.cc b/talk/base/base64_unittest.cc new file mode 100644 index 000000000..20869e4e8 --- /dev/null +++ b/talk/base/base64_unittest.cc @@ -0,0 +1,1018 @@ +/* + * libjingle + * Copyright 2011, 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/base/common.h" +#include "talk/base/base64.h" +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/stringutils.h" +#include "talk/base/stream.h" + +#include "talk/base/testbase64.h" + +using namespace std; +using namespace talk_base; + +static struct { + size_t plain_length; + const char* plaintext; + const char* cyphertext; +} base64_tests[] = { + + // Basic bit patterns; + // values obtained with "echo -n '...' | uuencode -m test" + + { 1, "\000", "AA==" }, + { 1, "\001", "AQ==" }, + { 1, "\002", "Ag==" }, + { 1, "\004", "BA==" }, + { 1, "\010", "CA==" }, + { 1, "\020", "EA==" }, + { 1, "\040", "IA==" }, + { 1, "\100", "QA==" }, + { 1, "\200", "gA==" }, + + { 1, "\377", "/w==" }, + { 1, "\376", "/g==" }, + { 1, "\375", "/Q==" }, + { 1, "\373", "+w==" }, + { 1, "\367", "9w==" }, + { 1, "\357", "7w==" }, + { 1, "\337", "3w==" }, + { 1, "\277", "vw==" }, + { 1, "\177", "fw==" }, + { 2, "\000\000", "AAA=" }, + { 2, "\000\001", "AAE=" }, + { 2, "\000\002", "AAI=" }, + { 2, "\000\004", "AAQ=" }, + { 2, "\000\010", "AAg=" }, + { 2, "\000\020", "ABA=" }, + { 2, "\000\040", "ACA=" }, + { 2, "\000\100", "AEA=" }, + { 2, "\000\200", "AIA=" }, + { 2, "\001\000", "AQA=" }, + { 2, "\002\000", "AgA=" }, + { 2, "\004\000", "BAA=" }, + { 2, "\010\000", "CAA=" }, + { 2, "\020\000", "EAA=" }, + { 2, "\040\000", "IAA=" }, + { 2, "\100\000", "QAA=" }, + { 2, "\200\000", "gAA=" }, + + { 2, "\377\377", "//8=" }, + { 2, "\377\376", "//4=" }, + { 2, "\377\375", "//0=" }, + { 2, "\377\373", "//s=" }, + { 2, "\377\367", "//c=" }, + { 2, "\377\357", "/+8=" }, + { 2, "\377\337", "/98=" }, + { 2, "\377\277", "/78=" }, + { 2, "\377\177", "/38=" }, + { 2, "\376\377", "/v8=" }, + { 2, "\375\377", "/f8=" }, + { 2, "\373\377", "+/8=" }, + { 2, "\367\377", "9/8=" }, + { 2, "\357\377", "7/8=" }, + { 2, "\337\377", "3/8=" }, + { 2, "\277\377", "v/8=" }, + { 2, "\177\377", "f/8=" }, + + { 3, "\000\000\000", "AAAA" }, + { 3, "\000\000\001", "AAAB" }, + { 3, "\000\000\002", "AAAC" }, + { 3, "\000\000\004", "AAAE" }, + { 3, "\000\000\010", "AAAI" }, + { 3, "\000\000\020", "AAAQ" }, + { 3, "\000\000\040", "AAAg" }, + { 3, "\000\000\100", "AABA" }, + { 3, "\000\000\200", "AACA" }, + { 3, "\000\001\000", "AAEA" }, + { 3, "\000\002\000", "AAIA" }, + { 3, "\000\004\000", "AAQA" }, + { 3, "\000\010\000", "AAgA" }, + { 3, "\000\020\000", "ABAA" }, + { 3, "\000\040\000", "ACAA" }, + { 3, "\000\100\000", "AEAA" }, + { 3, "\000\200\000", "AIAA" }, + { 3, "\001\000\000", "AQAA" }, + { 3, "\002\000\000", "AgAA" }, + { 3, "\004\000\000", "BAAA" }, + { 3, "\010\000\000", "CAAA" }, + { 3, "\020\000\000", "EAAA" }, + { 3, "\040\000\000", "IAAA" }, + { 3, "\100\000\000", "QAAA" }, + { 3, "\200\000\000", "gAAA" }, + + { 3, "\377\377\377", "////" }, + { 3, "\377\377\376", "///+" }, + { 3, "\377\377\375", "///9" }, + { 3, "\377\377\373", "///7" }, + { 3, "\377\377\367", "///3" }, + { 3, "\377\377\357", "///v" }, + { 3, "\377\377\337", "///f" }, + { 3, "\377\377\277", "//+/" }, + { 3, "\377\377\177", "//9/" }, + { 3, "\377\376\377", "//7/" }, + { 3, "\377\375\377", "//3/" }, + { 3, "\377\373\377", "//v/" }, + { 3, "\377\367\377", "//f/" }, + { 3, "\377\357\377", "/+//" }, + { 3, "\377\337\377", "/9//" }, + { 3, "\377\277\377", "/7//" }, + { 3, "\377\177\377", "/3//" }, + { 3, "\376\377\377", "/v//" }, + { 3, "\375\377\377", "/f//" }, + { 3, "\373\377\377", "+///" }, + { 3, "\367\377\377", "9///" }, + { 3, "\357\377\377", "7///" }, + { 3, "\337\377\377", "3///" }, + { 3, "\277\377\377", "v///" }, + { 3, "\177\377\377", "f///" }, + + // Random numbers: values obtained with + // + // #! /bin/bash + // dd bs=$1 count=1 if=/dev/random of=/tmp/bar.random + // od -N $1 -t o1 /tmp/bar.random + // uuencode -m test < /tmp/bar.random + // + // where $1 is the number of bytes (2, 3) + + { 2, "\243\361", "o/E=" }, + { 2, "\024\167", "FHc=" }, + { 2, "\313\252", "y6o=" }, + { 2, "\046\041", "JiE=" }, + { 2, "\145\236", "ZZ4=" }, + { 2, "\254\325", "rNU=" }, + { 2, "\061\330", "Mdg=" }, + { 2, "\245\032", "pRo=" }, + { 2, "\006\000", "BgA=" }, + { 2, "\375\131", "/Vk=" }, + { 2, "\303\210", "w4g=" }, + { 2, "\040\037", "IB8=" }, + { 2, "\261\372", "sfo=" }, + { 2, "\335\014", "3Qw=" }, + { 2, "\233\217", "m48=" }, + { 2, "\373\056", "+y4=" }, + { 2, "\247\232", "p5o=" }, + { 2, "\107\053", "Rys=" }, + { 2, "\204\077", "hD8=" }, + { 2, "\276\211", "vok=" }, + { 2, "\313\110", "y0g=" }, + { 2, "\363\376", "8/4=" }, + { 2, "\251\234", "qZw=" }, + { 2, "\103\262", "Q7I=" }, + { 2, "\142\312", "Yso=" }, + { 2, "\067\211", "N4k=" }, + { 2, "\220\001", "kAE=" }, + { 2, "\152\240", "aqA=" }, + { 2, "\367\061", "9zE=" }, + { 2, "\133\255", "W60=" }, + { 2, "\176\035", "fh0=" }, + { 2, "\032\231", "Gpk=" }, + + { 3, "\013\007\144", "Cwdk" }, + { 3, "\030\112\106", "GEpG" }, + { 3, "\047\325\046", "J9Um" }, + { 3, "\310\160\022", "yHAS" }, + { 3, "\131\100\237", "WUCf" }, + { 3, "\064\342\134", "NOJc" }, + { 3, "\010\177\004", "CH8E" }, + { 3, "\345\147\205", "5WeF" }, + { 3, "\300\343\360", "wOPw" }, + { 3, "\061\240\201", "MaCB" }, + { 3, "\225\333\044", "ldsk" }, + { 3, "\215\137\352", "jV/q" }, + { 3, "\371\147\160", "+Wdw" }, + { 3, "\030\320\051", "GNAp" }, + { 3, "\044\174\241", "JHyh" }, + { 3, "\260\127\037", "sFcf" }, + { 3, "\111\045\033", "SSUb" }, + { 3, "\202\114\107", "gkxH" }, + { 3, "\057\371\042", "L/ki" }, + { 3, "\223\247\244", "k6ek" }, + { 3, "\047\216\144", "J45k" }, + { 3, "\203\070\327", "gzjX" }, + { 3, "\247\140\072", "p2A6" }, + { 3, "\124\115\116", "VE1O" }, + { 3, "\157\162\050", "b3Io" }, + { 3, "\357\223\004", "75ME" }, + { 3, "\052\117\156", "Kk9u" }, + { 3, "\347\154\000", "52wA" }, + { 3, "\303\012\142", "wwpi" }, + { 3, "\060\035\362", "MB3y" }, + { 3, "\130\226\361", "WJbx" }, + { 3, "\173\013\071", "ews5" }, + { 3, "\336\004\027", "3gQX" }, + { 3, "\357\366\234", "7/ac" }, + { 3, "\353\304\111", "68RJ" }, + { 3, "\024\264\131", "FLRZ" }, + { 3, "\075\114\251", "PUyp" }, + { 3, "\315\031\225", "zRmV" }, + { 3, "\154\201\276", "bIG+" }, + { 3, "\200\066\072", "gDY6" }, + { 3, "\142\350\267", "Yui3" }, + { 3, "\033\000\166", "GwB2" }, + { 3, "\210\055\077", "iC0/" }, + { 3, "\341\037\124", "4R9U" }, + { 3, "\161\103\152", "cUNq" }, + { 3, "\270\142\131", "uGJZ" }, + { 3, "\337\076\074", "3z48" }, + { 3, "\375\106\362", "/Uby" }, + { 3, "\227\301\127", "l8FX" }, + { 3, "\340\002\234", "4AKc" }, + { 3, "\121\064\033", "UTQb" }, + { 3, "\157\134\143", "b1xj" }, + { 3, "\247\055\327", "py3X" }, + { 3, "\340\142\005", "4GIF" }, + { 3, "\060\260\143", "MLBj" }, + { 3, "\075\203\170", "PYN4" }, + { 3, "\143\160\016", "Y3AO" }, + { 3, "\313\013\063", "ywsz" }, + { 3, "\174\236\135", "fJ5d" }, + { 3, "\103\047\026", "QycW" }, + { 3, "\365\005\343", "9QXj" }, + { 3, "\271\160\223", "uXCT" }, + { 3, "\362\255\172", "8q16" }, + { 3, "\113\012\015", "SwoN" }, + + // various lengths, generated by this python script: + // + // from string import lowercase as lc + // for i in range(27): + // print '{ %2d, "%s",%s "%s" },' % (i, lc[:i], ' ' * (26-i), + // lc[:i].encode('base64').strip()) + + { 0, "abcdefghijklmnopqrstuvwxyz", "" }, + { 1, "abcdefghijklmnopqrstuvwxyz", "YQ==" }, + { 2, "abcdefghijklmnopqrstuvwxyz", "YWI=" }, + { 3, "abcdefghijklmnopqrstuvwxyz", "YWJj" }, + { 4, "abcdefghijklmnopqrstuvwxyz", "YWJjZA==" }, + { 5, "abcdefghijklmnopqrstuvwxyz", "YWJjZGU=" }, + { 6, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVm" }, + { 7, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZw==" }, + { 8, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2g=" }, + { 9, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hp" }, + { 10, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpag==" }, + { 11, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpams=" }, + { 12, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamts" }, + { 13, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbQ==" }, + { 14, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW4=" }, + { 15, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5v" }, + { 16, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcA==" }, + { 17, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHE=" }, + { 18, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFy" }, + { 19, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFycw==" }, + { 20, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFyc3Q=" }, + { 21, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1" }, + { 22, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dg==" }, + { 23, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnc=" }, + { 24, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4" }, + { 25, "abcdefghijklmnopqrstuvwxy", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eQ==" }, + { 26, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=" }, +}; +#if 0 +static struct { + const char* plaintext; + const char* cyphertext; +} base64_strings[] = { + + // The first few Google quotes + // Cyphertext created with "uuencode - GNU sharutils 4.2.1" + { + "Everyone! We're teetering on the brink of disaster." + " - Sergey Brin, 6/24/99, regarding the company's state " + "after the unleashing of Netscape/Google search", + + "RXZlcnlvbmUhICBXZSdyZSB0ZWV0ZXJpbmcgb24gdGhlIGJyaW5rIG9mIGRp" + "c2FzdGVyLiAtIFNlcmdleSBCcmluLCA2LzI0Lzk5LCByZWdhcmRpbmcgdGhl" + "IGNvbXBhbnkncyBzdGF0ZSBhZnRlciB0aGUgdW5sZWFzaGluZyBvZiBOZXRz" + "Y2FwZS9Hb29nbGUgc2VhcmNo" }, + + { + "I'm not sure why we're still alive, but we seem to be." + " - Larry Page, 6/24/99, while hiding in the kitchenette " + "during the Netscape traffic overflow", + + "SSdtIG5vdCBzdXJlIHdoeSB3ZSdyZSBzdGlsbCBhbGl2ZSwgYnV0IHdlIHNl" + "ZW0gdG8gYmUuIC0gTGFycnkgUGFnZSwgNi8yNC85OSwgd2hpbGUgaGlkaW5n" + "IGluIHRoZSBraXRjaGVuZXR0ZSBkdXJpbmcgdGhlIE5ldHNjYXBlIHRyYWZm" + "aWMgb3ZlcmZsb3c" }, + + { + "I think kids want porn." + " - Sergey Brin, 6/99, on why Google shouldn't prioritize a " + "filtered search for children and families", + + "SSB0aGluayBraWRzIHdhbnQgcG9ybi4gLSBTZXJnZXkgQnJpbiwgNi85OSwg" + "b24gd2h5IEdvb2dsZSBzaG91bGRuJ3QgcHJpb3JpdGl6ZSBhIGZpbHRlcmVk" + "IHNlYXJjaCBmb3IgY2hpbGRyZW4gYW5kIGZhbWlsaWVz" }, +}; +#endif +// Compare bytes 0..len-1 of x and y. If not equal, abort with verbose error +// message showing position and numeric value that differed. +// Handles embedded nulls just like any other byte. +// Only added because string.compare() in gcc-3.3.3 seems to misbehave with +// embedded nulls. +// TODO: switch back to string.compare() if/when gcc is fixed +#define EXPECT_EQ_ARRAY(len, x, y, msg) \ + for (size_t j = 0; j < len; ++j) { \ + if (x[j] != y[j]) { \ + LOG(LS_ERROR) << "" # x << " != " # y \ + << " byte " << j << " msg: " << msg; \ + } \ + } + +size_t Base64Escape(const unsigned char *src, size_t szsrc, char *dest, + size_t szdest) { + std::string escaped; + Base64::EncodeFromArray((const char *)src, szsrc, &escaped); + memcpy(dest, escaped.data(), min(escaped.size(), szdest)); + return escaped.size(); +} + +size_t Base64Unescape(const char *src, size_t szsrc, char *dest, + size_t szdest) { + std::string unescaped; + EXPECT_TRUE(Base64::DecodeFromArray(src, szsrc, Base64::DO_LAX, &unescaped, + NULL)); + memcpy(dest, unescaped.data(), min(unescaped.size(), szdest)); + return unescaped.size(); +} + +size_t Base64Unescape(const char *src, size_t szsrc, string *s) { + EXPECT_TRUE(Base64::DecodeFromArray(src, szsrc, Base64::DO_LAX, s, NULL)); + return s->size(); +} + +TEST(Base64, EncodeDecodeBattery) { + LOG(LS_VERBOSE) << "Testing base-64"; + + size_t i; + + // Check the short strings; this tests the math (and boundaries) + for( i = 0; i < sizeof(base64_tests) / sizeof(base64_tests[0]); ++i ) { + char encode_buffer[100]; + size_t encode_length; + char decode_buffer[100]; + size_t decode_length; + size_t cypher_length; + + LOG(LS_VERBOSE) << "B64: " << base64_tests[i].cyphertext; + + const unsigned char* unsigned_plaintext = + reinterpret_cast(base64_tests[i].plaintext); + + cypher_length = strlen(base64_tests[i].cyphertext); + + // The basic escape function: + memset(encode_buffer, 0, sizeof(encode_buffer)); + encode_length = Base64Escape(unsigned_plaintext, + base64_tests[i].plain_length, + encode_buffer, + sizeof(encode_buffer)); + // Is it of the expected length? + EXPECT_EQ(encode_length, cypher_length); + + // Is it the expected encoded value? + EXPECT_STREQ(encode_buffer, base64_tests[i].cyphertext); + + // If we encode it into a buffer of exactly the right length... + memset(encode_buffer, 0, sizeof(encode_buffer)); + encode_length = Base64Escape(unsigned_plaintext, + base64_tests[i].plain_length, + encode_buffer, + cypher_length); + // Is it still of the expected length? + EXPECT_EQ(encode_length, cypher_length); + + // And is the value still correct? (i.e., not losing the last byte) + EXPECT_STREQ(encode_buffer, base64_tests[i].cyphertext); + + // If we decode it back: + memset(decode_buffer, 0, sizeof(decode_buffer)); + decode_length = Base64Unescape(encode_buffer, + cypher_length, + decode_buffer, + sizeof(decode_buffer)); + + // Is it of the expected length? + EXPECT_EQ(decode_length, base64_tests[i].plain_length); + + // Is it the expected decoded value? + EXPECT_EQ(0, memcmp(decode_buffer, base64_tests[i].plaintext, decode_length)); + + // Our decoder treats the padding '=' characters at the end as + // optional. If encode_buffer has any, run some additional + // tests that fiddle with them. + char* first_equals = strchr(encode_buffer, '='); + if (first_equals) { + // How many equals signs does the string start with? + int equals = (*(first_equals+1) == '=') ? 2 : 1; + + // Try chopping off the equals sign(s) entirely. The decoder + // should still be okay with this. + string decoded2("this junk should also be ignored"); + *first_equals = '\0'; + EXPECT_NE(0U, Base64Unescape(encode_buffer, first_equals-encode_buffer, + &decoded2)); + EXPECT_EQ(decoded2.size(), base64_tests[i].plain_length); + EXPECT_EQ_ARRAY(decoded2.size(), decoded2.data(), base64_tests[i].plaintext, i); + + size_t len; + + // try putting some extra stuff after the equals signs, or in between them + if (equals == 2) { + sprintfn(first_equals, 6, " = = "); + len = first_equals - encode_buffer + 5; + } else { + sprintfn(first_equals, 6, " = "); + len = first_equals - encode_buffer + 3; + } + decoded2.assign("this junk should be ignored"); + EXPECT_NE(0U, Base64Unescape(encode_buffer, len, &decoded2)); + EXPECT_EQ(decoded2.size(), base64_tests[i].plain_length); + EXPECT_EQ_ARRAY(decoded2.size(), decoded2, base64_tests[i].plaintext, i); + } + } +} + +// here's a weird case: a giant base64 encoded stream which broke our base64 +// decoding. Let's test it explicitly. +const char SpecificTest[] = + "/9j/4AAQSkZJRgABAgEASABIAAD/4Q0HRXhpZgAATU0AKgAAAAgADAEOAAIAAAAgAAAAngEPAAI\n" + "AAAAFAAAAvgEQAAIAAAAJAAAAwwESAAMAAAABAAEAAAEaAAUAAAABAAAAzAEbAAUAAAABAAAA1A\n" + "EoAAMAAAABAAIAAAExAAIAAAAUAAAA3AEyAAIAAAAUAAAA8AE8AAIAAAAQAAABBAITAAMAAAABA\n" + "AIAAIdpAAQAAAABAAABFAAAAsQgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAFNPTlkA\n" + "RFNDLVAyMDAAAAAASAAAAAEAAABIAAAAAUFkb2JlIFBob3Rvc2hvcCA3LjAAMjAwNzowMTozMCA\n" + "yMzoxMDowNABNYWMgT1MgWCAxMC40LjgAAByCmgAFAAAAAQAAAmqCnQAFAAAAAQAAAnKIIgADAA\n" + "AAAQACAACIJwADAAAAAQBkAACQAAAHAAAABDAyMjCQAwACAAAAFAAAAnqQBAACAAAAFAAAAo6RA\n" + "QAHAAAABAECAwCRAgAFAAAAAQAAAqKSBAAKAAAAAQAAAqqSBQAFAAAAAQAAArKSBwADAAAAAQAF\n" + "AACSCAADAAAAAQAAAACSCQADAAAAAQAPAACSCgAFAAAAAQAAArqgAAAHAAAABDAxMDCgAQADAAA\n" + "AAf//AACgAgAEAAAAAQAAAGSgAwAEAAAAAQAAAGSjAAAHAAAAAQMAAACjAQAHAAAAAQEAAACkAQ\n" + "ADAAAAAQAAAACkAgADAAAAAQAAAACkAwADAAAAAQAAAACkBgADAAAAAQAAAACkCAADAAAAAQAAA\n" + "ACkCQADAAAAAQAAAACkCgADAAAAAQAAAAAAAAAAAAAACgAAAZAAAAAcAAAACjIwMDc6MDE6MjAg\n" + "MjM6MDU6NTIAMjAwNzowMToyMCAyMzowNTo1MgAAAAAIAAAAAQAAAAAAAAAKAAAAMAAAABAAAAB\n" + "PAAAACgAAAAYBAwADAAAAAQAGAAABGgAFAAAAAQAAAxIBGwAFAAAAAQAAAxoBKAADAAAAAQACAA\n" + "ACAQAEAAAAAQAAAyICAgAEAAAAAQAACd0AAAAAAAAASAAAAAEAAABIAAAAAf/Y/+AAEEpGSUYAA\n" + "QIBAEgASAAA/+0ADEFkb2JlX0NNAAL/7gAOQWRvYmUAZIAAAAAB/9sAhAAMCAgICQgMCQkMEQsK\n" + "CxEVDwwMDxUYExMVExMYEQwMDAwMDBEMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQ0LCw0\n" + "ODRAODhAUDg4OFBQODg4OFBEMDAwMDBERDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA\n" + "wMDAz/wAARCABkAGQDASIAAhEBAxEB/90ABAAH/8QBPwAAAQUBAQEBAQEAAAAAAAAAAwABAgQFB\n" + "gcICQoLAQABBQEBAQEBAQAAAAAAAAABAAIDBAUGBwgJCgsQAAEEAQMCBAIFBwYIBQMMMwEAAhED\n" + "BCESMQVBUWETInGBMgYUkaGxQiMkFVLBYjM0coLRQwclklPw4fFjczUWorKDJkSTVGRFwqN0Nhf\n" + "SVeJl8rOEw9N14/NGJ5SkhbSVxNTk9KW1xdXl9VZmdoaWprbG1ub2N0dXZ3eHl6e3x9fn9xEAAg\n" + "IBAgQEAwQFBgcHBgU1AQACEQMhMRIEQVFhcSITBTKBkRShsUIjwVLR8DMkYuFygpJDUxVjczTxJ\n" + "QYWorKDByY1wtJEk1SjF2RFVTZ0ZeLys4TD03Xj80aUpIW0lcTU5PSltcXV5fVWZnaGlqa2xtbm\n" + "9ic3R1dnd4eXp7fH/9oADAMBAAIRAxEAPwDy7bKNTUXNLz9EaJPDWMjxH4ozhtpYwaACT8ShaaW\n" + "bW0uEc9/JFfjj0Q4Hk/PRDxwX7y47W9z/AN9Cv4+O3ILK2DcRqT2CaSvEbcl1Jbz37KG1dBldLo\n" + "qaS4l9xGjG9v6yoDAdYIaIjUk+AREgo4y5sapirb8Yl0NHHdKvBNm4yA1o5Pc+SPEFvCWqB3HZF\n" + "Hj2SbWQ/afGFP0bHP8ATY0uc4w1o1JPkkimGiS2KvqlnmBkOZQTyydzgPMM9v8A0lp4v1Nx9gF1\n" + "tpdqJaGtH/S3I0i3lISXW/8AMqnd/O2bfg2eUkqVYf/Q8zuncO4Bj7lZ+n7f5Mj5KsJcY8NUZ4d\n" + "uEDVo1HkeU0rg3Om4H2rabCWUN7DQuK1n5FWKW4uCwG92gDRJBS6exhxmMboQI+Cv4WFTQ42Bs2\n" + "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\n" + "OEJJTQQEAAAAAAArHAIAAAIAAhwCeAAfICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAA\n" + "4QklNBCUAAAAAABD7Caa9B0wqNp2P4sxXqayFOEJJTQPqAAAAAB2wPD94bWwgdmVyc2lvbj0iMS\n" + "4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUgQ\n" + "29tcHV0ZXIvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Q\n" + "cm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk\n" + "+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYXQuUE1Ib3Jpem9udGFsUmVzPC9rZXk+Cgk8ZGljdD\n" + "4KCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuY3JlYXRvcjwva2V5PgoJCTxzdHJpbmc+Y\n" + "29tLmFwcGxlLnByaW50aW5nbWFuYWdlcjwvc3RyaW5nPgoJCTxrZXk+Y29tLmFwcGxlLnByaW50\n" + "LnRpY2tldC5pdGVtQXJyYXk8L2tleT4KCQk8YXJyYXk+CgkJCTxkaWN0PgoJCQkJPGtleT5jb20\n" + "uYXBwbGUucHJpbnQuUGFnZUZvcm1hdC5QTUhvcml6b250YWxSZXM8L2tleT4KCQkJCTxyZWFsPj\n" + "cyPC9yZWFsPgoJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNsaWVudDwva2V5PgoJC\n" + "QkJPHN0cmluZz5jb20uYXBwbGUucHJpbnRpbmdtYW5hZ2VyPC9zdHJpbmc+CgkJCQk8a2V5PmNv\n" + "bS5hcHBsZS5wcmludC50aWNrZXQubW9kRGF0ZTwva2V5PgoJCQkJPGRhdGU+MjAwNy0wMS0zMFQ\n" + "yMjowODo0MVo8L2RhdGU+CgkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuc3RhdGVGbG\n" + "FnPC9rZXk+CgkJCQk8aW50ZWdlcj4wPC9pbnRlZ2VyPgoJCQk8L2RpY3Q+CgkJPC9hcnJheT4KC\n" + "TwvZGljdD4KCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYXQuUE1PcmllbnRhdGlvbjwv\n" + "a2V5PgoJPGRpY3Q+CgkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNyZWF0b3I8L2tleT4\n" + "KCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21hbmFnZXI8L3N0cmluZz4KCQk8a2V5PmNvbS\n" + "5hcHBsZS5wcmludC50aWNrZXQuaXRlbUFycmF5PC9rZXk+CgkJPGFycmF5PgoJCQk8ZGljdD4KC\n" + "QkJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYXQuUE1PcmllbnRhdGlvbjwva2V5PgoJ\n" + "CQkJPGludGVnZXI+MTwvaW50ZWdlcj4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2tldC5\n" + "jbGllbnQ8L2tleT4KCQkJCTxzdHJpbmc+Y29tLmFwcGxlLnByaW50aW5nbWFuYWdlcjwvc3RyaW\n" + "5nPgoJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0Lm1vZERhdGU8L2tleT4KCQkJCTxkY\n" + "XRlPjIwMDctMDEtMzBUMjI6MDg6NDFaPC9kYXRlPgoJCQkJPGtleT5jb20uYXBwbGUucHJpbnQu\n" + "dGlja2V0LnN0YXRlRmxhZzwva2V5PgoJCQkJPGludGVnZXI+MDwvaW50ZWdlcj4KCQkJPC9kaWN\n" + "0PgoJCTwvYXJyYXk+Cgk8L2RpY3Q+Cgk8a2V5PmNvbS5hcHBsZS5wcmludC5QYWdlRm9ybWF0Ll\n" + "BNU2NhbGluZzwva2V5PgoJPGRpY3Q+CgkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNyZ\n" + "WF0b3I8L2tleT4KCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21hbmFnZXI8L3N0cmluZz4K\n" + "CQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuaXRlbUFycmF5PC9rZXk+CgkJPGFycmF5Pgo\n" + "JCQk8ZGljdD4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYXQuUE1TY2FsaW5nPC\n" + "9rZXk+CgkJCQk8cmVhbD4xPC9yZWFsPgoJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0L\n" + "mNsaWVudDwva2V5PgoJCQkJPHN0cmluZz5jb20uYXBwbGUucHJpbnRpbmdtYW5hZ2VyPC9zdHJp\n" + "bmc+CgkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQubW9kRGF0ZTwva2V5PgoJCQkJPGR\n" + "hdGU+MjAwNy0wMS0zMFQyMjowODo0MVo8L2RhdGU+CgkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC\n" + "50aWNrZXQuc3RhdGVGbGFnPC9rZXk+CgkJCQk8aW50ZWdlcj4wPC9pbnRlZ2VyPgoJCQk8L2RpY\n" + "3Q+CgkJPC9hcnJheT4KCTwvZGljdD4KCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYXQu\n" + "UE1WZXJ0aWNhbFJlczwva2V5PgoJPGRpY3Q+CgkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V\n" + "0LmNyZWF0b3I8L2tleT4KCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21hbmFnZXI8L3N0cm\n" + "luZz4KCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuaXRlbUFycmF5PC9rZXk+CgkJPGFyc\n" + "mF5PgoJCQk8ZGljdD4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYXQuUE1WZXJ0\n" + "aWNhbFJlczwva2V5PgoJCQkJPHJlYWw+NzI8L3JlYWw+CgkJCQk8a2V5PmNvbS5hcHBsZS5wcml\n" + "udC50aWNrZXQuY2xpZW50PC9rZXk+CgkJCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21hbm\n" + "FnZXI8L3N0cmluZz4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2tldC5tb2REYXRlPC9rZ\n" + "Xk+CgkJCQk8ZGF0ZT4yMDA3LTAxLTMwVDIyOjA4OjQxWjwvZGF0ZT4KCQkJCTxrZXk+Y29tLmFw\n" + "cGxlLnByaW50LnRpY2tldC5zdGF0ZUZsYWc8L2tleT4KCQkJCTxpbnRlZ2VyPjA8L2ludGVnZXI\n" + "+CgkJCTwvZGljdD4KCQk8L2FycmF5PgoJPC9kaWN0PgoJPGtleT5jb20uYXBwbGUucHJpbnQuUG\n" + "FnZUZvcm1hdC5QTVZlcnRpY2FsU2NhbGluZzwva2V5PgoJPGRpY3Q+CgkJPGtleT5jb20uYXBwb\n" + "GUucHJpbnQudGlja2V0LmNyZWF0b3I8L2tleT4KCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGlu\n" + "Z21hbmFnZXI8L3N0cmluZz4KCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuaXRlbUFycmF\n" + "5PC9rZXk+CgkJPGFycmF5PgoJCQk8ZGljdD4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2\n" + "VGb3JtYXQuUE1WZXJ0aWNhbFNjYWxpbmc8L2tleT4KCQkJCTxyZWFsPjE8L3JlYWw+CgkJCQk8a\n" + "2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuY2xpZW50PC9rZXk+CgkJCQk8c3RyaW5nPmNvbS5h\n" + "cHBsZS5wcmludGluZ21hbmFnZXI8L3N0cmluZz4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LnR\n" + "pY2tldC5tb2REYXRlPC9rZXk+CgkJCQk8ZGF0ZT4yMDA3LTAxLTMwVDIyOjA4OjQxWjwvZGF0ZT\n" + "4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2tldC5zdGF0ZUZsYWc8L2tleT4KCQkJCTxpb\n" + "nRlZ2VyPjA8L2ludGVnZXI+CgkJCTwvZGljdD4KCQk8L2FycmF5PgoJPC9kaWN0PgoJPGtleT5j\n" + "b20uYXBwbGUucHJpbnQuc3ViVGlja2V0LnBhcGVyX2luZm9fdGlja2V0PC9rZXk+Cgk8ZGljdD4\n" + "KCQk8a2V5PmNvbS5hcHBsZS5wcmludC5QYWdlRm9ybWF0LlBNQWRqdXN0ZWRQYWdlUmVjdDwva2\n" + "V5PgoJCTxkaWN0PgoJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuY3JlYXRvcjwva2V5P\n" + "goJCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21hbmFnZXI8L3N0cmluZz4KCQkJPGtleT5j\n" + "b20uYXBwbGUucHJpbnQudGlja2V0Lml0ZW1BcnJheTwva2V5PgoJCQk8YXJyYXk+CgkJCQk8ZGl\n" + "jdD4KCQkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC5QYWdlRm9ybWF0LlBNQWRqdXN0ZWRQYWdlUm\n" + "VjdDwva2V5PgoJCQkJCTxhcnJheT4KCQkJCQkJPHJlYWw+MC4wPC9yZWFsPgoJCQkJCQk8cmVhb\n" + "D4wLjA8L3JlYWw+CgkJCQkJCTxyZWFsPjczNDwvcmVhbD4KCQkJCQkJPHJlYWw+NTc2PC9yZWFs\n" + "PgoJCQkJCTwvYXJyYXk+CgkJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNsaWVudDw\n" + "va2V5PgoJCQkJCTxzdHJpbmc+Y29tLmFwcGxlLnByaW50aW5nbWFuYWdlcjwvc3RyaW5nPgoJCQ\n" + "kJCTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2tldC5tb2REYXRlPC9rZXk+CgkJCQkJPGRhdGU+M\n" + "jAwNy0wMS0zMFQyMjowODo0MVo8L2RhdGU+CgkJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlj\n" + "a2V0LnN0YXRlRmxhZzwva2V5PgoJCQkJCTxpbnRlZ2VyPjA8L2ludGVnZXI+CgkJCQk8L2RpY3Q\n" + "+CgkJCTwvYXJyYXk+CgkJPC9kaWN0PgoJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYX\n" + "QuUE1BZGp1c3RlZFBhcGVyUmVjdDwva2V5PgoJCTxkaWN0PgoJCQk8a2V5PmNvbS5hcHBsZS5wc\n" + "mludC50aWNrZXQuY3JlYXRvcjwva2V5PgoJCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21h\n" + "bmFnZXI8L3N0cmluZz4KCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0Lml0ZW1BcnJheTw\n" + "va2V5PgoJCQk8YXJyYXk+CgkJCQk8ZGljdD4KCQkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC5QYW\n" + "dlRm9ybWF0LlBNQWRqdXN0ZWRQYXBlclJlY3Q8L2tleT4KCQkJCQk8YXJyYXk+CgkJCQkJCTxyZ\n" + "WFsPi0xODwvcmVhbD4KCQkJCQkJPHJlYWw+LTE4PC9yZWFsPgoJCQkJCQk8cmVhbD43NzQ8L3Jl\n" + "YWw+CgkJCQkJCTxyZWFsPjU5NDwvcmVhbD4KCQkJCQk8L2FycmF5PgoJCQkJCTxrZXk+Y29tLmF\n" + "wcGxlLnByaW50LnRpY2tldC5jbGllbnQ8L2tleT4KCQkJCQk8c3RyaW5nPmNvbS5hcHBsZS5wcm\n" + "ludGluZ21hbmFnZXI8L3N0cmluZz4KCQkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQub\n" + "W9kRGF0ZTwva2V5PgoJCQkJCTxkYXRlPjIwMDctMDEtMzBUMjI6MDg6NDFaPC9kYXRlPgoJCQkJ\n" + "CTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2tldC5zdGF0ZUZsYWc8L2tleT4KCQkJCQk8aW50ZWd\n" + "lcj4wPC9pbnRlZ2VyPgoJCQkJPC9kaWN0PgoJCQk8L2FycmF5PgoJCTwvZGljdD4KCQk8a2V5Pm\n" + "NvbS5hcHBsZS5wcmludC5QYXBlckluZm8uUE1QYXBlck5hbWU8L2tleT4KCQk8ZGljdD4KCQkJP\n" + "GtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNyZWF0b3I8L2tleT4KCQkJPHN0cmluZz5jb20u\n" + "YXBwbGUucHJpbnQucG0uUG9zdFNjcmlwdDwvc3RyaW5nPgoJCQk8a2V5PmNvbS5hcHBsZS5wcml\n" + "udC50aWNrZXQuaXRlbUFycmF5PC9rZXk+CgkJCTxhcnJheT4KCQkJCTxkaWN0PgoJCQkJCTxrZX\n" + "k+Y29tLmFwcGxlLnByaW50LlBhcGVySW5mby5QTVBhcGVyTmFtZTwva2V5PgoJCQkJCTxzdHJpb\n" + "mc+bmEtbGV0dGVyPC9zdHJpbmc+CgkJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNs\n" + "aWVudDwva2V5PgoJCQkJCTxzdHJpbmc+Y29tLmFwcGxlLnByaW50LnBtLlBvc3RTY3JpcHQ8L3N\n" + "0cmluZz4KCQkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQubW9kRGF0ZTwva2V5PgoJCQ\n" + "kJCTxkYXRlPjIwMDMtMDctMDFUMTc6NDk6MzZaPC9kYXRlPgoJCQkJCTxrZXk+Y29tLmFwcGxlL\n" + "nByaW50LnRpY2tldC5zdGF0ZUZsYWc8L2tleT4KCQkJCQk8aW50ZWdlcj4xPC9pbnRlZ2VyPgoJ\n" + "CQkJPC9kaWN0PgoJCQk8L2FycmF5PgoJCTwvZGljdD4KCQk8a2V5PmNvbS5hcHBsZS5wcmludC5\n" + "QYXBlckluZm8uUE1VbmFkanVzdGVkUGFnZVJlY3Q8L2tleT4KCQk8ZGljdD4KCQkJPGtleT5jb2\n" + "0uYXBwbGUucHJpbnQudGlja2V0LmNyZWF0b3I8L2tleT4KCQkJPHN0cmluZz5jb20uYXBwbGUuc\n" + "HJpbnQucG0uUG9zdFNjcmlwdDwvc3RyaW5nPgoJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNr\n" + "ZXQuaXRlbUFycmF5PC9rZXk+CgkJCTxhcnJheT4KCQkJCTxkaWN0PgoJCQkJCTxrZXk+Y29tLmF\n" + "wcGxlLnByaW50LlBhcGVySW5mby5QTVVuYWRqdXN0ZWRQYWdlUmVjdDwva2V5PgoJCQkJCTxhcn\n" + "JheT4KCQkJCQkJPHJlYWw+MC4wPC9yZWFsPgoJCQkJCQk8cmVhbD4wLjA8L3JlYWw+CgkJCQkJC\n" + "TxyZWFsPjczNDwvcmVhbD4KCQkJCQkJPHJlYWw+NTc2PC9yZWFsPgoJCQkJCTwvYXJyYXk+CgkJ\n" + "CQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNsaWVudDwva2V5PgoJCQkJCTxzdHJpbmc\n" + "+Y29tLmFwcGxlLnByaW50aW5nbWFuYWdlcjwvc3RyaW5nPgoJCQkJCTxrZXk+Y29tLmFwcGxlLn\n" + "ByaW50LnRpY2tldC5tb2REYXRlPC9rZXk+CgkJCQkJPGRhdGU+MjAwNy0wMS0zMFQyMjowODo0M\n" + "Vo8L2RhdGU+CgkJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LnN0YXRlRmxhZzwva2V5\n" + "PgoJCQkJCTxpbnRlZ2VyPjA8L2ludGVnZXI+CgkJCQk8L2RpY3Q+CgkJCTwvYXJyYXk+CgkJPC9\n" + "kaWN0PgoJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhcGVySW5mby5QTVVuYWRqdXN0ZWRQYXBlcl\n" + "JlY3Q8L2tleT4KCQk8ZGljdD4KCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNyZWF0b\n" + "3I8L2tleT4KCQkJPHN0cmluZz5jb20uYXBwbGUucHJpbnQucG0uUG9zdFNjcmlwdDwvc3RyaW5n\n" + "PgoJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuaXRlbUFycmF5PC9rZXk+CgkJCTxhcnJ\n" + "heT4KCQkJCTxkaWN0PgoJCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhcGVySW5mby5QTVVuYW\n" + "RqdXN0ZWRQYXBlclJlY3Q8L2tleT4KCQkJCQk8YXJyYXk+CgkJCQkJCTxyZWFsPi0xODwvcmVhb\n" + "D4KCQkJCQkJPHJlYWw+LTE4PC9yZWFsPgoJCQkJCQk8cmVhbD43NzQ8L3JlYWw+CgkJCQkJCTxy\n" + "ZWFsPjU5NDwvcmVhbD4KCQkJCQk8L2FycmF5PgoJCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LnR\n" + "pY2tldC5jbGllbnQ8L2tleT4KCQkJCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21hbmFnZX\n" + "I8L3N0cmluZz4KCQkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQubW9kRGF0ZTwva2V5P\n" + "goJCQkJCTxkYXRlPjIwMDctMDEtMzBUMjI6MDg6NDFaPC9kYXRlPgoJCQkJCTxrZXk+Y29tLmFw\n" + "cGxlLnByaW50LnRpY2tldC5zdGF0ZUZsYWc8L2tleT4KCQkJCQk8aW50ZWdlcj4wPC9pbnRlZ2V\n" + "yPgoJCQkJPC9kaWN0PgoJCQk8L2FycmF5PgoJCTwvZGljdD4KCQk8a2V5PmNvbS5hcHBsZS5wcm\n" + "ludC5QYXBlckluZm8ucHBkLlBNUGFwZXJOYW1lPC9rZXk+CgkJPGRpY3Q+CgkJCTxrZXk+Y29tL\n" + "mFwcGxlLnByaW50LnRpY2tldC5jcmVhdG9yPC9rZXk+CgkJCTxzdHJpbmc+Y29tLmFwcGxlLnBy\n" + "aW50LnBtLlBvc3RTY3JpcHQ8L3N0cmluZz4KCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V\n" + "0Lml0ZW1BcnJheTwva2V5PgoJCQk8YXJyYXk+CgkJCQk8ZGljdD4KCQkJCQk8a2V5PmNvbS5hcH\n" + "BsZS5wcmludC5QYXBlckluZm8ucHBkLlBNUGFwZXJOYW1lPC9rZXk+CgkJCQkJPHN0cmluZz5VU\n" + "yBMZXR0ZXI8L3N0cmluZz4KCQkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuY2xpZW50\n" + "PC9rZXk+CgkJCQkJPHN0cmluZz5jb20uYXBwbGUucHJpbnQucG0uUG9zdFNjcmlwdDwvc3RyaW5\n" + "nPgoJCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2tldC5tb2REYXRlPC9rZXk+CgkJCQkJPG\n" + "RhdGU+MjAwMy0wNy0wMVQxNzo0OTozNlo8L2RhdGU+CgkJCQkJPGtleT5jb20uYXBwbGUucHJpb\n" + "nQudGlja2V0LnN0YXRlRmxhZzwva2V5PgoJCQkJCTxpbnRlZ2VyPjE8L2ludGVnZXI+CgkJCQk8\n" + "L2RpY3Q+CgkJCTwvYXJyYXk+CgkJPC9kaWN0PgoJCTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2t\n" + "ldC5BUElWZXJzaW9uPC9rZXk+CgkJPHN0cmluZz4wMC4yMDwvc3RyaW5nPgoJCTxrZXk+Y29tLm\n" + "FwcGxlLnByaW50LnRpY2tldC5wcml2YXRlTG9jazwva2V5PgoJCTxmYWxzZS8+CgkJPGtleT5jb\n" + "20uYXBwbGUucHJpbnQudGlja2V0LnR5cGU8L2tleT4KCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmlu\n" + "dC5QYXBlckluZm9UaWNrZXQ8L3N0cmluZz4KCTwvZGljdD4KCTxrZXk+Y29tLmFwcGxlLnByaW5\n" + "0LnRpY2tldC5BUElWZXJzaW9uPC9rZXk+Cgk8c3RyaW5nPjAwLjIwPC9zdHJpbmc+Cgk8a2V5Pm\n" + "NvbS5hcHBsZS5wcmludC50aWNrZXQucHJpdmF0ZUxvY2s8L2tleT4KCTxmYWxzZS8+Cgk8a2V5P\n" + "mNvbS5hcHBsZS5wcmludC50aWNrZXQudHlwZTwva2V5PgoJPHN0cmluZz5jb20uYXBwbGUucHJp\n" + "bnQuUGFnZUZvcm1hdFRpY2tldDwvc3RyaW5nPgo8L2RpY3Q+CjwvcGxpc3Q+CjhCSU0D6QAAAAA\n" + "AeAADAAAASABIAAAAAALeAkD/7v/uAwYCUgNnBSgD/AACAAAASABIAAAAAALYAigAAQAAAGQAAA\n" + "ABAAMDAwAAAAF//wABAAEAAAAAAAAAAAAAAABoCAAZAZAAAAAAACAAAAAAAAAAAAAAAAAAAAAAA\n" + "AAAAAAAAAAAADhCSU0D7QAAAAAAEABIAAAAAQABAEgAAAABAAE4QklNBCYAAAAAAA4AAAAAAAAA\n" + "AAAAP4AAADhCSU0EDQAAAAAABAAAAB44QklNBBkAAAAAAAQAAAAeOEJJTQPzAAAAAAAJAAAAAAA\n" + "AAAABADhCSU0ECgAAAAAAAQAAOEJJTScQAAAAAAAKAAEAAAAAAAAAAThCSU0D9QAAAAAASAAvZm\n" + "YAAQBsZmYABgAAAAAAAQAvZmYAAQChmZoABgAAAAAAAQAyAAAAAQBaAAAABgAAAAAAAQA1AAAAA\n" + "QAtAAAABgAAAAAAAThCSU0D+AAAAAAAcAAA/////////////////////////////wPoAAAAAP//\n" + "//////////////////////////8D6AAAAAD/////////////////////////////A+gAAAAA///\n" + "//////////////////////////wPoAAA4QklNBAgAAAAAABAAAAABAAACQAAAAkAAAAAAOEJJTQ\n" + "QeAAAAAAAEAAAAADhCSU0EGgAAAAADRQAAAAYAAAAAAAAAAAAAAGQAAABkAAAACABEAFMAQwAwA\n" + "DIAMwAyADUAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAGQAAABkAAAAAAAAAAAA\n" + "AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAEAAAAAAABudWxsAAAAAgAAAAZib3VuZHN\n" + "PYmpjAAAAAQAAAAAAAFJjdDEAAAAEAAAAAFRvcCBsb25nAAAAAAAAAABMZWZ0bG9uZwAAAAAAAA\n" + "AAQnRvbWxvbmcAAABkAAAAAFJnaHRsb25nAAAAZAAAAAZzbGljZXNWbExzAAAAAU9iamMAAAABA\n" + "AAAAAAFc2xpY2UAAAASAAAAB3NsaWNlSURsb25nAAAAAAAAAAdncm91cElEbG9uZwAAAAAAAAAG\n" + "b3JpZ2luZW51bQAAAAxFU2xpY2VPcmlnaW4AAAANYXV0b0dlbmVyYXRlZAAAAABUeXBlZW51bQA\n" + "AAApFU2xpY2VUeXBlAAAAAEltZyAAAAAGYm91bmRzT2JqYwAAAAEAAAAAAABSY3QxAAAABAAAAA\n" + "BUb3AgbG9uZwAAAAAAAAAATGVmdGxvbmcAAAAAAAAAAEJ0b21sb25nAAAAZAAAAABSZ2h0bG9uZ\n" + "wAAAGQAAAADdXJsVEVYVAAAAAEAAAAAAABudWxsVEVYVAAAAAEAAAAAAABNc2dlVEVYVAAAAAEA\n" + "AAAAAAZhbHRUYWdURVhUAAAAAQAAAAAADmNlbGxUZXh0SXNIVE1MYm9vbAEAAAAIY2VsbFRleHR\n" + "URVhUAAAAAQAAAAAACWhvcnpBbGlnbmVudW0AAAAPRVNsaWNlSG9yekFsaWduAAAAB2RlZmF1bH\n" + "QAAAAJdmVydEFsaWduZW51bQAAAA9FU2xpY2VWZXJ0QWxpZ24AAAAHZGVmYXVsdAAAAAtiZ0Nvb\n" + "G9yVHlwZWVudW0AAAARRVNsaWNlQkdDb2xvclR5cGUAAAAATm9uZQAAAAl0b3BPdXRzZXRsb25n\n" + "AAAAAAAAAApsZWZ0T3V0c2V0bG9uZwAAAAAAAAAMYm90dG9tT3V0c2V0bG9uZwAAAAAAAAALcml\n" + "naHRPdXRzZXRsb25nAAAAAAA4QklNBBEAAAAAAAEBADhCSU0EFAAAAAAABAAAAAE4QklNBAwAAA\n" + "AACfkAAAABAAAAZAAAAGQAAAEsAAB1MAAACd0AGAAB/9j/4AAQSkZJRgABAgEASABIAAD/7QAMQ\n" + "WRvYmVfQ00AAv/uAA5BZG9iZQBkgAAAAAH/2wCEAAwICAgJCAwJCQwRCwoLERUPDAwPFRgTExUT\n" + "ExgRDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBDQsLDQ4NEA4OEBQODg4UFA4\n" + "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\n" + "hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0n77u/JyBpZD0nVzVNME1wQ2VoaUh6cmVTek5UY3prY\n" + "zlkJz8+Cjw/YWRvYmUteGFwLWZpbHRlcnMgZXNjPSJDUiI/Pgo8eDp4YXBtZXRhIHhtbG5zOng9\n" + "J2Fkb2JlOm5zOm1ldGEvJyB4OnhhcHRrPSdYTVAgdG9vbGtpdCAyLjguMi0zMywgZnJhbWV3b3J\n" + "rIDEuNSc+CjxyZGY6UkRGIHhtbG5zOnJkZj0naHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi\n" + "1yZGYtc3ludGF4LW5zIycgeG1sbnM6aVg9J2h0dHA6Ly9ucy5hZG9iZS5jb20vaVgvMS4wLyc+C\n" + "gogPHJkZjpEZXNjcmlwdGlvbiBhYm91dD0ndXVpZDoyMmQwMmIwYS1iMjQ5LTExZGItOGFmOC05\n" + "MWQ1NDAzZjkyZjknCiAgeG1sbnM6cGRmPSdodHRwOi8vbnMuYWRvYmUuY29tL3BkZi8xLjMvJz4\n" + "KICA8IS0tIHBkZjpTdWJqZWN0IGlzIGFsaWFzZWQgLS0+CiA8L3JkZjpEZXNjcmlwdGlvbj4KCi\n" + "A8cmRmOkRlc2NyaXB0aW9uIGFib3V0PSd1dWlkOjIyZDAyYjBhLWIyNDktMTFkYi04YWY4LTkxZ\n" + "DU0MDNmOTJmOScKICB4bWxuczpwaG90b3Nob3A9J2h0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9z\n" + "aG9wLzEuMC8nPgogIDwhLS0gcGhvdG9zaG9wOkNhcHRpb24gaXMgYWxpYXNlZCAtLT4KIDwvcmR\n" + "mOkRlc2NyaXB0aW9uPgoKIDxyZGY6RGVzY3JpcHRpb24gYWJvdXQ9J3V1aWQ6MjJkMDJiMGEtYj\n" + "I0OS0xMWRiLThhZjgtOTFkNTQwM2Y5MmY5JwogIHhtbG5zOnhhcD0naHR0cDovL25zLmFkb2JlL\n" + "mNvbS94YXAvMS4wLyc+CiAgPCEtLSB4YXA6RGVzY3JpcHRpb24gaXMgYWxpYXNlZCAtLT4KIDwv\n" + "cmRmOkRlc2NyaXB0aW9uPgoKIDxyZGY6RGVzY3JpcHRpb24gYWJvdXQ9J3V1aWQ6MjJkMDJiMGE\n" + "tYjI0OS0xMWRiLThhZjgtOTFkNTQwM2Y5MmY5JwogIHhtbG5zOnhhcE1NPSdodHRwOi8vbnMuYW\n" + "RvYmUuY29tL3hhcC8xLjAvbW0vJz4KICA8eGFwTU06RG9jdW1lbnRJRD5hZG9iZTpkb2NpZDpwa\n" + "G90b3Nob3A6MjJkMDJiMDYtYjI0OS0xMWRiLThhZjgtOTFkNTQwM2Y5MmY5PC94YXBNTTpEb2N1\n" + "bWVudElEPgogPC9yZGY6RGVzY3JpcHRpb24+CgogPHJkZjpEZXNjcmlwdGlvbiBhYm91dD0ndXV\n" + "pZDoyMmQwMmIwYS1iMjQ5LTExZGItOGFmOC05MWQ1NDAzZjkyZjknCiAgeG1sbnM6ZGM9J2h0dH\n" + "A6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvJz4KICA8ZGM6ZGVzY3JpcHRpb24+CiAgIDxyZ\n" + "GY6QWx0PgogICAgPHJkZjpsaSB4bWw6bGFuZz0neC1kZWZhdWx0Jz4gICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgIDwvcmRmOkFsdD4KICA8L2RjOmRlc2NyaXB0aW9\n" + "uPgogPC9yZGY6RGVzY3JpcHRpb24+Cgo8L3JkZjpSREY+CjwveDp4YXBtZXRhPgogICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIA\n" + "ogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n" + "ICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n" + "AgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n" + "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAg\n" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n" + "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgIC\n" + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0ndyc/P\n" + "v/uAA5BZG9iZQBkQAAAAAH/2wCEAAQDAwMDAwQDAwQGBAMEBgcFBAQFBwgGBgcGBggKCAkJCQkI\n" + "CgoMDAwMDAoMDAwMDAwMDAwMDAwMDAwMDAwMDAwBBAUFCAcIDwoKDxQODg4UFA4ODg4UEQwMDAw\n" + "MEREMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDP/AABEIAGQAZAMBEQACEQEDEQ\n" + "H/3QAEAA3/xAGiAAAABwEBAQEBAAAAAAAAAAAEBQMCBgEABwgJCgsBAAICAwEBAQEBAAAAAAAAA\n" + "AEAAgMEBQYHCAkKCxAAAgEDAwIEAgYHAwQCBgJzAQIDEQQABSESMUFRBhNhInGBFDKRoQcVsUIj\n" + "wVLR4TMWYvAkcoLxJUM0U5KismNzwjVEJ5OjszYXVGR0w9LiCCaDCQoYGYSURUaktFbTVSga8uP\n" + "zxNTk9GV1hZWltcXV5fVmdoaWprbG1ub2N0dXZ3eHl6e3x9fn9zhIWGh4iJiouMjY6PgpOUlZaX\n" + "mJmam5ydnp+So6SlpqeoqaqrrK2ur6EQACAgECAwUFBAUGBAgDA20BAAIRAwQhEjFBBVETYSIGc\n" + "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\n" + "fngFN5ulv+l/pL/Lp/scerHo//2Q==\n"; + +static std::string gCommandLine; + +TEST(Base64, LargeSample) { + LOG(LS_VERBOSE) << "Testing specific base64 file"; + + char unescaped[64 * 1024]; + + // unescape that massive blob above + size_t size = Base64Unescape(SpecificTest, + sizeof(SpecificTest), + unescaped, + sizeof(unescaped)); + + EXPECT_EQ(size, sizeof(testbase64)); + EXPECT_EQ(0, memcmp(testbase64, unescaped, sizeof(testbase64))); +} + +bool DecodeTest(const char* encoded, size_t expect_unparsed, + const char* decoded, Base64::DecodeFlags flags) +{ + std::string result; + size_t consumed = 0, encoded_len = strlen(encoded); + bool success = Base64::DecodeFromArray(encoded, encoded_len, flags, + &result, &consumed); + size_t unparsed = encoded_len - consumed; + EXPECT_EQ(expect_unparsed, unparsed) << "\"" << encoded + << "\" -> \"" << decoded + << "\""; + EXPECT_STREQ(decoded, result.c_str()); + return success; +} + +#define Flags(x,y,z) \ + Base64::DO_PARSE_##x | Base64::DO_PAD_##y | Base64::DO_TERM_##z + +TEST(Base64, DecodeParseOptions) { + // Trailing whitespace + EXPECT_TRUE (DecodeTest("YWJjZA== ", 1, "abcd", Flags(STRICT, YES, CHAR))); + EXPECT_TRUE (DecodeTest("YWJjZA== ", 0, "abcd", Flags(WHITE, YES, CHAR))); + EXPECT_TRUE (DecodeTest("YWJjZA== ", 0, "abcd", Flags(ANY, YES, CHAR))); + + // Embedded whitespace + EXPECT_FALSE(DecodeTest("YWJjZA= =", 3, "abcd", Flags(STRICT, YES, CHAR))); + EXPECT_TRUE (DecodeTest("YWJjZA= =", 0, "abcd", Flags(WHITE, YES, CHAR))); + EXPECT_TRUE (DecodeTest("YWJjZA= =", 0, "abcd", Flags(ANY, YES, CHAR))); + + // Embedded non-base64 characters + EXPECT_FALSE(DecodeTest("YWJjZA=*=", 3, "abcd", Flags(STRICT, YES, CHAR))); + EXPECT_FALSE(DecodeTest("YWJjZA=*=", 3, "abcd", Flags(WHITE, YES, CHAR))); + EXPECT_TRUE (DecodeTest("YWJjZA=*=", 0, "abcd", Flags(ANY, YES, CHAR))); + + // Unexpected padding characters + EXPECT_FALSE(DecodeTest("YW=JjZA==", 7, "a", Flags(STRICT, YES, CHAR))); + EXPECT_FALSE(DecodeTest("YW=JjZA==", 7, "a", Flags(WHITE, YES, CHAR))); + EXPECT_TRUE (DecodeTest("YW=JjZA==", 0, "abcd", Flags(ANY, YES, CHAR))); +} + +TEST(Base64, DecodePadOptions) { + // Padding + EXPECT_TRUE (DecodeTest("YWJjZA==", 0, "abcd", Flags(STRICT, YES, CHAR))); + EXPECT_TRUE (DecodeTest("YWJjZA==", 0, "abcd", Flags(STRICT, ANY, CHAR))); + EXPECT_TRUE (DecodeTest("YWJjZA==", 2, "abcd", Flags(STRICT, NO, CHAR))); + + // Incomplete padding + EXPECT_FALSE(DecodeTest("YWJjZA=", 1, "abcd", Flags(STRICT, YES, CHAR))); + EXPECT_TRUE (DecodeTest("YWJjZA=", 1, "abcd", Flags(STRICT, ANY, CHAR))); + EXPECT_TRUE (DecodeTest("YWJjZA=", 1, "abcd", Flags(STRICT, NO, CHAR))); + + // No padding + EXPECT_FALSE(DecodeTest("YWJjZA", 0, "abcd", Flags(STRICT, YES, CHAR))); + EXPECT_TRUE (DecodeTest("YWJjZA", 0, "abcd", Flags(STRICT, ANY, CHAR))); + EXPECT_TRUE (DecodeTest("YWJjZA", 0, "abcd", Flags(STRICT, NO, CHAR))); +} + +TEST(Base64, DecodeTerminateOptions) { + // Complete quantum + EXPECT_TRUE (DecodeTest("YWJj", 0, "abc", Flags(STRICT, NO, BUFFER))); + EXPECT_TRUE (DecodeTest("YWJj", 0, "abc", Flags(STRICT, NO, CHAR))); + EXPECT_TRUE (DecodeTest("YWJj", 0, "abc", Flags(STRICT, NO, ANY))); + + // Complete quantum with trailing data + EXPECT_FALSE(DecodeTest("YWJj*", 1, "abc", Flags(STRICT, NO, BUFFER))); + EXPECT_TRUE (DecodeTest("YWJj*", 1, "abc", Flags(STRICT, NO, CHAR))); + EXPECT_TRUE (DecodeTest("YWJj*", 1, "abc", Flags(STRICT, NO, ANY))); + + // Incomplete quantum + EXPECT_FALSE(DecodeTest("YWJ", 0, "ab", Flags(STRICT, NO, BUFFER))); + EXPECT_FALSE(DecodeTest("YWJ", 0, "ab", Flags(STRICT, NO, CHAR))); + EXPECT_TRUE (DecodeTest("YWJ", 0, "ab", Flags(STRICT, NO, ANY))); +} + +TEST(Base64, GetNextBase64Char) { + // The table looks like this: + // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + char next_char; + EXPECT_TRUE(Base64::GetNextBase64Char('A', &next_char)); + EXPECT_EQ('B', next_char); + EXPECT_TRUE(Base64::GetNextBase64Char('Z', &next_char)); + EXPECT_EQ('a', next_char); + EXPECT_TRUE(Base64::GetNextBase64Char('/', &next_char)); + EXPECT_EQ('A', next_char); + EXPECT_FALSE(Base64::GetNextBase64Char('&', &next_char)); + EXPECT_FALSE(Base64::GetNextBase64Char('Z', NULL)); +} diff --git a/talk/base/basicdefs.h b/talk/base/basicdefs.h new file mode 100644 index 000000000..7829d452c --- /dev/null +++ b/talk/base/basicdefs.h @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_BASE_BASICDEFS_H_ +#define TALK_BASE_BASICDEFS_H_ + +#if HAVE_CONFIG_H +#include "config.h" // NOLINT +#endif + +#define ARRAY_SIZE(x) (static_cast(sizeof(x) / sizeof(x[0]))) + +#endif // TALK_BASE_BASICDEFS_H_ diff --git a/talk/base/basictypes.h b/talk/base/basictypes.h new file mode 100644 index 000000000..f7f5b6617 --- /dev/null +++ b/talk/base/basictypes.h @@ -0,0 +1,171 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_BASE_BASICTYPES_H_ +#define TALK_BASE_BASICTYPES_H_ + +#include // for NULL, size_t + +#if !(defined(_MSC_VER) && (_MSC_VER < 1600)) +#include // for uintptr_t +#endif + +#ifdef HAVE_CONFIG_H +#include "config.h" // NOLINT +#endif + +#include "talk/base/constructormagic.h" + +#if !defined(INT_TYPES_DEFINED) +#define INT_TYPES_DEFINED +#ifdef COMPILER_MSVC +typedef unsigned __int64 uint64; +typedef __int64 int64; +#ifndef INT64_C +#define INT64_C(x) x ## I64 +#endif +#ifndef UINT64_C +#define UINT64_C(x) x ## UI64 +#endif +#define INT64_F "I64" +#else // COMPILER_MSVC +// On Mac OS X, cssmconfig.h defines uint64 as uint64_t +// TODO(fbarchard): Use long long for compatibility with chromium on BSD/OSX. +#if defined(OSX) +typedef uint64_t uint64; +typedef int64_t int64; +#ifndef INT64_C +#define INT64_C(x) x ## LL +#endif +#ifndef UINT64_C +#define UINT64_C(x) x ## ULL +#endif +#define INT64_F "l" +#elif defined(__LP64__) +typedef unsigned long uint64; // NOLINT +typedef long int64; // NOLINT +#ifndef INT64_C +#define INT64_C(x) x ## L +#endif +#ifndef UINT64_C +#define UINT64_C(x) x ## UL +#endif +#define INT64_F "l" +#else // __LP64__ +typedef unsigned long long uint64; // NOLINT +typedef long long int64; // NOLINT +#ifndef INT64_C +#define INT64_C(x) x ## LL +#endif +#ifndef UINT64_C +#define UINT64_C(x) x ## ULL +#endif +#define INT64_F "ll" +#endif // __LP64__ +#endif // COMPILER_MSVC +typedef unsigned int uint32; +typedef int int32; +typedef unsigned short uint16; // NOLINT +typedef short int16; // NOLINT +typedef unsigned char uint8; +typedef signed char int8; +#endif // INT_TYPES_DEFINED + +// Detect compiler is for x86 or x64. +#if defined(__x86_64__) || defined(_M_X64) || \ + defined(__i386__) || defined(_M_IX86) +#define CPU_X86 1 +#endif +// Detect compiler is for arm. +#if defined(__arm__) || defined(_M_ARM) +#define CPU_ARM 1 +#endif +#if defined(CPU_X86) && defined(CPU_ARM) +#error CPU_X86 and CPU_ARM both defined. +#endif +#if !defined(ARCH_CPU_BIG_ENDIAN) && !defined(ARCH_CPU_LITTLE_ENDIAN) +// x86, arm or GCC provided __BYTE_ORDER__ macros +#if CPU_X86 || CPU_ARM || \ + (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) +#define ARCH_CPU_LITTLE_ENDIAN +#elif defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#define ARCH_CPU_BIG_ENDIAN +#else +#error ARCH_CPU_BIG_ENDIAN or ARCH_CPU_LITTLE_ENDIAN should be defined. +#endif +#endif +#if defined(ARCH_CPU_BIG_ENDIAN) && defined(ARCH_CPU_LITTLE_ENDIAN) +#error ARCH_CPU_BIG_ENDIAN and ARCH_CPU_LITTLE_ENDIAN both defined. +#endif + +#ifdef WIN32 +typedef int socklen_t; +#endif + +// The following only works for C++ +#ifdef __cplusplus +namespace talk_base { + template inline T _min(T a, T b) { return (a > b) ? b : a; } + template inline T _max(T a, T b) { return (a < b) ? b : a; } + + // For wait functions that take a number of milliseconds, kForever indicates + // unlimited time. + const int kForever = -1; +} + +#define ALIGNP(p, t) \ + (reinterpret_cast(((reinterpret_cast(p) + \ + ((t) - 1)) & ~((t) - 1)))) +#define IS_ALIGNED(p, a) (!((uintptr_t)(p) & ((a) - 1))) + +// Note: UNUSED is also defined in common.h +#ifndef UNUSED +#define UNUSED(x) Unused(static_cast(&x)) +#define UNUSED2(x, y) Unused(static_cast(&x)); \ + Unused(static_cast(&y)) +#define UNUSED3(x, y, z) Unused(static_cast(&x)); \ + Unused(static_cast(&y)); \ + Unused(static_cast(&z)) +#define UNUSED4(x, y, z, a) Unused(static_cast(&x)); \ + Unused(static_cast(&y)); \ + Unused(static_cast(&z)); \ + Unused(static_cast(&a)) +#define UNUSED5(x, y, z, a, b) Unused(static_cast(&x)); \ + Unused(static_cast(&y)); \ + Unused(static_cast(&z)); \ + Unused(static_cast(&a)); \ + Unused(static_cast(&b)) +inline void Unused(const void*) {} +#endif // UNUSED + +// Use these to declare and define a static local variable (static T;) so that +// it is leaked so that its destructors are not called at exit. +#define LIBJINGLE_DEFINE_STATIC_LOCAL(type, name, arguments) \ + static type& name = *new type arguments + +#endif // __cplusplus +#endif // TALK_BASE_BASICTYPES_H_ diff --git a/talk/base/basictypes_unittest.cc b/talk/base/basictypes_unittest.cc new file mode 100644 index 000000000..caf1115a4 --- /dev/null +++ b/talk/base/basictypes_unittest.cc @@ -0,0 +1,92 @@ +/* + * libjingle + * Copyright 2012 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/base/basictypes.h" + +#include "talk/base/gunit.h" + +namespace talk_base { + +TEST(BasicTypesTest, Endian) { + uint16 v16 = 0x1234u; + uint8 first_byte = *reinterpret_cast(&v16); +#if defined(ARCH_CPU_LITTLE_ENDIAN) + EXPECT_EQ(0x34u, first_byte); +#elif defined(ARCH_CPU_BIG_ENDIAN) + EXPECT_EQ(0x12u, first_byte); +#endif +} + +TEST(BasicTypesTest, SizeOfTypes) { + int8 i8 = -1; + uint8 u8 = 1u; + int16 i16 = -1; + uint16 u16 = 1u; + int32 i32 = -1; + uint32 u32 = 1u; + int64 i64 = -1; + uint64 u64 = 1u; + EXPECT_EQ(1u, sizeof(i8)); + EXPECT_EQ(1u, sizeof(u8)); + EXPECT_EQ(2u, sizeof(i16)); + EXPECT_EQ(2u, sizeof(u16)); + EXPECT_EQ(4u, sizeof(i32)); + EXPECT_EQ(4u, sizeof(u32)); + EXPECT_EQ(8u, sizeof(i64)); + EXPECT_EQ(8u, sizeof(u64)); + EXPECT_GT(0, i8); + EXPECT_LT(0u, u8); + EXPECT_GT(0, i16); + EXPECT_LT(0u, u16); + EXPECT_GT(0, i32); + EXPECT_LT(0u, u32); + EXPECT_GT(0, i64); + EXPECT_LT(0u, u64); +} + +TEST(BasicTypesTest, SizeOfConstants) { + EXPECT_EQ(8u, sizeof(INT64_C(0))); + EXPECT_EQ(8u, sizeof(UINT64_C(0))); + EXPECT_EQ(8u, sizeof(INT64_C(0x1234567887654321))); + EXPECT_EQ(8u, sizeof(UINT64_C(0x8765432112345678))); +} + +// Test CPU_ macros +#if !defined(CPU_ARM) && defined(__arm__) +#error expected CPU_ARM to be defined. +#endif +#if !defined(CPU_X86) && (defined(WIN32) || defined(OSX)) +#error expected CPU_X86 to be defined. +#endif +#if !defined(ARCH_CPU_LITTLE_ENDIAN) && \ + (defined(WIN32) || defined(OSX) || defined(CPU_X86)) +#error expected ARCH_CPU_LITTLE_ENDIAN to be defined. +#endif + +// TODO(fbarchard): Test all macros in basictypes.h + +} // namespace talk_base diff --git a/talk/base/bind.h b/talk/base/bind.h new file mode 100644 index 000000000..622cc679d --- /dev/null +++ b/talk/base/bind.h @@ -0,0 +1,397 @@ +// This file was GENERATED by command: +// pump.py bind.h.pump +// DO NOT EDIT BY HAND!!! + +/* + * libjingle + * Copyright 2012 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. + */ + +// To generate bind.h from bind.h.pump, execute: +// /home/build/google3/third_party/gtest/scripts/pump.py bind.h.pump + +// Bind() is an overloaded function that converts method calls into function +// objects (aka functors). It captures any arguments to the method by value +// when Bind is called, producing a stateful, nullary function object. Care +// should be taken about the lifetime of objects captured by Bind(); the +// returned functor knows nothing about the lifetime of the method's object or +// any arguments passed by pointer, and calling the functor with a destroyed +// object will surely do bad things. +// +// Example usage: +// struct Foo { +// int Test1() { return 42; } +// int Test2() const { return 52; } +// int Test3(int x) { return x*x; } +// float Test4(int x, float y) { return x + y; } +// }; +// +// int main() { +// Foo foo; +// cout << talk_base::Bind(&Foo::Test1, &foo)() << endl; +// cout << talk_base::Bind(&Foo::Test2, &foo)() << endl; +// cout << talk_base::Bind(&Foo::Test3, &foo, 3)() << endl; +// cout << talk_base::Bind(&Foo::Test4, &foo, 7, 8.5f)() << endl; +// } + +#ifndef TALK_BASE_BIND_H_ +#define TALK_BASE_BIND_H_ + +#define NONAME + +namespace talk_base { +namespace detail { +// This is needed because the template parameters in Bind can't be resolved +// if they're used both as parameters of the function pointer type and as +// parameters to Bind itself: the function pointer parameters are exact +// matches to the function prototype, but the parameters to bind have +// references stripped. This trick allows the compiler to dictate the Bind +// parameter types rather than deduce them. +template struct identity { typedef T type; }; +} // namespace detail + +template +class MethodFunctor0 { + public: + MethodFunctor0(MethodT method, ObjectT* object) + : method_(method), object_(object) {} + R operator()() const { + return (object_->*method_)(); } + private: + MethodT method_; + ObjectT* object_; +}; + +#define FP_T(x) R (ObjectT::*x)() + +template +MethodFunctor0 +Bind(FP_T(method), ObjectT* object) { + return MethodFunctor0( + method, object); +} + +#undef FP_T +#define FP_T(x) R (ObjectT::*x)() const + +template +MethodFunctor0 +Bind(FP_T(method), const ObjectT* object) { + return MethodFunctor0( + method, object); +} + +#undef FP_T + +template +class MethodFunctor1 { + public: + MethodFunctor1(MethodT method, ObjectT* object, + P1 p1) + : method_(method), object_(object), + p1_(p1) {} + R operator()() const { + return (object_->*method_)(p1_); } + private: + MethodT method_; + ObjectT* object_; + P1 p1_; +}; + +#define FP_T(x) R (ObjectT::*x)(P1) + +template +MethodFunctor1 +Bind(FP_T(method), ObjectT* object, + typename detail::identity::type p1) { + return MethodFunctor1( + method, object, p1); +} + +#undef FP_T +#define FP_T(x) R (ObjectT::*x)(P1) const + +template +MethodFunctor1 +Bind(FP_T(method), const ObjectT* object, + typename detail::identity::type p1) { + return MethodFunctor1( + method, object, p1); +} + +#undef FP_T + +template +class MethodFunctor2 { + public: + MethodFunctor2(MethodT method, ObjectT* object, + P1 p1, + P2 p2) + : method_(method), object_(object), + p1_(p1), + p2_(p2) {} + R operator()() const { + return (object_->*method_)(p1_, p2_); } + private: + MethodT method_; + ObjectT* object_; + P1 p1_; + P2 p2_; +}; + +#define FP_T(x) R (ObjectT::*x)(P1, P2) + +template +MethodFunctor2 +Bind(FP_T(method), ObjectT* object, + typename detail::identity::type p1, + typename detail::identity::type p2) { + return MethodFunctor2( + method, object, p1, p2); +} + +#undef FP_T +#define FP_T(x) R (ObjectT::*x)(P1, P2) const + +template +MethodFunctor2 +Bind(FP_T(method), const ObjectT* object, + typename detail::identity::type p1, + typename detail::identity::type p2) { + return MethodFunctor2( + method, object, p1, p2); +} + +#undef FP_T + +template +class MethodFunctor3 { + public: + MethodFunctor3(MethodT method, ObjectT* object, + P1 p1, + P2 p2, + P3 p3) + : method_(method), object_(object), + p1_(p1), + p2_(p2), + p3_(p3) {} + R operator()() const { + return (object_->*method_)(p1_, p2_, p3_); } + private: + MethodT method_; + ObjectT* object_; + P1 p1_; + P2 p2_; + P3 p3_; +}; + +#define FP_T(x) R (ObjectT::*x)(P1, P2, P3) + +template +MethodFunctor3 +Bind(FP_T(method), ObjectT* object, + typename detail::identity::type p1, + typename detail::identity::type p2, + typename detail::identity::type p3) { + return MethodFunctor3( + method, object, p1, p2, p3); +} + +#undef FP_T +#define FP_T(x) R (ObjectT::*x)(P1, P2, P3) const + +template +MethodFunctor3 +Bind(FP_T(method), const ObjectT* object, + typename detail::identity::type p1, + typename detail::identity::type p2, + typename detail::identity::type p3) { + return MethodFunctor3( + method, object, p1, p2, p3); +} + +#undef FP_T + +template +class MethodFunctor4 { + public: + MethodFunctor4(MethodT method, ObjectT* object, + P1 p1, + P2 p2, + P3 p3, + P4 p4) + : method_(method), object_(object), + p1_(p1), + p2_(p2), + p3_(p3), + p4_(p4) {} + R operator()() const { + return (object_->*method_)(p1_, p2_, p3_, p4_); } + private: + MethodT method_; + ObjectT* object_; + P1 p1_; + P2 p2_; + P3 p3_; + P4 p4_; +}; + +#define FP_T(x) R (ObjectT::*x)(P1, P2, P3, P4) + +template +MethodFunctor4 +Bind(FP_T(method), ObjectT* object, + typename detail::identity::type p1, + typename detail::identity::type p2, + typename detail::identity::type p3, + typename detail::identity::type p4) { + return MethodFunctor4( + method, object, p1, p2, p3, p4); +} + +#undef FP_T +#define FP_T(x) R (ObjectT::*x)(P1, P2, P3, P4) const + +template +MethodFunctor4 +Bind(FP_T(method), const ObjectT* object, + typename detail::identity::type p1, + typename detail::identity::type p2, + typename detail::identity::type p3, + typename detail::identity::type p4) { + return MethodFunctor4( + method, object, p1, p2, p3, p4); +} + +#undef FP_T + +template +class MethodFunctor5 { + public: + MethodFunctor5(MethodT method, ObjectT* object, + P1 p1, + P2 p2, + P3 p3, + P4 p4, + P5 p5) + : method_(method), object_(object), + p1_(p1), + p2_(p2), + p3_(p3), + p4_(p4), + p5_(p5) {} + R operator()() const { + return (object_->*method_)(p1_, p2_, p3_, p4_, p5_); } + private: + MethodT method_; + ObjectT* object_; + P1 p1_; + P2 p2_; + P3 p3_; + P4 p4_; + P5 p5_; +}; + +#define FP_T(x) R (ObjectT::*x)(P1, P2, P3, P4, P5) + +template +MethodFunctor5 +Bind(FP_T(method), ObjectT* object, + typename detail::identity::type p1, + typename detail::identity::type p2, + typename detail::identity::type p3, + typename detail::identity::type p4, + typename detail::identity::type p5) { + return MethodFunctor5( + method, object, p1, p2, p3, p4, p5); +} + +#undef FP_T +#define FP_T(x) R (ObjectT::*x)(P1, P2, P3, P4, P5) const + +template +MethodFunctor5 +Bind(FP_T(method), const ObjectT* object, + typename detail::identity::type p1, + typename detail::identity::type p2, + typename detail::identity::type p3, + typename detail::identity::type p4, + typename detail::identity::type p5) { + return MethodFunctor5( + method, object, p1, p2, p3, p4, p5); +} + +#undef FP_T + +} // namespace talk_base + +#undef NONAME + +#endif // TALK_BASE_BIND_H_ diff --git a/talk/base/bind.h.pump b/talk/base/bind.h.pump new file mode 100644 index 000000000..7f4c39e63 --- /dev/null +++ b/talk/base/bind.h.pump @@ -0,0 +1,125 @@ +/* + * libjingle + * Copyright 2012 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. + */ + +// To generate bind.h from bind.h.pump, execute: +// /home/build/google3/third_party/gtest/scripts/pump.py bind.h.pump + +// Bind() is an overloaded function that converts method calls into function +// objects (aka functors). It captures any arguments to the method by value +// when Bind is called, producing a stateful, nullary function object. Care +// should be taken about the lifetime of objects captured by Bind(); the +// returned functor knows nothing about the lifetime of the method's object or +// any arguments passed by pointer, and calling the functor with a destroyed +// object will surely do bad things. +// +// Example usage: +// struct Foo { +// int Test1() { return 42; } +// int Test2() const { return 52; } +// int Test3(int x) { return x*x; } +// float Test4(int x, float y) { return x + y; } +// }; +// +// int main() { +// Foo foo; +// cout << talk_base::Bind(&Foo::Test1, &foo)() << endl; +// cout << talk_base::Bind(&Foo::Test2, &foo)() << endl; +// cout << talk_base::Bind(&Foo::Test3, &foo, 3)() << endl; +// cout << talk_base::Bind(&Foo::Test4, &foo, 7, 8.5f)() << endl; +// } + +#ifndef TALK_BASE_BIND_H_ +#define TALK_BASE_BIND_H_ + +#define NONAME + +namespace talk_base { +namespace detail { +// This is needed because the template parameters in Bind can't be resolved +// if they're used both as parameters of the function pointer type and as +// parameters to Bind itself: the function pointer parameters are exact +// matches to the function prototype, but the parameters to bind have +// references stripped. This trick allows the compiler to dictate the Bind +// parameter types rather than deduce them. +template struct identity { typedef T type; }; +} // namespace detail + +$var n = 5 +$range i 0..n +$for i [[ +$range j 1..i + +template +class MethodFunctor$i { + public: + MethodFunctor$i(MethodT method, ObjectT* object$for j [[, + P$j p$j]]) + : method_(method), object_(object)$for j [[, + p$(j)_(p$j)]] {} + R operator()() const { + return (object_->*method_)($for j , [[p$(j)_]]); } + private: + MethodT method_; + ObjectT* object_;$for j [[ + + P$j p$(j)_;]] + +}; + +#define FP_T(x) R (ObjectT::*x)($for j , [[P$j]]) + +template +MethodFunctor$i +Bind(FP_T(method), ObjectT* object$for j [[, + typename detail::identity::type p$j]]) { + return MethodFunctor$i( + method, object$for j [[, p$j]]); +} + +#undef FP_T +#define FP_T(x) R (ObjectT::*x)($for j , [[P$j]]) const + +template +MethodFunctor$i +Bind(FP_T(method), const ObjectT* object$for j [[, + typename detail::identity::type p$j]]) { + return MethodFunctor$i( + method, object$for j [[, p$j]]); +} + +#undef FP_T + +]] + +} // namespace talk_base + +#undef NONAME + +#endif // TALK_BASE_BIND_H_ diff --git a/talk/base/bind_unittest.cc b/talk/base/bind_unittest.cc new file mode 100644 index 000000000..81bbddd6b --- /dev/null +++ b/talk/base/bind_unittest.cc @@ -0,0 +1,74 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/bind.h" +#include "talk/base/gunit.h" + +namespace talk_base { + +namespace { + +struct MethodBindTester { + void NullaryVoid() { ++call_count; } + int NullaryInt() { ++call_count; return 1; } + int NullaryConst() const { ++call_count; return 2; } + void UnaryVoid(int dummy) { ++call_count; } + template T Identity(T value) { ++call_count; return value; } + int UnaryByRef(int& value) const { ++call_count; return ++value; } // NOLINT + int Multiply(int a, int b) const { ++call_count; return a * b; } + mutable int call_count; +}; + +} // namespace + +TEST(BindTest, BindToMethod) { + MethodBindTester object = {0}; + EXPECT_EQ(0, object.call_count); + Bind(&MethodBindTester::NullaryVoid, &object)(); + EXPECT_EQ(1, object.call_count); + EXPECT_EQ(1, Bind(&MethodBindTester::NullaryInt, &object)()); + EXPECT_EQ(2, object.call_count); + EXPECT_EQ(2, Bind(&MethodBindTester::NullaryConst, + static_cast(&object))()); + EXPECT_EQ(3, object.call_count); + Bind(&MethodBindTester::UnaryVoid, &object, 5)(); + EXPECT_EQ(4, object.call_count); + EXPECT_EQ(100, Bind(&MethodBindTester::Identity, &object, 100)()); + EXPECT_EQ(5, object.call_count); + const std::string string_value("test string"); + EXPECT_EQ(string_value, Bind(&MethodBindTester::Identity, + &object, string_value)()); + EXPECT_EQ(6, object.call_count); + int value = 11; + EXPECT_EQ(12, Bind(&MethodBindTester::UnaryByRef, &object, value)()); + EXPECT_EQ(12, value); + EXPECT_EQ(7, object.call_count); + EXPECT_EQ(56, Bind(&MethodBindTester::Multiply, &object, 7, 8)()); + EXPECT_EQ(8, object.call_count); +} + +} // namespace talk_base diff --git a/talk/base/buffer.h b/talk/base/buffer.h new file mode 100644 index 000000000..311cfad72 --- /dev/null +++ b/talk/base/buffer.h @@ -0,0 +1,119 @@ +/* + * libjingle + * Copyright 2004-2010, 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. + */ + +#ifndef TALK_BASE_BUFFER_H_ +#define TALK_BASE_BUFFER_H_ + +#include + +#include "talk/base/scoped_ptr.h" + +namespace talk_base { + +// Basic buffer class, can be grown and shrunk dynamically. +// Unlike std::string/vector, does not initialize data when expanding capacity. +class Buffer { + public: + Buffer() { + Construct(NULL, 0, 0); + } + Buffer(const void* data, size_t length) { + Construct(data, length, length); + } + Buffer(const void* data, size_t length, size_t capacity) { + Construct(data, length, capacity); + } + Buffer(const Buffer& buf) { + Construct(buf.data(), buf.length(), buf.length()); + } + + const char* data() const { return data_.get(); } + char* data() { return data_.get(); } + // TODO: should this be size(), like STL? + size_t length() const { return length_; } + size_t capacity() const { return capacity_; } + + Buffer& operator=(const Buffer& buf) { + if (&buf != this) { + Construct(buf.data(), buf.length(), buf.length()); + } + return *this; + } + bool operator==(const Buffer& buf) const { + return (length_ == buf.length() && + memcmp(data_.get(), buf.data(), length_) == 0); + } + bool operator!=(const Buffer& buf) const { + return !operator==(buf); + } + + void SetData(const void* data, size_t length) { + ASSERT(data != NULL || length == 0); + SetLength(length); + memcpy(data_.get(), data, length); + } + void AppendData(const void* data, size_t length) { + ASSERT(data != NULL || length == 0); + size_t old_length = length_; + SetLength(length_ + length); + memcpy(data_.get() + old_length, data, length); + } + void SetLength(size_t length) { + SetCapacity(length); + length_ = length; + } + void SetCapacity(size_t capacity) { + if (capacity > capacity_) { + talk_base::scoped_array data(new char[capacity]); + memcpy(data.get(), data_.get(), length_); + data_.swap(data); + capacity_ = capacity; + } + } + + void TransferTo(Buffer* buf) { + ASSERT(buf != NULL); + buf->data_.reset(data_.release()); + buf->length_ = length_; + buf->capacity_ = capacity_; + Construct(NULL, 0, 0); + } + + protected: + void Construct(const void* data, size_t length, size_t capacity) { + data_.reset(new char[capacity_ = capacity]); + SetData(data, length); + } + + scoped_array data_; + size_t length_; + size_t capacity_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_BUFFER_H_ diff --git a/talk/base/buffer_unittest.cc b/talk/base/buffer_unittest.cc new file mode 100644 index 000000000..b0aa2433d --- /dev/null +++ b/talk/base/buffer_unittest.cc @@ -0,0 +1,160 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/buffer.h" +#include "talk/base/gunit.h" + +namespace talk_base { + +static const char kTestData[] = { + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF +}; + +TEST(BufferTest, TestConstructDefault) { + Buffer buf; + EXPECT_EQ(0U, buf.length()); + EXPECT_EQ(0U, buf.capacity()); + EXPECT_EQ(Buffer(), buf); +} + +TEST(BufferTest, TestConstructEmptyWithCapacity) { + Buffer buf(NULL, 0, 256U); + EXPECT_EQ(0U, buf.length()); + EXPECT_EQ(256U, buf.capacity()); + EXPECT_EQ(Buffer(), buf); +} + +TEST(BufferTest, TestConstructData) { + Buffer buf(kTestData, sizeof(kTestData)); + EXPECT_EQ(sizeof(kTestData), buf.length()); + EXPECT_EQ(sizeof(kTestData), buf.capacity()); + EXPECT_EQ(0, memcmp(buf.data(), kTestData, sizeof(kTestData))); + EXPECT_EQ(Buffer(kTestData, sizeof(kTestData)), buf); +} + +TEST(BufferTest, TestConstructDataWithCapacity) { + Buffer buf(kTestData, sizeof(kTestData), 256U); + EXPECT_EQ(sizeof(kTestData), buf.length()); + EXPECT_EQ(256U, buf.capacity()); + EXPECT_EQ(0, memcmp(buf.data(), kTestData, sizeof(kTestData))); + EXPECT_EQ(Buffer(kTestData, sizeof(kTestData)), buf); +} + +TEST(BufferTest, TestConstructCopy) { + Buffer buf1(kTestData, sizeof(kTestData), 256), buf2(buf1); + EXPECT_EQ(sizeof(kTestData), buf2.length()); + EXPECT_EQ(sizeof(kTestData), buf2.capacity()); // capacity isn't copied + EXPECT_EQ(0, memcmp(buf2.data(), kTestData, sizeof(kTestData))); + EXPECT_EQ(buf1, buf2); +} + +TEST(BufferTest, TestAssign) { + Buffer buf1, buf2(kTestData, sizeof(kTestData), 256); + EXPECT_NE(buf1, buf2); + buf1 = buf2; + EXPECT_EQ(sizeof(kTestData), buf1.length()); + EXPECT_EQ(sizeof(kTestData), buf1.capacity()); // capacity isn't copied + EXPECT_EQ(0, memcmp(buf1.data(), kTestData, sizeof(kTestData))); + EXPECT_EQ(buf1, buf2); +} + +TEST(BufferTest, TestSetData) { + Buffer buf; + buf.SetData(kTestData, sizeof(kTestData)); + EXPECT_EQ(sizeof(kTestData), buf.length()); + EXPECT_EQ(sizeof(kTestData), buf.capacity()); + EXPECT_EQ(0, memcmp(buf.data(), kTestData, sizeof(kTestData))); +} + +TEST(BufferTest, TestAppendData) { + Buffer buf(kTestData, sizeof(kTestData)); + buf.AppendData(kTestData, sizeof(kTestData)); + EXPECT_EQ(2 * sizeof(kTestData), buf.length()); + EXPECT_EQ(2 * sizeof(kTestData), buf.capacity()); + EXPECT_EQ(0, memcmp(buf.data(), kTestData, sizeof(kTestData))); + EXPECT_EQ(0, memcmp(buf.data() + sizeof(kTestData), + kTestData, sizeof(kTestData))); +} + +TEST(BufferTest, TestSetLengthSmaller) { + Buffer buf; + buf.SetData(kTestData, sizeof(kTestData)); + buf.SetLength(sizeof(kTestData) / 2); + EXPECT_EQ(sizeof(kTestData) / 2, buf.length()); + EXPECT_EQ(sizeof(kTestData), buf.capacity()); + EXPECT_EQ(0, memcmp(buf.data(), kTestData, sizeof(kTestData) / 2)); +} + +TEST(BufferTest, TestSetLengthLarger) { + Buffer buf; + buf.SetData(kTestData, sizeof(kTestData)); + buf.SetLength(sizeof(kTestData) * 2); + EXPECT_EQ(sizeof(kTestData) * 2, buf.length()); + EXPECT_EQ(sizeof(kTestData) * 2, buf.capacity()); + EXPECT_EQ(0, memcmp(buf.data(), kTestData, sizeof(kTestData))); +} + +TEST(BufferTest, TestSetCapacitySmaller) { + Buffer buf; + buf.SetData(kTestData, sizeof(kTestData)); + buf.SetCapacity(sizeof(kTestData) / 2); // should be ignored + EXPECT_EQ(sizeof(kTestData), buf.length()); + EXPECT_EQ(sizeof(kTestData), buf.capacity()); + EXPECT_EQ(0, memcmp(buf.data(), kTestData, sizeof(kTestData))); +} + +TEST(BufferTest, TestSetCapacityLarger) { + Buffer buf(kTestData, sizeof(kTestData)); + buf.SetCapacity(sizeof(kTestData) * 2); + EXPECT_EQ(sizeof(kTestData), buf.length()); + EXPECT_EQ(sizeof(kTestData) * 2, buf.capacity()); + EXPECT_EQ(0, memcmp(buf.data(), kTestData, sizeof(kTestData))); +} + +TEST(BufferTest, TestSetCapacityThenSetLength) { + Buffer buf(kTestData, sizeof(kTestData)); + buf.SetCapacity(sizeof(kTestData) * 4); + memcpy(buf.data() + sizeof(kTestData), kTestData, sizeof(kTestData)); + buf.SetLength(sizeof(kTestData) * 2); + EXPECT_EQ(sizeof(kTestData) * 2, buf.length()); + EXPECT_EQ(sizeof(kTestData) * 4, buf.capacity()); + EXPECT_EQ(0, memcmp(buf.data(), kTestData, sizeof(kTestData))); + EXPECT_EQ(0, memcmp(buf.data() + sizeof(kTestData), + kTestData, sizeof(kTestData))); +} + +TEST(BufferTest, TestTransfer) { + Buffer buf1(kTestData, sizeof(kTestData), 256U), buf2; + buf1.TransferTo(&buf2); + EXPECT_EQ(0U, buf1.length()); + EXPECT_EQ(0U, buf1.capacity()); + EXPECT_EQ(sizeof(kTestData), buf2.length()); + EXPECT_EQ(256U, buf2.capacity()); // capacity does transfer + EXPECT_EQ(0, memcmp(buf2.data(), kTestData, sizeof(kTestData))); +} + +} // namespace talk_base diff --git a/talk/base/bytebuffer.cc b/talk/base/bytebuffer.cc new file mode 100644 index 000000000..523475d82 --- /dev/null +++ b/talk/base/bytebuffer.cc @@ -0,0 +1,250 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/bytebuffer.h" + +#include +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/byteorder.h" + +namespace talk_base { + +static const int DEFAULT_SIZE = 4096; + +ByteBuffer::ByteBuffer() { + Construct(NULL, DEFAULT_SIZE, ORDER_NETWORK); +} + +ByteBuffer::ByteBuffer(ByteOrder byte_order) { + Construct(NULL, DEFAULT_SIZE, byte_order); +} + +ByteBuffer::ByteBuffer(const char* bytes, size_t len) { + Construct(bytes, len, ORDER_NETWORK); +} + +ByteBuffer::ByteBuffer(const char* bytes, size_t len, ByteOrder byte_order) { + Construct(bytes, len, byte_order); +} + +ByteBuffer::ByteBuffer(const char* bytes) { + Construct(bytes, strlen(bytes), ORDER_NETWORK); +} + +void ByteBuffer::Construct(const char* bytes, size_t len, + ByteOrder byte_order) { + version_ = 0; + start_ = 0; + size_ = len; + byte_order_ = byte_order; + bytes_ = new char[size_]; + + if (bytes) { + end_ = len; + memcpy(bytes_, bytes, end_); + } else { + end_ = 0; + } +} + +ByteBuffer::~ByteBuffer() { + delete[] bytes_; +} + +bool ByteBuffer::ReadUInt8(uint8* val) { + if (!val) return false; + + return ReadBytes(reinterpret_cast(val), 1); +} + +bool ByteBuffer::ReadUInt16(uint16* val) { + if (!val) return false; + + uint16 v; + if (!ReadBytes(reinterpret_cast(&v), 2)) { + return false; + } else { + *val = (byte_order_ == ORDER_NETWORK) ? NetworkToHost16(v) : v; + return true; + } +} + +bool ByteBuffer::ReadUInt24(uint32* val) { + if (!val) return false; + + uint32 v = 0; + char* read_into = reinterpret_cast(&v); + if (byte_order_ == ORDER_NETWORK || IsHostBigEndian()) { + ++read_into; + } + + if (!ReadBytes(read_into, 3)) { + return false; + } else { + *val = (byte_order_ == ORDER_NETWORK) ? NetworkToHost32(v) : v; + return true; + } +} + +bool ByteBuffer::ReadUInt32(uint32* val) { + if (!val) return false; + + uint32 v; + if (!ReadBytes(reinterpret_cast(&v), 4)) { + return false; + } else { + *val = (byte_order_ == ORDER_NETWORK) ? NetworkToHost32(v) : v; + return true; + } +} + +bool ByteBuffer::ReadUInt64(uint64* val) { + if (!val) return false; + + uint64 v; + if (!ReadBytes(reinterpret_cast(&v), 8)) { + return false; + } else { + *val = (byte_order_ == ORDER_NETWORK) ? NetworkToHost64(v) : v; + return true; + } +} + +bool ByteBuffer::ReadString(std::string* val, size_t len) { + if (!val) return false; + + if (len > Length()) { + return false; + } else { + val->append(bytes_ + start_, len); + start_ += len; + return true; + } +} + +bool ByteBuffer::ReadBytes(char* val, size_t len) { + if (len > Length()) { + return false; + } else { + memcpy(val, bytes_ + start_, len); + start_ += len; + return true; + } +} + +void ByteBuffer::WriteUInt8(uint8 val) { + WriteBytes(reinterpret_cast(&val), 1); +} + +void ByteBuffer::WriteUInt16(uint16 val) { + uint16 v = (byte_order_ == ORDER_NETWORK) ? HostToNetwork16(val) : val; + WriteBytes(reinterpret_cast(&v), 2); +} + +void ByteBuffer::WriteUInt24(uint32 val) { + uint32 v = (byte_order_ == ORDER_NETWORK) ? HostToNetwork32(val) : val; + char* start = reinterpret_cast(&v); + if (byte_order_ == ORDER_NETWORK || IsHostBigEndian()) { + ++start; + } + WriteBytes(start, 3); +} + +void ByteBuffer::WriteUInt32(uint32 val) { + uint32 v = (byte_order_ == ORDER_NETWORK) ? HostToNetwork32(val) : val; + WriteBytes(reinterpret_cast(&v), 4); +} + +void ByteBuffer::WriteUInt64(uint64 val) { + uint64 v = (byte_order_ == ORDER_NETWORK) ? HostToNetwork64(val) : val; + WriteBytes(reinterpret_cast(&v), 8); +} + +void ByteBuffer::WriteString(const std::string& val) { + WriteBytes(val.c_str(), val.size()); +} + +void ByteBuffer::WriteBytes(const char* val, size_t len) { + memcpy(ReserveWriteBuffer(len), val, len); +} + +char* ByteBuffer::ReserveWriteBuffer(size_t len) { + if (Length() + len > Capacity()) + Resize(Length() + len); + + char* start = bytes_ + end_; + end_ += len; + return start; +} + +void ByteBuffer::Resize(size_t size) { + size_t len = _min(end_ - start_, size); + if (size <= size_) { + // Don't reallocate, just move data backwards + memmove(bytes_, bytes_ + start_, len); + } else { + // Reallocate a larger buffer. + size_ = _max(size, 3 * size_ / 2); + char* new_bytes = new char[size_]; + memcpy(new_bytes, bytes_ + start_, len); + delete [] bytes_; + bytes_ = new_bytes; + } + start_ = 0; + end_ = len; + ++version_; +} + +bool ByteBuffer::Consume(size_t size) { + if (size > Length()) + return false; + start_ += size; + return true; +} + +ByteBuffer::ReadPosition ByteBuffer::GetReadPosition() const { + return ReadPosition(start_, version_); +} + +bool ByteBuffer::SetReadPosition(const ReadPosition &position) { + if (position.version_ != version_) { + return false; + } + start_ = position.start_; + return true; +} + +void ByteBuffer::Clear() { + memset(bytes_, 0, size_); + start_ = end_ = 0; + ++version_; +} + +} // namespace talk_base diff --git a/talk/base/bytebuffer.h b/talk/base/bytebuffer.h new file mode 100644 index 000000000..a12c59cbc --- /dev/null +++ b/talk/base/bytebuffer.h @@ -0,0 +1,136 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_BYTEBUFFER_H_ +#define TALK_BASE_BYTEBUFFER_H_ + +#include + +#include "talk/base/basictypes.h" +#include "talk/base/constructormagic.h" + +namespace talk_base { + +class ByteBuffer { + public: + + enum ByteOrder { + ORDER_NETWORK = 0, // Default, use network byte order (big endian). + ORDER_HOST, // Use the native order of the host. + }; + + // |byte_order| defines order of bytes in the buffer. + ByteBuffer(); + explicit ByteBuffer(ByteOrder byte_order); + ByteBuffer(const char* bytes, size_t len); + ByteBuffer(const char* bytes, size_t len, ByteOrder byte_order); + + // Initializes buffer from a zero-terminated string. + explicit ByteBuffer(const char* bytes); + + ~ByteBuffer(); + + const char* Data() const { return bytes_ + start_; } + size_t Length() const { return end_ - start_; } + size_t Capacity() const { return size_ - start_; } + ByteOrder Order() const { return byte_order_; } + + // Read a next value from the buffer. Return false if there isn't + // enough data left for the specified type. + bool ReadUInt8(uint8* val); + bool ReadUInt16(uint16* val); + bool ReadUInt24(uint32* val); + bool ReadUInt32(uint32* val); + bool ReadUInt64(uint64* val); + bool ReadBytes(char* val, size_t len); + + // Appends next |len| bytes from the buffer to |val|. Returns false + // if there is less than |len| bytes left. + bool ReadString(std::string* val, size_t len); + + // Write value to the buffer. Resizes the buffer when it is + // neccessary. + void WriteUInt8(uint8 val); + void WriteUInt16(uint16 val); + void WriteUInt24(uint32 val); + void WriteUInt32(uint32 val); + void WriteUInt64(uint64 val); + void WriteString(const std::string& val); + void WriteBytes(const char* val, size_t len); + + // Reserves the given number of bytes and returns a char* that can be written + // into. Useful for functions that require a char* buffer and not a + // ByteBuffer. + char* ReserveWriteBuffer(size_t len); + + // Resize the buffer to the specified |size|. This invalidates any remembered + // seek positions. + void Resize(size_t size); + + // Moves current position |size| bytes forward. Returns false if + // there is less than |size| bytes left in the buffer. Consume doesn't + // permanently remove data, so remembered read positions are still valid + // after this call. + bool Consume(size_t size); + + // Clears the contents of the buffer. After this, Length() will be 0. + void Clear(); + + // Used with GetReadPosition/SetReadPosition. + class ReadPosition { + friend class ByteBuffer; + ReadPosition(size_t start, int version) + : start_(start), version_(version) { } + size_t start_; + int version_; + }; + + // Remembers the current read position for a future SetReadPosition. Any + // calls to Shift or Resize in the interim will invalidate the position. + ReadPosition GetReadPosition() const; + + // If the given position is still valid, restores that read position. + bool SetReadPosition(const ReadPosition &position); + + private: + void Construct(const char* bytes, size_t size, ByteOrder byte_order); + + char* bytes_; + size_t size_; + size_t start_; + size_t end_; + int version_; + ByteOrder byte_order_; + + // There are sensible ways to define these, but they aren't needed in our code + // base. + DISALLOW_COPY_AND_ASSIGN(ByteBuffer); +}; + +} // namespace talk_base + +#endif // TALK_BASE_BYTEBUFFER_H_ diff --git a/talk/base/bytebuffer_unittest.cc b/talk/base/bytebuffer_unittest.cc new file mode 100644 index 000000000..2c734533c --- /dev/null +++ b/talk/base/bytebuffer_unittest.cc @@ -0,0 +1,228 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/bytebuffer.h" +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/gunit.h" + +namespace talk_base { + +TEST(ByteBufferTest, TestByteOrder) { + uint16 n16 = 1; + uint32 n32 = 1; + uint64 n64 = 1; + + EXPECT_EQ(n16, NetworkToHost16(HostToNetwork16(n16))); + EXPECT_EQ(n32, NetworkToHost32(HostToNetwork32(n32))); + EXPECT_EQ(n64, NetworkToHost64(HostToNetwork64(n64))); + + if (IsHostBigEndian()) { + // The host is the network (big) endian. + EXPECT_EQ(n16, HostToNetwork16(n16)); + EXPECT_EQ(n32, HostToNetwork32(n32)); + EXPECT_EQ(n64, HostToNetwork64(n64)); + + // GetBE converts big endian to little endian here. + EXPECT_EQ(n16 >> 8, GetBE16(&n16)); + EXPECT_EQ(n32 >> 24, GetBE32(&n32)); + EXPECT_EQ(n64 >> 56, GetBE64(&n64)); + } else { + // The host is little endian. + EXPECT_NE(n16, HostToNetwork16(n16)); + EXPECT_NE(n32, HostToNetwork32(n32)); + EXPECT_NE(n64, HostToNetwork64(n64)); + + // GetBE converts little endian to big endian here. + EXPECT_EQ(GetBE16(&n16), HostToNetwork16(n16)); + EXPECT_EQ(GetBE32(&n32), HostToNetwork32(n32)); + EXPECT_EQ(GetBE64(&n64), HostToNetwork64(n64)); + + // GetBE converts little endian to big endian here. + EXPECT_EQ(n16 << 8, GetBE16(&n16)); + EXPECT_EQ(n32 << 24, GetBE32(&n32)); + EXPECT_EQ(n64 << 56, GetBE64(&n64)); + } +} + +TEST(ByteBufferTest, TestBufferLength) { + ByteBuffer buffer; + size_t size = 0; + EXPECT_EQ(size, buffer.Length()); + + buffer.WriteUInt8(1); + ++size; + EXPECT_EQ(size, buffer.Length()); + + buffer.WriteUInt16(1); + size += 2; + EXPECT_EQ(size, buffer.Length()); + + buffer.WriteUInt24(1); + size += 3; + EXPECT_EQ(size, buffer.Length()); + + buffer.WriteUInt32(1); + size += 4; + EXPECT_EQ(size, buffer.Length()); + + buffer.WriteUInt64(1); + size += 8; + EXPECT_EQ(size, buffer.Length()); + + EXPECT_TRUE(buffer.Consume(0)); + EXPECT_EQ(size, buffer.Length()); + + EXPECT_TRUE(buffer.Consume(4)); + size -= 4; + EXPECT_EQ(size, buffer.Length()); +} + +TEST(ByteBufferTest, TestGetSetReadPosition) { + ByteBuffer buffer("ABCDEF", 6); + EXPECT_EQ(6U, buffer.Length()); + ByteBuffer::ReadPosition pos(buffer.GetReadPosition()); + EXPECT_TRUE(buffer.SetReadPosition(pos)); + EXPECT_EQ(6U, buffer.Length()); + std::string read; + EXPECT_TRUE(buffer.ReadString(&read, 3)); + EXPECT_EQ("ABC", read); + EXPECT_EQ(3U, buffer.Length()); + EXPECT_TRUE(buffer.SetReadPosition(pos)); + EXPECT_EQ(6U, buffer.Length()); + read.clear(); + EXPECT_TRUE(buffer.ReadString(&read, 3)); + EXPECT_EQ("ABC", read); + EXPECT_EQ(3U, buffer.Length()); + // For a resize by writing Capacity() number of bytes. + size_t capacity = buffer.Capacity(); + buffer.ReserveWriteBuffer(buffer.Capacity()); + EXPECT_EQ(capacity + 3U, buffer.Length()); + EXPECT_FALSE(buffer.SetReadPosition(pos)); + read.clear(); + EXPECT_TRUE(buffer.ReadString(&read, 3)); + EXPECT_EQ("DEF", read); +} + +TEST(ByteBufferTest, TestReadWriteBuffer) { + ByteBuffer::ByteOrder orders[2] = { ByteBuffer::ORDER_HOST, + ByteBuffer::ORDER_NETWORK }; + for (size_t i = 0; i < ARRAY_SIZE(orders); i++) { + ByteBuffer buffer(orders[i]); + EXPECT_EQ(orders[i], buffer.Order()); + uint8 ru8; + EXPECT_FALSE(buffer.ReadUInt8(&ru8)); + + // Write and read uint8. + uint8 wu8 = 1; + buffer.WriteUInt8(wu8); + EXPECT_TRUE(buffer.ReadUInt8(&ru8)); + EXPECT_EQ(wu8, ru8); + EXPECT_EQ(0U, buffer.Length()); + + // Write and read uint16. + uint16 wu16 = (1 << 8) + 1; + buffer.WriteUInt16(wu16); + uint16 ru16; + EXPECT_TRUE(buffer.ReadUInt16(&ru16)); + EXPECT_EQ(wu16, ru16); + EXPECT_EQ(0U, buffer.Length()); + + // Write and read uint24. + uint32 wu24 = (3 << 16) + (2 << 8) + 1; + buffer.WriteUInt24(wu24); + uint32 ru24; + EXPECT_TRUE(buffer.ReadUInt24(&ru24)); + EXPECT_EQ(wu24, ru24); + EXPECT_EQ(0U, buffer.Length()); + + // Write and read uint32. + uint32 wu32 = (4 << 24) + (3 << 16) + (2 << 8) + 1; + buffer.WriteUInt32(wu32); + uint32 ru32; + EXPECT_TRUE(buffer.ReadUInt32(&ru32)); + EXPECT_EQ(wu32, ru32); + EXPECT_EQ(0U, buffer.Length()); + + // Write and read uint64. + uint32 another32 = (8 << 24) + (7 << 16) + (6 << 8) + 5; + uint64 wu64 = (static_cast(another32) << 32) + wu32; + buffer.WriteUInt64(wu64); + uint64 ru64; + EXPECT_TRUE(buffer.ReadUInt64(&ru64)); + EXPECT_EQ(wu64, ru64); + EXPECT_EQ(0U, buffer.Length()); + + // Write and read string. + std::string write_string("hello"); + buffer.WriteString(write_string); + std::string read_string; + EXPECT_TRUE(buffer.ReadString(&read_string, write_string.size())); + EXPECT_EQ(write_string, read_string); + EXPECT_EQ(0U, buffer.Length()); + + // Write and read bytes + char write_bytes[] = "foo"; + buffer.WriteBytes(write_bytes, 3); + char read_bytes[3]; + EXPECT_TRUE(buffer.ReadBytes(read_bytes, 3)); + for (int i = 0; i < 3; ++i) { + EXPECT_EQ(write_bytes[i], read_bytes[i]); + } + EXPECT_EQ(0U, buffer.Length()); + + // Write and read reserved buffer space + char* write_dst = buffer.ReserveWriteBuffer(3); + memcpy(write_dst, write_bytes, 3); + memset(read_bytes, 0, 3); + EXPECT_TRUE(buffer.ReadBytes(read_bytes, 3)); + for (int i = 0; i < 3; ++i) { + EXPECT_EQ(write_bytes[i], read_bytes[i]); + } + EXPECT_EQ(0U, buffer.Length()); + + // Write and read in order. + buffer.WriteUInt8(wu8); + buffer.WriteUInt16(wu16); + buffer.WriteUInt24(wu24); + buffer.WriteUInt32(wu32); + buffer.WriteUInt64(wu64); + EXPECT_TRUE(buffer.ReadUInt8(&ru8)); + EXPECT_EQ(wu8, ru8); + EXPECT_TRUE(buffer.ReadUInt16(&ru16)); + EXPECT_EQ(wu16, ru16); + EXPECT_TRUE(buffer.ReadUInt24(&ru24)); + EXPECT_EQ(wu24, ru24); + EXPECT_TRUE(buffer.ReadUInt32(&ru32)); + EXPECT_EQ(wu32, ru32); + EXPECT_TRUE(buffer.ReadUInt64(&ru64)); + EXPECT_EQ(wu64, ru64); + EXPECT_EQ(0U, buffer.Length()); + } +} + +} // namespace talk_base diff --git a/talk/base/byteorder.h b/talk/base/byteorder.h new file mode 100644 index 000000000..c6d0dbbe0 --- /dev/null +++ b/talk/base/byteorder.h @@ -0,0 +1,185 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_BASE_BYTEORDER_H_ +#define TALK_BASE_BYTEORDER_H_ + +#ifdef POSIX +#include +#endif + +#ifdef WIN32 +#include +#endif + +#include "talk/base/basictypes.h" + +namespace talk_base { + +// Reading and writing of little and big-endian numbers from memory +// TODO: Optimized versions, with direct read/writes of +// integers in host-endian format, when the platform supports it. + +inline void Set8(void* memory, size_t offset, uint8 v) { + static_cast(memory)[offset] = v; +} + +inline uint8 Get8(const void* memory, size_t offset) { + return static_cast(memory)[offset]; +} + +inline void SetBE16(void* memory, uint16 v) { + Set8(memory, 0, static_cast(v >> 8)); + Set8(memory, 1, static_cast(v >> 0)); +} + +inline void SetBE32(void* memory, uint32 v) { + Set8(memory, 0, static_cast(v >> 24)); + Set8(memory, 1, static_cast(v >> 16)); + Set8(memory, 2, static_cast(v >> 8)); + Set8(memory, 3, static_cast(v >> 0)); +} + +inline void SetBE64(void* memory, uint64 v) { + Set8(memory, 0, static_cast(v >> 56)); + Set8(memory, 1, static_cast(v >> 48)); + Set8(memory, 2, static_cast(v >> 40)); + Set8(memory, 3, static_cast(v >> 32)); + Set8(memory, 4, static_cast(v >> 24)); + Set8(memory, 5, static_cast(v >> 16)); + Set8(memory, 6, static_cast(v >> 8)); + Set8(memory, 7, static_cast(v >> 0)); +} + +inline uint16 GetBE16(const void* memory) { + return static_cast((Get8(memory, 0) << 8) | + (Get8(memory, 1) << 0)); +} + +inline uint32 GetBE32(const void* memory) { + return (static_cast(Get8(memory, 0)) << 24) | + (static_cast(Get8(memory, 1)) << 16) | + (static_cast(Get8(memory, 2)) << 8) | + (static_cast(Get8(memory, 3)) << 0); +} + +inline uint64 GetBE64(const void* memory) { + return (static_cast(Get8(memory, 0)) << 56) | + (static_cast(Get8(memory, 1)) << 48) | + (static_cast(Get8(memory, 2)) << 40) | + (static_cast(Get8(memory, 3)) << 32) | + (static_cast(Get8(memory, 4)) << 24) | + (static_cast(Get8(memory, 5)) << 16) | + (static_cast(Get8(memory, 6)) << 8) | + (static_cast(Get8(memory, 7)) << 0); +} + +inline void SetLE16(void* memory, uint16 v) { + Set8(memory, 0, static_cast(v >> 0)); + Set8(memory, 1, static_cast(v >> 8)); +} + +inline void SetLE32(void* memory, uint32 v) { + Set8(memory, 0, static_cast(v >> 0)); + Set8(memory, 1, static_cast(v >> 8)); + Set8(memory, 2, static_cast(v >> 16)); + Set8(memory, 3, static_cast(v >> 24)); +} + +inline void SetLE64(void* memory, uint64 v) { + Set8(memory, 0, static_cast(v >> 0)); + Set8(memory, 1, static_cast(v >> 8)); + Set8(memory, 2, static_cast(v >> 16)); + Set8(memory, 3, static_cast(v >> 24)); + Set8(memory, 4, static_cast(v >> 32)); + Set8(memory, 5, static_cast(v >> 40)); + Set8(memory, 6, static_cast(v >> 48)); + Set8(memory, 7, static_cast(v >> 56)); +} + +inline uint16 GetLE16(const void* memory) { + return static_cast((Get8(memory, 0) << 0) | + (Get8(memory, 1) << 8)); +} + +inline uint32 GetLE32(const void* memory) { + return (static_cast(Get8(memory, 0)) << 0) | + (static_cast(Get8(memory, 1)) << 8) | + (static_cast(Get8(memory, 2)) << 16) | + (static_cast(Get8(memory, 3)) << 24); +} + +inline uint64 GetLE64(const void* memory) { + return (static_cast(Get8(memory, 0)) << 0) | + (static_cast(Get8(memory, 1)) << 8) | + (static_cast(Get8(memory, 2)) << 16) | + (static_cast(Get8(memory, 3)) << 24) | + (static_cast(Get8(memory, 4)) << 32) | + (static_cast(Get8(memory, 5)) << 40) | + (static_cast(Get8(memory, 6)) << 48) | + (static_cast(Get8(memory, 7)) << 56); +} + +// Check if the current host is big endian. +inline bool IsHostBigEndian() { + static const int number = 1; + return 0 == *reinterpret_cast(&number); +} + +inline uint16 HostToNetwork16(uint16 n) { + uint16 result; + SetBE16(&result, n); + return result; +} + +inline uint32 HostToNetwork32(uint32 n) { + uint32 result; + SetBE32(&result, n); + return result; +} + +inline uint64 HostToNetwork64(uint64 n) { + uint64 result; + SetBE64(&result, n); + return result; +} + +inline uint16 NetworkToHost16(uint16 n) { + return GetBE16(&n); +} + +inline uint32 NetworkToHost32(uint32 n) { + return GetBE32(&n); +} + +inline uint64 NetworkToHost64(uint64 n) { + return GetBE64(&n); +} + +} // namespace talk_base + +#endif // TALK_BASE_BYTEORDER_H_ diff --git a/talk/base/byteorder_unittest.cc b/talk/base/byteorder_unittest.cc new file mode 100644 index 000000000..01dd26ae9 --- /dev/null +++ b/talk/base/byteorder_unittest.cc @@ -0,0 +1,100 @@ +/* + * libjingle + * Copyright 2012 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/base/byteorder.h" + +#include "talk/base/basictypes.h" +#include "talk/base/gunit.h" + +namespace talk_base { + +// Test memory set functions put values into memory in expected order. +TEST(ByteOrderTest, TestSet) { + uint8 buf[8] = { 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u }; + Set8(buf, 0, 0xfb); + Set8(buf, 1, 0x12); + EXPECT_EQ(0xfb, buf[0]); + EXPECT_EQ(0x12, buf[1]); + SetBE16(buf, 0x1234); + EXPECT_EQ(0x12, buf[0]); + EXPECT_EQ(0x34, buf[1]); + SetLE16(buf, 0x1234); + EXPECT_EQ(0x34, buf[0]); + EXPECT_EQ(0x12, buf[1]); + SetBE32(buf, 0x12345678); + EXPECT_EQ(0x12, buf[0]); + EXPECT_EQ(0x34, buf[1]); + EXPECT_EQ(0x56, buf[2]); + EXPECT_EQ(0x78, buf[3]); + SetLE32(buf, 0x12345678); + EXPECT_EQ(0x78, buf[0]); + EXPECT_EQ(0x56, buf[1]); + EXPECT_EQ(0x34, buf[2]); + EXPECT_EQ(0x12, buf[3]); + SetBE64(buf, UINT64_C(0x0123456789abcdef)); + EXPECT_EQ(0x01, buf[0]); + EXPECT_EQ(0x23, buf[1]); + EXPECT_EQ(0x45, buf[2]); + EXPECT_EQ(0x67, buf[3]); + EXPECT_EQ(0x89, buf[4]); + EXPECT_EQ(0xab, buf[5]); + EXPECT_EQ(0xcd, buf[6]); + EXPECT_EQ(0xef, buf[7]); + SetLE64(buf, UINT64_C(0x0123456789abcdef)); + EXPECT_EQ(0xef, buf[0]); + EXPECT_EQ(0xcd, buf[1]); + EXPECT_EQ(0xab, buf[2]); + EXPECT_EQ(0x89, buf[3]); + EXPECT_EQ(0x67, buf[4]); + EXPECT_EQ(0x45, buf[5]); + EXPECT_EQ(0x23, buf[6]); + EXPECT_EQ(0x01, buf[7]); +} + +// Test memory get functions get values from memory in expected order. +TEST(ByteOrderTest, TestGet) { + uint8 buf[8]; + buf[0] = 0x01u; + buf[1] = 0x23u; + buf[2] = 0x45u; + buf[3] = 0x67u; + buf[4] = 0x89u; + buf[5] = 0xabu; + buf[6] = 0xcdu; + buf[7] = 0xefu; + EXPECT_EQ(0x01u, Get8(buf, 0)); + EXPECT_EQ(0x23u, Get8(buf, 1)); + EXPECT_EQ(0x0123u, GetBE16(buf)); + EXPECT_EQ(0x2301u, GetLE16(buf)); + EXPECT_EQ(0x01234567u, GetBE32(buf)); + EXPECT_EQ(0x67452301u, GetLE32(buf)); + EXPECT_EQ(UINT64_C(0x0123456789abcdef), GetBE64(buf)); + EXPECT_EQ(UINT64_C(0xefcdab8967452301), GetLE64(buf)); +} + +} // namespace talk_base + diff --git a/talk/base/checks.cc b/talk/base/checks.cc new file mode 100644 index 000000000..546678340 --- /dev/null +++ b/talk/base/checks.cc @@ -0,0 +1,47 @@ +/* + * libjingle + * Copyright 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 +#include +#include + +#include "talk/base/checks.h" +#include "talk/base/logging.h" + +void Fatal(const char* file, int line, const char* format, ...) { + char msg[256]; + + va_list arguments; + va_start(arguments, format); + vsnprintf(msg, sizeof(msg), format, arguments); + va_end(arguments); + + LOG(LS_ERROR) << "\n\n#\n# Fatal error in " << file + << ", line " << line << "\n#" << msg + << "\n#\n"; + abort(); +} diff --git a/talk/base/checks.h b/talk/base/checks.h new file mode 100644 index 000000000..83ad3723a --- /dev/null +++ b/talk/base/checks.h @@ -0,0 +1,44 @@ +/* + * libjingle + * Copyright 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. + */ + +// This module contains some basic debugging facilities. +// Originally comes from shared/commandlineflags/checks.h + +#ifndef TALK_BASE_CHECKS_H_ +#define TALK_BASE_CHECKS_H_ + +#include + +// Prints an error message to stderr and aborts execution. +void Fatal(const char* file, int line, const char* format, ...); + + +// The UNREACHABLE macro is very useful during development. +#define UNREACHABLE() \ + Fatal(__FILE__, __LINE__, "unreachable code") + +#endif // TALK_BASE_CHECKS_H_ diff --git a/talk/base/common.cc b/talk/base/common.cc new file mode 100644 index 000000000..842d925bf --- /dev/null +++ b/talk/base/common.cc @@ -0,0 +1,80 @@ +/* + * libjingle + * Copyright 2004--2005, 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 +#include +#include + +#if WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif // WIN32 + +#if OSX +#include +#endif // OSX + +#include +#include "talk/base/common.h" +#include "talk/base/logging.h" + +////////////////////////////////////////////////////////////////////// +// Assertions +////////////////////////////////////////////////////////////////////// + +namespace talk_base { + +void Break() { +#if WIN32 + ::DebugBreak(); +#elif OSX // !WIN32 + __asm__("int $3"); +#else // !OSX && !WIN32 +#if _DEBUG_HAVE_BACKTRACE + OutputTrace(); +#endif + abort(); +#endif // !OSX && !WIN32 +} + +static AssertLogger custom_assert_logger_ = NULL; + +void SetCustomAssertLogger(AssertLogger logger) { + custom_assert_logger_ = logger; +} + +void LogAssert(const char* function, const char* file, int line, + const char* expression) { + if (custom_assert_logger_) { + custom_assert_logger_(function, file, line, expression); + } else { + LOG(LS_ERROR) << file << "(" << line << ")" << ": ASSERT FAILED: " + << expression << " @ " << function; + } +} + +} // namespace talk_base diff --git a/talk/base/common.h b/talk/base/common.h new file mode 100644 index 000000000..d624ddc62 --- /dev/null +++ b/talk/base/common.h @@ -0,0 +1,188 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_BASE_COMMON_H_ +#define TALK_BASE_COMMON_H_ + +#include "talk/base/basictypes.h" +#include "talk/base/constructormagic.h" + +#if defined(_MSC_VER) +// warning C4355: 'this' : used in base member initializer list +#pragma warning(disable:4355) +#endif + +////////////////////////////////////////////////////////////////////// +// General Utilities +////////////////////////////////////////////////////////////////////// + +// Note: UNUSED is also defined in basictypes.h +#ifndef UNUSED +#define UNUSED(x) Unused(static_cast(&x)) +#define UNUSED2(x, y) Unused(static_cast(&x)); \ + Unused(static_cast(&y)) +#define UNUSED3(x, y, z) Unused(static_cast(&x)); \ + Unused(static_cast(&y)); \ + Unused(static_cast(&z)) +#define UNUSED4(x, y, z, a) Unused(static_cast(&x)); \ + Unused(static_cast(&y)); \ + Unused(static_cast(&z)); \ + Unused(static_cast(&a)) +#define UNUSED5(x, y, z, a, b) Unused(static_cast(&x)); \ + Unused(static_cast(&y)); \ + Unused(static_cast(&z)); \ + Unused(static_cast(&a)); \ + Unused(static_cast(&b)) +inline void Unused(const void*) {} +#endif // UNUSED + +#ifndef WIN32 +#define strnicmp(x, y, n) strncasecmp(x, y, n) +#define stricmp(x, y) strcasecmp(x, y) + +// TODO: Remove this. std::max should be used everywhere in the code. +// NOMINMAX must be defined where we include . +#define stdmax(x, y) std::max(x, y) +#else +#define stdmax(x, y) talk_base::_max(x, y) +#endif + +#define ARRAY_SIZE(x) (static_cast(sizeof(x) / sizeof(x[0]))) + +///////////////////////////////////////////////////////////////////////////// +// Assertions +///////////////////////////////////////////////////////////////////////////// + +#ifndef ENABLE_DEBUG +#define ENABLE_DEBUG _DEBUG +#endif // !defined(ENABLE_DEBUG) + +// Even for release builds, allow for the override of LogAssert. Though no +// macro is provided, this can still be used for explicit runtime asserts +// and allow applications to override the assert behavior. + +namespace talk_base { + +// LogAssert writes information about an assertion to the log. It's called by +// Assert (and from the ASSERT macro in debug mode) before any other action +// is taken (e.g. breaking the debugger, abort()ing, etc.). +void LogAssert(const char* function, const char* file, int line, + const char* expression); + +typedef void (*AssertLogger)(const char* function, + const char* file, + int line, + const char* expression); + +// Sets a custom assert logger to be used instead of the default LogAssert +// behavior. To clear the custom assert logger, pass NULL for |logger| and the +// default behavior will be restored. Only one custom assert logger can be set +// at a time, so this should generally be set during application startup and +// only by one component. +void SetCustomAssertLogger(AssertLogger logger); + +} // namespace talk_base + + +#if ENABLE_DEBUG + +namespace talk_base { + +// Break causes the debugger to stop executing, or the program to abort. +void Break(); + +inline bool Assert(bool result, const char* function, const char* file, + int line, const char* expression) { + if (!result) { + LogAssert(function, file, line, expression); + Break(); + return false; + } + return true; +} + +} // namespace talk_base + +#if defined(_MSC_VER) && _MSC_VER < 1300 +#define __FUNCTION__ "" +#endif + +#ifndef ASSERT +#define ASSERT(x) \ + (void)talk_base::Assert((x), __FUNCTION__, __FILE__, __LINE__, #x) +#endif + +#ifndef VERIFY +#define VERIFY(x) talk_base::Assert((x), __FUNCTION__, __FILE__, __LINE__, #x) +#endif + +#else // !ENABLE_DEBUG + +namespace talk_base { + +inline bool ImplicitCastToBool(bool result) { return result; } + +} // namespace talk_base + +#ifndef ASSERT +#define ASSERT(x) (void)0 +#endif + +#ifndef VERIFY +#define VERIFY(x) talk_base::ImplicitCastToBool(x) +#endif + +#endif // !ENABLE_DEBUG + +#define COMPILE_TIME_ASSERT(expr) char CTA_UNIQUE_NAME[expr] +#define CTA_UNIQUE_NAME CTA_MAKE_NAME(__LINE__) +#define CTA_MAKE_NAME(line) CTA_MAKE_NAME2(line) +#define CTA_MAKE_NAME2(line) constraint_ ## line + +// Forces compiler to inline, even against its better judgement. Use wisely. +#if defined(__GNUC__) +#define FORCE_INLINE __attribute__((always_inline)) +#elif defined(WIN32) +#define FORCE_INLINE __forceinline +#else +#define FORCE_INLINE +#endif + +// Borrowed from Chromium's base/compiler_specific.h. +// Annotate a virtual method indicating it must be overriding a virtual +// method in the parent class. +// Use like: +// virtual void foo() OVERRIDE; +#if defined(WIN32) +#define OVERRIDE override +#elif defined(__clang__) +#define OVERRIDE override +#else +#define OVERRIDE +#endif + +#endif // TALK_BASE_COMMON_H_ diff --git a/talk/base/constructormagic.h b/talk/base/constructormagic.h new file mode 100644 index 000000000..8b1f7ffac --- /dev/null +++ b/talk/base/constructormagic.h @@ -0,0 +1,55 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_CONSTRUCTORMAGIC_H_ +#define TALK_BASE_CONSTRUCTORMAGIC_H_ + +#define DISALLOW_ASSIGN(TypeName) \ + void operator=(const TypeName&) + +// A macro to disallow the evil copy constructor and operator= functions +// This should be used in the private: declarations for a class +#define DISALLOW_COPY_AND_ASSIGN(TypeName) \ + TypeName(const TypeName&); \ + DISALLOW_ASSIGN(TypeName) + +// Alternative, less-accurate legacy name. +#define DISALLOW_EVIL_CONSTRUCTORS(TypeName) \ + DISALLOW_COPY_AND_ASSIGN(TypeName) + +// A macro to disallow all the implicit constructors, namely the +// default constructor, copy constructor and operator= functions. +// +// This should be used in the private: declarations for a class +// that wants to prevent anyone from instantiating it. This is +// especially useful for classes containing only static methods. +#define DISALLOW_IMPLICIT_CONSTRUCTORS(TypeName) \ + TypeName(); \ + DISALLOW_EVIL_CONSTRUCTORS(TypeName) + + +#endif // TALK_BASE_CONSTRUCTORMAGIC_H_ diff --git a/talk/base/cpumonitor.cc b/talk/base/cpumonitor.cc new file mode 100644 index 000000000..0a7088740 --- /dev/null +++ b/talk/base/cpumonitor.cc @@ -0,0 +1,423 @@ +/* + * libjingle + * Copyright 2010 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/base/cpumonitor.h" + +#include + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/systeminfo.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" + +#ifdef WIN32 +#include "talk/base/win32.h" +#include +#endif + +#ifdef POSIX +#include +#endif + +#if defined(IOS) || defined(OSX) +#include +#include +#include +#include +#endif // defined(IOS) || defined(OSX) + +#if defined(LINUX) || defined(ANDROID) +#include +#include +#include +#include "talk/base/fileutils.h" +#include "talk/base/pathutils.h" +#endif // defined(LINUX) || defined(ANDROID) + +#if defined(IOS) || defined(OSX) +static uint64 TimeValueTToInt64(const time_value_t &time_value) { + return talk_base::kNumMicrosecsPerSec * time_value.seconds + + time_value.microseconds; +} +#endif // defined(IOS) || defined(OSX) + +// How CpuSampler works +// When threads switch, the time they spent is accumulated to system counters. +// The time can be treated as user, kernel or idle. +// user time is applications. +// kernel time is the OS, including the thread switching code itself. +// typically kernel time indicates IO. +// idle time is a process that wastes time when nothing is ready to run. +// +// User time is broken down by process (application). One of the applications +// is the current process. When you add up all application times, this is +// system time. If only your application is running, system time should be the +// same as process time. +// +// All cores contribute to these accumulators. A dual core process is able to +// process twice as many cycles as a single core. The actual code efficiency +// may be worse, due to contention, but the available cycles is exactly twice +// as many, and the cpu load will reflect the efficiency. Hyperthreads behave +// the same way. The load will reflect 200%, but the actual amount of work +// completed will be much less than a true dual core. +// +// Total available performance is the sum of all accumulators. +// If you tracked this for 1 second, it would essentially give you the clock +// rate - number of cycles per second. +// Speed step / Turbo Boost is not considered, so infact more processing time +// may be available. + +namespace talk_base { + +// Note Tests on Windows show 600 ms is minimum stable interval for Windows 7. +static const int32 kDefaultInterval = 950; // Slightly under 1 second. + +CpuSampler::CpuSampler() + : min_load_interval_(kDefaultInterval) +#ifdef WIN32 + , get_system_times_(NULL), + nt_query_system_information_(NULL), + force_fallback_(false) +#endif + { +} + +CpuSampler::~CpuSampler() { +} + +// Set minimum interval in ms between computing new load values. Default 950. +void CpuSampler::set_load_interval(int min_load_interval) { + min_load_interval_ = min_load_interval; +} + +bool CpuSampler::Init() { + sysinfo_.reset(new SystemInfo); + cpus_ = sysinfo_->GetMaxCpus(); + if (cpus_ == 0) { + return false; + } +#ifdef WIN32 + // Note that GetSystemTimes is available in Windows XP SP1 or later. + // http://msdn.microsoft.com/en-us/library/ms724400.aspx + // NtQuerySystemInformation is used as a fallback. + if (!force_fallback_) { + get_system_times_ = GetProcAddress(GetModuleHandle(L"kernel32.dll"), + "GetSystemTimes"); + } + nt_query_system_information_ = GetProcAddress(GetModuleHandle(L"ntdll.dll"), + "NtQuerySystemInformation"); + if ((get_system_times_ == NULL) && (nt_query_system_information_ == NULL)) { + return false; + } +#endif +#if defined(LINUX) || defined(ANDROID) + Pathname sname("/proc/stat"); + sfile_.reset(Filesystem::OpenFile(sname, "rb")); + if (!sfile_) { + LOG_ERR(LS_ERROR) << "open proc/stat failed:"; + return false; + } + if (!sfile_->DisableBuffering()) { + LOG_ERR(LS_ERROR) << "could not disable buffering for proc/stat"; + return false; + } +#endif // defined(LINUX) || defined(ANDROID) + GetProcessLoad(); // Initialize values. + GetSystemLoad(); + // Help next user call return valid data by recomputing load. + process_.prev_load_time_ = 0u; + system_.prev_load_time_ = 0u; + return true; +} + +float CpuSampler::UpdateCpuLoad(uint64 current_total_times, + uint64 current_cpu_times, + uint64 *prev_total_times, + uint64 *prev_cpu_times) { + float result = 0.f; + if (current_total_times < *prev_total_times || + current_cpu_times < *prev_cpu_times) { + LOG(LS_ERROR) << "Inconsistent time values are passed. ignored"; + } else { + const uint64 cpu_diff = current_cpu_times - *prev_cpu_times; + const uint64 total_diff = current_total_times - *prev_total_times; + result = (total_diff == 0ULL ? 0.f : + static_cast(1.0f * cpu_diff / total_diff)); + if (result > static_cast(cpus_)) { + result = static_cast(cpus_); + } + *prev_total_times = current_total_times; + *prev_cpu_times = current_cpu_times; + } + return result; +} + +float CpuSampler::GetSystemLoad() { + uint32 timenow = Time(); + int elapsed = static_cast(TimeDiff(timenow, system_.prev_load_time_)); + if (min_load_interval_ != 0 && system_.prev_load_time_ != 0u && + elapsed < min_load_interval_) { + return system_.prev_load_; + } +#ifdef WIN32 + uint64 total_times, cpu_times; + + typedef BOOL (_stdcall *GST_PROC)(LPFILETIME, LPFILETIME, LPFILETIME); + typedef NTSTATUS (WINAPI *QSI_PROC)(SYSTEM_INFORMATION_CLASS, + PVOID, ULONG, PULONG); + + GST_PROC get_system_times = reinterpret_cast(get_system_times_); + QSI_PROC nt_query_system_information = reinterpret_cast( + nt_query_system_information_); + + if (get_system_times) { + FILETIME idle_time, kernel_time, user_time; + if (!get_system_times(&idle_time, &kernel_time, &user_time)) { + LOG(LS_ERROR) << "::GetSystemTimes() failed: " << ::GetLastError(); + return 0.f; + } + // kernel_time includes Kernel idle time, so no need to + // include cpu_time as total_times + total_times = ToUInt64(kernel_time) + ToUInt64(user_time); + cpu_times = total_times - ToUInt64(idle_time); + + } else { + if (nt_query_system_information) { + ULONG returned_length = 0; + scoped_array processor_info( + new SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION[cpus_]); + nt_query_system_information( + ::SystemProcessorPerformanceInformation, + reinterpret_cast(processor_info.get()), + cpus_ * sizeof(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), + &returned_length); + + if (returned_length != + (cpus_ * sizeof(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION))) { + LOG(LS_ERROR) << "NtQuerySystemInformation has unexpected size"; + return 0.f; + } + + uint64 current_idle = 0; + uint64 current_kernel = 0; + uint64 current_user = 0; + for (int ix = 0; ix < cpus_; ++ix) { + current_idle += processor_info[ix].IdleTime.QuadPart; + current_kernel += processor_info[ix].UserTime.QuadPart; + current_user += processor_info[ix].KernelTime.QuadPart; + } + total_times = current_kernel + current_user; + cpu_times = total_times - current_idle; + } else { + return 0.f; + } + } +#endif // WIN32 + +#if defined(IOS) || defined(OSX) + host_cpu_load_info_data_t cpu_info; + mach_msg_type_number_t info_count = HOST_CPU_LOAD_INFO_COUNT; + if (KERN_SUCCESS != host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, + reinterpret_cast(&cpu_info), + &info_count)) { + LOG(LS_ERROR) << "::host_statistics() failed"; + return 0.f; + } + + const uint64 cpu_times = cpu_info.cpu_ticks[CPU_STATE_NICE] + + cpu_info.cpu_ticks[CPU_STATE_SYSTEM] + + cpu_info.cpu_ticks[CPU_STATE_USER]; + const uint64 total_times = cpu_times + cpu_info.cpu_ticks[CPU_STATE_IDLE]; +#endif // defined(IOS) || defined(OSX) + +#if defined(LINUX) || defined(ANDROID) + if (!sfile_) { + LOG(LS_ERROR) << "Invalid handle for proc/stat"; + return 0.f; + } + std::string statbuf; + sfile_->SetPosition(0); + if (!sfile_->ReadLine(&statbuf)) { + LOG_ERR(LS_ERROR) << "Could not read proc/stat file"; + return 0.f; + } + + unsigned long long user; + unsigned long long nice; + unsigned long long system; + unsigned long long idle; + if (sscanf(statbuf.c_str(), "cpu %Lu %Lu %Lu %Lu", + &user, &nice, + &system, &idle) != 4) { + LOG_ERR(LS_ERROR) << "Could not parse cpu info"; + return 0.f; + } + const uint64 cpu_times = nice + system + user; + const uint64 total_times = cpu_times + idle; +#endif // defined(LINUX) || defined(ANDROID) + system_.prev_load_time_ = timenow; + system_.prev_load_ = UpdateCpuLoad(total_times, + cpu_times * cpus_, + &system_.prev_total_times_, + &system_.prev_cpu_times_); + return system_.prev_load_; +} + +float CpuSampler::GetProcessLoad() { + uint32 timenow = Time(); + int elapsed = static_cast(TimeDiff(timenow, process_.prev_load_time_)); + if (min_load_interval_ != 0 && process_.prev_load_time_ != 0u && + elapsed < min_load_interval_) { + return process_.prev_load_; + } +#ifdef WIN32 + FILETIME current_file_time; + ::GetSystemTimeAsFileTime(¤t_file_time); + + FILETIME create_time, exit_time, kernel_time, user_time; + if (!::GetProcessTimes(::GetCurrentProcess(), + &create_time, &exit_time, &kernel_time, &user_time)) { + LOG(LS_ERROR) << "::GetProcessTimes() failed: " << ::GetLastError(); + return 0.f; + } + + const uint64 total_times = + ToUInt64(current_file_time) - ToUInt64(create_time); + const uint64 cpu_times = + (ToUInt64(kernel_time) + ToUInt64(user_time)); +#endif // WIN32 + +#ifdef POSIX + // Common to both OSX and Linux. + struct timeval tv; + gettimeofday(&tv, NULL); + const uint64 total_times = tv.tv_sec * kNumMicrosecsPerSec + tv.tv_usec; +#endif + +#if defined(IOS) || defined(OSX) + // Get live thread usage. + task_thread_times_info task_times_info; + mach_msg_type_number_t info_count = TASK_THREAD_TIMES_INFO_COUNT; + + if (KERN_SUCCESS != task_info(mach_task_self(), TASK_THREAD_TIMES_INFO, + reinterpret_cast(&task_times_info), + &info_count)) { + LOG(LS_ERROR) << "::task_info(TASK_THREAD_TIMES_INFO) failed"; + return 0.f; + } + + // Get terminated thread usage. + task_basic_info task_term_info; + info_count = TASK_BASIC_INFO_COUNT; + if (KERN_SUCCESS != task_info(mach_task_self(), TASK_BASIC_INFO, + reinterpret_cast(&task_term_info), + &info_count)) { + LOG(LS_ERROR) << "::task_info(TASK_BASIC_INFO) failed"; + return 0.f; + } + + const uint64 cpu_times = (TimeValueTToInt64(task_times_info.user_time) + + TimeValueTToInt64(task_times_info.system_time) + + TimeValueTToInt64(task_term_info.user_time) + + TimeValueTToInt64(task_term_info.system_time)); +#endif // defined(IOS) || defined(OSX) + +#if defined(LINUX) || defined(ANDROID) + rusage usage; + if (getrusage(RUSAGE_SELF, &usage) < 0) { + LOG_ERR(LS_ERROR) << "getrusage failed"; + return 0.f; + } + + const uint64 cpu_times = + (usage.ru_utime.tv_sec + usage.ru_stime.tv_sec) * kNumMicrosecsPerSec + + usage.ru_utime.tv_usec + usage.ru_stime.tv_usec; +#endif // defined(LINUX) || defined(ANDROID) + process_.prev_load_time_ = timenow; + process_.prev_load_ = UpdateCpuLoad(total_times, + cpu_times, + &process_.prev_total_times_, + &process_.prev_cpu_times_); + return process_.prev_load_; +} + +int CpuSampler::GetMaxCpus() const { + return cpus_; +} + +int CpuSampler::GetCurrentCpus() { + return sysinfo_->GetCurCpus(); +} + +/////////////////////////////////////////////////////////////////// +// Implementation of class CpuMonitor. +CpuMonitor::CpuMonitor(Thread* thread) + : monitor_thread_(thread) { +} + +CpuMonitor::~CpuMonitor() { + Stop(); +} + +void CpuMonitor::set_thread(Thread* thread) { + ASSERT(monitor_thread_ == NULL || monitor_thread_ == thread); + monitor_thread_ = thread; +} + +bool CpuMonitor::Start(int period_ms) { + if (!monitor_thread_ || !sampler_.Init()) return false; + + monitor_thread_->SignalQueueDestroyed.connect( + this, &CpuMonitor::OnMessageQueueDestroyed); + + period_ms_ = period_ms; + monitor_thread_->PostDelayed(period_ms_, this); + + return true; +} + +void CpuMonitor::Stop() { + if (monitor_thread_) { + monitor_thread_->Clear(this); + } +} + +void CpuMonitor::OnMessage(Message* msg) { + int max_cpus = sampler_.GetMaxCpus(); + int current_cpus = sampler_.GetCurrentCpus(); + float process_load = sampler_.GetProcessLoad(); + float system_load = sampler_.GetSystemLoad(); + SignalUpdate(current_cpus, max_cpus, process_load, system_load); + + if (monitor_thread_) { + monitor_thread_->PostDelayed(period_ms_, this); + } +} + +} // namespace talk_base diff --git a/talk/base/cpumonitor.h b/talk/base/cpumonitor.h new file mode 100644 index 000000000..e0c3655a0 --- /dev/null +++ b/talk/base/cpumonitor.h @@ -0,0 +1,140 @@ +/* + * libjingle + * Copyright 2010 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. + */ + +#ifndef TALK_BASE_CPUMONITOR_H_ +#define TALK_BASE_CPUMONITOR_H_ + +#include "talk/base/basictypes.h" +#include "talk/base/messagehandler.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" +#if defined(LINUX) || defined(ANDROID) +#include "talk/base/stream.h" +#endif // defined(LINUX) || defined(ANDROID) + +namespace talk_base { +class Thread; +class SystemInfo; + +struct CpuStats { + CpuStats() + : prev_total_times_(0), + prev_cpu_times_(0), + prev_load_(0.f), + prev_load_time_(0u) { + } + + uint64 prev_total_times_; + uint64 prev_cpu_times_; + float prev_load_; // Previous load value. + uint32 prev_load_time_; // Time previous load value was taken. +}; + +// CpuSampler samples the process and system load. +class CpuSampler { + public: + CpuSampler(); + ~CpuSampler(); + + // Initialize CpuSampler. Returns true if successful. + bool Init(); + + // Set minimum interval in ms between computing new load values. + // Default 950 ms. Set to 0 to disable interval. + void set_load_interval(int min_load_interval); + + // Return CPU load of current process as a float from 0 to 1. + float GetProcessLoad(); + + // Return CPU load of current process as a float from 0 to 1. + float GetSystemLoad(); + + // Return number of cpus. Includes hyperthreads. + int GetMaxCpus() const; + + // Return current number of cpus available to this process. + int GetCurrentCpus(); + + // For testing. Allows forcing of fallback to using NTDLL functions. + void set_force_fallback(bool fallback) { +#ifdef WIN32 + force_fallback_ = fallback; +#endif + } + + private: + float UpdateCpuLoad(uint64 current_total_times, + uint64 current_cpu_times, + uint64 *prev_total_times, + uint64 *prev_cpu_times); + CpuStats process_; + CpuStats system_; + int cpus_; + int min_load_interval_; // Minimum time between computing new load. + scoped_ptr sysinfo_; +#ifdef WIN32 + void* get_system_times_; + void* nt_query_system_information_; + bool force_fallback_; +#endif +#if defined(LINUX) || defined(ANDROID) + // File for reading /proc/stat + scoped_ptr sfile_; +#endif // defined(LINUX) || defined(ANDROID) +}; + +// CpuMonitor samples and signals the CPU load periodically. +class CpuMonitor + : public talk_base::MessageHandler, public sigslot::has_slots<> { + public: + explicit CpuMonitor(Thread* thread); + virtual ~CpuMonitor(); + void set_thread(Thread* thread); + + bool Start(int period_ms); + void Stop(); + // Signal parameters are current cpus, max cpus, process load and system load. + sigslot::signal4 SignalUpdate; + + protected: + // Override virtual method of parent MessageHandler. + virtual void OnMessage(talk_base::Message* msg); + // Clear the monitor thread and stop sending it messages if the thread goes + // away before our lifetime. + void OnMessageQueueDestroyed() { monitor_thread_ = NULL; } + + private: + Thread* monitor_thread_; + CpuSampler sampler_; + int period_ms_; + + DISALLOW_COPY_AND_ASSIGN(CpuMonitor); +}; + +} // namespace talk_base + +#endif // TALK_BASE_CPUMONITOR_H_ diff --git a/talk/base/cpumonitor_unittest.cc b/talk/base/cpumonitor_unittest.cc new file mode 100644 index 000000000..952b89e73 --- /dev/null +++ b/talk/base/cpumonitor_unittest.cc @@ -0,0 +1,402 @@ +/* + * libjingle + * Copyright 2010 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 +#include +#include + +#ifdef WIN32 +#include "talk/base/win32.h" +#endif + +#include "talk/base/cpumonitor.h" +#include "talk/base/flags.h" +#include "talk/base/gunit.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" +#include "talk/base/timing.h" + +namespace talk_base { + +static const int kMaxCpus = 1024; +static const int kSettleTime = 100; // Amount of time to between tests. +static const int kIdleTime = 500; // Amount of time to be idle in ms. +static const int kBusyTime = 1000; // Amount of time to be busy in ms. +static const int kLongInterval = 2000; // Interval longer than busy times + +class BusyThread : public talk_base::Thread { + public: + BusyThread(double load, double duration, double interval) : + load_(load), duration_(duration), interval_(interval) { + } + void Run() { + Timing time; + double busy_time = interval_ * load_ / 100.0; + for (;;) { + time.BusyWait(busy_time); + time.IdleWait(interval_ - busy_time); + if (duration_) { + duration_ -= interval_; + if (duration_ <= 0) { + break; + } + } + } + } + private: + double load_; + double duration_; + double interval_; +}; + +class CpuLoadListener : public sigslot::has_slots<> { + public: + CpuLoadListener() + : current_cpus_(0), + cpus_(0), + process_load_(.0f), + system_load_(.0f), + count_(0) { + } + + void OnCpuLoad(int current_cpus, int cpus, float proc_load, float sys_load) { + current_cpus_ = current_cpus; + cpus_ = cpus; + process_load_ = proc_load; + system_load_ = sys_load; + ++count_; + } + + int current_cpus() const { return current_cpus_; } + int cpus() const { return cpus_; } + float process_load() const { return process_load_; } + float system_load() const { return system_load_; } + int count() const { return count_; } + + private: + int current_cpus_; + int cpus_; + float process_load_; + float system_load_; + int count_; +}; + +// Set affinity (which cpu to run on), but respecting FLAG_affinity: +// -1 means no affinity - run on whatever cpu is available. +// 0 .. N means run on specific cpu. The tool will create N threads and call +// SetThreadAffinity on 0 to N - 1 as cpu. FLAG_affinity sets the first cpu +// so the range becomes affinity to affinity + N - 1 +// Note that this function affects Windows scheduling, effectively giving +// the thread with affinity for a specified CPU more priority on that CPU. +bool SetThreadAffinity(BusyThread* t, int cpu, int affinity) { +#ifdef WIN32 + if (affinity >= 0) { + return ::SetThreadAffinityMask(t->GetHandle(), + 1 << (cpu + affinity)) != FALSE; + } +#endif + return true; +} + +bool SetThreadPriority(BusyThread* t, int prio) { + if (!prio) { + return true; + } + bool ok = t->SetPriority(static_cast(prio)); + if (!ok) { + std::cout << "Error setting thread priority." << std::endl; + } + return ok; +} + +int CpuLoad(double cpuload, double duration, int numthreads, + int priority, double interval, int affinity) { + int ret = 0; + std::vector threads; + for (int i = 0; i < numthreads; ++i) { + threads.push_back(new BusyThread(cpuload, duration, interval)); + // NOTE(fbarchard): Priority must be done before Start. + if (!SetThreadPriority(threads[i], priority) || + !threads[i]->Start() || + !SetThreadAffinity(threads[i], i, affinity)) { + ret = 1; + break; + } + } + // Wait on each thread + if (ret == 0) { + for (int i = 0; i < numthreads; ++i) { + threads[i]->Stop(); + } + } + + for (int i = 0; i < numthreads; ++i) { + delete threads[i]; + } + return ret; +} + +// Make 2 CPUs busy +static void CpuTwoBusyLoop(int busytime) { + CpuLoad(100.0, busytime / 1000.0, 2, 1, 0.050, -1); +} + +// Make 1 CPUs busy +static void CpuBusyLoop(int busytime) { + CpuLoad(100.0, busytime / 1000.0, 1, 1, 0.050, -1); +} + +// Make 1 use half CPU time. +static void CpuHalfBusyLoop(int busytime) { + CpuLoad(50.0, busytime / 1000.0, 1, 1, 0.050, -1); +} + +void TestCpuSampler(bool test_proc, bool test_sys, bool force_fallback) { + CpuSampler sampler; + sampler.set_force_fallback(force_fallback); + EXPECT_TRUE(sampler.Init()); + sampler.set_load_interval(100); + int cpus = sampler.GetMaxCpus(); + + // Test1: CpuSampler under idle situation. + Thread::SleepMs(kSettleTime); + sampler.GetProcessLoad(); + sampler.GetSystemLoad(); + + Thread::SleepMs(kIdleTime); + + float proc_idle = 0.f, sys_idle = 0.f; + if (test_proc) { + proc_idle = sampler.GetProcessLoad(); + } + if (test_sys) { + sys_idle = sampler.GetSystemLoad(); + } + if (test_proc) { + LOG(LS_INFO) << "ProcessLoad Idle: " + << std::setiosflags(std::ios_base::fixed) + << std::setprecision(2) << std::setw(6) << proc_idle; + EXPECT_GE(proc_idle, 0.f); + EXPECT_LE(proc_idle, static_cast(cpus)); + } + if (test_sys) { + LOG(LS_INFO) << "SystemLoad Idle: " + << std::setiosflags(std::ios_base::fixed) + << std::setprecision(2) << std::setw(6) << sys_idle; + EXPECT_GE(sys_idle, 0.f); + EXPECT_LE(sys_idle, static_cast(cpus)); + } + + // Test2: CpuSampler with main process at 50% busy. + Thread::SleepMs(kSettleTime); + sampler.GetProcessLoad(); + sampler.GetSystemLoad(); + + CpuHalfBusyLoop(kBusyTime); + + float proc_halfbusy = 0.f, sys_halfbusy = 0.f; + if (test_proc) { + proc_halfbusy = sampler.GetProcessLoad(); + } + if (test_sys) { + sys_halfbusy = sampler.GetSystemLoad(); + } + if (test_proc) { + LOG(LS_INFO) << "ProcessLoad Halfbusy: " + << std::setiosflags(std::ios_base::fixed) + << std::setprecision(2) << std::setw(6) << proc_halfbusy; + EXPECT_GE(proc_halfbusy, 0.f); + EXPECT_LE(proc_halfbusy, static_cast(cpus)); + } + if (test_sys) { + LOG(LS_INFO) << "SystemLoad Halfbusy: " + << std::setiosflags(std::ios_base::fixed) + << std::setprecision(2) << std::setw(6) << sys_halfbusy; + EXPECT_GE(sys_halfbusy, 0.f); + EXPECT_LE(sys_halfbusy, static_cast(cpus)); + } + + // Test3: CpuSampler with main process busy. + Thread::SleepMs(kSettleTime); + sampler.GetProcessLoad(); + sampler.GetSystemLoad(); + + CpuBusyLoop(kBusyTime); + + float proc_busy = 0.f, sys_busy = 0.f; + if (test_proc) { + proc_busy = sampler.GetProcessLoad(); + } + if (test_sys) { + sys_busy = sampler.GetSystemLoad(); + } + if (test_proc) { + LOG(LS_INFO) << "ProcessLoad Busy: " + << std::setiosflags(std::ios_base::fixed) + << std::setprecision(2) << std::setw(6) << proc_busy; + EXPECT_GE(proc_busy, 0.f); + EXPECT_LE(proc_busy, static_cast(cpus)); + } + if (test_sys) { + LOG(LS_INFO) << "SystemLoad Busy: " + << std::setiosflags(std::ios_base::fixed) + << std::setprecision(2) << std::setw(6) << sys_busy; + EXPECT_GE(sys_busy, 0.f); + EXPECT_LE(sys_busy, static_cast(cpus)); + } + + // Test4: CpuSampler with 2 cpus process busy. + if (cpus >= 2) { + Thread::SleepMs(kSettleTime); + sampler.GetProcessLoad(); + sampler.GetSystemLoad(); + + CpuTwoBusyLoop(kBusyTime); + + float proc_twobusy = 0.f, sys_twobusy = 0.f; + if (test_proc) { + proc_twobusy = sampler.GetProcessLoad(); + } + if (test_sys) { + sys_twobusy = sampler.GetSystemLoad(); + } + if (test_proc) { + LOG(LS_INFO) << "ProcessLoad 2 CPU Busy:" + << std::setiosflags(std::ios_base::fixed) + << std::setprecision(2) << std::setw(6) << proc_twobusy; + EXPECT_GE(proc_twobusy, 0.f); + EXPECT_LE(proc_twobusy, static_cast(cpus)); + } + if (test_sys) { + LOG(LS_INFO) << "SystemLoad 2 CPU Busy: " + << std::setiosflags(std::ios_base::fixed) + << std::setprecision(2) << std::setw(6) << sys_twobusy; + EXPECT_GE(sys_twobusy, 0.f); + EXPECT_LE(sys_twobusy, static_cast(cpus)); + } + } + + // Test5: CpuSampler with idle process after being busy. + Thread::SleepMs(kSettleTime); + sampler.GetProcessLoad(); + sampler.GetSystemLoad(); + + Thread::SleepMs(kIdleTime); + + if (test_proc) { + proc_idle = sampler.GetProcessLoad(); + } + if (test_sys) { + sys_idle = sampler.GetSystemLoad(); + } + if (test_proc) { + LOG(LS_INFO) << "ProcessLoad Idle: " + << std::setiosflags(std::ios_base::fixed) + << std::setprecision(2) << std::setw(6) << proc_idle; + EXPECT_GE(proc_idle, 0.f); + EXPECT_LE(proc_idle, proc_busy); + } + if (test_sys) { + LOG(LS_INFO) << "SystemLoad Idle: " + << std::setiosflags(std::ios_base::fixed) + << std::setprecision(2) << std::setw(6) << sys_idle; + EXPECT_GE(sys_idle, 0.f); + EXPECT_LE(sys_idle, static_cast(cpus)); + } +} + +TEST(CpuMonitorTest, TestCpus) { + CpuSampler sampler; + EXPECT_TRUE(sampler.Init()); + int current_cpus = sampler.GetCurrentCpus(); + int cpus = sampler.GetMaxCpus(); + LOG(LS_INFO) << "Current Cpus: " << std::setw(9) << current_cpus; + LOG(LS_INFO) << "Maximum Cpus: " << std::setw(9) << cpus; + EXPECT_GT(cpus, 0); + EXPECT_LE(cpus, kMaxCpus); + EXPECT_GT(current_cpus, 0); + EXPECT_LE(current_cpus, cpus); +} + +#ifdef WIN32 +// Tests overall system CpuSampler using legacy OS fallback code if applicable. +TEST(CpuMonitorTest, TestGetSystemLoadForceFallback) { + TestCpuSampler(false, true, true); +} +#endif + +// Tests both process and system functions in use at same time. +TEST(CpuMonitorTest, TestGetBothLoad) { + TestCpuSampler(true, true, false); +} + +// Tests a query less than the interval produces the same value. +TEST(CpuMonitorTest, TestInterval) { + CpuSampler sampler; + EXPECT_TRUE(sampler.Init()); + + // Test1: Set interval to large value so sampler will not update. + sampler.set_load_interval(kLongInterval); + + sampler.GetProcessLoad(); + sampler.GetSystemLoad(); + + float proc_orig = sampler.GetProcessLoad(); + float sys_orig = sampler.GetSystemLoad(); + + Thread::SleepMs(kIdleTime); + + float proc_halftime = sampler.GetProcessLoad(); + float sys_halftime = sampler.GetSystemLoad(); + + EXPECT_EQ(proc_orig, proc_halftime); + EXPECT_EQ(sys_orig, sys_halftime); +} + +TEST(CpuMonitorTest, TestCpuMonitor) { + CpuMonitor monitor(Thread::Current()); + CpuLoadListener listener; + monitor.SignalUpdate.connect(&listener, &CpuLoadListener::OnCpuLoad); + EXPECT_TRUE(monitor.Start(10)); + Thread::Current()->ProcessMessages(50); + EXPECT_GT(listener.count(), 2); // We have checked cpu load more than twice. + EXPECT_GT(listener.current_cpus(), 0); + EXPECT_GT(listener.cpus(), 0); + EXPECT_GE(listener.process_load(), .0f); + EXPECT_GE(listener.system_load(), .0f); + + monitor.Stop(); + // Wait 20 ms to ake sure all signals are delivered. + Thread::Current()->ProcessMessages(20); + int old_count = listener.count(); + Thread::Current()->ProcessMessages(20); + // Verfy no more siganls. + EXPECT_EQ(old_count, listener.count()); +} + +} // namespace talk_base diff --git a/talk/base/crc32.cc b/talk/base/crc32.cc new file mode 100644 index 000000000..82998ee2e --- /dev/null +++ b/talk/base/crc32.cc @@ -0,0 +1,69 @@ +/* + * libjingle + * Copyright 2012, 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/base/crc32.h" + +#include "talk/base/basicdefs.h" + +namespace talk_base { + +// This implementation is based on the sample implementation in RFC 1952. + +// CRC32 polynomial, in reversed form. +// See RFC 1952, or http://en.wikipedia.org/wiki/Cyclic_redundancy_check +static const uint32 kCrc32Polynomial = 0xEDB88320; +static uint32 kCrc32Table[256] = { 0 }; + +static void EnsureCrc32TableInited() { + if (kCrc32Table[ARRAY_SIZE(kCrc32Table) - 1]) + return; // already inited + for (uint32 i = 0; i < ARRAY_SIZE(kCrc32Table); ++i) { + uint32 c = i; + for (size_t j = 0; j < 8; ++j) { + if (c & 1) { + c = kCrc32Polynomial ^ (c >> 1); + } else { + c >>= 1; + } + } + kCrc32Table[i] = c; + } +} + +uint32 UpdateCrc32(uint32 start, const void* buf, size_t len) { + EnsureCrc32TableInited(); + + uint32 c = start ^ 0xFFFFFFFF; + const uint8* u = static_cast(buf); + for (size_t i = 0; i < len; ++i) { + c = kCrc32Table[(c ^ u[i]) & 0xFF] ^ (c >> 8); + } + return c ^ 0xFFFFFFFF; +} + +} // namespace talk_base + diff --git a/talk/base/crc32.h b/talk/base/crc32.h new file mode 100644 index 000000000..144158beb --- /dev/null +++ b/talk/base/crc32.h @@ -0,0 +1,51 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_BASE_CRC32_H_ +#define TALK_BASE_CRC32_H_ + +#include + +#include "talk/base/basictypes.h" + +namespace talk_base { + +// Updates a CRC32 checksum with |len| bytes from |buf|. |initial| holds the +// checksum result from the previous update; for the first call, it should be 0. +uint32 UpdateCrc32(uint32 initial, const void* buf, size_t len); + +// Computes a CRC32 checksum using |len| bytes from |buf|. +inline uint32 ComputeCrc32(const void* buf, size_t len) { + return UpdateCrc32(0, buf, len); +} +inline uint32 ComputeCrc32(const std::string& str) { + return ComputeCrc32(str.c_str(), str.size()); +} + +} // namespace talk_base + +#endif // TALK_BASE_CRC32_H_ diff --git a/talk/base/crc32_unittest.cc b/talk/base/crc32_unittest.cc new file mode 100644 index 000000000..24333d3f1 --- /dev/null +++ b/talk/base/crc32_unittest.cc @@ -0,0 +1,52 @@ +/* + * libjingle + * Copyright 2012, 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/base/crc32.h" +#include "talk/base/gunit.h" + +#include + +namespace talk_base { + +TEST(Crc32Test, TestBasic) { + EXPECT_EQ(0U, ComputeCrc32("")); + EXPECT_EQ(0x352441C2U, ComputeCrc32("abc")); + EXPECT_EQ(0x171A3F5FU, + ComputeCrc32("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")); +} + +TEST(Crc32Test, TestMultipleUpdates) { + std::string input = + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; + uint32 c = 0; + for (size_t i = 0; i < input.size(); ++i) { + c = UpdateCrc32(c, &input[i], 1); + } + EXPECT_EQ(0x171A3F5FU, c); +} + +} // namespace talk_base diff --git a/talk/base/criticalsection.h b/talk/base/criticalsection.h new file mode 100644 index 000000000..c6ffbc0d9 --- /dev/null +++ b/talk/base/criticalsection.h @@ -0,0 +1,196 @@ +/* + * libjingle + * Copyright 2004, 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. + */ + +#ifndef TALK_BASE_CRITICALSECTION_H__ +#define TALK_BASE_CRITICALSECTION_H__ + +#include "talk/base/constructormagic.h" + +#ifdef WIN32 +#include "talk/base/win32.h" +#endif + +#ifdef POSIX +#include +#endif + +#ifdef _DEBUG +#define CS_TRACK_OWNER 1 +#endif // _DEBUG + +#if CS_TRACK_OWNER +#define TRACK_OWNER(x) x +#else // !CS_TRACK_OWNER +#define TRACK_OWNER(x) +#endif // !CS_TRACK_OWNER + +namespace talk_base { + +#ifdef WIN32 +class CriticalSection { + public: + CriticalSection() { + InitializeCriticalSection(&crit_); + // Windows docs say 0 is not a valid thread id + TRACK_OWNER(thread_ = 0); + } + ~CriticalSection() { + DeleteCriticalSection(&crit_); + } + void Enter() { + EnterCriticalSection(&crit_); + TRACK_OWNER(thread_ = GetCurrentThreadId()); + } + bool TryEnter() { + if (TryEnterCriticalSection(&crit_) != FALSE) { + TRACK_OWNER(thread_ = GetCurrentThreadId()); + return true; + } + return false; + } + void Leave() { + TRACK_OWNER(thread_ = 0); + LeaveCriticalSection(&crit_); + } + +#if CS_TRACK_OWNER + bool CurrentThreadIsOwner() const { return thread_ == GetCurrentThreadId(); } +#endif // CS_TRACK_OWNER + + private: + CRITICAL_SECTION crit_; + TRACK_OWNER(DWORD thread_); // The section's owning thread id +}; +#endif // WIN32 + +#ifdef POSIX +class CriticalSection { + public: + CriticalSection() { + pthread_mutexattr_t mutex_attribute; + pthread_mutexattr_init(&mutex_attribute); + pthread_mutexattr_settype(&mutex_attribute, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&mutex_, &mutex_attribute); + pthread_mutexattr_destroy(&mutex_attribute); + TRACK_OWNER(thread_ = 0); + } + ~CriticalSection() { + pthread_mutex_destroy(&mutex_); + } + void Enter() { + pthread_mutex_lock(&mutex_); + TRACK_OWNER(thread_ = pthread_self()); + } + bool TryEnter() { + if (pthread_mutex_trylock(&mutex_) == 0) { + TRACK_OWNER(thread_ = pthread_self()); + return true; + } + return false; + } + void Leave() { + TRACK_OWNER(thread_ = 0); + pthread_mutex_unlock(&mutex_); + } + +#if CS_TRACK_OWNER + bool CurrentThreadIsOwner() const { return pthread_equal(thread_, pthread_self()); } +#endif // CS_TRACK_OWNER + + private: + pthread_mutex_t mutex_; + TRACK_OWNER(pthread_t thread_); +}; +#endif // POSIX + +// CritScope, for serializing execution through a scope. +class CritScope { + public: + explicit CritScope(CriticalSection *pcrit) { + pcrit_ = pcrit; + pcrit_->Enter(); + } + ~CritScope() { + pcrit_->Leave(); + } + private: + CriticalSection *pcrit_; + DISALLOW_COPY_AND_ASSIGN(CritScope); +}; + +// Tries to lock a critical section on construction via +// CriticalSection::TryEnter, and unlocks on destruction if the +// lock was taken. Never blocks. +// +// IMPORTANT: Unlike CritScope, the lock may not be owned by this thread in +// subsequent code. Users *must* check locked() to determine if the +// lock was taken. If you're not calling locked(), you're doing it wrong! +class TryCritScope { + public: + explicit TryCritScope(CriticalSection *pcrit) { + pcrit_ = pcrit; + locked_ = pcrit_->TryEnter(); + } + ~TryCritScope() { + if (locked_) { + pcrit_->Leave(); + } + } + bool locked() const { + return locked_; + } + private: + CriticalSection *pcrit_; + bool locked_; + DISALLOW_COPY_AND_ASSIGN(TryCritScope); +}; + +// TODO: Move this to atomicops.h, which can't be done easily because of +// complex compile rules. +class AtomicOps { + public: +#ifdef WIN32 + // Assumes sizeof(int) == sizeof(LONG), which it is on Win32 and Win64. + static int Increment(int* i) { + return ::InterlockedIncrement(reinterpret_cast(i)); + } + static int Decrement(int* i) { + return ::InterlockedDecrement(reinterpret_cast(i)); + } +#else + static int Increment(int* i) { + return __sync_add_and_fetch(i, 1); + } + static int Decrement(int* i) { + return __sync_sub_and_fetch(i, 1); + } +#endif +}; + +} // namespace talk_base + +#endif // TALK_BASE_CRITICALSECTION_H__ diff --git a/talk/base/cryptstring.h b/talk/base/cryptstring.h new file mode 100644 index 000000000..eb39be229 --- /dev/null +++ b/talk/base/cryptstring.h @@ -0,0 +1,198 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _TALK_BASE_CRYPTSTRING_H_ +#define _TALK_BASE_CRYPTSTRING_H_ + +#include +#include +#include +#include "talk/base/linked_ptr.h" +#include "talk/base/scoped_ptr.h" + +namespace talk_base { + +class CryptStringImpl { +public: + virtual ~CryptStringImpl() {} + virtual size_t GetLength() const = 0; + virtual void CopyTo(char * dest, bool nullterminate) const = 0; + virtual std::string UrlEncode() const = 0; + virtual CryptStringImpl * Copy() const = 0; + virtual void CopyRawTo(std::vector * dest) const = 0; +}; + +class EmptyCryptStringImpl : public CryptStringImpl { +public: + virtual ~EmptyCryptStringImpl() {} + virtual size_t GetLength() const { return 0; } + virtual void CopyTo(char * dest, bool nullterminate) const { + if (nullterminate) { + *dest = '\0'; + } + } + virtual std::string UrlEncode() const { return ""; } + virtual CryptStringImpl * Copy() const { return new EmptyCryptStringImpl(); } + virtual void CopyRawTo(std::vector * dest) const { + dest->clear(); + } +}; + +class CryptString { +public: + CryptString() : impl_(new EmptyCryptStringImpl()) {} + size_t GetLength() const { return impl_->GetLength(); } + void CopyTo(char * dest, bool nullterminate) const { impl_->CopyTo(dest, nullterminate); } + CryptString(const CryptString & other) : impl_(other.impl_->Copy()) {} + explicit CryptString(const CryptStringImpl & impl) : impl_(impl.Copy()) {} + CryptString & operator=(const CryptString & other) { + if (this != &other) { + impl_.reset(other.impl_->Copy()); + } + return *this; + } + void Clear() { impl_.reset(new EmptyCryptStringImpl()); } + std::string UrlEncode() const { return impl_->UrlEncode(); } + void CopyRawTo(std::vector * dest) const { + return impl_->CopyRawTo(dest); + } + +private: + scoped_ptr impl_; +}; + + +// Used for constructing strings where a password is involved and we +// need to ensure that we zero memory afterwards +class FormatCryptString { +public: + FormatCryptString() { + storage_ = new char[32]; + capacity_ = 32; + length_ = 0; + storage_[0] = 0; + } + + void Append(const std::string & text) { + Append(text.data(), text.length()); + } + + void Append(const char * data, size_t length) { + EnsureStorage(length_ + length + 1); + memcpy(storage_ + length_, data, length); + length_ += length; + storage_[length_] = '\0'; + } + + void Append(const CryptString * password) { + size_t len = password->GetLength(); + EnsureStorage(length_ + len + 1); + password->CopyTo(storage_ + length_, true); + length_ += len; + } + + size_t GetLength() { + return length_; + } + + const char * GetData() { + return storage_; + } + + + // Ensures storage of at least n bytes + void EnsureStorage(size_t n) { + if (capacity_ >= n) { + return; + } + + size_t old_capacity = capacity_; + char * old_storage = storage_; + + for (;;) { + capacity_ *= 2; + if (capacity_ >= n) + break; + } + + storage_ = new char[capacity_]; + + if (old_capacity) { + memcpy(storage_, old_storage, length_); + + // zero memory in a way that an optimizer won't optimize it out + old_storage[0] = 0; + for (size_t i = 1; i < old_capacity; i++) { + old_storage[i] = old_storage[i - 1]; + } + delete[] old_storage; + } + } + + ~FormatCryptString() { + if (capacity_) { + storage_[0] = 0; + for (size_t i = 1; i < capacity_; i++) { + storage_[i] = storage_[i - 1]; + } + } + delete[] storage_; + } +private: + char * storage_; + size_t capacity_; + size_t length_; +}; + +class InsecureCryptStringImpl : public CryptStringImpl { + public: + std::string& password() { return password_; } + const std::string& password() const { return password_; } + + virtual ~InsecureCryptStringImpl() {} + virtual size_t GetLength() const { return password_.size(); } + virtual void CopyTo(char * dest, bool nullterminate) const { + memcpy(dest, password_.data(), password_.size()); + if (nullterminate) dest[password_.size()] = 0; + } + virtual std::string UrlEncode() const { return password_; } + virtual CryptStringImpl * Copy() const { + InsecureCryptStringImpl * copy = new InsecureCryptStringImpl; + copy->password() = password_; + return copy; + } + virtual void CopyRawTo(std::vector * dest) const { + dest->resize(password_.size()); + memcpy(&dest->front(), password_.data(), password_.size()); + } + private: + std::string password_; +}; + +} + +#endif // _TALK_BASE_CRYPTSTRING_H_ diff --git a/talk/base/dbus.cc b/talk/base/dbus.cc new file mode 100644 index 000000000..8e071c7c7 --- /dev/null +++ b/talk/base/dbus.cc @@ -0,0 +1,409 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifdef HAVE_DBUS_GLIB + +#include "talk/base/dbus.h" + +#include + +#include "talk/base/logging.h" +#include "talk/base/thread.h" + +namespace talk_base { + +// Avoid static object construction/destruction on startup/shutdown. +static pthread_once_t g_dbus_init_once = PTHREAD_ONCE_INIT; +static LibDBusGlibSymbolTable *g_dbus_symbol = NULL; + +// Releases DBus-Glib symbols. +static void ReleaseDBusGlibSymbol() { + if (g_dbus_symbol != NULL) { + delete g_dbus_symbol; + g_dbus_symbol = NULL; + } +} + +// Loads DBus-Glib symbols. +static void InitializeDBusGlibSymbol() { + // This is thread safe. + if (NULL == g_dbus_symbol) { + g_dbus_symbol = new LibDBusGlibSymbolTable(); + + // Loads dbus-glib + if (NULL == g_dbus_symbol || !g_dbus_symbol->Load()) { + LOG(LS_WARNING) << "Failed to load dbus-glib symbol table."; + ReleaseDBusGlibSymbol(); + } else { + // Nothing we can do if atexit() failed. Just ignore its returned value. + atexit(ReleaseDBusGlibSymbol); + } + } +} + +inline static LibDBusGlibSymbolTable *GetSymbols() { + return DBusMonitor::GetDBusGlibSymbolTable(); +} + +// Implementation of class DBusSigMessageData +DBusSigMessageData::DBusSigMessageData(DBusMessage *message) + : TypedMessageData(message) { + GetSymbols()->dbus_message_ref()(data()); +} + +DBusSigMessageData::~DBusSigMessageData() { + GetSymbols()->dbus_message_unref()(data()); +} + +// Implementation of class DBusSigFilter + +// Builds a DBus filter string from given DBus path, interface and member. +std::string DBusSigFilter::BuildFilterString(const std::string &path, + const std::string &interface, + const std::string &member) { + std::string ret(DBUS_TYPE "='" DBUS_SIGNAL "'"); + if (!path.empty()) { + ret += ("," DBUS_PATH "='"); + ret += path; + ret += "'"; + } + if (!interface.empty()) { + ret += ("," DBUS_INTERFACE "='"); + ret += interface; + ret += "'"; + } + if (!member.empty()) { + ret += ("," DBUS_MEMBER "='"); + ret += member; + ret += "'"; + } + return ret; +} + +// Forwards the message to the given instance. +DBusHandlerResult DBusSigFilter::DBusCallback(DBusConnection *dbus_conn, + DBusMessage *message, + void *instance) { + ASSERT(instance); + if (instance) { + return static_cast(instance)->Callback(message); + } + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +// Posts a message to caller thread. +DBusHandlerResult DBusSigFilter::Callback(DBusMessage *message) { + if (caller_thread_) { + caller_thread_->Post(this, DSM_SIGNAL, new DBusSigMessageData(message)); + } + // Don't "eat" the message here. Let it pop up. + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +// From MessageHandler. +void DBusSigFilter::OnMessage(Message *message) { + if (message != NULL && DSM_SIGNAL == message->message_id) { + DBusSigMessageData *msg = + static_cast(message->pdata); + if (msg) { + ProcessSignal(msg->data()); + delete msg; + } + } +} + +// Definition of private class DBusMonitoringThread. +// It creates a worker-thread to listen signals on DBus. The worker-thread will +// be running in a priate GMainLoop forever until either Stop() has been invoked +// or it hits an error. +class DBusMonitor::DBusMonitoringThread : public talk_base::Thread { + public: + explicit DBusMonitoringThread(DBusMonitor *monitor, + GMainContext *context, + GMainLoop *mainloop, + std::vector *filter_list) + : monitor_(monitor), + context_(context), + mainloop_(mainloop), + connection_(NULL), + idle_source_(NULL), + filter_list_(filter_list) { + ASSERT(monitor_); + ASSERT(context_); + ASSERT(mainloop_); + ASSERT(filter_list_); + } + + // Override virtual method of Thread. Context: worker-thread. + virtual void Run() { + ASSERT(NULL == connection_); + + // Setup DBus connection and start monitoring. + monitor_->OnMonitoringStatusChanged(DMS_INITIALIZING); + if (!Setup()) { + LOG(LS_ERROR) << "DBus monitoring setup failed."; + monitor_->OnMonitoringStatusChanged(DMS_FAILED); + CleanUp(); + return; + } + monitor_->OnMonitoringStatusChanged(DMS_RUNNING); + g_main_loop_run(mainloop_); + monitor_->OnMonitoringStatusChanged(DMS_STOPPED); + + // Done normally. Clean up DBus connection. + CleanUp(); + return; + } + + // Override virtual method of Thread. Context: caller-thread. + virtual void Stop() { + ASSERT(NULL == idle_source_); + // Add an idle source and let the gmainloop quit on idle. + idle_source_ = g_idle_source_new(); + if (idle_source_) { + g_source_set_callback(idle_source_, &Idle, this, NULL); + g_source_attach(idle_source_, context_); + } else { + LOG(LS_ERROR) << "g_idle_source_new() failed."; + QuitGMainloop(); // Try to quit anyway. + } + + Thread::Stop(); // Wait for the thread. + } + + private: + // Registers all DBus filters. + void RegisterAllFilters() { + ASSERT(NULL != GetSymbols()->dbus_g_connection_get_connection()( + connection_)); + + for (std::vector::iterator it = filter_list_->begin(); + it != filter_list_->end(); ++it) { + DBusSigFilter *filter = (*it); + if (!filter) { + LOG(LS_ERROR) << "DBusSigFilter list corrupted."; + continue; + } + + GetSymbols()->dbus_bus_add_match()( + GetSymbols()->dbus_g_connection_get_connection()(connection_), + filter->filter().c_str(), NULL); + + if (!GetSymbols()->dbus_connection_add_filter()( + GetSymbols()->dbus_g_connection_get_connection()(connection_), + &DBusSigFilter::DBusCallback, filter, NULL)) { + LOG(LS_ERROR) << "dbus_connection_add_filter() failed." + << "Filter: " << filter->filter(); + continue; + } + } + } + + // Unregisters all DBus filters. + void UnRegisterAllFilters() { + ASSERT(NULL != GetSymbols()->dbus_g_connection_get_connection()( + connection_)); + + for (std::vector::iterator it = filter_list_->begin(); + it != filter_list_->end(); ++it) { + DBusSigFilter *filter = (*it); + if (!filter) { + LOG(LS_ERROR) << "DBusSigFilter list corrupted."; + continue; + } + GetSymbols()->dbus_connection_remove_filter()( + GetSymbols()->dbus_g_connection_get_connection()(connection_), + &DBusSigFilter::DBusCallback, filter); + } + } + + // Sets up the monitoring thread. + bool Setup() { + g_main_context_push_thread_default(context_); + + // Start connection to dbus. + // If dbus daemon is not running, returns false immediately. + connection_ = GetSymbols()->dbus_g_bus_get_private()(monitor_->type_, + context_, NULL); + if (NULL == connection_) { + LOG(LS_ERROR) << "dbus_g_bus_get_private() unable to get connection."; + return false; + } + if (NULL == GetSymbols()->dbus_g_connection_get_connection()(connection_)) { + LOG(LS_ERROR) << "dbus_g_connection_get_connection() returns NULL. " + << "DBus daemon is probably not running."; + return false; + } + + // Application don't exit if DBus daemon die. + GetSymbols()->dbus_connection_set_exit_on_disconnect()( + GetSymbols()->dbus_g_connection_get_connection()(connection_), FALSE); + + // Connect all filters. + RegisterAllFilters(); + + return true; + } + + // Cleans up the monitoring thread. + void CleanUp() { + if (idle_source_) { + // We did an attach() with the GSource, so we need to destroy() it. + g_source_destroy(idle_source_); + // We need to unref() the GSource to end the last reference we got. + g_source_unref(idle_source_); + idle_source_ = NULL; + } + if (connection_) { + if (GetSymbols()->dbus_g_connection_get_connection()(connection_)) { + UnRegisterAllFilters(); + GetSymbols()->dbus_connection_close()( + GetSymbols()->dbus_g_connection_get_connection()(connection_)); + } + GetSymbols()->dbus_g_connection_unref()(connection_); + connection_ = NULL; + } + g_main_loop_unref(mainloop_); + mainloop_ = NULL; + g_main_context_unref(context_); + context_ = NULL; + } + + // Handles callback on Idle. We only add this source when ready to stop. + static gboolean Idle(gpointer data) { + static_cast(data)->QuitGMainloop(); + return TRUE; + } + + // We only hit this when ready to quit. + void QuitGMainloop() { + g_main_loop_quit(mainloop_); + } + + DBusMonitor *monitor_; + + GMainContext *context_; + GMainLoop *mainloop_; + DBusGConnection *connection_; + GSource *idle_source_; + + std::vector *filter_list_; +}; + +// Implementation of class DBusMonitor + +// Returns DBus-Glib symbol handle. Initialize it first if hasn't. +LibDBusGlibSymbolTable *DBusMonitor::GetDBusGlibSymbolTable() { + // This is multi-thread safe. + pthread_once(&g_dbus_init_once, InitializeDBusGlibSymbol); + + return g_dbus_symbol; +}; + +// Creates an instance of DBusMonitor +DBusMonitor *DBusMonitor::Create(DBusBusType type) { + if (NULL == DBusMonitor::GetDBusGlibSymbolTable()) { + return NULL; + } + return new DBusMonitor(type); +} + +DBusMonitor::DBusMonitor(DBusBusType type) + : type_(type), + status_(DMS_NOT_INITIALIZED), + monitoring_thread_(NULL) { + ASSERT(type_ == DBUS_BUS_SYSTEM || type_ == DBUS_BUS_SESSION); +} + +DBusMonitor::~DBusMonitor() { + StopMonitoring(); +} + +bool DBusMonitor::AddFilter(DBusSigFilter *filter) { + if (monitoring_thread_) { + return false; + } + if (!filter) { + return false; + } + filter_list_.push_back(filter); + return true; +} + +bool DBusMonitor::StartMonitoring() { + if (!monitoring_thread_) { + g_type_init(); + g_thread_init(NULL); + GetSymbols()->dbus_g_thread_init()(); + + GMainContext *context = g_main_context_new(); + if (NULL == context) { + LOG(LS_ERROR) << "g_main_context_new() failed."; + return false; + } + + GMainLoop *mainloop = g_main_loop_new(context, FALSE); + if (NULL == mainloop) { + LOG(LS_ERROR) << "g_main_loop_new() failed."; + g_main_context_unref(context); + return false; + } + + monitoring_thread_ = new DBusMonitoringThread(this, context, mainloop, + &filter_list_); + if (monitoring_thread_ == NULL) { + LOG(LS_ERROR) << "Failed to create DBus monitoring thread."; + g_main_context_unref(context); + g_main_loop_unref(mainloop); + return false; + } + monitoring_thread_->Start(); + } + return true; +} + +bool DBusMonitor::StopMonitoring() { + if (monitoring_thread_) { + monitoring_thread_->Stop(); + monitoring_thread_ = NULL; + } + return true; +} + +DBusMonitor::DBusMonitorStatus DBusMonitor::GetStatus() { + return status_; +} + +void DBusMonitor::OnMonitoringStatusChanged(DBusMonitorStatus status) { + status_ = status; +} + +#undef LATE + +} // namespace talk_base + +#endif // HAVE_DBUS_GLIB diff --git a/talk/base/dbus.h b/talk/base/dbus.h new file mode 100644 index 000000000..7dce35061 --- /dev/null +++ b/talk/base/dbus.h @@ -0,0 +1,185 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifndef TALK_BASE_DBUS_H_ +#define TALK_BASE_DBUS_H_ + +#ifdef HAVE_DBUS_GLIB + +#include + +#include +#include + +#include "talk/base/libdbusglibsymboltable.h" +#include "talk/base/messagehandler.h" +#include "talk/base/thread.h" + +namespace talk_base { + +#define DBUS_TYPE "type" +#define DBUS_SIGNAL "signal" +#define DBUS_PATH "path" +#define DBUS_INTERFACE "interface" +#define DBUS_MEMBER "member" + +#ifdef CHROMEOS +#define CROS_PM_PATH "/" +#define CROS_PM_INTERFACE "org.chromium.PowerManager" +#define CROS_SIG_POWERCHANGED "PowerStateChanged" +#define CROS_VALUE_SLEEP "mem" +#define CROS_VALUE_RESUME "on" +#else +#define UP_PATH "/org/freedesktop/UPower" +#define UP_INTERFACE "org.freedesktop.UPower" +#define UP_SIG_SLEEPING "Sleeping" +#define UP_SIG_RESUMING "Resuming" +#endif // CHROMEOS + +// Wraps a DBus messages. +class DBusSigMessageData : public TypedMessageData { + public: + explicit DBusSigMessageData(DBusMessage *message); + ~DBusSigMessageData(); +}; + +// DBusSigFilter is an abstract class that defines the interface of DBus +// signal handling. +// The subclasses implement ProcessSignal() for various purposes. +// When a DBus signal comes, a DSM_SIGNAL message is posted to the caller thread +// which will then invokes ProcessSignal(). +class DBusSigFilter : protected MessageHandler { + public: + enum DBusSigMessage { DSM_SIGNAL }; + + // This filter string should ususally come from BuildFilterString() + explicit DBusSigFilter(const std::string &filter) + : caller_thread_(Thread::Current()), filter_(filter) { + } + + // Builds a DBus monitor filter string from given DBus path, interface, and + // member. + // See http://dbus.freedesktop.org/doc/api/html/group__DBusConnection.html + static std::string BuildFilterString(const std::string &path, + const std::string &interface, + const std::string &member); + + // Handles callback on DBus messages by DBus system. + static DBusHandlerResult DBusCallback(DBusConnection *dbus_conn, + DBusMessage *message, + void *instance); + + // Handles callback on DBus messages to each DBusSigFilter instance. + DBusHandlerResult Callback(DBusMessage *message); + + // From MessageHandler. + virtual void OnMessage(Message *message); + + // Returns the DBus monitor filter string. + const std::string &filter() const { return filter_; } + + private: + // On caller thread. + virtual void ProcessSignal(DBusMessage *message) = 0; + + Thread *caller_thread_; + const std::string filter_; +}; + +// DBusMonitor is a class for DBus signal monitoring. +// +// The caller-thread calls AddFilter() first to add the signals that it wants to +// monitor and then calls StartMonitoring() to start the monitoring. +// This will create a worker-thread which listens on DBus connection and sends +// DBus signals back through the callback. +// The worker-thread will be running forever until either StopMonitoring() is +// called from the caller-thread or the worker-thread hit some error. +// +// Programming model: +// 1. Caller-thread: Creates an object of DBusMonitor. +// 2. Caller-thread: Calls DBusMonitor::AddFilter() one or several times. +// 3. Caller-thread: StartMonitoring(). +// ... +// 4. Worker-thread: DBus signal recieved. Post a message to caller-thread. +// 5. Caller-thread: DBusFilterBase::ProcessSignal() is invoked. +// ... +// 6. Caller-thread: StopMonitoring(). +// +// Assumption: +// AddFilter(), StartMonitoring(), and StopMonitoring() methods are called by +// a single thread. Hence, there is no need to make them thread safe. +class DBusMonitor { + public: + // Status of DBus monitoring. + enum DBusMonitorStatus { + DMS_NOT_INITIALIZED, // Not initialized. + DMS_INITIALIZING, // Initializing the monitoring thread. + DMS_RUNNING, // Monitoring. + DMS_STOPPED, // Not monitoring. Stopped normally. + DMS_FAILED, // Not monitoring. Failed. + }; + + // Returns the DBus-Glib symbol table. + // We should only use this function to access DBus-Glib symbols. + static LibDBusGlibSymbolTable *GetDBusGlibSymbolTable(); + + // Creates an instance of DBusMonitor. + static DBusMonitor *Create(DBusBusType type); + ~DBusMonitor(); + + // Adds a filter to DBusMonitor. + bool AddFilter(DBusSigFilter *filter); + + // Starts DBus message monitoring. + bool StartMonitoring(); + + // Stops DBus message monitoring. + bool StopMonitoring(); + + // Gets the status of DBus monitoring. + DBusMonitorStatus GetStatus(); + + private: + // Forward declaration. Defined in the .cc file. + class DBusMonitoringThread; + + explicit DBusMonitor(DBusBusType type); + + // Updates status_ when monitoring status has changed. + void OnMonitoringStatusChanged(DBusMonitorStatus status); + + DBusBusType type_; + DBusMonitorStatus status_; + DBusMonitoringThread *monitoring_thread_; + std::vector filter_list_; +}; + +} // namespace talk_base + +#endif // HAVE_DBUS_GLIB + +#endif // TALK_BASE_DBUS_H_ diff --git a/talk/base/dbus_unittest.cc b/talk/base/dbus_unittest.cc new file mode 100644 index 000000000..94f01c0fe --- /dev/null +++ b/talk/base/dbus_unittest.cc @@ -0,0 +1,249 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifdef HAVE_DBUS_GLIB + +#include "talk/base/dbus.h" +#include "talk/base/gunit.h" +#include "talk/base/thread.h" + +namespace talk_base { + +#define SIG_NAME "NameAcquired" + +static const uint32 kTimeoutMs = 5000U; + +class DBusSigFilterTest : public DBusSigFilter { + public: + // DBusSigFilterTest listens on DBus service itself for "NameAcquired" signal. + // This signal should be received when the application connects to DBus + // service and gains ownership of a name. + // http://dbus.freedesktop.org/doc/dbus-specification.html + DBusSigFilterTest() + : DBusSigFilter(GetFilter()), + message_received_(false) { + } + + bool MessageReceived() { + return message_received_; + } + + private: + static std::string GetFilter() { + return talk_base::DBusSigFilter::BuildFilterString("", "", SIG_NAME); + } + + // Implement virtual method of DBusSigFilter. On caller thread. + virtual void ProcessSignal(DBusMessage *message) { + EXPECT_TRUE(message != NULL); + message_received_ = true; + } + + bool message_received_; +}; + +TEST(DBusMonitorTest, StartStopStartStop) { + DBusSigFilterTest filter; + talk_base::scoped_ptr monitor; + monitor.reset(talk_base::DBusMonitor::Create(DBUS_BUS_SYSTEM)); + if (monitor) { + EXPECT_TRUE(monitor->AddFilter(&filter)); + + EXPECT_TRUE(monitor->StopMonitoring()); + EXPECT_EQ(monitor->GetStatus(), DBusMonitor::DMS_NOT_INITIALIZED); + + EXPECT_TRUE(monitor->StartMonitoring()); + EXPECT_EQ_WAIT(DBusMonitor::DMS_RUNNING, monitor->GetStatus(), kTimeoutMs); + EXPECT_TRUE(monitor->StopMonitoring()); + EXPECT_EQ(monitor->GetStatus(), DBusMonitor::DMS_STOPPED); + EXPECT_TRUE(monitor->StopMonitoring()); + EXPECT_EQ(monitor->GetStatus(), DBusMonitor::DMS_STOPPED); + + EXPECT_TRUE(monitor->StartMonitoring()); + EXPECT_EQ_WAIT(DBusMonitor::DMS_RUNNING, monitor->GetStatus(), kTimeoutMs); + EXPECT_TRUE(monitor->StartMonitoring()); + EXPECT_EQ(monitor->GetStatus(), DBusMonitor::DMS_RUNNING); + EXPECT_TRUE(monitor->StopMonitoring()); + EXPECT_EQ(monitor->GetStatus(), DBusMonitor::DMS_STOPPED); + } else { + LOG(LS_WARNING) << "DBus Monitor not started. Skipping test."; + } +} + +// DBusMonitorTest listens on DBus service itself for "NameAcquired" signal. +// This signal should be received when the application connects to DBus +// service and gains ownership of a name. +// This test is to make sure that we capture the "NameAcquired" signal. +TEST(DBusMonitorTest, ReceivedNameAcquiredSignal) { + DBusSigFilterTest filter; + talk_base::scoped_ptr monitor; + monitor.reset(talk_base::DBusMonitor::Create(DBUS_BUS_SYSTEM)); + if (monitor) { + EXPECT_TRUE(monitor->AddFilter(&filter)); + + EXPECT_TRUE(monitor->StartMonitoring()); + EXPECT_EQ_WAIT(DBusMonitor::DMS_RUNNING, monitor->GetStatus(), kTimeoutMs); + EXPECT_TRUE_WAIT(filter.MessageReceived(), kTimeoutMs); + EXPECT_TRUE(monitor->StopMonitoring()); + EXPECT_EQ(monitor->GetStatus(), DBusMonitor::DMS_STOPPED); + } else { + LOG(LS_WARNING) << "DBus Monitor not started. Skipping test."; + } +} + +TEST(DBusMonitorTest, ConcurrentMonitors) { + DBusSigFilterTest filter1; + talk_base::scoped_ptr monitor1; + monitor1.reset(talk_base::DBusMonitor::Create(DBUS_BUS_SYSTEM)); + if (monitor1) { + EXPECT_TRUE(monitor1->AddFilter(&filter1)); + DBusSigFilterTest filter2; + talk_base::scoped_ptr monitor2; + monitor2.reset(talk_base::DBusMonitor::Create(DBUS_BUS_SYSTEM)); + EXPECT_TRUE(monitor2->AddFilter(&filter2)); + + EXPECT_TRUE(monitor1->StartMonitoring()); + EXPECT_EQ_WAIT(DBusMonitor::DMS_RUNNING, monitor1->GetStatus(), kTimeoutMs); + EXPECT_TRUE(monitor2->StartMonitoring()); + EXPECT_EQ_WAIT(DBusMonitor::DMS_RUNNING, monitor2->GetStatus(), kTimeoutMs); + + EXPECT_TRUE_WAIT(filter2.MessageReceived(), kTimeoutMs); + EXPECT_TRUE(monitor2->StopMonitoring()); + EXPECT_EQ(monitor2->GetStatus(), DBusMonitor::DMS_STOPPED); + + EXPECT_TRUE_WAIT(filter1.MessageReceived(), kTimeoutMs); + EXPECT_TRUE(monitor1->StopMonitoring()); + EXPECT_EQ(monitor1->GetStatus(), DBusMonitor::DMS_STOPPED); + } else { + LOG(LS_WARNING) << "DBus Monitor not started. Skipping test."; + } +} + +TEST(DBusMonitorTest, ConcurrentFilters) { + DBusSigFilterTest filter1; + DBusSigFilterTest filter2; + talk_base::scoped_ptr monitor; + monitor.reset(talk_base::DBusMonitor::Create(DBUS_BUS_SYSTEM)); + if (monitor) { + EXPECT_TRUE(monitor->AddFilter(&filter1)); + EXPECT_TRUE(monitor->AddFilter(&filter2)); + + EXPECT_TRUE(monitor->StartMonitoring()); + EXPECT_EQ_WAIT(DBusMonitor::DMS_RUNNING, monitor->GetStatus(), kTimeoutMs); + + EXPECT_TRUE_WAIT(filter1.MessageReceived(), kTimeoutMs); + EXPECT_TRUE_WAIT(filter2.MessageReceived(), kTimeoutMs); + + EXPECT_TRUE(monitor->StopMonitoring()); + EXPECT_EQ(monitor->GetStatus(), DBusMonitor::DMS_STOPPED); + } else { + LOG(LS_WARNING) << "DBus Monitor not started. Skipping test."; + } +} + +TEST(DBusMonitorTest, NoAddFilterIfRunning) { + DBusSigFilterTest filter1; + DBusSigFilterTest filter2; + talk_base::scoped_ptr monitor; + monitor.reset(talk_base::DBusMonitor::Create(DBUS_BUS_SYSTEM)); + if (monitor) { + EXPECT_TRUE(monitor->AddFilter(&filter1)); + + EXPECT_TRUE(monitor->StartMonitoring()); + EXPECT_EQ_WAIT(DBusMonitor::DMS_RUNNING, monitor->GetStatus(), kTimeoutMs); + EXPECT_FALSE(monitor->AddFilter(&filter2)); + + EXPECT_TRUE(monitor->StopMonitoring()); + EXPECT_EQ(monitor->GetStatus(), DBusMonitor::DMS_STOPPED); + } else { + LOG(LS_WARNING) << "DBus Monitor not started. Skipping test."; + } +} + +TEST(DBusMonitorTest, AddFilterAfterStop) { + DBusSigFilterTest filter1; + DBusSigFilterTest filter2; + talk_base::scoped_ptr monitor; + monitor.reset(talk_base::DBusMonitor::Create(DBUS_BUS_SYSTEM)); + if (monitor) { + EXPECT_TRUE(monitor->AddFilter(&filter1)); + EXPECT_TRUE(monitor->StartMonitoring()); + EXPECT_EQ_WAIT(DBusMonitor::DMS_RUNNING, monitor->GetStatus(), kTimeoutMs); + EXPECT_TRUE_WAIT(filter1.MessageReceived(), kTimeoutMs); + EXPECT_TRUE(monitor->StopMonitoring()); + EXPECT_EQ(monitor->GetStatus(), DBusMonitor::DMS_STOPPED); + + EXPECT_TRUE(monitor->AddFilter(&filter2)); + EXPECT_TRUE(monitor->StartMonitoring()); + EXPECT_EQ_WAIT(DBusMonitor::DMS_RUNNING, monitor->GetStatus(), kTimeoutMs); + EXPECT_TRUE_WAIT(filter1.MessageReceived(), kTimeoutMs); + EXPECT_TRUE_WAIT(filter2.MessageReceived(), kTimeoutMs); + EXPECT_TRUE(monitor->StopMonitoring()); + EXPECT_EQ(monitor->GetStatus(), DBusMonitor::DMS_STOPPED); + } else { + LOG(LS_WARNING) << "DBus Monitor not started. Skipping test."; + } +} + +TEST(DBusMonitorTest, StopRightAfterStart) { + DBusSigFilterTest filter; + talk_base::scoped_ptr monitor; + monitor.reset(talk_base::DBusMonitor::Create(DBUS_BUS_SYSTEM)); + if (monitor) { + EXPECT_TRUE(monitor->AddFilter(&filter)); + + EXPECT_TRUE(monitor->StartMonitoring()); + EXPECT_TRUE(monitor->StopMonitoring()); + + // Stop the monitoring thread right after it had been started. + // If the monitoring thread got a chance to receive a DBus signal, it would + // post a message to the main thread and signal the main thread wakeup. + // This message will be cleaned out automatically when the filter get + // destructed. Here we also consume the wakeup signal (if there is one) so + // that the testing (main) thread is reset to a clean state. + talk_base::Thread::Current()->ProcessMessages(1); + } else { + LOG(LS_WARNING) << "DBus Monitor not started."; + } +} + +TEST(DBusSigFilter, BuildFilterString) { + EXPECT_EQ(DBusSigFilter::BuildFilterString("", "", ""), + (DBUS_TYPE "='" DBUS_SIGNAL "'")); + EXPECT_EQ(DBusSigFilter::BuildFilterString("p", "", ""), + (DBUS_TYPE "='" DBUS_SIGNAL "'," DBUS_PATH "='p'")); + EXPECT_EQ(DBusSigFilter::BuildFilterString("p","i", ""), + (DBUS_TYPE "='" DBUS_SIGNAL "'," DBUS_PATH "='p'," + DBUS_INTERFACE "='i'")); + EXPECT_EQ(DBusSigFilter::BuildFilterString("p","i","m"), + (DBUS_TYPE "='" DBUS_SIGNAL "'," DBUS_PATH "='p'," + DBUS_INTERFACE "='i'," DBUS_MEMBER "='m'")); +} + +} // namespace talk_base + +#endif // HAVE_DBUS_GLIB diff --git a/talk/base/diskcache.cc b/talk/base/diskcache.cc new file mode 100644 index 000000000..afaf9d26a --- /dev/null +++ b/talk/base/diskcache.cc @@ -0,0 +1,364 @@ +/* + * libjingle + * Copyright 2004--2005, 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 + +#ifdef WIN32 +#include "talk/base/win32.h" +#endif + +#include "talk/base/common.h" +#include "talk/base/diskcache.h" +#include "talk/base/fileutils.h" +#include "talk/base/pathutils.h" +#include "talk/base/stream.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" + +#ifdef _DEBUG +#define TRANSPARENT_CACHE_NAMES 1 +#else // !_DEBUG +#define TRANSPARENT_CACHE_NAMES 0 +#endif // !_DEBUG + +namespace talk_base { + +class DiskCache; + +/////////////////////////////////////////////////////////////////////////////// +// DiskCacheAdapter +/////////////////////////////////////////////////////////////////////////////// + +class DiskCacheAdapter : public StreamAdapterInterface { +public: + DiskCacheAdapter(const DiskCache* cache, const std::string& id, size_t index, + StreamInterface* stream) + : StreamAdapterInterface(stream), cache_(cache), id_(id), index_(index) + { } + virtual ~DiskCacheAdapter() { + Close(); + cache_->ReleaseResource(id_, index_); + } + +private: + const DiskCache* cache_; + std::string id_; + size_t index_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// DiskCache +/////////////////////////////////////////////////////////////////////////////// + +DiskCache::DiskCache() : max_cache_(0), total_size_(0), total_accessors_(0) { +} + +DiskCache::~DiskCache() { + ASSERT(0 == total_accessors_); +} + +bool DiskCache::Initialize(const std::string& folder, size_t size) { + if (!folder_.empty() || !Filesystem::CreateFolder(folder)) + return false; + + folder_ = folder; + max_cache_ = size; + ASSERT(0 == total_size_); + + if (!InitializeEntries()) + return false; + + return CheckLimit(); +} + +bool DiskCache::Purge() { + if (folder_.empty()) + return false; + + if (total_accessors_ > 0) { + LOG_F(LS_WARNING) << "Cache files open"; + return false; + } + + if (!PurgeFiles()) + return false; + + map_.clear(); + return true; +} + +bool DiskCache::LockResource(const std::string& id) { + Entry* entry = GetOrCreateEntry(id, true); + if (LS_LOCKED == entry->lock_state) + return false; + if ((LS_UNLOCKED == entry->lock_state) && (entry->accessors > 0)) + return false; + if ((total_size_ > max_cache_) && !CheckLimit()) { + LOG_F(LS_WARNING) << "Cache overfull"; + return false; + } + entry->lock_state = LS_LOCKED; + return true; +} + +StreamInterface* DiskCache::WriteResource(const std::string& id, size_t index) { + Entry* entry = GetOrCreateEntry(id, false); + if (LS_LOCKED != entry->lock_state) + return NULL; + + size_t previous_size = 0; + std::string filename(IdToFilename(id, index)); + FileStream::GetSize(filename, &previous_size); + ASSERT(previous_size <= entry->size); + if (previous_size > entry->size) { + previous_size = entry->size; + } + + scoped_ptr file(new FileStream); + if (!file->Open(filename, "wb", NULL)) { + LOG_F(LS_ERROR) << "Couldn't create cache file"; + return NULL; + } + + entry->streams = stdmax(entry->streams, index + 1); + entry->size -= previous_size; + total_size_ -= previous_size; + + entry->accessors += 1; + total_accessors_ += 1; + return new DiskCacheAdapter(this, id, index, file.release()); +} + +bool DiskCache::UnlockResource(const std::string& id) { + Entry* entry = GetOrCreateEntry(id, false); + if (LS_LOCKED != entry->lock_state) + return false; + + if (entry->accessors > 0) { + entry->lock_state = LS_UNLOCKING; + } else { + entry->lock_state = LS_UNLOCKED; + entry->last_modified = time(0); + CheckLimit(); + } + return true; +} + +StreamInterface* DiskCache::ReadResource(const std::string& id, + size_t index) const { + const Entry* entry = GetEntry(id); + if (LS_UNLOCKED != entry->lock_state) + return NULL; + if (index >= entry->streams) + return NULL; + + scoped_ptr file(new FileStream); + if (!file->Open(IdToFilename(id, index), "rb", NULL)) + return NULL; + + entry->accessors += 1; + total_accessors_ += 1; + return new DiskCacheAdapter(this, id, index, file.release()); +} + +bool DiskCache::HasResource(const std::string& id) const { + const Entry* entry = GetEntry(id); + return (NULL != entry) && (entry->streams > 0); +} + +bool DiskCache::HasResourceStream(const std::string& id, size_t index) const { + const Entry* entry = GetEntry(id); + if ((NULL == entry) || (index >= entry->streams)) + return false; + + std::string filename = IdToFilename(id, index); + + return FileExists(filename); +} + +bool DiskCache::DeleteResource(const std::string& id) { + Entry* entry = GetOrCreateEntry(id, false); + if (!entry) + return true; + + if ((LS_UNLOCKED != entry->lock_state) || (entry->accessors > 0)) + return false; + + bool success = true; + for (size_t index = 0; index < entry->streams; ++index) { + std::string filename = IdToFilename(id, index); + + if (!FileExists(filename)) + continue; + + if (!DeleteFile(filename)) { + LOG_F(LS_ERROR) << "Couldn't remove cache file: " << filename; + success = false; + } + } + + total_size_ -= entry->size; + map_.erase(id); + return success; +} + +bool DiskCache::CheckLimit() { +#ifdef _DEBUG + // Temporary check to make sure everything is working correctly. + size_t cache_size = 0; + for (EntryMap::iterator it = map_.begin(); it != map_.end(); ++it) { + cache_size += it->second.size; + } + ASSERT(cache_size == total_size_); +#endif // _DEBUG + + // TODO: Replace this with a non-brain-dead algorithm for clearing out the + // oldest resources... something that isn't O(n^2) + while (total_size_ > max_cache_) { + EntryMap::iterator oldest = map_.end(); + for (EntryMap::iterator it = map_.begin(); it != map_.end(); ++it) { + if ((LS_UNLOCKED != it->second.lock_state) || (it->second.accessors > 0)) + continue; + oldest = it; + break; + } + if (oldest == map_.end()) { + LOG_F(LS_WARNING) << "All resources are locked!"; + return false; + } + for (EntryMap::iterator it = oldest++; it != map_.end(); ++it) { + if (it->second.last_modified < oldest->second.last_modified) { + oldest = it; + } + } + if (!DeleteResource(oldest->first)) { + LOG_F(LS_ERROR) << "Couldn't delete from cache!"; + return false; + } + } + return true; +} + +std::string DiskCache::IdToFilename(const std::string& id, size_t index) const { +#ifdef TRANSPARENT_CACHE_NAMES + // This escapes colons and other filesystem characters, so the user can't open + // special devices (like "COM1:"), or access other directories. + size_t buffer_size = id.length()*3 + 1; + char* buffer = new char[buffer_size]; + encode(buffer, buffer_size, id.data(), id.length(), + unsafe_filename_characters(), '%'); + // TODO: ASSERT(strlen(buffer) < FileSystem::MaxBasenameLength()); +#else // !TRANSPARENT_CACHE_NAMES + // We might want to just use a hash of the filename at some point, both for + // obfuscation, and to avoid both filename length and escaping issues. + ASSERT(false); +#endif // !TRANSPARENT_CACHE_NAMES + + char extension[32]; + sprintfn(extension, ARRAY_SIZE(extension), ".%u", index); + + Pathname pathname; + pathname.SetFolder(folder_); + pathname.SetBasename(buffer); + pathname.SetExtension(extension); + +#ifdef TRANSPARENT_CACHE_NAMES + delete [] buffer; +#endif // TRANSPARENT_CACHE_NAMES + + return pathname.pathname(); +} + +bool DiskCache::FilenameToId(const std::string& filename, std::string* id, + size_t* index) const { + Pathname pathname(filename); + unsigned tempdex; + if (1 != sscanf(pathname.extension().c_str(), ".%u", &tempdex)) + return false; + + *index = static_cast(tempdex); + + size_t buffer_size = pathname.basename().length() + 1; + char* buffer = new char[buffer_size]; + decode(buffer, buffer_size, pathname.basename().data(), + pathname.basename().length(), '%'); + id->assign(buffer); + delete [] buffer; + return true; +} + +DiskCache::Entry* DiskCache::GetOrCreateEntry(const std::string& id, + bool create) { + EntryMap::iterator it = map_.find(id); + if (it != map_.end()) + return &it->second; + if (!create) + return NULL; + Entry e; + e.lock_state = LS_UNLOCKED; + e.accessors = 0; + e.size = 0; + e.streams = 0; + e.last_modified = time(0); + it = map_.insert(EntryMap::value_type(id, e)).first; + return &it->second; +} + +void DiskCache::ReleaseResource(const std::string& id, size_t index) const { + const Entry* entry = GetEntry(id); + if (!entry) { + LOG_F(LS_WARNING) << "Missing cache entry"; + ASSERT(false); + return; + } + + entry->accessors -= 1; + total_accessors_ -= 1; + + if (LS_UNLOCKED != entry->lock_state) { + // This is safe, because locked resources only issue WriteResource, which + // is non-const. Think about a better way to handle it. + DiskCache* this2 = const_cast(this); + Entry* entry2 = this2->GetOrCreateEntry(id, false); + + size_t new_size = 0; + std::string filename(IdToFilename(id, index)); + FileStream::GetSize(filename, &new_size); + entry2->size += new_size; + this2->total_size_ += new_size; + + if ((LS_UNLOCKING == entry->lock_state) && (0 == entry->accessors)) { + entry2->last_modified = time(0); + entry2->lock_state = LS_UNLOCKED; + this2->CheckLimit(); + } + } +} + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/diskcache.h b/talk/base/diskcache.h new file mode 100644 index 000000000..c5a1dfc31 --- /dev/null +++ b/talk/base/diskcache.h @@ -0,0 +1,142 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_DISKCACHE_H__ +#define TALK_BASE_DISKCACHE_H__ + +#include +#include + +#ifdef WIN32 +#undef UnlockResource +#endif // WIN32 + +namespace talk_base { + +class StreamInterface; + +/////////////////////////////////////////////////////////////////////////////// +// DiskCache - An LRU cache of streams, stored on disk. +// +// Streams are identified by a unique resource id. Multiple streams can be +// associated with each resource id, distinguished by an index. When old +// resources are flushed from the cache, all streams associated with those +// resources are removed together. +// DiskCache is designed to persist across executions of the program. It is +// safe for use from an arbitrary number of users on a single thread, but not +// from multiple threads or other processes. +/////////////////////////////////////////////////////////////////////////////// + +class DiskCache { +public: + DiskCache(); + virtual ~DiskCache(); + + bool Initialize(const std::string& folder, size_t size); + bool Purge(); + + bool LockResource(const std::string& id); + StreamInterface* WriteResource(const std::string& id, size_t index); + bool UnlockResource(const std::string& id); + + StreamInterface* ReadResource(const std::string& id, size_t index) const; + + bool HasResource(const std::string& id) const; + bool HasResourceStream(const std::string& id, size_t index) const; + bool DeleteResource(const std::string& id); + + protected: + virtual bool InitializeEntries() = 0; + virtual bool PurgeFiles() = 0; + + virtual bool FileExists(const std::string& filename) const = 0; + virtual bool DeleteFile(const std::string& filename) const = 0; + + enum LockState { LS_UNLOCKED, LS_LOCKED, LS_UNLOCKING }; + struct Entry { + LockState lock_state; + mutable size_t accessors; + size_t size; + size_t streams; + time_t last_modified; + }; + typedef std::map EntryMap; + friend class DiskCacheAdapter; + + bool CheckLimit(); + + std::string IdToFilename(const std::string& id, size_t index) const; + bool FilenameToId(const std::string& filename, std::string* id, + size_t* index) const; + + const Entry* GetEntry(const std::string& id) const { + return const_cast(this)->GetOrCreateEntry(id, false); + } + Entry* GetOrCreateEntry(const std::string& id, bool create); + + void ReleaseResource(const std::string& id, size_t index) const; + + std::string folder_; + size_t max_cache_, total_size_; + EntryMap map_; + mutable size_t total_accessors_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// CacheLock - Automatically manage locking and unlocking, with optional +// rollback semantics +/////////////////////////////////////////////////////////////////////////////// + +class CacheLock { +public: + CacheLock(DiskCache* cache, const std::string& id, bool rollback = false) + : cache_(cache), id_(id), rollback_(rollback) + { + locked_ = cache_->LockResource(id_); + } + ~CacheLock() { + if (locked_) { + cache_->UnlockResource(id_); + if (rollback_) { + cache_->DeleteResource(id_); + } + } + } + bool IsLocked() const { return locked_; } + void Commit() { rollback_ = false; } + +private: + DiskCache* cache_; + std::string id_; + bool rollback_, locked_; +}; + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_DISKCACHE_H__ diff --git a/talk/base/diskcache_win32.cc b/talk/base/diskcache_win32.cc new file mode 100644 index 000000000..b49ed8138 --- /dev/null +++ b/talk/base/diskcache_win32.cc @@ -0,0 +1,103 @@ +/* + * libjingle + * Copyright 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/base/win32.h" +#include +#include +#include + +#include + +#include "talk/base/common.h" +#include "talk/base/diskcache.h" +#include "talk/base/pathutils.h" +#include "talk/base/stream.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" + +#include "talk/base/diskcache_win32.h" + +namespace talk_base { + +bool DiskCacheWin32::InitializeEntries() { + // Note: We could store the cache information in a separate file, for faster + // initialization. Figuring it out empirically works, too. + + std::wstring path16 = ToUtf16(folder_); + path16.append(1, '*'); + + WIN32_FIND_DATA find_data; + HANDLE find_handle = FindFirstFile(path16.c_str(), &find_data); + if (find_handle != INVALID_HANDLE_VALUE) { + do { + size_t index; + std::string id; + if (!FilenameToId(ToUtf8(find_data.cFileName), &id, &index)) + continue; + + Entry* entry = GetOrCreateEntry(id, true); + entry->size += find_data.nFileSizeLow; + total_size_ += find_data.nFileSizeLow; + entry->streams = _max(entry->streams, index + 1); + FileTimeToUnixTime(find_data.ftLastWriteTime, &entry->last_modified); + + } while (FindNextFile(find_handle, &find_data)); + + FindClose(find_handle); + } + + return true; +} + +bool DiskCacheWin32::PurgeFiles() { + std::wstring path16 = ToUtf16(folder_); + path16.append(1, '*'); + path16.append(1, '\0'); + + SHFILEOPSTRUCT file_op = { 0 }; + file_op.wFunc = FO_DELETE; + file_op.pFrom = path16.c_str(); + file_op.fFlags = FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT + | FOF_NORECURSION | FOF_FILESONLY; + if (0 != SHFileOperation(&file_op)) { + LOG_F(LS_ERROR) << "Couldn't delete cache files"; + return false; + } + + return true; +} + +bool DiskCacheWin32::FileExists(const std::string& filename) const { + DWORD result = ::GetFileAttributes(ToUtf16(filename).c_str()); + return (INVALID_FILE_ATTRIBUTES != result); +} + +bool DiskCacheWin32::DeleteFile(const std::string& filename) const { + return ::DeleteFile(ToUtf16(filename).c_str()) != 0; +} + +} diff --git a/talk/base/diskcache_win32.h b/talk/base/diskcache_win32.h new file mode 100644 index 000000000..a5e8de5e2 --- /dev/null +++ b/talk/base/diskcache_win32.h @@ -0,0 +1,46 @@ +/* + * libjingle + * Copyright 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. + */ + +#ifndef TALK_BASE_DISKCACHEWIN32_H__ +#define TALK_BASE_DISKCACHEWIN32_H__ + +#include "talk/base/diskcache.h" + +namespace talk_base { + +class DiskCacheWin32 : public DiskCache { + protected: + virtual bool InitializeEntries(); + virtual bool PurgeFiles(); + + virtual bool FileExists(const std::string& filename) const; + virtual bool DeleteFile(const std::string& filename) const; +}; + +} + +#endif // TALK_BASE_DISKCACHEWIN32_H__ diff --git a/talk/base/event.cc b/talk/base/event.cc new file mode 100644 index 000000000..6089c8c9f --- /dev/null +++ b/talk/base/event.cc @@ -0,0 +1,139 @@ +/* + * libjingle + * Copyright 2004--2008, 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/base/event.h" + +#if defined(WIN32) +#include +#elif defined(POSIX) +#include +#include +#include +#else +#error "Must define either WIN32 or POSIX." +#endif + +namespace talk_base { + +#if defined(WIN32) + +Event::Event(bool manual_reset, bool initially_signaled) + : is_manual_reset_(manual_reset), + is_initially_signaled_(initially_signaled) { + event_handle_ = ::CreateEvent(NULL, // Security attributes. + is_manual_reset_, + is_initially_signaled_, + NULL); // Name. + ASSERT(event_handle_ != NULL); +} + +Event::~Event() { + CloseHandle(event_handle_); +} + +void Event::Set() { + SetEvent(event_handle_); +} + +void Event::Reset() { + ResetEvent(event_handle_); +} + +bool Event::Wait(int cms) { + DWORD ms = (cms == kForever)? INFINITE : cms; + return (WaitForSingleObject(event_handle_, ms) == WAIT_OBJECT_0); +} + +#elif defined(POSIX) + +Event::Event(bool manual_reset, bool initially_signaled) + : is_manual_reset_(manual_reset), + event_status_(initially_signaled) { + VERIFY(pthread_mutex_init(&event_mutex_, NULL) == 0); + VERIFY(pthread_cond_init(&event_cond_, NULL) == 0); +} + +Event::~Event() { + pthread_mutex_destroy(&event_mutex_); + pthread_cond_destroy(&event_cond_); +} + +void Event::Set() { + pthread_mutex_lock(&event_mutex_); + event_status_ = true; + pthread_cond_broadcast(&event_cond_); + pthread_mutex_unlock(&event_mutex_); +} + +void Event::Reset() { + pthread_mutex_lock(&event_mutex_); + event_status_ = false; + pthread_mutex_unlock(&event_mutex_); +} + +bool Event::Wait(int cms) { + pthread_mutex_lock(&event_mutex_); + int error = 0; + + if (cms != kForever) { + // Converting from seconds and microseconds (1e-6) plus + // milliseconds (1e-3) to seconds and nanoseconds (1e-9). + + struct timeval tv; + gettimeofday(&tv, NULL); + + struct timespec ts; + ts.tv_sec = tv.tv_sec + (cms / 1000); + ts.tv_nsec = tv.tv_usec * 1000 + (cms % 1000) * 1000000; + + // Handle overflow. + if (ts.tv_nsec >= 1000000000) { + ts.tv_sec++; + ts.tv_nsec -= 1000000000; + } + + while (!event_status_ && error == 0) + error = pthread_cond_timedwait(&event_cond_, &event_mutex_, &ts); + } else { + while (!event_status_ && error == 0) + error = pthread_cond_wait(&event_cond_, &event_mutex_); + } + + // NOTE(liulk): Exactly one thread will auto-reset this event. All + // the other threads will think it's unsignaled. This seems to be + // consistent with auto-reset events in WIN32. + if (error == 0 && !is_manual_reset_) + event_status_ = false; + + pthread_mutex_unlock(&event_mutex_); + + return (error == 0); +} + +#endif + +} // namespace talk_base diff --git a/talk/base/event.h b/talk/base/event.h new file mode 100644 index 000000000..72edae8e8 --- /dev/null +++ b/talk/base/event.h @@ -0,0 +1,68 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +#ifndef TALK_BASE_EVENT_H__ +#define TALK_BASE_EVENT_H__ + +#if defined(WIN32) +#include "talk/base/win32.h" // NOLINT: consider this a system header. +#elif defined(POSIX) +#include +#else +#error "Must define either WIN32 or POSIX." +#endif + +#include "talk/base/basictypes.h" +#include "talk/base/common.h" + +namespace talk_base { + +class Event { + public: + Event(bool manual_reset, bool initially_signaled); + ~Event(); + + void Set(); + void Reset(); + bool Wait(int cms); + + private: + bool is_manual_reset_; + +#if defined(WIN32) + bool is_initially_signaled_; + HANDLE event_handle_; +#elif defined(POSIX) + bool event_status_; + pthread_mutex_t event_mutex_; + pthread_cond_t event_cond_; +#endif +}; + +} // namespace talk_base + +#endif // TALK_BASE_EVENT_H__ diff --git a/talk/base/event_unittest.cc b/talk/base/event_unittest.cc new file mode 100644 index 000000000..5a3c1c6f7 --- /dev/null +++ b/talk/base/event_unittest.cc @@ -0,0 +1,59 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/event.h" +#include "talk/base/gunit.h" + +namespace talk_base { + +TEST(EventTest, InitiallySignaled) { + Event event(false, true); + ASSERT_TRUE(event.Wait(0)); +} + +TEST(EventTest, ManualReset) { + Event event(true, false); + ASSERT_FALSE(event.Wait(0)); + + event.Set(); + ASSERT_TRUE(event.Wait(0)); + ASSERT_TRUE(event.Wait(0)); + + event.Reset(); + ASSERT_FALSE(event.Wait(0)); +} + +TEST(EventTest, AutoReset) { + Event event(false, false); + ASSERT_FALSE(event.Wait(0)); + + event.Set(); + ASSERT_TRUE(event.Wait(0)); + ASSERT_FALSE(event.Wait(0)); +} + +} // namespace talk_base diff --git a/talk/base/fakecpumonitor.h b/talk/base/fakecpumonitor.h new file mode 100644 index 000000000..8b8e36dc4 --- /dev/null +++ b/talk/base/fakecpumonitor.h @@ -0,0 +1,49 @@ +/* + * libjingle + * Copyright 2013 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. + */ + +#ifndef TALK_BASE_FAKECPUMONITOR_H_ +#define TALK_BASE_FAKECPUMONITOR_H_ + +#include "talk/base/cpumonitor.h" + +namespace talk_base { + +class FakeCpuMonitor : public talk_base::CpuMonitor { + public: + explicit FakeCpuMonitor(Thread* thread) + : CpuMonitor(thread) { + } + ~FakeCpuMonitor() { + } + + virtual void OnMessage(talk_base::Message* msg) { + } +}; + +} // namespace talk_base + +#endif // TALK_BASE_FAKECPUMONITOR_H_ diff --git a/talk/base/fakenetwork.h b/talk/base/fakenetwork.h new file mode 100644 index 000000000..3bdc97fe7 --- /dev/null +++ b/talk/base/fakenetwork.h @@ -0,0 +1,136 @@ +/* + * libjingle + * Copyright 2009 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. + */ + +#ifndef TALK_BASE_FAKENETWORK_H_ +#define TALK_BASE_FAKENETWORK_H_ + +#include +#include + +#include "talk/base/network.h" +#include "talk/base/messagehandler.h" +#include "talk/base/socketaddress.h" +#include "talk/base/stringencode.h" +#include "talk/base/thread.h" + +namespace talk_base { + +const int kFakeIPv4NetworkPrefixLength = 24; +const int kFakeIPv6NetworkPrefixLength = 64; + +// Fake network manager that allows us to manually specify the IPs to use. +class FakeNetworkManager : public NetworkManagerBase, + public MessageHandler { + public: + FakeNetworkManager() + : thread_(Thread::Current()), + next_index_(0), + started_(false), + sent_first_update_(false) { + } + + typedef std::vector IfaceList; + + void AddInterface(const SocketAddress& iface) { + // ensure a unique name for the interface + SocketAddress address("test" + talk_base::ToString(next_index_++), 0); + address.SetResolvedIP(iface.ipaddr()); + ifaces_.push_back(address); + DoUpdateNetworks(); + } + + void RemoveInterface(const SocketAddress& iface) { + for (IfaceList::iterator it = ifaces_.begin(); + it != ifaces_.end(); ++it) { + if (it->EqualIPs(iface)) { + ifaces_.erase(it); + break; + } + } + DoUpdateNetworks(); + } + + virtual void StartUpdating() { + if (started_) { + if (sent_first_update_) + SignalNetworksChanged(); + return; + } + + started_ = true; + sent_first_update_ = false; + thread_->Post(this); + } + + virtual void StopUpdating() { + started_ = false; + } + + // MessageHandler interface. + virtual void OnMessage(Message* msg) { + DoUpdateNetworks(); + } + + private: + void DoUpdateNetworks() { + if (!started_) + return; + std::vector networks; + for (IfaceList::iterator it = ifaces_.begin(); + it != ifaces_.end(); ++it) { + int prefix_length = 0; + if (it->ipaddr().family() == AF_INET) { + prefix_length = kFakeIPv4NetworkPrefixLength; + } else if (it->ipaddr().family() == AF_INET6) { + prefix_length = kFakeIPv6NetworkPrefixLength; + } + IPAddress prefix = TruncateIP(it->ipaddr(), prefix_length); + scoped_ptr net(new Network(it->hostname(), + it->hostname(), + prefix, + prefix_length)); + net->AddIP(it->ipaddr()); + networks.push_back(net.release()); + } + bool changed; + MergeNetworkList(networks, &changed); + if (changed || !sent_first_update_) { + SignalNetworksChanged(); + sent_first_update_ = true; + } + } + + Thread* thread_; + IfaceList ifaces_; + int next_index_; + bool started_; + bool sent_first_update_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_FAKENETWORK_H_ diff --git a/talk/base/fakesslidentity.h b/talk/base/fakesslidentity.h new file mode 100644 index 000000000..f3c44e422 --- /dev/null +++ b/talk/base/fakesslidentity.h @@ -0,0 +1,69 @@ +/* + * libjingle + * Copyright 2012, The Libjingle Authors. + * + * 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. + */ + +#ifndef TALK_BASE_FAKESSLIDENTITY_H_ +#define TALK_BASE_FAKESSLIDENTITY_H_ + +#include "talk/base/messagedigest.h" +#include "talk/base/sslidentity.h" + +namespace talk_base { + +class FakeSSLCertificate : public talk_base::SSLCertificate { + public: + explicit FakeSSLCertificate(const std::string& data) : data_(data) {} + virtual FakeSSLCertificate* GetReference() const { + return new FakeSSLCertificate(*this); + } + virtual std::string ToPEMString() const { + return data_; + } + virtual bool ComputeDigest(const std::string &algorithm, + unsigned char *digest, std::size_t size, + std::size_t *length) const { + *length = talk_base::ComputeDigest(algorithm, data_.c_str(), data_.size(), + digest, size); + return (*length != 0); + } + private: + std::string data_; +}; + +class FakeSSLIdentity : public talk_base::SSLIdentity { + public: + explicit FakeSSLIdentity(const std::string& data) : cert_(data) {} + virtual FakeSSLIdentity* GetReference() const { + return new FakeSSLIdentity(*this); + } + virtual const FakeSSLCertificate& certificate() const { return cert_; } + private: + FakeSSLCertificate cert_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_FAKESSLIDENTITY_H_ diff --git a/talk/base/faketaskrunner.h b/talk/base/faketaskrunner.h new file mode 100644 index 000000000..6b5b0357e --- /dev/null +++ b/talk/base/faketaskrunner.h @@ -0,0 +1,55 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +// A fake TaskRunner for use in unit tests. + +#ifndef TALK_BASE_FAKETASKRUNNER_H_ +#define TALK_BASE_FAKETASKRUNNER_H_ + +#include "talk/base/taskparent.h" +#include "talk/base/taskrunner.h" + +namespace talk_base { + +class FakeTaskRunner : public TaskRunner { + public: + FakeTaskRunner() : current_time_(0) {} + virtual ~FakeTaskRunner() {} + + virtual void WakeTasks() { RunTasks(); } + + virtual int64 CurrentTime() { + // Implement if needed. + return current_time_++; + } + + int64 current_time_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_FAKETASKRUNNER_H_ diff --git a/talk/base/filelock.cc b/talk/base/filelock.cc new file mode 100644 index 000000000..77a474a20 --- /dev/null +++ b/talk/base/filelock.cc @@ -0,0 +1,79 @@ +/* + * libjingle + * Copyright 2009, 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/base/filelock.h" + +#include "talk/base/fileutils.h" +#include "talk/base/logging.h" +#include "talk/base/pathutils.h" +#include "talk/base/stream.h" + +namespace talk_base { + +FileLock::FileLock(const std::string& path, FileStream* file) + : path_(path), file_(file) { +} + +FileLock::~FileLock() { + MaybeUnlock(); +} + +void FileLock::Unlock() { + LOG_F(LS_INFO); + MaybeUnlock(); +} + +void FileLock::MaybeUnlock() { + if (file_) { + LOG(LS_INFO) << "Unlocking:" << path_; + file_->Close(); + Filesystem::DeleteFile(path_); + file_.reset(); + } +} + +FileLock* FileLock::TryLock(const std::string& path) { + FileStream* stream = new FileStream(); + bool ok = false; +#ifdef WIN32 + // Open and lock in a single operation. + ok = stream->OpenShare(path, "a", _SH_DENYRW, NULL); +#else // Linux and OSX + ok = stream->Open(path, "a", NULL) && stream->TryLock(); +#endif + if (ok) { + return new FileLock(path, stream); + } else { + // Something failed, either we didn't succeed to open the + // file or we failed to lock it. Anyway remove the heap + // allocated object and then return NULL to indicate failure. + delete stream; + return NULL; + } +} + +} // namespace talk_base diff --git a/talk/base/filelock.h b/talk/base/filelock.h new file mode 100644 index 000000000..a4936f521 --- /dev/null +++ b/talk/base/filelock.h @@ -0,0 +1,70 @@ +/* + * libjingle + * Copyright 2009, 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. + */ + +#ifndef TALK_BASE_FILELOCK_H_ +#define TALK_BASE_FILELOCK_H_ + +#include + +#include "talk/base/constructormagic.h" +#include "talk/base/scoped_ptr.h" + +namespace talk_base { + +class FileStream; + +// Implements a very simple cross process lock based on a file. +// When Lock(...) is called we try to open/create the file in read/write +// mode without any sharing. (Or locking it with flock(...) on Unix) +// If the process crash the OS will make sure that the file descriptor +// is released and another process can accuire the lock. +// This doesn't work on ancient OSX/Linux versions if used on NFS. +// (Nfs-client before: ~2.6 and Linux Kernel < 2.6.) +class FileLock { + public: + virtual ~FileLock(); + + // Attempts to lock the file. The caller owns the returned + // lock object. Returns NULL if the file already was locked. + static FileLock* TryLock(const std::string& path); + void Unlock(); + + protected: + FileLock(const std::string& path, FileStream* file); + + private: + void MaybeUnlock(); + + std::string path_; + scoped_ptr file_; + + DISALLOW_EVIL_CONSTRUCTORS(FileLock); +}; + +} // namespace talk_base + +#endif // TALK_BASE_FILELOCK_H_ diff --git a/talk/base/filelock_unittest.cc b/talk/base/filelock_unittest.cc new file mode 100644 index 000000000..e585f9191 --- /dev/null +++ b/talk/base/filelock_unittest.cc @@ -0,0 +1,104 @@ +/* + * libjingle + * Copyright 2009, 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 + +#include "talk/base/event.h" +#include "talk/base/filelock.h" +#include "talk/base/fileutils.h" +#include "talk/base/gunit.h" +#include "talk/base/pathutils.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/thread.h" + +namespace talk_base { + +const static std::string kLockFile = "TestLockFile"; +const static int kTimeoutMS = 5000; + +class FileLockTest : public testing::Test, public Runnable { + public: + FileLockTest() : done_(false, false), thread_lock_failed_(false) { + } + + virtual void Run(Thread* t) { + scoped_ptr lock(FileLock::TryLock(temp_file_.pathname())); + // The lock is already owned by the main thread of + // this test, therefore the TryLock(...) call should fail. + thread_lock_failed_ = lock.get() == NULL; + done_.Set(); + } + + protected: + virtual void SetUp() { + thread_lock_failed_ = false; + Filesystem::GetAppTempFolder(&temp_dir_); + temp_file_ = Pathname(temp_dir_.pathname(), kLockFile); + } + + void LockOnThread() { + locker_.Start(this); + done_.Wait(kTimeoutMS); + } + + Event done_; + Thread locker_; + bool thread_lock_failed_; + Pathname temp_dir_; + Pathname temp_file_; +}; + +TEST_F(FileLockTest, TestLockFileDeleted) { + scoped_ptr lock(FileLock::TryLock(temp_file_.pathname())); + EXPECT_TRUE(lock.get() != NULL); + EXPECT_FALSE(Filesystem::IsAbsent(temp_file_.pathname())); + lock->Unlock(); + EXPECT_TRUE(Filesystem::IsAbsent(temp_file_.pathname())); +} + +TEST_F(FileLockTest, TestLock) { + scoped_ptr lock(FileLock::TryLock(temp_file_.pathname())); + EXPECT_TRUE(lock.get() != NULL); +} + +TEST_F(FileLockTest, TestLockX2) { + scoped_ptr lock1(FileLock::TryLock(temp_file_.pathname())); + EXPECT_TRUE(lock1.get() != NULL); + + scoped_ptr lock2(FileLock::TryLock(temp_file_.pathname())); + EXPECT_TRUE(lock2.get() == NULL); +} + +TEST_F(FileLockTest, TestThreadedLock) { + scoped_ptr lock(FileLock::TryLock(temp_file_.pathname())); + EXPECT_TRUE(lock.get() != NULL); + + LockOnThread(); + EXPECT_TRUE(thread_lock_failed_); +} + +} // namespace talk_base diff --git a/talk/base/fileutils.cc b/talk/base/fileutils.cc new file mode 100644 index 000000000..ff34147db --- /dev/null +++ b/talk/base/fileutils.cc @@ -0,0 +1,297 @@ +/* + * 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 + +#ifdef WIN32 +#include "talk/base/win32.h" +#endif + +#include "talk/base/pathutils.h" +#include "talk/base/fileutils.h" +#include "talk/base/stringutils.h" +#include "talk/base/stream.h" + +#ifdef WIN32 +#include "talk/base/win32filesystem.h" +#else +#include "talk/base/unixfilesystem.h" +#endif + +#ifndef WIN32 +#define MAX_PATH 260 +#endif + +namespace talk_base { + +////////////////////////// +// Directory Iterator // +////////////////////////// + +// A DirectoryIterator is created with a given directory. It originally points +// to the first file in the directory, and can be advanecd with Next(). This +// allows you to get information about each file. + + // Constructor +DirectoryIterator::DirectoryIterator() +#ifdef _WIN32 + : handle_(INVALID_HANDLE_VALUE) { +#else + : dir_(NULL), dirent_(NULL) { +#endif +} + + // Destructor +DirectoryIterator::~DirectoryIterator() { +#ifdef WIN32 + if (handle_ != INVALID_HANDLE_VALUE) + ::FindClose(handle_); +#else + if (dir_) + closedir(dir_); +#endif +} + + // Starts traversing a directory. + // dir is the directory to traverse + // returns true if the directory exists and is valid +bool DirectoryIterator::Iterate(const Pathname &dir) { + directory_ = dir.pathname(); +#ifdef WIN32 + if (handle_ != INVALID_HANDLE_VALUE) + ::FindClose(handle_); + std::string d = dir.pathname() + '*'; + handle_ = ::FindFirstFile(ToUtf16(d).c_str(), &data_); + if (handle_ == INVALID_HANDLE_VALUE) + return false; +#else + if (dir_ != NULL) + closedir(dir_); + dir_ = ::opendir(directory_.c_str()); + if (dir_ == NULL) + return false; + dirent_ = readdir(dir_); + if (dirent_ == NULL) + return false; + + if (::stat(std::string(directory_ + Name()).c_str(), &stat_) != 0) + return false; +#endif + return true; +} + + // Advances to the next file + // returns true if there were more files in the directory. +bool DirectoryIterator::Next() { +#ifdef WIN32 + return ::FindNextFile(handle_, &data_) == TRUE; +#else + dirent_ = ::readdir(dir_); + if (dirent_ == NULL) + return false; + + return ::stat(std::string(directory_ + Name()).c_str(), &stat_) == 0; +#endif +} + + // returns true if the file currently pointed to is a directory +bool DirectoryIterator::IsDirectory() const { +#ifdef WIN32 + return (data_.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != FALSE; +#else + return S_ISDIR(stat_.st_mode); +#endif +} + + // returns the name of the file currently pointed to +std::string DirectoryIterator::Name() const { +#ifdef WIN32 + return ToUtf8(data_.cFileName); +#else + assert(dirent_ != NULL); + return dirent_->d_name; +#endif +} + + // returns the size of the file currently pointed to +size_t DirectoryIterator::FileSize() const { +#ifndef WIN32 + return stat_.st_size; +#else + return data_.nFileSizeLow; +#endif +} + + // returns the last modified time of this file +time_t DirectoryIterator::FileModifyTime() const { +#ifdef WIN32 + time_t val; + FileTimeToUnixTime(data_.ftLastWriteTime, &val); + return val; +#else + return stat_.st_mtime; +#endif +} + +FilesystemInterface* Filesystem::default_filesystem_ = NULL; + +FilesystemInterface *Filesystem::EnsureDefaultFilesystem() { + if (!default_filesystem_) { +#ifdef WIN32 + default_filesystem_ = new Win32Filesystem(); +#else + default_filesystem_ = new UnixFilesystem(); +#endif + } + return default_filesystem_; +} + +bool FilesystemInterface::CopyFolder(const Pathname &old_path, + const Pathname &new_path) { + bool success = true; + VERIFY(IsFolder(old_path)); + Pathname new_dir; + new_dir.SetFolder(new_path.pathname()); + Pathname old_dir; + old_dir.SetFolder(old_path.pathname()); + if (!CreateFolder(new_dir)) + return false; + DirectoryIterator *di = IterateDirectory(); + if (!di) + return false; + if (di->Iterate(old_dir.pathname())) { + do { + if (di->Name() == "." || di->Name() == "..") + continue; + Pathname source; + Pathname dest; + source.SetFolder(old_dir.pathname()); + dest.SetFolder(new_path.pathname()); + source.SetFilename(di->Name()); + dest.SetFilename(di->Name()); + if (!CopyFileOrFolder(source, dest)) + success = false; + } while (di->Next()); + } + delete di; + return success; +} + +bool FilesystemInterface::DeleteFolderContents(const Pathname &folder) { + bool success = true; + VERIFY(IsFolder(folder)); + DirectoryIterator *di = IterateDirectory(); + if (!di) + return false; + if (di->Iterate(folder)) { + do { + if (di->Name() == "." || di->Name() == "..") + continue; + Pathname subdir; + subdir.SetFolder(folder.pathname()); + if (di->IsDirectory()) { + subdir.AppendFolder(di->Name()); + if (!DeleteFolderAndContents(subdir)) { + success = false; + } + } else { + subdir.SetFilename(di->Name()); + if (!DeleteFile(subdir)) { + success = false; + } + } + } while (di->Next()); + } + delete di; + return success; +} + +bool FilesystemInterface::CleanAppTempFolder() { + Pathname path; + if (!GetAppTempFolder(&path)) + return false; + if (IsAbsent(path)) + return true; + if (!IsTemporaryPath(path)) { + ASSERT(false); + return false; + } + return DeleteFolderContents(path); +} + +Pathname Filesystem::GetCurrentDirectory() { + return EnsureDefaultFilesystem()->GetCurrentDirectory(); +} + +bool CreateUniqueFile(Pathname& path, bool create_empty) { + LOG(LS_INFO) << "Path " << path.pathname() << std::endl; + // If no folder is supplied, use the temporary folder + if (path.folder().empty()) { + Pathname temporary_path; + if (!Filesystem::GetTemporaryFolder(temporary_path, true, NULL)) { + printf("Get temp failed\n"); + return false; + } + path.SetFolder(temporary_path.pathname()); + } + + // If no filename is supplied, use a temporary name + if (path.filename().empty()) { + std::string folder(path.folder()); + std::string filename = Filesystem::TempFilename(folder, "gt"); + path.SetPathname(filename); + if (!create_empty) { + Filesystem::DeleteFile(path.pathname()); + } + return true; + } + + // Otherwise, create a unique name based on the given filename + // foo.txt -> foo-N.txt + const std::string basename = path.basename(); + const size_t MAX_VERSION = 100; + size_t version = 0; + while (version < MAX_VERSION) { + std::string pathname = path.pathname(); + + if (!Filesystem::IsFile(pathname)) { + if (create_empty) { + FileStream* fs = Filesystem::OpenFile(pathname, "w"); + delete fs; + } + return true; + } + version += 1; + char version_base[MAX_PATH]; + sprintfn(version_base, ARRAY_SIZE(version_base), "%s-%u", + basename.c_str(), version); + path.SetBasename(version_base); + } + return true; +} + +} // namespace talk_base diff --git a/talk/base/fileutils.h b/talk/base/fileutils.h new file mode 100644 index 000000000..186c96332 --- /dev/null +++ b/talk/base/fileutils.h @@ -0,0 +1,457 @@ +/* + * 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. + */ + +#ifndef TALK_BASE_FILEUTILS_H_ +#define TALK_BASE_FILEUTILS_H_ + +#include + +#ifdef WIN32 +#include "talk/base/win32.h" +#else +#include +#include +#include +#include +#endif + +#include "talk/base/basictypes.h" +#include "talk/base/common.h" +#include "talk/base/scoped_ptr.h" + +namespace talk_base { + +class FileStream; +class Pathname; + +////////////////////////// +// Directory Iterator // +////////////////////////// + +// A DirectoryIterator is created with a given directory. It originally points +// to the first file in the directory, and can be advanecd with Next(). This +// allows you to get information about each file. + +class DirectoryIterator { + friend class Filesystem; + public: + // Constructor + DirectoryIterator(); + // Destructor + virtual ~DirectoryIterator(); + + // Starts traversing a directory + // dir is the directory to traverse + // returns true if the directory exists and is valid + // The iterator will point to the first entry in the directory + virtual bool Iterate(const Pathname &path); + + // Advances to the next file + // returns true if there were more files in the directory. + virtual bool Next(); + + // returns true if the file currently pointed to is a directory + virtual bool IsDirectory() const; + + // returns the name of the file currently pointed to + virtual std::string Name() const; + + // returns the size of the file currently pointed to + virtual size_t FileSize() const; + + // returns the last modified time of the file currently pointed to + virtual time_t FileModifyTime() const; + + // checks whether current file is a special directory file "." or ".." + bool IsDots() const { + std::string filename(Name()); + return (filename.compare(".") == 0) || (filename.compare("..") == 0); + } + + private: + std::string directory_; +#ifdef WIN32 + WIN32_FIND_DATA data_; + HANDLE handle_; +#else + DIR *dir_; + struct dirent *dirent_; + struct stat stat_; +#endif +}; + +enum FileTimeType { FTT_CREATED, FTT_MODIFIED, FTT_ACCESSED }; + +class FilesystemInterface { + public: + virtual ~FilesystemInterface() {} + + // Returns a DirectoryIterator for a given pathname. + // TODO: Do fancy abstracted stuff + virtual DirectoryIterator *IterateDirectory() { + return new DirectoryIterator(); + } + + // Opens a file. Returns an open StreamInterface if function succeeds. + // Otherwise, returns NULL. + // TODO: Add an error param to indicate failure reason, similar to + // FileStream::Open + virtual FileStream *OpenFile(const Pathname &filename, + const std::string &mode) = 0; + + // Atomically creates an empty file accessible only to the current user if one + // does not already exist at the given path, otherwise fails. This is the only + // secure way to create a file in a shared temp directory (e.g., C:\Temp on + // Windows or /tmp on Linux). + // Note that if it is essential that a file be successfully created then the + // app must generate random names and retry on failure, or else it will be + // vulnerable to a trivial DoS. + virtual bool CreatePrivateFile(const Pathname &filename) = 0; + + // This will attempt to delete the path located at filename. + // It ASSERTS and returns false if the path points to a folder or a + // non-existent file. + virtual bool DeleteFile(const Pathname &filename) = 0; + + // This will attempt to delete the empty folder located at 'folder' + // It ASSERTS and returns false if the path points to a file or a non-existent + // folder. It fails normally if the folder is not empty or can otherwise + // not be deleted. + virtual bool DeleteEmptyFolder(const Pathname &folder) = 0; + + // This will call IterateDirectory, to get a directory iterator, and then + // call DeleteFolderAndContents and DeleteFile on every path contained in this + // folder. If the folder is empty, this returns true. + virtual bool DeleteFolderContents(const Pathname &folder); + + // This deletes the contents of a folder, recursively, and then deletes + // the folder itself. + virtual bool DeleteFolderAndContents(const Pathname &folder) { + return DeleteFolderContents(folder) && DeleteEmptyFolder(folder); + } + + // This will delete whatever is located at path, be it a file or a folder. + // If it is a folder, it will delete it recursively by calling + // DeleteFolderAndContents + bool DeleteFileOrFolder(const Pathname &path) { + if (IsFolder(path)) + return DeleteFolderAndContents(path); + else + return DeleteFile(path); + } + + // Creates a directory. This will call itself recursively to create /foo/bar + // even if /foo does not exist. Returns true if the function succeeds. + virtual bool CreateFolder(const Pathname &pathname) = 0; + + // This moves a file from old_path to new_path, where "old_path" is a + // plain file. This ASSERTs and returns false if old_path points to a + // directory, and returns true if the function succeeds. + // If the new path is on a different volume than the old path, this function + // will attempt to copy and, if that succeeds, delete the old path. + virtual bool MoveFolder(const Pathname &old_path, + const Pathname &new_path) = 0; + + // This moves a directory from old_path to new_path, where "old_path" is a + // directory. This ASSERTs and returns false if old_path points to a plain + // file, and returns true if the function succeeds. + // If the new path is on a different volume, this function will attempt to + // copy and if that succeeds, delete the old path. + virtual bool MoveFile(const Pathname &old_path, const Pathname &new_path) = 0; + + // This attempts to move whatever is located at old_path to new_path, + // be it a file or folder. + bool MoveFileOrFolder(const Pathname &old_path, const Pathname &new_path) { + if (IsFile(old_path)) { + return MoveFile(old_path, new_path); + } else { + return MoveFolder(old_path, new_path); + } + } + + // This copies a file from old_path to new_path. This method ASSERTs and + // returns false if old_path is a folder, and returns true if the copy + // succeeds. + virtual bool CopyFile(const Pathname &old_path, const Pathname &new_path) = 0; + + // This copies a folder from old_path to new_path. + bool CopyFolder(const Pathname &old_path, const Pathname &new_path); + + bool CopyFileOrFolder(const Pathname &old_path, const Pathname &new_path) { + if (IsFile(old_path)) + return CopyFile(old_path, new_path); + else + return CopyFolder(old_path, new_path); + } + + // Returns true if pathname refers to a directory + virtual bool IsFolder(const Pathname& pathname) = 0; + + // Returns true if pathname refers to a file + virtual bool IsFile(const Pathname& pathname) = 0; + + // Returns true if pathname refers to no filesystem object, every parent + // directory either exists, or is also absent. + virtual bool IsAbsent(const Pathname& pathname) = 0; + + // Returns true if pathname represents a temporary location on the system. + virtual bool IsTemporaryPath(const Pathname& pathname) = 0; + + // A folder appropriate for storing temporary files (Contents are + // automatically deleted when the program exits) + virtual bool GetTemporaryFolder(Pathname &path, bool create, + const std::string *append) = 0; + + virtual std::string TempFilename(const Pathname &dir, + const std::string &prefix) = 0; + + // Determines the size of the file indicated by path. + virtual bool GetFileSize(const Pathname& path, size_t* size) = 0; + + // Determines a timestamp associated with the file indicated by path. + virtual bool GetFileTime(const Pathname& path, FileTimeType which, + time_t* time) = 0; + + // Returns the path to the running application. + // Note: This is not guaranteed to work on all platforms. Be aware of the + // limitations before using it, and robustly handle failure. + virtual bool GetAppPathname(Pathname* path) = 0; + + // Get a folder that is unique to the current application, which is suitable + // for sharing data between executions of the app. If the per_user arg is + // true, the folder is also specific to the current user. + virtual bool GetAppDataFolder(Pathname* path, bool per_user) = 0; + + // Get a temporary folder that is unique to the current user and application. + // TODO: Re-evaluate the goals of this function. We probably just need any + // directory that won't collide with another existing directory, and which + // will be cleaned up when the program exits. + virtual bool GetAppTempFolder(Pathname* path) = 0; + + // Delete the contents of the folder returned by GetAppTempFolder + bool CleanAppTempFolder(); + + virtual bool GetDiskFreeSpace(const Pathname& path, int64 *freebytes) = 0; + + // Returns the absolute path of the current directory. + virtual Pathname GetCurrentDirectory() = 0; + + // Note: These might go into some shared config section later, but they're + // used by some methods in this interface, so we're leaving them here for now. + void SetOrganizationName(const std::string& organization) { + organization_name_ = organization; + } + void GetOrganizationName(std::string* organization) { + ASSERT(NULL != organization); + *organization = organization_name_; + } + void SetApplicationName(const std::string& application) { + application_name_ = application; + } + void GetApplicationName(std::string* application) { + ASSERT(NULL != application); + *application = application_name_; + } + + protected: + std::string organization_name_; + std::string application_name_; +}; + +class Filesystem { + public: + static FilesystemInterface *default_filesystem() { + ASSERT(default_filesystem_ != NULL); + return default_filesystem_; + } + + static void set_default_filesystem(FilesystemInterface *filesystem) { + default_filesystem_ = filesystem; + } + + static FilesystemInterface *swap_default_filesystem( + FilesystemInterface *filesystem) { + FilesystemInterface *cur = default_filesystem_; + default_filesystem_ = filesystem; + return cur; + } + + static DirectoryIterator *IterateDirectory() { + return EnsureDefaultFilesystem()->IterateDirectory(); + } + + static bool CreateFolder(const Pathname &pathname) { + return EnsureDefaultFilesystem()->CreateFolder(pathname); + } + + static FileStream *OpenFile(const Pathname &filename, + const std::string &mode) { + return EnsureDefaultFilesystem()->OpenFile(filename, mode); + } + + static bool CreatePrivateFile(const Pathname &filename) { + return EnsureDefaultFilesystem()->CreatePrivateFile(filename); + } + + static bool DeleteFile(const Pathname &filename) { + return EnsureDefaultFilesystem()->DeleteFile(filename); + } + + static bool DeleteEmptyFolder(const Pathname &folder) { + return EnsureDefaultFilesystem()->DeleteEmptyFolder(folder); + } + + static bool DeleteFolderContents(const Pathname &folder) { + return EnsureDefaultFilesystem()->DeleteFolderContents(folder); + } + + static bool DeleteFolderAndContents(const Pathname &folder) { + return EnsureDefaultFilesystem()->DeleteFolderAndContents(folder); + } + + static bool MoveFolder(const Pathname &old_path, const Pathname &new_path) { + return EnsureDefaultFilesystem()->MoveFolder(old_path, new_path); + } + + static bool MoveFile(const Pathname &old_path, const Pathname &new_path) { + return EnsureDefaultFilesystem()->MoveFile(old_path, new_path); + } + + static bool CopyFolder(const Pathname &old_path, const Pathname &new_path) { + return EnsureDefaultFilesystem()->CopyFolder(old_path, new_path); + } + + static bool CopyFile(const Pathname &old_path, const Pathname &new_path) { + return EnsureDefaultFilesystem()->CopyFile(old_path, new_path); + } + + static bool IsFolder(const Pathname& pathname) { + return EnsureDefaultFilesystem()->IsFolder(pathname); + } + + static bool IsFile(const Pathname &pathname) { + return EnsureDefaultFilesystem()->IsFile(pathname); + } + + static bool IsAbsent(const Pathname &pathname) { + return EnsureDefaultFilesystem()->IsAbsent(pathname); + } + + static bool IsTemporaryPath(const Pathname& pathname) { + return EnsureDefaultFilesystem()->IsTemporaryPath(pathname); + } + + static bool GetTemporaryFolder(Pathname &path, bool create, + const std::string *append) { + return EnsureDefaultFilesystem()->GetTemporaryFolder(path, create, append); + } + + static std::string TempFilename(const Pathname &dir, + const std::string &prefix) { + return EnsureDefaultFilesystem()->TempFilename(dir, prefix); + } + + static bool GetFileSize(const Pathname& path, size_t* size) { + return EnsureDefaultFilesystem()->GetFileSize(path, size); + } + + static bool GetFileTime(const Pathname& path, FileTimeType which, + time_t* time) { + return EnsureDefaultFilesystem()->GetFileTime(path, which, time); + } + + static bool GetAppPathname(Pathname* path) { + return EnsureDefaultFilesystem()->GetAppPathname(path); + } + + static bool GetAppDataFolder(Pathname* path, bool per_user) { + return EnsureDefaultFilesystem()->GetAppDataFolder(path, per_user); + } + + static bool GetAppTempFolder(Pathname* path) { + return EnsureDefaultFilesystem()->GetAppTempFolder(path); + } + + static bool CleanAppTempFolder() { + return EnsureDefaultFilesystem()->CleanAppTempFolder(); + } + + static bool GetDiskFreeSpace(const Pathname& path, int64 *freebytes) { + return EnsureDefaultFilesystem()->GetDiskFreeSpace(path, freebytes); + } + + // Definition has to be in the .cc file due to returning forward-declared + // Pathname by value. + static Pathname GetCurrentDirectory(); + + static void SetOrganizationName(const std::string& organization) { + EnsureDefaultFilesystem()->SetOrganizationName(organization); + } + + static void GetOrganizationName(std::string* organization) { + EnsureDefaultFilesystem()->GetOrganizationName(organization); + } + + static void SetApplicationName(const std::string& application) { + EnsureDefaultFilesystem()->SetApplicationName(application); + } + + static void GetApplicationName(std::string* application) { + EnsureDefaultFilesystem()->GetApplicationName(application); + } + + private: + static FilesystemInterface* default_filesystem_; + + static FilesystemInterface *EnsureDefaultFilesystem(); + DISALLOW_IMPLICIT_CONSTRUCTORS(Filesystem); +}; + +class FilesystemScope{ + public: + explicit FilesystemScope(FilesystemInterface *new_fs) { + old_fs_ = Filesystem::swap_default_filesystem(new_fs); + } + ~FilesystemScope() { + Filesystem::set_default_filesystem(old_fs_); + } + private: + FilesystemInterface* old_fs_; + DISALLOW_IMPLICIT_CONSTRUCTORS(FilesystemScope); +}; + +// Generates a unique filename based on the input path. If no path component +// is specified, it uses the temporary directory. If a filename is provided, +// up to 100 variations of form basename-N.extension are tried. When +// create_empty is true, an empty file of this name is created (which +// decreases the chance of a temporary filename collision with another +// process). +bool CreateUniqueFile(Pathname& path, bool create_empty); + +} // namespace talk_base + +#endif // TALK_BASE_FILEUTILS_H_ diff --git a/talk/base/fileutils_mock.h b/talk/base/fileutils_mock.h new file mode 100644 index 000000000..b91e80279 --- /dev/null +++ b/talk/base/fileutils_mock.h @@ -0,0 +1,270 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifndef TALK_BASE_FILEUTILS_MOCK_H_ +#define TALK_BASE_FILEUTILS_MOCK_H_ + +#include +#include +#include + +#include "talk/base/fileutils.h" +#include "talk/base/gunit.h" +#include "talk/base/pathutils.h" +#include "talk/base/stream.h" + +namespace talk_base { + +class FakeFileStream : public FileStream { + public: + explicit FakeFileStream(const std::string & contents) : + string_stream_(contents) + {} + + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + return string_stream_.Read(buffer, buffer_len, read, error); + } + + virtual void Close() { + return string_stream_.Close(); + } + virtual bool GetSize(size_t* size) const { + return string_stream_.GetSize(size); + } + + private: + StringStream string_stream_; +}; + +class FakeDirectoryIterator : public DirectoryIterator { + public: + typedef std::pair File; + + /* + * files should be sorted by directory + * put '/' at the end of file if you want it to be a directory + * + * Sample list: + * /var/dir/file1 + * /var/dir/file2 + * /var/dir/subdir1/ + * /var/dir/subdir2/ + * /var/dir2/file2 + * /var/dir3/ + * + * you can call Iterate for any path: /var, /var/dir, /var/dir2 + * unrelated files will be ignored + */ + explicit FakeDirectoryIterator(const std::vector& all_files) : + all_files_(all_files) {} + + virtual bool Iterate(const Pathname& path) { + path_iterator_ = all_files_.begin(); + path_ = path.pathname(); + + // make sure path ends end with '/' + if (path_.rfind(Pathname::DefaultFolderDelimiter()) != path_.size() - 1) + path_ += Pathname::DefaultFolderDelimiter(); + + return FakeDirectoryIterator::Search(std::string("")); + } + + virtual bool Next() { + std::string current_name = Name(); + path_iterator_++; + return FakeDirectoryIterator::Search(current_name); + } + + bool Search(const std::string& current_name) { + for (; path_iterator_ != all_files_.end(); path_iterator_++) { + if (path_iterator_->first.find(path_) == 0 + && Name().compare(current_name) != 0) { + return true; + } + } + + return false; + } + + virtual bool IsDirectory() const { + std::string sub_path = path_iterator_->first; + + return std::string::npos != + sub_path.find(Pathname::DefaultFolderDelimiter(), path_.size()); + } + + virtual std::string Name() const { + std::string sub_path = path_iterator_->first; + + // path - top level path (ex. /var/lib) + // sub_path - subpath under top level path (ex. /var/lib/dir/dir/file ) + // find shortest non-trivial common path. (ex. /var/lib/dir) + size_t start = path_.size(); + size_t end = sub_path.find(Pathname::DefaultFolderDelimiter(), start); + + if (end != std::string::npos) { + return sub_path.substr(start, end - start); + } else { + return sub_path.substr(start); + } + } + + private: + const std::vector all_files_; + + std::string path_; + std::vector::const_iterator path_iterator_; +}; + +class FakeFileSystem : public FilesystemInterface { + public: + typedef std::pair File; + + explicit FakeFileSystem(const std::vector& all_files) : + all_files_(all_files) {} + + virtual DirectoryIterator *IterateDirectory() { + return new FakeDirectoryIterator(all_files_); + } + + virtual FileStream * OpenFile( + const Pathname &filename, + const std::string &mode) { + std::vector::const_iterator i_files = all_files_.begin(); + std::string path = filename.pathname(); + + for (; i_files != all_files_.end(); i_files++) { + if (i_files->first.compare(path) == 0) { + return new FakeFileStream(i_files->second); + } + } + + return NULL; + } + + bool CreatePrivateFile(const Pathname &filename) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool DeleteFile(const Pathname &filename) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool DeleteEmptyFolder(const Pathname &folder) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool DeleteFolderContents(const Pathname &folder) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool DeleteFolderAndContents(const Pathname &folder) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool CreateFolder(const Pathname &pathname) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool MoveFolder(const Pathname &old_path, const Pathname &new_path) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool MoveFile(const Pathname &old_path, const Pathname &new_path) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool CopyFile(const Pathname &old_path, const Pathname &new_path) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool IsFolder(const Pathname &pathname) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool IsFile(const Pathname &pathname) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool IsAbsent(const Pathname &pathname) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool IsTemporaryPath(const Pathname &pathname) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool GetTemporaryFolder(Pathname &path, bool create, + const std::string *append) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + std::string TempFilename(const Pathname &dir, const std::string &prefix) { + EXPECT_TRUE(false) << "Unsupported operation"; + return std::string(); + } + bool GetFileSize(const Pathname &path, size_t *size) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool GetFileTime(const Pathname &path, FileTimeType which, + time_t* time) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool GetAppPathname(Pathname *path) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool GetAppDataFolder(Pathname *path, bool per_user) { + EXPECT_TRUE(per_user) << "Unsupported operation"; +#ifdef WIN32 + path->SetPathname("c:\\Users\\test_user", ""); +#else + path->SetPathname("/home/user/test_user", ""); +#endif + return true; + } + bool GetAppTempFolder(Pathname *path) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + bool GetDiskFreeSpace(const Pathname &path, int64 *freebytes) { + EXPECT_TRUE(false) << "Unsupported operation"; + return false; + } + Pathname GetCurrentDirectory() { + return Pathname(); + } + + private: + const std::vector all_files_; +}; +} // namespace talk_base + +#endif // TALK_BASE_FILEUTILS_MOCK_H_ diff --git a/talk/base/fileutils_unittest.cc b/talk/base/fileutils_unittest.cc new file mode 100644 index 000000000..992ea82f1 --- /dev/null +++ b/talk/base/fileutils_unittest.cc @@ -0,0 +1,148 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/fileutils.h" +#include "talk/base/gunit.h" +#include "talk/base/pathutils.h" +#include "talk/base/stream.h" + +namespace talk_base { + +// Make sure we can get a temp folder for the later tests. +TEST(FilesystemTest, GetTemporaryFolder) { + Pathname path; + EXPECT_TRUE(Filesystem::GetTemporaryFolder(path, true, NULL)); +} + +// Test creating a temp file, reading it back in, and deleting it. +TEST(FilesystemTest, TestOpenFile) { + Pathname path; + EXPECT_TRUE(Filesystem::GetTemporaryFolder(path, true, NULL)); + path.SetPathname(Filesystem::TempFilename(path, "ut")); + + FileStream* fs; + char buf[256]; + size_t bytes; + + fs = Filesystem::OpenFile(path, "wb"); + ASSERT_TRUE(fs != NULL); + EXPECT_EQ(SR_SUCCESS, fs->Write("test", 4, &bytes, NULL)); + EXPECT_EQ(4U, bytes); + delete fs; + + EXPECT_TRUE(Filesystem::IsFile(path)); + + fs = Filesystem::OpenFile(path, "rb"); + ASSERT_TRUE(fs != NULL); + EXPECT_EQ(SR_SUCCESS, fs->Read(buf, sizeof(buf), &bytes, NULL)); + EXPECT_EQ(4U, bytes); + delete fs; + + EXPECT_TRUE(Filesystem::DeleteFile(path)); + EXPECT_FALSE(Filesystem::IsFile(path)); +} + +// Test opening a non-existent file. +TEST(FilesystemTest, TestOpenBadFile) { + Pathname path; + EXPECT_TRUE(Filesystem::GetTemporaryFolder(path, true, NULL)); + path.SetFilename("not an actual file"); + + EXPECT_FALSE(Filesystem::IsFile(path)); + + FileStream* fs = Filesystem::OpenFile(path, "rb"); + EXPECT_FALSE(fs != NULL); +} + +// Test that CreatePrivateFile fails for existing files and succeeds for +// non-existent ones. +TEST(FilesystemTest, TestCreatePrivateFile) { + Pathname path; + EXPECT_TRUE(Filesystem::GetTemporaryFolder(path, true, NULL)); + path.SetFilename("private_file_test"); + + // First call should succeed because the file doesn't exist yet. + EXPECT_TRUE(Filesystem::CreatePrivateFile(path)); + // Next call should fail, because now it exists. + EXPECT_FALSE(Filesystem::CreatePrivateFile(path)); + + // Verify that we have permission to open the file for reading and writing. + scoped_ptr fs(Filesystem::OpenFile(path, "wb")); + EXPECT_TRUE(fs.get() != NULL); + // Have to close the file on Windows before it will let us delete it. + fs.reset(); + + // Verify that we have permission to delete the file. + EXPECT_TRUE(Filesystem::DeleteFile(path)); +} + +// Test checking for free disk space. +TEST(FilesystemTest, TestGetDiskFreeSpace) { + // Note that we should avoid picking any file/folder which could be located + // at the remotely mounted drive/device. + Pathname path; + ASSERT_TRUE(Filesystem::GetAppDataFolder(&path, true)); + + int64 free1 = 0; + EXPECT_TRUE(Filesystem::IsFolder(path)); + EXPECT_FALSE(Filesystem::IsFile(path)); + EXPECT_TRUE(Filesystem::GetDiskFreeSpace(path, &free1)); + EXPECT_GT(free1, 0); + + int64 free2 = 0; + path.AppendFolder("this_folder_doesnt_exist"); + EXPECT_FALSE(Filesystem::IsFolder(path)); + EXPECT_TRUE(Filesystem::IsAbsent(path)); + EXPECT_TRUE(Filesystem::GetDiskFreeSpace(path, &free2)); + // These should be the same disk, and disk free space should not have changed + // by more than 1% between the two calls. + EXPECT_LT(static_cast(free1 * .9), free2); + EXPECT_LT(free2, static_cast(free1 * 1.1)); + + int64 free3 = 0; + path.clear(); + EXPECT_TRUE(path.empty()); + EXPECT_TRUE(Filesystem::GetDiskFreeSpace(path, &free3)); + // Current working directory may not be where exe is. + // EXPECT_LT(static_cast(free1 * .9), free3); + // EXPECT_LT(free3, static_cast(free1 * 1.1)); + EXPECT_GT(free3, 0); +} + +// Tests that GetCurrentDirectory() returns something. +TEST(FilesystemTest, TestGetCurrentDirectory) { + EXPECT_FALSE(Filesystem::GetCurrentDirectory().empty()); +} + +// Tests that GetAppPathname returns something. +TEST(FilesystemTest, TestGetAppPathname) { + Pathname path; + EXPECT_TRUE(Filesystem::GetAppPathname(&path)); + EXPECT_FALSE(path.empty()); +} + +} // namespace talk_base diff --git a/talk/base/firewallsocketserver.cc b/talk/base/firewallsocketserver.cc new file mode 100644 index 000000000..ee2c22d48 --- /dev/null +++ b/talk/base/firewallsocketserver.cc @@ -0,0 +1,254 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/firewallsocketserver.h" + +#include +#include + +#include "talk/base/asyncsocket.h" +#include "talk/base/logging.h" + +namespace talk_base { + +class FirewallSocket : public AsyncSocketAdapter { + public: + FirewallSocket(FirewallSocketServer* server, AsyncSocket* socket, int type) + : AsyncSocketAdapter(socket), server_(server), type_(type) { + } + + virtual int Connect(const SocketAddress& addr) { + if (type_ == SOCK_STREAM) { + if (!server_->Check(FP_TCP, GetLocalAddress(), addr)) { + LOG(LS_VERBOSE) << "FirewallSocket outbound TCP connection from " + << GetLocalAddress().ToSensitiveString() << " to " + << addr.ToSensitiveString() << " denied"; + // TODO: Handle this asynchronously. + SetError(EHOSTUNREACH); + return SOCKET_ERROR; + } + } + return AsyncSocketAdapter::Connect(addr); + } + virtual int Send(const void* pv, size_t cb) { + return SendTo(pv, cb, GetRemoteAddress()); + } + virtual int SendTo(const void* pv, size_t cb, const SocketAddress& addr) { + if (type_ == SOCK_DGRAM) { + if (!server_->Check(FP_UDP, GetLocalAddress(), addr)) { + LOG(LS_VERBOSE) << "FirewallSocket outbound UDP packet from " + << GetLocalAddress().ToSensitiveString() << " to " + << addr.ToSensitiveString() << " dropped"; + return static_cast(cb); + } + } + return AsyncSocketAdapter::SendTo(pv, cb, addr); + } + virtual int Recv(void* pv, size_t cb) { + SocketAddress addr; + return RecvFrom(pv, cb, &addr); + } + virtual int RecvFrom(void* pv, size_t cb, SocketAddress* paddr) { + if (type_ == SOCK_DGRAM) { + while (true) { + int res = AsyncSocketAdapter::RecvFrom(pv, cb, paddr); + if (res <= 0) + return res; + if (server_->Check(FP_UDP, *paddr, GetLocalAddress())) + return res; + LOG(LS_VERBOSE) << "FirewallSocket inbound UDP packet from " + << paddr->ToSensitiveString() << " to " + << GetLocalAddress().ToSensitiveString() << " dropped"; + } + } + return AsyncSocketAdapter::RecvFrom(pv, cb, paddr); + } + + virtual int Listen(int backlog) { + if (!server_->tcp_listen_enabled()) { + LOG(LS_VERBOSE) << "FirewallSocket listen attempt denied"; + return -1; + } + + return AsyncSocketAdapter::Listen(backlog); + } + virtual AsyncSocket* Accept(SocketAddress* paddr) { + SocketAddress addr; + while (AsyncSocket* sock = AsyncSocketAdapter::Accept(&addr)) { + if (server_->Check(FP_TCP, addr, GetLocalAddress())) { + if (paddr) + *paddr = addr; + return sock; + } + sock->Close(); + delete sock; + LOG(LS_VERBOSE) << "FirewallSocket inbound TCP connection from " + << addr.ToSensitiveString() << " to " + << GetLocalAddress().ToSensitiveString() << " denied"; + } + return 0; + } + + private: + FirewallSocketServer* server_; + int type_; +}; + +FirewallSocketServer::FirewallSocketServer(SocketServer* server, + FirewallManager* manager, + bool should_delete_server) + : server_(server), manager_(manager), + should_delete_server_(should_delete_server), + udp_sockets_enabled_(true), tcp_sockets_enabled_(true), + tcp_listen_enabled_(true) { + if (manager_) + manager_->AddServer(this); +} + +FirewallSocketServer::~FirewallSocketServer() { + if (manager_) + manager_->RemoveServer(this); + + if (server_ && should_delete_server_) { + delete server_; + server_ = NULL; + } +} + +void FirewallSocketServer::AddRule(bool allow, FirewallProtocol p, + FirewallDirection d, + const SocketAddress& addr) { + SocketAddress src, dst; + if (d == FD_IN) { + dst = addr; + } else { + src = addr; + } + AddRule(allow, p, src, dst); +} + + +void FirewallSocketServer::AddRule(bool allow, FirewallProtocol p, + const SocketAddress& src, + const SocketAddress& dst) { + Rule r; + r.allow = allow; + r.p = p; + r.src = src; + r.dst = dst; + CritScope scope(&crit_); + rules_.push_back(r); +} + +void FirewallSocketServer::ClearRules() { + CritScope scope(&crit_); + rules_.clear(); +} + +bool FirewallSocketServer::Check(FirewallProtocol p, + const SocketAddress& src, + const SocketAddress& dst) { + CritScope scope(&crit_); + for (size_t i = 0; i < rules_.size(); ++i) { + const Rule& r = rules_[i]; + if ((r.p != p) && (r.p != FP_ANY)) + continue; + if ((r.src.ipaddr() != src.ipaddr()) && !r.src.IsNil()) + continue; + if ((r.src.port() != src.port()) && (r.src.port() != 0)) + continue; + if ((r.dst.ipaddr() != dst.ipaddr()) && !r.dst.IsNil()) + continue; + if ((r.dst.port() != dst.port()) && (r.dst.port() != 0)) + continue; + return r.allow; + } + return true; +} + +Socket* FirewallSocketServer::CreateSocket(int type) { + return CreateSocket(AF_INET, type); +} + +Socket* FirewallSocketServer::CreateSocket(int family, int type) { + return WrapSocket(server_->CreateAsyncSocket(family, type), type); +} + +AsyncSocket* FirewallSocketServer::CreateAsyncSocket(int type) { + return CreateAsyncSocket(AF_INET, type); +} + +AsyncSocket* FirewallSocketServer::CreateAsyncSocket(int family, int type) { + return WrapSocket(server_->CreateAsyncSocket(family, type), type); +} + +AsyncSocket* FirewallSocketServer::WrapSocket(AsyncSocket* sock, int type) { + if (!sock || + (type == SOCK_STREAM && !tcp_sockets_enabled_) || + (type == SOCK_DGRAM && !udp_sockets_enabled_)) { + LOG(LS_VERBOSE) << "FirewallSocketServer socket creation denied"; + return NULL; + } + return new FirewallSocket(this, sock, type); +} + +FirewallManager::FirewallManager() { +} + +FirewallManager::~FirewallManager() { + assert(servers_.empty()); +} + +void FirewallManager::AddServer(FirewallSocketServer* server) { + CritScope scope(&crit_); + servers_.push_back(server); +} + +void FirewallManager::RemoveServer(FirewallSocketServer* server) { + CritScope scope(&crit_); + servers_.erase(std::remove(servers_.begin(), servers_.end(), server), + servers_.end()); +} + +void FirewallManager::AddRule(bool allow, FirewallProtocol p, + FirewallDirection d, const SocketAddress& addr) { + CritScope scope(&crit_); + for (std::vector::const_iterator it = + servers_.begin(); it != servers_.end(); ++it) { + (*it)->AddRule(allow, p, d, addr); + } +} + +void FirewallManager::ClearRules() { + CritScope scope(&crit_); + for (std::vector::const_iterator it = + servers_.begin(); it != servers_.end(); ++it) { + (*it)->ClearRules(); + } +} + +} // namespace talk_base diff --git a/talk/base/firewallsocketserver.h b/talk/base/firewallsocketserver.h new file mode 100644 index 000000000..aa63c1172 --- /dev/null +++ b/talk/base/firewallsocketserver.h @@ -0,0 +1,137 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_FIREWALLSOCKETSERVER_H_ +#define TALK_BASE_FIREWALLSOCKETSERVER_H_ + +#include +#include "talk/base/socketserver.h" +#include "talk/base/criticalsection.h" + +namespace talk_base { + +class FirewallManager; + +// This SocketServer shim simulates a rule-based firewall server. + +enum FirewallProtocol { FP_UDP, FP_TCP, FP_ANY }; +enum FirewallDirection { FD_IN, FD_OUT, FD_ANY }; + +class FirewallSocketServer : public SocketServer { + public: + FirewallSocketServer(SocketServer * server, + FirewallManager * manager = NULL, + bool should_delete_server = false); + virtual ~FirewallSocketServer(); + + SocketServer* socketserver() const { return server_; } + void set_socketserver(SocketServer* server) { + if (server_ && should_delete_server_) { + delete server_; + server_ = NULL; + should_delete_server_ = false; + } + server_ = server; + } + + // Settings to control whether CreateSocket or Socket::Listen succeed. + void set_udp_sockets_enabled(bool enabled) { udp_sockets_enabled_ = enabled; } + void set_tcp_sockets_enabled(bool enabled) { tcp_sockets_enabled_ = enabled; } + bool tcp_listen_enabled() const { return tcp_listen_enabled_; } + void set_tcp_listen_enabled(bool enabled) { tcp_listen_enabled_ = enabled; } + + // Rules govern the behavior of Connect/Accept/Send/Recv attempts. + void AddRule(bool allow, FirewallProtocol p = FP_ANY, + FirewallDirection d = FD_ANY, + const SocketAddress& addr = SocketAddress()); + void AddRule(bool allow, FirewallProtocol p, + const SocketAddress& src, const SocketAddress& dst); + void ClearRules(); + + bool Check(FirewallProtocol p, + const SocketAddress& src, const SocketAddress& dst); + + virtual Socket* CreateSocket(int type); + virtual Socket* CreateSocket(int family, int type); + + virtual AsyncSocket* CreateAsyncSocket(int type); + virtual AsyncSocket* CreateAsyncSocket(int family, int type); + + virtual void SetMessageQueue(MessageQueue* queue) { + server_->SetMessageQueue(queue); + } + virtual bool Wait(int cms, bool process_io) { + return server_->Wait(cms, process_io); + } + virtual void WakeUp() { + return server_->WakeUp(); + } + + Socket * WrapSocket(Socket * sock, int type); + AsyncSocket * WrapSocket(AsyncSocket * sock, int type); + + private: + SocketServer * server_; + FirewallManager * manager_; + CriticalSection crit_; + struct Rule { + bool allow; + FirewallProtocol p; + FirewallDirection d; + SocketAddress src; + SocketAddress dst; + }; + std::vector rules_; + bool should_delete_server_; + bool udp_sockets_enabled_; + bool tcp_sockets_enabled_; + bool tcp_listen_enabled_; +}; + +// FirewallManager allows you to manage firewalls in multiple threads together + +class FirewallManager { + public: + FirewallManager(); + ~FirewallManager(); + + void AddServer(FirewallSocketServer * server); + void RemoveServer(FirewallSocketServer * server); + + void AddRule(bool allow, FirewallProtocol p = FP_ANY, + FirewallDirection d = FD_ANY, + const SocketAddress& addr = SocketAddress()); + void ClearRules(); + + private: + CriticalSection crit_; + std::vector servers_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_FIREWALLSOCKETSERVER_H_ diff --git a/talk/base/flags.cc b/talk/base/flags.cc new file mode 100644 index 000000000..10aee04fe --- /dev/null +++ b/talk/base/flags.cc @@ -0,0 +1,315 @@ +/* + * libjingle + * Copyright 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 +#include +#include + + +#ifdef WIN32 +#include "talk/base/win32.h" +#include +#endif + +#include "talk/base/flags.h" + + +// ----------------------------------------------------------------------------- +// Implementation of Flag + +Flag::Flag(const char* file, const char* name, const char* comment, + Type type, void* variable, FlagValue default__) + : file_(file), + name_(name), + comment_(comment), + type_(type), + variable_(reinterpret_cast(variable)), + default_(default__) { + FlagList::Register(this); +} + + +void Flag::SetToDefault() { + // Note that we cannot simply do '*variable_ = default_;' since + // flag variables are not really of type FlagValue and thus may + // be smaller! The FlagValue union is simply 'overlayed' on top + // of a flag variable for convenient access. Since union members + // are guarantee to be aligned at the beginning, this works. + switch (type_) { + case Flag::BOOL: + variable_->b = default_.b; + return; + case Flag::INT: + variable_->i = default_.i; + return; + case Flag::FLOAT: + variable_->f = default_.f; + return; + case Flag::STRING: + variable_->s = default_.s; + return; + } + UNREACHABLE(); +} + + +static const char* Type2String(Flag::Type type) { + switch (type) { + case Flag::BOOL: return "bool"; + case Flag::INT: return "int"; + case Flag::FLOAT: return "float"; + case Flag::STRING: return "string"; + } + UNREACHABLE(); + return NULL; +} + + +static void PrintFlagValue(Flag::Type type, FlagValue* p) { + switch (type) { + case Flag::BOOL: + printf("%s", (p->b ? "true" : "false")); + return; + case Flag::INT: + printf("%d", p->i); + return; + case Flag::FLOAT: + printf("%f", p->f); + return; + case Flag::STRING: + printf("%s", p->s); + return; + } + UNREACHABLE(); +} + + +void Flag::Print(bool print_current_value) { + printf(" --%s (%s) type: %s default: ", name_, comment_, + Type2String(type_)); + PrintFlagValue(type_, &default_); + if (print_current_value) { + printf(" current value: "); + PrintFlagValue(type_, variable_); + } + printf("\n"); +} + + +// ----------------------------------------------------------------------------- +// Implementation of FlagList + +Flag* FlagList::list_ = NULL; + + +FlagList::FlagList() { + list_ = NULL; +} + +void FlagList::Print(const char* file, bool print_current_value) { + // Since flag registration is likely by file (= C++ file), + // we don't need to sort by file and still get grouped output. + const char* current = NULL; + for (Flag* f = list_; f != NULL; f = f->next()) { + if (file == NULL || file == f->file()) { + if (current != f->file()) { + printf("Flags from %s:\n", f->file()); + current = f->file(); + } + f->Print(print_current_value); + } + } +} + + +Flag* FlagList::Lookup(const char* name) { + Flag* f = list_; + while (f != NULL && strcmp(name, f->name()) != 0) + f = f->next(); + return f; +} + + +void FlagList::SplitArgument(const char* arg, + char* buffer, int buffer_size, + const char** name, const char** value, + bool* is_bool) { + *name = NULL; + *value = NULL; + *is_bool = false; + + if (*arg == '-') { + // find the begin of the flag name + arg++; // remove 1st '-' + if (*arg == '-') + arg++; // remove 2nd '-' + if (arg[0] == 'n' && arg[1] == 'o') { + arg += 2; // remove "no" + *is_bool = true; + } + *name = arg; + + // find the end of the flag name + while (*arg != '\0' && *arg != '=') + arg++; + + // get the value if any + if (*arg == '=') { + // make a copy so we can NUL-terminate flag name + int n = static_cast(arg - *name); + if (n >= buffer_size) + Fatal(__FILE__, __LINE__, "CHECK(%s) failed", "n < buffer_size"); + memcpy(buffer, *name, n * sizeof(char)); + buffer[n] = '\0'; + *name = buffer; + // get the value + *value = arg + 1; + } + } +} + + +int FlagList::SetFlagsFromCommandLine(int* argc, const char** argv, + bool remove_flags) { + // parse arguments + for (int i = 1; i < *argc; /* see below */) { + int j = i; // j > 0 + const char* arg = argv[i++]; + + // split arg into flag components + char buffer[1024]; + const char* name; + const char* value; + bool is_bool; + SplitArgument(arg, buffer, sizeof buffer, &name, &value, &is_bool); + + if (name != NULL) { + // lookup the flag + Flag* flag = Lookup(name); + if (flag == NULL) { + fprintf(stderr, "Error: unrecognized flag %s\n", arg); + return j; + } + + // if we still need a flag value, use the next argument if available + if (flag->type() != Flag::BOOL && value == NULL) { + if (i < *argc) { + value = argv[i++]; + } else { + fprintf(stderr, "Error: missing value for flag %s of type %s\n", + arg, Type2String(flag->type())); + return j; + } + } + + // set the flag + char empty[] = { '\0' }; + char* endp = empty; + switch (flag->type()) { + case Flag::BOOL: + *flag->bool_variable() = !is_bool; + break; + case Flag::INT: + *flag->int_variable() = strtol(value, &endp, 10); + break; + case Flag::FLOAT: + *flag->float_variable() = strtod(value, &endp); + break; + case Flag::STRING: + *flag->string_variable() = value; + break; + } + + // handle errors + if ((flag->type() == Flag::BOOL && value != NULL) || + (flag->type() != Flag::BOOL && is_bool) || + *endp != '\0') { + fprintf(stderr, "Error: illegal value for flag %s of type %s\n", + arg, Type2String(flag->type())); + return j; + } + + // remove the flag & value from the command + if (remove_flags) + while (j < i) + argv[j++] = NULL; + } + } + + // shrink the argument list + if (remove_flags) { + int j = 1; + for (int i = 1; i < *argc; i++) { + if (argv[i] != NULL) + argv[j++] = argv[i]; + } + *argc = j; + } + + // parsed all flags successfully + return 0; +} + +void FlagList::Register(Flag* flag) { + assert(flag != NULL && strlen(flag->name()) > 0); + if (Lookup(flag->name()) != NULL) + Fatal(flag->file(), 0, "flag %s declared twice", flag->name()); + flag->next_ = list_; + list_ = flag; +} + +#ifdef WIN32 +WindowsCommandLineArguments::WindowsCommandLineArguments() { + // start by getting the command line. + LPTSTR command_line = ::GetCommandLine(); + // now, convert it to a list of wide char strings. + LPWSTR *wide_argv = ::CommandLineToArgvW(command_line, &argc_); + // now allocate an array big enough to hold that many string pointers. + argv_ = new char*[argc_]; + + // iterate over the returned wide strings; + for(int i = 0; i < argc_; ++i) { + std::string s = talk_base::ToUtf8(wide_argv[i], wcslen(wide_argv[i])); + char *buffer = new char[s.length() + 1]; + talk_base::strcpyn(buffer, s.length() + 1, s.c_str()); + + // make sure the argv array has the right string at this point. + argv_[i] = buffer; + } + LocalFree(wide_argv); +} + +WindowsCommandLineArguments::~WindowsCommandLineArguments() { + // need to free each string in the array, and then the array. + for(int i = 0; i < argc_; i++) { + delete[] argv_[i]; + } + + delete[] argv_; +} +#endif // WIN32 + diff --git a/talk/base/flags.h b/talk/base/flags.h new file mode 100644 index 000000000..f22e12510 --- /dev/null +++ b/talk/base/flags.h @@ -0,0 +1,284 @@ +/* + * libjingle + * Copyright 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. + */ + + +// Originally comes from shared/commandlineflags/flags.h + +// Flags are defined and declared using DEFINE_xxx and DECLARE_xxx macros, +// where xxx is the flag type. Flags are referred to via FLAG_yyy, +// where yyy is the flag name. For intialization and iteration of flags, +// see the FlagList class. For full programmatic access to any +// flag, see the Flag class. +// +// The implementation only relies and basic C++ functionality +// and needs no special library or STL support. + +#ifndef TALK_BASE_FLAGS_H__ +#define TALK_BASE_FLAGS_H__ + +#include + +#include "talk/base/checks.h" +#include "talk/base/common.h" + +// Internal use only. +union FlagValue { + // Note: Because in C++ non-bool values are silently converted into + // bool values ('bool b = "false";' results in b == true!), we pass + // and int argument to New_BOOL as this appears to be safer - sigh. + // In particular, it prevents the (not uncommon!) bug where a bool + // flag is defined via: DEFINE_bool(flag, "false", "some comment");. + static FlagValue New_BOOL(int b) { + FlagValue v; + v.b = (b != 0); + return v; + } + + static FlagValue New_INT(int i) { + FlagValue v; + v.i = i; + return v; + } + + static FlagValue New_FLOAT(float f) { + FlagValue v; + v.f = f; + return v; + } + + static FlagValue New_STRING(const char* s) { + FlagValue v; + v.s = s; + return v; + } + + bool b; + int i; + double f; + const char* s; +}; + + +// Each flag can be accessed programmatically via a Flag object. +class Flag { + public: + enum Type { BOOL, INT, FLOAT, STRING }; + + // Internal use only. + Flag(const char* file, const char* name, const char* comment, + Type type, void* variable, FlagValue default_); + + // General flag information + const char* file() const { return file_; } + const char* name() const { return name_; } + const char* comment() const { return comment_; } + + // Flag type + Type type() const { return type_; } + + // Flag variables + bool* bool_variable() const { + assert(type_ == BOOL); + return &variable_->b; + } + + int* int_variable() const { + assert(type_ == INT); + return &variable_->i; + } + + double* float_variable() const { + assert(type_ == FLOAT); + return &variable_->f; + } + + const char** string_variable() const { + assert(type_ == STRING); + return &variable_->s; + } + + // Default values + bool bool_default() const { + assert(type_ == BOOL); + return default_.b; + } + + int int_default() const { + assert(type_ == INT); + return default_.i; + } + + double float_default() const { + assert(type_ == FLOAT); + return default_.f; + } + + const char* string_default() const { + assert(type_ == STRING); + return default_.s; + } + + // Resets a flag to its default value + void SetToDefault(); + + // Iteration support + Flag* next() const { return next_; } + + // Prints flag information. The current flag value is only printed + // if print_current_value is set. + void Print(bool print_current_value); + + private: + const char* file_; + const char* name_; + const char* comment_; + + Type type_; + FlagValue* variable_; + FlagValue default_; + + Flag* next_; + + friend class FlagList; // accesses next_ +}; + + +// Internal use only. +#define DEFINE_FLAG(type, c_type, name, default, comment) \ + /* define and initialize the flag */ \ + c_type FLAG_##name = (default); \ + /* register the flag */ \ + static Flag Flag_##name(__FILE__, #name, (comment), \ + Flag::type, &FLAG_##name, \ + FlagValue::New_##type(default)) + + +// Internal use only. +#define DECLARE_FLAG(c_type, name) \ + /* declare the external flag */ \ + extern c_type FLAG_##name + + +// Use the following macros to define a new flag: +#define DEFINE_bool(name, default, comment) \ + DEFINE_FLAG(BOOL, bool, name, default, comment) +#define DEFINE_int(name, default, comment) \ + DEFINE_FLAG(INT, int, name, default, comment) +#define DEFINE_float(name, default, comment) \ + DEFINE_FLAG(FLOAT, double, name, default, comment) +#define DEFINE_string(name, default, comment) \ + DEFINE_FLAG(STRING, const char*, name, default, comment) + + +// Use the following macros to declare a flag defined elsewhere: +#define DECLARE_bool(name) DECLARE_FLAG(bool, name) +#define DECLARE_int(name) DECLARE_FLAG(int, name) +#define DECLARE_float(name) DECLARE_FLAG(double, name) +#define DECLARE_string(name) DECLARE_FLAG(const char*, name) + + +// The global list of all flags. +class FlagList { + public: + FlagList(); + + // The NULL-terminated list of all flags. Traverse with Flag::next(). + static Flag* list() { return list_; } + + // If file != NULL, prints information for all flags defined in file; + // otherwise prints information for all flags in all files. The current + // flag value is only printed if print_current_value is set. + static void Print(const char* file, bool print_current_value); + + // Lookup a flag by name. Returns the matching flag or NULL. + static Flag* Lookup(const char* name); + + // Helper function to parse flags: Takes an argument arg and splits it into + // a flag name and flag value (or NULL if they are missing). is_bool is set + // if the arg started with "-no" or "--no". The buffer may be used to NUL- + // terminate the name, it must be large enough to hold any possible name. + static void SplitArgument(const char* arg, + char* buffer, int buffer_size, + const char** name, const char** value, + bool* is_bool); + + // Set the flag values by parsing the command line. If remove_flags + // is set, the flags and associated values are removed from (argc, + // argv). Returns 0 if no error occurred. Otherwise, returns the + // argv index > 0 for the argument where an error occurred. In that + // case, (argc, argv) will remain unchanged indepdendent of the + // remove_flags value, and no assumptions about flag settings should + // be made. + // + // The following syntax for flags is accepted (both '-' and '--' are ok): + // + // --flag (bool flags only) + // --noflag (bool flags only) + // --flag=value (non-bool flags only, no spaces around '=') + // --flag value (non-bool flags only) + static int SetFlagsFromCommandLine(int* argc, + const char** argv, + bool remove_flags); + static inline int SetFlagsFromCommandLine(int* argc, + char** argv, + bool remove_flags) { + return SetFlagsFromCommandLine(argc, const_cast(argv), + remove_flags); + } + + // Registers a new flag. Called during program initialization. Not + // thread-safe. + static void Register(Flag* flag); + + private: + static Flag* list_; +}; + +#ifdef WIN32 +// A helper class to translate Windows command line arguments into UTF8, +// which then allows us to just pass them to the flags system. +// This encapsulates all the work of getting the command line and translating +// it to an array of 8-bit strings; all you have to do is create one of these, +// and then call argc() and argv(). +class WindowsCommandLineArguments { + public: + WindowsCommandLineArguments(); + ~WindowsCommandLineArguments(); + + int argc() { return argc_; } + char **argv() { return argv_; } + private: + int argc_; + char **argv_; + + private: + DISALLOW_EVIL_CONSTRUCTORS(WindowsCommandLineArguments); +}; +#endif // WIN32 + + +#endif // SHARED_COMMANDLINEFLAGS_FLAGS_H__ diff --git a/talk/base/gunit.h b/talk/base/gunit.h new file mode 100644 index 000000000..3a0321468 --- /dev/null +++ b/talk/base/gunit.h @@ -0,0 +1,112 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +#ifndef TALK_BASE_GUNIT_H_ +#define TALK_BASE_GUNIT_H_ + +#include "talk/base/logging.h" +#include "talk/base/thread.h" +#if defined(ANDROID) || defined(GTEST_RELATIVE_PATH) +#include "gtest/gtest.h" +#else +#include "testing/base/public/gunit.h" +#endif + +// forward declarations +namespace talk_base { +class Pathname; +} + +// Wait until "ex" is true, or "timeout" expires. +#define WAIT(ex, timeout) \ + for (uint32 start = talk_base::Time(); \ + !(ex) && talk_base::Time() < start + timeout;) \ + talk_base::Thread::Current()->ProcessMessages(1); + +// This returns the result of the test in res, so that we don't re-evaluate +// the expression in the XXXX_WAIT macros below, since that causes problems +// when the expression is only true the first time you check it. +#define WAIT_(ex, timeout, res) \ + do { \ + uint32 start = talk_base::Time(); \ + res = (ex); \ + while (!res && talk_base::Time() < start + timeout) { \ + talk_base::Thread::Current()->ProcessMessages(1); \ + res = (ex); \ + } \ + } while (0); + +// The typical EXPECT_XXXX and ASSERT_XXXXs, but done until true or a timeout. +#define EXPECT_TRUE_WAIT(ex, timeout) \ + do { \ + bool res; \ + WAIT_(ex, timeout, res); \ + if (!res) EXPECT_TRUE(ex); \ + } while (0); + +#define EXPECT_EQ_WAIT(v1, v2, timeout) \ + do { \ + bool res; \ + WAIT_(v1 == v2, timeout, res); \ + if (!res) EXPECT_EQ(v1, v2); \ + } while (0); + +#define ASSERT_TRUE_WAIT(ex, timeout) \ + do { \ + bool res; \ + WAIT_(ex, timeout, res); \ + if (!res) ASSERT_TRUE(ex); \ + } while (0); + +#define ASSERT_EQ_WAIT(v1, v2, timeout) \ + do { \ + bool res; \ + WAIT_(v1 == v2, timeout, res); \ + if (!res) ASSERT_EQ(v1, v2); \ + } while (0); + +// Version with a "soft" timeout and a margin. This logs if the timeout is +// exceeded, but it only fails if the expression still isn't true after the +// margin time passes. +#define EXPECT_TRUE_WAIT_MARGIN(ex, timeout, margin) \ + do { \ + bool res; \ + WAIT_(ex, timeout, res); \ + if (res) { \ + break; \ + } \ + LOG(LS_WARNING) << "Expression " << #ex << " still not true after " << \ + timeout << "ms; waiting an additional " << margin << "ms"; \ + WAIT_(ex, margin, res); \ + if (!res) { \ + EXPECT_TRUE(ex); \ + } \ + } while (0); + +talk_base::Pathname GetTalkDirectory(); + +#endif // TALK_BASE_GUNIT_H_ diff --git a/talk/base/gunit_prod.h b/talk/base/gunit_prod.h new file mode 100644 index 000000000..6be5c53f9 --- /dev/null +++ b/talk/base/gunit_prod.h @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_BASE_GUNIT_PROD_H_ +#define TALK_BASE_GUNIT_PROD_H_ + +#if defined(ANDROID) || defined (GTEST_RELATIVE_PATH) +#include "gtest/gtest_prod.h" +#else +#include "testing/base/gunit_prod.h" +#endif + +#endif // TALK_BASE_GUNIT_PROD_H_ diff --git a/talk/base/helpers.cc b/talk/base/helpers.cc new file mode 100644 index 000000000..c2d989b4d --- /dev/null +++ b/talk/base/helpers.cc @@ -0,0 +1,289 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/helpers.h" + +#include + +#include "talk/base/sslconfig.h" +#if defined(SSL_USE_OPENSSL) +#include +#elif defined(SSL_USE_NSS_RNG) +#include "pk11func.h" +#else +#ifdef WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#endif // WIN32 +#endif + +#include "talk/base/base64.h" +#include "talk/base/basictypes.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/timeutils.h" + +// Protect against max macro inclusion. +#undef max + +namespace talk_base { + +// Base class for RNG implementations. +class RandomGenerator { + public: + virtual ~RandomGenerator() {} + virtual bool Init(const void* seed, size_t len) = 0; + virtual bool Generate(void* buf, size_t len) = 0; +}; + +#if defined(SSL_USE_OPENSSL) +// The OpenSSL RNG. Need to make sure it doesn't run out of entropy. +class SecureRandomGenerator : public RandomGenerator { + public: + SecureRandomGenerator() : inited_(false) { + } + ~SecureRandomGenerator() { + } + virtual bool Init(const void* seed, size_t len) { + // By default, seed from the system state. + if (!inited_) { + if (RAND_poll() <= 0) { + return false; + } + inited_ = true; + } + // Allow app data to be mixed in, if provided. + if (seed) { + RAND_seed(seed, len); + } + return true; + } + virtual bool Generate(void* buf, size_t len) { + if (!inited_ && !Init(NULL, 0)) { + return false; + } + return (RAND_bytes(reinterpret_cast(buf), len) > 0); + } + + private: + bool inited_; +}; + +#elif defined(SSL_USE_NSS_RNG) +// The NSS RNG. +class SecureRandomGenerator : public RandomGenerator { + public: + SecureRandomGenerator() {} + ~SecureRandomGenerator() {} + virtual bool Init(const void* seed, size_t len) { + return true; + } + virtual bool Generate(void* buf, size_t len) { + return (PK11_GenerateRandom(reinterpret_cast(buf), + static_cast(len)) == SECSuccess); + } +}; + +#else +#ifdef WIN32 +class SecureRandomGenerator : public RandomGenerator { + public: + SecureRandomGenerator() : advapi32_(NULL), rtl_gen_random_(NULL) {} + ~SecureRandomGenerator() { + FreeLibrary(advapi32_); + } + + virtual bool Init(const void* seed, size_t seed_len) { + // We don't do any additional seeding on Win32, we just use the CryptoAPI + // RNG (which is exposed as a hidden function off of ADVAPI32 so that we + // don't need to drag in all of CryptoAPI) + if (rtl_gen_random_) { + return true; + } + + advapi32_ = LoadLibrary(L"advapi32.dll"); + if (!advapi32_) { + return false; + } + + rtl_gen_random_ = reinterpret_cast( + GetProcAddress(advapi32_, "SystemFunction036")); + if (!rtl_gen_random_) { + FreeLibrary(advapi32_); + return false; + } + + return true; + } + virtual bool Generate(void* buf, size_t len) { + if (!rtl_gen_random_ && !Init(NULL, 0)) { + return false; + } + return (rtl_gen_random_(buf, static_cast(len)) != FALSE); + } + + private: + typedef BOOL (WINAPI *RtlGenRandomProc)(PVOID, ULONG); + HINSTANCE advapi32_; + RtlGenRandomProc rtl_gen_random_; +}; + +#else + +#error No SSL implementation has been selected! + +#endif // WIN32 +#endif + +// A test random generator, for predictable output. +class TestRandomGenerator : public RandomGenerator { + public: + TestRandomGenerator() : seed_(7) { + } + ~TestRandomGenerator() { + } + virtual bool Init(const void* seed, size_t len) { + return true; + } + virtual bool Generate(void* buf, size_t len) { + for (size_t i = 0; i < len; ++i) { + static_cast(buf)[i] = static_cast(GetRandom()); + } + return true; + } + + private: + int GetRandom() { + return ((seed_ = seed_ * 214013L + 2531011L) >> 16) & 0x7fff; + } + int seed_; +}; + +// TODO: Use Base64::Base64Table instead. +static const char BASE64[64] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' +}; + +namespace { + +// This round about way of creating a global RNG is to safe-guard against +// indeterminant static initialization order. +scoped_ptr& GetGlobalRng() { + LIBJINGLE_DEFINE_STATIC_LOCAL(scoped_ptr, global_rng, + (new SecureRandomGenerator())); + return global_rng; +} + +RandomGenerator& Rng() { + return *GetGlobalRng(); +} + +} // namespace + +void SetRandomTestMode(bool test) { + if (!test) { + GetGlobalRng().reset(new SecureRandomGenerator()); + } else { + GetGlobalRng().reset(new TestRandomGenerator()); + } +} + +bool InitRandom(int seed) { + return InitRandom(reinterpret_cast(&seed), sizeof(seed)); +} + +bool InitRandom(const char* seed, size_t len) { + if (!Rng().Init(seed, len)) { + LOG(LS_ERROR) << "Failed to init random generator!"; + return false; + } + return true; +} + +std::string CreateRandomString(size_t len) { + std::string str; + CreateRandomString(len, &str); + return str; +} + +bool CreateRandomString(size_t len, + const char* table, int table_size, + std::string* str) { + str->clear(); + scoped_array bytes(new uint8[len]); + if (!Rng().Generate(bytes.get(), len)) { + LOG(LS_ERROR) << "Failed to generate random string!"; + return false; + } + str->reserve(len); + for (size_t i = 0; i < len; ++i) { + str->push_back(table[bytes[i] % table_size]); + } + return true; +} + +bool CreateRandomString(size_t len, std::string* str) { + return CreateRandomString(len, BASE64, 64, str); +} + +bool CreateRandomString(size_t len, const std::string& table, + std::string* str) { + return CreateRandomString(len, table.c_str(), + static_cast(table.size()), str); +} + +uint32 CreateRandomId() { + uint32 id; + if (!Rng().Generate(&id, sizeof(id))) { + LOG(LS_ERROR) << "Failed to generate random id!"; + } + return id; +} + +uint64 CreateRandomId64() { + return static_cast (CreateRandomId()) << 32 | CreateRandomId(); +} + +uint32 CreateRandomNonZeroId() { + uint32 id; + do { + id = CreateRandomId(); + } while (id == 0); + return id; +} + +double CreateRandomDouble() { + return CreateRandomId() / (std::numeric_limits::max() + + std::numeric_limits::epsilon()); +} + +} // namespace talk_base diff --git a/talk/base/helpers.h b/talk/base/helpers.h new file mode 100644 index 000000000..a297554ec --- /dev/null +++ b/talk/base/helpers.h @@ -0,0 +1,73 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_HELPERS_H_ +#define TALK_BASE_HELPERS_H_ + +#include +#include "talk/base/basictypes.h" + +namespace talk_base { + +// For testing, we can return predictable data. +void SetRandomTestMode(bool test); + +// Initializes the RNG, and seeds it with the specified entropy. +bool InitRandom(int seed); +bool InitRandom(const char* seed, size_t len); + +// Generates a (cryptographically) random string of the given length. +// We generate base64 values so that they will be printable. +// WARNING: could silently fail. Use the version below instead. +std::string CreateRandomString(size_t length); + +// Generates a (cryptographically) random string of the given length. +// We generate base64 values so that they will be printable. +// Return false if the random number generator failed. +bool CreateRandomString(size_t length, std::string* str); + +// Generates a (cryptographically) random string of the given length, +// with characters from the given table. Return false if the random +// number generator failed. +bool CreateRandomString(size_t length, const std::string& table, + std::string* str); + +// Generates a random id. +uint32 CreateRandomId(); + +// Generates a 64 bit random id. +uint64 CreateRandomId64(); + +// Generates a random id > 0. +uint32 CreateRandomNonZeroId(); + +// Generates a random double between 0.0 (inclusive) and 1.0 (exclusive). +double CreateRandomDouble(); + +} // namespace talk_base + +#endif // TALK_BASE_HELPERS_H_ diff --git a/talk/base/helpers_unittest.cc b/talk/base/helpers_unittest.cc new file mode 100644 index 000000000..0fe1d5b36 --- /dev/null +++ b/talk/base/helpers_unittest.cc @@ -0,0 +1,83 @@ +/* + * libjingle + * Copyright 2004--2011, 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 + +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" + +namespace talk_base { + +TEST(RandomTest, TestCreateRandomId) { + CreateRandomId(); +} + +TEST(RandomTest, TestCreateRandomDouble) { + for (int i = 0; i < 100; ++i) { + double r = CreateRandomDouble(); + EXPECT_GE(r, 0.0); + EXPECT_LT(r, 1.0); + } +} + +TEST(RandomTest, TestCreateNonZeroRandomId) { + EXPECT_NE(0U, CreateRandomNonZeroId()); +} + +TEST(RandomTest, TestCreateRandomString) { + std::string random = CreateRandomString(256); + EXPECT_EQ(256U, random.size()); + std::string random2; + EXPECT_TRUE(CreateRandomString(256, &random2)); + EXPECT_NE(random, random2); + EXPECT_EQ(256U, random2.size()); +} + +TEST(RandomTest, TestCreateRandomForTest) { + // Make sure we get the output we expect. + SetRandomTestMode(true); + EXPECT_EQ(2154761789U, CreateRandomId()); + EXPECT_EQ("h0ISP4S5SJKH/9EY", CreateRandomString(16)); + + // Reset and make sure we get the same output. + SetRandomTestMode(true); + EXPECT_EQ(2154761789U, CreateRandomId()); + EXPECT_EQ("h0ISP4S5SJKH/9EY", CreateRandomString(16)); + + // Test different character sets. + SetRandomTestMode(true); + std::string str; + EXPECT_TRUE(CreateRandomString(16, "a", &str)); + EXPECT_EQ("aaaaaaaaaaaaaaaa", str); + EXPECT_TRUE(CreateRandomString(16, "abc", &str)); + EXPECT_EQ("acbccaaaabbaacbb", str); + + // Turn off test mode for other tests. + SetRandomTestMode(false); +} + +} // namespace talk_base diff --git a/talk/base/host.cc b/talk/base/host.cc new file mode 100644 index 000000000..7decc49e6 --- /dev/null +++ b/talk/base/host.cc @@ -0,0 +1,49 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/host.h" + +#ifdef POSIX +#include +#endif // POSIX + +#include + +namespace talk_base { + +std::string GetHostName() { + // TODO: fix or get rid of this +#if 0 + struct utsname nm; + if (uname(&nm) < 0) + FatalError("uname", LAST_SYSTEM_ERROR); + return std::string(nm.nodename); +#endif + return "cricket"; +} + +} // namespace talk_base diff --git a/talk/base/host.h b/talk/base/host.h new file mode 100644 index 000000000..8528240a2 --- /dev/null +++ b/talk/base/host.h @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_HOST_H_ +#define TALK_BASE_HOST_H_ + +#include + +namespace talk_base { + +// Returns the name of the local host. +std::string GetHostName(); + +} // namespace talk_base + +#endif // TALK_BASE_HOST_H_ diff --git a/talk/base/host_unittest.cc b/talk/base/host_unittest.cc new file mode 100644 index 000000000..aba87af8f --- /dev/null +++ b/talk/base/host_unittest.cc @@ -0,0 +1,33 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/gunit.h" +#include "talk/base/host.h" + +TEST(Host, GetHostName) { + EXPECT_NE("", talk_base::GetHostName()); +} diff --git a/talk/base/httpbase.cc b/talk/base/httpbase.cc new file mode 100644 index 000000000..90c1a7879 --- /dev/null +++ b/talk/base/httpbase.cc @@ -0,0 +1,893 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +// Copyright 2005 Google Inc. All Rights Reserved. +// + + +#ifdef WIN32 +#include "talk/base/win32.h" +#else // !WIN32 +#define SEC_E_CERT_EXPIRED (-2146893016) +#endif // !WIN32 + +#include "talk/base/common.h" +#include "talk/base/httpbase.h" +#include "talk/base/logging.h" +#include "talk/base/socket.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" + +namespace talk_base { + +////////////////////////////////////////////////////////////////////// +// Helpers +////////////////////////////////////////////////////////////////////// + +bool MatchHeader(const char* str, size_t len, HttpHeader header) { + const char* const header_str = ToString(header); + const size_t header_len = strlen(header_str); + return (len == header_len) && (_strnicmp(str, header_str, header_len) == 0); +} + +enum { + MSG_READ +}; + +////////////////////////////////////////////////////////////////////// +// HttpParser +////////////////////////////////////////////////////////////////////// + +HttpParser::HttpParser() { + reset(); +} + +HttpParser::~HttpParser() { +} + +void +HttpParser::reset() { + state_ = ST_LEADER; + chunked_ = false; + data_size_ = SIZE_UNKNOWN; +} + +HttpParser::ProcessResult +HttpParser::Process(const char* buffer, size_t len, size_t* processed, + HttpError* error) { + *processed = 0; + *error = HE_NONE; + + if (state_ >= ST_COMPLETE) { + ASSERT(false); + return PR_COMPLETE; + } + + while (true) { + if (state_ < ST_DATA) { + size_t pos = *processed; + while ((pos < len) && (buffer[pos] != '\n')) { + pos += 1; + } + if (pos >= len) { + break; // don't have a full header + } + const char* line = buffer + *processed; + size_t len = (pos - *processed); + *processed = pos + 1; + while ((len > 0) && isspace(static_cast(line[len-1]))) { + len -= 1; + } + ProcessResult result = ProcessLine(line, len, error); + LOG(LS_VERBOSE) << "Processed line, result=" << result; + + if (PR_CONTINUE != result) { + return result; + } + } else if (data_size_ == 0) { + if (chunked_) { + state_ = ST_CHUNKTERM; + } else { + return PR_COMPLETE; + } + } else { + size_t available = len - *processed; + if (available <= 0) { + break; // no more data + } + if ((data_size_ != SIZE_UNKNOWN) && (available > data_size_)) { + available = data_size_; + } + size_t read = 0; + ProcessResult result = ProcessData(buffer + *processed, available, read, + error); + LOG(LS_VERBOSE) << "Processed data, result: " << result << " read: " + << read << " err: " << error; + + if (PR_CONTINUE != result) { + return result; + } + *processed += read; + if (data_size_ != SIZE_UNKNOWN) { + data_size_ -= read; + } + } + } + + return PR_CONTINUE; +} + +HttpParser::ProcessResult +HttpParser::ProcessLine(const char* line, size_t len, HttpError* error) { + LOG_F(LS_VERBOSE) << " state: " << state_ << " line: " + << std::string(line, len) << " len: " << len << " err: " + << error; + + switch (state_) { + case ST_LEADER: + state_ = ST_HEADERS; + return ProcessLeader(line, len, error); + + case ST_HEADERS: + if (len > 0) { + const char* value = strchrn(line, len, ':'); + if (!value) { + *error = HE_PROTOCOL; + return PR_COMPLETE; + } + size_t nlen = (value - line); + const char* eol = line + len; + do { + value += 1; + } while ((value < eol) && isspace(static_cast(*value))); + size_t vlen = eol - value; + if (MatchHeader(line, nlen, HH_CONTENT_LENGTH)) { + unsigned int temp_size; + if (sscanf(value, "%u", &temp_size) != 1) { + *error = HE_PROTOCOL; + return PR_COMPLETE; + } + data_size_ = static_cast(temp_size); + } else if (MatchHeader(line, nlen, HH_TRANSFER_ENCODING)) { + if ((vlen == 7) && (_strnicmp(value, "chunked", 7) == 0)) { + chunked_ = true; + } else if ((vlen == 8) && (_strnicmp(value, "identity", 8) == 0)) { + chunked_ = false; + } else { + *error = HE_PROTOCOL; + return PR_COMPLETE; + } + } + return ProcessHeader(line, nlen, value, vlen, error); + } else { + state_ = chunked_ ? ST_CHUNKSIZE : ST_DATA; + return ProcessHeaderComplete(chunked_, data_size_, error); + } + break; + + case ST_CHUNKSIZE: + if (len > 0) { + char* ptr = NULL; + data_size_ = strtoul(line, &ptr, 16); + if (ptr != line + len) { + *error = HE_PROTOCOL; + return PR_COMPLETE; + } + state_ = (data_size_ == 0) ? ST_TRAILERS : ST_DATA; + } else { + *error = HE_PROTOCOL; + return PR_COMPLETE; + } + break; + + case ST_CHUNKTERM: + if (len > 0) { + *error = HE_PROTOCOL; + return PR_COMPLETE; + } else { + state_ = chunked_ ? ST_CHUNKSIZE : ST_DATA; + } + break; + + case ST_TRAILERS: + if (len == 0) { + return PR_COMPLETE; + } + // *error = onHttpRecvTrailer(); + break; + + default: + ASSERT(false); + break; + } + + return PR_CONTINUE; +} + +bool +HttpParser::is_valid_end_of_input() const { + return (state_ == ST_DATA) && (data_size_ == SIZE_UNKNOWN); +} + +void +HttpParser::complete(HttpError error) { + if (state_ < ST_COMPLETE) { + state_ = ST_COMPLETE; + OnComplete(error); + } +} + +////////////////////////////////////////////////////////////////////// +// HttpBase::DocumentStream +////////////////////////////////////////////////////////////////////// + +class BlockingMemoryStream : public ExternalMemoryStream { +public: + BlockingMemoryStream(char* buffer, size_t size) + : ExternalMemoryStream(buffer, size) { } + + virtual StreamResult DoReserve(size_t size, int* error) { + return (buffer_length_ >= size) ? SR_SUCCESS : SR_BLOCK; + } +}; + +class HttpBase::DocumentStream : public StreamInterface { +public: + DocumentStream(HttpBase* base) : base_(base), error_(HE_DEFAULT) { } + + virtual StreamState GetState() const { + if (NULL == base_) + return SS_CLOSED; + if (HM_RECV == base_->mode_) + return SS_OPEN; + return SS_OPENING; + } + + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + if (!base_) { + if (error) *error = error_; + return (HE_NONE == error_) ? SR_EOS : SR_ERROR; + } + + if (HM_RECV != base_->mode_) { + return SR_BLOCK; + } + + // DoReceiveLoop writes http document data to the StreamInterface* document + // member of HttpData. In this case, we want this data to be written + // directly to our buffer. To accomplish this, we wrap our buffer with a + // StreamInterface, and replace the existing document with our wrapper. + // When the method returns, we restore the old document. Ideally, we would + // pass our StreamInterface* to DoReceiveLoop, but due to the callbacks + // of HttpParser, we would still need to store the pointer temporarily. + scoped_ptr + stream(new BlockingMemoryStream(reinterpret_cast(buffer), + buffer_len)); + + // Replace the existing document with our wrapped buffer. + base_->data_->document.swap(stream); + + // Pump the I/O loop. DoReceiveLoop is guaranteed not to attempt to + // complete the I/O process, which means that our wrapper is not in danger + // of being deleted. To ensure this, DoReceiveLoop returns true when it + // wants complete to be called. We make sure to uninstall our wrapper + // before calling complete(). + HttpError http_error; + bool complete = base_->DoReceiveLoop(&http_error); + + // Reinstall the original output document. + base_->data_->document.swap(stream); + + // If we reach the end of the receive stream, we disconnect our stream + // adapter from the HttpBase, and further calls to read will either return + // EOS or ERROR, appropriately. Finally, we call complete(). + StreamResult result = SR_BLOCK; + if (complete) { + HttpBase* base = Disconnect(http_error); + if (error) *error = error_; + result = (HE_NONE == error_) ? SR_EOS : SR_ERROR; + base->complete(http_error); + } + + // Even if we are complete, if some data was read we must return SUCCESS. + // Future Reads will return EOS or ERROR based on the error_ variable. + size_t position; + stream->GetPosition(&position); + if (position > 0) { + if (read) *read = position; + result = SR_SUCCESS; + } + return result; + } + + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error) { + if (error) *error = -1; + return SR_ERROR; + } + + virtual void Close() { + if (base_) { + HttpBase* base = Disconnect(HE_NONE); + if (HM_RECV == base->mode_ && base->http_stream_) { + // Read I/O could have been stalled on the user of this DocumentStream, + // so restart the I/O process now that we've removed ourselves. + base->http_stream_->PostEvent(SE_READ, 0); + } + } + } + + virtual bool GetAvailable(size_t* size) const { + if (!base_ || HM_RECV != base_->mode_) + return false; + size_t data_size = base_->GetDataRemaining(); + if (SIZE_UNKNOWN == data_size) + return false; + if (size) + *size = data_size; + return true; + } + + HttpBase* Disconnect(HttpError error) { + ASSERT(NULL != base_); + ASSERT(NULL != base_->doc_stream_); + HttpBase* base = base_; + base_->doc_stream_ = NULL; + base_ = NULL; + error_ = error; + return base; + } + +private: + HttpBase* base_; + HttpError error_; +}; + +////////////////////////////////////////////////////////////////////// +// HttpBase +////////////////////////////////////////////////////////////////////// + +HttpBase::HttpBase() : mode_(HM_NONE), data_(NULL), notify_(NULL), + http_stream_(NULL), doc_stream_(NULL) { +} + +HttpBase::~HttpBase() { + ASSERT(HM_NONE == mode_); +} + +bool +HttpBase::isConnected() const { + return (http_stream_ != NULL) && (http_stream_->GetState() == SS_OPEN); +} + +bool +HttpBase::attach(StreamInterface* stream) { + if ((mode_ != HM_NONE) || (http_stream_ != NULL) || (stream == NULL)) { + ASSERT(false); + return false; + } + http_stream_ = stream; + http_stream_->SignalEvent.connect(this, &HttpBase::OnHttpStreamEvent); + mode_ = (http_stream_->GetState() == SS_OPENING) ? HM_CONNECT : HM_NONE; + return true; +} + +StreamInterface* +HttpBase::detach() { + ASSERT(HM_NONE == mode_); + if (mode_ != HM_NONE) { + return NULL; + } + StreamInterface* stream = http_stream_; + http_stream_ = NULL; + if (stream) { + stream->SignalEvent.disconnect(this); + } + return stream; +} + +void +HttpBase::send(HttpData* data) { + ASSERT(HM_NONE == mode_); + if (mode_ != HM_NONE) { + return; + } else if (!isConnected()) { + OnHttpStreamEvent(http_stream_, SE_CLOSE, HE_DISCONNECTED); + return; + } + + mode_ = HM_SEND; + data_ = data; + len_ = 0; + ignore_data_ = chunk_data_ = false; + + if (data_->document) { + data_->document->SignalEvent.connect(this, &HttpBase::OnDocumentEvent); + } + + std::string encoding; + if (data_->hasHeader(HH_TRANSFER_ENCODING, &encoding) + && (encoding == "chunked")) { + chunk_data_ = true; + } + + len_ = data_->formatLeader(buffer_, sizeof(buffer_)); + len_ += strcpyn(buffer_ + len_, sizeof(buffer_) - len_, "\r\n"); + + header_ = data_->begin(); + if (header_ == data_->end()) { + // We must call this at least once, in the case where there are no headers. + queue_headers(); + } + + flush_data(); +} + +void +HttpBase::recv(HttpData* data) { + ASSERT(HM_NONE == mode_); + if (mode_ != HM_NONE) { + return; + } else if (!isConnected()) { + OnHttpStreamEvent(http_stream_, SE_CLOSE, HE_DISCONNECTED); + return; + } + + mode_ = HM_RECV; + data_ = data; + len_ = 0; + ignore_data_ = chunk_data_ = false; + + reset(); + if (doc_stream_) { + doc_stream_->SignalEvent(doc_stream_, SE_OPEN | SE_READ, 0); + } else { + read_and_process_data(); + } +} + +void +HttpBase::abort(HttpError err) { + if (mode_ != HM_NONE) { + if (http_stream_ != NULL) { + http_stream_->Close(); + } + do_complete(err); + } +} + +StreamInterface* HttpBase::GetDocumentStream() { + if (doc_stream_) + return NULL; + doc_stream_ = new DocumentStream(this); + return doc_stream_; +} + +HttpError HttpBase::HandleStreamClose(int error) { + if (http_stream_ != NULL) { + http_stream_->Close(); + } + if (error == 0) { + if ((mode_ == HM_RECV) && is_valid_end_of_input()) { + return HE_NONE; + } else { + return HE_DISCONNECTED; + } + } else if (error == SOCKET_EACCES) { + return HE_AUTH; + } else if (error == SEC_E_CERT_EXPIRED) { + return HE_CERTIFICATE_EXPIRED; + } + LOG_F(LS_ERROR) << "(" << error << ")"; + return (HM_CONNECT == mode_) ? HE_CONNECT_FAILED : HE_SOCKET_ERROR; +} + +bool HttpBase::DoReceiveLoop(HttpError* error) { + ASSERT(HM_RECV == mode_); + ASSERT(NULL != error); + + // Do to the latency between receiving read notifications from + // pseudotcpchannel, we rely on repeated calls to read in order to acheive + // ideal throughput. The number of reads is limited to prevent starving + // the caller. + + size_t loop_count = 0; + const size_t kMaxReadCount = 20; + bool process_requires_more_data = false; + do { + // The most frequent use of this function is response to new data available + // on http_stream_. Therefore, we optimize by attempting to read from the + // network first (as opposed to processing existing data first). + + if (len_ < sizeof(buffer_)) { + // Attempt to buffer more data. + size_t read; + int read_error; + StreamResult read_result = http_stream_->Read(buffer_ + len_, + sizeof(buffer_) - len_, + &read, &read_error); + switch (read_result) { + case SR_SUCCESS: + ASSERT(len_ + read <= sizeof(buffer_)); + len_ += read; + break; + case SR_BLOCK: + if (process_requires_more_data) { + // We're can't make progress until more data is available. + return false; + } + // Attempt to process the data already in our buffer. + break; + case SR_EOS: + // Clean close, with no error. Fall through to HandleStreamClose. + read_error = 0; + case SR_ERROR: + *error = HandleStreamClose(read_error); + return true; + } + } else if (process_requires_more_data) { + // We have too much unprocessed data in our buffer. This should only + // occur when a single HTTP header is longer than the buffer size (32K). + // Anything longer than that is almost certainly an error. + *error = HE_OVERFLOW; + return true; + } + + // Process data in our buffer. Process is not guaranteed to process all + // the buffered data. In particular, it will wait until a complete + // protocol element (such as http header, or chunk size) is available, + // before processing it in its entirety. Also, it is valid and sometimes + // necessary to call Process with an empty buffer, since the state machine + // may have interrupted state transitions to complete. + size_t processed; + ProcessResult process_result = Process(buffer_, len_, &processed, + error); + ASSERT(processed <= len_); + len_ -= processed; + memmove(buffer_, buffer_ + processed, len_); + switch (process_result) { + case PR_CONTINUE: + // We need more data to make progress. + process_requires_more_data = true; + break; + case PR_BLOCK: + // We're stalled on writing the processed data. + return false; + case PR_COMPLETE: + // *error already contains the correct code. + return true; + } + } while (++loop_count <= kMaxReadCount); + + LOG_F(LS_WARNING) << "danger of starvation"; + return false; +} + +void +HttpBase::read_and_process_data() { + HttpError error; + if (DoReceiveLoop(&error)) { + complete(error); + } +} + +void +HttpBase::flush_data() { + ASSERT(HM_SEND == mode_); + + // When send_required is true, no more buffering can occur without a network + // write. + bool send_required = (len_ >= sizeof(buffer_)); + + while (true) { + ASSERT(len_ <= sizeof(buffer_)); + + // HTTP is inherently sensitive to round trip latency, since a frequent use + // case is for small requests and responses to be sent back and forth, and + // the lack of pipelining forces a single request to take a minimum of the + // round trip time. As a result, it is to our benefit to pack as much data + // into each packet as possible. Thus, we defer network writes until we've + // buffered as much data as possible. + + if (!send_required && (header_ != data_->end())) { + // First, attempt to queue more header data. + send_required = queue_headers(); + } + + if (!send_required && data_->document) { + // Next, attempt to queue document data. + + const size_t kChunkDigits = 8; + size_t offset, reserve; + if (chunk_data_) { + // Reserve characters at the start for X-byte hex value and \r\n + offset = len_ + kChunkDigits + 2; + // ... and 2 characters at the end for \r\n + reserve = offset + 2; + } else { + offset = len_; + reserve = offset; + } + + if (reserve >= sizeof(buffer_)) { + send_required = true; + } else { + size_t read; + int error; + StreamResult result = data_->document->Read(buffer_ + offset, + sizeof(buffer_) - reserve, + &read, &error); + if (result == SR_SUCCESS) { + ASSERT(reserve + read <= sizeof(buffer_)); + if (chunk_data_) { + // Prepend the chunk length in hex. + // Note: sprintfn appends a null terminator, which is why we can't + // combine it with the line terminator. + sprintfn(buffer_ + len_, kChunkDigits + 1, "%.*x", + kChunkDigits, read); + // Add line terminator to the chunk length. + memcpy(buffer_ + len_ + kChunkDigits, "\r\n", 2); + // Add line terminator to the end of the chunk. + memcpy(buffer_ + offset + read, "\r\n", 2); + } + len_ = reserve + read; + } else if (result == SR_BLOCK) { + // Nothing to do but flush data to the network. + send_required = true; + } else if (result == SR_EOS) { + if (chunk_data_) { + // Append the empty chunk and empty trailers, then turn off + // chunking. + ASSERT(len_ + 5 <= sizeof(buffer_)); + memcpy(buffer_ + len_, "0\r\n\r\n", 5); + len_ += 5; + chunk_data_ = false; + } else if (0 == len_) { + // No more data to read, and no more data to write. + do_complete(); + return; + } + // Although we are done reading data, there is still data which needs + // to be flushed to the network. + send_required = true; + } else { + LOG_F(LS_ERROR) << "Read error: " << error; + do_complete(HE_STREAM); + return; + } + } + } + + if (0 == len_) { + // No data currently available to send. + if (!data_->document) { + // If there is no source document, that means we're done. + do_complete(); + } + return; + } + + size_t written; + int error; + StreamResult result = http_stream_->Write(buffer_, len_, &written, &error); + if (result == SR_SUCCESS) { + ASSERT(written <= len_); + len_ -= written; + memmove(buffer_, buffer_ + written, len_); + send_required = false; + } else if (result == SR_BLOCK) { + if (send_required) { + // Nothing more we can do until network is writeable. + return; + } + } else { + ASSERT(result == SR_ERROR); + LOG_F(LS_ERROR) << "error"; + OnHttpStreamEvent(http_stream_, SE_CLOSE, error); + return; + } + } + + ASSERT(false); +} + +bool +HttpBase::queue_headers() { + ASSERT(HM_SEND == mode_); + while (header_ != data_->end()) { + size_t len = sprintfn(buffer_ + len_, sizeof(buffer_) - len_, + "%.*s: %.*s\r\n", + header_->first.size(), header_->first.data(), + header_->second.size(), header_->second.data()); + if (len_ + len < sizeof(buffer_) - 3) { + len_ += len; + ++header_; + } else if (len_ == 0) { + LOG(WARNING) << "discarding header that is too long: " << header_->first; + ++header_; + } else { + // Not enough room for the next header, write to network first. + return true; + } + } + // End of headers + len_ += strcpyn(buffer_ + len_, sizeof(buffer_) - len_, "\r\n"); + return false; +} + +void +HttpBase::do_complete(HttpError err) { + ASSERT(mode_ != HM_NONE); + HttpMode mode = mode_; + mode_ = HM_NONE; + if (data_ && data_->document) { + data_->document->SignalEvent.disconnect(this); + } + data_ = NULL; + if ((HM_RECV == mode) && doc_stream_) { + ASSERT(HE_NONE != err); // We should have Disconnected doc_stream_ already. + DocumentStream* ds = doc_stream_; + ds->Disconnect(err); + ds->SignalEvent(ds, SE_CLOSE, err); + } + if (notify_) { + notify_->onHttpComplete(mode, err); + } +} + +// +// Stream Signals +// + +void +HttpBase::OnHttpStreamEvent(StreamInterface* stream, int events, int error) { + ASSERT(stream == http_stream_); + if ((events & SE_OPEN) && (mode_ == HM_CONNECT)) { + do_complete(); + return; + } + + if ((events & SE_WRITE) && (mode_ == HM_SEND)) { + flush_data(); + return; + } + + if ((events & SE_READ) && (mode_ == HM_RECV)) { + if (doc_stream_) { + doc_stream_->SignalEvent(doc_stream_, SE_READ, 0); + } else { + read_and_process_data(); + } + return; + } + + if ((events & SE_CLOSE) == 0) + return; + + HttpError http_error = HandleStreamClose(error); + if (mode_ == HM_RECV) { + complete(http_error); + } else if (mode_ != HM_NONE) { + do_complete(http_error); + } else if (notify_) { + notify_->onHttpClosed(http_error); + } +} + +void +HttpBase::OnDocumentEvent(StreamInterface* stream, int events, int error) { + ASSERT(stream == data_->document.get()); + if ((events & SE_WRITE) && (mode_ == HM_RECV)) { + read_and_process_data(); + return; + } + + if ((events & SE_READ) && (mode_ == HM_SEND)) { + flush_data(); + return; + } + + if (events & SE_CLOSE) { + LOG_F(LS_ERROR) << "Read error: " << error; + do_complete(HE_STREAM); + return; + } +} + +// +// HttpParser Implementation +// + +HttpParser::ProcessResult +HttpBase::ProcessLeader(const char* line, size_t len, HttpError* error) { + *error = data_->parseLeader(line, len); + return (HE_NONE == *error) ? PR_CONTINUE : PR_COMPLETE; +} + +HttpParser::ProcessResult +HttpBase::ProcessHeader(const char* name, size_t nlen, const char* value, + size_t vlen, HttpError* error) { + std::string sname(name, nlen), svalue(value, vlen); + data_->addHeader(sname, svalue); + return PR_CONTINUE; +} + +HttpParser::ProcessResult +HttpBase::ProcessHeaderComplete(bool chunked, size_t& data_size, + HttpError* error) { + StreamInterface* old_docstream = doc_stream_; + if (notify_) { + *error = notify_->onHttpHeaderComplete(chunked, data_size); + // The request must not be aborted as a result of this callback. + ASSERT(NULL != data_); + } + if ((HE_NONE == *error) && data_->document) { + data_->document->SignalEvent.connect(this, &HttpBase::OnDocumentEvent); + } + if (HE_NONE != *error) { + return PR_COMPLETE; + } + if (old_docstream != doc_stream_) { + // Break out of Process loop, since our I/O model just changed. + return PR_BLOCK; + } + return PR_CONTINUE; +} + +HttpParser::ProcessResult +HttpBase::ProcessData(const char* data, size_t len, size_t& read, + HttpError* error) { + if (ignore_data_ || !data_->document) { + read = len; + return PR_CONTINUE; + } + int write_error = 0; + switch (data_->document->Write(data, len, &read, &write_error)) { + case SR_SUCCESS: + return PR_CONTINUE; + case SR_BLOCK: + return PR_BLOCK; + case SR_EOS: + LOG_F(LS_ERROR) << "Unexpected EOS"; + *error = HE_STREAM; + return PR_COMPLETE; + case SR_ERROR: + default: + LOG_F(LS_ERROR) << "Write error: " << write_error; + *error = HE_STREAM; + return PR_COMPLETE; + } +} + +void +HttpBase::OnComplete(HttpError err) { + LOG_F(LS_VERBOSE); + do_complete(err); +} + +} // namespace talk_base diff --git a/talk/base/httpbase.h b/talk/base/httpbase.h new file mode 100644 index 000000000..97527eb24 --- /dev/null +++ b/talk/base/httpbase.h @@ -0,0 +1,201 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +// Copyright 2005 Google Inc. All Rights Reserved. +// + + +#ifndef TALK_BASE_HTTPBASE_H__ +#define TALK_BASE_HTTPBASE_H__ + +#include "talk/base/httpcommon.h" + +namespace talk_base { + +class StreamInterface; + +/////////////////////////////////////////////////////////////////////////////// +// HttpParser - Parses an HTTP stream provided via Process and end_of_input, and +// generates events for: +// Structural Elements: Leader, Headers, Document Data +// Events: End of Headers, End of Document, Errors +/////////////////////////////////////////////////////////////////////////////// + +class HttpParser { +public: + enum ProcessResult { PR_CONTINUE, PR_BLOCK, PR_COMPLETE }; + HttpParser(); + virtual ~HttpParser(); + + void reset(); + ProcessResult Process(const char* buffer, size_t len, size_t* processed, + HttpError* error); + bool is_valid_end_of_input() const; + void complete(HttpError err); + + size_t GetDataRemaining() const { return data_size_; } + +protected: + ProcessResult ProcessLine(const char* line, size_t len, HttpError* error); + + // HttpParser Interface + virtual ProcessResult ProcessLeader(const char* line, size_t len, + HttpError* error) = 0; + virtual ProcessResult ProcessHeader(const char* name, size_t nlen, + const char* value, size_t vlen, + HttpError* error) = 0; + virtual ProcessResult ProcessHeaderComplete(bool chunked, size_t& data_size, + HttpError* error) = 0; + virtual ProcessResult ProcessData(const char* data, size_t len, size_t& read, + HttpError* error) = 0; + virtual void OnComplete(HttpError err) = 0; + +private: + enum State { + ST_LEADER, ST_HEADERS, + ST_CHUNKSIZE, ST_CHUNKTERM, ST_TRAILERS, + ST_DATA, ST_COMPLETE + } state_; + bool chunked_; + size_t data_size_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// IHttpNotify +/////////////////////////////////////////////////////////////////////////////// + +enum HttpMode { HM_NONE, HM_CONNECT, HM_RECV, HM_SEND }; + +class IHttpNotify { +public: + virtual ~IHttpNotify() {} + virtual HttpError onHttpHeaderComplete(bool chunked, size_t& data_size) = 0; + virtual void onHttpComplete(HttpMode mode, HttpError err) = 0; + virtual void onHttpClosed(HttpError err) = 0; +}; + +/////////////////////////////////////////////////////////////////////////////// +// HttpBase - Provides a state machine for implementing HTTP-based components. +// Attach HttpBase to a StreamInterface which represents a bidirectional HTTP +// stream, and then call send() or recv() to initiate sending or receiving one +// side of an HTTP transaction. By default, HttpBase operates as an I/O pump, +// moving data from the HTTP stream to the HttpData object and vice versa. +// However, it can also operate in stream mode, in which case the user of the +// stream interface drives I/O via calls to Read(). +/////////////////////////////////////////////////////////////////////////////// + +class HttpBase +: private HttpParser, + public sigslot::has_slots<> +{ +public: + HttpBase(); + virtual ~HttpBase(); + + void notify(IHttpNotify* notify) { notify_ = notify; } + bool attach(StreamInterface* stream); + StreamInterface* stream() { return http_stream_; } + StreamInterface* detach(); + bool isConnected() const; + + void send(HttpData* data); + void recv(HttpData* data); + void abort(HttpError err); + + HttpMode mode() const { return mode_; } + + void set_ignore_data(bool ignore) { ignore_data_ = ignore; } + bool ignore_data() const { return ignore_data_; } + + // Obtaining this stream puts HttpBase into stream mode until the stream + // is closed. HttpBase can only expose one open stream interface at a time. + // Further calls will return NULL. + StreamInterface* GetDocumentStream(); + +protected: + // Do cleanup when the http stream closes (error may be 0 for a clean + // shutdown), and return the error code to signal. + HttpError HandleStreamClose(int error); + + // DoReceiveLoop acts as a data pump, pulling data from the http stream, + // pushing it through the HttpParser, and then populating the HttpData object + // based on the callbacks from the parser. One of the most interesting + // callbacks is ProcessData, which provides the actual http document body. + // This data is then written to the HttpData::document. As a result, data + // flows from the network to the document, with some incidental protocol + // parsing in between. + // Ideally, we would pass in the document* to DoReceiveLoop, to more easily + // support GetDocumentStream(). However, since the HttpParser is callback + // driven, we are forced to store the pointer somewhere until the callback + // is triggered. + // Returns true if the received document has finished, and + // HttpParser::complete should be called. + bool DoReceiveLoop(HttpError* err); + + void read_and_process_data(); + void flush_data(); + bool queue_headers(); + void do_complete(HttpError err = HE_NONE); + + void OnHttpStreamEvent(StreamInterface* stream, int events, int error); + void OnDocumentEvent(StreamInterface* stream, int events, int error); + + // HttpParser Interface + virtual ProcessResult ProcessLeader(const char* line, size_t len, + HttpError* error); + virtual ProcessResult ProcessHeader(const char* name, size_t nlen, + const char* value, size_t vlen, + HttpError* error); + virtual ProcessResult ProcessHeaderComplete(bool chunked, size_t& data_size, + HttpError* error); + virtual ProcessResult ProcessData(const char* data, size_t len, size_t& read, + HttpError* error); + virtual void OnComplete(HttpError err); + +private: + class DocumentStream; + friend class DocumentStream; + + enum { kBufferSize = 32 * 1024 }; + + HttpMode mode_; + HttpData* data_; + IHttpNotify* notify_; + StreamInterface* http_stream_; + DocumentStream* doc_stream_; + char buffer_[kBufferSize]; + size_t len_; + + bool ignore_data_, chunk_data_; + HttpData::const_iterator header_; +}; + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_HTTPBASE_H__ diff --git a/talk/base/httpbase_unittest.cc b/talk/base/httpbase_unittest.cc new file mode 100644 index 000000000..73ef9491f --- /dev/null +++ b/talk/base/httpbase_unittest.cc @@ -0,0 +1,536 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/gunit.h" +#include "talk/base/httpbase.h" +#include "talk/base/testutils.h" + +namespace talk_base { + +const char* const kHttpResponse = + "HTTP/1.1 200\r\n" + "Connection: Keep-Alive\r\n" + "Content-Type: text/plain\r\n" + "Proxy-Authorization: 42\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "00000008\r\n" + "Goodbye!\r\n" + "0\r\n\r\n"; + +const char* const kHttpEmptyResponse = + "HTTP/1.1 200\r\n" + "Connection: Keep-Alive\r\n" + "Content-Length: 0\r\n" + "Proxy-Authorization: 42\r\n" + "\r\n"; + +const char* const kHttpResponsePrefix = + "HTTP/1.1 200\r\n" + "Connection: Keep-Alive\r\n" + "Content-Type: text/plain\r\n" + "Proxy-Authorization: 42\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "8\r\n" + "Goodbye!\r\n"; + +class HttpBaseTest : public testing::Test, public IHttpNotify { +public: + enum EventType { E_HEADER_COMPLETE, E_COMPLETE, E_CLOSED }; + struct Event { + EventType event; + bool chunked; + size_t data_size; + HttpMode mode; + HttpError err; + }; + HttpBaseTest() : mem(NULL), obtain_stream(false), http_stream(NULL) { } + + virtual void SetUp() { } + virtual void TearDown() { + // Avoid an ASSERT, in case a test doesn't clean up properly + base.abort(HE_NONE); + } + + virtual HttpError onHttpHeaderComplete(bool chunked, size_t& data_size) { + LOG_F(LS_VERBOSE) << "chunked: " << chunked << " size: " << data_size; + Event e = { E_HEADER_COMPLETE, chunked, data_size, HM_NONE, HE_NONE}; + events.push_back(e); + if (obtain_stream) { + ObtainDocumentStream(); + } + return HE_NONE; + } + virtual void onHttpComplete(HttpMode mode, HttpError err) { + LOG_F(LS_VERBOSE) << "mode: " << mode << " err: " << err; + Event e = { E_COMPLETE, false, 0, mode, err }; + events.push_back(e); + } + virtual void onHttpClosed(HttpError err) { + LOG_F(LS_VERBOSE) << "err: " << err; + Event e = { E_CLOSED, false, 0, HM_NONE, err }; + events.push_back(e); + } + + void SetupSource(const char* response); + + void VerifyHeaderComplete(size_t event_count, bool empty_doc); + void VerifyDocumentContents(const char* expected_data, + size_t expected_length = SIZE_UNKNOWN); + + void ObtainDocumentStream(); + void VerifyDocumentStreamIsOpening(); + void VerifyDocumentStreamOpenEvent(); + void ReadDocumentStreamData(const char* expected_data); + void VerifyDocumentStreamIsEOS(); + + void SetupDocument(const char* response); + void VerifySourceContents(const char* expected_data, + size_t expected_length = SIZE_UNKNOWN); + + void VerifyTransferComplete(HttpMode mode, HttpError error); + + HttpBase base; + MemoryStream* mem; + HttpResponseData data; + + // The source of http data, and source events + testing::StreamSource src; + std::vector events; + + // Document stream, and stream events + bool obtain_stream; + StreamInterface* http_stream; + testing::StreamSink sink; +}; + +void HttpBaseTest::SetupSource(const char* http_data) { + LOG_F(LS_VERBOSE) << "Enter"; + + src.SetState(SS_OPENING); + src.QueueString(http_data); + + base.notify(this); + base.attach(&src); + EXPECT_TRUE(events.empty()); + + src.SetState(SS_OPEN); + ASSERT_EQ(1U, events.size()); + EXPECT_EQ(E_COMPLETE, events[0].event); + EXPECT_EQ(HM_CONNECT, events[0].mode); + EXPECT_EQ(HE_NONE, events[0].err); + events.clear(); + + mem = new MemoryStream; + data.document.reset(mem); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifyHeaderComplete(size_t event_count, bool empty_doc) { + LOG_F(LS_VERBOSE) << "Enter"; + + ASSERT_EQ(event_count, events.size()); + EXPECT_EQ(E_HEADER_COMPLETE, events[0].event); + + std::string header; + EXPECT_EQ(HVER_1_1, data.version); + EXPECT_EQ(static_cast(HC_OK), data.scode); + EXPECT_TRUE(data.hasHeader(HH_PROXY_AUTHORIZATION, &header)); + EXPECT_EQ("42", header); + EXPECT_TRUE(data.hasHeader(HH_CONNECTION, &header)); + EXPECT_EQ("Keep-Alive", header); + + if (empty_doc) { + EXPECT_FALSE(events[0].chunked); + EXPECT_EQ(0U, events[0].data_size); + + EXPECT_TRUE(data.hasHeader(HH_CONTENT_LENGTH, &header)); + EXPECT_EQ("0", header); + } else { + EXPECT_TRUE(events[0].chunked); + EXPECT_EQ(SIZE_UNKNOWN, events[0].data_size); + + EXPECT_TRUE(data.hasHeader(HH_CONTENT_TYPE, &header)); + EXPECT_EQ("text/plain", header); + EXPECT_TRUE(data.hasHeader(HH_TRANSFER_ENCODING, &header)); + EXPECT_EQ("chunked", header); + } + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifyDocumentContents(const char* expected_data, + size_t expected_length) { + LOG_F(LS_VERBOSE) << "Enter"; + + if (SIZE_UNKNOWN == expected_length) { + expected_length = strlen(expected_data); + } + EXPECT_EQ(mem, data.document.get()); + + size_t length; + mem->GetSize(&length); + EXPECT_EQ(expected_length, length); + EXPECT_TRUE(0 == memcmp(expected_data, mem->GetBuffer(), length)); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::ObtainDocumentStream() { + LOG_F(LS_VERBOSE) << "Enter"; + EXPECT_FALSE(http_stream); + http_stream = base.GetDocumentStream(); + ASSERT_TRUE(NULL != http_stream); + sink.Monitor(http_stream); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifyDocumentStreamIsOpening() { + LOG_F(LS_VERBOSE) << "Enter"; + ASSERT_TRUE(NULL != http_stream); + EXPECT_EQ(0, sink.Events(http_stream)); + EXPECT_EQ(SS_OPENING, http_stream->GetState()); + + size_t read = 0; + char buffer[5] = { 0 }; + EXPECT_EQ(SR_BLOCK, http_stream->Read(buffer, sizeof(buffer), &read, NULL)); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifyDocumentStreamOpenEvent() { + LOG_F(LS_VERBOSE) << "Enter"; + + ASSERT_TRUE(NULL != http_stream); + EXPECT_EQ(SE_OPEN | SE_READ, sink.Events(http_stream)); + EXPECT_EQ(SS_OPEN, http_stream->GetState()); + + // HTTP headers haven't arrived yet + EXPECT_EQ(0U, events.size()); + EXPECT_EQ(static_cast(HC_INTERNAL_SERVER_ERROR), data.scode); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::ReadDocumentStreamData(const char* expected_data) { + LOG_F(LS_VERBOSE) << "Enter"; + + ASSERT_TRUE(NULL != http_stream); + EXPECT_EQ(SS_OPEN, http_stream->GetState()); + + // Pump the HTTP I/O using Read, and verify the results. + size_t verified_length = 0; + const size_t expected_length = strlen(expected_data); + while (verified_length < expected_length) { + size_t read = 0; + char buffer[5] = { 0 }; + size_t amt_to_read = _min(expected_length - verified_length, sizeof(buffer)); + EXPECT_EQ(SR_SUCCESS, http_stream->Read(buffer, amt_to_read, &read, NULL)); + EXPECT_EQ(amt_to_read, read); + EXPECT_TRUE(0 == memcmp(expected_data + verified_length, buffer, read)); + verified_length += read; + } + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifyDocumentStreamIsEOS() { + LOG_F(LS_VERBOSE) << "Enter"; + + ASSERT_TRUE(NULL != http_stream); + size_t read = 0; + char buffer[5] = { 0 }; + EXPECT_EQ(SR_EOS, http_stream->Read(buffer, sizeof(buffer), &read, NULL)); + EXPECT_EQ(SS_CLOSED, http_stream->GetState()); + + // When EOS is caused by Read, we don't expect SE_CLOSE + EXPECT_EQ(0, sink.Events(http_stream)); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::SetupDocument(const char* document_data) { + LOG_F(LS_VERBOSE) << "Enter"; + src.SetState(SS_OPEN); + + base.notify(this); + base.attach(&src); + EXPECT_TRUE(events.empty()); + + if (document_data) { + // Note: we could just call data.set_success("text/plain", mem), but that + // won't allow us to use the chunked transfer encoding. + mem = new MemoryStream(document_data); + data.document.reset(mem); + data.setHeader(HH_CONTENT_TYPE, "text/plain"); + data.setHeader(HH_TRANSFER_ENCODING, "chunked"); + } else { + data.setHeader(HH_CONTENT_LENGTH, "0"); + } + data.scode = HC_OK; + data.setHeader(HH_PROXY_AUTHORIZATION, "42"); + data.setHeader(HH_CONNECTION, "Keep-Alive"); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifySourceContents(const char* expected_data, + size_t expected_length) { + LOG_F(LS_VERBOSE) << "Enter"; + if (SIZE_UNKNOWN == expected_length) { + expected_length = strlen(expected_data); + } + std::string contents = src.ReadData(); + EXPECT_EQ(expected_length, contents.length()); + EXPECT_TRUE(0 == memcmp(expected_data, contents.data(), expected_length)); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifyTransferComplete(HttpMode mode, HttpError error) { + LOG_F(LS_VERBOSE) << "Enter"; + // Verify that http operation has completed + ASSERT_TRUE(events.size() > 0); + size_t last_event = events.size() - 1; + EXPECT_EQ(E_COMPLETE, events[last_event].event); + EXPECT_EQ(mode, events[last_event].mode); + EXPECT_EQ(error, events[last_event].err); + LOG_F(LS_VERBOSE) << "Exit"; +} + +// +// Tests +// + +TEST_F(HttpBaseTest, SupportsSend) { + // Queue response document + SetupDocument("Goodbye!"); + + // Begin send + base.send(&data); + + // Send completed successfully + VerifyTransferComplete(HM_SEND, HE_NONE); + VerifySourceContents(kHttpResponse); +} + +TEST_F(HttpBaseTest, SupportsSendNoDocument) { + // Queue response document + SetupDocument(NULL); + + // Begin send + base.send(&data); + + // Send completed successfully + VerifyTransferComplete(HM_SEND, HE_NONE); + VerifySourceContents(kHttpEmptyResponse); +} + +TEST_F(HttpBaseTest, SignalsCompleteOnInterruptedSend) { + // This test is attempting to expose a bug that occurs when a particular + // base objects is used for receiving, and then used for sending. In + // particular, the HttpParser state is different after receiving. Simulate + // that here. + SetupSource(kHttpResponse); + base.recv(&data); + VerifyTransferComplete(HM_RECV, HE_NONE); + + src.Clear(); + data.clear(true); + events.clear(); + base.detach(); + + // Queue response document + SetupDocument("Goodbye!"); + + // Prevent entire response from being sent + const size_t kInterruptedLength = strlen(kHttpResponse) - 1; + src.SetWriteBlock(kInterruptedLength); + + // Begin send + base.send(&data); + + // Document is mostly complete, but no completion signal yet. + EXPECT_TRUE(events.empty()); + VerifySourceContents(kHttpResponse, kInterruptedLength); + + src.SetState(SS_CLOSED); + + // Send completed with disconnect error, and no additional data. + VerifyTransferComplete(HM_SEND, HE_DISCONNECTED); + EXPECT_TRUE(src.ReadData().empty()); +} + +TEST_F(HttpBaseTest, SupportsReceiveViaDocumentPush) { + // Queue response document + SetupSource(kHttpResponse); + + // Begin receive + base.recv(&data); + + // Document completed successfully + VerifyHeaderComplete(2, false); + VerifyTransferComplete(HM_RECV, HE_NONE); + VerifyDocumentContents("Goodbye!"); +} + +TEST_F(HttpBaseTest, SupportsReceiveViaStreamPull) { + // Switch to pull mode + ObtainDocumentStream(); + VerifyDocumentStreamIsOpening(); + + // Queue response document + SetupSource(kHttpResponse); + VerifyDocumentStreamIsOpening(); + + // Begin receive + base.recv(&data); + + // Pull document data + VerifyDocumentStreamOpenEvent(); + ReadDocumentStreamData("Goodbye!"); + VerifyDocumentStreamIsEOS(); + + // Document completed successfully + VerifyHeaderComplete(2, false); + VerifyTransferComplete(HM_RECV, HE_NONE); + VerifyDocumentContents(""); +} + +TEST_F(HttpBaseTest, DISABLED_AllowsCloseStreamBeforeDocumentIsComplete) { + + // TODO: Remove extra logging once test failure is understood + int old_sev = talk_base::LogMessage::GetLogToDebug(); + talk_base::LogMessage::LogToDebug(LS_VERBOSE); + + + // Switch to pull mode + ObtainDocumentStream(); + VerifyDocumentStreamIsOpening(); + + // Queue response document + SetupSource(kHttpResponse); + VerifyDocumentStreamIsOpening(); + + // Begin receive + base.recv(&data); + + // Pull some of the data + VerifyDocumentStreamOpenEvent(); + ReadDocumentStreamData("Goodb"); + + // We've seen the header by now + VerifyHeaderComplete(1, false); + + // Close the pull stream, this will transition back to push I/O. + http_stream->Close(); + Thread::Current()->ProcessMessages(0); + + // Remainder of document completed successfully + VerifyTransferComplete(HM_RECV, HE_NONE); + VerifyDocumentContents("ye!"); + + talk_base::LogMessage::LogToDebug(old_sev); +} + +TEST_F(HttpBaseTest, AllowsGetDocumentStreamInResponseToHttpHeader) { + // Queue response document + SetupSource(kHttpResponse); + + // Switch to pull mode in response to header arrival + obtain_stream = true; + + // Begin receive + base.recv(&data); + + // We've already seen the header, but not data has arrived + VerifyHeaderComplete(1, false); + VerifyDocumentContents(""); + + // Pull the document data + ReadDocumentStreamData("Goodbye!"); + VerifyDocumentStreamIsEOS(); + + // Document completed successfully + VerifyTransferComplete(HM_RECV, HE_NONE); + VerifyDocumentContents(""); +} + +TEST_F(HttpBaseTest, AllowsGetDocumentStreamWithEmptyDocumentBody) { + // Queue empty response document + SetupSource(kHttpEmptyResponse); + + // Switch to pull mode in response to header arrival + obtain_stream = true; + + // Begin receive + base.recv(&data); + + // We've already seen the header, but not data has arrived + VerifyHeaderComplete(1, true); + VerifyDocumentContents(""); + + // The document is still open, until we attempt to read + ASSERT_TRUE(NULL != http_stream); + EXPECT_EQ(SS_OPEN, http_stream->GetState()); + + // Attempt to read data, and discover EOS + VerifyDocumentStreamIsEOS(); + + // Document completed successfully + VerifyTransferComplete(HM_RECV, HE_NONE); + VerifyDocumentContents(""); +} + +TEST_F(HttpBaseTest, SignalsDocumentStreamCloseOnUnexpectedClose) { + // Switch to pull mode + ObtainDocumentStream(); + VerifyDocumentStreamIsOpening(); + + // Queue response document + SetupSource(kHttpResponsePrefix); + VerifyDocumentStreamIsOpening(); + + // Begin receive + base.recv(&data); + + // Pull document data + VerifyDocumentStreamOpenEvent(); + ReadDocumentStreamData("Goodbye!"); + + // Simulate unexpected close + src.SetState(SS_CLOSED); + + // Observe error event on document stream + EXPECT_EQ(testing::SSE_ERROR, sink.Events(http_stream)); + + // Future reads give an error + int error = 0; + char buffer[5] = { 0 }; + EXPECT_EQ(SR_ERROR, http_stream->Read(buffer, sizeof(buffer), NULL, &error)); + EXPECT_EQ(HE_DISCONNECTED, error); + + // Document completed with error + VerifyHeaderComplete(2, false); + VerifyTransferComplete(HM_RECV, HE_DISCONNECTED); + VerifyDocumentContents(""); +} + +} // namespace talk_base diff --git a/talk/base/httpclient.cc b/talk/base/httpclient.cc new file mode 100644 index 000000000..5a16676a3 --- /dev/null +++ b/talk/base/httpclient.cc @@ -0,0 +1,847 @@ +/* + * libjingle + * Copyright 2004--2005, 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 + +#include "talk/base/httpcommon-inl.h" + +#include "talk/base/asyncsocket.h" +#include "talk/base/common.h" +#include "talk/base/diskcache.h" +#include "talk/base/httpclient.h" +#include "talk/base/logging.h" +#include "talk/base/pathutils.h" +#include "talk/base/socketstream.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" + +namespace talk_base { + +////////////////////////////////////////////////////////////////////// +// Helpers +////////////////////////////////////////////////////////////////////// + +namespace { + +const size_t kCacheHeader = 0; +const size_t kCacheBody = 1; + +// Convert decimal string to integer +bool HttpStringToUInt(const std::string& str, size_t* val) { + ASSERT(NULL != val); + char* eos = NULL; + *val = strtoul(str.c_str(), &eos, 10); + return (*eos == '\0'); +} + +bool HttpShouldCache(const HttpTransaction& t) { + bool verb_allows_cache = (t.request.verb == HV_GET) + || (t.request.verb == HV_HEAD); + bool is_range_response = t.response.hasHeader(HH_CONTENT_RANGE, NULL); + bool has_expires = t.response.hasHeader(HH_EXPIRES, NULL); + bool request_allows_cache = + has_expires || (std::string::npos != t.request.path.find('?')); + bool response_allows_cache = + has_expires || HttpCodeIsCacheable(t.response.scode); + + bool may_cache = verb_allows_cache + && request_allows_cache + && response_allows_cache + && !is_range_response; + + std::string value; + if (t.response.hasHeader(HH_CACHE_CONTROL, &value)) { + HttpAttributeList directives; + HttpParseAttributes(value.data(), value.size(), directives); + // Response Directives Summary: + // public - always cacheable + // private - do not cache in a shared cache + // no-cache - may cache, but must revalidate whether fresh or stale + // no-store - sensitive information, do not cache or store in any way + // max-age - supplants Expires for staleness + // s-maxage - use as max-age for shared caches, ignore otherwise + // must-revalidate - may cache, but must revalidate after stale + // proxy-revalidate - shared cache must revalidate + if (HttpHasAttribute(directives, "no-store", NULL)) { + may_cache = false; + } else if (HttpHasAttribute(directives, "public", NULL)) { + may_cache = true; + } + } + return may_cache; +} + +enum HttpCacheState { + HCS_FRESH, // In cache, may use + HCS_STALE, // In cache, must revalidate + HCS_NONE // Not in cache +}; + +HttpCacheState HttpGetCacheState(const HttpTransaction& t) { + // Temporaries + std::string s_temp; + time_t u_temp; + + // Current time + size_t now = time(0); + + HttpAttributeList cache_control; + if (t.response.hasHeader(HH_CACHE_CONTROL, &s_temp)) { + HttpParseAttributes(s_temp.data(), s_temp.size(), cache_control); + } + + // Compute age of cache document + time_t date; + if (!t.response.hasHeader(HH_DATE, &s_temp) + || !HttpDateToSeconds(s_temp, &date)) + return HCS_NONE; + + // TODO: Timestamp when cache request sent and response received? + time_t request_time = date; + time_t response_time = date; + + time_t apparent_age = 0; + if (response_time > date) { + apparent_age = response_time - date; + } + + size_t corrected_received_age = apparent_age; + size_t i_temp; + if (t.response.hasHeader(HH_AGE, &s_temp) + && HttpStringToUInt(s_temp, (&i_temp))) { + u_temp = static_cast(i_temp); + corrected_received_age = stdmax(apparent_age, u_temp); + } + + size_t response_delay = response_time - request_time; + size_t corrected_initial_age = corrected_received_age + response_delay; + size_t resident_time = now - response_time; + size_t current_age = corrected_initial_age + resident_time; + + // Compute lifetime of document + size_t lifetime; + if (HttpHasAttribute(cache_control, "max-age", &s_temp)) { + lifetime = atoi(s_temp.c_str()); + } else if (t.response.hasHeader(HH_EXPIRES, &s_temp) + && HttpDateToSeconds(s_temp, &u_temp)) { + lifetime = u_temp - date; + } else if (t.response.hasHeader(HH_LAST_MODIFIED, &s_temp) + && HttpDateToSeconds(s_temp, &u_temp)) { + // TODO: Issue warning 113 if age > 24 hours + lifetime = static_cast(now - u_temp) / 10; + } else { + return HCS_STALE; + } + + return (lifetime > current_age) ? HCS_FRESH : HCS_STALE; +} + +enum HttpValidatorStrength { + HVS_NONE, + HVS_WEAK, + HVS_STRONG +}; + +HttpValidatorStrength +HttpRequestValidatorLevel(const HttpRequestData& request) { + if (HV_GET != request.verb) + return HVS_STRONG; + return request.hasHeader(HH_RANGE, NULL) ? HVS_STRONG : HVS_WEAK; +} + +HttpValidatorStrength +HttpResponseValidatorLevel(const HttpResponseData& response) { + std::string value; + if (response.hasHeader(HH_ETAG, &value)) { + bool is_weak = (strnicmp(value.c_str(), "W/", 2) == 0); + return is_weak ? HVS_WEAK : HVS_STRONG; + } + if (response.hasHeader(HH_LAST_MODIFIED, &value)) { + time_t last_modified, date; + if (HttpDateToSeconds(value, &last_modified) + && response.hasHeader(HH_DATE, &value) + && HttpDateToSeconds(value, &date) + && (last_modified + 60 < date)) { + return HVS_STRONG; + } + return HVS_WEAK; + } + return HVS_NONE; +} + +std::string GetCacheID(const HttpRequestData& request) { + std::string id, url; + id.append(ToString(request.verb)); + id.append("_"); + request.getAbsoluteUri(&url); + id.append(url); + return id; +} + +} // anonymous namespace + +////////////////////////////////////////////////////////////////////// +// Public Helpers +////////////////////////////////////////////////////////////////////// + +bool HttpWriteCacheHeaders(const HttpResponseData* response, + StreamInterface* output, size_t* size) { + size_t length = 0; + // Write all unknown and end-to-end headers to a cache file + for (HttpData::const_iterator it = response->begin(); + it != response->end(); ++it) { + HttpHeader header; + if (FromString(header, it->first) && !HttpHeaderIsEndToEnd(header)) + continue; + length += it->first.length() + 2 + it->second.length() + 2; + if (!output) + continue; + std::string formatted_header(it->first); + formatted_header.append(": "); + formatted_header.append(it->second); + formatted_header.append("\r\n"); + StreamResult result = output->WriteAll(formatted_header.data(), + formatted_header.length(), + NULL, NULL); + if (SR_SUCCESS != result) { + return false; + } + } + if (output && (SR_SUCCESS != output->WriteAll("\r\n", 2, NULL, NULL))) { + return false; + } + length += 2; + if (size) + *size = length; + return true; +} + +bool HttpReadCacheHeaders(StreamInterface* input, HttpResponseData* response, + HttpData::HeaderCombine combine) { + while (true) { + std::string formatted_header; + StreamResult result = input->ReadLine(&formatted_header); + if ((SR_EOS == result) || (1 == formatted_header.size())) { + break; + } + if (SR_SUCCESS != result) { + return false; + } + size_t end_of_name = formatted_header.find(':'); + if (std::string::npos == end_of_name) { + LOG_F(LS_WARNING) << "Malformed cache header"; + continue; + } + size_t start_of_value = end_of_name + 1; + size_t end_of_value = formatted_header.length(); + while ((start_of_value < end_of_value) + && isspace(formatted_header[start_of_value])) + ++start_of_value; + while ((start_of_value < end_of_value) + && isspace(formatted_header[end_of_value-1])) + --end_of_value; + size_t value_length = end_of_value - start_of_value; + + std::string name(formatted_header.substr(0, end_of_name)); + std::string value(formatted_header.substr(start_of_value, value_length)); + response->changeHeader(name, value, combine); + } + return true; +} + +////////////////////////////////////////////////////////////////////// +// HttpClient +////////////////////////////////////////////////////////////////////// + +const size_t kDefaultRetries = 1; +const size_t kMaxRedirects = 5; + +HttpClient::HttpClient(const std::string& agent, StreamPool* pool, + HttpTransaction* transaction) + : agent_(agent), pool_(pool), + transaction_(transaction), free_transaction_(false), + retries_(kDefaultRetries), attempt_(0), redirects_(0), + redirect_action_(REDIRECT_DEFAULT), + uri_form_(URI_DEFAULT), cache_(NULL), cache_state_(CS_READY), + resolver_(NULL) { + base_.notify(this); + if (NULL == transaction_) { + free_transaction_ = true; + transaction_ = new HttpTransaction; + } +} + +HttpClient::~HttpClient() { + base_.notify(NULL); + base_.abort(HE_SHUTDOWN); + if (resolver_) { + resolver_->Destroy(false); + } + release(); + if (free_transaction_) + delete transaction_; +} + +void HttpClient::reset() { + server_.Clear(); + request().clear(true); + response().clear(true); + context_.reset(); + redirects_ = 0; + base_.abort(HE_OPERATION_CANCELLED); +} + +void HttpClient::OnResolveResult(SignalThread* thread) { + if (thread != resolver_) { + return; + } + int error = resolver_->error(); + server_ = resolver_->address(); + resolver_->Destroy(false); + resolver_ = NULL; + if (error != 0) { + LOG(LS_ERROR) << "Error " << error << " resolving name: " + << server_; + onHttpComplete(HM_CONNECT, HE_CONNECT_FAILED); + } else { + connect(); + } +} + +void HttpClient::StartDNSLookup() { + resolver_ = new AsyncResolver(); + resolver_->set_address(server_); + resolver_->SignalWorkDone.connect(this, &HttpClient::OnResolveResult); + resolver_->Start(); +} + +void HttpClient::set_server(const SocketAddress& address) { + server_ = address; + // Setting 'Host' here allows it to be overridden before starting the request, + // if necessary. + request().setHeader(HH_HOST, HttpAddress(server_, false), true); +} + +StreamInterface* HttpClient::GetDocumentStream() { + return base_.GetDocumentStream(); +} + +void HttpClient::start() { + if (base_.mode() != HM_NONE) { + // call reset() to abort an in-progress request + ASSERT(false); + return; + } + + ASSERT(!IsCacheActive()); + + if (request().hasHeader(HH_TRANSFER_ENCODING, NULL)) { + // Exact size must be known on the client. Instead of using chunked + // encoding, wrap data with auto-caching file or memory stream. + ASSERT(false); + return; + } + + attempt_ = 0; + + // If no content has been specified, using length of 0. + request().setHeader(HH_CONTENT_LENGTH, "0", false); + + if (!agent_.empty()) { + request().setHeader(HH_USER_AGENT, agent_, false); + } + + UriForm uri_form = uri_form_; + if (PROXY_HTTPS == proxy_.type) { + // Proxies require absolute form + uri_form = URI_ABSOLUTE; + request().version = HVER_1_0; + request().setHeader(HH_PROXY_CONNECTION, "Keep-Alive", false); + } else { + request().setHeader(HH_CONNECTION, "Keep-Alive", false); + } + + if (URI_ABSOLUTE == uri_form) { + // Convert to absolute uri form + std::string url; + if (request().getAbsoluteUri(&url)) { + request().path = url; + } else { + LOG(LS_WARNING) << "Couldn't obtain absolute uri"; + } + } else if (URI_RELATIVE == uri_form) { + // Convert to relative uri form + std::string host, path; + if (request().getRelativeUri(&host, &path)) { + request().setHeader(HH_HOST, host); + request().path = path; + } else { + LOG(LS_WARNING) << "Couldn't obtain relative uri"; + } + } + + if ((NULL != cache_) && CheckCache()) { + return; + } + + connect(); +} + +void HttpClient::connect() { + int stream_err; + if (server_.IsUnresolvedIP()) { + StartDNSLookup(); + return; + } + StreamInterface* stream = pool_->RequestConnectedStream(server_, &stream_err); + if (stream == NULL) { + ASSERT(0 != stream_err); + LOG(LS_ERROR) << "RequestConnectedStream error: " << stream_err; + onHttpComplete(HM_CONNECT, HE_CONNECT_FAILED); + } else { + base_.attach(stream); + if (stream->GetState() == SS_OPEN) { + base_.send(&transaction_->request); + } + } +} + +void HttpClient::prepare_get(const std::string& url) { + reset(); + Url purl(url); + set_server(SocketAddress(purl.host(), purl.port())); + request().verb = HV_GET; + request().path = purl.full_path(); +} + +void HttpClient::prepare_post(const std::string& url, + const std::string& content_type, + StreamInterface* request_doc) { + reset(); + Url purl(url); + set_server(SocketAddress(purl.host(), purl.port())); + request().verb = HV_POST; + request().path = purl.full_path(); + request().setContent(content_type, request_doc); +} + +void HttpClient::release() { + if (StreamInterface* stream = base_.detach()) { + pool_->ReturnConnectedStream(stream); + } +} + +bool HttpClient::ShouldRedirect(std::string* location) const { + // TODO: Unittest redirection. + if ((REDIRECT_NEVER == redirect_action_) + || !HttpCodeIsRedirection(response().scode) + || !response().hasHeader(HH_LOCATION, location) + || (redirects_ >= kMaxRedirects)) + return false; + return (REDIRECT_ALWAYS == redirect_action_) + || (HC_SEE_OTHER == response().scode) + || (HV_HEAD == request().verb) + || (HV_GET == request().verb); +} + +bool HttpClient::BeginCacheFile() { + ASSERT(NULL != cache_); + ASSERT(CS_READY == cache_state_); + + std::string id = GetCacheID(request()); + CacheLock lock(cache_, id, true); + if (!lock.IsLocked()) { + LOG_F(LS_WARNING) << "Couldn't lock cache"; + return false; + } + + if (HE_NONE != WriteCacheHeaders(id)) { + return false; + } + + scoped_ptr stream(cache_->WriteResource(id, kCacheBody)); + if (!stream) { + LOG_F(LS_ERROR) << "Couldn't open body cache"; + return false; + } + lock.Commit(); + + // Let's secretly replace the response document with Folgers Crystals, + // er, StreamTap, so that we can mirror the data to our cache. + StreamInterface* output = response().document.release(); + if (!output) { + output = new NullStream; + } + StreamTap* tap = new StreamTap(output, stream.release()); + response().document.reset(tap); + return true; +} + +HttpError HttpClient::WriteCacheHeaders(const std::string& id) { + scoped_ptr stream(cache_->WriteResource(id, kCacheHeader)); + if (!stream) { + LOG_F(LS_ERROR) << "Couldn't open header cache"; + return HE_CACHE; + } + + if (!HttpWriteCacheHeaders(&transaction_->response, stream.get(), NULL)) { + LOG_F(LS_ERROR) << "Couldn't write header cache"; + return HE_CACHE; + } + + return HE_NONE; +} + +void HttpClient::CompleteCacheFile() { + // Restore previous response document + StreamTap* tap = static_cast(response().document.release()); + response().document.reset(tap->Detach()); + + int error; + StreamResult result = tap->GetTapResult(&error); + + // Delete the tap and cache stream (which completes cache unlock) + delete tap; + + if (SR_SUCCESS != result) { + LOG(LS_ERROR) << "Cache file error: " << error; + cache_->DeleteResource(GetCacheID(request())); + } +} + +bool HttpClient::CheckCache() { + ASSERT(NULL != cache_); + ASSERT(CS_READY == cache_state_); + + std::string id = GetCacheID(request()); + if (!cache_->HasResource(id)) { + // No cache file available + return false; + } + + HttpError error = ReadCacheHeaders(id, true); + + if (HE_NONE == error) { + switch (HttpGetCacheState(*transaction_)) { + case HCS_FRESH: + // Cache content is good, read from cache + break; + case HCS_STALE: + // Cache content may be acceptable. Issue a validation request. + if (PrepareValidate()) { + return false; + } + // Couldn't validate, fall through. + case HCS_NONE: + // Cache content is not useable. Issue a regular request. + response().clear(false); + return false; + } + } + + if (HE_NONE == error) { + error = ReadCacheBody(id); + cache_state_ = CS_READY; + } + + if (HE_CACHE == error) { + LOG_F(LS_WARNING) << "Cache failure, continuing with normal request"; + response().clear(false); + return false; + } + + SignalHttpClientComplete(this, error); + return true; +} + +HttpError HttpClient::ReadCacheHeaders(const std::string& id, bool override) { + scoped_ptr stream(cache_->ReadResource(id, kCacheHeader)); + if (!stream) { + return HE_CACHE; + } + + HttpData::HeaderCombine combine = + override ? HttpData::HC_REPLACE : HttpData::HC_AUTO; + + if (!HttpReadCacheHeaders(stream.get(), &transaction_->response, combine)) { + LOG_F(LS_ERROR) << "Error reading cache headers"; + return HE_CACHE; + } + + response().scode = HC_OK; + return HE_NONE; +} + +HttpError HttpClient::ReadCacheBody(const std::string& id) { + cache_state_ = CS_READING; + + HttpError error = HE_NONE; + + size_t data_size; + scoped_ptr stream(cache_->ReadResource(id, kCacheBody)); + if (!stream || !stream->GetAvailable(&data_size)) { + LOG_F(LS_ERROR) << "Unavailable cache body"; + error = HE_CACHE; + } else { + error = OnHeaderAvailable(false, false, data_size); + } + + if ((HE_NONE == error) + && (HV_HEAD != request().verb) + && response().document) { + char buffer[1024 * 64]; + StreamResult result = Flow(stream.get(), buffer, ARRAY_SIZE(buffer), + response().document.get()); + if (SR_SUCCESS != result) { + error = HE_STREAM; + } + } + + return error; +} + +bool HttpClient::PrepareValidate() { + ASSERT(CS_READY == cache_state_); + // At this point, request() contains the pending request, and response() + // contains the cached response headers. Reformat the request to validate + // the cached content. + HttpValidatorStrength vs_required = HttpRequestValidatorLevel(request()); + HttpValidatorStrength vs_available = HttpResponseValidatorLevel(response()); + if (vs_available < vs_required) { + return false; + } + std::string value; + if (response().hasHeader(HH_ETAG, &value)) { + request().addHeader(HH_IF_NONE_MATCH, value); + } + if (response().hasHeader(HH_LAST_MODIFIED, &value)) { + request().addHeader(HH_IF_MODIFIED_SINCE, value); + } + response().clear(false); + cache_state_ = CS_VALIDATING; + return true; +} + +HttpError HttpClient::CompleteValidate() { + ASSERT(CS_VALIDATING == cache_state_); + + std::string id = GetCacheID(request()); + + // Merge cached headers with new headers + HttpError error = ReadCacheHeaders(id, false); + if (HE_NONE != error) { + // Rewrite merged headers to cache + CacheLock lock(cache_, id); + error = WriteCacheHeaders(id); + } + if (HE_NONE != error) { + error = ReadCacheBody(id); + } + return error; +} + +HttpError HttpClient::OnHeaderAvailable(bool ignore_data, bool chunked, + size_t data_size) { + // If we are ignoring the data, this is an intermediate header. + // TODO: don't signal intermediate headers. Instead, do all header-dependent + // processing now, and either set up the next request, or fail outright. + // TODO: by default, only write response documents with a success code. + SignalHeaderAvailable(this, !ignore_data, ignore_data ? 0 : data_size); + if (!ignore_data && !chunked && (data_size != SIZE_UNKNOWN) + && response().document) { + // Attempt to pre-allocate space for the downloaded data. + if (!response().document->ReserveSize(data_size)) { + return HE_OVERFLOW; + } + } + return HE_NONE; +} + +// +// HttpBase Implementation +// + +HttpError HttpClient::onHttpHeaderComplete(bool chunked, size_t& data_size) { + if (CS_VALIDATING == cache_state_) { + if (HC_NOT_MODIFIED == response().scode) { + return CompleteValidate(); + } + // Should we remove conditional headers from request? + cache_state_ = CS_READY; + cache_->DeleteResource(GetCacheID(request())); + // Continue processing response as normal + } + + ASSERT(!IsCacheActive()); + if ((request().verb == HV_HEAD) || !HttpCodeHasBody(response().scode)) { + // HEAD requests and certain response codes contain no body + data_size = 0; + } + if (ShouldRedirect(NULL) + || ((HC_PROXY_AUTHENTICATION_REQUIRED == response().scode) + && (PROXY_HTTPS == proxy_.type))) { + // We're going to issue another request, so ignore the incoming data. + base_.set_ignore_data(true); + } + + HttpError error = OnHeaderAvailable(base_.ignore_data(), chunked, data_size); + if (HE_NONE != error) { + return error; + } + + if ((NULL != cache_) + && !base_.ignore_data() + && HttpShouldCache(*transaction_)) { + if (BeginCacheFile()) { + cache_state_ = CS_WRITING; + } + } + return HE_NONE; +} + +void HttpClient::onHttpComplete(HttpMode mode, HttpError err) { + if (((HE_DISCONNECTED == err) || (HE_CONNECT_FAILED == err) + || (HE_SOCKET_ERROR == err)) + && (HC_INTERNAL_SERVER_ERROR == response().scode) + && (attempt_ < retries_)) { + // If the response code has not changed from the default, then we haven't + // received anything meaningful from the server, so we are eligible for a + // retry. + ++attempt_; + if (request().document && !request().document->Rewind()) { + // Unable to replay the request document. + err = HE_STREAM; + } else { + release(); + connect(); + return; + } + } else if (err != HE_NONE) { + // fall through + } else if (mode == HM_CONNECT) { + base_.send(&transaction_->request); + return; + } else if ((mode == HM_SEND) || HttpCodeIsInformational(response().scode)) { + // If you're interested in informational headers, catch + // SignalHeaderAvailable. + base_.recv(&transaction_->response); + return; + } else { + if (!HttpShouldKeepAlive(response())) { + LOG(LS_VERBOSE) << "HttpClient: closing socket"; + base_.stream()->Close(); + } + std::string location; + if (ShouldRedirect(&location)) { + Url purl(location); + set_server(SocketAddress(purl.host(), purl.port())); + request().path = purl.full_path(); + if (response().scode == HC_SEE_OTHER) { + request().verb = HV_GET; + request().clearHeader(HH_CONTENT_TYPE); + request().clearHeader(HH_CONTENT_LENGTH); + request().document.reset(); + } else if (request().document && !request().document->Rewind()) { + // Unable to replay the request document. + ASSERT(REDIRECT_ALWAYS == redirect_action_); + err = HE_STREAM; + } + if (err == HE_NONE) { + ++redirects_; + context_.reset(); + response().clear(false); + release(); + start(); + return; + } + } else if ((HC_PROXY_AUTHENTICATION_REQUIRED == response().scode) + && (PROXY_HTTPS == proxy_.type)) { + std::string authorization, auth_method; + HttpData::const_iterator begin = response().begin(HH_PROXY_AUTHENTICATE); + HttpData::const_iterator end = response().end(HH_PROXY_AUTHENTICATE); + for (HttpData::const_iterator it = begin; it != end; ++it) { + HttpAuthContext *context = context_.get(); + HttpAuthResult res = HttpAuthenticate( + it->second.data(), it->second.size(), + proxy_.address, + ToString(request().verb), request().path, + proxy_.username, proxy_.password, + context, authorization, auth_method); + context_.reset(context); + if (res == HAR_RESPONSE) { + request().setHeader(HH_PROXY_AUTHORIZATION, authorization); + if (request().document && !request().document->Rewind()) { + err = HE_STREAM; + } else { + // Explicitly do not reset the HttpAuthContext + response().clear(false); + // TODO: Reuse socket when authenticating? + release(); + start(); + return; + } + } else if (res == HAR_IGNORE) { + LOG(INFO) << "Ignoring Proxy-Authenticate: " << auth_method; + continue; + } else { + break; + } + } + } + } + if (CS_WRITING == cache_state_) { + CompleteCacheFile(); + cache_state_ = CS_READY; + } else if (CS_READING == cache_state_) { + cache_state_ = CS_READY; + } + release(); + SignalHttpClientComplete(this, err); +} + +void HttpClient::onHttpClosed(HttpError err) { + // This shouldn't occur, since we return the stream to the pool upon command + // completion. + ASSERT(false); +} + +////////////////////////////////////////////////////////////////////// +// HttpClientDefault +////////////////////////////////////////////////////////////////////// + +HttpClientDefault::HttpClientDefault(SocketFactory* factory, + const std::string& agent, + HttpTransaction* transaction) + : ReuseSocketPool(factory ? factory : Thread::Current()->socketserver()), + HttpClient(agent, NULL, transaction) { + set_pool(this); +} + +////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/httpclient.h b/talk/base/httpclient.h new file mode 100644 index 000000000..2e77b0d18 --- /dev/null +++ b/talk/base/httpclient.h @@ -0,0 +1,219 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_HTTPCLIENT_H__ +#define TALK_BASE_HTTPCLIENT_H__ + +#include "talk/base/common.h" +#include "talk/base/httpbase.h" +#include "talk/base/nethelpers.h" +#include "talk/base/proxyinfo.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" +#include "talk/base/socketaddress.h" +#include "talk/base/socketpool.h" + +namespace talk_base { + +////////////////////////////////////////////////////////////////////// +// Client-specific http utilities +////////////////////////////////////////////////////////////////////// + +// Write cache-relevant response headers to output stream. If size is non-null, +// it contains the length of the output in bytes. output may be null if only +// the length is desired. +bool HttpWriteCacheHeaders(const HttpResponseData* response, + StreamInterface* output, size_t* size); +// Read cached headers from a stream, and them merge them into the response +// object using the specified combine operation. +bool HttpReadCacheHeaders(StreamInterface* input, + HttpResponseData* response, + HttpData::HeaderCombine combine); + +////////////////////////////////////////////////////////////////////// +// HttpClient +// Implements an HTTP 1.1 client. +////////////////////////////////////////////////////////////////////// + +class DiskCache; +class HttpClient; +class IPNetPool; + +class SignalThread; +// What to do: Define STRICT_HTTP_ERROR=1 in your makefile. Use HttpError in +// your code (HttpErrorType should only be used for code that is shared +// with groups which have not yet migrated). +#if STRICT_HTTP_ERROR +typedef HttpError HttpErrorType; +#else // !STRICT_HTTP_ERROR +typedef int HttpErrorType; +#endif // !STRICT_HTTP_ERROR + +class HttpClient : private IHttpNotify, public sigslot::has_slots<> { +public: + // If HttpRequestData and HttpResponseData objects are provided, they must + // be freed by the caller. Otherwise, an internal object is allocated. + HttpClient(const std::string& agent, StreamPool* pool, + HttpTransaction* transaction = NULL); + virtual ~HttpClient(); + + void set_pool(StreamPool* pool) { pool_ = pool; } + + void set_agent(const std::string& agent) { agent_ = agent; } + const std::string& agent() const { return agent_; } + + void set_proxy(const ProxyInfo& proxy) { proxy_ = proxy; } + const ProxyInfo& proxy() const { return proxy_; } + + // Request retries occur when the connection closes before the beginning of + // an http response is received. In these cases, the http server may have + // timed out the keepalive connection before it received our request. Note + // that if a request document cannot be rewound, no retry is made. The + // default is 1. + void set_request_retries(size_t retries) { retries_ = retries; } + size_t request_retries() const { return retries_; } + + enum RedirectAction { REDIRECT_DEFAULT, REDIRECT_ALWAYS, REDIRECT_NEVER }; + void set_redirect_action(RedirectAction action) { redirect_action_ = action; } + RedirectAction redirect_action() const { return redirect_action_; } + // Deprecated + void set_fail_redirect(bool fail_redirect) { + redirect_action_ = REDIRECT_NEVER; + } + bool fail_redirect() const { return (REDIRECT_NEVER == redirect_action_); } + + enum UriForm { URI_DEFAULT, URI_ABSOLUTE, URI_RELATIVE }; + void set_uri_form(UriForm form) { uri_form_ = form; } + UriForm uri_form() const { return uri_form_; } + + void set_cache(DiskCache* cache) { ASSERT(!IsCacheActive()); cache_ = cache; } + bool cache_enabled() const { return (NULL != cache_); } + + // reset clears the server, request, and response structures. It will also + // abort an active request. + void reset(); + + void set_server(const SocketAddress& address); + const SocketAddress& server() const { return server_; } + + // Note: in order for HttpClient to retry a POST in response to + // an authentication challenge, a redirect response, or socket disconnection, + // the request document must support 'replaying' by calling Rewind() on it. + // In the case where just a subset of a stream should be used as the request + // document, the stream may be wrapped with the StreamSegment adapter. + HttpTransaction* transaction() { return transaction_; } + const HttpTransaction* transaction() const { return transaction_; } + HttpRequestData& request() { return transaction_->request; } + const HttpRequestData& request() const { return transaction_->request; } + HttpResponseData& response() { return transaction_->response; } + const HttpResponseData& response() const { return transaction_->response; } + + // convenience methods + void prepare_get(const std::string& url); + void prepare_post(const std::string& url, const std::string& content_type, + StreamInterface* request_doc); + + // Convert HttpClient to a pull-based I/O model. + StreamInterface* GetDocumentStream(); + + // After you finish setting up your request, call start. + void start(); + + // Signalled when the header has finished downloading, before the document + // content is processed. You may change the response document in response + // to this signal. The second parameter indicates whether this is an + // intermediate (false) or final (true) header. An intermediate header is + // one that generates another request, such as a redirect or authentication + // challenge. The third parameter indicates the length of the response + // document, or else SIZE_UNKNOWN. Note: Do NOT abort the request in response + // to this signal. + sigslot::signal3 SignalHeaderAvailable; + // Signalled when the current request finishes. On success, err is 0. + sigslot::signal2 SignalHttpClientComplete; + +protected: + void connect(); + void release(); + + bool ShouldRedirect(std::string* location) const; + + bool BeginCacheFile(); + HttpError WriteCacheHeaders(const std::string& id); + void CompleteCacheFile(); + + bool CheckCache(); + HttpError ReadCacheHeaders(const std::string& id, bool override); + HttpError ReadCacheBody(const std::string& id); + + bool PrepareValidate(); + HttpError CompleteValidate(); + + HttpError OnHeaderAvailable(bool ignore_data, bool chunked, size_t data_size); + + void StartDNSLookup(); + void OnResolveResult(SignalThread* thread); + + // IHttpNotify Interface + virtual HttpError onHttpHeaderComplete(bool chunked, size_t& data_size); + virtual void onHttpComplete(HttpMode mode, HttpError err); + virtual void onHttpClosed(HttpError err); + +private: + enum CacheState { CS_READY, CS_WRITING, CS_READING, CS_VALIDATING }; + bool IsCacheActive() const { return (cache_state_ > CS_READY); } + + std::string agent_; + StreamPool* pool_; + HttpBase base_; + SocketAddress server_; + ProxyInfo proxy_; + HttpTransaction* transaction_; + bool free_transaction_; + size_t retries_, attempt_, redirects_; + RedirectAction redirect_action_; + UriForm uri_form_; + scoped_ptr context_; + DiskCache* cache_; + CacheState cache_state_; + AsyncResolver* resolver_; +}; + +////////////////////////////////////////////////////////////////////// +// HttpClientDefault - Default implementation of HttpClient +////////////////////////////////////////////////////////////////////// + +class HttpClientDefault : public ReuseSocketPool, public HttpClient { +public: + HttpClientDefault(SocketFactory* factory, const std::string& agent, + HttpTransaction* transaction = NULL); +}; + +////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_HTTPCLIENT_H__ diff --git a/talk/base/httpcommon-inl.h b/talk/base/httpcommon-inl.h new file mode 100644 index 000000000..c9eaffccb --- /dev/null +++ b/talk/base/httpcommon-inl.h @@ -0,0 +1,148 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_HTTPCOMMON_INL_H__ +#define TALK_BASE_HTTPCOMMON_INL_H__ + +#include "talk/base/common.h" +#include "talk/base/httpcommon.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// Url +/////////////////////////////////////////////////////////////////////////////// + +template +void Url::do_set_url(const CTYPE* val, size_t len) { + if (ascnicmp(val, "http://", 7) == 0) { + val += 7; len -= 7; + secure_ = false; + } else if (ascnicmp(val, "https://", 8) == 0) { + val += 8; len -= 8; + secure_ = true; + } else { + clear(); + return; + } + const CTYPE* path = strchrn(val, len, static_cast('/')); + if (!path) { + path = val + len; + } + size_t address_length = (path - val); + do_set_address(val, address_length); + do_set_full_path(path, len - address_length); +} + +template +void Url::do_set_address(const CTYPE* val, size_t len) { + if (const CTYPE* at = strchrn(val, len, static_cast('@'))) { + // Everything before the @ is a user:password combo, so skip it. + len -= at - val + 1; + val = at + 1; + } + if (const CTYPE* colon = strchrn(val, len, static_cast(':'))) { + host_.assign(val, colon - val); + // Note: In every case, we're guaranteed that colon is followed by a null, + // or non-numeric character. + port_ = static_cast(::strtoul(colon + 1, NULL, 10)); + // TODO: Consider checking for invalid data following port number. + } else { + host_.assign(val, len); + port_ = HttpDefaultPort(secure_); + } +} + +template +void Url::do_set_full_path(const CTYPE* val, size_t len) { + const CTYPE* query = strchrn(val, len, static_cast('?')); + if (!query) { + query = val + len; + } + size_t path_length = (query - val); + if (0 == path_length) { + // TODO: consider failing in this case. + path_.assign(1, static_cast('/')); + } else { + ASSERT(val[0] == static_cast('/')); + path_.assign(val, path_length); + } + query_.assign(query, len - path_length); +} + +template +void Url::do_get_url(string* val) const { + CTYPE protocol[9]; + asccpyn(protocol, ARRAY_SIZE(protocol), secure_ ? "https://" : "http://"); + val->append(protocol); + do_get_address(val); + do_get_full_path(val); +} + +template +void Url::do_get_address(string* val) const { + val->append(host_); + if (port_ != HttpDefaultPort(secure_)) { + CTYPE format[5], port[32]; + asccpyn(format, ARRAY_SIZE(format), ":%hu"); + sprintfn(port, ARRAY_SIZE(port), format, port_); + val->append(port); + } +} + +template +void Url::do_get_full_path(string* val) const { + val->append(path_); + val->append(query_); +} + +template +bool Url::get_attribute(const string& name, string* value) const { + if (query_.empty()) + return false; + + std::string::size_type pos = query_.find(name, 1); + if (std::string::npos == pos) + return false; + + pos += name.length() + 1; + if ((pos > query_.length()) || (static_cast('=') != query_[pos-1])) + return false; + + std::string::size_type end = query_.find(static_cast('&'), pos); + if (std::string::npos == end) { + end = query_.length(); + } + value->assign(query_.substr(pos, end - pos)); + return true; +} + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_HTTPCOMMON_INL_H__ diff --git a/talk/base/httpcommon.cc b/talk/base/httpcommon.cc new file mode 100644 index 000000000..458f2f9f8 --- /dev/null +++ b/talk/base/httpcommon.cc @@ -0,0 +1,1055 @@ +/* + * libjingle + * Copyright 2004--2005, 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 + +#ifdef WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#define SECURITY_WIN32 +#include +#endif + +#include "talk/base/httpcommon-inl.h" + +#include "talk/base/base64.h" +#include "talk/base/common.h" +#include "talk/base/cryptstring.h" +#include "talk/base/httpcommon.h" +#include "talk/base/socketaddress.h" +#include "talk/base/stringdigest.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" + +namespace talk_base { + +#ifdef WIN32 +extern const ConstantLabel SECURITY_ERRORS[]; +#endif + +////////////////////////////////////////////////////////////////////// +// Enum - TODO: expose globally later? +////////////////////////////////////////////////////////////////////// + +bool find_string(size_t& index, const std::string& needle, + const char* const haystack[], size_t max_index) { + for (index=0; index +struct Enum { + static const char** Names; + static size_t Size; + + static inline const char* Name(E val) { return Names[val]; } + static inline bool Parse(E& val, const std::string& name) { + size_t index; + if (!find_string(index, name, Names, Size)) + return false; + val = static_cast(index); + return true; + } + + E val; + + inline operator E&() { return val; } + inline Enum& operator=(E rhs) { val = rhs; return *this; } + + inline const char* name() const { return Name(val); } + inline bool assign(const std::string& name) { return Parse(val, name); } + inline Enum& operator=(const std::string& rhs) { assign(rhs); return *this; } +}; + +#define ENUM(e,n) \ + template<> const char** Enum::Names = n; \ + template<> size_t Enum::Size = sizeof(n)/sizeof(n[0]) + +////////////////////////////////////////////////////////////////////// +// HttpCommon +////////////////////////////////////////////////////////////////////// + +static const char* kHttpVersions[HVER_LAST+1] = { + "1.0", "1.1", "Unknown" +}; +ENUM(HttpVersion, kHttpVersions); + +static const char* kHttpVerbs[HV_LAST+1] = { + "GET", "POST", "PUT", "DELETE", "CONNECT", "HEAD" +}; +ENUM(HttpVerb, kHttpVerbs); + +static const char* kHttpHeaders[HH_LAST+1] = { + "Age", + "Cache-Control", + "Connection", + "Content-Disposition", + "Content-Length", + "Content-Range", + "Content-Type", + "Cookie", + "Date", + "ETag", + "Expires", + "Host", + "If-Modified-Since", + "If-None-Match", + "Keep-Alive", + "Last-Modified", + "Location", + "Proxy-Authenticate", + "Proxy-Authorization", + "Proxy-Connection", + "Range", + "Set-Cookie", + "TE", + "Trailers", + "Transfer-Encoding", + "Upgrade", + "User-Agent", + "WWW-Authenticate", +}; +ENUM(HttpHeader, kHttpHeaders); + +const char* ToString(HttpVersion version) { + return Enum::Name(version); +} + +bool FromString(HttpVersion& version, const std::string& str) { + return Enum::Parse(version, str); +} + +const char* ToString(HttpVerb verb) { + return Enum::Name(verb); +} + +bool FromString(HttpVerb& verb, const std::string& str) { + return Enum::Parse(verb, str); +} + +const char* ToString(HttpHeader header) { + return Enum::Name(header); +} + +bool FromString(HttpHeader& header, const std::string& str) { + return Enum::Parse(header, str); +} + +bool HttpCodeHasBody(uint32 code) { + return !HttpCodeIsInformational(code) + && (code != HC_NO_CONTENT) && (code != HC_NOT_MODIFIED); +} + +bool HttpCodeIsCacheable(uint32 code) { + switch (code) { + case HC_OK: + case HC_NON_AUTHORITATIVE: + case HC_PARTIAL_CONTENT: + case HC_MULTIPLE_CHOICES: + case HC_MOVED_PERMANENTLY: + case HC_GONE: + return true; + default: + return false; + } +} + +bool HttpHeaderIsEndToEnd(HttpHeader header) { + switch (header) { + case HH_CONNECTION: + case HH_KEEP_ALIVE: + case HH_PROXY_AUTHENTICATE: + case HH_PROXY_AUTHORIZATION: + case HH_PROXY_CONNECTION: // Note part of RFC... this is non-standard header + case HH_TE: + case HH_TRAILERS: + case HH_TRANSFER_ENCODING: + case HH_UPGRADE: + return false; + default: + return true; + } +} + +bool HttpHeaderIsCollapsible(HttpHeader header) { + switch (header) { + case HH_SET_COOKIE: + case HH_PROXY_AUTHENTICATE: + case HH_WWW_AUTHENTICATE: + return false; + default: + return true; + } +} + +bool HttpShouldKeepAlive(const HttpData& data) { + std::string connection; + if ((data.hasHeader(HH_PROXY_CONNECTION, &connection) + || data.hasHeader(HH_CONNECTION, &connection))) { + return (_stricmp(connection.c_str(), "Keep-Alive") == 0); + } + return (data.version >= HVER_1_1); +} + +namespace { + +inline bool IsEndOfAttributeName(size_t pos, size_t len, const char * data) { + if (pos >= len) + return true; + if (isspace(static_cast(data[pos]))) + return true; + // The reason for this complexity is that some attributes may contain trailing + // equal signs (like base64 tokens in Negotiate auth headers) + if ((pos+1 < len) && (data[pos] == '=') && + !isspace(static_cast(data[pos+1])) && + (data[pos+1] != '=')) { + return true; + } + return false; +} + +// TODO: unittest for EscapeAttribute and HttpComposeAttributes. + +std::string EscapeAttribute(const std::string& attribute) { + const size_t kMaxLength = attribute.length() * 2 + 1; + char* buffer = STACK_ARRAY(char, kMaxLength); + size_t len = escape(buffer, kMaxLength, attribute.data(), attribute.length(), + "\"", '\\'); + return std::string(buffer, len); +} + +} // anonymous namespace + +void HttpComposeAttributes(const HttpAttributeList& attributes, char separator, + std::string* composed) { + std::stringstream ss; + for (size_t i=0; i 0) { + ss << separator << " "; + } + ss << attributes[i].first; + if (!attributes[i].second.empty()) { + ss << "=\"" << EscapeAttribute(attributes[i].second) << "\""; + } + } + *composed = ss.str(); +} + +void HttpParseAttributes(const char * data, size_t len, + HttpAttributeList& attributes) { + size_t pos = 0; + while (true) { + // Skip leading whitespace + while ((pos < len) && isspace(static_cast(data[pos]))) { + ++pos; + } + + // End of attributes? + if (pos >= len) + return; + + // Find end of attribute name + size_t start = pos; + while (!IsEndOfAttributeName(pos, len, data)) { + ++pos; + } + + HttpAttribute attribute; + attribute.first.assign(data + start, data + pos); + + // Attribute has value? + if ((pos < len) && (data[pos] == '=')) { + ++pos; // Skip '=' + // Check if quoted value + if ((pos < len) && (data[pos] == '"')) { + while (++pos < len) { + if (data[pos] == '"') { + ++pos; + break; + } + if ((data[pos] == '\\') && (pos + 1 < len)) + ++pos; + attribute.second.append(1, data[pos]); + } + } else { + while ((pos < len) && + !isspace(static_cast(data[pos])) && + (data[pos] != ',')) { + attribute.second.append(1, data[pos++]); + } + } + } + + attributes.push_back(attribute); + if ((pos < len) && (data[pos] == ',')) ++pos; // Skip ',' + } +} + +bool HttpHasAttribute(const HttpAttributeList& attributes, + const std::string& name, + std::string* value) { + for (HttpAttributeList::const_iterator it = attributes.begin(); + it != attributes.end(); ++it) { + if (it->first == name) { + if (value) { + *value = it->second; + } + return true; + } + } + return false; +} + +bool HttpHasNthAttribute(HttpAttributeList& attributes, + size_t index, + std::string* name, + std::string* value) { + if (index >= attributes.size()) + return false; + + if (name) + *name = attributes[index].first; + if (value) + *value = attributes[index].second; + return true; +} + +bool HttpDateToSeconds(const std::string& date, time_t* seconds) { + const char* const kTimeZones[] = { + "UT", "GMT", "EST", "EDT", "CST", "CDT", "MST", "MDT", "PST", "PDT", + "A", "B", "C", "D", "E", "F", "G", "H", "I", "K", "L", "M", + "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y" + }; + const int kTimeZoneOffsets[] = { + 0, 0, -5, -4, -6, -5, -7, -6, -8, -7, + -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 + }; + + ASSERT(NULL != seconds); + struct tm tval; + memset(&tval, 0, sizeof(tval)); + char month[4], zone[6]; + memset(month, 0, sizeof(month)); + memset(zone, 0, sizeof(zone)); + + if (7 != sscanf(date.c_str(), "%*3s, %d %3s %d %d:%d:%d %5c", + &tval.tm_mday, month, &tval.tm_year, + &tval.tm_hour, &tval.tm_min, &tval.tm_sec, zone)) { + return false; + } + switch (toupper(month[2])) { + case 'N': tval.tm_mon = (month[1] == 'A') ? 0 : 5; break; + case 'B': tval.tm_mon = 1; break; + case 'R': tval.tm_mon = (month[0] == 'M') ? 2 : 3; break; + case 'Y': tval.tm_mon = 4; break; + case 'L': tval.tm_mon = 6; break; + case 'G': tval.tm_mon = 7; break; + case 'P': tval.tm_mon = 8; break; + case 'T': tval.tm_mon = 9; break; + case 'V': tval.tm_mon = 10; break; + case 'C': tval.tm_mon = 11; break; + } + tval.tm_year -= 1900; + size_t gmt, non_gmt = mktime(&tval); + if ((zone[0] == '+') || (zone[0] == '-')) { + if (!isdigit(zone[1]) || !isdigit(zone[2]) + || !isdigit(zone[3]) || !isdigit(zone[4])) { + return false; + } + int hours = (zone[1] - '0') * 10 + (zone[2] - '0'); + int minutes = (zone[3] - '0') * 10 + (zone[4] - '0'); + int offset = (hours * 60 + minutes) * 60; + gmt = non_gmt + ((zone[0] == '+') ? offset : -offset); + } else { + size_t zindex; + if (!find_string(zindex, zone, kTimeZones, ARRAY_SIZE(kTimeZones))) { + return false; + } + gmt = non_gmt + kTimeZoneOffsets[zindex] * 60 * 60; + } + // TODO: Android should support timezone, see b/2441195 +#if defined(OSX) || defined(ANDROID) || defined(BSD) + tm *tm_for_timezone = localtime((time_t *)&gmt); + *seconds = gmt + tm_for_timezone->tm_gmtoff; +#else + *seconds = gmt - timezone; +#endif + return true; +} + +std::string HttpAddress(const SocketAddress& address, bool secure) { + return (address.port() == HttpDefaultPort(secure)) + ? address.hostname() : address.ToString(); +} + +////////////////////////////////////////////////////////////////////// +// HttpData +////////////////////////////////////////////////////////////////////// + +void +HttpData::clear(bool release_document) { + // Clear headers first, since releasing a document may have far-reaching + // effects. + headers_.clear(); + if (release_document) { + document.reset(); + } +} + +void +HttpData::copy(const HttpData& src) { + headers_ = src.headers_; +} + +void +HttpData::changeHeader(const std::string& name, const std::string& value, + HeaderCombine combine) { + if (combine == HC_AUTO) { + HttpHeader header; + // Unrecognized headers are collapsible + combine = !FromString(header, name) || HttpHeaderIsCollapsible(header) + ? HC_YES : HC_NO; + } else if (combine == HC_REPLACE) { + headers_.erase(name); + combine = HC_NO; + } + // At this point, combine is one of (YES, NO, NEW) + if (combine != HC_NO) { + HeaderMap::iterator it = headers_.find(name); + if (it != headers_.end()) { + if (combine == HC_YES) { + it->second.append(","); + it->second.append(value); + } + return; + } + } + headers_.insert(HeaderMap::value_type(name, value)); +} + +size_t HttpData::clearHeader(const std::string& name) { + return headers_.erase(name); +} + +HttpData::iterator HttpData::clearHeader(iterator header) { + iterator deprecated = header++; + headers_.erase(deprecated); + return header; +} + +bool +HttpData::hasHeader(const std::string& name, std::string* value) const { + HeaderMap::const_iterator it = headers_.find(name); + if (it == headers_.end()) { + return false; + } else if (value) { + *value = it->second; + } + return true; +} + +void HttpData::setContent(const std::string& content_type, + StreamInterface* document) { + setHeader(HH_CONTENT_TYPE, content_type); + setDocumentAndLength(document); +} + +void HttpData::setDocumentAndLength(StreamInterface* document) { + // TODO: Consider calling Rewind() here? + ASSERT(!hasHeader(HH_CONTENT_LENGTH, NULL)); + ASSERT(!hasHeader(HH_TRANSFER_ENCODING, NULL)); + ASSERT(document != NULL); + this->document.reset(document); + size_t content_length = 0; + if (this->document->GetAvailable(&content_length)) { + char buffer[32]; + sprintfn(buffer, sizeof(buffer), "%d", content_length); + setHeader(HH_CONTENT_LENGTH, buffer); + } else { + setHeader(HH_TRANSFER_ENCODING, "chunked"); + } +} + +// +// HttpRequestData +// + +void +HttpRequestData::clear(bool release_document) { + verb = HV_GET; + path.clear(); + HttpData::clear(release_document); +} + +void +HttpRequestData::copy(const HttpRequestData& src) { + verb = src.verb; + path = src.path; + HttpData::copy(src); +} + +size_t +HttpRequestData::formatLeader(char* buffer, size_t size) const { + ASSERT(path.find(' ') == std::string::npos); + return sprintfn(buffer, size, "%s %.*s HTTP/%s", ToString(verb), path.size(), + path.data(), ToString(version)); +} + +HttpError +HttpRequestData::parseLeader(const char* line, size_t len) { + UNUSED(len); + unsigned int vmajor, vminor; + int vend, dstart, dend; + if ((sscanf(line, "%*s%n %n%*s%n HTTP/%u.%u", &vend, &dstart, &dend, + &vmajor, &vminor) != 2) + || (vmajor != 1)) { + return HE_PROTOCOL; + } + if (vminor == 0) { + version = HVER_1_0; + } else if (vminor == 1) { + version = HVER_1_1; + } else { + return HE_PROTOCOL; + } + std::string sverb(line, vend); + if (!FromString(verb, sverb.c_str())) { + return HE_PROTOCOL; // !?! HC_METHOD_NOT_SUPPORTED? + } + path.assign(line + dstart, line + dend); + return HE_NONE; +} + +bool HttpRequestData::getAbsoluteUri(std::string* uri) const { + if (HV_CONNECT == verb) + return false; + Url url(path); + if (url.valid()) { + uri->assign(path); + return true; + } + std::string host; + if (!hasHeader(HH_HOST, &host)) + return false; + url.set_address(host); + url.set_full_path(path); + uri->assign(url.url()); + return url.valid(); +} + +bool HttpRequestData::getRelativeUri(std::string* host, + std::string* path) const +{ + if (HV_CONNECT == verb) + return false; + Url url(this->path); + if (url.valid()) { + host->assign(url.address()); + path->assign(url.full_path()); + return true; + } + if (!hasHeader(HH_HOST, host)) + return false; + path->assign(this->path); + return true; +} + +// +// HttpResponseData +// + +void +HttpResponseData::clear(bool release_document) { + scode = HC_INTERNAL_SERVER_ERROR; + message.clear(); + HttpData::clear(release_document); +} + +void +HttpResponseData::copy(const HttpResponseData& src) { + scode = src.scode; + message = src.message; + HttpData::copy(src); +} + +void +HttpResponseData::set_success(uint32 scode) { + this->scode = scode; + message.clear(); + setHeader(HH_CONTENT_LENGTH, "0", false); +} + +void +HttpResponseData::set_success(const std::string& content_type, + StreamInterface* document, + uint32 scode) { + this->scode = scode; + message.erase(message.begin(), message.end()); + setContent(content_type, document); +} + +void +HttpResponseData::set_redirect(const std::string& location, uint32 scode) { + this->scode = scode; + message.clear(); + setHeader(HH_LOCATION, location); + setHeader(HH_CONTENT_LENGTH, "0", false); +} + +void +HttpResponseData::set_error(uint32 scode) { + this->scode = scode; + message.clear(); + setHeader(HH_CONTENT_LENGTH, "0", false); +} + +size_t +HttpResponseData::formatLeader(char* buffer, size_t size) const { + size_t len = sprintfn(buffer, size, "HTTP/%s %lu", ToString(version), scode); + if (!message.empty()) { + len += sprintfn(buffer + len, size - len, " %.*s", + message.size(), message.data()); + } + return len; +} + +HttpError +HttpResponseData::parseLeader(const char* line, size_t len) { + size_t pos = 0; + unsigned int vmajor, vminor, temp_scode; + int temp_pos; + if (sscanf(line, "HTTP %u%n", + &temp_scode, &temp_pos) == 1) { + // This server's response has no version. :( NOTE: This happens for every + // response to requests made from Chrome plugins, regardless of the server's + // behaviour. + LOG(LS_VERBOSE) << "HTTP version missing from response"; + version = HVER_UNKNOWN; + } else if ((sscanf(line, "HTTP/%u.%u %u%n", + &vmajor, &vminor, &temp_scode, &temp_pos) == 3) + && (vmajor == 1)) { + // This server's response does have a version. + if (vminor == 0) { + version = HVER_1_0; + } else if (vminor == 1) { + version = HVER_1_1; + } else { + return HE_PROTOCOL; + } + } else { + return HE_PROTOCOL; + } + scode = temp_scode; + pos = static_cast(temp_pos); + while ((pos < len) && isspace(static_cast(line[pos]))) ++pos; + message.assign(line + pos, len - pos); + return HE_NONE; +} + +////////////////////////////////////////////////////////////////////// +// Http Authentication +////////////////////////////////////////////////////////////////////// + +#define TEST_DIGEST 0 +#if TEST_DIGEST +/* +const char * const DIGEST_CHALLENGE = + "Digest realm=\"testrealm@host.com\"," + " qop=\"auth,auth-int\"," + " nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\"," + " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""; +const char * const DIGEST_METHOD = "GET"; +const char * const DIGEST_URI = + "/dir/index.html";; +const char * const DIGEST_CNONCE = + "0a4f113b"; +const char * const DIGEST_RESPONSE = + "6629fae49393a05397450978507c4ef1"; +//user_ = "Mufasa"; +//pass_ = "Circle Of Life"; +*/ +const char * const DIGEST_CHALLENGE = + "Digest realm=\"Squid proxy-caching web server\"," + " nonce=\"Nny4QuC5PwiSDixJ\"," + " qop=\"auth\"," + " stale=false"; +const char * const DIGEST_URI = + "/"; +const char * const DIGEST_CNONCE = + "6501d58e9a21cee1e7b5fec894ded024"; +const char * const DIGEST_RESPONSE = + "edffcb0829e755838b073a4a42de06bc"; +#endif + +std::string quote(const std::string& str) { + std::string result; + result.push_back('"'); + for (size_t i=0; iauth_method != auth_method)) + return HAR_IGNORE; + + // BASIC + if (_stricmp(auth_method.c_str(), "basic") == 0) { + if (context) + return HAR_CREDENTIALS; // Bad credentials + if (username.empty()) + return HAR_CREDENTIALS; // Missing credentials + + context = new HttpAuthContext(auth_method); + + // TODO: convert sensitive to a secure buffer that gets securely deleted + //std::string decoded = username + ":" + password; + size_t len = username.size() + password.GetLength() + 2; + char * sensitive = new char[len]; + size_t pos = strcpyn(sensitive, len, username.data(), username.size()); + pos += strcpyn(sensitive + pos, len - pos, ":"); + password.CopyTo(sensitive + pos, true); + + response = auth_method; + response.append(" "); + // TODO: create a sensitive-source version of Base64::encode + response.append(Base64::Encode(sensitive)); + memset(sensitive, 0, len); + delete [] sensitive; + return HAR_RESPONSE; + } + + // DIGEST + if (_stricmp(auth_method.c_str(), "digest") == 0) { + if (context) + return HAR_CREDENTIALS; // Bad credentials + if (username.empty()) + return HAR_CREDENTIALS; // Missing credentials + + context = new HttpAuthContext(auth_method); + + std::string cnonce, ncount; +#if TEST_DIGEST + method = DIGEST_METHOD; + uri = DIGEST_URI; + cnonce = DIGEST_CNONCE; +#else + char buffer[256]; + sprintf(buffer, "%d", static_cast(time(0))); + cnonce = MD5(buffer); +#endif + ncount = "00000001"; + + std::string realm, nonce, qop, opaque; + HttpHasAttribute(args, "realm", &realm); + HttpHasAttribute(args, "nonce", &nonce); + bool has_qop = HttpHasAttribute(args, "qop", &qop); + bool has_opaque = HttpHasAttribute(args, "opaque", &opaque); + + // TODO: convert sensitive to be secure buffer + //std::string A1 = username + ":" + realm + ":" + password; + size_t len = username.size() + realm.size() + password.GetLength() + 3; + char * sensitive = new char[len]; // A1 + size_t pos = strcpyn(sensitive, len, username.data(), username.size()); + pos += strcpyn(sensitive + pos, len - pos, ":"); + pos += strcpyn(sensitive + pos, len - pos, realm.c_str()); + pos += strcpyn(sensitive + pos, len - pos, ":"); + password.CopyTo(sensitive + pos, true); + + std::string A2 = method + ":" + uri; + std::string middle; + if (has_qop) { + qop = "auth"; + middle = nonce + ":" + ncount + ":" + cnonce + ":" + qop; + } else { + middle = nonce; + } + std::string HA1 = MD5(sensitive); + memset(sensitive, 0, len); + delete [] sensitive; + std::string HA2 = MD5(A2); + std::string dig_response = MD5(HA1 + ":" + middle + ":" + HA2); + +#if TEST_DIGEST + ASSERT(strcmp(dig_response.c_str(), DIGEST_RESPONSE) == 0); +#endif + + std::stringstream ss; + ss << auth_method; + ss << " username=" << quote(username); + ss << ", realm=" << quote(realm); + ss << ", nonce=" << quote(nonce); + ss << ", uri=" << quote(uri); + if (has_qop) { + ss << ", qop=" << qop; + ss << ", nc=" << ncount; + ss << ", cnonce=" << quote(cnonce); + } + ss << ", response=\"" << dig_response << "\""; + if (has_opaque) { + ss << ", opaque=" << quote(opaque); + } + response = ss.str(); + return HAR_RESPONSE; + } + +#ifdef WIN32 +#if 1 + bool want_negotiate = (_stricmp(auth_method.c_str(), "negotiate") == 0); + bool want_ntlm = (_stricmp(auth_method.c_str(), "ntlm") == 0); + // SPNEGO & NTLM + if (want_negotiate || want_ntlm) { + const size_t MAX_MESSAGE = 12000, MAX_SPN = 256; + char out_buf[MAX_MESSAGE], spn[MAX_SPN]; + +#if 0 // Requires funky windows versions + DWORD len = MAX_SPN; + if (DsMakeSpn("HTTP", server.HostAsURIString().c_str(), NULL, + server.port(), + 0, &len, spn) != ERROR_SUCCESS) { + LOG_F(WARNING) << "(Negotiate) - DsMakeSpn failed"; + return HAR_IGNORE; + } +#else + sprintfn(spn, MAX_SPN, "HTTP/%s", server.ToString().c_str()); +#endif + + SecBuffer out_sec; + out_sec.pvBuffer = out_buf; + out_sec.cbBuffer = sizeof(out_buf); + out_sec.BufferType = SECBUFFER_TOKEN; + + SecBufferDesc out_buf_desc; + out_buf_desc.ulVersion = 0; + out_buf_desc.cBuffers = 1; + out_buf_desc.pBuffers = &out_sec; + + const ULONG NEG_FLAGS_DEFAULT = + //ISC_REQ_ALLOCATE_MEMORY + ISC_REQ_CONFIDENTIALITY + //| ISC_REQ_EXTENDED_ERROR + //| ISC_REQ_INTEGRITY + | ISC_REQ_REPLAY_DETECT + | ISC_REQ_SEQUENCE_DETECT + //| ISC_REQ_STREAM + //| ISC_REQ_USE_SUPPLIED_CREDS + ; + + ::TimeStamp lifetime; + SECURITY_STATUS ret = S_OK; + ULONG ret_flags = 0, flags = NEG_FLAGS_DEFAULT; + + bool specify_credentials = !username.empty(); + size_t steps = 0; + + //uint32 now = Time(); + + NegotiateAuthContext * neg = static_cast(context); + if (neg) { + const size_t max_steps = 10; + if (++neg->steps >= max_steps) { + LOG(WARNING) << "AsyncHttpsProxySocket::Authenticate(Negotiate) too many retries"; + return HAR_ERROR; + } + steps = neg->steps; + + std::string challenge, decoded_challenge; + if (HttpHasNthAttribute(args, 1, &challenge, NULL) + && Base64::Decode(challenge, Base64::DO_STRICT, + &decoded_challenge, NULL)) { + SecBuffer in_sec; + in_sec.pvBuffer = const_cast(decoded_challenge.data()); + in_sec.cbBuffer = static_cast(decoded_challenge.size()); + in_sec.BufferType = SECBUFFER_TOKEN; + + SecBufferDesc in_buf_desc; + in_buf_desc.ulVersion = 0; + in_buf_desc.cBuffers = 1; + in_buf_desc.pBuffers = &in_sec; + + ret = InitializeSecurityContextA(&neg->cred, &neg->ctx, spn, flags, 0, SECURITY_NATIVE_DREP, &in_buf_desc, 0, &neg->ctx, &out_buf_desc, &ret_flags, &lifetime); + //LOG(INFO) << "$$$ InitializeSecurityContext @ " << TimeSince(now); + if (FAILED(ret)) { + LOG(LS_ERROR) << "InitializeSecurityContext returned: " + << ErrorName(ret, SECURITY_ERRORS); + return HAR_ERROR; + } + } else if (neg->specified_credentials) { + // Try again with default credentials + specify_credentials = false; + delete context; + context = neg = 0; + } else { + return HAR_CREDENTIALS; + } + } + + if (!neg) { + unsigned char userbuf[256], passbuf[256], domainbuf[16]; + SEC_WINNT_AUTH_IDENTITY_A auth_id, * pauth_id = 0; + if (specify_credentials) { + memset(&auth_id, 0, sizeof(auth_id)); + size_t len = password.GetLength()+1; + char * sensitive = new char[len]; + password.CopyTo(sensitive, true); + std::string::size_type pos = username.find('\\'); + if (pos == std::string::npos) { + auth_id.UserLength = static_cast( + _min(sizeof(userbuf) - 1, username.size())); + memcpy(userbuf, username.c_str(), auth_id.UserLength); + userbuf[auth_id.UserLength] = 0; + auth_id.DomainLength = 0; + domainbuf[auth_id.DomainLength] = 0; + auth_id.PasswordLength = static_cast( + _min(sizeof(passbuf) - 1, password.GetLength())); + memcpy(passbuf, sensitive, auth_id.PasswordLength); + passbuf[auth_id.PasswordLength] = 0; + } else { + auth_id.UserLength = static_cast( + _min(sizeof(userbuf) - 1, username.size() - pos - 1)); + memcpy(userbuf, username.c_str() + pos + 1, auth_id.UserLength); + userbuf[auth_id.UserLength] = 0; + auth_id.DomainLength = static_cast( + _min(sizeof(domainbuf) - 1, pos)); + memcpy(domainbuf, username.c_str(), auth_id.DomainLength); + domainbuf[auth_id.DomainLength] = 0; + auth_id.PasswordLength = static_cast( + _min(sizeof(passbuf) - 1, password.GetLength())); + memcpy(passbuf, sensitive, auth_id.PasswordLength); + passbuf[auth_id.PasswordLength] = 0; + } + memset(sensitive, 0, len); + delete [] sensitive; + auth_id.User = userbuf; + auth_id.Domain = domainbuf; + auth_id.Password = passbuf; + auth_id.Flags = SEC_WINNT_AUTH_IDENTITY_ANSI; + pauth_id = &auth_id; + LOG(LS_VERBOSE) << "Negotiate protocol: Using specified credentials"; + } else { + LOG(LS_VERBOSE) << "Negotiate protocol: Using default credentials"; + } + + CredHandle cred; + ret = AcquireCredentialsHandleA(0, want_negotiate ? NEGOSSP_NAME_A : NTLMSP_NAME_A, SECPKG_CRED_OUTBOUND, 0, pauth_id, 0, 0, &cred, &lifetime); + //LOG(INFO) << "$$$ AcquireCredentialsHandle @ " << TimeSince(now); + if (ret != SEC_E_OK) { + LOG(LS_ERROR) << "AcquireCredentialsHandle error: " + << ErrorName(ret, SECURITY_ERRORS); + return HAR_IGNORE; + } + + //CSecBufferBundle<5, CSecBufferBase::FreeSSPI> sb_out; + + CtxtHandle ctx; + ret = InitializeSecurityContextA(&cred, 0, spn, flags, 0, SECURITY_NATIVE_DREP, 0, 0, &ctx, &out_buf_desc, &ret_flags, &lifetime); + //LOG(INFO) << "$$$ InitializeSecurityContext @ " << TimeSince(now); + if (FAILED(ret)) { + LOG(LS_ERROR) << "InitializeSecurityContext returned: " + << ErrorName(ret, SECURITY_ERRORS); + FreeCredentialsHandle(&cred); + return HAR_IGNORE; + } + + ASSERT(!context); + context = neg = new NegotiateAuthContext(auth_method, cred, ctx); + neg->specified_credentials = specify_credentials; + neg->steps = steps; + } + + if ((ret == SEC_I_COMPLETE_NEEDED) || (ret == SEC_I_COMPLETE_AND_CONTINUE)) { + ret = CompleteAuthToken(&neg->ctx, &out_buf_desc); + //LOG(INFO) << "$$$ CompleteAuthToken @ " << TimeSince(now); + LOG(LS_VERBOSE) << "CompleteAuthToken returned: " + << ErrorName(ret, SECURITY_ERRORS); + if (FAILED(ret)) { + return HAR_ERROR; + } + } + + //LOG(INFO) << "$$$ NEGOTIATE took " << TimeSince(now) << "ms"; + + std::string decoded(out_buf, out_buf + out_sec.cbBuffer); + response = auth_method; + response.append(" "); + response.append(Base64::Encode(decoded)); + return HAR_RESPONSE; + } +#endif +#endif // WIN32 + + return HAR_IGNORE; +} + +////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/httpcommon.h b/talk/base/httpcommon.h new file mode 100644 index 000000000..1112f8d70 --- /dev/null +++ b/talk/base/httpcommon.h @@ -0,0 +1,463 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_HTTPCOMMON_H__ +#define TALK_BASE_HTTPCOMMON_H__ + +#include +#include +#include +#include "talk/base/basictypes.h" +#include "talk/base/common.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringutils.h" +#include "talk/base/stream.h" + +namespace talk_base { + +class CryptString; +class SocketAddress; + +////////////////////////////////////////////////////////////////////// +// Constants +////////////////////////////////////////////////////////////////////// + +enum HttpCode { + HC_OK = 200, + HC_NON_AUTHORITATIVE = 203, + HC_NO_CONTENT = 204, + HC_PARTIAL_CONTENT = 206, + + HC_MULTIPLE_CHOICES = 300, + HC_MOVED_PERMANENTLY = 301, + HC_FOUND = 302, + HC_SEE_OTHER = 303, + HC_NOT_MODIFIED = 304, + HC_MOVED_TEMPORARILY = 307, + + HC_BAD_REQUEST = 400, + HC_UNAUTHORIZED = 401, + HC_FORBIDDEN = 403, + HC_NOT_FOUND = 404, + HC_PROXY_AUTHENTICATION_REQUIRED = 407, + HC_GONE = 410, + + HC_INTERNAL_SERVER_ERROR = 500, + HC_NOT_IMPLEMENTED = 501, + HC_SERVICE_UNAVAILABLE = 503, +}; + +enum HttpVersion { + HVER_1_0, HVER_1_1, HVER_UNKNOWN, + HVER_LAST = HVER_UNKNOWN +}; + +enum HttpVerb { + HV_GET, HV_POST, HV_PUT, HV_DELETE, HV_CONNECT, HV_HEAD, + HV_LAST = HV_HEAD +}; + +enum HttpError { + HE_NONE, + HE_PROTOCOL, // Received non-valid HTTP data + HE_DISCONNECTED, // Connection closed unexpectedly + HE_OVERFLOW, // Received too much data for internal buffers + HE_CONNECT_FAILED, // The socket failed to connect. + HE_SOCKET_ERROR, // An error occurred on a connected socket + HE_SHUTDOWN, // Http object is being destroyed + HE_OPERATION_CANCELLED, // Connection aborted locally + HE_AUTH, // Proxy Authentication Required + HE_CERTIFICATE_EXPIRED, // During SSL negotiation + HE_STREAM, // Problem reading or writing to the document + HE_CACHE, // Problem reading from cache + HE_DEFAULT +}; + +enum HttpHeader { + HH_AGE, + HH_CACHE_CONTROL, + HH_CONNECTION, + HH_CONTENT_DISPOSITION, + HH_CONTENT_LENGTH, + HH_CONTENT_RANGE, + HH_CONTENT_TYPE, + HH_COOKIE, + HH_DATE, + HH_ETAG, + HH_EXPIRES, + HH_HOST, + HH_IF_MODIFIED_SINCE, + HH_IF_NONE_MATCH, + HH_KEEP_ALIVE, + HH_LAST_MODIFIED, + HH_LOCATION, + HH_PROXY_AUTHENTICATE, + HH_PROXY_AUTHORIZATION, + HH_PROXY_CONNECTION, + HH_RANGE, + HH_SET_COOKIE, + HH_TE, + HH_TRAILERS, + HH_TRANSFER_ENCODING, + HH_UPGRADE, + HH_USER_AGENT, + HH_WWW_AUTHENTICATE, + HH_LAST = HH_WWW_AUTHENTICATE +}; + +const uint16 HTTP_DEFAULT_PORT = 80; +const uint16 HTTP_SECURE_PORT = 443; + +////////////////////////////////////////////////////////////////////// +// Utility Functions +////////////////////////////////////////////////////////////////////// + +inline HttpError mkerr(HttpError err, HttpError def_err = HE_DEFAULT) { + return (err != HE_NONE) ? err : def_err; +} + +const char* ToString(HttpVersion version); +bool FromString(HttpVersion& version, const std::string& str); + +const char* ToString(HttpVerb verb); +bool FromString(HttpVerb& verb, const std::string& str); + +const char* ToString(HttpHeader header); +bool FromString(HttpHeader& header, const std::string& str); + +inline bool HttpCodeIsInformational(uint32 code) { return ((code / 100) == 1); } +inline bool HttpCodeIsSuccessful(uint32 code) { return ((code / 100) == 2); } +inline bool HttpCodeIsRedirection(uint32 code) { return ((code / 100) == 3); } +inline bool HttpCodeIsClientError(uint32 code) { return ((code / 100) == 4); } +inline bool HttpCodeIsServerError(uint32 code) { return ((code / 100) == 5); } + +bool HttpCodeHasBody(uint32 code); +bool HttpCodeIsCacheable(uint32 code); +bool HttpHeaderIsEndToEnd(HttpHeader header); +bool HttpHeaderIsCollapsible(HttpHeader header); + +struct HttpData; +bool HttpShouldKeepAlive(const HttpData& data); + +typedef std::pair HttpAttribute; +typedef std::vector HttpAttributeList; +void HttpComposeAttributes(const HttpAttributeList& attributes, char separator, + std::string* composed); +void HttpParseAttributes(const char * data, size_t len, + HttpAttributeList& attributes); +bool HttpHasAttribute(const HttpAttributeList& attributes, + const std::string& name, + std::string* value); +bool HttpHasNthAttribute(HttpAttributeList& attributes, + size_t index, + std::string* name, + std::string* value); + +// Convert RFC1123 date (DoW, DD Mon YYYY HH:MM:SS TZ) to unix timestamp +bool HttpDateToSeconds(const std::string& date, time_t* seconds); + +inline uint16 HttpDefaultPort(bool secure) { + return secure ? HTTP_SECURE_PORT : HTTP_DEFAULT_PORT; +} + +// Returns the http server notation for a given address +std::string HttpAddress(const SocketAddress& address, bool secure); + +// functional for insensitive std::string compare +struct iless { + bool operator()(const std::string& lhs, const std::string& rhs) const { + return (::_stricmp(lhs.c_str(), rhs.c_str()) < 0); + } +}; + +// put quotes around a string and escape any quotes inside it +std::string quote(const std::string& str); + +////////////////////////////////////////////////////////////////////// +// Url +////////////////////////////////////////////////////////////////////// + +template +class Url { +public: + typedef typename Traits::string string; + + // TODO: Implement Encode/Decode + static int Encode(const CTYPE* source, CTYPE* destination, size_t len); + static int Encode(const string& source, string& destination); + static int Decode(const CTYPE* source, CTYPE* destination, size_t len); + static int Decode(const string& source, string& destination); + + Url(const string& url) { do_set_url(url.c_str(), url.size()); } + Url(const string& path, const string& host, uint16 port = HTTP_DEFAULT_PORT) + : host_(host), port_(port), secure_(HTTP_SECURE_PORT == port) + { set_full_path(path); } + + bool valid() const { return !host_.empty(); } + void clear() { + host_.clear(); + port_ = HTTP_DEFAULT_PORT; + secure_ = false; + path_.assign(1, static_cast('/')); + query_.clear(); + } + + void set_url(const string& val) { + do_set_url(val.c_str(), val.size()); + } + string url() const { + string val; do_get_url(&val); return val; + } + + void set_address(const string& val) { + do_set_address(val.c_str(), val.size()); + } + string address() const { + string val; do_get_address(&val); return val; + } + + void set_full_path(const string& val) { + do_set_full_path(val.c_str(), val.size()); + } + string full_path() const { + string val; do_get_full_path(&val); return val; + } + + void set_host(const string& val) { host_ = val; } + const string& host() const { return host_; } + + void set_port(uint16 val) { port_ = val; } + uint16 port() const { return port_; } + + void set_secure(bool val) { secure_ = val; } + bool secure() const { return secure_; } + + void set_path(const string& val) { + if (val.empty()) { + path_.assign(1, static_cast('/')); + } else { + ASSERT(val[0] == static_cast('/')); + path_ = val; + } + } + const string& path() const { return path_; } + + void set_query(const string& val) { + ASSERT(val.empty() || (val[0] == static_cast('?'))); + query_ = val; + } + const string& query() const { return query_; } + + bool get_attribute(const string& name, string* value) const; + +private: + void do_set_url(const CTYPE* val, size_t len); + void do_set_address(const CTYPE* val, size_t len); + void do_set_full_path(const CTYPE* val, size_t len); + + void do_get_url(string* val) const; + void do_get_address(string* val) const; + void do_get_full_path(string* val) const; + + string host_, path_, query_; + uint16 port_; + bool secure_; +}; + +////////////////////////////////////////////////////////////////////// +// HttpData +////////////////////////////////////////////////////////////////////// + +struct HttpData { + typedef std::multimap HeaderMap; + typedef HeaderMap::const_iterator const_iterator; + typedef HeaderMap::iterator iterator; + + HttpVersion version; + scoped_ptr document; + + HttpData() : version(HVER_1_1) { } + + enum HeaderCombine { HC_YES, HC_NO, HC_AUTO, HC_REPLACE, HC_NEW }; + void changeHeader(const std::string& name, const std::string& value, + HeaderCombine combine); + inline void addHeader(const std::string& name, const std::string& value, + bool append = true) { + changeHeader(name, value, append ? HC_AUTO : HC_NO); + } + inline void setHeader(const std::string& name, const std::string& value, + bool overwrite = true) { + changeHeader(name, value, overwrite ? HC_REPLACE : HC_NEW); + } + // Returns count of erased headers + size_t clearHeader(const std::string& name); + // Returns iterator to next header + iterator clearHeader(iterator header); + + // keep in mind, this may not do what you want in the face of multiple headers + bool hasHeader(const std::string& name, std::string* value) const; + + inline const_iterator begin() const { + return headers_.begin(); + } + inline const_iterator end() const { + return headers_.end(); + } + inline iterator begin() { + return headers_.begin(); + } + inline iterator end() { + return headers_.end(); + } + inline const_iterator begin(const std::string& name) const { + return headers_.lower_bound(name); + } + inline const_iterator end(const std::string& name) const { + return headers_.upper_bound(name); + } + inline iterator begin(const std::string& name) { + return headers_.lower_bound(name); + } + inline iterator end(const std::string& name) { + return headers_.upper_bound(name); + } + + // Convenience methods using HttpHeader + inline void changeHeader(HttpHeader header, const std::string& value, + HeaderCombine combine) { + changeHeader(ToString(header), value, combine); + } + inline void addHeader(HttpHeader header, const std::string& value, + bool append = true) { + addHeader(ToString(header), value, append); + } + inline void setHeader(HttpHeader header, const std::string& value, + bool overwrite = true) { + setHeader(ToString(header), value, overwrite); + } + inline void clearHeader(HttpHeader header) { + clearHeader(ToString(header)); + } + inline bool hasHeader(HttpHeader header, std::string* value) const { + return hasHeader(ToString(header), value); + } + inline const_iterator begin(HttpHeader header) const { + return headers_.lower_bound(ToString(header)); + } + inline const_iterator end(HttpHeader header) const { + return headers_.upper_bound(ToString(header)); + } + inline iterator begin(HttpHeader header) { + return headers_.lower_bound(ToString(header)); + } + inline iterator end(HttpHeader header) { + return headers_.upper_bound(ToString(header)); + } + + void setContent(const std::string& content_type, StreamInterface* document); + void setDocumentAndLength(StreamInterface* document); + + virtual size_t formatLeader(char* buffer, size_t size) const = 0; + virtual HttpError parseLeader(const char* line, size_t len) = 0; + +protected: + virtual ~HttpData() { } + void clear(bool release_document); + void copy(const HttpData& src); + +private: + HeaderMap headers_; +}; + +struct HttpRequestData : public HttpData { + HttpVerb verb; + std::string path; + + HttpRequestData() : verb(HV_GET) { } + + void clear(bool release_document); + void copy(const HttpRequestData& src); + + virtual size_t formatLeader(char* buffer, size_t size) const; + virtual HttpError parseLeader(const char* line, size_t len); + + bool getAbsoluteUri(std::string* uri) const; + bool getRelativeUri(std::string* host, std::string* path) const; +}; + +struct HttpResponseData : public HttpData { + uint32 scode; + std::string message; + + HttpResponseData() : scode(HC_INTERNAL_SERVER_ERROR) { } + void clear(bool release_document); + void copy(const HttpResponseData& src); + + // Convenience methods + void set_success(uint32 scode = HC_OK); + void set_success(const std::string& content_type, StreamInterface* document, + uint32 scode = HC_OK); + void set_redirect(const std::string& location, + uint32 scode = HC_MOVED_TEMPORARILY); + void set_error(uint32 scode); + + virtual size_t formatLeader(char* buffer, size_t size) const; + virtual HttpError parseLeader(const char* line, size_t len); +}; + +struct HttpTransaction { + HttpRequestData request; + HttpResponseData response; +}; + +////////////////////////////////////////////////////////////////////// +// Http Authentication +////////////////////////////////////////////////////////////////////// + +struct HttpAuthContext { + std::string auth_method; + HttpAuthContext(const std::string& auth) : auth_method(auth) { } + virtual ~HttpAuthContext() { } +}; + +enum HttpAuthResult { HAR_RESPONSE, HAR_IGNORE, HAR_CREDENTIALS, HAR_ERROR }; + +// 'context' is used by this function to record information between calls. +// Start by passing a null pointer, then pass the same pointer each additional +// call. When the authentication attempt is finished, delete the context. +HttpAuthResult HttpAuthenticate( + const char * challenge, size_t len, + const SocketAddress& server, + const std::string& method, const std::string& uri, + const std::string& username, const CryptString& password, + HttpAuthContext *& context, std::string& response, std::string& auth_method); + +////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_HTTPCOMMON_H__ diff --git a/talk/base/httpcommon_unittest.cc b/talk/base/httpcommon_unittest.cc new file mode 100644 index 000000000..b60f59740 --- /dev/null +++ b/talk/base/httpcommon_unittest.cc @@ -0,0 +1,182 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/gunit.h" +#include "talk/base/httpcommon-inl.h" +#include "talk/base/httpcommon.h" + +namespace talk_base { + +#define TEST_PROTOCOL "http://" +#define TEST_HOST "www.google.com" +#define TEST_PATH "/folder/file.html" +#define TEST_QUERY "?query=x&attr=y" +#define TEST_URL TEST_PROTOCOL TEST_HOST TEST_PATH TEST_QUERY + +TEST(Url, DecomposesUrls) { + Url url(TEST_URL); + EXPECT_TRUE(url.valid()); + EXPECT_FALSE(url.secure()); + EXPECT_STREQ(TEST_HOST, url.host().c_str()); + EXPECT_EQ(80, url.port()); + EXPECT_STREQ(TEST_PATH, url.path().c_str()); + EXPECT_STREQ(TEST_QUERY, url.query().c_str()); + EXPECT_STREQ(TEST_HOST, url.address().c_str()); + EXPECT_STREQ(TEST_PATH TEST_QUERY, url.full_path().c_str()); + EXPECT_STREQ(TEST_URL, url.url().c_str()); +} + +TEST(Url, ComposesUrls) { + // Set in constructor + Url url(TEST_PATH TEST_QUERY, TEST_HOST, 80); + EXPECT_TRUE(url.valid()); + EXPECT_FALSE(url.secure()); + EXPECT_STREQ(TEST_HOST, url.host().c_str()); + EXPECT_EQ(80, url.port()); + EXPECT_STREQ(TEST_PATH, url.path().c_str()); + EXPECT_STREQ(TEST_QUERY, url.query().c_str()); + EXPECT_STREQ(TEST_HOST, url.address().c_str()); + EXPECT_STREQ(TEST_PATH TEST_QUERY, url.full_path().c_str()); + EXPECT_STREQ(TEST_URL, url.url().c_str()); + + url.clear(); + EXPECT_FALSE(url.valid()); + EXPECT_FALSE(url.secure()); + EXPECT_STREQ("", url.host().c_str()); + EXPECT_EQ(80, url.port()); + EXPECT_STREQ("/", url.path().c_str()); + EXPECT_STREQ("", url.query().c_str()); + + // Set component-wise + url.set_host(TEST_HOST); + url.set_port(80); + url.set_path(TEST_PATH); + url.set_query(TEST_QUERY); + EXPECT_TRUE(url.valid()); + EXPECT_FALSE(url.secure()); + EXPECT_STREQ(TEST_HOST, url.host().c_str()); + EXPECT_EQ(80, url.port()); + EXPECT_STREQ(TEST_PATH, url.path().c_str()); + EXPECT_STREQ(TEST_QUERY, url.query().c_str()); + EXPECT_STREQ(TEST_HOST, url.address().c_str()); + EXPECT_STREQ(TEST_PATH TEST_QUERY, url.full_path().c_str()); + EXPECT_STREQ(TEST_URL, url.url().c_str()); +} + +TEST(Url, EnsuresNonEmptyPath) { + Url url(TEST_PROTOCOL TEST_HOST); + EXPECT_TRUE(url.valid()); + EXPECT_STREQ("/", url.path().c_str()); + + url.clear(); + EXPECT_STREQ("/", url.path().c_str()); + url.set_path(""); + EXPECT_STREQ("/", url.path().c_str()); + + url.clear(); + EXPECT_STREQ("/", url.path().c_str()); + url.set_full_path(""); + EXPECT_STREQ("/", url.path().c_str()); +} + +TEST(Url, GetQueryAttributes) { + Url url(TEST_URL); + std::string value; + EXPECT_TRUE(url.get_attribute("query", &value)); + EXPECT_STREQ("x", value.c_str()); + value.clear(); + EXPECT_TRUE(url.get_attribute("attr", &value)); + EXPECT_STREQ("y", value.c_str()); + value.clear(); + EXPECT_FALSE(url.get_attribute("Query", &value)); + EXPECT_TRUE(value.empty()); +} + +TEST(Url, SkipsUserAndPassword) { + Url url("https://mail.google.com:pwd@badsite.com:12345/asdf"); + EXPECT_TRUE(url.valid()); + EXPECT_TRUE(url.secure()); + EXPECT_STREQ("badsite.com", url.host().c_str()); + EXPECT_EQ(12345, url.port()); + EXPECT_STREQ("/asdf", url.path().c_str()); + EXPECT_STREQ("badsite.com:12345", url.address().c_str()); +} + +TEST(Url, SkipsUser) { + Url url("https://mail.google.com@badsite.com:12345/asdf"); + EXPECT_TRUE(url.valid()); + EXPECT_TRUE(url.secure()); + EXPECT_STREQ("badsite.com", url.host().c_str()); + EXPECT_EQ(12345, url.port()); + EXPECT_STREQ("/asdf", url.path().c_str()); + EXPECT_STREQ("badsite.com:12345", url.address().c_str()); +} + +TEST(HttpResponseData, parseLeaderHttp1_0) { + static const char kResponseString[] = "HTTP/1.0 200 OK"; + HttpResponseData response; + EXPECT_EQ(HE_NONE, response.parseLeader(kResponseString, + sizeof(kResponseString) - 1)); + EXPECT_EQ(HVER_1_0, response.version); + EXPECT_EQ(200U, response.scode); +} + +TEST(HttpResponseData, parseLeaderHttp1_1) { + static const char kResponseString[] = "HTTP/1.1 200 OK"; + HttpResponseData response; + EXPECT_EQ(HE_NONE, response.parseLeader(kResponseString, + sizeof(kResponseString) - 1)); + EXPECT_EQ(HVER_1_1, response.version); + EXPECT_EQ(200U, response.scode); +} + +TEST(HttpResponseData, parseLeaderHttpUnknown) { + static const char kResponseString[] = "HTTP 200 OK"; + HttpResponseData response; + EXPECT_EQ(HE_NONE, response.parseLeader(kResponseString, + sizeof(kResponseString) - 1)); + EXPECT_EQ(HVER_UNKNOWN, response.version); + EXPECT_EQ(200U, response.scode); +} + +TEST(HttpResponseData, parseLeaderHttpFailure) { + static const char kResponseString[] = "HTTP/1.1 503 Service Unavailable"; + HttpResponseData response; + EXPECT_EQ(HE_NONE, response.parseLeader(kResponseString, + sizeof(kResponseString) - 1)); + EXPECT_EQ(HVER_1_1, response.version); + EXPECT_EQ(503U, response.scode); +} + +TEST(HttpResponseData, parseLeaderHttpInvalid) { + static const char kResponseString[] = "Durrrrr, what's HTTP?"; + HttpResponseData response; + EXPECT_EQ(HE_PROTOCOL, response.parseLeader(kResponseString, + sizeof(kResponseString) - 1)); +} + +} // namespace talk_base diff --git a/talk/base/httprequest.cc b/talk/base/httprequest.cc new file mode 100644 index 000000000..48c924efe --- /dev/null +++ b/talk/base/httprequest.cc @@ -0,0 +1,127 @@ +/* + * libjingle + * Copyright 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/base/httprequest.h" + +#include "talk/base/common.h" +#include "talk/base/firewallsocketserver.h" +#include "talk/base/httpclient.h" +#include "talk/base/logging.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/socketadapters.h" +#include "talk/base/socketpool.h" +#include "talk/base/ssladapter.h" + +using namespace talk_base; + +/////////////////////////////////////////////////////////////////////////////// +// HttpMonitor +/////////////////////////////////////////////////////////////////////////////// + +HttpMonitor::HttpMonitor(SocketServer *ss) { + ASSERT(Thread::Current() != NULL); + ss_ = ss; + reset(); +} + +void HttpMonitor::Connect(HttpClient *http) { + http->SignalHttpClientComplete.connect(this, + &HttpMonitor::OnHttpClientComplete); +} + +void HttpMonitor::OnHttpClientComplete(HttpClient * http, HttpErrorType error) { + complete_ = true; + error_ = error; + ss_->WakeUp(); +} + +/////////////////////////////////////////////////////////////////////////////// +// HttpRequest +/////////////////////////////////////////////////////////////////////////////// + +const int kDefaultHTTPTimeout = 30 * 1000; // 30 sec + +HttpRequest::HttpRequest(const std::string &user_agent) + : firewall_(0), port_(80), secure_(false), + timeout_(kDefaultHTTPTimeout), fail_redirect_(false), + client_(user_agent.c_str(), NULL), error_(HE_NONE) { +} + +void HttpRequest::Send() { + // TODO: Rewrite this to use the thread's native socket server, and a more + // natural flow? + + PhysicalSocketServer physical; + SocketServer * ss = &physical; + if (firewall_) { + ss = new FirewallSocketServer(ss, firewall_); + } + + SslSocketFactory factory(ss, client_.agent()); + factory.SetProxy(proxy_); + if (secure_) + factory.UseSSL(host_.c_str()); + + //factory.SetLogging("HttpRequest"); + + ReuseSocketPool pool(&factory); + client_.set_pool(&pool); + + bool transparent_proxy = (port_ == 80) && ((proxy_.type == PROXY_HTTPS) || + (proxy_.type == PROXY_UNKNOWN)); + + if (transparent_proxy) { + client_.set_proxy(proxy_); + } + client_.set_fail_redirect(fail_redirect_); + + SocketAddress server(host_, port_); + client_.set_server(server); + + LOG(LS_INFO) << "HttpRequest start: " << host_ + client_.request().path; + + HttpMonitor monitor(ss); + monitor.Connect(&client_); + client_.start(); + ss->Wait(timeout_, true); + if (!monitor.done()) { + LOG(LS_INFO) << "HttpRequest request timed out"; + client_.reset(); + return; + } + + set_error(monitor.error()); + if (error_) { + LOG(LS_INFO) << "HttpRequest request error: " << error_; + return; + } + + std::string value; + if (client_.response().hasHeader(HH_LOCATION, &value)) { + response_redirect_ = value.c_str(); + } +} diff --git a/talk/base/httprequest.h b/talk/base/httprequest.h new file mode 100644 index 000000000..2e13c328f --- /dev/null +++ b/talk/base/httprequest.h @@ -0,0 +1,132 @@ +/* + * libjingle + * Copyright 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. + */ + +#ifndef _HTTPREQUEST_H_ +#define _HTTPREQUEST_H_ + +#include "talk/base/httpclient.h" +#include "talk/base/logging.h" +#include "talk/base/proxyinfo.h" +#include "talk/base/socketserver.h" +#include "talk/base/thread.h" +#include "talk/base/sslsocketfactory.h" // Deprecated include + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// HttpRequest +/////////////////////////////////////////////////////////////////////////////// + +class FirewallManager; +class MemoryStream; + +class HttpRequest { +public: + HttpRequest(const std::string &user_agent); + + void Send(); + + void set_proxy(const ProxyInfo& proxy) { + proxy_ = proxy; + } + void set_firewall(FirewallManager * firewall) { + firewall_ = firewall; + } + + // The DNS name of the host to connect to. + const std::string& host() { return host_; } + void set_host(const std::string& host) { host_ = host; } + + // The port to connect to on the target host. + int port() { return port_; } + void set_port(int port) { port_ = port; } + + // Whether the request should use SSL. + bool secure() { return secure_; } + void set_secure(bool secure) { secure_ = secure; } + + // Returns the redirect when redirection occurs + const std::string& response_redirect() { return response_redirect_; } + + // Time to wait on the download, in ms. Default is 5000 (5s) + int timeout() { return timeout_; } + void set_timeout(int timeout) { timeout_ = timeout; } + + // Fail redirects to allow analysis of redirect urls, etc. + bool fail_redirect() const { return fail_redirect_; } + void set_fail_redirect(bool fail_redirect) { fail_redirect_ = fail_redirect; } + + HttpRequestData& request() { return client_.request(); } + HttpResponseData& response() { return client_.response(); } + HttpErrorType error() { return error_; } + +protected: + void set_error(HttpErrorType error) { error_ = error; } + +private: + ProxyInfo proxy_; + FirewallManager * firewall_; + std::string host_; + int port_; + bool secure_; + int timeout_; + bool fail_redirect_; + HttpClient client_; + HttpErrorType error_; + std::string response_redirect_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// HttpMonitor +/////////////////////////////////////////////////////////////////////////////// + +class HttpMonitor : public sigslot::has_slots<> { +public: + HttpMonitor(SocketServer *ss); + + void reset() { + complete_ = false; + error_ = HE_DEFAULT; + } + + bool done() const { return complete_; } + HttpErrorType error() const { return error_; } + + void Connect(HttpClient* http); + void OnHttpClientComplete(HttpClient * http, HttpErrorType error); + +private: + bool complete_; + HttpErrorType error_; + SocketServer *ss_; +}; + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base_ + +#endif // _HTTPREQUEST_H_ diff --git a/talk/base/httpserver.cc b/talk/base/httpserver.cc new file mode 100644 index 000000000..7d467c39f --- /dev/null +++ b/talk/base/httpserver.cc @@ -0,0 +1,305 @@ +/* + * libjingle + * Copyright 2004--2005, 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 + +#include "talk/base/httpcommon-inl.h" + +#include "talk/base/asyncsocket.h" +#include "talk/base/common.h" +#include "talk/base/httpserver.h" +#include "talk/base/logging.h" +#include "talk/base/socketstream.h" +#include "talk/base/thread.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// HttpServer +/////////////////////////////////////////////////////////////////////////////// + +HttpServer::HttpServer() : next_connection_id_(1), closing_(false) { +} + +HttpServer::~HttpServer() { + if (closing_) { + LOG(LS_WARNING) << "HttpServer::CloseAll has not completed"; + } + for (ConnectionMap::iterator it = connections_.begin(); + it != connections_.end(); + ++it) { + StreamInterface* stream = it->second->EndProcess(); + delete stream; + delete it->second; + } +} + +int +HttpServer::HandleConnection(StreamInterface* stream) { + int connection_id = next_connection_id_++; + ASSERT(connection_id != HTTP_INVALID_CONNECTION_ID); + Connection* connection = new Connection(connection_id, this); + connections_.insert(ConnectionMap::value_type(connection_id, connection)); + connection->BeginProcess(stream); + return connection_id; +} + +void +HttpServer::Respond(HttpServerTransaction* transaction) { + int connection_id = transaction->connection_id(); + if (Connection* connection = Find(connection_id)) { + connection->Respond(transaction); + } else { + delete transaction; + // We may be tempted to SignalHttpComplete, but that implies that a + // connection still exists. + } +} + +void +HttpServer::Close(int connection_id, bool force) { + if (Connection* connection = Find(connection_id)) { + connection->InitiateClose(force); + } +} + +void +HttpServer::CloseAll(bool force) { + if (connections_.empty()) { + SignalCloseAllComplete(this); + return; + } + closing_ = true; + std::list connections; + for (ConnectionMap::const_iterator it = connections_.begin(); + it != connections_.end(); ++it) { + connections.push_back(it->second); + } + for (std::list::const_iterator it = connections.begin(); + it != connections.end(); ++it) { + (*it)->InitiateClose(force); + } +} + +HttpServer::Connection* +HttpServer::Find(int connection_id) { + ConnectionMap::iterator it = connections_.find(connection_id); + if (it == connections_.end()) + return NULL; + return it->second; +} + +void +HttpServer::Remove(int connection_id) { + ConnectionMap::iterator it = connections_.find(connection_id); + if (it == connections_.end()) { + ASSERT(false); + return; + } + Connection* connection = it->second; + connections_.erase(it); + SignalConnectionClosed(this, connection_id, connection->EndProcess()); + delete connection; + if (closing_ && connections_.empty()) { + closing_ = false; + SignalCloseAllComplete(this); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// HttpServer::Connection +/////////////////////////////////////////////////////////////////////////////// + +HttpServer::Connection::Connection(int connection_id, HttpServer* server) + : connection_id_(connection_id), server_(server), + current_(NULL), signalling_(false), close_(false) { +} + +HttpServer::Connection::~Connection() { + // It's possible that an object hosted inside this transaction signalled + // an event which caused the connection to close. + Thread::Current()->Dispose(current_); +} + +void +HttpServer::Connection::BeginProcess(StreamInterface* stream) { + base_.notify(this); + base_.attach(stream); + current_ = new HttpServerTransaction(connection_id_); + if (base_.mode() != HM_CONNECT) + base_.recv(¤t_->request); +} + +StreamInterface* +HttpServer::Connection::EndProcess() { + base_.notify(NULL); + base_.abort(HE_DISCONNECTED); + return base_.detach(); +} + +void +HttpServer::Connection::Respond(HttpServerTransaction* transaction) { + ASSERT(current_ == NULL); + current_ = transaction; + if (current_->response.begin() == current_->response.end()) { + current_->response.set_error(HC_INTERNAL_SERVER_ERROR); + } + bool keep_alive = HttpShouldKeepAlive(current_->request); + current_->response.setHeader(HH_CONNECTION, + keep_alive ? "Keep-Alive" : "Close", + false); + close_ = !HttpShouldKeepAlive(current_->response); + base_.send(¤t_->response); +} + +void +HttpServer::Connection::InitiateClose(bool force) { + bool request_in_progress = (HM_SEND == base_.mode()) || (NULL == current_); + if (!signalling_ && (force || !request_in_progress)) { + server_->Remove(connection_id_); + } else { + close_ = true; + } +} + +// +// IHttpNotify Implementation +// + +HttpError +HttpServer::Connection::onHttpHeaderComplete(bool chunked, size_t& data_size) { + if (data_size == SIZE_UNKNOWN) { + data_size = 0; + } + ASSERT(current_ != NULL); + bool custom_document = false; + server_->SignalHttpRequestHeader(server_, current_, &custom_document); + if (!custom_document) { + current_->request.document.reset(new MemoryStream); + } + return HE_NONE; +} + +void +HttpServer::Connection::onHttpComplete(HttpMode mode, HttpError err) { + if (mode == HM_SEND) { + ASSERT(current_ != NULL); + signalling_ = true; + server_->SignalHttpRequestComplete(server_, current_, err); + signalling_ = false; + if (close_) { + // Force a close + err = HE_DISCONNECTED; + } + } + if (err != HE_NONE) { + server_->Remove(connection_id_); + } else if (mode == HM_CONNECT) { + base_.recv(¤t_->request); + } else if (mode == HM_RECV) { + ASSERT(current_ != NULL); + // TODO: do we need this? + //request_.document_->rewind(); + HttpServerTransaction* transaction = current_; + current_ = NULL; + server_->SignalHttpRequest(server_, transaction); + } else if (mode == HM_SEND) { + Thread::Current()->Dispose(current_->response.document.release()); + current_->request.clear(true); + current_->response.clear(true); + base_.recv(¤t_->request); + } else { + ASSERT(false); + } +} + +void +HttpServer::Connection::onHttpClosed(HttpError err) { + UNUSED(err); + server_->Remove(connection_id_); +} + +/////////////////////////////////////////////////////////////////////////////// +// HttpListenServer +/////////////////////////////////////////////////////////////////////////////// + +HttpListenServer::HttpListenServer() { + SignalConnectionClosed.connect(this, &HttpListenServer::OnConnectionClosed); +} + +HttpListenServer::~HttpListenServer() { +} + +int HttpListenServer::Listen(const SocketAddress& address) { + AsyncSocket* sock = + Thread::Current()->socketserver()->CreateAsyncSocket(address.family(), + SOCK_STREAM); + if (!sock) { + return SOCKET_ERROR; + } + listener_.reset(sock); + listener_->SignalReadEvent.connect(this, &HttpListenServer::OnReadEvent); + if ((listener_->Bind(address) != SOCKET_ERROR) && + (listener_->Listen(5) != SOCKET_ERROR)) + return 0; + return listener_->GetError(); +} + +bool HttpListenServer::GetAddress(SocketAddress* address) const { + if (!listener_) { + return false; + } + *address = listener_->GetLocalAddress(); + return !address->IsNil(); +} + +void HttpListenServer::StopListening() { + if (listener_) { + listener_->Close(); + } +} + +void HttpListenServer::OnReadEvent(AsyncSocket* socket) { + ASSERT(socket == listener_.get()); + ASSERT(listener_); + AsyncSocket* incoming = listener_->Accept(NULL); + if (incoming) { + StreamInterface* stream = new SocketStream(incoming); + //stream = new LoggingAdapter(stream, LS_VERBOSE, "HttpServer", false); + HandleConnection(stream); + } +} + +void HttpListenServer::OnConnectionClosed(HttpServer* server, + int connection_id, + StreamInterface* stream) { + Thread::Current()->Dispose(stream); +} + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/httpserver.h b/talk/base/httpserver.h new file mode 100644 index 000000000..67061ee91 --- /dev/null +++ b/talk/base/httpserver.h @@ -0,0 +1,154 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_HTTPSERVER_H__ +#define TALK_BASE_HTTPSERVER_H__ + +#include +#include "talk/base/httpbase.h" + +namespace talk_base { + +class AsyncSocket; +class HttpServer; +class SocketAddress; + +////////////////////////////////////////////////////////////////////// +// HttpServer +////////////////////////////////////////////////////////////////////// + +const int HTTP_INVALID_CONNECTION_ID = 0; + +struct HttpServerTransaction : public HttpTransaction { +public: + HttpServerTransaction(int id) : connection_id_(id) { } + int connection_id() const { return connection_id_; } + +private: + int connection_id_; +}; + +class HttpServer { +public: + HttpServer(); + virtual ~HttpServer(); + + int HandleConnection(StreamInterface* stream); + // Due to sigslot issues, we can't destroy some streams at an arbitrary time. + sigslot::signal3 SignalConnectionClosed; + + // This signal occurs when the HTTP request headers have been received, but + // before the request body is written to the request document. By default, + // the request document is a MemoryStream. By handling this signal, the + // document can be overridden, in which case the third signal argument should + // be set to true. In the case where the request body should be ignored, + // the document can be set to NULL. Note that the transaction object is still + // owened by the HttpServer at this point. + sigslot::signal3 + SignalHttpRequestHeader; + + // An HTTP request has been made, and is available in the transaction object. + // Populate the transaction's response, and then return the object via the + // Respond method. Note that during this time, ownership of the transaction + // object is transferred, so it may be passed between threads, although + // respond must be called on the server's active thread. + sigslot::signal2 SignalHttpRequest; + void Respond(HttpServerTransaction* transaction); + + // If you want to know when a request completes, listen to this event. + sigslot::signal3 + SignalHttpRequestComplete; + + // Stop processing the connection indicated by connection_id. + // Unless force is true, the server will complete sending a response that is + // in progress. + void Close(int connection_id, bool force); + void CloseAll(bool force); + + // After calling CloseAll, this event is signalled to indicate that all + // outstanding connections have closed. + sigslot::signal1 SignalCloseAllComplete; + +private: + class Connection : private IHttpNotify { + public: + Connection(int connection_id, HttpServer* server); + virtual ~Connection(); + + void BeginProcess(StreamInterface* stream); + StreamInterface* EndProcess(); + + void Respond(HttpServerTransaction* transaction); + void InitiateClose(bool force); + + // IHttpNotify Interface + virtual HttpError onHttpHeaderComplete(bool chunked, size_t& data_size); + virtual void onHttpComplete(HttpMode mode, HttpError err); + virtual void onHttpClosed(HttpError err); + + int connection_id_; + HttpServer* server_; + HttpBase base_; + HttpServerTransaction* current_; + bool signalling_, close_; + }; + + Connection* Find(int connection_id); + void Remove(int connection_id); + + friend class Connection; + typedef std::map ConnectionMap; + + ConnectionMap connections_; + int next_connection_id_; + bool closing_; +}; + +////////////////////////////////////////////////////////////////////// + +class HttpListenServer : public HttpServer, public sigslot::has_slots<> { +public: + HttpListenServer(); + virtual ~HttpListenServer(); + + int Listen(const SocketAddress& address); + bool GetAddress(SocketAddress* address) const; + void StopListening(); + +private: + void OnReadEvent(AsyncSocket* socket); + void OnConnectionClosed(HttpServer* server, int connection_id, + StreamInterface* stream); + + scoped_ptr listener_; +}; + +////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_HTTPSERVER_H__ diff --git a/talk/base/httpserver_unittest.cc b/talk/base/httpserver_unittest.cc new file mode 100644 index 000000000..d0e0760af --- /dev/null +++ b/talk/base/httpserver_unittest.cc @@ -0,0 +1,130 @@ +// Copyright 2007 Google Inc. +// All Rights Reserved. + + +#include "talk/base/gunit.h" +#include "talk/base/httpserver.h" +#include "talk/base/testutils.h" + +using namespace testing; + +namespace talk_base { + +namespace { + const char* const kRequest = + "GET /index.html HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n"; + + const char* const kResponse = + "HTTP/1.1 200\r\n" + "Connection: Close\r\n" + "Content-Length: 0\r\n" + "\r\n"; + + struct HttpServerMonitor : public sigslot::has_slots<> { + HttpServerTransaction* transaction; + bool server_closed, connection_closed; + + HttpServerMonitor(HttpServer* server) + : transaction(NULL), server_closed(false), connection_closed(false) { + server->SignalCloseAllComplete.connect(this, + &HttpServerMonitor::OnClosed); + server->SignalHttpRequest.connect(this, &HttpServerMonitor::OnRequest); + server->SignalHttpRequestComplete.connect(this, + &HttpServerMonitor::OnRequestComplete); + server->SignalConnectionClosed.connect(this, + &HttpServerMonitor::OnConnectionClosed); + } + void OnRequest(HttpServer*, HttpServerTransaction* t) { + ASSERT_FALSE(transaction); + transaction = t; + transaction->response.set_success(); + transaction->response.setHeader(HH_CONNECTION, "Close"); + } + void OnRequestComplete(HttpServer*, HttpServerTransaction* t, int) { + ASSERT_EQ(transaction, t); + transaction = NULL; + } + void OnClosed(HttpServer*) { + server_closed = true; + } + void OnConnectionClosed(HttpServer*, int, StreamInterface* stream) { + connection_closed = true; + delete stream; + } + }; + + void CreateClientConnection(HttpServer& server, + HttpServerMonitor& monitor, + bool send_request) { + StreamSource* client = new StreamSource; + client->SetState(SS_OPEN); + server.HandleConnection(client); + EXPECT_FALSE(monitor.server_closed); + EXPECT_FALSE(monitor.transaction); + + if (send_request) { + // Simulate a request + client->QueueString(kRequest); + EXPECT_FALSE(monitor.server_closed); + } + } +} // anonymous namespace + +TEST(HttpServer, DoesNotSignalCloseUnlessCloseAllIsCalled) { + HttpServer server; + HttpServerMonitor monitor(&server); + // Add an active client connection + CreateClientConnection(server, monitor, true); + // Simulate a response + ASSERT_TRUE(NULL != monitor.transaction); + server.Respond(monitor.transaction); + EXPECT_FALSE(monitor.transaction); + // Connection has closed, but no server close signal + EXPECT_FALSE(monitor.server_closed); + EXPECT_TRUE(monitor.connection_closed); +} + +TEST(HttpServer, SignalsCloseWhenNoConnectionsAreActive) { + HttpServer server; + HttpServerMonitor monitor(&server); + // Add an idle client connection + CreateClientConnection(server, monitor, false); + // Perform graceful close + server.CloseAll(false); + // Connections have all closed + EXPECT_TRUE(monitor.server_closed); + EXPECT_TRUE(monitor.connection_closed); +} + +TEST(HttpServer, SignalsCloseAfterGracefulCloseAll) { + HttpServer server; + HttpServerMonitor monitor(&server); + // Add an active client connection + CreateClientConnection(server, monitor, true); + // Initiate a graceful close + server.CloseAll(false); + EXPECT_FALSE(monitor.server_closed); + // Simulate a response + ASSERT_TRUE(NULL != monitor.transaction); + server.Respond(monitor.transaction); + EXPECT_FALSE(monitor.transaction); + // Connections have all closed + EXPECT_TRUE(monitor.server_closed); + EXPECT_TRUE(monitor.connection_closed); +} + +TEST(HttpServer, SignalsCloseAfterForcedCloseAll) { + HttpServer server; + HttpServerMonitor monitor(&server); + // Add an active client connection + CreateClientConnection(server, monitor, true); + // Initiate a forceful close + server.CloseAll(true); + // Connections have all closed + EXPECT_TRUE(monitor.server_closed); + EXPECT_TRUE(monitor.connection_closed); +} + +} // namespace talk_base diff --git a/talk/base/ifaddrs-android.cc b/talk/base/ifaddrs-android.cc new file mode 100644 index 000000000..78268406e --- /dev/null +++ b/talk/base/ifaddrs-android.cc @@ -0,0 +1,234 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#if defined(ANDROID) +#include "talk/base/ifaddrs-android.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct netlinkrequest { + nlmsghdr header; + ifaddrmsg msg; +}; + +namespace { +const int kMaxReadSize = 4096; +}; + +int set_ifname(struct ifaddrs* ifaddr, int interface) { + char buf[IFNAMSIZ] = {0}; + char* name = if_indextoname(interface, buf); + if (name == NULL) { + return -1; + } + ifaddr->ifa_name = new char[strlen(name) + 1]; + strncpy(ifaddr->ifa_name, name, strlen(name) + 1); + return 0; +} + +int set_flags(struct ifaddrs* ifaddr) { + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd == -1) { + return -1; + } + ifreq ifr; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ifaddr->ifa_name, IFNAMSIZ - 1); + int rc = ioctl(fd, SIOCGIFFLAGS, &ifr); + close(fd); + if (rc == -1) { + return -1; + } + ifaddr->ifa_flags = ifr.ifr_flags; + return 0; +} + +int set_addresses(struct ifaddrs* ifaddr, ifaddrmsg* msg, void* data, + size_t len) { + if (msg->ifa_family == AF_INET) { + sockaddr_in* sa = new sockaddr_in; + sa->sin_family = AF_INET; + memcpy(&sa->sin_addr, data, len); + ifaddr->ifa_addr = reinterpret_cast(sa); + } else if (msg->ifa_family == AF_INET6) { + sockaddr_in6* sa = new sockaddr_in6; + sa->sin6_family = AF_INET6; + sa->sin6_scope_id = msg->ifa_index; + memcpy(&sa->sin6_addr, data, len); + ifaddr->ifa_addr = reinterpret_cast(sa); + } else { + return -1; + } + return 0; +} + +int make_prefixes(struct ifaddrs* ifaddr, int family, int prefixlen) { + char* prefix = NULL; + if (family == AF_INET) { + sockaddr_in* mask = new sockaddr_in; + mask->sin_family = AF_INET; + memset(&mask->sin_addr, 0, sizeof(in_addr)); + ifaddr->ifa_netmask = reinterpret_cast(mask); + if (prefixlen > 32) { + prefixlen = 32; + } + prefix = reinterpret_cast(&mask->sin_addr); + } else if (family == AF_INET6) { + sockaddr_in6* mask = new sockaddr_in6; + mask->sin6_family = AF_INET6; + memset(&mask->sin6_addr, 0, sizeof(in6_addr)); + ifaddr->ifa_netmask = reinterpret_cast(mask); + if (prefixlen > 128) { + prefixlen = 128; + } + prefix = reinterpret_cast(&mask->sin6_addr); + } else { + return -1; + } + for (int i = 0; i < (prefixlen / 8); i++) { + *prefix++ = 0xFF; + } + char remainder = 0xff; + remainder <<= (8 - prefixlen % 8); + *prefix = remainder; + return 0; +} + +int populate_ifaddrs(struct ifaddrs* ifaddr, ifaddrmsg* msg, void* bytes, + size_t len) { + if (set_ifname(ifaddr, msg->ifa_index) != 0) { + return -1; + } + if (set_flags(ifaddr) != 0) { + return -1; + } + if (set_addresses(ifaddr, msg, bytes, len) != 0) { + return -1; + } + if (make_prefixes(ifaddr, msg->ifa_family, msg->ifa_prefixlen) != 0) { + return -1; + } + return 0; +} + +int getifaddrs(struct ifaddrs** result) { + int fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (fd < 0) { + return -1; + } + + netlinkrequest ifaddr_request; + memset(&ifaddr_request, 0, sizeof(ifaddr_request)); + ifaddr_request.header.nlmsg_flags = NLM_F_ROOT | NLM_F_REQUEST; + ifaddr_request.header.nlmsg_type = RTM_GETADDR; + ifaddr_request.header.nlmsg_len = NLMSG_LENGTH(sizeof(ifaddrmsg)); + + ssize_t count = send(fd, &ifaddr_request, ifaddr_request.header.nlmsg_len, 0); + if (static_cast(count) != ifaddr_request.header.nlmsg_len) { + close(fd); + return -1; + } + struct ifaddrs* start = NULL; + struct ifaddrs* current = NULL; + char buf[kMaxReadSize]; + ssize_t amount_read = recv(fd, &buf, kMaxReadSize, 0); + while (amount_read > 0) { + nlmsghdr* header = reinterpret_cast(&buf[0]); + size_t header_size = static_cast(amount_read); + for ( ; NLMSG_OK(header, header_size); + header = NLMSG_NEXT(header, header_size)) { + switch (header->nlmsg_type) { + case NLMSG_DONE: + // Success. Return. + *result = start; + close(fd); + return 0; + case NLMSG_ERROR: + close(fd); + freeifaddrs(start); + return -1; + case RTM_NEWADDR: { + ifaddrmsg* address_msg = + reinterpret_cast(NLMSG_DATA(header)); + rtattr* rta = IFA_RTA(address_msg); + ssize_t payload_len = IFA_PAYLOAD(header); + while (RTA_OK(rta, payload_len)) { + if (rta->rta_type == IFA_ADDRESS) { + int family = address_msg->ifa_family; + if (family == AF_INET || family == AF_INET6) { + ifaddrs* newest = new ifaddrs; + memset(newest, 0, sizeof(ifaddrs)); + if (current) { + current->ifa_next = newest; + } else { + start = newest; + } + if (populate_ifaddrs(newest, address_msg, RTA_DATA(rta), + RTA_PAYLOAD(rta)) != 0) { + freeifaddrs(start); + *result = NULL; + return -1; + } + current = newest; + } + } + rta = RTA_NEXT(rta, payload_len); + } + break; + } + } + } + amount_read = recv(fd, &buf, kMaxReadSize, 0); + } + close(fd); + freeifaddrs(start); + return -1; +} + +void freeifaddrs(struct ifaddrs* addrs) { + struct ifaddrs* last = NULL; + struct ifaddrs* cursor = addrs; + while (cursor) { + delete[] cursor->ifa_name; + delete cursor->ifa_addr; + delete cursor->ifa_netmask; + last = cursor; + cursor = cursor->ifa_next; + delete last; + } +} +#endif // defined(ANDROID) diff --git a/talk/base/ifaddrs-android.h b/talk/base/ifaddrs-android.h new file mode 100644 index 000000000..e7d81e813 --- /dev/null +++ b/talk/base/ifaddrs-android.h @@ -0,0 +1,50 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#ifndef TALK_BASE_IFADDRS_ANDROID_H_ +#define TALK_BASE_IFADDRS_ANDROID_H_ + +#include +#include +// Implementation of getifaddrs for Android. +// Fills out a list of ifaddr structs (see below) which contain information +// about every network interface available on the host. +// See 'man getifaddrs' on Linux or OS X (nb: it is not a POSIX function). +struct ifaddrs { + struct ifaddrs* ifa_next; + char* ifa_name; + unsigned int ifa_flags; + struct sockaddr* ifa_addr; + struct sockaddr* ifa_netmask; + // Real ifaddrs has broadcast, point to point and data members. + // We don't need them (yet?). +}; + +int getifaddrs(struct ifaddrs** result); +void freeifaddrs(struct ifaddrs* addrs); + +#endif // TALK_BASE_IFADDRS_ANDROID_H_ diff --git a/talk/base/ipaddress.cc b/talk/base/ipaddress.cc new file mode 100644 index 000000000..46725908d --- /dev/null +++ b/talk/base/ipaddress.cc @@ -0,0 +1,466 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifdef POSIX +#include +#include +#include +#ifdef OPENBSD +#include +#endif +#include +#include +#include +#include +#endif + +#include + +#include "talk/base/ipaddress.h" +#include "talk/base/byteorder.h" +#include "talk/base/nethelpers.h" +#include "talk/base/logging.h" +#include "talk/base/win32.h" + +namespace talk_base { + +// Prefixes used for categorizing IPv6 addresses. +static const in6_addr kULAPrefix = {{{0xfc, 0}}}; +static const in6_addr kV4MappedPrefix = {{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0xFF, 0xFF, 0}}}; +static const in6_addr k6To4Prefix = {{{0x20, 0x02, 0}}}; +static const in6_addr kTeredoPrefix = {{{0x20, 0x01, 0x00, 0x00}}}; +static const in6_addr kV4CompatibilityPrefix = {{{0}}}; +static const in6_addr kSiteLocalPrefix = {{{0xfe, 0xc0, 0}}}; +static const in6_addr k6BonePrefix = {{{0x3f, 0xfe, 0}}}; + +bool IPAddress::strip_sensitive_ = false; + +static bool IsPrivateV4(uint32 ip); +static in_addr ExtractMappedAddress(const in6_addr& addr); + +uint32 IPAddress::v4AddressAsHostOrderInteger() const { + if (family_ == AF_INET) { + return NetworkToHost32(u_.ip4.s_addr); + } else { + return 0; + } +} + +size_t IPAddress::Size() const { + switch (family_) { + case AF_INET: + return sizeof(in_addr); + case AF_INET6: + return sizeof(in6_addr); + } + return 0; +} + + +bool IPAddress::operator==(const IPAddress &other) const { + if (family_ != other.family_) { + return false; + } + if (family_ == AF_INET) { + return memcmp(&u_.ip4, &other.u_.ip4, sizeof(u_.ip4)) == 0; + } + if (family_ == AF_INET6) { + return memcmp(&u_.ip6, &other.u_.ip6, sizeof(u_.ip6)) == 0; + } + return family_ == AF_UNSPEC; +} + +bool IPAddress::operator!=(const IPAddress &other) const { + return !((*this) == other); +} + +bool IPAddress::operator >(const IPAddress &other) const { + return (*this) != other && !((*this) < other); +} + +bool IPAddress::operator <(const IPAddress &other) const { + // IPv4 is 'less than' IPv6 + if (family_ != other.family_) { + if (family_ == AF_UNSPEC) { + return true; + } + if (family_ == AF_INET && other.family_ == AF_INET6) { + return true; + } + return false; + } + // Comparing addresses of the same family. + switch (family_) { + case AF_INET: { + return NetworkToHost32(u_.ip4.s_addr) < + NetworkToHost32(other.u_.ip4.s_addr); + } + case AF_INET6: { + return memcmp(&u_.ip6.s6_addr, &other.u_.ip6.s6_addr, 16) < 0; + } + } + // Catches AF_UNSPEC and invalid addresses. + return false; +} + +std::ostream& operator<<(std::ostream& os, const IPAddress& ip) { + os << ip.ToString(); + return os; +} + +in6_addr IPAddress::ipv6_address() const { + return u_.ip6; +} + +in_addr IPAddress::ipv4_address() const { + return u_.ip4; +} + +std::string IPAddress::ToString() const { + if (family_ != AF_INET && family_ != AF_INET6) { + return std::string(); + } + char buf[INET6_ADDRSTRLEN] = {0}; + const void* src = &u_.ip4; + if (family_ == AF_INET6) { + src = &u_.ip6; + } + if (!talk_base::inet_ntop(family_, src, buf, sizeof(buf))) { + return std::string(); + } + return std::string(buf); +} + +std::string IPAddress::ToSensitiveString() const { + if (!strip_sensitive_) + return ToString(); + + switch (family_) { + case AF_INET: { + std::string address = ToString(); + size_t find_pos = address.rfind('.'); + if (find_pos == std::string::npos) + return std::string(); + address.resize(find_pos); + address += ".x"; + return address; + } + case AF_INET6: { + // TODO(grunell): Return a string of format 1:2:3:x:x:x:x:x or such + // instead of zeroing out. + return TruncateIP(*this, 128 - 80).ToString(); + } + } + return std::string(); +} + +IPAddress IPAddress::Normalized() const { + if (family_ != AF_INET6) { + return *this; + } + if (!IPIsV4Mapped(*this)) { + return *this; + } + in_addr addr = ExtractMappedAddress(u_.ip6); + return IPAddress(addr); +} + +IPAddress IPAddress::AsIPv6Address() const { + if (family_ != AF_INET) { + return *this; + } + in6_addr v6addr = kV4MappedPrefix; + ::memcpy(&v6addr.s6_addr[12], &u_.ip4.s_addr, sizeof(u_.ip4.s_addr)); + return IPAddress(v6addr); +} + +void IPAddress::set_strip_sensitive(bool enable) { + strip_sensitive_ = enable; +} + + +bool IsPrivateV4(uint32 ip_in_host_order) { + return ((ip_in_host_order >> 24) == 127) || + ((ip_in_host_order >> 24) == 10) || + ((ip_in_host_order >> 20) == ((172 << 4) | 1)) || + ((ip_in_host_order >> 16) == ((192 << 8) | 168)) || + ((ip_in_host_order >> 16) == ((169 << 8) | 254)); +} + +in_addr ExtractMappedAddress(const in6_addr& in6) { + in_addr ipv4; + ::memcpy(&ipv4.s_addr, &in6.s6_addr[12], sizeof(ipv4.s_addr)); + return ipv4; +} + +bool IPFromAddrInfo(struct addrinfo* info, IPAddress* out) { + if (!info || !info->ai_addr) { + return false; + } + if (info->ai_addr->sa_family == AF_INET) { + sockaddr_in* addr = reinterpret_cast(info->ai_addr); + *out = IPAddress(addr->sin_addr); + return true; + } else if (info->ai_addr->sa_family == AF_INET6) { + sockaddr_in6* addr = reinterpret_cast(info->ai_addr); + *out = IPAddress(addr->sin6_addr); + return true; + } + return false; +} + +bool IPFromString(const std::string& str, IPAddress* out) { + if (!out) { + return false; + } + in_addr addr; + if (talk_base::inet_pton(AF_INET, str.c_str(), &addr) == 0) { + in6_addr addr6; + if (talk_base::inet_pton(AF_INET6, str.c_str(), &addr6) == 0) { + *out = IPAddress(); + return false; + } + *out = IPAddress(addr6); + } else { + *out = IPAddress(addr); + } + return true; +} + +bool IPIsAny(const IPAddress& ip) { + switch (ip.family()) { + case AF_INET: + return ip == IPAddress(INADDR_ANY); + case AF_INET6: + return ip == IPAddress(in6addr_any); + case AF_UNSPEC: + return false; + } + return false; +} + +bool IPIsLoopback(const IPAddress& ip) { + switch (ip.family()) { + case AF_INET: { + return ip == IPAddress(INADDR_LOOPBACK); + } + case AF_INET6: { + return ip == IPAddress(in6addr_loopback); + } + } + return false; +} + +bool IPIsPrivate(const IPAddress& ip) { + switch (ip.family()) { + case AF_INET: { + return IsPrivateV4(ip.v4AddressAsHostOrderInteger()); + } + case AF_INET6: { + in6_addr v6 = ip.ipv6_address(); + return (v6.s6_addr[0] == 0xFE && v6.s6_addr[1] == 0x80) || + IPIsLoopback(ip); + } + } + return false; +} + +bool IPIsUnspec(const IPAddress& ip) { + return ip.family() == AF_UNSPEC; +} + +size_t HashIP(const IPAddress& ip) { + switch (ip.family()) { + case AF_INET: { + return ip.ipv4_address().s_addr; + } + case AF_INET6: { + in6_addr v6addr = ip.ipv6_address(); + const uint32* v6_as_ints = + reinterpret_cast(&v6addr.s6_addr); + return v6_as_ints[0] ^ v6_as_ints[1] ^ v6_as_ints[2] ^ v6_as_ints[3]; + } + } + return 0; +} + +IPAddress TruncateIP(const IPAddress& ip, int length) { + if (length < 0) { + return IPAddress(); + } + if (ip.family() == AF_INET) { + if (length > 31) { + return ip; + } + if (length == 0) { + return IPAddress(INADDR_ANY); + } + int mask = (0xFFFFFFFF << (32 - length)); + uint32 host_order_ip = NetworkToHost32(ip.ipv4_address().s_addr); + in_addr masked; + masked.s_addr = HostToNetwork32(host_order_ip & mask); + return IPAddress(masked); + } else if (ip.family() == AF_INET6) { + if (length > 127) { + return ip; + } + if (length == 0) { + return IPAddress(in6addr_any); + } + in6_addr v6addr = ip.ipv6_address(); + int position = length / 32; + int inner_length = 32 - (length - (position * 32)); + // Note: 64bit mask constant needed to allow possible 32-bit left shift. + uint32 inner_mask = 0xFFFFFFFFLL << inner_length; + uint32* v6_as_ints = + reinterpret_cast(&v6addr.s6_addr); + for (int i = 0; i < 4; ++i) { + if (i == position) { + uint32 host_order_inner = NetworkToHost32(v6_as_ints[i]); + v6_as_ints[i] = HostToNetwork32(host_order_inner & inner_mask); + } else if (i > position) { + v6_as_ints[i] = 0; + } + } + return IPAddress(v6addr); + } + return IPAddress(); +} + +int CountIPMaskBits(IPAddress mask) { + uint32 word_to_count = 0; + int bits = 0; + switch (mask.family()) { + case AF_INET: { + word_to_count = NetworkToHost32(mask.ipv4_address().s_addr); + break; + } + case AF_INET6: { + in6_addr v6addr = mask.ipv6_address(); + const uint32* v6_as_ints = + reinterpret_cast(&v6addr.s6_addr); + int i = 0; + for (; i < 4; ++i) { + if (v6_as_ints[i] != 0xFFFFFFFF) { + break; + } + } + if (i < 4) { + word_to_count = NetworkToHost32(v6_as_ints[i]); + } + bits = (i * 32); + break; + } + default: { + return 0; + } + } + if (word_to_count == 0) { + return bits; + } + + // Public domain bit-twiddling hack from: + // http://graphics.stanford.edu/~seander/bithacks.html + // Counts the trailing 0s in the word. + unsigned int zeroes = 32; + word_to_count &= -static_cast(word_to_count); + if (word_to_count) zeroes--; + if (word_to_count & 0x0000FFFF) zeroes -= 16; + if (word_to_count & 0x00FF00FF) zeroes -= 8; + if (word_to_count & 0x0F0F0F0F) zeroes -= 4; + if (word_to_count & 0x33333333) zeroes -= 2; + if (word_to_count & 0x55555555) zeroes -= 1; + + return bits + (32 - zeroes); +} + +bool IPIsHelper(const IPAddress& ip, const in6_addr& tomatch, int length) { + // Helper method for checking IP prefix matches (but only on whole byte + // lengths). Length is in bits. + in6_addr addr = ip.ipv6_address(); + return ::memcmp(&addr, &tomatch, (length >> 3)) == 0; +} + +bool IPIs6Bone(const IPAddress& ip) { + return IPIsHelper(ip, k6BonePrefix, 16); +} + +bool IPIs6To4(const IPAddress& ip) { + return IPIsHelper(ip, k6To4Prefix, 16); +} + +bool IPIsSiteLocal(const IPAddress& ip) { + // Can't use the helper because the prefix is 10 bits. + in6_addr addr = ip.ipv6_address(); + return addr.s6_addr[0] == 0xFE && (addr.s6_addr[1] & 0xC0) == 0xC0; +} + +bool IPIsULA(const IPAddress& ip) { + // Can't use the helper because the prefix is 7 bits. + in6_addr addr = ip.ipv6_address(); + return (addr.s6_addr[0] & 0xFE) == 0xFC; +} + +bool IPIsTeredo(const IPAddress& ip) { + return IPIsHelper(ip, kTeredoPrefix, 32); +} + +bool IPIsV4Compatibility(const IPAddress& ip) { + return IPIsHelper(ip, kV4CompatibilityPrefix, 96); +} + +bool IPIsV4Mapped(const IPAddress& ip) { + return IPIsHelper(ip, kV4MappedPrefix, 96); +} + +int IPAddressPrecedence(const IPAddress& ip) { + // Precedence values from RFC 3484-bis. Prefers native v4 over 6to4/Teredo. + if (ip.family() == AF_INET) { + return 30; + } else if (ip.family() == AF_INET6) { + if (IPIsLoopback(ip)) { + return 60; + } else if (IPIsULA(ip)) { + return 50; + } else if (IPIsV4Mapped(ip)) { + return 30; + } else if (IPIs6To4(ip)) { + return 20; + } else if (IPIsTeredo(ip)) { + return 10; + } else if (IPIsV4Compatibility(ip) || IPIsSiteLocal(ip) || IPIs6Bone(ip)) { + return 1; + } else { + // A 'normal' IPv6 address. + return 40; + } + } + return 0; +} + +} // Namespace talk base diff --git a/talk/base/ipaddress.h b/talk/base/ipaddress.h new file mode 100644 index 000000000..b60de8abc --- /dev/null +++ b/talk/base/ipaddress.h @@ -0,0 +1,158 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_BASE_IPADDRESS_H_ +#define TALK_BASE_IPADDRESS_H_ + +#ifdef POSIX +#include +#include +#include +#include +#endif +#ifdef WIN32 +#include +#include +#endif +#include +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/byteorder.h" +#ifdef WIN32 +#include "talk/base/win32.h" +#endif + +namespace talk_base { + +// Version-agnostic IP address class, wraps a union of in_addr and in6_addr. +class IPAddress { + public: + IPAddress() : family_(AF_UNSPEC) { + ::memset(&u_, 0, sizeof(u_)); + } + + explicit IPAddress(const in_addr &ip4) : family_(AF_INET) { + memset(&u_, 0, sizeof(u_)); + u_.ip4 = ip4; + } + + explicit IPAddress(const in6_addr &ip6) : family_(AF_INET6) { + u_.ip6 = ip6; + } + + explicit IPAddress(uint32 ip_in_host_byte_order) : family_(AF_INET) { + memset(&u_, 0, sizeof(u_)); + u_.ip4.s_addr = HostToNetwork32(ip_in_host_byte_order); + } + + IPAddress(const IPAddress &other) : family_(other.family_) { + ::memcpy(&u_, &other.u_, sizeof(u_)); + } + + ~IPAddress() {} + + const IPAddress & operator=(const IPAddress &other) { + family_ = other.family_; + ::memcpy(&u_, &other.u_, sizeof(u_)); + return *this; + } + + bool operator==(const IPAddress &other) const; + bool operator!=(const IPAddress &other) const; + bool operator <(const IPAddress &other) const; + bool operator >(const IPAddress &other) const; + friend std::ostream& operator<<(std::ostream& os, const IPAddress& addr); + + int family() const { return family_; } + in_addr ipv4_address() const; + in6_addr ipv6_address() const; + + // Returns the number of bytes needed to store the raw address. + size_t Size() const; + + // Wraps inet_ntop. + std::string ToString() const; + + // Same as ToString but anonymizes it by hiding the last part. + std::string ToSensitiveString() const; + + // Returns an unmapped address from a possibly-mapped address. + // Returns the same address if this isn't a mapped address. + IPAddress Normalized() const; + + // Returns this address as an IPv6 address. + // Maps v4 addresses (as ::ffff:a.b.c.d), returns v6 addresses unchanged. + IPAddress AsIPv6Address() const; + + // For socketaddress' benefit. Returns the IP in host byte order. + uint32 v4AddressAsHostOrderInteger() const; + + static void set_strip_sensitive(bool enable); + + private: + int family_; + union { + in_addr ip4; + in6_addr ip6; + } u_; + + static bool strip_sensitive_; +}; + +bool IPFromAddrInfo(struct addrinfo* info, IPAddress* out); +bool IPFromString(const std::string& str, IPAddress* out); +bool IPIsAny(const IPAddress& ip); +bool IPIsLoopback(const IPAddress& ip); +bool IPIsPrivate(const IPAddress& ip); +bool IPIsUnspec(const IPAddress& ip); +size_t HashIP(const IPAddress& ip); + +// These are only really applicable for IPv6 addresses. +bool IPIs6Bone(const IPAddress& ip); +bool IPIs6To4(const IPAddress& ip); +bool IPIsSiteLocal(const IPAddress& ip); +bool IPIsTeredo(const IPAddress& ip); +bool IPIsULA(const IPAddress& ip); +bool IPIsV4Compatibility(const IPAddress& ip); +bool IPIsV4Mapped(const IPAddress& ip); + +// Returns the precedence value for this IP as given in RFC3484. +int IPAddressPrecedence(const IPAddress& ip); + +// Returns 'ip' truncated to be 'length' bits long. +IPAddress TruncateIP(const IPAddress& ip, int length); + +// Returns the number of contiguously set bits, counting from the MSB in network +// byte order, in this IPAddress. Bits after the first 0 encountered are not +// counted. +int CountIPMaskBits(IPAddress mask); + +} // namespace talk_base + +#endif // TALK_BASE_IPADDRESS_H_ diff --git a/talk/base/ipaddress_unittest.cc b/talk/base/ipaddress_unittest.cc new file mode 100644 index 000000000..424b557cd --- /dev/null +++ b/talk/base/ipaddress_unittest.cc @@ -0,0 +1,888 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/gunit.h" +#include "talk/base/ipaddress.h" + +namespace talk_base { + +static const unsigned int kIPv4AddrSize = 4; +static const unsigned int kIPv6AddrSize = 16; +static const unsigned int kIPv4RFC1918Addr = 0xC0A80701; +static const unsigned int kIPv4PublicAddr = 0x01020304; +static const in6_addr kIPv6LinkLocalAddr = {{{0xfe, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0xbe, 0x30, 0x5b, 0xff, + 0xfe, 0xe5, 0x00, 0xc3}}}; +static const in6_addr kIPv6PublicAddr = {{{0x24, 0x01, 0xfa, 0x00, + 0x00, 0x04, 0x10, 0x00, + 0xbe, 0x30, 0x5b, 0xff, + 0xfe, 0xe5, 0x00, 0xc3}}}; +static const in6_addr kIPv6CompatAddr = {{{0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0xfe, 0xe5, 0x00, 0xc3}}}; +static const in6_addr kIPv4MappedAnyAddr = {{{0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00}}}; +static const in6_addr kIPv4MappedLoopbackAddr = {{{0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, + 0x7f, 0x00, 0x00, 0x01}}}; +static const in6_addr kIPv4MappedRFC1918Addr = {{{0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, + 0xc0, 0xa8, 0x07, 0x01}}}; +static const in6_addr kIPv4MappedPublicAddr = {{{0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, + 0x01, 0x02, 0x03, 0x04}}}; +static const in6_addr kIPv6AllNodes = {{{0xff, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01}}}; + +static const std::string kIPv4AnyAddrString = "0.0.0.0"; +static const std::string kIPv4LoopbackAddrString = "127.0.0.1"; +static const std::string kIPv4RFC1918AddrString = "192.168.7.1"; +static const std::string kIPv4PublicAddrString = "1.2.3.4"; +static const std::string kIPv4PublicAddrAnonymizedString = "1.2.3.x"; +static const std::string kIPv6AnyAddrString = "::"; +static const std::string kIPv6LoopbackAddrString = "::1"; +static const std::string kIPv6LinkLocalAddrString = "fe80::be30:5bff:fee5:c3"; +static const std::string kIPv6PublicAddrString = + "2401:fa00:4:1000:be30:5bff:fee5:c3"; +static const std::string kIPv6PublicAddrAnonymizedString = "2401:fa00:4::"; +static const std::string kIPv4MappedAnyAddrString = "::ffff:0:0"; +static const std::string kIPv4MappedRFC1918AddrString = "::ffff:c0a8:701"; +static const std::string kIPv4MappedLoopbackAddrString = "::ffff:7f00:1"; +static const std::string kIPv4MappedPublicAddrString = "::ffff:102:0304"; +static const std::string kIPv4MappedV4StyleAddrString = "::ffff:192.168.7.1"; + +static const std::string kIPv4BrokenString1 = "192.168.7."; +static const std::string kIPv4BrokenString2 = "192.168.7.1.1"; +static const std::string kIPv4BrokenString3 = "192.168.7.1:80"; +static const std::string kIPv4BrokenString4 = "192.168.7.ONE"; +static const std::string kIPv4BrokenString5 = "-192.168.7.1"; +static const std::string kIPv4BrokenString6 = "256.168.7.1"; +static const std::string kIPv6BrokenString1 = "2401:fa00:4:1000:be30"; +static const std::string kIPv6BrokenString2 = + "2401:fa00:4:1000:be30:5bff:fee5:c3:1"; +static const std::string kIPv6BrokenString3 = + "[2401:fa00:4:1000:be30:5bff:fee5:c3]:1"; +static const std::string kIPv6BrokenString4 = + "2401::4::be30"; +static const std::string kIPv6BrokenString5 = + "2401:::4:fee5:be30"; +static const std::string kIPv6BrokenString6 = + "2401f:fa00:4:1000:be30:5bff:fee5:c3"; +static const std::string kIPv6BrokenString7 = + "2401:ga00:4:1000:be30:5bff:fee5:c3"; +static const std::string kIPv6BrokenString8 = + "2401:fa000:4:1000:be30:5bff:fee5:c3"; +static const std::string kIPv6BrokenString9 = + "2401:fal0:4:1000:be30:5bff:fee5:c3"; +static const std::string kIPv6BrokenString10 = + "::ffff:192.168.7."; +static const std::string kIPv6BrokenString11 = + "::ffff:192.168.7.1.1.1"; +static const std::string kIPv6BrokenString12 = + "::fffe:192.168.7.1"; +static const std::string kIPv6BrokenString13 = + "::ffff:192.168.7.ff"; +static const std::string kIPv6BrokenString14 = + "0x2401:fa00:4:1000:be30:5bff:fee5:c3"; + +bool AreEqual(const IPAddress& addr, + const IPAddress& addr2) { + if ((IPIsAny(addr) != IPIsAny(addr2)) || + (IPIsLoopback(addr) != IPIsLoopback(addr2)) || + (IPIsPrivate(addr) != IPIsPrivate(addr2)) || + (HashIP(addr) != HashIP(addr2)) || + (addr.Size() != addr2.Size()) || + (addr.family() != addr2.family()) || + (addr.ToString() != addr2.ToString())) { + return false; + } + in_addr v4addr, v4addr2; + v4addr = addr.ipv4_address(); + v4addr2 = addr2.ipv4_address(); + if (0 != memcmp(&v4addr, &v4addr2, sizeof(v4addr))) { + return false; + } + in6_addr v6addr, v6addr2; + v6addr = addr.ipv6_address(); + v6addr2 = addr2.ipv6_address(); + if (0 != memcmp(&v6addr, &v6addr2, sizeof(v6addr))) { + return false; + } + return true; +} + +bool BrokenIPStringFails(const std::string& broken) { + IPAddress addr(0); // Intentionally make it v4. + if (IPFromString(kIPv4BrokenString1, &addr)) { + return false; + } + return addr.family() == AF_UNSPEC; +} + +bool CheckMaskCount(const std::string& mask, int expected_length) { + IPAddress addr; + return IPFromString(mask, &addr) && + (expected_length == CountIPMaskBits(addr)); +} + +bool TryInvalidMaskCount(const std::string& mask) { + // We don't care about the result at all, but we do want to know if + // CountIPMaskBits is going to crash or infinite loop or something. + IPAddress addr; + if (!IPFromString(mask, &addr)) { + return false; + } + CountIPMaskBits(addr); + return true; +} + +bool CheckTruncateIP(const std::string& initial, int truncate_length, + const std::string& expected_result) { + IPAddress addr, expected; + IPFromString(initial, &addr); + IPFromString(expected_result, &expected); + IPAddress truncated = TruncateIP(addr, truncate_length); + return truncated == expected; +} + +TEST(IPAddressTest, TestDefaultCtor) { + IPAddress addr; + EXPECT_FALSE(IPIsAny(addr)); + EXPECT_FALSE(IPIsLoopback(addr)); + EXPECT_FALSE(IPIsPrivate(addr)); + + EXPECT_EQ(0U, addr.Size()); + EXPECT_EQ(AF_UNSPEC, addr.family()); + EXPECT_EQ("", addr.ToString()); +} + +TEST(IPAddressTest, TestInAddrCtor) { + in_addr v4addr; + + // Test V4 Any address. + v4addr.s_addr = INADDR_ANY; + IPAddress addr(v4addr); + EXPECT_TRUE(IPIsAny(addr)); + EXPECT_FALSE(IPIsLoopback(addr)); + EXPECT_FALSE(IPIsPrivate(addr)); + EXPECT_EQ(kIPv4AddrSize, addr.Size()); + EXPECT_EQ(kIPv4AnyAddrString, addr.ToString()); + + // Test a V4 loopback address. + v4addr.s_addr = htonl(INADDR_LOOPBACK); + addr = IPAddress(v4addr); + EXPECT_FALSE(IPIsAny(addr)); + EXPECT_TRUE(IPIsLoopback(addr)); + EXPECT_TRUE(IPIsPrivate(addr)); + EXPECT_EQ(kIPv4AddrSize, addr.Size()); + EXPECT_EQ(kIPv4LoopbackAddrString, addr.ToString()); + + // Test an RFC1918 address. + v4addr.s_addr = htonl(kIPv4RFC1918Addr); + addr = IPAddress(v4addr); + EXPECT_FALSE(IPIsAny(addr)); + EXPECT_FALSE(IPIsLoopback(addr)); + EXPECT_TRUE(IPIsPrivate(addr)); + EXPECT_EQ(kIPv4AddrSize, addr.Size()); + EXPECT_EQ(kIPv4RFC1918AddrString, addr.ToString()); + + // Test a 'normal' v4 address. + v4addr.s_addr = htonl(kIPv4PublicAddr); + addr = IPAddress(v4addr); + EXPECT_FALSE(IPIsAny(addr)); + EXPECT_FALSE(IPIsLoopback(addr)); + EXPECT_FALSE(IPIsPrivate(addr)); + EXPECT_EQ(kIPv4AddrSize, addr.Size()); + EXPECT_EQ(kIPv4PublicAddrString, addr.ToString()); +} + +TEST(IPAddressTest, TestInAddr6Ctor) { + // Test v6 empty. + IPAddress addr(in6addr_any); + EXPECT_TRUE(IPIsAny(addr)); + EXPECT_FALSE(IPIsLoopback(addr)); + EXPECT_FALSE(IPIsPrivate(addr)); + EXPECT_EQ(kIPv6AddrSize, addr.Size()); + EXPECT_EQ(kIPv6AnyAddrString, addr.ToString()); + + // Test v6 loopback. + addr = IPAddress(in6addr_loopback); + EXPECT_FALSE(IPIsAny(addr)); + EXPECT_TRUE(IPIsLoopback(addr)); + EXPECT_TRUE(IPIsPrivate(addr)); + EXPECT_EQ(kIPv6AddrSize, addr.Size()); + EXPECT_EQ(kIPv6LoopbackAddrString, addr.ToString()); + + // Test v6 link-local. + addr = IPAddress(kIPv6LinkLocalAddr); + EXPECT_FALSE(IPIsAny(addr)); + EXPECT_FALSE(IPIsLoopback(addr)); + EXPECT_TRUE(IPIsPrivate(addr)); + EXPECT_EQ(kIPv6AddrSize, addr.Size()); + EXPECT_EQ(kIPv6LinkLocalAddrString, addr.ToString()); + + // Test v6 global address. + addr = IPAddress(kIPv6PublicAddr); + EXPECT_FALSE(IPIsAny(addr)); + EXPECT_FALSE(IPIsLoopback(addr)); + EXPECT_FALSE(IPIsPrivate(addr)); + EXPECT_EQ(kIPv6AddrSize, addr.Size()); + EXPECT_EQ(kIPv6PublicAddrString, addr.ToString()); +} + +TEST(IPAddressTest, TestUint32Ctor) { + // Test V4 Any address. + IPAddress addr(0); + EXPECT_TRUE(IPIsAny(addr)); + EXPECT_FALSE(IPIsLoopback(addr)); + EXPECT_FALSE(IPIsPrivate(addr)); + EXPECT_EQ(kIPv4AddrSize, addr.Size()); + EXPECT_EQ(kIPv4AnyAddrString, addr.ToString()); + + // Test a V4 loopback address. + addr = IPAddress(INADDR_LOOPBACK); + EXPECT_FALSE(IPIsAny(addr)); + EXPECT_TRUE(IPIsLoopback(addr)); + EXPECT_TRUE(IPIsPrivate(addr)); + EXPECT_EQ(kIPv4AddrSize, addr.Size()); + EXPECT_EQ(kIPv4LoopbackAddrString, addr.ToString()); + + // Test an RFC1918 address. + addr = IPAddress(kIPv4RFC1918Addr); + EXPECT_FALSE(IPIsAny(addr)); + EXPECT_FALSE(IPIsLoopback(addr)); + EXPECT_TRUE(IPIsPrivate(addr)); + EXPECT_EQ(kIPv4AddrSize, addr.Size()); + EXPECT_EQ(kIPv4RFC1918AddrString, addr.ToString()); + + // Test a 'normal' v4 address. + addr = IPAddress(kIPv4PublicAddr); + EXPECT_FALSE(IPIsAny(addr)); + EXPECT_FALSE(IPIsLoopback(addr)); + EXPECT_FALSE(IPIsPrivate(addr)); + EXPECT_EQ(kIPv4AddrSize, addr.Size()); + EXPECT_EQ(kIPv4PublicAddrString, addr.ToString()); +} + +TEST(IPAddressTest, TestCopyCtor) { + in_addr v4addr; + v4addr.s_addr = htonl(kIPv4PublicAddr); + IPAddress addr(v4addr); + IPAddress addr2(addr); + + EXPECT_PRED2(AreEqual, addr, addr2); + + addr = IPAddress(INADDR_ANY); + addr2 = IPAddress(addr); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr = IPAddress(INADDR_LOOPBACK); + addr2 = IPAddress(addr); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr = IPAddress(kIPv4PublicAddr); + addr2 = IPAddress(addr); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr = IPAddress(kIPv4RFC1918Addr); + addr2 = IPAddress(addr); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr = IPAddress(in6addr_any); + addr2 = IPAddress(addr); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr = IPAddress(in6addr_loopback); + addr2 = IPAddress(addr); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr = IPAddress(kIPv6LinkLocalAddr); + addr2 = IPAddress(addr); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr = IPAddress(kIPv6PublicAddr); + addr2 = IPAddress(addr); + EXPECT_PRED2(AreEqual, addr, addr2); +} + +TEST(IPAddressTest, TestEquality) { + // Check v4 equality + in_addr v4addr, v4addr2; + v4addr.s_addr = htonl(kIPv4PublicAddr); + v4addr2.s_addr = htonl(kIPv4PublicAddr + 1); + IPAddress addr(v4addr); + IPAddress addr2(v4addr2); + IPAddress addr3(v4addr); + + EXPECT_TRUE(addr == addr); + EXPECT_TRUE(addr2 == addr2); + EXPECT_TRUE(addr3 == addr3); + EXPECT_TRUE(addr == addr3); + EXPECT_TRUE(addr3 == addr); + EXPECT_FALSE(addr2 == addr); + EXPECT_FALSE(addr2 == addr3); + EXPECT_FALSE(addr == addr2); + EXPECT_FALSE(addr3 == addr2); + + // Check v6 equality + IPAddress addr4(kIPv6PublicAddr); + IPAddress addr5(kIPv6LinkLocalAddr); + IPAddress addr6(kIPv6PublicAddr); + + EXPECT_TRUE(addr4 == addr4); + EXPECT_TRUE(addr5 == addr5); + EXPECT_TRUE(addr4 == addr6); + EXPECT_TRUE(addr6 == addr4); + EXPECT_FALSE(addr4 == addr5); + EXPECT_FALSE(addr5 == addr4); + EXPECT_FALSE(addr6 == addr5); + EXPECT_FALSE(addr5 == addr6); + + // Check v4/v6 cross-equality + EXPECT_FALSE(addr == addr4); + EXPECT_FALSE(addr == addr5); + EXPECT_FALSE(addr == addr6); + EXPECT_FALSE(addr4 == addr); + EXPECT_FALSE(addr5 == addr); + EXPECT_FALSE(addr6 == addr); + EXPECT_FALSE(addr2 == addr4); + EXPECT_FALSE(addr2 == addr5); + EXPECT_FALSE(addr2 == addr6); + EXPECT_FALSE(addr4 == addr2); + EXPECT_FALSE(addr5 == addr2); + EXPECT_FALSE(addr6 == addr2); + EXPECT_FALSE(addr3 == addr4); + EXPECT_FALSE(addr3 == addr5); + EXPECT_FALSE(addr3 == addr6); + EXPECT_FALSE(addr4 == addr3); + EXPECT_FALSE(addr5 == addr3); + EXPECT_FALSE(addr6 == addr3); + + // Special cases: loopback and any. + // They're special but they're still not equal. + IPAddress v4loopback(htonl(INADDR_LOOPBACK)); + IPAddress v6loopback(in6addr_loopback); + EXPECT_FALSE(v4loopback == v6loopback); + + IPAddress v4any(0); + IPAddress v6any(in6addr_any); + EXPECT_FALSE(v4any == v6any); +} + +TEST(IPAddressTest, TestComparison) { + // Defined in 'ascending' order. + // v6 > v4, and intra-family sorting is purely numerical + IPAddress addr0; // AF_UNSPEC + IPAddress addr1(INADDR_ANY); // 0.0.0.0 + IPAddress addr2(kIPv4PublicAddr); // 1.2.3.4 + IPAddress addr3(INADDR_LOOPBACK); // 127.0.0.1 + IPAddress addr4(kIPv4RFC1918Addr); // 192.168.7.1. + IPAddress addr5(in6addr_any); // :: + IPAddress addr6(in6addr_loopback); // ::1 + IPAddress addr7(kIPv6PublicAddr); // 2401.... + IPAddress addr8(kIPv6LinkLocalAddr); // fe80.... + + EXPECT_TRUE(addr0 < addr1); + EXPECT_TRUE(addr1 < addr2); + EXPECT_TRUE(addr2 < addr3); + EXPECT_TRUE(addr3 < addr4); + EXPECT_TRUE(addr4 < addr5); + EXPECT_TRUE(addr5 < addr6); + EXPECT_TRUE(addr6 < addr7); + EXPECT_TRUE(addr7 < addr8); + + EXPECT_FALSE(addr0 > addr1); + EXPECT_FALSE(addr1 > addr2); + EXPECT_FALSE(addr2 > addr3); + EXPECT_FALSE(addr3 > addr4); + EXPECT_FALSE(addr4 > addr5); + EXPECT_FALSE(addr5 > addr6); + EXPECT_FALSE(addr6 > addr7); + EXPECT_FALSE(addr7 > addr8); + + EXPECT_FALSE(addr0 > addr0); + EXPECT_FALSE(addr1 > addr1); + EXPECT_FALSE(addr2 > addr2); + EXPECT_FALSE(addr3 > addr3); + EXPECT_FALSE(addr4 > addr4); + EXPECT_FALSE(addr5 > addr5); + EXPECT_FALSE(addr6 > addr6); + EXPECT_FALSE(addr7 > addr7); + EXPECT_FALSE(addr8 > addr8); + + EXPECT_FALSE(addr0 < addr0); + EXPECT_FALSE(addr1 < addr1); + EXPECT_FALSE(addr2 < addr2); + EXPECT_FALSE(addr3 < addr3); + EXPECT_FALSE(addr4 < addr4); + EXPECT_FALSE(addr5 < addr5); + EXPECT_FALSE(addr6 < addr6); + EXPECT_FALSE(addr7 < addr7); + EXPECT_FALSE(addr8 < addr8); +} + +TEST(IPAddressTest, TestFromString) { + IPAddress addr; + IPAddress addr2; + addr2 = IPAddress(INADDR_ANY); + + EXPECT_TRUE(IPFromString(kIPv4AnyAddrString, &addr)); + EXPECT_EQ(addr.ToString(), kIPv4AnyAddrString); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr2 = IPAddress(INADDR_LOOPBACK); + EXPECT_TRUE(IPFromString(kIPv4LoopbackAddrString, &addr)); + EXPECT_EQ(addr.ToString(), kIPv4LoopbackAddrString); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr2 = IPAddress(kIPv4RFC1918Addr); + EXPECT_TRUE(IPFromString(kIPv4RFC1918AddrString, &addr)); + EXPECT_EQ(addr.ToString(), kIPv4RFC1918AddrString); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr2 = IPAddress(kIPv4PublicAddr); + EXPECT_TRUE(IPFromString(kIPv4PublicAddrString, &addr)); + EXPECT_EQ(addr.ToString(), kIPv4PublicAddrString); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr2 = IPAddress(in6addr_any); + EXPECT_TRUE(IPFromString(kIPv6AnyAddrString, &addr)); + EXPECT_EQ(addr.ToString(), kIPv6AnyAddrString); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr2 = IPAddress(in6addr_loopback); + EXPECT_TRUE(IPFromString(kIPv6LoopbackAddrString, &addr)); + EXPECT_EQ(addr.ToString(), kIPv6LoopbackAddrString); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr2 = IPAddress(kIPv6LinkLocalAddr); + EXPECT_TRUE(IPFromString(kIPv6LinkLocalAddrString, &addr)); + EXPECT_EQ(addr.ToString(), kIPv6LinkLocalAddrString); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr2 = IPAddress(kIPv6PublicAddr); + EXPECT_TRUE(IPFromString(kIPv6PublicAddrString, &addr)); + EXPECT_EQ(addr.ToString(), kIPv6PublicAddrString); + EXPECT_PRED2(AreEqual, addr, addr2); + + addr2 = IPAddress(kIPv4MappedRFC1918Addr); + EXPECT_TRUE(IPFromString(kIPv4MappedV4StyleAddrString, &addr)); + EXPECT_PRED2(AreEqual, addr, addr2); + + // Broken cases, should set addr to AF_UNSPEC. + EXPECT_PRED1(BrokenIPStringFails, kIPv4BrokenString1); + EXPECT_PRED1(BrokenIPStringFails, kIPv4BrokenString2); + EXPECT_PRED1(BrokenIPStringFails, kIPv4BrokenString3); + EXPECT_PRED1(BrokenIPStringFails, kIPv4BrokenString4); + EXPECT_PRED1(BrokenIPStringFails, kIPv4BrokenString5); + EXPECT_PRED1(BrokenIPStringFails, kIPv4BrokenString6); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString1); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString2); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString3); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString4); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString5); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString6); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString7); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString8); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString9); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString10); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString11); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString12); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString13); + EXPECT_PRED1(BrokenIPStringFails, kIPv6BrokenString14); +} + +TEST(IPAddressTest, TestIPFromAddrInfo) { + struct sockaddr_in expected4; + struct sockaddr_in6 expected6; + struct addrinfo test_info; + struct addrinfo next_info; + memset(&next_info, 'A', sizeof(next_info)); + test_info.ai_next = &next_info; + // Check that we can get an IPv4 address out. + test_info.ai_addr = reinterpret_cast(&expected4); + expected4.sin_addr.s_addr = HostToNetwork32(kIPv4PublicAddr); + expected4.sin_family = AF_INET; + IPAddress expected(kIPv4PublicAddr); + IPAddress addr; + EXPECT_TRUE(IPFromAddrInfo(&test_info, &addr)); + EXPECT_EQ(expected, addr); + // Check that we can get an IPv6 address out. + expected6.sin6_addr = kIPv6PublicAddr; + expected6.sin6_family = AF_INET6; + expected = IPAddress(kIPv6PublicAddr); + test_info.ai_addr = reinterpret_cast(&expected6); + EXPECT_TRUE(IPFromAddrInfo(&test_info, &addr)); + EXPECT_EQ(expected, addr); + // Check that unspec fails. + expected6.sin6_family = AF_UNSPEC; + EXPECT_FALSE(IPFromAddrInfo(&test_info, &addr)); + // Check a zeroed out addrinfo doesn't crash us. + memset(&next_info, 0, sizeof(next_info)); + EXPECT_FALSE(IPFromAddrInfo(&next_info, &addr)); +} + +TEST(IPAddressTest, TestIsPrivate) { + EXPECT_FALSE(IPIsPrivate(IPAddress(INADDR_ANY))); + EXPECT_FALSE(IPIsPrivate(IPAddress(kIPv4PublicAddr))); + EXPECT_FALSE(IPIsPrivate(IPAddress(in6addr_any))); + EXPECT_FALSE(IPIsPrivate(IPAddress(kIPv6PublicAddr))); + EXPECT_FALSE(IPIsPrivate(IPAddress(kIPv4MappedAnyAddr))); + EXPECT_FALSE(IPIsPrivate(IPAddress(kIPv4MappedPublicAddr))); + + EXPECT_TRUE(IPIsPrivate(IPAddress(kIPv4RFC1918Addr))); + EXPECT_TRUE(IPIsPrivate(IPAddress(INADDR_LOOPBACK))); + EXPECT_TRUE(IPIsPrivate(IPAddress(in6addr_loopback))); + EXPECT_TRUE(IPIsPrivate(IPAddress(kIPv6LinkLocalAddr))); +} + +TEST(IPAddressTest, TestIsLoopback) { + EXPECT_FALSE(IPIsLoopback(IPAddress(INADDR_ANY))); + EXPECT_FALSE(IPIsLoopback(IPAddress(kIPv4PublicAddr))); + EXPECT_FALSE(IPIsLoopback(IPAddress(in6addr_any))); + EXPECT_FALSE(IPIsLoopback(IPAddress(kIPv6PublicAddr))); + EXPECT_FALSE(IPIsLoopback(IPAddress(kIPv4MappedAnyAddr))); + EXPECT_FALSE(IPIsLoopback(IPAddress(kIPv4MappedPublicAddr))); + + EXPECT_TRUE(IPIsLoopback(IPAddress(INADDR_LOOPBACK))); + EXPECT_TRUE(IPIsLoopback(IPAddress(in6addr_loopback))); +} + +TEST(IPAddressTest, TestNormalized) { + // Check normalizing a ::ffff:a.b.c.d address. + IPAddress addr; + EXPECT_TRUE(IPFromString(kIPv4MappedV4StyleAddrString, &addr)); + IPAddress addr2(kIPv4RFC1918Addr); + addr = addr.Normalized(); + EXPECT_EQ(addr2, addr); + + // Check normalizing a ::ffff:aabb:ccdd address. + addr = IPAddress(kIPv4MappedPublicAddr); + addr2 = IPAddress(kIPv4PublicAddr); + addr = addr.Normalized(); + EXPECT_EQ(addr, addr2); + + // Check that a non-mapped v6 addresses isn't altered. + addr = IPAddress(kIPv6PublicAddr); + addr2 = IPAddress(kIPv6PublicAddr); + addr = addr.Normalized(); + EXPECT_EQ(addr, addr2); + + // Check that addresses that look a bit like mapped addresses aren't altered + EXPECT_TRUE(IPFromString("fe80::ffff:0102:0304", &addr)); + addr2 = addr; + addr = addr.Normalized(); + EXPECT_EQ(addr, addr2); + EXPECT_TRUE(IPFromString("::0102:0304", &addr)); + addr2 = addr; + addr = addr.Normalized(); + EXPECT_EQ(addr, addr2); + // This string should 'work' as an IP address but is not a mapped address, + // so it shouldn't change on normalization. + EXPECT_TRUE(IPFromString("::192.168.7.1", &addr)); + addr2 = addr; + addr = addr.Normalized(); + EXPECT_EQ(addr, addr2); + + // Check that v4 addresses aren't altered. + addr = IPAddress(htonl(kIPv4PublicAddr)); + addr2 = IPAddress(htonl(kIPv4PublicAddr)); + addr = addr.Normalized(); + EXPECT_EQ(addr, addr2); +} + +TEST(IPAddressTest, TestAsIPv6Address) { + IPAddress addr(kIPv4PublicAddr); + IPAddress addr2(kIPv4MappedPublicAddr); + addr = addr.AsIPv6Address(); + EXPECT_EQ(addr, addr2); + + addr = IPAddress(kIPv4MappedPublicAddr); + addr2 = IPAddress(kIPv4MappedPublicAddr); + addr = addr.AsIPv6Address(); + EXPECT_EQ(addr, addr2); + + addr = IPAddress(kIPv6PublicAddr); + addr2 = IPAddress(kIPv6PublicAddr); + addr = addr.AsIPv6Address(); + EXPECT_EQ(addr, addr2); +} + +TEST(IPAddressTest, TestCountIPMaskBits) { + IPAddress mask; + // IPv4 on byte boundaries + EXPECT_PRED2(CheckMaskCount, "255.255.255.255", 32); + EXPECT_PRED2(CheckMaskCount, "255.255.255.0", 24); + EXPECT_PRED2(CheckMaskCount, "255.255.0.0", 16); + EXPECT_PRED2(CheckMaskCount, "255.0.0.0", 8); + EXPECT_PRED2(CheckMaskCount, "0.0.0.0", 0); + + // IPv4 not on byte boundaries + EXPECT_PRED2(CheckMaskCount, "128.0.0.0", 1); + EXPECT_PRED2(CheckMaskCount, "224.0.0.0", 3); + EXPECT_PRED2(CheckMaskCount, "255.248.0.0", 13); + EXPECT_PRED2(CheckMaskCount, "255.255.224.0", 19); + EXPECT_PRED2(CheckMaskCount, "255.255.255.252", 30); + + // V6 on byte boundaries + EXPECT_PRED2(CheckMaskCount, "::", 0); + EXPECT_PRED2(CheckMaskCount, "ff00::", 8); + EXPECT_PRED2(CheckMaskCount, "ffff::", 16); + EXPECT_PRED2(CheckMaskCount, "ffff:ff00::", 24); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff::", 32); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ff00::", 40); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff::", 48); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ff00::", 56); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff::", 64); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ff00::", 72); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ffff::", 80); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ffff:ff00::", 88); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ffff:ffff::", 96); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ffff:ffff:ff00:0000", 104); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:0000", 112); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00", 120); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 128); + + // V6 not on byte boundaries. + EXPECT_PRED2(CheckMaskCount, "8000::", 1); + EXPECT_PRED2(CheckMaskCount, "ff80::", 9); + EXPECT_PRED2(CheckMaskCount, "ffff:fe00::", 23); + EXPECT_PRED2(CheckMaskCount, "ffff:fffe::", 31); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:e000::", 35); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffe0::", 43); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:f800::", 53); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:fff8::", 61); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:fc00::", 70); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:fffc::", 78); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ffff:8000::", 81); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ffff:ff80::", 89); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ffff:ffff:fe00::", 103); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ffff:ffff:fffe:0000", 111); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fc00", 118); + EXPECT_PRED2(CheckMaskCount, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffc", 126); + + // Non-contiguous ranges. These are invalid but lets test them + // to make sure they don't crash anything or infinite loop or something. + EXPECT_PRED1(TryInvalidMaskCount, "217.0.0.0"); + EXPECT_PRED1(TryInvalidMaskCount, "255.185.0.0"); + EXPECT_PRED1(TryInvalidMaskCount, "255.255.251.0"); + EXPECT_PRED1(TryInvalidMaskCount, "255.255.251.255"); + EXPECT_PRED1(TryInvalidMaskCount, "255.255.254.201"); + EXPECT_PRED1(TryInvalidMaskCount, "::1"); + EXPECT_PRED1(TryInvalidMaskCount, "fe80::1"); + EXPECT_PRED1(TryInvalidMaskCount, "ff80::1"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff::1"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff:ff00:1::1"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff:ffff::ffff:1"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff:ffff:ff00:1::"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff:ffff:ffff::ff00"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff:ffff:ffff:ff00:1234::"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff:ffff:ffff:ffff:0012::ffff"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff:ffff:ffff:ffff:ff01::"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff:ffff:ffff:ffff:ffff:7f00::"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff:ffff:ffff:ffff:ffff:ff7a::"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff:ffff:ffff:ffff:ffff:ffff:7f00:0000"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff:ffff:ffff:ffff:ffff:ffff:ff70:0000"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:0211"); + EXPECT_PRED1(TryInvalidMaskCount, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff7f"); +} + +TEST(IPAddressTest, TestTruncateIP) { + EXPECT_PRED3(CheckTruncateIP, "255.255.255.255", 24, "255.255.255.0"); + EXPECT_PRED3(CheckTruncateIP, "255.255.255.255", 16, "255.255.0.0"); + EXPECT_PRED3(CheckTruncateIP, "255.255.255.255", 8, "255.0.0.0"); + EXPECT_PRED3(CheckTruncateIP, "202.67.7.255", 24, "202.67.7.0"); + EXPECT_PRED3(CheckTruncateIP, "202.129.65.205", 16, "202.129.0.0"); + EXPECT_PRED3(CheckTruncateIP, "55.25.2.77", 8, "55.0.0.0"); + EXPECT_PRED3(CheckTruncateIP, "74.128.99.254", 1, "0.0.0.0"); + EXPECT_PRED3(CheckTruncateIP, "106.55.99.254", 3, "96.0.0.0"); + EXPECT_PRED3(CheckTruncateIP, "172.167.53.222", 13, "172.160.0.0"); + EXPECT_PRED3(CheckTruncateIP, "255.255.224.0", 18, "255.255.192.0"); + EXPECT_PRED3(CheckTruncateIP, "255.255.255.252", 28, "255.255.255.240"); + + EXPECT_PRED3(CheckTruncateIP, "fe80:1111:2222:3333:4444:5555:6666:7777", 1, + "8000::"); + EXPECT_PRED3(CheckTruncateIP, "fff0:1111:2222:3333:4444:5555:6666:7777", 9, + "ff80::"); + EXPECT_PRED3(CheckTruncateIP, "ffff:ff80:1111:2222:3333:4444:5555:6666", 23, + "ffff:fe00::"); + EXPECT_PRED3(CheckTruncateIP, "ffff:ff80:1111:2222:3333:4444:5555:6666", 32, + "ffff:ff80::"); + EXPECT_PRED3(CheckTruncateIP, "2400:f9af:e456:1111:2222:3333:4444:5555", 35, + "2400:f9af:e000::"); + EXPECT_PRED3(CheckTruncateIP, "9999:1111:2233:4444:5555:6666:7777:8888", 53, + "9999:1111:2233:4000::"); + EXPECT_PRED3(CheckTruncateIP, "9999:1111:2233:4567:5555:6666:7777:8888", 64, + "9999:1111:2233:4567::"); + EXPECT_PRED3(CheckTruncateIP, "1111:2222:3333:4444:5555:6666:7777:8888", 68, + "1111:2222:3333:4444:5000::"); + EXPECT_PRED3(CheckTruncateIP, "1111:2222:3333:4444:5555:6666:7777:8888", 92, + "1111:2222:3333:4444:5555:6660::"); + EXPECT_PRED3(CheckTruncateIP, "1111:2222:3333:4444:5555:6666:7777:8888", 96, + "1111:2222:3333:4444:5555:6666::"); + EXPECT_PRED3(CheckTruncateIP, "1111:2222:3333:4444:5555:6666:7777:8888", 105, + "1111:2222:3333:4444:5555:6666:7700::"); + EXPECT_PRED3(CheckTruncateIP, "1111:2222:3333:4444:5555:6666:7777:8888", 124, + "1111:2222:3333:4444:5555:6666:7777:8880"); + + // Slightly degenerate cases + EXPECT_PRED3(CheckTruncateIP, "202.165.33.127", 32, "202.165.33.127"); + EXPECT_PRED3(CheckTruncateIP, "235.105.77.12", 0, "0.0.0.0"); + EXPECT_PRED3(CheckTruncateIP, "1111:2222:3333:4444:5555:6666:7777:8888", 128, + "1111:2222:3333:4444:5555:6666:7777:8888"); + EXPECT_PRED3(CheckTruncateIP, "1111:2222:3333:4444:5555:6666:7777:8888", 0, + "::"); +} + +TEST(IPAddressTest, TestCategorizeIPv6) { + // Test determining if an IPAddress is 6Bone/6To4/Teredo/etc. + // IPv4 address, should be none of these (not even v4compat/v4mapped). + IPAddress v4_addr(kIPv4PublicAddr); + EXPECT_FALSE(IPIs6Bone(v4_addr)); + EXPECT_FALSE(IPIs6To4(v4_addr)); + EXPECT_FALSE(IPIsSiteLocal(v4_addr)); + EXPECT_FALSE(IPIsTeredo(v4_addr)); + EXPECT_FALSE(IPIsULA(v4_addr)); + EXPECT_FALSE(IPIsV4Compatibility(v4_addr)); + EXPECT_FALSE(IPIsV4Mapped(v4_addr)); + // Linklocal (fe80::/16) adddress; should be none of these. + IPAddress linklocal_addr(kIPv6LinkLocalAddr); + EXPECT_FALSE(IPIs6Bone(linklocal_addr)); + EXPECT_FALSE(IPIs6To4(linklocal_addr)); + EXPECT_FALSE(IPIsSiteLocal(linklocal_addr)); + EXPECT_FALSE(IPIsTeredo(linklocal_addr)); + EXPECT_FALSE(IPIsULA(linklocal_addr)); + EXPECT_FALSE(IPIsV4Compatibility(linklocal_addr)); + EXPECT_FALSE(IPIsV4Mapped(linklocal_addr)); + // 'Normal' IPv6 address, should also be none of these. + IPAddress normal_addr(kIPv6PublicAddr); + EXPECT_FALSE(IPIs6Bone(normal_addr)); + EXPECT_FALSE(IPIs6To4(normal_addr)); + EXPECT_FALSE(IPIsSiteLocal(normal_addr)); + EXPECT_FALSE(IPIsTeredo(normal_addr)); + EXPECT_FALSE(IPIsULA(normal_addr)); + EXPECT_FALSE(IPIsV4Compatibility(normal_addr)); + EXPECT_FALSE(IPIsV4Mapped(normal_addr)); + // IPv4 mapped address (::ffff:123.123.123.123) + IPAddress v4mapped_addr(kIPv4MappedPublicAddr); + EXPECT_TRUE(IPIsV4Mapped(v4mapped_addr)); + EXPECT_FALSE(IPIsV4Compatibility(v4mapped_addr)); + EXPECT_FALSE(IPIs6Bone(v4mapped_addr)); + EXPECT_FALSE(IPIs6To4(v4mapped_addr)); + EXPECT_FALSE(IPIsSiteLocal(v4mapped_addr)); + EXPECT_FALSE(IPIsTeredo(v4mapped_addr)); + EXPECT_FALSE(IPIsULA(v4mapped_addr)); + // IPv4 compatibility address (::123.123.123.123) + IPAddress v4compat_addr; + IPFromString("::192.168.7.1", &v4compat_addr); + EXPECT_TRUE(IPIsV4Compatibility(v4compat_addr)); + EXPECT_FALSE(IPIs6Bone(v4compat_addr)); + EXPECT_FALSE(IPIs6To4(v4compat_addr)); + EXPECT_FALSE(IPIsSiteLocal(v4compat_addr)); + EXPECT_FALSE(IPIsTeredo(v4compat_addr)); + EXPECT_FALSE(IPIsULA(v4compat_addr)); + EXPECT_FALSE(IPIsV4Mapped(v4compat_addr)); + // 6Bone address (3FFE::/16) + IPAddress sixbone_addr; + IPFromString("3FFE:123:456::789:123", &sixbone_addr); + EXPECT_TRUE(IPIs6Bone(sixbone_addr)); + EXPECT_FALSE(IPIs6To4(sixbone_addr)); + EXPECT_FALSE(IPIsSiteLocal(sixbone_addr)); + EXPECT_FALSE(IPIsTeredo(sixbone_addr)); + EXPECT_FALSE(IPIsULA(sixbone_addr)); + EXPECT_FALSE(IPIsV4Mapped(sixbone_addr)); + EXPECT_FALSE(IPIsV4Compatibility(sixbone_addr)); + // Unique Local Address (FC::/7) + IPAddress ula_addr; + IPFromString("FC00:123:456::789:123", &ula_addr); + EXPECT_TRUE(IPIsULA(ula_addr)); + EXPECT_FALSE(IPIs6Bone(ula_addr)); + EXPECT_FALSE(IPIs6To4(ula_addr)); + EXPECT_FALSE(IPIsSiteLocal(ula_addr)); + EXPECT_FALSE(IPIsTeredo(ula_addr)); + EXPECT_FALSE(IPIsV4Mapped(ula_addr)); + EXPECT_FALSE(IPIsV4Compatibility(ula_addr)); + // 6To4 Address (2002::/16) + IPAddress sixtofour_addr; + IPFromString("2002:123:456::789:123", &sixtofour_addr); + EXPECT_TRUE(IPIs6To4(sixtofour_addr)); + EXPECT_FALSE(IPIs6Bone(sixtofour_addr)); + EXPECT_FALSE(IPIsSiteLocal(sixtofour_addr)); + EXPECT_FALSE(IPIsTeredo(sixtofour_addr)); + EXPECT_FALSE(IPIsULA(sixtofour_addr)); + EXPECT_FALSE(IPIsV4Compatibility(sixtofour_addr)); + EXPECT_FALSE(IPIsV4Mapped(sixtofour_addr)); + // Site Local address (FEC0::/10) + IPAddress sitelocal_addr; + IPFromString("FEC0:123:456::789:123", &sitelocal_addr); + EXPECT_TRUE(IPIsSiteLocal(sitelocal_addr)); + EXPECT_FALSE(IPIs6Bone(sitelocal_addr)); + EXPECT_FALSE(IPIs6To4(sitelocal_addr)); + EXPECT_FALSE(IPIsTeredo(sitelocal_addr)); + EXPECT_FALSE(IPIsULA(sitelocal_addr)); + EXPECT_FALSE(IPIsV4Compatibility(sitelocal_addr)); + EXPECT_FALSE(IPIsV4Mapped(sitelocal_addr)); + // Teredo Address (2001:0000::/32) + IPAddress teredo_addr; + IPFromString("2001:0000:123:456::789:123", &teredo_addr); + EXPECT_TRUE(IPIsTeredo(teredo_addr)); + EXPECT_FALSE(IPIsSiteLocal(teredo_addr)); + EXPECT_FALSE(IPIs6Bone(teredo_addr)); + EXPECT_FALSE(IPIs6To4(teredo_addr)); + EXPECT_FALSE(IPIsULA(teredo_addr)); + EXPECT_FALSE(IPIsV4Compatibility(teredo_addr)); + EXPECT_FALSE(IPIsV4Mapped(teredo_addr)); +} + +TEST(IPAddressTest, TestToSensitiveString) { + IPAddress addr_v4 = IPAddress(kIPv4PublicAddr); + EXPECT_EQ(kIPv4PublicAddrString, addr_v4.ToString()); + EXPECT_EQ(kIPv4PublicAddrString, addr_v4.ToSensitiveString()); + IPAddress::set_strip_sensitive(true); + EXPECT_EQ(kIPv4PublicAddrString, addr_v4.ToString()); + EXPECT_EQ(kIPv4PublicAddrAnonymizedString, addr_v4.ToSensitiveString()); + IPAddress::set_strip_sensitive(false); + + IPAddress addr_v6 = IPAddress(kIPv6PublicAddr); + EXPECT_EQ(kIPv6PublicAddrString, addr_v6.ToString()); + EXPECT_EQ(kIPv6PublicAddrString, addr_v6.ToSensitiveString()); + IPAddress::set_strip_sensitive(true); + EXPECT_EQ(kIPv6PublicAddrString, addr_v6.ToString()); + EXPECT_EQ(kIPv6PublicAddrAnonymizedString, addr_v6.ToSensitiveString()); + IPAddress::set_strip_sensitive(false); +} + +} // namespace talk_base diff --git a/talk/base/json.cc b/talk/base/json.cc new file mode 100644 index 000000000..af81e0694 --- /dev/null +++ b/talk/base/json.cc @@ -0,0 +1,313 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/json.h" + +#include + +#include +#include +#include + +bool GetStringFromJson(const Json::Value& in, std::string* out) { + if (!in.isString()) { + std::ostringstream s; + if (in.isBool()) { + s << std::boolalpha << in.asBool(); + } else if (in.isInt()) { + s << in.asInt(); + } else if (in.isUInt()) { + s << in.asUInt(); + } else if (in.isDouble()) { + s << in.asDouble(); + } else { + return false; + } + *out = s.str(); + } else { + *out = in.asString(); + } + return true; +} + +bool GetIntFromJson(const Json::Value& in, int* out) { + bool ret; + if (!in.isString()) { + ret = in.isConvertibleTo(Json::intValue); + if (ret) { + *out = in.asInt(); + } + } else { + long val; // NOLINT + const char* c_str = in.asCString(); + char* end_ptr; + errno = 0; + val = strtol(c_str, &end_ptr, 10); // NOLINT + ret = (end_ptr != c_str && *end_ptr == '\0' && !errno && + val >= INT_MIN && val <= INT_MAX); + *out = val; + } + return ret; +} + +bool GetUIntFromJson(const Json::Value& in, unsigned int* out) { + bool ret; + if (!in.isString()) { + ret = in.isConvertibleTo(Json::uintValue); + if (ret) { + *out = in.asUInt(); + } + } else { + unsigned long val; // NOLINT + const char* c_str = in.asCString(); + char* end_ptr; + errno = 0; + val = strtoul(c_str, &end_ptr, 10); // NOLINT + ret = (end_ptr != c_str && *end_ptr == '\0' && !errno && + val <= UINT_MAX); + *out = val; + } + return ret; +} + +bool GetBoolFromJson(const Json::Value& in, bool* out) { + bool ret; + if (!in.isString()) { + ret = in.isConvertibleTo(Json::booleanValue); + if (ret) { + *out = in.asBool(); + } + } else { + if (in.asString() == "true") { + *out = true; + ret = true; + } else if (in.asString() == "false") { + *out = false; + ret = true; + } else { + ret = false; + } + } + return ret; +} + +bool GetDoubleFromJson(const Json::Value& in, double* out) { + bool ret; + if (!in.isString()) { + ret = in.isConvertibleTo(Json::realValue); + if (ret) { + *out = in.asDouble(); + } + } else { + double val; + const char* c_str = in.asCString(); + char* end_ptr; + errno = 0; + val = strtod(c_str, &end_ptr); + ret = (end_ptr != c_str && *end_ptr == '\0' && !errno); + *out = val; + } + return ret; +} + +namespace { +template +bool JsonArrayToVector(const Json::Value& value, + bool (*getter)(const Json::Value& in, T* out), + std::vector *vec) { + vec->clear(); + if (!value.isArray()) { + return false; + } + + for (Json::Value::ArrayIndex i = 0; i < value.size(); ++i) { + T val; + if (!getter(value[i], &val)) { + return false; + } + vec->push_back(val); + } + + return true; +} +// Trivial getter helper +bool GetValueFromJson(const Json::Value& in, Json::Value* out) { + *out = in; + return true; +} +} // unnamed namespace + +bool JsonArrayToValueVector(const Json::Value& in, + std::vector* out) { + return JsonArrayToVector(in, GetValueFromJson, out); +} + +bool JsonArrayToIntVector(const Json::Value& in, + std::vector* out) { + return JsonArrayToVector(in, GetIntFromJson, out); +} + +bool JsonArrayToUIntVector(const Json::Value& in, + std::vector* out) { + return JsonArrayToVector(in, GetUIntFromJson, out); +} + +bool JsonArrayToStringVector(const Json::Value& in, + std::vector* out) { + return JsonArrayToVector(in, GetStringFromJson, out); +} + +bool JsonArrayToBoolVector(const Json::Value& in, + std::vector* out) { + return JsonArrayToVector(in, GetBoolFromJson, out); +} + +bool JsonArrayToDoubleVector(const Json::Value& in, + std::vector* out) { + return JsonArrayToVector(in, GetDoubleFromJson, out); +} + +namespace { +template +Json::Value VectorToJsonArray(const std::vector& vec) { + Json::Value result(Json::arrayValue); + for (size_t i = 0; i < vec.size(); ++i) { + result.append(Json::Value(vec[i])); + } + return result; +} +} // unnamed namespace + +Json::Value ValueVectorToJsonArray(const std::vector& in) { + return VectorToJsonArray(in); +} + +Json::Value IntVectorToJsonArray(const std::vector& in) { + return VectorToJsonArray(in); +} + +Json::Value UIntVectorToJsonArray(const std::vector& in) { + return VectorToJsonArray(in); +} + +Json::Value StringVectorToJsonArray(const std::vector& in) { + return VectorToJsonArray(in); +} + +Json::Value BoolVectorToJsonArray(const std::vector& in) { + return VectorToJsonArray(in); +} + +Json::Value DoubleVectorToJsonArray(const std::vector& in) { + return VectorToJsonArray(in); +} + +bool GetValueFromJsonArray(const Json::Value& in, size_t n, + Json::Value* out) { + if (!in.isArray() || !in.isValidIndex(static_cast(n))) { + return false; + } + + *out = in[static_cast(n)]; + return true; +} + +bool GetIntFromJsonArray(const Json::Value& in, size_t n, + int* out) { + Json::Value x; + return GetValueFromJsonArray(in, n, &x) && GetIntFromJson(x, out); +} + +bool GetUIntFromJsonArray(const Json::Value& in, size_t n, + unsigned int* out) { + Json::Value x; + return GetValueFromJsonArray(in, n, &x) && GetUIntFromJson(x, out); +} + +bool GetStringFromJsonArray(const Json::Value& in, size_t n, + std::string* out) { + Json::Value x; + return GetValueFromJsonArray(in, n, &x) && GetStringFromJson(x, out); +} + +bool GetBoolFromJsonArray(const Json::Value& in, size_t n, + bool* out) { + Json::Value x; + return GetValueFromJsonArray(in, n, &x) && GetBoolFromJson(x, out); +} + +bool GetDoubleFromJsonArray(const Json::Value& in, size_t n, + double* out) { + Json::Value x; + return GetValueFromJsonArray(in, n, &x) && GetDoubleFromJson(x, out); +} + +bool GetValueFromJsonObject(const Json::Value& in, const std::string& k, + Json::Value* out) { + if (!in.isObject() || !in.isMember(k)) { + return false; + } + + *out = in[k]; + return true; +} + +bool GetIntFromJsonObject(const Json::Value& in, const std::string& k, + int* out) { + Json::Value x; + return GetValueFromJsonObject(in, k, &x) && GetIntFromJson(x, out); +} + +bool GetUIntFromJsonObject(const Json::Value& in, const std::string& k, + unsigned int* out) { + Json::Value x; + return GetValueFromJsonObject(in, k, &x) && GetUIntFromJson(x, out); +} + +bool GetStringFromJsonObject(const Json::Value& in, const std::string& k, + std::string* out) { + Json::Value x; + return GetValueFromJsonObject(in, k, &x) && GetStringFromJson(x, out); +} + +bool GetBoolFromJsonObject(const Json::Value& in, const std::string& k, + bool* out) { + Json::Value x; + return GetValueFromJsonObject(in, k, &x) && GetBoolFromJson(x, out); +} + +bool GetDoubleFromJsonObject(const Json::Value& in, const std::string& k, + double* out) { + Json::Value x; + return GetValueFromJsonObject(in, k, &x) && GetDoubleFromJson(x, out); +} + +std::string JsonValueToString(const Json::Value& json) { + Json::FastWriter w; + std::string value = w.write(json); + return value.substr(0, value.size() - 1); // trim trailing newline +} diff --git a/talk/base/json.h b/talk/base/json.h new file mode 100644 index 000000000..50a412290 --- /dev/null +++ b/talk/base/json.h @@ -0,0 +1,106 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_JSON_H_ +#define TALK_BASE_JSON_H_ + +#include +#include + +#ifdef JSONCPP_RELATIVE_PATH +#include "json/json.h" +#else +#include "third_party/jsoncpp/json.h" +#endif + +// TODO: Move to talk_base namespace + +/////////////////////////////////////////////////////////////////////////////// +// JSON Helpers +/////////////////////////////////////////////////////////////////////////////// + +// Robust conversion operators, better than the ones in JsonCpp. +bool GetIntFromJson(const Json::Value& in, int* out); +bool GetUIntFromJson(const Json::Value& in, unsigned int* out); +bool GetStringFromJson(const Json::Value& in, std::string* out); +bool GetBoolFromJson(const Json::Value& in, bool* out); +bool GetDoubleFromJson(const Json::Value& in, double* out); + +// Pull values out of a JSON array. +bool GetValueFromJsonArray(const Json::Value& in, size_t n, + Json::Value* out); +bool GetIntFromJsonArray(const Json::Value& in, size_t n, + int* out); +bool GetUIntFromJsonArray(const Json::Value& in, size_t n, + unsigned int* out); +bool GetStringFromJsonArray(const Json::Value& in, size_t n, + std::string* out); +bool GetBoolFromJsonArray(const Json::Value& in, size_t n, + bool* out); +bool GetDoubleFromJsonArray(const Json::Value& in, size_t n, + double* out); + +// Convert json arrays to std::vector +bool JsonArrayToValueVector(const Json::Value& in, + std::vector* out); +bool JsonArrayToIntVector(const Json::Value& in, + std::vector* out); +bool JsonArrayToUIntVector(const Json::Value& in, + std::vector* out); +bool JsonArrayToStringVector(const Json::Value& in, + std::vector* out); +bool JsonArrayToBoolVector(const Json::Value& in, + std::vector* out); +bool JsonArrayToDoubleVector(const Json::Value& in, + std::vector* out); + +// Convert std::vector to json array +Json::Value ValueVectorToJsonArray(const std::vector& in); +Json::Value IntVectorToJsonArray(const std::vector& in); +Json::Value UIntVectorToJsonArray(const std::vector& in); +Json::Value StringVectorToJsonArray(const std::vector& in); +Json::Value BoolVectorToJsonArray(const std::vector& in); +Json::Value DoubleVectorToJsonArray(const std::vector& in); + +// Pull values out of a JSON object. +bool GetValueFromJsonObject(const Json::Value& in, const std::string& k, + Json::Value* out); +bool GetIntFromJsonObject(const Json::Value& in, const std::string& k, + int* out); +bool GetUIntFromJsonObject(const Json::Value& in, const std::string& k, + unsigned int* out); +bool GetStringFromJsonObject(const Json::Value& in, const std::string& k, + std::string* out); +bool GetBoolFromJsonObject(const Json::Value& in, const std::string& k, + bool* out); +bool GetDoubleFromJsonObject(const Json::Value& in, const std::string& k, + double* out); + +// Writes out a Json value as a string. +std::string JsonValueToString(const Json::Value& json); + +#endif // TALK_BASE_JSON_H_ diff --git a/talk/base/json_unittest.cc b/talk/base/json_unittest.cc new file mode 100644 index 000000000..96a797540 --- /dev/null +++ b/talk/base/json_unittest.cc @@ -0,0 +1,294 @@ +/* + * libjingle + * Copyright 2009, 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 +#include "talk/base/gunit.h" +#include "talk/base/json.h" + +static Json::Value in_s("foo"); +static Json::Value in_sn("99"); +static Json::Value in_si("-99"); +static Json::Value in_sb("true"); +static Json::Value in_sd("1.2"); +static Json::Value in_n(12); +static Json::Value in_i(-12); +static Json::Value in_u(34U); +static Json::Value in_b(true); +static Json::Value in_d(1.2); +static Json::Value big_sn("12345678901234567890"); +static Json::Value big_si("-12345678901234567890"); +static Json::Value big_u(0xFFFFFFFF); +static Json::Value bad_a(Json::arrayValue); +static Json::Value bad_o(Json::objectValue); + +TEST(JsonTest, GetString) { + std::string out; + EXPECT_TRUE(GetStringFromJson(in_s, &out)); + EXPECT_EQ("foo", out); + EXPECT_TRUE(GetStringFromJson(in_sn, &out)); + EXPECT_EQ("99", out); + EXPECT_TRUE(GetStringFromJson(in_si, &out)); + EXPECT_EQ("-99", out); + EXPECT_TRUE(GetStringFromJson(in_i, &out)); + EXPECT_EQ("-12", out); + EXPECT_TRUE(GetStringFromJson(in_n, &out)); + EXPECT_EQ("12", out); + EXPECT_TRUE(GetStringFromJson(in_u, &out)); + EXPECT_EQ("34", out); + EXPECT_TRUE(GetStringFromJson(in_b, &out)); + EXPECT_EQ("true", out); + // Not supported here yet. + EXPECT_FALSE(GetStringFromJson(bad_a, &out)); + EXPECT_FALSE(GetStringFromJson(bad_o, &out)); +} + +TEST(JsonTest, GetInt) { + int out; + EXPECT_TRUE(GetIntFromJson(in_sn, &out)); + EXPECT_EQ(99, out); + EXPECT_TRUE(GetIntFromJson(in_si, &out)); + EXPECT_EQ(-99, out); + EXPECT_TRUE(GetIntFromJson(in_n, &out)); + EXPECT_EQ(12, out); + EXPECT_TRUE(GetIntFromJson(in_i, &out)); + EXPECT_EQ(-12, out); + EXPECT_TRUE(GetIntFromJson(in_u, &out)); + EXPECT_EQ(34, out); + EXPECT_TRUE(GetIntFromJson(in_b, &out)); + EXPECT_EQ(1, out); + EXPECT_FALSE(GetIntFromJson(in_s, &out)); + EXPECT_FALSE(GetIntFromJson(big_sn, &out)); + EXPECT_FALSE(GetIntFromJson(big_si, &out)); + EXPECT_FALSE(GetIntFromJson(big_u, &out)); + EXPECT_FALSE(GetIntFromJson(bad_a, &out)); + EXPECT_FALSE(GetIntFromJson(bad_o, &out)); +} + +TEST(JsonTest, GetUInt) { + unsigned int out; + EXPECT_TRUE(GetUIntFromJson(in_sn, &out)); + EXPECT_EQ(99U, out); + EXPECT_TRUE(GetUIntFromJson(in_n, &out)); + EXPECT_EQ(12U, out); + EXPECT_TRUE(GetUIntFromJson(in_u, &out)); + EXPECT_EQ(34U, out); + EXPECT_TRUE(GetUIntFromJson(in_b, &out)); + EXPECT_EQ(1U, out); + EXPECT_TRUE(GetUIntFromJson(big_u, &out)); + EXPECT_EQ(0xFFFFFFFFU, out); + EXPECT_FALSE(GetUIntFromJson(in_s, &out)); + // TODO: Fail reading negative strings. + // EXPECT_FALSE(GetUIntFromJson(in_si, &out)); + EXPECT_FALSE(GetUIntFromJson(in_i, &out)); + EXPECT_FALSE(GetUIntFromJson(big_sn, &out)); + EXPECT_FALSE(GetUIntFromJson(big_si, &out)); + EXPECT_FALSE(GetUIntFromJson(bad_a, &out)); + EXPECT_FALSE(GetUIntFromJson(bad_o, &out)); +} + +TEST(JsonTest, GetBool) { + bool out; + EXPECT_TRUE(GetBoolFromJson(in_sb, &out)); + EXPECT_EQ(true, out); + EXPECT_TRUE(GetBoolFromJson(in_n, &out)); + EXPECT_EQ(true, out); + EXPECT_TRUE(GetBoolFromJson(in_i, &out)); + EXPECT_EQ(true, out); + EXPECT_TRUE(GetBoolFromJson(in_u, &out)); + EXPECT_EQ(true, out); + EXPECT_TRUE(GetBoolFromJson(in_b, &out)); + EXPECT_EQ(true, out); + EXPECT_TRUE(GetBoolFromJson(big_u, &out)); + EXPECT_EQ(true, out); + EXPECT_FALSE(GetBoolFromJson(in_s, &out)); + EXPECT_FALSE(GetBoolFromJson(in_sn, &out)); + EXPECT_FALSE(GetBoolFromJson(in_si, &out)); + EXPECT_FALSE(GetBoolFromJson(big_sn, &out)); + EXPECT_FALSE(GetBoolFromJson(big_si, &out)); + EXPECT_FALSE(GetBoolFromJson(bad_a, &out)); + EXPECT_FALSE(GetBoolFromJson(bad_o, &out)); +} + +TEST(JsonTest, GetDouble) { + double out; + EXPECT_TRUE(GetDoubleFromJson(in_sn, &out)); + EXPECT_EQ(99, out); + EXPECT_TRUE(GetDoubleFromJson(in_si, &out)); + EXPECT_EQ(-99, out); + EXPECT_TRUE(GetDoubleFromJson(in_sd, &out)); + EXPECT_EQ(1.2, out); + EXPECT_TRUE(GetDoubleFromJson(in_n, &out)); + EXPECT_EQ(12, out); + EXPECT_TRUE(GetDoubleFromJson(in_i, &out)); + EXPECT_EQ(-12, out); + EXPECT_TRUE(GetDoubleFromJson(in_u, &out)); + EXPECT_EQ(34, out); + EXPECT_TRUE(GetDoubleFromJson(in_b, &out)); + EXPECT_EQ(1, out); + EXPECT_TRUE(GetDoubleFromJson(in_d, &out)); + EXPECT_EQ(1.2, out); + EXPECT_FALSE(GetDoubleFromJson(in_s, &out)); +} + +TEST(JsonTest, GetFromArray) { + Json::Value a, out; + a.append(in_s); + a.append(in_i); + a.append(in_u); + a.append(in_b); + EXPECT_TRUE(GetValueFromJsonArray(a, 0, &out)); + EXPECT_TRUE(GetValueFromJsonArray(a, 3, &out)); + EXPECT_FALSE(GetValueFromJsonArray(a, 99, &out)); + EXPECT_FALSE(GetValueFromJsonArray(a, 0xFFFFFFFF, &out)); +} + +TEST(JsonTest, GetFromObject) { + Json::Value o, out; + o["string"] = in_s; + o["int"] = in_i; + o["uint"] = in_u; + o["bool"] = in_b; + EXPECT_TRUE(GetValueFromJsonObject(o, "int", &out)); + EXPECT_TRUE(GetValueFromJsonObject(o, "bool", &out)); + EXPECT_FALSE(GetValueFromJsonObject(o, "foo", &out)); + EXPECT_FALSE(GetValueFromJsonObject(o, "", &out)); +} + +namespace { +template +std::vector VecOf3(const T& a, const T& b, const T& c) { + std::vector in; + in.push_back(a); + in.push_back(b); + in.push_back(c); + return in; +} +template +Json::Value JsonVecOf3(const T& a, const T& b, const T& c) { + Json::Value in(Json::arrayValue); + in.append(a); + in.append(b); + in.append(c); + return in; +} +} // unnamed namespace + +TEST(JsonTest, ValueVectorToFromArray) { + std::vector in = VecOf3("a", "b", "c"); + Json::Value out = ValueVectorToJsonArray(in); + EXPECT_EQ(in.size(), out.size()); + for (Json::Value::ArrayIndex i = 0; i < in.size(); ++i) { + EXPECT_EQ(in[i].asString(), out[i].asString()); + } + Json::Value inj = JsonVecOf3("a", "b", "c"); + EXPECT_EQ(inj, out); + std::vector outj; + EXPECT_TRUE(JsonArrayToValueVector(inj, &outj)); + for (Json::Value::ArrayIndex i = 0; i < in.size(); i++) { + EXPECT_EQ(in[i], outj[i]); + } +} + +TEST(JsonTest, IntVectorToFromArray) { + std::vector in = VecOf3(1, 2, 3); + Json::Value out = IntVectorToJsonArray(in); + EXPECT_EQ(in.size(), out.size()); + for (Json::Value::ArrayIndex i = 0; i < in.size(); ++i) { + EXPECT_EQ(in[i], out[i].asInt()); + } + Json::Value inj = JsonVecOf3(1, 2, 3); + EXPECT_EQ(inj, out); + std::vector outj; + EXPECT_TRUE(JsonArrayToIntVector(inj, &outj)); + for (Json::Value::ArrayIndex i = 0; i < in.size(); i++) { + EXPECT_EQ(in[i], outj[i]); + } +} + +TEST(JsonTest, UIntVectorToFromArray) { + std::vector in = VecOf3(1, 2, 3); + Json::Value out = UIntVectorToJsonArray(in); + EXPECT_EQ(in.size(), out.size()); + for (Json::Value::ArrayIndex i = 0; i < in.size(); ++i) { + EXPECT_EQ(in[i], out[i].asUInt()); + } + Json::Value inj = JsonVecOf3(1, 2, 3); + EXPECT_EQ(inj, out); + std::vector outj; + EXPECT_TRUE(JsonArrayToUIntVector(inj, &outj)); + for (Json::Value::ArrayIndex i = 0; i < in.size(); i++) { + EXPECT_EQ(in[i], outj[i]); + } +} + +TEST(JsonTest, StringVectorToFromArray) { + std::vector in = VecOf3("a", "b", "c"); + Json::Value out = StringVectorToJsonArray(in); + EXPECT_EQ(in.size(), out.size()); + for (Json::Value::ArrayIndex i = 0; i < in.size(); ++i) { + EXPECT_EQ(in[i], out[i].asString()); + } + Json::Value inj = JsonVecOf3("a", "b", "c"); + EXPECT_EQ(inj, out); + std::vector outj; + EXPECT_TRUE(JsonArrayToStringVector(inj, &outj)); + for (Json::Value::ArrayIndex i = 0; i < in.size(); i++) { + EXPECT_EQ(in[i], outj[i]); + } +} + +TEST(JsonTest, BoolVectorToFromArray) { + std::vector in = VecOf3(false, true, false); + Json::Value out = BoolVectorToJsonArray(in); + EXPECT_EQ(in.size(), out.size()); + for (Json::Value::ArrayIndex i = 0; i < in.size(); ++i) { + EXPECT_EQ(in[i], out[i].asBool()); + } + Json::Value inj = JsonVecOf3(false, true, false); + EXPECT_EQ(inj, out); + std::vector outj; + EXPECT_TRUE(JsonArrayToBoolVector(inj, &outj)); + for (Json::Value::ArrayIndex i = 0; i < in.size(); i++) { + EXPECT_EQ(in[i], outj[i]); + } +} + +TEST(JsonTest, DoubleVectorToFromArray) { + std::vector in = VecOf3(1.0, 2.0, 3.0); + Json::Value out = DoubleVectorToJsonArray(in); + EXPECT_EQ(in.size(), out.size()); + for (Json::Value::ArrayIndex i = 0; i < in.size(); ++i) { + EXPECT_EQ(in[i], out[i].asDouble()); + } + Json::Value inj = JsonVecOf3(1.0, 2.0, 3.0); + EXPECT_EQ(inj, out); + std::vector outj; + EXPECT_TRUE(JsonArrayToDoubleVector(inj, &outj)); + for (Json::Value::ArrayIndex i = 0; i < in.size(); i++) { + EXPECT_EQ(in[i], outj[i]); + } +} diff --git a/talk/base/latebindingsymboltable.cc b/talk/base/latebindingsymboltable.cc new file mode 100644 index 000000000..222621904 --- /dev/null +++ b/talk/base/latebindingsymboltable.cc @@ -0,0 +1,157 @@ +/* + * libjingle + * Copyright 2004--2010, 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/base/latebindingsymboltable.h" + +#ifdef POSIX +#include +#endif + +#include "talk/base/logging.h" + +namespace talk_base { + +#ifdef POSIX +static const DllHandle kInvalidDllHandle = NULL; +#else +#error Not implemented +#endif + +static const char *GetDllError() { +#ifdef POSIX + const char *err = dlerror(); + if (err) { + return err; + } else { + return "No error"; + } +#else +#error Not implemented +#endif +} + +static bool LoadSymbol(DllHandle handle, + const char *symbol_name, + void **symbol) { +#ifdef POSIX + *symbol = dlsym(handle, symbol_name); + const char *err = dlerror(); + if (err) { + LOG(LS_ERROR) << "Error loading symbol " << symbol_name << ": " << err; + return false; + } else if (!*symbol) { + // ELF allows for symbols to be NULL, but that should never happen for our + // usage. + LOG(LS_ERROR) << "Symbol " << symbol_name << " is NULL"; + return false; + } + return true; +#else +#error Not implemented +#endif +} + +LateBindingSymbolTable::LateBindingSymbolTable(const TableInfo *info, + void **table) + : info_(info), + table_(table), + handle_(kInvalidDllHandle), + undefined_symbols_(false) { + ClearSymbols(); +} + +LateBindingSymbolTable::~LateBindingSymbolTable() { + Unload(); +} + +bool LateBindingSymbolTable::IsLoaded() const { + return handle_ != kInvalidDllHandle; +} + +bool LateBindingSymbolTable::Load() { + ASSERT(info_->dll_name != NULL); + return LoadFromPath(info_->dll_name); +} + +bool LateBindingSymbolTable::LoadFromPath(const char *dll_path) { + if (IsLoaded()) { + return true; + } + if (undefined_symbols_) { + // We do not attempt to load again because repeated attempts are not + // likely to succeed and DLL loading is costly. + LOG(LS_ERROR) << "We know there are undefined symbols"; + return false; + } + +#ifdef POSIX + handle_ = dlopen(dll_path, RTLD_NOW); +#else +#error Not implemented +#endif + + if (handle_ == kInvalidDllHandle) { + LOG(LS_WARNING) << "Can't load " << dll_path << ": " + << GetDllError(); + return false; + } +#ifdef POSIX + // Clear any old errors. + dlerror(); +#endif + for (int i = 0; i < info_->num_symbols; ++i) { + if (!LoadSymbol(handle_, info_->symbol_names[i], &table_[i])) { + undefined_symbols_ = true; + Unload(); + return false; + } + } + return true; +} + +void LateBindingSymbolTable::Unload() { + if (!IsLoaded()) { + return; + } + +#ifdef POSIX + if (dlclose(handle_) != 0) { + LOG(LS_ERROR) << GetDllError(); + } +#else +#error Not implemented +#endif + + handle_ = kInvalidDllHandle; + ClearSymbols(); +} + +void LateBindingSymbolTable::ClearSymbols() { + memset(table_, 0, sizeof(void *) * info_->num_symbols); +} + +} // namespace talk_base diff --git a/talk/base/latebindingsymboltable.cc.def b/talk/base/latebindingsymboltable.cc.def new file mode 100644 index 000000000..1f84f30fc --- /dev/null +++ b/talk/base/latebindingsymboltable.cc.def @@ -0,0 +1,85 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// This file is a supermacro +// (see http://wanderinghorse.net/computing/papers/supermacros_cpp.html) to +// expand a definition of a late-binding symbol table class. +// +// Arguments: +// LATE_BINDING_SYMBOL_TABLE_CLASS_NAME: Name of the class to generate. +// LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST: List of symbols to load from the DLL, +// as an X-Macro list (see http://www.drdobbs.com/blogs/cpp/228700289). +// LATE_BINDING_SYMBOL_TABLE_DLL_NAME: String literal for the DLL file name to +// load. +// +// From a .cc file, include the header file containing your call to the .h.def +// supermacro, and then call this supermacro (optionally from inside the +// namespace for the class to generate, if any). Example: +// +// #include "myclassname.h" +// +// namespace foo { +// +// #define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME MY_CLASS_NAME +// #define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST MY_SYMBOLS_LIST +// #define LATE_BINDING_SYMBOL_TABLE_DLL_NAME "libdll.so.n" +// #include "talk/base/latebindingsymboltable.cc.def" +// +// } + +#ifndef LATE_BINDING_SYMBOL_TABLE_CLASS_NAME +#error You must define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME +#endif + +#ifndef LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST +#error You must define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST +#endif + +#ifndef LATE_BINDING_SYMBOL_TABLE_DLL_NAME +#error You must define LATE_BINDING_SYMBOL_TABLE_DLL_NAME +#endif + +const ::talk_base::LateBindingSymbolTable::TableInfo + LATE_BINDING_SYMBOL_TABLE_CLASS_NAME::kTableInfo = { + LATE_BINDING_SYMBOL_TABLE_DLL_NAME, + SYMBOL_TABLE_SIZE, + (const char *const []){ +#define X(sym) \ + #sym, +LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST +#undef X + }, +}; + +LATE_BINDING_SYMBOL_TABLE_CLASS_NAME::LATE_BINDING_SYMBOL_TABLE_CLASS_NAME() + : ::talk_base::LateBindingSymbolTable(&kTableInfo, table_) {} + +LATE_BINDING_SYMBOL_TABLE_CLASS_NAME::~LATE_BINDING_SYMBOL_TABLE_CLASS_NAME() {} + +#undef LATE_BINDING_SYMBOL_TABLE_CLASS_NAME +#undef LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST +#undef LATE_BINDING_SYMBOL_TABLE_DLL_NAME diff --git a/talk/base/latebindingsymboltable.h b/talk/base/latebindingsymboltable.h new file mode 100644 index 000000000..a53648bfe --- /dev/null +++ b/talk/base/latebindingsymboltable.h @@ -0,0 +1,83 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_BASE_LATEBINDINGSYMBOLTABLE_H_ +#define TALK_BASE_LATEBINDINGSYMBOLTABLE_H_ + +#include + +#include "talk/base/common.h" + +namespace talk_base { + +#ifdef POSIX +typedef void *DllHandle; +#else +#error Not implemented for this platform +#endif + +// This is the base class for "symbol table" classes to simplify the dynamic +// loading of symbols from DLLs. Currently the implementation only supports +// Linux and OS X, and pure C symbols (or extern "C" symbols that wrap C++ +// functions). Sub-classes for specific DLLs are generated via the "supermacro" +// files latebindingsymboltable.h.def and latebindingsymboltable.cc.def. See +// talk/sound/pulseaudiosymboltable.(h|cc) for an example. +class LateBindingSymbolTable { + public: + struct TableInfo { + const char *dll_name; + int num_symbols; + // Array of size num_symbols. + const char *const *symbol_names; + }; + + LateBindingSymbolTable(const TableInfo *info, void **table); + ~LateBindingSymbolTable(); + + bool IsLoaded() const; + // Loads the DLL and the symbol table. Returns true iff the DLL and symbol + // table loaded successfully. + bool Load(); + // Like load, but allows overriding the dll path for when the dll path is + // dynamic. + bool LoadFromPath(const char *dll_path); + void Unload(); + + private: + void ClearSymbols(); + + const TableInfo *info_; + void **table_; + DllHandle handle_; + bool undefined_symbols_; + + DISALLOW_COPY_AND_ASSIGN(LateBindingSymbolTable); +}; + +} // namespace talk_base + +#endif // TALK_BASE_LATEBINDINGSYMBOLTABLE_H_ diff --git a/talk/base/latebindingsymboltable.h.def b/talk/base/latebindingsymboltable.h.def new file mode 100644 index 000000000..cd8c176f3 --- /dev/null +++ b/talk/base/latebindingsymboltable.h.def @@ -0,0 +1,99 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// This file is a supermacro +// (see http://wanderinghorse.net/computing/papers/supermacros_cpp.html) to +// expand a declaration of a late-binding symbol table class. +// +// Arguments: +// LATE_BINDING_SYMBOL_TABLE_CLASS_NAME: Name of the class to generate. +// LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST: List of symbols to load from the DLL, +// as an X-Macro list (see http://www.drdobbs.com/blogs/cpp/228700289). +// +// From a .h file, include the header(s) for the DLL to late-bind and the +// latebindingsymboltable.h header, and then call this supermacro (optionally +// from inside the namespace for the class to generate, if any). Example: +// +// #include +// +// #include "talk/base/latebindingsymboltable.h" +// +// namespace foo { +// +// #define MY_CLASS_NAME DesiredClassName +// #define MY_SYMBOLS_LIST X(acos) X(sin) X(tan) +// +// #define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME MY_CLASS_NAME +// #define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST MY_SYMBOLS_LIST +// #include "talk/base/latebindingsymboltable.h.def" +// +// } + +#ifndef TALK_BASE_LATEBINDINGSYMBOLTABLE_H_ +#error You must first include latebindingsymboltable.h +#endif + +#ifndef LATE_BINDING_SYMBOL_TABLE_CLASS_NAME +#error You must define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME +#endif + +#ifndef LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST +#error You must define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST +#endif + +class LATE_BINDING_SYMBOL_TABLE_CLASS_NAME : + public ::talk_base::LateBindingSymbolTable { + public: + LATE_BINDING_SYMBOL_TABLE_CLASS_NAME(); + ~LATE_BINDING_SYMBOL_TABLE_CLASS_NAME(); + +#define X(sym) \ + typeof(&::sym) sym() const { \ + ASSERT(::talk_base::LateBindingSymbolTable::IsLoaded()); \ + return reinterpret_cast(table_[SYMBOL_TABLE_INDEX_##sym]); \ + } +LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST +#undef X + + private: + enum { +#define X(sym) \ + SYMBOL_TABLE_INDEX_##sym, +LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST +#undef X + SYMBOL_TABLE_SIZE + }; + + static const ::talk_base::LateBindingSymbolTable::TableInfo kTableInfo; + + void *table_[SYMBOL_TABLE_SIZE]; + + DISALLOW_COPY_AND_ASSIGN(LATE_BINDING_SYMBOL_TABLE_CLASS_NAME); +}; + +#undef LATE_BINDING_SYMBOL_TABLE_CLASS_NAME +#undef LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST diff --git a/talk/base/latebindingsymboltable_unittest.cc b/talk/base/latebindingsymboltable_unittest.cc new file mode 100644 index 000000000..58afdcdbf --- /dev/null +++ b/talk/base/latebindingsymboltable_unittest.cc @@ -0,0 +1,72 @@ +/* + * libjingle + * Copyright 2010, 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. + */ + +#ifdef LINUX +#include +#endif + +#include "talk/base/gunit.h" +#include "talk/base/latebindingsymboltable.h" + +namespace talk_base { + +#ifdef LINUX + +#define LIBM_SYMBOLS_CLASS_NAME LibmTestSymbolTable +#define LIBM_SYMBOLS_LIST \ + X(acos) \ + X(sin) \ + X(tan) + +#define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME LIBM_SYMBOLS_CLASS_NAME +#define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST LIBM_SYMBOLS_LIST +#include "talk/base/latebindingsymboltable.h.def" + +#define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME LIBM_SYMBOLS_CLASS_NAME +#define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST LIBM_SYMBOLS_LIST +#define LATE_BINDING_SYMBOL_TABLE_DLL_NAME "libm.so.6" +#include "talk/base/latebindingsymboltable.cc.def" + +TEST(LateBindingSymbolTable, libm) { + LibmTestSymbolTable table; + EXPECT_FALSE(table.IsLoaded()); + ASSERT_TRUE(table.Load()); + EXPECT_TRUE(table.IsLoaded()); + EXPECT_EQ(table.acos()(0.5), acos(0.5)); + EXPECT_EQ(table.sin()(0.5), sin(0.5)); + EXPECT_EQ(table.tan()(0.5), tan(0.5)); + // It would be nice to check that the addresses are the same, but the nature + // of dynamic linking and relocation makes them actually be different. + table.Unload(); + EXPECT_FALSE(table.IsLoaded()); +} + +#else +#error Not implemented +#endif + +} // namespace talk_base diff --git a/talk/base/libdbusglibsymboltable.cc b/talk/base/libdbusglibsymboltable.cc new file mode 100644 index 000000000..9c4be7f3d --- /dev/null +++ b/talk/base/libdbusglibsymboltable.cc @@ -0,0 +1,41 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifdef HAVE_DBUS_GLIB + +#include "talk/base/libdbusglibsymboltable.h" + +namespace talk_base { + +#define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME LIBDBUS_GLIB_CLASS_NAME +#define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST LIBDBUS_GLIB_SYMBOLS_LIST +#define LATE_BINDING_SYMBOL_TABLE_DLL_NAME "libdbus-glib-1.so" +#include "talk/base/latebindingsymboltable.cc.def" + +} // namespace talk_base + +#endif // HAVE_DBUS_GLIB diff --git a/talk/base/libdbusglibsymboltable.h b/talk/base/libdbusglibsymboltable.h new file mode 100644 index 000000000..8dc140fb0 --- /dev/null +++ b/talk/base/libdbusglibsymboltable.h @@ -0,0 +1,73 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifndef TALK_BASE_LIBDBUSGLIBSYMBOLTABLE_H_ +#define TALK_BASE_LIBDBUSGLIBSYMBOLTABLE_H_ + +#ifdef HAVE_DBUS_GLIB + +#include +#include + +#include "talk/base/latebindingsymboltable.h" + +namespace talk_base { + +#define LIBDBUS_GLIB_CLASS_NAME LibDBusGlibSymbolTable +// The libdbus-glib symbols we need, as an X-Macro list. +// This list must contain precisely every libdbus-glib function that is used in +// dbus.cc. +#define LIBDBUS_GLIB_SYMBOLS_LIST \ + X(dbus_bus_add_match) \ + X(dbus_connection_add_filter) \ + X(dbus_connection_close) \ + X(dbus_connection_remove_filter) \ + X(dbus_connection_set_exit_on_disconnect) \ + X(dbus_g_bus_get) \ + X(dbus_g_bus_get_private) \ + X(dbus_g_connection_get_connection) \ + X(dbus_g_connection_unref) \ + X(dbus_g_thread_init) \ + X(dbus_message_get_interface) \ + X(dbus_message_get_member) \ + X(dbus_message_get_path) \ + X(dbus_message_get_type) \ + X(dbus_message_iter_get_arg_type) \ + X(dbus_message_iter_get_basic) \ + X(dbus_message_iter_init) \ + X(dbus_message_ref) \ + X(dbus_message_unref) + +#define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME LIBDBUS_GLIB_CLASS_NAME +#define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST LIBDBUS_GLIB_SYMBOLS_LIST +#include "talk/base/latebindingsymboltable.h.def" + +} // namespace talk_base + +#endif // HAVE_DBUS_GLIB + +#endif // TALK_BASE_LIBDBUSGLIBSYMBOLTABLE_H_ diff --git a/talk/base/linked_ptr.h b/talk/base/linked_ptr.h new file mode 100644 index 000000000..a98a36702 --- /dev/null +++ b/talk/base/linked_ptr.h @@ -0,0 +1,142 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +/* + * linked_ptr - simple reference linked pointer + * (like reference counting, just using a linked list of the references + * instead of their count.) + * + * The implementation stores three pointers for every linked_ptr, but + * does not allocate anything on the free store. + */ + +#ifndef TALK_BASE_LINKED_PTR_H__ +#define TALK_BASE_LINKED_PTR_H__ + +namespace talk_base { + +/* For ANSI-challenged compilers, you may want to #define + * NO_MEMBER_TEMPLATES, explicit or mutable */ +#define NO_MEMBER_TEMPLATES + +template class linked_ptr +{ +public: + +#ifndef NO_MEMBER_TEMPLATES +# define TEMPLATE_FUNCTION template + TEMPLATE_FUNCTION friend class linked_ptr; +#else +# define TEMPLATE_FUNCTION + typedef X Y; +#endif + + typedef X element_type; + + explicit linked_ptr(X* p = 0) throw() + : itsPtr(p) {itsPrev = itsNext = this;} + ~linked_ptr() + {release();} + linked_ptr(const linked_ptr& r) throw() + {acquire(r);} + linked_ptr& operator=(const linked_ptr& r) + { + if (this != &r) { + release(); + acquire(r); + } + return *this; + } + +#ifndef NO_MEMBER_TEMPLATES + template friend class linked_ptr; + template linked_ptr(const linked_ptr& r) throw() + {acquire(r);} + template linked_ptr& operator=(const linked_ptr& r) + { + if (this != &r) { + release(); + acquire(r); + } + return *this; + } +#endif // NO_MEMBER_TEMPLATES + + X& operator*() const throw() {return *itsPtr;} + X* operator->() const throw() {return itsPtr;} + X* get() const throw() {return itsPtr;} + bool unique() const throw() {return itsPrev ? itsPrev==this : true;} + +private: + X* itsPtr; + mutable const linked_ptr* itsPrev; + mutable const linked_ptr* itsNext; + + void acquire(const linked_ptr& r) throw() + { // insert this to the list + itsPtr = r.itsPtr; + itsNext = r.itsNext; + itsNext->itsPrev = this; + itsPrev = &r; +#ifndef mutable + r.itsNext = this; +#else // for ANSI-challenged compilers + (const_cast*>(&r))->itsNext = this; +#endif + } + +#ifndef NO_MEMBER_TEMPLATES + template void acquire(const linked_ptr& r) throw() + { // insert this to the list + itsPtr = r.itsPtr; + itsNext = r.itsNext; + itsNext->itsPrev = this; + itsPrev = &r; +#ifndef mutable + r.itsNext = this; +#else // for ANSI-challenged compilers + (const_cast*>(&r))->itsNext = this; +#endif + } +#endif // NO_MEMBER_TEMPLATES + + void release() + { // erase this from the list, delete if unique + if (unique()) delete itsPtr; + else { + itsPrev->itsNext = itsNext; + itsNext->itsPrev = itsPrev; + itsPrev = itsNext = 0; + } + itsPtr = 0; + } +}; + +} // namespace talk_base + +#endif // TALK_BASE_LINKED_PTR_H__ + diff --git a/talk/base/linux.cc b/talk/base/linux.cc new file mode 100644 index 000000000..644ec4506 --- /dev/null +++ b/talk/base/linux.cc @@ -0,0 +1,282 @@ +/* + * libjingle + * Copyright 2008 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. + */ + +#if defined(LINUX) || defined(ANDROID) +#include "talk/base/linux.h" + +#include + +#include +#include +#include + +#include +#include + +#include "talk/base/stringencode.h" + +namespace talk_base { + +static const char kCpuInfoFile[] = "/proc/cpuinfo"; +static const char kCpuMaxFreqFile[] = + "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq"; + +ProcCpuInfo::ProcCpuInfo() { +} + +ProcCpuInfo::~ProcCpuInfo() { +} + +bool ProcCpuInfo::LoadFromSystem() { + ConfigParser procfs; + if (!procfs.Open(kCpuInfoFile)) { + return false; + } + return procfs.Parse(§ions_); +}; + +bool ProcCpuInfo::GetSectionCount(size_t* count) { + if (sections_.empty()) { + return false; + } + if (count) { + *count = sections_.size(); + } + return true; +} + +bool ProcCpuInfo::GetNumCpus(int* num) { + if (sections_.empty()) { + return false; + } + int total_cpus = 0; +#if defined(__arm__) + // Count the number of blocks that have a "processor" key defined. On ARM, + // there may be extra blocks of information that aren't per-processor. + size_t section_count = sections_.size(); + for (size_t i = 0; i < section_count; ++i) { + int processor_id; + if (GetSectionIntValue(i, "processor", &processor_id)) { + ++total_cpus; + } + } + // Single core ARM systems don't include "processor" keys at all, so return + // that we have a single core if we didn't find any explicitly above. + if (total_cpus == 0) { + total_cpus = 1; + } +#else + // On X86, there is exactly one info section per processor. + total_cpus = static_cast(sections_.size()); +#endif + if (num) { + *num = total_cpus; + } + return true; +} + +bool ProcCpuInfo::GetNumPhysicalCpus(int* num) { + if (sections_.empty()) { + return false; + } + // TODO: /proc/cpuinfo only reports cores that are currently + // _online_, so this may underreport the number of physical cores. +#if defined(__arm__) + // ARM (currently) has no hyperthreading, so just return the same value + // as GetNumCpus. + return GetNumCpus(num); +#else + int total_cores = 0; + std::set physical_ids; + size_t section_count = sections_.size(); + for (size_t i = 0; i < section_count; ++i) { + int physical_id; + int cores; + // Count the cores for the physical id only if we have not counted the id. + if (GetSectionIntValue(i, "physical id", &physical_id) && + GetSectionIntValue(i, "cpu cores", &cores) && + physical_ids.find(physical_id) == physical_ids.end()) { + physical_ids.insert(physical_id); + total_cores += cores; + } + } + + if (num) { + *num = total_cores; + } + return true; +#endif +} + +bool ProcCpuInfo::GetCpuFamily(int* id) { + int cpu_family = 0; + +#if defined(__arm__) + // On some ARM platforms, there is no 'cpu family' in '/proc/cpuinfo'. But + // there is 'CPU Architecture' which can be used as 'cpu family'. + // See http://en.wikipedia.org/wiki/ARM_architecture for a good list of + // ARM cpu families, architectures, and their mappings. + // There may be multiple sessions that aren't per-processor. We need to scan + // through each session until we find the first 'CPU architecture'. + size_t section_count = sections_.size(); + for (size_t i = 0; i < section_count; ++i) { + if (GetSectionIntValue(i, "CPU architecture", &cpu_family)) { + // We returns the first one (if there are multiple entries). + break; + }; + } +#else + GetSectionIntValue(0, "cpu family", &cpu_family); +#endif + if (id) { + *id = cpu_family; + } + return true; +} + +bool ProcCpuInfo::GetSectionStringValue(size_t section_num, + const std::string& key, + std::string* result) { + if (section_num >= sections_.size()) { + return false; + } + ConfigParser::SimpleMap::iterator iter = sections_[section_num].find(key); + if (iter == sections_[section_num].end()) { + return false; + } + *result = iter->second; + return true; +} + +bool ProcCpuInfo::GetSectionIntValue(size_t section_num, + const std::string& key, + int* result) { + if (section_num >= sections_.size()) { + return false; + } + ConfigParser::SimpleMap::iterator iter = sections_[section_num].find(key); + if (iter == sections_[section_num].end()) { + return false; + } + return FromString(iter->second, result); +} + +ConfigParser::ConfigParser() {} + +ConfigParser::~ConfigParser() {} + +bool ConfigParser::Open(const std::string& filename) { + FileStream* fs = new FileStream(); + if (!fs->Open(filename, "r", NULL)) { + return false; + } + instream_.reset(fs); + return true; +} + +void ConfigParser::Attach(StreamInterface* stream) { + instream_.reset(stream); +} + +bool ConfigParser::Parse(MapVector* key_val_pairs) { + // Parses the file and places the found key-value pairs into key_val_pairs. + SimpleMap section; + while (ParseSection(§ion)) { + key_val_pairs->push_back(section); + section.clear(); + } + return (!key_val_pairs->empty()); +} + +bool ConfigParser::ParseSection(SimpleMap* key_val_pair) { + // Parses the next section in the filestream and places the found key-value + // pairs into key_val_pair. + std::string key, value; + while (ParseLine(&key, &value)) { + (*key_val_pair)[key] = value; + } + return (!key_val_pair->empty()); +} + +bool ConfigParser::ParseLine(std::string* key, std::string* value) { + // Parses the next line in the filestream and places the found key-value + // pair into key and val. + std::string line; + if ((instream_->ReadLine(&line)) == SR_EOS) { + return false; + } + std::vector tokens; + if (2 != split(line, ':', &tokens)) { + return false; + } + // Removes whitespace at the end of Key name + size_t pos = tokens[0].length() - 1; + while ((pos > 0) && isspace(tokens[0][pos])) { + pos--; + } + tokens[0].erase(pos + 1); + // Removes whitespace at the start of value + pos = 0; + while (pos < tokens[1].length() && isspace(tokens[1][pos])) { + pos++; + } + tokens[1].erase(0, pos); + *key = tokens[0]; + *value = tokens[1]; + return true; +} + + +std::string ReadLinuxUname() { + struct utsname buf; + if (uname(&buf) < 0) { + LOG_ERR(LS_ERROR) << "Can't call uname()"; + return std::string(); + } + std::ostringstream sstr; + sstr << buf.sysname << " " + << buf.release << " " + << buf.version << " " + << buf.machine; + return sstr.str(); +} + +int ReadCpuMaxFreq() { + FileStream fs; + std::string str; + int freq = -1; + if (!fs.Open(kCpuMaxFreqFile, "r", NULL) || + SR_SUCCESS != fs.ReadLine(&str) || + !FromString(str, &freq)) { + return -1; + } + return freq; +} + +} // namespace talk_base + +#endif // defined(LINUX) || defined(ANDROID) diff --git a/talk/base/linux.h b/talk/base/linux.h new file mode 100644 index 000000000..63e302181 --- /dev/null +++ b/talk/base/linux.h @@ -0,0 +1,135 @@ +/* + * libjingle + * Copyright 2008, 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. + */ + +#ifndef TALK_BASE_LINUX_H_ +#define TALK_BASE_LINUX_H_ + +#if defined(LINUX) || defined(ANDROID) +#include +#include +#include + +#include "talk/base/scoped_ptr.h" +#include "talk/base/stream.h" + +namespace talk_base { + +////////////////////////////////////////////////////////////////////////////// +// ConfigParser parses a FileStream of an ".ini."-type format into a map. +////////////////////////////////////////////////////////////////////////////// + +// Sample Usage: +// ConfigParser parser; +// ConfigParser::MapVector key_val_pairs; +// if (parser.Open(inifile) && parser.Parse(&key_val_pairs)) { +// for (int section_num=0; i < key_val_pairs.size(); ++section_num) { +// std::string val1 = key_val_pairs[section_num][key1]; +// std::string val2 = key_val_pairs[section_num][key2]; +// // Do something with valn; +// } +// } + +class ConfigParser { + public: + typedef std::map SimpleMap; + typedef std::vector MapVector; + + ConfigParser(); + virtual ~ConfigParser(); + + virtual bool Open(const std::string& filename); + virtual void Attach(StreamInterface* stream); + virtual bool Parse(MapVector* key_val_pairs); + virtual bool ParseSection(SimpleMap* key_val_pair); + virtual bool ParseLine(std::string* key, std::string* value); + + private: + scoped_ptr instream_; +}; + +////////////////////////////////////////////////////////////////////////////// +// ProcCpuInfo reads CPU info from the /proc subsystem on any *NIX platform. +////////////////////////////////////////////////////////////////////////////// + +// Sample Usage: +// ProcCpuInfo proc_info; +// int no_of_cpu; +// if (proc_info.LoadFromSystem()) { +// std::string out_str; +// proc_info.GetNumCpus(&no_of_cpu); +// proc_info.GetCpuStringValue(0, "vendor_id", &out_str); +// } +// } + +class ProcCpuInfo { + public: + ProcCpuInfo(); + virtual ~ProcCpuInfo(); + + // Reads the proc subsystem's cpu info into memory. If this fails, this + // returns false; if it succeeds, it returns true. + virtual bool LoadFromSystem(); + + // Obtains the number of logical CPU threads and places the value num. + virtual bool GetNumCpus(int* num); + + // Obtains the number of physical CPU cores and places the value num. + virtual bool GetNumPhysicalCpus(int* num); + + // Obtains the CPU family id. + virtual bool GetCpuFamily(int* id); + + // Obtains the number of sections in /proc/cpuinfo, which may be greater + // than the number of CPUs (e.g. on ARM) + virtual bool GetSectionCount(size_t* count); + + // Looks for the CPU proc item with the given name for the given section + // number and places the string value in result. + virtual bool GetSectionStringValue(size_t section_num, const std::string& key, + std::string* result); + + // Looks for the CPU proc item with the given name for the given section + // number and places the int value in result. + virtual bool GetSectionIntValue(size_t section_num, const std::string& key, + int* result); + + private: + ConfigParser::MapVector sections_; +}; + +// Returns the output of "uname". +std::string ReadLinuxUname(); + +// Returns the content (int) of +// /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq +// Returns -1 on error. +int ReadCpuMaxFreq(); + +} // namespace talk_base + +#endif // defined(LINUX) || defined(ANDROID) +#endif // TALK_BASE_LINUX_H_ diff --git a/talk/base/linux_unittest.cc b/talk/base/linux_unittest.cc new file mode 100644 index 000000000..efc7f87c1 --- /dev/null +++ b/talk/base/linux_unittest.cc @@ -0,0 +1,113 @@ +/* + * libjingle + * Copyright 2008, 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 +#include "talk/base/linux.h" +#include "talk/base/fileutils.h" +#include "talk/base/logging.h" +#include "talk/base/gunit.h" + +namespace talk_base { + +// These tests running on ARM are fairly specific to the output of the tegra2 +// ARM processor, and so may fail on other ARM-based systems. +TEST(ProcCpuInfo, GetProcInfo) { + ProcCpuInfo proc_info; + EXPECT_TRUE(proc_info.LoadFromSystem()); + + int out_cpus = 0; + EXPECT_TRUE(proc_info.GetNumCpus(&out_cpus)); + LOG(LS_INFO) << "GetNumCpus: " << out_cpus; + EXPECT_GT(out_cpus, 0); + + int out_cpus_phys = 0; + EXPECT_TRUE(proc_info.GetNumPhysicalCpus(&out_cpus_phys)); + LOG(LS_INFO) << "GetNumPhysicalCpus: " << out_cpus_phys; + EXPECT_GT(out_cpus_phys, 0); + EXPECT_LE(out_cpus_phys, out_cpus); + + int out_family = 0; + EXPECT_TRUE(proc_info.GetCpuFamily(&out_family)); + LOG(LS_INFO) << "cpu family: " << out_family; + EXPECT_GE(out_family, 4); + +#if defined(__arm__) + std::string out_processor; + EXPECT_TRUE(proc_info.GetSectionStringValue(0, "Processor", &out_processor)); + LOG(LS_INFO) << "Processor: " << out_processor; + EXPECT_NE(std::string::npos, out_processor.find("ARM")); + + // Most other info, such as model, stepping, vendor, etc. + // is missing on ARM systems. +#else + int out_model = 0; + EXPECT_TRUE(proc_info.GetSectionIntValue(0, "model", &out_model)); + LOG(LS_INFO) << "model: " << out_model; + + int out_stepping = 0; + EXPECT_TRUE(proc_info.GetSectionIntValue(0, "stepping", &out_stepping)); + LOG(LS_INFO) << "stepping: " << out_stepping; + + int out_processor = 0; + EXPECT_TRUE(proc_info.GetSectionIntValue(0, "processor", &out_processor)); + LOG(LS_INFO) << "processor: " << out_processor; + EXPECT_EQ(0, out_processor); + + std::string out_str; + EXPECT_TRUE(proc_info.GetSectionStringValue(0, "vendor_id", &out_str)); + LOG(LS_INFO) << "vendor_id: " << out_str; + EXPECT_FALSE(out_str.empty()); +#endif +} + +TEST(ConfigParser, ParseConfig) { + ConfigParser parser; + MemoryStream *test_stream = new MemoryStream( + "Key1: Value1\n" + "Key2\t: Value2\n" + "Key3:Value3\n" + "\n" + "Key1:Value1\n"); + ConfigParser::MapVector key_val_pairs; + parser.Attach(test_stream); + EXPECT_EQ(true, parser.Parse(&key_val_pairs)); + EXPECT_EQ(2U, key_val_pairs.size()); + EXPECT_EQ("Value1", key_val_pairs[0]["Key1"]); + EXPECT_EQ("Value2", key_val_pairs[0]["Key2"]); + EXPECT_EQ("Value3", key_val_pairs[0]["Key3"]); + EXPECT_EQ("Value1", key_val_pairs[1]["Key1"]); + key_val_pairs.clear(); + EXPECT_EQ(true, parser.Open("/proc/cpuinfo")); + EXPECT_EQ(true, parser.Parse(&key_val_pairs)); +} + +TEST(ReadLinuxUname, ReturnsSomething) { + std::string str = ReadLinuxUname(); + EXPECT_FALSE(str.empty()); +} + +} // namespace talk_base diff --git a/talk/base/linuxfdwalk.c b/talk/base/linuxfdwalk.c new file mode 100644 index 000000000..4179f41e3 --- /dev/null +++ b/talk/base/linuxfdwalk.c @@ -0,0 +1,98 @@ +/* + * libjingle + * Copyright 2004--2009, 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 +#include +#include +#include +#include + +#include "talk/base/linuxfdwalk.h" + +// Parses a file descriptor number in base 10, requiring the strict format used +// in /proc/*/fd. Returns the value, or -1 if not a valid string. +static int parse_fd(const char *s) { + if (!*s) { + // Empty string is invalid. + return -1; + } + int val = 0; + do { + if (*s < '0' || *s > '9') { + // Non-numeric characters anywhere are invalid. + return -1; + } + int digit = *s++ - '0'; + val = val * 10 + digit; + } while (*s); + return val; +} + +int fdwalk(void (*func)(void *, int), void *opaque) { + DIR *dir = opendir("/proc/self/fd"); + if (!dir) { + return -1; + } + int opendirfd = dirfd(dir); + int parse_errors = 0; + struct dirent *ent; + // Have to clear errno to distinguish readdir() completion from failure. + while (errno = 0, (ent = readdir(dir)) != NULL) { + if (strcmp(ent->d_name, ".") == 0 || + strcmp(ent->d_name, "..") == 0) { + continue; + } + // We avoid atoi or strtol because those are part of libc and they involve + // locale stuff, which is probably not safe from a post-fork context in a + // multi-threaded app. + int fd = parse_fd(ent->d_name); + if (fd < 0) { + parse_errors = 1; + continue; + } + if (fd != opendirfd) { + (*func)(opaque, fd); + } + } + int saved_errno = errno; + if (closedir(dir) < 0) { + if (!saved_errno) { + // Return the closedir error. + return -1; + } + // Else ignore it because we have a more relevant error to return. + } + if (saved_errno) { + errno = saved_errno; + return -1; + } else if (parse_errors) { + errno = EBADF; + return -1; + } else { + return 0; + } +} diff --git a/talk/base/linuxfdwalk.h b/talk/base/linuxfdwalk.h new file mode 100644 index 000000000..ea039bff9 --- /dev/null +++ b/talk/base/linuxfdwalk.h @@ -0,0 +1,51 @@ +/* + * libjingle + * Copyright 2004--2009, 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. + */ + +#ifndef TALK_BASE_LINUXFDWALK_H_ +#define TALK_BASE_LINUXFDWALK_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +// Linux port of SunOS's fdwalk(3) call. It loops over all open file descriptors +// and calls func on each one. Additionally, it is safe to use from the child +// of a fork that hasn't exec'ed yet, so you can use it to close all open file +// descriptors prior to exec'ing a daemon. +// The return value is 0 if successful, or else -1 and errno is set. The +// possible errors include any error that can be returned by opendir(), +// readdir(), or closedir(), plus EBADF if there are problems parsing the +// contents of /proc/self/fd. +// The file descriptors that are enumerated will not include the file descriptor +// used for the enumeration itself. +int fdwalk(void (*func)(void *, int), void *opaque); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // TALK_BASE_LINUXFDWALK_H_ diff --git a/talk/base/linuxfdwalk_unittest.cc b/talk/base/linuxfdwalk_unittest.cc new file mode 100644 index 000000000..ff14b669f --- /dev/null +++ b/talk/base/linuxfdwalk_unittest.cc @@ -0,0 +1,92 @@ +/* + * libjingle + * Copyright 2009, 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 +#include + +#include "talk/base/gunit.h" +#include "talk/base/linuxfdwalk.h" + +#include +#include +#include +#include + +static const int kArbitraryLargeFdNumber = 424; + +static void FdCheckVisitor(void *data, int fd) { + std::set *fds = static_cast *>(data); + EXPECT_EQ(1U, fds->erase(fd)); +} + +static void FdEnumVisitor(void *data, int fd) { + std::set *fds = static_cast *>(data); + EXPECT_TRUE(fds->insert(fd).second); +} + +// Checks that the set of open fds is exactly the given list. +static void CheckOpenFdList(std::set fds) { + EXPECT_EQ(0, fdwalk(&FdCheckVisitor, &fds)); + EXPECT_EQ(0U, fds.size()); +} + +static void GetOpenFdList(std::set *fds) { + fds->clear(); + EXPECT_EQ(0, fdwalk(&FdEnumVisitor, fds)); +} + +TEST(LinuxFdWalk, TestFdWalk) { + std::set fds; + GetOpenFdList(&fds); + std::ostringstream str; + // I have observed that the open set when starting a test is [0, 6]. Leaked + // fds would change that, but so can (e.g.) running under a debugger, so we + // can't really do an EXPECT. :( + str << "File descriptors open in test executable:"; + for (std::set::const_iterator i = fds.begin(); i != fds.end(); ++i) { + str << " " << *i; + } + LOG(LS_INFO) << str.str(); + // Open some files. + int fd1 = open("/dev/null", O_RDONLY); + EXPECT_LE(0, fd1); + int fd2 = open("/dev/null", O_WRONLY); + EXPECT_LE(0, fd2); + int fd3 = open("/dev/null", O_RDWR); + EXPECT_LE(0, fd3); + int fd4 = dup2(fd3, kArbitraryLargeFdNumber); + EXPECT_LE(0, fd4); + EXPECT_TRUE(fds.insert(fd1).second); + EXPECT_TRUE(fds.insert(fd2).second); + EXPECT_TRUE(fds.insert(fd3).second); + EXPECT_TRUE(fds.insert(fd4).second); + CheckOpenFdList(fds); + EXPECT_EQ(0, close(fd1)); + EXPECT_EQ(0, close(fd2)); + EXPECT_EQ(0, close(fd3)); + EXPECT_EQ(0, close(fd4)); +} diff --git a/talk/base/linuxwindowpicker.cc b/talk/base/linuxwindowpicker.cc new file mode 100644 index 000000000..75e47d50b --- /dev/null +++ b/talk/base/linuxwindowpicker.cc @@ -0,0 +1,835 @@ +/* + * libjingle + * Copyright 2010 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/base/linuxwindowpicker.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "talk/base/logging.h" + +namespace talk_base { + +// Convenience wrapper for XGetWindowProperty results. +template +class XWindowProperty { + public: + XWindowProperty(Display* display, Window window, Atom property) + : data_(NULL) { + const int kBitsPerByte = 8; + Atom actual_type; + int actual_format; + unsigned long bytes_after; // NOLINT: type required by XGetWindowProperty + int status = XGetWindowProperty(display, window, property, 0L, ~0L, False, + AnyPropertyType, &actual_type, + &actual_format, &size_, + &bytes_after, &data_); + succeeded_ = (status == Success); + if (!succeeded_) { + data_ = NULL; // Ensure nothing is freed. + } else if (sizeof(PropertyType) * kBitsPerByte != actual_format) { + LOG(LS_WARNING) << "Returned type size differs from " + "requested type size."; + succeeded_ = false; + // We still need to call XFree in this case, so leave data_ alone. + } + if (!succeeded_) { + size_ = 0; + } + } + + ~XWindowProperty() { + if (data_) { + XFree(data_); + } + } + + bool succeeded() const { return succeeded_; } + size_t size() const { return size_; } + const PropertyType* data() const { + return reinterpret_cast(data_); + } + PropertyType* data() { + return reinterpret_cast(data_); + } + + private: + bool succeeded_; + unsigned long size_; // NOLINT: type required by XGetWindowProperty + unsigned char* data_; + + DISALLOW_COPY_AND_ASSIGN(XWindowProperty); +}; + +// Stupid X11. It seems none of the synchronous returns codes from X11 calls +// are meaningful unless an asynchronous error handler is configured. This +// RAII class registers and unregisters an X11 error handler. +class XErrorSuppressor { + public: + explicit XErrorSuppressor(Display* display) + : display_(display), original_error_handler_(NULL) { + SuppressX11Errors(); + } + ~XErrorSuppressor() { + UnsuppressX11Errors(); + } + + private: + static int ErrorHandler(Display* display, XErrorEvent* e) { + char buf[256]; + XGetErrorText(display, e->error_code, buf, sizeof buf); + LOG(LS_WARNING) << "Received X11 error \"" << buf << "\" for request code " + << static_cast(e->request_code); + return 0; + } + + void SuppressX11Errors() { + XFlush(display_); + XSync(display_, False); + original_error_handler_ = XSetErrorHandler(&ErrorHandler); + } + + void UnsuppressX11Errors() { + XFlush(display_); + XSync(display_, False); + XErrorHandler handler = XSetErrorHandler(original_error_handler_); + if (handler != &ErrorHandler) { + LOG(LS_WARNING) << "Unbalanced XSetErrorHandler() calls detected. " + << "Final error handler may not be what you expect!"; + } + original_error_handler_ = NULL; + } + + Display* display_; + XErrorHandler original_error_handler_; + + DISALLOW_COPY_AND_ASSIGN(XErrorSuppressor); +}; + +// Hiding all X11 specifics inside its own class. This to avoid +// conflicts between talk and X11 header declarations. +class XWindowEnumerator { + public: + XWindowEnumerator() + : display_(NULL), + has_composite_extension_(false), + has_render_extension_(false) { + } + + ~XWindowEnumerator() { + if (display_ != NULL) { + XCloseDisplay(display_); + } + } + + bool Init() { + if (display_ != NULL) { + // Already initialized. + return true; + } + display_ = XOpenDisplay(NULL); + if (display_ == NULL) { + LOG(LS_ERROR) << "Failed to open display."; + return false; + } + + XErrorSuppressor error_suppressor(display_); + + wm_state_ = XInternAtom(display_, "WM_STATE", True); + net_wm_icon_ = XInternAtom(display_, "_NET_WM_ICON", False); + + int event_base, error_base, major_version, minor_version; + if (XCompositeQueryExtension(display_, &event_base, &error_base) && + XCompositeQueryVersion(display_, &major_version, &minor_version) && + // XCompositeNameWindowPixmap() requires version 0.2 + (major_version > 0 || minor_version >= 2)) { + has_composite_extension_ = true; + } else { + LOG(LS_INFO) << "Xcomposite extension not available or too old."; + } + + if (XRenderQueryExtension(display_, &event_base, &error_base) && + XRenderQueryVersion(display_, &major_version, &minor_version) && + // XRenderSetPictureTransform() requires version 0.6 + (major_version > 0 || minor_version >= 6)) { + has_render_extension_ = true; + } else { + LOG(LS_INFO) << "Xrender extension not available or too old."; + } + return true; + } + + bool EnumerateWindows(WindowDescriptionList* descriptions) { + if (!Init()) { + return false; + } + XErrorSuppressor error_suppressor(display_); + int num_screens = XScreenCount(display_); + bool result = false; + for (int i = 0; i < num_screens; ++i) { + if (EnumerateScreenWindows(descriptions, i)) { + // We know we succeded on at least one screen. + result = true; + } + } + return result; + } + + bool EnumerateDesktops(DesktopDescriptionList* descriptions) { + if (!Init()) { + return false; + } + XErrorSuppressor error_suppressor(display_); + Window default_root_window = XDefaultRootWindow(display_); + int num_screens = XScreenCount(display_); + for (int i = 0; i < num_screens; ++i) { + Window root_window = XRootWindow(display_, i); + DesktopId id(DesktopId(root_window, i)); + // TODO: Figure out an appropriate desktop title. + DesktopDescription desc(id, ""); + desc.set_primary(root_window == default_root_window); + descriptions->push_back(desc); + } + return num_screens > 0; + } + + bool IsVisible(const WindowId& id) { + if (!Init()) { + return false; + } + XErrorSuppressor error_suppressor(display_); + XWindowAttributes attr; + if (!XGetWindowAttributes(display_, id.id(), &attr)) { + LOG(LS_ERROR) << "XGetWindowAttributes() failed"; + return false; + } + return attr.map_state == IsViewable; + } + + bool MoveToFront(const WindowId& id) { + if (!Init()) { + return false; + } + XErrorSuppressor error_suppressor(display_); + unsigned int num_children; + Window* children; + Window parent; + Window root; + + // Find root window to pass event to. + int status = XQueryTree(display_, id.id(), &root, &parent, &children, + &num_children); + if (status == 0) { + LOG(LS_WARNING) << "Failed to query for child windows."; + return false; + } + if (children != NULL) { + XFree(children); + } + + // Move the window to front. + XRaiseWindow(display_, id.id()); + + // Some window managers (e.g., metacity in GNOME) consider it illegal to + // raise a window without also giving it input focus with + // _NET_ACTIVE_WINDOW, so XRaiseWindow() on its own isn't enough. + Atom atom = XInternAtom(display_, "_NET_ACTIVE_WINDOW", True); + if (atom != None) { + XEvent xev; + long event_mask; + + xev.xclient.type = ClientMessage; + xev.xclient.serial = 0; + xev.xclient.send_event = True; + xev.xclient.window = id.id(); + xev.xclient.message_type = atom; + + // The format member is set to 8, 16, or 32 and specifies whether the + // data should be viewed as a list of bytes, shorts, or longs. + xev.xclient.format = 32; + + xev.xclient.data.l[0] = 0; + xev.xclient.data.l[1] = 0; + xev.xclient.data.l[2] = 0; + xev.xclient.data.l[3] = 0; + xev.xclient.data.l[4] = 0; + + event_mask = SubstructureRedirectMask | SubstructureNotifyMask; + + XSendEvent(display_, root, False, event_mask, &xev); + } + XFlush(display_); + return true; + } + + uint8* GetWindowIcon(const WindowId& id, int* width, int* height) { + if (!Init()) { + return NULL; + } + XErrorSuppressor error_suppressor(display_); + Atom ret_type; + int format; + unsigned long length, bytes_after, size; + unsigned char* data = NULL; + + // Find out the size of the icon data. + if (XGetWindowProperty( + display_, id.id(), net_wm_icon_, 0, 0, False, XA_CARDINAL, + &ret_type, &format, &length, &size, &data) == Success && + data) { + XFree(data); + } else { + LOG(LS_ERROR) << "Failed to get size of the icon."; + return NULL; + } + // Get the icon data, the format is one uint32 each for width and height, + // followed by the actual pixel data. + if (size >= 2 && + XGetWindowProperty( + display_, id.id(), net_wm_icon_, 0, size, False, XA_CARDINAL, + &ret_type, &format, &length, &bytes_after, &data) == Success && + data) { + uint32* data_ptr = reinterpret_cast(data); + int w, h; + w = data_ptr[0]; + h = data_ptr[1]; + if (size < static_cast(w * h + 2)) { + XFree(data); + LOG(LS_ERROR) << "Not a vaild icon."; + return NULL; + } + uint8* rgba = + ArgbToRgba(&data_ptr[2], 0, 0, w, h, w, h, true); + XFree(data); + *width = w; + *height = h; + return rgba; + } else { + LOG(LS_ERROR) << "Failed to get window icon data."; + return NULL; + } + } + + uint8* GetWindowThumbnail(const WindowId& id, int width, int height) { + if (!Init()) { + return NULL; + } + + if (!has_composite_extension_) { + // Without the Xcomposite extension we would only get a good thumbnail if + // the whole window is visible on screen and not covered by any + // other window. This is not something we want so instead, just + // bail out. + LOG(LS_INFO) << "No Xcomposite extension detected."; + return NULL; + } + XErrorSuppressor error_suppressor(display_); + + Window root; + int x; + int y; + unsigned int src_width; + unsigned int src_height; + unsigned int border_width; + unsigned int depth; + + // In addition to needing X11 server-side support for Xcomposite, it + // actually needs to be turned on for this window in order to get a good + // thumbnail. If the user has modern hardware/drivers but isn't using a + // compositing window manager, that won't be the case. Here we + // automatically turn it on for shareable windows so that we can get + // thumbnails. We used to avoid it because the transition is visually ugly, + // but recent window managers don't always redirect windows which led to + // no thumbnails at all, which is a worse experience. + + // Redirect drawing to an offscreen buffer (ie, turn on compositing). + // X11 remembers what has requested this and will turn it off for us when + // we exit. + XCompositeRedirectWindow(display_, id.id(), CompositeRedirectAutomatic); + Pixmap src_pixmap = XCompositeNameWindowPixmap(display_, id.id()); + if (!src_pixmap) { + // Even if the backing pixmap doesn't exist, this still should have + // succeeded and returned a valid handle (it just wouldn't be a handle to + // anything). So this is a real error path. + LOG(LS_ERROR) << "XCompositeNameWindowPixmap() failed"; + return NULL; + } + if (!XGetGeometry(display_, src_pixmap, &root, &x, &y, + &src_width, &src_height, &border_width, + &depth)) { + // If the window does not actually have a backing pixmap, this is the path + // that will "fail", so it's a warning rather than an error. + LOG(LS_WARNING) << "XGetGeometry() failed (probably composite is not in " + << "use)"; + XFreePixmap(display_, src_pixmap); + return NULL; + } + + // If we get to here, then composite is in use for this window and it has a + // valid backing pixmap. + + XWindowAttributes attr; + if (!XGetWindowAttributes(display_, id.id(), &attr)) { + LOG(LS_ERROR) << "XGetWindowAttributes() failed"; + XFreePixmap(display_, src_pixmap); + return NULL; + } + + uint8* data = GetDrawableThumbnail(src_pixmap, + attr.visual, + src_width, + src_height, + width, + height); + XFreePixmap(display_, src_pixmap); + return data; + } + + int GetNumDesktops() { + if (!Init()) { + return -1; + } + + return XScreenCount(display_); + } + + uint8* GetDesktopThumbnail(const DesktopId& id, int width, int height) { + if (!Init()) { + return NULL; + } + XErrorSuppressor error_suppressor(display_); + + Window root_window = id.id(); + XWindowAttributes attr; + if (!XGetWindowAttributes(display_, root_window, &attr)) { + LOG(LS_ERROR) << "XGetWindowAttributes() failed"; + return NULL; + } + + return GetDrawableThumbnail(root_window, + attr.visual, + attr.width, + attr.height, + width, + height); + } + + bool GetDesktopDimensions(const DesktopId& id, int* width, int* height) { + if (!Init()) { + return false; + } + XErrorSuppressor error_suppressor(display_); + XWindowAttributes attr; + if (!XGetWindowAttributes(display_, id.id(), &attr)) { + LOG(LS_ERROR) << "XGetWindowAttributes() failed"; + return false; + } + *width = attr.width; + *height = attr.height; + return true; + } + + private: + uint8* GetDrawableThumbnail(Drawable src_drawable, + Visual* visual, + int src_width, + int src_height, + int dst_width, + int dst_height) { + if (!has_render_extension_) { + // Without the Xrender extension we would have to read the full window and + // scale it down in our process. Xrender is over a decade old so we aren't + // going to expend effort to support that situation. We still need to + // check though because probably some virtual VNC displays are in this + // category. + LOG(LS_INFO) << "No Xrender extension detected."; + return NULL; + } + + XRenderPictFormat* format = XRenderFindVisualFormat(display_, + visual); + if (!format) { + LOG(LS_ERROR) << "XRenderFindVisualFormat() failed"; + return NULL; + } + + // Create a picture to reference the window pixmap. + XRenderPictureAttributes pa; + pa.subwindow_mode = IncludeInferiors; // Don't clip child widgets + Picture src = XRenderCreatePicture(display_, + src_drawable, + format, + CPSubwindowMode, + &pa); + if (!src) { + LOG(LS_ERROR) << "XRenderCreatePicture() failed"; + return NULL; + } + + // Create a picture to reference the destination pixmap. + Pixmap dst_pixmap = XCreatePixmap(display_, + src_drawable, + dst_width, + dst_height, + format->depth); + if (!dst_pixmap) { + LOG(LS_ERROR) << "XCreatePixmap() failed"; + XRenderFreePicture(display_, src); + return NULL; + } + + Picture dst = XRenderCreatePicture(display_, dst_pixmap, format, 0, NULL); + if (!dst) { + LOG(LS_ERROR) << "XRenderCreatePicture() failed"; + XFreePixmap(display_, dst_pixmap); + XRenderFreePicture(display_, src); + return NULL; + } + + // Clear the background. + XRenderColor transparent = {0}; + XRenderFillRectangle(display_, + PictOpSrc, + dst, + &transparent, + 0, + 0, + dst_width, + dst_height); + + // Calculate how much we need to scale the image. + double scale_x = static_cast(dst_width) / + static_cast(src_width); + double scale_y = static_cast(dst_height) / + static_cast(src_height); + double scale = talk_base::_min(scale_y, scale_x); + + int scaled_width = round(src_width * scale); + int scaled_height = round(src_height * scale); + + // Render the thumbnail centered on both axis. + int centered_x = (dst_width - scaled_width) / 2; + int centered_y = (dst_height - scaled_height) / 2; + + // Scaling matrix + XTransform xform = { { + { XDoubleToFixed(1), XDoubleToFixed(0), XDoubleToFixed(0) }, + { XDoubleToFixed(0), XDoubleToFixed(1), XDoubleToFixed(0) }, + { XDoubleToFixed(0), XDoubleToFixed(0), XDoubleToFixed(scale) } + } }; + XRenderSetPictureTransform(display_, src, &xform); + + // Apply filter to smooth out the image. + XRenderSetPictureFilter(display_, src, FilterBest, NULL, 0); + + // Render the image to the destination picture. + XRenderComposite(display_, + PictOpSrc, + src, + None, + dst, + 0, + 0, + 0, + 0, + centered_x, + centered_y, + scaled_width, + scaled_height); + + // Get the pixel data from the X server. TODO: XGetImage + // might be slow here, compare with ShmGetImage. + XImage* image = XGetImage(display_, + dst_pixmap, + 0, + 0, + dst_width, + dst_height, + AllPlanes, ZPixmap); + uint8* data = ArgbToRgba(reinterpret_cast(image->data), + centered_x, + centered_y, + scaled_width, + scaled_height, + dst_width, + dst_height, + false); + XDestroyImage(image); + XRenderFreePicture(display_, dst); + XFreePixmap(display_, dst_pixmap); + XRenderFreePicture(display_, src); + return data; + } + + uint8* ArgbToRgba(uint32* argb_data, int x, int y, int w, int h, + int stride_x, int stride_y, bool has_alpha) { + uint8* p; + int len = stride_x * stride_y * 4; + uint8* data = new uint8[len]; + memset(data, 0, len); + p = data + 4 * (y * stride_x + x); + for (int i = 0; i < h; ++i) { + for (int j = 0; j < w; ++j) { + uint32 argb; + uint32 rgba; + argb = argb_data[stride_x * (y + i) + x + j]; + rgba = (argb << 8) | (argb >> 24); + *p = rgba >> 24; + ++p; + *p = (rgba >> 16) & 0xff; + ++p; + *p = (rgba >> 8) & 0xff; + ++p; + *p = has_alpha ? rgba & 0xFF : 0xFF; + ++p; + } + p += (stride_x - w) * 4; + } + return data; + } + + bool EnumerateScreenWindows(WindowDescriptionList* descriptions, int screen) { + Window parent; + Window *children; + int status; + unsigned int num_children; + Window root_window = XRootWindow(display_, screen); + status = XQueryTree(display_, root_window, &root_window, &parent, &children, + &num_children); + if (status == 0) { + LOG(LS_ERROR) << "Failed to query for child windows."; + return false; + } + for (unsigned int i = 0; i < num_children; ++i) { + // Iterate in reverse order to display windows from front to back. +#ifdef CHROMEOS + // TODO(jhorwich): Short-term fix for crbug.com/120229: Don't need to + // filter, just return all windows and let the picker scan through them. + Window app_window = children[num_children - 1 - i]; +#else + Window app_window = GetApplicationWindow(children[num_children - 1 - i]); +#endif + if (app_window && + !LinuxWindowPicker::IsDesktopElement(display_, app_window)) { + std::string title; + if (GetWindowTitle(app_window, &title)) { + WindowId id(app_window); + WindowDescription desc(id, title); + descriptions->push_back(desc); + } + } + } + if (children != NULL) { + XFree(children); + } + return true; + } + + bool GetWindowTitle(Window window, std::string* title) { + int status; + bool result = false; + XTextProperty window_name; + window_name.value = NULL; + if (window) { + status = XGetWMName(display_, window, &window_name); + if (status && window_name.value && window_name.nitems) { + int cnt; + char **list = NULL; + status = Xutf8TextPropertyToTextList(display_, &window_name, &list, + &cnt); + if (status >= Success && cnt && *list) { + if (cnt > 1) { + LOG(LS_INFO) << "Window has " << cnt + << " text properties, only using the first one."; + } + *title = *list; + result = true; + } + if (list != NULL) { + XFreeStringList(list); + } + } + if (window_name.value != NULL) { + XFree(window_name.value); + } + } + return result; + } + + Window GetApplicationWindow(Window window) { + Window root, parent; + Window app_window = 0; + Window *children; + unsigned int num_children; + Atom type = None; + int format; + unsigned long nitems, after; + unsigned char *data; + + int ret = XGetWindowProperty(display_, window, + wm_state_, 0L, 2, + False, wm_state_, &type, &format, + &nitems, &after, &data); + if (ret != Success) { + LOG(LS_ERROR) << "XGetWindowProperty failed with return code " << ret + << " for window " << window << "."; + return 0; + } + if (type != None) { + int64 state = static_cast(*data); + XFree(data); + return state == NormalState ? window : 0; + } + XFree(data); + if (!XQueryTree(display_, window, &root, &parent, &children, + &num_children)) { + LOG(LS_ERROR) << "Failed to query for child windows although window" + << "does not have a valid WM_STATE."; + return 0; + } + for (unsigned int i = 0; i < num_children; ++i) { + app_window = GetApplicationWindow(children[i]); + if (app_window) { + break; + } + } + if (children != NULL) { + XFree(children); + } + return app_window; + } + + Atom wm_state_; + Atom net_wm_icon_; + Display* display_; + bool has_composite_extension_; + bool has_render_extension_; +}; + +LinuxWindowPicker::LinuxWindowPicker() : enumerator_(new XWindowEnumerator()) { +} + +LinuxWindowPicker::~LinuxWindowPicker() { +} + +bool LinuxWindowPicker::IsDesktopElement(_XDisplay* display, Window window) { + if (window == 0) { + LOG(LS_WARNING) << "Zero is never a valid window."; + return false; + } + + // First look for _NET_WM_WINDOW_TYPE. The standard + // (http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#id2760306) + // says this hint *should* be present on all windows, and we use the existence + // of _NET_WM_WINDOW_TYPE_NORMAL in the property to indicate a window is not + // a desktop element (that is, only "normal" windows should be shareable). + Atom window_type_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE", True); + XWindowProperty window_type(display, window, window_type_atom); + if (window_type.succeeded() && window_type.size() > 0) { + Atom normal_window_type_atom = XInternAtom( + display, "_NET_WM_WINDOW_TYPE_NORMAL", True); + uint32_t* end = window_type.data() + window_type.size(); + bool is_normal = (end != std::find( + window_type.data(), end, normal_window_type_atom)); + return !is_normal; + } + + // Fall back on using the hint. + XClassHint class_hint; + Status s = XGetClassHint(display, window, &class_hint); + bool result = false; + if (s == 0) { + // No hints, assume this is a normal application window. + return result; + } + static const std::string gnome_panel("gnome-panel"); + static const std::string desktop_window("desktop_window"); + + if (gnome_panel.compare(class_hint.res_name) == 0 || + desktop_window.compare(class_hint.res_name) == 0) { + result = true; + } + XFree(class_hint.res_name); + XFree(class_hint.res_class); + return result; +} + +bool LinuxWindowPicker::Init() { + return enumerator_->Init(); +} + +bool LinuxWindowPicker::GetWindowList(WindowDescriptionList* descriptions) { + return enumerator_->EnumerateWindows(descriptions); +} + +bool LinuxWindowPicker::GetDesktopList(DesktopDescriptionList* descriptions) { + return enumerator_->EnumerateDesktops(descriptions); +} + +bool LinuxWindowPicker::IsVisible(const WindowId& id) { + return enumerator_->IsVisible(id); +} + +bool LinuxWindowPicker::MoveToFront(const WindowId& id) { + return enumerator_->MoveToFront(id); +} + + +uint8* LinuxWindowPicker::GetWindowIcon(const WindowId& id, int* width, + int* height) { + return enumerator_->GetWindowIcon(id, width, height); +} + +uint8* LinuxWindowPicker::GetWindowThumbnail(const WindowId& id, int width, + int height) { + return enumerator_->GetWindowThumbnail(id, width, height); +} + +int LinuxWindowPicker::GetNumDesktops() { + return enumerator_->GetNumDesktops(); +} + +uint8* LinuxWindowPicker::GetDesktopThumbnail(const DesktopId& id, + int width, + int height) { + return enumerator_->GetDesktopThumbnail(id, width, height); +} + +bool LinuxWindowPicker::GetDesktopDimensions(const DesktopId& id, int* width, + int* height) { + return enumerator_->GetDesktopDimensions(id, width, height); +} + +} // namespace talk_base diff --git a/talk/base/linuxwindowpicker.h b/talk/base/linuxwindowpicker.h new file mode 100644 index 000000000..8e45d8f42 --- /dev/null +++ b/talk/base/linuxwindowpicker.h @@ -0,0 +1,68 @@ +/* + * libjingle + * Copyright 2010 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. + */ + +#ifndef TALK_BASE_LINUXWINDOWPICKER_H_ +#define TALK_BASE_LINUXWINDOWPICKER_H_ + +#include "talk/base/basictypes.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/windowpicker.h" + +// Avoid include . +struct _XDisplay; +typedef unsigned long Window; + +namespace talk_base { + +class XWindowEnumerator; + +class LinuxWindowPicker : public WindowPicker { + public: + LinuxWindowPicker(); + ~LinuxWindowPicker(); + + static bool IsDesktopElement(_XDisplay* display, Window window); + + virtual bool Init(); + virtual bool IsVisible(const WindowId& id); + virtual bool MoveToFront(const WindowId& id); + virtual bool GetWindowList(WindowDescriptionList* descriptions); + virtual bool GetDesktopList(DesktopDescriptionList* descriptions); + virtual bool GetDesktopDimensions(const DesktopId& id, int* width, + int* height); + uint8* GetWindowIcon(const WindowId& id, int* width, int* height); + uint8* GetWindowThumbnail(const WindowId& id, int width, int height); + int GetNumDesktops(); + uint8* GetDesktopThumbnail(const DesktopId& id, int width, int height); + + private: + scoped_ptr enumerator_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_LINUXWINDOWPICKER_H_ diff --git a/talk/base/linuxwindowpicker_unittest.cc b/talk/base/linuxwindowpicker_unittest.cc new file mode 100644 index 000000000..5ea9c93fd --- /dev/null +++ b/talk/base/linuxwindowpicker_unittest.cc @@ -0,0 +1,54 @@ +/* + * libjingle + * Copyright 2010 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/base/gunit.h" +#include "talk/base/linuxwindowpicker.h" +#include "talk/base/logging.h" +#include "talk/base/windowpicker.h" + +#ifndef LINUX +#error Only for Linux +#endif + +namespace talk_base { + +TEST(LinuxWindowPickerTest, TestGetWindowList) { + LinuxWindowPicker window_picker; + WindowDescriptionList descriptions; + window_picker.Init(); + window_picker.GetWindowList(&descriptions); +} + +TEST(LinuxWindowPickerTest, TestGetDesktopList) { + LinuxWindowPicker window_picker; + DesktopDescriptionList descriptions; + EXPECT_TRUE(window_picker.Init()); + EXPECT_TRUE(window_picker.GetDesktopList(&descriptions)); + EXPECT_TRUE(descriptions.size() > 0); +} + +} // namespace talk_base diff --git a/talk/base/logging.cc b/talk/base/logging.cc new file mode 100644 index 000000000..6653d3451 --- /dev/null +++ b/talk/base/logging.cc @@ -0,0 +1,635 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifdef WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#define snprintf _snprintf +#undef ERROR // wingdi.h +#endif + +#ifdef OSX +#include +#elif defined(ANDROID) +#include +static const char kLibjingle[] = "libjingle"; +// Android has a 1024 limit on log inputs. We use 60 chars as an +// approx for the header/tag portion. +// See android/system/core/liblog/logd_write.c +static const int kMaxLogLineSize = 1024 - 60; +#endif // OSX || ANDROID + +#include + +#include +#include +#include +#include + +#include "talk/base/logging.h" +#include "talk/base/stream.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" +#include "talk/base/timeutils.h" + +namespace talk_base { + +///////////////////////////////////////////////////////////////////////////// +// Constant Labels +///////////////////////////////////////////////////////////////////////////// + +const char * FindLabel(int value, const ConstantLabel entries[]) { + for (int i = 0; entries[i].label; ++i) { + if (value == entries[i].value) { + return entries[i].label; + } + } + return 0; +} + +std::string ErrorName(int err, const ConstantLabel * err_table) { + if (err == 0) + return "No error"; + + if (err_table != 0) { + if (const char * value = FindLabel(err, err_table)) + return value; + } + + char buffer[16]; + snprintf(buffer, sizeof(buffer), "0x%08x", err); + return buffer; +} + +///////////////////////////////////////////////////////////////////////////// +// LogMessage +///////////////////////////////////////////////////////////////////////////// + +const int LogMessage::NO_LOGGING = LS_ERROR + 1; + +#if _DEBUG +static const int LOG_DEFAULT = LS_INFO; +#else // !_DEBUG +static const int LOG_DEFAULT = LogMessage::NO_LOGGING; +#endif // !_DEBUG + +// Global lock for log subsystem, only needed to serialize access to streams_. +CriticalSection LogMessage::crit_; + +// By default, release builds don't log, debug builds at info level +int LogMessage::min_sev_ = LOG_DEFAULT; +int LogMessage::dbg_sev_ = LOG_DEFAULT; + +// Don't bother printing context for the ubiquitous INFO log messages +int LogMessage::ctx_sev_ = LS_WARNING; + +// The list of logging streams currently configured. +// Note: we explicitly do not clean this up, because of the uncertain ordering +// of destructors at program exit. Let the person who sets the stream trigger +// cleanup by setting to NULL, or let it leak (safe at program exit). +LogMessage::StreamList LogMessage::streams_; + +// Boolean options default to false (0) +bool LogMessage::thread_, LogMessage::timestamp_; + +// If we're in diagnostic mode, we'll be explicitly set that way; default=false. +bool LogMessage::is_diagnostic_mode_ = false; + +LogMessage::LogMessage(const char* file, int line, LoggingSeverity sev, + LogErrorContext err_ctx, int err, const char* module) + : severity_(sev), + warn_slow_logs_delay_(WARN_SLOW_LOGS_DELAY) { + // Android's logging facility keeps track of timestamp and thread. +#ifndef ANDROID + if (timestamp_) { + uint32 time = TimeSince(LogStartTime()); + // Also ensure WallClockStartTime is initialized, so that it matches + // LogStartTime. + WallClockStartTime(); + print_stream_ << "[" << std::setfill('0') << std::setw(3) << (time / 1000) + << ":" << std::setw(3) << (time % 1000) << std::setfill(' ') + << "] "; + } + + if (thread_) { +#ifdef WIN32 + DWORD id = GetCurrentThreadId(); + print_stream_ << "[" << std::hex << id << std::dec << "] "; +#endif // WIN32 + } +#endif // !ANDROID + + if (severity_ >= ctx_sev_) { + print_stream_ << Describe(sev) << "(" << DescribeFile(file) + << ":" << line << "): "; + } + + if (err_ctx != ERRCTX_NONE) { + std::ostringstream tmp; + tmp << "[0x" << std::setfill('0') << std::hex << std::setw(8) << err << "]"; + switch (err_ctx) { + case ERRCTX_ERRNO: + tmp << " " << strerror(err); + break; +#if WIN32 + case ERRCTX_HRESULT: { + char msgbuf[256]; + DWORD flags = FORMAT_MESSAGE_FROM_SYSTEM; + HMODULE hmod = GetModuleHandleA(module); + if (hmod) + flags |= FORMAT_MESSAGE_FROM_HMODULE; + if (DWORD len = FormatMessageA( + flags, hmod, err, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + msgbuf, sizeof(msgbuf) / sizeof(msgbuf[0]), NULL)) { + while ((len > 0) && + isspace(static_cast(msgbuf[len-1]))) { + msgbuf[--len] = 0; + } + tmp << " " << msgbuf; + } + break; + } +#endif // WIN32 +#if OSX + case ERRCTX_OSSTATUS: { + tmp << " " << nonnull(GetMacOSStatusErrorString(err), "Unknown error"); + if (const char* desc = GetMacOSStatusCommentString(err)) { + tmp << ": " << desc; + } + break; + } +#endif // OSX + default: + break; + } + extra_ = tmp.str(); + } +} + +LogMessage::~LogMessage() { + if (!extra_.empty()) + print_stream_ << " : " << extra_; + print_stream_ << std::endl; + + const std::string& str = print_stream_.str(); + if (severity_ >= dbg_sev_) { + OutputToDebug(str, severity_); + } + + uint32 before = Time(); + // Must lock streams_ before accessing + CritScope cs(&crit_); + for (StreamList::iterator it = streams_.begin(); it != streams_.end(); ++it) { + if (severity_ >= it->second) { + OutputToStream(it->first, str); + } + } + uint32 delay = TimeSince(before); + if (delay >= warn_slow_logs_delay_) { + LogMessage slow_log_warning = + talk_base::LogMessage(__FILE__, __LINE__, LS_WARNING); + // If our warning is slow, we don't want to warn about it, because + // that would lead to inifinite recursion. So, give a really big + // number for the delay threshold. + slow_log_warning.warn_slow_logs_delay_ = UINT_MAX; + slow_log_warning.stream() << "Slow log: took " << delay << "ms to write " + << str.size() << " bytes."; + } +} + +uint32 LogMessage::LogStartTime() { + static const uint32 g_start = Time(); + return g_start; +} + +uint32 LogMessage::WallClockStartTime() { + static const uint32 g_start_wallclock = time(NULL); + return g_start_wallclock; +} + +void LogMessage::LogContext(int min_sev) { + ctx_sev_ = min_sev; +} + +void LogMessage::LogThreads(bool on) { + thread_ = on; +} + +void LogMessage::LogTimestamps(bool on) { + timestamp_ = on; +} + +void LogMessage::LogToDebug(int min_sev) { + dbg_sev_ = min_sev; + UpdateMinLogSeverity(); +} + +void LogMessage::LogToStream(StreamInterface* stream, int min_sev) { + CritScope cs(&crit_); + // Discard and delete all previously installed streams + for (StreamList::iterator it = streams_.begin(); it != streams_.end(); ++it) { + delete it->first; + } + streams_.clear(); + // Install the new stream, if specified + if (stream) { + AddLogToStream(stream, min_sev); + } +} + +int LogMessage::GetLogToStream(StreamInterface* stream) { + CritScope cs(&crit_); + int sev = NO_LOGGING; + for (StreamList::iterator it = streams_.begin(); it != streams_.end(); ++it) { + if (!stream || stream == it->first) { + sev = _min(sev, it->second); + } + } + return sev; +} + +void LogMessage::AddLogToStream(StreamInterface* stream, int min_sev) { + CritScope cs(&crit_); + streams_.push_back(std::make_pair(stream, min_sev)); + UpdateMinLogSeverity(); +} + +void LogMessage::RemoveLogToStream(StreamInterface* stream) { + CritScope cs(&crit_); + for (StreamList::iterator it = streams_.begin(); it != streams_.end(); ++it) { + if (stream == it->first) { + streams_.erase(it); + break; + } + } + UpdateMinLogSeverity(); +} + +void LogMessage::ConfigureLogging(const char* params, const char* filename) { + int current_level = LS_VERBOSE; + int debug_level = GetLogToDebug(); + int file_level = GetLogToStream(); + + std::vector tokens; + tokenize(params, ' ', &tokens); + + for (size_t i = 0; i < tokens.size(); ++i) { + if (tokens[i].empty()) + continue; + + // Logging features + if (tokens[i] == "tstamp") { + LogTimestamps(); + } else if (tokens[i] == "thread") { + LogThreads(); + + // Logging levels + } else if (tokens[i] == "sensitive") { + current_level = LS_SENSITIVE; + } else if (tokens[i] == "verbose") { + current_level = LS_VERBOSE; + } else if (tokens[i] == "info") { + current_level = LS_INFO; + } else if (tokens[i] == "warning") { + current_level = LS_WARNING; + } else if (tokens[i] == "error") { + current_level = LS_ERROR; + } else if (tokens[i] == "none") { + current_level = NO_LOGGING; + + // Logging targets + } else if (tokens[i] == "file") { + file_level = current_level; + } else if (tokens[i] == "debug") { + debug_level = current_level; + } + } + +#ifdef WIN32 + if ((NO_LOGGING != debug_level) && !::IsDebuggerPresent()) { + // First, attempt to attach to our parent's console... so if you invoke + // from the command line, we'll see the output there. Otherwise, create + // our own console window. + // Note: These methods fail if a console already exists, which is fine. + bool success = false; + typedef BOOL (WINAPI* PFN_AttachConsole)(DWORD); + if (HINSTANCE kernel32 = ::LoadLibrary(L"kernel32.dll")) { + // AttachConsole is defined on WinXP+. + if (PFN_AttachConsole attach_console = reinterpret_cast + (::GetProcAddress(kernel32, "AttachConsole"))) { + success = (FALSE != attach_console(ATTACH_PARENT_PROCESS)); + } + ::FreeLibrary(kernel32); + } + if (!success) { + ::AllocConsole(); + } + } +#endif // WIN32 + + scoped_ptr stream; + if (NO_LOGGING != file_level) { + stream.reset(new FileStream); + if (!stream->Open(filename, "wb", NULL) || !stream->DisableBuffering()) { + stream.reset(); + } + } + + LogToDebug(debug_level); + LogToStream(stream.release(), file_level); +} + +int LogMessage::ParseLogSeverity(const std::string& value) { + int level = NO_LOGGING; + if (value == "LS_SENSITIVE") { + level = LS_SENSITIVE; + } else if (value == "LS_VERBOSE") { + level = LS_VERBOSE; + } else if (value == "LS_INFO") { + level = LS_INFO; + } else if (value == "LS_WARNING") { + level = LS_WARNING; + } else if (value == "LS_ERROR") { + level = LS_ERROR; + } else if (isdigit(value[0])) { + level = atoi(value.c_str()); // NOLINT + } + return level; +} + +void LogMessage::UpdateMinLogSeverity() { + int min_sev = dbg_sev_; + for (StreamList::iterator it = streams_.begin(); it != streams_.end(); ++it) { + min_sev = _min(dbg_sev_, it->second); + } + min_sev_ = min_sev; +} + +const char* LogMessage::Describe(LoggingSeverity sev) { + switch (sev) { + case LS_SENSITIVE: return "Sensitive"; + case LS_VERBOSE: return "Verbose"; + case LS_INFO: return "Info"; + case LS_WARNING: return "Warning"; + case LS_ERROR: return "Error"; + default: return ""; + } +} + +const char* LogMessage::DescribeFile(const char* file) { + const char* end1 = ::strrchr(file, '/'); + const char* end2 = ::strrchr(file, '\\'); + if (!end1 && !end2) + return file; + else + return (end1 > end2) ? end1 + 1 : end2 + 1; +} + +void LogMessage::OutputToDebug(const std::string& str, + LoggingSeverity severity) { + bool log_to_stderr = true; +#if defined(OSX) && (!defined(DEBUG) || defined(NDEBUG)) + // On the Mac, all stderr output goes to the Console log and causes clutter. + // So in opt builds, don't log to stderr unless the user specifically sets + // a preference to do so. + CFStringRef key = CFStringCreateWithCString(kCFAllocatorDefault, + "logToStdErr", + kCFStringEncodingUTF8); + CFStringRef domain = CFBundleGetIdentifier(CFBundleGetMainBundle()); + if (key != NULL && domain != NULL) { + Boolean exists_and_is_valid; + Boolean should_log = + CFPreferencesGetAppBooleanValue(key, domain, &exists_and_is_valid); + // If the key doesn't exist or is invalid or is false, we will not log to + // stderr. + log_to_stderr = exists_and_is_valid && should_log; + } + if (key != NULL) { + CFRelease(key); + } +#endif +#ifdef WIN32 + // Always log to the debugger. + // Perhaps stderr should be controlled by a preference, as on Mac? + OutputDebugStringA(str.c_str()); + if (log_to_stderr) { + // This handles dynamically allocated consoles, too. + if (HANDLE error_handle = ::GetStdHandle(STD_ERROR_HANDLE)) { + log_to_stderr = false; + DWORD written = 0; + ::WriteFile(error_handle, str.data(), static_cast(str.size()), + &written, 0); + } + } +#endif // WIN32 +#ifdef ANDROID + // Android's logging facility uses severity to log messages but we + // need to map libjingle's severity levels to Android ones first. + // Also write to stderr which maybe available to executable started + // from the shell. + int prio; + switch (severity) { + case LS_SENSITIVE: + __android_log_write(ANDROID_LOG_INFO, kLibjingle, "SENSITIVE"); + if (log_to_stderr) { + fprintf(stderr, "SENSITIVE"); + fflush(stderr); + } + return; + case LS_VERBOSE: + prio = ANDROID_LOG_VERBOSE; + break; + case LS_INFO: + prio = ANDROID_LOG_INFO; + break; + case LS_WARNING: + prio = ANDROID_LOG_WARN; + break; + case LS_ERROR: + prio = ANDROID_LOG_ERROR; + break; + default: + prio = ANDROID_LOG_UNKNOWN; + } + + int size = str.size(); + int line = 0; + int idx = 0; + const int max_lines = size / kMaxLogLineSize + 1; + if (max_lines == 1) { + __android_log_print(prio, kLibjingle, "%.*s", size, str.c_str()); + } else { + while (size > 0) { + const int len = std::min(size, kMaxLogLineSize); + // Use the size of the string in the format (str may have \0 in the + // middle). + __android_log_print(prio, kLibjingle, "[%d/%d] %.*s", + line + 1, max_lines, + len, str.c_str() + idx); + idx += len; + size -= len; + ++line; + } + } +#endif // ANDROID + if (log_to_stderr) { + fprintf(stderr, "%s", str.c_str()); + fflush(stderr); + } +} + +void LogMessage::OutputToStream(StreamInterface* stream, + const std::string& str) { + // If write isn't fully successful, what are we going to do, log it? :) + stream->WriteAll(str.data(), str.size(), NULL, NULL); +} + +////////////////////////////////////////////////////////////////////// +// Logging Helpers +////////////////////////////////////////////////////////////////////// + +void LogMultiline(LoggingSeverity level, const char* label, bool input, + const void* data, size_t len, bool hex_mode, + LogMultilineState* state) { + if (!LOG_CHECK_LEVEL_V(level)) + return; + + const char * direction = (input ? " << " : " >> "); + + // NULL data means to flush our count of unprintable characters. + if (!data) { + if (state && state->unprintable_count_[input]) { + LOG_V(level) << label << direction << "## " + << state->unprintable_count_[input] + << " consecutive unprintable ##"; + state->unprintable_count_[input] = 0; + } + return; + } + + // The ctype classification functions want unsigned chars. + const unsigned char* udata = static_cast(data); + + if (hex_mode) { + const size_t LINE_SIZE = 24; + char hex_line[LINE_SIZE * 9 / 4 + 2], asc_line[LINE_SIZE + 1]; + while (len > 0) { + memset(asc_line, ' ', sizeof(asc_line)); + memset(hex_line, ' ', sizeof(hex_line)); + size_t line_len = _min(len, LINE_SIZE); + for (size_t i = 0; i < line_len; ++i) { + unsigned char ch = udata[i]; + asc_line[i] = isprint(ch) ? ch : '.'; + hex_line[i*2 + i/4] = hex_encode(ch >> 4); + hex_line[i*2 + i/4 + 1] = hex_encode(ch & 0xf); + } + asc_line[sizeof(asc_line)-1] = 0; + hex_line[sizeof(hex_line)-1] = 0; + LOG_V(level) << label << direction + << asc_line << " " << hex_line << " "; + udata += line_len; + len -= line_len; + } + return; + } + + size_t consecutive_unprintable = state ? state->unprintable_count_[input] : 0; + + const unsigned char* end = udata + len; + while (udata < end) { + const unsigned char* line = udata; + const unsigned char* end_of_line = strchrn(udata, + end - udata, + '\n'); + if (!end_of_line) { + udata = end_of_line = end; + } else { + udata = end_of_line + 1; + } + + bool is_printable = true; + + // If we are in unprintable mode, we need to see a line of at least + // kMinPrintableLine characters before we'll switch back. + const ptrdiff_t kMinPrintableLine = 4; + if (consecutive_unprintable && ((end_of_line - line) < kMinPrintableLine)) { + is_printable = false; + } else { + // Determine if the line contains only whitespace and printable + // characters. + bool is_entirely_whitespace = true; + for (const unsigned char* pos = line; pos < end_of_line; ++pos) { + if (isspace(*pos)) + continue; + is_entirely_whitespace = false; + if (!isprint(*pos)) { + is_printable = false; + break; + } + } + // Treat an empty line following unprintable data as unprintable. + if (consecutive_unprintable && is_entirely_whitespace) { + is_printable = false; + } + } + if (!is_printable) { + consecutive_unprintable += (udata - line); + continue; + } + // Print out the current line, but prefix with a count of prior unprintable + // characters. + if (consecutive_unprintable) { + LOG_V(level) << label << direction << "## " << consecutive_unprintable + << " consecutive unprintable ##"; + consecutive_unprintable = 0; + } + // Strip off trailing whitespace. + while ((end_of_line > line) && isspace(*(end_of_line-1))) { + --end_of_line; + } + // Filter out any private data + std::string substr(reinterpret_cast(line), end_of_line - line); + std::string::size_type pos_private = substr.find("Email"); + if (pos_private == std::string::npos) { + pos_private = substr.find("Passwd"); + } + if (pos_private == std::string::npos) { + LOG_V(level) << label << direction << substr; + } else { + LOG_V(level) << label << direction << "## omitted for privacy ##"; + } + } + + if (state) { + state->unprintable_count_[input] = consecutive_unprintable; + } +} + +////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/logging.h b/talk/base/logging.h new file mode 100644 index 000000000..2f341fa78 --- /dev/null +++ b/talk/base/logging.h @@ -0,0 +1,389 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +// LOG(...) an ostream target that can be used to send formatted +// output to a variety of logging targets, such as debugger console, stderr, +// file, or any StreamInterface. +// The severity level passed as the first argument to the LOGging +// functions is used as a filter, to limit the verbosity of the logging. +// Static members of LogMessage documented below are used to control the +// verbosity and target of the output. +// There are several variations on the LOG macro which facilitate logging +// of common error conditions, detailed below. + +// LOG(sev) logs the given stream at severity "sev", which must be a +// compile-time constant of the LoggingSeverity type, without the namespace +// prefix. +// LOG_V(sev) Like LOG(), but sev is a run-time variable of the LoggingSeverity +// type (basically, it just doesn't prepend the namespace). +// LOG_F(sev) Like LOG(), but includes the name of the current function. +// LOG_GLE(M)(sev [, mod]) attempt to add a string description of the +// HRESULT returned by GetLastError. The "M" variant allows searching of a +// DLL's string table for the error description. +// LOG_ERRNO(sev) attempts to add a string description of an errno-derived +// error. errno and associated facilities exist on both Windows and POSIX, +// but on Windows they only apply to the C/C++ runtime. +// LOG_ERR(sev) is an alias for the platform's normal error system, i.e. _GLE on +// Windows and _ERRNO on POSIX. +// (The above three also all have _EX versions that let you specify the error +// code, rather than using the last one.) +// LOG_E(sev, ctx, err, ...) logs a detailed error interpreted using the +// specified context. +// LOG_CHECK_LEVEL(sev) (and LOG_CHECK_LEVEL_V(sev)) can be used as a test +// before performing expensive or sensitive operations whose sole purpose is +// to output logging data at the desired level. +// Lastly, PLOG(sev, err) is an alias for LOG_ERR_EX. + +#ifndef TALK_BASE_LOGGING_H_ +#define TALK_BASE_LOGGING_H_ + +#ifdef HAVE_CONFIG_H +#include "config.h" // NOLINT +#endif + +#include +#include +#include +#include +#include "talk/base/basictypes.h" +#include "talk/base/criticalsection.h" + +namespace talk_base { + +class StreamInterface; + +/////////////////////////////////////////////////////////////////////////////// +// ConstantLabel can be used to easily generate string names from constant +// values. This can be useful for logging descriptive names of error messages. +// Usage: +// const ConstantLabel LIBRARY_ERRORS[] = { +// KLABEL(SOME_ERROR), +// KLABEL(SOME_OTHER_ERROR), +// ... +// LASTLABEL +// } +// +// int err = LibraryFunc(); +// LOG(LS_ERROR) << "LibraryFunc returned: " +// << ErrorName(err, LIBRARY_ERRORS); + +struct ConstantLabel { int value; const char * label; }; +#define KLABEL(x) { x, #x } +#define TLABEL(x, y) { x, y } +#define LASTLABEL { 0, 0 } + +const char * FindLabel(int value, const ConstantLabel entries[]); +std::string ErrorName(int err, const ConstantLabel* err_table); + +////////////////////////////////////////////////////////////////////// + +// Note that the non-standard LoggingSeverity aliases exist because they are +// still in broad use. The meanings of the levels are: +// LS_SENSITIVE: Information which should only be logged with the consent +// of the user, due to privacy concerns. +// LS_VERBOSE: This level is for data which we do not want to appear in the +// normal debug log, but should appear in diagnostic logs. +// LS_INFO: Chatty level used in debugging for all sorts of things, the default +// in debug builds. +// LS_WARNING: Something that may warrant investigation. +// LS_ERROR: Something that should not have occurred. +enum LoggingSeverity { LS_SENSITIVE, LS_VERBOSE, LS_INFO, LS_WARNING, LS_ERROR, + INFO = LS_INFO, + WARNING = LS_WARNING, + LERROR = LS_ERROR }; + +// LogErrorContext assists in interpreting the meaning of an error value. +enum LogErrorContext { + ERRCTX_NONE, + ERRCTX_ERRNO, // System-local errno + ERRCTX_HRESULT, // Windows HRESULT + ERRCTX_OSSTATUS, // MacOS OSStatus + + // Abbreviations for LOG_E macro + ERRCTX_EN = ERRCTX_ERRNO, // LOG_E(sev, EN, x) + ERRCTX_HR = ERRCTX_HRESULT, // LOG_E(sev, HR, x) + ERRCTX_OS = ERRCTX_OSSTATUS, // LOG_E(sev, OS, x) +}; + +class LogMessage { + public: + static const int NO_LOGGING; + static const uint32 WARN_SLOW_LOGS_DELAY = 50; // ms + + LogMessage(const char* file, int line, LoggingSeverity sev, + LogErrorContext err_ctx = ERRCTX_NONE, int err = 0, + const char* module = NULL); + ~LogMessage(); + + static inline bool Loggable(LoggingSeverity sev) { return (sev >= min_sev_); } + std::ostream& stream() { return print_stream_; } + + // Returns the time at which this function was called for the first time. + // The time will be used as the logging start time. + // If this is not called externally, the LogMessage ctor also calls it, in + // which case the logging start time will be the time of the first LogMessage + // instance is created. + static uint32 LogStartTime(); + + // Returns the wall clock equivalent of |LogStartTime|, in seconds from the + // epoch. + static uint32 WallClockStartTime(); + + // These are attributes which apply to all logging channels + // LogContext: Display the file and line number of the message + static void LogContext(int min_sev); + // LogThreads: Display the thread identifier of the current thread + static void LogThreads(bool on = true); + // LogTimestamps: Display the elapsed time of the program + static void LogTimestamps(bool on = true); + + // These are the available logging channels + // Debug: Debug console on Windows, otherwise stderr + static void LogToDebug(int min_sev); + static int GetLogToDebug() { return dbg_sev_; } + + // Stream: Any non-blocking stream interface. LogMessage takes ownership of + // the stream. Multiple streams may be specified by using AddLogToStream. + // LogToStream is retained for backwards compatibility; when invoked, it + // will discard any previously set streams and install the specified stream. + // GetLogToStream gets the severity for the specified stream, of if none + // is specified, the minimum stream severity. + // RemoveLogToStream removes the specified stream, without destroying it. + static void LogToStream(StreamInterface* stream, int min_sev); + static int GetLogToStream(StreamInterface* stream = NULL); + static void AddLogToStream(StreamInterface* stream, int min_sev); + static void RemoveLogToStream(StreamInterface* stream); + + // Testing against MinLogSeverity allows code to avoid potentially expensive + // logging operations by pre-checking the logging level. + static int GetMinLogSeverity() { return min_sev_; } + + static void SetDiagnosticMode(bool f) { is_diagnostic_mode_ = f; } + static bool IsDiagnosticMode() { return is_diagnostic_mode_; } + + // Parses the provided parameter stream to configure the options above. + // Useful for configuring logging from the command line. If file logging + // is enabled, it is output to the specified filename. + static void ConfigureLogging(const char* params, const char* filename); + + // Convert the string to a LS_ value; also accept numeric values. + static int ParseLogSeverity(const std::string& value); + + private: + typedef std::list > StreamList; + + // Updates min_sev_ appropriately when debug sinks change. + static void UpdateMinLogSeverity(); + + // These assist in formatting some parts of the debug output. + static const char* Describe(LoggingSeverity sev); + static const char* DescribeFile(const char* file); + + // These write out the actual log messages. + static void OutputToDebug(const std::string& msg, LoggingSeverity severity_); + static void OutputToStream(StreamInterface* stream, const std::string& msg); + + // The ostream that buffers the formatted message before output + std::ostringstream print_stream_; + + // The severity level of this message + LoggingSeverity severity_; + + // String data generated in the constructor, that should be appended to + // the message before output. + std::string extra_; + + // If time it takes to write to stream is more than this, log one + // additional warning about it. + uint32 warn_slow_logs_delay_; + + // Global lock for the logging subsystem + static CriticalSection crit_; + + // dbg_sev_ is the thresholds for those output targets + // min_sev_ is the minimum (most verbose) of those levels, and is used + // as a short-circuit in the logging macros to identify messages that won't + // be logged. + // ctx_sev_ is the minimum level at which file context is displayed + static int min_sev_, dbg_sev_, ctx_sev_; + + // The output streams and their associated severities + static StreamList streams_; + + // Flags for formatting options + static bool thread_, timestamp_; + + // are we in diagnostic mode (as defined by the app)? + static bool is_diagnostic_mode_; + + DISALLOW_EVIL_CONSTRUCTORS(LogMessage); +}; + +////////////////////////////////////////////////////////////////////// +// Logging Helpers +////////////////////////////////////////////////////////////////////// + +class LogMultilineState { + public: + size_t unprintable_count_[2]; + LogMultilineState() { + unprintable_count_[0] = unprintable_count_[1] = 0; + } +}; + +// When possible, pass optional state variable to track various data across +// multiple calls to LogMultiline. Otherwise, pass NULL. +void LogMultiline(LoggingSeverity level, const char* label, bool input, + const void* data, size_t len, bool hex_mode, + LogMultilineState* state); + +////////////////////////////////////////////////////////////////////// +// Macros which automatically disable logging when LOGGING == 0 +////////////////////////////////////////////////////////////////////// + +// If LOGGING is not explicitly defined, default to enabled in debug mode +#if !defined(LOGGING) +#if defined(_DEBUG) && !defined(NDEBUG) +#define LOGGING 1 +#else +#define LOGGING 0 +#endif +#endif // !defined(LOGGING) + +#ifndef LOG +#if LOGGING + +// The following non-obvious technique for implementation of a +// conditional log stream was stolen from google3/base/logging.h. + +// This class is used to explicitly ignore values in the conditional +// logging macros. This avoids compiler warnings like "value computed +// is not used" and "statement has no effect". + +class LogMessageVoidify { + public: + LogMessageVoidify() { } + // This has to be an operator with a precedence lower than << but + // higher than ?: + void operator&(std::ostream&) { } +}; + +#define LOG_SEVERITY_PRECONDITION(sev) \ + !(talk_base::LogMessage::Loggable(sev)) \ + ? (void) 0 \ + : talk_base::LogMessageVoidify() & + +#define LOG(sev) \ + LOG_SEVERITY_PRECONDITION(talk_base::sev) \ + talk_base::LogMessage(__FILE__, __LINE__, talk_base::sev).stream() + +// The _V version is for when a variable is passed in. It doesn't do the +// namespace concatination. +#define LOG_V(sev) \ + LOG_SEVERITY_PRECONDITION(sev) \ + talk_base::LogMessage(__FILE__, __LINE__, sev).stream() + +// The _F version prefixes the message with the current function name. +#if (defined(__GNUC__) && defined(_DEBUG)) || defined(WANT_PRETTY_LOG_F) +#define LOG_F(sev) LOG(sev) << __PRETTY_FUNCTION__ << ": " +#else +#define LOG_F(sev) LOG(sev) << __FUNCTION__ << ": " +#endif + +#define LOG_CHECK_LEVEL(sev) \ + talk_base::LogCheckLevel(talk_base::sev) +#define LOG_CHECK_LEVEL_V(sev) \ + talk_base::LogCheckLevel(sev) +inline bool LogCheckLevel(LoggingSeverity sev) { + return (LogMessage::GetMinLogSeverity() <= sev); +} + +#define LOG_E(sev, ctx, err, ...) \ + LOG_SEVERITY_PRECONDITION(talk_base::sev) \ + talk_base::LogMessage(__FILE__, __LINE__, talk_base::sev, \ + talk_base::ERRCTX_ ## ctx, err , ##__VA_ARGS__) \ + .stream() + +#else // !LOGGING + +// Hopefully, the compiler will optimize away some of this code. +// Note: syntax of "1 ? (void)0 : LogMessage" was causing errors in g++, +// converted to "while (false)" +#define LOG(sev) \ + while (false)talk_base:: LogMessage(NULL, 0, talk_base::sev).stream() +#define LOG_V(sev) \ + while (false) talk_base::LogMessage(NULL, 0, sev).stream() +#define LOG_F(sev) LOG(sev) << __FUNCTION__ << ": " +#define LOG_CHECK_LEVEL(sev) \ + false +#define LOG_CHECK_LEVEL_V(sev) \ + false + +#define LOG_E(sev, ctx, err, ...) \ + while (false) talk_base::LogMessage(__FILE__, __LINE__, talk_base::sev, \ + talk_base::ERRCTX_ ## ctx, err , ##__VA_ARGS__) \ + .stream() + +#endif // !LOGGING + +#define LOG_ERRNO_EX(sev, err) \ + LOG_E(sev, ERRNO, err) +#define LOG_ERRNO(sev) \ + LOG_ERRNO_EX(sev, errno) + +#ifdef WIN32 +#define LOG_GLE_EX(sev, err) \ + LOG_E(sev, HRESULT, err) +#define LOG_GLE(sev) \ + LOG_GLE_EX(sev, GetLastError()) +#define LOG_GLEM(sev, mod) \ + LOG_E(sev, HRESULT, GetLastError(), mod) +#define LOG_ERR_EX(sev, err) \ + LOG_GLE_EX(sev, err) +#define LOG_ERR(sev) \ + LOG_GLE(sev) +#define LAST_SYSTEM_ERROR \ + (::GetLastError()) +#elif POSIX +#define LOG_ERR_EX(sev, err) \ + LOG_ERRNO_EX(sev, err) +#define LOG_ERR(sev) \ + LOG_ERRNO(sev) +#define LAST_SYSTEM_ERROR \ + (errno) +#endif // WIN32 + +#define PLOG(sev, err) \ + LOG_ERR_EX(sev, err) + +// TODO(?): Add an "assert" wrapper that logs in the same manner. + +#endif // LOG + +} // namespace talk_base + +#endif // TALK_BASE_LOGGING_H_ diff --git a/talk/base/logging_unittest.cc b/talk/base/logging_unittest.cc new file mode 100644 index 000000000..b0c219fa3 --- /dev/null +++ b/talk/base/logging_unittest.cc @@ -0,0 +1,149 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/fileutils.h" +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/pathutils.h" +#include "talk/base/stream.h" +#include "talk/base/thread.h" + +namespace talk_base { + +// Test basic logging operation. We should get the INFO log but not the VERBOSE. +// We should restore the correct global state at the end. +TEST(LogTest, SingleStream) { + int sev = LogMessage::GetLogToStream(NULL); + + std::string str; + StringStream stream(str); + LogMessage::AddLogToStream(&stream, LS_INFO); + EXPECT_EQ(LS_INFO, LogMessage::GetLogToStream(&stream)); + + LOG(LS_INFO) << "INFO"; + LOG(LS_VERBOSE) << "VERBOSE"; + EXPECT_NE(std::string::npos, str.find("INFO")); + EXPECT_EQ(std::string::npos, str.find("VERBOSE")); + + LogMessage::RemoveLogToStream(&stream); + EXPECT_EQ(LogMessage::NO_LOGGING, LogMessage::GetLogToStream(&stream)); + + EXPECT_EQ(sev, LogMessage::GetLogToStream(NULL)); +} + +// Test using multiple log streams. The INFO stream should get the INFO message, +// the VERBOSE stream should get the INFO and the VERBOSE. +// We should restore the correct global state at the end. +TEST(LogTest, MultipleStreams) { + int sev = LogMessage::GetLogToStream(NULL); + + std::string str1, str2; + StringStream stream1(str1), stream2(str2); + LogMessage::AddLogToStream(&stream1, LS_INFO); + LogMessage::AddLogToStream(&stream2, LS_VERBOSE); + EXPECT_EQ(LS_INFO, LogMessage::GetLogToStream(&stream1)); + EXPECT_EQ(LS_VERBOSE, LogMessage::GetLogToStream(&stream2)); + + LOG(LS_INFO) << "INFO"; + LOG(LS_VERBOSE) << "VERBOSE"; + + EXPECT_NE(std::string::npos, str1.find("INFO")); + EXPECT_EQ(std::string::npos, str1.find("VERBOSE")); + EXPECT_NE(std::string::npos, str2.find("INFO")); + EXPECT_NE(std::string::npos, str2.find("VERBOSE")); + + LogMessage::RemoveLogToStream(&stream2); + LogMessage::RemoveLogToStream(&stream1); + EXPECT_EQ(LogMessage::NO_LOGGING, LogMessage::GetLogToStream(&stream2)); + EXPECT_EQ(LogMessage::NO_LOGGING, LogMessage::GetLogToStream(&stream1)); + + EXPECT_EQ(sev, LogMessage::GetLogToStream(NULL)); +} + +// Ensure we don't crash when adding/removing streams while threads are going. +// We should restore the correct global state at the end. +class LogThread : public Thread { + void Run() { + // LS_SENSITIVE to avoid cluttering up any real logging going on + LOG(LS_SENSITIVE) << "LOG"; + } +}; + +TEST(LogTest, MultipleThreads) { + int sev = LogMessage::GetLogToStream(NULL); + + LogThread thread1, thread2, thread3; + thread1.Start(); + thread2.Start(); + thread3.Start(); + + NullStream stream1, stream2, stream3; + for (int i = 0; i < 1000; ++i) { + LogMessage::AddLogToStream(&stream1, LS_INFO); + LogMessage::AddLogToStream(&stream2, LS_VERBOSE); + LogMessage::AddLogToStream(&stream3, LS_SENSITIVE); + LogMessage::RemoveLogToStream(&stream1); + LogMessage::RemoveLogToStream(&stream2); + LogMessage::RemoveLogToStream(&stream3); + } + + EXPECT_EQ(sev, LogMessage::GetLogToStream(NULL)); +} + + +TEST(LogTest, WallClockStartTime) { + uint32 time = LogMessage::WallClockStartTime(); + // Expect the time to be in a sensible range, e.g. > 2012-01-01. + EXPECT_GT(time, 1325376000u); +} + +// Test the time required to write 1000 80-character logs to an unbuffered file. +TEST(LogTest, Perf) { + Pathname path; + EXPECT_TRUE(Filesystem::GetTemporaryFolder(path, true, NULL)); + path.SetPathname(Filesystem::TempFilename(path, "ut")); + + FileStream stream; + EXPECT_TRUE(stream.Open(path.pathname(), "wb", NULL)); + stream.DisableBuffering(); + LogMessage::AddLogToStream(&stream, LS_SENSITIVE); + + uint32 start = Time(), finish; + std::string message('X', 80); + for (int i = 0; i < 1000; ++i) { + LOG(LS_SENSITIVE) << message; + } + finish = Time(); + + LogMessage::RemoveLogToStream(&stream); + stream.Close(); + Filesystem::DeleteFile(path); + + LOG(LS_INFO) << "Average log time: " << TimeDiff(finish, start) << " us"; +} + +} // namespace talk_base diff --git a/talk/base/macasyncsocket.cc b/talk/base/macasyncsocket.cc new file mode 100644 index 000000000..54ad60461 --- /dev/null +++ b/talk/base/macasyncsocket.cc @@ -0,0 +1,472 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +// thaloun@google.com (Tim Haloun) +// +// MacAsyncSocket is a kind of AsyncSocket. It does not support the SOCK_DGRAM +// type (yet). It works asynchronously, which means that users of this socket +// should connect to the various events declared in asyncsocket.h to receive +// notifications about this socket. It uses CFSockets for signals, but prefers +// the basic bsd socket operations rather than their CFSocket wrappers when +// possible. + +#include +#include + +#include "talk/base/macasyncsocket.h" + +#include "talk/base/logging.h" +#include "talk/base/macsocketserver.h" + +namespace talk_base { + +static const int kCallbackFlags = kCFSocketReadCallBack | + kCFSocketConnectCallBack | + kCFSocketWriteCallBack; + +MacAsyncSocket::MacAsyncSocket(MacBaseSocketServer* ss, int family) + : ss_(ss), + socket_(NULL), + native_socket_(INVALID_SOCKET), + source_(NULL), + current_callbacks_(0), + disabled_(false), + error_(0), + state_(CS_CLOSED), + resolver_(NULL) { + Initialize(family); +} + +MacAsyncSocket::~MacAsyncSocket() { + Close(); +} + +// Returns the address to which the socket is bound. If the socket is not +// bound, then the any-address is returned. +SocketAddress MacAsyncSocket::GetLocalAddress() const { + SocketAddress address; + + // The CFSocket doesn't pick up on implicit binds from the connect call. + // Calling bind in before connect explicitly causes errors, so just query + // the underlying bsd socket. + sockaddr_storage addr; + socklen_t addrlen = sizeof(addr); + int result = ::getsockname(native_socket_, + reinterpret_cast(&addr), &addrlen); + if (result >= 0) { + SocketAddressFromSockAddrStorage(addr, &address); + } + return address; +} + +// Returns the address to which the socket is connected. If the socket is not +// connected, then the any-address is returned. +SocketAddress MacAsyncSocket::GetRemoteAddress() const { + SocketAddress address; + + // Use native_socket for consistency with GetLocalAddress. + sockaddr_storage addr; + socklen_t addrlen = sizeof(addr); + int result = ::getpeername(native_socket_, + reinterpret_cast(&addr), &addrlen); + if (result >= 0) { + SocketAddressFromSockAddrStorage(addr, &address); + } + return address; +} + +// Bind the socket to a local address. +int MacAsyncSocket::Bind(const SocketAddress& address) { + sockaddr_storage saddr = {0}; + size_t len = address.ToSockAddrStorage(&saddr); + int err = ::bind(native_socket_, reinterpret_cast(&saddr), len); + if (err == SOCKET_ERROR) error_ = errno; + return err; +} + +void MacAsyncSocket::OnResolveResult(SignalThread* thread) { + if (thread != resolver_) { + return; + } + int error = resolver_->error(); + if (error == 0) { + error = DoConnect(resolver_->address()); + } else { + Close(); + } + if (error) { + error_ = error; + SignalCloseEvent(this, error_); + } +} + +// Connect to a remote address. +int MacAsyncSocket::Connect(const SocketAddress& addr) { + // TODO(djw): Consolidate all the connect->resolve->doconnect implementations. + if (state_ != CS_CLOSED) { + SetError(EALREADY); + return SOCKET_ERROR; + } + if (addr.IsUnresolved()) { + LOG(LS_VERBOSE) << "Resolving addr in MacAsyncSocket::Connect"; + resolver_ = new AsyncResolver(); + resolver_->set_address(addr); + resolver_->SignalWorkDone.connect(this, + &MacAsyncSocket::OnResolveResult); + resolver_->Start(); + state_ = CS_CONNECTING; + return 0; + } + return DoConnect(addr); +} + +int MacAsyncSocket::DoConnect(const SocketAddress& addr) { + if (!valid()) { + Initialize(addr.family()); + if (!valid()) + return SOCKET_ERROR; + } + + sockaddr_storage saddr; + size_t len = addr.ToSockAddrStorage(&saddr); + int result = ::connect(native_socket_, reinterpret_cast(&saddr), + len); + + if (result != SOCKET_ERROR) { + state_ = CS_CONNECTED; + } else { + error_ = errno; + if (error_ == EINPROGRESS) { + state_ = CS_CONNECTING; + result = 0; + } + } + return result; +} + +// Send to the remote end we're connected to. +int MacAsyncSocket::Send(const void* buffer, size_t length) { + if (!valid()) { + return SOCKET_ERROR; + } + + int sent = ::send(native_socket_, buffer, length, 0); + + if (sent == SOCKET_ERROR) { + error_ = errno; + + if (IsBlocking()) { + // Reenable the writable callback (once), since we are flow controlled. + CFSocketEnableCallBacks(socket_, kCallbackFlags); + current_callbacks_ = kCallbackFlags; + } + } + return sent; +} + +// Send to the given address. We may or may not be connected to anyone. +int MacAsyncSocket::SendTo(const void* buffer, size_t length, + const SocketAddress& address) { + if (!valid()) { + return SOCKET_ERROR; + } + + sockaddr_storage saddr; + size_t len = address.ToSockAddrStorage(&saddr); + int sent = ::sendto(native_socket_, buffer, length, 0, + reinterpret_cast(&saddr), len); + + if (sent == SOCKET_ERROR) { + error_ = errno; + } + + return sent; +} + +// Read data received from the remote end we're connected to. +int MacAsyncSocket::Recv(void* buffer, size_t length) { + int received = ::recv(native_socket_, reinterpret_cast(buffer), + length, 0); + if (received == SOCKET_ERROR) error_ = errno; + + // Recv should only be called when there is data to read + ASSERT((received != 0) || (length == 0)); + return received; +} + +// Read data received from any remote party +int MacAsyncSocket::RecvFrom(void* buffer, size_t length, + SocketAddress* out_addr) { + sockaddr_storage saddr; + socklen_t addr_len = sizeof(saddr); + int received = ::recvfrom(native_socket_, reinterpret_cast(buffer), + length, 0, reinterpret_cast(&saddr), + &addr_len); + if (received >= 0 && out_addr != NULL) { + SocketAddressFromSockAddrStorage(saddr, out_addr); + } else if (received == SOCKET_ERROR) { + error_ = errno; + } + return received; +} + +int MacAsyncSocket::Listen(int backlog) { + if (!valid()) { + return SOCKET_ERROR; + } + + int res = ::listen(native_socket_, backlog); + if (res != SOCKET_ERROR) + state_ = CS_CONNECTING; + else + error_ = errno; + + return res; +} + +MacAsyncSocket* MacAsyncSocket::Accept(SocketAddress* out_addr) { + sockaddr_storage saddr; + socklen_t addr_len = sizeof(saddr); + + int socket_fd = ::accept(native_socket_, reinterpret_cast(&saddr), + &addr_len); + if (socket_fd == INVALID_SOCKET) { + error_ = errno; + return NULL; + } + + MacAsyncSocket* s = new MacAsyncSocket(ss_, saddr.ss_family, socket_fd); + if (s && s->valid()) { + s->state_ = CS_CONNECTED; + if (out_addr) + SocketAddressFromSockAddrStorage(saddr, out_addr); + } else { + delete s; + s = NULL; + } + return s; +} + +int MacAsyncSocket::Close() { + if (source_ != NULL) { + CFRunLoopSourceInvalidate(source_); + CFRelease(source_); + if (ss_) ss_->UnregisterSocket(this); + source_ = NULL; + } + + if (socket_ != NULL) { + CFSocketInvalidate(socket_); + CFRelease(socket_); + socket_ = NULL; + } + + if (resolver_) { + resolver_->Destroy(false); + resolver_ = NULL; + } + + native_socket_ = INVALID_SOCKET; // invalidates the socket + error_ = 0; + state_ = CS_CLOSED; + return 0; +} + +int MacAsyncSocket::EstimateMTU(uint16* mtu) { + ASSERT(false && "NYI"); + return -1; +} + +int MacAsyncSocket::GetError() const { + return error_; +} + +void MacAsyncSocket::SetError(int error) { + error_ = error; +} + +Socket::ConnState MacAsyncSocket::GetState() const { + return state_; +} + +int MacAsyncSocket::GetOption(Option opt, int* value) { + ASSERT(false && "NYI"); + return -1; +} + +int MacAsyncSocket::SetOption(Option opt, int value) { + ASSERT(false && "NYI"); + return -1; +} + +void MacAsyncSocket::EnableCallbacks() { + if (valid()) { + disabled_ = false; + CFSocketEnableCallBacks(socket_, current_callbacks_); + } +} + +void MacAsyncSocket::DisableCallbacks() { + if (valid()) { + disabled_ = true; + CFSocketDisableCallBacks(socket_, kCallbackFlags); + } +} + +MacAsyncSocket::MacAsyncSocket(MacBaseSocketServer* ss, int family, + int native_socket) + : ss_(ss), + socket_(NULL), + native_socket_(native_socket), + source_(NULL), + current_callbacks_(0), + disabled_(false), + error_(0), + state_(CS_CLOSED), + resolver_(NULL) { + Initialize(family); +} + +// Create a new socket, wrapping the native socket if provided or creating one +// otherwise. In case of any failure, consume the native socket. We assume the +// wrapped socket is in the closed state. If this is not the case you must +// update the state_ field for this socket yourself. +void MacAsyncSocket::Initialize(int family) { + CFSocketContext ctx = { 0 }; + ctx.info = this; + + // First create the CFSocket + CFSocketRef cf_socket = NULL; + bool res = false; + if (native_socket_ == INVALID_SOCKET) { + cf_socket = CFSocketCreate(kCFAllocatorDefault, + family, SOCK_STREAM, IPPROTO_TCP, + kCallbackFlags, MacAsyncSocketCallBack, &ctx); + } else { + cf_socket = CFSocketCreateWithNative(kCFAllocatorDefault, + native_socket_, kCallbackFlags, + MacAsyncSocketCallBack, &ctx); + } + + if (cf_socket) { + res = true; + socket_ = cf_socket; + native_socket_ = CFSocketGetNative(cf_socket); + current_callbacks_ = kCallbackFlags; + } + + if (res) { + // Make the underlying socket asynchronous + res = (-1 != ::fcntl(native_socket_, F_SETFL, + ::fcntl(native_socket_, F_GETFL, 0) | O_NONBLOCK)); + } + + if (res) { + // Add this socket to the run loop, at priority 1 so that it will be + // queued behind any pending signals. + source_ = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket_, 1); + res = (source_ != NULL); + if (!res) errno = EINVAL; + } + + if (res) { + if (ss_) ss_->RegisterSocket(this); + CFRunLoopAddSource(CFRunLoopGetCurrent(), source_, kCFRunLoopCommonModes); + } + + if (!res) { + int error = errno; + Close(); // Clears error_. + error_ = error; + } +} + +// Call CFRelease on the result when done using it +CFDataRef MacAsyncSocket::CopyCFAddress(const SocketAddress& address) { + sockaddr_storage saddr; + size_t len = address.ToSockAddrStorage(&saddr); + + const UInt8* bytes = reinterpret_cast(&saddr); + + CFDataRef cf_address = CFDataCreate(kCFAllocatorDefault, + bytes, len); + + ASSERT(cf_address != NULL); + return cf_address; +} + +void MacAsyncSocket::MacAsyncSocketCallBack(CFSocketRef s, + CFSocketCallBackType callbackType, + CFDataRef address, + const void* data, + void* info) { + MacAsyncSocket* this_socket = + reinterpret_cast(info); + ASSERT(this_socket != NULL && this_socket->socket_ == s); + + // Don't signal any socket messages if the socketserver is not listening on + // them. When we are reenabled they will be requeued and will fire again. + if (this_socket->disabled_) + return; + + switch (callbackType) { + case kCFSocketReadCallBack: + // This callback is invoked in one of 3 situations: + // 1. A new connection is waiting to be accepted. + // 2. The remote end closed the connection (a recv will return 0). + // 3. Data is available to read. + // 4. The connection closed unhappily (recv will return -1). + if (this_socket->state_ == CS_CONNECTING) { + // Case 1. + this_socket->SignalReadEvent(this_socket); + } else { + char ch, amt; + amt = ::recv(this_socket->native_socket_, &ch, 1, MSG_PEEK); + if (amt == 0) { + // Case 2. + this_socket->state_ = CS_CLOSED; + + // Disable additional callbacks or we will signal close twice. + CFSocketDisableCallBacks(this_socket->socket_, kCFSocketReadCallBack); + this_socket->current_callbacks_ &= ~kCFSocketReadCallBack; + this_socket->SignalCloseEvent(this_socket, 0); + } else if (amt > 0) { + // Case 3. + this_socket->SignalReadEvent(this_socket); + } else { + // Case 4. + int error = errno; + if (error == EAGAIN) { + // Observed in practice. Let's hope it's a spurious or out of date + // signal, since we just eat it. + } else { + this_socket->error_ = error; + this_socket->SignalCloseEvent(this_socket, error); + } + } + } + break; + + case kCFSocketConnectCallBack: + if (data != NULL) { + // An error occured in the background while connecting + this_socket->error_ = errno; + this_socket->state_ = CS_CLOSED; + this_socket->SignalCloseEvent(this_socket, this_socket->error_); + } else { + this_socket->state_ = CS_CONNECTED; + this_socket->SignalConnectEvent(this_socket); + } + break; + + case kCFSocketWriteCallBack: + // Update our callback tracking. Write doesn't reenable, so it's off now. + this_socket->current_callbacks_ &= ~kCFSocketWriteCallBack; + this_socket->SignalWriteEvent(this_socket); + break; + + default: + ASSERT(false && "Invalid callback type for socket"); + } +} + +} // namespace talk_base diff --git a/talk/base/macasyncsocket.h b/talk/base/macasyncsocket.h new file mode 100644 index 000000000..12d2addaf --- /dev/null +++ b/talk/base/macasyncsocket.h @@ -0,0 +1,91 @@ +// Copyright 2008 Google Inc. All Rights Reserved. + +// +// MacAsyncSocket is a kind of AsyncSocket. It only creates sockets +// of the TCP type, and does not (yet) support listen and accept. It works +// asynchronously, which means that users of this socket should connect to +// the various events declared in asyncsocket.h to receive notifications about +// this socket. + +#ifndef TALK_BASE_MACASYNCSOCKET_H__ +#define TALK_BASE_MACASYNCSOCKET_H__ + +#include + +#include "talk/base/asyncsocket.h" +#include "talk/base/nethelpers.h" + +namespace talk_base { + +class MacBaseSocketServer; + +class MacAsyncSocket : public AsyncSocket, public sigslot::has_slots<> { + public: + MacAsyncSocket(MacBaseSocketServer* ss, int family); + virtual ~MacAsyncSocket(); + + bool valid() const { return source_ != NULL; } + + // Socket interface + virtual SocketAddress GetLocalAddress() const; + virtual SocketAddress GetRemoteAddress() const; + virtual int Bind(const SocketAddress& addr); + virtual int Connect(const SocketAddress& addr); + virtual int Send(const void* buffer, size_t length); + virtual int SendTo(const void* buffer, size_t length, + const SocketAddress& addr); + virtual int Recv(void* buffer, size_t length); + virtual int RecvFrom(void* buffer, size_t length, SocketAddress* out_addr); + virtual int Listen(int backlog); + virtual MacAsyncSocket* Accept(SocketAddress* out_addr); + virtual int Close(); + virtual int GetError() const; + virtual void SetError(int error); + virtual ConnState GetState() const; + virtual int EstimateMTU(uint16* mtu); + virtual int GetOption(Option opt, int* value); + virtual int SetOption(Option opt, int value); + + // For the MacBaseSocketServer to disable callbacks when process_io is false. + void EnableCallbacks(); + void DisableCallbacks(); + + protected: + void OnResolveResult(SignalThread* thread); + int DoConnect(const SocketAddress& addr); + + private: + // Creates an async socket from an existing bsd socket + MacAsyncSocket(MacBaseSocketServer* ss, int family, int native_socket); + + // Attaches the socket to the CFRunloop and sets the wrapped bsd socket + // to async mode + void Initialize(int family); + + // Translate the SocketAddress into a CFDataRef to pass to CF socket + // functions. Caller must call CFRelease on the result when done. + static CFDataRef CopyCFAddress(const SocketAddress& address); + + // Callback for the underlying CFSocketRef. + static void MacAsyncSocketCallBack(CFSocketRef s, + CFSocketCallBackType callbackType, + CFDataRef address, + const void* data, + void* info); + + MacBaseSocketServer* ss_; + CFSocketRef socket_; + int native_socket_; + CFRunLoopSourceRef source_; + int current_callbacks_; + bool disabled_; + int error_; + ConnState state_; + AsyncResolver* resolver_; + + DISALLOW_EVIL_CONSTRUCTORS(MacAsyncSocket); +}; + +} // namespace talk_base + +#endif // TALK_BASE_MACASYNCSOCKET_H__ diff --git a/talk/base/maccocoasocketserver.h b/talk/base/maccocoasocketserver.h new file mode 100644 index 000000000..f4aeb3397 --- /dev/null +++ b/talk/base/maccocoasocketserver.h @@ -0,0 +1,63 @@ +/* + * libjingle + * Copyright 2007, 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. + */ + +// A libjingle compatible SocketServer for OSX/iOS/Cocoa. + +#ifndef TALK_BASE_MACCOCOASOCKETSERVER_H_ +#define TALK_BASE_MACCOCOASOCKETSERVER_H_ + +#include "talk/base/macsocketserver.h" + +#ifdef __OBJC__ +@class NSTimer, MacCocoaSocketServerHelper; +#else +class NSTimer; +class MacCocoaSocketServerHelper; +#endif + +namespace talk_base { + +// A socketserver implementation that wraps the main cocoa +// application loop accessed through [NSApp run]. +class MacCocoaSocketServer : public MacBaseSocketServer { + public: + explicit MacCocoaSocketServer(); + virtual ~MacCocoaSocketServer(); + + virtual bool Wait(int cms, bool process_io); + virtual void WakeUp(); + + private: + MacCocoaSocketServerHelper* helper_; + NSTimer* timer_; // Weak. + + DISALLOW_EVIL_CONSTRUCTORS(MacCocoaSocketServer); +}; + +} // namespace talk_base + +#endif // TALK_BASE_MACCOCOASOCKETSERVER_H_ diff --git a/talk/base/maccocoasocketserver.mm b/talk/base/maccocoasocketserver.mm new file mode 100644 index 000000000..bf308e610 --- /dev/null +++ b/talk/base/maccocoasocketserver.mm @@ -0,0 +1,134 @@ +/* + * libjingle + * Copyright 2012, 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. + */ +#import "talk/base/maccocoasocketserver.h" + +#import +#import +#include + +#include "talk/base/scoped_autorelease_pool.h" + +// MacCocoaSocketServerHelper serves as a delegate to NSMachPort or a target for +// a timeout. +@interface MacCocoaSocketServerHelper : NSObject { + // This is a weak reference. This works fine since the + // talk_base::MacCocoaSocketServer owns this object. + talk_base::MacCocoaSocketServer* socketServer_; // Weak. +} +@end + +@implementation MacCocoaSocketServerHelper +- (id)initWithSocketServer:(talk_base::MacCocoaSocketServer*)ss { + self = [super init]; + if (self) { + socketServer_ = ss; + } + return self; +} + +- (void)timerFired:(NSTimer*)timer { + socketServer_->WakeUp(); +} +@end + +namespace talk_base { + +MacCocoaSocketServer::MacCocoaSocketServer() { + helper_ = [[MacCocoaSocketServerHelper alloc] initWithSocketServer:this]; + timer_ = nil; + + // Initialize the shared NSApplication + [NSApplication sharedApplication]; +} + +MacCocoaSocketServer::~MacCocoaSocketServer() { + [timer_ invalidate]; + [timer_ release]; + [helper_ release]; +} + +bool MacCocoaSocketServer::Wait(int cms, bool process_io) { + talk_base::ScopedAutoreleasePool pool; + if (!process_io && cms == 0) { + // No op. + return true; + } + + if (!process_io) { + // No way to listen to common modes and not get socket events, unless + // we disable each one's callbacks. + EnableSocketCallbacks(false); + } + + if (kForever != cms) { + // Install a timer that fires wakeup after cms has elapsed. + timer_ = + [NSTimer scheduledTimerWithTimeInterval:cms / 1000.0 + target:helper_ + selector:@selector(timerFired:) + userInfo:nil + repeats:NO]; + [timer_ retain]; + } + + // Run until WakeUp is called, which will call stop and exit this loop. + [NSApp run]; + + if (!process_io) { + // Reenable them. Hopefully this won't cause spurious callbacks or + // missing ones while they were disabled. + EnableSocketCallbacks(true); + } + + return true; +} + +void MacCocoaSocketServer::WakeUp() { + // Timer has either fired or shortcutted. + [timer_ invalidate]; + [timer_ release]; + timer_ = nil; + [NSApp stop:nil]; + + // NSApp stop only exits after finishing processing of the + // current event. Since we're potentially in a timer callback + // and not an NSEvent handler, we need to trigger a dummy one + // and turn the loop over. We may be able to skip this if we're + // on the ss' thread and not inside the app loop already. + NSEvent *event = [NSEvent otherEventWithType:NSApplicationDefined + location:NSMakePoint(0,0) + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:1 + data1:1 + data2:1]; + [NSApp postEvent:event atStart:YES]; +} + +} // namespace talk_base diff --git a/talk/base/maccocoasocketserver_unittest.mm b/talk/base/maccocoasocketserver_unittest.mm new file mode 100644 index 000000000..d6f4b2c11 --- /dev/null +++ b/talk/base/maccocoasocketserver_unittest.mm @@ -0,0 +1,64 @@ +/* + * libjingle + * Copyright 2009, 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/base/gunit.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/thread.h" +#include "talk/base/maccocoasocketserver.h" + +namespace talk_base { + +class WakeThread : public Thread { + public: + WakeThread(SocketServer* ss) : ss_(ss) { + } + void Run() { + ss_->WakeUp(); + } + private: + SocketServer* ss_; +}; + +// Test that MacCocoaSocketServer::Wait works as expected. +TEST(MacCocoaSocketServer, TestWait) { + MacCocoaSocketServer server; + uint32 start = Time(); + server.Wait(1000, true); + EXPECT_GE(TimeSince(start), 1000); +} + +// Test that MacCocoaSocketServer::Wakeup works as expected. +TEST(MacCocoaSocketServer, TestWakeup) { + MacCFSocketServer server; + WakeThread thread(&server); + uint32 start = Time(); + thread.Start(); + server.Wait(10000, true); + EXPECT_LT(TimeSince(start), 10000); +} + +} // namespace talk_base diff --git a/talk/base/maccocoathreadhelper.h b/talk/base/maccocoathreadhelper.h new file mode 100644 index 000000000..336e638b3 --- /dev/null +++ b/talk/base/maccocoathreadhelper.h @@ -0,0 +1,44 @@ +/* + * libjingle + * Copyright 2008 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. + */ + +// Helper function for using Cocoa with Posix threads. This header should be +// included from C/C++ files that want to use some Cocoa functionality without +// using the .mm extension (mostly for files that are compiled on multiple +// platforms). + +#ifndef TALK_BASE_MACCOCOATHREADHELPER_H__ +#define TALK_BASE_MACCOCOATHREADHELPER_H__ + +namespace talk_base { + +// Cocoa must be "put into multithreading mode" before Cocoa functionality can +// be used on POSIX threads. This function does that. +void InitCocoaMultiThreading(); + +} // namespace talk_base + +#endif // TALK_BASE_MACCOCOATHREADHELPER_H__ diff --git a/talk/base/maccocoathreadhelper.mm b/talk/base/maccocoathreadhelper.mm new file mode 100644 index 000000000..fee39723f --- /dev/null +++ b/talk/base/maccocoathreadhelper.mm @@ -0,0 +1,61 @@ +/* + * libjingle + * Copyright 2007, 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. + */ +// Helper function for using Cocoa with Posix threading. + +#import +#import + +#import "talk/base/maccocoathreadhelper.h" + +namespace talk_base { + +// Cocoa must be "put into multithreading mode" before Cocoa functionality can +// be used on POSIX threads. The way to do that is to spawn one thread that may +// immediately exit. +void InitCocoaMultiThreading() { + if ([NSThread isMultiThreaded] == NO) { + // The sole purpose of this autorelease pool is to avoid a console + // message on Leopard that tells us we're autoreleasing the thread + // with no autorelease pool in place; we can't set up an autorelease + // pool before this, because this is executed from an initializer, + // which is run before main. This means we leak an autorelease pool, + // and one thread, and if other objects are set up in initializers after + // this they'll be silently added to this pool and never released. + + // Doing NSAutoreleasePool* hack = [[NSAutoreleasePool alloc] init]; + // causes unused variable error. + NSAutoreleasePool* hack; + hack = [[NSAutoreleasePool alloc] init]; + [NSThread detachNewThreadSelector:@selector(class) + toTarget:[NSObject class] + withObject:nil]; + } + + assert([NSThread isMultiThreaded]); +} + +} // namespace talk_base diff --git a/talk/base/macconversion.cc b/talk/base/macconversion.cc new file mode 100644 index 000000000..4654e5303 --- /dev/null +++ b/talk/base/macconversion.cc @@ -0,0 +1,176 @@ +/* + * libjingle + * Copyright 2004--2009, 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. + */ + +#ifdef OSX + +#include + +#include "talk/base/logging.h" +#include "talk/base/macconversion.h" + +bool p_convertHostCFStringRefToCPPString( + const CFStringRef cfstr, std::string& cppstr) { + bool result = false; + + // First this must be non-null, + if (NULL != cfstr) { + // it must actually *be* a CFString, and not something just masquerading + // as one, + if (CFGetTypeID(cfstr) == CFStringGetTypeID()) { + // and we must be able to get the characters out of it. + // (The cfstr owns this buffer; it came from somewhere else, + // so someone else gets to take care of getting rid of the cfstr, + // and then this buffer will go away automatically.) + unsigned length = CFStringGetLength(cfstr); + char* buf = new char[1 + length]; + if (CFStringGetCString(cfstr, buf, 1 + length, kCFStringEncodingASCII)) { + if (strlen(buf) == length) { + cppstr.assign(buf); + result = true; + } + } + delete [] buf; + } + } + + return result; +} + +bool p_convertCFNumberToInt(CFNumberRef cfn, int* i) { + bool converted = false; + + // It must not be null. + if (NULL != cfn) { + // It must actually *be* a CFNumber and not something just masquerading + // as one. + if (CFGetTypeID(cfn) == CFNumberGetTypeID()) { + CFNumberType ntype = CFNumberGetType(cfn); + switch (ntype) { + case kCFNumberSInt8Type: + SInt8 sint8; + converted = CFNumberGetValue(cfn, ntype, static_cast(&sint8)); + if (converted) *i = static_cast(sint8); + break; + case kCFNumberSInt16Type: + SInt16 sint16; + converted = CFNumberGetValue(cfn, ntype, static_cast(&sint16)); + if (converted) *i = static_cast(sint16); + break; + case kCFNumberSInt32Type: + SInt32 sint32; + converted = CFNumberGetValue(cfn, ntype, static_cast(&sint32)); + if (converted) *i = static_cast(sint32); + break; + case kCFNumberSInt64Type: + SInt64 sint64; + converted = CFNumberGetValue(cfn, ntype, static_cast(&sint64)); + if (converted) *i = static_cast(sint64); + break; + case kCFNumberFloat32Type: + Float32 float32; + converted = CFNumberGetValue(cfn, ntype, + static_cast(&float32)); + if (converted) *i = static_cast(float32); + break; + case kCFNumberFloat64Type: + Float64 float64; + converted = CFNumberGetValue(cfn, ntype, + static_cast(&float64)); + if (converted) *i = static_cast(float64); + break; + case kCFNumberCharType: + char charvalue; + converted = CFNumberGetValue(cfn, ntype, + static_cast(&charvalue)); + if (converted) *i = static_cast(charvalue); + break; + case kCFNumberShortType: + short shortvalue; + converted = CFNumberGetValue(cfn, ntype, + static_cast(&shortvalue)); + if (converted) *i = static_cast(shortvalue); + break; + case kCFNumberIntType: + int intvalue; + converted = CFNumberGetValue(cfn, ntype, + static_cast(&intvalue)); + if (converted) *i = static_cast(intvalue); + break; + case kCFNumberLongType: + long longvalue; + converted = CFNumberGetValue(cfn, ntype, + static_cast(&longvalue)); + if (converted) *i = static_cast(longvalue); + break; + case kCFNumberLongLongType: + long long llvalue; + converted = CFNumberGetValue(cfn, ntype, + static_cast(&llvalue)); + if (converted) *i = static_cast(llvalue); + break; + case kCFNumberFloatType: + float floatvalue; + converted = CFNumberGetValue(cfn, ntype, + static_cast(&floatvalue)); + if (converted) *i = static_cast(floatvalue); + break; + case kCFNumberDoubleType: + double doublevalue; + converted = CFNumberGetValue(cfn, ntype, + static_cast(&doublevalue)); + if (converted) *i = static_cast(doublevalue); + break; + case kCFNumberCFIndexType: + CFIndex cfindex; + converted = CFNumberGetValue(cfn, ntype, + static_cast(&cfindex)); + if (converted) *i = static_cast(cfindex); + break; + default: + LOG(LS_ERROR) << "got unknown type."; + break; + } + } + } + + return converted; +} + +bool p_isCFNumberTrue(CFNumberRef cfn) { + // We assume it's false until proven otherwise. + bool result = false; + int asInt; + bool converted = p_convertCFNumberToInt(cfn, &asInt); + + if (converted && (0 != asInt)) { + result = true; + } + + return result; +} + +#endif // OSX diff --git a/talk/base/macconversion.h b/talk/base/macconversion.h new file mode 100644 index 000000000..a401cabdc --- /dev/null +++ b/talk/base/macconversion.h @@ -0,0 +1,56 @@ +/* + * libjingle + * Copyright 2004--2009, 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. + */ + +#ifndef TALK_BASE_MACCONVERSION_H_ +#define TALK_BASE_MACCONVERSION_H_ + +#ifdef OSX + +#include + +#include + +// given a CFStringRef, attempt to convert it to a C++ string. +// returns true if it succeeds, false otherwise. +// We can safely assume, given our context, that the string is +// going to be in ASCII, because it will either be an IP address, +// or a domain name, which is guaranteed to be ASCII-representable. +bool p_convertHostCFStringRefToCPPString(const CFStringRef cfstr, + std::string& cppstr); + +// Convert the CFNumber to an integer, putting the integer in the location +// given, and returhing true, if the conversion succeeds. +// If given a NULL or a non-CFNumber, returns false. +// This is pretty aggresive about trying to convert to int. +bool p_convertCFNumberToInt(CFNumberRef cfn, int* i); + +// given a CFNumberRef, determine if it represents a true value. +bool p_isCFNumberTrue(CFNumberRef cfn); + +#endif // OSX + +#endif // TALK_BASE_MACCONVERSION_H_ diff --git a/talk/base/macsocketserver.cc b/talk/base/macsocketserver.cc new file mode 100644 index 000000000..895b0bf1b --- /dev/null +++ b/talk/base/macsocketserver.cc @@ -0,0 +1,369 @@ + + +#include "talk/base/macsocketserver.h" + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/macasyncsocket.h" +#include "talk/base/macutils.h" +#include "talk/base/thread.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// MacBaseSocketServer +/////////////////////////////////////////////////////////////////////////////// + +MacBaseSocketServer::MacBaseSocketServer() { +} + +MacBaseSocketServer::~MacBaseSocketServer() { +} + +AsyncSocket* MacBaseSocketServer::CreateAsyncSocket(int type) { + return CreateAsyncSocket(AF_INET, type); +} + +AsyncSocket* MacBaseSocketServer::CreateAsyncSocket(int family, int type) { + if (SOCK_STREAM != type) + return NULL; + + MacAsyncSocket* socket = new MacAsyncSocket(this, family); + if (!socket->valid()) { + delete socket; + return NULL; + } + return socket; +} + +void MacBaseSocketServer::RegisterSocket(MacAsyncSocket* s) { + sockets_.insert(s); +} + +void MacBaseSocketServer::UnregisterSocket(MacAsyncSocket* s) { + VERIFY(1 == sockets_.erase(s)); // found 1 +} + +bool MacBaseSocketServer::SetPosixSignalHandler(int signum, + void (*handler)(int)) { + Dispatcher* dispatcher = signal_dispatcher(); + if (!PhysicalSocketServer::SetPosixSignalHandler(signum, handler)) { + return false; + } + + // Only register the FD once, when the first custom handler is installed. + if (!dispatcher && (dispatcher = signal_dispatcher())) { + CFFileDescriptorContext ctx = { 0 }; + ctx.info = this; + + CFFileDescriptorRef desc = CFFileDescriptorCreate( + kCFAllocatorDefault, + dispatcher->GetDescriptor(), + false, + &MacBaseSocketServer::FileDescriptorCallback, + &ctx); + if (!desc) { + return false; + } + + CFFileDescriptorEnableCallBacks(desc, kCFFileDescriptorReadCallBack); + CFRunLoopSourceRef ref = + CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, desc, 0); + + if (!ref) { + CFRelease(desc); + return false; + } + + CFRunLoopAddSource(CFRunLoopGetCurrent(), ref, kCFRunLoopCommonModes); + CFRelease(desc); + CFRelease(ref); + } + + return true; +} + +// Used to disable socket events from waking our message queue when +// process_io is false. Does not disable signal event handling though. +void MacBaseSocketServer::EnableSocketCallbacks(bool enable) { + for (std::set::iterator it = sockets().begin(); + it != sockets().end(); ++it) { + if (enable) { + (*it)->EnableCallbacks(); + } else { + (*it)->DisableCallbacks(); + } + } +} + +void MacBaseSocketServer::FileDescriptorCallback(CFFileDescriptorRef fd, + CFOptionFlags flags, + void* context) { + MacBaseSocketServer* this_ss = + reinterpret_cast(context); + ASSERT(this_ss); + Dispatcher* signal_dispatcher = this_ss->signal_dispatcher(); + ASSERT(signal_dispatcher); + + signal_dispatcher->OnPreEvent(DE_READ); + signal_dispatcher->OnEvent(DE_READ, 0); + CFFileDescriptorEnableCallBacks(fd, kCFFileDescriptorReadCallBack); +} + + +/////////////////////////////////////////////////////////////////////////////// +// MacCFSocketServer +/////////////////////////////////////////////////////////////////////////////// + +void WakeUpCallback(void* info) { + MacCFSocketServer* server = static_cast(info); + ASSERT(NULL != server); + server->OnWakeUpCallback(); +} + +MacCFSocketServer::MacCFSocketServer() + : run_loop_(CFRunLoopGetCurrent()), + wake_up_(NULL) { + CFRunLoopSourceContext ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.info = this; + ctx.perform = &WakeUpCallback; + wake_up_ = CFRunLoopSourceCreate(NULL, 0, &ctx); + ASSERT(NULL != wake_up_); + if (wake_up_) { + CFRunLoopAddSource(run_loop_, wake_up_, kCFRunLoopCommonModes); + } +} + +MacCFSocketServer::~MacCFSocketServer() { + if (wake_up_) { + CFRunLoopSourceInvalidate(wake_up_); + CFRelease(wake_up_); + } +} + +bool MacCFSocketServer::Wait(int cms, bool process_io) { + ASSERT(CFRunLoopGetCurrent() == run_loop_); + + if (!process_io && cms == 0) { + // No op. + return true; + } + + if (!process_io) { + // No way to listen to common modes and not get socket events, unless + // we disable each one's callbacks. + EnableSocketCallbacks(false); + } + + SInt32 result; + if (kForever == cms) { + do { + // Would prefer to run in a custom mode that only listens to wake_up, + // but we have qtkit sending work to the main thread which is effectively + // blocked here, causing deadlock. Thus listen to the common modes. + // TODO: If QTKit becomes thread safe, do the above. + result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10000000, false); + } while (result != kCFRunLoopRunFinished && result != kCFRunLoopRunStopped); + } else { + // TODO: In the case of 0ms wait, this will only process one event, so we + // may want to loop until it returns TimedOut. + CFTimeInterval seconds = cms / 1000.0; + result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, seconds, false); + } + + if (!process_io) { + // Reenable them. Hopefully this won't cause spurious callbacks or + // missing ones while they were disabled. + EnableSocketCallbacks(true); + } + + if (kCFRunLoopRunFinished == result) { + return false; + } + return true; +} + +void MacCFSocketServer::WakeUp() { + if (wake_up_) { + CFRunLoopSourceSignal(wake_up_); + CFRunLoopWakeUp(run_loop_); + } +} + +void MacCFSocketServer::OnWakeUpCallback() { + ASSERT(run_loop_ == CFRunLoopGetCurrent()); + CFRunLoopStop(run_loop_); +} + +/////////////////////////////////////////////////////////////////////////////// +// MacCarbonSocketServer +/////////////////////////////////////////////////////////////////////////////// +#ifndef CARBON_DEPRECATED + +const UInt32 kEventClassSocketServer = 'MCSS'; +const UInt32 kEventWakeUp = 'WAKE'; +const EventTypeSpec kEventWakeUpSpec[] = { + { kEventClassSocketServer, kEventWakeUp } +}; + +std::string DecodeEvent(EventRef event) { + std::string str; + DecodeFourChar(::GetEventClass(event), &str); + str.push_back(':'); + DecodeFourChar(::GetEventKind(event), &str); + return str; +} + +MacCarbonSocketServer::MacCarbonSocketServer() + : event_queue_(GetCurrentEventQueue()), wake_up_(NULL) { + VERIFY(noErr == CreateEvent(NULL, kEventClassSocketServer, kEventWakeUp, 0, + kEventAttributeUserEvent, &wake_up_)); +} + +MacCarbonSocketServer::~MacCarbonSocketServer() { + if (wake_up_) { + ReleaseEvent(wake_up_); + } +} + +bool MacCarbonSocketServer::Wait(int cms, bool process_io) { + ASSERT(GetCurrentEventQueue() == event_queue_); + + // Listen to all events if we're processing I/O. + // Only listen for our wakeup event if we're not. + UInt32 num_types = 0; + const EventTypeSpec* events = NULL; + if (!process_io) { + num_types = GetEventTypeCount(kEventWakeUpSpec); + events = kEventWakeUpSpec; + } + + EventTargetRef target = GetEventDispatcherTarget(); + EventTimeout timeout = + (kForever == cms) ? kEventDurationForever : cms / 1000.0; + EventTimeout end_time = GetCurrentEventTime() + timeout; + + bool done = false; + while (!done) { + EventRef event; + OSStatus result = ReceiveNextEvent(num_types, events, timeout, true, + &event); + if (noErr == result) { + if (wake_up_ != event) { + LOG_F(LS_VERBOSE) << "Dispatching event: " << DecodeEvent(event); + result = SendEventToEventTarget(event, target); + if ((noErr != result) && (eventNotHandledErr != result)) { + LOG_E(LS_ERROR, OS, result) << "SendEventToEventTarget"; + } + } else { + done = true; + } + ReleaseEvent(event); + } else if (eventLoopTimedOutErr == result) { + ASSERT(cms != kForever); + done = true; + } else if (eventLoopQuitErr == result) { + // Ignore this... we get spurious quits for a variety of reasons. + LOG_E(LS_VERBOSE, OS, result) << "ReceiveNextEvent"; + } else { + // Some strange error occurred. Log it. + LOG_E(LS_WARNING, OS, result) << "ReceiveNextEvent"; + return false; + } + if (kForever != cms) { + timeout = end_time - GetCurrentEventTime(); + } + } + return true; +} + +void MacCarbonSocketServer::WakeUp() { + if (!IsEventInQueue(event_queue_, wake_up_)) { + RetainEvent(wake_up_); + OSStatus result = PostEventToQueue(event_queue_, wake_up_, + kEventPriorityStandard); + if (noErr != result) { + LOG_E(LS_ERROR, OS, result) << "PostEventToQueue"; + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// MacCarbonAppSocketServer +/////////////////////////////////////////////////////////////////////////////// + +MacCarbonAppSocketServer::MacCarbonAppSocketServer() + : event_queue_(GetCurrentEventQueue()) { + // Install event handler + VERIFY(noErr == InstallApplicationEventHandler( + NewEventHandlerUPP(WakeUpEventHandler), 1, kEventWakeUpSpec, this, + &event_handler_)); + + // Install a timer and set it idle to begin with. + VERIFY(noErr == InstallEventLoopTimer(GetMainEventLoop(), + kEventDurationForever, + kEventDurationForever, + NewEventLoopTimerUPP(TimerHandler), + this, + &timer_)); +} + +MacCarbonAppSocketServer::~MacCarbonAppSocketServer() { + RemoveEventLoopTimer(timer_); + RemoveEventHandler(event_handler_); +} + +OSStatus MacCarbonAppSocketServer::WakeUpEventHandler( + EventHandlerCallRef next, EventRef event, void *data) { + QuitApplicationEventLoop(); + return noErr; +} + +void MacCarbonAppSocketServer::TimerHandler( + EventLoopTimerRef timer, void *data) { + QuitApplicationEventLoop(); +} + +bool MacCarbonAppSocketServer::Wait(int cms, bool process_io) { + if (!process_io && cms == 0) { + // No op. + return true; + } + if (kForever != cms) { + // Start a timer. + OSStatus error = + SetEventLoopTimerNextFireTime(timer_, cms / 1000.0); + if (error != noErr) { + LOG(LS_ERROR) << "Failed setting next fire time."; + } + } + if (!process_io) { + // No way to listen to common modes and not get socket events, unless + // we disable each one's callbacks. + EnableSocketCallbacks(false); + } + RunApplicationEventLoop(); + if (!process_io) { + // Reenable them. Hopefully this won't cause spurious callbacks or + // missing ones while they were disabled. + EnableSocketCallbacks(true); + } + return true; +} + +void MacCarbonAppSocketServer::WakeUp() { + // TODO: No-op if there's already a WakeUp in flight. + EventRef wake_up; + VERIFY(noErr == CreateEvent(NULL, kEventClassSocketServer, kEventWakeUp, 0, + kEventAttributeUserEvent, &wake_up)); + OSStatus result = PostEventToQueue(event_queue_, wake_up, + kEventPriorityStandard); + if (noErr != result) { + LOG_E(LS_ERROR, OS, result) << "PostEventToQueue"; + } + ReleaseEvent(wake_up); +} + +#endif +} // namespace talk_base diff --git a/talk/base/macsocketserver.h b/talk/base/macsocketserver.h new file mode 100644 index 000000000..2febb7f91 --- /dev/null +++ b/talk/base/macsocketserver.h @@ -0,0 +1,130 @@ +// Copyright 2007, Google Inc. + + +#ifndef TALK_BASE_MACSOCKETSERVER_H__ +#define TALK_BASE_MACSOCKETSERVER_H__ + +#include +#ifdef OSX // Invalid on IOS +#include +#endif +#include "talk/base/physicalsocketserver.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// MacBaseSocketServer +/////////////////////////////////////////////////////////////////////////////// +class MacAsyncSocket; + +class MacBaseSocketServer : public PhysicalSocketServer { + public: + MacBaseSocketServer(); + virtual ~MacBaseSocketServer(); + + // SocketServer Interface + virtual Socket* CreateSocket(int type) { return NULL; } + virtual Socket* CreateSocket(int family, int type) { return NULL; } + + virtual AsyncSocket* CreateAsyncSocket(int type); + virtual AsyncSocket* CreateAsyncSocket(int family, int type); + + virtual bool Wait(int cms, bool process_io) = 0; + virtual void WakeUp() = 0; + + void RegisterSocket(MacAsyncSocket* socket); + void UnregisterSocket(MacAsyncSocket* socket); + + // PhysicalSocketServer Overrides + virtual bool SetPosixSignalHandler(int signum, void (*handler)(int)); + + protected: + void EnableSocketCallbacks(bool enable); + const std::set& sockets() { + return sockets_; + } + + private: + static void FileDescriptorCallback(CFFileDescriptorRef ref, + CFOptionFlags flags, + void* context); + + std::set sockets_; +}; + +// Core Foundation implementation of the socket server. While idle it +// will run the current CF run loop. When the socket server has work +// to do the run loop will be paused. Does not support Carbon or Cocoa +// UI interaction. +class MacCFSocketServer : public MacBaseSocketServer { + public: + MacCFSocketServer(); + virtual ~MacCFSocketServer(); + + // SocketServer Interface + virtual bool Wait(int cms, bool process_io); + virtual void WakeUp(); + void OnWakeUpCallback(); + + private: + CFRunLoopRef run_loop_; + CFRunLoopSourceRef wake_up_; +}; + +#ifndef CARBON_DEPRECATED + +/////////////////////////////////////////////////////////////////////////////// +// MacCarbonSocketServer +/////////////////////////////////////////////////////////////////////////////// + +// Interacts with the Carbon event queue. While idle it will block, +// waiting for events. When the socket server has work to do, it will +// post a 'wake up' event to the queue, causing the thread to exit the +// event loop until the next call to Wait. Other events are dispatched +// to their target. Supports Carbon and Cocoa UI interaction. +class MacCarbonSocketServer : public MacBaseSocketServer { + public: + MacCarbonSocketServer(); + virtual ~MacCarbonSocketServer(); + + // SocketServer Interface + virtual bool Wait(int cms, bool process_io); + virtual void WakeUp(); + + private: + EventQueueRef event_queue_; + EventRef wake_up_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// MacCarbonAppSocketServer +/////////////////////////////////////////////////////////////////////////////// + +// Runs the Carbon application event loop on the current thread while +// idle. When the socket server has work to do, it will post an event +// to the queue, causing the thread to exit the event loop until the +// next call to Wait. Other events are automatically dispatched to +// their target. +class MacCarbonAppSocketServer : public MacBaseSocketServer { + public: + MacCarbonAppSocketServer(); + virtual ~MacCarbonAppSocketServer(); + + // SocketServer Interface + virtual bool Wait(int cms, bool process_io); + virtual void WakeUp(); + + private: + static OSStatus WakeUpEventHandler(EventHandlerCallRef next, EventRef event, + void *data); + static void TimerHandler(EventLoopTimerRef timer, void *data); + + EventQueueRef event_queue_; + EventHandlerRef event_handler_; + EventLoopTimerRef timer_; +}; + +#endif +} // namespace talk_base + +#endif // TALK_BASE_MACSOCKETSERVER_H__ diff --git a/talk/base/macsocketserver_unittest.cc b/talk/base/macsocketserver_unittest.cc new file mode 100644 index 000000000..07cce263c --- /dev/null +++ b/talk/base/macsocketserver_unittest.cc @@ -0,0 +1,250 @@ +/* + * libjingle + * Copyright 2009, 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/base/gunit.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/socket_unittest.h" +#include "talk/base/thread.h" +#include "talk/base/macsocketserver.h" + +namespace talk_base { + +class WakeThread : public Thread { + public: + WakeThread(SocketServer* ss) : ss_(ss) { + } + void Run() { + ss_->WakeUp(); + } + private: + SocketServer* ss_; +}; + +#ifndef CARBON_DEPRECATED + +// Test that MacCFSocketServer::Wait works as expected. +TEST(MacCFSocketServerTest, TestWait) { + MacCFSocketServer server; + uint32 start = Time(); + server.Wait(1000, true); + EXPECT_GE(TimeSince(start), 1000); +} + +// Test that MacCFSocketServer::Wakeup works as expected. +TEST(MacCFSocketServerTest, TestWakeup) { + MacCFSocketServer server; + WakeThread thread(&server); + uint32 start = Time(); + thread.Start(); + server.Wait(10000, true); + EXPECT_LT(TimeSince(start), 10000); +} + +// Test that MacCarbonSocketServer::Wait works as expected. +TEST(MacCarbonSocketServerTest, TestWait) { + MacCarbonSocketServer server; + uint32 start = Time(); + server.Wait(1000, true); + EXPECT_GE(TimeSince(start), 1000); +} + +// Test that MacCarbonSocketServer::Wakeup works as expected. +TEST(MacCarbonSocketServerTest, TestWakeup) { + MacCarbonSocketServer server; + WakeThread thread(&server); + uint32 start = Time(); + thread.Start(); + server.Wait(10000, true); + EXPECT_LT(TimeSince(start), 10000); +} + +// Test that MacCarbonAppSocketServer::Wait works as expected. +TEST(MacCarbonAppSocketServerTest, TestWait) { + MacCarbonAppSocketServer server; + uint32 start = Time(); + server.Wait(1000, true); + EXPECT_GE(TimeSince(start), 1000); +} + +// Test that MacCarbonAppSocketServer::Wakeup works as expected. +TEST(MacCarbonAppSocketServerTest, TestWakeup) { + MacCarbonAppSocketServer server; + WakeThread thread(&server); + uint32 start = Time(); + thread.Start(); + server.Wait(10000, true); + EXPECT_LT(TimeSince(start), 10000); +} + +#endif + +// Test that MacAsyncSocket passes all the generic Socket tests. +class MacAsyncSocketTest : public SocketTest { + protected: + MacAsyncSocketTest() + : server_(CreateSocketServer()), + scope_(server_.get()) {} + // Override for other implementations of MacBaseSocketServer. + virtual MacBaseSocketServer* CreateSocketServer() { + return new MacCFSocketServer(); + }; + talk_base::scoped_ptr server_; + SocketServerScope scope_; +}; + +TEST_F(MacAsyncSocketTest, TestConnectIPv4) { + SocketTest::TestConnectIPv4(); +} + +TEST_F(MacAsyncSocketTest, TestConnectIPv6) { + SocketTest::TestConnectIPv6(); +} + +TEST_F(MacAsyncSocketTest, TestConnectWithDnsLookupIPv4) { + SocketTest::TestConnectWithDnsLookupIPv4(); +} + +TEST_F(MacAsyncSocketTest, TestConnectWithDnsLookupIPv6) { + SocketTest::TestConnectWithDnsLookupIPv6(); +} + +TEST_F(MacAsyncSocketTest, TestConnectFailIPv4) { + SocketTest::TestConnectFailIPv4(); +} + +TEST_F(MacAsyncSocketTest, TestConnectFailIPv6) { + SocketTest::TestConnectFailIPv6(); +} + +// Reenable once we have mac async dns +TEST_F(MacAsyncSocketTest, DISABLED_TestConnectWithDnsLookupFailIPv4) { + SocketTest::TestConnectWithDnsLookupFailIPv4(); +} + +TEST_F(MacAsyncSocketTest, DISABLED_TestConnectWithDnsLookupFailIPv6) { + SocketTest::TestConnectWithDnsLookupFailIPv6(); +} + +TEST_F(MacAsyncSocketTest, TestConnectWithClosedSocketIPv4) { + SocketTest::TestConnectWithClosedSocketIPv4(); +} + +TEST_F(MacAsyncSocketTest, TestConnectWithClosedSocketIPv6) { + SocketTest::TestConnectWithClosedSocketIPv6(); +} + +// Flaky at the moment (10% failure rate). Seems the client doesn't get +// signalled in a timely manner... +TEST_F(MacAsyncSocketTest, DISABLED_TestServerCloseDuringConnectIPv4) { + SocketTest::TestServerCloseDuringConnectIPv4(); +} + +TEST_F(MacAsyncSocketTest, DISABLED_TestServerCloseDuringConnectIPv6) { + SocketTest::TestServerCloseDuringConnectIPv6(); +} +// Flaky at the moment (0.5% failure rate). Seems the client doesn't get +// signalled in a timely manner... +TEST_F(MacAsyncSocketTest, TestClientCloseDuringConnectIPv4) { + SocketTest::TestClientCloseDuringConnectIPv4(); +} + +TEST_F(MacAsyncSocketTest, TestClientCloseDuringConnectIPv6) { + SocketTest::TestClientCloseDuringConnectIPv6(); +} + +TEST_F(MacAsyncSocketTest, TestServerCloseIPv4) { + SocketTest::TestServerCloseIPv4(); +} + +TEST_F(MacAsyncSocketTest, TestServerCloseIPv6) { + SocketTest::TestServerCloseIPv6(); +} + +TEST_F(MacAsyncSocketTest, TestCloseInClosedCallbackIPv4) { + SocketTest::TestCloseInClosedCallbackIPv4(); +} + +TEST_F(MacAsyncSocketTest, TestCloseInClosedCallbackIPv6) { + SocketTest::TestCloseInClosedCallbackIPv6(); +} + +TEST_F(MacAsyncSocketTest, TestSocketServerWaitIPv4) { + SocketTest::TestSocketServerWaitIPv4(); +} + +TEST_F(MacAsyncSocketTest, TestSocketServerWaitIPv6) { + SocketTest::TestSocketServerWaitIPv6(); +} + +TEST_F(MacAsyncSocketTest, TestTcpIPv4) { + SocketTest::TestTcpIPv4(); +} + +TEST_F(MacAsyncSocketTest, TestTcpIPv6) { + SocketTest::TestTcpIPv6(); +} + +TEST_F(MacAsyncSocketTest, TestSingleFlowControlCallbackIPv4) { + SocketTest::TestSingleFlowControlCallbackIPv4(); +} + +TEST_F(MacAsyncSocketTest, TestSingleFlowControlCallbackIPv6) { + SocketTest::TestSingleFlowControlCallbackIPv6(); +} + +TEST_F(MacAsyncSocketTest, DISABLED_TestUdpIPv4) { + SocketTest::TestUdpIPv4(); +} + +TEST_F(MacAsyncSocketTest, DISABLED_TestUdpIPv6) { + SocketTest::TestUdpIPv6(); +} + +TEST_F(MacAsyncSocketTest, DISABLED_TestGetSetOptionsIPv4) { + SocketTest::TestGetSetOptionsIPv4(); +} + +TEST_F(MacAsyncSocketTest, DISABLED_TestGetSetOptionsIPv6) { + SocketTest::TestGetSetOptionsIPv6(); +} + +#ifndef CARBON_DEPRECATED +class MacCarbonAppAsyncSocketTest : public MacAsyncSocketTest { + virtual MacBaseSocketServer* CreateSocketServer() { + return new MacCarbonAppSocketServer(); + }; +}; + +TEST_F(MacCarbonAppAsyncSocketTest, TestSocketServerWaitIPv4) { + SocketTest::TestSocketServerWaitIPv4(); +} + +TEST_F(MacCarbonAppAsyncSocketTest, TestSocketServerWaitIPv6) { + SocketTest::TestSocketServerWaitIPv6(); +} +#endif +} // namespace talk_base diff --git a/talk/base/macutils.cc b/talk/base/macutils.cc new file mode 100644 index 000000000..c73b0fa6f --- /dev/null +++ b/talk/base/macutils.cc @@ -0,0 +1,231 @@ +/* + * libjingle + * Copyright 2007 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 + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/macutils.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringutils.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// + +bool ToUtf8(const CFStringRef str16, std::string* str8) { + if ((NULL == str16) || (NULL == str8)) + return false; + size_t maxlen = CFStringGetMaximumSizeForEncoding(CFStringGetLength(str16), + kCFStringEncodingUTF8) + + 1; + scoped_array buffer(new char[maxlen]); + if (!buffer || !CFStringGetCString(str16, buffer.get(), maxlen, + kCFStringEncodingUTF8)) + return false; + str8->assign(buffer.get()); + return true; +} + +bool ToUtf16(const std::string& str8, CFStringRef* str16) { + if (NULL == str16) + return false; + *str16 = CFStringCreateWithBytes(kCFAllocatorDefault, + reinterpret_cast(str8.data()), + str8.length(), kCFStringEncodingUTF8, + false); + return (NULL != *str16); +} + +#ifdef OSX +void DecodeFourChar(UInt32 fc, std::string* out) { + std::stringstream ss; + ss << '\''; + bool printable = true; + for (int i = 3; i >= 0; --i) { + char ch = (fc >> (8 * i)) & 0xFF; + if (isprint(static_cast(ch))) { + ss << ch; + } else { + printable = false; + break; + } + } + if (printable) { + ss << '\''; + } else { + ss.str(""); + ss << "0x" << std::hex << fc; + } + out->append(ss.str()); +} + +static bool GetGestalt(OSType ostype, int* value) { + ASSERT(NULL != value); + SInt32 native_value; + OSStatus result = Gestalt(ostype, &native_value); + if (noErr == result) { + *value = native_value; + return true; + } + std::string str; + DecodeFourChar(ostype, &str); + LOG_E(LS_ERROR, OS, result) << "Gestalt(" << str << ")"; + return false; +} + +bool GetOSVersion(int* major, int* minor, int* bugfix) { + ASSERT(major && minor && bugfix); + if (!GetGestalt(gestaltSystemVersion, major)) + return false; + if (*major < 0x1040) { + *bugfix = *major & 0xF; + *minor = (*major >> 4) & 0xF; + *major = (*major >> 8); + return true; + } + return GetGestalt(gestaltSystemVersionMajor, major) + && GetGestalt(gestaltSystemVersionMinor, minor) + && GetGestalt(gestaltSystemVersionBugFix, bugfix); +} + +MacOSVersionName GetOSVersionName() { + int major = 0, minor = 0, bugfix = 0; + if (!GetOSVersion(&major, &minor, &bugfix)) + return kMacOSUnknown; + if (major > 10) { + return kMacOSNewer; + } + if ((major < 10) || (minor < 3)) { + return kMacOSOlder; + } + switch (minor) { + case 3: + return kMacOSPanther; + case 4: + return kMacOSTiger; + case 5: + return kMacOSLeopard; + case 6: + return kMacOSSnowLeopard; + case 7: + return kMacOSLion; + case 8: + return kMacOSMountainLion; + } + return kMacOSNewer; +} + +bool GetQuickTimeVersion(std::string* out) { + int ver; + if (!GetGestalt(gestaltQuickTimeVersion, &ver)) + return false; + + std::stringstream ss; + ss << std::hex << ver; + *out = ss.str(); + return true; +} + +bool RunAppleScript(const std::string& script) { + // TODO(thaloun): Add a .mm file that contains something like this: + // NSString source from script + // NSAppleScript* appleScript = [[NSAppleScript alloc] initWithSource:&source] + // if (appleScript != nil) { + // [appleScript executeAndReturnError:nil] + // [appleScript release] +#ifndef CARBON_DEPRECATED + ComponentInstance component = NULL; + AEDesc script_desc; + AEDesc result_data; + OSStatus err; + OSAID script_id, result_id; + + AECreateDesc(typeNull, NULL, 0, &script_desc); + AECreateDesc(typeNull, NULL, 0, &result_data); + script_id = kOSANullScript; + result_id = kOSANullScript; + + component = OpenDefaultComponent(kOSAComponentType, typeAppleScript); + if (component == NULL) { + LOG(LS_ERROR) << "Failed opening Apple Script component"; + return false; + } + err = AECreateDesc(typeUTF8Text, script.data(), script.size(), &script_desc); + if (err != noErr) { + CloseComponent(component); + LOG(LS_ERROR) << "Failed creating Apple Script description"; + return false; + } + + err = OSACompile(component, &script_desc, kOSAModeCanInteract, &script_id); + if (err != noErr) { + AEDisposeDesc(&script_desc); + if (script_id != kOSANullScript) { + OSADispose(component, script_id); + } + CloseComponent(component); + LOG(LS_ERROR) << "Error compiling Apple Script"; + return false; + } + + err = OSAExecute(component, script_id, kOSANullScript, kOSAModeCanInteract, + &result_id); + + if (err == errOSAScriptError) { + LOG(LS_ERROR) << "Error when executing Apple Script: " << script; + AECreateDesc(typeNull, NULL, 0, &result_data); + OSAScriptError(component, kOSAErrorMessage, typeChar, &result_data); + int len = AEGetDescDataSize(&result_data); + char* data = (char*) malloc(len); + if (data != NULL) { + err = AEGetDescData(&result_data, data, len); + LOG(LS_ERROR) << "Script error: " << data; + } + AEDisposeDesc(&script_desc); + AEDisposeDesc(&result_data); + return false; + } + AEDisposeDesc(&script_desc); + if (script_id != kOSANullScript) { + OSADispose(component, script_id); + } + if (result_id != kOSANullScript) { + OSADispose(component, result_id); + } + CloseComponent(component); + return true; +#else + // TODO(thaloun): Support applescripts with the NSAppleScript API. + return false; +#endif // CARBON_DEPRECATED +} +#endif // OSX + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/macutils.h b/talk/base/macutils.h new file mode 100644 index 000000000..ad5e7ad2b --- /dev/null +++ b/talk/base/macutils.h @@ -0,0 +1,75 @@ +/* + * libjingle + * Copyright 2007 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. + */ + +#ifndef TALK_BASE_MACUTILS_H__ +#define TALK_BASE_MACUTILS_H__ + +#include +#ifdef OSX +#include +#endif +#include + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// + +// Note that some of these functions work for both iOS and Mac OS X. The ones +// that are specific to Mac are #ifdef'ed as such. + +bool ToUtf8(const CFStringRef str16, std::string* str8); +bool ToUtf16(const std::string& str8, CFStringRef* str16); + +#ifdef OSX +void DecodeFourChar(UInt32 fc, std::string* out); + +enum MacOSVersionName { + kMacOSUnknown, // ??? + kMacOSOlder, // 10.2- + kMacOSPanther, // 10.3 + kMacOSTiger, // 10.4 + kMacOSLeopard, // 10.5 + kMacOSSnowLeopard, // 10.6 + kMacOSLion, // 10.7 + kMacOSMountainLion, // 10.8 + kMacOSNewer, // 10.9+ +}; + +bool GetOSVersion(int* major, int* minor, int* bugfix); +MacOSVersionName GetOSVersionName(); +bool GetQuickTimeVersion(std::string* version); + +// Runs the given apple script. Only supports scripts that does not +// require user interaction. +bool RunAppleScript(const std::string& script); +#endif + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_MACUTILS_H__ diff --git a/talk/base/macutils_unittest.cc b/talk/base/macutils_unittest.cc new file mode 100644 index 000000000..25858a277 --- /dev/null +++ b/talk/base/macutils_unittest.cc @@ -0,0 +1,58 @@ +/* + * libjingle + * Copyright 2009, 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/base/gunit.h" +#include "talk/base/macutils.h" + +TEST(MacUtilsTest, GetOsVersionName) { + talk_base::MacOSVersionName ver = talk_base::GetOSVersionName(); + EXPECT_NE(talk_base::kMacOSUnknown, ver); +} + +TEST(MacUtilsTest, GetQuickTimeVersion) { + std::string version; + EXPECT_TRUE(talk_base::GetQuickTimeVersion(&version)); +} + +TEST(MacUtilsTest, RunAppleScriptCompileError) { + std::string script("set value to to 5"); + EXPECT_FALSE(talk_base::RunAppleScript(script)); +} + +TEST(MacUtilsTest, RunAppleScriptRuntimeError) { + std::string script("set value to 5 / 0"); + EXPECT_FALSE(talk_base::RunAppleScript(script)); +} + +#ifdef CARBON_DEPRECATED +TEST(MacUtilsTest, DISABLED_RunAppleScriptSuccess) { +#else +TEST(MacUtilsTest, RunAppleScriptSuccess) { +#endif + std::string script("set value to 5"); + EXPECT_TRUE(talk_base::RunAppleScript(script)); +} diff --git a/talk/base/macwindowpicker.cc b/talk/base/macwindowpicker.cc new file mode 100644 index 000000000..209b4ab04 --- /dev/null +++ b/talk/base/macwindowpicker.cc @@ -0,0 +1,250 @@ +// Copyright 2010 Google Inc. All Rights Reserved + + +#include "talk/base/macwindowpicker.h" + +#include +#include +#include + +#include "talk/base/logging.h" +#include "talk/base/macutils.h" + +namespace talk_base { + +static const char* kCoreGraphicsName = + "/System/Library/Frameworks/ApplicationServices.framework/Frameworks/" + "CoreGraphics.framework/CoreGraphics"; + +static const char* kWindowListCopyWindowInfo = "CGWindowListCopyWindowInfo"; +static const char* kWindowListCreateDescriptionFromArray = + "CGWindowListCreateDescriptionFromArray"; + +// Function pointer for holding the CGWindowListCopyWindowInfo function. +typedef CFArrayRef(*CGWindowListCopyWindowInfoProc)(CGWindowListOption, + CGWindowID); + +// Function pointer for holding the CGWindowListCreateDescriptionFromArray +// function. +typedef CFArrayRef(*CGWindowListCreateDescriptionFromArrayProc)(CFArrayRef); + +MacWindowPicker::MacWindowPicker() : lib_handle_(NULL), get_window_list_(NULL), + get_window_list_desc_(NULL) { +} + +MacWindowPicker::~MacWindowPicker() { + if (lib_handle_ != NULL) { + dlclose(lib_handle_); + } +} + +bool MacWindowPicker::Init() { + // TODO: If this class grows to use more dynamically functions + // from the CoreGraphics framework, consider using + // talk/base/latebindingsymboltable.h. + lib_handle_ = dlopen(kCoreGraphicsName, RTLD_NOW); + if (lib_handle_ == NULL) { + LOG(LS_ERROR) << "Could not load CoreGraphics"; + return false; + } + + get_window_list_ = dlsym(lib_handle_, kWindowListCopyWindowInfo); + get_window_list_desc_ = + dlsym(lib_handle_, kWindowListCreateDescriptionFromArray); + if (get_window_list_ == NULL || get_window_list_desc_ == NULL) { + // The CGWindowListCopyWindowInfo and the + // CGWindowListCreateDescriptionFromArray functions was introduced + // in Leopard(10.5) so this is a normal failure on Tiger. + LOG(LS_INFO) << "Failed to load Core Graphics symbols"; + dlclose(lib_handle_); + lib_handle_ = NULL; + return false; + } + + return true; +} + +bool MacWindowPicker::IsVisible(const WindowId& id) { + // Init if we're not already inited. + if (get_window_list_desc_ == NULL && !Init()) { + return false; + } + CGWindowID ids[1]; + ids[0] = id.id(); + CFArrayRef window_id_array = + CFArrayCreate(NULL, reinterpret_cast(&ids), 1, NULL); + + CFArrayRef window_array = + reinterpret_cast( + get_window_list_desc_)(window_id_array); + if (window_array == NULL || 0 == CFArrayGetCount(window_array)) { + // Could not find the window. It might have been closed. + LOG(LS_INFO) << "Window not found"; + CFRelease(window_id_array); + return false; + } + + CFDictionaryRef window = reinterpret_cast( + CFArrayGetValueAtIndex(window_array, 0)); + CFBooleanRef is_visible = reinterpret_cast( + CFDictionaryGetValue(window, kCGWindowIsOnscreen)); + + // Check that the window is visible. If not we might crash. + bool visible = false; + if (is_visible != NULL) { + visible = CFBooleanGetValue(is_visible); + } + CFRelease(window_id_array); + CFRelease(window_array); + return visible; +} + +bool MacWindowPicker::MoveToFront(const WindowId& id) { + // Init if we're not already initialized. + if (get_window_list_desc_ == NULL && !Init()) { + return false; + } + CGWindowID ids[1]; + ids[0] = id.id(); + CFArrayRef window_id_array = + CFArrayCreate(NULL, reinterpret_cast(&ids), 1, NULL); + + CFArrayRef window_array = + reinterpret_cast( + get_window_list_desc_)(window_id_array); + if (window_array == NULL || 0 == CFArrayGetCount(window_array)) { + // Could not find the window. It might have been closed. + LOG(LS_INFO) << "Window not found"; + CFRelease(window_id_array); + return false; + } + + CFDictionaryRef window = reinterpret_cast( + CFArrayGetValueAtIndex(window_array, 0)); + CFStringRef window_name_ref = reinterpret_cast( + CFDictionaryGetValue(window, kCGWindowName)); + CFNumberRef application_pid = reinterpret_cast( + CFDictionaryGetValue(window, kCGWindowOwnerPID)); + + int pid_val; + CFNumberGetValue(application_pid, kCFNumberIntType, &pid_val); + std::string window_name; + ToUtf8(window_name_ref, &window_name); + + // Build an applescript that sets the selected window to front + // within the application. Then set the application to front. + bool result = true; + std::stringstream ss; + ss << "tell application \"System Events\"\n" + << "set proc to the first item of (every process whose unix id is " + << pid_val + << ")\n" + << "tell proc to perform action \"AXRaise\" of window \"" + << window_name + << "\"\n" + << "set the frontmost of proc to true\n" + << "end tell"; + if (!RunAppleScript(ss.str())) { + // This might happen to for example X applications where the X + // server spawns of processes with their own PID but the X server + // is still registered as owner to the application windows. As a + // workaround, we put the X server process to front, meaning that + // all X applications will show up. The drawback with this + // workaround is that the application that we really wanted to set + // to front might be behind another X application. + ProcessSerialNumber psn; + pid_t pid = pid_val; + int res = GetProcessForPID(pid, &psn); + if (res != 0) { + LOG(LS_ERROR) << "Failed getting process for pid"; + result = false; + } + res = SetFrontProcess(&psn); + if (res != 0) { + LOG(LS_ERROR) << "Failed setting process to front"; + result = false; + } + } + CFRelease(window_id_array); + CFRelease(window_array); + return result; +} + +bool MacWindowPicker::GetDesktopList(DesktopDescriptionList* descriptions) { + const uint32_t kMaxDisplays = 128; + CGDirectDisplayID active_displays[kMaxDisplays]; + uint32_t display_count = 0; + + CGError err = CGGetActiveDisplayList(kMaxDisplays, + active_displays, + &display_count); + if (err != kCGErrorSuccess) { + LOG_E(LS_ERROR, OS, err) << "Failed to enumerate the active displays."; + return false; + } + for (uint32_t i = 0; i < display_count; ++i) { + DesktopId id(active_displays[i], static_cast(i)); + // TODO: Figure out an appropriate desktop title. + DesktopDescription desc(id, ""); + desc.set_primary(CGDisplayIsMain(id.id())); + descriptions->push_back(desc); + } + return display_count > 0; +} + +bool MacWindowPicker::GetDesktopDimensions(const DesktopId& id, + int* width, + int* height) { + *width = CGDisplayPixelsWide(id.id()); + *height = CGDisplayPixelsHigh(id.id()); + return true; +} + +bool MacWindowPicker::GetWindowList(WindowDescriptionList* descriptions) { + // Init if we're not already inited. + if (get_window_list_ == NULL && !Init()) { + return false; + } + + // Only get onscreen, non-desktop windows. + CFArrayRef window_array = + reinterpret_cast(get_window_list_)( + kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, + kCGNullWindowID); + if (window_array == NULL) { + return false; + } + + // Check windows to make sure they have an id, title, and use window layer 0. + CFIndex i; + CFIndex count = CFArrayGetCount(window_array); + for (i = 0; i < count; ++i) { + CFDictionaryRef window = reinterpret_cast( + CFArrayGetValueAtIndex(window_array, i)); + CFStringRef window_title = reinterpret_cast( + CFDictionaryGetValue(window, kCGWindowName)); + CFNumberRef window_id = reinterpret_cast( + CFDictionaryGetValue(window, kCGWindowNumber)); + CFNumberRef window_layer = reinterpret_cast( + CFDictionaryGetValue(window, kCGWindowLayer)); + if (window_title != NULL && window_id != NULL && window_layer != NULL) { + std::string title_str; + int id_val, layer_val; + ToUtf8(window_title, &title_str); + CFNumberGetValue(window_id, kCFNumberIntType, &id_val); + CFNumberGetValue(window_layer, kCFNumberIntType, &layer_val); + + // Discard windows without a title. + if (layer_val == 0 && title_str.length() > 0) { + WindowId id(static_cast(id_val)); + WindowDescription desc(id, title_str); + descriptions->push_back(desc); + } + } + } + + CFRelease(window_array); + return true; +} + +} // namespace talk_base diff --git a/talk/base/macwindowpicker.h b/talk/base/macwindowpicker.h new file mode 100644 index 000000000..85fcc36dc --- /dev/null +++ b/talk/base/macwindowpicker.h @@ -0,0 +1,31 @@ +// Copyright 2010 Google Inc. All Rights Reserved + + +#ifndef TALK_BASE_MACWINDOWPICKER_H_ +#define TALK_BASE_MACWINDOWPICKER_H_ + +#include "talk/base/windowpicker.h" + +namespace talk_base { + +class MacWindowPicker : public WindowPicker { + public: + MacWindowPicker(); + ~MacWindowPicker(); + virtual bool Init(); + virtual bool IsVisible(const WindowId& id); + virtual bool MoveToFront(const WindowId& id); + virtual bool GetWindowList(WindowDescriptionList* descriptions); + virtual bool GetDesktopList(DesktopDescriptionList* descriptions); + virtual bool GetDesktopDimensions(const DesktopId& id, int* width, + int* height); + + private: + void* lib_handle_; + void* get_window_list_; + void* get_window_list_desc_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_MACWINDOWPICKER_H_ diff --git a/talk/base/macwindowpicker_unittest.cc b/talk/base/macwindowpicker_unittest.cc new file mode 100644 index 000000000..9cb67db9e --- /dev/null +++ b/talk/base/macwindowpicker_unittest.cc @@ -0,0 +1,39 @@ +// Copyright 2010 Google Inc. All Rights Reserved + + +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/macutils.h" +#include "talk/base/macwindowpicker.h" +#include "talk/base/windowpicker.h" + +#ifndef OSX +#error Only for Mac OSX +#endif + +namespace talk_base { + +bool IsLeopardOrLater() { + return GetOSVersionName() >= kMacOSLeopard; +} + +// Test that this works on new versions and fails acceptably on old versions. +TEST(MacWindowPickerTest, TestGetWindowList) { + MacWindowPicker picker, picker2; + WindowDescriptionList descriptions; + if (IsLeopardOrLater()) { + EXPECT_TRUE(picker.Init()); + EXPECT_TRUE(picker.GetWindowList(&descriptions)); + EXPECT_TRUE(picker2.GetWindowList(&descriptions)); // Init is optional + } else { + EXPECT_FALSE(picker.Init()); + EXPECT_FALSE(picker.GetWindowList(&descriptions)); + EXPECT_FALSE(picker2.GetWindowList(&descriptions)); + } +} + +// TODO: Add verification of the actual parsing, ie, add +// functionality to inject a fake get_window_array function which +// provide a pre-constructed list of windows. + +} // namespace talk_base diff --git a/talk/base/mathutils.h b/talk/base/mathutils.h new file mode 100644 index 000000000..eeb110a17 --- /dev/null +++ b/talk/base/mathutils.h @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2005 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. + */ + +#ifndef TALK_BASE_MATHUTILS_H_ +#define TALK_BASE_MATHUTILS_H_ + +#include + +#ifndef M_PI +#define M_PI 3.14159265359f +#endif + +#endif // TALK_BASE_MATHUTILS_H_ diff --git a/talk/base/md5.cc b/talk/base/md5.cc new file mode 100644 index 000000000..84e06f992 --- /dev/null +++ b/talk/base/md5.cc @@ -0,0 +1,218 @@ +/* + * This code implements the MD5 message-digest algorithm. + * The algorithm is due to Ron Rivest. This code was + * written by Colin Plumb in 1993, no copyright is claimed. + * This code is in the public domain; do with it what you wish. + * + * Equivalent code is available from RSA Data Security, Inc. + * This code has been tested against that, and is equivalent, + * except that you don't need to include two pages of legalese + * with every copy. + * + * To compute the message digest of a chunk of bytes, declare an + * MD5Context structure, pass it to MD5Init, call MD5Update as + * needed on buffers full of bytes, and then call MD5Final, which + * will fill a supplied 16-byte array with the digest. + */ + +// Changes from original C code: +// Ported to C++, type casting, Google code style. + +#include "talk/base/md5.h" + +// TODO: Avoid memcmpy - hash directly from memory. +#include // for memcpy(). + +#include "talk/base/byteorder.h" // for ARCH_CPU_LITTLE_ENDIAN. + +#ifdef ARCH_CPU_LITTLE_ENDIAN +#define ByteReverse(buf, len) // Nothing. +#else // ARCH_CPU_BIG_ENDIAN +static void ByteReverse(uint32* buf, int len) { + for (int i = 0; i < len; ++i) { + buf[i] = talk_base::GetLE32(&buf[i]); + } +} +#endif + +// Start MD5 accumulation. Set bit count to 0 and buffer to mysterious +// initialization constants. +void MD5Init(MD5Context* ctx) { + ctx->buf[0] = 0x67452301; + ctx->buf[1] = 0xefcdab89; + ctx->buf[2] = 0x98badcfe; + ctx->buf[3] = 0x10325476; + ctx->bits[0] = 0; + ctx->bits[1] = 0; +} + +// Update context to reflect the concatenation of another buffer full of bytes. +void MD5Update(MD5Context* ctx, const uint8* buf, size_t len) { + // Update bitcount. + uint32 t = ctx->bits[0]; + if ((ctx->bits[0] = t + (static_cast(len) << 3)) < t) { + ctx->bits[1]++; // Carry from low to high. + } + ctx->bits[1] += len >> 29; + t = (t >> 3) & 0x3f; // Bytes already in shsInfo->data. + + // Handle any leading odd-sized chunks. + if (t) { + uint8* p = reinterpret_cast(ctx->in) + t; + + t = 64-t; + if (len < t) { + memcpy(p, buf, len); + return; + } + memcpy(p, buf, t); + ByteReverse(ctx->in, 16); + MD5Transform(ctx->buf, ctx->in); + buf += t; + len -= t; + } + + // Process data in 64-byte chunks. + while (len >= 64) { + memcpy(ctx->in, buf, 64); + ByteReverse(ctx->in, 16); + MD5Transform(ctx->buf, ctx->in); + buf += 64; + len -= 64; + } + + // Handle any remaining bytes of data. + memcpy(ctx->in, buf, len); +} + +// Final wrapup - pad to 64-byte boundary with the bit pattern. +// 1 0* (64-bit count of bits processed, MSB-first) +void MD5Final(MD5Context* ctx, uint8 digest[16]) { + // Compute number of bytes mod 64. + uint32 count = (ctx->bits[0] >> 3) & 0x3F; + + // Set the first char of padding to 0x80. This is safe since there is + // always at least one byte free. + uint8* p = reinterpret_cast(ctx->in) + count; + *p++ = 0x80; + + // Bytes of padding needed to make 64 bytes. + count = 64 - 1 - count; + + // Pad out to 56 mod 64. + if (count < 8) { + // Two lots of padding: Pad the first block to 64 bytes. + memset(p, 0, count); + ByteReverse(ctx->in, 16); + MD5Transform(ctx->buf, ctx->in); + + // Now fill the next block with 56 bytes. + memset(ctx->in, 0, 56); + } else { + // Pad block to 56 bytes. + memset(p, 0, count - 8); + } + ByteReverse(ctx->in, 14); + + // Append length in bits and transform. + ctx->in[14] = ctx->bits[0]; + ctx->in[15] = ctx->bits[1]; + + MD5Transform(ctx->buf, ctx->in); + ByteReverse(ctx->buf, 4); + memcpy(digest, ctx->buf, 16); + memset(ctx, 0, sizeof(*ctx)); // In case it's sensitive. +} + +// The four core functions - F1 is optimized somewhat. +// #define F1(x, y, z) (x & y | ~x & z) +#define F1(x, y, z) (z ^ (x & (y ^ z))) +#define F2(x, y, z) F1(z, x, y) +#define F3(x, y, z) (x ^ y ^ z) +#define F4(x, y, z) (y ^ (x | ~z)) + +// This is the central step in the MD5 algorithm. +#define MD5STEP(f, w, x, y, z, data, s) \ + (w += f(x, y, z) + data, w = w << s | w >> (32 - s), w += x) + +// The core of the MD5 algorithm, this alters an existing MD5 hash to +// reflect the addition of 16 longwords of new data. MD5Update blocks +// the data and converts bytes into longwords for this routine. +void MD5Transform(uint32 buf[4], const uint32 in[16]) { + uint32 a = buf[0]; + uint32 b = buf[1]; + uint32 c = buf[2]; + uint32 d = buf[3]; + + MD5STEP(F1, a, b, c, d, in[ 0] + 0xd76aa478, 7); + MD5STEP(F1, d, a, b, c, in[ 1] + 0xe8c7b756, 12); + MD5STEP(F1, c, d, a, b, in[ 2] + 0x242070db, 17); + MD5STEP(F1, b, c, d, a, in[ 3] + 0xc1bdceee, 22); + MD5STEP(F1, a, b, c, d, in[ 4] + 0xf57c0faf, 7); + MD5STEP(F1, d, a, b, c, in[ 5] + 0x4787c62a, 12); + MD5STEP(F1, c, d, a, b, in[ 6] + 0xa8304613, 17); + MD5STEP(F1, b, c, d, a, in[ 7] + 0xfd469501, 22); + MD5STEP(F1, a, b, c, d, in[ 8] + 0x698098d8, 7); + MD5STEP(F1, d, a, b, c, in[ 9] + 0x8b44f7af, 12); + MD5STEP(F1, c, d, a, b, in[10] + 0xffff5bb1, 17); + MD5STEP(F1, b, c, d, a, in[11] + 0x895cd7be, 22); + MD5STEP(F1, a, b, c, d, in[12] + 0x6b901122, 7); + MD5STEP(F1, d, a, b, c, in[13] + 0xfd987193, 12); + MD5STEP(F1, c, d, a, b, in[14] + 0xa679438e, 17); + MD5STEP(F1, b, c, d, a, in[15] + 0x49b40821, 22); + + MD5STEP(F2, a, b, c, d, in[ 1] + 0xf61e2562, 5); + MD5STEP(F2, d, a, b, c, in[ 6] + 0xc040b340, 9); + MD5STEP(F2, c, d, a, b, in[11] + 0x265e5a51, 14); + MD5STEP(F2, b, c, d, a, in[ 0] + 0xe9b6c7aa, 20); + MD5STEP(F2, a, b, c, d, in[ 5] + 0xd62f105d, 5); + MD5STEP(F2, d, a, b, c, in[10] + 0x02441453, 9); + MD5STEP(F2, c, d, a, b, in[15] + 0xd8a1e681, 14); + MD5STEP(F2, b, c, d, a, in[ 4] + 0xe7d3fbc8, 20); + MD5STEP(F2, a, b, c, d, in[ 9] + 0x21e1cde6, 5); + MD5STEP(F2, d, a, b, c, in[14] + 0xc33707d6, 9); + MD5STEP(F2, c, d, a, b, in[ 3] + 0xf4d50d87, 14); + MD5STEP(F2, b, c, d, a, in[ 8] + 0x455a14ed, 20); + MD5STEP(F2, a, b, c, d, in[13] + 0xa9e3e905, 5); + MD5STEP(F2, d, a, b, c, in[ 2] + 0xfcefa3f8, 9); + MD5STEP(F2, c, d, a, b, in[ 7] + 0x676f02d9, 14); + MD5STEP(F2, b, c, d, a, in[12] + 0x8d2a4c8a, 20); + + MD5STEP(F3, a, b, c, d, in[ 5] + 0xfffa3942, 4); + MD5STEP(F3, d, a, b, c, in[ 8] + 0x8771f681, 11); + MD5STEP(F3, c, d, a, b, in[11] + 0x6d9d6122, 16); + MD5STEP(F3, b, c, d, a, in[14] + 0xfde5380c, 23); + MD5STEP(F3, a, b, c, d, in[ 1] + 0xa4beea44, 4); + MD5STEP(F3, d, a, b, c, in[ 4] + 0x4bdecfa9, 11); + MD5STEP(F3, c, d, a, b, in[ 7] + 0xf6bb4b60, 16); + MD5STEP(F3, b, c, d, a, in[10] + 0xbebfbc70, 23); + MD5STEP(F3, a, b, c, d, in[13] + 0x289b7ec6, 4); + MD5STEP(F3, d, a, b, c, in[ 0] + 0xeaa127fa, 11); + MD5STEP(F3, c, d, a, b, in[ 3] + 0xd4ef3085, 16); + MD5STEP(F3, b, c, d, a, in[ 6] + 0x04881d05, 23); + MD5STEP(F3, a, b, c, d, in[ 9] + 0xd9d4d039, 4); + MD5STEP(F3, d, a, b, c, in[12] + 0xe6db99e5, 11); + MD5STEP(F3, c, d, a, b, in[15] + 0x1fa27cf8, 16); + MD5STEP(F3, b, c, d, a, in[ 2] + 0xc4ac5665, 23); + + MD5STEP(F4, a, b, c, d, in[ 0] + 0xf4292244, 6); + MD5STEP(F4, d, a, b, c, in[ 7] + 0x432aff97, 10); + MD5STEP(F4, c, d, a, b, in[14] + 0xab9423a7, 15); + MD5STEP(F4, b, c, d, a, in[ 5] + 0xfc93a039, 21); + MD5STEP(F4, a, b, c, d, in[12] + 0x655b59c3, 6); + MD5STEP(F4, d, a, b, c, in[ 3] + 0x8f0ccc92, 10); + MD5STEP(F4, c, d, a, b, in[10] + 0xffeff47d, 15); + MD5STEP(F4, b, c, d, a, in[ 1] + 0x85845dd1, 21); + MD5STEP(F4, a, b, c, d, in[ 8] + 0x6fa87e4f, 6); + MD5STEP(F4, d, a, b, c, in[15] + 0xfe2ce6e0, 10); + MD5STEP(F4, c, d, a, b, in[ 6] + 0xa3014314, 15); + MD5STEP(F4, b, c, d, a, in[13] + 0x4e0811a1, 21); + MD5STEP(F4, a, b, c, d, in[ 4] + 0xf7537e82, 6); + MD5STEP(F4, d, a, b, c, in[11] + 0xbd3af235, 10); + MD5STEP(F4, c, d, a, b, in[ 2] + 0x2ad7d2bb, 15); + MD5STEP(F4, b, c, d, a, in[ 9] + 0xeb86d391, 21); + buf[0] += a; + buf[1] += b; + buf[2] += c; + buf[3] += d; +} diff --git a/talk/base/md5.h b/talk/base/md5.h new file mode 100644 index 000000000..3aba3d27a --- /dev/null +++ b/talk/base/md5.h @@ -0,0 +1,40 @@ +/* + * This is the header file for the MD5 message-digest algorithm. + * The algorithm is due to Ron Rivest. This code was + * written by Colin Plumb in 1993, no copyright is claimed. + * This code is in the public domain; do with it what you wish. + * + * Equivalent code is available from RSA Data Security, Inc. + * This code has been tested against that, and is equivalent, + * except that you don't need to include two pages of legalese + * with every copy. + * To compute the message digest of a chunk of bytes, declare an + * MD5Context structure, pass it to MD5Init, call MD5Update as + * needed on buffers full of bytes, and then call MD5Final, which + * will fill a supplied 16-byte array with the digest. + * + */ + +// Changes(fbarchard): Ported to C++ and Google style guide. +// Made context first parameter in MD5Final for consistency with Sha1. + +#ifndef TALK_BASE_MD5_H_ +#define TALK_BASE_MD5_H_ + +#include "talk/base/basictypes.h" + +// Canonical name for a MD5 context structure, used in many crypto libs. +typedef struct MD5Context MD5_CTX; + +struct MD5Context { + uint32 buf[4]; + uint32 bits[2]; + uint32 in[16]; +}; + +void MD5Init(MD5Context* context); +void MD5Update(MD5Context* context, const uint8* data, size_t len); +void MD5Final(MD5Context* context, uint8 digest[16]); +void MD5Transform(uint32 buf[4], const uint32 in[16]); + +#endif // TALK_BASE_MD5_H_ diff --git a/talk/base/md5digest.h b/talk/base/md5digest.h new file mode 100644 index 000000000..a6c2ea9d7 --- /dev/null +++ b/talk/base/md5digest.h @@ -0,0 +1,63 @@ +/* + * libjingle + * Copyright 2012 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. + */ + +#ifndef TALK_BASE_MD5DIGEST_H_ +#define TALK_BASE_MD5DIGEST_H_ + +#include "talk/base/md5.h" +#include "talk/base/messagedigest.h" + +namespace talk_base { + +// A simple wrapper for our MD5 implementation. +class Md5Digest : public MessageDigest { + public: + enum { kSize = 16 }; + Md5Digest() { + MD5Init(&ctx_); + } + virtual size_t Size() const { + return kSize; + } + virtual void Update(const void* buf, size_t len) { + MD5Update(&ctx_, static_cast(buf), len); + } + virtual size_t Finish(void* buf, size_t len) { + if (len < kSize) { + return 0; + } + MD5Final(&ctx_, static_cast(buf)); + MD5Init(&ctx_); // Reset for next use. + return kSize; + } + private: + MD5_CTX ctx_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_MD5DIGEST_H_ diff --git a/talk/base/md5digest_unittest.cc b/talk/base/md5digest_unittest.cc new file mode 100644 index 000000000..40b19e5a3 --- /dev/null +++ b/talk/base/md5digest_unittest.cc @@ -0,0 +1,96 @@ +/* + * libjingle + * Copyright 2012 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/base/md5digest.h" +#include "talk/base/gunit.h" +#include "talk/base/stringencode.h" + +namespace talk_base { + +std::string Md5(const std::string& input) { + Md5Digest md5; + return ComputeDigest(&md5, input); +} + +TEST(Md5DigestTest, TestSize) { + Md5Digest md5; + EXPECT_EQ(16U, Md5Digest::kSize); + EXPECT_EQ(16U, md5.Size()); +} + +TEST(Md5DigestTest, TestBasic) { + // These are the standard MD5 test vectors from RFC 1321. + EXPECT_EQ("d41d8cd98f00b204e9800998ecf8427e", Md5("")); + EXPECT_EQ("0cc175b9c0f1b6a831c399e269772661", Md5("a")); + EXPECT_EQ("900150983cd24fb0d6963f7d28e17f72", Md5("abc")); + EXPECT_EQ("f96b697d7cb7938d525a2f31aaf161d0", Md5("message digest")); + EXPECT_EQ("c3fcd3d76192e4007dfb496cca67e13b", + Md5("abcdefghijklmnopqrstuvwxyz")); +} + +TEST(Md5DigestTest, TestMultipleUpdates) { + Md5Digest md5; + std::string input = "abcdefghijklmnopqrstuvwxyz"; + char output[Md5Digest::kSize]; + for (size_t i = 0; i < input.size(); ++i) { + md5.Update(&input[i], 1); + } + EXPECT_EQ(md5.Size(), md5.Finish(output, sizeof(output))); + EXPECT_EQ("c3fcd3d76192e4007dfb496cca67e13b", + hex_encode(output, sizeof(output))); +} + +TEST(Md5DigestTest, TestReuse) { + Md5Digest md5; + std::string input = "message digest"; + EXPECT_EQ("f96b697d7cb7938d525a2f31aaf161d0", ComputeDigest(&md5, input)); + input = "abcdefghijklmnopqrstuvwxyz"; + EXPECT_EQ("c3fcd3d76192e4007dfb496cca67e13b", ComputeDigest(&md5, input)); +} + +TEST(Md5DigestTest, TestBufferTooSmall) { + Md5Digest md5; + std::string input = "abcdefghijklmnopqrstuvwxyz"; + char output[Md5Digest::kSize - 1]; + md5.Update(input.c_str(), input.size()); + EXPECT_EQ(0U, md5.Finish(output, sizeof(output))); +} + +TEST(Md5DigestTest, TestBufferConst) { + Md5Digest md5; + const int kLongSize = 1000000; + std::string input(kLongSize, '\0'); + for (int i = 0; i < kLongSize; ++i) { + input[i] = static_cast(i); + } + md5.Update(input.c_str(), input.size()); + for (int i = 0; i < kLongSize; ++i) { + EXPECT_EQ(static_cast(i), input[i]); + } +} + +} // namespace talk_base diff --git a/talk/base/messagedigest.cc b/talk/base/messagedigest.cc new file mode 100644 index 000000000..6136ae28b --- /dev/null +++ b/talk/base/messagedigest.cc @@ -0,0 +1,184 @@ +/* + * libjingle + * Copyright 2011, 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/base/messagedigest.h" + +#include + +#include "talk/base/sslconfig.h" +#if SSL_USE_OPENSSL +#include "talk/base/openssldigest.h" +#else +#include "talk/base/md5digest.h" +#include "talk/base/sha1digest.h" +#endif +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringencode.h" + +namespace talk_base { + +// From RFC 4572. +const char DIGEST_MD5[] = "md5"; +const char DIGEST_SHA_1[] = "sha-1"; +const char DIGEST_SHA_224[] = "sha-224"; +const char DIGEST_SHA_256[] = "sha-256"; +const char DIGEST_SHA_384[] = "sha-384"; +const char DIGEST_SHA_512[] = "sha-512"; + +static const size_t kBlockSize = 64; // valid for SHA-256 and down + +MessageDigest* MessageDigestFactory::Create(const std::string& alg) { +#if SSL_USE_OPENSSL + MessageDigest* digest = new OpenSSLDigest(alg); + if (digest->Size() == 0) { // invalid algorithm + delete digest; + digest = NULL; + } + return digest; +#else + MessageDigest* digest = NULL; + if (alg == DIGEST_MD5) { + digest = new Md5Digest(); + } else if (alg == DIGEST_SHA_1) { + digest = new Sha1Digest(); + } + return digest; +#endif +} + +size_t ComputeDigest(MessageDigest* digest, const void* input, size_t in_len, + void* output, size_t out_len) { + digest->Update(input, in_len); + return digest->Finish(output, out_len); +} + +size_t ComputeDigest(const std::string& alg, const void* input, size_t in_len, + void* output, size_t out_len) { + scoped_ptr digest(MessageDigestFactory::Create(alg)); + return (digest) ? + ComputeDigest(digest.get(), input, in_len, output, out_len) : + 0; +} + +std::string ComputeDigest(MessageDigest* digest, const std::string& input) { + scoped_array output(new char[digest->Size()]); + ComputeDigest(digest, input.data(), input.size(), + output.get(), digest->Size()); + return hex_encode(output.get(), digest->Size()); +} + +bool ComputeDigest(const std::string& alg, const std::string& input, + std::string* output) { + scoped_ptr digest(MessageDigestFactory::Create(alg)); + if (!digest) { + return false; + } + *output = ComputeDigest(digest.get(), input); + return true; +} + +std::string ComputeDigest(const std::string& alg, const std::string& input) { + std::string output; + ComputeDigest(alg, input, &output); + return output; +} + +// Compute a RFC 2104 HMAC: H(K XOR opad, H(K XOR ipad, text)) +size_t ComputeHmac(MessageDigest* digest, + const void* key, size_t key_len, + const void* input, size_t in_len, + void* output, size_t out_len) { + // We only handle algorithms with a 64-byte blocksize. + // TODO: Add BlockSize() method to MessageDigest. + size_t block_len = kBlockSize; + if (digest->Size() > 32) { + return 0; + } + // Copy the key to a block-sized buffer to simplify padding. + // If the key is longer than a block, hash it and use the result instead. + scoped_array new_key(new uint8[block_len]); + if (key_len > block_len) { + ComputeDigest(digest, key, key_len, new_key.get(), block_len); + memset(new_key.get() + digest->Size(), 0, block_len - digest->Size()); + } else { + memcpy(new_key.get(), key, key_len); + memset(new_key.get() + key_len, 0, block_len - key_len); + } + // Set up the padding from the key, salting appropriately for each padding. + scoped_array o_pad(new uint8[block_len]), i_pad(new uint8[block_len]); + for (size_t i = 0; i < block_len; ++i) { + o_pad[i] = 0x5c ^ new_key[i]; + i_pad[i] = 0x36 ^ new_key[i]; + } + // Inner hash; hash the inner padding, and then the input buffer. + scoped_array inner(new uint8[digest->Size()]); + digest->Update(i_pad.get(), block_len); + digest->Update(input, in_len); + digest->Finish(inner.get(), digest->Size()); + // Outer hash; hash the outer padding, and then the result of the inner hash. + digest->Update(o_pad.get(), block_len); + digest->Update(inner.get(), digest->Size()); + return digest->Finish(output, out_len); +} + +size_t ComputeHmac(const std::string& alg, const void* key, size_t key_len, + const void* input, size_t in_len, + void* output, size_t out_len) { + scoped_ptr digest(MessageDigestFactory::Create(alg)); + if (!digest) { + return 0; + } + return ComputeHmac(digest.get(), key, key_len, + input, in_len, output, out_len); +} + +std::string ComputeHmac(MessageDigest* digest, const std::string& key, + const std::string& input) { + scoped_array output(new char[digest->Size()]); + ComputeHmac(digest, key.data(), key.size(), + input.data(), input.size(), output.get(), digest->Size()); + return hex_encode(output.get(), digest->Size()); +} + +bool ComputeHmac(const std::string& alg, const std::string& key, + const std::string& input, std::string* output) { + scoped_ptr digest(MessageDigestFactory::Create(alg)); + if (!digest) { + return false; + } + *output = ComputeHmac(digest.get(), key, input); + return true; +} + +std::string ComputeHmac(const std::string& alg, const std::string& key, + const std::string& input) { + std::string output; + ComputeHmac(alg, key, input, &output); + return output; +} + +} // namespace talk_base diff --git a/talk/base/messagedigest.h b/talk/base/messagedigest.h new file mode 100644 index 000000000..734082b01 --- /dev/null +++ b/talk/base/messagedigest.h @@ -0,0 +1,123 @@ +/* + * libjingle + * Copyright 2004, 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. + */ + +#ifndef TALK_BASE_MESSAGEDIGEST_H_ +#define TALK_BASE_MESSAGEDIGEST_H_ + +#include + +namespace talk_base { + +// Definitions for the digest algorithms. +extern const char DIGEST_MD5[]; +extern const char DIGEST_SHA_1[]; +extern const char DIGEST_SHA_224[]; +extern const char DIGEST_SHA_256[]; +extern const char DIGEST_SHA_384[]; +extern const char DIGEST_SHA_512[]; + +// A general class for computing hashes. +class MessageDigest { + public: + enum { kMaxSize = 64 }; // Maximum known size (SHA-512) + virtual ~MessageDigest() {} + // Returns the digest output size (e.g. 16 bytes for MD5). + virtual size_t Size() const = 0; + // Updates the digest with |len| bytes from |buf|. + virtual void Update(const void* buf, size_t len) = 0; + // Outputs the digest value to |buf| with length |len|. + // Returns the number of bytes written, i.e., Size(). + virtual size_t Finish(void* buf, size_t len) = 0; +}; + +// A factory class for creating digest objects. +class MessageDigestFactory { + public: + static MessageDigest* Create(const std::string& alg); +}; + +// Functions to create hashes. + +// Computes the hash of |in_len| bytes of |input|, using the |digest| hash +// implementation, and outputs the hash to the buffer |output|, which is +// |out_len| bytes long. Returns the number of bytes written to |output| if +// successful, or 0 if |out_len| was too small. +size_t ComputeDigest(MessageDigest* digest, const void* input, size_t in_len, + void* output, size_t out_len); +// Like the previous function, but creates a digest implementation based on +// the desired digest name |alg|, e.g. DIGEST_SHA_1. Returns 0 if there is no +// digest with the given name. +size_t ComputeDigest(const std::string& alg, const void* input, size_t in_len, + void* output, size_t out_len); +// Computes the hash of |input| using the |digest| hash implementation, and +// returns it as a hex-encoded string. +std::string ComputeDigest(MessageDigest* digest, const std::string& input); +// Like the previous function, but creates a digest implementation based on +// the desired digest name |alg|, e.g. DIGEST_SHA_1. Returns empty string if +// there is no digest with the given name. +std::string ComputeDigest(const std::string& alg, const std::string& input); +// Like the previous function, but returns an explicit result code. +bool ComputeDigest(const std::string& alg, const std::string& input, + std::string* output); + +// Shorthand way to compute a hex-encoded hash using MD5. +inline std::string MD5(const std::string& input) { + return ComputeDigest(DIGEST_MD5, input); +} + +// Functions to compute RFC 2104 HMACs. + +// Computes the HMAC of |in_len| bytes of |input|, using the |digest| hash +// implementation and |key_len| bytes of |key| to key the HMAC, and outputs +// the HMAC to the buffer |output|, which is |out_len| bytes long. Returns the +// number of bytes written to |output| if successful, or 0 if |out_len| was too +// small. +size_t ComputeHmac(MessageDigest* digest, const void* key, size_t key_len, + const void* input, size_t in_len, + void* output, size_t out_len); +// Like the previous function, but creates a digest implementation based on +// the desired digest name |alg|, e.g. DIGEST_SHA_1. Returns 0 if there is no +// digest with the given name. +size_t ComputeHmac(const std::string& alg, const void* key, size_t key_len, + const void* input, size_t in_len, + void* output, size_t out_len); +// Computes the HMAC of |input| using the |digest| hash implementation and |key| +// to key the HMAC, and returns it as a hex-encoded string. +std::string ComputeHmac(MessageDigest* digest, const std::string& key, + const std::string& input); +// Like the previous function, but creates a digest implementation based on +// the desired digest name |alg|, e.g. DIGEST_SHA_1. Returns empty string if +// there is no digest with the given name. +std::string ComputeHmac(const std::string& alg, const std::string& key, + const std::string& input); +// Like the previous function, but returns an explicit result code. +bool ComputeHmac(const std::string& alg, const std::string& key, + const std::string& input, std::string* output); + +} // namespace talk_base + +#endif // TALK_BASE_MESSAGEDIGEST_H_ diff --git a/talk/base/messagedigest_unittest.cc b/talk/base/messagedigest_unittest.cc new file mode 100644 index 000000000..cd68e860b --- /dev/null +++ b/talk/base/messagedigest_unittest.cc @@ -0,0 +1,168 @@ +/* + * libjingle + * Copyright 2012, 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/base/gunit.h" +#include "talk/base/messagedigest.h" +#include "talk/base/stringencode.h" + +namespace talk_base { + +// Test vectors from RFC 1321. +TEST(MessageDigestTest, TestMd5Digest) { + // Test the string versions of the APIs. + EXPECT_EQ("d41d8cd98f00b204e9800998ecf8427e", + ComputeDigest(DIGEST_MD5, "")); + EXPECT_EQ("900150983cd24fb0d6963f7d28e17f72", + ComputeDigest(DIGEST_MD5, "abc")); + EXPECT_EQ("c3fcd3d76192e4007dfb496cca67e13b", + ComputeDigest(DIGEST_MD5, "abcdefghijklmnopqrstuvwxyz")); + + // Test the raw buffer versions of the APIs; also check output buffer size. + char output[16]; + EXPECT_EQ(sizeof(output), + ComputeDigest(DIGEST_MD5, "abc", 3, output, sizeof(output))); + EXPECT_EQ("900150983cd24fb0d6963f7d28e17f72", + hex_encode(output, sizeof(output))); + EXPECT_EQ(0U, + ComputeDigest(DIGEST_MD5, "abc", 3, output, sizeof(output) - 1)); +} + +// Test vectors from RFC 3174. +TEST(MessageDigestTest, TestSha1Digest) { + // Test the string versions of the APIs. + EXPECT_EQ("da39a3ee5e6b4b0d3255bfef95601890afd80709", + ComputeDigest(DIGEST_SHA_1, "")); + EXPECT_EQ("a9993e364706816aba3e25717850c26c9cd0d89d", + ComputeDigest(DIGEST_SHA_1, "abc")); + EXPECT_EQ("84983e441c3bd26ebaae4aa1f95129e5e54670f1", + ComputeDigest(DIGEST_SHA_1, + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")); + + // Test the raw buffer versions of the APIs; also check output buffer size. + char output[20]; + EXPECT_EQ(sizeof(output), + ComputeDigest(DIGEST_SHA_1, "abc", 3, output, sizeof(output))); + EXPECT_EQ("a9993e364706816aba3e25717850c26c9cd0d89d", + hex_encode(output, sizeof(output))); + EXPECT_EQ(0U, + ComputeDigest(DIGEST_SHA_1, "abc", 3, output, sizeof(output) - 1)); +} + +// Test that we fail properly if a bad digest algorithm is specified. +TEST(MessageDigestTest, TestBadDigest) { + std::string output; + EXPECT_FALSE(ComputeDigest("sha-9000", "abc", &output)); + EXPECT_EQ("", ComputeDigest("sha-9000", "abc")); +} + +// Test vectors from RFC 2202. +TEST(MessageDigestTest, TestMd5Hmac) { + // Test the string versions of the APIs. + EXPECT_EQ("9294727a3638bb1c13f48ef8158bfc9d", + ComputeHmac(DIGEST_MD5, std::string(16, '\x0b'), "Hi There")); + EXPECT_EQ("750c783e6ab0b503eaa86e310a5db738", + ComputeHmac(DIGEST_MD5, "Jefe", "what do ya want for nothing?")); + EXPECT_EQ("56be34521d144c88dbb8c733f0e8b3f6", + ComputeHmac(DIGEST_MD5, std::string(16, '\xaa'), + std::string(50, '\xdd'))); + EXPECT_EQ("697eaf0aca3a3aea3a75164746ffaa79", + ComputeHmac(DIGEST_MD5, + "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19", + std::string(50, '\xcd'))); + EXPECT_EQ("56461ef2342edc00f9bab995690efd4c", + ComputeHmac(DIGEST_MD5, std::string(16, '\x0c'), + "Test With Truncation")); + EXPECT_EQ("6b1ab7fe4bd7bf8f0b62e6ce61b9d0cd", + ComputeHmac(DIGEST_MD5, std::string(80, '\xaa'), + "Test Using Larger Than Block-Size Key - Hash Key First")); + EXPECT_EQ("6f630fad67cda0ee1fb1f562db3aa53e", + ComputeHmac(DIGEST_MD5, std::string(80, '\xaa'), + "Test Using Larger Than Block-Size Key and Larger " + "Than One Block-Size Data")); + + // Test the raw buffer versions of the APIs; also check output buffer size. + std::string key(16, '\x0b'); + std::string input("Hi There"); + char output[16]; + EXPECT_EQ(sizeof(output), + ComputeHmac(DIGEST_MD5, key.c_str(), key.size(), + input.c_str(), input.size(), output, sizeof(output))); + EXPECT_EQ("9294727a3638bb1c13f48ef8158bfc9d", + hex_encode(output, sizeof(output))); + EXPECT_EQ(0U, + ComputeHmac(DIGEST_MD5, key.c_str(), key.size(), + input.c_str(), input.size(), output, sizeof(output) - 1)); +} + +// Test vectors from RFC 2202. +TEST(MessageDigestTest, TestSha1Hmac) { + // Test the string versions of the APIs. + EXPECT_EQ("b617318655057264e28bc0b6fb378c8ef146be00", + ComputeHmac(DIGEST_SHA_1, std::string(20, '\x0b'), "Hi There")); + EXPECT_EQ("effcdf6ae5eb2fa2d27416d5f184df9c259a7c79", + ComputeHmac(DIGEST_SHA_1, "Jefe", "what do ya want for nothing?")); + EXPECT_EQ("125d7342b9ac11cd91a39af48aa17b4f63f175d3", + ComputeHmac(DIGEST_SHA_1, std::string(20, '\xaa'), + std::string(50, '\xdd'))); + EXPECT_EQ("4c9007f4026250c6bc8414f9bf50c86c2d7235da", + ComputeHmac(DIGEST_SHA_1, + "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19", + std::string(50, '\xcd'))); + EXPECT_EQ("4c1a03424b55e07fe7f27be1d58bb9324a9a5a04", + ComputeHmac(DIGEST_SHA_1, std::string(20, '\x0c'), + "Test With Truncation")); + EXPECT_EQ("aa4ae5e15272d00e95705637ce8a3b55ed402112", + ComputeHmac(DIGEST_SHA_1, std::string(80, '\xaa'), + "Test Using Larger Than Block-Size Key - Hash Key First")); + EXPECT_EQ("e8e99d0f45237d786d6bbaa7965c7808bbff1a91", + ComputeHmac(DIGEST_SHA_1, std::string(80, '\xaa'), + "Test Using Larger Than Block-Size Key and Larger " + "Than One Block-Size Data")); + + // Test the raw buffer versions of the APIs; also check output buffer size. + std::string key(20, '\x0b'); + std::string input("Hi There"); + char output[20]; + EXPECT_EQ(sizeof(output), + ComputeHmac(DIGEST_SHA_1, key.c_str(), key.size(), + input.c_str(), input.size(), output, sizeof(output))); + EXPECT_EQ("b617318655057264e28bc0b6fb378c8ef146be00", + hex_encode(output, sizeof(output))); + EXPECT_EQ(0U, + ComputeHmac(DIGEST_SHA_1, key.c_str(), key.size(), + input.c_str(), input.size(), output, sizeof(output) - 1)); +} + +TEST(MessageDigestTest, TestBadHmac) { + std::string output; + EXPECT_FALSE(ComputeHmac("sha-9000", "key", "abc", &output)); + EXPECT_EQ("", ComputeHmac("sha-9000", "key", "abc")); +} + +} // namespace talk_base diff --git a/talk/base/messagehandler.cc b/talk/base/messagehandler.cc new file mode 100644 index 000000000..5b3585b16 --- /dev/null +++ b/talk/base/messagehandler.cc @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/messagehandler.h" +#include "talk/base/messagequeue.h" + +namespace talk_base { + +MessageHandler::~MessageHandler() { + MessageQueueManager::Instance()->Clear(this); +} + +} // namespace talk_base diff --git a/talk/base/messagehandler.h b/talk/base/messagehandler.h new file mode 100644 index 000000000..913edf8ce --- /dev/null +++ b/talk/base/messagehandler.h @@ -0,0 +1,53 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_MESSAGEHANDLER_H_ +#define TALK_BASE_MESSAGEHANDLER_H_ + +#include "talk/base/constructormagic.h" + +namespace talk_base { + +struct Message; + +// Messages get dispatched to a MessageHandler + +class MessageHandler { + public: + virtual void OnMessage(Message* msg) = 0; + + protected: + MessageHandler() {} + virtual ~MessageHandler(); + + private: + DISALLOW_COPY_AND_ASSIGN(MessageHandler); +}; + +} // namespace talk_base + +#endif // TALK_BASE_MESSAGEHANDLER_H_ diff --git a/talk/base/messagequeue.cc b/talk/base/messagequeue.cc new file mode 100644 index 000000000..5c4062216 --- /dev/null +++ b/talk/base/messagequeue.cc @@ -0,0 +1,388 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifdef POSIX +#include +#endif + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/messagequeue.h" +#include "talk/base/physicalsocketserver.h" + + +namespace talk_base { + +const uint32 kMaxMsgLatency = 150; // 150 ms + +//------------------------------------------------------------------ +// MessageQueueManager + +MessageQueueManager* MessageQueueManager::instance_; + +MessageQueueManager* MessageQueueManager::Instance() { + // Note: This is not thread safe, but it is first called before threads are + // spawned. + if (!instance_) + instance_ = new MessageQueueManager; + return instance_; +} + +MessageQueueManager::MessageQueueManager() { +} + +MessageQueueManager::~MessageQueueManager() { +} + +void MessageQueueManager::Add(MessageQueue *message_queue) { + // MessageQueueManager methods should be non-reentrant, so we + // ASSERT that is the case. If any of these ASSERT, please + // contact bpm or jbeda. + ASSERT(!crit_.CurrentThreadIsOwner()); + CritScope cs(&crit_); + message_queues_.push_back(message_queue); +} + +void MessageQueueManager::Remove(MessageQueue *message_queue) { + ASSERT(!crit_.CurrentThreadIsOwner()); // See note above. + // If this is the last MessageQueue, destroy the manager as well so that + // we don't leak this object at program shutdown. As mentioned above, this is + // not thread-safe, but this should only happen at program termination (when + // the ThreadManager is destroyed, and threads are no longer active). + bool destroy = false; + { + CritScope cs(&crit_); + std::vector::iterator iter; + iter = std::find(message_queues_.begin(), message_queues_.end(), + message_queue); + if (iter != message_queues_.end()) { + message_queues_.erase(iter); + } + destroy = message_queues_.empty(); + } + if (destroy) { + instance_ = NULL; + delete this; + } +} + +void MessageQueueManager::Clear(MessageHandler *handler) { + ASSERT(!crit_.CurrentThreadIsOwner()); // See note above. + CritScope cs(&crit_); + std::vector::iterator iter; + for (iter = message_queues_.begin(); iter != message_queues_.end(); iter++) + (*iter)->Clear(handler); +} + +//------------------------------------------------------------------ +// MessageQueue + +MessageQueue::MessageQueue(SocketServer* ss) + : ss_(ss), fStop_(false), fPeekKeep_(false), active_(false), + dmsgq_next_num_(0) { + if (!ss_) { + // Currently, MessageQueue holds a socket server, and is the base class for + // Thread. It seems like it makes more sense for Thread to hold the socket + // server, and provide it to the MessageQueue, since the Thread controls + // the I/O model, and MQ is agnostic to those details. Anyway, this causes + // messagequeue_unittest to depend on network libraries... yuck. + default_ss_.reset(new PhysicalSocketServer()); + ss_ = default_ss_.get(); + } + ss_->SetMessageQueue(this); +} + +MessageQueue::~MessageQueue() { + // The signal is done from here to ensure + // that it always gets called when the queue + // is going away. + SignalQueueDestroyed(); + if (active_) { + MessageQueueManager::Instance()->Remove(this); + Clear(NULL); + } + if (ss_) { + ss_->SetMessageQueue(NULL); + } +} + +void MessageQueue::set_socketserver(SocketServer* ss) { + ss_ = ss ? ss : default_ss_.get(); + ss_->SetMessageQueue(this); +} + +void MessageQueue::Quit() { + fStop_ = true; + ss_->WakeUp(); +} + +bool MessageQueue::IsQuitting() { + return fStop_; +} + +void MessageQueue::Restart() { + fStop_ = false; +} + +bool MessageQueue::Peek(Message *pmsg, int cmsWait) { + if (fPeekKeep_) { + *pmsg = msgPeek_; + return true; + } + if (!Get(pmsg, cmsWait)) + return false; + msgPeek_ = *pmsg; + fPeekKeep_ = true; + return true; +} + +bool MessageQueue::Get(Message *pmsg, int cmsWait, bool process_io) { + // Return and clear peek if present + // Always return the peek if it exists so there is Peek/Get symmetry + + if (fPeekKeep_) { + *pmsg = msgPeek_; + fPeekKeep_ = false; + return true; + } + + // Get w/wait + timer scan / dispatch + socket / event multiplexer dispatch + + int cmsTotal = cmsWait; + int cmsElapsed = 0; + uint32 msStart = Time(); + uint32 msCurrent = msStart; + while (true) { + // Check for sent messages + ReceiveSends(); + + // Check for posted events + int cmsDelayNext = kForever; + bool first_pass = true; + while (true) { + // All queue operations need to be locked, but nothing else in this loop + // (specifically handling disposed message) can happen inside the crit. + // Otherwise, disposed MessageHandlers will cause deadlocks. + { + CritScope cs(&crit_); + // On the first pass, check for delayed messages that have been + // triggered and calculate the next trigger time. + if (first_pass) { + first_pass = false; + while (!dmsgq_.empty()) { + if (TimeIsLater(msCurrent, dmsgq_.top().msTrigger_)) { + cmsDelayNext = TimeDiff(dmsgq_.top().msTrigger_, msCurrent); + break; + } + msgq_.push_back(dmsgq_.top().msg_); + dmsgq_.pop(); + } + } + // Pull a message off the message queue, if available. + if (msgq_.empty()) { + break; + } else { + *pmsg = msgq_.front(); + msgq_.pop_front(); + } + } // crit_ is released here. + + // Log a warning for time-sensitive messages that we're late to deliver. + if (pmsg->ts_sensitive) { + int32 delay = TimeDiff(msCurrent, pmsg->ts_sensitive); + if (delay > 0) { + LOG_F(LS_WARNING) << "id: " << pmsg->message_id << " delay: " + << (delay + kMaxMsgLatency) << "ms"; + } + } + // If this was a dispose message, delete it and skip it. + if (MQID_DISPOSE == pmsg->message_id) { + ASSERT(NULL == pmsg->phandler); + delete pmsg->pdata; + *pmsg = Message(); + continue; + } + return true; + } + + if (fStop_) + break; + + // Which is shorter, the delay wait or the asked wait? + + int cmsNext; + if (cmsWait == kForever) { + cmsNext = cmsDelayNext; + } else { + cmsNext = _max(0, cmsTotal - cmsElapsed); + if ((cmsDelayNext != kForever) && (cmsDelayNext < cmsNext)) + cmsNext = cmsDelayNext; + } + + // Wait and multiplex in the meantime + if (!ss_->Wait(cmsNext, process_io)) + return false; + + // If the specified timeout expired, return + + msCurrent = Time(); + cmsElapsed = TimeDiff(msCurrent, msStart); + if (cmsWait != kForever) { + if (cmsElapsed >= cmsWait) + return false; + } + } + return false; +} + +void MessageQueue::ReceiveSends() { +} + +void MessageQueue::Post(MessageHandler *phandler, uint32 id, + MessageData *pdata, bool time_sensitive) { + if (fStop_) + return; + + // Keep thread safe + // Add the message to the end of the queue + // Signal for the multiplexer to return + + CritScope cs(&crit_); + EnsureActive(); + Message msg; + msg.phandler = phandler; + msg.message_id = id; + msg.pdata = pdata; + if (time_sensitive) { + msg.ts_sensitive = Time() + kMaxMsgLatency; + } + msgq_.push_back(msg); + ss_->WakeUp(); +} + +void MessageQueue::DoDelayPost(int cmsDelay, uint32 tstamp, + MessageHandler *phandler, uint32 id, MessageData* pdata) { + if (fStop_) + return; + + // Keep thread safe + // Add to the priority queue. Gets sorted soonest first. + // Signal for the multiplexer to return. + + CritScope cs(&crit_); + EnsureActive(); + Message msg; + msg.phandler = phandler; + msg.message_id = id; + msg.pdata = pdata; + DelayedMessage dmsg(cmsDelay, tstamp, dmsgq_next_num_, msg); + dmsgq_.push(dmsg); + // If this message queue processes 1 message every millisecond for 50 days, + // we will wrap this number. Even then, only messages with identical times + // will be misordered, and then only briefly. This is probably ok. + VERIFY(0 != ++dmsgq_next_num_); + ss_->WakeUp(); +} + +int MessageQueue::GetDelay() { + CritScope cs(&crit_); + + if (!msgq_.empty()) + return 0; + + if (!dmsgq_.empty()) { + int delay = TimeUntil(dmsgq_.top().msTrigger_); + if (delay < 0) + delay = 0; + return delay; + } + + return kForever; +} + +void MessageQueue::Clear(MessageHandler *phandler, uint32 id, + MessageList* removed) { + CritScope cs(&crit_); + + // Remove messages with phandler + + if (fPeekKeep_ && msgPeek_.Match(phandler, id)) { + if (removed) { + removed->push_back(msgPeek_); + } else { + delete msgPeek_.pdata; + } + fPeekKeep_ = false; + } + + // Remove from ordered message queue + + for (MessageList::iterator it = msgq_.begin(); it != msgq_.end();) { + if (it->Match(phandler, id)) { + if (removed) { + removed->push_back(*it); + } else { + delete it->pdata; + } + it = msgq_.erase(it); + } else { + ++it; + } + } + + // Remove from priority queue. Not directly iterable, so use this approach + + PriorityQueue::container_type::iterator new_end = dmsgq_.container().begin(); + for (PriorityQueue::container_type::iterator it = new_end; + it != dmsgq_.container().end(); ++it) { + if (it->msg_.Match(phandler, id)) { + if (removed) { + removed->push_back(it->msg_); + } else { + delete it->msg_.pdata; + } + } else { + *new_end++ = *it; + } + } + dmsgq_.container().erase(new_end, dmsgq_.container().end()); + dmsgq_.reheap(); +} + +void MessageQueue::Dispatch(Message *pmsg) { + pmsg->phandler->OnMessage(pmsg); +} + +void MessageQueue::EnsureActive() { + ASSERT(crit_.CurrentThreadIsOwner()); + if (!active_) { + active_ = true; + MessageQueueManager::Instance()->Add(this); + } +} + +} // namespace talk_base diff --git a/talk/base/messagequeue.h b/talk/base/messagequeue.h new file mode 100644 index 000000000..331f20736 --- /dev/null +++ b/talk/base/messagequeue.h @@ -0,0 +1,264 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_MESSAGEQUEUE_H_ +#define TALK_BASE_MESSAGEQUEUE_H_ + +#include +#include +#include +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/constructormagic.h" +#include "talk/base/criticalsection.h" +#include "talk/base/messagehandler.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/scoped_ref_ptr.h" +#include "talk/base/sigslot.h" +#include "talk/base/socketserver.h" +#include "talk/base/timeutils.h" + +namespace talk_base { + +struct Message; +class MessageQueue; + +// MessageQueueManager does cleanup of of message queues + +class MessageQueueManager { + public: + static MessageQueueManager* Instance(); + + void Add(MessageQueue *message_queue); + void Remove(MessageQueue *message_queue); + void Clear(MessageHandler *handler); + + private: + MessageQueueManager(); + ~MessageQueueManager(); + + static MessageQueueManager* instance_; + // This list contains 'active' MessageQueues. + std::vector message_queues_; + CriticalSection crit_; +}; + +// Derive from this for specialized data +// App manages lifetime, except when messages are purged + +class MessageData { + public: + MessageData() {} + virtual ~MessageData() {} +}; + +template +class TypedMessageData : public MessageData { + public: + explicit TypedMessageData(const T& data) : data_(data) { } + const T& data() const { return data_; } + T& data() { return data_; } + private: + T data_; +}; + +// Like TypedMessageData, but for pointers that require a delete. +template +class ScopedMessageData : public MessageData { + public: + explicit ScopedMessageData(T* data) : data_(data) { } + const scoped_ptr& data() const { return data_; } + scoped_ptr& data() { return data_; } + private: + scoped_ptr data_; +}; + +// Like ScopedMessageData, but for reference counted pointers. +template +class ScopedRefMessageData : public MessageData { + public: + explicit ScopedRefMessageData(T* data) : data_(data) { } + const scoped_refptr& data() const { return data_; } + scoped_refptr& data() { return data_; } + private: + scoped_refptr data_; +}; + +template +inline MessageData* WrapMessageData(const T& data) { + return new TypedMessageData(data); +} + +template +inline const T& UseMessageData(MessageData* data) { + return static_cast< TypedMessageData* >(data)->data(); +} + +template +class DisposeData : public MessageData { + public: + explicit DisposeData(T* data) : data_(data) { } + virtual ~DisposeData() { delete data_; } + private: + T* data_; +}; + +const uint32 MQID_ANY = static_cast(-1); +const uint32 MQID_DISPOSE = static_cast(-2); + +// No destructor + +struct Message { + Message() { + memset(this, 0, sizeof(*this)); + } + inline bool Match(MessageHandler* handler, uint32 id) const { + return (handler == NULL || handler == phandler) + && (id == MQID_ANY || id == message_id); + } + MessageHandler *phandler; + uint32 message_id; + MessageData *pdata; + uint32 ts_sensitive; +}; + +typedef std::list MessageList; + +// DelayedMessage goes into a priority queue, sorted by trigger time. Messages +// with the same trigger time are processed in num_ (FIFO) order. + +class DelayedMessage { + public: + DelayedMessage(int delay, uint32 trigger, uint32 num, const Message& msg) + : cmsDelay_(delay), msTrigger_(trigger), num_(num), msg_(msg) { } + + bool operator< (const DelayedMessage& dmsg) const { + return (dmsg.msTrigger_ < msTrigger_) + || ((dmsg.msTrigger_ == msTrigger_) && (dmsg.num_ < num_)); + } + + int cmsDelay_; // for debugging + uint32 msTrigger_; + uint32 num_; + Message msg_; +}; + +class MessageQueue { + public: + explicit MessageQueue(SocketServer* ss = NULL); + virtual ~MessageQueue(); + + SocketServer* socketserver() { return ss_; } + void set_socketserver(SocketServer* ss); + + // Note: The behavior of MessageQueue has changed. When a MQ is stopped, + // futher Posts and Sends will fail. However, any pending Sends and *ready* + // Posts (as opposed to unexpired delayed Posts) will be delivered before + // Get (or Peek) returns false. By guaranteeing delivery of those messages, + // we eliminate the race condition when an MessageHandler and MessageQueue + // may be destroyed independently of each other. + virtual void Quit(); + virtual bool IsQuitting(); + virtual void Restart(); + + // Get() will process I/O until: + // 1) A message is available (returns true) + // 2) cmsWait seconds have elapsed (returns false) + // 3) Stop() is called (returns false) + virtual bool Get(Message *pmsg, int cmsWait = kForever, + bool process_io = true); + virtual bool Peek(Message *pmsg, int cmsWait = 0); + virtual void Post(MessageHandler *phandler, uint32 id = 0, + MessageData *pdata = NULL, bool time_sensitive = false); + virtual void PostDelayed(int cmsDelay, MessageHandler *phandler, + uint32 id = 0, MessageData *pdata = NULL) { + return DoDelayPost(cmsDelay, TimeAfter(cmsDelay), phandler, id, pdata); + } + virtual void PostAt(uint32 tstamp, MessageHandler *phandler, + uint32 id = 0, MessageData *pdata = NULL) { + return DoDelayPost(TimeUntil(tstamp), tstamp, phandler, id, pdata); + } + virtual void Clear(MessageHandler *phandler, uint32 id = MQID_ANY, + MessageList* removed = NULL); + virtual void Dispatch(Message *pmsg); + virtual void ReceiveSends(); + + // Amount of time until the next message can be retrieved + virtual int GetDelay(); + + bool empty() const { return size() == 0u; } + size_t size() const { + CritScope cs(&crit_); // msgq_.size() is not thread safe. + return msgq_.size() + dmsgq_.size() + (fPeekKeep_ ? 1u : 0u); + } + + // Internally posts a message which causes the doomed object to be deleted + template void Dispose(T* doomed) { + if (doomed) { + Post(NULL, MQID_DISPOSE, new DisposeData(doomed)); + } + } + + // When this signal is sent out, any references to this queue should + // no longer be used. + sigslot::signal0<> SignalQueueDestroyed; + + protected: + class PriorityQueue : public std::priority_queue { + public: + container_type& container() { return c; } + void reheap() { make_heap(c.begin(), c.end(), comp); } + }; + + void EnsureActive(); + void DoDelayPost(int cmsDelay, uint32 tstamp, MessageHandler *phandler, + uint32 id, MessageData* pdata); + + // The SocketServer is not owned by MessageQueue. + SocketServer* ss_; + // If a server isn't supplied in the constructor, use this one. + scoped_ptr default_ss_; + bool fStop_; + bool fPeekKeep_; + Message msgPeek_; + // A message queue is active if it has ever had a message posted to it. + // This also corresponds to being in MessageQueueManager's global list. + bool active_; + MessageList msgq_; + PriorityQueue dmsgq_; + uint32 dmsgq_next_num_; + mutable CriticalSection crit_; + + private: + DISALLOW_COPY_AND_ASSIGN(MessageQueue); +}; + +} // namespace talk_base + +#endif // TALK_BASE_MESSAGEQUEUE_H_ diff --git a/talk/base/messagequeue_unittest.cc b/talk/base/messagequeue_unittest.cc new file mode 100644 index 000000000..8e5554822 --- /dev/null +++ b/talk/base/messagequeue_unittest.cc @@ -0,0 +1,132 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/messagequeue.h" + +#include "talk/base/bind.h" +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" +#include "talk/base/nullsocketserver.h" + +using namespace talk_base; + +class MessageQueueTest: public testing::Test, public MessageQueue { + public: + bool IsLocked_Worker() { + if (!crit_.TryEnter()) { + return true; + } + crit_.Leave(); + return false; + } + bool IsLocked() { + // We have to do this on a worker thread, or else the TryEnter will + // succeed, since our critical sections are reentrant. + Thread worker; + worker.Start(); + return worker.Invoke( + talk_base::Bind(&MessageQueueTest::IsLocked_Worker, this)); + } +}; + +struct DeletedLockChecker { + DeletedLockChecker(MessageQueueTest* test, bool* was_locked, bool* deleted) + : test(test), was_locked(was_locked), deleted(deleted) { } + ~DeletedLockChecker() { + *deleted = true; + *was_locked = test->IsLocked(); + } + MessageQueueTest* test; + bool* was_locked; + bool* deleted; +}; + +static void DelayedPostsWithIdenticalTimesAreProcessedInFifoOrder( + MessageQueue* q) { + EXPECT_TRUE(q != NULL); + TimeStamp now = Time(); + q->PostAt(now, NULL, 3); + q->PostAt(now - 2, NULL, 0); + q->PostAt(now - 1, NULL, 1); + q->PostAt(now, NULL, 4); + q->PostAt(now - 1, NULL, 2); + + Message msg; + for (size_t i=0; i<5; ++i) { + memset(&msg, 0, sizeof(msg)); + EXPECT_TRUE(q->Get(&msg, 0)); + EXPECT_EQ(i, msg.message_id); + } + + EXPECT_FALSE(q->Get(&msg, 0)); // No more messages +} + +TEST_F(MessageQueueTest, + DelayedPostsWithIdenticalTimesAreProcessedInFifoOrder) { + MessageQueue q; + DelayedPostsWithIdenticalTimesAreProcessedInFifoOrder(&q); + NullSocketServer nullss; + MessageQueue q_nullss(&nullss); + DelayedPostsWithIdenticalTimesAreProcessedInFifoOrder(&q_nullss); +} + +TEST_F(MessageQueueTest, DisposeNotLocked) { + bool was_locked = true; + bool deleted = false; + DeletedLockChecker* d = new DeletedLockChecker(this, &was_locked, &deleted); + Dispose(d); + Message msg; + EXPECT_FALSE(Get(&msg, 0)); + EXPECT_TRUE(deleted); + EXPECT_FALSE(was_locked); +} + +class DeletedMessageHandler : public MessageHandler { + public: + explicit DeletedMessageHandler(bool* deleted) : deleted_(deleted) { } + ~DeletedMessageHandler() { + *deleted_ = true; + } + void OnMessage(Message* msg) { } + private: + bool* deleted_; +}; + +TEST_F(MessageQueueTest, DiposeHandlerWithPostedMessagePending) { + bool deleted = false; + DeletedMessageHandler *handler = new DeletedMessageHandler(&deleted); + // First, post a dispose. + Dispose(handler); + // Now, post a message, which should *not* be returned by Get(). + Post(handler, 1); + Message msg; + EXPECT_FALSE(Get(&msg, 0)); + EXPECT_TRUE(deleted); +} + diff --git a/talk/base/multipart.cc b/talk/base/multipart.cc new file mode 100644 index 000000000..d280ff33d --- /dev/null +++ b/talk/base/multipart.cc @@ -0,0 +1,268 @@ +// libjingle +// Copyright 2004--2010, 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/base/common.h" +#include "talk/base/httpcommon.h" +#include "talk/base/multipart.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// MultipartStream +/////////////////////////////////////////////////////////////////////////////// + +MultipartStream::MultipartStream(const std::string& type, + const std::string& boundary) + : type_(type), + boundary_(boundary), + adding_(true), + current_(0), + position_(0) { + // The content type should be multipart/*. + ASSERT(0 == strncmp(type_.c_str(), "multipart/", 10)); +} + +MultipartStream::~MultipartStream() { + Close(); +} + +void MultipartStream::GetContentType(std::string* content_type) { + ASSERT(NULL != content_type); + content_type->assign(type_); + content_type->append("; boundary="); + content_type->append(boundary_); +} + +bool MultipartStream::AddPart(StreamInterface* data_stream, + const std::string& content_disposition, + const std::string& content_type) { + if (!AddPart("", content_disposition, content_type)) + return false; + parts_.push_back(data_stream); + data_stream->SignalEvent.connect(this, &MultipartStream::OnEvent); + return true; +} + +bool MultipartStream::AddPart(const std::string& data, + const std::string& content_disposition, + const std::string& content_type) { + ASSERT(adding_); + if (!adding_) + return false; + std::stringstream ss; + if (!parts_.empty()) { + ss << "\r\n"; + } + ss << "--" << boundary_ << "\r\n"; + if (!content_disposition.empty()) { + ss << ToString(HH_CONTENT_DISPOSITION) << ": " + << content_disposition << "\r\n"; + } + if (!content_type.empty()) { + ss << ToString(HH_CONTENT_TYPE) << ": " + << content_type << "\r\n"; + } + ss << "\r\n" << data; + parts_.push_back(new MemoryStream(ss.str().data(), ss.str().size())); + return true; +} + +void MultipartStream::EndParts() { + ASSERT(adding_); + if (!adding_) + return; + + std::stringstream ss; + if (!parts_.empty()) { + ss << "\r\n"; + } + ss << "--" << boundary_ << "--" << "\r\n"; + parts_.push_back(new MemoryStream(ss.str().data(), ss.str().size())); + + ASSERT(0 == current_); + ASSERT(0 == position_); + adding_ = false; + SignalEvent(this, SE_OPEN | SE_READ, 0); +} + +size_t MultipartStream::GetPartSize(const std::string& data, + const std::string& content_disposition, + const std::string& content_type) const { + size_t size = 0; + if (!parts_.empty()) { + size += 2; // for "\r\n"; + } + size += boundary_.size() + 4; // for "--boundary_\r\n"; + if (!content_disposition.empty()) { + // for ToString(HH_CONTENT_DISPOSITION): content_disposition\r\n + size += std::string(ToString(HH_CONTENT_DISPOSITION)).size() + 2 + + content_disposition.size() + 2; + } + if (!content_type.empty()) { + // for ToString(HH_CONTENT_TYPE): content_type\r\n + size += std::string(ToString(HH_CONTENT_TYPE)).size() + 2 + + content_type.size() + 2; + } + size += 2 + data.size(); // for \r\ndata + return size; +} + +size_t MultipartStream::GetEndPartSize() const { + size_t size = 0; + if (!parts_.empty()) { + size += 2; // for "\r\n"; + } + size += boundary_.size() + 6; // for "--boundary_--\r\n"; + return size; +} + +// +// StreamInterface +// + +StreamState MultipartStream::GetState() const { + if (adding_) { + return SS_OPENING; + } + return (current_ < parts_.size()) ? SS_OPEN : SS_CLOSED; +} + +StreamResult MultipartStream::Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + if (adding_) { + return SR_BLOCK; + } + size_t local_read; + if (!read) read = &local_read; + while (current_ < parts_.size()) { + StreamResult result = parts_[current_]->Read(buffer, buffer_len, read, + error); + if (SR_EOS != result) { + if (SR_SUCCESS == result) { + position_ += *read; + } + return result; + } + ++current_; + } + return SR_EOS; +} + +StreamResult MultipartStream::Write(const void* data, size_t data_len, + size_t* written, int* error) { + if (error) { + *error = -1; + } + return SR_ERROR; +} + +void MultipartStream::Close() { + for (size_t i = 0; i < parts_.size(); ++i) { + delete parts_[i]; + } + parts_.clear(); + adding_ = false; + current_ = 0; + position_ = 0; +} + +bool MultipartStream::SetPosition(size_t position) { + if (adding_) { + return false; + } + size_t part_size, part_offset = 0; + for (size_t i = 0; i < parts_.size(); ++i) { + if (!parts_[i]->GetSize(&part_size)) { + return false; + } + if (part_offset + part_size > position) { + for (size_t j = i+1; j < _min(parts_.size(), current_+1); ++j) { + if (!parts_[j]->Rewind()) { + return false; + } + } + if (!parts_[i]->SetPosition(position - part_offset)) { + return false; + } + current_ = i; + position_ = position; + return true; + } + part_offset += part_size; + } + return false; +} + +bool MultipartStream::GetPosition(size_t* position) const { + if (position) { + *position = position_; + } + return true; +} + +bool MultipartStream::GetSize(size_t* size) const { + size_t part_size, total_size = 0; + for (size_t i = 0; i < parts_.size(); ++i) { + if (!parts_[i]->GetSize(&part_size)) { + return false; + } + total_size += part_size; + } + if (size) { + *size = total_size; + } + return true; +} + +bool MultipartStream::GetAvailable(size_t* size) const { + if (adding_) { + return false; + } + size_t part_size, total_size = 0; + for (size_t i = current_; i < parts_.size(); ++i) { + if (!parts_[i]->GetAvailable(&part_size)) { + return false; + } + total_size += part_size; + } + if (size) { + *size = total_size; + } + return true; +} + +// +// StreamInterface Slots +// + +void MultipartStream::OnEvent(StreamInterface* stream, int events, int error) { + if (adding_ || (current_ >= parts_.size()) || (parts_[current_] != stream)) { + return; + } + SignalEvent(this, events, error); +} + +} // namespace talk_base diff --git a/talk/base/multipart.h b/talk/base/multipart.h new file mode 100644 index 000000000..cce592b07 --- /dev/null +++ b/talk/base/multipart.h @@ -0,0 +1,94 @@ +// libjingle +// Copyright 2004--2010, 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. + +#ifndef TALK_BASE_MULTIPART_H__ +#define TALK_BASE_MULTIPART_H__ + +#include +#include + +#include "talk/base/sigslot.h" +#include "talk/base/stream.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// MultipartStream - Implements an RFC2046 multipart stream by concatenating +// the supplied parts together, and adding the correct boundaries. +/////////////////////////////////////////////////////////////////////////////// + +class MultipartStream : public StreamInterface, public sigslot::has_slots<> { + public: + MultipartStream(const std::string& type, const std::string& boundary); + virtual ~MultipartStream(); + + void GetContentType(std::string* content_type); + + // Note: If content_disposition and/or content_type are the empty string, + // they will be omitted. + bool AddPart(StreamInterface* data_stream, + const std::string& content_disposition, + const std::string& content_type); + bool AddPart(const std::string& data, + const std::string& content_disposition, + const std::string& content_type); + void EndParts(); + + // Calculates the size of a part before actually adding the part. + size_t GetPartSize(const std::string& data, + const std::string& content_disposition, + const std::string& content_type) const; + size_t GetEndPartSize() const; + + // StreamInterface + virtual StreamState GetState() const; + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error); + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + virtual void Close(); + virtual bool SetPosition(size_t position); + virtual bool GetPosition(size_t* position) const; + virtual bool GetSize(size_t* size) const; + virtual bool GetAvailable(size_t* size) const; + + private: + typedef std::vector PartList; + + // StreamInterface Slots + void OnEvent(StreamInterface* stream, int events, int error); + + std::string type_, boundary_; + PartList parts_; + bool adding_; + size_t current_; // The index into parts_ of the current read position. + size_t position_; // The current read position in bytes. + + DISALLOW_COPY_AND_ASSIGN(MultipartStream); +}; + +} // namespace talk_base + +#endif // TALK_BASE_MULTIPART_H__ diff --git a/talk/base/multipart_unittest.cc b/talk/base/multipart_unittest.cc new file mode 100644 index 000000000..18e3cf9a5 --- /dev/null +++ b/talk/base/multipart_unittest.cc @@ -0,0 +1,142 @@ +/* + * libjingle + * Copyright 2010, 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 + +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/pathutils.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/multipart.h" + +namespace talk_base { + +static const std::string kTestMultipartBoundary = "123456789987654321"; +static const std::string kTestContentType = + "multipart/form-data; boundary=123456789987654321"; +static const char kTestData[] = "This is a test."; +static const char kTestStreamContent[] = "This is a test stream."; + +TEST(MultipartTest, TestBasicOperations) { + MultipartStream multipart("multipart/form-data", kTestMultipartBoundary); + std::string content_type; + multipart.GetContentType(&content_type); + EXPECT_EQ(kTestContentType, content_type); + + EXPECT_EQ(talk_base::SS_OPENING, multipart.GetState()); + + // The multipart stream contains only --boundary--\r\n + size_t end_part_size = multipart.GetEndPartSize(); + multipart.EndParts(); + EXPECT_EQ(talk_base::SS_OPEN, multipart.GetState()); + size_t size; + EXPECT_TRUE(multipart.GetSize(&size)); + EXPECT_EQ(end_part_size, size); + + // Write is not supported. + EXPECT_EQ(talk_base::SR_ERROR, + multipart.Write(kTestData, sizeof(kTestData), NULL, NULL)); + + multipart.Close(); + EXPECT_EQ(talk_base::SS_CLOSED, multipart.GetState()); + EXPECT_TRUE(multipart.GetSize(&size)); + EXPECT_EQ(0U, size); +} + +TEST(MultipartTest, TestAddAndRead) { + MultipartStream multipart("multipart/form-data", kTestMultipartBoundary); + + size_t part_size = + multipart.GetPartSize(kTestData, "form-data; name=\"text\"", "text"); + EXPECT_TRUE(multipart.AddPart(kTestData, "form-data; name=\"text\"", "text")); + size_t size; + EXPECT_TRUE(multipart.GetSize(&size)); + EXPECT_EQ(part_size, size); + + talk_base::MemoryStream* stream = + new talk_base::MemoryStream(kTestStreamContent); + size_t stream_size = 0; + EXPECT_TRUE(stream->GetSize(&stream_size)); + part_size += + multipart.GetPartSize("", "form-data; name=\"stream\"", "stream"); + part_size += stream_size; + + EXPECT_TRUE(multipart.AddPart( + new talk_base::MemoryStream(kTestStreamContent), + "form-data; name=\"stream\"", + "stream")); + EXPECT_TRUE(multipart.GetSize(&size)); + EXPECT_EQ(part_size, size); + + // In adding state, block read. + char buffer[1024]; + EXPECT_EQ(talk_base::SR_BLOCK, + multipart.Read(buffer, sizeof(buffer), NULL, NULL)); + // Write is not supported. + EXPECT_EQ(talk_base::SR_ERROR, + multipart.Write(buffer, sizeof(buffer), NULL, NULL)); + + part_size += multipart.GetEndPartSize(); + multipart.EndParts(); + EXPECT_TRUE(multipart.GetSize(&size)); + EXPECT_EQ(part_size, size); + + // Read the multipart stream into StringStream + std::string str; + talk_base::StringStream str_stream(str); + EXPECT_EQ(talk_base::SR_SUCCESS, + Flow(&multipart, buffer, sizeof(buffer), &str_stream)); + EXPECT_EQ(size, str.length()); + + // Search three boundaries and two parts in the order. + size_t pos = 0; + pos = str.find(kTestMultipartBoundary); + EXPECT_NE(std::string::npos, pos); + pos += kTestMultipartBoundary.length(); + + pos = str.find(kTestData, pos); + EXPECT_NE(std::string::npos, pos); + pos += sizeof(kTestData); + + pos = str.find(kTestMultipartBoundary, pos); + EXPECT_NE(std::string::npos, pos); + pos += kTestMultipartBoundary.length(); + + pos = str.find(kTestStreamContent, pos); + EXPECT_NE(std::string::npos, pos); + pos += sizeof(kTestStreamContent); + + pos = str.find(kTestMultipartBoundary, pos); + EXPECT_NE(std::string::npos, pos); + pos += kTestMultipartBoundary.length(); + + pos = str.find(kTestMultipartBoundary, pos); + EXPECT_EQ(std::string::npos, pos); +} + +} // namespace talk_base diff --git a/talk/base/nat_unittest.cc b/talk/base/nat_unittest.cc new file mode 100644 index 000000000..03b1cd125 --- /dev/null +++ b/talk/base/nat_unittest.cc @@ -0,0 +1,359 @@ +/* + * libjingle + * Copyright 2004, 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 + +#include "talk/base/gunit.h" +#include "talk/base/host.h" +#include "talk/base/logging.h" +#include "talk/base/natserver.h" +#include "talk/base/natsocketfactory.h" +#include "talk/base/nethelpers.h" +#include "talk/base/network.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/testclient.h" +#include "talk/base/virtualsocketserver.h" + +using namespace talk_base; + +bool CheckReceive( + TestClient* client, bool should_receive, const char* buf, size_t size) { + return (should_receive) ? + client->CheckNextPacket(buf, size, 0) : + client->CheckNoPacket(); +} + +TestClient* CreateTestClient( + SocketFactory* factory, const SocketAddress& local_addr) { + AsyncUDPSocket* socket = AsyncUDPSocket::Create(factory, local_addr); + return new TestClient(socket); +} + +// Tests that when sending from internal_addr to external_addrs through the +// NAT type specified by nat_type, all external addrs receive the sent packet +// and, if exp_same is true, all use the same mapped-address on the NAT. +void TestSend( + SocketServer* internal, const SocketAddress& internal_addr, + SocketServer* external, const SocketAddress external_addrs[4], + NATType nat_type, bool exp_same) { + Thread th_int(internal); + Thread th_ext(external); + + SocketAddress server_addr = internal_addr; + server_addr.SetPort(0); // Auto-select a port + NATServer* nat = new NATServer( + nat_type, internal, server_addr, external, external_addrs[0]); + NATSocketFactory* natsf = new NATSocketFactory(internal, + nat->internal_address()); + + TestClient* in = CreateTestClient(natsf, internal_addr); + TestClient* out[4]; + for (int i = 0; i < 4; i++) + out[i] = CreateTestClient(external, external_addrs[i]); + + th_int.Start(); + th_ext.Start(); + + const char* buf = "filter_test"; + size_t len = strlen(buf); + + in->SendTo(buf, len, out[0]->address()); + SocketAddress trans_addr; + EXPECT_TRUE(out[0]->CheckNextPacket(buf, len, &trans_addr)); + + for (int i = 1; i < 4; i++) { + in->SendTo(buf, len, out[i]->address()); + SocketAddress trans_addr2; + EXPECT_TRUE(out[i]->CheckNextPacket(buf, len, &trans_addr2)); + bool are_same = (trans_addr == trans_addr2); + ASSERT_EQ(are_same, exp_same) << "same translated address"; + ASSERT_NE(AF_UNSPEC, trans_addr.family()); + ASSERT_NE(AF_UNSPEC, trans_addr2.family()); + } + + th_int.Stop(); + th_ext.Stop(); + + delete nat; + delete natsf; + delete in; + for (int i = 0; i < 4; i++) + delete out[i]; +} + +// Tests that when sending from external_addrs to internal_addr, the packet +// is delivered according to the specified filter_ip and filter_port rules. +void TestRecv( + SocketServer* internal, const SocketAddress& internal_addr, + SocketServer* external, const SocketAddress external_addrs[4], + NATType nat_type, bool filter_ip, bool filter_port) { + Thread th_int(internal); + Thread th_ext(external); + + SocketAddress server_addr = internal_addr; + server_addr.SetPort(0); // Auto-select a port + NATServer* nat = new NATServer( + nat_type, internal, server_addr, external, external_addrs[0]); + NATSocketFactory* natsf = new NATSocketFactory(internal, + nat->internal_address()); + + TestClient* in = CreateTestClient(natsf, internal_addr); + TestClient* out[4]; + for (int i = 0; i < 4; i++) + out[i] = CreateTestClient(external, external_addrs[i]); + + th_int.Start(); + th_ext.Start(); + + const char* buf = "filter_test"; + size_t len = strlen(buf); + + in->SendTo(buf, len, out[0]->address()); + SocketAddress trans_addr; + EXPECT_TRUE(out[0]->CheckNextPacket(buf, len, &trans_addr)); + + out[1]->SendTo(buf, len, trans_addr); + EXPECT_TRUE(CheckReceive(in, !filter_ip, buf, len)); + + out[2]->SendTo(buf, len, trans_addr); + EXPECT_TRUE(CheckReceive(in, !filter_port, buf, len)); + + out[3]->SendTo(buf, len, trans_addr); + EXPECT_TRUE(CheckReceive(in, !filter_ip && !filter_port, buf, len)); + + th_int.Stop(); + th_ext.Stop(); + + delete nat; + delete natsf; + delete in; + for (int i = 0; i < 4; i++) + delete out[i]; +} + +// Tests that NATServer allocates bindings properly. +void TestBindings( + SocketServer* internal, const SocketAddress& internal_addr, + SocketServer* external, const SocketAddress external_addrs[4]) { + TestSend(internal, internal_addr, external, external_addrs, + NAT_OPEN_CONE, true); + TestSend(internal, internal_addr, external, external_addrs, + NAT_ADDR_RESTRICTED, true); + TestSend(internal, internal_addr, external, external_addrs, + NAT_PORT_RESTRICTED, true); + TestSend(internal, internal_addr, external, external_addrs, + NAT_SYMMETRIC, false); +} + +// Tests that NATServer filters packets properly. +void TestFilters( + SocketServer* internal, const SocketAddress& internal_addr, + SocketServer* external, const SocketAddress external_addrs[4]) { + TestRecv(internal, internal_addr, external, external_addrs, + NAT_OPEN_CONE, false, false); + TestRecv(internal, internal_addr, external, external_addrs, + NAT_ADDR_RESTRICTED, true, false); + TestRecv(internal, internal_addr, external, external_addrs, + NAT_PORT_RESTRICTED, true, true); + TestRecv(internal, internal_addr, external, external_addrs, + NAT_SYMMETRIC, true, true); +} + +bool TestConnectivity(const SocketAddress& src, const IPAddress& dst) { + // The physical NAT tests require connectivity to the selected ip from the + // internal address used for the NAT. Things like firewalls can break that, so + // check to see if it's worth even trying with this ip. + scoped_ptr pss(new PhysicalSocketServer()); + scoped_ptr client(pss->CreateAsyncSocket(src.family(), + SOCK_DGRAM)); + scoped_ptr server(pss->CreateAsyncSocket(src.family(), + SOCK_DGRAM)); + if (client->Bind(SocketAddress(src.ipaddr(), 0)) != 0 || + server->Bind(SocketAddress(dst, 0)) != 0) { + return false; + } + const char* buf = "hello other socket"; + size_t len = strlen(buf); + int sent = client->SendTo(buf, len, server->GetLocalAddress()); + SocketAddress addr; + const size_t kRecvBufSize = 64; + char recvbuf[kRecvBufSize]; + Thread::Current()->SleepMs(100); + int received = server->RecvFrom(recvbuf, kRecvBufSize, &addr); + return received == sent && ::memcmp(buf, recvbuf, len) == 0; +} + +void TestPhysicalInternal(const SocketAddress& int_addr) { + BasicNetworkManager network_manager; + network_manager.set_ipv6_enabled(true); + network_manager.StartUpdating(); + // Process pending messages so the network list is updated. + Thread::Current()->ProcessMessages(0); + + std::vector networks; + network_manager.GetNetworks(&networks); + if (networks.empty()) { + LOG(LS_WARNING) << "Not enough network adapters for test."; + return; + } + + SocketAddress ext_addr1(int_addr); + SocketAddress ext_addr2; + // Find an available IP with matching family. The test breaks if int_addr + // can't talk to ip, so check for connectivity as well. + for (std::vector::iterator it = networks.begin(); + it != networks.end(); ++it) { + const IPAddress& ip = (*it)->ip(); + if (ip.family() == int_addr.family() && TestConnectivity(int_addr, ip)) { + ext_addr2.SetIP(ip); + break; + } + } + if (ext_addr2.IsNil()) { + LOG(LS_WARNING) << "No available IP of same family as " << int_addr; + return; + } + + LOG(LS_INFO) << "selected ip " << ext_addr2.ipaddr(); + + SocketAddress ext_addrs[4] = { + SocketAddress(ext_addr1), + SocketAddress(ext_addr2), + SocketAddress(ext_addr1), + SocketAddress(ext_addr2) + }; + + PhysicalSocketServer* int_pss = new PhysicalSocketServer(); + PhysicalSocketServer* ext_pss = new PhysicalSocketServer(); + + TestBindings(int_pss, int_addr, ext_pss, ext_addrs); + TestFilters(int_pss, int_addr, ext_pss, ext_addrs); +} + +TEST(NatTest, TestPhysicalIPv4) { + TestPhysicalInternal(SocketAddress("127.0.0.1", 0)); +} + +TEST(NatTest, TestPhysicalIPv6) { + if (HasIPv6Enabled()) { + TestPhysicalInternal(SocketAddress("::1", 0)); + } else { + LOG(LS_WARNING) << "No IPv6, skipping"; + } +} + +class TestVirtualSocketServer : public VirtualSocketServer { + public: + explicit TestVirtualSocketServer(SocketServer* ss) + : VirtualSocketServer(ss) {} + // Expose this publicly + IPAddress GetNextIP(int af) { return VirtualSocketServer::GetNextIP(af); } +}; + +void TestVirtualInternal(int family) { + TestVirtualSocketServer* int_vss = new TestVirtualSocketServer( + new PhysicalSocketServer()); + TestVirtualSocketServer* ext_vss = new TestVirtualSocketServer( + new PhysicalSocketServer()); + + SocketAddress int_addr; + SocketAddress ext_addrs[4]; + int_addr.SetIP(int_vss->GetNextIP(family)); + ext_addrs[0].SetIP(ext_vss->GetNextIP(int_addr.family())); + ext_addrs[1].SetIP(ext_vss->GetNextIP(int_addr.family())); + ext_addrs[2].SetIP(ext_addrs[0].ipaddr()); + ext_addrs[3].SetIP(ext_addrs[1].ipaddr()); + + TestBindings(int_vss, int_addr, ext_vss, ext_addrs); + TestFilters(int_vss, int_addr, ext_vss, ext_addrs); +} + +TEST(NatTest, TestVirtualIPv4) { + TestVirtualInternal(AF_INET); +} + +TEST(NatTest, TestVirtualIPv6) { + if (HasIPv6Enabled()) { + TestVirtualInternal(AF_INET6); + } else { + LOG(LS_WARNING) << "No IPv6, skipping"; + } +} + +// TODO: Finish this test +class NatTcpTest : public testing::Test, public sigslot::has_slots<> { + public: + NatTcpTest() : connected_(false) {} + virtual void SetUp() { + int_vss_ = new TestVirtualSocketServer(new PhysicalSocketServer()); + ext_vss_ = new TestVirtualSocketServer(new PhysicalSocketServer()); + nat_ = new NATServer(NAT_OPEN_CONE, int_vss_, SocketAddress(), + ext_vss_, SocketAddress()); + natsf_ = new NATSocketFactory(int_vss_, nat_->internal_address()); + } + void OnConnectEvent(AsyncSocket* socket) { + connected_ = true; + } + void OnAcceptEvent(AsyncSocket* socket) { + accepted_ = server_->Accept(NULL); + } + void OnCloseEvent(AsyncSocket* socket, int error) { + } + void ConnectEvents() { + server_->SignalReadEvent.connect(this, &NatTcpTest::OnAcceptEvent); + client_->SignalConnectEvent.connect(this, &NatTcpTest::OnConnectEvent); + } + TestVirtualSocketServer* int_vss_; + TestVirtualSocketServer* ext_vss_; + NATServer* nat_; + NATSocketFactory* natsf_; + AsyncSocket* client_; + AsyncSocket* server_; + AsyncSocket* accepted_; + bool connected_; +}; + +TEST_F(NatTcpTest, DISABLED_TestConnectOut) { + server_ = ext_vss_->CreateAsyncSocket(SOCK_STREAM); + server_->Bind(SocketAddress()); + server_->Listen(5); + + client_ = int_vss_->CreateAsyncSocket(SOCK_STREAM); + EXPECT_GE(0, client_->Bind(SocketAddress())); + EXPECT_GE(0, client_->Connect(server_->GetLocalAddress())); + + + ConnectEvents(); + + EXPECT_TRUE_WAIT(connected_, 1000); + EXPECT_EQ(client_->GetRemoteAddress(), server_->GetLocalAddress()); + EXPECT_EQ(client_->GetRemoteAddress(), accepted_->GetLocalAddress()); + EXPECT_EQ(client_->GetLocalAddress(), accepted_->GetRemoteAddress()); + + client_->Close(); +} +//#endif diff --git a/talk/base/natserver.cc b/talk/base/natserver.cc new file mode 100644 index 000000000..7a3a04509 --- /dev/null +++ b/talk/base/natserver.cc @@ -0,0 +1,190 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/natsocketfactory.h" +#include "talk/base/natserver.h" +#include "talk/base/logging.h" + +namespace talk_base { + +RouteCmp::RouteCmp(NAT* nat) : symmetric(nat->IsSymmetric()) { +} + +size_t RouteCmp::operator()(const SocketAddressPair& r) const { + size_t h = r.source().Hash(); + if (symmetric) + h ^= r.destination().Hash(); + return h; +} + +bool RouteCmp::operator()( + const SocketAddressPair& r1, const SocketAddressPair& r2) const { + if (r1.source() < r2.source()) + return true; + if (r2.source() < r1.source()) + return false; + if (symmetric && (r1.destination() < r2.destination())) + return true; + if (symmetric && (r2.destination() < r1.destination())) + return false; + return false; +} + +AddrCmp::AddrCmp(NAT* nat) + : use_ip(nat->FiltersIP()), use_port(nat->FiltersPort()) { +} + +size_t AddrCmp::operator()(const SocketAddress& a) const { + size_t h = 0; + if (use_ip) + h ^= HashIP(a.ipaddr()); + if (use_port) + h ^= a.port() | (a.port() << 16); + return h; +} + +bool AddrCmp::operator()( + const SocketAddress& a1, const SocketAddress& a2) const { + if (use_ip && (a1.ipaddr() < a2.ipaddr())) + return true; + if (use_ip && (a2.ipaddr() < a1.ipaddr())) + return false; + if (use_port && (a1.port() < a2.port())) + return true; + if (use_port && (a2.port() < a1.port())) + return false; + return false; +} + +NATServer::NATServer( + NATType type, SocketFactory* internal, const SocketAddress& internal_addr, + SocketFactory* external, const SocketAddress& external_ip) + : external_(external), external_ip_(external_ip.ipaddr(), 0) { + nat_ = NAT::Create(type); + + server_socket_ = AsyncUDPSocket::Create(internal, internal_addr); + server_socket_->SignalReadPacket.connect(this, &NATServer::OnInternalPacket); + + int_map_ = new InternalMap(RouteCmp(nat_)); + ext_map_ = new ExternalMap(); +} + +NATServer::~NATServer() { + for (InternalMap::iterator iter = int_map_->begin(); + iter != int_map_->end(); + iter++) + delete iter->second; + + delete nat_; + delete server_socket_; + delete int_map_; + delete ext_map_; +} + +void NATServer::OnInternalPacket( + AsyncPacketSocket* socket, const char* buf, size_t size, + const SocketAddress& addr) { + + // Read the intended destination from the wire. + SocketAddress dest_addr; + size_t length = UnpackAddressFromNAT(buf, size, &dest_addr); + + // Find the translation for these addresses (allocating one if necessary). + SocketAddressPair route(addr, dest_addr); + InternalMap::iterator iter = int_map_->find(route); + if (iter == int_map_->end()) { + Translate(route); + iter = int_map_->find(route); + } + ASSERT(iter != int_map_->end()); + + // Allow the destination to send packets back to the source. + iter->second->whitelist->insert(dest_addr); + + // Send the packet to its intended destination. + iter->second->socket->SendTo(buf + length, size - length, dest_addr); +} + +void NATServer::OnExternalPacket( + AsyncPacketSocket* socket, const char* buf, size_t size, + const SocketAddress& remote_addr) { + + SocketAddress local_addr = socket->GetLocalAddress(); + + // Find the translation for this addresses. + ExternalMap::iterator iter = ext_map_->find(local_addr); + ASSERT(iter != ext_map_->end()); + + // Allow the NAT to reject this packet. + if (Filter(iter->second, remote_addr)) { + LOG(LS_INFO) << "Packet from " << remote_addr.ToSensitiveString() + << " was filtered out by the NAT."; + return; + } + + // Forward this packet to the internal address. + // First prepend the address in a quasi-STUN format. + scoped_array real_buf(new char[size + kNATEncodedIPv6AddressSize]); + size_t addrlength = PackAddressForNAT(real_buf.get(), + size + kNATEncodedIPv6AddressSize, + remote_addr); + // Copy the data part after the address. + std::memcpy(real_buf.get() + addrlength, buf, size); + server_socket_->SendTo(real_buf.get(), size + addrlength, + iter->second->route.source()); +} + +void NATServer::Translate(const SocketAddressPair& route) { + AsyncUDPSocket* socket = AsyncUDPSocket::Create(external_, external_ip_); + + if (!socket) { + LOG(LS_ERROR) << "Couldn't find a free port!"; + return; + } + + TransEntry* entry = new TransEntry(route, socket, nat_); + (*int_map_)[route] = entry; + (*ext_map_)[socket->GetLocalAddress()] = entry; + socket->SignalReadPacket.connect(this, &NATServer::OnExternalPacket); +} + +bool NATServer::Filter(TransEntry* entry, const SocketAddress& ext_addr) { + return entry->whitelist->find(ext_addr) == entry->whitelist->end(); +} + +NATServer::TransEntry::TransEntry( + const SocketAddressPair& r, AsyncUDPSocket* s, NAT* nat) + : route(r), socket(s) { + whitelist = new AddressSet(AddrCmp(nat)); +} + +NATServer::TransEntry::~TransEntry() { + delete whitelist; + delete socket; +} + +} // namespace talk_base diff --git a/talk/base/natserver.h b/talk/base/natserver.h new file mode 100644 index 000000000..0a6083cbb --- /dev/null +++ b/talk/base/natserver.h @@ -0,0 +1,121 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_NATSERVER_H_ +#define TALK_BASE_NATSERVER_H_ + +#include +#include + +#include "talk/base/asyncudpsocket.h" +#include "talk/base/socketaddresspair.h" +#include "talk/base/thread.h" +#include "talk/base/socketfactory.h" +#include "talk/base/nattypes.h" + +namespace talk_base { + +// Change how routes (socketaddress pairs) are compared based on the type of +// NAT. The NAT server maintains a hashtable of the routes that it knows +// about. So these affect which routes are treated the same. +struct RouteCmp { + explicit RouteCmp(NAT* nat); + size_t operator()(const SocketAddressPair& r) const; + bool operator()( + const SocketAddressPair& r1, const SocketAddressPair& r2) const; + + bool symmetric; +}; + +// Changes how addresses are compared based on the filtering rules of the NAT. +struct AddrCmp { + explicit AddrCmp(NAT* nat); + size_t operator()(const SocketAddress& r) const; + bool operator()(const SocketAddress& r1, const SocketAddress& r2) const; + + bool use_ip; + bool use_port; +}; + +// Implements the NAT device. It listens for packets on the internal network, +// translates them, and sends them out over the external network. + +const int NAT_SERVER_PORT = 4237; + +class NATServer : public sigslot::has_slots<> { + public: + NATServer( + NATType type, SocketFactory* internal, const SocketAddress& internal_addr, + SocketFactory* external, const SocketAddress& external_ip); + ~NATServer(); + + SocketAddress internal_address() const { + return server_socket_->GetLocalAddress(); + } + + // Packets received on one of the networks. + void OnInternalPacket(AsyncPacketSocket* socket, const char* buf, + size_t size, const SocketAddress& addr); + void OnExternalPacket(AsyncPacketSocket* socket, const char* buf, + size_t size, const SocketAddress& remote_addr); + + private: + typedef std::set AddressSet; + + /* Records a translation and the associated external socket. */ + struct TransEntry { + TransEntry(const SocketAddressPair& r, AsyncUDPSocket* s, NAT* nat); + ~TransEntry(); + + SocketAddressPair route; + AsyncUDPSocket* socket; + AddressSet* whitelist; + }; + + typedef std::map InternalMap; + typedef std::map ExternalMap; + + /* Creates a new entry that translates the given route. */ + void Translate(const SocketAddressPair& route); + + /* Determines whether the NAT would filter out a packet from this address. */ + bool Filter(TransEntry* entry, const SocketAddress& ext_addr); + + NAT* nat_; + SocketFactory* internal_; + SocketFactory* external_; + SocketAddress external_ip_; + AsyncUDPSocket* server_socket_; + AsyncSocket* tcp_server_socket_; + InternalMap* int_map_; + ExternalMap* ext_map_; + DISALLOW_EVIL_CONSTRUCTORS(NATServer); +}; + +} // namespace talk_base + +#endif // TALK_BASE_NATSERVER_H_ diff --git a/talk/base/natserver_main.cc b/talk/base/natserver_main.cc new file mode 100644 index 000000000..a74810827 --- /dev/null +++ b/talk/base/natserver_main.cc @@ -0,0 +1,57 @@ +/* + * libjingle + * Copyright 2004--2005, 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 + +#include "talk/base/natserver.h" +#include "talk/base/host.h" +#include "talk/base/physicalsocketserver.h" + +using namespace talk_base; + +int main(int argc, char* argv[]) { + if (argc != 3) { + std::cerr << "usage: natserver " << std::endl; + exit(1); + } + + SocketAddress internal = SocketAddress(argv[1]); + SocketAddress external = SocketAddress(argv[2]); + if (internal.EqualIPs(external)) { + std::cerr << "internal and external IPs must differ" << std::endl; + exit(1); + } + + Thread* pthMain = Thread::Current(); + PhysicalSocketServer* ss = new PhysicalSocketServer(); + pthMain->set_socketserver(ss); + NATServer* server = new NATServer(NAT_OPEN_CONE, ss, internal, ss, external); + server = server; + + pthMain->Run(); + return 0; +} diff --git a/talk/base/natsocketfactory.cc b/talk/base/natsocketfactory.cc new file mode 100644 index 000000000..a7c4240b3 --- /dev/null +++ b/talk/base/natsocketfactory.cc @@ -0,0 +1,505 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/natsocketfactory.h" + +#include "talk/base/logging.h" +#include "talk/base/natserver.h" +#include "talk/base/virtualsocketserver.h" + +namespace talk_base { + +// Packs the given socketaddress into the buffer in buf, in the quasi-STUN +// format that the natserver uses. +// Returns 0 if an invalid address is passed. +size_t PackAddressForNAT(char* buf, size_t buf_size, + const SocketAddress& remote_addr) { + const IPAddress& ip = remote_addr.ipaddr(); + int family = ip.family(); + buf[0] = 0; + buf[1] = family; + // Writes the port. + *(reinterpret_cast(&buf[2])) = HostToNetwork16(remote_addr.port()); + if (family == AF_INET) { + ASSERT(buf_size >= kNATEncodedIPv4AddressSize); + in_addr v4addr = ip.ipv4_address(); + std::memcpy(&buf[4], &v4addr, kNATEncodedIPv4AddressSize - 4); + return kNATEncodedIPv4AddressSize; + } else if (family == AF_INET6) { + ASSERT(buf_size >= kNATEncodedIPv6AddressSize); + in6_addr v6addr = ip.ipv6_address(); + std::memcpy(&buf[4], &v6addr, kNATEncodedIPv6AddressSize - 4); + return kNATEncodedIPv6AddressSize; + } + return 0U; +} + +// Decodes the remote address from a packet that has been encoded with the nat's +// quasi-STUN format. Returns the length of the address (i.e., the offset into +// data where the original packet starts). +size_t UnpackAddressFromNAT(const char* buf, size_t buf_size, + SocketAddress* remote_addr) { + ASSERT(buf_size >= 8); + ASSERT(buf[0] == 0); + int family = buf[1]; + uint16 port = NetworkToHost16(*(reinterpret_cast(&buf[2]))); + if (family == AF_INET) { + const in_addr* v4addr = reinterpret_cast(&buf[4]); + *remote_addr = SocketAddress(IPAddress(*v4addr), port); + return kNATEncodedIPv4AddressSize; + } else if (family == AF_INET6) { + ASSERT(buf_size >= 20); + const in6_addr* v6addr = reinterpret_cast(&buf[4]); + *remote_addr = SocketAddress(IPAddress(*v6addr), port); + return kNATEncodedIPv6AddressSize; + } + return 0U; +} + + +// NATSocket +class NATSocket : public AsyncSocket, public sigslot::has_slots<> { + public: + explicit NATSocket(NATInternalSocketFactory* sf, int family, int type) + : sf_(sf), family_(family), type_(type), async_(true), connected_(false), + socket_(NULL), buf_(NULL), size_(0) { + } + + virtual ~NATSocket() { + delete socket_; + delete[] buf_; + } + + virtual SocketAddress GetLocalAddress() const { + return (socket_) ? socket_->GetLocalAddress() : SocketAddress(); + } + + virtual SocketAddress GetRemoteAddress() const { + return remote_addr_; // will be NIL if not connected + } + + virtual int Bind(const SocketAddress& addr) { + if (socket_) { // already bound, bubble up error + return -1; + } + + int result; + socket_ = sf_->CreateInternalSocket(family_, type_, addr, &server_addr_); + result = (socket_) ? socket_->Bind(addr) : -1; + if (result >= 0) { + socket_->SignalConnectEvent.connect(this, &NATSocket::OnConnectEvent); + socket_->SignalReadEvent.connect(this, &NATSocket::OnReadEvent); + socket_->SignalWriteEvent.connect(this, &NATSocket::OnWriteEvent); + socket_->SignalCloseEvent.connect(this, &NATSocket::OnCloseEvent); + } else { + server_addr_.Clear(); + delete socket_; + socket_ = NULL; + } + + return result; + } + + virtual int Connect(const SocketAddress& addr) { + if (!socket_) { // socket must be bound, for now + return -1; + } + + int result = 0; + if (type_ == SOCK_STREAM) { + result = socket_->Connect(server_addr_.IsNil() ? addr : server_addr_); + } else { + connected_ = true; + } + + if (result >= 0) { + remote_addr_ = addr; + } + + return result; + } + + virtual int Send(const void* data, size_t size) { + ASSERT(connected_); + return SendTo(data, size, remote_addr_); + } + + virtual int SendTo(const void* data, size_t size, const SocketAddress& addr) { + ASSERT(!connected_ || addr == remote_addr_); + if (server_addr_.IsNil() || type_ == SOCK_STREAM) { + return socket_->SendTo(data, size, addr); + } + // This array will be too large for IPv4 packets, but only by 12 bytes. + scoped_array buf(new char[size + kNATEncodedIPv6AddressSize]); + size_t addrlength = PackAddressForNAT(buf.get(), + size + kNATEncodedIPv6AddressSize, + addr); + size_t encoded_size = size + addrlength; + std::memcpy(buf.get() + addrlength, data, size); + int result = socket_->SendTo(buf.get(), encoded_size, server_addr_); + if (result >= 0) { + ASSERT(result == static_cast(encoded_size)); + result = result - static_cast(addrlength); + } + return result; + } + + virtual int Recv(void* data, size_t size) { + SocketAddress addr; + return RecvFrom(data, size, &addr); + } + + virtual int RecvFrom(void* data, size_t size, SocketAddress *out_addr) { + if (server_addr_.IsNil() || type_ == SOCK_STREAM) { + return socket_->RecvFrom(data, size, out_addr); + } + // Make sure we have enough room to read the requested amount plus the + // largest possible header address. + SocketAddress remote_addr; + Grow(size + kNATEncodedIPv6AddressSize); + + // Read the packet from the socket. + int result = socket_->RecvFrom(buf_, size_, &remote_addr); + if (result >= 0) { + ASSERT(remote_addr == server_addr_); + + // TODO: we need better framing so we know how many bytes we can + // return before we need to read the next address. For UDP, this will be + // fine as long as the reader always reads everything in the packet. + ASSERT((size_t)result < size_); + + // Decode the wire packet into the actual results. + SocketAddress real_remote_addr; + size_t addrlength = + UnpackAddressFromNAT(buf_, result, &real_remote_addr); + std::memcpy(data, buf_ + addrlength, result - addrlength); + + // Make sure this packet should be delivered before returning it. + if (!connected_ || (real_remote_addr == remote_addr_)) { + if (out_addr) + *out_addr = real_remote_addr; + result = result - static_cast(addrlength); + } else { + LOG(LS_ERROR) << "Dropping packet from unknown remote address: " + << real_remote_addr.ToString(); + result = 0; // Tell the caller we didn't read anything + } + } + + return result; + } + + virtual int Close() { + int result = 0; + if (socket_) { + result = socket_->Close(); + if (result >= 0) { + connected_ = false; + remote_addr_ = SocketAddress(); + delete socket_; + socket_ = NULL; + } + } + return result; + } + + virtual int Listen(int backlog) { + return socket_->Listen(backlog); + } + virtual AsyncSocket* Accept(SocketAddress *paddr) { + return socket_->Accept(paddr); + } + virtual int GetError() const { + return socket_->GetError(); + } + virtual void SetError(int error) { + socket_->SetError(error); + } + virtual ConnState GetState() const { + return connected_ ? CS_CONNECTED : CS_CLOSED; + } + virtual int EstimateMTU(uint16* mtu) { + return socket_->EstimateMTU(mtu); + } + virtual int GetOption(Option opt, int* value) { + return socket_->GetOption(opt, value); + } + virtual int SetOption(Option opt, int value) { + return socket_->SetOption(opt, value); + } + + void OnConnectEvent(AsyncSocket* socket) { + // If we're NATed, we need to send a request with the real addr to use. + ASSERT(socket == socket_); + if (server_addr_.IsNil()) { + connected_ = true; + SignalConnectEvent(this); + } else { + SendConnectRequest(); + } + } + void OnReadEvent(AsyncSocket* socket) { + // If we're NATed, we need to process the connect reply. + ASSERT(socket == socket_); + if (type_ == SOCK_STREAM && !server_addr_.IsNil() && !connected_) { + HandleConnectReply(); + } else { + SignalReadEvent(this); + } + } + void OnWriteEvent(AsyncSocket* socket) { + ASSERT(socket == socket_); + SignalWriteEvent(this); + } + void OnCloseEvent(AsyncSocket* socket, int error) { + ASSERT(socket == socket_); + SignalCloseEvent(this, error); + } + + private: + // Makes sure the buffer is at least the given size. + void Grow(size_t new_size) { + if (size_ < new_size) { + delete[] buf_; + size_ = new_size; + buf_ = new char[size_]; + } + } + + // Sends the destination address to the server to tell it to connect. + void SendConnectRequest() { + char buf[256]; + size_t length = PackAddressForNAT(buf, ARRAY_SIZE(buf), remote_addr_); + socket_->Send(buf, length); + } + + // Handles the byte sent back from the server and fires the appropriate event. + void HandleConnectReply() { + char code; + socket_->Recv(&code, sizeof(code)); + if (code == 0) { + SignalConnectEvent(this); + } else { + Close(); + SignalCloseEvent(this, code); + } + } + + NATInternalSocketFactory* sf_; + int family_; + int type_; + bool async_; + bool connected_; + SocketAddress remote_addr_; + SocketAddress server_addr_; // address of the NAT server + AsyncSocket* socket_; + char* buf_; + size_t size_; +}; + +// NATSocketFactory +NATSocketFactory::NATSocketFactory(SocketFactory* factory, + const SocketAddress& nat_addr) + : factory_(factory), nat_addr_(nat_addr) { +} + +Socket* NATSocketFactory::CreateSocket(int type) { + return CreateSocket(AF_INET, type); +} + +Socket* NATSocketFactory::CreateSocket(int family, int type) { + return new NATSocket(this, family, type); +} + +AsyncSocket* NATSocketFactory::CreateAsyncSocket(int type) { + return CreateAsyncSocket(AF_INET, type); +} + +AsyncSocket* NATSocketFactory::CreateAsyncSocket(int family, int type) { + return new NATSocket(this, family, type); +} + +AsyncSocket* NATSocketFactory::CreateInternalSocket(int family, int type, + const SocketAddress& local_addr, SocketAddress* nat_addr) { + *nat_addr = nat_addr_; + return factory_->CreateAsyncSocket(family, type); +} + +// NATSocketServer +NATSocketServer::NATSocketServer(SocketServer* server) + : server_(server), msg_queue_(NULL) { +} + +NATSocketServer::Translator* NATSocketServer::GetTranslator( + const SocketAddress& ext_ip) { + return nats_.Get(ext_ip); +} + +NATSocketServer::Translator* NATSocketServer::AddTranslator( + const SocketAddress& ext_ip, const SocketAddress& int_ip, NATType type) { + // Fail if a translator already exists with this extternal address. + if (nats_.Get(ext_ip)) + return NULL; + + return nats_.Add(ext_ip, new Translator(this, type, int_ip, server_, ext_ip)); +} + +void NATSocketServer::RemoveTranslator( + const SocketAddress& ext_ip) { + nats_.Remove(ext_ip); +} + +Socket* NATSocketServer::CreateSocket(int type) { + return CreateSocket(AF_INET, type); +} + +Socket* NATSocketServer::CreateSocket(int family, int type) { + return new NATSocket(this, family, type); +} + +AsyncSocket* NATSocketServer::CreateAsyncSocket(int type) { + return CreateAsyncSocket(AF_INET, type); +} + +AsyncSocket* NATSocketServer::CreateAsyncSocket(int family, int type) { + return new NATSocket(this, family, type); +} + +AsyncSocket* NATSocketServer::CreateInternalSocket(int family, int type, + const SocketAddress& local_addr, SocketAddress* nat_addr) { + AsyncSocket* socket = NULL; + Translator* nat = nats_.FindClient(local_addr); + if (nat) { + socket = nat->internal_factory()->CreateAsyncSocket(family, type); + *nat_addr = (type == SOCK_STREAM) ? + nat->internal_tcp_address() : nat->internal_address(); + } else { + socket = server_->CreateAsyncSocket(family, type); + } + return socket; +} + +// NATSocketServer::Translator +NATSocketServer::Translator::Translator( + NATSocketServer* server, NATType type, const SocketAddress& int_ip, + SocketFactory* ext_factory, const SocketAddress& ext_ip) + : server_(server) { + // Create a new private network, and a NATServer running on the private + // network that bridges to the external network. Also tell the private + // network to use the same message queue as us. + VirtualSocketServer* internal_server = new VirtualSocketServer(server_); + internal_server->SetMessageQueue(server_->queue()); + internal_factory_.reset(internal_server); + nat_server_.reset(new NATServer(type, internal_server, int_ip, + ext_factory, ext_ip)); +} + + +NATSocketServer::Translator* NATSocketServer::Translator::GetTranslator( + const SocketAddress& ext_ip) { + return nats_.Get(ext_ip); +} + +NATSocketServer::Translator* NATSocketServer::Translator::AddTranslator( + const SocketAddress& ext_ip, const SocketAddress& int_ip, NATType type) { + // Fail if a translator already exists with this extternal address. + if (nats_.Get(ext_ip)) + return NULL; + + AddClient(ext_ip); + return nats_.Add(ext_ip, + new Translator(server_, type, int_ip, server_, ext_ip)); +} +void NATSocketServer::Translator::RemoveTranslator( + const SocketAddress& ext_ip) { + nats_.Remove(ext_ip); + RemoveClient(ext_ip); +} + +bool NATSocketServer::Translator::AddClient( + const SocketAddress& int_ip) { + // Fail if a client already exists with this internal address. + if (clients_.find(int_ip) != clients_.end()) + return false; + + clients_.insert(int_ip); + return true; +} + +void NATSocketServer::Translator::RemoveClient( + const SocketAddress& int_ip) { + std::set::iterator it = clients_.find(int_ip); + if (it != clients_.end()) { + clients_.erase(it); + } +} + +NATSocketServer::Translator* NATSocketServer::Translator::FindClient( + const SocketAddress& int_ip) { + // See if we have the requested IP, or any of our children do. + return (clients_.find(int_ip) != clients_.end()) ? + this : nats_.FindClient(int_ip); +} + +// NATSocketServer::TranslatorMap +NATSocketServer::TranslatorMap::~TranslatorMap() { + for (TranslatorMap::iterator it = begin(); it != end(); ++it) { + delete it->second; + } +} + +NATSocketServer::Translator* NATSocketServer::TranslatorMap::Get( + const SocketAddress& ext_ip) { + TranslatorMap::iterator it = find(ext_ip); + return (it != end()) ? it->second : NULL; +} + +NATSocketServer::Translator* NATSocketServer::TranslatorMap::Add( + const SocketAddress& ext_ip, Translator* nat) { + (*this)[ext_ip] = nat; + return nat; +} + +void NATSocketServer::TranslatorMap::Remove( + const SocketAddress& ext_ip) { + TranslatorMap::iterator it = find(ext_ip); + if (it != end()) { + delete it->second; + erase(it); + } +} + +NATSocketServer::Translator* NATSocketServer::TranslatorMap::FindClient( + const SocketAddress& int_ip) { + Translator* nat = NULL; + for (TranslatorMap::iterator it = begin(); it != end() && !nat; ++it) { + nat = it->second->FindClient(int_ip); + } + return nat; +} + +} // namespace talk_base diff --git a/talk/base/natsocketfactory.h b/talk/base/natsocketfactory.h new file mode 100644 index 000000000..d02503fbd --- /dev/null +++ b/talk/base/natsocketfactory.h @@ -0,0 +1,183 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_NATSOCKETFACTORY_H_ +#define TALK_BASE_NATSOCKETFACTORY_H_ + +#include +#include +#include + +#include "talk/base/natserver.h" +#include "talk/base/socketaddress.h" +#include "talk/base/socketserver.h" + +namespace talk_base { + +const size_t kNATEncodedIPv4AddressSize = 8U; +const size_t kNATEncodedIPv6AddressSize = 20U; + +// Used by the NAT socket implementation. +class NATInternalSocketFactory { + public: + virtual ~NATInternalSocketFactory() {} + virtual AsyncSocket* CreateInternalSocket(int family, int type, + const SocketAddress& local_addr, SocketAddress* nat_addr) = 0; +}; + +// Creates sockets that will send all traffic through a NAT, using an existing +// NATServer instance running at nat_addr. The actual data is sent using sockets +// from a socket factory, given to the constructor. +class NATSocketFactory : public SocketFactory, public NATInternalSocketFactory { + public: + NATSocketFactory(SocketFactory* factory, const SocketAddress& nat_addr); + + // SocketFactory implementation + virtual Socket* CreateSocket(int type); + virtual Socket* CreateSocket(int family, int type); + virtual AsyncSocket* CreateAsyncSocket(int type); + virtual AsyncSocket* CreateAsyncSocket(int family, int type); + + // NATInternalSocketFactory implementation + virtual AsyncSocket* CreateInternalSocket(int family, int type, + const SocketAddress& local_addr, SocketAddress* nat_addr); + + private: + SocketFactory* factory_; + SocketAddress nat_addr_; + DISALLOW_EVIL_CONSTRUCTORS(NATSocketFactory); +}; + +// Creates sockets that will send traffic through a NAT depending on what +// address they bind to. This can be used to simulate a client on a NAT sending +// to a client that is not behind a NAT. +// Note that the internal addresses of clients must be unique. This is because +// there is only one socketserver per thread, and the Bind() address is used to +// figure out which NAT (if any) the socket should talk to. +// +// Example with 3 NATs (2 cascaded), and 3 clients. +// ss->AddTranslator("1.2.3.4", "192.168.0.1", NAT_ADDR_RESTRICTED); +// ss->AddTranslator("99.99.99.99", "10.0.0.1", NAT_SYMMETRIC)-> +// AddTranslator("10.0.0.2", "192.168.1.1", NAT_OPEN_CONE); +// ss->GetTranslator("1.2.3.4")->AddClient("1.2.3.4", "192.168.0.2"); +// ss->GetTranslator("99.99.99.99")->AddClient("10.0.0.3"); +// ss->GetTranslator("99.99.99.99")->GetTranslator("10.0.0.2")-> +// AddClient("192.168.1.2"); +class NATSocketServer : public SocketServer, public NATInternalSocketFactory { + public: + class Translator; + // holds a list of NATs + class TranslatorMap : private std::map { + public: + ~TranslatorMap(); + Translator* Get(const SocketAddress& ext_ip); + Translator* Add(const SocketAddress& ext_ip, Translator*); + void Remove(const SocketAddress& ext_ip); + Translator* FindClient(const SocketAddress& int_ip); + }; + + // a specific NAT + class Translator { + public: + Translator(NATSocketServer* server, NATType type, + const SocketAddress& int_addr, SocketFactory* ext_factory, + const SocketAddress& ext_addr); + + SocketFactory* internal_factory() { return internal_factory_.get(); } + SocketAddress internal_address() const { + return nat_server_->internal_address(); + } + SocketAddress internal_tcp_address() const { + return SocketAddress(); // nat_server_->internal_tcp_address(); + } + + Translator* GetTranslator(const SocketAddress& ext_ip); + Translator* AddTranslator(const SocketAddress& ext_ip, + const SocketAddress& int_ip, NATType type); + void RemoveTranslator(const SocketAddress& ext_ip); + + bool AddClient(const SocketAddress& int_ip); + void RemoveClient(const SocketAddress& int_ip); + + // Looks for the specified client in this or a child NAT. + Translator* FindClient(const SocketAddress& int_ip); + + private: + NATSocketServer* server_; + scoped_ptr internal_factory_; + scoped_ptr nat_server_; + TranslatorMap nats_; + std::set clients_; + }; + + explicit NATSocketServer(SocketServer* ss); + + SocketServer* socketserver() { return server_; } + MessageQueue* queue() { return msg_queue_; } + + Translator* GetTranslator(const SocketAddress& ext_ip); + Translator* AddTranslator(const SocketAddress& ext_ip, + const SocketAddress& int_ip, NATType type); + void RemoveTranslator(const SocketAddress& ext_ip); + + // SocketServer implementation + virtual Socket* CreateSocket(int type); + virtual Socket* CreateSocket(int family, int type); + + virtual AsyncSocket* CreateAsyncSocket(int type); + virtual AsyncSocket* CreateAsyncSocket(int family, int type); + + virtual void SetMessageQueue(MessageQueue* queue) { + msg_queue_ = queue; + server_->SetMessageQueue(queue); + } + virtual bool Wait(int cms, bool process_io) { + return server_->Wait(cms, process_io); + } + virtual void WakeUp() { + server_->WakeUp(); + } + + // NATInternalSocketFactory implementation + virtual AsyncSocket* CreateInternalSocket(int family, int type, + const SocketAddress& local_addr, SocketAddress* nat_addr); + + private: + SocketServer* server_; + MessageQueue* msg_queue_; + TranslatorMap nats_; + DISALLOW_EVIL_CONSTRUCTORS(NATSocketServer); +}; + +// Free-standing NAT helper functions. +size_t PackAddressForNAT(char* buf, size_t buf_size, + const SocketAddress& remote_addr); +size_t UnpackAddressFromNAT(const char* buf, size_t buf_size, + SocketAddress* remote_addr); +} // namespace talk_base + +#endif // TALK_BASE_NATSOCKETFACTORY_H_ diff --git a/talk/base/nattypes.cc b/talk/base/nattypes.cc new file mode 100644 index 000000000..290c3adde --- /dev/null +++ b/talk/base/nattypes.cc @@ -0,0 +1,72 @@ +/* + * libjingle + * Copyright 2004--2005, 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 + +#include "talk/base/nattypes.h" + +namespace talk_base { + +class SymmetricNAT : public NAT { +public: + bool IsSymmetric() { return true; } + bool FiltersIP() { return true; } + bool FiltersPort() { return true; } +}; + +class OpenConeNAT : public NAT { +public: + bool IsSymmetric() { return false; } + bool FiltersIP() { return false; } + bool FiltersPort() { return false; } +}; + +class AddressRestrictedNAT : public NAT { +public: + bool IsSymmetric() { return false; } + bool FiltersIP() { return true; } + bool FiltersPort() { return false; } +}; + +class PortRestrictedNAT : public NAT { +public: + bool IsSymmetric() { return false; } + bool FiltersIP() { return true; } + bool FiltersPort() { return true; } +}; + +NAT* NAT::Create(NATType type) { + switch (type) { + case NAT_OPEN_CONE: return new OpenConeNAT(); + case NAT_ADDR_RESTRICTED: return new AddressRestrictedNAT(); + case NAT_PORT_RESTRICTED: return new PortRestrictedNAT(); + case NAT_SYMMETRIC: return new SymmetricNAT(); + default: assert(0); return 0; + } +} + +} // namespace talk_base diff --git a/talk/base/nattypes.h b/talk/base/nattypes.h new file mode 100644 index 000000000..e9602c7d7 --- /dev/null +++ b/talk/base/nattypes.h @@ -0,0 +1,64 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_NATTYPE_H__ +#define TALK_BASE_NATTYPE_H__ + +namespace talk_base { + +/* Identifies each type of NAT that can be simulated. */ +enum NATType { + NAT_OPEN_CONE, + NAT_ADDR_RESTRICTED, + NAT_PORT_RESTRICTED, + NAT_SYMMETRIC +}; + +// Implements the rules for each specific type of NAT. +class NAT { +public: + virtual ~NAT() { } + + // Determines whether this NAT uses both source and destination address when + // checking whether a mapping already exists. + virtual bool IsSymmetric() = 0; + + // Determines whether this NAT drops packets received from a different IP + // the one last sent to. + virtual bool FiltersIP() = 0; + + // Determines whether this NAT drops packets received from a different port + // the one last sent to. + virtual bool FiltersPort() = 0; + + // Returns an implementation of the given type of NAT. + static NAT* Create(NATType type); +}; + +} // namespace talk_base + +#endif // TALK_BASE_NATTYPE_H__ diff --git a/talk/base/nethelpers.cc b/talk/base/nethelpers.cc new file mode 100644 index 000000000..eebc6cfa7 --- /dev/null +++ b/talk/base/nethelpers.cc @@ -0,0 +1,142 @@ +/* + * libjingle + * Copyright 2008, 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/base/nethelpers.h" + +#if defined(WIN32) +#include +#include +#include "talk/base/win32.h" +#endif + +#include "talk/base/byteorder.h" +#include "talk/base/signalthread.h" + +namespace talk_base { + +int ResolveHostname(const std::string& hostname, int family, + std::vector* addresses) { + if (!addresses) { + return -1; + } + addresses->clear(); + struct addrinfo* result = NULL; + struct addrinfo hints = {0}; + // TODO(djw): For now this is IPv4 only so existing users remain unaffected. + hints.ai_family = AF_INET; + hints.ai_flags = AI_ADDRCONFIG; + int ret = getaddrinfo(hostname.c_str(), NULL, &hints, &result); + if (ret != 0) { + return ret; + } + struct addrinfo* cursor = result; + for (; cursor; cursor = cursor->ai_next) { + if (family == AF_UNSPEC || cursor->ai_family == family) { + IPAddress ip; + if (IPFromAddrInfo(cursor, &ip)) { + addresses->push_back(ip); + } + } + } + freeaddrinfo(result); + return 0; +} + +// AsyncResolver +AsyncResolver::AsyncResolver() : error_(0) { +} + +void AsyncResolver::DoWork() { + error_ = ResolveHostname(addr_.hostname().c_str(), addr_.family(), + &addresses_); +} + +void AsyncResolver::OnWorkDone() { + if (addresses_.size() > 0) { + addr_.SetIP(addresses_[0]); + } +} + +const char* inet_ntop(int af, const void *src, char* dst, socklen_t size) { +#ifdef WIN32 + return win32_inet_ntop(af, src, dst, size); +#else + return ::inet_ntop(af, src, dst, size); +#endif +} + +int inet_pton(int af, const char* src, void *dst) { +#ifdef WIN32 + return win32_inet_pton(af, src, dst); +#else + return ::inet_pton(af, src, dst); +#endif +} + +bool HasIPv6Enabled() { +#ifndef WIN32 + // We only need to check this for Windows XP (so far). + return true; +#else + if (IsWindowsVistaOrLater()) { + return true; + } + if (!IsWindowsXpOrLater()) { + return false; + } + DWORD protbuff_size = 4096; + scoped_array protocols; + LPWSAPROTOCOL_INFOW protocol_infos = NULL; + int requested_protocols[2] = {AF_INET6, 0}; + + int err = 0; + int ret = 0; + // Check for protocols in a do-while loop until we provide a buffer large + // enough. (WSCEnumProtocols sets protbuff_size to its desired value). + // It is extremely unlikely that this will loop more than once. + do { + protocols.reset(new char[protbuff_size]); + protocol_infos = reinterpret_cast(protocols.get()); + ret = WSCEnumProtocols(requested_protocols, protocol_infos, + &protbuff_size, &err); + } while (ret == SOCKET_ERROR && err == WSAENOBUFS); + + if (ret == SOCKET_ERROR) { + return false; + } + + // Even if ret is positive, check specifically for IPv6. + // Non-IPv6 enabled WinXP will still return a RAW protocol. + for (int i = 0; i < ret; ++i) { + if (protocol_infos[i].iAddressFamily == AF_INET6) { + return true; + } + } + return false; +#endif +} +} // namespace talk_base diff --git a/talk/base/nethelpers.h b/talk/base/nethelpers.h new file mode 100644 index 000000000..66f79108f --- /dev/null +++ b/talk/base/nethelpers.h @@ -0,0 +1,77 @@ +/* + * libjingle + * Copyright 2008, 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. + */ + +#ifndef TALK_BASE_NETHELPERS_H_ +#define TALK_BASE_NETHELPERS_H_ + +#ifdef POSIX +#include +#include +#elif WIN32 +#include // NOLINT +#endif + +#include + +#include "talk/base/signalthread.h" +#include "talk/base/sigslot.h" +#include "talk/base/socketaddress.h" + +namespace talk_base { + +// AsyncResolver will perform async DNS resolution, signaling the result on +// the inherited SignalWorkDone when the operation completes. +class AsyncResolver : public SignalThread { + public: + AsyncResolver(); + + const SocketAddress& address() const { return addr_; } + const std::vector& addresses() const { return addresses_; } + void set_address(const SocketAddress& addr) { addr_ = addr; } + int error() const { return error_; } + void set_error(int error) { error_ = error; } + + + protected: + virtual void DoWork(); + virtual void OnWorkDone(); + + private: + SocketAddress addr_; + std::vector addresses_; + int error_; +}; + +// talk_base namespaced wrappers for inet_ntop and inet_pton so we can avoid +// the windows-native versions of these. +const char* inet_ntop(int af, const void *src, char* dst, socklen_t size); +int inet_pton(int af, const char* src, void *dst); + +bool HasIPv6Enabled(); +} // namespace talk_base + +#endif // TALK_BASE_NETHELPERS_H_ diff --git a/talk/base/network.cc b/talk/base/network.cc new file mode 100644 index 000000000..b32bb09ef --- /dev/null +++ b/talk/base/network.cc @@ -0,0 +1,542 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "talk/base/network.h" + +#ifdef POSIX +#include +#include +#include +#include +#include +#include +#ifdef ANDROID +#include "talk/base/ifaddrs-android.h" +#else +#include +#endif +#endif // POSIX + +#ifdef WIN32 +#include "talk/base/win32.h" +#include +#endif + +#include +#include + +#include "talk/base/host.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/socket.h" // includes something that makes windows happy +#include "talk/base/stream.h" +#include "talk/base/stringencode.h" +#include "talk/base/thread.h" + +namespace talk_base { +namespace { + +const uint32 kUpdateNetworksMessage = 1; +const uint32 kSignalNetworksMessage = 2; + +// Fetch list of networks every two seconds. +const int kNetworksUpdateIntervalMs = 2000; + + +// Makes a string key for this network. Used in the network manager's maps. +// Network objects are keyed on interface name, network prefix and the +// length of that prefix. +std::string MakeNetworkKey(const std::string& name, const IPAddress& prefix, + int prefix_length) { + std::ostringstream ost; + ost << name << "%" << prefix.ToString() << "/" << prefix_length; + return ost.str(); +} + +bool CompareNetworks(const Network* a, const Network* b) { + if (a->prefix_length() == b->prefix_length()) { + if (a->name() == b->name()) { + return a->prefix() < b->prefix(); + } + } + return a->name() < b->name(); +} + + +} // namespace + +NetworkManager::NetworkManager() { +} + +NetworkManager::~NetworkManager() { +} + +NetworkManagerBase::NetworkManagerBase() : ipv6_enabled_(true) { +} + +NetworkManagerBase::~NetworkManagerBase() { + for (NetworkMap::iterator i = networks_map_.begin(); + i != networks_map_.end(); ++i) { + delete i->second; + } +} + +void NetworkManagerBase::GetNetworks(NetworkList* result) const { + *result = networks_; +} + +void NetworkManagerBase::MergeNetworkList(const NetworkList& new_networks, + bool* changed) { + // Sort the list so that we can detect when it changes. + typedef std::pair > address_list; + std::map address_map; + NetworkList list(new_networks); + NetworkList merged_list; + std::sort(list.begin(), list.end(), CompareNetworks); + + *changed = false; + + if (networks_.size() != list.size()) + *changed = true; + + // First, build a set of network-keys to the ipaddresses. + for (uint32 i = 0; i < list.size(); ++i) { + bool might_add_to_merged_list = false; + std::string key = MakeNetworkKey(list[i]->name(), + list[i]->prefix(), + list[i]->prefix_length()); + if (address_map.find(key) == address_map.end()) { + address_map[key] = address_list(list[i], std::vector()); + might_add_to_merged_list = true; + } + const std::vector& addresses = list[i]->GetIPs(); + address_list& current_list = address_map[key]; + for (std::vector::const_iterator it = addresses.begin(); + it != addresses.end(); + ++it) { + current_list.second.push_back(*it); + } + if (!might_add_to_merged_list) { + delete list[i]; + } + } + + // Next, look for existing network objects to re-use. + for (std::map::iterator it = address_map.begin(); + it != address_map.end(); + ++it) { + const std::string& key = it->first; + Network* net = it->second.first; + NetworkMap::iterator existing = networks_map_.find(key); + if (existing == networks_map_.end()) { + // This network is new. Place it in the network map. + merged_list.push_back(net); + networks_map_[key] = net; + *changed = true; + } else { + // This network exists in the map already. Reset its IP addresses. + *changed = existing->second->SetIPs(it->second.second, *changed); + merged_list.push_back(existing->second); + if (existing->second != net) { + delete net; + } + } + } + networks_ = merged_list; +} + +BasicNetworkManager::BasicNetworkManager() + : thread_(NULL), + start_count_(0) { +} + +BasicNetworkManager::~BasicNetworkManager() { +} + +#if defined(POSIX) +void BasicNetworkManager::ConvertIfAddrs(struct ifaddrs* interfaces, + bool include_ignored, + NetworkList* networks) const { + NetworkMap current_networks; + for (struct ifaddrs* cursor = interfaces; + cursor != NULL; cursor = cursor->ifa_next) { + IPAddress prefix; + IPAddress mask; + IPAddress ip; + int scope_id = 0; + + // Some interfaces may not have address assigned. + if (!cursor->ifa_addr || !cursor->ifa_netmask) + continue; + + switch (cursor->ifa_addr->sa_family) { + case AF_INET: { + ip = IPAddress( + reinterpret_cast(cursor->ifa_addr)->sin_addr); + mask = IPAddress( + reinterpret_cast(cursor->ifa_netmask)->sin_addr); + break; + } + case AF_INET6: { + if (ipv6_enabled()) { + ip = IPAddress( + reinterpret_cast(cursor->ifa_addr)->sin6_addr); + mask = IPAddress( + reinterpret_cast(cursor->ifa_netmask)->sin6_addr); + scope_id = + reinterpret_cast(cursor->ifa_addr)->sin6_scope_id; + break; + } else { + continue; + } + } + default: { + continue; + } + } + int prefix_length = CountIPMaskBits(mask); + prefix = TruncateIP(ip, prefix_length); + std::string key = MakeNetworkKey(std::string(cursor->ifa_name), + prefix, prefix_length); + NetworkMap::iterator existing_network = current_networks.find(key); + if (existing_network == current_networks.end()) { + scoped_ptr network(new Network(cursor->ifa_name, + cursor->ifa_name, + prefix, + prefix_length)); + network->set_scope_id(scope_id); + network->AddIP(ip); + bool ignored = ((cursor->ifa_flags & IFF_LOOPBACK) || + IsIgnoredNetwork(*network)); + network->set_ignored(ignored); + if (include_ignored || !network->ignored()) { + networks->push_back(network.release()); + } + } else { + (*existing_network).second->AddIP(ip); + } + } +} + +bool BasicNetworkManager::CreateNetworks(bool include_ignored, + NetworkList* networks) const { + struct ifaddrs* interfaces; + int error = getifaddrs(&interfaces); + if (error != 0) { + LOG_ERR(LERROR) << "getifaddrs failed to gather interface data: " << error; + return false; + } + + ConvertIfAddrs(interfaces, include_ignored, networks); + + freeifaddrs(interfaces); + return true; +} + +#elif defined(WIN32) + +unsigned int GetPrefix(PIP_ADAPTER_PREFIX prefixlist, + const IPAddress& ip, IPAddress* prefix) { + IPAddress current_prefix; + IPAddress best_prefix; + unsigned int best_length = 0; + while (prefixlist) { + // Look for the longest matching prefix in the prefixlist. + if (prefixlist->Address.lpSockaddr == NULL || + prefixlist->Address.lpSockaddr->sa_family != ip.family()) { + prefixlist = prefixlist->Next; + continue; + } + switch (prefixlist->Address.lpSockaddr->sa_family) { + case AF_INET: { + sockaddr_in* v4_addr = + reinterpret_cast(prefixlist->Address.lpSockaddr); + current_prefix = IPAddress(v4_addr->sin_addr); + break; + } + case AF_INET6: { + sockaddr_in6* v6_addr = + reinterpret_cast(prefixlist->Address.lpSockaddr); + current_prefix = IPAddress(v6_addr->sin6_addr); + break; + } + default: { + prefixlist = prefixlist->Next; + continue; + } + } + if (TruncateIP(ip, prefixlist->PrefixLength) == current_prefix && + prefixlist->PrefixLength > best_length) { + best_prefix = current_prefix; + best_length = prefixlist->PrefixLength; + } + prefixlist = prefixlist->Next; + } + *prefix = best_prefix; + return best_length; +} + +bool BasicNetworkManager::CreateNetworks(bool include_ignored, + NetworkList* networks) const { + NetworkMap current_networks; + // MSDN recommends a 15KB buffer for the first try at GetAdaptersAddresses. + size_t buffer_size = 16384; + scoped_array adapter_info(new char[buffer_size]); + PIP_ADAPTER_ADDRESSES adapter_addrs = + reinterpret_cast(adapter_info.get()); + int adapter_flags = (GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_SKIP_ANYCAST | + GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_PREFIX); + int ret = 0; + do { + adapter_info.reset(new char[buffer_size]); + adapter_addrs = reinterpret_cast(adapter_info.get()); + ret = GetAdaptersAddresses(AF_UNSPEC, adapter_flags, + 0, adapter_addrs, + reinterpret_cast(&buffer_size)); + } while (ret == ERROR_BUFFER_OVERFLOW); + if (ret != ERROR_SUCCESS) { + return false; + } + int count = 0; + while (adapter_addrs) { + if (adapter_addrs->OperStatus == IfOperStatusUp) { + PIP_ADAPTER_UNICAST_ADDRESS address = adapter_addrs->FirstUnicastAddress; + PIP_ADAPTER_PREFIX prefixlist = adapter_addrs->FirstPrefix; + std::string name; + std::string description; +#ifdef _DEBUG + name = ToUtf8(adapter_addrs->FriendlyName, + wcslen(adapter_addrs->FriendlyName)); +#endif + description = ToUtf8(adapter_addrs->Description, + wcslen(adapter_addrs->Description)); + for (; address; address = address->Next) { +#ifndef _DEBUG + name = talk_base::ToString(count); +#endif + + IPAddress ip; + int scope_id = 0; + scoped_ptr network; + switch (address->Address.lpSockaddr->sa_family) { + case AF_INET: { + sockaddr_in* v4_addr = + reinterpret_cast(address->Address.lpSockaddr); + ip = IPAddress(v4_addr->sin_addr); + break; + } + case AF_INET6: { + if (ipv6_enabled()) { + sockaddr_in6* v6_addr = + reinterpret_cast(address->Address.lpSockaddr); + scope_id = v6_addr->sin6_scope_id; + ip = IPAddress(v6_addr->sin6_addr); + break; + } else { + continue; + } + } + default: { + continue; + } + } + IPAddress prefix; + int prefix_length = GetPrefix(prefixlist, ip, &prefix); + std::string key = MakeNetworkKey(name, prefix, prefix_length); + NetworkMap::iterator existing_network = current_networks.find(key); + if (existing_network == current_networks.end()) { + scoped_ptr network(new Network(name, + description, + prefix, + prefix_length)); + network->set_scope_id(scope_id); + network->AddIP(ip); + bool ignore = ((adapter_addrs->IfType == IF_TYPE_SOFTWARE_LOOPBACK) || + IsIgnoredNetwork(*network)); + network->set_ignored(ignore); + if (include_ignored || !network->ignored()) { + networks->push_back(network.release()); + } + } else { + (*existing_network).second->AddIP(ip); + } + } + // Count is per-adapter - all 'Networks' created from the same + // adapter need to have the same name. + ++count; + } + adapter_addrs = adapter_addrs->Next; + } + return true; +} +#endif // WIN32 + +bool BasicNetworkManager::IsIgnoredNetwork(const Network& network) { +#ifdef POSIX + // Ignore local networks (lo, lo0, etc) + // Also filter out VMware interfaces, typically named vmnet1 and vmnet8 + if (strncmp(network.name().c_str(), "vmnet", 5) == 0 || + strncmp(network.name().c_str(), "vnic", 4) == 0) { + return true; + } +#elif defined(WIN32) + // Ignore any HOST side vmware adapters with a description like: + // VMware Virtual Ethernet Adapter for VMnet1 + // but don't ignore any GUEST side adapters with a description like: + // VMware Accelerated AMD PCNet Adapter #2 + if (strstr(network.description().c_str(), "VMnet") != NULL) { + return true; + } +#endif + + // Ignore any networks with a 0.x.y.z IP + if (network.prefix().family() == AF_INET) { + return (network.prefix().v4AddressAsHostOrderInteger() < 0x01000000); + } + return false; +} + +void BasicNetworkManager::StartUpdating() { + thread_ = Thread::Current(); + if (start_count_) { + // If network interfaces are already discovered and signal is sent, + // we should trigger network signal immediately for the new clients + // to start allocating ports. + if (sent_first_update_) + thread_->Post(this, kSignalNetworksMessage); + } else { + thread_->Post(this, kUpdateNetworksMessage); + } + ++start_count_; +} + +void BasicNetworkManager::StopUpdating() { + ASSERT(Thread::Current() == thread_); + if (!start_count_) + return; + + --start_count_; + if (!start_count_) { + thread_->Clear(this); + sent_first_update_ = false; + } +} + +void BasicNetworkManager::OnMessage(Message* msg) { + switch (msg->message_id) { + case kUpdateNetworksMessage: { + DoUpdateNetworks(); + break; + } + case kSignalNetworksMessage: { + SignalNetworksChanged(); + break; + } + default: + ASSERT(false); + } +} + +void BasicNetworkManager::DoUpdateNetworks() { + if (!start_count_) + return; + + ASSERT(Thread::Current() == thread_); + + NetworkList list; + if (!CreateNetworks(false, &list)) { + SignalError(); + } else { + bool changed; + MergeNetworkList(list, &changed); + if (changed || !sent_first_update_) { + SignalNetworksChanged(); + sent_first_update_ = true; + } + } + + thread_->PostDelayed(kNetworksUpdateIntervalMs, this, kUpdateNetworksMessage); +} + +void BasicNetworkManager::DumpNetworks(bool include_ignored) { + NetworkList list; + CreateNetworks(include_ignored, &list); + LOG(LS_INFO) << "NetworkManager detected " << list.size() << " networks:"; + for (size_t i = 0; i < list.size(); ++i) { + const Network* network = list[i]; + if (!network->ignored() || include_ignored) { + LOG(LS_INFO) << network->ToString() << ": " + << network->description() + << ((network->ignored()) ? ", Ignored" : ""); + } + } +} + +Network::Network(const std::string& name, const std::string& desc, + const IPAddress& prefix, int prefix_length) + : name_(name), description_(desc), prefix_(prefix), + prefix_length_(prefix_length), scope_id_(0), ignored_(false), + uniform_numerator_(0), uniform_denominator_(0), exponential_numerator_(0), + exponential_denominator_(0) { +} + +std::string Network::ToString() const { + std::stringstream ss; + // Print out the first space-terminated token of the network desc, plus + // the IP address. + ss << "Net[" << description_.substr(0, description_.find(' ')) + << ":" << prefix_.ToSensitiveString() << "/" << prefix_length_ << "]"; + return ss.str(); +} + +// Sets the addresses of this network. Returns true if the address set changed. +// Change detection is short circuited if the changed argument is true. +bool Network::SetIPs(const std::vector& ips, bool changed) { + changed = changed || ips.size() != ips_.size(); + // Detect changes with a nested loop; n-squared but we expect on the order + // of 2-3 addresses per network. + for (std::vector::const_iterator it = ips.begin(); + !changed && it != ips.end(); + ++it) { + bool found = false; + for (std::vector::iterator inner_it = ips_.begin(); + !found && inner_it != ips_.end(); + ++inner_it) { + if (*it == *inner_it) { + found = true; + } + } + changed = !found; + } + ips_ = ips; + return changed; +} +} // namespace talk_base diff --git a/talk/base/network.h b/talk/base/network.h new file mode 100644 index 000000000..f87063da5 --- /dev/null +++ b/talk/base/network.h @@ -0,0 +1,227 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_NETWORK_H_ +#define TALK_BASE_NETWORK_H_ + +#include +#include +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/ipaddress.h" +#include "talk/base/messagehandler.h" +#include "talk/base/sigslot.h" + +#if defined(POSIX) +struct ifaddrs; +#endif // defined(POSIX) + +namespace talk_base { + +class Network; +class NetworkSession; +class Thread; + +// Generic network manager interface. It provides list of local +// networks. +class NetworkManager { + public: + typedef std::vector NetworkList; + + NetworkManager(); + virtual ~NetworkManager(); + + // Called when network list is updated. + sigslot::signal0<> SignalNetworksChanged; + + // Indicates a failure when getting list of network interfaces. + sigslot::signal0<> SignalError; + + // Start/Stop monitoring of network interfaces + // list. SignalNetworksChanged or SignalError is emitted immidiately + // after StartUpdating() is called. After that SignalNetworksChanged + // is emitted wheneven list of networks changes. + virtual void StartUpdating() = 0; + virtual void StopUpdating() = 0; + + // Returns the current list of networks available on this machine. + // UpdateNetworks() must be called before this method is called. + // It makes sure that repeated calls return the same object for a + // given network, so that quality is tracked appropriately. Does not + // include ignored networks. + virtual void GetNetworks(NetworkList* networks) const = 0; + + // Dumps a list of networks available to LS_INFO. + virtual void DumpNetworks(bool include_ignored) {} +}; + +// Base class for NetworkManager implementations. +class NetworkManagerBase : public NetworkManager { + public: + NetworkManagerBase(); + virtual ~NetworkManagerBase(); + + virtual void GetNetworks(std::vector* networks) const; + bool ipv6_enabled() const { return ipv6_enabled_; } + void set_ipv6_enabled(bool enabled) { ipv6_enabled_ = enabled; } + + protected: + typedef std::map NetworkMap; + // Updates |networks_| with the networks listed in |list|. If + // |network_map_| already has a Network object for a network listed + // in the |list| then it is reused. Accept ownership of the Network + // objects in the |list|. |changed| will be set to true if there is + // any change in the network list. + void MergeNetworkList(const NetworkList& list, bool* changed); + + private: + friend class NetworkTest; + void DoUpdateNetworks(); + + NetworkList networks_; + NetworkMap networks_map_; + bool ipv6_enabled_; +}; + +// Basic implementation of the NetworkManager interface that gets list +// of networks using OS APIs. +class BasicNetworkManager : public NetworkManagerBase, + public MessageHandler { + public: + BasicNetworkManager(); + virtual ~BasicNetworkManager(); + + virtual void StartUpdating(); + virtual void StopUpdating(); + + // Logs the available networks. + virtual void DumpNetworks(bool include_ignored); + + // MessageHandler interface. + virtual void OnMessage(Message* msg); + bool started() { return start_count_ > 0; } + + protected: +#if defined(POSIX) + // Separated from CreateNetworks for tests. + void ConvertIfAddrs(ifaddrs* interfaces, + bool include_ignored, + NetworkList* networks) const; +#endif // defined(POSIX) + + // Creates a network object for each network available on the machine. + bool CreateNetworks(bool include_ignored, NetworkList* networks) const; + + // Determines if a network should be ignored. + static bool IsIgnoredNetwork(const Network& network); + + private: + friend class NetworkTest; + + void DoUpdateNetworks(); + + Thread* thread_; + bool sent_first_update_; + int start_count_; +}; + +// Represents a Unix-type network interface, with a name and single address. +class Network { + public: + Network() : prefix_(INADDR_ANY), scope_id_(0) {} + Network(const std::string& name, const std::string& description, + const IPAddress& prefix, int prefix_length); + + // Returns the name of the interface this network is associated wtih. + const std::string& name() const { return name_; } + + // Returns the OS-assigned name for this network. This is useful for + // debugging but should not be sent over the wire (for privacy reasons). + const std::string& description() const { return description_; } + + // Returns the prefix for this network. + const IPAddress& prefix() const { return prefix_; } + // Returns the length, in bits, of this network's prefix. + int prefix_length() const { return prefix_length_; } + + // Returns the Network's current idea of the 'best' IP it has. + // 'Best' currently means the first one added. + // TODO: We should be preferring temporary addresses. + // Returns an unset IP if this network has no active addresses. + IPAddress ip() const { + if (ips_.size() == 0) { + return IPAddress(); + } + return ips_.at(0); + } + // Adds an active IP address to this network. Does not check for duplicates. + void AddIP(const IPAddress& ip) { ips_.push_back(ip); } + + // Sets the network's IP address list. Returns true if new IP addresses were + // detected. Passing true to already_changed skips this check. + bool SetIPs(const std::vector& ips, bool already_changed); + // Get the list of IP Addresses associated with this network. + const std::vector& GetIPs() { return ips_;} + // Clear the network's list of addresses. + void ClearIPs() { ips_.clear(); } + + // Returns the scope-id of the network's address. + // Should only be relevant for link-local IPv6 addresses. + int scope_id() const { return scope_id_; } + void set_scope_id(int id) { scope_id_ = id; } + + // Indicates whether this network should be ignored, perhaps because + // the IP is 0, or the interface is one we know is invalid. + bool ignored() const { return ignored_; } + void set_ignored(bool ignored) { ignored_ = ignored; } + + // Debugging description of this network + std::string ToString() const; + + private: + typedef std::vector SessionList; + + std::string name_; + std::string description_; + IPAddress prefix_; + int prefix_length_; + std::vector ips_; + int scope_id_; + bool ignored_; + SessionList sessions_; + double uniform_numerator_; + double uniform_denominator_; + double exponential_numerator_; + double exponential_denominator_; + + friend class NetworkManager; +}; +} // namespace talk_base + +#endif // TALK_BASE_NETWORK_H_ diff --git a/talk/base/network_unittest.cc b/talk/base/network_unittest.cc new file mode 100644 index 000000000..146b7853f --- /dev/null +++ b/talk/base/network_unittest.cc @@ -0,0 +1,512 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/network.h" + +#include +#if defined(POSIX) +#include +#ifndef ANDROID +#include +#else +#include "talk/base/ifaddrs-android.h" +#endif +#endif +#include "talk/base/gunit.h" + +namespace talk_base { + +class NetworkTest : public testing::Test, public sigslot::has_slots<> { + public: + NetworkTest() : callback_called_(false) {} + + void OnNetworksChanged() { + callback_called_ = true; + } + + void MergeNetworkList(BasicNetworkManager& network_manager, + const NetworkManager::NetworkList& list, + bool* changed ) { + network_manager.MergeNetworkList(list, changed); + } + + bool IsIgnoredNetwork(const Network& network) { + return BasicNetworkManager::IsIgnoredNetwork(network); + } + + NetworkManager::NetworkList GetNetworks( + const BasicNetworkManager& network_manager, bool include_ignored) { + NetworkManager::NetworkList list; + network_manager.CreateNetworks(include_ignored, &list); + return list; + } + +#if defined(POSIX) + // Separated from CreateNetworks for tests. + static void CallConvertIfAddrs(const BasicNetworkManager& network_manager, + struct ifaddrs* interfaces, + bool include_ignored, + NetworkManager::NetworkList* networks) { + network_manager.ConvertIfAddrs(interfaces, include_ignored, networks); + } +#endif // defined(POSIX) + + protected: + bool callback_called_; +}; + +// Test that the Network ctor works properly. +TEST_F(NetworkTest, TestNetworkConstruct) { + Network ipv4_network1("test_eth0", "Test Network Adapter 1", + IPAddress(0x12345600U), 24); + EXPECT_EQ("test_eth0", ipv4_network1.name()); + EXPECT_EQ("Test Network Adapter 1", ipv4_network1.description()); + EXPECT_EQ(IPAddress(0x12345600U), ipv4_network1.prefix()); + EXPECT_EQ(24, ipv4_network1.prefix_length()); + EXPECT_FALSE(ipv4_network1.ignored()); +} + +// Tests that our ignore function works properly. +TEST_F(NetworkTest, TestNetworkIgnore) { + Network ipv4_network1("test_eth0", "Test Network Adapter 1", + IPAddress(0x12345600U), 24); + Network ipv4_network2("test_eth1", "Test Network Adapter 2", + IPAddress(0x00010000U), 16); + EXPECT_FALSE(IsIgnoredNetwork(ipv4_network1)); + EXPECT_TRUE(IsIgnoredNetwork(ipv4_network2)); +} + +TEST_F(NetworkTest, TestCreateNetworks) { + BasicNetworkManager manager; + NetworkManager::NetworkList result = GetNetworks(manager, true); + // We should be able to bind to any addresses we find. + NetworkManager::NetworkList::iterator it; + for (it = result.begin(); + it != result.end(); + ++it) { + sockaddr_storage storage; + memset(&storage, 0, sizeof(storage)); + IPAddress ip = (*it)->ip(); + SocketAddress bindaddress(ip, 0); + bindaddress.SetScopeID((*it)->scope_id()); + // TODO: Make this use talk_base::AsyncSocket once it supports IPv6. + int fd = static_cast(socket(ip.family(), SOCK_STREAM, IPPROTO_TCP)); + if (fd > 0) { + size_t ipsize = bindaddress.ToSockAddrStorage(&storage); + EXPECT_GE(ipsize, 0U); + int success = ::bind(fd, + reinterpret_cast(&storage), + static_cast(ipsize)); + EXPECT_EQ(0, success); +#ifdef WIN32 + closesocket(fd); +#else + close(fd); +#endif + } + } +} + +// Test that UpdateNetworks succeeds. +TEST_F(NetworkTest, TestUpdateNetworks) { + BasicNetworkManager manager; + manager.SignalNetworksChanged.connect( + static_cast(this), &NetworkTest::OnNetworksChanged); + manager.StartUpdating(); + Thread::Current()->ProcessMessages(0); + EXPECT_TRUE(callback_called_); + callback_called_ = false; + // Callback should be triggered immediately when StartUpdating + // is called, after network update signal is already sent. + manager.StartUpdating(); + EXPECT_TRUE(manager.started()); + Thread::Current()->ProcessMessages(0); + EXPECT_TRUE(callback_called_); + manager.StopUpdating(); + EXPECT_TRUE(manager.started()); + manager.StopUpdating(); + EXPECT_FALSE(manager.started()); + manager.StopUpdating(); + EXPECT_FALSE(manager.started()); + callback_called_ = false; + // Callback should be triggered immediately after StartUpdating is called + // when start_count_ is reset to 0. + manager.StartUpdating(); + Thread::Current()->ProcessMessages(0); + EXPECT_TRUE(callback_called_); +} + +// Verify that MergeNetworkList() merges network lists properly. +TEST_F(NetworkTest, TestBasicMergeNetworkList) { + Network ipv4_network1("test_eth0", "Test Network Adapter 1", + IPAddress(0x12345600U), 24); + Network ipv4_network2("test_eth1", "Test Network Adapter 2", + IPAddress(0x00010000U), 16); + ipv4_network1.AddIP(IPAddress(0x12345678)); + ipv4_network2.AddIP(IPAddress(0x00010004)); + BasicNetworkManager manager; + + // Add ipv4_network1 to the list of networks. + NetworkManager::NetworkList list; + list.push_back(new Network(ipv4_network1)); + bool changed; + MergeNetworkList(manager, list, &changed); + EXPECT_TRUE(changed); + list.clear(); + + manager.GetNetworks(&list); + EXPECT_EQ(1U, list.size()); + EXPECT_EQ(ipv4_network1.ToString(), list[0]->ToString()); + Network* net1 = list[0]; + list.clear(); + + // Replace ipv4_network1 with ipv4_network2. + list.push_back(new Network(ipv4_network2)); + MergeNetworkList(manager, list, &changed); + EXPECT_TRUE(changed); + list.clear(); + + manager.GetNetworks(&list); + EXPECT_EQ(1U, list.size()); + EXPECT_EQ(ipv4_network2.ToString(), list[0]->ToString()); + Network* net2 = list[0]; + list.clear(); + + // Add Network2 back. + list.push_back(new Network(ipv4_network1)); + list.push_back(new Network(ipv4_network2)); + MergeNetworkList(manager, list, &changed); + EXPECT_TRUE(changed); + list.clear(); + + // Verify that we get previous instances of Network objects. + manager.GetNetworks(&list); + EXPECT_EQ(2U, list.size()); + EXPECT_TRUE((net1 == list[0] && net2 == list[1]) || + (net1 == list[1] && net2 == list[0])); + list.clear(); + + // Call MergeNetworkList() again and verify that we don't get update + // notification. + list.push_back(new Network(ipv4_network2)); + list.push_back(new Network(ipv4_network1)); + MergeNetworkList(manager, list, &changed); + EXPECT_FALSE(changed); + list.clear(); + + // Verify that we get previous instances of Network objects. + manager.GetNetworks(&list); + EXPECT_EQ(2U, list.size()); + EXPECT_TRUE((net1 == list[0] && net2 == list[1]) || + (net1 == list[1] && net2 == list[0])); + list.clear(); +} + +// Sets up some test IPv6 networks and appends them to list. +// Four networks are added - public and link local, for two interfaces. +void SetupNetworks(NetworkManager::NetworkList* list) { + IPAddress ip; + IPAddress prefix; + EXPECT_TRUE(IPFromString("fe80::1234:5678:abcd:ef12", &ip)); + EXPECT_TRUE(IPFromString("fe80::", &prefix)); + // First, fake link-locals. + Network ipv6_eth0_linklocalnetwork("test_eth0", "Test NetworkAdapter 1", + prefix, 64); + ipv6_eth0_linklocalnetwork.AddIP(ip); + EXPECT_TRUE(IPFromString("fe80::5678:abcd:ef12:3456", &ip)); + Network ipv6_eth1_linklocalnetwork("test_eth1", "Test NetworkAdapter 2", + prefix, 64); + ipv6_eth1_linklocalnetwork.AddIP(ip); + // Public networks: + EXPECT_TRUE(IPFromString("2401:fa00:4:1000:be30:5bff:fee5:c3", &ip)); + prefix = TruncateIP(ip, 64); + Network ipv6_eth0_publicnetwork1_ip1("test_eth0", "Test NetworkAdapter 1", + prefix, 64); + ipv6_eth0_publicnetwork1_ip1.AddIP(ip); + EXPECT_TRUE(IPFromString("2400:4030:1:2c00:be30:abcd:efab:cdef", &ip)); + prefix = TruncateIP(ip, 64); + Network ipv6_eth1_publicnetwork1_ip1("test_eth1", "Test NetworkAdapter 1", + prefix, 64); + ipv6_eth1_publicnetwork1_ip1.AddIP(ip); + list->push_back(new Network(ipv6_eth0_linklocalnetwork)); + list->push_back(new Network(ipv6_eth1_linklocalnetwork)); + list->push_back(new Network(ipv6_eth0_publicnetwork1_ip1)); + list->push_back(new Network(ipv6_eth1_publicnetwork1_ip1)); +} + +// Test that the basic network merging case works. +TEST_F(NetworkTest, TestIPv6MergeNetworkList) { + BasicNetworkManager manager; + manager.SignalNetworksChanged.connect( + static_cast(this), &NetworkTest::OnNetworksChanged); + NetworkManager::NetworkList original_list; + SetupNetworks(&original_list); + bool changed = false; + MergeNetworkList(manager, original_list, &changed); + EXPECT_TRUE(changed); + NetworkManager::NetworkList list; + manager.GetNetworks(&list); + EXPECT_EQ(original_list.size(), list.size()); + // Verify that the original members are in the merged list. + for (NetworkManager::NetworkList::iterator it = original_list.begin(); + it != original_list.end(); ++it) { + EXPECT_NE(list.end(), std::find(list.begin(), list.end(), *it)); + } +} + +// Tests that when two network lists that describe the same set of networks are +// merged, that the changed callback is not called, and that the original +// objects remain in the result list. +TEST_F(NetworkTest, TestNoChangeMerge) { + BasicNetworkManager manager; + manager.SignalNetworksChanged.connect( + static_cast(this), &NetworkTest::OnNetworksChanged); + NetworkManager::NetworkList original_list; + SetupNetworks(&original_list); + bool changed = false; + MergeNetworkList(manager, original_list, &changed); + EXPECT_TRUE(changed); + // Second list that describes the same networks but with new objects. + NetworkManager::NetworkList second_list; + SetupNetworks(&second_list); + changed = false; + MergeNetworkList(manager, second_list, &changed); + EXPECT_FALSE(changed); + NetworkManager::NetworkList resulting_list; + manager.GetNetworks(&resulting_list); + EXPECT_EQ(original_list.size(), resulting_list.size()); + // Verify that the original members are in the merged list. + for (NetworkManager::NetworkList::iterator it = original_list.begin(); + it != original_list.end(); ++it) { + EXPECT_NE(resulting_list.end(), + std::find(resulting_list.begin(), resulting_list.end(), *it)); + } + // Doublecheck that the new networks aren't in the list. + for (NetworkManager::NetworkList::iterator it = second_list.begin(); + it != second_list.end(); ++it) { + EXPECT_EQ(resulting_list.end(), + std::find(resulting_list.begin(), resulting_list.end(), *it)); + } +} + +// Test that we can merge a network that is the same as another network but with +// a different IP. The original network should remain in the list, but have its +// IP changed. +TEST_F(NetworkTest, MergeWithChangedIP) { + BasicNetworkManager manager; + manager.SignalNetworksChanged.connect( + static_cast(this), &NetworkTest::OnNetworksChanged); + NetworkManager::NetworkList original_list; + SetupNetworks(&original_list); + // Make a network that we're going to change. + IPAddress ip; + EXPECT_TRUE(IPFromString("2401:fa01:4:1000:be30:faa:fee:faa", &ip)); + IPAddress prefix = TruncateIP(ip, 64); + Network* network_to_change = new Network("test_eth0", + "Test Network Adapter 1", + prefix, 64); + Network* changed_network = new Network(*network_to_change); + network_to_change->AddIP(ip); + IPAddress changed_ip; + EXPECT_TRUE(IPFromString("2401:fa01:4:1000:be30:f00:f00:f00", &changed_ip)); + changed_network->AddIP(changed_ip); + original_list.push_back(network_to_change); + bool changed = false; + MergeNetworkList(manager, original_list, &changed); + NetworkManager::NetworkList second_list; + SetupNetworks(&second_list); + second_list.push_back(changed_network); + changed = false; + MergeNetworkList(manager, second_list, &changed); + EXPECT_TRUE(changed); + NetworkManager::NetworkList list; + manager.GetNetworks(&list); + EXPECT_EQ(original_list.size(), list.size()); + // Make sure the original network is still in the merged list. + EXPECT_NE(list.end(), + std::find(list.begin(), list.end(), network_to_change)); + EXPECT_EQ(changed_ip, network_to_change->GetIPs().at(0)); +} + +// Testing a similar case to above, but checking that a network can be updated +// with additional IPs (not just a replacement). +TEST_F(NetworkTest, TestMultipleIPMergeNetworkList) { + BasicNetworkManager manager; + manager.SignalNetworksChanged.connect( + static_cast(this), &NetworkTest::OnNetworksChanged); + NetworkManager::NetworkList original_list; + SetupNetworks(&original_list); + bool changed = false; + MergeNetworkList(manager, original_list, &changed); + EXPECT_TRUE(changed); + IPAddress ip; + IPAddress check_ip; + IPAddress prefix; + // Add a second IP to the public network on eth0 (2401:fa00:4:1000/64). + EXPECT_TRUE(IPFromString("2401:fa00:4:1000:be30:5bff:fee5:c6", &ip)); + prefix = TruncateIP(ip, 64); + Network ipv6_eth0_publicnetwork1_ip2("test_eth0", "Test NetworkAdapter 1", + prefix, 64); + // This is the IP that already existed in the public network on eth0. + EXPECT_TRUE(IPFromString("2401:fa00:4:1000:be30:5bff:fee5:c3", &check_ip)); + ipv6_eth0_publicnetwork1_ip2.AddIP(ip); + original_list.push_back(new Network(ipv6_eth0_publicnetwork1_ip2)); + changed = false; + MergeNetworkList(manager, original_list, &changed); + EXPECT_TRUE(changed); + // There should still be four networks. + NetworkManager::NetworkList list; + manager.GetNetworks(&list); + EXPECT_EQ(4U, list.size()); + // Check the gathered IPs. + int matchcount = 0; + for (NetworkManager::NetworkList::iterator it = list.begin(); + it != list.end(); ++it) { + if ((*it)->ToString() == original_list[2]->ToString()) { + ++matchcount; + EXPECT_EQ(1, matchcount); + // This should be the same network object as before. + EXPECT_EQ((*it), original_list[2]); + // But with two addresses now. + EXPECT_EQ(2U, (*it)->GetIPs().size()); + EXPECT_NE((*it)->GetIPs().end(), + std::find((*it)->GetIPs().begin(), + (*it)->GetIPs().end(), + check_ip)); + EXPECT_NE((*it)->GetIPs().end(), + std::find((*it)->GetIPs().begin(), + (*it)->GetIPs().end(), + ip)); + } else { + // Check the IP didn't get added anywhere it wasn't supposed to. + EXPECT_EQ((*it)->GetIPs().end(), + std::find((*it)->GetIPs().begin(), + (*it)->GetIPs().end(), + ip)); + } + } +} + +// Test that merge correctly distinguishes multiple networks on an interface. +TEST_F(NetworkTest, TestMultiplePublicNetworksOnOneInterfaceMerge) { + BasicNetworkManager manager; + manager.SignalNetworksChanged.connect( + static_cast(this), &NetworkTest::OnNetworksChanged); + NetworkManager::NetworkList original_list; + SetupNetworks(&original_list); + bool changed = false; + MergeNetworkList(manager, original_list, &changed); + EXPECT_TRUE(changed); + IPAddress ip; + IPAddress prefix; + // A second network for eth0. + EXPECT_TRUE(IPFromString("2400:4030:1:2c00:be30:5bff:fee5:c3", &ip)); + prefix = TruncateIP(ip, 64); + Network ipv6_eth0_publicnetwork2_ip1("test_eth0", "Test NetworkAdapter 1", + prefix, 64); + ipv6_eth0_publicnetwork2_ip1.AddIP(ip); + original_list.push_back(new Network(ipv6_eth0_publicnetwork2_ip1)); + changed = false; + MergeNetworkList(manager, original_list, &changed); + EXPECT_TRUE(changed); + // There should be five networks now. + NetworkManager::NetworkList list; + manager.GetNetworks(&list); + EXPECT_EQ(5U, list.size()); + // Check the resulting addresses. + for (NetworkManager::NetworkList::iterator it = list.begin(); + it != list.end(); ++it) { + if ((*it)->prefix() == ipv6_eth0_publicnetwork2_ip1.prefix() && + (*it)->name() == ipv6_eth0_publicnetwork2_ip1.name()) { + // Check the new network has 1 IP and that it's the correct one. + EXPECT_EQ(1U, (*it)->GetIPs().size()); + EXPECT_EQ(ip, (*it)->GetIPs().at(0)); + } else { + // Check the IP didn't get added anywhere it wasn't supposed to. + EXPECT_EQ((*it)->GetIPs().end(), + std::find((*it)->GetIPs().begin(), + (*it)->GetIPs().end(), + ip)); + } + } +} + +// Test that DumpNetworks works. +TEST_F(NetworkTest, TestDumpNetworks) { + BasicNetworkManager manager; + manager.DumpNetworks(true); +} + +// Test that we can toggle IPv6 on and off. +TEST_F(NetworkTest, TestIPv6Toggle) { + BasicNetworkManager manager; + bool ipv6_found = false; + NetworkManager::NetworkList list; +#ifndef WIN32 + // There should be at least one IPv6 network (fe80::/64 should be in there). + // TODO: Disabling this test on windows for the moment as the test + // machines don't seem to have IPv6 installed on them at all. + manager.set_ipv6_enabled(true); + list = GetNetworks(manager, true); + for (NetworkManager::NetworkList::iterator it = list.begin(); + it != list.end(); ++it) { + if ((*it)->prefix().family() == AF_INET6) { + ipv6_found = true; + break; + } + } + EXPECT_TRUE(ipv6_found); +#endif + ipv6_found = false; + manager.set_ipv6_enabled(false); + list = GetNetworks(manager, true); + for (NetworkManager::NetworkList::iterator it = list.begin(); + it != list.end(); ++it) { + if ((*it)->prefix().family() == AF_INET6) { + ipv6_found = true; + break; + } + } + EXPECT_FALSE(ipv6_found); +} + +#if defined(POSIX) +// Verify that we correctly handle interfaces with no address. +TEST_F(NetworkTest, TestConvertIfAddrsNoAddress) { + ifaddrs list; + memset(&list, 0, sizeof(list)); + list.ifa_name = const_cast("test_iface"); + + NetworkManager::NetworkList result; + BasicNetworkManager manager; + CallConvertIfAddrs(manager, &list, true, &result); + EXPECT_TRUE(result.empty()); +} +#endif // defined(POSIX) + + +} // namespace talk_base diff --git a/talk/base/nssidentity.cc b/talk/base/nssidentity.cc new file mode 100644 index 000000000..b58788880 --- /dev/null +++ b/talk/base/nssidentity.cc @@ -0,0 +1,428 @@ +/* + * libjingle + * Copyright 2012, Google Inc. + * Copyright 2012, RTFM, 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. + */ + +#if HAVE_CONFIG_H +#include "config.h" +#endif // HAVE_CONFIG_H + +#if HAVE_NSS_SSL_H + +#include "talk/base/nssidentity.h" + +#include + +#include "cert.h" +#include "cryptohi.h" +#include "keyhi.h" +#include "nss.h" +#include "pk11pub.h" +#include "sechash.h" + +#include "talk/base/base64.h" +#include "talk/base/logging.h" +#include "talk/base/helpers.h" +#include "talk/base/nssstreamadapter.h" + +namespace talk_base { + +// Helper function to parse PEM-encoded DER. +static bool PemToDer(const std::string& pem_type, + const std::string& pem_string, + std::string* der) { + // Find the inner body. We need this to fulfill the contract of + // returning pem_length. + size_t header = pem_string.find("-----BEGIN " + pem_type + "-----"); + if (header == std::string::npos) + return false; + + size_t body = pem_string.find("\n", header); + if (body == std::string::npos) + return false; + + size_t trailer = pem_string.find("-----END " + pem_type + "-----"); + if (trailer == std::string::npos) + return false; + + std::string inner = pem_string.substr(body + 1, trailer - (body + 1)); + + *der = Base64::Decode(inner, Base64::DO_PARSE_WHITE | + Base64::DO_PAD_ANY | + Base64::DO_TERM_BUFFER); + return true; +} + +static std::string DerToPem(const std::string& pem_type, + const unsigned char *data, + size_t length) { + std::stringstream result; + + result << "-----BEGIN " << pem_type << "-----\n"; + + std::string tmp; + Base64::EncodeFromArray(data, length, &tmp); + result << tmp; + + result << "-----END " << pem_type << "-----\n"; + + return result.str(); +} + +NSSKeyPair::~NSSKeyPair() { + if (privkey_) + SECKEY_DestroyPrivateKey(privkey_); + if (pubkey_) + SECKEY_DestroyPublicKey(pubkey_); +} + +NSSKeyPair *NSSKeyPair::Generate() { + SECKEYPrivateKey *privkey = NULL; + SECKEYPublicKey *pubkey = NULL; + PK11RSAGenParams rsaparams; + rsaparams.keySizeInBits = 1024; + rsaparams.pe = 0x010001; // 65537 -- a common RSA public exponent. + + privkey = PK11_GenerateKeyPair(NSSContext::GetSlot(), + CKM_RSA_PKCS_KEY_PAIR_GEN, + &rsaparams, &pubkey, PR_FALSE /*permanent*/, + PR_FALSE /*sensitive*/, NULL); + if (!privkey) { + LOG(LS_ERROR) << "Couldn't generate key pair"; + return NULL; + } + + return new NSSKeyPair(privkey, pubkey); +} + +// Just make a copy. +NSSKeyPair *NSSKeyPair::GetReference() { + SECKEYPrivateKey *privkey = SECKEY_CopyPrivateKey(privkey_); + if (!privkey) + return NULL; + + SECKEYPublicKey *pubkey = SECKEY_CopyPublicKey(pubkey_); + if (!pubkey) { + SECKEY_DestroyPrivateKey(privkey); + return NULL; + } + + return new NSSKeyPair(privkey, pubkey); +} + +NSSCertificate *NSSCertificate::FromPEMString(const std::string &pem_string) { + std::string der; + if (!PemToDer("CERTIFICATE", pem_string, &der)) + return NULL; + + SECItem der_cert; + der_cert.data = reinterpret_cast(const_cast( + der.data())); + der_cert.len = der.size(); + CERTCertificate *cert = CERT_NewTempCertificate(CERT_GetDefaultCertDB(), + &der_cert, NULL, PR_FALSE, PR_TRUE); + + if (!cert) + return NULL; + + return new NSSCertificate(cert); +} + +NSSCertificate *NSSCertificate::GetReference() const { + CERTCertificate *certificate = CERT_DupCertificate(certificate_); + if (!certificate) + return NULL; + + return new NSSCertificate(certificate); +} + +std::string NSSCertificate::ToPEMString() const { + return DerToPem("CERTIFICATE", + certificate_->derCert.data, + certificate_->derCert.len); +} + +bool NSSCertificate::GetDigestLength(const std::string &algorithm, + std::size_t *length) { + const SECHashObject *ho; + + if (!GetDigestObject(algorithm, &ho)) + return false; + + *length = ho->length; + + return true; +} + +bool NSSCertificate::ComputeDigest(const std::string &algorithm, + unsigned char *digest, std::size_t size, + std::size_t *length) const { + const SECHashObject *ho; + + if (!GetDigestObject(algorithm, &ho)) + return false; + + if (size < ho->length) // Sanity check for fit + return false; + + SECStatus rv = HASH_HashBuf(ho->type, digest, + certificate_->derCert.data, + certificate_->derCert.len); + if (rv != SECSuccess) + return false; + + *length = ho->length; + + return true; +} + +bool NSSCertificate::Equals(const NSSCertificate *tocompare) const { + if (!certificate_->derCert.len) + return false; + if (!tocompare->certificate_->derCert.len) + return false; + + if (certificate_->derCert.len != tocompare->certificate_->derCert.len) + return false; + + return memcmp(certificate_->derCert.data, + tocompare->certificate_->derCert.data, + certificate_->derCert.len) == 0; +} + + +bool NSSCertificate::GetDigestObject(const std::string &algorithm, + const SECHashObject **hop) { + const SECHashObject *ho; + HASH_HashType hash_type; + + if (algorithm == DIGEST_SHA_1) { + hash_type = HASH_AlgSHA1; + // HASH_AlgSHA224 is not supported in the chromium linux build system. +#if 0 + } else if (algorithm == DIGEST_SHA_224) { + hash_type = HASH_AlgSHA224; +#endif + } else if (algorithm == DIGEST_SHA_256) { + hash_type = HASH_AlgSHA256; + } else if (algorithm == DIGEST_SHA_384) { + hash_type = HASH_AlgSHA384; + } else if (algorithm == DIGEST_SHA_512) { + hash_type = HASH_AlgSHA512; + } else { + return false; + } + + ho = HASH_GetHashObject(hash_type); + + ASSERT(ho->length >= 20); // Can't happen + *hop = ho; + + return true; +} + + +NSSIdentity *NSSIdentity::Generate(const std::string &common_name) { + std::string subject_name_string = "CN=" + common_name; + CERTName *subject_name = CERT_AsciiToName( + const_cast(subject_name_string.c_str())); + NSSIdentity *identity = NULL; + CERTSubjectPublicKeyInfo *spki = NULL; + CERTCertificateRequest *certreq = NULL; + CERTValidity *validity; + CERTCertificate *certificate = NULL; + NSSKeyPair *keypair = NSSKeyPair::Generate(); + SECItem inner_der; + SECStatus rv; + PLArenaPool* arena; + SECItem signed_cert; + PRTime not_before, not_after; + PRTime now = PR_Now(); + PRTime one_day; + + inner_der.len = 0; + inner_der.data = NULL; + + if (!keypair) { + LOG(LS_ERROR) << "Couldn't generate key pair"; + goto fail; + } + + if (!subject_name) { + LOG(LS_ERROR) << "Couldn't convert subject name " << subject_name; + goto fail; + } + + spki = SECKEY_CreateSubjectPublicKeyInfo(keypair->pubkey()); + if (!spki) { + LOG(LS_ERROR) << "Couldn't create SPKI"; + goto fail; + } + + certreq = CERT_CreateCertificateRequest(subject_name, spki, NULL); + if (!certreq) { + LOG(LS_ERROR) << "Couldn't create certificate signing request"; + goto fail; + } + + one_day = 86400; + one_day *= PR_USEC_PER_SEC; + not_before = now - one_day; + not_after = now + 30 * one_day; + + validity = CERT_CreateValidity(not_before, not_after); + if (!validity) { + LOG(LS_ERROR) << "Couldn't create validity"; + goto fail; + } + + unsigned long serial; + // Note: This serial in principle could collide, but it's unlikely + rv = PK11_GenerateRandom(reinterpret_cast(&serial), + sizeof(serial)); + if (rv != SECSuccess) { + LOG(LS_ERROR) << "Couldn't generate random serial"; + goto fail; + } + + certificate = CERT_CreateCertificate(serial, subject_name, validity, certreq); + if (!certificate) { + LOG(LS_ERROR) << "Couldn't create certificate"; + goto fail; + } + + arena = certificate->arena; + + rv = SECOID_SetAlgorithmID(arena, &certificate->signature, + SEC_OID_PKCS1_SHA1_WITH_RSA_ENCRYPTION, NULL); + if (rv != SECSuccess) + goto fail; + + // Set version to X509v3. + *(certificate->version.data) = 2; + certificate->version.len = 1; + + if (!SEC_ASN1EncodeItem(arena, &inner_der, certificate, + SEC_ASN1_GET(CERT_CertificateTemplate))) + goto fail; + + rv = SEC_DerSignData(arena, &signed_cert, inner_der.data, inner_der.len, + keypair->privkey(), + SEC_OID_PKCS1_SHA1_WITH_RSA_ENCRYPTION); + if (rv != SECSuccess) { + LOG(LS_ERROR) << "Couldn't sign certificate"; + goto fail; + } + certificate->derCert = signed_cert; + + identity = new NSSIdentity(keypair, new NSSCertificate(certificate)); + + goto done; + + fail: + delete keypair; + CERT_DestroyCertificate(certificate); + + done: + if (subject_name) CERT_DestroyName(subject_name); + if (spki) SECKEY_DestroySubjectPublicKeyInfo(spki); + if (certreq) CERT_DestroyCertificateRequest(certreq); + if (validity) CERT_DestroyValidity(validity); + return identity; +} + +SSLIdentity* NSSIdentity::FromPEMStrings(const std::string& private_key, + const std::string& certificate) { + std::string private_key_der; + if (!PemToDer( + "RSA PRIVATE KEY", private_key, &private_key_der)) + return NULL; + + SECItem private_key_item; + private_key_item.data = + reinterpret_cast( + const_cast(private_key_der.c_str())); + private_key_item.len = private_key_der.size(); + + const unsigned int key_usage = KU_KEY_ENCIPHERMENT | KU_DATA_ENCIPHERMENT | + KU_DIGITAL_SIGNATURE; + + SECKEYPrivateKey* privkey = NULL; + SECStatus rv = + PK11_ImportDERPrivateKeyInfoAndReturnKey(NSSContext::GetSlot(), + &private_key_item, + NULL, NULL, PR_FALSE, PR_FALSE, + key_usage, &privkey, NULL); + if (rv != SECSuccess) { + LOG(LS_ERROR) << "Couldn't import private key"; + return NULL; + } + + SECKEYPublicKey *pubkey = SECKEY_ConvertToPublicKey(privkey); + if (rv != SECSuccess) { + SECKEY_DestroyPrivateKey(privkey); + LOG(LS_ERROR) << "Couldn't convert private key to public key"; + return NULL; + } + + // Assign to a scoped_ptr so we don't leak on error. + scoped_ptr keypair(new NSSKeyPair(privkey, pubkey)); + + scoped_ptr cert(NSSCertificate::FromPEMString(certificate)); + if (!cert) { + LOG(LS_ERROR) << "Couldn't parse certificate"; + return NULL; + } + + // TODO(ekr@rtfm.com): Check the public key against the certificate. + + return new NSSIdentity(keypair.release(), cert.release()); +} + +NSSIdentity *NSSIdentity::GetReference() const { + NSSKeyPair *keypair = keypair_->GetReference(); + if (!keypair) + return NULL; + + NSSCertificate *certificate = certificate_->GetReference(); + if (!certificate) { + delete keypair; + return NULL; + } + + return new NSSIdentity(keypair, certificate); +} + + +NSSCertificate &NSSIdentity::certificate() const { + return *certificate_; +} + + +} // talk_base namespace + +#endif // HAVE_NSS_SSL_H + diff --git a/talk/base/nssidentity.h b/talk/base/nssidentity.h new file mode 100644 index 000000000..725c54627 --- /dev/null +++ b/talk/base/nssidentity.h @@ -0,0 +1,128 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +#ifndef TALK_BASE_NSSIDENTITY_H_ +#define TALK_BASE_NSSIDENTITY_H_ + +#include + +#include "cert.h" +#include "nspr.h" +#include "hasht.h" +#include "keythi.h" + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sslidentity.h" + +namespace talk_base { + +class NSSKeyPair { + public: + NSSKeyPair(SECKEYPrivateKey* privkey, SECKEYPublicKey* pubkey) : + privkey_(privkey), pubkey_(pubkey) {} + ~NSSKeyPair(); + + // Generate a 1024-bit RSA key pair. + static NSSKeyPair* Generate(); + NSSKeyPair* GetReference(); + + SECKEYPrivateKey* privkey() const { return privkey_; } + SECKEYPublicKey * pubkey() const { return pubkey_; } + + private: + SECKEYPrivateKey* privkey_; + SECKEYPublicKey* pubkey_; + + DISALLOW_EVIL_CONSTRUCTORS(NSSKeyPair); +}; + + +class NSSCertificate : public SSLCertificate { + public: + static NSSCertificate* FromPEMString(const std::string& pem_string); + explicit NSSCertificate(CERTCertificate* cert) : certificate_(cert) {} + virtual ~NSSCertificate() { + if (certificate_) + CERT_DestroyCertificate(certificate_); + } + + virtual NSSCertificate* GetReference() const; + + virtual std::string ToPEMString() const; + + virtual bool ComputeDigest(const std::string& algorithm, + unsigned char* digest, std::size_t size, + std::size_t* length) const; + + CERTCertificate* certificate() { return certificate_; } + + // Helper function to get the length of a digest + static bool GetDigestLength(const std::string& algorithm, + std::size_t* length); + + // Comparison + bool Equals(const NSSCertificate* tocompare) const; + + private: + static bool GetDigestObject(const std::string& algorithm, + const SECHashObject** hash_object); + + CERTCertificate* certificate_; + + DISALLOW_EVIL_CONSTRUCTORS(NSSCertificate); +}; + +// Represents a SSL key pair and certificate for NSS. +class NSSIdentity : public SSLIdentity { + public: + static NSSIdentity* Generate(const std::string& common_name); + static SSLIdentity* FromPEMStrings(const std::string& private_key, + const std::string& certificate); + virtual ~NSSIdentity() { + LOG(LS_INFO) << "Destroying NSS identity"; + } + + virtual NSSIdentity* GetReference() const; + virtual NSSCertificate& certificate() const; + + NSSKeyPair* keypair() const { return keypair_.get(); } + + private: + NSSIdentity(NSSKeyPair* keypair, NSSCertificate* cert) : + keypair_(keypair), certificate_(cert) {} + + talk_base::scoped_ptr keypair_; + talk_base::scoped_ptr certificate_; + + DISALLOW_EVIL_CONSTRUCTORS(NSSIdentity); +}; + +} // namespace talk_base + +#endif // TALK_BASE_NSSIDENTITY_H_ diff --git a/talk/base/nssstreamadapter.cc b/talk/base/nssstreamadapter.cc new file mode 100644 index 000000000..c9a540d52 --- /dev/null +++ b/talk/base/nssstreamadapter.cc @@ -0,0 +1,1007 @@ +/* + * libjingle + * Copyright 2004--2008, Google Inc. + * Copyright 2004--2011, RTFM, 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 + +#if HAVE_CONFIG_H +#include "config.h" +#endif // HAVE_CONFIG_H + +#if HAVE_NSS_SSL_H + +#include "talk/base/nssstreamadapter.h" + +#include "keyhi.h" +#include "nspr.h" +#include "nss.h" +#include "pk11pub.h" +#include "secerr.h" + +#ifdef NSS_SSL_RELATIVE_PATH +#include "ssl.h" +#include "sslerr.h" +#include "sslproto.h" +#else +#include "net/third_party/nss/ssl/ssl.h" +#include "net/third_party/nss/ssl/sslerr.h" +#include "net/third_party/nss/ssl/sslproto.h" +#endif + +#include "talk/base/nssidentity.h" +#include "talk/base/thread.h" + +namespace talk_base { + +PRDescIdentity NSSStreamAdapter::nspr_layer_identity = PR_INVALID_IO_LAYER; + +#define UNIMPLEMENTED \ + PR_SetError(PR_NOT_IMPLEMENTED_ERROR, 0); \ + LOG(LS_ERROR) \ + << "Call to unimplemented function "<< __FUNCTION__; ASSERT(false) + +#ifdef SRTP_AES128_CM_HMAC_SHA1_80 +#define HAVE_DTLS_SRTP +#endif + +#ifdef HAVE_DTLS_SRTP +// SRTP cipher suite table +struct SrtpCipherMapEntry { + const char* external_name; + PRUint16 cipher_id; +}; + +// This isn't elegant, but it's better than an external reference +static const SrtpCipherMapEntry kSrtpCipherMap[] = { + {"AES_CM_128_HMAC_SHA1_80", SRTP_AES128_CM_HMAC_SHA1_80 }, + {"AES_CM_128_HMAC_SHA1_32", SRTP_AES128_CM_HMAC_SHA1_32 }, + {NULL, 0} +}; +#endif + + +// Implementation of NSPR methods +static PRStatus StreamClose(PRFileDesc *socket) { + // Noop + return PR_SUCCESS; +} + +static PRInt32 StreamRead(PRFileDesc *socket, void *buf, PRInt32 length) { + StreamInterface *stream = reinterpret_cast(socket->secret); + size_t read; + int error; + StreamResult result = stream->Read(buf, length, &read, &error); + if (result == SR_SUCCESS) { + return read; + } + + if (result == SR_EOS) { + return 0; + } + + if (result == SR_BLOCK) { + PR_SetError(PR_WOULD_BLOCK_ERROR, 0); + return -1; + } + + PR_SetError(PR_UNKNOWN_ERROR, error); + return -1; +} + +static PRInt32 StreamWrite(PRFileDesc *socket, const void *buf, + PRInt32 length) { + StreamInterface *stream = reinterpret_cast(socket->secret); + size_t written; + int error; + StreamResult result = stream->Write(buf, length, &written, &error); + if (result == SR_SUCCESS) { + return written; + } + + if (result == SR_BLOCK) { + LOG(LS_INFO) << + "NSSStreamAdapter: write to underlying transport would block"; + PR_SetError(PR_WOULD_BLOCK_ERROR, 0); + return -1; + } + + LOG(LS_ERROR) << "Write error"; + PR_SetError(PR_UNKNOWN_ERROR, error); + return -1; +} + +static PRInt32 StreamAvailable(PRFileDesc *socket) { + UNIMPLEMENTED; + return -1; +} + +PRInt64 StreamAvailable64(PRFileDesc *socket) { + UNIMPLEMENTED; + return -1; +} + +static PRStatus StreamSync(PRFileDesc *socket) { + UNIMPLEMENTED; + return PR_FAILURE; +} + +static PROffset32 StreamSeek(PRFileDesc *socket, PROffset32 offset, + PRSeekWhence how) { + UNIMPLEMENTED; + return -1; +} + +static PROffset64 StreamSeek64(PRFileDesc *socket, PROffset64 offset, + PRSeekWhence how) { + UNIMPLEMENTED; + return -1; +} + +static PRStatus StreamFileInfo(PRFileDesc *socket, PRFileInfo *info) { + UNIMPLEMENTED; + return PR_FAILURE; +} + +static PRStatus StreamFileInfo64(PRFileDesc *socket, PRFileInfo64 *info) { + UNIMPLEMENTED; + return PR_FAILURE; +} + +static PRInt32 StreamWritev(PRFileDesc *socket, const PRIOVec *iov, + PRInt32 iov_size, PRIntervalTime timeout) { + UNIMPLEMENTED; + return -1; +} + +static PRStatus StreamConnect(PRFileDesc *socket, const PRNetAddr *addr, + PRIntervalTime timeout) { + UNIMPLEMENTED; + return PR_FAILURE; +} + +static PRFileDesc *StreamAccept(PRFileDesc *sd, PRNetAddr *addr, + PRIntervalTime timeout) { + UNIMPLEMENTED; + return NULL; +} + +static PRStatus StreamBind(PRFileDesc *socket, const PRNetAddr *addr) { + UNIMPLEMENTED; + return PR_FAILURE; +} + +static PRStatus StreamListen(PRFileDesc *socket, PRIntn depth) { + UNIMPLEMENTED; + return PR_FAILURE; +} + +static PRStatus StreamShutdown(PRFileDesc *socket, PRIntn how) { + UNIMPLEMENTED; + return PR_FAILURE; +} + +// Note: this is always nonblocking and ignores the timeout. +// TODO(ekr@rtfm.com): In future verify that the socket is +// actually in non-blocking mode. +// This function does not support peek. +static PRInt32 StreamRecv(PRFileDesc *socket, void *buf, PRInt32 amount, + PRIntn flags, PRIntervalTime to) { + ASSERT(flags == 0); + + if (flags != 0) { + PR_SetError(PR_NOT_IMPLEMENTED_ERROR, 0); + return -1; + } + + return StreamRead(socket, buf, amount); +} + +// Note: this is always nonblocking and assumes a zero timeout. +// This function does not support peek. +static PRInt32 StreamSend(PRFileDesc *socket, const void *buf, + PRInt32 amount, PRIntn flags, + PRIntervalTime to) { + ASSERT(flags == 0); + + return StreamWrite(socket, buf, amount); +} + +static PRInt32 StreamRecvfrom(PRFileDesc *socket, void *buf, + PRInt32 amount, PRIntn flags, + PRNetAddr *addr, PRIntervalTime to) { + UNIMPLEMENTED; + return -1; +} + +static PRInt32 StreamSendto(PRFileDesc *socket, const void *buf, + PRInt32 amount, PRIntn flags, + const PRNetAddr *addr, PRIntervalTime to) { + UNIMPLEMENTED; + return -1; +} + +static PRInt16 StreamPoll(PRFileDesc *socket, PRInt16 in_flags, + PRInt16 *out_flags) { + UNIMPLEMENTED; + return -1; +} + +static PRInt32 StreamAcceptRead(PRFileDesc *sd, PRFileDesc **nd, + PRNetAddr **raddr, + void *buf, PRInt32 amount, PRIntervalTime t) { + UNIMPLEMENTED; + return -1; +} + +static PRInt32 StreamTransmitFile(PRFileDesc *sd, PRFileDesc *socket, + const void *headers, PRInt32 hlen, + PRTransmitFileFlags flags, PRIntervalTime t) { + UNIMPLEMENTED; + return -1; +} + +static PRStatus StreamGetPeerName(PRFileDesc *socket, PRNetAddr *addr) { + // TODO(ekr@rtfm.com): Modify to return unique names for each channel + // somehow, as opposed to always the same static address. The current + // implementation messes up the session cache, which is why it's off + // elsewhere + addr->inet.family = PR_AF_INET; + addr->inet.port = 0; + addr->inet.ip = 0; + + return PR_SUCCESS; +} + +static PRStatus StreamGetSockName(PRFileDesc *socket, PRNetAddr *addr) { + UNIMPLEMENTED; + return PR_FAILURE; +} + +static PRStatus StreamGetSockOption(PRFileDesc *socket, PRSocketOptionData *opt) { + switch (opt->option) { + case PR_SockOpt_Nonblocking: + opt->value.non_blocking = PR_TRUE; + return PR_SUCCESS; + default: + UNIMPLEMENTED; + break; + } + + return PR_FAILURE; +} + +// Imitate setting socket options. These are mostly noops. +static PRStatus StreamSetSockOption(PRFileDesc *socket, + const PRSocketOptionData *opt) { + switch (opt->option) { + case PR_SockOpt_Nonblocking: + return PR_SUCCESS; + case PR_SockOpt_NoDelay: + return PR_SUCCESS; + default: + UNIMPLEMENTED; + break; + } + + return PR_FAILURE; +} + +static PRInt32 StreamSendfile(PRFileDesc *out, PRSendFileData *in, + PRTransmitFileFlags flags, PRIntervalTime to) { + UNIMPLEMENTED; + return -1; +} + +static PRStatus StreamConnectContinue(PRFileDesc *socket, PRInt16 flags) { + UNIMPLEMENTED; + return PR_FAILURE; +} + +static PRIntn StreamReserved(PRFileDesc *socket) { + UNIMPLEMENTED; + return -1; +} + +static const struct PRIOMethods nss_methods = { + PR_DESC_LAYERED, + StreamClose, + StreamRead, + StreamWrite, + StreamAvailable, + StreamAvailable64, + StreamSync, + StreamSeek, + StreamSeek64, + StreamFileInfo, + StreamFileInfo64, + StreamWritev, + StreamConnect, + StreamAccept, + StreamBind, + StreamListen, + StreamShutdown, + StreamRecv, + StreamSend, + StreamRecvfrom, + StreamSendto, + StreamPoll, + StreamAcceptRead, + StreamTransmitFile, + StreamGetSockName, + StreamGetPeerName, + StreamReserved, + StreamReserved, + StreamGetSockOption, + StreamSetSockOption, + StreamSendfile, + StreamConnectContinue, + StreamReserved, + StreamReserved, + StreamReserved, + StreamReserved +}; + +NSSStreamAdapter::NSSStreamAdapter(StreamInterface *stream) + : SSLStreamAdapterHelper(stream), + ssl_fd_(NULL), + cert_ok_(false) { +} + +bool NSSStreamAdapter::Init() { + if (nspr_layer_identity == PR_INVALID_IO_LAYER) { + nspr_layer_identity = PR_GetUniqueIdentity("nssstreamadapter"); + } + PRFileDesc *pr_fd = PR_CreateIOLayerStub(nspr_layer_identity, &nss_methods); + if (!pr_fd) + return false; + pr_fd->secret = reinterpret_cast(stream()); + + PRFileDesc *ssl_fd; + if (ssl_mode_ == SSL_MODE_DTLS) { + ssl_fd = DTLS_ImportFD(NULL, pr_fd); + } else { + ssl_fd = SSL_ImportFD(NULL, pr_fd); + } + ASSERT(ssl_fd != NULL); // This should never happen + if (!ssl_fd) { + PR_Close(pr_fd); + return false; + } + + SECStatus rv; + // Turn on security. + rv = SSL_OptionSet(ssl_fd, SSL_SECURITY, PR_TRUE); + if (rv != SECSuccess) { + LOG(LS_ERROR) << "Error enabling security on SSL Socket"; + return false; + } + + // Disable SSLv2. + rv = SSL_OptionSet(ssl_fd, SSL_ENABLE_SSL2, PR_FALSE); + if (rv != SECSuccess) { + LOG(LS_ERROR) << "Error disabling SSL2"; + return false; + } + + // Disable caching. + // TODO(ekr@rtfm.com): restore this when I have the caching + // identity set. + rv = SSL_OptionSet(ssl_fd, SSL_NO_CACHE, PR_TRUE); + if (rv != SECSuccess) { + LOG(LS_ERROR) << "Error disabling cache"; + return false; + } + + // Disable session tickets. + rv = SSL_OptionSet(ssl_fd, SSL_ENABLE_SESSION_TICKETS, PR_FALSE); + if (rv != SECSuccess) { + LOG(LS_ERROR) << "Error enabling tickets"; + return false; + } + + // Disable renegotiation. + rv = SSL_OptionSet(ssl_fd, SSL_ENABLE_RENEGOTIATION, + SSL_RENEGOTIATE_NEVER); + if (rv != SECSuccess) { + LOG(LS_ERROR) << "Error disabling renegotiation"; + return false; + } + + // Disable false start. + rv = SSL_OptionSet(ssl_fd, SSL_ENABLE_FALSE_START, PR_FALSE); + if (rv != SECSuccess) { + LOG(LS_ERROR) << "Error disabling false start"; + return false; + } + + ssl_fd_ = ssl_fd; + + return true; +} + +NSSStreamAdapter::~NSSStreamAdapter() { + if (ssl_fd_) + PR_Close(ssl_fd_); +}; + + +int NSSStreamAdapter::BeginSSL() { + SECStatus rv; + + if (!Init()) { + Error("Init", -1, false); + return -1; + } + + ASSERT(state_ == SSL_CONNECTING); + // The underlying stream has been opened. If we are in peer-to-peer mode + // then a peer certificate must have been specified by now. + ASSERT(!ssl_server_name_.empty() || + peer_certificate_.get() != NULL || + !peer_certificate_digest_algorithm_.empty()); + LOG(LS_INFO) << "BeginSSL: " + << (!ssl_server_name_.empty() ? ssl_server_name_ : + "with peer"); + + if (role_ == SSL_CLIENT) { + LOG(LS_INFO) << "BeginSSL: as client"; + + rv = SSL_GetClientAuthDataHook(ssl_fd_, GetClientAuthDataHook, + this); + if (rv != SECSuccess) { + Error("BeginSSL", -1, false); + return -1; + } + } else { + LOG(LS_INFO) << "BeginSSL: as server"; + NSSIdentity *identity; + + if (identity_.get()) { + identity = static_cast(identity_.get()); + } else { + LOG(LS_ERROR) << "Can't be an SSL server without an identity"; + Error("BeginSSL", -1, false); + return -1; + } + rv = SSL_ConfigSecureServer(ssl_fd_, identity->certificate().certificate(), + identity->keypair()->privkey(), + kt_rsa); + if (rv != SECSuccess) { + Error("BeginSSL", -1, false); + return -1; + } + + // Insist on a certificate from the client + rv = SSL_OptionSet(ssl_fd_, SSL_REQUEST_CERTIFICATE, PR_TRUE); + if (rv != SECSuccess) { + Error("BeginSSL", -1, false); + return -1; + } + + rv = SSL_OptionSet(ssl_fd_, SSL_REQUIRE_CERTIFICATE, PR_TRUE); + if (rv != SECSuccess) { + Error("BeginSSL", -1, false); + return -1; + } + } + + // Set the version range. + SSLVersionRange vrange; + vrange.min = (ssl_mode_ == SSL_MODE_DTLS) ? + SSL_LIBRARY_VERSION_TLS_1_1 : + SSL_LIBRARY_VERSION_TLS_1_0; + vrange.max = SSL_LIBRARY_VERSION_TLS_1_1; + + rv = SSL_VersionRangeSet(ssl_fd_, &vrange); + if (rv != SECSuccess) { + Error("BeginSSL", -1, false); + return -1; + } + + // SRTP +#ifdef HAVE_DTLS_SRTP + if (!srtp_ciphers_.empty()) { + rv = SSL_SetSRTPCiphers(ssl_fd_, &srtp_ciphers_[0], srtp_ciphers_.size()); + if (rv != SECSuccess) { + Error("BeginSSL", -1, false); + return -1; + } + } +#endif + + // Certificate validation + rv = SSL_AuthCertificateHook(ssl_fd_, AuthCertificateHook, this); + if (rv != SECSuccess) { + Error("BeginSSL", -1, false); + return -1; + } + + // Now start the handshake + rv = SSL_ResetHandshake(ssl_fd_, role_ == SSL_SERVER ? PR_TRUE : PR_FALSE); + if (rv != SECSuccess) { + Error("BeginSSL", -1, false); + return -1; + } + + return ContinueSSL(); +} + +int NSSStreamAdapter::ContinueSSL() { + LOG(LS_INFO) << "ContinueSSL"; + ASSERT(state_ == SSL_CONNECTING); + + // Clear the DTLS timer + Thread::Current()->Clear(this, MSG_DTLS_TIMEOUT); + + SECStatus rv = SSL_ForceHandshake(ssl_fd_); + + if (rv == SECSuccess) { + LOG(LS_INFO) << "Handshake complete"; + + ASSERT(cert_ok_); + if (!cert_ok_) { + Error("ContinueSSL", -1, true); + return -1; + } + + state_ = SSL_CONNECTED; + StreamAdapterInterface::OnEvent(stream(), SE_OPEN|SE_READ|SE_WRITE, 0); + return 0; + } + + PRInt32 err = PR_GetError(); + switch (err) { + case SSL_ERROR_RX_MALFORMED_HANDSHAKE: + if (ssl_mode_ != SSL_MODE_DTLS) { + Error("ContinueSSL", -1, true); + return -1; + } else { + LOG(LS_INFO) << "Malformed DTLS message. Ignoring."; + // Fall through + } + case PR_WOULD_BLOCK_ERROR: + LOG(LS_INFO) << "Would have blocked"; + if (ssl_mode_ == SSL_MODE_DTLS) { + PRIntervalTime timeout; + + SECStatus rv = DTLS_GetHandshakeTimeout(ssl_fd_, &timeout); + if (rv == SECSuccess) { + LOG(LS_INFO) << "Timeout is " << timeout << " ms"; + Thread::Current()->PostDelayed(PR_IntervalToMilliseconds(timeout), + this, MSG_DTLS_TIMEOUT, 0); + } + } + + return 0; + default: + LOG(LS_INFO) << "Error " << err; + break; + } + + Error("ContinueSSL", -1, true); + return -1; +} + +void NSSStreamAdapter::Cleanup() { + if (state_ != SSL_ERROR) { + state_ = SSL_CLOSED; + } + + if (ssl_fd_) { + PR_Close(ssl_fd_); + ssl_fd_ = NULL; + } + + identity_.reset(); + peer_certificate_.reset(); + + Thread::Current()->Clear(this, MSG_DTLS_TIMEOUT); +} + +StreamResult NSSStreamAdapter::Read(void* data, size_t data_len, + size_t* read, int* error) { + // SSL_CONNECTED sanity check. + switch (state_) { + case SSL_NONE: + case SSL_WAIT: + case SSL_CONNECTING: + return SR_BLOCK; + + case SSL_CONNECTED: + break; + + case SSL_CLOSED: + return SR_EOS; + + case SSL_ERROR: + default: + if (error) + *error = ssl_error_code_; + return SR_ERROR; + } + + PRInt32 rv = PR_Read(ssl_fd_, data, data_len); + + if (rv == 0) { + return SR_EOS; + } + + // Error + if (rv < 0) { + PRInt32 err = PR_GetError(); + + switch (err) { + case PR_WOULD_BLOCK_ERROR: + return SR_BLOCK; + default: + Error("Read", -1, false); + *error = err; // libjingle semantics are that this is impl-specific + return SR_ERROR; + } + } + + // Success + *read = rv; + + return SR_SUCCESS; +} + +StreamResult NSSStreamAdapter::Write(const void* data, size_t data_len, + size_t* written, int* error) { + // SSL_CONNECTED sanity check. + switch (state_) { + case SSL_NONE: + case SSL_WAIT: + case SSL_CONNECTING: + return SR_BLOCK; + + case SSL_CONNECTED: + break; + + case SSL_ERROR: + case SSL_CLOSED: + default: + if (error) + *error = ssl_error_code_; + return SR_ERROR; + } + + PRInt32 rv = PR_Write(ssl_fd_, data, data_len); + + // Error + if (rv < 0) { + PRInt32 err = PR_GetError(); + + switch (err) { + case PR_WOULD_BLOCK_ERROR: + return SR_BLOCK; + default: + Error("Write", -1, false); + *error = err; // libjingle semantics are that this is impl-specific + return SR_ERROR; + } + } + + // Success + *written = rv; + + return SR_SUCCESS; +} + +void NSSStreamAdapter::OnEvent(StreamInterface* stream, int events, + int err) { + int events_to_signal = 0; + int signal_error = 0; + ASSERT(stream == this->stream()); + if ((events & SE_OPEN)) { + LOG(LS_INFO) << "NSSStreamAdapter::OnEvent SE_OPEN"; + if (state_ != SSL_WAIT) { + ASSERT(state_ == SSL_NONE); + events_to_signal |= SE_OPEN; + } else { + state_ = SSL_CONNECTING; + if (int err = BeginSSL()) { + Error("BeginSSL", err, true); + return; + } + } + } + if ((events & (SE_READ|SE_WRITE))) { + LOG(LS_INFO) << "NSSStreamAdapter::OnEvent" + << ((events & SE_READ) ? " SE_READ" : "") + << ((events & SE_WRITE) ? " SE_WRITE" : ""); + if (state_ == SSL_NONE) { + events_to_signal |= events & (SE_READ|SE_WRITE); + } else if (state_ == SSL_CONNECTING) { + if (int err = ContinueSSL()) { + Error("ContinueSSL", err, true); + return; + } + } else if (state_ == SSL_CONNECTED) { + if (events & SE_WRITE) { + LOG(LS_INFO) << " -- onStreamWriteable"; + events_to_signal |= SE_WRITE; + } + if (events & SE_READ) { + LOG(LS_INFO) << " -- onStreamReadable"; + events_to_signal |= SE_READ; + } + } + } + if ((events & SE_CLOSE)) { + LOG(LS_INFO) << "NSSStreamAdapter::OnEvent(SE_CLOSE, " << err << ")"; + Cleanup(); + events_to_signal |= SE_CLOSE; + // SE_CLOSE is the only event that uses the final parameter to OnEvent(). + ASSERT(signal_error == 0); + signal_error = err; + } + if (events_to_signal) + StreamAdapterInterface::OnEvent(stream, events_to_signal, signal_error); +} + +void NSSStreamAdapter::OnMessage(Message* msg) { + // Process our own messages and then pass others to the superclass + if (MSG_DTLS_TIMEOUT == msg->message_id) { + LOG(LS_INFO) << "DTLS timeout expired"; + ContinueSSL(); + } else { + StreamInterface::OnMessage(msg); + } +} + +// Certificate verification callback. Called to check any certificate +SECStatus NSSStreamAdapter::AuthCertificateHook(void *arg, + PRFileDesc *fd, + PRBool checksig, + PRBool isServer) { + LOG(LS_INFO) << "NSSStreamAdapter::AuthCertificateHook"; + NSSCertificate peer_cert(SSL_PeerCertificate(fd)); + bool ok = false; + + // TODO(ekr@rtfm.com): Should we be enforcing self-signed like + // the OpenSSL version? + NSSStreamAdapter *stream = reinterpret_cast(arg); + + if (stream->peer_certificate_.get()) { + LOG(LS_INFO) << "Checking against specified certificate"; + + // The peer certificate was specified + if (reinterpret_cast(stream->peer_certificate_.get())-> + Equals(&peer_cert)) { + LOG(LS_INFO) << "Accepted peer certificate"; + ok = true; + } + } else if (!stream->peer_certificate_digest_algorithm_.empty()) { + LOG(LS_INFO) << "Checking against specified digest"; + // The peer certificate digest was specified + unsigned char digest[64]; // Maximum size + std::size_t digest_length; + + if (!peer_cert.ComputeDigest( + stream->peer_certificate_digest_algorithm_, + digest, sizeof(digest), &digest_length)) { + LOG(LS_ERROR) << "Digest computation failed"; + } else { + Buffer computed_digest(digest, digest_length); + if (computed_digest == stream->peer_certificate_digest_value_) { + LOG(LS_INFO) << "Accepted peer certificate"; + ok = true; + } + } + } else { + // Other modes, but we haven't implemented yet + // TODO(ekr@rtfm.com): Implement real certificate validation + UNIMPLEMENTED; + } + + if (ok) { + stream->cert_ok_ = true; + return SECSuccess; + } + + if (!ok && stream->ignore_bad_cert()) { + LOG(LS_WARNING) << "Ignoring cert error while verifying cert chain"; + stream->cert_ok_ = true; + return SECSuccess; + } + + PORT_SetError(SEC_ERROR_UNTRUSTED_CERT); + return SECFailure; +} + + +SECStatus NSSStreamAdapter::GetClientAuthDataHook(void *arg, PRFileDesc *fd, + CERTDistNames *caNames, + CERTCertificate **pRetCert, + SECKEYPrivateKey **pRetKey) { + LOG(LS_INFO) << "Client cert requested"; + NSSStreamAdapter *stream = reinterpret_cast(arg); + + if (!stream->identity_.get()) { + LOG(LS_ERROR) << "No identity available"; + return SECFailure; + } + + NSSIdentity *identity = static_cast(stream->identity_.get()); + // Destroyed internally by NSS + *pRetCert = CERT_DupCertificate(identity->certificate().certificate()); + *pRetKey = SECKEY_CopyPrivateKey(identity->keypair()->privkey()); + + return SECSuccess; +} + +// RFC 5705 Key Exporter +bool NSSStreamAdapter::ExportKeyingMaterial(const std::string& label, + const uint8* context, + size_t context_len, + bool use_context, + uint8* result, + size_t result_len) { + SECStatus rv = SSL_ExportKeyingMaterial(ssl_fd_, + label.c_str(), label.size(), + use_context, + context, context_len, + result, result_len); + + return rv == SECSuccess; +} + +bool NSSStreamAdapter::SetDtlsSrtpCiphers( + const std::vector& ciphers) { +#ifdef HAVE_DTLS_SRTP + std::vector internal_ciphers; + if (state_ != SSL_NONE) + return false; + + for (std::vector::const_iterator cipher = ciphers.begin(); + cipher != ciphers.end(); ++cipher) { + bool found = false; + for (const SrtpCipherMapEntry *entry = kSrtpCipherMap; entry->cipher_id; + ++entry) { + if (*cipher == entry->external_name) { + found = true; + internal_ciphers.push_back(entry->cipher_id); + break; + } + } + + if (!found) { + LOG(LS_ERROR) << "Could not find cipher: " << *cipher; + return false; + } + } + + if (internal_ciphers.empty()) + return false; + + srtp_ciphers_ = internal_ciphers; + + return true; +#else + return false; +#endif +} + +bool NSSStreamAdapter::GetDtlsSrtpCipher(std::string* cipher) { +#ifdef HAVE_DTLS_SRTP + ASSERT(state_ == SSL_CONNECTED); + if (state_ != SSL_CONNECTED) + return false; + + PRUint16 selected_cipher; + + SECStatus rv = SSL_GetSRTPCipher(ssl_fd_, &selected_cipher); + if (rv == SECFailure) + return false; + + for (const SrtpCipherMapEntry *entry = kSrtpCipherMap; + entry->cipher_id; ++entry) { + if (selected_cipher == entry->cipher_id) { + *cipher = entry->external_name; + return true; + } + } + + ASSERT(false); // This should never happen +#endif + return false; +} + + +bool NSSContext::initialized; +NSSContext *NSSContext::global_nss_context; + +// Static initialization and shutdown +NSSContext *NSSContext::Instance() { + if (!global_nss_context) { + NSSContext *new_ctx = new NSSContext(); + + if (!(new_ctx->slot_ = PK11_GetInternalSlot())) { + delete new_ctx; + goto fail; + } + + global_nss_context = new_ctx; + } + + fail: + return global_nss_context; +} + + + +bool NSSContext::InitializeSSL(VerificationCallback callback) { + ASSERT(!callback); + + if (!initialized) { + SECStatus rv; + + rv = NSS_NoDB_Init(NULL); + if (rv != SECSuccess) { + LOG(LS_ERROR) << "Couldn't initialize NSS error=" << + PORT_GetError(); + return false; + } + + NSS_SetDomesticPolicy(); + + initialized = true; + } + + return true; +} + +bool NSSContext::InitializeSSLThread() { + // Not needed + return true; +} + +bool NSSContext::CleanupSSL() { + // Not needed + return true; +} + +bool NSSStreamAdapter::HaveDtls() { + return true; +} + +bool NSSStreamAdapter::HaveDtlsSrtp() { +#ifdef HAVE_DTLS_SRTP + return true; +#else + return false; +#endif +} + +bool NSSStreamAdapter::HaveExporter() { + return true; +} + +} // namespace talk_base + +#endif // HAVE_NSS_SSL_H diff --git a/talk/base/nssstreamadapter.h b/talk/base/nssstreamadapter.h new file mode 100644 index 000000000..219f6193f --- /dev/null +++ b/talk/base/nssstreamadapter.h @@ -0,0 +1,130 @@ +/* + * libjingle + * Copyright 2004--2008, Google Inc. + * Copyright 2011, RTFM, 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. + */ + +#ifndef TALK_BASE_NSSSTREAMADAPTER_H_ +#define TALK_BASE_NSSSTREAMADAPTER_H_ + +#include +#include + +#include "nspr.h" +#include "nss.h" +#include "secmodt.h" + +#include "talk/base/buffer.h" +#include "talk/base/nssidentity.h" +#include "talk/base/ssladapter.h" +#include "talk/base/sslstreamadapter.h" +#include "talk/base/sslstreamadapterhelper.h" + +namespace talk_base { + +// Singleton +class NSSContext { + public: + NSSContext() {} + ~NSSContext() { + } + + static PK11SlotInfo *GetSlot() { + return Instance() ? Instance()->slot_: NULL; + } + + static NSSContext *Instance(); + static bool InitializeSSL(VerificationCallback callback); + static bool InitializeSSLThread(); + static bool CleanupSSL(); + + private: + PK11SlotInfo *slot_; // The PKCS-11 slot + static bool initialized; // Was this initialized? + static NSSContext *global_nss_context; // The global context +}; + + +class NSSStreamAdapter : public SSLStreamAdapterHelper { + public: + explicit NSSStreamAdapter(StreamInterface* stream); + virtual ~NSSStreamAdapter(); + bool Init(); + + virtual StreamResult Read(void* data, size_t data_len, + size_t* read, int* error); + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + void OnMessage(Message *msg); + + // Key Extractor interface + virtual bool ExportKeyingMaterial(const std::string& label, + const uint8* context, + size_t context_len, + bool use_context, + uint8* result, + size_t result_len); + + // DTLS-SRTP interface + virtual bool SetDtlsSrtpCiphers(const std::vector& ciphers); + virtual bool GetDtlsSrtpCipher(std::string* cipher); + + // Capabilities interfaces + static bool HaveDtls(); + static bool HaveDtlsSrtp(); + static bool HaveExporter(); + + protected: + // Override SSLStreamAdapter + virtual void OnEvent(StreamInterface* stream, int events, int err); + + // Override SSLStreamAdapterHelper + virtual int BeginSSL(); + virtual void Cleanup(); + virtual bool GetDigestLength(const std::string &algorithm, + std::size_t *length) { + return NSSCertificate::GetDigestLength(algorithm, length); + } + + private: + int ContinueSSL(); + static SECStatus AuthCertificateHook(void *arg, PRFileDesc *fd, + PRBool checksig, PRBool isServer); + static SECStatus GetClientAuthDataHook(void *arg, PRFileDesc *fd, + CERTDistNames *caNames, + CERTCertificate **pRetCert, + SECKEYPrivateKey **pRetKey); + + PRFileDesc *ssl_fd_; // NSS's SSL file descriptor + static bool initialized; // Was InitializeSSL() called? + bool cert_ok_; // Did we get and check a cert + std::vector srtp_ciphers_; // SRTP cipher list + + static PRDescIdentity nspr_layer_identity; // The NSPR layer identity +}; + +} // namespace talk_base + +#endif // TALK_BASE_NSSSTREAMADAPTER_H_ diff --git a/talk/base/nullsocketserver.h b/talk/base/nullsocketserver.h new file mode 100644 index 000000000..6b3b2887e --- /dev/null +++ b/talk/base/nullsocketserver.h @@ -0,0 +1,78 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_BASE_NULLSOCKETSERVER_H_ +#define TALK_BASE_NULLSOCKETSERVER_H_ + +#include "talk/base/event.h" +#include "talk/base/physicalsocketserver.h" + +namespace talk_base { + +// NullSocketServer + +class NullSocketServer : public talk_base::SocketServer { + public: + NullSocketServer() : event_(false, false) {} + + virtual bool Wait(int cms, bool process_io) { + event_.Wait(cms); + return true; + } + + virtual void WakeUp() { + event_.Set(); + } + + virtual talk_base::Socket* CreateSocket(int type) { + ASSERT(false); + return NULL; + } + + virtual talk_base::Socket* CreateSocket(int family, int type) { + ASSERT(false); + return NULL; + } + + virtual talk_base::AsyncSocket* CreateAsyncSocket(int type) { + ASSERT(false); + return NULL; + } + + virtual talk_base::AsyncSocket* CreateAsyncSocket(int family, int type) { + ASSERT(false); + return NULL; + } + + + private: + talk_base::Event event_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_NULLSOCKETSERVER_H_ diff --git a/talk/base/nullsocketserver_unittest.cc b/talk/base/nullsocketserver_unittest.cc new file mode 100644 index 000000000..18cde2db8 --- /dev/null +++ b/talk/base/nullsocketserver_unittest.cc @@ -0,0 +1,64 @@ +/* + * libjingle + * Copyright 2012, 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/base/gunit.h" +#include "talk/base/nullsocketserver.h" + +namespace talk_base { + +static const uint32 kTimeout = 5000U; + +class NullSocketServerTest + : public testing::Test, + public MessageHandler { + public: + NullSocketServerTest() {} + protected: + virtual void OnMessage(Message* message) { + ss_.WakeUp(); + } + NullSocketServer ss_; +}; + +TEST_F(NullSocketServerTest, WaitAndSet) { + Thread thread; + EXPECT_TRUE(thread.Start()); + thread.Post(this, 0); + // The process_io will be ignored. + const bool process_io = true; + EXPECT_TRUE_WAIT(ss_.Wait(talk_base::kForever, process_io), kTimeout); +} + +TEST_F(NullSocketServerTest, TestWait) { + uint32 start = Time(); + ss_.Wait(200, true); + // The actual wait time is dependent on the resolution of the timer used by + // the Event class. Allow for the event to signal ~20ms early. + EXPECT_GE(TimeSince(start), 180); +} + +} // namespace talk_base diff --git a/talk/base/openssladapter.cc b/talk/base/openssladapter.cc new file mode 100644 index 000000000..50391e5c2 --- /dev/null +++ b/talk/base/openssladapter.cc @@ -0,0 +1,908 @@ +/* + * libjingle + * Copyright 2008, 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. + */ + +#if HAVE_OPENSSL_SSL_H + +#include "talk/base/openssladapter.h" + +#if defined(POSIX) +#include +#endif + +// Must be included first before openssl headers. +#include "talk/base/win32.h" // NOLINT + +#include +#include +#include +#include +#include +#include +#include + +#if HAVE_CONFIG_H +#include "config.h" +#endif // HAVE_CONFIG_H + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/sslroots.h" +#include "talk/base/stringutils.h" + +// TODO: Use a nicer abstraction for mutex. + +#if defined(WIN32) + #define MUTEX_TYPE HANDLE + #define MUTEX_SETUP(x) (x) = CreateMutex(NULL, FALSE, NULL) + #define MUTEX_CLEANUP(x) CloseHandle(x) + #define MUTEX_LOCK(x) WaitForSingleObject((x), INFINITE) + #define MUTEX_UNLOCK(x) ReleaseMutex(x) + #define THREAD_ID GetCurrentThreadId() +#elif defined(_POSIX_THREADS) + // _POSIX_THREADS is normally defined in unistd.h if pthreads are available + // on your platform. + #define MUTEX_TYPE pthread_mutex_t + #define MUTEX_SETUP(x) pthread_mutex_init(&(x), NULL) + #define MUTEX_CLEANUP(x) pthread_mutex_destroy(&(x)) + #define MUTEX_LOCK(x) pthread_mutex_lock(&(x)) + #define MUTEX_UNLOCK(x) pthread_mutex_unlock(&(x)) + #define THREAD_ID pthread_self() +#else + #error You must define mutex operations appropriate for your platform! +#endif + +struct CRYPTO_dynlock_value { + MUTEX_TYPE mutex; +}; + +////////////////////////////////////////////////////////////////////// +// SocketBIO +////////////////////////////////////////////////////////////////////// + +static int socket_write(BIO* h, const char* buf, int num); +static int socket_read(BIO* h, char* buf, int size); +static int socket_puts(BIO* h, const char* str); +static long socket_ctrl(BIO* h, int cmd, long arg1, void* arg2); +static int socket_new(BIO* h); +static int socket_free(BIO* data); + +static BIO_METHOD methods_socket = { + BIO_TYPE_BIO, + "socket", + socket_write, + socket_read, + socket_puts, + 0, + socket_ctrl, + socket_new, + socket_free, + NULL, +}; + +BIO_METHOD* BIO_s_socket2() { return(&methods_socket); } + +BIO* BIO_new_socket(talk_base::AsyncSocket* socket) { + BIO* ret = BIO_new(BIO_s_socket2()); + if (ret == NULL) { + return NULL; + } + ret->ptr = socket; + return ret; +} + +static int socket_new(BIO* b) { + b->shutdown = 0; + b->init = 1; + b->num = 0; // 1 means socket closed + b->ptr = 0; + return 1; +} + +static int socket_free(BIO* b) { + if (b == NULL) + return 0; + return 1; +} + +static int socket_read(BIO* b, char* out, int outl) { + if (!out) + return -1; + talk_base::AsyncSocket* socket = static_cast(b->ptr); + BIO_clear_retry_flags(b); + int result = socket->Recv(out, outl); + if (result > 0) { + return result; + } else if (result == 0) { + b->num = 1; + } else if (socket->IsBlocking()) { + BIO_set_retry_read(b); + } + return -1; +} + +static int socket_write(BIO* b, const char* in, int inl) { + if (!in) + return -1; + talk_base::AsyncSocket* socket = static_cast(b->ptr); + BIO_clear_retry_flags(b); + int result = socket->Send(in, inl); + if (result > 0) { + return result; + } else if (socket->IsBlocking()) { + BIO_set_retry_write(b); + } + return -1; +} + +static int socket_puts(BIO* b, const char* str) { + return socket_write(b, str, strlen(str)); +} + +static long socket_ctrl(BIO* b, int cmd, long num, void* ptr) { + UNUSED(num); + UNUSED(ptr); + + switch (cmd) { + case BIO_CTRL_RESET: + return 0; + case BIO_CTRL_EOF: + return b->num; + case BIO_CTRL_WPENDING: + case BIO_CTRL_PENDING: + return 0; + case BIO_CTRL_FLUSH: + return 1; + default: + return 0; + } +} + +///////////////////////////////////////////////////////////////////////////// +// OpenSSLAdapter +///////////////////////////////////////////////////////////////////////////// + +namespace talk_base { + +// This array will store all of the mutexes available to OpenSSL. +static MUTEX_TYPE* mutex_buf = NULL; + +static void locking_function(int mode, int n, const char * file, int line) { + if (mode & CRYPTO_LOCK) { + MUTEX_LOCK(mutex_buf[n]); + } else { + MUTEX_UNLOCK(mutex_buf[n]); + } +} + +static unsigned long id_function() { // NOLINT + // Use old-style C cast because THREAD_ID's type varies with the platform, + // in some cases requiring static_cast, and in others requiring + // reinterpret_cast. + return (unsigned long)THREAD_ID; // NOLINT +} + +static CRYPTO_dynlock_value* dyn_create_function(const char* file, int line) { + CRYPTO_dynlock_value* value = new CRYPTO_dynlock_value; + if (!value) + return NULL; + MUTEX_SETUP(value->mutex); + return value; +} + +static void dyn_lock_function(int mode, CRYPTO_dynlock_value* l, + const char* file, int line) { + if (mode & CRYPTO_LOCK) { + MUTEX_LOCK(l->mutex); + } else { + MUTEX_UNLOCK(l->mutex); + } +} + +static void dyn_destroy_function(CRYPTO_dynlock_value* l, + const char* file, int line) { + MUTEX_CLEANUP(l->mutex); + delete l; +} + +VerificationCallback OpenSSLAdapter::custom_verify_callback_ = NULL; + +bool OpenSSLAdapter::InitializeSSL(VerificationCallback callback) { + if (!InitializeSSLThread() || !SSL_library_init()) + return false; + SSL_load_error_strings(); + ERR_load_BIO_strings(); + OpenSSL_add_all_algorithms(); + RAND_poll(); + custom_verify_callback_ = callback; + return true; +} + +bool OpenSSLAdapter::InitializeSSLThread() { + mutex_buf = new MUTEX_TYPE[CRYPTO_num_locks()]; + if (!mutex_buf) + return false; + for (int i = 0; i < CRYPTO_num_locks(); ++i) + MUTEX_SETUP(mutex_buf[i]); + + // we need to cast our id_function to return an unsigned long -- pthread_t is + // a pointer + CRYPTO_set_id_callback(id_function); + CRYPTO_set_locking_callback(locking_function); + CRYPTO_set_dynlock_create_callback(dyn_create_function); + CRYPTO_set_dynlock_lock_callback(dyn_lock_function); + CRYPTO_set_dynlock_destroy_callback(dyn_destroy_function); + return true; +} + +bool OpenSSLAdapter::CleanupSSL() { + if (!mutex_buf) + return false; + CRYPTO_set_id_callback(NULL); + CRYPTO_set_locking_callback(NULL); + CRYPTO_set_dynlock_create_callback(NULL); + CRYPTO_set_dynlock_lock_callback(NULL); + CRYPTO_set_dynlock_destroy_callback(NULL); + for (int i = 0; i < CRYPTO_num_locks(); ++i) + MUTEX_CLEANUP(mutex_buf[i]); + delete [] mutex_buf; + mutex_buf = NULL; + return true; +} + +OpenSSLAdapter::OpenSSLAdapter(AsyncSocket* socket) + : SSLAdapter(socket), + state_(SSL_NONE), + ssl_read_needs_write_(false), + ssl_write_needs_read_(false), + restartable_(false), + ssl_(NULL), ssl_ctx_(NULL), + custom_verification_succeeded_(false) { +} + +OpenSSLAdapter::~OpenSSLAdapter() { + Cleanup(); +} + +int +OpenSSLAdapter::StartSSL(const char* hostname, bool restartable) { + if (state_ != SSL_NONE) + return -1; + + ssl_host_name_ = hostname; + restartable_ = restartable; + + if (socket_->GetState() != Socket::CS_CONNECTED) { + state_ = SSL_WAIT; + return 0; + } + + state_ = SSL_CONNECTING; + if (int err = BeginSSL()) { + Error("BeginSSL", err, false); + return err; + } + + return 0; +} + +int +OpenSSLAdapter::BeginSSL() { + LOG(LS_INFO) << "BeginSSL: " << ssl_host_name_; + ASSERT(state_ == SSL_CONNECTING); + + int err = 0; + BIO* bio = NULL; + + // First set up the context + if (!ssl_ctx_) + ssl_ctx_ = SetupSSLContext(); + + if (!ssl_ctx_) { + err = -1; + goto ssl_error; + } + + bio = BIO_new_socket(static_cast(socket_)); + if (!bio) { + err = -1; + goto ssl_error; + } + + ssl_ = SSL_new(ssl_ctx_); + if (!ssl_) { + err = -1; + goto ssl_error; + } + + SSL_set_app_data(ssl_, this); + + SSL_set_bio(ssl_, bio, bio); + SSL_set_mode(ssl_, SSL_MODE_ENABLE_PARTIAL_WRITE | + SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + + // the SSL object owns the bio now + bio = NULL; + + // Do the connect + err = ContinueSSL(); + if (err != 0) + goto ssl_error; + + return err; + +ssl_error: + Cleanup(); + if (bio) + BIO_free(bio); + + return err; +} + +int +OpenSSLAdapter::ContinueSSL() { + ASSERT(state_ == SSL_CONNECTING); + + int code = SSL_connect(ssl_); + switch (SSL_get_error(ssl_, code)) { + case SSL_ERROR_NONE: + if (!SSLPostConnectionCheck(ssl_, ssl_host_name_.c_str())) { + LOG(LS_ERROR) << "TLS post connection check failed"; + // make sure we close the socket + Cleanup(); + // The connect failed so return -1 to shut down the socket + return -1; + } + + state_ = SSL_CONNECTED; + AsyncSocketAdapter::OnConnectEvent(this); +#if 0 // TODO: worry about this + // Don't let ourselves go away during the callbacks + PRefPtr lock(this); + LOG(LS_INFO) << " -- onStreamReadable"; + AsyncSocketAdapter::OnReadEvent(this); + LOG(LS_INFO) << " -- onStreamWriteable"; + AsyncSocketAdapter::OnWriteEvent(this); +#endif + break; + + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + break; + + case SSL_ERROR_ZERO_RETURN: + default: + LOG(LS_WARNING) << "ContinueSSL -- error " << code; + return (code != 0) ? code : -1; + } + + return 0; +} + +void +OpenSSLAdapter::Error(const char* context, int err, bool signal) { + LOG(LS_WARNING) << "OpenSSLAdapter::Error(" + << context << ", " << err << ")"; + state_ = SSL_ERROR; + SetError(err); + if (signal) + AsyncSocketAdapter::OnCloseEvent(this, err); +} + +void +OpenSSLAdapter::Cleanup() { + LOG(LS_INFO) << "Cleanup"; + + state_ = SSL_NONE; + ssl_read_needs_write_ = false; + ssl_write_needs_read_ = false; + custom_verification_succeeded_ = false; + + if (ssl_) { + SSL_free(ssl_); + ssl_ = NULL; + } + + if (ssl_ctx_) { + SSL_CTX_free(ssl_ctx_); + ssl_ctx_ = NULL; + } +} + +// +// AsyncSocket Implementation +// + +int +OpenSSLAdapter::Send(const void* pv, size_t cb) { + //LOG(LS_INFO) << "OpenSSLAdapter::Send(" << cb << ")"; + + switch (state_) { + case SSL_NONE: + return AsyncSocketAdapter::Send(pv, cb); + + case SSL_WAIT: + case SSL_CONNECTING: + SetError(EWOULDBLOCK); + return SOCKET_ERROR; + + case SSL_CONNECTED: + break; + + case SSL_ERROR: + default: + return SOCKET_ERROR; + } + + // OpenSSL will return an error if we try to write zero bytes + if (cb == 0) + return 0; + + ssl_write_needs_read_ = false; + + int code = SSL_write(ssl_, pv, cb); + switch (SSL_get_error(ssl_, code)) { + case SSL_ERROR_NONE: + //LOG(LS_INFO) << " -- success"; + return code; + case SSL_ERROR_WANT_READ: + //LOG(LS_INFO) << " -- error want read"; + ssl_write_needs_read_ = true; + SetError(EWOULDBLOCK); + break; + case SSL_ERROR_WANT_WRITE: + //LOG(LS_INFO) << " -- error want write"; + SetError(EWOULDBLOCK); + break; + case SSL_ERROR_ZERO_RETURN: + //LOG(LS_INFO) << " -- remote side closed"; + SetError(EWOULDBLOCK); + // do we need to signal closure? + break; + default: + //LOG(LS_INFO) << " -- error " << code; + Error("SSL_write", (code ? code : -1), false); + break; + } + + return SOCKET_ERROR; +} + +int +OpenSSLAdapter::Recv(void* pv, size_t cb) { + //LOG(LS_INFO) << "OpenSSLAdapter::Recv(" << cb << ")"; + switch (state_) { + + case SSL_NONE: + return AsyncSocketAdapter::Recv(pv, cb); + + case SSL_WAIT: + case SSL_CONNECTING: + SetError(EWOULDBLOCK); + return SOCKET_ERROR; + + case SSL_CONNECTED: + break; + + case SSL_ERROR: + default: + return SOCKET_ERROR; + } + + // Don't trust OpenSSL with zero byte reads + if (cb == 0) + return 0; + + ssl_read_needs_write_ = false; + + int code = SSL_read(ssl_, pv, cb); + switch (SSL_get_error(ssl_, code)) { + case SSL_ERROR_NONE: + //LOG(LS_INFO) << " -- success"; + return code; + case SSL_ERROR_WANT_READ: + //LOG(LS_INFO) << " -- error want read"; + SetError(EWOULDBLOCK); + break; + case SSL_ERROR_WANT_WRITE: + //LOG(LS_INFO) << " -- error want write"; + ssl_read_needs_write_ = true; + SetError(EWOULDBLOCK); + break; + case SSL_ERROR_ZERO_RETURN: + //LOG(LS_INFO) << " -- remote side closed"; + SetError(EWOULDBLOCK); + // do we need to signal closure? + break; + default: + //LOG(LS_INFO) << " -- error " << code; + Error("SSL_read", (code ? code : -1), false); + break; + } + + return SOCKET_ERROR; +} + +int +OpenSSLAdapter::Close() { + Cleanup(); + state_ = restartable_ ? SSL_WAIT : SSL_NONE; + return AsyncSocketAdapter::Close(); +} + +Socket::ConnState +OpenSSLAdapter::GetState() const { + //if (signal_close_) + // return CS_CONNECTED; + ConnState state = socket_->GetState(); + if ((state == CS_CONNECTED) + && ((state_ == SSL_WAIT) || (state_ == SSL_CONNECTING))) + state = CS_CONNECTING; + return state; +} + +void +OpenSSLAdapter::OnConnectEvent(AsyncSocket* socket) { + LOG(LS_INFO) << "OpenSSLAdapter::OnConnectEvent"; + if (state_ != SSL_WAIT) { + ASSERT(state_ == SSL_NONE); + AsyncSocketAdapter::OnConnectEvent(socket); + return; + } + + state_ = SSL_CONNECTING; + if (int err = BeginSSL()) { + AsyncSocketAdapter::OnCloseEvent(socket, err); + } +} + +void +OpenSSLAdapter::OnReadEvent(AsyncSocket* socket) { + //LOG(LS_INFO) << "OpenSSLAdapter::OnReadEvent"; + + if (state_ == SSL_NONE) { + AsyncSocketAdapter::OnReadEvent(socket); + return; + } + + if (state_ == SSL_CONNECTING) { + if (int err = ContinueSSL()) { + Error("ContinueSSL", err); + } + return; + } + + if (state_ != SSL_CONNECTED) + return; + + // Don't let ourselves go away during the callbacks + //PRefPtr lock(this); // TODO: fix this + if (ssl_write_needs_read_) { + //LOG(LS_INFO) << " -- onStreamWriteable"; + AsyncSocketAdapter::OnWriteEvent(socket); + } + + //LOG(LS_INFO) << " -- onStreamReadable"; + AsyncSocketAdapter::OnReadEvent(socket); +} + +void +OpenSSLAdapter::OnWriteEvent(AsyncSocket* socket) { + //LOG(LS_INFO) << "OpenSSLAdapter::OnWriteEvent"; + + if (state_ == SSL_NONE) { + AsyncSocketAdapter::OnWriteEvent(socket); + return; + } + + if (state_ == SSL_CONNECTING) { + if (int err = ContinueSSL()) { + Error("ContinueSSL", err); + } + return; + } + + if (state_ != SSL_CONNECTED) + return; + + // Don't let ourselves go away during the callbacks + //PRefPtr lock(this); // TODO: fix this + + if (ssl_read_needs_write_) { + //LOG(LS_INFO) << " -- onStreamReadable"; + AsyncSocketAdapter::OnReadEvent(socket); + } + + //LOG(LS_INFO) << " -- onStreamWriteable"; + AsyncSocketAdapter::OnWriteEvent(socket); +} + +void +OpenSSLAdapter::OnCloseEvent(AsyncSocket* socket, int err) { + LOG(LS_INFO) << "OpenSSLAdapter::OnCloseEvent(" << err << ")"; + AsyncSocketAdapter::OnCloseEvent(socket, err); +} + +// This code is taken from the "Network Security with OpenSSL" +// sample in chapter 5 + +bool OpenSSLAdapter::VerifyServerName(SSL* ssl, const char* host, + bool ignore_bad_cert) { + if (!host) + return false; + + // Checking the return from SSL_get_peer_certificate here is not strictly + // necessary. With our setup, it is not possible for it to return + // NULL. However, it is good form to check the return. + X509* certificate = SSL_get_peer_certificate(ssl); + if (!certificate) + return false; + + // Logging certificates is extremely verbose. So it is disabled by default. +#ifdef LOG_CERTIFICATES + { + LOG(LS_INFO) << "Certificate from server:"; + BIO* mem = BIO_new(BIO_s_mem()); + X509_print_ex(mem, certificate, XN_FLAG_SEP_CPLUS_SPC, X509_FLAG_NO_HEADER); + BIO_write(mem, "\0", 1); + char* buffer; + BIO_get_mem_data(mem, &buffer); + LOG(LS_INFO) << buffer; + BIO_free(mem); + + char* cipher_description = + SSL_CIPHER_description(SSL_get_current_cipher(ssl), NULL, 128); + LOG(LS_INFO) << "Cipher: " << cipher_description; + OPENSSL_free(cipher_description); + } +#endif + + bool ok = false; + int extension_count = X509_get_ext_count(certificate); + for (int i = 0; i < extension_count; ++i) { + X509_EXTENSION* extension = X509_get_ext(certificate, i); + int extension_nid = OBJ_obj2nid(X509_EXTENSION_get_object(extension)); + + if (extension_nid == NID_subject_alt_name) { +#if OPENSSL_VERSION_NUMBER >= 0x10000000L + const X509V3_EXT_METHOD* meth = X509V3_EXT_get(extension); +#else + X509V3_EXT_METHOD* meth = X509V3_EXT_get(extension); +#endif + if (!meth) + break; + + void* ext_str = NULL; + + // We assign this to a local variable, instead of passing the address + // directly to ASN1_item_d2i. + // See http://readlist.com/lists/openssl.org/openssl-users/0/4761.html. + unsigned char* ext_value_data = extension->value->data; + +#if OPENSSL_VERSION_NUMBER >= 0x0090800fL + const unsigned char **ext_value_data_ptr = + (const_cast(&ext_value_data)); +#else + unsigned char **ext_value_data_ptr = &ext_value_data; +#endif + + if (meth->it) { + ext_str = ASN1_item_d2i(NULL, ext_value_data_ptr, + extension->value->length, + ASN1_ITEM_ptr(meth->it)); + } else { + ext_str = meth->d2i(NULL, ext_value_data_ptr, extension->value->length); + } + + STACK_OF(CONF_VALUE)* value = meth->i2v(meth, ext_str, NULL); + for (int j = 0; j < sk_CONF_VALUE_num(value); ++j) { + CONF_VALUE* nval = sk_CONF_VALUE_value(value, j); + // The value for nval can contain wildcards + if (!strcmp(nval->name, "DNS") && string_match(host, nval->value)) { + ok = true; + break; + } + } + sk_CONF_VALUE_pop_free(value, X509V3_conf_free); + value = NULL; + + if (meth->it) { + ASN1_item_free(reinterpret_cast(ext_str), + ASN1_ITEM_ptr(meth->it)); + } else { + meth->ext_free(ext_str); + } + ext_str = NULL; + } + if (ok) + break; + } + + char data[256]; + X509_name_st* subject; + if (!ok + && ((subject = X509_get_subject_name(certificate)) != NULL) + && (X509_NAME_get_text_by_NID(subject, NID_commonName, + data, sizeof(data)) > 0)) { + data[sizeof(data)-1] = 0; + if (_stricmp(data, host) == 0) + ok = true; + } + + X509_free(certificate); + + // This should only ever be turned on for debugging and development. + if (!ok && ignore_bad_cert) { + LOG(LS_WARNING) << "TLS certificate check FAILED. " + << "Allowing connection anyway."; + ok = true; + } + + return ok; +} + +bool OpenSSLAdapter::SSLPostConnectionCheck(SSL* ssl, const char* host) { + bool ok = VerifyServerName(ssl, host, ignore_bad_cert()); + + if (ok) { + ok = (SSL_get_verify_result(ssl) == X509_V_OK || + custom_verification_succeeded_); + } + + if (!ok && ignore_bad_cert()) { + LOG(LS_INFO) << "Other TLS post connection checks failed."; + ok = true; + } + + return ok; +} + +#if _DEBUG + +// We only use this for tracing and so it is only needed in debug mode + +void +OpenSSLAdapter::SSLInfoCallback(const SSL* s, int where, int ret) { + const char* str = "undefined"; + int w = where & ~SSL_ST_MASK; + if (w & SSL_ST_CONNECT) { + str = "SSL_connect"; + } else if (w & SSL_ST_ACCEPT) { + str = "SSL_accept"; + } + if (where & SSL_CB_LOOP) { + LOG(LS_INFO) << str << ":" << SSL_state_string_long(s); + } else if (where & SSL_CB_ALERT) { + str = (where & SSL_CB_READ) ? "read" : "write"; + LOG(LS_INFO) << "SSL3 alert " << str + << ":" << SSL_alert_type_string_long(ret) + << ":" << SSL_alert_desc_string_long(ret); + } else if (where & SSL_CB_EXIT) { + if (ret == 0) { + LOG(LS_INFO) << str << ":failed in " << SSL_state_string_long(s); + } else if (ret < 0) { + LOG(LS_INFO) << str << ":error in " << SSL_state_string_long(s); + } + } +} + +#endif // _DEBUG + +int +OpenSSLAdapter::SSLVerifyCallback(int ok, X509_STORE_CTX* store) { +#if _DEBUG + if (!ok) { + char data[256]; + X509* cert = X509_STORE_CTX_get_current_cert(store); + int depth = X509_STORE_CTX_get_error_depth(store); + int err = X509_STORE_CTX_get_error(store); + + LOG(LS_INFO) << "Error with certificate at depth: " << depth; + X509_NAME_oneline(X509_get_issuer_name(cert), data, sizeof(data)); + LOG(LS_INFO) << " issuer = " << data; + X509_NAME_oneline(X509_get_subject_name(cert), data, sizeof(data)); + LOG(LS_INFO) << " subject = " << data; + LOG(LS_INFO) << " err = " << err + << ":" << X509_verify_cert_error_string(err); + } +#endif + + // Get our stream pointer from the store + SSL* ssl = reinterpret_cast( + X509_STORE_CTX_get_ex_data(store, + SSL_get_ex_data_X509_STORE_CTX_idx())); + + OpenSSLAdapter* stream = + reinterpret_cast(SSL_get_app_data(ssl)); + + if (!ok && custom_verify_callback_) { + void* cert = + reinterpret_cast(X509_STORE_CTX_get_current_cert(store)); + if (custom_verify_callback_(cert)) { + stream->custom_verification_succeeded_ = true; + LOG(LS_INFO) << "validated certificate using custom callback"; + ok = true; + } + } + + // Should only be used for debugging and development. + if (!ok && stream->ignore_bad_cert()) { + LOG(LS_WARNING) << "Ignoring cert error while verifying cert chain"; + ok = 1; + } + + return ok; +} + +bool OpenSSLAdapter::ConfigureTrustedRootCertificates(SSL_CTX* ctx) { + // Add the root cert that we care about to the SSL context + int count_of_added_certs = 0; + for (int i = 0; i < ARRAY_SIZE(kSSLCertCertificateList); i++) { + const unsigned char* cert_buffer = kSSLCertCertificateList[i]; + size_t cert_buffer_len = kSSLCertCertificateSizeList[i]; + X509* cert = d2i_X509(NULL, &cert_buffer, cert_buffer_len); + if (cert) { + int return_value = X509_STORE_add_cert(SSL_CTX_get_cert_store(ctx), cert); + if (return_value == 0) { + LOG(LS_WARNING) << "Unable to add certificate."; + } else { + count_of_added_certs++; + } + X509_free(cert); + } + } + return count_of_added_certs > 0; +} + +SSL_CTX* +OpenSSLAdapter::SetupSSLContext() { + SSL_CTX* ctx = SSL_CTX_new(TLSv1_client_method()); + if (ctx == NULL) { + unsigned long error = ERR_get_error(); // NOLINT: type used by OpenSSL. + LOG(LS_WARNING) << "SSL_CTX creation failed: " + << '"' << ERR_reason_error_string(error) << "\" " + << "(error=" << error << ')'; + return NULL; + } + if (!ConfigureTrustedRootCertificates(ctx)) { + SSL_CTX_free(ctx); + return NULL; + } + +#ifdef _DEBUG + SSL_CTX_set_info_callback(ctx, SSLInfoCallback); +#endif + + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, SSLVerifyCallback); + SSL_CTX_set_verify_depth(ctx, 4); + SSL_CTX_set_cipher_list(ctx, "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"); + + return ctx; +} + +} // namespace talk_base + +#endif // HAVE_OPENSSL_SSL_H diff --git a/talk/base/openssladapter.h b/talk/base/openssladapter.h new file mode 100644 index 000000000..c89c29248 --- /dev/null +++ b/talk/base/openssladapter.h @@ -0,0 +1,105 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_OPENSSLADAPTER_H__ +#define TALK_BASE_OPENSSLADAPTER_H__ + +#include +#include "talk/base/ssladapter.h" + +typedef struct ssl_st SSL; +typedef struct ssl_ctx_st SSL_CTX; +typedef struct x509_store_ctx_st X509_STORE_CTX; + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// + +class OpenSSLAdapter : public SSLAdapter { +public: + static bool InitializeSSL(VerificationCallback callback); + static bool InitializeSSLThread(); + static bool CleanupSSL(); + + OpenSSLAdapter(AsyncSocket* socket); + virtual ~OpenSSLAdapter(); + + virtual int StartSSL(const char* hostname, bool restartable); + virtual int Send(const void* pv, size_t cb); + virtual int Recv(void* pv, size_t cb); + virtual int Close(); + + // Note that the socket returns ST_CONNECTING while SSL is being negotiated. + virtual ConnState GetState() const; + +protected: + virtual void OnConnectEvent(AsyncSocket* socket); + virtual void OnReadEvent(AsyncSocket* socket); + virtual void OnWriteEvent(AsyncSocket* socket); + virtual void OnCloseEvent(AsyncSocket* socket, int err); + +private: + enum SSLState { + SSL_NONE, SSL_WAIT, SSL_CONNECTING, SSL_CONNECTED, SSL_ERROR + }; + + int BeginSSL(); + int ContinueSSL(); + void Error(const char* context, int err, bool signal = true); + void Cleanup(); + + static bool VerifyServerName(SSL* ssl, const char* host, + bool ignore_bad_cert); + bool SSLPostConnectionCheck(SSL* ssl, const char* host); +#if _DEBUG + static void SSLInfoCallback(const SSL* s, int where, int ret); +#endif // !_DEBUG + static int SSLVerifyCallback(int ok, X509_STORE_CTX* store); + static VerificationCallback custom_verify_callback_; + friend class OpenSSLStreamAdapter; // for custom_verify_callback_; + + static bool ConfigureTrustedRootCertificates(SSL_CTX* ctx); + static SSL_CTX* SetupSSLContext(); + + SSLState state_; + bool ssl_read_needs_write_; + bool ssl_write_needs_read_; + // If true, socket will retain SSL configuration after Close. + bool restartable_; + + SSL* ssl_; + SSL_CTX* ssl_ctx_; + std::string ssl_host_name_; + + bool custom_verification_succeeded_; +}; + +///////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_OPENSSLADAPTER_H__ diff --git a/talk/base/openssldigest.cc b/talk/base/openssldigest.cc new file mode 100644 index 000000000..bb0e027cf --- /dev/null +++ b/talk/base/openssldigest.cc @@ -0,0 +1,114 @@ +/* + * libjingle + * Copyright 2004--2012, 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. + */ + +#if HAVE_OPENSSL_SSL_H + +#include "talk/base/openssldigest.h" + +#include "talk/base/common.h" + +namespace talk_base { + +OpenSSLDigest::OpenSSLDigest(const std::string& algorithm) { + EVP_MD_CTX_init(&ctx_); + if (GetDigestEVP(algorithm, &md_)) { + EVP_DigestInit_ex(&ctx_, md_, NULL); + } else { + md_ = NULL; + } +} + +OpenSSLDigest::~OpenSSLDigest() { + EVP_MD_CTX_cleanup(&ctx_); +} + +size_t OpenSSLDigest::Size() const { + if (!md_) { + return 0; + } + return EVP_MD_size(md_); +} + +void OpenSSLDigest::Update(const void* buf, size_t len) { + if (!md_) { + return; + } + EVP_DigestUpdate(&ctx_, buf, len); +} + +size_t OpenSSLDigest::Finish(void* buf, size_t len) { + if (!md_ || len < Size()) { + return 0; + } + unsigned int md_len; + EVP_DigestFinal_ex(&ctx_, static_cast(buf), &md_len); + EVP_DigestInit_ex(&ctx_, md_, NULL); // prepare for future Update()s + ASSERT(md_len == Size()); + return md_len; +} + +bool OpenSSLDigest::GetDigestEVP(const std::string& algorithm, + const EVP_MD** mdp) { + const EVP_MD* md; + if (algorithm == DIGEST_MD5) { + md = EVP_md5(); + } else if (algorithm == DIGEST_SHA_1) { + md = EVP_sha1(); +#if OPENSSL_VERSION_NUMBER >= 0x00908000L + } else if (algorithm == DIGEST_SHA_224) { + md = EVP_sha224(); + } else if (algorithm == DIGEST_SHA_256) { + md = EVP_sha256(); + } else if (algorithm == DIGEST_SHA_384) { + md = EVP_sha384(); + } else if (algorithm == DIGEST_SHA_512) { + md = EVP_sha512(); +#endif + } else { + return false; + } + + // Can't happen + ASSERT(EVP_MD_size(md) >= 16); + *mdp = md; + return true; +} + +bool OpenSSLDigest::GetDigestSize(const std::string& algorithm, + size_t* length) { + const EVP_MD *md; + if (!GetDigestEVP(algorithm, &md)) + return false; + + *length = EVP_MD_size(md); + return true; +} + +} // namespace talk_base + +#endif // HAVE_OPENSSL_SSL_H + diff --git a/talk/base/openssldigest.h b/talk/base/openssldigest.h new file mode 100644 index 000000000..0a12189d0 --- /dev/null +++ b/talk/base/openssldigest.h @@ -0,0 +1,64 @@ +/* + * libjingle + * Copyright 2004--2012, 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. + */ + +#ifndef TALK_BASE_OPENSSLDIGEST_H_ +#define TALK_BASE_OPENSSLDIGEST_H_ + +#include + +#include "talk/base/messagedigest.h" + +namespace talk_base { + +// An implementation of the digest class that uses OpenSSL. +class OpenSSLDigest : public MessageDigest { + public: + // Creates an OpenSSLDigest with |algorithm| as the hash algorithm. + explicit OpenSSLDigest(const std::string& algorithm); + ~OpenSSLDigest(); + // Returns the digest output size (e.g. 16 bytes for MD5). + virtual size_t Size() const; + // Updates the digest with |len| bytes from |buf|. + virtual void Update(const void* buf, size_t len); + // Outputs the digest value to |buf| with length |len|. + virtual size_t Finish(void* buf, size_t len); + + // Helper function to look up a digest. + static bool GetDigestEVP(const std::string &algorithm, + const EVP_MD** md); + // Helper function to get the length of a digest. + static bool GetDigestSize(const std::string &algorithm, + size_t* len); + + private: + EVP_MD_CTX ctx_; + const EVP_MD* md_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_OPENSSLDIGEST_H_ diff --git a/talk/base/opensslidentity.cc b/talk/base/opensslidentity.cc new file mode 100644 index 000000000..a48c94fd7 --- /dev/null +++ b/talk/base/opensslidentity.cc @@ -0,0 +1,342 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +#if HAVE_OPENSSL_SSL_H + +#include "talk/base/opensslidentity.h" + +// Must be included first before openssl headers. +#include "talk/base/win32.h" // NOLINT + +#include +#include +#include +#include +#include +#include +#include + +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/openssldigest.h" + +namespace talk_base { + +// We could have exposed a myriad of parameters for the crypto stuff, +// but keeping it simple seems best. + +// Strength of generated keys. Those are RSA. +static const int KEY_LENGTH = 1024; + +// Random bits for certificate serial number +static const int SERIAL_RAND_BITS = 64; + +// Certificate validity lifetime +static const int CERTIFICATE_LIFETIME = 60*60*24*365; // one year, arbitrarily +// Certificate validity window. +// This is to compensate for slightly incorrect system clocks. +static const int CERTIFICATE_WINDOW = -60*60*24; + +// Generate a key pair. Caller is responsible for freeing the returned object. +static EVP_PKEY* MakeKey() { + LOG(LS_INFO) << "Making key pair"; + EVP_PKEY* pkey = EVP_PKEY_new(); +#if OPENSSL_VERSION_NUMBER < 0x00908000l + // Only RSA_generate_key is available. Use that. + RSA* rsa = RSA_generate_key(KEY_LENGTH, 0x10001, NULL, NULL); + if (!EVP_PKEY_assign_RSA(pkey, rsa)) { + EVP_PKEY_free(pkey); + RSA_free(rsa); + return NULL; + } +#else + // RSA_generate_key is deprecated. Use _ex version. + BIGNUM* exponent = BN_new(); + RSA* rsa = RSA_new(); + if (!pkey || !exponent || !rsa || + !BN_set_word(exponent, 0x10001) || // 65537 RSA exponent + !RSA_generate_key_ex(rsa, KEY_LENGTH, exponent, NULL) || + !EVP_PKEY_assign_RSA(pkey, rsa)) { + EVP_PKEY_free(pkey); + BN_free(exponent); + RSA_free(rsa); + return NULL; + } + // ownership of rsa struct was assigned, don't free it. + BN_free(exponent); +#endif + LOG(LS_INFO) << "Returning key pair"; + return pkey; +} + +// Generate a self-signed certificate, with the public key from the +// given key pair. Caller is responsible for freeing the returned object. +static X509* MakeCertificate(EVP_PKEY* pkey, const char* common_name) { + LOG(LS_INFO) << "Making certificate for " << common_name; + X509* x509 = NULL; + BIGNUM* serial_number = NULL; + X509_NAME* name = NULL; + + if ((x509=X509_new()) == NULL) + goto error; + + if (!X509_set_pubkey(x509, pkey)) + goto error; + + // serial number + // temporary reference to serial number inside x509 struct + ASN1_INTEGER* asn1_serial_number; + if ((serial_number = BN_new()) == NULL || + !BN_pseudo_rand(serial_number, SERIAL_RAND_BITS, 0, 0) || + (asn1_serial_number = X509_get_serialNumber(x509)) == NULL || + !BN_to_ASN1_INTEGER(serial_number, asn1_serial_number)) + goto error; + + if (!X509_set_version(x509, 0L)) // version 1 + goto error; + + // There are a lot of possible components for the name entries. In + // our P2P SSL mode however, the certificates are pre-exchanged + // (through the secure XMPP channel), and so the certificate + // identification is arbitrary. It can't be empty, so we set some + // arbitrary common_name. Note that this certificate goes out in + // clear during SSL negotiation, so there may be a privacy issue in + // putting anything recognizable here. + if ((name = X509_NAME_new()) == NULL || + !X509_NAME_add_entry_by_NID(name, NID_commonName, MBSTRING_UTF8, + (unsigned char*)common_name, -1, -1, 0) || + !X509_set_subject_name(x509, name) || + !X509_set_issuer_name(x509, name)) + goto error; + + if (!X509_gmtime_adj(X509_get_notBefore(x509), CERTIFICATE_WINDOW) || + !X509_gmtime_adj(X509_get_notAfter(x509), CERTIFICATE_LIFETIME)) + goto error; + + if (!X509_sign(x509, pkey, EVP_sha1())) + goto error; + + BN_free(serial_number); + X509_NAME_free(name); + LOG(LS_INFO) << "Returning certificate"; + return x509; + + error: + BN_free(serial_number); + X509_NAME_free(name); + X509_free(x509); + return NULL; +} + +// This dumps the SSL error stack to the log. +static void LogSSLErrors(const std::string& prefix) { + char error_buf[200]; + unsigned long err; + + while ((err = ERR_get_error()) != 0) { + ERR_error_string_n(err, error_buf, sizeof(error_buf)); + LOG(LS_ERROR) << prefix << ": " << error_buf << "\n"; + } +} + +OpenSSLKeyPair* OpenSSLKeyPair::Generate() { + EVP_PKEY* pkey = MakeKey(); + if (!pkey) { + LogSSLErrors("Generating key pair"); + return NULL; + } + return new OpenSSLKeyPair(pkey); +} + +OpenSSLKeyPair::~OpenSSLKeyPair() { + EVP_PKEY_free(pkey_); +} + +void OpenSSLKeyPair::AddReference() { + CRYPTO_add(&pkey_->references, 1, CRYPTO_LOCK_EVP_PKEY); +} + +#ifdef _DEBUG +// Print a certificate to the log, for debugging. +static void PrintCert(X509* x509) { + BIO* temp_memory_bio = BIO_new(BIO_s_mem()); + if (!temp_memory_bio) { + LOG_F(LS_ERROR) << "Failed to allocate temporary memory bio"; + return; + } + X509_print_ex(temp_memory_bio, x509, XN_FLAG_SEP_CPLUS_SPC, 0); + BIO_write(temp_memory_bio, "\0", 1); + char* buffer; + BIO_get_mem_data(temp_memory_bio, &buffer); + LOG(LS_VERBOSE) << buffer; + BIO_free(temp_memory_bio); +} +#endif + +OpenSSLCertificate* OpenSSLCertificate::Generate( + OpenSSLKeyPair* key_pair, const std::string& common_name) { + std::string actual_common_name = common_name; + if (actual_common_name.empty()) + // Use a random string, arbitrarily 8chars long. + actual_common_name = CreateRandomString(8); + X509* x509 = MakeCertificate(key_pair->pkey(), actual_common_name.c_str()); + if (!x509) { + LogSSLErrors("Generating certificate"); + return NULL; + } +#ifdef _DEBUG + PrintCert(x509); +#endif + return new OpenSSLCertificate(x509); +} + +OpenSSLCertificate* OpenSSLCertificate::FromPEMString( + const std::string& pem_string) { + BIO* bio = BIO_new_mem_buf(const_cast(pem_string.c_str()), -1); + if (!bio) + return NULL; + (void)BIO_set_close(bio, BIO_NOCLOSE); + BIO_set_mem_eof_return(bio, 0); + X509 *x509 = PEM_read_bio_X509(bio, NULL, NULL, + const_cast("\0")); + BIO_free(bio); + if (x509) + return new OpenSSLCertificate(x509); + else + return NULL; +} + +bool OpenSSLCertificate::ComputeDigest(const std::string &algorithm, + unsigned char *digest, + std::size_t size, + std::size_t *length) const { + return ComputeDigest(x509_, algorithm, digest, size, length); +} + +bool OpenSSLCertificate::ComputeDigest(const X509 *x509, + const std::string &algorithm, + unsigned char *digest, + std::size_t size, + std::size_t *length) { + const EVP_MD *md; + unsigned int n; + + if (!OpenSSLDigest::GetDigestEVP(algorithm, &md)) + return false; + + if (size < static_cast(EVP_MD_size(md))) + return false; + + X509_digest(x509, md, digest, &n); + + *length = n; + + return true; +} + +OpenSSLCertificate::~OpenSSLCertificate() { + X509_free(x509_); +} + +std::string OpenSSLCertificate::ToPEMString() const { + BIO* bio = BIO_new(BIO_s_mem()); + if (!bio) + return NULL; + if (!PEM_write_bio_X509(bio, x509_)) { + BIO_free(bio); + return NULL; + } + BIO_write(bio, "\0", 1); + char* buffer; + BIO_get_mem_data(bio, &buffer); + std::string ret(buffer); + BIO_free(bio); + return ret; +} + +void OpenSSLCertificate::AddReference() const { + CRYPTO_add(&x509_->references, 1, CRYPTO_LOCK_X509); +} + +OpenSSLIdentity* OpenSSLIdentity::Generate(const std::string& common_name) { + OpenSSLKeyPair *key_pair = OpenSSLKeyPair::Generate(); + if (key_pair) { + OpenSSLCertificate *certificate = + OpenSSLCertificate::Generate(key_pair, common_name); + if (certificate) + return new OpenSSLIdentity(key_pair, certificate); + delete key_pair; + } + LOG(LS_INFO) << "Identity generation failed"; + return NULL; +} + +SSLIdentity* OpenSSLIdentity::FromPEMStrings( + const std::string& private_key, + const std::string& certificate) { + scoped_ptr cert( + OpenSSLCertificate::FromPEMString(certificate)); + if (!cert) { + LOG(LS_ERROR) << "Failed to create OpenSSLCertificate from PEM string."; + return NULL; + } + + BIO* bio = BIO_new_mem_buf(const_cast(private_key.c_str()), -1); + if (!bio) { + LOG(LS_ERROR) << "Failed to create a new BIO buffer."; + return NULL; + } + (void)BIO_set_close(bio, BIO_NOCLOSE); + BIO_set_mem_eof_return(bio, 0); + EVP_PKEY *pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, + const_cast("\0")); + BIO_free(bio); + + if (!pkey) { + LOG(LS_ERROR) << "Failed to create the private key from PEM string."; + return NULL; + } + + return new OpenSSLIdentity(new OpenSSLKeyPair(pkey), + cert.release()); +} + +bool OpenSSLIdentity::ConfigureIdentity(SSL_CTX* ctx) { + // 1 is the documented success return code. + if (SSL_CTX_use_certificate(ctx, certificate_->x509()) != 1 || + SSL_CTX_use_PrivateKey(ctx, key_pair_->pkey()) != 1) { + LogSSLErrors("Configuring key and certificate"); + return false; + } + return true; +} + +} // namespace talk_base + +#endif // HAVE_OPENSSL_SSL_H + + diff --git a/talk/base/opensslidentity.h b/talk/base/opensslidentity.h new file mode 100644 index 000000000..ca001b5cf --- /dev/null +++ b/talk/base/opensslidentity.h @@ -0,0 +1,151 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +#ifndef TALK_BASE_OPENSSLIDENTITY_H__ +#define TALK_BASE_OPENSSLIDENTITY_H__ + +#include +#include + +#include + +#include "talk/base/common.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sslidentity.h" + +typedef struct ssl_ctx_st SSL_CTX; + +namespace talk_base { + +// OpenSSLKeyPair encapsulates an OpenSSL EVP_PKEY* keypair object, +// which is reference counted inside the OpenSSL library. +class OpenSSLKeyPair { + public: + explicit OpenSSLKeyPair(EVP_PKEY* pkey) : pkey_(pkey) { + ASSERT(pkey_ != NULL); + } + + static OpenSSLKeyPair* Generate(); + + virtual ~OpenSSLKeyPair(); + + virtual OpenSSLKeyPair* GetReference() { + AddReference(); + return new OpenSSLKeyPair(pkey_); + } + + EVP_PKEY* pkey() const { return pkey_; } + + private: + void AddReference(); + + EVP_PKEY* pkey_; + + DISALLOW_EVIL_CONSTRUCTORS(OpenSSLKeyPair); +}; + +// OpenSSLCertificate encapsulates an OpenSSL X509* certificate object, +// which is also reference counted inside the OpenSSL library. +class OpenSSLCertificate : public SSLCertificate { + public: + static OpenSSLCertificate* Generate(OpenSSLKeyPair* key_pair, + const std::string& common_name); + static OpenSSLCertificate* FromPEMString(const std::string& pem_string); + + virtual ~OpenSSLCertificate(); + + virtual OpenSSLCertificate* GetReference() const { + AddReference(); + return new OpenSSLCertificate(x509_); + } + + X509* x509() const { return x509_; } + + virtual std::string ToPEMString() const; + + // Compute the digest of the certificate given algorithm + virtual bool ComputeDigest(const std::string &algorithm, + unsigned char *digest, std::size_t size, + std::size_t *length) const; + + // Compute the digest of a certificate as an X509 * + static bool ComputeDigest(const X509 *x509, + const std::string &algorithm, + unsigned char *digest, + std::size_t size, + std::size_t *length); + + private: + explicit OpenSSLCertificate(X509* x509) : x509_(x509) { + ASSERT(x509_ != NULL); + } + void AddReference() const; + + X509* x509_; + + DISALLOW_EVIL_CONSTRUCTORS(OpenSSLCertificate); +}; + +// Holds a keypair and certificate together, and a method to generate +// them consistently. +class OpenSSLIdentity : public SSLIdentity { + public: + static OpenSSLIdentity* Generate(const std::string& common_name); + static SSLIdentity* FromPEMStrings(const std::string& private_key, + const std::string& certificate); + virtual ~OpenSSLIdentity() { } + + virtual const OpenSSLCertificate& certificate() const { + return *certificate_; + } + + virtual OpenSSLIdentity* GetReference() const { + return new OpenSSLIdentity(key_pair_->GetReference(), + certificate_->GetReference()); + } + + // Configure an SSL context object to use our key and certificate. + bool ConfigureIdentity(SSL_CTX* ctx); + + private: + OpenSSLIdentity(OpenSSLKeyPair* key_pair, + OpenSSLCertificate* certificate) + : key_pair_(key_pair), certificate_(certificate) { + ASSERT(key_pair != NULL); + ASSERT(certificate != NULL); + } + + scoped_ptr key_pair_; + scoped_ptr certificate_; + + DISALLOW_EVIL_CONSTRUCTORS(OpenSSLIdentity); +}; + + +} // namespace talk_base + +#endif // TALK_BASE_OPENSSLIDENTITY_H__ diff --git a/talk/base/opensslstreamadapter.cc b/talk/base/opensslstreamadapter.cc new file mode 100644 index 000000000..16021a96b --- /dev/null +++ b/talk/base/opensslstreamadapter.cc @@ -0,0 +1,940 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +#if HAVE_CONFIG_H +#include "config.h" +#endif // HAVE_CONFIG_H + +#if HAVE_OPENSSL_SSL_H + +#include "talk/base/opensslstreamadapter.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/stream.h" +#include "talk/base/openssladapter.h" +#include "talk/base/openssldigest.h" +#include "talk/base/opensslidentity.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" + +namespace talk_base { + +#if (OPENSSL_VERSION_NUMBER >= 0x10001000L) +#define HAVE_DTLS_SRTP +#endif + +#if (OPENSSL_VERSION_NUMBER >= 0x10000000L) +#define HAVE_DTLS +#endif + +#ifdef HAVE_DTLS_SRTP +// SRTP cipher suite table +struct SrtpCipherMapEntry { + const char* external_name; + const char* internal_name; +}; + +// This isn't elegant, but it's better than an external reference +static SrtpCipherMapEntry SrtpCipherMap[] = { + {"AES_CM_128_HMAC_SHA1_80", "SRTP_AES128_CM_SHA1_80"}, + {"AES_CM_128_HMAC_SHA1_32", "SRTP_AES128_CM_SHA1_32"}, + {NULL, NULL} +}; +#endif + +////////////////////////////////////////////////////////////////////// +// StreamBIO +////////////////////////////////////////////////////////////////////// + +static int stream_write(BIO* h, const char* buf, int num); +static int stream_read(BIO* h, char* buf, int size); +static int stream_puts(BIO* h, const char* str); +static long stream_ctrl(BIO* h, int cmd, long arg1, void* arg2); +static int stream_new(BIO* h); +static int stream_free(BIO* data); + +static BIO_METHOD methods_stream = { + BIO_TYPE_BIO, + "stream", + stream_write, + stream_read, + stream_puts, + 0, + stream_ctrl, + stream_new, + stream_free, + NULL, +}; + +static BIO_METHOD* BIO_s_stream() { return(&methods_stream); } + +static BIO* BIO_new_stream(StreamInterface* stream) { + BIO* ret = BIO_new(BIO_s_stream()); + if (ret == NULL) + return NULL; + ret->ptr = stream; + return ret; +} + +// bio methods return 1 (or at least non-zero) on success and 0 on failure. + +static int stream_new(BIO* b) { + b->shutdown = 0; + b->init = 1; + b->num = 0; // 1 means end-of-stream + b->ptr = 0; + return 1; +} + +static int stream_free(BIO* b) { + if (b == NULL) + return 0; + return 1; +} + +static int stream_read(BIO* b, char* out, int outl) { + if (!out) + return -1; + StreamInterface* stream = static_cast(b->ptr); + BIO_clear_retry_flags(b); + size_t read; + int error; + StreamResult result = stream->Read(out, outl, &read, &error); + if (result == SR_SUCCESS) { + return read; + } else if (result == SR_EOS) { + b->num = 1; + } else if (result == SR_BLOCK) { + BIO_set_retry_read(b); + } + return -1; +} + +static int stream_write(BIO* b, const char* in, int inl) { + if (!in) + return -1; + StreamInterface* stream = static_cast(b->ptr); + BIO_clear_retry_flags(b); + size_t written; + int error; + StreamResult result = stream->Write(in, inl, &written, &error); + if (result == SR_SUCCESS) { + return written; + } else if (result == SR_BLOCK) { + BIO_set_retry_write(b); + } + return -1; +} + +static int stream_puts(BIO* b, const char* str) { + return stream_write(b, str, strlen(str)); +} + +static long stream_ctrl(BIO* b, int cmd, long num, void* ptr) { + UNUSED(num); + UNUSED(ptr); + + switch (cmd) { + case BIO_CTRL_RESET: + return 0; + case BIO_CTRL_EOF: + return b->num; + case BIO_CTRL_WPENDING: + case BIO_CTRL_PENDING: + return 0; + case BIO_CTRL_FLUSH: + return 1; + default: + return 0; + } +} + +///////////////////////////////////////////////////////////////////////////// +// OpenSSLStreamAdapter +///////////////////////////////////////////////////////////////////////////// + +OpenSSLStreamAdapter::OpenSSLStreamAdapter(StreamInterface* stream) + : SSLStreamAdapter(stream), + state_(SSL_NONE), + role_(SSL_CLIENT), + ssl_read_needs_write_(false), ssl_write_needs_read_(false), + ssl_(NULL), ssl_ctx_(NULL), + custom_verification_succeeded_(false), + ssl_mode_(SSL_MODE_TLS) { +} + +OpenSSLStreamAdapter::~OpenSSLStreamAdapter() { + Cleanup(); +} + +void OpenSSLStreamAdapter::SetIdentity(SSLIdentity* identity) { + ASSERT(!identity_); + identity_.reset(static_cast(identity)); +} + +void OpenSSLStreamAdapter::SetServerRole(SSLRole role) { + role_ = role; +} + +void OpenSSLStreamAdapter::SetPeerCertificate(SSLCertificate* cert) { + ASSERT(!peer_certificate_); + ASSERT(peer_certificate_digest_algorithm_.empty()); + ASSERT(ssl_server_name_.empty()); + peer_certificate_.reset(static_cast(cert)); +} + +bool OpenSSLStreamAdapter::SetPeerCertificateDigest(const std::string + &digest_alg, + const unsigned char* + digest_val, + size_t digest_len) { + ASSERT(!peer_certificate_); + ASSERT(peer_certificate_digest_algorithm_.size() == 0); + ASSERT(ssl_server_name_.empty()); + size_t expected_len; + + if (!OpenSSLDigest::GetDigestSize(digest_alg, &expected_len)) { + LOG(LS_WARNING) << "Unknown digest algorithm: " << digest_alg; + return false; + } + if (expected_len != digest_len) + return false; + + peer_certificate_digest_value_.SetData(digest_val, digest_len); + peer_certificate_digest_algorithm_ = digest_alg; + + return true; +} + +// Key Extractor interface +bool OpenSSLStreamAdapter::ExportKeyingMaterial(const std::string& label, + const uint8* context, + size_t context_len, + bool use_context, + uint8* result, + size_t result_len) { +#ifdef HAVE_DTLS_SRTP + int i; + + i = SSL_export_keying_material(ssl_, result, result_len, + label.c_str(), label.length(), + const_cast(context), + context_len, use_context); + + if (i != 1) + return false; + + return true; +#else + return false; +#endif +} + +bool OpenSSLStreamAdapter::SetDtlsSrtpCiphers( + const std::vector& ciphers) { + std::string internal_ciphers; + + if (state_ != SSL_NONE) + return false; + +#ifdef HAVE_DTLS_SRTP + for (std::vector::const_iterator cipher = ciphers.begin(); + cipher != ciphers.end(); ++cipher) { + bool found = false; + for (SrtpCipherMapEntry *entry = SrtpCipherMap; entry->internal_name; + ++entry) { + if (*cipher == entry->external_name) { + found = true; + if (!internal_ciphers.empty()) + internal_ciphers += ":"; + internal_ciphers += entry->internal_name; + break; + } + } + + if (!found) { + LOG(LS_ERROR) << "Could not find cipher: " << *cipher; + return false; + } + } + + if (internal_ciphers.empty()) + return false; + + srtp_ciphers_ = internal_ciphers; + return true; +#else + return false; +#endif +} + +bool OpenSSLStreamAdapter::GetDtlsSrtpCipher(std::string* cipher) { +#ifdef HAVE_DTLS_SRTP + ASSERT(state_ == SSL_CONNECTED); + if (state_ != SSL_CONNECTED) + return false; + + SRTP_PROTECTION_PROFILE *srtp_profile = + SSL_get_selected_srtp_profile(ssl_); + + if (!srtp_profile) + return false; + + for (SrtpCipherMapEntry *entry = SrtpCipherMap; + entry->internal_name; ++entry) { + if (!strcmp(entry->internal_name, srtp_profile->name)) { + *cipher = entry->external_name; + return true; + } + } + + ASSERT(false); // This should never happen + + return false; +#else + return false; +#endif +} + +int OpenSSLStreamAdapter::StartSSLWithServer(const char* server_name) { + ASSERT(server_name != NULL && server_name[0] != '\0'); + ssl_server_name_ = server_name; + return StartSSL(); +} + +int OpenSSLStreamAdapter::StartSSLWithPeer() { + ASSERT(ssl_server_name_.empty()); + // It is permitted to specify peer_certificate_ only later. + return StartSSL(); +} + +void OpenSSLStreamAdapter::SetMode(SSLMode mode) { + ASSERT(state_ == SSL_NONE); + ssl_mode_ = mode; +} + +// +// StreamInterface Implementation +// + +StreamResult OpenSSLStreamAdapter::Write(const void* data, size_t data_len, + size_t* written, int* error) { + LOG(LS_VERBOSE) << "OpenSSLStreamAdapter::Write(" << data_len << ")"; + + switch (state_) { + case SSL_NONE: + // pass-through in clear text + return StreamAdapterInterface::Write(data, data_len, written, error); + + case SSL_WAIT: + case SSL_CONNECTING: + return SR_BLOCK; + + case SSL_CONNECTED: + break; + + case SSL_ERROR: + case SSL_CLOSED: + default: + if (error) + *error = ssl_error_code_; + return SR_ERROR; + } + + // OpenSSL will return an error if we try to write zero bytes + if (data_len == 0) { + if (written) + *written = 0; + return SR_SUCCESS; + } + + ssl_write_needs_read_ = false; + + int code = SSL_write(ssl_, data, data_len); + int ssl_error = SSL_get_error(ssl_, code); + switch (ssl_error) { + case SSL_ERROR_NONE: + LOG(LS_VERBOSE) << " -- success"; + ASSERT(0 < code && static_cast(code) <= data_len); + if (written) + *written = code; + return SR_SUCCESS; + case SSL_ERROR_WANT_READ: + LOG(LS_VERBOSE) << " -- error want read"; + ssl_write_needs_read_ = true; + return SR_BLOCK; + case SSL_ERROR_WANT_WRITE: + LOG(LS_VERBOSE) << " -- error want write"; + return SR_BLOCK; + + case SSL_ERROR_ZERO_RETURN: + default: + Error("SSL_write", (ssl_error ? ssl_error : -1), false); + if (error) + *error = ssl_error_code_; + return SR_ERROR; + } + // not reached +} + +StreamResult OpenSSLStreamAdapter::Read(void* data, size_t data_len, + size_t* read, int* error) { + LOG(LS_VERBOSE) << "OpenSSLStreamAdapter::Read(" << data_len << ")"; + switch (state_) { + case SSL_NONE: + // pass-through in clear text + return StreamAdapterInterface::Read(data, data_len, read, error); + + case SSL_WAIT: + case SSL_CONNECTING: + return SR_BLOCK; + + case SSL_CONNECTED: + break; + + case SSL_CLOSED: + return SR_EOS; + + case SSL_ERROR: + default: + if (error) + *error = ssl_error_code_; + return SR_ERROR; + } + + // Don't trust OpenSSL with zero byte reads + if (data_len == 0) { + if (read) + *read = 0; + return SR_SUCCESS; + } + + ssl_read_needs_write_ = false; + + int code = SSL_read(ssl_, data, data_len); + int ssl_error = SSL_get_error(ssl_, code); + switch (ssl_error) { + case SSL_ERROR_NONE: + LOG(LS_VERBOSE) << " -- success"; + ASSERT(0 < code && static_cast(code) <= data_len); + if (read) + *read = code; + + if (ssl_mode_ == SSL_MODE_DTLS) { + // Enforce atomic reads -- this is a short read + unsigned int pending = SSL_pending(ssl_); + + if (pending) { + LOG(LS_INFO) << " -- short DTLS read. flushing"; + FlushInput(pending); + if (error) + *error = SSE_MSG_TRUNC; + return SR_ERROR; + } + } + return SR_SUCCESS; + case SSL_ERROR_WANT_READ: + LOG(LS_VERBOSE) << " -- error want read"; + return SR_BLOCK; + case SSL_ERROR_WANT_WRITE: + LOG(LS_VERBOSE) << " -- error want write"; + ssl_read_needs_write_ = true; + return SR_BLOCK; + case SSL_ERROR_ZERO_RETURN: + LOG(LS_VERBOSE) << " -- remote side closed"; + return SR_EOS; + break; + default: + LOG(LS_VERBOSE) << " -- error " << code; + Error("SSL_read", (ssl_error ? ssl_error : -1), false); + if (error) + *error = ssl_error_code_; + return SR_ERROR; + } + // not reached +} + +void OpenSSLStreamAdapter::FlushInput(unsigned int left) { + unsigned char buf[2048]; + + while (left) { + // This should always succeed + int toread = (sizeof(buf) < left) ? sizeof(buf) : left; + int code = SSL_read(ssl_, buf, toread); + + int ssl_error = SSL_get_error(ssl_, code); + ASSERT(ssl_error == SSL_ERROR_NONE); + + if (ssl_error != SSL_ERROR_NONE) { + LOG(LS_VERBOSE) << " -- error " << code; + Error("SSL_read", (ssl_error ? ssl_error : -1), false); + return; + } + + LOG(LS_VERBOSE) << " -- flushed " << code << " bytes"; + left -= code; + } +} + +void OpenSSLStreamAdapter::Close() { + Cleanup(); + ASSERT(state_ == SSL_CLOSED || state_ == SSL_ERROR); + StreamAdapterInterface::Close(); +} + +StreamState OpenSSLStreamAdapter::GetState() const { + switch (state_) { + case SSL_WAIT: + case SSL_CONNECTING: + return SS_OPENING; + case SSL_CONNECTED: + return SS_OPEN; + default: + return SS_CLOSED; + }; + // not reached +} + +void OpenSSLStreamAdapter::OnEvent(StreamInterface* stream, int events, + int err) { + int events_to_signal = 0; + int signal_error = 0; + ASSERT(stream == this->stream()); + if ((events & SE_OPEN)) { + LOG(LS_VERBOSE) << "OpenSSLStreamAdapter::OnEvent SE_OPEN"; + if (state_ != SSL_WAIT) { + ASSERT(state_ == SSL_NONE); + events_to_signal |= SE_OPEN; + } else { + state_ = SSL_CONNECTING; + if (int err = BeginSSL()) { + Error("BeginSSL", err, true); + return; + } + } + } + if ((events & (SE_READ|SE_WRITE))) { + LOG(LS_VERBOSE) << "OpenSSLStreamAdapter::OnEvent" + << ((events & SE_READ) ? " SE_READ" : "") + << ((events & SE_WRITE) ? " SE_WRITE" : ""); + if (state_ == SSL_NONE) { + events_to_signal |= events & (SE_READ|SE_WRITE); + } else if (state_ == SSL_CONNECTING) { + if (int err = ContinueSSL()) { + Error("ContinueSSL", err, true); + return; + } + } else if (state_ == SSL_CONNECTED) { + if (((events & SE_READ) && ssl_write_needs_read_) || + (events & SE_WRITE)) { + LOG(LS_VERBOSE) << " -- onStreamWriteable"; + events_to_signal |= SE_WRITE; + } + if (((events & SE_WRITE) && ssl_read_needs_write_) || + (events & SE_READ)) { + LOG(LS_VERBOSE) << " -- onStreamReadable"; + events_to_signal |= SE_READ; + } + } + } + if ((events & SE_CLOSE)) { + LOG(LS_VERBOSE) << "OpenSSLStreamAdapter::OnEvent(SE_CLOSE, " << err << ")"; + Cleanup(); + events_to_signal |= SE_CLOSE; + // SE_CLOSE is the only event that uses the final parameter to OnEvent(). + ASSERT(signal_error == 0); + signal_error = err; + } + if (events_to_signal) + StreamAdapterInterface::OnEvent(stream, events_to_signal, signal_error); +} + +int OpenSSLStreamAdapter::StartSSL() { + ASSERT(state_ == SSL_NONE); + + if (StreamAdapterInterface::GetState() != SS_OPEN) { + state_ = SSL_WAIT; + return 0; + } + + state_ = SSL_CONNECTING; + if (int err = BeginSSL()) { + Error("BeginSSL", err, false); + return err; + } + + return 0; +} + +int OpenSSLStreamAdapter::BeginSSL() { + ASSERT(state_ == SSL_CONNECTING); + // The underlying stream has open. If we are in peer-to-peer mode + // then a peer certificate must have been specified by now. + ASSERT(!ssl_server_name_.empty() || + peer_certificate_ || + !peer_certificate_digest_algorithm_.empty()); + LOG(LS_INFO) << "BeginSSL: " + << (!ssl_server_name_.empty() ? ssl_server_name_ : + "with peer"); + + BIO* bio = NULL; + + // First set up the context + ASSERT(ssl_ctx_ == NULL); + ssl_ctx_ = SetupSSLContext(); + if (!ssl_ctx_) + return -1; + + bio = BIO_new_stream(static_cast(stream())); + if (!bio) + return -1; + + ssl_ = SSL_new(ssl_ctx_); + if (!ssl_) { + BIO_free(bio); + return -1; + } + + SSL_set_app_data(ssl_, this); + + SSL_set_bio(ssl_, bio, bio); // the SSL object owns the bio now. + + SSL_set_mode(ssl_, SSL_MODE_ENABLE_PARTIAL_WRITE | + SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + + // Do the connect + return ContinueSSL(); +} + +int OpenSSLStreamAdapter::ContinueSSL() { + LOG(LS_VERBOSE) << "ContinueSSL"; + ASSERT(state_ == SSL_CONNECTING); + + // Clear the DTLS timer + Thread::Current()->Clear(this, MSG_TIMEOUT); + + int code = (role_ == SSL_CLIENT) ? SSL_connect(ssl_) : SSL_accept(ssl_); + int ssl_error; + switch (ssl_error = SSL_get_error(ssl_, code)) { + case SSL_ERROR_NONE: + LOG(LS_VERBOSE) << " -- success"; + + if (!SSLPostConnectionCheck(ssl_, ssl_server_name_.c_str(), + peer_certificate_ ? + peer_certificate_->x509() : NULL, + peer_certificate_digest_algorithm_)) { + LOG(LS_ERROR) << "TLS post connection check failed"; + return -1; + } + + state_ = SSL_CONNECTED; + StreamAdapterInterface::OnEvent(stream(), SE_OPEN|SE_READ|SE_WRITE, 0); + break; + + case SSL_ERROR_WANT_READ: { + LOG(LS_VERBOSE) << " -- error want read"; +#ifdef HAVE_DTLS + struct timeval timeout; + if (DTLSv1_get_timeout(ssl_, &timeout)) { + int delay = timeout.tv_sec * 1000 + timeout.tv_usec/1000; + + Thread::Current()->PostDelayed(delay, this, MSG_TIMEOUT, 0); + } +#endif + } + break; + + case SSL_ERROR_WANT_WRITE: + LOG(LS_VERBOSE) << " -- error want write"; + break; + + case SSL_ERROR_ZERO_RETURN: + default: + LOG(LS_VERBOSE) << " -- error " << code; + return (ssl_error != 0) ? ssl_error : -1; + } + + return 0; +} + +void OpenSSLStreamAdapter::Error(const char* context, int err, bool signal) { + LOG(LS_WARNING) << "OpenSSLStreamAdapter::Error(" + << context << ", " << err << ")"; + state_ = SSL_ERROR; + ssl_error_code_ = err; + Cleanup(); + if (signal) + StreamAdapterInterface::OnEvent(stream(), SE_CLOSE, err); +} + +void OpenSSLStreamAdapter::Cleanup() { + LOG(LS_INFO) << "Cleanup"; + + if (state_ != SSL_ERROR) { + state_ = SSL_CLOSED; + ssl_error_code_ = 0; + } + + if (ssl_) { + SSL_free(ssl_); + ssl_ = NULL; + } + if (ssl_ctx_) { + SSL_CTX_free(ssl_ctx_); + ssl_ctx_ = NULL; + } + identity_.reset(); + peer_certificate_.reset(); + + // Clear the DTLS timer + Thread::Current()->Clear(this, MSG_TIMEOUT); +} + + +void OpenSSLStreamAdapter::OnMessage(Message* msg) { + // Process our own messages and then pass others to the superclass + if (MSG_TIMEOUT == msg->message_id) { + LOG(LS_INFO) << "DTLS timeout expired"; +#ifdef HAVE_DTLS + DTLSv1_handle_timeout(ssl_); +#endif + ContinueSSL(); + } else { + StreamInterface::OnMessage(msg); + } +} + +SSL_CTX* OpenSSLStreamAdapter::SetupSSLContext() { + SSL_CTX *ctx = NULL; + + if (role_ == SSL_CLIENT) { +#ifdef HAVE_DTLS + ctx = SSL_CTX_new(ssl_mode_ == SSL_MODE_DTLS ? + DTLSv1_client_method() : TLSv1_client_method()); +#else + ctx = SSL_CTX_new(TLSv1_client_method()); +#endif + } else { +#ifdef HAVE_DTLS + ctx = SSL_CTX_new(ssl_mode_ == SSL_MODE_DTLS ? + DTLSv1_server_method() : TLSv1_server_method()); +#else + ctx = SSL_CTX_new(TLSv1_server_method()); +#endif + } + if (ctx == NULL) + return NULL; + + if (identity_ && !identity_->ConfigureIdentity(ctx)) { + SSL_CTX_free(ctx); + return NULL; + } + + if (!peer_certificate_) { // traditional mode + // Add the root cert to the SSL context + if (!OpenSSLAdapter::ConfigureTrustedRootCertificates(ctx)) { + SSL_CTX_free(ctx); + return NULL; + } + } + + if (peer_certificate_ && role_ == SSL_SERVER) + // we must specify which client cert to ask for + SSL_CTX_add_client_CA(ctx, peer_certificate_->x509()); + +#ifdef _DEBUG + SSL_CTX_set_info_callback(ctx, OpenSSLAdapter::SSLInfoCallback); +#endif + + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER |SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + SSLVerifyCallback); + SSL_CTX_set_verify_depth(ctx, 4); + SSL_CTX_set_cipher_list(ctx, "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"); + +#ifdef HAVE_DTLS_SRTP + if (!srtp_ciphers_.empty()) { + if (SSL_CTX_set_tlsext_use_srtp(ctx, srtp_ciphers_.c_str())) { + SSL_CTX_free(ctx); + return NULL; + } + } +#endif + + return ctx; +} + +int OpenSSLStreamAdapter::SSLVerifyCallback(int ok, X509_STORE_CTX* store) { +#if _DEBUG + if (!ok) { + char data[256]; + X509* cert = X509_STORE_CTX_get_current_cert(store); + int depth = X509_STORE_CTX_get_error_depth(store); + int err = X509_STORE_CTX_get_error(store); + + LOG(LS_INFO) << "Error with certificate at depth: " << depth; + X509_NAME_oneline(X509_get_issuer_name(cert), data, sizeof(data)); + LOG(LS_INFO) << " issuer = " << data; + X509_NAME_oneline(X509_get_subject_name(cert), data, sizeof(data)); + LOG(LS_INFO) << " subject = " << data; + LOG(LS_INFO) << " err = " << err + << ":" << X509_verify_cert_error_string(err); + } +#endif + + // Get our SSL structure from the store + SSL* ssl = reinterpret_cast(X509_STORE_CTX_get_ex_data( + store, + SSL_get_ex_data_X509_STORE_CTX_idx())); + + OpenSSLStreamAdapter* stream = + reinterpret_cast(SSL_get_app_data(ssl)); + + // In peer-to-peer mode, no root cert / certificate authority was + // specified, so the libraries knows of no certificate to accept, + // and therefore it will necessarily call here on the first cert it + // tries to verify. + if (!ok && stream->peer_certificate_) { + X509* cert = X509_STORE_CTX_get_current_cert(store); + int err = X509_STORE_CTX_get_error(store); + // peer-to-peer mode: allow the certificate to be self-signed, + // assuming it matches the cert that was specified. + if (err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && + X509_cmp(cert, stream->peer_certificate_->x509()) == 0) { + LOG(LS_INFO) << "Accepted self-signed peer certificate authority"; + ok = 1; + } + } else if (!ok && !stream->peer_certificate_digest_algorithm_.empty()) { + X509* cert = X509_STORE_CTX_get_current_cert(store); + int err = X509_STORE_CTX_get_error(store); + + // peer-to-peer mode: allow the certificate to be self-signed, + // assuming it matches the digest that was specified. + if (err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) { + unsigned char digest[EVP_MAX_MD_SIZE]; + std::size_t digest_length; + + if (OpenSSLCertificate:: + ComputeDigest(cert, + stream->peer_certificate_digest_algorithm_, + digest, sizeof(digest), + &digest_length)) { + Buffer computed_digest(digest, digest_length); + if (computed_digest == stream->peer_certificate_digest_value_) { + LOG(LS_INFO) << + "Accepted self-signed peer certificate authority"; + ok = 1; + } + } + } + } else if (!ok && OpenSSLAdapter::custom_verify_callback_) { + // this applies only in traditional mode + void* cert = + reinterpret_cast(X509_STORE_CTX_get_current_cert(store)); + if (OpenSSLAdapter::custom_verify_callback_(cert)) { + stream->custom_verification_succeeded_ = true; + LOG(LS_INFO) << "validated certificate using custom callback"; + ok = 1; + } + } + + if (!ok && stream->ignore_bad_cert()) { + LOG(LS_WARNING) << "Ignoring cert error while verifying cert chain"; + ok = 1; + } + + return ok; +} + +// This code is taken from the "Network Security with OpenSSL" +// sample in chapter 5 +bool OpenSSLStreamAdapter::SSLPostConnectionCheck(SSL* ssl, + const char* server_name, + const X509* peer_cert, + const std::string + &peer_digest) { + ASSERT(server_name != NULL); + bool ok; + if (server_name[0] != '\0') { // traditional mode + ok = OpenSSLAdapter::VerifyServerName(ssl, server_name, ignore_bad_cert()); + + if (ok) { + ok = (SSL_get_verify_result(ssl) == X509_V_OK || + custom_verification_succeeded_); + } + } else { // peer-to-peer mode + ASSERT((peer_cert != NULL) || (!peer_digest.empty())); + // no server name validation + ok = true; + } + + if (!ok && ignore_bad_cert()) { + LOG(LS_ERROR) << "SSL_get_verify_result(ssl) = " + << SSL_get_verify_result(ssl); + LOG(LS_INFO) << "Other TLS post connection checks failed."; + ok = true; + } + + return ok; +} + +bool OpenSSLStreamAdapter::HaveDtls() { +#ifdef HAVE_DTLS + return true; +#else + return false; +#endif +} + +bool OpenSSLStreamAdapter::HaveDtlsSrtp() { +#ifdef HAVE_DTLS_SRTP + return true; +#else + return false; +#endif +} + +bool OpenSSLStreamAdapter::HaveExporter() { +#ifdef HAVE_DTLS_SRTP + return true; +#else + return false; +#endif +} + +} // namespace talk_base + +#endif // HAVE_OPENSSL_SSL_H diff --git a/talk/base/opensslstreamadapter.h b/talk/base/opensslstreamadapter.h new file mode 100644 index 000000000..8e92a10a5 --- /dev/null +++ b/talk/base/opensslstreamadapter.h @@ -0,0 +1,215 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +#ifndef TALK_BASE_OPENSSLSTREAMADAPTER_H__ +#define TALK_BASE_OPENSSLSTREAMADAPTER_H__ + +#include +#include + +#include "talk/base/buffer.h" +#include "talk/base/sslstreamadapter.h" +#include "talk/base/opensslidentity.h" + +typedef struct ssl_st SSL; +typedef struct ssl_ctx_st SSL_CTX; +typedef struct x509_store_ctx_st X509_STORE_CTX; + +namespace talk_base { + +// This class was written with OpenSSLAdapter (a socket adapter) as a +// starting point. It has similar structure and functionality, with +// the peer-to-peer mode added. +// +// Static methods to initialize and deinit the SSL library are in +// OpenSSLAdapter. This class also uses +// OpenSSLAdapter::custom_verify_callback_ (a static field). These +// should probably be moved out to a neutral class. +// +// In a few cases I have factored out some OpenSSLAdapter code into +// static methods so it can be reused from this class. Eventually that +// code should probably be moved to a common support +// class. Unfortunately there remain a few duplicated sections of +// code. I have not done more restructuring because I did not want to +// affect existing code that uses OpenSSLAdapter. +// +// This class does not support the SSL connection restart feature +// present in OpenSSLAdapter. I am not entirely sure how the feature +// is useful and I am not convinced that it works properly. +// +// This implementation is careful to disallow data exchange after an +// SSL error, and it has an explicit SSL_CLOSED state. It should not +// be possible to send any data in clear after one of the StartSSL +// methods has been called. + +// Look in sslstreamadapter.h for documentation of the methods. + +class OpenSSLIdentity; + +/////////////////////////////////////////////////////////////////////////////// + +class OpenSSLStreamAdapter : public SSLStreamAdapter { + public: + explicit OpenSSLStreamAdapter(StreamInterface* stream); + virtual ~OpenSSLStreamAdapter(); + + virtual void SetIdentity(SSLIdentity* identity); + + // Default argument is for compatibility + virtual void SetServerRole(SSLRole role = SSL_SERVER); + virtual void SetPeerCertificate(SSLCertificate* cert); + virtual bool SetPeerCertificateDigest(const std::string& digest_alg, + const unsigned char* digest_val, + size_t digest_len); + + virtual int StartSSLWithServer(const char* server_name); + virtual int StartSSLWithPeer(); + virtual void SetMode(SSLMode mode); + + virtual StreamResult Read(void* data, size_t data_len, + size_t* read, int* error); + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + virtual void Close(); + virtual StreamState GetState() const; + + // Key Extractor interface + virtual bool ExportKeyingMaterial(const std::string& label, + const uint8* context, + size_t context_len, + bool use_context, + uint8* result, + size_t result_len); + + + // DTLS-SRTP interface + virtual bool SetDtlsSrtpCiphers(const std::vector& ciphers); + virtual bool GetDtlsSrtpCipher(std::string* cipher); + + // Capabilities interfaces + static bool HaveDtls(); + static bool HaveDtlsSrtp(); + static bool HaveExporter(); + + protected: + virtual void OnEvent(StreamInterface* stream, int events, int err); + + private: + enum SSLState { + // Before calling one of the StartSSL methods, data flows + // in clear text. + SSL_NONE, + SSL_WAIT, // waiting for the stream to open to start SSL negotiation + SSL_CONNECTING, // SSL negotiation in progress + SSL_CONNECTED, // SSL stream successfully established + SSL_ERROR, // some SSL error occurred, stream is closed + SSL_CLOSED // Clean close + }; + + enum { MSG_TIMEOUT = MSG_MAX+1}; + + // The following three methods return 0 on success and a negative + // error code on failure. The error code may be from OpenSSL or -1 + // on some other error cases, so it can't really be interpreted + // unfortunately. + + // Go from state SSL_NONE to either SSL_CONNECTING or SSL_WAIT, + // depending on whether the underlying stream is already open or + // not. + int StartSSL(); + // Prepare SSL library, state is SSL_CONNECTING. + int BeginSSL(); + // Perform SSL negotiation steps. + int ContinueSSL(); + + // Error handler helper. signal is given as true for errors in + // asynchronous contexts (when an error method was not returned + // through some other method), and in that case an SE_CLOSE event is + // raised on the stream with the specified error. + // A 0 error means a graceful close, otherwise there is not really enough + // context to interpret the error code. + void Error(const char* context, int err, bool signal); + void Cleanup(); + + // Override MessageHandler + virtual void OnMessage(Message* msg); + + // Flush the input buffers by reading left bytes (for DTLS) + void FlushInput(unsigned int left); + + // SSL library configuration + SSL_CTX* SetupSSLContext(); + // SSL verification check + bool SSLPostConnectionCheck(SSL* ssl, const char* server_name, + const X509* peer_cert, + const std::string& peer_digest); + // SSL certification verification error handler, called back from + // the openssl library. Returns an int interpreted as a boolean in + // the C style: zero means verification failure, non-zero means + // passed. + static int SSLVerifyCallback(int ok, X509_STORE_CTX* store); + + + SSLState state_; + SSLRole role_; + int ssl_error_code_; // valid when state_ == SSL_ERROR or SSL_CLOSED + // Whether the SSL negotiation is blocked on needing to read or + // write to the wrapped stream. + bool ssl_read_needs_write_; + bool ssl_write_needs_read_; + + SSL* ssl_; + SSL_CTX* ssl_ctx_; + + // Our key and certificate, mostly useful in peer-to-peer mode. + scoped_ptr identity_; + // in traditional mode, the server name that the server's certificate + // must specify. Empty in peer-to-peer mode. + std::string ssl_server_name_; + // In peer-to-peer mode, the certificate that the peer must + // present. Empty in traditional mode. + scoped_ptr peer_certificate_; + // In peer-to-peer mode, the digest of the certificate that + // the peer must present. + Buffer peer_certificate_digest_value_; + std::string peer_certificate_digest_algorithm_; + + // OpenSSLAdapter::custom_verify_callback_ result + bool custom_verification_succeeded_; + + // The DtlsSrtp ciphers + std::string srtp_ciphers_; + + // Do DTLS or not + SSLMode ssl_mode_; +}; + +///////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_OPENSSLSTREAMADAPTER_H__ diff --git a/talk/base/optionsfile.cc b/talk/base/optionsfile.cc new file mode 100644 index 000000000..82a5c8698 --- /dev/null +++ b/talk/base/optionsfile.cc @@ -0,0 +1,201 @@ +/* + * libjingle + * Copyright 2008, 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/base/optionsfile.h" + +#include + +#include "talk/base/logging.h" +#include "talk/base/stream.h" +#include "talk/base/stringencode.h" + +namespace talk_base { + +OptionsFile::OptionsFile(const std::string &path) : path_(path) { +} + +bool OptionsFile::Load() { + options_.clear(); + // Open file. + FileStream stream; + int err; + if (!stream.Open(path_, "r", &err)) { + LOG_F(LS_WARNING) << "Could not open file, err=" << err; + // We do not consider this an error because we expect there to be no file + // until the user saves a setting. + return true; + } + // Read in all its data. + std::string line; + StreamResult res; + for (;;) { + res = stream.ReadLine(&line); + if (res != SR_SUCCESS) { + break; + } + size_t equals_pos = line.find('='); + if (equals_pos == std::string::npos) { + // We do not consider this an error. Instead we ignore the line and + // keep going. + LOG_F(LS_WARNING) << "Ignoring malformed line in " << path_; + continue; + } + std::string key(line, 0, equals_pos); + std::string value(line, equals_pos + 1, line.length() - (equals_pos + 1)); + options_[key] = value; + } + if (res != SR_EOS) { + LOG_F(LS_ERROR) << "Error when reading from file"; + return false; + } else { + return true; + } +} + +bool OptionsFile::Save() { + // Open file. + FileStream stream; + int err; + if (!stream.Open(path_, "w", &err)) { + LOG_F(LS_ERROR) << "Could not open file, err=" << err; + return false; + } + // Write out all the data. + StreamResult res = SR_SUCCESS; + size_t written; + int error; + for (OptionsMap::const_iterator i = options_.begin(); i != options_.end(); + ++i) { + res = stream.WriteAll(i->first.c_str(), i->first.length(), &written, + &error); + if (res != SR_SUCCESS) { + break; + } + res = stream.WriteAll("=", 1, &written, &error); + if (res != SR_SUCCESS) { + break; + } + res = stream.WriteAll(i->second.c_str(), i->second.length(), &written, + &error); + if (res != SR_SUCCESS) { + break; + } + res = stream.WriteAll("\n", 1, &written, &error); + if (res != SR_SUCCESS) { + break; + } + } + if (res != SR_SUCCESS) { + LOG_F(LS_ERROR) << "Unable to write to file"; + return false; + } else { + return true; + } +} + +bool OptionsFile::IsLegalName(const std::string &name) { + for (size_t pos = 0; pos < name.length(); ++pos) { + if (name[pos] == '\n' || name[pos] == '\\' || name[pos] == '=') { + // Illegal character. + LOG(LS_WARNING) << "Ignoring operation for illegal option " << name; + return false; + } + } + return true; +} + +bool OptionsFile::IsLegalValue(const std::string &value) { + for (size_t pos = 0; pos < value.length(); ++pos) { + if (value[pos] == '\n' || value[pos] == '\\') { + // Illegal character. + LOG(LS_WARNING) << "Ignoring operation for illegal value " << value; + return false; + } + } + return true; +} + +bool OptionsFile::GetStringValue(const std::string& option, + std::string *out_val) const { + LOG(LS_VERBOSE) << "OptionsFile::GetStringValue " + << option; + if (!IsLegalName(option)) { + return false; + } + OptionsMap::const_iterator i = options_.find(option); + if (i == options_.end()) { + return false; + } + *out_val = i->second; + return true; +} + +bool OptionsFile::GetIntValue(const std::string& option, + int *out_val) const { + LOG(LS_VERBOSE) << "OptionsFile::GetIntValue " + << option; + if (!IsLegalName(option)) { + return false; + } + OptionsMap::const_iterator i = options_.find(option); + if (i == options_.end()) { + return false; + } + return FromString(i->second, out_val); +} + +bool OptionsFile::SetStringValue(const std::string& option, + const std::string& value) { + LOG(LS_VERBOSE) << "OptionsFile::SetStringValue " + << option << ":" << value; + if (!IsLegalName(option) || !IsLegalValue(value)) { + return false; + } + options_[option] = value; + return true; +} + +bool OptionsFile::SetIntValue(const std::string& option, + int value) { + LOG(LS_VERBOSE) << "OptionsFile::SetIntValue " + << option << ":" << value; + if (!IsLegalName(option)) { + return false; + } + return ToString(value, &options_[option]); +} + +bool OptionsFile::RemoveValue(const std::string& option) { + LOG(LS_VERBOSE) << "OptionsFile::RemoveValue " << option; + if (!IsLegalName(option)) { + return false; + } + options_.erase(option); + return true; +} + +} // namespace talk_base diff --git a/talk/base/optionsfile.h b/talk/base/optionsfile.h new file mode 100644 index 000000000..9e5f457c0 --- /dev/null +++ b/talk/base/optionsfile.h @@ -0,0 +1,66 @@ +/* + * libjingle + * Copyright 2008, 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. + */ + +#ifndef TALK_BASE_OPTIONSFILE_H_ +#define TALK_BASE_OPTIONSFILE_H_ + +#include +#include + +namespace talk_base { + +// Implements storage of simple options in a text file on disk. This is +// cross-platform, but it is intended mostly for Linux where there is no +// first-class options storage system. +class OptionsFile { + public: + OptionsFile(const std::string &path); + + // Loads the file from disk, overwriting the in-memory values. + bool Load(); + // Saves the contents in memory, overwriting the on-disk values. + bool Save(); + + bool GetStringValue(const std::string& option, std::string* out_val) const; + bool GetIntValue(const std::string& option, int* out_val) const; + bool SetStringValue(const std::string& option, const std::string& val); + bool SetIntValue(const std::string& option, int val); + bool RemoveValue(const std::string& option); + + private: + typedef std::map OptionsMap; + + static bool IsLegalName(const std::string &name); + static bool IsLegalValue(const std::string &value); + + std::string path_; + OptionsMap options_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_OPTIONSFILE_H_ diff --git a/talk/base/optionsfile_unittest.cc b/talk/base/optionsfile_unittest.cc new file mode 100644 index 000000000..65861ff49 --- /dev/null +++ b/talk/base/optionsfile_unittest.cc @@ -0,0 +1,178 @@ +/* + * libjingle + * Copyright 2008, 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/base/gunit.h" +#include "talk/base/optionsfile.h" + +namespace talk_base { + +#ifdef ANDROID +static const char *kTestFile = "/sdcard/.testfile"; +#elif CHROMEOS +static const char *kTestFile = "/tmp/.testfile"; +#else +static const char *kTestFile = ".testfile"; +#endif + +static const std::string kTestOptionA = "test-option-a"; +static const std::string kTestOptionB = "test-option-b"; +static const std::string kTestString1 = "a string"; +static const std::string kTestString2 = "different string"; +static const std::string kOptionWithEquals = "foo=bar"; +static const std::string kOptionWithNewline = "foo\nbar"; +static const std::string kValueWithEquals = "baz=quux"; +static const std::string kValueWithNewline = "baz\nquux"; +static const std::string kEmptyString = ""; +static const char kOptionWithUtf8[] = {'O', 'p', 't', '\302', '\256', 'i', 'o', + 'n', '\342', '\204', '\242', '\0'}; // Opt(R)io(TM). +static const char kValueWithUtf8[] = {'V', 'a', 'l', '\302', '\256', 'v', 'e', + '\342', '\204', '\242', '\0'}; // Val(R)ue(TM). +static int kTestInt1 = 12345; +static int kTestInt2 = 67890; +static int kNegInt = -634; +static int kZero = 0; + +TEST(OptionsFile, GetSetString) { + OptionsFile store(kTestFile); + // Clear contents of the file on disk. + EXPECT_TRUE(store.Save()); + std::string out1, out2; + EXPECT_FALSE(store.GetStringValue(kTestOptionA, &out1)); + EXPECT_FALSE(store.GetStringValue(kTestOptionB, &out2)); + EXPECT_TRUE(store.SetStringValue(kTestOptionA, kTestString1)); + EXPECT_TRUE(store.Save()); + EXPECT_TRUE(store.Load()); + EXPECT_TRUE(store.SetStringValue(kTestOptionB, kTestString2)); + EXPECT_TRUE(store.Save()); + EXPECT_TRUE(store.Load()); + EXPECT_TRUE(store.GetStringValue(kTestOptionA, &out1)); + EXPECT_TRUE(store.GetStringValue(kTestOptionB, &out2)); + EXPECT_EQ(kTestString1, out1); + EXPECT_EQ(kTestString2, out2); + EXPECT_TRUE(store.RemoveValue(kTestOptionA)); + EXPECT_TRUE(store.Save()); + EXPECT_TRUE(store.Load()); + EXPECT_TRUE(store.RemoveValue(kTestOptionB)); + EXPECT_TRUE(store.Save()); + EXPECT_TRUE(store.Load()); + EXPECT_FALSE(store.GetStringValue(kTestOptionA, &out1)); + EXPECT_FALSE(store.GetStringValue(kTestOptionB, &out2)); +} + +TEST(OptionsFile, GetSetInt) { + OptionsFile store(kTestFile); + // Clear contents of the file on disk. + EXPECT_TRUE(store.Save()); + int out1, out2; + EXPECT_FALSE(store.GetIntValue(kTestOptionA, &out1)); + EXPECT_FALSE(store.GetIntValue(kTestOptionB, &out2)); + EXPECT_TRUE(store.SetIntValue(kTestOptionA, kTestInt1)); + EXPECT_TRUE(store.Save()); + EXPECT_TRUE(store.Load()); + EXPECT_TRUE(store.SetIntValue(kTestOptionB, kTestInt2)); + EXPECT_TRUE(store.Save()); + EXPECT_TRUE(store.Load()); + EXPECT_TRUE(store.GetIntValue(kTestOptionA, &out1)); + EXPECT_TRUE(store.GetIntValue(kTestOptionB, &out2)); + EXPECT_EQ(kTestInt1, out1); + EXPECT_EQ(kTestInt2, out2); + EXPECT_TRUE(store.RemoveValue(kTestOptionA)); + EXPECT_TRUE(store.Save()); + EXPECT_TRUE(store.Load()); + EXPECT_TRUE(store.RemoveValue(kTestOptionB)); + EXPECT_TRUE(store.Save()); + EXPECT_TRUE(store.Load()); + EXPECT_FALSE(store.GetIntValue(kTestOptionA, &out1)); + EXPECT_FALSE(store.GetIntValue(kTestOptionB, &out2)); + EXPECT_TRUE(store.SetIntValue(kTestOptionA, kNegInt)); + EXPECT_TRUE(store.GetIntValue(kTestOptionA, &out1)); + EXPECT_EQ(kNegInt, out1); + EXPECT_TRUE(store.SetIntValue(kTestOptionA, kZero)); + EXPECT_TRUE(store.GetIntValue(kTestOptionA, &out1)); + EXPECT_EQ(kZero, out1); +} + +TEST(OptionsFile, Persist) { + { + OptionsFile store(kTestFile); + // Clear contents of the file on disk. + EXPECT_TRUE(store.Save()); + EXPECT_TRUE(store.SetStringValue(kTestOptionA, kTestString1)); + EXPECT_TRUE(store.SetIntValue(kTestOptionB, kNegInt)); + EXPECT_TRUE(store.Save()); + } + { + OptionsFile store(kTestFile); + // Load the saved contents from above. + EXPECT_TRUE(store.Load()); + std::string out1; + int out2; + EXPECT_TRUE(store.GetStringValue(kTestOptionA, &out1)); + EXPECT_TRUE(store.GetIntValue(kTestOptionB, &out2)); + EXPECT_EQ(kTestString1, out1); + EXPECT_EQ(kNegInt, out2); + } +} + +TEST(OptionsFile, SpecialCharacters) { + OptionsFile store(kTestFile); + // Clear contents of the file on disk. + EXPECT_TRUE(store.Save()); + std::string out; + EXPECT_FALSE(store.SetStringValue(kOptionWithEquals, kTestString1)); + EXPECT_FALSE(store.GetStringValue(kOptionWithEquals, &out)); + EXPECT_FALSE(store.SetStringValue(kOptionWithNewline, kTestString1)); + EXPECT_FALSE(store.GetStringValue(kOptionWithNewline, &out)); + EXPECT_TRUE(store.SetStringValue(kOptionWithUtf8, kValueWithUtf8)); + EXPECT_TRUE(store.SetStringValue(kTestOptionA, kTestString1)); + EXPECT_TRUE(store.Save()); + EXPECT_TRUE(store.Load()); + EXPECT_TRUE(store.GetStringValue(kTestOptionA, &out)); + EXPECT_EQ(kTestString1, out); + EXPECT_TRUE(store.GetStringValue(kOptionWithUtf8, &out)); + EXPECT_EQ(kValueWithUtf8, out); + EXPECT_FALSE(store.SetStringValue(kTestOptionA, kValueWithNewline)); + EXPECT_TRUE(store.GetStringValue(kTestOptionA, &out)); + EXPECT_EQ(kTestString1, out); + EXPECT_TRUE(store.SetStringValue(kTestOptionA, kValueWithEquals)); + EXPECT_TRUE(store.Save()); + EXPECT_TRUE(store.Load()); + EXPECT_TRUE(store.GetStringValue(kTestOptionA, &out)); + EXPECT_EQ(kValueWithEquals, out); + EXPECT_TRUE(store.SetStringValue(kEmptyString, kTestString2)); + EXPECT_TRUE(store.Save()); + EXPECT_TRUE(store.Load()); + EXPECT_TRUE(store.GetStringValue(kEmptyString, &out)); + EXPECT_EQ(kTestString2, out); + EXPECT_TRUE(store.SetStringValue(kTestOptionB, kEmptyString)); + EXPECT_TRUE(store.Save()); + EXPECT_TRUE(store.Load()); + EXPECT_TRUE(store.GetStringValue(kTestOptionB, &out)); + EXPECT_EQ(kEmptyString, out); +} + +} // namespace talk_base diff --git a/talk/base/pathutils.cc b/talk/base/pathutils.cc new file mode 100644 index 000000000..02aba7ff1 --- /dev/null +++ b/talk/base/pathutils.cc @@ -0,0 +1,268 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifdef WIN32 +#include "talk/base/win32.h" +#include +#include +#include +#endif // WIN32 + +#include "talk/base/common.h" +#include "talk/base/fileutils.h" +#include "talk/base/logging.h" +#include "talk/base/pathutils.h" +#include "talk/base/stringutils.h" +#include "talk/base/urlencode.h" + +namespace talk_base { + +static const char EMPTY_STR[] = ""; + +// EXT_DELIM separates a file basename from extension +const char EXT_DELIM = '.'; + +// FOLDER_DELIMS separate folder segments and the filename +const char* const FOLDER_DELIMS = "/\\"; + +// DEFAULT_FOLDER_DELIM is the preferred delimiter for this platform +#if WIN32 +const char DEFAULT_FOLDER_DELIM = '\\'; +#else // !WIN32 +const char DEFAULT_FOLDER_DELIM = '/'; +#endif // !WIN32 + +/////////////////////////////////////////////////////////////////////////////// +// Pathname - parsing of pathnames into components, and vice versa +/////////////////////////////////////////////////////////////////////////////// + +bool Pathname::IsFolderDelimiter(char ch) { + return (NULL != ::strchr(FOLDER_DELIMS, ch)); +} + +char Pathname::DefaultFolderDelimiter() { + return DEFAULT_FOLDER_DELIM; +} + +Pathname::Pathname() + : folder_delimiter_(DEFAULT_FOLDER_DELIM) { +} + +Pathname::Pathname(const std::string& pathname) + : folder_delimiter_(DEFAULT_FOLDER_DELIM) { + SetPathname(pathname); +} + +Pathname::Pathname(const std::string& folder, const std::string& filename) + : folder_delimiter_(DEFAULT_FOLDER_DELIM) { + SetPathname(folder, filename); +} + +void Pathname::SetFolderDelimiter(char delimiter) { + ASSERT(IsFolderDelimiter(delimiter)); + folder_delimiter_ = delimiter; +} + +void Pathname::Normalize() { + for (size_t i=0; i= 2) { + pos = folder_.find_last_of(FOLDER_DELIMS, folder_.length() - 2); + } + if (pos != std::string::npos) { + return folder_.substr(pos + 1); + } else { + return folder_; + } +} + +std::string Pathname::parent_folder() const { + std::string::size_type pos = std::string::npos; + if (folder_.size() >= 2) { + pos = folder_.find_last_of(FOLDER_DELIMS, folder_.length() - 2); + } + if (pos != std::string::npos) { + return folder_.substr(0, pos + 1); + } else { + return EMPTY_STR; + } +} + +void Pathname::SetFolder(const std::string& folder) { + folder_.assign(folder); + // Ensure folder ends in a path delimiter + if (!folder_.empty() && !IsFolderDelimiter(folder_[folder_.length()-1])) { + folder_.push_back(folder_delimiter_); + } +} + +void Pathname::AppendFolder(const std::string& folder) { + folder_.append(folder); + // Ensure folder ends in a path delimiter + if (!folder_.empty() && !IsFolderDelimiter(folder_[folder_.length()-1])) { + folder_.push_back(folder_delimiter_); + } +} + +std::string Pathname::basename() const { + return basename_; +} + +bool Pathname::SetBasename(const std::string& basename) { + if(basename.find_first_of(FOLDER_DELIMS) != std::string::npos) { + return false; + } + basename_.assign(basename); + return true; +} + +std::string Pathname::extension() const { + return extension_; +} + +bool Pathname::SetExtension(const std::string& extension) { + if (extension.find_first_of(FOLDER_DELIMS) != std::string::npos || + extension.find_first_of(EXT_DELIM, 1) != std::string::npos) { + return false; + } + extension_.assign(extension); + // Ensure extension begins with the extension delimiter + if (!extension_.empty() && (extension_[0] != EXT_DELIM)) { + extension_.insert(extension_.begin(), EXT_DELIM); + } + return true; +} + +std::string Pathname::filename() const { + std::string filename(basename_); + filename.append(extension_); + return filename; +} + +bool Pathname::SetFilename(const std::string& filename) { + std::string::size_type pos = filename.rfind(EXT_DELIM); + if ((pos == std::string::npos) || (pos == 0)) { + return SetExtension(EMPTY_STR) && SetBasename(filename); + } else { + return SetExtension(filename.substr(pos)) && SetBasename(filename.substr(0, pos)); + } +} + +#ifdef WIN32 +bool Pathname::GetDrive(char *drive, uint32 bytes) const { + return GetDrive(drive, bytes, folder_); +} + +// static +bool Pathname::GetDrive(char *drive, uint32 bytes, + const std::string& pathname) { + // need at lease 4 bytes to save c: + if (bytes < 4 || pathname.size() < 3) { + return false; + } + + memcpy(drive, pathname.c_str(), 3); + drive[3] = 0; + // sanity checking + return (isalpha(drive[0]) && + drive[1] == ':' && + drive[2] == '\\'); +} +#endif + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/pathutils.h b/talk/base/pathutils.h new file mode 100644 index 000000000..ab2aacd3c --- /dev/null +++ b/talk/base/pathutils.h @@ -0,0 +1,180 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_PATHUTILS_H__ +#define TALK_BASE_PATHUTILS_H__ + +#include +// Temporary, until deprecated helpers are removed. +#include "talk/base/fileutils.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// Pathname - parsing of pathnames into components, and vice versa. +// +// To establish consistent terminology, a filename never contains a folder +// component. A folder never contains a filename. A pathname may include +// a folder and/or filename component. Here are some examples: +// +// pathname() /home/john/example.txt +// folder() /home/john/ +// filename() example.txt +// parent_folder() /home/ +// folder_name() john/ +// basename() example +// extension() .txt +// +// Basename may begin, end, and/or include periods, but no folder delimiters. +// If extension exists, it consists of a period followed by zero or more +// non-period/non-delimiter characters, and basename is non-empty. +/////////////////////////////////////////////////////////////////////////////// + +class Pathname { +public: + // Folder delimiters are slash and backslash + static bool IsFolderDelimiter(char ch); + static char DefaultFolderDelimiter(); + + Pathname(); + Pathname(const std::string& pathname); + Pathname(const std::string& folder, const std::string& filename); + + // Set's the default folder delimiter for this Pathname + char folder_delimiter() const { return folder_delimiter_; } + void SetFolderDelimiter(char delimiter); + + // Normalize changes all folder delimiters to folder_delimiter() + void Normalize(); + + // Reset to the empty pathname + void clear(); + + // Returns true if the pathname is empty. Note: this->pathname().empty() + // is always false. + bool empty() const; + + std::string url() const; + + // Returns the folder and filename components. If the pathname is empty, + // returns a string representing the current directory (as a relative path, + // i.e., "."). + std::string pathname() const; + void SetPathname(const std::string& pathname); + void SetPathname(const std::string& folder, const std::string& filename); + + // Append pathname to the current folder (if any). Any existing filename + // will be discarded. + void AppendPathname(const std::string& pathname); + + std::string folder() const; + std::string folder_name() const; + std::string parent_folder() const; + // SetFolder and AppendFolder will append a folder delimiter, if needed. + void SetFolder(const std::string& folder); + void AppendFolder(const std::string& folder); + + std::string basename() const; + bool SetBasename(const std::string& basename); + + std::string extension() const; + // SetExtension will prefix a period, if needed. + bool SetExtension(const std::string& extension); + + std::string filename() const; + bool SetFilename(const std::string& filename); + +#ifdef WIN32 + bool GetDrive(char *drive, uint32 bytes) const; + static bool GetDrive(char *drive, uint32 bytes,const std::string& pathname); +#endif + +private: + std::string folder_, basename_, extension_; + char folder_delimiter_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// Global Helpers (deprecated) +/////////////////////////////////////////////////////////////////////////////// + +inline void SetOrganizationName(const std::string& organization) { + Filesystem::SetOrganizationName(organization); +} +inline void SetApplicationName(const std::string& application) { + Filesystem::SetApplicationName(application); +} +inline void GetOrganizationName(std::string* organization) { + Filesystem::GetOrganizationName(organization); +} +inline void GetApplicationName(std::string* application) { + Filesystem::GetApplicationName(application); +} +inline bool CreateFolder(const Pathname& path) { + return Filesystem::CreateFolder(path); +} +inline bool FinishPath(Pathname& path, bool create, const std::string& append) { + if (!append.empty()) + path.AppendFolder(append); + return !create || CreateFolder(path); +} +// Note: this method uses the convention of / for the temporary +// folder. Filesystem uses /. We will be migrating exclusively +// to // eventually. Since these are temp folders, +// it's probably ok to orphan them during the transition. +inline bool GetTemporaryFolder(Pathname& path, bool create, + const std::string& append) { + std::string application_name; + Filesystem::GetApplicationName(&application_name); + ASSERT(!application_name.empty()); + return Filesystem::GetTemporaryFolder(path, create, &application_name) + && FinishPath(path, create, append); +} +inline bool GetAppDataFolder(Pathname& path, bool create, + const std::string& append) { + ASSERT(!create); // TODO: Support create flag on Filesystem::GetAppDataFolder. + return Filesystem::GetAppDataFolder(&path, true) + && FinishPath(path, create, append); +} +inline bool CleanupTemporaryFolder() { + Pathname path; + if (!GetTemporaryFolder(path, false, "")) + return false; + if (Filesystem::IsAbsent(path)) + return true; + if (!Filesystem::IsTemporaryPath(path)) { + ASSERT(false); + return false; + } + return Filesystem::DeleteFolderContents(path); +} + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_PATHUTILS_H__ diff --git a/talk/base/pathutils_unittest.cc b/talk/base/pathutils_unittest.cc new file mode 100644 index 000000000..0a9739b42 --- /dev/null +++ b/talk/base/pathutils_unittest.cc @@ -0,0 +1,65 @@ +/* + * libjingle + * Copyright 2007, 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/base/pathutils.h" +#include "talk/base/gunit.h" + +TEST(Pathname, ReturnsDotForEmptyPathname) { + const std::string kCWD = + std::string(".") + talk_base::Pathname::DefaultFolderDelimiter(); + + talk_base::Pathname path("/", ""); + EXPECT_FALSE(path.empty()); + EXPECT_FALSE(path.folder().empty()); + EXPECT_TRUE (path.filename().empty()); + EXPECT_FALSE(path.pathname().empty()); + EXPECT_EQ(std::string("/"), path.pathname()); + + path.SetPathname("", "foo"); + EXPECT_FALSE(path.empty()); + EXPECT_TRUE (path.folder().empty()); + EXPECT_FALSE(path.filename().empty()); + EXPECT_FALSE(path.pathname().empty()); + EXPECT_EQ(std::string("foo"), path.pathname()); + + path.SetPathname("", ""); + EXPECT_TRUE (path.empty()); + EXPECT_TRUE (path.folder().empty()); + EXPECT_TRUE (path.filename().empty()); + EXPECT_FALSE(path.pathname().empty()); + EXPECT_EQ(kCWD, path.pathname()); + + path.SetPathname(kCWD, ""); + EXPECT_FALSE(path.empty()); + EXPECT_FALSE(path.folder().empty()); + EXPECT_TRUE (path.filename().empty()); + EXPECT_FALSE(path.pathname().empty()); + EXPECT_EQ(kCWD, path.pathname()); + + talk_base::Pathname path2("c:/foo bar.txt"); + EXPECT_EQ(path2.url(), std::string("file:///c:/foo%20bar.txt")); +} diff --git a/talk/base/physicalsocketserver.cc b/talk/base/physicalsocketserver.cc new file mode 100644 index 000000000..8a1bb5c26 --- /dev/null +++ b/talk/base/physicalsocketserver.cc @@ -0,0 +1,1673 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#if defined(_MSC_VER) && _MSC_VER < 1300 +#pragma warning(disable:4786) +#endif + +#include + +#ifdef POSIX +#include +#include +#include +#include +#include +#include +#endif + +#ifdef WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#undef SetPort +#endif + +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/nethelpers.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/timeutils.h" +#include "talk/base/winping.h" +#include "talk/base/win32socketinit.h" + +// stm: this will tell us if we are on OSX +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#ifdef POSIX +#include // for TCP_NODELAY +#define IP_MTU 14 // Until this is integrated from linux/in.h to netinet/in.h +typedef void* SockOptArg; +#endif // POSIX + +#ifdef WIN32 +typedef char* SockOptArg; +#endif + +namespace talk_base { + +// Standard MTUs, from RFC 1191 +const uint16 PACKET_MAXIMUMS[] = { + 65535, // Theoretical maximum, Hyperchannel + 32000, // Nothing + 17914, // 16Mb IBM Token Ring + 8166, // IEEE 802.4 + //4464, // IEEE 802.5 (4Mb max) + 4352, // FDDI + //2048, // Wideband Network + 2002, // IEEE 802.5 (4Mb recommended) + //1536, // Expermental Ethernet Networks + //1500, // Ethernet, Point-to-Point (default) + 1492, // IEEE 802.3 + 1006, // SLIP, ARPANET + //576, // X.25 Networks + //544, // DEC IP Portal + //512, // NETBIOS + 508, // IEEE 802/Source-Rt Bridge, ARCNET + 296, // Point-to-Point (low delay) + 68, // Official minimum + 0, // End of list marker +}; + +static const int IP_HEADER_SIZE = 20u; +static const int IPV6_HEADER_SIZE = 40u; +static const int ICMP_HEADER_SIZE = 8u; +static const int ICMP_PING_TIMEOUT_MILLIS = 10000u; + +class PhysicalSocket : public AsyncSocket, public sigslot::has_slots<> { + public: + PhysicalSocket(PhysicalSocketServer* ss, SOCKET s = INVALID_SOCKET) + : ss_(ss), s_(s), enabled_events_(0), error_(0), + state_((s == INVALID_SOCKET) ? CS_CLOSED : CS_CONNECTED), + resolver_(NULL) { +#ifdef WIN32 + // EnsureWinsockInit() ensures that winsock is initialized. The default + // version of this function doesn't do anything because winsock is + // initialized by constructor of a static object. If neccessary libjingle + // users can link it with a different version of this function by replacing + // win32socketinit.cc. See win32socketinit.cc for more details. + EnsureWinsockInit(); +#endif + if (s_ != INVALID_SOCKET) { + enabled_events_ = DE_READ | DE_WRITE; + + int type = SOCK_STREAM; + socklen_t len = sizeof(type); + VERIFY(0 == getsockopt(s_, SOL_SOCKET, SO_TYPE, (SockOptArg)&type, &len)); + udp_ = (SOCK_DGRAM == type); + } + } + + virtual ~PhysicalSocket() { + Close(); + } + + // Creates the underlying OS socket (same as the "socket" function). + virtual bool Create(int family, int type) { + Close(); + s_ = ::socket(family, type, 0); + udp_ = (SOCK_DGRAM == type); + UpdateLastError(); + if (udp_) + enabled_events_ = DE_READ | DE_WRITE; + return s_ != INVALID_SOCKET; + } + + SocketAddress GetLocalAddress() const { + sockaddr_storage addr_storage = {0}; + socklen_t addrlen = sizeof(addr_storage); + sockaddr* addr = reinterpret_cast(&addr_storage); + int result = ::getsockname(s_, addr, &addrlen); + SocketAddress address; + if (result >= 0) { + SocketAddressFromSockAddrStorage(addr_storage, &address); + } else { + LOG(LS_WARNING) << "GetLocalAddress: unable to get local addr, socket=" + << s_; + } + return address; + } + + SocketAddress GetRemoteAddress() const { + sockaddr_storage addr_storage = {0}; + socklen_t addrlen = sizeof(addr_storage); + sockaddr* addr = reinterpret_cast(&addr_storage); + int result = ::getpeername(s_, addr, &addrlen); + SocketAddress address; + if (result >= 0) { + SocketAddressFromSockAddrStorage(addr_storage, &address); + } else { + LOG(LS_WARNING) << "GetRemoteAddress: unable to get remote addr, socket=" + << s_; + } + return address; + } + + int Bind(const SocketAddress& bind_addr) { + sockaddr_storage addr_storage; + size_t len = bind_addr.ToSockAddrStorage(&addr_storage); + sockaddr* addr = reinterpret_cast(&addr_storage); + int err = ::bind(s_, addr, static_cast(len)); + UpdateLastError(); +#ifdef _DEBUG + if (0 == err) { + dbg_addr_ = "Bound @ "; + dbg_addr_.append(GetLocalAddress().ToString()); + } +#endif // _DEBUG + return err; + } + + int Connect(const SocketAddress& addr) { + // TODO: Implicit creation is required to reconnect... + // ...but should we make it more explicit? + if (state_ != CS_CLOSED) { + SetError(EALREADY); + return SOCKET_ERROR; + } + if (addr.IsUnresolved()) { + LOG(LS_VERBOSE) << "Resolving addr in PhysicalSocket::Connect"; + resolver_ = new AsyncResolver(); + resolver_->set_address(addr); + resolver_->SignalWorkDone.connect(this, &PhysicalSocket::OnResolveResult); + resolver_->Start(); + state_ = CS_CONNECTING; + return 0; + } + + return DoConnect(addr); + } + + int DoConnect(const SocketAddress& connect_addr) { + if ((s_ == INVALID_SOCKET) && + !Create(connect_addr.family(), SOCK_STREAM)) { + return SOCKET_ERROR; + } + sockaddr_storage addr_storage; + size_t len = connect_addr.ToSockAddrStorage(&addr_storage); + sockaddr* addr = reinterpret_cast(&addr_storage); + int err = ::connect(s_, addr, static_cast(len)); + UpdateLastError(); + if (err == 0) { + state_ = CS_CONNECTED; + } else if (IsBlockingError(error_)) { + state_ = CS_CONNECTING; + enabled_events_ |= DE_CONNECT; + } else { + return SOCKET_ERROR; + } + + enabled_events_ |= DE_READ | DE_WRITE; + return 0; + } + + int GetError() const { + return error_; + } + + void SetError(int error) { + error_ = error; + } + + ConnState GetState() const { + return state_; + } + + int GetOption(Option opt, int* value) { + int slevel; + int sopt; + if (TranslateOption(opt, &slevel, &sopt) == -1) + return -1; + socklen_t optlen = sizeof(*value); + int ret = ::getsockopt(s_, slevel, sopt, (SockOptArg)value, &optlen); + if (ret != -1 && opt == OPT_DONTFRAGMENT) { +#ifdef LINUX + *value = (*value != IP_PMTUDISC_DONT) ? 1 : 0; +#endif + } + return ret; + } + + int SetOption(Option opt, int value) { + int slevel; + int sopt; + if (TranslateOption(opt, &slevel, &sopt) == -1) + return -1; + if (opt == OPT_DONTFRAGMENT) { +#ifdef LINUX + value = (value) ? IP_PMTUDISC_DO : IP_PMTUDISC_DONT; +#endif + } + return ::setsockopt(s_, slevel, sopt, (SockOptArg)&value, sizeof(value)); + } + + int Send(const void *pv, size_t cb) { + int sent = ::send(s_, reinterpret_cast(pv), (int)cb, +#ifdef LINUX + // Suppress SIGPIPE. Without this, attempting to send on a socket whose + // other end is closed will result in a SIGPIPE signal being raised to + // our process, which by default will terminate the process, which we + // don't want. By specifying this flag, we'll just get the error EPIPE + // instead and can handle the error gracefully. + MSG_NOSIGNAL +#else + 0 +#endif + ); + UpdateLastError(); + MaybeRemapSendError(); + // We have seen minidumps where this may be false. + ASSERT(sent <= static_cast(cb)); + if ((sent < 0) && IsBlockingError(error_)) { + enabled_events_ |= DE_WRITE; + } + return sent; + } + + int SendTo(const void* buffer, size_t length, const SocketAddress& addr) { + sockaddr_storage saddr; + size_t len = addr.ToSockAddrStorage(&saddr); + int sent = ::sendto( + s_, static_cast(buffer), static_cast(length), +#ifdef LINUX + // Suppress SIGPIPE. See above for explanation. + MSG_NOSIGNAL, +#else + 0, +#endif + reinterpret_cast(&saddr), static_cast(len)); + UpdateLastError(); + MaybeRemapSendError(); + // We have seen minidumps where this may be false. + ASSERT(sent <= static_cast(length)); + if ((sent < 0) && IsBlockingError(error_)) { + enabled_events_ |= DE_WRITE; + } + return sent; + } + + int Recv(void* buffer, size_t length) { + int received = ::recv(s_, static_cast(buffer), + static_cast(length), 0); + if ((received == 0) && (length != 0)) { + // Note: on graceful shutdown, recv can return 0. In this case, we + // pretend it is blocking, and then signal close, so that simplifying + // assumptions can be made about Recv. + LOG(LS_WARNING) << "EOF from socket; deferring close event"; + // Must turn this back on so that the select() loop will notice the close + // event. + enabled_events_ |= DE_READ; + error_ = EWOULDBLOCK; + return SOCKET_ERROR; + } + UpdateLastError(); + bool success = (received >= 0) || IsBlockingError(error_); + if (udp_ || success) { + enabled_events_ |= DE_READ; + } + if (!success) { + LOG_F(LS_VERBOSE) << "Error = " << error_; + } + return received; + } + + int RecvFrom(void* buffer, size_t length, SocketAddress *out_addr) { + sockaddr_storage addr_storage; + socklen_t addr_len = sizeof(addr_storage); + sockaddr* addr = reinterpret_cast(&addr_storage); + int received = ::recvfrom(s_, static_cast(buffer), + static_cast(length), 0, addr, &addr_len); + UpdateLastError(); + if ((received >= 0) && (out_addr != NULL)) + SocketAddressFromSockAddrStorage(addr_storage, out_addr); + bool success = (received >= 0) || IsBlockingError(error_); + if (udp_ || success) { + enabled_events_ |= DE_READ; + } + if (!success) { + LOG_F(LS_VERBOSE) << "Error = " << error_; + } + return received; + } + + int Listen(int backlog) { + int err = ::listen(s_, backlog); + UpdateLastError(); + if (err == 0) { + state_ = CS_CONNECTING; + enabled_events_ |= DE_ACCEPT; +#ifdef _DEBUG + dbg_addr_ = "Listening @ "; + dbg_addr_.append(GetLocalAddress().ToString()); +#endif // _DEBUG + } + return err; + } + + AsyncSocket* Accept(SocketAddress *out_addr) { + sockaddr_storage addr_storage; + socklen_t addr_len = sizeof(addr_storage); + sockaddr* addr = reinterpret_cast(&addr_storage); + SOCKET s = ::accept(s_, addr, &addr_len); + UpdateLastError(); + if (s == INVALID_SOCKET) + return NULL; + enabled_events_ |= DE_ACCEPT; + if (out_addr != NULL) + SocketAddressFromSockAddrStorage(addr_storage, out_addr); + return ss_->WrapSocket(s); + } + + int Close() { + if (s_ == INVALID_SOCKET) + return 0; + int err = ::closesocket(s_); + UpdateLastError(); + s_ = INVALID_SOCKET; + state_ = CS_CLOSED; + enabled_events_ = 0; + if (resolver_) { + resolver_->Destroy(false); + resolver_ = NULL; + } + return err; + } + + int EstimateMTU(uint16* mtu) { + SocketAddress addr = GetRemoteAddress(); + if (addr.IsAny()) { + error_ = ENOTCONN; + return -1; + } + +#if defined(WIN32) + // Gets the interface MTU (TTL=1) for the interface used to reach |addr|. + WinPing ping; + if (!ping.IsValid()) { + error_ = EINVAL; // can't think of a better error ID + return -1; + } + int header_size = ICMP_HEADER_SIZE; + if (addr.family() == AF_INET6) { + header_size += IPV6_HEADER_SIZE; + } else if (addr.family() == AF_INET) { + header_size += IP_HEADER_SIZE; + } + + for (int level = 0; PACKET_MAXIMUMS[level + 1] > 0; ++level) { + int32 size = PACKET_MAXIMUMS[level] - header_size; + WinPing::PingResult result = ping.Ping(addr.ipaddr(), size, + ICMP_PING_TIMEOUT_MILLIS, + 1, false); + if (result == WinPing::PING_FAIL) { + error_ = EINVAL; // can't think of a better error ID + return -1; + } else if (result != WinPing::PING_TOO_LARGE) { + *mtu = PACKET_MAXIMUMS[level]; + return 0; + } + } + + ASSERT(false); + return -1; +#elif defined(IOS) || defined(OSX) + // No simple way to do this on Mac OS X. + // SIOCGIFMTU would work if we knew which interface would be used, but + // figuring that out is pretty complicated. For now we'll return an error + // and let the caller pick a default MTU. + error_ = EINVAL; + return -1; +#elif defined(LINUX) || defined(ANDROID) + // Gets the path MTU. + int value; + socklen_t vlen = sizeof(value); + int err = getsockopt(s_, IPPROTO_IP, IP_MTU, &value, &vlen); + if (err < 0) { + UpdateLastError(); + return err; + } + + ASSERT((0 <= value) && (value <= 65536)); + *mtu = value; + return 0; +#endif + } + + SocketServer* socketserver() { return ss_; } + + protected: + void OnResolveResult(SignalThread* thread) { + if (thread != resolver_) { + return; + } + + int error = resolver_->error(); + if (error == 0) { + error = DoConnect(resolver_->address()); + } else { + Close(); + } + + if (error) { + error_ = error; + SignalCloseEvent(this, error_); + } + } + + void UpdateLastError() { + error_ = LAST_SYSTEM_ERROR; + } + + void MaybeRemapSendError() { +#if defined(OSX) + // https://developer.apple.com/library/mac/documentation/Darwin/ + // Reference/ManPages/man2/sendto.2.html + // ENOBUFS - The output queue for a network interface is full. + // This generally indicates that the interface has stopped sending, + // but may be caused by transient congestion. + if (error_ == ENOBUFS) { + error_ = EWOULDBLOCK; + } +#endif + } + + static int TranslateOption(Option opt, int* slevel, int* sopt) { + switch (opt) { + case OPT_DONTFRAGMENT: +#ifdef WIN32 + *slevel = IPPROTO_IP; + *sopt = IP_DONTFRAGMENT; + break; +#elif defined(IOS) || defined(OSX) || defined(BSD) + LOG(LS_WARNING) << "Socket::OPT_DONTFRAGMENT not supported."; + return -1; +#elif defined(POSIX) + *slevel = IPPROTO_IP; + *sopt = IP_MTU_DISCOVER; + break; +#endif + case OPT_RCVBUF: + *slevel = SOL_SOCKET; + *sopt = SO_RCVBUF; + break; + case OPT_SNDBUF: + *slevel = SOL_SOCKET; + *sopt = SO_SNDBUF; + break; + case OPT_NODELAY: + *slevel = IPPROTO_TCP; + *sopt = TCP_NODELAY; + break; + default: + ASSERT(false); + return -1; + } + return 0; + } + + PhysicalSocketServer* ss_; + SOCKET s_; + uint8 enabled_events_; + bool udp_; + int error_; + ConnState state_; + AsyncResolver* resolver_; + +#ifdef _DEBUG + std::string dbg_addr_; +#endif // _DEBUG; +}; + +#ifdef POSIX +class EventDispatcher : public Dispatcher { + public: + EventDispatcher(PhysicalSocketServer* ss) : ss_(ss), fSignaled_(false) { + if (pipe(afd_) < 0) + LOG(LERROR) << "pipe failed"; + ss_->Add(this); + } + + virtual ~EventDispatcher() { + ss_->Remove(this); + close(afd_[0]); + close(afd_[1]); + } + + virtual void Signal() { + CritScope cs(&crit_); + if (!fSignaled_) { + const uint8 b[1] = { 0 }; + if (VERIFY(1 == write(afd_[1], b, sizeof(b)))) { + fSignaled_ = true; + } + } + } + + virtual uint32 GetRequestedEvents() { + return DE_READ; + } + + virtual void OnPreEvent(uint32 ff) { + // It is not possible to perfectly emulate an auto-resetting event with + // pipes. This simulates it by resetting before the event is handled. + + CritScope cs(&crit_); + if (fSignaled_) { + uint8 b[4]; // Allow for reading more than 1 byte, but expect 1. + VERIFY(1 == read(afd_[0], b, sizeof(b))); + fSignaled_ = false; + } + } + + virtual void OnEvent(uint32 ff, int err) { + ASSERT(false); + } + + virtual int GetDescriptor() { + return afd_[0]; + } + + virtual bool IsDescriptorClosed() { + return false; + } + + private: + PhysicalSocketServer *ss_; + int afd_[2]; + bool fSignaled_; + CriticalSection crit_; +}; + +// These two classes use the self-pipe trick to deliver POSIX signals to our +// select loop. This is the only safe, reliable, cross-platform way to do +// non-trivial things with a POSIX signal in an event-driven program (until +// proper pselect() implementations become ubiquitous). + +class PosixSignalHandler { + public: + // POSIX only specifies 32 signals, but in principle the system might have + // more and the programmer might choose to use them, so we size our array + // for 128. + static const int kNumPosixSignals = 128; + + // There is just a single global instance. (Signal handlers do not get any + // sort of user-defined void * parameter, so they can't access anything that + // isn't global.) + static PosixSignalHandler* Instance() { + LIBJINGLE_DEFINE_STATIC_LOCAL(PosixSignalHandler, instance, ()); + return &instance; + } + + // Returns true if the given signal number is set. + bool IsSignalSet(int signum) const { + ASSERT(signum < ARRAY_SIZE(received_signal_)); + if (signum < ARRAY_SIZE(received_signal_)) { + return received_signal_[signum]; + } else { + return false; + } + } + + // Clears the given signal number. + void ClearSignal(int signum) { + ASSERT(signum < ARRAY_SIZE(received_signal_)); + if (signum < ARRAY_SIZE(received_signal_)) { + received_signal_[signum] = false; + } + } + + // Returns the file descriptor to monitor for signal events. + int GetDescriptor() const { + return afd_[0]; + } + + // This is called directly from our real signal handler, so it must be + // signal-handler-safe. That means it cannot assume anything about the + // user-level state of the process, since the handler could be executed at any + // time on any thread. + void OnPosixSignalReceived(int signum) { + if (signum >= ARRAY_SIZE(received_signal_)) { + // We don't have space in our array for this. + return; + } + // Set a flag saying we've seen this signal. + received_signal_[signum] = true; + // Notify application code that we got a signal. + const uint8 b[1] = { 0 }; + if (-1 == write(afd_[1], b, sizeof(b))) { + // Nothing we can do here. If there's an error somehow then there's + // nothing we can safely do from a signal handler. + // No, we can't even safely log it. + // But, we still have to check the return value here. Otherwise, + // GCC 4.4.1 complains ignoring return value. Even (void) doesn't help. + return; + } + } + + private: + PosixSignalHandler() { + if (pipe(afd_) < 0) { + LOG_ERR(LS_ERROR) << "pipe failed"; + return; + } + if (fcntl(afd_[0], F_SETFL, O_NONBLOCK) < 0) { + LOG_ERR(LS_WARNING) << "fcntl #1 failed"; + } + if (fcntl(afd_[1], F_SETFL, O_NONBLOCK) < 0) { + LOG_ERR(LS_WARNING) << "fcntl #2 failed"; + } + memset(const_cast(static_cast(received_signal_)), + 0, + sizeof(received_signal_)); + } + + ~PosixSignalHandler() { + int fd1 = afd_[0]; + int fd2 = afd_[1]; + // We clobber the stored file descriptor numbers here or else in principle + // a signal that happens to be delivered during application termination + // could erroneously write a zero byte to an unrelated file handle in + // OnPosixSignalReceived() if some other file happens to be opened later + // during shutdown and happens to be given the same file descriptor number + // as our pipe had. Unfortunately even with this precaution there is still a + // race where that could occur if said signal happens to be handled + // concurrently with this code and happens to have already read the value of + // afd_[1] from memory before we clobber it, but that's unlikely. + afd_[0] = -1; + afd_[1] = -1; + close(fd1); + close(fd2); + } + + int afd_[2]; + // These are boolean flags that will be set in our signal handler and read + // and cleared from Wait(). There is a race involved in this, but it is + // benign. The signal handler sets the flag before signaling the pipe, so + // we'll never end up blocking in select() while a flag is still true. + // However, if two of the same signal arrive close to each other then it's + // possible that the second time the handler may set the flag while it's still + // true, meaning that signal will be missed. But the first occurrence of it + // will still be handled, so this isn't a problem. + // Volatile is not necessary here for correctness, but this data _is_ volatile + // so I've marked it as such. + volatile uint8 received_signal_[kNumPosixSignals]; +}; + +class PosixSignalDispatcher : public Dispatcher { + public: + PosixSignalDispatcher(PhysicalSocketServer *owner) : owner_(owner) { + owner_->Add(this); + } + + virtual ~PosixSignalDispatcher() { + owner_->Remove(this); + } + + virtual uint32 GetRequestedEvents() { + return DE_READ; + } + + virtual void OnPreEvent(uint32 ff) { + // Events might get grouped if signals come very fast, so we read out up to + // 16 bytes to make sure we keep the pipe empty. + uint8 b[16]; + ssize_t ret = read(GetDescriptor(), b, sizeof(b)); + if (ret < 0) { + LOG_ERR(LS_WARNING) << "Error in read()"; + } else if (ret == 0) { + LOG(LS_WARNING) << "Should have read at least one byte"; + } + } + + virtual void OnEvent(uint32 ff, int err) { + for (int signum = 0; signum < PosixSignalHandler::kNumPosixSignals; + ++signum) { + if (PosixSignalHandler::Instance()->IsSignalSet(signum)) { + PosixSignalHandler::Instance()->ClearSignal(signum); + HandlerMap::iterator i = handlers_.find(signum); + if (i == handlers_.end()) { + // This can happen if a signal is delivered to our process at around + // the same time as we unset our handler for it. It is not an error + // condition, but it's unusual enough to be worth logging. + LOG(LS_INFO) << "Received signal with no handler: " << signum; + } else { + // Otherwise, execute our handler. + (*i->second)(signum); + } + } + } + } + + virtual int GetDescriptor() { + return PosixSignalHandler::Instance()->GetDescriptor(); + } + + virtual bool IsDescriptorClosed() { + return false; + } + + void SetHandler(int signum, void (*handler)(int)) { + handlers_[signum] = handler; + } + + void ClearHandler(int signum) { + handlers_.erase(signum); + } + + bool HasHandlers() { + return !handlers_.empty(); + } + + private: + typedef std::map HandlerMap; + + HandlerMap handlers_; + // Our owner. + PhysicalSocketServer *owner_; +}; + +class SocketDispatcher : public Dispatcher, public PhysicalSocket { + public: + explicit SocketDispatcher(PhysicalSocketServer *ss) : PhysicalSocket(ss) { + } + SocketDispatcher(SOCKET s, PhysicalSocketServer *ss) : PhysicalSocket(ss, s) { + } + + virtual ~SocketDispatcher() { + Close(); + } + + bool Initialize() { + ss_->Add(this); + fcntl(s_, F_SETFL, fcntl(s_, F_GETFL, 0) | O_NONBLOCK); + return true; + } + + virtual bool Create(int type) { + return Create(AF_INET, type); + } + + virtual bool Create(int family, int type) { + // Change the socket to be non-blocking. + if (!PhysicalSocket::Create(family, type)) + return false; + + return Initialize(); + } + + virtual int GetDescriptor() { + return s_; + } + + virtual bool IsDescriptorClosed() { + // We don't have a reliable way of distinguishing end-of-stream + // from readability. So test on each readable call. Is this + // inefficient? Probably. + char ch; + ssize_t res = ::recv(s_, &ch, 1, MSG_PEEK); + if (res > 0) { + // Data available, so not closed. + return false; + } else if (res == 0) { + // EOF, so closed. + return true; + } else { // error + switch (errno) { + // Returned if we've already closed s_. + case EBADF: + // Returned during ungraceful peer shutdown. + case ECONNRESET: + return true; + default: + // Assume that all other errors are just blocking errors, meaning the + // connection is still good but we just can't read from it right now. + // This should only happen when connecting (and at most once), because + // in all other cases this function is only called if the file + // descriptor is already known to be in the readable state. However, + // it's not necessary a problem if we spuriously interpret a + // "connection lost"-type error as a blocking error, because typically + // the next recv() will get EOF, so we'll still eventually notice that + // the socket is closed. + LOG_ERR(LS_WARNING) << "Assuming benign blocking error"; + return false; + } + } + } + + virtual uint32 GetRequestedEvents() { + return enabled_events_; + } + + virtual void OnPreEvent(uint32 ff) { + if ((ff & DE_CONNECT) != 0) + state_ = CS_CONNECTED; + if ((ff & DE_CLOSE) != 0) + state_ = CS_CLOSED; + } + + virtual void OnEvent(uint32 ff, int err) { + // Make sure we deliver connect/accept first. Otherwise, consumers may see + // something like a READ followed by a CONNECT, which would be odd. + if ((ff & DE_CONNECT) != 0) { + enabled_events_ &= ~DE_CONNECT; + SignalConnectEvent(this); + } + if ((ff & DE_ACCEPT) != 0) { + enabled_events_ &= ~DE_ACCEPT; + SignalReadEvent(this); + } + if ((ff & DE_READ) != 0) { + enabled_events_ &= ~DE_READ; + SignalReadEvent(this); + } + if ((ff & DE_WRITE) != 0) { + enabled_events_ &= ~DE_WRITE; + SignalWriteEvent(this); + } + if ((ff & DE_CLOSE) != 0) { + // The socket is now dead to us, so stop checking it. + enabled_events_ = 0; + SignalCloseEvent(this, err); + } + } + + virtual int Close() { + if (s_ == INVALID_SOCKET) + return 0; + + ss_->Remove(this); + return PhysicalSocket::Close(); + } +}; + +class FileDispatcher: public Dispatcher, public AsyncFile { + public: + FileDispatcher(int fd, PhysicalSocketServer *ss) : ss_(ss), fd_(fd) { + set_readable(true); + + ss_->Add(this); + + fcntl(fd_, F_SETFL, fcntl(fd_, F_GETFL, 0) | O_NONBLOCK); + } + + virtual ~FileDispatcher() { + ss_->Remove(this); + } + + SocketServer* socketserver() { return ss_; } + + virtual int GetDescriptor() { + return fd_; + } + + virtual bool IsDescriptorClosed() { + return false; + } + + virtual uint32 GetRequestedEvents() { + return flags_; + } + + virtual void OnPreEvent(uint32 ff) { + } + + virtual void OnEvent(uint32 ff, int err) { + if ((ff & DE_READ) != 0) + SignalReadEvent(this); + if ((ff & DE_WRITE) != 0) + SignalWriteEvent(this); + if ((ff & DE_CLOSE) != 0) + SignalCloseEvent(this, err); + } + + virtual bool readable() { + return (flags_ & DE_READ) != 0; + } + + virtual void set_readable(bool value) { + flags_ = value ? (flags_ | DE_READ) : (flags_ & ~DE_READ); + } + + virtual bool writable() { + return (flags_ & DE_WRITE) != 0; + } + + virtual void set_writable(bool value) { + flags_ = value ? (flags_ | DE_WRITE) : (flags_ & ~DE_WRITE); + } + + private: + PhysicalSocketServer* ss_; + int fd_; + int flags_; +}; + +AsyncFile* PhysicalSocketServer::CreateFile(int fd) { + return new FileDispatcher(fd, this); +} + +#endif // POSIX + +#ifdef WIN32 +static uint32 FlagsToEvents(uint32 events) { + uint32 ffFD = FD_CLOSE; + if (events & DE_READ) + ffFD |= FD_READ; + if (events & DE_WRITE) + ffFD |= FD_WRITE; + if (events & DE_CONNECT) + ffFD |= FD_CONNECT; + if (events & DE_ACCEPT) + ffFD |= FD_ACCEPT; + return ffFD; +} + +class EventDispatcher : public Dispatcher { + public: + EventDispatcher(PhysicalSocketServer *ss) : ss_(ss) { + hev_ = WSACreateEvent(); + if (hev_) { + ss_->Add(this); + } + } + + ~EventDispatcher() { + if (hev_ != NULL) { + ss_->Remove(this); + WSACloseEvent(hev_); + hev_ = NULL; + } + } + + virtual void Signal() { + if (hev_ != NULL) + WSASetEvent(hev_); + } + + virtual uint32 GetRequestedEvents() { + return 0; + } + + virtual void OnPreEvent(uint32 ff) { + WSAResetEvent(hev_); + } + + virtual void OnEvent(uint32 ff, int err) { + } + + virtual WSAEVENT GetWSAEvent() { + return hev_; + } + + virtual SOCKET GetSocket() { + return INVALID_SOCKET; + } + + virtual bool CheckSignalClose() { return false; } + +private: + PhysicalSocketServer* ss_; + WSAEVENT hev_; +}; + +class SocketDispatcher : public Dispatcher, public PhysicalSocket { + public: + static int next_id_; + int id_; + bool signal_close_; + int signal_err_; + + SocketDispatcher(PhysicalSocketServer* ss) + : PhysicalSocket(ss), + id_(0), + signal_close_(false) { + } + + SocketDispatcher(SOCKET s, PhysicalSocketServer* ss) + : PhysicalSocket(ss, s), + id_(0), + signal_close_(false) { + } + + virtual ~SocketDispatcher() { + Close(); + } + + bool Initialize() { + ASSERT(s_ != INVALID_SOCKET); + // Must be a non-blocking + u_long argp = 1; + ioctlsocket(s_, FIONBIO, &argp); + ss_->Add(this); + return true; + } + + virtual bool Create(int type) { + return Create(AF_INET, type); + } + + virtual bool Create(int family, int type) { + // Create socket + if (!PhysicalSocket::Create(family, type)) + return false; + + if (!Initialize()) + return false; + + do { id_ = ++next_id_; } while (id_ == 0); + return true; + } + + virtual int Close() { + if (s_ == INVALID_SOCKET) + return 0; + + id_ = 0; + signal_close_ = false; + ss_->Remove(this); + return PhysicalSocket::Close(); + } + + virtual uint32 GetRequestedEvents() { + return enabled_events_; + } + + virtual void OnPreEvent(uint32 ff) { + if ((ff & DE_CONNECT) != 0) + state_ = CS_CONNECTED; + // We set CS_CLOSED from CheckSignalClose. + } + + virtual void OnEvent(uint32 ff, int err) { + int cache_id = id_; + // Make sure we deliver connect/accept first. Otherwise, consumers may see + // something like a READ followed by a CONNECT, which would be odd. + if (((ff & DE_CONNECT) != 0) && (id_ == cache_id)) { + if (ff != DE_CONNECT) + LOG(LS_VERBOSE) << "Signalled with DE_CONNECT: " << ff; + enabled_events_ &= ~DE_CONNECT; +#ifdef _DEBUG + dbg_addr_ = "Connected @ "; + dbg_addr_.append(GetRemoteAddress().ToString()); +#endif // _DEBUG + SignalConnectEvent(this); + } + if (((ff & DE_ACCEPT) != 0) && (id_ == cache_id)) { + enabled_events_ &= ~DE_ACCEPT; + SignalReadEvent(this); + } + if ((ff & DE_READ) != 0) { + enabled_events_ &= ~DE_READ; + SignalReadEvent(this); + } + if (((ff & DE_WRITE) != 0) && (id_ == cache_id)) { + enabled_events_ &= ~DE_WRITE; + SignalWriteEvent(this); + } + if (((ff & DE_CLOSE) != 0) && (id_ == cache_id)) { + signal_close_ = true; + signal_err_ = err; + } + } + + virtual WSAEVENT GetWSAEvent() { + return WSA_INVALID_EVENT; + } + + virtual SOCKET GetSocket() { + return s_; + } + + virtual bool CheckSignalClose() { + if (!signal_close_) + return false; + + char ch; + if (recv(s_, &ch, 1, MSG_PEEK) > 0) + return false; + + state_ = CS_CLOSED; + signal_close_ = false; + SignalCloseEvent(this, signal_err_); + return true; + } +}; + +int SocketDispatcher::next_id_ = 0; + +#endif // WIN32 + +// Sets the value of a boolean value to false when signaled. +class Signaler : public EventDispatcher { + public: + Signaler(PhysicalSocketServer* ss, bool* pf) + : EventDispatcher(ss), pf_(pf) { + } + virtual ~Signaler() { } + + void OnEvent(uint32 ff, int err) { + if (pf_) + *pf_ = false; + } + + private: + bool *pf_; +}; + +PhysicalSocketServer::PhysicalSocketServer() + : fWait_(false), + last_tick_tracked_(0), + last_tick_dispatch_count_(0) { + signal_wakeup_ = new Signaler(this, &fWait_); +#ifdef WIN32 + socket_ev_ = WSACreateEvent(); +#endif +} + +PhysicalSocketServer::~PhysicalSocketServer() { +#ifdef WIN32 + WSACloseEvent(socket_ev_); +#endif +#ifdef POSIX + signal_dispatcher_.reset(); +#endif + delete signal_wakeup_; + ASSERT(dispatchers_.empty()); +} + +void PhysicalSocketServer::WakeUp() { + signal_wakeup_->Signal(); +} + +Socket* PhysicalSocketServer::CreateSocket(int type) { + return CreateSocket(AF_INET, type); +} + +Socket* PhysicalSocketServer::CreateSocket(int family, int type) { + PhysicalSocket* socket = new PhysicalSocket(this); + if (socket->Create(family, type)) { + return socket; + } else { + delete socket; + return 0; + } +} + +AsyncSocket* PhysicalSocketServer::CreateAsyncSocket(int type) { + return CreateAsyncSocket(AF_INET, type); +} + +AsyncSocket* PhysicalSocketServer::CreateAsyncSocket(int family, int type) { + SocketDispatcher* dispatcher = new SocketDispatcher(this); + if (dispatcher->Create(family, type)) { + return dispatcher; + } else { + delete dispatcher; + return 0; + } +} + +AsyncSocket* PhysicalSocketServer::WrapSocket(SOCKET s) { + SocketDispatcher* dispatcher = new SocketDispatcher(s, this); + if (dispatcher->Initialize()) { + return dispatcher; + } else { + delete dispatcher; + return 0; + } +} + +void PhysicalSocketServer::Add(Dispatcher *pdispatcher) { + CritScope cs(&crit_); + // Prevent duplicates. This can cause dead dispatchers to stick around. + DispatcherList::iterator pos = std::find(dispatchers_.begin(), + dispatchers_.end(), + pdispatcher); + if (pos != dispatchers_.end()) + return; + dispatchers_.push_back(pdispatcher); +} + +void PhysicalSocketServer::Remove(Dispatcher *pdispatcher) { + CritScope cs(&crit_); + DispatcherList::iterator pos = std::find(dispatchers_.begin(), + dispatchers_.end(), + pdispatcher); + ASSERT(pos != dispatchers_.end()); + size_t index = pos - dispatchers_.begin(); + dispatchers_.erase(pos); + for (IteratorList::iterator it = iterators_.begin(); it != iterators_.end(); + ++it) { + if (index < **it) { + --**it; + } + } +} + +#ifdef POSIX +bool PhysicalSocketServer::Wait(int cmsWait, bool process_io) { + // Calculate timing information + + struct timeval *ptvWait = NULL; + struct timeval tvWait; + struct timeval tvStop; + if (cmsWait != kForever) { + // Calculate wait timeval + tvWait.tv_sec = cmsWait / 1000; + tvWait.tv_usec = (cmsWait % 1000) * 1000; + ptvWait = &tvWait; + + // Calculate when to return in a timeval + gettimeofday(&tvStop, NULL); + tvStop.tv_sec += tvWait.tv_sec; + tvStop.tv_usec += tvWait.tv_usec; + if (tvStop.tv_usec >= 1000000) { + tvStop.tv_usec -= 1000000; + tvStop.tv_sec += 1; + } + } + + // Zero all fd_sets. Don't need to do this inside the loop since + // select() zeros the descriptors not signaled + + fd_set fdsRead; + FD_ZERO(&fdsRead); + fd_set fdsWrite; + FD_ZERO(&fdsWrite); + + fWait_ = true; + + while (fWait_) { + int fdmax = -1; + { + CritScope cr(&crit_); + for (size_t i = 0; i < dispatchers_.size(); ++i) { + // Query dispatchers for read and write wait state + Dispatcher *pdispatcher = dispatchers_[i]; + ASSERT(pdispatcher); + if (!process_io && (pdispatcher != signal_wakeup_)) + continue; + int fd = pdispatcher->GetDescriptor(); + if (fd > fdmax) + fdmax = fd; + + uint32 ff = pdispatcher->GetRequestedEvents(); + if (ff & (DE_READ | DE_ACCEPT)) + FD_SET(fd, &fdsRead); + if (ff & (DE_WRITE | DE_CONNECT)) + FD_SET(fd, &fdsWrite); + } + } + + // Wait then call handlers as appropriate + // < 0 means error + // 0 means timeout + // > 0 means count of descriptors ready + int n = select(fdmax + 1, &fdsRead, &fdsWrite, NULL, ptvWait); + + // If error, return error. + if (n < 0) { + if (errno != EINTR) { + LOG_E(LS_ERROR, EN, errno) << "select"; + return false; + } + // Else ignore the error and keep going. If this EINTR was for one of the + // signals managed by this PhysicalSocketServer, the + // PosixSignalDeliveryDispatcher will be in the signaled state in the next + // iteration. + } else if (n == 0) { + // If timeout, return success + return true; + } else { + // We have signaled descriptors + CritScope cr(&crit_); + for (size_t i = 0; i < dispatchers_.size(); ++i) { + Dispatcher *pdispatcher = dispatchers_[i]; + int fd = pdispatcher->GetDescriptor(); + uint32 ff = 0; + int errcode = 0; + + // Reap any error code, which can be signaled through reads or writes. + // TODO: Should we set errcode if getsockopt fails? + if (FD_ISSET(fd, &fdsRead) || FD_ISSET(fd, &fdsWrite)) { + socklen_t len = sizeof(errcode); + ::getsockopt(fd, SOL_SOCKET, SO_ERROR, &errcode, &len); + } + + // Check readable descriptors. If we're waiting on an accept, signal + // that. Otherwise we're waiting for data, check to see if we're + // readable or really closed. + // TODO: Only peek at TCP descriptors. + if (FD_ISSET(fd, &fdsRead)) { + FD_CLR(fd, &fdsRead); + if (pdispatcher->GetRequestedEvents() & DE_ACCEPT) { + ff |= DE_ACCEPT; + } else if (errcode || pdispatcher->IsDescriptorClosed()) { + ff |= DE_CLOSE; + } else { + ff |= DE_READ; + } + } + + // Check writable descriptors. If we're waiting on a connect, detect + // success versus failure by the reaped error code. + if (FD_ISSET(fd, &fdsWrite)) { + FD_CLR(fd, &fdsWrite); + if (pdispatcher->GetRequestedEvents() & DE_CONNECT) { + if (!errcode) { + ff |= DE_CONNECT; + } else { + ff |= DE_CLOSE; + } + } else { + ff |= DE_WRITE; + } + } + + // Tell the descriptor about the event. + if (ff != 0) { + pdispatcher->OnPreEvent(ff); + pdispatcher->OnEvent(ff, errcode); + } + } + } + + // Recalc the time remaining to wait. Doing it here means it doesn't get + // calced twice the first time through the loop + if (ptvWait) { + ptvWait->tv_sec = 0; + ptvWait->tv_usec = 0; + struct timeval tvT; + gettimeofday(&tvT, NULL); + if ((tvStop.tv_sec > tvT.tv_sec) + || ((tvStop.tv_sec == tvT.tv_sec) + && (tvStop.tv_usec > tvT.tv_usec))) { + ptvWait->tv_sec = tvStop.tv_sec - tvT.tv_sec; + ptvWait->tv_usec = tvStop.tv_usec - tvT.tv_usec; + if (ptvWait->tv_usec < 0) { + ASSERT(ptvWait->tv_sec > 0); + ptvWait->tv_usec += 1000000; + ptvWait->tv_sec -= 1; + } + } + } + } + + return true; +} + +static void GlobalSignalHandler(int signum) { + PosixSignalHandler::Instance()->OnPosixSignalReceived(signum); +} + +bool PhysicalSocketServer::SetPosixSignalHandler(int signum, + void (*handler)(int)) { + // If handler is SIG_IGN or SIG_DFL then clear our user-level handler, + // otherwise set one. + if (handler == SIG_IGN || handler == SIG_DFL) { + if (!InstallSignal(signum, handler)) { + return false; + } + if (signal_dispatcher_) { + signal_dispatcher_->ClearHandler(signum); + if (!signal_dispatcher_->HasHandlers()) { + signal_dispatcher_.reset(); + } + } + } else { + if (!signal_dispatcher_) { + signal_dispatcher_.reset(new PosixSignalDispatcher(this)); + } + signal_dispatcher_->SetHandler(signum, handler); + if (!InstallSignal(signum, &GlobalSignalHandler)) { + return false; + } + } + return true; +} + +Dispatcher* PhysicalSocketServer::signal_dispatcher() { + return signal_dispatcher_.get(); +} + +bool PhysicalSocketServer::InstallSignal(int signum, void (*handler)(int)) { + struct sigaction act; + // It doesn't really matter what we set this mask to. + if (sigemptyset(&act.sa_mask) != 0) { + LOG_ERR(LS_ERROR) << "Couldn't set mask"; + return false; + } + act.sa_handler = handler; + // Use SA_RESTART so that our syscalls don't get EINTR, since we don't need it + // and it's a nuisance. Though some syscalls still return EINTR and there's no + // real standard for which ones. :( + act.sa_flags = SA_RESTART; + if (sigaction(signum, &act, NULL) != 0) { + LOG_ERR(LS_ERROR) << "Couldn't set sigaction"; + return false; + } + return true; +} +#endif // POSIX + +#ifdef WIN32 +bool PhysicalSocketServer::Wait(int cmsWait, bool process_io) { + int cmsTotal = cmsWait; + int cmsElapsed = 0; + uint32 msStart = Time(); + +#if LOGGING + if (last_tick_dispatch_count_ == 0) { + last_tick_tracked_ = msStart; + } +#endif + + fWait_ = true; + while (fWait_) { + std::vector events; + std::vector event_owners; + + events.push_back(socket_ev_); + + { + CritScope cr(&crit_); + size_t i = 0; + iterators_.push_back(&i); + // Don't track dispatchers_.size(), because we want to pick up any new + // dispatchers that were added while processing the loop. + while (i < dispatchers_.size()) { + Dispatcher* disp = dispatchers_[i++]; + if (!process_io && (disp != signal_wakeup_)) + continue; + SOCKET s = disp->GetSocket(); + if (disp->CheckSignalClose()) { + // We just signalled close, don't poll this socket + } else if (s != INVALID_SOCKET) { + WSAEventSelect(s, + events[0], + FlagsToEvents(disp->GetRequestedEvents())); + } else { + events.push_back(disp->GetWSAEvent()); + event_owners.push_back(disp); + } + } + ASSERT(iterators_.back() == &i); + iterators_.pop_back(); + } + + // Which is shorter, the delay wait or the asked wait? + + int cmsNext; + if (cmsWait == kForever) { + cmsNext = cmsWait; + } else { + cmsNext = _max(0, cmsTotal - cmsElapsed); + } + + // Wait for one of the events to signal + DWORD dw = WSAWaitForMultipleEvents(static_cast(events.size()), + &events[0], + false, + cmsNext, + false); + +#if 0 // LOGGING + // we track this information purely for logging purposes. + last_tick_dispatch_count_++; + if (last_tick_dispatch_count_ >= 1000) { + int32 elapsed = TimeSince(last_tick_tracked_); + LOG(INFO) << "PhysicalSocketServer took " << elapsed + << "ms for 1000 events"; + + // If we get more than 1000 events in a second, we are spinning badly + // (normally it should take about 8-20 seconds). + ASSERT(elapsed > 1000); + + last_tick_tracked_ = Time(); + last_tick_dispatch_count_ = 0; + } +#endif + + if (dw == WSA_WAIT_FAILED) { + // Failed? + // TODO: need a better strategy than this! + int error = WSAGetLastError(); + ASSERT(false); + return false; + } else if (dw == WSA_WAIT_TIMEOUT) { + // Timeout? + return true; + } else { + // Figure out which one it is and call it + CritScope cr(&crit_); + int index = dw - WSA_WAIT_EVENT_0; + if (index > 0) { + --index; // The first event is the socket event + event_owners[index]->OnPreEvent(0); + event_owners[index]->OnEvent(0, 0); + } else if (process_io) { + size_t i = 0, end = dispatchers_.size(); + iterators_.push_back(&i); + iterators_.push_back(&end); // Don't iterate over new dispatchers. + while (i < end) { + Dispatcher* disp = dispatchers_[i++]; + SOCKET s = disp->GetSocket(); + if (s == INVALID_SOCKET) + continue; + + WSANETWORKEVENTS wsaEvents; + int err = WSAEnumNetworkEvents(s, events[0], &wsaEvents); + if (err == 0) { + +#if LOGGING + { + if ((wsaEvents.lNetworkEvents & FD_READ) && + wsaEvents.iErrorCode[FD_READ_BIT] != 0) { + LOG(WARNING) << "PhysicalSocketServer got FD_READ_BIT error " + << wsaEvents.iErrorCode[FD_READ_BIT]; + } + if ((wsaEvents.lNetworkEvents & FD_WRITE) && + wsaEvents.iErrorCode[FD_WRITE_BIT] != 0) { + LOG(WARNING) << "PhysicalSocketServer got FD_WRITE_BIT error " + << wsaEvents.iErrorCode[FD_WRITE_BIT]; + } + if ((wsaEvents.lNetworkEvents & FD_CONNECT) && + wsaEvents.iErrorCode[FD_CONNECT_BIT] != 0) { + LOG(WARNING) << "PhysicalSocketServer got FD_CONNECT_BIT error " + << wsaEvents.iErrorCode[FD_CONNECT_BIT]; + } + if ((wsaEvents.lNetworkEvents & FD_ACCEPT) && + wsaEvents.iErrorCode[FD_ACCEPT_BIT] != 0) { + LOG(WARNING) << "PhysicalSocketServer got FD_ACCEPT_BIT error " + << wsaEvents.iErrorCode[FD_ACCEPT_BIT]; + } + if ((wsaEvents.lNetworkEvents & FD_CLOSE) && + wsaEvents.iErrorCode[FD_CLOSE_BIT] != 0) { + LOG(WARNING) << "PhysicalSocketServer got FD_CLOSE_BIT error " + << wsaEvents.iErrorCode[FD_CLOSE_BIT]; + } + } +#endif + uint32 ff = 0; + int errcode = 0; + if (wsaEvents.lNetworkEvents & FD_READ) + ff |= DE_READ; + if (wsaEvents.lNetworkEvents & FD_WRITE) + ff |= DE_WRITE; + if (wsaEvents.lNetworkEvents & FD_CONNECT) { + if (wsaEvents.iErrorCode[FD_CONNECT_BIT] == 0) { + ff |= DE_CONNECT; + } else { + ff |= DE_CLOSE; + errcode = wsaEvents.iErrorCode[FD_CONNECT_BIT]; + } + } + if (wsaEvents.lNetworkEvents & FD_ACCEPT) + ff |= DE_ACCEPT; + if (wsaEvents.lNetworkEvents & FD_CLOSE) { + ff |= DE_CLOSE; + errcode = wsaEvents.iErrorCode[FD_CLOSE_BIT]; + } + if (ff != 0) { + disp->OnPreEvent(ff); + disp->OnEvent(ff, errcode); + } + } + } + ASSERT(iterators_.back() == &end); + iterators_.pop_back(); + ASSERT(iterators_.back() == &i); + iterators_.pop_back(); + } + + // Reset the network event until new activity occurs + WSAResetEvent(socket_ev_); + } + + // Break? + if (!fWait_) + break; + cmsElapsed = TimeSince(msStart); + if ((cmsWait != kForever) && (cmsElapsed >= cmsWait)) { + break; + } + } + + // Done + return true; +} +#endif // WIN32 + +} // namespace talk_base diff --git a/talk/base/physicalsocketserver.h b/talk/base/physicalsocketserver.h new file mode 100644 index 000000000..709f85ab1 --- /dev/null +++ b/talk/base/physicalsocketserver.h @@ -0,0 +1,139 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_PHYSICALSOCKETSERVER_H__ +#define TALK_BASE_PHYSICALSOCKETSERVER_H__ + +#include + +#include "talk/base/asyncfile.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/socketserver.h" +#include "talk/base/criticalsection.h" + +#ifdef POSIX +typedef int SOCKET; +#endif // POSIX + +namespace talk_base { + +// Event constants for the Dispatcher class. +enum DispatcherEvent { + DE_READ = 0x0001, + DE_WRITE = 0x0002, + DE_CONNECT = 0x0004, + DE_CLOSE = 0x0008, + DE_ACCEPT = 0x0010, +}; + +class Signaler; +#ifdef POSIX +class PosixSignalDispatcher; +#endif + +class Dispatcher { + public: + virtual ~Dispatcher() {} + virtual uint32 GetRequestedEvents() = 0; + virtual void OnPreEvent(uint32 ff) = 0; + virtual void OnEvent(uint32 ff, int err) = 0; +#ifdef WIN32 + virtual WSAEVENT GetWSAEvent() = 0; + virtual SOCKET GetSocket() = 0; + virtual bool CheckSignalClose() = 0; +#elif POSIX + virtual int GetDescriptor() = 0; + virtual bool IsDescriptorClosed() = 0; +#endif +}; + +// A socket server that provides the real sockets of the underlying OS. +class PhysicalSocketServer : public SocketServer { + public: + PhysicalSocketServer(); + virtual ~PhysicalSocketServer(); + + // SocketFactory: + virtual Socket* CreateSocket(int type); + virtual Socket* CreateSocket(int family, int type); + + virtual AsyncSocket* CreateAsyncSocket(int type); + virtual AsyncSocket* CreateAsyncSocket(int family, int type); + + // Internal Factory for Accept + AsyncSocket* WrapSocket(SOCKET s); + + // SocketServer: + virtual bool Wait(int cms, bool process_io); + virtual void WakeUp(); + + void Add(Dispatcher* dispatcher); + void Remove(Dispatcher* dispatcher); + +#ifdef POSIX + AsyncFile* CreateFile(int fd); + + // Sets the function to be executed in response to the specified POSIX signal. + // The function is executed from inside Wait() using the "self-pipe trick"-- + // regardless of which thread receives the signal--and hence can safely + // manipulate user-level data structures. + // "handler" may be SIG_IGN, SIG_DFL, or a user-specified function, just like + // with signal(2). + // Only one PhysicalSocketServer should have user-level signal handlers. + // Dispatching signals on multiple PhysicalSocketServers is not reliable. + // The signal mask is not modified. It is the caller's responsibily to + // maintain it as desired. + virtual bool SetPosixSignalHandler(int signum, void (*handler)(int)); + + protected: + Dispatcher* signal_dispatcher(); +#endif + + private: + typedef std::vector DispatcherList; + typedef std::vector IteratorList; + +#ifdef POSIX + static bool InstallSignal(int signum, void (*handler)(int)); + + scoped_ptr signal_dispatcher_; +#endif + DispatcherList dispatchers_; + IteratorList iterators_; + Signaler* signal_wakeup_; + CriticalSection crit_; + bool fWait_; + uint32 last_tick_tracked_; + int last_tick_dispatch_count_; +#ifdef WIN32 + WSAEVENT socket_ev_; +#endif +}; + +} // namespace talk_base + +#endif // TALK_BASE_PHYSICALSOCKETSERVER_H__ diff --git a/talk/base/physicalsocketserver_unittest.cc b/talk/base/physicalsocketserver_unittest.cc new file mode 100644 index 000000000..b7f68481a --- /dev/null +++ b/talk/base/physicalsocketserver_unittest.cc @@ -0,0 +1,299 @@ +/* + * libjingle + * Copyright 2004--2011, 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 +#include + +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/socket_unittest.h" +#include "talk/base/thread.h" + +namespace talk_base { + +class PhysicalSocketTest : public SocketTest { +}; + +TEST_F(PhysicalSocketTest, TestConnectIPv4) { + SocketTest::TestConnectIPv4(); +} + +TEST_F(PhysicalSocketTest, TestConnectIPv6) { + SocketTest::TestConnectIPv6(); +} + +TEST_F(PhysicalSocketTest, TestConnectWithDnsLookupIPv4) { + SocketTest::TestConnectWithDnsLookupIPv4(); +} + +TEST_F(PhysicalSocketTest, TestConnectWithDnsLookupIPv6) { + SocketTest::TestConnectWithDnsLookupIPv6(); +} + +TEST_F(PhysicalSocketTest, TestConnectFailIPv4) { + SocketTest::TestConnectFailIPv4(); +} + +TEST_F(PhysicalSocketTest, TestConnectFailIPv6) { + SocketTest::TestConnectFailIPv6(); +} + +TEST_F(PhysicalSocketTest, TestConnectWithDnsLookupFailIPv4) { + SocketTest::TestConnectWithDnsLookupFailIPv4(); +} + + +TEST_F(PhysicalSocketTest, TestConnectWithDnsLookupFailIPv6) { + SocketTest::TestConnectWithDnsLookupFailIPv6(); +} + + +#ifdef OSX +// This test crashes the OS X kernel on 10.6 (at bsd/netinet/tcp_subr.c:2118). +TEST_F(PhysicalSocketTest, DISABLED_TestConnectWithClosedSocketIPv4) { +#else +TEST_F(PhysicalSocketTest, TestConnectWithClosedSocketIPv4) { +#endif + SocketTest::TestConnectWithClosedSocketIPv4(); +} + +#ifdef OSX +// This test crashes the OS X kernel on 10.6 (at bsd/netinet/tcp_subr.c:2118). +TEST_F(PhysicalSocketTest, DISABLED_TestConnectWithClosedSocketIPv6) { +#else +TEST_F(PhysicalSocketTest, TestConnectWithClosedSocketIPv6) { +#endif + SocketTest::TestConnectWithClosedSocketIPv6(); +} + +TEST_F(PhysicalSocketTest, TestConnectWhileNotClosedIPv4) { + SocketTest::TestConnectWhileNotClosedIPv4(); +} + +TEST_F(PhysicalSocketTest, TestConnectWhileNotClosedIPv6) { + SocketTest::TestConnectWhileNotClosedIPv6(); +} + +TEST_F(PhysicalSocketTest, TestServerCloseDuringConnectIPv4) { + SocketTest::TestServerCloseDuringConnectIPv4(); +} + +TEST_F(PhysicalSocketTest, TestServerCloseDuringConnectIPv6) { + SocketTest::TestServerCloseDuringConnectIPv6(); +} + +TEST_F(PhysicalSocketTest, TestClientCloseDuringConnectIPv4) { + SocketTest::TestClientCloseDuringConnectIPv4(); +} + +TEST_F(PhysicalSocketTest, TestClientCloseDuringConnectIPv6) { + SocketTest::TestClientCloseDuringConnectIPv6(); +} + +TEST_F(PhysicalSocketTest, TestServerCloseIPv4) { + SocketTest::TestServerCloseIPv4(); +} + +TEST_F(PhysicalSocketTest, TestServerCloseIPv6) { + SocketTest::TestServerCloseIPv6(); +} + +TEST_F(PhysicalSocketTest, TestCloseInClosedCallbackIPv4) { + SocketTest::TestCloseInClosedCallbackIPv4(); +} + +TEST_F(PhysicalSocketTest, TestCloseInClosedCallbackIPv6) { + SocketTest::TestCloseInClosedCallbackIPv6(); +} + +TEST_F(PhysicalSocketTest, TestSocketServerWaitIPv4) { + SocketTest::TestSocketServerWaitIPv4(); +} + +TEST_F(PhysicalSocketTest, TestSocketServerWaitIPv6) { + SocketTest::TestSocketServerWaitIPv6(); +} + +TEST_F(PhysicalSocketTest, TestTcpIPv4) { + SocketTest::TestTcpIPv4(); +} + +TEST_F(PhysicalSocketTest, TestTcpIPv6) { + SocketTest::TestTcpIPv6(); +} + +TEST_F(PhysicalSocketTest, TestUdpIPv4) { + SocketTest::TestUdpIPv4(); +} + +TEST_F(PhysicalSocketTest, TestUdpIPv6) { + SocketTest::TestUdpIPv6(); +} + +TEST_F(PhysicalSocketTest, TestUdpReadyToSendIPv4) { + SocketTest::TestUdpReadyToSendIPv4(); +} + +TEST_F(PhysicalSocketTest, TestUdpReadyToSendIPv6) { + SocketTest::TestUdpReadyToSendIPv6(); +} + +TEST_F(PhysicalSocketTest, TestGetSetOptionsIPv4) { + SocketTest::TestGetSetOptionsIPv4(); +} + +TEST_F(PhysicalSocketTest, TestGetSetOptionsIPv6) { + SocketTest::TestGetSetOptionsIPv6(); +} + +#ifdef POSIX + +class PosixSignalDeliveryTest : public testing::Test { + public: + static void RecordSignal(int signum) { + signals_received_.push_back(signum); + signaled_thread_ = Thread::Current(); + } + + protected: + void SetUp() { + ss_.reset(new PhysicalSocketServer()); + } + + void TearDown() { + ss_.reset(NULL); + signals_received_.clear(); + signaled_thread_ = NULL; + } + + bool ExpectSignal(int signum) { + if (signals_received_.empty()) { + LOG(LS_ERROR) << "ExpectSignal(): No signal received"; + return false; + } + if (signals_received_[0] != signum) { + LOG(LS_ERROR) << "ExpectSignal(): Received signal " << + signals_received_[0] << ", expected " << signum; + return false; + } + signals_received_.erase(signals_received_.begin()); + return true; + } + + bool ExpectNone() { + bool ret = signals_received_.empty(); + if (!ret) { + LOG(LS_ERROR) << "ExpectNone(): Received signal " << signals_received_[0] + << ", expected none"; + } + return ret; + } + + static std::vector signals_received_; + static Thread *signaled_thread_; + + scoped_ptr ss_; +}; + +std::vector PosixSignalDeliveryTest::signals_received_; +Thread *PosixSignalDeliveryTest::signaled_thread_ = NULL; + +// Test receiving a synchronous signal while not in Wait() and then entering +// Wait() afterwards. +TEST_F(PosixSignalDeliveryTest, RaiseThenWait) { + ss_->SetPosixSignalHandler(SIGTERM, &RecordSignal); + raise(SIGTERM); + EXPECT_TRUE(ss_->Wait(0, true)); + EXPECT_TRUE(ExpectSignal(SIGTERM)); + EXPECT_TRUE(ExpectNone()); +} + +// Test that we can handle getting tons of repeated signals and that we see all +// the different ones. +TEST_F(PosixSignalDeliveryTest, InsanelyManySignals) { + ss_->SetPosixSignalHandler(SIGTERM, &RecordSignal); + ss_->SetPosixSignalHandler(SIGINT, &RecordSignal); + for (int i = 0; i < 10000; ++i) { + raise(SIGTERM); + } + raise(SIGINT); + EXPECT_TRUE(ss_->Wait(0, true)); + // Order will be lowest signal numbers first. + EXPECT_TRUE(ExpectSignal(SIGINT)); + EXPECT_TRUE(ExpectSignal(SIGTERM)); + EXPECT_TRUE(ExpectNone()); +} + +// Test that a signal during a Wait() call is detected. +TEST_F(PosixSignalDeliveryTest, SignalDuringWait) { + ss_->SetPosixSignalHandler(SIGALRM, &RecordSignal); + alarm(1); + EXPECT_TRUE(ss_->Wait(1500, true)); + EXPECT_TRUE(ExpectSignal(SIGALRM)); + EXPECT_TRUE(ExpectNone()); +} + +class RaiseSigTermRunnable : public Runnable { + void Run(Thread *thread) { + thread->socketserver()->Wait(1000, false); + + // Allow SIGTERM. This will be the only thread with it not masked so it will + // be delivered to us. + sigset_t mask; + sigemptyset(&mask); + pthread_sigmask(SIG_SETMASK, &mask, NULL); + + // Raise it. + raise(SIGTERM); + } +}; + +// Test that it works no matter what thread the kernel chooses to give the +// signal to (since it's not guaranteed to be the one that Wait() runs on). +TEST_F(PosixSignalDeliveryTest, SignalOnDifferentThread) { + ss_->SetPosixSignalHandler(SIGTERM, &RecordSignal); + // Mask out SIGTERM so that it can't be delivered to this thread. + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGTERM); + EXPECT_EQ(0, pthread_sigmask(SIG_SETMASK, &mask, NULL)); + // Start a new thread that raises it. It will have to be delivered to that + // thread. Our implementation should safely handle it and dispatch + // RecordSignal() on this thread. + scoped_ptr thread(new Thread()); + thread->Start(new RaiseSigTermRunnable()); + EXPECT_TRUE(ss_->Wait(1500, true)); + EXPECT_TRUE(ExpectSignal(SIGTERM)); + EXPECT_EQ(Thread::Current(), signaled_thread_); + EXPECT_TRUE(ExpectNone()); +} + +#endif + +} // namespace talk_base diff --git a/talk/base/posix.cc b/talk/base/posix.cc new file mode 100644 index 000000000..3502f6d3c --- /dev/null +++ b/talk/base/posix.cc @@ -0,0 +1,148 @@ +/* + * libjingle + * Copyright 2004--2009, 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/base/posix.h" + +#include +#include +#include + +#ifdef LINUX +#include "talk/base/linuxfdwalk.h" +#endif +#include "talk/base/logging.h" + +namespace talk_base { + +#ifdef LINUX +static void closefds(void *close_errors, int fd) { + if (fd <= 2) { + // We leave stdin/out/err open to the browser's terminal, if any. + return; + } + if (close(fd) < 0) { + *static_cast(close_errors) = true; + } +} +#endif + +enum { + EXIT_FLAG_CHDIR_ERRORS = 1 << 0, +#ifdef LINUX + EXIT_FLAG_FDWALK_ERRORS = 1 << 1, + EXIT_FLAG_CLOSE_ERRORS = 1 << 2, +#endif + EXIT_FLAG_SECOND_FORK_FAILED = 1 << 3, +}; + +bool RunAsDaemon(const char *file, const char *const argv[]) { + // Fork intermediate child to daemonize. + pid_t pid = fork(); + if (pid < 0) { + LOG_ERR(LS_ERROR) << "fork()"; + return false; + } else if (!pid) { + // Child. + + // We try to close all fds and change directory to /, but if that fails we + // keep going because it's not critical. + int exit_code = 0; + if (chdir("/") < 0) { + exit_code |= EXIT_FLAG_CHDIR_ERRORS; + } +#ifdef LINUX + bool close_errors = false; + if (fdwalk(&closefds, &close_errors) < 0) { + exit_code |= EXIT_FLAG_FDWALK_ERRORS; + } + if (close_errors) { + exit_code |= EXIT_FLAG_CLOSE_ERRORS; + } +#endif + + // Fork again to become a daemon. + pid = fork(); + // It is important that everything here use _exit() and not exit(), because + // exit() would call the destructors of all global variables in the whole + // process, which is both unnecessary and unsafe. + if (pid < 0) { + exit_code |= EXIT_FLAG_SECOND_FORK_FAILED; + _exit(exit_code); // if second fork failed + } else if (!pid) { + // Child. + // Successfully daemonized. Run command. + // POSIX requires the args to be typed as non-const for historical + // reasons, but it mandates that the actual implementation be const, so + // the cast is safe. + execvp(file, const_cast(argv)); + _exit(255); // if execvp failed + } + + // Parent. + // Successfully spawned process, but report any problems to the parent where + // we can log them. + _exit(exit_code); + } + + // Parent. Reap intermediate child. + int status; + pid_t child = waitpid(pid, &status, 0); + if (child < 0) { + LOG_ERR(LS_ERROR) << "Error in waitpid()"; + return false; + } + if (child != pid) { + // Should never happen (see man page). + LOG(LS_ERROR) << "waitpid() chose wrong child???"; + return false; + } + if (!WIFEXITED(status)) { + LOG(LS_ERROR) << "Intermediate child killed uncleanly"; // Probably crashed + return false; + } + + int exit_code = WEXITSTATUS(status); + if (exit_code & EXIT_FLAG_CHDIR_ERRORS) { + LOG(LS_WARNING) << "Child reported probles calling chdir()"; + } +#ifdef LINUX + if (exit_code & EXIT_FLAG_FDWALK_ERRORS) { + LOG(LS_WARNING) << "Child reported problems calling fdwalk()"; + } + if (exit_code & EXIT_FLAG_CLOSE_ERRORS) { + LOG(LS_WARNING) << "Child reported problems calling close()"; + } +#endif + if (exit_code & EXIT_FLAG_SECOND_FORK_FAILED) { + LOG(LS_ERROR) << "Failed to daemonize"; + // This means the command was not launched, so failure. + return false; + } + return true; +} + +} // namespace talk_base diff --git a/talk/base/posix.h b/talk/base/posix.h new file mode 100644 index 000000000..15c2c906a --- /dev/null +++ b/talk/base/posix.h @@ -0,0 +1,42 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_BASE_POSIX_H_ +#define TALK_BASE_POSIX_H_ + +namespace talk_base { + +// Runs the given executable name as a daemon, so that it executes concurrently +// with this process. Upon completion, the daemon process will automatically be +// reaped by init(8), so an error exit status or a failure to start the +// executable are not reported. Returns true if the daemon process was forked +// successfully, else false. +bool RunAsDaemon(const char *file, const char *const argv[]); + +} // namespace talk_base + +#endif // TALK_BASE_POSIX_H_ diff --git a/talk/base/profiler.cc b/talk/base/profiler.cc new file mode 100644 index 000000000..9d0f32bb2 --- /dev/null +++ b/talk/base/profiler.cc @@ -0,0 +1,171 @@ +/* + * libjingle + * Copyright 2013, 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/base/profiler.h" + +#include + +#include "talk/base/timeutils.h" + +namespace { + +// When written to an ostream, FormattedTime chooses an appropriate scale and +// suffix for a time value given in seconds. +class FormattedTime { + public: + explicit FormattedTime(double t) : time_(t) {} + double time() const { return time_; } + private: + double time_; +}; + +std::ostream& operator<<(std::ostream& stream, const FormattedTime& time) { + if (time.time() < 1.0) { + stream << (time.time() * 1000.0) << "ms"; + } else { + stream << time.time() << 's'; + } + return stream; +} + +} // namespace + +namespace talk_base { + +ProfilerEvent::ProfilerEvent() + : total_time_(0.0), + mean_(0.0), + sum_of_squared_differences_(0.0), + start_count_(0), + event_count_(0) { +} + +void ProfilerEvent::Start() { + if (start_count_ == 0) { + current_start_time_ = TimeNanos(); + } + ++start_count_; +} + +void ProfilerEvent::Stop() { + uint64 stop_time = TimeNanos(); + --start_count_; + ASSERT(start_count_ >= 0); + if (start_count_ == 0) { + double elapsed = static_cast(stop_time - current_start_time_) / + kNumNanosecsPerSec; + total_time_ += elapsed; + if (event_count_ == 0) { + minimum_ = maximum_ = elapsed; + } else { + minimum_ = _min(minimum_, elapsed); + maximum_ = _max(maximum_, elapsed); + } + // Online variance and mean algorithm: http://en.wikipedia.org/wiki/ + // Algorithms_for_calculating_variance#Online_algorithm + ++event_count_; + double delta = elapsed - mean_; + mean_ = mean_ + delta / event_count_; + sum_of_squared_differences_ += delta * (elapsed - mean_); + } +} + +double ProfilerEvent::standard_deviation() const { + if (event_count_ <= 1) return 0.0; + return sqrt(sum_of_squared_differences_ / (event_count_ - 1.0)); +} + +Profiler* Profiler::Instance() { + LIBJINGLE_DEFINE_STATIC_LOCAL(Profiler, instance, ()); + return &instance; +} + +void Profiler::StartEvent(const std::string& event_name) { + events_[event_name].Start(); +} + +void Profiler::StopEvent(const std::string& event_name) { + events_[event_name].Stop(); +} + +void Profiler::ReportToLog(const char* file, int line, + LoggingSeverity severity_to_use, + const std::string& event_prefix) { + if (!LogMessage::Loggable(severity_to_use)) { + return; + } + { // Output first line. + LogMessage msg(file, line, severity_to_use); + msg.stream() << "=== Profile report "; + if (event_prefix.empty()) { + msg.stream() << "(prefix: '" << event_prefix << "') "; + } + msg.stream() << "==="; + } + typedef std::map::const_iterator iterator; + for (iterator it = events_.begin(); it != events_.end(); ++it) { + if (event_prefix.empty() || it->first.find(event_prefix) == 0) { + LogMessage(file, line, severity_to_use).stream() + << it->first << " count=" << it->second.event_count() + << " total=" << FormattedTime(it->second.total_time()) + << " mean=" << FormattedTime(it->second.mean()) + << " min=" << FormattedTime(it->second.minimum()) + << " max=" << FormattedTime(it->second.maximum()) + << " sd=" << it->second.standard_deviation(); + } + } + LogMessage(file, line, severity_to_use).stream() + << "=== End profile report ==="; +} + +void Profiler::ReportAllToLog(const char* file, int line, + LoggingSeverity severity_to_use) { + ReportToLog(file, line, severity_to_use, ""); +} + +const ProfilerEvent* Profiler::GetEvent(const std::string& event_name) const { + std::map::const_iterator it = + events_.find(event_name); + return (it == events_.end()) ? NULL : &it->second; +} + +bool Profiler::Clear() { + bool result = true; + // Clear all events that aren't started. + std::map::iterator it = events_.begin(); + while (it != events_.end()) { + if (it->second.is_started()) { + ++it; // Can't clear started events. + result = false; + } else { + events_.erase(it++); + } + } + return result; +} + +} // namespace talk_base diff --git a/talk/base/profiler.h b/talk/base/profiler.h new file mode 100644 index 000000000..1198b8e26 --- /dev/null +++ b/talk/base/profiler.h @@ -0,0 +1,169 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +// A simple wall-clock profiler for instrumented code. +// Example: +// void MyLongFunction() { +// PROFILE_F(); // Time the execution of this function. +// // Do something +// { // Time just what is in this scope. +// PROFILE("My event"); +// // Do something else +// } +// } +// Another example: +// void StartAsyncProcess() { +// PROFILE_START("My event"); +// DoSomethingAsyncAndThenCall(&Callback); +// } +// void Callback() { +// PROFILE_STOP("My async event"); +// // Handle callback. +// } + +#ifndef TALK_BASE_PROFILER_H_ +#define TALK_BASE_PROFILER_H_ + +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" + +// Profiling could be switched via a build flag, but for now, it's always on. +#define ENABLE_PROFILING + +#ifdef ENABLE_PROFILING + +#define UV_HELPER2(x) _uv_ ## x +#define UV_HELPER(x) UV_HELPER2(x) +#define UNIQUE_VAR UV_HELPER(__LINE__) + +// Profiles the current scope. +#define PROFILE(msg) talk_base::ProfilerScope UNIQUE_VAR(msg) +// When placed at the start of a function, profiles the current function. +#define PROFILE_F() PROFILE(__FUNCTION__) +// Reports current timings to the log at severity |sev|. +#define PROFILE_DUMP_ALL(sev) \ + talk_base::Profiler::Instance()->ReportAllToLog(__FILE__, __LINE__, sev) +// Reports current timings for all events whose names are prefixed by |prefix| +// to the log at severity |sev|. Using a unique event name as |prefix| will +// report only that event. +#define PROFILE_DUMP(sev, prefix) \ + talk_base::Profiler::Instance()->ReportToLog(__FILE__, __LINE__, sev, prefix) +// Starts and stops a profile event. Useful when an event is not easily +// captured within a scope (eg, an async call with a callback when done). +#define PROFILE_START(msg) talk_base::Profiler::Instance()->StartEvent(msg) +#define PROFILE_STOP(msg) talk_base::Profiler::Instance()->StopEvent(msg) +// TODO(ryanpetrie): Consider adding PROFILE_DUMP_EVERY(sev, iterations) + +#undef UV_HELPER2 +#undef UV_HELPER +#undef UNIQUE_VAR + +#else // ENABLE_PROFILING + +#define PROFILE(msg) (void)0 +#define PROFILE_F() (void)0 +#define PROFILE_DUMP_ALL(sev) (void)0 +#define PROFILE_DUMP(sev, prefix) (void)0 +#define PROFILE_START(msg) (void)0 +#define PROFILE_STOP(msg) (void)0 + +#endif // ENABLE_PROFILING + +namespace talk_base { + +// Tracks information for one profiler event. +class ProfilerEvent { + public: + ProfilerEvent(); + void Start(); + void Stop(); + double standard_deviation() const; + double total_time() const { return total_time_; } + double mean() const { return mean_; } + double minimum() const { return minimum_; } + double maximum() const { return maximum_; } + int event_count() const { return event_count_; } + bool is_started() const { return start_count_ > 0; } + + private: + uint64 current_start_time_; + double total_time_; + double mean_; + double sum_of_squared_differences_; + double minimum_; + double maximum_; + int start_count_; + int event_count_; +}; + +// Singleton that owns ProfilerEvents and reports results. Prefer to use +// macros, defined above, rather than directly calling Profiler methods. +class Profiler { + public: + void StartEvent(const std::string& event_name); + void StopEvent(const std::string& event_name); + void ReportToLog(const char* file, int line, LoggingSeverity severity_to_use, + const std::string& event_prefix); + void ReportAllToLog(const char* file, int line, + LoggingSeverity severity_to_use); + const ProfilerEvent* GetEvent(const std::string& event_name) const; + // Clears all _stopped_ events. Returns true if _all_ events were cleared. + bool Clear(); + + static Profiler* Instance(); + private: + Profiler() {} + + std::map events_; + + DISALLOW_COPY_AND_ASSIGN(Profiler); +}; + +// Starts an event on construction and stops it on destruction. +// Used by PROFILE macro. +class ProfilerScope { + public: + explicit ProfilerScope(const std::string& event_name) + : event_name_(event_name) { + Profiler::Instance()->StartEvent(event_name_); + } + ~ProfilerScope() { + Profiler::Instance()->StopEvent(event_name_); + } + private: + std::string event_name_; + + DISALLOW_COPY_AND_ASSIGN(ProfilerScope); +}; + +} // namespace talk_base + +#endif // TALK_BASE_PROFILER_H_ diff --git a/talk/base/profiler_unittest.cc b/talk/base/profiler_unittest.cc new file mode 100644 index 000000000..f451e5fab --- /dev/null +++ b/talk/base/profiler_unittest.cc @@ -0,0 +1,126 @@ +/* + * libjingle + * Copyright 2004--2013, 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/base/gunit.h" +#include "talk/base/profiler.h" +#include "talk/base/thread.h" + +namespace { + +const int kWaitMs = 250; +const double kWaitSec = 0.250; +const double kTolerance = 0.1; + +const char* TestFunc() { + PROFILE_F(); + talk_base::Thread::SleepMs(kWaitMs); + return __FUNCTION__; +} + +} // namespace + +namespace talk_base { + +TEST(ProfilerTest, TestFunction) { + ASSERT_TRUE(Profiler::Instance()->Clear()); + // Profile a long-running function. + const char* function_name = TestFunc(); + const ProfilerEvent* event = Profiler::Instance()->GetEvent(function_name); + ASSERT_TRUE(event != NULL); + EXPECT_FALSE(event->is_started()); + EXPECT_EQ(1, event->event_count()); + EXPECT_NEAR(kWaitSec, event->mean(), kTolerance); + // Run it a second time. + TestFunc(); + EXPECT_FALSE(event->is_started()); + EXPECT_EQ(2, event->event_count()); + EXPECT_NEAR(kWaitSec, event->mean(), kTolerance); + EXPECT_NEAR(kWaitSec * 2, event->total_time(), kTolerance * 2); + EXPECT_DOUBLE_EQ(event->mean(), event->total_time() / event->event_count()); +} + +TEST(ProfilerTest, TestScopedEvents) { + const std::string kEvent1Name = "Event 1"; + const std::string kEvent2Name = "Event 2"; + const int kEvent2WaitMs = 150; + const double kEvent2WaitSec = 0.150; + const ProfilerEvent* event1; + const ProfilerEvent* event2; + ASSERT_TRUE(Profiler::Instance()->Clear()); + { // Profile a scope. + PROFILE(kEvent1Name); + event1 = Profiler::Instance()->GetEvent(kEvent1Name); + ASSERT_TRUE(event1 != NULL); + EXPECT_TRUE(event1->is_started()); + EXPECT_EQ(0, event1->event_count()); + talk_base::Thread::SleepMs(kWaitMs); + EXPECT_TRUE(event1->is_started()); + } + // Check the result. + EXPECT_FALSE(event1->is_started()); + EXPECT_EQ(1, event1->event_count()); + EXPECT_NEAR(kWaitSec, event1->mean(), kTolerance); + { // Profile a second event. + PROFILE(kEvent2Name); + event2 = Profiler::Instance()->GetEvent(kEvent2Name); + ASSERT_TRUE(event2 != NULL); + EXPECT_FALSE(event1->is_started()); + EXPECT_TRUE(event2->is_started()); + talk_base::Thread::SleepMs(kEvent2WaitMs); + } + // Check the result. + EXPECT_FALSE(event2->is_started()); + EXPECT_EQ(1, event2->event_count()); + EXPECT_NEAR(kEvent2WaitSec, event2->mean(), kTolerance); + // Make sure event1 is unchanged. + EXPECT_FALSE(event1->is_started()); + EXPECT_EQ(1, event1->event_count()); + { // Run another event 1. + PROFILE(kEvent1Name); + EXPECT_TRUE(event1->is_started()); + talk_base::Thread::SleepMs(kWaitMs); + } + // Check the result. + EXPECT_FALSE(event1->is_started()); + EXPECT_EQ(2, event1->event_count()); + EXPECT_NEAR(kWaitSec, event1->mean(), kTolerance); + EXPECT_NEAR(kWaitSec * 2, event1->total_time(), kTolerance * 2); + EXPECT_DOUBLE_EQ(event1->mean(), + event1->total_time() / event1->event_count()); +} + +TEST(ProfilerTest, Clear) { + ASSERT_TRUE(Profiler::Instance()->Clear()); + PROFILE_START("event"); + EXPECT_FALSE(Profiler::Instance()->Clear()); + EXPECT_TRUE(Profiler::Instance()->GetEvent("event") != NULL); + PROFILE_STOP("event"); + EXPECT_TRUE(Profiler::Instance()->Clear()); + EXPECT_EQ(NULL, Profiler::Instance()->GetEvent("event")); +} + +} // namespace talk_base diff --git a/talk/base/proxy_unittest.cc b/talk/base/proxy_unittest.cc new file mode 100644 index 000000000..4ace2925e --- /dev/null +++ b/talk/base/proxy_unittest.cc @@ -0,0 +1,152 @@ +/* + * libjingle + * Copyright 2009, 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 +#include "talk/base/autodetectproxy.h" +#include "talk/base/gunit.h" +#include "talk/base/httpserver.h" +#include "talk/base/proxyserver.h" +#include "talk/base/socketadapters.h" +#include "talk/base/testclient.h" +#include "talk/base/testechoserver.h" +#include "talk/base/virtualsocketserver.h" + +using talk_base::Socket; +using talk_base::Thread; +using talk_base::SocketAddress; + +static const SocketAddress kSocksProxyIntAddr("1.2.3.4", 1080); +static const SocketAddress kSocksProxyExtAddr("1.2.3.5", 0); +static const SocketAddress kHttpsProxyIntAddr("1.2.3.4", 443); +static const SocketAddress kHttpsProxyExtAddr("1.2.3.5", 0); +static const SocketAddress kBogusProxyIntAddr("1.2.3.4", 999); + +// Used to run a proxy detect on the current thread. Otherwise we would need +// to make both threads share the same VirtualSocketServer. +class AutoDetectProxyRunner : public talk_base::AutoDetectProxy { + public: + explicit AutoDetectProxyRunner(const std::string& agent) + : AutoDetectProxy(agent) {} + void Run() { + DoWork(); + Thread::Current()->Restart(); // needed to reset the messagequeue + } +}; + +// Sets up a virtual socket server and HTTPS/SOCKS5 proxy servers. +class ProxyTest : public testing::Test { + public: + ProxyTest() : ss_(new talk_base::VirtualSocketServer(NULL)) { + Thread::Current()->set_socketserver(ss_.get()); + socks_.reset(new talk_base::SocksProxyServer( + ss_.get(), kSocksProxyIntAddr, ss_.get(), kSocksProxyExtAddr)); + https_.reset(new talk_base::HttpListenServer()); + https_->Listen(kHttpsProxyIntAddr); + } + ~ProxyTest() { + Thread::Current()->set_socketserver(NULL); + } + + talk_base::SocketServer* ss() { return ss_.get(); } + + talk_base::ProxyType DetectProxyType(const SocketAddress& address) { + talk_base::ProxyType type; + AutoDetectProxyRunner* detect = new AutoDetectProxyRunner("unittest/1.0"); + detect->set_proxy(address); + detect->Run(); // blocks until done + type = detect->proxy().type; + detect->Destroy(false); + return type; + } + + private: + talk_base::scoped_ptr ss_; + talk_base::scoped_ptr socks_; + // TODO: Make this a real HTTPS proxy server. + talk_base::scoped_ptr https_; +}; + +// Tests whether we can use a SOCKS5 proxy to connect to a server. +TEST_F(ProxyTest, TestSocks5Connect) { + talk_base::AsyncSocket* socket = + ss()->CreateAsyncSocket(kSocksProxyIntAddr.family(), SOCK_STREAM); + talk_base::AsyncSocksProxySocket* proxy_socket = + new talk_base::AsyncSocksProxySocket(socket, kSocksProxyIntAddr, + "", talk_base::CryptString()); + // TODO: IPv6-ize these tests when proxy supports IPv6. + + talk_base::TestEchoServer server(Thread::Current(), + SocketAddress(INADDR_ANY, 0)); + + talk_base::AsyncTCPSocket* packet_socket = talk_base::AsyncTCPSocket::Create( + proxy_socket, SocketAddress(INADDR_ANY, 0), server.address()); + EXPECT_TRUE(packet_socket != NULL); + talk_base::TestClient client(packet_socket); + + EXPECT_EQ(Socket::CS_CONNECTING, proxy_socket->GetState()); + EXPECT_TRUE(client.CheckConnected()); + EXPECT_EQ(Socket::CS_CONNECTED, proxy_socket->GetState()); + EXPECT_EQ(server.address(), client.remote_address()); + client.Send("foo", 3); + EXPECT_TRUE(client.CheckNextPacket("foo", 3, NULL)); + EXPECT_TRUE(client.CheckNoPacket()); +} + +/* +// Tests whether we can use a HTTPS proxy to connect to a server. +TEST_F(ProxyTest, TestHttpsConnect) { + AsyncSocket* socket = ss()->CreateAsyncSocket(SOCK_STREAM); + AsyncHttpsProxySocket* proxy_socket = new AsyncHttpsProxySocket( + socket, "unittest/1.0", kHttpsProxyIntAddress, "", CryptString()); + TestClient client(new AsyncTCPSocket(proxy_socket)); + TestEchoServer server(Thread::Current(), SocketAddress()); + + EXPECT_TRUE(client.Connect(server.address())); + EXPECT_TRUE(client.CheckConnected()); + EXPECT_EQ(server.address(), client.remote_address()); + client.Send("foo", 3); + EXPECT_TRUE(client.CheckNextPacket("foo", 3, NULL)); + EXPECT_TRUE(client.CheckNoPacket()); +} +*/ + +// Tests whether we can autodetect a SOCKS5 proxy. +TEST_F(ProxyTest, TestAutoDetectSocks5) { + EXPECT_EQ(talk_base::PROXY_SOCKS5, DetectProxyType(kSocksProxyIntAddr)); +} + +/* +// Tests whether we can autodetect a HTTPS proxy. +TEST_F(ProxyTest, TestAutoDetectHttps) { + EXPECT_EQ(talk_base::PROXY_HTTPS, DetectProxyType(kHttpsProxyIntAddr)); +} +*/ + +// Tests whether we fail properly for no proxy. +TEST_F(ProxyTest, TestAutoDetectBogus) { + EXPECT_EQ(talk_base::PROXY_UNKNOWN, DetectProxyType(kBogusProxyIntAddr)); +} diff --git a/talk/base/proxydetect.cc b/talk/base/proxydetect.cc new file mode 100644 index 000000000..d3c998365 --- /dev/null +++ b/talk/base/proxydetect.cc @@ -0,0 +1,1263 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/proxydetect.h" + +#ifdef WIN32 +#include "talk/base/win32.h" +#include +#endif // WIN32 + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#ifdef OSX +#include +#include +#include +#include +#include "macconversion.h" +#endif + +#include + +#include "talk/base/fileutils.h" +#include "talk/base/httpcommon.h" +#include "talk/base/httpcommon-inl.h" +#include "talk/base/pathutils.h" +#include "talk/base/stringutils.h" + +#ifdef WIN32 +#define _TRY_WINHTTP 1 +#define _TRY_JSPROXY 0 +#define _TRY_WM_FINDPROXY 0 +#define _TRY_IE_LAN_SETTINGS 1 +#endif // WIN32 + +// For all platforms try Firefox. +#define _TRY_FIREFOX 1 + +// Use profiles.ini to find the correct profile for this user. +// If not set, we'll just look for the default one. +#define USE_FIREFOX_PROFILES_INI 1 + +static const size_t kMaxLineLength = 1024; +static const char kFirefoxPattern[] = "Firefox"; +static const char kInternetExplorerPattern[] = "MSIE"; + +struct StringMap { + public: + void Add(const char * name, const char * value) { map_[name] = value; } + const std::string& Get(const char * name, const char * def = "") const { + std::map::const_iterator it = + map_.find(name); + if (it != map_.end()) + return it->second; + def_ = def; + return def_; + } + bool IsSet(const char * name) const { + return (map_.find(name) != map_.end()); + } + private: + std::map map_; + mutable std::string def_; +}; + +enum UserAgent { + UA_FIREFOX, + UA_INTERNETEXPLORER, + UA_OTHER, + UA_UNKNOWN +}; + +#if _TRY_WINHTTP +//#include +// Note: From winhttp.h + +const char WINHTTP[] = "winhttp"; + +typedef LPVOID HINTERNET; + +typedef struct { + DWORD dwAccessType; // see WINHTTP_ACCESS_* types below + LPWSTR lpszProxy; // proxy server list + LPWSTR lpszProxyBypass; // proxy bypass list +} WINHTTP_PROXY_INFO, * LPWINHTTP_PROXY_INFO; + +typedef struct { + DWORD dwFlags; + DWORD dwAutoDetectFlags; + LPCWSTR lpszAutoConfigUrl; + LPVOID lpvReserved; + DWORD dwReserved; + BOOL fAutoLogonIfChallenged; +} WINHTTP_AUTOPROXY_OPTIONS; + +typedef struct { + BOOL fAutoDetect; + LPWSTR lpszAutoConfigUrl; + LPWSTR lpszProxy; + LPWSTR lpszProxyBypass; +} WINHTTP_CURRENT_USER_IE_PROXY_CONFIG; + +extern "C" { + typedef HINTERNET (WINAPI * pfnWinHttpOpen) + ( + IN LPCWSTR pwszUserAgent, + IN DWORD dwAccessType, + IN LPCWSTR pwszProxyName OPTIONAL, + IN LPCWSTR pwszProxyBypass OPTIONAL, + IN DWORD dwFlags + ); + typedef BOOL (STDAPICALLTYPE * pfnWinHttpCloseHandle) + ( + IN HINTERNET hInternet + ); + typedef BOOL (STDAPICALLTYPE * pfnWinHttpGetProxyForUrl) + ( + IN HINTERNET hSession, + IN LPCWSTR lpcwszUrl, + IN WINHTTP_AUTOPROXY_OPTIONS * pAutoProxyOptions, + OUT WINHTTP_PROXY_INFO * pProxyInfo + ); + typedef BOOL (STDAPICALLTYPE * pfnWinHttpGetIEProxyConfig) + ( + IN OUT WINHTTP_CURRENT_USER_IE_PROXY_CONFIG * pProxyConfig + ); + +} // extern "C" + +#define WINHTTP_AUTOPROXY_AUTO_DETECT 0x00000001 +#define WINHTTP_AUTOPROXY_CONFIG_URL 0x00000002 +#define WINHTTP_AUTOPROXY_RUN_INPROCESS 0x00010000 +#define WINHTTP_AUTOPROXY_RUN_OUTPROCESS_ONLY 0x00020000 +#define WINHTTP_AUTO_DETECT_TYPE_DHCP 0x00000001 +#define WINHTTP_AUTO_DETECT_TYPE_DNS_A 0x00000002 +#define WINHTTP_ACCESS_TYPE_DEFAULT_PROXY 0 +#define WINHTTP_ACCESS_TYPE_NO_PROXY 1 +#define WINHTTP_ACCESS_TYPE_NAMED_PROXY 3 +#define WINHTTP_NO_PROXY_NAME NULL +#define WINHTTP_NO_PROXY_BYPASS NULL + +#endif // _TRY_WINHTTP + +#if _TRY_JSPROXY +extern "C" { + typedef BOOL (STDAPICALLTYPE * pfnInternetGetProxyInfo) + ( + LPCSTR lpszUrl, + DWORD dwUrlLength, + LPSTR lpszUrlHostName, + DWORD dwUrlHostNameLength, + LPSTR * lplpszProxyHostName, + LPDWORD lpdwProxyHostNameLength + ); +} // extern "C" +#endif // _TRY_JSPROXY + +#if _TRY_WM_FINDPROXY +#include +#include +#include +#endif // _TRY_WM_FINDPROXY + +#if _TRY_IE_LAN_SETTINGS +#include +#include +#endif // _TRY_IE_LAN_SETTINGS + +namespace talk_base { + +////////////////////////////////////////////////////////////////////// +// Utility Functions +////////////////////////////////////////////////////////////////////// + +#ifdef WIN32 +#ifdef _UNICODE + +typedef std::wstring tstring; +std::string Utf8String(const tstring& str) { return ToUtf8(str); } + +#else // !_UNICODE + +typedef std::string tstring; +std::string Utf8String(const tstring& str) { return str; } + +#endif // !_UNICODE +#endif // WIN32 + +bool ProxyItemMatch(const Url& url, char * item, size_t len) { + // hostname:443 + if (char * port = ::strchr(item, ':')) { + *port++ = '\0'; + if (url.port() != atol(port)) { + return false; + } + } + + // A.B.C.D or A.B.C.D/24 + int a, b, c, d, m; + int match = sscanf(item, "%d.%d.%d.%d/%d", &a, &b, &c, &d, &m); + if (match >= 4) { + uint32 ip = ((a & 0xFF) << 24) | ((b & 0xFF) << 16) | ((c & 0xFF) << 8) | + (d & 0xFF); + if ((match < 5) || (m > 32)) + m = 32; + else if (m < 0) + m = 0; + uint32 mask = (m == 0) ? 0 : (~0UL) << (32 - m); + SocketAddress addr(url.host(), 0); + // TODO: Support IPv6 proxyitems. This code block is IPv4 only anyway. + return !addr.IsUnresolved() && + ((addr.ipaddr().v4AddressAsHostOrderInteger() & mask) == (ip & mask)); + } + + // .foo.com + if (*item == '.') { + size_t hostlen = url.host().length(); + return (hostlen > len) + && (stricmp(url.host().c_str() + (hostlen - len), item) == 0); + } + + // localhost or www.*.com + if (!string_match(url.host().c_str(), item)) + return false; + + return true; +} + +bool ProxyListMatch(const Url& url, const std::string& proxy_list, + char sep) { + const size_t BUFSIZE = 256; + char buffer[BUFSIZE]; + const char* list = proxy_list.c_str(); + while (*list) { + // Remove leading space + if (isspace(*list)) { + ++list; + continue; + } + // Break on separator + size_t len; + const char * start = list; + if (const char * end = ::strchr(list, sep)) { + len = (end - list); + list += len + 1; + } else { + len = strlen(list); + list += len; + } + // Remove trailing space + while ((len > 0) && isspace(start[len-1])) + --len; + // Check for oversized entry + if (len >= BUFSIZE) + continue; + memcpy(buffer, start, len); + buffer[len] = 0; + if (!ProxyItemMatch(url, buffer, len)) + continue; + return true; + } + return false; +} + +bool Better(ProxyType lhs, const ProxyType rhs) { + // PROXY_NONE, PROXY_HTTPS, PROXY_SOCKS5, PROXY_UNKNOWN + const int PROXY_VALUE[5] = { 0, 2, 3, 1 }; + return (PROXY_VALUE[lhs] > PROXY_VALUE[rhs]); +} + +bool ParseProxy(const std::string& saddress, ProxyInfo* proxy) { + const size_t kMaxAddressLength = 1024; + // Allow semicolon, space, or tab as an address separator + const char* const kAddressSeparator = " ;\t"; + + ProxyType ptype; + std::string host; + uint16 port; + + const char* address = saddress.c_str(); + while (*address) { + size_t len; + const char * start = address; + if (const char * sep = strchr(address, kAddressSeparator)) { + len = (sep - address); + address += len + 1; + while (*address != '\0' && ::strchr(kAddressSeparator, *address)) { + address += 1; + } + } else { + len = strlen(address); + address += len; + } + + if (len > kMaxAddressLength - 1) { + LOG(LS_WARNING) << "Proxy address too long [" << start << "]"; + continue; + } + + char buffer[kMaxAddressLength]; + memcpy(buffer, start, len); + buffer[len] = 0; + + char * colon = ::strchr(buffer, ':'); + if (!colon) { + LOG(LS_WARNING) << "Proxy address without port [" << buffer << "]"; + continue; + } + + *colon = 0; + char * endptr; + port = static_cast(strtol(colon + 1, &endptr, 0)); + if (*endptr != 0) { + LOG(LS_WARNING) << "Proxy address with invalid port [" << buffer << "]"; + continue; + } + + if (char * equals = ::strchr(buffer, '=')) { + *equals = 0; + host = equals + 1; + if (_stricmp(buffer, "socks") == 0) { + ptype = PROXY_SOCKS5; + } else if (_stricmp(buffer, "https") == 0) { + ptype = PROXY_HTTPS; + } else { + LOG(LS_WARNING) << "Proxy address with unknown protocol [" + << buffer << "]"; + ptype = PROXY_UNKNOWN; + } + } else { + host = buffer; + ptype = PROXY_UNKNOWN; + } + + if (Better(ptype, proxy->type)) { + proxy->type = ptype; + proxy->address.SetIP(host); + proxy->address.SetPort(port); + } + } + + return proxy->type != PROXY_NONE; +} + +UserAgent GetAgent(const char* agent) { + if (agent) { + std::string agent_str(agent); + if (agent_str.find(kFirefoxPattern) != std::string::npos) { + return UA_FIREFOX; + } else if (agent_str.find(kInternetExplorerPattern) != std::string::npos) { + return UA_INTERNETEXPLORER; + } else if (agent_str.empty()) { + return UA_UNKNOWN; + } + } + return UA_OTHER; +} + +bool EndsWith(const std::string& a, const std::string& b) { + if (b.size() > a.size()) { + return false; + } + int result = a.compare(a.size() - b.size(), b.size(), b); + return result == 0; +} + +bool GetFirefoxProfilePath(Pathname* path) { +#ifdef WIN32 + wchar_t w_path[MAX_PATH]; + if (SHGetFolderPath(0, CSIDL_APPDATA, 0, SHGFP_TYPE_CURRENT, w_path) != + S_OK) { + LOG(LS_ERROR) << "SHGetFolderPath failed"; + return false; + } + path->SetFolder(ToUtf8(w_path, wcslen(w_path))); + path->AppendFolder("Mozilla"); + path->AppendFolder("Firefox"); +#elif OSX + FSRef fr; + if (0 != FSFindFolder(kUserDomain, kApplicationSupportFolderType, + kCreateFolder, &fr)) { + LOG(LS_ERROR) << "FSFindFolder failed"; + return false; + } + char buffer[NAME_MAX + 1]; + if (0 != FSRefMakePath(&fr, reinterpret_cast(buffer), + ARRAY_SIZE(buffer))) { + LOG(LS_ERROR) << "FSRefMakePath failed"; + return false; + } + path->SetFolder(std::string(buffer)); + path->AppendFolder("Firefox"); +#else + char* user_home = getenv("HOME"); + if (user_home == NULL) { + return false; + } + path->SetFolder(std::string(user_home)); + path->AppendFolder(".mozilla"); + path->AppendFolder("firefox"); +#endif // WIN32 + return true; +} + +bool GetDefaultFirefoxProfile(Pathname* profile_path) { + ASSERT(NULL != profile_path); + Pathname path; + if (!GetFirefoxProfilePath(&path)) { + return false; + } + +#if USE_FIREFOX_PROFILES_INI + // [Profile0] + // Name=default + // IsRelative=1 + // Path=Profiles/2de53ejb.default + // Default=1 + + // Note: we are looking for the first entry with "Default=1", or the last + // entry in the file + path.SetFilename("profiles.ini"); + FileStream* fs = Filesystem::OpenFile(path, "r"); + if (!fs) { + return false; + } + Pathname candidate; + bool relative = true; + std::string line; + while (fs->ReadLine(&line) == SR_SUCCESS) { + if (line.length() == 0) { + continue; + } + if (line.at(0) == '[') { + relative = true; + candidate.clear(); + } else if (line.find("IsRelative=") == 0 && + line.length() >= 12) { + // TODO: The initial Linux public launch revealed a fairly + // high number of machines where IsRelative= did not have anything after + // it. Perhaps that is legal profiles.ini syntax? + relative = (line.at(11) != '0'); + } else if (line.find("Path=") == 0 && + line.length() >= 6) { + if (relative) { + candidate = path; + } else { + candidate.clear(); + } + candidate.AppendFolder(line.substr(5)); + } else if (line.find("Default=") == 0 && + line.length() >= 9) { + if ((line.at(8) != '0') && !candidate.empty()) { + break; + } + } + } + fs->Close(); + if (candidate.empty()) { + return false; + } + profile_path->SetPathname(candidate.pathname()); + +#else // !USE_FIREFOX_PROFILES_INI + path.AppendFolder("Profiles"); + DirectoryIterator* it = Filesystem::IterateDirectory(); + it->Iterate(path); + std::string extension(".default"); + while (!EndsWith(it->Name(), extension)) { + if (!it->Next()) { + return false; + } + } + + profile_path->SetPathname(path); + profile->AppendFolder("Profiles"); + profile->AppendFolder(it->Name()); + delete it; + +#endif // !USE_FIREFOX_PROFILES_INI + + return true; +} + +bool ReadFirefoxPrefs(const Pathname& filename, + const char * prefix, + StringMap* settings) { + FileStream* fs = Filesystem::OpenFile(filename, "r"); + if (!fs) { + LOG(LS_ERROR) << "Failed to open file: " << filename.pathname(); + return false; + } + + std::string line; + while (fs->ReadLine(&line) == SR_SUCCESS) { + size_t prefix_len = strlen(prefix); + + // Skip blank lines and too long lines. + if ((line.length() == 0) || (line.length() > kMaxLineLength) + || (line.at(0) == '#') || line.compare(0, 2, "/*") == 0 + || line.compare(0, 2, " *") == 0) { + continue; + } + + char buffer[kMaxLineLength]; + strcpyn(buffer, sizeof(buffer), line.c_str()); + int nstart = 0, nend = 0, vstart = 0, vend = 0; + sscanf(buffer, "user_pref(\"%n%*[^\"]%n\", %n%*[^)]%n);", + &nstart, &nend, &vstart, &vend); + if (vend > 0) { + char* name = buffer + nstart; + name[nend - nstart] = 0; + if ((vend - vstart >= 2) && (buffer[vstart] == '"')) { + vstart += 1; + vend -= 1; + } + char* value = buffer + vstart; + value[vend - vstart] = 0; + if ((strncmp(name, prefix, prefix_len) == 0) && *value) { + settings->Add(name + prefix_len, value); + } + } else { + LOG_F(LS_WARNING) << "Unparsed pref [" << buffer << "]"; + } + } + fs->Close(); + return true; +} + +bool GetFirefoxProxySettings(const char* url, ProxyInfo* proxy) { + Url purl(url); + Pathname path; + bool success = false; + if (GetDefaultFirefoxProfile(&path)) { + StringMap settings; + path.SetFilename("prefs.js"); + if (ReadFirefoxPrefs(path, "network.proxy.", &settings)) { + success = true; + proxy->bypass_list = + settings.Get("no_proxies_on", "localhost, 127.0.0.1"); + if (settings.Get("type") == "1") { + // User has manually specified a proxy, try to figure out what + // type it is. + if (ProxyListMatch(purl, proxy->bypass_list.c_str(), ',')) { + // Our url is in the list of url's to bypass proxy. + } else if (settings.Get("share_proxy_settings") == "true") { + proxy->type = PROXY_UNKNOWN; + proxy->address.SetIP(settings.Get("http")); + proxy->address.SetPort(atoi(settings.Get("http_port").c_str())); + } else if (settings.IsSet("socks")) { + proxy->type = PROXY_SOCKS5; + proxy->address.SetIP(settings.Get("socks")); + proxy->address.SetPort(atoi(settings.Get("socks_port").c_str())); + } else if (settings.IsSet("ssl")) { + proxy->type = PROXY_HTTPS; + proxy->address.SetIP(settings.Get("ssl")); + proxy->address.SetPort(atoi(settings.Get("ssl_port").c_str())); + } else if (settings.IsSet("http")) { + proxy->type = PROXY_HTTPS; + proxy->address.SetIP(settings.Get("http")); + proxy->address.SetPort(atoi(settings.Get("http_port").c_str())); + } + } else if (settings.Get("type") == "2") { + // Browser is configured to get proxy settings from a given url. + proxy->autoconfig_url = settings.Get("autoconfig_url").c_str(); + } else if (settings.Get("type") == "4") { + // Browser is configured to auto detect proxy config. + proxy->autodetect = true; + } else { + // No proxy set. + } + } + } + return success; +} + +#ifdef WIN32 // Windows specific implementation for reading Internet + // Explorer proxy settings. + +void LogGetProxyFault() { + LOG_GLEM(LERROR, WINHTTP) << "WinHttpGetProxyForUrl faulted!!"; +} + +BOOL MyWinHttpGetProxyForUrl(pfnWinHttpGetProxyForUrl pWHGPFU, + HINTERNET hWinHttp, LPCWSTR url, + WINHTTP_AUTOPROXY_OPTIONS *options, + WINHTTP_PROXY_INFO *info) { + // WinHttpGetProxyForUrl() can call plugins which can crash. + // In the case of McAfee scriptproxy.dll, it does crash in + // older versions. Try to catch crashes here and treat as an + // error. + BOOL success = FALSE; + +#if (_HAS_EXCEPTIONS == 0) + __try { + success = pWHGPFU(hWinHttp, url, options, info); + } __except(EXCEPTION_EXECUTE_HANDLER) { + // This is a separate function to avoid + // Visual C++ error 2712 when compiling with C++ EH + LogGetProxyFault(); + } +#else + success = pWHGPFU(hWinHttp, url, options, info); +#endif // (_HAS_EXCEPTIONS == 0) + + return success; +} + +bool IsDefaultBrowserFirefox() { + HKEY key; + LONG result = RegOpenKeyEx(HKEY_CLASSES_ROOT, L"http\\shell\\open\\command", + 0, KEY_READ, &key); + if (ERROR_SUCCESS != result) + return false; + + wchar_t* value = NULL; + DWORD size, type; + result = RegQueryValueEx(key, L"", 0, &type, NULL, &size); + if (REG_SZ != type) { + result = ERROR_ACCESS_DENIED; // Any error is fine + } else if (ERROR_SUCCESS == result) { + value = new wchar_t[size+1]; + BYTE* buffer = reinterpret_cast(value); + result = RegQueryValueEx(key, L"", 0, &type, buffer, &size); + } + RegCloseKey(key); + + bool success = false; + if (ERROR_SUCCESS == result) { + value[size] = L'\0'; + for (size_t i = 0; i < size; ++i) { + value[i] = tolowercase(value[i]); + } + success = (NULL != strstr(value, L"firefox.exe")); + } + delete [] value; + return success; +} + +bool GetWinHttpProxySettings(const char* url, ProxyInfo* proxy) { + HMODULE winhttp_handle = LoadLibrary(L"winhttp.dll"); + if (winhttp_handle == NULL) { + LOG(LS_ERROR) << "Failed to load winhttp.dll."; + return false; + } + WINHTTP_CURRENT_USER_IE_PROXY_CONFIG iecfg; + memset(&iecfg, 0, sizeof(iecfg)); + Url purl(url); + pfnWinHttpGetIEProxyConfig pWHGIEPC = + reinterpret_cast( + GetProcAddress(winhttp_handle, + "WinHttpGetIEProxyConfigForCurrentUser")); + bool success = false; + if (pWHGIEPC && pWHGIEPC(&iecfg)) { + // We were read proxy config successfully. + success = true; + if (iecfg.fAutoDetect) { + proxy->autodetect = true; + } + if (iecfg.lpszAutoConfigUrl) { + proxy->autoconfig_url = ToUtf8(iecfg.lpszAutoConfigUrl); + GlobalFree(iecfg.lpszAutoConfigUrl); + } + if (iecfg.lpszProxyBypass) { + proxy->bypass_list = ToUtf8(iecfg.lpszProxyBypass); + GlobalFree(iecfg.lpszProxyBypass); + } + if (iecfg.lpszProxy) { + if (!ProxyListMatch(purl, proxy->bypass_list, ';')) { + ParseProxy(ToUtf8(iecfg.lpszProxy), proxy); + } + GlobalFree(iecfg.lpszProxy); + } + } + FreeLibrary(winhttp_handle); + return success; +} + +// Uses the WinHTTP API to auto detect proxy for the given url. Firefox and IE +// have slightly different option dialogs for proxy settings. In Firefox, +// either a location of a proxy configuration file can be specified or auto +// detection can be selected. In IE theese two options can be independently +// selected. For the case where both options are selected (only IE) we try to +// fetch the config file first, and if that fails we'll perform an auto +// detection. +// +// Returns true if we successfully performed an auto detection not depending on +// whether we found a proxy or not. Returns false on error. +bool WinHttpAutoDetectProxyForUrl(const char* agent, const char* url, + ProxyInfo* proxy) { + Url purl(url); + bool success = true; + HMODULE winhttp_handle = LoadLibrary(L"winhttp.dll"); + if (winhttp_handle == NULL) { + LOG(LS_ERROR) << "Failed to load winhttp.dll."; + return false; + } + pfnWinHttpOpen pWHO = + reinterpret_cast(GetProcAddress(winhttp_handle, + "WinHttpOpen")); + pfnWinHttpCloseHandle pWHCH = + reinterpret_cast( + GetProcAddress(winhttp_handle, "WinHttpCloseHandle")); + pfnWinHttpGetProxyForUrl pWHGPFU = + reinterpret_cast( + GetProcAddress(winhttp_handle, "WinHttpGetProxyForUrl")); + if (pWHO && pWHCH && pWHGPFU) { + if (HINTERNET hWinHttp = pWHO(ToUtf16(agent).c_str(), + WINHTTP_ACCESS_TYPE_NO_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0)) { + BOOL result = FALSE; + WINHTTP_PROXY_INFO info; + memset(&info, 0, sizeof(info)); + if (proxy->autodetect) { + // Use DHCP and DNS to try to find any proxy to use. + WINHTTP_AUTOPROXY_OPTIONS options; + memset(&options, 0, sizeof(options)); + options.fAutoLogonIfChallenged = TRUE; + + options.dwFlags |= WINHTTP_AUTOPROXY_AUTO_DETECT; + options.dwAutoDetectFlags |= WINHTTP_AUTO_DETECT_TYPE_DHCP + | WINHTTP_AUTO_DETECT_TYPE_DNS_A; + result = MyWinHttpGetProxyForUrl( + pWHGPFU, hWinHttp, ToUtf16(url).c_str(), &options, &info); + } + if (!result && !proxy->autoconfig_url.empty()) { + // We have the location of a proxy config file. Download it and + // execute it to find proxy settings for our url. + WINHTTP_AUTOPROXY_OPTIONS options; + memset(&options, 0, sizeof(options)); + memset(&info, 0, sizeof(info)); + options.fAutoLogonIfChallenged = TRUE; + + std::wstring autoconfig_url16((ToUtf16)(proxy->autoconfig_url)); + options.dwFlags |= WINHTTP_AUTOPROXY_CONFIG_URL; + options.lpszAutoConfigUrl = autoconfig_url16.c_str(); + + result = MyWinHttpGetProxyForUrl( + pWHGPFU, hWinHttp, ToUtf16(url).c_str(), &options, &info); + } + if (result) { + // Either the given auto config url was valid or auto + // detection found a proxy on this network. + if (info.lpszProxy) { + // TODO: Does this bypass list differ from the list + // retreived from GetWinHttpProxySettings earlier? + if (info.lpszProxyBypass) { + proxy->bypass_list = ToUtf8(info.lpszProxyBypass); + GlobalFree(info.lpszProxyBypass); + } else { + proxy->bypass_list.clear(); + } + if (!ProxyListMatch(purl, proxy->bypass_list, ';')) { + // Found proxy for this URL. If parsing the address turns + // out ok then we are successful. + success = ParseProxy(ToUtf8(info.lpszProxy), proxy); + } + GlobalFree(info.lpszProxy); + } + } else { + // We could not find any proxy for this url. + LOG(LS_INFO) << "No proxy detected for " << url; + } + pWHCH(hWinHttp); + } + } else { + LOG(LS_ERROR) << "Failed loading WinHTTP functions."; + success = false; + } + FreeLibrary(winhttp_handle); + return success; +} + +#if 0 // Below functions currently not used. + +bool GetJsProxySettings(const char* url, ProxyInfo* proxy) { + Url purl(url); + bool success = false; + + if (HMODULE hModJS = LoadLibrary(_T("jsproxy.dll"))) { + pfnInternetGetProxyInfo pIGPI = + reinterpret_cast( + GetProcAddress(hModJS, "InternetGetProxyInfo")); + if (pIGPI) { + char proxy[256], host[256]; + memset(proxy, 0, sizeof(proxy)); + char * ptr = proxy; + DWORD proxylen = sizeof(proxy); + std::string surl = Utf8String(url); + DWORD hostlen = _snprintf(host, sizeof(host), "http%s://%S", + purl.secure() ? "s" : "", purl.server()); + if (pIGPI(surl.data(), surl.size(), host, hostlen, &ptr, &proxylen)) { + LOG(INFO) << "Proxy: " << proxy; + } else { + LOG_GLE(INFO) << "InternetGetProxyInfo"; + } + } + FreeLibrary(hModJS); + } + return success; +} + +bool GetWmProxySettings(const char* url, ProxyInfo* proxy) { + Url purl(url); + bool success = false; + + INSNetSourceCreator * nsc = 0; + HRESULT hr = CoCreateInstance(CLSID_ClientNetManager, 0, CLSCTX_ALL, + IID_INSNetSourceCreator, (LPVOID *) &nsc); + if (SUCCEEDED(hr)) { + if (SUCCEEDED(hr = nsc->Initialize())) { + VARIANT dispatch; + VariantInit(&dispatch); + if (SUCCEEDED(hr = nsc->GetNetSourceAdminInterface(L"http", &dispatch))) { + IWMSInternalAdminNetSource * ians = 0; + if (SUCCEEDED(hr = dispatch.pdispVal->QueryInterface( + IID_IWMSInternalAdminNetSource, (LPVOID *) &ians))) { + _bstr_t host(purl.server()); + BSTR proxy = 0; + BOOL bProxyEnabled = FALSE; + DWORD port, context = 0; + if (SUCCEEDED(hr = ians->FindProxyForURL( + L"http", host, &bProxyEnabled, &proxy, &port, &context))) { + success = true; + if (bProxyEnabled) { + _bstr_t sproxy = proxy; + proxy->ptype = PT_HTTPS; + proxy->host = sproxy; + proxy->port = port; + } + } + SysFreeString(proxy); + if (FAILED(hr = ians->ShutdownProxyContext(context))) { + LOG(LS_INFO) << "IWMSInternalAdminNetSource::ShutdownProxyContext" + << "failed: " << hr; + } + ians->Release(); + } + } + VariantClear(&dispatch); + if (FAILED(hr = nsc->Shutdown())) { + LOG(LS_INFO) << "INSNetSourceCreator::Shutdown failed: " << hr; + } + } + nsc->Release(); + } + return success; +} + +bool GetIePerConnectionProxySettings(const char* url, ProxyInfo* proxy) { + Url purl(url); + bool success = false; + + INTERNET_PER_CONN_OPTION_LIST list; + INTERNET_PER_CONN_OPTION options[3]; + memset(&list, 0, sizeof(list)); + memset(&options, 0, sizeof(options)); + + list.dwSize = sizeof(list); + list.dwOptionCount = 3; + list.pOptions = options; + options[0].dwOption = INTERNET_PER_CONN_FLAGS; + options[1].dwOption = INTERNET_PER_CONN_PROXY_SERVER; + options[2].dwOption = INTERNET_PER_CONN_PROXY_BYPASS; + DWORD dwSize = sizeof(list); + + if (!InternetQueryOption(0, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, + &dwSize)) { + LOG(LS_INFO) << "InternetQueryOption failed: " << GetLastError(); + } else if ((options[0].Value.dwValue & PROXY_TYPE_PROXY) != 0) { + success = true; + if (!ProxyListMatch(purl, nonnull(options[2].Value.pszValue), _T(';'))) { + ParseProxy(nonnull(options[1].Value.pszValue), proxy); + } + } else if ((options[0].Value.dwValue & PROXY_TYPE_DIRECT) != 0) { + success = true; + } else { + LOG(LS_INFO) << "unknown internet access type: " + << options[0].Value.dwValue; + } + if (options[1].Value.pszValue) { + GlobalFree(options[1].Value.pszValue); + } + if (options[2].Value.pszValue) { + GlobalFree(options[2].Value.pszValue); + } + return success; +} + +#endif // 0 + +// Uses the InternetQueryOption function to retrieve proxy settings +// from the registry. This will only give us the 'static' settings, +// ie, not any information about auto config etc. +bool GetIeLanProxySettings(const char* url, ProxyInfo* proxy) { + Url purl(url); + bool success = false; + + wchar_t buffer[1024]; + memset(buffer, 0, sizeof(buffer)); + INTERNET_PROXY_INFO * info = reinterpret_cast(buffer); + DWORD dwSize = sizeof(buffer); + + if (!InternetQueryOption(0, INTERNET_OPTION_PROXY, info, &dwSize)) { + LOG(LS_INFO) << "InternetQueryOption failed: " << GetLastError(); + } else if (info->dwAccessType == INTERNET_OPEN_TYPE_DIRECT) { + success = true; + } else if (info->dwAccessType == INTERNET_OPEN_TYPE_PROXY) { + success = true; + if (!ProxyListMatch(purl, nonnull(reinterpret_cast( + info->lpszProxyBypass)), ' ')) { + ParseProxy(nonnull(reinterpret_cast(info->lpszProxy)), + proxy); + } + } else { + LOG(LS_INFO) << "unknown internet access type: " << info->dwAccessType; + } + return success; +} + +bool GetIeProxySettings(const char* agent, const char* url, ProxyInfo* proxy) { + bool success = GetWinHttpProxySettings(url, proxy); + if (!success) { + // TODO: Should always call this if no proxy were detected by + // GetWinHttpProxySettings? + // WinHttp failed. Try using the InternetOptionQuery method instead. + return GetIeLanProxySettings(url, proxy); + } + return true; +} + +#endif // WIN32 + +#ifdef OSX // OSX specific implementation for reading system wide + // proxy settings. + +bool p_getProxyInfoForTypeFromDictWithKeys(ProxyInfo* proxy, + ProxyType type, + const CFDictionaryRef proxyDict, + const CFStringRef enabledKey, + const CFStringRef hostKey, + const CFStringRef portKey) { + // whether or not we set up the proxy info. + bool result = false; + + // we use this as a scratch variable for determining if operations + // succeeded. + bool converted = false; + + // the data we need to construct the SocketAddress for the proxy. + std::string hostname; + int port; + + if ((proxyDict != NULL) && + (CFGetTypeID(proxyDict) == CFDictionaryGetTypeID())) { + // CoreFoundation stuff that we'll have to get from + // the dictionaries and interpret or convert into more usable formats. + CFNumberRef enabledCFNum; + CFNumberRef portCFNum; + CFStringRef hostCFStr; + + enabledCFNum = (CFNumberRef)CFDictionaryGetValue(proxyDict, enabledKey); + + if (p_isCFNumberTrue(enabledCFNum)) { + // let's see if we can get the address and port. + hostCFStr = (CFStringRef)CFDictionaryGetValue(proxyDict, hostKey); + converted = p_convertHostCFStringRefToCPPString(hostCFStr, hostname); + if (converted) { + portCFNum = (CFNumberRef)CFDictionaryGetValue(proxyDict, portKey); + converted = p_convertCFNumberToInt(portCFNum, &port); + if (converted) { + // we have something enabled, with a hostname and a port. + // That's sufficient to set up the proxy info. + proxy->type = type; + proxy->address.SetIP(hostname); + proxy->address.SetPort(port); + result = true; + } + } + } + } + + return result; +} + +// Looks for proxy information in the given dictionary, +// return true if it found sufficient information to define one, +// false otherwise. This is guaranteed to not change the values in proxy +// unless a full-fledged proxy description was discovered in the dictionary. +// However, at the present time this does not support username or password. +// Checks first for a SOCKS proxy, then for HTTPS, then HTTP. +bool GetMacProxySettingsFromDictionary(ProxyInfo* proxy, + const CFDictionaryRef proxyDict) { + // the function result. + bool gotProxy = false; + + + // first we see if there's a SOCKS proxy in place. + gotProxy = p_getProxyInfoForTypeFromDictWithKeys(proxy, + PROXY_SOCKS5, + proxyDict, + kSCPropNetProxiesSOCKSEnable, + kSCPropNetProxiesSOCKSProxy, + kSCPropNetProxiesSOCKSPort); + + if (!gotProxy) { + // okay, no SOCKS proxy, let's look for https. + gotProxy = p_getProxyInfoForTypeFromDictWithKeys(proxy, + PROXY_HTTPS, + proxyDict, + kSCPropNetProxiesHTTPSEnable, + kSCPropNetProxiesHTTPSProxy, + kSCPropNetProxiesHTTPSPort); + if (!gotProxy) { + // Finally, try HTTP proxy. Note that flute doesn't + // differentiate between HTTPS and HTTP, hence we are using the + // same flute type here, ie. PROXY_HTTPS. + gotProxy = p_getProxyInfoForTypeFromDictWithKeys( + proxy, PROXY_HTTPS, proxyDict, kSCPropNetProxiesHTTPEnable, + kSCPropNetProxiesHTTPProxy, kSCPropNetProxiesHTTPPort); + } + } + return gotProxy; +} + +// TODO(hughv) Update keychain functions. They work on 10.8, but are depricated. +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +bool p_putPasswordInProxyInfo(ProxyInfo* proxy) { + bool result = true; // by default we assume we're good. + // for all we know there isn't any password. We'll set to false + // if we find a problem. + + // Ask the keychain for an internet password search for the given protocol. + OSStatus oss = 0; + SecKeychainAttributeList attrList; + attrList.count = 3; + SecKeychainAttribute attributes[3]; + attrList.attr = attributes; + + attributes[0].tag = kSecProtocolItemAttr; + attributes[0].length = sizeof(SecProtocolType); + SecProtocolType protocol; + switch (proxy->type) { + case PROXY_HTTPS : + protocol = kSecProtocolTypeHTTPS; + break; + case PROXY_SOCKS5 : + protocol = kSecProtocolTypeSOCKS; + break; + default : + LOG(LS_ERROR) << "asked for proxy password for unknown proxy type."; + result = false; + break; + } + attributes[0].data = &protocol; + + UInt32 port = proxy->address.port(); + attributes[1].tag = kSecPortItemAttr; + attributes[1].length = sizeof(UInt32); + attributes[1].data = &port; + + std::string ip = proxy->address.ipaddr().ToString(); + attributes[2].tag = kSecServerItemAttr; + attributes[2].length = ip.length(); + attributes[2].data = const_cast(ip.c_str()); + + if (result) { + LOG(LS_INFO) << "trying to get proxy username/password"; + SecKeychainSearchRef sref; + oss = SecKeychainSearchCreateFromAttributes(NULL, + kSecInternetPasswordItemClass, + &attrList, &sref); + if (0 == oss) { + LOG(LS_INFO) << "SecKeychainSearchCreateFromAttributes was good"; + // Get the first item, if there is one. + SecKeychainItemRef iref; + oss = SecKeychainSearchCopyNext(sref, &iref); + if (0 == oss) { + LOG(LS_INFO) << "...looks like we have the username/password data"; + // If there is, get the username and the password. + + SecKeychainAttributeInfo attribsToGet; + attribsToGet.count = 1; + UInt32 tag = kSecAccountItemAttr; + UInt32 format = CSSM_DB_ATTRIBUTE_FORMAT_STRING; + void *data; + UInt32 length; + SecKeychainAttributeList *localList; + + attribsToGet.tag = &tag; + attribsToGet.format = &format; + OSStatus copyres = SecKeychainItemCopyAttributesAndData(iref, + &attribsToGet, + NULL, + &localList, + &length, + &data); + if (0 == copyres) { + LOG(LS_INFO) << "...and we can pull it out."; + // now, we know from experimentation (sadly not from docs) + // that the username is in the local attribute list, + // and the password in the data, + // both without null termination but with info on their length. + // grab the password from the data. + std::string password; + password.append(static_cast(data), length); + + // make the password into a CryptString + // huh, at the time of writing, you can't. + // so we'll skip that for now and come back to it later. + + // now put the username in the proxy. + if (1 <= localList->attr->length) { + proxy->username.append( + static_cast(localList->attr->data), + localList->attr->length); + LOG(LS_INFO) << "username is " << proxy->username; + } else { + LOG(LS_ERROR) << "got keychain entry with no username"; + result = false; + } + } else { + LOG(LS_ERROR) << "couldn't copy info from keychain."; + result = false; + } + SecKeychainItemFreeAttributesAndData(localList, data); + } else if (errSecItemNotFound == oss) { + LOG(LS_INFO) << "...username/password info not found"; + } else { + // oooh, neither 0 nor itemNotFound. + LOG(LS_ERROR) << "Couldn't get keychain information, error code" << oss; + result = false; + } + } else if (errSecItemNotFound == oss) { // noop + } else { + // oooh, neither 0 nor itemNotFound. + LOG(LS_ERROR) << "Couldn't get keychain information, error code" << oss; + result = false; + } + } + + return result; +} + +bool GetMacProxySettings(ProxyInfo* proxy) { + // based on the Apple Technical Q&A QA1234 + // http://developer.apple.com/qa/qa2001/qa1234.html + CFDictionaryRef proxyDict = SCDynamicStoreCopyProxies(NULL); + bool result = false; + + if (proxyDict != NULL) { + // sending it off to another function makes it easier to unit test + // since we can make our own dictionary to hand to that function. + result = GetMacProxySettingsFromDictionary(proxy, proxyDict); + + if (result) { + result = p_putPasswordInProxyInfo(proxy); + } + + // We created the dictionary with something that had the + // word 'copy' in it, so we have to release it, according + // to the Carbon memory management standards. + CFRelease(proxyDict); + } else { + LOG(LS_ERROR) << "SCDynamicStoreCopyProxies failed"; + } + + return result; +} +#endif // OSX + +bool AutoDetectProxySettings(const char* agent, const char* url, + ProxyInfo* proxy) { +#ifdef WIN32 + return WinHttpAutoDetectProxyForUrl(agent, url, proxy); +#else + LOG(LS_WARNING) << "Proxy auto-detection not implemented for this platform"; + return false; +#endif +} + +bool GetSystemDefaultProxySettings(const char* agent, const char* url, + ProxyInfo* proxy) { +#ifdef WIN32 + return GetIeProxySettings(agent, url, proxy); +#elif OSX + return GetMacProxySettings(proxy); +#else + // TODO: Get System settings if browser is not firefox. + return GetFirefoxProxySettings(url, proxy); +#endif +} + +bool GetProxySettingsForUrl(const char* agent, const char* url, + ProxyInfo* proxy, bool long_operation) { + UserAgent a = GetAgent(agent); + bool result; + switch (a) { + case UA_FIREFOX: { + result = GetFirefoxProxySettings(url, proxy); + break; + } +#ifdef WIN32 + case UA_INTERNETEXPLORER: + result = GetIeProxySettings(agent, url, proxy); + break; + case UA_UNKNOWN: + // Agent not defined, check default browser. + if (IsDefaultBrowserFirefox()) { + result = GetFirefoxProxySettings(url, proxy); + } else { + result = GetIeProxySettings(agent, url, proxy); + } + break; +#endif // WIN32 + default: + result = GetSystemDefaultProxySettings(agent, url, proxy); + break; + } + + // TODO: Consider using the 'long_operation' parameter to + // decide whether to do the auto detection. + if (result && (proxy->autodetect || + !proxy->autoconfig_url.empty())) { + // Use WinHTTP to auto detect proxy for us. + result = AutoDetectProxySettings(agent, url, proxy); + if (!result) { + // Either auto detection is not supported or we simply didn't + // find any proxy, reset type. + proxy->type = talk_base::PROXY_NONE; + } + } + return result; +} + +} // namespace talk_base diff --git a/talk/base/proxydetect.h b/talk/base/proxydetect.h new file mode 100644 index 000000000..4218fbb0d --- /dev/null +++ b/talk/base/proxydetect.h @@ -0,0 +1,48 @@ +/* + * libjingle + * Copyright 2007, 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. + */ + +#ifndef _PROXYDETECT_H_ +#define _PROXYDETECT_H_ + +#include "talk/base/proxyinfo.h" + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +namespace talk_base { +// Auto-detect the proxy server. Returns true if a proxy is configured, +// although hostname may be empty if the proxy is not required for +// the given URL. + +bool GetProxySettingsForUrl(const char* agent, const char* url, + talk_base::ProxyInfo* proxy, + bool long_operation = false); + +} // namespace talk_base + +#endif // _PROXYDETECT_H_ diff --git a/talk/base/proxydetect_unittest.cc b/talk/base/proxydetect_unittest.cc new file mode 100644 index 000000000..685066d94 --- /dev/null +++ b/talk/base/proxydetect_unittest.cc @@ -0,0 +1,182 @@ +/* + * libjingle + * Copyright 2010, 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 + +#include "talk/base/fileutils_mock.h" +#include "talk/base/proxydetect.h" + +namespace talk_base { + +static const std::string kFirefoxProfilesIni = + "[Profile0]\n" + "Name=default\n" + "IsRelative=1\n" + "Path=Profiles/2de53ejb.default\n" + "Default=1\n"; + +static const std::string kFirefoxHeader = + "# Mozilla User Preferences\n" + "\n" + "/* Some Comments\n" + "*\n" + "*/\n" + "\n"; + +static const std::string kFirefoxCorruptHeader = + "iuahueqe32164"; + +static const std::string kProxyAddress = "proxy.net.com"; +static const int kProxyPort = 9999; + +// Mocking out platform specific path to firefox prefs file. +class FirefoxPrefsFileSystem : public FakeFileSystem { + public: + explicit FirefoxPrefsFileSystem(const std::vector& all_files) : + FakeFileSystem(all_files) { + } + virtual FileStream* OpenFile(const Pathname& filename, + const std::string& mode) { + // TODO: We could have a platform dependent check of paths here. + std::string name = filename.basename(); + name.append(filename.extension()); + EXPECT_TRUE(name.compare("prefs.js") == 0 || + name.compare("profiles.ini") == 0); + FileStream* stream = FakeFileSystem::OpenFile(name, mode); + return stream; + } +}; + +class ProxyDetectTest : public testing::Test { +}; + +bool GetProxyInfo(const std::string prefs, ProxyInfo* info) { + std::vector files; + files.push_back(talk_base::FakeFileSystem::File("profiles.ini", + kFirefoxProfilesIni)); + files.push_back(talk_base::FakeFileSystem::File("prefs.js", prefs)); + talk_base::FilesystemScope fs(new talk_base::FirefoxPrefsFileSystem(files)); + return GetProxySettingsForUrl("Firefox", "www.google.com", info, false); +} + +// Verifies that an empty Firefox prefs file results in no proxy detected. +TEST_F(ProxyDetectTest, DISABLED_TestFirefoxEmptyPrefs) { + ProxyInfo proxy_info; + EXPECT_TRUE(GetProxyInfo(kFirefoxHeader, &proxy_info)); + EXPECT_EQ(PROXY_NONE, proxy_info.type); +} + +// Verifies that corrupted prefs file results in no proxy detected. +TEST_F(ProxyDetectTest, DISABLED_TestFirefoxCorruptedPrefs) { + ProxyInfo proxy_info; + EXPECT_TRUE(GetProxyInfo(kFirefoxCorruptHeader, &proxy_info)); + EXPECT_EQ(PROXY_NONE, proxy_info.type); +} + +// Verifies that SOCKS5 proxy is detected if configured. SOCKS uses a +// handshake protocol to inform the proxy software about the +// connection that the client is trying to make and may be used for +// any form of TCP or UDP socket connection. +TEST_F(ProxyDetectTest, DISABLED_TestFirefoxProxySocks) { + ProxyInfo proxy_info; + SocketAddress proxy_address("proxy.socks.com", 6666); + std::string prefs(kFirefoxHeader); + prefs.append("user_pref(\"network.proxy.socks\", \"proxy.socks.com\");\n"); + prefs.append("user_pref(\"network.proxy.socks_port\", 6666);\n"); + prefs.append("user_pref(\"network.proxy.type\", 1);\n"); + + EXPECT_TRUE(GetProxyInfo(prefs, &proxy_info)); + + EXPECT_EQ(PROXY_SOCKS5, proxy_info.type); + EXPECT_EQ(proxy_address, proxy_info.address); +} + +// Verified that SSL proxy is detected if configured. SSL proxy is an +// extention of a HTTP proxy to support secure connections. +TEST_F(ProxyDetectTest, DISABLED_TestFirefoxProxySsl) { + ProxyInfo proxy_info; + SocketAddress proxy_address("proxy.ssl.com", 7777); + std::string prefs(kFirefoxHeader); + + prefs.append("user_pref(\"network.proxy.ssl\", \"proxy.ssl.com\");\n"); + prefs.append("user_pref(\"network.proxy.ssl_port\", 7777);\n"); + prefs.append("user_pref(\"network.proxy.type\", 1);\n"); + + EXPECT_TRUE(GetProxyInfo(prefs, &proxy_info)); + + EXPECT_EQ(PROXY_HTTPS, proxy_info.type); + EXPECT_EQ(proxy_address, proxy_info.address); +} + +// Verifies that a HTTP proxy is detected if configured. +TEST_F(ProxyDetectTest, DISABLED_TestFirefoxProxyHttp) { + ProxyInfo proxy_info; + SocketAddress proxy_address("proxy.http.com", 8888); + std::string prefs(kFirefoxHeader); + + prefs.append("user_pref(\"network.proxy.http\", \"proxy.http.com\");\n"); + prefs.append("user_pref(\"network.proxy.http_port\", 8888);\n"); + prefs.append("user_pref(\"network.proxy.type\", 1);\n"); + + EXPECT_TRUE(GetProxyInfo(prefs, &proxy_info)); + + EXPECT_EQ(PROXY_HTTPS, proxy_info.type); + EXPECT_EQ(proxy_address, proxy_info.address); +} + +// Verifies detection of automatic proxy detection. +TEST_F(ProxyDetectTest, DISABLED_TestFirefoxProxyAuto) { + ProxyInfo proxy_info; + std::string prefs(kFirefoxHeader); + + prefs.append("user_pref(\"network.proxy.type\", 4);\n"); + + EXPECT_TRUE(GetProxyInfo(prefs, &proxy_info)); + + EXPECT_EQ(PROXY_NONE, proxy_info.type); + EXPECT_TRUE(proxy_info.autodetect); + EXPECT_TRUE(proxy_info.autoconfig_url.empty()); +} + +// Verifies detection of automatic proxy detection using a static url +// to config file. +TEST_F(ProxyDetectTest, DISABLED_TestFirefoxProxyAutoUrl) { + ProxyInfo proxy_info; + std::string prefs(kFirefoxHeader); + + prefs.append( + "user_pref(\"network.proxy.autoconfig_url\", \"http://a/b.pac\");\n"); + prefs.append("user_pref(\"network.proxy.type\", 2);\n"); + + EXPECT_TRUE(GetProxyInfo(prefs, &proxy_info)); + + EXPECT_FALSE(proxy_info.autodetect); + EXPECT_EQ(PROXY_NONE, proxy_info.type); + EXPECT_EQ(0, proxy_info.autoconfig_url.compare("http://a/b.pac")); +} + +} // namespace talk_base diff --git a/talk/base/proxyinfo.cc b/talk/base/proxyinfo.cc new file mode 100644 index 000000000..1d9c588a6 --- /dev/null +++ b/talk/base/proxyinfo.cc @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/proxyinfo.h" + +namespace talk_base { + +const char * ProxyToString(ProxyType proxy) { + const char * const PROXY_NAMES[] = { "none", "https", "socks5", "unknown" }; + return PROXY_NAMES[proxy]; +} + +} // namespace talk_base diff --git a/talk/base/proxyinfo.h b/talk/base/proxyinfo.h new file mode 100644 index 000000000..9e28f1ab1 --- /dev/null +++ b/talk/base/proxyinfo.h @@ -0,0 +1,59 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_PROXYINFO_H__ +#define TALK_BASE_PROXYINFO_H__ + +#include +#include "talk/base/socketaddress.h" +#include "talk/base/cryptstring.h" + +namespace talk_base { + +enum ProxyType { + PROXY_NONE, + PROXY_HTTPS, + PROXY_SOCKS5, + PROXY_UNKNOWN +}; +const char * ProxyToString(ProxyType proxy); + +struct ProxyInfo { + ProxyType type; + SocketAddress address; + std::string autoconfig_url; + bool autodetect; + std::string bypass_list; + std::string username; + CryptString password; + + ProxyInfo() : type(PROXY_NONE), autodetect(false) { } +}; + +} // namespace talk_base + +#endif // TALK_BASE_PROXYINFO_H__ diff --git a/talk/base/proxyserver.cc b/talk/base/proxyserver.cc new file mode 100644 index 000000000..416a5c7e5 --- /dev/null +++ b/talk/base/proxyserver.cc @@ -0,0 +1,161 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/proxyserver.h" + +#include +#include "talk/base/socketfactory.h" + +namespace talk_base { + +// ProxyServer +ProxyServer::ProxyServer( + SocketFactory* int_factory, const SocketAddress& int_addr, + SocketFactory* ext_factory, const SocketAddress& ext_ip) + : ext_factory_(ext_factory), ext_ip_(ext_ip.ipaddr(), 0), // strip off port + server_socket_(int_factory->CreateAsyncSocket(int_addr.family(), + SOCK_STREAM)) { + ASSERT(server_socket_.get() != NULL); + ASSERT(int_addr.family() == AF_INET || int_addr.family() == AF_INET6); + server_socket_->Bind(int_addr); + server_socket_->Listen(5); + server_socket_->SignalReadEvent.connect(this, &ProxyServer::OnAcceptEvent); +} + +ProxyServer::~ProxyServer() { + for (BindingList::iterator it = bindings_.begin(); + it != bindings_.end(); ++it) { + delete (*it); + } +} + +void ProxyServer::OnAcceptEvent(AsyncSocket* socket) { + ASSERT(socket != NULL && socket == server_socket_.get()); + AsyncSocket* int_socket = socket->Accept(NULL); + AsyncProxyServerSocket* wrapped_socket = WrapSocket(int_socket); + AsyncSocket* ext_socket = ext_factory_->CreateAsyncSocket(ext_ip_.family(), + SOCK_STREAM); + if (ext_socket) { + ext_socket->Bind(ext_ip_); + bindings_.push_back(new ProxyBinding(wrapped_socket, ext_socket)); + } else { + LOG(LS_ERROR) << "Unable to create external socket on proxy accept event"; + } +} + +void ProxyServer::OnBindingDestroyed(ProxyBinding* binding) { + BindingList::iterator it = + std::find(bindings_.begin(), bindings_.end(), binding); + delete (*it); + bindings_.erase(it); +} + +// ProxyBinding +ProxyBinding::ProxyBinding(AsyncProxyServerSocket* int_socket, + AsyncSocket* ext_socket) + : int_socket_(int_socket), ext_socket_(ext_socket), connected_(false), + out_buffer_(kBufferSize), in_buffer_(kBufferSize) { + int_socket_->SignalConnectRequest.connect(this, + &ProxyBinding::OnConnectRequest); + int_socket_->SignalReadEvent.connect(this, &ProxyBinding::OnInternalRead); + int_socket_->SignalWriteEvent.connect(this, &ProxyBinding::OnInternalWrite); + int_socket_->SignalCloseEvent.connect(this, &ProxyBinding::OnInternalClose); + ext_socket_->SignalConnectEvent.connect(this, + &ProxyBinding::OnExternalConnect); + ext_socket_->SignalReadEvent.connect(this, &ProxyBinding::OnExternalRead); + ext_socket_->SignalWriteEvent.connect(this, &ProxyBinding::OnExternalWrite); + ext_socket_->SignalCloseEvent.connect(this, &ProxyBinding::OnExternalClose); +} + +void ProxyBinding::OnConnectRequest(AsyncProxyServerSocket* socket, + const SocketAddress& addr) { + ASSERT(!connected_ && ext_socket_.get() != NULL); + ext_socket_->Connect(addr); + // TODO: handle errors here +} + +void ProxyBinding::OnInternalRead(AsyncSocket* socket) { + Read(int_socket_.get(), &out_buffer_); + Write(ext_socket_.get(), &out_buffer_); +} + +void ProxyBinding::OnInternalWrite(AsyncSocket* socket) { + Write(int_socket_.get(), &in_buffer_); +} + +void ProxyBinding::OnInternalClose(AsyncSocket* socket, int err) { + Destroy(); +} + +void ProxyBinding::OnExternalConnect(AsyncSocket* socket) { + ASSERT(socket != NULL); + connected_ = true; + int_socket_->SendConnectResult(0, socket->GetRemoteAddress()); +} + +void ProxyBinding::OnExternalRead(AsyncSocket* socket) { + Read(ext_socket_.get(), &in_buffer_); + Write(int_socket_.get(), &in_buffer_); +} + +void ProxyBinding::OnExternalWrite(AsyncSocket* socket) { + Write(ext_socket_.get(), &out_buffer_); +} + +void ProxyBinding::OnExternalClose(AsyncSocket* socket, int err) { + if (!connected_) { + int_socket_->SendConnectResult(err, SocketAddress()); + } + Destroy(); +} + +void ProxyBinding::Read(AsyncSocket* socket, FifoBuffer* buffer) { + // Only read if the buffer is empty. + ASSERT(socket != NULL); + size_t size; + int read; + if (buffer->GetBuffered(&size) && size == 0) { + void* p = buffer->GetWriteBuffer(&size); + read = socket->Recv(p, size); + buffer->ConsumeWriteBuffer(_max(read, 0)); + } +} + +void ProxyBinding::Write(AsyncSocket* socket, FifoBuffer* buffer) { + ASSERT(socket != NULL); + size_t size; + int written; + const void* p = buffer->GetReadData(&size); + written = socket->Send(p, size); + buffer->ConsumeReadData(_max(written, 0)); +} + +void ProxyBinding::Destroy() { + SignalDestroyed(this); +} + +} // namespace talk_base diff --git a/talk/base/proxyserver.h b/talk/base/proxyserver.h new file mode 100644 index 000000000..8e1ab6b78 --- /dev/null +++ b/talk/base/proxyserver.h @@ -0,0 +1,113 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_PROXYSERVER_H_ +#define TALK_BASE_PROXYSERVER_H_ + +#include +#include "talk/base/asyncsocket.h" +#include "talk/base/socketadapters.h" +#include "talk/base/socketaddress.h" +#include "talk/base/stream.h" + +namespace talk_base { + +class SocketFactory; + +// ProxyServer is a base class that allows for easy construction of proxy +// servers. With its helper class ProxyBinding, it contains all the necessary +// logic for receiving and bridging connections. The specific client-server +// proxy protocol is implemented by an instance of the AsyncProxyServerSocket +// class; children of ProxyServer implement WrapSocket appropriately to return +// the correct protocol handler. + +class ProxyBinding : public sigslot::has_slots<> { + public: + ProxyBinding(AsyncProxyServerSocket* in_socket, AsyncSocket* out_socket); + sigslot::signal1 SignalDestroyed; + + private: + void OnConnectRequest(AsyncProxyServerSocket* socket, + const SocketAddress& addr); + void OnInternalRead(AsyncSocket* socket); + void OnInternalWrite(AsyncSocket* socket); + void OnInternalClose(AsyncSocket* socket, int err); + void OnExternalConnect(AsyncSocket* socket); + void OnExternalRead(AsyncSocket* socket); + void OnExternalWrite(AsyncSocket* socket); + void OnExternalClose(AsyncSocket* socket, int err); + + static void Read(AsyncSocket* socket, FifoBuffer* buffer); + static void Write(AsyncSocket* socket, FifoBuffer* buffer); + void Destroy(); + + static const int kBufferSize = 4096; + scoped_ptr int_socket_; + scoped_ptr ext_socket_; + bool connected_; + FifoBuffer out_buffer_; + FifoBuffer in_buffer_; + DISALLOW_EVIL_CONSTRUCTORS(ProxyBinding); +}; + +class ProxyServer : public sigslot::has_slots<> { + public: + ProxyServer(SocketFactory* int_factory, const SocketAddress& int_addr, + SocketFactory* ext_factory, const SocketAddress& ext_ip); + virtual ~ProxyServer(); + + protected: + void OnAcceptEvent(AsyncSocket* socket); + virtual AsyncProxyServerSocket* WrapSocket(AsyncSocket* socket) = 0; + void OnBindingDestroyed(ProxyBinding* binding); + + private: + typedef std::list BindingList; + SocketFactory* ext_factory_; + SocketAddress ext_ip_; + scoped_ptr server_socket_; + BindingList bindings_; + DISALLOW_EVIL_CONSTRUCTORS(ProxyServer); +}; + +// SocksProxyServer is a simple extension of ProxyServer to implement SOCKS. +class SocksProxyServer : public ProxyServer { + public: + SocksProxyServer(SocketFactory* int_factory, const SocketAddress& int_addr, + SocketFactory* ext_factory, const SocketAddress& ext_ip) + : ProxyServer(int_factory, int_addr, ext_factory, ext_ip) { + } + protected: + AsyncProxyServerSocket* WrapSocket(AsyncSocket* socket) { + return new AsyncSocksProxyServerSocket(socket); + } + DISALLOW_EVIL_CONSTRUCTORS(SocksProxyServer); +}; + +} // namespace talk_base + +#endif // TALK_BASE_PROXYSERVER_H_ diff --git a/talk/base/ratelimiter.cc b/talk/base/ratelimiter.cc new file mode 100644 index 000000000..6df7a18d9 --- /dev/null +++ b/talk/base/ratelimiter.cc @@ -0,0 +1,46 @@ +/* + * libjingle + * Copyright 2012, 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/base/ratelimiter.h" + +namespace talk_base { + +bool RateLimiter::CanUse(size_t desired, double time) { + return ((time > period_end_ && desired <= max_per_period_) || + (used_in_period_ + desired) <= max_per_period_); +} + +void RateLimiter::Use(size_t used, double time) { + if (time > period_end_) { + period_start_ = time; + period_end_ = time + period_length_; + used_in_period_ = 0; + } + used_in_period_ += used; +} + +} // namespace talk_base diff --git a/talk/base/ratelimiter.h b/talk/base/ratelimiter.h new file mode 100644 index 000000000..255afb4ad --- /dev/null +++ b/talk/base/ratelimiter.h @@ -0,0 +1,80 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_BASE_RATELIMITER_H_ +#define TALK_BASE_RATELIMITER_H_ + +#include +#include "talk/base/basictypes.h" + +namespace talk_base { + +// Limits the rate of use to a certain maximum quantity per period of +// time. Use, for example, for simple bandwidth throttling. +// +// It's implemented like a diet plan: You have so many calories per +// day. If you hit the limit, you can't eat any more until the next +// day. +class RateLimiter { + public: + // For example, 100kb per second. + RateLimiter(size_t max, double period) + : max_per_period_(max), + period_length_(period), + used_in_period_(0), + period_start_(0.0), + period_end_(period) { + } + virtual ~RateLimiter() {} + + // Returns true if if the desired quantity is available in the + // current period (< (max - used)). Once the given time passes the + // end of the period, used is set to zero and more use is available. + bool CanUse(size_t desired, double time); + // Increment the quantity used this period. If past the end of a + // period, a new period is started. + void Use(size_t used, double time); + + size_t used_in_period() const { + return used_in_period_; + } + + size_t max_per_period() const { + return max_per_period_; + } + + private: + size_t max_per_period_; + double period_length_; + size_t used_in_period_; + double period_start_; + double period_end_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_RATELIMITER_H_ diff --git a/talk/base/ratelimiter_unittest.cc b/talk/base/ratelimiter_unittest.cc new file mode 100644 index 000000000..3c1a1df43 --- /dev/null +++ b/talk/base/ratelimiter_unittest.cc @@ -0,0 +1,76 @@ +/* + * libjingle + * Copyright 2012, 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/base/gunit.h" +#include "talk/base/ratelimiter.h" + +namespace talk_base { + +TEST(RateLimiterTest, TestCanUse) { + // Diet: Can eat 2,000 calories per day. + RateLimiter limiter = RateLimiter(2000, 1.0); + + double monday = 1.0; + double tuesday = 2.0; + double thursday = 4.0; + + EXPECT_TRUE(limiter.CanUse(0, monday)); + EXPECT_TRUE(limiter.CanUse(1000, monday)); + EXPECT_TRUE(limiter.CanUse(1999, monday)); + EXPECT_TRUE(limiter.CanUse(2000, monday)); + EXPECT_FALSE(limiter.CanUse(2001, monday)); + + limiter.Use(1000, monday); + + EXPECT_TRUE(limiter.CanUse(0, monday)); + EXPECT_TRUE(limiter.CanUse(999, monday)); + EXPECT_TRUE(limiter.CanUse(1000, monday)); + EXPECT_FALSE(limiter.CanUse(1001, monday)); + + limiter.Use(1000, monday); + + EXPECT_TRUE(limiter.CanUse(0, monday)); + EXPECT_FALSE(limiter.CanUse(1, monday)); + + EXPECT_TRUE(limiter.CanUse(0, tuesday)); + EXPECT_TRUE(limiter.CanUse(1, tuesday)); + EXPECT_TRUE(limiter.CanUse(1999, tuesday)); + EXPECT_TRUE(limiter.CanUse(2000, tuesday)); + EXPECT_FALSE(limiter.CanUse(2001, tuesday)); + + limiter.Use(1000, tuesday); + + EXPECT_TRUE(limiter.CanUse(1000, tuesday)); + EXPECT_FALSE(limiter.CanUse(1001, tuesday)); + + limiter.Use(1000, thursday); + + EXPECT_TRUE(limiter.CanUse(1000, tuesday)); + EXPECT_FALSE(limiter.CanUse(1001, tuesday)); +} + +} // namespace talk_base diff --git a/talk/base/ratetracker.cc b/talk/base/ratetracker.cc new file mode 100644 index 000000000..383df9391 --- /dev/null +++ b/talk/base/ratetracker.cc @@ -0,0 +1,80 @@ +/* + * libjingle + * Copyright 2004--2010, 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/base/ratetracker.h" +#include "talk/base/timeutils.h" + +namespace talk_base { + +RateTracker::RateTracker() + : total_units_(0), units_second_(0), + last_units_second_time_(static_cast(-1)), + last_units_second_calc_(0) { +} + +size_t RateTracker::total_units() const { + return total_units_; +} + +size_t RateTracker::units_second() { + // Snapshot units / second calculator. Determine how many seconds have + // elapsed since our last reference point. If over 1 second, establish + // a new reference point that is an integer number of seconds since the + // last one, and compute the units over that interval. + uint32 current_time = Time(); + if (last_units_second_time_ != static_cast(-1)) { + int delta = talk_base::TimeDiff(current_time, last_units_second_time_); + if (delta >= 1000) { + int fraction_time = delta % 1000; + int seconds = delta / 1000; + int fraction_units = + static_cast(total_units_ - last_units_second_calc_) * + fraction_time / delta; + // Compute "units received during the interval" / "seconds in interval" + units_second_ = + (total_units_ - last_units_second_calc_ - fraction_units) / seconds; + last_units_second_time_ = current_time - fraction_time; + last_units_second_calc_ = total_units_ - fraction_units; + } + } + if (last_units_second_time_ == static_cast(-1)) { + last_units_second_time_ = current_time; + last_units_second_calc_ = total_units_; + } + + return units_second_; +} + +void RateTracker::Update(size_t units) { + total_units_ += units; +} + +uint32 RateTracker::Time() const { + return talk_base::Time(); +} + +} // namespace talk_base diff --git a/talk/base/ratetracker.h b/talk/base/ratetracker.h new file mode 100644 index 000000000..28c7bb36e --- /dev/null +++ b/talk/base/ratetracker.h @@ -0,0 +1,59 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_BASE_RATETRACKER_H_ +#define TALK_BASE_RATETRACKER_H_ + +#include +#include "talk/base/basictypes.h" + +namespace talk_base { + +// Computes instantaneous units per second. +class RateTracker { + public: + RateTracker(); + virtual ~RateTracker() {} + + size_t total_units() const; + size_t units_second(); + void Update(size_t units); + + protected: + // overrideable for tests + virtual uint32 Time() const; + + private: + size_t total_units_; + size_t units_second_; + uint32 last_units_second_time_; + size_t last_units_second_calc_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_RATETRACKER_H_ diff --git a/talk/base/ratetracker_unittest.cc b/talk/base/ratetracker_unittest.cc new file mode 100644 index 000000000..979d907a9 --- /dev/null +++ b/talk/base/ratetracker_unittest.cc @@ -0,0 +1,91 @@ +/* + * libjingle + * Copyright 2010, 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/base/gunit.h" +#include "talk/base/ratetracker.h" + +namespace talk_base { + +class RateTrackerForTest : public RateTracker { + public: + RateTrackerForTest() : time_(0) {} + virtual uint32 Time() const { return time_; } + void AdvanceTime(uint32 delta) { time_ += delta; } + + private: + uint32 time_; +}; + +TEST(RateTrackerTest, TestBasics) { + RateTrackerForTest tracker; + EXPECT_EQ(0U, tracker.total_units()); + EXPECT_EQ(0U, tracker.units_second()); + + // Add a sample. + tracker.Update(1234); + // Advance the clock by 100 ms. + tracker.AdvanceTime(100); + // total_units should advance, but units_second should stay 0. + EXPECT_EQ(1234U, tracker.total_units()); + EXPECT_EQ(0U, tracker.units_second()); + + // Repeat. + tracker.Update(1234); + tracker.AdvanceTime(100); + EXPECT_EQ(1234U * 2, tracker.total_units()); + EXPECT_EQ(0U, tracker.units_second()); + + // Advance the clock by 800 ms, so we've elapsed a full second. + // units_second should now be filled in properly. + tracker.AdvanceTime(800); + EXPECT_EQ(1234U * 2, tracker.total_units()); + EXPECT_EQ(1234U * 2, tracker.units_second()); + + // Poll the tracker again immediately. The reported rate should stay the same. + EXPECT_EQ(1234U * 2, tracker.total_units()); + EXPECT_EQ(1234U * 2, tracker.units_second()); + + // Do nothing and advance by a second. We should drop down to zero. + tracker.AdvanceTime(1000); + EXPECT_EQ(1234U * 2, tracker.total_units()); + EXPECT_EQ(0U, tracker.units_second()); + + // Send a bunch of data at a constant rate for 5.5 "seconds". + // We should report the rate properly. + for (int i = 0; i < 5500; i += 100) { + tracker.Update(9876U); + tracker.AdvanceTime(100); + } + EXPECT_EQ(9876U * 10, tracker.units_second()); + + // Advance the clock by 500 ms. Since we sent nothing over this half-second, + // the reported rate should be reduced by half. + tracker.AdvanceTime(500); + EXPECT_EQ(9876U * 5, tracker.units_second()); +} + +} // namespace talk_base diff --git a/talk/base/refcount.h b/talk/base/refcount.h new file mode 100644 index 000000000..38cf14762 --- /dev/null +++ b/talk/base/refcount.h @@ -0,0 +1,95 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_APP_BASE_REFCOUNT_H_ +#define TALK_APP_BASE_REFCOUNT_H_ + +#include + +#include "talk/base/criticalsection.h" + +namespace talk_base { + +// Reference count interface. +class RefCountInterface { + public: + virtual int AddRef() = 0; + virtual int Release() = 0; + protected: + virtual ~RefCountInterface() {} +}; + +template +class RefCountedObject : public T { + public: + RefCountedObject() : ref_count_(0) { + } + + template + explicit RefCountedObject(P p) : T(p), ref_count_(0) { + } + + template + RefCountedObject(P1 p1, P2 p2) : T(p1, p2), ref_count_(0) { + } + + template + RefCountedObject(P1 p1, P2 p2, P3 p3) : T(p1, p2, p3), ref_count_(0) { + } + + template + RefCountedObject(P1 p1, P2 p2, P3 p3, P4 p4) + : T(p1, p2, p3, p4), ref_count_(0) { + } + + template + RefCountedObject(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5) + : T(p1, p2, p3, p4, p5), ref_count_(0) { + } + + virtual int AddRef() { + return talk_base::AtomicOps::Increment(&ref_count_); + } + + virtual int Release() { + int count = talk_base::AtomicOps::Decrement(&ref_count_); + if (!count) { + delete this; + } + return count; + } + + protected: + virtual ~RefCountedObject() { + } + + int ref_count_; +}; + +} // namespace talk_base + +#endif // TALK_APP_BASE_REFCOUNT_H_ diff --git a/talk/base/referencecountedsingletonfactory.h b/talk/base/referencecountedsingletonfactory.h new file mode 100644 index 000000000..7f90b046b --- /dev/null +++ b/talk/base/referencecountedsingletonfactory.h @@ -0,0 +1,174 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_BASE_REFERENCECOUNTEDSINGLETONFACTORY_H_ +#define TALK_BASE_REFERENCECOUNTEDSINGLETONFACTORY_H_ + +#include "talk/base/common.h" +#include "talk/base/criticalsection.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" + +namespace talk_base { + +template class rcsf_ptr; + +// A ReferenceCountedSingletonFactory is an object which owns another object, +// and doles out the owned object to consumers in a reference-counted manner. +// Thus, the factory owns at most one object of the desired kind, and +// hands consumers a special pointer to it, through which they can access it. +// When the consumers delete the pointer, the reference count goes down, +// and if the reference count hits zero, the factory can throw the object +// away. If a consumer requests the pointer and the factory has none, +// it can create one on the fly and pass it back. +template +class ReferenceCountedSingletonFactory { + friend class rcsf_ptr; + public: + ReferenceCountedSingletonFactory() : ref_count_(0) {} + + virtual ~ReferenceCountedSingletonFactory() { + ASSERT(ref_count_ == 0); + } + + protected: + // Must be implemented in a sub-class. The sub-class may choose whether or not + // to cache the instance across lifetimes by either reset()'ing or not + // reset()'ing the scoped_ptr in CleanupInstance(). + virtual bool SetupInstance() = 0; + virtual void CleanupInstance() = 0; + + scoped_ptr instance_; + + private: + Interface* GetInstance() { + talk_base::CritScope cs(&crit_); + if (ref_count_ == 0) { + if (!SetupInstance()) { + LOG(LS_VERBOSE) << "Failed to setup instance"; + return NULL; + } + ASSERT(instance_.get() != NULL); + } + ++ref_count_; + + LOG(LS_VERBOSE) << "Number of references: " << ref_count_; + return instance_.get(); + } + + void ReleaseInstance() { + talk_base::CritScope cs(&crit_); + ASSERT(ref_count_ > 0); + ASSERT(instance_.get() != NULL); + --ref_count_; + LOG(LS_VERBOSE) << "Number of references: " << ref_count_; + if (ref_count_ == 0) { + CleanupInstance(); + } + } + + CriticalSection crit_; + int ref_count_; + + DISALLOW_COPY_AND_ASSIGN(ReferenceCountedSingletonFactory); +}; + +template +class rcsf_ptr { + public: + // Create a pointer that uses the factory to get the instance. + // This is lazy - it won't generate the instance until it is requested. + explicit rcsf_ptr(ReferenceCountedSingletonFactory* factory) + : instance_(NULL), + factory_(factory) { + } + + ~rcsf_ptr() { + release(); + } + + Interface& operator*() { + EnsureAcquired(); + return *instance_; + } + + Interface* operator->() { + EnsureAcquired(); + return instance_; + } + + // Gets the pointer, creating the singleton if necessary. May return NULL if + // creation failed. + Interface* get() { + Acquire(); + return instance_; + } + + // Set instance to NULL and tell the factory we aren't using the instance + // anymore. + void release() { + if (instance_) { + instance_ = NULL; + factory_->ReleaseInstance(); + } + } + + // Lets us know whether instance is valid or not right now. + // Even though attempts to use the instance will automatically create it, it + // is advisable to check this because creation can fail. + bool valid() const { + return instance_ != NULL; + } + + // Returns the factory that this pointer is using. + ReferenceCountedSingletonFactory* factory() const { + return factory_; + } + + private: + void EnsureAcquired() { + Acquire(); + ASSERT(instance_ != NULL); + } + + void Acquire() { + // Since we're getting a singleton back, acquire is a noop if instance is + // already populated. + if (!instance_) { + instance_ = factory_->GetInstance(); + } + } + + Interface* instance_; + ReferenceCountedSingletonFactory* factory_; + + DISALLOW_IMPLICIT_CONSTRUCTORS(rcsf_ptr); +}; + +}; // namespace talk_base + +#endif // TALK_BASE_REFERENCECOUNTEDSINGLETONFACTORY_H_ diff --git a/talk/base/referencecountedsingletonfactory_unittest.cc b/talk/base/referencecountedsingletonfactory_unittest.cc new file mode 100644 index 000000000..3fc7fd20f --- /dev/null +++ b/talk/base/referencecountedsingletonfactory_unittest.cc @@ -0,0 +1,149 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/gunit.h" +#include "talk/base/referencecountedsingletonfactory.h" + +namespace talk_base { + +class MyExistenceWatcher { + public: + MyExistenceWatcher() { create_called_ = true; } + ~MyExistenceWatcher() { delete_called_ = true; } + + static bool create_called_; + static bool delete_called_; +}; + +bool MyExistenceWatcher::create_called_ = false; +bool MyExistenceWatcher::delete_called_ = false; + +class TestReferenceCountedSingletonFactory : + public ReferenceCountedSingletonFactory { + protected: + virtual bool SetupInstance() { + instance_.reset(new MyExistenceWatcher()); + return true; + } + + virtual void CleanupInstance() { + instance_.reset(); + } +}; + +static void DoCreateAndGoOutOfScope( + ReferenceCountedSingletonFactory *factory) { + rcsf_ptr ptr(factory); + ptr.get(); + // and now ptr should go out of scope. +} + +TEST(ReferenceCountedSingletonFactory, ZeroReferenceCountCausesDeletion) { + TestReferenceCountedSingletonFactory factory; + MyExistenceWatcher::delete_called_ = false; + DoCreateAndGoOutOfScope(&factory); + EXPECT_TRUE(MyExistenceWatcher::delete_called_); +} + +TEST(ReferenceCountedSingletonFactory, NonZeroReferenceCountDoesNotDelete) { + TestReferenceCountedSingletonFactory factory; + rcsf_ptr ptr(&factory); + ptr.get(); + MyExistenceWatcher::delete_called_ = false; + DoCreateAndGoOutOfScope(&factory); + EXPECT_FALSE(MyExistenceWatcher::delete_called_); +} + +TEST(ReferenceCountedSingletonFactory, ReturnedPointersReferToSameThing) { + TestReferenceCountedSingletonFactory factory; + rcsf_ptr one(&factory), two(&factory); + + EXPECT_EQ(one.get(), two.get()); +} + +TEST(ReferenceCountedSingletonFactory, Release) { + TestReferenceCountedSingletonFactory factory; + + rcsf_ptr one(&factory); + one.get(); + + MyExistenceWatcher::delete_called_ = false; + one.release(); + EXPECT_TRUE(MyExistenceWatcher::delete_called_); +} + +TEST(ReferenceCountedSingletonFactory, GetWithoutRelease) { + TestReferenceCountedSingletonFactory factory; + rcsf_ptr one(&factory); + one.get(); + + MyExistenceWatcher::create_called_ = false; + one.get(); + EXPECT_FALSE(MyExistenceWatcher::create_called_); +} + +TEST(ReferenceCountedSingletonFactory, GetAfterRelease) { + TestReferenceCountedSingletonFactory factory; + rcsf_ptr one(&factory); + + MyExistenceWatcher::create_called_ = false; + one.release(); + one.get(); + EXPECT_TRUE(MyExistenceWatcher::create_called_); +} + +TEST(ReferenceCountedSingletonFactory, MultipleReleases) { + TestReferenceCountedSingletonFactory factory; + rcsf_ptr one(&factory), two(&factory); + + MyExistenceWatcher::create_called_ = false; + MyExistenceWatcher::delete_called_ = false; + one.release(); + EXPECT_FALSE(MyExistenceWatcher::delete_called_); + one.release(); + EXPECT_FALSE(MyExistenceWatcher::delete_called_); + one.release(); + EXPECT_FALSE(MyExistenceWatcher::delete_called_); + one.get(); + EXPECT_TRUE(MyExistenceWatcher::create_called_); +} + +TEST(ReferenceCountedSingletonFactory, Existentialism) { + TestReferenceCountedSingletonFactory factory; + + rcsf_ptr one(&factory); + + MyExistenceWatcher::create_called_ = false; + MyExistenceWatcher::delete_called_ = false; + + one.get(); + EXPECT_TRUE(MyExistenceWatcher::create_called_); + one.release(); + EXPECT_TRUE(MyExistenceWatcher::delete_called_); +} + +} // namespace talk_base diff --git a/talk/base/rollingaccumulator.h b/talk/base/rollingaccumulator.h new file mode 100644 index 000000000..cdad0251f --- /dev/null +++ b/talk/base/rollingaccumulator.h @@ -0,0 +1,137 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_BASE_ROLLINGACCUMULATOR_H_ +#define TALK_BASE_ROLLINGACCUMULATOR_H_ + +#include + +#include "talk/base/common.h" + +namespace talk_base { + +// RollingAccumulator stores and reports statistics +// over N most recent samples. +// +// T is assumed to be an int, long, double or float. +template +class RollingAccumulator { + public: + explicit RollingAccumulator(size_t max_count) + : count_(0), + next_index_(0), + sum_(0.0), + sum_2_(0.0), + samples_(max_count) { + } + ~RollingAccumulator() { + } + + size_t max_count() const { + return samples_.size(); + } + + size_t count() const { + return count_; + } + + void AddSample(T sample) { + if (count_ == max_count()) { + // Remove oldest sample. + T sample_to_remove = samples_[next_index_]; + sum_ -= sample_to_remove; + sum_2_ -= sample_to_remove * sample_to_remove; + } else { + // Increase count of samples. + ++count_; + } + // Add new sample. + samples_[next_index_] = sample; + sum_ += sample; + sum_2_ += sample * sample; + // Update next_index_. + next_index_ = (next_index_ + 1) % max_count(); + } + + T ComputeSum() const { + return static_cast(sum_); + } + + T ComputeMean() const { + if (count_ == 0) { + return static_cast(0); + } + return static_cast(sum_ / count_); + } + + // O(n) time complexity. + // Weights nth sample with weight (learning_rate)^n. Learning_rate should be + // between (0.0, 1.0], otherwise the non-weighted mean is returned. + T ComputeWeightedMean(double learning_rate) const { + if (count_ < 1 || learning_rate <= 0.0 || learning_rate >= 1.0) { + return ComputeMean(); + } + double weighted_mean = 0.0; + double current_weight = 1.0; + double weight_sum = 0.0; + const size_t max_size = max_count(); + for (size_t i = 0; i < count_; ++i) { + current_weight *= learning_rate; + weight_sum += current_weight; + // Add max_size to prevent underflow. + size_t index = (next_index_ + max_size - i - 1) % max_size; + weighted_mean += current_weight * samples_[index]; + } + return static_cast(weighted_mean / weight_sum); + } + + // Compute estimated variance. Estimation is more accurate + // as the number of samples grows. + T ComputeVariance() const { + if (count_ == 0) { + return static_cast(0); + } + // Var = E[x^2] - (E[x])^2 + double count_inv = 1.0 / count_; + double mean_2 = sum_2_ * count_inv; + double mean = sum_ * count_inv; + return static_cast(mean_2 - (mean * mean)); + } + + private: + size_t count_; + size_t next_index_; + double sum_; // Sum(x) + double sum_2_; // Sum(x*x) + std::vector samples_; + + DISALLOW_COPY_AND_ASSIGN(RollingAccumulator); +}; + +} // namespace talk_base + +#endif // TALK_BASE_ROLLINGACCUMULATOR_H_ diff --git a/talk/base/rollingaccumulator_unittest.cc b/talk/base/rollingaccumulator_unittest.cc new file mode 100644 index 000000000..c28310336 --- /dev/null +++ b/talk/base/rollingaccumulator_unittest.cc @@ -0,0 +1,102 @@ +/* + * libjingle + * Copyright 2011, 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/base/gunit.h" +#include "talk/base/rollingaccumulator.h" + +namespace talk_base { + +namespace { + +const double kLearningRate = 0.5; + +} // namespace + +TEST(RollingAccumulatorTest, ZeroSamples) { + RollingAccumulator accum(10); + + EXPECT_EQ(0U, accum.count()); + EXPECT_EQ(0, accum.ComputeMean()); + EXPECT_EQ(0, accum.ComputeVariance()); +} + +TEST(RollingAccumulatorTest, SomeSamples) { + RollingAccumulator accum(10); + for (int i = 0; i < 4; ++i) { + accum.AddSample(i); + } + + EXPECT_EQ(4U, accum.count()); + EXPECT_EQ(6, accum.ComputeSum()); + EXPECT_EQ(1, accum.ComputeMean()); + EXPECT_EQ(2, accum.ComputeWeightedMean(kLearningRate)); + EXPECT_EQ(1, accum.ComputeVariance()); +} + +TEST(RollingAccumulatorTest, RollingSamples) { + RollingAccumulator accum(10); + for (int i = 0; i < 12; ++i) { + accum.AddSample(i); + } + + EXPECT_EQ(10U, accum.count()); + EXPECT_EQ(65, accum.ComputeSum()); + EXPECT_EQ(6, accum.ComputeMean()); + EXPECT_EQ(10, accum.ComputeWeightedMean(kLearningRate)); + EXPECT_NEAR(9, accum.ComputeVariance(), 1); +} + +TEST(RollingAccumulatorTest, RollingSamplesDouble) { + RollingAccumulator accum(10); + for (int i = 0; i < 23; ++i) { + accum.AddSample(5 * i); + } + + EXPECT_EQ(10u, accum.count()); + EXPECT_DOUBLE_EQ(875.0, accum.ComputeSum()); + EXPECT_DOUBLE_EQ(87.5, accum.ComputeMean()); + EXPECT_NEAR(105.049, accum.ComputeWeightedMean(kLearningRate), 0.1); + EXPECT_NEAR(229.166667, accum.ComputeVariance(), 25); +} + +TEST(RollingAccumulatorTest, ComputeWeightedMeanCornerCases) { + RollingAccumulator accum(10); + EXPECT_EQ(0, accum.ComputeWeightedMean(kLearningRate)); + EXPECT_EQ(0, accum.ComputeWeightedMean(0.0)); + EXPECT_EQ(0, accum.ComputeWeightedMean(1.1)); + + for (int i = 0; i < 8; ++i) { + accum.AddSample(i); + } + + EXPECT_EQ(3, accum.ComputeMean()); + EXPECT_EQ(3, accum.ComputeWeightedMean(0)); + EXPECT_EQ(3, accum.ComputeWeightedMean(1.1)); + EXPECT_EQ(6, accum.ComputeWeightedMean(kLearningRate)); +} + +} // namespace talk_base diff --git a/talk/base/schanneladapter.cc b/talk/base/schanneladapter.cc new file mode 100644 index 000000000..a376328e2 --- /dev/null +++ b/talk/base/schanneladapter.cc @@ -0,0 +1,719 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/win32.h" +#define SECURITY_WIN32 +#include +#include + +#include +#include + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/schanneladapter.h" +#include "talk/base/sec_buffer.h" +#include "talk/base/thread.h" + +namespace talk_base { + +///////////////////////////////////////////////////////////////////////////// +// SChannelAdapter +///////////////////////////////////////////////////////////////////////////// + +extern const ConstantLabel SECURITY_ERRORS[]; + +const ConstantLabel SCHANNEL_BUFFER_TYPES[] = { + KLABEL(SECBUFFER_EMPTY), // 0 + KLABEL(SECBUFFER_DATA), // 1 + KLABEL(SECBUFFER_TOKEN), // 2 + KLABEL(SECBUFFER_PKG_PARAMS), // 3 + KLABEL(SECBUFFER_MISSING), // 4 + KLABEL(SECBUFFER_EXTRA), // 5 + KLABEL(SECBUFFER_STREAM_TRAILER), // 6 + KLABEL(SECBUFFER_STREAM_HEADER), // 7 + KLABEL(SECBUFFER_MECHLIST), // 11 + KLABEL(SECBUFFER_MECHLIST_SIGNATURE), // 12 + KLABEL(SECBUFFER_TARGET), // 13 + KLABEL(SECBUFFER_CHANNEL_BINDINGS), // 14 + LASTLABEL +}; + +void DescribeBuffer(LoggingSeverity severity, const char* prefix, + const SecBuffer& sb) { + LOG_V(severity) + << prefix + << "(" << sb.cbBuffer + << ", " << FindLabel(sb.BufferType & ~SECBUFFER_ATTRMASK, + SCHANNEL_BUFFER_TYPES) + << ", " << sb.pvBuffer << ")"; +} + +void DescribeBuffers(LoggingSeverity severity, const char* prefix, + const SecBufferDesc* sbd) { + if (!LOG_CHECK_LEVEL_V(severity)) + return; + LOG_V(severity) << prefix << "("; + for (size_t i=0; icBuffers; ++i) { + DescribeBuffer(severity, " ", sbd->pBuffers[i]); + } + LOG_V(severity) << ")"; +} + +const ULONG SSL_FLAGS_DEFAULT = ISC_REQ_ALLOCATE_MEMORY + | ISC_REQ_CONFIDENTIALITY + | ISC_REQ_EXTENDED_ERROR + | ISC_REQ_INTEGRITY + | ISC_REQ_REPLAY_DETECT + | ISC_REQ_SEQUENCE_DETECT + | ISC_REQ_STREAM; + //| ISC_REQ_USE_SUPPLIED_CREDS; + +typedef std::vector SChannelBuffer; + +struct SChannelAdapter::SSLImpl { + CredHandle cred; + CtxtHandle ctx; + bool cred_init, ctx_init; + SChannelBuffer inbuf, outbuf, readable; + SecPkgContext_StreamSizes sizes; + + SSLImpl() : cred_init(false), ctx_init(false) { } +}; + +SChannelAdapter::SChannelAdapter(AsyncSocket* socket) + : SSLAdapter(socket), state_(SSL_NONE), + restartable_(false), signal_close_(false), message_pending_(false), + impl_(new SSLImpl) { +} + +SChannelAdapter::~SChannelAdapter() { + Cleanup(); +} + +int +SChannelAdapter::StartSSL(const char* hostname, bool restartable) { + if (state_ != SSL_NONE) + return ERROR_ALREADY_INITIALIZED; + + ssl_host_name_ = hostname; + restartable_ = restartable; + + if (socket_->GetState() != Socket::CS_CONNECTED) { + state_ = SSL_WAIT; + return 0; + } + + state_ = SSL_CONNECTING; + if (int err = BeginSSL()) { + Error("BeginSSL", err, false); + return err; + } + + return 0; +} + +int +SChannelAdapter::BeginSSL() { + LOG(LS_VERBOSE) << "BeginSSL: " << ssl_host_name_; + ASSERT(state_ == SSL_CONNECTING); + + SECURITY_STATUS ret; + + SCHANNEL_CRED sc_cred = { 0 }; + sc_cred.dwVersion = SCHANNEL_CRED_VERSION; + //sc_cred.dwMinimumCipherStrength = 128; // Note: use system default + sc_cred.dwFlags = SCH_CRED_NO_DEFAULT_CREDS | SCH_CRED_AUTO_CRED_VALIDATION; + + ret = AcquireCredentialsHandle(NULL, UNISP_NAME, SECPKG_CRED_OUTBOUND, NULL, + &sc_cred, NULL, NULL, &impl_->cred, NULL); + if (ret != SEC_E_OK) { + LOG(LS_ERROR) << "AcquireCredentialsHandle error: " + << ErrorName(ret, SECURITY_ERRORS); + return ret; + } + impl_->cred_init = true; + + if (LOG_CHECK_LEVEL(LS_VERBOSE)) { + SecPkgCred_CipherStrengths cipher_strengths = { 0 }; + ret = QueryCredentialsAttributes(&impl_->cred, + SECPKG_ATTR_CIPHER_STRENGTHS, + &cipher_strengths); + if (SUCCEEDED(ret)) { + LOG(LS_VERBOSE) << "SChannel cipher strength: " + << cipher_strengths.dwMinimumCipherStrength << " - " + << cipher_strengths.dwMaximumCipherStrength; + } + + SecPkgCred_SupportedAlgs supported_algs = { 0 }; + ret = QueryCredentialsAttributes(&impl_->cred, + SECPKG_ATTR_SUPPORTED_ALGS, + &supported_algs); + if (SUCCEEDED(ret)) { + LOG(LS_VERBOSE) << "SChannel supported algorithms:"; + for (DWORD i=0; ipwszName : L"Unknown"; + LOG(LS_VERBOSE) << " " << ToUtf8(alg_name) << " (" << alg_id << ")"; + } + CSecBufferBase::FreeSSPI(supported_algs.palgSupportedAlgs); + } + } + + ULONG flags = SSL_FLAGS_DEFAULT, ret_flags = 0; + if (ignore_bad_cert()) + flags |= ISC_REQ_MANUAL_CRED_VALIDATION; + + CSecBufferBundle<2, CSecBufferBase::FreeSSPI> sb_out; + ret = InitializeSecurityContextA(&impl_->cred, NULL, + const_cast(ssl_host_name_.c_str()), + flags, 0, 0, NULL, 0, + &impl_->ctx, sb_out.desc(), + &ret_flags, NULL); + if (SUCCEEDED(ret)) + impl_->ctx_init = true; + return ProcessContext(ret, NULL, sb_out.desc()); +} + +int +SChannelAdapter::ContinueSSL() { + LOG(LS_VERBOSE) << "ContinueSSL"; + ASSERT(state_ == SSL_CONNECTING); + + SECURITY_STATUS ret; + + CSecBufferBundle<2> sb_in; + sb_in[0].BufferType = SECBUFFER_TOKEN; + sb_in[0].cbBuffer = static_cast(impl_->inbuf.size()); + sb_in[0].pvBuffer = &impl_->inbuf[0]; + //DescribeBuffers(LS_VERBOSE, "Input Buffer ", sb_in.desc()); + + ULONG flags = SSL_FLAGS_DEFAULT, ret_flags = 0; + if (ignore_bad_cert()) + flags |= ISC_REQ_MANUAL_CRED_VALIDATION; + + CSecBufferBundle<2, CSecBufferBase::FreeSSPI> sb_out; + ret = InitializeSecurityContextA(&impl_->cred, &impl_->ctx, + const_cast(ssl_host_name_.c_str()), + flags, 0, 0, sb_in.desc(), 0, + NULL, sb_out.desc(), + &ret_flags, NULL); + return ProcessContext(ret, sb_in.desc(), sb_out.desc()); +} + +int +SChannelAdapter::ProcessContext(long int status, _SecBufferDesc* sbd_in, + _SecBufferDesc* sbd_out) { + if (status != SEC_E_OK && status != SEC_I_CONTINUE_NEEDED && + status != SEC_E_INCOMPLETE_MESSAGE) { + LOG(LS_ERROR) + << "InitializeSecurityContext error: " + << ErrorName(status, SECURITY_ERRORS); + } + //if (sbd_in) + // DescribeBuffers(LS_VERBOSE, "Input Buffer ", sbd_in); + //if (sbd_out) + // DescribeBuffers(LS_VERBOSE, "Output Buffer ", sbd_out); + + if (status == SEC_E_INCOMPLETE_MESSAGE) { + // Wait for more input from server. + return Flush(); + } + + if (FAILED(status)) { + // We can't continue. Common errors: + // SEC_E_CERT_EXPIRED - Typically, this means the computer clock is wrong. + return status; + } + + // Note: we check both input and output buffers for SECBUFFER_EXTRA. + // Experience shows it appearing in the input, but the documentation claims + // it should appear in the output. + size_t extra = 0; + if (sbd_in) { + for (size_t i=0; icBuffers; ++i) { + SecBuffer& buffer = sbd_in->pBuffers[i]; + if (buffer.BufferType == SECBUFFER_EXTRA) { + extra += buffer.cbBuffer; + } + } + } + if (sbd_out) { + for (size_t i=0; icBuffers; ++i) { + SecBuffer& buffer = sbd_out->pBuffers[i]; + if (buffer.BufferType == SECBUFFER_EXTRA) { + extra += buffer.cbBuffer; + } else if (buffer.BufferType == SECBUFFER_TOKEN) { + impl_->outbuf.insert(impl_->outbuf.end(), + reinterpret_cast(buffer.pvBuffer), + reinterpret_cast(buffer.pvBuffer) + buffer.cbBuffer); + } + } + } + + if (extra) { + ASSERT(extra <= impl_->inbuf.size()); + size_t consumed = impl_->inbuf.size() - extra; + memmove(&impl_->inbuf[0], &impl_->inbuf[consumed], extra); + impl_->inbuf.resize(extra); + } else { + impl_->inbuf.clear(); + } + + if (SEC_I_CONTINUE_NEEDED == status) { + // Send data to server and wait for response. + // Note: ContinueSSL will result in a Flush, anyway. + return impl_->inbuf.empty() ? Flush() : ContinueSSL(); + } + + if (SEC_E_OK == status) { + LOG(LS_VERBOSE) << "QueryContextAttributes"; + status = QueryContextAttributes(&impl_->ctx, SECPKG_ATTR_STREAM_SIZES, + &impl_->sizes); + if (FAILED(status)) { + LOG(LS_ERROR) << "QueryContextAttributes error: " + << ErrorName(status, SECURITY_ERRORS); + return status; + } + + state_ = SSL_CONNECTED; + + if (int err = DecryptData()) { + return err; + } else if (int err = Flush()) { + return err; + } else { + // If we decrypted any data, queue up a notification here + PostEvent(); + // Signal our connectedness + AsyncSocketAdapter::OnConnectEvent(this); + } + return 0; + } + + if (SEC_I_INCOMPLETE_CREDENTIALS == status) { + // We don't support client authentication in schannel. + return status; + } + + // We don't expect any other codes + ASSERT(false); + return status; +} + +int +SChannelAdapter::DecryptData() { + SChannelBuffer& inbuf = impl_->inbuf; + SChannelBuffer& readable = impl_->readable; + + while (!inbuf.empty()) { + CSecBufferBundle<4> in_buf; + in_buf[0].BufferType = SECBUFFER_DATA; + in_buf[0].cbBuffer = static_cast(inbuf.size()); + in_buf[0].pvBuffer = &inbuf[0]; + + //DescribeBuffers(LS_VERBOSE, "Decrypt In ", in_buf.desc()); + SECURITY_STATUS status = DecryptMessage(&impl_->ctx, in_buf.desc(), 0, 0); + //DescribeBuffers(LS_VERBOSE, "Decrypt Out ", in_buf.desc()); + + // Note: We are explicitly treating SEC_E_OK, SEC_I_CONTEXT_EXPIRED, and + // any other successful results as continue. + if (SUCCEEDED(status)) { + size_t data_len = 0, extra_len = 0; + for (size_t i=0; icBuffers; ++i) { + if (in_buf[i].BufferType == SECBUFFER_DATA) { + data_len += in_buf[i].cbBuffer; + readable.insert(readable.end(), + reinterpret_cast(in_buf[i].pvBuffer), + reinterpret_cast(in_buf[i].pvBuffer) + in_buf[i].cbBuffer); + } else if (in_buf[i].BufferType == SECBUFFER_EXTRA) { + extra_len += in_buf[i].cbBuffer; + } + } + // There is a bug on Win2K where SEC_I_CONTEXT_EXPIRED is misclassified. + if ((data_len == 0) && (inbuf[0] == 0x15)) { + status = SEC_I_CONTEXT_EXPIRED; + } + if (extra_len) { + size_t consumed = inbuf.size() - extra_len; + memmove(&inbuf[0], &inbuf[consumed], extra_len); + inbuf.resize(extra_len); + } else { + inbuf.clear(); + } + // TODO: Handle SEC_I_CONTEXT_EXPIRED to do clean shutdown + if (status != SEC_E_OK) { + LOG(LS_INFO) << "DecryptMessage returned continuation code: " + << ErrorName(status, SECURITY_ERRORS); + } + continue; + } + + if (status == SEC_E_INCOMPLETE_MESSAGE) { + break; + } else { + return status; + } + } + + return 0; +} + +void +SChannelAdapter::Cleanup() { + if (impl_->ctx_init) + DeleteSecurityContext(&impl_->ctx); + if (impl_->cred_init) + FreeCredentialsHandle(&impl_->cred); + delete impl_; +} + +void +SChannelAdapter::PostEvent() { + // Check if there's anything notable to signal + if (impl_->readable.empty() && !signal_close_) + return; + + // Only one post in the queue at a time + if (message_pending_) + return; + + if (Thread* thread = Thread::Current()) { + message_pending_ = true; + thread->Post(this); + } else { + LOG(LS_ERROR) << "No thread context available for SChannelAdapter"; + ASSERT(false); + } +} + +void +SChannelAdapter::Error(const char* context, int err, bool signal) { + LOG(LS_WARNING) << "SChannelAdapter::Error(" + << context << ", " + << ErrorName(err, SECURITY_ERRORS) << ")"; + state_ = SSL_ERROR; + SetError(err); + if (signal) + AsyncSocketAdapter::OnCloseEvent(this, err); +} + +int +SChannelAdapter::Read() { + char buffer[4096]; + SChannelBuffer& inbuf = impl_->inbuf; + while (true) { + int ret = AsyncSocketAdapter::Recv(buffer, sizeof(buffer)); + if (ret > 0) { + inbuf.insert(inbuf.end(), buffer, buffer + ret); + } else if (GetError() == EWOULDBLOCK) { + return 0; // Blocking + } else { + return GetError(); + } + } +} + +int +SChannelAdapter::Flush() { + int result = 0; + size_t pos = 0; + SChannelBuffer& outbuf = impl_->outbuf; + while (pos < outbuf.size()) { + int sent = AsyncSocketAdapter::Send(&outbuf[pos], outbuf.size() - pos); + if (sent > 0) { + pos += sent; + } else if (GetError() == EWOULDBLOCK) { + break; // Blocking + } else { + result = GetError(); + break; + } + } + if (int remainder = static_cast(outbuf.size() - pos)) { + memmove(&outbuf[0], &outbuf[pos], remainder); + outbuf.resize(remainder); + } else { + outbuf.clear(); + } + return result; +} + +// +// AsyncSocket Implementation +// + +int +SChannelAdapter::Send(const void* pv, size_t cb) { + switch (state_) { + case SSL_NONE: + return AsyncSocketAdapter::Send(pv, cb); + + case SSL_WAIT: + case SSL_CONNECTING: + SetError(EWOULDBLOCK); + return SOCKET_ERROR; + + case SSL_CONNECTED: + break; + + case SSL_ERROR: + default: + return SOCKET_ERROR; + } + + size_t written = 0; + SChannelBuffer& outbuf = impl_->outbuf; + while (written < cb) { + const size_t encrypt_len = std::min(cb - written, + impl_->sizes.cbMaximumMessage); + + CSecBufferBundle<4> out_buf; + out_buf[0].BufferType = SECBUFFER_STREAM_HEADER; + out_buf[0].cbBuffer = impl_->sizes.cbHeader; + out_buf[1].BufferType = SECBUFFER_DATA; + out_buf[1].cbBuffer = static_cast(encrypt_len); + out_buf[2].BufferType = SECBUFFER_STREAM_TRAILER; + out_buf[2].cbBuffer = impl_->sizes.cbTrailer; + + size_t packet_len = out_buf[0].cbBuffer + + out_buf[1].cbBuffer + + out_buf[2].cbBuffer; + + SChannelBuffer message; + message.resize(packet_len); + out_buf[0].pvBuffer = &message[0]; + out_buf[1].pvBuffer = &message[out_buf[0].cbBuffer]; + out_buf[2].pvBuffer = &message[out_buf[0].cbBuffer + out_buf[1].cbBuffer]; + + memcpy(out_buf[1].pvBuffer, + static_cast(pv) + written, + encrypt_len); + + //DescribeBuffers(LS_VERBOSE, "Encrypt In ", out_buf.desc()); + SECURITY_STATUS res = EncryptMessage(&impl_->ctx, 0, out_buf.desc(), 0); + //DescribeBuffers(LS_VERBOSE, "Encrypt Out ", out_buf.desc()); + + if (FAILED(res)) { + Error("EncryptMessage", res, false); + return SOCKET_ERROR; + } + + // We assume that the header and data segments do not change length, + // or else encrypting the concatenated packet in-place is wrong. + ASSERT(out_buf[0].cbBuffer == impl_->sizes.cbHeader); + ASSERT(out_buf[1].cbBuffer == static_cast(encrypt_len)); + + // However, the length of the trailer may change due to padding. + ASSERT(out_buf[2].cbBuffer <= impl_->sizes.cbTrailer); + + packet_len = out_buf[0].cbBuffer + + out_buf[1].cbBuffer + + out_buf[2].cbBuffer; + + written += encrypt_len; + outbuf.insert(outbuf.end(), &message[0], &message[packet_len-1]+1); + } + + if (int err = Flush()) { + state_ = SSL_ERROR; + SetError(err); + return SOCKET_ERROR; + } + + return static_cast(written); +} + +int +SChannelAdapter::Recv(void* pv, size_t cb) { + switch (state_) { + case SSL_NONE: + return AsyncSocketAdapter::Recv(pv, cb); + + case SSL_WAIT: + case SSL_CONNECTING: + SetError(EWOULDBLOCK); + return SOCKET_ERROR; + + case SSL_CONNECTED: + break; + + case SSL_ERROR: + default: + return SOCKET_ERROR; + } + + SChannelBuffer& readable = impl_->readable; + if (readable.empty()) { + SetError(EWOULDBLOCK); + return SOCKET_ERROR; + } + size_t read = _min(cb, readable.size()); + memcpy(pv, &readable[0], read); + if (size_t remaining = readable.size() - read) { + memmove(&readable[0], &readable[read], remaining); + readable.resize(remaining); + } else { + readable.clear(); + } + + PostEvent(); + return static_cast(read); +} + +int +SChannelAdapter::Close() { + if (!impl_->readable.empty()) { + LOG(WARNING) << "SChannelAdapter::Close with readable data"; + // Note: this isn't strictly an error, but we're using it temporarily to + // track bugs. + //ASSERT(false); + } + if (state_ == SSL_CONNECTED) { + DWORD token = SCHANNEL_SHUTDOWN; + CSecBufferBundle<1> sb_in; + sb_in[0].BufferType = SECBUFFER_TOKEN; + sb_in[0].cbBuffer = sizeof(token); + sb_in[0].pvBuffer = &token; + ApplyControlToken(&impl_->ctx, sb_in.desc()); + // TODO: In theory, to do a nice shutdown, we need to begin shutdown + // negotiation with more calls to InitializeSecurityContext. Since the + // socket api doesn't support nice shutdown at this point, we don't bother. + } + Cleanup(); + impl_ = new SSLImpl; + state_ = restartable_ ? SSL_WAIT : SSL_NONE; + signal_close_ = false; + message_pending_ = false; + return AsyncSocketAdapter::Close(); +} + +Socket::ConnState +SChannelAdapter::GetState() const { + if (signal_close_) + return CS_CONNECTED; + ConnState state = socket_->GetState(); + if ((state == CS_CONNECTED) + && ((state_ == SSL_WAIT) || (state_ == SSL_CONNECTING))) + state = CS_CONNECTING; + return state; +} + +void +SChannelAdapter::OnConnectEvent(AsyncSocket* socket) { + LOG(LS_VERBOSE) << "SChannelAdapter::OnConnectEvent"; + if (state_ != SSL_WAIT) { + ASSERT(state_ == SSL_NONE); + AsyncSocketAdapter::OnConnectEvent(socket); + return; + } + + state_ = SSL_CONNECTING; + if (int err = BeginSSL()) { + Error("BeginSSL", err); + } +} + +void +SChannelAdapter::OnReadEvent(AsyncSocket* socket) { + if (state_ == SSL_NONE) { + AsyncSocketAdapter::OnReadEvent(socket); + return; + } + + if (int err = Read()) { + Error("Read", err); + return; + } + + if (impl_->inbuf.empty()) + return; + + if (state_ == SSL_CONNECTED) { + if (int err = DecryptData()) { + Error("DecryptData", err); + } else if (!impl_->readable.empty()) { + AsyncSocketAdapter::OnReadEvent(this); + } + } else if (state_ == SSL_CONNECTING) { + if (int err = ContinueSSL()) { + Error("ContinueSSL", err); + } + } +} + +void +SChannelAdapter::OnWriteEvent(AsyncSocket* socket) { + if (state_ == SSL_NONE) { + AsyncSocketAdapter::OnWriteEvent(socket); + return; + } + + if (int err = Flush()) { + Error("Flush", err); + return; + } + + // See if we have more data to write + if (!impl_->outbuf.empty()) + return; + + // Buffer is empty, submit notification + if (state_ == SSL_CONNECTED) { + AsyncSocketAdapter::OnWriteEvent(socket); + } +} + +void +SChannelAdapter::OnCloseEvent(AsyncSocket* socket, int err) { + if ((state_ == SSL_NONE) || impl_->readable.empty()) { + AsyncSocketAdapter::OnCloseEvent(socket, err); + return; + } + + // If readable is non-empty, then we have a pending Message + // that will allow us to signal close (eventually). + signal_close_ = true; +} + +void +SChannelAdapter::OnMessage(Message* pmsg) { + if (!message_pending_) + return; // This occurs when socket is closed + + message_pending_ = false; + if (!impl_->readable.empty()) { + AsyncSocketAdapter::OnReadEvent(this); + } else if (signal_close_) { + signal_close_ = false; + AsyncSocketAdapter::OnCloseEvent(this, 0); // TODO: cache this error? + } +} + +} // namespace talk_base diff --git a/talk/base/schanneladapter.h b/talk/base/schanneladapter.h new file mode 100644 index 000000000..a5ab7b30b --- /dev/null +++ b/talk/base/schanneladapter.h @@ -0,0 +1,94 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_SCHANNELADAPTER_H__ +#define TALK_BASE_SCHANNELADAPTER_H__ + +#include +#include "talk/base/ssladapter.h" +#include "talk/base/messagequeue.h" +struct _SecBufferDesc; + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// + +class SChannelAdapter : public SSLAdapter, public MessageHandler { +public: + SChannelAdapter(AsyncSocket* socket); + virtual ~SChannelAdapter(); + + virtual int StartSSL(const char* hostname, bool restartable); + virtual int Send(const void* pv, size_t cb); + virtual int Recv(void* pv, size_t cb); + virtual int Close(); + + // Note that the socket returns ST_CONNECTING while SSL is being negotiated. + virtual ConnState GetState() const; + +protected: + enum SSLState { + SSL_NONE, SSL_WAIT, SSL_CONNECTING, SSL_CONNECTED, SSL_ERROR + }; + struct SSLImpl; + + virtual void OnConnectEvent(AsyncSocket* socket); + virtual void OnReadEvent(AsyncSocket* socket); + virtual void OnWriteEvent(AsyncSocket* socket); + virtual void OnCloseEvent(AsyncSocket* socket, int err); + virtual void OnMessage(Message* pmsg); + + int BeginSSL(); + int ContinueSSL(); + int ProcessContext(long int status, _SecBufferDesc* sbd_in, + _SecBufferDesc* sbd_out); + int DecryptData(); + + int Read(); + int Flush(); + void Error(const char* context, int err, bool signal = true); + void Cleanup(); + + void PostEvent(); + +private: + SSLState state_; + std::string ssl_host_name_; + // If true, socket will retain SSL configuration after Close. + bool restartable_; + // If true, we are delaying signalling close until all data is read. + bool signal_close_; + // If true, we are waiting to be woken up to signal readability or closure. + bool message_pending_; + SSLImpl* impl_; +}; + +///////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_SCHANNELADAPTER_H__ diff --git a/talk/base/scoped_autorelease_pool.h b/talk/base/scoped_autorelease_pool.h new file mode 100644 index 000000000..611f811fa --- /dev/null +++ b/talk/base/scoped_autorelease_pool.h @@ -0,0 +1,76 @@ +/* + * libjingle + * Copyright 2008 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. + */ + +// Automatically initialize and and free an autoreleasepool. Never allocate +// an instance of this class using "new" - that will result in a compile-time +// error. Only use it as a stack object. +// +// Note: NSAutoreleasePool docs say that you should not normally need to +// declare an NSAutoreleasePool as a member of an object - but there's nothing +// that indicates it will be a problem, as long as the stack lifetime of the +// pool exactly matches the stack lifetime of the object. + +#ifndef TALK_BASE_SCOPED_AUTORELEASE_POOL_H__ +#define TALK_BASE_SCOPED_AUTORELEASE_POOL_H__ + +#if defined(IOS) || defined(OSX) + +#include "talk/base/common.h" + +// This header may be included from Obj-C files or C++ files. +#ifdef __OBJC__ +@class NSAutoreleasePool; +#else +class NSAutoreleasePool; +#endif + +namespace talk_base { + +class ScopedAutoreleasePool { + public: + ScopedAutoreleasePool(); + ~ScopedAutoreleasePool(); + + private: + // Declaring private overrides of new and delete here enforces the "only use + // as a stack object" discipline. + // + // Note: new is declared as "throw()" to get around a gcc warning about new + // returning NULL, but this method will never get called and therefore will + // never actually throw any exception. + void* operator new(size_t size) throw() { return NULL; } + void operator delete (void* ptr) {} + + NSAutoreleasePool* pool_; + + DISALLOW_EVIL_CONSTRUCTORS(ScopedAutoreleasePool); +}; + +} // namespace talk_base + +#endif // IOS || OSX +#endif // TALK_BASE_SCOPED_AUTORELEASE_POOL_H__ diff --git a/talk/base/scoped_autorelease_pool.mm b/talk/base/scoped_autorelease_pool.mm new file mode 100644 index 000000000..4009c7b7b --- /dev/null +++ b/talk/base/scoped_autorelease_pool.mm @@ -0,0 +1,42 @@ +/* + * libjingle + * Copyright 2008, 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. + */ + +#import + +#import "talk/base/scoped_autorelease_pool.h" + +namespace talk_base { + +ScopedAutoreleasePool::ScopedAutoreleasePool() { + pool_ = [[NSAutoreleasePool alloc] init]; +} + +ScopedAutoreleasePool::~ScopedAutoreleasePool() { + [pool_ drain]; +} + +} // namespace talk_base diff --git a/talk/base/scoped_ptr.h b/talk/base/scoped_ptr.h new file mode 100644 index 000000000..5a8364e10 --- /dev/null +++ b/talk/base/scoped_ptr.h @@ -0,0 +1,277 @@ +// (C) Copyright Greg Colvin and Beman Dawes 1998, 1999. +// Copyright (c) 2001, 2002 Peter Dimov +// +// Permission to copy, use, modify, sell and distribute this software +// is granted provided this copyright notice appears in all copies. +// This software is provided "as is" without express or implied +// warranty, and with no claim as to its suitability for any purpose. +// +// See http://www.boost.org/libs/smart_ptr/scoped_ptr.htm for documentation. +// + +// scoped_ptr mimics a built-in pointer except that it guarantees deletion +// of the object pointed to, either on destruction of the scoped_ptr or via +// an explicit reset(). scoped_ptr is a simple solution for simple needs; +// use shared_ptr or std::auto_ptr if your needs are more complex. + +// scoped_ptr_malloc added in by Google. When one of +// these goes out of scope, instead of doing a delete or delete[], it +// calls free(). scoped_ptr_malloc is likely to see much more +// use than any other specializations. + +// release() added in by Google. Use this to conditionally +// transfer ownership of a heap-allocated object to the caller, usually on +// method success. +#ifndef TALK_BASE_SCOPED_PTR_H__ +#define TALK_BASE_SCOPED_PTR_H__ + +#include // for std::ptrdiff_t +#include // for free() decl + +#include "talk/base/common.h" // for ASSERT + +#ifdef _WIN32 +namespace std { using ::ptrdiff_t; }; +#endif // _WIN32 + +namespace talk_base { + +template +class scoped_ptr { + private: + + T* ptr; + + scoped_ptr(scoped_ptr const &); + scoped_ptr & operator=(scoped_ptr const &); + + public: + + typedef T element_type; + + explicit scoped_ptr(T* p = NULL): ptr(p) {} + + ~scoped_ptr() { + typedef char type_must_be_complete[sizeof(T)]; + delete ptr; + } + + void reset(T* p = NULL) { + typedef char type_must_be_complete[sizeof(T)]; + + if (ptr != p) { + T* obj = ptr; + ptr = p; + // Delete last, in case obj destructor indirectly results in ~scoped_ptr + delete obj; + } + } + + T& operator*() const { + ASSERT(ptr != NULL); + return *ptr; + } + + T* operator->() const { + ASSERT(ptr != NULL); + return ptr; + } + + T* get() const { + return ptr; + } + + void swap(scoped_ptr & b) { + T* tmp = b.ptr; + b.ptr = ptr; + ptr = tmp; + } + + T* release() { + T* tmp = ptr; + ptr = NULL; + return tmp; + } + + T** accept() { + if (ptr) { + delete ptr; + ptr = NULL; + } + return &ptr; + } + + T** use() { + return &ptr; + } + + // Allow scoped_ptr to be used in boolean expressions, but not + // implicitly convertible to a real bool (which is dangerous). + // Borrowed from chromium's scoped_ptr implementation. + typedef T* scoped_ptr::*Testable; + operator Testable() const { return ptr ? &scoped_ptr::ptr : NULL; } + +}; + +template inline +void swap(scoped_ptr& a, scoped_ptr& b) { + a.swap(b); +} + + + + +// scoped_array extends scoped_ptr to arrays. Deletion of the array pointed to +// is guaranteed, either on destruction of the scoped_array or via an explicit +// reset(). Use shared_array or std::vector if your needs are more complex. + +template +class scoped_array { + private: + + T* ptr; + + scoped_array(scoped_array const &); + scoped_array & operator=(scoped_array const &); + + public: + + typedef T element_type; + + explicit scoped_array(T* p = NULL) : ptr(p) {} + + ~scoped_array() { + typedef char type_must_be_complete[sizeof(T)]; + delete[] ptr; + } + + void reset(T* p = NULL) { + typedef char type_must_be_complete[sizeof(T)]; + + if (ptr != p) { + T* arr = ptr; + ptr = p; + // Delete last, in case arr destructor indirectly results in ~scoped_array + delete [] arr; + } + } + + T& operator[](std::ptrdiff_t i) const { + ASSERT(ptr != NULL); + ASSERT(i >= 0); + return ptr[i]; + } + + T* get() const { + return ptr; + } + + void swap(scoped_array & b) { + T* tmp = b.ptr; + b.ptr = ptr; + ptr = tmp; + } + + T* release() { + T* tmp = ptr; + ptr = NULL; + return tmp; + } + + T** accept() { + if (ptr) { + delete [] ptr; + ptr = NULL; + } + return &ptr; + } + + // Allow scoped_array to be used in boolean expressions, but not + // implicitly convertible to a real bool (which is dangerous). + // Borrowed from chromium's scoped_array implementation. + typedef T* scoped_array::*Testable; + operator Testable() const { return ptr ? &scoped_array::ptr : NULL; } +}; + +template inline +void swap(scoped_array& a, scoped_array& b) { + a.swap(b); +} + +// scoped_ptr_malloc<> is similar to scoped_ptr<>, but it accepts a +// second template argument, the function used to free the object. + +template class scoped_ptr_malloc { + private: + + T* ptr; + + scoped_ptr_malloc(scoped_ptr_malloc const &); + scoped_ptr_malloc & operator=(scoped_ptr_malloc const &); + + public: + + typedef T element_type; + + explicit scoped_ptr_malloc(T* p = 0): ptr(p) {} + + ~scoped_ptr_malloc() { + FF(ptr); + } + + void reset(T* p = 0) { + if (ptr != p) { + FF(ptr); + ptr = p; + } + } + + T& operator*() const { + ASSERT(ptr != 0); + return *ptr; + } + + T* operator->() const { + ASSERT(ptr != 0); + return ptr; + } + + T* get() const { + return ptr; + } + + void swap(scoped_ptr_malloc & b) { + T* tmp = b.ptr; + b.ptr = ptr; + ptr = tmp; + } + + T* release() { + T* tmp = ptr; + ptr = 0; + return tmp; + } + + T** accept() { + if (ptr) { + FF(ptr); + ptr = 0; + } + return &ptr; + } + + // Allow scoped_ptr_malloc to be used in boolean expressions, but not + // implicitly convertible to a real bool (which is dangerous). + // Borrowed from chromium's scoped_ptr_malloc implementation. + typedef T* scoped_ptr_malloc::*Testable; + operator Testable() const { return ptr ? &scoped_ptr_malloc::ptr : NULL; } +}; + +template inline +void swap(scoped_ptr_malloc& a, scoped_ptr_malloc& b) { + a.swap(b); +} + +} // namespace talk_base + +#endif // #ifndef TALK_BASE_SCOPED_PTR_H__ diff --git a/talk/base/scoped_ref_ptr.h b/talk/base/scoped_ref_ptr.h new file mode 100644 index 000000000..3ce72cbce --- /dev/null +++ b/talk/base/scoped_ref_ptr.h @@ -0,0 +1,162 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +// Originally these classes are from Chromium. +// http://src.chromium.org/viewvc/chrome/trunk/src/base/memory/ref_counted.h?view=markup + +// +// A smart pointer class for reference counted objects. Use this class instead +// of calling AddRef and Release manually on a reference counted object to +// avoid common memory leaks caused by forgetting to Release an object +// reference. Sample usage: +// +// class MyFoo : public RefCounted { +// ... +// }; +// +// void some_function() { +// scoped_refptr foo = new MyFoo(); +// foo->Method(param); +// // |foo| is released when this function returns +// } +// +// void some_other_function() { +// scoped_refptr foo = new MyFoo(); +// ... +// foo = NULL; // explicitly releases |foo| +// ... +// if (foo) +// foo->Method(param); +// } +// +// The above examples show how scoped_refptr acts like a pointer to T. +// Given two scoped_refptr classes, it is also possible to exchange +// references between the two objects, like so: +// +// { +// scoped_refptr a = new MyFoo(); +// scoped_refptr b; +// +// b.swap(a); +// // now, |b| references the MyFoo object, and |a| references NULL. +// } +// +// To make both |a| and |b| in the above example reference the same MyFoo +// object, simply use the assignment operator: +// +// { +// scoped_refptr a = new MyFoo(); +// scoped_refptr b; +// +// b = a; +// // now, |a| and |b| each own a reference to the same MyFoo object. +// } +// + +#ifndef TALK_BASE_SCOPED_REF_PTR_H_ +#define TALK_BASE_SCOPED_REF_PTR_H_ + +namespace talk_base { + +template +class scoped_refptr { + public: + scoped_refptr() : ptr_(NULL) { + } + + scoped_refptr(T* p) : ptr_(p) { + if (ptr_) + ptr_->AddRef(); + } + + scoped_refptr(const scoped_refptr& r) : ptr_(r.ptr_) { + if (ptr_) + ptr_->AddRef(); + } + + template + scoped_refptr(const scoped_refptr& r) : ptr_(r.get()) { + if (ptr_) + ptr_->AddRef(); + } + + ~scoped_refptr() { + if (ptr_) + ptr_->Release(); + } + + T* get() const { return ptr_; } + operator T*() const { return ptr_; } + T* operator->() const { return ptr_; } + + // Release a pointer. + // The return value is the current pointer held by this object. + // If this object holds a NULL pointer, the return value is NULL. + // After this operation, this object will hold a NULL pointer, + // and will not own the object any more. + T* release() { + T* retVal = ptr_; + ptr_ = NULL; + return retVal; + } + + scoped_refptr& operator=(T* p) { + // AddRef first so that self assignment should work + if (p) + p->AddRef(); + if (ptr_ ) + ptr_ ->Release(); + ptr_ = p; + return *this; + } + + scoped_refptr& operator=(const scoped_refptr& r) { + return *this = r.ptr_; + } + + template + scoped_refptr& operator=(const scoped_refptr& r) { + return *this = r.get(); + } + + void swap(T** pp) { + T* p = ptr_; + ptr_ = *pp; + *pp = p; + } + + void swap(scoped_refptr& r) { + swap(&r.ptr_); + } + + protected: + T* ptr_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_SCOPED_REF_PTR_H_ diff --git a/talk/base/sec_buffer.h b/talk/base/sec_buffer.h new file mode 100644 index 000000000..585e27f04 --- /dev/null +++ b/talk/base/sec_buffer.h @@ -0,0 +1,173 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +// @file Contains utility classes that make it easier to use SecBuffers + +#ifndef TALK_BASE_SEC_BUFFER_H__ +#define TALK_BASE_SEC_BUFFER_H__ + +namespace talk_base { + +// A base class for CSecBuffer. Contains +// all implementation that does not require +// template arguments. +class CSecBufferBase : public SecBuffer { + public: + CSecBufferBase() { + Clear(); + } + + // Uses the SSPI to free a pointer, must be + // used for buffers returned from SSPI APIs. + static void FreeSSPI(void *ptr) { + if ( ptr ) { + SECURITY_STATUS status; + status = ::FreeContextBuffer(ptr); + ASSERT(SEC_E_OK == status); // "Freeing context buffer" + } + } + + // Deletes a buffer with operator delete + static void FreeDelete(void *ptr) { + delete [] reinterpret_cast(ptr); + } + + // A noop delete, for buffers over other + // people's memory + static void FreeNone(void *ptr) { + } + + protected: + // Clears the buffer to EMPTY & NULL + void Clear() { + this->BufferType = SECBUFFER_EMPTY; + this->cbBuffer = 0; + this->pvBuffer = NULL; + } +}; + +// Wrapper class for SecBuffer to take care +// of initialization and destruction. +template +class CSecBuffer: public CSecBufferBase { + public: + // Initializes buffer to empty & NULL + CSecBuffer() { + } + + // Frees any allocated memory + ~CSecBuffer() { + Release(); + } + + // Frees the buffer appropriately, and re-nulls + void Release() { + pfnFreeBuffer(this->pvBuffer); + Clear(); + } + + private: + // A placeholder function for compile-time asserts on the class + void CompileAsserts() { + // never invoked... + assert(false); // _T("Notreached") + + // This class must not extend the size of SecBuffer, since + // we use arrays of CSecBuffer in CSecBufferBundle below + cassert(sizeof(CSecBuffer == sizeof(SecBuffer))); + } +}; + +// Contains all generic implementation for the +// SecBufferBundle class +class SecBufferBundleBase { + public: +}; + +// A template class that bundles a SecBufferDesc with +// one or more SecBuffers for convenience. Can take +// care of deallocating buffers appropriately, as indicated +// by pfnFreeBuffer function. +// By default does no deallocation. +template +class CSecBufferBundle : public SecBufferBundleBase { + public: + // Constructs a security buffer bundle with num_buffers + // buffers, all of which are empty and nulled. + CSecBufferBundle() { + desc_.ulVersion = SECBUFFER_VERSION; + desc_.cBuffers = num_buffers; + desc_.pBuffers = buffers_; + } + + // Frees all currently used buffers. + ~CSecBufferBundle() { + Release(); + } + + // Accessor for the descriptor + PSecBufferDesc desc() { + return &desc_; + } + + // Accessor for the descriptor + const PSecBufferDesc desc() const { + return &desc_; + } + + // returns the i-th security buffer + SecBuffer &operator[] (size_t num) { + ASSERT(num < num_buffers); // "Buffer index out of bounds" + return buffers_[num]; + } + + // returns the i-th security buffer + const SecBuffer &operator[] (size_t num) const { + ASSERT(num < num_buffers); // "Buffer index out of bounds" + return buffers_[num]; + } + + // Frees all non-NULL security buffers, + // using the deallocation function + void Release() { + for ( size_t i = 0; i < num_buffers; ++i ) { + buffers_[i].Release(); + } + } + + private: + // Our descriptor + SecBufferDesc desc_; + // Our bundled buffers, each takes care of its own + // initialization and destruction + CSecBuffer buffers_[num_buffers]; +}; + +} // namespace talk_base + +#endif // TALK_BASE_SEC_BUFFER_H__ diff --git a/talk/base/sha1.cc b/talk/base/sha1.cc new file mode 100644 index 000000000..d52d56dca --- /dev/null +++ b/talk/base/sha1.cc @@ -0,0 +1,282 @@ +/* + * SHA-1 in C + * By Steve Reid + * 100% Public Domain + * + * ----------------- + * Modified 7/98 + * By James H. Brown + * Still 100% Public Domain + * + * Corrected a problem which generated improper hash values on 16 bit machines + * Routine SHA1Update changed from + * void SHA1Update(SHA1_CTX* context, unsigned char* data, unsigned int + * len) + * to + * void SHA1Update(SHA1_CTX* context, unsigned char* data, unsigned + * long len) + * + * The 'len' parameter was declared an int which works fine on 32 bit machines. + * However, on 16 bit machines an int is too small for the shifts being done + * against + * it. This caused the hash function to generate incorrect values if len was + * greater than 8191 (8K - 1) due to the 'len << 3' on line 3 of SHA1Update(). + * + * Since the file IO in main() reads 16K at a time, any file 8K or larger would + * be guaranteed to generate the wrong hash (e.g. Test Vector #3, a million + * "a"s). + * + * I also changed the declaration of variables i & j in SHA1Update to + * unsigned long from unsigned int for the same reason. + * + * These changes should make no difference to any 32 bit implementations since + * an + * int and a long are the same size in those environments. + * + * -- + * I also corrected a few compiler warnings generated by Borland C. + * 1. Added #include for exit() prototype + * 2. Removed unused variable 'j' in SHA1Final + * 3. Changed exit(0) to return(0) at end of main. + * + * ALL changes I made can be located by searching for comments containing 'JHB' + * ----------------- + * Modified 8/98 + * By Steve Reid + * Still 100% public domain + * + * 1- Removed #include and used return() instead of exit() + * 2- Fixed overwriting of finalcount in SHA1Final() (discovered by Chris Hall) + * 3- Changed email address from steve@edmweb.com to sreid@sea-to-sky.net + * + * ----------------- + * Modified 4/01 + * By Saul Kravitz + * Still 100% PD + * Modified to run on Compaq Alpha hardware. + * + * ----------------- + * Modified 07/2002 + * By Ralph Giles + * Still 100% public domain + * modified for use with stdint types, autoconf + * code cleanup, removed attribution comments + * switched SHA1Final() argument order for consistency + * use SHA1_ prefix for public api + * move public api to sha1.h + * + * ----------------- + * Modified 02/2012 + * By Justin Uberti + * Remove underscore from SHA1 prefix to avoid conflict with OpenSSL + * Remove test code + * Untabify + * + * ----------------- + * Modified 03/2012 + * By Ronghua Wu + * Change the typedef of uint32(8)_t to uint32(8). We need this because in the + * chromium android build, the stdio.h will include stdint.h which already + * defined uint32(8)_t. + * + * ----------------- + * Modified 04/2012 + * By Frank Barchard + * Ported to C++, Google style, change len to size_t, enable SHA1HANDSOFF + * + * Test Vectors (from FIPS PUB 180-1) + * "abc" + * A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D + * "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" + * 84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1 + * A million repetitions of "a" + * 34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F + */ + +// Enabling SHA1HANDSOFF preserves the caller's data buffer. +// Disabling SHA1HANDSOFF the buffer will be modified (end swapped). +#define SHA1HANDSOFF + +#include "talk/base/sha1.h" + +#include +#include + +void SHA1Transform(uint32 state[5], const uint8 buffer[64]); + +#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits)))) + +// blk0() and blk() perform the initial expand. +// I got the idea of expanding during the round function from SSLeay +// FIXME: can we do this in an endian-proof way? +#ifdef ARCH_CPU_BIG_ENDIAN +#define blk0(i) block->l[i] +#else +#define blk0(i) (block->l[i] = (rol(block->l[i], 24) & 0xFF00FF00) | \ + (rol(block->l[i], 8) & 0x00FF00FF)) +#endif +#define blk(i) (block->l[i & 15] = rol(block->l[(i + 13) & 15] ^ \ + block->l[(i + 8) & 15] ^ block->l[(i + 2) & 15] ^ block->l[i & 15], 1)) + +// (R0+R1), R2, R3, R4 are the different operations used in SHA1. +#define R0(v, w, x, y, z, i) \ + z += ((w & (x ^ y)) ^ y) + blk0(i) + 0x5A827999 + rol(v, 5); \ + w = rol(w, 30); +#define R1(v, w, x, y, z, i) \ + z += ((w & (x ^ y)) ^ y) + blk(i) + 0x5A827999 + rol(v, 5); \ + w = rol(w, 30); +#define R2(v, w, x, y, z, i) \ + z += (w ^ x ^ y) + blk(i) + 0x6ED9EBA1 + rol(v, 5);\ + w = rol(w, 30); +#define R3(v, w, x, y, z, i) \ + z += (((w | x) & y) | (w & x)) + blk(i) + 0x8F1BBCDC + rol(v, 5); \ + w = rol(w, 30); +#define R4(v, w, x, y, z, i) \ + z += (w ^ x ^ y) + blk(i) + 0xCA62C1D6 + rol(v, 5); \ + w = rol(w, 30); + +#ifdef VERBOSE // SAK +void SHAPrintContext(SHA1_CTX *context, char *msg) { + printf("%s (%d,%d) %x %x %x %x %x\n", + msg, + context->count[0], context->count[1], + context->state[0], + context->state[1], + context->state[2], + context->state[3], + context->state[4]); +} +#endif /* VERBOSE */ + +// Hash a single 512-bit block. This is the core of the algorithm. +void SHA1Transform(uint32 state[5], const uint8 buffer[64]) { + union CHAR64LONG16 { + uint8 c[64]; + uint32 l[16]; + }; +#ifdef SHA1HANDSOFF + static uint8 workspace[64]; + memcpy(workspace, buffer, 64); + CHAR64LONG16* block = reinterpret_cast(workspace); +#else + // Note(fbarchard): This option does modify the user's data buffer. + CHAR64LONG16* block = const_cast( + reinterpret_cast(buffer)); +#endif + + // Copy context->state[] to working vars. + uint32 a = state[0]; + uint32 b = state[1]; + uint32 c = state[2]; + uint32 d = state[3]; + uint32 e = state[4]; + + // 4 rounds of 20 operations each. Loop unrolled. + // Note(fbarchard): The following has lint warnings for multiple ; on + // a line and no space after , but is left as-is to be similar to the + // original code. + R0(a,b,c,d,e,0); R0(e,a,b,c,d,1); R0(d,e,a,b,c,2); R0(c,d,e,a,b,3); + R0(b,c,d,e,a,4); R0(a,b,c,d,e,5); R0(e,a,b,c,d,6); R0(d,e,a,b,c,7); + R0(c,d,e,a,b,8); R0(b,c,d,e,a,9); R0(a,b,c,d,e,10); R0(e,a,b,c,d,11); + R0(d,e,a,b,c,12); R0(c,d,e,a,b,13); R0(b,c,d,e,a,14); R0(a,b,c,d,e,15); + R1(e,a,b,c,d,16); R1(d,e,a,b,c,17); R1(c,d,e,a,b,18); R1(b,c,d,e,a,19); + R2(a,b,c,d,e,20); R2(e,a,b,c,d,21); R2(d,e,a,b,c,22); R2(c,d,e,a,b,23); + R2(b,c,d,e,a,24); R2(a,b,c,d,e,25); R2(e,a,b,c,d,26); R2(d,e,a,b,c,27); + R2(c,d,e,a,b,28); R2(b,c,d,e,a,29); R2(a,b,c,d,e,30); R2(e,a,b,c,d,31); + R2(d,e,a,b,c,32); R2(c,d,e,a,b,33); R2(b,c,d,e,a,34); R2(a,b,c,d,e,35); + R2(e,a,b,c,d,36); R2(d,e,a,b,c,37); R2(c,d,e,a,b,38); R2(b,c,d,e,a,39); + R3(a,b,c,d,e,40); R3(e,a,b,c,d,41); R3(d,e,a,b,c,42); R3(c,d,e,a,b,43); + R3(b,c,d,e,a,44); R3(a,b,c,d,e,45); R3(e,a,b,c,d,46); R3(d,e,a,b,c,47); + R3(c,d,e,a,b,48); R3(b,c,d,e,a,49); R3(a,b,c,d,e,50); R3(e,a,b,c,d,51); + R3(d,e,a,b,c,52); R3(c,d,e,a,b,53); R3(b,c,d,e,a,54); R3(a,b,c,d,e,55); + R3(e,a,b,c,d,56); R3(d,e,a,b,c,57); R3(c,d,e,a,b,58); R3(b,c,d,e,a,59); + R4(a,b,c,d,e,60); R4(e,a,b,c,d,61); R4(d,e,a,b,c,62); R4(c,d,e,a,b,63); + R4(b,c,d,e,a,64); R4(a,b,c,d,e,65); R4(e,a,b,c,d,66); R4(d,e,a,b,c,67); + R4(c,d,e,a,b,68); R4(b,c,d,e,a,69); R4(a,b,c,d,e,70); R4(e,a,b,c,d,71); + R4(d,e,a,b,c,72); R4(c,d,e,a,b,73); R4(b,c,d,e,a,74); R4(a,b,c,d,e,75); + R4(e,a,b,c,d,76); R4(d,e,a,b,c,77); R4(c,d,e,a,b,78); R4(b,c,d,e,a,79); + + // Add the working vars back into context.state[]. + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + state[4] += e; +} + +// SHA1Init - Initialize new context. +void SHA1Init(SHA1_CTX* context) { + // SHA1 initialization constants. + context->state[0] = 0x67452301; + context->state[1] = 0xEFCDAB89; + context->state[2] = 0x98BADCFE; + context->state[3] = 0x10325476; + context->state[4] = 0xC3D2E1F0; + context->count[0] = context->count[1] = 0; +} + +// Run your data through this. +void SHA1Update(SHA1_CTX* context, const uint8* data, size_t input_len) { + size_t i = 0; + +#ifdef VERBOSE + SHAPrintContext(context, "before"); +#endif + + // Compute number of bytes mod 64. + size_t index = (context->count[0] >> 3) & 63; + + // Update number of bits. + // TODO: Use uint64 instead of 2 uint32 for count. + // count[0] has low 29 bits for byte count + 3 pad 0's making 32 bits for + // bit count. + // Add bit count to low uint32 + context->count[0] += static_cast(input_len << 3); + if (context->count[0] < static_cast(input_len << 3)) { + ++context->count[1]; // if overlow (carry), add one to high word + } + context->count[1] += static_cast(input_len >> 29); + if ((index + input_len) > 63) { + i = 64 - index; + memcpy(&context->buffer[index], data, i); + SHA1Transform(context->state, context->buffer); + for (; i + 63 < input_len; i += 64) { + SHA1Transform(context->state, data + i); + } + index = 0; + } + memcpy(&context->buffer[index], &data[i], input_len - i); + +#ifdef VERBOSE + SHAPrintContext(context, "after "); +#endif +} + +// Add padding and return the message digest. +void SHA1Final(SHA1_CTX* context, uint8 digest[SHA1_DIGEST_SIZE]) { + uint8 finalcount[8]; + for (int i = 0; i < 8; ++i) { + // Endian independent + finalcount[i] = static_cast( + (context->count[(i >= 4 ? 0 : 1)] >> ((3 - (i & 3)) * 8) ) & 255); + } + SHA1Update(context, reinterpret_cast("\200"), 1); + while ((context->count[0] & 504) != 448) { + SHA1Update(context, reinterpret_cast("\0"), 1); + } + SHA1Update(context, finalcount, 8); // Should cause a SHA1Transform(). + for (int i = 0; i < SHA1_DIGEST_SIZE; ++i) { + digest[i] = static_cast( + (context->state[i >> 2] >> ((3 - (i & 3)) * 8) ) & 255); + } + + // Wipe variables. + memset(context->buffer, 0, 64); + memset(context->state, 0, 20); + memset(context->count, 0, 8); + memset(finalcount, 0, 8); // SWR + +#ifdef SHA1HANDSOFF // Make SHA1Transform overwrite its own static vars. + SHA1Transform(context->state, context->buffer); +#endif +} diff --git a/talk/base/sha1.h b/talk/base/sha1.h new file mode 100644 index 000000000..262b744cd --- /dev/null +++ b/talk/base/sha1.h @@ -0,0 +1,28 @@ +/* + * SHA-1 in C + * By Steve Reid + * 100% Public Domain + * +*/ + +// Ported to C++, Google style and uses basictypes.h + +#ifndef TALK_BASE_SHA1_H_ +#define TALK_BASE_SHA1_H_ + +#include "talk/base/basictypes.h" + +struct SHA1_CTX { + uint32 state[5]; + // TODO: Change bit count to uint64. + uint32 count[2]; // Bit count of input. + uint8 buffer[64]; +}; + +#define SHA1_DIGEST_SIZE 20 + +void SHA1Init(SHA1_CTX* context); +void SHA1Update(SHA1_CTX* context, const uint8* data, size_t len); +void SHA1Final(SHA1_CTX* context, uint8 digest[SHA1_DIGEST_SIZE]); + +#endif // TALK_BASE_SHA1_H_ diff --git a/talk/base/sha1digest.h b/talk/base/sha1digest.h new file mode 100644 index 000000000..c8b1e46a1 --- /dev/null +++ b/talk/base/sha1digest.h @@ -0,0 +1,64 @@ +/* + * libjingle + * Copyright 2012 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. + */ + +#ifndef TALK_BASE_SHA1DIGEST_H_ +#define TALK_BASE_SHA1DIGEST_H_ + +#include "talk/base/messagedigest.h" +#include "talk/base/sha1.h" + +namespace talk_base { + +// A simple wrapper for our SHA-1 implementation. +class Sha1Digest : public MessageDigest { + public: + enum { kSize = SHA1_DIGEST_SIZE }; + Sha1Digest() { + SHA1Init(&ctx_); + } + virtual size_t Size() const { + return kSize; + } + virtual void Update(const void* buf, size_t len) { + SHA1Update(&ctx_, static_cast(buf), len); + } + virtual size_t Finish(void* buf, size_t len) { + if (len < kSize) { + return 0; + } + SHA1Final(&ctx_, static_cast(buf)); + SHA1Init(&ctx_); // Reset for next use. + return kSize; + } + + private: + SHA1_CTX ctx_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_SHA1DIGEST_H_ diff --git a/talk/base/sha1digest_unittest.cc b/talk/base/sha1digest_unittest.cc new file mode 100644 index 000000000..5ab681924 --- /dev/null +++ b/talk/base/sha1digest_unittest.cc @@ -0,0 +1,99 @@ +/* + * libjingle + * Copyright 2012 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/base/sha1digest.h" +#include "talk/base/gunit.h" +#include "talk/base/stringencode.h" + +namespace talk_base { + +std::string Sha1(const std::string& input) { + Sha1Digest sha1; + return ComputeDigest(&sha1, input); +} + +TEST(Sha1DigestTest, TestSize) { + Sha1Digest sha1; + EXPECT_EQ(20U, Sha1Digest::kSize); + EXPECT_EQ(20U, sha1.Size()); +} + +TEST(Sha1DigestTest, TestBasic) { + // Test vectors from sha1.c. + EXPECT_EQ("da39a3ee5e6b4b0d3255bfef95601890afd80709", Sha1("")); + EXPECT_EQ("a9993e364706816aba3e25717850c26c9cd0d89d", Sha1("abc")); + EXPECT_EQ("84983e441c3bd26ebaae4aa1f95129e5e54670f1", + Sha1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")); + std::string a_million_as(1000000, 'a'); + EXPECT_EQ("34aa973cd4c4daa4f61eeb2bdbad27316534016f", Sha1(a_million_as)); +} + +TEST(Sha1DigestTest, TestMultipleUpdates) { + Sha1Digest sha1; + std::string input = + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; + char output[Sha1Digest::kSize]; + for (size_t i = 0; i < input.size(); ++i) { + sha1.Update(&input[i], 1); + } + EXPECT_EQ(sha1.Size(), sha1.Finish(output, sizeof(output))); + EXPECT_EQ("84983e441c3bd26ebaae4aa1f95129e5e54670f1", + hex_encode(output, sizeof(output))); +} + +TEST(Sha1DigestTest, TestReuse) { + Sha1Digest sha1; + std::string input = "abc"; + EXPECT_EQ("a9993e364706816aba3e25717850c26c9cd0d89d", + ComputeDigest(&sha1, input)); + input = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; + EXPECT_EQ("84983e441c3bd26ebaae4aa1f95129e5e54670f1", + ComputeDigest(&sha1, input)); +} + +TEST(Sha1DigestTest, TestBufferTooSmall) { + Sha1Digest sha1; + std::string input = "abcdefghijklmnopqrstuvwxyz"; + char output[Sha1Digest::kSize - 1]; + sha1.Update(input.c_str(), input.size()); + EXPECT_EQ(0U, sha1.Finish(output, sizeof(output))); +} + +TEST(Sha1DigestTest, TestBufferConst) { + Sha1Digest sha1; + const int kLongSize = 1000000; + std::string input(kLongSize, '\0'); + for (int i = 0; i < kLongSize; ++i) { + input[i] = static_cast(i); + } + sha1.Update(input.c_str(), input.size()); + for (int i = 0; i < kLongSize; ++i) { + EXPECT_EQ(static_cast(i), input[i]); + } +} + +} // namespace talk_base diff --git a/talk/base/sharedexclusivelock.cc b/talk/base/sharedexclusivelock.cc new file mode 100644 index 000000000..0b0439aa5 --- /dev/null +++ b/talk/base/sharedexclusivelock.cc @@ -0,0 +1,61 @@ +/* + * libjingle + * Copyright 2011, 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/base/sharedexclusivelock.h" + +namespace talk_base { + +SharedExclusiveLock::SharedExclusiveLock() + : shared_count_is_zero_(true, true), + shared_count_(0) { +} + +void SharedExclusiveLock::LockExclusive() { + cs_exclusive_.Enter(); + shared_count_is_zero_.Wait(talk_base::kForever); +} + +void SharedExclusiveLock::UnlockExclusive() { + cs_exclusive_.Leave(); +} + +void SharedExclusiveLock::LockShared() { + CritScope exclusive_scope(&cs_exclusive_); + CritScope shared_scope(&cs_shared_); + if (++shared_count_ == 1) { + shared_count_is_zero_.Reset(); + } +} + +void SharedExclusiveLock::UnlockShared() { + CritScope shared_scope(&cs_shared_); + if (--shared_count_ == 0) { + shared_count_is_zero_.Set(); + } +} + +} // namespace talk_base diff --git a/talk/base/sharedexclusivelock.h b/talk/base/sharedexclusivelock.h new file mode 100644 index 000000000..2bdd85483 --- /dev/null +++ b/talk/base/sharedexclusivelock.h @@ -0,0 +1,93 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_BASE_SHAREDEXCLUSIVELOCK_H_ +#define TALK_BASE_SHAREDEXCLUSIVELOCK_H_ + +#include "talk/base/constructormagic.h" +#include "talk/base/criticalsection.h" +#include "talk/base/event.h" + +namespace talk_base { + +// This class provides shared-exclusive lock. It can be used in cases like +// multiple-readers/single-writer model. +class SharedExclusiveLock { + public: + SharedExclusiveLock(); + + // Locking/unlocking methods. It is encouraged to use SharedScope or + // ExclusiveScope for protection. + void LockExclusive(); + void UnlockExclusive(); + void LockShared(); + void UnlockShared(); + + private: + talk_base::CriticalSection cs_exclusive_; + talk_base::CriticalSection cs_shared_; + talk_base::Event shared_count_is_zero_; + int shared_count_; + + DISALLOW_COPY_AND_ASSIGN(SharedExclusiveLock); +}; + +class SharedScope { + public: + explicit SharedScope(SharedExclusiveLock* lock) : lock_(lock) { + lock_->LockShared(); + } + + ~SharedScope() { + lock_->UnlockShared(); + } + + private: + SharedExclusiveLock* lock_; + + DISALLOW_COPY_AND_ASSIGN(SharedScope); +}; + +class ExclusiveScope { + public: + explicit ExclusiveScope(SharedExclusiveLock* lock) : lock_(lock) { + lock_->LockExclusive(); + } + + ~ExclusiveScope() { + lock_->UnlockExclusive(); + } + + private: + SharedExclusiveLock* lock_; + + DISALLOW_COPY_AND_ASSIGN(ExclusiveScope); +}; + +} // namespace talk_base + +#endif // TALK_BASE_SHAREDEXCLUSIVELOCK_H_ diff --git a/talk/base/sharedexclusivelock_unittest.cc b/talk/base/sharedexclusivelock_unittest.cc new file mode 100644 index 000000000..46b7fdfdc --- /dev/null +++ b/talk/base/sharedexclusivelock_unittest.cc @@ -0,0 +1,234 @@ +/* + * libjingle + * Copyright 2011, 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/base/common.h" +#include "talk/base/gunit.h" +#include "talk/base/messagehandler.h" +#include "talk/base/messagequeue.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sharedexclusivelock.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" + +namespace talk_base { + +static const uint32 kMsgRead = 0; +static const uint32 kMsgWrite = 0; +static const int kNoWaitThresholdInMs = 10; +static const int kWaitThresholdInMs = 80; +static const int kProcessTimeInMs = 100; +static const int kProcessTimeoutInMs = 5000; + +class SharedExclusiveTask : public MessageHandler { + public: + SharedExclusiveTask(SharedExclusiveLock* shared_exclusive_lock, + int* value, + bool* done) + : shared_exclusive_lock_(shared_exclusive_lock), + waiting_time_in_ms_(0), + value_(value), + done_(done) { + worker_thread_.reset(new Thread()); + worker_thread_->Start(); + } + + int waiting_time_in_ms() const { return waiting_time_in_ms_; } + + protected: + scoped_ptr worker_thread_; + SharedExclusiveLock* shared_exclusive_lock_; + int waiting_time_in_ms_; + int* value_; + bool* done_; +}; + +class ReadTask : public SharedExclusiveTask { + public: + ReadTask(SharedExclusiveLock* shared_exclusive_lock, int* value, bool* done) + : SharedExclusiveTask(shared_exclusive_lock, value, done) { + } + + void PostRead(int* value) { + worker_thread_->Post(this, kMsgRead, new TypedMessageData(value)); + } + + private: + virtual void OnMessage(Message* message) { + ASSERT(talk_base::Thread::Current() == worker_thread_.get()); + ASSERT(message != NULL); + ASSERT(message->message_id == kMsgRead); + + TypedMessageData* message_data = + static_cast*>(message->pdata); + + uint32 start_time = Time(); + { + SharedScope ss(shared_exclusive_lock_); + waiting_time_in_ms_ = TimeDiff(Time(), start_time); + + Thread::SleepMs(kProcessTimeInMs); + *message_data->data() = *value_; + *done_ = true; + } + delete message->pdata; + message->pdata = NULL; + } +}; + +class WriteTask : public SharedExclusiveTask { + public: + WriteTask(SharedExclusiveLock* shared_exclusive_lock, int* value, bool* done) + : SharedExclusiveTask(shared_exclusive_lock, value, done) { + } + + void PostWrite(int value) { + worker_thread_->Post(this, kMsgWrite, new TypedMessageData(value)); + } + + private: + virtual void OnMessage(Message* message) { + ASSERT(talk_base::Thread::Current() == worker_thread_.get()); + ASSERT(message != NULL); + ASSERT(message->message_id == kMsgWrite); + + TypedMessageData* message_data = + static_cast*>(message->pdata); + + uint32 start_time = Time(); + { + ExclusiveScope es(shared_exclusive_lock_); + waiting_time_in_ms_ = TimeDiff(Time(), start_time); + + Thread::SleepMs(kProcessTimeInMs); + *value_ = message_data->data(); + *done_ = true; + } + delete message->pdata; + message->pdata = NULL; + } +}; + +// Unit test for SharedExclusiveLock. +class SharedExclusiveLockTest + : public testing::Test { + public: + SharedExclusiveLockTest() : value_(0) { + } + + virtual void SetUp() { + shared_exclusive_lock_.reset(new SharedExclusiveLock()); + } + + protected: + scoped_ptr shared_exclusive_lock_; + int value_; +}; + +TEST_F(SharedExclusiveLockTest, TestSharedShared) { + int value0, value1; + bool done0, done1; + ReadTask reader0(shared_exclusive_lock_.get(), &value_, &done0); + ReadTask reader1(shared_exclusive_lock_.get(), &value_, &done1); + + // Test shared locks can be shared without waiting. + { + SharedScope ss(shared_exclusive_lock_.get()); + value_ = 1; + done0 = false; + done1 = false; + reader0.PostRead(&value0); + reader1.PostRead(&value1); + Thread::SleepMs(kProcessTimeInMs); + } + + EXPECT_TRUE_WAIT(done0, kProcessTimeoutInMs); + EXPECT_EQ(1, value0); + EXPECT_LE(reader0.waiting_time_in_ms(), kNoWaitThresholdInMs); + EXPECT_TRUE_WAIT(done1, kProcessTimeoutInMs); + EXPECT_EQ(1, value1); + EXPECT_LE(reader1.waiting_time_in_ms(), kNoWaitThresholdInMs); +} + +TEST_F(SharedExclusiveLockTest, TestSharedExclusive) { + bool done; + WriteTask writer(shared_exclusive_lock_.get(), &value_, &done); + + // Test exclusive lock needs to wait for shared lock. + { + SharedScope ss(shared_exclusive_lock_.get()); + value_ = 1; + done = false; + writer.PostWrite(2); + Thread::SleepMs(kProcessTimeInMs); + EXPECT_EQ(1, value_); + } + + EXPECT_TRUE_WAIT(done, kProcessTimeoutInMs); + EXPECT_EQ(2, value_); + EXPECT_GE(writer.waiting_time_in_ms(), kWaitThresholdInMs); +} + +TEST_F(SharedExclusiveLockTest, TestExclusiveShared) { + int value; + bool done; + ReadTask reader(shared_exclusive_lock_.get(), &value_, &done); + + // Test shared lock needs to wait for exclusive lock. + { + ExclusiveScope es(shared_exclusive_lock_.get()); + value_ = 1; + done = false; + reader.PostRead(&value); + Thread::SleepMs(kProcessTimeInMs); + value_ = 2; + } + + EXPECT_TRUE_WAIT(done, kProcessTimeoutInMs); + EXPECT_EQ(2, value); + EXPECT_GE(reader.waiting_time_in_ms(), kWaitThresholdInMs); +} + +TEST_F(SharedExclusiveLockTest, TestExclusiveExclusive) { + bool done; + WriteTask writer(shared_exclusive_lock_.get(), &value_, &done); + + // Test exclusive lock needs to wait for exclusive lock. + { + ExclusiveScope es(shared_exclusive_lock_.get()); + value_ = 1; + done = false; + writer.PostWrite(2); + Thread::SleepMs(kProcessTimeInMs); + EXPECT_EQ(1, value_); + } + + EXPECT_TRUE_WAIT(done, kProcessTimeoutInMs); + EXPECT_EQ(2, value_); + EXPECT_GE(writer.waiting_time_in_ms(), kWaitThresholdInMs); +} + +} // namespace talk_base diff --git a/talk/base/signalthread.cc b/talk/base/signalthread.cc new file mode 100644 index 000000000..88f3ff7d4 --- /dev/null +++ b/talk/base/signalthread.cc @@ -0,0 +1,166 @@ +/* + * libjingle + * Copyright 2004--2009, 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/base/signalthread.h" + +#include "talk/base/common.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// SignalThread +/////////////////////////////////////////////////////////////////////////////// + +SignalThread::SignalThread() + : main_(Thread::Current()), + worker_(this), + state_(kInit), + refcount_(1) { + main_->SignalQueueDestroyed.connect(this, + &SignalThread::OnMainThreadDestroyed); + worker_.SetName("SignalThread", this); +} + +SignalThread::~SignalThread() { + ASSERT(refcount_ == 0); +} + +bool SignalThread::SetName(const std::string& name, const void* obj) { + EnterExit ee(this); + ASSERT(main_->IsCurrent()); + ASSERT(kInit == state_); + return worker_.SetName(name, obj); +} + +bool SignalThread::SetPriority(ThreadPriority priority) { + EnterExit ee(this); + ASSERT(main_->IsCurrent()); + ASSERT(kInit == state_); + return worker_.SetPriority(priority); +} + +void SignalThread::Start() { + EnterExit ee(this); + ASSERT(main_->IsCurrent()); + if (kInit == state_ || kComplete == state_) { + state_ = kRunning; + OnWorkStart(); + worker_.Start(); + } else { + ASSERT(false); + } +} + +void SignalThread::Destroy(bool wait) { + EnterExit ee(this); + ASSERT(main_->IsCurrent()); + if ((kInit == state_) || (kComplete == state_)) { + refcount_--; + } else if (kRunning == state_ || kReleasing == state_) { + state_ = kStopping; + // OnWorkStop() must follow Quit(), so that when the thread wakes up due to + // OWS(), ContinueWork() will return false. + worker_.Quit(); + OnWorkStop(); + if (wait) { + // Release the thread's lock so that it can return from ::Run. + cs_.Leave(); + worker_.Stop(); + cs_.Enter(); + refcount_--; + } + } else { + ASSERT(false); + } +} + +void SignalThread::Release() { + EnterExit ee(this); + ASSERT(main_->IsCurrent()); + if (kComplete == state_) { + refcount_--; + } else if (kRunning == state_) { + state_ = kReleasing; + } else { + // if (kInit == state_) use Destroy() + ASSERT(false); + } +} + +bool SignalThread::ContinueWork() { + EnterExit ee(this); + ASSERT(worker_.IsCurrent()); + return worker_.ProcessMessages(0); +} + +void SignalThread::OnMessage(Message *msg) { + EnterExit ee(this); + if (ST_MSG_WORKER_DONE == msg->message_id) { + ASSERT(main_->IsCurrent()); + OnWorkDone(); + bool do_delete = false; + if (kRunning == state_) { + state_ = kComplete; + } else { + do_delete = true; + } + if (kStopping != state_) { + // Before signaling that the work is done, make sure that the worker + // thread actually is done. We got here because DoWork() finished and + // Run() posted the ST_MSG_WORKER_DONE message. This means the worker + // thread is about to go away anyway, but sometimes it doesn't actually + // finish before SignalWorkDone is processed, and for a reusable + // SignalThread this makes an assert in thread.cc fire. + // + // Calling Stop() on the worker ensures that the OS thread that underlies + // the worker will finish, and will be set to NULL, enabling us to call + // Start() again. + worker_.Stop(); + SignalWorkDone(this); + } + if (do_delete) { + refcount_--; + } + } +} + +void SignalThread::Run() { + DoWork(); + { + EnterExit ee(this); + if (main_) { + main_->Post(this, ST_MSG_WORKER_DONE); + } + } +} + +void SignalThread::OnMainThreadDestroyed() { + EnterExit ee(this); + main_ = NULL; +} + +} // namespace talk_base diff --git a/talk/base/signalthread.h b/talk/base/signalthread.h new file mode 100644 index 000000000..79c00be31 --- /dev/null +++ b/talk/base/signalthread.h @@ -0,0 +1,172 @@ +/* + * libjingle + * Copyright 2004--2009, 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. + */ + +#ifndef TALK_BASE_SIGNALTHREAD_H_ +#define TALK_BASE_SIGNALTHREAD_H_ + +#include + +#include "talk/base/constructormagic.h" +#include "talk/base/sigslot.h" +#include "talk/base/thread.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// SignalThread - Base class for worker threads. The main thread should call +// Start() to begin work, and then follow one of these models: +// Normal: Wait for SignalWorkDone, and then call Release to destroy. +// Cancellation: Call Release(true), to abort the worker thread. +// Fire-and-forget: Call Release(false), which allows the thread to run to +// completion, and then self-destruct without further notification. +// Periodic tasks: Wait for SignalWorkDone, then eventually call Start() +// again to repeat the task. When the instance isn't needed anymore, +// call Release. DoWork, OnWorkStart and OnWorkStop are called again, +// on a new thread. +// The subclass should override DoWork() to perform the background task. By +// periodically calling ContinueWork(), it can check for cancellation. +// OnWorkStart and OnWorkDone can be overridden to do pre- or post-work +// tasks in the context of the main thread. +/////////////////////////////////////////////////////////////////////////////// + +class SignalThread + : public sigslot::has_slots<>, + protected MessageHandler { + public: + SignalThread(); + + // Context: Main Thread. Call before Start to change the worker's name. + bool SetName(const std::string& name, const void* obj); + + // Context: Main Thread. Call before Start to change the worker's priority. + bool SetPriority(ThreadPriority priority); + + // Context: Main Thread. Call to begin the worker thread. + void Start(); + + // Context: Main Thread. If the worker thread is not running, deletes the + // object immediately. Otherwise, asks the worker thread to abort processing, + // and schedules the object to be deleted once the worker exits. + // SignalWorkDone will not be signalled. If wait is true, does not return + // until the thread is deleted. + void Destroy(bool wait); + + // Context: Main Thread. If the worker thread is complete, deletes the + // object immediately. Otherwise, schedules the object to be deleted once + // the worker thread completes. SignalWorkDone will be signalled. + void Release(); + + // Context: Main Thread. Signalled when work is complete. + sigslot::signal1 SignalWorkDone; + + enum { ST_MSG_WORKER_DONE, ST_MSG_FIRST_AVAILABLE }; + + protected: + virtual ~SignalThread(); + + Thread* worker() { return &worker_; } + + // Context: Main Thread. Subclass should override to do pre-work setup. + virtual void OnWorkStart() { } + + // Context: Worker Thread. Subclass should override to do work. + virtual void DoWork() = 0; + + // Context: Worker Thread. Subclass should call periodically to + // dispatch messages and determine if the thread should terminate. + bool ContinueWork(); + + // Context: Worker Thread. Subclass should override when extra work is + // needed to abort the worker thread. + virtual void OnWorkStop() { } + + // Context: Main Thread. Subclass should override to do post-work cleanup. + virtual void OnWorkDone() { } + + // Context: Any Thread. If subclass overrides, be sure to call the base + // implementation. Do not use (message_id < ST_MSG_FIRST_AVAILABLE) + virtual void OnMessage(Message *msg); + + private: + enum State { + kInit, // Initialized, but not started + kRunning, // Started and doing work + kReleasing, // Same as running, but to be deleted when work is done + kComplete, // Work is done + kStopping, // Work is being interrupted + }; + + class Worker : public Thread { + public: + explicit Worker(SignalThread* parent) : parent_(parent) {} + virtual void Run() { parent_->Run(); } + + private: + SignalThread* parent_; + + DISALLOW_IMPLICIT_CONSTRUCTORS(Worker); + }; + + class EnterExit { + public: + explicit EnterExit(SignalThread* t) : t_(t) { + t_->cs_.Enter(); + // If refcount_ is zero then the object has already been deleted and we + // will be double-deleting it in ~EnterExit()! (shouldn't happen) + ASSERT(t_->refcount_ != 0); + ++t_->refcount_; + } + ~EnterExit() { + bool d = (0 == --t_->refcount_); + t_->cs_.Leave(); + if (d) + delete t_; + } + + private: + SignalThread* t_; + + DISALLOW_IMPLICIT_CONSTRUCTORS(EnterExit); + }; + + void Run(); + void OnMainThreadDestroyed(); + + Thread* main_; + Worker worker_; + CriticalSection cs_; + State state_; + int refcount_; + + DISALLOW_COPY_AND_ASSIGN(SignalThread); +}; + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_SIGNALTHREAD_H_ diff --git a/talk/base/signalthread_unittest.cc b/talk/base/signalthread_unittest.cc new file mode 100644 index 000000000..4ad59613d --- /dev/null +++ b/talk/base/signalthread_unittest.cc @@ -0,0 +1,209 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/gunit.h" +#include "talk/base/signalthread.h" +#include "talk/base/thread.h" + +using namespace talk_base; + +class SignalThreadTest : public testing::Test, public sigslot::has_slots<> { + public: + class SlowSignalThread : public SignalThread { + public: + SlowSignalThread(SignalThreadTest* harness) : harness_(harness) { + } + + virtual ~SlowSignalThread() { + EXPECT_EQ(harness_->main_thread_, Thread::Current()); + ++harness_->thread_deleted_; + } + + const SignalThreadTest* harness() { return harness_; } + + protected: + virtual void OnWorkStart() { + ASSERT_TRUE(harness_ != NULL); + ++harness_->thread_started_; + EXPECT_EQ(harness_->main_thread_, Thread::Current()); + EXPECT_FALSE(worker()->started()); // not started yet + } + + virtual void OnWorkStop() { + ++harness_->thread_stopped_; + EXPECT_EQ(harness_->main_thread_, Thread::Current()); + EXPECT_TRUE(worker()->started()); // not stopped yet + } + + virtual void OnWorkDone() { + ++harness_->thread_done_; + EXPECT_EQ(harness_->main_thread_, Thread::Current()); + EXPECT_TRUE(worker()->started()); // not stopped yet + } + + virtual void DoWork() { + EXPECT_NE(harness_->main_thread_, Thread::Current()); + EXPECT_EQ(worker(), Thread::Current()); + Thread::Current()->socketserver()->Wait(250, false); + } + + private: + SignalThreadTest* harness_; + DISALLOW_EVIL_CONSTRUCTORS(SlowSignalThread); + }; + + void OnWorkComplete(talk_base::SignalThread* thread) { + SlowSignalThread* t = static_cast(thread); + EXPECT_EQ(t->harness(), this); + EXPECT_EQ(main_thread_, Thread::Current()); + + ++thread_completed_; + if (!called_release_) { + thread->Release(); + } + } + + virtual void SetUp() { + main_thread_ = Thread::Current(); + thread_ = new SlowSignalThread(this); + thread_->SignalWorkDone.connect(this, &SignalThreadTest::OnWorkComplete); + called_release_ = false; + thread_started_ = 0; + thread_done_ = 0; + thread_completed_ = 0; + thread_stopped_ = 0; + thread_deleted_ = 0; + } + + virtual void TearDown() { + } + + Thread* main_thread_; + SlowSignalThread* thread_; + bool called_release_; + + int thread_started_; + int thread_done_; + int thread_completed_; + int thread_stopped_; + int thread_deleted_; +}; + +class OwnerThread : public Thread, public sigslot::has_slots<> { + public: + explicit OwnerThread(SignalThreadTest* harness) + : harness_(harness), + has_run_(false) { + } + + virtual void Run() { + SignalThreadTest::SlowSignalThread* signal_thread = + new SignalThreadTest::SlowSignalThread(harness_); + signal_thread->SignalWorkDone.connect(this, &OwnerThread::OnWorkDone); + signal_thread->Start(); + Thread::Current()->socketserver()->Wait(100, false); + signal_thread->Release(); + has_run_ = true; + } + + bool has_run() { return has_run_; } + void OnWorkDone(SignalThread* signal_thread) { + FAIL() << " This shouldn't get called."; + } + + private: + SignalThreadTest* harness_; + bool has_run_; + DISALLOW_EVIL_CONSTRUCTORS(OwnerThread); +}; + +// Test for when the main thread goes away while the +// signal thread is still working. This may happen +// when shutting down the process. +TEST_F(SignalThreadTest, OwnerThreadGoesAway) { + { + scoped_ptr owner(new OwnerThread(this)); + main_thread_ = owner.get(); + owner->Start(); + while (!owner->has_run()) { + Thread::Current()->socketserver()->Wait(10, false); + } + } + // At this point the main thread has gone away. + // Give the SignalThread a little time to do its callback, + // which will crash if the signal thread doesn't handle + // this situation well. + Thread::Current()->socketserver()->Wait(500, false); +} + +#define EXPECT_STATE(started, done, completed, stopped, deleted) \ + EXPECT_EQ(started, thread_started_); \ + EXPECT_EQ(done, thread_done_); \ + EXPECT_EQ(completed, thread_completed_); \ + EXPECT_EQ(stopped, thread_stopped_); \ + EXPECT_EQ(deleted, thread_deleted_); + +TEST_F(SignalThreadTest, ThreadFinishes) { + thread_->Start(); + EXPECT_STATE(1, 0, 0, 0, 0); + Thread::SleepMs(500); + EXPECT_STATE(1, 0, 0, 0, 0); + Thread::Current()->ProcessMessages(0); + EXPECT_STATE(1, 1, 1, 0, 1); +} + +TEST_F(SignalThreadTest, ReleasedThreadFinishes) { + thread_->Start(); + EXPECT_STATE(1, 0, 0, 0, 0); + thread_->Release(); + called_release_ = true; + EXPECT_STATE(1, 0, 0, 0, 0); + Thread::SleepMs(500); + EXPECT_STATE(1, 0, 0, 0, 0); + Thread::Current()->ProcessMessages(0); + EXPECT_STATE(1, 1, 1, 0, 1); +} + +TEST_F(SignalThreadTest, DestroyedThreadCleansUp) { + thread_->Start(); + EXPECT_STATE(1, 0, 0, 0, 0); + thread_->Destroy(true); + EXPECT_STATE(1, 0, 0, 1, 1); + Thread::Current()->ProcessMessages(0); + EXPECT_STATE(1, 0, 0, 1, 1); +} + +TEST_F(SignalThreadTest, DeferredDestroyedThreadCleansUp) { + thread_->Start(); + EXPECT_STATE(1, 0, 0, 0, 0); + thread_->Destroy(false); + EXPECT_STATE(1, 0, 0, 1, 0); + Thread::SleepMs(500); + EXPECT_STATE(1, 0, 0, 1, 0); + Thread::Current()->ProcessMessages(0); + EXPECT_STATE(1, 1, 0, 1, 1); +} diff --git a/talk/base/sigslot.h b/talk/base/sigslot.h new file mode 100644 index 000000000..192aaa742 --- /dev/null +++ b/talk/base/sigslot.h @@ -0,0 +1,2850 @@ +// sigslot.h: Signal/Slot classes +// +// Written by Sarah Thompson (sarah@telergy.com) 2002. +// +// License: Public domain. You are free to use this code however you like, with the proviso that +// the author takes on no responsibility or liability for any use. +// +// QUICK DOCUMENTATION +// +// (see also the full documentation at http://sigslot.sourceforge.net/) +// +// #define switches +// SIGSLOT_PURE_ISO - Define this to force ISO C++ compliance. This also disables +// all of the thread safety support on platforms where it is +// available. +// +// SIGSLOT_USE_POSIX_THREADS - Force use of Posix threads when using a C++ compiler other than +// gcc on a platform that supports Posix threads. (When using gcc, +// this is the default - use SIGSLOT_PURE_ISO to disable this if +// necessary) +// +// SIGSLOT_DEFAULT_MT_POLICY - Where thread support is enabled, this defaults to multi_threaded_global. +// Otherwise, the default is single_threaded. #define this yourself to +// override the default. In pure ISO mode, anything other than +// single_threaded will cause a compiler error. +// +// PLATFORM NOTES +// +// Win32 - On Win32, the WIN32 symbol must be #defined. Most mainstream +// compilers do this by default, but you may need to define it +// yourself if your build environment is less standard. This causes +// the Win32 thread support to be compiled in and used automatically. +// +// Unix/Linux/BSD, etc. - If you're using gcc, it is assumed that you have Posix threads +// available, so they are used automatically. You can override this +// (as under Windows) with the SIGSLOT_PURE_ISO switch. If you're using +// something other than gcc but still want to use Posix threads, you +// need to #define SIGSLOT_USE_POSIX_THREADS. +// +// ISO C++ - If none of the supported platforms are detected, or if +// SIGSLOT_PURE_ISO is defined, all multithreading support is turned off, +// along with any code that might cause a pure ISO C++ environment to +// complain. Before you ask, gcc -ansi -pedantic won't compile this +// library, but gcc -ansi is fine. Pedantic mode seems to throw a lot of +// errors that aren't really there. If you feel like investigating this, +// please contact the author. +// +// +// THREADING MODES +// +// single_threaded - Your program is assumed to be single threaded from the point of view +// of signal/slot usage (i.e. all objects using signals and slots are +// created and destroyed from a single thread). Behaviour if objects are +// destroyed concurrently is undefined (i.e. you'll get the occasional +// segmentation fault/memory exception). +// +// multi_threaded_global - Your program is assumed to be multi threaded. Objects using signals and +// slots can be safely created and destroyed from any thread, even when +// connections exist. In multi_threaded_global mode, this is achieved by a +// single global mutex (actually a critical section on Windows because they +// are faster). This option uses less OS resources, but results in more +// opportunities for contention, possibly resulting in more context switches +// than are strictly necessary. +// +// multi_threaded_local - Behaviour in this mode is essentially the same as multi_threaded_global, +// except that each signal, and each object that inherits has_slots, all +// have their own mutex/critical section. In practice, this means that +// mutex collisions (and hence context switches) only happen if they are +// absolutely essential. However, on some platforms, creating a lot of +// mutexes can slow down the whole OS, so use this option with care. +// +// USING THE LIBRARY +// +// See the full documentation at http://sigslot.sourceforge.net/ +// +// +// Libjingle specific: +// This file has been modified such that has_slots and signalx do not have to be +// using the same threading requirements. E.g. it is possible to connect a +// has_slots and signal0 or +// has_slots and signal0. +// If has_slots is single threaded the user must ensure that it is not trying +// to connect or disconnect to signalx concurrently or data race may occur. +// If signalx is single threaded the user must ensure that disconnect, connect +// or signal is not happening concurrently or data race may occur. + +#ifndef TALK_BASE_SIGSLOT_H__ +#define TALK_BASE_SIGSLOT_H__ + +#include +#include +#include + +// On our copy of sigslot.h, we set single threading as default. +#define SIGSLOT_DEFAULT_MT_POLICY single_threaded + +#if defined(SIGSLOT_PURE_ISO) || (!defined(WIN32) && !defined(__GNUG__) && !defined(SIGSLOT_USE_POSIX_THREADS)) +# define _SIGSLOT_SINGLE_THREADED +#elif defined(WIN32) +# define _SIGSLOT_HAS_WIN32_THREADS +# if !defined(WIN32_LEAN_AND_MEAN) +# define WIN32_LEAN_AND_MEAN +# endif +# include "talk/base/win32.h" +#elif defined(__GNUG__) || defined(SIGSLOT_USE_POSIX_THREADS) +# define _SIGSLOT_HAS_POSIX_THREADS +# include +#else +# define _SIGSLOT_SINGLE_THREADED +#endif + +#ifndef SIGSLOT_DEFAULT_MT_POLICY +# ifdef _SIGSLOT_SINGLE_THREADED +# define SIGSLOT_DEFAULT_MT_POLICY single_threaded +# else +# define SIGSLOT_DEFAULT_MT_POLICY multi_threaded_local +# endif +#endif + +// TODO: change this namespace to talk_base? +namespace sigslot { + + class single_threaded + { + public: + single_threaded() + { + ; + } + + virtual ~single_threaded() + { + ; + } + + virtual void lock() + { + ; + } + + virtual void unlock() + { + ; + } + }; + +#ifdef _SIGSLOT_HAS_WIN32_THREADS + // The multi threading policies only get compiled in if they are enabled. + class multi_threaded_global + { + public: + multi_threaded_global() + { + static bool isinitialised = false; + + if(!isinitialised) + { + InitializeCriticalSection(get_critsec()); + isinitialised = true; + } + } + + multi_threaded_global(const multi_threaded_global&) + { + ; + } + + virtual ~multi_threaded_global() + { + ; + } + + virtual void lock() + { + EnterCriticalSection(get_critsec()); + } + + virtual void unlock() + { + LeaveCriticalSection(get_critsec()); + } + + private: + CRITICAL_SECTION* get_critsec() + { + static CRITICAL_SECTION g_critsec; + return &g_critsec; + } + }; + + class multi_threaded_local + { + public: + multi_threaded_local() + { + InitializeCriticalSection(&m_critsec); + } + + multi_threaded_local(const multi_threaded_local&) + { + InitializeCriticalSection(&m_critsec); + } + + virtual ~multi_threaded_local() + { + DeleteCriticalSection(&m_critsec); + } + + virtual void lock() + { + EnterCriticalSection(&m_critsec); + } + + virtual void unlock() + { + LeaveCriticalSection(&m_critsec); + } + + private: + CRITICAL_SECTION m_critsec; + }; +#endif // _SIGSLOT_HAS_WIN32_THREADS + +#ifdef _SIGSLOT_HAS_POSIX_THREADS + // The multi threading policies only get compiled in if they are enabled. + class multi_threaded_global + { + public: + multi_threaded_global() + { + pthread_mutex_init(get_mutex(), NULL); + } + + multi_threaded_global(const multi_threaded_global&) + { + ; + } + + virtual ~multi_threaded_global() + { + ; + } + + virtual void lock() + { + pthread_mutex_lock(get_mutex()); + } + + virtual void unlock() + { + pthread_mutex_unlock(get_mutex()); + } + + private: + pthread_mutex_t* get_mutex() + { + static pthread_mutex_t g_mutex; + return &g_mutex; + } + }; + + class multi_threaded_local + { + public: + multi_threaded_local() + { + pthread_mutex_init(&m_mutex, NULL); + } + + multi_threaded_local(const multi_threaded_local&) + { + pthread_mutex_init(&m_mutex, NULL); + } + + virtual ~multi_threaded_local() + { + pthread_mutex_destroy(&m_mutex); + } + + virtual void lock() + { + pthread_mutex_lock(&m_mutex); + } + + virtual void unlock() + { + pthread_mutex_unlock(&m_mutex); + } + + private: + pthread_mutex_t m_mutex; + }; +#endif // _SIGSLOT_HAS_POSIX_THREADS + + template + class lock_block + { + public: + mt_policy *m_mutex; + + lock_block(mt_policy *mtx) + : m_mutex(mtx) + { + m_mutex->lock(); + } + + ~lock_block() + { + m_mutex->unlock(); + } + }; + + class has_slots_interface; + + template + class _connection_base0 + { + public: + virtual ~_connection_base0() {} + virtual has_slots_interface* getdest() const = 0; + virtual void emit() = 0; + virtual _connection_base0* clone() = 0; + virtual _connection_base0* duplicate(has_slots_interface* pnewdest) = 0; + }; + + template + class _connection_base1 + { + public: + virtual ~_connection_base1() {} + virtual has_slots_interface* getdest() const = 0; + virtual void emit(arg1_type) = 0; + virtual _connection_base1* clone() = 0; + virtual _connection_base1* duplicate(has_slots_interface* pnewdest) = 0; + }; + + template + class _connection_base2 + { + public: + virtual ~_connection_base2() {} + virtual has_slots_interface* getdest() const = 0; + virtual void emit(arg1_type, arg2_type) = 0; + virtual _connection_base2* clone() = 0; + virtual _connection_base2* duplicate(has_slots_interface* pnewdest) = 0; + }; + + template + class _connection_base3 + { + public: + virtual ~_connection_base3() {} + virtual has_slots_interface* getdest() const = 0; + virtual void emit(arg1_type, arg2_type, arg3_type) = 0; + virtual _connection_base3* clone() = 0; + virtual _connection_base3* duplicate(has_slots_interface* pnewdest) = 0; + }; + + template + class _connection_base4 + { + public: + virtual ~_connection_base4() {} + virtual has_slots_interface* getdest() const = 0; + virtual void emit(arg1_type, arg2_type, arg3_type, arg4_type) = 0; + virtual _connection_base4* clone() = 0; + virtual _connection_base4* duplicate(has_slots_interface* pnewdest) = 0; + }; + + template + class _connection_base5 + { + public: + virtual ~_connection_base5() {} + virtual has_slots_interface* getdest() const = 0; + virtual void emit(arg1_type, arg2_type, arg3_type, arg4_type, + arg5_type) = 0; + virtual _connection_base5* clone() = 0; + virtual _connection_base5* duplicate(has_slots_interface* pnewdest) = 0; + }; + + template + class _connection_base6 + { + public: + virtual ~_connection_base6() {} + virtual has_slots_interface* getdest() const = 0; + virtual void emit(arg1_type, arg2_type, arg3_type, arg4_type, arg5_type, + arg6_type) = 0; + virtual _connection_base6* clone() = 0; + virtual _connection_base6* duplicate(has_slots_interface* pnewdest) = 0; + }; + + template + class _connection_base7 + { + public: + virtual ~_connection_base7() {} + virtual has_slots_interface* getdest() const = 0; + virtual void emit(arg1_type, arg2_type, arg3_type, arg4_type, arg5_type, + arg6_type, arg7_type) = 0; + virtual _connection_base7* clone() = 0; + virtual _connection_base7* duplicate(has_slots_interface* pnewdest) = 0; + }; + + template + class _connection_base8 + { + public: + virtual ~_connection_base8() {} + virtual has_slots_interface* getdest() const = 0; + virtual void emit(arg1_type, arg2_type, arg3_type, arg4_type, arg5_type, + arg6_type, arg7_type, arg8_type) = 0; + virtual _connection_base8* clone() = 0; + virtual _connection_base8* duplicate(has_slots_interface* pnewdest) = 0; + }; + + class _signal_base_interface + { + public: + virtual void slot_disconnect(has_slots_interface* pslot) = 0; + virtual void slot_duplicate(const has_slots_interface* poldslot, has_slots_interface* pnewslot) = 0; + }; + + template + class _signal_base : public _signal_base_interface, public mt_policy + { + }; + + class has_slots_interface + { + public: + has_slots_interface() + { + ; + } + + virtual void signal_connect(_signal_base_interface* sender) = 0; + + virtual void signal_disconnect(_signal_base_interface* sender) = 0; + + virtual ~has_slots_interface() + { + } + + virtual void disconnect_all() = 0; + }; + + template + class has_slots : public has_slots_interface, public mt_policy + { + private: + typedef std::set<_signal_base_interface*> sender_set; + typedef sender_set::const_iterator const_iterator; + + public: + has_slots() + { + ; + } + + has_slots(const has_slots& hs) + { + lock_block lock(this); + const_iterator it = hs.m_senders.begin(); + const_iterator itEnd = hs.m_senders.end(); + + while(it != itEnd) + { + (*it)->slot_duplicate(&hs, this); + m_senders.insert(*it); + ++it; + } + } + + void signal_connect(_signal_base_interface* sender) + { + lock_block lock(this); + m_senders.insert(sender); + } + + void signal_disconnect(_signal_base_interface* sender) + { + lock_block lock(this); + m_senders.erase(sender); + } + + virtual ~has_slots() + { + disconnect_all(); + } + + void disconnect_all() + { + lock_block lock(this); + const_iterator it = m_senders.begin(); + const_iterator itEnd = m_senders.end(); + + while(it != itEnd) + { + (*it)->slot_disconnect(this); + ++it; + } + + m_senders.erase(m_senders.begin(), m_senders.end()); + } + + private: + sender_set m_senders; + }; + + template + class _signal_base0 : public _signal_base + { + public: + typedef std::list<_connection_base0 *> connections_list; + + _signal_base0() + { + ; + } + + _signal_base0(const _signal_base0& s) + : _signal_base(s) + { + lock_block lock(this); + typename connections_list::const_iterator it = s.m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = s.m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_connect(this); + m_connected_slots.push_back((*it)->clone()); + + ++it; + } + } + + ~_signal_base0() + { + disconnect_all(); + } + + bool is_empty() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + return it == itEnd; + } + + void disconnect_all() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_disconnect(this); + delete *it; + + ++it; + } + + m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end()); + } + +#ifdef _DEBUG + bool connected(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + while(it != itEnd) + { + itNext = it; + ++itNext; + if ((*it)->getdest() == pclass) + return true; + it = itNext; + } + return false; + } +#endif + + void disconnect(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == pclass) + { + delete *it; + m_connected_slots.erase(it); + pclass->signal_disconnect(this); + return; + } + + ++it; + } + } + + void slot_disconnect(has_slots_interface* pslot) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + typename connections_list::iterator itNext = it; + ++itNext; + + if((*it)->getdest() == pslot) + { + delete *it; + m_connected_slots.erase(it); + } + + it = itNext; + } + } + + void slot_duplicate(const has_slots_interface* oldtarget, has_slots_interface* newtarget) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == oldtarget) + { + m_connected_slots.push_back((*it)->duplicate(newtarget)); + } + + ++it; + } + } + + protected: + connections_list m_connected_slots; + }; + + template + class _signal_base1 : public _signal_base + { + public: + typedef std::list<_connection_base1 *> connections_list; + + _signal_base1() + { + ; + } + + _signal_base1(const _signal_base1& s) + : _signal_base(s) + { + lock_block lock(this); + typename connections_list::const_iterator it = s.m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = s.m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_connect(this); + m_connected_slots.push_back((*it)->clone()); + + ++it; + } + } + + void slot_duplicate(const has_slots_interface* oldtarget, has_slots_interface* newtarget) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == oldtarget) + { + m_connected_slots.push_back((*it)->duplicate(newtarget)); + } + + ++it; + } + } + + ~_signal_base1() + { + disconnect_all(); + } + + bool is_empty() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + return it == itEnd; + } + + void disconnect_all() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_disconnect(this); + delete *it; + + ++it; + } + + m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end()); + } + +#ifdef _DEBUG + bool connected(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + while(it != itEnd) + { + itNext = it; + ++itNext; + if ((*it)->getdest() == pclass) + return true; + it = itNext; + } + return false; + } +#endif + + void disconnect(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == pclass) + { + delete *it; + m_connected_slots.erase(it); + pclass->signal_disconnect(this); + return; + } + + ++it; + } + } + + void slot_disconnect(has_slots_interface* pslot) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + typename connections_list::iterator itNext = it; + ++itNext; + + if((*it)->getdest() == pslot) + { + delete *it; + m_connected_slots.erase(it); + } + + it = itNext; + } + } + + + protected: + connections_list m_connected_slots; + }; + + template + class _signal_base2 : public _signal_base + { + public: + typedef std::list<_connection_base2 *> + connections_list; + + _signal_base2() + { + ; + } + + _signal_base2(const _signal_base2& s) + : _signal_base(s) + { + lock_block lock(this); + typename connections_list::const_iterator it = s.m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = s.m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_connect(this); + m_connected_slots.push_back((*it)->clone()); + + ++it; + } + } + + void slot_duplicate(const has_slots_interface* oldtarget, has_slots_interface* newtarget) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == oldtarget) + { + m_connected_slots.push_back((*it)->duplicate(newtarget)); + } + + ++it; + } + } + + ~_signal_base2() + { + disconnect_all(); + } + + bool is_empty() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + return it == itEnd; + } + + void disconnect_all() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_disconnect(this); + delete *it; + + ++it; + } + + m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end()); + } + +#ifdef _DEBUG + bool connected(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + while(it != itEnd) + { + itNext = it; + ++itNext; + if ((*it)->getdest() == pclass) + return true; + it = itNext; + } + return false; + } +#endif + + void disconnect(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == pclass) + { + delete *it; + m_connected_slots.erase(it); + pclass->signal_disconnect(this); + return; + } + + ++it; + } + } + + void slot_disconnect(has_slots_interface* pslot) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + typename connections_list::iterator itNext = it; + ++itNext; + + if((*it)->getdest() == pslot) + { + delete *it; + m_connected_slots.erase(it); + } + + it = itNext; + } + } + + protected: + connections_list m_connected_slots; + }; + + template + class _signal_base3 : public _signal_base + { + public: + typedef std::list<_connection_base3 *> + connections_list; + + _signal_base3() + { + ; + } + + _signal_base3(const _signal_base3& s) + : _signal_base(s) + { + lock_block lock(this); + typename connections_list::const_iterator it = s.m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = s.m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_connect(this); + m_connected_slots.push_back((*it)->clone()); + + ++it; + } + } + + void slot_duplicate(const has_slots_interface* oldtarget, has_slots_interface* newtarget) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == oldtarget) + { + m_connected_slots.push_back((*it)->duplicate(newtarget)); + } + + ++it; + } + } + + ~_signal_base3() + { + disconnect_all(); + } + + bool is_empty() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + return it == itEnd; + } + + void disconnect_all() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_disconnect(this); + delete *it; + + ++it; + } + + m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end()); + } + +#ifdef _DEBUG + bool connected(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + while(it != itEnd) + { + itNext = it; + ++itNext; + if ((*it)->getdest() == pclass) + return true; + it = itNext; + } + return false; + } +#endif + + void disconnect(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == pclass) + { + delete *it; + m_connected_slots.erase(it); + pclass->signal_disconnect(this); + return; + } + + ++it; + } + } + + void slot_disconnect(has_slots_interface* pslot) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + typename connections_list::iterator itNext = it; + ++itNext; + + if((*it)->getdest() == pslot) + { + delete *it; + m_connected_slots.erase(it); + } + + it = itNext; + } + } + + protected: + connections_list m_connected_slots; + }; + + template + class _signal_base4 : public _signal_base + { + public: + typedef std::list<_connection_base4 *> connections_list; + + _signal_base4() + { + ; + } + + _signal_base4(const _signal_base4& s) + : _signal_base(s) + { + lock_block lock(this); + typename connections_list::const_iterator it = s.m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = s.m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_connect(this); + m_connected_slots.push_back((*it)->clone()); + + ++it; + } + } + + void slot_duplicate(const has_slots_interface* oldtarget, has_slots_interface* newtarget) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == oldtarget) + { + m_connected_slots.push_back((*it)->duplicate(newtarget)); + } + + ++it; + } + } + + ~_signal_base4() + { + disconnect_all(); + } + + bool is_empty() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + return it == itEnd; + } + + void disconnect_all() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_disconnect(this); + delete *it; + + ++it; + } + + m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end()); + } + +#ifdef _DEBUG + bool connected(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + while(it != itEnd) + { + itNext = it; + ++itNext; + if ((*it)->getdest() == pclass) + return true; + it = itNext; + } + return false; + } +#endif + + void disconnect(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == pclass) + { + delete *it; + m_connected_slots.erase(it); + pclass->signal_disconnect(this); + return; + } + + ++it; + } + } + + void slot_disconnect(has_slots_interface* pslot) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + typename connections_list::iterator itNext = it; + ++itNext; + + if((*it)->getdest() == pslot) + { + delete *it; + m_connected_slots.erase(it); + } + + it = itNext; + } + } + + protected: + connections_list m_connected_slots; + }; + + template + class _signal_base5 : public _signal_base + { + public: + typedef std::list<_connection_base5 *> connections_list; + + _signal_base5() + { + ; + } + + _signal_base5(const _signal_base5& s) + : _signal_base(s) + { + lock_block lock(this); + typename connections_list::const_iterator it = s.m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = s.m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_connect(this); + m_connected_slots.push_back((*it)->clone()); + + ++it; + } + } + + void slot_duplicate(const has_slots_interface* oldtarget, has_slots_interface* newtarget) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == oldtarget) + { + m_connected_slots.push_back((*it)->duplicate(newtarget)); + } + + ++it; + } + } + + ~_signal_base5() + { + disconnect_all(); + } + + bool is_empty() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + return it == itEnd; + } + + void disconnect_all() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_disconnect(this); + delete *it; + + ++it; + } + + m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end()); + } + +#ifdef _DEBUG + bool connected(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + while(it != itEnd) + { + itNext = it; + ++itNext; + if ((*it)->getdest() == pclass) + return true; + it = itNext; + } + return false; + } +#endif + + void disconnect(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == pclass) + { + delete *it; + m_connected_slots.erase(it); + pclass->signal_disconnect(this); + return; + } + + ++it; + } + } + + void slot_disconnect(has_slots_interface* pslot) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + typename connections_list::iterator itNext = it; + ++itNext; + + if((*it)->getdest() == pslot) + { + delete *it; + m_connected_slots.erase(it); + } + + it = itNext; + } + } + + protected: + connections_list m_connected_slots; + }; + + template + class _signal_base6 : public _signal_base + { + public: + typedef std::list<_connection_base6 *> connections_list; + + _signal_base6() + { + ; + } + + _signal_base6(const _signal_base6& s) + : _signal_base(s) + { + lock_block lock(this); + typename connections_list::const_iterator it = s.m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = s.m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_connect(this); + m_connected_slots.push_back((*it)->clone()); + + ++it; + } + } + + void slot_duplicate(const has_slots_interface* oldtarget, has_slots_interface* newtarget) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == oldtarget) + { + m_connected_slots.push_back((*it)->duplicate(newtarget)); + } + + ++it; + } + } + + ~_signal_base6() + { + disconnect_all(); + } + + bool is_empty() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + return it == itEnd; + } + + void disconnect_all() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_disconnect(this); + delete *it; + + ++it; + } + + m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end()); + } + +#ifdef _DEBUG + bool connected(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + while(it != itEnd) + { + itNext = it; + ++itNext; + if ((*it)->getdest() == pclass) + return true; + it = itNext; + } + return false; + } +#endif + + void disconnect(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == pclass) + { + delete *it; + m_connected_slots.erase(it); + pclass->signal_disconnect(this); + return; + } + + ++it; + } + } + + void slot_disconnect(has_slots_interface* pslot) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + typename connections_list::iterator itNext = it; + ++itNext; + + if((*it)->getdest() == pslot) + { + delete *it; + m_connected_slots.erase(it); + } + + it = itNext; + } + } + + protected: + connections_list m_connected_slots; + }; + + template + class _signal_base7 : public _signal_base + { + public: + typedef std::list<_connection_base7 *> connections_list; + + _signal_base7() + { + ; + } + + _signal_base7(const _signal_base7& s) + : _signal_base(s) + { + lock_block lock(this); + typename connections_list::const_iterator it = s.m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = s.m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_connect(this); + m_connected_slots.push_back((*it)->clone()); + + ++it; + } + } + + void slot_duplicate(const has_slots_interface* oldtarget, has_slots_interface* newtarget) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == oldtarget) + { + m_connected_slots.push_back((*it)->duplicate(newtarget)); + } + + ++it; + } + } + + ~_signal_base7() + { + disconnect_all(); + } + + bool is_empty() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + return it == itEnd; + } + + void disconnect_all() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_disconnect(this); + delete *it; + + ++it; + } + + m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end()); + } + +#ifdef _DEBUG + bool connected(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + while(it != itEnd) + { + itNext = it; + ++itNext; + if ((*it)->getdest() == pclass) + return true; + it = itNext; + } + return false; + } +#endif + + void disconnect(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == pclass) + { + delete *it; + m_connected_slots.erase(it); + pclass->signal_disconnect(this); + return; + } + + ++it; + } + } + + void slot_disconnect(has_slots_interface* pslot) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + typename connections_list::iterator itNext = it; + ++itNext; + + if((*it)->getdest() == pslot) + { + delete *it; + m_connected_slots.erase(it); + } + + it = itNext; + } + } + + protected: + connections_list m_connected_slots; + }; + + template + class _signal_base8 : public _signal_base + { + public: + typedef std::list<_connection_base8 *> + connections_list; + + _signal_base8() + { + ; + } + + _signal_base8(const _signal_base8& s) + : _signal_base(s) + { + lock_block lock(this); + typename connections_list::const_iterator it = s.m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = s.m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_connect(this); + m_connected_slots.push_back((*it)->clone()); + + ++it; + } + } + + void slot_duplicate(const has_slots_interface* oldtarget, has_slots_interface* newtarget) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == oldtarget) + { + m_connected_slots.push_back((*it)->duplicate(newtarget)); + } + + ++it; + } + } + + ~_signal_base8() + { + disconnect_all(); + } + + bool is_empty() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + return it == itEnd; + } + + void disconnect_all() + { + lock_block lock(this); + typename connections_list::const_iterator it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + (*it)->getdest()->signal_disconnect(this); + delete *it; + + ++it; + } + + m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end()); + } + +#ifdef _DEBUG + bool connected(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + while(it != itEnd) + { + itNext = it; + ++itNext; + if ((*it)->getdest() == pclass) + return true; + it = itNext; + } + return false; + } +#endif + + void disconnect(has_slots_interface* pclass) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + if((*it)->getdest() == pclass) + { + delete *it; + m_connected_slots.erase(it); + pclass->signal_disconnect(this); + return; + } + + ++it; + } + } + + void slot_disconnect(has_slots_interface* pslot) + { + lock_block lock(this); + typename connections_list::iterator it = m_connected_slots.begin(); + typename connections_list::iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + typename connections_list::iterator itNext = it; + ++itNext; + + if((*it)->getdest() == pslot) + { + delete *it; + m_connected_slots.erase(it); + } + + it = itNext; + } + } + + protected: + connections_list m_connected_slots; + }; + + + template + class _connection0 : public _connection_base0 + { + public: + _connection0() + { + m_pobject = NULL; + m_pmemfun = NULL; + } + + _connection0(dest_type* pobject, void (dest_type::*pmemfun)()) + { + m_pobject = pobject; + m_pmemfun = pmemfun; + } + + virtual ~_connection0() + { + } + + virtual _connection_base0* clone() + { + return new _connection0(*this); + } + + virtual _connection_base0* duplicate(has_slots_interface* pnewdest) + { + return new _connection0((dest_type *)pnewdest, m_pmemfun); + } + + virtual void emit() + { + (m_pobject->*m_pmemfun)(); + } + + virtual has_slots_interface* getdest() const + { + return m_pobject; + } + + private: + dest_type* m_pobject; + void (dest_type::* m_pmemfun)(); + }; + + template + class _connection1 : public _connection_base1 + { + public: + _connection1() + { + m_pobject = NULL; + m_pmemfun = NULL; + } + + _connection1(dest_type* pobject, void (dest_type::*pmemfun)(arg1_type)) + { + m_pobject = pobject; + m_pmemfun = pmemfun; + } + + virtual ~_connection1() + { + } + + virtual _connection_base1* clone() + { + return new _connection1(*this); + } + + virtual _connection_base1* duplicate(has_slots_interface* pnewdest) + { + return new _connection1((dest_type *)pnewdest, m_pmemfun); + } + + virtual void emit(arg1_type a1) + { + (m_pobject->*m_pmemfun)(a1); + } + + virtual has_slots_interface* getdest() const + { + return m_pobject; + } + + private: + dest_type* m_pobject; + void (dest_type::* m_pmemfun)(arg1_type); + }; + + template + class _connection2 : public _connection_base2 + { + public: + _connection2() + { + m_pobject = NULL; + m_pmemfun = NULL; + } + + _connection2(dest_type* pobject, void (dest_type::*pmemfun)(arg1_type, + arg2_type)) + { + m_pobject = pobject; + m_pmemfun = pmemfun; + } + + virtual ~_connection2() + { + } + + virtual _connection_base2* clone() + { + return new _connection2(*this); + } + + virtual _connection_base2* duplicate(has_slots_interface* pnewdest) + { + return new _connection2((dest_type *)pnewdest, m_pmemfun); + } + + virtual void emit(arg1_type a1, arg2_type a2) + { + (m_pobject->*m_pmemfun)(a1, a2); + } + + virtual has_slots_interface* getdest() const + { + return m_pobject; + } + + private: + dest_type* m_pobject; + void (dest_type::* m_pmemfun)(arg1_type, arg2_type); + }; + + template + class _connection3 : public _connection_base3 + { + public: + _connection3() + { + m_pobject = NULL; + m_pmemfun = NULL; + } + + _connection3(dest_type* pobject, void (dest_type::*pmemfun)(arg1_type, + arg2_type, arg3_type)) + { + m_pobject = pobject; + m_pmemfun = pmemfun; + } + + virtual ~_connection3() + { + } + + virtual _connection_base3* clone() + { + return new _connection3(*this); + } + + virtual _connection_base3* duplicate(has_slots_interface* pnewdest) + { + return new _connection3((dest_type *)pnewdest, m_pmemfun); + } + + virtual void emit(arg1_type a1, arg2_type a2, arg3_type a3) + { + (m_pobject->*m_pmemfun)(a1, a2, a3); + } + + virtual has_slots_interface* getdest() const + { + return m_pobject; + } + + private: + dest_type* m_pobject; + void (dest_type::* m_pmemfun)(arg1_type, arg2_type, arg3_type); + }; + + template + class _connection4 : public _connection_base4 + { + public: + _connection4() + { + m_pobject = NULL; + m_pmemfun = NULL; + } + + _connection4(dest_type* pobject, void (dest_type::*pmemfun)(arg1_type, + arg2_type, arg3_type, arg4_type)) + { + m_pobject = pobject; + m_pmemfun = pmemfun; + } + + virtual ~_connection4() + { + } + + virtual _connection_base4* clone() + { + return new _connection4(*this); + } + + virtual _connection_base4* duplicate(has_slots_interface* pnewdest) + { + return new _connection4((dest_type *)pnewdest, m_pmemfun); + } + + virtual void emit(arg1_type a1, arg2_type a2, arg3_type a3, + arg4_type a4) + { + (m_pobject->*m_pmemfun)(a1, a2, a3, a4); + } + + virtual has_slots_interface* getdest() const + { + return m_pobject; + } + + private: + dest_type* m_pobject; + void (dest_type::* m_pmemfun)(arg1_type, arg2_type, arg3_type, + arg4_type); + }; + + template + class _connection5 : public _connection_base5 + { + public: + _connection5() + { + m_pobject = NULL; + m_pmemfun = NULL; + } + + _connection5(dest_type* pobject, void (dest_type::*pmemfun)(arg1_type, + arg2_type, arg3_type, arg4_type, arg5_type)) + { + m_pobject = pobject; + m_pmemfun = pmemfun; + } + + virtual ~_connection5() + { + } + + virtual _connection_base5* clone() + { + return new _connection5(*this); + } + + virtual _connection_base5* duplicate(has_slots_interface* pnewdest) + { + return new _connection5((dest_type *)pnewdest, m_pmemfun); + } + + virtual void emit(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4, + arg5_type a5) + { + (m_pobject->*m_pmemfun)(a1, a2, a3, a4, a5); + } + + virtual has_slots_interface* getdest() const + { + return m_pobject; + } + + private: + dest_type* m_pobject; + void (dest_type::* m_pmemfun)(arg1_type, arg2_type, arg3_type, arg4_type, + arg5_type); + }; + + template + class _connection6 : public _connection_base6 + { + public: + _connection6() + { + m_pobject = NULL; + m_pmemfun = NULL; + } + + _connection6(dest_type* pobject, void (dest_type::*pmemfun)(arg1_type, + arg2_type, arg3_type, arg4_type, arg5_type, arg6_type)) + { + m_pobject = pobject; + m_pmemfun = pmemfun; + } + + virtual ~_connection6() + { + } + + virtual _connection_base6* clone() + { + return new _connection6(*this); + } + + virtual _connection_base6* duplicate(has_slots_interface* pnewdest) + { + return new _connection6((dest_type *)pnewdest, m_pmemfun); + } + + virtual void emit(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4, + arg5_type a5, arg6_type a6) + { + (m_pobject->*m_pmemfun)(a1, a2, a3, a4, a5, a6); + } + + virtual has_slots_interface* getdest() const + { + return m_pobject; + } + + private: + dest_type* m_pobject; + void (dest_type::* m_pmemfun)(arg1_type, arg2_type, arg3_type, arg4_type, + arg5_type, arg6_type); + }; + + template + class _connection7 : public _connection_base7 + { + public: + _connection7() + { + m_pobject = NULL; + m_pmemfun = NULL; + } + + _connection7(dest_type* pobject, void (dest_type::*pmemfun)(arg1_type, + arg2_type, arg3_type, arg4_type, arg5_type, arg6_type, arg7_type)) + { + m_pobject = pobject; + m_pmemfun = pmemfun; + } + + virtual ~_connection7() + { + } + + virtual _connection_base7* clone() + { + return new _connection7(*this); + } + + virtual _connection_base7* duplicate(has_slots_interface* pnewdest) + { + return new _connection7((dest_type *)pnewdest, m_pmemfun); + } + + virtual void emit(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4, + arg5_type a5, arg6_type a6, arg7_type a7) + { + (m_pobject->*m_pmemfun)(a1, a2, a3, a4, a5, a6, a7); + } + + virtual has_slots_interface* getdest() const + { + return m_pobject; + } + + private: + dest_type* m_pobject; + void (dest_type::* m_pmemfun)(arg1_type, arg2_type, arg3_type, arg4_type, + arg5_type, arg6_type, arg7_type); + }; + + template + class _connection8 : public _connection_base8 + { + public: + _connection8() + { + m_pobject = NULL; + m_pmemfun = NULL; + } + + _connection8(dest_type* pobject, void (dest_type::*pmemfun)(arg1_type, + arg2_type, arg3_type, arg4_type, arg5_type, arg6_type, + arg7_type, arg8_type)) + { + m_pobject = pobject; + m_pmemfun = pmemfun; + } + + virtual ~_connection8() + { + } + + virtual _connection_base8* clone() + { + return new _connection8(*this); + } + + virtual _connection_base8* duplicate(has_slots_interface* pnewdest) + { + return new _connection8((dest_type *)pnewdest, m_pmemfun); + } + + virtual void emit(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4, + arg5_type a5, arg6_type a6, arg7_type a7, arg8_type a8) + { + (m_pobject->*m_pmemfun)(a1, a2, a3, a4, a5, a6, a7, a8); + } + + virtual has_slots_interface* getdest() const + { + return m_pobject; + } + + private: + dest_type* m_pobject; + void (dest_type::* m_pmemfun)(arg1_type, arg2_type, arg3_type, arg4_type, + arg5_type, arg6_type, arg7_type, arg8_type); + }; + + template + class signal0 : public _signal_base0 + { + public: + typedef _signal_base0 base; + typedef typename base::connections_list connections_list; + using base::m_connected_slots; + + signal0() + { + ; + } + + signal0(const signal0& s) + : _signal_base0(s) + { + ; + } + + template + void connect(desttype* pclass, void (desttype::*pmemfun)()) + { + lock_block lock(this); + _connection0* conn = + new _connection0(pclass, pmemfun); + m_connected_slots.push_back(conn); + pclass->signal_connect(this); + } + + void emit() + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(); + + it = itNext; + } + } + + void operator()() + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(); + + it = itNext; + } + } + }; + + template + class signal1 : public _signal_base1 + { + public: + typedef _signal_base1 base; + typedef typename base::connections_list connections_list; + using base::m_connected_slots; + + signal1() + { + ; + } + + signal1(const signal1& s) + : _signal_base1(s) + { + ; + } + + template + void connect(desttype* pclass, void (desttype::*pmemfun)(arg1_type)) + { + lock_block lock(this); + _connection1* conn = + new _connection1(pclass, pmemfun); + m_connected_slots.push_back(conn); + pclass->signal_connect(this); + } + + void emit(arg1_type a1) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1); + + it = itNext; + } + } + + void operator()(arg1_type a1) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1); + + it = itNext; + } + } + }; + + template + class signal2 : public _signal_base2 + { + public: + typedef _signal_base2 base; + typedef typename base::connections_list connections_list; + using base::m_connected_slots; + + signal2() + { + ; + } + + signal2(const signal2& s) + : _signal_base2(s) + { + ; + } + + template + void connect(desttype* pclass, void (desttype::*pmemfun)(arg1_type, + arg2_type)) + { + lock_block lock(this); + _connection2* conn = new + _connection2(pclass, pmemfun); + m_connected_slots.push_back(conn); + pclass->signal_connect(this); + } + + void emit(arg1_type a1, arg2_type a2) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2); + + it = itNext; + } + } + + void operator()(arg1_type a1, arg2_type a2) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2); + + it = itNext; + } + } + }; + + template + class signal3 : public _signal_base3 + { + public: + typedef _signal_base3 base; + typedef typename base::connections_list connections_list; + using base::m_connected_slots; + + signal3() + { + ; + } + + signal3(const signal3& s) + : _signal_base3(s) + { + ; + } + + template + void connect(desttype* pclass, void (desttype::*pmemfun)(arg1_type, + arg2_type, arg3_type)) + { + lock_block lock(this); + _connection3* conn = + new _connection3(pclass, + pmemfun); + m_connected_slots.push_back(conn); + pclass->signal_connect(this); + } + + void emit(arg1_type a1, arg2_type a2, arg3_type a3) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2, a3); + + it = itNext; + } + } + + void operator()(arg1_type a1, arg2_type a2, arg3_type a3) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2, a3); + + it = itNext; + } + } + }; + + template + class signal4 : public _signal_base4 + { + public: + typedef _signal_base4 base; + typedef typename base::connections_list connections_list; + using base::m_connected_slots; + + signal4() + { + ; + } + + signal4(const signal4& s) + : _signal_base4(s) + { + ; + } + + template + void connect(desttype* pclass, void (desttype::*pmemfun)(arg1_type, + arg2_type, arg3_type, arg4_type)) + { + lock_block lock(this); + _connection4* + conn = new _connection4(pclass, pmemfun); + m_connected_slots.push_back(conn); + pclass->signal_connect(this); + } + + void emit(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2, a3, a4); + + it = itNext; + } + } + + void operator()(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2, a3, a4); + + it = itNext; + } + } + }; + + template + class signal5 : public _signal_base5 + { + public: + typedef _signal_base5 base; + typedef typename base::connections_list connections_list; + using base::m_connected_slots; + + signal5() + { + ; + } + + signal5(const signal5& s) + : _signal_base5(s) + { + ; + } + + template + void connect(desttype* pclass, void (desttype::*pmemfun)(arg1_type, + arg2_type, arg3_type, arg4_type, arg5_type)) + { + lock_block lock(this); + _connection5* conn = new _connection5(pclass, pmemfun); + m_connected_slots.push_back(conn); + pclass->signal_connect(this); + } + + void emit(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4, + arg5_type a5) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2, a3, a4, a5); + + it = itNext; + } + } + + void operator()(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4, + arg5_type a5) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2, a3, a4, a5); + + it = itNext; + } + } + }; + + + template + class signal6 : public _signal_base6 + { + public: + typedef _signal_base6 base; + typedef typename base::connections_list connections_list; + using base::m_connected_slots; + + signal6() + { + ; + } + + signal6(const signal6& s) + : _signal_base6(s) + { + ; + } + + template + void connect(desttype* pclass, void (desttype::*pmemfun)(arg1_type, + arg2_type, arg3_type, arg4_type, arg5_type, arg6_type)) + { + lock_block lock(this); + _connection6* conn = + new _connection6(pclass, pmemfun); + m_connected_slots.push_back(conn); + pclass->signal_connect(this); + } + + void emit(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4, + arg5_type a5, arg6_type a6) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2, a3, a4, a5, a6); + + it = itNext; + } + } + + void operator()(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4, + arg5_type a5, arg6_type a6) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2, a3, a4, a5, a6); + + it = itNext; + } + } + }; + + template + class signal7 : public _signal_base7 + { + public: + typedef _signal_base7 base; + typedef typename base::connections_list connections_list; + using base::m_connected_slots; + + signal7() + { + ; + } + + signal7(const signal7& s) + : _signal_base7(s) + { + ; + } + + template + void connect(desttype* pclass, void (desttype::*pmemfun)(arg1_type, + arg2_type, arg3_type, arg4_type, arg5_type, arg6_type, + arg7_type)) + { + lock_block lock(this); + _connection7* conn = + new _connection7(pclass, pmemfun); + m_connected_slots.push_back(conn); + pclass->signal_connect(this); + } + + void emit(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4, + arg5_type a5, arg6_type a6, arg7_type a7) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2, a3, a4, a5, a6, a7); + + it = itNext; + } + } + + void operator()(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4, + arg5_type a5, arg6_type a6, arg7_type a7) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2, a3, a4, a5, a6, a7); + + it = itNext; + } + } + }; + + template + class signal8 : public _signal_base8 + { + public: + typedef _signal_base8 base; + typedef typename base::connections_list connections_list; + using base::m_connected_slots; + + signal8() + { + ; + } + + signal8(const signal8& s) + : _signal_base8(s) + { + ; + } + + template + void connect(desttype* pclass, void (desttype::*pmemfun)(arg1_type, + arg2_type, arg3_type, arg4_type, arg5_type, arg6_type, + arg7_type, arg8_type)) + { + lock_block lock(this); + _connection8* conn = + new _connection8(pclass, pmemfun); + m_connected_slots.push_back(conn); + pclass->signal_connect(this); + } + + void emit(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4, + arg5_type a5, arg6_type a6, arg7_type a7, arg8_type a8) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2, a3, a4, a5, a6, a7, a8); + + it = itNext; + } + } + + void operator()(arg1_type a1, arg2_type a2, arg3_type a3, arg4_type a4, + arg5_type a5, arg6_type a6, arg7_type a7, arg8_type a8) + { + lock_block lock(this); + typename connections_list::const_iterator itNext, it = m_connected_slots.begin(); + typename connections_list::const_iterator itEnd = m_connected_slots.end(); + + while(it != itEnd) + { + itNext = it; + ++itNext; + + (*it)->emit(a1, a2, a3, a4, a5, a6, a7, a8); + + it = itNext; + } + } + }; + +}; // namespace sigslot + +#endif // TALK_BASE_SIGSLOT_H__ diff --git a/talk/base/sigslot_unittest.cc b/talk/base/sigslot_unittest.cc new file mode 100644 index 000000000..62b03c24d --- /dev/null +++ b/talk/base/sigslot_unittest.cc @@ -0,0 +1,267 @@ +/* + * libjingle + * Copyright 2012, 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/base/sigslot.h" + +#include "talk/base/gunit.h" + +// This function, when passed a has_slots or signalx, will break the build if +// its threading requirement is not single threaded +static bool TemplateIsST(const sigslot::single_threaded* p) { + return true; +} +// This function, when passed a has_slots or signalx, will break the build if +// its threading requirement is not multi threaded +static bool TemplateIsMT(const sigslot::multi_threaded_local* p) { + return true; +} + +class SigslotDefault : public testing::Test, public sigslot::has_slots<> { + protected: + sigslot::signal0<> signal_; +}; + +template +class SigslotReceiver : public sigslot::has_slots { + public: + SigslotReceiver() : signal_(NULL), signal_count_(0) { + } + ~SigslotReceiver() { + } + + void Connect(sigslot::signal0* signal) { + if (!signal) return; + Disconnect(); + signal_ = signal; + signal->connect(this, + &SigslotReceiver::OnSignal); + } + void Disconnect() { + if (!signal_) return; + signal_->disconnect(this); + signal_ = NULL; + } + void OnSignal() { + ++signal_count_; + } + int signal_count() { return signal_count_; } + + private: + sigslot::signal0* signal_; + int signal_count_; +}; + +template +class SigslotSlotTest : public testing::Test { + protected: + SigslotSlotTest() { + mt_signal_policy mt_policy; + TemplateIsMT(&mt_policy); + } + + virtual void SetUp() { + Connect(); + } + virtual void TearDown() { + Disconnect(); + } + + void Disconnect() { + st_receiver_.Disconnect(); + mt_receiver_.Disconnect(); + } + + void Connect() { + st_receiver_.Connect(&SignalSTLoopback); + mt_receiver_.Connect(&SignalMTLoopback); + } + + int st_loop_back_count() { return st_receiver_.signal_count(); } + int mt_loop_back_count() { return mt_receiver_.signal_count(); } + + sigslot::signal0<> SignalSTLoopback; + SigslotReceiver st_receiver_; + sigslot::signal0 SignalMTLoopback; + SigslotReceiver mt_receiver_; +}; + +typedef SigslotSlotTest<> SigslotSTSlotTest; +typedef SigslotSlotTest SigslotMTSlotTest; + +class multi_threaded_local_fake : public sigslot::multi_threaded_local { + public: + multi_threaded_local_fake() : lock_count_(0), unlock_count_(0) { + } + + virtual void lock() { + ++lock_count_; + } + virtual void unlock() { + ++unlock_count_; + } + + int lock_count() { return lock_count_; } + + bool InCriticalSection() { return lock_count_ != unlock_count_; } + + protected: + int lock_count_; + int unlock_count_; +}; + +typedef SigslotSlotTest SigslotMTLockBase; + +class SigslotMTLockTest : public SigslotMTLockBase { + protected: + SigslotMTLockTest() {} + + virtual void SetUp() { + EXPECT_EQ(0, SlotLockCount()); + SigslotMTLockBase::SetUp(); + // Connects to two signals (ST and MT). However, + // SlotLockCount() only gets the count for the + // MT signal (there are two separate SigslotReceiver which + // keep track of their own count). + EXPECT_EQ(1, SlotLockCount()); + } + virtual void TearDown() { + const int previous_lock_count = SlotLockCount(); + SigslotMTLockBase::TearDown(); + // Disconnects from two signals. Note analogous to SetUp(). + EXPECT_EQ(previous_lock_count + 1, SlotLockCount()); + } + + int SlotLockCount() { return mt_receiver_.lock_count(); } + void Signal() { SignalMTLoopback(); } + int SignalLockCount() { return SignalMTLoopback.lock_count(); } + int signal_count() { return mt_loop_back_count(); } + bool InCriticalSection() { return SignalMTLoopback.InCriticalSection(); } +}; + +// This test will always succeed. However, if the default template instantiation +// changes from single threaded to multi threaded it will break the build here. +TEST_F(SigslotDefault, DefaultIsST) { + EXPECT_TRUE(TemplateIsST(this)); + EXPECT_TRUE(TemplateIsST(&signal_)); +} + +// ST slot, ST signal +TEST_F(SigslotSTSlotTest, STLoopbackTest) { + SignalSTLoopback(); + EXPECT_EQ(1, st_loop_back_count()); + EXPECT_EQ(0, mt_loop_back_count()); +} + +// ST slot, MT signal +TEST_F(SigslotSTSlotTest, MTLoopbackTest) { + SignalMTLoopback(); + EXPECT_EQ(1, mt_loop_back_count()); + EXPECT_EQ(0, st_loop_back_count()); +} + +// ST slot, both ST and MT (separate) signal +TEST_F(SigslotSTSlotTest, AllLoopbackTest) { + SignalSTLoopback(); + SignalMTLoopback(); + EXPECT_EQ(1, mt_loop_back_count()); + EXPECT_EQ(1, st_loop_back_count()); +} + +TEST_F(SigslotSTSlotTest, Reconnect) { + SignalSTLoopback(); + SignalMTLoopback(); + EXPECT_EQ(1, mt_loop_back_count()); + EXPECT_EQ(1, st_loop_back_count()); + Disconnect(); + SignalSTLoopback(); + SignalMTLoopback(); + EXPECT_EQ(1, mt_loop_back_count()); + EXPECT_EQ(1, st_loop_back_count()); + Connect(); + SignalSTLoopback(); + SignalMTLoopback(); + EXPECT_EQ(2, mt_loop_back_count()); + EXPECT_EQ(2, st_loop_back_count()); +} + +// MT slot, ST signal +TEST_F(SigslotMTSlotTest, STLoopbackTest) { + SignalSTLoopback(); + EXPECT_EQ(1, st_loop_back_count()); + EXPECT_EQ(0, mt_loop_back_count()); +} + +// MT slot, MT signal +TEST_F(SigslotMTSlotTest, MTLoopbackTest) { + SignalMTLoopback(); + EXPECT_EQ(1, mt_loop_back_count()); + EXPECT_EQ(0, st_loop_back_count()); +} + +// MT slot, both ST and MT (separate) signal +TEST_F(SigslotMTSlotTest, AllLoopbackTest) { + SignalMTLoopback(); + SignalSTLoopback(); + EXPECT_EQ(1, st_loop_back_count()); + EXPECT_EQ(1, mt_loop_back_count()); +} + +// Test that locks are acquired and released correctly. +TEST_F(SigslotMTLockTest, LockSanity) { + const int lock_count = SignalLockCount(); + Signal(); + EXPECT_FALSE(InCriticalSection()); + EXPECT_EQ(lock_count + 1, SignalLockCount()); + EXPECT_EQ(1, signal_count()); +} + +// Destroy signal and slot in different orders. +TEST(DestructionOrder, SignalFirst) { + sigslot::signal0<>* signal = new sigslot::signal0<>; + SigslotReceiver<>* receiver = new SigslotReceiver<>(); + receiver->Connect(signal); + (*signal)(); + EXPECT_EQ(1, receiver->signal_count()); + delete signal; + delete receiver; +} + +TEST(DestructionOrder, SlotFirst) { + sigslot::signal0<>* signal = new sigslot::signal0<>; + SigslotReceiver<>* receiver = new SigslotReceiver<>(); + receiver->Connect(signal); + (*signal)(); + EXPECT_EQ(1, receiver->signal_count()); + + delete receiver; + (*signal)(); + delete signal; +} diff --git a/talk/base/sigslotrepeater.h b/talk/base/sigslotrepeater.h new file mode 100644 index 000000000..628089b5d --- /dev/null +++ b/talk/base/sigslotrepeater.h @@ -0,0 +1,111 @@ +/* + * libjingle + * Copyright 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. + */ + +#ifndef TALK_BASE_SIGSLOTREPEATER_H__ +#define TALK_BASE_SIGSLOTREPEATER_H__ + +// repeaters are both signals and slots, which are designed as intermediate +// pass-throughs for signals and slots which don't know about each other (for +// modularity or encapsulation). This eliminates the need to declare a signal +// handler whose sole purpose is to fire another signal. The repeater connects +// to the originating signal using the 'repeat' method. When the repeated +// signal fires, the repeater will also fire. + +#include "talk/base/sigslot.h" + +namespace sigslot { + + template + class repeater0 : public signal0, + public has_slots + { + public: + typedef signal0 base_type; + typedef repeater0 this_type; + + repeater0() { } + repeater0(const this_type& s) : base_type(s) { } + + void reemit() { signal0::emit(); } + void repeat(base_type &s) { s.connect(this, &this_type::reemit); } + void stop(base_type &s) { s.disconnect(this); } + }; + + template + class repeater1 : public signal1, + public has_slots + { + public: + typedef signal1 base_type; + typedef repeater1 this_type; + + repeater1() { } + repeater1(const this_type& s) : base_type(s) { } + + void reemit(arg1_type a1) { signal1::emit(a1); } + void repeat(base_type& s) { s.connect(this, &this_type::reemit); } + void stop(base_type &s) { s.disconnect(this); } + }; + + template + class repeater2 : public signal2, + public has_slots + { + public: + typedef signal2 base_type; + typedef repeater2 this_type; + + repeater2() { } + repeater2(const this_type& s) : base_type(s) { } + + void reemit(arg1_type a1, arg2_type a2) { signal2::emit(a1,a2); } + void repeat(base_type& s) { s.connect(this, &this_type::reemit); } + void stop(base_type &s) { s.disconnect(this); } + }; + + template + class repeater3 : public signal3, + public has_slots + { + public: + typedef signal3 base_type; + typedef repeater3 this_type; + + repeater3() { } + repeater3(const this_type& s) : base_type(s) { } + + void reemit(arg1_type a1, arg2_type a2, arg3_type a3) { + signal3::emit(a1,a2,a3); + } + void repeat(base_type& s) { s.connect(this, &this_type::reemit); } + void stop(base_type &s) { s.disconnect(this); } + }; + +} // namespace sigslot + +#endif // TALK_BASE_SIGSLOTREPEATER_H__ diff --git a/talk/base/socket.h b/talk/base/socket.h new file mode 100644 index 000000000..9932cdada --- /dev/null +++ b/talk/base/socket.h @@ -0,0 +1,201 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_SOCKET_H__ +#define TALK_BASE_SOCKET_H__ + +#include + +#ifdef POSIX +#include +#include +#include +#include +#define SOCKET_EACCES EACCES +#endif + +#ifdef WIN32 +#include "talk/base/win32.h" +#endif + +#include "talk/base/basictypes.h" +#include "talk/base/socketaddress.h" + +// Rather than converting errors into a private namespace, +// Reuse the POSIX socket api errors. Note this depends on +// Win32 compatibility. + +#ifdef WIN32 +#undef EWOULDBLOCK // Remove errno.h's definition for each macro below. +#define EWOULDBLOCK WSAEWOULDBLOCK +#undef EINPROGRESS +#define EINPROGRESS WSAEINPROGRESS +#undef EALREADY +#define EALREADY WSAEALREADY +#undef ENOTSOCK +#define ENOTSOCK WSAENOTSOCK +#undef EDESTADDRREQ +#define EDESTADDRREQ WSAEDESTADDRREQ +#undef EMSGSIZE +#define EMSGSIZE WSAEMSGSIZE +#undef EPROTOTYPE +#define EPROTOTYPE WSAEPROTOTYPE +#undef ENOPROTOOPT +#define ENOPROTOOPT WSAENOPROTOOPT +#undef EPROTONOSUPPORT +#define EPROTONOSUPPORT WSAEPROTONOSUPPORT +#undef ESOCKTNOSUPPORT +#define ESOCKTNOSUPPORT WSAESOCKTNOSUPPORT +#undef EOPNOTSUPP +#define EOPNOTSUPP WSAEOPNOTSUPP +#undef EPFNOSUPPORT +#define EPFNOSUPPORT WSAEPFNOSUPPORT +#undef EAFNOSUPPORT +#define EAFNOSUPPORT WSAEAFNOSUPPORT +#undef EADDRINUSE +#define EADDRINUSE WSAEADDRINUSE +#undef EADDRNOTAVAIL +#define EADDRNOTAVAIL WSAEADDRNOTAVAIL +#undef ENETDOWN +#define ENETDOWN WSAENETDOWN +#undef ENETUNREACH +#define ENETUNREACH WSAENETUNREACH +#undef ENETRESET +#define ENETRESET WSAENETRESET +#undef ECONNABORTED +#define ECONNABORTED WSAECONNABORTED +#undef ECONNRESET +#define ECONNRESET WSAECONNRESET +#undef ENOBUFS +#define ENOBUFS WSAENOBUFS +#undef EISCONN +#define EISCONN WSAEISCONN +#undef ENOTCONN +#define ENOTCONN WSAENOTCONN +#undef ESHUTDOWN +#define ESHUTDOWN WSAESHUTDOWN +#undef ETOOMANYREFS +#define ETOOMANYREFS WSAETOOMANYREFS +#undef ETIMEDOUT +#define ETIMEDOUT WSAETIMEDOUT +#undef ECONNREFUSED +#define ECONNREFUSED WSAECONNREFUSED +#undef ELOOP +#define ELOOP WSAELOOP +#undef ENAMETOOLONG +#define ENAMETOOLONG WSAENAMETOOLONG +#undef EHOSTDOWN +#define EHOSTDOWN WSAEHOSTDOWN +#undef EHOSTUNREACH +#define EHOSTUNREACH WSAEHOSTUNREACH +#undef ENOTEMPTY +#define ENOTEMPTY WSAENOTEMPTY +#undef EPROCLIM +#define EPROCLIM WSAEPROCLIM +#undef EUSERS +#define EUSERS WSAEUSERS +#undef EDQUOT +#define EDQUOT WSAEDQUOT +#undef ESTALE +#define ESTALE WSAESTALE +#undef EREMOTE +#define EREMOTE WSAEREMOTE +#undef EACCES +#define SOCKET_EACCES WSAEACCES +#endif // WIN32 + +#ifdef POSIX +#define INVALID_SOCKET (-1) +#define SOCKET_ERROR (-1) +#define closesocket(s) close(s) +#endif // POSIX + +namespace talk_base { + +inline bool IsBlockingError(int e) { + return (e == EWOULDBLOCK) || (e == EAGAIN) || (e == EINPROGRESS); +} + +// General interface for the socket implementations of various networks. The +// methods match those of normal UNIX sockets very closely. +class Socket { + public: + virtual ~Socket() {} + + // Returns the address to which the socket is bound. If the socket is not + // bound, then the any-address is returned. + virtual SocketAddress GetLocalAddress() const = 0; + + // Returns the address to which the socket is connected. If the socket is + // not connected, then the any-address is returned. + virtual SocketAddress GetRemoteAddress() const = 0; + + virtual int Bind(const SocketAddress& addr) = 0; + virtual int Connect(const SocketAddress& addr) = 0; + virtual int Send(const void *pv, size_t cb) = 0; + virtual int SendTo(const void *pv, size_t cb, const SocketAddress& addr) = 0; + virtual int Recv(void *pv, size_t cb) = 0; + virtual int RecvFrom(void *pv, size_t cb, SocketAddress *paddr) = 0; + virtual int Listen(int backlog) = 0; + virtual Socket *Accept(SocketAddress *paddr) = 0; + virtual int Close() = 0; + virtual int GetError() const = 0; + virtual void SetError(int error) = 0; + inline bool IsBlocking() const { return IsBlockingError(GetError()); } + + enum ConnState { + CS_CLOSED, + CS_CONNECTING, + CS_CONNECTED + }; + virtual ConnState GetState() const = 0; + + // Fills in the given uint16 with the current estimate of the MTU along the + // path to the address to which this socket is connected. NOTE: This method + // can block for up to 10 seconds on Windows. + virtual int EstimateMTU(uint16* mtu) = 0; + + enum Option { + OPT_DONTFRAGMENT, + OPT_RCVBUF, // receive buffer size + OPT_SNDBUF, // send buffer size + OPT_NODELAY, // whether Nagle algorithm is enabled + OPT_IPV6_V6ONLY // Whether the socket is IPv6 only. + }; + virtual int GetOption(Option opt, int* value) = 0; + virtual int SetOption(Option opt, int value) = 0; + + protected: + Socket() {} + + private: + DISALLOW_EVIL_CONSTRUCTORS(Socket); +}; + +} // namespace talk_base + +#endif // TALK_BASE_SOCKET_H__ diff --git a/talk/base/socket_unittest.cc b/talk/base/socket_unittest.cc new file mode 100644 index 000000000..dd4b1e526 --- /dev/null +++ b/talk/base/socket_unittest.cc @@ -0,0 +1,1018 @@ +/* + * libjingle + * Copyright 2007, 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/base/socket_unittest.h" + +#include "talk/base/asyncudpsocket.h" +#include "talk/base/gunit.h" +#include "talk/base/nethelpers.h" +#include "talk/base/socketserver.h" +#include "talk/base/testclient.h" +#include "talk/base/testutils.h" +#include "talk/base/thread.h" + +namespace talk_base { + +#define MAYBE_SKIP_IPV6 \ + if (!HasIPv6Enabled()) { \ + LOG(LS_INFO) << "No IPv6... skipping"; \ + return; \ + } + + +void SocketTest::TestConnectIPv4() { + ConnectInternal(kIPv4Loopback); +} + +void SocketTest::TestConnectIPv6() { + MAYBE_SKIP_IPV6; + ConnectInternal(kIPv6Loopback); +} + +void SocketTest::TestConnectWithDnsLookupIPv4() { + ConnectWithDnsLookupInternal(kIPv4Loopback, "localhost"); +} + +void SocketTest::TestConnectWithDnsLookupIPv6() { + // TODO: Enable this when DNS resolution supports IPv6. + LOG(LS_INFO) << "Skipping IPv6 DNS test"; + // ConnectWithDnsLookupInternal(kIPv6Loopback, "localhost6"); +} + +void SocketTest::TestConnectFailIPv4() { + ConnectFailInternal(kIPv4Loopback); +} + +void SocketTest::TestConnectFailIPv6() { + MAYBE_SKIP_IPV6; + ConnectFailInternal(kIPv6Loopback); +} + +void SocketTest::TestConnectWithDnsLookupFailIPv4() { + ConnectWithDnsLookupFailInternal(kIPv4Loopback); +} + +void SocketTest::TestConnectWithDnsLookupFailIPv6() { + MAYBE_SKIP_IPV6; + ConnectWithDnsLookupFailInternal(kIPv6Loopback); +} + +void SocketTest::TestConnectWithClosedSocketIPv4() { + ConnectWithClosedSocketInternal(kIPv4Loopback); +} + +void SocketTest::TestConnectWithClosedSocketIPv6() { + MAYBE_SKIP_IPV6; + ConnectWithClosedSocketInternal(kIPv6Loopback); +} + +void SocketTest::TestConnectWhileNotClosedIPv4() { + ConnectWhileNotClosedInternal(kIPv4Loopback); +} + +void SocketTest::TestConnectWhileNotClosedIPv6() { + MAYBE_SKIP_IPV6; + ConnectWhileNotClosedInternal(kIPv6Loopback); +} + +void SocketTest::TestServerCloseDuringConnectIPv4() { + ServerCloseDuringConnectInternal(kIPv4Loopback); +} + +void SocketTest::TestServerCloseDuringConnectIPv6() { + MAYBE_SKIP_IPV6; + ServerCloseDuringConnectInternal(kIPv6Loopback); +} + +void SocketTest::TestClientCloseDuringConnectIPv4() { + ClientCloseDuringConnectInternal(kIPv4Loopback); +} + +void SocketTest::TestClientCloseDuringConnectIPv6() { + MAYBE_SKIP_IPV6; + ClientCloseDuringConnectInternal(kIPv6Loopback); +} + +void SocketTest::TestServerCloseIPv4() { + ServerCloseInternal(kIPv4Loopback); +} + +void SocketTest::TestServerCloseIPv6() { + MAYBE_SKIP_IPV6; + ServerCloseInternal(kIPv6Loopback); +} + +void SocketTest::TestCloseInClosedCallbackIPv4() { + CloseInClosedCallbackInternal(kIPv4Loopback); +} + +void SocketTest::TestCloseInClosedCallbackIPv6() { + MAYBE_SKIP_IPV6; + CloseInClosedCallbackInternal(kIPv6Loopback); +} + +void SocketTest::TestSocketServerWaitIPv4() { + SocketServerWaitInternal(kIPv4Loopback); +} + +void SocketTest::TestSocketServerWaitIPv6() { + MAYBE_SKIP_IPV6; + SocketServerWaitInternal(kIPv6Loopback); +} + +void SocketTest::TestTcpIPv4() { + TcpInternal(kIPv4Loopback); +} + +void SocketTest::TestTcpIPv6() { + MAYBE_SKIP_IPV6; + TcpInternal(kIPv6Loopback); +} + +void SocketTest::TestSingleFlowControlCallbackIPv4() { + SingleFlowControlCallbackInternal(kIPv4Loopback); +} + +void SocketTest::TestSingleFlowControlCallbackIPv6() { + MAYBE_SKIP_IPV6; + SingleFlowControlCallbackInternal(kIPv6Loopback); +} + +void SocketTest::TestUdpIPv4() { + UdpInternal(kIPv4Loopback); +} + +void SocketTest::TestUdpIPv6() { + MAYBE_SKIP_IPV6; + UdpInternal(kIPv6Loopback); +} + +void SocketTest::TestUdpReadyToSendIPv4() { +#if !defined(OSX) + // TODO(ronghuawu): Enable this test (currently failed on build bots) on mac. + UdpReadyToSend(kIPv4Loopback); +#endif +} + +void SocketTest::TestUdpReadyToSendIPv6() { +#if defined(WIN32) + // TODO(ronghuawu): Enable this test (currently flakey) on mac and linux. + MAYBE_SKIP_IPV6; + UdpReadyToSend(kIPv6Loopback); +#endif +} + +void SocketTest::TestGetSetOptionsIPv4() { + GetSetOptionsInternal(kIPv4Loopback); +} + +void SocketTest::TestGetSetOptionsIPv6() { + MAYBE_SKIP_IPV6; + GetSetOptionsInternal(kIPv6Loopback); +} + +// For unbound sockets, GetLocalAddress / GetRemoteAddress return AF_UNSPEC +// values on Windows, but an empty address of the same family on Linux/MacOS X. +bool IsUnspecOrEmptyIP(const IPAddress& address) { +#ifndef WIN32 + return IPIsAny(address); +#else + return address.family() == AF_UNSPEC; +#endif +} + +void SocketTest::ConnectInternal(const IPAddress& loopback) { + testing::StreamSink sink; + SocketAddress accept_addr; + + // Create client. + scoped_ptr client(ss_->CreateAsyncSocket(loopback.family(), + SOCK_STREAM)); + sink.Monitor(client.get()); + EXPECT_EQ(AsyncSocket::CS_CLOSED, client->GetState()); + EXPECT_PRED1(IsUnspecOrEmptyIP, client->GetLocalAddress().ipaddr()); + + // Create server and listen. + scoped_ptr server( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(server.get()); + EXPECT_EQ(0, server->Bind(SocketAddress(loopback, 0))); + EXPECT_EQ(0, server->Listen(5)); + EXPECT_EQ(AsyncSocket::CS_CONNECTING, server->GetState()); + + // Ensure no pending server connections, since we haven't done anything yet. + EXPECT_FALSE(sink.Check(server.get(), testing::SSE_READ)); + EXPECT_TRUE(NULL == server->Accept(&accept_addr)); + EXPECT_TRUE(accept_addr.IsNil()); + + // Attempt connect to listening socket. + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + EXPECT_FALSE(client->GetLocalAddress().IsNil()); + EXPECT_NE(server->GetLocalAddress(), client->GetLocalAddress()); + + // Client is connecting, outcome not yet determined. + EXPECT_EQ(AsyncSocket::CS_CONNECTING, client->GetState()); + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_OPEN)); + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_CLOSE)); + + // Server has pending connection, accept it. + EXPECT_TRUE_WAIT((sink.Check(server.get(), testing::SSE_READ)), kTimeout); + scoped_ptr accepted(server->Accept(&accept_addr)); + ASSERT_TRUE(accepted); + EXPECT_FALSE(accept_addr.IsNil()); + EXPECT_EQ(accepted->GetRemoteAddress(), accept_addr); + + // Connected from server perspective, check the addresses are correct. + EXPECT_EQ(AsyncSocket::CS_CONNECTED, accepted->GetState()); + EXPECT_EQ(server->GetLocalAddress(), accepted->GetLocalAddress()); + EXPECT_EQ(client->GetLocalAddress(), accepted->GetRemoteAddress()); + + // Connected from client perspective, check the addresses are correct. + EXPECT_EQ_WAIT(AsyncSocket::CS_CONNECTED, client->GetState(), kTimeout); + EXPECT_TRUE(sink.Check(client.get(), testing::SSE_OPEN)); + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_CLOSE)); + EXPECT_EQ(client->GetRemoteAddress(), server->GetLocalAddress()); + EXPECT_EQ(client->GetRemoteAddress(), accepted->GetLocalAddress()); +} + +void SocketTest::ConnectWithDnsLookupInternal(const IPAddress& loopback, + const std::string& host) { + testing::StreamSink sink; + SocketAddress accept_addr; + + // Create client. + scoped_ptr client( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(client.get()); + + // Create server and listen. + scoped_ptr server( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(server.get()); + EXPECT_EQ(0, server->Bind(SocketAddress(loopback, 0))); + EXPECT_EQ(0, server->Listen(5)); + + // Attempt connect to listening socket. + SocketAddress dns_addr(server->GetLocalAddress()); + dns_addr.SetIP(host); + EXPECT_EQ(0, client->Connect(dns_addr)); + // TODO: Bind when doing DNS lookup. + //EXPECT_NE(kEmptyAddr, client->GetLocalAddress()); // Implicit Bind + + // Client is connecting, outcome not yet determined. + EXPECT_EQ(AsyncSocket::CS_CONNECTING, client->GetState()); + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_OPEN)); + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_CLOSE)); + + // Server has pending connection, accept it. + EXPECT_TRUE_WAIT((sink.Check(server.get(), testing::SSE_READ)), kTimeout); + scoped_ptr accepted(server->Accept(&accept_addr)); + ASSERT_TRUE(accepted); + EXPECT_FALSE(accept_addr.IsNil()); + EXPECT_EQ(accepted->GetRemoteAddress(), accept_addr); + + // Connected from server perspective, check the addresses are correct. + EXPECT_EQ(AsyncSocket::CS_CONNECTED, accepted->GetState()); + EXPECT_EQ(server->GetLocalAddress(), accepted->GetLocalAddress()); + EXPECT_EQ(client->GetLocalAddress(), accepted->GetRemoteAddress()); + + // Connected from client perspective, check the addresses are correct. + EXPECT_EQ_WAIT(AsyncSocket::CS_CONNECTED, client->GetState(), kTimeout); + EXPECT_TRUE(sink.Check(client.get(), testing::SSE_OPEN)); + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_CLOSE)); + EXPECT_EQ(client->GetRemoteAddress(), server->GetLocalAddress()); + EXPECT_EQ(client->GetRemoteAddress(), accepted->GetLocalAddress()); +} + +void SocketTest::ConnectFailInternal(const IPAddress& loopback) { + testing::StreamSink sink; + SocketAddress accept_addr; + + // Create client. + scoped_ptr client( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(client.get()); + + // Create server, but don't listen yet. + scoped_ptr server( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(server.get()); + EXPECT_EQ(0, server->Bind(SocketAddress(loopback, 0))); + + // Attempt connect to a non-existent socket. + // We don't connect to the server socket created above, since on + // MacOS it takes about 75 seconds to get back an error! + SocketAddress bogus_addr(loopback, 65535); + EXPECT_EQ(0, client->Connect(bogus_addr)); + + // Wait for connection to fail (ECONNREFUSED). + EXPECT_EQ_WAIT(AsyncSocket::CS_CLOSED, client->GetState(), kTimeout); + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_OPEN)); + EXPECT_TRUE(sink.Check(client.get(), testing::SSE_ERROR)); + EXPECT_TRUE(client->GetRemoteAddress().IsNil()); + + // Should be no pending server connections. + EXPECT_FALSE(sink.Check(server.get(), testing::SSE_READ)); + EXPECT_TRUE(NULL == server->Accept(&accept_addr)); + EXPECT_EQ(IPAddress(), accept_addr.ipaddr()); +} + +void SocketTest::ConnectWithDnsLookupFailInternal(const IPAddress& loopback) { + testing::StreamSink sink; + SocketAddress accept_addr; + + // Create client. + scoped_ptr client( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(client.get()); + + // Create server, but don't listen yet. + scoped_ptr server( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(server.get()); + EXPECT_EQ(0, server->Bind(SocketAddress(loopback, 0))); + + // Attempt connect to a non-existent host. + // We don't connect to the server socket created above, since on + // MacOS it takes about 75 seconds to get back an error! + SocketAddress bogus_dns_addr("not-a-real-hostname", 65535); + EXPECT_EQ(0, client->Connect(bogus_dns_addr)); + + // Wait for connection to fail (EHOSTNOTFOUND). + EXPECT_EQ_WAIT(AsyncSocket::CS_CLOSED, client->GetState(), kTimeout); + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_OPEN)); + EXPECT_TRUE(sink.Check(client.get(), testing::SSE_ERROR)); + EXPECT_TRUE(client->GetRemoteAddress().IsNil()); + // Should be no pending server connections. + EXPECT_FALSE(sink.Check(server.get(), testing::SSE_READ)); + EXPECT_TRUE(NULL == server->Accept(&accept_addr)); + EXPECT_TRUE(accept_addr.IsNil()); +} + +void SocketTest::ConnectWithClosedSocketInternal(const IPAddress& loopback) { + // Create server and listen. + scoped_ptr server( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + EXPECT_EQ(0, server->Bind(SocketAddress(loopback, 0))); + EXPECT_EQ(0, server->Listen(5)); + + // Create a client and put in to CS_CLOSED state. + scoped_ptr client( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + EXPECT_EQ(0, client->Close()); + EXPECT_EQ(AsyncSocket::CS_CLOSED, client->GetState()); + + // Connect() should reinitialize the socket, and put it in to CS_CONNECTING. + EXPECT_EQ(0, client->Connect(SocketAddress(server->GetLocalAddress()))); + EXPECT_EQ(AsyncSocket::CS_CONNECTING, client->GetState()); +} + +void SocketTest::ConnectWhileNotClosedInternal(const IPAddress& loopback) { + // Create server and listen. + testing::StreamSink sink; + scoped_ptr server( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(server.get()); + EXPECT_EQ(0, server->Bind(SocketAddress(loopback, 0))); + EXPECT_EQ(0, server->Listen(5)); + // Create client, connect. + scoped_ptr client( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + EXPECT_EQ(0, client->Connect(SocketAddress(server->GetLocalAddress()))); + EXPECT_EQ(AsyncSocket::CS_CONNECTING, client->GetState()); + // Try to connect again. Should fail, but not interfere with original attempt. + EXPECT_EQ(SOCKET_ERROR, + client->Connect(SocketAddress(server->GetLocalAddress()))); + + // Accept the original connection. + SocketAddress accept_addr; + EXPECT_TRUE_WAIT((sink.Check(server.get(), testing::SSE_READ)), kTimeout); + scoped_ptr accepted(server->Accept(&accept_addr)); + ASSERT_TRUE(accepted); + EXPECT_FALSE(accept_addr.IsNil()); + + // Check the states and addresses. + EXPECT_EQ(AsyncSocket::CS_CONNECTED, accepted->GetState()); + EXPECT_EQ(server->GetLocalAddress(), accepted->GetLocalAddress()); + EXPECT_EQ(client->GetLocalAddress(), accepted->GetRemoteAddress()); + EXPECT_EQ_WAIT(AsyncSocket::CS_CONNECTED, client->GetState(), kTimeout); + EXPECT_EQ(client->GetRemoteAddress(), server->GetLocalAddress()); + EXPECT_EQ(client->GetRemoteAddress(), accepted->GetLocalAddress()); + + // Try to connect again, to an unresolved hostname. + // Shouldn't break anything. + EXPECT_EQ(SOCKET_ERROR, + client->Connect(SocketAddress("localhost", + server->GetLocalAddress().port()))); + EXPECT_EQ(AsyncSocket::CS_CONNECTED, accepted->GetState()); + EXPECT_EQ(AsyncSocket::CS_CONNECTED, client->GetState()); + EXPECT_EQ(client->GetRemoteAddress(), server->GetLocalAddress()); + EXPECT_EQ(client->GetRemoteAddress(), accepted->GetLocalAddress()); +} + +void SocketTest::ServerCloseDuringConnectInternal(const IPAddress& loopback) { + testing::StreamSink sink; + + // Create client. + scoped_ptr client( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(client.get()); + + // Create server and listen. + scoped_ptr server( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(server.get()); + EXPECT_EQ(0, server->Bind(SocketAddress(loopback, 0))); + EXPECT_EQ(0, server->Listen(5)); + + // Attempt connect to listening socket. + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + + // Close down the server while the socket is in the accept queue. + EXPECT_TRUE_WAIT(sink.Check(server.get(), testing::SSE_READ), kTimeout); + server->Close(); + + // This should fail the connection for the client. Clean up. + EXPECT_EQ_WAIT(AsyncSocket::CS_CLOSED, client->GetState(), kTimeout); + EXPECT_TRUE(sink.Check(client.get(), testing::SSE_ERROR)); + client->Close(); +} + +void SocketTest::ClientCloseDuringConnectInternal(const IPAddress& loopback) { + testing::StreamSink sink; + SocketAddress accept_addr; + + // Create client. + scoped_ptr client( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(client.get()); + + // Create server and listen. + scoped_ptr server( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(server.get()); + EXPECT_EQ(0, server->Bind(SocketAddress(loopback, 0))); + EXPECT_EQ(0, server->Listen(5)); + + // Attempt connect to listening socket. + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + + // Close down the client while the socket is in the accept queue. + EXPECT_TRUE_WAIT(sink.Check(server.get(), testing::SSE_READ), kTimeout); + client->Close(); + + // The connection should still be able to be accepted. + scoped_ptr accepted(server->Accept(&accept_addr)); + ASSERT_TRUE(accepted); + sink.Monitor(accepted.get()); + EXPECT_EQ(AsyncSocket::CS_CONNECTED, accepted->GetState()); + + // The accepted socket should then close (possibly with err, timing-related) + EXPECT_EQ_WAIT(AsyncSocket::CS_CLOSED, accepted->GetState(), kTimeout); + EXPECT_TRUE(sink.Check(accepted.get(), testing::SSE_CLOSE) || + sink.Check(accepted.get(), testing::SSE_ERROR)); + + // The client should not get a close event. + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_CLOSE)); +} + +void SocketTest::ServerCloseInternal(const IPAddress& loopback) { + testing::StreamSink sink; + SocketAddress accept_addr; + + // Create client. + scoped_ptr client( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(client.get()); + + // Create server and listen. + scoped_ptr server( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(server.get()); + EXPECT_EQ(0, server->Bind(SocketAddress(loopback, 0))); + EXPECT_EQ(0, server->Listen(5)); + + // Attempt connection. + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + + // Accept connection. + EXPECT_TRUE_WAIT((sink.Check(server.get(), testing::SSE_READ)), kTimeout); + scoped_ptr accepted(server->Accept(&accept_addr)); + ASSERT_TRUE(accepted); + sink.Monitor(accepted.get()); + + // Both sides are now connected. + EXPECT_EQ_WAIT(AsyncSocket::CS_CONNECTED, client->GetState(), kTimeout); + EXPECT_TRUE(sink.Check(client.get(), testing::SSE_OPEN)); + EXPECT_EQ(client->GetRemoteAddress(), accepted->GetLocalAddress()); + EXPECT_EQ(accepted->GetRemoteAddress(), client->GetLocalAddress()); + + // Send data to the client, and then close the connection. + EXPECT_EQ(1, accepted->Send("a", 1)); + accepted->Close(); + EXPECT_EQ(AsyncSocket::CS_CLOSED, accepted->GetState()); + + // Expect that the client is notified, and has not yet closed. + EXPECT_TRUE_WAIT(sink.Check(client.get(), testing::SSE_READ), kTimeout); + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_CLOSE)); + EXPECT_EQ(AsyncSocket::CS_CONNECTED, client->GetState()); + + // Ensure the data can be read. + char buffer[10]; + EXPECT_EQ(1, client->Recv(buffer, sizeof(buffer))); + EXPECT_EQ('a', buffer[0]); + + // Now we should close, but the remote address will remain. + EXPECT_EQ_WAIT(AsyncSocket::CS_CLOSED, client->GetState(), kTimeout); + EXPECT_TRUE(sink.Check(client.get(), testing::SSE_CLOSE)); + EXPECT_FALSE(client->GetRemoteAddress().IsAnyIP()); + + // The closer should not get a close signal. + EXPECT_FALSE(sink.Check(accepted.get(), testing::SSE_CLOSE)); + EXPECT_TRUE(accepted->GetRemoteAddress().IsNil()); + + // And the closee should only get a single signal. + Thread::Current()->ProcessMessages(0); + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_CLOSE)); + + // Close down the client and ensure all is good. + client->Close(); + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_CLOSE)); + EXPECT_TRUE(client->GetRemoteAddress().IsNil()); +} + +class SocketCloser : public sigslot::has_slots<> { + public: + void OnClose(AsyncSocket* socket, int error) { + socket->Close(); // Deleting here would blow up the vector of handlers + // for the socket's signal. + } +}; + +void SocketTest::CloseInClosedCallbackInternal(const IPAddress& loopback) { + testing::StreamSink sink; + SocketCloser closer; + SocketAddress accept_addr; + + // Create client. + scoped_ptr client( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(client.get()); + client->SignalCloseEvent.connect(&closer, &SocketCloser::OnClose); + + // Create server and listen. + scoped_ptr server( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(server.get()); + EXPECT_EQ(0, server->Bind(SocketAddress(loopback, 0))); + EXPECT_EQ(0, server->Listen(5)); + + // Attempt connection. + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + + // Accept connection. + EXPECT_TRUE_WAIT((sink.Check(server.get(), testing::SSE_READ)), kTimeout); + scoped_ptr accepted(server->Accept(&accept_addr)); + ASSERT_TRUE(accepted); + sink.Monitor(accepted.get()); + + // Both sides are now connected. + EXPECT_EQ_WAIT(AsyncSocket::CS_CONNECTED, client->GetState(), kTimeout); + EXPECT_TRUE(sink.Check(client.get(), testing::SSE_OPEN)); + EXPECT_EQ(client->GetRemoteAddress(), accepted->GetLocalAddress()); + EXPECT_EQ(accepted->GetRemoteAddress(), client->GetLocalAddress()); + + // Send data to the client, and then close the connection. + accepted->Close(); + EXPECT_EQ(AsyncSocket::CS_CLOSED, accepted->GetState()); + + // Expect that the client is notified, and has not yet closed. + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_CLOSE)); + EXPECT_EQ(AsyncSocket::CS_CONNECTED, client->GetState()); + + // Now we should be closed and invalidated + EXPECT_EQ_WAIT(AsyncSocket::CS_CLOSED, client->GetState(), kTimeout); + EXPECT_TRUE(sink.Check(client.get(), testing::SSE_CLOSE)); + EXPECT_TRUE(Socket::CS_CLOSED == client->GetState()); +} + +class Sleeper : public MessageHandler { + public: + Sleeper() {} + void OnMessage(Message* msg) { + Thread::Current()->SleepMs(500); + } +}; + +void SocketTest::SocketServerWaitInternal(const IPAddress& loopback) { + testing::StreamSink sink; + SocketAddress accept_addr; + + // Create & connect server and client sockets. + scoped_ptr client( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + scoped_ptr server( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(client.get()); + sink.Monitor(server.get()); + EXPECT_EQ(0, server->Bind(SocketAddress(loopback, 0))); + EXPECT_EQ(0, server->Listen(5)); + + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + EXPECT_TRUE_WAIT((sink.Check(server.get(), testing::SSE_READ)), kTimeout); + + scoped_ptr accepted(server->Accept(&accept_addr)); + ASSERT_TRUE(accepted); + sink.Monitor(accepted.get()); + EXPECT_EQ(AsyncSocket::CS_CONNECTED, accepted->GetState()); + EXPECT_EQ(server->GetLocalAddress(), accepted->GetLocalAddress()); + EXPECT_EQ(client->GetLocalAddress(), accepted->GetRemoteAddress()); + + EXPECT_EQ_WAIT(AsyncSocket::CS_CONNECTED, client->GetState(), kTimeout); + EXPECT_TRUE(sink.Check(client.get(), testing::SSE_OPEN)); + EXPECT_FALSE(sink.Check(client.get(), testing::SSE_CLOSE)); + EXPECT_EQ(client->GetRemoteAddress(), server->GetLocalAddress()); + EXPECT_EQ(client->GetRemoteAddress(), accepted->GetLocalAddress()); + + // Do an i/o operation, triggering an eventual callback. + EXPECT_FALSE(sink.Check(accepted.get(), testing::SSE_READ)); + char buf[1024] = {0}; + + EXPECT_EQ(1024, client->Send(buf, 1024)); + EXPECT_FALSE(sink.Check(accepted.get(), testing::SSE_READ)); + + // Shouldn't signal when blocked in a thread Send, where process_io is false. + scoped_ptr thread(new Thread()); + thread->Start(); + Sleeper sleeper; + TypedMessageData data(client.get()); + thread->Send(&sleeper, 0, &data); + EXPECT_FALSE(sink.Check(accepted.get(), testing::SSE_READ)); + + // But should signal when process_io is true. + EXPECT_TRUE_WAIT((sink.Check(accepted.get(), testing::SSE_READ)), kTimeout); + EXPECT_LT(0, accepted->Recv(buf, 1024)); +} + +void SocketTest::TcpInternal(const IPAddress& loopback) { + testing::StreamSink sink; + SocketAddress accept_addr; + + // Create test data. + const size_t kDataSize = 1024 * 1024; + scoped_array send_buffer(new char[kDataSize]); + scoped_array recv_buffer(new char[kDataSize]); + size_t send_pos = 0, recv_pos = 0; + for (size_t i = 0; i < kDataSize; ++i) { + send_buffer[i] = static_cast(i % 256); + recv_buffer[i] = 0; + } + + // Create client. + scoped_ptr client( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(client.get()); + + // Create server and listen. + scoped_ptr server( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(server.get()); + EXPECT_EQ(0, server->Bind(SocketAddress(loopback, 0))); + EXPECT_EQ(0, server->Listen(5)); + + // Attempt connection. + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + + // Accept connection. + EXPECT_TRUE_WAIT((sink.Check(server.get(), testing::SSE_READ)), kTimeout); + scoped_ptr accepted(server->Accept(&accept_addr)); + ASSERT_TRUE(accepted); + sink.Monitor(accepted.get()); + + // Both sides are now connected. + EXPECT_EQ_WAIT(AsyncSocket::CS_CONNECTED, client->GetState(), kTimeout); + EXPECT_TRUE(sink.Check(client.get(), testing::SSE_OPEN)); + EXPECT_EQ(client->GetRemoteAddress(), accepted->GetLocalAddress()); + EXPECT_EQ(accepted->GetRemoteAddress(), client->GetLocalAddress()); + + // Send and receive a bunch of data. + bool send_waiting_for_writability = false; + bool send_expect_success = true; + bool recv_waiting_for_readability = true; + bool recv_expect_success = false; + int data_in_flight = 0; + while (recv_pos < kDataSize) { + // Send as much as we can if we've been cleared to send. + while (!send_waiting_for_writability && send_pos < kDataSize) { + int tosend = static_cast(kDataSize - send_pos); + int sent = accepted->Send(send_buffer.get() + send_pos, tosend); + if (send_expect_success) { + // The first Send() after connecting or getting writability should + // succeed and send some data. + EXPECT_GT(sent, 0); + send_expect_success = false; + } + if (sent >= 0) { + EXPECT_LE(sent, tosend); + send_pos += sent; + data_in_flight += sent; + } else { + ASSERT_TRUE(accepted->IsBlocking()); + send_waiting_for_writability = true; + } + } + + // Read all the sent data. + while (data_in_flight > 0) { + if (recv_waiting_for_readability) { + // Wait until data is available. + EXPECT_TRUE_WAIT(sink.Check(client.get(), testing::SSE_READ), kTimeout); + recv_waiting_for_readability = false; + recv_expect_success = true; + } + + // Receive as much as we can get in a single recv call. + int rcvd = client->Recv(recv_buffer.get() + recv_pos, + kDataSize - recv_pos); + + if (recv_expect_success) { + // The first Recv() after getting readability should succeed and receive + // some data. + // TODO: The following line is disabled due to flakey pulse + // builds. Re-enable if/when possible. + // EXPECT_GT(rcvd, 0); + recv_expect_success = false; + } + if (rcvd >= 0) { + EXPECT_LE(rcvd, data_in_flight); + recv_pos += rcvd; + data_in_flight -= rcvd; + } else { + ASSERT_TRUE(client->IsBlocking()); + recv_waiting_for_readability = true; + } + } + + // Once all that we've sent has been rcvd, expect to be able to send again. + if (send_waiting_for_writability) { + EXPECT_TRUE_WAIT(sink.Check(accepted.get(), testing::SSE_WRITE), + kTimeout); + send_waiting_for_writability = false; + send_expect_success = true; + } + } + + // The received data matches the sent data. + EXPECT_EQ(kDataSize, send_pos); + EXPECT_EQ(kDataSize, recv_pos); + EXPECT_EQ(0, memcmp(recv_buffer.get(), send_buffer.get(), kDataSize)); + + // Close down. + accepted->Close(); + EXPECT_EQ_WAIT(AsyncSocket::CS_CLOSED, client->GetState(), kTimeout); + EXPECT_TRUE(sink.Check(client.get(), testing::SSE_CLOSE)); + client->Close(); +} + +void SocketTest::SingleFlowControlCallbackInternal(const IPAddress& loopback) { + testing::StreamSink sink; + SocketAddress accept_addr; + + // Create client. + scoped_ptr client( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(client.get()); + + // Create server and listen. + scoped_ptr server( + ss_->CreateAsyncSocket(loopback.family(), SOCK_STREAM)); + sink.Monitor(server.get()); + EXPECT_EQ(0, server->Bind(SocketAddress(loopback, 0))); + EXPECT_EQ(0, server->Listen(5)); + + // Attempt connection. + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + + // Accept connection. + EXPECT_TRUE_WAIT((sink.Check(server.get(), testing::SSE_READ)), kTimeout); + scoped_ptr accepted(server->Accept(&accept_addr)); + ASSERT_TRUE(accepted); + sink.Monitor(accepted.get()); + + // Both sides are now connected. + EXPECT_EQ_WAIT(AsyncSocket::CS_CONNECTED, client->GetState(), kTimeout); + EXPECT_TRUE(sink.Check(client.get(), testing::SSE_OPEN)); + EXPECT_EQ(client->GetRemoteAddress(), accepted->GetLocalAddress()); + EXPECT_EQ(accepted->GetRemoteAddress(), client->GetLocalAddress()); + + // Expect a writable callback from the connect. + EXPECT_TRUE_WAIT(sink.Check(accepted.get(), testing::SSE_WRITE), kTimeout); + + // Fill the socket buffer. + char buf[1024 * 16] = {0}; + int sends = 0; + while (++sends && accepted->Send(&buf, ARRAY_SIZE(buf)) != -1) {} + EXPECT_TRUE(accepted->IsBlocking()); + + // Wait until data is available. + EXPECT_TRUE_WAIT(sink.Check(client.get(), testing::SSE_READ), kTimeout); + + // Pull data. + for (int i = 0; i < sends; ++i) { + client->Recv(buf, ARRAY_SIZE(buf)); + } + + // Expect at least one additional writable callback. + EXPECT_TRUE_WAIT(sink.Check(accepted.get(), testing::SSE_WRITE), kTimeout); + + // Adding data in response to the writeable callback shouldn't cause infinite + // callbacks. + int extras = 0; + for (int i = 0; i < 100; ++i) { + accepted->Send(&buf, ARRAY_SIZE(buf)); + talk_base::Thread::Current()->ProcessMessages(1); + if (sink.Check(accepted.get(), testing::SSE_WRITE)) { + extras++; + } + } + EXPECT_LT(extras, 2); + + // Close down. + accepted->Close(); + client->Close(); +} + +void SocketTest::UdpInternal(const IPAddress& loopback) { + SocketAddress empty = EmptySocketAddressWithFamily(loopback.family()); + // Test basic bind and connect behavior. + AsyncSocket* socket = + ss_->CreateAsyncSocket(loopback.family(), SOCK_DGRAM); + EXPECT_EQ(AsyncSocket::CS_CLOSED, socket->GetState()); + EXPECT_EQ(0, socket->Bind(SocketAddress(loopback, 0))); + SocketAddress addr1 = socket->GetLocalAddress(); + EXPECT_EQ(0, socket->Connect(addr1)); + EXPECT_EQ(AsyncSocket::CS_CONNECTED, socket->GetState()); + socket->Close(); + EXPECT_EQ(AsyncSocket::CS_CLOSED, socket->GetState()); + delete socket; + + // Test send/receive behavior. + scoped_ptr client1( + new TestClient(AsyncUDPSocket::Create(ss_, addr1))); + scoped_ptr client2( + new TestClient(AsyncUDPSocket::Create(ss_, empty))); + + SocketAddress addr2; + EXPECT_EQ(3, client2->SendTo("foo", 3, addr1)); + EXPECT_TRUE(client1->CheckNextPacket("foo", 3, &addr2)); + + SocketAddress addr3; + EXPECT_EQ(6, client1->SendTo("bizbaz", 6, addr2)); + EXPECT_TRUE(client2->CheckNextPacket("bizbaz", 6, &addr3)); + EXPECT_EQ(addr3, addr1); + // TODO: figure out what the intent is here + for (int i = 0; i < 10; ++i) { + client2.reset(new TestClient(AsyncUDPSocket::Create(ss_, empty))); + + SocketAddress addr4; + EXPECT_EQ(3, client2->SendTo("foo", 3, addr1)); + EXPECT_TRUE(client1->CheckNextPacket("foo", 3, &addr4)); + EXPECT_EQ(addr4.ipaddr(), addr2.ipaddr()); + + SocketAddress addr5; + EXPECT_EQ(6, client1->SendTo("bizbaz", 6, addr4)); + EXPECT_TRUE(client2->CheckNextPacket("bizbaz", 6, &addr5)); + EXPECT_EQ(addr5, addr1); + + addr2 = addr4; + } +} + +void SocketTest::UdpReadyToSend(const IPAddress& loopback) { + SocketAddress empty = EmptySocketAddressWithFamily(loopback.family()); + // RFC 5737 - The blocks 192.0.2.0/24 (TEST-NET-1) ... are provided for use in + // documentation. + // RFC 3849 - 2001:DB8::/32 as a documentation-only prefix. + std::string dest = (loopback.family() == AF_INET6) ? + "2001:db8::1" : "192.0.2.0"; + SocketAddress test_addr(dest, 2345); + + // Test send + scoped_ptr client( + new TestClient(AsyncUDPSocket::Create(ss_, empty))); + int test_packet_size = 1200; + talk_base::scoped_array test_packet(new char[test_packet_size]); + // Set the send buffer size to the same size as the test packet to have a + // better chance to get EWOULDBLOCK. + int send_buffer_size = test_packet_size; +#if defined(LINUX) + send_buffer_size /= 2; +#endif + client->SetOption(talk_base::Socket::OPT_SNDBUF, send_buffer_size); + + int error = 0; + uint32 start_ms = Time(); + int sent_packet_num = 0; + int expected_error = EWOULDBLOCK; + while (start_ms + kTimeout > Time()) { + int ret = client->SendTo(test_packet.get(), test_packet_size, test_addr); + ++sent_packet_num; + if (ret != test_packet_size) { + error = client->GetError(); + if (error == expected_error) { + LOG(LS_INFO) << "Got expected error code after sending " + << sent_packet_num << " packets."; + break; + } + } + } + EXPECT_EQ(expected_error, error); + EXPECT_FALSE(client->ready_to_send()); + EXPECT_TRUE_WAIT(client->ready_to_send(), kTimeout); + LOG(LS_INFO) << "Got SignalReadyToSend"; +} + +void SocketTest::GetSetOptionsInternal(const IPAddress& loopback) { + talk_base::scoped_ptr socket( + ss_->CreateAsyncSocket(loopback.family(), SOCK_DGRAM)); + socket->Bind(SocketAddress(loopback, 0)); + + // Check SNDBUF/RCVBUF. + const int desired_size = 12345; +#if defined(LINUX) || defined(ANDROID) + // Yes, really. It's in the kernel source. + const int expected_size = desired_size * 2; +#else // !LINUX && !ANDROID + const int expected_size = desired_size; +#endif // !LINUX && !ANDROID + int recv_size = 0; + int send_size = 0; + // get the initial sizes + ASSERT_NE(-1, socket->GetOption(Socket::OPT_RCVBUF, &recv_size)); + ASSERT_NE(-1, socket->GetOption(Socket::OPT_SNDBUF, &send_size)); + // set our desired sizes + ASSERT_NE(-1, socket->SetOption(Socket::OPT_RCVBUF, desired_size)); + ASSERT_NE(-1, socket->SetOption(Socket::OPT_SNDBUF, desired_size)); + // get the sizes again + ASSERT_NE(-1, socket->GetOption(Socket::OPT_RCVBUF, &recv_size)); + ASSERT_NE(-1, socket->GetOption(Socket::OPT_SNDBUF, &send_size)); + // make sure they are right + ASSERT_EQ(expected_size, recv_size); + ASSERT_EQ(expected_size, send_size); + + // Check that we can't set NODELAY on a UDP socket. + int current_nd, desired_nd = 1; + ASSERT_EQ(-1, socket->GetOption(Socket::OPT_NODELAY, ¤t_nd)); + ASSERT_EQ(-1, socket->SetOption(Socket::OPT_NODELAY, desired_nd)); + + // Skip the esimate MTU test for IPv6 for now. + if (loopback.family() != AF_INET6) { + // Try estimating MTU. + talk_base::scoped_ptr + mtu_socket( + ss_->CreateAsyncSocket(loopback.family(), SOCK_DGRAM)); + mtu_socket->Bind(SocketAddress(loopback, 0)); + uint16 mtu; + // should fail until we connect + ASSERT_EQ(-1, mtu_socket->EstimateMTU(&mtu)); + mtu_socket->Connect(SocketAddress(loopback, 0)); +#if defined(WIN32) + // now it should succeed + ASSERT_NE(-1, mtu_socket->EstimateMTU(&mtu)); + ASSERT_GE(mtu, 1492); // should be at least the 1492 "plateau" on localhost +#elif defined(OSX) + // except on OSX, where it's not yet implemented + ASSERT_EQ(-1, mtu_socket->EstimateMTU(&mtu)); +#else + // and the behavior seems unpredictable on Linux, + // failing on the build machine + // but succeeding on my Ubiquity instance. +#endif + } +} + +} // namespace talk_base diff --git a/talk/base/socket_unittest.h b/talk/base/socket_unittest.h new file mode 100644 index 000000000..86c4c9310 --- /dev/null +++ b/talk/base/socket_unittest.h @@ -0,0 +1,105 @@ +/* + * libjingle + * Copyright 2009, 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. + */ + +#ifndef TALK_BASE_SOCKET_UNITTEST_H_ +#define TALK_BASE_SOCKET_UNITTEST_H_ + +#include "talk/base/gunit.h" +#include "talk/base/thread.h" + +namespace talk_base { + +// Generic socket tests, to be used when testing individual socketservers. +// Derive your specific test class from SocketTest, install your +// socketserver, and call the SocketTest test methods. +class SocketTest : public testing::Test { + protected: + SocketTest() : ss_(NULL), kIPv4Loopback(INADDR_LOOPBACK), + kIPv6Loopback(in6addr_loopback) {} + virtual void SetUp() { ss_ = Thread::Current()->socketserver(); } + void TestConnectIPv4(); + void TestConnectIPv6(); + void TestConnectWithDnsLookupIPv4(); + void TestConnectWithDnsLookupIPv6(); + void TestConnectFailIPv4(); + void TestConnectFailIPv6(); + void TestConnectWithDnsLookupFailIPv4(); + void TestConnectWithDnsLookupFailIPv6(); + void TestConnectWithClosedSocketIPv4(); + void TestConnectWithClosedSocketIPv6(); + void TestConnectWhileNotClosedIPv4(); + void TestConnectWhileNotClosedIPv6(); + void TestServerCloseDuringConnectIPv4(); + void TestServerCloseDuringConnectIPv6(); + void TestClientCloseDuringConnectIPv4(); + void TestClientCloseDuringConnectIPv6(); + void TestServerCloseIPv4(); + void TestServerCloseIPv6(); + void TestCloseInClosedCallbackIPv4(); + void TestCloseInClosedCallbackIPv6(); + void TestSocketServerWaitIPv4(); + void TestSocketServerWaitIPv6(); + void TestTcpIPv4(); + void TestTcpIPv6(); + void TestSingleFlowControlCallbackIPv4(); + void TestSingleFlowControlCallbackIPv6(); + void TestUdpIPv4(); + void TestUdpIPv6(); + void TestUdpReadyToSendIPv4(); + void TestUdpReadyToSendIPv6(); + void TestGetSetOptionsIPv4(); + void TestGetSetOptionsIPv6(); + + private: + void ConnectInternal(const IPAddress& loopback); + void ConnectWithDnsLookupInternal(const IPAddress& loopback, + const std::string& host); + void ConnectFailInternal(const IPAddress& loopback); + + void ConnectWithDnsLookupFailInternal(const IPAddress& loopback); + void ConnectWithClosedSocketInternal(const IPAddress& loopback); + void ConnectWhileNotClosedInternal(const IPAddress& loopback); + void ServerCloseDuringConnectInternal(const IPAddress& loopback); + void ClientCloseDuringConnectInternal(const IPAddress& loopback); + void ServerCloseInternal(const IPAddress& loopback); + void CloseInClosedCallbackInternal(const IPAddress& loopback); + void SocketServerWaitInternal(const IPAddress& loopback); + void TcpInternal(const IPAddress& loopback); + void SingleFlowControlCallbackInternal(const IPAddress& loopback); + void UdpInternal(const IPAddress& loopback); + void UdpReadyToSend(const IPAddress& loopback); + void GetSetOptionsInternal(const IPAddress& loopback); + + static const int kTimeout = 5000; // ms + SocketServer* ss_; + const IPAddress kIPv4Loopback; + const IPAddress kIPv6Loopback; +}; + +} // namespace talk_base + +#endif // TALK_BASE_SOCKET_UNITTEST_H_ diff --git a/talk/base/socketadapters.cc b/talk/base/socketadapters.cc new file mode 100644 index 000000000..4361eeca2 --- /dev/null +++ b/talk/base/socketadapters.cc @@ -0,0 +1,910 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#if defined(_MSC_VER) && _MSC_VER < 1300 +#pragma warning(disable:4786) +#endif + +#include +#include + +#ifdef WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#define SECURITY_WIN32 +#include +#endif + +#include "talk/base/bytebuffer.h" +#include "talk/base/common.h" +#include "talk/base/httpcommon.h" +#include "talk/base/logging.h" +#include "talk/base/socketadapters.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" + +#ifdef WIN32 +#include "talk/base/sec_buffer.h" +#endif // WIN32 + +namespace talk_base { + +BufferedReadAdapter::BufferedReadAdapter(AsyncSocket* socket, size_t size) + : AsyncSocketAdapter(socket), buffer_size_(size), + data_len_(0), buffering_(false) { + buffer_ = new char[buffer_size_]; +} + +BufferedReadAdapter::~BufferedReadAdapter() { + delete [] buffer_; +} + +int BufferedReadAdapter::Send(const void *pv, size_t cb) { + if (buffering_) { + // TODO: Spoof error better; Signal Writeable + socket_->SetError(EWOULDBLOCK); + return -1; + } + return AsyncSocketAdapter::Send(pv, cb); +} + +int BufferedReadAdapter::Recv(void *pv, size_t cb) { + if (buffering_) { + socket_->SetError(EWOULDBLOCK); + return -1; + } + + size_t read = 0; + + if (data_len_) { + read = _min(cb, data_len_); + memcpy(pv, buffer_, read); + data_len_ -= read; + if (data_len_ > 0) { + memmove(buffer_, buffer_ + read, data_len_); + } + pv = static_cast(pv) + read; + cb -= read; + } + + // FIX: If cb == 0, we won't generate another read event + + int res = AsyncSocketAdapter::Recv(pv, cb); + if (res < 0) + return res; + + return res + static_cast(read); +} + +void BufferedReadAdapter::BufferInput(bool on) { + buffering_ = on; +} + +void BufferedReadAdapter::OnReadEvent(AsyncSocket * socket) { + ASSERT(socket == socket_); + + if (!buffering_) { + AsyncSocketAdapter::OnReadEvent(socket); + return; + } + + if (data_len_ >= buffer_size_) { + LOG(INFO) << "Input buffer overflow"; + ASSERT(false); + data_len_ = 0; + } + + int len = socket_->Recv(buffer_ + data_len_, buffer_size_ - data_len_); + if (len < 0) { + // TODO: Do something better like forwarding the error to the user. + LOG_ERR(INFO) << "Recv"; + return; + } + + data_len_ += len; + + ProcessInput(buffer_, &data_len_); +} + +/////////////////////////////////////////////////////////////////////////////// + +// This is a SSL v2 CLIENT_HELLO message. +// TODO: Should this have a session id? The response doesn't have a +// certificate, so the hello should have a session id. +static const uint8 kSslClientHello[] = { + 0x80, 0x46, // msg len + 0x01, // CLIENT_HELLO + 0x03, 0x01, // SSL 3.1 + 0x00, 0x2d, // ciphersuite len + 0x00, 0x00, // session id len + 0x00, 0x10, // challenge len + 0x01, 0x00, 0x80, 0x03, 0x00, 0x80, 0x07, 0x00, 0xc0, // ciphersuites + 0x06, 0x00, 0x40, 0x02, 0x00, 0x80, 0x04, 0x00, 0x80, // + 0x00, 0x00, 0x04, 0x00, 0xfe, 0xff, 0x00, 0x00, 0x0a, // + 0x00, 0xfe, 0xfe, 0x00, 0x00, 0x09, 0x00, 0x00, 0x64, // + 0x00, 0x00, 0x62, 0x00, 0x00, 0x03, 0x00, 0x00, 0x06, // + 0x1f, 0x17, 0x0c, 0xa6, 0x2f, 0x00, 0x78, 0xfc, // challenge + 0x46, 0x55, 0x2e, 0xb1, 0x83, 0x39, 0xf1, 0xea // +}; + +// This is a TLSv1 SERVER_HELLO message. +static const uint8 kSslServerHello[] = { + 0x16, // handshake message + 0x03, 0x01, // SSL 3.1 + 0x00, 0x4a, // message len + 0x02, // SERVER_HELLO + 0x00, 0x00, 0x46, // handshake len + 0x03, 0x01, // SSL 3.1 + 0x42, 0x85, 0x45, 0xa7, 0x27, 0xa9, 0x5d, 0xa0, // server random + 0xb3, 0xc5, 0xe7, 0x53, 0xda, 0x48, 0x2b, 0x3f, // + 0xc6, 0x5a, 0xca, 0x89, 0xc1, 0x58, 0x52, 0xa1, // + 0x78, 0x3c, 0x5b, 0x17, 0x46, 0x00, 0x85, 0x3f, // + 0x20, // session id len + 0x0e, 0xd3, 0x06, 0x72, 0x5b, 0x5b, 0x1b, 0x5f, // session id + 0x15, 0xac, 0x13, 0xf9, 0x88, 0x53, 0x9d, 0x9b, // + 0xe8, 0x3d, 0x7b, 0x0c, 0x30, 0x32, 0x6e, 0x38, // + 0x4d, 0xa2, 0x75, 0x57, 0x41, 0x6c, 0x34, 0x5c, // + 0x00, 0x04, // RSA/RC4-128/MD5 + 0x00 // null compression +}; + +AsyncSSLSocket::AsyncSSLSocket(AsyncSocket* socket) + : BufferedReadAdapter(socket, 1024) { +} + +int AsyncSSLSocket::Connect(const SocketAddress& addr) { + // Begin buffering before we connect, so that there isn't a race condition + // between potential senders and receiving the OnConnectEvent signal + BufferInput(true); + return BufferedReadAdapter::Connect(addr); +} + +void AsyncSSLSocket::OnConnectEvent(AsyncSocket * socket) { + ASSERT(socket == socket_); + // TODO: we could buffer output too... + VERIFY(sizeof(kSslClientHello) == + DirectSend(kSslClientHello, sizeof(kSslClientHello))); +} + +void AsyncSSLSocket::ProcessInput(char* data, size_t* len) { + if (*len < sizeof(kSslServerHello)) + return; + + if (memcmp(kSslServerHello, data, sizeof(kSslServerHello)) != 0) { + Close(); + SignalCloseEvent(this, 0); // TODO: error code? + return; + } + + *len -= sizeof(kSslServerHello); + if (*len > 0) { + memmove(data, data + sizeof(kSslServerHello), *len); + } + + bool remainder = (*len > 0); + BufferInput(false); + SignalConnectEvent(this); + + // FIX: if SignalConnect causes the socket to be destroyed, we are in trouble + if (remainder) + SignalReadEvent(this); +} + +AsyncSSLServerSocket::AsyncSSLServerSocket(AsyncSocket* socket) + : BufferedReadAdapter(socket, 1024) { + BufferInput(true); +} + +void AsyncSSLServerSocket::ProcessInput(char* data, size_t* len) { + // We only accept client hello messages. + if (*len < sizeof(kSslClientHello)) { + return; + } + + if (memcmp(kSslClientHello, data, sizeof(kSslClientHello)) != 0) { + Close(); + SignalCloseEvent(this, 0); + return; + } + + *len -= sizeof(kSslClientHello); + + // Clients should not send more data until the handshake is completed. + ASSERT(*len == 0); + + // Send a server hello back to the client. + DirectSend(kSslServerHello, sizeof(kSslServerHello)); + + // Handshake completed for us, redirect input to our parent. + BufferInput(false); +} + +/////////////////////////////////////////////////////////////////////////////// + +AsyncHttpsProxySocket::AsyncHttpsProxySocket(AsyncSocket* socket, + const std::string& user_agent, + const SocketAddress& proxy, + const std::string& username, + const CryptString& password) + : BufferedReadAdapter(socket, 1024), proxy_(proxy), agent_(user_agent), + user_(username), pass_(password), force_connect_(false), state_(PS_ERROR), + context_(0) { +} + +AsyncHttpsProxySocket::~AsyncHttpsProxySocket() { + delete context_; +} + +int AsyncHttpsProxySocket::Connect(const SocketAddress& addr) { + int ret; + LOG(LS_VERBOSE) << "AsyncHttpsProxySocket::Connect(" + << proxy_.ToSensitiveString() << ")"; + dest_ = addr; + state_ = PS_INIT; + if (ShouldIssueConnect()) { + BufferInput(true); + } + ret = BufferedReadAdapter::Connect(proxy_); + // TODO: Set state_ appropriately if Connect fails. + return ret; +} + +SocketAddress AsyncHttpsProxySocket::GetRemoteAddress() const { + return dest_; +} + +int AsyncHttpsProxySocket::Close() { + headers_.clear(); + state_ = PS_ERROR; + dest_.Clear(); + delete context_; + context_ = NULL; + return BufferedReadAdapter::Close(); +} + +Socket::ConnState AsyncHttpsProxySocket::GetState() const { + if (state_ < PS_TUNNEL) { + return CS_CONNECTING; + } else if (state_ == PS_TUNNEL) { + return CS_CONNECTED; + } else { + return CS_CLOSED; + } +} + +void AsyncHttpsProxySocket::OnConnectEvent(AsyncSocket * socket) { + LOG(LS_VERBOSE) << "AsyncHttpsProxySocket::OnConnectEvent"; + if (!ShouldIssueConnect()) { + state_ = PS_TUNNEL; + BufferedReadAdapter::OnConnectEvent(socket); + return; + } + SendRequest(); +} + +void AsyncHttpsProxySocket::OnCloseEvent(AsyncSocket * socket, int err) { + LOG(LS_VERBOSE) << "AsyncHttpsProxySocket::OnCloseEvent(" << err << ")"; + if ((state_ == PS_WAIT_CLOSE) && (err == 0)) { + state_ = PS_ERROR; + Connect(dest_); + } else { + BufferedReadAdapter::OnCloseEvent(socket, err); + } +} + +void AsyncHttpsProxySocket::ProcessInput(char* data, size_t* len) { + size_t start = 0; + for (size_t pos = start; state_ < PS_TUNNEL && pos < *len;) { + if (state_ == PS_SKIP_BODY) { + size_t consume = _min(*len - pos, content_length_); + pos += consume; + start = pos; + content_length_ -= consume; + if (content_length_ == 0) { + EndResponse(); + } + continue; + } + + if (data[pos++] != '\n') + continue; + + size_t len = pos - start - 1; + if ((len > 0) && (data[start + len - 1] == '\r')) + --len; + + data[start + len] = 0; + ProcessLine(data + start, len); + start = pos; + } + + *len -= start; + if (*len > 0) { + memmove(data, data + start, *len); + } + + if (state_ != PS_TUNNEL) + return; + + bool remainder = (*len > 0); + BufferInput(false); + SignalConnectEvent(this); + + // FIX: if SignalConnect causes the socket to be destroyed, we are in trouble + if (remainder) + SignalReadEvent(this); // TODO: signal this?? +} + +bool AsyncHttpsProxySocket::ShouldIssueConnect() const { + // TODO: Think about whether a more sophisticated test + // than dest port == 80 is needed. + return force_connect_ || (dest_.port() != 80); +} + +void AsyncHttpsProxySocket::SendRequest() { + std::stringstream ss; + ss << "CONNECT " << dest_.ToString() << " HTTP/1.0\r\n"; + ss << "User-Agent: " << agent_ << "\r\n"; + ss << "Host: " << dest_.HostAsURIString() << "\r\n"; + ss << "Content-Length: 0\r\n"; + ss << "Proxy-Connection: Keep-Alive\r\n"; + ss << headers_; + ss << "\r\n"; + std::string str = ss.str(); + DirectSend(str.c_str(), str.size()); + state_ = PS_LEADER; + expect_close_ = true; + content_length_ = 0; + headers_.clear(); + + LOG(LS_VERBOSE) << "AsyncHttpsProxySocket >> " << str; +} + +void AsyncHttpsProxySocket::ProcessLine(char * data, size_t len) { + LOG(LS_VERBOSE) << "AsyncHttpsProxySocket << " << data; + + if (len == 0) { + if (state_ == PS_TUNNEL_HEADERS) { + state_ = PS_TUNNEL; + } else if (state_ == PS_ERROR_HEADERS) { + Error(defer_error_); + return; + } else if (state_ == PS_SKIP_HEADERS) { + if (content_length_) { + state_ = PS_SKIP_BODY; + } else { + EndResponse(); + return; + } + } else { + static bool report = false; + if (!unknown_mechanisms_.empty() && !report) { + report = true; + std::string msg( + "Unable to connect to the Google Talk service due to an incompatibility " + "with your proxy.\r\nPlease help us resolve this issue by submitting the " + "following information to us using our technical issue submission form " + "at:\r\n\r\n" + "http://www.google.com/support/talk/bin/request.py\r\n\r\n" + "We apologize for the inconvenience.\r\n\r\n" + "Information to submit to Google: " + ); + //std::string msg("Please report the following information to foo@bar.com:\r\nUnknown methods: "); + msg.append(unknown_mechanisms_); +#ifdef WIN32 + MessageBoxA(0, msg.c_str(), "Oops!", MB_OK); +#endif +#ifdef POSIX + // TODO: Raise a signal so the UI can be separated. + LOG(LS_ERROR) << "Oops!\n\n" << msg; +#endif + } + // Unexpected end of headers + Error(0); + return; + } + } else if (state_ == PS_LEADER) { + unsigned int code; + if (sscanf(data, "HTTP/%*u.%*u %u", &code) != 1) { + Error(0); + return; + } + switch (code) { + case 200: + // connection good! + state_ = PS_TUNNEL_HEADERS; + return; +#if defined(HTTP_STATUS_PROXY_AUTH_REQ) && (HTTP_STATUS_PROXY_AUTH_REQ != 407) +#error Wrong code for HTTP_STATUS_PROXY_AUTH_REQ +#endif + case 407: // HTTP_STATUS_PROXY_AUTH_REQ + state_ = PS_AUTHENTICATE; + return; + default: + defer_error_ = 0; + state_ = PS_ERROR_HEADERS; + return; + } + } else if ((state_ == PS_AUTHENTICATE) + && (_strnicmp(data, "Proxy-Authenticate:", 19) == 0)) { + std::string response, auth_method; + switch (HttpAuthenticate(data + 19, len - 19, + proxy_, "CONNECT", "/", + user_, pass_, context_, response, auth_method)) { + case HAR_IGNORE: + LOG(LS_VERBOSE) << "Ignoring Proxy-Authenticate: " << auth_method; + if (!unknown_mechanisms_.empty()) + unknown_mechanisms_.append(", "); + unknown_mechanisms_.append(auth_method); + break; + case HAR_RESPONSE: + headers_ = "Proxy-Authorization: "; + headers_.append(response); + headers_.append("\r\n"); + state_ = PS_SKIP_HEADERS; + unknown_mechanisms_.clear(); + break; + case HAR_CREDENTIALS: + defer_error_ = SOCKET_EACCES; + state_ = PS_ERROR_HEADERS; + unknown_mechanisms_.clear(); + break; + case HAR_ERROR: + defer_error_ = 0; + state_ = PS_ERROR_HEADERS; + unknown_mechanisms_.clear(); + break; + } + } else if (_strnicmp(data, "Content-Length:", 15) == 0) { + content_length_ = strtoul(data + 15, 0, 0); + } else if (_strnicmp(data, "Proxy-Connection: Keep-Alive", 28) == 0) { + expect_close_ = false; + /* + } else if (_strnicmp(data, "Connection: close", 17) == 0) { + expect_close_ = true; + */ + } +} + +void AsyncHttpsProxySocket::EndResponse() { + if (!expect_close_) { + SendRequest(); + return; + } + + // No point in waiting for the server to close... let's close now + // TODO: Refactor out PS_WAIT_CLOSE + state_ = PS_WAIT_CLOSE; + BufferedReadAdapter::Close(); + OnCloseEvent(this, 0); +} + +void AsyncHttpsProxySocket::Error(int error) { + BufferInput(false); + Close(); + SetError(error); + SignalCloseEvent(this, error); +} + +/////////////////////////////////////////////////////////////////////////////// + +AsyncSocksProxySocket::AsyncSocksProxySocket(AsyncSocket* socket, + const SocketAddress& proxy, + const std::string& username, + const CryptString& password) + : BufferedReadAdapter(socket, 1024), state_(SS_ERROR), proxy_(proxy), + user_(username), pass_(password) { +} + +int AsyncSocksProxySocket::Connect(const SocketAddress& addr) { + int ret; + dest_ = addr; + state_ = SS_INIT; + BufferInput(true); + ret = BufferedReadAdapter::Connect(proxy_); + // TODO: Set state_ appropriately if Connect fails. + return ret; +} + +SocketAddress AsyncSocksProxySocket::GetRemoteAddress() const { + return dest_; +} + +int AsyncSocksProxySocket::Close() { + state_ = SS_ERROR; + dest_.Clear(); + return BufferedReadAdapter::Close(); +} + +Socket::ConnState AsyncSocksProxySocket::GetState() const { + if (state_ < SS_TUNNEL) { + return CS_CONNECTING; + } else if (state_ == SS_TUNNEL) { + return CS_CONNECTED; + } else { + return CS_CLOSED; + } +} + +void AsyncSocksProxySocket::OnConnectEvent(AsyncSocket* socket) { + SendHello(); +} + +void AsyncSocksProxySocket::ProcessInput(char* data, size_t* len) { + ASSERT(state_ < SS_TUNNEL); + + ByteBuffer response(data, *len); + + if (state_ == SS_HELLO) { + uint8 ver, method; + if (!response.ReadUInt8(&ver) || + !response.ReadUInt8(&method)) + return; + + if (ver != 5) { + Error(0); + return; + } + + if (method == 0) { + SendConnect(); + } else if (method == 2) { + SendAuth(); + } else { + Error(0); + return; + } + } else if (state_ == SS_AUTH) { + uint8 ver, status; + if (!response.ReadUInt8(&ver) || + !response.ReadUInt8(&status)) + return; + + if ((ver != 1) || (status != 0)) { + Error(SOCKET_EACCES); + return; + } + + SendConnect(); + } else if (state_ == SS_CONNECT) { + uint8 ver, rep, rsv, atyp; + if (!response.ReadUInt8(&ver) || + !response.ReadUInt8(&rep) || + !response.ReadUInt8(&rsv) || + !response.ReadUInt8(&atyp)) + return; + + if ((ver != 5) || (rep != 0)) { + Error(0); + return; + } + + uint16 port; + if (atyp == 1) { + uint32 addr; + if (!response.ReadUInt32(&addr) || + !response.ReadUInt16(&port)) + return; + LOG(LS_VERBOSE) << "Bound on " << addr << ":" << port; + } else if (atyp == 3) { + uint8 len; + std::string addr; + if (!response.ReadUInt8(&len) || + !response.ReadString(&addr, len) || + !response.ReadUInt16(&port)) + return; + LOG(LS_VERBOSE) << "Bound on " << addr << ":" << port; + } else if (atyp == 4) { + std::string addr; + if (!response.ReadString(&addr, 16) || + !response.ReadUInt16(&port)) + return; + LOG(LS_VERBOSE) << "Bound on :" << port; + } else { + Error(0); + return; + } + + state_ = SS_TUNNEL; + } + + // Consume parsed data + *len = response.Length(); + memcpy(data, response.Data(), *len); + + if (state_ != SS_TUNNEL) + return; + + bool remainder = (*len > 0); + BufferInput(false); + SignalConnectEvent(this); + + // FIX: if SignalConnect causes the socket to be destroyed, we are in trouble + if (remainder) + SignalReadEvent(this); // TODO: signal this?? +} + +void AsyncSocksProxySocket::SendHello() { + ByteBuffer request; + request.WriteUInt8(5); // Socks Version + if (user_.empty()) { + request.WriteUInt8(1); // Authentication Mechanisms + request.WriteUInt8(0); // No authentication + } else { + request.WriteUInt8(2); // Authentication Mechanisms + request.WriteUInt8(0); // No authentication + request.WriteUInt8(2); // Username/Password + } + DirectSend(request.Data(), request.Length()); + state_ = SS_HELLO; +} + +void AsyncSocksProxySocket::SendAuth() { + ByteBuffer request; + request.WriteUInt8(1); // Negotiation Version + request.WriteUInt8(static_cast(user_.size())); + request.WriteString(user_); // Username + request.WriteUInt8(static_cast(pass_.GetLength())); + size_t len = pass_.GetLength() + 1; + char * sensitive = new char[len]; + pass_.CopyTo(sensitive, true); + request.WriteString(sensitive); // Password + memset(sensitive, 0, len); + delete [] sensitive; + DirectSend(request.Data(), request.Length()); + state_ = SS_AUTH; +} + +void AsyncSocksProxySocket::SendConnect() { + ByteBuffer request; + request.WriteUInt8(5); // Socks Version + request.WriteUInt8(1); // CONNECT + request.WriteUInt8(0); // Reserved + if (dest_.IsUnresolved()) { + std::string hostname = dest_.hostname(); + request.WriteUInt8(3); // DOMAINNAME + request.WriteUInt8(static_cast(hostname.size())); + request.WriteString(hostname); // Destination Hostname + } else { + request.WriteUInt8(1); // IPV4 + request.WriteUInt32(dest_.ip()); // Destination IP + } + request.WriteUInt16(dest_.port()); // Destination Port + DirectSend(request.Data(), request.Length()); + state_ = SS_CONNECT; +} + +void AsyncSocksProxySocket::Error(int error) { + state_ = SS_ERROR; + BufferInput(false); + Close(); + SetError(SOCKET_EACCES); + SignalCloseEvent(this, error); +} + +AsyncSocksProxyServerSocket::AsyncSocksProxyServerSocket(AsyncSocket* socket) + : AsyncProxyServerSocket(socket, kBufferSize), state_(SS_HELLO) { + BufferInput(true); +} + +void AsyncSocksProxyServerSocket::ProcessInput(char* data, size_t* len) { + // TODO: See if the whole message has arrived + ASSERT(state_ < SS_CONNECT_PENDING); + + ByteBuffer response(data, *len); + if (state_ == SS_HELLO) { + HandleHello(&response); + } else if (state_ == SS_AUTH) { + HandleAuth(&response); + } else if (state_ == SS_CONNECT) { + HandleConnect(&response); + } + + // Consume parsed data + *len = response.Length(); + memcpy(data, response.Data(), *len); +} + +void AsyncSocksProxyServerSocket::DirectSend(const ByteBuffer& buf) { + BufferedReadAdapter::DirectSend(buf.Data(), buf.Length()); +} + +void AsyncSocksProxyServerSocket::HandleHello(ByteBuffer* request) { + uint8 ver, num_methods; + if (!request->ReadUInt8(&ver) || + !request->ReadUInt8(&num_methods)) { + Error(0); + return; + } + + if (ver != 5) { + Error(0); + return; + } + + // Handle either no-auth (0) or user/pass auth (2) + uint8 method = 0xFF; + if (num_methods > 0 && !request->ReadUInt8(&method)) { + Error(0); + return; + } + + // TODO: Ask the server which method to use. + SendHelloReply(method); + if (method == 0) { + state_ = SS_CONNECT; + } else if (method == 2) { + state_ = SS_AUTH; + } else { + state_ = SS_ERROR; + } +} + +void AsyncSocksProxyServerSocket::SendHelloReply(int method) { + ByteBuffer response; + response.WriteUInt8(5); // Socks Version + response.WriteUInt8(method); // Auth method + DirectSend(response); +} + +void AsyncSocksProxyServerSocket::HandleAuth(ByteBuffer* request) { + uint8 ver, user_len, pass_len; + std::string user, pass; + if (!request->ReadUInt8(&ver) || + !request->ReadUInt8(&user_len) || + !request->ReadString(&user, user_len) || + !request->ReadUInt8(&pass_len) || + !request->ReadString(&pass, pass_len)) { + Error(0); + return; + } + + // TODO: Allow for checking of credentials. + SendAuthReply(0); + state_ = SS_CONNECT; +} + +void AsyncSocksProxyServerSocket::SendAuthReply(int result) { + ByteBuffer response; + response.WriteUInt8(1); // Negotiation Version + response.WriteUInt8(result); + DirectSend(response); +} + +void AsyncSocksProxyServerSocket::HandleConnect(ByteBuffer* request) { + uint8 ver, command, reserved, addr_type; + uint32 ip; + uint16 port; + if (!request->ReadUInt8(&ver) || + !request->ReadUInt8(&command) || + !request->ReadUInt8(&reserved) || + !request->ReadUInt8(&addr_type) || + !request->ReadUInt32(&ip) || + !request->ReadUInt16(&port)) { + Error(0); + return; + } + + if (ver != 5 || command != 1 || + reserved != 0 || addr_type != 1) { + Error(0); + return; + } + + SignalConnectRequest(this, SocketAddress(ip, port)); + state_ = SS_CONNECT_PENDING; +} + +void AsyncSocksProxyServerSocket::SendConnectResult(int result, + const SocketAddress& addr) { + if (state_ != SS_CONNECT_PENDING) + return; + + ByteBuffer response; + response.WriteUInt8(5); // Socks version + response.WriteUInt8((result != 0)); // 0x01 is generic error + response.WriteUInt8(0); // reserved + response.WriteUInt8(1); // IPv4 address + response.WriteUInt32(addr.ip()); + response.WriteUInt16(addr.port()); + DirectSend(response); + BufferInput(false); + state_ = SS_TUNNEL; +} + +void AsyncSocksProxyServerSocket::Error(int error) { + state_ = SS_ERROR; + BufferInput(false); + Close(); + SetError(SOCKET_EACCES); + SignalCloseEvent(this, error); +} + +/////////////////////////////////////////////////////////////////////////////// + +LoggingSocketAdapter::LoggingSocketAdapter(AsyncSocket* socket, + LoggingSeverity level, + const char * label, bool hex_mode) + : AsyncSocketAdapter(socket), level_(level), hex_mode_(hex_mode) { + label_.append("["); + label_.append(label); + label_.append("]"); +} + +int LoggingSocketAdapter::Send(const void *pv, size_t cb) { + int res = AsyncSocketAdapter::Send(pv, cb); + if (res > 0) + LogMultiline(level_, label_.c_str(), false, pv, res, hex_mode_, &lms_); + return res; +} + +int LoggingSocketAdapter::SendTo(const void *pv, size_t cb, + const SocketAddress& addr) { + int res = AsyncSocketAdapter::SendTo(pv, cb, addr); + if (res > 0) + LogMultiline(level_, label_.c_str(), false, pv, res, hex_mode_, &lms_); + return res; +} + +int LoggingSocketAdapter::Recv(void *pv, size_t cb) { + int res = AsyncSocketAdapter::Recv(pv, cb); + if (res > 0) + LogMultiline(level_, label_.c_str(), true, pv, res, hex_mode_, &lms_); + return res; +} + +int LoggingSocketAdapter::RecvFrom(void *pv, size_t cb, SocketAddress *paddr) { + int res = AsyncSocketAdapter::RecvFrom(pv, cb, paddr); + if (res > 0) + LogMultiline(level_, label_.c_str(), true, pv, res, hex_mode_, &lms_); + return res; +} + +int LoggingSocketAdapter::Close() { + LogMultiline(level_, label_.c_str(), false, NULL, 0, hex_mode_, &lms_); + LogMultiline(level_, label_.c_str(), true, NULL, 0, hex_mode_, &lms_); + LOG_V(level_) << label_ << " Closed locally"; + return socket_->Close(); +} + +void LoggingSocketAdapter::OnConnectEvent(AsyncSocket * socket) { + LOG_V(level_) << label_ << " Connected"; + AsyncSocketAdapter::OnConnectEvent(socket); +} + +void LoggingSocketAdapter::OnCloseEvent(AsyncSocket * socket, int err) { + LogMultiline(level_, label_.c_str(), false, NULL, 0, hex_mode_, &lms_); + LogMultiline(level_, label_.c_str(), true, NULL, 0, hex_mode_, &lms_); + LOG_V(level_) << label_ << " Closed with error: " << err; + AsyncSocketAdapter::OnCloseEvent(socket, err); +} + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/socketadapters.h b/talk/base/socketadapters.h new file mode 100644 index 000000000..320da6f35 --- /dev/null +++ b/talk/base/socketadapters.h @@ -0,0 +1,261 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_SOCKETADAPTERS_H_ +#define TALK_BASE_SOCKETADAPTERS_H_ + +#include +#include + +#include "talk/base/asyncsocket.h" +#include "talk/base/cryptstring.h" +#include "talk/base/logging.h" + +namespace talk_base { + +struct HttpAuthContext; +class ByteBuffer; + +/////////////////////////////////////////////////////////////////////////////// + +// Implements a socket adapter that can buffer and process data internally, +// as in the case of connecting to a proxy, where you must speak the proxy +// protocol before commencing normal socket behavior. +class BufferedReadAdapter : public AsyncSocketAdapter { + public: + BufferedReadAdapter(AsyncSocket* socket, size_t buffer_size); + virtual ~BufferedReadAdapter(); + + virtual int Send(const void* pv, size_t cb); + virtual int Recv(void* pv, size_t cb); + + protected: + int DirectSend(const void* pv, size_t cb) { + return AsyncSocketAdapter::Send(pv, cb); + } + + void BufferInput(bool on = true); + virtual void ProcessInput(char* data, size_t* len) = 0; + + virtual void OnReadEvent(AsyncSocket * socket); + + private: + char * buffer_; + size_t buffer_size_, data_len_; + bool buffering_; + DISALLOW_EVIL_CONSTRUCTORS(BufferedReadAdapter); +}; + +/////////////////////////////////////////////////////////////////////////////// + +// Interface for implementing proxy server sockets. +class AsyncProxyServerSocket : public BufferedReadAdapter { + public: + AsyncProxyServerSocket(AsyncSocket* socket, size_t buffer_size) + : BufferedReadAdapter(socket, buffer_size) {} + sigslot::signal2 SignalConnectRequest; + virtual void SendConnectResult(int err, const SocketAddress& addr) = 0; +}; + +/////////////////////////////////////////////////////////////////////////////// + +// Implements a socket adapter that performs the client side of a +// fake SSL handshake. Used for "ssltcp" P2P functionality. +class AsyncSSLSocket : public BufferedReadAdapter { + public: + explicit AsyncSSLSocket(AsyncSocket* socket); + + virtual int Connect(const SocketAddress& addr); + + protected: + virtual void OnConnectEvent(AsyncSocket* socket); + virtual void ProcessInput(char* data, size_t* len); + DISALLOW_EVIL_CONSTRUCTORS(AsyncSSLSocket); +}; + +// Implements a socket adapter that performs the server side of a +// fake SSL handshake. Used when implementing a relay server that does "ssltcp". +class AsyncSSLServerSocket : public BufferedReadAdapter { + public: + explicit AsyncSSLServerSocket(AsyncSocket* socket); + + protected: + virtual void ProcessInput(char* data, size_t* len); + DISALLOW_EVIL_CONSTRUCTORS(AsyncSSLServerSocket); +}; + +/////////////////////////////////////////////////////////////////////////////// + +// Implements a socket adapter that speaks the HTTP/S proxy protocol. +class AsyncHttpsProxySocket : public BufferedReadAdapter { + public: + AsyncHttpsProxySocket(AsyncSocket* socket, const std::string& user_agent, + const SocketAddress& proxy, + const std::string& username, const CryptString& password); + virtual ~AsyncHttpsProxySocket(); + + // If connect is forced, the adapter will always issue an HTTP CONNECT to the + // target address. Otherwise, it will connect only if the destination port + // is not port 80. + void SetForceConnect(bool force) { force_connect_ = force; } + + virtual int Connect(const SocketAddress& addr); + virtual SocketAddress GetRemoteAddress() const; + virtual int Close(); + virtual ConnState GetState() const; + + protected: + virtual void OnConnectEvent(AsyncSocket* socket); + virtual void OnCloseEvent(AsyncSocket* socket, int err); + virtual void ProcessInput(char* data, size_t* len); + + bool ShouldIssueConnect() const; + void SendRequest(); + void ProcessLine(char* data, size_t len); + void EndResponse(); + void Error(int error); + + private: + SocketAddress proxy_, dest_; + std::string agent_, user_, headers_; + CryptString pass_; + bool force_connect_; + size_t content_length_; + int defer_error_; + bool expect_close_; + enum ProxyState { + PS_INIT, PS_LEADER, PS_AUTHENTICATE, PS_SKIP_HEADERS, PS_ERROR_HEADERS, + PS_TUNNEL_HEADERS, PS_SKIP_BODY, PS_TUNNEL, PS_WAIT_CLOSE, PS_ERROR + } state_; + HttpAuthContext * context_; + std::string unknown_mechanisms_; + DISALLOW_EVIL_CONSTRUCTORS(AsyncHttpsProxySocket); +}; + +/* TODO: Implement this. +class AsyncHttpsProxyServerSocket : public AsyncProxyServerSocket { + public: + explicit AsyncHttpsProxyServerSocket(AsyncSocket* socket); + + private: + virtual void ProcessInput(char * data, size_t& len); + void Error(int error); + DISALLOW_EVIL_CONSTRUCTORS(AsyncHttpsProxyServerSocket); +}; +*/ + +/////////////////////////////////////////////////////////////////////////////// + +// Implements a socket adapter that speaks the SOCKS proxy protocol. +class AsyncSocksProxySocket : public BufferedReadAdapter { + public: + AsyncSocksProxySocket(AsyncSocket* socket, const SocketAddress& proxy, + const std::string& username, const CryptString& password); + + virtual int Connect(const SocketAddress& addr); + virtual SocketAddress GetRemoteAddress() const; + virtual int Close(); + virtual ConnState GetState() const; + + protected: + virtual void OnConnectEvent(AsyncSocket* socket); + virtual void ProcessInput(char* data, size_t* len); + + void SendHello(); + void SendConnect(); + void SendAuth(); + void Error(int error); + + private: + enum State { + SS_INIT, SS_HELLO, SS_AUTH, SS_CONNECT, SS_TUNNEL, SS_ERROR + }; + State state_; + SocketAddress proxy_, dest_; + std::string user_; + CryptString pass_; + DISALLOW_EVIL_CONSTRUCTORS(AsyncSocksProxySocket); +}; + +// Implements a proxy server socket for the SOCKS protocol. +class AsyncSocksProxyServerSocket : public AsyncProxyServerSocket { + public: + explicit AsyncSocksProxyServerSocket(AsyncSocket* socket); + + private: + virtual void ProcessInput(char* data, size_t* len); + void DirectSend(const ByteBuffer& buf); + + void HandleHello(ByteBuffer* request); + void SendHelloReply(int method); + void HandleAuth(ByteBuffer* request); + void SendAuthReply(int result); + void HandleConnect(ByteBuffer* request); + virtual void SendConnectResult(int result, const SocketAddress& addr); + + void Error(int error); + + static const int kBufferSize = 1024; + enum State { + SS_HELLO, SS_AUTH, SS_CONNECT, SS_CONNECT_PENDING, SS_TUNNEL, SS_ERROR + }; + State state_; + DISALLOW_EVIL_CONSTRUCTORS(AsyncSocksProxyServerSocket); +}; + +/////////////////////////////////////////////////////////////////////////////// + +// Implements a socket adapter that logs everything that it sends and receives. +class LoggingSocketAdapter : public AsyncSocketAdapter { + public: + LoggingSocketAdapter(AsyncSocket* socket, LoggingSeverity level, + const char * label, bool hex_mode = false); + + virtual int Send(const void *pv, size_t cb); + virtual int SendTo(const void *pv, size_t cb, const SocketAddress& addr); + virtual int Recv(void *pv, size_t cb); + virtual int RecvFrom(void *pv, size_t cb, SocketAddress *paddr); + virtual int Close(); + + protected: + virtual void OnConnectEvent(AsyncSocket * socket); + virtual void OnCloseEvent(AsyncSocket * socket, int err); + + private: + LoggingSeverity level_; + std::string label_; + bool hex_mode_; + LogMultilineState lms_; + DISALLOW_EVIL_CONSTRUCTORS(LoggingSocketAdapter); +}; + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_SOCKETADAPTERS_H_ diff --git a/talk/base/socketaddress.cc b/talk/base/socketaddress.cc new file mode 100644 index 000000000..193a23282 --- /dev/null +++ b/talk/base/socketaddress.cc @@ -0,0 +1,398 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/socketaddress.h" + +#ifdef POSIX +#include +#include +#include +#if defined(OPENBSD) +#include +#endif +#include +#include +#include +#include +#endif + +#include + +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/nethelpers.h" + +#ifdef WIN32 +#include "talk/base/win32.h" +#endif + +namespace talk_base { + +SocketAddress::SocketAddress() { + Clear(); +} + +SocketAddress::SocketAddress(const std::string& hostname, int port) { + SetIP(hostname); + SetPort(port); +} + +SocketAddress::SocketAddress(uint32 ip_as_host_order_integer, int port) { + SetIP(IPAddress(ip_as_host_order_integer)); + SetPort(port); +} + +SocketAddress::SocketAddress(const IPAddress& ip, int port) { + SetIP(ip); + SetPort(port); +} + +SocketAddress::SocketAddress(const SocketAddress& addr) { + this->operator=(addr); +} + +void SocketAddress::Clear() { + hostname_.clear(); + literal_ = false; + ip_ = IPAddress(); + port_ = 0; + scope_id_ = 0; +} + +bool SocketAddress::IsNil() const { + return hostname_.empty() && IPIsUnspec(ip_) && 0 == port_; +} + +bool SocketAddress::IsComplete() const { + return (!IPIsAny(ip_)) && (0 != port_); +} + +SocketAddress& SocketAddress::operator=(const SocketAddress& addr) { + hostname_ = addr.hostname_; + ip_ = addr.ip_; + port_ = addr.port_; + literal_ = addr.literal_; + scope_id_ = addr.scope_id_; + return *this; +} + +void SocketAddress::SetIP(uint32 ip_as_host_order_integer) { + hostname_.clear(); + literal_ = false; + ip_ = IPAddress(ip_as_host_order_integer); + scope_id_ = 0; +} + +void SocketAddress::SetIP(const IPAddress& ip) { + hostname_.clear(); + literal_ = false; + ip_ = ip; + scope_id_ = 0; +} + +void SocketAddress::SetIP(const std::string& hostname) { + hostname_ = hostname; + literal_ = IPFromString(hostname, &ip_); + if (!literal_) { + ip_ = IPAddress(); + } + scope_id_ = 0; +} + +void SocketAddress::SetResolvedIP(uint32 ip_as_host_order_integer) { + ip_ = IPAddress(ip_as_host_order_integer); + scope_id_ = 0; +} + +void SocketAddress::SetResolvedIP(const IPAddress& ip) { + ip_ = ip; + scope_id_ = 0; +} + +void SocketAddress::SetPort(int port) { + ASSERT((0 <= port) && (port < 65536)); + port_ = port; +} + +uint32 SocketAddress::ip() const { + return ip_.v4AddressAsHostOrderInteger(); +} + +const IPAddress& SocketAddress::ipaddr() const { + return ip_; +} + +uint16 SocketAddress::port() const { + return port_; +} + +std::string SocketAddress::HostAsURIString() const { + // If the hostname was a literal IP string, it may need to have square + // brackets added (for SocketAddress::ToString()). + if (!literal_ && !hostname_.empty()) + return hostname_; + if (ip_.family() == AF_INET6) { + return "[" + ip_.ToString() + "]"; + } else { + return ip_.ToString(); + } +} + +std::string SocketAddress::HostAsSensitiveURIString() const { + // If the hostname was a literal IP string, it may need to have square + // brackets added (for SocketAddress::ToString()). + if (!literal_ && !hostname_.empty()) + return hostname_; + if (ip_.family() == AF_INET6) { + return "[" + ip_.ToSensitiveString() + "]"; + } else { + return ip_.ToSensitiveString(); + } +} + +std::string SocketAddress::PortAsString() const { + std::ostringstream ost; + ost << port_; + return ost.str(); +} + +std::string SocketAddress::ToString() const { + std::ostringstream ost; + ost << *this; + return ost.str(); +} + +std::string SocketAddress::ToSensitiveString() const { + std::ostringstream ost; + ost << HostAsSensitiveURIString() << ":" << port(); + return ost.str(); +} + +bool SocketAddress::FromString(const std::string& str) { + if (str.at(0) == '[') { + std::string::size_type closebracket = str.rfind(']'); + if (closebracket != std::string::npos) { + std::string::size_type colon = str.find(':', closebracket); + if (colon != std::string::npos && colon > closebracket) { + SetPort(strtoul(str.substr(colon + 1).c_str(), NULL, 10)); + SetIP(str.substr(1, closebracket - 1)); + } else { + return false; + } + } + } else { + std::string::size_type pos = str.find(':'); + if (std::string::npos == pos) + return false; + SetPort(strtoul(str.substr(pos + 1).c_str(), NULL, 10)); + SetIP(str.substr(0, pos)); + } + return true; +} + +std::ostream& operator<<(std::ostream& os, const SocketAddress& addr) { + os << addr.HostAsURIString() << ":" << addr.port(); + return os; +} + +bool SocketAddress::IsAnyIP() const { + return IPIsAny(ip_); +} + +bool SocketAddress::IsLoopbackIP() const { + return IPIsLoopback(ip_) || (IPIsAny(ip_) && + 0 == strcmp(hostname_.c_str(), "localhost")); +} + +bool SocketAddress::IsPrivateIP() const { + return IPIsPrivate(ip_); +} + +bool SocketAddress::IsUnresolvedIP() const { + return IPIsUnspec(ip_) && !literal_ && !hostname_.empty(); +} + +bool SocketAddress::operator==(const SocketAddress& addr) const { + return EqualIPs(addr) && EqualPorts(addr); +} + +bool SocketAddress::operator<(const SocketAddress& addr) const { + if (ip_ < addr.ip_) + return true; + else if (addr.ip_ < ip_) + return false; + + // We only check hostnames if both IPs are zero. This matches EqualIPs() + if (addr.IsAnyIP()) { + if (hostname_ < addr.hostname_) + return true; + else if (addr.hostname_ < hostname_) + return false; + } + + return port_ < addr.port_; +} + +bool SocketAddress::EqualIPs(const SocketAddress& addr) const { + return (ip_ == addr.ip_) && + ((!IPIsAny(ip_)) || (hostname_ == addr.hostname_)); +} + +bool SocketAddress::EqualPorts(const SocketAddress& addr) const { + return (port_ == addr.port_); +} + +size_t SocketAddress::Hash() const { + size_t h = 0; + h ^= HashIP(ip_); + h ^= port_ | (port_ << 16); + return h; +} + +void SocketAddress::ToSockAddr(sockaddr_in* saddr) const { + memset(saddr, 0, sizeof(*saddr)); + if (ip_.family() != AF_INET) { + saddr->sin_family = AF_UNSPEC; + return; + } + saddr->sin_family = AF_INET; + saddr->sin_port = HostToNetwork16(port_); + if (IPIsAny(ip_)) { + saddr->sin_addr.s_addr = INADDR_ANY; + } else { + saddr->sin_addr = ip_.ipv4_address(); + } +} + +bool SocketAddress::FromSockAddr(const sockaddr_in& saddr) { + if (saddr.sin_family != AF_INET) + return false; + SetIP(NetworkToHost32(saddr.sin_addr.s_addr)); + SetPort(NetworkToHost16(saddr.sin_port)); + literal_ = false; + return true; +} + +static size_t ToSockAddrStorageHelper(sockaddr_storage* addr, + IPAddress ip, int port, int scope_id) { + memset(addr, 0, sizeof(sockaddr_storage)); + addr->ss_family = ip.family(); + if (addr->ss_family == AF_INET6) { + sockaddr_in6* saddr = reinterpret_cast(addr); + saddr->sin6_addr = ip.ipv6_address(); + saddr->sin6_port = HostToNetwork16(port); + saddr->sin6_scope_id = scope_id; + return sizeof(sockaddr_in6); + } else if (addr->ss_family == AF_INET) { + sockaddr_in* saddr = reinterpret_cast(addr); + saddr->sin_addr = ip.ipv4_address(); + saddr->sin_port = HostToNetwork16(port); + return sizeof(sockaddr_in); + } + return 0; +} + +size_t SocketAddress::ToDualStackSockAddrStorage(sockaddr_storage *addr) const { + return ToSockAddrStorageHelper(addr, ip_.AsIPv6Address(), port_, scope_id_); +} + +size_t SocketAddress::ToSockAddrStorage(sockaddr_storage* addr) const { + return ToSockAddrStorageHelper(addr, ip_, port_, scope_id_); +} + +std::string SocketAddress::IPToString(uint32 ip_as_host_order_integer) { + return IPAddress(ip_as_host_order_integer).ToString(); +} + +std::string IPToSensitiveString(uint32 ip_as_host_order_integer) { + return IPAddress(ip_as_host_order_integer).ToSensitiveString(); +} + +bool SocketAddress::StringToIP(const std::string& hostname, uint32* ip) { + in_addr addr; + if (talk_base::inet_pton(AF_INET, hostname.c_str(), &addr) == 0) + return false; + *ip = NetworkToHost32(addr.s_addr); + return true; +} + +bool SocketAddress::StringToIP(const std::string& hostname, IPAddress* ip) { + in_addr addr4; + if (talk_base::inet_pton(AF_INET, hostname.c_str(), &addr4) > 0) { + if (ip) { + *ip = IPAddress(addr4); + } + return true; + } + + in6_addr addr6; + if (talk_base::inet_pton(AF_INET6, hostname.c_str(), &addr6) > 0) { + if (ip) { + *ip = IPAddress(addr6); + } + return true; + } + return false; +} + +uint32 SocketAddress::StringToIP(const std::string& hostname) { + uint32 ip = 0; + StringToIP(hostname, &ip); + return ip; +} + +bool SocketAddressFromSockAddrStorage(const sockaddr_storage& addr, + SocketAddress* out) { + if (!out) { + return false; + } + if (addr.ss_family == AF_INET) { + const sockaddr_in* saddr = reinterpret_cast(&addr); + *out = SocketAddress(IPAddress(saddr->sin_addr), + NetworkToHost16(saddr->sin_port)); + return true; + } else if (addr.ss_family == AF_INET6) { + const sockaddr_in6* saddr = reinterpret_cast(&addr); + *out = SocketAddress(IPAddress(saddr->sin6_addr), + NetworkToHost16(saddr->sin6_port)); + out->SetScopeID(saddr->sin6_scope_id); + return true; + } + return false; +} + +SocketAddress EmptySocketAddressWithFamily(int family) { + if (family == AF_INET) { + return SocketAddress(IPAddress(INADDR_ANY), 0); + } else if (family == AF_INET6) { + return SocketAddress(IPAddress(in6addr_any), 0); + } + return SocketAddress(); +} + +} // namespace talk_base diff --git a/talk/base/socketaddress.h b/talk/base/socketaddress.h new file mode 100644 index 000000000..08f2659a3 --- /dev/null +++ b/talk/base/socketaddress.h @@ -0,0 +1,231 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_SOCKETADDRESS_H_ +#define TALK_BASE_SOCKETADDRESS_H_ + +#include +#include +#include +#include "talk/base/basictypes.h" +#include "talk/base/ipaddress.h" + +#undef SetPort + +struct sockaddr_in; +struct sockaddr_storage; + +namespace talk_base { + +// Records an IP address and port. +class SocketAddress { + public: + // Creates a nil address. + SocketAddress(); + + // Creates the address with the given host and port. Host may be a + // literal IP string or a hostname to be resolved later. + SocketAddress(const std::string& hostname, int port); + + // Creates the address with the given IP and port. + // IP is given as an integer in host byte order. V4 only, to be deprecated. + SocketAddress(uint32 ip_as_host_order_integer, int port); + + // Creates the address with the given IP and port. + SocketAddress(const IPAddress& ip, int port); + + // Creates a copy of the given address. + SocketAddress(const SocketAddress& addr); + + // Resets to the nil address. + void Clear(); + + // Determines if this is a nil address (empty hostname, any IP, null port) + bool IsNil() const; + + // Returns true if ip and port are set. + bool IsComplete() const; + + // Replaces our address with the given one. + SocketAddress& operator=(const SocketAddress& addr); + + // Changes the IP of this address to the given one, and clears the hostname + // IP is given as an integer in host byte order. V4 only, to be deprecated.. + void SetIP(uint32 ip_as_host_order_integer); + + // Changes the IP of this address to the given one, and clears the hostname. + void SetIP(const IPAddress& ip); + + // Changes the hostname of this address to the given one. + // Does not resolve the address; use Resolve to do so. + void SetIP(const std::string& hostname); + + // Sets the IP address while retaining the hostname. Useful for bypassing + // DNS for a pre-resolved IP. + // IP is given as an integer in host byte order. V4 only, to be deprecated. + void SetResolvedIP(uint32 ip_as_host_order_integer); + + // Sets the IP address while retaining the hostname. Useful for bypassing + // DNS for a pre-resolved IP. + void SetResolvedIP(const IPAddress& ip); + + // Changes the port of this address to the given one. + void SetPort(int port); + + // Returns the hostname. + const std::string& hostname() const { return hostname_; } + + // Returns the IP address as a host byte order integer. + // Returns 0 for non-v4 addresses. + uint32 ip() const; + + const IPAddress& ipaddr() const; + + int family() const {return ip_.family(); } + + // Returns the port part of this address. + uint16 port() const; + + // Returns the scope ID associated with this address. Scope IDs are a + // necessary addition to IPv6 link-local addresses, with different network + // interfaces having different scope-ids for their link-local addresses. + // IPv4 address do not have scope_ids and sockaddr_in structures do not have + // a field for them. + int scope_id() const {return scope_id_; } + void SetScopeID(int id) { scope_id_ = id; } + + // Returns the 'host' portion of the address (hostname or IP) in a form + // suitable for use in a URI. If both IP and hostname are present, hostname + // is preferred. IPv6 addresses are enclosed in square brackets ('[' and ']'). + std::string HostAsURIString() const; + + // Same as HostAsURIString but anonymizes IP addresses by hiding the last + // part. + std::string HostAsSensitiveURIString() const; + + // Returns the port as a string. + std::string PortAsString() const; + + // Returns hostname:port or [hostname]:port. + std::string ToString() const; + + // Same as ToString but anonymizes it by hiding the last part. + std::string ToSensitiveString() const; + + // Parses hostname:port and [hostname]:port. + bool FromString(const std::string& str); + + friend std::ostream& operator<<(std::ostream& os, const SocketAddress& addr); + + // Determines whether this represents a missing / any IP address. + // That is, 0.0.0.0 or ::. + // Hostname and/or port may be set. + bool IsAnyIP() const; + inline bool IsAny() const { return IsAnyIP(); } // deprecated + + // Determines whether the IP address refers to a loopback address. + // For v4 addresses this means the address is in the range 127.0.0.0/8. + // For v6 addresses this means the address is ::1. + bool IsLoopbackIP() const; + + // Determines whether the IP address is in one of the private ranges: + // For v4: 127.0.0.0/8 10.0.0.0/8 192.168.0.0/16 172.16.0.0/12. + // For v6: FE80::/16 and ::1. + bool IsPrivateIP() const; + + // Determines whether the hostname has been resolved to an IP. + bool IsUnresolvedIP() const; + inline bool IsUnresolved() const { return IsUnresolvedIP(); } // deprecated + + // Determines whether this address is identical to the given one. + bool operator ==(const SocketAddress& addr) const; + inline bool operator !=(const SocketAddress& addr) const { + return !this->operator ==(addr); + } + + // Compares based on IP and then port. + bool operator <(const SocketAddress& addr) const; + + // Determines whether this address has the same IP as the one given. + bool EqualIPs(const SocketAddress& addr) const; + + // Determines whether this address has the same port as the one given. + bool EqualPorts(const SocketAddress& addr) const; + + // Hashes this address into a small number. + size_t Hash() const; + + // Write this address to a sockaddr_in. + // If IPv6, will zero out the sockaddr_in and sets family to AF_UNSPEC. + void ToSockAddr(sockaddr_in* saddr) const; + + // Read this address from a sockaddr_in. + bool FromSockAddr(const sockaddr_in& saddr); + + // Read and write the address to/from a sockaddr_storage. + // Dual stack version always sets family to AF_INET6, and maps v4 addresses. + // The other version doesn't map, and outputs an AF_INET address for + // v4 or mapped addresses, and AF_INET6 addresses for others. + // Returns the size of the sockaddr_in or sockaddr_in6 structure that is + // written to the sockaddr_storage, or zero on failure. + size_t ToDualStackSockAddrStorage(sockaddr_storage* saddr) const; + size_t ToSockAddrStorage(sockaddr_storage* saddr) const; + + // Converts the IP address given in 'compact form' into dotted form. + // IP is given as an integer in host byte order. V4 only, to be deprecated. + // TODO: Deprecate this. + static std::string IPToString(uint32 ip_as_host_order_integer); + + // Same as IPToString but anonymizes it by hiding the last part. + // TODO: Deprecate this. + static std::string IPToSensitiveString(uint32 ip_as_host_order_integer); + + // Converts the IP address given in dotted form into compact form. + // Only dotted names (A.B.C.D) are converted. + // Output integer is returned in host byte order. + // TODO: Deprecate, replace wth agnostic versions. + static bool StringToIP(const std::string& str, uint32* ip); + static uint32 StringToIP(const std::string& str); + + // Converts the IP address given in printable form into an IPAddress. + static bool StringToIP(const std::string& str, IPAddress* ip); + + private: + std::string hostname_; + IPAddress ip_; + uint16 port_; + int scope_id_; + bool literal_; // Indicates that 'hostname_' contains a literal IP string. +}; + +bool SocketAddressFromSockAddrStorage(const sockaddr_storage& saddr, + SocketAddress* out); +SocketAddress EmptySocketAddressWithFamily(int family); + +} // namespace talk_base + +#endif // TALK_BASE_SOCKETADDRESS_H_ diff --git a/talk/base/socketaddress_unittest.cc b/talk/base/socketaddress_unittest.cc new file mode 100644 index 000000000..c57db8d5a --- /dev/null +++ b/talk/base/socketaddress_unittest.cc @@ -0,0 +1,352 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifdef POSIX +#include // for sockaddr_in +#endif + +#include "talk/base/gunit.h" +#include "talk/base/socketaddress.h" +#include "talk/base/ipaddress.h" + +namespace talk_base { + +const in6_addr kTestV6Addr = { { {0x20, 0x01, 0x0d, 0xb8, + 0x10, 0x20, 0x30, 0x40, + 0x50, 0x60, 0x70, 0x80, + 0x90, 0xA0, 0xB0, 0xC0} } }; +const in6_addr kMappedV4Addr = { { {0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xFF, + 0x01, 0x02, 0x03, 0x04} } }; +const std::string kTestV6AddrString = "2001:db8:1020:3040:5060:7080:90a0:b0c0"; +const std::string kTestV6AddrAnonymizedString = "2001:db8:1020::"; +const std::string kTestV6AddrFullString = + "[2001:db8:1020:3040:5060:7080:90a0:b0c0]:5678"; +const std::string kTestV6AddrFullAnonymizedString = "[2001:db8:1020::]:5678"; + +TEST(SocketAddressTest, TestDefaultCtor) { + SocketAddress addr; + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(), addr.ipaddr()); + EXPECT_EQ(0, addr.port()); + EXPECT_EQ("", addr.hostname()); +} + +TEST(SocketAddressTest, TestIPPortCtor) { + SocketAddress addr(IPAddress(0x01020304), 5678); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(0x01020304U), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("", addr.hostname()); + EXPECT_EQ("1.2.3.4:5678", addr.ToString()); +} + +TEST(SocketAddressTest, TestIPv4StringPortCtor) { + SocketAddress addr("1.2.3.4", 5678); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(0x01020304U), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("1.2.3.4", addr.hostname()); + EXPECT_EQ("1.2.3.4:5678", addr.ToString()); +} + +TEST(SocketAddressTest, TestIPv6StringPortCtor) { + SocketAddress addr2(kTestV6AddrString, 1234); + IPAddress tocheck(kTestV6Addr); + + EXPECT_FALSE(addr2.IsUnresolvedIP()); + EXPECT_EQ(tocheck, addr2.ipaddr()); + EXPECT_EQ(1234, addr2.port()); + EXPECT_EQ(kTestV6AddrString, addr2.hostname()); + EXPECT_EQ("[" + kTestV6AddrString + "]:1234", addr2.ToString()); +} + +TEST(SocketAddressTest, TestSpecialStringPortCtor) { + // inet_addr doesn't handle this address properly. + SocketAddress addr("255.255.255.255", 5678); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(0xFFFFFFFFU), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("255.255.255.255", addr.hostname()); + EXPECT_EQ("255.255.255.255:5678", addr.ToString()); +} + +TEST(SocketAddressTest, TestHostnamePortCtor) { + SocketAddress addr("a.b.com", 5678); + EXPECT_TRUE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("a.b.com", addr.hostname()); + EXPECT_EQ("a.b.com:5678", addr.ToString()); +} + +TEST(SocketAddressTest, TestCopyCtor) { + SocketAddress from("1.2.3.4", 5678); + SocketAddress addr(from); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(0x01020304U), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("1.2.3.4", addr.hostname()); + EXPECT_EQ("1.2.3.4:5678", addr.ToString()); +} + +TEST(SocketAddressTest, TestAssign) { + SocketAddress from("1.2.3.4", 5678); + SocketAddress addr(IPAddress(0x88888888), 9999); + addr = from; + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(0x01020304U), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("1.2.3.4", addr.hostname()); + EXPECT_EQ("1.2.3.4:5678", addr.ToString()); +} + +TEST(SocketAddressTest, TestSetIPPort) { + SocketAddress addr(IPAddress(0x88888888), 9999); + addr.SetIP(IPAddress(0x01020304)); + addr.SetPort(5678); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(0x01020304U), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("", addr.hostname()); + EXPECT_EQ("1.2.3.4:5678", addr.ToString()); +} + +TEST(SocketAddressTest, TestSetIPFromString) { + SocketAddress addr(IPAddress(0x88888888), 9999); + addr.SetIP("1.2.3.4"); + addr.SetPort(5678); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(0x01020304U), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("1.2.3.4", addr.hostname()); + EXPECT_EQ("1.2.3.4:5678", addr.ToString()); +} + +TEST(SocketAddressTest, TestSetIPFromHostname) { + SocketAddress addr(IPAddress(0x88888888), 9999); + addr.SetIP("a.b.com"); + addr.SetPort(5678); + EXPECT_TRUE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("a.b.com", addr.hostname()); + EXPECT_EQ("a.b.com:5678", addr.ToString()); + addr.SetResolvedIP(IPAddress(0x01020304)); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(0x01020304U), addr.ipaddr()); + EXPECT_EQ("a.b.com", addr.hostname()); + EXPECT_EQ("a.b.com:5678", addr.ToString()); +} + +TEST(SocketAddressTest, TestFromIPv4String) { + SocketAddress addr; + EXPECT_TRUE(addr.FromString("1.2.3.4:5678")); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(0x01020304U), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("1.2.3.4", addr.hostname()); + EXPECT_EQ("1.2.3.4:5678", addr.ToString()); +} + +TEST(SocketAddressTest, TestFromIPv6String) { + SocketAddress addr; + EXPECT_TRUE(addr.FromString(kTestV6AddrFullString)); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ(kTestV6AddrString, addr.hostname()); + EXPECT_EQ(kTestV6AddrFullString, addr.ToString()); +} + +TEST(SocketAddressTest, TestFromHostname) { + SocketAddress addr; + EXPECT_TRUE(addr.FromString("a.b.com:5678")); + EXPECT_TRUE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("a.b.com", addr.hostname()); + EXPECT_EQ("a.b.com:5678", addr.ToString()); +} + +TEST(SocketAddressTest, TestToFromSockAddr) { + SocketAddress from("1.2.3.4", 5678), addr; + sockaddr_in addr_in; + from.ToSockAddr(&addr_in); + EXPECT_TRUE(addr.FromSockAddr(addr_in)); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(0x01020304U), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("", addr.hostname()); + EXPECT_EQ("1.2.3.4:5678", addr.ToString()); +} + +TEST(SocketAddressTest, TestToFromSockAddrStorage) { + SocketAddress from("1.2.3.4", 5678), addr; + sockaddr_storage addr_storage; + from.ToSockAddrStorage(&addr_storage); + EXPECT_TRUE(SocketAddressFromSockAddrStorage(addr_storage, &addr)); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(0x01020304U), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("", addr.hostname()); + EXPECT_EQ("1.2.3.4:5678", addr.ToString()); + + addr.Clear(); + from.ToDualStackSockAddrStorage(&addr_storage); + EXPECT_TRUE(SocketAddressFromSockAddrStorage(addr_storage, &addr)); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(kMappedV4Addr), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("", addr.hostname()); + EXPECT_EQ("[::ffff:1.2.3.4]:5678", addr.ToString()); + + addr.Clear(); + memset(&addr_storage, 0, sizeof(sockaddr_storage)); + from = SocketAddress(kTestV6AddrString, 5678); + from.SetScopeID(6); + from.ToSockAddrStorage(&addr_storage); + EXPECT_TRUE(SocketAddressFromSockAddrStorage(addr_storage, &addr)); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(kTestV6Addr), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("", addr.hostname()); + EXPECT_EQ(kTestV6AddrFullString, addr.ToString()); + EXPECT_EQ(6, addr.scope_id()); + + addr.Clear(); + from.ToDualStackSockAddrStorage(&addr_storage); + EXPECT_TRUE(SocketAddressFromSockAddrStorage(addr_storage, &addr)); + EXPECT_FALSE(addr.IsUnresolvedIP()); + EXPECT_EQ(IPAddress(kTestV6Addr), addr.ipaddr()); + EXPECT_EQ(5678, addr.port()); + EXPECT_EQ("", addr.hostname()); + EXPECT_EQ(kTestV6AddrFullString, addr.ToString()); + EXPECT_EQ(6, addr.scope_id()); + + addr = from; + addr_storage.ss_family = AF_UNSPEC; + EXPECT_FALSE(SocketAddressFromSockAddrStorage(addr_storage, &addr)); + EXPECT_EQ(from, addr); + + EXPECT_FALSE(SocketAddressFromSockAddrStorage(addr_storage, NULL)); +} + +bool AreEqual(const SocketAddress& addr1, + const SocketAddress& addr2) { + return addr1 == addr2 && addr2 == addr1 && + !(addr1 != addr2) && !(addr2 != addr1); +} + +bool AreUnequal(const SocketAddress& addr1, + const SocketAddress& addr2) { + return !(addr1 == addr2) && !(addr2 == addr1) && + addr1 != addr2 && addr2 != addr1; +} + +TEST(SocketAddressTest, TestEqualityOperators) { + SocketAddress addr1("1.2.3.4", 5678); + SocketAddress addr2("1.2.3.4", 5678); + EXPECT_PRED2(AreEqual, addr1, addr2); + + addr2 = SocketAddress("0.0.0.1", 5678); + EXPECT_PRED2(AreUnequal, addr1, addr2); + + addr2 = SocketAddress("1.2.3.4", 1234); + EXPECT_PRED2(AreUnequal, addr1, addr2); + + addr2 = SocketAddress(kTestV6AddrString, 5678); + EXPECT_PRED2(AreUnequal, addr1, addr2); + + addr1 = SocketAddress(kTestV6AddrString, 5678); + EXPECT_PRED2(AreEqual, addr1, addr2); + + addr2 = SocketAddress(kTestV6AddrString, 1234); + EXPECT_PRED2(AreUnequal, addr1, addr2); + + addr2 = SocketAddress("fe80::1", 5678); + EXPECT_PRED2(AreUnequal, addr1, addr2); +} + +bool IsLessThan(const SocketAddress& addr1, + const SocketAddress& addr2) { + return addr1 < addr2 && + !(addr2 < addr1) && + !(addr1 == addr2); +} + +TEST(SocketAddressTest, TestComparisonOperator) { + SocketAddress addr1("1.2.3.4", 5678); + SocketAddress addr2("1.2.3.4", 5678); + + EXPECT_FALSE(addr1 < addr2); + EXPECT_FALSE(addr2 < addr1); + + addr2 = SocketAddress("1.2.3.4", 5679); + EXPECT_PRED2(IsLessThan, addr1, addr2); + + addr2 = SocketAddress("2.2.3.4", 49152); + EXPECT_PRED2(IsLessThan, addr1, addr2); + + addr2 = SocketAddress(kTestV6AddrString, 5678); + EXPECT_PRED2(IsLessThan, addr1, addr2); + + addr1 = SocketAddress("fe80::1", 5678); + EXPECT_PRED2(IsLessThan, addr2, addr1); + + addr2 = SocketAddress("fe80::1", 5679); + EXPECT_PRED2(IsLessThan, addr1, addr2); + + addr2 = SocketAddress("fe80::1", 5678); + EXPECT_FALSE(addr1 < addr2); + EXPECT_FALSE(addr2 < addr1); +} + +TEST(SocketAddressTest, TestToSensitiveString) { + SocketAddress addr_v4("1.2.3.4", 5678); + EXPECT_EQ("1.2.3.4", addr_v4.HostAsURIString()); + EXPECT_EQ("1.2.3.4:5678", addr_v4.ToString()); + EXPECT_EQ("1.2.3.4", addr_v4.HostAsSensitiveURIString()); + EXPECT_EQ("1.2.3.4:5678", addr_v4.ToSensitiveString()); + IPAddress::set_strip_sensitive(true); + EXPECT_EQ("1.2.3.x", addr_v4.HostAsSensitiveURIString()); + EXPECT_EQ("1.2.3.x:5678", addr_v4.ToSensitiveString()); + IPAddress::set_strip_sensitive(false); + + SocketAddress addr_v6(kTestV6AddrString, 5678); + EXPECT_EQ("[" + kTestV6AddrString + "]", addr_v6.HostAsURIString()); + EXPECT_EQ(kTestV6AddrFullString, addr_v6.ToString()); + EXPECT_EQ("[" + kTestV6AddrString + "]", addr_v6.HostAsSensitiveURIString()); + EXPECT_EQ(kTestV6AddrFullString, addr_v6.ToSensitiveString()); + IPAddress::set_strip_sensitive(true); + EXPECT_EQ("[" + kTestV6AddrAnonymizedString + "]", + addr_v6.HostAsSensitiveURIString()); + EXPECT_EQ(kTestV6AddrFullAnonymizedString, addr_v6.ToSensitiveString()); + IPAddress::set_strip_sensitive(false); +} + +} // namespace talk_base diff --git a/talk/base/socketaddresspair.cc b/talk/base/socketaddresspair.cc new file mode 100644 index 000000000..7f190a90b --- /dev/null +++ b/talk/base/socketaddresspair.cc @@ -0,0 +1,58 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/socketaddresspair.h" + +namespace talk_base { + +SocketAddressPair::SocketAddressPair( + const SocketAddress& src, const SocketAddress& dest) + : src_(src), dest_(dest) { +} + + +bool SocketAddressPair::operator ==(const SocketAddressPair& p) const { + return (src_ == p.src_) && (dest_ == p.dest_); +} + +bool SocketAddressPair::operator <(const SocketAddressPair& p) const { + if (src_ < p.src_) + return true; + if (p.src_ < src_) + return false; + if (dest_ < p.dest_) + return true; + if (p.dest_ < dest_) + return false; + return false; +} + +size_t SocketAddressPair::Hash() const { + return src_.Hash() ^ dest_.Hash(); +} + +} // namespace talk_base diff --git a/talk/base/socketaddresspair.h b/talk/base/socketaddresspair.h new file mode 100644 index 000000000..10f5d3048 --- /dev/null +++ b/talk/base/socketaddresspair.h @@ -0,0 +1,58 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_SOCKETADDRESSPAIR_H__ +#define TALK_BASE_SOCKETADDRESSPAIR_H__ + +#include "talk/base/socketaddress.h" + +namespace talk_base { + +// Records a pair (source,destination) of socket addresses. The two addresses +// identify a connection between two machines. (For UDP, this "connection" is +// not maintained explicitly in a socket.) +class SocketAddressPair { +public: + SocketAddressPair() {} + SocketAddressPair(const SocketAddress& srs, const SocketAddress& dest); + + const SocketAddress& source() const { return src_; } + const SocketAddress& destination() const { return dest_; } + + bool operator ==(const SocketAddressPair& r) const; + bool operator <(const SocketAddressPair& r) const; + + size_t Hash() const; + +private: + SocketAddress src_; + SocketAddress dest_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_SOCKETADDRESSPAIR_H__ diff --git a/talk/base/socketfactory.h b/talk/base/socketfactory.h new file mode 100644 index 000000000..291df6015 --- /dev/null +++ b/talk/base/socketfactory.h @@ -0,0 +1,55 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_SOCKETFACTORY_H__ +#define TALK_BASE_SOCKETFACTORY_H__ + +#include "talk/base/socket.h" +#include "talk/base/asyncsocket.h" + +namespace talk_base { + +class SocketFactory { +public: + virtual ~SocketFactory() {} + + // Returns a new socket for blocking communication. The type can be + // SOCK_DGRAM and SOCK_STREAM. + // TODO: C++ inheritance rules mean that all users must have both + // CreateSocket(int) and CreateSocket(int,int). Will remove CreateSocket(int) + // (and CreateAsyncSocket(int) when all callers are changed. + virtual Socket* CreateSocket(int type) = 0; + virtual Socket* CreateSocket(int family, int type) = 0; + // Returns a new socket for nonblocking communication. The type can be + // SOCK_DGRAM and SOCK_STREAM. + virtual AsyncSocket* CreateAsyncSocket(int type) = 0; + virtual AsyncSocket* CreateAsyncSocket(int family, int type) = 0; +}; + +} // namespace talk_base + +#endif // TALK_BASE_SOCKETFACTORY_H__ diff --git a/talk/base/socketpool.cc b/talk/base/socketpool.cc new file mode 100644 index 000000000..10d43031d --- /dev/null +++ b/talk/base/socketpool.cc @@ -0,0 +1,297 @@ +/* + * libjingle + * Copyright 2004--2005, 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 + +#include "talk/base/asyncsocket.h" +#include "talk/base/logging.h" +#include "talk/base/socketfactory.h" +#include "talk/base/socketpool.h" +#include "talk/base/socketstream.h" +#include "talk/base/thread.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// StreamCache - Caches a set of open streams, defers creation to a separate +// StreamPool. +/////////////////////////////////////////////////////////////////////////////// + +StreamCache::StreamCache(StreamPool* pool) : pool_(pool) { +} + +StreamCache::~StreamCache() { + for (ConnectedList::iterator it = active_.begin(); it != active_.end(); + ++it) { + delete it->second; + } + for (ConnectedList::iterator it = cached_.begin(); it != cached_.end(); + ++it) { + delete it->second; + } +} + +StreamInterface* StreamCache::RequestConnectedStream( + const SocketAddress& remote, int* err) { + LOG_F(LS_VERBOSE) << "(" << remote << ")"; + for (ConnectedList::iterator it = cached_.begin(); it != cached_.end(); + ++it) { + if (remote == it->first) { + it->second->SignalEvent.disconnect(this); + // Move from cached_ to active_ + active_.push_front(*it); + cached_.erase(it); + if (err) + *err = 0; + LOG_F(LS_VERBOSE) << "Providing cached stream"; + return active_.front().second; + } + } + if (StreamInterface* stream = pool_->RequestConnectedStream(remote, err)) { + // We track active streams so that we can remember their address + active_.push_front(ConnectedStream(remote, stream)); + LOG_F(LS_VERBOSE) << "Providing new stream"; + return active_.front().second; + } + return NULL; +} + +void StreamCache::ReturnConnectedStream(StreamInterface* stream) { + for (ConnectedList::iterator it = active_.begin(); it != active_.end(); + ++it) { + if (stream == it->second) { + LOG_F(LS_VERBOSE) << "(" << it->first << ")"; + if (stream->GetState() == SS_CLOSED) { + // Return closed streams + LOG_F(LS_VERBOSE) << "Returning closed stream"; + pool_->ReturnConnectedStream(it->second); + } else { + // Monitor open streams + stream->SignalEvent.connect(this, &StreamCache::OnStreamEvent); + LOG_F(LS_VERBOSE) << "Caching stream"; + cached_.push_front(*it); + } + active_.erase(it); + return; + } + } + ASSERT(false); +} + +void StreamCache::OnStreamEvent(StreamInterface* stream, int events, int err) { + if ((events & SE_CLOSE) == 0) { + LOG_F(LS_WARNING) << "(" << events << ", " << err + << ") received non-close event"; + return; + } + for (ConnectedList::iterator it = cached_.begin(); it != cached_.end(); + ++it) { + if (stream == it->second) { + LOG_F(LS_VERBOSE) << "(" << it->first << ")"; + // We don't cache closed streams, so return it. + it->second->SignalEvent.disconnect(this); + LOG_F(LS_VERBOSE) << "Returning closed stream"; + pool_->ReturnConnectedStream(it->second); + cached_.erase(it); + return; + } + } + ASSERT(false); +} + +////////////////////////////////////////////////////////////////////// +// NewSocketPool +////////////////////////////////////////////////////////////////////// + +NewSocketPool::NewSocketPool(SocketFactory* factory) : factory_(factory) { +} + +NewSocketPool::~NewSocketPool() { +} + +StreamInterface* +NewSocketPool::RequestConnectedStream(const SocketAddress& remote, int* err) { + AsyncSocket* socket = + factory_->CreateAsyncSocket(remote.family(), SOCK_STREAM); + if (!socket) { + if (err) + *err = -1; + return NULL; + } + if ((socket->Connect(remote) != 0) && !socket->IsBlocking()) { + if (err) + *err = socket->GetError(); + delete socket; + return NULL; + } + if (err) + *err = 0; + return new SocketStream(socket); +} + +void +NewSocketPool::ReturnConnectedStream(StreamInterface* stream) { + Thread::Current()->Dispose(stream); +} + +////////////////////////////////////////////////////////////////////// +// ReuseSocketPool +////////////////////////////////////////////////////////////////////// + +ReuseSocketPool::ReuseSocketPool(SocketFactory* factory) +: factory_(factory), stream_(NULL), checked_out_(false) { +} + +ReuseSocketPool::~ReuseSocketPool() { + ASSERT(!checked_out_); + delete stream_; +} + +StreamInterface* +ReuseSocketPool::RequestConnectedStream(const SocketAddress& remote, int* err) { + // Only one socket can be used from this "pool" at a time + ASSERT(!checked_out_); + if (!stream_) { + LOG_F(LS_VERBOSE) << "Creating new socket"; + int family = remote.family(); + // TODO: Deal with this when we/I clean up DNS resolution. + if (remote.IsUnresolvedIP()) { + family = AF_INET; + } + AsyncSocket* socket = + factory_->CreateAsyncSocket(family, SOCK_STREAM); + if (!socket) { + if (err) + *err = -1; + return NULL; + } + stream_ = new SocketStream(socket); + } + if ((stream_->GetState() == SS_OPEN) && (remote == remote_)) { + LOG_F(LS_VERBOSE) << "Reusing connection to: " << remote_; + } else { + remote_ = remote; + stream_->Close(); + if ((stream_->GetSocket()->Connect(remote_) != 0) + && !stream_->GetSocket()->IsBlocking()) { + if (err) + *err = stream_->GetSocket()->GetError(); + return NULL; + } else { + LOG_F(LS_VERBOSE) << "Opening connection to: " << remote_; + } + } + stream_->SignalEvent.disconnect(this); + checked_out_ = true; + if (err) + *err = 0; + return stream_; +} + +void +ReuseSocketPool::ReturnConnectedStream(StreamInterface* stream) { + ASSERT(stream == stream_); + ASSERT(checked_out_); + checked_out_ = false; + // Until the socket is reused, monitor it to determine if it closes. + stream_->SignalEvent.connect(this, &ReuseSocketPool::OnStreamEvent); +} + +void +ReuseSocketPool::OnStreamEvent(StreamInterface* stream, int events, int err) { + ASSERT(stream == stream_); + ASSERT(!checked_out_); + + // If the stream was written to and then immediately returned to us then + // we may get a writable notification for it, which we should ignore. + if (events == SE_WRITE) { + LOG_F(LS_VERBOSE) << "Pooled Socket unexpectedly writable: ignoring"; + return; + } + + // If the peer sent data, we can't process it, so drop the connection. + // If the socket has closed, clean it up. + // In either case, we'll reconnect it the next time it is used. + ASSERT(0 != (events & (SE_READ|SE_CLOSE))); + if (0 != (events & SE_CLOSE)) { + LOG_F(LS_VERBOSE) << "Connection closed with error: " << err; + } else { + LOG_F(LS_VERBOSE) << "Pooled Socket unexpectedly readable: closing"; + } + stream_->Close(); +} + +/////////////////////////////////////////////////////////////////////////////// +// LoggingPoolAdapter - Adapts a StreamPool to supply streams with attached +// LoggingAdapters. +/////////////////////////////////////////////////////////////////////////////// + +LoggingPoolAdapter::LoggingPoolAdapter( + StreamPool* pool, LoggingSeverity level, const std::string& label, + bool binary_mode) + : pool_(pool), level_(level), label_(label), binary_mode_(binary_mode) { +} + +LoggingPoolAdapter::~LoggingPoolAdapter() { + for (StreamList::iterator it = recycle_bin_.begin(); + it != recycle_bin_.end(); ++it) { + delete *it; + } +} + +StreamInterface* LoggingPoolAdapter::RequestConnectedStream( + const SocketAddress& remote, int* err) { + if (StreamInterface* stream = pool_->RequestConnectedStream(remote, err)) { + ASSERT(SS_CLOSED != stream->GetState()); + std::stringstream ss; + ss << label_ << "(0x" << std::setfill('0') << std::hex << std::setw(8) + << stream << ")"; + LOG_V(level_) << ss.str() + << ((SS_OPEN == stream->GetState()) ? " Connected" + : " Connecting") + << " to " << remote; + if (recycle_bin_.empty()) { + return new LoggingAdapter(stream, level_, ss.str(), binary_mode_); + } + LoggingAdapter* logging = recycle_bin_.front(); + recycle_bin_.pop_front(); + logging->set_label(ss.str()); + logging->Attach(stream); + return logging; + } + return NULL; +} + +void LoggingPoolAdapter::ReturnConnectedStream(StreamInterface* stream) { + LoggingAdapter* logging = static_cast(stream); + pool_->ReturnConnectedStream(logging->Detach()); + recycle_bin_.push_back(logging); +} + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/socketpool.h b/talk/base/socketpool.h new file mode 100644 index 000000000..847d8ffb4 --- /dev/null +++ b/talk/base/socketpool.h @@ -0,0 +1,160 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_SOCKETPOOL_H_ +#define TALK_BASE_SOCKETPOOL_H_ + +#include +#include +#include "talk/base/logging.h" +#include "talk/base/sigslot.h" +#include "talk/base/socketaddress.h" + +namespace talk_base { + +class AsyncSocket; +class LoggingAdapter; +class SocketFactory; +class SocketStream; +class StreamInterface; + +////////////////////////////////////////////////////////////////////// +// StreamPool +////////////////////////////////////////////////////////////////////// + +class StreamPool { +public: + virtual ~StreamPool() { } + + virtual StreamInterface* RequestConnectedStream(const SocketAddress& remote, + int* err) = 0; + virtual void ReturnConnectedStream(StreamInterface* stream) = 0; +}; + +/////////////////////////////////////////////////////////////////////////////// +// StreamCache - Caches a set of open streams, defers creation/destruction to +// the supplied StreamPool. +/////////////////////////////////////////////////////////////////////////////// + +class StreamCache : public StreamPool, public sigslot::has_slots<> { +public: + StreamCache(StreamPool* pool); + virtual ~StreamCache(); + + // StreamPool Interface + virtual StreamInterface* RequestConnectedStream(const SocketAddress& remote, + int* err); + virtual void ReturnConnectedStream(StreamInterface* stream); + +private: + typedef std::pair ConnectedStream; + typedef std::list ConnectedList; + + void OnStreamEvent(StreamInterface* stream, int events, int err); + + // We delegate stream creation and deletion to this pool. + StreamPool* pool_; + // Streams that are in use (returned from RequestConnectedStream). + ConnectedList active_; + // Streams which were returned to us, but are still open. + ConnectedList cached_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// NewSocketPool +// Creates a new stream on every request +/////////////////////////////////////////////////////////////////////////////// + +class NewSocketPool : public StreamPool { +public: + NewSocketPool(SocketFactory* factory); + virtual ~NewSocketPool(); + + // StreamPool Interface + virtual StreamInterface* RequestConnectedStream(const SocketAddress& remote, + int* err); + virtual void ReturnConnectedStream(StreamInterface* stream); + +private: + SocketFactory* factory_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// ReuseSocketPool +// Maintains a single socket at a time, and will reuse it without closing if +// the destination address is the same. +/////////////////////////////////////////////////////////////////////////////// + +class ReuseSocketPool : public StreamPool, public sigslot::has_slots<> { +public: + ReuseSocketPool(SocketFactory* factory); + virtual ~ReuseSocketPool(); + + // StreamPool Interface + virtual StreamInterface* RequestConnectedStream(const SocketAddress& remote, + int* err); + virtual void ReturnConnectedStream(StreamInterface* stream); + +private: + void OnStreamEvent(StreamInterface* stream, int events, int err); + + SocketFactory* factory_; + SocketStream* stream_; + SocketAddress remote_; + bool checked_out_; // Whether the stream is currently checked out +}; + +/////////////////////////////////////////////////////////////////////////////// +// LoggingPoolAdapter - Adapts a StreamPool to supply streams with attached +// LoggingAdapters. +/////////////////////////////////////////////////////////////////////////////// + +class LoggingPoolAdapter : public StreamPool { +public: + LoggingPoolAdapter(StreamPool* pool, LoggingSeverity level, + const std::string& label, bool binary_mode); + virtual ~LoggingPoolAdapter(); + + // StreamPool Interface + virtual StreamInterface* RequestConnectedStream(const SocketAddress& remote, + int* err); + virtual void ReturnConnectedStream(StreamInterface* stream); + +private: + StreamPool* pool_; + LoggingSeverity level_; + std::string label_; + bool binary_mode_; + typedef std::deque StreamList; + StreamList recycle_bin_; +}; + +////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_SOCKETPOOL_H_ diff --git a/talk/base/socketserver.h b/talk/base/socketserver.h new file mode 100644 index 000000000..151ce615f --- /dev/null +++ b/talk/base/socketserver.h @@ -0,0 +1,61 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_SOCKETSERVER_H_ +#define TALK_BASE_SOCKETSERVER_H_ + +#include "talk/base/socketfactory.h" + +namespace talk_base { + +class MessageQueue; + +// Provides the ability to wait for activity on a set of sockets. The Thread +// class provides a nice wrapper on a socket server. +// +// The server is also a socket factory. The sockets it creates will be +// notified of asynchronous I/O from this server's Wait method. +class SocketServer : public SocketFactory { + public: + // When the socket server is installed into a Thread, this function is + // called to allow the socket server to use the thread's message queue for + // any messaging that it might need to perform. + virtual void SetMessageQueue(MessageQueue* queue) {} + + // Sleeps until: + // 1) cms milliseconds have elapsed (unless cms == kForever) + // 2) WakeUp() is called + // While sleeping, I/O is performed if process_io is true. + virtual bool Wait(int cms, bool process_io) = 0; + + // Causes the current wait (if one is in progress) to wake up. + virtual void WakeUp() = 0; +}; + +} // namespace talk_base + +#endif // TALK_BASE_SOCKETSERVER_H_ diff --git a/talk/base/socketstream.cc b/talk/base/socketstream.cc new file mode 100644 index 000000000..3dc5a9500 --- /dev/null +++ b/talk/base/socketstream.cc @@ -0,0 +1,138 @@ +/* + * libjingle + * Copyright 2010, 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/base/socketstream.h" + +namespace talk_base { + +SocketStream::SocketStream(AsyncSocket* socket) : socket_(NULL) { + Attach(socket); +} + +SocketStream::~SocketStream() { + delete socket_; +} + +void SocketStream::Attach(AsyncSocket* socket) { + if (socket_) + delete socket_; + socket_ = socket; + if (socket_) { + socket_->SignalConnectEvent.connect(this, &SocketStream::OnConnectEvent); + socket_->SignalReadEvent.connect(this, &SocketStream::OnReadEvent); + socket_->SignalWriteEvent.connect(this, &SocketStream::OnWriteEvent); + socket_->SignalCloseEvent.connect(this, &SocketStream::OnCloseEvent); + } +} + +AsyncSocket* SocketStream::Detach() { + AsyncSocket* socket = socket_; + if (socket_) { + socket_->SignalConnectEvent.disconnect(this); + socket_->SignalReadEvent.disconnect(this); + socket_->SignalWriteEvent.disconnect(this); + socket_->SignalCloseEvent.disconnect(this); + socket_ = NULL; + } + return socket; +} + +StreamState SocketStream::GetState() const { + ASSERT(socket_ != NULL); + switch (socket_->GetState()) { + case Socket::CS_CONNECTED: + return SS_OPEN; + case Socket::CS_CONNECTING: + return SS_OPENING; + case Socket::CS_CLOSED: + default: + return SS_CLOSED; + } +} + +StreamResult SocketStream::Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + ASSERT(socket_ != NULL); + int result = socket_->Recv(buffer, buffer_len); + if (result < 0) { + if (socket_->IsBlocking()) + return SR_BLOCK; + if (error) + *error = socket_->GetError(); + return SR_ERROR; + } + if ((result > 0) || (buffer_len == 0)) { + if (read) + *read = result; + return SR_SUCCESS; + } + return SR_EOS; +} + +StreamResult SocketStream::Write(const void* data, size_t data_len, + size_t* written, int* error) { + ASSERT(socket_ != NULL); + int result = socket_->Send(data, data_len); + if (result < 0) { + if (socket_->IsBlocking()) + return SR_BLOCK; + if (error) + *error = socket_->GetError(); + return SR_ERROR; + } + if (written) + *written = result; + return SR_SUCCESS; +} + +void SocketStream::Close() { + ASSERT(socket_ != NULL); + socket_->Close(); +} + +void SocketStream::OnConnectEvent(AsyncSocket* socket) { + ASSERT(socket == socket_); + SignalEvent(this, SE_OPEN | SE_READ | SE_WRITE, 0); +} + +void SocketStream::OnReadEvent(AsyncSocket* socket) { + ASSERT(socket == socket_); + SignalEvent(this, SE_READ, 0); +} + +void SocketStream::OnWriteEvent(AsyncSocket* socket) { + ASSERT(socket == socket_); + SignalEvent(this, SE_WRITE, 0); +} + +void SocketStream::OnCloseEvent(AsyncSocket* socket, int err) { + ASSERT(socket == socket_); + SignalEvent(this, SE_CLOSE, err); +} + + +} // namespace talk_base diff --git a/talk/base/socketstream.h b/talk/base/socketstream.h new file mode 100644 index 000000000..591dc4c10 --- /dev/null +++ b/talk/base/socketstream.h @@ -0,0 +1,74 @@ +/* + * libjingle + * Copyright 2005--2010, 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. + */ + +#ifndef TALK_BASE_SOCKETSTREAM_H_ +#define TALK_BASE_SOCKETSTREAM_H_ + +#include "talk/base/asyncsocket.h" +#include "talk/base/common.h" +#include "talk/base/stream.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// + +class SocketStream : public StreamInterface, public sigslot::has_slots<> { + public: + explicit SocketStream(AsyncSocket* socket); + virtual ~SocketStream(); + + void Attach(AsyncSocket* socket); + AsyncSocket* Detach(); + + AsyncSocket* GetSocket() { return socket_; } + + virtual StreamState GetState() const; + + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error); + + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + + virtual void Close(); + + private: + void OnConnectEvent(AsyncSocket* socket); + void OnReadEvent(AsyncSocket* socket); + void OnWriteEvent(AsyncSocket* socket); + void OnCloseEvent(AsyncSocket* socket, int err); + + AsyncSocket* socket_; + + DISALLOW_EVIL_CONSTRUCTORS(SocketStream); +}; + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_SOCKETSTREAM_H_ diff --git a/talk/base/ssladapter.cc b/talk/base/ssladapter.cc new file mode 100644 index 000000000..b7d82943a --- /dev/null +++ b/talk/base/ssladapter.cc @@ -0,0 +1,113 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#if HAVE_CONFIG_H +#include "config.h" +#endif // HAVE_CONFIG_H + +#include "talk/base/ssladapter.h" + +#include "talk/base/sslconfig.h" + +#if SSL_USE_SCHANNEL + +#include "schanneladapter.h" + +#elif SSL_USE_OPENSSL // && !SSL_USE_SCHANNEL + +#include "openssladapter.h" + +#elif SSL_USE_NSS // && !SSL_USE_CHANNEL && !SSL_USE_OPENSSL + +#include "nssstreamadapter.h" + +#endif // SSL_USE_OPENSSL && !SSL_USE_SCHANNEL && !SSL_USE_NSS + +/////////////////////////////////////////////////////////////////////////////// + +namespace talk_base { + +SSLAdapter* +SSLAdapter::Create(AsyncSocket* socket) { +#if SSL_USE_SCHANNEL + return new SChannelAdapter(socket); +#elif SSL_USE_OPENSSL // && !SSL_USE_SCHANNEL + return new OpenSSLAdapter(socket); +#else // !SSL_USE_OPENSSL && !SSL_USE_SCHANNEL + return NULL; +#endif // !SSL_USE_OPENSSL && !SSL_USE_SCHANNEL +} + +/////////////////////////////////////////////////////////////////////////////// + +#if SSL_USE_OPENSSL + +bool InitializeSSL(VerificationCallback callback) { + return OpenSSLAdapter::InitializeSSL(callback); +} + +bool InitializeSSLThread() { + return OpenSSLAdapter::InitializeSSLThread(); +} + +bool CleanupSSL() { + return OpenSSLAdapter::CleanupSSL(); +} + +#elif SSL_USE_NSS // !SSL_USE_OPENSSL + +bool InitializeSSL(VerificationCallback callback) { + return NSSContext::InitializeSSL(callback); +} + +bool InitializeSSLThread() { + return NSSContext::InitializeSSLThread(); +} + +bool CleanupSSL() { + return NSSContext::CleanupSSL(); +} + +#else // !SSL_USE_OPENSSL && !SSL_USE_NSS + +bool InitializeSSL(VerificationCallback callback) { + return true; +} + +bool InitializeSSLThread() { + return true; +} + +bool CleanupSSL() { + return true; +} + +#endif // !SSL_USE_OPENSSL && !SSL_USE_NSS + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/ssladapter.h b/talk/base/ssladapter.h new file mode 100644 index 000000000..1583dc2eb --- /dev/null +++ b/talk/base/ssladapter.h @@ -0,0 +1,76 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_SSLADAPTER_H_ +#define TALK_BASE_SSLADAPTER_H_ + +#include "talk/base/asyncsocket.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// + +class SSLAdapter : public AsyncSocketAdapter { + public: + explicit SSLAdapter(AsyncSocket* socket) + : AsyncSocketAdapter(socket), ignore_bad_cert_(false) { } + + bool ignore_bad_cert() const { return ignore_bad_cert_; } + void set_ignore_bad_cert(bool ignore) { ignore_bad_cert_ = ignore; } + + // StartSSL returns 0 if successful. + // If StartSSL is called while the socket is closed or connecting, the SSL + // negotiation will begin as soon as the socket connects. + virtual int StartSSL(const char* hostname, bool restartable) = 0; + + // Create the default SSL adapter for this platform + static SSLAdapter* Create(AsyncSocket* socket); + + private: + // If true, the server certificate need not match the configured hostname. + bool ignore_bad_cert_; +}; + +/////////////////////////////////////////////////////////////////////////////// + +typedef bool (*VerificationCallback)(void* cert); + +// Call this on the main thread, before using SSL. +// Call CleanupSSLThread when finished with SSL. +bool InitializeSSL(VerificationCallback callback = NULL); + +// Call to initialize additional threads. +bool InitializeSSLThread(); + +// Call to cleanup additional threads, and also the main thread. +bool CleanupSSL(); + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_SSLADAPTER_H_ diff --git a/talk/base/sslconfig.h b/talk/base/sslconfig.h new file mode 100644 index 000000000..cc3a73353 --- /dev/null +++ b/talk/base/sslconfig.h @@ -0,0 +1,50 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_BASE_SSLCONFIG_H_ +#define TALK_BASE_SSLCONFIG_H_ + +// If no preference has been indicated, default to SChannel on Windows and +// OpenSSL everywhere else, if it is available. +#if !defined(SSL_USE_SCHANNEL) && !defined(SSL_USE_OPENSSL) && \ + !defined(SSL_USE_NSS) +#if defined(WIN32) + +#define SSL_USE_SCHANNEL 1 + +#else // defined(WIN32) + +#if defined(HAVE_OPENSSL_SSL_H) +#define SSL_USE_OPENSSL 1 +#elif defined(HAVE_NSS_SSL_H) +#define SSL_USE_NSS 1 +#endif + +#endif // !defined(WIN32) +#endif + +#endif // TALK_BASE_SSLCONFIG_H_ diff --git a/talk/base/sslfingerprint.h b/talk/base/sslfingerprint.h new file mode 100644 index 000000000..4d41156f8 --- /dev/null +++ b/talk/base/sslfingerprint.h @@ -0,0 +1,109 @@ +/* + * libjingle + * Copyright 2012, Google Inc. + * Copyright 2012, RTFM 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. + */ + +#ifndef TALK_BASE_SSLFINGERPRINT_H_ +#define TALK_BASE_SSLFINGERPRINT_H_ + +#include +#include + +#include "talk/base/buffer.h" +#include "talk/base/helpers.h" +#include "talk/base/messagedigest.h" +#include "talk/base/sslidentity.h" +#include "talk/base/stringencode.h" + +namespace talk_base { + +struct SSLFingerprint { + static SSLFingerprint* Create(const std::string& algorithm, + const talk_base::SSLIdentity* identity) { + if (!identity) { + return NULL; + } + + uint8 digest_val[64]; + size_t digest_len; + bool ret = identity->certificate().ComputeDigest( + algorithm, digest_val, sizeof(digest_val), &digest_len); + if (!ret) { + return NULL; + } + + return new SSLFingerprint(algorithm, digest_val, digest_len); + } + + static SSLFingerprint* CreateFromRfc4572(const std::string& algorithm, + const std::string& fingerprint) { + if (algorithm.empty()) + return NULL; + + if (fingerprint.empty()) + return NULL; + + size_t value_len; + char value[talk_base::MessageDigest::kMaxSize]; + value_len = talk_base::hex_decode_with_delimiter(value, sizeof(value), + fingerprint.c_str(), + fingerprint.length(), + ':'); + if (!value_len) + return NULL; + + return new SSLFingerprint(algorithm, + reinterpret_cast(value), + value_len); + } + + SSLFingerprint(const std::string& algorithm, const uint8* digest_in, + size_t digest_len) : algorithm(algorithm) { + digest.SetData(digest_in, digest_len); + } + SSLFingerprint(const SSLFingerprint& from) + : algorithm(from.algorithm), digest(from.digest) {} + bool operator==(const SSLFingerprint& other) const { + return algorithm == other.algorithm && + digest == other.digest; + } + + std::string GetRfc4572Fingerprint() const { + std::string fingerprint = + talk_base::hex_encode_with_delimiter( + digest.data(), digest.length(), ':'); + std::transform(fingerprint.begin(), fingerprint.end(), + fingerprint.begin(), ::toupper); + return fingerprint; + } + + std::string algorithm; + talk_base::Buffer digest; +}; + +} // namespace talk_base + +#endif // TALK_BASE_SSLFINGERPRINT_H_ diff --git a/talk/base/sslidentity.cc b/talk/base/sslidentity.cc new file mode 100644 index 000000000..497805200 --- /dev/null +++ b/talk/base/sslidentity.cc @@ -0,0 +1,104 @@ +/* + * libjingle + * Copyright 2004, 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. + */ + +// Handling of certificates and keypairs for SSLStreamAdapter's peer mode. +#if HAVE_CONFIG_H +#include "config.h" +#endif // HAVE_CONFIG_H + +#include "talk/base/sslidentity.h" + +#include + +#include "talk/base/sslconfig.h" + +#if SSL_USE_SCHANNEL + +#elif SSL_USE_OPENSSL // !SSL_USE_SCHANNEL + +#include "talk/base/opensslidentity.h" + +#elif SSL_USE_NSS // !SSL_USE_SCHANNEL && !SSL_USE_OPENSSL + +#include "talk/base/nssidentity.h" + +#endif // SSL_USE_SCHANNEL + +namespace talk_base { + +#if SSL_USE_SCHANNEL + +SSLCertificate* SSLCertificate::FromPEMString(const std::string& pem_string) { + return NULL; +} + +SSLIdentity* SSLIdentity::Generate(const std::string& common_name) { + return NULL; +} + +SSLIdentity* SSLIdentity::FromPEMStrings(const std::string& private_key, + const std::string& certificate) { + return NULL; +} + +#elif SSL_USE_OPENSSL // !SSL_USE_SCHANNEL + +SSLCertificate* SSLCertificate::FromPEMString(const std::string& pem_string) { + return OpenSSLCertificate::FromPEMString(pem_string); +} + +SSLIdentity* SSLIdentity::Generate(const std::string& common_name) { + return OpenSSLIdentity::Generate(common_name); +} + +SSLIdentity* SSLIdentity::FromPEMStrings(const std::string& private_key, + const std::string& certificate) { + return OpenSSLIdentity::FromPEMStrings(private_key, certificate); +} + +#elif SSL_USE_NSS // !SSL_USE_OPENSSL && !SSL_USE_SCHANNEL + +SSLCertificate* SSLCertificate::FromPEMString(const std::string& pem_string) { + return NSSCertificate::FromPEMString(pem_string); +} + +SSLIdentity* SSLIdentity::Generate(const std::string& common_name) { + return NSSIdentity::Generate(common_name); +} + +SSLIdentity* SSLIdentity::FromPEMStrings(const std::string& private_key, + const std::string& certificate) { + return NSSIdentity::FromPEMStrings(private_key, certificate); +} + +#else // !SSL_USE_OPENSSL && !SSL_USE_SCHANNEL && !SSL_USE_NSS + +#error "No SSL implementation" + +#endif // SSL_USE_SCHANNEL + +} // namespace talk_base diff --git a/talk/base/sslidentity.h b/talk/base/sslidentity.h new file mode 100644 index 000000000..b63c06647 --- /dev/null +++ b/talk/base/sslidentity.h @@ -0,0 +1,100 @@ +/* + * libjingle + * Copyright 2004, 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. + */ + +// Handling of certificates and keypairs for SSLStreamAdapter's peer mode. + +#ifndef TALK_BASE_SSLIDENTITY_H_ +#define TALK_BASE_SSLIDENTITY_H_ + +#include +#include "talk/base/messagedigest.h" + +namespace talk_base { + +// Abstract interface overridden by SSL library specific +// implementations. + +// A somewhat opaque type used to encapsulate a certificate. +// Wraps the SSL library's notion of a certificate, with reference counting. +// The SSLCertificate object is pretty much immutable once created. +// (The OpenSSL implementation only does reference counting and +// possibly caching of intermediate results.) +class SSLCertificate { + public: + // Parses and build a certificate from a PEM encoded string. + // Returns NULL on failure. + // The length of the string representation of the certificate is + // stored in *pem_length if it is non-NULL, and only if + // parsing was successful. + // Caller is responsible for freeing the returned object. + static SSLCertificate* FromPEMString(const std::string& pem_string); + virtual ~SSLCertificate() {} + + // Returns a new SSLCertificate object instance wrapping the same + // underlying certificate. + // Caller is responsible for freeing the returned object. + virtual SSLCertificate* GetReference() const = 0; + + // Returns a PEM encoded string representation of the certificate. + virtual std::string ToPEMString() const = 0; + + // Compute the digest of the certificate given algorithm + virtual bool ComputeDigest(const std::string &algorithm, + unsigned char *digest, std::size_t size, + std::size_t *length) const = 0; +}; + +// Our identity in an SSL negotiation: a keypair and certificate (both +// with the same public key). +// This too is pretty much immutable once created. +class SSLIdentity { + public: + // Generates an identity (keypair and self-signed certificate). If + // common_name is non-empty, it will be used for the certificate's + // subject and issuer name, otherwise a random string will be used. + // Returns NULL on failure. + // Caller is responsible for freeing the returned object. + static SSLIdentity* Generate(const std::string& common_name); + + // Construct an identity from a private key and a certificate. + static SSLIdentity* FromPEMStrings(const std::string& private_key, + const std::string& certificate); + + virtual ~SSLIdentity() {} + + // Returns a new SSLIdentity object instance wrapping the same + // identity information. + // Caller is responsible for freeing the returned object. + virtual SSLIdentity* GetReference() const = 0; + + // Returns a temporary reference to the certificate. + virtual const SSLCertificate& certificate() const = 0; +}; + +} // namespace talk_base + +#endif // TALK_BASE_SSLIDENTITY_H__ diff --git a/talk/base/sslidentity_unittest.cc b/talk/base/sslidentity_unittest.cc new file mode 100644 index 000000000..3605c008a --- /dev/null +++ b/talk/base/sslidentity_unittest.cc @@ -0,0 +1,190 @@ +/* + * libjingle + * Copyright 2011, Google Inc. + * Portions Copyright 2011, RTFM, 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 + +#include "talk/base/gunit.h" +#include "talk/base/ssladapter.h" +#include "talk/base/sslidentity.h" + +const char kTestCertificate[] = "-----BEGIN CERTIFICATE-----\n" + "MIIB6TCCAVICAQYwDQYJKoZIhvcNAQEEBQAwWzELMAkGA1UEBhMCQVUxEzARBgNV\n" + "BAgTClF1ZWVuc2xhbmQxGjAYBgNVBAoTEUNyeXB0U29mdCBQdHkgTHRkMRswGQYD\n" + "VQQDExJUZXN0IENBICgxMDI0IGJpdCkwHhcNMDAxMDE2MjIzMTAzWhcNMDMwMTE0\n" + "MjIzMTAzWjBjMQswCQYDVQQGEwJBVTETMBEGA1UECBMKUXVlZW5zbGFuZDEaMBgG\n" + "A1UEChMRQ3J5cHRTb2Z0IFB0eSBMdGQxIzAhBgNVBAMTGlNlcnZlciB0ZXN0IGNl\n" + "cnQgKDUxMiBiaXQpMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJ+zw4Qnlf8SMVIP\n" + "Fe9GEcStgOY2Ww/dgNdhjeD8ckUJNP5VZkVDTGiXav6ooKXfX3j/7tdkuD8Ey2//\n" + "Kv7+ue0CAwEAATANBgkqhkiG9w0BAQQFAAOBgQCT0grFQeZaqYb5EYfk20XixZV4\n" + "GmyAbXMftG1Eo7qGiMhYzRwGNWxEYojf5PZkYZXvSqZ/ZXHXa4g59jK/rJNnaVGM\n" + "k+xIX8mxQvlV0n5O9PIha5BX5teZnkHKgL8aKKLKW1BK7YTngsfSzzaeame5iKfz\n" + "itAE+OjGF+PFKbwX8Q==\n" + "-----END CERTIFICATE-----\n"; + +const unsigned char kTestCertSha1[] = {0xA6, 0xC8, 0x59, 0xEA, + 0xC3, 0x7E, 0x6D, 0x33, + 0xCF, 0xE2, 0x69, 0x9D, + 0x74, 0xE6, 0xF6, 0x8A, + 0x9E, 0x47, 0xA7, 0xCA}; + +class SSLIdentityTest : public testing::Test { + public: + SSLIdentityTest() : + identity1_(NULL), identity2_(NULL) { + } + + ~SSLIdentityTest() { + } + + static void SetUpTestCase() { + talk_base::InitializeSSL(); + } + + virtual void SetUp() { + identity1_.reset(talk_base::SSLIdentity::Generate("test1")); + identity2_.reset(talk_base::SSLIdentity::Generate("test2")); + + ASSERT_TRUE(identity1_); + ASSERT_TRUE(identity2_); + + test_cert_.reset( + talk_base::SSLCertificate::FromPEMString(kTestCertificate)); + ASSERT_TRUE(test_cert_); + } + + void TestDigest(const std::string &algorithm, size_t expected_len, + const unsigned char *expected_digest = NULL) { + unsigned char digest1[64]; + unsigned char digest1b[64]; + unsigned char digest2[64]; + size_t digest1_len; + size_t digest1b_len; + size_t digest2_len; + bool rv; + + rv = identity1_->certificate().ComputeDigest(algorithm, + digest1, sizeof(digest1), + &digest1_len); + EXPECT_TRUE(rv); + EXPECT_EQ(expected_len, digest1_len); + + rv = identity1_->certificate().ComputeDigest(algorithm, + digest1b, sizeof(digest1b), + &digest1b_len); + EXPECT_TRUE(rv); + EXPECT_EQ(expected_len, digest1b_len); + EXPECT_EQ(0, memcmp(digest1, digest1b, expected_len)); + + + rv = identity2_->certificate().ComputeDigest(algorithm, + digest2, sizeof(digest2), + &digest2_len); + EXPECT_TRUE(rv); + EXPECT_EQ(expected_len, digest2_len); + EXPECT_NE(0, memcmp(digest1, digest2, expected_len)); + + // If we have an expected hash for the test cert, check it. + if (expected_digest) { + unsigned char digest3[64]; + size_t digest3_len; + + rv = test_cert_->ComputeDigest(algorithm, digest3, sizeof(digest3), + &digest3_len); + EXPECT_TRUE(rv); + EXPECT_EQ(expected_len, digest3_len); + EXPECT_EQ(0, memcmp(digest3, expected_digest, expected_len)); + } + } + + private: + talk_base::scoped_ptr identity1_; + talk_base::scoped_ptr identity2_; + talk_base::scoped_ptr test_cert_; +}; + +TEST_F(SSLIdentityTest, DigestSHA1) { + TestDigest(talk_base::DIGEST_SHA_1, 20, kTestCertSha1); +} + +// HASH_AlgSHA224 is not supported in the chromium linux build. +#if SSL_USE_NSS +TEST_F(SSLIdentityTest, DISABLED_DigestSHA224) { +#else +TEST_F(SSLIdentityTest, DigestSHA224) { +#endif + TestDigest(talk_base::DIGEST_SHA_224, 28); +} + +TEST_F(SSLIdentityTest, DigestSHA256) { + TestDigest(talk_base::DIGEST_SHA_256, 32); +} + +TEST_F(SSLIdentityTest, DigestSHA384) { + TestDigest(talk_base::DIGEST_SHA_384, 48); +} + +TEST_F(SSLIdentityTest, DigestSHA512) { + TestDigest(talk_base::DIGEST_SHA_512, 64); +} + +TEST_F(SSLIdentityTest, FromPEMStrings) { + static const char kRSA_PRIVATE_KEY_PEM[] = + "-----BEGIN RSA PRIVATE KEY-----\n" + "MIICXQIBAAKBgQDCueE4a9hDMZ3sbVZdlXOz9ZA+cvzie3zJ9gXnT/BCt9P4b9HE\n" + "vD/tr73YBqD3Wr5ZWScmyGYF9EMn0r3rzBxv6oooLU5TdUvOm4rzUjkCLQaQML8o\n" + "NxXq+qW/j3zUKGikLhaaAl/amaX2zSWUsRQ1CpngQ3+tmDNH4/25TncNmQIDAQAB\n" + "AoGAUcuU0Id0k10fMjYHZk4mCPzot2LD2Tr4Aznl5vFMQipHzv7hhZtx2xzMSRcX\n" + "vG+Qr6VkbcUWHgApyWubvZXCh3+N7Vo2aYdMAQ8XqmFpBdIrL5CVdVfqFfEMlgEy\n" + "LSZNG5klnrIfl3c7zQVovLr4eMqyl2oGfAqPQz75+fecv1UCQQD6wNHch9NbAG1q\n" + "yuFEhMARB6gDXb+5SdzFjjtTWW5uJfm4DcZLoYyaIZm0uxOwsUKd0Rsma+oGitS1\n" + "CXmuqfpPAkEAxszyN3vIdpD44SREEtyKZBMNOk5pEIIGdbeMJC5/XHvpxww9xkoC\n" + "+39NbvUZYd54uT+rafbx4QZKc0h9xA/HlwJBAL37lYVWy4XpPv1olWCKi9LbUCqs\n" + "vvQtyD1N1BkEayy9TQRsO09WKOcmigRqsTJwOx7DLaTgokEuspYvhagWVPUCQE/y\n" + "0+YkTbYBD1Xbs9SyBKXCU6uDJRWSdO6aZi2W1XloC9gUwDMiSJjD1Wwt/YsyYPJ+\n" + "/Hyc5yFL2l0KZimW/vkCQQCjuZ/lPcH46EuzhdbRfumDOG5N3ld7UhGI1TIRy17W\n" + "dGF90cG33/L6BfS8Ll+fkkW/2AMRk8FDvF4CZi2nfW4L\n" + "-----END RSA PRIVATE KEY-----\n"; + + static const char kCERT_PEM[] = + "-----BEGIN CERTIFICATE-----\n" + "MIIBmTCCAQICCQCPNJORW/M13DANBgkqhkiG9w0BAQUFADARMQ8wDQYDVQQDDAZ3\n" + "ZWJydGMwHhcNMTMwNjE0MjIzMDAxWhcNMTQwNjE0MjIzMDAxWjARMQ8wDQYDVQQD\n" + "DAZ3ZWJydGMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMK54Thr2EMxnext\n" + "Vl2Vc7P1kD5y/OJ7fMn2BedP8EK30/hv0cS8P+2vvdgGoPdavllZJybIZgX0QyfS\n" + "vevMHG/qiigtTlN1S86bivNSOQItBpAwvyg3Fer6pb+PfNQoaKQuFpoCX9qZpfbN\n" + "JZSxFDUKmeBDf62YM0fj/blOdw2ZAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAECMt\n" + "UZb35H8TnjGx4XPzco/kbnurMLFFWcuve/DwTsuf10Ia9N4md8LY0UtgIgtyNqWc\n" + "ZwyRMwxONF6ty3wcaIiPbGqiAa55T3YRuPibkRmck9CjrmM9JAtyvqHnpHd2TsBD\n" + "qCV42aXS3onOXDQ1ibuWq0fr0//aj0wo4KV474c=\n" + "-----END CERTIFICATE-----\n"; + + talk_base::scoped_ptr identity( + talk_base::SSLIdentity::FromPEMStrings(kRSA_PRIVATE_KEY_PEM, kCERT_PEM)); + EXPECT_TRUE(identity); + EXPECT_EQ(kCERT_PEM, identity->certificate().ToPEMString()); +} diff --git a/talk/base/sslroots.h b/talk/base/sslroots.h new file mode 100644 index 000000000..0f983cd60 --- /dev/null +++ b/talk/base/sslroots.h @@ -0,0 +1,4930 @@ +// This file is the root certificates in C form that are needed to connect to +// Google. + +// It was generated with the following command line: +// > python //depot/googleclient/talk/tools/generate_sslroots.py +// //depot/google3/security/cacerts/for_connecting_to_google/roots.pem + +/* subject:/C=SE/O=AddTrust AB/OU=AddTrust External TTP Network/CN=AddTrust External CA Root */ +/* issuer :/C=SE/O=AddTrust AB/OU=AddTrust External TTP Network/CN=AddTrust External CA Root */ + + +const unsigned char AddTrust_External_Root_certificate[1082]={ +0x30,0x82,0x04,0x36,0x30,0x82,0x03,0x1E,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x6F,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x53,0x45,0x31,0x14, +0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x13,0x0B,0x41,0x64,0x64,0x54,0x72,0x75,0x73, +0x74,0x20,0x41,0x42,0x31,0x26,0x30,0x24,0x06,0x03,0x55,0x04,0x0B,0x13,0x1D,0x41, +0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x45,0x78,0x74,0x65,0x72,0x6E,0x61,0x6C, +0x20,0x54,0x54,0x50,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x31,0x22,0x30,0x20, +0x06,0x03,0x55,0x04,0x03,0x13,0x19,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20, +0x45,0x78,0x74,0x65,0x72,0x6E,0x61,0x6C,0x20,0x43,0x41,0x20,0x52,0x6F,0x6F,0x74, +0x30,0x1E,0x17,0x0D,0x30,0x30,0x30,0x35,0x33,0x30,0x31,0x30,0x34,0x38,0x33,0x38, +0x5A,0x17,0x0D,0x32,0x30,0x30,0x35,0x33,0x30,0x31,0x30,0x34,0x38,0x33,0x38,0x5A, +0x30,0x6F,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x53,0x45,0x31, +0x14,0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x13,0x0B,0x41,0x64,0x64,0x54,0x72,0x75, +0x73,0x74,0x20,0x41,0x42,0x31,0x26,0x30,0x24,0x06,0x03,0x55,0x04,0x0B,0x13,0x1D, +0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x45,0x78,0x74,0x65,0x72,0x6E,0x61, +0x6C,0x20,0x54,0x54,0x50,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x31,0x22,0x30, +0x20,0x06,0x03,0x55,0x04,0x03,0x13,0x19,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74, +0x20,0x45,0x78,0x74,0x65,0x72,0x6E,0x61,0x6C,0x20,0x43,0x41,0x20,0x52,0x6F,0x6F, +0x74,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01, +0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01, +0x01,0x00,0xB7,0xF7,0x1A,0x33,0xE6,0xF2,0x00,0x04,0x2D,0x39,0xE0,0x4E,0x5B,0xED, +0x1F,0xBC,0x6C,0x0F,0xCD,0xB5,0xFA,0x23,0xB6,0xCE,0xDE,0x9B,0x11,0x33,0x97,0xA4, +0x29,0x4C,0x7D,0x93,0x9F,0xBD,0x4A,0xBC,0x93,0xED,0x03,0x1A,0xE3,0x8F,0xCF,0xE5, +0x6D,0x50,0x5A,0xD6,0x97,0x29,0x94,0x5A,0x80,0xB0,0x49,0x7A,0xDB,0x2E,0x95,0xFD, +0xB8,0xCA,0xBF,0x37,0x38,0x2D,0x1E,0x3E,0x91,0x41,0xAD,0x70,0x56,0xC7,0xF0,0x4F, +0x3F,0xE8,0x32,0x9E,0x74,0xCA,0xC8,0x90,0x54,0xE9,0xC6,0x5F,0x0F,0x78,0x9D,0x9A, +0x40,0x3C,0x0E,0xAC,0x61,0xAA,0x5E,0x14,0x8F,0x9E,0x87,0xA1,0x6A,0x50,0xDC,0xD7, +0x9A,0x4E,0xAF,0x05,0xB3,0xA6,0x71,0x94,0x9C,0x71,0xB3,0x50,0x60,0x0A,0xC7,0x13, +0x9D,0x38,0x07,0x86,0x02,0xA8,0xE9,0xA8,0x69,0x26,0x18,0x90,0xAB,0x4C,0xB0,0x4F, +0x23,0xAB,0x3A,0x4F,0x84,0xD8,0xDF,0xCE,0x9F,0xE1,0x69,0x6F,0xBB,0xD7,0x42,0xD7, +0x6B,0x44,0xE4,0xC7,0xAD,0xEE,0x6D,0x41,0x5F,0x72,0x5A,0x71,0x08,0x37,0xB3,0x79, +0x65,0xA4,0x59,0xA0,0x94,0x37,0xF7,0x00,0x2F,0x0D,0xC2,0x92,0x72,0xDA,0xD0,0x38, +0x72,0xDB,0x14,0xA8,0x45,0xC4,0x5D,0x2A,0x7D,0xB7,0xB4,0xD6,0xC4,0xEE,0xAC,0xCD, +0x13,0x44,0xB7,0xC9,0x2B,0xDD,0x43,0x00,0x25,0xFA,0x61,0xB9,0x69,0x6A,0x58,0x23, +0x11,0xB7,0xA7,0x33,0x8F,0x56,0x75,0x59,0xF5,0xCD,0x29,0xD7,0x46,0xB7,0x0A,0x2B, +0x65,0xB6,0xD3,0x42,0x6F,0x15,0xB2,0xB8,0x7B,0xFB,0xEF,0xE9,0x5D,0x53,0xD5,0x34, +0x5A,0x27,0x02,0x03,0x01,0x00,0x01,0xA3,0x81,0xDC,0x30,0x81,0xD9,0x30,0x1D,0x06, +0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0xAD,0xBD,0x98,0x7A,0x34,0xB4,0x26,0xF7, +0xFA,0xC4,0x26,0x54,0xEF,0x03,0xBD,0xE0,0x24,0xCB,0x54,0x1A,0x30,0x0B,0x06,0x03, +0x55,0x1D,0x0F,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13, +0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x81,0x99,0x06,0x03,0x55, +0x1D,0x23,0x04,0x81,0x91,0x30,0x81,0x8E,0x80,0x14,0xAD,0xBD,0x98,0x7A,0x34,0xB4, +0x26,0xF7,0xFA,0xC4,0x26,0x54,0xEF,0x03,0xBD,0xE0,0x24,0xCB,0x54,0x1A,0xA1,0x73, +0xA4,0x71,0x30,0x6F,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x53, +0x45,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x13,0x0B,0x41,0x64,0x64,0x54, +0x72,0x75,0x73,0x74,0x20,0x41,0x42,0x31,0x26,0x30,0x24,0x06,0x03,0x55,0x04,0x0B, +0x13,0x1D,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x45,0x78,0x74,0x65,0x72, +0x6E,0x61,0x6C,0x20,0x54,0x54,0x50,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x31, +0x22,0x30,0x20,0x06,0x03,0x55,0x04,0x03,0x13,0x19,0x41,0x64,0x64,0x54,0x72,0x75, +0x73,0x74,0x20,0x45,0x78,0x74,0x65,0x72,0x6E,0x61,0x6C,0x20,0x43,0x41,0x20,0x52, +0x6F,0x6F,0x74,0x82,0x01,0x01,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D, +0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0xB0,0x9B,0xE0,0x85,0x25,0xC2, +0xD6,0x23,0xE2,0x0F,0x96,0x06,0x92,0x9D,0x41,0x98,0x9C,0xD9,0x84,0x79,0x81,0xD9, +0x1E,0x5B,0x14,0x07,0x23,0x36,0x65,0x8F,0xB0,0xD8,0x77,0xBB,0xAC,0x41,0x6C,0x47, +0x60,0x83,0x51,0xB0,0xF9,0x32,0x3D,0xE7,0xFC,0xF6,0x26,0x13,0xC7,0x80,0x16,0xA5, +0xBF,0x5A,0xFC,0x87,0xCF,0x78,0x79,0x89,0x21,0x9A,0xE2,0x4C,0x07,0x0A,0x86,0x35, +0xBC,0xF2,0xDE,0x51,0xC4,0xD2,0x96,0xB7,0xDC,0x7E,0x4E,0xEE,0x70,0xFD,0x1C,0x39, +0xEB,0x0C,0x02,0x51,0x14,0x2D,0x8E,0xBD,0x16,0xE0,0xC1,0xDF,0x46,0x75,0xE7,0x24, +0xAD,0xEC,0xF4,0x42,0xB4,0x85,0x93,0x70,0x10,0x67,0xBA,0x9D,0x06,0x35,0x4A,0x18, +0xD3,0x2B,0x7A,0xCC,0x51,0x42,0xA1,0x7A,0x63,0xD1,0xE6,0xBB,0xA1,0xC5,0x2B,0xC2, +0x36,0xBE,0x13,0x0D,0xE6,0xBD,0x63,0x7E,0x79,0x7B,0xA7,0x09,0x0D,0x40,0xAB,0x6A, +0xDD,0x8F,0x8A,0xC3,0xF6,0xF6,0x8C,0x1A,0x42,0x05,0x51,0xD4,0x45,0xF5,0x9F,0xA7, +0x62,0x21,0x68,0x15,0x20,0x43,0x3C,0x99,0xE7,0x7C,0xBD,0x24,0xD8,0xA9,0x91,0x17, +0x73,0x88,0x3F,0x56,0x1B,0x31,0x38,0x18,0xB4,0x71,0x0F,0x9A,0xCD,0xC8,0x0E,0x9E, +0x8E,0x2E,0x1B,0xE1,0x8C,0x98,0x83,0xCB,0x1F,0x31,0xF1,0x44,0x4C,0xC6,0x04,0x73, +0x49,0x76,0x60,0x0F,0xC7,0xF8,0xBD,0x17,0x80,0x6B,0x2E,0xE9,0xCC,0x4C,0x0E,0x5A, +0x9A,0x79,0x0F,0x20,0x0A,0x2E,0xD5,0x9E,0x63,0x26,0x1E,0x55,0x92,0x94,0xD8,0x82, +0x17,0x5A,0x7B,0xD0,0xBC,0xC7,0x8F,0x4E,0x86,0x04, +}; + + +/* subject:/C=SE/O=AddTrust AB/OU=AddTrust TTP Network/CN=AddTrust Class 1 CA Root */ +/* issuer :/C=SE/O=AddTrust AB/OU=AddTrust TTP Network/CN=AddTrust Class 1 CA Root */ + + +const unsigned char AddTrust_Low_Value_Services_Root_certificate[1052]={ +0x30,0x82,0x04,0x18,0x30,0x82,0x03,0x00,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x65,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x53,0x45,0x31,0x14, +0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x13,0x0B,0x41,0x64,0x64,0x54,0x72,0x75,0x73, +0x74,0x20,0x41,0x42,0x31,0x1D,0x30,0x1B,0x06,0x03,0x55,0x04,0x0B,0x13,0x14,0x41, +0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x54,0x54,0x50,0x20,0x4E,0x65,0x74,0x77, +0x6F,0x72,0x6B,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x03,0x13,0x18,0x41,0x64, +0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x31,0x20,0x43, +0x41,0x20,0x52,0x6F,0x6F,0x74,0x30,0x1E,0x17,0x0D,0x30,0x30,0x30,0x35,0x33,0x30, +0x31,0x30,0x33,0x38,0x33,0x31,0x5A,0x17,0x0D,0x32,0x30,0x30,0x35,0x33,0x30,0x31, +0x30,0x33,0x38,0x33,0x31,0x5A,0x30,0x65,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04, +0x06,0x13,0x02,0x53,0x45,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x13,0x0B, +0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x41,0x42,0x31,0x1D,0x30,0x1B,0x06, +0x03,0x55,0x04,0x0B,0x13,0x14,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x54, +0x54,0x50,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x31,0x21,0x30,0x1F,0x06,0x03, +0x55,0x04,0x03,0x13,0x18,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x43,0x6C, +0x61,0x73,0x73,0x20,0x31,0x20,0x43,0x41,0x20,0x52,0x6F,0x6F,0x74,0x30,0x82,0x01, +0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00, +0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0x96,0x96, +0xD4,0x21,0x49,0x60,0xE2,0x6B,0xE8,0x41,0x07,0x0C,0xDE,0xC4,0xE0,0xDC,0x13,0x23, +0xCD,0xC1,0x35,0xC7,0xFB,0xD6,0x4E,0x11,0x0A,0x67,0x5E,0xF5,0x06,0x5B,0x6B,0xA5, +0x08,0x3B,0x5B,0x29,0x16,0x3A,0xE7,0x87,0xB2,0x34,0x06,0xC5,0xBC,0x05,0xA5,0x03, +0x7C,0x82,0xCB,0x29,0x10,0xAE,0xE1,0x88,0x81,0xBD,0xD6,0x9E,0xD3,0xFE,0x2D,0x56, +0xC1,0x15,0xCE,0xE3,0x26,0x9D,0x15,0x2E,0x10,0xFB,0x06,0x8F,0x30,0x04,0xDE,0xA7, +0xB4,0x63,0xB4,0xFF,0xB1,0x9C,0xAE,0x3C,0xAF,0x77,0xB6,0x56,0xC5,0xB5,0xAB,0xA2, +0xE9,0x69,0x3A,0x3D,0x0E,0x33,0x79,0x32,0x3F,0x70,0x82,0x92,0x99,0x61,0x6D,0x8D, +0x30,0x08,0x8F,0x71,0x3F,0xA6,0x48,0x57,0x19,0xF8,0x25,0xDC,0x4B,0x66,0x5C,0xA5, +0x74,0x8F,0x98,0xAE,0xC8,0xF9,0xC0,0x06,0x22,0xE7,0xAC,0x73,0xDF,0xA5,0x2E,0xFB, +0x52,0xDC,0xB1,0x15,0x65,0x20,0xFA,0x35,0x66,0x69,0xDE,0xDF,0x2C,0xF1,0x6E,0xBC, +0x30,0xDB,0x2C,0x24,0x12,0xDB,0xEB,0x35,0x35,0x68,0x90,0xCB,0x00,0xB0,0x97,0x21, +0x3D,0x74,0x21,0x23,0x65,0x34,0x2B,0xBB,0x78,0x59,0xA3,0xD6,0xE1,0x76,0x39,0x9A, +0xA4,0x49,0x8E,0x8C,0x74,0xAF,0x6E,0xA4,0x9A,0xA3,0xD9,0x9B,0xD2,0x38,0x5C,0x9B, +0xA2,0x18,0xCC,0x75,0x23,0x84,0xBE,0xEB,0xE2,0x4D,0x33,0x71,0x8E,0x1A,0xF0,0xC2, +0xF8,0xC7,0x1D,0xA2,0xAD,0x03,0x97,0x2C,0xF8,0xCF,0x25,0xC6,0xF6,0xB8,0x24,0x31, +0xB1,0x63,0x5D,0x92,0x7F,0x63,0xF0,0x25,0xC9,0x53,0x2E,0x1F,0xBF,0x4D,0x02,0x03, +0x01,0x00,0x01,0xA3,0x81,0xD2,0x30,0x81,0xCF,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E, +0x04,0x16,0x04,0x14,0x95,0xB1,0xB4,0xF0,0x94,0xB6,0xBD,0xC7,0xDA,0xD1,0x11,0x09, +0x21,0xBE,0xC1,0xAF,0x49,0xFD,0x10,0x7B,0x30,0x0B,0x06,0x03,0x55,0x1D,0x0F,0x04, +0x04,0x03,0x02,0x01,0x06,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04, +0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x81,0x8F,0x06,0x03,0x55,0x1D,0x23,0x04,0x81, +0x87,0x30,0x81,0x84,0x80,0x14,0x95,0xB1,0xB4,0xF0,0x94,0xB6,0xBD,0xC7,0xDA,0xD1, +0x11,0x09,0x21,0xBE,0xC1,0xAF,0x49,0xFD,0x10,0x7B,0xA1,0x69,0xA4,0x67,0x30,0x65, +0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x53,0x45,0x31,0x14,0x30, +0x12,0x06,0x03,0x55,0x04,0x0A,0x13,0x0B,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74, +0x20,0x41,0x42,0x31,0x1D,0x30,0x1B,0x06,0x03,0x55,0x04,0x0B,0x13,0x14,0x41,0x64, +0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x54,0x54,0x50,0x20,0x4E,0x65,0x74,0x77,0x6F, +0x72,0x6B,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x03,0x13,0x18,0x41,0x64,0x64, +0x54,0x72,0x75,0x73,0x74,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x31,0x20,0x43,0x41, +0x20,0x52,0x6F,0x6F,0x74,0x82,0x01,0x01,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x2C,0x6D,0x64,0x1B, +0x1F,0xCD,0x0D,0xDD,0xB9,0x01,0xFA,0x96,0x63,0x34,0x32,0x48,0x47,0x99,0xAE,0x97, +0xED,0xFD,0x72,0x16,0xA6,0x73,0x47,0x5A,0xF4,0xEB,0xDD,0xE9,0xF5,0xD6,0xFB,0x45, +0xCC,0x29,0x89,0x44,0x5D,0xBF,0x46,0x39,0x3D,0xE8,0xEE,0xBC,0x4D,0x54,0x86,0x1E, +0x1D,0x6C,0xE3,0x17,0x27,0x43,0xE1,0x89,0x56,0x2B,0xA9,0x6F,0x72,0x4E,0x49,0x33, +0xE3,0x72,0x7C,0x2A,0x23,0x9A,0xBC,0x3E,0xFF,0x28,0x2A,0xED,0xA3,0xFF,0x1C,0x23, +0xBA,0x43,0x57,0x09,0x67,0x4D,0x4B,0x62,0x06,0x2D,0xF8,0xFF,0x6C,0x9D,0x60,0x1E, +0xD8,0x1C,0x4B,0x7D,0xB5,0x31,0x2F,0xD9,0xD0,0x7C,0x5D,0xF8,0xDE,0x6B,0x83,0x18, +0x78,0x37,0x57,0x2F,0xE8,0x33,0x07,0x67,0xDF,0x1E,0xC7,0x6B,0x2A,0x95,0x76,0xAE, +0x8F,0x57,0xA3,0xF0,0xF4,0x52,0xB4,0xA9,0x53,0x08,0xCF,0xE0,0x4F,0xD3,0x7A,0x53, +0x8B,0xFD,0xBB,0x1C,0x56,0x36,0xF2,0xFE,0xB2,0xB6,0xE5,0x76,0xBB,0xD5,0x22,0x65, +0xA7,0x3F,0xFE,0xD1,0x66,0xAD,0x0B,0xBC,0x6B,0x99,0x86,0xEF,0x3F,0x7D,0xF3,0x18, +0x32,0xCA,0x7B,0xC6,0xE3,0xAB,0x64,0x46,0x95,0xF8,0x26,0x69,0xD9,0x55,0x83,0x7B, +0x2C,0x96,0x07,0xFF,0x59,0x2C,0x44,0xA3,0xC6,0xE5,0xE9,0xA9,0xDC,0xA1,0x63,0x80, +0x5A,0x21,0x5E,0x21,0xCF,0x53,0x54,0xF0,0xBA,0x6F,0x89,0xDB,0xA8,0xAA,0x95,0xCF, +0x8B,0xE3,0x71,0xCC,0x1E,0x1B,0x20,0x44,0x08,0xC0,0x7A,0xB6,0x40,0xFD,0xC4,0xE4, +0x35,0xE1,0x1D,0x16,0x1C,0xD0,0xBC,0x2B,0x8E,0xD6,0x71,0xD9, +}; + + +/* subject:/C=SE/O=AddTrust AB/OU=AddTrust TTP Network/CN=AddTrust Public CA Root */ +/* issuer :/C=SE/O=AddTrust AB/OU=AddTrust TTP Network/CN=AddTrust Public CA Root */ + + +const unsigned char AddTrust_Public_Services_Root_certificate[1049]={ +0x30,0x82,0x04,0x15,0x30,0x82,0x02,0xFD,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x64,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x53,0x45,0x31,0x14, +0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x13,0x0B,0x41,0x64,0x64,0x54,0x72,0x75,0x73, +0x74,0x20,0x41,0x42,0x31,0x1D,0x30,0x1B,0x06,0x03,0x55,0x04,0x0B,0x13,0x14,0x41, +0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x54,0x54,0x50,0x20,0x4E,0x65,0x74,0x77, +0x6F,0x72,0x6B,0x31,0x20,0x30,0x1E,0x06,0x03,0x55,0x04,0x03,0x13,0x17,0x41,0x64, +0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x50,0x75,0x62,0x6C,0x69,0x63,0x20,0x43,0x41, +0x20,0x52,0x6F,0x6F,0x74,0x30,0x1E,0x17,0x0D,0x30,0x30,0x30,0x35,0x33,0x30,0x31, +0x30,0x34,0x31,0x35,0x30,0x5A,0x17,0x0D,0x32,0x30,0x30,0x35,0x33,0x30,0x31,0x30, +0x34,0x31,0x35,0x30,0x5A,0x30,0x64,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06, +0x13,0x02,0x53,0x45,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x13,0x0B,0x41, +0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x41,0x42,0x31,0x1D,0x30,0x1B,0x06,0x03, +0x55,0x04,0x0B,0x13,0x14,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x54,0x54, +0x50,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x31,0x20,0x30,0x1E,0x06,0x03,0x55, +0x04,0x03,0x13,0x17,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x50,0x75,0x62, +0x6C,0x69,0x63,0x20,0x43,0x41,0x20,0x52,0x6F,0x6F,0x74,0x30,0x82,0x01,0x22,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82, +0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xE9,0x1A,0x30,0x8F, +0x83,0x88,0x14,0xC1,0x20,0xD8,0x3C,0x9B,0x8F,0x1B,0x7E,0x03,0x74,0xBB,0xDA,0x69, +0xD3,0x46,0xA5,0xF8,0x8E,0xC2,0x0C,0x11,0x90,0x51,0xA5,0x2F,0x66,0x54,0x40,0x55, +0xEA,0xDB,0x1F,0x4A,0x56,0xEE,0x9F,0x23,0x6E,0xF4,0x39,0xCB,0xA1,0xB9,0x6F,0xF2, +0x7E,0xF9,0x5D,0x87,0x26,0x61,0x9E,0x1C,0xF8,0xE2,0xEC,0xA6,0x81,0xF8,0x21,0xC5, +0x24,0xCC,0x11,0x0C,0x3F,0xDB,0x26,0x72,0x7A,0xC7,0x01,0x97,0x07,0x17,0xF9,0xD7, +0x18,0x2C,0x30,0x7D,0x0E,0x7A,0x1E,0x62,0x1E,0xC6,0x4B,0xC0,0xFD,0x7D,0x62,0x77, +0xD3,0x44,0x1E,0x27,0xF6,0x3F,0x4B,0x44,0xB3,0xB7,0x38,0xD9,0x39,0x1F,0x60,0xD5, +0x51,0x92,0x73,0x03,0xB4,0x00,0x69,0xE3,0xF3,0x14,0x4E,0xEE,0xD1,0xDC,0x09,0xCF, +0x77,0x34,0x46,0x50,0xB0,0xF8,0x11,0xF2,0xFE,0x38,0x79,0xF7,0x07,0x39,0xFE,0x51, +0x92,0x97,0x0B,0x5B,0x08,0x5F,0x34,0x86,0x01,0xAD,0x88,0x97,0xEB,0x66,0xCD,0x5E, +0xD1,0xFF,0xDC,0x7D,0xF2,0x84,0xDA,0xBA,0x77,0xAD,0xDC,0x80,0x08,0xC7,0xA7,0x87, +0xD6,0x55,0x9F,0x97,0x6A,0xE8,0xC8,0x11,0x64,0xBA,0xE7,0x19,0x29,0x3F,0x11,0xB3, +0x78,0x90,0x84,0x20,0x52,0x5B,0x11,0xEF,0x78,0xD0,0x83,0xF6,0xD5,0x48,0x90,0xD0, +0x30,0x1C,0xCF,0x80,0xF9,0x60,0xFE,0x79,0xE4,0x88,0xF2,0xDD,0x00,0xEB,0x94,0x45, +0xEB,0x65,0x94,0x69,0x40,0xBA,0xC0,0xD5,0xB4,0xB8,0xBA,0x7D,0x04,0x11,0xA8,0xEB, +0x31,0x05,0x96,0x94,0x4E,0x58,0x21,0x8E,0x9F,0xD0,0x60,0xFD,0x02,0x03,0x01,0x00, +0x01,0xA3,0x81,0xD1,0x30,0x81,0xCE,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16, +0x04,0x14,0x81,0x3E,0x37,0xD8,0x92,0xB0,0x1F,0x77,0x9F,0x5C,0xB4,0xAB,0x73,0xAA, +0xE7,0xF6,0x34,0x60,0x2F,0xFA,0x30,0x0B,0x06,0x03,0x55,0x1D,0x0F,0x04,0x04,0x03, +0x02,0x01,0x06,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30, +0x03,0x01,0x01,0xFF,0x30,0x81,0x8E,0x06,0x03,0x55,0x1D,0x23,0x04,0x81,0x86,0x30, +0x81,0x83,0x80,0x14,0x81,0x3E,0x37,0xD8,0x92,0xB0,0x1F,0x77,0x9F,0x5C,0xB4,0xAB, +0x73,0xAA,0xE7,0xF6,0x34,0x60,0x2F,0xFA,0xA1,0x68,0xA4,0x66,0x30,0x64,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x53,0x45,0x31,0x14,0x30,0x12,0x06, +0x03,0x55,0x04,0x0A,0x13,0x0B,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x41, +0x42,0x31,0x1D,0x30,0x1B,0x06,0x03,0x55,0x04,0x0B,0x13,0x14,0x41,0x64,0x64,0x54, +0x72,0x75,0x73,0x74,0x20,0x54,0x54,0x50,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B, +0x31,0x20,0x30,0x1E,0x06,0x03,0x55,0x04,0x03,0x13,0x17,0x41,0x64,0x64,0x54,0x72, +0x75,0x73,0x74,0x20,0x50,0x75,0x62,0x6C,0x69,0x63,0x20,0x43,0x41,0x20,0x52,0x6F, +0x6F,0x74,0x82,0x01,0x01,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01, +0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x03,0xF7,0x15,0x4A,0xF8,0x24,0xDA, +0x23,0x56,0x16,0x93,0x76,0xDD,0x36,0x28,0xB9,0xAE,0x1B,0xB8,0xC3,0xF1,0x64,0xBA, +0x20,0x18,0x78,0x95,0x29,0x27,0x57,0x05,0xBC,0x7C,0x2A,0xF4,0xB9,0x51,0x55,0xDA, +0x87,0x02,0xDE,0x0F,0x16,0x17,0x31,0xF8,0xAA,0x79,0x2E,0x09,0x13,0xBB,0xAF,0xB2, +0x20,0x19,0x12,0xE5,0x93,0xF9,0x4B,0xF9,0x83,0xE8,0x44,0xD5,0xB2,0x41,0x25,0xBF, +0x88,0x75,0x6F,0xFF,0x10,0xFC,0x4A,0x54,0xD0,0x5F,0xF0,0xFA,0xEF,0x36,0x73,0x7D, +0x1B,0x36,0x45,0xC6,0x21,0x6D,0xB4,0x15,0xB8,0x4E,0xCF,0x9C,0x5C,0xA5,0x3D,0x5A, +0x00,0x8E,0x06,0xE3,0x3C,0x6B,0x32,0x7B,0xF2,0x9F,0xF0,0xB6,0xFD,0xDF,0xF0,0x28, +0x18,0x48,0xF0,0xC6,0xBC,0xD0,0xBF,0x34,0x80,0x96,0xC2,0x4A,0xB1,0x6D,0x8E,0xC7, +0x90,0x45,0xDE,0x2F,0x67,0xAC,0x45,0x04,0xA3,0x7A,0xDC,0x55,0x92,0xC9,0x47,0x66, +0xD8,0x1A,0x8C,0xC7,0xED,0x9C,0x4E,0x9A,0xE0,0x12,0xBB,0xB5,0x6A,0x4C,0x84,0xE1, +0xE1,0x22,0x0D,0x87,0x00,0x64,0xFE,0x8C,0x7D,0x62,0x39,0x65,0xA6,0xEF,0x42,0xB6, +0x80,0x25,0x12,0x61,0x01,0xA8,0x24,0x13,0x70,0x00,0x11,0x26,0x5F,0xFA,0x35,0x50, +0xC5,0x48,0xCC,0x06,0x47,0xE8,0x27,0xD8,0x70,0x8D,0x5F,0x64,0xE6,0xA1,0x44,0x26, +0x5E,0x22,0xEC,0x92,0xCD,0xFF,0x42,0x9A,0x44,0x21,0x6D,0x5C,0xC5,0xE3,0x22,0x1D, +0x5F,0x47,0x12,0xE7,0xCE,0x5F,0x5D,0xFA,0xD8,0xAA,0xB1,0x33,0x2D,0xD9,0x76,0xF2, +0x4E,0x3A,0x33,0x0C,0x2B,0xB3,0x2D,0x90,0x06, +}; + + +/* subject:/C=SE/O=AddTrust AB/OU=AddTrust TTP Network/CN=AddTrust Qualified CA Root */ +/* issuer :/C=SE/O=AddTrust AB/OU=AddTrust TTP Network/CN=AddTrust Qualified CA Root */ + + +const unsigned char AddTrust_Qualified_Certificates_Root_certificate[1058]={ +0x30,0x82,0x04,0x1E,0x30,0x82,0x03,0x06,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x67,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x53,0x45,0x31,0x14, +0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x13,0x0B,0x41,0x64,0x64,0x54,0x72,0x75,0x73, +0x74,0x20,0x41,0x42,0x31,0x1D,0x30,0x1B,0x06,0x03,0x55,0x04,0x0B,0x13,0x14,0x41, +0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x54,0x54,0x50,0x20,0x4E,0x65,0x74,0x77, +0x6F,0x72,0x6B,0x31,0x23,0x30,0x21,0x06,0x03,0x55,0x04,0x03,0x13,0x1A,0x41,0x64, +0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x51,0x75,0x61,0x6C,0x69,0x66,0x69,0x65,0x64, +0x20,0x43,0x41,0x20,0x52,0x6F,0x6F,0x74,0x30,0x1E,0x17,0x0D,0x30,0x30,0x30,0x35, +0x33,0x30,0x31,0x30,0x34,0x34,0x35,0x30,0x5A,0x17,0x0D,0x32,0x30,0x30,0x35,0x33, +0x30,0x31,0x30,0x34,0x34,0x35,0x30,0x5A,0x30,0x67,0x31,0x0B,0x30,0x09,0x06,0x03, +0x55,0x04,0x06,0x13,0x02,0x53,0x45,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04,0x0A, +0x13,0x0B,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x41,0x42,0x31,0x1D,0x30, +0x1B,0x06,0x03,0x55,0x04,0x0B,0x13,0x14,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74, +0x20,0x54,0x54,0x50,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x31,0x23,0x30,0x21, +0x06,0x03,0x55,0x04,0x03,0x13,0x1A,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20, +0x51,0x75,0x61,0x6C,0x69,0x66,0x69,0x65,0x64,0x20,0x43,0x41,0x20,0x52,0x6F,0x6F, +0x74,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01, +0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01, +0x01,0x00,0xE4,0x1E,0x9A,0xFE,0xDC,0x09,0x5A,0x87,0xA4,0x9F,0x47,0xBE,0x11,0x5F, +0xAF,0x84,0x34,0xDB,0x62,0x3C,0x79,0x78,0xB7,0xE9,0x30,0xB5,0xEC,0x0C,0x1C,0x2A, +0xC4,0x16,0xFF,0xE0,0xEC,0x71,0xEB,0x8A,0xF5,0x11,0x6E,0xED,0x4F,0x0D,0x91,0xD2, +0x12,0x18,0x2D,0x49,0x15,0x01,0xC2,0xA4,0x22,0x13,0xC7,0x11,0x64,0xFF,0x22,0x12, +0x9A,0xB9,0x8E,0x5C,0x2F,0x08,0xCF,0x71,0x6A,0xB3,0x67,0x01,0x59,0xF1,0x5D,0x46, +0xF3,0xB0,0x78,0xA5,0xF6,0x0E,0x42,0x7A,0xE3,0x7F,0x1B,0xCC,0xD0,0xF0,0xB7,0x28, +0xFD,0x2A,0xEA,0x9E,0xB3,0xB0,0xB9,0x04,0xAA,0xFD,0xF6,0xC7,0xB4,0xB1,0xB8,0x2A, +0xA0,0xFB,0x58,0xF1,0x19,0xA0,0x6F,0x70,0x25,0x7E,0x3E,0x69,0x4A,0x7F,0x0F,0x22, +0xD8,0xEF,0xAD,0x08,0x11,0x9A,0x29,0x99,0xE1,0xAA,0x44,0x45,0x9A,0x12,0x5E,0x3E, +0x9D,0x6D,0x52,0xFC,0xE7,0xA0,0x3D,0x68,0x2F,0xF0,0x4B,0x70,0x7C,0x13,0x38,0xAD, +0xBC,0x15,0x25,0xF1,0xD6,0xCE,0xAB,0xA2,0xC0,0x31,0xD6,0x2F,0x9F,0xE0,0xFF,0x14, +0x59,0xFC,0x84,0x93,0xD9,0x87,0x7C,0x4C,0x54,0x13,0xEB,0x9F,0xD1,0x2D,0x11,0xF8, +0x18,0x3A,0x3A,0xDE,0x25,0xD9,0xF7,0xD3,0x40,0xED,0xA4,0x06,0x12,0xC4,0x3B,0xE1, +0x91,0xC1,0x56,0x35,0xF0,0x14,0xDC,0x65,0x36,0x09,0x6E,0xAB,0xA4,0x07,0xC7,0x35, +0xD1,0xC2,0x03,0x33,0x36,0x5B,0x75,0x26,0x6D,0x42,0xF1,0x12,0x6B,0x43,0x6F,0x4B, +0x71,0x94,0xFA,0x34,0x1D,0xED,0x13,0x6E,0xCA,0x80,0x7F,0x98,0x2F,0x6C,0xB9,0x65, +0xD8,0xE9,0x02,0x03,0x01,0x00,0x01,0xA3,0x81,0xD4,0x30,0x81,0xD1,0x30,0x1D,0x06, +0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x39,0x95,0x8B,0x62,0x8B,0x5C,0xC9,0xD4, +0x80,0xBA,0x58,0x0F,0x97,0x3F,0x15,0x08,0x43,0xCC,0x98,0xA7,0x30,0x0B,0x06,0x03, +0x55,0x1D,0x0F,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13, +0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x81,0x91,0x06,0x03,0x55, +0x1D,0x23,0x04,0x81,0x89,0x30,0x81,0x86,0x80,0x14,0x39,0x95,0x8B,0x62,0x8B,0x5C, +0xC9,0xD4,0x80,0xBA,0x58,0x0F,0x97,0x3F,0x15,0x08,0x43,0xCC,0x98,0xA7,0xA1,0x6B, +0xA4,0x69,0x30,0x67,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x53, +0x45,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x13,0x0B,0x41,0x64,0x64,0x54, +0x72,0x75,0x73,0x74,0x20,0x41,0x42,0x31,0x1D,0x30,0x1B,0x06,0x03,0x55,0x04,0x0B, +0x13,0x14,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x54,0x54,0x50,0x20,0x4E, +0x65,0x74,0x77,0x6F,0x72,0x6B,0x31,0x23,0x30,0x21,0x06,0x03,0x55,0x04,0x03,0x13, +0x1A,0x41,0x64,0x64,0x54,0x72,0x75,0x73,0x74,0x20,0x51,0x75,0x61,0x6C,0x69,0x66, +0x69,0x65,0x64,0x20,0x43,0x41,0x20,0x52,0x6F,0x6F,0x74,0x82,0x01,0x01,0x30,0x0D, +0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01, +0x01,0x00,0x19,0xAB,0x75,0xEA,0xF8,0x8B,0x65,0x61,0x95,0x13,0xBA,0x69,0x04,0xEF, +0x86,0xCA,0x13,0xA0,0xC7,0xAA,0x4F,0x64,0x1B,0x3F,0x18,0xF6,0xA8,0x2D,0x2C,0x55, +0x8F,0x05,0xB7,0x30,0xEA,0x42,0x6A,0x1D,0xC0,0x25,0x51,0x2D,0xA7,0xBF,0x0C,0xB3, +0xED,0xEF,0x08,0x7F,0x6C,0x3C,0x46,0x1A,0xEA,0x18,0x43,0xDF,0x76,0xCC,0xF9,0x66, +0x86,0x9C,0x2C,0x68,0xF5,0xE9,0x17,0xF8,0x31,0xB3,0x18,0xC4,0xD6,0x48,0x7D,0x23, +0x4C,0x68,0xC1,0x7E,0xBB,0x01,0x14,0x6F,0xC5,0xD9,0x6E,0xDE,0xBB,0x04,0x42,0x6A, +0xF8,0xF6,0x5C,0x7D,0xE5,0xDA,0xFA,0x87,0xEB,0x0D,0x35,0x52,0x67,0xD0,0x9E,0x97, +0x76,0x05,0x93,0x3F,0x95,0xC7,0x01,0xE6,0x69,0x55,0x38,0x7F,0x10,0x61,0x99,0xC9, +0xE3,0x5F,0xA6,0xCA,0x3E,0x82,0x63,0x48,0xAA,0xE2,0x08,0x48,0x3E,0xAA,0xF2,0xB2, +0x85,0x62,0xA6,0xB4,0xA7,0xD9,0xBD,0x37,0x9C,0x68,0xB5,0x2D,0x56,0x7D,0xB0,0xB7, +0x3F,0xA0,0xB1,0x07,0xD6,0xE9,0x4F,0xDC,0xDE,0x45,0x71,0x30,0x32,0x7F,0x1B,0x2E, +0x09,0xF9,0xBF,0x52,0xA1,0xEE,0xC2,0x80,0x3E,0x06,0x5C,0x2E,0x55,0x40,0xC1,0x1B, +0xF5,0x70,0x45,0xB0,0xDC,0x5D,0xFA,0xF6,0x72,0x5A,0x77,0xD2,0x63,0xCD,0xCF,0x58, +0x89,0x00,0x42,0x63,0x3F,0x79,0x39,0xD0,0x44,0xB0,0x82,0x6E,0x41,0x19,0xE8,0xDD, +0xE0,0xC1,0x88,0x5A,0xD1,0x1E,0x71,0x93,0x1F,0x24,0x30,0x74,0xE5,0x1E,0xA8,0xDE, +0x3C,0x27,0x37,0x7F,0x83,0xAE,0x9E,0x77,0xCF,0xF0,0x30,0xB1,0xFF,0x4B,0x99,0xE8, +0xC6,0xA1, +}; + + +/* subject:/C=US/O=AffirmTrust/CN=AffirmTrust Commercial */ +/* issuer :/C=US/O=AffirmTrust/CN=AffirmTrust Commercial */ + + +const unsigned char AffirmTrust_Commercial_certificate[848]={ +0x30,0x82,0x03,0x4C,0x30,0x82,0x02,0x34,0xA0,0x03,0x02,0x01,0x02,0x02,0x08,0x77, +0x77,0x06,0x27,0x26,0xA9,0xB1,0x7C,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7, +0x0D,0x01,0x01,0x0B,0x05,0x00,0x30,0x44,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04, +0x06,0x13,0x02,0x55,0x53,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x0C,0x0B, +0x41,0x66,0x66,0x69,0x72,0x6D,0x54,0x72,0x75,0x73,0x74,0x31,0x1F,0x30,0x1D,0x06, +0x03,0x55,0x04,0x03,0x0C,0x16,0x41,0x66,0x66,0x69,0x72,0x6D,0x54,0x72,0x75,0x73, +0x74,0x20,0x43,0x6F,0x6D,0x6D,0x65,0x72,0x63,0x69,0x61,0x6C,0x30,0x1E,0x17,0x0D, +0x31,0x30,0x30,0x31,0x32,0x39,0x31,0x34,0x30,0x36,0x30,0x36,0x5A,0x17,0x0D,0x33, +0x30,0x31,0x32,0x33,0x31,0x31,0x34,0x30,0x36,0x30,0x36,0x5A,0x30,0x44,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x14,0x30,0x12,0x06, +0x03,0x55,0x04,0x0A,0x0C,0x0B,0x41,0x66,0x66,0x69,0x72,0x6D,0x54,0x72,0x75,0x73, +0x74,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x03,0x0C,0x16,0x41,0x66,0x66,0x69, +0x72,0x6D,0x54,0x72,0x75,0x73,0x74,0x20,0x43,0x6F,0x6D,0x6D,0x65,0x72,0x63,0x69, +0x61,0x6C,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D, +0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82, +0x01,0x01,0x00,0xF6,0x1B,0x4F,0x67,0x07,0x2B,0xA1,0x15,0xF5,0x06,0x22,0xCB,0x1F, +0x01,0xB2,0xE3,0x73,0x45,0x06,0x44,0x49,0x2C,0xBB,0x49,0x25,0x14,0xD6,0xCE,0xC3, +0xB7,0xAB,0x2C,0x4F,0xC6,0x41,0x32,0x94,0x57,0xFA,0x12,0xA7,0x5B,0x0E,0xE2,0x8F, +0x1F,0x1E,0x86,0x19,0xA7,0xAA,0xB5,0x2D,0xB9,0x5F,0x0D,0x8A,0xC2,0xAF,0x85,0x35, +0x79,0x32,0x2D,0xBB,0x1C,0x62,0x37,0xF2,0xB1,0x5B,0x4A,0x3D,0xCA,0xCD,0x71,0x5F, +0xE9,0x42,0xBE,0x94,0xE8,0xC8,0xDE,0xF9,0x22,0x48,0x64,0xC6,0xE5,0xAB,0xC6,0x2B, +0x6D,0xAD,0x05,0xF0,0xFA,0xD5,0x0B,0xCF,0x9A,0xE5,0xF0,0x50,0xA4,0x8B,0x3B,0x47, +0xA5,0x23,0x5B,0x7A,0x7A,0xF8,0x33,0x3F,0xB8,0xEF,0x99,0x97,0xE3,0x20,0xC1,0xD6, +0x28,0x89,0xCF,0x94,0xFB,0xB9,0x45,0xED,0xE3,0x40,0x17,0x11,0xD4,0x74,0xF0,0x0B, +0x31,0xE2,0x2B,0x26,0x6A,0x9B,0x4C,0x57,0xAE,0xAC,0x20,0x3E,0xBA,0x45,0x7A,0x05, +0xF3,0xBD,0x9B,0x69,0x15,0xAE,0x7D,0x4E,0x20,0x63,0xC4,0x35,0x76,0x3A,0x07,0x02, +0xC9,0x37,0xFD,0xC7,0x47,0xEE,0xE8,0xF1,0x76,0x1D,0x73,0x15,0xF2,0x97,0xA4,0xB5, +0xC8,0x7A,0x79,0xD9,0x42,0xAA,0x2B,0x7F,0x5C,0xFE,0xCE,0x26,0x4F,0xA3,0x66,0x81, +0x35,0xAF,0x44,0xBA,0x54,0x1E,0x1C,0x30,0x32,0x65,0x9D,0xE6,0x3C,0x93,0x5E,0x50, +0x4E,0x7A,0xE3,0x3A,0xD4,0x6E,0xCC,0x1A,0xFB,0xF9,0xD2,0x37,0xAE,0x24,0x2A,0xAB, +0x57,0x03,0x22,0x28,0x0D,0x49,0x75,0x7F,0xB7,0x28,0xDA,0x75,0xBF,0x8E,0xE3,0xDC, +0x0E,0x79,0x31,0x02,0x03,0x01,0x00,0x01,0xA3,0x42,0x30,0x40,0x30,0x1D,0x06,0x03, +0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x9D,0x93,0xC6,0x53,0x8B,0x5E,0xCA,0xAF,0x3F, +0x9F,0x1E,0x0F,0xE5,0x99,0x95,0xBC,0x24,0xF6,0x94,0x8F,0x30,0x0F,0x06,0x03,0x55, +0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03, +0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x0D,0x06,0x09, +0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x03,0x82,0x01,0x01,0x00, +0x58,0xAC,0xF4,0x04,0x0E,0xCD,0xC0,0x0D,0xFF,0x0A,0xFD,0xD4,0xBA,0x16,0x5F,0x29, +0xBD,0x7B,0x68,0x99,0x58,0x49,0xD2,0xB4,0x1D,0x37,0x4D,0x7F,0x27,0x7D,0x46,0x06, +0x5D,0x43,0xC6,0x86,0x2E,0x3E,0x73,0xB2,0x26,0x7D,0x4F,0x93,0xA9,0xB6,0xC4,0x2A, +0x9A,0xAB,0x21,0x97,0x14,0xB1,0xDE,0x8C,0xD3,0xAB,0x89,0x15,0xD8,0x6B,0x24,0xD4, +0xF1,0x16,0xAE,0xD8,0xA4,0x5C,0xD4,0x7F,0x51,0x8E,0xED,0x18,0x01,0xB1,0x93,0x63, +0xBD,0xBC,0xF8,0x61,0x80,0x9A,0x9E,0xB1,0xCE,0x42,0x70,0xE2,0xA9,0x7D,0x06,0x25, +0x7D,0x27,0xA1,0xFE,0x6F,0xEC,0xB3,0x1E,0x24,0xDA,0xE3,0x4B,0x55,0x1A,0x00,0x3B, +0x35,0xB4,0x3B,0xD9,0xD7,0x5D,0x30,0xFD,0x81,0x13,0x89,0xF2,0xC2,0x06,0x2B,0xED, +0x67,0xC4,0x8E,0xC9,0x43,0xB2,0x5C,0x6B,0x15,0x89,0x02,0xBC,0x62,0xFC,0x4E,0xF2, +0xB5,0x33,0xAA,0xB2,0x6F,0xD3,0x0A,0xA2,0x50,0xE3,0xF6,0x3B,0xE8,0x2E,0x44,0xC2, +0xDB,0x66,0x38,0xA9,0x33,0x56,0x48,0xF1,0x6D,0x1B,0x33,0x8D,0x0D,0x8C,0x3F,0x60, +0x37,0x9D,0xD3,0xCA,0x6D,0x7E,0x34,0x7E,0x0D,0x9F,0x72,0x76,0x8B,0x1B,0x9F,0x72, +0xFD,0x52,0x35,0x41,0x45,0x02,0x96,0x2F,0x1C,0xB2,0x9A,0x73,0x49,0x21,0xB1,0x49, +0x47,0x45,0x47,0xB4,0xEF,0x6A,0x34,0x11,0xC9,0x4D,0x9A,0xCC,0x59,0xB7,0xD6,0x02, +0x9E,0x5A,0x4E,0x65,0xB5,0x94,0xAE,0x1B,0xDF,0x29,0xB0,0x16,0xF1,0xBF,0x00,0x9E, +0x07,0x3A,0x17,0x64,0xB5,0x04,0xB5,0x23,0x21,0x99,0x0A,0x95,0x3B,0x97,0x7C,0xEF, +}; + + +/* subject:/C=US/O=AffirmTrust/CN=AffirmTrust Networking */ +/* issuer :/C=US/O=AffirmTrust/CN=AffirmTrust Networking */ + + +const unsigned char AffirmTrust_Networking_certificate[848]={ +0x30,0x82,0x03,0x4C,0x30,0x82,0x02,0x34,0xA0,0x03,0x02,0x01,0x02,0x02,0x08,0x7C, +0x4F,0x04,0x39,0x1C,0xD4,0x99,0x2D,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7, +0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x44,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04, +0x06,0x13,0x02,0x55,0x53,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x0C,0x0B, +0x41,0x66,0x66,0x69,0x72,0x6D,0x54,0x72,0x75,0x73,0x74,0x31,0x1F,0x30,0x1D,0x06, +0x03,0x55,0x04,0x03,0x0C,0x16,0x41,0x66,0x66,0x69,0x72,0x6D,0x54,0x72,0x75,0x73, +0x74,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x69,0x6E,0x67,0x30,0x1E,0x17,0x0D, +0x31,0x30,0x30,0x31,0x32,0x39,0x31,0x34,0x30,0x38,0x32,0x34,0x5A,0x17,0x0D,0x33, +0x30,0x31,0x32,0x33,0x31,0x31,0x34,0x30,0x38,0x32,0x34,0x5A,0x30,0x44,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x14,0x30,0x12,0x06, +0x03,0x55,0x04,0x0A,0x0C,0x0B,0x41,0x66,0x66,0x69,0x72,0x6D,0x54,0x72,0x75,0x73, +0x74,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x03,0x0C,0x16,0x41,0x66,0x66,0x69, +0x72,0x6D,0x54,0x72,0x75,0x73,0x74,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x69, +0x6E,0x67,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D, +0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82, +0x01,0x01,0x00,0xB4,0x84,0xCC,0x33,0x17,0x2E,0x6B,0x94,0x6C,0x6B,0x61,0x52,0xA0, +0xEB,0xA3,0xCF,0x79,0x94,0x4C,0xE5,0x94,0x80,0x99,0xCB,0x55,0x64,0x44,0x65,0x8F, +0x67,0x64,0xE2,0x06,0xE3,0x5C,0x37,0x49,0xF6,0x2F,0x9B,0x84,0x84,0x1E,0x2D,0xF2, +0x60,0x9D,0x30,0x4E,0xCC,0x84,0x85,0xE2,0x2C,0xCF,0x1E,0x9E,0xFE,0x36,0xAB,0x33, +0x77,0x35,0x44,0xD8,0x35,0x96,0x1A,0x3D,0x36,0xE8,0x7A,0x0E,0xD8,0xD5,0x47,0xA1, +0x6A,0x69,0x8B,0xD9,0xFC,0xBB,0x3A,0xAE,0x79,0x5A,0xD5,0xF4,0xD6,0x71,0xBB,0x9A, +0x90,0x23,0x6B,0x9A,0xB7,0x88,0x74,0x87,0x0C,0x1E,0x5F,0xB9,0x9E,0x2D,0xFA,0xAB, +0x53,0x2B,0xDC,0xBB,0x76,0x3E,0x93,0x4C,0x08,0x08,0x8C,0x1E,0xA2,0x23,0x1C,0xD4, +0x6A,0xAD,0x22,0xBA,0x99,0x01,0x2E,0x6D,0x65,0xCB,0xBE,0x24,0x66,0x55,0x24,0x4B, +0x40,0x44,0xB1,0x1B,0xD7,0xE1,0xC2,0x85,0xC0,0xDE,0x10,0x3F,0x3D,0xED,0xB8,0xFC, +0xF1,0xF1,0x23,0x53,0xDC,0xBF,0x65,0x97,0x6F,0xD9,0xF9,0x40,0x71,0x8D,0x7D,0xBD, +0x95,0xD4,0xCE,0xBE,0xA0,0x5E,0x27,0x23,0xDE,0xFD,0xA6,0xD0,0x26,0x0E,0x00,0x29, +0xEB,0x3C,0x46,0xF0,0x3D,0x60,0xBF,0x3F,0x50,0xD2,0xDC,0x26,0x41,0x51,0x9E,0x14, +0x37,0x42,0x04,0xA3,0x70,0x57,0xA8,0x1B,0x87,0xED,0x2D,0xFA,0x7B,0xEE,0x8C,0x0A, +0xE3,0xA9,0x66,0x89,0x19,0xCB,0x41,0xF9,0xDD,0x44,0x36,0x61,0xCF,0xE2,0x77,0x46, +0xC8,0x7D,0xF6,0xF4,0x92,0x81,0x36,0xFD,0xDB,0x34,0xF1,0x72,0x7E,0xF3,0x0C,0x16, +0xBD,0xB4,0x15,0x02,0x03,0x01,0x00,0x01,0xA3,0x42,0x30,0x40,0x30,0x1D,0x06,0x03, +0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x07,0x1F,0xD2,0xE7,0x9C,0xDA,0xC2,0x6E,0xA2, +0x40,0xB4,0xB0,0x7A,0x50,0x10,0x50,0x74,0xC4,0xC8,0xBD,0x30,0x0F,0x06,0x03,0x55, +0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03, +0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x0D,0x06,0x09, +0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00, +0x89,0x57,0xB2,0x16,0x7A,0xA8,0xC2,0xFD,0xD6,0xD9,0x9B,0x9B,0x34,0xC2,0x9C,0xB4, +0x32,0x14,0x4D,0xA7,0xA4,0xDF,0xEC,0xBE,0xA7,0xBE,0xF8,0x43,0xDB,0x91,0x37,0xCE, +0xB4,0x32,0x2E,0x50,0x55,0x1A,0x35,0x4E,0x76,0x43,0x71,0x20,0xEF,0x93,0x77,0x4E, +0x15,0x70,0x2E,0x87,0xC3,0xC1,0x1D,0x6D,0xDC,0xCB,0xB5,0x27,0xD4,0x2C,0x56,0xD1, +0x52,0x53,0x3A,0x44,0xD2,0x73,0xC8,0xC4,0x1B,0x05,0x65,0x5A,0x62,0x92,0x9C,0xEE, +0x41,0x8D,0x31,0xDB,0xE7,0x34,0xEA,0x59,0x21,0xD5,0x01,0x7A,0xD7,0x64,0xB8,0x64, +0x39,0xCD,0xC9,0xED,0xAF,0xED,0x4B,0x03,0x48,0xA7,0xA0,0x99,0x01,0x80,0xDC,0x65, +0xA3,0x36,0xAE,0x65,0x59,0x48,0x4F,0x82,0x4B,0xC8,0x65,0xF1,0x57,0x1D,0xE5,0x59, +0x2E,0x0A,0x3F,0x6C,0xD8,0xD1,0xF5,0xE5,0x09,0xB4,0x6C,0x54,0x00,0x0A,0xE0,0x15, +0x4D,0x87,0x75,0x6D,0xB7,0x58,0x96,0x5A,0xDD,0x6D,0xD2,0x00,0xA0,0xF4,0x9B,0x48, +0xBE,0xC3,0x37,0xA4,0xBA,0x36,0xE0,0x7C,0x87,0x85,0x97,0x1A,0x15,0xA2,0xDE,0x2E, +0xA2,0x5B,0xBD,0xAF,0x18,0xF9,0x90,0x50,0xCD,0x70,0x59,0xF8,0x27,0x67,0x47,0xCB, +0xC7,0xA0,0x07,0x3A,0x7D,0xD1,0x2C,0x5D,0x6C,0x19,0x3A,0x66,0xB5,0x7D,0xFD,0x91, +0x6F,0x82,0xB1,0xBE,0x08,0x93,0xDB,0x14,0x47,0xF1,0xA2,0x37,0xC7,0x45,0x9E,0x3C, +0xC7,0x77,0xAF,0x64,0xA8,0x93,0xDF,0xF6,0x69,0x83,0x82,0x60,0xF2,0x49,0x42,0x34, +0xED,0x5A,0x00,0x54,0x85,0x1C,0x16,0x36,0x92,0x0C,0x5C,0xFA,0xA6,0xAD,0xBF,0xDB, +}; + + +/* subject:/C=US/O=AffirmTrust/CN=AffirmTrust Premium */ +/* issuer :/C=US/O=AffirmTrust/CN=AffirmTrust Premium */ + + +const unsigned char AffirmTrust_Premium_certificate[1354]={ +0x30,0x82,0x05,0x46,0x30,0x82,0x03,0x2E,0xA0,0x03,0x02,0x01,0x02,0x02,0x08,0x6D, +0x8C,0x14,0x46,0xB1,0xA6,0x0A,0xEE,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7, +0x0D,0x01,0x01,0x0C,0x05,0x00,0x30,0x41,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04, +0x06,0x13,0x02,0x55,0x53,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x0C,0x0B, +0x41,0x66,0x66,0x69,0x72,0x6D,0x54,0x72,0x75,0x73,0x74,0x31,0x1C,0x30,0x1A,0x06, +0x03,0x55,0x04,0x03,0x0C,0x13,0x41,0x66,0x66,0x69,0x72,0x6D,0x54,0x72,0x75,0x73, +0x74,0x20,0x50,0x72,0x65,0x6D,0x69,0x75,0x6D,0x30,0x1E,0x17,0x0D,0x31,0x30,0x30, +0x31,0x32,0x39,0x31,0x34,0x31,0x30,0x33,0x36,0x5A,0x17,0x0D,0x34,0x30,0x31,0x32, +0x33,0x31,0x31,0x34,0x31,0x30,0x33,0x36,0x5A,0x30,0x41,0x31,0x0B,0x30,0x09,0x06, +0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04, +0x0A,0x0C,0x0B,0x41,0x66,0x66,0x69,0x72,0x6D,0x54,0x72,0x75,0x73,0x74,0x31,0x1C, +0x30,0x1A,0x06,0x03,0x55,0x04,0x03,0x0C,0x13,0x41,0x66,0x66,0x69,0x72,0x6D,0x54, +0x72,0x75,0x73,0x74,0x20,0x50,0x72,0x65,0x6D,0x69,0x75,0x6D,0x30,0x82,0x02,0x22, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03, +0x82,0x02,0x0F,0x00,0x30,0x82,0x02,0x0A,0x02,0x82,0x02,0x01,0x00,0xC4,0x12,0xDF, +0xA9,0x5F,0xFE,0x41,0xDD,0xDD,0xF5,0x9F,0x8A,0xE3,0xF6,0xAC,0xE1,0x3C,0x78,0x9A, +0xBC,0xD8,0xF0,0x7F,0x7A,0xA0,0x33,0x2A,0xDC,0x8D,0x20,0x5B,0xAE,0x2D,0x6F,0xE7, +0x93,0xD9,0x36,0x70,0x6A,0x68,0xCF,0x8E,0x51,0xA3,0x85,0x5B,0x67,0x04,0xA0,0x10, +0x24,0x6F,0x5D,0x28,0x82,0xC1,0x97,0x57,0xD8,0x48,0x29,0x13,0xB6,0xE1,0xBE,0x91, +0x4D,0xDF,0x85,0x0C,0x53,0x18,0x9A,0x1E,0x24,0xA2,0x4F,0x8F,0xF0,0xA2,0x85,0x0B, +0xCB,0xF4,0x29,0x7F,0xD2,0xA4,0x58,0xEE,0x26,0x4D,0xC9,0xAA,0xA8,0x7B,0x9A,0xD9, +0xFA,0x38,0xDE,0x44,0x57,0x15,0xE5,0xF8,0x8C,0xC8,0xD9,0x48,0xE2,0x0D,0x16,0x27, +0x1D,0x1E,0xC8,0x83,0x85,0x25,0xB7,0xBA,0xAA,0x55,0x41,0xCC,0x03,0x22,0x4B,0x2D, +0x91,0x8D,0x8B,0xE6,0x89,0xAF,0x66,0xC7,0xE9,0xFF,0x2B,0xE9,0x3C,0xAC,0xDA,0xD2, +0xB3,0xC3,0xE1,0x68,0x9C,0x89,0xF8,0x7A,0x00,0x56,0xDE,0xF4,0x55,0x95,0x6C,0xFB, +0xBA,0x64,0xDD,0x62,0x8B,0xDF,0x0B,0x77,0x32,0xEB,0x62,0xCC,0x26,0x9A,0x9B,0xBB, +0xAA,0x62,0x83,0x4C,0xB4,0x06,0x7A,0x30,0xC8,0x29,0xBF,0xED,0x06,0x4D,0x97,0xB9, +0x1C,0xC4,0x31,0x2B,0xD5,0x5F,0xBC,0x53,0x12,0x17,0x9C,0x99,0x57,0x29,0x66,0x77, +0x61,0x21,0x31,0x07,0x2E,0x25,0x49,0x9D,0x18,0xF2,0xEE,0xF3,0x2B,0x71,0x8C,0xB5, +0xBA,0x39,0x07,0x49,0x77,0xFC,0xEF,0x2E,0x92,0x90,0x05,0x8D,0x2D,0x2F,0x77,0x7B, +0xEF,0x43,0xBF,0x35,0xBB,0x9A,0xD8,0xF9,0x73,0xA7,0x2C,0xF2,0xD0,0x57,0xEE,0x28, +0x4E,0x26,0x5F,0x8F,0x90,0x68,0x09,0x2F,0xB8,0xF8,0xDC,0x06,0xE9,0x2E,0x9A,0x3E, +0x51,0xA7,0xD1,0x22,0xC4,0x0A,0xA7,0x38,0x48,0x6C,0xB3,0xF9,0xFF,0x7D,0xAB,0x86, +0x57,0xE3,0xBA,0xD6,0x85,0x78,0x77,0xBA,0x43,0xEA,0x48,0x7F,0xF6,0xD8,0xBE,0x23, +0x6D,0x1E,0xBF,0xD1,0x36,0x6C,0x58,0x5C,0xF1,0xEE,0xA4,0x19,0x54,0x1A,0xF5,0x03, +0xD2,0x76,0xE6,0xE1,0x8C,0xBD,0x3C,0xB3,0xD3,0x48,0x4B,0xE2,0xC8,0xF8,0x7F,0x92, +0xA8,0x76,0x46,0x9C,0x42,0x65,0x3E,0xA4,0x1E,0xC1,0x07,0x03,0x5A,0x46,0x2D,0xB8, +0x97,0xF3,0xB7,0xD5,0xB2,0x55,0x21,0xEF,0xBA,0xDC,0x4C,0x00,0x97,0xFB,0x14,0x95, +0x27,0x33,0xBF,0xE8,0x43,0x47,0x46,0xD2,0x08,0x99,0x16,0x60,0x3B,0x9A,0x7E,0xD2, +0xE6,0xED,0x38,0xEA,0xEC,0x01,0x1E,0x3C,0x48,0x56,0x49,0x09,0xC7,0x4C,0x37,0x00, +0x9E,0x88,0x0E,0xC0,0x73,0xE1,0x6F,0x66,0xE9,0x72,0x47,0x30,0x3E,0x10,0xE5,0x0B, +0x03,0xC9,0x9A,0x42,0x00,0x6C,0xC5,0x94,0x7E,0x61,0xC4,0x8A,0xDF,0x7F,0x82,0x1A, +0x0B,0x59,0xC4,0x59,0x32,0x77,0xB3,0xBC,0x60,0x69,0x56,0x39,0xFD,0xB4,0x06,0x7B, +0x2C,0xD6,0x64,0x36,0xD9,0xBD,0x48,0xED,0x84,0x1F,0x7E,0xA5,0x22,0x8F,0x2A,0xB8, +0x42,0xF4,0x82,0xB7,0xD4,0x53,0x90,0x78,0x4E,0x2D,0x1A,0xFD,0x81,0x6F,0x44,0xD7, +0x3B,0x01,0x74,0x96,0x42,0xE0,0x00,0xE2,0x2E,0x6B,0xEA,0xC5,0xEE,0x72,0xAC,0xBB, +0xBF,0xFE,0xEA,0xAA,0xA8,0xF8,0xDC,0xF6,0xB2,0x79,0x8A,0xB6,0x67,0x02,0x03,0x01, +0x00,0x01,0xA3,0x42,0x30,0x40,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04, +0x14,0x9D,0xC0,0x67,0xA6,0x0C,0x22,0xD9,0x26,0xF5,0x45,0xAB,0xA6,0x65,0x52,0x11, +0x27,0xD8,0x45,0xAC,0x63,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04, +0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF, +0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D, +0x01,0x01,0x0C,0x05,0x00,0x03,0x82,0x02,0x01,0x00,0xB3,0x57,0x4D,0x10,0x62,0x4E, +0x3A,0xE4,0xAC,0xEA,0xB8,0x1C,0xAF,0x32,0x23,0xC8,0xB3,0x49,0x5A,0x51,0x9C,0x76, +0x28,0x8D,0x79,0xAA,0x57,0x46,0x17,0xD5,0xF5,0x52,0xF6,0xB7,0x44,0xE8,0x08,0x44, +0xBF,0x18,0x84,0xD2,0x0B,0x80,0xCD,0xC5,0x12,0xFD,0x00,0x55,0x05,0x61,0x87,0x41, +0xDC,0xB5,0x24,0x9E,0x3C,0xC4,0xD8,0xC8,0xFB,0x70,0x9E,0x2F,0x78,0x96,0x83,0x20, +0x36,0xDE,0x7C,0x0F,0x69,0x13,0x88,0xA5,0x75,0x36,0x98,0x08,0xA6,0xC6,0xDF,0xAC, +0xCE,0xE3,0x58,0xD6,0xB7,0x3E,0xDE,0xBA,0xF3,0xEB,0x34,0x40,0xD8,0xA2,0x81,0xF5, +0x78,0x3F,0x2F,0xD5,0xA5,0xFC,0xD9,0xA2,0xD4,0x5E,0x04,0x0E,0x17,0xAD,0xFE,0x41, +0xF0,0xE5,0xB2,0x72,0xFA,0x44,0x82,0x33,0x42,0xE8,0x2D,0x58,0xF7,0x56,0x8C,0x62, +0x3F,0xBA,0x42,0xB0,0x9C,0x0C,0x5C,0x7E,0x2E,0x65,0x26,0x5C,0x53,0x4F,0x00,0xB2, +0x78,0x7E,0xA1,0x0D,0x99,0x2D,0x8D,0xB8,0x1D,0x8E,0xA2,0xC4,0xB0,0xFD,0x60,0xD0, +0x30,0xA4,0x8E,0xC8,0x04,0x62,0xA9,0xC4,0xED,0x35,0xDE,0x7A,0x97,0xED,0x0E,0x38, +0x5E,0x92,0x2F,0x93,0x70,0xA5,0xA9,0x9C,0x6F,0xA7,0x7D,0x13,0x1D,0x7E,0xC6,0x08, +0x48,0xB1,0x5E,0x67,0xEB,0x51,0x08,0x25,0xE9,0xE6,0x25,0x6B,0x52,0x29,0x91,0x9C, +0xD2,0x39,0x73,0x08,0x57,0xDE,0x99,0x06,0xB4,0x5B,0x9D,0x10,0x06,0xE1,0xC2,0x00, +0xA8,0xB8,0x1C,0x4A,0x02,0x0A,0x14,0xD0,0xC1,0x41,0xCA,0xFB,0x8C,0x35,0x21,0x7D, +0x82,0x38,0xF2,0xA9,0x54,0x91,0x19,0x35,0x93,0x94,0x6D,0x6A,0x3A,0xC5,0xB2,0xD0, +0xBB,0x89,0x86,0x93,0xE8,0x9B,0xC9,0x0F,0x3A,0xA7,0x7A,0xB8,0xA1,0xF0,0x78,0x46, +0xFA,0xFC,0x37,0x2F,0xE5,0x8A,0x84,0xF3,0xDF,0xFE,0x04,0xD9,0xA1,0x68,0xA0,0x2F, +0x24,0xE2,0x09,0x95,0x06,0xD5,0x95,0xCA,0xE1,0x24,0x96,0xEB,0x7C,0xF6,0x93,0x05, +0xBB,0xED,0x73,0xE9,0x2D,0xD1,0x75,0x39,0xD7,0xE7,0x24,0xDB,0xD8,0x4E,0x5F,0x43, +0x8F,0x9E,0xD0,0x14,0x39,0xBF,0x55,0x70,0x48,0x99,0x57,0x31,0xB4,0x9C,0xEE,0x4A, +0x98,0x03,0x96,0x30,0x1F,0x60,0x06,0xEE,0x1B,0x23,0xFE,0x81,0x60,0x23,0x1A,0x47, +0x62,0x85,0xA5,0xCC,0x19,0x34,0x80,0x6F,0xB3,0xAC,0x1A,0xE3,0x9F,0xF0,0x7B,0x48, +0xAD,0xD5,0x01,0xD9,0x67,0xB6,0xA9,0x72,0x93,0xEA,0x2D,0x66,0xB5,0xB2,0xB8,0xE4, +0x3D,0x3C,0xB2,0xEF,0x4C,0x8C,0xEA,0xEB,0x07,0xBF,0xAB,0x35,0x9A,0x55,0x86,0xBC, +0x18,0xA6,0xB5,0xA8,0x5E,0xB4,0x83,0x6C,0x6B,0x69,0x40,0xD3,0x9F,0xDC,0xF1,0xC3, +0x69,0x6B,0xB9,0xE1,0x6D,0x09,0xF4,0xF1,0xAA,0x50,0x76,0x0A,0x7A,0x7D,0x7A,0x17, +0xA1,0x55,0x96,0x42,0x99,0x31,0x09,0xDD,0x60,0x11,0x8D,0x05,0x30,0x7E,0xE6,0x8E, +0x46,0xD1,0x9D,0x14,0xDA,0xC7,0x17,0xE4,0x05,0x96,0x8C,0xC4,0x24,0xB5,0x1B,0xCF, +0x14,0x07,0xB2,0x40,0xF8,0xA3,0x9E,0x41,0x86,0xBC,0x04,0xD0,0x6B,0x96,0xC8,0x2A, +0x80,0x34,0xFD,0xBF,0xEF,0x06,0xA3,0xDD,0x58,0xC5,0x85,0x3D,0x3E,0x8F,0xFE,0x9E, +0x29,0xE0,0xB6,0xB8,0x09,0x68,0x19,0x1C,0x18,0x43, +}; + + +/* subject:/C=US/O=AffirmTrust/CN=AffirmTrust Premium ECC */ +/* issuer :/C=US/O=AffirmTrust/CN=AffirmTrust Premium ECC */ + + +const unsigned char AffirmTrust_Premium_ECC_certificate[514]={ +0x30,0x82,0x01,0xFE,0x30,0x82,0x01,0x85,0xA0,0x03,0x02,0x01,0x02,0x02,0x08,0x74, +0x97,0x25,0x8A,0xC7,0x3F,0x7A,0x54,0x30,0x0A,0x06,0x08,0x2A,0x86,0x48,0xCE,0x3D, +0x04,0x03,0x03,0x30,0x45,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02, +0x55,0x53,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x0C,0x0B,0x41,0x66,0x66, +0x69,0x72,0x6D,0x54,0x72,0x75,0x73,0x74,0x31,0x20,0x30,0x1E,0x06,0x03,0x55,0x04, +0x03,0x0C,0x17,0x41,0x66,0x66,0x69,0x72,0x6D,0x54,0x72,0x75,0x73,0x74,0x20,0x50, +0x72,0x65,0x6D,0x69,0x75,0x6D,0x20,0x45,0x43,0x43,0x30,0x1E,0x17,0x0D,0x31,0x30, +0x30,0x31,0x32,0x39,0x31,0x34,0x32,0x30,0x32,0x34,0x5A,0x17,0x0D,0x34,0x30,0x31, +0x32,0x33,0x31,0x31,0x34,0x32,0x30,0x32,0x34,0x5A,0x30,0x45,0x31,0x0B,0x30,0x09, +0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x14,0x30,0x12,0x06,0x03,0x55, +0x04,0x0A,0x0C,0x0B,0x41,0x66,0x66,0x69,0x72,0x6D,0x54,0x72,0x75,0x73,0x74,0x31, +0x20,0x30,0x1E,0x06,0x03,0x55,0x04,0x03,0x0C,0x17,0x41,0x66,0x66,0x69,0x72,0x6D, +0x54,0x72,0x75,0x73,0x74,0x20,0x50,0x72,0x65,0x6D,0x69,0x75,0x6D,0x20,0x45,0x43, +0x43,0x30,0x76,0x30,0x10,0x06,0x07,0x2A,0x86,0x48,0xCE,0x3D,0x02,0x01,0x06,0x05, +0x2B,0x81,0x04,0x00,0x22,0x03,0x62,0x00,0x04,0x0D,0x30,0x5E,0x1B,0x15,0x9D,0x03, +0xD0,0xA1,0x79,0x35,0xB7,0x3A,0x3C,0x92,0x7A,0xCA,0x15,0x1C,0xCD,0x62,0xF3,0x9C, +0x26,0x5C,0x07,0x3D,0xE5,0x54,0xFA,0xA3,0xD6,0xCC,0x12,0xEA,0xF4,0x14,0x5F,0xE8, +0x8E,0x19,0xAB,0x2F,0x2E,0x48,0xE6,0xAC,0x18,0x43,0x78,0xAC,0xD0,0x37,0xC3,0xBD, +0xB2,0xCD,0x2C,0xE6,0x47,0xE2,0x1A,0xE6,0x63,0xB8,0x3D,0x2E,0x2F,0x78,0xC4,0x4F, +0xDB,0xF4,0x0F,0xA4,0x68,0x4C,0x55,0x72,0x6B,0x95,0x1D,0x4E,0x18,0x42,0x95,0x78, +0xCC,0x37,0x3C,0x91,0xE2,0x9B,0x65,0x2B,0x29,0xA3,0x42,0x30,0x40,0x30,0x1D,0x06, +0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x9A,0xAF,0x29,0x7A,0xC0,0x11,0x35,0x35, +0x26,0x51,0x30,0x00,0xC3,0x6A,0xFE,0x40,0xD5,0xAE,0xD6,0x3C,0x30,0x0F,0x06,0x03, +0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06, +0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x0A,0x06, +0x08,0x2A,0x86,0x48,0xCE,0x3D,0x04,0x03,0x03,0x03,0x67,0x00,0x30,0x64,0x02,0x30, +0x17,0x09,0xF3,0x87,0x88,0x50,0x5A,0xAF,0xC8,0xC0,0x42,0xBF,0x47,0x5F,0xF5,0x6C, +0x6A,0x86,0xE0,0xC4,0x27,0x74,0xE4,0x38,0x53,0xD7,0x05,0x7F,0x1B,0x34,0xE3,0xC6, +0x2F,0xB3,0xCA,0x09,0x3C,0x37,0x9D,0xD7,0xE7,0xB8,0x46,0xF1,0xFD,0xA1,0xE2,0x71, +0x02,0x30,0x42,0x59,0x87,0x43,0xD4,0x51,0xDF,0xBA,0xD3,0x09,0x32,0x5A,0xCE,0x88, +0x7E,0x57,0x3D,0x9C,0x5F,0x42,0x6B,0xF5,0x07,0x2D,0xB5,0xF0,0x82,0x93,0xF9,0x59, +0x6F,0xAE,0x64,0xFA,0x58,0xE5,0x8B,0x1E,0xE3,0x63,0xBE,0xB5,0x81,0xCD,0x6F,0x02, +0x8C,0x79, +}; + + +/* subject:/C=US/O=America Online Inc./CN=America Online Root Certification Authority 1 */ +/* issuer :/C=US/O=America Online Inc./CN=America Online Root Certification Authority 1 */ + + +const unsigned char America_Online_Root_Certification_Authority_1_certificate[936]={ +0x30,0x82,0x03,0xA4,0x30,0x82,0x02,0x8C,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x63,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x1C, +0x30,0x1A,0x06,0x03,0x55,0x04,0x0A,0x13,0x13,0x41,0x6D,0x65,0x72,0x69,0x63,0x61, +0x20,0x4F,0x6E,0x6C,0x69,0x6E,0x65,0x20,0x49,0x6E,0x63,0x2E,0x31,0x36,0x30,0x34, +0x06,0x03,0x55,0x04,0x03,0x13,0x2D,0x41,0x6D,0x65,0x72,0x69,0x63,0x61,0x20,0x4F, +0x6E,0x6C,0x69,0x6E,0x65,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69, +0x74,0x79,0x20,0x31,0x30,0x1E,0x17,0x0D,0x30,0x32,0x30,0x35,0x32,0x38,0x30,0x36, +0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x37,0x31,0x31,0x31,0x39,0x32,0x30,0x34, +0x33,0x30,0x30,0x5A,0x30,0x63,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13, +0x02,0x55,0x53,0x31,0x1C,0x30,0x1A,0x06,0x03,0x55,0x04,0x0A,0x13,0x13,0x41,0x6D, +0x65,0x72,0x69,0x63,0x61,0x20,0x4F,0x6E,0x6C,0x69,0x6E,0x65,0x20,0x49,0x6E,0x63, +0x2E,0x31,0x36,0x30,0x34,0x06,0x03,0x55,0x04,0x03,0x13,0x2D,0x41,0x6D,0x65,0x72, +0x69,0x63,0x61,0x20,0x4F,0x6E,0x6C,0x69,0x6E,0x65,0x20,0x52,0x6F,0x6F,0x74,0x20, +0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75, +0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x20,0x31,0x30,0x82,0x01,0x22,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F, +0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xA8,0x2F,0xE8,0xA4,0x69,0x06, +0x03,0x47,0xC3,0xE9,0x2A,0x98,0xFF,0x19,0xA2,0x70,0x9A,0xC6,0x50,0xB2,0x7E,0xA5, +0xDF,0x68,0x4D,0x1B,0x7C,0x0F,0xB6,0x97,0x68,0x7D,0x2D,0xA6,0x8B,0x97,0xE9,0x64, +0x86,0xC9,0xA3,0xEF,0xA0,0x86,0xBF,0x60,0x65,0x9C,0x4B,0x54,0x88,0xC2,0x48,0xC5, +0x4A,0x39,0xBF,0x14,0xE3,0x59,0x55,0xE5,0x19,0xB4,0x74,0xC8,0xB4,0x05,0x39,0x5C, +0x16,0xA5,0xE2,0x95,0x05,0xE0,0x12,0xAE,0x59,0x8B,0xA2,0x33,0x68,0x58,0x1C,0xA6, +0xD4,0x15,0xB7,0xD8,0x9F,0xD7,0xDC,0x71,0xAB,0x7E,0x9A,0xBF,0x9B,0x8E,0x33,0x0F, +0x22,0xFD,0x1F,0x2E,0xE7,0x07,0x36,0xEF,0x62,0x39,0xC5,0xDD,0xCB,0xBA,0x25,0x14, +0x23,0xDE,0x0C,0xC6,0x3D,0x3C,0xCE,0x82,0x08,0xE6,0x66,0x3E,0xDA,0x51,0x3B,0x16, +0x3A,0xA3,0x05,0x7F,0xA0,0xDC,0x87,0xD5,0x9C,0xFC,0x72,0xA9,0xA0,0x7D,0x78,0xE4, +0xB7,0x31,0x55,0x1E,0x65,0xBB,0xD4,0x61,0xB0,0x21,0x60,0xED,0x10,0x32,0x72,0xC5, +0x92,0x25,0x1E,0xF8,0x90,0x4A,0x18,0x78,0x47,0xDF,0x7E,0x30,0x37,0x3E,0x50,0x1B, +0xDB,0x1C,0xD3,0x6B,0x9A,0x86,0x53,0x07,0xB0,0xEF,0xAC,0x06,0x78,0xF8,0x84,0x99, +0xFE,0x21,0x8D,0x4C,0x80,0xB6,0x0C,0x82,0xF6,0x66,0x70,0x79,0x1A,0xD3,0x4F,0xA3, +0xCF,0xF1,0xCF,0x46,0xB0,0x4B,0x0F,0x3E,0xDD,0x88,0x62,0xB8,0x8C,0xA9,0x09,0x28, +0x3B,0x7A,0xC7,0x97,0xE1,0x1E,0xE5,0xF4,0x9F,0xC0,0xC0,0xAE,0x24,0xA0,0xC8,0xA1, +0xD9,0x0F,0xD6,0x7B,0x26,0x82,0x69,0x32,0x3D,0xA7,0x02,0x03,0x01,0x00,0x01,0xA3, +0x63,0x30,0x61,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30, +0x03,0x01,0x01,0xFF,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x00, +0xAD,0xD9,0xA3,0xF6,0x79,0xF6,0x6E,0x74,0xA9,0x7F,0x33,0x3D,0x81,0x17,0xD7,0x4C, +0xCF,0x33,0xDE,0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14, +0x00,0xAD,0xD9,0xA3,0xF6,0x79,0xF6,0x6E,0x74,0xA9,0x7F,0x33,0x3D,0x81,0x17,0xD7, +0x4C,0xCF,0x33,0xDE,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04, +0x03,0x02,0x01,0x86,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01, +0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x7C,0x8A,0xD1,0x1F,0x18,0x37,0x82,0xE0, +0xB8,0xB0,0xA3,0xED,0x56,0x95,0xC8,0x62,0x61,0x9C,0x05,0xA2,0xCD,0xC2,0x62,0x26, +0x61,0xCD,0x10,0x16,0xD7,0xCC,0xB4,0x65,0x34,0xD0,0x11,0x8A,0xAD,0xA8,0xA9,0x05, +0x66,0xEF,0x74,0xF3,0x6D,0x5F,0x9D,0x99,0xAF,0xF6,0x8B,0xFB,0xEB,0x52,0xB2,0x05, +0x98,0xA2,0x6F,0x2A,0xC5,0x54,0xBD,0x25,0xBD,0x5F,0xAE,0xC8,0x86,0xEA,0x46,0x2C, +0xC1,0xB3,0xBD,0xC1,0xE9,0x49,0x70,0x18,0x16,0x97,0x08,0x13,0x8C,0x20,0xE0,0x1B, +0x2E,0x3A,0x47,0xCB,0x1E,0xE4,0x00,0x30,0x95,0x5B,0xF4,0x45,0xA3,0xC0,0x1A,0xB0, +0x01,0x4E,0xAB,0xBD,0xC0,0x23,0x6E,0x63,0x3F,0x80,0x4A,0xC5,0x07,0xED,0xDC,0xE2, +0x6F,0xC7,0xC1,0x62,0xF1,0xE3,0x72,0xD6,0x04,0xC8,0x74,0x67,0x0B,0xFA,0x88,0xAB, +0xA1,0x01,0xC8,0x6F,0xF0,0x14,0xAF,0xD2,0x99,0xCD,0x51,0x93,0x7E,0xED,0x2E,0x38, +0xC7,0xBD,0xCE,0x46,0x50,0x3D,0x72,0xE3,0x79,0x25,0x9D,0x9B,0x88,0x2B,0x10,0x20, +0xDD,0xA5,0xB8,0x32,0x9F,0x8D,0xE0,0x29,0xDF,0x21,0x74,0x86,0x82,0xDB,0x2F,0x82, +0x30,0xC6,0xC7,0x35,0x86,0xB3,0xF9,0x96,0x5F,0x46,0xDB,0x0C,0x45,0xFD,0xF3,0x50, +0xC3,0x6F,0xC6,0xC3,0x48,0xAD,0x46,0xA6,0xE1,0x27,0x47,0x0A,0x1D,0x0E,0x9B,0xB6, +0xC2,0x77,0x7F,0x63,0xF2,0xE0,0x7D,0x1A,0xBE,0xFC,0xE0,0xDF,0xD7,0xC7,0xA7,0x6C, +0xB0,0xF9,0xAE,0xBA,0x3C,0xFD,0x74,0xB4,0x11,0xE8,0x58,0x0D,0x80,0xBC,0xD3,0xA8, +0x80,0x3A,0x99,0xED,0x75,0xCC,0x46,0x7B, +}; + + +/* subject:/C=US/O=America Online Inc./CN=America Online Root Certification Authority 2 */ +/* issuer :/C=US/O=America Online Inc./CN=America Online Root Certification Authority 2 */ + + +const unsigned char America_Online_Root_Certification_Authority_2_certificate[1448]={ +0x30,0x82,0x05,0xA4,0x30,0x82,0x03,0x8C,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x63,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x1C, +0x30,0x1A,0x06,0x03,0x55,0x04,0x0A,0x13,0x13,0x41,0x6D,0x65,0x72,0x69,0x63,0x61, +0x20,0x4F,0x6E,0x6C,0x69,0x6E,0x65,0x20,0x49,0x6E,0x63,0x2E,0x31,0x36,0x30,0x34, +0x06,0x03,0x55,0x04,0x03,0x13,0x2D,0x41,0x6D,0x65,0x72,0x69,0x63,0x61,0x20,0x4F, +0x6E,0x6C,0x69,0x6E,0x65,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69, +0x74,0x79,0x20,0x32,0x30,0x1E,0x17,0x0D,0x30,0x32,0x30,0x35,0x32,0x38,0x30,0x36, +0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x37,0x30,0x39,0x32,0x39,0x31,0x34,0x30, +0x38,0x30,0x30,0x5A,0x30,0x63,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13, +0x02,0x55,0x53,0x31,0x1C,0x30,0x1A,0x06,0x03,0x55,0x04,0x0A,0x13,0x13,0x41,0x6D, +0x65,0x72,0x69,0x63,0x61,0x20,0x4F,0x6E,0x6C,0x69,0x6E,0x65,0x20,0x49,0x6E,0x63, +0x2E,0x31,0x36,0x30,0x34,0x06,0x03,0x55,0x04,0x03,0x13,0x2D,0x41,0x6D,0x65,0x72, +0x69,0x63,0x61,0x20,0x4F,0x6E,0x6C,0x69,0x6E,0x65,0x20,0x52,0x6F,0x6F,0x74,0x20, +0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75, +0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x20,0x32,0x30,0x82,0x02,0x22,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x02,0x0F, +0x00,0x30,0x82,0x02,0x0A,0x02,0x82,0x02,0x01,0x00,0xCC,0x41,0x45,0x1D,0xE9,0x3D, +0x4D,0x10,0xF6,0x8C,0xB1,0x41,0xC9,0xE0,0x5E,0xCB,0x0D,0xB7,0xBF,0x47,0x73,0xD3, +0xF0,0x55,0x4D,0xDD,0xC6,0x0C,0xFA,0xB1,0x66,0x05,0x6A,0xCD,0x78,0xB4,0xDC,0x02, +0xDB,0x4E,0x81,0xF3,0xD7,0xA7,0x7C,0x71,0xBC,0x75,0x63,0xA0,0x5D,0xE3,0x07,0x0C, +0x48,0xEC,0x25,0xC4,0x03,0x20,0xF4,0xFF,0x0E,0x3B,0x12,0xFF,0x9B,0x8D,0xE1,0xC6, +0xD5,0x1B,0xB4,0x6D,0x22,0xE3,0xB1,0xDB,0x7F,0x21,0x64,0xAF,0x86,0xBC,0x57,0x22, +0x2A,0xD6,0x47,0x81,0x57,0x44,0x82,0x56,0x53,0xBD,0x86,0x14,0x01,0x0B,0xFC,0x7F, +0x74,0xA4,0x5A,0xAE,0xF1,0xBA,0x11,0xB5,0x9B,0x58,0x5A,0x80,0xB4,0x37,0x78,0x09, +0x33,0x7C,0x32,0x47,0x03,0x5C,0xC4,0xA5,0x83,0x48,0xF4,0x57,0x56,0x6E,0x81,0x36, +0x27,0x18,0x4F,0xEC,0x9B,0x28,0xC2,0xD4,0xB4,0xD7,0x7C,0x0C,0x3E,0x0C,0x2B,0xDF, +0xCA,0x04,0xD7,0xC6,0x8E,0xEA,0x58,0x4E,0xA8,0xA4,0xA5,0x18,0x1C,0x6C,0x45,0x98, +0xA3,0x41,0xD1,0x2D,0xD2,0xC7,0x6D,0x8D,0x19,0xF1,0xAD,0x79,0xB7,0x81,0x3F,0xBD, +0x06,0x82,0x27,0x2D,0x10,0x58,0x05,0xB5,0x78,0x05,0xB9,0x2F,0xDB,0x0C,0x6B,0x90, +0x90,0x7E,0x14,0x59,0x38,0xBB,0x94,0x24,0x13,0xE5,0xD1,0x9D,0x14,0xDF,0xD3,0x82, +0x4D,0x46,0xF0,0x80,0x39,0x52,0x32,0x0F,0xE3,0x84,0xB2,0x7A,0x43,0xF2,0x5E,0xDE, +0x5F,0x3F,0x1D,0xDD,0xE3,0xB2,0x1B,0xA0,0xA1,0x2A,0x23,0x03,0x6E,0x2E,0x01,0x15, +0x87,0x5C,0xA6,0x75,0x75,0xC7,0x97,0x61,0xBE,0xDE,0x86,0xDC,0xD4,0x48,0xDB,0xBD, +0x2A,0xBF,0x4A,0x55,0xDA,0xE8,0x7D,0x50,0xFB,0xB4,0x80,0x17,0xB8,0x94,0xBF,0x01, +0x3D,0xEA,0xDA,0xBA,0x7C,0xE0,0x58,0x67,0x17,0xB9,0x58,0xE0,0x88,0x86,0x46,0x67, +0x6C,0x9D,0x10,0x47,0x58,0x32,0xD0,0x35,0x7C,0x79,0x2A,0x90,0xA2,0x5A,0x10,0x11, +0x23,0x35,0xAD,0x2F,0xCC,0xE4,0x4A,0x5B,0xA7,0xC8,0x27,0xF2,0x83,0xDE,0x5E,0xBB, +0x5E,0x77,0xE7,0xE8,0xA5,0x6E,0x63,0xC2,0x0D,0x5D,0x61,0xD0,0x8C,0xD2,0x6C,0x5A, +0x21,0x0E,0xCA,0x28,0xA3,0xCE,0x2A,0xE9,0x95,0xC7,0x48,0xCF,0x96,0x6F,0x1D,0x92, +0x25,0xC8,0xC6,0xC6,0xC1,0xC1,0x0C,0x05,0xAC,0x26,0xC4,0xD2,0x75,0xD2,0xE1,0x2A, +0x67,0xC0,0x3D,0x5B,0xA5,0x9A,0xEB,0xCF,0x7B,0x1A,0xA8,0x9D,0x14,0x45,0xE5,0x0F, +0xA0,0x9A,0x65,0xDE,0x2F,0x28,0xBD,0xCE,0x6F,0x94,0x66,0x83,0x48,0x29,0xD8,0xEA, +0x65,0x8C,0xAF,0x93,0xD9,0x64,0x9F,0x55,0x57,0x26,0xBF,0x6F,0xCB,0x37,0x31,0x99, +0xA3,0x60,0xBB,0x1C,0xAD,0x89,0x34,0x32,0x62,0xB8,0x43,0x21,0x06,0x72,0x0C,0xA1, +0x5C,0x6D,0x46,0xC5,0xFA,0x29,0xCF,0x30,0xDE,0x89,0xDC,0x71,0x5B,0xDD,0xB6,0x37, +0x3E,0xDF,0x50,0xF5,0xB8,0x07,0x25,0x26,0xE5,0xBC,0xB5,0xFE,0x3C,0x02,0xB3,0xB7, +0xF8,0xBE,0x43,0xC1,0x87,0x11,0x94,0x9E,0x23,0x6C,0x17,0x8A,0xB8,0x8A,0x27,0x0C, +0x54,0x47,0xF0,0xA9,0xB3,0xC0,0x80,0x8C,0xA0,0x27,0xEB,0x1D,0x19,0xE3,0x07,0x8E, +0x77,0x70,0xCA,0x2B,0xF4,0x7D,0x76,0xE0,0x78,0x67,0x02,0x03,0x01,0x00,0x01,0xA3, +0x63,0x30,0x61,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30, +0x03,0x01,0x01,0xFF,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x4D, +0x45,0xC1,0x68,0x38,0xBB,0x73,0xA9,0x69,0xA1,0x20,0xE7,0xED,0xF5,0x22,0xA1,0x23, +0x14,0xD7,0x9E,0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14, +0x4D,0x45,0xC1,0x68,0x38,0xBB,0x73,0xA9,0x69,0xA1,0x20,0xE7,0xED,0xF5,0x22,0xA1, +0x23,0x14,0xD7,0x9E,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04, +0x03,0x02,0x01,0x86,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01, +0x05,0x05,0x00,0x03,0x82,0x02,0x01,0x00,0x67,0x6B,0x06,0xB9,0x5F,0x45,0x3B,0x2A, +0x4B,0x33,0xB3,0xE6,0x1B,0x6B,0x59,0x4E,0x22,0xCC,0xB9,0xB7,0xA4,0x25,0xC9,0xA7, +0xC4,0xF0,0x54,0x96,0x0B,0x64,0xF3,0xB1,0x58,0x4F,0x5E,0x51,0xFC,0xB2,0x97,0x7B, +0x27,0x65,0xC2,0xE5,0xCA,0xE7,0x0D,0x0C,0x25,0x7B,0x62,0xE3,0xFA,0x9F,0xB4,0x87, +0xB7,0x45,0x46,0xAF,0x83,0xA5,0x97,0x48,0x8C,0xA5,0xBD,0xF1,0x16,0x2B,0x9B,0x76, +0x2C,0x7A,0x35,0x60,0x6C,0x11,0x80,0x97,0xCC,0xA9,0x92,0x52,0xE6,0x2B,0xE6,0x69, +0xED,0xA9,0xF8,0x36,0x2D,0x2C,0x77,0xBF,0x61,0x48,0xD1,0x63,0x0B,0xB9,0x5B,0x52, +0xED,0x18,0xB0,0x43,0x42,0x22,0xA6,0xB1,0x77,0xAE,0xDE,0x69,0xC5,0xCD,0xC7,0x1C, +0xA1,0xB1,0xA5,0x1C,0x10,0xFB,0x18,0xBE,0x1A,0x70,0xDD,0xC1,0x92,0x4B,0xBE,0x29, +0x5A,0x9D,0x3F,0x35,0xBE,0xE5,0x7D,0x51,0xF8,0x55,0xE0,0x25,0x75,0x23,0x87,0x1E, +0x5C,0xDC,0xBA,0x9D,0xB0,0xAC,0xB3,0x69,0xDB,0x17,0x83,0xC9,0xF7,0xDE,0x0C,0xBC, +0x08,0xDC,0x91,0x9E,0xA8,0xD0,0xD7,0x15,0x37,0x73,0xA5,0x35,0xB8,0xFC,0x7E,0xC5, +0x44,0x40,0x06,0xC3,0xEB,0xF8,0x22,0x80,0x5C,0x47,0xCE,0x02,0xE3,0x11,0x9F,0x44, +0xFF,0xFD,0x9A,0x32,0xCC,0x7D,0x64,0x51,0x0E,0xEB,0x57,0x26,0x76,0x3A,0xE3,0x1E, +0x22,0x3C,0xC2,0xA6,0x36,0xDD,0x19,0xEF,0xA7,0xFC,0x12,0xF3,0x26,0xC0,0x59,0x31, +0x85,0x4C,0x9C,0xD8,0xCF,0xDF,0xA4,0xCC,0xCC,0x29,0x93,0xFF,0x94,0x6D,0x76,0x5C, +0x13,0x08,0x97,0xF2,0xED,0xA5,0x0B,0x4D,0xDD,0xE8,0xC9,0x68,0x0E,0x66,0xD3,0x00, +0x0E,0x33,0x12,0x5B,0xBC,0x95,0xE5,0x32,0x90,0xA8,0xB3,0xC6,0x6C,0x83,0xAD,0x77, +0xEE,0x8B,0x7E,0x7E,0xB1,0xA9,0xAB,0xD3,0xE1,0xF1,0xB6,0xC0,0xB1,0xEA,0x88,0xC0, +0xE7,0xD3,0x90,0xE9,0x28,0x92,0x94,0x7B,0x68,0x7B,0x97,0x2A,0x0A,0x67,0x2D,0x85, +0x02,0x38,0x10,0xE4,0x03,0x61,0xD4,0xDA,0x25,0x36,0xC7,0x08,0x58,0x2D,0xA1,0xA7, +0x51,0xAF,0x30,0x0A,0x49,0xF5,0xA6,0x69,0x87,0x07,0x2D,0x44,0x46,0x76,0x8E,0x2A, +0xE5,0x9A,0x3B,0xD7,0x18,0xA2,0xFC,0x9C,0x38,0x10,0xCC,0xC6,0x3B,0xD2,0xB5,0x17, +0x3A,0x6F,0xFD,0xAE,0x25,0xBD,0xF5,0x72,0x59,0x64,0xB1,0x74,0x2A,0x38,0x5F,0x18, +0x4C,0xDF,0xCF,0x71,0x04,0x5A,0x36,0xD4,0xBF,0x2F,0x99,0x9C,0xE8,0xD9,0xBA,0xB1, +0x95,0xE6,0x02,0x4B,0x21,0xA1,0x5B,0xD5,0xC1,0x4F,0x8F,0xAE,0x69,0x6D,0x53,0xDB, +0x01,0x93,0xB5,0x5C,0x1E,0x18,0xDD,0x64,0x5A,0xCA,0x18,0x28,0x3E,0x63,0x04,0x11, +0xFD,0x1C,0x8D,0x00,0x0F,0xB8,0x37,0xDF,0x67,0x8A,0x9D,0x66,0xA9,0x02,0x6A,0x91, +0xFF,0x13,0xCA,0x2F,0x5D,0x83,0xBC,0x87,0x93,0x6C,0xDC,0x24,0x51,0x16,0x04,0x25, +0x66,0xFA,0xB3,0xD9,0xC2,0xBA,0x29,0xBE,0x9A,0x48,0x38,0x82,0x99,0xF4,0xBF,0x3B, +0x4A,0x31,0x19,0xF9,0xBF,0x8E,0x21,0x33,0x14,0xCA,0x4F,0x54,0x5F,0xFB,0xCE,0xFB, +0x8F,0x71,0x7F,0xFD,0x5E,0x19,0xA0,0x0F,0x4B,0x91,0xB8,0xC4,0x54,0xBC,0x06,0xB0, +0x45,0x8F,0x26,0x91,0xA2,0x8E,0xFE,0xA9, +}; + + +/* subject:/C=IE/O=Baltimore/OU=CyberTrust/CN=Baltimore CyberTrust Root */ +/* issuer :/C=IE/O=Baltimore/OU=CyberTrust/CN=Baltimore CyberTrust Root */ + + +const unsigned char Baltimore_CyberTrust_Root_certificate[891]={ +0x30,0x82,0x03,0x77,0x30,0x82,0x02,0x5F,0xA0,0x03,0x02,0x01,0x02,0x02,0x04,0x02, +0x00,0x00,0xB9,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05, +0x05,0x00,0x30,0x5A,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x49, +0x45,0x31,0x12,0x30,0x10,0x06,0x03,0x55,0x04,0x0A,0x13,0x09,0x42,0x61,0x6C,0x74, +0x69,0x6D,0x6F,0x72,0x65,0x31,0x13,0x30,0x11,0x06,0x03,0x55,0x04,0x0B,0x13,0x0A, +0x43,0x79,0x62,0x65,0x72,0x54,0x72,0x75,0x73,0x74,0x31,0x22,0x30,0x20,0x06,0x03, +0x55,0x04,0x03,0x13,0x19,0x42,0x61,0x6C,0x74,0x69,0x6D,0x6F,0x72,0x65,0x20,0x43, +0x79,0x62,0x65,0x72,0x54,0x72,0x75,0x73,0x74,0x20,0x52,0x6F,0x6F,0x74,0x30,0x1E, +0x17,0x0D,0x30,0x30,0x30,0x35,0x31,0x32,0x31,0x38,0x34,0x36,0x30,0x30,0x5A,0x17, +0x0D,0x32,0x35,0x30,0x35,0x31,0x32,0x32,0x33,0x35,0x39,0x30,0x30,0x5A,0x30,0x5A, +0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x49,0x45,0x31,0x12,0x30, +0x10,0x06,0x03,0x55,0x04,0x0A,0x13,0x09,0x42,0x61,0x6C,0x74,0x69,0x6D,0x6F,0x72, +0x65,0x31,0x13,0x30,0x11,0x06,0x03,0x55,0x04,0x0B,0x13,0x0A,0x43,0x79,0x62,0x65, +0x72,0x54,0x72,0x75,0x73,0x74,0x31,0x22,0x30,0x20,0x06,0x03,0x55,0x04,0x03,0x13, +0x19,0x42,0x61,0x6C,0x74,0x69,0x6D,0x6F,0x72,0x65,0x20,0x43,0x79,0x62,0x65,0x72, +0x54,0x72,0x75,0x73,0x74,0x20,0x52,0x6F,0x6F,0x74,0x30,0x82,0x01,0x22,0x30,0x0D, +0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01, +0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xA3,0x04,0xBB,0x22,0xAB, +0x98,0x3D,0x57,0xE8,0x26,0x72,0x9A,0xB5,0x79,0xD4,0x29,0xE2,0xE1,0xE8,0x95,0x80, +0xB1,0xB0,0xE3,0x5B,0x8E,0x2B,0x29,0x9A,0x64,0xDF,0xA1,0x5D,0xED,0xB0,0x09,0x05, +0x6D,0xDB,0x28,0x2E,0xCE,0x62,0xA2,0x62,0xFE,0xB4,0x88,0xDA,0x12,0xEB,0x38,0xEB, +0x21,0x9D,0xC0,0x41,0x2B,0x01,0x52,0x7B,0x88,0x77,0xD3,0x1C,0x8F,0xC7,0xBA,0xB9, +0x88,0xB5,0x6A,0x09,0xE7,0x73,0xE8,0x11,0x40,0xA7,0xD1,0xCC,0xCA,0x62,0x8D,0x2D, +0xE5,0x8F,0x0B,0xA6,0x50,0xD2,0xA8,0x50,0xC3,0x28,0xEA,0xF5,0xAB,0x25,0x87,0x8A, +0x9A,0x96,0x1C,0xA9,0x67,0xB8,0x3F,0x0C,0xD5,0xF7,0xF9,0x52,0x13,0x2F,0xC2,0x1B, +0xD5,0x70,0x70,0xF0,0x8F,0xC0,0x12,0xCA,0x06,0xCB,0x9A,0xE1,0xD9,0xCA,0x33,0x7A, +0x77,0xD6,0xF8,0xEC,0xB9,0xF1,0x68,0x44,0x42,0x48,0x13,0xD2,0xC0,0xC2,0xA4,0xAE, +0x5E,0x60,0xFE,0xB6,0xA6,0x05,0xFC,0xB4,0xDD,0x07,0x59,0x02,0xD4,0x59,0x18,0x98, +0x63,0xF5,0xA5,0x63,0xE0,0x90,0x0C,0x7D,0x5D,0xB2,0x06,0x7A,0xF3,0x85,0xEA,0xEB, +0xD4,0x03,0xAE,0x5E,0x84,0x3E,0x5F,0xFF,0x15,0xED,0x69,0xBC,0xF9,0x39,0x36,0x72, +0x75,0xCF,0x77,0x52,0x4D,0xF3,0xC9,0x90,0x2C,0xB9,0x3D,0xE5,0xC9,0x23,0x53,0x3F, +0x1F,0x24,0x98,0x21,0x5C,0x07,0x99,0x29,0xBD,0xC6,0x3A,0xEC,0xE7,0x6E,0x86,0x3A, +0x6B,0x97,0x74,0x63,0x33,0xBD,0x68,0x18,0x31,0xF0,0x78,0x8D,0x76,0xBF,0xFC,0x9E, +0x8E,0x5D,0x2A,0x86,0xA7,0x4D,0x90,0xDC,0x27,0x1A,0x39,0x02,0x03,0x01,0x00,0x01, +0xA3,0x45,0x30,0x43,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0xE5, +0x9D,0x59,0x30,0x82,0x47,0x58,0xCC,0xAC,0xFA,0x08,0x54,0x36,0x86,0x7B,0x3A,0xB5, +0x04,0x4D,0xF0,0x30,0x12,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x08,0x30, +0x06,0x01,0x01,0xFF,0x02,0x01,0x03,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01, +0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7, +0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x85,0x0C,0x5D,0x8E,0xE4, +0x6F,0x51,0x68,0x42,0x05,0xA0,0xDD,0xBB,0x4F,0x27,0x25,0x84,0x03,0xBD,0xF7,0x64, +0xFD,0x2D,0xD7,0x30,0xE3,0xA4,0x10,0x17,0xEB,0xDA,0x29,0x29,0xB6,0x79,0x3F,0x76, +0xF6,0x19,0x13,0x23,0xB8,0x10,0x0A,0xF9,0x58,0xA4,0xD4,0x61,0x70,0xBD,0x04,0x61, +0x6A,0x12,0x8A,0x17,0xD5,0x0A,0xBD,0xC5,0xBC,0x30,0x7C,0xD6,0xE9,0x0C,0x25,0x8D, +0x86,0x40,0x4F,0xEC,0xCC,0xA3,0x7E,0x38,0xC6,0x37,0x11,0x4F,0xED,0xDD,0x68,0x31, +0x8E,0x4C,0xD2,0xB3,0x01,0x74,0xEE,0xBE,0x75,0x5E,0x07,0x48,0x1A,0x7F,0x70,0xFF, +0x16,0x5C,0x84,0xC0,0x79,0x85,0xB8,0x05,0xFD,0x7F,0xBE,0x65,0x11,0xA3,0x0F,0xC0, +0x02,0xB4,0xF8,0x52,0x37,0x39,0x04,0xD5,0xA9,0x31,0x7A,0x18,0xBF,0xA0,0x2A,0xF4, +0x12,0x99,0xF7,0xA3,0x45,0x82,0xE3,0x3C,0x5E,0xF5,0x9D,0x9E,0xB5,0xC8,0x9E,0x7C, +0x2E,0xC8,0xA4,0x9E,0x4E,0x08,0x14,0x4B,0x6D,0xFD,0x70,0x6D,0x6B,0x1A,0x63,0xBD, +0x64,0xE6,0x1F,0xB7,0xCE,0xF0,0xF2,0x9F,0x2E,0xBB,0x1B,0xB7,0xF2,0x50,0x88,0x73, +0x92,0xC2,0xE2,0xE3,0x16,0x8D,0x9A,0x32,0x02,0xAB,0x8E,0x18,0xDD,0xE9,0x10,0x11, +0xEE,0x7E,0x35,0xAB,0x90,0xAF,0x3E,0x30,0x94,0x7A,0xD0,0x33,0x3D,0xA7,0x65,0x0F, +0xF5,0xFC,0x8E,0x9E,0x62,0xCF,0x47,0x44,0x2C,0x01,0x5D,0xBB,0x1D,0xB5,0x32,0xD2, +0x47,0xD2,0x38,0x2E,0xD0,0xFE,0x81,0xDC,0x32,0x6A,0x1E,0xB5,0xEE,0x3C,0xD5,0xFC, +0xE7,0x81,0x1D,0x19,0xC3,0x24,0x42,0xEA,0x63,0x39,0xA9, +}; + + +/* subject:/C=GB/ST=Greater Manchester/L=Salford/O=Comodo CA Limited/CN=AAA Certificate Services */ +/* issuer :/C=GB/ST=Greater Manchester/L=Salford/O=Comodo CA Limited/CN=AAA Certificate Services */ + + +const unsigned char Comodo_AAA_Services_root_certificate[1078]={ +0x30,0x82,0x04,0x32,0x30,0x82,0x03,0x1A,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x7B,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x47,0x42,0x31,0x1B, +0x30,0x19,0x06,0x03,0x55,0x04,0x08,0x0C,0x12,0x47,0x72,0x65,0x61,0x74,0x65,0x72, +0x20,0x4D,0x61,0x6E,0x63,0x68,0x65,0x73,0x74,0x65,0x72,0x31,0x10,0x30,0x0E,0x06, +0x03,0x55,0x04,0x07,0x0C,0x07,0x53,0x61,0x6C,0x66,0x6F,0x72,0x64,0x31,0x1A,0x30, +0x18,0x06,0x03,0x55,0x04,0x0A,0x0C,0x11,0x43,0x6F,0x6D,0x6F,0x64,0x6F,0x20,0x43, +0x41,0x20,0x4C,0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x21,0x30,0x1F,0x06,0x03,0x55, +0x04,0x03,0x0C,0x18,0x41,0x41,0x41,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63, +0x61,0x74,0x65,0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x30,0x1E,0x17,0x0D, +0x30,0x34,0x30,0x31,0x30,0x31,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x32, +0x38,0x31,0x32,0x33,0x31,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x7B,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x47,0x42,0x31,0x1B,0x30,0x19,0x06, +0x03,0x55,0x04,0x08,0x0C,0x12,0x47,0x72,0x65,0x61,0x74,0x65,0x72,0x20,0x4D,0x61, +0x6E,0x63,0x68,0x65,0x73,0x74,0x65,0x72,0x31,0x10,0x30,0x0E,0x06,0x03,0x55,0x04, +0x07,0x0C,0x07,0x53,0x61,0x6C,0x66,0x6F,0x72,0x64,0x31,0x1A,0x30,0x18,0x06,0x03, +0x55,0x04,0x0A,0x0C,0x11,0x43,0x6F,0x6D,0x6F,0x64,0x6F,0x20,0x43,0x41,0x20,0x4C, +0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x03,0x0C, +0x18,0x41,0x41,0x41,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65, +0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x30,0x82,0x01,0x22,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F, +0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xBE,0x40,0x9D,0xF4,0x6E,0xE1, +0xEA,0x76,0x87,0x1C,0x4D,0x45,0x44,0x8E,0xBE,0x46,0xC8,0x83,0x06,0x9D,0xC1,0x2A, +0xFE,0x18,0x1F,0x8E,0xE4,0x02,0xFA,0xF3,0xAB,0x5D,0x50,0x8A,0x16,0x31,0x0B,0x9A, +0x06,0xD0,0xC5,0x70,0x22,0xCD,0x49,0x2D,0x54,0x63,0xCC,0xB6,0x6E,0x68,0x46,0x0B, +0x53,0xEA,0xCB,0x4C,0x24,0xC0,0xBC,0x72,0x4E,0xEA,0xF1,0x15,0xAE,0xF4,0x54,0x9A, +0x12,0x0A,0xC3,0x7A,0xB2,0x33,0x60,0xE2,0xDA,0x89,0x55,0xF3,0x22,0x58,0xF3,0xDE, +0xDC,0xCF,0xEF,0x83,0x86,0xA2,0x8C,0x94,0x4F,0x9F,0x68,0xF2,0x98,0x90,0x46,0x84, +0x27,0xC7,0x76,0xBF,0xE3,0xCC,0x35,0x2C,0x8B,0x5E,0x07,0x64,0x65,0x82,0xC0,0x48, +0xB0,0xA8,0x91,0xF9,0x61,0x9F,0x76,0x20,0x50,0xA8,0x91,0xC7,0x66,0xB5,0xEB,0x78, +0x62,0x03,0x56,0xF0,0x8A,0x1A,0x13,0xEA,0x31,0xA3,0x1E,0xA0,0x99,0xFD,0x38,0xF6, +0xF6,0x27,0x32,0x58,0x6F,0x07,0xF5,0x6B,0xB8,0xFB,0x14,0x2B,0xAF,0xB7,0xAA,0xCC, +0xD6,0x63,0x5F,0x73,0x8C,0xDA,0x05,0x99,0xA8,0x38,0xA8,0xCB,0x17,0x78,0x36,0x51, +0xAC,0xE9,0x9E,0xF4,0x78,0x3A,0x8D,0xCF,0x0F,0xD9,0x42,0xE2,0x98,0x0C,0xAB,0x2F, +0x9F,0x0E,0x01,0xDE,0xEF,0x9F,0x99,0x49,0xF1,0x2D,0xDF,0xAC,0x74,0x4D,0x1B,0x98, +0xB5,0x47,0xC5,0xE5,0x29,0xD1,0xF9,0x90,0x18,0xC7,0x62,0x9C,0xBE,0x83,0xC7,0x26, +0x7B,0x3E,0x8A,0x25,0xC7,0xC0,0xDD,0x9D,0xE6,0x35,0x68,0x10,0x20,0x9D,0x8F,0xD8, +0xDE,0xD2,0xC3,0x84,0x9C,0x0D,0x5E,0xE8,0x2F,0xC9,0x02,0x03,0x01,0x00,0x01,0xA3, +0x81,0xC0,0x30,0x81,0xBD,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14, +0xA0,0x11,0x0A,0x23,0x3E,0x96,0xF1,0x07,0xEC,0xE2,0xAF,0x29,0xEF,0x82,0xA5,0x7F, +0xD0,0x30,0xA4,0xB4,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04, +0x03,0x02,0x01,0x06,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05, +0x30,0x03,0x01,0x01,0xFF,0x30,0x7B,0x06,0x03,0x55,0x1D,0x1F,0x04,0x74,0x30,0x72, +0x30,0x38,0xA0,0x36,0xA0,0x34,0x86,0x32,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x63, +0x72,0x6C,0x2E,0x63,0x6F,0x6D,0x6F,0x64,0x6F,0x63,0x61,0x2E,0x63,0x6F,0x6D,0x2F, +0x41,0x41,0x41,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x53,0x65, +0x72,0x76,0x69,0x63,0x65,0x73,0x2E,0x63,0x72,0x6C,0x30,0x36,0xA0,0x34,0xA0,0x32, +0x86,0x30,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x63,0x72,0x6C,0x2E,0x63,0x6F,0x6D, +0x6F,0x64,0x6F,0x2E,0x6E,0x65,0x74,0x2F,0x41,0x41,0x41,0x43,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x65,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x2E,0x63, +0x72,0x6C,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05, +0x00,0x03,0x82,0x01,0x01,0x00,0x08,0x56,0xFC,0x02,0xF0,0x9B,0xE8,0xFF,0xA4,0xFA, +0xD6,0x7B,0xC6,0x44,0x80,0xCE,0x4F,0xC4,0xC5,0xF6,0x00,0x58,0xCC,0xA6,0xB6,0xBC, +0x14,0x49,0x68,0x04,0x76,0xE8,0xE6,0xEE,0x5D,0xEC,0x02,0x0F,0x60,0xD6,0x8D,0x50, +0x18,0x4F,0x26,0x4E,0x01,0xE3,0xE6,0xB0,0xA5,0xEE,0xBF,0xBC,0x74,0x54,0x41,0xBF, +0xFD,0xFC,0x12,0xB8,0xC7,0x4F,0x5A,0xF4,0x89,0x60,0x05,0x7F,0x60,0xB7,0x05,0x4A, +0xF3,0xF6,0xF1,0xC2,0xBF,0xC4,0xB9,0x74,0x86,0xB6,0x2D,0x7D,0x6B,0xCC,0xD2,0xF3, +0x46,0xDD,0x2F,0xC6,0xE0,0x6A,0xC3,0xC3,0x34,0x03,0x2C,0x7D,0x96,0xDD,0x5A,0xC2, +0x0E,0xA7,0x0A,0x99,0xC1,0x05,0x8B,0xAB,0x0C,0x2F,0xF3,0x5C,0x3A,0xCF,0x6C,0x37, +0x55,0x09,0x87,0xDE,0x53,0x40,0x6C,0x58,0xEF,0xFC,0xB6,0xAB,0x65,0x6E,0x04,0xF6, +0x1B,0xDC,0x3C,0xE0,0x5A,0x15,0xC6,0x9E,0xD9,0xF1,0x59,0x48,0x30,0x21,0x65,0x03, +0x6C,0xEC,0xE9,0x21,0x73,0xEC,0x9B,0x03,0xA1,0xE0,0x37,0xAD,0xA0,0x15,0x18,0x8F, +0xFA,0xBA,0x02,0xCE,0xA7,0x2C,0xA9,0x10,0x13,0x2C,0xD4,0xE5,0x08,0x26,0xAB,0x22, +0x97,0x60,0xF8,0x90,0x5E,0x74,0xD4,0xA2,0x9A,0x53,0xBD,0xF2,0xA9,0x68,0xE0,0xA2, +0x6E,0xC2,0xD7,0x6C,0xB1,0xA3,0x0F,0x9E,0xBF,0xEB,0x68,0xE7,0x56,0xF2,0xAE,0xF2, +0xE3,0x2B,0x38,0x3A,0x09,0x81,0xB5,0x6B,0x85,0xD7,0xBE,0x2D,0xED,0x3F,0x1A,0xB7, +0xB2,0x63,0xE2,0xF5,0x62,0x2C,0x82,0xD4,0x6A,0x00,0x41,0x50,0xF1,0x39,0x83,0x9F, +0x95,0xE9,0x36,0x96,0x98,0x6E, +}; + + +/* subject:/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO Certification Authority */ +/* issuer :/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO Certification Authority */ + + +const unsigned char COMODO_Certification_Authority_certificate[1057]={ +0x30,0x82,0x04,0x1D,0x30,0x82,0x03,0x05,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x4E, +0x81,0x2D,0x8A,0x82,0x65,0xE0,0x0B,0x02,0xEE,0x3E,0x35,0x02,0x46,0xE5,0x3D,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x81, +0x81,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x47,0x42,0x31,0x1B, +0x30,0x19,0x06,0x03,0x55,0x04,0x08,0x13,0x12,0x47,0x72,0x65,0x61,0x74,0x65,0x72, +0x20,0x4D,0x61,0x6E,0x63,0x68,0x65,0x73,0x74,0x65,0x72,0x31,0x10,0x30,0x0E,0x06, +0x03,0x55,0x04,0x07,0x13,0x07,0x53,0x61,0x6C,0x66,0x6F,0x72,0x64,0x31,0x1A,0x30, +0x18,0x06,0x03,0x55,0x04,0x0A,0x13,0x11,0x43,0x4F,0x4D,0x4F,0x44,0x4F,0x20,0x43, +0x41,0x20,0x4C,0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x27,0x30,0x25,0x06,0x03,0x55, +0x04,0x03,0x13,0x1E,0x43,0x4F,0x4D,0x4F,0x44,0x4F,0x20,0x43,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69, +0x74,0x79,0x30,0x1E,0x17,0x0D,0x30,0x36,0x31,0x32,0x30,0x31,0x30,0x30,0x30,0x30, +0x30,0x30,0x5A,0x17,0x0D,0x32,0x39,0x31,0x32,0x33,0x31,0x32,0x33,0x35,0x39,0x35, +0x39,0x5A,0x30,0x81,0x81,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02, +0x47,0x42,0x31,0x1B,0x30,0x19,0x06,0x03,0x55,0x04,0x08,0x13,0x12,0x47,0x72,0x65, +0x61,0x74,0x65,0x72,0x20,0x4D,0x61,0x6E,0x63,0x68,0x65,0x73,0x74,0x65,0x72,0x31, +0x10,0x30,0x0E,0x06,0x03,0x55,0x04,0x07,0x13,0x07,0x53,0x61,0x6C,0x66,0x6F,0x72, +0x64,0x31,0x1A,0x30,0x18,0x06,0x03,0x55,0x04,0x0A,0x13,0x11,0x43,0x4F,0x4D,0x4F, +0x44,0x4F,0x20,0x43,0x41,0x20,0x4C,0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x27,0x30, +0x25,0x06,0x03,0x55,0x04,0x03,0x13,0x1E,0x43,0x4F,0x4D,0x4F,0x44,0x4F,0x20,0x43, +0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74, +0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82, +0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xD0,0x40,0x8B,0x8B,0x72,0xE3,0x91,0x1B,0xF7, +0x51,0xC1,0x1B,0x54,0x04,0x98,0xD3,0xA9,0xBF,0xC1,0xE6,0x8A,0x5D,0x3B,0x87,0xFB, +0xBB,0x88,0xCE,0x0D,0xE3,0x2F,0x3F,0x06,0x96,0xF0,0xA2,0x29,0x50,0x99,0xAE,0xDB, +0x3B,0xA1,0x57,0xB0,0x74,0x51,0x71,0xCD,0xED,0x42,0x91,0x4D,0x41,0xFE,0xA9,0xC8, +0xD8,0x6A,0x86,0x77,0x44,0xBB,0x59,0x66,0x97,0x50,0x5E,0xB4,0xD4,0x2C,0x70,0x44, +0xCF,0xDA,0x37,0x95,0x42,0x69,0x3C,0x30,0xC4,0x71,0xB3,0x52,0xF0,0x21,0x4D,0xA1, +0xD8,0xBA,0x39,0x7C,0x1C,0x9E,0xA3,0x24,0x9D,0xF2,0x83,0x16,0x98,0xAA,0x16,0x7C, +0x43,0x9B,0x15,0x5B,0xB7,0xAE,0x34,0x91,0xFE,0xD4,0x62,0x26,0x18,0x46,0x9A,0x3F, +0xEB,0xC1,0xF9,0xF1,0x90,0x57,0xEB,0xAC,0x7A,0x0D,0x8B,0xDB,0x72,0x30,0x6A,0x66, +0xD5,0xE0,0x46,0xA3,0x70,0xDC,0x68,0xD9,0xFF,0x04,0x48,0x89,0x77,0xDE,0xB5,0xE9, +0xFB,0x67,0x6D,0x41,0xE9,0xBC,0x39,0xBD,0x32,0xD9,0x62,0x02,0xF1,0xB1,0xA8,0x3D, +0x6E,0x37,0x9C,0xE2,0x2F,0xE2,0xD3,0xA2,0x26,0x8B,0xC6,0xB8,0x55,0x43,0x88,0xE1, +0x23,0x3E,0xA5,0xD2,0x24,0x39,0x6A,0x47,0xAB,0x00,0xD4,0xA1,0xB3,0xA9,0x25,0xFE, +0x0D,0x3F,0xA7,0x1D,0xBA,0xD3,0x51,0xC1,0x0B,0xA4,0xDA,0xAC,0x38,0xEF,0x55,0x50, +0x24,0x05,0x65,0x46,0x93,0x34,0x4F,0x2D,0x8D,0xAD,0xC6,0xD4,0x21,0x19,0xD2,0x8E, +0xCA,0x05,0x61,0x71,0x07,0x73,0x47,0xE5,0x8A,0x19,0x12,0xBD,0x04,0x4D,0xCE,0x4E, +0x9C,0xA5,0x48,0xAC,0xBB,0x26,0xF7,0x02,0x03,0x01,0x00,0x01,0xA3,0x81,0x8E,0x30, +0x81,0x8B,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x0B,0x58,0xE5, +0x8B,0xC6,0x4C,0x15,0x37,0xA4,0x40,0xA9,0x30,0xA9,0x21,0xBE,0x47,0x36,0x5A,0x56, +0xFF,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01, +0x06,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01, +0x01,0xFF,0x30,0x49,0x06,0x03,0x55,0x1D,0x1F,0x04,0x42,0x30,0x40,0x30,0x3E,0xA0, +0x3C,0xA0,0x3A,0x86,0x38,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x63,0x72,0x6C,0x2E, +0x63,0x6F,0x6D,0x6F,0x64,0x6F,0x63,0x61,0x2E,0x63,0x6F,0x6D,0x2F,0x43,0x4F,0x4D, +0x4F,0x44,0x4F,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E, +0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x2E,0x63,0x72,0x6C,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01, +0x00,0x3E,0x98,0x9E,0x9B,0xF6,0x1B,0xE9,0xD7,0x39,0xB7,0x78,0xAE,0x1D,0x72,0x18, +0x49,0xD3,0x87,0xE4,0x43,0x82,0xEB,0x3F,0xC9,0xAA,0xF5,0xA8,0xB5,0xEF,0x55,0x7C, +0x21,0x52,0x65,0xF9,0xD5,0x0D,0xE1,0x6C,0xF4,0x3E,0x8C,0x93,0x73,0x91,0x2E,0x02, +0xC4,0x4E,0x07,0x71,0x6F,0xC0,0x8F,0x38,0x61,0x08,0xA8,0x1E,0x81,0x0A,0xC0,0x2F, +0x20,0x2F,0x41,0x8B,0x91,0xDC,0x48,0x45,0xBC,0xF1,0xC6,0xDE,0xBA,0x76,0x6B,0x33, +0xC8,0x00,0x2D,0x31,0x46,0x4C,0xED,0xE7,0x9D,0xCF,0x88,0x94,0xFF,0x33,0xC0,0x56, +0xE8,0x24,0x86,0x26,0xB8,0xD8,0x38,0x38,0xDF,0x2A,0x6B,0xDD,0x12,0xCC,0xC7,0x3F, +0x47,0x17,0x4C,0xA2,0xC2,0x06,0x96,0x09,0xD6,0xDB,0xFE,0x3F,0x3C,0x46,0x41,0xDF, +0x58,0xE2,0x56,0x0F,0x3C,0x3B,0xC1,0x1C,0x93,0x35,0xD9,0x38,0x52,0xAC,0xEE,0xC8, +0xEC,0x2E,0x30,0x4E,0x94,0x35,0xB4,0x24,0x1F,0x4B,0x78,0x69,0xDA,0xF2,0x02,0x38, +0xCC,0x95,0x52,0x93,0xF0,0x70,0x25,0x59,0x9C,0x20,0x67,0xC4,0xEE,0xF9,0x8B,0x57, +0x61,0xF4,0x92,0x76,0x7D,0x3F,0x84,0x8D,0x55,0xB7,0xE8,0xE5,0xAC,0xD5,0xF1,0xF5, +0x19,0x56,0xA6,0x5A,0xFB,0x90,0x1C,0xAF,0x93,0xEB,0xE5,0x1C,0xD4,0x67,0x97,0x5D, +0x04,0x0E,0xBE,0x0B,0x83,0xA6,0x17,0x83,0xB9,0x30,0x12,0xA0,0xC5,0x33,0x15,0x05, +0xB9,0x0D,0xFB,0xC7,0x05,0x76,0xE3,0xD8,0x4A,0x8D,0xFC,0x34,0x17,0xA3,0xC6,0x21, +0x28,0xBE,0x30,0x45,0x31,0x1E,0xC7,0x78,0xBE,0x58,0x61,0x38,0xAC,0x3B,0xE2,0x01, +0x65, +}; + + +/* subject:/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO ECC Certification Authority */ +/* issuer :/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO ECC Certification Authority */ + + +const unsigned char COMODO_ECC_Certification_Authority_certificate[653]={ +0x30,0x82,0x02,0x89,0x30,0x82,0x02,0x0F,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x1F, +0x47,0xAF,0xAA,0x62,0x00,0x70,0x50,0x54,0x4C,0x01,0x9E,0x9B,0x63,0x99,0x2A,0x30, +0x0A,0x06,0x08,0x2A,0x86,0x48,0xCE,0x3D,0x04,0x03,0x03,0x30,0x81,0x85,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x47,0x42,0x31,0x1B,0x30,0x19,0x06, +0x03,0x55,0x04,0x08,0x13,0x12,0x47,0x72,0x65,0x61,0x74,0x65,0x72,0x20,0x4D,0x61, +0x6E,0x63,0x68,0x65,0x73,0x74,0x65,0x72,0x31,0x10,0x30,0x0E,0x06,0x03,0x55,0x04, +0x07,0x13,0x07,0x53,0x61,0x6C,0x66,0x6F,0x72,0x64,0x31,0x1A,0x30,0x18,0x06,0x03, +0x55,0x04,0x0A,0x13,0x11,0x43,0x4F,0x4D,0x4F,0x44,0x4F,0x20,0x43,0x41,0x20,0x4C, +0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x2B,0x30,0x29,0x06,0x03,0x55,0x04,0x03,0x13, +0x22,0x43,0x4F,0x4D,0x4F,0x44,0x4F,0x20,0x45,0x43,0x43,0x20,0x43,0x65,0x72,0x74, +0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72, +0x69,0x74,0x79,0x30,0x1E,0x17,0x0D,0x30,0x38,0x30,0x33,0x30,0x36,0x30,0x30,0x30, +0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x38,0x30,0x31,0x31,0x38,0x32,0x33,0x35,0x39, +0x35,0x39,0x5A,0x30,0x81,0x85,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13, +0x02,0x47,0x42,0x31,0x1B,0x30,0x19,0x06,0x03,0x55,0x04,0x08,0x13,0x12,0x47,0x72, +0x65,0x61,0x74,0x65,0x72,0x20,0x4D,0x61,0x6E,0x63,0x68,0x65,0x73,0x74,0x65,0x72, +0x31,0x10,0x30,0x0E,0x06,0x03,0x55,0x04,0x07,0x13,0x07,0x53,0x61,0x6C,0x66,0x6F, +0x72,0x64,0x31,0x1A,0x30,0x18,0x06,0x03,0x55,0x04,0x0A,0x13,0x11,0x43,0x4F,0x4D, +0x4F,0x44,0x4F,0x20,0x43,0x41,0x20,0x4C,0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x2B, +0x30,0x29,0x06,0x03,0x55,0x04,0x03,0x13,0x22,0x43,0x4F,0x4D,0x4F,0x44,0x4F,0x20, +0x45,0x43,0x43,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F, +0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x76,0x30,0x10,0x06, +0x07,0x2A,0x86,0x48,0xCE,0x3D,0x02,0x01,0x06,0x05,0x2B,0x81,0x04,0x00,0x22,0x03, +0x62,0x00,0x04,0x03,0x47,0x7B,0x2F,0x75,0xC9,0x82,0x15,0x85,0xFB,0x75,0xE4,0x91, +0x16,0xD4,0xAB,0x62,0x99,0xF5,0x3E,0x52,0x0B,0x06,0xCE,0x41,0x00,0x7F,0x97,0xE1, +0x0A,0x24,0x3C,0x1D,0x01,0x04,0xEE,0x3D,0xD2,0x8D,0x09,0x97,0x0C,0xE0,0x75,0xE4, +0xFA,0xFB,0x77,0x8A,0x2A,0xF5,0x03,0x60,0x4B,0x36,0x8B,0x16,0x23,0x16,0xAD,0x09, +0x71,0xF4,0x4A,0xF4,0x28,0x50,0xB4,0xFE,0x88,0x1C,0x6E,0x3F,0x6C,0x2F,0x2F,0x09, +0x59,0x5B,0xA5,0x5B,0x0B,0x33,0x99,0xE2,0xC3,0x3D,0x89,0xF9,0x6A,0x2C,0xEF,0xB2, +0xD3,0x06,0xE9,0xA3,0x42,0x30,0x40,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16, +0x04,0x14,0x75,0x71,0xA7,0x19,0x48,0x19,0xBC,0x9D,0x9D,0xEA,0x41,0x47,0xDF,0x94, +0xC4,0x48,0x77,0x99,0xD3,0x79,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF, +0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF, +0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0A,0x06,0x08,0x2A,0x86,0x48,0xCE,0x3D, +0x04,0x03,0x03,0x03,0x68,0x00,0x30,0x65,0x02,0x31,0x00,0xEF,0x03,0x5B,0x7A,0xAC, +0xB7,0x78,0x0A,0x72,0xB7,0x88,0xDF,0xFF,0xB5,0x46,0x14,0x09,0x0A,0xFA,0xA0,0xE6, +0x7D,0x08,0xC6,0x1A,0x87,0xBD,0x18,0xA8,0x73,0xBD,0x26,0xCA,0x60,0x0C,0x9D,0xCE, +0x99,0x9F,0xCF,0x5C,0x0F,0x30,0xE1,0xBE,0x14,0x31,0xEA,0x02,0x30,0x14,0xF4,0x93, +0x3C,0x49,0xA7,0x33,0x7A,0x90,0x46,0x47,0xB3,0x63,0x7D,0x13,0x9B,0x4E,0xB7,0x6F, +0x18,0x37,0x80,0x53,0xFE,0xDD,0x20,0xE0,0x35,0x9A,0x36,0xD1,0xC7,0x01,0xB9,0xE6, +0xDC,0xDD,0xF3,0xFF,0x1D,0x2C,0x3A,0x16,0x57,0xD9,0x92,0x39,0xD6, +}; + + +/* subject:/C=GB/ST=Greater Manchester/L=Salford/O=Comodo CA Limited/CN=Secure Certificate Services */ +/* issuer :/C=GB/ST=Greater Manchester/L=Salford/O=Comodo CA Limited/CN=Secure Certificate Services */ + + +const unsigned char Comodo_Secure_Services_root_certificate[1091]={ +0x30,0x82,0x04,0x3F,0x30,0x82,0x03,0x27,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x7E,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x47,0x42,0x31,0x1B, +0x30,0x19,0x06,0x03,0x55,0x04,0x08,0x0C,0x12,0x47,0x72,0x65,0x61,0x74,0x65,0x72, +0x20,0x4D,0x61,0x6E,0x63,0x68,0x65,0x73,0x74,0x65,0x72,0x31,0x10,0x30,0x0E,0x06, +0x03,0x55,0x04,0x07,0x0C,0x07,0x53,0x61,0x6C,0x66,0x6F,0x72,0x64,0x31,0x1A,0x30, +0x18,0x06,0x03,0x55,0x04,0x0A,0x0C,0x11,0x43,0x6F,0x6D,0x6F,0x64,0x6F,0x20,0x43, +0x41,0x20,0x4C,0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x24,0x30,0x22,0x06,0x03,0x55, +0x04,0x03,0x0C,0x1B,0x53,0x65,0x63,0x75,0x72,0x65,0x20,0x43,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x30, +0x1E,0x17,0x0D,0x30,0x34,0x30,0x31,0x30,0x31,0x30,0x30,0x30,0x30,0x30,0x30,0x5A, +0x17,0x0D,0x32,0x38,0x31,0x32,0x33,0x31,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30, +0x7E,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x47,0x42,0x31,0x1B, +0x30,0x19,0x06,0x03,0x55,0x04,0x08,0x0C,0x12,0x47,0x72,0x65,0x61,0x74,0x65,0x72, +0x20,0x4D,0x61,0x6E,0x63,0x68,0x65,0x73,0x74,0x65,0x72,0x31,0x10,0x30,0x0E,0x06, +0x03,0x55,0x04,0x07,0x0C,0x07,0x53,0x61,0x6C,0x66,0x6F,0x72,0x64,0x31,0x1A,0x30, +0x18,0x06,0x03,0x55,0x04,0x0A,0x0C,0x11,0x43,0x6F,0x6D,0x6F,0x64,0x6F,0x20,0x43, +0x41,0x20,0x4C,0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x24,0x30,0x22,0x06,0x03,0x55, +0x04,0x03,0x0C,0x1B,0x53,0x65,0x63,0x75,0x72,0x65,0x20,0x43,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x30, +0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01, +0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00, +0xC0,0x71,0x33,0x82,0x8A,0xD0,0x70,0xEB,0x73,0x87,0x82,0x40,0xD5,0x1D,0xE4,0xCB, +0xC9,0x0E,0x42,0x90,0xF9,0xDE,0x34,0xB9,0xA1,0xBA,0x11,0xF4,0x25,0x85,0xF3,0xCC, +0x72,0x6D,0xF2,0x7B,0x97,0x6B,0xB3,0x07,0xF1,0x77,0x24,0x91,0x5F,0x25,0x8F,0xF6, +0x74,0x3D,0xE4,0x80,0xC2,0xF8,0x3C,0x0D,0xF3,0xBF,0x40,0xEA,0xF7,0xC8,0x52,0xD1, +0x72,0x6F,0xEF,0xC8,0xAB,0x41,0xB8,0x6E,0x2E,0x17,0x2A,0x95,0x69,0x0C,0xCD,0xD2, +0x1E,0x94,0x7B,0x2D,0x94,0x1D,0xAA,0x75,0xD7,0xB3,0x98,0xCB,0xAC,0xBC,0x64,0x53, +0x40,0xBC,0x8F,0xAC,0xAC,0x36,0xCB,0x5C,0xAD,0xBB,0xDD,0xE0,0x94,0x17,0xEC,0xD1, +0x5C,0xD0,0xBF,0xEF,0xA5,0x95,0xC9,0x90,0xC5,0xB0,0xAC,0xFB,0x1B,0x43,0xDF,0x7A, +0x08,0x5D,0xB7,0xB8,0xF2,0x40,0x1B,0x2B,0x27,0x9E,0x50,0xCE,0x5E,0x65,0x82,0x88, +0x8C,0x5E,0xD3,0x4E,0x0C,0x7A,0xEA,0x08,0x91,0xB6,0x36,0xAA,0x2B,0x42,0xFB,0xEA, +0xC2,0xA3,0x39,0xE5,0xDB,0x26,0x38,0xAD,0x8B,0x0A,0xEE,0x19,0x63,0xC7,0x1C,0x24, +0xDF,0x03,0x78,0xDA,0xE6,0xEA,0xC1,0x47,0x1A,0x0B,0x0B,0x46,0x09,0xDD,0x02,0xFC, +0xDE,0xCB,0x87,0x5F,0xD7,0x30,0x63,0x68,0xA1,0xAE,0xDC,0x32,0xA1,0xBA,0xBE,0xFE, +0x44,0xAB,0x68,0xB6,0xA5,0x17,0x15,0xFD,0xBD,0xD5,0xA7,0xA7,0x9A,0xE4,0x44,0x33, +0xE9,0x88,0x8E,0xFC,0xED,0x51,0xEB,0x93,0x71,0x4E,0xAD,0x01,0xE7,0x44,0x8E,0xAB, +0x2D,0xCB,0xA8,0xFE,0x01,0x49,0x48,0xF0,0xC0,0xDD,0xC7,0x68,0xD8,0x92,0xFE,0x3D, +0x02,0x03,0x01,0x00,0x01,0xA3,0x81,0xC7,0x30,0x81,0xC4,0x30,0x1D,0x06,0x03,0x55, +0x1D,0x0E,0x04,0x16,0x04,0x14,0x3C,0xD8,0x93,0x88,0xC2,0xC0,0x82,0x09,0xCC,0x01, +0x99,0x06,0x93,0x20,0xE9,0x9E,0x70,0x09,0x63,0x4F,0x30,0x0E,0x06,0x03,0x55,0x1D, +0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x0F,0x06,0x03,0x55,0x1D, +0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x81,0x81,0x06,0x03, +0x55,0x1D,0x1F,0x04,0x7A,0x30,0x78,0x30,0x3B,0xA0,0x39,0xA0,0x37,0x86,0x35,0x68, +0x74,0x74,0x70,0x3A,0x2F,0x2F,0x63,0x72,0x6C,0x2E,0x63,0x6F,0x6D,0x6F,0x64,0x6F, +0x63,0x61,0x2E,0x63,0x6F,0x6D,0x2F,0x53,0x65,0x63,0x75,0x72,0x65,0x43,0x65,0x72, +0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73, +0x2E,0x63,0x72,0x6C,0x30,0x39,0xA0,0x37,0xA0,0x35,0x86,0x33,0x68,0x74,0x74,0x70, +0x3A,0x2F,0x2F,0x63,0x72,0x6C,0x2E,0x63,0x6F,0x6D,0x6F,0x64,0x6F,0x2E,0x6E,0x65, +0x74,0x2F,0x53,0x65,0x63,0x75,0x72,0x65,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63, +0x61,0x74,0x65,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x2E,0x63,0x72,0x6C,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82, +0x01,0x01,0x00,0x87,0x01,0x6D,0x23,0x1D,0x7E,0x5B,0x17,0x7D,0xC1,0x61,0x32,0xCF, +0x8F,0xE7,0xF3,0x8A,0x94,0x59,0x66,0xE0,0x9E,0x28,0xA8,0x5E,0xD3,0xB7,0xF4,0x34, +0xE6,0xAA,0x39,0xB2,0x97,0x16,0xC5,0x82,0x6F,0x32,0xA4,0xE9,0x8C,0xE7,0xAF,0xFD, +0xEF,0xC2,0xE8,0xB9,0x4B,0xAA,0xA3,0xF4,0xE6,0xDA,0x8D,0x65,0x21,0xFB,0xBA,0x80, +0xEB,0x26,0x28,0x85,0x1A,0xFE,0x39,0x8C,0xDE,0x5B,0x04,0x04,0xB4,0x54,0xF9,0xA3, +0x67,0x9E,0x41,0xFA,0x09,0x52,0xCC,0x05,0x48,0xA8,0xC9,0x3F,0x21,0x04,0x1E,0xCE, +0x48,0x6B,0xFC,0x85,0xE8,0xC2,0x7B,0xAF,0x7F,0xB7,0xCC,0xF8,0x5F,0x3A,0xFD,0x35, +0xC6,0x0D,0xEF,0x97,0xDC,0x4C,0xAB,0x11,0xE1,0x6B,0xCB,0x31,0xD1,0x6C,0xFB,0x48, +0x80,0xAB,0xDC,0x9C,0x37,0xB8,0x21,0x14,0x4B,0x0D,0x71,0x3D,0xEC,0x83,0x33,0x6E, +0xD1,0x6E,0x32,0x16,0xEC,0x98,0xC7,0x16,0x8B,0x59,0xA6,0x34,0xAB,0x05,0x57,0x2D, +0x93,0xF7,0xAA,0x13,0xCB,0xD2,0x13,0xE2,0xB7,0x2E,0x3B,0xCD,0x6B,0x50,0x17,0x09, +0x68,0x3E,0xB5,0x26,0x57,0xEE,0xB6,0xE0,0xB6,0xDD,0xB9,0x29,0x80,0x79,0x7D,0x8F, +0xA3,0xF0,0xA4,0x28,0xA4,0x15,0xC4,0x85,0xF4,0x27,0xD4,0x6B,0xBF,0xE5,0x5C,0xE4, +0x65,0x02,0x76,0x54,0xB4,0xE3,0x37,0x66,0x24,0xD3,0x19,0x61,0xC8,0x52,0x10,0xE5, +0x8B,0x37,0x9A,0xB9,0xA9,0xF9,0x1D,0xBF,0xEA,0x99,0x92,0x61,0x96,0xFF,0x01,0xCD, +0xA1,0x5F,0x0D,0xBC,0x71,0xBC,0x0E,0xAC,0x0B,0x1D,0x47,0x45,0x1D,0xC1,0xEC,0x7C, +0xEC,0xFD,0x29, +}; + + +/* subject:/C=GB/ST=Greater Manchester/L=Salford/O=Comodo CA Limited/CN=Trusted Certificate Services */ +/* issuer :/C=GB/ST=Greater Manchester/L=Salford/O=Comodo CA Limited/CN=Trusted Certificate Services */ + + +const unsigned char Comodo_Trusted_Services_root_certificate[1095]={ +0x30,0x82,0x04,0x43,0x30,0x82,0x03,0x2B,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x7F,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x47,0x42,0x31,0x1B, +0x30,0x19,0x06,0x03,0x55,0x04,0x08,0x0C,0x12,0x47,0x72,0x65,0x61,0x74,0x65,0x72, +0x20,0x4D,0x61,0x6E,0x63,0x68,0x65,0x73,0x74,0x65,0x72,0x31,0x10,0x30,0x0E,0x06, +0x03,0x55,0x04,0x07,0x0C,0x07,0x53,0x61,0x6C,0x66,0x6F,0x72,0x64,0x31,0x1A,0x30, +0x18,0x06,0x03,0x55,0x04,0x0A,0x0C,0x11,0x43,0x6F,0x6D,0x6F,0x64,0x6F,0x20,0x43, +0x41,0x20,0x4C,0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x25,0x30,0x23,0x06,0x03,0x55, +0x04,0x03,0x0C,0x1C,0x54,0x72,0x75,0x73,0x74,0x65,0x64,0x20,0x43,0x65,0x72,0x74, +0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73, +0x30,0x1E,0x17,0x0D,0x30,0x34,0x30,0x31,0x30,0x31,0x30,0x30,0x30,0x30,0x30,0x30, +0x5A,0x17,0x0D,0x32,0x38,0x31,0x32,0x33,0x31,0x32,0x33,0x35,0x39,0x35,0x39,0x5A, +0x30,0x7F,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x47,0x42,0x31, +0x1B,0x30,0x19,0x06,0x03,0x55,0x04,0x08,0x0C,0x12,0x47,0x72,0x65,0x61,0x74,0x65, +0x72,0x20,0x4D,0x61,0x6E,0x63,0x68,0x65,0x73,0x74,0x65,0x72,0x31,0x10,0x30,0x0E, +0x06,0x03,0x55,0x04,0x07,0x0C,0x07,0x53,0x61,0x6C,0x66,0x6F,0x72,0x64,0x31,0x1A, +0x30,0x18,0x06,0x03,0x55,0x04,0x0A,0x0C,0x11,0x43,0x6F,0x6D,0x6F,0x64,0x6F,0x20, +0x43,0x41,0x20,0x4C,0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x25,0x30,0x23,0x06,0x03, +0x55,0x04,0x03,0x0C,0x1C,0x54,0x72,0x75,0x73,0x74,0x65,0x64,0x20,0x43,0x65,0x72, +0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65, +0x73,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01, +0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01, +0x01,0x00,0xDF,0x71,0x6F,0x36,0x58,0x53,0x5A,0xF2,0x36,0x54,0x57,0x80,0xC4,0x74, +0x08,0x20,0xED,0x18,0x7F,0x2A,0x1D,0xE6,0x35,0x9A,0x1E,0x25,0xAC,0x9C,0xE5,0x96, +0x7E,0x72,0x52,0xA0,0x15,0x42,0xDB,0x59,0xDD,0x64,0x7A,0x1A,0xD0,0xB8,0x7B,0xDD, +0x39,0x15,0xBC,0x55,0x48,0xC4,0xED,0x3A,0x00,0xEA,0x31,0x11,0xBA,0xF2,0x71,0x74, +0x1A,0x67,0xB8,0xCF,0x33,0xCC,0xA8,0x31,0xAF,0xA3,0xE3,0xD7,0x7F,0xBF,0x33,0x2D, +0x4C,0x6A,0x3C,0xEC,0x8B,0xC3,0x92,0xD2,0x53,0x77,0x24,0x74,0x9C,0x07,0x6E,0x70, +0xFC,0xBD,0x0B,0x5B,0x76,0xBA,0x5F,0xF2,0xFF,0xD7,0x37,0x4B,0x4A,0x60,0x78,0xF7, +0xF0,0xFA,0xCA,0x70,0xB4,0xEA,0x59,0xAA,0xA3,0xCE,0x48,0x2F,0xA9,0xC3,0xB2,0x0B, +0x7E,0x17,0x72,0x16,0x0C,0xA6,0x07,0x0C,0x1B,0x38,0xCF,0xC9,0x62,0xB7,0x3F,0xA0, +0x93,0xA5,0x87,0x41,0xF2,0xB7,0x70,0x40,0x77,0xD8,0xBE,0x14,0x7C,0xE3,0xA8,0xC0, +0x7A,0x8E,0xE9,0x63,0x6A,0xD1,0x0F,0x9A,0xC6,0xD2,0xF4,0x8B,0x3A,0x14,0x04,0x56, +0xD4,0xED,0xB8,0xCC,0x6E,0xF5,0xFB,0xE2,0x2C,0x58,0xBD,0x7F,0x4F,0x6B,0x2B,0xF7, +0x60,0x24,0x58,0x24,0xCE,0x26,0xEF,0x34,0x91,0x3A,0xD5,0xE3,0x81,0xD0,0xB2,0xF0, +0x04,0x02,0xD7,0x5B,0xB7,0x3E,0x92,0xAC,0x6B,0x12,0x8A,0xF9,0xE4,0x05,0xB0,0x3B, +0x91,0x49,0x5C,0xB2,0xEB,0x53,0xEA,0xF8,0x9F,0x47,0x86,0xEE,0xBF,0x95,0xC0,0xC0, +0x06,0x9F,0xD2,0x5B,0x5E,0x11,0x1B,0xF4,0xC7,0x04,0x35,0x29,0xD2,0x55,0x5C,0xE4, +0xED,0xEB,0x02,0x03,0x01,0x00,0x01,0xA3,0x81,0xC9,0x30,0x81,0xC6,0x30,0x1D,0x06, +0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0xC5,0x7B,0x58,0xBD,0xED,0xDA,0x25,0x69, +0xD2,0xF7,0x59,0x16,0xA8,0xB3,0x32,0xC0,0x7B,0x27,0x5B,0xF4,0x30,0x0E,0x06,0x03, +0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x0F,0x06,0x03, +0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x81,0x83, +0x06,0x03,0x55,0x1D,0x1F,0x04,0x7C,0x30,0x7A,0x30,0x3C,0xA0,0x3A,0xA0,0x38,0x86, +0x36,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x63,0x72,0x6C,0x2E,0x63,0x6F,0x6D,0x6F, +0x64,0x6F,0x63,0x61,0x2E,0x63,0x6F,0x6D,0x2F,0x54,0x72,0x75,0x73,0x74,0x65,0x64, +0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x53,0x65,0x72,0x76,0x69, +0x63,0x65,0x73,0x2E,0x63,0x72,0x6C,0x30,0x3A,0xA0,0x38,0xA0,0x36,0x86,0x34,0x68, +0x74,0x74,0x70,0x3A,0x2F,0x2F,0x63,0x72,0x6C,0x2E,0x63,0x6F,0x6D,0x6F,0x64,0x6F, +0x2E,0x6E,0x65,0x74,0x2F,0x54,0x72,0x75,0x73,0x74,0x65,0x64,0x43,0x65,0x72,0x74, +0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x2E, +0x63,0x72,0x6C,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05, +0x05,0x00,0x03,0x82,0x01,0x01,0x00,0xC8,0x93,0x81,0x3B,0x89,0xB4,0xAF,0xB8,0x84, +0x12,0x4C,0x8D,0xD2,0xF0,0xDB,0x70,0xBA,0x57,0x86,0x15,0x34,0x10,0xB9,0x2F,0x7F, +0x1E,0xB0,0xA8,0x89,0x60,0xA1,0x8A,0xC2,0x77,0x0C,0x50,0x4A,0x9B,0x00,0x8B,0xD8, +0x8B,0xF4,0x41,0xE2,0xD0,0x83,0x8A,0x4A,0x1C,0x14,0x06,0xB0,0xA3,0x68,0x05,0x70, +0x31,0x30,0xA7,0x53,0x9B,0x0E,0xE9,0x4A,0xA0,0x58,0x69,0x67,0x0E,0xAE,0x9D,0xF6, +0xA5,0x2C,0x41,0xBF,0x3C,0x06,0x6B,0xE4,0x59,0xCC,0x6D,0x10,0xF1,0x96,0x6F,0x1F, +0xDF,0xF4,0x04,0x02,0xA4,0x9F,0x45,0x3E,0xC8,0xD8,0xFA,0x36,0x46,0x44,0x50,0x3F, +0x82,0x97,0x91,0x1F,0x28,0xDB,0x18,0x11,0x8C,0x2A,0xE4,0x65,0x83,0x57,0x12,0x12, +0x8C,0x17,0x3F,0x94,0x36,0xFE,0x5D,0xB0,0xC0,0x04,0x77,0x13,0xB8,0xF4,0x15,0xD5, +0x3F,0x38,0xCC,0x94,0x3A,0x55,0xD0,0xAC,0x98,0xF5,0xBA,0x00,0x5F,0xE0,0x86,0x19, +0x81,0x78,0x2F,0x28,0xC0,0x7E,0xD3,0xCC,0x42,0x0A,0xF5,0xAE,0x50,0xA0,0xD1,0x3E, +0xC6,0xA1,0x71,0xEC,0x3F,0xA0,0x20,0x8C,0x66,0x3A,0x89,0xB4,0x8E,0xD4,0xD8,0xB1, +0x4D,0x25,0x47,0xEE,0x2F,0x88,0xC8,0xB5,0xE1,0x05,0x45,0xC0,0xBE,0x14,0x71,0xDE, +0x7A,0xFD,0x8E,0x7B,0x7D,0x4D,0x08,0x96,0xA5,0x12,0x73,0xF0,0x2D,0xCA,0x37,0x27, +0x74,0x12,0x27,0x4C,0xCB,0xB6,0x97,0xE9,0xD9,0xAE,0x08,0x6D,0x5A,0x39,0x40,0xDD, +0x05,0x47,0x75,0x6A,0x5A,0x21,0xB3,0xA3,0x18,0xCF,0x4E,0xF7,0x2E,0x57,0xB7,0x98, +0x70,0x5E,0xC8,0xC4,0x78,0xB0,0x62, +}; + + +/* subject:/O=Cybertrust, Inc/CN=Cybertrust Global Root */ +/* issuer :/O=Cybertrust, Inc/CN=Cybertrust Global Root */ + + +const unsigned char Cybertrust_Global_Root_certificate[933]={ +0x30,0x82,0x03,0xA1,0x30,0x82,0x02,0x89,0xA0,0x03,0x02,0x01,0x02,0x02,0x0B,0x04, +0x00,0x00,0x00,0x00,0x01,0x0F,0x85,0xAA,0x2D,0x48,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x3B,0x31,0x18,0x30,0x16,0x06, +0x03,0x55,0x04,0x0A,0x13,0x0F,0x43,0x79,0x62,0x65,0x72,0x74,0x72,0x75,0x73,0x74, +0x2C,0x20,0x49,0x6E,0x63,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x03,0x13,0x16, +0x43,0x79,0x62,0x65,0x72,0x74,0x72,0x75,0x73,0x74,0x20,0x47,0x6C,0x6F,0x62,0x61, +0x6C,0x20,0x52,0x6F,0x6F,0x74,0x30,0x1E,0x17,0x0D,0x30,0x36,0x31,0x32,0x31,0x35, +0x30,0x38,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x32,0x31,0x31,0x32,0x31,0x35,0x30, +0x38,0x30,0x30,0x30,0x30,0x5A,0x30,0x3B,0x31,0x18,0x30,0x16,0x06,0x03,0x55,0x04, +0x0A,0x13,0x0F,0x43,0x79,0x62,0x65,0x72,0x74,0x72,0x75,0x73,0x74,0x2C,0x20,0x49, +0x6E,0x63,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x03,0x13,0x16,0x43,0x79,0x62, +0x65,0x72,0x74,0x72,0x75,0x73,0x74,0x20,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x20,0x52, +0x6F,0x6F,0x74,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7, +0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02, +0x82,0x01,0x01,0x00,0xF8,0xC8,0xBC,0xBD,0x14,0x50,0x66,0x13,0xFF,0xF0,0xD3,0x79, +0xEC,0x23,0xF2,0xB7,0x1A,0xC7,0x8E,0x85,0xF1,0x12,0x73,0xA6,0x19,0xAA,0x10,0xDB, +0x9C,0xA2,0x65,0x74,0x5A,0x77,0x3E,0x51,0x7D,0x56,0xF6,0xDC,0x23,0xB6,0xD4,0xED, +0x5F,0x58,0xB1,0x37,0x4D,0xD5,0x49,0x0E,0x6E,0xF5,0x6A,0x87,0xD6,0xD2,0x8C,0xD2, +0x27,0xC6,0xE2,0xFF,0x36,0x9F,0x98,0x65,0xA0,0x13,0x4E,0xC6,0x2A,0x64,0x9B,0xD5, +0x90,0x12,0xCF,0x14,0x06,0xF4,0x3B,0xE3,0xD4,0x28,0xBE,0xE8,0x0E,0xF8,0xAB,0x4E, +0x48,0x94,0x6D,0x8E,0x95,0x31,0x10,0x5C,0xED,0xA2,0x2D,0xBD,0xD5,0x3A,0x6D,0xB2, +0x1C,0xBB,0x60,0xC0,0x46,0x4B,0x01,0xF5,0x49,0xAE,0x7E,0x46,0x8A,0xD0,0x74,0x8D, +0xA1,0x0C,0x02,0xCE,0xEE,0xFC,0xE7,0x8F,0xB8,0x6B,0x66,0xF3,0x7F,0x44,0x00,0xBF, +0x66,0x25,0x14,0x2B,0xDD,0x10,0x30,0x1D,0x07,0x96,0x3F,0x4D,0xF6,0x6B,0xB8,0x8F, +0xB7,0x7B,0x0C,0xA5,0x38,0xEB,0xDE,0x47,0xDB,0xD5,0x5D,0x39,0xFC,0x88,0xA7,0xF3, +0xD7,0x2A,0x74,0xF1,0xE8,0x5A,0xA2,0x3B,0x9F,0x50,0xBA,0xA6,0x8C,0x45,0x35,0xC2, +0x50,0x65,0x95,0xDC,0x63,0x82,0xEF,0xDD,0xBF,0x77,0x4D,0x9C,0x62,0xC9,0x63,0x73, +0x16,0xD0,0x29,0x0F,0x49,0xA9,0x48,0xF0,0xB3,0xAA,0xB7,0x6C,0xC5,0xA7,0x30,0x39, +0x40,0x5D,0xAE,0xC4,0xE2,0x5D,0x26,0x53,0xF0,0xCE,0x1C,0x23,0x08,0x61,0xA8,0x94, +0x19,0xBA,0x04,0x62,0x40,0xEC,0x1F,0x38,0x70,0x77,0x12,0x06,0x71,0xA7,0x30,0x18, +0x5D,0x25,0x27,0xA5,0x02,0x03,0x01,0x00,0x01,0xA3,0x81,0xA5,0x30,0x81,0xA2,0x30, +0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30, +0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF, +0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0xB6,0x08,0x7B,0x0D,0x7A, +0xCC,0xAC,0x20,0x4C,0x86,0x56,0x32,0x5E,0xCF,0xAB,0x6E,0x85,0x2D,0x70,0x57,0x30, +0x3F,0x06,0x03,0x55,0x1D,0x1F,0x04,0x38,0x30,0x36,0x30,0x34,0xA0,0x32,0xA0,0x30, +0x86,0x2E,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x77,0x77,0x77,0x32,0x2E,0x70,0x75, +0x62,0x6C,0x69,0x63,0x2D,0x74,0x72,0x75,0x73,0x74,0x2E,0x63,0x6F,0x6D,0x2F,0x63, +0x72,0x6C,0x2F,0x63,0x74,0x2F,0x63,0x74,0x72,0x6F,0x6F,0x74,0x2E,0x63,0x72,0x6C, +0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14,0xB6,0x08,0x7B, +0x0D,0x7A,0xCC,0xAC,0x20,0x4C,0x86,0x56,0x32,0x5E,0xCF,0xAB,0x6E,0x85,0x2D,0x70, +0x57,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00, +0x03,0x82,0x01,0x01,0x00,0x56,0xEF,0x0A,0x23,0xA0,0x54,0x4E,0x95,0x97,0xC9,0xF8, +0x89,0xDA,0x45,0xC1,0xD4,0xA3,0x00,0x25,0xF4,0x1F,0x13,0xAB,0xB7,0xA3,0x85,0x58, +0x69,0xC2,0x30,0xAD,0xD8,0x15,0x8A,0x2D,0xE3,0xC9,0xCD,0x81,0x5A,0xF8,0x73,0x23, +0x5A,0xA7,0x7C,0x05,0xF3,0xFD,0x22,0x3B,0x0E,0xD1,0x06,0xC4,0xDB,0x36,0x4C,0x73, +0x04,0x8E,0xE5,0xB0,0x22,0xE4,0xC5,0xF3,0x2E,0xA5,0xD9,0x23,0xE3,0xB8,0x4E,0x4A, +0x20,0xA7,0x6E,0x02,0x24,0x9F,0x22,0x60,0x67,0x7B,0x8B,0x1D,0x72,0x09,0xC5,0x31, +0x5C,0xE9,0x79,0x9F,0x80,0x47,0x3D,0xAD,0xA1,0x0B,0x07,0x14,0x3D,0x47,0xFF,0x03, +0x69,0x1A,0x0C,0x0B,0x44,0xE7,0x63,0x25,0xA7,0x7F,0xB2,0xC9,0xB8,0x76,0x84,0xED, +0x23,0xF6,0x7D,0x07,0xAB,0x45,0x7E,0xD3,0xDF,0xB3,0xBF,0xE9,0x8A,0xB6,0xCD,0xA8, +0xA2,0x67,0x2B,0x52,0xD5,0xB7,0x65,0xF0,0x39,0x4C,0x63,0xA0,0x91,0x79,0x93,0x52, +0x0F,0x54,0xDD,0x83,0xBB,0x9F,0xD1,0x8F,0xA7,0x53,0x73,0xC3,0xCB,0xFF,0x30,0xEC, +0x7C,0x04,0xB8,0xD8,0x44,0x1F,0x93,0x5F,0x71,0x09,0x22,0xB7,0x6E,0x3E,0xEA,0x1C, +0x03,0x4E,0x9D,0x1A,0x20,0x61,0xFB,0x81,0x37,0xEC,0x5E,0xFC,0x0A,0x45,0xAB,0xD7, +0xE7,0x17,0x55,0xD0,0xA0,0xEA,0x60,0x9B,0xA6,0xF6,0xE3,0x8C,0x5B,0x29,0xC2,0x06, +0x60,0x14,0x9D,0x2D,0x97,0x4C,0xA9,0x93,0x15,0x9D,0x61,0xC4,0x01,0x5F,0x48,0xD6, +0x58,0xBD,0x56,0x31,0x12,0x4E,0x11,0xC8,0x21,0xE0,0xB3,0x11,0x91,0x65,0xDB,0xB4, +0xA6,0x88,0x38,0xCE,0x55, +}; + + +/* subject:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Assured ID Root CA */ +/* issuer :/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Assured ID Root CA */ + + +const unsigned char DigiCert_Assured_ID_Root_CA_certificate[955]={ +0x30,0x82,0x03,0xB7,0x30,0x82,0x02,0x9F,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x0C, +0xE7,0xE0,0xE5,0x17,0xD8,0x46,0xFE,0x8F,0xE5,0x60,0xFC,0x1B,0xF0,0x30,0x39,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x65, +0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x15,0x30, +0x13,0x06,0x03,0x55,0x04,0x0A,0x13,0x0C,0x44,0x69,0x67,0x69,0x43,0x65,0x72,0x74, +0x20,0x49,0x6E,0x63,0x31,0x19,0x30,0x17,0x06,0x03,0x55,0x04,0x0B,0x13,0x10,0x77, +0x77,0x77,0x2E,0x64,0x69,0x67,0x69,0x63,0x65,0x72,0x74,0x2E,0x63,0x6F,0x6D,0x31, +0x24,0x30,0x22,0x06,0x03,0x55,0x04,0x03,0x13,0x1B,0x44,0x69,0x67,0x69,0x43,0x65, +0x72,0x74,0x20,0x41,0x73,0x73,0x75,0x72,0x65,0x64,0x20,0x49,0x44,0x20,0x52,0x6F, +0x6F,0x74,0x20,0x43,0x41,0x30,0x1E,0x17,0x0D,0x30,0x36,0x31,0x31,0x31,0x30,0x30, +0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x31,0x31,0x31,0x31,0x30,0x30,0x30, +0x30,0x30,0x30,0x30,0x5A,0x30,0x65,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06, +0x13,0x02,0x55,0x53,0x31,0x15,0x30,0x13,0x06,0x03,0x55,0x04,0x0A,0x13,0x0C,0x44, +0x69,0x67,0x69,0x43,0x65,0x72,0x74,0x20,0x49,0x6E,0x63,0x31,0x19,0x30,0x17,0x06, +0x03,0x55,0x04,0x0B,0x13,0x10,0x77,0x77,0x77,0x2E,0x64,0x69,0x67,0x69,0x63,0x65, +0x72,0x74,0x2E,0x63,0x6F,0x6D,0x31,0x24,0x30,0x22,0x06,0x03,0x55,0x04,0x03,0x13, +0x1B,0x44,0x69,0x67,0x69,0x43,0x65,0x72,0x74,0x20,0x41,0x73,0x73,0x75,0x72,0x65, +0x64,0x20,0x49,0x44,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x41,0x30,0x82,0x01,0x22, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03, +0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xAD,0x0E,0x15, +0xCE,0xE4,0x43,0x80,0x5C,0xB1,0x87,0xF3,0xB7,0x60,0xF9,0x71,0x12,0xA5,0xAE,0xDC, +0x26,0x94,0x88,0xAA,0xF4,0xCE,0xF5,0x20,0x39,0x28,0x58,0x60,0x0C,0xF8,0x80,0xDA, +0xA9,0x15,0x95,0x32,0x61,0x3C,0xB5,0xB1,0x28,0x84,0x8A,0x8A,0xDC,0x9F,0x0A,0x0C, +0x83,0x17,0x7A,0x8F,0x90,0xAC,0x8A,0xE7,0x79,0x53,0x5C,0x31,0x84,0x2A,0xF6,0x0F, +0x98,0x32,0x36,0x76,0xCC,0xDE,0xDD,0x3C,0xA8,0xA2,0xEF,0x6A,0xFB,0x21,0xF2,0x52, +0x61,0xDF,0x9F,0x20,0xD7,0x1F,0xE2,0xB1,0xD9,0xFE,0x18,0x64,0xD2,0x12,0x5B,0x5F, +0xF9,0x58,0x18,0x35,0xBC,0x47,0xCD,0xA1,0x36,0xF9,0x6B,0x7F,0xD4,0xB0,0x38,0x3E, +0xC1,0x1B,0xC3,0x8C,0x33,0xD9,0xD8,0x2F,0x18,0xFE,0x28,0x0F,0xB3,0xA7,0x83,0xD6, +0xC3,0x6E,0x44,0xC0,0x61,0x35,0x96,0x16,0xFE,0x59,0x9C,0x8B,0x76,0x6D,0xD7,0xF1, +0xA2,0x4B,0x0D,0x2B,0xFF,0x0B,0x72,0xDA,0x9E,0x60,0xD0,0x8E,0x90,0x35,0xC6,0x78, +0x55,0x87,0x20,0xA1,0xCF,0xE5,0x6D,0x0A,0xC8,0x49,0x7C,0x31,0x98,0x33,0x6C,0x22, +0xE9,0x87,0xD0,0x32,0x5A,0xA2,0xBA,0x13,0x82,0x11,0xED,0x39,0x17,0x9D,0x99,0x3A, +0x72,0xA1,0xE6,0xFA,0xA4,0xD9,0xD5,0x17,0x31,0x75,0xAE,0x85,0x7D,0x22,0xAE,0x3F, +0x01,0x46,0x86,0xF6,0x28,0x79,0xC8,0xB1,0xDA,0xE4,0x57,0x17,0xC4,0x7E,0x1C,0x0E, +0xB0,0xB4,0x92,0xA6,0x56,0xB3,0xBD,0xB2,0x97,0xED,0xAA,0xA7,0xF0,0xB7,0xC5,0xA8, +0x3F,0x95,0x16,0xD0,0xFF,0xA1,0x96,0xEB,0x08,0x5F,0x18,0x77,0x4F,0x02,0x03,0x01, +0x00,0x01,0xA3,0x63,0x30,0x61,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF, +0x04,0x04,0x03,0x02,0x01,0x86,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF, +0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16, +0x04,0x14,0x45,0xEB,0xA2,0xAF,0xF4,0x92,0xCB,0x82,0x31,0x2D,0x51,0x8B,0xA7,0xA7, +0x21,0x9D,0xF3,0x6D,0xC8,0x0F,0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18,0x30, +0x16,0x80,0x14,0x45,0xEB,0xA2,0xAF,0xF4,0x92,0xCB,0x82,0x31,0x2D,0x51,0x8B,0xA7, +0xA7,0x21,0x9D,0xF3,0x6D,0xC8,0x0F,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7, +0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0xA2,0x0E,0xBC,0xDF,0xE2, +0xED,0xF0,0xE3,0x72,0x73,0x7A,0x64,0x94,0xBF,0xF7,0x72,0x66,0xD8,0x32,0xE4,0x42, +0x75,0x62,0xAE,0x87,0xEB,0xF2,0xD5,0xD9,0xDE,0x56,0xB3,0x9F,0xCC,0xCE,0x14,0x28, +0xB9,0x0D,0x97,0x60,0x5C,0x12,0x4C,0x58,0xE4,0xD3,0x3D,0x83,0x49,0x45,0x58,0x97, +0x35,0x69,0x1A,0xA8,0x47,0xEA,0x56,0xC6,0x79,0xAB,0x12,0xD8,0x67,0x81,0x84,0xDF, +0x7F,0x09,0x3C,0x94,0xE6,0xB8,0x26,0x2C,0x20,0xBD,0x3D,0xB3,0x28,0x89,0xF7,0x5F, +0xFF,0x22,0xE2,0x97,0x84,0x1F,0xE9,0x65,0xEF,0x87,0xE0,0xDF,0xC1,0x67,0x49,0xB3, +0x5D,0xEB,0xB2,0x09,0x2A,0xEB,0x26,0xED,0x78,0xBE,0x7D,0x3F,0x2B,0xF3,0xB7,0x26, +0x35,0x6D,0x5F,0x89,0x01,0xB6,0x49,0x5B,0x9F,0x01,0x05,0x9B,0xAB,0x3D,0x25,0xC1, +0xCC,0xB6,0x7F,0xC2,0xF1,0x6F,0x86,0xC6,0xFA,0x64,0x68,0xEB,0x81,0x2D,0x94,0xEB, +0x42,0xB7,0xFA,0x8C,0x1E,0xDD,0x62,0xF1,0xBE,0x50,0x67,0xB7,0x6C,0xBD,0xF3,0xF1, +0x1F,0x6B,0x0C,0x36,0x07,0x16,0x7F,0x37,0x7C,0xA9,0x5B,0x6D,0x7A,0xF1,0x12,0x46, +0x60,0x83,0xD7,0x27,0x04,0xBE,0x4B,0xCE,0x97,0xBE,0xC3,0x67,0x2A,0x68,0x11,0xDF, +0x80,0xE7,0x0C,0x33,0x66,0xBF,0x13,0x0D,0x14,0x6E,0xF3,0x7F,0x1F,0x63,0x10,0x1E, +0xFA,0x8D,0x1B,0x25,0x6D,0x6C,0x8F,0xA5,0xB7,0x61,0x01,0xB1,0xD2,0xA3,0x26,0xA1, +0x10,0x71,0x9D,0xAD,0xE2,0xC3,0xF9,0xC3,0x99,0x51,0xB7,0x2B,0x07,0x08,0xCE,0x2E, +0xE6,0x50,0xB2,0xA7,0xFA,0x0A,0x45,0x2F,0xA2,0xF0,0xF2, +}; + + +/* subject:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Global Root CA */ +/* issuer :/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Global Root CA */ + + +const unsigned char DigiCert_Global_Root_CA_certificate[947]={ +0x30,0x82,0x03,0xAF,0x30,0x82,0x02,0x97,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x08, +0x3B,0xE0,0x56,0x90,0x42,0x46,0xB1,0xA1,0x75,0x6A,0xC9,0x59,0x91,0xC7,0x4A,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x61, +0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x15,0x30, +0x13,0x06,0x03,0x55,0x04,0x0A,0x13,0x0C,0x44,0x69,0x67,0x69,0x43,0x65,0x72,0x74, +0x20,0x49,0x6E,0x63,0x31,0x19,0x30,0x17,0x06,0x03,0x55,0x04,0x0B,0x13,0x10,0x77, +0x77,0x77,0x2E,0x64,0x69,0x67,0x69,0x63,0x65,0x72,0x74,0x2E,0x63,0x6F,0x6D,0x31, +0x20,0x30,0x1E,0x06,0x03,0x55,0x04,0x03,0x13,0x17,0x44,0x69,0x67,0x69,0x43,0x65, +0x72,0x74,0x20,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43, +0x41,0x30,0x1E,0x17,0x0D,0x30,0x36,0x31,0x31,0x31,0x30,0x30,0x30,0x30,0x30,0x30, +0x30,0x5A,0x17,0x0D,0x33,0x31,0x31,0x31,0x31,0x30,0x30,0x30,0x30,0x30,0x30,0x30, +0x5A,0x30,0x61,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53, +0x31,0x15,0x30,0x13,0x06,0x03,0x55,0x04,0x0A,0x13,0x0C,0x44,0x69,0x67,0x69,0x43, +0x65,0x72,0x74,0x20,0x49,0x6E,0x63,0x31,0x19,0x30,0x17,0x06,0x03,0x55,0x04,0x0B, +0x13,0x10,0x77,0x77,0x77,0x2E,0x64,0x69,0x67,0x69,0x63,0x65,0x72,0x74,0x2E,0x63, +0x6F,0x6D,0x31,0x20,0x30,0x1E,0x06,0x03,0x55,0x04,0x03,0x13,0x17,0x44,0x69,0x67, +0x69,0x43,0x65,0x72,0x74,0x20,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x20,0x52,0x6F,0x6F, +0x74,0x20,0x43,0x41,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A, +0x02,0x82,0x01,0x01,0x00,0xE2,0x3B,0xE1,0x11,0x72,0xDE,0xA8,0xA4,0xD3,0xA3,0x57, +0xAA,0x50,0xA2,0x8F,0x0B,0x77,0x90,0xC9,0xA2,0xA5,0xEE,0x12,0xCE,0x96,0x5B,0x01, +0x09,0x20,0xCC,0x01,0x93,0xA7,0x4E,0x30,0xB7,0x53,0xF7,0x43,0xC4,0x69,0x00,0x57, +0x9D,0xE2,0x8D,0x22,0xDD,0x87,0x06,0x40,0x00,0x81,0x09,0xCE,0xCE,0x1B,0x83,0xBF, +0xDF,0xCD,0x3B,0x71,0x46,0xE2,0xD6,0x66,0xC7,0x05,0xB3,0x76,0x27,0x16,0x8F,0x7B, +0x9E,0x1E,0x95,0x7D,0xEE,0xB7,0x48,0xA3,0x08,0xDA,0xD6,0xAF,0x7A,0x0C,0x39,0x06, +0x65,0x7F,0x4A,0x5D,0x1F,0xBC,0x17,0xF8,0xAB,0xBE,0xEE,0x28,0xD7,0x74,0x7F,0x7A, +0x78,0x99,0x59,0x85,0x68,0x6E,0x5C,0x23,0x32,0x4B,0xBF,0x4E,0xC0,0xE8,0x5A,0x6D, +0xE3,0x70,0xBF,0x77,0x10,0xBF,0xFC,0x01,0xF6,0x85,0xD9,0xA8,0x44,0x10,0x58,0x32, +0xA9,0x75,0x18,0xD5,0xD1,0xA2,0xBE,0x47,0xE2,0x27,0x6A,0xF4,0x9A,0x33,0xF8,0x49, +0x08,0x60,0x8B,0xD4,0x5F,0xB4,0x3A,0x84,0xBF,0xA1,0xAA,0x4A,0x4C,0x7D,0x3E,0xCF, +0x4F,0x5F,0x6C,0x76,0x5E,0xA0,0x4B,0x37,0x91,0x9E,0xDC,0x22,0xE6,0x6D,0xCE,0x14, +0x1A,0x8E,0x6A,0xCB,0xFE,0xCD,0xB3,0x14,0x64,0x17,0xC7,0x5B,0x29,0x9E,0x32,0xBF, +0xF2,0xEE,0xFA,0xD3,0x0B,0x42,0xD4,0xAB,0xB7,0x41,0x32,0xDA,0x0C,0xD4,0xEF,0xF8, +0x81,0xD5,0xBB,0x8D,0x58,0x3F,0xB5,0x1B,0xE8,0x49,0x28,0xA2,0x70,0xDA,0x31,0x04, +0xDD,0xF7,0xB2,0x16,0xF2,0x4C,0x0A,0x4E,0x07,0xA8,0xED,0x4A,0x3D,0x5E,0xB5,0x7F, +0xA3,0x90,0xC3,0xAF,0x27,0x02,0x03,0x01,0x00,0x01,0xA3,0x63,0x30,0x61,0x30,0x0E, +0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x86,0x30,0x0F, +0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30, +0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x03,0xDE,0x50,0x35,0x56,0xD1, +0x4C,0xBB,0x66,0xF0,0xA3,0xE2,0x1B,0x1B,0xC3,0x97,0xB2,0x3D,0xD1,0x55,0x30,0x1F, +0x06,0x03,0x55,0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14,0x03,0xDE,0x50,0x35,0x56, +0xD1,0x4C,0xBB,0x66,0xF0,0xA3,0xE2,0x1B,0x1B,0xC3,0x97,0xB2,0x3D,0xD1,0x55,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82, +0x01,0x01,0x00,0xCB,0x9C,0x37,0xAA,0x48,0x13,0x12,0x0A,0xFA,0xDD,0x44,0x9C,0x4F, +0x52,0xB0,0xF4,0xDF,0xAE,0x04,0xF5,0x79,0x79,0x08,0xA3,0x24,0x18,0xFC,0x4B,0x2B, +0x84,0xC0,0x2D,0xB9,0xD5,0xC7,0xFE,0xF4,0xC1,0x1F,0x58,0xCB,0xB8,0x6D,0x9C,0x7A, +0x74,0xE7,0x98,0x29,0xAB,0x11,0xB5,0xE3,0x70,0xA0,0xA1,0xCD,0x4C,0x88,0x99,0x93, +0x8C,0x91,0x70,0xE2,0xAB,0x0F,0x1C,0xBE,0x93,0xA9,0xFF,0x63,0xD5,0xE4,0x07,0x60, +0xD3,0xA3,0xBF,0x9D,0x5B,0x09,0xF1,0xD5,0x8E,0xE3,0x53,0xF4,0x8E,0x63,0xFA,0x3F, +0xA7,0xDB,0xB4,0x66,0xDF,0x62,0x66,0xD6,0xD1,0x6E,0x41,0x8D,0xF2,0x2D,0xB5,0xEA, +0x77,0x4A,0x9F,0x9D,0x58,0xE2,0x2B,0x59,0xC0,0x40,0x23,0xED,0x2D,0x28,0x82,0x45, +0x3E,0x79,0x54,0x92,0x26,0x98,0xE0,0x80,0x48,0xA8,0x37,0xEF,0xF0,0xD6,0x79,0x60, +0x16,0xDE,0xAC,0xE8,0x0E,0xCD,0x6E,0xAC,0x44,0x17,0x38,0x2F,0x49,0xDA,0xE1,0x45, +0x3E,0x2A,0xB9,0x36,0x53,0xCF,0x3A,0x50,0x06,0xF7,0x2E,0xE8,0xC4,0x57,0x49,0x6C, +0x61,0x21,0x18,0xD5,0x04,0xAD,0x78,0x3C,0x2C,0x3A,0x80,0x6B,0xA7,0xEB,0xAF,0x15, +0x14,0xE9,0xD8,0x89,0xC1,0xB9,0x38,0x6C,0xE2,0x91,0x6C,0x8A,0xFF,0x64,0xB9,0x77, +0x25,0x57,0x30,0xC0,0x1B,0x24,0xA3,0xE1,0xDC,0xE9,0xDF,0x47,0x7C,0xB5,0xB4,0x24, +0x08,0x05,0x30,0xEC,0x2D,0xBD,0x0B,0xBF,0x45,0xBF,0x50,0xB9,0xA9,0xF3,0xEB,0x98, +0x01,0x12,0xAD,0xC8,0x88,0xC6,0x98,0x34,0x5F,0x8D,0x0A,0x3C,0xC6,0xE9,0xD5,0x95, +0x95,0x6D,0xDE, +}; + + +/* subject:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert High Assurance EV Root CA */ +/* issuer :/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert High Assurance EV Root CA */ + + +const unsigned char DigiCert_High_Assurance_EV_Root_CA_certificate[969]={ +0x30,0x82,0x03,0xC5,0x30,0x82,0x02,0xAD,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x02, +0xAC,0x5C,0x26,0x6A,0x0B,0x40,0x9B,0x8F,0x0B,0x79,0xF2,0xAE,0x46,0x25,0x77,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x6C, +0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x15,0x30, +0x13,0x06,0x03,0x55,0x04,0x0A,0x13,0x0C,0x44,0x69,0x67,0x69,0x43,0x65,0x72,0x74, +0x20,0x49,0x6E,0x63,0x31,0x19,0x30,0x17,0x06,0x03,0x55,0x04,0x0B,0x13,0x10,0x77, +0x77,0x77,0x2E,0x64,0x69,0x67,0x69,0x63,0x65,0x72,0x74,0x2E,0x63,0x6F,0x6D,0x31, +0x2B,0x30,0x29,0x06,0x03,0x55,0x04,0x03,0x13,0x22,0x44,0x69,0x67,0x69,0x43,0x65, +0x72,0x74,0x20,0x48,0x69,0x67,0x68,0x20,0x41,0x73,0x73,0x75,0x72,0x61,0x6E,0x63, +0x65,0x20,0x45,0x56,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x41,0x30,0x1E,0x17,0x0D, +0x30,0x36,0x31,0x31,0x31,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33, +0x31,0x31,0x31,0x31,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x30,0x6C,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x15,0x30,0x13,0x06, +0x03,0x55,0x04,0x0A,0x13,0x0C,0x44,0x69,0x67,0x69,0x43,0x65,0x72,0x74,0x20,0x49, +0x6E,0x63,0x31,0x19,0x30,0x17,0x06,0x03,0x55,0x04,0x0B,0x13,0x10,0x77,0x77,0x77, +0x2E,0x64,0x69,0x67,0x69,0x63,0x65,0x72,0x74,0x2E,0x63,0x6F,0x6D,0x31,0x2B,0x30, +0x29,0x06,0x03,0x55,0x04,0x03,0x13,0x22,0x44,0x69,0x67,0x69,0x43,0x65,0x72,0x74, +0x20,0x48,0x69,0x67,0x68,0x20,0x41,0x73,0x73,0x75,0x72,0x61,0x6E,0x63,0x65,0x20, +0x45,0x56,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x41,0x30,0x82,0x01,0x22,0x30,0x0D, +0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01, +0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xC6,0xCC,0xE5,0x73,0xE6, +0xFB,0xD4,0xBB,0xE5,0x2D,0x2D,0x32,0xA6,0xDF,0xE5,0x81,0x3F,0xC9,0xCD,0x25,0x49, +0xB6,0x71,0x2A,0xC3,0xD5,0x94,0x34,0x67,0xA2,0x0A,0x1C,0xB0,0x5F,0x69,0xA6,0x40, +0xB1,0xC4,0xB7,0xB2,0x8F,0xD0,0x98,0xA4,0xA9,0x41,0x59,0x3A,0xD3,0xDC,0x94,0xD6, +0x3C,0xDB,0x74,0x38,0xA4,0x4A,0xCC,0x4D,0x25,0x82,0xF7,0x4A,0xA5,0x53,0x12,0x38, +0xEE,0xF3,0x49,0x6D,0x71,0x91,0x7E,0x63,0xB6,0xAB,0xA6,0x5F,0xC3,0xA4,0x84,0xF8, +0x4F,0x62,0x51,0xBE,0xF8,0xC5,0xEC,0xDB,0x38,0x92,0xE3,0x06,0xE5,0x08,0x91,0x0C, +0xC4,0x28,0x41,0x55,0xFB,0xCB,0x5A,0x89,0x15,0x7E,0x71,0xE8,0x35,0xBF,0x4D,0x72, +0x09,0x3D,0xBE,0x3A,0x38,0x50,0x5B,0x77,0x31,0x1B,0x8D,0xB3,0xC7,0x24,0x45,0x9A, +0xA7,0xAC,0x6D,0x00,0x14,0x5A,0x04,0xB7,0xBA,0x13,0xEB,0x51,0x0A,0x98,0x41,0x41, +0x22,0x4E,0x65,0x61,0x87,0x81,0x41,0x50,0xA6,0x79,0x5C,0x89,0xDE,0x19,0x4A,0x57, +0xD5,0x2E,0xE6,0x5D,0x1C,0x53,0x2C,0x7E,0x98,0xCD,0x1A,0x06,0x16,0xA4,0x68,0x73, +0xD0,0x34,0x04,0x13,0x5C,0xA1,0x71,0xD3,0x5A,0x7C,0x55,0xDB,0x5E,0x64,0xE1,0x37, +0x87,0x30,0x56,0x04,0xE5,0x11,0xB4,0x29,0x80,0x12,0xF1,0x79,0x39,0x88,0xA2,0x02, +0x11,0x7C,0x27,0x66,0xB7,0x88,0xB7,0x78,0xF2,0xCA,0x0A,0xA8,0x38,0xAB,0x0A,0x64, +0xC2,0xBF,0x66,0x5D,0x95,0x84,0xC1,0xA1,0x25,0x1E,0x87,0x5D,0x1A,0x50,0x0B,0x20, +0x12,0xCC,0x41,0xBB,0x6E,0x0B,0x51,0x38,0xB8,0x4B,0xCB,0x02,0x03,0x01,0x00,0x01, +0xA3,0x63,0x30,0x61,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04, +0x03,0x02,0x01,0x86,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05, +0x30,0x03,0x01,0x01,0xFF,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14, +0xB1,0x3E,0xC3,0x69,0x03,0xF8,0xBF,0x47,0x01,0xD4,0x98,0x26,0x1A,0x08,0x02,0xEF, +0x63,0x64,0x2B,0xC3,0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18,0x30,0x16,0x80, +0x14,0xB1,0x3E,0xC3,0x69,0x03,0xF8,0xBF,0x47,0x01,0xD4,0x98,0x26,0x1A,0x08,0x02, +0xEF,0x63,0x64,0x2B,0xC3,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01, +0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x1C,0x1A,0x06,0x97,0xDC,0xD7,0x9C, +0x9F,0x3C,0x88,0x66,0x06,0x08,0x57,0x21,0xDB,0x21,0x47,0xF8,0x2A,0x67,0xAA,0xBF, +0x18,0x32,0x76,0x40,0x10,0x57,0xC1,0x8A,0xF3,0x7A,0xD9,0x11,0x65,0x8E,0x35,0xFA, +0x9E,0xFC,0x45,0xB5,0x9E,0xD9,0x4C,0x31,0x4B,0xB8,0x91,0xE8,0x43,0x2C,0x8E,0xB3, +0x78,0xCE,0xDB,0xE3,0x53,0x79,0x71,0xD6,0xE5,0x21,0x94,0x01,0xDA,0x55,0x87,0x9A, +0x24,0x64,0xF6,0x8A,0x66,0xCC,0xDE,0x9C,0x37,0xCD,0xA8,0x34,0xB1,0x69,0x9B,0x23, +0xC8,0x9E,0x78,0x22,0x2B,0x70,0x43,0xE3,0x55,0x47,0x31,0x61,0x19,0xEF,0x58,0xC5, +0x85,0x2F,0x4E,0x30,0xF6,0xA0,0x31,0x16,0x23,0xC8,0xE7,0xE2,0x65,0x16,0x33,0xCB, +0xBF,0x1A,0x1B,0xA0,0x3D,0xF8,0xCA,0x5E,0x8B,0x31,0x8B,0x60,0x08,0x89,0x2D,0x0C, +0x06,0x5C,0x52,0xB7,0xC4,0xF9,0x0A,0x98,0xD1,0x15,0x5F,0x9F,0x12,0xBE,0x7C,0x36, +0x63,0x38,0xBD,0x44,0xA4,0x7F,0xE4,0x26,0x2B,0x0A,0xC4,0x97,0x69,0x0D,0xE9,0x8C, +0xE2,0xC0,0x10,0x57,0xB8,0xC8,0x76,0x12,0x91,0x55,0xF2,0x48,0x69,0xD8,0xBC,0x2A, +0x02,0x5B,0x0F,0x44,0xD4,0x20,0x31,0xDB,0xF4,0xBA,0x70,0x26,0x5D,0x90,0x60,0x9E, +0xBC,0x4B,0x17,0x09,0x2F,0xB4,0xCB,0x1E,0x43,0x68,0xC9,0x07,0x27,0xC1,0xD2,0x5C, +0xF7,0xEA,0x21,0xB9,0x68,0x12,0x9C,0x3C,0x9C,0xBF,0x9E,0xFC,0x80,0x5C,0x9B,0x63, +0xCD,0xEC,0x47,0xAA,0x25,0x27,0x67,0xA0,0x37,0xF3,0x00,0x82,0x7D,0x54,0xD7,0xA9, +0xF8,0xE9,0x2E,0x13,0xA3,0x77,0xE8,0x1F,0x4A, +}; + + +/* subject:/O=Entrust.net/OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/OU=(c) 1999 Entrust.net Limited/CN=Entrust.net Certification Authority (2048) */ +/* issuer :/O=Entrust.net/OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/OU=(c) 1999 Entrust.net Limited/CN=Entrust.net Certification Authority (2048) */ + + +const unsigned char Entrust_net_Premium_2048_Secure_Server_CA_certificate[1120]={ +0x30,0x82,0x04,0x5C,0x30,0x82,0x03,0x44,0xA0,0x03,0x02,0x01,0x02,0x02,0x04,0x38, +0x63,0xB9,0x66,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05, +0x05,0x00,0x30,0x81,0xB4,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x13,0x0B, +0x45,0x6E,0x74,0x72,0x75,0x73,0x74,0x2E,0x6E,0x65,0x74,0x31,0x40,0x30,0x3E,0x06, +0x03,0x55,0x04,0x0B,0x14,0x37,0x77,0x77,0x77,0x2E,0x65,0x6E,0x74,0x72,0x75,0x73, +0x74,0x2E,0x6E,0x65,0x74,0x2F,0x43,0x50,0x53,0x5F,0x32,0x30,0x34,0x38,0x20,0x69, +0x6E,0x63,0x6F,0x72,0x70,0x2E,0x20,0x62,0x79,0x20,0x72,0x65,0x66,0x2E,0x20,0x28, +0x6C,0x69,0x6D,0x69,0x74,0x73,0x20,0x6C,0x69,0x61,0x62,0x2E,0x29,0x31,0x25,0x30, +0x23,0x06,0x03,0x55,0x04,0x0B,0x13,0x1C,0x28,0x63,0x29,0x20,0x31,0x39,0x39,0x39, +0x20,0x45,0x6E,0x74,0x72,0x75,0x73,0x74,0x2E,0x6E,0x65,0x74,0x20,0x4C,0x69,0x6D, +0x69,0x74,0x65,0x64,0x31,0x33,0x30,0x31,0x06,0x03,0x55,0x04,0x03,0x13,0x2A,0x45, +0x6E,0x74,0x72,0x75,0x73,0x74,0x2E,0x6E,0x65,0x74,0x20,0x43,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69, +0x74,0x79,0x20,0x28,0x32,0x30,0x34,0x38,0x29,0x30,0x1E,0x17,0x0D,0x39,0x39,0x31, +0x32,0x32,0x34,0x31,0x37,0x35,0x30,0x35,0x31,0x5A,0x17,0x0D,0x31,0x39,0x31,0x32, +0x32,0x34,0x31,0x38,0x32,0x30,0x35,0x31,0x5A,0x30,0x81,0xB4,0x31,0x14,0x30,0x12, +0x06,0x03,0x55,0x04,0x0A,0x13,0x0B,0x45,0x6E,0x74,0x72,0x75,0x73,0x74,0x2E,0x6E, +0x65,0x74,0x31,0x40,0x30,0x3E,0x06,0x03,0x55,0x04,0x0B,0x14,0x37,0x77,0x77,0x77, +0x2E,0x65,0x6E,0x74,0x72,0x75,0x73,0x74,0x2E,0x6E,0x65,0x74,0x2F,0x43,0x50,0x53, +0x5F,0x32,0x30,0x34,0x38,0x20,0x69,0x6E,0x63,0x6F,0x72,0x70,0x2E,0x20,0x62,0x79, +0x20,0x72,0x65,0x66,0x2E,0x20,0x28,0x6C,0x69,0x6D,0x69,0x74,0x73,0x20,0x6C,0x69, +0x61,0x62,0x2E,0x29,0x31,0x25,0x30,0x23,0x06,0x03,0x55,0x04,0x0B,0x13,0x1C,0x28, +0x63,0x29,0x20,0x31,0x39,0x39,0x39,0x20,0x45,0x6E,0x74,0x72,0x75,0x73,0x74,0x2E, +0x6E,0x65,0x74,0x20,0x4C,0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x33,0x30,0x31,0x06, +0x03,0x55,0x04,0x03,0x13,0x2A,0x45,0x6E,0x74,0x72,0x75,0x73,0x74,0x2E,0x6E,0x65, +0x74,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20, +0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x20,0x28,0x32,0x30,0x34,0x38,0x29, +0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01, +0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01, +0x00,0xAD,0x4D,0x4B,0xA9,0x12,0x86,0xB2,0xEA,0xA3,0x20,0x07,0x15,0x16,0x64,0x2A, +0x2B,0x4B,0xD1,0xBF,0x0B,0x4A,0x4D,0x8E,0xED,0x80,0x76,0xA5,0x67,0xB7,0x78,0x40, +0xC0,0x73,0x42,0xC8,0x68,0xC0,0xDB,0x53,0x2B,0xDD,0x5E,0xB8,0x76,0x98,0x35,0x93, +0x8B,0x1A,0x9D,0x7C,0x13,0x3A,0x0E,0x1F,0x5B,0xB7,0x1E,0xCF,0xE5,0x24,0x14,0x1E, +0xB1,0x81,0xA9,0x8D,0x7D,0xB8,0xCC,0x6B,0x4B,0x03,0xF1,0x02,0x0C,0xDC,0xAB,0xA5, +0x40,0x24,0x00,0x7F,0x74,0x94,0xA1,0x9D,0x08,0x29,0xB3,0x88,0x0B,0xF5,0x87,0x77, +0x9D,0x55,0xCD,0xE4,0xC3,0x7E,0xD7,0x6A,0x64,0xAB,0x85,0x14,0x86,0x95,0x5B,0x97, +0x32,0x50,0x6F,0x3D,0xC8,0xBA,0x66,0x0C,0xE3,0xFC,0xBD,0xB8,0x49,0xC1,0x76,0x89, +0x49,0x19,0xFD,0xC0,0xA8,0xBD,0x89,0xA3,0x67,0x2F,0xC6,0x9F,0xBC,0x71,0x19,0x60, +0xB8,0x2D,0xE9,0x2C,0xC9,0x90,0x76,0x66,0x7B,0x94,0xE2,0xAF,0x78,0xD6,0x65,0x53, +0x5D,0x3C,0xD6,0x9C,0xB2,0xCF,0x29,0x03,0xF9,0x2F,0xA4,0x50,0xB2,0xD4,0x48,0xCE, +0x05,0x32,0x55,0x8A,0xFD,0xB2,0x64,0x4C,0x0E,0xE4,0x98,0x07,0x75,0xDB,0x7F,0xDF, +0xB9,0x08,0x55,0x60,0x85,0x30,0x29,0xF9,0x7B,0x48,0xA4,0x69,0x86,0xE3,0x35,0x3F, +0x1E,0x86,0x5D,0x7A,0x7A,0x15,0xBD,0xEF,0x00,0x8E,0x15,0x22,0x54,0x17,0x00,0x90, +0x26,0x93,0xBC,0x0E,0x49,0x68,0x91,0xBF,0xF8,0x47,0xD3,0x9D,0x95,0x42,0xC1,0x0E, +0x4D,0xDF,0x6F,0x26,0xCF,0xC3,0x18,0x21,0x62,0x66,0x43,0x70,0xD6,0xD5,0xC0,0x07, +0xE1,0x02,0x03,0x01,0x00,0x01,0xA3,0x74,0x30,0x72,0x30,0x11,0x06,0x09,0x60,0x86, +0x48,0x01,0x86,0xF8,0x42,0x01,0x01,0x04,0x04,0x03,0x02,0x00,0x07,0x30,0x1F,0x06, +0x03,0x55,0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14,0x55,0xE4,0x81,0xD1,0x11,0x80, +0xBE,0xD8,0x89,0xB9,0x08,0xA3,0x31,0xF9,0xA1,0x24,0x09,0x16,0xB9,0x70,0x30,0x1D, +0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x55,0xE4,0x81,0xD1,0x11,0x80,0xBE, +0xD8,0x89,0xB9,0x08,0xA3,0x31,0xF9,0xA1,0x24,0x09,0x16,0xB9,0x70,0x30,0x1D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF6,0x7D,0x07,0x41,0x00,0x04,0x10,0x30,0x0E,0x1B,0x08, +0x56,0x35,0x2E,0x30,0x3A,0x34,0x2E,0x30,0x03,0x02,0x04,0x90,0x30,0x0D,0x06,0x09, +0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00, +0x59,0x47,0xAC,0x21,0x84,0x8A,0x17,0xC9,0x9C,0x89,0x53,0x1E,0xBA,0x80,0x85,0x1A, +0xC6,0x3C,0x4E,0x3E,0xB1,0x9C,0xB6,0x7C,0xC6,0x92,0x5D,0x18,0x64,0x02,0xE3,0xD3, +0x06,0x08,0x11,0x61,0x7C,0x63,0xE3,0x2B,0x9D,0x31,0x03,0x70,0x76,0xD2,0xA3,0x28, +0xA0,0xF4,0xBB,0x9A,0x63,0x73,0xED,0x6D,0xE5,0x2A,0xDB,0xED,0x14,0xA9,0x2B,0xC6, +0x36,0x11,0xD0,0x2B,0xEB,0x07,0x8B,0xA5,0xDA,0x9E,0x5C,0x19,0x9D,0x56,0x12,0xF5, +0x54,0x29,0xC8,0x05,0xED,0xB2,0x12,0x2A,0x8D,0xF4,0x03,0x1B,0xFF,0xE7,0x92,0x10, +0x87,0xB0,0x3A,0xB5,0xC3,0x9D,0x05,0x37,0x12,0xA3,0xC7,0xF4,0x15,0xB9,0xD5,0xA4, +0x39,0x16,0x9B,0x53,0x3A,0x23,0x91,0xF1,0xA8,0x82,0xA2,0x6A,0x88,0x68,0xC1,0x79, +0x02,0x22,0xBC,0xAA,0xA6,0xD6,0xAE,0xDF,0xB0,0x14,0x5F,0xB8,0x87,0xD0,0xDD,0x7C, +0x7F,0x7B,0xFF,0xAF,0x1C,0xCF,0xE6,0xDB,0x07,0xAD,0x5E,0xDB,0x85,0x9D,0xD0,0x2B, +0x0D,0x33,0xDB,0x04,0xD1,0xE6,0x49,0x40,0x13,0x2B,0x76,0xFB,0x3E,0xE9,0x9C,0x89, +0x0F,0x15,0xCE,0x18,0xB0,0x85,0x78,0x21,0x4F,0x6B,0x4F,0x0E,0xFA,0x36,0x67,0xCD, +0x07,0xF2,0xFF,0x08,0xD0,0xE2,0xDE,0xD9,0xBF,0x2A,0xAF,0xB8,0x87,0x86,0x21,0x3C, +0x04,0xCA,0xB7,0x94,0x68,0x7F,0xCF,0x3C,0xE9,0x98,0xD7,0x38,0xFF,0xEC,0xC0,0xD9, +0x50,0xF0,0x2E,0x4B,0x58,0xAE,0x46,0x6F,0xD0,0x2E,0xC3,0x60,0xDA,0x72,0x55,0x72, +0xBD,0x4C,0x45,0x9E,0x61,0xBA,0xBF,0x84,0x81,0x92,0x03,0xD1,0xD2,0x69,0x7C,0xC5, +}; + + +/* subject:/C=US/O=Entrust.net/OU=www.entrust.net/CPS incorp. by ref. (limits liab.)/OU=(c) 1999 Entrust.net Limited/CN=Entrust.net Secure Server Certification Authority */ +/* issuer :/C=US/O=Entrust.net/OU=www.entrust.net/CPS incorp. by ref. (limits liab.)/OU=(c) 1999 Entrust.net Limited/CN=Entrust.net Secure Server Certification Authority */ + + +const unsigned char Entrust_net_Secure_Server_CA_certificate[1244]={ +0x30,0x82,0x04,0xD8,0x30,0x82,0x04,0x41,0xA0,0x03,0x02,0x01,0x02,0x02,0x04,0x37, +0x4A,0xD2,0x43,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05, +0x05,0x00,0x30,0x81,0xC3,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02, +0x55,0x53,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x13,0x0B,0x45,0x6E,0x74, +0x72,0x75,0x73,0x74,0x2E,0x6E,0x65,0x74,0x31,0x3B,0x30,0x39,0x06,0x03,0x55,0x04, +0x0B,0x13,0x32,0x77,0x77,0x77,0x2E,0x65,0x6E,0x74,0x72,0x75,0x73,0x74,0x2E,0x6E, +0x65,0x74,0x2F,0x43,0x50,0x53,0x20,0x69,0x6E,0x63,0x6F,0x72,0x70,0x2E,0x20,0x62, +0x79,0x20,0x72,0x65,0x66,0x2E,0x20,0x28,0x6C,0x69,0x6D,0x69,0x74,0x73,0x20,0x6C, +0x69,0x61,0x62,0x2E,0x29,0x31,0x25,0x30,0x23,0x06,0x03,0x55,0x04,0x0B,0x13,0x1C, +0x28,0x63,0x29,0x20,0x31,0x39,0x39,0x39,0x20,0x45,0x6E,0x74,0x72,0x75,0x73,0x74, +0x2E,0x6E,0x65,0x74,0x20,0x4C,0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x3A,0x30,0x38, +0x06,0x03,0x55,0x04,0x03,0x13,0x31,0x45,0x6E,0x74,0x72,0x75,0x73,0x74,0x2E,0x6E, +0x65,0x74,0x20,0x53,0x65,0x63,0x75,0x72,0x65,0x20,0x53,0x65,0x72,0x76,0x65,0x72, +0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41, +0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x1E,0x17,0x0D,0x39,0x39,0x30,0x35, +0x32,0x35,0x31,0x36,0x30,0x39,0x34,0x30,0x5A,0x17,0x0D,0x31,0x39,0x30,0x35,0x32, +0x35,0x31,0x36,0x33,0x39,0x34,0x30,0x5A,0x30,0x81,0xC3,0x31,0x0B,0x30,0x09,0x06, +0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04, +0x0A,0x13,0x0B,0x45,0x6E,0x74,0x72,0x75,0x73,0x74,0x2E,0x6E,0x65,0x74,0x31,0x3B, +0x30,0x39,0x06,0x03,0x55,0x04,0x0B,0x13,0x32,0x77,0x77,0x77,0x2E,0x65,0x6E,0x74, +0x72,0x75,0x73,0x74,0x2E,0x6E,0x65,0x74,0x2F,0x43,0x50,0x53,0x20,0x69,0x6E,0x63, +0x6F,0x72,0x70,0x2E,0x20,0x62,0x79,0x20,0x72,0x65,0x66,0x2E,0x20,0x28,0x6C,0x69, +0x6D,0x69,0x74,0x73,0x20,0x6C,0x69,0x61,0x62,0x2E,0x29,0x31,0x25,0x30,0x23,0x06, +0x03,0x55,0x04,0x0B,0x13,0x1C,0x28,0x63,0x29,0x20,0x31,0x39,0x39,0x39,0x20,0x45, +0x6E,0x74,0x72,0x75,0x73,0x74,0x2E,0x6E,0x65,0x74,0x20,0x4C,0x69,0x6D,0x69,0x74, +0x65,0x64,0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04,0x03,0x13,0x31,0x45,0x6E,0x74, +0x72,0x75,0x73,0x74,0x2E,0x6E,0x65,0x74,0x20,0x53,0x65,0x63,0x75,0x72,0x65,0x20, +0x53,0x65,0x72,0x76,0x65,0x72,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61, +0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x81, +0x9D,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00, +0x03,0x81,0x8B,0x00,0x30,0x81,0x87,0x02,0x81,0x81,0x00,0xCD,0x28,0x83,0x34,0x54, +0x1B,0x89,0xF3,0x0F,0xAF,0x37,0x91,0x31,0xFF,0xAF,0x31,0x60,0xC9,0xA8,0xE8,0xB2, +0x10,0x68,0xED,0x9F,0xE7,0x93,0x36,0xF1,0x0A,0x64,0xBB,0x47,0xF5,0x04,0x17,0x3F, +0x23,0x47,0x4D,0xC5,0x27,0x19,0x81,0x26,0x0C,0x54,0x72,0x0D,0x88,0x2D,0xD9,0x1F, +0x9A,0x12,0x9F,0xBC,0xB3,0x71,0xD3,0x80,0x19,0x3F,0x47,0x66,0x7B,0x8C,0x35,0x28, +0xD2,0xB9,0x0A,0xDF,0x24,0xDA,0x9C,0xD6,0x50,0x79,0x81,0x7A,0x5A,0xD3,0x37,0xF7, +0xC2,0x4A,0xD8,0x29,0x92,0x26,0x64,0xD1,0xE4,0x98,0x6C,0x3A,0x00,0x8A,0xF5,0x34, +0x9B,0x65,0xF8,0xED,0xE3,0x10,0xFF,0xFD,0xB8,0x49,0x58,0xDC,0xA0,0xDE,0x82,0x39, +0x6B,0x81,0xB1,0x16,0x19,0x61,0xB9,0x54,0xB6,0xE6,0x43,0x02,0x01,0x03,0xA3,0x82, +0x01,0xD7,0x30,0x82,0x01,0xD3,0x30,0x11,0x06,0x09,0x60,0x86,0x48,0x01,0x86,0xF8, +0x42,0x01,0x01,0x04,0x04,0x03,0x02,0x00,0x07,0x30,0x82,0x01,0x19,0x06,0x03,0x55, +0x1D,0x1F,0x04,0x82,0x01,0x10,0x30,0x82,0x01,0x0C,0x30,0x81,0xDE,0xA0,0x81,0xDB, +0xA0,0x81,0xD8,0xA4,0x81,0xD5,0x30,0x81,0xD2,0x31,0x0B,0x30,0x09,0x06,0x03,0x55, +0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x14,0x30,0x12,0x06,0x03,0x55,0x04,0x0A,0x13, +0x0B,0x45,0x6E,0x74,0x72,0x75,0x73,0x74,0x2E,0x6E,0x65,0x74,0x31,0x3B,0x30,0x39, +0x06,0x03,0x55,0x04,0x0B,0x13,0x32,0x77,0x77,0x77,0x2E,0x65,0x6E,0x74,0x72,0x75, +0x73,0x74,0x2E,0x6E,0x65,0x74,0x2F,0x43,0x50,0x53,0x20,0x69,0x6E,0x63,0x6F,0x72, +0x70,0x2E,0x20,0x62,0x79,0x20,0x72,0x65,0x66,0x2E,0x20,0x28,0x6C,0x69,0x6D,0x69, +0x74,0x73,0x20,0x6C,0x69,0x61,0x62,0x2E,0x29,0x31,0x25,0x30,0x23,0x06,0x03,0x55, +0x04,0x0B,0x13,0x1C,0x28,0x63,0x29,0x20,0x31,0x39,0x39,0x39,0x20,0x45,0x6E,0x74, +0x72,0x75,0x73,0x74,0x2E,0x6E,0x65,0x74,0x20,0x4C,0x69,0x6D,0x69,0x74,0x65,0x64, +0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04,0x03,0x13,0x31,0x45,0x6E,0x74,0x72,0x75, +0x73,0x74,0x2E,0x6E,0x65,0x74,0x20,0x53,0x65,0x63,0x75,0x72,0x65,0x20,0x53,0x65, +0x72,0x76,0x65,0x72,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69, +0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x31,0x0D,0x30,0x0B, +0x06,0x03,0x55,0x04,0x03,0x13,0x04,0x43,0x52,0x4C,0x31,0x30,0x29,0xA0,0x27,0xA0, +0x25,0x86,0x23,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E,0x65,0x6E, +0x74,0x72,0x75,0x73,0x74,0x2E,0x6E,0x65,0x74,0x2F,0x43,0x52,0x4C,0x2F,0x6E,0x65, +0x74,0x31,0x2E,0x63,0x72,0x6C,0x30,0x2B,0x06,0x03,0x55,0x1D,0x10,0x04,0x24,0x30, +0x22,0x80,0x0F,0x31,0x39,0x39,0x39,0x30,0x35,0x32,0x35,0x31,0x36,0x30,0x39,0x34, +0x30,0x5A,0x81,0x0F,0x32,0x30,0x31,0x39,0x30,0x35,0x32,0x35,0x31,0x36,0x30,0x39, +0x34,0x30,0x5A,0x30,0x0B,0x06,0x03,0x55,0x1D,0x0F,0x04,0x04,0x03,0x02,0x01,0x06, +0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14,0xF0,0x17,0x62, +0x13,0x55,0x3D,0xB3,0xFF,0x0A,0x00,0x6B,0xFB,0x50,0x84,0x97,0xF3,0xED,0x62,0xD0, +0x1A,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0xF0,0x17,0x62,0x13, +0x55,0x3D,0xB3,0xFF,0x0A,0x00,0x6B,0xFB,0x50,0x84,0x97,0xF3,0xED,0x62,0xD0,0x1A, +0x30,0x0C,0x06,0x03,0x55,0x1D,0x13,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x19, +0x06,0x09,0x2A,0x86,0x48,0x86,0xF6,0x7D,0x07,0x41,0x00,0x04,0x0C,0x30,0x0A,0x1B, +0x04,0x56,0x34,0x2E,0x30,0x03,0x02,0x04,0x90,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48, +0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x81,0x81,0x00,0x90,0xDC,0x30,0x02, +0xFA,0x64,0x74,0xC2,0xA7,0x0A,0xA5,0x7C,0x21,0x8D,0x34,0x17,0xA8,0xFB,0x47,0x0E, +0xFF,0x25,0x7C,0x8D,0x13,0x0A,0xFB,0xE4,0x98,0xB5,0xEF,0x8C,0xF8,0xC5,0x10,0x0D, +0xF7,0x92,0xBE,0xF1,0xC3,0xD5,0xD5,0x95,0x6A,0x04,0xBB,0x2C,0xCE,0x26,0x36,0x65, +0xC8,0x31,0xC6,0xE7,0xEE,0x3F,0xE3,0x57,0x75,0x84,0x7A,0x11,0xEF,0x46,0x4F,0x18, +0xF4,0xD3,0x98,0xBB,0xA8,0x87,0x32,0xBA,0x72,0xF6,0x3C,0xE2,0x3D,0x9F,0xD7,0x1D, +0xD9,0xC3,0x60,0x43,0x8C,0x58,0x0E,0x22,0x96,0x2F,0x62,0xA3,0x2C,0x1F,0xBA,0xAD, +0x05,0xEF,0xAB,0x32,0x78,0x87,0xA0,0x54,0x73,0x19,0xB5,0x5C,0x05,0xF9,0x52,0x3E, +0x6D,0x2D,0x45,0x0B,0xF7,0x0A,0x93,0xEA,0xED,0x06,0xF9,0xB2, +}; + + +/* subject:/C=US/O=Entrust, Inc./OU=www.entrust.net/CPS is incorporated by reference/OU=(c) 2006 Entrust, Inc./CN=Entrust Root Certification Authority */ +/* issuer :/C=US/O=Entrust, Inc./OU=www.entrust.net/CPS is incorporated by reference/OU=(c) 2006 Entrust, Inc./CN=Entrust Root Certification Authority */ + + +const unsigned char Entrust_Root_Certification_Authority_certificate[1173]={ +0x30,0x82,0x04,0x91,0x30,0x82,0x03,0x79,0xA0,0x03,0x02,0x01,0x02,0x02,0x04,0x45, +0x6B,0x50,0x54,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05, +0x05,0x00,0x30,0x81,0xB0,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02, +0x55,0x53,0x31,0x16,0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x45,0x6E,0x74, +0x72,0x75,0x73,0x74,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x39,0x30,0x37,0x06,0x03, +0x55,0x04,0x0B,0x13,0x30,0x77,0x77,0x77,0x2E,0x65,0x6E,0x74,0x72,0x75,0x73,0x74, +0x2E,0x6E,0x65,0x74,0x2F,0x43,0x50,0x53,0x20,0x69,0x73,0x20,0x69,0x6E,0x63,0x6F, +0x72,0x70,0x6F,0x72,0x61,0x74,0x65,0x64,0x20,0x62,0x79,0x20,0x72,0x65,0x66,0x65, +0x72,0x65,0x6E,0x63,0x65,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x0B,0x13,0x16, +0x28,0x63,0x29,0x20,0x32,0x30,0x30,0x36,0x20,0x45,0x6E,0x74,0x72,0x75,0x73,0x74, +0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x2D,0x30,0x2B,0x06,0x03,0x55,0x04,0x03,0x13, +0x24,0x45,0x6E,0x74,0x72,0x75,0x73,0x74,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x65, +0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68, +0x6F,0x72,0x69,0x74,0x79,0x30,0x1E,0x17,0x0D,0x30,0x36,0x31,0x31,0x32,0x37,0x32, +0x30,0x32,0x33,0x34,0x32,0x5A,0x17,0x0D,0x32,0x36,0x31,0x31,0x32,0x37,0x32,0x30, +0x35,0x33,0x34,0x32,0x5A,0x30,0x81,0xB0,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04, +0x06,0x13,0x02,0x55,0x53,0x31,0x16,0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D, +0x45,0x6E,0x74,0x72,0x75,0x73,0x74,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x39,0x30, +0x37,0x06,0x03,0x55,0x04,0x0B,0x13,0x30,0x77,0x77,0x77,0x2E,0x65,0x6E,0x74,0x72, +0x75,0x73,0x74,0x2E,0x6E,0x65,0x74,0x2F,0x43,0x50,0x53,0x20,0x69,0x73,0x20,0x69, +0x6E,0x63,0x6F,0x72,0x70,0x6F,0x72,0x61,0x74,0x65,0x64,0x20,0x62,0x79,0x20,0x72, +0x65,0x66,0x65,0x72,0x65,0x6E,0x63,0x65,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04, +0x0B,0x13,0x16,0x28,0x63,0x29,0x20,0x32,0x30,0x30,0x36,0x20,0x45,0x6E,0x74,0x72, +0x75,0x73,0x74,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x2D,0x30,0x2B,0x06,0x03,0x55, +0x04,0x03,0x13,0x24,0x45,0x6E,0x74,0x72,0x75,0x73,0x74,0x20,0x52,0x6F,0x6F,0x74, +0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41, +0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09, +0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00, +0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xB6,0x95,0xB6,0x43,0x42,0xFA,0xC6, +0x6D,0x2A,0x6F,0x48,0xDF,0x94,0x4C,0x39,0x57,0x05,0xEE,0xC3,0x79,0x11,0x41,0x68, +0x36,0xED,0xEC,0xFE,0x9A,0x01,0x8F,0xA1,0x38,0x28,0xFC,0xF7,0x10,0x46,0x66,0x2E, +0x4D,0x1E,0x1A,0xB1,0x1A,0x4E,0xC6,0xD1,0xC0,0x95,0x88,0xB0,0xC9,0xFF,0x31,0x8B, +0x33,0x03,0xDB,0xB7,0x83,0x7B,0x3E,0x20,0x84,0x5E,0xED,0xB2,0x56,0x28,0xA7,0xF8, +0xE0,0xB9,0x40,0x71,0x37,0xC5,0xCB,0x47,0x0E,0x97,0x2A,0x68,0xC0,0x22,0x95,0x62, +0x15,0xDB,0x47,0xD9,0xF5,0xD0,0x2B,0xFF,0x82,0x4B,0xC9,0xAD,0x3E,0xDE,0x4C,0xDB, +0x90,0x80,0x50,0x3F,0x09,0x8A,0x84,0x00,0xEC,0x30,0x0A,0x3D,0x18,0xCD,0xFB,0xFD, +0x2A,0x59,0x9A,0x23,0x95,0x17,0x2C,0x45,0x9E,0x1F,0x6E,0x43,0x79,0x6D,0x0C,0x5C, +0x98,0xFE,0x48,0xA7,0xC5,0x23,0x47,0x5C,0x5E,0xFD,0x6E,0xE7,0x1E,0xB4,0xF6,0x68, +0x45,0xD1,0x86,0x83,0x5B,0xA2,0x8A,0x8D,0xB1,0xE3,0x29,0x80,0xFE,0x25,0x71,0x88, +0xAD,0xBE,0xBC,0x8F,0xAC,0x52,0x96,0x4B,0xAA,0x51,0x8D,0xE4,0x13,0x31,0x19,0xE8, +0x4E,0x4D,0x9F,0xDB,0xAC,0xB3,0x6A,0xD5,0xBC,0x39,0x54,0x71,0xCA,0x7A,0x7A,0x7F, +0x90,0xDD,0x7D,0x1D,0x80,0xD9,0x81,0xBB,0x59,0x26,0xC2,0x11,0xFE,0xE6,0x93,0xE2, +0xF7,0x80,0xE4,0x65,0xFB,0x34,0x37,0x0E,0x29,0x80,0x70,0x4D,0xAF,0x38,0x86,0x2E, +0x9E,0x7F,0x57,0xAF,0x9E,0x17,0xAE,0xEB,0x1C,0xCB,0x28,0x21,0x5F,0xB6,0x1C,0xD8, +0xE7,0xA2,0x04,0x22,0xF9,0xD3,0xDA,0xD8,0xCB,0x02,0x03,0x01,0x00,0x01,0xA3,0x81, +0xB0,0x30,0x81,0xAD,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04, +0x03,0x02,0x01,0x06,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05, +0x30,0x03,0x01,0x01,0xFF,0x30,0x2B,0x06,0x03,0x55,0x1D,0x10,0x04,0x24,0x30,0x22, +0x80,0x0F,0x32,0x30,0x30,0x36,0x31,0x31,0x32,0x37,0x32,0x30,0x32,0x33,0x34,0x32, +0x5A,0x81,0x0F,0x32,0x30,0x32,0x36,0x31,0x31,0x32,0x37,0x32,0x30,0x35,0x33,0x34, +0x32,0x5A,0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14,0x68, +0x90,0xE4,0x67,0xA4,0xA6,0x53,0x80,0xC7,0x86,0x66,0xA4,0xF1,0xF7,0x4B,0x43,0xFB, +0x84,0xBD,0x6D,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x68,0x90, +0xE4,0x67,0xA4,0xA6,0x53,0x80,0xC7,0x86,0x66,0xA4,0xF1,0xF7,0x4B,0x43,0xFB,0x84, +0xBD,0x6D,0x30,0x1D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF6,0x7D,0x07,0x41,0x00,0x04, +0x10,0x30,0x0E,0x1B,0x08,0x56,0x37,0x2E,0x31,0x3A,0x34,0x2E,0x30,0x03,0x02,0x04, +0x90,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00, +0x03,0x82,0x01,0x01,0x00,0x93,0xD4,0x30,0xB0,0xD7,0x03,0x20,0x2A,0xD0,0xF9,0x63, +0xE8,0x91,0x0C,0x05,0x20,0xA9,0x5F,0x19,0xCA,0x7B,0x72,0x4E,0xD4,0xB1,0xDB,0xD0, +0x96,0xFB,0x54,0x5A,0x19,0x2C,0x0C,0x08,0xF7,0xB2,0xBC,0x85,0xA8,0x9D,0x7F,0x6D, +0x3B,0x52,0xB3,0x2A,0xDB,0xE7,0xD4,0x84,0x8C,0x63,0xF6,0x0F,0xCB,0x26,0x01,0x91, +0x50,0x6C,0xF4,0x5F,0x14,0xE2,0x93,0x74,0xC0,0x13,0x9E,0x30,0x3A,0x50,0xE3,0xB4, +0x60,0xC5,0x1C,0xF0,0x22,0x44,0x8D,0x71,0x47,0xAC,0xC8,0x1A,0xC9,0xE9,0x9B,0x9A, +0x00,0x60,0x13,0xFF,0x70,0x7E,0x5F,0x11,0x4D,0x49,0x1B,0xB3,0x15,0x52,0x7B,0xC9, +0x54,0xDA,0xBF,0x9D,0x95,0xAF,0x6B,0x9A,0xD8,0x9E,0xE9,0xF1,0xE4,0x43,0x8D,0xE2, +0x11,0x44,0x3A,0xBF,0xAF,0xBD,0x83,0x42,0x73,0x52,0x8B,0xAA,0xBB,0xA7,0x29,0xCF, +0xF5,0x64,0x1C,0x0A,0x4D,0xD1,0xBC,0xAA,0xAC,0x9F,0x2A,0xD0,0xFF,0x7F,0x7F,0xDA, +0x7D,0xEA,0xB1,0xED,0x30,0x25,0xC1,0x84,0xDA,0x34,0xD2,0x5B,0x78,0x83,0x56,0xEC, +0x9C,0x36,0xC3,0x26,0xE2,0x11,0xF6,0x67,0x49,0x1D,0x92,0xAB,0x8C,0xFB,0xEB,0xFF, +0x7A,0xEE,0x85,0x4A,0xA7,0x50,0x80,0xF0,0xA7,0x5C,0x4A,0x94,0x2E,0x5F,0x05,0x99, +0x3C,0x52,0x41,0xE0,0xCD,0xB4,0x63,0xCF,0x01,0x43,0xBA,0x9C,0x83,0xDC,0x8F,0x60, +0x3B,0xF3,0x5A,0xB4,0xB4,0x7B,0xAE,0xDA,0x0B,0x90,0x38,0x75,0xEF,0x81,0x1D,0x66, +0xD2,0xF7,0x57,0x70,0x36,0xB3,0xBF,0xFC,0x28,0xAF,0x71,0x25,0x85,0x5B,0x13,0xFE, +0x1E,0x7F,0x5A,0xB4,0x3C, +}; + + +/* subject:/C=US/O=Equifax/OU=Equifax Secure Certificate Authority */ +/* issuer :/C=US/O=Equifax/OU=Equifax Secure Certificate Authority */ + + +const unsigned char Equifax_Secure_CA_certificate[804]={ +0x30,0x82,0x03,0x20,0x30,0x82,0x02,0x89,0xA0,0x03,0x02,0x01,0x02,0x02,0x04,0x35, +0xDE,0xF4,0xCF,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05, +0x05,0x00,0x30,0x4E,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55, +0x53,0x31,0x10,0x30,0x0E,0x06,0x03,0x55,0x04,0x0A,0x13,0x07,0x45,0x71,0x75,0x69, +0x66,0x61,0x78,0x31,0x2D,0x30,0x2B,0x06,0x03,0x55,0x04,0x0B,0x13,0x24,0x45,0x71, +0x75,0x69,0x66,0x61,0x78,0x20,0x53,0x65,0x63,0x75,0x72,0x65,0x20,0x43,0x65,0x72, +0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69, +0x74,0x79,0x30,0x1E,0x17,0x0D,0x39,0x38,0x30,0x38,0x32,0x32,0x31,0x36,0x34,0x31, +0x35,0x31,0x5A,0x17,0x0D,0x31,0x38,0x30,0x38,0x32,0x32,0x31,0x36,0x34,0x31,0x35, +0x31,0x5A,0x30,0x4E,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55, +0x53,0x31,0x10,0x30,0x0E,0x06,0x03,0x55,0x04,0x0A,0x13,0x07,0x45,0x71,0x75,0x69, +0x66,0x61,0x78,0x31,0x2D,0x30,0x2B,0x06,0x03,0x55,0x04,0x0B,0x13,0x24,0x45,0x71, +0x75,0x69,0x66,0x61,0x78,0x20,0x53,0x65,0x63,0x75,0x72,0x65,0x20,0x43,0x65,0x72, +0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69, +0x74,0x79,0x30,0x81,0x9F,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01, +0x01,0x01,0x05,0x00,0x03,0x81,0x8D,0x00,0x30,0x81,0x89,0x02,0x81,0x81,0x00,0xC1, +0x5D,0xB1,0x58,0x67,0x08,0x62,0xEE,0xA0,0x9A,0x2D,0x1F,0x08,0x6D,0x91,0x14,0x68, +0x98,0x0A,0x1E,0xFE,0xDA,0x04,0x6F,0x13,0x84,0x62,0x21,0xC3,0xD1,0x7C,0xCE,0x9F, +0x05,0xE0,0xB8,0x01,0xF0,0x4E,0x34,0xEC,0xE2,0x8A,0x95,0x04,0x64,0xAC,0xF1,0x6B, +0x53,0x5F,0x05,0xB3,0xCB,0x67,0x80,0xBF,0x42,0x02,0x8E,0xFE,0xDD,0x01,0x09,0xEC, +0xE1,0x00,0x14,0x4F,0xFC,0xFB,0xF0,0x0C,0xDD,0x43,0xBA,0x5B,0x2B,0xE1,0x1F,0x80, +0x70,0x99,0x15,0x57,0x93,0x16,0xF1,0x0F,0x97,0x6A,0xB7,0xC2,0x68,0x23,0x1C,0xCC, +0x4D,0x59,0x30,0xAC,0x51,0x1E,0x3B,0xAF,0x2B,0xD6,0xEE,0x63,0x45,0x7B,0xC5,0xD9, +0x5F,0x50,0xD2,0xE3,0x50,0x0F,0x3A,0x88,0xE7,0xBF,0x14,0xFD,0xE0,0xC7,0xB9,0x02, +0x03,0x01,0x00,0x01,0xA3,0x82,0x01,0x09,0x30,0x82,0x01,0x05,0x30,0x70,0x06,0x03, +0x55,0x1D,0x1F,0x04,0x69,0x30,0x67,0x30,0x65,0xA0,0x63,0xA0,0x61,0xA4,0x5F,0x30, +0x5D,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x10, +0x30,0x0E,0x06,0x03,0x55,0x04,0x0A,0x13,0x07,0x45,0x71,0x75,0x69,0x66,0x61,0x78, +0x31,0x2D,0x30,0x2B,0x06,0x03,0x55,0x04,0x0B,0x13,0x24,0x45,0x71,0x75,0x69,0x66, +0x61,0x78,0x20,0x53,0x65,0x63,0x75,0x72,0x65,0x20,0x43,0x65,0x72,0x74,0x69,0x66, +0x69,0x63,0x61,0x74,0x65,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x31, +0x0D,0x30,0x0B,0x06,0x03,0x55,0x04,0x03,0x13,0x04,0x43,0x52,0x4C,0x31,0x30,0x1A, +0x06,0x03,0x55,0x1D,0x10,0x04,0x13,0x30,0x11,0x81,0x0F,0x32,0x30,0x31,0x38,0x30, +0x38,0x32,0x32,0x31,0x36,0x34,0x31,0x35,0x31,0x5A,0x30,0x0B,0x06,0x03,0x55,0x1D, +0x0F,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18, +0x30,0x16,0x80,0x14,0x48,0xE6,0x68,0xF9,0x2B,0xD2,0xB2,0x95,0xD7,0x47,0xD8,0x23, +0x20,0x10,0x4F,0x33,0x98,0x90,0x9F,0xD4,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04, +0x16,0x04,0x14,0x48,0xE6,0x68,0xF9,0x2B,0xD2,0xB2,0x95,0xD7,0x47,0xD8,0x23,0x20, +0x10,0x4F,0x33,0x98,0x90,0x9F,0xD4,0x30,0x0C,0x06,0x03,0x55,0x1D,0x13,0x04,0x05, +0x30,0x03,0x01,0x01,0xFF,0x30,0x1A,0x06,0x09,0x2A,0x86,0x48,0x86,0xF6,0x7D,0x07, +0x41,0x00,0x04,0x0D,0x30,0x0B,0x1B,0x05,0x56,0x33,0x2E,0x30,0x63,0x03,0x02,0x06, +0xC0,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00, +0x03,0x81,0x81,0x00,0x58,0xCE,0x29,0xEA,0xFC,0xF7,0xDE,0xB5,0xCE,0x02,0xB9,0x17, +0xB5,0x85,0xD1,0xB9,0xE3,0xE0,0x95,0xCC,0x25,0x31,0x0D,0x00,0xA6,0x92,0x6E,0x7F, +0xB6,0x92,0x63,0x9E,0x50,0x95,0xD1,0x9A,0x6F,0xE4,0x11,0xDE,0x63,0x85,0x6E,0x98, +0xEE,0xA8,0xFF,0x5A,0xC8,0xD3,0x55,0xB2,0x66,0x71,0x57,0xDE,0xC0,0x21,0xEB,0x3D, +0x2A,0xA7,0x23,0x49,0x01,0x04,0x86,0x42,0x7B,0xFC,0xEE,0x7F,0xA2,0x16,0x52,0xB5, +0x67,0x67,0xD3,0x40,0xDB,0x3B,0x26,0x58,0xB2,0x28,0x77,0x3D,0xAE,0x14,0x77,0x61, +0xD6,0xFA,0x2A,0x66,0x27,0xA0,0x0D,0xFA,0xA7,0x73,0x5C,0xEA,0x70,0xF1,0x94,0x21, +0x65,0x44,0x5F,0xFA,0xFC,0xEF,0x29,0x68,0xA9,0xA2,0x87,0x79,0xEF,0x79,0xEF,0x4F, +0xAC,0x07,0x77,0x38, +}; + + +/* subject:/C=US/O=Equifax Secure Inc./CN=Equifax Secure eBusiness CA-1 */ +/* issuer :/C=US/O=Equifax Secure Inc./CN=Equifax Secure eBusiness CA-1 */ + + +const unsigned char Equifax_Secure_eBusiness_CA_1_certificate[646]={ +0x30,0x82,0x02,0x82,0x30,0x82,0x01,0xEB,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x04, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x04,0x05,0x00,0x30, +0x53,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x1C, +0x30,0x1A,0x06,0x03,0x55,0x04,0x0A,0x13,0x13,0x45,0x71,0x75,0x69,0x66,0x61,0x78, +0x20,0x53,0x65,0x63,0x75,0x72,0x65,0x20,0x49,0x6E,0x63,0x2E,0x31,0x26,0x30,0x24, +0x06,0x03,0x55,0x04,0x03,0x13,0x1D,0x45,0x71,0x75,0x69,0x66,0x61,0x78,0x20,0x53, +0x65,0x63,0x75,0x72,0x65,0x20,0x65,0x42,0x75,0x73,0x69,0x6E,0x65,0x73,0x73,0x20, +0x43,0x41,0x2D,0x31,0x30,0x1E,0x17,0x0D,0x39,0x39,0x30,0x36,0x32,0x31,0x30,0x34, +0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x32,0x30,0x30,0x36,0x32,0x31,0x30,0x34,0x30, +0x30,0x30,0x30,0x5A,0x30,0x53,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13, +0x02,0x55,0x53,0x31,0x1C,0x30,0x1A,0x06,0x03,0x55,0x04,0x0A,0x13,0x13,0x45,0x71, +0x75,0x69,0x66,0x61,0x78,0x20,0x53,0x65,0x63,0x75,0x72,0x65,0x20,0x49,0x6E,0x63, +0x2E,0x31,0x26,0x30,0x24,0x06,0x03,0x55,0x04,0x03,0x13,0x1D,0x45,0x71,0x75,0x69, +0x66,0x61,0x78,0x20,0x53,0x65,0x63,0x75,0x72,0x65,0x20,0x65,0x42,0x75,0x73,0x69, +0x6E,0x65,0x73,0x73,0x20,0x43,0x41,0x2D,0x31,0x30,0x81,0x9F,0x30,0x0D,0x06,0x09, +0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x81,0x8D,0x00,0x30, +0x81,0x89,0x02,0x81,0x81,0x00,0xCE,0x2F,0x19,0xBC,0x17,0xB7,0x77,0xDE,0x93,0xA9, +0x5F,0x5A,0x0D,0x17,0x4F,0x34,0x1A,0x0C,0x98,0xF4,0x22,0xD9,0x59,0xD4,0xC4,0x68, +0x46,0xF0,0xB4,0x35,0xC5,0x85,0x03,0x20,0xC6,0xAF,0x45,0xA5,0x21,0x51,0x45,0x41, +0xEB,0x16,0x58,0x36,0x32,0x6F,0xE2,0x50,0x62,0x64,0xF9,0xFD,0x51,0x9C,0xAA,0x24, +0xD9,0xF4,0x9D,0x83,0x2A,0x87,0x0A,0x21,0xD3,0x12,0x38,0x34,0x6C,0x8D,0x00,0x6E, +0x5A,0xA0,0xD9,0x42,0xEE,0x1A,0x21,0x95,0xF9,0x52,0x4C,0x55,0x5A,0xC5,0x0F,0x38, +0x4F,0x46,0xFA,0x6D,0xF8,0x2E,0x35,0xD6,0x1D,0x7C,0xEB,0xE2,0xF0,0xB0,0x75,0x80, +0xC8,0xA9,0x13,0xAC,0xBE,0x88,0xEF,0x3A,0x6E,0xAB,0x5F,0x2A,0x38,0x62,0x02,0xB0, +0x12,0x7B,0xFE,0x8F,0xA6,0x03,0x02,0x03,0x01,0x00,0x01,0xA3,0x66,0x30,0x64,0x30, +0x11,0x06,0x09,0x60,0x86,0x48,0x01,0x86,0xF8,0x42,0x01,0x01,0x04,0x04,0x03,0x02, +0x00,0x07,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03, +0x01,0x01,0xFF,0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14, +0x4A,0x78,0x32,0x52,0x11,0xDB,0x59,0x16,0x36,0x5E,0xDF,0xC1,0x14,0x36,0x40,0x6A, +0x47,0x7C,0x4C,0xA1,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x4A, +0x78,0x32,0x52,0x11,0xDB,0x59,0x16,0x36,0x5E,0xDF,0xC1,0x14,0x36,0x40,0x6A,0x47, +0x7C,0x4C,0xA1,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x04, +0x05,0x00,0x03,0x81,0x81,0x00,0x75,0x5B,0xA8,0x9B,0x03,0x11,0xE6,0xE9,0x56,0x4C, +0xCD,0xF9,0xA9,0x4C,0xC0,0x0D,0x9A,0xF3,0xCC,0x65,0x69,0xE6,0x25,0x76,0xCC,0x59, +0xB7,0xD6,0x54,0xC3,0x1D,0xCD,0x99,0xAC,0x19,0xDD,0xB4,0x85,0xD5,0xE0,0x3D,0xFC, +0x62,0x20,0xA7,0x84,0x4B,0x58,0x65,0xF1,0xE2,0xF9,0x95,0x21,0x3F,0xF5,0xD4,0x7E, +0x58,0x1E,0x47,0x87,0x54,0x3E,0x58,0xA1,0xB5,0xB5,0xF8,0x2A,0xEF,0x71,0xE7,0xBC, +0xC3,0xF6,0xB1,0x49,0x46,0xE2,0xD7,0xA0,0x6B,0xE5,0x56,0x7A,0x9A,0x27,0x98,0x7C, +0x46,0x62,0x14,0xE7,0xC9,0xFC,0x6E,0x03,0x12,0x79,0x80,0x38,0x1D,0x48,0x82,0x8D, +0xFC,0x17,0xFE,0x2A,0x96,0x2B,0xB5,0x62,0xA6,0xA6,0x3D,0xBD,0x7F,0x92,0x59,0xCD, +0x5A,0x2A,0x82,0xB2,0x37,0x79, +}; + + +/* subject:/C=US/O=Equifax Secure/OU=Equifax Secure eBusiness CA-2 */ +/* issuer :/C=US/O=Equifax Secure/OU=Equifax Secure eBusiness CA-2 */ + + +const unsigned char Equifax_Secure_eBusiness_CA_2_certificate[804]={ +0x30,0x82,0x03,0x20,0x30,0x82,0x02,0x89,0xA0,0x03,0x02,0x01,0x02,0x02,0x04,0x37, +0x70,0xCF,0xB5,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05, +0x05,0x00,0x30,0x4E,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55, +0x53,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04,0x0A,0x13,0x0E,0x45,0x71,0x75,0x69, +0x66,0x61,0x78,0x20,0x53,0x65,0x63,0x75,0x72,0x65,0x31,0x26,0x30,0x24,0x06,0x03, +0x55,0x04,0x0B,0x13,0x1D,0x45,0x71,0x75,0x69,0x66,0x61,0x78,0x20,0x53,0x65,0x63, +0x75,0x72,0x65,0x20,0x65,0x42,0x75,0x73,0x69,0x6E,0x65,0x73,0x73,0x20,0x43,0x41, +0x2D,0x32,0x30,0x1E,0x17,0x0D,0x39,0x39,0x30,0x36,0x32,0x33,0x31,0x32,0x31,0x34, +0x34,0x35,0x5A,0x17,0x0D,0x31,0x39,0x30,0x36,0x32,0x33,0x31,0x32,0x31,0x34,0x34, +0x35,0x5A,0x30,0x4E,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55, +0x53,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04,0x0A,0x13,0x0E,0x45,0x71,0x75,0x69, +0x66,0x61,0x78,0x20,0x53,0x65,0x63,0x75,0x72,0x65,0x31,0x26,0x30,0x24,0x06,0x03, +0x55,0x04,0x0B,0x13,0x1D,0x45,0x71,0x75,0x69,0x66,0x61,0x78,0x20,0x53,0x65,0x63, +0x75,0x72,0x65,0x20,0x65,0x42,0x75,0x73,0x69,0x6E,0x65,0x73,0x73,0x20,0x43,0x41, +0x2D,0x32,0x30,0x81,0x9F,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01, +0x01,0x01,0x05,0x00,0x03,0x81,0x8D,0x00,0x30,0x81,0x89,0x02,0x81,0x81,0x00,0xE4, +0x39,0x39,0x93,0x1E,0x52,0x06,0x1B,0x28,0x36,0xF8,0xB2,0xA3,0x29,0xC5,0xED,0x8E, +0xB2,0x11,0xBD,0xFE,0xEB,0xE7,0xB4,0x74,0xC2,0x8F,0xFF,0x05,0xE7,0xD9,0x9D,0x06, +0xBF,0x12,0xC8,0x3F,0x0E,0xF2,0xD6,0xD1,0x24,0xB2,0x11,0xDE,0xD1,0x73,0x09,0x8A, +0xD4,0xB1,0x2C,0x98,0x09,0x0D,0x1E,0x50,0x46,0xB2,0x83,0xA6,0x45,0x8D,0x62,0x68, +0xBB,0x85,0x1B,0x20,0x70,0x32,0xAA,0x40,0xCD,0xA6,0x96,0x5F,0xC4,0x71,0x37,0x3F, +0x04,0xF3,0xB7,0x41,0x24,0x39,0x07,0x1A,0x1E,0x2E,0x61,0x58,0xA0,0x12,0x0B,0xE5, +0xA5,0xDF,0xC5,0xAB,0xEA,0x37,0x71,0xCC,0x1C,0xC8,0x37,0x3A,0xB9,0x97,0x52,0xA7, +0xAC,0xC5,0x6A,0x24,0x94,0x4E,0x9C,0x7B,0xCF,0xC0,0x6A,0xD6,0xDF,0x21,0xBD,0x02, +0x03,0x01,0x00,0x01,0xA3,0x82,0x01,0x09,0x30,0x82,0x01,0x05,0x30,0x70,0x06,0x03, +0x55,0x1D,0x1F,0x04,0x69,0x30,0x67,0x30,0x65,0xA0,0x63,0xA0,0x61,0xA4,0x5F,0x30, +0x5D,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17, +0x30,0x15,0x06,0x03,0x55,0x04,0x0A,0x13,0x0E,0x45,0x71,0x75,0x69,0x66,0x61,0x78, +0x20,0x53,0x65,0x63,0x75,0x72,0x65,0x31,0x26,0x30,0x24,0x06,0x03,0x55,0x04,0x0B, +0x13,0x1D,0x45,0x71,0x75,0x69,0x66,0x61,0x78,0x20,0x53,0x65,0x63,0x75,0x72,0x65, +0x20,0x65,0x42,0x75,0x73,0x69,0x6E,0x65,0x73,0x73,0x20,0x43,0x41,0x2D,0x32,0x31, +0x0D,0x30,0x0B,0x06,0x03,0x55,0x04,0x03,0x13,0x04,0x43,0x52,0x4C,0x31,0x30,0x1A, +0x06,0x03,0x55,0x1D,0x10,0x04,0x13,0x30,0x11,0x81,0x0F,0x32,0x30,0x31,0x39,0x30, +0x36,0x32,0x33,0x31,0x32,0x31,0x34,0x34,0x35,0x5A,0x30,0x0B,0x06,0x03,0x55,0x1D, +0x0F,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18, +0x30,0x16,0x80,0x14,0x50,0x9E,0x0B,0xEA,0xAF,0x5E,0xB9,0x20,0x48,0xA6,0x50,0x6A, +0xCB,0xFD,0xD8,0x20,0x7A,0xA7,0x82,0x76,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04, +0x16,0x04,0x14,0x50,0x9E,0x0B,0xEA,0xAF,0x5E,0xB9,0x20,0x48,0xA6,0x50,0x6A,0xCB, +0xFD,0xD8,0x20,0x7A,0xA7,0x82,0x76,0x30,0x0C,0x06,0x03,0x55,0x1D,0x13,0x04,0x05, +0x30,0x03,0x01,0x01,0xFF,0x30,0x1A,0x06,0x09,0x2A,0x86,0x48,0x86,0xF6,0x7D,0x07, +0x41,0x00,0x04,0x0D,0x30,0x0B,0x1B,0x05,0x56,0x33,0x2E,0x30,0x63,0x03,0x02,0x06, +0xC0,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00, +0x03,0x81,0x81,0x00,0x0C,0x86,0x82,0xAD,0xE8,0x4E,0x1A,0xF5,0x8E,0x89,0x27,0xE2, +0x35,0x58,0x3D,0x29,0xB4,0x07,0x8F,0x36,0x50,0x95,0xBF,0x6E,0xC1,0x9E,0xEB,0xC4, +0x90,0xB2,0x85,0xA8,0xBB,0xB7,0x42,0xE0,0x0F,0x07,0x39,0xDF,0xFB,0x9E,0x90,0xB2, +0xD1,0xC1,0x3E,0x53,0x9F,0x03,0x44,0xB0,0x7E,0x4B,0xF4,0x6F,0xE4,0x7C,0x1F,0xE7, +0xE2,0xB1,0xE4,0xB8,0x9A,0xEF,0xC3,0xBD,0xCE,0xDE,0x0B,0x32,0x34,0xD9,0xDE,0x28, +0xED,0x33,0x6B,0xC4,0xD4,0xD7,0x3D,0x12,0x58,0xAB,0x7D,0x09,0x2D,0xCB,0x70,0xF5, +0x13,0x8A,0x94,0xA1,0x27,0xA4,0xD6,0x70,0xC5,0x6D,0x94,0xB5,0xC9,0x7D,0x9D,0xA0, +0xD2,0xC6,0x08,0x49,0xD9,0x66,0x9B,0xA6,0xD3,0xF4,0x0B,0xDC,0xC5,0x26,0x57,0xE1, +0x91,0x30,0xEA,0xCD, +}; + + +/* subject:/C=US/O=Equifax Secure Inc./CN=Equifax Secure Global eBusiness CA-1 */ +/* issuer :/C=US/O=Equifax Secure Inc./CN=Equifax Secure Global eBusiness CA-1 */ + + +const unsigned char Equifax_Secure_Global_eBusiness_CA_certificate[660]={ +0x30,0x82,0x02,0x90,0x30,0x82,0x01,0xF9,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x04,0x05,0x00,0x30, +0x5A,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x1C, +0x30,0x1A,0x06,0x03,0x55,0x04,0x0A,0x13,0x13,0x45,0x71,0x75,0x69,0x66,0x61,0x78, +0x20,0x53,0x65,0x63,0x75,0x72,0x65,0x20,0x49,0x6E,0x63,0x2E,0x31,0x2D,0x30,0x2B, +0x06,0x03,0x55,0x04,0x03,0x13,0x24,0x45,0x71,0x75,0x69,0x66,0x61,0x78,0x20,0x53, +0x65,0x63,0x75,0x72,0x65,0x20,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x20,0x65,0x42,0x75, +0x73,0x69,0x6E,0x65,0x73,0x73,0x20,0x43,0x41,0x2D,0x31,0x30,0x1E,0x17,0x0D,0x39, +0x39,0x30,0x36,0x32,0x31,0x30,0x34,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x32,0x30, +0x30,0x36,0x32,0x31,0x30,0x34,0x30,0x30,0x30,0x30,0x5A,0x30,0x5A,0x31,0x0B,0x30, +0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x1C,0x30,0x1A,0x06,0x03, +0x55,0x04,0x0A,0x13,0x13,0x45,0x71,0x75,0x69,0x66,0x61,0x78,0x20,0x53,0x65,0x63, +0x75,0x72,0x65,0x20,0x49,0x6E,0x63,0x2E,0x31,0x2D,0x30,0x2B,0x06,0x03,0x55,0x04, +0x03,0x13,0x24,0x45,0x71,0x75,0x69,0x66,0x61,0x78,0x20,0x53,0x65,0x63,0x75,0x72, +0x65,0x20,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x20,0x65,0x42,0x75,0x73,0x69,0x6E,0x65, +0x73,0x73,0x20,0x43,0x41,0x2D,0x31,0x30,0x81,0x9F,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x81,0x8D,0x00,0x30,0x81,0x89, +0x02,0x81,0x81,0x00,0xBA,0xE7,0x17,0x90,0x02,0x65,0xB1,0x34,0x55,0x3C,0x49,0xC2, +0x51,0xD5,0xDF,0xA7,0xD1,0x37,0x8F,0xD1,0xE7,0x81,0x73,0x41,0x52,0x60,0x9B,0x9D, +0xA1,0x17,0x26,0x78,0xAD,0xC7,0xB1,0xE8,0x26,0x94,0x32,0xB5,0xDE,0x33,0x8D,0x3A, +0x2F,0xDB,0xF2,0x9A,0x7A,0x5A,0x73,0x98,0xA3,0x5C,0xE9,0xFB,0x8A,0x73,0x1B,0x5C, +0xE7,0xC3,0xBF,0x80,0x6C,0xCD,0xA9,0xF4,0xD6,0x2B,0xC0,0xF7,0xF9,0x99,0xAA,0x63, +0xA2,0xB1,0x47,0x02,0x0F,0xD4,0xE4,0x51,0x3A,0x12,0x3C,0x6C,0x8A,0x5A,0x54,0x84, +0x70,0xDB,0xC1,0xC5,0x90,0xCF,0x72,0x45,0xCB,0xA8,0x59,0xC0,0xCD,0x33,0x9D,0x3F, +0xA3,0x96,0xEB,0x85,0x33,0x21,0x1C,0x3E,0x1E,0x3E,0x60,0x6E,0x76,0x9C,0x67,0x85, +0xC5,0xC8,0xC3,0x61,0x02,0x03,0x01,0x00,0x01,0xA3,0x66,0x30,0x64,0x30,0x11,0x06, +0x09,0x60,0x86,0x48,0x01,0x86,0xF8,0x42,0x01,0x01,0x04,0x04,0x03,0x02,0x00,0x07, +0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01, +0xFF,0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14,0xBE,0xA8, +0xA0,0x74,0x72,0x50,0x6B,0x44,0xB7,0xC9,0x23,0xD8,0xFB,0xA8,0xFF,0xB3,0x57,0x6B, +0x68,0x6C,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0xBE,0xA8,0xA0, +0x74,0x72,0x50,0x6B,0x44,0xB7,0xC9,0x23,0xD8,0xFB,0xA8,0xFF,0xB3,0x57,0x6B,0x68, +0x6C,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x04,0x05,0x00, +0x03,0x81,0x81,0x00,0x30,0xE2,0x01,0x51,0xAA,0xC7,0xEA,0x5F,0xDA,0xB9,0xD0,0x65, +0x0F,0x30,0xD6,0x3E,0xDA,0x0D,0x14,0x49,0x6E,0x91,0x93,0x27,0x14,0x31,0xEF,0xC4, +0xF7,0x2D,0x45,0xF8,0xEC,0xC7,0xBF,0xA2,0x41,0x0D,0x23,0xB4,0x92,0xF9,0x19,0x00, +0x67,0xBD,0x01,0xAF,0xCD,0xE0,0x71,0xFC,0x5A,0xCF,0x64,0xC4,0xE0,0x96,0x98,0xD0, +0xA3,0x40,0xE2,0x01,0x8A,0xEF,0x27,0x07,0xF1,0x65,0x01,0x8A,0x44,0x2D,0x06,0x65, +0x75,0x52,0xC0,0x86,0x10,0x20,0x21,0x5F,0x6C,0x6B,0x0F,0x6C,0xAE,0x09,0x1C,0xAF, +0xF2,0xA2,0x18,0x34,0xC4,0x75,0xA4,0x73,0x1C,0xF1,0x8D,0xDC,0xEF,0xAD,0xF9,0xB3, +0x76,0xB4,0x92,0xBF,0xDC,0x95,0x10,0x1E,0xBE,0xCB,0xC8,0x3B,0x5A,0x84,0x60,0x19, +0x56,0x94,0xA9,0x55, +}; + + +/* subject:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA */ +/* issuer :/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA */ + + +const unsigned char GeoTrust_Global_CA_certificate[856]={ +0x30,0x82,0x03,0x54,0x30,0x82,0x02,0x3C,0xA0,0x03,0x02,0x01,0x02,0x02,0x03,0x02, +0x34,0x56,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05, +0x00,0x30,0x42,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53, +0x31,0x16,0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x47,0x65,0x6F,0x54,0x72, +0x75,0x73,0x74,0x20,0x49,0x6E,0x63,0x2E,0x31,0x1B,0x30,0x19,0x06,0x03,0x55,0x04, +0x03,0x13,0x12,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x47,0x6C,0x6F,0x62, +0x61,0x6C,0x20,0x43,0x41,0x30,0x1E,0x17,0x0D,0x30,0x32,0x30,0x35,0x32,0x31,0x30, +0x34,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x32,0x32,0x30,0x35,0x32,0x31,0x30,0x34, +0x30,0x30,0x30,0x30,0x5A,0x30,0x42,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06, +0x13,0x02,0x55,0x53,0x31,0x16,0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x47, +0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x49,0x6E,0x63,0x2E,0x31,0x1B,0x30,0x19, +0x06,0x03,0x55,0x04,0x03,0x13,0x12,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20, +0x47,0x6C,0x6F,0x62,0x61,0x6C,0x20,0x43,0x41,0x30,0x82,0x01,0x22,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F, +0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xDA,0xCC,0x18,0x63,0x30,0xFD, +0xF4,0x17,0x23,0x1A,0x56,0x7E,0x5B,0xDF,0x3C,0x6C,0x38,0xE4,0x71,0xB7,0x78,0x91, +0xD4,0xBC,0xA1,0xD8,0x4C,0xF8,0xA8,0x43,0xB6,0x03,0xE9,0x4D,0x21,0x07,0x08,0x88, +0xDA,0x58,0x2F,0x66,0x39,0x29,0xBD,0x05,0x78,0x8B,0x9D,0x38,0xE8,0x05,0xB7,0x6A, +0x7E,0x71,0xA4,0xE6,0xC4,0x60,0xA6,0xB0,0xEF,0x80,0xE4,0x89,0x28,0x0F,0x9E,0x25, +0xD6,0xED,0x83,0xF3,0xAD,0xA6,0x91,0xC7,0x98,0xC9,0x42,0x18,0x35,0x14,0x9D,0xAD, +0x98,0x46,0x92,0x2E,0x4F,0xCA,0xF1,0x87,0x43,0xC1,0x16,0x95,0x57,0x2D,0x50,0xEF, +0x89,0x2D,0x80,0x7A,0x57,0xAD,0xF2,0xEE,0x5F,0x6B,0xD2,0x00,0x8D,0xB9,0x14,0xF8, +0x14,0x15,0x35,0xD9,0xC0,0x46,0xA3,0x7B,0x72,0xC8,0x91,0xBF,0xC9,0x55,0x2B,0xCD, +0xD0,0x97,0x3E,0x9C,0x26,0x64,0xCC,0xDF,0xCE,0x83,0x19,0x71,0xCA,0x4E,0xE6,0xD4, +0xD5,0x7B,0xA9,0x19,0xCD,0x55,0xDE,0xC8,0xEC,0xD2,0x5E,0x38,0x53,0xE5,0x5C,0x4F, +0x8C,0x2D,0xFE,0x50,0x23,0x36,0xFC,0x66,0xE6,0xCB,0x8E,0xA4,0x39,0x19,0x00,0xB7, +0x95,0x02,0x39,0x91,0x0B,0x0E,0xFE,0x38,0x2E,0xD1,0x1D,0x05,0x9A,0xF6,0x4D,0x3E, +0x6F,0x0F,0x07,0x1D,0xAF,0x2C,0x1E,0x8F,0x60,0x39,0xE2,0xFA,0x36,0x53,0x13,0x39, +0xD4,0x5E,0x26,0x2B,0xDB,0x3D,0xA8,0x14,0xBD,0x32,0xEB,0x18,0x03,0x28,0x52,0x04, +0x71,0xE5,0xAB,0x33,0x3D,0xE1,0x38,0xBB,0x07,0x36,0x84,0x62,0x9C,0x79,0xEA,0x16, +0x30,0xF4,0x5F,0xC0,0x2B,0xE8,0x71,0x6B,0xE4,0xF9,0x02,0x03,0x01,0x00,0x01,0xA3, +0x53,0x30,0x51,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30, +0x03,0x01,0x01,0xFF,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0xC0, +0x7A,0x98,0x68,0x8D,0x89,0xFB,0xAB,0x05,0x64,0x0C,0x11,0x7D,0xAA,0x7D,0x65,0xB8, +0xCA,0xCC,0x4E,0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14, +0xC0,0x7A,0x98,0x68,0x8D,0x89,0xFB,0xAB,0x05,0x64,0x0C,0x11,0x7D,0xAA,0x7D,0x65, +0xB8,0xCA,0xCC,0x4E,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01, +0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x35,0xE3,0x29,0x6A,0xE5,0x2F,0x5D,0x54, +0x8E,0x29,0x50,0x94,0x9F,0x99,0x1A,0x14,0xE4,0x8F,0x78,0x2A,0x62,0x94,0xA2,0x27, +0x67,0x9E,0xD0,0xCF,0x1A,0x5E,0x47,0xE9,0xC1,0xB2,0xA4,0xCF,0xDD,0x41,0x1A,0x05, +0x4E,0x9B,0x4B,0xEE,0x4A,0x6F,0x55,0x52,0xB3,0x24,0xA1,0x37,0x0A,0xEB,0x64,0x76, +0x2A,0x2E,0x2C,0xF3,0xFD,0x3B,0x75,0x90,0xBF,0xFA,0x71,0xD8,0xC7,0x3D,0x37,0xD2, +0xB5,0x05,0x95,0x62,0xB9,0xA6,0xDE,0x89,0x3D,0x36,0x7B,0x38,0x77,0x48,0x97,0xAC, +0xA6,0x20,0x8F,0x2E,0xA6,0xC9,0x0C,0xC2,0xB2,0x99,0x45,0x00,0xC7,0xCE,0x11,0x51, +0x22,0x22,0xE0,0xA5,0xEA,0xB6,0x15,0x48,0x09,0x64,0xEA,0x5E,0x4F,0x74,0xF7,0x05, +0x3E,0xC7,0x8A,0x52,0x0C,0xDB,0x15,0xB4,0xBD,0x6D,0x9B,0xE5,0xC6,0xB1,0x54,0x68, +0xA9,0xE3,0x69,0x90,0xB6,0x9A,0xA5,0x0F,0xB8,0xB9,0x3F,0x20,0x7D,0xAE,0x4A,0xB5, +0xB8,0x9C,0xE4,0x1D,0xB6,0xAB,0xE6,0x94,0xA5,0xC1,0xC7,0x83,0xAD,0xDB,0xF5,0x27, +0x87,0x0E,0x04,0x6C,0xD5,0xFF,0xDD,0xA0,0x5D,0xED,0x87,0x52,0xB7,0x2B,0x15,0x02, +0xAE,0x39,0xA6,0x6A,0x74,0xE9,0xDA,0xC4,0xE7,0xBC,0x4D,0x34,0x1E,0xA9,0x5C,0x4D, +0x33,0x5F,0x92,0x09,0x2F,0x88,0x66,0x5D,0x77,0x97,0xC7,0x1D,0x76,0x13,0xA9,0xD5, +0xE5,0xF1,0x16,0x09,0x11,0x35,0xD5,0xAC,0xDB,0x24,0x71,0x70,0x2C,0x98,0x56,0x0B, +0xD9,0x17,0xB4,0xD1,0xE3,0x51,0x2B,0x5E,0x75,0xE8,0xD5,0xD0,0xDC,0x4F,0x34,0xED, +0xC2,0x05,0x66,0x80,0xA1,0xCB,0xE6,0x33, +}; + + +/* subject:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA 2 */ +/* issuer :/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA 2 */ + + +const unsigned char GeoTrust_Global_CA_2_certificate[874]={ +0x30,0x82,0x03,0x66,0x30,0x82,0x02,0x4E,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x44,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x16, +0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73, +0x74,0x20,0x49,0x6E,0x63,0x2E,0x31,0x1D,0x30,0x1B,0x06,0x03,0x55,0x04,0x03,0x13, +0x14,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x47,0x6C,0x6F,0x62,0x61,0x6C, +0x20,0x43,0x41,0x20,0x32,0x30,0x1E,0x17,0x0D,0x30,0x34,0x30,0x33,0x30,0x34,0x30, +0x35,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x31,0x39,0x30,0x33,0x30,0x34,0x30,0x35, +0x30,0x30,0x30,0x30,0x5A,0x30,0x44,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06, +0x13,0x02,0x55,0x53,0x31,0x16,0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x47, +0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x49,0x6E,0x63,0x2E,0x31,0x1D,0x30,0x1B, +0x06,0x03,0x55,0x04,0x03,0x13,0x14,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20, +0x47,0x6C,0x6F,0x62,0x61,0x6C,0x20,0x43,0x41,0x20,0x32,0x30,0x82,0x01,0x22,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82, +0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xEF,0x3C,0x4D,0x40, +0x3D,0x10,0xDF,0x3B,0x53,0x00,0xE1,0x67,0xFE,0x94,0x60,0x15,0x3E,0x85,0x88,0xF1, +0x89,0x0D,0x90,0xC8,0x28,0x23,0x99,0x05,0xE8,0x2B,0x20,0x9D,0xC6,0xF3,0x60,0x46, +0xD8,0xC1,0xB2,0xD5,0x8C,0x31,0xD9,0xDC,0x20,0x79,0x24,0x81,0xBF,0x35,0x32,0xFC, +0x63,0x69,0xDB,0xB1,0x2A,0x6B,0xEE,0x21,0x58,0xF2,0x08,0xE9,0x78,0xCB,0x6F,0xCB, +0xFC,0x16,0x52,0xC8,0x91,0xC4,0xFF,0x3D,0x73,0xDE,0xB1,0x3E,0xA7,0xC2,0x7D,0x66, +0xC1,0xF5,0x7E,0x52,0x24,0x1A,0xE2,0xD5,0x67,0x91,0xD0,0x82,0x10,0xD7,0x78,0x4B, +0x4F,0x2B,0x42,0x39,0xBD,0x64,0x2D,0x40,0xA0,0xB0,0x10,0xD3,0x38,0x48,0x46,0x88, +0xA1,0x0C,0xBB,0x3A,0x33,0x2A,0x62,0x98,0xFB,0x00,0x9D,0x13,0x59,0x7F,0x6F,0x3B, +0x72,0xAA,0xEE,0xA6,0x0F,0x86,0xF9,0x05,0x61,0xEA,0x67,0x7F,0x0C,0x37,0x96,0x8B, +0xE6,0x69,0x16,0x47,0x11,0xC2,0x27,0x59,0x03,0xB3,0xA6,0x60,0xC2,0x21,0x40,0x56, +0xFA,0xA0,0xC7,0x7D,0x3A,0x13,0xE3,0xEC,0x57,0xC7,0xB3,0xD6,0xAE,0x9D,0x89,0x80, +0xF7,0x01,0xE7,0x2C,0xF6,0x96,0x2B,0x13,0x0D,0x79,0x2C,0xD9,0xC0,0xE4,0x86,0x7B, +0x4B,0x8C,0x0C,0x72,0x82,0x8A,0xFB,0x17,0xCD,0x00,0x6C,0x3A,0x13,0x3C,0xB0,0x84, +0x87,0x4B,0x16,0x7A,0x29,0xB2,0x4F,0xDB,0x1D,0xD4,0x0B,0xF3,0x66,0x37,0xBD,0xD8, +0xF6,0x57,0xBB,0x5E,0x24,0x7A,0xB8,0x3C,0x8B,0xB9,0xFA,0x92,0x1A,0x1A,0x84,0x9E, +0xD8,0x74,0x8F,0xAA,0x1B,0x7F,0x5E,0xF4,0xFE,0x45,0x22,0x21,0x02,0x03,0x01,0x00, +0x01,0xA3,0x63,0x30,0x61,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04, +0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04, +0x14,0x71,0x38,0x36,0xF2,0x02,0x31,0x53,0x47,0x2B,0x6E,0xBA,0x65,0x46,0xA9,0x10, +0x15,0x58,0x20,0x05,0x09,0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18,0x30,0x16, +0x80,0x14,0x71,0x38,0x36,0xF2,0x02,0x31,0x53,0x47,0x2B,0x6E,0xBA,0x65,0x46,0xA9, +0x10,0x15,0x58,0x20,0x05,0x09,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF, +0x04,0x04,0x03,0x02,0x01,0x86,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D, +0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x03,0xF7,0xB5,0x2B,0xAB,0x5D, +0x10,0xFC,0x7B,0xB2,0xB2,0x5E,0xAC,0x9B,0x0E,0x7E,0x53,0x78,0x59,0x3E,0x42,0x04, +0xFE,0x75,0xA3,0xAD,0xAC,0x81,0x4E,0xD7,0x02,0x8B,0x5E,0xC4,0x2D,0xC8,0x52,0x76, +0xC7,0x2C,0x1F,0xFC,0x81,0x32,0x98,0xD1,0x4B,0xC6,0x92,0x93,0x33,0x35,0x31,0x2F, +0xFC,0xD8,0x1D,0x44,0xDD,0xE0,0x81,0x7F,0x9D,0xE9,0x8B,0xE1,0x64,0x91,0x62,0x0B, +0x39,0x08,0x8C,0xAC,0x74,0x9D,0x59,0xD9,0x7A,0x59,0x52,0x97,0x11,0xB9,0x16,0x7B, +0x6F,0x45,0xD3,0x96,0xD9,0x31,0x7D,0x02,0x36,0x0F,0x9C,0x3B,0x6E,0xCF,0x2C,0x0D, +0x03,0x46,0x45,0xEB,0xA0,0xF4,0x7F,0x48,0x44,0xC6,0x08,0x40,0xCC,0xDE,0x1B,0x70, +0xB5,0x29,0xAD,0xBA,0x8B,0x3B,0x34,0x65,0x75,0x1B,0x71,0x21,0x1D,0x2C,0x14,0x0A, +0xB0,0x96,0x95,0xB8,0xD6,0xEA,0xF2,0x65,0xFB,0x29,0xBA,0x4F,0xEA,0x91,0x93,0x74, +0x69,0xB6,0xF2,0xFF,0xE1,0x1A,0xD0,0x0C,0xD1,0x76,0x85,0xCB,0x8A,0x25,0xBD,0x97, +0x5E,0x2C,0x6F,0x15,0x99,0x26,0xE7,0xB6,0x29,0xFF,0x22,0xEC,0xC9,0x02,0xC7,0x56, +0x00,0xCD,0x49,0xB9,0xB3,0x6C,0x7B,0x53,0x04,0x1A,0xE2,0xA8,0xC9,0xAA,0x12,0x05, +0x23,0xC2,0xCE,0xE7,0xBB,0x04,0x02,0xCC,0xC0,0x47,0xA2,0xE4,0xC4,0x29,0x2F,0x5B, +0x45,0x57,0x89,0x51,0xEE,0x3C,0xEB,0x52,0x08,0xFF,0x07,0x35,0x1E,0x9F,0x35,0x6A, +0x47,0x4A,0x56,0x98,0xD1,0x5A,0x85,0x1F,0x8C,0xF5,0x22,0xBF,0xAB,0xCE,0x83,0xF3, +0xE2,0x22,0x29,0xAE,0x7D,0x83,0x40,0xA8,0xBA,0x6C, +}; + + +/* subject:/C=US/O=GeoTrust Inc./CN=GeoTrust Primary Certification Authority */ +/* issuer :/C=US/O=GeoTrust Inc./CN=GeoTrust Primary Certification Authority */ + + +const unsigned char GeoTrust_Primary_Certification_Authority_certificate[896]={ +0x30,0x82,0x03,0x7C,0x30,0x82,0x02,0x64,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x18, +0xAC,0xB5,0x6A,0xFD,0x69,0xB6,0x15,0x3A,0x63,0x6C,0xAF,0xDA,0xFA,0xC4,0xA1,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x58, +0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x16,0x30, +0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74, +0x20,0x49,0x6E,0x63,0x2E,0x31,0x31,0x30,0x2F,0x06,0x03,0x55,0x04,0x03,0x13,0x28, +0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x50,0x72,0x69,0x6D,0x61,0x72,0x79, +0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41, +0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x1E,0x17,0x0D,0x30,0x36,0x31,0x31, +0x32,0x37,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x36,0x30,0x37,0x31, +0x36,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x58,0x31,0x0B,0x30,0x09,0x06,0x03, +0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x16,0x30,0x14,0x06,0x03,0x55,0x04,0x0A, +0x13,0x0D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x49,0x6E,0x63,0x2E,0x31, +0x31,0x30,0x2F,0x06,0x03,0x55,0x04,0x03,0x13,0x28,0x47,0x65,0x6F,0x54,0x72,0x75, +0x73,0x74,0x20,0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69, +0x74,0x79,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D, +0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82, +0x01,0x01,0x00,0xBE,0xB8,0x15,0x7B,0xFF,0xD4,0x7C,0x7D,0x67,0xAD,0x83,0x64,0x7B, +0xC8,0x42,0x53,0x2D,0xDF,0xF6,0x84,0x08,0x20,0x61,0xD6,0x01,0x59,0x6A,0x9C,0x44, +0x11,0xAF,0xEF,0x76,0xFD,0x95,0x7E,0xCE,0x61,0x30,0xBB,0x7A,0x83,0x5F,0x02,0xBD, +0x01,0x66,0xCA,0xEE,0x15,0x8D,0x6F,0xA1,0x30,0x9C,0xBD,0xA1,0x85,0x9E,0x94,0x3A, +0xF3,0x56,0x88,0x00,0x31,0xCF,0xD8,0xEE,0x6A,0x96,0x02,0xD9,0xED,0x03,0x8C,0xFB, +0x75,0x6D,0xE7,0xEA,0xB8,0x55,0x16,0x05,0x16,0x9A,0xF4,0xE0,0x5E,0xB1,0x88,0xC0, +0x64,0x85,0x5C,0x15,0x4D,0x88,0xC7,0xB7,0xBA,0xE0,0x75,0xE9,0xAD,0x05,0x3D,0x9D, +0xC7,0x89,0x48,0xE0,0xBB,0x28,0xC8,0x03,0xE1,0x30,0x93,0x64,0x5E,0x52,0xC0,0x59, +0x70,0x22,0x35,0x57,0x88,0x8A,0xF1,0x95,0x0A,0x83,0xD7,0xBC,0x31,0x73,0x01,0x34, +0xED,0xEF,0x46,0x71,0xE0,0x6B,0x02,0xA8,0x35,0x72,0x6B,0x97,0x9B,0x66,0xE0,0xCB, +0x1C,0x79,0x5F,0xD8,0x1A,0x04,0x68,0x1E,0x47,0x02,0xE6,0x9D,0x60,0xE2,0x36,0x97, +0x01,0xDF,0xCE,0x35,0x92,0xDF,0xBE,0x67,0xC7,0x6D,0x77,0x59,0x3B,0x8F,0x9D,0xD6, +0x90,0x15,0x94,0xBC,0x42,0x34,0x10,0xC1,0x39,0xF9,0xB1,0x27,0x3E,0x7E,0xD6,0x8A, +0x75,0xC5,0xB2,0xAF,0x96,0xD3,0xA2,0xDE,0x9B,0xE4,0x98,0xBE,0x7D,0xE1,0xE9,0x81, +0xAD,0xB6,0x6F,0xFC,0xD7,0x0E,0xDA,0xE0,0x34,0xB0,0x0D,0x1A,0x77,0xE7,0xE3,0x08, +0x98,0xEF,0x58,0xFA,0x9C,0x84,0xB7,0x36,0xAF,0xC2,0xDF,0xAC,0xD2,0xF4,0x10,0x06, +0x70,0x71,0x35,0x02,0x03,0x01,0x00,0x01,0xA3,0x42,0x30,0x40,0x30,0x0F,0x06,0x03, +0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06, +0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x1D,0x06, +0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x2C,0xD5,0x50,0x41,0x97,0x15,0x8B,0xF0, +0x8F,0x36,0x61,0x5B,0x4A,0xFB,0x6B,0xD9,0x99,0xC9,0x33,0x92,0x30,0x0D,0x06,0x09, +0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00, +0x5A,0x70,0x7F,0x2C,0xDD,0xB7,0x34,0x4F,0xF5,0x86,0x51,0xA9,0x26,0xBE,0x4B,0xB8, +0xAA,0xF1,0x71,0x0D,0xDC,0x61,0xC7,0xA0,0xEA,0x34,0x1E,0x7A,0x77,0x0F,0x04,0x35, +0xE8,0x27,0x8F,0x6C,0x90,0xBF,0x91,0x16,0x24,0x46,0x3E,0x4A,0x4E,0xCE,0x2B,0x16, +0xD5,0x0B,0x52,0x1D,0xFC,0x1F,0x67,0xA2,0x02,0x45,0x31,0x4F,0xCE,0xF3,0xFA,0x03, +0xA7,0x79,0x9D,0x53,0x6A,0xD9,0xDA,0x63,0x3A,0xF8,0x80,0xD7,0xD3,0x99,0xE1,0xA5, +0xE1,0xBE,0xD4,0x55,0x71,0x98,0x35,0x3A,0xBE,0x93,0xEA,0xAE,0xAD,0x42,0xB2,0x90, +0x6F,0xE0,0xFC,0x21,0x4D,0x35,0x63,0x33,0x89,0x49,0xD6,0x9B,0x4E,0xCA,0xC7,0xE7, +0x4E,0x09,0x00,0xF7,0xDA,0xC7,0xEF,0x99,0x62,0x99,0x77,0xB6,0x95,0x22,0x5E,0x8A, +0xA0,0xAB,0xF4,0xB8,0x78,0x98,0xCA,0x38,0x19,0x99,0xC9,0x72,0x9E,0x78,0xCD,0x4B, +0xAC,0xAF,0x19,0xA0,0x73,0x12,0x2D,0xFC,0xC2,0x41,0xBA,0x81,0x91,0xDA,0x16,0x5A, +0x31,0xB7,0xF9,0xB4,0x71,0x80,0x12,0x48,0x99,0x72,0x73,0x5A,0x59,0x53,0xC1,0x63, +0x52,0x33,0xED,0xA7,0xC9,0xD2,0x39,0x02,0x70,0xFA,0xE0,0xB1,0x42,0x66,0x29,0xAA, +0x9B,0x51,0xED,0x30,0x54,0x22,0x14,0x5F,0xD9,0xAB,0x1D,0xC1,0xE4,0x94,0xF0,0xF8, +0xF5,0x2B,0xF7,0xEA,0xCA,0x78,0x46,0xD6,0xB8,0x91,0xFD,0xA6,0x0D,0x2B,0x1A,0x14, +0x01,0x3E,0x80,0xF0,0x42,0xA0,0x95,0x07,0x5E,0x6D,0xCD,0xCC,0x4B,0xA4,0x45,0x8D, +0xAB,0x12,0xE8,0xB3,0xDE,0x5A,0xE5,0xA0,0x7C,0xE8,0x0F,0x22,0x1D,0x5A,0xE9,0x59, +}; + + +/* subject:/C=US/O=GeoTrust Inc./OU=(c) 2007 GeoTrust Inc. - For authorized use only/CN=GeoTrust Primary Certification Authority - G2 */ +/* issuer :/C=US/O=GeoTrust Inc./OU=(c) 2007 GeoTrust Inc. - For authorized use only/CN=GeoTrust Primary Certification Authority - G2 */ + + +const unsigned char GeoTrust_Primary_Certification_Authority___G2_certificate[690]={ +0x30,0x82,0x02,0xAE,0x30,0x82,0x02,0x35,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x3C, +0xB2,0xF4,0x48,0x0A,0x00,0xE2,0xFE,0xEB,0x24,0x3B,0x5E,0x60,0x3E,0xC3,0x6B,0x30, +0x0A,0x06,0x08,0x2A,0x86,0x48,0xCE,0x3D,0x04,0x03,0x03,0x30,0x81,0x98,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x16,0x30,0x14,0x06, +0x03,0x55,0x04,0x0A,0x13,0x0D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x49, +0x6E,0x63,0x2E,0x31,0x39,0x30,0x37,0x06,0x03,0x55,0x04,0x0B,0x13,0x30,0x28,0x63, +0x29,0x20,0x32,0x30,0x30,0x37,0x20,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20, +0x49,0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74,0x68,0x6F, +0x72,0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79,0x31,0x36, +0x30,0x34,0x06,0x03,0x55,0x04,0x03,0x13,0x2D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73, +0x74,0x20,0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69,0x66, +0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74, +0x79,0x20,0x2D,0x20,0x47,0x32,0x30,0x1E,0x17,0x0D,0x30,0x37,0x31,0x31,0x30,0x35, +0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x38,0x30,0x31,0x31,0x38,0x32, +0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x81,0x98,0x31,0x0B,0x30,0x09,0x06,0x03,0x55, +0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x16,0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13, +0x0D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x49,0x6E,0x63,0x2E,0x31,0x39, +0x30,0x37,0x06,0x03,0x55,0x04,0x0B,0x13,0x30,0x28,0x63,0x29,0x20,0x32,0x30,0x30, +0x37,0x20,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x49,0x6E,0x63,0x2E,0x20, +0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74,0x68,0x6F,0x72,0x69,0x7A,0x65,0x64, +0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79,0x31,0x36,0x30,0x34,0x06,0x03,0x55, +0x04,0x03,0x13,0x2D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x50,0x72,0x69, +0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69, +0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x20,0x2D,0x20,0x47, +0x32,0x30,0x76,0x30,0x10,0x06,0x07,0x2A,0x86,0x48,0xCE,0x3D,0x02,0x01,0x06,0x05, +0x2B,0x81,0x04,0x00,0x22,0x03,0x62,0x00,0x04,0x15,0xB1,0xE8,0xFD,0x03,0x15,0x43, +0xE5,0xAC,0xEB,0x87,0x37,0x11,0x62,0xEF,0xD2,0x83,0x36,0x52,0x7D,0x45,0x57,0x0B, +0x4A,0x8D,0x7B,0x54,0x3B,0x3A,0x6E,0x5F,0x15,0x02,0xC0,0x50,0xA6,0xCF,0x25,0x2F, +0x7D,0xCA,0x48,0xB8,0xC7,0x50,0x63,0x1C,0x2A,0x21,0x08,0x7C,0x9A,0x36,0xD8,0x0B, +0xFE,0xD1,0x26,0xC5,0x58,0x31,0x30,0x28,0x25,0xF3,0x5D,0x5D,0xA3,0xB8,0xB6,0xA5, +0xB4,0x92,0xED,0x6C,0x2C,0x9F,0xEB,0xDD,0x43,0x89,0xA2,0x3C,0x4B,0x48,0x91,0x1D, +0x50,0xEC,0x26,0xDF,0xD6,0x60,0x2E,0xBD,0x21,0xA3,0x42,0x30,0x40,0x30,0x0F,0x06, +0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E, +0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x1D, +0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x15,0x5F,0x35,0x57,0x51,0x55,0xFB, +0x25,0xB2,0xAD,0x03,0x69,0xFC,0x01,0xA3,0xFA,0xBE,0x11,0x55,0xD5,0x30,0x0A,0x06, +0x08,0x2A,0x86,0x48,0xCE,0x3D,0x04,0x03,0x03,0x03,0x67,0x00,0x30,0x64,0x02,0x30, +0x64,0x96,0x59,0xA6,0xE8,0x09,0xDE,0x8B,0xBA,0xFA,0x5A,0x88,0x88,0xF0,0x1F,0x91, +0xD3,0x46,0xA8,0xF2,0x4A,0x4C,0x02,0x63,0xFB,0x6C,0x5F,0x38,0xDB,0x2E,0x41,0x93, +0xA9,0x0E,0xE6,0x9D,0xDC,0x31,0x1C,0xB2,0xA0,0xA7,0x18,0x1C,0x79,0xE1,0xC7,0x36, +0x02,0x30,0x3A,0x56,0xAF,0x9A,0x74,0x6C,0xF6,0xFB,0x83,0xE0,0x33,0xD3,0x08,0x5F, +0xA1,0x9C,0xC2,0x5B,0x9F,0x46,0xD6,0xB6,0xCB,0x91,0x06,0x63,0xA2,0x06,0xE7,0x33, +0xAC,0x3E,0xA8,0x81,0x12,0xD0,0xCB,0xBA,0xD0,0x92,0x0B,0xB6,0x9E,0x96,0xAA,0x04, +0x0F,0x8A, +}; + + +/* subject:/C=US/O=GeoTrust Inc./OU=(c) 2008 GeoTrust Inc. - For authorized use only/CN=GeoTrust Primary Certification Authority - G3 */ +/* issuer :/C=US/O=GeoTrust Inc./OU=(c) 2008 GeoTrust Inc. - For authorized use only/CN=GeoTrust Primary Certification Authority - G3 */ + + +const unsigned char GeoTrust_Primary_Certification_Authority___G3_certificate[1026]={ +0x30,0x82,0x03,0xFE,0x30,0x82,0x02,0xE6,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x15, +0xAC,0x6E,0x94,0x19,0xB2,0x79,0x4B,0x41,0xF6,0x27,0xA9,0xC3,0x18,0x0F,0x1F,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x30,0x81, +0x98,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x16, +0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73, +0x74,0x20,0x49,0x6E,0x63,0x2E,0x31,0x39,0x30,0x37,0x06,0x03,0x55,0x04,0x0B,0x13, +0x30,0x28,0x63,0x29,0x20,0x32,0x30,0x30,0x38,0x20,0x47,0x65,0x6F,0x54,0x72,0x75, +0x73,0x74,0x20,0x49,0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75, +0x74,0x68,0x6F,0x72,0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C, +0x79,0x31,0x36,0x30,0x34,0x06,0x03,0x55,0x04,0x03,0x13,0x2D,0x47,0x65,0x6F,0x54, +0x72,0x75,0x73,0x74,0x20,0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72, +0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F, +0x72,0x69,0x74,0x79,0x20,0x2D,0x20,0x47,0x33,0x30,0x1E,0x17,0x0D,0x30,0x38,0x30, +0x34,0x30,0x32,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x37,0x31,0x32, +0x30,0x31,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x81,0x98,0x31,0x0B,0x30,0x09, +0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x16,0x30,0x14,0x06,0x03,0x55, +0x04,0x0A,0x13,0x0D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x49,0x6E,0x63, +0x2E,0x31,0x39,0x30,0x37,0x06,0x03,0x55,0x04,0x0B,0x13,0x30,0x28,0x63,0x29,0x20, +0x32,0x30,0x30,0x38,0x20,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x49,0x6E, +0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74,0x68,0x6F,0x72,0x69, +0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79,0x31,0x36,0x30,0x34, +0x06,0x03,0x55,0x04,0x03,0x13,0x2D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20, +0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63, +0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x20, +0x2D,0x20,0x47,0x33,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A, +0x02,0x82,0x01,0x01,0x00,0xDC,0xE2,0x5E,0x62,0x58,0x1D,0x33,0x57,0x39,0x32,0x33, +0xFA,0xEB,0xCB,0x87,0x8C,0xA7,0xD4,0x4A,0xDD,0x06,0x88,0xEA,0x64,0x8E,0x31,0x98, +0xA5,0x38,0x90,0x1E,0x98,0xCF,0x2E,0x63,0x2B,0xF0,0x46,0xBC,0x44,0xB2,0x89,0xA1, +0xC0,0x28,0x0C,0x49,0x70,0x21,0x95,0x9F,0x64,0xC0,0xA6,0x93,0x12,0x02,0x65,0x26, +0x86,0xC6,0xA5,0x89,0xF0,0xFA,0xD7,0x84,0xA0,0x70,0xAF,0x4F,0x1A,0x97,0x3F,0x06, +0x44,0xD5,0xC9,0xEB,0x72,0x10,0x7D,0xE4,0x31,0x28,0xFB,0x1C,0x61,0xE6,0x28,0x07, +0x44,0x73,0x92,0x22,0x69,0xA7,0x03,0x88,0x6C,0x9D,0x63,0xC8,0x52,0xDA,0x98,0x27, +0xE7,0x08,0x4C,0x70,0x3E,0xB4,0xC9,0x12,0xC1,0xC5,0x67,0x83,0x5D,0x33,0xF3,0x03, +0x11,0xEC,0x6A,0xD0,0x53,0xE2,0xD1,0xBA,0x36,0x60,0x94,0x80,0xBB,0x61,0x63,0x6C, +0x5B,0x17,0x7E,0xDF,0x40,0x94,0x1E,0xAB,0x0D,0xC2,0x21,0x28,0x70,0x88,0xFF,0xD6, +0x26,0x6C,0x6C,0x60,0x04,0x25,0x4E,0x55,0x7E,0x7D,0xEF,0xBF,0x94,0x48,0xDE,0xB7, +0x1D,0xDD,0x70,0x8D,0x05,0x5F,0x88,0xA5,0x9B,0xF2,0xC2,0xEE,0xEA,0xD1,0x40,0x41, +0x6D,0x62,0x38,0x1D,0x56,0x06,0xC5,0x03,0x47,0x51,0x20,0x19,0xFC,0x7B,0x10,0x0B, +0x0E,0x62,0xAE,0x76,0x55,0xBF,0x5F,0x77,0xBE,0x3E,0x49,0x01,0x53,0x3D,0x98,0x25, +0x03,0x76,0x24,0x5A,0x1D,0xB4,0xDB,0x89,0xEA,0x79,0xE5,0xB6,0xB3,0x3B,0x3F,0xBA, +0x4C,0x28,0x41,0x7F,0x06,0xAC,0x6A,0x8E,0xC1,0xD0,0xF6,0x05,0x1D,0x7D,0xE6,0x42, +0x86,0xE3,0xA5,0xD5,0x47,0x02,0x03,0x01,0x00,0x01,0xA3,0x42,0x30,0x40,0x30,0x0F, +0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30, +0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30, +0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0xC4,0x79,0xCA,0x8E,0xA1,0x4E, +0x03,0x1D,0x1C,0xDC,0x6B,0xDB,0x31,0x5B,0x94,0x3E,0x3F,0x30,0x7F,0x2D,0x30,0x0D, +0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x03,0x82,0x01, +0x01,0x00,0x2D,0xC5,0x13,0xCF,0x56,0x80,0x7B,0x7A,0x78,0xBD,0x9F,0xAE,0x2C,0x99, +0xE7,0xEF,0xDA,0xDF,0x94,0x5E,0x09,0x69,0xA7,0xE7,0x6E,0x68,0x8C,0xBD,0x72,0xBE, +0x47,0xA9,0x0E,0x97,0x12,0xB8,0x4A,0xF1,0x64,0xD3,0x39,0xDF,0x25,0x34,0xD4,0xC1, +0xCD,0x4E,0x81,0xF0,0x0F,0x04,0xC4,0x24,0xB3,0x34,0x96,0xC6,0xA6,0xAA,0x30,0xDF, +0x68,0x61,0x73,0xD7,0xF9,0x8E,0x85,0x89,0xEF,0x0E,0x5E,0x95,0x28,0x4A,0x2A,0x27, +0x8F,0x10,0x8E,0x2E,0x7C,0x86,0xC4,0x02,0x9E,0xDA,0x0C,0x77,0x65,0x0E,0x44,0x0D, +0x92,0xFD,0xFD,0xB3,0x16,0x36,0xFA,0x11,0x0D,0x1D,0x8C,0x0E,0x07,0x89,0x6A,0x29, +0x56,0xF7,0x72,0xF4,0xDD,0x15,0x9C,0x77,0x35,0x66,0x57,0xAB,0x13,0x53,0xD8,0x8E, +0xC1,0x40,0xC5,0xD7,0x13,0x16,0x5A,0x72,0xC7,0xB7,0x69,0x01,0xC4,0x7A,0xB1,0x83, +0x01,0x68,0x7D,0x8D,0x41,0xA1,0x94,0x18,0xC1,0x25,0x5C,0xFC,0xF0,0xFE,0x83,0x02, +0x87,0x7C,0x0D,0x0D,0xCF,0x2E,0x08,0x5C,0x4A,0x40,0x0D,0x3E,0xEC,0x81,0x61,0xE6, +0x24,0xDB,0xCA,0xE0,0x0E,0x2D,0x07,0xB2,0x3E,0x56,0xDC,0x8D,0xF5,0x41,0x85,0x07, +0x48,0x9B,0x0C,0x0B,0xCB,0x49,0x3F,0x7D,0xEC,0xB7,0xFD,0xCB,0x8D,0x67,0x89,0x1A, +0xAB,0xED,0xBB,0x1E,0xA3,0x00,0x08,0x08,0x17,0x2A,0x82,0x5C,0x31,0x5D,0x46,0x8A, +0x2D,0x0F,0x86,0x9B,0x74,0xD9,0x45,0xFB,0xD4,0x40,0xB1,0x7A,0xAA,0x68,0x2D,0x86, +0xB2,0x99,0x22,0xE1,0xC1,0x2B,0xC7,0x9C,0xF8,0xF3,0x5F,0xA8,0x82,0x12,0xEB,0x19, +0x11,0x2D, +}; + + +/* subject:/C=US/O=GeoTrust Inc./CN=GeoTrust Universal CA */ +/* issuer :/C=US/O=GeoTrust Inc./CN=GeoTrust Universal CA */ + + +const unsigned char GeoTrust_Universal_CA_certificate[1388]={ +0x30,0x82,0x05,0x68,0x30,0x82,0x03,0x50,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x45,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x16, +0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73, +0x74,0x20,0x49,0x6E,0x63,0x2E,0x31,0x1E,0x30,0x1C,0x06,0x03,0x55,0x04,0x03,0x13, +0x15,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x55,0x6E,0x69,0x76,0x65,0x72, +0x73,0x61,0x6C,0x20,0x43,0x41,0x30,0x1E,0x17,0x0D,0x30,0x34,0x30,0x33,0x30,0x34, +0x30,0x35,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x32,0x39,0x30,0x33,0x30,0x34,0x30, +0x35,0x30,0x30,0x30,0x30,0x5A,0x30,0x45,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04, +0x06,0x13,0x02,0x55,0x53,0x31,0x16,0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D, +0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x49,0x6E,0x63,0x2E,0x31,0x1E,0x30, +0x1C,0x06,0x03,0x55,0x04,0x03,0x13,0x15,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74, +0x20,0x55,0x6E,0x69,0x76,0x65,0x72,0x73,0x61,0x6C,0x20,0x43,0x41,0x30,0x82,0x02, +0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00, +0x03,0x82,0x02,0x0F,0x00,0x30,0x82,0x02,0x0A,0x02,0x82,0x02,0x01,0x00,0xA6,0x15, +0x55,0xA0,0xA3,0xC6,0xE0,0x1F,0x8C,0x9D,0x21,0x50,0xD7,0xC1,0xBE,0x2B,0x5B,0xB5, +0xA4,0x9E,0xA1,0xD9,0x72,0x58,0xBD,0x00,0x1B,0x4C,0xBF,0x61,0xC9,0x14,0x1D,0x45, +0x82,0xAB,0xC6,0x1D,0x80,0xD6,0x3D,0xEB,0x10,0x9C,0x3A,0xAF,0x6D,0x24,0xF8,0xBC, +0x71,0x01,0x9E,0x06,0xF5,0x7C,0x5F,0x1E,0xC1,0x0E,0x55,0xCA,0x83,0x9A,0x59,0x30, +0xAE,0x19,0xCB,0x30,0x48,0x95,0xED,0x22,0x37,0x8D,0xF4,0x4A,0x9A,0x72,0x66,0x3E, +0xAD,0x95,0xC0,0xE0,0x16,0x00,0xE0,0x10,0x1F,0x2B,0x31,0x0E,0xD7,0x94,0x54,0xD3, +0x42,0x33,0xA0,0x34,0x1D,0x1E,0x45,0x76,0xDD,0x4F,0xCA,0x18,0x37,0xEC,0x85,0x15, +0x7A,0x19,0x08,0xFC,0xD5,0xC7,0x9C,0xF0,0xF2,0xA9,0x2E,0x10,0xA9,0x92,0xE6,0x3D, +0x58,0x3D,0xA9,0x16,0x68,0x3C,0x2F,0x75,0x21,0x18,0x7F,0x28,0x77,0xA5,0xE1,0x61, +0x17,0xB7,0xA6,0xE9,0xF8,0x1E,0x99,0xDB,0x73,0x6E,0xF4,0x0A,0xA2,0x21,0x6C,0xEE, +0xDA,0xAA,0x85,0x92,0x66,0xAF,0xF6,0x7A,0x6B,0x82,0xDA,0xBA,0x22,0x08,0x35,0x0F, +0xCF,0x42,0xF1,0x35,0xFA,0x6A,0xEE,0x7E,0x2B,0x25,0xCC,0x3A,0x11,0xE4,0x6D,0xAF, +0x73,0xB2,0x76,0x1D,0xAD,0xD0,0xB2,0x78,0x67,0x1A,0xA4,0x39,0x1C,0x51,0x0B,0x67, +0x56,0x83,0xFD,0x38,0x5D,0x0D,0xCE,0xDD,0xF0,0xBB,0x2B,0x96,0x1F,0xDE,0x7B,0x32, +0x52,0xFD,0x1D,0xBB,0xB5,0x06,0xA1,0xB2,0x21,0x5E,0xA5,0xD6,0x95,0x68,0x7F,0xF0, +0x99,0x9E,0xDC,0x45,0x08,0x3E,0xE7,0xD2,0x09,0x0D,0x35,0x94,0xDD,0x80,0x4E,0x53, +0x97,0xD7,0xB5,0x09,0x44,0x20,0x64,0x16,0x17,0x03,0x02,0x4C,0x53,0x0D,0x68,0xDE, +0xD5,0xAA,0x72,0x4D,0x93,0x6D,0x82,0x0E,0xDB,0x9C,0xBD,0xCF,0xB4,0xF3,0x5C,0x5D, +0x54,0x7A,0x69,0x09,0x96,0xD6,0xDB,0x11,0xC1,0x8D,0x75,0xA8,0xB4,0xCF,0x39,0xC8, +0xCE,0x3C,0xBC,0x24,0x7C,0xE6,0x62,0xCA,0xE1,0xBD,0x7D,0xA7,0xBD,0x57,0x65,0x0B, +0xE4,0xFE,0x25,0xED,0xB6,0x69,0x10,0xDC,0x28,0x1A,0x46,0xBD,0x01,0x1D,0xD0,0x97, +0xB5,0xE1,0x98,0x3B,0xC0,0x37,0x64,0xD6,0x3D,0x94,0xEE,0x0B,0xE1,0xF5,0x28,0xAE, +0x0B,0x56,0xBF,0x71,0x8B,0x23,0x29,0x41,0x8E,0x86,0xC5,0x4B,0x52,0x7B,0xD8,0x71, +0xAB,0x1F,0x8A,0x15,0xA6,0x3B,0x83,0x5A,0xD7,0x58,0x01,0x51,0xC6,0x4C,0x41,0xD9, +0x7F,0xD8,0x41,0x67,0x72,0xA2,0x28,0xDF,0x60,0x83,0xA9,0x9E,0xC8,0x7B,0xFC,0x53, +0x73,0x72,0x59,0xF5,0x93,0x7A,0x17,0x76,0x0E,0xCE,0xF7,0xE5,0x5C,0xD9,0x0B,0x55, +0x34,0xA2,0xAA,0x5B,0xB5,0x6A,0x54,0xE7,0x13,0xCA,0x57,0xEC,0x97,0x6D,0xF4,0x5E, +0x06,0x2F,0x45,0x8B,0x58,0xD4,0x23,0x16,0x92,0xE4,0x16,0x6E,0x28,0x63,0x59,0x30, +0xDF,0x50,0x01,0x9C,0x63,0x89,0x1A,0x9F,0xDB,0x17,0x94,0x82,0x70,0x37,0xC3,0x24, +0x9E,0x9A,0x47,0xD6,0x5A,0xCA,0x4E,0xA8,0x69,0x89,0x72,0x1F,0x91,0x6C,0xDB,0x7E, +0x9E,0x1B,0xAD,0xC7,0x1F,0x73,0xDD,0x2C,0x4F,0x19,0x65,0xFD,0x7F,0x93,0x40,0x10, +0x2E,0xD2,0xF0,0xED,0x3C,0x9E,0x2E,0x28,0x3E,0x69,0x26,0x33,0xC5,0x7B,0x02,0x03, +0x01,0x00,0x01,0xA3,0x63,0x30,0x61,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01, +0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04, +0x16,0x04,0x14,0xDA,0xBB,0x2E,0xAA,0xB0,0x0C,0xB8,0x88,0x26,0x51,0x74,0x5C,0x6D, +0x03,0xD3,0xC0,0xD8,0x8F,0x7A,0xD6,0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18, +0x30,0x16,0x80,0x14,0xDA,0xBB,0x2E,0xAA,0xB0,0x0C,0xB8,0x88,0x26,0x51,0x74,0x5C, +0x6D,0x03,0xD3,0xC0,0xD8,0x8F,0x7A,0xD6,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01, +0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x86,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x02,0x01,0x00,0x31,0x78,0xE6,0xC7, +0xB5,0xDF,0xB8,0x94,0x40,0xC9,0x71,0xC4,0xA8,0x35,0xEC,0x46,0x1D,0xC2,0x85,0xF3, +0x28,0x58,0x86,0xB0,0x0B,0xFC,0x8E,0xB2,0x39,0x8F,0x44,0x55,0xAB,0x64,0x84,0x5C, +0x69,0xA9,0xD0,0x9A,0x38,0x3C,0xFA,0xE5,0x1F,0x35,0xE5,0x44,0xE3,0x80,0x79,0x94, +0x68,0xA4,0xBB,0xC4,0x9F,0x3D,0xE1,0x34,0xCD,0x30,0x46,0x8B,0x54,0x2B,0x95,0xA5, +0xEF,0xF7,0x3F,0x99,0x84,0xFD,0x35,0xE6,0xCF,0x31,0xC6,0xDC,0x6A,0xBF,0xA7,0xD7, +0x23,0x08,0xE1,0x98,0x5E,0xC3,0x5A,0x08,0x76,0xA9,0xA6,0xAF,0x77,0x2F,0xB7,0x60, +0xBD,0x44,0x46,0x6A,0xEF,0x97,0xFF,0x73,0x95,0xC1,0x8E,0xE8,0x93,0xFB,0xFD,0x31, +0xB7,0xEC,0x57,0x11,0x11,0x45,0x9B,0x30,0xF1,0x1A,0x88,0x39,0xC1,0x4F,0x3C,0xA7, +0x00,0xD5,0xC7,0xFC,0xAB,0x6D,0x80,0x22,0x70,0xA5,0x0C,0xE0,0x5D,0x04,0x29,0x02, +0xFB,0xCB,0xA0,0x91,0xD1,0x7C,0xD6,0xC3,0x7E,0x50,0xD5,0x9D,0x58,0xBE,0x41,0x38, +0xEB,0xB9,0x75,0x3C,0x15,0xD9,0x9B,0xC9,0x4A,0x83,0x59,0xC0,0xDA,0x53,0xFD,0x33, +0xBB,0x36,0x18,0x9B,0x85,0x0F,0x15,0xDD,0xEE,0x2D,0xAC,0x76,0x93,0xB9,0xD9,0x01, +0x8D,0x48,0x10,0xA8,0xFB,0xF5,0x38,0x86,0xF1,0xDB,0x0A,0xC6,0xBD,0x84,0xA3,0x23, +0x41,0xDE,0xD6,0x77,0x6F,0x85,0xD4,0x85,0x1C,0x50,0xE0,0xAE,0x51,0x8A,0xBA,0x8D, +0x3E,0x76,0xE2,0xB9,0xCA,0x27,0xF2,0x5F,0x9F,0xEF,0x6E,0x59,0x0D,0x06,0xD8,0x2B, +0x17,0xA4,0xD2,0x7C,0x6B,0xBB,0x5F,0x14,0x1A,0x48,0x8F,0x1A,0x4C,0xE7,0xB3,0x47, +0x1C,0x8E,0x4C,0x45,0x2B,0x20,0xEE,0x48,0xDF,0xE7,0xDD,0x09,0x8E,0x18,0xA8,0xDA, +0x40,0x8D,0x92,0x26,0x11,0x53,0x61,0x73,0x5D,0xEB,0xBD,0xE7,0xC4,0x4D,0x29,0x37, +0x61,0xEB,0xAC,0x39,0x2D,0x67,0x2E,0x16,0xD6,0xF5,0x00,0x83,0x85,0xA1,0xCC,0x7F, +0x76,0xC4,0x7D,0xE4,0xB7,0x4B,0x66,0xEF,0x03,0x45,0x60,0x69,0xB6,0x0C,0x52,0x96, +0x92,0x84,0x5E,0xA6,0xA3,0xB5,0xA4,0x3E,0x2B,0xD9,0xCC,0xD8,0x1B,0x47,0xAA,0xF2, +0x44,0xDA,0x4F,0xF9,0x03,0xE8,0xF0,0x14,0xCB,0x3F,0xF3,0x83,0xDE,0xD0,0xC1,0x54, +0xE3,0xB7,0xE8,0x0A,0x37,0x4D,0x8B,0x20,0x59,0x03,0x30,0x19,0xA1,0x2C,0xC8,0xBD, +0x11,0x1F,0xDF,0xAE,0xC9,0x4A,0xC5,0xF3,0x27,0x66,0x66,0x86,0xAC,0x68,0x91,0xFF, +0xD9,0xE6,0x53,0x1C,0x0F,0x8B,0x5C,0x69,0x65,0x0A,0x26,0xC8,0x1E,0x34,0xC3,0x5D, +0x51,0x7B,0xD7,0xA9,0x9C,0x06,0xA1,0x36,0xDD,0xD5,0x89,0x94,0xBC,0xD9,0xE4,0x2D, +0x0C,0x5E,0x09,0x6C,0x08,0x97,0x7C,0xA3,0x3D,0x7C,0x93,0xFF,0x3F,0xA1,0x14,0xA7, +0xCF,0xB5,0x5D,0xEB,0xDB,0xDB,0x1C,0xC4,0x76,0xDF,0x88,0xB9,0xBD,0x45,0x05,0x95, +0x1B,0xAE,0xFC,0x46,0x6A,0x4C,0xAF,0x48,0xE3,0xCE,0xAE,0x0F,0xD2,0x7E,0xEB,0xE6, +0x6C,0x9C,0x4F,0x81,0x6A,0x7A,0x64,0xAC,0xBB,0x3E,0xD5,0xE7,0xCB,0x76,0x2E,0xC5, +0xA7,0x48,0xC1,0x5C,0x90,0x0F,0xCB,0xC8,0x3F,0xFA,0xE6,0x32,0xE1,0x8D,0x1B,0x6F, +0xA4,0xE6,0x8E,0xD8,0xF9,0x29,0x48,0x8A,0xCE,0x73,0xFE,0x2C, +}; + + +/* subject:/C=US/O=GeoTrust Inc./CN=GeoTrust Universal CA 2 */ +/* issuer :/C=US/O=GeoTrust Inc./CN=GeoTrust Universal CA 2 */ + + +const unsigned char GeoTrust_Universal_CA_2_certificate[1392]={ +0x30,0x82,0x05,0x6C,0x30,0x82,0x03,0x54,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x47,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x16, +0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73, +0x74,0x20,0x49,0x6E,0x63,0x2E,0x31,0x20,0x30,0x1E,0x06,0x03,0x55,0x04,0x03,0x13, +0x17,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x55,0x6E,0x69,0x76,0x65,0x72, +0x73,0x61,0x6C,0x20,0x43,0x41,0x20,0x32,0x30,0x1E,0x17,0x0D,0x30,0x34,0x30,0x33, +0x30,0x34,0x30,0x35,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x32,0x39,0x30,0x33,0x30, +0x34,0x30,0x35,0x30,0x30,0x30,0x30,0x5A,0x30,0x47,0x31,0x0B,0x30,0x09,0x06,0x03, +0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x16,0x30,0x14,0x06,0x03,0x55,0x04,0x0A, +0x13,0x0D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x49,0x6E,0x63,0x2E,0x31, +0x20,0x30,0x1E,0x06,0x03,0x55,0x04,0x03,0x13,0x17,0x47,0x65,0x6F,0x54,0x72,0x75, +0x73,0x74,0x20,0x55,0x6E,0x69,0x76,0x65,0x72,0x73,0x61,0x6C,0x20,0x43,0x41,0x20, +0x32,0x30,0x82,0x02,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01, +0x01,0x01,0x05,0x00,0x03,0x82,0x02,0x0F,0x00,0x30,0x82,0x02,0x0A,0x02,0x82,0x02, +0x01,0x00,0xB3,0x54,0x52,0xC1,0xC9,0x3E,0xF2,0xD9,0xDC,0xB1,0x53,0x1A,0x59,0x29, +0xE7,0xB1,0xC3,0x45,0x28,0xE5,0xD7,0xD1,0xED,0xC5,0xC5,0x4B,0xA1,0xAA,0x74,0x7B, +0x57,0xAF,0x4A,0x26,0xFC,0xD8,0xF5,0x5E,0xA7,0x6E,0x19,0xDB,0x74,0x0C,0x4F,0x35, +0x5B,0x32,0x0B,0x01,0xE3,0xDB,0xEB,0x7A,0x77,0x35,0xEA,0xAA,0x5A,0xE0,0xD6,0xE8, +0xA1,0x57,0x94,0xF0,0x90,0xA3,0x74,0x56,0x94,0x44,0x30,0x03,0x1E,0x5C,0x4E,0x2B, +0x85,0x26,0x74,0x82,0x7A,0x0C,0x76,0xA0,0x6F,0x4D,0xCE,0x41,0x2D,0xA0,0x15,0x06, +0x14,0x5F,0xB7,0x42,0xCD,0x7B,0x8F,0x58,0x61,0x34,0xDC,0x2A,0x08,0xF9,0x2E,0xC3, +0x01,0xA6,0x22,0x44,0x1C,0x4C,0x07,0x82,0xE6,0x5B,0xCE,0xD0,0x4A,0x7C,0x04,0xD3, +0x19,0x73,0x27,0xF0,0xAA,0x98,0x7F,0x2E,0xAF,0x4E,0xEB,0x87,0x1E,0x24,0x77,0x6A, +0x5D,0xB6,0xE8,0x5B,0x45,0xBA,0xDC,0xC3,0xA1,0x05,0x6F,0x56,0x8E,0x8F,0x10,0x26, +0xA5,0x49,0xC3,0x2E,0xD7,0x41,0x87,0x22,0xE0,0x4F,0x86,0xCA,0x60,0xB5,0xEA,0xA1, +0x63,0xC0,0x01,0x97,0x10,0x79,0xBD,0x00,0x3C,0x12,0x6D,0x2B,0x15,0xB1,0xAC,0x4B, +0xB1,0xEE,0x18,0xB9,0x4E,0x96,0xDC,0xDC,0x76,0xFF,0x3B,0xBE,0xCF,0x5F,0x03,0xC0, +0xFC,0x3B,0xE8,0xBE,0x46,0x1B,0xFF,0xDA,0x40,0xC2,0x52,0xF7,0xFE,0xE3,0x3A,0xF7, +0x6A,0x77,0x35,0xD0,0xDA,0x8D,0xEB,0x5E,0x18,0x6A,0x31,0xC7,0x1E,0xBA,0x3C,0x1B, +0x28,0xD6,0x6B,0x54,0xC6,0xAA,0x5B,0xD7,0xA2,0x2C,0x1B,0x19,0xCC,0xA2,0x02,0xF6, +0x9B,0x59,0xBD,0x37,0x6B,0x86,0xB5,0x6D,0x82,0xBA,0xD8,0xEA,0xC9,0x56,0xBC,0xA9, +0x36,0x58,0xFD,0x3E,0x19,0xF3,0xED,0x0C,0x26,0xA9,0x93,0x38,0xF8,0x4F,0xC1,0x5D, +0x22,0x06,0xD0,0x97,0xEA,0xE1,0xAD,0xC6,0x55,0xE0,0x81,0x2B,0x28,0x83,0x3A,0xFA, +0xF4,0x7B,0x21,0x51,0x00,0xBE,0x52,0x38,0xCE,0xCD,0x66,0x79,0xA8,0xF4,0x81,0x56, +0xE2,0xD0,0x83,0x09,0x47,0x51,0x5B,0x50,0x6A,0xCF,0xDB,0x48,0x1A,0x5D,0x3E,0xF7, +0xCB,0xF6,0x65,0xF7,0x6C,0xF1,0x95,0xF8,0x02,0x3B,0x32,0x56,0x82,0x39,0x7A,0x5B, +0xBD,0x2F,0x89,0x1B,0xBF,0xA1,0xB4,0xE8,0xFF,0x7F,0x8D,0x8C,0xDF,0x03,0xF1,0x60, +0x4E,0x58,0x11,0x4C,0xEB,0xA3,0x3F,0x10,0x2B,0x83,0x9A,0x01,0x73,0xD9,0x94,0x6D, +0x84,0x00,0x27,0x66,0xAC,0xF0,0x70,0x40,0x09,0x42,0x92,0xAD,0x4F,0x93,0x0D,0x61, +0x09,0x51,0x24,0xD8,0x92,0xD5,0x0B,0x94,0x61,0xB2,0x87,0xB2,0xED,0xFF,0x9A,0x35, +0xFF,0x85,0x54,0xCA,0xED,0x44,0x43,0xAC,0x1B,0x3C,0x16,0x6B,0x48,0x4A,0x0A,0x1C, +0x40,0x88,0x1F,0x92,0xC2,0x0B,0x00,0x05,0xFF,0xF2,0xC8,0x02,0x4A,0xA4,0xAA,0xA9, +0xCC,0x99,0x96,0x9C,0x2F,0x58,0xE0,0x7D,0xE1,0xBE,0xBB,0x07,0xDC,0x5F,0x04,0x72, +0x5C,0x31,0x34,0xC3,0xEC,0x5F,0x2D,0xE0,0x3D,0x64,0x90,0x22,0xE6,0xD1,0xEC,0xB8, +0x2E,0xDD,0x59,0xAE,0xD9,0xA1,0x37,0xBF,0x54,0x35,0xDC,0x73,0x32,0x4F,0x8C,0x04, +0x1E,0x33,0xB2,0xC9,0x46,0xF1,0xD8,0x5C,0xC8,0x55,0x50,0xC9,0x68,0xBD,0xA8,0xBA, +0x36,0x09,0x02,0x03,0x01,0x00,0x01,0xA3,0x63,0x30,0x61,0x30,0x0F,0x06,0x03,0x55, +0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x1D,0x06,0x03, +0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x76,0xF3,0x55,0xE1,0xFA,0xA4,0x36,0xFB,0xF0, +0x9F,0x5C,0x62,0x71,0xED,0x3C,0xF4,0x47,0x38,0x10,0x2B,0x30,0x1F,0x06,0x03,0x55, +0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14,0x76,0xF3,0x55,0xE1,0xFA,0xA4,0x36,0xFB, +0xF0,0x9F,0x5C,0x62,0x71,0xED,0x3C,0xF4,0x47,0x38,0x10,0x2B,0x30,0x0E,0x06,0x03, +0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x86,0x30,0x0D,0x06,0x09, +0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x02,0x01,0x00, +0x66,0xC1,0xC6,0x23,0xF3,0xD9,0xE0,0x2E,0x6E,0x5F,0xE8,0xCF,0xAE,0xB0,0xB0,0x25, +0x4D,0x2B,0xF8,0x3B,0x58,0x9B,0x40,0x24,0x37,0x5A,0xCB,0xAB,0x16,0x49,0xFF,0xB3, +0x75,0x79,0x33,0xA1,0x2F,0x6D,0x70,0x17,0x34,0x91,0xFE,0x67,0x7E,0x8F,0xEC,0x9B, +0xE5,0x5E,0x82,0xA9,0x55,0x1F,0x2F,0xDC,0xD4,0x51,0x07,0x12,0xFE,0xAC,0x16,0x3E, +0x2C,0x35,0xC6,0x63,0xFC,0xDC,0x10,0xEB,0x0D,0xA3,0xAA,0xD0,0x7C,0xCC,0xD1,0xD0, +0x2F,0x51,0x2E,0xC4,0x14,0x5A,0xDE,0xE8,0x19,0xE1,0x3E,0xC6,0xCC,0xA4,0x29,0xE7, +0x2E,0x84,0xAA,0x06,0x30,0x78,0x76,0x54,0x73,0x28,0x98,0x59,0x38,0xE0,0x00,0x0D, +0x62,0xD3,0x42,0x7D,0x21,0x9F,0xAE,0x3D,0x3A,0x8C,0xD5,0xFA,0x77,0x0D,0x18,0x2B, +0x16,0x0E,0x5F,0x36,0xE1,0xFC,0x2A,0xB5,0x30,0x24,0xCF,0xE0,0x63,0x0C,0x7B,0x58, +0x1A,0xFE,0x99,0xBA,0x42,0x12,0xB1,0x91,0xF4,0x7C,0x68,0xE2,0xC8,0xE8,0xAF,0x2C, +0xEA,0xC9,0x7E,0xAE,0xBB,0x2A,0x3D,0x0D,0x15,0xDC,0x34,0x95,0xB6,0x18,0x74,0xA8, +0x6A,0x0F,0xC7,0xB4,0xF4,0x13,0xC4,0xE4,0x5B,0xED,0x0A,0xD2,0xA4,0x97,0x4C,0x2A, +0xED,0x2F,0x6C,0x12,0x89,0x3D,0xF1,0x27,0x70,0xAA,0x6A,0x03,0x52,0x21,0x9F,0x40, +0xA8,0x67,0x50,0xF2,0xF3,0x5A,0x1F,0xDF,0xDF,0x23,0xF6,0xDC,0x78,0x4E,0xE6,0x98, +0x4F,0x55,0x3A,0x53,0xE3,0xEF,0xF2,0xF4,0x9F,0xC7,0x7C,0xD8,0x58,0xAF,0x29,0x22, +0x97,0xB8,0xE0,0xBD,0x91,0x2E,0xB0,0x76,0xEC,0x57,0x11,0xCF,0xEF,0x29,0x44,0xF3, +0xE9,0x85,0x7A,0x60,0x63,0xE4,0x5D,0x33,0x89,0x17,0xD9,0x31,0xAA,0xDA,0xD6,0xF3, +0x18,0x35,0x72,0xCF,0x87,0x2B,0x2F,0x63,0x23,0x84,0x5D,0x84,0x8C,0x3F,0x57,0xA0, +0x88,0xFC,0x99,0x91,0x28,0x26,0x69,0x99,0xD4,0x8F,0x97,0x44,0xBE,0x8E,0xD5,0x48, +0xB1,0xA4,0x28,0x29,0xF1,0x15,0xB4,0xE1,0xE5,0x9E,0xDD,0xF8,0x8F,0xA6,0x6F,0x26, +0xD7,0x09,0x3C,0x3A,0x1C,0x11,0x0E,0xA6,0x6C,0x37,0xF7,0xAD,0x44,0x87,0x2C,0x28, +0xC7,0xD8,0x74,0x82,0xB3,0xD0,0x6F,0x4A,0x57,0xBB,0x35,0x29,0x27,0xA0,0x8B,0xE8, +0x21,0xA7,0x87,0x64,0x36,0x5D,0xCC,0xD8,0x16,0xAC,0xC7,0xB2,0x27,0x40,0x92,0x55, +0x38,0x28,0x8D,0x51,0x6E,0xDD,0x14,0x67,0x53,0x6C,0x71,0x5C,0x26,0x84,0x4D,0x75, +0x5A,0xB6,0x7E,0x60,0x56,0xA9,0x4D,0xAD,0xFB,0x9B,0x1E,0x97,0xF3,0x0D,0xD9,0xD2, +0x97,0x54,0x77,0xDA,0x3D,0x12,0xB7,0xE0,0x1E,0xEF,0x08,0x06,0xAC,0xF9,0x85,0x87, +0xE9,0xA2,0xDC,0xAF,0x7E,0x18,0x12,0x83,0xFD,0x56,0x17,0x41,0x2E,0xD5,0x29,0x82, +0x7D,0x99,0xF4,0x31,0xF6,0x71,0xA9,0xCF,0x2C,0x01,0x27,0xA5,0x05,0xB9,0xAA,0xB2, +0x48,0x4E,0x2A,0xEF,0x9F,0x93,0x52,0x51,0x95,0x3C,0x52,0x73,0x8E,0x56,0x4C,0x17, +0x40,0xC0,0x09,0x28,0xE4,0x8B,0x6A,0x48,0x53,0xDB,0xEC,0xCD,0x55,0x55,0xF1,0xC6, +0xF8,0xE9,0xA2,0x2C,0x4C,0xA6,0xD1,0x26,0x5F,0x7E,0xAF,0x5A,0x4C,0xDA,0x1F,0xA6, +0xF2,0x1C,0x2C,0x7E,0xAE,0x02,0x16,0xD2,0x56,0xD0,0x2F,0x57,0x53,0x47,0xE8,0x92, +}; + + +/* subject:/C=BE/O=GlobalSign nv-sa/OU=Root CA/CN=GlobalSign Root CA */ +/* issuer :/C=BE/O=GlobalSign nv-sa/OU=Root CA/CN=GlobalSign Root CA */ + + +const unsigned char GlobalSign_Root_CA_certificate[889]={ +0x30,0x82,0x03,0x75,0x30,0x82,0x02,0x5D,0xA0,0x03,0x02,0x01,0x02,0x02,0x0B,0x04, +0x00,0x00,0x00,0x00,0x01,0x15,0x4B,0x5A,0xC3,0x94,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x57,0x31,0x0B,0x30,0x09,0x06, +0x03,0x55,0x04,0x06,0x13,0x02,0x42,0x45,0x31,0x19,0x30,0x17,0x06,0x03,0x55,0x04, +0x0A,0x13,0x10,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x53,0x69,0x67,0x6E,0x20,0x6E,0x76, +0x2D,0x73,0x61,0x31,0x10,0x30,0x0E,0x06,0x03,0x55,0x04,0x0B,0x13,0x07,0x52,0x6F, +0x6F,0x74,0x20,0x43,0x41,0x31,0x1B,0x30,0x19,0x06,0x03,0x55,0x04,0x03,0x13,0x12, +0x47,0x6C,0x6F,0x62,0x61,0x6C,0x53,0x69,0x67,0x6E,0x20,0x52,0x6F,0x6F,0x74,0x20, +0x43,0x41,0x30,0x1E,0x17,0x0D,0x39,0x38,0x30,0x39,0x30,0x31,0x31,0x32,0x30,0x30, +0x30,0x30,0x5A,0x17,0x0D,0x32,0x38,0x30,0x31,0x32,0x38,0x31,0x32,0x30,0x30,0x30, +0x30,0x5A,0x30,0x57,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x42, +0x45,0x31,0x19,0x30,0x17,0x06,0x03,0x55,0x04,0x0A,0x13,0x10,0x47,0x6C,0x6F,0x62, +0x61,0x6C,0x53,0x69,0x67,0x6E,0x20,0x6E,0x76,0x2D,0x73,0x61,0x31,0x10,0x30,0x0E, +0x06,0x03,0x55,0x04,0x0B,0x13,0x07,0x52,0x6F,0x6F,0x74,0x20,0x43,0x41,0x31,0x1B, +0x30,0x19,0x06,0x03,0x55,0x04,0x03,0x13,0x12,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x53, +0x69,0x67,0x6E,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x41,0x30,0x82,0x01,0x22,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82, +0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xDA,0x0E,0xE6,0x99, +0x8D,0xCE,0xA3,0xE3,0x4F,0x8A,0x7E,0xFB,0xF1,0x8B,0x83,0x25,0x6B,0xEA,0x48,0x1F, +0xF1,0x2A,0xB0,0xB9,0x95,0x11,0x04,0xBD,0xF0,0x63,0xD1,0xE2,0x67,0x66,0xCF,0x1C, +0xDD,0xCF,0x1B,0x48,0x2B,0xEE,0x8D,0x89,0x8E,0x9A,0xAF,0x29,0x80,0x65,0xAB,0xE9, +0xC7,0x2D,0x12,0xCB,0xAB,0x1C,0x4C,0x70,0x07,0xA1,0x3D,0x0A,0x30,0xCD,0x15,0x8D, +0x4F,0xF8,0xDD,0xD4,0x8C,0x50,0x15,0x1C,0xEF,0x50,0xEE,0xC4,0x2E,0xF7,0xFC,0xE9, +0x52,0xF2,0x91,0x7D,0xE0,0x6D,0xD5,0x35,0x30,0x8E,0x5E,0x43,0x73,0xF2,0x41,0xE9, +0xD5,0x6A,0xE3,0xB2,0x89,0x3A,0x56,0x39,0x38,0x6F,0x06,0x3C,0x88,0x69,0x5B,0x2A, +0x4D,0xC5,0xA7,0x54,0xB8,0x6C,0x89,0xCC,0x9B,0xF9,0x3C,0xCA,0xE5,0xFD,0x89,0xF5, +0x12,0x3C,0x92,0x78,0x96,0xD6,0xDC,0x74,0x6E,0x93,0x44,0x61,0xD1,0x8D,0xC7,0x46, +0xB2,0x75,0x0E,0x86,0xE8,0x19,0x8A,0xD5,0x6D,0x6C,0xD5,0x78,0x16,0x95,0xA2,0xE9, +0xC8,0x0A,0x38,0xEB,0xF2,0x24,0x13,0x4F,0x73,0x54,0x93,0x13,0x85,0x3A,0x1B,0xBC, +0x1E,0x34,0xB5,0x8B,0x05,0x8C,0xB9,0x77,0x8B,0xB1,0xDB,0x1F,0x20,0x91,0xAB,0x09, +0x53,0x6E,0x90,0xCE,0x7B,0x37,0x74,0xB9,0x70,0x47,0x91,0x22,0x51,0x63,0x16,0x79, +0xAE,0xB1,0xAE,0x41,0x26,0x08,0xC8,0x19,0x2B,0xD1,0x46,0xAA,0x48,0xD6,0x64,0x2A, +0xD7,0x83,0x34,0xFF,0x2C,0x2A,0xC1,0x6C,0x19,0x43,0x4A,0x07,0x85,0xE7,0xD3,0x7C, +0xF6,0x21,0x68,0xEF,0xEA,0xF2,0x52,0x9F,0x7F,0x93,0x90,0xCF,0x02,0x03,0x01,0x00, +0x01,0xA3,0x42,0x30,0x40,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04, +0x04,0x03,0x02,0x01,0x06,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04, +0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04, +0x14,0x60,0x7B,0x66,0x1A,0x45,0x0D,0x97,0xCA,0x89,0x50,0x2F,0x7D,0x04,0xCD,0x34, +0xA8,0xFF,0xFC,0xFD,0x4B,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01, +0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0xD6,0x73,0xE7,0x7C,0x4F,0x76,0xD0, +0x8D,0xBF,0xEC,0xBA,0xA2,0xBE,0x34,0xC5,0x28,0x32,0xB5,0x7C,0xFC,0x6C,0x9C,0x2C, +0x2B,0xBD,0x09,0x9E,0x53,0xBF,0x6B,0x5E,0xAA,0x11,0x48,0xB6,0xE5,0x08,0xA3,0xB3, +0xCA,0x3D,0x61,0x4D,0xD3,0x46,0x09,0xB3,0x3E,0xC3,0xA0,0xE3,0x63,0x55,0x1B,0xF2, +0xBA,0xEF,0xAD,0x39,0xE1,0x43,0xB9,0x38,0xA3,0xE6,0x2F,0x8A,0x26,0x3B,0xEF,0xA0, +0x50,0x56,0xF9,0xC6,0x0A,0xFD,0x38,0xCD,0xC4,0x0B,0x70,0x51,0x94,0x97,0x98,0x04, +0xDF,0xC3,0x5F,0x94,0xD5,0x15,0xC9,0x14,0x41,0x9C,0xC4,0x5D,0x75,0x64,0x15,0x0D, +0xFF,0x55,0x30,0xEC,0x86,0x8F,0xFF,0x0D,0xEF,0x2C,0xB9,0x63,0x46,0xF6,0xAA,0xFC, +0xDF,0xBC,0x69,0xFD,0x2E,0x12,0x48,0x64,0x9A,0xE0,0x95,0xF0,0xA6,0xEF,0x29,0x8F, +0x01,0xB1,0x15,0xB5,0x0C,0x1D,0xA5,0xFE,0x69,0x2C,0x69,0x24,0x78,0x1E,0xB3,0xA7, +0x1C,0x71,0x62,0xEE,0xCA,0xC8,0x97,0xAC,0x17,0x5D,0x8A,0xC2,0xF8,0x47,0x86,0x6E, +0x2A,0xC4,0x56,0x31,0x95,0xD0,0x67,0x89,0x85,0x2B,0xF9,0x6C,0xA6,0x5D,0x46,0x9D, +0x0C,0xAA,0x82,0xE4,0x99,0x51,0xDD,0x70,0xB7,0xDB,0x56,0x3D,0x61,0xE4,0x6A,0xE1, +0x5C,0xD6,0xF6,0xFE,0x3D,0xDE,0x41,0xCC,0x07,0xAE,0x63,0x52,0xBF,0x53,0x53,0xF4, +0x2B,0xE9,0xC7,0xFD,0xB6,0xF7,0x82,0x5F,0x85,0xD2,0x41,0x18,0xDB,0x81,0xB3,0x04, +0x1C,0xC5,0x1F,0xA4,0x80,0x6F,0x15,0x20,0xC9,0xDE,0x0C,0x88,0x0A,0x1D,0xD6,0x66, +0x55,0xE2,0xFC,0x48,0xC9,0x29,0x26,0x69,0xE0, +}; + + +/* subject:/OU=GlobalSign Root CA - R2/O=GlobalSign/CN=GlobalSign */ +/* issuer :/OU=GlobalSign Root CA - R2/O=GlobalSign/CN=GlobalSign */ + + +const unsigned char GlobalSign_Root_CA___R2_certificate[958]={ +0x30,0x82,0x03,0xBA,0x30,0x82,0x02,0xA2,0xA0,0x03,0x02,0x01,0x02,0x02,0x0B,0x04, +0x00,0x00,0x00,0x00,0x01,0x0F,0x86,0x26,0xE6,0x0D,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x4C,0x31,0x20,0x30,0x1E,0x06, +0x03,0x55,0x04,0x0B,0x13,0x17,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x53,0x69,0x67,0x6E, +0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x41,0x20,0x2D,0x20,0x52,0x32,0x31,0x13,0x30, +0x11,0x06,0x03,0x55,0x04,0x0A,0x13,0x0A,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x53,0x69, +0x67,0x6E,0x31,0x13,0x30,0x11,0x06,0x03,0x55,0x04,0x03,0x13,0x0A,0x47,0x6C,0x6F, +0x62,0x61,0x6C,0x53,0x69,0x67,0x6E,0x30,0x1E,0x17,0x0D,0x30,0x36,0x31,0x32,0x31, +0x35,0x30,0x38,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x32,0x31,0x31,0x32,0x31,0x35, +0x30,0x38,0x30,0x30,0x30,0x30,0x5A,0x30,0x4C,0x31,0x20,0x30,0x1E,0x06,0x03,0x55, +0x04,0x0B,0x13,0x17,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x53,0x69,0x67,0x6E,0x20,0x52, +0x6F,0x6F,0x74,0x20,0x43,0x41,0x20,0x2D,0x20,0x52,0x32,0x31,0x13,0x30,0x11,0x06, +0x03,0x55,0x04,0x0A,0x13,0x0A,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x53,0x69,0x67,0x6E, +0x31,0x13,0x30,0x11,0x06,0x03,0x55,0x04,0x03,0x13,0x0A,0x47,0x6C,0x6F,0x62,0x61, +0x6C,0x53,0x69,0x67,0x6E,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48, +0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01, +0x0A,0x02,0x82,0x01,0x01,0x00,0xA6,0xCF,0x24,0x0E,0xBE,0x2E,0x6F,0x28,0x99,0x45, +0x42,0xC4,0xAB,0x3E,0x21,0x54,0x9B,0x0B,0xD3,0x7F,0x84,0x70,0xFA,0x12,0xB3,0xCB, +0xBF,0x87,0x5F,0xC6,0x7F,0x86,0xD3,0xB2,0x30,0x5C,0xD6,0xFD,0xAD,0xF1,0x7B,0xDC, +0xE5,0xF8,0x60,0x96,0x09,0x92,0x10,0xF5,0xD0,0x53,0xDE,0xFB,0x7B,0x7E,0x73,0x88, +0xAC,0x52,0x88,0x7B,0x4A,0xA6,0xCA,0x49,0xA6,0x5E,0xA8,0xA7,0x8C,0x5A,0x11,0xBC, +0x7A,0x82,0xEB,0xBE,0x8C,0xE9,0xB3,0xAC,0x96,0x25,0x07,0x97,0x4A,0x99,0x2A,0x07, +0x2F,0xB4,0x1E,0x77,0xBF,0x8A,0x0F,0xB5,0x02,0x7C,0x1B,0x96,0xB8,0xC5,0xB9,0x3A, +0x2C,0xBC,0xD6,0x12,0xB9,0xEB,0x59,0x7D,0xE2,0xD0,0x06,0x86,0x5F,0x5E,0x49,0x6A, +0xB5,0x39,0x5E,0x88,0x34,0xEC,0xBC,0x78,0x0C,0x08,0x98,0x84,0x6C,0xA8,0xCD,0x4B, +0xB4,0xA0,0x7D,0x0C,0x79,0x4D,0xF0,0xB8,0x2D,0xCB,0x21,0xCA,0xD5,0x6C,0x5B,0x7D, +0xE1,0xA0,0x29,0x84,0xA1,0xF9,0xD3,0x94,0x49,0xCB,0x24,0x62,0x91,0x20,0xBC,0xDD, +0x0B,0xD5,0xD9,0xCC,0xF9,0xEA,0x27,0x0A,0x2B,0x73,0x91,0xC6,0x9D,0x1B,0xAC,0xC8, +0xCB,0xE8,0xE0,0xA0,0xF4,0x2F,0x90,0x8B,0x4D,0xFB,0xB0,0x36,0x1B,0xF6,0x19,0x7A, +0x85,0xE0,0x6D,0xF2,0x61,0x13,0x88,0x5C,0x9F,0xE0,0x93,0x0A,0x51,0x97,0x8A,0x5A, +0xCE,0xAF,0xAB,0xD5,0xF7,0xAA,0x09,0xAA,0x60,0xBD,0xDC,0xD9,0x5F,0xDF,0x72,0xA9, +0x60,0x13,0x5E,0x00,0x01,0xC9,0x4A,0xFA,0x3F,0xA4,0xEA,0x07,0x03,0x21,0x02,0x8E, +0x82,0xCA,0x03,0xC2,0x9B,0x8F,0x02,0x03,0x01,0x00,0x01,0xA3,0x81,0x9C,0x30,0x81, +0x99,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01, +0x06,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01, +0x01,0xFF,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x9B,0xE2,0x07, +0x57,0x67,0x1C,0x1E,0xC0,0x6A,0x06,0xDE,0x59,0xB4,0x9A,0x2D,0xDF,0xDC,0x19,0x86, +0x2E,0x30,0x36,0x06,0x03,0x55,0x1D,0x1F,0x04,0x2F,0x30,0x2D,0x30,0x2B,0xA0,0x29, +0xA0,0x27,0x86,0x25,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x63,0x72,0x6C,0x2E,0x67, +0x6C,0x6F,0x62,0x61,0x6C,0x73,0x69,0x67,0x6E,0x2E,0x6E,0x65,0x74,0x2F,0x72,0x6F, +0x6F,0x74,0x2D,0x72,0x32,0x2E,0x63,0x72,0x6C,0x30,0x1F,0x06,0x03,0x55,0x1D,0x23, +0x04,0x18,0x30,0x16,0x80,0x14,0x9B,0xE2,0x07,0x57,0x67,0x1C,0x1E,0xC0,0x6A,0x06, +0xDE,0x59,0xB4,0x9A,0x2D,0xDF,0xDC,0x19,0x86,0x2E,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x99,0x81, +0x53,0x87,0x1C,0x68,0x97,0x86,0x91,0xEC,0xE0,0x4A,0xB8,0x44,0x0B,0xAB,0x81,0xAC, +0x27,0x4F,0xD6,0xC1,0xB8,0x1C,0x43,0x78,0xB3,0x0C,0x9A,0xFC,0xEA,0x2C,0x3C,0x6E, +0x61,0x1B,0x4D,0x4B,0x29,0xF5,0x9F,0x05,0x1D,0x26,0xC1,0xB8,0xE9,0x83,0x00,0x62, +0x45,0xB6,0xA9,0x08,0x93,0xB9,0xA9,0x33,0x4B,0x18,0x9A,0xC2,0xF8,0x87,0x88,0x4E, +0xDB,0xDD,0x71,0x34,0x1A,0xC1,0x54,0xDA,0x46,0x3F,0xE0,0xD3,0x2A,0xAB,0x6D,0x54, +0x22,0xF5,0x3A,0x62,0xCD,0x20,0x6F,0xBA,0x29,0x89,0xD7,0xDD,0x91,0xEE,0xD3,0x5C, +0xA2,0x3E,0xA1,0x5B,0x41,0xF5,0xDF,0xE5,0x64,0x43,0x2D,0xE9,0xD5,0x39,0xAB,0xD2, +0xA2,0xDF,0xB7,0x8B,0xD0,0xC0,0x80,0x19,0x1C,0x45,0xC0,0x2D,0x8C,0xE8,0xF8,0x2D, +0xA4,0x74,0x56,0x49,0xC5,0x05,0xB5,0x4F,0x15,0xDE,0x6E,0x44,0x78,0x39,0x87,0xA8, +0x7E,0xBB,0xF3,0x79,0x18,0x91,0xBB,0xF4,0x6F,0x9D,0xC1,0xF0,0x8C,0x35,0x8C,0x5D, +0x01,0xFB,0xC3,0x6D,0xB9,0xEF,0x44,0x6D,0x79,0x46,0x31,0x7E,0x0A,0xFE,0xA9,0x82, +0xC1,0xFF,0xEF,0xAB,0x6E,0x20,0xC4,0x50,0xC9,0x5F,0x9D,0x4D,0x9B,0x17,0x8C,0x0C, +0xE5,0x01,0xC9,0xA0,0x41,0x6A,0x73,0x53,0xFA,0xA5,0x50,0xB4,0x6E,0x25,0x0F,0xFB, +0x4C,0x18,0xF4,0xFD,0x52,0xD9,0x8E,0x69,0xB1,0xE8,0x11,0x0F,0xDE,0x88,0xD8,0xFB, +0x1D,0x49,0xF7,0xAA,0xDE,0x95,0xCF,0x20,0x78,0xC2,0x60,0x12,0xDB,0x25,0x40,0x8C, +0x6A,0xFC,0x7E,0x42,0x38,0x40,0x64,0x12,0xF7,0x9E,0x81,0xE1,0x93,0x2E, +}; + + +/* subject:/OU=GlobalSign Root CA - R3/O=GlobalSign/CN=GlobalSign */ +/* issuer :/OU=GlobalSign Root CA - R3/O=GlobalSign/CN=GlobalSign */ + + +const unsigned char GlobalSign_Root_CA___R3_certificate[867]={ +0x30,0x82,0x03,0x5F,0x30,0x82,0x02,0x47,0xA0,0x03,0x02,0x01,0x02,0x02,0x0B,0x04, +0x00,0x00,0x00,0x00,0x01,0x21,0x58,0x53,0x08,0xA2,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x30,0x4C,0x31,0x20,0x30,0x1E,0x06, +0x03,0x55,0x04,0x0B,0x13,0x17,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x53,0x69,0x67,0x6E, +0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x41,0x20,0x2D,0x20,0x52,0x33,0x31,0x13,0x30, +0x11,0x06,0x03,0x55,0x04,0x0A,0x13,0x0A,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x53,0x69, +0x67,0x6E,0x31,0x13,0x30,0x11,0x06,0x03,0x55,0x04,0x03,0x13,0x0A,0x47,0x6C,0x6F, +0x62,0x61,0x6C,0x53,0x69,0x67,0x6E,0x30,0x1E,0x17,0x0D,0x30,0x39,0x30,0x33,0x31, +0x38,0x31,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x32,0x39,0x30,0x33,0x31,0x38, +0x31,0x30,0x30,0x30,0x30,0x30,0x5A,0x30,0x4C,0x31,0x20,0x30,0x1E,0x06,0x03,0x55, +0x04,0x0B,0x13,0x17,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x53,0x69,0x67,0x6E,0x20,0x52, +0x6F,0x6F,0x74,0x20,0x43,0x41,0x20,0x2D,0x20,0x52,0x33,0x31,0x13,0x30,0x11,0x06, +0x03,0x55,0x04,0x0A,0x13,0x0A,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x53,0x69,0x67,0x6E, +0x31,0x13,0x30,0x11,0x06,0x03,0x55,0x04,0x03,0x13,0x0A,0x47,0x6C,0x6F,0x62,0x61, +0x6C,0x53,0x69,0x67,0x6E,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48, +0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01, +0x0A,0x02,0x82,0x01,0x01,0x00,0xCC,0x25,0x76,0x90,0x79,0x06,0x78,0x22,0x16,0xF5, +0xC0,0x83,0xB6,0x84,0xCA,0x28,0x9E,0xFD,0x05,0x76,0x11,0xC5,0xAD,0x88,0x72,0xFC, +0x46,0x02,0x43,0xC7,0xB2,0x8A,0x9D,0x04,0x5F,0x24,0xCB,0x2E,0x4B,0xE1,0x60,0x82, +0x46,0xE1,0x52,0xAB,0x0C,0x81,0x47,0x70,0x6C,0xDD,0x64,0xD1,0xEB,0xF5,0x2C,0xA3, +0x0F,0x82,0x3D,0x0C,0x2B,0xAE,0x97,0xD7,0xB6,0x14,0x86,0x10,0x79,0xBB,0x3B,0x13, +0x80,0x77,0x8C,0x08,0xE1,0x49,0xD2,0x6A,0x62,0x2F,0x1F,0x5E,0xFA,0x96,0x68,0xDF, +0x89,0x27,0x95,0x38,0x9F,0x06,0xD7,0x3E,0xC9,0xCB,0x26,0x59,0x0D,0x73,0xDE,0xB0, +0xC8,0xE9,0x26,0x0E,0x83,0x15,0xC6,0xEF,0x5B,0x8B,0xD2,0x04,0x60,0xCA,0x49,0xA6, +0x28,0xF6,0x69,0x3B,0xF6,0xCB,0xC8,0x28,0x91,0xE5,0x9D,0x8A,0x61,0x57,0x37,0xAC, +0x74,0x14,0xDC,0x74,0xE0,0x3A,0xEE,0x72,0x2F,0x2E,0x9C,0xFB,0xD0,0xBB,0xBF,0xF5, +0x3D,0x00,0xE1,0x06,0x33,0xE8,0x82,0x2B,0xAE,0x53,0xA6,0x3A,0x16,0x73,0x8C,0xDD, +0x41,0x0E,0x20,0x3A,0xC0,0xB4,0xA7,0xA1,0xE9,0xB2,0x4F,0x90,0x2E,0x32,0x60,0xE9, +0x57,0xCB,0xB9,0x04,0x92,0x68,0x68,0xE5,0x38,0x26,0x60,0x75,0xB2,0x9F,0x77,0xFF, +0x91,0x14,0xEF,0xAE,0x20,0x49,0xFC,0xAD,0x40,0x15,0x48,0xD1,0x02,0x31,0x61,0x19, +0x5E,0xB8,0x97,0xEF,0xAD,0x77,0xB7,0x64,0x9A,0x7A,0xBF,0x5F,0xC1,0x13,0xEF,0x9B, +0x62,0xFB,0x0D,0x6C,0xE0,0x54,0x69,0x16,0xA9,0x03,0xDA,0x6E,0xE9,0x83,0x93,0x71, +0x76,0xC6,0x69,0x85,0x82,0x17,0x02,0x03,0x01,0x00,0x01,0xA3,0x42,0x30,0x40,0x30, +0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30, +0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF, +0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x8F,0xF0,0x4B,0x7F,0xA8, +0x2E,0x45,0x24,0xAE,0x4D,0x50,0xFA,0x63,0x9A,0x8B,0xDE,0xE2,0xDD,0x1B,0xBC,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x03,0x82, +0x01,0x01,0x00,0x4B,0x40,0xDB,0xC0,0x50,0xAA,0xFE,0xC8,0x0C,0xEF,0xF7,0x96,0x54, +0x45,0x49,0xBB,0x96,0x00,0x09,0x41,0xAC,0xB3,0x13,0x86,0x86,0x28,0x07,0x33,0xCA, +0x6B,0xE6,0x74,0xB9,0xBA,0x00,0x2D,0xAE,0xA4,0x0A,0xD3,0xF5,0xF1,0xF1,0x0F,0x8A, +0xBF,0x73,0x67,0x4A,0x83,0xC7,0x44,0x7B,0x78,0xE0,0xAF,0x6E,0x6C,0x6F,0x03,0x29, +0x8E,0x33,0x39,0x45,0xC3,0x8E,0xE4,0xB9,0x57,0x6C,0xAA,0xFC,0x12,0x96,0xEC,0x53, +0xC6,0x2D,0xE4,0x24,0x6C,0xB9,0x94,0x63,0xFB,0xDC,0x53,0x68,0x67,0x56,0x3E,0x83, +0xB8,0xCF,0x35,0x21,0xC3,0xC9,0x68,0xFE,0xCE,0xDA,0xC2,0x53,0xAA,0xCC,0x90,0x8A, +0xE9,0xF0,0x5D,0x46,0x8C,0x95,0xDD,0x7A,0x58,0x28,0x1A,0x2F,0x1D,0xDE,0xCD,0x00, +0x37,0x41,0x8F,0xED,0x44,0x6D,0xD7,0x53,0x28,0x97,0x7E,0xF3,0x67,0x04,0x1E,0x15, +0xD7,0x8A,0x96,0xB4,0xD3,0xDE,0x4C,0x27,0xA4,0x4C,0x1B,0x73,0x73,0x76,0xF4,0x17, +0x99,0xC2,0x1F,0x7A,0x0E,0xE3,0x2D,0x08,0xAD,0x0A,0x1C,0x2C,0xFF,0x3C,0xAB,0x55, +0x0E,0x0F,0x91,0x7E,0x36,0xEB,0xC3,0x57,0x49,0xBE,0xE1,0x2E,0x2D,0x7C,0x60,0x8B, +0xC3,0x41,0x51,0x13,0x23,0x9D,0xCE,0xF7,0x32,0x6B,0x94,0x01,0xA8,0x99,0xE7,0x2C, +0x33,0x1F,0x3A,0x3B,0x25,0xD2,0x86,0x40,0xCE,0x3B,0x2C,0x86,0x78,0xC9,0x61,0x2F, +0x14,0xBA,0xEE,0xDB,0x55,0x6F,0xDF,0x84,0xEE,0x05,0x09,0x4D,0xBD,0x28,0xD8,0x72, +0xCE,0xD3,0x62,0x50,0x65,0x1E,0xEB,0x92,0x97,0x83,0x31,0xD9,0xB3,0xB5,0xCA,0x47, +0x58,0x3F,0x5F, +}; + + +/* subject:/C=US/O=The Go Daddy Group, Inc./OU=Go Daddy Class 2 Certification Authority */ +/* issuer :/C=US/O=The Go Daddy Group, Inc./OU=Go Daddy Class 2 Certification Authority */ + + +const unsigned char Go_Daddy_Class_2_CA_certificate[1028]={ +0x30,0x82,0x04,0x00,0x30,0x82,0x02,0xE8,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x00, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x63,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x21, +0x30,0x1F,0x06,0x03,0x55,0x04,0x0A,0x13,0x18,0x54,0x68,0x65,0x20,0x47,0x6F,0x20, +0x44,0x61,0x64,0x64,0x79,0x20,0x47,0x72,0x6F,0x75,0x70,0x2C,0x20,0x49,0x6E,0x63, +0x2E,0x31,0x31,0x30,0x2F,0x06,0x03,0x55,0x04,0x0B,0x13,0x28,0x47,0x6F,0x20,0x44, +0x61,0x64,0x64,0x79,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x32,0x20,0x43,0x65,0x72, +0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F, +0x72,0x69,0x74,0x79,0x30,0x1E,0x17,0x0D,0x30,0x34,0x30,0x36,0x32,0x39,0x31,0x37, +0x30,0x36,0x32,0x30,0x5A,0x17,0x0D,0x33,0x34,0x30,0x36,0x32,0x39,0x31,0x37,0x30, +0x36,0x32,0x30,0x5A,0x30,0x63,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13, +0x02,0x55,0x53,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x0A,0x13,0x18,0x54,0x68, +0x65,0x20,0x47,0x6F,0x20,0x44,0x61,0x64,0x64,0x79,0x20,0x47,0x72,0x6F,0x75,0x70, +0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x31,0x30,0x2F,0x06,0x03,0x55,0x04,0x0B,0x13, +0x28,0x47,0x6F,0x20,0x44,0x61,0x64,0x64,0x79,0x20,0x43,0x6C,0x61,0x73,0x73,0x20, +0x32,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20, +0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x82,0x01,0x20,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0D, +0x00,0x30,0x82,0x01,0x08,0x02,0x82,0x01,0x01,0x00,0xDE,0x9D,0xD7,0xEA,0x57,0x18, +0x49,0xA1,0x5B,0xEB,0xD7,0x5F,0x48,0x86,0xEA,0xBE,0xDD,0xFF,0xE4,0xEF,0x67,0x1C, +0xF4,0x65,0x68,0xB3,0x57,0x71,0xA0,0x5E,0x77,0xBB,0xED,0x9B,0x49,0xE9,0x70,0x80, +0x3D,0x56,0x18,0x63,0x08,0x6F,0xDA,0xF2,0xCC,0xD0,0x3F,0x7F,0x02,0x54,0x22,0x54, +0x10,0xD8,0xB2,0x81,0xD4,0xC0,0x75,0x3D,0x4B,0x7F,0xC7,0x77,0xC3,0x3E,0x78,0xAB, +0x1A,0x03,0xB5,0x20,0x6B,0x2F,0x6A,0x2B,0xB1,0xC5,0x88,0x7E,0xC4,0xBB,0x1E,0xB0, +0xC1,0xD8,0x45,0x27,0x6F,0xAA,0x37,0x58,0xF7,0x87,0x26,0xD7,0xD8,0x2D,0xF6,0xA9, +0x17,0xB7,0x1F,0x72,0x36,0x4E,0xA6,0x17,0x3F,0x65,0x98,0x92,0xDB,0x2A,0x6E,0x5D, +0xA2,0xFE,0x88,0xE0,0x0B,0xDE,0x7F,0xE5,0x8D,0x15,0xE1,0xEB,0xCB,0x3A,0xD5,0xE2, +0x12,0xA2,0x13,0x2D,0xD8,0x8E,0xAF,0x5F,0x12,0x3D,0xA0,0x08,0x05,0x08,0xB6,0x5C, +0xA5,0x65,0x38,0x04,0x45,0x99,0x1E,0xA3,0x60,0x60,0x74,0xC5,0x41,0xA5,0x72,0x62, +0x1B,0x62,0xC5,0x1F,0x6F,0x5F,0x1A,0x42,0xBE,0x02,0x51,0x65,0xA8,0xAE,0x23,0x18, +0x6A,0xFC,0x78,0x03,0xA9,0x4D,0x7F,0x80,0xC3,0xFA,0xAB,0x5A,0xFC,0xA1,0x40,0xA4, +0xCA,0x19,0x16,0xFE,0xB2,0xC8,0xEF,0x5E,0x73,0x0D,0xEE,0x77,0xBD,0x9A,0xF6,0x79, +0x98,0xBC,0xB1,0x07,0x67,0xA2,0x15,0x0D,0xDD,0xA0,0x58,0xC6,0x44,0x7B,0x0A,0x3E, +0x62,0x28,0x5F,0xBA,0x41,0x07,0x53,0x58,0xCF,0x11,0x7E,0x38,0x74,0xC5,0xF8,0xFF, +0xB5,0x69,0x90,0x8F,0x84,0x74,0xEA,0x97,0x1B,0xAF,0x02,0x01,0x03,0xA3,0x81,0xC0, +0x30,0x81,0xBD,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0xD2,0xC4, +0xB0,0xD2,0x91,0xD4,0x4C,0x11,0x71,0xB3,0x61,0xCB,0x3D,0xA1,0xFE,0xDD,0xA8,0x6A, +0xD4,0xE3,0x30,0x81,0x8D,0x06,0x03,0x55,0x1D,0x23,0x04,0x81,0x85,0x30,0x81,0x82, +0x80,0x14,0xD2,0xC4,0xB0,0xD2,0x91,0xD4,0x4C,0x11,0x71,0xB3,0x61,0xCB,0x3D,0xA1, +0xFE,0xDD,0xA8,0x6A,0xD4,0xE3,0xA1,0x67,0xA4,0x65,0x30,0x63,0x31,0x0B,0x30,0x09, +0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x21,0x30,0x1F,0x06,0x03,0x55, +0x04,0x0A,0x13,0x18,0x54,0x68,0x65,0x20,0x47,0x6F,0x20,0x44,0x61,0x64,0x64,0x79, +0x20,0x47,0x72,0x6F,0x75,0x70,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x31,0x30,0x2F, +0x06,0x03,0x55,0x04,0x0B,0x13,0x28,0x47,0x6F,0x20,0x44,0x61,0x64,0x64,0x79,0x20, +0x43,0x6C,0x61,0x73,0x73,0x20,0x32,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63, +0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x82, +0x01,0x00,0x30,0x0C,0x06,0x03,0x55,0x1D,0x13,0x04,0x05,0x30,0x03,0x01,0x01,0xFF, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03, +0x82,0x01,0x01,0x00,0x32,0x4B,0xF3,0xB2,0xCA,0x3E,0x91,0xFC,0x12,0xC6,0xA1,0x07, +0x8C,0x8E,0x77,0xA0,0x33,0x06,0x14,0x5C,0x90,0x1E,0x18,0xF7,0x08,0xA6,0x3D,0x0A, +0x19,0xF9,0x87,0x80,0x11,0x6E,0x69,0xE4,0x96,0x17,0x30,0xFF,0x34,0x91,0x63,0x72, +0x38,0xEE,0xCC,0x1C,0x01,0xA3,0x1D,0x94,0x28,0xA4,0x31,0xF6,0x7A,0xC4,0x54,0xD7, +0xF6,0xE5,0x31,0x58,0x03,0xA2,0xCC,0xCE,0x62,0xDB,0x94,0x45,0x73,0xB5,0xBF,0x45, +0xC9,0x24,0xB5,0xD5,0x82,0x02,0xAD,0x23,0x79,0x69,0x8D,0xB8,0xB6,0x4D,0xCE,0xCF, +0x4C,0xCA,0x33,0x23,0xE8,0x1C,0x88,0xAA,0x9D,0x8B,0x41,0x6E,0x16,0xC9,0x20,0xE5, +0x89,0x9E,0xCD,0x3B,0xDA,0x70,0xF7,0x7E,0x99,0x26,0x20,0x14,0x54,0x25,0xAB,0x6E, +0x73,0x85,0xE6,0x9B,0x21,0x9D,0x0A,0x6C,0x82,0x0E,0xA8,0xF8,0xC2,0x0C,0xFA,0x10, +0x1E,0x6C,0x96,0xEF,0x87,0x0D,0xC4,0x0F,0x61,0x8B,0xAD,0xEE,0x83,0x2B,0x95,0xF8, +0x8E,0x92,0x84,0x72,0x39,0xEB,0x20,0xEA,0x83,0xED,0x83,0xCD,0x97,0x6E,0x08,0xBC, +0xEB,0x4E,0x26,0xB6,0x73,0x2B,0xE4,0xD3,0xF6,0x4C,0xFE,0x26,0x71,0xE2,0x61,0x11, +0x74,0x4A,0xFF,0x57,0x1A,0x87,0x0F,0x75,0x48,0x2E,0xCF,0x51,0x69,0x17,0xA0,0x02, +0x12,0x61,0x95,0xD5,0xD1,0x40,0xB2,0x10,0x4C,0xEE,0xC4,0xAC,0x10,0x43,0xA6,0xA5, +0x9E,0x0A,0xD5,0x95,0x62,0x9A,0x0D,0xCF,0x88,0x82,0xC5,0x32,0x0C,0xE4,0x2B,0x9F, +0x45,0xE6,0x0D,0x9F,0x28,0x9C,0xB1,0xB9,0x2A,0x5A,0x57,0xAD,0x37,0x0F,0xAF,0x1D, +0x7F,0xDB,0xBD,0x9F, +}; + + +/* subject:/C=US/ST=Arizona/L=Scottsdale/O=GoDaddy.com, Inc./CN=Go Daddy Root Certificate Authority - G2 */ +/* issuer :/C=US/ST=Arizona/L=Scottsdale/O=GoDaddy.com, Inc./CN=Go Daddy Root Certificate Authority - G2 */ + + +const unsigned char Go_Daddy_Root_Certificate_Authority___G2_certificate[969]={ +0x30,0x82,0x03,0xC5,0x30,0x82,0x02,0xAD,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x00, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x30, +0x81,0x83,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31, +0x10,0x30,0x0E,0x06,0x03,0x55,0x04,0x08,0x13,0x07,0x41,0x72,0x69,0x7A,0x6F,0x6E, +0x61,0x31,0x13,0x30,0x11,0x06,0x03,0x55,0x04,0x07,0x13,0x0A,0x53,0x63,0x6F,0x74, +0x74,0x73,0x64,0x61,0x6C,0x65,0x31,0x1A,0x30,0x18,0x06,0x03,0x55,0x04,0x0A,0x13, +0x11,0x47,0x6F,0x44,0x61,0x64,0x64,0x79,0x2E,0x63,0x6F,0x6D,0x2C,0x20,0x49,0x6E, +0x63,0x2E,0x31,0x31,0x30,0x2F,0x06,0x03,0x55,0x04,0x03,0x13,0x28,0x47,0x6F,0x20, +0x44,0x61,0x64,0x64,0x79,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79, +0x20,0x2D,0x20,0x47,0x32,0x30,0x1E,0x17,0x0D,0x30,0x39,0x30,0x39,0x30,0x31,0x30, +0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x37,0x31,0x32,0x33,0x31,0x32,0x33, +0x35,0x39,0x35,0x39,0x5A,0x30,0x81,0x83,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04, +0x06,0x13,0x02,0x55,0x53,0x31,0x10,0x30,0x0E,0x06,0x03,0x55,0x04,0x08,0x13,0x07, +0x41,0x72,0x69,0x7A,0x6F,0x6E,0x61,0x31,0x13,0x30,0x11,0x06,0x03,0x55,0x04,0x07, +0x13,0x0A,0x53,0x63,0x6F,0x74,0x74,0x73,0x64,0x61,0x6C,0x65,0x31,0x1A,0x30,0x18, +0x06,0x03,0x55,0x04,0x0A,0x13,0x11,0x47,0x6F,0x44,0x61,0x64,0x64,0x79,0x2E,0x63, +0x6F,0x6D,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x31,0x30,0x2F,0x06,0x03,0x55,0x04, +0x03,0x13,0x28,0x47,0x6F,0x20,0x44,0x61,0x64,0x64,0x79,0x20,0x52,0x6F,0x6F,0x74, +0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x41,0x75,0x74, +0x68,0x6F,0x72,0x69,0x74,0x79,0x20,0x2D,0x20,0x47,0x32,0x30,0x82,0x01,0x22,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82, +0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xBF,0x71,0x62,0x08, +0xF1,0xFA,0x59,0x34,0xF7,0x1B,0xC9,0x18,0xA3,0xF7,0x80,0x49,0x58,0xE9,0x22,0x83, +0x13,0xA6,0xC5,0x20,0x43,0x01,0x3B,0x84,0xF1,0xE6,0x85,0x49,0x9F,0x27,0xEA,0xF6, +0x84,0x1B,0x4E,0xA0,0xB4,0xDB,0x70,0x98,0xC7,0x32,0x01,0xB1,0x05,0x3E,0x07,0x4E, +0xEE,0xF4,0xFA,0x4F,0x2F,0x59,0x30,0x22,0xE7,0xAB,0x19,0x56,0x6B,0xE2,0x80,0x07, +0xFC,0xF3,0x16,0x75,0x80,0x39,0x51,0x7B,0xE5,0xF9,0x35,0xB6,0x74,0x4E,0xA9,0x8D, +0x82,0x13,0xE4,0xB6,0x3F,0xA9,0x03,0x83,0xFA,0xA2,0xBE,0x8A,0x15,0x6A,0x7F,0xDE, +0x0B,0xC3,0xB6,0x19,0x14,0x05,0xCA,0xEA,0xC3,0xA8,0x04,0x94,0x3B,0x46,0x7C,0x32, +0x0D,0xF3,0x00,0x66,0x22,0xC8,0x8D,0x69,0x6D,0x36,0x8C,0x11,0x18,0xB7,0xD3,0xB2, +0x1C,0x60,0xB4,0x38,0xFA,0x02,0x8C,0xCE,0xD3,0xDD,0x46,0x07,0xDE,0x0A,0x3E,0xEB, +0x5D,0x7C,0xC8,0x7C,0xFB,0xB0,0x2B,0x53,0xA4,0x92,0x62,0x69,0x51,0x25,0x05,0x61, +0x1A,0x44,0x81,0x8C,0x2C,0xA9,0x43,0x96,0x23,0xDF,0xAC,0x3A,0x81,0x9A,0x0E,0x29, +0xC5,0x1C,0xA9,0xE9,0x5D,0x1E,0xB6,0x9E,0x9E,0x30,0x0A,0x39,0xCE,0xF1,0x88,0x80, +0xFB,0x4B,0x5D,0xCC,0x32,0xEC,0x85,0x62,0x43,0x25,0x34,0x02,0x56,0x27,0x01,0x91, +0xB4,0x3B,0x70,0x2A,0x3F,0x6E,0xB1,0xE8,0x9C,0x88,0x01,0x7D,0x9F,0xD4,0xF9,0xDB, +0x53,0x6D,0x60,0x9D,0xBF,0x2C,0xE7,0x58,0xAB,0xB8,0x5F,0x46,0xFC,0xCE,0xC4,0x1B, +0x03,0x3C,0x09,0xEB,0x49,0x31,0x5C,0x69,0x46,0xB3,0xE0,0x47,0x02,0x03,0x01,0x00, +0x01,0xA3,0x42,0x30,0x40,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04, +0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF, +0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04, +0x14,0x3A,0x9A,0x85,0x07,0x10,0x67,0x28,0xB6,0xEF,0xF6,0xBD,0x05,0x41,0x6E,0x20, +0xC1,0x94,0xDA,0x0F,0xDE,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01, +0x01,0x0B,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x99,0xDB,0x5D,0x79,0xD5,0xF9,0x97, +0x59,0x67,0x03,0x61,0xF1,0x7E,0x3B,0x06,0x31,0x75,0x2D,0xA1,0x20,0x8E,0x4F,0x65, +0x87,0xB4,0xF7,0xA6,0x9C,0xBC,0xD8,0xE9,0x2F,0xD0,0xDB,0x5A,0xEE,0xCF,0x74,0x8C, +0x73,0xB4,0x38,0x42,0xDA,0x05,0x7B,0xF8,0x02,0x75,0xB8,0xFD,0xA5,0xB1,0xD7,0xAE, +0xF6,0xD7,0xDE,0x13,0xCB,0x53,0x10,0x7E,0x8A,0x46,0xD1,0x97,0xFA,0xB7,0x2E,0x2B, +0x11,0xAB,0x90,0xB0,0x27,0x80,0xF9,0xE8,0x9F,0x5A,0xE9,0x37,0x9F,0xAB,0xE4,0xDF, +0x6C,0xB3,0x85,0x17,0x9D,0x3D,0xD9,0x24,0x4F,0x79,0x91,0x35,0xD6,0x5F,0x04,0xEB, +0x80,0x83,0xAB,0x9A,0x02,0x2D,0xB5,0x10,0xF4,0xD8,0x90,0xC7,0x04,0x73,0x40,0xED, +0x72,0x25,0xA0,0xA9,0x9F,0xEC,0x9E,0xAB,0x68,0x12,0x99,0x57,0xC6,0x8F,0x12,0x3A, +0x09,0xA4,0xBD,0x44,0xFD,0x06,0x15,0x37,0xC1,0x9B,0xE4,0x32,0xA3,0xED,0x38,0xE8, +0xD8,0x64,0xF3,0x2C,0x7E,0x14,0xFC,0x02,0xEA,0x9F,0xCD,0xFF,0x07,0x68,0x17,0xDB, +0x22,0x90,0x38,0x2D,0x7A,0x8D,0xD1,0x54,0xF1,0x69,0xE3,0x5F,0x33,0xCA,0x7A,0x3D, +0x7B,0x0A,0xE3,0xCA,0x7F,0x5F,0x39,0xE5,0xE2,0x75,0xBA,0xC5,0x76,0x18,0x33,0xCE, +0x2C,0xF0,0x2F,0x4C,0xAD,0xF7,0xB1,0xE7,0xCE,0x4F,0xA8,0xC4,0x9B,0x4A,0x54,0x06, +0xC5,0x7F,0x7D,0xD5,0x08,0x0F,0xE2,0x1C,0xFE,0x7E,0x17,0xB8,0xAC,0x5E,0xF6,0xD4, +0x16,0xB2,0x43,0x09,0x0C,0x4D,0xF6,0xA7,0x6B,0xB4,0x99,0x84,0x65,0xCA,0x7A,0x88, +0xE2,0xE2,0x44,0xBE,0x5C,0xF7,0xEA,0x1C,0xF5, +}; + + +/* subject:/C=US/O=GTE Corporation/OU=GTE CyberTrust Solutions, Inc./CN=GTE CyberTrust Global Root */ +/* issuer :/C=US/O=GTE Corporation/OU=GTE CyberTrust Solutions, Inc./CN=GTE CyberTrust Global Root */ + + +const unsigned char GTE_CyberTrust_Global_Root_certificate[606]={ +0x30,0x82,0x02,0x5A,0x30,0x82,0x01,0xC3,0x02,0x02,0x01,0xA5,0x30,0x0D,0x06,0x09, +0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x04,0x05,0x00,0x30,0x75,0x31,0x0B,0x30, +0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x18,0x30,0x16,0x06,0x03, +0x55,0x04,0x0A,0x13,0x0F,0x47,0x54,0x45,0x20,0x43,0x6F,0x72,0x70,0x6F,0x72,0x61, +0x74,0x69,0x6F,0x6E,0x31,0x27,0x30,0x25,0x06,0x03,0x55,0x04,0x0B,0x13,0x1E,0x47, +0x54,0x45,0x20,0x43,0x79,0x62,0x65,0x72,0x54,0x72,0x75,0x73,0x74,0x20,0x53,0x6F, +0x6C,0x75,0x74,0x69,0x6F,0x6E,0x73,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x23,0x30, +0x21,0x06,0x03,0x55,0x04,0x03,0x13,0x1A,0x47,0x54,0x45,0x20,0x43,0x79,0x62,0x65, +0x72,0x54,0x72,0x75,0x73,0x74,0x20,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x20,0x52,0x6F, +0x6F,0x74,0x30,0x1E,0x17,0x0D,0x39,0x38,0x30,0x38,0x31,0x33,0x30,0x30,0x32,0x39, +0x30,0x30,0x5A,0x17,0x0D,0x31,0x38,0x30,0x38,0x31,0x33,0x32,0x33,0x35,0x39,0x30, +0x30,0x5A,0x30,0x75,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55, +0x53,0x31,0x18,0x30,0x16,0x06,0x03,0x55,0x04,0x0A,0x13,0x0F,0x47,0x54,0x45,0x20, +0x43,0x6F,0x72,0x70,0x6F,0x72,0x61,0x74,0x69,0x6F,0x6E,0x31,0x27,0x30,0x25,0x06, +0x03,0x55,0x04,0x0B,0x13,0x1E,0x47,0x54,0x45,0x20,0x43,0x79,0x62,0x65,0x72,0x54, +0x72,0x75,0x73,0x74,0x20,0x53,0x6F,0x6C,0x75,0x74,0x69,0x6F,0x6E,0x73,0x2C,0x20, +0x49,0x6E,0x63,0x2E,0x31,0x23,0x30,0x21,0x06,0x03,0x55,0x04,0x03,0x13,0x1A,0x47, +0x54,0x45,0x20,0x43,0x79,0x62,0x65,0x72,0x54,0x72,0x75,0x73,0x74,0x20,0x47,0x6C, +0x6F,0x62,0x61,0x6C,0x20,0x52,0x6F,0x6F,0x74,0x30,0x81,0x9F,0x30,0x0D,0x06,0x09, +0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x81,0x8D,0x00,0x30, +0x81,0x89,0x02,0x81,0x81,0x00,0x95,0x0F,0xA0,0xB6,0xF0,0x50,0x9C,0xE8,0x7A,0xC7, +0x88,0xCD,0xDD,0x17,0x0E,0x2E,0xB0,0x94,0xD0,0x1B,0x3D,0x0E,0xF6,0x94,0xC0,0x8A, +0x94,0xC7,0x06,0xC8,0x90,0x97,0xC8,0xB8,0x64,0x1A,0x7A,0x7E,0x6C,0x3C,0x53,0xE1, +0x37,0x28,0x73,0x60,0x7F,0xB2,0x97,0x53,0x07,0x9F,0x53,0xF9,0x6D,0x58,0x94,0xD2, +0xAF,0x8D,0x6D,0x88,0x67,0x80,0xE6,0xED,0xB2,0x95,0xCF,0x72,0x31,0xCA,0xA5,0x1C, +0x72,0xBA,0x5C,0x02,0xE7,0x64,0x42,0xE7,0xF9,0xA9,0x2C,0xD6,0x3A,0x0D,0xAC,0x8D, +0x42,0xAA,0x24,0x01,0x39,0xE6,0x9C,0x3F,0x01,0x85,0x57,0x0D,0x58,0x87,0x45,0xF8, +0xD3,0x85,0xAA,0x93,0x69,0x26,0x85,0x70,0x48,0x80,0x3F,0x12,0x15,0xC7,0x79,0xB4, +0x1F,0x05,0x2F,0x3B,0x62,0x99,0x02,0x03,0x01,0x00,0x01,0x30,0x0D,0x06,0x09,0x2A, +0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x04,0x05,0x00,0x03,0x81,0x81,0x00,0x6D,0xEB, +0x1B,0x09,0xE9,0x5E,0xD9,0x51,0xDB,0x67,0x22,0x61,0xA4,0x2A,0x3C,0x48,0x77,0xE3, +0xA0,0x7C,0xA6,0xDE,0x73,0xA2,0x14,0x03,0x85,0x3D,0xFB,0xAB,0x0E,0x30,0xC5,0x83, +0x16,0x33,0x81,0x13,0x08,0x9E,0x7B,0x34,0x4E,0xDF,0x40,0xC8,0x74,0xD7,0xB9,0x7D, +0xDC,0xF4,0x76,0x55,0x7D,0x9B,0x63,0x54,0x18,0xE9,0xF0,0xEA,0xF3,0x5C,0xB1,0xD9, +0x8B,0x42,0x1E,0xB9,0xC0,0x95,0x4E,0xBA,0xFA,0xD5,0xE2,0x7C,0xF5,0x68,0x61,0xBF, +0x8E,0xEC,0x05,0x97,0x5F,0x5B,0xB0,0xD7,0xA3,0x85,0x34,0xC4,0x24,0xA7,0x0D,0x0F, +0x95,0x93,0xEF,0xCB,0x94,0xD8,0x9E,0x1F,0x9D,0x5C,0x85,0x6D,0xC7,0xAA,0xAE,0x4F, +0x1F,0x22,0xB5,0xCD,0x95,0xAD,0xBA,0xA7,0xCC,0xF9,0xAB,0x0B,0x7A,0x7F, +}; + + +/* subject:/C=US/O=Network Solutions L.L.C./CN=Network Solutions Certificate Authority */ +/* issuer :/C=US/O=Network Solutions L.L.C./CN=Network Solutions Certificate Authority */ + + +const unsigned char Network_Solutions_Certificate_Authority_certificate[1002]={ +0x30,0x82,0x03,0xE6,0x30,0x82,0x02,0xCE,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x57, +0xCB,0x33,0x6F,0xC2,0x5C,0x16,0xE6,0x47,0x16,0x17,0xE3,0x90,0x31,0x68,0xE0,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x62, +0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x21,0x30, +0x1F,0x06,0x03,0x55,0x04,0x0A,0x13,0x18,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x20, +0x53,0x6F,0x6C,0x75,0x74,0x69,0x6F,0x6E,0x73,0x20,0x4C,0x2E,0x4C,0x2E,0x43,0x2E, +0x31,0x30,0x30,0x2E,0x06,0x03,0x55,0x04,0x03,0x13,0x27,0x4E,0x65,0x74,0x77,0x6F, +0x72,0x6B,0x20,0x53,0x6F,0x6C,0x75,0x74,0x69,0x6F,0x6E,0x73,0x20,0x43,0x65,0x72, +0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69, +0x74,0x79,0x30,0x1E,0x17,0x0D,0x30,0x36,0x31,0x32,0x30,0x31,0x30,0x30,0x30,0x30, +0x30,0x30,0x5A,0x17,0x0D,0x32,0x39,0x31,0x32,0x33,0x31,0x32,0x33,0x35,0x39,0x35, +0x39,0x5A,0x30,0x62,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55, +0x53,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x0A,0x13,0x18,0x4E,0x65,0x74,0x77, +0x6F,0x72,0x6B,0x20,0x53,0x6F,0x6C,0x75,0x74,0x69,0x6F,0x6E,0x73,0x20,0x4C,0x2E, +0x4C,0x2E,0x43,0x2E,0x31,0x30,0x30,0x2E,0x06,0x03,0x55,0x04,0x03,0x13,0x27,0x4E, +0x65,0x74,0x77,0x6F,0x72,0x6B,0x20,0x53,0x6F,0x6C,0x75,0x74,0x69,0x6F,0x6E,0x73, +0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x41,0x75,0x74, +0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82, +0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xE4,0xBC,0x7E,0x92,0x30,0x6D,0xC6,0xD8,0x8E, +0x2B,0x0B,0xBC,0x46,0xCE,0xE0,0x27,0x96,0xDE,0xDE,0xF9,0xFA,0x12,0xD3,0x3C,0x33, +0x73,0xB3,0x04,0x2F,0xBC,0x71,0x8C,0xE5,0x9F,0xB6,0x22,0x60,0x3E,0x5F,0x5D,0xCE, +0x09,0xFF,0x82,0x0C,0x1B,0x9A,0x51,0x50,0x1A,0x26,0x89,0xDD,0xD5,0x61,0x5D,0x19, +0xDC,0x12,0x0F,0x2D,0x0A,0xA2,0x43,0x5D,0x17,0xD0,0x34,0x92,0x20,0xEA,0x73,0xCF, +0x38,0x2C,0x06,0x26,0x09,0x7A,0x72,0xF7,0xFA,0x50,0x32,0xF8,0xC2,0x93,0xD3,0x69, +0xA2,0x23,0xCE,0x41,0xB1,0xCC,0xE4,0xD5,0x1F,0x36,0xD1,0x8A,0x3A,0xF8,0x8C,0x63, +0xE2,0x14,0x59,0x69,0xED,0x0D,0xD3,0x7F,0x6B,0xE8,0xB8,0x03,0xE5,0x4F,0x6A,0xE5, +0x98,0x63,0x69,0x48,0x05,0xBE,0x2E,0xFF,0x33,0xB6,0xE9,0x97,0x59,0x69,0xF8,0x67, +0x19,0xAE,0x93,0x61,0x96,0x44,0x15,0xD3,0x72,0xB0,0x3F,0xBC,0x6A,0x7D,0xEC,0x48, +0x7F,0x8D,0xC3,0xAB,0xAA,0x71,0x2B,0x53,0x69,0x41,0x53,0x34,0xB5,0xB0,0xB9,0xC5, +0x06,0x0A,0xC4,0xB0,0x45,0xF5,0x41,0x5D,0x6E,0x89,0x45,0x7B,0x3D,0x3B,0x26,0x8C, +0x74,0xC2,0xE5,0xD2,0xD1,0x7D,0xB2,0x11,0xD4,0xFB,0x58,0x32,0x22,0x9A,0x80,0xC9, +0xDC,0xFD,0x0C,0xE9,0x7F,0x5E,0x03,0x97,0xCE,0x3B,0x00,0x14,0x87,0x27,0x70,0x38, +0xA9,0x8E,0x6E,0xB3,0x27,0x76,0x98,0x51,0xE0,0x05,0xE3,0x21,0xAB,0x1A,0xD5,0x85, +0x22,0x3C,0x29,0xB5,0x9A,0x16,0xC5,0x80,0xA8,0xF4,0xBB,0x6B,0x30,0x8F,0x2F,0x46, +0x02,0xA2,0xB1,0x0C,0x22,0xE0,0xD3,0x02,0x03,0x01,0x00,0x01,0xA3,0x81,0x97,0x30, +0x81,0x94,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x21,0x30,0xC9, +0xFB,0x00,0xD7,0x4E,0x98,0xDA,0x87,0xAA,0x2A,0xD0,0xA7,0x2E,0xB1,0x40,0x31,0xA7, +0x4C,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01, +0x06,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01, +0x01,0xFF,0x30,0x52,0x06,0x03,0x55,0x1D,0x1F,0x04,0x4B,0x30,0x49,0x30,0x47,0xA0, +0x45,0xA0,0x43,0x86,0x41,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x63,0x72,0x6C,0x2E, +0x6E,0x65,0x74,0x73,0x6F,0x6C,0x73,0x73,0x6C,0x2E,0x63,0x6F,0x6D,0x2F,0x4E,0x65, +0x74,0x77,0x6F,0x72,0x6B,0x53,0x6F,0x6C,0x75,0x74,0x69,0x6F,0x6E,0x73,0x43,0x65, +0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x41,0x75,0x74,0x68,0x6F,0x72,0x69, +0x74,0x79,0x2E,0x63,0x72,0x6C,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D, +0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0xBB,0xAE,0x4B,0xE7,0xB7,0x57, +0xEB,0x7F,0xAA,0x2D,0xB7,0x73,0x47,0x85,0x6A,0xC1,0xE4,0xA5,0x1D,0xE4,0xE7,0x3C, +0xE9,0xF4,0x59,0x65,0x77,0xB5,0x7A,0x5B,0x5A,0x8D,0x25,0x36,0xE0,0x7A,0x97,0x2E, +0x38,0xC0,0x57,0x60,0x83,0x98,0x06,0x83,0x9F,0xB9,0x76,0x7A,0x6E,0x50,0xE0,0xBA, +0x88,0x2C,0xFC,0x45,0xCC,0x18,0xB0,0x99,0x95,0x51,0x0E,0xEC,0x1D,0xB8,0x88,0xFF, +0x87,0x50,0x1C,0x82,0xC2,0xE3,0xE0,0x32,0x80,0xBF,0xA0,0x0B,0x47,0xC8,0xC3,0x31, +0xEF,0x99,0x67,0x32,0x80,0x4F,0x17,0x21,0x79,0x0C,0x69,0x5C,0xDE,0x5E,0x34,0xAE, +0x02,0xB5,0x26,0xEA,0x50,0xDF,0x7F,0x18,0x65,0x2C,0xC9,0xF2,0x63,0xE1,0xA9,0x07, +0xFE,0x7C,0x71,0x1F,0x6B,0x33,0x24,0x6A,0x1E,0x05,0xF7,0x05,0x68,0xC0,0x6A,0x12, +0xCB,0x2E,0x5E,0x61,0xCB,0xAE,0x28,0xD3,0x7E,0xC2,0xB4,0x66,0x91,0x26,0x5F,0x3C, +0x2E,0x24,0x5F,0xCB,0x58,0x0F,0xEB,0x28,0xEC,0xAF,0x11,0x96,0xF3,0xDC,0x7B,0x6F, +0xC0,0xA7,0x88,0xF2,0x53,0x77,0xB3,0x60,0x5E,0xAE,0xAE,0x28,0xDA,0x35,0x2C,0x6F, +0x34,0x45,0xD3,0x26,0xE1,0xDE,0xEC,0x5B,0x4F,0x27,0x6B,0x16,0x7C,0xBD,0x44,0x04, +0x18,0x82,0xB3,0x89,0x79,0x17,0x10,0x71,0x3D,0x7A,0xA2,0x16,0x4E,0xF5,0x01,0xCD, +0xA4,0x6C,0x65,0x68,0xA1,0x49,0x76,0x5C,0x43,0xC9,0xD8,0xBC,0x36,0x67,0x6C,0xA5, +0x94,0xB5,0xD4,0xCC,0xB9,0xBD,0x6A,0x35,0x56,0x21,0xDE,0xD8,0xC3,0xEB,0xFB,0xCB, +0xA4,0x60,0x4C,0xB0,0x55,0xA0,0xA0,0x7B,0x57,0xB2, +}; + + +/* subject:/L=ValiCert Validation Network/O=ValiCert, Inc./OU=ValiCert Class 3 Policy Validation Authority/CN=http://www.valicert.com//emailAddress=info@valicert.com */ +/* issuer :/L=ValiCert Validation Network/O=ValiCert, Inc./OU=ValiCert Class 3 Policy Validation Authority/CN=http://www.valicert.com//emailAddress=info@valicert.com */ + + +const unsigned char RSA_Root_Certificate_1_certificate[747]={ +0x30,0x82,0x02,0xE7,0x30,0x82,0x02,0x50,0x02,0x01,0x01,0x30,0x0D,0x06,0x09,0x2A, +0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x81,0xBB,0x31,0x24,0x30, +0x22,0x06,0x03,0x55,0x04,0x07,0x13,0x1B,0x56,0x61,0x6C,0x69,0x43,0x65,0x72,0x74, +0x20,0x56,0x61,0x6C,0x69,0x64,0x61,0x74,0x69,0x6F,0x6E,0x20,0x4E,0x65,0x74,0x77, +0x6F,0x72,0x6B,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04,0x0A,0x13,0x0E,0x56,0x61, +0x6C,0x69,0x43,0x65,0x72,0x74,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x35,0x30,0x33, +0x06,0x03,0x55,0x04,0x0B,0x13,0x2C,0x56,0x61,0x6C,0x69,0x43,0x65,0x72,0x74,0x20, +0x43,0x6C,0x61,0x73,0x73,0x20,0x33,0x20,0x50,0x6F,0x6C,0x69,0x63,0x79,0x20,0x56, +0x61,0x6C,0x69,0x64,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72, +0x69,0x74,0x79,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x03,0x13,0x18,0x68,0x74, +0x74,0x70,0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E,0x76,0x61,0x6C,0x69,0x63,0x65,0x72, +0x74,0x2E,0x63,0x6F,0x6D,0x2F,0x31,0x20,0x30,0x1E,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x09,0x01,0x16,0x11,0x69,0x6E,0x66,0x6F,0x40,0x76,0x61,0x6C,0x69, +0x63,0x65,0x72,0x74,0x2E,0x63,0x6F,0x6D,0x30,0x1E,0x17,0x0D,0x39,0x39,0x30,0x36, +0x32,0x36,0x30,0x30,0x32,0x32,0x33,0x33,0x5A,0x17,0x0D,0x31,0x39,0x30,0x36,0x32, +0x36,0x30,0x30,0x32,0x32,0x33,0x33,0x5A,0x30,0x81,0xBB,0x31,0x24,0x30,0x22,0x06, +0x03,0x55,0x04,0x07,0x13,0x1B,0x56,0x61,0x6C,0x69,0x43,0x65,0x72,0x74,0x20,0x56, +0x61,0x6C,0x69,0x64,0x61,0x74,0x69,0x6F,0x6E,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72, +0x6B,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04,0x0A,0x13,0x0E,0x56,0x61,0x6C,0x69, +0x43,0x65,0x72,0x74,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x35,0x30,0x33,0x06,0x03, +0x55,0x04,0x0B,0x13,0x2C,0x56,0x61,0x6C,0x69,0x43,0x65,0x72,0x74,0x20,0x43,0x6C, +0x61,0x73,0x73,0x20,0x33,0x20,0x50,0x6F,0x6C,0x69,0x63,0x79,0x20,0x56,0x61,0x6C, +0x69,0x64,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74, +0x79,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x03,0x13,0x18,0x68,0x74,0x74,0x70, +0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E,0x76,0x61,0x6C,0x69,0x63,0x65,0x72,0x74,0x2E, +0x63,0x6F,0x6D,0x2F,0x31,0x20,0x30,0x1E,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D, +0x01,0x09,0x01,0x16,0x11,0x69,0x6E,0x66,0x6F,0x40,0x76,0x61,0x6C,0x69,0x63,0x65, +0x72,0x74,0x2E,0x63,0x6F,0x6D,0x30,0x81,0x9F,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48, +0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x81,0x8D,0x00,0x30,0x81,0x89,0x02, +0x81,0x81,0x00,0xE3,0x98,0x51,0x96,0x1C,0xE8,0xD5,0xB1,0x06,0x81,0x6A,0x57,0xC3, +0x72,0x75,0x93,0xAB,0xCF,0x9E,0xA6,0xFC,0xF3,0x16,0x52,0xD6,0x2D,0x4D,0x9F,0x35, +0x44,0xA8,0x2E,0x04,0x4D,0x07,0x49,0x8A,0x38,0x29,0xF5,0x77,0x37,0xE7,0xB7,0xAB, +0x5D,0xDF,0x36,0x71,0x14,0x99,0x8F,0xDC,0xC2,0x92,0xF1,0xE7,0x60,0x92,0x97,0xEC, +0xD8,0x48,0xDC,0xBF,0xC1,0x02,0x20,0xC6,0x24,0xA4,0x28,0x4C,0x30,0x5A,0x76,0x6D, +0xB1,0x5C,0xF3,0xDD,0xDE,0x9E,0x10,0x71,0xA1,0x88,0xC7,0x5B,0x9B,0x41,0x6D,0xCA, +0xB0,0xB8,0x8E,0x15,0xEE,0xAD,0x33,0x2B,0xCF,0x47,0x04,0x5C,0x75,0x71,0x0A,0x98, +0x24,0x98,0x29,0xA7,0x49,0x59,0xA5,0xDD,0xF8,0xB7,0x43,0x62,0x61,0xF3,0xD3,0xE2, +0xD0,0x55,0x3F,0x02,0x03,0x01,0x00,0x01,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x81,0x81,0x00,0x56,0xBB,0x02,0x58,0x84, +0x67,0x08,0x2C,0xDF,0x1F,0xDB,0x7B,0x49,0x33,0xF5,0xD3,0x67,0x9D,0xF4,0xB4,0x0A, +0x10,0xB3,0xC9,0xC5,0x2C,0xE2,0x92,0x6A,0x71,0x78,0x27,0xF2,0x70,0x83,0x42,0xD3, +0x3E,0xCF,0xA9,0x54,0xF4,0xF1,0xD8,0x92,0x16,0x8C,0xD1,0x04,0xCB,0x4B,0xAB,0xC9, +0x9F,0x45,0xAE,0x3C,0x8A,0xA9,0xB0,0x71,0x33,0x5D,0xC8,0xC5,0x57,0xDF,0xAF,0xA8, +0x35,0xB3,0x7F,0x89,0x87,0xE9,0xE8,0x25,0x92,0xB8,0x7F,0x85,0x7A,0xAE,0xD6,0xBC, +0x1E,0x37,0x58,0x2A,0x67,0xC9,0x91,0xCF,0x2A,0x81,0x3E,0xED,0xC6,0x39,0xDF,0xC0, +0x3E,0x19,0x9C,0x19,0xCC,0x13,0x4D,0x82,0x41,0xB5,0x8C,0xDE,0xE0,0x3D,0x60,0x08, +0x20,0x0F,0x45,0x7E,0x6B,0xA2,0x7F,0xA3,0x8C,0x15,0xEE, +}; + + +/* subject:/C=US/O=Starfield Technologies, Inc./OU=Starfield Class 2 Certification Authority */ +/* issuer :/C=US/O=Starfield Technologies, Inc./OU=Starfield Class 2 Certification Authority */ + + +const unsigned char Starfield_Class_2_CA_certificate[1043]={ +0x30,0x82,0x04,0x0F,0x30,0x82,0x02,0xF7,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x00, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, +0x68,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x25, +0x30,0x23,0x06,0x03,0x55,0x04,0x0A,0x13,0x1C,0x53,0x74,0x61,0x72,0x66,0x69,0x65, +0x6C,0x64,0x20,0x54,0x65,0x63,0x68,0x6E,0x6F,0x6C,0x6F,0x67,0x69,0x65,0x73,0x2C, +0x20,0x49,0x6E,0x63,0x2E,0x31,0x32,0x30,0x30,0x06,0x03,0x55,0x04,0x0B,0x13,0x29, +0x53,0x74,0x61,0x72,0x66,0x69,0x65,0x6C,0x64,0x20,0x43,0x6C,0x61,0x73,0x73,0x20, +0x32,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20, +0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x1E,0x17,0x0D,0x30,0x34,0x30, +0x36,0x32,0x39,0x31,0x37,0x33,0x39,0x31,0x36,0x5A,0x17,0x0D,0x33,0x34,0x30,0x36, +0x32,0x39,0x31,0x37,0x33,0x39,0x31,0x36,0x5A,0x30,0x68,0x31,0x0B,0x30,0x09,0x06, +0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x25,0x30,0x23,0x06,0x03,0x55,0x04, +0x0A,0x13,0x1C,0x53,0x74,0x61,0x72,0x66,0x69,0x65,0x6C,0x64,0x20,0x54,0x65,0x63, +0x68,0x6E,0x6F,0x6C,0x6F,0x67,0x69,0x65,0x73,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31, +0x32,0x30,0x30,0x06,0x03,0x55,0x04,0x0B,0x13,0x29,0x53,0x74,0x61,0x72,0x66,0x69, +0x65,0x6C,0x64,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x32,0x20,0x43,0x65,0x72,0x74, +0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72, +0x69,0x74,0x79,0x30,0x82,0x01,0x20,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7, +0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0D,0x00,0x30,0x82,0x01,0x08,0x02, +0x82,0x01,0x01,0x00,0xB7,0x32,0xC8,0xFE,0xE9,0x71,0xA6,0x04,0x85,0xAD,0x0C,0x11, +0x64,0xDF,0xCE,0x4D,0xEF,0xC8,0x03,0x18,0x87,0x3F,0xA1,0xAB,0xFB,0x3C,0xA6,0x9F, +0xF0,0xC3,0xA1,0xDA,0xD4,0xD8,0x6E,0x2B,0x53,0x90,0xFB,0x24,0xA4,0x3E,0x84,0xF0, +0x9E,0xE8,0x5F,0xEC,0xE5,0x27,0x44,0xF5,0x28,0xA6,0x3F,0x7B,0xDE,0xE0,0x2A,0xF0, +0xC8,0xAF,0x53,0x2F,0x9E,0xCA,0x05,0x01,0x93,0x1E,0x8F,0x66,0x1C,0x39,0xA7,0x4D, +0xFA,0x5A,0xB6,0x73,0x04,0x25,0x66,0xEB,0x77,0x7F,0xE7,0x59,0xC6,0x4A,0x99,0x25, +0x14,0x54,0xEB,0x26,0xC7,0xF3,0x7F,0x19,0xD5,0x30,0x70,0x8F,0xAF,0xB0,0x46,0x2A, +0xFF,0xAD,0xEB,0x29,0xED,0xD7,0x9F,0xAA,0x04,0x87,0xA3,0xD4,0xF9,0x89,0xA5,0x34, +0x5F,0xDB,0x43,0x91,0x82,0x36,0xD9,0x66,0x3C,0xB1,0xB8,0xB9,0x82,0xFD,0x9C,0x3A, +0x3E,0x10,0xC8,0x3B,0xEF,0x06,0x65,0x66,0x7A,0x9B,0x19,0x18,0x3D,0xFF,0x71,0x51, +0x3C,0x30,0x2E,0x5F,0xBE,0x3D,0x77,0x73,0xB2,0x5D,0x06,0x6C,0xC3,0x23,0x56,0x9A, +0x2B,0x85,0x26,0x92,0x1C,0xA7,0x02,0xB3,0xE4,0x3F,0x0D,0xAF,0x08,0x79,0x82,0xB8, +0x36,0x3D,0xEA,0x9C,0xD3,0x35,0xB3,0xBC,0x69,0xCA,0xF5,0xCC,0x9D,0xE8,0xFD,0x64, +0x8D,0x17,0x80,0x33,0x6E,0x5E,0x4A,0x5D,0x99,0xC9,0x1E,0x87,0xB4,0x9D,0x1A,0xC0, +0xD5,0x6E,0x13,0x35,0x23,0x5E,0xDF,0x9B,0x5F,0x3D,0xEF,0xD6,0xF7,0x76,0xC2,0xEA, +0x3E,0xBB,0x78,0x0D,0x1C,0x42,0x67,0x6B,0x04,0xD8,0xF8,0xD6,0xDA,0x6F,0x8B,0xF2, +0x44,0xA0,0x01,0xAB,0x02,0x01,0x03,0xA3,0x81,0xC5,0x30,0x81,0xC2,0x30,0x1D,0x06, +0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0xBF,0x5F,0xB7,0xD1,0xCE,0xDD,0x1F,0x86, +0xF4,0x5B,0x55,0xAC,0xDC,0xD7,0x10,0xC2,0x0E,0xA9,0x88,0xE7,0x30,0x81,0x92,0x06, +0x03,0x55,0x1D,0x23,0x04,0x81,0x8A,0x30,0x81,0x87,0x80,0x14,0xBF,0x5F,0xB7,0xD1, +0xCE,0xDD,0x1F,0x86,0xF4,0x5B,0x55,0xAC,0xDC,0xD7,0x10,0xC2,0x0E,0xA9,0x88,0xE7, +0xA1,0x6C,0xA4,0x6A,0x30,0x68,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13, +0x02,0x55,0x53,0x31,0x25,0x30,0x23,0x06,0x03,0x55,0x04,0x0A,0x13,0x1C,0x53,0x74, +0x61,0x72,0x66,0x69,0x65,0x6C,0x64,0x20,0x54,0x65,0x63,0x68,0x6E,0x6F,0x6C,0x6F, +0x67,0x69,0x65,0x73,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x32,0x30,0x30,0x06,0x03, +0x55,0x04,0x0B,0x13,0x29,0x53,0x74,0x61,0x72,0x66,0x69,0x65,0x6C,0x64,0x20,0x43, +0x6C,0x61,0x73,0x73,0x20,0x32,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61, +0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x82,0x01, +0x00,0x30,0x0C,0x06,0x03,0x55,0x1D,0x13,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82, +0x01,0x01,0x00,0x05,0x9D,0x3F,0x88,0x9D,0xD1,0xC9,0x1A,0x55,0xA1,0xAC,0x69,0xF3, +0xF3,0x59,0xDA,0x9B,0x01,0x87,0x1A,0x4F,0x57,0xA9,0xA1,0x79,0x09,0x2A,0xDB,0xF7, +0x2F,0xB2,0x1E,0xCC,0xC7,0x5E,0x6A,0xD8,0x83,0x87,0xA1,0x97,0xEF,0x49,0x35,0x3E, +0x77,0x06,0x41,0x58,0x62,0xBF,0x8E,0x58,0xB8,0x0A,0x67,0x3F,0xEC,0xB3,0xDD,0x21, +0x66,0x1F,0xC9,0x54,0xFA,0x72,0xCC,0x3D,0x4C,0x40,0xD8,0x81,0xAF,0x77,0x9E,0x83, +0x7A,0xBB,0xA2,0xC7,0xF5,0x34,0x17,0x8E,0xD9,0x11,0x40,0xF4,0xFC,0x2C,0x2A,0x4D, +0x15,0x7F,0xA7,0x62,0x5D,0x2E,0x25,0xD3,0x00,0x0B,0x20,0x1A,0x1D,0x68,0xF9,0x17, +0xB8,0xF4,0xBD,0x8B,0xED,0x28,0x59,0xDD,0x4D,0x16,0x8B,0x17,0x83,0xC8,0xB2,0x65, +0xC7,0x2D,0x7A,0xA5,0xAA,0xBC,0x53,0x86,0x6D,0xDD,0x57,0xA4,0xCA,0xF8,0x20,0x41, +0x0B,0x68,0xF0,0xF4,0xFB,0x74,0xBE,0x56,0x5D,0x7A,0x79,0xF5,0xF9,0x1D,0x85,0xE3, +0x2D,0x95,0xBE,0xF5,0x71,0x90,0x43,0xCC,0x8D,0x1F,0x9A,0x00,0x0A,0x87,0x29,0xE9, +0x55,0x22,0x58,0x00,0x23,0xEA,0xE3,0x12,0x43,0x29,0x5B,0x47,0x08,0xDD,0x8C,0x41, +0x6A,0x65,0x06,0xA8,0xE5,0x21,0xAA,0x41,0xB4,0x95,0x21,0x95,0xB9,0x7D,0xD1,0x34, +0xAB,0x13,0xD6,0xAD,0xBC,0xDC,0xE2,0x3D,0x39,0xCD,0xBD,0x3E,0x75,0x70,0xA1,0x18, +0x59,0x03,0xC9,0x22,0xB4,0x8F,0x9C,0xD5,0x5E,0x2A,0xD7,0xA5,0xB6,0xD4,0x0A,0x6D, +0xF8,0xB7,0x40,0x11,0x46,0x9A,0x1F,0x79,0x0E,0x62,0xBF,0x0F,0x97,0xEC,0xE0,0x2F, +0x1F,0x17,0x94, +}; + + +/* subject:/C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./CN=Starfield Root Certificate Authority - G2 */ +/* issuer :/C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./CN=Starfield Root Certificate Authority - G2 */ + + +const unsigned char Starfield_Root_Certificate_Authority___G2_certificate[993]={ +0x30,0x82,0x03,0xDD,0x30,0x82,0x02,0xC5,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x00, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x30, +0x81,0x8F,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31, +0x10,0x30,0x0E,0x06,0x03,0x55,0x04,0x08,0x13,0x07,0x41,0x72,0x69,0x7A,0x6F,0x6E, +0x61,0x31,0x13,0x30,0x11,0x06,0x03,0x55,0x04,0x07,0x13,0x0A,0x53,0x63,0x6F,0x74, +0x74,0x73,0x64,0x61,0x6C,0x65,0x31,0x25,0x30,0x23,0x06,0x03,0x55,0x04,0x0A,0x13, +0x1C,0x53,0x74,0x61,0x72,0x66,0x69,0x65,0x6C,0x64,0x20,0x54,0x65,0x63,0x68,0x6E, +0x6F,0x6C,0x6F,0x67,0x69,0x65,0x73,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x32,0x30, +0x30,0x06,0x03,0x55,0x04,0x03,0x13,0x29,0x53,0x74,0x61,0x72,0x66,0x69,0x65,0x6C, +0x64,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61, +0x74,0x65,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x20,0x2D,0x20,0x47, +0x32,0x30,0x1E,0x17,0x0D,0x30,0x39,0x30,0x39,0x30,0x31,0x30,0x30,0x30,0x30,0x30, +0x30,0x5A,0x17,0x0D,0x33,0x37,0x31,0x32,0x33,0x31,0x32,0x33,0x35,0x39,0x35,0x39, +0x5A,0x30,0x81,0x8F,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55, +0x53,0x31,0x10,0x30,0x0E,0x06,0x03,0x55,0x04,0x08,0x13,0x07,0x41,0x72,0x69,0x7A, +0x6F,0x6E,0x61,0x31,0x13,0x30,0x11,0x06,0x03,0x55,0x04,0x07,0x13,0x0A,0x53,0x63, +0x6F,0x74,0x74,0x73,0x64,0x61,0x6C,0x65,0x31,0x25,0x30,0x23,0x06,0x03,0x55,0x04, +0x0A,0x13,0x1C,0x53,0x74,0x61,0x72,0x66,0x69,0x65,0x6C,0x64,0x20,0x54,0x65,0x63, +0x68,0x6E,0x6F,0x6C,0x6F,0x67,0x69,0x65,0x73,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31, +0x32,0x30,0x30,0x06,0x03,0x55,0x04,0x03,0x13,0x29,0x53,0x74,0x61,0x72,0x66,0x69, +0x65,0x6C,0x64,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69, +0x63,0x61,0x74,0x65,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x20,0x2D, +0x20,0x47,0x32,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7, +0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02, +0x82,0x01,0x01,0x00,0xBD,0xED,0xC1,0x03,0xFC,0xF6,0x8F,0xFC,0x02,0xB1,0x6F,0x5B, +0x9F,0x48,0xD9,0x9D,0x79,0xE2,0xA2,0xB7,0x03,0x61,0x56,0x18,0xC3,0x47,0xB6,0xD7, +0xCA,0x3D,0x35,0x2E,0x89,0x43,0xF7,0xA1,0x69,0x9B,0xDE,0x8A,0x1A,0xFD,0x13,0x20, +0x9C,0xB4,0x49,0x77,0x32,0x29,0x56,0xFD,0xB9,0xEC,0x8C,0xDD,0x22,0xFA,0x72,0xDC, +0x27,0x61,0x97,0xEE,0xF6,0x5A,0x84,0xEC,0x6E,0x19,0xB9,0x89,0x2C,0xDC,0x84,0x5B, +0xD5,0x74,0xFB,0x6B,0x5F,0xC5,0x89,0xA5,0x10,0x52,0x89,0x46,0x55,0xF4,0xB8,0x75, +0x1C,0xE6,0x7F,0xE4,0x54,0xAE,0x4B,0xF8,0x55,0x72,0x57,0x02,0x19,0xF8,0x17,0x71, +0x59,0xEB,0x1E,0x28,0x07,0x74,0xC5,0x9D,0x48,0xBE,0x6C,0xB4,0xF4,0xA4,0xB0,0xF3, +0x64,0x37,0x79,0x92,0xC0,0xEC,0x46,0x5E,0x7F,0xE1,0x6D,0x53,0x4C,0x62,0xAF,0xCD, +0x1F,0x0B,0x63,0xBB,0x3A,0x9D,0xFB,0xFC,0x79,0x00,0x98,0x61,0x74,0xCF,0x26,0x82, +0x40,0x63,0xF3,0xB2,0x72,0x6A,0x19,0x0D,0x99,0xCA,0xD4,0x0E,0x75,0xCC,0x37,0xFB, +0x8B,0x89,0xC1,0x59,0xF1,0x62,0x7F,0x5F,0xB3,0x5F,0x65,0x30,0xF8,0xA7,0xB7,0x4D, +0x76,0x5A,0x1E,0x76,0x5E,0x34,0xC0,0xE8,0x96,0x56,0x99,0x8A,0xB3,0xF0,0x7F,0xA4, +0xCD,0xBD,0xDC,0x32,0x31,0x7C,0x91,0xCF,0xE0,0x5F,0x11,0xF8,0x6B,0xAA,0x49,0x5C, +0xD1,0x99,0x94,0xD1,0xA2,0xE3,0x63,0x5B,0x09,0x76,0xB5,0x56,0x62,0xE1,0x4B,0x74, +0x1D,0x96,0xD4,0x26,0xD4,0x08,0x04,0x59,0xD0,0x98,0x0E,0x0E,0xE6,0xDE,0xFC,0xC3, +0xEC,0x1F,0x90,0xF1,0x02,0x03,0x01,0x00,0x01,0xA3,0x42,0x30,0x40,0x30,0x0F,0x06, +0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E, +0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x1D, +0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x7C,0x0C,0x32,0x1F,0xA7,0xD9,0x30, +0x7F,0xC4,0x7D,0x68,0xA3,0x62,0xA8,0xA1,0xCE,0xAB,0x07,0x5B,0x27,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x03,0x82,0x01,0x01, +0x00,0x11,0x59,0xFA,0x25,0x4F,0x03,0x6F,0x94,0x99,0x3B,0x9A,0x1F,0x82,0x85,0x39, +0xD4,0x76,0x05,0x94,0x5E,0xE1,0x28,0x93,0x6D,0x62,0x5D,0x09,0xC2,0xA0,0xA8,0xD4, +0xB0,0x75,0x38,0xF1,0x34,0x6A,0x9D,0xE4,0x9F,0x8A,0x86,0x26,0x51,0xE6,0x2C,0xD1, +0xC6,0x2D,0x6E,0x95,0x20,0x4A,0x92,0x01,0xEC,0xB8,0x8A,0x67,0x7B,0x31,0xE2,0x67, +0x2E,0x8C,0x95,0x03,0x26,0x2E,0x43,0x9D,0x4A,0x31,0xF6,0x0E,0xB5,0x0C,0xBB,0xB7, +0xE2,0x37,0x7F,0x22,0xBA,0x00,0xA3,0x0E,0x7B,0x52,0xFB,0x6B,0xBB,0x3B,0xC4,0xD3, +0x79,0x51,0x4E,0xCD,0x90,0xF4,0x67,0x07,0x19,0xC8,0x3C,0x46,0x7A,0x0D,0x01,0x7D, +0xC5,0x58,0xE7,0x6D,0xE6,0x85,0x30,0x17,0x9A,0x24,0xC4,0x10,0xE0,0x04,0xF7,0xE0, +0xF2,0x7F,0xD4,0xAA,0x0A,0xFF,0x42,0x1D,0x37,0xED,0x94,0xE5,0x64,0x59,0x12,0x20, +0x77,0x38,0xD3,0x32,0x3E,0x38,0x81,0x75,0x96,0x73,0xFA,0x68,0x8F,0xB1,0xCB,0xCE, +0x1F,0xC5,0xEC,0xFA,0x9C,0x7E,0xCF,0x7E,0xB1,0xF1,0x07,0x2D,0xB6,0xFC,0xBF,0xCA, +0xA4,0xBF,0xD0,0x97,0x05,0x4A,0xBC,0xEA,0x18,0x28,0x02,0x90,0xBD,0x54,0x78,0x09, +0x21,0x71,0xD3,0xD1,0x7D,0x1D,0xD9,0x16,0xB0,0xA9,0x61,0x3D,0xD0,0x0A,0x00,0x22, +0xFC,0xC7,0x7B,0xCB,0x09,0x64,0x45,0x0B,0x3B,0x40,0x81,0xF7,0x7D,0x7C,0x32,0xF5, +0x98,0xCA,0x58,0x8E,0x7D,0x2A,0xEE,0x90,0x59,0x73,0x64,0xF9,0x36,0x74,0x5E,0x25, +0xA1,0xF5,0x66,0x05,0x2E,0x7F,0x39,0x15,0xA9,0x2A,0xFB,0x50,0x8B,0x8E,0x85,0x69, +0xF4, +}; + + +/* subject:/C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./CN=Starfield Services Root Certificate Authority - G2 */ +/* issuer :/C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./CN=Starfield Services Root Certificate Authority - G2 */ + + +const unsigned char Starfield_Services_Root_Certificate_Authority___G2_certificate[1011]={ +0x30,0x82,0x03,0xEF,0x30,0x82,0x02,0xD7,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x00, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x30, +0x81,0x98,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31, +0x10,0x30,0x0E,0x06,0x03,0x55,0x04,0x08,0x13,0x07,0x41,0x72,0x69,0x7A,0x6F,0x6E, +0x61,0x31,0x13,0x30,0x11,0x06,0x03,0x55,0x04,0x07,0x13,0x0A,0x53,0x63,0x6F,0x74, +0x74,0x73,0x64,0x61,0x6C,0x65,0x31,0x25,0x30,0x23,0x06,0x03,0x55,0x04,0x0A,0x13, +0x1C,0x53,0x74,0x61,0x72,0x66,0x69,0x65,0x6C,0x64,0x20,0x54,0x65,0x63,0x68,0x6E, +0x6F,0x6C,0x6F,0x67,0x69,0x65,0x73,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x3B,0x30, +0x39,0x06,0x03,0x55,0x04,0x03,0x13,0x32,0x53,0x74,0x61,0x72,0x66,0x69,0x65,0x6C, +0x64,0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x20,0x52,0x6F,0x6F,0x74,0x20, +0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x41,0x75,0x74,0x68, +0x6F,0x72,0x69,0x74,0x79,0x20,0x2D,0x20,0x47,0x32,0x30,0x1E,0x17,0x0D,0x30,0x39, +0x30,0x39,0x30,0x31,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x37,0x31, +0x32,0x33,0x31,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x81,0x98,0x31,0x0B,0x30, +0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x10,0x30,0x0E,0x06,0x03, +0x55,0x04,0x08,0x13,0x07,0x41,0x72,0x69,0x7A,0x6F,0x6E,0x61,0x31,0x13,0x30,0x11, +0x06,0x03,0x55,0x04,0x07,0x13,0x0A,0x53,0x63,0x6F,0x74,0x74,0x73,0x64,0x61,0x6C, +0x65,0x31,0x25,0x30,0x23,0x06,0x03,0x55,0x04,0x0A,0x13,0x1C,0x53,0x74,0x61,0x72, +0x66,0x69,0x65,0x6C,0x64,0x20,0x54,0x65,0x63,0x68,0x6E,0x6F,0x6C,0x6F,0x67,0x69, +0x65,0x73,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x3B,0x30,0x39,0x06,0x03,0x55,0x04, +0x03,0x13,0x32,0x53,0x74,0x61,0x72,0x66,0x69,0x65,0x6C,0x64,0x20,0x53,0x65,0x72, +0x76,0x69,0x63,0x65,0x73,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79, +0x20,0x2D,0x20,0x47,0x32,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48, +0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01, +0x0A,0x02,0x82,0x01,0x01,0x00,0xD5,0x0C,0x3A,0xC4,0x2A,0xF9,0x4E,0xE2,0xF5,0xBE, +0x19,0x97,0x5F,0x8E,0x88,0x53,0xB1,0x1F,0x3F,0xCB,0xCF,0x9F,0x20,0x13,0x6D,0x29, +0x3A,0xC8,0x0F,0x7D,0x3C,0xF7,0x6B,0x76,0x38,0x63,0xD9,0x36,0x60,0xA8,0x9B,0x5E, +0x5C,0x00,0x80,0xB2,0x2F,0x59,0x7F,0xF6,0x87,0xF9,0x25,0x43,0x86,0xE7,0x69,0x1B, +0x52,0x9A,0x90,0xE1,0x71,0xE3,0xD8,0x2D,0x0D,0x4E,0x6F,0xF6,0xC8,0x49,0xD9,0xB6, +0xF3,0x1A,0x56,0xAE,0x2B,0xB6,0x74,0x14,0xEB,0xCF,0xFB,0x26,0xE3,0x1A,0xBA,0x1D, +0x96,0x2E,0x6A,0x3B,0x58,0x94,0x89,0x47,0x56,0xFF,0x25,0xA0,0x93,0x70,0x53,0x83, +0xDA,0x84,0x74,0x14,0xC3,0x67,0x9E,0x04,0x68,0x3A,0xDF,0x8E,0x40,0x5A,0x1D,0x4A, +0x4E,0xCF,0x43,0x91,0x3B,0xE7,0x56,0xD6,0x00,0x70,0xCB,0x52,0xEE,0x7B,0x7D,0xAE, +0x3A,0xE7,0xBC,0x31,0xF9,0x45,0xF6,0xC2,0x60,0xCF,0x13,0x59,0x02,0x2B,0x80,0xCC, +0x34,0x47,0xDF,0xB9,0xDE,0x90,0x65,0x6D,0x02,0xCF,0x2C,0x91,0xA6,0xA6,0xE7,0xDE, +0x85,0x18,0x49,0x7C,0x66,0x4E,0xA3,0x3A,0x6D,0xA9,0xB5,0xEE,0x34,0x2E,0xBA,0x0D, +0x03,0xB8,0x33,0xDF,0x47,0xEB,0xB1,0x6B,0x8D,0x25,0xD9,0x9B,0xCE,0x81,0xD1,0x45, +0x46,0x32,0x96,0x70,0x87,0xDE,0x02,0x0E,0x49,0x43,0x85,0xB6,0x6C,0x73,0xBB,0x64, +0xEA,0x61,0x41,0xAC,0xC9,0xD4,0x54,0xDF,0x87,0x2F,0xC7,0x22,0xB2,0x26,0xCC,0x9F, +0x59,0x54,0x68,0x9F,0xFC,0xBE,0x2A,0x2F,0xC4,0x55,0x1C,0x75,0x40,0x60,0x17,0x85, +0x02,0x55,0x39,0x8B,0x7F,0x05,0x02,0x03,0x01,0x00,0x01,0xA3,0x42,0x30,0x40,0x30, +0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF, +0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06, +0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x9C,0x5F,0x00,0xDF,0xAA, +0x01,0xD7,0x30,0x2B,0x38,0x88,0xA2,0xB8,0x6D,0x4A,0x9C,0xF2,0x11,0x91,0x83,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x03,0x82, +0x01,0x01,0x00,0x4B,0x36,0xA6,0x84,0x77,0x69,0xDD,0x3B,0x19,0x9F,0x67,0x23,0x08, +0x6F,0x0E,0x61,0xC9,0xFD,0x84,0xDC,0x5F,0xD8,0x36,0x81,0xCD,0xD8,0x1B,0x41,0x2D, +0x9F,0x60,0xDD,0xC7,0x1A,0x68,0xD9,0xD1,0x6E,0x86,0xE1,0x88,0x23,0xCF,0x13,0xDE, +0x43,0xCF,0xE2,0x34,0xB3,0x04,0x9D,0x1F,0x29,0xD5,0xBF,0xF8,0x5E,0xC8,0xD5,0xC1, +0xBD,0xEE,0x92,0x6F,0x32,0x74,0xF2,0x91,0x82,0x2F,0xBD,0x82,0x42,0x7A,0xAD,0x2A, +0xB7,0x20,0x7D,0x4D,0xBC,0x7A,0x55,0x12,0xC2,0x15,0xEA,0xBD,0xF7,0x6A,0x95,0x2E, +0x6C,0x74,0x9F,0xCF,0x1C,0xB4,0xF2,0xC5,0x01,0xA3,0x85,0xD0,0x72,0x3E,0xAD,0x73, +0xAB,0x0B,0x9B,0x75,0x0C,0x6D,0x45,0xB7,0x8E,0x94,0xAC,0x96,0x37,0xB5,0xA0,0xD0, +0x8F,0x15,0x47,0x0E,0xE3,0xE8,0x83,0xDD,0x8F,0xFD,0xEF,0x41,0x01,0x77,0xCC,0x27, +0xA9,0x62,0x85,0x33,0xF2,0x37,0x08,0xEF,0x71,0xCF,0x77,0x06,0xDE,0xC8,0x19,0x1D, +0x88,0x40,0xCF,0x7D,0x46,0x1D,0xFF,0x1E,0xC7,0xE1,0xCE,0xFF,0x23,0xDB,0xC6,0xFA, +0x8D,0x55,0x4E,0xA9,0x02,0xE7,0x47,0x11,0x46,0x3E,0xF4,0xFD,0xBD,0x7B,0x29,0x26, +0xBB,0xA9,0x61,0x62,0x37,0x28,0xB6,0x2D,0x2A,0xF6,0x10,0x86,0x64,0xC9,0x70,0xA7, +0xD2,0xAD,0xB7,0x29,0x70,0x79,0xEA,0x3C,0xDA,0x63,0x25,0x9F,0xFD,0x68,0xB7,0x30, +0xEC,0x70,0xFB,0x75,0x8A,0xB7,0x6D,0x60,0x67,0xB2,0x1E,0xC8,0xB9,0xE9,0xD8,0xA8, +0x6F,0x02,0x8B,0x67,0x0D,0x4D,0x26,0x57,0x71,0xDA,0x20,0xFC,0xC1,0x4A,0x50,0x8D, +0xB1,0x28,0xBA, +}; + + +/* subject:/C=IL/O=StartCom Ltd./OU=Secure Digital Certificate Signing/CN=StartCom Certification Authority */ +/* issuer :/C=IL/O=StartCom Ltd./OU=Secure Digital Certificate Signing/CN=StartCom Certification Authority */ + + +const unsigned char StartCom_Certification_Authority_certificate[1931]={ +0x30,0x82,0x07,0x87,0x30,0x82,0x05,0x6F,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x2D, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x30, +0x7D,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x49,0x4C,0x31,0x16, +0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x53,0x74,0x61,0x72,0x74,0x43,0x6F, +0x6D,0x20,0x4C,0x74,0x64,0x2E,0x31,0x2B,0x30,0x29,0x06,0x03,0x55,0x04,0x0B,0x13, +0x22,0x53,0x65,0x63,0x75,0x72,0x65,0x20,0x44,0x69,0x67,0x69,0x74,0x61,0x6C,0x20, +0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x53,0x69,0x67,0x6E, +0x69,0x6E,0x67,0x31,0x29,0x30,0x27,0x06,0x03,0x55,0x04,0x03,0x13,0x20,0x53,0x74, +0x61,0x72,0x74,0x43,0x6F,0x6D,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61, +0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x1E, +0x17,0x0D,0x30,0x36,0x30,0x39,0x31,0x37,0x31,0x39,0x34,0x36,0x33,0x37,0x5A,0x17, +0x0D,0x33,0x36,0x30,0x39,0x31,0x37,0x31,0x39,0x34,0x36,0x33,0x36,0x5A,0x30,0x7D, +0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x49,0x4C,0x31,0x16,0x30, +0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x53,0x74,0x61,0x72,0x74,0x43,0x6F,0x6D, +0x20,0x4C,0x74,0x64,0x2E,0x31,0x2B,0x30,0x29,0x06,0x03,0x55,0x04,0x0B,0x13,0x22, +0x53,0x65,0x63,0x75,0x72,0x65,0x20,0x44,0x69,0x67,0x69,0x74,0x61,0x6C,0x20,0x43, +0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x20,0x53,0x69,0x67,0x6E,0x69, +0x6E,0x67,0x31,0x29,0x30,0x27,0x06,0x03,0x55,0x04,0x03,0x13,0x20,0x53,0x74,0x61, +0x72,0x74,0x43,0x6F,0x6D,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74, +0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x82,0x02, +0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00, +0x03,0x82,0x02,0x0F,0x00,0x30,0x82,0x02,0x0A,0x02,0x82,0x02,0x01,0x00,0xC1,0x88, +0xDB,0x09,0xBC,0x6C,0x46,0x7C,0x78,0x9F,0x95,0x7B,0xB5,0x33,0x90,0xF2,0x72,0x62, +0xD6,0xC1,0x36,0x20,0x22,0x24,0x5E,0xCE,0xE9,0x77,0xF2,0x43,0x0A,0xA2,0x06,0x64, +0xA4,0xCC,0x8E,0x36,0xF8,0x38,0xE6,0x23,0xF0,0x6E,0x6D,0xB1,0x3C,0xDD,0x72,0xA3, +0x85,0x1C,0xA1,0xD3,0x3D,0xB4,0x33,0x2B,0xD3,0x2F,0xAF,0xFE,0xEA,0xB0,0x41,0x59, +0x67,0xB6,0xC4,0x06,0x7D,0x0A,0x9E,0x74,0x85,0xD6,0x79,0x4C,0x80,0x37,0x7A,0xDF, +0x39,0x05,0x52,0x59,0xF7,0xF4,0x1B,0x46,0x43,0xA4,0xD2,0x85,0x85,0xD2,0xC3,0x71, +0xF3,0x75,0x62,0x34,0xBA,0x2C,0x8A,0x7F,0x1E,0x8F,0xEE,0xED,0x34,0xD0,0x11,0xC7, +0x96,0xCD,0x52,0x3D,0xBA,0x33,0xD6,0xDD,0x4D,0xDE,0x0B,0x3B,0x4A,0x4B,0x9F,0xC2, +0x26,0x2F,0xFA,0xB5,0x16,0x1C,0x72,0x35,0x77,0xCA,0x3C,0x5D,0xE6,0xCA,0xE1,0x26, +0x8B,0x1A,0x36,0x76,0x5C,0x01,0xDB,0x74,0x14,0x25,0xFE,0xED,0xB5,0xA0,0x88,0x0F, +0xDD,0x78,0xCA,0x2D,0x1F,0x07,0x97,0x30,0x01,0x2D,0x72,0x79,0xFA,0x46,0xD6,0x13, +0x2A,0xA8,0xB9,0xA6,0xAB,0x83,0x49,0x1D,0xE5,0xF2,0xEF,0xDD,0xE4,0x01,0x8E,0x18, +0x0A,0x8F,0x63,0x53,0x16,0x85,0x62,0xA9,0x0E,0x19,0x3A,0xCC,0xB5,0x66,0xA6,0xC2, +0x6B,0x74,0x07,0xE4,0x2B,0xE1,0x76,0x3E,0xB4,0x6D,0xD8,0xF6,0x44,0xE1,0x73,0x62, +0x1F,0x3B,0xC4,0xBE,0xA0,0x53,0x56,0x25,0x6C,0x51,0x09,0xF7,0xAA,0xAB,0xCA,0xBF, +0x76,0xFD,0x6D,0x9B,0xF3,0x9D,0xDB,0xBF,0x3D,0x66,0xBC,0x0C,0x56,0xAA,0xAF,0x98, +0x48,0x95,0x3A,0x4B,0xDF,0xA7,0x58,0x50,0xD9,0x38,0x75,0xA9,0x5B,0xEA,0x43,0x0C, +0x02,0xFF,0x99,0xEB,0xE8,0x6C,0x4D,0x70,0x5B,0x29,0x65,0x9C,0xDD,0xAA,0x5D,0xCC, +0xAF,0x01,0x31,0xEC,0x0C,0xEB,0xD2,0x8D,0xE8,0xEA,0x9C,0x7B,0xE6,0x6E,0xF7,0x27, +0x66,0x0C,0x1A,0x48,0xD7,0x6E,0x42,0xE3,0x3F,0xDE,0x21,0x3E,0x7B,0xE1,0x0D,0x70, +0xFB,0x63,0xAA,0xA8,0x6C,0x1A,0x54,0xB4,0x5C,0x25,0x7A,0xC9,0xA2,0xC9,0x8B,0x16, +0xA6,0xBB,0x2C,0x7E,0x17,0x5E,0x05,0x4D,0x58,0x6E,0x12,0x1D,0x01,0xEE,0x12,0x10, +0x0D,0xC6,0x32,0x7F,0x18,0xFF,0xFC,0xF4,0xFA,0xCD,0x6E,0x91,0xE8,0x36,0x49,0xBE, +0x1A,0x48,0x69,0x8B,0xC2,0x96,0x4D,0x1A,0x12,0xB2,0x69,0x17,0xC1,0x0A,0x90,0xD6, +0xFA,0x79,0x22,0x48,0xBF,0xBA,0x7B,0x69,0xF8,0x70,0xC7,0xFA,0x7A,0x37,0xD8,0xD8, +0x0D,0xD2,0x76,0x4F,0x57,0xFF,0x90,0xB7,0xE3,0x91,0xD2,0xDD,0xEF,0xC2,0x60,0xB7, +0x67,0x3A,0xDD,0xFE,0xAA,0x9C,0xF0,0xD4,0x8B,0x7F,0x72,0x22,0xCE,0xC6,0x9F,0x97, +0xB6,0xF8,0xAF,0x8A,0xA0,0x10,0xA8,0xD9,0xFB,0x18,0xC6,0xB6,0xB5,0x5C,0x52,0x3C, +0x89,0xB6,0x19,0x2A,0x73,0x01,0x0A,0x0F,0x03,0xB3,0x12,0x60,0xF2,0x7A,0x2F,0x81, +0xDB,0xA3,0x6E,0xFF,0x26,0x30,0x97,0xF5,0x8B,0xDD,0x89,0x57,0xB6,0xAD,0x3D,0xB3, +0xAF,0x2B,0xC5,0xB7,0x76,0x02,0xF0,0xA5,0xD6,0x2B,0x9A,0x86,0x14,0x2A,0x72,0xF6, +0xE3,0x33,0x8C,0x5D,0x09,0x4B,0x13,0xDF,0xBB,0x8C,0x74,0x13,0x52,0x4B,0x02,0x03, +0x01,0x00,0x01,0xA3,0x82,0x02,0x10,0x30,0x82,0x02,0x0C,0x30,0x0F,0x06,0x03,0x55, +0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03, +0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x1D,0x06,0x03, +0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x4E,0x0B,0xEF,0x1A,0xA4,0x40,0x5B,0xA5,0x17, +0x69,0x87,0x30,0xCA,0x34,0x68,0x43,0xD0,0x41,0xAE,0xF2,0x30,0x1F,0x06,0x03,0x55, +0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14,0x4E,0x0B,0xEF,0x1A,0xA4,0x40,0x5B,0xA5, +0x17,0x69,0x87,0x30,0xCA,0x34,0x68,0x43,0xD0,0x41,0xAE,0xF2,0x30,0x82,0x01,0x5A, +0x06,0x03,0x55,0x1D,0x20,0x04,0x82,0x01,0x51,0x30,0x82,0x01,0x4D,0x30,0x82,0x01, +0x49,0x06,0x0B,0x2B,0x06,0x01,0x04,0x01,0x81,0xB5,0x37,0x01,0x01,0x01,0x30,0x82, +0x01,0x38,0x30,0x2E,0x06,0x08,0x2B,0x06,0x01,0x05,0x05,0x07,0x02,0x01,0x16,0x22, +0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E,0x73,0x74,0x61,0x72,0x74, +0x73,0x73,0x6C,0x2E,0x63,0x6F,0x6D,0x2F,0x70,0x6F,0x6C,0x69,0x63,0x79,0x2E,0x70, +0x64,0x66,0x30,0x34,0x06,0x08,0x2B,0x06,0x01,0x05,0x05,0x07,0x02,0x01,0x16,0x28, +0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E,0x73,0x74,0x61,0x72,0x74, +0x73,0x73,0x6C,0x2E,0x63,0x6F,0x6D,0x2F,0x69,0x6E,0x74,0x65,0x72,0x6D,0x65,0x64, +0x69,0x61,0x74,0x65,0x2E,0x70,0x64,0x66,0x30,0x81,0xCF,0x06,0x08,0x2B,0x06,0x01, +0x05,0x05,0x07,0x02,0x02,0x30,0x81,0xC2,0x30,0x27,0x16,0x20,0x53,0x74,0x61,0x72, +0x74,0x20,0x43,0x6F,0x6D,0x6D,0x65,0x72,0x63,0x69,0x61,0x6C,0x20,0x28,0x53,0x74, +0x61,0x72,0x74,0x43,0x6F,0x6D,0x29,0x20,0x4C,0x74,0x64,0x2E,0x30,0x03,0x02,0x01, +0x01,0x1A,0x81,0x96,0x4C,0x69,0x6D,0x69,0x74,0x65,0x64,0x20,0x4C,0x69,0x61,0x62, +0x69,0x6C,0x69,0x74,0x79,0x2C,0x20,0x72,0x65,0x61,0x64,0x20,0x74,0x68,0x65,0x20, +0x73,0x65,0x63,0x74,0x69,0x6F,0x6E,0x20,0x2A,0x4C,0x65,0x67,0x61,0x6C,0x20,0x4C, +0x69,0x6D,0x69,0x74,0x61,0x74,0x69,0x6F,0x6E,0x73,0x2A,0x20,0x6F,0x66,0x20,0x74, +0x68,0x65,0x20,0x53,0x74,0x61,0x72,0x74,0x43,0x6F,0x6D,0x20,0x43,0x65,0x72,0x74, +0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72, +0x69,0x74,0x79,0x20,0x50,0x6F,0x6C,0x69,0x63,0x79,0x20,0x61,0x76,0x61,0x69,0x6C, +0x61,0x62,0x6C,0x65,0x20,0x61,0x74,0x20,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x77, +0x77,0x77,0x2E,0x73,0x74,0x61,0x72,0x74,0x73,0x73,0x6C,0x2E,0x63,0x6F,0x6D,0x2F, +0x70,0x6F,0x6C,0x69,0x63,0x79,0x2E,0x70,0x64,0x66,0x30,0x11,0x06,0x09,0x60,0x86, +0x48,0x01,0x86,0xF8,0x42,0x01,0x01,0x04,0x04,0x03,0x02,0x00,0x07,0x30,0x38,0x06, +0x09,0x60,0x86,0x48,0x01,0x86,0xF8,0x42,0x01,0x0D,0x04,0x2B,0x16,0x29,0x53,0x74, +0x61,0x72,0x74,0x43,0x6F,0x6D,0x20,0x46,0x72,0x65,0x65,0x20,0x53,0x53,0x4C,0x20, +0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75, +0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7, +0x0D,0x01,0x01,0x0B,0x05,0x00,0x03,0x82,0x02,0x01,0x00,0x8E,0x8F,0xE7,0xDC,0x94, +0x79,0x7C,0xF1,0x85,0x7F,0x9F,0x49,0x6F,0x6B,0xCA,0x5D,0xFB,0x8C,0xFE,0x04,0xC5, +0xC1,0x62,0xD1,0x7D,0x42,0x8A,0xBC,0x53,0xB7,0x94,0x03,0x66,0x30,0x3F,0xB1,0xE7, +0x0A,0xA7,0x50,0x20,0x55,0x25,0x7F,0x76,0x7A,0x14,0x0D,0xEB,0x04,0x0E,0x40,0xE6, +0x3E,0xD8,0x88,0xAB,0x07,0x27,0x83,0xA9,0x75,0xA6,0x37,0x73,0xC7,0xFD,0x4B,0xD2, +0x4D,0xAD,0x17,0x40,0xC8,0x46,0xBE,0x3B,0x7F,0x51,0xFC,0xC3,0xB6,0x05,0x31,0xDC, +0xCD,0x85,0x22,0x4E,0x71,0xB7,0xF2,0x71,0x5E,0xB0,0x1A,0xC6,0xBA,0x93,0x8B,0x78, +0x92,0x4A,0x85,0xF8,0x78,0x0F,0x83,0xFE,0x2F,0xAD,0x2C,0xF7,0xE4,0xA4,0xBB,0x2D, +0xD0,0xE7,0x0D,0x3A,0xB8,0x3E,0xCE,0xF6,0x78,0xF6,0xAE,0x47,0x24,0xCA,0xA3,0x35, +0x36,0xCE,0xC7,0xC6,0x87,0x98,0xDA,0xEC,0xFB,0xE9,0xB2,0xCE,0x27,0x9B,0x88,0xC3, +0x04,0xA1,0xF6,0x0B,0x59,0x68,0xAF,0xC9,0xDB,0x10,0x0F,0x4D,0xF6,0x64,0x63,0x5C, +0xA5,0x12,0x6F,0x92,0xB2,0x93,0x94,0xC7,0x88,0x17,0x0E,0x93,0xB6,0x7E,0x62,0x8B, +0x90,0x7F,0xAB,0x4E,0x9F,0xFC,0xE3,0x75,0x14,0x4F,0x2A,0x32,0xDF,0x5B,0x0D,0xE0, +0xF5,0x7B,0x93,0x0D,0xAB,0xA1,0xCF,0x87,0xE1,0xA5,0x04,0x45,0xE8,0x3C,0x12,0xA5, +0x09,0xC5,0xB0,0xD1,0xB7,0x53,0xF3,0x60,0x14,0xBA,0x85,0x69,0x6A,0x21,0x7C,0x1F, +0x75,0x61,0x17,0x20,0x17,0x7B,0x6C,0x3B,0x41,0x29,0x5C,0xE1,0xAC,0x5A,0xD1,0xCD, +0x8C,0x9B,0xEB,0x60,0x1D,0x19,0xEC,0xF7,0xE5,0xB0,0xDA,0xF9,0x79,0x18,0xA5,0x45, +0x3F,0x49,0x43,0x57,0xD2,0xDD,0x24,0xD5,0x2C,0xA3,0xFD,0x91,0x8D,0x27,0xB5,0xE5, +0xEB,0x14,0x06,0x9A,0x4C,0x7B,0x21,0xBB,0x3A,0xAD,0x30,0x06,0x18,0xC0,0xD8,0xC1, +0x6B,0x2C,0x7F,0x59,0x5C,0x5D,0x91,0xB1,0x70,0x22,0x57,0xEB,0x8A,0x6B,0x48,0x4A, +0xD5,0x0F,0x29,0xEC,0xC6,0x40,0xC0,0x2F,0x88,0x4C,0x68,0x01,0x17,0x77,0xF4,0x24, +0x19,0x4F,0xBD,0xFA,0xE1,0xB2,0x20,0x21,0x4B,0xDD,0x1A,0xD8,0x29,0x7D,0xAA,0xB8, +0xDE,0x54,0xEC,0x21,0x55,0x80,0x6C,0x1E,0xF5,0x30,0xC8,0xA3,0x10,0xE5,0xB2,0xE6, +0x2A,0x14,0x31,0xC3,0x85,0x2D,0x8C,0x98,0xB1,0x86,0x5A,0x4F,0x89,0x59,0x2D,0xB9, +0xC7,0xF7,0x1C,0xC8,0x8A,0x7F,0xC0,0x9D,0x05,0x4A,0xE6,0x42,0x4F,0x62,0xA3,0x6D, +0x29,0xA4,0x1F,0x85,0xAB,0xDB,0xE5,0x81,0xC8,0xAD,0x2A,0x3D,0x4C,0x5D,0x5B,0x84, +0x26,0x71,0xC4,0x85,0x5E,0x71,0x24,0xCA,0xA5,0x1B,0x6C,0xD8,0x61,0xD3,0x1A,0xE0, +0x54,0xDB,0xCE,0xBA,0xA9,0x32,0xB5,0x22,0xF6,0x73,0x41,0x09,0x5D,0xB8,0x17,0x5D, +0x0E,0x0F,0x99,0x90,0xD6,0x47,0xDA,0x6F,0x0A,0x3A,0x62,0x28,0x14,0x67,0x82,0xD9, +0xF1,0xD0,0x80,0x59,0x9B,0xCB,0x31,0xD8,0x9B,0x0F,0x8C,0x77,0x4E,0xB5,0x68,0x8A, +0xF2,0x6C,0xF6,0x24,0x0E,0x2D,0x6C,0x70,0xC5,0x73,0xD1,0xDE,0x14,0xD0,0x71,0x8F, +0xB6,0xD3,0x7B,0x02,0xF6,0xE3,0xB8,0xD4,0x09,0x6E,0x6B,0x9E,0x75,0x84,0x39,0xE6, +0x7F,0x25,0xA5,0xF2,0x48,0x00,0xC0,0xA4,0x01,0xDA,0x3F, +}; + + +/* subject:/C=IL/O=StartCom Ltd./CN=StartCom Certification Authority G2 */ +/* issuer :/C=IL/O=StartCom Ltd./CN=StartCom Certification Authority G2 */ + + +const unsigned char StartCom_Certification_Authority_G2_certificate[1383]={ +0x30,0x82,0x05,0x63,0x30,0x82,0x03,0x4B,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x3B, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x30, +0x53,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x49,0x4C,0x31,0x16, +0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x53,0x74,0x61,0x72,0x74,0x43,0x6F, +0x6D,0x20,0x4C,0x74,0x64,0x2E,0x31,0x2C,0x30,0x2A,0x06,0x03,0x55,0x04,0x03,0x13, +0x23,0x53,0x74,0x61,0x72,0x74,0x43,0x6F,0x6D,0x20,0x43,0x65,0x72,0x74,0x69,0x66, +0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74, +0x79,0x20,0x47,0x32,0x30,0x1E,0x17,0x0D,0x31,0x30,0x30,0x31,0x30,0x31,0x30,0x31, +0x30,0x30,0x30,0x31,0x5A,0x17,0x0D,0x33,0x39,0x31,0x32,0x33,0x31,0x32,0x33,0x35, +0x39,0x30,0x31,0x5A,0x30,0x53,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13, +0x02,0x49,0x4C,0x31,0x16,0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x53,0x74, +0x61,0x72,0x74,0x43,0x6F,0x6D,0x20,0x4C,0x74,0x64,0x2E,0x31,0x2C,0x30,0x2A,0x06, +0x03,0x55,0x04,0x03,0x13,0x23,0x53,0x74,0x61,0x72,0x74,0x43,0x6F,0x6D,0x20,0x43, +0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74, +0x68,0x6F,0x72,0x69,0x74,0x79,0x20,0x47,0x32,0x30,0x82,0x02,0x22,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x02,0x0F, +0x00,0x30,0x82,0x02,0x0A,0x02,0x82,0x02,0x01,0x00,0xB6,0x89,0x36,0x5B,0x07,0xB7, +0x20,0x36,0xBD,0x82,0xBB,0xE1,0x16,0x20,0x03,0x95,0x7A,0xAF,0x0E,0xA3,0x55,0xC9, +0x25,0x99,0x4A,0xC5,0xD0,0x56,0x41,0x87,0x90,0x4D,0x21,0x60,0xA4,0x14,0x87,0x3B, +0xCD,0xFD,0xB2,0x3E,0xB4,0x67,0x03,0x6A,0xED,0xE1,0x0F,0x4B,0xC0,0x91,0x85,0x70, +0x45,0xE0,0x42,0x9E,0xDE,0x29,0x23,0xD4,0x01,0x0D,0xA0,0x10,0x79,0xB8,0xDB,0x03, +0xBD,0xF3,0xA9,0x2F,0xD1,0xC6,0xE0,0x0F,0xCB,0x9E,0x8A,0x14,0x0A,0xB8,0xBD,0xF6, +0x56,0x62,0xF1,0xC5,0x72,0xB6,0x32,0x25,0xD9,0xB2,0xF3,0xBD,0x65,0xC5,0x0D,0x2C, +0x6E,0xD5,0x92,0x6F,0x18,0x8B,0x00,0x41,0x14,0x82,0x6F,0x40,0x20,0x26,0x7A,0x28, +0x0F,0xF5,0x1E,0x7F,0x27,0xF7,0x94,0xB1,0x37,0x3D,0xB7,0xC7,0x91,0xF7,0xE2,0x01, +0xEC,0xFD,0x94,0x89,0xE1,0xCC,0x6E,0xD3,0x36,0xD6,0x0A,0x19,0x79,0xAE,0xD7,0x34, +0x82,0x65,0xFF,0x7C,0x42,0xBB,0xB6,0xDD,0x0B,0xA6,0x34,0xAF,0x4B,0x60,0xFE,0x7F, +0x43,0x49,0x06,0x8B,0x8C,0x43,0xB8,0x56,0xF2,0xD9,0x7F,0x21,0x43,0x17,0xEA,0xA7, +0x48,0x95,0x01,0x75,0x75,0xEA,0x2B,0xA5,0x43,0x95,0xEA,0x15,0x84,0x9D,0x08,0x8D, +0x26,0x6E,0x55,0x9B,0xAB,0xDC,0xD2,0x39,0xD2,0x31,0x1D,0x60,0xE2,0xAC,0xCC,0x56, +0x45,0x24,0xF5,0x1C,0x54,0xAB,0xEE,0x86,0xDD,0x96,0x32,0x85,0xF8,0x4C,0x4F,0xE8, +0x95,0x76,0xB6,0x05,0xDD,0x36,0x23,0x67,0xBC,0xFF,0x15,0xE2,0xCA,0x3B,0xE6,0xA6, +0xEC,0x3B,0xEC,0x26,0x11,0x34,0x48,0x8D,0xF6,0x80,0x2B,0x1A,0x23,0x02,0xEB,0x8A, +0x1C,0x3A,0x76,0x2A,0x7B,0x56,0x16,0x1C,0x72,0x2A,0xB3,0xAA,0xE3,0x60,0xA5,0x00, +0x9F,0x04,0x9B,0xE2,0x6F,0x1E,0x14,0x58,0x5B,0xA5,0x6C,0x8B,0x58,0x3C,0xC3,0xBA, +0x4E,0x3A,0x5C,0xF7,0xE1,0x96,0x2B,0x3E,0xEF,0x07,0xBC,0xA4,0xE5,0x5D,0xCC,0x4D, +0x9F,0x0D,0xE1,0xDC,0xAA,0xBB,0xE1,0x6E,0x1A,0xEC,0x8F,0xE1,0xB6,0x4C,0x4D,0x79, +0x72,0x5D,0x17,0x35,0x0B,0x1D,0xD7,0xC1,0x47,0xDA,0x96,0x24,0xE0,0xD0,0x72,0xA8, +0x5A,0x5F,0x66,0x2D,0x10,0xDC,0x2F,0x2A,0x13,0xAE,0x26,0xFE,0x0A,0x1C,0x19,0xCC, +0xD0,0x3E,0x0B,0x9C,0xC8,0x09,0x2E,0xF9,0x5B,0x96,0x7A,0x47,0x9C,0xE9,0x7A,0xF3, +0x05,0x50,0x74,0x95,0x73,0x9E,0x30,0x09,0xF3,0x97,0x82,0x5E,0xE6,0x8F,0x39,0x08, +0x1E,0x59,0xE5,0x35,0x14,0x42,0x13,0xFF,0x00,0x9C,0xF7,0xBE,0xAA,0x50,0xCF,0xE2, +0x51,0x48,0xD7,0xB8,0x6F,0xAF,0xF8,0x4E,0x7E,0x33,0x98,0x92,0x14,0x62,0x3A,0x75, +0x63,0xCF,0x7B,0xFA,0xDE,0x82,0x3B,0xA9,0xBB,0x39,0xE2,0xC4,0xBD,0x2C,0x00,0x0E, +0xC8,0x17,0xAC,0x13,0xEF,0x4D,0x25,0x8E,0xD8,0xB3,0x90,0x2F,0xA9,0xDA,0x29,0x7D, +0x1D,0xAF,0x74,0x3A,0xB2,0x27,0xC0,0xC1,0x1E,0x3E,0x75,0xA3,0x16,0xA9,0xAF,0x7A, +0x22,0x5D,0x9F,0x13,0x1A,0xCF,0xA7,0xA0,0xEB,0xE3,0x86,0x0A,0xD3,0xFD,0xE6,0x96, +0x95,0xD7,0x23,0xC8,0x37,0xDD,0xC4,0x7C,0xAA,0x36,0xAC,0x98,0x1A,0x12,0xB1,0xE0, +0x4E,0xE8,0xB1,0x3B,0xF5,0xD6,0x6F,0xF1,0x30,0xD7,0x02,0x03,0x01,0x00,0x01,0xA3, +0x42,0x30,0x40,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30, +0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04, +0x03,0x02,0x01,0x06,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x4B, +0xC5,0xB4,0x40,0x6B,0xAD,0x1C,0xB3,0xA5,0x1C,0x65,0x6E,0x46,0x36,0x89,0x87,0x05, +0x0C,0x0E,0xB6,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B, +0x05,0x00,0x03,0x82,0x02,0x01,0x00,0x73,0x57,0x3F,0x2C,0xD5,0x95,0x32,0x7E,0x37, +0xDB,0x96,0x92,0xEB,0x19,0x5E,0x7E,0x53,0xE7,0x41,0xEC,0x11,0xB6,0x47,0xEF,0xB5, +0xDE,0xED,0x74,0x5C,0xC5,0xF1,0x8E,0x49,0xE0,0xFC,0x6E,0x99,0x13,0xCD,0x9F,0x8A, +0xDA,0xCD,0x3A,0x0A,0xD8,0x3A,0x5A,0x09,0x3F,0x5F,0x34,0xD0,0x2F,0x03,0xD2,0x66, +0x1D,0x1A,0xBD,0x9C,0x90,0x37,0xC8,0x0C,0x8E,0x07,0x5A,0x94,0x45,0x46,0x2A,0xE6, +0xBE,0x7A,0xDA,0xA1,0xA9,0xA4,0x69,0x12,0x92,0xB0,0x7D,0x36,0xD4,0x44,0x87,0xD7, +0x51,0xF1,0x29,0x63,0xD6,0x75,0xCD,0x16,0xE4,0x27,0x89,0x1D,0xF8,0xC2,0x32,0x48, +0xFD,0xDB,0x99,0xD0,0x8F,0x5F,0x54,0x74,0xCC,0xAC,0x67,0x34,0x11,0x62,0xD9,0x0C, +0x0A,0x37,0x87,0xD1,0xA3,0x17,0x48,0x8E,0xD2,0x17,0x1D,0xF6,0xD7,0xFD,0xDB,0x65, +0xEB,0xFD,0xA8,0xD4,0xF5,0xD6,0x4F,0xA4,0x5B,0x75,0xE8,0xC5,0xD2,0x60,0xB2,0xDB, +0x09,0x7E,0x25,0x8B,0x7B,0xBA,0x52,0x92,0x9E,0x3E,0xE8,0xC5,0x77,0xA1,0x3C,0xE0, +0x4A,0x73,0x6B,0x61,0xCF,0x86,0xDC,0x43,0xFF,0xFF,0x21,0xFE,0x23,0x5D,0x24,0x4A, +0xF5,0xD3,0x6D,0x0F,0x62,0x04,0x05,0x57,0x82,0xDA,0x6E,0xA4,0x33,0x25,0x79,0x4B, +0x2E,0x54,0x19,0x8B,0xCC,0x2C,0x3D,0x30,0xE9,0xD1,0x06,0xFF,0xE8,0x32,0x46,0xBE, +0xB5,0x33,0x76,0x77,0xA8,0x01,0x5D,0x96,0xC1,0xC1,0xD5,0xBE,0xAE,0x25,0xC0,0xC9, +0x1E,0x0A,0x09,0x20,0x88,0xA1,0x0E,0xC9,0xF3,0x6F,0x4D,0x82,0x54,0x00,0x20,0xA7, +0xD2,0x8F,0xE4,0x39,0x54,0x17,0x2E,0x8D,0x1E,0xB8,0x1B,0xBB,0x1B,0xBD,0x9A,0x4E, +0x3B,0x10,0x34,0xDC,0x9C,0x88,0x53,0xEF,0xA2,0x31,0x5B,0x58,0x4F,0x91,0x62,0xC8, +0xC2,0x9A,0x9A,0xCD,0x15,0x5D,0x38,0xA9,0xD6,0xBE,0xF8,0x13,0xB5,0x9F,0x12,0x69, +0xF2,0x50,0x62,0xAC,0xFB,0x17,0x37,0xF4,0xEE,0xB8,0x75,0x67,0x60,0x10,0xFB,0x83, +0x50,0xF9,0x44,0xB5,0x75,0x9C,0x40,0x17,0xB2,0xFE,0xFD,0x79,0x5D,0x6E,0x58,0x58, +0x5F,0x30,0xFC,0x00,0xAE,0xAF,0x33,0xC1,0x0E,0x4E,0x6C,0xBA,0xA7,0xA6,0xA1,0x7F, +0x32,0xDB,0x38,0xE0,0xB1,0x72,0x17,0x0A,0x2B,0x91,0xEC,0x6A,0x63,0x26,0xED,0x89, +0xD4,0x78,0xCC,0x74,0x1E,0x05,0xF8,0x6B,0xFE,0x8C,0x6A,0x76,0x39,0x29,0xAE,0x65, +0x23,0x12,0x95,0x08,0x22,0x1C,0x97,0xCE,0x5B,0x06,0xEE,0x0C,0xE2,0xBB,0xBC,0x1F, +0x44,0x93,0xF6,0xD8,0x38,0x45,0x05,0x21,0xED,0xE4,0xAD,0xAB,0x12,0xB6,0x03,0xA4, +0x42,0x2E,0x2D,0xC4,0x09,0x3A,0x03,0x67,0x69,0x84,0x9A,0xE1,0x59,0x90,0x8A,0x28, +0x85,0xD5,0x5D,0x74,0xB1,0xD1,0x0E,0x20,0x58,0x9B,0x13,0xA5,0xB0,0x63,0xA6,0xED, +0x7B,0x47,0xFD,0x45,0x55,0x30,0xA4,0xEE,0x9A,0xD4,0xE6,0xE2,0x87,0xEF,0x98,0xC9, +0x32,0x82,0x11,0x29,0x22,0xBC,0x00,0x0A,0x31,0x5E,0x2D,0x0F,0xC0,0x8E,0xE9,0x6B, +0xB2,0x8F,0x2E,0x06,0xD8,0xD1,0x91,0xC7,0xC6,0x12,0xF4,0x4C,0xFD,0x30,0x17,0xC3, +0xC1,0xDA,0x38,0x5B,0xE3,0xA9,0xEA,0xE6,0xA1,0xBA,0x79,0xEF,0x73,0xD8,0xB6,0x53, +0x57,0x2D,0xF6,0xD0,0xE1,0xD7,0x48, +}; + + +/* subject:/C=DE/O=TC TrustCenter GmbH/OU=TC TrustCenter Class 2 CA/CN=TC TrustCenter Class 2 CA II */ +/* issuer :/C=DE/O=TC TrustCenter GmbH/OU=TC TrustCenter Class 2 CA/CN=TC TrustCenter Class 2 CA II */ + + +const unsigned char TC_TrustCenter_Class_2_CA_II_certificate[1198]={ +0x30,0x82,0x04,0xAA,0x30,0x82,0x03,0x92,0xA0,0x03,0x02,0x01,0x02,0x02,0x0E,0x2E, +0x6A,0x00,0x01,0x00,0x02,0x1F,0xD7,0x52,0x21,0x2C,0x11,0x5C,0x3B,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x76,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x44,0x45,0x31,0x1C,0x30,0x1A,0x06, +0x03,0x55,0x04,0x0A,0x13,0x13,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65, +0x6E,0x74,0x65,0x72,0x20,0x47,0x6D,0x62,0x48,0x31,0x22,0x30,0x20,0x06,0x03,0x55, +0x04,0x0B,0x13,0x19,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74, +0x65,0x72,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x32,0x20,0x43,0x41,0x31,0x25,0x30, +0x23,0x06,0x03,0x55,0x04,0x03,0x13,0x1C,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74, +0x43,0x65,0x6E,0x74,0x65,0x72,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x32,0x20,0x43, +0x41,0x20,0x49,0x49,0x30,0x1E,0x17,0x0D,0x30,0x36,0x30,0x31,0x31,0x32,0x31,0x34, +0x33,0x38,0x34,0x33,0x5A,0x17,0x0D,0x32,0x35,0x31,0x32,0x33,0x31,0x32,0x32,0x35, +0x39,0x35,0x39,0x5A,0x30,0x76,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13, +0x02,0x44,0x45,0x31,0x1C,0x30,0x1A,0x06,0x03,0x55,0x04,0x0A,0x13,0x13,0x54,0x43, +0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74,0x65,0x72,0x20,0x47,0x6D,0x62, +0x48,0x31,0x22,0x30,0x20,0x06,0x03,0x55,0x04,0x0B,0x13,0x19,0x54,0x43,0x20,0x54, +0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74,0x65,0x72,0x20,0x43,0x6C,0x61,0x73,0x73, +0x20,0x32,0x20,0x43,0x41,0x31,0x25,0x30,0x23,0x06,0x03,0x55,0x04,0x03,0x13,0x1C, +0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74,0x65,0x72,0x20,0x43, +0x6C,0x61,0x73,0x73,0x20,0x32,0x20,0x43,0x41,0x20,0x49,0x49,0x30,0x82,0x01,0x22, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03, +0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xAB,0x80,0x87, +0x9B,0x8E,0xF0,0xC3,0x7C,0x87,0xD7,0xE8,0x24,0x82,0x11,0xB3,0x3C,0xDD,0x43,0x62, +0xEE,0xF8,0xC3,0x45,0xDA,0xE8,0xE1,0xA0,0x5F,0xD1,0x2A,0xB2,0xEA,0x93,0x68,0xDF, +0xB4,0xC8,0xD6,0x43,0xE9,0xC4,0x75,0x59,0x7F,0xFC,0xE1,0x1D,0xF8,0x31,0x70,0x23, +0x1B,0x88,0x9E,0x27,0xB9,0x7B,0xFD,0x3A,0xD2,0xC9,0xA9,0xE9,0x14,0x2F,0x90,0xBE, +0x03,0x52,0xC1,0x49,0xCD,0xF6,0xFD,0xE4,0x08,0x66,0x0B,0x57,0x8A,0xA2,0x42,0xA0, +0xB8,0xD5,0x7F,0x69,0x5C,0x90,0x32,0xB2,0x97,0x0D,0xCA,0x4A,0xDC,0x46,0x3E,0x02, +0x55,0x89,0x53,0xE3,0x1A,0x5A,0xCB,0x36,0xC6,0x07,0x56,0xF7,0x8C,0xCF,0x11,0xF4, +0x4C,0xBB,0x30,0x70,0x04,0x95,0xA5,0xF6,0x39,0x8C,0xFD,0x73,0x81,0x08,0x7D,0x89, +0x5E,0x32,0x1E,0x22,0xA9,0x22,0x45,0x4B,0xB0,0x66,0x2E,0x30,0xCC,0x9F,0x65,0xFD, +0xFC,0xCB,0x81,0xA9,0xF1,0xE0,0x3B,0xAF,0xA3,0x86,0xD1,0x89,0xEA,0xC4,0x45,0x79, +0x50,0x5D,0xAE,0xE9,0x21,0x74,0x92,0x4D,0x8B,0x59,0x82,0x8F,0x94,0xE3,0xE9,0x4A, +0xF1,0xE7,0x49,0xB0,0x14,0xE3,0xF5,0x62,0xCB,0xD5,0x72,0xBD,0x1F,0xB9,0xD2,0x9F, +0xA0,0xCD,0xA8,0xFA,0x01,0xC8,0xD9,0x0D,0xDF,0xDA,0xFC,0x47,0x9D,0xB3,0xC8,0x54, +0xDF,0x49,0x4A,0xF1,0x21,0xA9,0xFE,0x18,0x4E,0xEE,0x48,0xD4,0x19,0xBB,0xEF,0x7D, +0xE4,0xE2,0x9D,0xCB,0x5B,0xB6,0x6E,0xFF,0xE3,0xCD,0x5A,0xE7,0x74,0x82,0x05,0xBA, +0x80,0x25,0x38,0xCB,0xE4,0x69,0x9E,0xAF,0x41,0xAA,0x1A,0x84,0xF5,0x02,0x03,0x01, +0x00,0x01,0xA3,0x82,0x01,0x34,0x30,0x82,0x01,0x30,0x30,0x0F,0x06,0x03,0x55,0x1D, +0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03,0x55, +0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x1D,0x06,0x03,0x55, +0x1D,0x0E,0x04,0x16,0x04,0x14,0xE3,0xAB,0x54,0x4C,0x80,0xA1,0xDB,0x56,0x43,0xB7, +0x91,0x4A,0xCB,0xF3,0x82,0x7A,0x13,0x5C,0x08,0xAB,0x30,0x81,0xED,0x06,0x03,0x55, +0x1D,0x1F,0x04,0x81,0xE5,0x30,0x81,0xE2,0x30,0x81,0xDF,0xA0,0x81,0xDC,0xA0,0x81, +0xD9,0x86,0x35,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E,0x74,0x72, +0x75,0x73,0x74,0x63,0x65,0x6E,0x74,0x65,0x72,0x2E,0x64,0x65,0x2F,0x63,0x72,0x6C, +0x2F,0x76,0x32,0x2F,0x74,0x63,0x5F,0x63,0x6C,0x61,0x73,0x73,0x5F,0x32,0x5F,0x63, +0x61,0x5F,0x49,0x49,0x2E,0x63,0x72,0x6C,0x86,0x81,0x9F,0x6C,0x64,0x61,0x70,0x3A, +0x2F,0x2F,0x77,0x77,0x77,0x2E,0x74,0x72,0x75,0x73,0x74,0x63,0x65,0x6E,0x74,0x65, +0x72,0x2E,0x64,0x65,0x2F,0x43,0x4E,0x3D,0x54,0x43,0x25,0x32,0x30,0x54,0x72,0x75, +0x73,0x74,0x43,0x65,0x6E,0x74,0x65,0x72,0x25,0x32,0x30,0x43,0x6C,0x61,0x73,0x73, +0x25,0x32,0x30,0x32,0x25,0x32,0x30,0x43,0x41,0x25,0x32,0x30,0x49,0x49,0x2C,0x4F, +0x3D,0x54,0x43,0x25,0x32,0x30,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74,0x65, +0x72,0x25,0x32,0x30,0x47,0x6D,0x62,0x48,0x2C,0x4F,0x55,0x3D,0x72,0x6F,0x6F,0x74, +0x63,0x65,0x72,0x74,0x73,0x2C,0x44,0x43,0x3D,0x74,0x72,0x75,0x73,0x74,0x63,0x65, +0x6E,0x74,0x65,0x72,0x2C,0x44,0x43,0x3D,0x64,0x65,0x3F,0x63,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x65,0x52,0x65,0x76,0x6F,0x63,0x61,0x74,0x69,0x6F,0x6E, +0x4C,0x69,0x73,0x74,0x3F,0x62,0x61,0x73,0x65,0x3F,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x8C,0xD7, +0xDF,0x7E,0xEE,0x1B,0x80,0x10,0xB3,0x83,0xF5,0xDB,0x11,0xEA,0x6B,0x4B,0xA8,0x92, +0x18,0xD9,0xF7,0x07,0x39,0xF5,0x2C,0xBE,0x06,0x75,0x7A,0x68,0x53,0x15,0x1C,0xEA, +0x4A,0xED,0x5E,0xFC,0x23,0xB2,0x13,0xA0,0xD3,0x09,0xFF,0xF6,0xF6,0x2E,0x6B,0x41, +0x71,0x79,0xCD,0xE2,0x6D,0xFD,0xAE,0x59,0x6B,0x85,0x1D,0xB8,0x4E,0x22,0x9A,0xED, +0x66,0x39,0x6E,0x4B,0x94,0xE6,0x55,0xFC,0x0B,0x1B,0x8B,0x77,0xC1,0x53,0x13,0x66, +0x89,0xD9,0x28,0xD6,0x8B,0xF3,0x45,0x4A,0x63,0xB7,0xFD,0x7B,0x0B,0x61,0x5D,0xB8, +0x6D,0xBE,0xC3,0xDC,0x5B,0x79,0xD2,0xED,0x86,0xE5,0xA2,0x4D,0xBE,0x5E,0x74,0x7C, +0x6A,0xED,0x16,0x38,0x1F,0x7F,0x58,0x81,0x5A,0x1A,0xEB,0x32,0x88,0x2D,0xB2,0xF3, +0x39,0x77,0x80,0xAF,0x5E,0xB6,0x61,0x75,0x29,0xDB,0x23,0x4D,0x88,0xCA,0x50,0x28, +0xCB,0x85,0xD2,0xD3,0x10,0xA2,0x59,0x6E,0xD3,0x93,0x54,0x00,0x7A,0xA2,0x46,0x95, +0x86,0x05,0x9C,0xA9,0x19,0x98,0xE5,0x31,0x72,0x0C,0x00,0xE2,0x67,0xD9,0x40,0xE0, +0x24,0x33,0x7B,0x6F,0x2C,0xB9,0x5C,0xAB,0x65,0x9D,0x2C,0xAC,0x76,0xEA,0x35,0x99, +0xF5,0x97,0xB9,0x0F,0x24,0xEC,0xC7,0x76,0x21,0x28,0x65,0xAE,0x57,0xE8,0x07,0x88, +0x75,0x4A,0x56,0xA0,0xD2,0x05,0x3A,0xA4,0xE6,0x8D,0x92,0x88,0x2C,0xF3,0xF2,0xE1, +0xC1,0xC6,0x61,0xDB,0x41,0xC5,0xC7,0x9B,0xF7,0x0E,0x1A,0x51,0x45,0xC2,0x61,0x6B, +0xDC,0x64,0x27,0x17,0x8C,0x5A,0xB7,0xDA,0x74,0x28,0xCD,0x97,0xE4,0xBD, +}; + + +/* subject:/C=DE/O=TC TrustCenter GmbH/OU=TC TrustCenter Class 3 CA/CN=TC TrustCenter Class 3 CA II */ +/* issuer :/C=DE/O=TC TrustCenter GmbH/OU=TC TrustCenter Class 3 CA/CN=TC TrustCenter Class 3 CA II */ + + +const unsigned char TC_TrustCenter_Class_3_CA_II_certificate[1198]={ +0x30,0x82,0x04,0xAA,0x30,0x82,0x03,0x92,0xA0,0x03,0x02,0x01,0x02,0x02,0x0E,0x4A, +0x47,0x00,0x01,0x00,0x02,0xE5,0xA0,0x5D,0xD6,0x3F,0x00,0x51,0xBF,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x76,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x44,0x45,0x31,0x1C,0x30,0x1A,0x06, +0x03,0x55,0x04,0x0A,0x13,0x13,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65, +0x6E,0x74,0x65,0x72,0x20,0x47,0x6D,0x62,0x48,0x31,0x22,0x30,0x20,0x06,0x03,0x55, +0x04,0x0B,0x13,0x19,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74, +0x65,0x72,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x33,0x20,0x43,0x41,0x31,0x25,0x30, +0x23,0x06,0x03,0x55,0x04,0x03,0x13,0x1C,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74, +0x43,0x65,0x6E,0x74,0x65,0x72,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x33,0x20,0x43, +0x41,0x20,0x49,0x49,0x30,0x1E,0x17,0x0D,0x30,0x36,0x30,0x31,0x31,0x32,0x31,0x34, +0x34,0x31,0x35,0x37,0x5A,0x17,0x0D,0x32,0x35,0x31,0x32,0x33,0x31,0x32,0x32,0x35, +0x39,0x35,0x39,0x5A,0x30,0x76,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13, +0x02,0x44,0x45,0x31,0x1C,0x30,0x1A,0x06,0x03,0x55,0x04,0x0A,0x13,0x13,0x54,0x43, +0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74,0x65,0x72,0x20,0x47,0x6D,0x62, +0x48,0x31,0x22,0x30,0x20,0x06,0x03,0x55,0x04,0x0B,0x13,0x19,0x54,0x43,0x20,0x54, +0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74,0x65,0x72,0x20,0x43,0x6C,0x61,0x73,0x73, +0x20,0x33,0x20,0x43,0x41,0x31,0x25,0x30,0x23,0x06,0x03,0x55,0x04,0x03,0x13,0x1C, +0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74,0x65,0x72,0x20,0x43, +0x6C,0x61,0x73,0x73,0x20,0x33,0x20,0x43,0x41,0x20,0x49,0x49,0x30,0x82,0x01,0x22, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03, +0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xB4,0xE0,0xBB, +0x51,0xBB,0x39,0x5C,0x8B,0x04,0xC5,0x4C,0x79,0x1C,0x23,0x86,0x31,0x10,0x63,0x43, +0x55,0x27,0x3F,0xC6,0x45,0xC7,0xA4,0x3D,0xEC,0x09,0x0D,0x1A,0x1E,0x20,0xC2,0x56, +0x1E,0xDE,0x1B,0x37,0x07,0x30,0x22,0x2F,0x6F,0xF1,0x06,0xF1,0xAB,0xAD,0xD6,0xC8, +0xAB,0x61,0xA3,0x2F,0x43,0xC4,0xB0,0xB2,0x2D,0xFC,0xC3,0x96,0x69,0x7B,0x7E,0x8A, +0xE4,0xCC,0xC0,0x39,0x12,0x90,0x42,0x60,0xC9,0xCC,0x35,0x68,0xEE,0xDA,0x5F,0x90, +0x56,0x5F,0xCD,0x1C,0x4D,0x5B,0x58,0x49,0xEB,0x0E,0x01,0x4F,0x64,0xFA,0x2C,0x3C, +0x89,0x58,0xD8,0x2F,0x2E,0xE2,0xB0,0x68,0xE9,0x22,0x3B,0x75,0x89,0xD6,0x44,0x1A, +0x65,0xF2,0x1B,0x97,0x26,0x1D,0x28,0x6D,0xAC,0xE8,0xBD,0x59,0x1D,0x2B,0x24,0xF6, +0xD6,0x84,0x03,0x66,0x88,0x24,0x00,0x78,0x60,0xF1,0xF8,0xAB,0xFE,0x02,0xB2,0x6B, +0xFB,0x22,0xFB,0x35,0xE6,0x16,0xD1,0xAD,0xF6,0x2E,0x12,0xE4,0xFA,0x35,0x6A,0xE5, +0x19,0xB9,0x5D,0xDB,0x3B,0x1E,0x1A,0xFB,0xD3,0xFF,0x15,0x14,0x08,0xD8,0x09,0x6A, +0xBA,0x45,0x9D,0x14,0x79,0x60,0x7D,0xAF,0x40,0x8A,0x07,0x73,0xB3,0x93,0x96,0xD3, +0x74,0x34,0x8D,0x3A,0x37,0x29,0xDE,0x5C,0xEC,0xF5,0xEE,0x2E,0x31,0xC2,0x20,0xDC, +0xBE,0xF1,0x4F,0x7F,0x23,0x52,0xD9,0x5B,0xE2,0x64,0xD9,0x9C,0xAA,0x07,0x08,0xB5, +0x45,0xBD,0xD1,0xD0,0x31,0xC1,0xAB,0x54,0x9F,0xA9,0xD2,0xC3,0x62,0x60,0x03,0xF1, +0xBB,0x39,0x4A,0x92,0x4A,0x3D,0x0A,0xB9,0x9D,0xC5,0xA0,0xFE,0x37,0x02,0x03,0x01, +0x00,0x01,0xA3,0x82,0x01,0x34,0x30,0x82,0x01,0x30,0x30,0x0F,0x06,0x03,0x55,0x1D, +0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03,0x55, +0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x1D,0x06,0x03,0x55, +0x1D,0x0E,0x04,0x16,0x04,0x14,0xD4,0xA2,0xFC,0x9F,0xB3,0xC3,0xD8,0x03,0xD3,0x57, +0x5C,0x07,0xA4,0xD0,0x24,0xA7,0xC0,0xF2,0x00,0xD4,0x30,0x81,0xED,0x06,0x03,0x55, +0x1D,0x1F,0x04,0x81,0xE5,0x30,0x81,0xE2,0x30,0x81,0xDF,0xA0,0x81,0xDC,0xA0,0x81, +0xD9,0x86,0x35,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E,0x74,0x72, +0x75,0x73,0x74,0x63,0x65,0x6E,0x74,0x65,0x72,0x2E,0x64,0x65,0x2F,0x63,0x72,0x6C, +0x2F,0x76,0x32,0x2F,0x74,0x63,0x5F,0x63,0x6C,0x61,0x73,0x73,0x5F,0x33,0x5F,0x63, +0x61,0x5F,0x49,0x49,0x2E,0x63,0x72,0x6C,0x86,0x81,0x9F,0x6C,0x64,0x61,0x70,0x3A, +0x2F,0x2F,0x77,0x77,0x77,0x2E,0x74,0x72,0x75,0x73,0x74,0x63,0x65,0x6E,0x74,0x65, +0x72,0x2E,0x64,0x65,0x2F,0x43,0x4E,0x3D,0x54,0x43,0x25,0x32,0x30,0x54,0x72,0x75, +0x73,0x74,0x43,0x65,0x6E,0x74,0x65,0x72,0x25,0x32,0x30,0x43,0x6C,0x61,0x73,0x73, +0x25,0x32,0x30,0x33,0x25,0x32,0x30,0x43,0x41,0x25,0x32,0x30,0x49,0x49,0x2C,0x4F, +0x3D,0x54,0x43,0x25,0x32,0x30,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74,0x65, +0x72,0x25,0x32,0x30,0x47,0x6D,0x62,0x48,0x2C,0x4F,0x55,0x3D,0x72,0x6F,0x6F,0x74, +0x63,0x65,0x72,0x74,0x73,0x2C,0x44,0x43,0x3D,0x74,0x72,0x75,0x73,0x74,0x63,0x65, +0x6E,0x74,0x65,0x72,0x2C,0x44,0x43,0x3D,0x64,0x65,0x3F,0x63,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x65,0x52,0x65,0x76,0x6F,0x63,0x61,0x74,0x69,0x6F,0x6E, +0x4C,0x69,0x73,0x74,0x3F,0x62,0x61,0x73,0x65,0x3F,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x36,0x60, +0xE4,0x70,0xF7,0x06,0x20,0x43,0xD9,0x23,0x1A,0x42,0xF2,0xF8,0xA3,0xB2,0xB9,0x4D, +0x8A,0xB4,0xF3,0xC2,0x9A,0x55,0x31,0x7C,0xC4,0x3B,0x67,0x9A,0xB4,0xDF,0x4D,0x0E, +0x8A,0x93,0x4A,0x17,0x8B,0x1B,0x8D,0xCA,0x89,0xE1,0xCF,0x3A,0x1E,0xAC,0x1D,0xF1, +0x9C,0x32,0xB4,0x8E,0x59,0x76,0xA2,0x41,0x85,0x25,0x37,0xA0,0x13,0xD0,0xF5,0x7C, +0x4E,0xD5,0xEA,0x96,0xE2,0x6E,0x72,0xC1,0xBB,0x2A,0xFE,0x6C,0x6E,0xF8,0x91,0x98, +0x46,0xFC,0xC9,0x1B,0x57,0x5B,0xEA,0xC8,0x1A,0x3B,0x3F,0xB0,0x51,0x98,0x3C,0x07, +0xDA,0x2C,0x59,0x01,0xDA,0x8B,0x44,0xE8,0xE1,0x74,0xFD,0xA7,0x68,0xDD,0x54,0xBA, +0x83,0x46,0xEC,0xC8,0x46,0xB5,0xF8,0xAF,0x97,0xC0,0x3B,0x09,0x1C,0x8F,0xCE,0x72, +0x96,0x3D,0x33,0x56,0x70,0xBC,0x96,0xCB,0xD8,0xD5,0x7D,0x20,0x9A,0x83,0x9F,0x1A, +0xDC,0x39,0xF1,0xC5,0x72,0xA3,0x11,0x03,0xFD,0x3B,0x42,0x52,0x29,0xDB,0xE8,0x01, +0xF7,0x9B,0x5E,0x8C,0xD6,0x8D,0x86,0x4E,0x19,0xFA,0xBC,0x1C,0xBE,0xC5,0x21,0xA5, +0x87,0x9E,0x78,0x2E,0x36,0xDB,0x09,0x71,0xA3,0x72,0x34,0xF8,0x6C,0xE3,0x06,0x09, +0xF2,0x5E,0x56,0xA5,0xD3,0xDD,0x98,0xFA,0xD4,0xE6,0x06,0xF4,0xF0,0xB6,0x20,0x63, +0x4B,0xEA,0x29,0xBD,0xAA,0x82,0x66,0x1E,0xFB,0x81,0xAA,0xA7,0x37,0xAD,0x13,0x18, +0xE6,0x92,0xC3,0x81,0xC1,0x33,0xBB,0x88,0x1E,0xA1,0xE7,0xE2,0xB4,0xBD,0x31,0x6C, +0x0E,0x51,0x3D,0x6F,0xFB,0x96,0x56,0x80,0xE2,0x36,0x17,0xD1,0xDC,0xE4, +}; + + +/* subject:/C=DE/O=TC TrustCenter GmbH/OU=TC TrustCenter Universal CA/CN=TC TrustCenter Universal CA I */ +/* issuer :/C=DE/O=TC TrustCenter GmbH/OU=TC TrustCenter Universal CA/CN=TC TrustCenter Universal CA I */ + + +const unsigned char TC_TrustCenter_Universal_CA_I_certificate[993]={ +0x30,0x82,0x03,0xDD,0x30,0x82,0x02,0xC5,0xA0,0x03,0x02,0x01,0x02,0x02,0x0E,0x1D, +0xA2,0x00,0x01,0x00,0x02,0xEC,0xB7,0x60,0x80,0x78,0x8D,0xB6,0x06,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x79,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x44,0x45,0x31,0x1C,0x30,0x1A,0x06, +0x03,0x55,0x04,0x0A,0x13,0x13,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65, +0x6E,0x74,0x65,0x72,0x20,0x47,0x6D,0x62,0x48,0x31,0x24,0x30,0x22,0x06,0x03,0x55, +0x04,0x0B,0x13,0x1B,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74, +0x65,0x72,0x20,0x55,0x6E,0x69,0x76,0x65,0x72,0x73,0x61,0x6C,0x20,0x43,0x41,0x31, +0x26,0x30,0x24,0x06,0x03,0x55,0x04,0x03,0x13,0x1D,0x54,0x43,0x20,0x54,0x72,0x75, +0x73,0x74,0x43,0x65,0x6E,0x74,0x65,0x72,0x20,0x55,0x6E,0x69,0x76,0x65,0x72,0x73, +0x61,0x6C,0x20,0x43,0x41,0x20,0x49,0x30,0x1E,0x17,0x0D,0x30,0x36,0x30,0x33,0x32, +0x32,0x31,0x35,0x35,0x34,0x32,0x38,0x5A,0x17,0x0D,0x32,0x35,0x31,0x32,0x33,0x31, +0x32,0x32,0x35,0x39,0x35,0x39,0x5A,0x30,0x79,0x31,0x0B,0x30,0x09,0x06,0x03,0x55, +0x04,0x06,0x13,0x02,0x44,0x45,0x31,0x1C,0x30,0x1A,0x06,0x03,0x55,0x04,0x0A,0x13, +0x13,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74,0x65,0x72,0x20, +0x47,0x6D,0x62,0x48,0x31,0x24,0x30,0x22,0x06,0x03,0x55,0x04,0x0B,0x13,0x1B,0x54, +0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74,0x65,0x72,0x20,0x55,0x6E, +0x69,0x76,0x65,0x72,0x73,0x61,0x6C,0x20,0x43,0x41,0x31,0x26,0x30,0x24,0x06,0x03, +0x55,0x04,0x03,0x13,0x1D,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E, +0x74,0x65,0x72,0x20,0x55,0x6E,0x69,0x76,0x65,0x72,0x73,0x61,0x6C,0x20,0x43,0x41, +0x20,0x49,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D, +0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82, +0x01,0x01,0x00,0xA4,0x77,0x23,0x96,0x44,0xAF,0x90,0xF4,0x31,0xA7,0x10,0xF4,0x26, +0x87,0x9C,0xF3,0x38,0xD9,0x0F,0x5E,0xDE,0xCF,0x41,0xE8,0x31,0xAD,0xC6,0x74,0x91, +0x24,0x96,0x78,0x1E,0x09,0xA0,0x9B,0x9A,0x95,0x4A,0x4A,0xF5,0x62,0x7C,0x02,0xA8, +0xCA,0xAC,0xFB,0x5A,0x04,0x76,0x39,0xDE,0x5F,0xF1,0xF9,0xB3,0xBF,0xF3,0x03,0x58, +0x55,0xD2,0xAA,0xB7,0xE3,0x04,0x22,0xD1,0xF8,0x94,0xDA,0x22,0x08,0x00,0x8D,0xD3, +0x7C,0x26,0x5D,0xCC,0x77,0x79,0xE7,0x2C,0x78,0x39,0xA8,0x26,0x73,0x0E,0xA2,0x5D, +0x25,0x69,0x85,0x4F,0x55,0x0E,0x9A,0xEF,0xC6,0xB9,0x44,0xE1,0x57,0x3D,0xDF,0x1F, +0x54,0x22,0xE5,0x6F,0x65,0xAA,0x33,0x84,0x3A,0xF3,0xCE,0x7A,0xBE,0x55,0x97,0xAE, +0x8D,0x12,0x0F,0x14,0x33,0xE2,0x50,0x70,0xC3,0x49,0x87,0x13,0xBC,0x51,0xDE,0xD7, +0x98,0x12,0x5A,0xEF,0x3A,0x83,0x33,0x92,0x06,0x75,0x8B,0x92,0x7C,0x12,0x68,0x7B, +0x70,0x6A,0x0F,0xB5,0x9B,0xB6,0x77,0x5B,0x48,0x59,0x9D,0xE4,0xEF,0x5A,0xAD,0xF3, +0xC1,0x9E,0xD4,0xD7,0x45,0x4E,0xCA,0x56,0x34,0x21,0xBC,0x3E,0x17,0x5B,0x6F,0x77, +0x0C,0x48,0x01,0x43,0x29,0xB0,0xDD,0x3F,0x96,0x6E,0xE6,0x95,0xAA,0x0C,0xC0,0x20, +0xB6,0xFD,0x3E,0x36,0x27,0x9C,0xE3,0x5C,0xCF,0x4E,0x81,0xDC,0x19,0xBB,0x91,0x90, +0x7D,0xEC,0xE6,0x97,0x04,0x1E,0x93,0xCC,0x22,0x49,0xD7,0x97,0x86,0xB6,0x13,0x0A, +0x3C,0x43,0x23,0x77,0x7E,0xF0,0xDC,0xE6,0xCD,0x24,0x1F,0x3B,0x83,0x9B,0x34,0x3A, +0x83,0x34,0xE3,0x02,0x03,0x01,0x00,0x01,0xA3,0x63,0x30,0x61,0x30,0x1F,0x06,0x03, +0x55,0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14,0x92,0xA4,0x75,0x2C,0xA4,0x9E,0xBE, +0x81,0x44,0xEB,0x79,0xFC,0x8A,0xC5,0x95,0xA5,0xEB,0x10,0x75,0x73,0x30,0x0F,0x06, +0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E, +0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x86,0x30,0x1D, +0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x92,0xA4,0x75,0x2C,0xA4,0x9E,0xBE, +0x81,0x44,0xEB,0x79,0xFC,0x8A,0xC5,0x95,0xA5,0xEB,0x10,0x75,0x73,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01, +0x00,0x28,0xD2,0xE0,0x86,0xD5,0xE6,0xF8,0x7B,0xF0,0x97,0xDC,0x22,0x6B,0x3B,0x95, +0x14,0x56,0x0F,0x11,0x30,0xA5,0x9A,0x4F,0x3A,0xB0,0x3A,0xE0,0x06,0xCB,0x65,0xF5, +0xED,0xC6,0x97,0x27,0xFE,0x25,0xF2,0x57,0xE6,0x5E,0x95,0x8C,0x3E,0x64,0x60,0x15, +0x5A,0x7F,0x2F,0x0D,0x01,0xC5,0xB1,0x60,0xFD,0x45,0x35,0xCF,0xF0,0xB2,0xBF,0x06, +0xD9,0xEF,0x5A,0xBE,0xB3,0x62,0x21,0xB4,0xD7,0xAB,0x35,0x7C,0x53,0x3E,0xA6,0x27, +0xF1,0xA1,0x2D,0xDA,0x1A,0x23,0x9D,0xCC,0xDD,0xEC,0x3C,0x2D,0x9E,0x27,0x34,0x5D, +0x0F,0xC2,0x36,0x79,0xBC,0xC9,0x4A,0x62,0x2D,0xED,0x6B,0xD9,0x7D,0x41,0x43,0x7C, +0xB6,0xAA,0xCA,0xED,0x61,0xB1,0x37,0x82,0x15,0x09,0x1A,0x8A,0x16,0x30,0xD8,0xEC, +0xC9,0xD6,0x47,0x72,0x78,0x4B,0x10,0x46,0x14,0x8E,0x5F,0x0E,0xAF,0xEC,0xC7,0x2F, +0xAB,0x10,0xD7,0xB6,0xF1,0x6E,0xEC,0x86,0xB2,0xC2,0xE8,0x0D,0x92,0x73,0xDC,0xA2, +0xF4,0x0F,0x3A,0xBF,0x61,0x23,0x10,0x89,0x9C,0x48,0x40,0x6E,0x70,0x00,0xB3,0xD3, +0xBA,0x37,0x44,0x58,0x11,0x7A,0x02,0x6A,0x88,0xF0,0x37,0x34,0xF0,0x19,0xE9,0xAC, +0xD4,0x65,0x73,0xF6,0x69,0x8C,0x64,0x94,0x3A,0x79,0x85,0x29,0xB0,0x16,0x2B,0x0C, +0x82,0x3F,0x06,0x9C,0xC7,0xFD,0x10,0x2B,0x9E,0x0F,0x2C,0xB6,0x9E,0xE3,0x15,0xBF, +0xD9,0x36,0x1C,0xBA,0x25,0x1A,0x52,0x3D,0x1A,0xEC,0x22,0x0C,0x1C,0xE0,0xA4,0xA2, +0x3D,0xF0,0xE8,0x39,0xCF,0x81,0xC0,0x7B,0xED,0x5D,0x1F,0x6F,0xC5,0xD0,0x0B,0xD7, +0x98, +}; + + +/* subject:/C=DE/O=TC TrustCenter GmbH/OU=TC TrustCenter Universal CA/CN=TC TrustCenter Universal CA III */ +/* issuer :/C=DE/O=TC TrustCenter GmbH/OU=TC TrustCenter Universal CA/CN=TC TrustCenter Universal CA III */ + + +const unsigned char TC_TrustCenter_Universal_CA_III_certificate[997]={ +0x30,0x82,0x03,0xE1,0x30,0x82,0x02,0xC9,0xA0,0x03,0x02,0x01,0x02,0x02,0x0E,0x63, +0x25,0x00,0x01,0x00,0x02,0x14,0x8D,0x33,0x15,0x02,0xE4,0x6C,0xF4,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x7B,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x44,0x45,0x31,0x1C,0x30,0x1A,0x06, +0x03,0x55,0x04,0x0A,0x13,0x13,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65, +0x6E,0x74,0x65,0x72,0x20,0x47,0x6D,0x62,0x48,0x31,0x24,0x30,0x22,0x06,0x03,0x55, +0x04,0x0B,0x13,0x1B,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74, +0x65,0x72,0x20,0x55,0x6E,0x69,0x76,0x65,0x72,0x73,0x61,0x6C,0x20,0x43,0x41,0x31, +0x28,0x30,0x26,0x06,0x03,0x55,0x04,0x03,0x13,0x1F,0x54,0x43,0x20,0x54,0x72,0x75, +0x73,0x74,0x43,0x65,0x6E,0x74,0x65,0x72,0x20,0x55,0x6E,0x69,0x76,0x65,0x72,0x73, +0x61,0x6C,0x20,0x43,0x41,0x20,0x49,0x49,0x49,0x30,0x1E,0x17,0x0D,0x30,0x39,0x30, +0x39,0x30,0x39,0x30,0x38,0x31,0x35,0x32,0x37,0x5A,0x17,0x0D,0x32,0x39,0x31,0x32, +0x33,0x31,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x7B,0x31,0x0B,0x30,0x09,0x06, +0x03,0x55,0x04,0x06,0x13,0x02,0x44,0x45,0x31,0x1C,0x30,0x1A,0x06,0x03,0x55,0x04, +0x0A,0x13,0x13,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74,0x65, +0x72,0x20,0x47,0x6D,0x62,0x48,0x31,0x24,0x30,0x22,0x06,0x03,0x55,0x04,0x0B,0x13, +0x1B,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43,0x65,0x6E,0x74,0x65,0x72,0x20, +0x55,0x6E,0x69,0x76,0x65,0x72,0x73,0x61,0x6C,0x20,0x43,0x41,0x31,0x28,0x30,0x26, +0x06,0x03,0x55,0x04,0x03,0x13,0x1F,0x54,0x43,0x20,0x54,0x72,0x75,0x73,0x74,0x43, +0x65,0x6E,0x74,0x65,0x72,0x20,0x55,0x6E,0x69,0x76,0x65,0x72,0x73,0x61,0x6C,0x20, +0x43,0x41,0x20,0x49,0x49,0x49,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82, +0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xC2,0xDA,0x9C,0x62,0xB0,0xB9,0x71,0x12,0xB0, +0x0B,0xC8,0x1A,0x57,0xB2,0xAE,0x83,0x14,0x99,0xB3,0x34,0x4B,0x9B,0x90,0xA2,0xC5, +0xE7,0xE7,0x2F,0x02,0xA0,0x4D,0x2D,0xA4,0xFA,0x85,0xDA,0x9B,0x25,0x85,0x2D,0x40, +0x28,0x20,0x6D,0xEA,0xE0,0xBD,0xB1,0x48,0x83,0x22,0x29,0x44,0x9F,0x4E,0x83,0xEE, +0x35,0x51,0x13,0x73,0x74,0xD5,0xBC,0xF2,0x30,0x66,0x94,0x53,0xC0,0x40,0x36,0x2F, +0x0C,0x84,0x65,0xCE,0x0F,0x6E,0xC2,0x58,0x93,0xE8,0x2C,0x0B,0x3A,0xE9,0xC1,0x8E, +0xFB,0xF2,0x6B,0xCA,0x3C,0xE2,0x9C,0x4E,0x8E,0xE4,0xF9,0x7D,0xD3,0x27,0x9F,0x1B, +0xD5,0x67,0x78,0x87,0x2D,0x7F,0x0B,0x47,0xB3,0xC7,0xE8,0xC9,0x48,0x7C,0xAF,0x2F, +0xCC,0x0A,0xD9,0x41,0xEF,0x9F,0xFE,0x9A,0xE1,0xB2,0xAE,0xF9,0x53,0xB5,0xE5,0xE9, +0x46,0x9F,0x60,0xE3,0xDF,0x8D,0xD3,0x7F,0xFB,0x96,0x7E,0xB3,0xB5,0x72,0xF8,0x4B, +0xAD,0x08,0x79,0xCD,0x69,0x89,0x40,0x27,0xF5,0x2A,0xC1,0xAD,0x43,0xEC,0xA4,0x53, +0xC8,0x61,0xB6,0xF7,0xD2,0x79,0x2A,0x67,0x18,0x76,0x48,0x6D,0x5B,0x25,0x01,0xD1, +0x26,0xC5,0xB7,0x57,0x69,0x23,0x15,0x5B,0x61,0x8A,0xAD,0xF0,0x1B,0x2D,0xD9,0xAF, +0x5C,0xF1,0x26,0x90,0x69,0xA9,0xD5,0x0C,0x40,0xF5,0x33,0x80,0x43,0x8F,0x9C,0xA3, +0x76,0x2A,0x45,0xB4,0xAF,0xBF,0x7F,0x3E,0x87,0x3F,0x76,0xC5,0xCD,0x2A,0xDE,0x20, +0xC5,0x16,0x58,0xCB,0xF9,0x1B,0xF5,0x0F,0xCB,0x0D,0x11,0x52,0x64,0xB8,0xD2,0x76, +0x62,0x77,0x83,0xF1,0x58,0x9F,0xFF,0x02,0x03,0x01,0x00,0x01,0xA3,0x63,0x30,0x61, +0x30,0x1F,0x06,0x03,0x55,0x1D,0x23,0x04,0x18,0x30,0x16,0x80,0x14,0x56,0xE7,0xE1, +0x5B,0x25,0x43,0x80,0xE0,0xF6,0x8C,0xE1,0x71,0xBC,0x8E,0xE5,0x80,0x2F,0xC4,0x48, +0xE2,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01, +0x01,0xFF,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02, +0x01,0x06,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x56,0xE7,0xE1, +0x5B,0x25,0x43,0x80,0xE0,0xF6,0x8C,0xE1,0x71,0xBC,0x8E,0xE5,0x80,0x2F,0xC4,0x48, +0xE2,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00, +0x03,0x82,0x01,0x01,0x00,0x83,0xC7,0xAF,0xEA,0x7F,0x4D,0x0A,0x3C,0x39,0xB1,0x68, +0xBE,0x7B,0x6D,0x89,0x2E,0xE9,0xB3,0x09,0xE7,0x18,0x57,0x8D,0x85,0x9A,0x17,0xF3, +0x76,0x42,0x50,0x13,0x0F,0xC7,0x90,0x6F,0x33,0xAD,0xC5,0x49,0x60,0x2B,0x6C,0x49, +0x58,0x19,0xD4,0xE2,0xBE,0xB7,0xBF,0xAB,0x49,0xBC,0x94,0xC8,0xAB,0xBE,0x28,0x6C, +0x16,0x68,0xE0,0xC8,0x97,0x46,0x20,0xA0,0x68,0x67,0x60,0x88,0x39,0x20,0x51,0xD8, +0x68,0x01,0x11,0xCE,0xA7,0xF6,0x11,0x07,0xF6,0xEC,0xEC,0xAC,0x1A,0x1F,0xB2,0x66, +0x6E,0x56,0x67,0x60,0x7A,0x74,0x5E,0xC0,0x6D,0x97,0x36,0xAE,0xB5,0x0D,0x5D,0x66, +0x73,0xC0,0x25,0x32,0x45,0xD8,0x4A,0x06,0x07,0x8F,0xC4,0xB7,0x07,0xB1,0x4D,0x06, +0x0D,0xE1,0xA5,0xEB,0xF4,0x75,0xCA,0xBA,0x9C,0xD0,0xBD,0xB3,0xD3,0x32,0x24,0x4C, +0xEE,0x7E,0xE2,0x76,0x04,0x4B,0x49,0x53,0xD8,0xF2,0xE9,0x54,0x33,0xFC,0xE5,0x71, +0x1F,0x3D,0x14,0x5C,0x96,0x4B,0xF1,0x3A,0xF2,0x00,0xBB,0x6C,0xB4,0xFA,0x96,0x55, +0x08,0x88,0x09,0xC1,0xCC,0x91,0x19,0x29,0xB0,0x20,0x2D,0xFF,0xCB,0x38,0xA4,0x40, +0xE1,0x17,0xBE,0x79,0x61,0x80,0xFF,0x07,0x03,0x86,0x4C,0x4E,0x7B,0x06,0x9F,0x11, +0x86,0x8D,0x89,0xEE,0x27,0xC4,0xDB,0xE2,0xBC,0x19,0x8E,0x0B,0xC3,0xC3,0x13,0xC7, +0x2D,0x03,0x63,0x3B,0xD3,0xE8,0xE4,0xA2,0x2A,0xC2,0x82,0x08,0x94,0x16,0x54,0xF0, +0xEF,0x1F,0x27,0x90,0x25,0xB8,0x0D,0x0E,0x28,0x1B,0x47,0x77,0x47,0xBD,0x1C,0xA8, +0x25,0xF1,0x94,0xB4,0x66, +}; + + +/* subject:/C=ZA/ST=Western Cape/L=Cape Town/O=Thawte Consulting cc/OU=Certification Services Division/CN=Thawte Premium Server CA/emailAddress=premium-server@thawte.com */ +/* issuer :/C=ZA/ST=Western Cape/L=Cape Town/O=Thawte Consulting cc/OU=Certification Services Division/CN=Thawte Premium Server CA/emailAddress=premium-server@thawte.com */ + + +const unsigned char Thawte_Premium_Server_CA_certificate[811]={ +0x30,0x82,0x03,0x27,0x30,0x82,0x02,0x90,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x04,0x05,0x00,0x30, +0x81,0xCE,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x5A,0x41,0x31, +0x15,0x30,0x13,0x06,0x03,0x55,0x04,0x08,0x13,0x0C,0x57,0x65,0x73,0x74,0x65,0x72, +0x6E,0x20,0x43,0x61,0x70,0x65,0x31,0x12,0x30,0x10,0x06,0x03,0x55,0x04,0x07,0x13, +0x09,0x43,0x61,0x70,0x65,0x20,0x54,0x6F,0x77,0x6E,0x31,0x1D,0x30,0x1B,0x06,0x03, +0x55,0x04,0x0A,0x13,0x14,0x54,0x68,0x61,0x77,0x74,0x65,0x20,0x43,0x6F,0x6E,0x73, +0x75,0x6C,0x74,0x69,0x6E,0x67,0x20,0x63,0x63,0x31,0x28,0x30,0x26,0x06,0x03,0x55, +0x04,0x0B,0x13,0x1F,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F, +0x6E,0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x20,0x44,0x69,0x76,0x69,0x73, +0x69,0x6F,0x6E,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x03,0x13,0x18,0x54,0x68, +0x61,0x77,0x74,0x65,0x20,0x50,0x72,0x65,0x6D,0x69,0x75,0x6D,0x20,0x53,0x65,0x72, +0x76,0x65,0x72,0x20,0x43,0x41,0x31,0x28,0x30,0x26,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x09,0x01,0x16,0x19,0x70,0x72,0x65,0x6D,0x69,0x75,0x6D,0x2D,0x73, +0x65,0x72,0x76,0x65,0x72,0x40,0x74,0x68,0x61,0x77,0x74,0x65,0x2E,0x63,0x6F,0x6D, +0x30,0x1E,0x17,0x0D,0x39,0x36,0x30,0x38,0x30,0x31,0x30,0x30,0x30,0x30,0x30,0x30, +0x5A,0x17,0x0D,0x32,0x30,0x31,0x32,0x33,0x31,0x32,0x33,0x35,0x39,0x35,0x39,0x5A, +0x30,0x81,0xCE,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x5A,0x41, +0x31,0x15,0x30,0x13,0x06,0x03,0x55,0x04,0x08,0x13,0x0C,0x57,0x65,0x73,0x74,0x65, +0x72,0x6E,0x20,0x43,0x61,0x70,0x65,0x31,0x12,0x30,0x10,0x06,0x03,0x55,0x04,0x07, +0x13,0x09,0x43,0x61,0x70,0x65,0x20,0x54,0x6F,0x77,0x6E,0x31,0x1D,0x30,0x1B,0x06, +0x03,0x55,0x04,0x0A,0x13,0x14,0x54,0x68,0x61,0x77,0x74,0x65,0x20,0x43,0x6F,0x6E, +0x73,0x75,0x6C,0x74,0x69,0x6E,0x67,0x20,0x63,0x63,0x31,0x28,0x30,0x26,0x06,0x03, +0x55,0x04,0x0B,0x13,0x1F,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69, +0x6F,0x6E,0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x20,0x44,0x69,0x76,0x69, +0x73,0x69,0x6F,0x6E,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x03,0x13,0x18,0x54, +0x68,0x61,0x77,0x74,0x65,0x20,0x50,0x72,0x65,0x6D,0x69,0x75,0x6D,0x20,0x53,0x65, +0x72,0x76,0x65,0x72,0x20,0x43,0x41,0x31,0x28,0x30,0x26,0x06,0x09,0x2A,0x86,0x48, +0x86,0xF7,0x0D,0x01,0x09,0x01,0x16,0x19,0x70,0x72,0x65,0x6D,0x69,0x75,0x6D,0x2D, +0x73,0x65,0x72,0x76,0x65,0x72,0x40,0x74,0x68,0x61,0x77,0x74,0x65,0x2E,0x63,0x6F, +0x6D,0x30,0x81,0x9F,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01, +0x01,0x05,0x00,0x03,0x81,0x8D,0x00,0x30,0x81,0x89,0x02,0x81,0x81,0x00,0xD2,0x36, +0x36,0x6A,0x8B,0xD7,0xC2,0x5B,0x9E,0xDA,0x81,0x41,0x62,0x8F,0x38,0xEE,0x49,0x04, +0x55,0xD6,0xD0,0xEF,0x1C,0x1B,0x95,0x16,0x47,0xEF,0x18,0x48,0x35,0x3A,0x52,0xF4, +0x2B,0x6A,0x06,0x8F,0x3B,0x2F,0xEA,0x56,0xE3,0xAF,0x86,0x8D,0x9E,0x17,0xF7,0x9E, +0xB4,0x65,0x75,0x02,0x4D,0xEF,0xCB,0x09,0xA2,0x21,0x51,0xD8,0x9B,0xD0,0x67,0xD0, +0xBA,0x0D,0x92,0x06,0x14,0x73,0xD4,0x93,0xCB,0x97,0x2A,0x00,0x9C,0x5C,0x4E,0x0C, +0xBC,0xFA,0x15,0x52,0xFC,0xF2,0x44,0x6E,0xDA,0x11,0x4A,0x6E,0x08,0x9F,0x2F,0x2D, +0xE3,0xF9,0xAA,0x3A,0x86,0x73,0xB6,0x46,0x53,0x58,0xC8,0x89,0x05,0xBD,0x83,0x11, +0xB8,0x73,0x3F,0xAA,0x07,0x8D,0xF4,0x42,0x4D,0xE7,0x40,0x9D,0x1C,0x37,0x02,0x03, +0x01,0x00,0x01,0xA3,0x13,0x30,0x11,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01, +0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x01,0x04,0x05,0x00,0x03,0x81,0x81,0x00,0x26,0x48,0x2C,0x16,0xC2, +0x58,0xFA,0xE8,0x16,0x74,0x0C,0xAA,0xAA,0x5F,0x54,0x3F,0xF2,0xD7,0xC9,0x78,0x60, +0x5E,0x5E,0x6E,0x37,0x63,0x22,0x77,0x36,0x7E,0xB2,0x17,0xC4,0x34,0xB9,0xF5,0x08, +0x85,0xFC,0xC9,0x01,0x38,0xFF,0x4D,0xBE,0xF2,0x16,0x42,0x43,0xE7,0xBB,0x5A,0x46, +0xFB,0xC1,0xC6,0x11,0x1F,0xF1,0x4A,0xB0,0x28,0x46,0xC9,0xC3,0xC4,0x42,0x7D,0xBC, +0xFA,0xAB,0x59,0x6E,0xD5,0xB7,0x51,0x88,0x11,0xE3,0xA4,0x85,0x19,0x6B,0x82,0x4C, +0xA4,0x0C,0x12,0xAD,0xE9,0xA4,0xAE,0x3F,0xF1,0xC3,0x49,0x65,0x9A,0x8C,0xC5,0xC8, +0x3E,0x25,0xB7,0x94,0x99,0xBB,0x92,0x32,0x71,0x07,0xF0,0x86,0x5E,0xED,0x50,0x27, +0xA6,0x0D,0xA6,0x23,0xF9,0xBB,0xCB,0xA6,0x07,0x14,0x42, +}; + + +/* subject:/C=US/O=thawte, Inc./OU=Certification Services Division/OU=(c) 2006 thawte, Inc. - For authorized use only/CN=thawte Primary Root CA */ +/* issuer :/C=US/O=thawte, Inc./OU=Certification Services Division/OU=(c) 2006 thawte, Inc. - For authorized use only/CN=thawte Primary Root CA */ + + +const unsigned char thawte_Primary_Root_CA_certificate[1060]={ +0x30,0x82,0x04,0x20,0x30,0x82,0x03,0x08,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x34, +0x4E,0xD5,0x57,0x20,0xD5,0xED,0xEC,0x49,0xF4,0x2F,0xCE,0x37,0xDB,0x2B,0x6D,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x81, +0xA9,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x15, +0x30,0x13,0x06,0x03,0x55,0x04,0x0A,0x13,0x0C,0x74,0x68,0x61,0x77,0x74,0x65,0x2C, +0x20,0x49,0x6E,0x63,0x2E,0x31,0x28,0x30,0x26,0x06,0x03,0x55,0x04,0x0B,0x13,0x1F, +0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x53,0x65, +0x72,0x76,0x69,0x63,0x65,0x73,0x20,0x44,0x69,0x76,0x69,0x73,0x69,0x6F,0x6E,0x31, +0x38,0x30,0x36,0x06,0x03,0x55,0x04,0x0B,0x13,0x2F,0x28,0x63,0x29,0x20,0x32,0x30, +0x30,0x36,0x20,0x74,0x68,0x61,0x77,0x74,0x65,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x20, +0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74,0x68,0x6F,0x72,0x69,0x7A,0x65,0x64, +0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55, +0x04,0x03,0x13,0x16,0x74,0x68,0x61,0x77,0x74,0x65,0x20,0x50,0x72,0x69,0x6D,0x61, +0x72,0x79,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x41,0x30,0x1E,0x17,0x0D,0x30,0x36, +0x31,0x31,0x31,0x37,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x36,0x30, +0x37,0x31,0x36,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x81,0xA9,0x31,0x0B,0x30, +0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x15,0x30,0x13,0x06,0x03, +0x55,0x04,0x0A,0x13,0x0C,0x74,0x68,0x61,0x77,0x74,0x65,0x2C,0x20,0x49,0x6E,0x63, +0x2E,0x31,0x28,0x30,0x26,0x06,0x03,0x55,0x04,0x0B,0x13,0x1F,0x43,0x65,0x72,0x74, +0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x53,0x65,0x72,0x76,0x69,0x63, +0x65,0x73,0x20,0x44,0x69,0x76,0x69,0x73,0x69,0x6F,0x6E,0x31,0x38,0x30,0x36,0x06, +0x03,0x55,0x04,0x0B,0x13,0x2F,0x28,0x63,0x29,0x20,0x32,0x30,0x30,0x36,0x20,0x74, +0x68,0x61,0x77,0x74,0x65,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F, +0x72,0x20,0x61,0x75,0x74,0x68,0x6F,0x72,0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65, +0x20,0x6F,0x6E,0x6C,0x79,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x03,0x13,0x16, +0x74,0x68,0x61,0x77,0x74,0x65,0x20,0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x52, +0x6F,0x6F,0x74,0x20,0x43,0x41,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82, +0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xAC,0xA0,0xF0,0xFB,0x80,0x59,0xD4,0x9C,0xC7, +0xA4,0xCF,0x9D,0xA1,0x59,0x73,0x09,0x10,0x45,0x0C,0x0D,0x2C,0x6E,0x68,0xF1,0x6C, +0x5B,0x48,0x68,0x49,0x59,0x37,0xFC,0x0B,0x33,0x19,0xC2,0x77,0x7F,0xCC,0x10,0x2D, +0x95,0x34,0x1C,0xE6,0xEB,0x4D,0x09,0xA7,0x1C,0xD2,0xB8,0xC9,0x97,0x36,0x02,0xB7, +0x89,0xD4,0x24,0x5F,0x06,0xC0,0xCC,0x44,0x94,0x94,0x8D,0x02,0x62,0x6F,0xEB,0x5A, +0xDD,0x11,0x8D,0x28,0x9A,0x5C,0x84,0x90,0x10,0x7A,0x0D,0xBD,0x74,0x66,0x2F,0x6A, +0x38,0xA0,0xE2,0xD5,0x54,0x44,0xEB,0x1D,0x07,0x9F,0x07,0xBA,0x6F,0xEE,0xE9,0xFD, +0x4E,0x0B,0x29,0xF5,0x3E,0x84,0xA0,0x01,0xF1,0x9C,0xAB,0xF8,0x1C,0x7E,0x89,0xA4, +0xE8,0xA1,0xD8,0x71,0x65,0x0D,0xA3,0x51,0x7B,0xEE,0xBC,0xD2,0x22,0x60,0x0D,0xB9, +0x5B,0x9D,0xDF,0xBA,0xFC,0x51,0x5B,0x0B,0xAF,0x98,0xB2,0xE9,0x2E,0xE9,0x04,0xE8, +0x62,0x87,0xDE,0x2B,0xC8,0xD7,0x4E,0xC1,0x4C,0x64,0x1E,0xDD,0xCF,0x87,0x58,0xBA, +0x4A,0x4F,0xCA,0x68,0x07,0x1D,0x1C,0x9D,0x4A,0xC6,0xD5,0x2F,0x91,0xCC,0x7C,0x71, +0x72,0x1C,0xC5,0xC0,0x67,0xEB,0x32,0xFD,0xC9,0x92,0x5C,0x94,0xDA,0x85,0xC0,0x9B, +0xBF,0x53,0x7D,0x2B,0x09,0xF4,0x8C,0x9D,0x91,0x1F,0x97,0x6A,0x52,0xCB,0xDE,0x09, +0x36,0xA4,0x77,0xD8,0x7B,0x87,0x50,0x44,0xD5,0x3E,0x6E,0x29,0x69,0xFB,0x39,0x49, +0x26,0x1E,0x09,0xA5,0x80,0x7B,0x40,0x2D,0xEB,0xE8,0x27,0x85,0xC9,0xFE,0x61,0xFD, +0x7E,0xE6,0x7C,0x97,0x1D,0xD5,0x9D,0x02,0x03,0x01,0x00,0x01,0xA3,0x42,0x30,0x40, +0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01, +0xFF,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01, +0x06,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x7B,0x5B,0x45,0xCF, +0xAF,0xCE,0xCB,0x7A,0xFD,0x31,0x92,0x1A,0x6A,0xB6,0xF3,0x46,0xEB,0x57,0x48,0x50, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03, +0x82,0x01,0x01,0x00,0x79,0x11,0xC0,0x4B,0xB3,0x91,0xB6,0xFC,0xF0,0xE9,0x67,0xD4, +0x0D,0x6E,0x45,0xBE,0x55,0xE8,0x93,0xD2,0xCE,0x03,0x3F,0xED,0xDA,0x25,0xB0,0x1D, +0x57,0xCB,0x1E,0x3A,0x76,0xA0,0x4C,0xEC,0x50,0x76,0xE8,0x64,0x72,0x0C,0xA4,0xA9, +0xF1,0xB8,0x8B,0xD6,0xD6,0x87,0x84,0xBB,0x32,0xE5,0x41,0x11,0xC0,0x77,0xD9,0xB3, +0x60,0x9D,0xEB,0x1B,0xD5,0xD1,0x6E,0x44,0x44,0xA9,0xA6,0x01,0xEC,0x55,0x62,0x1D, +0x77,0xB8,0x5C,0x8E,0x48,0x49,0x7C,0x9C,0x3B,0x57,0x11,0xAC,0xAD,0x73,0x37,0x8E, +0x2F,0x78,0x5C,0x90,0x68,0x47,0xD9,0x60,0x60,0xE6,0xFC,0x07,0x3D,0x22,0x20,0x17, +0xC4,0xF7,0x16,0xE9,0xC4,0xD8,0x72,0xF9,0xC8,0x73,0x7C,0xDF,0x16,0x2F,0x15,0xA9, +0x3E,0xFD,0x6A,0x27,0xB6,0xA1,0xEB,0x5A,0xBA,0x98,0x1F,0xD5,0xE3,0x4D,0x64,0x0A, +0x9D,0x13,0xC8,0x61,0xBA,0xF5,0x39,0x1C,0x87,0xBA,0xB8,0xBD,0x7B,0x22,0x7F,0xF6, +0xFE,0xAC,0x40,0x79,0xE5,0xAC,0x10,0x6F,0x3D,0x8F,0x1B,0x79,0x76,0x8B,0xC4,0x37, +0xB3,0x21,0x18,0x84,0xE5,0x36,0x00,0xEB,0x63,0x20,0x99,0xB9,0xE9,0xFE,0x33,0x04, +0xBB,0x41,0xC8,0xC1,0x02,0xF9,0x44,0x63,0x20,0x9E,0x81,0xCE,0x42,0xD3,0xD6,0x3F, +0x2C,0x76,0xD3,0x63,0x9C,0x59,0xDD,0x8F,0xA6,0xE1,0x0E,0xA0,0x2E,0x41,0xF7,0x2E, +0x95,0x47,0xCF,0xBC,0xFD,0x33,0xF3,0xF6,0x0B,0x61,0x7E,0x7E,0x91,0x2B,0x81,0x47, +0xC2,0x27,0x30,0xEE,0xA7,0x10,0x5D,0x37,0x8F,0x5C,0x39,0x2B,0xE4,0x04,0xF0,0x7B, +0x8D,0x56,0x8C,0x68, +}; + + +/* subject:/C=US/O=thawte, Inc./OU=(c) 2007 thawte, Inc. - For authorized use only/CN=thawte Primary Root CA - G2 */ +/* issuer :/C=US/O=thawte, Inc./OU=(c) 2007 thawte, Inc. - For authorized use only/CN=thawte Primary Root CA - G2 */ + + +const unsigned char thawte_Primary_Root_CA___G2_certificate[652]={ +0x30,0x82,0x02,0x88,0x30,0x82,0x02,0x0D,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x35, +0xFC,0x26,0x5C,0xD9,0x84,0x4F,0xC9,0x3D,0x26,0x3D,0x57,0x9B,0xAE,0xD7,0x56,0x30, +0x0A,0x06,0x08,0x2A,0x86,0x48,0xCE,0x3D,0x04,0x03,0x03,0x30,0x81,0x84,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x15,0x30,0x13,0x06, +0x03,0x55,0x04,0x0A,0x13,0x0C,0x74,0x68,0x61,0x77,0x74,0x65,0x2C,0x20,0x49,0x6E, +0x63,0x2E,0x31,0x38,0x30,0x36,0x06,0x03,0x55,0x04,0x0B,0x13,0x2F,0x28,0x63,0x29, +0x20,0x32,0x30,0x30,0x37,0x20,0x74,0x68,0x61,0x77,0x74,0x65,0x2C,0x20,0x49,0x6E, +0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74,0x68,0x6F,0x72,0x69, +0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79,0x31,0x24,0x30,0x22, +0x06,0x03,0x55,0x04,0x03,0x13,0x1B,0x74,0x68,0x61,0x77,0x74,0x65,0x20,0x50,0x72, +0x69,0x6D,0x61,0x72,0x79,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x41,0x20,0x2D,0x20, +0x47,0x32,0x30,0x1E,0x17,0x0D,0x30,0x37,0x31,0x31,0x30,0x35,0x30,0x30,0x30,0x30, +0x30,0x30,0x5A,0x17,0x0D,0x33,0x38,0x30,0x31,0x31,0x38,0x32,0x33,0x35,0x39,0x35, +0x39,0x5A,0x30,0x81,0x84,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02, +0x55,0x53,0x31,0x15,0x30,0x13,0x06,0x03,0x55,0x04,0x0A,0x13,0x0C,0x74,0x68,0x61, +0x77,0x74,0x65,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x38,0x30,0x36,0x06,0x03,0x55, +0x04,0x0B,0x13,0x2F,0x28,0x63,0x29,0x20,0x32,0x30,0x30,0x37,0x20,0x74,0x68,0x61, +0x77,0x74,0x65,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20, +0x61,0x75,0x74,0x68,0x6F,0x72,0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F, +0x6E,0x6C,0x79,0x31,0x24,0x30,0x22,0x06,0x03,0x55,0x04,0x03,0x13,0x1B,0x74,0x68, +0x61,0x77,0x74,0x65,0x20,0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x52,0x6F,0x6F, +0x74,0x20,0x43,0x41,0x20,0x2D,0x20,0x47,0x32,0x30,0x76,0x30,0x10,0x06,0x07,0x2A, +0x86,0x48,0xCE,0x3D,0x02,0x01,0x06,0x05,0x2B,0x81,0x04,0x00,0x22,0x03,0x62,0x00, +0x04,0xA2,0xD5,0x9C,0x82,0x7B,0x95,0x9D,0xF1,0x52,0x78,0x87,0xFE,0x8A,0x16,0xBF, +0x05,0xE6,0xDF,0xA3,0x02,0x4F,0x0D,0x07,0xC6,0x00,0x51,0xBA,0x0C,0x02,0x52,0x2D, +0x22,0xA4,0x42,0x39,0xC4,0xFE,0x8F,0xEA,0xC9,0xC1,0xBE,0xD4,0x4D,0xFF,0x9F,0x7A, +0x9E,0xE2,0xB1,0x7C,0x9A,0xAD,0xA7,0x86,0x09,0x73,0x87,0xD1,0xE7,0x9A,0xE3,0x7A, +0xA5,0xAA,0x6E,0xFB,0xBA,0xB3,0x70,0xC0,0x67,0x88,0xA2,0x35,0xD4,0xA3,0x9A,0xB1, +0xFD,0xAD,0xC2,0xEF,0x31,0xFA,0xA8,0xB9,0xF3,0xFB,0x08,0xC6,0x91,0xD1,0xFB,0x29, +0x95,0xA3,0x42,0x30,0x40,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04, +0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF, +0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04, +0x14,0x9A,0xD8,0x00,0x30,0x00,0xE7,0x6B,0x7F,0x85,0x18,0xEE,0x8B,0xB6,0xCE,0x8A, +0x0C,0xF8,0x11,0xE1,0xBB,0x30,0x0A,0x06,0x08,0x2A,0x86,0x48,0xCE,0x3D,0x04,0x03, +0x03,0x03,0x69,0x00,0x30,0x66,0x02,0x31,0x00,0xDD,0xF8,0xE0,0x57,0x47,0x5B,0xA7, +0xE6,0x0A,0xC3,0xBD,0xF5,0x80,0x8A,0x97,0x35,0x0D,0x1B,0x89,0x3C,0x54,0x86,0x77, +0x28,0xCA,0xA1,0xF4,0x79,0xDE,0xB5,0xE6,0x38,0xB0,0xF0,0x65,0x70,0x8C,0x7F,0x02, +0x54,0xC2,0xBF,0xFF,0xD8,0xA1,0x3E,0xD9,0xCF,0x02,0x31,0x00,0xC4,0x8D,0x94,0xFC, +0xDC,0x53,0xD2,0xDC,0x9D,0x78,0x16,0x1F,0x15,0x33,0x23,0x53,0x52,0xE3,0x5A,0x31, +0x5D,0x9D,0xCA,0xAE,0xBD,0x13,0x29,0x44,0x0D,0x27,0x5B,0xA8,0xE7,0x68,0x9C,0x12, +0xF7,0x58,0x3F,0x2E,0x72,0x02,0x57,0xA3,0x8F,0xA1,0x14,0x2E, +}; + + +/* subject:/C=US/O=thawte, Inc./OU=Certification Services Division/OU=(c) 2008 thawte, Inc. - For authorized use only/CN=thawte Primary Root CA - G3 */ +/* issuer :/C=US/O=thawte, Inc./OU=Certification Services Division/OU=(c) 2008 thawte, Inc. - For authorized use only/CN=thawte Primary Root CA - G3 */ + + +const unsigned char thawte_Primary_Root_CA___G3_certificate[1070]={ +0x30,0x82,0x04,0x2A,0x30,0x82,0x03,0x12,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x60, +0x01,0x97,0xB7,0x46,0xA7,0xEA,0xB4,0xB4,0x9A,0xD6,0x4B,0x2F,0xF7,0x90,0xFB,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x30,0x81, +0xAE,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x15, +0x30,0x13,0x06,0x03,0x55,0x04,0x0A,0x13,0x0C,0x74,0x68,0x61,0x77,0x74,0x65,0x2C, +0x20,0x49,0x6E,0x63,0x2E,0x31,0x28,0x30,0x26,0x06,0x03,0x55,0x04,0x0B,0x13,0x1F, +0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x53,0x65, +0x72,0x76,0x69,0x63,0x65,0x73,0x20,0x44,0x69,0x76,0x69,0x73,0x69,0x6F,0x6E,0x31, +0x38,0x30,0x36,0x06,0x03,0x55,0x04,0x0B,0x13,0x2F,0x28,0x63,0x29,0x20,0x32,0x30, +0x30,0x38,0x20,0x74,0x68,0x61,0x77,0x74,0x65,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x20, +0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74,0x68,0x6F,0x72,0x69,0x7A,0x65,0x64, +0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79,0x31,0x24,0x30,0x22,0x06,0x03,0x55, +0x04,0x03,0x13,0x1B,0x74,0x68,0x61,0x77,0x74,0x65,0x20,0x50,0x72,0x69,0x6D,0x61, +0x72,0x79,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x41,0x20,0x2D,0x20,0x47,0x33,0x30, +0x1E,0x17,0x0D,0x30,0x38,0x30,0x34,0x30,0x32,0x30,0x30,0x30,0x30,0x30,0x30,0x5A, +0x17,0x0D,0x33,0x37,0x31,0x32,0x30,0x31,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30, +0x81,0xAE,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31, +0x15,0x30,0x13,0x06,0x03,0x55,0x04,0x0A,0x13,0x0C,0x74,0x68,0x61,0x77,0x74,0x65, +0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x28,0x30,0x26,0x06,0x03,0x55,0x04,0x0B,0x13, +0x1F,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x53, +0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x20,0x44,0x69,0x76,0x69,0x73,0x69,0x6F,0x6E, +0x31,0x38,0x30,0x36,0x06,0x03,0x55,0x04,0x0B,0x13,0x2F,0x28,0x63,0x29,0x20,0x32, +0x30,0x30,0x38,0x20,0x74,0x68,0x61,0x77,0x74,0x65,0x2C,0x20,0x49,0x6E,0x63,0x2E, +0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74,0x68,0x6F,0x72,0x69,0x7A,0x65, +0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79,0x31,0x24,0x30,0x22,0x06,0x03, +0x55,0x04,0x03,0x13,0x1B,0x74,0x68,0x61,0x77,0x74,0x65,0x20,0x50,0x72,0x69,0x6D, +0x61,0x72,0x79,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x41,0x20,0x2D,0x20,0x47,0x33, +0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01, +0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01, +0x00,0xB2,0xBF,0x27,0x2C,0xFB,0xDB,0xD8,0x5B,0xDD,0x78,0x7B,0x1B,0x9E,0x77,0x66, +0x81,0xCB,0x3E,0xBC,0x7C,0xAE,0xF3,0xA6,0x27,0x9A,0x34,0xA3,0x68,0x31,0x71,0x38, +0x33,0x62,0xE4,0xF3,0x71,0x66,0x79,0xB1,0xA9,0x65,0xA3,0xA5,0x8B,0xD5,0x8F,0x60, +0x2D,0x3F,0x42,0xCC,0xAA,0x6B,0x32,0xC0,0x23,0xCB,0x2C,0x41,0xDD,0xE4,0xDF,0xFC, +0x61,0x9C,0xE2,0x73,0xB2,0x22,0x95,0x11,0x43,0x18,0x5F,0xC4,0xB6,0x1F,0x57,0x6C, +0x0A,0x05,0x58,0x22,0xC8,0x36,0x4C,0x3A,0x7C,0xA5,0xD1,0xCF,0x86,0xAF,0x88,0xA7, +0x44,0x02,0x13,0x74,0x71,0x73,0x0A,0x42,0x59,0x02,0xF8,0x1B,0x14,0x6B,0x42,0xDF, +0x6F,0x5F,0xBA,0x6B,0x82,0xA2,0x9D,0x5B,0xE7,0x4A,0xBD,0x1E,0x01,0x72,0xDB,0x4B, +0x74,0xE8,0x3B,0x7F,0x7F,0x7D,0x1F,0x04,0xB4,0x26,0x9B,0xE0,0xB4,0x5A,0xAC,0x47, +0x3D,0x55,0xB8,0xD7,0xB0,0x26,0x52,0x28,0x01,0x31,0x40,0x66,0xD8,0xD9,0x24,0xBD, +0xF6,0x2A,0xD8,0xEC,0x21,0x49,0x5C,0x9B,0xF6,0x7A,0xE9,0x7F,0x55,0x35,0x7E,0x96, +0x6B,0x8D,0x93,0x93,0x27,0xCB,0x92,0xBB,0xEA,0xAC,0x40,0xC0,0x9F,0xC2,0xF8,0x80, +0xCF,0x5D,0xF4,0x5A,0xDC,0xCE,0x74,0x86,0xA6,0x3E,0x6C,0x0B,0x53,0xCA,0xBD,0x92, +0xCE,0x19,0x06,0x72,0xE6,0x0C,0x5C,0x38,0x69,0xC7,0x04,0xD6,0xBC,0x6C,0xCE,0x5B, +0xF6,0xF7,0x68,0x9C,0xDC,0x25,0x15,0x48,0x88,0xA1,0xE9,0xA9,0xF8,0x98,0x9C,0xE0, +0xF3,0xD5,0x31,0x28,0x61,0x11,0x6C,0x67,0x96,0x8D,0x39,0x99,0xCB,0xC2,0x45,0x24, +0x39,0x02,0x03,0x01,0x00,0x01,0xA3,0x42,0x30,0x40,0x30,0x0F,0x06,0x03,0x55,0x1D, +0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03,0x55, +0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x1D,0x06,0x03,0x55, +0x1D,0x0E,0x04,0x16,0x04,0x14,0xAD,0x6C,0xAA,0x94,0x60,0x9C,0xED,0xE4,0xFF,0xFA, +0x3E,0x0A,0x74,0x2B,0x63,0x03,0xF7,0xB6,0x59,0xBF,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x1A,0x40, +0xD8,0x95,0x65,0xAC,0x09,0x92,0x89,0xC6,0x39,0xF4,0x10,0xE5,0xA9,0x0E,0x66,0x53, +0x5D,0x78,0xDE,0xFA,0x24,0x91,0xBB,0xE7,0x44,0x51,0xDF,0xC6,0x16,0x34,0x0A,0xEF, +0x6A,0x44,0x51,0xEA,0x2B,0x07,0x8A,0x03,0x7A,0xC3,0xEB,0x3F,0x0A,0x2C,0x52,0x16, +0xA0,0x2B,0x43,0xB9,0x25,0x90,0x3F,0x70,0xA9,0x33,0x25,0x6D,0x45,0x1A,0x28,0x3B, +0x27,0xCF,0xAA,0xC3,0x29,0x42,0x1B,0xDF,0x3B,0x4C,0xC0,0x33,0x34,0x5B,0x41,0x88, +0xBF,0x6B,0x2B,0x65,0xAF,0x28,0xEF,0xB2,0xF5,0xC3,0xAA,0x66,0xCE,0x7B,0x56,0xEE, +0xB7,0xC8,0xCB,0x67,0xC1,0xC9,0x9C,0x1A,0x18,0xB8,0xC4,0xC3,0x49,0x03,0xF1,0x60, +0x0E,0x50,0xCD,0x46,0xC5,0xF3,0x77,0x79,0xF7,0xB6,0x15,0xE0,0x38,0xDB,0xC7,0x2F, +0x28,0xA0,0x0C,0x3F,0x77,0x26,0x74,0xD9,0x25,0x12,0xDA,0x31,0xDA,0x1A,0x1E,0xDC, +0x29,0x41,0x91,0x22,0x3C,0x69,0xA7,0xBB,0x02,0xF2,0xB6,0x5C,0x27,0x03,0x89,0xF4, +0x06,0xEA,0x9B,0xE4,0x72,0x82,0xE3,0xA1,0x09,0xC1,0xE9,0x00,0x19,0xD3,0x3E,0xD4, +0x70,0x6B,0xBA,0x71,0xA6,0xAA,0x58,0xAE,0xF4,0xBB,0xE9,0x6C,0xB6,0xEF,0x87,0xCC, +0x9B,0xBB,0xFF,0x39,0xE6,0x56,0x61,0xD3,0x0A,0xA7,0xC4,0x5C,0x4C,0x60,0x7B,0x05, +0x77,0x26,0x7A,0xBF,0xD8,0x07,0x52,0x2C,0x62,0xF7,0x70,0x63,0xD9,0x39,0xBC,0x6F, +0x1C,0xC2,0x79,0xDC,0x76,0x29,0xAF,0xCE,0xC5,0x2C,0x64,0x04,0x5E,0x88,0x36,0x6E, +0x31,0xD4,0x40,0x1A,0x62,0x34,0x36,0x3F,0x35,0x01,0xAE,0xAC,0x63,0xA0, +}; + + +/* subject:/C=ZA/ST=Western Cape/L=Cape Town/O=Thawte Consulting cc/OU=Certification Services Division/CN=Thawte Server CA/emailAddress=server-certs@thawte.com */ +/* issuer :/C=ZA/ST=Western Cape/L=Cape Town/O=Thawte Consulting cc/OU=Certification Services Division/CN=Thawte Server CA/emailAddress=server-certs@thawte.com */ + + +const unsigned char Thawte_Server_CA_certificate[791]={ +0x30,0x82,0x03,0x13,0x30,0x82,0x02,0x7C,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x04,0x05,0x00,0x30, +0x81,0xC4,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x5A,0x41,0x31, +0x15,0x30,0x13,0x06,0x03,0x55,0x04,0x08,0x13,0x0C,0x57,0x65,0x73,0x74,0x65,0x72, +0x6E,0x20,0x43,0x61,0x70,0x65,0x31,0x12,0x30,0x10,0x06,0x03,0x55,0x04,0x07,0x13, +0x09,0x43,0x61,0x70,0x65,0x20,0x54,0x6F,0x77,0x6E,0x31,0x1D,0x30,0x1B,0x06,0x03, +0x55,0x04,0x0A,0x13,0x14,0x54,0x68,0x61,0x77,0x74,0x65,0x20,0x43,0x6F,0x6E,0x73, +0x75,0x6C,0x74,0x69,0x6E,0x67,0x20,0x63,0x63,0x31,0x28,0x30,0x26,0x06,0x03,0x55, +0x04,0x0B,0x13,0x1F,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F, +0x6E,0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x20,0x44,0x69,0x76,0x69,0x73, +0x69,0x6F,0x6E,0x31,0x19,0x30,0x17,0x06,0x03,0x55,0x04,0x03,0x13,0x10,0x54,0x68, +0x61,0x77,0x74,0x65,0x20,0x53,0x65,0x72,0x76,0x65,0x72,0x20,0x43,0x41,0x31,0x26, +0x30,0x24,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x09,0x01,0x16,0x17,0x73, +0x65,0x72,0x76,0x65,0x72,0x2D,0x63,0x65,0x72,0x74,0x73,0x40,0x74,0x68,0x61,0x77, +0x74,0x65,0x2E,0x63,0x6F,0x6D,0x30,0x1E,0x17,0x0D,0x39,0x36,0x30,0x38,0x30,0x31, +0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x32,0x30,0x31,0x32,0x33,0x31,0x32, +0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x81,0xC4,0x31,0x0B,0x30,0x09,0x06,0x03,0x55, +0x04,0x06,0x13,0x02,0x5A,0x41,0x31,0x15,0x30,0x13,0x06,0x03,0x55,0x04,0x08,0x13, +0x0C,0x57,0x65,0x73,0x74,0x65,0x72,0x6E,0x20,0x43,0x61,0x70,0x65,0x31,0x12,0x30, +0x10,0x06,0x03,0x55,0x04,0x07,0x13,0x09,0x43,0x61,0x70,0x65,0x20,0x54,0x6F,0x77, +0x6E,0x31,0x1D,0x30,0x1B,0x06,0x03,0x55,0x04,0x0A,0x13,0x14,0x54,0x68,0x61,0x77, +0x74,0x65,0x20,0x43,0x6F,0x6E,0x73,0x75,0x6C,0x74,0x69,0x6E,0x67,0x20,0x63,0x63, +0x31,0x28,0x30,0x26,0x06,0x03,0x55,0x04,0x0B,0x13,0x1F,0x43,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65, +0x73,0x20,0x44,0x69,0x76,0x69,0x73,0x69,0x6F,0x6E,0x31,0x19,0x30,0x17,0x06,0x03, +0x55,0x04,0x03,0x13,0x10,0x54,0x68,0x61,0x77,0x74,0x65,0x20,0x53,0x65,0x72,0x76, +0x65,0x72,0x20,0x43,0x41,0x31,0x26,0x30,0x24,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7, +0x0D,0x01,0x09,0x01,0x16,0x17,0x73,0x65,0x72,0x76,0x65,0x72,0x2D,0x63,0x65,0x72, +0x74,0x73,0x40,0x74,0x68,0x61,0x77,0x74,0x65,0x2E,0x63,0x6F,0x6D,0x30,0x81,0x9F, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03, +0x81,0x8D,0x00,0x30,0x81,0x89,0x02,0x81,0x81,0x00,0xD3,0xA4,0x50,0x6E,0xC8,0xFF, +0x56,0x6B,0xE6,0xCF,0x5D,0xB6,0xEA,0x0C,0x68,0x75,0x47,0xA2,0xAA,0xC2,0xDA,0x84, +0x25,0xFC,0xA8,0xF4,0x47,0x51,0xDA,0x85,0xB5,0x20,0x74,0x94,0x86,0x1E,0x0F,0x75, +0xC9,0xE9,0x08,0x61,0xF5,0x06,0x6D,0x30,0x6E,0x15,0x19,0x02,0xE9,0x52,0xC0,0x62, +0xDB,0x4D,0x99,0x9E,0xE2,0x6A,0x0C,0x44,0x38,0xCD,0xFE,0xBE,0xE3,0x64,0x09,0x70, +0xC5,0xFE,0xB1,0x6B,0x29,0xB6,0x2F,0x49,0xC8,0x3B,0xD4,0x27,0x04,0x25,0x10,0x97, +0x2F,0xE7,0x90,0x6D,0xC0,0x28,0x42,0x99,0xD7,0x4C,0x43,0xDE,0xC3,0xF5,0x21,0x6D, +0x54,0x9F,0x5D,0xC3,0x58,0xE1,0xC0,0xE4,0xD9,0x5B,0xB0,0xB8,0xDC,0xB4,0x7B,0xDF, +0x36,0x3A,0xC2,0xB5,0x66,0x22,0x12,0xD6,0x87,0x0D,0x02,0x03,0x01,0x00,0x01,0xA3, +0x13,0x30,0x11,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30, +0x03,0x01,0x01,0xFF,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01, +0x04,0x05,0x00,0x03,0x81,0x81,0x00,0x07,0xFA,0x4C,0x69,0x5C,0xFB,0x95,0xCC,0x46, +0xEE,0x85,0x83,0x4D,0x21,0x30,0x8E,0xCA,0xD9,0xA8,0x6F,0x49,0x1A,0xE6,0xDA,0x51, +0xE3,0x60,0x70,0x6C,0x84,0x61,0x11,0xA1,0x1A,0xC8,0x48,0x3E,0x59,0x43,0x7D,0x4F, +0x95,0x3D,0xA1,0x8B,0xB7,0x0B,0x62,0x98,0x7A,0x75,0x8A,0xDD,0x88,0x4E,0x4E,0x9E, +0x40,0xDB,0xA8,0xCC,0x32,0x74,0xB9,0x6F,0x0D,0xC6,0xE3,0xB3,0x44,0x0B,0xD9,0x8A, +0x6F,0x9A,0x29,0x9B,0x99,0x18,0x28,0x3B,0xD1,0xE3,0x40,0x28,0x9A,0x5A,0x3C,0xD5, +0xB5,0xE7,0x20,0x1B,0x8B,0xCA,0xA4,0xAB,0x8D,0xE9,0x51,0xD9,0xE2,0x4C,0x2C,0x59, +0xA9,0xDA,0xB9,0xB2,0x75,0x1B,0xF6,0x42,0xF2,0xEF,0xC7,0xF2,0x18,0xF9,0x89,0xBC, +0xA3,0xFF,0x8A,0x23,0x2E,0x70,0x47, +}; + + +/* subject:/C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN - DATACorp SGC */ +/* issuer :/C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN - DATACorp SGC */ + + +const unsigned char UTN_DATACorp_SGC_Root_CA_certificate[1122]={ +0x30,0x82,0x04,0x5E,0x30,0x82,0x03,0x46,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x44, +0xBE,0x0C,0x8B,0x50,0x00,0x21,0xB4,0x11,0xD3,0x2A,0x68,0x06,0xA9,0xAD,0x69,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x81, +0x93,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x08,0x13,0x02,0x55,0x54,0x31,0x17,0x30,0x15,0x06, +0x03,0x55,0x04,0x07,0x13,0x0E,0x53,0x61,0x6C,0x74,0x20,0x4C,0x61,0x6B,0x65,0x20, +0x43,0x69,0x74,0x79,0x31,0x1E,0x30,0x1C,0x06,0x03,0x55,0x04,0x0A,0x13,0x15,0x54, +0x68,0x65,0x20,0x55,0x53,0x45,0x52,0x54,0x52,0x55,0x53,0x54,0x20,0x4E,0x65,0x74, +0x77,0x6F,0x72,0x6B,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x0B,0x13,0x18,0x68, +0x74,0x74,0x70,0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E,0x75,0x73,0x65,0x72,0x74,0x72, +0x75,0x73,0x74,0x2E,0x63,0x6F,0x6D,0x31,0x1B,0x30,0x19,0x06,0x03,0x55,0x04,0x03, +0x13,0x12,0x55,0x54,0x4E,0x20,0x2D,0x20,0x44,0x41,0x54,0x41,0x43,0x6F,0x72,0x70, +0x20,0x53,0x47,0x43,0x30,0x1E,0x17,0x0D,0x39,0x39,0x30,0x36,0x32,0x34,0x31,0x38, +0x35,0x37,0x32,0x31,0x5A,0x17,0x0D,0x31,0x39,0x30,0x36,0x32,0x34,0x31,0x39,0x30, +0x36,0x33,0x30,0x5A,0x30,0x81,0x93,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06, +0x13,0x02,0x55,0x53,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x08,0x13,0x02,0x55, +0x54,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04,0x07,0x13,0x0E,0x53,0x61,0x6C,0x74, +0x20,0x4C,0x61,0x6B,0x65,0x20,0x43,0x69,0x74,0x79,0x31,0x1E,0x30,0x1C,0x06,0x03, +0x55,0x04,0x0A,0x13,0x15,0x54,0x68,0x65,0x20,0x55,0x53,0x45,0x52,0x54,0x52,0x55, +0x53,0x54,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x31,0x21,0x30,0x1F,0x06,0x03, +0x55,0x04,0x0B,0x13,0x18,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E, +0x75,0x73,0x65,0x72,0x74,0x72,0x75,0x73,0x74,0x2E,0x63,0x6F,0x6D,0x31,0x1B,0x30, +0x19,0x06,0x03,0x55,0x04,0x03,0x13,0x12,0x55,0x54,0x4E,0x20,0x2D,0x20,0x44,0x41, +0x54,0x41,0x43,0x6F,0x72,0x70,0x20,0x53,0x47,0x43,0x30,0x82,0x01,0x22,0x30,0x0D, +0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01, +0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xDF,0xEE,0x58,0x10,0xA2, +0x2B,0x6E,0x55,0xC4,0x8E,0xBF,0x2E,0x46,0x09,0xE7,0xE0,0x08,0x0F,0x2E,0x2B,0x7A, +0x13,0x94,0x1B,0xBD,0xF6,0xB6,0x80,0x8E,0x65,0x05,0x93,0x00,0x1E,0xBC,0xAF,0xE2, +0x0F,0x8E,0x19,0x0D,0x12,0x47,0xEC,0xAC,0xAD,0xA3,0xFA,0x2E,0x70,0xF8,0xDE,0x6E, +0xFB,0x56,0x42,0x15,0x9E,0x2E,0x5C,0xEF,0x23,0xDE,0x21,0xB9,0x05,0x76,0x27,0x19, +0x0F,0x4F,0xD6,0xC3,0x9C,0xB4,0xBE,0x94,0x19,0x63,0xF2,0xA6,0x11,0x0A,0xEB,0x53, +0x48,0x9C,0xBE,0xF2,0x29,0x3B,0x16,0xE8,0x1A,0xA0,0x4C,0xA6,0xC9,0xF4,0x18,0x59, +0x68,0xC0,0x70,0xF2,0x53,0x00,0xC0,0x5E,0x50,0x82,0xA5,0x56,0x6F,0x36,0xF9,0x4A, +0xE0,0x44,0x86,0xA0,0x4D,0x4E,0xD6,0x47,0x6E,0x49,0x4A,0xCB,0x67,0xD7,0xA6,0xC4, +0x05,0xB9,0x8E,0x1E,0xF4,0xFC,0xFF,0xCD,0xE7,0x36,0xE0,0x9C,0x05,0x6C,0xB2,0x33, +0x22,0x15,0xD0,0xB4,0xE0,0xCC,0x17,0xC0,0xB2,0xC0,0xF4,0xFE,0x32,0x3F,0x29,0x2A, +0x95,0x7B,0xD8,0xF2,0xA7,0x4E,0x0F,0x54,0x7C,0xA1,0x0D,0x80,0xB3,0x09,0x03,0xC1, +0xFF,0x5C,0xDD,0x5E,0x9A,0x3E,0xBC,0xAE,0xBC,0x47,0x8A,0x6A,0xAE,0x71,0xCA,0x1F, +0xB1,0x2A,0xB8,0x5F,0x42,0x05,0x0B,0xEC,0x46,0x30,0xD1,0x72,0x0B,0xCA,0xE9,0x56, +0x6D,0xF5,0xEF,0xDF,0x78,0xBE,0x61,0xBA,0xB2,0xA5,0xAE,0x04,0x4C,0xBC,0xA8,0xAC, +0x69,0x15,0x97,0xBD,0xEF,0xEB,0xB4,0x8C,0xBF,0x35,0xF8,0xD4,0xC3,0xD1,0x28,0x0E, +0x5C,0x3A,0x9F,0x70,0x18,0x33,0x20,0x77,0xC4,0xA2,0xAF,0x02,0x03,0x01,0x00,0x01, +0xA3,0x81,0xAB,0x30,0x81,0xA8,0x30,0x0B,0x06,0x03,0x55,0x1D,0x0F,0x04,0x04,0x03, +0x02,0x01,0xC6,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30, +0x03,0x01,0x01,0xFF,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x53, +0x32,0xD1,0xB3,0xCF,0x7F,0xFA,0xE0,0xF1,0xA0,0x5D,0x85,0x4E,0x92,0xD2,0x9E,0x45, +0x1D,0xB4,0x4F,0x30,0x3D,0x06,0x03,0x55,0x1D,0x1F,0x04,0x36,0x30,0x34,0x30,0x32, +0xA0,0x30,0xA0,0x2E,0x86,0x2C,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x63,0x72,0x6C, +0x2E,0x75,0x73,0x65,0x72,0x74,0x72,0x75,0x73,0x74,0x2E,0x63,0x6F,0x6D,0x2F,0x55, +0x54,0x4E,0x2D,0x44,0x41,0x54,0x41,0x43,0x6F,0x72,0x70,0x53,0x47,0x43,0x2E,0x63, +0x72,0x6C,0x30,0x2A,0x06,0x03,0x55,0x1D,0x25,0x04,0x23,0x30,0x21,0x06,0x08,0x2B, +0x06,0x01,0x05,0x05,0x07,0x03,0x01,0x06,0x0A,0x2B,0x06,0x01,0x04,0x01,0x82,0x37, +0x0A,0x03,0x03,0x06,0x09,0x60,0x86,0x48,0x01,0x86,0xF8,0x42,0x04,0x01,0x30,0x0D, +0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01, +0x01,0x00,0x27,0x35,0x97,0x00,0x8A,0x8B,0x28,0xBD,0xC6,0x33,0x30,0x1E,0x29,0xFC, +0xE2,0xF7,0xD5,0x98,0xD4,0x40,0xBB,0x60,0xCA,0xBF,0xAB,0x17,0x2C,0x09,0x36,0x7F, +0x50,0xFA,0x41,0xDC,0xAE,0x96,0x3A,0x0A,0x23,0x3E,0x89,0x59,0xC9,0xA3,0x07,0xED, +0x1B,0x37,0xAD,0xFC,0x7C,0xBE,0x51,0x49,0x5A,0xDE,0x3A,0x0A,0x54,0x08,0x16,0x45, +0xC2,0x99,0xB1,0x87,0xCD,0x8C,0x68,0xE0,0x69,0x03,0xE9,0xC4,0x4E,0x98,0xB2,0x3B, +0x8C,0x16,0xB3,0x0E,0xA0,0x0C,0x98,0x50,0x9B,0x93,0xA9,0x70,0x09,0xC8,0x2C,0xA3, +0x8F,0xDF,0x02,0xE4,0xE0,0x71,0x3A,0xF1,0xB4,0x23,0x72,0xA0,0xAA,0x01,0xDF,0xDF, +0x98,0x3E,0x14,0x50,0xA0,0x31,0x26,0xBD,0x28,0xE9,0x5A,0x30,0x26,0x75,0xF9,0x7B, +0x60,0x1C,0x8D,0xF3,0xCD,0x50,0x26,0x6D,0x04,0x27,0x9A,0xDF,0xD5,0x0D,0x45,0x47, +0x29,0x6B,0x2C,0xE6,0x76,0xD9,0xA9,0x29,0x7D,0x32,0xDD,0xC9,0x36,0x3C,0xBD,0xAE, +0x35,0xF1,0x11,0x9E,0x1D,0xBB,0x90,0x3F,0x12,0x47,0x4E,0x8E,0xD7,0x7E,0x0F,0x62, +0x73,0x1D,0x52,0x26,0x38,0x1C,0x18,0x49,0xFD,0x30,0x74,0x9A,0xC4,0xE5,0x22,0x2F, +0xD8,0xC0,0x8D,0xED,0x91,0x7A,0x4C,0x00,0x8F,0x72,0x7F,0x5D,0xDA,0xDD,0x1B,0x8B, +0x45,0x6B,0xE7,0xDD,0x69,0x97,0xA8,0xC5,0x56,0x4C,0x0F,0x0C,0xF6,0x9F,0x7A,0x91, +0x37,0xF6,0x97,0x82,0xE0,0xDD,0x71,0x69,0xFF,0x76,0x3F,0x60,0x4D,0x3C,0xCF,0xF7, +0x99,0xF9,0xC6,0x57,0xF4,0xC9,0x55,0x39,0x78,0xBA,0x2C,0x79,0xC9,0xA6,0x88,0x2B, +0xF4,0x08, +}; + + +/* subject:/C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Hardware */ +/* issuer :/C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Hardware */ + + +const unsigned char UTN_USERFirst_Hardware_Root_CA_certificate[1144]={ +0x30,0x82,0x04,0x74,0x30,0x82,0x03,0x5C,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x44, +0xBE,0x0C,0x8B,0x50,0x00,0x24,0xB4,0x11,0xD3,0x36,0x2A,0xFE,0x65,0x0A,0xFD,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x81, +0x97,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x08,0x13,0x02,0x55,0x54,0x31,0x17,0x30,0x15,0x06, +0x03,0x55,0x04,0x07,0x13,0x0E,0x53,0x61,0x6C,0x74,0x20,0x4C,0x61,0x6B,0x65,0x20, +0x43,0x69,0x74,0x79,0x31,0x1E,0x30,0x1C,0x06,0x03,0x55,0x04,0x0A,0x13,0x15,0x54, +0x68,0x65,0x20,0x55,0x53,0x45,0x52,0x54,0x52,0x55,0x53,0x54,0x20,0x4E,0x65,0x74, +0x77,0x6F,0x72,0x6B,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x0B,0x13,0x18,0x68, +0x74,0x74,0x70,0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E,0x75,0x73,0x65,0x72,0x74,0x72, +0x75,0x73,0x74,0x2E,0x63,0x6F,0x6D,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x03, +0x13,0x16,0x55,0x54,0x4E,0x2D,0x55,0x53,0x45,0x52,0x46,0x69,0x72,0x73,0x74,0x2D, +0x48,0x61,0x72,0x64,0x77,0x61,0x72,0x65,0x30,0x1E,0x17,0x0D,0x39,0x39,0x30,0x37, +0x30,0x39,0x31,0x38,0x31,0x30,0x34,0x32,0x5A,0x17,0x0D,0x31,0x39,0x30,0x37,0x30, +0x39,0x31,0x38,0x31,0x39,0x32,0x32,0x5A,0x30,0x81,0x97,0x31,0x0B,0x30,0x09,0x06, +0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04, +0x08,0x13,0x02,0x55,0x54,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04,0x07,0x13,0x0E, +0x53,0x61,0x6C,0x74,0x20,0x4C,0x61,0x6B,0x65,0x20,0x43,0x69,0x74,0x79,0x31,0x1E, +0x30,0x1C,0x06,0x03,0x55,0x04,0x0A,0x13,0x15,0x54,0x68,0x65,0x20,0x55,0x53,0x45, +0x52,0x54,0x52,0x55,0x53,0x54,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x31,0x21, +0x30,0x1F,0x06,0x03,0x55,0x04,0x0B,0x13,0x18,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F, +0x77,0x77,0x77,0x2E,0x75,0x73,0x65,0x72,0x74,0x72,0x75,0x73,0x74,0x2E,0x63,0x6F, +0x6D,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x03,0x13,0x16,0x55,0x54,0x4E,0x2D, +0x55,0x53,0x45,0x52,0x46,0x69,0x72,0x73,0x74,0x2D,0x48,0x61,0x72,0x64,0x77,0x61, +0x72,0x65,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D, +0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82, +0x01,0x01,0x00,0xB1,0xF7,0xC3,0x38,0x3F,0xB4,0xA8,0x7F,0xCF,0x39,0x82,0x51,0x67, +0xD0,0x6D,0x9F,0xD2,0xFF,0x58,0xF3,0xE7,0x9F,0x2B,0xEC,0x0D,0x89,0x54,0x99,0xB9, +0x38,0x99,0x16,0xF7,0xE0,0x21,0x79,0x48,0xC2,0xBB,0x61,0x74,0x12,0x96,0x1D,0x3C, +0x6A,0x72,0xD5,0x3C,0x10,0x67,0x3A,0x39,0xED,0x2B,0x13,0xCD,0x66,0xEB,0x95,0x09, +0x33,0xA4,0x6C,0x97,0xB1,0xE8,0xC6,0xEC,0xC1,0x75,0x79,0x9C,0x46,0x5E,0x8D,0xAB, +0xD0,0x6A,0xFD,0xB9,0x2A,0x55,0x17,0x10,0x54,0xB3,0x19,0xF0,0x9A,0xF6,0xF1,0xB1, +0x5D,0xB6,0xA7,0x6D,0xFB,0xE0,0x71,0x17,0x6B,0xA2,0x88,0xFB,0x00,0xDF,0xFE,0x1A, +0x31,0x77,0x0C,0x9A,0x01,0x7A,0xB1,0x32,0xE3,0x2B,0x01,0x07,0x38,0x6E,0xC3,0xA5, +0x5E,0x23,0xBC,0x45,0x9B,0x7B,0x50,0xC1,0xC9,0x30,0x8F,0xDB,0xE5,0x2B,0x7A,0xD3, +0x5B,0xFB,0x33,0x40,0x1E,0xA0,0xD5,0x98,0x17,0xBC,0x8B,0x87,0xC3,0x89,0xD3,0x5D, +0xA0,0x8E,0xB2,0xAA,0xAA,0xF6,0x8E,0x69,0x88,0x06,0xC5,0xFA,0x89,0x21,0xF3,0x08, +0x9D,0x69,0x2E,0x09,0x33,0x9B,0x29,0x0D,0x46,0x0F,0x8C,0xCC,0x49,0x34,0xB0,0x69, +0x51,0xBD,0xF9,0x06,0xCD,0x68,0xAD,0x66,0x4C,0xBC,0x3E,0xAC,0x61,0xBD,0x0A,0x88, +0x0E,0xC8,0xDF,0x3D,0xEE,0x7C,0x04,0x4C,0x9D,0x0A,0x5E,0x6B,0x91,0xD6,0xEE,0xC7, +0xED,0x28,0x8D,0xAB,0x4D,0x87,0x89,0x73,0xD0,0x6E,0xA4,0xD0,0x1E,0x16,0x8B,0x14, +0xE1,0x76,0x44,0x03,0x7F,0x63,0xAC,0xE4,0xCD,0x49,0x9C,0xC5,0x92,0xF4,0xAB,0x32, +0xA1,0x48,0x5B,0x02,0x03,0x01,0x00,0x01,0xA3,0x81,0xB9,0x30,0x81,0xB6,0x30,0x0B, +0x06,0x03,0x55,0x1D,0x0F,0x04,0x04,0x03,0x02,0x01,0xC6,0x30,0x0F,0x06,0x03,0x55, +0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x1D,0x06,0x03, +0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0xA1,0x72,0x5F,0x26,0x1B,0x28,0x98,0x43,0x95, +0x5D,0x07,0x37,0xD5,0x85,0x96,0x9D,0x4B,0xD2,0xC3,0x45,0x30,0x44,0x06,0x03,0x55, +0x1D,0x1F,0x04,0x3D,0x30,0x3B,0x30,0x39,0xA0,0x37,0xA0,0x35,0x86,0x33,0x68,0x74, +0x74,0x70,0x3A,0x2F,0x2F,0x63,0x72,0x6C,0x2E,0x75,0x73,0x65,0x72,0x74,0x72,0x75, +0x73,0x74,0x2E,0x63,0x6F,0x6D,0x2F,0x55,0x54,0x4E,0x2D,0x55,0x53,0x45,0x52,0x46, +0x69,0x72,0x73,0x74,0x2D,0x48,0x61,0x72,0x64,0x77,0x61,0x72,0x65,0x2E,0x63,0x72, +0x6C,0x30,0x31,0x06,0x03,0x55,0x1D,0x25,0x04,0x2A,0x30,0x28,0x06,0x08,0x2B,0x06, +0x01,0x05,0x05,0x07,0x03,0x01,0x06,0x08,0x2B,0x06,0x01,0x05,0x05,0x07,0x03,0x05, +0x06,0x08,0x2B,0x06,0x01,0x05,0x05,0x07,0x03,0x06,0x06,0x08,0x2B,0x06,0x01,0x05, +0x05,0x07,0x03,0x07,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01, +0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x47,0x19,0x0F,0xDE,0x74,0xC6,0x99,0x97, +0xAF,0xFC,0xAD,0x28,0x5E,0x75,0x8E,0xEB,0x2D,0x67,0xEE,0x4E,0x7B,0x2B,0xD7,0x0C, +0xFF,0xF6,0xDE,0xCB,0x55,0xA2,0x0A,0xE1,0x4C,0x54,0x65,0x93,0x60,0x6B,0x9F,0x12, +0x9C,0xAD,0x5E,0x83,0x2C,0xEB,0x5A,0xAE,0xC0,0xE4,0x2D,0xF4,0x00,0x63,0x1D,0xB8, +0xC0,0x6C,0xF2,0xCF,0x49,0xBB,0x4D,0x93,0x6F,0x06,0xA6,0x0A,0x22,0xB2,0x49,0x62, +0x08,0x4E,0xFF,0xC8,0xC8,0x14,0xB2,0x88,0x16,0x5D,0xE7,0x01,0xE4,0x12,0x95,0xE5, +0x45,0x34,0xB3,0x8B,0x69,0xBD,0xCF,0xB4,0x85,0x8F,0x75,0x51,0x9E,0x7D,0x3A,0x38, +0x3A,0x14,0x48,0x12,0xC6,0xFB,0xA7,0x3B,0x1A,0x8D,0x0D,0x82,0x40,0x07,0xE8,0x04, +0x08,0x90,0xA1,0x89,0xCB,0x19,0x50,0xDF,0xCA,0x1C,0x01,0xBC,0x1D,0x04,0x19,0x7B, +0x10,0x76,0x97,0x3B,0xEE,0x90,0x90,0xCA,0xC4,0x0E,0x1F,0x16,0x6E,0x75,0xEF,0x33, +0xF8,0xD3,0x6F,0x5B,0x1E,0x96,0xE3,0xE0,0x74,0x77,0x74,0x7B,0x8A,0xA2,0x6E,0x2D, +0xDD,0x76,0xD6,0x39,0x30,0x82,0xF0,0xAB,0x9C,0x52,0xF2,0x2A,0xC7,0xAF,0x49,0x5E, +0x7E,0xC7,0x68,0xE5,0x82,0x81,0xC8,0x6A,0x27,0xF9,0x27,0x88,0x2A,0xD5,0x58,0x50, +0x95,0x1F,0xF0,0x3B,0x1C,0x57,0xBB,0x7D,0x14,0x39,0x62,0x2B,0x9A,0xC9,0x94,0x92, +0x2A,0xA3,0x22,0x0C,0xFF,0x89,0x26,0x7D,0x5F,0x23,0x2B,0x47,0xD7,0x15,0x1D,0xA9, +0x6A,0x9E,0x51,0x0D,0x2A,0x51,0x9E,0x81,0xF9,0xD4,0x3B,0x5E,0x70,0x12,0x7F,0x10, +0x32,0x9C,0x1E,0xBB,0x9D,0xF8,0x66,0xA8, +}; + + +/* subject:/L=ValiCert Validation Network/O=ValiCert, Inc./OU=ValiCert Class 1 Policy Validation Authority/CN=http://www.valicert.com//emailAddress=info@valicert.com */ +/* issuer :/L=ValiCert Validation Network/O=ValiCert, Inc./OU=ValiCert Class 1 Policy Validation Authority/CN=http://www.valicert.com//emailAddress=info@valicert.com */ + + +const unsigned char ValiCert_Class_1_VA_certificate[747]={ +0x30,0x82,0x02,0xE7,0x30,0x82,0x02,0x50,0x02,0x01,0x01,0x30,0x0D,0x06,0x09,0x2A, +0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x81,0xBB,0x31,0x24,0x30, +0x22,0x06,0x03,0x55,0x04,0x07,0x13,0x1B,0x56,0x61,0x6C,0x69,0x43,0x65,0x72,0x74, +0x20,0x56,0x61,0x6C,0x69,0x64,0x61,0x74,0x69,0x6F,0x6E,0x20,0x4E,0x65,0x74,0x77, +0x6F,0x72,0x6B,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04,0x0A,0x13,0x0E,0x56,0x61, +0x6C,0x69,0x43,0x65,0x72,0x74,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x35,0x30,0x33, +0x06,0x03,0x55,0x04,0x0B,0x13,0x2C,0x56,0x61,0x6C,0x69,0x43,0x65,0x72,0x74,0x20, +0x43,0x6C,0x61,0x73,0x73,0x20,0x31,0x20,0x50,0x6F,0x6C,0x69,0x63,0x79,0x20,0x56, +0x61,0x6C,0x69,0x64,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72, +0x69,0x74,0x79,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x03,0x13,0x18,0x68,0x74, +0x74,0x70,0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E,0x76,0x61,0x6C,0x69,0x63,0x65,0x72, +0x74,0x2E,0x63,0x6F,0x6D,0x2F,0x31,0x20,0x30,0x1E,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x09,0x01,0x16,0x11,0x69,0x6E,0x66,0x6F,0x40,0x76,0x61,0x6C,0x69, +0x63,0x65,0x72,0x74,0x2E,0x63,0x6F,0x6D,0x30,0x1E,0x17,0x0D,0x39,0x39,0x30,0x36, +0x32,0x35,0x32,0x32,0x32,0x33,0x34,0x38,0x5A,0x17,0x0D,0x31,0x39,0x30,0x36,0x32, +0x35,0x32,0x32,0x32,0x33,0x34,0x38,0x5A,0x30,0x81,0xBB,0x31,0x24,0x30,0x22,0x06, +0x03,0x55,0x04,0x07,0x13,0x1B,0x56,0x61,0x6C,0x69,0x43,0x65,0x72,0x74,0x20,0x56, +0x61,0x6C,0x69,0x64,0x61,0x74,0x69,0x6F,0x6E,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72, +0x6B,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04,0x0A,0x13,0x0E,0x56,0x61,0x6C,0x69, +0x43,0x65,0x72,0x74,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x35,0x30,0x33,0x06,0x03, +0x55,0x04,0x0B,0x13,0x2C,0x56,0x61,0x6C,0x69,0x43,0x65,0x72,0x74,0x20,0x43,0x6C, +0x61,0x73,0x73,0x20,0x31,0x20,0x50,0x6F,0x6C,0x69,0x63,0x79,0x20,0x56,0x61,0x6C, +0x69,0x64,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74, +0x79,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x03,0x13,0x18,0x68,0x74,0x74,0x70, +0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E,0x76,0x61,0x6C,0x69,0x63,0x65,0x72,0x74,0x2E, +0x63,0x6F,0x6D,0x2F,0x31,0x20,0x30,0x1E,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D, +0x01,0x09,0x01,0x16,0x11,0x69,0x6E,0x66,0x6F,0x40,0x76,0x61,0x6C,0x69,0x63,0x65, +0x72,0x74,0x2E,0x63,0x6F,0x6D,0x30,0x81,0x9F,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48, +0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x81,0x8D,0x00,0x30,0x81,0x89,0x02, +0x81,0x81,0x00,0xD8,0x59,0x82,0x7A,0x89,0xB8,0x96,0xBA,0xA6,0x2F,0x68,0x6F,0x58, +0x2E,0xA7,0x54,0x1C,0x06,0x6E,0xF4,0xEA,0x8D,0x48,0xBC,0x31,0x94,0x17,0xF0,0xF3, +0x4E,0xBC,0xB2,0xB8,0x35,0x92,0x76,0xB0,0xD0,0xA5,0xA5,0x01,0xD7,0x00,0x03,0x12, +0x22,0x19,0x08,0xF8,0xFF,0x11,0x23,0x9B,0xCE,0x07,0xF5,0xBF,0x69,0x1A,0x26,0xFE, +0x4E,0xE9,0xD1,0x7F,0x9D,0x2C,0x40,0x1D,0x59,0x68,0x6E,0xA6,0xF8,0x58,0xB0,0x9D, +0x1A,0x8F,0xD3,0x3F,0xF1,0xDC,0x19,0x06,0x81,0xA8,0x0E,0xE0,0x3A,0xDD,0xC8,0x53, +0x45,0x09,0x06,0xE6,0x0F,0x70,0xC3,0xFA,0x40,0xA6,0x0E,0xE2,0x56,0x05,0x0F,0x18, +0x4D,0xFC,0x20,0x82,0xD1,0x73,0x55,0x74,0x8D,0x76,0x72,0xA0,0x1D,0x9D,0x1D,0xC0, +0xDD,0x3F,0x71,0x02,0x03,0x01,0x00,0x01,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x81,0x81,0x00,0x50,0x68,0x3D,0x49,0xF4, +0x2C,0x1C,0x06,0x94,0xDF,0x95,0x60,0x7F,0x96,0x7B,0x17,0xFE,0x4F,0x71,0xAD,0x64, +0xC8,0xDD,0x77,0xD2,0xEF,0x59,0x55,0xE8,0x3F,0xE8,0x8E,0x05,0x2A,0x21,0xF2,0x07, +0xD2,0xB5,0xA7,0x52,0xFE,0x9C,0xB1,0xB6,0xE2,0x5B,0x77,0x17,0x40,0xEA,0x72,0xD6, +0x23,0xCB,0x28,0x81,0x32,0xC3,0x00,0x79,0x18,0xEC,0x59,0x17,0x89,0xC9,0xC6,0x6A, +0x1E,0x71,0xC9,0xFD,0xB7,0x74,0xA5,0x25,0x45,0x69,0xC5,0x48,0xAB,0x19,0xE1,0x45, +0x8A,0x25,0x6B,0x19,0xEE,0xE5,0xBB,0x12,0xF5,0x7F,0xF7,0xA6,0x8D,0x51,0xC3,0xF0, +0x9D,0x74,0xB7,0xA9,0x3E,0xA0,0xA5,0xFF,0xB6,0x49,0x03,0x13,0xDA,0x22,0xCC,0xED, +0x71,0x82,0x2B,0x99,0xCF,0x3A,0xB7,0xF5,0x2D,0x72,0xC8, +}; + + +/* subject:/L=ValiCert Validation Network/O=ValiCert, Inc./OU=ValiCert Class 2 Policy Validation Authority/CN=http://www.valicert.com//emailAddress=info@valicert.com */ +/* issuer :/L=ValiCert Validation Network/O=ValiCert, Inc./OU=ValiCert Class 2 Policy Validation Authority/CN=http://www.valicert.com//emailAddress=info@valicert.com */ + + +const unsigned char ValiCert_Class_2_VA_certificate[747]={ +0x30,0x82,0x02,0xE7,0x30,0x82,0x02,0x50,0x02,0x01,0x01,0x30,0x0D,0x06,0x09,0x2A, +0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x81,0xBB,0x31,0x24,0x30, +0x22,0x06,0x03,0x55,0x04,0x07,0x13,0x1B,0x56,0x61,0x6C,0x69,0x43,0x65,0x72,0x74, +0x20,0x56,0x61,0x6C,0x69,0x64,0x61,0x74,0x69,0x6F,0x6E,0x20,0x4E,0x65,0x74,0x77, +0x6F,0x72,0x6B,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04,0x0A,0x13,0x0E,0x56,0x61, +0x6C,0x69,0x43,0x65,0x72,0x74,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x35,0x30,0x33, +0x06,0x03,0x55,0x04,0x0B,0x13,0x2C,0x56,0x61,0x6C,0x69,0x43,0x65,0x72,0x74,0x20, +0x43,0x6C,0x61,0x73,0x73,0x20,0x32,0x20,0x50,0x6F,0x6C,0x69,0x63,0x79,0x20,0x56, +0x61,0x6C,0x69,0x64,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72, +0x69,0x74,0x79,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x03,0x13,0x18,0x68,0x74, +0x74,0x70,0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E,0x76,0x61,0x6C,0x69,0x63,0x65,0x72, +0x74,0x2E,0x63,0x6F,0x6D,0x2F,0x31,0x20,0x30,0x1E,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x09,0x01,0x16,0x11,0x69,0x6E,0x66,0x6F,0x40,0x76,0x61,0x6C,0x69, +0x63,0x65,0x72,0x74,0x2E,0x63,0x6F,0x6D,0x30,0x1E,0x17,0x0D,0x39,0x39,0x30,0x36, +0x32,0x36,0x30,0x30,0x31,0x39,0x35,0x34,0x5A,0x17,0x0D,0x31,0x39,0x30,0x36,0x32, +0x36,0x30,0x30,0x31,0x39,0x35,0x34,0x5A,0x30,0x81,0xBB,0x31,0x24,0x30,0x22,0x06, +0x03,0x55,0x04,0x07,0x13,0x1B,0x56,0x61,0x6C,0x69,0x43,0x65,0x72,0x74,0x20,0x56, +0x61,0x6C,0x69,0x64,0x61,0x74,0x69,0x6F,0x6E,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72, +0x6B,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04,0x0A,0x13,0x0E,0x56,0x61,0x6C,0x69, +0x43,0x65,0x72,0x74,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x35,0x30,0x33,0x06,0x03, +0x55,0x04,0x0B,0x13,0x2C,0x56,0x61,0x6C,0x69,0x43,0x65,0x72,0x74,0x20,0x43,0x6C, +0x61,0x73,0x73,0x20,0x32,0x20,0x50,0x6F,0x6C,0x69,0x63,0x79,0x20,0x56,0x61,0x6C, +0x69,0x64,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74, +0x79,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x03,0x13,0x18,0x68,0x74,0x74,0x70, +0x3A,0x2F,0x2F,0x77,0x77,0x77,0x2E,0x76,0x61,0x6C,0x69,0x63,0x65,0x72,0x74,0x2E, +0x63,0x6F,0x6D,0x2F,0x31,0x20,0x30,0x1E,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D, +0x01,0x09,0x01,0x16,0x11,0x69,0x6E,0x66,0x6F,0x40,0x76,0x61,0x6C,0x69,0x63,0x65, +0x72,0x74,0x2E,0x63,0x6F,0x6D,0x30,0x81,0x9F,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48, +0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x81,0x8D,0x00,0x30,0x81,0x89,0x02, +0x81,0x81,0x00,0xCE,0x3A,0x71,0xCA,0xE5,0xAB,0xC8,0x59,0x92,0x55,0xD7,0xAB,0xD8, +0x74,0x0E,0xF9,0xEE,0xD9,0xF6,0x55,0x47,0x59,0x65,0x47,0x0E,0x05,0x55,0xDC,0xEB, +0x98,0x36,0x3C,0x5C,0x53,0x5D,0xD3,0x30,0xCF,0x38,0xEC,0xBD,0x41,0x89,0xED,0x25, +0x42,0x09,0x24,0x6B,0x0A,0x5E,0xB3,0x7C,0xDD,0x52,0x2D,0x4C,0xE6,0xD4,0xD6,0x7D, +0x5A,0x59,0xA9,0x65,0xD4,0x49,0x13,0x2D,0x24,0x4D,0x1C,0x50,0x6F,0xB5,0xC1,0x85, +0x54,0x3B,0xFE,0x71,0xE4,0xD3,0x5C,0x42,0xF9,0x80,0xE0,0x91,0x1A,0x0A,0x5B,0x39, +0x36,0x67,0xF3,0x3F,0x55,0x7C,0x1B,0x3F,0xB4,0x5F,0x64,0x73,0x34,0xE3,0xB4,0x12, +0xBF,0x87,0x64,0xF8,0xDA,0x12,0xFF,0x37,0x27,0xC1,0xB3,0x43,0xBB,0xEF,0x7B,0x6E, +0x2E,0x69,0xF7,0x02,0x03,0x01,0x00,0x01,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x81,0x81,0x00,0x3B,0x7F,0x50,0x6F,0x6F, +0x50,0x94,0x99,0x49,0x62,0x38,0x38,0x1F,0x4B,0xF8,0xA5,0xC8,0x3E,0xA7,0x82,0x81, +0xF6,0x2B,0xC7,0xE8,0xC5,0xCE,0xE8,0x3A,0x10,0x82,0xCB,0x18,0x00,0x8E,0x4D,0xBD, +0xA8,0x58,0x7F,0xA1,0x79,0x00,0xB5,0xBB,0xE9,0x8D,0xAF,0x41,0xD9,0x0F,0x34,0xEE, +0x21,0x81,0x19,0xA0,0x32,0x49,0x28,0xF4,0xC4,0x8E,0x56,0xD5,0x52,0x33,0xFD,0x50, +0xD5,0x7E,0x99,0x6C,0x03,0xE4,0xC9,0x4C,0xFC,0xCB,0x6C,0xAB,0x66,0xB3,0x4A,0x21, +0x8C,0xE5,0xB5,0x0C,0x32,0x3E,0x10,0xB2,0xCC,0x6C,0xA1,0xDC,0x9A,0x98,0x4C,0x02, +0x5B,0xF3,0xCE,0xB9,0x9E,0xA5,0x72,0x0E,0x4A,0xB7,0x3F,0x3C,0xE6,0x16,0x68,0xF8, +0xBE,0xED,0x74,0x4C,0xBC,0x5B,0xD5,0x62,0x1F,0x43,0xDD, +}; + + +/* subject:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority */ +/* issuer :/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority */ + + +const unsigned char Verisign_Class_3_Public_Primary_Certification_Authority_certificate[576]={ +0x30,0x82,0x02,0x3C,0x30,0x82,0x01,0xA5,0x02,0x10,0x3C,0x91,0x31,0xCB,0x1F,0xF6, +0xD0,0x1B,0x0E,0x9A,0xB8,0xD0,0x44,0xBF,0x12,0xBE,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x5F,0x31,0x0B,0x30,0x09,0x06, +0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04, +0x0A,0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20,0x49,0x6E,0x63, +0x2E,0x31,0x37,0x30,0x35,0x06,0x03,0x55,0x04,0x0B,0x13,0x2E,0x43,0x6C,0x61,0x73, +0x73,0x20,0x33,0x20,0x50,0x75,0x62,0x6C,0x69,0x63,0x20,0x50,0x72,0x69,0x6D,0x61, +0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E, +0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x1E,0x17,0x0D,0x39,0x36, +0x30,0x31,0x32,0x39,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x32,0x38,0x30, +0x38,0x30,0x32,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x5F,0x31,0x0B,0x30,0x09, +0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17,0x30,0x15,0x06,0x03,0x55, +0x04,0x0A,0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20,0x49,0x6E, +0x63,0x2E,0x31,0x37,0x30,0x35,0x06,0x03,0x55,0x04,0x0B,0x13,0x2E,0x43,0x6C,0x61, +0x73,0x73,0x20,0x33,0x20,0x50,0x75,0x62,0x6C,0x69,0x63,0x20,0x50,0x72,0x69,0x6D, +0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F, +0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x81,0x9F,0x30,0x0D, +0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x81,0x8D, +0x00,0x30,0x81,0x89,0x02,0x81,0x81,0x00,0xC9,0x5C,0x59,0x9E,0xF2,0x1B,0x8A,0x01, +0x14,0xB4,0x10,0xDF,0x04,0x40,0xDB,0xE3,0x57,0xAF,0x6A,0x45,0x40,0x8F,0x84,0x0C, +0x0B,0xD1,0x33,0xD9,0xD9,0x11,0xCF,0xEE,0x02,0x58,0x1F,0x25,0xF7,0x2A,0xA8,0x44, +0x05,0xAA,0xEC,0x03,0x1F,0x78,0x7F,0x9E,0x93,0xB9,0x9A,0x00,0xAA,0x23,0x7D,0xD6, +0xAC,0x85,0xA2,0x63,0x45,0xC7,0x72,0x27,0xCC,0xF4,0x4C,0xC6,0x75,0x71,0xD2,0x39, +0xEF,0x4F,0x42,0xF0,0x75,0xDF,0x0A,0x90,0xC6,0x8E,0x20,0x6F,0x98,0x0F,0xF8,0xAC, +0x23,0x5F,0x70,0x29,0x36,0xA4,0xC9,0x86,0xE7,0xB1,0x9A,0x20,0xCB,0x53,0xA5,0x85, +0xE7,0x3D,0xBE,0x7D,0x9A,0xFE,0x24,0x45,0x33,0xDC,0x76,0x15,0xED,0x0F,0xA2,0x71, +0x64,0x4C,0x65,0x2E,0x81,0x68,0x45,0xA7,0x02,0x03,0x01,0x00,0x01,0x30,0x0D,0x06, +0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x81,0x81,0x00, +0x10,0x72,0x52,0xA9,0x05,0x14,0x19,0x32,0x08,0x41,0xF0,0xC5,0x6B,0x0A,0xCC,0x7E, +0x0F,0x21,0x19,0xCD,0xE4,0x67,0xDC,0x5F,0xA9,0x1B,0xE6,0xCA,0xE8,0x73,0x9D,0x22, +0xD8,0x98,0x6E,0x73,0x03,0x61,0x91,0xC5,0x7C,0xB0,0x45,0x40,0x6E,0x44,0x9D,0x8D, +0xB0,0xB1,0x96,0x74,0x61,0x2D,0x0D,0xA9,0x45,0xD2,0xA4,0x92,0x2A,0xD6,0x9A,0x75, +0x97,0x6E,0x3F,0x53,0xFD,0x45,0x99,0x60,0x1D,0xA8,0x2B,0x4C,0xF9,0x5E,0xA7,0x09, +0xD8,0x75,0x30,0xD7,0xD2,0x65,0x60,0x3D,0x67,0xD6,0x48,0x55,0x75,0x69,0x3F,0x91, +0xF5,0x48,0x0B,0x47,0x69,0x22,0x69,0x82,0x96,0xBE,0xC9,0xC8,0x38,0x86,0x4A,0x7A, +0x2C,0x73,0x19,0x48,0x69,0x4E,0x6B,0x7C,0x65,0xBF,0x0F,0xFC,0x70,0xCE,0x88,0x90, +}; + + +/* subject:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network */ +/* issuer :/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network */ + + +const unsigned char Verisign_Class_3_Public_Primary_Certification_Authority___G2_certificate[774]={ +0x30,0x82,0x03,0x02,0x30,0x82,0x02,0x6B,0x02,0x10,0x7D,0xD9,0xFE,0x07,0xCF,0xA8, +0x1E,0xB7,0x10,0x79,0x67,0xFB,0xA7,0x89,0x34,0xC6,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x81,0xC1,0x31,0x0B,0x30,0x09, +0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17,0x30,0x15,0x06,0x03,0x55, +0x04,0x0A,0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20,0x49,0x6E, +0x63,0x2E,0x31,0x3C,0x30,0x3A,0x06,0x03,0x55,0x04,0x0B,0x13,0x33,0x43,0x6C,0x61, +0x73,0x73,0x20,0x33,0x20,0x50,0x75,0x62,0x6C,0x69,0x63,0x20,0x50,0x72,0x69,0x6D, +0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F, +0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x20,0x2D,0x20,0x47,0x32, +0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04,0x0B,0x13,0x31,0x28,0x63,0x29,0x20,0x31, +0x39,0x39,0x38,0x20,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20,0x49,0x6E, +0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74,0x68,0x6F,0x72,0x69, +0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79,0x31,0x1F,0x30,0x1D, +0x06,0x03,0x55,0x04,0x0B,0x13,0x16,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x20, +0x54,0x72,0x75,0x73,0x74,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x30,0x1E,0x17, +0x0D,0x39,0x38,0x30,0x35,0x31,0x38,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D, +0x32,0x38,0x30,0x38,0x30,0x31,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x81,0xC1, +0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17,0x30, +0x15,0x06,0x03,0x55,0x04,0x0A,0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E, +0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x3C,0x30,0x3A,0x06,0x03,0x55,0x04,0x0B,0x13, +0x33,0x43,0x6C,0x61,0x73,0x73,0x20,0x33,0x20,0x50,0x75,0x62,0x6C,0x69,0x63,0x20, +0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63, +0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x20, +0x2D,0x20,0x47,0x32,0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04,0x0B,0x13,0x31,0x28, +0x63,0x29,0x20,0x31,0x39,0x39,0x38,0x20,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E, +0x2C,0x20,0x49,0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74, +0x68,0x6F,0x72,0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79, +0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x0B,0x13,0x16,0x56,0x65,0x72,0x69,0x53, +0x69,0x67,0x6E,0x20,0x54,0x72,0x75,0x73,0x74,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72, +0x6B,0x30,0x81,0x9F,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01, +0x01,0x05,0x00,0x03,0x81,0x8D,0x00,0x30,0x81,0x89,0x02,0x81,0x81,0x00,0xCC,0x5E, +0xD1,0x11,0x5D,0x5C,0x69,0xD0,0xAB,0xD3,0xB9,0x6A,0x4C,0x99,0x1F,0x59,0x98,0x30, +0x8E,0x16,0x85,0x20,0x46,0x6D,0x47,0x3F,0xD4,0x85,0x20,0x84,0xE1,0x6D,0xB3,0xF8, +0xA4,0xED,0x0C,0xF1,0x17,0x0F,0x3B,0xF9,0xA7,0xF9,0x25,0xD7,0xC1,0xCF,0x84,0x63, +0xF2,0x7C,0x63,0xCF,0xA2,0x47,0xF2,0xC6,0x5B,0x33,0x8E,0x64,0x40,0x04,0x68,0xC1, +0x80,0xB9,0x64,0x1C,0x45,0x77,0xC7,0xD8,0x6E,0xF5,0x95,0x29,0x3C,0x50,0xE8,0x34, +0xD7,0x78,0x1F,0xA8,0xBA,0x6D,0x43,0x91,0x95,0x8F,0x45,0x57,0x5E,0x7E,0xC5,0xFB, +0xCA,0xA4,0x04,0xEB,0xEA,0x97,0x37,0x54,0x30,0x6F,0xBB,0x01,0x47,0x32,0x33,0xCD, +0xDC,0x57,0x9B,0x64,0x69,0x61,0xF8,0x9B,0x1D,0x1C,0x89,0x4F,0x5C,0x67,0x02,0x03, +0x01,0x00,0x01,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05, +0x05,0x00,0x03,0x81,0x81,0x00,0x51,0x4D,0xCD,0xBE,0x5C,0xCB,0x98,0x19,0x9C,0x15, +0xB2,0x01,0x39,0x78,0x2E,0x4D,0x0F,0x67,0x70,0x70,0x99,0xC6,0x10,0x5A,0x94,0xA4, +0x53,0x4D,0x54,0x6D,0x2B,0xAF,0x0D,0x5D,0x40,0x8B,0x64,0xD3,0xD7,0xEE,0xDE,0x56, +0x61,0x92,0x5F,0xA6,0xC4,0x1D,0x10,0x61,0x36,0xD3,0x2C,0x27,0x3C,0xE8,0x29,0x09, +0xB9,0x11,0x64,0x74,0xCC,0xB5,0x73,0x9F,0x1C,0x48,0xA9,0xBC,0x61,0x01,0xEE,0xE2, +0x17,0xA6,0x0C,0xE3,0x40,0x08,0x3B,0x0E,0xE7,0xEB,0x44,0x73,0x2A,0x9A,0xF1,0x69, +0x92,0xEF,0x71,0x14,0xC3,0x39,0xAC,0x71,0xA7,0x91,0x09,0x6F,0xE4,0x71,0x06,0xB3, +0xBA,0x59,0x57,0x26,0x79,0x00,0xF6,0xF8,0x0D,0xA2,0x33,0x30,0x28,0xD4,0xAA,0x58, +0xA0,0x9D,0x9D,0x69,0x91,0xFD, +}; + + +/* subject:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 1999 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority - G3 */ +/* issuer :/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 1999 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority - G3 */ + + +const unsigned char Verisign_Class_3_Public_Primary_Certification_Authority___G3_certificate[1054]={ +0x30,0x82,0x04,0x1A,0x30,0x82,0x03,0x02,0x02,0x11,0x00,0x9B,0x7E,0x06,0x49,0xA3, +0x3E,0x62,0xB9,0xD5,0xEE,0x90,0x48,0x71,0x29,0xEF,0x57,0x30,0x0D,0x06,0x09,0x2A, +0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x81,0xCA,0x31,0x0B,0x30, +0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17,0x30,0x15,0x06,0x03, +0x55,0x04,0x0A,0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20,0x49, +0x6E,0x63,0x2E,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x0B,0x13,0x16,0x56,0x65, +0x72,0x69,0x53,0x69,0x67,0x6E,0x20,0x54,0x72,0x75,0x73,0x74,0x20,0x4E,0x65,0x74, +0x77,0x6F,0x72,0x6B,0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04,0x0B,0x13,0x31,0x28, +0x63,0x29,0x20,0x31,0x39,0x39,0x39,0x20,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E, +0x2C,0x20,0x49,0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74, +0x68,0x6F,0x72,0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79, +0x31,0x45,0x30,0x43,0x06,0x03,0x55,0x04,0x03,0x13,0x3C,0x56,0x65,0x72,0x69,0x53, +0x69,0x67,0x6E,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x33,0x20,0x50,0x75,0x62,0x6C, +0x69,0x63,0x20,0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69, +0x74,0x79,0x20,0x2D,0x20,0x47,0x33,0x30,0x1E,0x17,0x0D,0x39,0x39,0x31,0x30,0x30, +0x31,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x36,0x30,0x37,0x31,0x36, +0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x81,0xCA,0x31,0x0B,0x30,0x09,0x06,0x03, +0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04,0x0A, +0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20,0x49,0x6E,0x63,0x2E, +0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x0B,0x13,0x16,0x56,0x65,0x72,0x69,0x53, +0x69,0x67,0x6E,0x20,0x54,0x72,0x75,0x73,0x74,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72, +0x6B,0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04,0x0B,0x13,0x31,0x28,0x63,0x29,0x20, +0x31,0x39,0x39,0x39,0x20,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20,0x49, +0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74,0x68,0x6F,0x72, +0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79,0x31,0x45,0x30, +0x43,0x06,0x03,0x55,0x04,0x03,0x13,0x3C,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E, +0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x33,0x20,0x50,0x75,0x62,0x6C,0x69,0x63,0x20, +0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63, +0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x20, +0x2D,0x20,0x47,0x33,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A, +0x02,0x82,0x01,0x01,0x00,0xCB,0xBA,0x9C,0x52,0xFC,0x78,0x1F,0x1A,0x1E,0x6F,0x1B, +0x37,0x73,0xBD,0xF8,0xC9,0x6B,0x94,0x12,0x30,0x4F,0xF0,0x36,0x47,0xF5,0xD0,0x91, +0x0A,0xF5,0x17,0xC8,0xA5,0x61,0xC1,0x16,0x40,0x4D,0xFB,0x8A,0x61,0x90,0xE5,0x76, +0x20,0xC1,0x11,0x06,0x7D,0xAB,0x2C,0x6E,0xA6,0xF5,0x11,0x41,0x8E,0xFA,0x2D,0xAD, +0x2A,0x61,0x59,0xA4,0x67,0x26,0x4C,0xD0,0xE8,0xBC,0x52,0x5B,0x70,0x20,0x04,0x58, +0xD1,0x7A,0xC9,0xA4,0x69,0xBC,0x83,0x17,0x64,0xAD,0x05,0x8B,0xBC,0xD0,0x58,0xCE, +0x8D,0x8C,0xF5,0xEB,0xF0,0x42,0x49,0x0B,0x9D,0x97,0x27,0x67,0x32,0x6E,0xE1,0xAE, +0x93,0x15,0x1C,0x70,0xBC,0x20,0x4D,0x2F,0x18,0xDE,0x92,0x88,0xE8,0x6C,0x85,0x57, +0x11,0x1A,0xE9,0x7E,0xE3,0x26,0x11,0x54,0xA2,0x45,0x96,0x55,0x83,0xCA,0x30,0x89, +0xE8,0xDC,0xD8,0xA3,0xED,0x2A,0x80,0x3F,0x7F,0x79,0x65,0x57,0x3E,0x15,0x20,0x66, +0x08,0x2F,0x95,0x93,0xBF,0xAA,0x47,0x2F,0xA8,0x46,0x97,0xF0,0x12,0xE2,0xFE,0xC2, +0x0A,0x2B,0x51,0xE6,0x76,0xE6,0xB7,0x46,0xB7,0xE2,0x0D,0xA6,0xCC,0xA8,0xC3,0x4C, +0x59,0x55,0x89,0xE6,0xE8,0x53,0x5C,0x1C,0xEA,0x9D,0xF0,0x62,0x16,0x0B,0xA7,0xC9, +0x5F,0x0C,0xF0,0xDE,0xC2,0x76,0xCE,0xAF,0xF7,0x6A,0xF2,0xFA,0x41,0xA6,0xA2,0x33, +0x14,0xC9,0xE5,0x7A,0x63,0xD3,0x9E,0x62,0x37,0xD5,0x85,0x65,0x9E,0x0E,0xE6,0x53, +0x24,0x74,0x1B,0x5E,0x1D,0x12,0x53,0x5B,0xC7,0x2C,0xE7,0x83,0x49,0x3B,0x15,0xAE, +0x8A,0x68,0xB9,0x57,0x97,0x02,0x03,0x01,0x00,0x01,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x11,0x14, +0x96,0xC1,0xAB,0x92,0x08,0xF7,0x3F,0x2F,0xC9,0xB2,0xFE,0xE4,0x5A,0x9F,0x64,0xDE, +0xDB,0x21,0x4F,0x86,0x99,0x34,0x76,0x36,0x57,0xDD,0xD0,0x15,0x2F,0xC5,0xAD,0x7F, +0x15,0x1F,0x37,0x62,0x73,0x3E,0xD4,0xE7,0x5F,0xCE,0x17,0x03,0xDB,0x35,0xFA,0x2B, +0xDB,0xAE,0x60,0x09,0x5F,0x1E,0x5F,0x8F,0x6E,0xBB,0x0B,0x3D,0xEA,0x5A,0x13,0x1E, +0x0C,0x60,0x6F,0xB5,0xC0,0xB5,0x23,0x22,0x2E,0x07,0x0B,0xCB,0xA9,0x74,0xCB,0x47, +0xBB,0x1D,0xC1,0xD7,0xA5,0x6B,0xCC,0x2F,0xD2,0x42,0xFD,0x49,0xDD,0xA7,0x89,0xCF, +0x53,0xBA,0xDA,0x00,0x5A,0x28,0xBF,0x82,0xDF,0xF8,0xBA,0x13,0x1D,0x50,0x86,0x82, +0xFD,0x8E,0x30,0x8F,0x29,0x46,0xB0,0x1E,0x3D,0x35,0xDA,0x38,0x62,0x16,0x18,0x4A, +0xAD,0xE6,0xB6,0x51,0x6C,0xDE,0xAF,0x62,0xEB,0x01,0xD0,0x1E,0x24,0xFE,0x7A,0x8F, +0x12,0x1A,0x12,0x68,0xB8,0xFB,0x66,0x99,0x14,0x14,0x45,0x5C,0xAE,0xE7,0xAE,0x69, +0x17,0x81,0x2B,0x5A,0x37,0xC9,0x5E,0x2A,0xF4,0xC6,0xE2,0xA1,0x5C,0x54,0x9B,0xA6, +0x54,0x00,0xCF,0xF0,0xF1,0xC1,0xC7,0x98,0x30,0x1A,0x3B,0x36,0x16,0xDB,0xA3,0x6E, +0xEA,0xFD,0xAD,0xB2,0xC2,0xDA,0xEF,0x02,0x47,0x13,0x8A,0xC0,0xF1,0xB3,0x31,0xAD, +0x4F,0x1C,0xE1,0x4F,0x9C,0xAF,0x0F,0x0C,0x9D,0xF7,0x78,0x0D,0xD8,0xF4,0x35,0x56, +0x80,0xDA,0xB7,0x6D,0x17,0x8F,0x9D,0x1E,0x81,0x64,0xE1,0xFE,0xC5,0x45,0xBA,0xAD, +0x6B,0xB9,0x0A,0x7A,0x4E,0x4F,0x4B,0x84,0xEE,0x4B,0xF1,0x7D,0xDD,0x11, +}; + + +/* subject:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2007 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority - G4 */ +/* issuer :/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2007 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority - G4 */ + + +const unsigned char VeriSign_Class_3_Public_Primary_Certification_Authority___G4_certificate[904]={ +0x30,0x82,0x03,0x84,0x30,0x82,0x03,0x0A,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x2F, +0x80,0xFE,0x23,0x8C,0x0E,0x22,0x0F,0x48,0x67,0x12,0x28,0x91,0x87,0xAC,0xB3,0x30, +0x0A,0x06,0x08,0x2A,0x86,0x48,0xCE,0x3D,0x04,0x03,0x03,0x30,0x81,0xCA,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17,0x30,0x15,0x06, +0x03,0x55,0x04,0x0A,0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20, +0x49,0x6E,0x63,0x2E,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x0B,0x13,0x16,0x56, +0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x20,0x54,0x72,0x75,0x73,0x74,0x20,0x4E,0x65, +0x74,0x77,0x6F,0x72,0x6B,0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04,0x0B,0x13,0x31, +0x28,0x63,0x29,0x20,0x32,0x30,0x30,0x37,0x20,0x56,0x65,0x72,0x69,0x53,0x69,0x67, +0x6E,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75, +0x74,0x68,0x6F,0x72,0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C, +0x79,0x31,0x45,0x30,0x43,0x06,0x03,0x55,0x04,0x03,0x13,0x3C,0x56,0x65,0x72,0x69, +0x53,0x69,0x67,0x6E,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x33,0x20,0x50,0x75,0x62, +0x6C,0x69,0x63,0x20,0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74, +0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72, +0x69,0x74,0x79,0x20,0x2D,0x20,0x47,0x34,0x30,0x1E,0x17,0x0D,0x30,0x37,0x31,0x31, +0x30,0x35,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x38,0x30,0x31,0x31, +0x38,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x81,0xCA,0x31,0x0B,0x30,0x09,0x06, +0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04, +0x0A,0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20,0x49,0x6E,0x63, +0x2E,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x0B,0x13,0x16,0x56,0x65,0x72,0x69, +0x53,0x69,0x67,0x6E,0x20,0x54,0x72,0x75,0x73,0x74,0x20,0x4E,0x65,0x74,0x77,0x6F, +0x72,0x6B,0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04,0x0B,0x13,0x31,0x28,0x63,0x29, +0x20,0x32,0x30,0x30,0x37,0x20,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20, +0x49,0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74,0x68,0x6F, +0x72,0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79,0x31,0x45, +0x30,0x43,0x06,0x03,0x55,0x04,0x03,0x13,0x3C,0x56,0x65,0x72,0x69,0x53,0x69,0x67, +0x6E,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x33,0x20,0x50,0x75,0x62,0x6C,0x69,0x63, +0x20,0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69, +0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79, +0x20,0x2D,0x20,0x47,0x34,0x30,0x76,0x30,0x10,0x06,0x07,0x2A,0x86,0x48,0xCE,0x3D, +0x02,0x01,0x06,0x05,0x2B,0x81,0x04,0x00,0x22,0x03,0x62,0x00,0x04,0xA7,0x56,0x7A, +0x7C,0x52,0xDA,0x64,0x9B,0x0E,0x2D,0x5C,0xD8,0x5E,0xAC,0x92,0x3D,0xFE,0x01,0xE6, +0x19,0x4A,0x3D,0x14,0x03,0x4B,0xFA,0x60,0x27,0x20,0xD9,0x83,0x89,0x69,0xFA,0x54, +0xC6,0x9A,0x18,0x5E,0x55,0x2A,0x64,0xDE,0x06,0xF6,0x8D,0x4A,0x3B,0xAD,0x10,0x3C, +0x65,0x3D,0x90,0x88,0x04,0x89,0xE0,0x30,0x61,0xB3,0xAE,0x5D,0x01,0xA7,0x7B,0xDE, +0x7C,0xB2,0xBE,0xCA,0x65,0x61,0x00,0x86,0xAE,0xDA,0x8F,0x7B,0xD0,0x89,0xAD,0x4D, +0x1D,0x59,0x9A,0x41,0xB1,0xBC,0x47,0x80,0xDC,0x9E,0x62,0xC3,0xF9,0xA3,0x81,0xB2, +0x30,0x81,0xAF,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05,0x30, +0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04, +0x03,0x02,0x01,0x06,0x30,0x6D,0x06,0x08,0x2B,0x06,0x01,0x05,0x05,0x07,0x01,0x0C, +0x04,0x61,0x30,0x5F,0xA1,0x5D,0xA0,0x5B,0x30,0x59,0x30,0x57,0x30,0x55,0x16,0x09, +0x69,0x6D,0x61,0x67,0x65,0x2F,0x67,0x69,0x66,0x30,0x21,0x30,0x1F,0x30,0x07,0x06, +0x05,0x2B,0x0E,0x03,0x02,0x1A,0x04,0x14,0x8F,0xE5,0xD3,0x1A,0x86,0xAC,0x8D,0x8E, +0x6B,0xC3,0xCF,0x80,0x6A,0xD4,0x48,0x18,0x2C,0x7B,0x19,0x2E,0x30,0x25,0x16,0x23, +0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x6C,0x6F,0x67,0x6F,0x2E,0x76,0x65,0x72,0x69, +0x73,0x69,0x67,0x6E,0x2E,0x63,0x6F,0x6D,0x2F,0x76,0x73,0x6C,0x6F,0x67,0x6F,0x2E, +0x67,0x69,0x66,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0xB3,0x16, +0x91,0xFD,0xEE,0xA6,0x6E,0xE4,0xB5,0x2E,0x49,0x8F,0x87,0x78,0x81,0x80,0xEC,0xE5, +0xB1,0xB5,0x30,0x0A,0x06,0x08,0x2A,0x86,0x48,0xCE,0x3D,0x04,0x03,0x03,0x03,0x68, +0x00,0x30,0x65,0x02,0x30,0x66,0x21,0x0C,0x18,0x26,0x60,0x5A,0x38,0x7B,0x56,0x42, +0xE0,0xA7,0xFC,0x36,0x84,0x51,0x91,0x20,0x2C,0x76,0x4D,0x43,0x3D,0xC4,0x1D,0x84, +0x23,0xD0,0xAC,0xD6,0x7C,0x35,0x06,0xCE,0xCD,0x69,0xBD,0x90,0x0D,0xDB,0x6C,0x48, +0x42,0x1D,0x0E,0xAA,0x42,0x02,0x31,0x00,0x9C,0x3D,0x48,0x39,0x23,0x39,0x58,0x1A, +0x15,0x12,0x59,0x6A,0x9E,0xEF,0xD5,0x59,0xB2,0x1D,0x52,0x2C,0x99,0x71,0xCD,0xC7, +0x29,0xDF,0x1B,0x2A,0x61,0x7B,0x71,0xD1,0xDE,0xF3,0xC0,0xE5,0x0D,0x3A,0x4A,0xAA, +0x2D,0xA7,0xD8,0x86,0x2A,0xDD,0x2E,0x10, +}; + + +/* subject:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2006 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority - G5 */ +/* issuer :/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2006 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority - G5 */ + + +const unsigned char VeriSign_Class_3_Public_Primary_Certification_Authority___G5_certificate[1239]={ +0x30,0x82,0x04,0xD3,0x30,0x82,0x03,0xBB,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x18, +0xDA,0xD1,0x9E,0x26,0x7D,0xE8,0xBB,0x4A,0x21,0x58,0xCD,0xCC,0x6B,0x3B,0x4A,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x81, +0xCA,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17, +0x30,0x15,0x06,0x03,0x55,0x04,0x0A,0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67, +0x6E,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x0B, +0x13,0x16,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x20,0x54,0x72,0x75,0x73,0x74, +0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04, +0x0B,0x13,0x31,0x28,0x63,0x29,0x20,0x32,0x30,0x30,0x36,0x20,0x56,0x65,0x72,0x69, +0x53,0x69,0x67,0x6E,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72, +0x20,0x61,0x75,0x74,0x68,0x6F,0x72,0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20, +0x6F,0x6E,0x6C,0x79,0x31,0x45,0x30,0x43,0x06,0x03,0x55,0x04,0x03,0x13,0x3C,0x56, +0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x33,0x20, +0x50,0x75,0x62,0x6C,0x69,0x63,0x20,0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43, +0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74, +0x68,0x6F,0x72,0x69,0x74,0x79,0x20,0x2D,0x20,0x47,0x35,0x30,0x1E,0x17,0x0D,0x30, +0x36,0x31,0x31,0x30,0x38,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x36, +0x30,0x37,0x31,0x36,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x81,0xCA,0x31,0x0B, +0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17,0x30,0x15,0x06, +0x03,0x55,0x04,0x0A,0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20, +0x49,0x6E,0x63,0x2E,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x0B,0x13,0x16,0x56, +0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x20,0x54,0x72,0x75,0x73,0x74,0x20,0x4E,0x65, +0x74,0x77,0x6F,0x72,0x6B,0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04,0x0B,0x13,0x31, +0x28,0x63,0x29,0x20,0x32,0x30,0x30,0x36,0x20,0x56,0x65,0x72,0x69,0x53,0x69,0x67, +0x6E,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75, +0x74,0x68,0x6F,0x72,0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C, +0x79,0x31,0x45,0x30,0x43,0x06,0x03,0x55,0x04,0x03,0x13,0x3C,0x56,0x65,0x72,0x69, +0x53,0x69,0x67,0x6E,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x33,0x20,0x50,0x75,0x62, +0x6C,0x69,0x63,0x20,0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74, +0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72, +0x69,0x74,0x79,0x20,0x2D,0x20,0x47,0x35,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09, +0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00, +0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xAF,0x24,0x08,0x08,0x29,0x7A,0x35, +0x9E,0x60,0x0C,0xAA,0xE7,0x4B,0x3B,0x4E,0xDC,0x7C,0xBC,0x3C,0x45,0x1C,0xBB,0x2B, +0xE0,0xFE,0x29,0x02,0xF9,0x57,0x08,0xA3,0x64,0x85,0x15,0x27,0xF5,0xF1,0xAD,0xC8, +0x31,0x89,0x5D,0x22,0xE8,0x2A,0xAA,0xA6,0x42,0xB3,0x8F,0xF8,0xB9,0x55,0xB7,0xB1, +0xB7,0x4B,0xB3,0xFE,0x8F,0x7E,0x07,0x57,0xEC,0xEF,0x43,0xDB,0x66,0x62,0x15,0x61, +0xCF,0x60,0x0D,0xA4,0xD8,0xDE,0xF8,0xE0,0xC3,0x62,0x08,0x3D,0x54,0x13,0xEB,0x49, +0xCA,0x59,0x54,0x85,0x26,0xE5,0x2B,0x8F,0x1B,0x9F,0xEB,0xF5,0xA1,0x91,0xC2,0x33, +0x49,0xD8,0x43,0x63,0x6A,0x52,0x4B,0xD2,0x8F,0xE8,0x70,0x51,0x4D,0xD1,0x89,0x69, +0x7B,0xC7,0x70,0xF6,0xB3,0xDC,0x12,0x74,0xDB,0x7B,0x5D,0x4B,0x56,0xD3,0x96,0xBF, +0x15,0x77,0xA1,0xB0,0xF4,0xA2,0x25,0xF2,0xAF,0x1C,0x92,0x67,0x18,0xE5,0xF4,0x06, +0x04,0xEF,0x90,0xB9,0xE4,0x00,0xE4,0xDD,0x3A,0xB5,0x19,0xFF,0x02,0xBA,0xF4,0x3C, +0xEE,0xE0,0x8B,0xEB,0x37,0x8B,0xEC,0xF4,0xD7,0xAC,0xF2,0xF6,0xF0,0x3D,0xAF,0xDD, +0x75,0x91,0x33,0x19,0x1D,0x1C,0x40,0xCB,0x74,0x24,0x19,0x21,0x93,0xD9,0x14,0xFE, +0xAC,0x2A,0x52,0xC7,0x8F,0xD5,0x04,0x49,0xE4,0x8D,0x63,0x47,0x88,0x3C,0x69,0x83, +0xCB,0xFE,0x47,0xBD,0x2B,0x7E,0x4F,0xC5,0x95,0xAE,0x0E,0x9D,0xD4,0xD1,0x43,0xC0, +0x67,0x73,0xE3,0x14,0x08,0x7E,0xE5,0x3F,0x9F,0x73,0xB8,0x33,0x0A,0xCF,0x5D,0x3F, +0x34,0x87,0x96,0x8A,0xEE,0x53,0xE8,0x25,0x15,0x02,0x03,0x01,0x00,0x01,0xA3,0x81, +0xB2,0x30,0x81,0xAF,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05, +0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04, +0x04,0x03,0x02,0x01,0x06,0x30,0x6D,0x06,0x08,0x2B,0x06,0x01,0x05,0x05,0x07,0x01, +0x0C,0x04,0x61,0x30,0x5F,0xA1,0x5D,0xA0,0x5B,0x30,0x59,0x30,0x57,0x30,0x55,0x16, +0x09,0x69,0x6D,0x61,0x67,0x65,0x2F,0x67,0x69,0x66,0x30,0x21,0x30,0x1F,0x30,0x07, +0x06,0x05,0x2B,0x0E,0x03,0x02,0x1A,0x04,0x14,0x8F,0xE5,0xD3,0x1A,0x86,0xAC,0x8D, +0x8E,0x6B,0xC3,0xCF,0x80,0x6A,0xD4,0x48,0x18,0x2C,0x7B,0x19,0x2E,0x30,0x25,0x16, +0x23,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x6C,0x6F,0x67,0x6F,0x2E,0x76,0x65,0x72, +0x69,0x73,0x69,0x67,0x6E,0x2E,0x63,0x6F,0x6D,0x2F,0x76,0x73,0x6C,0x6F,0x67,0x6F, +0x2E,0x67,0x69,0x66,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x7F, +0xD3,0x65,0xA7,0xC2,0xDD,0xEC,0xBB,0xF0,0x30,0x09,0xF3,0x43,0x39,0xFA,0x02,0xAF, +0x33,0x31,0x33,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05, +0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x93,0x24,0x4A,0x30,0x5F,0x62,0xCF,0xD8,0x1A, +0x98,0x2F,0x3D,0xEA,0xDC,0x99,0x2D,0xBD,0x77,0xF6,0xA5,0x79,0x22,0x38,0xEC,0xC4, +0xA7,0xA0,0x78,0x12,0xAD,0x62,0x0E,0x45,0x70,0x64,0xC5,0xE7,0x97,0x66,0x2D,0x98, +0x09,0x7E,0x5F,0xAF,0xD6,0xCC,0x28,0x65,0xF2,0x01,0xAA,0x08,0x1A,0x47,0xDE,0xF9, +0xF9,0x7C,0x92,0x5A,0x08,0x69,0x20,0x0D,0xD9,0x3E,0x6D,0x6E,0x3C,0x0D,0x6E,0xD8, +0xE6,0x06,0x91,0x40,0x18,0xB9,0xF8,0xC1,0xED,0xDF,0xDB,0x41,0xAA,0xE0,0x96,0x20, +0xC9,0xCD,0x64,0x15,0x38,0x81,0xC9,0x94,0xEE,0xA2,0x84,0x29,0x0B,0x13,0x6F,0x8E, +0xDB,0x0C,0xDD,0x25,0x02,0xDB,0xA4,0x8B,0x19,0x44,0xD2,0x41,0x7A,0x05,0x69,0x4A, +0x58,0x4F,0x60,0xCA,0x7E,0x82,0x6A,0x0B,0x02,0xAA,0x25,0x17,0x39,0xB5,0xDB,0x7F, +0xE7,0x84,0x65,0x2A,0x95,0x8A,0xBD,0x86,0xDE,0x5E,0x81,0x16,0x83,0x2D,0x10,0xCC, +0xDE,0xFD,0xA8,0x82,0x2A,0x6D,0x28,0x1F,0x0D,0x0B,0xC4,0xE5,0xE7,0x1A,0x26,0x19, +0xE1,0xF4,0x11,0x6F,0x10,0xB5,0x95,0xFC,0xE7,0x42,0x05,0x32,0xDB,0xCE,0x9D,0x51, +0x5E,0x28,0xB6,0x9E,0x85,0xD3,0x5B,0xEF,0xA5,0x7D,0x45,0x40,0x72,0x8E,0xB7,0x0E, +0x6B,0x0E,0x06,0xFB,0x33,0x35,0x48,0x71,0xB8,0x9D,0x27,0x8B,0xC4,0x65,0x5F,0x0D, +0x86,0x76,0x9C,0x44,0x7A,0xF6,0x95,0x5C,0xF6,0x5D,0x32,0x08,0x33,0xA4,0x54,0xB6, +0x18,0x3F,0x68,0x5C,0xF2,0x42,0x4A,0x85,0x38,0x54,0x83,0x5F,0xD1,0xE8,0x2C,0xF2, +0xAC,0x11,0xD6,0xA8,0xED,0x63,0x6A, +}; + + +/* subject:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 1999 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 4 Public Primary Certification Authority - G3 */ +/* issuer :/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 1999 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 4 Public Primary Certification Authority - G3 */ + + +const unsigned char Verisign_Class_4_Public_Primary_Certification_Authority___G3_certificate[1054]={ +0x30,0x82,0x04,0x1A,0x30,0x82,0x03,0x02,0x02,0x11,0x00,0xEC,0xA0,0xA7,0x8B,0x6E, +0x75,0x6A,0x01,0xCF,0xC4,0x7C,0xCC,0x2F,0x94,0x5E,0xD7,0x30,0x0D,0x06,0x09,0x2A, +0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x81,0xCA,0x31,0x0B,0x30, +0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17,0x30,0x15,0x06,0x03, +0x55,0x04,0x0A,0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20,0x49, +0x6E,0x63,0x2E,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x0B,0x13,0x16,0x56,0x65, +0x72,0x69,0x53,0x69,0x67,0x6E,0x20,0x54,0x72,0x75,0x73,0x74,0x20,0x4E,0x65,0x74, +0x77,0x6F,0x72,0x6B,0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04,0x0B,0x13,0x31,0x28, +0x63,0x29,0x20,0x31,0x39,0x39,0x39,0x20,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E, +0x2C,0x20,0x49,0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74, +0x68,0x6F,0x72,0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79, +0x31,0x45,0x30,0x43,0x06,0x03,0x55,0x04,0x03,0x13,0x3C,0x56,0x65,0x72,0x69,0x53, +0x69,0x67,0x6E,0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x34,0x20,0x50,0x75,0x62,0x6C, +0x69,0x63,0x20,0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69, +0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69, +0x74,0x79,0x20,0x2D,0x20,0x47,0x33,0x30,0x1E,0x17,0x0D,0x39,0x39,0x31,0x30,0x30, +0x31,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x36,0x30,0x37,0x31,0x36, +0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x81,0xCA,0x31,0x0B,0x30,0x09,0x06,0x03, +0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17,0x30,0x15,0x06,0x03,0x55,0x04,0x0A, +0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20,0x49,0x6E,0x63,0x2E, +0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x0B,0x13,0x16,0x56,0x65,0x72,0x69,0x53, +0x69,0x67,0x6E,0x20,0x54,0x72,0x75,0x73,0x74,0x20,0x4E,0x65,0x74,0x77,0x6F,0x72, +0x6B,0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04,0x0B,0x13,0x31,0x28,0x63,0x29,0x20, +0x31,0x39,0x39,0x39,0x20,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x2C,0x20,0x49, +0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72,0x20,0x61,0x75,0x74,0x68,0x6F,0x72, +0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20,0x6F,0x6E,0x6C,0x79,0x31,0x45,0x30, +0x43,0x06,0x03,0x55,0x04,0x03,0x13,0x3C,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E, +0x20,0x43,0x6C,0x61,0x73,0x73,0x20,0x34,0x20,0x50,0x75,0x62,0x6C,0x69,0x63,0x20, +0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63, +0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x20, +0x2D,0x20,0x47,0x33,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86, +0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A, +0x02,0x82,0x01,0x01,0x00,0xAD,0xCB,0xA5,0x11,0x69,0xC6,0x59,0xAB,0xF1,0x8F,0xB5, +0x19,0x0F,0x56,0xCE,0xCC,0xB5,0x1F,0x20,0xE4,0x9E,0x26,0x25,0x4B,0xE0,0x73,0x65, +0x89,0x59,0xDE,0xD0,0x83,0xE4,0xF5,0x0F,0xB5,0xBB,0xAD,0xF1,0x7C,0xE8,0x21,0xFC, +0xE4,0xE8,0x0C,0xEE,0x7C,0x45,0x22,0x19,0x76,0x92,0xB4,0x13,0xB7,0x20,0x5B,0x09, +0xFA,0x61,0xAE,0xA8,0xF2,0xA5,0x8D,0x85,0xC2,0x2A,0xD6,0xDE,0x66,0x36,0xD2,0x9B, +0x02,0xF4,0xA8,0x92,0x60,0x7C,0x9C,0x69,0xB4,0x8F,0x24,0x1E,0xD0,0x86,0x52,0xF6, +0x32,0x9C,0x41,0x58,0x1E,0x22,0xBD,0xCD,0x45,0x62,0x95,0x08,0x6E,0xD0,0x66,0xDD, +0x53,0xA2,0xCC,0xF0,0x10,0xDC,0x54,0x73,0x8B,0x04,0xA1,0x46,0x33,0x33,0x5C,0x17, +0x40,0xB9,0x9E,0x4D,0xD3,0xF3,0xBE,0x55,0x83,0xE8,0xB1,0x89,0x8E,0x5A,0x7C,0x9A, +0x96,0x22,0x90,0x3B,0x88,0x25,0xF2,0xD2,0x53,0x88,0x02,0x0C,0x0B,0x78,0xF2,0xE6, +0x37,0x17,0x4B,0x30,0x46,0x07,0xE4,0x80,0x6D,0xA6,0xD8,0x96,0x2E,0xE8,0x2C,0xF8, +0x11,0xB3,0x38,0x0D,0x66,0xA6,0x9B,0xEA,0xC9,0x23,0x5B,0xDB,0x8E,0xE2,0xF3,0x13, +0x8E,0x1A,0x59,0x2D,0xAA,0x02,0xF0,0xEC,0xA4,0x87,0x66,0xDC,0xC1,0x3F,0xF5,0xD8, +0xB9,0xF4,0xEC,0x82,0xC6,0xD2,0x3D,0x95,0x1D,0xE5,0xC0,0x4F,0x84,0xC9,0xD9,0xA3, +0x44,0x28,0x06,0x6A,0xD7,0x45,0xAC,0xF0,0x6B,0x6A,0xEF,0x4E,0x5F,0xF8,0x11,0x82, +0x1E,0x38,0x63,0x34,0x66,0x50,0xD4,0x3E,0x93,0x73,0xFA,0x30,0xC3,0x66,0xAD,0xFF, +0x93,0x2D,0x97,0xEF,0x03,0x02,0x03,0x01,0x00,0x01,0x30,0x0D,0x06,0x09,0x2A,0x86, +0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x8F,0xFA, +0x25,0x6B,0x4F,0x5B,0xE4,0xA4,0x4E,0x27,0x55,0xAB,0x22,0x15,0x59,0x3C,0xCA,0xB5, +0x0A,0xD4,0x4A,0xDB,0xAB,0xDD,0xA1,0x5F,0x53,0xC5,0xA0,0x57,0x39,0xC2,0xCE,0x47, +0x2B,0xBE,0x3A,0xC8,0x56,0xBF,0xC2,0xD9,0x27,0x10,0x3A,0xB1,0x05,0x3C,0xC0,0x77, +0x31,0xBB,0x3A,0xD3,0x05,0x7B,0x6D,0x9A,0x1C,0x30,0x8C,0x80,0xCB,0x93,0x93,0x2A, +0x83,0xAB,0x05,0x51,0x82,0x02,0x00,0x11,0x67,0x6B,0xF3,0x88,0x61,0x47,0x5F,0x03, +0x93,0xD5,0x5B,0x0D,0xE0,0xF1,0xD4,0xA1,0x32,0x35,0x85,0xB2,0x3A,0xDB,0xB0,0x82, +0xAB,0xD1,0xCB,0x0A,0xBC,0x4F,0x8C,0x5B,0xC5,0x4B,0x00,0x3B,0x1F,0x2A,0x82,0xA6, +0x7E,0x36,0x85,0xDC,0x7E,0x3C,0x67,0x00,0xB5,0xE4,0x3B,0x52,0xE0,0xA8,0xEB,0x5D, +0x15,0xF9,0xC6,0x6D,0xF0,0xAD,0x1D,0x0E,0x85,0xB7,0xA9,0x9A,0x73,0x14,0x5A,0x5B, +0x8F,0x41,0x28,0xC0,0xD5,0xE8,0x2D,0x4D,0xA4,0x5E,0xCD,0xAA,0xD9,0xED,0xCE,0xDC, +0xD8,0xD5,0x3C,0x42,0x1D,0x17,0xC1,0x12,0x5D,0x45,0x38,0xC3,0x38,0xF3,0xFC,0x85, +0x2E,0x83,0x46,0x48,0xB2,0xD7,0x20,0x5F,0x92,0x36,0x8F,0xE7,0x79,0x0F,0x98,0x5E, +0x99,0xE8,0xF0,0xD0,0xA4,0xBB,0xF5,0x53,0xBD,0x2A,0xCE,0x59,0xB0,0xAF,0x6E,0x7F, +0x6C,0xBB,0xD2,0x1E,0x00,0xB0,0x21,0xED,0xF8,0x41,0x62,0x82,0xB9,0xD8,0xB2,0xC4, +0xBB,0x46,0x50,0xF3,0x31,0xC5,0x8F,0x01,0xA8,0x74,0xEB,0xF5,0x78,0x27,0xDA,0xE7, +0xF7,0x66,0x43,0xF3,0x9E,0x83,0x3E,0x20,0xAA,0xC3,0x35,0x60,0x91,0xCE, +}; + + +/* subject:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2008 VeriSign, Inc. - For authorized use only/CN=VeriSign Universal Root Certification Authority */ +/* issuer :/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2008 VeriSign, Inc. - For authorized use only/CN=VeriSign Universal Root Certification Authority */ + + +const unsigned char VeriSign_Universal_Root_Certification_Authority_certificate[1213]={ +0x30,0x82,0x04,0xB9,0x30,0x82,0x03,0xA1,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x40, +0x1A,0xC4,0x64,0x21,0xB3,0x13,0x21,0x03,0x0E,0xBB,0xE4,0x12,0x1A,0xC5,0x1D,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x30,0x81, +0xBD,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17, +0x30,0x15,0x06,0x03,0x55,0x04,0x0A,0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67, +0x6E,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x0B, +0x13,0x16,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x20,0x54,0x72,0x75,0x73,0x74, +0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04, +0x0B,0x13,0x31,0x28,0x63,0x29,0x20,0x32,0x30,0x30,0x38,0x20,0x56,0x65,0x72,0x69, +0x53,0x69,0x67,0x6E,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72, +0x20,0x61,0x75,0x74,0x68,0x6F,0x72,0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20, +0x6F,0x6E,0x6C,0x79,0x31,0x38,0x30,0x36,0x06,0x03,0x55,0x04,0x03,0x13,0x2F,0x56, +0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x20,0x55,0x6E,0x69,0x76,0x65,0x72,0x73,0x61, +0x6C,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61, +0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x1E, +0x17,0x0D,0x30,0x38,0x30,0x34,0x30,0x32,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17, +0x0D,0x33,0x37,0x31,0x32,0x30,0x31,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x81, +0xBD,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x17, +0x30,0x15,0x06,0x03,0x55,0x04,0x0A,0x13,0x0E,0x56,0x65,0x72,0x69,0x53,0x69,0x67, +0x6E,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x31,0x1F,0x30,0x1D,0x06,0x03,0x55,0x04,0x0B, +0x13,0x16,0x56,0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x20,0x54,0x72,0x75,0x73,0x74, +0x20,0x4E,0x65,0x74,0x77,0x6F,0x72,0x6B,0x31,0x3A,0x30,0x38,0x06,0x03,0x55,0x04, +0x0B,0x13,0x31,0x28,0x63,0x29,0x20,0x32,0x30,0x30,0x38,0x20,0x56,0x65,0x72,0x69, +0x53,0x69,0x67,0x6E,0x2C,0x20,0x49,0x6E,0x63,0x2E,0x20,0x2D,0x20,0x46,0x6F,0x72, +0x20,0x61,0x75,0x74,0x68,0x6F,0x72,0x69,0x7A,0x65,0x64,0x20,0x75,0x73,0x65,0x20, +0x6F,0x6E,0x6C,0x79,0x31,0x38,0x30,0x36,0x06,0x03,0x55,0x04,0x03,0x13,0x2F,0x56, +0x65,0x72,0x69,0x53,0x69,0x67,0x6E,0x20,0x55,0x6E,0x69,0x76,0x65,0x72,0x73,0x61, +0x6C,0x20,0x52,0x6F,0x6F,0x74,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61, +0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x82, +0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05, +0x00,0x03,0x82,0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xC7, +0x61,0x37,0x5E,0xB1,0x01,0x34,0xDB,0x62,0xD7,0x15,0x9B,0xFF,0x58,0x5A,0x8C,0x23, +0x23,0xD6,0x60,0x8E,0x91,0xD7,0x90,0x98,0x83,0x7A,0xE6,0x58,0x19,0x38,0x8C,0xC5, +0xF6,0xE5,0x64,0x85,0xB4,0xA2,0x71,0xFB,0xED,0xBD,0xB9,0xDA,0xCD,0x4D,0x00,0xB4, +0xC8,0x2D,0x73,0xA5,0xC7,0x69,0x71,0x95,0x1F,0x39,0x3C,0xB2,0x44,0x07,0x9C,0xE8, +0x0E,0xFA,0x4D,0x4A,0xC4,0x21,0xDF,0x29,0x61,0x8F,0x32,0x22,0x61,0x82,0xC5,0x87, +0x1F,0x6E,0x8C,0x7C,0x5F,0x16,0x20,0x51,0x44,0xD1,0x70,0x4F,0x57,0xEA,0xE3,0x1C, +0xE3,0xCC,0x79,0xEE,0x58,0xD8,0x0E,0xC2,0xB3,0x45,0x93,0xC0,0x2C,0xE7,0x9A,0x17, +0x2B,0x7B,0x00,0x37,0x7A,0x41,0x33,0x78,0xE1,0x33,0xE2,0xF3,0x10,0x1A,0x7F,0x87, +0x2C,0xBE,0xF6,0xF5,0xF7,0x42,0xE2,0xE5,0xBF,0x87,0x62,0x89,0x5F,0x00,0x4B,0xDF, +0xC5,0xDD,0xE4,0x75,0x44,0x32,0x41,0x3A,0x1E,0x71,0x6E,0x69,0xCB,0x0B,0x75,0x46, +0x08,0xD1,0xCA,0xD2,0x2B,0x95,0xD0,0xCF,0xFB,0xB9,0x40,0x6B,0x64,0x8C,0x57,0x4D, +0xFC,0x13,0x11,0x79,0x84,0xED,0x5E,0x54,0xF6,0x34,0x9F,0x08,0x01,0xF3,0x10,0x25, +0x06,0x17,0x4A,0xDA,0xF1,0x1D,0x7A,0x66,0x6B,0x98,0x60,0x66,0xA4,0xD9,0xEF,0xD2, +0x2E,0x82,0xF1,0xF0,0xEF,0x09,0xEA,0x44,0xC9,0x15,0x6A,0xE2,0x03,0x6E,0x33,0xD3, +0xAC,0x9F,0x55,0x00,0xC7,0xF6,0x08,0x6A,0x94,0xB9,0x5F,0xDC,0xE0,0x33,0xF1,0x84, +0x60,0xF9,0x5B,0x27,0x11,0xB4,0xFC,0x16,0xF2,0xBB,0x56,0x6A,0x80,0x25,0x8D,0x02, +0x03,0x01,0x00,0x01,0xA3,0x81,0xB2,0x30,0x81,0xAF,0x30,0x0F,0x06,0x03,0x55,0x1D, +0x13,0x01,0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03,0x55, +0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x6D,0x06,0x08,0x2B, +0x06,0x01,0x05,0x05,0x07,0x01,0x0C,0x04,0x61,0x30,0x5F,0xA1,0x5D,0xA0,0x5B,0x30, +0x59,0x30,0x57,0x30,0x55,0x16,0x09,0x69,0x6D,0x61,0x67,0x65,0x2F,0x67,0x69,0x66, +0x30,0x21,0x30,0x1F,0x30,0x07,0x06,0x05,0x2B,0x0E,0x03,0x02,0x1A,0x04,0x14,0x8F, +0xE5,0xD3,0x1A,0x86,0xAC,0x8D,0x8E,0x6B,0xC3,0xCF,0x80,0x6A,0xD4,0x48,0x18,0x2C, +0x7B,0x19,0x2E,0x30,0x25,0x16,0x23,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x6C,0x6F, +0x67,0x6F,0x2E,0x76,0x65,0x72,0x69,0x73,0x69,0x67,0x6E,0x2E,0x63,0x6F,0x6D,0x2F, +0x76,0x73,0x6C,0x6F,0x67,0x6F,0x2E,0x67,0x69,0x66,0x30,0x1D,0x06,0x03,0x55,0x1D, +0x0E,0x04,0x16,0x04,0x14,0xB6,0x77,0xFA,0x69,0x48,0x47,0x9F,0x53,0x12,0xD5,0xC2, +0xEA,0x07,0x32,0x76,0x07,0xD1,0x97,0x07,0x19,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48, +0x86,0xF7,0x0D,0x01,0x01,0x0B,0x05,0x00,0x03,0x82,0x01,0x01,0x00,0x4A,0xF8,0xF8, +0xB0,0x03,0xE6,0x2C,0x67,0x7B,0xE4,0x94,0x77,0x63,0xCC,0x6E,0x4C,0xF9,0x7D,0x0E, +0x0D,0xDC,0xC8,0xB9,0x35,0xB9,0x70,0x4F,0x63,0xFA,0x24,0xFA,0x6C,0x83,0x8C,0x47, +0x9D,0x3B,0x63,0xF3,0x9A,0xF9,0x76,0x32,0x95,0x91,0xB1,0x77,0xBC,0xAC,0x9A,0xBE, +0xB1,0xE4,0x31,0x21,0xC6,0x81,0x95,0x56,0x5A,0x0E,0xB1,0xC2,0xD4,0xB1,0xA6,0x59, +0xAC,0xF1,0x63,0xCB,0xB8,0x4C,0x1D,0x59,0x90,0x4A,0xEF,0x90,0x16,0x28,0x1F,0x5A, +0xAE,0x10,0xFB,0x81,0x50,0x38,0x0C,0x6C,0xCC,0xF1,0x3D,0xC3,0xF5,0x63,0xE3,0xB3, +0xE3,0x21,0xC9,0x24,0x39,0xE9,0xFD,0x15,0x66,0x46,0xF4,0x1B,0x11,0xD0,0x4D,0x73, +0xA3,0x7D,0x46,0xF9,0x3D,0xED,0xA8,0x5F,0x62,0xD4,0xF1,0x3F,0xF8,0xE0,0x74,0x57, +0x2B,0x18,0x9D,0x81,0xB4,0xC4,0x28,0xDA,0x94,0x97,0xA5,0x70,0xEB,0xAC,0x1D,0xBE, +0x07,0x11,0xF0,0xD5,0xDB,0xDD,0xE5,0x8C,0xF0,0xD5,0x32,0xB0,0x83,0xE6,0x57,0xE2, +0x8F,0xBF,0xBE,0xA1,0xAA,0xBF,0x3D,0x1D,0xB5,0xD4,0x38,0xEA,0xD7,0xB0,0x5C,0x3A, +0x4F,0x6A,0x3F,0x8F,0xC0,0x66,0x6C,0x63,0xAA,0xE9,0xD9,0xA4,0x16,0xF4,0x81,0xD1, +0x95,0x14,0x0E,0x7D,0xCD,0x95,0x34,0xD9,0xD2,0x8F,0x70,0x73,0x81,0x7B,0x9C,0x7E, +0xBD,0x98,0x61,0xD8,0x45,0x87,0x98,0x90,0xC5,0xEB,0x86,0x30,0xC6,0x35,0xBF,0xF0, +0xFF,0xC3,0x55,0x88,0x83,0x4B,0xEF,0x05,0x92,0x06,0x71,0xF2,0xB8,0x98,0x93,0xB7, +0xEC,0xCD,0x82,0x61,0xF1,0x38,0xE6,0x4F,0x97,0x98,0x2A,0x5A,0x8D, +}; + + +/* subject:/C=US/OU=www.xrampsecurity.com/O=XRamp Security Services Inc/CN=XRamp Global Certification Authority */ +/* issuer :/C=US/OU=www.xrampsecurity.com/O=XRamp Security Services Inc/CN=XRamp Global Certification Authority */ + + +const unsigned char XRamp_Global_CA_Root_certificate[1076]={ +0x30,0x82,0x04,0x30,0x30,0x82,0x03,0x18,0xA0,0x03,0x02,0x01,0x02,0x02,0x10,0x50, +0x94,0x6C,0xEC,0x18,0xEA,0xD5,0x9C,0x4D,0xD5,0x97,0xEF,0x75,0x8F,0xA0,0xAD,0x30, +0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x81, +0x82,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x1E, +0x30,0x1C,0x06,0x03,0x55,0x04,0x0B,0x13,0x15,0x77,0x77,0x77,0x2E,0x78,0x72,0x61, +0x6D,0x70,0x73,0x65,0x63,0x75,0x72,0x69,0x74,0x79,0x2E,0x63,0x6F,0x6D,0x31,0x24, +0x30,0x22,0x06,0x03,0x55,0x04,0x0A,0x13,0x1B,0x58,0x52,0x61,0x6D,0x70,0x20,0x53, +0x65,0x63,0x75,0x72,0x69,0x74,0x79,0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73, +0x20,0x49,0x6E,0x63,0x31,0x2D,0x30,0x2B,0x06,0x03,0x55,0x04,0x03,0x13,0x24,0x58, +0x52,0x61,0x6D,0x70,0x20,0x47,0x6C,0x6F,0x62,0x61,0x6C,0x20,0x43,0x65,0x72,0x74, +0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72, +0x69,0x74,0x79,0x30,0x1E,0x17,0x0D,0x30,0x34,0x31,0x31,0x30,0x31,0x31,0x37,0x31, +0x34,0x30,0x34,0x5A,0x17,0x0D,0x33,0x35,0x30,0x31,0x30,0x31,0x30,0x35,0x33,0x37, +0x31,0x39,0x5A,0x30,0x81,0x82,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13, +0x02,0x55,0x53,0x31,0x1E,0x30,0x1C,0x06,0x03,0x55,0x04,0x0B,0x13,0x15,0x77,0x77, +0x77,0x2E,0x78,0x72,0x61,0x6D,0x70,0x73,0x65,0x63,0x75,0x72,0x69,0x74,0x79,0x2E, +0x63,0x6F,0x6D,0x31,0x24,0x30,0x22,0x06,0x03,0x55,0x04,0x0A,0x13,0x1B,0x58,0x52, +0x61,0x6D,0x70,0x20,0x53,0x65,0x63,0x75,0x72,0x69,0x74,0x79,0x20,0x53,0x65,0x72, +0x76,0x69,0x63,0x65,0x73,0x20,0x49,0x6E,0x63,0x31,0x2D,0x30,0x2B,0x06,0x03,0x55, +0x04,0x03,0x13,0x24,0x58,0x52,0x61,0x6D,0x70,0x20,0x47,0x6C,0x6F,0x62,0x61,0x6C, +0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41, +0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x82,0x01,0x22,0x30,0x0D,0x06,0x09, +0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F,0x00, +0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0x98,0x24,0x1E,0xBD,0x15,0xB4,0xBA, +0xDF,0xC7,0x8C,0xA5,0x27,0xB6,0x38,0x0B,0x69,0xF3,0xB6,0x4E,0xA8,0x2C,0x2E,0x21, +0x1D,0x5C,0x44,0xDF,0x21,0x5D,0x7E,0x23,0x74,0xFE,0x5E,0x7E,0xB4,0x4A,0xB7,0xA6, +0xAD,0x1F,0xAE,0xE0,0x06,0x16,0xE2,0x9B,0x5B,0xD9,0x67,0x74,0x6B,0x5D,0x80,0x8F, +0x29,0x9D,0x86,0x1B,0xD9,0x9C,0x0D,0x98,0x6D,0x76,0x10,0x28,0x58,0xE4,0x65,0xB0, +0x7F,0x4A,0x98,0x79,0x9F,0xE0,0xC3,0x31,0x7E,0x80,0x2B,0xB5,0x8C,0xC0,0x40,0x3B, +0x11,0x86,0xD0,0xCB,0xA2,0x86,0x36,0x60,0xA4,0xD5,0x30,0x82,0x6D,0xD9,0x6E,0xD0, +0x0F,0x12,0x04,0x33,0x97,0x5F,0x4F,0x61,0x5A,0xF0,0xE4,0xF9,0x91,0xAB,0xE7,0x1D, +0x3B,0xBC,0xE8,0xCF,0xF4,0x6B,0x2D,0x34,0x7C,0xE2,0x48,0x61,0x1C,0x8E,0xF3,0x61, +0x44,0xCC,0x6F,0xA0,0x4A,0xA9,0x94,0xB0,0x4D,0xDA,0xE7,0xA9,0x34,0x7A,0x72,0x38, +0xA8,0x41,0xCC,0x3C,0x94,0x11,0x7D,0xEB,0xC8,0xA6,0x8C,0xB7,0x86,0xCB,0xCA,0x33, +0x3B,0xD9,0x3D,0x37,0x8B,0xFB,0x7A,0x3E,0x86,0x2C,0xE7,0x73,0xD7,0x0A,0x57,0xAC, +0x64,0x9B,0x19,0xEB,0xF4,0x0F,0x04,0x08,0x8A,0xAC,0x03,0x17,0x19,0x64,0xF4,0x5A, +0x25,0x22,0x8D,0x34,0x2C,0xB2,0xF6,0x68,0x1D,0x12,0x6D,0xD3,0x8A,0x1E,0x14,0xDA, +0xC4,0x8F,0xA6,0xE2,0x23,0x85,0xD5,0x7A,0x0D,0xBD,0x6A,0xE0,0xE9,0xEC,0xEC,0x17, +0xBB,0x42,0x1B,0x67,0xAA,0x25,0xED,0x45,0x83,0x21,0xFC,0xC1,0xC9,0x7C,0xD5,0x62, +0x3E,0xFA,0xF2,0xC5,0x2D,0xD3,0xFD,0xD4,0x65,0x02,0x03,0x01,0x00,0x01,0xA3,0x81, +0x9F,0x30,0x81,0x9C,0x30,0x13,0x06,0x09,0x2B,0x06,0x01,0x04,0x01,0x82,0x37,0x14, +0x02,0x04,0x06,0x1E,0x04,0x00,0x43,0x00,0x41,0x30,0x0B,0x06,0x03,0x55,0x1D,0x0F, +0x04,0x04,0x03,0x02,0x01,0x86,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF, +0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16, +0x04,0x14,0xC6,0x4F,0xA2,0x3D,0x06,0x63,0x84,0x09,0x9C,0xCE,0x62,0xE4,0x04,0xAC, +0x8D,0x5C,0xB5,0xE9,0xB6,0x1B,0x30,0x36,0x06,0x03,0x55,0x1D,0x1F,0x04,0x2F,0x30, +0x2D,0x30,0x2B,0xA0,0x29,0xA0,0x27,0x86,0x25,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F, +0x63,0x72,0x6C,0x2E,0x78,0x72,0x61,0x6D,0x70,0x73,0x65,0x63,0x75,0x72,0x69,0x74, +0x79,0x2E,0x63,0x6F,0x6D,0x2F,0x58,0x47,0x43,0x41,0x2E,0x63,0x72,0x6C,0x30,0x10, +0x06,0x09,0x2B,0x06,0x01,0x04,0x01,0x82,0x37,0x15,0x01,0x04,0x03,0x02,0x01,0x01, +0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03, +0x82,0x01,0x01,0x00,0x91,0x15,0x39,0x03,0x01,0x1B,0x67,0xFB,0x4A,0x1C,0xF9,0x0A, +0x60,0x5B,0xA1,0xDA,0x4D,0x97,0x62,0xF9,0x24,0x53,0x27,0xD7,0x82,0x64,0x4E,0x90, +0x2E,0xC3,0x49,0x1B,0x2B,0x9A,0xDC,0xFC,0xA8,0x78,0x67,0x35,0xF1,0x1D,0xF0,0x11, +0xBD,0xB7,0x48,0xE3,0x10,0xF6,0x0D,0xDF,0x3F,0xD2,0xC9,0xB6,0xAA,0x55,0xA4,0x48, +0xBA,0x02,0xDB,0xDE,0x59,0x2E,0x15,0x5B,0x3B,0x9D,0x16,0x7D,0x47,0xD7,0x37,0xEA, +0x5F,0x4D,0x76,0x12,0x36,0xBB,0x1F,0xD7,0xA1,0x81,0x04,0x46,0x20,0xA3,0x2C,0x6D, +0xA9,0x9E,0x01,0x7E,0x3F,0x29,0xCE,0x00,0x93,0xDF,0xFD,0xC9,0x92,0x73,0x89,0x89, +0x64,0x9E,0xE7,0x2B,0xE4,0x1C,0x91,0x2C,0xD2,0xB9,0xCE,0x7D,0xCE,0x6F,0x31,0x99, +0xD3,0xE6,0xBE,0xD2,0x1E,0x90,0xF0,0x09,0x14,0x79,0x5C,0x23,0xAB,0x4D,0xD2,0xDA, +0x21,0x1F,0x4D,0x99,0x79,0x9D,0xE1,0xCF,0x27,0x9F,0x10,0x9B,0x1C,0x88,0x0D,0xB0, +0x8A,0x64,0x41,0x31,0xB8,0x0E,0x6C,0x90,0x24,0xA4,0x9B,0x5C,0x71,0x8F,0xBA,0xBB, +0x7E,0x1C,0x1B,0xDB,0x6A,0x80,0x0F,0x21,0xBC,0xE9,0xDB,0xA6,0xB7,0x40,0xF4,0xB2, +0x8B,0xA9,0xB1,0xE4,0xEF,0x9A,0x1A,0xD0,0x3D,0x69,0x99,0xEE,0xA8,0x28,0xA3,0xE1, +0x3C,0xB3,0xF0,0xB2,0x11,0x9C,0xCF,0x7C,0x40,0xE6,0xDD,0xE7,0x43,0x7D,0xA2,0xD8, +0x3A,0xB5,0xA9,0x8D,0xF2,0x34,0x99,0xC4,0xD4,0x10,0xE1,0x06,0xFD,0x09,0x84,0x10, +0x3B,0xEE,0xC4,0x4C,0xF4,0xEC,0x27,0x7C,0x42,0xC2,0x74,0x7C,0x82,0x8A,0x09,0xC9, +0xB4,0x03,0x25,0xBC, +}; + + +const unsigned char* kSSLCertCertificateList[] = { + AddTrust_External_Root_certificate, + AddTrust_Low_Value_Services_Root_certificate, + AddTrust_Public_Services_Root_certificate, + AddTrust_Qualified_Certificates_Root_certificate, + AffirmTrust_Commercial_certificate, + AffirmTrust_Networking_certificate, + AffirmTrust_Premium_certificate, + AffirmTrust_Premium_ECC_certificate, + America_Online_Root_Certification_Authority_1_certificate, + America_Online_Root_Certification_Authority_2_certificate, + Baltimore_CyberTrust_Root_certificate, + Comodo_AAA_Services_root_certificate, + COMODO_Certification_Authority_certificate, + COMODO_ECC_Certification_Authority_certificate, + Comodo_Secure_Services_root_certificate, + Comodo_Trusted_Services_root_certificate, + Cybertrust_Global_Root_certificate, + DigiCert_Assured_ID_Root_CA_certificate, + DigiCert_Global_Root_CA_certificate, + DigiCert_High_Assurance_EV_Root_CA_certificate, + Entrust_net_Premium_2048_Secure_Server_CA_certificate, + Entrust_net_Secure_Server_CA_certificate, + Entrust_Root_Certification_Authority_certificate, + Equifax_Secure_CA_certificate, + Equifax_Secure_eBusiness_CA_1_certificate, + Equifax_Secure_eBusiness_CA_2_certificate, + Equifax_Secure_Global_eBusiness_CA_certificate, + GeoTrust_Global_CA_certificate, + GeoTrust_Global_CA_2_certificate, + GeoTrust_Primary_Certification_Authority_certificate, + GeoTrust_Primary_Certification_Authority___G2_certificate, + GeoTrust_Primary_Certification_Authority___G3_certificate, + GeoTrust_Universal_CA_certificate, + GeoTrust_Universal_CA_2_certificate, + GlobalSign_Root_CA_certificate, + GlobalSign_Root_CA___R2_certificate, + GlobalSign_Root_CA___R3_certificate, + Go_Daddy_Class_2_CA_certificate, + Go_Daddy_Root_Certificate_Authority___G2_certificate, + GTE_CyberTrust_Global_Root_certificate, + Network_Solutions_Certificate_Authority_certificate, + RSA_Root_Certificate_1_certificate, + Starfield_Class_2_CA_certificate, + Starfield_Root_Certificate_Authority___G2_certificate, + Starfield_Services_Root_Certificate_Authority___G2_certificate, + StartCom_Certification_Authority_certificate, + StartCom_Certification_Authority_G2_certificate, + TC_TrustCenter_Class_2_CA_II_certificate, + TC_TrustCenter_Class_3_CA_II_certificate, + TC_TrustCenter_Universal_CA_I_certificate, + TC_TrustCenter_Universal_CA_III_certificate, + Thawte_Premium_Server_CA_certificate, + thawte_Primary_Root_CA_certificate, + thawte_Primary_Root_CA___G2_certificate, + thawte_Primary_Root_CA___G3_certificate, + Thawte_Server_CA_certificate, + UTN_DATACorp_SGC_Root_CA_certificate, + UTN_USERFirst_Hardware_Root_CA_certificate, + ValiCert_Class_1_VA_certificate, + ValiCert_Class_2_VA_certificate, + Verisign_Class_3_Public_Primary_Certification_Authority_certificate, + Verisign_Class_3_Public_Primary_Certification_Authority___G2_certificate, + Verisign_Class_3_Public_Primary_Certification_Authority___G3_certificate, + VeriSign_Class_3_Public_Primary_Certification_Authority___G4_certificate, + VeriSign_Class_3_Public_Primary_Certification_Authority___G5_certificate, + Verisign_Class_4_Public_Primary_Certification_Authority___G3_certificate, + VeriSign_Universal_Root_Certification_Authority_certificate, + XRamp_Global_CA_Root_certificate, +}; + +const size_t kSSLCertCertificateSizeList[] = { + 1082, + 1052, + 1049, + 1058, + 848, + 848, + 1354, + 514, + 936, + 1448, + 891, + 1078, + 1057, + 653, + 1091, + 1095, + 933, + 955, + 947, + 969, + 1120, + 1244, + 1173, + 804, + 646, + 804, + 660, + 856, + 874, + 896, + 690, + 1026, + 1388, + 1392, + 889, + 958, + 867, + 1028, + 969, + 606, + 1002, + 747, + 1043, + 993, + 1011, + 1931, + 1383, + 1198, + 1198, + 993, + 997, + 811, + 1060, + 652, + 1070, + 791, + 1122, + 1144, + 747, + 747, + 576, + 774, + 1054, + 904, + 1239, + 1054, + 1213, + 1076, +}; + diff --git a/talk/base/sslsocketfactory.cc b/talk/base/sslsocketfactory.cc new file mode 100644 index 000000000..f44724e3e --- /dev/null +++ b/talk/base/sslsocketfactory.cc @@ -0,0 +1,192 @@ +/* + * libjingle + * Copyright 2007, 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/base/autodetectproxy.h" +#include "talk/base/httpcommon.h" +#include "talk/base/httpcommon-inl.h" +#include "talk/base/socketadapters.h" +#include "talk/base/ssladapter.h" +#include "talk/base/sslsocketfactory.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// ProxySocketAdapter +// TODO: Consider combining AutoDetectProxy and ProxySocketAdapter. I think +// the socket adapter is the more appropriate idiom for automatic proxy +// detection. We may or may not want to combine proxydetect.* as well. +/////////////////////////////////////////////////////////////////////////////// + +class ProxySocketAdapter : public AsyncSocketAdapter { + public: + ProxySocketAdapter(SslSocketFactory* factory, int family, int type) + : AsyncSocketAdapter(NULL), factory_(factory), family_(family), + type_(type), detect_(NULL) { + } + virtual ~ProxySocketAdapter() { + Close(); + } + + virtual int Connect(const SocketAddress& addr) { + ASSERT(NULL == detect_); + ASSERT(NULL == socket_); + remote_ = addr; + if (remote_.IsAnyIP() && remote_.hostname().empty()) { + LOG_F(LS_ERROR) << "Empty address"; + return SOCKET_ERROR; + } + Url url("/", remote_.HostAsURIString(), remote_.port()); + detect_ = new AutoDetectProxy(factory_->agent_); + detect_->set_server_url(url.url()); + detect_->SignalWorkDone.connect(this, + &ProxySocketAdapter::OnProxyDetectionComplete); + detect_->Start(); + return SOCKET_ERROR; + } + virtual int GetError() const { + if (socket_) { + return socket_->GetError(); + } + return detect_ ? EWOULDBLOCK : EADDRNOTAVAIL; + } + virtual int Close() { + if (socket_) { + return socket_->Close(); + } + if (detect_) { + detect_->Destroy(false); + detect_ = NULL; + } + return 0; + } + virtual ConnState GetState() const { + if (socket_) { + return socket_->GetState(); + } + return detect_ ? CS_CONNECTING : CS_CLOSED; + } + +private: + // AutoDetectProxy Slots + void OnProxyDetectionComplete(SignalThread* thread) { + ASSERT(detect_ == thread); + Attach(factory_->CreateProxySocket(detect_->proxy(), family_, type_)); + detect_->Release(); + detect_ = NULL; + if (0 == AsyncSocketAdapter::Connect(remote_)) { + SignalConnectEvent(this); + } else if (!IsBlockingError(socket_->GetError())) { + SignalCloseEvent(this, socket_->GetError()); + } + } + + SslSocketFactory* factory_; + int family_; + int type_; + SocketAddress remote_; + AutoDetectProxy* detect_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// SslSocketFactory +/////////////////////////////////////////////////////////////////////////////// + +Socket* SslSocketFactory::CreateSocket(int type) { + return CreateSocket(AF_INET, type); +} + +Socket* SslSocketFactory::CreateSocket(int family, int type) { + return factory_->CreateSocket(family, type); +} + +AsyncSocket* SslSocketFactory::CreateAsyncSocket(int type) { + return CreateAsyncSocket(AF_INET, type); +} + +AsyncSocket* SslSocketFactory::CreateAsyncSocket(int family, int type) { + if (autodetect_proxy_) { + return new ProxySocketAdapter(this, family, type); + } else { + return CreateProxySocket(proxy_, family, type); + } +} + + +AsyncSocket* SslSocketFactory::CreateProxySocket(const ProxyInfo& proxy, + int family, + int type) { + AsyncSocket* socket = factory_->CreateAsyncSocket(family, type); + if (!socket) + return NULL; + + // Binary logging happens at the lowest level + if (!logging_label_.empty() && binary_mode_) { + socket = new LoggingSocketAdapter(socket, logging_level_, + logging_label_.c_str(), binary_mode_); + } + + if (proxy.type) { + AsyncSocket* proxy_socket = 0; + if (proxy_.type == PROXY_SOCKS5) { + proxy_socket = new AsyncSocksProxySocket(socket, proxy.address, + proxy.username, proxy.password); + } else { + // Note: we are trying unknown proxies as HTTPS currently + AsyncHttpsProxySocket* http_proxy = + new AsyncHttpsProxySocket(socket, agent_, proxy.address, + proxy.username, proxy.password); + http_proxy->SetForceConnect(force_connect_ || !hostname_.empty()); + proxy_socket = http_proxy; + } + if (!proxy_socket) { + delete socket; + return NULL; + } + socket = proxy_socket; // for our purposes the proxy is now the socket + } + + if (!hostname_.empty()) { + if (SSLAdapter* ssl_adapter = SSLAdapter::Create(socket)) { + ssl_adapter->set_ignore_bad_cert(ignore_bad_cert_); + ssl_adapter->StartSSL(hostname_.c_str(), true); + socket = ssl_adapter; + } else { + LOG_F(LS_ERROR) << "SSL unavailable"; + } + } + + // Regular logging occurs at the highest level + if (!logging_label_.empty() && !binary_mode_) { + socket = new LoggingSocketAdapter(socket, logging_level_, + logging_label_.c_str(), binary_mode_); + } + return socket; +} + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/sslsocketfactory.h b/talk/base/sslsocketfactory.h new file mode 100644 index 000000000..32acd15a2 --- /dev/null +++ b/talk/base/sslsocketfactory.h @@ -0,0 +1,98 @@ +/* + * libjingle + * Copyright 2007, 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. + */ + +#ifndef TALK_BASE_SSLSOCKETFACTORY_H__ +#define TALK_BASE_SSLSOCKETFACTORY_H__ + +#include "talk/base/proxyinfo.h" +#include "talk/base/socketserver.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// SslSocketFactory +/////////////////////////////////////////////////////////////////////////////// + +class SslSocketFactory : public SocketFactory { + public: + SslSocketFactory(SocketFactory* factory, const std::string& user_agent) + : factory_(factory), agent_(user_agent), autodetect_proxy_(true), + force_connect_(false), logging_level_(LS_VERBOSE), binary_mode_(false), + ignore_bad_cert_(false) { + } + + void SetAutoDetectProxy() { + autodetect_proxy_ = true; + } + void SetForceConnect(bool force) { + force_connect_ = force; + } + void SetProxy(const ProxyInfo& proxy) { + autodetect_proxy_ = false; + proxy_ = proxy; + } + bool autodetect_proxy() const { return autodetect_proxy_; } + const ProxyInfo& proxy() const { return proxy_; } + + void UseSSL(const char* hostname) { hostname_ = hostname; } + void DisableSSL() { hostname_.clear(); } + void SetIgnoreBadCert(bool ignore) { ignore_bad_cert_ = ignore; } + bool ignore_bad_cert() const { return ignore_bad_cert_; } + + void SetLogging(LoggingSeverity level, const std::string& label, + bool binary_mode = false) { + logging_level_ = level; + logging_label_ = label; + binary_mode_ = binary_mode; + } + + // SocketFactory Interface + virtual Socket* CreateSocket(int type); + virtual Socket* CreateSocket(int family, int type); + + virtual AsyncSocket* CreateAsyncSocket(int type); + virtual AsyncSocket* CreateAsyncSocket(int family, int type); + + private: + friend class ProxySocketAdapter; + AsyncSocket* CreateProxySocket(const ProxyInfo& proxy, int family, int type); + + SocketFactory* factory_; + std::string agent_; + bool autodetect_proxy_, force_connect_; + ProxyInfo proxy_; + std::string hostname_, logging_label_; + LoggingSeverity logging_level_; + bool binary_mode_; + bool ignore_bad_cert_; +}; + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_SSLSOCKETFACTORY_H__ diff --git a/talk/base/sslstreamadapter.cc b/talk/base/sslstreamadapter.cc new file mode 100644 index 000000000..dc59ee06c --- /dev/null +++ b/talk/base/sslstreamadapter.cc @@ -0,0 +1,94 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +#if HAVE_CONFIG_H +#include "config.h" +#endif // HAVE_CONFIG_H + +#include "talk/base/sslstreamadapter.h" +#include "talk/base/sslconfig.h" + +#if SSL_USE_SCHANNEL + +// SChannel support for DTLS and peer-to-peer mode are not +// done. +#elif SSL_USE_OPENSSL // && !SSL_USE_SCHANNEL + +#include "talk/base/opensslstreamadapter.h" + +#elif SSL_USE_NSS // && !SSL_USE_SCHANNEL && !SSL_USE_OPENSSL + +#include "talk/base/nssstreamadapter.h" + +#endif // !SSL_USE_OPENSSL && !SSL_USE_SCHANNEL && !SSL_USE_NSS + +/////////////////////////////////////////////////////////////////////////////// + +namespace talk_base { + +SSLStreamAdapter* SSLStreamAdapter::Create(StreamInterface* stream) { +#if SSL_USE_SCHANNEL + return NULL; +#elif SSL_USE_OPENSSL // !SSL_USE_SCHANNEL + return new OpenSSLStreamAdapter(stream); +#elif SSL_USE_NSS // !SSL_USE_SCHANNEL && !SSL_USE_OPENSSL + return new NSSStreamAdapter(stream); +#else // !SSL_USE_SCHANNEL && !SSL_USE_OPENSSL && !SSL_USE_NSS + return NULL; +#endif +} + +// Note: this matches the logic above with SCHANNEL dominating +#if SSL_USE_SCHANNEL +bool SSLStreamAdapter::HaveDtls() { return false; } +bool SSLStreamAdapter::HaveDtlsSrtp() { return false; } +bool SSLStreamAdapter::HaveExporter() { return false; } +#elif SSL_USE_OPENSSL +bool SSLStreamAdapter::HaveDtls() { + return OpenSSLStreamAdapter::HaveDtls(); +} +bool SSLStreamAdapter::HaveDtlsSrtp() { + return OpenSSLStreamAdapter::HaveDtlsSrtp(); +} +bool SSLStreamAdapter::HaveExporter() { + return OpenSSLStreamAdapter::HaveExporter(); +} +#elif SSL_USE_NSS +bool SSLStreamAdapter::HaveDtls() { + return NSSStreamAdapter::HaveDtls(); +} +bool SSLStreamAdapter::HaveDtlsSrtp() { + return NSSStreamAdapter::HaveDtlsSrtp(); +} +bool SSLStreamAdapter::HaveExporter() { + return NSSStreamAdapter::HaveExporter(); +} +#endif // !SSL_USE_SCHANNEL && !SSL_USE_OPENSSL && !SSL_USE_NSS + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/sslstreamadapter.h b/talk/base/sslstreamadapter.h new file mode 100644 index 000000000..2afe1daf1 --- /dev/null +++ b/talk/base/sslstreamadapter.h @@ -0,0 +1,185 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +#ifndef TALK_BASE_SSLSTREAMADAPTER_H__ +#define TALK_BASE_SSLSTREAMADAPTER_H__ + +#include +#include + +#include "talk/base/stream.h" +#include "talk/base/sslidentity.h" + +namespace talk_base { + +// SSLStreamAdapter : A StreamInterfaceAdapter that does SSL/TLS. +// After SSL has been started, the stream will only open on successful +// SSL verification of certificates, and the communication is +// encrypted of course. +// +// This class was written with SSLAdapter as a starting point. It +// offers a similar interface, with two differences: there is no +// support for a restartable SSL connection, and this class has a +// peer-to-peer mode. +// +// The SSL library requires initialization and cleanup. Static method +// for doing this are in SSLAdapter. They should possibly be moved out +// to a neutral class. + + +enum SSLRole { SSL_CLIENT, SSL_SERVER }; +enum SSLMode { SSL_MODE_TLS, SSL_MODE_DTLS }; + +// Errors for Read -- in the high range so no conflict with OpenSSL. +enum { SSE_MSG_TRUNC = 0xff0001 }; + +class SSLStreamAdapter : public StreamAdapterInterface { + public: + // Instantiate an SSLStreamAdapter wrapping the given stream, + // (using the selected implementation for the platform). + // Caller is responsible for freeing the returned object. + static SSLStreamAdapter* Create(StreamInterface* stream); + + explicit SSLStreamAdapter(StreamInterface* stream) + : StreamAdapterInterface(stream), ignore_bad_cert_(false) { } + + void set_ignore_bad_cert(bool ignore) { ignore_bad_cert_ = ignore; } + bool ignore_bad_cert() const { return ignore_bad_cert_; } + + // Specify our SSL identity: key and certificate. Mostly this is + // only used in the peer-to-peer mode (unless we actually want to + // provide a client certificate to a server). + // SSLStream takes ownership of the SSLIdentity object and will + // free it when appropriate. Should be called no more than once on a + // given SSLStream instance. + virtual void SetIdentity(SSLIdentity* identity) = 0; + + // Call this to indicate that we are to play the server's role in + // the peer-to-peer mode. + // The default argument is for backward compatibility + // TODO(ekr@rtfm.com): rename this SetRole to reflect its new function + virtual void SetServerRole(SSLRole role = SSL_SERVER) = 0; + + // Do DTLS or TLS + virtual void SetMode(SSLMode mode) = 0; + + // The mode of operation is selected by calling either + // StartSSLWithServer or StartSSLWithPeer. + // Use of the stream prior to calling either of these functions will + // pass data in clear text. + // Calling one of these functions causes SSL negotiation to begin as + // soon as possible: right away if the underlying wrapped stream is + // already opened, or else as soon as it opens. + // + // These functions return a negative error code on failure. + // Returning 0 means success so far, but negotiation is probably not + // complete and will continue asynchronously. In that case, the + // exposed stream will open after successful negotiation and + // verification, or an SE_CLOSE event will be raised if negotiation + // fails. + + // StartSSLWithServer starts SSL negotiation with a server in + // traditional mode. server_name specifies the expected server name + // which the server's certificate needs to specify. + virtual int StartSSLWithServer(const char* server_name) = 0; + + // StartSSLWithPeer starts negotiation in the special peer-to-peer + // mode. + // Generally, SetIdentity() and possibly SetServerRole() should have + // been called before this. + // SetPeerCertificate() must also be called. It may be called after + // StartSSLWithPeer() but must be called before the underlying + // stream opens. + virtual int StartSSLWithPeer() = 0; + + // Specify the certificate that our peer is expected to use in + // peer-to-peer mode. Only this certificate will be accepted during + // SSL verification. The certificate is assumed to have been + // obtained through some other secure channel (such as the XMPP + // channel). (This could also specify the certificate authority that + // will sign the peer's certificate.) + // SSLStream takes ownership of the SSLCertificate object and will + // free it when appropriate. Should be called no more than once on a + // given SSLStream instance. + virtual void SetPeerCertificate(SSLCertificate* cert) = 0; + + // Specify the digest of the certificate that our peer is expected to use in + // peer-to-peer mode. Only this certificate will be accepted during + // SSL verification. The certificate is assumed to have been + // obtained through some other secure channel (such as the XMPP + // channel). Unlike SetPeerCertificate(), this must specify the + // terminal certificate, not just a CA. + // SSLStream makes a copy of the digest value. + virtual bool SetPeerCertificateDigest(const std::string& digest_alg, + const unsigned char* digest_val, + size_t digest_len) = 0; + + // Key Exporter interface from RFC 5705 + // Arguments are: + // label -- the exporter label. + // part of the RFC defining each exporter + // usage (IN) + // context/context_len -- a context to bind to for this connection; + // optional, can be NULL, 0 (IN) + // use_context -- whether to use the context value + // (needed to distinguish no context from + // zero-length ones). + // result -- where to put the computed value + // result_len -- the length of the computed value + virtual bool ExportKeyingMaterial(const std::string& label, + const uint8* context, + size_t context_len, + bool use_context, + uint8* result, + size_t result_len) { + return false; // Default is unsupported + } + + + // DTLS-SRTP interface + virtual bool SetDtlsSrtpCiphers(const std::vector& ciphers) { + return false; + } + + virtual bool GetDtlsSrtpCipher(std::string* cipher) { + return false; + } + + // Capabilities testing + static bool HaveDtls(); + static bool HaveDtlsSrtp(); + static bool HaveExporter(); + + // If true, the server certificate need not match the configured + // server_name, and in fact missing certificate authority and other + // verification errors are ignored. + bool ignore_bad_cert_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_SSLSTREAMADAPTER_H__ diff --git a/talk/base/sslstreamadapter_unittest.cc b/talk/base/sslstreamadapter_unittest.cc new file mode 100644 index 000000000..3b08baf29 --- /dev/null +++ b/talk/base/sslstreamadapter_unittest.cc @@ -0,0 +1,886 @@ +/* + * libjingle + * Copyright 2011, Google Inc. + * Portions Copyright 2011, RTFM, 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 +#include +#include + +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/ssladapter.h" +#include "talk/base/sslconfig.h" +#include "talk/base/sslidentity.h" +#include "talk/base/sslstreamadapter.h" +#include "talk/base/stream.h" + +static const int kBlockSize = 4096; +static const char kAES_CM_HMAC_SHA1_80[] = "AES_CM_128_HMAC_SHA1_80"; +static const char kAES_CM_HMAC_SHA1_32[] = "AES_CM_128_HMAC_SHA1_32"; +static const char kExporterLabel[] = "label"; +static const unsigned char kExporterContext[] = "context"; +static int kExporterContextLen = sizeof(kExporterContext); + +static const char kRSA_PRIVATE_KEY_PEM[] = + "-----BEGIN RSA PRIVATE KEY-----\n" + "MIICXQIBAAKBgQDCueE4a9hDMZ3sbVZdlXOz9ZA+cvzie3zJ9gXnT/BCt9P4b9HE\n" + "vD/tr73YBqD3Wr5ZWScmyGYF9EMn0r3rzBxv6oooLU5TdUvOm4rzUjkCLQaQML8o\n" + "NxXq+qW/j3zUKGikLhaaAl/amaX2zSWUsRQ1CpngQ3+tmDNH4/25TncNmQIDAQAB\n" + "AoGAUcuU0Id0k10fMjYHZk4mCPzot2LD2Tr4Aznl5vFMQipHzv7hhZtx2xzMSRcX\n" + "vG+Qr6VkbcUWHgApyWubvZXCh3+N7Vo2aYdMAQ8XqmFpBdIrL5CVdVfqFfEMlgEy\n" + "LSZNG5klnrIfl3c7zQVovLr4eMqyl2oGfAqPQz75+fecv1UCQQD6wNHch9NbAG1q\n" + "yuFEhMARB6gDXb+5SdzFjjtTWW5uJfm4DcZLoYyaIZm0uxOwsUKd0Rsma+oGitS1\n" + "CXmuqfpPAkEAxszyN3vIdpD44SREEtyKZBMNOk5pEIIGdbeMJC5/XHvpxww9xkoC\n" + "+39NbvUZYd54uT+rafbx4QZKc0h9xA/HlwJBAL37lYVWy4XpPv1olWCKi9LbUCqs\n" + "vvQtyD1N1BkEayy9TQRsO09WKOcmigRqsTJwOx7DLaTgokEuspYvhagWVPUCQE/y\n" + "0+YkTbYBD1Xbs9SyBKXCU6uDJRWSdO6aZi2W1XloC9gUwDMiSJjD1Wwt/YsyYPJ+\n" + "/Hyc5yFL2l0KZimW/vkCQQCjuZ/lPcH46EuzhdbRfumDOG5N3ld7UhGI1TIRy17W\n" + "dGF90cG33/L6BfS8Ll+fkkW/2AMRk8FDvF4CZi2nfW4L\n" + "-----END RSA PRIVATE KEY-----\n"; + +static const char kCERT_PEM[] = + "-----BEGIN CERTIFICATE-----\n" + "MIIBmTCCAQICCQCPNJORW/M13DANBgkqhkiG9w0BAQUFADARMQ8wDQYDVQQDDAZ3\n" + "ZWJydGMwHhcNMTMwNjE0MjIzMDAxWhcNMTQwNjE0MjIzMDAxWjARMQ8wDQYDVQQD\n" + "DAZ3ZWJydGMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMK54Thr2EMxnext\n" + "Vl2Vc7P1kD5y/OJ7fMn2BedP8EK30/hv0cS8P+2vvdgGoPdavllZJybIZgX0QyfS\n" + "vevMHG/qiigtTlN1S86bivNSOQItBpAwvyg3Fer6pb+PfNQoaKQuFpoCX9qZpfbN\n" + "JZSxFDUKmeBDf62YM0fj/blOdw2ZAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAECMt\n" + "UZb35H8TnjGx4XPzco/kbnurMLFFWcuve/DwTsuf10Ia9N4md8LY0UtgIgtyNqWc\n" + "ZwyRMwxONF6ty3wcaIiPbGqiAa55T3YRuPibkRmck9CjrmM9JAtyvqHnpHd2TsBD\n" + "qCV42aXS3onOXDQ1ibuWq0fr0//aj0wo4KV474c=\n" + "-----END CERTIFICATE-----\n"; + +#define MAYBE_SKIP_TEST(feature) \ + if (!(talk_base::SSLStreamAdapter::feature())) { \ + LOG(LS_INFO) << "Feature disabled... skipping"; \ + return; \ + } + +class SSLStreamAdapterTestBase; + +class SSLDummyStream : public talk_base::StreamInterface, + public sigslot::has_slots<> { + public: + explicit SSLDummyStream(SSLStreamAdapterTestBase *test, + const std::string &side, + talk_base::FifoBuffer *in, + talk_base::FifoBuffer *out) : + test_(test), + side_(side), + in_(in), + out_(out), + first_packet_(true) { + in_->SignalEvent.connect(this, &SSLDummyStream::OnEventIn); + out_->SignalEvent.connect(this, &SSLDummyStream::OnEventOut); + } + + virtual talk_base::StreamState GetState() const { return talk_base::SS_OPEN; } + + virtual talk_base::StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + talk_base::StreamResult r; + + r = in_->Read(buffer, buffer_len, read, error); + if (r == talk_base::SR_BLOCK) + return talk_base::SR_BLOCK; + if (r == talk_base::SR_EOS) + return talk_base::SR_EOS; + + if (r != talk_base::SR_SUCCESS) { + ADD_FAILURE(); + return talk_base::SR_ERROR; + } + + return talk_base::SR_SUCCESS; + } + + // Catch readability events on in and pass them up. + virtual void OnEventIn(talk_base::StreamInterface *stream, int sig, + int err) { + int mask = (talk_base::SE_READ | talk_base::SE_CLOSE); + + if (sig & mask) { + LOG(LS_INFO) << "SSLDummyStream::OnEvent side=" << side_ << " sig=" + << sig << " forwarding upward"; + PostEvent(sig & mask, 0); + } + } + + // Catch writeability events on out and pass them up. + virtual void OnEventOut(talk_base::StreamInterface *stream, int sig, + int err) { + if (sig & talk_base::SE_WRITE) { + LOG(LS_INFO) << "SSLDummyStream::OnEvent side=" << side_ << " sig=" + << sig << " forwarding upward"; + + PostEvent(sig & talk_base::SE_WRITE, 0); + } + } + + // Write to the outgoing FifoBuffer + talk_base::StreamResult WriteData(const void* data, size_t data_len, + size_t* written, int* error) { + return out_->Write(data, data_len, written, error); + } + + // Defined later + virtual talk_base::StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + + virtual void Close() { + LOG(LS_INFO) << "Closing outbound stream"; + out_->Close(); + } + + private: + SSLStreamAdapterTestBase *test_; + const std::string side_; + talk_base::FifoBuffer *in_; + talk_base::FifoBuffer *out_; + bool first_packet_; +}; + +static const int kFifoBufferSize = 4096; + +class SSLStreamAdapterTestBase : public testing::Test, + public sigslot::has_slots<> { + public: + SSLStreamAdapterTestBase(const std::string& client_cert_pem, + const std::string& client_private_key_pem, + bool dtls) : + client_buffer_(kFifoBufferSize), server_buffer_(kFifoBufferSize), + client_stream_( + new SSLDummyStream(this, "c2s", &client_buffer_, &server_buffer_)), + server_stream_( + new SSLDummyStream(this, "s2c", &server_buffer_, &client_buffer_)), + client_ssl_(talk_base::SSLStreamAdapter::Create(client_stream_)), + server_ssl_(talk_base::SSLStreamAdapter::Create(server_stream_)), + client_identity_(NULL), server_identity_(NULL), + delay_(0), mtu_(1460), loss_(0), lose_first_packet_(false), + damage_(false), dtls_(dtls), + handshake_wait_(5000), identities_set_(false) { + // Set use of the test RNG to get predictable loss patterns. + talk_base::SetRandomTestMode(true); + + // Set up the slots + client_ssl_->SignalEvent.connect(this, &SSLStreamAdapterTestBase::OnEvent); + server_ssl_->SignalEvent.connect(this, &SSLStreamAdapterTestBase::OnEvent); + + if (!client_cert_pem.empty() && !client_private_key_pem.empty()) { + client_identity_ = talk_base::SSLIdentity::FromPEMStrings( + client_private_key_pem, client_cert_pem); + } else { + client_identity_ = talk_base::SSLIdentity::Generate("client"); + } + server_identity_ = talk_base::SSLIdentity::Generate("server"); + + client_ssl_->SetIdentity(client_identity_); + server_ssl_->SetIdentity(server_identity_); + } + + ~SSLStreamAdapterTestBase() { + // Put it back for the next test. + talk_base::SetRandomTestMode(false); + } + + static void SetUpTestCase() { + talk_base::InitializeSSL(); + } + + virtual void OnEvent(talk_base::StreamInterface *stream, int sig, int err) { + LOG(LS_INFO) << "SSLStreamAdapterTestBase::OnEvent sig=" << sig; + + if (sig & talk_base::SE_READ) { + ReadData(stream); + } + + if ((stream == client_ssl_.get()) && (sig & talk_base::SE_WRITE)) { + WriteData(); + } + } + + void SetPeerIdentitiesByCertificate(bool correct) { + LOG(LS_INFO) << "Setting peer identities by certificate"; + + if (correct) { + client_ssl_->SetPeerCertificate(server_identity_->certificate(). + GetReference()); + server_ssl_->SetPeerCertificate(client_identity_->certificate(). + GetReference()); + } else { + // If incorrect, set up to expect our own certificate at the peer + client_ssl_->SetPeerCertificate(client_identity_->certificate(). + GetReference()); + server_ssl_->SetPeerCertificate(server_identity_->certificate(). + GetReference()); + } + identities_set_ = true; + } + + void SetPeerIdentitiesByDigest(bool correct) { + unsigned char digest[20]; + size_t digest_len; + bool rv; + + LOG(LS_INFO) << "Setting peer identities by digest"; + + rv = server_identity_->certificate().ComputeDigest(talk_base::DIGEST_SHA_1, + digest, 20, + &digest_len); + ASSERT_TRUE(rv); + if (!correct) { + LOG(LS_INFO) << "Setting bogus digest for server cert"; + digest[0]++; + } + rv = client_ssl_->SetPeerCertificateDigest(talk_base::DIGEST_SHA_1, digest, + digest_len); + ASSERT_TRUE(rv); + + + rv = client_identity_->certificate().ComputeDigest(talk_base::DIGEST_SHA_1, + digest, 20, &digest_len); + ASSERT_TRUE(rv); + if (!correct) { + LOG(LS_INFO) << "Setting bogus digest for client cert"; + digest[0]++; + } + rv = server_ssl_->SetPeerCertificateDigest(talk_base::DIGEST_SHA_1, digest, + digest_len); + ASSERT_TRUE(rv); + + identities_set_ = true; + } + + void TestHandshake(bool expect_success = true) { + server_ssl_->SetMode(dtls_ ? talk_base::SSL_MODE_DTLS : + talk_base::SSL_MODE_TLS); + client_ssl_->SetMode(dtls_ ? talk_base::SSL_MODE_DTLS : + talk_base::SSL_MODE_TLS); + + if (!dtls_) { + // Make sure we simulate a reliable network for TLS. + // This is just a check to make sure that people don't write wrong + // tests. + ASSERT((mtu_ == 1460) && (loss_ == 0) && (lose_first_packet_ == 0)); + } + + if (!identities_set_) + SetPeerIdentitiesByDigest(true); + + // Start the handshake + int rv; + + server_ssl_->SetServerRole(); + rv = server_ssl_->StartSSLWithPeer(); + ASSERT_EQ(0, rv); + + rv = client_ssl_->StartSSLWithPeer(); + ASSERT_EQ(0, rv); + + // Now run the handshake + if (expect_success) { + EXPECT_TRUE_WAIT((client_ssl_->GetState() == talk_base::SS_OPEN) + && (server_ssl_->GetState() == talk_base::SS_OPEN), + handshake_wait_); + } else { + EXPECT_TRUE_WAIT(client_ssl_->GetState() == talk_base::SS_CLOSED, + handshake_wait_); + } + } + + talk_base::StreamResult DataWritten(SSLDummyStream *from, const void *data, + size_t data_len, size_t *written, + int *error) { + // Randomly drop loss_ percent of packets + if (talk_base::CreateRandomId() % 100 < static_cast(loss_)) { + LOG(LS_INFO) << "Randomly dropping packet, size=" << data_len; + *written = data_len; + return talk_base::SR_SUCCESS; + } + if (dtls_ && (data_len > mtu_)) { + LOG(LS_INFO) << "Dropping packet > mtu, size=" << data_len; + *written = data_len; + return talk_base::SR_SUCCESS; + } + + // Optionally damage application data (type 23). Note that we don't damage + // handshake packets and we damage the last byte to keep the header + // intact but break the MAC. + if (damage_ && (*static_cast(data) == 23)) { + std::vector buf(data_len); + + LOG(LS_INFO) << "Damaging packet"; + + memcpy(&buf[0], data, data_len); + buf[data_len - 1]++; + + return from->WriteData(&buf[0], data_len, written, error); + } + + return from->WriteData(data, data_len, written, error); + } + + void SetDelay(int delay) { + delay_ = delay; + } + int GetDelay() { return delay_; } + + void SetLoseFirstPacket(bool lose) { + lose_first_packet_ = lose; + } + bool GetLoseFirstPacket() { return lose_first_packet_; } + + void SetLoss(int percent) { + loss_ = percent; + } + + void SetDamage() { + damage_ = true; + } + + void SetMtu(size_t mtu) { + mtu_ = mtu; + } + + void SetHandshakeWait(int wait) { + handshake_wait_ = wait; + } + + void SetDtlsSrtpCiphers(const std::vector &ciphers, + bool client) { + if (client) + client_ssl_->SetDtlsSrtpCiphers(ciphers); + else + server_ssl_->SetDtlsSrtpCiphers(ciphers); + } + + bool GetDtlsSrtpCipher(bool client, std::string *retval) { + if (client) + return client_ssl_->GetDtlsSrtpCipher(retval); + else + return server_ssl_->GetDtlsSrtpCipher(retval); + } + + bool ExportKeyingMaterial(const char *label, + const unsigned char *context, + size_t context_len, + bool use_context, + bool client, + unsigned char *result, + size_t result_len) { + if (client) + return client_ssl_->ExportKeyingMaterial(label, + context, context_len, + use_context, + result, result_len); + else + return server_ssl_->ExportKeyingMaterial(label, + context, context_len, + use_context, + result, result_len); + } + + // To be implemented by subclasses. + virtual void WriteData() = 0; + virtual void ReadData(talk_base::StreamInterface *stream) = 0; + virtual void TestTransfer(int size) = 0; + + protected: + talk_base::FifoBuffer client_buffer_; + talk_base::FifoBuffer server_buffer_; + SSLDummyStream *client_stream_; // freed by client_ssl_ destructor + SSLDummyStream *server_stream_; // freed by server_ssl_ destructor + talk_base::scoped_ptr client_ssl_; + talk_base::scoped_ptr server_ssl_; + talk_base::SSLIdentity *client_identity_; // freed by client_ssl_ destructor + talk_base::SSLIdentity *server_identity_; // freed by server_ssl_ destructor + int delay_; + size_t mtu_; + int loss_; + bool lose_first_packet_; + bool damage_; + bool dtls_; + int handshake_wait_; + bool identities_set_; +}; + +class SSLStreamAdapterTestTLS : public SSLStreamAdapterTestBase { + public: + SSLStreamAdapterTestTLS() : + SSLStreamAdapterTestBase("", "", false) { + }; + + // Test data transfer for TLS + virtual void TestTransfer(int size) { + LOG(LS_INFO) << "Starting transfer test with " << size << " bytes"; + // Create some dummy data to send. + size_t received; + + send_stream_.ReserveSize(size); + for (int i = 0; i < size; ++i) { + char ch = static_cast(i); + send_stream_.Write(&ch, 1, NULL, NULL); + } + send_stream_.Rewind(); + + // Prepare the receive stream. + recv_stream_.ReserveSize(size); + + // Start sending + WriteData(); + + // Wait for the client to close + EXPECT_TRUE_WAIT(server_ssl_->GetState() == talk_base::SS_CLOSED, 10000); + + // Now check the data + recv_stream_.GetSize(&received); + + EXPECT_EQ(static_cast(size), received); + EXPECT_EQ(0, memcmp(send_stream_.GetBuffer(), + recv_stream_.GetBuffer(), size)); + } + + void WriteData() { + size_t position, tosend, size; + talk_base::StreamResult rv; + size_t sent; + char block[kBlockSize]; + + send_stream_.GetSize(&size); + if (!size) + return; + + for (;;) { + send_stream_.GetPosition(&position); + if (send_stream_.Read(block, sizeof(block), &tosend, NULL) != + talk_base::SR_EOS) { + rv = client_ssl_->Write(block, tosend, &sent, 0); + + if (rv == talk_base::SR_SUCCESS) { + send_stream_.SetPosition(position + sent); + LOG(LS_VERBOSE) << "Sent: " << position + sent; + } else if (rv == talk_base::SR_BLOCK) { + LOG(LS_VERBOSE) << "Blocked..."; + send_stream_.SetPosition(position); + break; + } else { + ADD_FAILURE(); + break; + } + } else { + // Now close + LOG(LS_INFO) << "Wrote " << position << " bytes. Closing"; + client_ssl_->Close(); + break; + } + } + }; + + virtual void ReadData(talk_base::StreamInterface *stream) { + char buffer[1600]; + size_t bread; + int err2; + talk_base::StreamResult r; + + for (;;) { + r = stream->Read(buffer, sizeof(buffer), &bread, &err2); + + if (r == talk_base::SR_ERROR || r == talk_base::SR_EOS) { + // Unfortunately, errors are the way that the stream adapter + // signals close in OpenSSL + stream->Close(); + return; + } + + if (r == talk_base::SR_BLOCK) + break; + + ASSERT_EQ(talk_base::SR_SUCCESS, r); + LOG(LS_INFO) << "Read " << bread; + + recv_stream_.Write(buffer, bread, NULL, NULL); + } + } + + private: + talk_base::MemoryStream send_stream_; + talk_base::MemoryStream recv_stream_; +}; + +class SSLStreamAdapterTestDTLS : public SSLStreamAdapterTestBase { + public: + SSLStreamAdapterTestDTLS() : + SSLStreamAdapterTestBase("", "", true), + packet_size_(1000), count_(0), sent_(0) { + } + + SSLStreamAdapterTestDTLS(const std::string& cert_pem, + const std::string& private_key_pem) : + SSLStreamAdapterTestBase(cert_pem, private_key_pem, true), + packet_size_(1000), count_(0), sent_(0) { + } + + virtual void WriteData() { + unsigned char *packet = new unsigned char[1600]; + + do { + memset(packet, sent_ & 0xff, packet_size_); + *(reinterpret_cast(packet)) = sent_; + + size_t sent; + int rv = client_ssl_->Write(packet, packet_size_, &sent, 0); + if (rv == talk_base::SR_SUCCESS) { + LOG(LS_VERBOSE) << "Sent: " << sent_; + sent_++; + } else if (rv == talk_base::SR_BLOCK) { + LOG(LS_VERBOSE) << "Blocked..."; + break; + } else { + ADD_FAILURE(); + break; + } + } while (sent_ < count_); + + delete [] packet; + } + + virtual void ReadData(talk_base::StreamInterface *stream) { + unsigned char *buffer = new unsigned char[2000]; + size_t bread; + int err2; + talk_base::StreamResult r; + + for (;;) { + r = stream->Read(buffer, 2000, + &bread, &err2); + + if (r == talk_base::SR_ERROR) { + // Unfortunately, errors are the way that the stream adapter + // signals close right now + stream->Close(); + return; + } + + if (r == talk_base::SR_BLOCK) + break; + + ASSERT_EQ(talk_base::SR_SUCCESS, r); + LOG(LS_INFO) << "Read " << bread; + + // Now parse the datagram + ASSERT_EQ(packet_size_, bread); + uint32_t packet_num = *(reinterpret_cast(buffer)); + + for (size_t i = 4; i < packet_size_; i++) { + ASSERT_EQ((packet_num & 0xff), buffer[i]); + } + received_.insert(packet_num); + } + } + + virtual void TestTransfer(int count) { + count_ = count; + + WriteData(); + + EXPECT_TRUE_WAIT(sent_ == count_, 10000); + LOG(LS_INFO) << "sent_ == " << sent_; + + if (damage_) { + WAIT(false, 2000); + EXPECT_EQ(0U, received_.size()); + } else if (loss_ == 0) { + EXPECT_EQ_WAIT(static_cast(sent_), received_.size(), 1000); + } else { + LOG(LS_INFO) << "Sent " << sent_ << " packets; received " << + received_.size(); + } + }; + + private: + size_t packet_size_; + int count_; + int sent_; + std::set received_; +}; + + +talk_base::StreamResult SSLDummyStream::Write(const void* data, size_t data_len, + size_t* written, int* error) { + *written = data_len; + + LOG(LS_INFO) << "Writing to loopback " << data_len; + + if (first_packet_) { + first_packet_ = false; + if (test_->GetLoseFirstPacket()) { + LOG(LS_INFO) << "Losing initial packet of length " << data_len; + return talk_base::SR_SUCCESS; + } + } + + return test_->DataWritten(this, data, data_len, written, error); + + return talk_base::SR_SUCCESS; +}; + +class SSLStreamAdapterTestDTLSFromPEMStrings : public SSLStreamAdapterTestDTLS { + public: + SSLStreamAdapterTestDTLSFromPEMStrings() : + SSLStreamAdapterTestDTLS(kCERT_PEM, kRSA_PRIVATE_KEY_PEM) { + } +}; + +// Basic tests: TLS + +// Test that we cannot read/write if we have not yet handshaked. +// This test only applies to NSS because OpenSSL has passthrough +// semantics for I/O before the handshake is started. +#if SSL_USE_NSS +TEST_F(SSLStreamAdapterTestTLS, TestNoReadWriteBeforeConnect) { + talk_base::StreamResult rv; + char block[kBlockSize]; + size_t dummy; + + rv = client_ssl_->Write(block, sizeof(block), &dummy, NULL); + ASSERT_EQ(talk_base::SR_BLOCK, rv); + + rv = client_ssl_->Read(block, sizeof(block), &dummy, NULL); + ASSERT_EQ(talk_base::SR_BLOCK, rv); +} +#endif + + +// Test that we can make a handshake work +TEST_F(SSLStreamAdapterTestTLS, TestTLSConnect) { + TestHandshake(); +}; + +// Test transfer -- trivial +TEST_F(SSLStreamAdapterTestTLS, TestTLSTransfer) { + TestHandshake(); + TestTransfer(100000); +}; + +// Test read-write after close. +TEST_F(SSLStreamAdapterTestTLS, ReadWriteAfterClose) { + TestHandshake(); + TestTransfer(100000); + client_ssl_->Close(); + + talk_base::StreamResult rv; + char block[kBlockSize]; + size_t dummy; + + // It's an error to write after closed. + rv = client_ssl_->Write(block, sizeof(block), &dummy, NULL); + ASSERT_EQ(talk_base::SR_ERROR, rv); + + // But after closed read gives you EOS. + rv = client_ssl_->Read(block, sizeof(block), &dummy, NULL); + ASSERT_EQ(talk_base::SR_EOS, rv); +}; + +// Test a handshake with a bogus peer digest +TEST_F(SSLStreamAdapterTestTLS, TestTLSBogusDigest) { + SetPeerIdentitiesByDigest(false); + TestHandshake(false); +}; + +// Test a handshake with a peer certificate +TEST_F(SSLStreamAdapterTestTLS, TestTLSPeerCertificate) { + SetPeerIdentitiesByCertificate(true); + TestHandshake(); +}; + +// Test a handshake with a bogus peer certificate +TEST_F(SSLStreamAdapterTestTLS, TestTLSBogusPeerCertificate) { + SetPeerIdentitiesByCertificate(false); + TestHandshake(false); +}; +// Test moving a bunch of data + +// Basic tests: DTLS +// Test that we can make a handshake work +TEST_F(SSLStreamAdapterTestDTLS, TestDTLSConnect) { + MAYBE_SKIP_TEST(HaveDtls); + TestHandshake(); +}; + +// Test that we can make a handshake work if the first packet in +// each direction is lost. This gives us predictable loss +// rather than having to tune random +TEST_F(SSLStreamAdapterTestDTLS, TestDTLSConnectWithLostFirstPacket) { + MAYBE_SKIP_TEST(HaveDtls); + SetLoseFirstPacket(true); + TestHandshake(); +}; + +// Test a handshake with loss and delay +TEST_F(SSLStreamAdapterTestDTLS, + TestDTLSConnectWithLostFirstPacketDelay2s) { + MAYBE_SKIP_TEST(HaveDtls); + SetLoseFirstPacket(true); + SetDelay(2000); + SetHandshakeWait(20000); + TestHandshake(); +}; + +// Test a handshake with small MTU +TEST_F(SSLStreamAdapterTestDTLS, TestDTLSConnectWithSmallMtu) { + MAYBE_SKIP_TEST(HaveDtls); + SetMtu(700); + SetHandshakeWait(20000); + TestHandshake(); +}; + +// Test transfer -- trivial +TEST_F(SSLStreamAdapterTestDTLS, TestDTLSTransfer) { + MAYBE_SKIP_TEST(HaveDtls); + TestHandshake(); + TestTransfer(100); +}; + +TEST_F(SSLStreamAdapterTestDTLS, TestDTLSTransferWithLoss) { + MAYBE_SKIP_TEST(HaveDtls); + TestHandshake(); + SetLoss(10); + TestTransfer(100); +}; + +TEST_F(SSLStreamAdapterTestDTLS, TestDTLSTransferWithDamage) { + MAYBE_SKIP_TEST(HaveDtls); + SetDamage(); // Must be called first because first packet + // write happens at end of handshake. + TestHandshake(); + TestTransfer(100); +}; + +// Test DTLS-SRTP with all high ciphers +TEST_F(SSLStreamAdapterTestDTLS, TestDTLSSrtpHigh) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + std::vector high; + high.push_back(kAES_CM_HMAC_SHA1_80); + SetDtlsSrtpCiphers(high, true); + SetDtlsSrtpCiphers(high, false); + TestHandshake(); + + std::string client_cipher; + ASSERT_TRUE(GetDtlsSrtpCipher(true, &client_cipher)); + std::string server_cipher; + ASSERT_TRUE(GetDtlsSrtpCipher(false, &server_cipher)); + + ASSERT_EQ(client_cipher, server_cipher); + ASSERT_EQ(client_cipher, kAES_CM_HMAC_SHA1_80); +}; + +// Test DTLS-SRTP with all low ciphers +TEST_F(SSLStreamAdapterTestDTLS, TestDTLSSrtpLow) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + std::vector low; + low.push_back(kAES_CM_HMAC_SHA1_32); + SetDtlsSrtpCiphers(low, true); + SetDtlsSrtpCiphers(low, false); + TestHandshake(); + + std::string client_cipher; + ASSERT_TRUE(GetDtlsSrtpCipher(true, &client_cipher)); + std::string server_cipher; + ASSERT_TRUE(GetDtlsSrtpCipher(false, &server_cipher)); + + ASSERT_EQ(client_cipher, server_cipher); + ASSERT_EQ(client_cipher, kAES_CM_HMAC_SHA1_32); +}; + + +// Test DTLS-SRTP with a mismatch -- should not converge +TEST_F(SSLStreamAdapterTestDTLS, TestDTLSSrtpHighLow) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + std::vector high; + high.push_back(kAES_CM_HMAC_SHA1_80); + std::vector low; + low.push_back(kAES_CM_HMAC_SHA1_32); + SetDtlsSrtpCiphers(high, true); + SetDtlsSrtpCiphers(low, false); + TestHandshake(); + + std::string client_cipher; + ASSERT_FALSE(GetDtlsSrtpCipher(true, &client_cipher)); + std::string server_cipher; + ASSERT_FALSE(GetDtlsSrtpCipher(false, &server_cipher)); +}; + +// Test DTLS-SRTP with each side being mixed -- should select high +TEST_F(SSLStreamAdapterTestDTLS, TestDTLSSrtpMixed) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + std::vector mixed; + mixed.push_back(kAES_CM_HMAC_SHA1_80); + mixed.push_back(kAES_CM_HMAC_SHA1_32); + SetDtlsSrtpCiphers(mixed, true); + SetDtlsSrtpCiphers(mixed, false); + TestHandshake(); + + std::string client_cipher; + ASSERT_TRUE(GetDtlsSrtpCipher(true, &client_cipher)); + std::string server_cipher; + ASSERT_TRUE(GetDtlsSrtpCipher(false, &server_cipher)); + + ASSERT_EQ(client_cipher, server_cipher); + ASSERT_EQ(client_cipher, kAES_CM_HMAC_SHA1_80); +}; + +// Test an exporter +TEST_F(SSLStreamAdapterTestDTLS, TestDTLSExporter) { + MAYBE_SKIP_TEST(HaveExporter); + TestHandshake(); + unsigned char client_out[20]; + unsigned char server_out[20]; + + bool result; + result = ExportKeyingMaterial(kExporterLabel, + kExporterContext, kExporterContextLen, + true, true, + client_out, sizeof(client_out)); + ASSERT_TRUE(result); + + result = ExportKeyingMaterial(kExporterLabel, + kExporterContext, kExporterContextLen, + true, false, + server_out, sizeof(server_out)); + ASSERT_TRUE(result); + + ASSERT_TRUE(!memcmp(client_out, server_out, sizeof(client_out))); +} + +// Test data transfer using certs created from strings. +TEST_F(SSLStreamAdapterTestDTLSFromPEMStrings, TestTransfer) { + MAYBE_SKIP_TEST(HaveDtls); + TestHandshake(); + TestTransfer(100); +} diff --git a/talk/base/sslstreamadapterhelper.cc b/talk/base/sslstreamadapterhelper.cc new file mode 100644 index 000000000..5a1a25550 --- /dev/null +++ b/talk/base/sslstreamadapterhelper.cc @@ -0,0 +1,147 @@ +/* + * libjingle + * Copyright 2004--2008, Google Inc. + * Copyright 2012, RTFM, 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 + +#if HAVE_CONFIG_H +#include "config.h" +#endif // HAVE_CONFIG_H + +#include "talk/base/sslstreamadapterhelper.h" + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/stream.h" + +namespace talk_base { + +void SSLStreamAdapterHelper::SetIdentity(SSLIdentity* identity) { + ASSERT(identity_.get() == NULL); + identity_.reset(identity); +} + +void SSLStreamAdapterHelper::SetServerRole(SSLRole role) { + role_ = role; +} + +int SSLStreamAdapterHelper::StartSSLWithServer(const char* server_name) { + ASSERT(server_name != NULL && server_name[0] != '\0'); + ssl_server_name_ = server_name; + return StartSSL(); +} + +int SSLStreamAdapterHelper::StartSSLWithPeer() { + ASSERT(ssl_server_name_.empty()); + // It is permitted to specify peer_certificate_ only later. + return StartSSL(); +} + +void SSLStreamAdapterHelper::SetMode(SSLMode mode) { + ASSERT(state_ == SSL_NONE); + ssl_mode_ = mode; +} + +StreamState SSLStreamAdapterHelper::GetState() const { + switch (state_) { + case SSL_WAIT: + case SSL_CONNECTING: + return SS_OPENING; + case SSL_CONNECTED: + return SS_OPEN; + default: + return SS_CLOSED; + }; + // not reached +} + +void SSLStreamAdapterHelper::SetPeerCertificate(SSLCertificate* cert) { + ASSERT(peer_certificate_.get() == NULL); + ASSERT(peer_certificate_digest_algorithm_.empty()); + ASSERT(ssl_server_name_.empty()); + peer_certificate_.reset(cert); +} + +bool SSLStreamAdapterHelper::SetPeerCertificateDigest( + const std::string &digest_alg, + const unsigned char* digest_val, + size_t digest_len) { + ASSERT(peer_certificate_.get() == NULL); + ASSERT(peer_certificate_digest_algorithm_.empty()); + ASSERT(ssl_server_name_.empty()); + size_t expected_len; + + if (!GetDigestLength(digest_alg, &expected_len)) { + LOG(LS_WARNING) << "Unknown digest algorithm: " << digest_alg; + return false; + } + if (expected_len != digest_len) + return false; + + peer_certificate_digest_value_.SetData(digest_val, digest_len); + peer_certificate_digest_algorithm_ = digest_alg; + + return true; +} + +void SSLStreamAdapterHelper::Error(const char* context, int err, bool signal) { + LOG(LS_WARNING) << "SSLStreamAdapterHelper::Error(" + << context << ", " << err << "," << signal << ")"; + state_ = SSL_ERROR; + ssl_error_code_ = err; + Cleanup(); + if (signal) + StreamAdapterInterface::OnEvent(stream(), SE_CLOSE, err); +} + +void SSLStreamAdapterHelper::Close() { + Cleanup(); + ASSERT(state_ == SSL_CLOSED || state_ == SSL_ERROR); + StreamAdapterInterface::Close(); +} + +int SSLStreamAdapterHelper::StartSSL() { + ASSERT(state_ == SSL_NONE); + + if (StreamAdapterInterface::GetState() != SS_OPEN) { + state_ = SSL_WAIT; + return 0; + } + + state_ = SSL_CONNECTING; + int err = BeginSSL(); + if (err) { + Error("BeginSSL", err, false); + return err; + } + + return 0; +} + +} // namespace talk_base + diff --git a/talk/base/sslstreamadapterhelper.h b/talk/base/sslstreamadapterhelper.h new file mode 100644 index 000000000..e8cb3b08b --- /dev/null +++ b/talk/base/sslstreamadapterhelper.h @@ -0,0 +1,137 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +#ifndef TALK_BASE_SSLSTREAMADAPTERHELPER_H_ +#define TALK_BASE_SSLSTREAMADAPTERHELPER_H_ + +#include +#include + +#include "talk/base/buffer.h" +#include "talk/base/stream.h" +#include "talk/base/sslidentity.h" +#include "talk/base/sslstreamadapter.h" + +namespace talk_base { + +// SSLStreamAdapterHelper : A stream adapter which implements much +// of the logic that is common between the known implementations +// (NSS and OpenSSL) +class SSLStreamAdapterHelper : public SSLStreamAdapter { + public: + explicit SSLStreamAdapterHelper(StreamInterface* stream) + : SSLStreamAdapter(stream), + state_(SSL_NONE), + role_(SSL_CLIENT), + ssl_error_code_(0), // Not meaningful yet + ssl_mode_(SSL_MODE_TLS) {} + + + // Overrides of SSLStreamAdapter + virtual void SetIdentity(SSLIdentity* identity); + virtual void SetServerRole(SSLRole role = SSL_SERVER); + virtual void SetMode(SSLMode mode); + + virtual int StartSSLWithServer(const char* server_name); + virtual int StartSSLWithPeer(); + + virtual void SetPeerCertificate(SSLCertificate* cert); + virtual bool SetPeerCertificateDigest(const std::string& digest_alg, + const unsigned char* digest_val, + size_t digest_len); + virtual StreamState GetState() const; + virtual void Close(); + + protected: + // Internal helper methods + // The following method returns 0 on success and a negative + // error code on failure. The error code may be either -1 or + // from the impl on some other error cases, so it can't really be + // interpreted unfortunately. + + // Perform SSL negotiation steps. + int ContinueSSL(); + + // Error handler helper. signal is given as true for errors in + // asynchronous contexts (when an error code was not returned + // through some other method), and in that case an SE_CLOSE event is + // raised on the stream with the specified error. + // A 0 error means a graceful close, otherwise there is not really enough + // context to interpret the error code. + virtual void Error(const char* context, int err, bool signal); + + // Must be implemented by descendents + virtual int BeginSSL() = 0; + virtual void Cleanup() = 0; + virtual bool GetDigestLength(const std::string &algorithm, + std::size_t *length) = 0; + + enum SSLState { + // Before calling one of the StartSSL methods, data flows + // in clear text. + SSL_NONE, + SSL_WAIT, // waiting for the stream to open to start SSL negotiation + SSL_CONNECTING, // SSL negotiation in progress + SSL_CONNECTED, // SSL stream successfully established + SSL_ERROR, // some SSL error occurred, stream is closed + SSL_CLOSED // Clean close + }; + + // MSG_MAX is the maximum generic stream message number. + enum { MSG_DTLS_TIMEOUT = MSG_MAX + 1 }; + + SSLState state_; + SSLRole role_; + int ssl_error_code_; // valid when state_ == SSL_ERROR + + // Our key and certificate, mostly useful in peer-to-peer mode. + scoped_ptr identity_; + // in traditional mode, the server name that the server's certificate + // must specify. Empty in peer-to-peer mode. + std::string ssl_server_name_; + // In peer-to-peer mode, the certificate that the peer must + // present. Empty in traditional mode. + scoped_ptr peer_certificate_; + + // In peer-to-peer mode, the digest of the certificate that + // the peer must present. + Buffer peer_certificate_digest_value_; + std::string peer_certificate_digest_algorithm_; + + // Do DTLS or not + SSLMode ssl_mode_; + + private: + // Go from state SSL_NONE to either SSL_CONNECTING or SSL_WAIT, + // depending on whether the underlying stream is already open or + // not. Returns 0 on success and a negative value on error. + int StartSSL(); +}; + +} // namespace talk_base + +#endif // TALK_BASE_SSLSTREAMADAPTERHELPER_H_ diff --git a/talk/base/stream.cc b/talk/base/stream.cc new file mode 100644 index 000000000..20adfcfa9 --- /dev/null +++ b/talk/base/stream.cc @@ -0,0 +1,1252 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#if defined(POSIX) +#include +#endif // POSIX +#include +#include +#include +#include +#include "talk/base/basictypes.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/messagequeue.h" +#include "talk/base/stream.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" + +#ifdef WIN32 +#include "talk/base/win32.h" +#define fileno _fileno +#endif + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// StreamInterface +/////////////////////////////////////////////////////////////////////////////// +StreamInterface::~StreamInterface() { +} + +StreamResult StreamInterface::WriteAll(const void* data, size_t data_len, + size_t* written, int* error) { + StreamResult result = SR_SUCCESS; + size_t total_written = 0, current_written; + while (total_written < data_len) { + result = Write(static_cast(data) + total_written, + data_len - total_written, ¤t_written, error); + if (result != SR_SUCCESS) + break; + total_written += current_written; + } + if (written) + *written = total_written; + return result; +} + +StreamResult StreamInterface::ReadAll(void* buffer, size_t buffer_len, + size_t* read, int* error) { + StreamResult result = SR_SUCCESS; + size_t total_read = 0, current_read; + while (total_read < buffer_len) { + result = Read(static_cast(buffer) + total_read, + buffer_len - total_read, ¤t_read, error); + if (result != SR_SUCCESS) + break; + total_read += current_read; + } + if (read) + *read = total_read; + return result; +} + +StreamResult StreamInterface::ReadLine(std::string* line) { + line->clear(); + StreamResult result = SR_SUCCESS; + while (true) { + char ch; + result = Read(&ch, sizeof(ch), NULL, NULL); + if (result != SR_SUCCESS) { + break; + } + if (ch == '\n') { + break; + } + line->push_back(ch); + } + if (!line->empty()) { // give back the line we've collected so far with + result = SR_SUCCESS; // a success code. Otherwise return the last code + } + return result; +} + +void StreamInterface::PostEvent(Thread* t, int events, int err) { + t->Post(this, MSG_POST_EVENT, new StreamEventData(events, err)); +} + +void StreamInterface::PostEvent(int events, int err) { + PostEvent(Thread::Current(), events, err); +} + +StreamInterface::StreamInterface() { +} + +void StreamInterface::OnMessage(Message* msg) { + if (MSG_POST_EVENT == msg->message_id) { + StreamEventData* pe = static_cast(msg->pdata); + SignalEvent(this, pe->events, pe->error); + delete msg->pdata; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// StreamAdapterInterface +/////////////////////////////////////////////////////////////////////////////// + +StreamAdapterInterface::StreamAdapterInterface(StreamInterface* stream, + bool owned) + : stream_(stream), owned_(owned) { + if (NULL != stream_) + stream_->SignalEvent.connect(this, &StreamAdapterInterface::OnEvent); +} + +void StreamAdapterInterface::Attach(StreamInterface* stream, bool owned) { + if (NULL != stream_) + stream_->SignalEvent.disconnect(this); + if (owned_) + delete stream_; + stream_ = stream; + owned_ = owned; + if (NULL != stream_) + stream_->SignalEvent.connect(this, &StreamAdapterInterface::OnEvent); +} + +StreamInterface* StreamAdapterInterface::Detach() { + if (NULL != stream_) + stream_->SignalEvent.disconnect(this); + StreamInterface* stream = stream_; + stream_ = NULL; + return stream; +} + +StreamAdapterInterface::~StreamAdapterInterface() { + if (owned_) + delete stream_; +} + +/////////////////////////////////////////////////////////////////////////////// +// StreamTap +/////////////////////////////////////////////////////////////////////////////// + +StreamTap::StreamTap(StreamInterface* stream, StreamInterface* tap) + : StreamAdapterInterface(stream), tap_(NULL), tap_result_(SR_SUCCESS), + tap_error_(0) { + AttachTap(tap); +} + +void StreamTap::AttachTap(StreamInterface* tap) { + tap_.reset(tap); +} + +StreamInterface* StreamTap::DetachTap() { + return tap_.release(); +} + +StreamResult StreamTap::GetTapResult(int* error) { + if (error) { + *error = tap_error_; + } + return tap_result_; +} + +StreamResult StreamTap::Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + size_t backup_read; + if (!read) { + read = &backup_read; + } + StreamResult res = StreamAdapterInterface::Read(buffer, buffer_len, + read, error); + if ((res == SR_SUCCESS) && (tap_result_ == SR_SUCCESS)) { + tap_result_ = tap_->WriteAll(buffer, *read, NULL, &tap_error_); + } + return res; +} + +StreamResult StreamTap::Write(const void* data, size_t data_len, + size_t* written, int* error) { + size_t backup_written; + if (!written) { + written = &backup_written; + } + StreamResult res = StreamAdapterInterface::Write(data, data_len, + written, error); + if ((res == SR_SUCCESS) && (tap_result_ == SR_SUCCESS)) { + tap_result_ = tap_->WriteAll(data, *written, NULL, &tap_error_); + } + return res; +} + +/////////////////////////////////////////////////////////////////////////////// +// StreamSegment +/////////////////////////////////////////////////////////////////////////////// + +StreamSegment::StreamSegment(StreamInterface* stream) + : StreamAdapterInterface(stream), start_(SIZE_UNKNOWN), pos_(0), + length_(SIZE_UNKNOWN) { + // It's ok for this to fail, in which case start_ is left as SIZE_UNKNOWN. + stream->GetPosition(&start_); +} + +StreamSegment::StreamSegment(StreamInterface* stream, size_t length) + : StreamAdapterInterface(stream), start_(SIZE_UNKNOWN), pos_(0), + length_(length) { + // It's ok for this to fail, in which case start_ is left as SIZE_UNKNOWN. + stream->GetPosition(&start_); +} + +StreamResult StreamSegment::Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + if (SIZE_UNKNOWN != length_) { + if (pos_ >= length_) + return SR_EOS; + buffer_len = _min(buffer_len, length_ - pos_); + } + size_t backup_read; + if (!read) { + read = &backup_read; + } + StreamResult result = StreamAdapterInterface::Read(buffer, buffer_len, + read, error); + if (SR_SUCCESS == result) { + pos_ += *read; + } + return result; +} + +bool StreamSegment::SetPosition(size_t position) { + if (SIZE_UNKNOWN == start_) + return false; // Not seekable + if ((SIZE_UNKNOWN != length_) && (position > length_)) + return false; // Seek past end of segment + if (!StreamAdapterInterface::SetPosition(start_ + position)) + return false; + pos_ = position; + return true; +} + +bool StreamSegment::GetPosition(size_t* position) const { + if (SIZE_UNKNOWN == start_) + return false; // Not seekable + if (!StreamAdapterInterface::GetPosition(position)) + return false; + if (position) { + ASSERT(*position >= start_); + *position -= start_; + } + return true; +} + +bool StreamSegment::GetSize(size_t* size) const { + if (!StreamAdapterInterface::GetSize(size)) + return false; + if (size) { + if (SIZE_UNKNOWN != start_) { + ASSERT(*size >= start_); + *size -= start_; + } + if (SIZE_UNKNOWN != length_) { + *size = _min(*size, length_); + } + } + return true; +} + +bool StreamSegment::GetAvailable(size_t* size) const { + if (!StreamAdapterInterface::GetAvailable(size)) + return false; + if (size && (SIZE_UNKNOWN != length_)) + *size = _min(*size, length_ - pos_); + return true; +} + +/////////////////////////////////////////////////////////////////////////////// +// NullStream +/////////////////////////////////////////////////////////////////////////////// + +NullStream::NullStream() { +} + +NullStream::~NullStream() { +} + +StreamState NullStream::GetState() const { + return SS_OPEN; +} + +StreamResult NullStream::Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + if (error) *error = -1; + return SR_ERROR; +} + +StreamResult NullStream::Write(const void* data, size_t data_len, + size_t* written, int* error) { + if (written) *written = data_len; + return SR_SUCCESS; +} + +void NullStream::Close() { +} + +/////////////////////////////////////////////////////////////////////////////// +// FileStream +/////////////////////////////////////////////////////////////////////////////// + +FileStream::FileStream() : file_(NULL) { +} + +FileStream::~FileStream() { + FileStream::Close(); +} + +bool FileStream::Open(const std::string& filename, const char* mode, + int* error) { + Close(); +#ifdef WIN32 + std::wstring wfilename; + if (Utf8ToWindowsFilename(filename, &wfilename)) { + file_ = _wfopen(wfilename.c_str(), ToUtf16(mode).c_str()); + } else { + if (error) { + *error = -1; + return false; + } + } +#else + file_ = fopen(filename.c_str(), mode); +#endif + if (!file_ && error) { + *error = errno; + } + return (file_ != NULL); +} + +bool FileStream::OpenShare(const std::string& filename, const char* mode, + int shflag, int* error) { + Close(); +#ifdef WIN32 + std::wstring wfilename; + if (Utf8ToWindowsFilename(filename, &wfilename)) { + file_ = _wfsopen(wfilename.c_str(), ToUtf16(mode).c_str(), shflag); + if (!file_ && error) { + *error = errno; + return false; + } + return file_ != NULL; + } else { + if (error) { + *error = -1; + } + return false; + } +#else + return Open(filename, mode, error); +#endif +} + +bool FileStream::DisableBuffering() { + if (!file_) + return false; + return (setvbuf(file_, NULL, _IONBF, 0) == 0); +} + +StreamState FileStream::GetState() const { + return (file_ == NULL) ? SS_CLOSED : SS_OPEN; +} + +StreamResult FileStream::Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + if (!file_) + return SR_EOS; + size_t result = fread(buffer, 1, buffer_len, file_); + if ((result == 0) && (buffer_len > 0)) { + if (feof(file_)) + return SR_EOS; + if (error) + *error = errno; + return SR_ERROR; + } + if (read) + *read = result; + return SR_SUCCESS; +} + +StreamResult FileStream::Write(const void* data, size_t data_len, + size_t* written, int* error) { + if (!file_) + return SR_EOS; + size_t result = fwrite(data, 1, data_len, file_); + if ((result == 0) && (data_len > 0)) { + if (error) + *error = errno; + return SR_ERROR; + } + if (written) + *written = result; + return SR_SUCCESS; +} + +void FileStream::Close() { + if (file_) { + DoClose(); + file_ = NULL; + } +} + +bool FileStream::SetPosition(size_t position) { + if (!file_) + return false; + return (fseek(file_, static_cast(position), SEEK_SET) == 0); +} + +bool FileStream::GetPosition(size_t* position) const { + ASSERT(NULL != position); + if (!file_) + return false; + long result = ftell(file_); + if (result < 0) + return false; + if (position) + *position = result; + return true; +} + +bool FileStream::GetSize(size_t* size) const { + ASSERT(NULL != size); + if (!file_) + return false; + struct stat file_stats; + if (fstat(fileno(file_), &file_stats) != 0) + return false; + if (size) + *size = file_stats.st_size; + return true; +} + +bool FileStream::GetAvailable(size_t* size) const { + ASSERT(NULL != size); + if (!GetSize(size)) + return false; + long result = ftell(file_); + if (result < 0) + return false; + if (size) + *size -= result; + return true; +} + +bool FileStream::ReserveSize(size_t size) { + // TODO: extend the file to the proper length + return true; +} + +bool FileStream::GetSize(const std::string& filename, size_t* size) { + struct stat file_stats; + if (stat(filename.c_str(), &file_stats) != 0) + return false; + *size = file_stats.st_size; + return true; +} + +bool FileStream::Flush() { + if (file_) { + return (0 == fflush(file_)); + } + // try to flush empty file? + ASSERT(false); + return false; +} + +#if defined(POSIX) + +bool FileStream::TryLock() { + if (file_ == NULL) { + // Stream not open. + ASSERT(false); + return false; + } + + return flock(fileno(file_), LOCK_EX|LOCK_NB) == 0; +} + +bool FileStream::Unlock() { + if (file_ == NULL) { + // Stream not open. + ASSERT(false); + return false; + } + + return flock(fileno(file_), LOCK_UN) == 0; +} + +#endif + +void FileStream::DoClose() { + fclose(file_); +} + +AsyncWriteStream::~AsyncWriteStream() { + write_thread_->Clear(this, 0, NULL); + ClearBufferAndWrite(); + + CritScope cs(&crit_stream_); + stream_.reset(); +} + +// This is needed by some stream writers, such as RtpDumpWriter. +bool AsyncWriteStream::GetPosition(size_t* position) const { + CritScope cs(&crit_stream_); + return stream_->GetPosition(position); +} + +// This is needed by some stream writers, such as the plugin log writers. +StreamResult AsyncWriteStream::Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + CritScope cs(&crit_stream_); + return stream_->Read(buffer, buffer_len, read, error); +} + +void AsyncWriteStream::Close() { + if (state_ == SS_CLOSED) { + return; + } + + write_thread_->Clear(this, 0, NULL); + ClearBufferAndWrite(); + + CritScope cs(&crit_stream_); + stream_->Close(); + state_ = SS_CLOSED; +} + +StreamResult AsyncWriteStream::Write(const void* data, size_t data_len, + size_t* written, int* error) { + if (state_ == SS_CLOSED) { + return SR_ERROR; + } + + size_t previous_buffer_length = 0; + { + CritScope cs(&crit_buffer_); + previous_buffer_length = buffer_.length(); + buffer_.AppendData(data, data_len); + } + + if (previous_buffer_length == 0) { + // If there's stuff already in the buffer, then we already called + // Post and the write_thread_ hasn't pulled it out yet, so we + // don't need to re-Post. + write_thread_->Post(this, 0, NULL); + } + // Return immediately, assuming that it works. + if (written) { + *written = data_len; + } + return SR_SUCCESS; +} + +void AsyncWriteStream::OnMessage(talk_base::Message* pmsg) { + ClearBufferAndWrite(); +} + +bool AsyncWriteStream::Flush() { + if (state_ == SS_CLOSED) { + return false; + } + + ClearBufferAndWrite(); + + CritScope cs(&crit_stream_); + return stream_->Flush(); +} + +void AsyncWriteStream::ClearBufferAndWrite() { + Buffer to_write; + { + CritScope cs_buffer(&crit_buffer_); + buffer_.TransferTo(&to_write); + } + + if (to_write.length() > 0) { + CritScope cs(&crit_stream_); + stream_->WriteAll(to_write.data(), to_write.length(), NULL, NULL); + } +} + +#ifdef POSIX + +// Have to identically rewrite the FileStream destructor or else it would call +// the base class's Close() instead of the sub-class's. +POpenStream::~POpenStream() { + POpenStream::Close(); +} + +bool POpenStream::Open(const std::string& subcommand, + const char* mode, + int* error) { + Close(); + file_ = popen(subcommand.c_str(), mode); + if (file_ == NULL) { + if (error) + *error = errno; + return false; + } + return true; +} + +bool POpenStream::OpenShare(const std::string& subcommand, const char* mode, + int shflag, int* error) { + return Open(subcommand, mode, error); +} + +void POpenStream::DoClose() { + wait_status_ = pclose(file_); +} + +#endif + +/////////////////////////////////////////////////////////////////////////////// +// MemoryStream +/////////////////////////////////////////////////////////////////////////////// + +MemoryStreamBase::MemoryStreamBase() + : buffer_(NULL), buffer_length_(0), data_length_(0), + seek_position_(0) { +} + +StreamState MemoryStreamBase::GetState() const { + return SS_OPEN; +} + +StreamResult MemoryStreamBase::Read(void* buffer, size_t bytes, + size_t* bytes_read, int* error) { + if (seek_position_ >= data_length_) { + return SR_EOS; + } + size_t available = data_length_ - seek_position_; + if (bytes > available) { + // Read partial buffer + bytes = available; + } + memcpy(buffer, &buffer_[seek_position_], bytes); + seek_position_ += bytes; + if (bytes_read) { + *bytes_read = bytes; + } + return SR_SUCCESS; +} + +StreamResult MemoryStreamBase::Write(const void* buffer, size_t bytes, + size_t* bytes_written, int* error) { + size_t available = buffer_length_ - seek_position_; + if (0 == available) { + // Increase buffer size to the larger of: + // a) new position rounded up to next 256 bytes + // b) double the previous length + size_t new_buffer_length = _max(((seek_position_ + bytes) | 0xFF) + 1, + buffer_length_ * 2); + StreamResult result = DoReserve(new_buffer_length, error); + if (SR_SUCCESS != result) { + return result; + } + ASSERT(buffer_length_ >= new_buffer_length); + available = buffer_length_ - seek_position_; + } + + if (bytes > available) { + bytes = available; + } + memcpy(&buffer_[seek_position_], buffer, bytes); + seek_position_ += bytes; + if (data_length_ < seek_position_) { + data_length_ = seek_position_; + } + if (bytes_written) { + *bytes_written = bytes; + } + return SR_SUCCESS; +} + +void MemoryStreamBase::Close() { + // nothing to do +} + +bool MemoryStreamBase::SetPosition(size_t position) { + if (position > data_length_) + return false; + seek_position_ = position; + return true; +} + +bool MemoryStreamBase::GetPosition(size_t* position) const { + if (position) + *position = seek_position_; + return true; +} + +bool MemoryStreamBase::GetSize(size_t* size) const { + if (size) + *size = data_length_; + return true; +} + +bool MemoryStreamBase::GetAvailable(size_t* size) const { + if (size) + *size = data_length_ - seek_position_; + return true; +} + +bool MemoryStreamBase::ReserveSize(size_t size) { + return (SR_SUCCESS == DoReserve(size, NULL)); +} + +StreamResult MemoryStreamBase::DoReserve(size_t size, int* error) { + return (buffer_length_ >= size) ? SR_SUCCESS : SR_EOS; +} + +/////////////////////////////////////////////////////////////////////////////// + +MemoryStream::MemoryStream() + : buffer_alloc_(NULL) { +} + +MemoryStream::MemoryStream(const char* data) + : buffer_alloc_(NULL) { + SetData(data, strlen(data)); +} + +MemoryStream::MemoryStream(const void* data, size_t length) + : buffer_alloc_(NULL) { + SetData(data, length); +} + +MemoryStream::~MemoryStream() { + delete [] buffer_alloc_; +} + +void MemoryStream::SetData(const void* data, size_t length) { + data_length_ = buffer_length_ = length; + delete [] buffer_alloc_; + buffer_alloc_ = new char[buffer_length_ + kAlignment]; + buffer_ = reinterpret_cast(ALIGNP(buffer_alloc_, kAlignment)); + memcpy(buffer_, data, data_length_); + seek_position_ = 0; +} + +StreamResult MemoryStream::DoReserve(size_t size, int* error) { + if (buffer_length_ >= size) + return SR_SUCCESS; + + if (char* new_buffer_alloc = new char[size + kAlignment]) { + char* new_buffer = reinterpret_cast( + ALIGNP(new_buffer_alloc, kAlignment)); + memcpy(new_buffer, buffer_, data_length_); + delete [] buffer_alloc_; + buffer_alloc_ = new_buffer_alloc; + buffer_ = new_buffer; + buffer_length_ = size; + return SR_SUCCESS; + } + + if (error) { + *error = ENOMEM; + } + return SR_ERROR; +} + +/////////////////////////////////////////////////////////////////////////////// + +ExternalMemoryStream::ExternalMemoryStream() { +} + +ExternalMemoryStream::ExternalMemoryStream(void* data, size_t length) { + SetData(data, length); +} + +ExternalMemoryStream::~ExternalMemoryStream() { +} + +void ExternalMemoryStream::SetData(void* data, size_t length) { + data_length_ = buffer_length_ = length; + buffer_ = static_cast(data); + seek_position_ = 0; +} + +/////////////////////////////////////////////////////////////////////////////// +// FifoBuffer +/////////////////////////////////////////////////////////////////////////////// + +FifoBuffer::FifoBuffer(size_t size) + : state_(SS_OPEN), buffer_(new char[size]), buffer_length_(size), + data_length_(0), read_position_(0), owner_(Thread::Current()) { + // all events are done on the owner_ thread +} + +FifoBuffer::FifoBuffer(size_t size, Thread* owner) + : state_(SS_OPEN), buffer_(new char[size]), buffer_length_(size), + data_length_(0), read_position_(0), owner_(owner) { + // all events are done on the owner_ thread +} + +FifoBuffer::~FifoBuffer() { +} + +bool FifoBuffer::GetBuffered(size_t* size) const { + CritScope cs(&crit_); + *size = data_length_; + return true; +} + +bool FifoBuffer::SetCapacity(size_t size) { + CritScope cs(&crit_); + if (data_length_ > size) { + return false; + } + + if (size != buffer_length_) { + char* buffer = new char[size]; + const size_t copy = data_length_; + const size_t tail_copy = _min(copy, buffer_length_ - read_position_); + memcpy(buffer, &buffer_[read_position_], tail_copy); + memcpy(buffer + tail_copy, &buffer_[0], copy - tail_copy); + buffer_.reset(buffer); + read_position_ = 0; + buffer_length_ = size; + } + return true; +} + +StreamResult FifoBuffer::ReadOffset(void* buffer, size_t bytes, + size_t offset, size_t* bytes_read) { + CritScope cs(&crit_); + return ReadOffsetLocked(buffer, bytes, offset, bytes_read); +} + +StreamResult FifoBuffer::WriteOffset(const void* buffer, size_t bytes, + size_t offset, size_t* bytes_written) { + CritScope cs(&crit_); + return WriteOffsetLocked(buffer, bytes, offset, bytes_written); +} + +StreamState FifoBuffer::GetState() const { + return state_; +} + +StreamResult FifoBuffer::Read(void* buffer, size_t bytes, + size_t* bytes_read, int* error) { + CritScope cs(&crit_); + const bool was_writable = data_length_ < buffer_length_; + size_t copy = 0; + StreamResult result = ReadOffsetLocked(buffer, bytes, 0, ©); + + if (result == SR_SUCCESS) { + // If read was successful then adjust the read position and number of + // bytes buffered. + read_position_ = (read_position_ + copy) % buffer_length_; + data_length_ -= copy; + if (bytes_read) { + *bytes_read = copy; + } + + // if we were full before, and now we're not, post an event + if (!was_writable && copy > 0) { + PostEvent(owner_, SE_WRITE, 0); + } + } + return result; +} + +StreamResult FifoBuffer::Write(const void* buffer, size_t bytes, + size_t* bytes_written, int* error) { + CritScope cs(&crit_); + + const bool was_readable = (data_length_ > 0); + size_t copy = 0; + StreamResult result = WriteOffsetLocked(buffer, bytes, 0, ©); + + if (result == SR_SUCCESS) { + // If write was successful then adjust the number of readable bytes. + data_length_ += copy; + if (bytes_written) { + *bytes_written = copy; + } + + // if we didn't have any data to read before, and now we do, post an event + if (!was_readable && copy > 0) { + PostEvent(owner_, SE_READ, 0); + } + } + return result; +} + +void FifoBuffer::Close() { + CritScope cs(&crit_); + state_ = SS_CLOSED; +} + +const void* FifoBuffer::GetReadData(size_t* size) { + CritScope cs(&crit_); + *size = (read_position_ + data_length_ <= buffer_length_) ? + data_length_ : buffer_length_ - read_position_; + return &buffer_[read_position_]; +} + +void FifoBuffer::ConsumeReadData(size_t size) { + CritScope cs(&crit_); + ASSERT(size <= data_length_); + const bool was_writable = data_length_ < buffer_length_; + read_position_ = (read_position_ + size) % buffer_length_; + data_length_ -= size; + if (!was_writable && size > 0) { + PostEvent(owner_, SE_WRITE, 0); + } +} + +void* FifoBuffer::GetWriteBuffer(size_t* size) { + CritScope cs(&crit_); + if (state_ == SS_CLOSED) { + return NULL; + } + + // if empty, reset the write position to the beginning, so we can get + // the biggest possible block + if (data_length_ == 0) { + read_position_ = 0; + } + + const size_t write_position = (read_position_ + data_length_) + % buffer_length_; + *size = (write_position > read_position_ || data_length_ == 0) ? + buffer_length_ - write_position : read_position_ - write_position; + return &buffer_[write_position]; +} + +void FifoBuffer::ConsumeWriteBuffer(size_t size) { + CritScope cs(&crit_); + ASSERT(size <= buffer_length_ - data_length_); + const bool was_readable = (data_length_ > 0); + data_length_ += size; + if (!was_readable && size > 0) { + PostEvent(owner_, SE_READ, 0); + } +} + +bool FifoBuffer::GetWriteRemaining(size_t* size) const { + CritScope cs(&crit_); + *size = buffer_length_ - data_length_; + return true; +} + +StreamResult FifoBuffer::ReadOffsetLocked(void* buffer, + size_t bytes, + size_t offset, + size_t* bytes_read) { + if (offset >= data_length_) { + return (state_ != SS_CLOSED) ? SR_BLOCK : SR_EOS; + } + + const size_t available = data_length_ - offset; + const size_t read_position = (read_position_ + offset) % buffer_length_; + const size_t copy = _min(bytes, available); + const size_t tail_copy = _min(copy, buffer_length_ - read_position); + char* const p = static_cast(buffer); + memcpy(p, &buffer_[read_position], tail_copy); + memcpy(p + tail_copy, &buffer_[0], copy - tail_copy); + + if (bytes_read) { + *bytes_read = copy; + } + return SR_SUCCESS; +} + +StreamResult FifoBuffer::WriteOffsetLocked(const void* buffer, + size_t bytes, + size_t offset, + size_t* bytes_written) { + if (state_ == SS_CLOSED) { + return SR_EOS; + } + + if (data_length_ + offset >= buffer_length_) { + return SR_BLOCK; + } + + const size_t available = buffer_length_ - data_length_ - offset; + const size_t write_position = (read_position_ + data_length_ + offset) + % buffer_length_; + const size_t copy = _min(bytes, available); + const size_t tail_copy = _min(copy, buffer_length_ - write_position); + const char* const p = static_cast(buffer); + memcpy(&buffer_[write_position], p, tail_copy); + memcpy(&buffer_[0], p + tail_copy, copy - tail_copy); + + if (bytes_written) { + *bytes_written = copy; + } + return SR_SUCCESS; +} + + + +/////////////////////////////////////////////////////////////////////////////// +// LoggingAdapter +/////////////////////////////////////////////////////////////////////////////// + +LoggingAdapter::LoggingAdapter(StreamInterface* stream, LoggingSeverity level, + const std::string& label, bool hex_mode) + : StreamAdapterInterface(stream), level_(level), hex_mode_(hex_mode) { + set_label(label); +} + +void LoggingAdapter::set_label(const std::string& label) { + label_.assign("["); + label_.append(label); + label_.append("]"); +} + +StreamResult LoggingAdapter::Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + size_t local_read; if (!read) read = &local_read; + StreamResult result = StreamAdapterInterface::Read(buffer, buffer_len, read, + error); + if (result == SR_SUCCESS) { + LogMultiline(level_, label_.c_str(), true, buffer, *read, hex_mode_, &lms_); + } + return result; +} + +StreamResult LoggingAdapter::Write(const void* data, size_t data_len, + size_t* written, int* error) { + size_t local_written; + if (!written) written = &local_written; + StreamResult result = StreamAdapterInterface::Write(data, data_len, written, + error); + if (result == SR_SUCCESS) { + LogMultiline(level_, label_.c_str(), false, data, *written, hex_mode_, + &lms_); + } + return result; +} + +void LoggingAdapter::Close() { + LogMultiline(level_, label_.c_str(), false, NULL, 0, hex_mode_, &lms_); + LogMultiline(level_, label_.c_str(), true, NULL, 0, hex_mode_, &lms_); + LOG_V(level_) << label_ << " Closed locally"; + StreamAdapterInterface::Close(); +} + +void LoggingAdapter::OnEvent(StreamInterface* stream, int events, int err) { + if (events & SE_OPEN) { + LOG_V(level_) << label_ << " Open"; + } else if (events & SE_CLOSE) { + LogMultiline(level_, label_.c_str(), false, NULL, 0, hex_mode_, &lms_); + LogMultiline(level_, label_.c_str(), true, NULL, 0, hex_mode_, &lms_); + LOG_V(level_) << label_ << " Closed with error: " << err; + } + StreamAdapterInterface::OnEvent(stream, events, err); +} + +/////////////////////////////////////////////////////////////////////////////// +// StringStream - Reads/Writes to an external std::string +/////////////////////////////////////////////////////////////////////////////// + +StringStream::StringStream(std::string& str) + : str_(str), read_pos_(0), read_only_(false) { +} + +StringStream::StringStream(const std::string& str) + : str_(const_cast(str)), read_pos_(0), read_only_(true) { +} + +StreamState StringStream::GetState() const { + return SS_OPEN; +} + +StreamResult StringStream::Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + size_t available = _min(buffer_len, str_.size() - read_pos_); + if (!available) + return SR_EOS; + memcpy(buffer, str_.data() + read_pos_, available); + read_pos_ += available; + if (read) + *read = available; + return SR_SUCCESS; +} + +StreamResult StringStream::Write(const void* data, size_t data_len, + size_t* written, int* error) { + if (read_only_) { + if (error) { + *error = -1; + } + return SR_ERROR; + } + str_.append(static_cast(data), + static_cast(data) + data_len); + if (written) + *written = data_len; + return SR_SUCCESS; +} + +void StringStream::Close() { +} + +bool StringStream::SetPosition(size_t position) { + if (position > str_.size()) + return false; + read_pos_ = position; + return true; +} + +bool StringStream::GetPosition(size_t* position) const { + if (position) + *position = read_pos_; + return true; +} + +bool StringStream::GetSize(size_t* size) const { + if (size) + *size = str_.size(); + return true; +} + +bool StringStream::GetAvailable(size_t* size) const { + if (size) + *size = str_.size() - read_pos_; + return true; +} + +bool StringStream::ReserveSize(size_t size) { + if (read_only_) + return false; + str_.reserve(size); + return true; +} + +/////////////////////////////////////////////////////////////////////////////// +// StreamReference +/////////////////////////////////////////////////////////////////////////////// + +StreamReference::StreamReference(StreamInterface* stream) + : StreamAdapterInterface(stream, false) { + // owner set to false so the destructor does not free the stream. + stream_ref_count_ = new StreamRefCount(stream); +} + +StreamInterface* StreamReference::NewReference() { + stream_ref_count_->AddReference(); + return new StreamReference(stream_ref_count_, stream()); +} + +StreamReference::~StreamReference() { + stream_ref_count_->Release(); +} + +StreamReference::StreamReference(StreamRefCount* stream_ref_count, + StreamInterface* stream) + : StreamAdapterInterface(stream, false), + stream_ref_count_(stream_ref_count) { +} + +/////////////////////////////////////////////////////////////////////////////// + +StreamResult Flow(StreamInterface* source, + char* buffer, size_t buffer_len, + StreamInterface* sink, + size_t* data_len /* = NULL */) { + ASSERT(buffer_len > 0); + + StreamResult result; + size_t count, read_pos, write_pos; + if (data_len) { + read_pos = *data_len; + } else { + read_pos = 0; + } + + bool end_of_stream = false; + do { + // Read until buffer is full, end of stream, or error + while (!end_of_stream && (read_pos < buffer_len)) { + result = source->Read(buffer + read_pos, buffer_len - read_pos, + &count, NULL); + if (result == SR_EOS) { + end_of_stream = true; + } else if (result != SR_SUCCESS) { + if (data_len) { + *data_len = read_pos; + } + return result; + } else { + read_pos += count; + } + } + + // Write until buffer is empty, or error (including end of stream) + write_pos = 0; + while (write_pos < read_pos) { + result = sink->Write(buffer + write_pos, read_pos - write_pos, + &count, NULL); + if (result != SR_SUCCESS) { + if (data_len) { + *data_len = read_pos - write_pos; + if (write_pos > 0) { + memmove(buffer, buffer + write_pos, *data_len); + } + } + return result; + } + write_pos += count; + } + + read_pos = 0; + } while (!end_of_stream); + + if (data_len) { + *data_len = 0; + } + return SR_SUCCESS; +} + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/stream.h b/talk/base/stream.h new file mode 100644 index 000000000..6700c402d --- /dev/null +++ b/talk/base/stream.h @@ -0,0 +1,807 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_BASE_STREAM_H_ +#define TALK_BASE_STREAM_H_ + +#include "talk/base/basictypes.h" +#include "talk/base/buffer.h" +#include "talk/base/criticalsection.h" +#include "talk/base/logging.h" +#include "talk/base/messagehandler.h" +#include "talk/base/messagequeue.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// StreamInterface is a generic asynchronous stream interface, supporting read, +// write, and close operations, and asynchronous signalling of state changes. +// The interface is designed with file, memory, and socket implementations in +// mind. Some implementations offer extended operations, such as seeking. +/////////////////////////////////////////////////////////////////////////////// + +// The following enumerations are declared outside of the StreamInterface +// class for brevity in use. + +// The SS_OPENING state indicates that the stream will signal open or closed +// in the future. +enum StreamState { SS_CLOSED, SS_OPENING, SS_OPEN }; + +// Stream read/write methods return this value to indicate various success +// and failure conditions described below. +enum StreamResult { SR_ERROR, SR_SUCCESS, SR_BLOCK, SR_EOS }; + +// StreamEvents are used to asynchronously signal state transitionss. The flags +// may be combined. +// SE_OPEN: The stream has transitioned to the SS_OPEN state +// SE_CLOSE: The stream has transitioned to the SS_CLOSED state +// SE_READ: Data is available, so Read is likely to not return SR_BLOCK +// SE_WRITE: Data can be written, so Write is likely to not return SR_BLOCK +enum StreamEvent { SE_OPEN = 1, SE_READ = 2, SE_WRITE = 4, SE_CLOSE = 8 }; + +class Thread; + +struct StreamEventData : public MessageData { + int events, error; + StreamEventData(int ev, int er) : events(ev), error(er) { } +}; + +class StreamInterface : public MessageHandler { + public: + enum { + MSG_POST_EVENT = 0xF1F1, MSG_MAX = MSG_POST_EVENT + }; + + virtual ~StreamInterface(); + + virtual StreamState GetState() const = 0; + + // Read attempts to fill buffer of size buffer_len. Write attempts to send + // data_len bytes stored in data. The variables read and write are set only + // on SR_SUCCESS (see below). Likewise, error is only set on SR_ERROR. + // Read and Write return a value indicating: + // SR_ERROR: an error occurred, which is returned in a non-null error + // argument. Interpretation of the error requires knowledge of the + // stream's concrete type, which limits its usefulness. + // SR_SUCCESS: some number of bytes were successfully written, which is + // returned in a non-null read/write argument. + // SR_BLOCK: the stream is in non-blocking mode, and the operation would + // block, or the stream is in SS_OPENING state. + // SR_EOS: the end-of-stream has been reached, or the stream is in the + // SS_CLOSED state. + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error) = 0; + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error) = 0; + // Attempt to transition to the SS_CLOSED state. SE_CLOSE will not be + // signalled as a result of this call. + virtual void Close() = 0; + + // Streams may signal one or more StreamEvents to indicate state changes. + // The first argument identifies the stream on which the state change occured. + // The second argument is a bit-wise combination of StreamEvents. + // If SE_CLOSE is signalled, then the third argument is the associated error + // code. Otherwise, the value is undefined. + // Note: Not all streams will support asynchronous event signalling. However, + // SS_OPENING and SR_BLOCK returned from stream member functions imply that + // certain events will be raised in the future. + sigslot::signal3 SignalEvent; + + // Like calling SignalEvent, but posts a message to the specified thread, + // which will call SignalEvent. This helps unroll the stack and prevent + // re-entrancy. + void PostEvent(Thread* t, int events, int err); + // Like the aforementioned method, but posts to the current thread. + void PostEvent(int events, int err); + + // + // OPTIONAL OPERATIONS + // + // Not all implementations will support the following operations. In general, + // a stream will only support an operation if it reasonably efficient to do + // so. For example, while a socket could buffer incoming data to support + // seeking, it will not do so. Instead, a buffering stream adapter should + // be used. + // + // Even though several of these operations are related, you should + // always use whichever operation is most relevant. For example, you may + // be tempted to use GetSize() and GetPosition() to deduce the result of + // GetAvailable(). However, a stream which is read-once may support the + // latter operation but not the former. + // + + // The following four methods are used to avoid copying data multiple times. + + // GetReadData returns a pointer to a buffer which is owned by the stream. + // The buffer contains data_len bytes. NULL is returned if no data is + // available, or if the method fails. If the caller processes the data, it + // must call ConsumeReadData with the number of processed bytes. GetReadData + // does not require a matching call to ConsumeReadData if the data is not + // processed. Read and ConsumeReadData invalidate the buffer returned by + // GetReadData. + virtual const void* GetReadData(size_t* data_len) { return NULL; } + virtual void ConsumeReadData(size_t used) {} + + // GetWriteBuffer returns a pointer to a buffer which is owned by the stream. + // The buffer has a capacity of buf_len bytes. NULL is returned if there is + // no buffer available, or if the method fails. The call may write data to + // the buffer, and then call ConsumeWriteBuffer with the number of bytes + // written. GetWriteBuffer does not require a matching call to + // ConsumeWriteData if no data is written. Write, ForceWrite, and + // ConsumeWriteData invalidate the buffer returned by GetWriteBuffer. + // TODO: Allow the caller to specify a minimum buffer size. If the specified + // amount of buffer is not yet available, return NULL and Signal SE_WRITE + // when it is available. If the requested amount is too large, return an + // error. + virtual void* GetWriteBuffer(size_t* buf_len) { return NULL; } + virtual void ConsumeWriteBuffer(size_t used) {} + + // Write data_len bytes found in data, circumventing any throttling which + // would could cause SR_BLOCK to be returned. Returns true if all the data + // was written. Otherwise, the method is unsupported, or an unrecoverable + // error occurred, and the error value is set. This method should be used + // sparingly to write critical data which should not be throttled. A stream + // which cannot circumvent its blocking constraints should not implement this + // method. + // NOTE: This interface is being considered experimentally at the moment. It + // would be used by JUDP and BandwidthStream as a way to circumvent certain + // soft limits in writing. + //virtual bool ForceWrite(const void* data, size_t data_len, int* error) { + // if (error) *error = -1; + // return false; + //} + + // Seek to a byte offset from the beginning of the stream. Returns false if + // the stream does not support seeking, or cannot seek to the specified + // position. + virtual bool SetPosition(size_t position) { return false; } + + // Get the byte offset of the current position from the start of the stream. + // Returns false if the position is not known. + virtual bool GetPosition(size_t* position) const { return false; } + + // Get the byte length of the entire stream. Returns false if the length + // is not known. + virtual bool GetSize(size_t* size) const { return false; } + + // Return the number of Read()-able bytes remaining before end-of-stream. + // Returns false if not known. + virtual bool GetAvailable(size_t* size) const { return false; } + + // Return the number of Write()-able bytes remaining before end-of-stream. + // Returns false if not known. + virtual bool GetWriteRemaining(size_t* size) const { return false; } + + // Return true if flush is successful. + virtual bool Flush() { return false; } + + // Communicates the amount of data which will be written to the stream. The + // stream may choose to preallocate memory to accomodate this data. The + // stream may return false to indicate that there is not enough room (ie, + // Write will return SR_EOS/SR_ERROR at some point). Note that calling this + // function should not affect the existing state of data in the stream. + virtual bool ReserveSize(size_t size) { return true; } + + // + // CONVENIENCE METHODS + // + // These methods are implemented in terms of other methods, for convenience. + // + + // Seek to the start of the stream. + inline bool Rewind() { return SetPosition(0); } + + // WriteAll is a helper function which repeatedly calls Write until all the + // data is written, or something other than SR_SUCCESS is returned. Note that + // unlike Write, the argument 'written' is always set, and may be non-zero + // on results other than SR_SUCCESS. The remaining arguments have the + // same semantics as Write. + StreamResult WriteAll(const void* data, size_t data_len, + size_t* written, int* error); + + // Similar to ReadAll. Calls Read until buffer_len bytes have been read, or + // until a non-SR_SUCCESS result is returned. 'read' is always set. + StreamResult ReadAll(void* buffer, size_t buffer_len, + size_t* read, int* error); + + // ReadLine is a helper function which repeatedly calls Read until it hits + // the end-of-line character, or something other than SR_SUCCESS. + // TODO: this is too inefficient to keep here. Break this out into a buffered + // readline object or adapter + StreamResult ReadLine(std::string* line); + + protected: + StreamInterface(); + + // MessageHandler Interface + virtual void OnMessage(Message* msg); + + private: + DISALLOW_EVIL_CONSTRUCTORS(StreamInterface); +}; + +/////////////////////////////////////////////////////////////////////////////// +// StreamAdapterInterface is a convenient base-class for adapting a stream. +// By default, all operations are pass-through. Override the methods that you +// require adaptation. Streams should really be upgraded to reference-counted. +// In the meantime, use the owned flag to indicate whether the adapter should +// own the adapted stream. +/////////////////////////////////////////////////////////////////////////////// + +class StreamAdapterInterface : public StreamInterface, + public sigslot::has_slots<> { + public: + explicit StreamAdapterInterface(StreamInterface* stream, bool owned = true); + + // Core Stream Interface + virtual StreamState GetState() const { + return stream_->GetState(); + } + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + return stream_->Read(buffer, buffer_len, read, error); + } + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error) { + return stream_->Write(data, data_len, written, error); + } + virtual void Close() { + stream_->Close(); + } + + // Optional Stream Interface + /* Note: Many stream adapters were implemented prior to this Read/Write + interface. Therefore, a simple pass through of data in those cases may + be broken. At a later time, we should do a once-over pass of all + adapters, and make them compliant with these interfaces, after which this + code can be uncommented. + virtual const void* GetReadData(size_t* data_len) { + return stream_->GetReadData(data_len); + } + virtual void ConsumeReadData(size_t used) { + stream_->ConsumeReadData(used); + } + + virtual void* GetWriteBuffer(size_t* buf_len) { + return stream_->GetWriteBuffer(buf_len); + } + virtual void ConsumeWriteBuffer(size_t used) { + stream_->ConsumeWriteBuffer(used); + } + */ + + /* Note: This interface is currently undergoing evaluation. + virtual bool ForceWrite(const void* data, size_t data_len, int* error) { + return stream_->ForceWrite(data, data_len, error); + } + */ + + virtual bool SetPosition(size_t position) { + return stream_->SetPosition(position); + } + virtual bool GetPosition(size_t* position) const { + return stream_->GetPosition(position); + } + virtual bool GetSize(size_t* size) const { + return stream_->GetSize(size); + } + virtual bool GetAvailable(size_t* size) const { + return stream_->GetAvailable(size); + } + virtual bool GetWriteRemaining(size_t* size) const { + return stream_->GetWriteRemaining(size); + } + virtual bool ReserveSize(size_t size) { + return stream_->ReserveSize(size); + } + virtual bool Flush() { + return stream_->Flush(); + } + + void Attach(StreamInterface* stream, bool owned = true); + StreamInterface* Detach(); + + protected: + virtual ~StreamAdapterInterface(); + + // Note that the adapter presents itself as the origin of the stream events, + // since users of the adapter may not recognize the adapted object. + virtual void OnEvent(StreamInterface* stream, int events, int err) { + SignalEvent(this, events, err); + } + StreamInterface* stream() { return stream_; } + + private: + StreamInterface* stream_; + bool owned_; + DISALLOW_EVIL_CONSTRUCTORS(StreamAdapterInterface); +}; + +/////////////////////////////////////////////////////////////////////////////// +// StreamTap is a non-modifying, pass-through adapter, which copies all data +// in either direction to the tap. Note that errors or blocking on writing to +// the tap will prevent further tap writes from occurring. +/////////////////////////////////////////////////////////////////////////////// + +class StreamTap : public StreamAdapterInterface { + public: + explicit StreamTap(StreamInterface* stream, StreamInterface* tap); + + void AttachTap(StreamInterface* tap); + StreamInterface* DetachTap(); + StreamResult GetTapResult(int* error); + + // StreamAdapterInterface Interface + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error); + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + + private: + scoped_ptr tap_; + StreamResult tap_result_; + int tap_error_; + DISALLOW_EVIL_CONSTRUCTORS(StreamTap); +}; + +/////////////////////////////////////////////////////////////////////////////// +// StreamSegment adapts a read stream, to expose a subset of the adapted +// stream's data. This is useful for cases where a stream contains multiple +// documents concatenated together. StreamSegment can expose a subset of +// the data as an independent stream, including support for rewinding and +// seeking. +/////////////////////////////////////////////////////////////////////////////// + +class StreamSegment : public StreamAdapterInterface { + public: + // The current position of the adapted stream becomes the beginning of the + // segment. If a length is specified, it bounds the length of the segment. + explicit StreamSegment(StreamInterface* stream); + explicit StreamSegment(StreamInterface* stream, size_t length); + + // StreamAdapterInterface Interface + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error); + virtual bool SetPosition(size_t position); + virtual bool GetPosition(size_t* position) const; + virtual bool GetSize(size_t* size) const; + virtual bool GetAvailable(size_t* size) const; + + private: + size_t start_, pos_, length_; + DISALLOW_EVIL_CONSTRUCTORS(StreamSegment); +}; + +/////////////////////////////////////////////////////////////////////////////// +// NullStream gives errors on read, and silently discards all written data. +/////////////////////////////////////////////////////////////////////////////// + +class NullStream : public StreamInterface { + public: + NullStream(); + virtual ~NullStream(); + + // StreamInterface Interface + virtual StreamState GetState() const; + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error); + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + virtual void Close(); +}; + +/////////////////////////////////////////////////////////////////////////////// +// FileStream is a simple implementation of a StreamInterface, which does not +// support asynchronous notification. +/////////////////////////////////////////////////////////////////////////////// + +class FileStream : public StreamInterface { + public: + FileStream(); + virtual ~FileStream(); + + // The semantics of filename and mode are the same as stdio's fopen + virtual bool Open(const std::string& filename, const char* mode, int* error); + virtual bool OpenShare(const std::string& filename, const char* mode, + int shflag, int* error); + + // By default, reads and writes are buffered for efficiency. Disabling + // buffering causes writes to block until the bytes on disk are updated. + virtual bool DisableBuffering(); + + virtual StreamState GetState() const; + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error); + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + virtual void Close(); + virtual bool SetPosition(size_t position); + virtual bool GetPosition(size_t* position) const; + virtual bool GetSize(size_t* size) const; + virtual bool GetAvailable(size_t* size) const; + virtual bool ReserveSize(size_t size); + + virtual bool Flush(); + +#if defined(POSIX) + // Tries to aquire an exclusive lock on the file. + // Use OpenShare(...) on win32 to get similar functionality. + bool TryLock(); + bool Unlock(); +#endif + + // Note: Deprecated in favor of Filesystem::GetFileSize(). + static bool GetSize(const std::string& filename, size_t* size); + + protected: + virtual void DoClose(); + + FILE* file_; + + private: + DISALLOW_EVIL_CONSTRUCTORS(FileStream); +}; + + +// A stream which pushes writes onto a separate thread and +// returns from the write call immediately. +class AsyncWriteStream : public StreamInterface { + public: + // Takes ownership of the stream, but not the thread. + AsyncWriteStream(StreamInterface* stream, talk_base::Thread* write_thread) + : stream_(stream), + write_thread_(write_thread), + state_(stream ? stream->GetState() : SS_CLOSED) { + } + + virtual ~AsyncWriteStream(); + + // StreamInterface Interface + virtual StreamState GetState() const { return state_; } + // This is needed by some stream writers, such as RtpDumpWriter. + virtual bool GetPosition(size_t* position) const; + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error); + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + virtual void Close(); + virtual bool Flush(); + + protected: + // From MessageHandler + virtual void OnMessage(talk_base::Message* pmsg); + virtual void ClearBufferAndWrite(); + + private: + talk_base::scoped_ptr stream_; + Thread* write_thread_; + StreamState state_; + Buffer buffer_; + mutable CriticalSection crit_stream_; + CriticalSection crit_buffer_; + + DISALLOW_EVIL_CONSTRUCTORS(AsyncWriteStream); +}; + + +#ifdef POSIX +// A FileStream that is actually not a file, but the output or input of a +// sub-command. See "man 3 popen" for documentation of the underlying OS popen() +// function. +class POpenStream : public FileStream { + public: + POpenStream() : wait_status_(-1) {} + virtual ~POpenStream(); + + virtual bool Open(const std::string& subcommand, const char* mode, + int* error); + // Same as Open(). shflag is ignored. + virtual bool OpenShare(const std::string& subcommand, const char* mode, + int shflag, int* error); + + // Returns the wait status from the last Close() of an Open()'ed stream, or + // -1 if no Open()+Close() has been done on this object. Meaning of the number + // is documented in "man 2 wait". + int GetWaitStatus() const { return wait_status_; } + + protected: + virtual void DoClose(); + + private: + int wait_status_; +}; +#endif // POSIX + +/////////////////////////////////////////////////////////////////////////////// +// MemoryStream is a simple implementation of a StreamInterface over in-memory +// data. Data is read and written at the current seek position. Reads return +// end-of-stream when they reach the end of data. Writes actually extend the +// end of data mark. +/////////////////////////////////////////////////////////////////////////////// + +class MemoryStreamBase : public StreamInterface { + public: + virtual StreamState GetState() const; + virtual StreamResult Read(void* buffer, size_t bytes, size_t* bytes_read, + int* error); + virtual StreamResult Write(const void* buffer, size_t bytes, + size_t* bytes_written, int* error); + virtual void Close(); + virtual bool SetPosition(size_t position); + virtual bool GetPosition(size_t* position) const; + virtual bool GetSize(size_t* size) const; + virtual bool GetAvailable(size_t* size) const; + virtual bool ReserveSize(size_t size); + + char* GetBuffer() { return buffer_; } + const char* GetBuffer() const { return buffer_; } + + protected: + MemoryStreamBase(); + + virtual StreamResult DoReserve(size_t size, int* error); + + // Invariant: 0 <= seek_position <= data_length_ <= buffer_length_ + char* buffer_; + size_t buffer_length_; + size_t data_length_; + size_t seek_position_; + + private: + DISALLOW_EVIL_CONSTRUCTORS(MemoryStreamBase); +}; + +// MemoryStream dynamically resizes to accomodate written data. + +class MemoryStream : public MemoryStreamBase { + public: + MemoryStream(); + explicit MemoryStream(const char* data); // Calls SetData(data, strlen(data)) + MemoryStream(const void* data, size_t length); // Calls SetData(data, length) + virtual ~MemoryStream(); + + void SetData(const void* data, size_t length); + + protected: + virtual StreamResult DoReserve(size_t size, int* error); + // Memory Streams are aligned for efficiency. + static const int kAlignment = 16; + char* buffer_alloc_; +}; + +// ExternalMemoryStream adapts an external memory buffer, so writes which would +// extend past the end of the buffer will return end-of-stream. + +class ExternalMemoryStream : public MemoryStreamBase { + public: + ExternalMemoryStream(); + ExternalMemoryStream(void* data, size_t length); + virtual ~ExternalMemoryStream(); + + void SetData(void* data, size_t length); +}; + +// FifoBuffer allows for efficient, thread-safe buffering of data between +// writer and reader. As the data can wrap around the end of the buffer, +// MemoryStreamBase can't help us here. + +class FifoBuffer : public StreamInterface { + public: + // Creates a FIFO buffer with the specified capacity. + explicit FifoBuffer(size_t length); + // Creates a FIFO buffer with the specified capacity and owner + FifoBuffer(size_t length, Thread* owner); + virtual ~FifoBuffer(); + // Gets the amount of data currently readable from the buffer. + bool GetBuffered(size_t* data_len) const; + // Resizes the buffer to the specified capacity. Fails if data_length_ > size + bool SetCapacity(size_t length); + + // Read into |buffer| with an offset from the current read position, offset + // is specified in number of bytes. + // This method doesn't adjust read position nor the number of available + // bytes, user has to call ConsumeReadData() to do this. + StreamResult ReadOffset(void* buffer, size_t bytes, size_t offset, + size_t* bytes_read); + + // Write |buffer| with an offset from the current write position, offset is + // specified in number of bytes. + // This method doesn't adjust the number of buffered bytes, user has to call + // ConsumeWriteBuffer() to do this. + StreamResult WriteOffset(const void* buffer, size_t bytes, size_t offset, + size_t* bytes_written); + + // StreamInterface methods + virtual StreamState GetState() const; + virtual StreamResult Read(void* buffer, size_t bytes, + size_t* bytes_read, int* error); + virtual StreamResult Write(const void* buffer, size_t bytes, + size_t* bytes_written, int* error); + virtual void Close(); + virtual const void* GetReadData(size_t* data_len); + virtual void ConsumeReadData(size_t used); + virtual void* GetWriteBuffer(size_t* buf_len); + virtual void ConsumeWriteBuffer(size_t used); + virtual bool GetWriteRemaining(size_t* size) const; + + private: + // Helper method that implements ReadOffset. Caller must acquire a lock + // when calling this method. + StreamResult ReadOffsetLocked(void* buffer, size_t bytes, size_t offset, + size_t* bytes_read); + + // Helper method that implements WriteOffset. Caller must acquire a lock + // when calling this method. + StreamResult WriteOffsetLocked(const void* buffer, size_t bytes, + size_t offset, size_t* bytes_written); + + StreamState state_; // keeps the opened/closed state of the stream + scoped_array buffer_; // the allocated buffer + size_t buffer_length_; // size of the allocated buffer + size_t data_length_; // amount of readable data in the buffer + size_t read_position_; // offset to the readable data + Thread* owner_; // stream callbacks are dispatched on this thread + mutable CriticalSection crit_; // object lock + DISALLOW_EVIL_CONSTRUCTORS(FifoBuffer); +}; + +/////////////////////////////////////////////////////////////////////////////// + +class LoggingAdapter : public StreamAdapterInterface { + public: + LoggingAdapter(StreamInterface* stream, LoggingSeverity level, + const std::string& label, bool hex_mode = false); + + void set_label(const std::string& label); + + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error); + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + virtual void Close(); + + protected: + virtual void OnEvent(StreamInterface* stream, int events, int err); + + private: + LoggingSeverity level_; + std::string label_; + bool hex_mode_; + LogMultilineState lms_; + + DISALLOW_EVIL_CONSTRUCTORS(LoggingAdapter); +}; + +/////////////////////////////////////////////////////////////////////////////// +// StringStream - Reads/Writes to an external std::string +/////////////////////////////////////////////////////////////////////////////// + +class StringStream : public StreamInterface { + public: + explicit StringStream(std::string& str); + explicit StringStream(const std::string& str); + + virtual StreamState GetState() const; + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error); + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + virtual void Close(); + virtual bool SetPosition(size_t position); + virtual bool GetPosition(size_t* position) const; + virtual bool GetSize(size_t* size) const; + virtual bool GetAvailable(size_t* size) const; + virtual bool ReserveSize(size_t size); + + private: + std::string& str_; + size_t read_pos_; + bool read_only_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// StreamReference - A reference counting stream adapter +/////////////////////////////////////////////////////////////////////////////// + +// Keep in mind that the streams and adapters defined in this file are +// not thread-safe, so this has limited uses. + +// A StreamRefCount holds the reference count and a pointer to the +// wrapped stream. It deletes the wrapped stream when there are no +// more references. We can then have multiple StreamReference +// instances pointing to one StreamRefCount, all wrapping the same +// stream. + +class StreamReference : public StreamAdapterInterface { + class StreamRefCount; + public: + // Constructor for the first reference to a stream + // Note: get more references through NewReference(). Use this + // constructor only once on a given stream. + explicit StreamReference(StreamInterface* stream); + StreamInterface* GetStream() { return stream(); } + StreamInterface* NewReference(); + virtual ~StreamReference(); + + private: + class StreamRefCount { + public: + explicit StreamRefCount(StreamInterface* stream) + : stream_(stream), ref_count_(1) { + } + void AddReference() { + CritScope lock(&cs_); + ++ref_count_; + } + void Release() { + int ref_count; + { // Atomic ops would have been a better fit here. + CritScope lock(&cs_); + ref_count = --ref_count_; + } + if (ref_count == 0) { + delete stream_; + delete this; + } + } + private: + StreamInterface* stream_; + int ref_count_; + CriticalSection cs_; + DISALLOW_EVIL_CONSTRUCTORS(StreamRefCount); + }; + + // Constructor for adding references + explicit StreamReference(StreamRefCount* stream_ref_count, + StreamInterface* stream); + + StreamRefCount* stream_ref_count_; + DISALLOW_EVIL_CONSTRUCTORS(StreamReference); +}; + +/////////////////////////////////////////////////////////////////////////////// + +// Flow attempts to move bytes from source to sink via buffer of size +// buffer_len. The function returns SR_SUCCESS when source reaches +// end-of-stream (returns SR_EOS), and all the data has been written successful +// to sink. Alternately, if source returns SR_BLOCK or SR_ERROR, or if sink +// returns SR_BLOCK, SR_ERROR, or SR_EOS, then the function immediately returns +// with the unexpected StreamResult value. +// data_len is the length of the valid data in buffer. in case of error +// this is the data that read from source but can't move to destination. +// as a pass in parameter, it indicates data in buffer that should move to sink +StreamResult Flow(StreamInterface* source, + char* buffer, size_t buffer_len, + StreamInterface* sink, size_t* data_len = NULL); + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_STREAM_H_ diff --git a/talk/base/stream_unittest.cc b/talk/base/stream_unittest.cc new file mode 100644 index 000000000..856c943fc --- /dev/null +++ b/talk/base/stream_unittest.cc @@ -0,0 +1,509 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/gunit.h" +#include "talk/base/stream.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// TestStream +/////////////////////////////////////////////////////////////////////////////// + +class TestStream : public StreamInterface { + public: + TestStream() : pos_(0) { } + + virtual StreamState GetState() const { return SS_OPEN; } + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + unsigned char* uc_buffer = static_cast(buffer); + for (size_t i = 0; i < buffer_len; ++i) { + uc_buffer[i] = static_cast(pos_++); + } + if (read) + *read = buffer_len; + return SR_SUCCESS; + } + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error) { + if (error) + *error = -1; + return SR_ERROR; + } + virtual void Close() { } + virtual bool SetPosition(size_t position) { + pos_ = position; + return true; + } + virtual bool GetPosition(size_t* position) const { + if (position) *position = pos_; + return true; + } + virtual bool GetSize(size_t* size) const { + return false; + } + virtual bool GetAvailable(size_t* size) const { + return false; + } + + private: + size_t pos_; +}; + +bool VerifyTestBuffer(unsigned char* buffer, size_t len, + unsigned char value) { + bool passed = true; + for (size_t i = 0; i < len; ++i) { + if (buffer[i] != value++) { + passed = false; + break; + } + } + // Ensure that we don't pass again without re-writing + memset(buffer, 0, len); + return passed; +} + +void SeekTest(StreamInterface* stream, const unsigned char value) { + size_t bytes; + unsigned char buffer[13] = { 0 }; + const size_t kBufSize = sizeof(buffer); + + EXPECT_EQ(stream->Read(buffer, kBufSize, &bytes, NULL), SR_SUCCESS); + EXPECT_EQ(bytes, kBufSize); + EXPECT_TRUE(VerifyTestBuffer(buffer, kBufSize, value)); + EXPECT_TRUE(stream->GetPosition(&bytes)); + EXPECT_EQ(13U, bytes); + + EXPECT_TRUE(stream->SetPosition(7)); + + EXPECT_EQ(stream->Read(buffer, kBufSize, &bytes, NULL), SR_SUCCESS); + EXPECT_EQ(bytes, kBufSize); + EXPECT_TRUE(VerifyTestBuffer(buffer, kBufSize, value + 7)); + EXPECT_TRUE(stream->GetPosition(&bytes)); + EXPECT_EQ(20U, bytes); +} + +TEST(StreamSegment, TranslatesPosition) { + TestStream* test = new TestStream; + // Verify behavior of original stream + SeekTest(test, 0); + StreamSegment* segment = new StreamSegment(test); + // Verify behavior of adapted stream (all values offset by 20) + SeekTest(segment, 20); + delete segment; +} + +TEST(StreamSegment, SupportsArtificialTermination) { + TestStream* test = new TestStream; + + size_t bytes; + unsigned char buffer[5000] = { 0 }; + const size_t kBufSize = sizeof(buffer); + + { + StreamInterface* stream = test; + + // Read a lot of bytes + EXPECT_EQ(stream->Read(buffer, kBufSize, &bytes, NULL), SR_SUCCESS); + EXPECT_EQ(bytes, kBufSize); + EXPECT_TRUE(VerifyTestBuffer(buffer, kBufSize, 0)); + + // Test seeking far ahead + EXPECT_TRUE(stream->SetPosition(12345)); + + // Read a bunch more bytes + EXPECT_EQ(stream->Read(buffer, kBufSize, &bytes, NULL), SR_SUCCESS); + EXPECT_EQ(bytes, kBufSize); + EXPECT_TRUE(VerifyTestBuffer(buffer, kBufSize, 12345 % 256)); + } + + // Create a segment of test stream in range [100,600) + EXPECT_TRUE(test->SetPosition(100)); + StreamSegment* segment = new StreamSegment(test, 500); + + { + StreamInterface* stream = segment; + + EXPECT_EQ(stream->Read(buffer, kBufSize, &bytes, NULL), SR_SUCCESS); + EXPECT_EQ(500U, bytes); + EXPECT_TRUE(VerifyTestBuffer(buffer, 500, 100)); + EXPECT_EQ(stream->Read(buffer, kBufSize, &bytes, NULL), SR_EOS); + + // Test seeking past "end" of stream + EXPECT_FALSE(stream->SetPosition(12345)); + EXPECT_FALSE(stream->SetPosition(501)); + + // Test seeking to end (edge case) + EXPECT_TRUE(stream->SetPosition(500)); + EXPECT_EQ(stream->Read(buffer, kBufSize, &bytes, NULL), SR_EOS); + + // Test seeking to start + EXPECT_TRUE(stream->SetPosition(0)); + EXPECT_EQ(stream->Read(buffer, kBufSize, &bytes, NULL), SR_SUCCESS); + EXPECT_EQ(500U, bytes); + EXPECT_TRUE(VerifyTestBuffer(buffer, 500, 100)); + EXPECT_EQ(stream->Read(buffer, kBufSize, &bytes, NULL), SR_EOS); + } + + delete segment; +} + +TEST(FifoBufferTest, TestAll) { + const size_t kSize = 16; + const char in[kSize * 2 + 1] = "0123456789ABCDEFGHIJKLMNOPQRSTUV"; + char out[kSize * 2]; + void* p; + const void* q; + size_t bytes; + FifoBuffer buf(kSize); + StreamInterface* stream = &buf; + + // Test assumptions about base state + EXPECT_EQ(SS_OPEN, stream->GetState()); + EXPECT_EQ(SR_BLOCK, stream->Read(out, kSize, &bytes, NULL)); + EXPECT_TRUE(NULL != stream->GetReadData(&bytes)); + EXPECT_EQ((size_t)0, bytes); + stream->ConsumeReadData(0); + EXPECT_TRUE(NULL != stream->GetWriteBuffer(&bytes)); + EXPECT_EQ(kSize, bytes); + stream->ConsumeWriteBuffer(0); + + // Try a full write + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize, &bytes, NULL)); + EXPECT_EQ(kSize, bytes); + + // Try a write that should block + EXPECT_EQ(SR_BLOCK, stream->Write(in, kSize, &bytes, NULL)); + + // Try a full read + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize, &bytes, NULL)); + EXPECT_EQ(kSize, bytes); + EXPECT_EQ(0, memcmp(in, out, kSize)); + + // Try a read that should block + EXPECT_EQ(SR_BLOCK, stream->Read(out, kSize, &bytes, NULL)); + + // Try a too-big write + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize * 2, &bytes, NULL)); + EXPECT_EQ(bytes, kSize); + + // Try a too-big read + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize * 2, &bytes, NULL)); + EXPECT_EQ(kSize, bytes); + EXPECT_EQ(0, memcmp(in, out, kSize)); + + // Try some small writes and reads + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize / 2, &bytes, NULL)); + EXPECT_EQ(kSize / 2, bytes); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize / 2, &bytes, NULL)); + EXPECT_EQ(kSize / 2, bytes); + EXPECT_EQ(0, memcmp(in, out, kSize / 2)); + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize / 2, &bytes, NULL)); + EXPECT_EQ(kSize / 2, bytes); + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize / 2, &bytes, NULL)); + EXPECT_EQ(kSize / 2, bytes); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize / 2, &bytes, NULL)); + EXPECT_EQ(kSize / 2, bytes); + EXPECT_EQ(0, memcmp(in, out, kSize / 2)); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize / 2, &bytes, NULL)); + EXPECT_EQ(kSize / 2, bytes); + EXPECT_EQ(0, memcmp(in, out, kSize / 2)); + + // Try wraparound reads and writes in the following pattern + // WWWWWWWWWWWW.... 0123456789AB.... + // RRRRRRRRXXXX.... ........89AB.... + // WWWW....XXXXWWWW 4567....89AB0123 + // XXXX....RRRRXXXX 4567........0123 + // XXXXWWWWWWWWXXXX 4567012345670123 + // RRRRXXXXXXXXRRRR ....01234567.... + // ....RRRRRRRR.... ................ + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize * 3 / 4, &bytes, NULL)); + EXPECT_EQ(kSize * 3 / 4, bytes); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize / 2, &bytes, NULL)); + EXPECT_EQ(kSize / 2, bytes); + EXPECT_EQ(0, memcmp(in, out, kSize / 2)); + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize / 2, &bytes, NULL)); + EXPECT_EQ(kSize / 2, bytes); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize / 4, &bytes, NULL)); + EXPECT_EQ(kSize / 4 , bytes); + EXPECT_EQ(0, memcmp(in + kSize / 2, out, kSize / 4)); + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize / 2, &bytes, NULL)); + EXPECT_EQ(kSize / 2, bytes); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize / 2, &bytes, NULL)); + EXPECT_EQ(kSize / 2 , bytes); + EXPECT_EQ(0, memcmp(in, out, kSize / 2)); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize / 2, &bytes, NULL)); + EXPECT_EQ(kSize / 2 , bytes); + EXPECT_EQ(0, memcmp(in, out, kSize / 2)); + + // Use GetWriteBuffer to reset the read_position for the next tests + stream->GetWriteBuffer(&bytes); + stream->ConsumeWriteBuffer(0); + + // Try using GetReadData to do a full read + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize, &bytes, NULL)); + q = stream->GetReadData(&bytes); + EXPECT_TRUE(NULL != q); + EXPECT_EQ(kSize, bytes); + EXPECT_EQ(0, memcmp(q, in, kSize)); + stream->ConsumeReadData(kSize); + EXPECT_EQ(SR_BLOCK, stream->Read(out, kSize, &bytes, NULL)); + + // Try using GetReadData to do some small reads + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize, &bytes, NULL)); + q = stream->GetReadData(&bytes); + EXPECT_TRUE(NULL != q); + EXPECT_EQ(kSize, bytes); + EXPECT_EQ(0, memcmp(q, in, kSize / 2)); + stream->ConsumeReadData(kSize / 2); + q = stream->GetReadData(&bytes); + EXPECT_TRUE(NULL != q); + EXPECT_EQ(kSize / 2, bytes); + EXPECT_EQ(0, memcmp(q, in + kSize / 2, kSize / 2)); + stream->ConsumeReadData(kSize / 2); + EXPECT_EQ(SR_BLOCK, stream->Read(out, kSize, &bytes, NULL)); + + // Try using GetReadData in a wraparound case + // WWWWWWWWWWWWWWWW 0123456789ABCDEF + // RRRRRRRRRRRRXXXX ............CDEF + // WWWWWWWW....XXXX 01234567....CDEF + // ............RRRR 01234567........ + // RRRRRRRR........ ................ + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize, &bytes, NULL)); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize * 3 / 4, &bytes, NULL)); + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize / 2, &bytes, NULL)); + q = stream->GetReadData(&bytes); + EXPECT_TRUE(NULL != q); + EXPECT_EQ(kSize / 4, bytes); + EXPECT_EQ(0, memcmp(q, in + kSize * 3 / 4, kSize / 4)); + stream->ConsumeReadData(kSize / 4); + q = stream->GetReadData(&bytes); + EXPECT_TRUE(NULL != q); + EXPECT_EQ(kSize / 2, bytes); + EXPECT_EQ(0, memcmp(q, in, kSize / 2)); + stream->ConsumeReadData(kSize / 2); + + // Use GetWriteBuffer to reset the read_position for the next tests + stream->GetWriteBuffer(&bytes); + stream->ConsumeWriteBuffer(0); + + // Try using GetWriteBuffer to do a full write + p = stream->GetWriteBuffer(&bytes); + EXPECT_TRUE(NULL != p); + EXPECT_EQ(kSize, bytes); + memcpy(p, in, kSize); + stream->ConsumeWriteBuffer(kSize); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize, &bytes, NULL)); + EXPECT_EQ(kSize, bytes); + EXPECT_EQ(0, memcmp(in, out, kSize)); + + // Try using GetWriteBuffer to do some small writes + p = stream->GetWriteBuffer(&bytes); + EXPECT_TRUE(NULL != p); + EXPECT_EQ(kSize, bytes); + memcpy(p, in, kSize / 2); + stream->ConsumeWriteBuffer(kSize / 2); + p = stream->GetWriteBuffer(&bytes); + EXPECT_TRUE(NULL != p); + EXPECT_EQ(kSize / 2, bytes); + memcpy(p, in + kSize / 2, kSize / 2); + stream->ConsumeWriteBuffer(kSize / 2); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize, &bytes, NULL)); + EXPECT_EQ(kSize, bytes); + EXPECT_EQ(0, memcmp(in, out, kSize)); + + // Try using GetWriteBuffer in a wraparound case + // WWWWWWWWWWWW.... 0123456789AB.... + // RRRRRRRRXXXX.... ........89AB.... + // ........XXXXWWWW ........89AB0123 + // WWWW....XXXXXXXX 4567....89AB0123 + // RRRR....RRRRRRRR ................ + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize * 3 / 4, &bytes, NULL)); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize / 2, &bytes, NULL)); + p = stream->GetWriteBuffer(&bytes); + EXPECT_TRUE(NULL != p); + EXPECT_EQ(kSize / 4, bytes); + memcpy(p, in, kSize / 4); + stream->ConsumeWriteBuffer(kSize / 4); + p = stream->GetWriteBuffer(&bytes); + EXPECT_TRUE(NULL != p); + EXPECT_EQ(kSize / 2, bytes); + memcpy(p, in + kSize / 4, kSize / 4); + stream->ConsumeWriteBuffer(kSize / 4); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize * 3 / 4, &bytes, NULL)); + EXPECT_EQ(kSize * 3 / 4, bytes); + EXPECT_EQ(0, memcmp(in + kSize / 2, out, kSize / 4)); + EXPECT_EQ(0, memcmp(in, out + kSize / 4, kSize / 4)); + + // Check that the stream is now empty + EXPECT_EQ(SR_BLOCK, stream->Read(out, kSize, &bytes, NULL)); + + // Try growing the buffer + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize, &bytes, NULL)); + EXPECT_EQ(kSize, bytes); + EXPECT_TRUE(buf.SetCapacity(kSize * 2)); + EXPECT_EQ(SR_SUCCESS, stream->Write(in + kSize, kSize, &bytes, NULL)); + EXPECT_EQ(kSize, bytes); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize * 2, &bytes, NULL)); + EXPECT_EQ(kSize * 2, bytes); + EXPECT_EQ(0, memcmp(in, out, kSize * 2)); + + // Try shrinking the buffer + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize, &bytes, NULL)); + EXPECT_EQ(kSize, bytes); + EXPECT_TRUE(buf.SetCapacity(kSize)); + EXPECT_EQ(SR_BLOCK, stream->Write(in, kSize, &bytes, NULL)); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize, &bytes, NULL)); + EXPECT_EQ(kSize, bytes); + EXPECT_EQ(0, memcmp(in, out, kSize)); + + // Write to the stream, close it, read the remaining bytes + EXPECT_EQ(SR_SUCCESS, stream->Write(in, kSize / 2, &bytes, NULL)); + stream->Close(); + EXPECT_EQ(SS_CLOSED, stream->GetState()); + EXPECT_EQ(SR_EOS, stream->Write(in, kSize / 2, &bytes, NULL)); + EXPECT_EQ(SR_SUCCESS, stream->Read(out, kSize / 2, &bytes, NULL)); + EXPECT_EQ(0, memcmp(in, out, kSize / 2)); + EXPECT_EQ(SR_EOS, stream->Read(out, kSize / 2, &bytes, NULL)); +} + +TEST(FifoBufferTest, FullBufferCheck) { + FifoBuffer buff(10); + buff.ConsumeWriteBuffer(10); + + size_t free; + EXPECT_TRUE(buff.GetWriteBuffer(&free) != NULL); + EXPECT_EQ(0U, free); +} + +TEST(FifoBufferTest, WriteOffsetAndReadOffset) { + const size_t kSize = 16; + const char in[kSize * 2 + 1] = "0123456789ABCDEFGHIJKLMNOPQRSTUV"; + char out[kSize * 2]; + FifoBuffer buf(kSize); + + // Write 14 bytes. + EXPECT_EQ(SR_SUCCESS, buf.Write(in, 14, NULL, NULL)); + + // Make sure data is in |buf|. + size_t buffered; + EXPECT_TRUE(buf.GetBuffered(&buffered)); + EXPECT_EQ(14u, buffered); + + // Read 10 bytes. + buf.ConsumeReadData(10); + + // There should be now 12 bytes of available space. + size_t remaining; + EXPECT_TRUE(buf.GetWriteRemaining(&remaining)); + EXPECT_EQ(12u, remaining); + + // Write at offset 12, this should fail. + EXPECT_EQ(SR_BLOCK, buf.WriteOffset(in, 10, 12, NULL)); + + // Write 8 bytes at offset 4, this wraps around the buffer. + EXPECT_EQ(SR_SUCCESS, buf.WriteOffset(in, 8, 4, NULL)); + + // Number of available space remains the same until we call + // ConsumeWriteBuffer(). + EXPECT_TRUE(buf.GetWriteRemaining(&remaining)); + EXPECT_EQ(12u, remaining); + buf.ConsumeWriteBuffer(12); + + // There's 4 bytes bypassed and 4 bytes no read so skip them and verify the + // 8 bytes written. + size_t read; + EXPECT_EQ(SR_SUCCESS, buf.ReadOffset(out, 8, 8, &read)); + EXPECT_EQ(8u, read); + EXPECT_EQ(0, memcmp(out, in, 8)); + + // There should still be 16 bytes available for reading. + EXPECT_TRUE(buf.GetBuffered(&buffered)); + EXPECT_EQ(16u, buffered); + + // Read at offset 16, this should fail since we don't have that much data. + EXPECT_EQ(SR_BLOCK, buf.ReadOffset(out, 10, 16, NULL)); +} + +TEST(AsyncWriteTest, TestWrite) { + FifoBuffer* buf = new FifoBuffer(100); + AsyncWriteStream stream(buf, Thread::Current()); + EXPECT_EQ(SS_OPEN, stream.GetState()); + + // Write "abc". Will go to the logging thread, which is the current + // thread. + stream.Write("abc", 3, NULL, NULL); + char bytes[100]; + size_t count; + // Messages on the thread's queue haven't been processed, so "abc" + // hasn't been written yet. + EXPECT_NE(SR_SUCCESS, buf->ReadOffset(&bytes, 3, 0, &count)); + // Now we process the messages on the thread's queue, so "abc" has + // been written. + EXPECT_TRUE_WAIT(SR_SUCCESS == buf->ReadOffset(&bytes, 3, 0, &count), 10); + EXPECT_EQ(3u, count); + EXPECT_EQ(0, memcmp(bytes, "abc", 3)); + + // Write "def". Will go to the logging thread, which is the current + // thread. + stream.Write("d", 1, &count, NULL); + stream.Write("e", 1, &count, NULL); + stream.Write("f", 1, &count, NULL); + EXPECT_EQ(1u, count); + // Messages on the thread's queue haven't been processed, so "def" + // hasn't been written yet. + EXPECT_NE(SR_SUCCESS, buf->ReadOffset(&bytes, 3, 3, &count)); + // Flush() causes the message to be processed, so "def" has now been + // written. + stream.Flush(); + EXPECT_EQ(SR_SUCCESS, buf->ReadOffset(&bytes, 3, 3, &count)); + EXPECT_EQ(3u, count); + EXPECT_EQ(0, memcmp(bytes, "def", 3)); + + // Write "xyz". Will go to the logging thread, which is the current + // thread. + stream.Write("xyz", 3, &count, NULL); + EXPECT_EQ(3u, count); + // Messages on the thread's queue haven't been processed, so "xyz" + // hasn't been written yet. + EXPECT_NE(SR_SUCCESS, buf->ReadOffset(&bytes, 3, 6, &count)); + // Close() causes the message to be processed, so "xyz" has now been + // written. + stream.Close(); + EXPECT_EQ(SR_SUCCESS, buf->ReadOffset(&bytes, 3, 6, &count)); + EXPECT_EQ(3u, count); + EXPECT_EQ(0, memcmp(bytes, "xyz", 3)); + EXPECT_EQ(SS_CLOSED, stream.GetState()); + + // Is't closed, so the writes should fail. + EXPECT_EQ(SR_ERROR, stream.Write("000", 3, NULL, NULL)); + +} + +} // namespace talk_base diff --git a/talk/base/stringdigest.h b/talk/base/stringdigest.h new file mode 100644 index 000000000..f03e92eb3 --- /dev/null +++ b/talk/base/stringdigest.h @@ -0,0 +1,34 @@ +/* + * libjingle + * Copyright 2004, 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. + */ + +#ifndef TALK_BASE_STRINGDIGEST_H_ +#define TALK_BASE_STRINGDIGEST_H_ + +// TODO: Update remaining callers to use messagedigest.h instead +#include "talk/base/messagedigest.h" + +#endif // TALK_BASE_STRINGDIGEST_H_ diff --git a/talk/base/stringencode.cc b/talk/base/stringencode.cc new file mode 100644 index 000000000..194848e1e --- /dev/null +++ b/talk/base/stringencode.cc @@ -0,0 +1,674 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/stringencode.h" + +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/common.h" +#include "talk/base/stringutils.h" + +namespace talk_base { + +///////////////////////////////////////////////////////////////////////////// +// String Encoding Utilities +///////////////////////////////////////////////////////////////////////////// + +size_t escape(char * buffer, size_t buflen, + const char * source, size_t srclen, + const char * illegal, char escape) { + ASSERT(NULL != buffer); // TODO: estimate output size + if (buflen <= 0) + return 0; + + size_t srcpos = 0, bufpos = 0; + while ((srcpos < srclen) && (bufpos + 1 < buflen)) { + char ch = source[srcpos++]; + if ((ch == escape) || ::strchr(illegal, ch)) { + if (bufpos + 2 >= buflen) + break; + buffer[bufpos++] = escape; + } + buffer[bufpos++] = ch; + } + + buffer[bufpos] = '\0'; + return bufpos; +} + +size_t unescape(char * buffer, size_t buflen, + const char * source, size_t srclen, + char escape) { + ASSERT(NULL != buffer); // TODO: estimate output size + if (buflen <= 0) + return 0; + + size_t srcpos = 0, bufpos = 0; + while ((srcpos < srclen) && (bufpos + 1 < buflen)) { + char ch = source[srcpos++]; + if ((ch == escape) && (srcpos < srclen)) { + ch = source[srcpos++]; + } + buffer[bufpos++] = ch; + } + buffer[bufpos] = '\0'; + return bufpos; +} + +size_t encode(char * buffer, size_t buflen, + const char * source, size_t srclen, + const char * illegal, char escape) { + ASSERT(NULL != buffer); // TODO: estimate output size + if (buflen <= 0) + return 0; + + size_t srcpos = 0, bufpos = 0; + while ((srcpos < srclen) && (bufpos + 1 < buflen)) { + char ch = source[srcpos++]; + if ((ch != escape) && !::strchr(illegal, ch)) { + buffer[bufpos++] = ch; + } else if (bufpos + 3 >= buflen) { + break; + } else { + buffer[bufpos+0] = escape; + buffer[bufpos+1] = hex_encode((static_cast(ch) >> 4) & 0xF); + buffer[bufpos+2] = hex_encode((static_cast(ch) ) & 0xF); + bufpos += 3; + } + } + buffer[bufpos] = '\0'; + return bufpos; +} + +size_t decode(char * buffer, size_t buflen, + const char * source, size_t srclen, + char escape) { + if (buflen <= 0) + return 0; + + unsigned char h1, h2; + size_t srcpos = 0, bufpos = 0; + while ((srcpos < srclen) && (bufpos + 1 < buflen)) { + char ch = source[srcpos++]; + if ((ch == escape) + && (srcpos + 1 < srclen) + && hex_decode(source[srcpos], &h1) + && hex_decode(source[srcpos+1], &h2)) { + buffer[bufpos++] = (h1 << 4) | h2; + srcpos += 2; + } else { + buffer[bufpos++] = ch; + } + } + buffer[bufpos] = '\0'; + return bufpos; +} + +const char* unsafe_filename_characters() { + // It might be better to have a single specification which is the union of + // all operating systems, unless one system is overly restrictive. +#ifdef WIN32 + return "\\/:*?\"<>|"; +#else // !WIN32 + // TODO + ASSERT(false); + return ""; +#endif // !WIN23 +} + +const unsigned char URL_UNSAFE = 0x1; // 0-33 "#$%&+,/:;<=>?@[\]^`{|} 127 +const unsigned char XML_UNSAFE = 0x2; // "&'<> +const unsigned char HTML_UNSAFE = 0x2; // "&'<> + +// ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 6 5 7 8 9 : ; < = > ? +//@ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ +//` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ + +const unsigned char ASCII_CLASS[128] = { + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,0,3,1,1,1,3,2,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,1,1,3,1,3,1, + 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0, + 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,1, +}; + +size_t url_encode(char * buffer, size_t buflen, + const char * source, size_t srclen) { + if (NULL == buffer) + return srclen * 3 + 1; + if (buflen <= 0) + return 0; + + size_t srcpos = 0, bufpos = 0; + while ((srcpos < srclen) && (bufpos + 1 < buflen)) { + unsigned char ch = source[srcpos++]; + if ((ch < 128) && (ASCII_CLASS[ch] & URL_UNSAFE)) { + if (bufpos + 3 >= buflen) { + break; + } + buffer[bufpos+0] = '%'; + buffer[bufpos+1] = hex_encode((ch >> 4) & 0xF); + buffer[bufpos+2] = hex_encode((ch ) & 0xF); + bufpos += 3; + } else { + buffer[bufpos++] = ch; + } + } + buffer[bufpos] = '\0'; + return bufpos; +} + +size_t url_decode(char * buffer, size_t buflen, + const char * source, size_t srclen) { + if (NULL == buffer) + return srclen + 1; + if (buflen <= 0) + return 0; + + unsigned char h1, h2; + size_t srcpos = 0, bufpos = 0; + while ((srcpos < srclen) && (bufpos + 1 < buflen)) { + unsigned char ch = source[srcpos++]; + if (ch == '+') { + buffer[bufpos++] = ' '; + } else if ((ch == '%') + && (srcpos + 1 < srclen) + && hex_decode(source[srcpos], &h1) + && hex_decode(source[srcpos+1], &h2)) + { + buffer[bufpos++] = (h1 << 4) | h2; + srcpos += 2; + } else { + buffer[bufpos++] = ch; + } + } + buffer[bufpos] = '\0'; + return bufpos; +} + +size_t utf8_decode(const char* source, size_t srclen, unsigned long* value) { + const unsigned char* s = reinterpret_cast(source); + if ((s[0] & 0x80) == 0x00) { // Check s[0] == 0xxxxxxx + *value = s[0]; + return 1; + } + if ((srclen < 2) || ((s[1] & 0xC0) != 0x80)) { // Check s[1] != 10xxxxxx + return 0; + } + // Accumulate the trailer byte values in value16, and combine it with the + // relevant bits from s[0], once we've determined the sequence length. + unsigned long value16 = (s[1] & 0x3F); + if ((s[0] & 0xE0) == 0xC0) { // Check s[0] == 110xxxxx + *value = ((s[0] & 0x1F) << 6) | value16; + return 2; + } + if ((srclen < 3) || ((s[2] & 0xC0) != 0x80)) { // Check s[2] != 10xxxxxx + return 0; + } + value16 = (value16 << 6) | (s[2] & 0x3F); + if ((s[0] & 0xF0) == 0xE0) { // Check s[0] == 1110xxxx + *value = ((s[0] & 0x0F) << 12) | value16; + return 3; + } + if ((srclen < 4) || ((s[3] & 0xC0) != 0x80)) { // Check s[3] != 10xxxxxx + return 0; + } + value16 = (value16 << 6) | (s[3] & 0x3F); + if ((s[0] & 0xF8) == 0xF0) { // Check s[0] == 11110xxx + *value = ((s[0] & 0x07) << 18) | value16; + return 4; + } + return 0; +} + +size_t utf8_encode(char* buffer, size_t buflen, unsigned long value) { + if ((value <= 0x7F) && (buflen >= 1)) { + buffer[0] = static_cast(value); + return 1; + } + if ((value <= 0x7FF) && (buflen >= 2)) { + buffer[0] = 0xC0 | static_cast(value >> 6); + buffer[1] = 0x80 | static_cast(value & 0x3F); + return 2; + } + if ((value <= 0xFFFF) && (buflen >= 3)) { + buffer[0] = 0xE0 | static_cast(value >> 12); + buffer[1] = 0x80 | static_cast((value >> 6) & 0x3F); + buffer[2] = 0x80 | static_cast(value & 0x3F); + return 3; + } + if ((value <= 0x1FFFFF) && (buflen >= 4)) { + buffer[0] = 0xF0 | static_cast(value >> 18); + buffer[1] = 0x80 | static_cast((value >> 12) & 0x3F); + buffer[2] = 0x80 | static_cast((value >> 6) & 0x3F); + buffer[3] = 0x80 | static_cast(value & 0x3F); + return 4; + } + return 0; +} + +size_t html_encode(char * buffer, size_t buflen, + const char * source, size_t srclen) { + ASSERT(NULL != buffer); // TODO: estimate output size + if (buflen <= 0) + return 0; + + size_t srcpos = 0, bufpos = 0; + while ((srcpos < srclen) && (bufpos + 1 < buflen)) { + unsigned char ch = source[srcpos]; + if (ch < 128) { + srcpos += 1; + if (ASCII_CLASS[ch] & HTML_UNSAFE) { + const char * escseq = 0; + size_t esclen = 0; + switch (ch) { + case '<': escseq = "<"; esclen = 4; break; + case '>': escseq = ">"; esclen = 4; break; + case '\'': escseq = "'"; esclen = 5; break; + case '\"': escseq = """; esclen = 6; break; + case '&': escseq = "&"; esclen = 5; break; + default: ASSERT(false); + } + if (bufpos + esclen >= buflen) { + break; + } + memcpy(buffer + bufpos, escseq, esclen); + bufpos += esclen; + } else { + buffer[bufpos++] = ch; + } + } else { + // Largest value is 0x1FFFFF => � (10 characters) + char escseq[11]; + unsigned long val; + if (size_t vallen = utf8_decode(&source[srcpos], srclen - srcpos, &val)) { + srcpos += vallen; + } else { + // Not a valid utf8 sequence, just use the raw character. + val = static_cast(source[srcpos++]); + } + size_t esclen = sprintfn(escseq, ARRAY_SIZE(escseq), "&#%lu;", val); + if (bufpos + esclen >= buflen) { + break; + } + memcpy(buffer + bufpos, escseq, esclen); + bufpos += esclen; + } + } + buffer[bufpos] = '\0'; + return bufpos; +} + +size_t html_decode(char * buffer, size_t buflen, + const char * source, size_t srclen) { + ASSERT(NULL != buffer); // TODO: estimate output size + return xml_decode(buffer, buflen, source, srclen); +} + +size_t xml_encode(char * buffer, size_t buflen, + const char * source, size_t srclen) { + ASSERT(NULL != buffer); // TODO: estimate output size + if (buflen <= 0) + return 0; + + size_t srcpos = 0, bufpos = 0; + while ((srcpos < srclen) && (bufpos + 1 < buflen)) { + unsigned char ch = source[srcpos++]; + if ((ch < 128) && (ASCII_CLASS[ch] & XML_UNSAFE)) { + const char * escseq = 0; + size_t esclen = 0; + switch (ch) { + case '<': escseq = "<"; esclen = 4; break; + case '>': escseq = ">"; esclen = 4; break; + case '\'': escseq = "'"; esclen = 6; break; + case '\"': escseq = """; esclen = 6; break; + case '&': escseq = "&"; esclen = 5; break; + default: ASSERT(false); + } + if (bufpos + esclen >= buflen) { + break; + } + memcpy(buffer + bufpos, escseq, esclen); + bufpos += esclen; + } else { + buffer[bufpos++] = ch; + } + } + buffer[bufpos] = '\0'; + return bufpos; +} + +size_t xml_decode(char * buffer, size_t buflen, + const char * source, size_t srclen) { + ASSERT(NULL != buffer); // TODO: estimate output size + if (buflen <= 0) + return 0; + + size_t srcpos = 0, bufpos = 0; + while ((srcpos < srclen) && (bufpos + 1 < buflen)) { + unsigned char ch = source[srcpos++]; + if (ch != '&') { + buffer[bufpos++] = ch; + } else if ((srcpos + 2 < srclen) + && (memcmp(source + srcpos, "lt;", 3) == 0)) { + buffer[bufpos++] = '<'; + srcpos += 3; + } else if ((srcpos + 2 < srclen) + && (memcmp(source + srcpos, "gt;", 3) == 0)) { + buffer[bufpos++] = '>'; + srcpos += 3; + } else if ((srcpos + 4 < srclen) + && (memcmp(source + srcpos, "apos;", 5) == 0)) { + buffer[bufpos++] = '\''; + srcpos += 5; + } else if ((srcpos + 4 < srclen) + && (memcmp(source + srcpos, "quot;", 5) == 0)) { + buffer[bufpos++] = '\"'; + srcpos += 5; + } else if ((srcpos + 3 < srclen) + && (memcmp(source + srcpos, "amp;", 4) == 0)) { + buffer[bufpos++] = '&'; + srcpos += 4; + } else if ((srcpos < srclen) && (source[srcpos] == '#')) { + int int_base = 10; + if ((srcpos + 1 < srclen) && (source[srcpos+1] == 'x')) { + int_base = 16; + srcpos += 1; + } + char * ptr; + // TODO: Fix hack (ptr may go past end of data) + unsigned long val = strtoul(source + srcpos + 1, &ptr, int_base); + if ((static_cast(ptr - source) < srclen) && (*ptr == ';')) { + srcpos = ptr - source + 1; + } else { + // Not a valid escape sequence. + break; + } + if (size_t esclen = utf8_encode(buffer + bufpos, buflen - bufpos, val)) { + bufpos += esclen; + } else { + // Not enough room to encode the character, or illegal character + break; + } + } else { + // Unrecognized escape sequence. + break; + } + } + buffer[bufpos] = '\0'; + return bufpos; +} + +static const char HEX[] = "0123456789abcdef"; + +char hex_encode(unsigned char val) { + ASSERT(val < 16); + return (val < 16) ? HEX[val] : '!'; +} + +bool hex_decode(char ch, unsigned char* val) { + if ((ch >= '0') && (ch <= '9')) { + *val = ch - '0'; + } else if ((ch >= 'A') && (ch <= 'Z')) { + *val = (ch - 'A') + 10; + } else if ((ch >= 'a') && (ch <= 'z')) { + *val = (ch - 'a') + 10; + } else { + return false; + } + return true; +} + +size_t hex_encode(char* buffer, size_t buflen, + const char* csource, size_t srclen) { + return hex_encode_with_delimiter(buffer, buflen, csource, srclen, 0); +} + +size_t hex_encode_with_delimiter(char* buffer, size_t buflen, + const char* csource, size_t srclen, + char delimiter) { + ASSERT(NULL != buffer); // TODO: estimate output size + if (buflen == 0) + return 0; + + // Init and check bounds. + const unsigned char* bsource = + reinterpret_cast(csource); + size_t srcpos = 0, bufpos = 0; + size_t needed = delimiter ? (srclen * 3) : (srclen * 2 + 1); + if (buflen < needed) + return 0; + + while (srcpos < srclen) { + unsigned char ch = bsource[srcpos++]; + buffer[bufpos ] = hex_encode((ch >> 4) & 0xF); + buffer[bufpos+1] = hex_encode((ch ) & 0xF); + bufpos += 2; + + // Don't write a delimiter after the last byte. + if (delimiter && (srcpos < srclen)) { + buffer[bufpos] = delimiter; + ++bufpos; + } + } + + // Null terminate. + buffer[bufpos] = '\0'; + return bufpos; +} + +std::string hex_encode(const char* source, size_t srclen) { + return hex_encode_with_delimiter(source, srclen, 0); +} + +std::string hex_encode_with_delimiter(const char* source, size_t srclen, + char delimiter) { + const size_t kBufferSize = srclen * 3; + char* buffer = STACK_ARRAY(char, kBufferSize); + size_t length = hex_encode_with_delimiter(buffer, kBufferSize, + source, srclen, delimiter); + ASSERT(srclen == 0 || length > 0); + return std::string(buffer, length); +} + +size_t hex_decode(char * cbuffer, size_t buflen, + const char * source, size_t srclen) { + return hex_decode_with_delimiter(cbuffer, buflen, source, srclen, 0); +} + +size_t hex_decode_with_delimiter(char* cbuffer, size_t buflen, + const char* source, size_t srclen, + char delimiter) { + ASSERT(NULL != cbuffer); // TODO: estimate output size + if (buflen == 0) + return 0; + + // Init and bounds check. + unsigned char* bbuffer = reinterpret_cast(cbuffer); + size_t srcpos = 0, bufpos = 0; + size_t needed = (delimiter) ? (srclen + 1) / 3 : srclen / 2; + if (buflen < needed) + return 0; + + while (srcpos < srclen) { + if ((srclen - srcpos) < 2) { + // This means we have an odd number of bytes. + return 0; + } + + unsigned char h1, h2; + if (!hex_decode(source[srcpos], &h1) || + !hex_decode(source[srcpos + 1], &h2)) + return 0; + + bbuffer[bufpos++] = (h1 << 4) | h2; + srcpos += 2; + + // Remove the delimiter if needed. + if (delimiter && (srclen - srcpos) > 1) { + if (source[srcpos] != delimiter) + return 0; + ++srcpos; + } + } + + return bufpos; +} + +size_t hex_decode(char* buffer, size_t buflen, const std::string& source) { + return hex_decode_with_delimiter(buffer, buflen, source, 0); +} +size_t hex_decode_with_delimiter(char* buffer, size_t buflen, + const std::string& source, char delimiter) { + return hex_decode_with_delimiter(buffer, buflen, + source.c_str(), source.length(), delimiter); +} + +size_t transform(std::string& value, size_t maxlen, const std::string& source, + Transform t) { + char* buffer = STACK_ARRAY(char, maxlen + 1); + size_t length = t(buffer, maxlen + 1, source.data(), source.length()); + value.assign(buffer, length); + return length; +} + +std::string s_transform(const std::string& source, Transform t) { + // Ask transformation function to approximate the destination size (returns upper bound) + size_t maxlen = t(NULL, 0, source.data(), source.length()); + char * buffer = STACK_ARRAY(char, maxlen); + size_t len = t(buffer, maxlen, source.data(), source.length()); + std::string result(buffer, len); + return result; +} + +size_t tokenize(const std::string& source, char delimiter, + std::vector* fields) { + ASSERT(NULL != fields); + fields->clear(); + size_t last = 0; + for (size_t i = 0; i < source.length(); ++i) { + if (source[i] == delimiter) { + if (i != last) { + fields->push_back(source.substr(last, i - last)); + } + last = i + 1; + } + } + if (last != source.length()) { + fields->push_back(source.substr(last, source.length() - last)); + } + return fields->size(); +} + +size_t tokenize_append(const std::string& source, char delimiter, + std::vector* fields) { + if (!fields) return 0; + + std::vector new_fields; + tokenize(source, delimiter, &new_fields); + fields->insert(fields->end(), new_fields.begin(), new_fields.end()); + return fields->size(); +} + +size_t tokenize(const std::string& source, char delimiter, char start_mark, + char end_mark, std::vector* fields) { + if (!fields) return 0; + fields->clear(); + + std::string remain_source = source; + while (!remain_source.empty()) { + size_t start_pos = remain_source.find(start_mark); + if (std::string::npos == start_pos) break; + std::string pre_mark; + if (start_pos > 0) { + pre_mark = remain_source.substr(0, start_pos - 1); + } + + ++start_pos; + size_t end_pos = remain_source.find(end_mark, start_pos); + if (std::string::npos == end_pos) break; + + // We have found the matching marks. First tokenize the pre-mask. Then add + // the marked part as a single field. Finally, loop back for the post-mark. + tokenize_append(pre_mark, delimiter, fields); + fields->push_back(remain_source.substr(start_pos, end_pos - start_pos)); + remain_source = remain_source.substr(end_pos + 1); + } + + return tokenize_append(remain_source, delimiter, fields); +} + +size_t split(const std::string& source, char delimiter, + std::vector* fields) { + ASSERT(NULL != fields); + fields->clear(); + size_t last = 0; + for (size_t i = 0; i < source.length(); ++i) { + if (source[i] == delimiter) { + fields->push_back(source.substr(last, i - last)); + last = i + 1; + } + } + fields->push_back(source.substr(last, source.length() - last)); + return fields->size(); +} + +char make_char_safe_for_filename(char c) { + if (c < 32) + return '_'; + + switch (c) { + case '<': + case '>': + case ':': + case '"': + case '/': + case '\\': + case '|': + case '*': + case '?': + return '_'; + + default: + return c; + } +} + +/* +void sprintf(std::string& value, size_t maxlen, const char * format, ...) { + char * buffer = STACK_ARRAY(char, maxlen + 1); + va_list args; + va_start(args, format); + value.assign(buffer, vsprintfn(buffer, maxlen + 1, format, args)); + va_end(args); +} +*/ + +///////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/stringencode.h b/talk/base/stringencode.h new file mode 100644 index 000000000..872dfd47e --- /dev/null +++ b/talk/base/stringencode.h @@ -0,0 +1,227 @@ +/* + * libjingle + * Copyright 2004, 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. + */ + +#ifndef TALK_BASE_STRINGENCODE_H_ +#define TALK_BASE_STRINGENCODE_H_ + +#include +#include +#include + +#include "talk/base/common.h" + +namespace talk_base { + +////////////////////////////////////////////////////////////////////// +// String Encoding Utilities +////////////////////////////////////////////////////////////////////// + +// Convert an unsigned value to it's utf8 representation. Returns the length +// of the encoded string, or 0 if the encoding is longer than buflen - 1. +size_t utf8_encode(char* buffer, size_t buflen, unsigned long value); +// Decode the utf8 encoded value pointed to by source. Returns the number of +// bytes used by the encoding, or 0 if the encoding is invalid. +size_t utf8_decode(const char* source, size_t srclen, unsigned long* value); + +// Escaping prefixes illegal characters with the escape character. Compact, but +// illegal characters still appear in the string. +size_t escape(char * buffer, size_t buflen, + const char * source, size_t srclen, + const char * illegal, char escape); +// Note: in-place unescaping (buffer == source) is allowed. +size_t unescape(char * buffer, size_t buflen, + const char * source, size_t srclen, + char escape); + +// Encoding replaces illegal characters with the escape character and 2 hex +// chars, so it's a little less compact than escape, but completely removes +// illegal characters. note that hex digits should not be used as illegal +// characters. +size_t encode(char * buffer, size_t buflen, + const char * source, size_t srclen, + const char * illegal, char escape); +// Note: in-place decoding (buffer == source) is allowed. +size_t decode(char * buffer, size_t buflen, + const char * source, size_t srclen, + char escape); + +// Returns a list of characters that may be unsafe for use in the name of a +// file, suitable for passing to the 'illegal' member of escape or encode. +const char* unsafe_filename_characters(); + +// url_encode is an encode operation with a predefined set of illegal characters +// and escape character (for use in URLs, obviously). +size_t url_encode(char * buffer, size_t buflen, + const char * source, size_t srclen); +// Note: in-place decoding (buffer == source) is allowed. +size_t url_decode(char * buffer, size_t buflen, + const char * source, size_t srclen); + +// html_encode prevents data embedded in html from containing markup. +size_t html_encode(char * buffer, size_t buflen, + const char * source, size_t srclen); +// Note: in-place decoding (buffer == source) is allowed. +size_t html_decode(char * buffer, size_t buflen, + const char * source, size_t srclen); + +// xml_encode makes data suitable for inside xml attributes and values. +size_t xml_encode(char * buffer, size_t buflen, + const char * source, size_t srclen); +// Note: in-place decoding (buffer == source) is allowed. +size_t xml_decode(char * buffer, size_t buflen, + const char * source, size_t srclen); + +// Convert an unsigned value from 0 to 15 to the hex character equivalent... +char hex_encode(unsigned char val); +// ...and vice-versa. +bool hex_decode(char ch, unsigned char* val); + +// hex_encode shows the hex representation of binary data in ascii. +size_t hex_encode(char* buffer, size_t buflen, + const char* source, size_t srclen); + +// hex_encode, but separate each byte representation with a delimiter. +// |delimiter| == 0 means no delimiter +// If the buffer is too short, we return 0 +size_t hex_encode_with_delimiter(char* buffer, size_t buflen, + const char* source, size_t srclen, + char delimiter); + +// Helper functions for hex_encode. +std::string hex_encode(const char* source, size_t srclen); +std::string hex_encode_with_delimiter(const char* source, size_t srclen, + char delimiter); + +// hex_decode converts ascii hex to binary. +size_t hex_decode(char* buffer, size_t buflen, + const char* source, size_t srclen); + +// hex_decode, assuming that there is a delimiter between every byte +// pair. +// |delimiter| == 0 means no delimiter +// If the buffer is too short or the data is invalid, we return 0. +size_t hex_decode_with_delimiter(char* buffer, size_t buflen, + const char* source, size_t srclen, + char delimiter); + +// Helper functions for hex_decode. +size_t hex_decode(char* buffer, size_t buflen, const std::string& source); +size_t hex_decode_with_delimiter(char* buffer, size_t buflen, + const std::string& source, char delimiter); + +// Apply any suitable string transform (including the ones above) to an STL +// string. Stack-allocated temporary space is used for the transformation, +// so value and source may refer to the same string. +typedef size_t (*Transform)(char * buffer, size_t buflen, + const char * source, size_t srclen); +size_t transform(std::string& value, size_t maxlen, const std::string& source, + Transform t); + +// Return the result of applying transform t to source. +std::string s_transform(const std::string& source, Transform t); + +// Convenience wrappers. +inline std::string s_url_encode(const std::string& source) { + return s_transform(source, url_encode); +} +inline std::string s_url_decode(const std::string& source) { + return s_transform(source, url_decode); +} + +// Splits the source string into multiple fields separated by delimiter, +// with duplicates of delimiter creating empty fields. +size_t split(const std::string& source, char delimiter, + std::vector* fields); + +// Splits the source string into multiple fields separated by delimiter, +// with duplicates of delimiter ignored. Trailing delimiter ignored. +size_t tokenize(const std::string& source, char delimiter, + std::vector* fields); + +// Tokenize and append the tokens to fields. Return the new size of fields. +size_t tokenize_append(const std::string& source, char delimiter, + std::vector* fields); + +// Splits the source string into multiple fields separated by delimiter, with +// duplicates of delimiter ignored. Trailing delimiter ignored. A substring in +// between the start_mark and the end_mark is treated as a single field. Return +// the size of fields. For example, if source is "filename +// \"/Library/Application Support/media content.txt\"", delimiter is ' ', and +// the start_mark and end_mark are '"', this method returns two fields: +// "filename" and "/Library/Application Support/media content.txt". +size_t tokenize(const std::string& source, char delimiter, char start_mark, + char end_mark, std::vector* fields); + +// Safe sprintf to std::string +//void sprintf(std::string& value, size_t maxlen, const char * format, ...) +// PRINTF_FORMAT(3); + +// Convert arbitrary values to/from a string. + +template +static bool ToString(const T &t, std::string* s) { + ASSERT(NULL != s); + std::ostringstream oss; + oss << std::boolalpha << t; + *s = oss.str(); + return !oss.fail(); +} + +template +static bool FromString(const std::string& s, T* t) { + ASSERT(NULL != t); + std::istringstream iss(s); + iss >> std::boolalpha >> *t; + return !iss.fail(); +} + +// Inline versions of the string conversion routines. + +template +static inline std::string ToString(const T& val) { + std::string str; ToString(val, &str); return str; +} + +template +static inline T FromString(const std::string& str) { + T val; FromString(str, &val); return val; +} + +template +static inline T FromString(const T& defaultValue, const std::string& str) { + T val(defaultValue); FromString(str, &val); return val; +} + +// simple function to strip out characters which shouldn't be +// used in filenames +char make_char_safe_for_filename(char c); + +////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_STRINGENCODE_H__ diff --git a/talk/base/stringencode_unittest.cc b/talk/base/stringencode_unittest.cc new file mode 100644 index 000000000..c1ec53fa1 --- /dev/null +++ b/talk/base/stringencode_unittest.cc @@ -0,0 +1,402 @@ +/* + * libjingle + * Copyright 2004, 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/base/common.h" +#include "talk/base/gunit.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" + +namespace talk_base { + +TEST(Utf8EncodeTest, EncodeDecode) { + const struct Utf8Test { + const char* encoded; + size_t encsize, enclen; + unsigned long decoded; + } kTests[] = { + { "a ", 5, 1, 'a' }, + { "\x7F ", 5, 1, 0x7F }, + { "\xC2\x80 ", 5, 2, 0x80 }, + { "\xDF\xBF ", 5, 2, 0x7FF }, + { "\xE0\xA0\x80 ", 5, 3, 0x800 }, + { "\xEF\xBF\xBF ", 5, 3, 0xFFFF }, + { "\xF0\x90\x80\x80 ", 5, 4, 0x10000 }, + { "\xF0\x90\x80\x80 ", 3, 0, 0x10000 }, + { "\xF0\xF0\x80\x80 ", 5, 0, 0 }, + { "\xF0\x90\x80 ", 5, 0, 0 }, + { "\x90\x80\x80 ", 5, 0, 0 }, + { NULL, 0, 0 }, + }; + for (size_t i = 0; kTests[i].encoded; ++i) { + unsigned long val = 0; + ASSERT_EQ(kTests[i].enclen, utf8_decode(kTests[i].encoded, + kTests[i].encsize, + &val)); + unsigned long result = (kTests[i].enclen == 0) ? 0 : kTests[i].decoded; + ASSERT_EQ(result, val); + + if (kTests[i].decoded == 0) { + // Not an interesting encoding test case + continue; + } + + char buffer[5]; + memset(buffer, 0x01, ARRAY_SIZE(buffer)); + ASSERT_EQ(kTests[i].enclen, utf8_encode(buffer, + kTests[i].encsize, + kTests[i].decoded)); + ASSERT_TRUE(memcmp(buffer, kTests[i].encoded, kTests[i].enclen) == 0); + // Make sure remainder of buffer is unchanged + ASSERT_TRUE(memory_check(buffer + kTests[i].enclen, + 0x1, + ARRAY_SIZE(buffer) - kTests[i].enclen)); + } +} + +class HexEncodeTest : public testing::Test { + public: + HexEncodeTest() : enc_res_(0), dec_res_(0) { + for (size_t i = 0; i < sizeof(data_); ++i) { + data_[i] = (i + 128) & 0xff; + } + memset(decoded_, 0x7f, sizeof(decoded_)); + } + + char data_[10]; + char encoded_[31]; + char decoded_[11]; + size_t enc_res_; + size_t dec_res_; +}; + +// Test that we can convert to/from hex with no delimiter. +TEST_F(HexEncodeTest, TestWithNoDelimiter) { + enc_res_ = hex_encode(encoded_, sizeof(encoded_), data_, sizeof(data_)); + ASSERT_EQ(sizeof(data_) * 2, enc_res_); + ASSERT_STREQ("80818283848586878889", encoded_); + dec_res_ = hex_decode(decoded_, sizeof(decoded_), encoded_, enc_res_); + ASSERT_EQ(sizeof(data_), dec_res_); + ASSERT_EQ(0, memcmp(data_, decoded_, dec_res_)); +} + +// Test that we can convert to/from hex with a colon delimiter. +TEST_F(HexEncodeTest, TestWithDelimiter) { + enc_res_ = hex_encode_with_delimiter(encoded_, sizeof(encoded_), + data_, sizeof(data_), ':'); + ASSERT_EQ(sizeof(data_) * 3 - 1, enc_res_); + ASSERT_STREQ("80:81:82:83:84:85:86:87:88:89", encoded_); + dec_res_ = hex_decode_with_delimiter(decoded_, sizeof(decoded_), + encoded_, enc_res_, ':'); + ASSERT_EQ(sizeof(data_), dec_res_); + ASSERT_EQ(0, memcmp(data_, decoded_, dec_res_)); +} + +// Test that encoding with one delimiter and decoding with another fails. +TEST_F(HexEncodeTest, TestWithWrongDelimiter) { + enc_res_ = hex_encode_with_delimiter(encoded_, sizeof(encoded_), + data_, sizeof(data_), ':'); + ASSERT_EQ(sizeof(data_) * 3 - 1, enc_res_); + dec_res_ = hex_decode_with_delimiter(decoded_, sizeof(decoded_), + encoded_, enc_res_, '/'); + ASSERT_EQ(0U, dec_res_); +} + +// Test that encoding without a delimiter and decoding with one fails. +TEST_F(HexEncodeTest, TestExpectedDelimiter) { + enc_res_ = hex_encode(encoded_, sizeof(encoded_), data_, sizeof(data_)); + ASSERT_EQ(sizeof(data_) * 2, enc_res_); + dec_res_ = hex_decode_with_delimiter(decoded_, sizeof(decoded_), + encoded_, enc_res_, ':'); + ASSERT_EQ(0U, dec_res_); +} + +// Test that encoding with a delimiter and decoding without one fails. +TEST_F(HexEncodeTest, TestExpectedNoDelimiter) { + enc_res_ = hex_encode_with_delimiter(encoded_, sizeof(encoded_), + data_, sizeof(data_), ':'); + ASSERT_EQ(sizeof(data_) * 3 - 1, enc_res_); + dec_res_ = hex_decode(decoded_, sizeof(decoded_), encoded_, enc_res_); + ASSERT_EQ(0U, dec_res_); +} + +// Test that we handle a zero-length buffer with no delimiter. +TEST_F(HexEncodeTest, TestZeroLengthNoDelimiter) { + enc_res_ = hex_encode(encoded_, sizeof(encoded_), "", 0); + ASSERT_EQ(0U, enc_res_); + dec_res_ = hex_decode(decoded_, sizeof(decoded_), encoded_, enc_res_); + ASSERT_EQ(0U, dec_res_); +} + +// Test that we handle a zero-length buffer with a delimiter. +TEST_F(HexEncodeTest, TestZeroLengthWithDelimiter) { + enc_res_ = hex_encode_with_delimiter(encoded_, sizeof(encoded_), "", 0, ':'); + ASSERT_EQ(0U, enc_res_); + dec_res_ = hex_decode_with_delimiter(decoded_, sizeof(decoded_), + encoded_, enc_res_, ':'); + ASSERT_EQ(0U, dec_res_); +} + +// Test the std::string variants that take no delimiter. +TEST_F(HexEncodeTest, TestHelpersNoDelimiter) { + std::string result = hex_encode(data_, sizeof(data_)); + ASSERT_EQ("80818283848586878889", result); + dec_res_ = hex_decode(decoded_, sizeof(decoded_), result); + ASSERT_EQ(sizeof(data_), dec_res_); + ASSERT_EQ(0, memcmp(data_, decoded_, dec_res_)); +} + +// Test the std::string variants that use a delimiter. +TEST_F(HexEncodeTest, TestHelpersWithDelimiter) { + std::string result = hex_encode_with_delimiter(data_, sizeof(data_), ':'); + ASSERT_EQ("80:81:82:83:84:85:86:87:88:89", result); + dec_res_ = hex_decode_with_delimiter(decoded_, sizeof(decoded_), result, ':'); + ASSERT_EQ(sizeof(data_), dec_res_); + ASSERT_EQ(0, memcmp(data_, decoded_, dec_res_)); +} + +// Test that encoding into a too-small output buffer (without delimiter) fails. +TEST_F(HexEncodeTest, TestEncodeTooShort) { + enc_res_ = hex_encode_with_delimiter(encoded_, sizeof(data_) * 2, + data_, sizeof(data_), 0); + ASSERT_EQ(0U, enc_res_); +} + +// Test that encoding into a too-small output buffer (with delimiter) fails. +TEST_F(HexEncodeTest, TestEncodeWithDelimiterTooShort) { + enc_res_ = hex_encode_with_delimiter(encoded_, sizeof(data_) * 3 - 1, + data_, sizeof(data_), ':'); + ASSERT_EQ(0U, enc_res_); +} + +// Test that decoding into a too-small output buffer fails. +TEST_F(HexEncodeTest, TestDecodeTooShort) { + dec_res_ = hex_decode_with_delimiter(decoded_, 4, "0123456789", 10, 0); + ASSERT_EQ(0U, dec_res_); + ASSERT_EQ(0x7f, decoded_[4]); +} + +// Test that decoding non-hex data fails. +TEST_F(HexEncodeTest, TestDecodeBogusData) { + dec_res_ = hex_decode_with_delimiter(decoded_, sizeof(decoded_), "xyz", 3, 0); + ASSERT_EQ(0U, dec_res_); +} + +// Test that decoding an odd number of hex characters fails. +TEST_F(HexEncodeTest, TestDecodeOddHexDigits) { + dec_res_ = hex_decode_with_delimiter(decoded_, sizeof(decoded_), "012", 3, 0); + ASSERT_EQ(0U, dec_res_); +} + +// Test that decoding a string with too many delimiters fails. +TEST_F(HexEncodeTest, TestDecodeWithDelimiterTooManyDelimiters) { + dec_res_ = hex_decode_with_delimiter(decoded_, 4, "01::23::45::67", 14, ':'); + ASSERT_EQ(0U, dec_res_); +} + +// Test that decoding a string with a leading delimiter fails. +TEST_F(HexEncodeTest, TestDecodeWithDelimiterLeadingDelimiter) { + dec_res_ = hex_decode_with_delimiter(decoded_, 4, ":01:23:45:67", 12, ':'); + ASSERT_EQ(0U, dec_res_); +} + +// Test that decoding a string with a trailing delimiter fails. +TEST_F(HexEncodeTest, TestDecodeWithDelimiterTrailingDelimiter) { + dec_res_ = hex_decode_with_delimiter(decoded_, 4, "01:23:45:67:", 12, ':'); + ASSERT_EQ(0U, dec_res_); +} + +// Tests counting substrings. +TEST(TokenizeTest, CountSubstrings) { + std::vector fields; + + EXPECT_EQ(5ul, tokenize("one two three four five", ' ', &fields)); + fields.clear(); + EXPECT_EQ(1ul, tokenize("one", ' ', &fields)); + + // Extra spaces should be ignored. + fields.clear(); + EXPECT_EQ(5ul, tokenize(" one two three four five ", ' ', &fields)); + fields.clear(); + EXPECT_EQ(1ul, tokenize(" one ", ' ', &fields)); + fields.clear(); + EXPECT_EQ(0ul, tokenize(" ", ' ', &fields)); +} + +// Tests comparing substrings. +TEST(TokenizeTest, CompareSubstrings) { + std::vector fields; + + tokenize("find middle one", ' ', &fields); + ASSERT_EQ(3ul, fields.size()); + ASSERT_STREQ("middle", fields.at(1).c_str()); + fields.clear(); + + // Extra spaces should be ignored. + tokenize(" find middle one ", ' ', &fields); + ASSERT_EQ(3ul, fields.size()); + ASSERT_STREQ("middle", fields.at(1).c_str()); + fields.clear(); + tokenize(" ", ' ', &fields); + ASSERT_EQ(0ul, fields.size()); +} + +TEST(TokenizeTest, TokenizeAppend) { + ASSERT_EQ(0ul, tokenize_append("A B C", ' ', NULL)); + + std::vector fields; + + tokenize_append("A B C", ' ', &fields); + ASSERT_EQ(3ul, fields.size()); + ASSERT_STREQ("B", fields.at(1).c_str()); + + tokenize_append("D E", ' ', &fields); + ASSERT_EQ(5ul, fields.size()); + ASSERT_STREQ("B", fields.at(1).c_str()); + ASSERT_STREQ("E", fields.at(4).c_str()); +} + +TEST(TokenizeTest, TokenizeWithMarks) { + ASSERT_EQ(0ul, tokenize("D \"A B", ' ', '(', ')', NULL)); + + std::vector fields; + tokenize("A B C", ' ', '"', '"', &fields); + ASSERT_EQ(3ul, fields.size()); + ASSERT_STREQ("C", fields.at(2).c_str()); + + tokenize("\"A B\" C", ' ', '"', '"', &fields); + ASSERT_EQ(2ul, fields.size()); + ASSERT_STREQ("A B", fields.at(0).c_str()); + + tokenize("D \"A B\" C", ' ', '"', '"', &fields); + ASSERT_EQ(3ul, fields.size()); + ASSERT_STREQ("D", fields.at(0).c_str()); + ASSERT_STREQ("A B", fields.at(1).c_str()); + + tokenize("D \"A B\" C \"E F\"", ' ', '"', '"', &fields); + ASSERT_EQ(4ul, fields.size()); + ASSERT_STREQ("D", fields.at(0).c_str()); + ASSERT_STREQ("A B", fields.at(1).c_str()); + ASSERT_STREQ("E F", fields.at(3).c_str()); + + // No matching marks. + tokenize("D \"A B", ' ', '"', '"', &fields); + ASSERT_EQ(3ul, fields.size()); + ASSERT_STREQ("D", fields.at(0).c_str()); + ASSERT_STREQ("\"A", fields.at(1).c_str()); + + tokenize("D (A B) C (E F) G", ' ', '(', ')', &fields); + ASSERT_EQ(5ul, fields.size()); + ASSERT_STREQ("D", fields.at(0).c_str()); + ASSERT_STREQ("A B", fields.at(1).c_str()); + ASSERT_STREQ("E F", fields.at(3).c_str()); +} + +// Tests counting substrings. +TEST(SplitTest, CountSubstrings) { + std::vector fields; + + EXPECT_EQ(5ul, split("one,two,three,four,five", ',', &fields)); + fields.clear(); + EXPECT_EQ(1ul, split("one", ',', &fields)); + + // Empty fields between commas count. + fields.clear(); + EXPECT_EQ(5ul, split("one,,three,four,five", ',', &fields)); + fields.clear(); + EXPECT_EQ(3ul, split(",three,", ',', &fields)); + fields.clear(); + EXPECT_EQ(1ul, split("", ',', &fields)); +} + +// Tests comparing substrings. +TEST(SplitTest, CompareSubstrings) { + std::vector fields; + + split("find,middle,one", ',', &fields); + ASSERT_EQ(3ul, fields.size()); + ASSERT_STREQ("middle", fields.at(1).c_str()); + fields.clear(); + + // Empty fields between commas count. + split("find,,middle,one", ',', &fields); + ASSERT_EQ(4ul, fields.size()); + ASSERT_STREQ("middle", fields.at(2).c_str()); + fields.clear(); + split("", ',', &fields); + ASSERT_EQ(1ul, fields.size()); + ASSERT_STREQ("", fields.at(0).c_str()); +} + +TEST(BoolTest, DecodeValid) { + bool value; + EXPECT_TRUE(FromString("true", &value)); + EXPECT_TRUE(value); + EXPECT_TRUE(FromString("true,", &value)); + EXPECT_TRUE(value); + EXPECT_TRUE(FromString("true , true", &value)); + EXPECT_TRUE(value); + EXPECT_TRUE(FromString("true ,\n false", &value)); + EXPECT_TRUE(value); + EXPECT_TRUE(FromString(" true \n", &value)); + EXPECT_TRUE(value); + + EXPECT_TRUE(FromString("false", &value)); + EXPECT_FALSE(value); + EXPECT_TRUE(FromString(" false ", &value)); + EXPECT_FALSE(value); + EXPECT_TRUE(FromString(" false, ", &value)); + EXPECT_FALSE(value); + + EXPECT_TRUE(FromString("true\n")); + EXPECT_FALSE(FromString("false\n")); +} + +TEST(BoolTest, DecodeInvalid) { + bool value; + EXPECT_FALSE(FromString("True", &value)); + EXPECT_FALSE(FromString("TRUE", &value)); + EXPECT_FALSE(FromString("False", &value)); + EXPECT_FALSE(FromString("FALSE", &value)); + EXPECT_FALSE(FromString("0", &value)); + EXPECT_FALSE(FromString("1", &value)); + EXPECT_FALSE(FromString("0,", &value)); + EXPECT_FALSE(FromString("1,", &value)); + EXPECT_FALSE(FromString("1,0", &value)); + EXPECT_FALSE(FromString("1.", &value)); + EXPECT_FALSE(FromString("1.0", &value)); + EXPECT_FALSE(FromString("", &value)); + EXPECT_FALSE(FromString("false\nfalse")); +} + +TEST(BoolTest, RoundTrip) { + bool value; + EXPECT_TRUE(FromString(ToString(true), &value)); + EXPECT_TRUE(value); + EXPECT_TRUE(FromString(ToString(false), &value)); + EXPECT_FALSE(value); +} +} // namespace talk_base diff --git a/talk/base/stringutils.cc b/talk/base/stringutils.cc new file mode 100644 index 000000000..c4c2b2f59 --- /dev/null +++ b/talk/base/stringutils.cc @@ -0,0 +1,150 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/stringutils.h" +#include "talk/base/common.h" + +namespace talk_base { + +bool memory_check(const void* memory, int c, size_t count) { + const char* char_memory = static_cast(memory); + char char_c = static_cast(c); + for (size_t i = 0; i < count; ++i) { + if (char_memory[i] != char_c) { + return false; + } + } + return true; +} + +bool string_match(const char* target, const char* pattern) { + while (*pattern) { + if (*pattern == '*') { + if (!*++pattern) { + return true; + } + while (*target) { + if ((toupper(*pattern) == toupper(*target)) + && string_match(target + 1, pattern + 1)) { + return true; + } + ++target; + } + return false; + } else { + if (toupper(*pattern) != toupper(*target)) { + return false; + } + ++target; + ++pattern; + } + } + return !*target; +} + +#ifdef WIN32 +int ascii_string_compare(const wchar_t* s1, const char* s2, size_t n, + CharacterTransformation transformation) { + wchar_t c1, c2; + while (true) { + if (n-- == 0) return 0; + c1 = transformation(*s1); + // Double check that characters are not UTF-8 + ASSERT(static_cast(*s2) < 128); + // Note: *s2 gets implicitly promoted to wchar_t + c2 = transformation(*s2); + if (c1 != c2) return (c1 < c2) ? -1 : 1; + if (!c1) return 0; + ++s1; + ++s2; + } +} + +size_t asccpyn(wchar_t* buffer, size_t buflen, + const char* source, size_t srclen) { + if (buflen <= 0) + return 0; + + if (srclen == SIZE_UNKNOWN) { + srclen = strlenn(source, buflen - 1); + } else if (srclen >= buflen) { + srclen = buflen - 1; + } +#if _DEBUG + // Double check that characters are not UTF-8 + for (size_t pos = 0; pos < srclen; ++pos) + ASSERT(static_cast(source[pos]) < 128); +#endif // _DEBUG + std::copy(source, source + srclen, buffer); + buffer[srclen] = 0; + return srclen; +} + +#endif // WIN32 + +void replace_substrs(const char *search, + size_t search_len, + const char *replace, + size_t replace_len, + std::string *s) { + size_t pos = 0; + while ((pos = s->find(search, pos, search_len)) != std::string::npos) { + s->replace(pos, search_len, replace, replace_len); + pos += replace_len; + } +} + +bool starts_with(const char *s1, const char *s2) { + return strncmp(s1, s2, strlen(s2)) == 0; +} + +bool ends_with(const char *s1, const char *s2) { + size_t s1_length = strlen(s1); + size_t s2_length = strlen(s2); + + if (s2_length > s1_length) { + return false; + } + + const char* start = s1 + (s1_length - s2_length); + return strncmp(start, s2, s2_length) == 0; +} + +static const char kWhitespace[] = " \n\r\t"; + +std::string string_trim(const std::string& s) { + std::string::size_type first = s.find_first_not_of(kWhitespace); + std::string::size_type last = s.find_last_not_of(kWhitespace); + + if (first == std::string::npos || last == std::string::npos) { + return std::string(""); + } + + return s.substr(first, last - first + 1); +} + +} // namespace talk_base diff --git a/talk/base/stringutils.h b/talk/base/stringutils.h new file mode 100644 index 000000000..9f9e1a65a --- /dev/null +++ b/talk/base/stringutils.h @@ -0,0 +1,335 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_STRINGUTILS_H__ +#define TALK_BASE_STRINGUTILS_H__ + +#include +#include +#include + +#ifdef WIN32 +#include +#include +#define alloca _alloca +#endif // WIN32 + +#ifdef POSIX +#ifdef BSD +#include +#else // BSD +#include +#endif // !BSD +#endif // POSIX + +#include +#include + +#include "talk/base/basictypes.h" + +/////////////////////////////////////////////////////////////////////////////// +// Generic string/memory utilities +/////////////////////////////////////////////////////////////////////////////// + +#define STACK_ARRAY(TYPE, LEN) static_cast(::alloca((LEN)*sizeof(TYPE))) + +namespace talk_base { + +// Complement to memset. Verifies memory consists of count bytes of value c. +bool memory_check(const void* memory, int c, size_t count); + +// Determines whether the simple wildcard pattern matches target. +// Alpha characters in pattern match case-insensitively. +// Asterisks in pattern match 0 or more characters. +// Ex: string_match("www.TEST.GOOGLE.COM", "www.*.com") -> true +bool string_match(const char* target, const char* pattern); + +} // namespace talk_base + +/////////////////////////////////////////////////////////////////////////////// +// Rename a bunch of common string functions so they are consistent across +// platforms and between char and wchar_t variants. +// Here is the full list of functions that are unified: +// strlen, strcmp, stricmp, strncmp, strnicmp +// strchr, vsnprintf, strtoul, tolowercase +// tolowercase is like tolower, but not compatible with end-of-file value +// +// It's not clear if we will ever use wchar_t strings on unix. In theory, +// all strings should be Utf8 all the time, except when interfacing with Win32 +// APIs that require Utf16. +/////////////////////////////////////////////////////////////////////////////// + +inline char tolowercase(char c) { + return static_cast(tolower(c)); +} + +#ifdef WIN32 + +inline size_t strlen(const wchar_t* s) { + return wcslen(s); +} +inline int strcmp(const wchar_t* s1, const wchar_t* s2) { + return wcscmp(s1, s2); +} +inline int stricmp(const wchar_t* s1, const wchar_t* s2) { + return _wcsicmp(s1, s2); +} +inline int strncmp(const wchar_t* s1, const wchar_t* s2, size_t n) { + return wcsncmp(s1, s2, n); +} +inline int strnicmp(const wchar_t* s1, const wchar_t* s2, size_t n) { + return _wcsnicmp(s1, s2, n); +} +inline const wchar_t* strchr(const wchar_t* s, wchar_t c) { + return wcschr(s, c); +} +inline const wchar_t* strstr(const wchar_t* haystack, const wchar_t* needle) { + return wcsstr(haystack, needle); +} +#ifndef vsnprintf +inline int vsnprintf(wchar_t* buf, size_t n, const wchar_t* fmt, va_list args) { + return _vsnwprintf(buf, n, fmt, args); +} +#endif // !vsnprintf +inline unsigned long strtoul(const wchar_t* snum, wchar_t** end, int base) { + return wcstoul(snum, end, base); +} +inline wchar_t tolowercase(wchar_t c) { + return static_cast(towlower(c)); +} + +#endif // WIN32 + +#ifdef POSIX + +inline int _stricmp(const char* s1, const char* s2) { + return strcasecmp(s1, s2); +} +inline int _strnicmp(const char* s1, const char* s2, size_t n) { + return strncasecmp(s1, s2, n); +} + +#endif // POSIX + +/////////////////////////////////////////////////////////////////////////////// +// Traits simplifies porting string functions to be CTYPE-agnostic +/////////////////////////////////////////////////////////////////////////////// + +namespace talk_base { + +const size_t SIZE_UNKNOWN = static_cast(-1); + +template +struct Traits { + // STL string type + //typedef XXX string; + // Null-terminated string + //inline static const CTYPE* empty_str(); +}; + +/////////////////////////////////////////////////////////////////////////////// +// String utilities which work with char or wchar_t +/////////////////////////////////////////////////////////////////////////////// + +template +inline const CTYPE* nonnull(const CTYPE* str, const CTYPE* def_str = NULL) { + return str ? str : (def_str ? def_str : Traits::empty_str()); +} + +template +const CTYPE* strchr(const CTYPE* str, const CTYPE* chs) { + for (size_t i=0; str[i]; ++i) { + for (size_t j=0; chs[j]; ++j) { + if (str[i] == chs[j]) { + return str + i; + } + } + } + return 0; +} + +template +const CTYPE* strchrn(const CTYPE* str, size_t slen, CTYPE ch) { + for (size_t i=0; i +size_t strlenn(const CTYPE* buffer, size_t buflen) { + size_t bufpos = 0; + while (buffer[bufpos] && (bufpos < buflen)) { + ++bufpos; + } + return bufpos; +} + +// Safe versions of strncpy, strncat, snprintf and vsnprintf that always +// null-terminate. + +template +size_t strcpyn(CTYPE* buffer, size_t buflen, + const CTYPE* source, size_t srclen = SIZE_UNKNOWN) { + if (buflen <= 0) + return 0; + + if (srclen == SIZE_UNKNOWN) { + srclen = strlenn(source, buflen - 1); + } else if (srclen >= buflen) { + srclen = buflen - 1; + } + memcpy(buffer, source, srclen * sizeof(CTYPE)); + buffer[srclen] = 0; + return srclen; +} + +template +size_t strcatn(CTYPE* buffer, size_t buflen, + const CTYPE* source, size_t srclen = SIZE_UNKNOWN) { + if (buflen <= 0) + return 0; + + size_t bufpos = strlenn(buffer, buflen - 1); + return bufpos + strcpyn(buffer + bufpos, buflen - bufpos, source, srclen); +} + +// Some compilers (clang specifically) require vsprintfn be defined before +// sprintfn. +template +size_t vsprintfn(CTYPE* buffer, size_t buflen, const CTYPE* format, + va_list args) { + int len = vsnprintf(buffer, buflen, format, args); + if ((len < 0) || (static_cast(len) >= buflen)) { + len = static_cast(buflen - 1); + buffer[len] = 0; + } + return len; +} + +template +size_t sprintfn(CTYPE* buffer, size_t buflen, const CTYPE* format, ...); +template +size_t sprintfn(CTYPE* buffer, size_t buflen, const CTYPE* format, ...) { + va_list args; + va_start(args, format); + size_t len = vsprintfn(buffer, buflen, format, args); + va_end(args); + return len; +} + +/////////////////////////////////////////////////////////////////////////////// +// Allow safe comparing and copying ascii (not UTF-8) with both wide and +// non-wide character strings. +/////////////////////////////////////////////////////////////////////////////// + +inline int asccmp(const char* s1, const char* s2) { + return strcmp(s1, s2); +} +inline int ascicmp(const char* s1, const char* s2) { + return _stricmp(s1, s2); +} +inline int ascncmp(const char* s1, const char* s2, size_t n) { + return strncmp(s1, s2, n); +} +inline int ascnicmp(const char* s1, const char* s2, size_t n) { + return _strnicmp(s1, s2, n); +} +inline size_t asccpyn(char* buffer, size_t buflen, + const char* source, size_t srclen = SIZE_UNKNOWN) { + return strcpyn(buffer, buflen, source, srclen); +} + +#ifdef WIN32 + +typedef wchar_t(*CharacterTransformation)(wchar_t); +inline wchar_t identity(wchar_t c) { return c; } +int ascii_string_compare(const wchar_t* s1, const char* s2, size_t n, + CharacterTransformation transformation); + +inline int asccmp(const wchar_t* s1, const char* s2) { + return ascii_string_compare(s1, s2, static_cast(-1), identity); +} +inline int ascicmp(const wchar_t* s1, const char* s2) { + return ascii_string_compare(s1, s2, static_cast(-1), tolowercase); +} +inline int ascncmp(const wchar_t* s1, const char* s2, size_t n) { + return ascii_string_compare(s1, s2, n, identity); +} +inline int ascnicmp(const wchar_t* s1, const char* s2, size_t n) { + return ascii_string_compare(s1, s2, n, tolowercase); +} +size_t asccpyn(wchar_t* buffer, size_t buflen, + const char* source, size_t srclen = SIZE_UNKNOWN); + +#endif // WIN32 + +/////////////////////////////////////////////////////////////////////////////// +// Traits specializations +/////////////////////////////////////////////////////////////////////////////// + +template<> +struct Traits { + typedef std::string string; + inline static const char* empty_str() { return ""; } +}; + +/////////////////////////////////////////////////////////////////////////////// +// Traits specializations (Windows only, currently) +/////////////////////////////////////////////////////////////////////////////// + +#ifdef WIN32 + +template<> +struct Traits { + typedef std::wstring string; + inline static const wchar_t* Traits::empty_str() { return L""; } +}; + +#endif // WIN32 + +// Replaces all occurrences of "search" with "replace". +void replace_substrs(const char *search, + size_t search_len, + const char *replace, + size_t replace_len, + std::string *s); + +// True iff s1 starts with s2. +bool starts_with(const char *s1, const char *s2); + +// True iff s1 ends with s2. +bool ends_with(const char *s1, const char *s2); + +// Remove leading and trailing whitespaces. +std::string string_trim(const std::string& s); + +} // namespace talk_base + +#endif // TALK_BASE_STRINGUTILS_H__ diff --git a/talk/base/stringutils_unittest.cc b/talk/base/stringutils_unittest.cc new file mode 100644 index 000000000..56118698b --- /dev/null +++ b/talk/base/stringutils_unittest.cc @@ -0,0 +1,126 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/gunit.h" +#include "talk/base/stringutils.h" +#include "talk/base/common.h" + +namespace talk_base { + +// Tests for string_match(). + +TEST(string_matchTest, Matches) { + EXPECT_TRUE( string_match("A.B.C.D", "a.b.c.d")); + EXPECT_TRUE( string_match("www.TEST.GOOGLE.COM", "www.*.com")); + EXPECT_TRUE( string_match("127.0.0.1", "12*.0.*1")); + EXPECT_TRUE( string_match("127.1.0.21", "12*.0.*1")); + EXPECT_FALSE(string_match("127.0.0.0", "12*.0.*1")); + EXPECT_FALSE(string_match("127.0.0.0", "12*.0.*1")); + EXPECT_FALSE(string_match("127.1.1.21", "12*.0.*1")); +} + +// It's not clear if we will ever use wchar_t strings on unix. In theory, +// all strings should be Utf8 all the time, except when interfacing with Win32 +// APIs that require Utf16. + +#ifdef WIN32 + +// Tests for ascii_string_compare(). + +// Tests NULL input. +TEST(ascii_string_compareTest, NullInput) { + // The following results in an access violation in + // ascii_string_compare. Is this a bug or by design? stringutils.h + // should document the expected behavior in this case. + + // EXPECT_EQ(0, ascii_string_compare(NULL, NULL, 1, identity)); +} + +// Tests comparing two strings of different lengths. +TEST(ascii_string_compareTest, DifferentLengths) { + EXPECT_EQ(-1, ascii_string_compare(L"Test", "Test1", 5, identity)); +} + +// Tests the case where the buffer size is smaller than the string +// lengths. +TEST(ascii_string_compareTest, SmallBuffer) { + EXPECT_EQ(0, ascii_string_compare(L"Test", "Test1", 3, identity)); +} + +// Tests the case where the buffer is not full. +TEST(ascii_string_compareTest, LargeBuffer) { + EXPECT_EQ(0, ascii_string_compare(L"Test", "Test", 10, identity)); +} + +// Tests comparing two eqaul strings. +TEST(ascii_string_compareTest, Equal) { + EXPECT_EQ(0, ascii_string_compare(L"Test", "Test", 5, identity)); + EXPECT_EQ(0, ascii_string_compare(L"TeSt", "tEsT", 5, tolowercase)); +} + +// Tests comparing a smller string to a larger one. +TEST(ascii_string_compareTest, LessThan) { + EXPECT_EQ(-1, ascii_string_compare(L"abc", "abd", 4, identity)); + EXPECT_EQ(-1, ascii_string_compare(L"ABC", "abD", 5, tolowercase)); +} + +// Tests comparing a larger string to a smaller one. +TEST(ascii_string_compareTest, GreaterThan) { + EXPECT_EQ(1, ascii_string_compare(L"xyz", "xy", 5, identity)); + EXPECT_EQ(1, ascii_string_compare(L"abc", "ABB", 5, tolowercase)); +} +#endif // WIN32 + +TEST(string_trim_Test, Trimming) { + EXPECT_EQ("temp", string_trim("\n\r\t temp \n\r\t")); + EXPECT_EQ("temp\n\r\t temp", string_trim(" temp\n\r\t temp ")); + EXPECT_EQ("temp temp", string_trim("temp temp")); + EXPECT_EQ("", string_trim(" \r\n\t")); + EXPECT_EQ("", string_trim("")); +} + +TEST(string_startsTest, StartsWith) { + EXPECT_TRUE(starts_with("foobar", "foo")); + EXPECT_TRUE(starts_with("foobar", "foobar")); + EXPECT_TRUE(starts_with("foobar", "")); + EXPECT_TRUE(starts_with("", "")); + EXPECT_FALSE(starts_with("foobar", "bar")); + EXPECT_FALSE(starts_with("foobar", "foobarbaz")); + EXPECT_FALSE(starts_with("", "f")); +} + +TEST(string_endsTest, EndsWith) { + EXPECT_TRUE(ends_with("foobar", "bar")); + EXPECT_TRUE(ends_with("foobar", "foobar")); + EXPECT_TRUE(ends_with("foobar", "")); + EXPECT_TRUE(ends_with("", "")); + EXPECT_FALSE(ends_with("foobar", "foo")); + EXPECT_FALSE(ends_with("foobar", "foobarbaz")); + EXPECT_FALSE(ends_with("", "f")); +} + +} // namespace talk_base diff --git a/talk/base/systeminfo.cc b/talk/base/systeminfo.cc new file mode 100644 index 000000000..ec1865889 --- /dev/null +++ b/talk/base/systeminfo.cc @@ -0,0 +1,533 @@ +/* + * libjingle + * Copyright 2008 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/base/systeminfo.h" + +#if defined(WIN32) +#include +#ifndef EXCLUDE_D3D9 +#include +#endif +#include // for __cpuid() +#elif defined(OSX) +#include +#include +#elif defined(LINUX) || defined(ANDROID) +#include +#endif +#if defined(OSX) || defined(IOS) +#include +#endif + +#if defined(WIN32) +#include "talk/base/scoped_ptr.h" +#include "talk/base/win32.h" +#elif defined(OSX) +#include "talk/base/macconversion.h" +#elif defined(LINUX) || defined(ANDROID) +#include "talk/base/linux.h" +#endif +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/stringutils.h" + +namespace talk_base { + +// See Also: http://msdn.microsoft.com/en-us/library/ms683194(v=vs.85).aspx +#if defined(WIN32) +typedef BOOL (WINAPI *LPFN_GLPI)( + PSYSTEM_LOGICAL_PROCESSOR_INFORMATION, + PDWORD); + +static void GetProcessorInformation(int* physical_cpus, int* cache_size) { + // GetLogicalProcessorInformation() is available on Windows XP SP3 and beyond. + LPFN_GLPI glpi = reinterpret_cast(GetProcAddress( + GetModuleHandle(L"kernel32"), + "GetLogicalProcessorInformation")); + if (NULL == glpi) { + return; + } + // Determine buffer size, allocate and get processor information. + // Size can change between calls (unlikely), so a loop is done. + DWORD return_length = 0; + scoped_array infos; + while (!glpi(infos.get(), &return_length)) { + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + infos.reset(new SYSTEM_LOGICAL_PROCESSOR_INFORMATION[ + return_length / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION)]); + } else { + return; + } + } + *physical_cpus = 0; + *cache_size = 0; + for (size_t i = 0; + i < return_length / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION); ++i) { + if (infos[i].Relationship == RelationProcessorCore) { + ++*physical_cpus; + } else if (infos[i].Relationship == RelationCache) { + int next_cache_size = static_cast(infos[i].Cache.Size); + if (next_cache_size >= *cache_size) { + *cache_size = next_cache_size; + } + } + } + return; +} +#else +// TODO(fbarchard): Use gcc 4.4 provided cpuid intrinsic +// 32 bit fpic requires ebx be preserved +#if (defined(__pic__) || defined(__APPLE__)) && defined(__i386__) +static inline void __cpuid(int cpu_info[4], int info_type) { + __asm__ volatile ( // NOLINT + "mov %%ebx, %%edi\n" + "cpuid\n" + "xchg %%edi, %%ebx\n" + : "=a"(cpu_info[0]), "=D"(cpu_info[1]), "=c"(cpu_info[2]), "=d"(cpu_info[3]) + : "a"(info_type) + ); // NOLINT +} +#elif defined(__i386__) || defined(__x86_64__) +static inline void __cpuid(int cpu_info[4], int info_type) { + __asm__ volatile ( // NOLINT + "cpuid\n" + : "=a"(cpu_info[0]), "=b"(cpu_info[1]), "=c"(cpu_info[2]), "=d"(cpu_info[3]) + : "a"(info_type) + ); // NOLINT +} +#endif +#endif // WIN32 + +// Note(fbarchard): +// Family and model are extended family and extended model. 8 bits each. +SystemInfo::SystemInfo() + : physical_cpus_(1), logical_cpus_(1), cache_size_(0), + cpu_family_(0), cpu_model_(0), cpu_stepping_(0), + cpu_speed_(0), memory_(0) { + // Initialize the basic information. +#if defined(__arm__) || defined(_M_ARM) + cpu_arch_ = SI_ARCH_ARM; +#elif defined(__x86_64__) || defined(_M_X64) + cpu_arch_ = SI_ARCH_X64; +#elif defined(__i386__) || defined(_M_IX86) + cpu_arch_ = SI_ARCH_X86; +#else + cpu_arch_ = SI_ARCH_UNKNOWN; +#endif + +#if defined(WIN32) + SYSTEM_INFO si; + GetSystemInfo(&si); + logical_cpus_ = si.dwNumberOfProcessors; + GetProcessorInformation(&physical_cpus_, &cache_size_); + if (physical_cpus_ <= 0) { + physical_cpus_ = logical_cpus_; + } + cpu_family_ = si.wProcessorLevel; + cpu_model_ = si.wProcessorRevision >> 8; + cpu_stepping_ = si.wProcessorRevision & 0xFF; +#elif defined(OSX) || defined(IOS) + uint32_t sysctl_value; + size_t length = sizeof(sysctl_value); + if (!sysctlbyname("hw.physicalcpu_max", &sysctl_value, &length, NULL, 0)) { + physical_cpus_ = static_cast(sysctl_value); + } + length = sizeof(sysctl_value); + if (!sysctlbyname("hw.logicalcpu_max", &sysctl_value, &length, NULL, 0)) { + logical_cpus_ = static_cast(sysctl_value); + } + uint64_t sysctl_value64; + length = sizeof(sysctl_value64); + if (!sysctlbyname("hw.l3cachesize", &sysctl_value64, &length, NULL, 0)) { + cache_size_ = static_cast(sysctl_value64); + } + if (!cache_size_) { + length = sizeof(sysctl_value64); + if (!sysctlbyname("hw.l2cachesize", &sysctl_value64, &length, NULL, 0)) { + cache_size_ = static_cast(sysctl_value64); + } + } + length = sizeof(sysctl_value); + if (!sysctlbyname("machdep.cpu.family", &sysctl_value, &length, NULL, 0)) { + cpu_family_ = static_cast(sysctl_value); + } + length = sizeof(sysctl_value); + if (!sysctlbyname("machdep.cpu.model", &sysctl_value, &length, NULL, 0)) { + cpu_model_ = static_cast(sysctl_value); + } + length = sizeof(sysctl_value); + if (!sysctlbyname("machdep.cpu.stepping", &sysctl_value, &length, NULL, 0)) { + cpu_stepping_ = static_cast(sysctl_value); + } +#else // LINUX || ANDROID + ProcCpuInfo proc_info; + if (proc_info.LoadFromSystem()) { + proc_info.GetNumCpus(&logical_cpus_); + proc_info.GetNumPhysicalCpus(&physical_cpus_); + proc_info.GetCpuFamily(&cpu_family_); +#if defined(CPU_X86) + // These values only apply to x86 systems. + proc_info.GetSectionIntValue(0, "model", &cpu_model_); + proc_info.GetSectionIntValue(0, "stepping", &cpu_stepping_); + proc_info.GetSectionIntValue(0, "cpu MHz", &cpu_speed_); + proc_info.GetSectionIntValue(0, "cache size", &cache_size_); + cache_size_ *= 1024; +#endif + } + // ProcCpuInfo reads cpu speed from "cpu MHz" under /proc/cpuinfo. + // But that number is a moving target which can change on-the-fly according to + // many factors including system workload. + // See /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors. + // The one in /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq is more + // accurate. We use it as our cpu speed when it is available. + // cpuinfo_max_freq is measured in KHz and requires conversion to MHz. + int max_freq = talk_base::ReadCpuMaxFreq(); + if (max_freq > 0) { + cpu_speed_ = max_freq / 1000; + } +#endif +// For L2 CacheSize see also +// http://www.flounder.com/cpuid_explorer2.htm#CPUID(0x800000006) +#ifdef CPU_X86 + if (cache_size_ == 0) { + int cpu_info[4]; + __cpuid(cpu_info, 0x80000000); // query maximum extended cpuid function. + if (static_cast(cpu_info[0]) >= 0x80000006) { + __cpuid(cpu_info, 0x80000006); + cache_size_ = (cpu_info[2] >> 16) * 1024; + } + } +#endif +} + +// Return the number of cpu threads available to the system. +int SystemInfo::GetMaxCpus() { + return logical_cpus_; +} + +// Return the number of cpu cores available to the system. +int SystemInfo::GetMaxPhysicalCpus() { + return physical_cpus_; +} + +// Return the number of cpus available to the process. Since affinity can be +// changed on the fly, do not cache this value. +// Can be affected by heat. +int SystemInfo::GetCurCpus() { + int cur_cpus; +#if defined(WIN32) + DWORD_PTR process_mask, system_mask; + ::GetProcessAffinityMask(::GetCurrentProcess(), &process_mask, &system_mask); + for (cur_cpus = 0; process_mask; ++cur_cpus) { + // Sparse-ones algorithm. There are slightly faster methods out there but + // they are unintuitive and won't make a difference on a single dword. + process_mask &= (process_mask - 1); + } +#elif defined(OSX) || defined(IOS) + uint32_t sysctl_value; + size_t length = sizeof(sysctl_value); + int error = sysctlbyname("hw.ncpu", &sysctl_value, &length, NULL, 0); + cur_cpus = !error ? static_cast(sysctl_value) : 1; +#else + // Linux, Solaris, ANDROID + cur_cpus = static_cast(sysconf(_SC_NPROCESSORS_ONLN)); +#endif + return cur_cpus; +} + +// Return the type of this CPU. +SystemInfo::Architecture SystemInfo::GetCpuArchitecture() { + return cpu_arch_; +} + +// Returns the vendor string from the cpu, e.g. "GenuineIntel", "AuthenticAMD". +// See "Intel Processor Identification and the CPUID Instruction" +// (Intel document number: 241618) +std::string SystemInfo::GetCpuVendor() { + if (cpu_vendor_.empty()) { +#if defined(CPU_X86) + int cpu_info[4]; + __cpuid(cpu_info, 0); + cpu_info[0] = cpu_info[1]; // Reorder output + cpu_info[1] = cpu_info[3]; + cpu_info[2] = cpu_info[2]; + cpu_info[3] = 0; + cpu_vendor_ = std::string(reinterpret_cast(&cpu_info[0])); +#elif defined(CPU_ARM) + cpu_vendor_ = std::string("ARM"); +#else + cpu_vendor_ = std::string("Undefined"); +#endif + } + return cpu_vendor_; +} + +int SystemInfo::GetCpuCacheSize() { + return cache_size_; +} + +// Return the "family" of this CPU. +int SystemInfo::GetCpuFamily() { + return cpu_family_; +} + +// Return the "model" of this CPU. +int SystemInfo::GetCpuModel() { + return cpu_model_; +} + +// Return the "stepping" of this CPU. +int SystemInfo::GetCpuStepping() { + return cpu_stepping_; +} + +// Return the clockrate of the primary processor in Mhz. This value can be +// cached. Returns -1 on error. +int SystemInfo::GetMaxCpuSpeed() { + if (cpu_speed_) { + return cpu_speed_; + } +#if defined(WIN32) + HKEY key; + static const WCHAR keyName[] = + L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0"; + + if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, keyName , 0, KEY_QUERY_VALUE, &key) + == ERROR_SUCCESS) { + DWORD data, len; + len = sizeof(data); + + if (RegQueryValueEx(key, L"~Mhz", 0, 0, reinterpret_cast(&data), + &len) == ERROR_SUCCESS) { + cpu_speed_ = data; + } else { + LOG(LS_WARNING) << "Failed to query registry value HKLM\\" << keyName + << "\\~Mhz"; + cpu_speed_ = -1; + } + + RegCloseKey(key); + } else { + LOG(LS_WARNING) << "Failed to open registry key HKLM\\" << keyName; + cpu_speed_ = -1; + } +#elif defined(IOS) || defined(OSX) + uint64_t sysctl_value; + size_t length = sizeof(sysctl_value); + int error = sysctlbyname("hw.cpufrequency_max", &sysctl_value, &length, + NULL, 0); + cpu_speed_ = !error ? static_cast(sysctl_value/1000000) : -1; +#else + // TODO(fbarchard): Implement using proc/cpuinfo + cpu_speed_ = 0; +#endif + return cpu_speed_; +} + +// Dynamically check the current clockrate, which could be reduced because of +// powersaving profiles. Eventually for windows we want to query WMI for +// root\WMI::ProcessorPerformance.InstanceName="Processor_Number_0".frequency +int SystemInfo::GetCurCpuSpeed() { +#if defined(WIN32) + // TODO(fbarchard): Add WMI check, requires COM initialization + // NOTE(fbarchard): Testable on Sandy Bridge. + return GetMaxCpuSpeed(); +#elif defined(IOS) || defined(OSX) + uint64_t sysctl_value; + size_t length = sizeof(sysctl_value); + int error = sysctlbyname("hw.cpufrequency", &sysctl_value, &length, NULL, 0); + return !error ? static_cast(sysctl_value/1000000) : GetMaxCpuSpeed(); +#else // LINUX || ANDROID + // TODO(fbarchard): Use proc/cpuinfo for Cur speed on Linux. + return GetMaxCpuSpeed(); +#endif +} + +// Returns the amount of installed physical memory in Bytes. Cacheable. +// Returns -1 on error. +int64 SystemInfo::GetMemorySize() { + if (memory_) { + return memory_; + } + +#if defined(WIN32) + MEMORYSTATUSEX status = {0}; + status.dwLength = sizeof(status); + + if (GlobalMemoryStatusEx(&status)) { + memory_ = status.ullTotalPhys; + } else { + LOG_GLE(LS_WARNING) << "GlobalMemoryStatusEx failed."; + memory_ = -1; + } + +#elif defined(OSX) || defined(IOS) + size_t len = sizeof(memory_); + int error = sysctlbyname("hw.memsize", &memory_, &len, NULL, 0); + if (error || memory_ == 0) { + memory_ = -1; + } +#else // LINUX || ANDROID + memory_ = static_cast(sysconf(_SC_PHYS_PAGES)) * + static_cast(sysconf(_SC_PAGESIZE)); + if (memory_ < 0) { + LOG(LS_WARNING) << "sysconf(_SC_PHYS_PAGES) failed." + << "sysconf(_SC_PHYS_PAGES) " << sysconf(_SC_PHYS_PAGES) + << "sysconf(_SC_PAGESIZE) " << sysconf(_SC_PAGESIZE); + memory_ = -1; + } +#endif + + return memory_; +} + + +// Return the name of the machine model we are currently running on. +// This is a human readable string that consists of the name and version +// number of the hardware, i.e 'MacBookAir1,1'. Returns an empty string if +// model can not be determined. The string is cached for subsequent calls. +std::string SystemInfo::GetMachineModel() { + if (!machine_model_.empty()) { + return machine_model_; + } + +#if defined(OSX) || defined(IOS) + char buffer[128]; + size_t length = sizeof(buffer); + int error = sysctlbyname("hw.model", buffer, &length, NULL, 0); + if (!error) { + machine_model_.assign(buffer, length - 1); + } else { + machine_model_.clear(); + } +#else + machine_model_ = "Not available"; +#endif + + return machine_model_; +} + +#ifdef OSX +// Helper functions to query IOKit for video hardware properties. +static CFTypeRef SearchForProperty(io_service_t port, CFStringRef name) { + return IORegistryEntrySearchCFProperty(port, kIOServicePlane, + name, kCFAllocatorDefault, + kIORegistryIterateRecursively | kIORegistryIterateParents); +} + +static void GetProperty(io_service_t port, CFStringRef name, int* value) { + if (!value) return; + CFTypeRef ref = SearchForProperty(port, name); + if (ref) { + CFTypeID refType = CFGetTypeID(ref); + if (CFNumberGetTypeID() == refType) { + CFNumberRef number = reinterpret_cast(ref); + p_convertCFNumberToInt(number, value); + } else if (CFDataGetTypeID() == refType) { + CFDataRef data = reinterpret_cast(ref); + if (CFDataGetLength(data) == sizeof(UInt32)) { + *value = *reinterpret_cast(CFDataGetBytePtr(data)); + } + } + CFRelease(ref); + } +} + +static void GetProperty(io_service_t port, CFStringRef name, + std::string* value) { + if (!value) return; + CFTypeRef ref = SearchForProperty(port, name); + if (ref) { + CFTypeID refType = CFGetTypeID(ref); + if (CFStringGetTypeID() == refType) { + CFStringRef stringRef = reinterpret_cast(ref); + p_convertHostCFStringRefToCPPString(stringRef, *value); + } else if (CFDataGetTypeID() == refType) { + CFDataRef dataRef = reinterpret_cast(ref); + *value = std::string(reinterpret_cast( + CFDataGetBytePtr(dataRef)), CFDataGetLength(dataRef)); + } + CFRelease(ref); + } +} +#endif + +// Fills a struct with information on the graphics adapater and returns true +// iff successful. +bool SystemInfo::GetGpuInfo(GpuInfo *info) { + if (!info) return false; +#if defined(WIN32) && !defined(EXCLUDE_D3D9) + D3DADAPTER_IDENTIFIER9 identifier; + HRESULT hr = E_FAIL; + HINSTANCE d3d_lib = LoadLibrary(L"d3d9.dll"); + + if (d3d_lib) { + typedef IDirect3D9* (WINAPI *D3DCreate9Proc)(UINT); + D3DCreate9Proc d3d_create_proc = reinterpret_cast( + GetProcAddress(d3d_lib, "Direct3DCreate9")); + if (d3d_create_proc) { + IDirect3D9* d3d = d3d_create_proc(D3D_SDK_VERSION); + if (d3d) { + hr = d3d->GetAdapterIdentifier(D3DADAPTER_DEFAULT, 0, &identifier); + d3d->Release(); + } + } + FreeLibrary(d3d_lib); + } + + if (hr != D3D_OK) { + LOG(LS_ERROR) << "Failed to access Direct3D9 information."; + return false; + } + + info->device_name = identifier.DeviceName; + info->description = identifier.Description; + info->vendor_id = identifier.VendorId; + info->device_id = identifier.DeviceId; + info->driver = identifier.Driver; + // driver_version format: product.version.subversion.build + std::stringstream ss; + ss << HIWORD(identifier.DriverVersion.HighPart) << "." + << LOWORD(identifier.DriverVersion.HighPart) << "." + << HIWORD(identifier.DriverVersion.LowPart) << "." + << LOWORD(identifier.DriverVersion.LowPart); + info->driver_version = ss.str(); + return true; +#elif defined(OSX) + // We'll query the IOKit for the gpu of the main display. + io_service_t display_service_port = CGDisplayIOServicePort( + kCGDirectMainDisplay); + GetProperty(display_service_port, CFSTR("vendor-id"), &info->vendor_id); + GetProperty(display_service_port, CFSTR("device-id"), &info->device_id); + GetProperty(display_service_port, CFSTR("model"), &info->description); + return true; +#else // LINUX || ANDROID + // TODO(fbarchard): Implement this on Linux + return false; +#endif +} +} // namespace talk_base diff --git a/talk/base/systeminfo.h b/talk/base/systeminfo.h new file mode 100644 index 000000000..f84b5fe98 --- /dev/null +++ b/talk/base/systeminfo.h @@ -0,0 +1,98 @@ +/* + * libjingle + * Copyright 2008 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. + */ + +#ifndef TALK_BASE_SYSTEMINFO_H__ +#define TALK_BASE_SYSTEMINFO_H__ + +#include + +#include "talk/base/basictypes.h" + +namespace talk_base { + +class SystemInfo { + public: + enum Architecture { + SI_ARCH_UNKNOWN = -1, + SI_ARCH_X86 = 0, + SI_ARCH_X64 = 1, + SI_ARCH_ARM = 2 + }; + + SystemInfo(); + + // The number of CPU Cores in the system. + int GetMaxPhysicalCpus(); + // The number of CPU Threads in the system. + int GetMaxCpus(); + // The number of CPU Threads currently available to this process. + int GetCurCpus(); + // Identity of the CPUs. + Architecture GetCpuArchitecture(); + std::string GetCpuVendor(); + int GetCpuFamily(); + int GetCpuModel(); + int GetCpuStepping(); + // Return size of CPU cache in bytes. Uses largest available cache (L3). + int GetCpuCacheSize(); + // Estimated speed of the CPUs, in MHz. e.g. 2400 for 2.4 GHz + int GetMaxCpuSpeed(); + int GetCurCpuSpeed(); + // Total amount of physical memory, in bytes. + int64 GetMemorySize(); + // The model name of the machine, e.g. "MacBookAir1,1" + std::string GetMachineModel(); + + // The gpu identifier + struct GpuInfo { + GpuInfo() : vendor_id(0), device_id(0) {} + std::string device_name; + std::string description; + int vendor_id; + int device_id; + std::string driver; + std::string driver_version; + }; + bool GetGpuInfo(GpuInfo *info); + + private: + int physical_cpus_; + int logical_cpus_; + int cache_size_; + Architecture cpu_arch_; + std::string cpu_vendor_; + int cpu_family_; + int cpu_model_; + int cpu_stepping_; + int cpu_speed_; + int64 memory_; + std::string machine_model_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_SYSTEMINFO_H__ diff --git a/talk/base/systeminfo_unittest.cc b/talk/base/systeminfo_unittest.cc new file mode 100644 index 000000000..310b1eb62 --- /dev/null +++ b/talk/base/systeminfo_unittest.cc @@ -0,0 +1,211 @@ +/* + * libjingle + * Copyright 2009 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/base/gunit.h" +#include "talk/base/stringutils.h" +#include "talk/base/systeminfo.h" + +#if defined(CPU_X86) || defined(CPU_ARM) +TEST(SystemInfoTest, CpuVendorNonEmpty) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "CpuVendor: " << info.GetCpuVendor(); + EXPECT_FALSE(info.GetCpuVendor().empty()); +} + +// Tests Vendor identification is Intel or AMD. +// See Also http://en.wikipedia.org/wiki/CPUID +TEST(SystemInfoTest, CpuVendorIntelAMDARM) { + talk_base::SystemInfo info; +#if defined(CPU_X86) + EXPECT_TRUE(talk_base::string_match(info.GetCpuVendor().c_str(), + "GenuineIntel") || + talk_base::string_match(info.GetCpuVendor().c_str(), + "AuthenticAMD")); +#elif defined(CPU_ARM) + EXPECT_TRUE(talk_base::string_match(info.GetCpuVendor().c_str(), "ARM")); +#endif +} +#endif // defined(CPU_X86) || defined(CPU_ARM) + +// Tests CpuArchitecture matches expectations. +TEST(SystemInfoTest, GetCpuArchitecture) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "CpuArchitecture: " << info.GetCpuArchitecture(); + talk_base::SystemInfo::Architecture architecture = info.GetCpuArchitecture(); +#if defined(CPU_X86) || defined(CPU_ARM) + if (sizeof(intptr_t) == 8) { + EXPECT_EQ(talk_base::SystemInfo::SI_ARCH_X64, architecture); + } else if (sizeof(intptr_t) == 4) { +#if defined(CPU_ARM) + EXPECT_EQ(talk_base::SystemInfo::SI_ARCH_ARM, architecture); +#else + EXPECT_EQ(talk_base::SystemInfo::SI_ARCH_X86, architecture); +#endif + } +#endif +} + +// Tests Cpu Cache Size +TEST(SystemInfoTest, CpuCacheSize) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "CpuCacheSize: " << info.GetCpuCacheSize(); + EXPECT_GE(info.GetCpuCacheSize(), 8192); // 8 KB min cache + EXPECT_LE(info.GetCpuCacheSize(), 1024 * 1024 * 1024); // 1 GB max cache +} + +// Tests MachineModel is set. On Mac test machine model is known. +TEST(SystemInfoTest, MachineModelKnown) { + talk_base::SystemInfo info; + EXPECT_FALSE(info.GetMachineModel().empty()); + const char *machine_model = info.GetMachineModel().c_str(); + LOG(LS_INFO) << "MachineModel: " << machine_model; + bool known = true; +#if defined(OSX) + // Full list as of May 2012. Update when new OSX based models are added. + known = talk_base::string_match(machine_model, "MacBookPro*") || + talk_base::string_match(machine_model, "MacBookAir*") || + talk_base::string_match(machine_model, "MacBook*") || + talk_base::string_match(machine_model, "MacPro*") || + talk_base::string_match(machine_model, "Macmini*") || + talk_base::string_match(machine_model, "iMac*") || + talk_base::string_match(machine_model, "Xserve*"); +#elif !defined(IOS) + // All other machines return Not available. + known = talk_base::string_match(info.GetMachineModel().c_str(), + "Not available"); +#endif + if (!known) { + LOG(LS_WARNING) << "Machine Model Unknown: " << machine_model; + } +} + +// Tests maximum cpu clockrate. +TEST(SystemInfoTest, CpuMaxCpuSpeed) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "MaxCpuSpeed: " << info.GetMaxCpuSpeed(); + EXPECT_GT(info.GetMaxCpuSpeed(), 0); + EXPECT_LT(info.GetMaxCpuSpeed(), 100000); // 100 Ghz +} + +// Tests current cpu clockrate. +TEST(SystemInfoTest, CpuCurCpuSpeed) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "MaxCurSpeed: " << info.GetCurCpuSpeed(); + EXPECT_GT(info.GetCurCpuSpeed(), 0); + EXPECT_LT(info.GetMaxCpuSpeed(), 100000); +} + +// Tests physical memory size. +TEST(SystemInfoTest, MemorySize) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "MemorySize: " << info.GetMemorySize(); + EXPECT_GT(info.GetMemorySize(), -1); +} + +// Tests number of logical cpus available to the system. +TEST(SystemInfoTest, MaxCpus) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "MaxCpus: " << info.GetMaxCpus(); + EXPECT_GT(info.GetMaxCpus(), 0); +} + +// Tests number of physical cpus available to the system. +TEST(SystemInfoTest, MaxPhysicalCpus) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "MaxPhysicalCpus: " << info.GetMaxPhysicalCpus(); + EXPECT_GT(info.GetMaxPhysicalCpus(), 0); + EXPECT_LE(info.GetMaxPhysicalCpus(), info.GetMaxCpus()); +} + +// Tests number of logical cpus available to the process. +TEST(SystemInfoTest, CurCpus) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "CurCpus: " << info.GetCurCpus(); + EXPECT_GT(info.GetCurCpus(), 0); + EXPECT_LE(info.GetCurCpus(), info.GetMaxCpus()); +} + +#ifdef CPU_X86 +// CPU family/model/stepping is only available on X86. The following tests +// that they are set when running on x86 CPUs. Valid Family/Model/Stepping +// values are non-zero on known CPUs. + +// Tests Intel CPU Family identification. +TEST(SystemInfoTest, CpuFamily) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "CpuFamily: " << info.GetCpuFamily(); + EXPECT_GT(info.GetCpuFamily(), 0); +} + +// Tests Intel CPU Model identification. +TEST(SystemInfoTest, CpuModel) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "CpuModel: " << info.GetCpuModel(); + EXPECT_GT(info.GetCpuModel(), 0); +} + +// Tests Intel CPU Stepping identification. +TEST(SystemInfoTest, CpuStepping) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "CpuStepping: " << info.GetCpuStepping(); + EXPECT_GT(info.GetCpuStepping(), 0); +} +#else // CPU_X86 +// If not running on x86 CPU the following tests expect the functions to +// return 0. +TEST(SystemInfoTest, CpuFamily) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "CpuFamily: " << info.GetCpuFamily(); + EXPECT_EQ(0, info.GetCpuFamily()); +} + +// Tests Intel CPU Model identification. +TEST(SystemInfoTest, CpuModel) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "CpuModel: " << info.GetCpuModel(); + EXPECT_EQ(0, info.GetCpuModel()); +} + +// Tests Intel CPU Stepping identification. +TEST(SystemInfoTest, CpuStepping) { + talk_base::SystemInfo info; + LOG(LS_INFO) << "CpuStepping: " << info.GetCpuStepping(); + EXPECT_EQ(0, info.GetCpuStepping()); +} +#endif // CPU_X86 + +#if WIN32 && !defined(EXCLUDE_D3D9) +TEST(SystemInfoTest, GpuInfo) { + talk_base::SystemInfo info; + talk_base::SystemInfo::GpuInfo gi; + EXPECT_TRUE(info.GetGpuInfo(&gi)); + LOG(LS_INFO) << "GpuDriver: " << gi.driver; + EXPECT_FALSE(gi.driver.empty()); + LOG(LS_INFO) << "GpuDriverVersion: " << gi.driver_version; + EXPECT_FALSE(gi.driver_version.empty()); +} +#endif diff --git a/talk/base/task.cc b/talk/base/task.cc new file mode 100644 index 000000000..c37797cf6 --- /dev/null +++ b/talk/base/task.cc @@ -0,0 +1,289 @@ +/* + * 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/base/task.h" +#include "talk/base/common.h" +#include "talk/base/taskrunner.h" + +namespace talk_base { + +int32 Task::unique_id_seed_ = 0; + +Task::Task(TaskParent *parent) + : TaskParent(this, parent), + state_(STATE_INIT), + blocked_(false), + done_(false), + aborted_(false), + busy_(false), + error_(false), + start_time_(0), + timeout_time_(0), + timeout_seconds_(0), + timeout_suspended_(false) { + unique_id_ = unique_id_seed_++; + + // sanity check that we didn't roll-over our id seed + ASSERT(unique_id_ < unique_id_seed_); +} + +Task::~Task() { + // Is this task being deleted in the correct manner? + ASSERT(!done_ || GetRunner()->is_ok_to_delete(this)); + ASSERT(state_ == STATE_INIT || done_); + ASSERT(state_ == STATE_INIT || blocked_); + + // If the task is being deleted without being done, it + // means that it hasn't been removed from its parent. + // This happens if a task is deleted outside of TaskRunner. + if (!done_) { + Stop(); + } +} + +int64 Task::CurrentTime() { + return GetRunner()->CurrentTime(); +} + +int64 Task::ElapsedTime() { + return CurrentTime() - start_time_; +} + +void Task::Start() { + if (state_ != STATE_INIT) + return; + // Set the start time before starting the task. Otherwise if the task + // finishes quickly and deletes the Task object, setting start_time_ + // will crash. + start_time_ = CurrentTime(); + GetRunner()->StartTask(this); +} + +void Task::Step() { + if (done_) { +#ifdef _DEBUG + // we do not know how !blocked_ happens when done_ - should be impossible. + // But it causes problems, so in retail build, we force blocked_, and + // under debug we assert. + ASSERT(blocked_); +#else + blocked_ = true; +#endif + return; + } + + // Async Error() was called + if (error_) { + done_ = true; + state_ = STATE_ERROR; + blocked_ = true; +// obsolete - an errored task is not considered done now +// SignalDone(); + + Stop(); +#ifdef _DEBUG + // verify that stop removed this from its parent + ASSERT(!parent()->IsChildTask(this)); +#endif + return; + } + + busy_ = true; + int new_state = Process(state_); + busy_ = false; + + if (aborted_) { + Abort(true); // no need to wake because we're awake + return; + } + + if (new_state == STATE_BLOCKED) { + blocked_ = true; + // Let the timeout continue + } else { + state_ = new_state; + blocked_ = false; + ResetTimeout(); + } + + if (new_state == STATE_DONE) { + done_ = true; + } else if (new_state == STATE_ERROR) { + done_ = true; + error_ = true; + } + + if (done_) { +// obsolete - call this yourself +// SignalDone(); + + Stop(); +#if _DEBUG + // verify that stop removed this from its parent + ASSERT(!parent()->IsChildTask(this)); +#endif + blocked_ = true; + } +} + +void Task::Abort(bool nowake) { + // Why only check for done_ (instead of "aborted_ || done_")? + // + // If aborted_ && !done_, it means the logic for aborting still + // needs to be executed (because busy_ must have been true when + // Abort() was previously called). + if (done_) + return; + aborted_ = true; + if (!busy_) { + done_ = true; + blocked_ = true; + error_ = true; + + // "done_" is set before calling "Stop()" to ensure that this code + // doesn't execute more than once (recursively) for the same task. + Stop(); +#ifdef _DEBUG + // verify that stop removed this from its parent + ASSERT(!parent()->IsChildTask(this)); +#endif + if (!nowake) { + // WakeTasks to self-delete. + // Don't call Wake() because it is a no-op after "done_" is set. + // Even if Wake() did run, it clears "blocked_" which isn't desireable. + GetRunner()->WakeTasks(); + } + } +} + +void Task::Wake() { + if (done_) + return; + if (blocked_) { + blocked_ = false; + GetRunner()->WakeTasks(); + } +} + +void Task::Error() { + if (error_ || done_) + return; + error_ = true; + Wake(); +} + +std::string Task::GetStateName(int state) const { + switch (state) { + case STATE_BLOCKED: return "BLOCKED"; + case STATE_INIT: return "INIT"; + case STATE_START: return "START"; + case STATE_DONE: return "DONE"; + case STATE_ERROR: return "ERROR"; + case STATE_RESPONSE: return "RESPONSE"; + } + return "??"; +} + +int Task::Process(int state) { + int newstate = STATE_ERROR; + + if (TimedOut()) { + ClearTimeout(); + newstate = OnTimeout(); + SignalTimeout(); + } else { + switch (state) { + case STATE_INIT: + newstate = STATE_START; + break; + case STATE_START: + newstate = ProcessStart(); + break; + case STATE_RESPONSE: + newstate = ProcessResponse(); + break; + case STATE_DONE: + case STATE_ERROR: + newstate = STATE_BLOCKED; + break; + } + } + + return newstate; +} + +void Task::Stop() { + // No need to wake because we're either awake or in abort + TaskParent::OnStopped(this); +} + +void Task::set_timeout_seconds(const int timeout_seconds) { + timeout_seconds_ = timeout_seconds; + ResetTimeout(); +} + +bool Task::TimedOut() { + return timeout_seconds_ && + timeout_time_ && + CurrentTime() >= timeout_time_; +} + +void Task::ResetTimeout() { + int64 previous_timeout_time = timeout_time_; + bool timeout_allowed = (state_ != STATE_INIT) + && (state_ != STATE_DONE) + && (state_ != STATE_ERROR); + if (timeout_seconds_ && timeout_allowed && !timeout_suspended_) + timeout_time_ = CurrentTime() + + (timeout_seconds_ * kSecToMsec * kMsecTo100ns); + else + timeout_time_ = 0; + + GetRunner()->UpdateTaskTimeout(this, previous_timeout_time); +} + +void Task::ClearTimeout() { + int64 previous_timeout_time = timeout_time_; + timeout_time_ = 0; + GetRunner()->UpdateTaskTimeout(this, previous_timeout_time); +} + +void Task::SuspendTimeout() { + if (!timeout_suspended_) { + timeout_suspended_ = true; + ResetTimeout(); + } +} + +void Task::ResumeTimeout() { + if (timeout_suspended_) { + timeout_suspended_ = false; + ResetTimeout(); + } +} + +} // namespace talk_base diff --git a/talk/base/task.h b/talk/base/task.h new file mode 100644 index 000000000..10e6f5c22 --- /dev/null +++ b/talk/base/task.h @@ -0,0 +1,194 @@ +/* + * 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. + */ + +#ifndef TALK_BASE_TASK_H__ +#define TALK_BASE_TASK_H__ + +#include +#include "talk/base/basictypes.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" +#include "talk/base/taskparent.h" + +///////////////////////////////////////////////////////////////////// +// +// TASK +// +///////////////////////////////////////////////////////////////////// +// +// Task is a state machine infrastructure. States are pushed forward by +// pushing forwards a TaskRunner that holds on to all Tasks. The purpose +// of Task is threefold: +// +// (1) It manages ongoing work on the UI thread. Multitasking without +// threads, keeping it easy, keeping it real. :-) It does this by +// organizing a set of states for each task. When you return from your +// Process*() function, you return an integer for the next state. You do +// not go onto the next state yourself. Every time you enter a state, +// you check to see if you can do anything yet. If not, you return +// STATE_BLOCKED. If you _could_ do anything, do not return +// STATE_BLOCKED - even if you end up in the same state, return +// STATE_mysamestate. When you are done, return STATE_DONE and then the +// task will self-delete sometime afterwards. +// +// (2) It helps you avoid all those reentrancy problems when you chain +// too many triggers on one thread. Basically if you want to tell a task +// to process something for you, you feed your task some information and +// then you Wake() it. Don't tell it to process it right away. If it +// might be working on something as you send it information, you may want +// to have a queue in the task. +// +// (3) Finally it helps manage parent tasks and children. If a parent +// task gets aborted, all the children tasks are too. The nice thing +// about this, for example, is if you have one parent task that +// represents, say, and Xmpp connection, then you can spawn a whole bunch +// of infinite lifetime child tasks and now worry about cleaning them up. +// When the parent task goes to STATE_DONE, the task engine will make +// sure all those children are aborted and get deleted. +// +// Notice that Task has a few built-in states, e.g., +// +// STATE_INIT - the task isn't running yet +// STATE_START - the task is in its first state +// STATE_RESPONSE - the task is in its second state +// STATE_DONE - the task is done +// +// STATE_ERROR - indicates an error - we should audit the error code in +// light of any usage of it to see if it should be improved. When I +// first put down the task stuff I didn't have a good sense of what was +// needed for Abort and Error, and now the subclasses of Task will ground +// the design in a stronger way. +// +// STATE_NEXT - the first undefined state number. (like WM_USER) - you +// can start defining more task states there. +// +// When you define more task states, just override Process(int state) and +// add your own switch statement. If you want to delegate to +// Task::Process, you can effectively delegate to its switch statement. +// No fancy method pointers or such - this is all just pretty low tech, +// easy to debug, and fast. +// +// Also notice that Task has some primitive built-in timeout functionality. +// +// A timeout is defined as "the task stays in STATE_BLOCKED longer than +// timeout_seconds_." +// +// Descendant classes can override this behavior by calling the +// various protected methods to change the timeout behavior. For +// instance, a descendand might call SuspendTimeout() when it knows +// that it isn't waiting for anything that might timeout, but isn't +// yet in the STATE_DONE state. +// + +namespace talk_base { + +// Executes a sequence of steps +class Task : public TaskParent { + public: + Task(TaskParent *parent); + virtual ~Task(); + + int32 unique_id() { return unique_id_; } + + void Start(); + void Step(); + int GetState() const { return state_; } + bool HasError() const { return (GetState() == STATE_ERROR); } + bool Blocked() const { return blocked_; } + bool IsDone() const { return done_; } + int64 ElapsedTime(); + + // Called from outside to stop task without any more callbacks + void Abort(bool nowake = false); + + bool TimedOut(); + + int64 timeout_time() const { return timeout_time_; } + int timeout_seconds() const { return timeout_seconds_; } + void set_timeout_seconds(int timeout_seconds); + + sigslot::signal0<> SignalTimeout; + + // Called inside the task to signal that the task may be unblocked + void Wake(); + + protected: + + enum { + STATE_BLOCKED = -1, + STATE_INIT = 0, + STATE_START = 1, + STATE_DONE = 2, + STATE_ERROR = 3, + STATE_RESPONSE = 4, + STATE_NEXT = 5, // Subclasses which need more states start here and higher + }; + + // Called inside to advise that the task should wake and signal an error + void Error(); + + int64 CurrentTime(); + + virtual std::string GetStateName(int state) const; + virtual int Process(int state); + virtual void Stop(); + virtual int ProcessStart() = 0; + virtual int ProcessResponse() { return STATE_DONE; } + + void ResetTimeout(); + void ClearTimeout(); + + void SuspendTimeout(); + void ResumeTimeout(); + + protected: + virtual int OnTimeout() { + // by default, we are finished after timing out + return STATE_DONE; + } + + private: + void Done(); + + int state_; + bool blocked_; + bool done_; + bool aborted_; + bool busy_; + bool error_; + int64 start_time_; + int64 timeout_time_; + int timeout_seconds_; + bool timeout_suspended_; + int32 unique_id_; + + static int32 unique_id_seed_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_TASK_H__ diff --git a/talk/base/task_unittest.cc b/talk/base/task_unittest.cc new file mode 100644 index 000000000..0c4a7a20e --- /dev/null +++ b/talk/base/task_unittest.cc @@ -0,0 +1,562 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifdef POSIX +#include +#endif // POSIX + +// TODO: Remove this once the cause of sporadic failures in these +// tests is tracked down. +#include + +#ifdef WIN32 +#include "talk/base/win32.h" +#endif // WIN32 + +#include "talk/base/common.h" +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/task.h" +#include "talk/base/taskrunner.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" + +namespace talk_base { + +static int64 GetCurrentTime() { + return static_cast(Time()) * 10000; +} + +// feel free to change these numbers. Note that '0' won't work, though +#define STUCK_TASK_COUNT 5 +#define HAPPY_TASK_COUNT 20 + +// this is a generic timeout task which, when it signals timeout, will +// include the unique ID of the task in the signal (we don't use this +// in production code because we haven't yet had occasion to generate +// an array of the same types of task) + +class IdTimeoutTask : public Task, public sigslot::has_slots<> { + public: + explicit IdTimeoutTask(TaskParent *parent) : Task(parent) { + SignalTimeout.connect(this, &IdTimeoutTask::OnLocalTimeout); + } + + sigslot::signal1 SignalTimeoutId; + sigslot::signal1 SignalDoneId; + + virtual int ProcessStart() { + return STATE_RESPONSE; + } + + void OnLocalTimeout() { + SignalTimeoutId(unique_id()); + } + + protected: + virtual void Stop() { + SignalDoneId(unique_id()); + Task::Stop(); + } +}; + +class StuckTask : public IdTimeoutTask { + public: + explicit StuckTask(TaskParent *parent) : IdTimeoutTask(parent) {} + virtual int ProcessStart() { + return STATE_BLOCKED; + } +}; + +class HappyTask : public IdTimeoutTask { + public: + explicit HappyTask(TaskParent *parent) : IdTimeoutTask(parent) { + time_to_perform_ = rand() % (STUCK_TASK_COUNT / 2); + } + virtual int ProcessStart() { + if (ElapsedTime() > (time_to_perform_ * 1000 * 10000)) + return STATE_RESPONSE; + else + return STATE_BLOCKED; + } + + private: + int time_to_perform_; +}; + +// simple implementation of a task runner which uses Windows' +// GetSystemTimeAsFileTime() to get the current clock ticks + +class MyTaskRunner : public TaskRunner { + public: + virtual void WakeTasks() { RunTasks(); } + virtual int64 CurrentTime() { + return GetCurrentTime(); + } + + bool timeout_change() const { + return timeout_change_; + } + + void clear_timeout_change() { + timeout_change_ = false; + } + protected: + virtual void OnTimeoutChange() { + timeout_change_ = true; + } + bool timeout_change_; +}; + +// +// this unit test is primarily concerned (for now) with the timeout +// functionality in tasks. It works as follows: +// +// * Create a bunch of tasks, some "stuck" (ie., guaranteed to timeout) +// and some "happy" (will immediately finish). +// * Set the timeout on the "stuck" tasks to some number of seconds between +// 1 and the number of stuck tasks +// * Start all the stuck & happy tasks in random order +// * Wait "number of stuck tasks" seconds and make sure everything timed out + +class TaskTest : public sigslot::has_slots<> { + public: + TaskTest() {} + + // no need to delete any tasks; the task runner owns them + ~TaskTest() {} + + void Start() { + // create and configure tasks + for (int i = 0; i < STUCK_TASK_COUNT; ++i) { + stuck_[i].task_ = new StuckTask(&task_runner_); + stuck_[i].task_->SignalTimeoutId.connect(this, + &TaskTest::OnTimeoutStuck); + stuck_[i].timed_out_ = false; + stuck_[i].xlat_ = stuck_[i].task_->unique_id(); + stuck_[i].task_->set_timeout_seconds(i + 1); + LOG(LS_INFO) << "Task " << stuck_[i].xlat_ << " created with timeout " + << stuck_[i].task_->timeout_seconds(); + } + + for (int i = 0; i < HAPPY_TASK_COUNT; ++i) { + happy_[i].task_ = new HappyTask(&task_runner_); + happy_[i].task_->SignalTimeoutId.connect(this, + &TaskTest::OnTimeoutHappy); + happy_[i].task_->SignalDoneId.connect(this, + &TaskTest::OnDoneHappy); + happy_[i].timed_out_ = false; + happy_[i].xlat_ = happy_[i].task_->unique_id(); + } + + // start all the tasks in random order + int stuck_index = 0; + int happy_index = 0; + for (int i = 0; i < STUCK_TASK_COUNT + HAPPY_TASK_COUNT; ++i) { + if ((stuck_index < STUCK_TASK_COUNT) && + (happy_index < HAPPY_TASK_COUNT)) { + if (rand() % 2 == 1) { + stuck_[stuck_index++].task_->Start(); + } else { + happy_[happy_index++].task_->Start(); + } + } else if (stuck_index < STUCK_TASK_COUNT) { + stuck_[stuck_index++].task_->Start(); + } else { + happy_[happy_index++].task_->Start(); + } + } + + for (int i = 0; i < STUCK_TASK_COUNT; ++i) { + std::cout << "Stuck task #" << i << " timeout is " << + stuck_[i].task_->timeout_seconds() << " at " << + stuck_[i].task_->timeout_time() << std::endl; + } + + // just a little self-check to make sure we started all the tasks + ASSERT_EQ(STUCK_TASK_COUNT, stuck_index); + ASSERT_EQ(HAPPY_TASK_COUNT, happy_index); + + // run the unblocked tasks + LOG(LS_INFO) << "Running tasks"; + task_runner_.RunTasks(); + + std::cout << "Start time is " << GetCurrentTime() << std::endl; + + // give all the stuck tasks time to timeout + for (int i = 0; !task_runner_.AllChildrenDone() && i < STUCK_TASK_COUNT; + ++i) { + Thread::Current()->ProcessMessages(1000); + for (int j = 0; j < HAPPY_TASK_COUNT; ++j) { + if (happy_[j].task_) { + happy_[j].task_->Wake(); + } + } + LOG(LS_INFO) << "Polling tasks"; + task_runner_.PollTasks(); + } + + // We see occasional test failures here due to the stuck tasks not having + // timed-out yet, which seems like it should be impossible. To help track + // this down we have added logging of the timing information, which we send + // directly to stdout so that we get it in opt builds too. + std::cout << "End time is " << GetCurrentTime() << std::endl; + } + + void OnTimeoutStuck(const int id) { + LOG(LS_INFO) << "Timed out task " << id; + + int i; + for (i = 0; i < STUCK_TASK_COUNT; ++i) { + if (stuck_[i].xlat_ == id) { + stuck_[i].timed_out_ = true; + stuck_[i].task_ = NULL; + break; + } + } + + // getting a bad ID here is a failure, but let's continue + // running to see what else might go wrong + EXPECT_LT(i, STUCK_TASK_COUNT); + } + + void OnTimeoutHappy(const int id) { + int i; + for (i = 0; i < HAPPY_TASK_COUNT; ++i) { + if (happy_[i].xlat_ == id) { + happy_[i].timed_out_ = true; + happy_[i].task_ = NULL; + break; + } + } + + // getting a bad ID here is a failure, but let's continue + // running to see what else might go wrong + EXPECT_LT(i, HAPPY_TASK_COUNT); + } + + void OnDoneHappy(const int id) { + int i; + for (i = 0; i < HAPPY_TASK_COUNT; ++i) { + if (happy_[i].xlat_ == id) { + happy_[i].task_ = NULL; + break; + } + } + + // getting a bad ID here is a failure, but let's continue + // running to see what else might go wrong + EXPECT_LT(i, HAPPY_TASK_COUNT); + } + + void check_passed() { + EXPECT_TRUE(task_runner_.AllChildrenDone()); + + // make sure none of our happy tasks timed out + for (int i = 0; i < HAPPY_TASK_COUNT; ++i) { + EXPECT_FALSE(happy_[i].timed_out_); + } + + // make sure all of our stuck tasks timed out + for (int i = 0; i < STUCK_TASK_COUNT; ++i) { + EXPECT_TRUE(stuck_[i].timed_out_); + if (!stuck_[i].timed_out_) { + std::cout << "Stuck task #" << i << " timeout is at " + << stuck_[i].task_->timeout_time() << std::endl; + } + } + + std::cout.flush(); + } + + private: + struct TaskInfo { + IdTimeoutTask *task_; + bool timed_out_; + int xlat_; + }; + + MyTaskRunner task_runner_; + TaskInfo stuck_[STUCK_TASK_COUNT]; + TaskInfo happy_[HAPPY_TASK_COUNT]; +}; + +TEST(start_task_test, Timeout) { + TaskTest task_test; + task_test.Start(); + task_test.check_passed(); +} + +// Test for aborting the task while it is running + +class AbortTask : public Task { + public: + explicit AbortTask(TaskParent *parent) : Task(parent) { + set_timeout_seconds(1); + } + + virtual int ProcessStart() { + Abort(); + return STATE_NEXT; + } + private: + DISALLOW_EVIL_CONSTRUCTORS(AbortTask); +}; + +class TaskAbortTest : public sigslot::has_slots<> { + public: + TaskAbortTest() {} + + // no need to delete any tasks; the task runner owns them + ~TaskAbortTest() {} + + void Start() { + Task *abort_task = new AbortTask(&task_runner_); + abort_task->SignalTimeout.connect(this, &TaskAbortTest::OnTimeout); + abort_task->Start(); + + // run the task + task_runner_.RunTasks(); + } + + private: + void OnTimeout() { + FAIL() << "Task timed out instead of aborting."; + } + + MyTaskRunner task_runner_; + DISALLOW_EVIL_CONSTRUCTORS(TaskAbortTest); +}; + +TEST(start_task_test, Abort) { + TaskAbortTest abort_test; + abort_test.Start(); +} + +// Test for aborting a task to verify that it does the Wake operation +// which gets it deleted. + +class SetBoolOnDeleteTask : public Task { + public: + SetBoolOnDeleteTask(TaskParent *parent, bool *set_when_deleted) + : Task(parent), + set_when_deleted_(set_when_deleted) { + EXPECT_TRUE(NULL != set_when_deleted); + EXPECT_FALSE(*set_when_deleted); + } + + virtual ~SetBoolOnDeleteTask() { + *set_when_deleted_ = true; + } + + virtual int ProcessStart() { + return STATE_BLOCKED; + } + + private: + bool* set_when_deleted_; + DISALLOW_EVIL_CONSTRUCTORS(SetBoolOnDeleteTask); +}; + +class AbortShouldWakeTest : public sigslot::has_slots<> { + public: + AbortShouldWakeTest() {} + + // no need to delete any tasks; the task runner owns them + ~AbortShouldWakeTest() {} + + void Start() { + bool task_deleted = false; + Task *task_to_abort = new SetBoolOnDeleteTask(&task_runner_, &task_deleted); + task_to_abort->Start(); + + // Task::Abort() should call TaskRunner::WakeTasks(). WakeTasks calls + // TaskRunner::RunTasks() immediately which should delete the task. + task_to_abort->Abort(); + EXPECT_TRUE(task_deleted); + + if (!task_deleted) { + // avoid a crash (due to referencing a local variable) + // if the test fails. + task_runner_.RunTasks(); + } + } + + private: + void OnTimeout() { + FAIL() << "Task timed out instead of aborting."; + } + + MyTaskRunner task_runner_; + DISALLOW_EVIL_CONSTRUCTORS(AbortShouldWakeTest); +}; + +TEST(start_task_test, AbortShouldWake) { + AbortShouldWakeTest abort_should_wake_test; + abort_should_wake_test.Start(); +} + +// Validate that TaskRunner's OnTimeoutChange gets called appropriately +// * When a task calls UpdateTaskTimeout +// * When the next timeout task time, times out +class TimeoutChangeTest : public sigslot::has_slots<> { + public: + TimeoutChangeTest() + : task_count_(ARRAY_SIZE(stuck_tasks_)) {} + + // no need to delete any tasks; the task runner owns them + ~TimeoutChangeTest() {} + + void Start() { + for (int i = 0; i < task_count_; ++i) { + stuck_tasks_[i] = new StuckTask(&task_runner_); + stuck_tasks_[i]->set_timeout_seconds(i + 2); + stuck_tasks_[i]->SignalTimeoutId.connect(this, + &TimeoutChangeTest::OnTimeoutId); + } + + for (int i = task_count_ - 1; i >= 0; --i) { + stuck_tasks_[i]->Start(); + } + task_runner_.clear_timeout_change(); + + // At this point, our timeouts are set as follows + // task[0] is 2 seconds, task[1] at 3 seconds, etc. + + stuck_tasks_[0]->set_timeout_seconds(2); + // Now, task[0] is 2 seconds, task[1] at 3 seconds... + // so timeout change shouldn't be called. + EXPECT_FALSE(task_runner_.timeout_change()); + task_runner_.clear_timeout_change(); + + stuck_tasks_[0]->set_timeout_seconds(1); + // task[0] is 1 seconds, task[1] at 3 seconds... + // The smallest timeout got smaller so timeout change be called. + EXPECT_TRUE(task_runner_.timeout_change()); + task_runner_.clear_timeout_change(); + + stuck_tasks_[1]->set_timeout_seconds(2); + // task[0] is 1 seconds, task[1] at 2 seconds... + // The smallest timeout is still 1 second so no timeout change. + EXPECT_FALSE(task_runner_.timeout_change()); + task_runner_.clear_timeout_change(); + + while (task_count_ > 0) { + int previous_count = task_count_; + task_runner_.PollTasks(); + if (previous_count != task_count_) { + // We only get here when a task times out. When that + // happens, the timeout change should get called because + // the smallest timeout is now in the past. + EXPECT_TRUE(task_runner_.timeout_change()); + task_runner_.clear_timeout_change(); + } + Thread::Current()->socketserver()->Wait(500, false); + } + } + + private: + void OnTimeoutId(const int id) { + for (int i = 0; i < ARRAY_SIZE(stuck_tasks_); ++i) { + if (stuck_tasks_[i] && stuck_tasks_[i]->unique_id() == id) { + task_count_--; + stuck_tasks_[i] = NULL; + break; + } + } + } + + MyTaskRunner task_runner_; + StuckTask* (stuck_tasks_[3]); + int task_count_; + DISALLOW_EVIL_CONSTRUCTORS(TimeoutChangeTest); +}; + +TEST(start_task_test, TimeoutChange) { + TimeoutChangeTest timeout_change_test; + timeout_change_test.Start(); +} + +class DeleteTestTaskRunner : public TaskRunner { + public: + DeleteTestTaskRunner() { + } + virtual void WakeTasks() { } + virtual int64 CurrentTime() { + return GetCurrentTime(); + } + private: + DISALLOW_EVIL_CONSTRUCTORS(DeleteTestTaskRunner); +}; + +TEST(unstarted_task_test, DeleteTask) { + // This test ensures that we don't + // crash if a task is deleted without running it. + DeleteTestTaskRunner task_runner; + HappyTask* happy_task = new HappyTask(&task_runner); + happy_task->Start(); + + // try deleting the task directly + HappyTask* child_happy_task = new HappyTask(happy_task); + delete child_happy_task; + + // run the unblocked tasks + task_runner.RunTasks(); +} + +TEST(unstarted_task_test, DoNotDeleteTask1) { + // This test ensures that we don't + // crash if a task runner is deleted without + // running a certain task. + DeleteTestTaskRunner task_runner; + HappyTask* happy_task = new HappyTask(&task_runner); + happy_task->Start(); + + HappyTask* child_happy_task = new HappyTask(happy_task); + child_happy_task->Start(); + + // Never run the tasks +} + +TEST(unstarted_task_test, DoNotDeleteTask2) { + // This test ensures that we don't + // crash if a taskrunner is delete with a + // task that has never been started. + DeleteTestTaskRunner task_runner; + HappyTask* happy_task = new HappyTask(&task_runner); + happy_task->Start(); + + // Do not start the task. + // Note: this leaks memory, so don't do this. + // Instead, always run your tasks or delete them. + new HappyTask(happy_task); + + // run the unblocked tasks + task_runner.RunTasks(); +} + +} // namespace talk_base diff --git a/talk/base/taskparent.cc b/talk/base/taskparent.cc new file mode 100644 index 000000000..f05ee82a3 --- /dev/null +++ b/talk/base/taskparent.cc @@ -0,0 +1,112 @@ +/* + * 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 + +#include "talk/base/taskparent.h" + +#include "talk/base/task.h" +#include "talk/base/taskrunner.h" + +namespace talk_base { + +TaskParent::TaskParent(Task* derived_instance, TaskParent *parent) + : parent_(parent) { + ASSERT(derived_instance != NULL); + ASSERT(parent != NULL); + runner_ = parent->GetRunner(); + parent_->AddChild(derived_instance); + Initialize(); +} + +TaskParent::TaskParent(TaskRunner *derived_instance) + : parent_(NULL), + runner_(derived_instance) { + ASSERT(derived_instance != NULL); + Initialize(); +} + +// Does common initialization of member variables +void TaskParent::Initialize() { + children_.reset(new ChildSet()); + child_error_ = false; +} + +void TaskParent::AddChild(Task *child) { + children_->insert(child); +} + +#ifdef _DEBUG +bool TaskParent::IsChildTask(Task *task) { + ASSERT(task != NULL); + return task->parent_ == this && children_->find(task) != children_->end(); +} +#endif + +bool TaskParent::AllChildrenDone() { + for (ChildSet::iterator it = children_->begin(); + it != children_->end(); + ++it) { + if (!(*it)->IsDone()) + return false; + } + return true; +} + +bool TaskParent::AnyChildError() { + return child_error_; +} + +void TaskParent::AbortAllChildren() { + if (children_->size() > 0) { +#ifdef _DEBUG + runner_->IncrementAbortCount(); +#endif + + ChildSet copy = *children_; + for (ChildSet::iterator it = copy.begin(); it != copy.end(); ++it) { + (*it)->Abort(true); // Note we do not wake + } + +#ifdef _DEBUG + runner_->DecrementAbortCount(); +#endif + } +} + +void TaskParent::OnStopped(Task *task) { + AbortAllChildren(); + parent_->OnChildStopped(task); +} + +void TaskParent::OnChildStopped(Task *child) { + if (child->HasError()) + child_error_ = true; + children_->erase(child); +} + +} // namespace talk_base diff --git a/talk/base/taskparent.h b/talk/base/taskparent.h new file mode 100644 index 000000000..e2093d6e7 --- /dev/null +++ b/talk/base/taskparent.h @@ -0,0 +1,79 @@ +/* + * 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. + */ + +#ifndef TALK_BASE_TASKPARENT_H__ +#define TALK_BASE_TASKPARENT_H__ + +#include + +#include "talk/base/basictypes.h" +#include "talk/base/scoped_ptr.h" + +namespace talk_base { + +class Task; +class TaskRunner; + +class TaskParent { + public: + TaskParent(Task *derived_instance, TaskParent *parent); + explicit TaskParent(TaskRunner *derived_instance); + virtual ~TaskParent() { } + + TaskParent *GetParent() { return parent_; } + TaskRunner *GetRunner() { return runner_; } + + bool AllChildrenDone(); + bool AnyChildError(); +#ifdef _DEBUG + bool IsChildTask(Task *task); +#endif + + protected: + void OnStopped(Task *task); + void AbortAllChildren(); + TaskParent *parent() { + return parent_; + } + + private: + void Initialize(); + void OnChildStopped(Task *child); + void AddChild(Task *child); + + TaskParent *parent_; + TaskRunner *runner_; + bool child_error_; + typedef std::set ChildSet; + scoped_ptr children_; + DISALLOW_EVIL_CONSTRUCTORS(TaskParent); +}; + + +} // namespace talk_base + +#endif // TALK_BASE_TASKPARENT_H__ diff --git a/talk/base/taskrunner.cc b/talk/base/taskrunner.cc new file mode 100644 index 000000000..0c0816c5e --- /dev/null +++ b/talk/base/taskrunner.cc @@ -0,0 +1,241 @@ +/* + * 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 + +#include "talk/base/taskrunner.h" + +#include "talk/base/common.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/task.h" +#include "talk/base/logging.h" + +namespace talk_base { + +TaskRunner::TaskRunner() + : TaskParent(this), + next_timeout_task_(NULL), + tasks_running_(false) +#ifdef _DEBUG + , abort_count_(0), + deleting_task_(NULL) +#endif +{ +} + +TaskRunner::~TaskRunner() { + // this kills and deletes children silently! + AbortAllChildren(); + InternalRunTasks(true); +} + +void TaskRunner::StartTask(Task * task) { + tasks_.push_back(task); + + // the task we just started could be about to timeout -- + // make sure our "next timeout task" is correct + UpdateTaskTimeout(task, 0); + + WakeTasks(); +} + +void TaskRunner::RunTasks() { + InternalRunTasks(false); +} + +void TaskRunner::InternalRunTasks(bool in_destructor) { + // This shouldn't run while an abort is happening. + // If that occurs, then tasks may be deleted in this method, + // but pointers to them will still be in the + // "ChildSet copy" in TaskParent::AbortAllChildren. + // Subsequent use of those task may cause data corruption or crashes. + ASSERT(!abort_count_); + // Running continues until all tasks are Blocked (ok for a small # of tasks) + if (tasks_running_) { + return; // don't reenter + } + + tasks_running_ = true; + + int64 previous_timeout_time = next_task_timeout(); + + int did_run = true; + while (did_run) { + did_run = false; + // use indexing instead of iterators because tasks_ may grow + for (size_t i = 0; i < tasks_.size(); ++i) { + while (!tasks_[i]->Blocked()) { + tasks_[i]->Step(); + did_run = true; + } + } + } + // Tasks are deleted when running has paused + bool need_timeout_recalc = false; + for (size_t i = 0; i < tasks_.size(); ++i) { + if (tasks_[i]->IsDone()) { + Task* task = tasks_[i]; + if (next_timeout_task_ && + task->unique_id() == next_timeout_task_->unique_id()) { + next_timeout_task_ = NULL; + need_timeout_recalc = true; + } + +#ifdef _DEBUG + deleting_task_ = task; +#endif + delete task; +#ifdef _DEBUG + deleting_task_ = NULL; +#endif + tasks_[i] = NULL; + } + } + // Finally, remove nulls + std::vector::iterator it; + it = std::remove(tasks_.begin(), + tasks_.end(), + reinterpret_cast(NULL)); + + tasks_.erase(it, tasks_.end()); + + if (need_timeout_recalc) + RecalcNextTimeout(NULL); + + // Make sure that adjustments are done to account + // for any timeout changes (but don't call this + // while being destroyed since it calls a pure virtual function). + if (!in_destructor) + CheckForTimeoutChange(previous_timeout_time); + + tasks_running_ = false; +} + +void TaskRunner::PollTasks() { + // see if our "next potentially timed-out task" has indeed timed out. + // If it has, wake it up, then queue up the next task in line + // Repeat while we have new timed-out tasks. + // TODO: We need to guard against WakeTasks not updating + // next_timeout_task_. Maybe also add documentation in the header file once + // we understand this code better. + Task* old_timeout_task = NULL; + while (next_timeout_task_ && + old_timeout_task != next_timeout_task_ && + next_timeout_task_->TimedOut()) { + old_timeout_task = next_timeout_task_; + next_timeout_task_->Wake(); + WakeTasks(); + } +} + +int64 TaskRunner::next_task_timeout() const { + if (next_timeout_task_) { + return next_timeout_task_->timeout_time(); + } + return 0; +} + +// this function gets called frequently -- when each task changes +// state to something other than DONE, ERROR or BLOCKED, it calls +// ResetTimeout(), which will call this function to make sure that +// the next timeout-able task hasn't changed. The logic in this function +// prevents RecalcNextTimeout() from getting called in most cases, +// effectively making the task scheduler O-1 instead of O-N + +void TaskRunner::UpdateTaskTimeout(Task* task, + int64 previous_task_timeout_time) { + ASSERT(task != NULL); + int64 previous_timeout_time = next_task_timeout(); + bool task_is_timeout_task = next_timeout_task_ != NULL && + task->unique_id() == next_timeout_task_->unique_id(); + if (task_is_timeout_task) { + previous_timeout_time = previous_task_timeout_time; + } + + // if the relevant task has a timeout, then + // check to see if it's closer than the current + // "about to timeout" task + if (task->timeout_time()) { + if (next_timeout_task_ == NULL || + (task->timeout_time() <= next_timeout_task_->timeout_time())) { + next_timeout_task_ = task; + } + } else if (task_is_timeout_task) { + // otherwise, if the task doesn't have a timeout, + // and it used to be our "about to timeout" task, + // walk through all the tasks looking for the real + // "about to timeout" task + RecalcNextTimeout(task); + } + + // Note when task_running_, then the running routine + // (TaskRunner::InternalRunTasks) is responsible for calling + // CheckForTimeoutChange. + if (!tasks_running_) { + CheckForTimeoutChange(previous_timeout_time); + } +} + +void TaskRunner::RecalcNextTimeout(Task *exclude_task) { + // walk through all the tasks looking for the one + // which satisfies the following: + // it's not finished already + // we're not excluding it + // it has the closest timeout time + + int64 next_timeout_time = 0; + next_timeout_task_ = NULL; + + for (size_t i = 0; i < tasks_.size(); ++i) { + Task *task = tasks_[i]; + // if the task isn't complete, and it actually has a timeout time + if (!task->IsDone() && (task->timeout_time() > 0)) + // if it doesn't match our "exclude" task + if (exclude_task == NULL || + exclude_task->unique_id() != task->unique_id()) + // if its timeout time is sooner than our current timeout time + if (next_timeout_time == 0 || + task->timeout_time() <= next_timeout_time) { + // set this task as our next-to-timeout + next_timeout_time = task->timeout_time(); + next_timeout_task_ = task; + } + } +} + +void TaskRunner::CheckForTimeoutChange(int64 previous_timeout_time) { + int64 next_timeout = next_task_timeout(); + bool timeout_change = (previous_timeout_time == 0 && next_timeout != 0) || + next_timeout < previous_timeout_time || + (previous_timeout_time <= CurrentTime() && + previous_timeout_time != next_timeout); + if (timeout_change) { + OnTimeoutChange(); + } +} + +} // namespace talk_base diff --git a/talk/base/taskrunner.h b/talk/base/taskrunner.h new file mode 100644 index 000000000..f34a60926 --- /dev/null +++ b/talk/base/taskrunner.h @@ -0,0 +1,117 @@ +/* + * 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. + */ + +#ifndef TALK_BASE_TASKRUNNER_H__ +#define TALK_BASE_TASKRUNNER_H__ + +#include + +#include "talk/base/basictypes.h" +#include "talk/base/sigslot.h" +#include "talk/base/taskparent.h" + +namespace talk_base { +class Task; + +const int64 kSecToMsec = 1000; +const int64 kMsecTo100ns = 10000; +const int64 kSecTo100ns = kSecToMsec * kMsecTo100ns; + +class TaskRunner : public TaskParent, public sigslot::has_slots<> { + public: + TaskRunner(); + virtual ~TaskRunner(); + + virtual void WakeTasks() = 0; + + // Returns the current time in 100ns units. It is used for + // determining timeouts. The origin is not important, only + // the units and that rollover while the computer is running. + // + // On Windows, GetSystemTimeAsFileTime is the typical implementation. + virtual int64 CurrentTime() = 0 ; + + void StartTask(Task *task); + void RunTasks(); + void PollTasks(); + + void UpdateTaskTimeout(Task *task, int64 previous_task_timeout_time); + +#ifdef _DEBUG + bool is_ok_to_delete(Task* task) { + return task == deleting_task_; + } + + void IncrementAbortCount() { + ++abort_count_; + } + + void DecrementAbortCount() { + --abort_count_; + } +#endif + + // Returns the next absolute time when a task times out + // OR "0" if there is no next timeout. + int64 next_task_timeout() const; + + protected: + // The primary usage of this method is to know if + // a callback timer needs to be set-up or adjusted. + // This method will be called + // * when the next_task_timeout() becomes a smaller value OR + // * when next_task_timeout() has changed values and the previous + // value is in the past. + // + // If the next_task_timeout moves to the future, this method will *not* + // get called (because it subclass should check next_task_timeout() + // when its timer goes off up to see if it needs to set-up a new timer). + // + // Note that this maybe called conservatively. In that it may be + // called when no time change has happened. + virtual void OnTimeoutChange() { + // by default, do nothing. + } + + private: + void InternalRunTasks(bool in_destructor); + void CheckForTimeoutChange(int64 previous_timeout_time); + + std::vector tasks_; + Task *next_timeout_task_; + bool tasks_running_; +#ifdef _DEBUG + int abort_count_; + Task* deleting_task_; +#endif + + void RecalcNextTimeout(Task *exclude_task); +}; + +} // namespace talk_base + +#endif // TASK_BASE_TASKRUNNER_H__ diff --git a/talk/base/testbase64.h b/talk/base/testbase64.h new file mode 100644 index 000000000..39dd00ce3 --- /dev/null +++ b/talk/base/testbase64.h @@ -0,0 +1,5 @@ +/* This file was generated by googleclient/talk/binary2header.sh */ + +static unsigned char testbase64[] = { +0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x02, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0xff, 0xe1, 0x0d, 0x07, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0c, 0x01, 0x0e, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x9e, 0x01, 0x0f, 0x00, 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0xbe, 0x01, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0xc3, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x1a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xcc, 0x01, 0x1b, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xd4, 0x01, 0x28, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x01, 0x31, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0xdc, 0x01, 0x32, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0xf0, 0x01, 0x3c, 0x00, 0x02, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x01, 0x04, 0x02, 0x13, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x87, 0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x02, 0xc4, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x53, 0x4f, 0x4e, 0x59, 0x00, 0x44, 0x53, 0x43, 0x2d, 0x50, 0x32, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x20, 0x37, 0x2e, 0x30, 0x00, 0x32, 0x30, 0x30, 0x37, 0x3a, 0x30, 0x31, 0x3a, 0x33, 0x30, 0x20, 0x32, 0x33, 0x3a, 0x31, 0x30, 0x3a, 0x30, 0x34, 0x00, 0x4d, 0x61, 0x63, 0x20, 0x4f, 0x53, 0x20, 0x58, 0x20, 0x31, 0x30, 0x2e, 0x34, 0x2e, 0x38, 0x00, 0x00, 0x1c, 0x82, 0x9a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x6a, 0x82, 0x9d, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x72, 0x88, 0x22, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x88, 0x27, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x64, 0x00, 0x00, 0x90, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, 0x32, 0x32, 0x30, 0x90, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x02, 0x7a, 0x90, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x02, 0x8e, 0x91, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x01, 0x02, 0x03, 0x00, 0x91, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0xa2, 0x92, 0x04, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0xaa, 0x92, 0x05, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0xb2, 0x92, 0x07, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x05, 0x00, 0x00, 0x92, 0x08, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x92, 0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0f, 0x00, 0x00, 0x92, 0x0a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0xba, 0xa0, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, 0x31, 0x30, 0x30, 0xa0, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0x00, 0x00, 0xa0, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0xa0, 0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0xa3, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0xa3, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0xa4, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x06, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x08, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x0a, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x01, 0x90, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x0a, 0x32, 0x30, 0x30, 0x37, 0x3a, 0x30, 0x31, 0x3a, 0x32, 0x30, 0x20, 0x32, 0x33, 0x3a, 0x30, 0x35, 0x3a, 0x35, 0x32, 0x00, 0x32, 0x30, 0x30, 0x37, 0x3a, 0x30, 0x31, 0x3a, 0x32, 0x30, 0x20, 0x32, 0x33, 0x3a, 0x30, 0x35, 0x3a, 0x35, 0x32, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x01, 0x1a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x12, 0x01, 0x1b, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x1a, 0x01, 0x28, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x02, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x22, 0x02, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x09, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x02, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0xff, 0xed, 0x00, 0x0c, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x5f, 0x43, 0x4d, 0x00, 0x02, 0xff, 0xee, 0x00, 0x0e, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x00, 0x64, 0x80, 0x00, 0x00, 0x00, 0x01, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x0c, 0x08, 0x08, 0x08, 0x09, 0x08, 0x0c, 0x09, 0x09, 0x0c, 0x11, 0x0b, 0x0a, 0x0b, 0x11, 0x15, 0x0f, 0x0c, 0x0c, 0x0f, 0x15, 0x18, 0x13, 0x13, 0x15, 0x13, 0x13, 0x18, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x01, 0x0d, 0x0b, 0x0b, 0x0d, 0x0e, 0x0d, 0x10, 0x0e, 0x0e, 0x10, 0x14, 0x0e, 0x0e, 0x0e, 0x14, 0x14, 0x0e, 0x0e, 0x0e, 0x0e, 0x14, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0x64, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xdd, 0x00, 0x04, 0x00, 0x07, 0xff, 0xc4, 0x01, 0x3f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x02, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x01, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x01, 0x04, 0x01, 0x03, 0x02, 0x04, 0x02, 0x05, 0x07, 0x06, 0x08, 0x05, 0x03, 0x0c, 0x33, 0x01, 0x00, 0x02, 0x11, 0x03, 0x04, 0x21, 0x12, 0x31, 0x05, 0x41, 0x51, 0x61, 0x13, 0x22, 0x71, 0x81, 0x32, 0x06, 0x14, 0x91, 0xa1, 0xb1, 0x42, 0x23, 0x24, 0x15, 0x52, 0xc1, 0x62, 0x33, 0x34, 0x72, 0x82, 0xd1, 0x43, 0x07, 0x25, 0x92, 0x53, 0xf0, 0xe1, 0xf1, 0x63, 0x73, 0x35, 0x16, 0xa2, 0xb2, 0x83, 0x26, 0x44, 0x93, 0x54, 0x64, 0x45, 0xc2, 0xa3, 0x74, 0x36, 0x17, 0xd2, 0x55, 0xe2, 0x65, 0xf2, 0xb3, 0x84, 0xc3, 0xd3, 0x75, 0xe3, 0xf3, 0x46, 0x27, 0x94, 0xa4, 0x85, 0xb4, 0x95, 0xc4, 0xd4, 0xe4, 0xf4, 0xa5, 0xb5, 0xc5, 0xd5, 0xe5, 0xf5, 0x56, 0x66, 0x76, 0x86, 0x96, 0xa6, 0xb6, 0xc6, 0xd6, 0xe6, 0xf6, 0x37, 0x47, 0x57, 0x67, 0x77, 0x87, 0x97, 0xa7, 0xb7, 0xc7, 0xd7, 0xe7, 0xf7, 0x11, 0x00, 0x02, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x05, 0x06, 0x07, 0x07, 0x06, 0x05, 0x35, 0x01, 0x00, 0x02, 0x11, 0x03, 0x21, 0x31, 0x12, 0x04, 0x41, 0x51, 0x61, 0x71, 0x22, 0x13, 0x05, 0x32, 0x81, 0x91, 0x14, 0xa1, 0xb1, 0x42, 0x23, 0xc1, 0x52, 0xd1, 0xf0, 0x33, 0x24, 0x62, 0xe1, 0x72, 0x82, 0x92, 0x43, 0x53, 0x15, 0x63, 0x73, 0x34, 0xf1, 0x25, 0x06, 0x16, 0xa2, 0xb2, 0x83, 0x07, 0x26, 0x35, 0xc2, 0xd2, 0x44, 0x93, 0x54, 0xa3, 0x17, 0x64, 0x45, 0x55, 0x36, 0x74, 0x65, 0xe2, 0xf2, 0xb3, 0x84, 0xc3, 0xd3, 0x75, 0xe3, 0xf3, 0x46, 0x94, 0xa4, 0x85, 0xb4, 0x95, 0xc4, 0xd4, 0xe4, 0xf4, 0xa5, 0xb5, 0xc5, 0xd5, 0xe5, 0xf5, 0x56, 0x66, 0x76, 0x86, 0x96, 0xa6, 0xb6, 0xc6, 0xd6, 0xe6, 0xf6, 0x27, 0x37, 0x47, 0x57, 0x67, 0x77, 0x87, 0x97, 0xa7, 0xb7, 0xc7, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf2, 0xed, 0xb2, 0x8d, 0x4d, 0x45, 0xcd, 0x2f, 0x3f, 0x44, 0x68, 0x93, 0xc3, 0x58, 0xc8, 0xf1, 0x1f, 0x8a, 0x33, 0x86, 0xda, 0x58, 0xc1, 0xa0, 0x02, 0x4f, 0xc4, 0xa1, 0x69, 0xa5, 0x9b, 0x5b, 0x4b, 0x84, 0x73, 0xdf, 0xc9, 0x15, 0xf8, 0xe3, 0xd1, 0x0e, 0x07, 0x93, 0xf3, 0xd1, 0x0f, 0x1c, 0x17, 0xef, 0x2e, 0x3b, 0x5b, 0xdc, 0xff, 0x00, 0xdf, 0x42, 0xbf, 0x8f, 0x8e, 0xdc, 0x82, 0xca, 0xd8, 0x37, 0x11, 0xa9, 0x3d, 0x82, 0x69, 0x2b, 0xc4, 0x6d, 0xc9, 0x75, 0x25, 0xbc, 0xf7, 0xec, 0xa1, 0xb5, 0x74, 0x19, 0x5d, 0x2e, 0x8a, 0x9a, 0x4b, 0x89, 0x7d, 0xc4, 0x68, 0xc6, 0xf6, 0xfe, 0xb2, 0xa0, 0x30, 0x1d, 0x60, 0x86, 0x88, 0x8d, 0x49, 0x3e, 0x01, 0x11, 0x20, 0xa3, 0x8c, 0xb9, 0xb1, 0xaa, 0x62, 0xad, 0xbf, 0x18, 0x97, 0x43, 0x47, 0x1d, 0xd2, 0xaf, 0x04, 0xd9, 0xb8, 0xc8, 0x0d, 0x68, 0xe4, 0xf7, 0x3e, 0x48, 0xf1, 0x05, 0xbc, 0x25, 0xaa, 0x07, 0x71, 0xd9, 0x14, 0x78, 0xf6, 0x49, 0xb5, 0x90, 0xfd, 0xa7, 0xc6, 0x14, 0xfd, 0x1b, 0x1c, 0xff, 0x00, 0x4d, 0x8d, 0x2e, 0x73, 0x8c, 0x35, 0xa3, 0x52, 0x4f, 0x92, 0x48, 0xa6, 0x1a, 0x24, 0xb6, 0x2a, 0xfa, 0xa5, 0x9e, 0x60, 0x64, 0x39, 0x94, 0x13, 0xcb, 0x27, 0x73, 0x80, 0xf3, 0x0c, 0xf6, 0xff, 0x00, 0xd2, 0x5a, 0x78, 0xbf, 0x53, 0x71, 0xf6, 0x01, 0x75, 0xb6, 0x97, 0x6a, 0x25, 0xa1, 0xad, 0x1f, 0xf4, 0xb7, 0x23, 0x48, 0xb7, 0x94, 0x84, 0x97, 0x5b, 0xff, 0x00, 0x32, 0xa9, 0xdd, 0xfc, 0xed, 0x9b, 0x7e, 0x0d, 0x9e, 0x52, 0x4a, 0x95, 0x61, 0xff, 0xd0, 0xf3, 0x3b, 0xa7, 0x70, 0xee, 0x01, 0x8f, 0xb9, 0x59, 0xfa, 0x7e, 0xdf, 0xe4, 0xc8, 0xf9, 0x2a, 0xc2, 0x5c, 0x63, 0xc3, 0x54, 0x67, 0x87, 0x6e, 0x10, 0x35, 0x68, 0xd4, 0x79, 0x1e, 0x53, 0x4a, 0xe0, 0xdc, 0xe9, 0xb8, 0x1f, 0x6a, 0xda, 0x6c, 0x25, 0x94, 0x37, 0xb0, 0xd0, 0xb8, 0xad, 0x67, 0xe4, 0x55, 0x8a, 0x5b, 0x8b, 0x82, 0xc0, 0x6f, 0x76, 0x80, 0x34, 0x49, 0x05, 0x2e, 0x9e, 0xc6, 0x1c, 0x66, 0x31, 0xba, 0x10, 0x23, 0xe0, 0xaf, 0xe1, 0x61, 0x53, 0x43, 0x8d, 0x81, 0xb3, 0x67, 0xef, 0x9e, 0x49, 0x2a, 0x12, 0x6c, 0xb6, 0x63, 0x1a, 0x0c, 0x31, 0xba, 0x55, 0xcd, 0xac, 0xfa, 0x8e, 0xdf, 0x91, 0x6e, 0x91, 0xd9, 0xb3, 0xc9, 0x73, 0x90, 0x7a, 0xab, 0x6a, 0xc2, 0xa4, 0x60, 0xe2, 0x8f, 0xd2, 0x38, 0x03, 0x7d, 0x9e, 0x0d, 0xff, 0x00, 0xcc, 0xd6, 0xd3, 0x6b, 0x71, 0x67, 0xd2, 0x3e, 0x64, 0x72, 0xab, 0xdb, 0x8d, 0x54, 0x39, 0xc5, 0x83, 0x6b, 0x3d, 0xee, 0x2e, 0xd4, 0x92, 0x3c, 0x4a, 0x56, 0xba, 0xb4, 0x79, 0x5c, 0xf7, 0xb2, 0x96, 0x6c, 0x8d, 0xaf, 0x80, 0x48, 0x3c, 0xf0, 0xb2, 0x1f, 0x63, 0x9c, 0xe9, 0x3f, 0x24, 0x5c, 0xdb, 0xdd, 0x76, 0x43, 0xde, 0xfd, 0x5c, 0xe3, 0x24, 0xfc, 0x50, 0x00, 0x93, 0x0a, 0x78, 0x8a, 0x0d, 0x49, 0xca, 0xcf, 0x93, 0x63, 0x1b, 0x7d, 0xd7, 0x57, 0x50, 0xd5, 0xef, 0x70, 0x6b, 0x4f, 0xc7, 0x45, 0xdb, 0x74, 0x9e, 0x8d, 0x5e, 0x33, 0x83, 0xd8, 0x37, 0xdd, 0xc3, 0xac, 0x3d, 0xbf, 0x92, 0xc5, 0x5b, 0xea, 0xbf, 0xd5, 0x62, 0xc0, 0xdc, 0xbc, 0xbd, 0x2d, 0x22, 0x5a, 0xcf, 0xdd, 0x69, 0xff, 0x00, 0xd1, 0x8e, 0x5d, 0xa5, 0x38, 0xb5, 0xb0, 0x00, 0xc6, 0xc4, 0x24, 0x4a, 0xd6, 0x8d, 0x18, 0x04, 0x49, 0x88, 0x9e, 0x55, 0xd6, 0x61, 0xb0, 0xc1, 0x70, 0x32, 0xdd, 0x3c, 0x95, 0xda, 0xf1, 0xfe, 0xf5, 0x62, 0xbc, 0x76, 0x8e, 0x75, 0x28, 0x02, 0xa2, 0xe7, 0x7d, 0x92, 0xb9, 0x84, 0x96, 0x96, 0xda, 0xf7, 0x70, 0x12, 0x4e, 0x5a, 0xff, 0x00, 0xff, 0xd1, 0xf3, 0x7a, 0x21, 0xaf, 0xde, 0xef, 0xa2, 0x22, 0x55, 0xfc, 0x5a, 0xbd, 0x42, 0xfb, 0x08, 0xfa, 0x67, 0x4f, 0x82, 0xcd, 0x6d, 0x85, 0xc0, 0x56, 0x3b, 0x90, 0xb7, 0xf0, 0x2a, 0x0e, 0x63, 0x58, 0x3b, 0xf2, 0xa3, 0x9e, 0x8c, 0xb8, 0x86, 0xbe, 0x49, 0xf1, 0x2c, 0x0c, 0x86, 0xb4, 0x4c, 0x69, 0xe4, 0xaf, 0x6e, 0xcc, 0x6b, 0x7d, 0x46, 0xb3, 0x70, 0xec, 0x38, 0x51, 0x7d, 0x02, 0x8a, 0xc7, 0xa6, 0xd9, 0x20, 0x68, 0x0f, 0x8f, 0x8a, 0xcf, 0xc9, 0xc2, 0xea, 0x59, 0x5b, 0x48, 0xb0, 0x91, 0xae, 0xe6, 0xc9, 0x03, 0xc9, 0x30, 0x51, 0x66, 0xd4, 0x0d, 0xad, 0xbd, 0x5f, 0x53, 0xcc, 0x6b, 0xb6, 0x90, 0x5a, 0x3b, 0x83, 0x0b, 0x43, 0x17, 0x31, 0xd6, 0xc3, 0x6e, 0x12, 0x3b, 0x79, 0xac, 0xc1, 0x89, 0x47, 0xd9, 0xe8, 0x63, 0x98, 0x45, 0xed, 0x6c, 0x5a, 0xf1, 0xa0, 0x27, 0xc5, 0x5b, 0xc3, 0x6f, 0xa6, 0xe0, 0x1c, 0x7d, 0xb3, 0xa2, 0x69, 0x34, 0x7b, 0xae, 0x1a, 0x8d, 0x45, 0x17, 0x9d, 0xeb, 0xfd, 0x21, 0xd8, 0xb9, 0xae, 0xb5, 0x80, 0xbb, 0x1e, 0xd2, 0x5c, 0xd7, 0x78, 0x13, 0xf9, 0xae, 0x4b, 0xea, 0xc7, 0x4a, 0x39, 0xbd, 0x55, 0xb3, 0xed, 0x66, 0x38, 0xf5, 0x09, 0x22, 0x41, 0x23, 0xe8, 0x37, 0xfb, 0x4b, 0xa1, 0xeb, 0xd6, 0xfe, 0x88, 0x31, 0xbf, 0x41, 0xc0, 0xee, 0xd2, 0x74, 0x02, 0x78, 0x53, 0xfa, 0x97, 0x43, 0x19, 0x85, 0x65, 0xff, 0x00, 0x9d, 0x71, 0x33, 0xe4, 0x1a, 0x7d, 0x8d, 0x53, 0x42, 0x56, 0x35, 0x6b, 0xe5, 0x80, 0x06, 0xc7, 0x57, 0xa7, 0xc4, 0xa9, 0xdb, 0xb6, 0x81, 0x1f, 0xeb, 0xd9, 0x69, 0x56, 0xc2, 0xd0, 0x00, 0xe5, 0x55, 0xc0, 0x12, 0xc2, 0xd7, 0x4e, 0xa2, 0x5a, 0x7c, 0x0a, 0xd0, 0x63, 0x9a, 0xd1, 0xaf, 0xd2, 0xe2, 0x3c, 0x12, 0x62, 0x66, 0xc6, 0x42, 0x23, 0x5a, 0x49, 0x8f, 0x10, 0xa2, 0xd2, 0x3e, 0x28, 0x9d, 0xc4, 0x88, 0x09, 0x29, 0x16, 0xc3, 0x3c, 0x24, 0x8d, 0xe6, 0x92, 0x72, 0x1f, 0xff, 0xd2, 0xf3, 0xbb, 0xb0, 0xfe, 0xcb, 0x99, 0xe9, 0xce, 0xf6, 0x88, 0x2d, 0x77, 0x91, 0x5b, 0x3d, 0x3d, 0xd0, 0xe6, 0x90, 0xa9, 0x65, 0x57, 0x38, 0x95, 0xdd, 0xcb, 0x9a, 0x7d, 0xce, 0xf2, 0x3f, 0x44, 0x23, 0x60, 0x58, 0x76, 0xe9, 0xca, 0x8c, 0xea, 0x1b, 0x31, 0x02, 0x32, 0x23, 0xea, 0xee, 0xb1, 0xcd, 0xb0, 0xc7, 0x87, 0x74, 0x7a, 0xeb, 0x70, 0x1a, 0x71, 0xe1, 0xfe, 0xe4, 0x1c, 0x1d, 0xae, 0xe5, 0x69, 0xd8, 0xfa, 0x99, 0x50, 0x0d, 0x1a, 0xf7, 0x2a, 0x3a, 0x0c, 0xf4, 0x1a, 0x8e, 0xc7, 0x27, 0x5d, 0xbf, 0x18, 0x41, 0xdc, 0xc2, 0xf0, 0x7f, 0x74, 0xf6, 0x3a, 0x22, 0x66, 0xdb, 0x68, 0xc6, 0x80, 0x48, 0x6b, 0x88, 0x06, 0x39, 0x0d, 0xee, 0xaa, 0x1f, 0xb3, 0xd5, 0x1b, 0x83, 0xd8, 0x3b, 0x38, 0x8f, 0x69, 0xfe, 0xdf, 0xd1, 0x4d, 0x29, 0xa1, 0x4c, 0x7a, 0xf4, 0xbf, 0xa7, 0x92, 0xcf, 0xa5, 0x20, 0x08, 0xf3, 0xf6, 0xff, 0x00, 0x15, 0xbb, 0xd1, 0x31, 0xd9, 0x5e, 0x3d, 0x75, 0x56, 0x36, 0x88, 0x00, 0x81, 0xe0, 0x16, 0x5e, 0x55, 0x74, 0x3f, 0x00, 0x9d, 0xe0, 0xcc, 0x69, 0xe7, 0x3a, 0x2d, 0xbe, 0x90, 0x00, 0xa9, 0xae, 0xef, 0x1f, 0x95, 0x4b, 0x0d, 0x9a, 0xdc, 0xc7, 0x45, 0xfe, 0xb1, 0x7d, 0x60, 0xa7, 0xa1, 0xe0, 0x1f, 0x4e, 0x1d, 0x99, 0x69, 0x02, 0x9a, 0xcf, 0x1f, 0xca, 0x7b, 0xbf, 0x90, 0xc5, 0xc2, 0xb3, 0xeb, 0x57, 0xd6, 0x03, 0x6b, 0xae, 0x39, 0xb6, 0x82, 0xe3, 0x31, 0xa1, 0x68, 0xf2, 0x6b, 0x5c, 0x12, 0xfa, 0xe1, 0x91, 0x66, 0x47, 0x5d, 0xb8, 0x3b, 0x4f, 0x44, 0x36, 0xb6, 0x8f, 0x28, 0xdd, 0xff, 0x00, 0x7e, 0x46, 0xab, 0x12, 0x2b, 0x65, 0x55, 0x32, 0xa7, 0x62, 0xb6, 0xbd, 0xf7, 0x64, 0x10, 0xdb, 0x03, 0x9f, 0x1b, 0x9e, 0xc7, 0xd9, 0xb8, 0x3b, 0x1f, 0x67, 0xf3, 0x6c, 0x52, 0x80, 0xd7, 0x7d, 0x0f, 0xea, 0x7f, 0x5d, 0x1d, 0x67, 0xa6, 0x0b, 0x1e, 0x47, 0xda, 0x69, 0x3b, 0x2e, 0x03, 0xc7, 0xf3, 0x5f, 0x1f, 0xf0, 0x8b, 0xa1, 0x02, 0x46, 0xba, 0x79, 0xaf, 0x32, 0xff, 0x00, 0x16, 0xad, 0xca, 0x1d, 0x57, 0x2a, 0xdc, 0x79, 0x18, 0x41, 0xb0, 0xf6, 0x9e, 0xe4, 0x9f, 0xd0, 0x8f, 0xeb, 0x31, 0xab, 0xd2, 0x83, 0xa4, 0xcb, 0x8c, 0xb8, 0xa0, 0x42, 0x12, 0x7b, 0x67, 0x9f, 0x2f, 0xf5, 0x09, 0x26, 0x96, 0xc4, 0xce, 0xa9, 0x20, 0xa7, 0xff, 0xd3, 0xf3, 0x2f, 0xb4, 0x5d, 0xe9, 0x0a, 0xb7, 0x9f, 0x4c, 0x19, 0xdb, 0x3a, 0x2d, 0x5e, 0x94, 0xfd, 0xc4, 0xb7, 0xc5, 0x62, 0xf9, 0x2b, 0xfd, 0x2e, 0xe3, 0x5d, 0xe0, 0x7c, 0x13, 0x48, 0xd1, 0x92, 0x12, 0xa9, 0x0b, 0x7a, 0xbc, 0x2d, 0xc2, 0x7f, 0x92, 0x60, 0xab, 0x4e, 0x79, 0x2e, 0x00, 0xf0, 0xaa, 0xe1, 0xda, 0x3d, 0x43, 0xfc, 0xad, 0x55, 0xbb, 0x80, 0x79, 0x81, 0xa0, 0xe6, 0x54, 0x32, 0x6d, 0x02, 0xbe, 0xf3, 0x61, 0x81, 0xa8, 0x44, 0x14, 0x03, 0x59, 0x0e, 0x1c, 0xf6, 0x1f, 0xdc, 0xb2, 0xec, 0xa3, 0x23, 0x77, 0xe8, 0x6e, 0x70, 0xf2, 0x25, 0x1f, 0x1f, 0x17, 0xa9, 0x6d, 0x71, 0x36, 0x97, 0x47, 0x00, 0xa4, 0x02, 0xe0, 0x2c, 0x7c, 0xc1, 0xab, 0xd5, 0x31, 0x85, 0x35, 0xd4, 0xe6, 0x13, 0x02, 0xd6, 0x4b, 0x67, 0x48, 0x2b, 0xa9, 0xe9, 0x2e, 0x02, 0xb6, 0x4f, 0x82, 0xe5, 0x7a, 0x95, 0x19, 0xc6, 0x87, 0x3d, 0xfb, 0xa2, 0xb8, 0x79, 0x1e, 0x4d, 0x3b, 0x96, 0xcf, 0x4f, 0xbd, 0xcd, 0xa2, 0xa2, 0x1f, 0xa0, 0x82, 0xd3, 0xfc, 0x97, 0x05, 0x24, 0x36, 0x6b, 0xf3, 0x31, 0xa2, 0x35, 0x79, 0xef, 0xad, 0xf8, 0xae, 0xaf, 0xaf, 0xd8, 0xf2, 0xd8, 0x6d, 0xed, 0x6b, 0xda, 0x7b, 0x18, 0x1b, 0x5d, 0xff, 0x00, 0x52, 0xb1, 0x6d, 0xf0, 0x81, 0x31, 0xca, 0xf4, 0x6e, 0xb1, 0x80, 0xce, 0xb1, 0x84, 0xc0, 0x21, 0xb7, 0xd6, 0x77, 0x31, 0xd1, 0x27, 0xc1, 0xcd, 0xfe, 0xd2, 0xe3, 0xec, 0xe8, 0x1d, 0x45, 0x96, 0xb0, 0x9a, 0xb7, 0x87, 0x3f, 0x68, 0x2d, 0xf7, 0x01, 0x1f, 0xbe, 0xd1, 0xf4, 0x7f, 0xb4, 0xa4, 0x0d, 0x77, 0xbb, 0xfa, 0x8f, 0x80, 0x3a, 0x7f, 0x43, 0xaa, 0xe2, 0xdf, 0xd2, 0x65, 0x7e, 0x95, 0xe4, 0x0f, 0x1f, 0xa1, 0xfe, 0x6b, 0x16, 0x9f, 0x52, 0xfa, 0xc1, 0xd3, 0xba, 0x6d, 0x26, 0xdc, 0xac, 0x86, 0xd4, 0xd9, 0x0d, 0x31, 0x2e, 0x74, 0x9e, 0xdb, 0x59, 0x2e, 0x55, 0xe8, 0xc9, 0xb2, 0x96, 0xd5, 0x4b, 0x9f, 0xb8, 0x6d, 0xda, 0x1c, 0x04, 0x09, 0x03, 0xfe, 0x8a, 0xc6, 0xfa, 0xd3, 0xf5, 0x6a, 0xbe, 0xbb, 0x5b, 0x2e, 0xc6, 0xb5, 0x94, 0xe6, 0xd5, 0x20, 0x97, 0x7d, 0x1b, 0x1b, 0xf9, 0xad, 0x7c, 0x7d, 0x17, 0xb7, 0xf3, 0x1e, 0x92, 0x1b, 0x7f, 0xf8, 0xe0, 0x7d, 0x59, 0xdd, 0xfd, 0x32, 0xd8, 0x8f, 0xa5, 0xe8, 0x3a, 0x12, 0x5c, 0x3f, 0xfc, 0xc4, 0xfa, 0xc3, 0xb3, 0x77, 0xa7, 0x56, 0xed, 0xdb, 0x76, 0x7a, 0x8d, 0xdd, 0x1f, 0xbf, 0xfd, 0x44, 0x92, 0x56, 0x8f, 0xff, 0xd4, 0xf2, 0xe8, 0x86, 0x17, 0x1e, 0xfa, 0x04, 0x56, 0x4b, 0x43, 0x6c, 0x6f, 0x2d, 0xe5, 0x46, 0x01, 0x64, 0x2b, 0x14, 0x32, 0x5b, 0xb4, 0xa0, 0x52, 0x1d, 0xde, 0x9b, 0x94, 0xdb, 0xab, 0x6b, 0x81, 0xf7, 0x05, 0xb0, 0xd7, 0x07, 0xb2, 0x27, 0x55, 0xc6, 0x57, 0x65, 0xd8, 0x76, 0x6e, 0x64, 0xed, 0xee, 0x16, 0xce, 0x27, 0x57, 0x63, 0xda, 0x0c, 0xc2, 0x8e, 0x51, 0x67, 0x84, 0xfa, 0x1d, 0xdd, 0x62, 0xc7, 0x07, 0xe9, 0xf7, 0xa3, 0xd6, 0x6c, 0x02, 0x41, 0x55, 0x31, 0xf3, 0x2b, 0xb3, 0xba, 0x2b, 0x2e, 0x68, 0x24, 0x1d, 0x47, 0x64, 0xca, 0xa6, 0x50, 0x41, 0x65, 0x90, 0x6c, 0xb1, 0xa5, 0xae, 0x33, 0x23, 0x51, 0xe4, 0xab, 0x7d, 0x5d, 0xcb, 0xb6, 0xcc, 0x37, 0xd0, 0x40, 0x73, 0x71, 0xde, 0x58, 0x09, 0xe7, 0x6f, 0x2c, 0x44, 0xc9, 0xc9, 0xae, 0xba, 0x9d, 0x63, 0x88, 0x01, 0xa0, 0x95, 0x9d, 0xf5, 0x3f, 0x2a, 0xe6, 0x67, 0xdb, 0x50, 0x83, 0x55, 0xad, 0x36, 0x3e, 0x78, 0x10, 0x74, 0x77, 0xfd, 0x2d, 0xaa, 0x4c, 0x7d, 0x58, 0x73, 0x91, 0xa0, 0x0f, 0x51, 0x45, 0xb7, 0x33, 0xdd, 0x58, 0x69, 0x1d, 0xd8, 0x0c, 0x9f, 0x96, 0x88, 0x19, 0x99, 0x19, 0xac, 0xcf, 0xa3, 0xd2, 0xad, 0xb5, 0xdb, 0x76, 0x8f, 0xad, 0xc4, 0xea, 0xcf, 0xdf, 0x7e, 0xdf, 0xdd, 0xfc, 0xd5, 0xa3, 0x5e, 0x43, 0x2b, 0x6b, 0xb2, 0xad, 0x3b, 0x6a, 0xa4, 0x13, 0xa7, 0x04, 0xac, 0x7a, 0x6f, 0xb3, 0x23, 0x26, 0xcc, 0xfb, 0xb4, 0x75, 0x8e, 0x01, 0x83, 0xf7, 0x58, 0x3e, 0x8b, 0x53, 0xa7, 0x2a, 0x1a, 0x31, 0x42, 0x36, 0x5d, 0x4c, 0x9a, 0xf2, 0xdc, 0xc6, 0xfe, 0x98, 0xb4, 0x34, 0xcb, 0x48, 0x0a, 0x8f, 0xdb, 0xb2, 0xeb, 0x76, 0xd6, 0x07, 0x5c, 0x59, 0xc9, 0x64, 0x8f, 0x93, 0xa7, 0x73, 0x16, 0x83, 0xaf, 0x0e, 0xa4, 0x33, 0xef, 0x50, 0xc5, 0x0c, 0xda, 0x59, 0x10, 0x06, 0x8a, 0x2e, 0x29, 0x0e, 0xac, 0xc2, 0x31, 0x3d, 0x36, 0x69, 0x7e, 0xd6, 0xcc, 0xf5, 0x3d, 0x6f, 0xb3, 0xeb, 0x1b, 0x76, 0xef, 0x3b, 0xa3, 0xfa, 0xc9, 0x2b, 0x5f, 0x66, 0x6f, 0xa9, 0x1e, 0x73, 0xf2, 0x49, 0x2e, 0x39, 0xf7, 0x4f, 0xb7, 0x8d, 0xff, 0xd5, 0xf3, 0x26, 0xfe, 0x0a, 0xc5, 0x1b, 0xa7, 0xcb, 0xb2, 0xcf, 0x49, 0x03, 0xb2, 0x46, 0xee, 0xd9, 0xd9, 0xb3, 0xf4, 0x9f, 0x25, 0x4a, 0xdf, 0x4b, 0x77, 0xe8, 0x27, 0xd4, 0xef, 0x1c, 0x2a, 0x29, 0x26, 0xc5, 0x7c, 0x9d, 0x6c, 0x7f, 0xb7, 0x6e, 0x1b, 0x26, 0x7f, 0x05, 0xa3, 0xfe, 0x53, 0x8d, 0x62, 0x57, 0x30, 0x92, 0x12, 0xfa, 0x2f, 0x86, 0xdf, 0xa4, 0xec, 0x67, 0xfe, 0xd0, 0xf4, 0xff, 0x00, 0x4d, 0xfc, 0xdf, 0x78, 0xe1, 0x68, 0x7d, 0x54, 0x99, 0xbf, 0x6f, 0xf3, 0xbe, 0xdf, 0x8e, 0xdd, 0x7f, 0xef, 0xeb, 0x97, 0x49, 0x3e, 0x3b, 0x7f, 0x06, 0x2c, 0x9f, 0x37, 0x5f, 0xf0, 0x9f, 0x4c, 0xeb, 0x7b, 0xbf, 0x67, 0x55, 0xe8, 0xff, 0x00, 0x31, 0xbc, 0x7a, 0x9e, 0x31, 0xdb, 0xfe, 0x92, 0xae, 0x37, 0x7a, 0x4d, 0xdb, 0xe2, 0x17, 0x9d, 0xa4, 0xa3, 0xc9, 0xba, 0xfc, 0x7b, 0x7d, 0x5f, 0x52, 0xa7, 0x7e, 0xd1, 0x28, 0xf8, 0xf3, 0xb0, 0xc7, 0x32, 0xbc, 0x99, 0x24, 0xc5, 0xe3, 0xab, 0xeb, 0x1f, 0xa4, 0xf5, 0xfc, 0xe1, 0x25, 0xe4, 0xe9, 0x24, 0x97, 0xff, 0xd9, 0xff, 0xed, 0x2e, 0x1c, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x20, 0x33, 0x2e, 0x30, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x1c, 0x02, 0x00, 0x00, 0x02, 0x00, 0x02, 0x1c, 0x02, 0x78, 0x00, 0x1f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xfb, 0x09, 0xa6, 0xbd, 0x07, 0x4c, 0x2a, 0x36, 0x9d, 0x8f, 0xe2, 0xcc, 0x57, 0xa9, 0xac, 0x85, 0x38, 0x42, 0x49, 0x4d, 0x03, 0xea, 0x00, 0x00, 0x00, 0x00, 0x1d, 0xb0, 0x3c, 0x3f, 0x78, 0x6d, 0x6c, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3d, 0x22, 0x31, 0x2e, 0x30, 0x22, 0x20, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x3d, 0x22, 0x55, 0x54, 0x46, 0x2d, 0x38, 0x22, 0x3f, 0x3e, 0x0a, 0x3c, 0x21, 0x44, 0x4f, 0x43, 0x54, 0x59, 0x50, 0x45, 0x20, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x20, 0x22, 0x2d, 0x2f, 0x2f, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x2f, 0x44, 0x54, 0x44, 0x20, 0x50, 0x4c, 0x49, 0x53, 0x54, 0x20, 0x31, 0x2e, 0x30, 0x2f, 0x2f, 0x45, 0x4e, 0x22, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x44, 0x54, 0x44, 0x73, 0x2f, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x2d, 0x31, 0x2e, 0x30, 0x2e, 0x64, 0x74, 0x64, 0x22, 0x3e, 0x0a, 0x3c, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3d, 0x22, 0x31, 0x2e, 0x30, 0x22, 0x3e, 0x0a, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x48, 0x6f, 0x72, 0x69, 0x7a, 0x6f, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x48, 0x6f, 0x72, 0x69, 0x7a, 0x6f, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x37, 0x32, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x31, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x53, 0x63, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x53, 0x63, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x31, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x56, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x56, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x37, 0x32, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x56, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x56, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x31, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x73, 0x75, 0x62, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x70, 0x61, 0x70, 0x65, 0x72, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x5f, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x30, 0x2e, 0x30, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x30, 0x2e, 0x30, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x37, 0x33, 0x34, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x35, 0x37, 0x36, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x70, 0x65, 0x72, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x70, 0x65, 0x72, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x2d, 0x31, 0x38, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x2d, 0x31, 0x38, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x37, 0x37, 0x34, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x35, 0x39, 0x34, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x50, 0x4d, 0x50, 0x61, 0x70, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x6d, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x50, 0x4d, 0x50, 0x61, 0x70, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x6e, 0x61, 0x2d, 0x6c, 0x65, 0x74, 0x74, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x6d, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x33, 0x2d, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x54, 0x31, 0x37, 0x3a, 0x34, 0x39, 0x3a, 0x33, 0x36, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x31, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x50, 0x4d, 0x55, 0x6e, 0x61, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x6d, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x50, 0x4d, 0x55, 0x6e, 0x61, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x30, 0x2e, 0x30, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x30, 0x2e, 0x30, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x37, 0x33, 0x34, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x35, 0x37, 0x36, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x50, 0x4d, 0x55, 0x6e, 0x61, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x70, 0x65, 0x72, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x6d, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x50, 0x4d, 0x55, 0x6e, 0x61, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x70, 0x65, 0x72, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x2d, 0x31, 0x38, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x2d, 0x31, 0x38, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x37, 0x37, 0x34, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x35, 0x39, 0x34, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x70, 0x70, 0x64, 0x2e, 0x50, 0x4d, 0x50, 0x61, 0x70, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x6d, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x70, 0x70, 0x64, 0x2e, 0x50, 0x4d, 0x50, 0x61, 0x70, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x55, 0x53, 0x20, 0x4c, 0x65, 0x74, 0x74, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x6d, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x33, 0x2d, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x54, 0x31, 0x37, 0x3a, 0x34, 0x39, 0x3a, 0x33, 0x36, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x31, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x41, 0x50, 0x49, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x30, 0x30, 0x2e, 0x32, 0x30, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x63, 0x6b, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2f, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x41, 0x50, 0x49, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x30, 0x30, 0x2e, 0x32, 0x30, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x63, 0x6b, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2f, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x3c, 0x2f, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x3e, 0x0a, 0x38, 0x42, 0x49, 0x4d, 0x03, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x03, 0x00, 0x00, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x02, 0xde, 0x02, 0x40, 0xff, 0xee, 0xff, 0xee, 0x03, 0x06, 0x02, 0x52, 0x03, 0x67, 0x05, 0x28, 0x03, 0xfc, 0x00, 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x02, 0xd8, 0x02, 0x28, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x03, 0x03, 0x00, 0x00, 0x00, 0x01, 0x7f, 0xff, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x08, 0x00, 0x19, 0x01, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x03, 0xed, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x80, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1e, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1e, 0x38, 0x42, 0x49, 0x4d, 0x03, 0xf3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x27, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x38, 0x42, 0x49, 0x4d, 0x03, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x2f, 0x66, 0x66, 0x00, 0x01, 0x00, 0x6c, 0x66, 0x66, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x2f, 0x66, 0x66, 0x00, 0x01, 0x00, 0xa1, 0x99, 0x9a, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x32, 0x00, 0x00, 0x00, 0x01, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x35, 0x00, 0x00, 0x00, 0x01, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x38, 0x42, 0x49, 0x4d, 0x03, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x03, 0x45, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x08, 0x00, 0x44, 0x00, 0x53, 0x00, 0x43, 0x00, 0x30, 0x00, 0x32, 0x00, 0x33, 0x00, 0x32, 0x00, 0x35, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6e, 0x75, 0x6c, 0x6c, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x4f, 0x62, 0x6a, 0x63, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x63, 0x74, 0x31, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x54, 0x6f, 0x70, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x65, 0x66, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x74, 0x6f, 0x6d, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x52, 0x67, 0x68, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x06, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x73, 0x56, 0x6c, 0x4c, 0x73, 0x00, 0x00, 0x00, 0x01, 0x4f, 0x62, 0x6a, 0x63, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x07, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x49, 0x44, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x44, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x65, 0x6e, 0x75, 0x6d, 0x00, 0x00, 0x00, 0x0c, 0x45, 0x53, 0x6c, 0x69, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x0d, 0x61, 0x75, 0x74, 0x6f, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x00, 0x00, 0x00, 0x00, 0x54, 0x79, 0x70, 0x65, 0x65, 0x6e, 0x75, 0x6d, 0x00, 0x00, 0x00, 0x0a, 0x45, 0x53, 0x6c, 0x69, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x00, 0x00, 0x00, 0x00, 0x49, 0x6d, 0x67, 0x20, 0x00, 0x00, 0x00, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x4f, 0x62, 0x6a, 0x63, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x63, 0x74, 0x31, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x54, 0x6f, 0x70, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x65, 0x66, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x74, 0x6f, 0x6d, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x52, 0x67, 0x68, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x03, 0x75, 0x72, 0x6c, 0x54, 0x45, 0x58, 0x54, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6e, 0x75, 0x6c, 0x6c, 0x54, 0x45, 0x58, 0x54, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x73, 0x67, 0x65, 0x54, 0x45, 0x58, 0x54, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x61, 0x6c, 0x74, 0x54, 0x61, 0x67, 0x54, 0x45, 0x58, 0x54, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x63, 0x65, 0x6c, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x49, 0x73, 0x48, 0x54, 0x4d, 0x4c, 0x62, 0x6f, 0x6f, 0x6c, 0x01, 0x00, 0x00, 0x00, 0x08, 0x63, 0x65, 0x6c, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x54, 0x45, 0x58, 0x54, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x68, 0x6f, 0x72, 0x7a, 0x41, 0x6c, 0x69, 0x67, 0x6e, 0x65, 0x6e, 0x75, 0x6d, 0x00, 0x00, 0x00, 0x0f, 0x45, 0x53, 0x6c, 0x69, 0x63, 0x65, 0x48, 0x6f, 0x72, 0x7a, 0x41, 0x6c, 0x69, 0x67, 0x6e, 0x00, 0x00, 0x00, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x00, 0x00, 0x00, 0x09, 0x76, 0x65, 0x72, 0x74, 0x41, 0x6c, 0x69, 0x67, 0x6e, 0x65, 0x6e, 0x75, 0x6d, 0x00, 0x00, 0x00, 0x0f, 0x45, 0x53, 0x6c, 0x69, 0x63, 0x65, 0x56, 0x65, 0x72, 0x74, 0x41, 0x6c, 0x69, 0x67, 0x6e, 0x00, 0x00, 0x00, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x00, 0x00, 0x00, 0x0b, 0x62, 0x67, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x65, 0x6e, 0x75, 0x6d, 0x00, 0x00, 0x00, 0x11, 0x45, 0x53, 0x6c, 0x69, 0x63, 0x65, 0x42, 0x47, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x00, 0x00, 0x00, 0x00, 0x4e, 0x6f, 0x6e, 0x65, 0x00, 0x00, 0x00, 0x09, 0x74, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x73, 0x65, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x6c, 0x65, 0x66, 0x74, 0x4f, 0x75, 0x74, 0x73, 0x65, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x62, 0x6f, 0x74, 0x74, 0x6f, 0x6d, 0x4f, 0x75, 0x74, 0x73, 0x65, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x72, 0x69, 0x67, 0x68, 0x74, 0x4f, 0x75, 0x74, 0x73, 0x65, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x09, 0xf9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x01, 0x2c, 0x00, 0x00, 0x75, 0x30, 0x00, 0x00, 0x09, 0xdd, 0x00, 0x18, 0x00, 0x01, 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x02, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0xff, 0xed, 0x00, 0x0c, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x5f, 0x43, 0x4d, 0x00, 0x02, 0xff, 0xee, 0x00, 0x0e, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x00, 0x64, 0x80, 0x00, 0x00, 0x00, 0x01, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x0c, 0x08, 0x08, 0x08, 0x09, 0x08, 0x0c, 0x09, 0x09, 0x0c, 0x11, 0x0b, 0x0a, 0x0b, 0x11, 0x15, 0x0f, 0x0c, 0x0c, 0x0f, 0x15, 0x18, 0x13, 0x13, 0x15, 0x13, 0x13, 0x18, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x01, 0x0d, 0x0b, 0x0b, 0x0d, 0x0e, 0x0d, 0x10, 0x0e, 0x0e, 0x10, 0x14, 0x0e, 0x0e, 0x0e, 0x14, 0x14, 0x0e, 0x0e, 0x0e, 0x0e, 0x14, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0x64, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xdd, 0x00, 0x04, 0x00, 0x07, 0xff, 0xc4, 0x01, 0x3f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x02, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x01, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x01, 0x04, 0x01, 0x03, 0x02, 0x04, 0x02, 0x05, 0x07, 0x06, 0x08, 0x05, 0x03, 0x0c, 0x33, 0x01, 0x00, 0x02, 0x11, 0x03, 0x04, 0x21, 0x12, 0x31, 0x05, 0x41, 0x51, 0x61, 0x13, 0x22, 0x71, 0x81, 0x32, 0x06, 0x14, 0x91, 0xa1, 0xb1, 0x42, 0x23, 0x24, 0x15, 0x52, 0xc1, 0x62, 0x33, 0x34, 0x72, 0x82, 0xd1, 0x43, 0x07, 0x25, 0x92, 0x53, 0xf0, 0xe1, 0xf1, 0x63, 0x73, 0x35, 0x16, 0xa2, 0xb2, 0x83, 0x26, 0x44, 0x93, 0x54, 0x64, 0x45, 0xc2, 0xa3, 0x74, 0x36, 0x17, 0xd2, 0x55, 0xe2, 0x65, 0xf2, 0xb3, 0x84, 0xc3, 0xd3, 0x75, 0xe3, 0xf3, 0x46, 0x27, 0x94, 0xa4, 0x85, 0xb4, 0x95, 0xc4, 0xd4, 0xe4, 0xf4, 0xa5, 0xb5, 0xc5, 0xd5, 0xe5, 0xf5, 0x56, 0x66, 0x76, 0x86, 0x96, 0xa6, 0xb6, 0xc6, 0xd6, 0xe6, 0xf6, 0x37, 0x47, 0x57, 0x67, 0x77, 0x87, 0x97, 0xa7, 0xb7, 0xc7, 0xd7, 0xe7, 0xf7, 0x11, 0x00, 0x02, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x05, 0x06, 0x07, 0x07, 0x06, 0x05, 0x35, 0x01, 0x00, 0x02, 0x11, 0x03, 0x21, 0x31, 0x12, 0x04, 0x41, 0x51, 0x61, 0x71, 0x22, 0x13, 0x05, 0x32, 0x81, 0x91, 0x14, 0xa1, 0xb1, 0x42, 0x23, 0xc1, 0x52, 0xd1, 0xf0, 0x33, 0x24, 0x62, 0xe1, 0x72, 0x82, 0x92, 0x43, 0x53, 0x15, 0x63, 0x73, 0x34, 0xf1, 0x25, 0x06, 0x16, 0xa2, 0xb2, 0x83, 0x07, 0x26, 0x35, 0xc2, 0xd2, 0x44, 0x93, 0x54, 0xa3, 0x17, 0x64, 0x45, 0x55, 0x36, 0x74, 0x65, 0xe2, 0xf2, 0xb3, 0x84, 0xc3, 0xd3, 0x75, 0xe3, 0xf3, 0x46, 0x94, 0xa4, 0x85, 0xb4, 0x95, 0xc4, 0xd4, 0xe4, 0xf4, 0xa5, 0xb5, 0xc5, 0xd5, 0xe5, 0xf5, 0x56, 0x66, 0x76, 0x86, 0x96, 0xa6, 0xb6, 0xc6, 0xd6, 0xe6, 0xf6, 0x27, 0x37, 0x47, 0x57, 0x67, 0x77, 0x87, 0x97, 0xa7, 0xb7, 0xc7, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf2, 0xed, 0xb2, 0x8d, 0x4d, 0x45, 0xcd, 0x2f, 0x3f, 0x44, 0x68, 0x93, 0xc3, 0x58, 0xc8, 0xf1, 0x1f, 0x8a, 0x33, 0x86, 0xda, 0x58, 0xc1, 0xa0, 0x02, 0x4f, 0xc4, 0xa1, 0x69, 0xa5, 0x9b, 0x5b, 0x4b, 0x84, 0x73, 0xdf, 0xc9, 0x15, 0xf8, 0xe3, 0xd1, 0x0e, 0x07, 0x93, 0xf3, 0xd1, 0x0f, 0x1c, 0x17, 0xef, 0x2e, 0x3b, 0x5b, 0xdc, 0xff, 0x00, 0xdf, 0x42, 0xbf, 0x8f, 0x8e, 0xdc, 0x82, 0xca, 0xd8, 0x37, 0x11, 0xa9, 0x3d, 0x82, 0x69, 0x2b, 0xc4, 0x6d, 0xc9, 0x75, 0x25, 0xbc, 0xf7, 0xec, 0xa1, 0xb5, 0x74, 0x19, 0x5d, 0x2e, 0x8a, 0x9a, 0x4b, 0x89, 0x7d, 0xc4, 0x68, 0xc6, 0xf6, 0xfe, 0xb2, 0xa0, 0x30, 0x1d, 0x60, 0x86, 0x88, 0x8d, 0x49, 0x3e, 0x01, 0x11, 0x20, 0xa3, 0x8c, 0xb9, 0xb1, 0xaa, 0x62, 0xad, 0xbf, 0x18, 0x97, 0x43, 0x47, 0x1d, 0xd2, 0xaf, 0x04, 0xd9, 0xb8, 0xc8, 0x0d, 0x68, 0xe4, 0xf7, 0x3e, 0x48, 0xf1, 0x05, 0xbc, 0x25, 0xaa, 0x07, 0x71, 0xd9, 0x14, 0x78, 0xf6, 0x49, 0xb5, 0x90, 0xfd, 0xa7, 0xc6, 0x14, 0xfd, 0x1b, 0x1c, 0xff, 0x00, 0x4d, 0x8d, 0x2e, 0x73, 0x8c, 0x35, 0xa3, 0x52, 0x4f, 0x92, 0x48, 0xa6, 0x1a, 0x24, 0xb6, 0x2a, 0xfa, 0xa5, 0x9e, 0x60, 0x64, 0x39, 0x94, 0x13, 0xcb, 0x27, 0x73, 0x80, 0xf3, 0x0c, 0xf6, 0xff, 0x00, 0xd2, 0x5a, 0x78, 0xbf, 0x53, 0x71, 0xf6, 0x01, 0x75, 0xb6, 0x97, 0x6a, 0x25, 0xa1, 0xad, 0x1f, 0xf4, 0xb7, 0x23, 0x48, 0xb7, 0x94, 0x84, 0x97, 0x5b, 0xff, 0x00, 0x32, 0xa9, 0xdd, 0xfc, 0xed, 0x9b, 0x7e, 0x0d, 0x9e, 0x52, 0x4a, 0x95, 0x61, 0xff, 0xd0, 0xf3, 0x3b, 0xa7, 0x70, 0xee, 0x01, 0x8f, 0xb9, 0x59, 0xfa, 0x7e, 0xdf, 0xe4, 0xc8, 0xf9, 0x2a, 0xc2, 0x5c, 0x63, 0xc3, 0x54, 0x67, 0x87, 0x6e, 0x10, 0x35, 0x68, 0xd4, 0x79, 0x1e, 0x53, 0x4a, 0xe0, 0xdc, 0xe9, 0xb8, 0x1f, 0x6a, 0xda, 0x6c, 0x25, 0x94, 0x37, 0xb0, 0xd0, 0xb8, 0xad, 0x67, 0xe4, 0x55, 0x8a, 0x5b, 0x8b, 0x82, 0xc0, 0x6f, 0x76, 0x80, 0x34, 0x49, 0x05, 0x2e, 0x9e, 0xc6, 0x1c, 0x66, 0x31, 0xba, 0x10, 0x23, 0xe0, 0xaf, 0xe1, 0x61, 0x53, 0x43, 0x8d, 0x81, 0xb3, 0x67, 0xef, 0x9e, 0x49, 0x2a, 0x12, 0x6c, 0xb6, 0x63, 0x1a, 0x0c, 0x31, 0xba, 0x55, 0xcd, 0xac, 0xfa, 0x8e, 0xdf, 0x91, 0x6e, 0x91, 0xd9, 0xb3, 0xc9, 0x73, 0x90, 0x7a, 0xab, 0x6a, 0xc2, 0xa4, 0x60, 0xe2, 0x8f, 0xd2, 0x38, 0x03, 0x7d, 0x9e, 0x0d, 0xff, 0x00, 0xcc, 0xd6, 0xd3, 0x6b, 0x71, 0x67, 0xd2, 0x3e, 0x64, 0x72, 0xab, 0xdb, 0x8d, 0x54, 0x39, 0xc5, 0x83, 0x6b, 0x3d, 0xee, 0x2e, 0xd4, 0x92, 0x3c, 0x4a, 0x56, 0xba, 0xb4, 0x79, 0x5c, 0xf7, 0xb2, 0x96, 0x6c, 0x8d, 0xaf, 0x80, 0x48, 0x3c, 0xf0, 0xb2, 0x1f, 0x63, 0x9c, 0xe9, 0x3f, 0x24, 0x5c, 0xdb, 0xdd, 0x76, 0x43, 0xde, 0xfd, 0x5c, 0xe3, 0x24, 0xfc, 0x50, 0x00, 0x93, 0x0a, 0x78, 0x8a, 0x0d, 0x49, 0xca, 0xcf, 0x93, 0x63, 0x1b, 0x7d, 0xd7, 0x57, 0x50, 0xd5, 0xef, 0x70, 0x6b, 0x4f, 0xc7, 0x45, 0xdb, 0x74, 0x9e, 0x8d, 0x5e, 0x33, 0x83, 0xd8, 0x37, 0xdd, 0xc3, 0xac, 0x3d, 0xbf, 0x92, 0xc5, 0x5b, 0xea, 0xbf, 0xd5, 0x62, 0xc0, 0xdc, 0xbc, 0xbd, 0x2d, 0x22, 0x5a, 0xcf, 0xdd, 0x69, 0xff, 0x00, 0xd1, 0x8e, 0x5d, 0xa5, 0x38, 0xb5, 0xb0, 0x00, 0xc6, 0xc4, 0x24, 0x4a, 0xd6, 0x8d, 0x18, 0x04, 0x49, 0x88, 0x9e, 0x55, 0xd6, 0x61, 0xb0, 0xc1, 0x70, 0x32, 0xdd, 0x3c, 0x95, 0xda, 0xf1, 0xfe, 0xf5, 0x62, 0xbc, 0x76, 0x8e, 0x75, 0x28, 0x02, 0xa2, 0xe7, 0x7d, 0x92, 0xb9, 0x84, 0x96, 0x96, 0xda, 0xf7, 0x70, 0x12, 0x4e, 0x5a, 0xff, 0x00, 0xff, 0xd1, 0xf3, 0x7a, 0x21, 0xaf, 0xde, 0xef, 0xa2, 0x22, 0x55, 0xfc, 0x5a, 0xbd, 0x42, 0xfb, 0x08, 0xfa, 0x67, 0x4f, 0x82, 0xcd, 0x6d, 0x85, 0xc0, 0x56, 0x3b, 0x90, 0xb7, 0xf0, 0x2a, 0x0e, 0x63, 0x58, 0x3b, 0xf2, 0xa3, 0x9e, 0x8c, 0xb8, 0x86, 0xbe, 0x49, 0xf1, 0x2c, 0x0c, 0x86, 0xb4, 0x4c, 0x69, 0xe4, 0xaf, 0x6e, 0xcc, 0x6b, 0x7d, 0x46, 0xb3, 0x70, 0xec, 0x38, 0x51, 0x7d, 0x02, 0x8a, 0xc7, 0xa6, 0xd9, 0x20, 0x68, 0x0f, 0x8f, 0x8a, 0xcf, 0xc9, 0xc2, 0xea, 0x59, 0x5b, 0x48, 0xb0, 0x91, 0xae, 0xe6, 0xc9, 0x03, 0xc9, 0x30, 0x51, 0x66, 0xd4, 0x0d, 0xad, 0xbd, 0x5f, 0x53, 0xcc, 0x6b, 0xb6, 0x90, 0x5a, 0x3b, 0x83, 0x0b, 0x43, 0x17, 0x31, 0xd6, 0xc3, 0x6e, 0x12, 0x3b, 0x79, 0xac, 0xc1, 0x89, 0x47, 0xd9, 0xe8, 0x63, 0x98, 0x45, 0xed, 0x6c, 0x5a, 0xf1, 0xa0, 0x27, 0xc5, 0x5b, 0xc3, 0x6f, 0xa6, 0xe0, 0x1c, 0x7d, 0xb3, 0xa2, 0x69, 0x34, 0x7b, 0xae, 0x1a, 0x8d, 0x45, 0x17, 0x9d, 0xeb, 0xfd, 0x21, 0xd8, 0xb9, 0xae, 0xb5, 0x80, 0xbb, 0x1e, 0xd2, 0x5c, 0xd7, 0x78, 0x13, 0xf9, 0xae, 0x4b, 0xea, 0xc7, 0x4a, 0x39, 0xbd, 0x55, 0xb3, 0xed, 0x66, 0x38, 0xf5, 0x09, 0x22, 0x41, 0x23, 0xe8, 0x37, 0xfb, 0x4b, 0xa1, 0xeb, 0xd6, 0xfe, 0x88, 0x31, 0xbf, 0x41, 0xc0, 0xee, 0xd2, 0x74, 0x02, 0x78, 0x53, 0xfa, 0x97, 0x43, 0x19, 0x85, 0x65, 0xff, 0x00, 0x9d, 0x71, 0x33, 0xe4, 0x1a, 0x7d, 0x8d, 0x53, 0x42, 0x56, 0x35, 0x6b, 0xe5, 0x80, 0x06, 0xc7, 0x57, 0xa7, 0xc4, 0xa9, 0xdb, 0xb6, 0x81, 0x1f, 0xeb, 0xd9, 0x69, 0x56, 0xc2, 0xd0, 0x00, 0xe5, 0x55, 0xc0, 0x12, 0xc2, 0xd7, 0x4e, 0xa2, 0x5a, 0x7c, 0x0a, 0xd0, 0x63, 0x9a, 0xd1, 0xaf, 0xd2, 0xe2, 0x3c, 0x12, 0x62, 0x66, 0xc6, 0x42, 0x23, 0x5a, 0x49, 0x8f, 0x10, 0xa2, 0xd2, 0x3e, 0x28, 0x9d, 0xc4, 0x88, 0x09, 0x29, 0x16, 0xc3, 0x3c, 0x24, 0x8d, 0xe6, 0x92, 0x72, 0x1f, 0xff, 0xd2, 0xf3, 0xbb, 0xb0, 0xfe, 0xcb, 0x99, 0xe9, 0xce, 0xf6, 0x88, 0x2d, 0x77, 0x91, 0x5b, 0x3d, 0x3d, 0xd0, 0xe6, 0x90, 0xa9, 0x65, 0x57, 0x38, 0x95, 0xdd, 0xcb, 0x9a, 0x7d, 0xce, 0xf2, 0x3f, 0x44, 0x23, 0x60, 0x58, 0x76, 0xe9, 0xca, 0x8c, 0xea, 0x1b, 0x31, 0x02, 0x32, 0x23, 0xea, 0xee, 0xb1, 0xcd, 0xb0, 0xc7, 0x87, 0x74, 0x7a, 0xeb, 0x70, 0x1a, 0x71, 0xe1, 0xfe, 0xe4, 0x1c, 0x1d, 0xae, 0xe5, 0x69, 0xd8, 0xfa, 0x99, 0x50, 0x0d, 0x1a, 0xf7, 0x2a, 0x3a, 0x0c, 0xf4, 0x1a, 0x8e, 0xc7, 0x27, 0x5d, 0xbf, 0x18, 0x41, 0xdc, 0xc2, 0xf0, 0x7f, 0x74, 0xf6, 0x3a, 0x22, 0x66, 0xdb, 0x68, 0xc6, 0x80, 0x48, 0x6b, 0x88, 0x06, 0x39, 0x0d, 0xee, 0xaa, 0x1f, 0xb3, 0xd5, 0x1b, 0x83, 0xd8, 0x3b, 0x38, 0x8f, 0x69, 0xfe, 0xdf, 0xd1, 0x4d, 0x29, 0xa1, 0x4c, 0x7a, 0xf4, 0xbf, 0xa7, 0x92, 0xcf, 0xa5, 0x20, 0x08, 0xf3, 0xf6, 0xff, 0x00, 0x15, 0xbb, 0xd1, 0x31, 0xd9, 0x5e, 0x3d, 0x75, 0x56, 0x36, 0x88, 0x00, 0x81, 0xe0, 0x16, 0x5e, 0x55, 0x74, 0x3f, 0x00, 0x9d, 0xe0, 0xcc, 0x69, 0xe7, 0x3a, 0x2d, 0xbe, 0x90, 0x00, 0xa9, 0xae, 0xef, 0x1f, 0x95, 0x4b, 0x0d, 0x9a, 0xdc, 0xc7, 0x45, 0xfe, 0xb1, 0x7d, 0x60, 0xa7, 0xa1, 0xe0, 0x1f, 0x4e, 0x1d, 0x99, 0x69, 0x02, 0x9a, 0xcf, 0x1f, 0xca, 0x7b, 0xbf, 0x90, 0xc5, 0xc2, 0xb3, 0xeb, 0x57, 0xd6, 0x03, 0x6b, 0xae, 0x39, 0xb6, 0x82, 0xe3, 0x31, 0xa1, 0x68, 0xf2, 0x6b, 0x5c, 0x12, 0xfa, 0xe1, 0x91, 0x66, 0x47, 0x5d, 0xb8, 0x3b, 0x4f, 0x44, 0x36, 0xb6, 0x8f, 0x28, 0xdd, 0xff, 0x00, 0x7e, 0x46, 0xab, 0x12, 0x2b, 0x65, 0x55, 0x32, 0xa7, 0x62, 0xb6, 0xbd, 0xf7, 0x64, 0x10, 0xdb, 0x03, 0x9f, 0x1b, 0x9e, 0xc7, 0xd9, 0xb8, 0x3b, 0x1f, 0x67, 0xf3, 0x6c, 0x52, 0x80, 0xd7, 0x7d, 0x0f, 0xea, 0x7f, 0x5d, 0x1d, 0x67, 0xa6, 0x0b, 0x1e, 0x47, 0xda, 0x69, 0x3b, 0x2e, 0x03, 0xc7, 0xf3, 0x5f, 0x1f, 0xf0, 0x8b, 0xa1, 0x02, 0x46, 0xba, 0x79, 0xaf, 0x32, 0xff, 0x00, 0x16, 0xad, 0xca, 0x1d, 0x57, 0x2a, 0xdc, 0x79, 0x18, 0x41, 0xb0, 0xf6, 0x9e, 0xe4, 0x9f, 0xd0, 0x8f, 0xeb, 0x31, 0xab, 0xd2, 0x83, 0xa4, 0xcb, 0x8c, 0xb8, 0xa0, 0x42, 0x12, 0x7b, 0x67, 0x9f, 0x2f, 0xf5, 0x09, 0x26, 0x96, 0xc4, 0xce, 0xa9, 0x20, 0xa7, 0xff, 0xd3, 0xf3, 0x2f, 0xb4, 0x5d, 0xe9, 0x0a, 0xb7, 0x9f, 0x4c, 0x19, 0xdb, 0x3a, 0x2d, 0x5e, 0x94, 0xfd, 0xc4, 0xb7, 0xc5, 0x62, 0xf9, 0x2b, 0xfd, 0x2e, 0xe3, 0x5d, 0xe0, 0x7c, 0x13, 0x48, 0xd1, 0x92, 0x12, 0xa9, 0x0b, 0x7a, 0xbc, 0x2d, 0xc2, 0x7f, 0x92, 0x60, 0xab, 0x4e, 0x79, 0x2e, 0x00, 0xf0, 0xaa, 0xe1, 0xda, 0x3d, 0x43, 0xfc, 0xad, 0x55, 0xbb, 0x80, 0x79, 0x81, 0xa0, 0xe6, 0x54, 0x32, 0x6d, 0x02, 0xbe, 0xf3, 0x61, 0x81, 0xa8, 0x44, 0x14, 0x03, 0x59, 0x0e, 0x1c, 0xf6, 0x1f, 0xdc, 0xb2, 0xec, 0xa3, 0x23, 0x77, 0xe8, 0x6e, 0x70, 0xf2, 0x25, 0x1f, 0x1f, 0x17, 0xa9, 0x6d, 0x71, 0x36, 0x97, 0x47, 0x00, 0xa4, 0x02, 0xe0, 0x2c, 0x7c, 0xc1, 0xab, 0xd5, 0x31, 0x85, 0x35, 0xd4, 0xe6, 0x13, 0x02, 0xd6, 0x4b, 0x67, 0x48, 0x2b, 0xa9, 0xe9, 0x2e, 0x02, 0xb6, 0x4f, 0x82, 0xe5, 0x7a, 0x95, 0x19, 0xc6, 0x87, 0x3d, 0xfb, 0xa2, 0xb8, 0x79, 0x1e, 0x4d, 0x3b, 0x96, 0xcf, 0x4f, 0xbd, 0xcd, 0xa2, 0xa2, 0x1f, 0xa0, 0x82, 0xd3, 0xfc, 0x97, 0x05, 0x24, 0x36, 0x6b, 0xf3, 0x31, 0xa2, 0x35, 0x79, 0xef, 0xad, 0xf8, 0xae, 0xaf, 0xaf, 0xd8, 0xf2, 0xd8, 0x6d, 0xed, 0x6b, 0xda, 0x7b, 0x18, 0x1b, 0x5d, 0xff, 0x00, 0x52, 0xb1, 0x6d, 0xf0, 0x81, 0x31, 0xca, 0xf4, 0x6e, 0xb1, 0x80, 0xce, 0xb1, 0x84, 0xc0, 0x21, 0xb7, 0xd6, 0x77, 0x31, 0xd1, 0x27, 0xc1, 0xcd, 0xfe, 0xd2, 0xe3, 0xec, 0xe8, 0x1d, 0x45, 0x96, 0xb0, 0x9a, 0xb7, 0x87, 0x3f, 0x68, 0x2d, 0xf7, 0x01, 0x1f, 0xbe, 0xd1, 0xf4, 0x7f, 0xb4, 0xa4, 0x0d, 0x77, 0xbb, 0xfa, 0x8f, 0x80, 0x3a, 0x7f, 0x43, 0xaa, 0xe2, 0xdf, 0xd2, 0x65, 0x7e, 0x95, 0xe4, 0x0f, 0x1f, 0xa1, 0xfe, 0x6b, 0x16, 0x9f, 0x52, 0xfa, 0xc1, 0xd3, 0xba, 0x6d, 0x26, 0xdc, 0xac, 0x86, 0xd4, 0xd9, 0x0d, 0x31, 0x2e, 0x74, 0x9e, 0xdb, 0x59, 0x2e, 0x55, 0xe8, 0xc9, 0xb2, 0x96, 0xd5, 0x4b, 0x9f, 0xb8, 0x6d, 0xda, 0x1c, 0x04, 0x09, 0x03, 0xfe, 0x8a, 0xc6, 0xfa, 0xd3, 0xf5, 0x6a, 0xbe, 0xbb, 0x5b, 0x2e, 0xc6, 0xb5, 0x94, 0xe6, 0xd5, 0x20, 0x97, 0x7d, 0x1b, 0x1b, 0xf9, 0xad, 0x7c, 0x7d, 0x17, 0xb7, 0xf3, 0x1e, 0x92, 0x1b, 0x7f, 0xf8, 0xe0, 0x7d, 0x59, 0xdd, 0xfd, 0x32, 0xd8, 0x8f, 0xa5, 0xe8, 0x3a, 0x12, 0x5c, 0x3f, 0xfc, 0xc4, 0xfa, 0xc3, 0xb3, 0x77, 0xa7, 0x56, 0xed, 0xdb, 0x76, 0x7a, 0x8d, 0xdd, 0x1f, 0xbf, 0xfd, 0x44, 0x92, 0x56, 0x8f, 0xff, 0xd4, 0xf2, 0xe8, 0x86, 0x17, 0x1e, 0xfa, 0x04, 0x56, 0x4b, 0x43, 0x6c, 0x6f, 0x2d, 0xe5, 0x46, 0x01, 0x64, 0x2b, 0x14, 0x32, 0x5b, 0xb4, 0xa0, 0x52, 0x1d, 0xde, 0x9b, 0x94, 0xdb, 0xab, 0x6b, 0x81, 0xf7, 0x05, 0xb0, 0xd7, 0x07, 0xb2, 0x27, 0x55, 0xc6, 0x57, 0x65, 0xd8, 0x76, 0x6e, 0x64, 0xed, 0xee, 0x16, 0xce, 0x27, 0x57, 0x63, 0xda, 0x0c, 0xc2, 0x8e, 0x51, 0x67, 0x84, 0xfa, 0x1d, 0xdd, 0x62, 0xc7, 0x07, 0xe9, 0xf7, 0xa3, 0xd6, 0x6c, 0x02, 0x41, 0x55, 0x31, 0xf3, 0x2b, 0xb3, 0xba, 0x2b, 0x2e, 0x68, 0x24, 0x1d, 0x47, 0x64, 0xca, 0xa6, 0x50, 0x41, 0x65, 0x90, 0x6c, 0xb1, 0xa5, 0xae, 0x33, 0x23, 0x51, 0xe4, 0xab, 0x7d, 0x5d, 0xcb, 0xb6, 0xcc, 0x37, 0xd0, 0x40, 0x73, 0x71, 0xde, 0x58, 0x09, 0xe7, 0x6f, 0x2c, 0x44, 0xc9, 0xc9, 0xae, 0xba, 0x9d, 0x63, 0x88, 0x01, 0xa0, 0x95, 0x9d, 0xf5, 0x3f, 0x2a, 0xe6, 0x67, 0xdb, 0x50, 0x83, 0x55, 0xad, 0x36, 0x3e, 0x78, 0x10, 0x74, 0x77, 0xfd, 0x2d, 0xaa, 0x4c, 0x7d, 0x58, 0x73, 0x91, 0xa0, 0x0f, 0x51, 0x45, 0xb7, 0x33, 0xdd, 0x58, 0x69, 0x1d, 0xd8, 0x0c, 0x9f, 0x96, 0x88, 0x19, 0x99, 0x19, 0xac, 0xcf, 0xa3, 0xd2, 0xad, 0xb5, 0xdb, 0x76, 0x8f, 0xad, 0xc4, 0xea, 0xcf, 0xdf, 0x7e, 0xdf, 0xdd, 0xfc, 0xd5, 0xa3, 0x5e, 0x43, 0x2b, 0x6b, 0xb2, 0xad, 0x3b, 0x6a, 0xa4, 0x13, 0xa7, 0x04, 0xac, 0x7a, 0x6f, 0xb3, 0x23, 0x26, 0xcc, 0xfb, 0xb4, 0x75, 0x8e, 0x01, 0x83, 0xf7, 0x58, 0x3e, 0x8b, 0x53, 0xa7, 0x2a, 0x1a, 0x31, 0x42, 0x36, 0x5d, 0x4c, 0x9a, 0xf2, 0xdc, 0xc6, 0xfe, 0x98, 0xb4, 0x34, 0xcb, 0x48, 0x0a, 0x8f, 0xdb, 0xb2, 0xeb, 0x76, 0xd6, 0x07, 0x5c, 0x59, 0xc9, 0x64, 0x8f, 0x93, 0xa7, 0x73, 0x16, 0x83, 0xaf, 0x0e, 0xa4, 0x33, 0xef, 0x50, 0xc5, 0x0c, 0xda, 0x59, 0x10, 0x06, 0x8a, 0x2e, 0x29, 0x0e, 0xac, 0xc2, 0x31, 0x3d, 0x36, 0x69, 0x7e, 0xd6, 0xcc, 0xf5, 0x3d, 0x6f, 0xb3, 0xeb, 0x1b, 0x76, 0xef, 0x3b, 0xa3, 0xfa, 0xc9, 0x2b, 0x5f, 0x66, 0x6f, 0xa9, 0x1e, 0x73, 0xf2, 0x49, 0x2e, 0x39, 0xf7, 0x4f, 0xb7, 0x8d, 0xff, 0xd5, 0xf3, 0x26, 0xfe, 0x0a, 0xc5, 0x1b, 0xa7, 0xcb, 0xb2, 0xcf, 0x49, 0x03, 0xb2, 0x46, 0xee, 0xd9, 0xd9, 0xb3, 0xf4, 0x9f, 0x25, 0x4a, 0xdf, 0x4b, 0x77, 0xe8, 0x27, 0xd4, 0xef, 0x1c, 0x2a, 0x29, 0x26, 0xc5, 0x7c, 0x9d, 0x6c, 0x7f, 0xb7, 0x6e, 0x1b, 0x26, 0x7f, 0x05, 0xa3, 0xfe, 0x53, 0x8d, 0x62, 0x57, 0x30, 0x92, 0x12, 0xfa, 0x2f, 0x86, 0xdf, 0xa4, 0xec, 0x67, 0xfe, 0xd0, 0xf4, 0xff, 0x00, 0x4d, 0xfc, 0xdf, 0x78, 0xe1, 0x68, 0x7d, 0x54, 0x99, 0xbf, 0x6f, 0xf3, 0xbe, 0xdf, 0x8e, 0xdd, 0x7f, 0xef, 0xeb, 0x97, 0x49, 0x3e, 0x3b, 0x7f, 0x06, 0x2c, 0x9f, 0x37, 0x5f, 0xf0, 0x9f, 0x4c, 0xeb, 0x7b, 0xbf, 0x67, 0x55, 0xe8, 0xff, 0x00, 0x31, 0xbc, 0x7a, 0x9e, 0x31, 0xdb, 0xfe, 0x92, 0xae, 0x37, 0x7a, 0x4d, 0xdb, 0xe2, 0x17, 0x9d, 0xa4, 0xa3, 0xc9, 0xba, 0xfc, 0x7b, 0x7d, 0x5f, 0x52, 0xa7, 0x7e, 0xd1, 0x28, 0xf8, 0xf3, 0xb0, 0xc7, 0x32, 0xbc, 0x99, 0x24, 0xc5, 0xe3, 0xab, 0xeb, 0x1f, 0xa4, 0xf5, 0xfc, 0xe1, 0x25, 0xe4, 0xe9, 0x24, 0x97, 0xff, 0xd9, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x41, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x62, 0x00, 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x68, 0x00, 0x6f, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x73, 0x00, 0x68, 0x00, 0x6f, 0x00, 0x70, 0x00, 0x00, 0x00, 0x13, 0x00, 0x41, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x62, 0x00, 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x68, 0x00, 0x6f, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x73, 0x00, 0x68, 0x00, 0x6f, 0x00, 0x70, 0x00, 0x20, 0x00, 0x37, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x00, 0x00, 0x01, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0xff, 0xe1, 0x15, 0x67, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x00, 0x3c, 0x3f, 0x78, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x20, 0x62, 0x65, 0x67, 0x69, 0x6e, 0x3d, 0x27, 0xef, 0xbb, 0xbf, 0x27, 0x20, 0x69, 0x64, 0x3d, 0x27, 0x57, 0x35, 0x4d, 0x30, 0x4d, 0x70, 0x43, 0x65, 0x68, 0x69, 0x48, 0x7a, 0x72, 0x65, 0x53, 0x7a, 0x4e, 0x54, 0x63, 0x7a, 0x6b, 0x63, 0x39, 0x64, 0x27, 0x3f, 0x3e, 0x0a, 0x3c, 0x3f, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2d, 0x78, 0x61, 0x70, 0x2d, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x20, 0x65, 0x73, 0x63, 0x3d, 0x22, 0x43, 0x52, 0x22, 0x3f, 0x3e, 0x0a, 0x3c, 0x78, 0x3a, 0x78, 0x61, 0x70, 0x6d, 0x65, 0x74, 0x61, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, 0x3d, 0x27, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x3a, 0x6e, 0x73, 0x3a, 0x6d, 0x65, 0x74, 0x61, 0x2f, 0x27, 0x20, 0x78, 0x3a, 0x78, 0x61, 0x70, 0x74, 0x6b, 0x3d, 0x27, 0x58, 0x4d, 0x50, 0x20, 0x74, 0x6f, 0x6f, 0x6c, 0x6b, 0x69, 0x74, 0x20, 0x32, 0x2e, 0x38, 0x2e, 0x32, 0x2d, 0x33, 0x33, 0x2c, 0x20, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x20, 0x31, 0x2e, 0x35, 0x27, 0x3e, 0x0a, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x52, 0x44, 0x46, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x72, 0x64, 0x66, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x31, 0x39, 0x39, 0x39, 0x2f, 0x30, 0x32, 0x2f, 0x32, 0x32, 0x2d, 0x72, 0x64, 0x66, 0x2d, 0x73, 0x79, 0x6e, 0x74, 0x61, 0x78, 0x2d, 0x6e, 0x73, 0x23, 0x27, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x69, 0x58, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x58, 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x27, 0x3e, 0x0a, 0x0a, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x27, 0x75, 0x75, 0x69, 0x64, 0x3a, 0x32, 0x32, 0x64, 0x30, 0x32, 0x62, 0x30, 0x61, 0x2d, 0x62, 0x32, 0x34, 0x39, 0x2d, 0x31, 0x31, 0x64, 0x62, 0x2d, 0x38, 0x61, 0x66, 0x38, 0x2d, 0x39, 0x31, 0x64, 0x35, 0x34, 0x30, 0x33, 0x66, 0x39, 0x32, 0x66, 0x39, 0x27, 0x0a, 0x20, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x70, 0x64, 0x66, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x64, 0x66, 0x2f, 0x31, 0x2e, 0x33, 0x2f, 0x27, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x21, 0x2d, 0x2d, 0x20, 0x70, 0x64, 0x66, 0x3a, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x20, 0x69, 0x73, 0x20, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x64, 0x20, 0x2d, 0x2d, 0x3e, 0x0a, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x0a, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x27, 0x75, 0x75, 0x69, 0x64, 0x3a, 0x32, 0x32, 0x64, 0x30, 0x32, 0x62, 0x30, 0x61, 0x2d, 0x62, 0x32, 0x34, 0x39, 0x2d, 0x31, 0x31, 0x64, 0x62, 0x2d, 0x38, 0x61, 0x66, 0x38, 0x2d, 0x39, 0x31, 0x64, 0x35, 0x34, 0x30, 0x33, 0x66, 0x39, 0x32, 0x66, 0x39, 0x27, 0x0a, 0x20, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x27, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x21, 0x2d, 0x2d, 0x20, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x3a, 0x43, 0x61, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x69, 0x73, 0x20, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x64, 0x20, 0x2d, 0x2d, 0x3e, 0x0a, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x0a, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x27, 0x75, 0x75, 0x69, 0x64, 0x3a, 0x32, 0x32, 0x64, 0x30, 0x32, 0x62, 0x30, 0x61, 0x2d, 0x62, 0x32, 0x34, 0x39, 0x2d, 0x31, 0x31, 0x64, 0x62, 0x2d, 0x38, 0x61, 0x66, 0x38, 0x2d, 0x39, 0x31, 0x64, 0x35, 0x34, 0x30, 0x33, 0x66, 0x39, 0x32, 0x66, 0x39, 0x27, 0x0a, 0x20, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, 0x61, 0x70, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x27, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x21, 0x2d, 0x2d, 0x20, 0x78, 0x61, 0x70, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x69, 0x73, 0x20, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x64, 0x20, 0x2d, 0x2d, 0x3e, 0x0a, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x0a, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x27, 0x75, 0x75, 0x69, 0x64, 0x3a, 0x32, 0x32, 0x64, 0x30, 0x32, 0x62, 0x30, 0x61, 0x2d, 0x62, 0x32, 0x34, 0x39, 0x2d, 0x31, 0x31, 0x64, 0x62, 0x2d, 0x38, 0x61, 0x66, 0x38, 0x2d, 0x39, 0x31, 0x64, 0x35, 0x34, 0x30, 0x33, 0x66, 0x39, 0x32, 0x66, 0x39, 0x27, 0x0a, 0x20, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, 0x61, 0x70, 0x4d, 0x4d, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x6d, 0x6d, 0x2f, 0x27, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x78, 0x61, 0x70, 0x4d, 0x4d, 0x3a, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x3e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x3a, 0x64, 0x6f, 0x63, 0x69, 0x64, 0x3a, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x3a, 0x32, 0x32, 0x64, 0x30, 0x32, 0x62, 0x30, 0x36, 0x2d, 0x62, 0x32, 0x34, 0x39, 0x2d, 0x31, 0x31, 0x64, 0x62, 0x2d, 0x38, 0x61, 0x66, 0x38, 0x2d, 0x39, 0x31, 0x64, 0x35, 0x34, 0x30, 0x33, 0x66, 0x39, 0x32, 0x66, 0x39, 0x3c, 0x2f, 0x78, 0x61, 0x70, 0x4d, 0x4d, 0x3a, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x3e, 0x0a, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x0a, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x27, 0x75, 0x75, 0x69, 0x64, 0x3a, 0x32, 0x32, 0x64, 0x30, 0x32, 0x62, 0x30, 0x61, 0x2d, 0x62, 0x32, 0x34, 0x39, 0x2d, 0x31, 0x31, 0x64, 0x62, 0x2d, 0x38, 0x61, 0x66, 0x38, 0x2d, 0x39, 0x31, 0x64, 0x35, 0x34, 0x30, 0x33, 0x66, 0x39, 0x32, 0x66, 0x39, 0x27, 0x0a, 0x20, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x64, 0x63, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x70, 0x75, 0x72, 0x6c, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x64, 0x63, 0x2f, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x31, 0x2e, 0x31, 0x2f, 0x27, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x64, 0x63, 0x3a, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x41, 0x6c, 0x74, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x6c, 0x69, 0x20, 0x78, 0x6d, 0x6c, 0x3a, 0x6c, 0x61, 0x6e, 0x67, 0x3d, 0x27, 0x78, 0x2d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x27, 0x3e, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x6c, 0x69, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x41, 0x6c, 0x74, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x2f, 0x64, 0x63, 0x3a, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x0a, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x52, 0x44, 0x46, 0x3e, 0x0a, 0x3c, 0x2f, 0x78, 0x3a, 0x78, 0x61, 0x70, 0x6d, 0x65, 0x74, 0x61, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x3c, 0x3f, 0x78, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x20, 0x65, 0x6e, 0x64, 0x3d, 0x27, 0x77, 0x27, 0x3f, 0x3e, 0xff, 0xee, 0x00, 0x0e, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x00, 0x64, 0x40, 0x00, 0x00, 0x00, 0x01, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x04, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x06, 0x04, 0x03, 0x04, 0x06, 0x07, 0x05, 0x04, 0x04, 0x05, 0x07, 0x08, 0x06, 0x06, 0x07, 0x06, 0x06, 0x08, 0x0a, 0x08, 0x09, 0x09, 0x09, 0x09, 0x08, 0x0a, 0x0a, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0a, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x01, 0x04, 0x05, 0x05, 0x08, 0x07, 0x08, 0x0f, 0x0a, 0x0a, 0x0f, 0x14, 0x0e, 0x0e, 0x0e, 0x14, 0x14, 0x0e, 0x0e, 0x0e, 0x0e, 0x14, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0x64, 0x03, 0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xdd, 0x00, 0x04, 0x00, 0x0d, 0xff, 0xc4, 0x01, 0xa2, 0x00, 0x00, 0x00, 0x07, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x05, 0x03, 0x02, 0x06, 0x01, 0x00, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x01, 0x00, 0x02, 0x02, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x02, 0x06, 0x07, 0x03, 0x04, 0x02, 0x06, 0x02, 0x73, 0x01, 0x02, 0x03, 0x11, 0x04, 0x00, 0x05, 0x21, 0x12, 0x31, 0x41, 0x51, 0x06, 0x13, 0x61, 0x22, 0x71, 0x81, 0x14, 0x32, 0x91, 0xa1, 0x07, 0x15, 0xb1, 0x42, 0x23, 0xc1, 0x52, 0xd1, 0xe1, 0x33, 0x16, 0x62, 0xf0, 0x24, 0x72, 0x82, 0xf1, 0x25, 0x43, 0x34, 0x53, 0x92, 0xa2, 0xb2, 0x63, 0x73, 0xc2, 0x35, 0x44, 0x27, 0x93, 0xa3, 0xb3, 0x36, 0x17, 0x54, 0x64, 0x74, 0xc3, 0xd2, 0xe2, 0x08, 0x26, 0x83, 0x09, 0x0a, 0x18, 0x19, 0x84, 0x94, 0x45, 0x46, 0xa4, 0xb4, 0x56, 0xd3, 0x55, 0x28, 0x1a, 0xf2, 0xe3, 0xf3, 0xc4, 0xd4, 0xe4, 0xf4, 0x65, 0x75, 0x85, 0x95, 0xa5, 0xb5, 0xc5, 0xd5, 0xe5, 0xf5, 0x66, 0x76, 0x86, 0x96, 0xa6, 0xb6, 0xc6, 0xd6, 0xe6, 0xf6, 0x37, 0x47, 0x57, 0x67, 0x77, 0x87, 0x97, 0xa7, 0xb7, 0xc7, 0xd7, 0xe7, 0xf7, 0x38, 0x48, 0x58, 0x68, 0x78, 0x88, 0x98, 0xa8, 0xb8, 0xc8, 0xd8, 0xe8, 0xf8, 0x29, 0x39, 0x49, 0x59, 0x69, 0x79, 0x89, 0x99, 0xa9, 0xb9, 0xc9, 0xd9, 0xe9, 0xf9, 0x2a, 0x3a, 0x4a, 0x5a, 0x6a, 0x7a, 0x8a, 0x9a, 0xaa, 0xba, 0xca, 0xda, 0xea, 0xfa, 0x11, 0x00, 0x02, 0x02, 0x01, 0x02, 0x03, 0x05, 0x05, 0x04, 0x05, 0x06, 0x04, 0x08, 0x03, 0x03, 0x6d, 0x01, 0x00, 0x02, 0x11, 0x03, 0x04, 0x21, 0x12, 0x31, 0x41, 0x05, 0x51, 0x13, 0x61, 0x22, 0x06, 0x71, 0x81, 0x91, 0x32, 0xa1, 0xb1, 0xf0, 0x14, 0xc1, 0xd1, 0xe1, 0x23, 0x42, 0x15, 0x52, 0x62, 0x72, 0xf1, 0x33, 0x24, 0x34, 0x43, 0x82, 0x16, 0x92, 0x53, 0x25, 0xa2, 0x63, 0xb2, 0xc2, 0x07, 0x73, 0xd2, 0x35, 0xe2, 0x44, 0x83, 0x17, 0x54, 0x93, 0x08, 0x09, 0x0a, 0x18, 0x19, 0x26, 0x36, 0x45, 0x1a, 0x27, 0x64, 0x74, 0x55, 0x37, 0xf2, 0xa3, 0xb3, 0xc3, 0x28, 0x29, 0xd3, 0xe3, 0xf3, 0x84, 0x94, 0xa4, 0xb4, 0xc4, 0xd4, 0xe4, 0xf4, 0x65, 0x75, 0x85, 0x95, 0xa5, 0xb5, 0xc5, 0xd5, 0xe5, 0xf5, 0x46, 0x56, 0x66, 0x76, 0x86, 0x96, 0xa6, 0xb6, 0xc6, 0xd6, 0xe6, 0xf6, 0x47, 0x57, 0x67, 0x77, 0x87, 0x97, 0xa7, 0xb7, 0xc7, 0xd7, 0xe7, 0xf7, 0x38, 0x48, 0x58, 0x68, 0x78, 0x88, 0x98, 0xa8, 0xb8, 0xc8, 0xd8, 0xe8, 0xf8, 0x39, 0x49, 0x59, 0x69, 0x79, 0x89, 0x99, 0xa9, 0xb9, 0xc9, 0xd9, 0xe9, 0xf9, 0x2a, 0x3a, 0x4a, 0x5a, 0x6a, 0x7a, 0x8a, 0x9a, 0xaa, 0xba, 0xca, 0xda, 0xea, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf0, 0x67, 0xa6, 0x5c, 0x0f, 0x01, 0xd4, 0x7e, 0x18, 0x12, 0x98, 0xe9, 0xd6, 0x2d, 0x34, 0x6d, 0x70, 0xdf, 0xdc, 0xa1, 0xe3, 0xec, 0x5b, 0xfb, 0x32, 0x24, 0xb2, 0x01, 0x1f, 0x15, 0xa4, 0x52, 0x4a, 0x82, 0x31, 0xf1, 0xfe, 0xd1, 0x3d, 0x14, 0x64, 0x49, 0x64, 0x22, 0x98, 0xcf, 0xa5, 0x46, 0x6c, 0x16, 0x55, 0x71, 0x56, 0x62, 0x28, 0x07, 0xc5, 0x45, 0x15, 0xa0, 0xc8, 0x89, 0x33, 0xe1, 0x63, 0xd2, 0xd8, 0x34, 0x44, 0x17, 0xa0, 0x2c, 0x4d, 0x16, 0xbb, 0xed, 0xdc, 0xf8, 0x64, 0xc1, 0x6b, 0x31, 0x42, 0x18, 0x8e, 0xc7, 0xb5, 0x2a, 0x7d, 0xb2, 0x56, 0xc5, 0x61, 0x8c, 0xf2, 0xa0, 0x1b, 0x1e, 0x83, 0x0d, 0xa1, 0x63, 0x50, 0x1f, 0x97, 0x7c, 0x2a, 0xa9, 0x1a, 0x9a, 0x86, 0x4f, 0xb4, 0xb4, 0x38, 0x0a, 0xa6, 0x0b, 0xb8, 0x0c, 0x05, 0x14, 0xf8, 0x76, 0x3e, 0x19, 0x14, 0xb6, 0x78, 0xf8, 0x8c, 0x2a, 0xd5, 0x01, 0xdc, 0x6f, 0x8a, 0x1a, 0xe3, 0x8d, 0xab, 0xff, 0xd0, 0xf0, 0xec, 0xe9, 0x15, 0xb5, 0xb9, 0x5a, 0x7c, 0x4c, 0xa2, 0x9e, 0x24, 0xf5, 0xca, 0xc6, 0xe5, 0x99, 0xd9, 0x34, 0x99, 0x04, 0x3a, 0x7d, 0xb5, 0xba, 0xd5, 0x51, 0x63, 0x0e, 0xc7, 0xc5, 0x9b, 0x73, 0xf8, 0xe4, 0x6f, 0x76, 0xca, 0xd9, 0xda, 0x54, 0x6d, 0x72, 0x2e, 0x1a, 0x57, 0x11, 0x44, 0x40, 0x0d, 0x27, 0x7a, 0x0f, 0xd9, 0x5f, 0x12, 0x69, 0x4c, 0x84, 0xcd, 0x36, 0xe3, 0x85, 0xb2, 0xcd, 0x2f, 0x4a, 0x8b, 0x58, 0x36, 0xf6, 0x76, 0xa8, 0x64, 0x64, 0x3c, 0xa4, 0x93, 0xaa, 0x25, 0x3c, 0x49, 0xda, 0xa4, 0xe5, 0x26, 0x54, 0xe4, 0x8c, 0x7c, 0x5c, 0x93, 0x4d, 0x67, 0xc9, 0x3a, 0x6e, 0x9f, 0x13, 0xb4, 0xce, 0xf7, 0x3a, 0x9b, 0xad, 0x52, 0xd6, 0x2a, 0xd1, 0x49, 0xee, 0xc7, 0xf8, 0x64, 0x46, 0x42, 0x4e, 0xcd, 0x92, 0xc2, 0x00, 0xdd, 0x8a, 0x47, 0xe5, 0x69, 0x6e, 0xd4, 0xa4, 0x08, 0x16, 0x83, 0x9c, 0x8c, 0xdd, 0x95, 0x6b, 0xb9, 0xf6, 0xef, 0x97, 0x78, 0x94, 0xe3, 0x78, 0x04, 0xa4, 0xf3, 0xe8, 0xee, 0x64, 0xe1, 0x12, 0x10, 0x05, 0x6a, 0xc7, 0xc0, 0x6f, 0x53, 0xf3, 0xc9, 0x89, 0xb4, 0x9c, 0x4e, 0xb4, 0xf2, 0xd3, 0xde, 0x7a, 0xd2, 0x19, 0x16, 0x38, 0x61, 0x5d, 0xd9, 0x88, 0x05, 0x9c, 0xf4, 0x0a, 0x0f, 0x5f, 0x73, 0x84, 0xe4, 0xa4, 0xc7, 0x0d, 0xa5, 0xf1, 0x59, 0xba, 0x5c, 0x08, 0x98, 0x6f, 0xc8, 0x20, 0xfa, 0x4e, 0x4e, 0xf6, 0x69, 0xe1, 0xa2, 0x89, 0xfd, 0x1f, 0x77, 0x2c, 0xe6, 0xce, 0xd6, 0x17, 0x9a, 0x69, 0xdb, 0xd3, 0x86, 0x18, 0xc1, 0x67, 0x77, 0x26, 0x80, 0x28, 0x1b, 0x93, 0x88, 0x41, 0x0f, 0x40, 0xb0, 0xfc, 0x87, 0xf3, 0x43, 0x98, 0xd7, 0x58, 0x96, 0xdb, 0x4d, 0x91, 0x88, 0xe5, 0x6c, 0x58, 0xdc, 0x5c, 0x2a, 0xf7, 0x2c, 0xb1, 0xfc, 0x20, 0x8f, 0x02, 0xd9, 0x65, 0x06, 0xbe, 0x26, 0x6f, 0xa2, 0x7f, 0xce, 0x3d, 0x69, 0x26, 0xdd, 0x13, 0x52, 0xbf, 0xbd, 0x92, 0x62, 0x59, 0x4c, 0x90, 0xac, 0x50, 0x45, 0x5e, 0xbb, 0x09, 0x03, 0x12, 0x29, 0x84, 0x00, 0xc4, 0xc9, 0x11, 0xff, 0x00, 0x42, 0xe7, 0xa7, 0x7a, 0xd4, 0xfd, 0x21, 0x79, 0xe9, 0x78, 0x71, 0x8b, 0x95, 0x39, 0x75, 0xaf, 0x4e, 0x98, 0x78, 0x42, 0x38, 0xdf, 0xff, 0xd1, 0xf0, 0xe6, 0xa0, 0x58, 0xc8, 0x84, 0x9a, 0xaa, 0x30, 0x55, 0xf9, 0x0a, 0x6f, 0x90, 0x0c, 0xca, 0x72, 0x48, 0xb8, 0x1e, 0x89, 0xa7, 0x23, 0x17, 0x24, 0xff, 0x00, 0x61, 0xb6, 0x54, 0x76, 0x6e, 0x1b, 0xa7, 0xbe, 0x50, 0xf2, 0xc1, 0xd7, 0x4c, 0x52, 0x5e, 0x33, 0x5b, 0xe9, 0x10, 0xf4, 0x54, 0x3c, 0x5e, 0x77, 0xee, 0x49, 0xec, 0x2b, 0xb6, 0x63, 0xe4, 0xc9, 0xc3, 0xef, 0x73, 0xf0, 0xe1, 0x32, 0x1b, 0xf2, 0x7a, 0x05, 0xce, 0xad, 0x65, 0xa1, 0x98, 0xb4, 0x0f, 0x2a, 0x5b, 0x23, 0xeb, 0x12, 0x00, 0x88, 0xb0, 0xa8, 0x66, 0x46, 0x3d, 0xea, 0x7b, 0xfb, 0x9e, 0x99, 0x89, 0xbc, 0x8d, 0x97, 0x3a, 0x34, 0x05, 0x32, 0x5d, 0x1f, 0xc9, 0x1a, 0x8c, 0x36, 0x8c, 0x6f, 0x66, 0xfa, 0xc6, 0xb7, 0x7d, 0xf0, 0x94, 0x04, 0xf0, 0x88, 0xc9, 0xd5, 0x9d, 0x8d, 0x4b, 0x11, 0xd4, 0x9f, 0xbb, 0x25, 0xc5, 0xdc, 0xa2, 0x03, 0x99, 0x4b, 0xbc, 0xf3, 0x0d, 0x97, 0x96, 0x74, 0xe5, 0xf2, 0xb6, 0x80, 0x95, 0xbd, 0x99, 0x15, 0xf5, 0x4b, 0xd2, 0x37, 0x58, 0x46, 0xd4, 0x27, 0xc5, 0xce, 0xc1, 0x7c, 0x30, 0x8e, 0x68, 0x94, 0x7b, 0x9e, 0x6d, 0xe6, 0x7b, 0x9b, 0x5d, 0x3a, 0xd8, 0xdb, 0x32, 0xfa, 0x77, 0x65, 0x15, 0xe4, 0x57, 0xa7, 0x21, 0x55, 0x04, 0x57, 0xef, 0xd8, 0x66, 0x56, 0x38, 0x19, 0x1b, 0xe8, 0xe0, 0x67, 0x98, 0xc7, 0x1a, 0x1c, 0xde, 0x71, 0x71, 0x79, 0x2c, 0xf2, 0xfa, 0x8c, 0x48, 0xec, 0xb5, 0x24, 0x9a, 0x0c, 0xce, 0x75, 0x29, 0xae, 0x8c, 0x67, 0xd4, 0xb5, 0x0b, 0x4b, 0x04, 0x05, 0xef, 0x2e, 0x66, 0x8e, 0x18, 0x08, 0x15, 0xdd, 0x8f, 0x11, 0xb0, 0xeb, 0x4c, 0x04, 0x5b, 0x21, 0x2a, 0x7d, 0x41, 0xe4, 0x4f, 0xcb, 0xcb, 0x5d, 0x12, 0x45, 0xb8, 0xb7, 0x53, 0x71, 0xaa, 0x9f, 0x86, 0x5b, 0xd6, 0x50, 0x4a, 0xed, 0xba, 0x46, 0x77, 0x00, 0x13, 0xd4, 0x8c, 0x85, 0xd3, 0x12, 0x6d, 0xeb, 0x1a, 0x67, 0x95, 0xd9, 0x39, 0x39, 0x50, 0xac, 0xff, 0x00, 0x6f, 0xc4, 0xff, 0x00, 0x1c, 0x81, 0x92, 0xb2, 0x6b, 0x6d, 0x02, 0xdd, 0xbd, 0x36, 0x92, 0x36, 0x2d, 0x1f, 0xc0, 0x2a, 0x0b, 0x28, 0x1b, 0x91, 0x41, 0xf4, 0x9c, 0xb6, 0x25, 0x81, 0x46, 0xfe, 0x81, 0xb5, 0xad, 0x3d, 0xba, 0x57, 0xb7, 0xf9, 0xf6, 0xc9, 0xb0, 0x7f, 0xff, 0xd2, 0xf0, 0xe2, 0x86, 0x95, 0xc4, 0x67, 0x7e, 0x3f, 0x11, 0xf7, 0xa8, 0x19, 0x06, 0x69, 0x8d, 0xca, 0xca, 0x24, 0x8f, 0xd3, 0x52, 0x24, 0x89, 0x47, 0x25, 0x1f, 0xcb, 0x20, 0xf8, 0xb2, 0xb2, 0x76, 0x6e, 0x88, 0x36, 0xf6, 0x6f, 0x2a, 0xc1, 0x6e, 0xfa, 0x45, 0xad, 0xbc, 0x3f, 0x0b, 0x46, 0x81, 0x4d, 0x46, 0xea, 0x7a, 0x9a, 0x83, 0x9a, 0xa9, 0xdd, 0xbb, 0xec, 0x7b, 0x06, 0x5b, 0xe5, 0xcf, 0x2e, 0x69, 0xfa, 0x5c, 0xcd, 0x7b, 0x14, 0x5e, 0xa5, 0xee, 0xf5, 0xb8, 0x7d, 0xdd, 0x99, 0xba, 0xef, 0x91, 0x16, 0x5b, 0x36, 0xb6, 0x65, 0x0d, 0xac, 0xb2, 0x5b, 0xed, 0x34, 0x81, 0x7a, 0xbb, 0x46, 0x40, 0x6a, 0x9e, 0xb4, 0x39, 0x31, 0x13, 0x49, 0xda, 0xd2, 0x9b, 0xed, 0x1e, 0xc4, 0x24, 0xb3, 0x35, 0xb2, 0x88, 0x60, 0x06, 0xe6, 0x56, 0x98, 0x96, 0x79, 0x1e, 0x31, 0x51, 0xc9, 0x8f, 0xcb, 0x00, 0xe6, 0xb3, 0xe4, 0xf9, 0x2b, 0xcc, 0x7a, 0x94, 0xda, 0x96, 0xa9, 0x71, 0x77, 0x70, 0x79, 0xcd, 0x33, 0x97, 0x76, 0x3f, 0xcc, 0xc6, 0xa6, 0x9f, 0x2e, 0x99, 0xb9, 0xc6, 0x2a, 0x21, 0xe6, 0x73, 0xca, 0xe6, 0x4a, 0x51, 0x1a, 0x99, 0x1c, 0x28, 0x04, 0x93, 0xd0, 0x0e, 0xa4, 0xe4, 0xda, 0x5f, 0x50, 0xfe, 0x4a, 0xfe, 0x48, 0xb5, 0xb2, 0xc1, 0xe6, 0x1f, 0x31, 0x7e, 0xef, 0x52, 0x91, 0x43, 0xc3, 0x6e, 0x77, 0xf4, 0x22, 0x6d, 0xbf, 0xe4, 0x63, 0x0e, 0xbf, 0xca, 0x36, 0xeb, 0x5c, 0x84, 0xa5, 0x48, 0x7d, 0x3b, 0x61, 0xa1, 0xdb, 0x5b, 0x2c, 0x71, 0xda, 0x45, 0xc4, 0x28, 0x00, 0x81, 0xdb, 0x31, 0xc9, 0xb4, 0xb2, 0x3b, 0x5d, 0x27, 0xa5, 0x05, 0x1b, 0xc7, 0xdb, 0x10, 0xa9, 0xbd, 0xa6, 0x93, 0x0c, 0x75, 0xe4, 0x39, 0x35, 0x41, 0x3d, 0xc5, 0x06, 0xdb, 0x8e, 0xfd, 0x46, 0x5b, 0x1d, 0x98, 0x95, 0x4f, 0x46, 0xdb, 0xd5, 0xfb, 0x29, 0x5e, 0x9d, 0x0d, 0x32, 0xeb, 0x61, 0x4f, 0xff, 0xd3, 0xf1, 0x46, 0x9a, 0x16, 0x1b, 0x91, 0x71, 0x28, 0xac, 0x4a, 0x14, 0x30, 0x3e, 0x19, 0x54, 0xb9, 0x36, 0xc7, 0x9b, 0x2d, 0xd1, 0x6c, 0x45, 0xe3, 0xdc, 0xde, 0xc8, 0x95, 0x5b, 0x87, 0xf8, 0x41, 0x1d, 0x10, 0x54, 0x01, 0x98, 0x79, 0x25, 0xd1, 0xda, 0xe9, 0xe1, 0xb5, 0x9e, 0xac, 0xeb, 0x42, 0xba, 0x8e, 0xdf, 0x8c, 0x31, 0x21, 0x70, 0xb4, 0x5d, 0xbe, 0xc5, 0x7c, 0x2b, 0xed, 0xe1, 0x94, 0x18, 0xb9, 0x51, 0x3d, 0x03, 0x2c, 0x13, 0x6b, 0xf1, 0x42, 0x6e, 0xe2, 0xb7, 0x12, 0xa0, 0xdd, 0x50, 0x9f, 0x4f, 0x6f, 0xa7, 0x6f, 0xc7, 0x03, 0x61, 0xa0, 0x83, 0xb5, 0xf3, 0x97, 0x98, 0x20, 0x9c, 0x44, 0xea, 0xd0, 0xad, 0x48, 0x64, 0x90, 0x21, 0xd8, 0x9f, 0xa7, 0xa6, 0x44, 0xca, 0x99, 0xc6, 0x36, 0xcb, 0x74, 0x5d, 0x7e, 0x5b, 0xfe, 0x31, 0x6a, 0x31, 0xf3, 0x8c, 0xd0, 0xad, 0x40, 0xa3, 0x1f, 0x7c, 0x44, 0xd6, 0x51, 0xd9, 0xe0, 0x5f, 0x9a, 0x7e, 0x41, 0x9f, 0x40, 0xf3, 0x14, 0xba, 0x85, 0xba, 0x34, 0xba, 0x2d, 0xfb, 0x34, 0xd0, 0xcf, 0x4f, 0xb0, 0xce, 0x6a, 0x51, 0xe9, 0xb0, 0x20, 0xf4, 0xf1, 0x19, 0xb2, 0xc3, 0x90, 0x11, 0x4e, 0x97, 0x55, 0x80, 0x83, 0xc4, 0x17, 0x7e, 0x4c, 0x79, 0x19, 0xfc, 0xd1, 0xe7, 0x78, 0x4b, 0x91, 0x1d, 0xae, 0x92, 0xa6, 0xf6, 0x46, 0x75, 0xe4, 0xad, 0x22, 0x1f, 0xdd, 0xa1, 0x07, 0xb3, 0x1e, 0xfe, 0xd9, 0x92, 0xeb, 0x4b, 0xed, 0xfd, 0x0a, 0xc2, 0x63, 0x27, 0xa4, 0x88, 0x17, 0x60, 0x49, 0x35, 0xdc, 0x8e, 0xa5, 0x7d, 0xab, 0xd3, 0x28, 0x90, 0x50, 0xcd, 0xed, 0x2d, 0xda, 0x15, 0x55, 0x51, 0xf1, 0x1a, 0x0a, 0xf7, 0x39, 0x5d, 0xaa, 0x77, 0x6f, 0x01, 0x8e, 0xa7, 0x7d, 0xfa, 0xff, 0x00, 0x66, 0x10, 0xa8, 0xb8, 0x63, 0x76, 0x90, 0xa8, 0x20, 0x06, 0x56, 0xdb, 0x61, 0xda, 0xbd, 0x4f, 0xcb, 0x24, 0x15, 0x0f, 0xf5, 0x66, 0xe5, 0x5f, 0x4c, 0x53, 0xc3, 0xb7, 0xce, 0x99, 0x6b, 0x17, 0xff, 0xd4, 0xf0, 0xec, 0x57, 0x6f, 0x32, 0xa5, 0xa4, 0x43, 0x76, 0x75, 0xa9, 0xf1, 0x03, 0xfa, 0x64, 0x08, 0x6c, 0x8e, 0xfb, 0x3d, 0x7f, 0xcb, 0x16, 0x2b, 0x3d, 0xbc, 0x16, 0xa3, 0x66, 0x6d, 0x98, 0xfb, 0x1e, 0xb9, 0xac, 0xc8, 0x77, 0xb7, 0x7d, 0x01, 0xb3, 0x37, 0xb8, 0xd3, 0x46, 0x95, 0x68, 0x86, 0xd2, 0x2e, 0x4e, 0xab, 0xf0, 0x23, 0x11, 0x4e, 0x5f, 0xcd, 0x98, 0xe7, 0x25, 0x96, 0x71, 0x83, 0x0f, 0xd6, 0x3c, 0xb9, 0xe7, 0x0d, 0x7c, 0x41, 0x22, 0x5e, 0xb3, 0x20, 0x0c, 0x65, 0x80, 0xc8, 0x63, 0x8e, 0xbb, 0x95, 0xa5, 0x07, 0xeb, 0xcc, 0xac, 0x73, 0x83, 0x4e, 0x5c, 0x59, 0x09, 0xd8, 0xec, 0xc8, 0x57, 0x41, 0xd3, 0x4e, 0x95, 0xa5, 0x5b, 0x4b, 0x6a, 0xcb, 0xab, 0x43, 0x10, 0x4b, 0xeb, 0x85, 0xa2, 0x2c, 0x8e, 0x3f, 0x68, 0x54, 0xf5, 0x00, 0xd3, 0x97, 0x7a, 0x65, 0x79, 0xa6, 0x24, 0x76, 0x6f, 0xd3, 0x62, 0x96, 0x30, 0x78, 0xcb, 0x21, 0xf2, 0xf4, 0x22, 0xce, 0x54, 0x8e, 0x46, 0x26, 0x10, 0x7e, 0x0a, 0xf5, 0xd8, 0xf5, 0x1f, 0x31, 0x98, 0x83, 0x73, 0xb3, 0x91, 0xcd, 0x67, 0xe6, 0x7d, 0xe8, 0x16, 0x69, 0x6f, 0x10, 0x1f, 0x54, 0x9a, 0x37, 0xf5, 0x41, 0x5e, 0x7f, 0x0a, 0x29, 0x62, 0x02, 0xf8, 0x9c, 0xc8, 0x8c, 0x77, 0x6a, 0x99, 0xa0, 0x89, 0xff, 0x00, 0x9c, 0x74, 0xd2, 0xed, 0xed, 0xfc, 0xbb, 0x7b, 0xaa, 0x9a, 0x7d, 0x62, 0xfe, 0x46, 0x2d, 0xfe, 0x4c, 0x51, 0x31, 0x11, 0xa9, 0xf6, 0xef, 0x9b, 0x30, 0x5e, 0x7b, 0x38, 0xdd, 0xf4, 0x7f, 0x95, 0x94, 0xbc, 0x12, 0x43, 0x30, 0x6a, 0xb2, 0xf3, 0x86, 0x40, 0x3e, 0xcb, 0xd7, 0x6a, 0xd7, 0xb1, 0xe9, 0x8f, 0x37, 0x19, 0x97, 0x41, 0x2c, 0x71, 0x20, 0xf5, 0x36, 0x9c, 0x55, 0x78, 0x1d, 0x8a, 0x91, 0xd7, 0x11, 0x14, 0x5a, 0x3e, 0x19, 0x03, 0x10, 0x6b, 0xca, 0xbd, 0x86, 0xf8, 0x9d, 0x95, 0x18, 0x36, 0x65, 0x2e, 0xbc, 0x54, 0x1f, 0xa2, 0x99, 0x00, 0x59, 0x2a, 0x6f, 0x5e, 0x55, 0x15, 0xe9, 0x5f, 0xc3, 0x2f, 0xb6, 0x14, 0xff, 0x00, 0xff, 0xd5, 0xf1, 0x95, 0xfe, 0x80, 0x74, 0x0d, 0x7c, 0xd9, 0x89, 0x3d, 0x78, 0x57, 0x8b, 0xc5, 0x28, 0xe8, 0x55, 0xf7, 0x1f, 0x48, 0xca, 0x38, 0xb8, 0x83, 0x9f, 0x93, 0x07, 0x85, 0x3a, 0x7a, 0x6f, 0x95, 0x66, 0x2b, 0x2c, 0x4c, 0x0d, 0x14, 0x00, 0x3e, 0x9c, 0xc3, 0x98, 0x76, 0xb8, 0x45, 0xbd, 0x02, 0xde, 0x48, 0xee, 0xdc, 0xa0, 0x15, 0xe2, 0x2b, 0xc8, 0x8a, 0x8a, 0xfd, 0x3b, 0x66, 0x3f, 0x00, 0x73, 0x84, 0x2d, 0x36, 0xb5, 0xb5, 0x9e, 0x35, 0x1c, 0x29, 0xc4, 0xfe, 0xc8, 0x04, 0x7f, 0xc4, 0x69, 0x91, 0xe1, 0x67, 0x2c, 0x4a, 0xd2, 0xe9, 0x4e, 0xe3, 0xd4, 0xf4, 0x81, 0x5a, 0x12, 0xc5, 0x41, 0x3f, 0x79, 0x38, 0x9b, 0x60, 0x20, 0x07, 0x34, 0xb0, 0xc9, 0x03, 0x5c, 0x23, 0x03, 0x53, 0x13, 0x56, 0x88, 0xdf, 0x09, 0xda, 0x9b, 0xd3, 0xb6, 0x52, 0x0e, 0xec, 0xe4, 0x29, 0x24, 0xfc, 0xd0, 0xe7, 0x75, 0xe5, 0x57, 0x6b, 0x61, 0xfb, 0xf0, 0xca, 0xaa, 0x57, 0xa8, 0xe6, 0x78, 0x1a, 0x7d, 0xf9, 0x95, 0x8a, 0x5e, 0xa0, 0xe3, 0x67, 0x8f, 0xa0, 0xbd, 0x5b, 0xf2, 0xdf, 0x4a, 0x82, 0xcb, 0x4a, 0xb3, 0xb0, 0xb4, 0x41, 0x0a, 0x70, 0x48, 0xd9, 0x57, 0x60, 0x51, 0x3a, 0x8f, 0xbc, 0xe6, 0x7b, 0xcb, 0xe4, 0x3b, 0xa7, 0x3f, 0x9b, 0x9f, 0x9a, 0xba, 0x77, 0xe5, 0x5f, 0x95, 0x9c, 0x59, 0x94, 0x9f, 0xcd, 0x37, 0x8c, 0xa9, 0xa6, 0xd9, 0x39, 0xaa, 0xd0, 0x7d, 0xa9, 0x1c, 0x03, 0x5e, 0x09, 0xff, 0x00, 0x0c, 0x76, 0xcb, 0x62, 0x2d, 0xa5, 0xf2, 0x85, 0xbf, 0xe7, 0x87, 0xe6, 0xa3, 0x5e, 0x4d, 0xa8, 0xc9, 0xe6, 0x8b, 0xd5, 0x69, 0x5c, 0xb0, 0x4a, 0xab, 0xc4, 0xb5, 0x35, 0x0a, 0xaa, 0xea, 0x40, 0x03, 0xa0, 0xf6, 0xcb, 0x40, 0x4d, 0x3e, 0xdb, 0xff, 0x00, 0x9c, 0x7f, 0xfc, 0xce, 0x4f, 0xcc, 0xbf, 0x26, 0x25, 0xe5, 0xd3, 0x2f, 0xe9, 0xdd, 0x3d, 0xfe, 0xab, 0xa9, 0xaa, 0xd2, 0xa6, 0x40, 0x2a, 0xb2, 0x71, 0x00, 0x01, 0xea, 0x0d, 0xe8, 0x3a, 0x64, 0x25, 0x16, 0x1c, 0x8b, 0xd9, 0x51, 0x39, 0x28, 0x12, 0x51, 0x41, 0xfd, 0xa3, 0xd2, 0xb9, 0x4f, 0x0d, 0x33, 0xb5, 0xf4, 0x87, 0x9d, 0x79, 0x0e, 0xb4, 0xaf, 0x6a, 0xf8, 0xf1, 0xf0, 0xc9, 0xda, 0xbf, 0xff, 0xd6, 0xf2, 0xc6, 0xb5, 0x68, 0x64, 0xd0, 0x6d, 0x35, 0x20, 0x39, 0xcd, 0x13, 0x0f, 0x5e, 0x61, 0xfc, 0x8f, 0x40, 0x8b, 0x5e, 0xe0, 0x66, 0x1c, 0x4f, 0xaa, 0x9d, 0xe6, 0xa6, 0x1e, 0x91, 0x2e, 0xa9, 0x87, 0x95, 0xee, 0x9c, 0xc5, 0x55, 0x34, 0x60, 0x40, 0xae, 0x57, 0x30, 0xd9, 0xa7, 0x95, 0xbd, 0x6f, 0xcb, 0x26, 0x39, 0x40, 0x0d, 0x4e, 0xc0, 0x9f, 0x9e, 0x50, 0x5d, 0xac, 0x79, 0x33, 0x8b, 0xbb, 0x9b, 0x3b, 0x6b, 0x35, 0x48, 0x54, 0x09, 0x29, 0x56, 0x7f, 0xe1, 0x86, 0x72, 0x00, 0x2c, 0x6e, 0xf7, 0x63, 0x3e, 0x63, 0xbd, 0xbd, 0x5d, 0x20, 0x2a, 0xb3, 0xa4, 0x33, 0x48, 0xab, 0x21, 0x43, 0xf1, 0x2c, 0x47, 0xed, 0x1d, 0xbc, 0x73, 0x18, 0x9b, 0x64, 0x28, 0x96, 0x3a, 0xc7, 0x49, 0xb0, 0xf4, 0xcc, 0xe9, 0x73, 0x6c, 0xb4, 0xf8, 0x67, 0x92, 0x32, 0x21, 0x70, 0x7b, 0x89, 0x05, 0x57, 0xef, 0x38, 0x28, 0x94, 0x4a, 0x7d, 0x13, 0x7d, 0x6a, 0xd3, 0x4c, 0xb8, 0xf2, 0xc3, 0xc8, 0x2e, 0x03, 0xf3, 0xe2, 0x7d, 0x33, 0xb7, 0xc5, 0xcc, 0x71, 0x03, 0xc6, 0xb9, 0x64, 0x06, 0xe2, 0x9a, 0xf2, 0x4f, 0xd2, 0x6d, 0xe9, 0xfe, 0x41, 0x45, 0x5b, 0x18, 0x66, 0xa5, 0x64, 0x09, 0xf4, 0xd5, 0xb7, 0xcd, 0x93, 0xc7, 0xcf, 0x9b, 0xe5, 0x6f, 0xf9, 0xc8, 0x0d, 0x56, 0xeb, 0x59, 0xfc, 0xce, 0xd5, 0x12, 0x61, 0xc4, 0x69, 0xe9, 0x0d, 0xa4, 0x4b, 0xfe, 0x48, 0x40, 0xd5, 0x3e, 0xe4, 0xb6, 0x64, 0x8e, 0x4c, 0x02, 0x61, 0x65, 0xa0, 0x14, 0xb4, 0xb6, 0xb0, 0xb1, 0xb6, 0xb2, 0x97, 0xcb, 0xf1, 0x5a, 0x2d, 0xc6, 0xa5, 0xac, 0xb4, 0x70, 0x5d, 0xc7, 0x3d, 0xc1, 0x51, 0x24, 0x91, 0xc9, 0x31, 0x75, 0x6b, 0x70, 0x9f, 0x14, 0x68, 0x01, 0x46, 0xe4, 0xb5, 0xa3, 0x17, 0xcb, 0x40, 0x61, 0x6f, 0x47, 0xff, 0x00, 0x9c, 0x3a, 0x8f, 0x5b, 0x4f, 0x3c, 0x6b, 0xb7, 0xfa, 0x30, 0x91, 0x3c, 0xa4, 0xb1, 0x95, 0xb9, 0x82, 0x42, 0x0a, 0xbc, 0x8e, 0xe4, 0xdb, 0xa9, 0xef, 0xc9, 0x17, 0x91, 0x24, 0x7c, 0xb2, 0x05, 0x64, 0xfb, 0x75, 0x64, 0x32, 0x39, 0x69, 0x5b, 0x9c, 0xad, 0xb9, 0xdb, 0xa7, 0xb5, 0x3b, 0x53, 0x2a, 0x21, 0x41, 0x44, 0xf3, 0x8b, 0x8f, 0x2e, 0x43, 0x9d, 0x2b, 0xd4, 0x57, 0x23, 0x41, 0x36, 0xff, 0x00, 0xff, 0xd7, 0xf0, 0xc0, 0xd5, 0xb5, 0x11, 0x64, 0xb6, 0x3f, 0x59, 0x90, 0xd9, 0xab, 0x06, 0xf4, 0x79, 0x7c, 0x3b, 0x74, 0xc8, 0x08, 0x8b, 0xb6, 0xe3, 0x96, 0x55, 0x57, 0xb3, 0x3e, 0xf2, 0x35, 0xc7, 0xd6, 0x0b, 0x45, 0x5d, 0xdc, 0x8a, 0x7d, 0xd9, 0x8d, 0x94, 0x3b, 0x3d, 0x1c, 0x9e, 0xc3, 0xe5, 0xc3, 0x2c, 0x7c, 0xc5, 0x0f, 0xee, 0xdb, 0x8b, 0x0c, 0xc4, 0x26, 0x9d, 0xa0, 0x9a, 0x7d, 0x2c, 0xe5, 0xe4, 0x55, 0x7f, 0xee, 0xc1, 0x15, 0x04, 0xd0, 0x12, 0x3c, 0x72, 0x89, 0x1b, 0x2c, 0xcc, 0xa8, 0x2a, 0x8b, 0x87, 0xbb, 0x63, 0x1a, 0x28, 0x65, 0xf0, 0xed, 0xf2, 0xc3, 0xc2, 0x0a, 0x06, 0x4a, 0x46, 0xc7, 0xa5, 0xa3, 0x59, 0xc8, 0xb2, 0xc7, 0x45, 0x22, 0x9c, 0x14, 0x54, 0x10, 0x46, 0xf5, 0x1d, 0x32, 0x5c, 0x14, 0x14, 0xe4, 0x32, 0x2f, 0x3a, 0xf3, 0xb6, 0x90, 0x9a, 0x6d, 0xae, 0x9f, 0x3d, 0xab, 0xb8, 0x8a, 0x3b, 0xf8, 0x39, 0x44, 0x58, 0xf0, 0x08, 0xd5, 0x14, 0xa5, 0x7b, 0x65, 0x98, 0x8e, 0xfb, 0xb5, 0x67, 0x87, 0xa5, 0xef, 0x5e, 0x44, 0x96, 0x35, 0xb5, 0xb6, 0x59, 0x36, 0xfd, 0xd8, 0xa0, 0xf1, 0x20, 0x53, 0x33, 0xc0, 0x79, 0x59, 0x73, 0x7c, 0xd7, 0xf9, 0xfb, 0xa2, 0xcd, 0x67, 0xf9, 0xa7, 0x7b, 0x72, 0xf1, 0x71, 0x83, 0x53, 0x86, 0x0b, 0x98, 0x24, 0x22, 0x8a, 0xcc, 0x88, 0x23, 0x7f, 0xb8, 0xae, 0xf9, 0x7c, 0x50, 0x1e, 0x5f, 0x7c, 0x48, 0x21, 0x44, 0x6b, 0xce, 0x9b, 0xb0, 0x1b, 0x9e, 0xf5, 0xaf, 0x8e, 0x4d, 0x5f, 0x7a, 0x7f, 0xce, 0x34, 0xf9, 0x5d, 0x3c, 0xa3, 0xf9, 0x69, 0x63, 0xa9, 0x3c, 0x27, 0xeb, 0xda, 0xe1, 0x37, 0xd7, 0x2e, 0xaa, 0xdb, 0x06, 0xda, 0x30, 0x49, 0xfe, 0x54, 0x03, 0x03, 0x49, 0xdc, 0xb3, 0xaf, 0x38, 0xfe, 0x6a, 0xf9, 0x47, 0xc9, 0x3a, 0x74, 0x97, 0xfa, 0xf6, 0xaf, 0x15, 0x85, 0xb8, 0x75, 0x89, 0xb8, 0x87, 0x9a, 0x72, 0xee, 0x2a, 0x14, 0x24, 0x60, 0xb1, 0xa8, 0xdf, 0x07, 0x0b, 0x2d, 0xcb, 0xcf, 0x7f, 0xe8, 0x6a, 0xff, 0x00, 0x26, 0xbd, 0x6a, 0x7f, 0x89, 0x2f, 0xf8, 0x52, 0x9e, 0xb7, 0xe8, 0xb9, 0xb8, 0x57, 0xc2, 0x95, 0xe9, 0x8f, 0x08, 0x5a, 0x2f, 0xff, 0xd0, 0xf0, 0x4d, 0x40, 0xaa, 0xd7, 0x00, 0x64, 0xcb, 0x3c, 0x97, 0xa8, 0xb5, 0x9e, 0xa3, 0x1a, 0xd6, 0x84, 0x95, 0x3f, 0x45, 0x72, 0x9c, 0xa2, 0xc3, 0x99, 0xa5, 0x9d, 0x49, 0xf4, 0x17, 0x97, 0xaf, 0x63, 0x17, 0x52, 0x6f, 0xf0, 0xc8, 0x43, 0x6f, 0x9a, 0xe9, 0x07, 0x70, 0x0e, 0xec, 0x83, 0x51, 0x44, 0xb8, 0x61, 0x1a, 0x9e, 0x11, 0xd3, 0x91, 0x60, 0x68, 0x6b, 0xd3, 0x31, 0x4f, 0x36, 0xd3, 0x4c, 0x52, 0xef, 0x4c, 0xd5, 0x0c, 0xc4, 0x69, 0xda, 0x94, 0xc8, 0x3a, 0xf0, 0x66, 0x07, 0x73, 0xe0, 0x40, 0xfd, 0x79, 0x93, 0x12, 0x1c, 0x9c, 0x32, 0xc7, 0xfc, 0x41, 0x33, 0xd2, 0xb4, 0x6f, 0x38, 0x98, 0x65, 0x76, 0xbf, 0x69, 0x42, 0xd0, 0xaa, 0xc9, 0xde, 0x95, 0xad, 0x28, 0x46, 0x4e, 0xac, 0x39, 0x77, 0x80, 0x11, 0xbf, 0xd8, 0xc7, 0x7c, 0xe1, 0xa5, 0xf9, 0x92, 0x4d, 0x32, 0x5b, 0x8b, 0x93, 0x27, 0xa7, 0x68, 0x56, 0xe2, 0x45, 0xda, 0x85, 0x61, 0x6e, 0x67, 0xad, 0x6b, 0xb0, 0x38, 0xc2, 0x81, 0xe4, 0xc7, 0x52, 0x31, 0x1c, 0x67, 0x86, 0x5b, 0xbd, 0x37, 0xca, 0x7a, 0x94, 0xb1, 0x69, 0xb6, 0x2e, 0xb7, 0x15, 0x48, 0xc2, 0xb4, 0x52, 0x53, 0xac, 0x32, 0xaf, 0xb1, 0xed, 0x9b, 0x10, 0x36, 0x78, 0x5c, 0x9f, 0x51, 0x64, 0x1f, 0x98, 0x3e, 0x58, 0xb6, 0xfc, 0xc8, 0xf2, 0xe5, 0xbc, 0x68, 0x52, 0x2d, 0x5a, 0xd1, 0x84, 0xb6, 0xf3, 0x95, 0x0e, 0xc0, 0x85, 0xe2, 0xcb, 0xd8, 0xd1, 0xbb, 0xe4, 0xc1, 0xa6, 0x97, 0xce, 0x17, 0x5f, 0x95, 0xde, 0x6d, 0xb6, 0xbe, 0xb7, 0x69, 0x34, 0xf3, 0x3c, 0x72, 0xcf, 0xe8, 0xa3, 0x45, 0x49, 0x95, 0x4a, 0x90, 0x3e, 0x35, 0x5a, 0x95, 0x1d, 0xfe, 0x21, 0x93, 0x4d, 0xbe, 0xd2, 0xd2, 0xf5, 0x8b, 0xbd, 0x32, 0x2d, 0x3f, 0x4c, 0x9a, 0xe4, 0xca, 0x9e, 0x90, 0x85, 0x65, 0x55, 0x08, 0x85, 0x91, 0x01, 0x3b, 0x0a, 0x05, 0xe9, 0xb0, 0xc0, 0x5a, 0xc3, 0xcd, 0x3f, 0x3b, 0x7f, 0x26, 0xec, 0xff, 0x00, 0x35, 0x6d, 0x6d, 0xb5, 0x3d, 0x16, 0xfe, 0x0d, 0x3b, 0xcd, 0x96, 0x01, 0x92, 0x46, 0x9e, 0xa2, 0x0b, 0xc8, 0xb7, 0x28, 0x92, 0x71, 0xfb, 0x2e, 0xa7, 0xec, 0x3d, 0x0f, 0xc2, 0x68, 0x71, 0x05, 0x95, 0xd3, 0xe7, 0x9f, 0xfa, 0x16, 0x2f, 0xcd, 0x7f, 0x43, 0xd6, 0xfa, 0xa5, 0x97, 0xab, 0xeb, 0x7a, 0x5f, 0x55, 0xfa, 0xec, 0x5e, 0xaf, 0x0f, 0xf7, 0xed, 0x2b, 0x4e, 0x15, 0xff, 0x00, 0x65, 0xdf, 0x8e, 0x14, 0xf1, 0xbf, 0xff, 0xd1, 0xf0, 0x5a, 0xa7, 0x18, 0x5e, 0x56, 0x1f, 0x68, 0x71, 0x5f, 0xa7, 0xbe, 0x2a, 0x98, 0xdb, 0xfa, 0x90, 0x24, 0x37, 0xb0, 0xfd, 0xb8, 0xa8, 0x58, 0x78, 0xae, 0x43, 0xc9, 0xb4, 0x6d, 0xbb, 0xda, 0x3c, 0xa1, 0xad, 0x43, 0xa8, 0xda, 0xc5, 0x2a, 0x3d, 0x26, 0x5a, 0x02, 0x2b, 0xbe, 0x60, 0x64, 0x8d, 0x17, 0x6f, 0x8b, 0x20, 0x90, 0x7a, 0x3c, 0x32, 0x8b, 0xa8, 0x02, 0xf3, 0xfd, 0xe0, 0x1b, 0x11, 0x98, 0x66, 0x3b, 0xb9, 0x62, 0x54, 0x83, 0x36, 0xf2, 0xa4, 0xe4, 0x29, 0x34, 0xeb, 0xc8, 0x74, 0xae, 0x0d, 0xc3, 0x65, 0x82, 0x13, 0x6b, 0x57, 0xba, 0x54, 0xe4, 0x8c, 0x41, 0x1b, 0x75, 0xa7, 0xe0, 0x72, 0x5c, 0x4c, 0x84, 0x50, 0x5a, 0xb3, 0xdd, 0xdd, 0xc3, 0x24, 0x33, 0xb1, 0x60, 0xe0, 0x86, 0x52, 0x45, 0x38, 0xd2, 0x87, 0x24, 0x26, 0x6d, 0x8c, 0xe1, 0x41, 0x25, 0xfc, 0xa3, 0xd7, 0x2f, 0x6f, 0x3c, 0xbf, 0x73, 0xa5, 0xb2, 0x2c, 0xd1, 0x69, 0x17, 0x2f, 0x6b, 0x14, 0x8c, 0x0f, 0x21, 0x0d, 0x79, 0x46, 0x09, 0x15, 0xed, 0xb7, 0x4e, 0xd9, 0xb9, 0x8b, 0xcb, 0xe4, 0xa2, 0x5e, 0xa3, 0xa6, 0xdf, 0x6a, 0x36, 0xe4, 0xcd, 0x69, 0x1c, 0x4e, 0x84, 0x7c, 0x76, 0xab, 0x21, 0x67, 0xa8, 0xa7, 0xd9, 0xf8, 0x4d, 0x2b, 0xf3, 0xc3, 0x4d, 0x49, 0x57, 0x98, 0x75, 0x6f, 0x31, 0xda, 0xf9, 0xa3, 0x4b, 0xfd, 0x1f, 0x69, 0x1d, 0xae, 0xa1, 0xa9, 0x7e, 0xee, 0xe6, 0xd2, 0x79, 0x18, 0xf3, 0xb5, 0x1f, 0xee, 0xd9, 0x0a, 0x01, 0x4e, 0x3f, 0xb3, 0x4d, 0xf2, 0x9c, 0xb9, 0x04, 0x05, 0xb7, 0xe2, 0x87, 0x1e, 0xdd, 0x19, 0x3e, 0xaf, 0x6b, 0xae, 0xcb, 0x6d, 0x13, 0x0d, 0x45, 0xa2, 0x8e, 0x06, 0xe5, 0x13, 0x2a, 0x02, 0x01, 0x5e, 0x82, 0xb5, 0x04, 0xe6, 0x11, 0xd4, 0xcd, 0xda, 0x43, 0x49, 0x8e, 0xb7, 0xdc, 0xb1, 0x51, 0xe6, 0x4d, 0x76, 0xd2, 0x61, 0x15, 0xaa, 0x4b, 0xa8, 0xc9, 0x6e, 0x49, 0x79, 0x20, 0xe6, 0x8c, 0x49, 0xad, 0x43, 0x16, 0xe4, 0xa7, 0xaf, 0x43, 0xd3, 0x26, 0x35, 0x75, 0xcd, 0xa8, 0xe8, 0x87, 0x46, 0xbf, 0xc7, 0x9a, 0xff, 0x00, 0xd6, 0xbf, 0x48, 0xfe, 0x88, 0xfd, 0xe7, 0x0f, 0xab, 0xfa, 0x3f, 0x58, 0x7f, 0x5f, 0x8d, 0x3f, 0x9f, 0xa7, 0x5e, 0xd4, 0xc3, 0xf9, 0xd1, 0x7c, 0xb6, 0x47, 0xe4, 0x3a, 0x5b, 0xff, 0xd2, 0xf0, 0xb7, 0xa6, 0x1e, 0xdf, 0xd3, 0xf6, 0xa5, 0x71, 0x54, 0xdb, 0x4b, 0x80, 0x3c, 0x42, 0x26, 0xee, 0x29, 0xbe, 0x51, 0x23, 0x4e, 0x44, 0x05, 0x84, 0x45, 0xa5, 0xd5, 0xf7, 0x97, 0x2e, 0xfd, 0x6b, 0x6a, 0x98, 0x09, 0xab, 0xc7, 0xfc, 0x46, 0x3b, 0x4c, 0x26, 0x32, 0x30, 0x3e, 0x4f, 0x49, 0xd0, 0xfc, 0xfb, 0x05, 0xd4, 0x4a, 0x7d, 0x40, 0xac, 0x3a, 0x8e, 0x84, 0x1c, 0xc5, 0x96, 0x2a, 0x73, 0xe1, 0x9c, 0x16, 0x6d, 0xa5, 0x79, 0x86, 0xd6, 0xec, 0x80, 0x5a, 0xa0, 0xf5, 0xca, 0xcc, 0x5c, 0xa1, 0x2b, 0x1b, 0x26, 0x30, 0x6a, 0x31, 0x46, 0xcf, 0x1c, 0x87, 0x94, 0x64, 0x9e, 0x3d, 0xb6, 0xf0, 0xca, 0xa8, 0x39, 0x51, 0x99, 0x42, 0x6b, 0x1a, 0xc5, 0xa5, 0xa5, 0x94, 0xf7, 0x92, 0xc8, 0xaa, 0xb1, 0x23, 0x30, 0x04, 0xf8, 0x0e, 0x9f, 0x4e, 0x4a, 0x11, 0xb2, 0xd5, 0x9b, 0x25, 0x06, 0x1b, 0xff, 0x00, 0x38, 0xfd, 0xad, 0xdf, 0xda, 0xf9, 0xa2, 0xfe, 0xc5, 0x42, 0xbe, 0x9b, 0x7f, 0x0b, 0xdd, 0xdd, 0x07, 0xaf, 0x14, 0x68, 0xd8, 0x71, 0x6d, 0xbb, 0x90, 0xfc, 0x73, 0x6e, 0xf2, 0xf2, 0xdd, 0xf4, 0xad, 0xa6, 0xab, 0x6d, 0x69, 0x14, 0xfa, 0xee, 0xa0, 0xe2, 0x0b, 0x0d, 0x39, 0x19, 0xfe, 0x11, 0xc5, 0x1a, 0x4a, 0x1d, 0x8f, 0x73, 0x4f, 0xf8, 0x96, 0x0b, 0x40, 0x8d, 0xec, 0xf3, 0x6d, 0x3f, 0x52, 0xba, 0xd6, 0x35, 0x8b, 0xbf, 0x36, 0x6a, 0x5f, 0x0d, 0xc5, 0xdc, 0xa8, 0xb6, 0xa8, 0x7a, 0xc5, 0x6c, 0x9b, 0x22, 0x0f, 0xa3, 0x73, 0x9a, 0xbc, 0xb3, 0xe2, 0x36, 0xed, 0xb1, 0x43, 0x80, 0x53, 0xd0, 0xa7, 0xd4, 0x44, 0xfa, 0x7a, 0xda, 0x83, 0xbd, 0x3e, 0x2f, 0xa7, 0x2b, 0xad, 0x9b, 0xb8, 0x8d, 0xa8, 0xe8, 0x91, 0xdb, 0xfa, 0x2d, 0x6f, 0xc3, 0x8a, 0x2d, 0x56, 0xa3, 0xad, 0x4f, 0x5c, 0xa4, 0x0d, 0xdc, 0xa3, 0xca, 0xd0, 0xbf, 0xa1, 0xe3, 0xfa, 0xe7, 0x0f, 0xf2, 0xb9, 0x57, 0xbf, 0x1a, 0xe4, 0xb8, 0x57, 0xc5, 0xdd, 0xff, 0xd3, 0xf0, 0xcc, 0x5d, 0x7b, 0x70, 0xc5, 0x53, 0x6d, 0x2f, 0xd5, 0xe4, 0x69, 0xfd, 0xdf, 0xec, 0xd7, 0xad, 0x7d, 0xb2, 0x8c, 0x8d, 0xd8, 0xed, 0x91, 0x9f, 0x43, 0xea, 0xe7, 0xeb, 0x94, 0xad, 0x3e, 0x1e, 0x95, 0xfc, 0x72, 0x81, 0x7d, 0x1c, 0x9d, 0xba, 0xb1, 0x7b, 0xdf, 0xa9, 0x7a, 0xdf, 0xee, 0x2f, 0xd4, 0xfa, 0xe7, 0xed, 0x7a, 0x7f, 0xdd, 0xff, 0x00, 0xb2, 0xae, 0x64, 0x0b, 0xea, 0xe3, 0x9a, 0xbf, 0x4a, 0x6f, 0xa4, 0xff, 0x00, 0x89, 0xbd, 0x45, 0xfa, 0xb5, 0x79, 0xf7, 0xeb, 0xc7, 0xe9, 0xae, 0x57, 0x2e, 0x17, 0x23, 0x1f, 0x89, 0xd1, 0x99, 0x8f, 0xf1, 0xa7, 0x11, 0xcf, 0xd3, 0xf5, 0x29, 0xb5, 0x6b, 0xd3, 0xe8, 0xcc, 0x7f, 0x45, 0xb9, 0xa3, 0xc5, 0x62, 0xbe, 0x68, 0xff, 0x00, 0x15, 0xfd, 0x4c, 0xfe, 0x90, 0xaf, 0xd4, 0xab, 0xf1, 0x7a, 0x7f, 0x62, 0x9d, 0xab, 0xdf, 0x32, 0xb1, 0x70, 0x5e, 0xdc, 0xdc, 0x2d, 0x47, 0x8b, 0x5e, 0xae, 0x4c, 0xbf, 0xf2, 0x37, 0x9f, 0x3d, 0x5b, 0xd2, 0xff, 0x00, 0x8e, 0x87, 0xee, 0x29, 0x5a, 0xf2, 0xf4, 0xaa, 0xd4, 0xa5, 0x36, 0xa7, 0x3a, 0x57, 0xfd, 0x8e, 0x64, 0x3a, 0xf2, 0xf6, 0xbf, 0xcc, 0x7f, 0x5b, 0xfc, 0x23, 0xa7, 0xfe, 0x8e, 0xff, 0x00, 0x8e, 0x37, 0xd6, 0x63, 0xfa, 0xe5, 0x2b, 0xcb, 0x87, 0xec, 0xd6, 0xbd, 0xb9, 0x7d, 0xac, 0xc7, 0xcd, 0x7c, 0x2d, 0xf8, 0x2b, 0x89, 0x26, 0x8f, 0xd4, 0xfa, 0x94, 0x3e, 0x85, 0x29, 0xc9, 0x69, 0xfc, 0x33, 0x58, 0x5d, 0x9c, 0x79, 0xb2, 0xbb, 0x0f, 0xac, 0x7a, 0x2b, 0xea, 0x75, 0xef, 0x92, 0x0c, 0x53, 0x3d, 0x2f, 0xd4, 0xfa, 0xbb, 0xfa, 0x74, 0xf5, 0x39, 0x9a, 0xd7, 0xe7, 0x80, 0x53, 0x79, 0xba, 0x5b, 0xfe, 0x97, 0xfa, 0x4b, 0xfc, 0xba, 0x7f, 0xb1, 0xc7, 0xab, 0x1e, 0x8f, 0xff, 0xd9 +}; diff --git a/talk/base/testclient.cc b/talk/base/testclient.cc new file mode 100644 index 000000000..0e7625f73 --- /dev/null +++ b/talk/base/testclient.cc @@ -0,0 +1,155 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/testclient.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" + +namespace talk_base { + +// DESIGN: Each packet received is put it into a list of packets. +// Callers can retrieve received packets from any thread by calling +// NextPacket. + +TestClient::TestClient(AsyncPacketSocket* socket) + : socket_(socket), ready_to_send_(false) { + packets_ = new std::vector(); + socket_->SignalReadPacket.connect(this, &TestClient::OnPacket); + socket_->SignalReadyToSend.connect(this, &TestClient::OnReadyToSend); +} + +TestClient::~TestClient() { + delete socket_; + for (unsigned i = 0; i < packets_->size(); i++) + delete (*packets_)[i]; + delete packets_; +} + +bool TestClient::CheckConnState(AsyncPacketSocket::State state) { + // Wait for our timeout value until the socket reaches the desired state. + uint32 end = TimeAfter(kTimeout); + while (socket_->GetState() != state && TimeUntil(end) > 0) + Thread::Current()->ProcessMessages(1); + return (socket_->GetState() == state); +} + +int TestClient::Send(const char* buf, size_t size) { + return socket_->Send(buf, size); +} + +int TestClient::SendTo(const char* buf, size_t size, + const SocketAddress& dest) { + return socket_->SendTo(buf, size, dest); +} + +TestClient::Packet* TestClient::NextPacket() { + // If no packets are currently available, we go into a get/dispatch loop for + // at most 1 second. If, during the loop, a packet arrives, then we can stop + // early and return it. + + // Note that the case where no packet arrives is important. We often want to + // test that a packet does not arrive. + + // Note also that we only try to pump our current thread's message queue. + // Pumping another thread's queue could lead to messages being dispatched from + // the wrong thread to non-thread-safe objects. + + uint32 end = TimeAfter(kTimeout); + while (packets_->size() == 0 && TimeUntil(end) > 0) + Thread::Current()->ProcessMessages(1); + + // Return the first packet placed in the queue. + Packet* packet = NULL; + if (packets_->size() > 0) { + CritScope cs(&crit_); + packet = packets_->front(); + packets_->erase(packets_->begin()); + } + + return packet; +} + +bool TestClient::CheckNextPacket(const char* buf, size_t size, + SocketAddress* addr) { + bool res = false; + Packet* packet = NextPacket(); + if (packet) { + res = (packet->size == size && std::memcmp(packet->buf, buf, size) == 0); + if (addr) + *addr = packet->addr; + delete packet; + } + return res; +} + +bool TestClient::CheckNoPacket() { + bool res; + Packet* packet = NextPacket(); + res = (packet == NULL); + delete packet; + return res; +} + +int TestClient::GetError() { + return socket_->GetError(); +} + +int TestClient::SetOption(Socket::Option opt, int value) { + return socket_->SetOption(opt, value); +} + +bool TestClient::ready_to_send() const { + return ready_to_send_; +} + +void TestClient::OnPacket(AsyncPacketSocket* socket, const char* buf, + size_t size, const SocketAddress& remote_addr) { + CritScope cs(&crit_); + packets_->push_back(new Packet(remote_addr, buf, size)); +} + +void TestClient::OnReadyToSend(AsyncPacketSocket* socket) { + ready_to_send_ = true; +} + +TestClient::Packet::Packet(const SocketAddress& a, const char* b, size_t s) + : addr(a), buf(0), size(s) { + buf = new char[size]; + memcpy(buf, b, size); +} + +TestClient::Packet::Packet(const Packet& p) + : addr(p.addr), buf(0), size(p.size) { + buf = new char[size]; + memcpy(buf, p.buf, size); +} + +TestClient::Packet::~Packet() { + delete[] buf; +} + +} // namespace talk_base diff --git a/talk/base/testclient.h b/talk/base/testclient.h new file mode 100644 index 000000000..1e1780a29 --- /dev/null +++ b/talk/base/testclient.h @@ -0,0 +1,109 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_TESTCLIENT_H_ +#define TALK_BASE_TESTCLIENT_H_ + +#include +#include "talk/base/asyncudpsocket.h" +#include "talk/base/criticalsection.h" + +namespace talk_base { + +// A simple client that can send TCP or UDP data and check that it receives +// what it expects to receive. Useful for testing server functionality. +class TestClient : public sigslot::has_slots<> { + public: + // Records the contents of a packet that was received. + struct Packet { + Packet(const SocketAddress& a, const char* b, size_t s); + Packet(const Packet& p); + virtual ~Packet(); + + SocketAddress addr; + char* buf; + size_t size; + }; + + // Creates a client that will send and receive with the given socket and + // will post itself messages with the given thread. + explicit TestClient(AsyncPacketSocket* socket); + ~TestClient(); + + SocketAddress address() const { return socket_->GetLocalAddress(); } + SocketAddress remote_address() const { return socket_->GetRemoteAddress(); } + + // Checks that the socket moves to the specified connect state. + bool CheckConnState(AsyncPacketSocket::State state); + + // Checks that the socket is connected to the remote side. + bool CheckConnected() { + return CheckConnState(AsyncPacketSocket::STATE_CONNECTED); + } + + // Sends using the clients socket. + int Send(const char* buf, size_t size); + + // Sends using the clients socket to the given destination. + int SendTo(const char* buf, size_t size, const SocketAddress& dest); + + // Returns the next packet received by the client or 0 if none is received + // within a reasonable amount of time. The caller must delete the packet + // when done with it. + Packet* NextPacket(); + + // Checks that the next packet has the given contents. Returns the remote + // address that the packet was sent from. + bool CheckNextPacket(const char* buf, size_t len, SocketAddress* addr); + + // Checks that no packets have arrived or will arrive in the next second. + bool CheckNoPacket(); + + int GetError(); + int SetOption(Socket::Option opt, int value); + + bool ready_to_send() const; + + private: + static const int kTimeout = 1000; + // Workaround for the fact that AsyncPacketSocket::GetConnState doesn't exist. + Socket::ConnState GetState(); + // Slot for packets read on the socket. + void OnPacket(AsyncPacketSocket* socket, const char* buf, size_t len, + const SocketAddress& remote_addr); + void OnReadyToSend(AsyncPacketSocket* socket); + + CriticalSection crit_; + AsyncPacketSocket* socket_; + std::vector* packets_; + bool ready_to_send_; + DISALLOW_EVIL_CONSTRUCTORS(TestClient); +}; + +} // namespace talk_base + +#endif // TALK_BASE_TESTCLIENT_H_ diff --git a/talk/base/testclient_unittest.cc b/talk/base/testclient_unittest.cc new file mode 100644 index 000000000..126923673 --- /dev/null +++ b/talk/base/testclient_unittest.cc @@ -0,0 +1,95 @@ +/* + * libjingle + * Copyright 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/base/gunit.h" +#include "talk/base/host.h" +#include "talk/base/nethelpers.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/testclient.h" +#include "talk/base/testechoserver.h" +#include "talk/base/thread.h" + +using namespace talk_base; + +void TestUdpInternal(const SocketAddress& loopback) { + Thread *main = Thread::Current(); + AsyncSocket* socket = main->socketserver() + ->CreateAsyncSocket(loopback.family(), SOCK_DGRAM); + socket->Bind(loopback); + + TestClient client(new AsyncUDPSocket(socket)); + SocketAddress addr = client.address(), from; + EXPECT_EQ(3, client.SendTo("foo", 3, addr)); + EXPECT_TRUE(client.CheckNextPacket("foo", 3, &from)); + EXPECT_EQ(from, addr); + EXPECT_TRUE(client.CheckNoPacket()); +} + +void TestTcpInternal(const SocketAddress& loopback) { + Thread *main = Thread::Current(); + TestEchoServer server(main, loopback); + + AsyncSocket* socket = main->socketserver() + ->CreateAsyncSocket(loopback.family(), SOCK_STREAM); + AsyncTCPSocket* tcp_socket = AsyncTCPSocket::Create( + socket, loopback, server.address()); + ASSERT_TRUE(tcp_socket != NULL); + + TestClient client(tcp_socket); + SocketAddress addr = client.address(), from; + EXPECT_TRUE(client.CheckConnected()); + EXPECT_EQ(3, client.Send("foo", 3)); + EXPECT_TRUE(client.CheckNextPacket("foo", 3, &from)); + EXPECT_EQ(from, server.address()); + EXPECT_TRUE(client.CheckNoPacket()); +} + +// Tests whether the TestClient can send UDP to itself. +TEST(TestClientTest, TestUdpIPv4) { + TestUdpInternal(SocketAddress("127.0.0.1", 0)); +} + +TEST(TestClientTest, TestUdpIPv6) { + if (HasIPv6Enabled()) { + TestUdpInternal(SocketAddress("::1", 0)); + } else { + LOG(LS_INFO) << "Skipping IPv6 test."; + } +} + +// Tests whether the TestClient can connect to a server and exchange data. +TEST(TestClientTest, TestTcpIPv4) { + TestTcpInternal(SocketAddress("127.0.0.1", 0)); +} + +TEST(TestClientTest, TestTcpIPv6) { + if (HasIPv6Enabled()) { + TestTcpInternal(SocketAddress("::1", 0)); + } else { + LOG(LS_INFO) << "Skipping IPv6 test."; + } +} diff --git a/talk/base/testechoserver.h b/talk/base/testechoserver.h new file mode 100644 index 000000000..9bb5178c0 --- /dev/null +++ b/talk/base/testechoserver.h @@ -0,0 +1,88 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifndef TALK_BASE_TESTECHOSERVER_H_ +#define TALK_BASE_TESTECHOSERVER_H_ + +#include +#include "talk/base/asynctcpsocket.h" +#include "talk/base/socketaddress.h" +#include "talk/base/sigslot.h" +#include "talk/base/thread.h" + +namespace talk_base { + +// A test echo server, echoes back any packets sent to it. +// Useful for unit tests. +class TestEchoServer : public sigslot::has_slots<> { + public: + TestEchoServer(Thread* thread, const SocketAddress& addr) + : server_socket_(thread->socketserver()->CreateAsyncSocket(addr.family(), + SOCK_STREAM)) { + server_socket_->Bind(addr); + server_socket_->Listen(5); + server_socket_->SignalReadEvent.connect(this, &TestEchoServer::OnAccept); + } + ~TestEchoServer() { + for (ClientList::iterator it = client_sockets_.begin(); + it != client_sockets_.end(); ++it) { + delete *it; + } + } + + SocketAddress address() const { return server_socket_->GetLocalAddress(); } + + private: + void OnAccept(AsyncSocket* socket) { + AsyncSocket* raw_socket = socket->Accept(NULL); + if (raw_socket) { + AsyncTCPSocket* packet_socket = new AsyncTCPSocket(raw_socket, false); + packet_socket->SignalReadPacket.connect(this, &TestEchoServer::OnPacket); + packet_socket->SignalClose.connect(this, &TestEchoServer::OnClose); + client_sockets_.push_back(packet_socket); + } + } + void OnPacket(AsyncPacketSocket* socket, const char* buf, size_t size, + const SocketAddress& remote_addr) { + socket->Send(buf, size); + } + void OnClose(AsyncPacketSocket* socket, int err) { + ClientList::iterator it = + std::find(client_sockets_.begin(), client_sockets_.end(), socket); + client_sockets_.erase(it); + Thread::Current()->Dispose(socket); + } + + typedef std::list ClientList; + scoped_ptr server_socket_; + ClientList client_sockets_; + DISALLOW_EVIL_CONSTRUCTORS(TestEchoServer); +}; + +} // namespace talk_base + +#endif // TALK_BASE_TESTECHOSERVER_H_ diff --git a/talk/base/testutils.h b/talk/base/testutils.h new file mode 100644 index 000000000..769d95f64 --- /dev/null +++ b/talk/base/testutils.h @@ -0,0 +1,570 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifndef TALK_BASE_TESTUTILS_H__ +#define TALK_BASE_TESTUTILS_H__ + +// Utilities for testing talk_base infrastructure in unittests + +#include +#include +#include "talk/base/asyncsocket.h" +#include "talk/base/common.h" +#include "talk/base/gunit.h" +#include "talk/base/nethelpers.h" +#include "talk/base/stream.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" + +namespace testing { + +using namespace talk_base; + +/////////////////////////////////////////////////////////////////////////////// +// StreamSink - Monitor asynchronously signalled events from StreamInterface +// or AsyncSocket (which should probably be a StreamInterface. +/////////////////////////////////////////////////////////////////////////////// + +// Note: Any event that is an error is treaded as SSE_ERROR instead of that +// event. + +enum StreamSinkEvent { + SSE_OPEN = SE_OPEN, + SSE_READ = SE_READ, + SSE_WRITE = SE_WRITE, + SSE_CLOSE = SE_CLOSE, + SSE_ERROR = 16 +}; + +class StreamSink : public sigslot::has_slots<> { + public: + void Monitor(StreamInterface* stream) { + stream->SignalEvent.connect(this, &StreamSink::OnEvent); + events_.erase(stream); + } + void Unmonitor(StreamInterface* stream) { + stream->SignalEvent.disconnect(this); + // In case you forgot to unmonitor a previous object with this address + events_.erase(stream); + } + bool Check(StreamInterface* stream, StreamSinkEvent event, bool reset = true) { + return DoCheck(stream, event, reset); + } + int Events(StreamInterface* stream, bool reset = true) { + return DoEvents(stream, reset); + } + + void Monitor(AsyncSocket* socket) { + socket->SignalConnectEvent.connect(this, &StreamSink::OnConnectEvent); + socket->SignalReadEvent.connect(this, &StreamSink::OnReadEvent); + socket->SignalWriteEvent.connect(this, &StreamSink::OnWriteEvent); + socket->SignalCloseEvent.connect(this, &StreamSink::OnCloseEvent); + // In case you forgot to unmonitor a previous object with this address + events_.erase(socket); + } + void Unmonitor(AsyncSocket* socket) { + socket->SignalConnectEvent.disconnect(this); + socket->SignalReadEvent.disconnect(this); + socket->SignalWriteEvent.disconnect(this); + socket->SignalCloseEvent.disconnect(this); + events_.erase(socket); + } + bool Check(AsyncSocket* socket, StreamSinkEvent event, bool reset = true) { + return DoCheck(socket, event, reset); + } + int Events(AsyncSocket* socket, bool reset = true) { + return DoEvents(socket, reset); + } + + private: + typedef std::map EventMap; + + void OnEvent(StreamInterface* stream, int events, int error) { + if (error) { + events = SSE_ERROR; + } + AddEvents(stream, events); + } + void OnConnectEvent(AsyncSocket* socket) { + AddEvents(socket, SSE_OPEN); + } + void OnReadEvent(AsyncSocket* socket) { + AddEvents(socket, SSE_READ); + } + void OnWriteEvent(AsyncSocket* socket) { + AddEvents(socket, SSE_WRITE); + } + void OnCloseEvent(AsyncSocket* socket, int error) { + AddEvents(socket, (0 == error) ? SSE_CLOSE : SSE_ERROR); + } + + void AddEvents(void* obj, int events) { + EventMap::iterator it = events_.find(obj); + if (events_.end() == it) { + events_.insert(EventMap::value_type(obj, events)); + } else { + it->second |= events; + } + } + bool DoCheck(void* obj, StreamSinkEvent event, bool reset) { + EventMap::iterator it = events_.find(obj); + if ((events_.end() == it) || (0 == (it->second & event))) { + return false; + } + if (reset) { + it->second &= ~event; + } + return true; + } + int DoEvents(void* obj, bool reset) { + EventMap::iterator it = events_.find(obj); + if (events_.end() == it) + return 0; + int events = it->second; + if (reset) { + it->second = 0; + } + return events; + } + + EventMap events_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// StreamSource - Implements stream interface and simulates asynchronous +// events on the stream, without a network. Also buffers written data. +/////////////////////////////////////////////////////////////////////////////// + +class StreamSource : public StreamInterface { +public: + StreamSource() { + Clear(); + } + + void Clear() { + readable_data_.clear(); + written_data_.clear(); + state_ = SS_CLOSED; + read_block_ = 0; + write_block_ = SIZE_UNKNOWN; + } + void QueueString(const char* data) { + QueueData(data, strlen(data)); + } + void QueueStringF(const char* format, ...) { + va_list args; + va_start(args, format); + char buffer[1024]; + size_t len = vsprintfn(buffer, sizeof(buffer), format, args); + ASSERT(len < sizeof(buffer) - 1); + va_end(args); + QueueData(buffer, len); + } + void QueueData(const char* data, size_t len) { + readable_data_.insert(readable_data_.end(), data, data + len); + if ((SS_OPEN == state_) && (readable_data_.size() == len)) { + SignalEvent(this, SE_READ, 0); + } + } + std::string ReadData() { + std::string data; + // avoid accessing written_data_[0] if it is undefined + if (written_data_.size() > 0) { + data.insert(0, &written_data_[0], written_data_.size()); + } + written_data_.clear(); + return data; + } + void SetState(StreamState state) { + int events = 0; + if ((SS_OPENING == state_) && (SS_OPEN == state)) { + events |= SE_OPEN; + if (!readable_data_.empty()) { + events |= SE_READ; + } + } else if ((SS_CLOSED != state_) && (SS_CLOSED == state)) { + events |= SE_CLOSE; + } + state_ = state; + if (events) { + SignalEvent(this, events, 0); + } + } + // Will cause Read to block when there are pos bytes in the read queue. + void SetReadBlock(size_t pos) { read_block_ = pos; } + // Will cause Write to block when there are pos bytes in the write queue. + void SetWriteBlock(size_t pos) { write_block_ = pos; } + + virtual StreamState GetState() const { return state_; } + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + if (SS_CLOSED == state_) { + if (error) *error = -1; + return SR_ERROR; + } + if ((SS_OPENING == state_) || (readable_data_.size() <= read_block_)) { + return SR_BLOCK; + } + size_t count = _min(buffer_len, readable_data_.size() - read_block_); + memcpy(buffer, &readable_data_[0], count); + size_t new_size = readable_data_.size() - count; + // Avoid undefined access beyond the last element of the vector. + // This only happens when new_size is 0. + if (count < readable_data_.size()) { + memmove(&readable_data_[0], &readable_data_[count], new_size); + } + readable_data_.resize(new_size); + if (read) *read = count; + return SR_SUCCESS; + } + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error) { + if (SS_CLOSED == state_) { + if (error) *error = -1; + return SR_ERROR; + } + if (SS_OPENING == state_) { + return SR_BLOCK; + } + if (SIZE_UNKNOWN != write_block_) { + if (written_data_.size() >= write_block_) { + return SR_BLOCK; + } + if (data_len > (write_block_ - written_data_.size())) { + data_len = write_block_ - written_data_.size(); + } + } + if (written) *written = data_len; + const char* cdata = static_cast(data); + written_data_.insert(written_data_.end(), cdata, cdata + data_len); + return SR_SUCCESS; + } + virtual void Close() { state_ = SS_CLOSED; } + +private: + typedef std::vector Buffer; + Buffer readable_data_, written_data_; + StreamState state_; + size_t read_block_, write_block_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// SocketTestClient +// Creates a simulated client for testing. Works on real and virtual networks. +/////////////////////////////////////////////////////////////////////////////// + +class SocketTestClient : public sigslot::has_slots<> { +public: + SocketTestClient() { + Init(NULL, AF_INET); + } + SocketTestClient(AsyncSocket* socket) { + Init(socket, socket->GetLocalAddress().family()); + } + SocketTestClient(const SocketAddress& address) { + Init(NULL, address.family()); + socket_->Connect(address); + } + + AsyncSocket* socket() { return socket_.get(); } + + void QueueString(const char* data) { + QueueData(data, strlen(data)); + } + void QueueStringF(const char* format, ...) { + va_list args; + va_start(args, format); + char buffer[1024]; + size_t len = vsprintfn(buffer, sizeof(buffer), format, args); + ASSERT(len < sizeof(buffer) - 1); + va_end(args); + QueueData(buffer, len); + } + void QueueData(const char* data, size_t len) { + send_buffer_.insert(send_buffer_.end(), data, data + len); + if (Socket::CS_CONNECTED == socket_->GetState()) { + Flush(); + } + } + std::string ReadData() { + std::string data(&recv_buffer_[0], recv_buffer_.size()); + recv_buffer_.clear(); + return data; + } + + bool IsConnected() const { + return (Socket::CS_CONNECTED == socket_->GetState()); + } + bool IsClosed() const { + return (Socket::CS_CLOSED == socket_->GetState()); + } + +private: + typedef std::vector Buffer; + + void Init(AsyncSocket* socket, int family) { + if (!socket) { + socket = Thread::Current()->socketserver() + ->CreateAsyncSocket(family, SOCK_STREAM); + } + socket_.reset(socket); + socket_->SignalConnectEvent.connect(this, + &SocketTestClient::OnConnectEvent); + socket_->SignalReadEvent.connect(this, &SocketTestClient::OnReadEvent); + socket_->SignalWriteEvent.connect(this, &SocketTestClient::OnWriteEvent); + socket_->SignalCloseEvent.connect(this, &SocketTestClient::OnCloseEvent); + } + + void Flush() { + size_t sent = 0; + while (sent < send_buffer_.size()) { + int result = socket_->Send(&send_buffer_[sent], + send_buffer_.size() - sent); + if (result > 0) { + sent += result; + } else { + break; + } + } + size_t new_size = send_buffer_.size() - sent; + memmove(&send_buffer_[0], &send_buffer_[sent], new_size); + send_buffer_.resize(new_size); + } + + void OnConnectEvent(AsyncSocket* socket) { + if (!send_buffer_.empty()) { + Flush(); + } + } + void OnReadEvent(AsyncSocket* socket) { + char data[64 * 1024]; + int result = socket_->Recv(data, ARRAY_SIZE(data)); + if (result > 0) { + recv_buffer_.insert(recv_buffer_.end(), data, data + result); + } + } + void OnWriteEvent(AsyncSocket* socket) { + if (!send_buffer_.empty()) { + Flush(); + } + } + void OnCloseEvent(AsyncSocket* socket, int error) { + } + + scoped_ptr socket_; + Buffer send_buffer_, recv_buffer_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// SocketTestServer +// Creates a simulated server for testing. Works on real and virtual networks. +/////////////////////////////////////////////////////////////////////////////// + +class SocketTestServer : public sigslot::has_slots<> { + public: + SocketTestServer(const SocketAddress& address) + : socket_(Thread::Current()->socketserver() + ->CreateAsyncSocket(address.family(), SOCK_STREAM)) + { + socket_->SignalReadEvent.connect(this, &SocketTestServer::OnReadEvent); + socket_->Bind(address); + socket_->Listen(5); + } + virtual ~SocketTestServer() { + clear(); + } + + size_t size() const { return clients_.size(); } + SocketTestClient* client(size_t index) const { return clients_[index]; } + SocketTestClient* operator[](size_t index) const { return client(index); } + + void clear() { + for (size_t i=0; i(socket_->Accept(NULL)); + if (!accepted) + return; + clients_.push_back(new SocketTestClient(accepted)); + } + + scoped_ptr socket_; + std::vector clients_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// Generic Utilities +/////////////////////////////////////////////////////////////////////////////// + +inline bool ReadFile(const char* filename, std::string* contents) { + FILE* fp = fopen(filename, "rb"); + if (!fp) + return false; + char buffer[1024*64]; + size_t read; + contents->clear(); + while ((read = fread(buffer, 1, sizeof(buffer), fp))) { + contents->append(buffer, read); + } + bool success = (0 != feof(fp)); + fclose(fp); + return success; +} + +/////////////////////////////////////////////////////////////////////////////// +// Unittest predicates which are similar to STREQ, but for raw memory +/////////////////////////////////////////////////////////////////////////////// + +inline AssertionResult CmpHelperMemEq(const char* expected_expression, + const char* expected_length_expression, + const char* actual_expression, + const char* actual_length_expression, + const void* expected, + size_t expected_length, + const void* actual, + size_t actual_length) +{ + if ((expected_length == actual_length) + && (0 == memcmp(expected, actual, expected_length))) { + return AssertionSuccess(); + } + + Message msg; + msg << "Value of: " << actual_expression + << " [" << actual_length_expression << "]"; + if (true) { //!actual_value.Equals(actual_expression)) { + size_t buffer_size = actual_length * 2 + 1; + char* buffer = STACK_ARRAY(char, buffer_size); + hex_encode(buffer, buffer_size, + reinterpret_cast(actual), actual_length); + msg << "\n Actual: " << buffer << " [" << actual_length << "]"; + } + + msg << "\nExpected: " << expected_expression + << " [" << expected_length_expression << "]"; + if (true) { //!expected_value.Equals(expected_expression)) { + size_t buffer_size = expected_length * 2 + 1; + char* buffer = STACK_ARRAY(char, buffer_size); + hex_encode(buffer, buffer_size, + reinterpret_cast(expected), expected_length); + msg << "\nWhich is: " << buffer << " [" << expected_length << "]"; + } + + return AssertionFailure(msg); +} + +inline AssertionResult CmpHelperFileEq(const char* expected_expression, + const char* expected_length_expression, + const char* actual_filename, + const void* expected, + size_t expected_length, + const char* filename) +{ + std::string contents; + if (!ReadFile(filename, &contents)) { + Message msg; + msg << "File '" << filename << "' could not be read."; + return AssertionFailure(msg); + } + return CmpHelperMemEq(expected_expression, expected_length_expression, + actual_filename, "", + expected, expected_length, + contents.c_str(), contents.size()); +} + +#define EXPECT_MEMEQ(expected, expected_length, actual, actual_length) \ + EXPECT_PRED_FORMAT4(::testing::CmpHelperMemEq, expected, expected_length, \ + actual, actual_length) + +#define ASSERT_MEMEQ(expected, expected_length, actual, actual_length) \ + ASSERT_PRED_FORMAT4(::testing::CmpHelperMemEq, expected, expected_length, \ + actual, actual_length) + +#define EXPECT_FILEEQ(expected, expected_length, filename) \ + EXPECT_PRED_FORMAT3(::testing::CmpHelperFileEq, expected, expected_length, \ + filename) + +#define ASSERT_FILEEQ(expected, expected_length, filename) \ + ASSERT_PRED_FORMAT3(::testing::CmpHelperFileEq, expected, expected_length, \ + filename) + +/////////////////////////////////////////////////////////////////////////////// +// Helpers for initializing constant memory with integers in a particular byte +// order +/////////////////////////////////////////////////////////////////////////////// + +#define BYTE_CAST(x) static_cast((x) & 0xFF) + +// Declare a N-bit integer as a little-endian sequence of bytes +#define LE16(x) BYTE_CAST(((uint16)x) >> 0), BYTE_CAST(((uint16)x) >> 8) + +#define LE32(x) BYTE_CAST(((uint32)x) >> 0), BYTE_CAST(((uint32)x) >> 8), \ + BYTE_CAST(((uint32)x) >> 16), BYTE_CAST(((uint32)x) >> 24) + +#define LE64(x) BYTE_CAST(((uint64)x) >> 0), BYTE_CAST(((uint64)x) >> 8), \ + BYTE_CAST(((uint64)x) >> 16), BYTE_CAST(((uint64)x) >> 24), \ + BYTE_CAST(((uint64)x) >> 32), BYTE_CAST(((uint64)x) >> 40), \ + BYTE_CAST(((uint64)x) >> 48), BYTE_CAST(((uint64)x) >> 56) + +// Declare a N-bit integer as a big-endian (Internet) sequence of bytes +#define BE16(x) BYTE_CAST(((uint16)x) >> 8), BYTE_CAST(((uint16)x) >> 0) + +#define BE32(x) BYTE_CAST(((uint32)x) >> 24), BYTE_CAST(((uint32)x) >> 16), \ + BYTE_CAST(((uint32)x) >> 8), BYTE_CAST(((uint32)x) >> 0) + +#define BE64(x) BYTE_CAST(((uint64)x) >> 56), BYTE_CAST(((uint64)x) >> 48), \ + BYTE_CAST(((uint64)x) >> 40), BYTE_CAST(((uint64)x) >> 32), \ + BYTE_CAST(((uint64)x) >> 24), BYTE_CAST(((uint64)x) >> 16), \ + BYTE_CAST(((uint64)x) >> 8), BYTE_CAST(((uint64)x) >> 0) + +// Declare a N-bit integer as a this-endian (local machine) sequence of bytes +#ifndef BIG_ENDIAN +#define BIG_ENDIAN 1 +#endif // BIG_ENDIAN + +#if BIG_ENDIAN +#define TE16 BE16 +#define TE32 BE32 +#define TE64 BE64 +#else // !BIG_ENDIAN +#define TE16 LE16 +#define TE32 LE32 +#define TE64 LE64 +#endif // !BIG_ENDIAN + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace testing + +#endif // TALK_BASE_TESTUTILS_H__ diff --git a/talk/base/thread.cc b/talk/base/thread.cc new file mode 100644 index 000000000..d21d5f195 --- /dev/null +++ b/talk/base/thread.cc @@ -0,0 +1,582 @@ +/* + * libjingle + * Copyright 2004 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/base/thread.h" + +#ifndef __has_feature +#define __has_feature(x) 0 // Compatibility with non-clang or LLVM compilers. +#endif // __has_feature + +#if defined(WIN32) +#include +#elif defined(POSIX) +#include +#endif + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/stringutils.h" +#include "talk/base/timeutils.h" + +#if !__has_feature(objc_arc) && (defined(OSX) || defined(IOS)) +#include "talk/base/maccocoathreadhelper.h" +#include "talk/base/scoped_autorelease_pool.h" +#endif + +namespace talk_base { + +ThreadManager* ThreadManager::Instance() { + LIBJINGLE_DEFINE_STATIC_LOCAL(ThreadManager, thread_manager, ()); + return &thread_manager; +} + +// static +Thread* Thread::Current() { + return ThreadManager::Instance()->CurrentThread(); +} + +#ifdef POSIX +ThreadManager::ThreadManager() { + pthread_key_create(&key_, NULL); +#ifndef NO_MAIN_THREAD_WRAPPING + WrapCurrentThread(); +#endif +#if !__has_feature(objc_arc) && (defined(OSX) || defined(IOS)) + // Under Automatic Reference Counting (ARC), you cannot use autorelease pools + // directly. Instead, you use @autoreleasepool blocks instead. Also, we are + // maintaining thread safety using immutability within context of GCD dispatch + // queues in this case. + InitCocoaMultiThreading(); +#endif +} + +ThreadManager::~ThreadManager() { +#if __has_feature(objc_arc) + @autoreleasepool +#elif defined(OSX) || defined(IOS) + // This is called during exit, at which point apparently no NSAutoreleasePools + // are available; but we might still need them to do cleanup (or we get the + // "no autoreleasepool in place, just leaking" warning when exiting). + ScopedAutoreleasePool pool; +#endif + { + UnwrapCurrentThread(); + pthread_key_delete(key_); + } +} + +Thread *ThreadManager::CurrentThread() { + return static_cast(pthread_getspecific(key_)); +} + +void ThreadManager::SetCurrentThread(Thread *thread) { + pthread_setspecific(key_, thread); +} +#endif + +#ifdef WIN32 +ThreadManager::ThreadManager() { + key_ = TlsAlloc(); +#ifndef NO_MAIN_THREAD_WRAPPING + WrapCurrentThread(); +#endif +} + +ThreadManager::~ThreadManager() { + UnwrapCurrentThread(); + TlsFree(key_); +} + +Thread *ThreadManager::CurrentThread() { + return static_cast(TlsGetValue(key_)); +} + +void ThreadManager::SetCurrentThread(Thread *thread) { + TlsSetValue(key_, thread); +} +#endif + +Thread *ThreadManager::WrapCurrentThread() { + Thread* result = CurrentThread(); + if (NULL == result) { + result = new Thread(); + result->WrapCurrentWithThreadManager(this); + } + return result; +} + +void ThreadManager::UnwrapCurrentThread() { + Thread* t = CurrentThread(); + if (t && !(t->IsOwned())) { + t->UnwrapCurrent(); + delete t; + } +} + +struct ThreadInit { + Thread* thread; + Runnable* runnable; +}; + +Thread::Thread(SocketServer* ss) + : MessageQueue(ss), + priority_(PRIORITY_NORMAL), + started_(false), + has_sends_(false), +#if defined(WIN32) + thread_(NULL), + thread_id_(0), +#endif + owned_(true), + delete_self_when_complete_(false) { + SetName("Thread", this); // default name +} + +Thread::~Thread() { + Stop(); + if (active_) + Clear(NULL); +} + +bool Thread::SleepMs(int milliseconds) { +#ifdef WIN32 + ::Sleep(milliseconds); + return true; +#else + // POSIX has both a usleep() and a nanosleep(), but the former is deprecated, + // so we use nanosleep() even though it has greater precision than necessary. + struct timespec ts; + ts.tv_sec = milliseconds / 1000; + ts.tv_nsec = (milliseconds % 1000) * 1000000; + int ret = nanosleep(&ts, NULL); + if (ret != 0) { + LOG_ERR(LS_WARNING) << "nanosleep() returning early"; + return false; + } + return true; +#endif +} + +bool Thread::SetName(const std::string& name, const void* obj) { + if (started_) return false; + name_ = name; + if (obj) { + char buf[16]; + sprintfn(buf, sizeof(buf), " 0x%p", obj); + name_ += buf; + } + return true; +} + +bool Thread::SetPriority(ThreadPriority priority) { +#if defined(WIN32) + if (started_) { + BOOL ret = FALSE; + if (priority == PRIORITY_NORMAL) { + ret = ::SetThreadPriority(thread_, THREAD_PRIORITY_NORMAL); + } else if (priority == PRIORITY_HIGH) { + ret = ::SetThreadPriority(thread_, THREAD_PRIORITY_HIGHEST); + } else if (priority == PRIORITY_ABOVE_NORMAL) { + ret = ::SetThreadPriority(thread_, THREAD_PRIORITY_ABOVE_NORMAL); + } else if (priority == PRIORITY_IDLE) { + ret = ::SetThreadPriority(thread_, THREAD_PRIORITY_IDLE); + } + if (!ret) { + return false; + } + } + priority_ = priority; + return true; +#else + // TODO: Implement for Linux/Mac if possible. + if (started_) return false; + priority_ = priority; + return true; +#endif +} + +bool Thread::Start(Runnable* runnable) { + ASSERT(owned_); + if (!owned_) return false; + ASSERT(!started_); + if (started_) return false; + + Restart(); // reset fStop_ if the thread is being restarted + + // Make sure that ThreadManager is created on the main thread before + // we start a new thread. + ThreadManager::Instance(); + + ThreadInit* init = new ThreadInit; + init->thread = this; + init->runnable = runnable; +#if defined(WIN32) + DWORD flags = 0; + if (priority_ != PRIORITY_NORMAL) { + flags = CREATE_SUSPENDED; + } + thread_ = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)PreRun, init, flags, + &thread_id_); + if (thread_) { + started_ = true; + if (priority_ != PRIORITY_NORMAL) { + SetPriority(priority_); + ::ResumeThread(thread_); + } + } else { + return false; + } +#elif defined(POSIX) + pthread_attr_t attr; + pthread_attr_init(&attr); + if (priority_ != PRIORITY_NORMAL) { + if (priority_ == PRIORITY_IDLE) { + // There is no POSIX-standard way to set a below-normal priority for an + // individual thread (only whole process), so let's not support it. + LOG(LS_WARNING) << "PRIORITY_IDLE not supported"; + } else { + // Set real-time round-robin policy. + if (pthread_attr_setschedpolicy(&attr, SCHED_RR) != 0) { + LOG(LS_ERROR) << "pthread_attr_setschedpolicy"; + } + struct sched_param param; + if (pthread_attr_getschedparam(&attr, ¶m) != 0) { + LOG(LS_ERROR) << "pthread_attr_getschedparam"; + } else { + // The numbers here are arbitrary. + if (priority_ == PRIORITY_HIGH) { + param.sched_priority = 6; // 6 = HIGH + } else { + ASSERT(priority_ == PRIORITY_ABOVE_NORMAL); + param.sched_priority = 4; // 4 = ABOVE_NORMAL + } + if (pthread_attr_setschedparam(&attr, ¶m) != 0) { + LOG(LS_ERROR) << "pthread_attr_setschedparam"; + } + } + } + } + int error_code = pthread_create(&thread_, &attr, PreRun, init); + if (0 != error_code) { + LOG(LS_ERROR) << "Unable to create pthread, error " << error_code; + return false; + } + started_ = true; +#endif + return true; +} + +void Thread::Join() { + if (started_) { + ASSERT(!IsCurrent()); +#if defined(WIN32) + WaitForSingleObject(thread_, INFINITE); + CloseHandle(thread_); + thread_ = NULL; + thread_id_ = 0; +#elif defined(POSIX) + void *pv; + pthread_join(thread_, &pv); +#endif + started_ = false; + } +} + +#ifdef WIN32 +// As seen on MSDN. +// http://msdn.microsoft.com/en-us/library/xcb2z8hs(VS.71).aspx +#define MSDEV_SET_THREAD_NAME 0x406D1388 +typedef struct tagTHREADNAME_INFO { + DWORD dwType; + LPCSTR szName; + DWORD dwThreadID; + DWORD dwFlags; +} THREADNAME_INFO; + +void SetThreadName(DWORD dwThreadID, LPCSTR szThreadName) { + THREADNAME_INFO info; + info.dwType = 0x1000; + info.szName = szThreadName; + info.dwThreadID = dwThreadID; + info.dwFlags = 0; + + __try { + RaiseException(MSDEV_SET_THREAD_NAME, 0, sizeof(info) / sizeof(DWORD), + reinterpret_cast(&info)); + } + __except(EXCEPTION_CONTINUE_EXECUTION) { + } +} +#endif // WIN32 + +void* Thread::PreRun(void* pv) { + ThreadInit* init = static_cast(pv); + ThreadManager::Instance()->SetCurrentThread(init->thread); +#if defined(WIN32) + SetThreadName(GetCurrentThreadId(), init->thread->name_.c_str()); +#elif defined(POSIX) + // TODO: See if naming exists for pthreads. +#endif +#if __has_feature(objc_arc) + @autoreleasepool +#elif defined(OSX) || defined(IOS) + // Make sure the new thread has an autoreleasepool + ScopedAutoreleasePool pool; +#endif + { + if (init->runnable) { + init->runnable->Run(init->thread); + } else { + init->thread->Run(); + } + if (init->thread->delete_self_when_complete_) { + init->thread->started_ = false; + delete init->thread; + } + delete init; + return NULL; + } +} + +void Thread::Run() { + ProcessMessages(kForever); +} + +bool Thread::IsOwned() { + return owned_; +} + +void Thread::Stop() { + MessageQueue::Quit(); + Join(); +} + +void Thread::Send(MessageHandler *phandler, uint32 id, MessageData *pdata) { + if (fStop_) + return; + + // Sent messages are sent to the MessageHandler directly, in the context + // of "thread", like Win32 SendMessage. If in the right context, + // call the handler directly. + + Message msg; + msg.phandler = phandler; + msg.message_id = id; + msg.pdata = pdata; + if (IsCurrent()) { + phandler->OnMessage(&msg); + return; + } + + AutoThread thread; + Thread *current_thread = Thread::Current(); + ASSERT(current_thread != NULL); // AutoThread ensures this + + bool ready = false; + { + CritScope cs(&crit_); + EnsureActive(); + _SendMessage smsg; + smsg.thread = current_thread; + smsg.msg = msg; + smsg.ready = &ready; + sendlist_.push_back(smsg); + has_sends_ = true; + } + + // Wait for a reply + + ss_->WakeUp(); + + bool waited = false; + while (!ready) { + current_thread->ReceiveSends(); + current_thread->socketserver()->Wait(kForever, false); + waited = true; + } + + // Our Wait loop above may have consumed some WakeUp events for this + // MessageQueue, that weren't relevant to this Send. Losing these WakeUps can + // cause problems for some SocketServers. + // + // Concrete example: + // Win32SocketServer on thread A calls Send on thread B. While processing the + // message, thread B Posts a message to A. We consume the wakeup for that + // Post while waiting for the Send to complete, which means that when we exit + // this loop, we need to issue another WakeUp, or else the Posted message + // won't be processed in a timely manner. + + if (waited) { + current_thread->socketserver()->WakeUp(); + } +} + +void Thread::ReceiveSends() { + // Before entering critical section, check boolean. + + if (!has_sends_) + return; + + // Receive a sent message. Cleanup scenarios: + // - thread sending exits: We don't allow this, since thread can exit + // only via Join, so Send must complete. + // - thread receiving exits: Wakeup/set ready in Thread::Clear() + // - object target cleared: Wakeup/set ready in Thread::Clear() + crit_.Enter(); + while (!sendlist_.empty()) { + _SendMessage smsg = sendlist_.front(); + sendlist_.pop_front(); + crit_.Leave(); + smsg.msg.phandler->OnMessage(&smsg.msg); + crit_.Enter(); + *smsg.ready = true; + smsg.thread->socketserver()->WakeUp(); + } + has_sends_ = false; + crit_.Leave(); +} + +void Thread::Clear(MessageHandler *phandler, uint32 id, + MessageList* removed) { + CritScope cs(&crit_); + + // Remove messages on sendlist_ with phandler + // Object target cleared: remove from send list, wakeup/set ready + // if sender not NULL. + + std::list<_SendMessage>::iterator iter = sendlist_.begin(); + while (iter != sendlist_.end()) { + _SendMessage smsg = *iter; + if (smsg.msg.Match(phandler, id)) { + if (removed) { + removed->push_back(smsg.msg); + } else { + delete smsg.msg.pdata; + } + iter = sendlist_.erase(iter); + *smsg.ready = true; + smsg.thread->socketserver()->WakeUp(); + continue; + } + ++iter; + } + + MessageQueue::Clear(phandler, id, removed); +} + +bool Thread::ProcessMessages(int cmsLoop) { + uint32 msEnd = (kForever == cmsLoop) ? 0 : TimeAfter(cmsLoop); + int cmsNext = cmsLoop; + + while (true) { +#if __has_feature(objc_arc) + @autoreleasepool +#elif defined(OSX) || defined(IOS) + // see: http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSAutoreleasePool_Class/Reference/Reference.html + // Each thread is supposed to have an autorelease pool. Also for event loops + // like this, autorelease pool needs to be created and drained/released + // for each cycle. + ScopedAutoreleasePool pool; +#endif + { + Message msg; + if (!Get(&msg, cmsNext)) + return !IsQuitting(); + Dispatch(&msg); + + if (cmsLoop != kForever) { + cmsNext = TimeUntil(msEnd); + if (cmsNext < 0) + return true; + } + } + } +} + +bool Thread::WrapCurrent() { + return WrapCurrentWithThreadManager(ThreadManager::Instance()); +} + +bool Thread::WrapCurrentWithThreadManager(ThreadManager* thread_manager) { + if (started_) + return false; +#if defined(WIN32) + // We explicitly ask for no rights other than synchronization. + // This gives us the best chance of succeeding. + thread_ = OpenThread(SYNCHRONIZE, FALSE, GetCurrentThreadId()); + if (!thread_) { + LOG_GLE(LS_ERROR) << "Unable to get handle to thread."; + return false; + } + thread_id_ = GetCurrentThreadId(); +#elif defined(POSIX) + thread_ = pthread_self(); +#endif + owned_ = false; + started_ = true; + thread_manager->SetCurrentThread(this); + return true; +} + +void Thread::UnwrapCurrent() { + // Clears the platform-specific thread-specific storage. + ThreadManager::Instance()->SetCurrentThread(NULL); +#ifdef WIN32 + if (!CloseHandle(thread_)) { + LOG_GLE(LS_ERROR) << "When unwrapping thread, failed to close handle."; + } +#endif + started_ = false; +} + + +AutoThread::AutoThread(SocketServer* ss) : Thread(ss) { + if (!ThreadManager::Instance()->CurrentThread()) { + ThreadManager::Instance()->SetCurrentThread(this); + } +} + +AutoThread::~AutoThread() { + if (ThreadManager::Instance()->CurrentThread() == this) { + ThreadManager::Instance()->SetCurrentThread(NULL); + } +} + +#ifdef WIN32 +void ComThread::Run() { + HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); + ASSERT(SUCCEEDED(hr)); + if (SUCCEEDED(hr)) { + Thread::Run(); + CoUninitialize(); + } else { + LOG(LS_ERROR) << "CoInitialize failed, hr=" << hr; + } +} +#endif + +} // namespace talk_base diff --git a/talk/base/thread.h b/talk/base/thread.h new file mode 100644 index 000000000..55ec0daf7 --- /dev/null +++ b/talk/base/thread.h @@ -0,0 +1,323 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_BASE_THREAD_H_ +#define TALK_BASE_THREAD_H_ + +#include +#include +#include +#include + +#ifdef POSIX +#include +#endif +#include "talk/base/constructormagic.h" +#include "talk/base/messagequeue.h" + +#ifdef WIN32 +#include "talk/base/win32.h" +#endif + +namespace talk_base { + +class Thread; + +class ThreadManager { + public: + ThreadManager(); + ~ThreadManager(); + + static ThreadManager* Instance(); + + Thread* CurrentThread(); + void SetCurrentThread(Thread* thread); + + // Returns a thread object with its thread_ ivar set + // to whatever the OS uses to represent the thread. + // If there already *is* a Thread object corresponding to this thread, + // this method will return that. Otherwise it creates a new Thread + // object whose wrapped() method will return true, and whose + // handle will, on Win32, be opened with only synchronization privileges - + // if you need more privilegs, rather than changing this method, please + // write additional code to adjust the privileges, or call a different + // factory method of your own devising, because this one gets used in + // unexpected contexts (like inside browser plugins) and it would be a + // shame to break it. It is also conceivable on Win32 that we won't even + // be able to get synchronization privileges, in which case the result + // will have a NULL handle. + Thread *WrapCurrentThread(); + void UnwrapCurrentThread(); + + private: +#ifdef POSIX + pthread_key_t key_; +#endif + +#ifdef WIN32 + DWORD key_; +#endif + + DISALLOW_COPY_AND_ASSIGN(ThreadManager); +}; + +struct _SendMessage { + _SendMessage() {} + Thread *thread; + Message msg; + bool *ready; +}; + +enum ThreadPriority { + PRIORITY_IDLE = -1, + PRIORITY_NORMAL = 0, + PRIORITY_ABOVE_NORMAL = 1, + PRIORITY_HIGH = 2, +}; + +class Runnable { + public: + virtual ~Runnable() {} + virtual void Run(Thread* thread) = 0; + + protected: + Runnable() {} + + private: + DISALLOW_COPY_AND_ASSIGN(Runnable); +}; + +class Thread : public MessageQueue { + public: + explicit Thread(SocketServer* ss = NULL); + virtual ~Thread(); + + static Thread* Current(); + + bool IsCurrent() const { + return Current() == this; + } + + // Sleeps the calling thread for the specified number of milliseconds, during + // which time no processing is performed. Returns false if sleeping was + // interrupted by a signal (POSIX only). + static bool SleepMs(int millis); + + // Sets the thread's name, for debugging. Must be called before Start(). + // If |obj| is non-NULL, its value is appended to |name|. + const std::string& name() const { return name_; } + bool SetName(const std::string& name, const void* obj); + + // Sets the thread's priority. Must be called before Start(). + ThreadPriority priority() const { return priority_; } + bool SetPriority(ThreadPriority priority); + + // Starts the execution of the thread. + bool started() const { return started_; } + bool Start(Runnable* runnable = NULL); + + // Used for fire-and-forget threads. Deletes this thread object when the + // Run method returns. + void Release() { + delete_self_when_complete_ = true; + } + + // Tells the thread to stop and waits until it is joined. + // Never call Stop on the current thread. Instead use the inherited Quit + // function which will exit the base MessageQueue without terminating the + // underlying OS thread. + virtual void Stop(); + + // By default, Thread::Run() calls ProcessMessages(kForever). To do other + // work, override Run(). To receive and dispatch messages, call + // ProcessMessages occasionally. + virtual void Run(); + + virtual void Send(MessageHandler *phandler, uint32 id = 0, + MessageData *pdata = NULL); + + // Convenience method to invoke a functor on another thread. Caller must + // provide the |ReturnT| template argument, which cannot (easily) be deduced. + // Uses Send() internally, which blocks the current thread until execution + // is complete. + // Ex: bool result = thread.Invoke(&MyFunctionReturningBool); + template + ReturnT Invoke(const FunctorT& functor) { + FunctorMessageHandler handler(functor); + Send(&handler); + return handler.result(); + } + + // From MessageQueue + virtual void Clear(MessageHandler *phandler, uint32 id = MQID_ANY, + MessageList* removed = NULL); + virtual void ReceiveSends(); + + // ProcessMessages will process I/O and dispatch messages until: + // 1) cms milliseconds have elapsed (returns true) + // 2) Stop() is called (returns false) + bool ProcessMessages(int cms); + + // Returns true if this is a thread that we created using the standard + // constructor, false if it was created by a call to + // ThreadManager::WrapCurrentThread(). The main thread of an application + // is generally not owned, since the OS representation of the thread + // obviously exists before we can get to it. + // You cannot call Start on non-owned threads. + bool IsOwned(); + +#ifdef WIN32 + HANDLE GetHandle() const { + return thread_; + } + DWORD GetId() const { + return thread_id_; + } +#elif POSIX + pthread_t GetPThread() { + return thread_; + } +#endif + + // This method should be called when thread is created using non standard + // method, like derived implementation of talk_base::Thread and it can not be + // started by calling Start(). This will set started flag to true and + // owned to false. This must be called from the current thread. + // NOTE: These methods should be used by the derived classes only, added here + // only for testing. + bool WrapCurrent(); + void UnwrapCurrent(); + + protected: + // Blocks the calling thread until this thread has terminated. + void Join(); + + private: + // Helper class to facilitate executing a functor on a thread. + template + class FunctorMessageHandler : public MessageHandler { + public: + explicit FunctorMessageHandler(const FunctorT& functor) + : functor_(functor) {} + virtual void OnMessage(Message* msg) { + result_ = functor_(); + } + const ReturnT& result() const { return result_; } + private: + FunctorT functor_; + ReturnT result_; + }; + + // Specialization for ReturnT of void. + template + class FunctorMessageHandler : public MessageHandler { + public: + explicit FunctorMessageHandler(const FunctorT& functor) + : functor_(functor) {} + virtual void OnMessage(Message* msg) { functor_(); } + void result() const {} + private: + FunctorT functor_; + }; + + static void *PreRun(void *pv); + + // ThreadManager calls this instead WrapCurrent() because + // ThreadManager::Instance() cannot be used while ThreadManager is + // being created. + bool WrapCurrentWithThreadManager(ThreadManager* thread_manager); + + std::list<_SendMessage> sendlist_; + std::string name_; + ThreadPriority priority_; + bool started_; + bool has_sends_; + +#ifdef POSIX + pthread_t thread_; +#endif + +#ifdef WIN32 + HANDLE thread_; + DWORD thread_id_; +#endif + + bool owned_; + bool delete_self_when_complete_; + + friend class ThreadManager; + + DISALLOW_COPY_AND_ASSIGN(Thread); +}; + +// AutoThread automatically installs itself at construction +// uninstalls at destruction, if a Thread object is +// _not already_ associated with the current OS thread. + +class AutoThread : public Thread { + public: + explicit AutoThread(SocketServer* ss = 0); + virtual ~AutoThread(); + + private: + DISALLOW_COPY_AND_ASSIGN(AutoThread); +}; + +// Win32 extension for threads that need to use COM +#ifdef WIN32 +class ComThread : public Thread { + public: + ComThread() {} + + protected: + virtual void Run(); + + private: + DISALLOW_COPY_AND_ASSIGN(ComThread); +}; +#endif + +// Provides an easy way to install/uninstall a socketserver on a thread. +class SocketServerScope { + public: + explicit SocketServerScope(SocketServer* ss) { + old_ss_ = Thread::Current()->socketserver(); + Thread::Current()->set_socketserver(ss); + } + ~SocketServerScope() { + Thread::Current()->set_socketserver(old_ss_); + } + + private: + SocketServer* old_ss_; + + DISALLOW_IMPLICIT_CONSTRUCTORS(SocketServerScope); +}; + +} // namespace talk_base + +#endif // TALK_BASE_THREAD_H_ diff --git a/talk/base/thread_unittest.cc b/talk/base/thread_unittest.cc new file mode 100644 index 000000000..11b493da9 --- /dev/null +++ b/talk/base/thread_unittest.cc @@ -0,0 +1,329 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/asyncudpsocket.h" +#include "talk/base/event.h" +#include "talk/base/gunit.h" +#include "talk/base/host.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/socketaddress.h" +#include "talk/base/thread.h" + +#ifdef WIN32 +#include // NOLINT +#endif + +using namespace talk_base; + +const int MAX = 65536; + +// Generates a sequence of numbers (collaboratively). +class TestGenerator { + public: + TestGenerator() : last(0), count(0) {} + + int Next(int prev) { + int result = prev + last; + last = result; + count += 1; + return result; + } + + int last; + int count; +}; + +struct TestMessage : public MessageData { + explicit TestMessage(int v) : value(v) {} + virtual ~TestMessage() {} + + int value; +}; + +// Receives on a socket and sends by posting messages. +class SocketClient : public TestGenerator, public sigslot::has_slots<> { + public: + SocketClient(AsyncSocket* socket, const SocketAddress& addr, + Thread* post_thread, MessageHandler* phandler) + : socket_(AsyncUDPSocket::Create(socket, addr)), + post_thread_(post_thread), + post_handler_(phandler) { + socket_->SignalReadPacket.connect(this, &SocketClient::OnPacket); + } + + ~SocketClient() { + delete socket_; + } + + SocketAddress address() const { return socket_->GetLocalAddress(); } + + void OnPacket(AsyncPacketSocket* socket, const char* buf, size_t size, + const SocketAddress& remote_addr) { + EXPECT_EQ(size, sizeof(uint32)); + uint32 prev = reinterpret_cast(buf)[0]; + uint32 result = Next(prev); + + //socket_->set_readable(last < MAX); + post_thread_->PostDelayed(200, post_handler_, 0, new TestMessage(result)); + } + + private: + AsyncUDPSocket* socket_; + Thread* post_thread_; + MessageHandler* post_handler_; +}; + +// Receives messages and sends on a socket. +class MessageClient : public MessageHandler, public TestGenerator { + public: + MessageClient(Thread* pth, Socket* socket) + : thread_(pth), socket_(socket) { + } + + virtual ~MessageClient() { + delete socket_; + } + + virtual void OnMessage(Message *pmsg) { + TestMessage* msg = static_cast(pmsg->pdata); + int result = Next(msg->value); + EXPECT_GE(socket_->Send(&result, sizeof(result)), 0); + delete msg; + } + + private: + Thread* thread_; + Socket* socket_; +}; + +class CustomThread : public talk_base::Thread { + public: + CustomThread() {} + virtual ~CustomThread() {} + bool Start() { return false; } +}; + + +// A thread that does nothing when it runs and signals an event +// when it is destroyed. +class SignalWhenDestroyedThread : public Thread { + public: + SignalWhenDestroyedThread(Event* event) + : event_(event) { + } + + virtual ~SignalWhenDestroyedThread() { + event_->Set(); + } + + virtual void Run() { + // Do nothing. + } + + private: + Event* event_; +}; + +// Function objects to test Thread::Invoke. +struct Functor1 { + int operator()() { return 42; } +}; +class Functor2 { + public: + explicit Functor2(bool* flag) : flag_(flag) {} + void operator()() { if (flag_) *flag_ = true; } + private: + bool* flag_; +}; + + +TEST(ThreadTest, Main) { + const SocketAddress addr("127.0.0.1", 0); + + // Create the messaging client on its own thread. + Thread th1; + Socket* socket = th1.socketserver()->CreateAsyncSocket(addr.family(), + SOCK_DGRAM); + MessageClient msg_client(&th1, socket); + + // Create the socket client on its own thread. + Thread th2; + AsyncSocket* asocket = + th2.socketserver()->CreateAsyncSocket(addr.family(), SOCK_DGRAM); + SocketClient sock_client(asocket, addr, &th1, &msg_client); + + socket->Connect(sock_client.address()); + + th1.Start(); + th2.Start(); + + // Get the messages started. + th1.PostDelayed(100, &msg_client, 0, new TestMessage(1)); + + // Give the clients a little while to run. + // Messages will be processed at 100, 300, 500, 700, 900. + Thread* th_main = Thread::Current(); + th_main->ProcessMessages(1000); + + // Stop the sending client. Give the receiver a bit longer to run, in case + // it is running on a machine that is under load (e.g. the build machine). + th1.Stop(); + th_main->ProcessMessages(200); + th2.Stop(); + + // Make sure the results were correct + EXPECT_EQ(5, msg_client.count); + EXPECT_EQ(34, msg_client.last); + EXPECT_EQ(5, sock_client.count); + EXPECT_EQ(55, sock_client.last); +} + +// Test that setting thread names doesn't cause a malfunction. +// There's no easy way to verify the name was set properly at this time. +TEST(ThreadTest, Names) { + // Default name + Thread *thread; + thread = new Thread(); + EXPECT_TRUE(thread->Start()); + thread->Stop(); + delete thread; + thread = new Thread(); + // Name with no object parameter + EXPECT_TRUE(thread->SetName("No object", NULL)); + EXPECT_TRUE(thread->Start()); + thread->Stop(); + delete thread; + // Really long name + thread = new Thread(); + EXPECT_TRUE(thread->SetName("Abcdefghijklmnopqrstuvwxyz1234567890", this)); + EXPECT_TRUE(thread->Start()); + thread->Stop(); + delete thread; +} + +// Test that setting thread priorities doesn't cause a malfunction. +// There's no easy way to verify the priority was set properly at this time. +TEST(ThreadTest, Priorities) { + Thread *thread; + thread = new Thread(); + EXPECT_TRUE(thread->SetPriority(PRIORITY_HIGH)); + EXPECT_TRUE(thread->Start()); + thread->Stop(); + delete thread; + thread = new Thread(); + EXPECT_TRUE(thread->SetPriority(PRIORITY_ABOVE_NORMAL)); + EXPECT_TRUE(thread->Start()); + thread->Stop(); + delete thread; + + thread = new Thread(); + EXPECT_TRUE(thread->Start()); +#ifdef WIN32 + EXPECT_TRUE(thread->SetPriority(PRIORITY_ABOVE_NORMAL)); +#else + EXPECT_FALSE(thread->SetPriority(PRIORITY_ABOVE_NORMAL)); +#endif + thread->Stop(); + delete thread; + +} + +TEST(ThreadTest, Wrap) { + Thread* current_thread = Thread::Current(); + current_thread->UnwrapCurrent(); + CustomThread* cthread = new CustomThread(); + EXPECT_TRUE(cthread->WrapCurrent()); + EXPECT_TRUE(cthread->started()); + EXPECT_FALSE(cthread->IsOwned()); + cthread->UnwrapCurrent(); + EXPECT_FALSE(cthread->started()); + delete cthread; + current_thread->WrapCurrent(); +} + +// Test that calling Release on a thread causes it to self-destruct when +// it's finished running +TEST(ThreadTest, Release) { + scoped_ptr event(new Event(true, false)); + // Ensure the event is initialized. + event->Reset(); + + Thread* thread = new SignalWhenDestroyedThread(event.get()); + thread->Start(); + thread->Release(); + + // The event should get signaled when the thread completes, which should + // be nearly instantaneous, since it doesn't do anything. For safety, + // give it 3 seconds in case the machine is under load. + bool signaled = event->Wait(3000); + EXPECT_TRUE(signaled); +} + +TEST(ThreadTest, Invoke) { + // Create and start the thread. + Thread thread; + thread.Start(); + // Try calling functors. + EXPECT_EQ(42, thread.Invoke(Functor1())); + bool called = false; + Functor2 f2(&called); + thread.Invoke(f2); + EXPECT_TRUE(called); + // Try calling bare functions. + struct LocalFuncs { + static int Func1() { return 999; } + static void Func2() {} + }; + EXPECT_EQ(999, thread.Invoke(&LocalFuncs::Func1)); + thread.Invoke(&LocalFuncs::Func2); +} + +#ifdef WIN32 +class ComThreadTest : public testing::Test, public MessageHandler { + public: + ComThreadTest() : done_(false) {} + protected: + virtual void OnMessage(Message* message) { + HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); + // S_FALSE means the thread was already inited for a multithread apartment. + EXPECT_EQ(S_FALSE, hr); + if (SUCCEEDED(hr)) { + CoUninitialize(); + } + done_ = true; + } + bool done_; +}; + +TEST_F(ComThreadTest, ComInited) { + Thread* thread = new ComThread(); + EXPECT_TRUE(thread->Start()); + thread->Post(this, 0); + EXPECT_TRUE_WAIT(done_, 1000); + delete thread; +} +#endif diff --git a/talk/base/timeutils.cc b/talk/base/timeutils.cc new file mode 100644 index 000000000..66b9bf2c7 --- /dev/null +++ b/talk/base/timeutils.cc @@ -0,0 +1,201 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifdef POSIX +#include +#if defined(OSX) || defined(IOS) +#include +#endif +#endif + +#ifdef WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#endif + +#include "talk/base/common.h" +#include "talk/base/timeutils.h" + +#define EFFICIENT_IMPLEMENTATION 1 + +namespace talk_base { + +const uint32 LAST = 0xFFFFFFFF; +const uint32 HALF = 0x80000000; + +uint64 TimeNanos() { + int64 ticks = 0; +#if defined(OSX) || defined(IOS) + static mach_timebase_info_data_t timebase; + if (timebase.denom == 0) { + // Get the timebase if this is the first time we run. + // Recommended by Apple's QA1398. + VERIFY(KERN_SUCCESS == mach_timebase_info(&timebase)); + } + // Use timebase to convert absolute time tick units into nanoseconds. + ticks = mach_absolute_time() * timebase.numer / timebase.denom; +#elif defined(POSIX) + struct timespec ts; + // TODO: Do we need to handle the case when CLOCK_MONOTONIC + // is not supported? + clock_gettime(CLOCK_MONOTONIC, &ts); + ticks = kNumNanosecsPerSec * static_cast(ts.tv_sec) + + static_cast(ts.tv_nsec); +#elif defined(WIN32) + static volatile LONG last_timegettime = 0; + static volatile int64 num_wrap_timegettime = 0; + volatile LONG* last_timegettime_ptr = &last_timegettime; + DWORD now = timeGetTime(); + // Atomically update the last gotten time + DWORD old = InterlockedExchange(last_timegettime_ptr, now); + if (now < old) { + // If now is earlier than old, there may have been a race between + // threads. + // 0x0fffffff ~3.1 days, the code will not take that long to execute + // so it must have been a wrap around. + if (old > 0xf0000000 && now < 0x0fffffff) { + num_wrap_timegettime++; + } + } + ticks = now + (num_wrap_timegettime << 32); + // TODO: Calculate with nanosecond precision. Otherwise, we're just + // wasting a multiply and divide when doing Time() on Windows. + ticks = ticks * kNumNanosecsPerMillisec; +#endif + return ticks; +} + +uint32 Time() { + return static_cast(TimeNanos() / kNumNanosecsPerMillisec); +} + +#if defined(WIN32) +static const uint64 kFileTimeToUnixTimeEpochOffset = 116444736000000000ULL; + +struct timeval { + long tv_sec, tv_usec; // NOLINT +}; + +// Emulate POSIX gettimeofday(). +// Based on breakpad/src/third_party/glog/src/utilities.cc +static int gettimeofday(struct timeval *tv, void *tz) { + // FILETIME is measured in tens of microseconds since 1601-01-01 UTC. + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + + LARGE_INTEGER li; + li.LowPart = ft.dwLowDateTime; + li.HighPart = ft.dwHighDateTime; + + // Convert to seconds and microseconds since Unix time Epoch. + int64 micros = (li.QuadPart - kFileTimeToUnixTimeEpochOffset) / 10; + tv->tv_sec = static_cast(micros / kNumMicrosecsPerSec); // NOLINT + tv->tv_usec = static_cast(micros % kNumMicrosecsPerSec); // NOLINT + + return 0; +} + +// Emulate POSIX gmtime_r(). +static struct tm *gmtime_r(const time_t *timep, struct tm *result) { + // On Windows, gmtime is thread safe. + struct tm *tm = gmtime(timep); // NOLINT + if (tm == NULL) { + return NULL; + } + *result = *tm; + return result; +} +#endif // WIN32 + +void CurrentTmTime(struct tm *tm, int *microseconds) { + struct timeval timeval; + if (gettimeofday(&timeval, NULL) < 0) { + // Incredibly unlikely code path. + timeval.tv_sec = timeval.tv_usec = 0; + } + time_t secs = timeval.tv_sec; + gmtime_r(&secs, tm); + *microseconds = timeval.tv_usec; +} + +uint32 TimeAfter(int32 elapsed) { + ASSERT(elapsed >= 0); + ASSERT(static_cast(elapsed) < HALF); + return Time() + elapsed; +} + +bool TimeIsBetween(uint32 earlier, uint32 middle, uint32 later) { + if (earlier <= later) { + return ((earlier <= middle) && (middle <= later)); + } else { + return !((later < middle) && (middle < earlier)); + } +} + +bool TimeIsLaterOrEqual(uint32 earlier, uint32 later) { +#if EFFICIENT_IMPLEMENTATION + int32 diff = later - earlier; + return (diff >= 0 && static_cast(diff) < HALF); +#else + const bool later_or_equal = TimeIsBetween(earlier, later, earlier + HALF); + return later_or_equal; +#endif +} + +bool TimeIsLater(uint32 earlier, uint32 later) { +#if EFFICIENT_IMPLEMENTATION + int32 diff = later - earlier; + return (diff > 0 && static_cast(diff) < HALF); +#else + const bool earlier_or_equal = TimeIsBetween(later, earlier, later + HALF); + return !earlier_or_equal; +#endif +} + +int32 TimeDiff(uint32 later, uint32 earlier) { +#if EFFICIENT_IMPLEMENTATION + return later - earlier; +#else + const bool later_or_equal = TimeIsBetween(earlier, later, earlier + HALF); + if (later_or_equal) { + if (earlier <= later) { + return static_cast(later - earlier); + } else { + return static_cast(later + (LAST - earlier) + 1); + } + } else { + if (later <= earlier) { + return -static_cast(earlier - later); + } else { + return -static_cast(earlier + (LAST - later) + 1); + } + } +#endif +} + +} // namespace talk_base diff --git a/talk/base/timeutils.h b/talk/base/timeutils.h new file mode 100644 index 000000000..545e86a12 --- /dev/null +++ b/talk/base/timeutils.h @@ -0,0 +1,98 @@ +/* + * libjingle + * Copyright 2005 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. + */ + +#ifndef TALK_BASE_TIMEUTILS_H_ +#define TALK_BASE_TIMEUTILS_H_ + +#include + +#include "talk/base/basictypes.h" + +namespace talk_base { + +static const int64 kNumMillisecsPerSec = INT64_C(1000); +static const int64 kNumMicrosecsPerSec = INT64_C(1000000); +static const int64 kNumNanosecsPerSec = INT64_C(1000000000); + +static const int64 kNumMicrosecsPerMillisec = kNumMicrosecsPerSec / + kNumMillisecsPerSec; +static const int64 kNumNanosecsPerMillisec = kNumNanosecsPerSec / + kNumMillisecsPerSec; + +// January 1970, in NTP milliseconds. +static const int64 kJan1970AsNtpMillisecs = INT64_C(2208988800000); + +typedef uint32 TimeStamp; + +// Returns the current time in milliseconds. +uint32 Time(); +// Returns the current time in nanoseconds. +uint64 TimeNanos(); + +// Stores current time in *tm and microseconds in *microseconds. +void CurrentTmTime(struct tm *tm, int *microseconds); + +// Returns a future timestamp, 'elapsed' milliseconds from now. +uint32 TimeAfter(int32 elapsed); + +// Comparisons between time values, which can wrap around. +bool TimeIsBetween(uint32 earlier, uint32 middle, uint32 later); // Inclusive +bool TimeIsLaterOrEqual(uint32 earlier, uint32 later); // Inclusive +bool TimeIsLater(uint32 earlier, uint32 later); // Exclusive + +// Returns the later of two timestamps. +inline uint32 TimeMax(uint32 ts1, uint32 ts2) { + return TimeIsLaterOrEqual(ts1, ts2) ? ts2 : ts1; +} + +// Returns the earlier of two timestamps. +inline uint32 TimeMin(uint32 ts1, uint32 ts2) { + return TimeIsLaterOrEqual(ts1, ts2) ? ts1 : ts2; +} + +// Number of milliseconds that would elapse between 'earlier' and 'later' +// timestamps. The value is negative if 'later' occurs before 'earlier'. +int32 TimeDiff(uint32 later, uint32 earlier); + +// The number of milliseconds that have elapsed since 'earlier'. +inline int32 TimeSince(uint32 earlier) { + return TimeDiff(Time(), earlier); +} + +// The number of milliseconds that will elapse between now and 'later'. +inline int32 TimeUntil(uint32 later) { + return TimeDiff(later, Time()); +} + +// Converts a unix timestamp in nanoseconds to an NTP timestamp in ms. +inline int64 UnixTimestampNanosecsToNtpMillisecs(int64 unix_ts_ns) { + return unix_ts_ns / kNumNanosecsPerMillisec + kJan1970AsNtpMillisecs; +} + +} // namespace talk_base + +#endif // TALK_BASE_TIMEUTILS_H_ diff --git a/talk/base/timeutils_unittest.cc b/talk/base/timeutils_unittest.cc new file mode 100644 index 000000000..c90f6a49f --- /dev/null +++ b/talk/base/timeutils_unittest.cc @@ -0,0 +1,163 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/common.h" +#include "talk/base/gunit.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" + +namespace talk_base { + +TEST(TimeTest, TimeInMs) { + uint32 ts_earlier = Time(); + Thread::SleepMs(100); + uint32 ts_now = Time(); + // Allow for the thread to wakeup ~20ms early. + EXPECT_GE(ts_now, ts_earlier + 80); + // Make sure the Time is not returning in smaller unit like microseconds. + EXPECT_LT(ts_now, ts_earlier + 1000); +} + +TEST(TimeTest, Comparison) { + // Obtain two different times, in known order + TimeStamp ts_earlier = Time(); + Thread::SleepMs(100); + TimeStamp ts_now = Time(); + EXPECT_NE(ts_earlier, ts_now); + + // Common comparisons + EXPECT_TRUE( TimeIsLaterOrEqual(ts_earlier, ts_now)); + EXPECT_TRUE( TimeIsLater( ts_earlier, ts_now)); + EXPECT_FALSE(TimeIsLaterOrEqual(ts_now, ts_earlier)); + EXPECT_FALSE(TimeIsLater( ts_now, ts_earlier)); + + // Edge cases + EXPECT_TRUE( TimeIsLaterOrEqual(ts_earlier, ts_earlier)); + EXPECT_FALSE(TimeIsLater( ts_earlier, ts_earlier)); + + // Obtain a third time + TimeStamp ts_later = TimeAfter(100); + EXPECT_NE(ts_now, ts_later); + EXPECT_TRUE( TimeIsLater(ts_now, ts_later)); + EXPECT_TRUE( TimeIsLater(ts_earlier, ts_later)); + + // Common comparisons + EXPECT_TRUE( TimeIsBetween(ts_earlier, ts_now, ts_later)); + EXPECT_FALSE(TimeIsBetween(ts_earlier, ts_later, ts_now)); + EXPECT_FALSE(TimeIsBetween(ts_now, ts_earlier, ts_later)); + EXPECT_TRUE( TimeIsBetween(ts_now, ts_later, ts_earlier)); + EXPECT_TRUE( TimeIsBetween(ts_later, ts_earlier, ts_now)); + EXPECT_FALSE(TimeIsBetween(ts_later, ts_now, ts_earlier)); + + // Edge cases + EXPECT_TRUE( TimeIsBetween(ts_earlier, ts_earlier, ts_earlier)); + EXPECT_TRUE( TimeIsBetween(ts_earlier, ts_earlier, ts_later)); + EXPECT_TRUE( TimeIsBetween(ts_earlier, ts_later, ts_later)); + + // Earlier of two times + EXPECT_EQ(ts_earlier, TimeMin(ts_earlier, ts_earlier)); + EXPECT_EQ(ts_earlier, TimeMin(ts_earlier, ts_now)); + EXPECT_EQ(ts_earlier, TimeMin(ts_earlier, ts_later)); + EXPECT_EQ(ts_earlier, TimeMin(ts_now, ts_earlier)); + EXPECT_EQ(ts_earlier, TimeMin(ts_later, ts_earlier)); + + // Later of two times + EXPECT_EQ(ts_earlier, TimeMax(ts_earlier, ts_earlier)); + EXPECT_EQ(ts_now, TimeMax(ts_earlier, ts_now)); + EXPECT_EQ(ts_later, TimeMax(ts_earlier, ts_later)); + EXPECT_EQ(ts_now, TimeMax(ts_now, ts_earlier)); + EXPECT_EQ(ts_later, TimeMax(ts_later, ts_earlier)); +} + +TEST(TimeTest, Intervals) { + TimeStamp ts_earlier = Time(); + TimeStamp ts_later = TimeAfter(500); + + // We can't depend on ts_later and ts_earlier to be exactly 500 apart + // since time elapses between the calls to Time() and TimeAfter(500) + EXPECT_LE(500, TimeDiff(ts_later, ts_earlier)); + EXPECT_GE(-500, TimeDiff(ts_earlier, ts_later)); + + // Time has elapsed since ts_earlier + EXPECT_GE(TimeSince(ts_earlier), 0); + + // ts_earlier is earlier than now, so TimeUntil ts_earlier is -ve + EXPECT_LE(TimeUntil(ts_earlier), 0); + + // ts_later likely hasn't happened yet, so TimeSince could be -ve + // but within 500 + EXPECT_GE(TimeSince(ts_later), -500); + + // TimeUntil ts_later is at most 500 + EXPECT_LE(TimeUntil(ts_later), 500); +} + +TEST(TimeTest, BoundaryComparison) { + // Obtain two different times, in known order + TimeStamp ts_earlier = static_cast(-50); + TimeStamp ts_later = ts_earlier + 100; + EXPECT_NE(ts_earlier, ts_later); + + // Common comparisons + EXPECT_TRUE( TimeIsLaterOrEqual(ts_earlier, ts_later)); + EXPECT_TRUE( TimeIsLater( ts_earlier, ts_later)); + EXPECT_FALSE(TimeIsLaterOrEqual(ts_later, ts_earlier)); + EXPECT_FALSE(TimeIsLater( ts_later, ts_earlier)); + + // Earlier of two times + EXPECT_EQ(ts_earlier, TimeMin(ts_earlier, ts_earlier)); + EXPECT_EQ(ts_earlier, TimeMin(ts_earlier, ts_later)); + EXPECT_EQ(ts_earlier, TimeMin(ts_later, ts_earlier)); + + // Later of two times + EXPECT_EQ(ts_earlier, TimeMax(ts_earlier, ts_earlier)); + EXPECT_EQ(ts_later, TimeMax(ts_earlier, ts_later)); + EXPECT_EQ(ts_later, TimeMax(ts_later, ts_earlier)); + + // Interval + EXPECT_EQ(100, TimeDiff(ts_later, ts_earlier)); + EXPECT_EQ(-100, TimeDiff(ts_earlier, ts_later)); +} + +TEST(TimeTest, CurrentTmTime) { + struct tm tm; + int microseconds; + + time_t before = ::time(NULL); + CurrentTmTime(&tm, µseconds); + time_t after = ::time(NULL); + + // Assert that 'tm' represents a time between 'before' and 'after'. + // mktime() uses local time, so we have to compensate for that. + time_t local_delta = before - ::mktime(::gmtime(&before)); // NOLINT + time_t t = ::mktime(&tm) + local_delta; + + EXPECT_TRUE(before <= t && t <= after); + EXPECT_TRUE(0 <= microseconds && microseconds < 1000000); +} + +} // namespace talk_base diff --git a/talk/base/timing.cc b/talk/base/timing.cc new file mode 100644 index 000000000..4df9f1f34 --- /dev/null +++ b/talk/base/timing.cc @@ -0,0 +1,129 @@ +/* + * libjingle + * Copyright 2008, 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/base/timing.h" +#include "talk/base/timeutils.h" + +#if defined(POSIX) +#include +#include +#include +#if defined(OSX) +#include +#include +#endif +#elif defined(WIN32) +#include +#include "talk/base/win32.h" +#endif + +namespace talk_base { + +Timing::Timing() { +#if defined(WIN32) + // This may fail, but we handle failure gracefully in the methods + // that use it (use alternative sleep method). + // + // TODO: Make it possible for user to tell if IdleWait will + // be done at lesser resolution because of this. + timer_handle_ = CreateWaitableTimer(NULL, // Security attributes. + FALSE, // Manual reset? + NULL); // Timer name. +#endif +} + +Timing::~Timing() { +#if defined(WIN32) + if (timer_handle_ != NULL) + CloseHandle(timer_handle_); +#endif +} + +double Timing::WallTimeNow() { +#if defined(POSIX) + struct timeval time; + gettimeofday(&time, NULL); + // Convert from second (1.0) and microsecond (1e-6). + return (static_cast(time.tv_sec) + + static_cast(time.tv_usec) * 1.0e-6); + +#elif defined(WIN32) + struct _timeb time; + _ftime(&time); + // Convert from second (1.0) and milliseconds (1e-3). + return (static_cast(time.time) + + static_cast(time.millitm) * 1.0e-3); +#endif +} + +double Timing::TimerNow() { + return (static_cast(TimeNanos()) / kNumNanosecsPerSec); +} + +double Timing::BusyWait(double period) { + double start_time = TimerNow(); + while (TimerNow() - start_time < period) { + } + return TimerNow() - start_time; +} + +double Timing::IdleWait(double period) { + double start_time = TimerNow(); + +#if defined(POSIX) + double sec_int, sec_frac = modf(period, &sec_int); + struct timespec ts; + ts.tv_sec = static_cast(sec_int); + ts.tv_nsec = static_cast(sec_frac * 1.0e9); // NOLINT + + // NOTE(liulk): for the NOLINT above, long is the appropriate POSIX + // type. + + // POSIX nanosleep may be interrupted by signals. + while (nanosleep(&ts, &ts) == -1 && errno == EINTR) { + } + +#elif defined(WIN32) + if (timer_handle_ != NULL) { + LARGE_INTEGER due_time; + + // Negative indicates relative time. The unit is 100 nanoseconds. + due_time.QuadPart = -LONGLONG(period * 1.0e7); + + SetWaitableTimer(timer_handle_, &due_time, 0, NULL, NULL, TRUE); + WaitForSingleObject(timer_handle_, INFINITE); + } else { + // Still attempts to sleep with lesser resolution. + // The unit is in milliseconds. + Sleep(DWORD(period * 1.0e3)); + } +#endif + + return TimerNow() - start_time; +} + +} // namespace talk_base diff --git a/talk/base/timing.h b/talk/base/timing.h new file mode 100644 index 000000000..f2bf0130b --- /dev/null +++ b/talk/base/timing.h @@ -0,0 +1,76 @@ +/* + * libjingle + * Copyright 2008, 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. + */ + +#ifndef TALK_BASE_TIMING_H_ +#define TALK_BASE_TIMING_H_ + +#if defined(WIN32) +#include "talk/base/win32.h" +#endif + +namespace talk_base { + +class Timing { + public: + Timing(); + virtual ~Timing(); + + // WallTimeNow() returns the current wall-clock time in seconds, + // within 10 milliseconds resolution. + virtual double WallTimeNow(); + + // TimerNow() is like WallTimeNow(), but is monotonically + // increasing. It returns seconds in resolution of 10 microseconds + // or better. Although timer and wall-clock time have the same + // timing unit, they do not necessarily correlate because wall-clock + // time may be adjusted backwards, hence not monotonic. + // Made virtual so we can make a fake one. + virtual double TimerNow(); + + // BusyWait() exhausts CPU as long as the time elapsed is less than + // the specified interval in seconds. Returns the actual waiting + // time based on TimerNow() measurement. + double BusyWait(double period); + + // IdleWait() relinquishes control of CPU for specified period in + // seconds. It uses highest resolution sleep mechanism as possible, + // but does not otherwise guarantee the accuracy. Returns the + // actual waiting time based on TimerNow() measurement. + // + // This function is not re-entrant for an object. Create a fresh + // Timing object for each thread. + double IdleWait(double period); + + private: +#if defined(WIN32) + HANDLE timer_handle_; +#endif +}; + +} // namespace talk_base + +#endif // TALK_BASE_TIMING_H_ diff --git a/talk/base/transformadapter.cc b/talk/base/transformadapter.cc new file mode 100644 index 000000000..53a55a85b --- /dev/null +++ b/talk/base/transformadapter.cc @@ -0,0 +1,202 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/transformadapter.h" + +#include + +#include "talk/base/common.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// + +TransformAdapter::TransformAdapter(StreamInterface * stream, + TransformInterface * transform, + bool direction_read) + : StreamAdapterInterface(stream), transform_(transform), + direction_read_(direction_read), state_(ST_PROCESSING), len_(0) { +} + +TransformAdapter::~TransformAdapter() { + TransformAdapter::Close(); + delete transform_; +} + +StreamResult +TransformAdapter::Read(void * buffer, size_t buffer_len, + size_t * read, int * error) { + if (!direction_read_) + return SR_EOS; + + while (state_ != ST_ERROR) { + if (state_ == ST_COMPLETE) + return SR_EOS; + + // Buffer more data + if ((state_ == ST_PROCESSING) && (len_ < sizeof(buffer_))) { + size_t subread; + StreamResult result = StreamAdapterInterface::Read( + buffer_ + len_, + sizeof(buffer_) - len_, + &subread, + &error_); + if (result == SR_BLOCK) { + return SR_BLOCK; + } else if (result == SR_ERROR) { + state_ = ST_ERROR; + break; + } else if (result == SR_EOS) { + state_ = ST_FLUSHING; + } else { + len_ += subread; + } + } + + // Process buffered data + size_t in_len = len_; + size_t out_len = buffer_len; + StreamResult result = transform_->Transform(buffer_, &in_len, + buffer, &out_len, + (state_ == ST_FLUSHING)); + ASSERT(result != SR_BLOCK); + if (result == SR_EOS) { + // Note: Don't signal SR_EOS this iteration, unless out_len is zero + state_ = ST_COMPLETE; + } else if (result == SR_ERROR) { + state_ = ST_ERROR; + error_ = -1; // TODO: propagate error + break; + } else if ((out_len == 0) && (state_ == ST_FLUSHING)) { + // If there is no output AND no more input, then something is wrong + state_ = ST_ERROR; + error_ = -1; // TODO: better error code? + break; + } + + len_ -= in_len; + if (len_ > 0) + memmove(buffer_, buffer_ + in_len, len_); + + if (out_len == 0) + continue; + + if (read) + *read = out_len; + return SR_SUCCESS; + } + + if (error) + *error = error_; + return SR_ERROR; +} + +StreamResult +TransformAdapter::Write(const void * data, size_t data_len, + size_t * written, int * error) { + if (direction_read_) + return SR_EOS; + + size_t bytes_written = 0; + while (state_ != ST_ERROR) { + if (state_ == ST_COMPLETE) + return SR_EOS; + + if (len_ < sizeof(buffer_)) { + // Process buffered data + size_t in_len = data_len; + size_t out_len = sizeof(buffer_) - len_; + StreamResult result = transform_->Transform(data, &in_len, + buffer_ + len_, &out_len, + (state_ == ST_FLUSHING)); + + ASSERT(result != SR_BLOCK); + if (result == SR_EOS) { + // Note: Don't signal SR_EOS this iteration, unless no data written + state_ = ST_COMPLETE; + } else if (result == SR_ERROR) { + ASSERT(false); // When this happens, think about what should be done + state_ = ST_ERROR; + error_ = -1; // TODO: propagate error + break; + } + + len_ = out_len; + bytes_written = in_len; + } + + size_t pos = 0; + while (pos < len_) { + size_t subwritten; + StreamResult result = StreamAdapterInterface::Write(buffer_ + pos, + len_ - pos, + &subwritten, + &error_); + if (result == SR_BLOCK) { + ASSERT(false); // TODO: we should handle this + return SR_BLOCK; + } else if (result == SR_ERROR) { + state_ = ST_ERROR; + break; + } else if (result == SR_EOS) { + state_ = ST_COMPLETE; + break; + } + + pos += subwritten; + } + + len_ -= pos; + if (len_ > 0) + memmove(buffer_, buffer_ + pos, len_); + + if (bytes_written == 0) + continue; + + if (written) + *written = bytes_written; + return SR_SUCCESS; + } + + if (error) + *error = error_; + return SR_ERROR; +} + +void +TransformAdapter::Close() { + if (!direction_read_ && (state_ == ST_PROCESSING)) { + state_ = ST_FLUSHING; + do { + Write(0, 0, NULL, NULL); + } while (state_ == ST_FLUSHING); + } + state_ = ST_COMPLETE; + StreamAdapterInterface::Close(); +} + +} // namespace talk_base diff --git a/talk/base/transformadapter.h b/talk/base/transformadapter.h new file mode 100644 index 000000000..e96a13d2e --- /dev/null +++ b/talk/base/transformadapter.h @@ -0,0 +1,97 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_TRANSFORMADAPTER_H__ +#define TALK_BASE_TRANSFORMADAPTER_H__ + +#include "talk/base/stream.h" + +namespace talk_base { +/////////////////////////////////////////////////////////////////////////////// + +class TransformInterface { +public: + virtual ~TransformInterface() { } + + // Transform should convert the in_len bytes of input into the out_len-sized + // output buffer. If flush is true, there will be no more data following + // input. + // After the transformation, in_len contains the number of bytes consumed, and + // out_len contains the number of bytes ready in output. + // Note: Transform should not return SR_BLOCK, as there is no asynchronous + // notification available. + virtual StreamResult Transform(const void * input, size_t * in_len, + void * output, size_t * out_len, + bool flush) = 0; +}; + +/////////////////////////////////////////////////////////////////////////////// + +// TransformAdapter causes all data passed through to be transformed by the +// supplied TransformInterface object, which may apply compression, encryption, +// etc. + +class TransformAdapter : public StreamAdapterInterface { +public: + // Note that the transformation is unidirectional, in the direction specified + // by the constructor. Operations in the opposite direction result in SR_EOS. + TransformAdapter(StreamInterface * stream, + TransformInterface * transform, + bool direction_read); + virtual ~TransformAdapter(); + + virtual StreamResult Read(void * buffer, size_t buffer_len, + size_t * read, int * error); + virtual StreamResult Write(const void * data, size_t data_len, + size_t * written, int * error); + virtual void Close(); + + // Apriori, we can't tell what the transformation does to the stream length. + virtual bool GetAvailable(size_t* size) const { return false; } + virtual bool ReserveSize(size_t size) { return true; } + + // Transformations might not be restartable + virtual bool Rewind() { return false; } + +private: + enum State { ST_PROCESSING, ST_FLUSHING, ST_COMPLETE, ST_ERROR }; + enum { BUFFER_SIZE = 1024 }; + + TransformInterface * transform_; + bool direction_read_; + State state_; + int error_; + + char buffer_[BUFFER_SIZE]; + size_t len_; +}; + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_TRANSFORMADAPTER_H__ diff --git a/talk/base/unittest_main.cc b/talk/base/unittest_main.cc new file mode 100644 index 000000000..bca3671b0 --- /dev/null +++ b/talk/base/unittest_main.cc @@ -0,0 +1,118 @@ +// Copyright 2007 Google Inc. All Rights Reserved. + +// juberti@google.com (Justin Uberti) +// +// A reuseable entry point for gunit tests. + +#ifdef WIN32 +#include +#endif + +#include "talk/base/flags.h" +#include "talk/base/fileutils.h" +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/pathutils.h" + +DEFINE_bool(help, false, "prints this message"); +DEFINE_string(log, "", "logging options to use"); +#ifdef WIN32 +DEFINE_int(crt_break_alloc, -1, "memory allocation to break on"); +DEFINE_bool(default_error_handlers, false, + "leave the default exception/dbg handler functions in place"); + +void TestInvalidParameterHandler(const wchar_t* expression, + const wchar_t* function, + const wchar_t* file, + unsigned int line, + uintptr_t pReserved) { + LOG(LS_ERROR) << "InvalidParameter Handler called. Exiting."; + LOG(LS_ERROR) << expression << std::endl << function << std::endl << file + << std::endl << line; + exit(1); +} +void TestPureCallHandler() { + LOG(LS_ERROR) << "Purecall Handler called. Exiting."; + exit(1); +} +int TestCrtReportHandler(int report_type, char* msg, int* retval) { + LOG(LS_ERROR) << "CrtReport Handler called..."; + LOG(LS_ERROR) << msg; + if (report_type == _CRT_ASSERT) { + exit(1); + } else { + *retval = 0; + return TRUE; + } +} +#endif // WIN32 + +talk_base::Pathname GetTalkDirectory() { + // Locate talk directory. + talk_base::Pathname path = talk_base::Filesystem::GetCurrentDirectory(); + std::string talk_folder_name("talk"); + talk_folder_name += path.folder_delimiter(); + while (path.folder_name() != talk_folder_name && !path.empty()) { + path.SetFolder(path.parent_folder()); + } + + // If not running inside "talk" folder, then assume running in its parent + // folder. + if (path.empty()) { + path = talk_base::Filesystem::GetCurrentDirectory(); + path.AppendFolder("talk"); + // Make sure the folder exist. + if (!talk_base::Filesystem::IsFolder(path)) { + path.clear(); + } + } + return path; +} + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + FlagList::SetFlagsFromCommandLine(&argc, argv, false); + if (FLAG_help) { + FlagList::Print(NULL, false); + return 0; + } + +#ifdef WIN32 + if (!FLAG_default_error_handlers) { + // Make sure any errors don't throw dialogs hanging the test run. + _set_invalid_parameter_handler(TestInvalidParameterHandler); + _set_purecall_handler(TestPureCallHandler); + _CrtSetReportHook2(_CRT_RPTHOOK_INSTALL, TestCrtReportHandler); + } + +#ifdef _DEBUG // Turn on memory leak checking on Windows. + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF |_CRTDBG_LEAK_CHECK_DF); + if (FLAG_crt_break_alloc >= 0) { + _crtBreakAlloc = FLAG_crt_break_alloc; + } +#endif // _DEBUG +#endif // WIN32 + + talk_base::Filesystem::SetOrganizationName("google"); + talk_base::Filesystem::SetApplicationName("unittest"); + + // By default, log timestamps. Allow overrides by used of a --log flag. + talk_base::LogMessage::LogTimestamps(); + if (*FLAG_log != '\0') { + talk_base::LogMessage::ConfigureLogging(FLAG_log, "unittest.log"); + } + + int res = RUN_ALL_TESTS(); + + // clean up logging so we don't appear to leak memory. + talk_base::LogMessage::ConfigureLogging("", ""); + +#ifdef WIN32 + // Unhook crt function so that we don't ever log after statics have been + // uninitialized. + if (!FLAG_default_error_handlers) + _CrtSetReportHook2(_CRT_RPTHOOK_REMOVE, TestCrtReportHandler); +#endif + + return res; +} diff --git a/talk/base/unixfilesystem.cc b/talk/base/unixfilesystem.cc new file mode 100644 index 000000000..74168f267 --- /dev/null +++ b/talk/base/unixfilesystem.cc @@ -0,0 +1,546 @@ +/* + * 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/base/unixfilesystem.h" + +#include +#include +#include +#include +#include + +#ifdef OSX +#include +#include +#include +#include "talk/base/macutils.h" +#endif // OSX + +#if defined(POSIX) && !defined(OSX) +#include +#ifdef ANDROID +#include +#else +#include +#endif // ANDROID +#include +#include +#include +#endif // POSIX && !OSX + +#ifdef LINUX +#include +#include +#endif + +#include "talk/base/fileutils.h" +#include "talk/base/pathutils.h" +#include "talk/base/stream.h" +#include "talk/base/stringutils.h" + +namespace talk_base { + +#if !defined(ANDROID) && !defined(IOS) +char* UnixFilesystem::app_temp_path_ = NULL; +#else +char* UnixFilesystem::provided_app_data_folder_ = NULL; +char* UnixFilesystem::provided_app_temp_folder_ = NULL; + +void UnixFilesystem::SetAppDataFolder(const std::string& folder) { + delete [] provided_app_data_folder_; + provided_app_data_folder_ = CopyString(folder); +} + +void UnixFilesystem::SetAppTempFolder(const std::string& folder) { + delete [] provided_app_temp_folder_; + provided_app_temp_folder_ = CopyString(folder); +} +#endif + +bool UnixFilesystem::CreateFolder(const Pathname &path, mode_t mode) { + std::string pathname(path.pathname()); + int len = pathname.length(); + if ((len == 0) || (pathname[len - 1] != '/')) + return false; + + struct stat st; + int res = ::stat(pathname.c_str(), &st); + if (res == 0) { + // Something exists at this location, check if it is a directory + return S_ISDIR(st.st_mode) != 0; + } else if (errno != ENOENT) { + // Unexpected error + return false; + } + + // Directory doesn't exist, look up one directory level + do { + --len; + } while ((len > 0) && (pathname[len - 1] != '/')); + + if (!CreateFolder(Pathname(pathname.substr(0, len)), mode)) { + return false; + } + + LOG(LS_INFO) << "Creating folder: " << pathname; + return (0 == ::mkdir(pathname.c_str(), mode)); +} + +bool UnixFilesystem::CreateFolder(const Pathname &path) { + return CreateFolder(path, 0755); +} + +FileStream *UnixFilesystem::OpenFile(const Pathname &filename, + const std::string &mode) { + FileStream *fs = new FileStream(); + if (fs && !fs->Open(filename.pathname().c_str(), mode.c_str(), NULL)) { + delete fs; + fs = NULL; + } + return fs; +} + +bool UnixFilesystem::CreatePrivateFile(const Pathname &filename) { + int fd = open(filename.pathname().c_str(), + O_RDWR | O_CREAT | O_EXCL, + S_IRUSR | S_IWUSR); + if (fd < 0) { + LOG_ERR(LS_ERROR) << "open() failed."; + return false; + } + // Don't need to keep the file descriptor. + if (close(fd) < 0) { + LOG_ERR(LS_ERROR) << "close() failed."; + // Continue. + } + return true; +} + +bool UnixFilesystem::DeleteFile(const Pathname &filename) { + LOG(LS_INFO) << "Deleting file:" << filename.pathname(); + + if (!IsFile(filename)) { + ASSERT(IsFile(filename)); + return false; + } + return ::unlink(filename.pathname().c_str()) == 0; +} + +bool UnixFilesystem::DeleteEmptyFolder(const Pathname &folder) { + LOG(LS_INFO) << "Deleting folder" << folder.pathname(); + + if (!IsFolder(folder)) { + ASSERT(IsFolder(folder)); + return false; + } + std::string no_slash(folder.pathname(), 0, folder.pathname().length()-1); + return ::rmdir(no_slash.c_str()) == 0; +} + +bool UnixFilesystem::GetTemporaryFolder(Pathname &pathname, bool create, + const std::string *append) { +#ifdef OSX + FSRef fr; + if (0 != FSFindFolder(kOnAppropriateDisk, kTemporaryFolderType, + kCreateFolder, &fr)) + return false; + unsigned char buffer[NAME_MAX+1]; + if (0 != FSRefMakePath(&fr, buffer, ARRAY_SIZE(buffer))) + return false; + pathname.SetPathname(reinterpret_cast(buffer), ""); +#elif defined(ANDROID) || defined(IOS) + ASSERT(provided_app_temp_folder_ != NULL); + pathname.SetPathname(provided_app_temp_folder_, ""); +#else // !OSX && !ANDROID + if (const char* tmpdir = getenv("TMPDIR")) { + pathname.SetPathname(tmpdir, ""); + } else if (const char* tmp = getenv("TMP")) { + pathname.SetPathname(tmp, ""); + } else { +#ifdef P_tmpdir + pathname.SetPathname(P_tmpdir, ""); +#else // !P_tmpdir + pathname.SetPathname("/tmp/", ""); +#endif // !P_tmpdir + } +#endif // !OSX && !ANDROID + if (append) { + ASSERT(!append->empty()); + pathname.AppendFolder(*append); + } + return !create || CreateFolder(pathname); +} + +std::string UnixFilesystem::TempFilename(const Pathname &dir, + const std::string &prefix) { + int len = dir.pathname().size() + prefix.size() + 2 + 6; + char *tempname = new char[len]; + + snprintf(tempname, len, "%s/%sXXXXXX", dir.pathname().c_str(), + prefix.c_str()); + int fd = ::mkstemp(tempname); + if (fd != -1) + ::close(fd); + std::string ret(tempname); + delete[] tempname; + + return ret; +} + +bool UnixFilesystem::MoveFile(const Pathname &old_path, + const Pathname &new_path) { + if (!IsFile(old_path)) { + ASSERT(IsFile(old_path)); + return false; + } + LOG(LS_VERBOSE) << "Moving " << old_path.pathname() + << " to " << new_path.pathname(); + if (rename(old_path.pathname().c_str(), new_path.pathname().c_str()) != 0) { + if (errno != EXDEV) + return false; + if (!CopyFile(old_path, new_path)) + return false; + if (!DeleteFile(old_path)) + return false; + } + return true; +} + +bool UnixFilesystem::MoveFolder(const Pathname &old_path, + const Pathname &new_path) { + if (!IsFolder(old_path)) { + ASSERT(IsFolder(old_path)); + return false; + } + LOG(LS_VERBOSE) << "Moving " << old_path.pathname() + << " to " << new_path.pathname(); + if (rename(old_path.pathname().c_str(), new_path.pathname().c_str()) != 0) { + if (errno != EXDEV) + return false; + if (!CopyFolder(old_path, new_path)) + return false; + if (!DeleteFolderAndContents(old_path)) + return false; + } + return true; +} + +bool UnixFilesystem::IsFolder(const Pathname &path) { + struct stat st; + if (stat(path.pathname().c_str(), &st) < 0) + return false; + return S_ISDIR(st.st_mode); +} + +bool UnixFilesystem::CopyFile(const Pathname &old_path, + const Pathname &new_path) { + LOG(LS_VERBOSE) << "Copying " << old_path.pathname() + << " to " << new_path.pathname(); + char buf[256]; + size_t len; + + StreamInterface *source = OpenFile(old_path, "rb"); + if (!source) + return false; + + StreamInterface *dest = OpenFile(new_path, "wb"); + if (!dest) { + delete source; + return false; + } + + while (source->Read(buf, sizeof(buf), &len, NULL) == SR_SUCCESS) + dest->Write(buf, len, NULL, NULL); + + delete source; + delete dest; + return true; +} + +bool UnixFilesystem::IsTemporaryPath(const Pathname& pathname) { +#if defined(ANDROID) || defined(IOS) + ASSERT(provided_app_temp_folder_ != NULL); +#endif + + const char* const kTempPrefixes[] = { +#if defined(ANDROID) || defined(IOS) + provided_app_temp_folder_, +#else + "/tmp/", "/var/tmp/", +#ifdef OSX + "/private/tmp/", "/private/var/tmp/", "/private/var/folders/", +#endif // OSX +#endif // ANDROID || IOS + }; + for (size_t i = 0; i < ARRAY_SIZE(kTempPrefixes); ++i) { + if (0 == strncmp(pathname.pathname().c_str(), kTempPrefixes[i], + strlen(kTempPrefixes[i]))) + return true; + } + return false; +} + +bool UnixFilesystem::IsFile(const Pathname& pathname) { + struct stat st; + int res = ::stat(pathname.pathname().c_str(), &st); + // Treat symlinks, named pipes, etc. all as files. + return res == 0 && !S_ISDIR(st.st_mode); +} + +bool UnixFilesystem::IsAbsent(const Pathname& pathname) { + struct stat st; + int res = ::stat(pathname.pathname().c_str(), &st); + // Note: we specifically maintain ENOTDIR as an error, because that implies + // that you could not call CreateFolder(pathname). + return res != 0 && ENOENT == errno; +} + +bool UnixFilesystem::GetFileSize(const Pathname& pathname, size_t *size) { + struct stat st; + if (::stat(pathname.pathname().c_str(), &st) != 0) + return false; + *size = st.st_size; + return true; +} + +bool UnixFilesystem::GetFileTime(const Pathname& path, FileTimeType which, + time_t* time) { + struct stat st; + if (::stat(path.pathname().c_str(), &st) != 0) + return false; + switch (which) { + case FTT_CREATED: + *time = st.st_ctime; + break; + case FTT_MODIFIED: + *time = st.st_mtime; + break; + case FTT_ACCESSED: + *time = st.st_atime; + break; + default: + return false; + } + return true; +} + +bool UnixFilesystem::GetAppPathname(Pathname* path) { +#ifdef OSX + ProcessSerialNumber psn = { 0, kCurrentProcess }; + CFDictionaryRef procinfo = ProcessInformationCopyDictionary(&psn, + kProcessDictionaryIncludeAllInformationMask); + if (NULL == procinfo) + return false; + CFStringRef cfpath = (CFStringRef) CFDictionaryGetValue(procinfo, + kIOBundleExecutableKey); + std::string path8; + bool success = ToUtf8(cfpath, &path8); + CFRelease(procinfo); + if (success) + path->SetPathname(path8); + return success; +#else // OSX + char buffer[NAME_MAX+1]; + size_t len = readlink("/proc/self/exe", buffer, ARRAY_SIZE(buffer) - 1); + if (len <= 0) + return false; + buffer[len] = '\0'; + path->SetPathname(buffer); + return true; +#endif // OSX +} + +bool UnixFilesystem::GetAppDataFolder(Pathname* path, bool per_user) { + ASSERT(!organization_name_.empty()); + ASSERT(!application_name_.empty()); + + // First get the base directory for app data. +#ifdef OSX + if (per_user) { + // Use ~/Library/Application Support/// + FSRef fr; + if (0 != FSFindFolder(kUserDomain, kApplicationSupportFolderType, + kCreateFolder, &fr)) + return false; + unsigned char buffer[NAME_MAX+1]; + if (0 != FSRefMakePath(&fr, buffer, ARRAY_SIZE(buffer))) + return false; + path->SetPathname(reinterpret_cast(buffer), ""); + } else { + // TODO + return false; + } +#elif defined(ANDROID) || defined(IOS) // && !OSX + ASSERT(provided_app_data_folder_ != NULL); + path->SetPathname(provided_app_data_folder_, ""); +#elif defined(LINUX) // && !OSX && !defined(ANDROID) && !defined(IOS) + if (per_user) { + // We follow the recommendations in + // http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + // It specifies separate directories for data and config files, but + // GetAppDataFolder() does not distinguish. We just return the config dir + // path. + const char* xdg_config_home = getenv("XDG_CONFIG_HOME"); + if (xdg_config_home) { + path->SetPathname(xdg_config_home, ""); + } else { + // XDG says to default to $HOME/.config. We also support falling back to + // other synonyms for HOME if for some reason it is not defined. + const char* homedir; + if (const char* home = getenv("HOME")) { + homedir = home; + } else if (const char* dotdir = getenv("DOTDIR")) { + homedir = dotdir; + } else if (passwd* pw = getpwuid(geteuid())) { + homedir = pw->pw_dir; + } else { + return false; + } + path->SetPathname(homedir, ""); + path->AppendFolder(".config"); + } + } else { + // XDG does not define a standard directory for writable global data. Let's + // just use this. + path->SetPathname("/var/cache/", ""); + } +#endif // !OSX && !defined(ANDROID) && !defined(LINUX) + + // Now add on a sub-path for our app. +#if defined(OSX) || defined(ANDROID) || defined(IOS) + path->AppendFolder(organization_name_); + path->AppendFolder(application_name_); +#elif defined(LINUX) + // XDG says to use a single directory level, so we concatenate the org and app + // name with a hyphen. We also do the Linuxy thing and convert to all + // lowercase with no spaces. + std::string subdir(organization_name_); + subdir.append("-"); + subdir.append(application_name_); + replace_substrs(" ", 1, "", 0, &subdir); + std::transform(subdir.begin(), subdir.end(), subdir.begin(), ::tolower); + path->AppendFolder(subdir); +#endif + if (!CreateFolder(*path, 0700)) { + return false; + } + // If the folder already exists, it may have the wrong mode or be owned by + // someone else, both of which are security problems. Setting the mode + // avoids both issues since it will fail if the path is not owned by us. + if (0 != ::chmod(path->pathname().c_str(), 0700)) { + LOG_ERR(LS_ERROR) << "Can't set mode on " << path; + return false; + } + return true; +} + +bool UnixFilesystem::GetAppTempFolder(Pathname* path) { +#if defined(ANDROID) || defined(IOS) + ASSERT(provided_app_temp_folder_ != NULL); + path->SetPathname(provided_app_temp_folder_); + return true; +#else + ASSERT(!application_name_.empty()); + // TODO: Consider whether we are worried about thread safety. + if (app_temp_path_ != NULL && strlen(app_temp_path_) > 0) { + path->SetPathname(app_temp_path_); + return true; + } + + // Create a random directory as /tmp/-- + char buffer[128]; + sprintfn(buffer, ARRAY_SIZE(buffer), "-%d-%d", + static_cast(getpid()), + static_cast(time(0))); + std::string folder(application_name_); + folder.append(buffer); + if (!GetTemporaryFolder(*path, true, &folder)) + return false; + + delete [] app_temp_path_; + app_temp_path_ = CopyString(path->pathname()); + // TODO: atexit(DeleteFolderAndContents(app_temp_path_)); + return true; +#endif +} + +bool UnixFilesystem::GetDiskFreeSpace(const Pathname& path, int64 *freebytes) { + ASSERT(NULL != freebytes); + // TODO: Consider making relative paths absolute using cwd. + // TODO: When popping off a symlink, push back on the components of the + // symlink, so we don't jump out of the target disk inadvertently. + Pathname existing_path(path.folder(), ""); + while (!existing_path.folder().empty() && IsAbsent(existing_path)) { + existing_path.SetFolder(existing_path.parent_folder()); + } +#ifdef ANDROID + struct statfs vfs; + memset(&vfs, 0, sizeof(vfs)); + if (0 != statfs(existing_path.pathname().c_str(), &vfs)) + return false; +#else + struct statvfs vfs; + memset(&vfs, 0, sizeof(vfs)); + if (0 != statvfs(existing_path.pathname().c_str(), &vfs)) + return false; +#endif // ANDROID +#if defined(LINUX) || defined(ANDROID) + *freebytes = static_cast(vfs.f_bsize) * vfs.f_bavail; +#elif defined(OSX) + *freebytes = static_cast(vfs.f_frsize) * vfs.f_bavail; +#endif + + return true; +} + +Pathname UnixFilesystem::GetCurrentDirectory() { + Pathname cwd; + char buffer[PATH_MAX]; + char *path = getcwd(buffer, PATH_MAX); + + if (!path) { + LOG_ERR(LS_ERROR) << "getcwd() failed"; + return cwd; // returns empty pathname + } + cwd.SetFolder(std::string(path)); + + return cwd; +} + +char* UnixFilesystem::CopyString(const std::string& str) { + size_t size = str.length() + 1; + + char* buf = new char[size]; + if (!buf) { + return NULL; + } + + strcpyn(buf, size, str.c_str()); + return buf; +} + +} // namespace talk_base diff --git a/talk/base/unixfilesystem.h b/talk/base/unixfilesystem.h new file mode 100644 index 000000000..aa9c920e6 --- /dev/null +++ b/talk/base/unixfilesystem.h @@ -0,0 +1,139 @@ +/* + * 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. + */ + +#ifndef TALK_BASE_UNIXFILESYSTEM_H_ +#define TALK_BASE_UNIXFILESYSTEM_H_ + +#include + +#include "talk/base/fileutils.h" + +namespace talk_base { + +class UnixFilesystem : public FilesystemInterface { + public: + +#if defined(ANDROID) || defined(IOS) +// Android does not have a native code API to fetch the app data or temp +// folders. That needs to be passed into this class from Java. Similarly, iOS +// only supports an Objective-C API for fetching the folder locations, so that +// needs to be passed in here from Objective-C. + + static void SetAppDataFolder(const std::string& folder); + static void SetAppTempFolder(const std::string& folder); +#endif + + // Opens a file. Returns an open StreamInterface if function succeeds. + // Otherwise, returns NULL. + virtual FileStream *OpenFile(const Pathname &filename, + const std::string &mode); + + // Atomically creates an empty file accessible only to the current user if one + // does not already exist at the given path, otherwise fails. + virtual bool CreatePrivateFile(const Pathname &filename); + + // This will attempt to delete the file located at filename. + // It will fail with VERIY if you pass it a non-existant file, or a directory. + virtual bool DeleteFile(const Pathname &filename); + + // This will attempt to delete the folder located at 'folder' + // It ASSERTs and returns false if you pass it a non-existant folder or a + // plain file. + virtual bool DeleteEmptyFolder(const Pathname &folder); + + // Creates a directory. This will call itself recursively to create /foo/bar + // even if /foo does not exist. All created directories are created with the + // given mode. + // Returns TRUE if function succeeds + virtual bool CreateFolder(const Pathname &pathname, mode_t mode); + + // As above, with mode = 0755. + virtual bool CreateFolder(const Pathname &pathname); + + // This moves a file from old_path to new_path, where "file" can be a plain + // file or directory, which will be moved recursively. + // Returns true if function succeeds. + virtual bool MoveFile(const Pathname &old_path, const Pathname &new_path); + virtual bool MoveFolder(const Pathname &old_path, const Pathname &new_path); + + // This copies a file from old_path to _new_path where "file" can be a plain + // file or directory, which will be copied recursively. + // Returns true if function succeeds + virtual bool CopyFile(const Pathname &old_path, const Pathname &new_path); + + // Returns true if a pathname is a directory + virtual bool IsFolder(const Pathname& pathname); + + // Returns true if pathname represents a temporary location on the system. + virtual bool IsTemporaryPath(const Pathname& pathname); + + // Returns true of pathname represents an existing file + virtual bool IsFile(const Pathname& pathname); + + // Returns true if pathname refers to no filesystem object, every parent + // directory either exists, or is also absent. + virtual bool IsAbsent(const Pathname& pathname); + + virtual std::string TempFilename(const Pathname &dir, + const std::string &prefix); + + // A folder appropriate for storing temporary files (Contents are + // automatically deleted when the program exists) + virtual bool GetTemporaryFolder(Pathname &path, bool create, + const std::string *append); + + virtual bool GetFileSize(const Pathname& path, size_t* size); + virtual bool GetFileTime(const Pathname& path, FileTimeType which, + time_t* time); + + // Returns the path to the running application. + virtual bool GetAppPathname(Pathname* path); + + virtual bool GetAppDataFolder(Pathname* path, bool per_user); + + // Get a temporary folder that is unique to the current user and application. + virtual bool GetAppTempFolder(Pathname* path); + + virtual bool GetDiskFreeSpace(const Pathname& path, int64 *freebytes); + + // Returns the absolute path of the current directory. + virtual Pathname GetCurrentDirectory(); + + private: +#if defined(ANDROID) || defined(IOS) + static char* provided_app_data_folder_; + static char* provided_app_temp_folder_; +#else + static char* app_temp_path_; +#endif + + static char* CopyString(const std::string& str); +}; + +} // namespace talk_base + +#endif // TALK_BASE_UNIXFILESYSTEM_H_ diff --git a/talk/base/urlencode.cc b/talk/base/urlencode.cc new file mode 100644 index 000000000..6fe71785e --- /dev/null +++ b/talk/base/urlencode.cc @@ -0,0 +1,196 @@ +/* + * libjingle + * Copyright 2008, 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/base/urlencode.h" + +#include "talk/base/common.h" +#include "talk/base/stringutils.h" + +static int HexPairValue(const char * code) { + int value = 0; + const char * pch = code; + for (;;) { + int digit = *pch++; + if (digit >= '0' && digit <= '9') { + value += digit - '0'; + } + else if (digit >= 'A' && digit <= 'F') { + value += digit - 'A' + 10; + } + else if (digit >= 'a' && digit <= 'f') { + value += digit - 'a' + 10; + } + else { + return -1; + } + if (pch == code + 2) + return value; + value <<= 4; + } +} + +int InternalUrlDecode(const char *source, char *dest, + bool encode_space_as_plus) { + char * start = dest; + + while (*source) { + switch (*source) { + case '+': + if (encode_space_as_plus) { + *(dest++) = ' '; + } else { + *dest++ = *source; + } + break; + case '%': + if (source[1] && source[2]) { + int value = HexPairValue(source + 1); + if (value >= 0) { + *(dest++) = value; + source += 2; + } + else { + *dest++ = '?'; + } + } + else { + *dest++ = '?'; + } + break; + default: + *dest++ = *source; + } + source++; + } + + *dest = 0; + return static_cast(dest - start); +} + +int UrlDecode(const char *source, char *dest) { + return InternalUrlDecode(source, dest, true); +} + +int UrlDecodeWithoutEncodingSpaceAsPlus(const char *source, char *dest) { + return InternalUrlDecode(source, dest, false); +} + +bool IsValidUrlChar(char ch, bool unsafe_only) { + if (unsafe_only) { + return !(ch <= ' ' || strchr("\\\"^&`<>[]{}", ch)); + } else { + return isalnum(ch) || strchr("-_.!~*'()", ch); + } +} + +int InternalUrlEncode(const char *source, char *dest, unsigned int max, + bool encode_space_as_plus, bool unsafe_only) { + static const char *digits = "0123456789ABCDEF"; + if (max == 0) { + return 0; + } + + char *start = dest; + while (static_cast(dest - start) < max && *source) { + unsigned char ch = static_cast(*source); + if (*source == ' ' && encode_space_as_plus && !unsafe_only) { + *dest++ = '+'; + } else if (IsValidUrlChar(ch, unsafe_only)) { + *dest++ = *source; + } else { + if (static_cast(dest - start) + 4 > max) { + break; + } + *dest++ = '%'; + *dest++ = digits[(ch >> 4) & 0x0F]; + *dest++ = digits[ ch & 0x0F]; + } + source++; + } + ASSERT(static_cast(dest - start) < max); + *dest = 0; + + return static_cast(dest - start); +} + +int UrlEncode(const char *source, char *dest, unsigned max) { + return InternalUrlEncode(source, dest, max, true, false); +} + +int UrlEncodeWithoutEncodingSpaceAsPlus(const char *source, char *dest, + unsigned max) { + return InternalUrlEncode(source, dest, max, false, false); +} + +int UrlEncodeOnlyUnsafeChars(const char *source, char *dest, unsigned max) { + return InternalUrlEncode(source, dest, max, false, true); +} + +std::string +InternalUrlDecodeString(const std::string & encoded, + bool encode_space_as_plus) { + size_t needed_length = encoded.length() + 1; + char* buf = STACK_ARRAY(char, needed_length); + InternalUrlDecode(encoded.c_str(), buf, encode_space_as_plus); + return buf; +} + +std::string +UrlDecodeString(const std::string & encoded) { + return InternalUrlDecodeString(encoded, true); +} + +std::string +UrlDecodeStringWithoutEncodingSpaceAsPlus(const std::string & encoded) { + return InternalUrlDecodeString(encoded, false); +} + +std::string +InternalUrlEncodeString(const std::string & decoded, + bool encode_space_as_plus, + bool unsafe_only) { + int needed_length = static_cast(decoded.length()) * 3 + 1; + char* buf = STACK_ARRAY(char, needed_length); + InternalUrlEncode(decoded.c_str(), buf, needed_length, + encode_space_as_plus, unsafe_only); + return buf; +} + +std::string +UrlEncodeString(const std::string & decoded) { + return InternalUrlEncodeString(decoded, true, false); +} + +std::string +UrlEncodeStringWithoutEncodingSpaceAsPlus(const std::string & decoded) { + return InternalUrlEncodeString(decoded, false, false); +} + +std::string +UrlEncodeStringForOnlyUnsafeChars(const std::string & decoded) { + return InternalUrlEncodeString(decoded, false, true); +} diff --git a/talk/base/urlencode.h b/talk/base/urlencode.h new file mode 100644 index 000000000..05165e82e --- /dev/null +++ b/talk/base/urlencode.h @@ -0,0 +1,60 @@ +/* + * libjingle + * Copyright 2008, 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. + */ + +#ifndef _URLENCODE_H_ +#define _URLENCODE_H_ + +#include + +// Decode all encoded characters. Also decode + as space. +int UrlDecode(const char *source, char *dest); + +// Decode all encoded characters. +int UrlDecodeWithoutEncodingSpaceAsPlus(const char *source, char *dest); + +// Encode all characters except alphas, numbers, and -_.!~*'() +// Also encode space as +. +int UrlEncode(const char *source, char *dest, unsigned max); + +// Encode all characters except alphas, numbers, and -_.!~*'() +int UrlEncodeWithoutEncodingSpaceAsPlus(const char *source, char *dest, + unsigned max); + +// Encode only unsafe chars, including \ "^&`<>[]{} +// Also encode space as %20, instead of + +int UrlEncodeOnlyUnsafeChars(const char *source, char *dest, unsigned max); + +std::string UrlDecodeString(const std::string & encoded); +std::string UrlDecodeStringWithoutEncodingSpaceAsPlus( + const std::string & encoded); +std::string UrlEncodeString(const std::string & decoded); +std::string UrlEncodeStringWithoutEncodingSpaceAsPlus( + const std::string & decoded); +std::string UrlEncodeStringForOnlyUnsafeChars(const std::string & decoded); + +#endif + diff --git a/talk/base/urlencode_unittest.cc b/talk/base/urlencode_unittest.cc new file mode 100644 index 000000000..f71cd75bd --- /dev/null +++ b/talk/base/urlencode_unittest.cc @@ -0,0 +1,98 @@ +/* + * libjingle + * Copyright 2004--2011, 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/base/common.h" +#include "talk/base/gunit.h" +#include "talk/base/thread.h" +#include "talk/base/urlencode.h" + +TEST(Urlencode, SourceTooLong) { + char source[] = "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^"; + char dest[1]; + ASSERT_EQ(0, UrlEncode(source, dest, ARRAY_SIZE(dest))); + ASSERT_EQ('\0', dest[0]); + + dest[0] = 'a'; + ASSERT_EQ(0, UrlEncode(source, dest, 0)); + ASSERT_EQ('a', dest[0]); +} + +TEST(Urlencode, OneCharacterConversion) { + char source[] = "^"; + char dest[4]; + ASSERT_EQ(3, UrlEncode(source, dest, ARRAY_SIZE(dest))); + ASSERT_STREQ("%5E", dest); +} + +TEST(Urlencode, ShortDestinationNoEncoding) { + // In this case we have a destination that would not be + // big enough to hold an encoding but is big enough to + // hold the text given. + char source[] = "aa"; + char dest[3]; + ASSERT_EQ(2, UrlEncode(source, dest, ARRAY_SIZE(dest))); + ASSERT_STREQ("aa", dest); +} + +TEST(Urlencode, ShortDestinationEncoding) { + // In this case we have a destination that is not + // big enough to hold the encoding. + char source[] = "&"; + char dest[3]; + ASSERT_EQ(0, UrlEncode(source, dest, ARRAY_SIZE(dest))); + ASSERT_EQ('\0', dest[0]); +} + +TEST(Urlencode, Encoding1) { + char source[] = "A^ "; + char dest[8]; + ASSERT_EQ(5, UrlEncode(source, dest, ARRAY_SIZE(dest))); + ASSERT_STREQ("A%5E+", dest); +} + +TEST(Urlencode, Encoding2) { + char source[] = "A^ "; + char dest[8]; + ASSERT_EQ(7, UrlEncodeWithoutEncodingSpaceAsPlus(source, dest, + ARRAY_SIZE(dest))); + ASSERT_STREQ("A%5E%20", dest); +} + +TEST(Urldecode, Decoding1) { + char source[] = "A%5E+"; + char dest[8]; + ASSERT_EQ(3, UrlDecode(source, dest)); + ASSERT_STREQ("A^ ", dest); +} + +TEST(Urldecode, Decoding2) { + char source[] = "A%5E+"; + char dest[8]; + ASSERT_EQ(3, UrlDecodeWithoutEncodingSpaceAsPlus(source, dest)); + ASSERT_STREQ("A^+", dest); +} diff --git a/talk/base/versionparsing.cc b/talk/base/versionparsing.cc new file mode 100644 index 000000000..03f3dec1e --- /dev/null +++ b/talk/base/versionparsing.cc @@ -0,0 +1,74 @@ +/* + * libjingle + * Copyright 2004--2010, 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/base/versionparsing.h" + +#include + +namespace talk_base { + +bool ParseVersionString(const std::string& version_str, + int num_expected_segments, + int version[]) { + size_t pos = 0; + for (int i = 0;;) { + size_t dot_pos = version_str.find('.', pos); + size_t n; + if (dot_pos == std::string::npos) { + // npos here is a special value meaning "to the end of the string" + n = std::string::npos; + } else { + n = dot_pos - pos; + } + + version[i] = atoi(version_str.substr(pos, n).c_str()); + + if (++i >= num_expected_segments) break; + + if (dot_pos == std::string::npos) { + // Previous segment was not terminated by a dot, but there's supposed to + // be more segments, so that's an error. + return false; + } + pos = dot_pos + 1; + } + return true; +} + +int CompareVersions(const int version1[], + const int version2[], + int num_segments) { + for (int i = 0; i < num_segments; ++i) { + int diff = version1[i] - version2[i]; + if (diff != 0) { + return diff; + } + } + return 0; +} + +} // namespace talk_base diff --git a/talk/base/versionparsing.h b/talk/base/versionparsing.h new file mode 100644 index 000000000..c66ad258d --- /dev/null +++ b/talk/base/versionparsing.h @@ -0,0 +1,52 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_BASE_VERSIONPARSING_H_ +#define TALK_BASE_VERSIONPARSING_H_ + +#include + +namespace talk_base { + +// Parses a version string into an array. "num_expected_segments" must be the +// number of numerical segments that the version is expected to have (e.g., +// "1.1.2.0" has 4). "version" must be an array of that length to hold the +// parsed numbers. +// Returns "true" iff successful. +bool ParseVersionString(const std::string& version_str, + int num_expected_segments, + int version[]); + +// Computes the lexicographical order of two versions. The return value +// indicates the order in the standard way (e.g., see strcmp()). +int CompareVersions(const int version1[], + const int version2[], + int num_segments); + +} // namespace talk_base + +#endif // TALK_BASE_VERSIONPARSING_H_ diff --git a/talk/base/versionparsing_unittest.cc b/talk/base/versionparsing_unittest.cc new file mode 100644 index 000000000..b0832656f --- /dev/null +++ b/talk/base/versionparsing_unittest.cc @@ -0,0 +1,91 @@ +/* + * libjingle + * Copyright 2010, 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/base/versionparsing.h" + +#include "talk/base/gunit.h" + +namespace talk_base { + +static const int kExampleSegments = 4; + +typedef int ExampleVersion[kExampleSegments]; + +TEST(VersionParsing, TestGoodParse) { + ExampleVersion ver; + std::string str1("1.1.2.0"); + static const ExampleVersion expect1 = {1, 1, 2, 0}; + EXPECT_TRUE(ParseVersionString(str1, kExampleSegments, ver)); + EXPECT_EQ(0, CompareVersions(ver, expect1, kExampleSegments)); + std::string str2("2.0.0.1"); + static const ExampleVersion expect2 = {2, 0, 0, 1}; + EXPECT_TRUE(ParseVersionString(str2, kExampleSegments, ver)); + EXPECT_EQ(0, CompareVersions(ver, expect2, kExampleSegments)); +} + +TEST(VersionParsing, TestBadParse) { + ExampleVersion ver; + std::string str1("1.1.2"); + EXPECT_FALSE(ParseVersionString(str1, kExampleSegments, ver)); + std::string str2(""); + EXPECT_FALSE(ParseVersionString(str2, kExampleSegments, ver)); + std::string str3("garbarge"); + EXPECT_FALSE(ParseVersionString(str3, kExampleSegments, ver)); +} + +TEST(VersionParsing, TestCompare) { + static const ExampleVersion ver1 = {1, 0, 21, 0}; + static const ExampleVersion ver2 = {1, 1, 2, 0}; + static const ExampleVersion ver3 = {1, 1, 3, 0}; + static const ExampleVersion ver4 = {1, 1, 3, 9861}; + + // Test that every combination of comparisons has the expected outcome. + EXPECT_EQ(0, CompareVersions(ver1, ver1, kExampleSegments)); + EXPECT_EQ(0, CompareVersions(ver2, ver2, kExampleSegments)); + EXPECT_EQ(0, CompareVersions(ver3, ver3, kExampleSegments)); + EXPECT_EQ(0, CompareVersions(ver4, ver4, kExampleSegments)); + + EXPECT_GT(0, CompareVersions(ver1, ver2, kExampleSegments)); + EXPECT_LT(0, CompareVersions(ver2, ver1, kExampleSegments)); + + EXPECT_GT(0, CompareVersions(ver1, ver3, kExampleSegments)); + EXPECT_LT(0, CompareVersions(ver3, ver1, kExampleSegments)); + + EXPECT_GT(0, CompareVersions(ver1, ver4, kExampleSegments)); + EXPECT_LT(0, CompareVersions(ver4, ver1, kExampleSegments)); + + EXPECT_GT(0, CompareVersions(ver2, ver3, kExampleSegments)); + EXPECT_LT(0, CompareVersions(ver3, ver2, kExampleSegments)); + + EXPECT_GT(0, CompareVersions(ver2, ver4, kExampleSegments)); + EXPECT_LT(0, CompareVersions(ver4, ver2, kExampleSegments)); + + EXPECT_GT(0, CompareVersions(ver3, ver4, kExampleSegments)); + EXPECT_LT(0, CompareVersions(ver4, ver3, kExampleSegments)); +} + +} // namespace talk_base diff --git a/talk/base/virtualsocket_unittest.cc b/talk/base/virtualsocket_unittest.cc new file mode 100644 index 000000000..244568e55 --- /dev/null +++ b/talk/base/virtualsocket_unittest.cc @@ -0,0 +1,1016 @@ +/* + * libjingle + * Copyright 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 +#ifdef POSIX +#include +#endif +#include + +#include "talk/base/logging.h" +#include "talk/base/gunit.h" +#include "talk/base/testclient.h" +#include "talk/base/testutils.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" +#include "talk/base/virtualsocketserver.h" + +using namespace talk_base; + +// Sends at a constant rate but with random packet sizes. +struct Sender : public MessageHandler { + Sender(Thread* th, AsyncSocket* s, uint32 rt) + : thread(th), socket(new AsyncUDPSocket(s)), + done(false), rate(rt), count(0) { + last_send = Time(); + thread->PostDelayed(NextDelay(), this, 1); + } + + uint32 NextDelay() { + uint32 size = (rand() % 4096) + 1; + return 1000 * size / rate; + } + + void OnMessage(Message* pmsg) { + ASSERT_EQ(1u, pmsg->message_id); + + if (done) + return; + + uint32 cur_time = Time(); + uint32 delay = cur_time - last_send; + uint32 size = rate * delay / 1000; + size = std::min(size, 4096); + size = std::max(size, sizeof(uint32)); + + count += size; + memcpy(dummy, &cur_time, sizeof(cur_time)); + socket->Send(dummy, size); + + last_send = cur_time; + thread->PostDelayed(NextDelay(), this, 1); + } + + Thread* thread; + scoped_ptr socket; + bool done; + uint32 rate; // bytes per second + uint32 count; + uint32 last_send; + char dummy[4096]; +}; + +struct Receiver : public MessageHandler, public sigslot::has_slots<> { + Receiver(Thread* th, AsyncSocket* s, uint32 bw) + : thread(th), socket(new AsyncUDPSocket(s)), bandwidth(bw), done(false), + count(0), sec_count(0), sum(0), sum_sq(0), samples(0) { + socket->SignalReadPacket.connect(this, &Receiver::OnReadPacket); + thread->PostDelayed(1000, this, 1); + } + + ~Receiver() { + thread->Clear(this); + } + + void OnReadPacket(AsyncPacketSocket* s, const char* data, size_t size, + const SocketAddress& remote_addr) { + ASSERT_EQ(socket.get(), s); + ASSERT_GE(size, 4U); + + count += size; + sec_count += size; + + uint32 send_time = *reinterpret_cast(data); + uint32 recv_time = Time(); + uint32 delay = recv_time - send_time; + sum += delay; + sum_sq += delay * delay; + samples += 1; + } + + void OnMessage(Message* pmsg) { + ASSERT_EQ(1u, pmsg->message_id); + + if (done) + return; + + // It is always possible for us to receive more than expected because + // packets can be further delayed in delivery. + if (bandwidth > 0) + ASSERT_TRUE(sec_count <= 5 * bandwidth / 4); + sec_count = 0; + thread->PostDelayed(1000, this, 1); + } + + Thread* thread; + scoped_ptr socket; + uint32 bandwidth; + bool done; + size_t count; + size_t sec_count; + double sum; + double sum_sq; + uint32 samples; +}; + +class VirtualSocketServerTest : public testing::Test { + public: + VirtualSocketServerTest() : ss_(new VirtualSocketServer(NULL)), + kIPv4AnyAddress(IPAddress(INADDR_ANY), 0), + kIPv6AnyAddress(IPAddress(in6addr_any), 0) { + } + + void CheckAddressIncrementalization(const SocketAddress& post, + const SocketAddress& pre) { + EXPECT_EQ(post.port(), pre.port() + 1); + IPAddress post_ip = post.ipaddr(); + IPAddress pre_ip = pre.ipaddr(); + EXPECT_EQ(pre_ip.family(), post_ip.family()); + if (post_ip.family() == AF_INET) { + in_addr pre_ipv4 = pre_ip.ipv4_address(); + in_addr post_ipv4 = post_ip.ipv4_address(); + int difference = ntohl(post_ipv4.s_addr) - ntohl(pre_ipv4.s_addr); + EXPECT_EQ(1, difference); + } else if (post_ip.family() == AF_INET6) { + in6_addr post_ip6 = post_ip.ipv6_address(); + in6_addr pre_ip6 = pre_ip.ipv6_address(); + uint32* post_as_ints = reinterpret_cast(&post_ip6.s6_addr); + uint32* pre_as_ints = reinterpret_cast(&pre_ip6.s6_addr); + EXPECT_EQ(post_as_ints[3], pre_as_ints[3] + 1); + } + } + + void BasicTest(const SocketAddress& initial_addr) { + AsyncSocket* socket = ss_->CreateAsyncSocket(initial_addr.family(), + SOCK_DGRAM); + socket->Bind(initial_addr); + SocketAddress server_addr = socket->GetLocalAddress(); + // Make sure VSS didn't switch families on us. + EXPECT_EQ(server_addr.family(), initial_addr.family()); + + TestClient* client1 = new TestClient(new AsyncUDPSocket(socket)); + AsyncSocket* socket2 = + ss_->CreateAsyncSocket(initial_addr.family(), SOCK_DGRAM); + TestClient* client2 = new TestClient(new AsyncUDPSocket(socket2)); + + SocketAddress client2_addr; + EXPECT_EQ(3, client2->SendTo("foo", 3, server_addr)); + EXPECT_TRUE(client1->CheckNextPacket("foo", 3, &client2_addr)); + + SocketAddress client1_addr; + EXPECT_EQ(6, client1->SendTo("bizbaz", 6, client2_addr)); + EXPECT_TRUE(client2->CheckNextPacket("bizbaz", 6, &client1_addr)); + EXPECT_EQ(client1_addr, server_addr); + + SocketAddress empty = EmptySocketAddressWithFamily(initial_addr.family()); + for (int i = 0; i < 10; i++) { + client2 = new TestClient(AsyncUDPSocket::Create(ss_, empty)); + + SocketAddress next_client2_addr; + EXPECT_EQ(3, client2->SendTo("foo", 3, server_addr)); + EXPECT_TRUE(client1->CheckNextPacket("foo", 3, &next_client2_addr)); + CheckAddressIncrementalization(next_client2_addr, client2_addr); + // EXPECT_EQ(next_client2_addr.port(), client2_addr.port() + 1); + + SocketAddress server_addr2; + EXPECT_EQ(6, client1->SendTo("bizbaz", 6, next_client2_addr)); + EXPECT_TRUE(client2->CheckNextPacket("bizbaz", 6, &server_addr2)); + EXPECT_EQ(server_addr2, server_addr); + + client2_addr = next_client2_addr; + } + } + + // initial_addr should be made from either INADDR_ANY or in6addr_any. + void ConnectTest(const SocketAddress& initial_addr) { + testing::StreamSink sink; + SocketAddress accept_addr; + const SocketAddress kEmptyAddr = + EmptySocketAddressWithFamily(initial_addr.family()); + + // Create client + AsyncSocket* client = ss_->CreateAsyncSocket(initial_addr.family(), + SOCK_STREAM); + sink.Monitor(client); + EXPECT_EQ(client->GetState(), AsyncSocket::CS_CLOSED); + EXPECT_TRUE(client->GetLocalAddress().IsNil()); + + // Create server + AsyncSocket* server = ss_->CreateAsyncSocket(initial_addr.family(), + SOCK_STREAM); + sink.Monitor(server); + EXPECT_NE(0, server->Listen(5)); // Bind required + EXPECT_EQ(0, server->Bind(initial_addr)); + EXPECT_EQ(server->GetLocalAddress().family(), initial_addr.family()); + EXPECT_EQ(0, server->Listen(5)); + EXPECT_EQ(server->GetState(), AsyncSocket::CS_CONNECTING); + + // No pending server connections + EXPECT_FALSE(sink.Check(server, testing::SSE_READ)); + EXPECT_TRUE(NULL == server->Accept(&accept_addr)); + EXPECT_EQ(AF_UNSPEC, accept_addr.family()); + + // Attempt connect to listening socket + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + EXPECT_NE(client->GetLocalAddress(), kEmptyAddr); // Implicit Bind + EXPECT_NE(AF_UNSPEC, client->GetLocalAddress().family()); // Implicit Bind + EXPECT_NE(client->GetLocalAddress(), server->GetLocalAddress()); + + // Client is connecting + EXPECT_EQ(client->GetState(), AsyncSocket::CS_CONNECTING); + EXPECT_FALSE(sink.Check(client, testing::SSE_OPEN)); + EXPECT_FALSE(sink.Check(client, testing::SSE_CLOSE)); + + ss_->ProcessMessagesUntilIdle(); + + // Client still connecting + EXPECT_EQ(client->GetState(), AsyncSocket::CS_CONNECTING); + EXPECT_FALSE(sink.Check(client, testing::SSE_OPEN)); + EXPECT_FALSE(sink.Check(client, testing::SSE_CLOSE)); + + // Server has pending connection + EXPECT_TRUE(sink.Check(server, testing::SSE_READ)); + Socket* accepted = server->Accept(&accept_addr); + EXPECT_TRUE(NULL != accepted); + EXPECT_NE(accept_addr, kEmptyAddr); + EXPECT_EQ(accepted->GetRemoteAddress(), accept_addr); + + EXPECT_EQ(accepted->GetState(), AsyncSocket::CS_CONNECTED); + EXPECT_EQ(accepted->GetLocalAddress(), server->GetLocalAddress()); + EXPECT_EQ(accepted->GetRemoteAddress(), client->GetLocalAddress()); + + ss_->ProcessMessagesUntilIdle(); + + // Client has connected + EXPECT_EQ(client->GetState(), AsyncSocket::CS_CONNECTED); + EXPECT_TRUE(sink.Check(client, testing::SSE_OPEN)); + EXPECT_FALSE(sink.Check(client, testing::SSE_CLOSE)); + EXPECT_EQ(client->GetRemoteAddress(), server->GetLocalAddress()); + EXPECT_EQ(client->GetRemoteAddress(), accepted->GetLocalAddress()); + } + + void ConnectToNonListenerTest(const SocketAddress& initial_addr) { + testing::StreamSink sink; + SocketAddress accept_addr; + const SocketAddress nil_addr; + const SocketAddress empty_addr = + EmptySocketAddressWithFamily(initial_addr.family()); + + // Create client + AsyncSocket* client = ss_->CreateAsyncSocket(initial_addr.family(), + SOCK_STREAM); + sink.Monitor(client); + + // Create server + AsyncSocket* server = ss_->CreateAsyncSocket(initial_addr.family(), + SOCK_STREAM); + sink.Monitor(server); + EXPECT_EQ(0, server->Bind(initial_addr)); + EXPECT_EQ(server->GetLocalAddress().family(), initial_addr.family()); + // Attempt connect to non-listening socket + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + + ss_->ProcessMessagesUntilIdle(); + + // No pending server connections + EXPECT_FALSE(sink.Check(server, testing::SSE_READ)); + EXPECT_TRUE(NULL == server->Accept(&accept_addr)); + EXPECT_EQ(accept_addr, nil_addr); + + // Connection failed + EXPECT_EQ(client->GetState(), AsyncSocket::CS_CLOSED); + EXPECT_FALSE(sink.Check(client, testing::SSE_OPEN)); + EXPECT_TRUE(sink.Check(client, testing::SSE_ERROR)); + EXPECT_EQ(client->GetRemoteAddress(), nil_addr); + } + + void CloseDuringConnectTest(const SocketAddress& initial_addr) { + testing::StreamSink sink; + SocketAddress accept_addr; + const SocketAddress empty_addr = + EmptySocketAddressWithFamily(initial_addr.family()); + + // Create client and server + AsyncSocket* client = ss_->CreateAsyncSocket(initial_addr.family(), + SOCK_STREAM); + sink.Monitor(client); + AsyncSocket* server = ss_->CreateAsyncSocket(initial_addr.family(), + SOCK_STREAM); + sink.Monitor(server); + + // Initiate connect + EXPECT_EQ(0, server->Bind(initial_addr)); + EXPECT_EQ(server->GetLocalAddress().family(), initial_addr.family()); + + EXPECT_EQ(0, server->Listen(5)); + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + + // Server close before socket enters accept queue + EXPECT_FALSE(sink.Check(server, testing::SSE_READ)); + server->Close(); + + ss_->ProcessMessagesUntilIdle(); + + // Result: connection failed + EXPECT_EQ(client->GetState(), AsyncSocket::CS_CLOSED); + EXPECT_TRUE(sink.Check(client, testing::SSE_ERROR)); + + // New server + delete server; + server = ss_->CreateAsyncSocket(initial_addr.family(), SOCK_STREAM); + sink.Monitor(server); + + // Initiate connect + EXPECT_EQ(0, server->Bind(initial_addr)); + EXPECT_EQ(server->GetLocalAddress().family(), initial_addr.family()); + + EXPECT_EQ(0, server->Listen(5)); + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + + ss_->ProcessMessagesUntilIdle(); + + // Server close while socket is in accept queue + EXPECT_TRUE(sink.Check(server, testing::SSE_READ)); + server->Close(); + + ss_->ProcessMessagesUntilIdle(); + + // Result: connection failed + EXPECT_EQ(client->GetState(), AsyncSocket::CS_CLOSED); + EXPECT_TRUE(sink.Check(client, testing::SSE_ERROR)); + + // New server + delete server; + server = ss_->CreateAsyncSocket(initial_addr.family(), SOCK_STREAM); + sink.Monitor(server); + + // Initiate connect + EXPECT_EQ(0, server->Bind(initial_addr)); + EXPECT_EQ(server->GetLocalAddress().family(), initial_addr.family()); + + EXPECT_EQ(0, server->Listen(5)); + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + + ss_->ProcessMessagesUntilIdle(); + + // Server accepts connection + EXPECT_TRUE(sink.Check(server, testing::SSE_READ)); + AsyncSocket* accepted = server->Accept(&accept_addr); + ASSERT_TRUE(NULL != accepted); + sink.Monitor(accepted); + + // Client closes before connection complets + EXPECT_EQ(accepted->GetState(), AsyncSocket::CS_CONNECTED); + + // Connected message has not been processed yet. + EXPECT_EQ(client->GetState(), AsyncSocket::CS_CONNECTING); + client->Close(); + + ss_->ProcessMessagesUntilIdle(); + + // Result: accepted socket closes + EXPECT_EQ(accepted->GetState(), AsyncSocket::CS_CLOSED); + EXPECT_TRUE(sink.Check(accepted, testing::SSE_CLOSE)); + EXPECT_FALSE(sink.Check(client, testing::SSE_CLOSE)); + } + + void CloseTest(const SocketAddress& initial_addr) { + testing::StreamSink sink; + const SocketAddress kEmptyAddr; + + // Create clients + AsyncSocket* a = ss_->CreateAsyncSocket(initial_addr.family(), SOCK_STREAM); + sink.Monitor(a); + a->Bind(initial_addr); + EXPECT_EQ(a->GetLocalAddress().family(), initial_addr.family()); + + + AsyncSocket* b = ss_->CreateAsyncSocket(initial_addr.family(), SOCK_STREAM); + sink.Monitor(b); + b->Bind(initial_addr); + EXPECT_EQ(b->GetLocalAddress().family(), initial_addr.family()); + + EXPECT_EQ(0, a->Connect(b->GetLocalAddress())); + EXPECT_EQ(0, b->Connect(a->GetLocalAddress())); + + ss_->ProcessMessagesUntilIdle(); + + EXPECT_TRUE(sink.Check(a, testing::SSE_OPEN)); + EXPECT_EQ(a->GetState(), AsyncSocket::CS_CONNECTED); + EXPECT_EQ(a->GetRemoteAddress(), b->GetLocalAddress()); + + EXPECT_TRUE(sink.Check(b, testing::SSE_OPEN)); + EXPECT_EQ(b->GetState(), AsyncSocket::CS_CONNECTED); + EXPECT_EQ(b->GetRemoteAddress(), a->GetLocalAddress()); + + EXPECT_EQ(1, a->Send("a", 1)); + b->Close(); + EXPECT_EQ(1, a->Send("b", 1)); + + ss_->ProcessMessagesUntilIdle(); + + char buffer[10]; + EXPECT_FALSE(sink.Check(b, testing::SSE_READ)); + EXPECT_EQ(-1, b->Recv(buffer, 10)); + + EXPECT_TRUE(sink.Check(a, testing::SSE_CLOSE)); + EXPECT_EQ(a->GetState(), AsyncSocket::CS_CLOSED); + EXPECT_EQ(a->GetRemoteAddress(), kEmptyAddr); + + EXPECT_FALSE(sink.Check(b, testing::SSE_CLOSE)); // No signal for Closer + EXPECT_EQ(b->GetState(), AsyncSocket::CS_CLOSED); + EXPECT_EQ(b->GetRemoteAddress(), kEmptyAddr); + } + + void TcpSendTest(const SocketAddress& initial_addr) { + testing::StreamSink sink; + const SocketAddress kEmptyAddr; + + // Connect two sockets + AsyncSocket* a = ss_->CreateAsyncSocket(initial_addr.family(), SOCK_STREAM); + sink.Monitor(a); + a->Bind(initial_addr); + EXPECT_EQ(a->GetLocalAddress().family(), initial_addr.family()); + + AsyncSocket* b = ss_->CreateAsyncSocket(initial_addr.family(), SOCK_STREAM); + sink.Monitor(b); + b->Bind(initial_addr); + EXPECT_EQ(b->GetLocalAddress().family(), initial_addr.family()); + + EXPECT_EQ(0, a->Connect(b->GetLocalAddress())); + EXPECT_EQ(0, b->Connect(a->GetLocalAddress())); + + ss_->ProcessMessagesUntilIdle(); + + const size_t kBufferSize = 2000; + ss_->set_send_buffer_capacity(kBufferSize); + ss_->set_recv_buffer_capacity(kBufferSize); + + const size_t kDataSize = 5000; + char send_buffer[kDataSize], recv_buffer[kDataSize]; + for (size_t i = 0; i < kDataSize; ++i) + send_buffer[i] = static_cast(i % 256); + memset(recv_buffer, 0, sizeof(recv_buffer)); + size_t send_pos = 0, recv_pos = 0; + + // Can't send more than send buffer in one write + int result = a->Send(send_buffer + send_pos, kDataSize - send_pos); + EXPECT_EQ(static_cast(kBufferSize), result); + send_pos += result; + + ss_->ProcessMessagesUntilIdle(); + EXPECT_FALSE(sink.Check(a, testing::SSE_WRITE)); + EXPECT_TRUE(sink.Check(b, testing::SSE_READ)); + + // Receive buffer is already filled, fill send buffer again + result = a->Send(send_buffer + send_pos, kDataSize - send_pos); + EXPECT_EQ(static_cast(kBufferSize), result); + send_pos += result; + + ss_->ProcessMessagesUntilIdle(); + EXPECT_FALSE(sink.Check(a, testing::SSE_WRITE)); + EXPECT_FALSE(sink.Check(b, testing::SSE_READ)); + + // No more room in send or receive buffer + result = a->Send(send_buffer + send_pos, kDataSize - send_pos); + EXPECT_EQ(-1, result); + EXPECT_TRUE(a->IsBlocking()); + + // Read a subset of the data + result = b->Recv(recv_buffer + recv_pos, 500); + EXPECT_EQ(500, result); + recv_pos += result; + + ss_->ProcessMessagesUntilIdle(); + EXPECT_TRUE(sink.Check(a, testing::SSE_WRITE)); + EXPECT_TRUE(sink.Check(b, testing::SSE_READ)); + + // Room for more on the sending side + result = a->Send(send_buffer + send_pos, kDataSize - send_pos); + EXPECT_EQ(500, result); + send_pos += result; + + // Empty the recv buffer + while (true) { + result = b->Recv(recv_buffer + recv_pos, kDataSize - recv_pos); + if (result < 0) { + EXPECT_EQ(-1, result); + EXPECT_TRUE(b->IsBlocking()); + break; + } + recv_pos += result; + } + + ss_->ProcessMessagesUntilIdle(); + EXPECT_TRUE(sink.Check(b, testing::SSE_READ)); + + // Continue to empty the recv buffer + while (true) { + result = b->Recv(recv_buffer + recv_pos, kDataSize - recv_pos); + if (result < 0) { + EXPECT_EQ(-1, result); + EXPECT_TRUE(b->IsBlocking()); + break; + } + recv_pos += result; + } + + // Send last of the data + result = a->Send(send_buffer + send_pos, kDataSize - send_pos); + EXPECT_EQ(500, result); + send_pos += result; + + ss_->ProcessMessagesUntilIdle(); + EXPECT_TRUE(sink.Check(b, testing::SSE_READ)); + + // Receive the last of the data + while (true) { + result = b->Recv(recv_buffer + recv_pos, kDataSize - recv_pos); + if (result < 0) { + EXPECT_EQ(-1, result); + EXPECT_TRUE(b->IsBlocking()); + break; + } + recv_pos += result; + } + + ss_->ProcessMessagesUntilIdle(); + EXPECT_FALSE(sink.Check(b, testing::SSE_READ)); + + // The received data matches the sent data + EXPECT_EQ(kDataSize, send_pos); + EXPECT_EQ(kDataSize, recv_pos); + EXPECT_EQ(0, memcmp(recv_buffer, send_buffer, kDataSize)); + } + + void TcpSendsPacketsInOrderTest(const SocketAddress& initial_addr) { + const SocketAddress kEmptyAddr; + + // Connect two sockets + AsyncSocket* a = ss_->CreateAsyncSocket(initial_addr.family(), + SOCK_STREAM); + AsyncSocket* b = ss_->CreateAsyncSocket(initial_addr.family(), + SOCK_STREAM); + a->Bind(initial_addr); + EXPECT_EQ(a->GetLocalAddress().family(), initial_addr.family()); + + b->Bind(initial_addr); + EXPECT_EQ(b->GetLocalAddress().family(), initial_addr.family()); + + EXPECT_EQ(0, a->Connect(b->GetLocalAddress())); + EXPECT_EQ(0, b->Connect(a->GetLocalAddress())); + ss_->ProcessMessagesUntilIdle(); + + // First, deliver all packets in 0 ms. + char buffer[2] = { 0, 0 }; + const char cNumPackets = 10; + for (char i = 0; i < cNumPackets; ++i) { + buffer[0] = '0' + i; + EXPECT_EQ(1, a->Send(buffer, 1)); + } + + ss_->ProcessMessagesUntilIdle(); + + for (char i = 0; i < cNumPackets; ++i) { + EXPECT_EQ(1, b->Recv(buffer, sizeof(buffer))); + EXPECT_EQ(static_cast('0' + i), buffer[0]); + } + + // Next, deliver packets at random intervals + const uint32 mean = 50; + const uint32 stddev = 50; + + ss_->set_delay_mean(mean); + ss_->set_delay_stddev(stddev); + ss_->UpdateDelayDistribution(); + + for (char i = 0; i < cNumPackets; ++i) { + buffer[0] = 'A' + i; + EXPECT_EQ(1, a->Send(buffer, 1)); + } + + ss_->ProcessMessagesUntilIdle(); + + for (char i = 0; i < cNumPackets; ++i) { + EXPECT_EQ(1, b->Recv(buffer, sizeof(buffer))); + EXPECT_EQ(static_cast('A' + i), buffer[0]); + } + } + + void BandwidthTest(const SocketAddress& initial_addr) { + AsyncSocket* send_socket = + ss_->CreateAsyncSocket(initial_addr.family(), SOCK_DGRAM); + AsyncSocket* recv_socket = + ss_->CreateAsyncSocket(initial_addr.family(), SOCK_DGRAM); + ASSERT_EQ(0, send_socket->Bind(initial_addr)); + ASSERT_EQ(0, recv_socket->Bind(initial_addr)); + EXPECT_EQ(send_socket->GetLocalAddress().family(), initial_addr.family()); + EXPECT_EQ(recv_socket->GetLocalAddress().family(), initial_addr.family()); + ASSERT_EQ(0, send_socket->Connect(recv_socket->GetLocalAddress())); + + uint32 bandwidth = 64 * 1024; + ss_->set_bandwidth(bandwidth); + + Thread* pthMain = Thread::Current(); + Sender sender(pthMain, send_socket, 80 * 1024); + Receiver receiver(pthMain, recv_socket, bandwidth); + + pthMain->ProcessMessages(5000); + sender.done = true; + pthMain->ProcessMessages(5000); + + ASSERT_TRUE(receiver.count >= 5 * 3 * bandwidth / 4); + ASSERT_TRUE(receiver.count <= 6 * bandwidth); // queue could drain for 1s + + ss_->set_bandwidth(0); + } + + void DelayTest(const SocketAddress& initial_addr) { + time_t seed = ::time(NULL); + LOG(LS_VERBOSE) << "seed = " << seed; + srand(static_cast(seed)); + + const uint32 mean = 2000; + const uint32 stddev = 500; + + ss_->set_delay_mean(mean); + ss_->set_delay_stddev(stddev); + ss_->UpdateDelayDistribution(); + + AsyncSocket* send_socket = + ss_->CreateAsyncSocket(initial_addr.family(), SOCK_DGRAM); + AsyncSocket* recv_socket = + ss_->CreateAsyncSocket(initial_addr.family(), SOCK_DGRAM); + ASSERT_EQ(0, send_socket->Bind(initial_addr)); + ASSERT_EQ(0, recv_socket->Bind(initial_addr)); + EXPECT_EQ(send_socket->GetLocalAddress().family(), initial_addr.family()); + EXPECT_EQ(recv_socket->GetLocalAddress().family(), initial_addr.family()); + ASSERT_EQ(0, send_socket->Connect(recv_socket->GetLocalAddress())); + + Thread* pthMain = Thread::Current(); + // Avg packet size is 2K, so at 200KB/s for 10s, we should see about + // 1000 packets, which is necessary to get a good distribution. + Sender sender(pthMain, send_socket, 100 * 2 * 1024); + Receiver receiver(pthMain, recv_socket, 0); + + pthMain->ProcessMessages(10000); + sender.done = receiver.done = true; + ss_->ProcessMessagesUntilIdle(); + + const double sample_mean = receiver.sum / receiver.samples; + double num = + receiver.samples * receiver.sum_sq - receiver.sum * receiver.sum; + double den = receiver.samples * (receiver.samples - 1); + const double sample_stddev = std::sqrt(num / den); + LOG(LS_VERBOSE) << "mean=" << sample_mean << " stddev=" << sample_stddev; + + EXPECT_LE(500u, receiver.samples); + // We initially used a 0.1 fudge factor, but on the build machine, we + // have seen the value differ by as much as 0.13. + EXPECT_NEAR(mean, sample_mean, 0.15 * mean); + EXPECT_NEAR(stddev, sample_stddev, 0.15 * stddev); + + ss_->set_delay_mean(0); + ss_->set_delay_stddev(0); + ss_->UpdateDelayDistribution(); + } + + // Test cross-family communication between a client bound to client_addr and a + // server bound to server_addr. shouldSucceed indicates if communication is + // expected to work or not. + void CrossFamilyConnectionTest(const SocketAddress& client_addr, + const SocketAddress& server_addr, + bool shouldSucceed) { + testing::StreamSink sink; + SocketAddress accept_address; + const SocketAddress kEmptyAddr; + + // Client gets a IPv4 address + AsyncSocket* client = ss_->CreateAsyncSocket(client_addr.family(), + SOCK_STREAM); + sink.Monitor(client); + EXPECT_EQ(client->GetState(), AsyncSocket::CS_CLOSED); + EXPECT_EQ(client->GetLocalAddress(), kEmptyAddr); + client->Bind(client_addr); + + // Server gets a non-mapped non-any IPv6 address. + // IPv4 sockets should not be able to connect to this. + AsyncSocket* server = ss_->CreateAsyncSocket(server_addr.family(), + SOCK_STREAM); + sink.Monitor(server); + server->Bind(server_addr); + server->Listen(5); + + if (shouldSucceed) { + EXPECT_EQ(0, client->Connect(server->GetLocalAddress())); + ss_->ProcessMessagesUntilIdle(); + EXPECT_TRUE(sink.Check(server, testing::SSE_READ)); + Socket* accepted = server->Accept(&accept_address); + EXPECT_TRUE(NULL != accepted); + EXPECT_NE(kEmptyAddr, accept_address); + ss_->ProcessMessagesUntilIdle(); + EXPECT_TRUE(sink.Check(client, testing::SSE_OPEN)); + EXPECT_EQ(client->GetRemoteAddress(), server->GetLocalAddress()); + } else { + // Check that the connection failed. + EXPECT_EQ(-1, client->Connect(server->GetLocalAddress())); + ss_->ProcessMessagesUntilIdle(); + + EXPECT_FALSE(sink.Check(server, testing::SSE_READ)); + EXPECT_TRUE(NULL == server->Accept(&accept_address)); + EXPECT_EQ(accept_address, kEmptyAddr); + EXPECT_EQ(client->GetState(), AsyncSocket::CS_CLOSED); + EXPECT_FALSE(sink.Check(client, testing::SSE_OPEN)); + EXPECT_EQ(client->GetRemoteAddress(), kEmptyAddr); + } + } + + // Test cross-family datagram sending between a client bound to client_addr + // and a server bound to server_addr. shouldSucceed indicates if sending is + // expected to succed or not. + void CrossFamilyDatagramTest(const SocketAddress& client_addr, + const SocketAddress& server_addr, + bool shouldSucceed) { + AsyncSocket* socket = ss_->CreateAsyncSocket(SOCK_DGRAM); + socket->Bind(server_addr); + SocketAddress bound_server_addr = socket->GetLocalAddress(); + TestClient* client1 = new TestClient(new AsyncUDPSocket(socket)); + + AsyncSocket* socket2 = ss_->CreateAsyncSocket(SOCK_DGRAM); + socket2->Bind(client_addr); + TestClient* client2 = new TestClient(new AsyncUDPSocket(socket2)); + SocketAddress client2_addr; + + if (shouldSucceed) { + EXPECT_EQ(3, client2->SendTo("foo", 3, bound_server_addr)); + EXPECT_TRUE(client1->CheckNextPacket("foo", 3, &client2_addr)); + SocketAddress client1_addr; + EXPECT_EQ(6, client1->SendTo("bizbaz", 6, client2_addr)); + EXPECT_TRUE(client2->CheckNextPacket("bizbaz", 6, &client1_addr)); + EXPECT_EQ(client1_addr, bound_server_addr); + } else { + EXPECT_EQ(-1, client2->SendTo("foo", 3, bound_server_addr)); + EXPECT_FALSE(client1->CheckNextPacket("foo", 3, 0)); + } + } + + protected: + virtual void SetUp() { + Thread::Current()->set_socketserver(ss_); + } + virtual void TearDown() { + Thread::Current()->set_socketserver(NULL); + } + + VirtualSocketServer* ss_; + const SocketAddress kIPv4AnyAddress; + const SocketAddress kIPv6AnyAddress; +}; + +TEST_F(VirtualSocketServerTest, basic_v4) { + SocketAddress ipv4_test_addr(IPAddress(INADDR_ANY), 5000); + BasicTest(ipv4_test_addr); +} + +TEST_F(VirtualSocketServerTest, basic_v6) { + SocketAddress ipv6_test_addr(IPAddress(in6addr_any), 5000); + BasicTest(ipv6_test_addr); +} + +TEST_F(VirtualSocketServerTest, connect_v4) { + ConnectTest(kIPv4AnyAddress); +} + +TEST_F(VirtualSocketServerTest, connect_v6) { + ConnectTest(kIPv6AnyAddress); +} + +TEST_F(VirtualSocketServerTest, connect_to_non_listener_v4) { + ConnectToNonListenerTest(kIPv4AnyAddress); +} + +TEST_F(VirtualSocketServerTest, connect_to_non_listener_v6) { + ConnectToNonListenerTest(kIPv6AnyAddress); +} + +TEST_F(VirtualSocketServerTest, close_during_connect_v4) { + CloseDuringConnectTest(kIPv4AnyAddress); +} + +TEST_F(VirtualSocketServerTest, close_during_connect_v6) { + CloseDuringConnectTest(kIPv6AnyAddress); +} + +TEST_F(VirtualSocketServerTest, close_v4) { + CloseTest(kIPv4AnyAddress); +} + +TEST_F(VirtualSocketServerTest, close_v6) { + CloseTest(kIPv6AnyAddress); +} + +TEST_F(VirtualSocketServerTest, tcp_send_v4) { + TcpSendTest(kIPv4AnyAddress); +} + +TEST_F(VirtualSocketServerTest, tcp_send_v6) { + TcpSendTest(kIPv6AnyAddress); +} + +TEST_F(VirtualSocketServerTest, TcpSendsPacketsInOrder_v4) { + TcpSendsPacketsInOrderTest(kIPv4AnyAddress); +} + +TEST_F(VirtualSocketServerTest, TcpSendsPacketsInOrder_v6) { + TcpSendsPacketsInOrderTest(kIPv6AnyAddress); +} + +TEST_F(VirtualSocketServerTest, bandwidth_v4) { + SocketAddress ipv4_test_addr(IPAddress(INADDR_ANY), 1000); + BandwidthTest(ipv4_test_addr); +} + +TEST_F(VirtualSocketServerTest, bandwidth_v6) { + SocketAddress ipv6_test_addr(IPAddress(in6addr_any), 1000); + BandwidthTest(ipv6_test_addr); +} + +TEST_F(VirtualSocketServerTest, delay_v4) { + SocketAddress ipv4_test_addr(IPAddress(INADDR_ANY), 1000); + DelayTest(ipv4_test_addr); +} + +TEST_F(VirtualSocketServerTest, delay_v6) { + SocketAddress ipv6_test_addr(IPAddress(in6addr_any), 1000); + DelayTest(ipv6_test_addr); +} + +// Works, receiving socket sees 127.0.0.2. +TEST_F(VirtualSocketServerTest, CanConnectFromMappedIPv6ToIPv4Any) { + CrossFamilyConnectionTest(SocketAddress("::ffff:127.0.0.2", 0), + SocketAddress("0.0.0.0", 5000), + true); +} + +// Fails. +TEST_F(VirtualSocketServerTest, CantConnectFromUnMappedIPv6ToIPv4Any) { + CrossFamilyConnectionTest(SocketAddress("::2", 0), + SocketAddress("0.0.0.0", 5000), + false); +} + +// Fails. +TEST_F(VirtualSocketServerTest, CantConnectFromUnMappedIPv6ToMappedIPv6) { + CrossFamilyConnectionTest(SocketAddress("::2", 0), + SocketAddress("::ffff:127.0.0.1", 5000), + false); +} + +// Works. receiving socket sees ::ffff:127.0.0.2. +TEST_F(VirtualSocketServerTest, CanConnectFromIPv4ToIPv6Any) { + CrossFamilyConnectionTest(SocketAddress("127.0.0.2", 0), + SocketAddress("::", 5000), + true); +} + +// Fails. +TEST_F(VirtualSocketServerTest, CantConnectFromIPv4ToUnMappedIPv6) { + CrossFamilyConnectionTest(SocketAddress("127.0.0.2", 0), + SocketAddress("::1", 5000), + false); +} + +// Works. Receiving socket sees ::ffff:127.0.0.1. +TEST_F(VirtualSocketServerTest, CanConnectFromIPv4ToMappedIPv6) { + CrossFamilyConnectionTest(SocketAddress("127.0.0.1", 0), + SocketAddress("::ffff:127.0.0.2", 5000), + true); +} + +// Works, receiving socket sees a result from GetNextIP. +TEST_F(VirtualSocketServerTest, CanConnectFromUnboundIPv6ToIPv4Any) { + CrossFamilyConnectionTest(SocketAddress("::", 0), + SocketAddress("0.0.0.0", 5000), + true); +} + +// Works, receiving socket sees whatever GetNextIP gave the client. +TEST_F(VirtualSocketServerTest, CanConnectFromUnboundIPv4ToIPv6Any) { + CrossFamilyConnectionTest(SocketAddress("0.0.0.0", 0), + SocketAddress("::", 5000), + true); +} + +TEST_F(VirtualSocketServerTest, CanSendDatagramFromUnboundIPv4ToIPv6Any) { + CrossFamilyDatagramTest(SocketAddress("0.0.0.0", 0), + SocketAddress("::", 5000), + true); +} + +TEST_F(VirtualSocketServerTest, CanSendDatagramFromMappedIPv6ToIPv4Any) { + CrossFamilyDatagramTest(SocketAddress("::ffff:127.0.0.1", 0), + SocketAddress("0.0.0.0", 5000), + true); +} + +TEST_F(VirtualSocketServerTest, CantSendDatagramFromUnMappedIPv6ToIPv4Any) { + CrossFamilyDatagramTest(SocketAddress("::2", 0), + SocketAddress("0.0.0.0", 5000), + false); +} + +TEST_F(VirtualSocketServerTest, CantSendDatagramFromUnMappedIPv6ToMappedIPv6) { + CrossFamilyDatagramTest(SocketAddress("::2", 0), + SocketAddress("::ffff:127.0.0.1", 5000), + false); +} + +TEST_F(VirtualSocketServerTest, CanSendDatagramFromIPv4ToIPv6Any) { + CrossFamilyDatagramTest(SocketAddress("127.0.0.2", 0), + SocketAddress("::", 5000), + true); +} + +TEST_F(VirtualSocketServerTest, CantSendDatagramFromIPv4ToUnMappedIPv6) { + CrossFamilyDatagramTest(SocketAddress("127.0.0.2", 0), + SocketAddress("::1", 5000), + false); +} + +TEST_F(VirtualSocketServerTest, CanSendDatagramFromIPv4ToMappedIPv6) { + CrossFamilyDatagramTest(SocketAddress("127.0.0.1", 0), + SocketAddress("::ffff:127.0.0.2", 5000), + true); +} + +TEST_F(VirtualSocketServerTest, CanSendDatagramFromUnboundIPv6ToIPv4Any) { + CrossFamilyDatagramTest(SocketAddress("::", 0), + SocketAddress("0.0.0.0", 5000), + true); +} + +TEST_F(VirtualSocketServerTest, CreatesStandardDistribution) { + const uint32 kTestMean[] = { 10, 100, 333, 1000 }; + const double kTestDev[] = { 0.25, 0.1, 0.01 }; + // TODO: The current code only works for 1000 data points or more. + const uint32 kTestSamples[] = { /*10, 100,*/ 1000 }; + for (size_t midx = 0; midx < ARRAY_SIZE(kTestMean); ++midx) { + for (size_t didx = 0; didx < ARRAY_SIZE(kTestDev); ++didx) { + for (size_t sidx = 0; sidx < ARRAY_SIZE(kTestSamples); ++sidx) { + ASSERT_LT(0u, kTestSamples[sidx]); + const uint32 kStdDev = + static_cast(kTestDev[didx] * kTestMean[midx]); + VirtualSocketServer::Function* f = + VirtualSocketServer::CreateDistribution(kTestMean[midx], + kStdDev, + kTestSamples[sidx]); + ASSERT_TRUE(NULL != f); + ASSERT_EQ(kTestSamples[sidx], f->size()); + double sum = 0; + for (uint32 i = 0; i < f->size(); ++i) { + sum += (*f)[i].second; + } + const double mean = sum / f->size(); + double sum_sq_dev = 0; + for (uint32 i = 0; i < f->size(); ++i) { + double dev = (*f)[i].second - mean; + sum_sq_dev += dev * dev; + } + const double stddev = std::sqrt(sum_sq_dev / f->size()); + EXPECT_NEAR(kTestMean[midx], mean, 0.1 * kTestMean[midx]) + << "M=" << kTestMean[midx] + << " SD=" << kStdDev + << " N=" << kTestSamples[sidx]; + EXPECT_NEAR(kStdDev, stddev, 0.1 * kStdDev) + << "M=" << kTestMean[midx] + << " SD=" << kStdDev + << " N=" << kTestSamples[sidx]; + delete f; + } + } + } +} diff --git a/talk/base/virtualsocketserver.cc b/talk/base/virtualsocketserver.cc new file mode 100644 index 000000000..c8cac0e95 --- /dev/null +++ b/talk/base/virtualsocketserver.cc @@ -0,0 +1,1117 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/virtualsocketserver.h" + +#include + +#include +#include +#include +#include + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/socketaddresspair.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" + +namespace talk_base { +#ifdef WIN32 +const in_addr kInitialNextIPv4 = { {0x01, 0, 0, 0} }; +#else +// This value is entirely arbitrary, hence the lack of concern about endianness. +const in_addr kInitialNextIPv4 = { 0x01000000 }; +#endif +// Starts at ::2 so as to not cause confusion with ::1. +const in6_addr kInitialNextIPv6 = { { { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 + } } }; + +const uint16 kFirstEphemeralPort = 49152; +const uint16 kLastEphemeralPort = 65535; +const uint16 kEphemeralPortCount = kLastEphemeralPort - kFirstEphemeralPort + 1; +const uint32 kDefaultNetworkCapacity = 64 * 1024; +const uint32 kDefaultTcpBufferSize = 32 * 1024; + +const uint32 UDP_HEADER_SIZE = 28; // IP + UDP headers +const uint32 TCP_HEADER_SIZE = 40; // IP + TCP headers +const uint32 TCP_MSS = 1400; // Maximum segment size + +// Note: The current algorithm doesn't work for sample sizes smaller than this. +const int NUM_SAMPLES = 1000; + +enum { + MSG_ID_PACKET, + MSG_ID_CONNECT, + MSG_ID_DISCONNECT, +}; + +// Packets are passed between sockets as messages. We copy the data just like +// the kernel does. +class Packet : public MessageData { + public: + Packet(const char* data, size_t size, const SocketAddress& from) + : size_(size), consumed_(0), from_(from) { + ASSERT(NULL != data); + data_ = new char[size_]; + std::memcpy(data_, data, size_); + } + + virtual ~Packet() { + delete[] data_; + } + + const char* data() const { return data_ + consumed_; } + size_t size() const { return size_ - consumed_; } + const SocketAddress& from() const { return from_; } + + // Remove the first size bytes from the data. + void Consume(size_t size) { + ASSERT(size + consumed_ < size_); + consumed_ += size; + } + + private: + char* data_; + size_t size_, consumed_; + SocketAddress from_; +}; + +struct MessageAddress : public MessageData { + explicit MessageAddress(const SocketAddress& a) : addr(a) { } + SocketAddress addr; +}; + +// Implements the socket interface using the virtual network. Packets are +// passed as messages using the message queue of the socket server. +class VirtualSocket : public AsyncSocket, public MessageHandler { + public: + VirtualSocket(VirtualSocketServer* server, int family, int type, bool async) + : server_(server), family_(family), type_(type), async_(async), + state_(CS_CLOSED), listen_queue_(NULL), write_enabled_(false), + network_size_(0), recv_buffer_size_(0), bound_(false), was_any_(false) { + ASSERT((type_ == SOCK_DGRAM) || (type_ == SOCK_STREAM)); + ASSERT(async_ || (type_ != SOCK_STREAM)); // We only support async streams + } + + virtual ~VirtualSocket() { + Close(); + + for (RecvBuffer::iterator it = recv_buffer_.begin(); + it != recv_buffer_.end(); ++it) { + delete *it; + } + } + + virtual SocketAddress GetLocalAddress() const { + return local_addr_; + } + + virtual SocketAddress GetRemoteAddress() const { + return remote_addr_; + } + + // Used by server sockets to set the local address without binding. + void SetLocalAddress(const SocketAddress& addr) { + local_addr_ = addr; + } + + virtual int Bind(const SocketAddress& addr) { + if (!local_addr_.IsNil()) { + error_ = EINVAL; + return -1; + } + local_addr_ = addr; + int result = server_->Bind(this, &local_addr_); + if (result != 0) { + local_addr_.Clear(); + error_ = EADDRINUSE; + } else { + bound_ = true; + was_any_ = addr.IsAnyIP(); + } + return result; + } + + virtual int Connect(const SocketAddress& addr) { + return InitiateConnect(addr, true); + } + + virtual int Close() { + if (!local_addr_.IsNil() && bound_) { + // Remove from the binding table. + server_->Unbind(local_addr_, this); + bound_ = false; + } + + if (SOCK_STREAM == type_) { + // Cancel pending sockets + if (listen_queue_) { + while (!listen_queue_->empty()) { + SocketAddress addr = listen_queue_->front(); + + // Disconnect listening socket. + server_->Disconnect(server_->LookupBinding(addr)); + listen_queue_->pop_front(); + } + delete listen_queue_; + listen_queue_ = NULL; + } + // Disconnect stream sockets + if (CS_CONNECTED == state_) { + // Disconnect remote socket, check if it is a child of a server socket. + VirtualSocket* socket = + server_->LookupConnection(local_addr_, remote_addr_); + if (!socket) { + // Not a server socket child, then see if it is bound. + // TODO: If this is indeed a server socket that has no + // children this will cause the server socket to be + // closed. This might lead to unexpected results, how to fix this? + socket = server_->LookupBinding(remote_addr_); + } + server_->Disconnect(socket); + + // Remove mapping for both directions. + server_->RemoveConnection(remote_addr_, local_addr_); + server_->RemoveConnection(local_addr_, remote_addr_); + } + // Cancel potential connects + MessageList msgs; + if (server_->msg_queue_) { + server_->msg_queue_->Clear(this, MSG_ID_CONNECT, &msgs); + } + for (MessageList::iterator it = msgs.begin(); it != msgs.end(); ++it) { + ASSERT(NULL != it->pdata); + MessageAddress* data = static_cast(it->pdata); + + // Lookup remote side. + VirtualSocket* socket = server_->LookupConnection(local_addr_, + data->addr); + if (socket) { + // Server socket, remote side is a socket retreived by + // accept. Accepted sockets are not bound so we will not + // find it by looking in the bindings table. + server_->Disconnect(socket); + server_->RemoveConnection(local_addr_, data->addr); + } else { + server_->Disconnect(server_->LookupBinding(data->addr)); + } + delete data; + } + // Clear incoming packets and disconnect messages + if (server_->msg_queue_) { + server_->msg_queue_->Clear(this); + } + } + + state_ = CS_CLOSED; + local_addr_.Clear(); + remote_addr_.Clear(); + return 0; + } + + virtual int Send(const void *pv, size_t cb) { + if (CS_CONNECTED != state_) { + error_ = ENOTCONN; + return -1; + } + if (SOCK_DGRAM == type_) { + return SendUdp(pv, cb, remote_addr_); + } else { + return SendTcp(pv, cb); + } + } + + virtual int SendTo(const void *pv, size_t cb, const SocketAddress& addr) { + if (SOCK_DGRAM == type_) { + return SendUdp(pv, cb, addr); + } else { + if (CS_CONNECTED != state_) { + error_ = ENOTCONN; + return -1; + } + return SendTcp(pv, cb); + } + } + + virtual int Recv(void *pv, size_t cb) { + SocketAddress addr; + return RecvFrom(pv, cb, &addr); + } + + virtual int RecvFrom(void *pv, size_t cb, SocketAddress *paddr) { + // If we don't have a packet, then either error or wait for one to arrive. + if (recv_buffer_.empty()) { + if (async_) { + error_ = EAGAIN; + return -1; + } + while (recv_buffer_.empty()) { + Message msg; + server_->msg_queue_->Get(&msg); + server_->msg_queue_->Dispatch(&msg); + } + } + + // Return the packet at the front of the queue. + Packet* packet = recv_buffer_.front(); + size_t data_read = _min(cb, packet->size()); + std::memcpy(pv, packet->data(), data_read); + *paddr = packet->from(); + + if (data_read < packet->size()) { + packet->Consume(data_read); + } else { + recv_buffer_.pop_front(); + delete packet; + } + + if (SOCK_STREAM == type_) { + bool was_full = (recv_buffer_size_ == server_->recv_buffer_capacity_); + recv_buffer_size_ -= data_read; + if (was_full) { + VirtualSocket* sender = server_->LookupBinding(remote_addr_); + ASSERT(NULL != sender); + server_->SendTcp(sender); + } + } + + return static_cast(data_read); + } + + virtual int Listen(int backlog) { + ASSERT(SOCK_STREAM == type_); + ASSERT(CS_CLOSED == state_); + if (local_addr_.IsNil()) { + error_ = EINVAL; + return -1; + } + ASSERT(NULL == listen_queue_); + listen_queue_ = new ListenQueue; + state_ = CS_CONNECTING; + return 0; + } + + virtual VirtualSocket* Accept(SocketAddress *paddr) { + if (NULL == listen_queue_) { + error_ = EINVAL; + return NULL; + } + while (!listen_queue_->empty()) { + VirtualSocket* socket = new VirtualSocket(server_, AF_INET, type_, + async_); + + // Set the new local address to the same as this server socket. + socket->SetLocalAddress(local_addr_); + // Sockets made from a socket that 'was Any' need to inherit that. + socket->set_was_any(was_any_); + SocketAddress remote_addr(listen_queue_->front()); + int result = socket->InitiateConnect(remote_addr, false); + listen_queue_->pop_front(); + if (result != 0) { + delete socket; + continue; + } + socket->CompleteConnect(remote_addr, false); + if (paddr) { + *paddr = remote_addr; + } + return socket; + } + error_ = EWOULDBLOCK; + return NULL; + } + + virtual int GetError() const { + return error_; + } + + virtual void SetError(int error) { + error_ = error; + } + + virtual ConnState GetState() const { + return state_; + } + + virtual int GetOption(Option opt, int* value) { + OptionsMap::const_iterator it = options_map_.find(opt); + if (it == options_map_.end()) { + return -1; + } + *value = it->second; + return 0; // 0 is success to emulate getsockopt() + } + + virtual int SetOption(Option opt, int value) { + options_map_[opt] = value; + return 0; // 0 is success to emulate setsockopt() + } + + virtual int EstimateMTU(uint16* mtu) { + if (CS_CONNECTED != state_) + return ENOTCONN; + else + return 65536; + } + + void OnMessage(Message *pmsg) { + if (pmsg->message_id == MSG_ID_PACKET) { + //ASSERT(!local_addr_.IsAny()); + ASSERT(NULL != pmsg->pdata); + Packet* packet = static_cast(pmsg->pdata); + + recv_buffer_.push_back(packet); + + if (async_) { + SignalReadEvent(this); + } + } else if (pmsg->message_id == MSG_ID_CONNECT) { + ASSERT(NULL != pmsg->pdata); + MessageAddress* data = static_cast(pmsg->pdata); + if (listen_queue_ != NULL) { + listen_queue_->push_back(data->addr); + if (async_) { + SignalReadEvent(this); + } + } else if ((SOCK_STREAM == type_) && (CS_CONNECTING == state_)) { + CompleteConnect(data->addr, true); + } else { + LOG(LS_VERBOSE) << "Socket at " << local_addr_ << " is not listening"; + server_->Disconnect(server_->LookupBinding(data->addr)); + } + delete data; + } else if (pmsg->message_id == MSG_ID_DISCONNECT) { + ASSERT(SOCK_STREAM == type_); + if (CS_CLOSED != state_) { + int error = (CS_CONNECTING == state_) ? ECONNREFUSED : 0; + state_ = CS_CLOSED; + remote_addr_.Clear(); + if (async_) { + SignalCloseEvent(this, error); + } + } + } else { + ASSERT(false); + } + } + + bool was_any() { return was_any_; } + void set_was_any(bool was_any) { was_any_ = was_any; } + + private: + struct NetworkEntry { + size_t size; + uint32 done_time; + }; + + typedef std::deque ListenQueue; + typedef std::deque NetworkQueue; + typedef std::vector SendBuffer; + typedef std::list RecvBuffer; + typedef std::map OptionsMap; + + int InitiateConnect(const SocketAddress& addr, bool use_delay) { + if (!remote_addr_.IsNil()) { + error_ = (CS_CONNECTED == state_) ? EISCONN : EINPROGRESS; + return -1; + } + if (local_addr_.IsNil()) { + // If there's no local address set, grab a random one in the correct AF. + int result = 0; + if (addr.ipaddr().family() == AF_INET) { + result = Bind(SocketAddress("0.0.0.0", 0)); + } else if (addr.ipaddr().family() == AF_INET6) { + result = Bind(SocketAddress("::", 0)); + } + if (result != 0) { + return result; + } + } + if (type_ == SOCK_DGRAM) { + remote_addr_ = addr; + state_ = CS_CONNECTED; + } else { + int result = server_->Connect(this, addr, use_delay); + if (result != 0) { + error_ = EHOSTUNREACH; + return -1; + } + state_ = CS_CONNECTING; + } + return 0; + } + + void CompleteConnect(const SocketAddress& addr, bool notify) { + ASSERT(CS_CONNECTING == state_); + remote_addr_ = addr; + state_ = CS_CONNECTED; + server_->AddConnection(remote_addr_, local_addr_, this); + if (async_ && notify) { + SignalConnectEvent(this); + } + } + + int SendUdp(const void* pv, size_t cb, const SocketAddress& addr) { + // If we have not been assigned a local port, then get one. + if (local_addr_.IsNil()) { + local_addr_ = EmptySocketAddressWithFamily(addr.ipaddr().family()); + int result = server_->Bind(this, &local_addr_); + if (result != 0) { + local_addr_.Clear(); + error_ = EADDRINUSE; + return result; + } + } + + // Send the data in a message to the appropriate socket. + return server_->SendUdp(this, static_cast(pv), cb, addr); + } + + int SendTcp(const void* pv, size_t cb) { + size_t capacity = server_->send_buffer_capacity_ - send_buffer_.size(); + if (0 == capacity) { + write_enabled_ = true; + error_ = EWOULDBLOCK; + return -1; + } + size_t consumed = _min(cb, capacity); + const char* cpv = static_cast(pv); + send_buffer_.insert(send_buffer_.end(), cpv, cpv + consumed); + server_->SendTcp(this); + return static_cast(consumed); + } + + VirtualSocketServer* server_; + int family_; + int type_; + bool async_; + ConnState state_; + int error_; + SocketAddress local_addr_; + SocketAddress remote_addr_; + + // Pending sockets which can be Accepted + ListenQueue* listen_queue_; + + // Data which tcp has buffered for sending + SendBuffer send_buffer_; + bool write_enabled_; + + // Critical section to protect the recv_buffer and queue_ + CriticalSection crit_; + + // Network model that enforces bandwidth and capacity constraints + NetworkQueue network_; + size_t network_size_; + + // Data which has been received from the network + RecvBuffer recv_buffer_; + // The amount of data which is in flight or in recv_buffer_ + size_t recv_buffer_size_; + + // Is this socket bound? + bool bound_; + + // When we bind a socket to Any, VSS's Bind gives it another address. For + // dual-stack sockets, we want to distinguish between sockets that were + // explicitly given a particular address and sockets that had one picked + // for them by VSS. + bool was_any_; + + // Store the options that are set + OptionsMap options_map_; + + friend class VirtualSocketServer; +}; + +VirtualSocketServer::VirtualSocketServer(SocketServer* ss) + : server_(ss), server_owned_(false), msg_queue_(NULL), stop_on_idle_(false), + network_delay_(Time()), next_ipv4_(kInitialNextIPv4), + next_ipv6_(kInitialNextIPv6), next_port_(kFirstEphemeralPort), + bindings_(new AddressMap()), connections_(new ConnectionMap()), + bandwidth_(0), network_capacity_(kDefaultNetworkCapacity), + send_buffer_capacity_(kDefaultTcpBufferSize), + recv_buffer_capacity_(kDefaultTcpBufferSize), + delay_mean_(0), delay_stddev_(0), delay_samples_(NUM_SAMPLES), + delay_dist_(NULL), drop_prob_(0.0) { + if (!server_) { + server_ = new PhysicalSocketServer(); + server_owned_ = true; + } + UpdateDelayDistribution(); +} + +VirtualSocketServer::~VirtualSocketServer() { + delete bindings_; + delete connections_; + delete delay_dist_; + if (server_owned_) { + delete server_; + } +} + +IPAddress VirtualSocketServer::GetNextIP(int family) { + if (family == AF_INET) { + IPAddress next_ip(next_ipv4_); + next_ipv4_.s_addr = + HostToNetwork32(NetworkToHost32(next_ipv4_.s_addr) + 1); + return next_ip; + } else if (family == AF_INET6) { + IPAddress next_ip(next_ipv6_); + uint32* as_ints = reinterpret_cast(&next_ipv6_.s6_addr); + as_ints[3] += 1; + return next_ip; + } + return IPAddress(); +} + +uint16 VirtualSocketServer::GetNextPort() { + uint16 port = next_port_; + if (next_port_ < kLastEphemeralPort) { + ++next_port_; + } else { + next_port_ = kFirstEphemeralPort; + } + return port; +} + +Socket* VirtualSocketServer::CreateSocket(int type) { + return CreateSocket(AF_INET, type); +} + +Socket* VirtualSocketServer::CreateSocket(int family, int type) { + return CreateSocketInternal(family, type); +} + +AsyncSocket* VirtualSocketServer::CreateAsyncSocket(int type) { + return CreateAsyncSocket(AF_INET, type); +} + +AsyncSocket* VirtualSocketServer::CreateAsyncSocket(int family, int type) { + return CreateSocketInternal(family, type); +} + +VirtualSocket* VirtualSocketServer::CreateSocketInternal(int family, int type) { + return new VirtualSocket(this, family, type, true); +} + +void VirtualSocketServer::SetMessageQueue(MessageQueue* msg_queue) { + msg_queue_ = msg_queue; + if (msg_queue_) { + msg_queue_->SignalQueueDestroyed.connect(this, + &VirtualSocketServer::OnMessageQueueDestroyed); + } +} + +bool VirtualSocketServer::Wait(int cmsWait, bool process_io) { + ASSERT(msg_queue_ == Thread::Current()); + if (stop_on_idle_ && Thread::Current()->empty()) { + return false; + } + return socketserver()->Wait(cmsWait, process_io); +} + +void VirtualSocketServer::WakeUp() { + socketserver()->WakeUp(); +} + +bool VirtualSocketServer::ProcessMessagesUntilIdle() { + ASSERT(msg_queue_ == Thread::Current()); + stop_on_idle_ = true; + while (!msg_queue_->empty()) { + Message msg; + if (msg_queue_->Get(&msg, kForever)) { + msg_queue_->Dispatch(&msg); + } + } + stop_on_idle_ = false; + return !msg_queue_->IsQuitting(); +} + +int VirtualSocketServer::Bind(VirtualSocket* socket, + const SocketAddress& addr) { + ASSERT(NULL != socket); + // Address must be completely specified at this point + ASSERT(!IPIsUnspec(addr.ipaddr())); + ASSERT(addr.port() != 0); + + // Normalize the address (turns v6-mapped addresses into v4-addresses). + SocketAddress normalized(addr.ipaddr().Normalized(), addr.port()); + + AddressMap::value_type entry(normalized, socket); + return bindings_->insert(entry).second ? 0 : -1; +} + +int VirtualSocketServer::Bind(VirtualSocket* socket, SocketAddress* addr) { + ASSERT(NULL != socket); + + if (IPIsAny(addr->ipaddr())) { + addr->SetIP(GetNextIP(addr->ipaddr().family())); + } else if (!IPIsUnspec(addr->ipaddr())) { + addr->SetIP(addr->ipaddr().Normalized()); + } else { + ASSERT(false); + } + + if (addr->port() == 0) { + for (int i = 0; i < kEphemeralPortCount; ++i) { + addr->SetPort(GetNextPort()); + if (bindings_->find(*addr) == bindings_->end()) { + break; + } + } + } + + return Bind(socket, *addr); +} + +VirtualSocket* VirtualSocketServer::LookupBinding(const SocketAddress& addr) { + SocketAddress normalized(addr.ipaddr().Normalized(), + addr.port()); + AddressMap::iterator it = bindings_->find(normalized); + return (bindings_->end() != it) ? it->second : NULL; +} + +int VirtualSocketServer::Unbind(const SocketAddress& addr, + VirtualSocket* socket) { + SocketAddress normalized(addr.ipaddr().Normalized(), + addr.port()); + ASSERT((*bindings_)[normalized] == socket); + bindings_->erase(bindings_->find(normalized)); + return 0; +} + +void VirtualSocketServer::AddConnection(const SocketAddress& local, + const SocketAddress& remote, + VirtualSocket* remote_socket) { + // Add this socket pair to our routing table. This will allow + // multiple clients to connect to the same server address. + SocketAddress local_normalized(local.ipaddr().Normalized(), + local.port()); + SocketAddress remote_normalized(remote.ipaddr().Normalized(), + remote.port()); + SocketAddressPair address_pair(local_normalized, remote_normalized); + connections_->insert(std::pair(address_pair, remote_socket)); +} + +VirtualSocket* VirtualSocketServer::LookupConnection( + const SocketAddress& local, + const SocketAddress& remote) { + SocketAddress local_normalized(local.ipaddr().Normalized(), + local.port()); + SocketAddress remote_normalized(remote.ipaddr().Normalized(), + remote.port()); + SocketAddressPair address_pair(local_normalized, remote_normalized); + ConnectionMap::iterator it = connections_->find(address_pair); + return (connections_->end() != it) ? it->second : NULL; +} + +void VirtualSocketServer::RemoveConnection(const SocketAddress& local, + const SocketAddress& remote) { + SocketAddress local_normalized(local.ipaddr().Normalized(), + local.port()); + SocketAddress remote_normalized(remote.ipaddr().Normalized(), + remote.port()); + SocketAddressPair address_pair(local_normalized, remote_normalized); + connections_->erase(address_pair); +} + +static double Random() { + return static_cast(rand()) / RAND_MAX; +} + +int VirtualSocketServer::Connect(VirtualSocket* socket, + const SocketAddress& remote_addr, + bool use_delay) { + uint32 delay = use_delay ? GetRandomTransitDelay() : 0; + VirtualSocket* remote = LookupBinding(remote_addr); + if (!CanInteractWith(socket, remote)) { + LOG(LS_INFO) << "Address family mismatch between " + << socket->GetLocalAddress() << " and " << remote_addr; + return -1; + } + if (remote != NULL) { + SocketAddress addr = socket->GetLocalAddress(); + msg_queue_->PostDelayed(delay, remote, MSG_ID_CONNECT, + new MessageAddress(addr)); + } else { + LOG(LS_INFO) << "No one listening at " << remote_addr; + msg_queue_->PostDelayed(delay, socket, MSG_ID_DISCONNECT); + } + return 0; +} + +bool VirtualSocketServer::Disconnect(VirtualSocket* socket) { + if (socket) { + // Remove the mapping. + msg_queue_->Post(socket, MSG_ID_DISCONNECT); + return true; + } + return false; +} + +int VirtualSocketServer::SendUdp(VirtualSocket* socket, + const char* data, size_t data_size, + const SocketAddress& remote_addr) { + // See if we want to drop this packet. + if (Random() < drop_prob_) { + LOG(LS_VERBOSE) << "Dropping packet: bad luck"; + return static_cast(data_size); + } + + VirtualSocket* recipient = LookupBinding(remote_addr); + if (!recipient) { + // Make a fake recipient for address family checking. + scoped_ptr dummy_socket( + CreateSocketInternal(AF_INET, SOCK_DGRAM)); + dummy_socket->SetLocalAddress(remote_addr); + if (!CanInteractWith(socket, dummy_socket.get())) { + LOG(LS_VERBOSE) << "Incompatible address families: " + << socket->GetLocalAddress() << " and " << remote_addr; + return -1; + } + LOG(LS_VERBOSE) << "No one listening at " << remote_addr; + return static_cast(data_size); + } + + if (!CanInteractWith(socket, recipient)) { + LOG(LS_VERBOSE) << "Incompatible address families: " + << socket->GetLocalAddress() << " and " << remote_addr; + return -1; + } + + CritScope cs(&socket->crit_); + + uint32 cur_time = Time(); + PurgeNetworkPackets(socket, cur_time); + + // Determine whether we have enough bandwidth to accept this packet. To do + // this, we need to update the send queue. Once we know it's current size, + // we know whether we can fit this packet. + // + // NOTE: There are better algorithms for maintaining such a queue (such as + // "Derivative Random Drop"); however, this algorithm is a more accurate + // simulation of what a normal network would do. + + size_t packet_size = data_size + UDP_HEADER_SIZE; + if (socket->network_size_ + packet_size > network_capacity_) { + LOG(LS_VERBOSE) << "Dropping packet: network capacity exceeded"; + return static_cast(data_size); + } + + AddPacketToNetwork(socket, recipient, cur_time, data, data_size, + UDP_HEADER_SIZE, false); + + return static_cast(data_size); +} + +void VirtualSocketServer::SendTcp(VirtualSocket* socket) { + // TCP can't send more data than will fill up the receiver's buffer. + // We track the data that is in the buffer plus data in flight using the + // recipient's recv_buffer_size_. Anything beyond that must be stored in the + // sender's buffer. We will trigger the buffered data to be sent when data + // is read from the recv_buffer. + + // Lookup the local/remote pair in the connections table. + VirtualSocket* recipient = LookupConnection(socket->local_addr_, + socket->remote_addr_); + if (!recipient) { + LOG(LS_VERBOSE) << "Sending data to no one."; + return; + } + + CritScope cs(&socket->crit_); + + uint32 cur_time = Time(); + PurgeNetworkPackets(socket, cur_time); + + while (true) { + size_t available = recv_buffer_capacity_ - recipient->recv_buffer_size_; + size_t max_data_size = _min(available, TCP_MSS - TCP_HEADER_SIZE); + size_t data_size = _min(socket->send_buffer_.size(), max_data_size); + if (0 == data_size) + break; + + AddPacketToNetwork(socket, recipient, cur_time, &socket->send_buffer_[0], + data_size, TCP_HEADER_SIZE, true); + recipient->recv_buffer_size_ += data_size; + + size_t new_buffer_size = socket->send_buffer_.size() - data_size; + // Avoid undefined access beyond the last element of the vector. + // This only happens when new_buffer_size is 0. + if (data_size < socket->send_buffer_.size()) { + // memmove is required for potentially overlapping source/destination. + memmove(&socket->send_buffer_[0], &socket->send_buffer_[data_size], + new_buffer_size); + } + socket->send_buffer_.resize(new_buffer_size); + } + + if (socket->write_enabled_ + && (socket->send_buffer_.size() < send_buffer_capacity_)) { + socket->write_enabled_ = false; + socket->SignalWriteEvent(socket); + } +} + +void VirtualSocketServer::AddPacketToNetwork(VirtualSocket* sender, + VirtualSocket* recipient, + uint32 cur_time, + const char* data, + size_t data_size, + size_t header_size, + bool ordered) { + VirtualSocket::NetworkEntry entry; + entry.size = data_size + header_size; + + sender->network_size_ += entry.size; + uint32 send_delay = SendDelay(static_cast(sender->network_size_)); + entry.done_time = cur_time + send_delay; + sender->network_.push_back(entry); + + // Find the delay for crossing the many virtual hops of the network. + uint32 transit_delay = GetRandomTransitDelay(); + + // Post the packet as a message to be delivered (on our own thread) + Packet* p = new Packet(data, data_size, sender->local_addr_); + uint32 ts = TimeAfter(send_delay + transit_delay); + if (ordered) { + // Ensure that new packets arrive after previous ones + // TODO: consider ordering on a per-socket basis, since this + // introduces artifical delay. + ts = TimeMax(ts, network_delay_); + } + msg_queue_->PostAt(ts, recipient, MSG_ID_PACKET, p); + network_delay_ = TimeMax(ts, network_delay_); +} + +void VirtualSocketServer::PurgeNetworkPackets(VirtualSocket* socket, + uint32 cur_time) { + while (!socket->network_.empty() && + (socket->network_.front().done_time <= cur_time)) { + ASSERT(socket->network_size_ >= socket->network_.front().size); + socket->network_size_ -= socket->network_.front().size; + socket->network_.pop_front(); + } +} + +uint32 VirtualSocketServer::SendDelay(uint32 size) { + if (bandwidth_ == 0) + return 0; + else + return 1000 * size / bandwidth_; +} + +#if 0 +void PrintFunction(std::vector >* f) { + return; + double sum = 0; + for (uint32 i = 0; i < f->size(); ++i) { + std::cout << (*f)[i].first << '\t' << (*f)[i].second << std::endl; + sum += (*f)[i].second; + } + if (!f->empty()) { + const double mean = sum / f->size(); + double sum_sq_dev = 0; + for (uint32 i = 0; i < f->size(); ++i) { + double dev = (*f)[i].second - mean; + sum_sq_dev += dev * dev; + } + std::cout << "Mean = " << mean << " StdDev = " + << sqrt(sum_sq_dev / f->size()) << std::endl; + } +} +#endif // + +void VirtualSocketServer::UpdateDelayDistribution() { + Function* dist = CreateDistribution(delay_mean_, delay_stddev_, + delay_samples_); + // We take a lock just to make sure we don't leak memory. + { + CritScope cs(&delay_crit_); + delete delay_dist_; + delay_dist_ = dist; + } +} + +static double PI = 4 * std::atan(1.0); + +static double Normal(double x, double mean, double stddev) { + double a = (x - mean) * (x - mean) / (2 * stddev * stddev); + return std::exp(-a) / (stddev * sqrt(2 * PI)); +} + +#if 0 // static unused gives a warning +static double Pareto(double x, double min, double k) { + if (x < min) + return 0; + else + return k * std::pow(min, k) / std::pow(x, k+1); +} +#endif + +VirtualSocketServer::Function* VirtualSocketServer::CreateDistribution( + uint32 mean, uint32 stddev, uint32 samples) { + Function* f = new Function(); + + if (0 == stddev) { + f->push_back(Point(mean, 1.0)); + } else { + double start = 0; + if (mean >= 4 * static_cast(stddev)) + start = mean - 4 * static_cast(stddev); + double end = mean + 4 * static_cast(stddev); + + for (uint32 i = 0; i < samples; i++) { + double x = start + (end - start) * i / (samples - 1); + double y = Normal(x, mean, stddev); + f->push_back(Point(x, y)); + } + } + return Resample(Invert(Accumulate(f)), 0, 1, samples); +} + +uint32 VirtualSocketServer::GetRandomTransitDelay() { + size_t index = rand() % delay_dist_->size(); + double delay = (*delay_dist_)[index].second; + //LOG_F(LS_INFO) << "random[" << index << "] = " << delay; + return static_cast(delay); +} + +struct FunctionDomainCmp { + bool operator()(const VirtualSocketServer::Point& p1, + const VirtualSocketServer::Point& p2) { + return p1.first < p2.first; + } + bool operator()(double v1, const VirtualSocketServer::Point& p2) { + return v1 < p2.first; + } + bool operator()(const VirtualSocketServer::Point& p1, double v2) { + return p1.first < v2; + } +}; + +VirtualSocketServer::Function* VirtualSocketServer::Accumulate(Function* f) { + ASSERT(f->size() >= 1); + double v = 0; + for (Function::size_type i = 0; i < f->size() - 1; ++i) { + double dx = (*f)[i + 1].first - (*f)[i].first; + double avgy = ((*f)[i + 1].second + (*f)[i].second) / 2; + (*f)[i].second = v; + v = v + dx * avgy; + } + (*f)[f->size()-1].second = v; + return f; +} + +VirtualSocketServer::Function* VirtualSocketServer::Invert(Function* f) { + for (Function::size_type i = 0; i < f->size(); ++i) + std::swap((*f)[i].first, (*f)[i].second); + + std::sort(f->begin(), f->end(), FunctionDomainCmp()); + return f; +} + +VirtualSocketServer::Function* VirtualSocketServer::Resample( + Function* f, double x1, double x2, uint32 samples) { + Function* g = new Function(); + + for (size_t i = 0; i < samples; i++) { + double x = x1 + (x2 - x1) * i / (samples - 1); + double y = Evaluate(f, x); + g->push_back(Point(x, y)); + } + + delete f; + return g; +} + +double VirtualSocketServer::Evaluate(Function* f, double x) { + Function::iterator iter = + std::lower_bound(f->begin(), f->end(), x, FunctionDomainCmp()); + if (iter == f->begin()) { + return (*f)[0].second; + } else if (iter == f->end()) { + ASSERT(f->size() >= 1); + return (*f)[f->size() - 1].second; + } else if (iter->first == x) { + return iter->second; + } else { + double x1 = (iter - 1)->first; + double y1 = (iter - 1)->second; + double x2 = iter->first; + double y2 = iter->second; + return y1 + (y2 - y1) * (x - x1) / (x2 - x1); + } +} + +bool VirtualSocketServer::CanInteractWith(VirtualSocket* local, + VirtualSocket* remote) { + if (!local || !remote) { + return false; + } + IPAddress local_ip = local->GetLocalAddress().ipaddr(); + IPAddress remote_ip = remote->GetLocalAddress().ipaddr(); + IPAddress local_normalized = local_ip.Normalized(); + IPAddress remote_normalized = remote_ip.Normalized(); + // Check if the addresses are the same family after Normalization (turns + // mapped IPv6 address into IPv4 addresses). + // This will stop unmapped V6 addresses from talking to mapped V6 addresses. + if (local_normalized.family() == remote_normalized.family()) { + return true; + } + + // If ip1 is IPv4 and ip2 is :: and ip2 is not IPV6_V6ONLY. + int remote_v6_only = 0; + remote->GetOption(Socket::OPT_IPV6_V6ONLY, &remote_v6_only); + if (local_ip.family() == AF_INET && !remote_v6_only && IPIsAny(remote_ip)) { + return true; + } + // Same check, backwards. + int local_v6_only = 0; + local->GetOption(Socket::OPT_IPV6_V6ONLY, &local_v6_only); + if (remote_ip.family() == AF_INET && !local_v6_only && IPIsAny(local_ip)) { + return true; + } + + // Check to see if either socket was explicitly bound to IPv6-any. + // These sockets can talk with anyone. + if (local_ip.family() == AF_INET6 && local->was_any()) { + return true; + } + if (remote_ip.family() == AF_INET6 && remote->was_any()) { + return true; + } + + return false; +} + +} // namespace talk_base diff --git a/talk/base/virtualsocketserver.h b/talk/base/virtualsocketserver.h new file mode 100644 index 000000000..280ae6572 --- /dev/null +++ b/talk/base/virtualsocketserver.h @@ -0,0 +1,250 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_VIRTUALSOCKETSERVER_H_ +#define TALK_BASE_VIRTUALSOCKETSERVER_H_ + +#include +#include +#include + +#include "talk/base/messagequeue.h" +#include "talk/base/socketserver.h" + +namespace talk_base { + +class VirtualSocket; +class SocketAddressPair; + +// Simulates a network in the same manner as a loopback interface. The +// interface can create as many addresses as you want. All of the sockets +// created by this network will be able to communicate with one another, unless +// they are bound to addresses from incompatible families. +class VirtualSocketServer : public SocketServer, public sigslot::has_slots<> { + public: + // TODO: Add "owned" parameter. + // If "owned" is set, the supplied socketserver will be deleted later. + explicit VirtualSocketServer(SocketServer* ss); + virtual ~VirtualSocketServer(); + + SocketServer* socketserver() { return server_; } + + // Limits the network bandwidth (maximum bytes per second). Zero means that + // all sends occur instantly. Defaults to 0. + uint32 bandwidth() const { return bandwidth_; } + void set_bandwidth(uint32 bandwidth) { bandwidth_ = bandwidth; } + + // Limits the amount of data which can be in flight on the network without + // packet loss (on a per sender basis). Defaults to 64 KB. + uint32 network_capacity() const { return network_capacity_; } + void set_network_capacity(uint32 capacity) { + network_capacity_ = capacity; + } + + // The amount of data which can be buffered by tcp on the sender's side + uint32 send_buffer_capacity() const { return send_buffer_capacity_; } + void set_send_buffer_capacity(uint32 capacity) { + send_buffer_capacity_ = capacity; + } + + // The amount of data which can be buffered by tcp on the receiver's side + uint32 recv_buffer_capacity() const { return recv_buffer_capacity_; } + void set_recv_buffer_capacity(uint32 capacity) { + recv_buffer_capacity_ = capacity; + } + + // Controls the (transit) delay for packets sent in the network. This does + // not inclue the time required to sit in the send queue. Both of these + // values are measured in milliseconds. Defaults to no delay. + uint32 delay_mean() const { return delay_mean_; } + uint32 delay_stddev() const { return delay_stddev_; } + uint32 delay_samples() const { return delay_samples_; } + void set_delay_mean(uint32 delay_mean) { delay_mean_ = delay_mean; } + void set_delay_stddev(uint32 delay_stddev) { + delay_stddev_ = delay_stddev; + } + void set_delay_samples(uint32 delay_samples) { + delay_samples_ = delay_samples; + } + + // If the (transit) delay parameters are modified, this method should be + // called to recompute the new distribution. + void UpdateDelayDistribution(); + + // Controls the (uniform) probability that any sent packet is dropped. This + // is separate from calculations to drop based on queue size. + double drop_probability() { return drop_prob_; } + void set_drop_probability(double drop_prob) { + assert((0 <= drop_prob) && (drop_prob <= 1)); + drop_prob_ = drop_prob; + } + + // SocketFactory: + virtual Socket* CreateSocket(int type); + virtual Socket* CreateSocket(int family, int type); + + virtual AsyncSocket* CreateAsyncSocket(int type); + virtual AsyncSocket* CreateAsyncSocket(int family, int type); + + // SocketServer: + virtual void SetMessageQueue(MessageQueue* queue); + virtual bool Wait(int cms, bool process_io); + virtual void WakeUp(); + + typedef std::pair Point; + typedef std::vector Function; + + static Function* CreateDistribution(uint32 mean, uint32 stddev, + uint32 samples); + + // Similar to Thread::ProcessMessages, but it only processes messages until + // there are no immediate messages or pending network traffic. Returns false + // if Thread::Stop() was called. + bool ProcessMessagesUntilIdle(); + + protected: + // Returns a new IP not used before in this network. + IPAddress GetNextIP(int family); + uint16 GetNextPort(); + + VirtualSocket* CreateSocketInternal(int family, int type); + + // Binds the given socket to addr, assigning and IP and Port if necessary + int Bind(VirtualSocket* socket, SocketAddress* addr); + + // Binds the given socket to the given (fully-defined) address. + int Bind(VirtualSocket* socket, const SocketAddress& addr); + + // Find the socket bound to the given address + VirtualSocket* LookupBinding(const SocketAddress& addr); + + int Unbind(const SocketAddress& addr, VirtualSocket* socket); + + // Adds a mapping between this socket pair and the socket. + void AddConnection(const SocketAddress& client, + const SocketAddress& server, + VirtualSocket* socket); + + // Find the socket pair corresponding to this server address. + VirtualSocket* LookupConnection(const SocketAddress& client, + const SocketAddress& server); + + void RemoveConnection(const SocketAddress& client, + const SocketAddress& server); + + // Connects the given socket to the socket at the given address + int Connect(VirtualSocket* socket, const SocketAddress& remote_addr, + bool use_delay); + + // Sends a disconnect message to the socket at the given address + bool Disconnect(VirtualSocket* socket); + + // Sends the given packet to the socket at the given address (if one exists). + int SendUdp(VirtualSocket* socket, const char* data, size_t data_size, + const SocketAddress& remote_addr); + + // Moves as much data as possible from the sender's buffer to the network + void SendTcp(VirtualSocket* socket); + + // Places a packet on the network. + void AddPacketToNetwork(VirtualSocket* socket, VirtualSocket* recipient, + uint32 cur_time, const char* data, size_t data_size, + size_t header_size, bool ordered); + + // Removes stale packets from the network + void PurgeNetworkPackets(VirtualSocket* socket, uint32 cur_time); + + // Computes the number of milliseconds required to send a packet of this size. + uint32 SendDelay(uint32 size); + + // Returns a random transit delay chosen from the appropriate distribution. + uint32 GetRandomTransitDelay(); + + // Basic operations on functions. Those that return a function also take + // ownership of the function given (and hence, may modify or delete it). + static Function* Accumulate(Function* f); + static Function* Invert(Function* f); + static Function* Resample(Function* f, double x1, double x2, uint32 samples); + static double Evaluate(Function* f, double x); + + // NULL out our message queue if it goes away. Necessary in the case where + // our lifetime is greater than that of the thread we are using, since we + // try to send Close messages for all connected sockets when we shutdown. + void OnMessageQueueDestroyed() { msg_queue_ = NULL; } + + // Determine if two sockets should be able to communicate. + // We don't (currently) specify an address family for sockets; instead, + // the currently bound address is used to infer the address family. + // Any socket that is not explicitly bound to an IPv4 address is assumed to be + // dual-stack capable. + // This function tests if two addresses can communicate, as well as the + // sockets to which they may be bound (the addresses may or may not yet be + // bound to the sockets). + // First the addresses are tested (after normalization): + // If both have the same family, then communication is OK. + // If only one is IPv4 then false, unless the other is bound to ::. + // This applies even if the IPv4 address is 0.0.0.0. + // The socket arguments are optional; the sockets are checked to see if they + // were explicitly bound to IPv6-any ('::'), and if so communication is + // permitted. + // NB: This scheme doesn't permit non-dualstack IPv6 sockets. + static bool CanInteractWith(VirtualSocket* local, VirtualSocket* remote); + + private: + friend class VirtualSocket; + + typedef std::map AddressMap; + typedef std::map ConnectionMap; + + SocketServer* server_; + bool server_owned_; + MessageQueue* msg_queue_; + bool stop_on_idle_; + uint32 network_delay_; + in_addr next_ipv4_; + in6_addr next_ipv6_; + uint16 next_port_; + AddressMap* bindings_; + ConnectionMap* connections_; + + uint32 bandwidth_; + uint32 network_capacity_; + uint32 send_buffer_capacity_; + uint32 recv_buffer_capacity_; + uint32 delay_mean_; + uint32 delay_stddev_; + uint32 delay_samples_; + Function* delay_dist_; + CriticalSection delay_crit_; + + double drop_prob_; + DISALLOW_EVIL_CONSTRUCTORS(VirtualSocketServer); +}; + +} // namespace talk_base + +#endif // TALK_BASE_VIRTUALSOCKETSERVER_H_ diff --git a/talk/base/win32.cc b/talk/base/win32.cc new file mode 100644 index 000000000..a9fda8af9 --- /dev/null +++ b/talk/base/win32.cc @@ -0,0 +1,473 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/win32.h" + +#include +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" + +namespace talk_base { + +// Helper function declarations for inet_ntop/inet_pton. +static const char* inet_ntop_v4(const void* src, char* dst, socklen_t size); +static const char* inet_ntop_v6(const void* src, char* dst, socklen_t size); +static int inet_pton_v4(const char* src, void* dst); +static int inet_pton_v6(const char* src, void* dst); + +// Implementation of inet_ntop (create a printable representation of an +// ip address). XP doesn't have its own inet_ntop, and +// WSAAddressToString requires both IPv6 to be installed and for Winsock +// to be initialized. +const char* win32_inet_ntop(int af, const void *src, + char* dst, socklen_t size) { + if (!src || !dst) { + return NULL; + } + switch (af) { + case AF_INET: { + return inet_ntop_v4(src, dst, size); + } + case AF_INET6: { + return inet_ntop_v6(src, dst, size); + } + } + return NULL; +} + +// As above, but for inet_pton. Implements inet_pton for v4 and v6. +// Note that our inet_ntop will output normal 'dotted' v4 addresses only. +int win32_inet_pton(int af, const char* src, void* dst) { + if (!src || !dst) { + return 0; + } + if (af == AF_INET) { + return inet_pton_v4(src, dst); + } else if (af == AF_INET6) { + return inet_pton_v6(src, dst); + } + return -1; +} + +// Helper function for inet_ntop for IPv4 addresses. +// Outputs "dotted-quad" decimal notation. +const char* inet_ntop_v4(const void* src, char* dst, socklen_t size) { + if (size < INET_ADDRSTRLEN) { + return NULL; + } + const struct in_addr* as_in_addr = + reinterpret_cast(src); + talk_base::sprintfn(dst, size, "%d.%d.%d.%d", + as_in_addr->S_un.S_un_b.s_b1, + as_in_addr->S_un.S_un_b.s_b2, + as_in_addr->S_un.S_un_b.s_b3, + as_in_addr->S_un.S_un_b.s_b4); + return dst; +} + +// Helper function for inet_ntop for IPv6 addresses. +const char* inet_ntop_v6(const void* src, char* dst, socklen_t size) { + if (size < INET6_ADDRSTRLEN) { + return NULL; + } + const uint16* as_shorts = + reinterpret_cast(src); + int runpos[8]; + int current = 1; + int max = 1; + int maxpos = -1; + int run_array_size = ARRAY_SIZE(runpos); + // Run over the address marking runs of 0s. + for (int i = 0; i < run_array_size; ++i) { + if (as_shorts[i] == 0) { + runpos[i] = current; + if (current > max) { + maxpos = i; + max = current; + } + ++current; + } else { + runpos[i] = -1; + current =1; + } + } + + if (max > 1) { + int tmpmax = maxpos; + // Run back through, setting -1 for all but the longest run. + for (int i = run_array_size - 1; i >= 0; i--) { + if (i > tmpmax) { + runpos[i] = -1; + } else if (runpos[i] == -1) { + // We're less than maxpos, we hit a -1, so the 'good' run is done. + // Setting tmpmax -1 means all remaining positions get set to -1. + tmpmax = -1; + } + } + } + + char* cursor = dst; + // Print IPv4 compatible and IPv4 mapped addresses using the IPv4 helper. + // These addresses have an initial run of either eight zero-bytes followed + // by 0xFFFF, or an initial run of ten zero-bytes. + if (runpos[0] == 1 && (maxpos == 5 || + (maxpos == 4 && as_shorts[5] == 0xFFFF))) { + *cursor++ = ':'; + *cursor++ = ':'; + if (maxpos == 4) { + cursor += talk_base::sprintfn(cursor, INET6_ADDRSTRLEN - 2, "ffff:"); + } + const struct in_addr* as_v4 = + reinterpret_cast(&(as_shorts[6])); + inet_ntop_v4(as_v4, cursor, + static_cast(INET6_ADDRSTRLEN - (cursor - dst))); + } else { + for (int i = 0; i < run_array_size; ++i) { + if (runpos[i] == -1) { + cursor += talk_base::sprintfn(cursor, + INET6_ADDRSTRLEN - (cursor - dst), + "%x", NetworkToHost16(as_shorts[i])); + if (i != 7 && runpos[i + 1] != 1) { + *cursor++ = ':'; + } + } else if (runpos[i] == 1) { + // Entered the run; print the colons and skip the run. + *cursor++ = ':'; + *cursor++ = ':'; + i += (max - 1); + } + } + } + return dst; +} + +// Helper function for inet_pton for IPv4 addresses. +// |src| points to a character string containing an IPv4 network address in +// dotted-decimal format, "ddd.ddd.ddd.ddd", where ddd is a decimal number +// of up to three digits in the range 0 to 255. +// The address is converted and copied to dst, +// which must be sizeof(struct in_addr) (4) bytes (32 bits) long. +int inet_pton_v4(const char* src, void* dst) { + const int kIpv4AddressSize = 4; + int found = 0; + const char* src_pos = src; + unsigned char result[kIpv4AddressSize] = {0}; + + while (*src_pos != '\0') { + // strtol won't treat whitespace characters in the begining as an error, + // so check to ensure this is started with digit before passing to strtol. + if (!isdigit(*src_pos)) { + return 0; + } + char* end_pos; + long value = strtol(src_pos, &end_pos, 10); + if (value < 0 || value > 255 || src_pos == end_pos) { + return 0; + } + ++found; + if (found > kIpv4AddressSize) { + return 0; + } + result[found - 1] = static_cast(value); + src_pos = end_pos; + if (*src_pos == '.') { + // There's more. + ++src_pos; + } else if (*src_pos != '\0') { + // If it's neither '.' nor '\0' then return fail. + return 0; + } + } + if (found != kIpv4AddressSize) { + return 0; + } + memcpy(dst, result, sizeof(result)); + return 1; +} + +// Helper function for inet_pton for IPv6 addresses. +int inet_pton_v6(const char* src, void* dst) { + // sscanf will pick any other invalid chars up, but it parses 0xnnnn as hex. + // Check for literal x in the input string. + const char* readcursor = src; + char c = *readcursor++; + while (c) { + if (c == 'x') { + return 0; + } + c = *readcursor++; + } + readcursor = src; + + struct in6_addr an_addr; + memset(&an_addr, 0, sizeof(an_addr)); + + uint16* addr_cursor = reinterpret_cast(&an_addr.s6_addr[0]); + uint16* addr_end = reinterpret_cast(&an_addr.s6_addr[16]); + bool seencompressed = false; + + // Addresses that start with "::" (i.e., a run of initial zeros) or + // "::ffff:" can potentially be IPv4 mapped or compatibility addresses. + // These have dotted-style IPv4 addresses on the end (e.g. "::192.168.7.1"). + if (*readcursor == ':' && *(readcursor+1) == ':' && + *(readcursor + 2) != 0) { + // Check for periods, which we'll take as a sign of v4 addresses. + const char* addrstart = readcursor + 2; + if (talk_base::strchr(addrstart, ".")) { + const char* colon = talk_base::strchr(addrstart, "::"); + if (colon) { + uint16 a_short; + int bytesread = 0; + if (sscanf(addrstart, "%hx%n", &a_short, &bytesread) != 1 || + a_short != 0xFFFF || bytesread != 4) { + // Colons + periods means has to be ::ffff:a.b.c.d. But it wasn't. + return 0; + } else { + an_addr.s6_addr[10] = 0xFF; + an_addr.s6_addr[11] = 0xFF; + addrstart = colon + 1; + } + } + struct in_addr v4; + if (inet_pton_v4(addrstart, &v4.s_addr)) { + memcpy(&an_addr.s6_addr[12], &v4, sizeof(v4)); + memcpy(dst, &an_addr, sizeof(an_addr)); + return 1; + } else { + // Invalid v4 address. + return 0; + } + } + } + + // For addresses without a trailing IPv4 component ('normal' IPv6 addresses). + while (*readcursor != 0 && addr_cursor < addr_end) { + if (*readcursor == ':') { + if (*(readcursor + 1) == ':') { + if (seencompressed) { + // Can only have one compressed run of zeroes ("::") per address. + return 0; + } + // Hit a compressed run. Count colons to figure out how much of the + // address is skipped. + readcursor += 2; + const char* coloncounter = readcursor; + int coloncount = 0; + if (*coloncounter == 0) { + // Special case - trailing ::. + addr_cursor = addr_end; + } else { + while (*coloncounter) { + if (*coloncounter == ':') { + ++coloncount; + } + ++coloncounter; + } + // (coloncount + 1) is the number of shorts left in the address. + addr_cursor = addr_end - (coloncount + 1); + seencompressed = true; + } + } else { + ++readcursor; + } + } else { + uint16 word; + int bytesread = 0; + if (sscanf(readcursor, "%hx%n", &word, &bytesread) != 1) { + return 0; + } else { + *addr_cursor = HostToNetwork16(word); + ++addr_cursor; + readcursor += bytesread; + if (*readcursor != ':' && *readcursor != '\0') { + return 0; + } + } + } + } + + if (*readcursor != '\0' || addr_cursor < addr_end) { + // Catches addresses too short or too long. + return 0; + } + memcpy(dst, &an_addr, sizeof(an_addr)); + return 1; +} + +// +// Unix time is in seconds relative to 1/1/1970. So we compute the windows +// FILETIME of that time/date, then we add/subtract in appropriate units to +// convert to/from unix time. +// The units of FILETIME are 100ns intervals, so by multiplying by or dividing +// by 10000000, we can convert to/from seconds. +// +// FileTime = UnixTime*10000000 + FileTime(1970) +// UnixTime = (FileTime-FileTime(1970))/10000000 +// + +void FileTimeToUnixTime(const FILETIME& ft, time_t* ut) { + ASSERT(NULL != ut); + + // FILETIME has an earlier date base than time_t (1/1/1970), so subtract off + // the difference. + SYSTEMTIME base_st; + memset(&base_st, 0, sizeof(base_st)); + base_st.wDay = 1; + base_st.wMonth = 1; + base_st.wYear = 1970; + + FILETIME base_ft; + SystemTimeToFileTime(&base_st, &base_ft); + + ULARGE_INTEGER base_ul, current_ul; + memcpy(&base_ul, &base_ft, sizeof(FILETIME)); + memcpy(¤t_ul, &ft, sizeof(FILETIME)); + + // Divide by big number to convert to seconds, then subtract out the 1970 + // base date value. + const ULONGLONG RATIO = 10000000; + *ut = static_cast((current_ul.QuadPart - base_ul.QuadPart) / RATIO); +} + +void UnixTimeToFileTime(const time_t& ut, FILETIME* ft) { + ASSERT(NULL != ft); + + // FILETIME has an earlier date base than time_t (1/1/1970), so add in + // the difference. + SYSTEMTIME base_st; + memset(&base_st, 0, sizeof(base_st)); + base_st.wDay = 1; + base_st.wMonth = 1; + base_st.wYear = 1970; + + FILETIME base_ft; + SystemTimeToFileTime(&base_st, &base_ft); + + ULARGE_INTEGER base_ul; + memcpy(&base_ul, &base_ft, sizeof(FILETIME)); + + // Multiply by big number to convert to 100ns units, then add in the 1970 + // base date value. + const ULONGLONG RATIO = 10000000; + ULARGE_INTEGER current_ul; + current_ul.QuadPart = base_ul.QuadPart + static_cast(ut) * RATIO; + memcpy(ft, ¤t_ul, sizeof(FILETIME)); +} + +bool Utf8ToWindowsFilename(const std::string& utf8, std::wstring* filename) { + // TODO: Integrate into fileutils.h + // TODO: Handle wide and non-wide cases via TCHAR? + // TODO: Skip \\?\ processing if the length is not > MAX_PATH? + // TODO: Write unittests + + // Convert to Utf16 + int wlen = ::MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), + static_cast(utf8.length() + 1), NULL, + 0); + if (0 == wlen) { + return false; + } + wchar_t* wfilename = STACK_ARRAY(wchar_t, wlen); + if (0 == ::MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), + static_cast(utf8.length() + 1), + wfilename, wlen)) { + return false; + } + // Replace forward slashes with backslashes + std::replace(wfilename, wfilename + wlen, L'/', L'\\'); + // Convert to complete filename + DWORD full_len = ::GetFullPathName(wfilename, 0, NULL, NULL); + if (0 == full_len) { + return false; + } + wchar_t* filepart = NULL; + wchar_t* full_filename = STACK_ARRAY(wchar_t, full_len + 6); + wchar_t* start = full_filename + 6; + if (0 == ::GetFullPathName(wfilename, full_len, start, &filepart)) { + return false; + } + // Add long-path prefix + const wchar_t kLongPathPrefix[] = L"\\\\?\\UNC"; + if ((start[0] != L'\\') || (start[1] != L'\\')) { + // Non-unc path: + // Becomes: \\?\ + start -= 4; + ASSERT(start >= full_filename); + memcpy(start, kLongPathPrefix, 4 * sizeof(wchar_t)); + } else if (start[2] != L'?') { + // Unc path: \\\ + // Becomes: \\?\UNC\\ + start -= 6; + ASSERT(start >= full_filename); + memcpy(start, kLongPathPrefix, 7 * sizeof(wchar_t)); + } else { + // Already in long-path form. + } + filename->assign(start); + return true; +} + +bool GetOsVersion(int* major, int* minor, int* build) { + OSVERSIONINFO info = {0}; + info.dwOSVersionInfoSize = sizeof(info); + if (GetVersionEx(&info)) { + if (major) *major = info.dwMajorVersion; + if (minor) *minor = info.dwMinorVersion; + if (build) *build = info.dwBuildNumber; + return true; + } + return false; +} + +bool GetCurrentProcessIntegrityLevel(int* level) { + bool ret = false; + HANDLE process = ::GetCurrentProcess(), token; + if (OpenProcessToken(process, TOKEN_QUERY | TOKEN_QUERY_SOURCE, &token)) { + DWORD size; + if (!GetTokenInformation(token, TokenIntegrityLevel, NULL, 0, &size) && + GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + + char* buf = STACK_ARRAY(char, size); + TOKEN_MANDATORY_LABEL* til = + reinterpret_cast(buf); + if (GetTokenInformation(token, TokenIntegrityLevel, til, size, &size)) { + + DWORD count = *GetSidSubAuthorityCount(til->Label.Sid); + *level = *GetSidSubAuthority(til->Label.Sid, count - 1); + ret = true; + } + } + CloseHandle(token); + } + return ret; +} +} // namespace talk_base diff --git a/talk/base/win32.h b/talk/base/win32.h new file mode 100644 index 000000000..617d639ad --- /dev/null +++ b/talk/base/win32.h @@ -0,0 +1,146 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_WIN32_H_ +#define TALK_BASE_WIN32_H_ + +#ifdef WIN32 + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +// Make sure we don't get min/max macros +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include + +#ifndef SECURITY_MANDATORY_LABEL_AUTHORITY +// Add defines that we use if we are compiling against older sdks +#define SECURITY_MANDATORY_MEDIUM_RID (0x00002000L) +#define TokenIntegrityLevel static_cast(0x19) +typedef struct _TOKEN_MANDATORY_LABEL { + SID_AND_ATTRIBUTES Label; +} TOKEN_MANDATORY_LABEL, *PTOKEN_MANDATORY_LABEL; +#endif // SECURITY_MANDATORY_LABEL_AUTHORITY + +#undef SetPort + +#include + +#include "talk/base/stringutils.h" +#include "talk/base/basictypes.h" + +namespace talk_base { + +const char* win32_inet_ntop(int af, const void *src, char* dst, socklen_t size); +int win32_inet_pton(int af, const char* src, void *dst); + +/////////////////////////////////////////////////////////////////////////////// + +inline std::wstring ToUtf16(const char* utf8, size_t len) { + int len16 = ::MultiByteToWideChar(CP_UTF8, 0, utf8, static_cast(len), + NULL, 0); + wchar_t* ws = STACK_ARRAY(wchar_t, len16); + ::MultiByteToWideChar(CP_UTF8, 0, utf8, static_cast(len), ws, len16); + return std::wstring(ws, len16); +} + +inline std::wstring ToUtf16(const std::string& str) { + return ToUtf16(str.data(), str.length()); +} + +inline std::string ToUtf8(const wchar_t* wide, size_t len) { + int len8 = ::WideCharToMultiByte(CP_UTF8, 0, wide, static_cast(len), + NULL, 0, NULL, NULL); + char* ns = STACK_ARRAY(char, len8); + ::WideCharToMultiByte(CP_UTF8, 0, wide, static_cast(len), ns, len8, + NULL, NULL); + return std::string(ns, len8); +} + +inline std::string ToUtf8(const wchar_t* wide) { + return ToUtf8(wide, wcslen(wide)); +} + +inline std::string ToUtf8(const std::wstring& wstr) { + return ToUtf8(wstr.data(), wstr.length()); +} + +// Convert FILETIME to time_t +void FileTimeToUnixTime(const FILETIME& ft, time_t* ut); + +// Convert time_t to FILETIME +void UnixTimeToFileTime(const time_t& ut, FILETIME * ft); + +// Convert a Utf8 path representation to a non-length-limited Unicode pathname. +bool Utf8ToWindowsFilename(const std::string& utf8, std::wstring* filename); + +// Convert a FILETIME to a UInt64 +inline uint64 ToUInt64(const FILETIME& ft) { + ULARGE_INTEGER r = {ft.dwLowDateTime, ft.dwHighDateTime}; + return r.QuadPart; +} + +enum WindowsMajorVersions { + kWindows2000 = 5, + kWindowsVista = 6, +}; +bool GetOsVersion(int* major, int* minor, int* build); + +inline bool IsWindowsVistaOrLater() { + int major; + return (GetOsVersion(&major, NULL, NULL) && major >= kWindowsVista); +} + +inline bool IsWindowsXpOrLater() { + int major, minor; + return (GetOsVersion(&major, &minor, NULL) && + (major >= kWindowsVista || + (major == kWindows2000 && minor >= 1))); +} + +// Determine the current integrity level of the process. +bool GetCurrentProcessIntegrityLevel(int* level); + +inline bool IsCurrentProcessLowIntegrity() { + int level; + return (GetCurrentProcessIntegrityLevel(&level) && + level < SECURITY_MANDATORY_MEDIUM_RID); +} + +bool AdjustCurrentProcessPrivilege(const TCHAR* privilege, bool to_enable); + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // WIN32 +#endif // TALK_BASE_WIN32_H_ diff --git a/talk/base/win32_unittest.cc b/talk/base/win32_unittest.cc new file mode 100644 index 000000000..502de5b5c --- /dev/null +++ b/talk/base/win32_unittest.cc @@ -0,0 +1,79 @@ +/* + * libjingle + * Copyright 2010, 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 + +#include "talk/base/gunit.h" +#include "talk/base/nethelpers.h" +#include "talk/base/win32.h" +#include "talk/base/winping.h" + +#ifndef WIN32 +#error Only for Windows +#endif + +namespace talk_base { + +class Win32Test : public testing::Test { + public: + Win32Test() { + } +}; + +TEST_F(Win32Test, FileTimeToUInt64Test) { + FILETIME ft; + ft.dwHighDateTime = 0xBAADF00D; + ft.dwLowDateTime = 0xFEED3456; + + uint64 expected = 0xBAADF00DFEED3456; + EXPECT_EQ(expected, ToUInt64(ft)); +} + +TEST_F(Win32Test, WinPingTest) { + WinPing ping; + ASSERT_TRUE(ping.IsValid()); + + // Test valid ping cases. + WinPing::PingResult result = ping.Ping(IPAddress(INADDR_LOOPBACK), 20, 50, 1, + false); + ASSERT_EQ(WinPing::PING_SUCCESS, result); + if (HasIPv6Enabled()) { + WinPing::PingResult v6result = ping.Ping(IPAddress(in6addr_loopback), 20, + 50, 1, false); + ASSERT_EQ(WinPing::PING_SUCCESS, v6result); + } + + // Test invalid parameter cases. + ASSERT_EQ(WinPing::PING_INVALID_PARAMS, ping.Ping( + IPAddress(INADDR_LOOPBACK), 0, 50, 1, false)); + ASSERT_EQ(WinPing::PING_INVALID_PARAMS, ping.Ping( + IPAddress(INADDR_LOOPBACK), 20, 0, 1, false)); + ASSERT_EQ(WinPing::PING_INVALID_PARAMS, ping.Ping( + IPAddress(INADDR_LOOPBACK), 20, 50, 0, false)); +} + +} // namespace talk_base diff --git a/talk/base/win32filesystem.cc b/talk/base/win32filesystem.cc new file mode 100644 index 000000000..42c038855 --- /dev/null +++ b/talk/base/win32filesystem.cc @@ -0,0 +1,477 @@ +/* + * 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/base/win32filesystem.h" + +#include "talk/base/win32.h" +#include +#include +#include + +#include "talk/base/fileutils.h" +#include "talk/base/pathutils.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stream.h" +#include "talk/base/stringutils.h" + +// In several places in this file, we test the integrity level of the process +// before calling GetLongPathName. We do this because calling GetLongPathName +// when running under protected mode IE (a low integrity process) can result in +// a virtualized path being returned, which is wrong if you only plan to read. +// TODO: Waiting to hear back from IE team on whether this is the +// best approach; IEIsProtectedModeProcess is another possible solution. + +namespace talk_base { + +bool Win32Filesystem::CreateFolder(const Pathname &pathname) { + if (pathname.pathname().empty() || !pathname.filename().empty()) + return false; + + std::wstring path16; + if (!Utf8ToWindowsFilename(pathname.pathname(), &path16)) + return false; + + DWORD res = ::GetFileAttributes(path16.c_str()); + if (res != INVALID_FILE_ATTRIBUTES) { + // Something exists at this location, check if it is a directory + return ((res & FILE_ATTRIBUTE_DIRECTORY) != 0); + } else if ((GetLastError() != ERROR_FILE_NOT_FOUND) + && (GetLastError() != ERROR_PATH_NOT_FOUND)) { + // Unexpected error + return false; + } + + // Directory doesn't exist, look up one directory level + if (!pathname.parent_folder().empty()) { + Pathname parent(pathname); + parent.SetFolder(pathname.parent_folder()); + if (!CreateFolder(parent)) { + return false; + } + } + + return (::CreateDirectory(path16.c_str(), NULL) != 0); +} + +FileStream *Win32Filesystem::OpenFile(const Pathname &filename, + const std::string &mode) { + FileStream *fs = new FileStream(); + if (fs && !fs->Open(filename.pathname().c_str(), mode.c_str(), NULL)) { + delete fs; + fs = NULL; + } + return fs; +} + +bool Win32Filesystem::CreatePrivateFile(const Pathname &filename) { + // To make the file private to the current user, we first must construct a + // SECURITY_DESCRIPTOR specifying an ACL. This code is mostly based upon + // http://msdn.microsoft.com/en-us/library/ms707085%28VS.85%29.aspx + + // Get the current process token. + HANDLE process_token = INVALID_HANDLE_VALUE; + if (!::OpenProcessToken(::GetCurrentProcess(), + TOKEN_QUERY, + &process_token)) { + LOG_ERR(LS_ERROR) << "OpenProcessToken() failed"; + return false; + } + + // Get the size of its TOKEN_USER structure. Return value is not checked + // because we expect it to fail. + DWORD token_user_size = 0; + (void)::GetTokenInformation(process_token, + TokenUser, + NULL, + 0, + &token_user_size); + + // Get the TOKEN_USER structure. + scoped_array token_user_bytes(new char[token_user_size]); + PTOKEN_USER token_user = reinterpret_cast( + token_user_bytes.get()); + memset(token_user, 0, token_user_size); + BOOL success = ::GetTokenInformation(process_token, + TokenUser, + token_user, + token_user_size, + &token_user_size); + // We're now done with this. + ::CloseHandle(process_token); + if (!success) { + LOG_ERR(LS_ERROR) << "GetTokenInformation() failed"; + return false; + } + + if (!IsValidSid(token_user->User.Sid)) { + LOG_ERR(LS_ERROR) << "Current process has invalid user SID"; + return false; + } + + // Compute size needed for an ACL that allows access to just this user. + int acl_size = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) + + GetLengthSid(token_user->User.Sid); + + // Allocate it. + scoped_array acl_bytes(new char[acl_size]); + PACL acl = reinterpret_cast(acl_bytes.get()); + memset(acl, 0, acl_size); + if (!::InitializeAcl(acl, acl_size, ACL_REVISION)) { + LOG_ERR(LS_ERROR) << "InitializeAcl() failed"; + return false; + } + + // Allow access to only the current user. + if (!::AddAccessAllowedAce(acl, + ACL_REVISION, + GENERIC_READ | GENERIC_WRITE | STANDARD_RIGHTS_ALL, + token_user->User.Sid)) { + LOG_ERR(LS_ERROR) << "AddAccessAllowedAce() failed"; + return false; + } + + // Now make the security descriptor. + SECURITY_DESCRIPTOR security_descriptor; + if (!::InitializeSecurityDescriptor(&security_descriptor, + SECURITY_DESCRIPTOR_REVISION)) { + LOG_ERR(LS_ERROR) << "InitializeSecurityDescriptor() failed"; + return false; + } + + // Put the ACL in it. + if (!::SetSecurityDescriptorDacl(&security_descriptor, + TRUE, + acl, + FALSE)) { + LOG_ERR(LS_ERROR) << "SetSecurityDescriptorDacl() failed"; + return false; + } + + // Finally create the file. + SECURITY_ATTRIBUTES security_attributes; + security_attributes.nLength = sizeof(security_attributes); + security_attributes.lpSecurityDescriptor = &security_descriptor; + security_attributes.bInheritHandle = FALSE; + HANDLE handle = ::CreateFile( + ToUtf16(filename.pathname()).c_str(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, + &security_attributes, + CREATE_NEW, + 0, + NULL); + if (INVALID_HANDLE_VALUE == handle) { + LOG_ERR(LS_ERROR) << "CreateFile() failed"; + return false; + } + if (!::CloseHandle(handle)) { + LOG_ERR(LS_ERROR) << "CloseFile() failed"; + // Continue. + } + return true; +} + +bool Win32Filesystem::DeleteFile(const Pathname &filename) { + LOG(LS_INFO) << "Deleting file " << filename.pathname(); + if (!IsFile(filename)) { + ASSERT(IsFile(filename)); + return false; + } + return ::DeleteFile(ToUtf16(filename.pathname()).c_str()) != 0; +} + +bool Win32Filesystem::DeleteEmptyFolder(const Pathname &folder) { + LOG(LS_INFO) << "Deleting folder " << folder.pathname(); + + std::string no_slash(folder.pathname(), 0, folder.pathname().length()-1); + return ::RemoveDirectory(ToUtf16(no_slash).c_str()) != 0; +} + +bool Win32Filesystem::GetTemporaryFolder(Pathname &pathname, bool create, + const std::string *append) { + wchar_t buffer[MAX_PATH + 1]; + if (!::GetTempPath(ARRAY_SIZE(buffer), buffer)) + return false; + if (!IsCurrentProcessLowIntegrity() && + !::GetLongPathName(buffer, buffer, ARRAY_SIZE(buffer))) + return false; + size_t len = strlen(buffer); + if ((len > 0) && (buffer[len-1] != '\\')) { + len += strcpyn(buffer + len, ARRAY_SIZE(buffer) - len, L"\\"); + } + if (len >= ARRAY_SIZE(buffer) - 1) + return false; + pathname.clear(); + pathname.SetFolder(ToUtf8(buffer)); + if (append != NULL) { + ASSERT(!append->empty()); + pathname.AppendFolder(*append); + } + return !create || CreateFolder(pathname); +} + +std::string Win32Filesystem::TempFilename(const Pathname &dir, + const std::string &prefix) { + wchar_t filename[MAX_PATH]; + if (::GetTempFileName(ToUtf16(dir.pathname()).c_str(), + ToUtf16(prefix).c_str(), 0, filename) != 0) + return ToUtf8(filename); + ASSERT(false); + return ""; +} + +bool Win32Filesystem::MoveFile(const Pathname &old_path, + const Pathname &new_path) { + if (!IsFile(old_path)) { + ASSERT(IsFile(old_path)); + return false; + } + LOG(LS_INFO) << "Moving " << old_path.pathname() + << " to " << new_path.pathname(); + return ::MoveFile(ToUtf16(old_path.pathname()).c_str(), + ToUtf16(new_path.pathname()).c_str()) != 0; +} + +bool Win32Filesystem::MoveFolder(const Pathname &old_path, + const Pathname &new_path) { + if (!IsFolder(old_path)) { + ASSERT(IsFolder(old_path)); + return false; + } + LOG(LS_INFO) << "Moving " << old_path.pathname() + << " to " << new_path.pathname(); + if (::MoveFile(ToUtf16(old_path.pathname()).c_str(), + ToUtf16(new_path.pathname()).c_str()) == 0) { + if (::GetLastError() != ERROR_NOT_SAME_DEVICE) { + LOG_GLE(LS_ERROR) << "Failed to move file"; + return false; + } + if (!CopyFolder(old_path, new_path)) + return false; + if (!DeleteFolderAndContents(old_path)) + return false; + } + return true; +} + +bool Win32Filesystem::IsFolder(const Pathname &path) { + WIN32_FILE_ATTRIBUTE_DATA data = {0}; + if (0 == ::GetFileAttributesEx(ToUtf16(path.pathname()).c_str(), + GetFileExInfoStandard, &data)) + return false; + return (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == + FILE_ATTRIBUTE_DIRECTORY; +} + +bool Win32Filesystem::IsFile(const Pathname &path) { + WIN32_FILE_ATTRIBUTE_DATA data = {0}; + if (0 == ::GetFileAttributesEx(ToUtf16(path.pathname()).c_str(), + GetFileExInfoStandard, &data)) + return false; + return (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0; +} + +bool Win32Filesystem::IsAbsent(const Pathname& path) { + WIN32_FILE_ATTRIBUTE_DATA data = {0}; + if (0 != ::GetFileAttributesEx(ToUtf16(path.pathname()).c_str(), + GetFileExInfoStandard, &data)) + return false; + DWORD err = ::GetLastError(); + return (ERROR_FILE_NOT_FOUND == err || ERROR_PATH_NOT_FOUND == err); +} + +bool Win32Filesystem::CopyFile(const Pathname &old_path, + const Pathname &new_path) { + return ::CopyFile(ToUtf16(old_path.pathname()).c_str(), + ToUtf16(new_path.pathname()).c_str(), TRUE) != 0; +} + +bool Win32Filesystem::IsTemporaryPath(const Pathname& pathname) { + TCHAR buffer[MAX_PATH + 1]; + if (!::GetTempPath(ARRAY_SIZE(buffer), buffer)) + return false; + if (!IsCurrentProcessLowIntegrity() && + !::GetLongPathName(buffer, buffer, ARRAY_SIZE(buffer))) + return false; + return (::strnicmp(ToUtf16(pathname.pathname()).c_str(), + buffer, strlen(buffer)) == 0); +} + +bool Win32Filesystem::GetFileSize(const Pathname &pathname, size_t *size) { + WIN32_FILE_ATTRIBUTE_DATA data = {0}; + if (::GetFileAttributesEx(ToUtf16(pathname.pathname()).c_str(), + GetFileExInfoStandard, &data) == 0) + return false; + *size = data.nFileSizeLow; + return true; +} + +bool Win32Filesystem::GetFileTime(const Pathname& path, FileTimeType which, + time_t* time) { + WIN32_FILE_ATTRIBUTE_DATA data = {0}; + if (::GetFileAttributesEx(ToUtf16(path.pathname()).c_str(), + GetFileExInfoStandard, &data) == 0) + return false; + switch (which) { + case FTT_CREATED: + FileTimeToUnixTime(data.ftCreationTime, time); + break; + case FTT_MODIFIED: + FileTimeToUnixTime(data.ftLastWriteTime, time); + break; + case FTT_ACCESSED: + FileTimeToUnixTime(data.ftLastAccessTime, time); + break; + default: + return false; + } + return true; +} + +bool Win32Filesystem::GetAppPathname(Pathname* path) { + TCHAR buffer[MAX_PATH + 1]; + if (0 == ::GetModuleFileName(NULL, buffer, ARRAY_SIZE(buffer))) + return false; + path->SetPathname(ToUtf8(buffer)); + return true; +} + +bool Win32Filesystem::GetAppDataFolder(Pathname* path, bool per_user) { + ASSERT(!organization_name_.empty()); + ASSERT(!application_name_.empty()); + TCHAR buffer[MAX_PATH + 1]; + int csidl = per_user ? CSIDL_LOCAL_APPDATA : CSIDL_COMMON_APPDATA; + if (!::SHGetSpecialFolderPath(NULL, buffer, csidl, TRUE)) + return false; + if (!IsCurrentProcessLowIntegrity() && + !::GetLongPathName(buffer, buffer, ARRAY_SIZE(buffer))) + return false; + size_t len = strcatn(buffer, ARRAY_SIZE(buffer), __T("\\")); + len += strcpyn(buffer + len, ARRAY_SIZE(buffer) - len, + ToUtf16(organization_name_).c_str()); + if ((len > 0) && (buffer[len-1] != __T('\\'))) { + len += strcpyn(buffer + len, ARRAY_SIZE(buffer) - len, __T("\\")); + } + len += strcpyn(buffer + len, ARRAY_SIZE(buffer) - len, + ToUtf16(application_name_).c_str()); + if ((len > 0) && (buffer[len-1] != __T('\\'))) { + len += strcpyn(buffer + len, ARRAY_SIZE(buffer) - len, __T("\\")); + } + if (len >= ARRAY_SIZE(buffer) - 1) + return false; + path->clear(); + path->SetFolder(ToUtf8(buffer)); + return CreateFolder(*path); +} + +bool Win32Filesystem::GetAppTempFolder(Pathname* path) { + if (!GetAppPathname(path)) + return false; + std::string filename(path->filename()); + return GetTemporaryFolder(*path, true, &filename); +} + +bool Win32Filesystem::GetDiskFreeSpace(const Pathname& path, int64 *freebytes) { + if (!freebytes) { + return false; + } + char drive[4]; + std::wstring drive16; + const wchar_t* target_drive = NULL; + if (path.GetDrive(drive, sizeof(drive))) { + drive16 = ToUtf16(drive); + target_drive = drive16.c_str(); + } else if (path.folder().substr(0, 2) == "\\\\") { + // UNC path, fail. + // TODO: Handle UNC paths. + return false; + } else { + // The path is probably relative. GetDriveType and GetDiskFreeSpaceEx + // use the current drive if NULL is passed as the drive name. + // TODO: Add method to Pathname to determine if the path is relative. + // TODO: Add method to Pathname to convert a path to absolute. + } + UINT driveType = ::GetDriveType(target_drive); + if ( (driveType & DRIVE_REMOTE) || (driveType & DRIVE_UNKNOWN) ) { + LOG(LS_VERBOSE) << " remove or unknown drive " << drive; + return false; + } + + int64 totalNumberOfBytes; // receives the number of bytes on disk + int64 totalNumberOfFreeBytes; // receives the free bytes on disk + // make sure things won't change in 64 bit machine + // TODO replace with compile time assert + ASSERT(sizeof(ULARGE_INTEGER) == sizeof(uint64)); //NOLINT + if (::GetDiskFreeSpaceEx(target_drive, + (PULARGE_INTEGER)freebytes, + (PULARGE_INTEGER)&totalNumberOfBytes, + (PULARGE_INTEGER)&totalNumberOfFreeBytes)) { + return true; + } else { + LOG(LS_VERBOSE) << " GetDiskFreeSpaceEx returns error "; + return false; + } +} + +Pathname Win32Filesystem::GetCurrentDirectory() { + Pathname cwd; + int path_len = 0; + scoped_array path; + do { + int needed = ::GetCurrentDirectory(path_len, path.get()); + if (needed == 0) { + // Error. + LOG_GLE(LS_ERROR) << "::GetCurrentDirectory() failed"; + return cwd; // returns empty pathname + } + if (needed <= path_len) { + // It wrote successfully. + break; + } + // Else need to re-alloc for "needed". + path.reset(new wchar_t[needed]); + path_len = needed; + } while (true); + cwd.SetFolder(ToUtf8(path.get())); + return cwd; +} + +// TODO: Consider overriding DeleteFolderAndContents for speed and potentially +// better OS integration (recycle bin?) +/* + std::wstring temp_path16 = ToUtf16(temp_path.pathname()); + temp_path16.append(1, '*'); + temp_path16.append(1, '\0'); + + SHFILEOPSTRUCT file_op = { 0 }; + file_op.wFunc = FO_DELETE; + file_op.pFrom = temp_path16.c_str(); + file_op.fFlags = FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT; + return (0 == SHFileOperation(&file_op)); +*/ + +} // namespace talk_base diff --git a/talk/base/win32filesystem.h b/talk/base/win32filesystem.h new file mode 100644 index 000000000..c17bdd9bc --- /dev/null +++ b/talk/base/win32filesystem.h @@ -0,0 +1,118 @@ +/* + * 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. + */ + +#ifndef _TALK_BASE_WIN32FILESYSTEM_H__ +#define _TALK_BASE_WIN32FILESYSTEM_H__ + +#include "fileutils.h" + +namespace talk_base { + +class Win32Filesystem : public FilesystemInterface { + public: + // Opens a file. Returns an open StreamInterface if function succeeds. Otherwise, + // returns NULL. + virtual FileStream *OpenFile(const Pathname &filename, + const std::string &mode); + + // Atomically creates an empty file accessible only to the current user if one + // does not already exist at the given path, otherwise fails. + virtual bool CreatePrivateFile(const Pathname &filename); + + // This will attempt to delete the path located at filename. + // If the path points to a folder, it will fail with VERIFY + virtual bool DeleteFile(const Pathname &filename); + + // This will attempt to delete an empty folder. If the path does not point to + // a folder, it fails with VERIFY. If the folder is not empty, it fails normally + virtual bool DeleteEmptyFolder(const Pathname &folder); + + // Creates a directory. This will call itself recursively to create /foo/bar even if + // /foo does not exist. + // Returns TRUE if function succeeds + virtual bool CreateFolder(const Pathname &pathname); + + // This moves a file from old_path to new_path. If the new path is on a + // different volume than the old, it will attempt to copy and then delete + // the folder + // Returns true if the file is successfully moved + virtual bool MoveFile(const Pathname &old_path, const Pathname &new_path); + + // Moves a folder from old_path to new_path. If the new path is on a different + // volume from the old, it will attempt to Copy and then Delete the folder + // Returns true if the folder is successfully moved + virtual bool MoveFolder(const Pathname &old_path, const Pathname &new_path); + + // This copies a file from old_path to _new_path + // Returns true if function succeeds + virtual bool CopyFile(const Pathname &old_path, const Pathname &new_path); + + // Returns true if a pathname is a directory + virtual bool IsFolder(const Pathname& pathname); + + // Returns true if a file exists at path + virtual bool IsFile(const Pathname &path); + + // Returns true if pathname refers to no filesystem object, every parent + // directory either exists, or is also absent. + virtual bool IsAbsent(const Pathname& pathname); + + // Returns true if pathname represents a temporary location on the system. + virtual bool IsTemporaryPath(const Pathname& pathname); + + // All of the following functions set pathname and return true if successful. + // Returned paths always include a trailing backslash. + // If create is true, the path will be recursively created. + // If append is non-NULL, it will be appended (and possibly created). + + virtual std::string TempFilename(const Pathname &dir, const std::string &prefix); + + virtual bool GetFileSize(const Pathname& path, size_t* size); + virtual bool GetFileTime(const Pathname& path, FileTimeType which, + time_t* time); + + // A folder appropriate for storing temporary files (Contents are + // automatically deleted when the program exists) + virtual bool GetTemporaryFolder(Pathname &path, bool create, + const std::string *append); + + // Returns the path to the running application. + virtual bool GetAppPathname(Pathname* path); + + virtual bool GetAppDataFolder(Pathname* path, bool per_user); + + // Get a temporary folder that is unique to the current user and application. + virtual bool GetAppTempFolder(Pathname* path); + + virtual bool GetDiskFreeSpace(const Pathname& path, int64 *freebytes); + + virtual Pathname GetCurrentDirectory(); +}; + +} // namespace talk_base + +#endif // _WIN32FILESYSTEM_H__ diff --git a/talk/base/win32regkey.cc b/talk/base/win32regkey.cc new file mode 100644 index 000000000..cb98cafad --- /dev/null +++ b/talk/base/win32regkey.cc @@ -0,0 +1,1119 @@ +/* + * libjingle + * Copyright 2003-2008, 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. + */ + +// Registry configuration wrapers class implementation +// +// Change made by S. Ganesh - ganesh@google.com: +// Use SHQueryValueEx instead of RegQueryValueEx throughout. +// A call to the SHLWAPI function is essentially a call to the standard +// function but with post-processing: +// * to fix REG_SZ or REG_EXPAND_SZ data that is not properly null-terminated; +// * to expand REG_EXPAND_SZ data. + +#include "talk/base/win32regkey.h" + +#include + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" + +namespace talk_base { + +RegKey::RegKey() { + h_key_ = NULL; +} + +RegKey::~RegKey() { + Close(); +} + +HRESULT RegKey::Create(HKEY parent_key, const wchar_t* key_name) { + return Create(parent_key, + key_name, + REG_NONE, + REG_OPTION_NON_VOLATILE, + KEY_ALL_ACCESS, + NULL, + NULL); +} + +HRESULT RegKey::Open(HKEY parent_key, const wchar_t* key_name) { + return Open(parent_key, key_name, KEY_ALL_ACCESS); +} + +bool RegKey::HasValue(const TCHAR* value_name) const { + return (ERROR_SUCCESS == ::RegQueryValueEx(h_key_, value_name, NULL, + NULL, NULL, NULL)); +} + +HRESULT RegKey::SetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD value) { + ASSERT(full_key_name != NULL); + + return SetValueStaticHelper(full_key_name, value_name, REG_DWORD, &value); +} + +HRESULT RegKey::SetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD64 value) { + ASSERT(full_key_name != NULL); + + return SetValueStaticHelper(full_key_name, value_name, REG_QWORD, &value); +} + +HRESULT RegKey::SetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + float value) { + ASSERT(full_key_name != NULL); + + return SetValueStaticHelper(full_key_name, value_name, + REG_BINARY, &value, sizeof(value)); +} + +HRESULT RegKey::SetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + double value) { + ASSERT(full_key_name != NULL); + + return SetValueStaticHelper(full_key_name, value_name, + REG_BINARY, &value, sizeof(value)); +} + +HRESULT RegKey::SetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + const TCHAR* value) { + ASSERT(full_key_name != NULL); + ASSERT(value != NULL); + + return SetValueStaticHelper(full_key_name, value_name, + REG_SZ, const_cast(value)); +} + +HRESULT RegKey::SetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + const uint8* value, + DWORD byte_count) { + ASSERT(full_key_name != NULL); + + return SetValueStaticHelper(full_key_name, value_name, REG_BINARY, + const_cast(value), byte_count); +} + +HRESULT RegKey::SetValueMultiSZ(const wchar_t* full_key_name, + const wchar_t* value_name, + const uint8* value, + DWORD byte_count) { + ASSERT(full_key_name != NULL); + + return SetValueStaticHelper(full_key_name, value_name, REG_MULTI_SZ, + const_cast(value), byte_count); +} + +HRESULT RegKey::GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD* value) { + ASSERT(full_key_name != NULL); + ASSERT(value != NULL); + + return GetValueStaticHelper(full_key_name, value_name, REG_DWORD, value); +} + +HRESULT RegKey::GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD64* value) { + ASSERT(full_key_name != NULL); + ASSERT(value != NULL); + + return GetValueStaticHelper(full_key_name, value_name, REG_QWORD, value); +} + +HRESULT RegKey::GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + float* value) { + ASSERT(value != NULL); + ASSERT(full_key_name != NULL); + + DWORD byte_count = 0; + scoped_array buffer; + HRESULT hr = GetValueStaticHelper(full_key_name, value_name, + REG_BINARY, buffer.accept(), &byte_count); + if (SUCCEEDED(hr)) { + ASSERT(byte_count == sizeof(*value)); + if (byte_count == sizeof(*value)) { + *value = *reinterpret_cast(buffer.get()); + } + } + return hr; +} + +HRESULT RegKey::GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + double* value) { + ASSERT(value != NULL); + ASSERT(full_key_name != NULL); + + DWORD byte_count = 0; + scoped_array buffer; + HRESULT hr = GetValueStaticHelper(full_key_name, value_name, + REG_BINARY, buffer.accept(), &byte_count); + if (SUCCEEDED(hr)) { + ASSERT(byte_count == sizeof(*value)); + if (byte_count == sizeof(*value)) { + *value = *reinterpret_cast(buffer.get()); + } + } + return hr; +} + +HRESULT RegKey::GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + wchar_t** value) { + ASSERT(full_key_name != NULL); + ASSERT(value != NULL); + + return GetValueStaticHelper(full_key_name, value_name, REG_SZ, value); +} + +HRESULT RegKey::GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + std::wstring* value) { + ASSERT(full_key_name != NULL); + ASSERT(value != NULL); + + scoped_array buffer; + HRESULT hr = RegKey::GetValue(full_key_name, value_name, buffer.accept()); + if (SUCCEEDED(hr)) { + value->assign(buffer.get()); + } + return hr; +} + +HRESULT RegKey::GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + std::vector* value) { + ASSERT(full_key_name != NULL); + ASSERT(value != NULL); + + return GetValueStaticHelper(full_key_name, value_name, REG_MULTI_SZ, value); +} + +HRESULT RegKey::GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + uint8** value, + DWORD* byte_count) { + ASSERT(full_key_name != NULL); + ASSERT(value != NULL); + ASSERT(byte_count != NULL); + + return GetValueStaticHelper(full_key_name, value_name, + REG_BINARY, value, byte_count); +} + +HRESULT RegKey::DeleteSubKey(const wchar_t* key_name) { + ASSERT(key_name != NULL); + ASSERT(h_key_ != NULL); + + LONG res = ::RegDeleteKey(h_key_, key_name); + HRESULT hr = HRESULT_FROM_WIN32(res); + if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || + hr == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)) { + hr = S_FALSE; + } + return hr; +} + +HRESULT RegKey::DeleteValue(const wchar_t* value_name) { + ASSERT(h_key_ != NULL); + + LONG res = ::RegDeleteValue(h_key_, value_name); + HRESULT hr = HRESULT_FROM_WIN32(res); + if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || + hr == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)) { + hr = S_FALSE; + } + return hr; +} + +HRESULT RegKey::Close() { + HRESULT hr = S_OK; + if (h_key_ != NULL) { + LONG res = ::RegCloseKey(h_key_); + hr = HRESULT_FROM_WIN32(res); + h_key_ = NULL; + } + return hr; +} + +HRESULT RegKey::Create(HKEY parent_key, + const wchar_t* key_name, + wchar_t* lpszClass, + DWORD options, + REGSAM sam_desired, + LPSECURITY_ATTRIBUTES lpSecAttr, + LPDWORD lpdwDisposition) { + ASSERT(key_name != NULL); + ASSERT(parent_key != NULL); + + DWORD dw = 0; + HKEY h_key = NULL; + LONG res = ::RegCreateKeyEx(parent_key, key_name, 0, lpszClass, options, + sam_desired, lpSecAttr, &h_key, &dw); + HRESULT hr = HRESULT_FROM_WIN32(res); + + if (lpdwDisposition) { + *lpdwDisposition = dw; + } + + // we have to close the currently opened key + // before replacing it with the new one + if (hr == S_OK) { + hr = Close(); + ASSERT(hr == S_OK); + h_key_ = h_key; + } + return hr; +} + +HRESULT RegKey::Open(HKEY parent_key, + const wchar_t* key_name, + REGSAM sam_desired) { + ASSERT(key_name != NULL); + ASSERT(parent_key != NULL); + + HKEY h_key = NULL; + LONG res = ::RegOpenKeyEx(parent_key, key_name, 0, sam_desired, &h_key); + HRESULT hr = HRESULT_FROM_WIN32(res); + + // we have to close the currently opened key + // before replacing it with the new one + if (hr == S_OK) { + // close the currently opened key if any + hr = Close(); + ASSERT(hr == S_OK); + h_key_ = h_key; + } + return hr; +} + +// save the key and all of its subkeys and values to a file +HRESULT RegKey::Save(const wchar_t* full_key_name, const wchar_t* file_name) { + ASSERT(full_key_name != NULL); + ASSERT(file_name != NULL); + + std::wstring key_name(full_key_name); + HKEY h_key = GetRootKeyInfo(&key_name); + if (!h_key) { + return E_FAIL; + } + + RegKey key; + HRESULT hr = key.Open(h_key, key_name.c_str(), KEY_READ); + if (FAILED(hr)) { + return hr; + } + + AdjustCurrentProcessPrivilege(SE_BACKUP_NAME, true); + LONG res = ::RegSaveKey(key.h_key_, file_name, NULL); + AdjustCurrentProcessPrivilege(SE_BACKUP_NAME, false); + + return HRESULT_FROM_WIN32(res); +} + +// restore the key and all of its subkeys and values which are saved into a file +HRESULT RegKey::Restore(const wchar_t* full_key_name, + const wchar_t* file_name) { + ASSERT(full_key_name != NULL); + ASSERT(file_name != NULL); + + std::wstring key_name(full_key_name); + HKEY h_key = GetRootKeyInfo(&key_name); + if (!h_key) { + return E_FAIL; + } + + RegKey key; + HRESULT hr = key.Open(h_key, key_name.c_str(), KEY_WRITE); + if (FAILED(hr)) { + return hr; + } + + AdjustCurrentProcessPrivilege(SE_RESTORE_NAME, true); + LONG res = ::RegRestoreKey(key.h_key_, file_name, REG_FORCE_RESTORE); + AdjustCurrentProcessPrivilege(SE_RESTORE_NAME, false); + + return HRESULT_FROM_WIN32(res); +} + +// check if the current key has the specified subkey +bool RegKey::HasSubkey(const wchar_t* key_name) const { + ASSERT(key_name != NULL); + + RegKey key; + HRESULT hr = key.Open(h_key_, key_name, KEY_READ); + key.Close(); + return hr == S_OK; +} + +// static flush key +HRESULT RegKey::FlushKey(const wchar_t* full_key_name) { + ASSERT(full_key_name != NULL); + + HRESULT hr = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND); + // get the root HKEY + std::wstring key_name(full_key_name); + HKEY h_key = GetRootKeyInfo(&key_name); + + if (h_key != NULL) { + LONG res = ::RegFlushKey(h_key); + hr = HRESULT_FROM_WIN32(res); + } + return hr; +} + +// static SET helper +HRESULT RegKey::SetValueStaticHelper(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD type, + LPVOID value, + DWORD byte_count) { + ASSERT(full_key_name != NULL); + + HRESULT hr = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND); + // get the root HKEY + std::wstring key_name(full_key_name); + HKEY h_key = GetRootKeyInfo(&key_name); + + if (h_key != NULL) { + RegKey key; + hr = key.Create(h_key, key_name.c_str()); + if (hr == S_OK) { + switch (type) { + case REG_DWORD: + hr = key.SetValue(value_name, *(static_cast(value))); + break; + case REG_QWORD: + hr = key.SetValue(value_name, *(static_cast(value))); + break; + case REG_SZ: + hr = key.SetValue(value_name, static_cast(value)); + break; + case REG_BINARY: + hr = key.SetValue(value_name, static_cast(value), + byte_count); + break; + case REG_MULTI_SZ: + hr = key.SetValue(value_name, static_cast(value), + byte_count, type); + break; + default: + ASSERT(false); + hr = HRESULT_FROM_WIN32(ERROR_DATATYPE_MISMATCH); + break; + } + // close the key after writing + HRESULT temp_hr = key.Close(); + if (hr == S_OK) { + hr = temp_hr; + } + } + } + return hr; +} + +// static GET helper +HRESULT RegKey::GetValueStaticHelper(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD type, + LPVOID value, + DWORD* byte_count) { + ASSERT(full_key_name != NULL); + + HRESULT hr = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND); + // get the root HKEY + std::wstring key_name(full_key_name); + HKEY h_key = GetRootKeyInfo(&key_name); + + if (h_key != NULL) { + RegKey key; + hr = key.Open(h_key, key_name.c_str(), KEY_READ); + if (hr == S_OK) { + switch (type) { + case REG_DWORD: + hr = key.GetValue(value_name, reinterpret_cast(value)); + break; + case REG_QWORD: + hr = key.GetValue(value_name, reinterpret_cast(value)); + break; + case REG_SZ: + hr = key.GetValue(value_name, reinterpret_cast(value)); + break; + case REG_MULTI_SZ: + hr = key.GetValue(value_name, reinterpret_cast< + std::vector*>(value)); + break; + case REG_BINARY: + hr = key.GetValue(value_name, reinterpret_cast(value), + byte_count); + break; + default: + ASSERT(false); + hr = HRESULT_FROM_WIN32(ERROR_DATATYPE_MISMATCH); + break; + } + // close the key after writing + HRESULT temp_hr = key.Close(); + if (hr == S_OK) { + hr = temp_hr; + } + } + } + return hr; +} + +// GET helper +HRESULT RegKey::GetValueHelper(const wchar_t* value_name, + DWORD* type, + uint8** value, + DWORD* byte_count) const { + ASSERT(byte_count != NULL); + ASSERT(value != NULL); + ASSERT(type != NULL); + + // init return buffer + *value = NULL; + + // get the size of the return data buffer + LONG res = ::SHQueryValueEx(h_key_, value_name, NULL, type, NULL, byte_count); + HRESULT hr = HRESULT_FROM_WIN32(res); + + if (hr == S_OK) { + // if the value length is 0, nothing to do + if (*byte_count != 0) { + // allocate the buffer + *value = new byte[*byte_count]; + ASSERT(*value != NULL); + + // make the call again to get the data + res = ::SHQueryValueEx(h_key_, value_name, NULL, + type, *value, byte_count); + hr = HRESULT_FROM_WIN32(res); + ASSERT(hr == S_OK); + } + } + return hr; +} + +// Int32 Get +HRESULT RegKey::GetValue(const wchar_t* value_name, DWORD* value) const { + ASSERT(value != NULL); + + DWORD type = 0; + DWORD byte_count = sizeof(DWORD); + LONG res = ::SHQueryValueEx(h_key_, value_name, NULL, &type, + value, &byte_count); + HRESULT hr = HRESULT_FROM_WIN32(res); + ASSERT((hr != S_OK) || (type == REG_DWORD)); + ASSERT((hr != S_OK) || (byte_count == sizeof(DWORD))); + return hr; +} + +// Int64 Get +HRESULT RegKey::GetValue(const wchar_t* value_name, DWORD64* value) const { + ASSERT(value != NULL); + + DWORD type = 0; + DWORD byte_count = sizeof(DWORD64); + LONG res = ::SHQueryValueEx(h_key_, value_name, NULL, &type, + value, &byte_count); + HRESULT hr = HRESULT_FROM_WIN32(res); + ASSERT((hr != S_OK) || (type == REG_QWORD)); + ASSERT((hr != S_OK) || (byte_count == sizeof(DWORD64))); + return hr; +} + +// String Get +HRESULT RegKey::GetValue(const wchar_t* value_name, wchar_t** value) const { + ASSERT(value != NULL); + + DWORD byte_count = 0; + DWORD type = 0; + + // first get the size of the string buffer + LONG res = ::SHQueryValueEx(h_key_, value_name, NULL, + &type, NULL, &byte_count); + HRESULT hr = HRESULT_FROM_WIN32(res); + + if (hr == S_OK) { + // allocate room for the string and a terminating \0 + *value = new wchar_t[(byte_count / sizeof(wchar_t)) + 1]; + + if ((*value) != NULL) { + if (byte_count != 0) { + // make the call again + res = ::SHQueryValueEx(h_key_, value_name, NULL, &type, + *value, &byte_count); + hr = HRESULT_FROM_WIN32(res); + } else { + (*value)[0] = L'\0'; + } + + ASSERT((hr != S_OK) || (type == REG_SZ) || + (type == REG_MULTI_SZ) || (type == REG_EXPAND_SZ)); + } else { + hr = E_OUTOFMEMORY; + } + } + + return hr; +} + +// get a string value +HRESULT RegKey::GetValue(const wchar_t* value_name, std::wstring* value) const { + ASSERT(value != NULL); + + DWORD byte_count = 0; + DWORD type = 0; + + // first get the size of the string buffer + LONG res = ::SHQueryValueEx(h_key_, value_name, NULL, + &type, NULL, &byte_count); + HRESULT hr = HRESULT_FROM_WIN32(res); + + if (hr == S_OK) { + if (byte_count != 0) { + // Allocate some memory and make the call again + value->resize(byte_count / sizeof(wchar_t) + 1); + res = ::SHQueryValueEx(h_key_, value_name, NULL, &type, + &value->at(0), &byte_count); + hr = HRESULT_FROM_WIN32(res); + value->resize(wcslen(value->data())); + } else { + value->clear(); + } + + ASSERT((hr != S_OK) || (type == REG_SZ) || + (type == REG_MULTI_SZ) || (type == REG_EXPAND_SZ)); + } + + return hr; +} + +// convert REG_MULTI_SZ bytes to string array +HRESULT RegKey::MultiSZBytesToStringArray(const uint8* buffer, + DWORD byte_count, + std::vector* value) { + ASSERT(buffer != NULL); + ASSERT(value != NULL); + + const wchar_t* data = reinterpret_cast(buffer); + DWORD data_len = byte_count / sizeof(wchar_t); + value->clear(); + if (data_len > 1) { + // must be terminated by two null characters + if (data[data_len - 1] != 0 || data[data_len - 2] != 0) { + return E_INVALIDARG; + } + + // put null-terminated strings into arrays + while (*data) { + std::wstring str(data); + value->push_back(str); + data += str.length() + 1; + } + } + return S_OK; +} + +// get a std::vector value from REG_MULTI_SZ type +HRESULT RegKey::GetValue(const wchar_t* value_name, + std::vector* value) const { + ASSERT(value != NULL); + + DWORD byte_count = 0; + DWORD type = 0; + uint8* buffer = 0; + + // first get the size of the buffer + HRESULT hr = GetValueHelper(value_name, &type, &buffer, &byte_count); + ASSERT((hr != S_OK) || (type == REG_MULTI_SZ)); + + if (SUCCEEDED(hr)) { + hr = MultiSZBytesToStringArray(buffer, byte_count, value); + } + + return hr; +} + +// Binary data Get +HRESULT RegKey::GetValue(const wchar_t* value_name, + uint8** value, + DWORD* byte_count) const { + ASSERT(byte_count != NULL); + ASSERT(value != NULL); + + DWORD type = 0; + HRESULT hr = GetValueHelper(value_name, &type, value, byte_count); + ASSERT((hr != S_OK) || (type == REG_MULTI_SZ) || (type == REG_BINARY)); + return hr; +} + +// Raw data get +HRESULT RegKey::GetValue(const wchar_t* value_name, + uint8** value, + DWORD* byte_count, + DWORD*type) const { + ASSERT(type != NULL); + ASSERT(byte_count != NULL); + ASSERT(value != NULL); + + return GetValueHelper(value_name, type, value, byte_count); +} + +// Int32 set +HRESULT RegKey::SetValue(const wchar_t* value_name, DWORD value) const { + ASSERT(h_key_ != NULL); + + LONG res = ::RegSetValueEx(h_key_, value_name, NULL, REG_DWORD, + reinterpret_cast(&value), + sizeof(DWORD)); + return HRESULT_FROM_WIN32(res); +} + +// Int64 set +HRESULT RegKey::SetValue(const wchar_t* value_name, DWORD64 value) const { + ASSERT(h_key_ != NULL); + + LONG res = ::RegSetValueEx(h_key_, value_name, NULL, REG_QWORD, + reinterpret_cast(&value), + sizeof(DWORD64)); + return HRESULT_FROM_WIN32(res); +} + +// String set +HRESULT RegKey::SetValue(const wchar_t* value_name, + const wchar_t* value) const { + ASSERT(value != NULL); + ASSERT(h_key_ != NULL); + + LONG res = ::RegSetValueEx(h_key_, value_name, NULL, REG_SZ, + reinterpret_cast(value), + (lstrlen(value) + 1) * sizeof(wchar_t)); + return HRESULT_FROM_WIN32(res); +} + +// Binary data set +HRESULT RegKey::SetValue(const wchar_t* value_name, + const uint8* value, + DWORD byte_count) const { + ASSERT(h_key_ != NULL); + + // special case - if 'value' is NULL make sure byte_count is zero + if (value == NULL) { + byte_count = 0; + } + + LONG res = ::RegSetValueEx(h_key_, value_name, NULL, + REG_BINARY, value, byte_count); + return HRESULT_FROM_WIN32(res); +} + +// Raw data set +HRESULT RegKey::SetValue(const wchar_t* value_name, + const uint8* value, + DWORD byte_count, + DWORD type) const { + ASSERT(value != NULL); + ASSERT(h_key_ != NULL); + + LONG res = ::RegSetValueEx(h_key_, value_name, NULL, type, value, byte_count); + return HRESULT_FROM_WIN32(res); +} + +bool RegKey::HasKey(const wchar_t* full_key_name) { + ASSERT(full_key_name != NULL); + + // get the root HKEY + std::wstring key_name(full_key_name); + HKEY h_key = GetRootKeyInfo(&key_name); + + if (h_key != NULL) { + RegKey key; + HRESULT hr = key.Open(h_key, key_name.c_str(), KEY_READ); + key.Close(); + return S_OK == hr; + } + return false; +} + +// static version of HasValue +bool RegKey::HasValue(const wchar_t* full_key_name, const wchar_t* value_name) { + ASSERT(full_key_name != NULL); + + bool has_value = false; + // get the root HKEY + std::wstring key_name(full_key_name); + HKEY h_key = GetRootKeyInfo(&key_name); + + if (h_key != NULL) { + RegKey key; + if (key.Open(h_key, key_name.c_str(), KEY_READ) == S_OK) { + has_value = key.HasValue(value_name); + key.Close(); + } + } + return has_value; +} + +HRESULT RegKey::GetValueType(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD* value_type) { + ASSERT(full_key_name != NULL); + ASSERT(value_type != NULL); + + *value_type = REG_NONE; + + std::wstring key_name(full_key_name); + HKEY h_key = GetRootKeyInfo(&key_name); + + RegKey key; + HRESULT hr = key.Open(h_key, key_name.c_str(), KEY_READ); + if (SUCCEEDED(hr)) { + LONG res = ::SHQueryValueEx(key.h_key_, value_name, NULL, value_type, + NULL, NULL); + if (res != ERROR_SUCCESS) { + hr = HRESULT_FROM_WIN32(res); + } + } + + return hr; +} + +HRESULT RegKey::DeleteKey(const wchar_t* full_key_name) { + ASSERT(full_key_name != NULL); + + return DeleteKey(full_key_name, true); +} + +HRESULT RegKey::DeleteKey(const wchar_t* full_key_name, bool recursively) { + ASSERT(full_key_name != NULL); + + // need to open the parent key first + // get the root HKEY + std::wstring key_name(full_key_name); + HKEY h_key = GetRootKeyInfo(&key_name); + + // get the parent key + std::wstring parent_key(GetParentKeyInfo(&key_name)); + + RegKey key; + HRESULT hr = key.Open(h_key, parent_key.c_str()); + + if (hr == S_OK) { + hr = recursively ? key.RecurseDeleteSubKey(key_name.c_str()) + : key.DeleteSubKey(key_name.c_str()); + } else if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || + hr == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)) { + hr = S_FALSE; + } + + key.Close(); + return hr; +} + +HRESULT RegKey::DeleteValue(const wchar_t* full_key_name, + const wchar_t* value_name) { + ASSERT(full_key_name != NULL); + + HRESULT hr = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND); + // get the root HKEY + std::wstring key_name(full_key_name); + HKEY h_key = GetRootKeyInfo(&key_name); + + if (h_key != NULL) { + RegKey key; + hr = key.Open(h_key, key_name.c_str()); + if (hr == S_OK) { + hr = key.DeleteValue(value_name); + key.Close(); + } + } + return hr; +} + +HRESULT RegKey::RecurseDeleteSubKey(const wchar_t* key_name) { + ASSERT(key_name != NULL); + + RegKey key; + HRESULT hr = key.Open(h_key_, key_name); + + if (hr == S_OK) { + // enumerate all subkeys of this key and recursivelly delete them + FILETIME time = {0}; + wchar_t key_name_buf[kMaxKeyNameChars] = {0}; + DWORD key_name_buf_size = kMaxKeyNameChars; + while (hr == S_OK && + ::RegEnumKeyEx(key.h_key_, 0, key_name_buf, &key_name_buf_size, + NULL, NULL, NULL, &time) == ERROR_SUCCESS) { + hr = key.RecurseDeleteSubKey(key_name_buf); + + // restore the buffer size + key_name_buf_size = kMaxKeyNameChars; + } + // close the top key + key.Close(); + } + + if (hr == S_OK) { + // the key has no more children keys + // delete the key and all of its values + hr = DeleteSubKey(key_name); + } + + return hr; +} + +HKEY RegKey::GetRootKeyInfo(std::wstring* full_key_name) { + ASSERT(full_key_name != NULL); + + HKEY h_key = NULL; + // get the root HKEY + size_t index = full_key_name->find(L'\\'); + std::wstring root_key; + + if (index == -1) { + root_key = *full_key_name; + *full_key_name = L""; + } else { + root_key = full_key_name->substr(0, index); + *full_key_name = full_key_name->substr(index + 1, + full_key_name->length() - index - 1); + } + + for (std::wstring::iterator iter = root_key.begin(); + iter != root_key.end(); ++iter) { + *iter = toupper(*iter); + } + + if (!root_key.compare(L"HKLM") || + !root_key.compare(L"HKEY_LOCAL_MACHINE")) { + h_key = HKEY_LOCAL_MACHINE; + } else if (!root_key.compare(L"HKCU") || + !root_key.compare(L"HKEY_CURRENT_USER")) { + h_key = HKEY_CURRENT_USER; + } else if (!root_key.compare(L"HKU") || + !root_key.compare(L"HKEY_USERS")) { + h_key = HKEY_USERS; + } else if (!root_key.compare(L"HKCR") || + !root_key.compare(L"HKEY_CLASSES_ROOT")) { + h_key = HKEY_CLASSES_ROOT; + } + + return h_key; +} + + +// Returns true if this key name is 'safe' for deletion +// (doesn't specify a key root) +bool RegKey::SafeKeyNameForDeletion(const wchar_t* key_name) { + ASSERT(key_name != NULL); + std::wstring key(key_name); + + HKEY root_key = GetRootKeyInfo(&key); + + if (!root_key) { + key = key_name; + } + if (key.empty()) { + return false; + } + bool found_subkey = false, backslash_found = false; + for (size_t i = 0 ; i < key.length() ; ++i) { + if (key[i] == L'\\') { + backslash_found = true; + } else if (backslash_found) { + found_subkey = true; + break; + } + } + return (root_key == HKEY_USERS) ? found_subkey : true; +} + +std::wstring RegKey::GetParentKeyInfo(std::wstring* key_name) { + ASSERT(key_name != NULL); + + // get the parent key + size_t index = key_name->rfind(L'\\'); + std::wstring parent_key; + if (index == -1) { + parent_key = L""; + } else { + parent_key = key_name->substr(0, index); + *key_name = key_name->substr(index + 1, key_name->length() - index - 1); + } + + return parent_key; +} + +// get the number of values for this key +uint32 RegKey::GetValueCount() { + DWORD num_values = 0; + + LONG res = ::RegQueryInfoKey( + h_key_, // key handle + NULL, // buffer for class name + NULL, // size of class string + NULL, // reserved + NULL, // number of subkeys + NULL, // longest subkey size + NULL, // longest class string + &num_values, // number of values for this key + NULL, // longest value name + NULL, // longest value data + NULL, // security descriptor + NULL); // last write time + + ASSERT(res == ERROR_SUCCESS); + return num_values; +} + +// Enumerators for the value_names for this key + +// Called to get the value name for the given value name index +// Use GetValueCount() to get the total value_name count for this key +// Returns failure if no key at the specified index +HRESULT RegKey::GetValueNameAt(int index, std::wstring* value_name, + DWORD* type) { + ASSERT(value_name != NULL); + + LONG res = ERROR_SUCCESS; + wchar_t value_name_buf[kMaxValueNameChars] = {0}; + DWORD value_name_buf_size = kMaxValueNameChars; + res = ::RegEnumValue(h_key_, index, value_name_buf, &value_name_buf_size, + NULL, type, NULL, NULL); + + if (res == ERROR_SUCCESS) { + value_name->assign(value_name_buf); + } + + return HRESULT_FROM_WIN32(res); +} + +uint32 RegKey::GetSubkeyCount() { + // number of values for key + DWORD num_subkeys = 0; + + LONG res = ::RegQueryInfoKey( + h_key_, // key handle + NULL, // buffer for class name + NULL, // size of class string + NULL, // reserved + &num_subkeys, // number of subkeys + NULL, // longest subkey size + NULL, // longest class string + NULL, // number of values for this key + NULL, // longest value name + NULL, // longest value data + NULL, // security descriptor + NULL); // last write time + + ASSERT(res == ERROR_SUCCESS); + return num_subkeys; +} + +HRESULT RegKey::GetSubkeyNameAt(int index, std::wstring* key_name) { + ASSERT(key_name != NULL); + + LONG res = ERROR_SUCCESS; + wchar_t key_name_buf[kMaxKeyNameChars] = {0}; + DWORD key_name_buf_size = kMaxKeyNameChars; + + res = ::RegEnumKeyEx(h_key_, index, key_name_buf, &key_name_buf_size, + NULL, NULL, NULL, NULL); + + if (res == ERROR_SUCCESS) { + key_name->assign(key_name_buf); + } + + return HRESULT_FROM_WIN32(res); +} + +// Is the key empty: having no sub-keys and values +bool RegKey::IsKeyEmpty(const wchar_t* full_key_name) { + ASSERT(full_key_name != NULL); + + bool is_empty = true; + + // Get the root HKEY + std::wstring key_name(full_key_name); + HKEY h_key = GetRootKeyInfo(&key_name); + + // Open the key to check + if (h_key != NULL) { + RegKey key; + HRESULT hr = key.Open(h_key, key_name.c_str(), KEY_READ); + if (SUCCEEDED(hr)) { + is_empty = key.GetSubkeyCount() == 0 && key.GetValueCount() == 0; + key.Close(); + } + } + + return is_empty; +} + +bool AdjustCurrentProcessPrivilege(const TCHAR* privilege, bool to_enable) { + ASSERT(privilege != NULL); + + bool ret = false; + HANDLE token; + if (::OpenProcessToken(::GetCurrentProcess(), + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token)) { + LUID luid; + memset(&luid, 0, sizeof(luid)); + if (::LookupPrivilegeValue(NULL, privilege, &luid)) { + TOKEN_PRIVILEGES privs; + privs.PrivilegeCount = 1; + privs.Privileges[0].Luid = luid; + privs.Privileges[0].Attributes = to_enable ? SE_PRIVILEGE_ENABLED : 0; + if (::AdjustTokenPrivileges(token, FALSE, &privs, 0, NULL, 0)) { + ret = true; + } else { + LOG_GLE(LS_ERROR) << "AdjustTokenPrivileges failed"; + } + } else { + LOG_GLE(LS_ERROR) << "LookupPrivilegeValue failed"; + } + CloseHandle(token); + } else { + LOG_GLE(LS_ERROR) << "OpenProcessToken(GetCurrentProcess) failed"; + } + + return ret; +} + +} // namespace talk_base diff --git a/talk/base/win32regkey.h b/talk/base/win32regkey.h new file mode 100644 index 000000000..9f01ce144 --- /dev/null +++ b/talk/base/win32regkey.h @@ -0,0 +1,354 @@ +/* + * libjingle + * Copyright 2003-2007, 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. + */ + +// Registry configuration wrappers class +// +// Offers static functions for convenient +// fast access for individual values +// +// Also provides a wrapper class for efficient +// batch operations on values of a given registry key. +// + +#ifndef TALK_BASE_WIN32REGKEY_H_ +#define TALK_BASE_WIN32REGKEY_H_ + +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/win32.h" + +namespace talk_base { + +// maximum sizes registry key and value names +const int kMaxKeyNameChars = 255 + 1; +const int kMaxValueNameChars = 16383 + 1; + +class RegKey { + public: + // constructor + RegKey(); + + // destructor + ~RegKey(); + + // create a reg key + HRESULT Create(HKEY parent_key, const wchar_t* key_name); + + HRESULT Create(HKEY parent_key, + const wchar_t* key_name, + wchar_t* reg_class, + DWORD options, + REGSAM sam_desired, + LPSECURITY_ATTRIBUTES lp_sec_attr, + LPDWORD lp_disposition); + + // open an existing reg key + HRESULT Open(HKEY parent_key, const wchar_t* key_name); + + HRESULT Open(HKEY parent_key, const wchar_t* key_name, REGSAM sam_desired); + + // close this reg key + HRESULT Close(); + + // check if the key has a specified value + bool HasValue(const wchar_t* value_name) const; + + // get the number of values for this key + uint32 GetValueCount(); + + // Called to get the value name for the given value name index + // Use GetValueCount() to get the total value_name count for this key + // Returns failure if no key at the specified index + // If you modify the key while enumerating, the indexes will be out of order. + // Since the index order is not guaranteed, you need to reset your counting + // loop. + // 'type' refers to REG_DWORD, REG_QWORD, etc.. + // 'type' can be NULL if not interested in the value type + HRESULT GetValueNameAt(int index, std::wstring* value_name, DWORD* type); + + // check if the current key has the specified subkey + bool HasSubkey(const wchar_t* key_name) const; + + // get the number of subkeys for this key + uint32 GetSubkeyCount(); + + // Called to get the key name for the given key index + // Use GetSubkeyCount() to get the total count for this key + // Returns failure if no key at the specified index + // If you modify the key while enumerating, the indexes will be out of order. + // Since the index order is not guaranteed, you need to reset your counting + // loop. + HRESULT GetSubkeyNameAt(int index, std::wstring* key_name); + + // SETTERS + + // set an int32 value - use when reading multiple values from a key + HRESULT SetValue(const wchar_t* value_name, DWORD value) const; + + // set an int64 value + HRESULT SetValue(const wchar_t* value_name, DWORD64 value) const; + + // set a string value + HRESULT SetValue(const wchar_t* value_name, const wchar_t* value) const; + + // set binary data + HRESULT SetValue(const wchar_t* value_name, + const uint8* value, + DWORD byte_count) const; + + // set raw data, including type + HRESULT SetValue(const wchar_t* value_name, + const uint8* value, + DWORD byte_count, + DWORD type) const; + + // GETTERS + + // get an int32 value + HRESULT GetValue(const wchar_t* value_name, DWORD* value) const; + + // get an int64 value + HRESULT GetValue(const wchar_t* value_name, DWORD64* value) const; + + // get a string value - the caller must free the return buffer + HRESULT GetValue(const wchar_t* value_name, wchar_t** value) const; + + // get a string value + HRESULT GetValue(const wchar_t* value_name, std::wstring* value) const; + + // get a std::vector value from REG_MULTI_SZ type + HRESULT GetValue(const wchar_t* value_name, + std::vector* value) const; + + // get binary data - the caller must free the return buffer + HRESULT GetValue(const wchar_t* value_name, + uint8** value, + DWORD* byte_count) const; + + // get raw data, including type - the caller must free the return buffer + HRESULT GetValue(const wchar_t* value_name, + uint8** value, + DWORD* byte_count, + DWORD* type) const; + + // STATIC VERSIONS + + // flush + static HRESULT FlushKey(const wchar_t* full_key_name); + + // check if a key exists + static bool HasKey(const wchar_t* full_key_name); + + // check if the key has a specified value + static bool HasValue(const wchar_t* full_key_name, const wchar_t* value_name); + + // SETTERS + + // STATIC int32 set + static HRESULT SetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD value); + + // STATIC int64 set + static HRESULT SetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD64 value); + + // STATIC float set + static HRESULT SetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + float value); + + // STATIC double set + static HRESULT SetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + double value); + + // STATIC string set + static HRESULT SetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + const wchar_t* value); + + // STATIC binary data set + static HRESULT SetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + const uint8* value, + DWORD byte_count); + + // STATIC multi-string set + static HRESULT SetValueMultiSZ(const wchar_t* full_key_name, + const TCHAR* value_name, + const uint8* value, + DWORD byte_count); + + // GETTERS + + // STATIC int32 get + static HRESULT GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD* value); + + // STATIC int64 get + // + // Note: if you are using time64 you should + // likely use GetLimitedTimeValue (util.h) instead of this method. + static HRESULT GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD64* value); + + // STATIC float get + static HRESULT GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + float* value); + + // STATIC double get + static HRESULT GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + double* value); + + // STATIC string get + // Note: the caller must free the return buffer for wchar_t* version + static HRESULT GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + wchar_t** value); + static HRESULT GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + std::wstring* value); + + // STATIC REG_MULTI_SZ get + static HRESULT GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + std::vector* value); + + // STATIC get binary data - the caller must free the return buffer + static HRESULT GetValue(const wchar_t* full_key_name, + const wchar_t* value_name, + uint8** value, + DWORD* byte_count); + + // Get type of a registry value + static HRESULT GetValueType(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD* value_type); + + // delete a subkey of the current key (with no subkeys) + HRESULT DeleteSubKey(const wchar_t* key_name); + + // recursively delete a sub key of the current key (and all its subkeys) + HRESULT RecurseDeleteSubKey(const wchar_t* key_name); + + // STATIC version of delete key - handles nested keys also + // delete a key and all its sub-keys recursively + // Returns S_FALSE if key didn't exist, S_OK if deletion was successful, + // and failure otherwise. + static HRESULT DeleteKey(const wchar_t* full_key_name); + + // STATIC version of delete key + // delete a key recursively or non-recursively + // Returns S_FALSE if key didn't exist, S_OK if deletion was successful, + // and failure otherwise. + static HRESULT DeleteKey(const wchar_t* full_key_name, bool recursive); + + // delete the specified value + HRESULT DeleteValue(const wchar_t* value_name); + + // STATIC version of delete value + // Returns S_FALSE if key didn't exist, S_OK if deletion was successful, + // and failure otherwise. + static HRESULT DeleteValue(const wchar_t* full_key_name, + const wchar_t* value_name); + + // Peek inside (use a RegKey as a smart wrapper around a registry handle) + HKEY key() { return h_key_; } + + // helper function to get the HKEY and the root key from a string + // modifies the argument in place and returns the key name + // e.g. HKLM\\Software\\Google\... returns HKLM, "Software\\Google\..." + // Necessary for the static versions that use the full name of the reg key + static HKEY GetRootKeyInfo(std::wstring* full_key_name); + + // Returns true if this key name is 'safe' for deletion (doesn't specify a key + // root) + static bool SafeKeyNameForDeletion(const wchar_t* key_name); + + // save the key and all of its subkeys and values to a file + static HRESULT Save(const wchar_t* full_key_name, const wchar_t* file_name); + + // restore the key and all of its subkeys and values which are saved into a + // file + static HRESULT Restore(const wchar_t* full_key_name, + const wchar_t* file_name); + + // Is the key empty: having no sub-keys and values + static bool IsKeyEmpty(const wchar_t* full_key_name); + + private: + + // helper function to get any value from the registry + // used when the size of the data is unknown + HRESULT GetValueHelper(const wchar_t* value_name, + DWORD* type, uint8** value, + DWORD* byte_count) const; + + // helper function to get the parent key name and the subkey from a string + // modifies the argument in place and returns the key name + // Necessary for the static versions that use the full name of the reg key + static std::wstring GetParentKeyInfo(std::wstring* key_name); + + // common SET Helper for the static case + static HRESULT SetValueStaticHelper(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD type, + LPVOID value, + DWORD byte_count = 0); + + // common GET Helper for the static case + static HRESULT GetValueStaticHelper(const wchar_t* full_key_name, + const wchar_t* value_name, + DWORD type, + LPVOID value, + DWORD* byte_count = NULL); + + // convert REG_MULTI_SZ bytes to string array + static HRESULT MultiSZBytesToStringArray(const uint8* buffer, + DWORD byte_count, + std::vector* value); + + // the HKEY for the current key + HKEY h_key_; + + // for unittest + friend void RegKeyHelperFunctionsTest(); + + DISALLOW_EVIL_CONSTRUCTORS(RegKey); +}; + +} // namespace talk_base + +#endif // TALK_BASE_WIN32REGKEY_H_ diff --git a/talk/base/win32regkey_unittest.cc b/talk/base/win32regkey_unittest.cc new file mode 100644 index 000000000..1dd8fe434 --- /dev/null +++ b/talk/base/win32regkey_unittest.cc @@ -0,0 +1,607 @@ +/* + * libjingle + * Copyright 2003-2008, 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. + */ + +// Unittest for registry access API + +#include "talk/base/gunit.h" +#include "talk/base/common.h" +#include "talk/base/win32regkey.h" + +namespace talk_base { + +#ifndef EXPECT_SUCCEEDED +#define EXPECT_SUCCEEDED(x) EXPECT_TRUE(SUCCEEDED(x)) +#endif + +#ifndef EXPECT_FAILED +#define EXPECT_FAILED(x) EXPECT_TRUE(FAILED(x)) +#endif + +#define kBaseKey L"Software\\Google\\__TEST" +#define kSubkeyName L"subkey_test" + +const wchar_t kRkey1[] = kBaseKey; +const wchar_t kRkey1SubkeyName[] = kSubkeyName; +const wchar_t kRkey1Subkey[] = kBaseKey L"\\" kSubkeyName; +const wchar_t kFullRkey1[] = L"HKCU\\" kBaseKey; +const wchar_t kFullRkey1Subkey[] = L"HKCU\\" kBaseKey L"\\" kSubkeyName; + +const wchar_t kValNameInt[] = L"Int32 Value"; +const DWORD kIntVal = 20; +const DWORD kIntVal2 = 30; + +const wchar_t kValNameInt64[] = L"Int64 Value"; +const DWORD64 kIntVal64 = 119600064000000000uI64; + +const wchar_t kValNameFloat[] = L"Float Value"; +const float kFloatVal = 12.3456789f; + +const wchar_t kValNameDouble[] = L"Double Value"; +const double kDoubleVal = 98.7654321; + +const wchar_t kValNameStr[] = L"Str Value"; +const wchar_t kStrVal[] = L"Some string data 1"; +const wchar_t kStrVal2[] = L"Some string data 2"; + +const wchar_t kValNameBinary[] = L"Binary Value"; +const char kBinaryVal[] = "Some binary data abcdefghi 1"; +const char kBinaryVal2[] = "Some binary data abcdefghi 2"; + +const wchar_t kValNameMultiStr[] = L"MultiStr Value"; +const wchar_t kMultiSZ[] = L"abc\0def\0P12345\0"; +const wchar_t kEmptyMultiSZ[] = L""; +const wchar_t kInvalidMultiSZ[] = {L'6', L'7', L'8'}; + +// friend function of RegKey +void RegKeyHelperFunctionsTest() { + // Try out some dud values + std::wstring temp_key = L""; + EXPECT_TRUE(RegKey::GetRootKeyInfo(&temp_key) == NULL); + EXPECT_STREQ(temp_key.c_str(), L""); + + temp_key = L"a"; + EXPECT_TRUE(RegKey::GetRootKeyInfo(&temp_key) == NULL); + EXPECT_STREQ(temp_key.c_str(), L""); + + // The basics + temp_key = L"HKLM\\a"; + EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_LOCAL_MACHINE); + EXPECT_STREQ(temp_key.c_str(), L"a"); + + temp_key = L"HKEY_LOCAL_MACHINE\\a"; + EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_LOCAL_MACHINE); + EXPECT_STREQ(temp_key.c_str(), L"a"); + + temp_key = L"HKCU\\a"; + EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_CURRENT_USER); + EXPECT_STREQ(temp_key.c_str(), L"a"); + + temp_key = L"HKEY_CURRENT_USER\\a"; + EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_CURRENT_USER); + EXPECT_STREQ(temp_key.c_str(), L"a"); + + temp_key = L"HKU\\a"; + EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_USERS); + EXPECT_STREQ(temp_key.c_str(), L"a"); + + temp_key = L"HKEY_USERS\\a"; + EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_USERS); + EXPECT_STREQ(temp_key.c_str(), L"a"); + + temp_key = L"HKCR\\a"; + EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_CLASSES_ROOT); + EXPECT_STREQ(temp_key.c_str(), L"a"); + + temp_key = L"HKEY_CLASSES_ROOT\\a"; + EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_CLASSES_ROOT); + EXPECT_STREQ(temp_key.c_str(), L"a"); + + // Make sure it is case insensitive + temp_key = L"hkcr\\a"; + EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_CLASSES_ROOT); + EXPECT_STREQ(temp_key.c_str(), L"a"); + + temp_key = L"hkey_CLASSES_ROOT\\a"; + EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_CLASSES_ROOT); + EXPECT_STREQ(temp_key.c_str(), L"a"); + + // + // Test RegKey::GetParentKeyInfo + // + + // dud cases + temp_key = L""; + EXPECT_STREQ(RegKey::GetParentKeyInfo(&temp_key).c_str(), L""); + EXPECT_STREQ(temp_key.c_str(), L""); + + temp_key = L"a"; + EXPECT_STREQ(RegKey::GetParentKeyInfo(&temp_key).c_str(), L""); + EXPECT_STREQ(temp_key.c_str(), L"a"); + + temp_key = L"a\\b"; + EXPECT_STREQ(RegKey::GetParentKeyInfo(&temp_key).c_str(), L"a"); + EXPECT_STREQ(temp_key.c_str(), L"b"); + + temp_key = L"\\b"; + EXPECT_STREQ(RegKey::GetParentKeyInfo(&temp_key).c_str(), L""); + EXPECT_STREQ(temp_key.c_str(), L"b"); + + // Some regular cases + temp_key = L"HKEY_CLASSES_ROOT\\moon"; + EXPECT_STREQ(RegKey::GetParentKeyInfo(&temp_key).c_str(), + L"HKEY_CLASSES_ROOT"); + EXPECT_STREQ(temp_key.c_str(), L"moon"); + + temp_key = L"HKEY_CLASSES_ROOT\\moon\\doggy"; + EXPECT_STREQ(RegKey::GetParentKeyInfo(&temp_key).c_str(), + L"HKEY_CLASSES_ROOT\\moon"); + EXPECT_STREQ(temp_key.c_str(), L"doggy"); + + // + // Test MultiSZBytesToStringArray + // + + std::vector result; + EXPECT_SUCCEEDED(RegKey::MultiSZBytesToStringArray( + reinterpret_cast(kMultiSZ), sizeof(kMultiSZ), &result)); + EXPECT_EQ(result.size(), 3); + EXPECT_STREQ(result[0].c_str(), L"abc"); + EXPECT_STREQ(result[1].c_str(), L"def"); + EXPECT_STREQ(result[2].c_str(), L"P12345"); + + EXPECT_SUCCEEDED(RegKey::MultiSZBytesToStringArray( + reinterpret_cast(kEmptyMultiSZ), + sizeof(kEmptyMultiSZ), &result)); + EXPECT_EQ(result.size(), 0); + EXPECT_FALSE(SUCCEEDED(RegKey::MultiSZBytesToStringArray( + reinterpret_cast(kInvalidMultiSZ), + sizeof(kInvalidMultiSZ), &result))); +} + +TEST(RegKeyTest, RegKeyHelperFunctionsTest) { + RegKeyHelperFunctionsTest(); +} + +TEST(RegKeyTest, RegKeyNonStaticFunctionsTest) { + DWORD int_val = 0; + DWORD64 int64_val = 0; + wchar_t* str_val = NULL; + uint8* binary_val = NULL; + DWORD uint8_count = 0; + + // Just in case... + // make sure the no test key residue is left from previous aborted runs + RegKey::DeleteKey(kFullRkey1); + + // initial state + RegKey r_key; + EXPECT_TRUE(r_key.key() == NULL); + + // create a reg key + EXPECT_SUCCEEDED(r_key.Create(HKEY_CURRENT_USER, kRkey1)); + + // do the create twice - it should return the already created one + EXPECT_SUCCEEDED(r_key.Create(HKEY_CURRENT_USER, kRkey1)); + + // now do an open - should work just fine + EXPECT_SUCCEEDED(r_key.Open(HKEY_CURRENT_USER, kRkey1)); + + // get an in-existent value + EXPECT_EQ(r_key.GetValue(kValNameInt, &int_val), + HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); + + // set and get some values + + // set an INT 32 + EXPECT_SUCCEEDED(r_key.SetValue(kValNameInt, kIntVal)); + + // check that the value exists + EXPECT_TRUE(r_key.HasValue(kValNameInt)); + + // read it back + EXPECT_SUCCEEDED(r_key.GetValue(kValNameInt, &int_val)); + EXPECT_EQ(int_val, kIntVal); + + // set it again! + EXPECT_SUCCEEDED(r_key.SetValue(kValNameInt, kIntVal2)); + + // read it again + EXPECT_SUCCEEDED(r_key.GetValue(kValNameInt, &int_val)); + EXPECT_EQ(int_val, kIntVal2); + + // delete the value + EXPECT_SUCCEEDED(r_key.DeleteValue(kValNameInt)); + + // check that the value is gone + EXPECT_FALSE(r_key.HasValue(kValNameInt)); + + // set an INT 64 + EXPECT_SUCCEEDED(r_key.SetValue(kValNameInt64, kIntVal64)); + + // check that the value exists + EXPECT_TRUE(r_key.HasValue(kValNameInt64)); + + // read it back + EXPECT_SUCCEEDED(r_key.GetValue(kValNameInt64, &int64_val)); + EXPECT_EQ(int64_val, kIntVal64); + + // delete the value + EXPECT_SUCCEEDED(r_key.DeleteValue(kValNameInt64)); + + // check that the value is gone + EXPECT_FALSE(r_key.HasValue(kValNameInt64)); + + // set a string + EXPECT_SUCCEEDED(r_key.SetValue(kValNameStr, kStrVal)); + + // check that the value exists + EXPECT_TRUE(r_key.HasValue(kValNameStr)); + + // read it back + EXPECT_SUCCEEDED(r_key.GetValue(kValNameStr, &str_val)); + EXPECT_TRUE(lstrcmp(str_val, kStrVal) == 0); + delete[] str_val; + + // set it again + EXPECT_SUCCEEDED(r_key.SetValue(kValNameStr, kStrVal2)); + + // read it again + EXPECT_SUCCEEDED(r_key.GetValue(kValNameStr, &str_val)); + EXPECT_TRUE(lstrcmp(str_val, kStrVal2) == 0); + delete[] str_val; + + // delete the value + EXPECT_SUCCEEDED(r_key.DeleteValue(kValNameStr)); + + // check that the value is gone + EXPECT_FALSE(r_key.HasValue(kValNameInt)); + + // set a binary value + EXPECT_SUCCEEDED(r_key.SetValue(kValNameBinary, + reinterpret_cast(kBinaryVal), sizeof(kBinaryVal) - 1)); + + // check that the value exists + EXPECT_TRUE(r_key.HasValue(kValNameBinary)); + + // read it back + EXPECT_SUCCEEDED(r_key.GetValue(kValNameBinary, &binary_val, &uint8_count)); + EXPECT_TRUE(memcmp(binary_val, kBinaryVal, sizeof(kBinaryVal) - 1) == 0); + delete[] binary_val; + + // set it again + EXPECT_SUCCEEDED(r_key.SetValue(kValNameBinary, + reinterpret_cast(kBinaryVal2), sizeof(kBinaryVal) - 1)); + + // read it again + EXPECT_SUCCEEDED(r_key.GetValue(kValNameBinary, &binary_val, &uint8_count)); + EXPECT_TRUE(memcmp(binary_val, kBinaryVal2, sizeof(kBinaryVal2) - 1) == 0); + delete[] binary_val; + + // delete the value + EXPECT_SUCCEEDED(r_key.DeleteValue(kValNameBinary)); + + // check that the value is gone + EXPECT_FALSE(r_key.HasValue(kValNameBinary)); + + // set some values and check the total count + + // set an INT 32 + EXPECT_SUCCEEDED(r_key.SetValue(kValNameInt, kIntVal)); + + // set an INT 64 + EXPECT_SUCCEEDED(r_key.SetValue(kValNameInt64, kIntVal64)); + + // set a string + EXPECT_SUCCEEDED(r_key.SetValue(kValNameStr, kStrVal)); + + // set a binary value + EXPECT_SUCCEEDED(r_key.SetValue(kValNameBinary, + reinterpret_cast(kBinaryVal), sizeof(kBinaryVal) - 1)); + + // get the value count + uint32 value_count = r_key.GetValueCount(); + EXPECT_EQ(value_count, 4); + + // check the value names + std::wstring value_name; + DWORD type = 0; + + EXPECT_SUCCEEDED(r_key.GetValueNameAt(0, &value_name, &type)); + EXPECT_STREQ(value_name.c_str(), kValNameInt); + EXPECT_EQ(type, REG_DWORD); + + EXPECT_SUCCEEDED(r_key.GetValueNameAt(1, &value_name, &type)); + EXPECT_STREQ(value_name.c_str(), kValNameInt64); + EXPECT_EQ(type, REG_QWORD); + + EXPECT_SUCCEEDED(r_key.GetValueNameAt(2, &value_name, &type)); + EXPECT_STREQ(value_name.c_str(), kValNameStr); + EXPECT_EQ(type, REG_SZ); + + EXPECT_SUCCEEDED(r_key.GetValueNameAt(3, &value_name, &type)); + EXPECT_STREQ(value_name.c_str(), kValNameBinary); + EXPECT_EQ(type, REG_BINARY); + + // check that there are no more values + EXPECT_FAILED(r_key.GetValueNameAt(4, &value_name, &type)); + + uint32 subkey_count = r_key.GetSubkeyCount(); + EXPECT_EQ(subkey_count, 0); + + // now create a subkey and make sure we can get the name + RegKey temp_key; + EXPECT_SUCCEEDED(temp_key.Create(HKEY_CURRENT_USER, kRkey1Subkey)); + + // check the subkey exists + EXPECT_TRUE(r_key.HasSubkey(kRkey1SubkeyName)); + + // check the name + EXPECT_EQ(r_key.GetSubkeyCount(), 1); + + std::wstring subkey_name; + EXPECT_SUCCEEDED(r_key.GetSubkeyNameAt(0, &subkey_name)); + EXPECT_STREQ(subkey_name.c_str(), kRkey1SubkeyName); + + // delete the key + EXPECT_SUCCEEDED(r_key.DeleteSubKey(kRkey1)); + + // close this key + EXPECT_SUCCEEDED(r_key.Close()); + + // whack the whole key + EXPECT_SUCCEEDED(RegKey::DeleteKey(kFullRkey1)); +} + +TEST(RegKeyTest, RegKeyStaticFunctionsTest) { + DWORD int_val = 0; + DWORD64 int64_val = 0; + float float_val = 0; + double double_val = 0; + wchar_t* str_val = NULL; + std::wstring wstr_val; + uint8* binary_val = NULL; + DWORD uint8_count = 0; + + // Just in case... + // make sure the no test key residue is left from previous aborted runs + RegKey::DeleteKey(kFullRkey1); + + // get an in-existent value from an un-existent key + EXPECT_EQ(RegKey::GetValue(kFullRkey1, kValNameInt, &int_val), + HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); + + // set int32 + EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameInt, kIntVal)); + + // check that the value exists + EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameInt)); + + // get an in-existent value from an existent key + EXPECT_EQ(RegKey::GetValue(kFullRkey1, L"bogus", &int_val), + HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); + + // read it back + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameInt, &int_val)); + EXPECT_EQ(int_val, kIntVal); + + // delete the value + EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameInt)); + + // check that the value is gone + EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameInt)); + + // set int64 + EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameInt64, kIntVal64)); + + // check that the value exists + EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameInt64)); + + // read it back + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameInt64, &int64_val)); + EXPECT_EQ(int64_val, kIntVal64); + + // delete the value + EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameInt64)); + + // check that the value is gone + EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameInt64)); + + // set float + EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameFloat, kFloatVal)); + + // check that the value exists + EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameFloat)); + + // read it back + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameFloat, &float_val)); + EXPECT_EQ(float_val, kFloatVal); + + // delete the value + EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameFloat)); + + // check that the value is gone + EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameFloat)); + EXPECT_FAILED(RegKey::GetValue(kFullRkey1, kValNameFloat, &float_val)); + + // set double + EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameDouble, kDoubleVal)); + + // check that the value exists + EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameDouble)); + + // read it back + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameDouble, &double_val)); + EXPECT_EQ(double_val, kDoubleVal); + + // delete the value + EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameDouble)); + + // check that the value is gone + EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameDouble)); + EXPECT_FAILED(RegKey::GetValue(kFullRkey1, kValNameDouble, &double_val)); + + // set string + EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameStr, kStrVal)); + + // check that the value exists + EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameStr)); + + // read it back + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameStr, &str_val)); + EXPECT_TRUE(lstrcmp(str_val, kStrVal) == 0); + delete[] str_val; + + // read it back in std::wstring + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameStr, &wstr_val)); + EXPECT_STREQ(wstr_val.c_str(), kStrVal); + + // get an in-existent value from an existent key + EXPECT_EQ(RegKey::GetValue(kFullRkey1, L"bogus", &str_val), + HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); + + // delete the value + EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameStr)); + + // check that the value is gone + EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameStr)); + + // set binary + EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameBinary, + reinterpret_cast(kBinaryVal), sizeof(kBinaryVal)-1)); + + // check that the value exists + EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameBinary)); + + // read it back + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameBinary, + &binary_val, &uint8_count)); + EXPECT_TRUE(memcmp(binary_val, kBinaryVal, sizeof(kBinaryVal)-1) == 0); + delete[] binary_val; + + // delete the value + EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameBinary)); + + // check that the value is gone + EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameBinary)); + + // special case - set a binary value with length 0 + EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameBinary, + reinterpret_cast(kBinaryVal), 0)); + + // check that the value exists + EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameBinary)); + + // read it back + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameBinary, + &binary_val, &uint8_count)); + EXPECT_EQ(uint8_count, 0); + EXPECT_TRUE(binary_val == NULL); + delete[] binary_val; + + // delete the value + EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameBinary)); + + // check that the value is gone + EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameBinary)); + + // special case - set a NULL binary value + EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameBinary, NULL, 100)); + + // check that the value exists + EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameBinary)); + + // read it back + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameBinary, + &binary_val, &uint8_count)); + EXPECT_EQ(uint8_count, 0); + EXPECT_TRUE(binary_val == NULL); + delete[] binary_val; + + // delete the value + EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameBinary)); + + // check that the value is gone + EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameBinary)); + + // test read/write REG_MULTI_SZ value + std::vector result; + EXPECT_SUCCEEDED(RegKey::SetValueMultiSZ(kFullRkey1, kValNameMultiStr, + reinterpret_cast(kMultiSZ), sizeof(kMultiSZ))); + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameMultiStr, &result)); + EXPECT_EQ(result.size(), 3); + EXPECT_STREQ(result[0].c_str(), L"abc"); + EXPECT_STREQ(result[1].c_str(), L"def"); + EXPECT_STREQ(result[2].c_str(), L"P12345"); + EXPECT_SUCCEEDED(RegKey::SetValueMultiSZ(kFullRkey1, kValNameMultiStr, + reinterpret_cast(kEmptyMultiSZ), sizeof(kEmptyMultiSZ))); + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameMultiStr, &result)); + EXPECT_EQ(result.size(), 0); + // writing REG_MULTI_SZ value will automatically add ending null characters + EXPECT_SUCCEEDED(RegKey::SetValueMultiSZ(kFullRkey1, kValNameMultiStr, + reinterpret_cast(kInvalidMultiSZ), sizeof(kInvalidMultiSZ))); + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameMultiStr, &result)); + EXPECT_EQ(result.size(), 1); + EXPECT_STREQ(result[0].c_str(), L"678"); + + // Run the following test only in dev machine + // This is because the build machine might not have admin privilege +#ifdef IS_PRIVATE_BUILD + // get a temp file name + wchar_t temp_path[MAX_PATH] = {0}; + EXPECT_LT(::GetTempPath(ARRAY_SIZE(temp_path), temp_path), + static_cast(ARRAY_SIZE(temp_path))); + wchar_t temp_file[MAX_PATH] = {0}; + EXPECT_NE(::GetTempFileName(temp_path, L"rkut_", + ::GetTickCount(), temp_file), 0); + + // test save + EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1Subkey, kValNameInt, kIntVal)); + EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1Subkey, kValNameInt64, kIntVal64)); + EXPECT_SUCCEEDED(RegKey::Save(kFullRkey1Subkey, temp_file)); + EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1Subkey, kValNameInt)); + EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1Subkey, kValNameInt64)); + + // test restore + EXPECT_SUCCEEDED(RegKey::Restore(kFullRkey1Subkey, temp_file)); + int_val = 0; + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1Subkey, kValNameInt, &int_val)); + EXPECT_EQ(int_val, kIntVal); + int64_val = 0; + EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1Subkey, + kValNameInt64, + &int64_val)); + EXPECT_EQ(int64_val, kIntVal64); + + // delete the temp file + EXPECT_EQ(TRUE, ::DeleteFile(temp_file)); +#endif + + // whack the whole key + EXPECT_SUCCEEDED(RegKey::DeleteKey(kFullRkey1)); +} + +} // namespace talk_base diff --git a/talk/base/win32securityerrors.cc b/talk/base/win32securityerrors.cc new file mode 100644 index 000000000..50f4f66b8 --- /dev/null +++ b/talk/base/win32securityerrors.cc @@ -0,0 +1,66 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/win32.h" +#include "talk/base/logging.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// + +extern const ConstantLabel SECURITY_ERRORS[]; + +const ConstantLabel SECURITY_ERRORS[] = { + KLABEL(SEC_I_COMPLETE_AND_CONTINUE), + KLABEL(SEC_I_COMPLETE_NEEDED), + KLABEL(SEC_I_CONTEXT_EXPIRED), + KLABEL(SEC_I_CONTINUE_NEEDED), + KLABEL(SEC_I_INCOMPLETE_CREDENTIALS), + KLABEL(SEC_I_RENEGOTIATE), + KLABEL(SEC_E_CERT_EXPIRED), + KLABEL(SEC_E_INCOMPLETE_MESSAGE), + KLABEL(SEC_E_INSUFFICIENT_MEMORY), + KLABEL(SEC_E_INTERNAL_ERROR), + KLABEL(SEC_E_INVALID_HANDLE), + KLABEL(SEC_E_INVALID_TOKEN), + KLABEL(SEC_E_LOGON_DENIED), + KLABEL(SEC_E_NO_AUTHENTICATING_AUTHORITY), + KLABEL(SEC_E_NO_CREDENTIALS), + KLABEL(SEC_E_NOT_OWNER), + KLABEL(SEC_E_OK), + KLABEL(SEC_E_SECPKG_NOT_FOUND), + KLABEL(SEC_E_TARGET_UNKNOWN), + KLABEL(SEC_E_UNKNOWN_CREDENTIALS), + KLABEL(SEC_E_UNSUPPORTED_FUNCTION), + KLABEL(SEC_E_UNTRUSTED_ROOT), + KLABEL(SEC_E_WRONG_PRINCIPAL), + LASTLABEL +}; + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/win32socketinit.cc b/talk/base/win32socketinit.cc new file mode 100644 index 000000000..f6ac66630 --- /dev/null +++ b/talk/base/win32socketinit.cc @@ -0,0 +1,63 @@ +/* + * libjingle + * Copyright 2009, 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/base/win32socketinit.h" + +#include "talk/base/win32.h" + +namespace talk_base { + +// Please don't remove this function. +void EnsureWinsockInit() { + // The default implementation uses a global initializer, so WSAStartup + // happens at module load time. Thus we don't need to do anything here. + // The hook is provided so that a client that statically links with + // libjingle can override it, to provide its own initialization. +} + +#ifdef WIN32 +class WinsockInitializer { + public: + WinsockInitializer() { + WSADATA wsaData; + WORD wVersionRequested = MAKEWORD(1, 0); + err_ = WSAStartup(wVersionRequested, &wsaData); + } + ~WinsockInitializer() { + if (!err_) + WSACleanup(); + } + int error() { + return err_; + } + private: + int err_; +}; +WinsockInitializer g_winsockinit; +#endif + +} // namespace talk_base diff --git a/talk/base/win32socketinit.h b/talk/base/win32socketinit.h new file mode 100644 index 000000000..f56b7ff03 --- /dev/null +++ b/talk/base/win32socketinit.h @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2009, 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. + */ + +#ifndef TALK_BASE_WIN32SOCKETINIT_H_ +#define TALK_BASE_WIN32SOCKETINIT_H_ + +namespace talk_base { + +void EnsureWinsockInit(); + +} // namespace talk_base + +#endif // TALK_BASE_WIN32SOCKETINIT_H_ diff --git a/talk/base/win32socketserver.cc b/talk/base/win32socketserver.cc new file mode 100644 index 000000000..55128e7a2 --- /dev/null +++ b/talk/base/win32socketserver.cc @@ -0,0 +1,864 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/win32socketserver.h" +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/winping.h" +#include "talk/base/win32window.h" +#include // NOLINT + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// Win32Socket +/////////////////////////////////////////////////////////////////////////////// + +// TODO: Move this to a common place where PhysicalSocketServer can +// share it. +// Standard MTUs +static const uint16 PACKET_MAXIMUMS[] = { + 65535, // Theoretical maximum, Hyperchannel + 32000, // Nothing + 17914, // 16Mb IBM Token Ring + 8166, // IEEE 802.4 + // 4464 // IEEE 802.5 (4Mb max) + 4352, // FDDI + // 2048, // Wideband Network + 2002, // IEEE 802.5 (4Mb recommended) + // 1536, // Expermental Ethernet Networks + // 1500, // Ethernet, Point-to-Point (default) + 1492, // IEEE 802.3 + 1006, // SLIP, ARPANET + // 576, // X.25 Networks + // 544, // DEC IP Portal + // 512, // NETBIOS + 508, // IEEE 802/Source-Rt Bridge, ARCNET + 296, // Point-to-Point (low delay) + 68, // Official minimum + 0, // End of list marker +}; + +static const int IP_HEADER_SIZE = 20u; +static const int ICMP_HEADER_SIZE = 8u; +static const int ICMP_PING_TIMEOUT_MILLIS = 10000u; + +// TODO: Enable for production builds also? Use FormatMessage? +#ifdef _DEBUG +LPCSTR WSAErrorToString(int error, LPCSTR *description_result) { + LPCSTR string = "Unspecified"; + LPCSTR description = "Unspecified description"; + switch (error) { + case ERROR_SUCCESS: + string = "SUCCESS"; + description = "Operation succeeded"; + break; + case WSAEWOULDBLOCK: + string = "WSAEWOULDBLOCK"; + description = "Using a non-blocking socket, will notify later"; + break; + case WSAEACCES: + string = "WSAEACCES"; + description = "Access denied, or sharing violation"; + break; + case WSAEADDRNOTAVAIL: + string = "WSAEADDRNOTAVAIL"; + description = "Address is not valid in this context"; + break; + case WSAENETDOWN: + string = "WSAENETDOWN"; + description = "Network is down"; + break; + case WSAENETUNREACH: + string = "WSAENETUNREACH"; + description = "Network is up, but unreachable"; + break; + case WSAENETRESET: + string = "WSANETRESET"; + description = "Connection has been reset due to keep-alive activity"; + break; + case WSAECONNABORTED: + string = "WSAECONNABORTED"; + description = "Aborted by host"; + break; + case WSAECONNRESET: + string = "WSAECONNRESET"; + description = "Connection reset by host"; + break; + case WSAETIMEDOUT: + string = "WSAETIMEDOUT"; + description = "Timed out, host failed to respond"; + break; + case WSAECONNREFUSED: + string = "WSAECONNREFUSED"; + description = "Host actively refused connection"; + break; + case WSAEHOSTDOWN: + string = "WSAEHOSTDOWN"; + description = "Host is down"; + break; + case WSAEHOSTUNREACH: + string = "WSAEHOSTUNREACH"; + description = "Host is unreachable"; + break; + case WSAHOST_NOT_FOUND: + string = "WSAHOST_NOT_FOUND"; + description = "No such host is known"; + break; + } + if (description_result) { + *description_result = description; + } + return string; +} + +void ReportWSAError(LPCSTR context, int error, const SocketAddress& address) { + LPCSTR description_string; + LPCSTR error_string = WSAErrorToString(error, &description_string); + LOG(LS_INFO) << context << " = " << error + << " (" << error_string << ":" << description_string << ") [" + << address.ToString() << "]"; +} +#else +void ReportWSAError(LPCSTR context, int error, const SocketAddress& address) {} +#endif + +///////////////////////////////////////////////////////////////////////////// +// Win32Socket::EventSink +///////////////////////////////////////////////////////////////////////////// + +#define WM_SOCKETNOTIFY (WM_USER + 50) +#define WM_DNSNOTIFY (WM_USER + 51) + +struct Win32Socket::DnsLookup { + HANDLE handle; + uint16 port; + char buffer[MAXGETHOSTSTRUCT]; +}; + +class Win32Socket::EventSink : public Win32Window { + public: + explicit EventSink(Win32Socket * parent) : parent_(parent) { } + + void Dispose(); + + virtual bool OnMessage(UINT uMsg, WPARAM wParam, LPARAM lParam, + LRESULT& result); + virtual void OnNcDestroy(); + + private: + bool OnSocketNotify(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& result); + bool OnDnsNotify(WPARAM wParam, LPARAM lParam, LRESULT& result); + + Win32Socket * parent_; +}; + +void Win32Socket::EventSink::Dispose() { + parent_ = NULL; + if (::IsWindow(handle())) { + ::DestroyWindow(handle()); + } else { + delete this; + } +} + +bool Win32Socket::EventSink::OnMessage(UINT uMsg, WPARAM wParam, + LPARAM lParam, LRESULT& result) { + switch (uMsg) { + case WM_SOCKETNOTIFY: + case WM_TIMER: + return OnSocketNotify(uMsg, wParam, lParam, result); + case WM_DNSNOTIFY: + return OnDnsNotify(wParam, lParam, result); + } + return false; +} + +bool Win32Socket::EventSink::OnSocketNotify(UINT uMsg, WPARAM wParam, + LPARAM lParam, LRESULT& result) { + result = 0; + + int wsa_event = WSAGETSELECTEVENT(lParam); + int wsa_error = WSAGETSELECTERROR(lParam); + + // Treat connect timeouts as close notifications + if (uMsg == WM_TIMER) { + wsa_event = FD_CLOSE; + wsa_error = WSAETIMEDOUT; + } + + if (parent_) + parent_->OnSocketNotify(static_cast(wParam), wsa_event, wsa_error); + return true; +} + +bool Win32Socket::EventSink::OnDnsNotify(WPARAM wParam, LPARAM lParam, + LRESULT& result) { + result = 0; + + int error = WSAGETASYNCERROR(lParam); + if (parent_) + parent_->OnDnsNotify(reinterpret_cast(wParam), error); + return true; +} + +void Win32Socket::EventSink::OnNcDestroy() { + if (parent_) { + LOG(LS_ERROR) << "EventSink hwnd is being destroyed, but the event sink" + " hasn't yet been disposed."; + } else { + delete this; + } +} + +///////////////////////////////////////////////////////////////////////////// +// Win32Socket +///////////////////////////////////////////////////////////////////////////// + +Win32Socket::Win32Socket() + : socket_(INVALID_SOCKET), error_(0), state_(CS_CLOSED), connect_time_(0), + closing_(false), close_error_(0), sink_(NULL), dns_(NULL) { +} + +Win32Socket::~Win32Socket() { + Close(); +} + +bool Win32Socket::CreateT(int family, int type) { + Close(); + int proto = (SOCK_DGRAM == type) ? IPPROTO_UDP : IPPROTO_TCP; + socket_ = ::WSASocket(family, type, proto, NULL, NULL, 0); + if (socket_ == INVALID_SOCKET) { + UpdateLastError(); + return false; + } + if ((SOCK_DGRAM == type) && !SetAsync(FD_READ | FD_WRITE)) { + return false; + } + return true; +} + +int Win32Socket::Attach(SOCKET s) { + ASSERT(socket_ == INVALID_SOCKET); + if (socket_ != INVALID_SOCKET) + return SOCKET_ERROR; + + ASSERT(s != INVALID_SOCKET); + if (s == INVALID_SOCKET) + return SOCKET_ERROR; + + socket_ = s; + state_ = CS_CONNECTED; + + if (!SetAsync(FD_READ | FD_WRITE | FD_CLOSE)) + return SOCKET_ERROR; + + return 0; +} + +void Win32Socket::SetTimeout(int ms) { + if (sink_) + ::SetTimer(sink_->handle(), 1, ms, 0); +} + +SocketAddress Win32Socket::GetLocalAddress() const { + sockaddr_storage addr = {0}; + socklen_t addrlen = sizeof(addr); + int result = ::getsockname(socket_, reinterpret_cast(&addr), + &addrlen); + SocketAddress address; + if (result >= 0) { + SocketAddressFromSockAddrStorage(addr, &address); + } else { + LOG(LS_WARNING) << "GetLocalAddress: unable to get local addr, socket=" + << socket_; + } + return address; +} + +SocketAddress Win32Socket::GetRemoteAddress() const { + sockaddr_storage addr = {0}; + socklen_t addrlen = sizeof(addr); + int result = ::getpeername(socket_, reinterpret_cast(&addr), + &addrlen); + SocketAddress address; + if (result >= 0) { + SocketAddressFromSockAddrStorage(addr, &address); + } else { + LOG(LS_WARNING) << "GetRemoteAddress: unable to get remote addr, socket=" + << socket_; + } + return address; +} + +int Win32Socket::Bind(const SocketAddress& addr) { + ASSERT(socket_ != INVALID_SOCKET); + if (socket_ == INVALID_SOCKET) + return SOCKET_ERROR; + + sockaddr_storage saddr; + size_t len = addr.ToSockAddrStorage(&saddr); + int err = ::bind(socket_, + reinterpret_cast(&saddr), + static_cast(len)); + UpdateLastError(); + return err; +} + +int Win32Socket::Connect(const SocketAddress& addr) { + if (state_ != CS_CLOSED) { + SetError(EALREADY); + return SOCKET_ERROR; + } + + if (!addr.IsUnresolvedIP()) { + return DoConnect(addr); + } + + LOG_F(LS_INFO) << "async dns lookup (" << addr.hostname() << ")"; + DnsLookup * dns = new DnsLookup; + if (!sink_) { + // Explicitly create the sink ourselves here; we can't rely on SetAsync + // because we don't have a socket_ yet. + CreateSink(); + } + // TODO: Replace with IPv6 compatible lookup. + dns->handle = WSAAsyncGetHostByName(sink_->handle(), WM_DNSNOTIFY, + addr.hostname().c_str(), dns->buffer, + sizeof(dns->buffer)); + + if (!dns->handle) { + LOG_F(LS_ERROR) << "WSAAsyncGetHostByName error: " << WSAGetLastError(); + delete dns; + UpdateLastError(); + Close(); + return SOCKET_ERROR; + } + + dns->port = addr.port(); + dns_ = dns; + state_ = CS_CONNECTING; + return 0; +} + +int Win32Socket::DoConnect(const SocketAddress& addr) { + if ((socket_ == INVALID_SOCKET) && !CreateT(addr.family(), SOCK_STREAM)) { + return SOCKET_ERROR; + } + if (!SetAsync(FD_READ | FD_WRITE | FD_CONNECT | FD_CLOSE)) { + return SOCKET_ERROR; + } + + sockaddr_storage saddr = {0}; + size_t len = addr.ToSockAddrStorage(&saddr); + connect_time_ = Time(); + int result = connect(socket_, + reinterpret_cast(&saddr), + static_cast(len)); + if (result != SOCKET_ERROR) { + state_ = CS_CONNECTED; + } else { + int code = WSAGetLastError(); + if (code == WSAEWOULDBLOCK) { + state_ = CS_CONNECTING; + } else { + ReportWSAError("WSAAsync:connect", code, addr); + error_ = code; + Close(); + return SOCKET_ERROR; + } + } + addr_ = addr; + + return 0; +} + +int Win32Socket::GetError() const { + return error_; +} + +void Win32Socket::SetError(int error) { + error_ = error; +} + +Socket::ConnState Win32Socket::GetState() const { + return state_; +} + +int Win32Socket::GetOption(Option opt, int* value) { + int slevel; + int sopt; + if (TranslateOption(opt, &slevel, &sopt) == -1) + return -1; + + char* p = reinterpret_cast(value); + int optlen = sizeof(value); + return ::getsockopt(socket_, slevel, sopt, p, &optlen); +} + +int Win32Socket::SetOption(Option opt, int value) { + int slevel; + int sopt; + if (TranslateOption(opt, &slevel, &sopt) == -1) + return -1; + + const char* p = reinterpret_cast(&value); + return ::setsockopt(socket_, slevel, sopt, p, sizeof(value)); +} + +int Win32Socket::Send(const void* buffer, size_t length) { + int sent = ::send(socket_, + reinterpret_cast(buffer), + static_cast(length), + 0); + UpdateLastError(); + return sent; +} + +int Win32Socket::SendTo(const void* buffer, size_t length, + const SocketAddress& addr) { + sockaddr_storage saddr; + size_t addr_len = addr.ToSockAddrStorage(&saddr); + int sent = ::sendto(socket_, reinterpret_cast(buffer), + static_cast(length), 0, + reinterpret_cast(&saddr), + static_cast(addr_len)); + UpdateLastError(); + return sent; +} + +int Win32Socket::Recv(void* buffer, size_t length) { + int received = ::recv(socket_, static_cast(buffer), + static_cast(length), 0); + UpdateLastError(); + if (closing_ && received <= static_cast(length)) + PostClosed(); + return received; +} + +int Win32Socket::RecvFrom(void* buffer, size_t length, + SocketAddress* out_addr) { + sockaddr_storage saddr; + socklen_t addr_len = sizeof(saddr); + int received = ::recvfrom(socket_, static_cast(buffer), + static_cast(length), 0, + reinterpret_cast(&saddr), &addr_len); + UpdateLastError(); + if (received != SOCKET_ERROR) + SocketAddressFromSockAddrStorage(saddr, out_addr); + if (closing_ && received <= static_cast(length)) + PostClosed(); + return received; +} + +int Win32Socket::Listen(int backlog) { + int err = ::listen(socket_, backlog); + if (!SetAsync(FD_ACCEPT)) + return SOCKET_ERROR; + + UpdateLastError(); + if (err == 0) + state_ = CS_CONNECTING; + return err; +} + +Win32Socket* Win32Socket::Accept(SocketAddress* out_addr) { + sockaddr_storage saddr; + socklen_t addr_len = sizeof(saddr); + SOCKET s = ::accept(socket_, reinterpret_cast(&saddr), &addr_len); + UpdateLastError(); + if (s == INVALID_SOCKET) + return NULL; + if (out_addr) + SocketAddressFromSockAddrStorage(saddr, out_addr); + Win32Socket* socket = new Win32Socket; + if (0 == socket->Attach(s)) + return socket; + delete socket; + return NULL; +} + +int Win32Socket::Close() { + int err = 0; + if (socket_ != INVALID_SOCKET) { + err = ::closesocket(socket_); + socket_ = INVALID_SOCKET; + closing_ = false; + close_error_ = 0; + UpdateLastError(); + } + if (dns_) { + WSACancelAsyncRequest(dns_->handle); + delete dns_; + dns_ = NULL; + } + if (sink_) { + sink_->Dispose(); + sink_ = NULL; + } + addr_.Clear(); + state_ = CS_CLOSED; + return err; +} + +int Win32Socket::EstimateMTU(uint16* mtu) { + SocketAddress addr = GetRemoteAddress(); + if (addr.IsAny()) { + error_ = ENOTCONN; + return -1; + } + + WinPing ping; + if (!ping.IsValid()) { + error_ = EINVAL; // can't think of a better error ID + return -1; + } + + for (int level = 0; PACKET_MAXIMUMS[level + 1] > 0; ++level) { + int32 size = PACKET_MAXIMUMS[level] - IP_HEADER_SIZE - ICMP_HEADER_SIZE; + WinPing::PingResult result = ping.Ping(addr.ipaddr(), size, + ICMP_PING_TIMEOUT_MILLIS, 1, false); + if (result == WinPing::PING_FAIL) { + error_ = EINVAL; // can't think of a better error ID + return -1; + } + if (result != WinPing::PING_TOO_LARGE) { + *mtu = PACKET_MAXIMUMS[level]; + return 0; + } + } + + ASSERT(false); + return 0; +} + +void Win32Socket::CreateSink() { + ASSERT(NULL == sink_); + + // Create window + sink_ = new EventSink(this); + sink_->Create(NULL, L"EventSink", 0, 0, 0, 0, 10, 10); +} + +bool Win32Socket::SetAsync(int events) { + if (NULL == sink_) { + CreateSink(); + ASSERT(NULL != sink_); + } + + // start the async select + if (WSAAsyncSelect(socket_, sink_->handle(), WM_SOCKETNOTIFY, events) + == SOCKET_ERROR) { + UpdateLastError(); + Close(); + return false; + } + + return true; +} + +bool Win32Socket::HandleClosed(int close_error) { + // WM_CLOSE will be received before all data has been read, so we need to + // hold on to it until the read buffer has been drained. + char ch; + closing_ = true; + close_error_ = close_error; + return (::recv(socket_, &ch, 1, MSG_PEEK) <= 0); +} + +void Win32Socket::PostClosed() { + // If we see that the buffer is indeed drained, then send the close. + closing_ = false; + ::PostMessage(sink_->handle(), WM_SOCKETNOTIFY, + socket_, WSAMAKESELECTREPLY(FD_CLOSE, close_error_)); +} + +void Win32Socket::UpdateLastError() { + error_ = WSAGetLastError(); +} + +int Win32Socket::TranslateOption(Option opt, int* slevel, int* sopt) { + switch (opt) { + case OPT_DONTFRAGMENT: + *slevel = IPPROTO_IP; + *sopt = IP_DONTFRAGMENT; + break; + case OPT_RCVBUF: + *slevel = SOL_SOCKET; + *sopt = SO_RCVBUF; + break; + case OPT_SNDBUF: + *slevel = SOL_SOCKET; + *sopt = SO_SNDBUF; + break; + case OPT_NODELAY: + *slevel = IPPROTO_TCP; + *sopt = TCP_NODELAY; + break; + default: + ASSERT(false); + return -1; + } + return 0; +} + +void Win32Socket::OnSocketNotify(SOCKET socket, int event, int error) { + // Ignore events if we're already closed. + if (socket != socket_) + return; + + error_ = error; + switch (event) { + case FD_CONNECT: + if (error != ERROR_SUCCESS) { + ReportWSAError("WSAAsync:connect notify", error, addr_); +#ifdef _DEBUG + int32 duration = TimeSince(connect_time_); + LOG(LS_INFO) << "WSAAsync:connect error (" << duration + << " ms), faking close"; +#endif + state_ = CS_CLOSED; + // If you get an error connecting, close doesn't really do anything + // and it certainly doesn't send back any close notification, but + // we really only maintain a few states, so it is easiest to get + // back into a known state by pretending that a close happened, even + // though the connect event never did occur. + SignalCloseEvent(this, error); + } else { +#ifdef _DEBUG + int32 duration = TimeSince(connect_time_); + LOG(LS_INFO) << "WSAAsync:connect (" << duration << " ms)"; +#endif + state_ = CS_CONNECTED; + SignalConnectEvent(this); + } + break; + + case FD_ACCEPT: + case FD_READ: + if (error != ERROR_SUCCESS) { + ReportWSAError("WSAAsync:read notify", error, addr_); + } else { + SignalReadEvent(this); + } + break; + + case FD_WRITE: + if (error != ERROR_SUCCESS) { + ReportWSAError("WSAAsync:write notify", error, addr_); + } else { + SignalWriteEvent(this); + } + break; + + case FD_CLOSE: + if (HandleClosed(error)) { + ReportWSAError("WSAAsync:close notify", error, addr_); + state_ = CS_CLOSED; + SignalCloseEvent(this, error); + } + break; + } +} + +void Win32Socket::OnDnsNotify(HANDLE task, int error) { + if (!dns_ || dns_->handle != task) + return; + + uint32 ip = 0; + if (error == 0) { + hostent* pHost = reinterpret_cast(dns_->buffer); + uint32 net_ip = *reinterpret_cast(pHost->h_addr_list[0]); + ip = NetworkToHost32(net_ip); + } + + LOG_F(LS_INFO) << "(" << IPAddress(ip).ToSensitiveString() + << ", " << error << ")"; + + if (error == 0) { + SocketAddress address(ip, dns_->port); + error = DoConnect(address); + } else { + Close(); + } + + if (error) { + error_ = error; + SignalCloseEvent(this, error_); + } else { + delete dns_; + dns_ = NULL; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Win32SocketServer +// Provides cricket base services on top of a win32 gui thread +/////////////////////////////////////////////////////////////////////////////// + +static UINT s_wm_wakeup_id = 0; +const TCHAR Win32SocketServer::kWindowName[] = L"libjingle Message Window"; + +Win32SocketServer::Win32SocketServer(MessageQueue* message_queue) + : message_queue_(message_queue), + wnd_(this), + posted_(false), + hdlg_(NULL) { + if (s_wm_wakeup_id == 0) + s_wm_wakeup_id = RegisterWindowMessage(L"WM_WAKEUP"); + if (!wnd_.Create(NULL, kWindowName, 0, 0, 0, 0, 0, 0)) { + LOG_GLE(LS_ERROR) << "Failed to create message window."; + } +} + +Win32SocketServer::~Win32SocketServer() { + if (wnd_.handle() != NULL) { + KillTimer(wnd_.handle(), 1); + wnd_.Destroy(); + } +} + +Socket* Win32SocketServer::CreateSocket(int type) { + return CreateSocket(AF_INET, type); +} + +Socket* Win32SocketServer::CreateSocket(int family, int type) { + return CreateAsyncSocket(family, type); +} + +AsyncSocket* Win32SocketServer::CreateAsyncSocket(int type) { + return CreateAsyncSocket(AF_INET, type); +} + +AsyncSocket* Win32SocketServer::CreateAsyncSocket(int family, int type) { + Win32Socket* socket = new Win32Socket; + if (socket->CreateT(family, type)) { + return socket; + } + delete socket; + return NULL; +} + +void Win32SocketServer::SetMessageQueue(MessageQueue* queue) { + message_queue_ = queue; +} + +bool Win32SocketServer::Wait(int cms, bool process_io) { + BOOL b; + if (process_io) { + // Spin the Win32 message pump at least once, and as long as requested. + // This is the Thread::ProcessMessages case. + uint32 start = Time(); + do { + MSG msg; + SetTimer(wnd_.handle(), 0, cms, NULL); + // Get the next available message. If we have a modeless dialog, give + // give the message to IsDialogMessage, which will return true if it + // was a message for the dialog that it handled internally. + // Otherwise, dispatch as usual via Translate/DispatchMessage. + b = GetMessage(&msg, NULL, 0, 0); + if (b == -1) { + LOG_GLE(LS_ERROR) << "GetMessage failed."; + return false; + } else if(b) { + if (!hdlg_ || !IsDialogMessage(hdlg_, &msg)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + KillTimer(wnd_.handle(), 0); + } while (b && TimeSince(start) < cms); + } else if (cms != 0) { + // Sit and wait forever for a WakeUp. This is the Thread::Send case. + ASSERT(cms == -1); + MSG msg; + b = GetMessage(&msg, NULL, s_wm_wakeup_id, s_wm_wakeup_id); + { + CritScope scope(&cs_); + posted_ = false; + } + } else { + // No-op (cms == 0 && !process_io). This is the Pump case. + b = TRUE; + } + return (b != FALSE); +} + +void Win32SocketServer::WakeUp() { + if (wnd_.handle()) { + // Set the "message pending" flag, if not already set. + { + CritScope scope(&cs_); + if (posted_) + return; + posted_ = true; + } + + PostMessage(wnd_.handle(), s_wm_wakeup_id, 0, 0); + } +} + +void Win32SocketServer::Pump() { + // Clear the "message pending" flag. + { + CritScope scope(&cs_); + posted_ = false; + } + + // Dispatch all the messages that are currently in our queue. If new messages + // are posted during the dispatch, they will be handled in the next Pump. + // We use max(1, ...) to make sure we try to dispatch at least once, since + // this allow us to process "sent" messages, not included in the size() count. + Message msg; + for (size_t max_messages_to_process = _max(1, message_queue_->size()); + max_messages_to_process > 0 && message_queue_->Get(&msg, 0, false); + --max_messages_to_process) { + message_queue_->Dispatch(&msg); + } + + // Anything remaining? + int delay = message_queue_->GetDelay(); + if (delay == -1) { + KillTimer(wnd_.handle(), 1); + } else { + SetTimer(wnd_.handle(), 1, delay, NULL); + } +} + +bool Win32SocketServer::MessageWindow::OnMessage(UINT wm, WPARAM wp, + LPARAM lp, LRESULT& lr) { + bool handled = false; + if (wm == s_wm_wakeup_id || (wm == WM_TIMER && wp == 1)) { + ss_->Pump(); + lr = 0; + handled = true; + } + return handled; +} + +} // namespace talk_base diff --git a/talk/base/win32socketserver.h b/talk/base/win32socketserver.h new file mode 100644 index 000000000..1fa65235e --- /dev/null +++ b/talk/base/win32socketserver.h @@ -0,0 +1,180 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_WIN32SOCKETSERVER_H_ +#define TALK_BASE_WIN32SOCKETSERVER_H_ + +#ifdef WIN32 +#include "talk/base/asyncsocket.h" +#include "talk/base/criticalsection.h" +#include "talk/base/messagequeue.h" +#include "talk/base/socketserver.h" +#include "talk/base/socketfactory.h" +#include "talk/base/socket.h" +#include "talk/base/thread.h" +#include "talk/base/win32window.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// Win32Socket +/////////////////////////////////////////////////////////////////////////////// + +class Win32Socket : public AsyncSocket { + public: + Win32Socket(); + virtual ~Win32Socket(); + + bool CreateT(int family, int type); + + int Attach(SOCKET s); + void SetTimeout(int ms); + + // AsyncSocket Interface + virtual SocketAddress GetLocalAddress() const; + virtual SocketAddress GetRemoteAddress() const; + virtual int Bind(const SocketAddress& addr); + virtual int Connect(const SocketAddress& addr); + virtual int Send(const void *buffer, size_t length); + virtual int SendTo(const void *buffer, size_t length, const SocketAddress& addr); + virtual int Recv(void *buffer, size_t length); + virtual int RecvFrom(void *buffer, size_t length, SocketAddress *out_addr); + virtual int Listen(int backlog); + virtual Win32Socket *Accept(SocketAddress *out_addr); + virtual int Close(); + virtual int GetError() const; + virtual void SetError(int error); + virtual ConnState GetState() const; + virtual int EstimateMTU(uint16* mtu); + virtual int GetOption(Option opt, int* value); + virtual int SetOption(Option opt, int value); + + private: + void CreateSink(); + bool SetAsync(int events); + int DoConnect(const SocketAddress& addr); + bool HandleClosed(int close_error); + void PostClosed(); + void UpdateLastError(); + static int TranslateOption(Option opt, int* slevel, int* sopt); + + void OnSocketNotify(SOCKET socket, int event, int error); + void OnDnsNotify(HANDLE task, int error); + + SOCKET socket_; + int error_; + ConnState state_; + SocketAddress addr_; // address that we connected to (see DoConnect) + uint32 connect_time_; + bool closing_; + int close_error_; + + class EventSink; + friend class EventSink; + EventSink * sink_; + + struct DnsLookup; + DnsLookup * dns_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// Win32SocketServer +/////////////////////////////////////////////////////////////////////////////// + +class Win32SocketServer : public SocketServer { + public: + explicit Win32SocketServer(MessageQueue* message_queue); + virtual ~Win32SocketServer(); + + void set_modeless_dialog(HWND hdlg) { + hdlg_ = hdlg; + } + + // SocketServer Interface + virtual Socket* CreateSocket(int type); + virtual Socket* CreateSocket(int family, int type); + + virtual AsyncSocket* CreateAsyncSocket(int type); + virtual AsyncSocket* CreateAsyncSocket(int family, int type); + + virtual void SetMessageQueue(MessageQueue* queue); + virtual bool Wait(int cms, bool process_io); + virtual void WakeUp(); + + void Pump(); + + HWND handle() { return wnd_.handle(); } + + private: + class MessageWindow : public Win32Window { + public: + explicit MessageWindow(Win32SocketServer* ss) : ss_(ss) {} + private: + virtual bool OnMessage(UINT msg, WPARAM wp, LPARAM lp, LRESULT& result); + Win32SocketServer* ss_; + }; + + static const TCHAR kWindowName[]; + MessageQueue *message_queue_; + MessageWindow wnd_; + CriticalSection cs_; + bool posted_; + HWND hdlg_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// Win32Thread. Automatically pumps Windows messages. +/////////////////////////////////////////////////////////////////////////////// + +class Win32Thread : public Thread { + public: + Win32Thread() : ss_(this), id_(0) { + set_socketserver(&ss_); + } + virtual ~Win32Thread() { + set_socketserver(NULL); + } + virtual void Run() { + id_ = GetCurrentThreadId(); + Thread::Run(); + id_ = 0; + } + virtual void Quit() { + PostThreadMessage(id_, WM_QUIT, 0, 0); + } + private: + Win32SocketServer ss_; + DWORD id_; +}; + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // WIN32 + +#endif // TALK_BASE_WIN32SOCKETSERVER_H_ diff --git a/talk/base/win32socketserver_unittest.cc b/talk/base/win32socketserver_unittest.cc new file mode 100644 index 000000000..f78a4446d --- /dev/null +++ b/talk/base/win32socketserver_unittest.cc @@ -0,0 +1,151 @@ +// Copyright 2009 Google Inc. All Rights Reserved. + + +#include "talk/base/gunit.h" +#include "talk/base/socket_unittest.h" +#include "talk/base/thread.h" +#include "talk/base/win32socketserver.h" + +namespace talk_base { + +// Test that Win32SocketServer::Wait works as expected. +TEST(Win32SocketServerTest, TestWait) { + Win32SocketServer server(NULL); + uint32 start = Time(); + server.Wait(1000, true); + EXPECT_GE(TimeSince(start), 1000); +} + +// Test that Win32Socket::Pump does not touch general Windows messages. +TEST(Win32SocketServerTest, TestPump) { + Win32SocketServer server(NULL); + SocketServerScope scope(&server); + EXPECT_EQ(TRUE, PostMessage(NULL, WM_USER, 999, 0)); + server.Pump(); + MSG msg; + EXPECT_EQ(TRUE, PeekMessage(&msg, NULL, WM_USER, 0, PM_REMOVE)); + EXPECT_EQ(WM_USER, msg.message); + EXPECT_EQ(999, msg.wParam); +} + +// Test that Win32Socket passes all the generic Socket tests. +class Win32SocketTest : public SocketTest { + protected: + Win32SocketTest() : server_(NULL), scope_(&server_) {} + Win32SocketServer server_; + SocketServerScope scope_; +}; + +TEST_F(Win32SocketTest, TestConnectIPv4) { + SocketTest::TestConnectIPv4(); +} + +TEST_F(Win32SocketTest, TestConnectIPv6) { + SocketTest::TestConnectIPv6(); +} + +TEST_F(Win32SocketTest, TestConnectWithDnsLookupIPv4) { + SocketTest::TestConnectWithDnsLookupIPv4(); +} + +TEST_F(Win32SocketTest, TestConnectWithDnsLookupIPv6) { + SocketTest::TestConnectWithDnsLookupIPv6(); +} + +TEST_F(Win32SocketTest, TestConnectFailIPv4) { + SocketTest::TestConnectFailIPv4(); +} + +TEST_F(Win32SocketTest, TestConnectFailIPv6) { + SocketTest::TestConnectFailIPv6(); +} + +TEST_F(Win32SocketTest, TestConnectWithDnsLookupFailIPv4) { + SocketTest::TestConnectWithDnsLookupFailIPv4(); +} + +TEST_F(Win32SocketTest, TestConnectWithDnsLookupFailIPv6) { + SocketTest::TestConnectWithDnsLookupFailIPv6(); +} + +TEST_F(Win32SocketTest, TestConnectWithClosedSocketIPv4) { + SocketTest::TestConnectWithClosedSocketIPv4(); +} + +TEST_F(Win32SocketTest, TestConnectWithClosedSocketIPv6) { + SocketTest::TestConnectWithClosedSocketIPv6(); +} + +TEST_F(Win32SocketTest, TestConnectWhileNotClosedIPv4) { + SocketTest::TestConnectWhileNotClosedIPv4(); +} + +TEST_F(Win32SocketTest, TestConnectWhileNotClosedIPv6) { + SocketTest::TestConnectWhileNotClosedIPv6(); +} + +TEST_F(Win32SocketTest, TestServerCloseDuringConnectIPv4) { + SocketTest::TestServerCloseDuringConnectIPv4(); +} + +TEST_F(Win32SocketTest, TestServerCloseDuringConnectIPv6) { + SocketTest::TestServerCloseDuringConnectIPv6(); +} + +TEST_F(Win32SocketTest, TestClientCloseDuringConnectIPv4) { + SocketTest::TestClientCloseDuringConnectIPv4(); +} + +TEST_F(Win32SocketTest, TestClientCloseDuringConnectIPv6) { + SocketTest::TestClientCloseDuringConnectIPv6(); +} + +TEST_F(Win32SocketTest, TestServerCloseIPv4) { + SocketTest::TestServerCloseIPv4(); +} + +TEST_F(Win32SocketTest, TestServerCloseIPv6) { + SocketTest::TestServerCloseIPv6(); +} + +TEST_F(Win32SocketTest, TestCloseInClosedCallbackIPv4) { + SocketTest::TestCloseInClosedCallbackIPv4(); +} + +TEST_F(Win32SocketTest, TestCloseInClosedCallbackIPv6) { + SocketTest::TestCloseInClosedCallbackIPv6(); +} + +TEST_F(Win32SocketTest, TestSocketServerWaitIPv4) { + SocketTest::TestSocketServerWaitIPv4(); +} + +TEST_F(Win32SocketTest, TestSocketServerWaitIPv6) { + SocketTest::TestSocketServerWaitIPv6(); +} + +TEST_F(Win32SocketTest, TestTcpIPv4) { + SocketTest::TestTcpIPv4(); +} + +TEST_F(Win32SocketTest, TestTcpIPv6) { + SocketTest::TestTcpIPv6(); +} + +TEST_F(Win32SocketTest, TestUdpIPv4) { + SocketTest::TestUdpIPv4(); +} + +TEST_F(Win32SocketTest, TestUdpIPv6) { + SocketTest::TestUdpIPv6(); +} + +TEST_F(Win32SocketTest, TestGetSetOptionsIPv4) { + SocketTest::TestGetSetOptionsIPv4(); +} + +TEST_F(Win32SocketTest, TestGetSetOptionsIPv6) { + SocketTest::TestGetSetOptionsIPv6(); +} + +} // namespace talk_base diff --git a/talk/base/win32toolhelp.h b/talk/base/win32toolhelp.h new file mode 100644 index 000000000..64a191a1c --- /dev/null +++ b/talk/base/win32toolhelp.h @@ -0,0 +1,166 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + + +#ifndef TALK_BASE_WIN32TOOLHELP_H_ +#define TALK_BASE_WIN32TOOLHELP_H_ + +#ifndef WIN32 +#error WIN32 Only +#endif + +#include "talk/base/win32.h" + +// Should be included first, but that causes redefinitions. +#include + +#include "talk/base/constructormagic.h" + +namespace talk_base { + +// The toolhelp api used to enumerate processes and their modules +// on Windows is very repetetive and clunky to use. This little +// template wraps it to make it a little more programmer friendly. +// +// Traits: Traits type that adapts the enumerator to the corresponding +// win32 toolhelp api. Each traits class need to: +// - define the type of the enumerated data as a public symbol Type +// +// - implement bool First(HANDLE, T*) normally calls a +// Xxxx32First method in the toolhelp API. Ex Process32First(...) +// +// - implement bool Next(HANDLE, T*) normally calls a +// Xxxx32Next method in the toolhelp API. Ex Process32Next(...) +// +// - implement bool CloseHandle(HANDLE) +// +template +class ToolhelpEnumeratorBase { + public: + ToolhelpEnumeratorBase(HANDLE snapshot) + : snapshot_(snapshot), broken_(false), first_(true) { + + // Clear out the Traits::Type structure instance. + Zero(¤t_); + } + + virtual ~ToolhelpEnumeratorBase() { + Close(); + } + + // Moves forward to the next object using the First and Next + // pointers. If either First or Next ever indicates an failure + // all subsequent calls to this method will fail; the enumerator + // object is considered broken. + bool Next() { + if (!Valid()) { + return false; + } + + // Move the iteration forward. + current_.dwSize = sizeof(typename Traits::Type); + bool incr_ok = false; + if (first_) { + incr_ok = Traits::First(snapshot_, ¤t_); + first_ = false; + } else { + incr_ok = Traits::Next(snapshot_, ¤t_); + } + + if (!incr_ok) { + Zero(¤t_); + broken_ = true; + } + + return incr_ok; + } + + const typename Traits::Type& current() const { + return current_; + } + + void Close() { + if (snapshot_ != INVALID_HANDLE_VALUE) { + Traits::CloseHandle(snapshot_); + snapshot_ = INVALID_HANDLE_VALUE; + } + } + + private: + // Checks the state of the snapshot handle. + bool Valid() { + return snapshot_ != INVALID_HANDLE_VALUE && !broken_; + } + + static void Zero(typename Traits::Type* buff) { + ZeroMemory(buff, sizeof(typename Traits::Type)); + } + + HANDLE snapshot_; + typename Traits::Type current_; + bool broken_; + bool first_; +}; + +class ToolhelpTraits { + public: + static HANDLE CreateSnapshot(uint32 flags, uint32 process_id) { + return CreateToolhelp32Snapshot(flags, process_id); + } + + static bool CloseHandle(HANDLE handle) { + return ::CloseHandle(handle) == TRUE; + } +}; + +class ToolhelpProcessTraits : public ToolhelpTraits { + public: + typedef PROCESSENTRY32 Type; + + static bool First(HANDLE handle, Type* t) { + return ::Process32First(handle, t) == TRUE; + } + + static bool Next(HANDLE handle, Type* t) { + return ::Process32Next(handle, t) == TRUE; + } +}; + +class ProcessEnumerator : public ToolhelpEnumeratorBase { + public: + ProcessEnumerator() + : ToolhelpEnumeratorBase( + ToolhelpProcessTraits::CreateSnapshot(TH32CS_SNAPPROCESS, 0)) { + } + + private: + DISALLOW_EVIL_CONSTRUCTORS(ProcessEnumerator); +}; + +class ToolhelpModuleTraits : public ToolhelpTraits { + public: + typedef MODULEENTRY32 Type; + + static bool First(HANDLE handle, Type* t) { + return ::Module32First(handle, t) == TRUE; + } + + static bool Next(HANDLE handle, Type* t) { + return ::Module32Next(handle, t) == TRUE; + } +}; + +class ModuleEnumerator : public ToolhelpEnumeratorBase { + public: + explicit ModuleEnumerator(uint32 process_id) + : ToolhelpEnumeratorBase( + ToolhelpModuleTraits::CreateSnapshot(TH32CS_SNAPMODULE, + process_id)) { + } + + private: + DISALLOW_EVIL_CONSTRUCTORS(ModuleEnumerator); +}; + +} // namespace talk_base + +#endif // TALK_BASE_WIN32TOOLHELP_H_ diff --git a/talk/base/win32toolhelp_unittest.cc b/talk/base/win32toolhelp_unittest.cc new file mode 100644 index 000000000..529bef95a --- /dev/null +++ b/talk/base/win32toolhelp_unittest.cc @@ -0,0 +1,296 @@ +/* + * libjingle + * Copyright 2010, 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/base/gunit.h" +#include "talk/base/pathutils.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/win32toolhelp.h" + +namespace talk_base { + +typedef struct { + // Required to match the toolhelp api struct 'design'. + DWORD dwSize; + int a; + uint32 b; +} TestData; + +class Win32ToolhelpTest : public testing::Test { + public: + Win32ToolhelpTest() { + } + + HANDLE AsHandle() { + return reinterpret_cast(this); + } + + static Win32ToolhelpTest* AsFixture(HANDLE handle) { + return reinterpret_cast(handle); + } + + static bool First(HANDLE handle, TestData* d) { + Win32ToolhelpTest* tst = Win32ToolhelpTest::AsFixture(handle); + // This method should be called only once for every test. + // If it is called more than once it return false which + // should break the test. + EXPECT_EQ(0, tst->first_called_); // Just to be safe. + if (tst->first_called_ > 0) { + return false; + } + + *d = kTestData[0]; + tst->index_ = 1; + ++(tst->first_called_); + return true; + } + + static bool Next(HANDLE handle, TestData* d) { + Win32ToolhelpTest* tst = Win32ToolhelpTest::AsFixture(handle); + ++(tst->next_called_); + + if (tst->index_ >= kTestDataSize) { + return FALSE; + } + + *d = kTestData[tst->index_]; + ++(tst->index_); + return true; + } + + static bool Fail(HANDLE handle, TestData* d) { + Win32ToolhelpTest* tst = Win32ToolhelpTest::AsFixture(handle); + ++(tst->fail_called_); + return false; + } + + static bool CloseHandle(HANDLE handle) { + Win32ToolhelpTest* tst = Win32ToolhelpTest::AsFixture(handle); + ++(tst->close_handle_called_); + return true; + } + + protected: + virtual void SetUp() { + fail_called_ = 0; + first_called_ = 0; + next_called_ = 0; + close_handle_called_ = 0; + index_ = 0; + } + + static bool AllZero(const TestData& data) { + return data.dwSize == 0 && data.a == 0 && data.b == 0; + } + + static bool Equals(const TestData& expected, const TestData& actual) { + return expected.dwSize == actual.dwSize + && expected.a == actual.a + && expected.b == actual.b; + } + + bool CheckCallCounters(int first, int next, int fail, int close) { + bool match = first_called_ == first && next_called_ == next + && fail_called_ == fail && close_handle_called_ == close; + + if (!match) { + LOG(LS_ERROR) << "Expected: (" + << first << ", " + << next << ", " + << fail << ", " + << close << ")"; + + LOG(LS_ERROR) << "Actual: (" + << first_called_ << ", " + << next_called_ << ", " + << fail_called_ << ", " + << close_handle_called_ << ")"; + } + return match; + } + + static const int kTestDataSize = 3; + static const TestData kTestData[]; + int index_; + int first_called_; + int fail_called_; + int next_called_; + int close_handle_called_; +}; + +const TestData Win32ToolhelpTest::kTestData[] = { + {1, 1, 1}, {2, 2, 2}, {3, 3, 3} +}; + + +class TestTraits { + public: + typedef TestData Type; + + static bool First(HANDLE handle, Type* t) { + return Win32ToolhelpTest::First(handle, t); + } + + static bool Next(HANDLE handle, Type* t) { + return Win32ToolhelpTest::Next(handle, t); + } + + static bool CloseHandle(HANDLE handle) { + return Win32ToolhelpTest::CloseHandle(handle); + } +}; + +class BadFirstTraits { + public: + typedef TestData Type; + + static bool First(HANDLE handle, Type* t) { + return Win32ToolhelpTest::Fail(handle, t); + } + + static bool Next(HANDLE handle, Type* t) { + // This should never be called. + ADD_FAILURE(); + return false; + } + + static bool CloseHandle(HANDLE handle) { + return Win32ToolhelpTest::CloseHandle(handle); + } +}; + +class BadNextTraits { + public: + typedef TestData Type; + + static bool First(HANDLE handle, Type* t) { + return Win32ToolhelpTest::First(handle, t); + } + + static bool Next(HANDLE handle, Type* t) { + return Win32ToolhelpTest::Fail(handle, t); + } + + static bool CloseHandle(HANDLE handle) { + return Win32ToolhelpTest::CloseHandle(handle); + } +}; + +// The toolhelp in normally inherited but most of +// these tests only excercise the methods from the +// traits therefore I use a typedef to make the +// test code easier to read. +typedef talk_base::ToolhelpEnumeratorBase EnumeratorForTest; + +TEST_F(Win32ToolhelpTest, TestNextWithInvalidCtorHandle) { + EnumeratorForTest t(INVALID_HANDLE_VALUE); + + EXPECT_FALSE(t.Next()); + EXPECT_TRUE(CheckCallCounters(0, 0, 0, 0)); +} + +// Tests that Next() returns false if the first-pointer +// function fails. +TEST_F(Win32ToolhelpTest, TestNextFirstFails) { + typedef talk_base::ToolhelpEnumeratorBase BadEnumerator; + talk_base::scoped_ptr t(new BadEnumerator(AsHandle())); + + // If next ever fails it shall always fail. + EXPECT_FALSE(t->Next()); + EXPECT_FALSE(t->Next()); + EXPECT_FALSE(t->Next()); + t.reset(); + EXPECT_TRUE(CheckCallCounters(0, 0, 1, 1)); +} + +// Tests that Next() returns false if the next-pointer +// function fails. +TEST_F(Win32ToolhelpTest, TestNextNextFails) { + typedef talk_base::ToolhelpEnumeratorBase BadEnumerator; + talk_base::scoped_ptr t(new BadEnumerator(AsHandle())); + + // If next ever fails it shall always fail. No more calls + // shall be dispatched to Next(...). + EXPECT_TRUE(t->Next()); + EXPECT_FALSE(t->Next()); + EXPECT_FALSE(t->Next()); + t.reset(); + EXPECT_TRUE(CheckCallCounters(1, 0, 1, 1)); +} + + +// Tests that current returns an object is all zero's +// if Next() hasn't been called. +TEST_F(Win32ToolhelpTest, TestCurrentNextNotCalled) { + talk_base::scoped_ptr t(new EnumeratorForTest(AsHandle())); + EXPECT_TRUE(AllZero(t->current())); + t.reset(); + EXPECT_TRUE(CheckCallCounters(0, 0, 0, 1)); +} + +// Tests the simple everything works path through the code. +TEST_F(Win32ToolhelpTest, TestCurrentNextCalled) { + talk_base::scoped_ptr t(new EnumeratorForTest(AsHandle())); + + EXPECT_TRUE(t->Next()); + EXPECT_TRUE(Equals(t->current(), kTestData[0])); + EXPECT_TRUE(t->Next()); + EXPECT_TRUE(Equals(t->current(), kTestData[1])); + EXPECT_TRUE(t->Next()); + EXPECT_TRUE(Equals(t->current(), kTestData[2])); + EXPECT_FALSE(t->Next()); + t.reset(); + EXPECT_TRUE(CheckCallCounters(1, 3, 0, 1)); +} + +TEST_F(Win32ToolhelpTest, TestCurrentProcess) { + int size = MAX_PATH; + WCHAR buf[MAX_PATH]; + GetModuleFileName(NULL, buf, ARRAY_SIZE(buf)); + std::wstring name = ToUtf16(Pathname(ToUtf8(buf)).filename()); + + talk_base::ProcessEnumerator processes; + bool found = false; + while (processes.Next()) { + if (!name.compare(processes.current().szExeFile)) { + found = true; + break; + } + } + EXPECT_TRUE(found); + + talk_base::ModuleEnumerator modules(processes.current().th32ProcessID); + found = false; + while (modules.Next()) { + if (!name.compare(modules.current().szModule)) { + found = true; + break; + } + } + EXPECT_TRUE(found); +} + +} // namespace talk_base diff --git a/talk/base/win32window.cc b/talk/base/win32window.cc new file mode 100644 index 000000000..b11c34924 --- /dev/null +++ b/talk/base/win32window.cc @@ -0,0 +1,138 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/win32window.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// Win32Window +/////////////////////////////////////////////////////////////////////////////// + +static const wchar_t kWindowBaseClassName[] = L"WindowBaseClass"; +HINSTANCE Win32Window::instance_ = NULL; +ATOM Win32Window::window_class_ = 0; + +Win32Window::Win32Window() : wnd_(NULL) { +} + +Win32Window::~Win32Window() { + ASSERT(NULL == wnd_); +} + +bool Win32Window::Create(HWND parent, const wchar_t* title, DWORD style, + DWORD exstyle, int x, int y, int cx, int cy) { + if (wnd_) { + // Window already exists. + return false; + } + + if (!window_class_) { + if (!GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&Win32Window::WndProc), + &instance_)) { + LOG_GLE(LS_ERROR) << "GetModuleHandleEx failed"; + return false; + } + + // Class not registered, register it. + WNDCLASSEX wcex; + memset(&wcex, 0, sizeof(wcex)); + wcex.cbSize = sizeof(wcex); + wcex.hInstance = instance_; + wcex.lpfnWndProc = &Win32Window::WndProc; + wcex.lpszClassName = kWindowBaseClassName; + window_class_ = ::RegisterClassEx(&wcex); + if (!window_class_) { + LOG_GLE(LS_ERROR) << "RegisterClassEx failed"; + return false; + } + } + wnd_ = ::CreateWindowEx(exstyle, kWindowBaseClassName, title, style, + x, y, cx, cy, parent, NULL, instance_, this); + return (NULL != wnd_); +} + +void Win32Window::Destroy() { + VERIFY(::DestroyWindow(wnd_) != FALSE); +} + +void Win32Window::Shutdown() { + if (window_class_) { + ::UnregisterClass(MAKEINTATOM(window_class_), instance_); + window_class_ = 0; + } +} + +bool Win32Window::OnMessage(UINT uMsg, WPARAM wParam, LPARAM lParam, + LRESULT& result) { + switch (uMsg) { + case WM_CLOSE: + if (!OnClose()) { + result = 0; + return true; + } + break; + } + return false; +} + +LRESULT Win32Window::WndProc(HWND hwnd, UINT uMsg, + WPARAM wParam, LPARAM lParam) { + Win32Window* that = reinterpret_cast( + ::GetWindowLongPtr(hwnd, GWLP_USERDATA)); + if (!that && (WM_CREATE == uMsg)) { + CREATESTRUCT* cs = reinterpret_cast(lParam); + that = static_cast(cs->lpCreateParams); + that->wnd_ = hwnd; + ::SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast(that)); + } + if (that) { + LRESULT result; + bool handled = that->OnMessage(uMsg, wParam, lParam, result); + if (WM_DESTROY == uMsg) { + for (HWND child = ::GetWindow(hwnd, GW_CHILD); child; + child = ::GetWindow(child, GW_HWNDNEXT)) { + LOG(LS_INFO) << "Child window: " << static_cast(child); + } + } + if (WM_NCDESTROY == uMsg) { + ::SetWindowLongPtr(hwnd, GWLP_USERDATA, NULL); + that->wnd_ = NULL; + that->OnNcDestroy(); + } + if (handled) { + return result; + } + } + return ::DefWindowProc(hwnd, uMsg, wParam, lParam); +} + +} // namespace talk_base diff --git a/talk/base/win32window.h b/talk/base/win32window.h new file mode 100644 index 000000000..37f36964a --- /dev/null +++ b/talk/base/win32window.h @@ -0,0 +1,77 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_WIN32WINDOW_H_ +#define TALK_BASE_WIN32WINDOW_H_ + +#ifdef WIN32 + +#include "talk/base/win32.h" + +namespace talk_base { + +/////////////////////////////////////////////////////////////////////////////// +// Win32Window +/////////////////////////////////////////////////////////////////////////////// + +class Win32Window { + public: + Win32Window(); + virtual ~Win32Window(); + + HWND handle() const { return wnd_; } + + bool Create(HWND parent, const wchar_t* title, DWORD style, DWORD exstyle, + int x, int y, int cx, int cy); + void Destroy(); + + // Call this when your DLL unloads. + static void Shutdown(); + + protected: + virtual bool OnMessage(UINT uMsg, WPARAM wParam, LPARAM lParam, + LRESULT& result); + + virtual bool OnClose() { return true; } + virtual void OnNcDestroy() { } + + private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, + LPARAM lParam); + + HWND wnd_; + static HINSTANCE instance_; + static ATOM window_class_; +}; + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // WIN32 + +#endif // TALK_BASE_WIN32WINDOW_H_ diff --git a/talk/base/win32window_unittest.cc b/talk/base/win32window_unittest.cc new file mode 100644 index 000000000..96173b789 --- /dev/null +++ b/talk/base/win32window_unittest.cc @@ -0,0 +1,83 @@ +/* + * libjingle + * Copyright 2009, 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/base/gunit.h" +#include "talk/base/common.h" +#include "talk/base/win32window.h" +#include "talk/base/logging.h" + +static LRESULT kDummyResult = 0x1234ABCD; + +class TestWindow : public talk_base::Win32Window { + public: + TestWindow() : destroyed_(false) { memset(&msg_, 0, sizeof(msg_)); } + const MSG& msg() const { return msg_; } + bool destroyed() const { return destroyed_; } + + virtual bool OnMessage(UINT uMsg, WPARAM wParam, + LPARAM lParam, LRESULT& result) { + msg_.message = uMsg; + msg_.wParam = wParam; + msg_.lParam = lParam; + result = kDummyResult; + return true; + } + virtual void OnNcDestroy() { + destroyed_ = true; + } + + private: + MSG msg_; + bool destroyed_; +}; + +TEST(Win32WindowTest, Basics) { + TestWindow wnd; + EXPECT_TRUE(wnd.handle() == NULL); + EXPECT_FALSE(wnd.destroyed()); + EXPECT_TRUE(wnd.Create(0, L"Test", 0, 0, 0, 0, 100, 100)); + EXPECT_TRUE(wnd.handle() != NULL); + EXPECT_EQ(kDummyResult, ::SendMessage(wnd.handle(), WM_USER, 1, 2)); + EXPECT_EQ(WM_USER, wnd.msg().message); + EXPECT_EQ(1, wnd.msg().wParam); + EXPECT_EQ(2, wnd.msg().lParam); + wnd.Destroy(); + EXPECT_TRUE(wnd.handle() == NULL); + EXPECT_TRUE(wnd.destroyed()); +} + +TEST(Win32WindowTest, MultipleWindows) { + TestWindow wnd1, wnd2; + EXPECT_TRUE(wnd1.Create(0, L"Test", 0, 0, 0, 0, 100, 100)); + EXPECT_TRUE(wnd2.Create(0, L"Test", 0, 0, 0, 0, 100, 100)); + EXPECT_TRUE(wnd1.handle() != NULL); + EXPECT_TRUE(wnd2.handle() != NULL); + wnd1.Destroy(); + wnd2.Destroy(); + EXPECT_TRUE(wnd2.handle() == NULL); + EXPECT_TRUE(wnd1.handle() == NULL); +} diff --git a/talk/base/win32windowpicker.cc b/talk/base/win32windowpicker.cc new file mode 100644 index 000000000..d996c0eb9 --- /dev/null +++ b/talk/base/win32windowpicker.cc @@ -0,0 +1,137 @@ +// Copyright 2010 Google Inc. All Rights Reserved + + +#include "talk/base/win32windowpicker.h" + +#include +#include + +#include "talk/base/common.h" +#include "talk/base/logging.h" + +namespace talk_base { + +namespace { + +// Window class names that we want to filter out. +const char kProgramManagerClass[] = "Progman"; +const char kButtonClass[] = "Button"; + +} // namespace + +BOOL CALLBACK Win32WindowPicker::EnumProc(HWND hwnd, LPARAM l_param) { + WindowDescriptionList* descriptions = + reinterpret_cast(l_param); + + // Skip windows that are invisible, minimized, have no title, or are owned, + // unless they have the app window style set. Except for minimized windows, + // this is what Alt-Tab does. + // TODO: Figure out how to grab a thumbnail of a minimized window and + // include them in the list. + int len = GetWindowTextLength(hwnd); + HWND owner = GetWindow(hwnd, GW_OWNER); + LONG exstyle = GetWindowLong(hwnd, GWL_EXSTYLE); + if (len == 0 || IsIconic(hwnd) || !IsWindowVisible(hwnd) || + (owner && !(exstyle & WS_EX_APPWINDOW))) { + // TODO: Investigate if windows without title still could be + // interesting to share. We could use the name of the process as title: + // + // GetWindowThreadProcessId() + // OpenProcess() + // QueryFullProcessImageName() + return TRUE; + } + + // Skip the Program Manager window and the Start button. + TCHAR class_name_w[500]; + ::GetClassName(hwnd, class_name_w, 500); + std::string class_name = ToUtf8(class_name_w); + if (class_name == kProgramManagerClass || class_name == kButtonClass) { + // We don't want the Program Manager window nor the Start button. + return TRUE; + } + + TCHAR window_title[500]; + GetWindowText(hwnd, window_title, ARRAY_SIZE(window_title)); + std::string title = ToUtf8(window_title); + + WindowId id(hwnd); + WindowDescription desc(id, title); + descriptions->push_back(desc); + return TRUE; +} + +BOOL CALLBACK Win32WindowPicker::MonitorEnumProc(HMONITOR h_monitor, + HDC hdc_monitor, + LPRECT lprc_monitor, + LPARAM l_param) { + DesktopDescriptionList* desktop_desc = + reinterpret_cast(l_param); + + DesktopId id(h_monitor, static_cast(desktop_desc->size())); + // TODO: Figure out an appropriate desktop title. + DesktopDescription desc(id, ""); + + // Determine whether it's the primary monitor. + MONITORINFO monitor_info = {0}; + monitor_info.cbSize = sizeof(monitor_info); + bool primary = (GetMonitorInfo(h_monitor, &monitor_info) && + (monitor_info.dwFlags & MONITORINFOF_PRIMARY) != 0); + desc.set_primary(primary); + + desktop_desc->push_back(desc); + return TRUE; +} + +Win32WindowPicker::Win32WindowPicker() { +} + +bool Win32WindowPicker::Init() { + return true; +} +// TODO: Consider changing enumeration to clear() descriptions +// before append(). +bool Win32WindowPicker::GetWindowList(WindowDescriptionList* descriptions) { + LPARAM desc = reinterpret_cast(descriptions); + return EnumWindows(Win32WindowPicker::EnumProc, desc) != FALSE; +} + +bool Win32WindowPicker::GetDesktopList(DesktopDescriptionList* descriptions) { + // Create a fresh WindowDescriptionList so that we can use desktop_desc.size() + // in MonitorEnumProc to compute the desktop index. + DesktopDescriptionList desktop_desc; + HDC hdc = GetDC(NULL); + bool success = false; + if (EnumDisplayMonitors(hdc, NULL, Win32WindowPicker::MonitorEnumProc, + reinterpret_cast(&desktop_desc)) != FALSE) { + // Append the desktop descriptions to the end of the returned descriptions. + descriptions->insert(descriptions->end(), desktop_desc.begin(), + desktop_desc.end()); + success = true; + } + ReleaseDC(NULL, hdc); + return success; +} + +bool Win32WindowPicker::GetDesktopDimensions(const DesktopId& id, + int* width, + int* height) { + MONITORINFOEX monitor_info; + monitor_info.cbSize = sizeof(MONITORINFOEX); + if (!GetMonitorInfo(id.id(), &monitor_info)) { + return false; + } + *width = monitor_info.rcMonitor.right - monitor_info.rcMonitor.left; + *height = monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top; + return true; +} + +bool Win32WindowPicker::IsVisible(const WindowId& id) { + return (::IsWindow(id.id()) != FALSE && ::IsWindowVisible(id.id()) != FALSE); +} + +bool Win32WindowPicker::MoveToFront(const WindowId& id) { + return SetForegroundWindow(id.id()) != FALSE; +} + +} // namespace talk_base diff --git a/talk/base/win32windowpicker.h b/talk/base/win32windowpicker.h new file mode 100644 index 000000000..5e8fc6ad9 --- /dev/null +++ b/talk/base/win32windowpicker.h @@ -0,0 +1,33 @@ +// Copyright 2010 Google Inc. All Rights Reserved + + +#ifndef TALK_BASE_WIN32WINDOWPICKER_H_ +#define TALK_BASE_WIN32WINDOWPICKER_H_ + +#include "talk/base/win32.h" +#include "talk/base/windowpicker.h" + +namespace talk_base { + +class Win32WindowPicker : public WindowPicker { + public: + Win32WindowPicker(); + virtual bool Init(); + virtual bool IsVisible(const WindowId& id); + virtual bool MoveToFront(const WindowId& id); + virtual bool GetWindowList(WindowDescriptionList* descriptions); + virtual bool GetDesktopList(DesktopDescriptionList* descriptions); + virtual bool GetDesktopDimensions(const DesktopId& id, int* width, + int* height); + + protected: + static BOOL CALLBACK EnumProc(HWND hwnd, LPARAM l_param); + static BOOL CALLBACK MonitorEnumProc(HMONITOR h_monitor, + HDC hdc_monitor, + LPRECT lprc_monitor, + LPARAM l_param); +}; + +} // namespace talk_base + +#endif // TALK_BASE_WIN32WINDOWPICKER_H_ diff --git a/talk/base/win32windowpicker_unittest.cc b/talk/base/win32windowpicker_unittest.cc new file mode 100644 index 000000000..b418fd78f --- /dev/null +++ b/talk/base/win32windowpicker_unittest.cc @@ -0,0 +1,93 @@ +// Copyright 2010 Google Inc. All Rights Reserved + + +#include "talk/base/gunit.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/win32window.h" +#include "talk/base/win32windowpicker.h" +#include "talk/base/windowpicker.h" + +#ifndef WIN32 +#error Only for Windows +#endif + +namespace talk_base { + +static const TCHAR* kVisibleWindowTitle = L"Visible Window"; +static const TCHAR* kInvisibleWindowTitle = L"Invisible Window"; + +class Win32WindowPickerForTest : public Win32WindowPicker { + public: + Win32WindowPickerForTest() { + EXPECT_TRUE(visible_window_.Create(NULL, kVisibleWindowTitle, WS_VISIBLE, + 0, 0, 0, 0, 0)); + EXPECT_TRUE(invisible_window_.Create(NULL, kInvisibleWindowTitle, 0, + 0, 0, 0, 0, 0)); + } + + ~Win32WindowPickerForTest() { + visible_window_.Destroy(); + invisible_window_.Destroy(); + } + + virtual bool GetWindowList(WindowDescriptionList* descriptions) { + if (!Win32WindowPicker::EnumProc(visible_window_.handle(), + reinterpret_cast(descriptions))) { + return false; + } + if (!Win32WindowPicker::EnumProc(invisible_window_.handle(), + reinterpret_cast(descriptions))) { + return false; + } + return true; + } + + Win32Window* visible_window() { + return &visible_window_; + } + + Win32Window* invisible_window() { + return &invisible_window_; + } + + private: + Win32Window visible_window_; + Win32Window invisible_window_; +}; + +TEST(Win32WindowPickerTest, TestGetWindowList) { + Win32WindowPickerForTest window_picker; + WindowDescriptionList descriptions; + EXPECT_TRUE(window_picker.GetWindowList(&descriptions)); + EXPECT_EQ(1, descriptions.size()); + WindowDescription desc = descriptions.front(); + EXPECT_EQ(window_picker.visible_window()->handle(), desc.id().id()); + TCHAR window_title[500]; + GetWindowText(window_picker.visible_window()->handle(), window_title, + ARRAY_SIZE(window_title)); + EXPECT_EQ(0, wcscmp(window_title, kVisibleWindowTitle)); +} + +TEST(Win32WindowPickerTest, TestIsVisible) { + Win32WindowPickerForTest window_picker; + HWND visible_id = window_picker.visible_window()->handle(); + HWND invisible_id = window_picker.invisible_window()->handle(); + EXPECT_TRUE(window_picker.IsVisible(WindowId(visible_id))); + EXPECT_FALSE(window_picker.IsVisible(WindowId(invisible_id))); +} + +TEST(Win32WindowPickerTest, TestMoveToFront) { + Win32WindowPickerForTest window_picker; + HWND visible_id = window_picker.visible_window()->handle(); + HWND invisible_id = window_picker.invisible_window()->handle(); + + // There are a number of condition where SetForegroundWindow might + // fail depending on the state of the calling process. To be on the + // safe side we doesn't expect MoveToFront to return true, just test + // that we don't crash. + window_picker.MoveToFront(WindowId(visible_id)); + window_picker.MoveToFront(WindowId(invisible_id)); +} + +} // namespace talk_base diff --git a/talk/base/window.h b/talk/base/window.h new file mode 100644 index 000000000..ad9467ad2 --- /dev/null +++ b/talk/base/window.h @@ -0,0 +1,141 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_WINDOW_H_ +#define TALK_BASE_WINDOW_H_ + +#include "talk/base/stringencode.h" + +// Define platform specific window types. +#if defined(LINUX) +typedef unsigned long Window; // Avoid include . +#elif defined(WIN32) +// We commonly include win32.h in talk/base so just include it here. +#include "talk/base/win32.h" // Include HWND, HMONITOR. +#elif defined(OSX) +typedef unsigned int CGWindowID; +typedef unsigned int CGDirectDisplayID; +#endif + +namespace talk_base { + +class WindowId { + public: + // Define WindowT for each platform. +#if defined(LINUX) + typedef Window WindowT; +#elif defined(WIN32) + typedef HWND WindowT; +#elif defined(OSX) + typedef CGWindowID WindowT; +#else + typedef unsigned int WindowT; +#endif + + static WindowId Cast(uint64 id) { +#if defined(WIN32) + return WindowId(reinterpret_cast(id)); +#else + return WindowId(static_cast(id)); +#endif + } + + static uint64 Format(const WindowT& id) { +#if defined(WIN32) + return static_cast(reinterpret_cast(id)); +#else + return static_cast(id); +#endif + } + + WindowId() : id_(0) {} + WindowId(const WindowT& id) : id_(id) {} // NOLINT + const WindowT& id() const { return id_; } + bool IsValid() const { return id_ != 0; } + bool Equals(const WindowId& other) const { + return id_ == other.id(); + } + + private: + WindowT id_; +}; + +class DesktopId { + public: + // Define DesktopT for each platform. +#if defined(LINUX) + typedef Window DesktopT; +#elif defined(WIN32) + typedef HMONITOR DesktopT; +#elif defined(OSX) + typedef CGDirectDisplayID DesktopT; +#else + typedef unsigned int DesktopT; +#endif + + static DesktopId Cast(int id, int index) { +#if defined(WIN32) + return DesktopId(reinterpret_cast(id), index); +#else + return DesktopId(static_cast(id), index); +#endif + } + + DesktopId() : id_(0), index_(-1) {} + DesktopId(const DesktopT& id, int index) // NOLINT + : id_(id), index_(index) { + } + const DesktopT& id() const { return id_; } + int index() const { return index_; } + bool IsValid() const { return index_ != -1; } + bool Equals(const DesktopId& other) const { + return id_ == other.id() && index_ == other.index(); + } + + private: + // Id is the platform specific desktop identifier. + DesktopT id_; + // Index is the desktop index as enumerated by each platform. + // Desktop capturer typically takes the index instead of id. + int index_; +}; + +// Window event types. +enum WindowEvent { + WE_RESIZE = 0, + WE_CLOSE = 1, + WE_MINIMIZE = 2, + WE_RESTORE = 3, +}; + +inline std::string ToString(const WindowId& window) { + return ToString(window.id()); +} + +} // namespace talk_base + +#endif // TALK_BASE_WINDOW_H_ diff --git a/talk/base/windowpicker.h b/talk/base/windowpicker.h new file mode 100644 index 000000000..e948d4c26 --- /dev/null +++ b/talk/base/windowpicker.h @@ -0,0 +1,78 @@ +// Copyright 2010 Google Inc. All Rights Reserved + +// thorcarpenter@google.com (Thor Carpenter) + +#ifndef TALK_BASE_WINDOWPICKER_H_ +#define TALK_BASE_WINDOWPICKER_H_ + +#include +#include + +#include "talk/base/window.h" + +namespace talk_base { + +class WindowDescription { + public: + WindowDescription() : id_() {} + WindowDescription(const WindowId& id, const std::string& title) + : id_(id), title_(title) { + } + const WindowId& id() const { return id_; } + void set_id(const WindowId& id) { id_ = id; } + const std::string& title() const { return title_; } + void set_title(const std::string& title) { title_ = title; } + + private: + WindowId id_; + std::string title_; +}; + +class DesktopDescription { + public: + DesktopDescription() : id_() {} + DesktopDescription(const DesktopId& id, const std::string& title) + : id_(id), title_(title), primary_(false) { + } + const DesktopId& id() const { return id_; } + void set_id(const DesktopId& id) { id_ = id; } + const std::string& title() const { return title_; } + void set_title(const std::string& title) { title_ = title; } + // Indicates whether it is the primary desktop in the system. + bool primary() const { return primary_; } + void set_primary(bool primary) { primary_ = primary; } + + private: + DesktopId id_; + std::string title_; + bool primary_; +}; + +typedef std::vector WindowDescriptionList; +typedef std::vector DesktopDescriptionList; + +class WindowPicker { + public: + virtual ~WindowPicker() {} + virtual bool Init() = 0; + + // TODO: Move this two methods to window.h when we no longer need to load + // CoreGraphics dynamically. + virtual bool IsVisible(const WindowId& id) = 0; + virtual bool MoveToFront(const WindowId& id) = 0; + + // Gets a list of window description and appends to descriptions. + // Returns true if successful. + virtual bool GetWindowList(WindowDescriptionList* descriptions) = 0; + // Gets a list of desktop descriptions and appends to descriptions. + // Returns true if successful. + virtual bool GetDesktopList(DesktopDescriptionList* descriptions) = 0; + // Gets the width and height of a desktop. + // Returns true if successful. + virtual bool GetDesktopDimensions(const DesktopId& id, int* width, + int* height) = 0; +}; + +} // namespace talk_base + +#endif // TALK_BASE_WINDOWPICKER_H_ diff --git a/talk/base/windowpicker_unittest.cc b/talk/base/windowpicker_unittest.cc new file mode 100644 index 000000000..e1a815d40 --- /dev/null +++ b/talk/base/windowpicker_unittest.cc @@ -0,0 +1,55 @@ +#include "talk/base/gunit.h" +#include "talk/base/window.h" +#include "talk/base/windowpicker.h" +#include "talk/base/windowpickerfactory.h" + +#ifdef OSX +# define DISABLE_ON_MAC(name) DISABLED_ ## name +#else +# define DISABLE_ON_MAC(name) name +#endif + +TEST(WindowPickerTest, GetWindowList) { + if (!talk_base::WindowPickerFactory::IsSupported()) { + LOG(LS_INFO) << "skipping test: window capturing is not supported with " + << "current configuration."; + } + talk_base::scoped_ptr picker( + talk_base::WindowPickerFactory::CreateWindowPicker()); + EXPECT_TRUE(picker->Init()); + talk_base::WindowDescriptionList descriptions; + EXPECT_TRUE(picker->GetWindowList(&descriptions)); +} + +// TODO(hughv) Investigate why this fails on pulse but not locally after +// upgrading to XCode 4.5. The failure is GetDesktopList returning FALSE. +TEST(WindowPickerTest, DISABLE_ON_MAC(GetDesktopList)) { + if (!talk_base::WindowPickerFactory::IsSupported()) { + LOG(LS_INFO) << "skipping test: window capturing is not supported with " + << "current configuration."; + } + talk_base::scoped_ptr picker( + talk_base::WindowPickerFactory::CreateWindowPicker()); + EXPECT_TRUE(picker->Init()); + talk_base::DesktopDescriptionList descriptions; + EXPECT_TRUE(picker->GetDesktopList(&descriptions)); + if (descriptions.size() > 0) { + int width = 0; + int height = 0; + EXPECT_TRUE(picker->GetDesktopDimensions(descriptions[0].id(), &width, + &height)); + EXPECT_GT(width, 0); + EXPECT_GT(height, 0); + + // Test |IsPrimaryDesktop|. Only one desktop should be a primary. + bool found_primary = false; + for (talk_base::DesktopDescriptionList::iterator it = descriptions.begin(); + it != descriptions.end(); ++it) { + if (it->primary()) { + EXPECT_FALSE(found_primary); + found_primary = true; + } + } + EXPECT_TRUE(found_primary); + } +} diff --git a/talk/base/windowpickerfactory.h b/talk/base/windowpickerfactory.h new file mode 100644 index 000000000..b55cc7dc3 --- /dev/null +++ b/talk/base/windowpickerfactory.h @@ -0,0 +1,76 @@ +/* + * libjingle + * Copyright 2010 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. + */ + +#ifndef TALK_BASE_WINDOWPICKERFACTORY_H_ +#define TALK_BASE_WINDOWPICKERFACTORY_H_ + +#if defined(WIN32) +#include "talk/base/win32windowpicker.h" +#elif defined(OSX) +#include "talk/base/macutils.h" +#include "talk/base/macwindowpicker.h" +#elif defined(LINUX) +#include "talk/base/linuxwindowpicker.h" +#endif + +#include "talk/base/windowpicker.h" + +namespace talk_base { + +class WindowPickerFactory { + public: + virtual ~WindowPickerFactory() {} + + // Instance method for dependency injection. + virtual WindowPicker* Create() { + return CreateWindowPicker(); + } + + static WindowPicker* CreateWindowPicker() { +#if defined(WIN32) + return new Win32WindowPicker(); +#elif defined(OSX) + return new MacWindowPicker(); +#elif defined(LINUX) + return new LinuxWindowPicker(); +#else + return NULL; +#endif + } + + static bool IsSupported() { +#ifdef OSX + return GetOSVersionName() >= kMacOSLeopard; +#else + return true; +#endif + } +}; + +} // namespace talk_base + +#endif // TALK_BASE_WINDOWPICKERFACTORY_H_ diff --git a/talk/base/winfirewall.cc b/talk/base/winfirewall.cc new file mode 100644 index 000000000..e87ee5a44 --- /dev/null +++ b/talk/base/winfirewall.cc @@ -0,0 +1,172 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/winfirewall.h" + +#include "talk/base/win32.h" + +#include +#include + +#define RELEASE(lpUnk) do { \ + if ((lpUnk) != NULL) { \ + (lpUnk)->Release(); \ + (lpUnk) = NULL; \ + } \ +} while (0) + +namespace talk_base { + +////////////////////////////////////////////////////////////////////// +// WinFirewall +////////////////////////////////////////////////////////////////////// + +WinFirewall::WinFirewall() : mgr_(NULL), policy_(NULL), profile_(NULL) { +} + +WinFirewall::~WinFirewall() { + Shutdown(); +} + +bool WinFirewall::Initialize(HRESULT* result) { + if (mgr_) { + if (result) { + *result = S_OK; + } + return true; + } + + HRESULT hr = CoCreateInstance(__uuidof(NetFwMgr), + 0, CLSCTX_INPROC_SERVER, + __uuidof(INetFwMgr), + reinterpret_cast(&mgr_)); + if (SUCCEEDED(hr) && (mgr_ != NULL)) + hr = mgr_->get_LocalPolicy(&policy_); + if (SUCCEEDED(hr) && (policy_ != NULL)) + hr = policy_->get_CurrentProfile(&profile_); + + if (result) + *result = hr; + return SUCCEEDED(hr) && (profile_ != NULL); +} + +void WinFirewall::Shutdown() { + RELEASE(profile_); + RELEASE(policy_); + RELEASE(mgr_); +} + +bool WinFirewall::Enabled() const { + if (!profile_) + return false; + + VARIANT_BOOL fwEnabled = VARIANT_FALSE; + profile_->get_FirewallEnabled(&fwEnabled); + return (fwEnabled != VARIANT_FALSE); +} + +bool WinFirewall::QueryAuthorized(const char* filename, bool* authorized) + const { + return QueryAuthorizedW(ToUtf16(filename).c_str(), authorized); +} + +bool WinFirewall::QueryAuthorizedW(const wchar_t* filename, bool* authorized) + const { + *authorized = false; + bool success = false; + + if (!profile_) + return false; + + _bstr_t bfilename = filename; + + INetFwAuthorizedApplications* apps = NULL; + HRESULT hr = profile_->get_AuthorizedApplications(&apps); + if (SUCCEEDED(hr) && (apps != NULL)) { + INetFwAuthorizedApplication* app = NULL; + hr = apps->Item(bfilename, &app); + if (SUCCEEDED(hr) && (app != NULL)) { + VARIANT_BOOL fwEnabled = VARIANT_FALSE; + hr = app->get_Enabled(&fwEnabled); + app->Release(); + + if (SUCCEEDED(hr)) { + success = true; + *authorized = (fwEnabled != VARIANT_FALSE); + } + } else if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) { + // No entry in list of authorized apps + success = true; + } else { + // Unexpected error + } + apps->Release(); + } + + return success; +} + +bool WinFirewall::AddApplication(const char* filename, + const char* friendly_name, + bool authorized, + HRESULT* result) { + return AddApplicationW(ToUtf16(filename).c_str(), + ToUtf16(friendly_name).c_str(), authorized, result); +} + +bool WinFirewall::AddApplicationW(const wchar_t* filename, + const wchar_t* friendly_name, + bool authorized, + HRESULT* result) { + INetFwAuthorizedApplications* apps = NULL; + HRESULT hr = profile_->get_AuthorizedApplications(&apps); + if (SUCCEEDED(hr) && (apps != NULL)) { + INetFwAuthorizedApplication* app = NULL; + hr = CoCreateInstance(__uuidof(NetFwAuthorizedApplication), + 0, CLSCTX_INPROC_SERVER, + __uuidof(INetFwAuthorizedApplication), + reinterpret_cast(&app)); + if (SUCCEEDED(hr) && (app != NULL)) { + _bstr_t bstr = filename; + hr = app->put_ProcessImageFileName(bstr); + bstr = friendly_name; + if (SUCCEEDED(hr)) + hr = app->put_Name(bstr); + if (SUCCEEDED(hr)) + hr = app->put_Enabled(authorized ? VARIANT_TRUE : VARIANT_FALSE); + if (SUCCEEDED(hr)) + hr = apps->Add(app); + app->Release(); + } + apps->Release(); + } + if (result) + *result = hr; + return SUCCEEDED(hr); +} + +} // namespace talk_base diff --git a/talk/base/winfirewall.h b/talk/base/winfirewall.h new file mode 100644 index 000000000..11d687e10 --- /dev/null +++ b/talk/base/winfirewall.h @@ -0,0 +1,73 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_WINFIREWALL_H_ +#define TALK_BASE_WINFIREWALL_H_ + +#ifndef _HRESULT_DEFINED +#define _HRESULT_DEFINED +typedef long HRESULT; // Can't forward declare typedef, but don't need all win +#endif // !_HRESULT_DEFINED + +struct INetFwMgr; +struct INetFwPolicy; +struct INetFwProfile; + +namespace talk_base { + +////////////////////////////////////////////////////////////////////// +// WinFirewall +////////////////////////////////////////////////////////////////////// + +class WinFirewall { + public: + WinFirewall(); + ~WinFirewall(); + + bool Initialize(HRESULT* result); + void Shutdown(); + + bool Enabled() const; + bool QueryAuthorized(const char* filename, bool* authorized) const; + bool QueryAuthorizedW(const wchar_t* filename, bool* authorized) const; + + bool AddApplication(const char* filename, const char* friendly_name, + bool authorized, HRESULT* result); + bool AddApplicationW(const wchar_t* filename, const wchar_t* friendly_name, + bool authorized, HRESULT* result); + + private: + INetFwMgr* mgr_; + INetFwPolicy* policy_; + INetFwProfile* profile_; +}; + +////////////////////////////////////////////////////////////////////// + +} // namespace talk_base + +#endif // TALK_BASE_WINFIREWALL_H_ diff --git a/talk/base/winfirewall_unittest.cc b/talk/base/winfirewall_unittest.cc new file mode 100644 index 000000000..9987716bd --- /dev/null +++ b/talk/base/winfirewall_unittest.cc @@ -0,0 +1,57 @@ +/* + * libjingle + * Copyright 2010, 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/base/gunit.h" +#include "talk/base/winfirewall.h" + +#include + +namespace talk_base { + +TEST(WinFirewallTest, ReadStatus) { + ::CoInitialize(NULL); + WinFirewall fw; + HRESULT hr; + bool authorized; + + EXPECT_FALSE(fw.QueryAuthorized("bogus.exe", &authorized)); + EXPECT_TRUE(fw.Initialize(&hr)); + EXPECT_EQ(S_OK, hr); + + EXPECT_TRUE(fw.QueryAuthorized("bogus.exe", &authorized)); + + // Unless we mock out INetFwMgr we can't really have an expectation either way + // about whether we're authorized. It will depend on the settings of the + // machine running the test. Same goes for AddApplication. + + fw.Shutdown(); + EXPECT_FALSE(fw.QueryAuthorized("bogus.exe", &authorized)); + + ::CoUninitialize(); +} + +} // namespace talk_base diff --git a/talk/base/winping.cc b/talk/base/winping.cc new file mode 100644 index 000000000..001740ad2 --- /dev/null +++ b/talk/base/winping.cc @@ -0,0 +1,376 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/winping.h" + +#include +#include + +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/ipaddress.h" +#include "talk/base/logging.h" +#include "talk/base/nethelpers.h" +#include "talk/base/socketaddress.h" + +namespace talk_base { + +////////////////////////////////////////////////////////////////////// +// Found in IPExport.h +////////////////////////////////////////////////////////////////////// + +typedef struct icmp_echo_reply { + ULONG Address; // Replying address + ULONG Status; // Reply IP_STATUS + ULONG RoundTripTime; // RTT in milliseconds + USHORT DataSize; // Reply data size in bytes + USHORT Reserved; // Reserved for system use + PVOID Data; // Pointer to the reply data + struct ip_option_information Options; // Reply options +} ICMP_ECHO_REPLY, * PICMP_ECHO_REPLY; + +typedef struct icmpv6_echo_reply_lh { + sockaddr_in6 Address; + ULONG Status; + unsigned int RoundTripTime; +} ICMPV6_ECHO_REPLY, *PICMPV6_ECHO_REPLY; + +// +// IP_STATUS codes returned from IP APIs +// + +#define IP_STATUS_BASE 11000 + +#define IP_SUCCESS 0 +#define IP_BUF_TOO_SMALL (IP_STATUS_BASE + 1) +#define IP_DEST_NET_UNREACHABLE (IP_STATUS_BASE + 2) +#define IP_DEST_HOST_UNREACHABLE (IP_STATUS_BASE + 3) +#define IP_DEST_PROT_UNREACHABLE (IP_STATUS_BASE + 4) +#define IP_DEST_PORT_UNREACHABLE (IP_STATUS_BASE + 5) +#define IP_NO_RESOURCES (IP_STATUS_BASE + 6) +#define IP_BAD_OPTION (IP_STATUS_BASE + 7) +#define IP_HW_ERROR (IP_STATUS_BASE + 8) +#define IP_PACKET_TOO_BIG (IP_STATUS_BASE + 9) +#define IP_REQ_TIMED_OUT (IP_STATUS_BASE + 10) +#define IP_BAD_REQ (IP_STATUS_BASE + 11) +#define IP_BAD_ROUTE (IP_STATUS_BASE + 12) +#define IP_TTL_EXPIRED_TRANSIT (IP_STATUS_BASE + 13) +#define IP_TTL_EXPIRED_REASSEM (IP_STATUS_BASE + 14) +#define IP_PARAM_PROBLEM (IP_STATUS_BASE + 15) +#define IP_SOURCE_QUENCH (IP_STATUS_BASE + 16) +#define IP_OPTION_TOO_BIG (IP_STATUS_BASE + 17) +#define IP_BAD_DESTINATION (IP_STATUS_BASE + 18) + +#define IP_ADDR_DELETED (IP_STATUS_BASE + 19) +#define IP_SPEC_MTU_CHANGE (IP_STATUS_BASE + 20) +#define IP_MTU_CHANGE (IP_STATUS_BASE + 21) +#define IP_UNLOAD (IP_STATUS_BASE + 22) +#define IP_ADDR_ADDED (IP_STATUS_BASE + 23) +#define IP_MEDIA_CONNECT (IP_STATUS_BASE + 24) +#define IP_MEDIA_DISCONNECT (IP_STATUS_BASE + 25) +#define IP_BIND_ADAPTER (IP_STATUS_BASE + 26) +#define IP_UNBIND_ADAPTER (IP_STATUS_BASE + 27) +#define IP_DEVICE_DOES_NOT_EXIST (IP_STATUS_BASE + 28) +#define IP_DUPLICATE_ADDRESS (IP_STATUS_BASE + 29) +#define IP_INTERFACE_METRIC_CHANGE (IP_STATUS_BASE + 30) +#define IP_RECONFIG_SECFLTR (IP_STATUS_BASE + 31) +#define IP_NEGOTIATING_IPSEC (IP_STATUS_BASE + 32) +#define IP_INTERFACE_WOL_CAPABILITY_CHANGE (IP_STATUS_BASE + 33) +#define IP_DUPLICATE_IPADD (IP_STATUS_BASE + 34) + +#define IP_GENERAL_FAILURE (IP_STATUS_BASE + 50) +#define MAX_IP_STATUS IP_GENERAL_FAILURE +#define IP_PENDING (IP_STATUS_BASE + 255) + +// +// Values used in the IP header Flags field. +// +#define IP_FLAG_DF 0x2 // Don't fragment this packet. + +// +// Supported IP Option Types. +// +// These types define the options which may be used in the OptionsData field +// of the ip_option_information structure. See RFC 791 for a complete +// description of each. +// +#define IP_OPT_EOL 0 // End of list option +#define IP_OPT_NOP 1 // No operation +#define IP_OPT_SECURITY 0x82 // Security option +#define IP_OPT_LSRR 0x83 // Loose source route +#define IP_OPT_SSRR 0x89 // Strict source route +#define IP_OPT_RR 0x7 // Record route +#define IP_OPT_TS 0x44 // Timestamp +#define IP_OPT_SID 0x88 // Stream ID (obsolete) +#define IP_OPT_ROUTER_ALERT 0x94 // Router Alert Option + +#define MAX_OPT_SIZE 40 // Maximum length of IP options in bytes + +////////////////////////////////////////////////////////////////////// +// Global Constants and Types +////////////////////////////////////////////////////////////////////// + +const char * const ICMP_DLL_NAME = "Iphlpapi.dll"; +const char * const ICMP_CREATE_FUNC = "IcmpCreateFile"; +const char * const ICMP_CLOSE_FUNC = "IcmpCloseHandle"; +const char * const ICMP_SEND_FUNC = "IcmpSendEcho"; +const char * const ICMP6_CREATE_FUNC = "Icmp6CreateFile"; +const char * const ICMP6_CLOSE_FUNC = "Icmp6CloseHandle"; +const char * const ICMP6_SEND_FUNC = "Icmp6SendEcho2"; + +inline uint32 ReplySize(uint32 data_size, int family) { + if (family == AF_INET) { + // A ping error message is 8 bytes long, so make sure we allow for at least + // 8 bytes of reply data. + return sizeof(ICMP_ECHO_REPLY) + talk_base::_max(8, data_size); + } else if (family == AF_INET6) { + // Per MSDN, Send6IcmpEcho2 needs at least one ICMPV6_ECHO_REPLY, + // 8 bytes for ICMP header, _and_ an IO_BLOCK_STATUS (2 pointers), + // in addition to the data size. + return sizeof(ICMPV6_ECHO_REPLY) + data_size + 8 + (2 * sizeof(DWORD*)); + } else { + return 0; + } +} + +////////////////////////////////////////////////////////////////////// +// WinPing +////////////////////////////////////////////////////////////////////// + +WinPing::WinPing() + : dll_(0), hping_(INVALID_HANDLE_VALUE), create_(0), close_(0), send_(0), + create6_(0), send6_(0), data_(0), dlen_(0), reply_(0), + rlen_(0), valid_(false) { + + dll_ = LoadLibraryA(ICMP_DLL_NAME); + if (!dll_) { + LOG(LERROR) << "LoadLibrary: " << GetLastError(); + return; + } + + create_ = (PIcmpCreateFile) GetProcAddress(dll_, ICMP_CREATE_FUNC); + close_ = (PIcmpCloseHandle) GetProcAddress(dll_, ICMP_CLOSE_FUNC); + send_ = (PIcmpSendEcho) GetProcAddress(dll_, ICMP_SEND_FUNC); + if (!create_ || !close_ || !send_) { + LOG(LERROR) << "GetProcAddress(ICMP_*): " << GetLastError(); + return; + } + hping_ = create_(); + if (hping_ == INVALID_HANDLE_VALUE) { + LOG(LERROR) << "IcmpCreateFile: " << GetLastError(); + return; + } + + if (HasIPv6Enabled()) { + create6_ = (PIcmp6CreateFile) GetProcAddress(dll_, ICMP6_CREATE_FUNC); + send6_ = (PIcmp6SendEcho2) GetProcAddress(dll_, ICMP6_SEND_FUNC); + if (!create6_ || !send6_) { + LOG(LERROR) << "GetProcAddress(ICMP6_*): " << GetLastError(); + return; + } + hping6_ = create6_(); + if (hping6_ == INVALID_HANDLE_VALUE) { + LOG(LERROR) << "Icmp6CreateFile: " << GetLastError(); + } + } + + dlen_ = 0; + rlen_ = ReplySize(dlen_, AF_INET); + data_ = new char[dlen_]; + reply_ = new char[rlen_]; + + valid_ = true; +} + +WinPing::~WinPing() { + if ((hping_ != INVALID_HANDLE_VALUE) && close_) { + if (!close_(hping_)) + LOG(WARNING) << "IcmpCloseHandle: " << GetLastError(); + } + if ((hping6_ != INVALID_HANDLE_VALUE) && close_) { + if (!close_(hping6_)) { + LOG(WARNING) << "Icmp6CloseHandle: " << GetLastError(); + } + } + + if (dll_) + FreeLibrary(dll_); + + delete[] data_; + delete[] reply_; +} + +WinPing::PingResult WinPing::Ping( + IPAddress ip, uint32 data_size, uint32 timeout, uint8 ttl, + bool allow_fragments) { + + if (data_size == 0 || timeout == 0 || ttl == 0) { + LOG(LERROR) << "IcmpSendEcho: data_size/timeout/ttl is 0."; + return PING_INVALID_PARAMS; + } + + assert(IsValid()); + + IP_OPTION_INFORMATION ipopt; + memset(&ipopt, 0, sizeof(ipopt)); + if (!allow_fragments) + ipopt.Flags |= IP_FLAG_DF; + ipopt.Ttl = ttl; + + uint32 reply_size = ReplySize(data_size, ip.family()); + + if (data_size > dlen_) { + delete [] data_; + dlen_ = data_size; + data_ = new char[dlen_]; + memset(data_, 'z', dlen_); + } + + if (reply_size > rlen_) { + delete [] reply_; + rlen_ = reply_size; + reply_ = new char[rlen_]; + } + DWORD result = 0; + if (ip.family() == AF_INET) { + result = send_(hping_, ip.ipv4_address().S_un.S_addr, + data_, uint16(data_size), &ipopt, + reply_, reply_size, timeout); + } else if (ip.family() == AF_INET6) { + sockaddr_in6 src = {0}; + sockaddr_in6 dst = {0}; + src.sin6_family = AF_INET6; + dst.sin6_family = AF_INET6; + dst.sin6_addr = ip.ipv6_address(); + result = send6_(hping6_, NULL, NULL, NULL, + &src, &dst, + data_, int16(data_size), &ipopt, + reply_, reply_size, timeout); + } + if (result == 0) { + DWORD error = GetLastError(); + if (error == IP_PACKET_TOO_BIG) + return PING_TOO_LARGE; + if (error == IP_REQ_TIMED_OUT) + return PING_TIMEOUT; + LOG(LERROR) << "IcmpSendEcho(" << ip.ToSensitiveString() + << ", " << data_size << "): " << error; + return PING_FAIL; + } + + return PING_SUCCESS; +} + +////////////////////////////////////////////////////////////////////// +// Microsoft Documenation +////////////////////////////////////////////////////////////////////// +// +// Routine Name: +// +// IcmpCreateFile +// +// Routine Description: +// +// Opens a handle on which ICMP Echo Requests can be issued. +// +// Arguments: +// +// None. +// +// Return Value: +// +// An open file handle or INVALID_HANDLE_VALUE. Extended error information +// is available by calling GetLastError(). +// +////////////////////////////////////////////////////////////////////// +// +// Routine Name: +// +// IcmpCloseHandle +// +// Routine Description: +// +// Closes a handle opened by ICMPOpenFile. +// +// Arguments: +// +// IcmpHandle - The handle to close. +// +// Return Value: +// +// TRUE if the handle was closed successfully, otherwise FALSE. Extended +// error information is available by calling GetLastError(). +// +////////////////////////////////////////////////////////////////////// +// +// Routine Name: +// +// IcmpSendEcho +// +// Routine Description: +// +// Sends an ICMP Echo request and returns any replies. The +// call returns when the timeout has expired or the reply buffer +// is filled. +// +// Arguments: +// +// IcmpHandle - An open handle returned by ICMPCreateFile. +// +// DestinationAddress - The destination of the echo request. +// +// RequestData - A buffer containing the data to send in the +// request. +// +// RequestSize - The number of bytes in the request data buffer. +// +// RequestOptions - Pointer to the IP header options for the request. +// May be NULL. +// +// ReplyBuffer - A buffer to hold any replies to the request. +// On return, the buffer will contain an array of +// ICMP_ECHO_REPLY structures followed by the +// options and data for the replies. The buffer +// should be large enough to hold at least one +// ICMP_ECHO_REPLY structure plus +// MAX(RequestSize, 8) bytes of data since an ICMP +// error message contains 8 bytes of data. +// +// ReplySize - The size in bytes of the reply buffer. +// +// Timeout - The time in milliseconds to wait for replies. +// +// Return Value: +// +// Returns the number of ICMP_ECHO_REPLY structures stored in ReplyBuffer. +// The status of each reply is contained in the structure. If the return +// value is zero, extended error information is available via +// GetLastError(). +// +////////////////////////////////////////////////////////////////////// + +} // namespace talk_base diff --git a/talk/base/winping.h b/talk/base/winping.h new file mode 100644 index 000000000..34b5bbda2 --- /dev/null +++ b/talk/base/winping.h @@ -0,0 +1,120 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_BASE_WINPING_H__ +#define TALK_BASE_WINPING_H__ + +#ifdef WIN32 + +#include "talk/base/win32.h" +#include "talk/base/basictypes.h" +#include "talk/base/IPAddress.h" + +namespace talk_base { + +// This class wraps a Win32 API for doing ICMP pinging. This API, unlike the +// the normal socket APIs (as implemented on Win9x), will return an error if +// an ICMP packet with the dont-fragment bit set is too large. This means this +// class can be used to detect the MTU to a given address. + +typedef struct ip_option_information { + UCHAR Ttl; // Time To Live + UCHAR Tos; // Type Of Service + UCHAR Flags; // IP header flags + UCHAR OptionsSize; // Size in bytes of options data + PUCHAR OptionsData; // Pointer to options data +} IP_OPTION_INFORMATION, * PIP_OPTION_INFORMATION; + +typedef HANDLE (WINAPI *PIcmpCreateFile)(); + +typedef BOOL (WINAPI *PIcmpCloseHandle)(HANDLE icmp_handle); + +typedef HANDLE (WINAPI *PIcmp6CreateFile)(); + +typedef BOOL (WINAPI *PIcmp6CloseHandle)(HANDLE icmp_handle); + +typedef DWORD (WINAPI *PIcmpSendEcho)( + HANDLE IcmpHandle, + ULONG DestinationAddress, + LPVOID RequestData, + WORD RequestSize, + PIP_OPTION_INFORMATION RequestOptions, + LPVOID ReplyBuffer, + DWORD ReplySize, + DWORD Timeout); + +typedef DWORD (WINAPI *PIcmp6SendEcho2)( + HANDLE IcmpHandle, + HANDLE Event, + FARPROC ApcRoutine, + PVOID ApcContext, + struct sockaddr_in6 *SourceAddress, + struct sockaddr_in6 *DestinationAddress, + LPVOID RequestData, + WORD RequestSize, + PIP_OPTION_INFORMATION RequestOptions, + LPVOID ReplyBuffer, + DWORD ReplySize, + DWORD Timeout +); + +class WinPing { +public: + WinPing(); + ~WinPing(); + + // Determines whether the class was initialized correctly. + bool IsValid() { return valid_; } + + // Attempts to send a ping with the given parameters. + enum PingResult { PING_FAIL, PING_INVALID_PARAMS, + PING_TOO_LARGE, PING_TIMEOUT, PING_SUCCESS }; + PingResult Ping( + IPAddress ip, uint32 data_size, uint32 timeout_millis, uint8 ttl, + bool allow_fragments); + +private: + HMODULE dll_; + HANDLE hping_; + HANDLE hping6_; + PIcmpCreateFile create_; + PIcmpCloseHandle close_; + PIcmpSendEcho send_; + PIcmp6CreateFile create6_; + PIcmp6SendEcho2 send6_; + char* data_; + uint32 dlen_; + char* reply_; + uint32 rlen_; + bool valid_; +}; + +} // namespace talk_base + +#endif // WIN32 + +#endif // TALK_BASE_WINPING_H__ diff --git a/talk/base/worker.cc b/talk/base/worker.cc new file mode 100644 index 000000000..28fcc9fa8 --- /dev/null +++ b/talk/base/worker.cc @@ -0,0 +1,92 @@ +/* + * libjingle + * Copyright 2004--2010, 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/base/worker.h" + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/thread.h" + +namespace talk_base { + +enum { + MSG_HAVEWORK = 0, +}; + +Worker::Worker() : worker_thread_(NULL) {} + +Worker::~Worker() { + // We need to already be stopped before being destroyed. We cannot call + // StopWork() from here because the subclass's data has already been + // destructed, so OnStop() cannot be called. + ASSERT(!worker_thread_); +} + +bool Worker::StartWork() { + talk_base::Thread *me = talk_base::Thread::Current(); + if (worker_thread_) { + if (worker_thread_ == me) { + // Already working on this thread, so nothing to do. + return true; + } else { + LOG(LS_ERROR) << "Automatically switching threads is not supported"; + ASSERT(false); + return false; + } + } + worker_thread_ = me; + OnStart(); + return true; +} + +bool Worker::StopWork() { + if (!worker_thread_) { + // Already not working, so nothing to do. + return true; + } else if (worker_thread_ != talk_base::Thread::Current()) { + LOG(LS_ERROR) << "Stopping from a different thread is not supported"; + ASSERT(false); + return false; + } + OnStop(); + worker_thread_->Clear(this, MSG_HAVEWORK); + worker_thread_ = NULL; + return true; +} + +void Worker::HaveWork() { + ASSERT(worker_thread_ != NULL); + worker_thread_->Post(this, MSG_HAVEWORK); +} + +void Worker::OnMessage(talk_base::Message *msg) { + ASSERT(msg->message_id == MSG_HAVEWORK); + ASSERT(worker_thread_ == talk_base::Thread::Current()); + OnHaveWork(); +} + +} // namespace talk_base diff --git a/talk/base/worker.h b/talk/base/worker.h new file mode 100644 index 000000000..582fe1b9f --- /dev/null +++ b/talk/base/worker.h @@ -0,0 +1,89 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_BASE_WORKER_H_ +#define TALK_BASE_WORKER_H_ + +#include "talk/base/constructormagic.h" +#include "talk/base/messagehandler.h" + +namespace talk_base { + +class Thread; + +// A worker is an object that performs some specific long-lived task in an +// event-driven manner. +// The only method that should be considered thread-safe is HaveWork(), which +// allows you to signal the availability of work from any thread. All other +// methods are thread-hostile. Specifically: +// StartWork()/StopWork() should not be called concurrently with themselves or +// each other, and it is an error to call them while the worker is running on +// a different thread. +// The destructor may not be called if the worker is currently running +// (regardless of the thread), but you can call StopWork() in a subclass's +// destructor. +class Worker : private MessageHandler { + public: + Worker(); + + // Destroys this Worker, but it must have already been stopped via StopWork(). + virtual ~Worker(); + + // Attaches the worker to the current thread and begins processing work if not + // already doing so. + bool StartWork(); + // Stops processing work if currently doing so and detaches from the current + // thread. + bool StopWork(); + + protected: + // Signal that work is available to be done. May only be called within the + // lifetime of a OnStart()/OnStop() pair. + void HaveWork(); + + // These must be implemented by a subclass. + // Called on the worker thread to start working. + virtual void OnStart() = 0; + // Called on the worker thread when work has been signalled via HaveWork(). + virtual void OnHaveWork() = 0; + // Called on the worker thread to stop working. Upon return, any pending + // OnHaveWork() calls are cancelled. + virtual void OnStop() = 0; + + private: + // Inherited from MessageHandler. + virtual void OnMessage(Message *msg); + + // The thread that is currently doing the work. + Thread *worker_thread_; + + DISALLOW_COPY_AND_ASSIGN(Worker); +}; + +} // namespace talk_base + +#endif // TALK_BASE_WORKER_H_ diff --git a/talk/build/build_jar.sh b/talk/build/build_jar.sh new file mode 100755 index 000000000..e2953aa08 --- /dev/null +++ b/talk/build/build_jar.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# libjingle +# Copyright 2013, 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. + +# javac & jar wrapper helping to simplify gyp action specification. + +set -e # Exit on any error. + +# Allow build-error parsers (such as emacs' compilation-mode) to find failing +# files easily. +echo "$0: Entering directory \``pwd`'" + +JAVA_HOME="$1"; shift +JAR_NAME="$1"; shift +TMP_DIR="$1"; shift +CLASSPATH="$1"; shift + +if [ -z "$1" ]; then + echo "Usage: $0 jar-name temp-work-dir source-path-dir .so-to-bundle " \ + "classpath path/to/Source1.java path/to/Source2.java ..." >&2 + exit 1 +fi + +rm -rf "$TMP_DIR" +mkdir -p "$TMP_DIR" + +$JAVA_HOME/bin/javac -Xlint:deprecation -Xlint:unchecked -d "$TMP_DIR" \ + -classpath "$CLASSPATH" "$@" +$JAVA_HOME/bin/jar cf "$JAR_NAME" -C "$TMP_DIR" . diff --git a/talk/build/common.gypi b/talk/build/common.gypi new file mode 100644 index 000000000..28481ebea --- /dev/null +++ b/talk/build/common.gypi @@ -0,0 +1,117 @@ +# +# libjingle +# Copyright 2012, 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. +# + +# This file contains common settings for building libjingle components. + +{ + 'variables': { + # TODO(ronghuawu): Chromium build will need a different libjingle_root. + 'libjingle_root%': '<(DEPTH)', + # TODO(ronghuawu): For now, disable the Chrome plugins, which causes a + # flood of chromium-style warnings. + 'clang_use_chrome_plugins%': 0, + 'libpeer_target_type%': 'static_library', + 'java_home%': ' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/talk/examples/android/README b/talk/examples/android/README new file mode 100644 index 000000000..feabadb97 --- /dev/null +++ b/talk/examples/android/README @@ -0,0 +1,43 @@ +This directory contains an example Android client for http://apprtc.appspot.com + +Prerequisites: +- Make sure gclient is checking out tools necessary to target Android: your + .gclient file should contain a line like: + target_os = ['android', 'unix'] + Make sure to re-run gclient sync after adding this to download the tools. +- Env vars need to be set up to target Android; easiest way to do this is to run + (from the libjingle trunk directory): + . ./build/android/envsetup.sh + Note that this clobbers any previously-set $GYP_DEFINES so it must be done + before the next item. +- Set up webrtc-related GYP variables: + export GYP_DEFINES="build_with_libjingle=1 build_with_chromium=0 libjingle_java=1 $GYP_DEFINES" + export JAVA_HOME= + export PATH=$JAVA_HOME/bin:$PATH + To cause WEBRTC_LOGGING to emit to Android's logcat, add enable_tracing=1 to + the $GYP_DEFINES above. +- When targeting both desktop & android, make sure to use a different output_dir + value in $GYP_GENERATOR_FLAGS or you'll likely end up with mismatched ARM & + x86 output artifacts. If you use an output_dir other than out/ make sure to + modify the command-lines below appropriately. +- Finally, run "gclient runhooks" to generate Android-targeting .ninja files. + +Example of building & using the app: + +cd /trunk +ninja -C out/Debug AppRTCDemo +adb install -r out/Debug/AppRTCDemo-debug.apk + +In desktop chrome, navigate to http://apprtc.appspot.com and note the r= room +this redirects to. Launch AppRTC on the device and enter the same into +the dialog box. + +Alternatively, replace the from the desktop chrome into the following +command: +adb shell am start -a android.intent.action.VIEW -d '"https://apprtc.appspot.com/?r="' +This should result in the app launching on Android and connecting to the apprtc +page displayed in the desktop browser. + +Yet another way to is to send the apprtc room URL to the Android device (e.g. using +https://chrome.google.com/webstore/detail/google-chrome-to-phone-ex/oadboiipflhobonjjffjbfekfjcgkhco) +and choose to open the URL with the AppRTCDemo app. diff --git a/talk/examples/android/ant.properties b/talk/examples/android/ant.properties new file mode 100644 index 000000000..b0971e891 --- /dev/null +++ b/talk/examples/android/ant.properties @@ -0,0 +1,17 @@ +# This file is used to override default values used by the Ant build system. +# +# This file must be checked into Version Control Systems, as it is +# integral to the build system of your project. + +# This file is only used by the Ant script. + +# You can use this to override default values such as +# 'source.dir' for the location of your java source folder and +# 'out.dir' for the location of your output folder. + +# You can also use it define how the release builds are signed by declaring +# the following properties: +# 'key.store' for the location of your keystore and +# 'key.alias' for the name of the key to use. +# The password will be asked during the build when you use the 'release' target. + diff --git a/talk/examples/android/assets/channel.html b/talk/examples/android/assets/channel.html new file mode 100644 index 000000000..86c2b4403 --- /dev/null +++ b/talk/examples/android/assets/channel.html @@ -0,0 +1,54 @@ + + + + + + + + + diff --git a/talk/examples/android/build.xml b/talk/examples/android/build.xml new file mode 100644 index 000000000..ae0679489 --- /dev/null +++ b/talk/examples/android/build.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/talk/examples/android/jni/Android.mk b/talk/examples/android/jni/Android.mk new file mode 100644 index 000000000..8e8016003 --- /dev/null +++ b/talk/examples/android/jni/Android.mk @@ -0,0 +1,2 @@ +# This space intentionally left blank (required for Android build system). + diff --git a/talk/examples/android/project.properties b/talk/examples/android/project.properties new file mode 100644 index 000000000..a3ee5ab64 --- /dev/null +++ b/talk/examples/android/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 diff --git a/talk/examples/android/res/drawable-hdpi/ic_launcher.png b/talk/examples/android/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..f3e9d12cc631e9c643b4ab43d649a43d2ef771d3 GIT binary patch literal 7503 zcmV-V9kAkwP)mK1swNRS%R+wSblod0)r!zFq}iux0;|9zg%Gdq)Pc|UW`dC$9zvH#mkqa?;m zHZx}N1!GOWWX$0^#$5L^=J73KUb`9dK7j2$#ys~h=Dr<&!`1=EZQ~hh5{%oj|KUrc zRK`pp$Yv*FF5ffem&{nV!;B3CL$N;s+tKxm!2Ym)@%wy;l3LG z&)HEhW{yENL!`7q#PmAC*hma~J|rb#yBquIKnYYJ1IPB_Z=11=fUqze>$jINAKZuC zG{!9dXCz5!u?Z=HRPsB-*ozqG+bN92r7`v;A}TqZv3%@bhKw8FZap`_75vXBa14Ln ziT_=RV^a}TgCV&6F2?L7>3?-Ujkht@0(Hb6V&1?2qri6<>Awg!SqaJviOFe2(Bhz>!W3P5dY2}!0>`u^q2NX1wx#L_?x8dGE~2AGj*jEpj% z#dRKt5I~PWkN>LzKZ0EBe}?;;g8S`*$8n4|)}_DJ<$zI68%Yk%`o?7Az{=~R<#D9^ z1nD-A%1cp&`#J|fUn7#HNGAa4<`mAD*nX-U%s;7=N2D8v>&3W#4IXD9`lfzp zJX=2Nsb+%d-0h&T$4S5d&ex}d{`Zp80HJ;clnVD2saHZRYLoOVM-iT!CC+3A$XS3h zn=JJ-sm7uz`~iXt>6UIxHL1yr!ejTAW;MmL*hETCO6p7SLI?aDC!ZI;F}0e9LY<5!eJjOSNa+toz{#kAsGJm2 zHY=q$3sMC$=aVt4#SceVvrAvFrnk2;>+4C(@^lh2mt_5ZPWmZK4MJuda%wW?h^O_u z)aP3)INqm4MD{E8QL14rm*fq1T9Q55DOxkC_58Fkp@Cx;EOqkx;xlurlPH*Fn9cO4 z3xqRENB$r|s6j#|PDHjjyO1YuoOtn-ChwOewtTJTpzPI}9Qh!v((R?X99jQbr7W;o zDeI`sbn>Dc>y~ty*%*Y;(?(T5f*D${7EM1)_h}KS?9(DLXSl2Mz@rAA z{1EC>i!y);kCBHK;#^a9r?9(u_PzI;f2Z5ywuL+1`ZHeR{h40lxrtM^UaimdTFm8o zPUBUcqx4FT7j!w!ZSUl;mYO}xDmeIwY8GltaF!*Tb=cJ|GBeOAHhZ*ljCzr4ta_7c ztm=DNoJJ{&%RMKHQy03$scy?-mBq59n!E1NIk$R`jJ-Re)2rq0JGj>^GBMXL#gku+ zL2L$+D0IAZus)i8i=bm52NZydc#J{_y6tFEUHY>1&Duqdm&HS^&I&o+=lDFo^Fo2| z89v`9Unua&aHJi$FyDfAe-To~4E zs1WAtCA{0jN}SfHvG`%5Mzjnhf^Wcna0DcSl*YeK^3RZtr2|#t#&jB71I3L^#0pUB z)0}!T4(P-J|7s!E_pXrZbBb4c?-A6k7V#?iFkOyK z+lqtCPLuGcCNo@PWijJWYX?QvV3+9Z4_#u_I}jcD@;J>cd7Qcmg7of7H3ASl;#6c{ zsfs+8=aSztCB5+WY8v@cD81db3r%yD(F{9#nrCN6kzgg*0=@%BKnh5+H_dExFYy{b67nScB}6gLUoYnQ)`_`(CD{HT z=KG}S^Sl!HJdX)nrc+?mE*mElar1}HQWmq1xZ5o}{-X1eY=t~VxnCZux(qoL5W=~~ zsRj2qm58m8Scrk3wky=sbxl6~BO#y0L~N(&lNM6MkT)qds2{}!22xyL4_XG+g3rM| zkOs1Ws%M}9^<$6=PJt`nZm*tH3F-iceXeH@;)a4n-H6*3K>Cb!5Y&cvHRRk!<0lHa zk|aN&9xW&l@_bJUd9AnbYPYwz43~h)R9lCnpVg#BEF1+{aiFO~L|T8Bn4Gz?81-&> ztojGHI88ZZaFQ4)T98KMR*wg0|FMP!Bo@%vug}q({g-Lgjx758(_OS<#cJBKcroo! zETp~ld=HX=3Y-EL7cHb8A`}L$JO+i}W~72jK}}=?>3|;aIL=2bB>l&8NWWt;=~IW3 zUKNDs=>$P-9tbi>@_zs%0iCGvD;0CS&k7o^tvZ!Qm@dPqqtp^e{ZoD5py``^q?v8h zaX+W%tVx)ewxi~pfuJgg`H!6Yg5>pre3q-|g@gi{{Otu=l~F{yPv50ur-~>q?;M@U zQquXfG&-M>Ocxq(5nKZ|L2+sdm82OdeGJM$HP8V*J&gn)N?Ys;Y01QA9U%VHcH+;+ z5`Sha2?hNjqCLd;|FR&7Tt5-%rss2gYEZoM^y=21=rU#hqGMJrH!!;yi+1Q{i{|r_ zJzOI)UvrIC#>--rd2X?)d+tls{Qb0QC`cwn&r&6Iiq}wR(mC3qxb*@Flvf}ooJ6yXL)f?Sv6p!O&! zXng_A+;^D{ox4YcCAFl}3&aZ|iK0k<%0(1NKTkkDSLKlS+k z3cj_7q#|D9wN0mLJzAIT(&lawvtiB9Av;DLAB5>V(KSY;Mqw^>i&N`}u0fkl zP}GsDly$X??o?_?F9;-vf2JTjw!o24TZkw+M8cK1B%FK^f}VdYLB>?`t<>jvXX-Sb zv3iv}sOlJVWX_7@Zq7?GLuGNQeexJ-)>79%kp9;M`7Tq@Ynx9|TuLFSiYn+{HD{3X zXJ14qinKRKC_`PkwvdEVFGEnL$BQ7O8;Z78ukpFasl30@sonoom)@*3bB;w5>1ayWIXNxB?)2j&u)Iof53DpDSomxpQnuKPox=WiiMD%KjLncDXf zr}Un!SGMlMoFlW|l11lyB#X^E;}(}&@^=;_&1gtP9R%Iht9+ugSza$Qm&M1YqCkIw zMd$^jSs6s|e_uiVs6+k&pX*<$SNp`(WVar|T%xn4VwJuctMmUa$k-Q&V!m$~1TCw{ z@*2vVqq8Qv#-c%t)0}sMAostqAVcGr=ZnVCr&Lh;EakIWz0Mq?vqPL?RI##H%_&Ur z_uT%*HORQ2_0tJz%w%ex#rkZoSD1Zl&TB2>)bm~A)M`Z0ZMUUZoGw+1f71vO^cvq9 zPUUwMg?gS|S|5XL?5X6-oBko!Rai<0mm)n{2Skd$9N69h1 zMkM6pY}1yU)*-8h*=F zH1xC6v@ErVG+1hsJVQ$jJp|Q4P&EWqK~OCO=^*F6q=$;i_2v0I-zu!fl<0w0)@6GR zs6J}llDUNMv9kX#yG_d|^?2uK)n~4;>a%V!>N1GIY^Ih{?ID6rK~Ui{C#V*J?n2NN z2s#fz*C41EQfeDWdWfJ}UgLLzQ~T_!%kq4yIz!g6130oo%1hQv}z^lP=-7%_m(_HT_cC22ZD0QB{GA2V$x|yLJq|pI!EfOB@m>2 zW&{=MZc%PUIwclG((E(;Bt>2q+5jm^a0Ao=?x%uKnEk4Gjn64g>9eISyH!Y8s&lL2 z-43>j2JgZ)lHMq6p*YsTIXW{8skYH2Mw9Crtu{zrjz2wC>nHr2n{UgMq%oqwi&H`4eU;Dk+Qnsiy;qwd?=HQW_~13^hL^cY`(}|D0Z!5CNs=VgqbwBdB1BOo z7PLB-h*aKT=C@+2ncKxz;=ZNmomr&1_#*^WKU0FTAn3yzQPlV15b{6OjXLK)N2BxF zil5}R6*X#q;kL?;*Q$NBLauiyxFD!Kck6RJ=jt=ud)FMaa!GpVJFUTldX#0?RB^nA z!(!zG=NQcz$T{l1B>S>kWLk-Q{yv@4ds~FolM}>2QQIgwDGh>(o+&|hASefd7Tk=Y zUYCZD>})r3IN6Tc7qk;Ya-S2osM-j5sx~#{D&O06YM%lK`bMwvoTJTbJ+S7WgUh8U zkNU){RJ5ml+uVF^s=s~IvDe+A(jvVgj_&uF|LuA2saq=DC&ugi!dHm@T(p^Dl9EYv zq43!hgb3<|2$G-aM$I6|?nFDWb8b6vqVhTYXW9ODE-Ag$_f%dRxZGCZoYJLB)d?FX z>Fwvo{s1_8C)K9OoP*wh^S>DS+T7L4hE7|O`uf`o?hFi@S@%l#9C1=aJgrYUKqoF< zq0-9AXD7uli)EFk)BK_+>UrihlI8axYjp>*Qg;wtvN{Ug(>m6@o!b7?_GG`$vr@b! z-AZ%odg%ajB;(u8jh^U346XVpEaJxv?fAvJgWugSX-Lwl2}24Og}z=rKYWb%QPe{E zc1IFjyl{akDk=<54xLCnf1aRQbvNmFNjlBFw3xbUUMH8#o@ANci7Zk(iPncZ3wHav z)U??haBT3-j*(+_wGUcxu&G^w<|#k;DSf2(#&E|@KZd%_JRjzzKk0~@Ye=$9O^bNA#8$>3%x`Q5~$v1(5 z$@X4#mY?@j+I`w%xkExgf2Oc&%AV#!RfIIV-GN!x4KwYpAK7GuFvdJn2r)0!hgj;g z6P?8B`Ms%T>ulmqBokkJokXq<&yWLpoJ!#VPbf|5h6TY0Z-9R+^n#x1I4xZ&y+S)K z?xXQ(3i8_#Og39zAhxMDv5kE|5V6GmdX^Y;fvxHDCHuI?FgC->iG@E!1W^DpqkpiL z{BV<@`Y{$;g^(tv_z;U~G1QXNk83V+)BK2wA4U4qEhJo4lURC_sPZ0BO$Cv*mWb0q z(33}%lqS-Eb-a#hbTw3^tENg_)kCS!RvIX+Eu$NicPPC`NwMlg8ocj)l5hPdStLSG z0^}t0CAI;AHumRPLeMR?zE?6^(_>ZRNlqld^kq-aGg z#FX8n^Eb~?#t)ga^UOZldU7XiIFa;F))#zDYYV=jW$Mi|Cu1f3`}^6{e)}kL{B$sx zCO}R?zn`)pNW@ml*7d#2*7S;GA9e15C~EPG=|b>c=@PUl0d)A=x)+rl_o zUKj^4VYVcMg7NkwOm-(>_VXk}4Ip9Fa1yr9BQbXm(XC5Jwd#ip;!!scF?HG+DyuG~ zA4`hp6Mcz0tn}!{jMT7QCrCvKHkpGwe zrski%MCKb_XdtB_20?m6QxOEkvG~q?*m7S77W{;ttu7wIY%7Azx@kvS&gREiW$@#F zCdhydMAb9C1qoAqN%&|eQf?E`k7s@=s3DLea*$J7NhM{s>Eg|^bo^pEZBuTg$bAYL zwP^(PjQL&sz z&2F%Dy_d82z@VoRM8V9aCfF>18*4d3m|%14CxY%Hi3zbHVXUR``i%A@tPLeG`x~O$ zKNyR(P+xaqJbavCBF!Na+)inW-}ehYMzYD7W+}-6`ctKF%J5_2HSw2L`~4I z3rgtA;r^~7f)=yY-2$1St=*H?pkQXHeZ7enyu?|hOO>3k1g-giOu$G_QYRPptj7s_7x`AjWHjN zSuBYUvpkPktQJyuApR%CLamL_^Nx&!gz+RE{{j(m+Sn6GlI|9fh*T0!93mm*QxcLE zlCXL#aSMCX+3CKtX&KewN0|;pqqr7=)DTpT!mLBO35F<=N4GdoM$+Y%}fWfp52m~Eb=6iWgV$eeD~i+P$G)6#Hal@NV>w-_uj?U1dd>zbaG`= zp5jZv(gzr&DB^~jbxH$2l!UD}}pg64zE_pL7=u_2)N7k2G`jl~PNG`cs19yqm2?dVSQbBji~A zrmsm=4rTU;qVAm34~?}tq7SjYh5@1$+3-d|s6+3_NtoA_L@9+Z%^OlF0yElFKN8+@ zL66kDzGt$2APD;$1XYb|DjuKILR>!CP8dAhfonBi&RH+^;TtdSC?Ewy4CZ%;Aj9$k zYp_0$)8`~+#bsDEpX3pAC=0o`q%hi0SRW>5%F6vR8AVrb1 zE(=H6yzfoI^tPZ43GYJK1UqB5Efw;x``9)_l;FnM3RfpI<@UVQqBi^kr?L(owkmBF z*S@;(s;*iLM4E9#WAE~mk&|ec*7b-UDYeeAbv={Vy1;SllMbz!!o})0e`U;2i?DFj zy}>$gBh5nfAr>1UDi72C9Z1p|B58bcNOCkX5sYj0(=J=OKF;<*gNgbjqK;9RHD*L> zu40@``H67b>oL=u^7}`4X&hE`x@5AtM=@L9M{5{df|MRX;sYKb4#=HwH6@@?aLA9n)kA>e1&ys8yaAW__Cvv#y2!4g(bo zgjypqkVfNvE~O#1_+K=gh(a;Us+JNibIKeSW~Kbr{jJh^@uiD-JVP!0-I^@dU=qPQS$LYDby% z;6lv8A?Z^w#3~yDEQF*AV-tyJGOqT2T~L^<9=EB%Omk9+C)&I* zJi_zUX7K@UV=!wl(_})(6%4o>+d2$XFh%@FfgxD$)bd z)O#=6&?g%MJ_8w-A)*M^Z(@53`!~?vT*Fq9auR~F*}A?v46AjiAg@8qQ6O@rc>mx0 zN{rNHmvm=M>t1KAYDcg@-AMCb?P$wsy3v*~+*peZe2Dc|h>|_n9^yi*4soFt`w=la zg;0wv5c9Dx)?%J+gjtw=r0D={F!PgI3EZdIPrech-|KXwsw?)MHsQ!)wPYx^zQl{TgJAXKQ6;(4P{QH!ix2Q( z>(KluFk`~y<2Q9X?fzjlBpC?S0qz;LYo@4un7H{MoyA4@_KaG(3fe&=6U Z_26(X?UGb~rB<=NU%kslvv7U z0MB1d=05;KsmK~CeqIkCQeo@@Mp)>*n<=e?Uqy;~B%BH?A(=Xou~9*O4b%kAXg8p@x5hdMXGJT+n&mpK_#^dBd#TVcrW3gL~yxB>62iJn_sQ7Nu?S z@+&5OYejAS04p5?%3Mj2h_ZWLkAM&uhB5+8DIZ)NAhX?Q&xZkU7 zU146i(r@vhaRJK=njnp^Ekq-H9-=Xs2EY7%)y&A@7gxHt*=dczokZCW2sxft5?{H1 z`1ONsXKm|(_HjehZmCMOgIB3^Oo;ruO$bhDIRyT>xp($gYHkmhzc(fzPnZ*|G3^i2 znwlb(SJ}f0tE7lRGaveoQa*Y3(|lfUf4;=q8#fWViO&eV&)w`N%*5GVKKQL~FK&CP zFO%MnWU>&;X1KQEB8`j3AkOCNh@*p%qi<1u*|qHR3lU? zwL)u{*1*CGOgv_}AdfDt#+m=D#J3h|aZ^?nZplc;kBJK63*yVn3_NxRCkTr=1KZS@ zXwRCA_GM{k-iBz2XWqbiv>d^#RfggE=lH>X_HcgdlQP_1y7 zB-+EYMix;Z$l+Qej@DJu+TaY1>9Hz_s7%aH{FI*1^d zep&-&Z>>f1`KxG`IC3uI&pKooW!8?oX+QGXNhC7RkrEMq6K0CJBAKGzZaEMT10nf_ z1(Z%jm{w@NRVb>!h_kn!#M(=($T+9-c1K7=-cpVH#7iVJ_%0zDz7EkGQ_*1v`8@>Z z8&@ke=C2|OOyaFVszM{q+gXPv8rqQmT$xTME+KC_io9+*2@SnN#wo;zGorEo6h}hE zO0D@+xK@zvmymoUmb0qZ22qHb3qcy=TcKLhiSYZGP*p6a2EQO0RkI){&p3}3KOw9@ zu-&(W%yAMo$JE*lQ4_&`aplv&dB&|ujnEuPYmWn`5HI!Z#{3h^ik@ySI# z;rWMI|0SQvFMnv3Yrkpgwva?Z$J=T!=kyGmUo{N%B-3h+<&r5@VkT7+6I5GT4g`-v zedj)0t@rHpy6Lf?eADXCJj1cj+;VI9-#%b7HhwDC)?0oNp<~t>RG*!R1CEZ6)l~!J z3S*qzYK}fHn)-bpRfIirxdeP}zrd7gK4dvi5nX(_G9>x3-YIkUK51S;g4Z7PsI*<#j~cd2 zeX8Tr4f(R=+$m~G)-55CgpM>;<9O37^eao0d+r*+{Yyul9`WI@X=%DXzFqE!nxdh% zINo(OOYd1HW&4V@!sli2i+w1!HX^oL>CB>I4m&Rg*RKllO+-n=cDoJR?AF_8?P$XW zYa?zyRffqsUqX*fsR+f76DbX_E@270797x}5dINDrAa*sc!p;KS9>!)Cq`!P%s{?> z3(6-Cqik)aIn1sL5uC1Z4iT%Z4eJ~0@$fkTKRH#7J8q%$)Mz5oQ+-@bJ}&yBP~$$xa1rbC%1_Z$&O^Mf$p(tSUYky4%Z8*{ zSeLYuWadEbfN1!sx73uB>(x~7vyOuy?c?1i+9$i`Na-G{Xq{Ta)7=-di9NH#3Ghhk zb974#H@<1Ee{WbhXb`L!mF7%geZt$YA#oiPB`$^4k4}YELla^7;6TXj<}V>%hk;k? zlhC*EIZ!&lXCdHnD)?YmKe4F$dBVytf5>|s}}Kj|?h+O?Sf Q0000007*qoM6N<$f{m2HR{#J2 literal 0 HcmV?d00001 diff --git a/talk/examples/android/res/drawable-mdpi/ic_launcher.png b/talk/examples/android/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..9709a1e928ec9e5f8007b97b8dad2a7196bf6f29 GIT binary patch literal 3835 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 literal 0 HcmV?d00001 diff --git a/talk/examples/android/res/drawable-xhdpi/ic_launcher.png b/talk/examples/android/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db2c4f6900b919017218a23d000e47ef8727dd03 GIT binary patch literal 12056 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 + + AppRTC + diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java new file mode 100644 index 000000000..fe4156437 --- /dev/null +++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java @@ -0,0 +1,432 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.appspot.apprtc; + +import android.app.Activity; +import android.os.AsyncTask; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.webrtc.MediaConstraints; +import org.webrtc.PeerConnection; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.LinkedList; +import java.util.List; +import java.util.Scanner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Negotiates signaling for chatting with apprtc.appspot.com "rooms". + * Uses the client<->server specifics of the apprtc AppEngine webapp. + * + * To use: create an instance of this object (registering a message handler) and + * call connectToRoom(). Once that's done call sendMessage() and wait for the + * registered handler to be called with received messages. + */ +public class AppRTCClient { + private static final String TAG = "AppRTCClient"; + private GAEChannelClient channelClient; + private final Activity activity; + private final GAEChannelClient.MessageHandler gaeHandler; + private final IceServersObserver iceServersObserver; + + // These members are only read/written under sendQueue's lock. + private LinkedList sendQueue = new LinkedList(); + private AppRTCSignalingParameters appRTCSignalingParameters; + + /** + * Callback fired once the room's signaling parameters specify the set of + * ICE servers to use. + */ + public static interface IceServersObserver { + public void onIceServers(List iceServers); + } + + public AppRTCClient( + Activity activity, GAEChannelClient.MessageHandler gaeHandler, + IceServersObserver iceServersObserver) { + this.activity = activity; + this.gaeHandler = gaeHandler; + this.iceServersObserver = iceServersObserver; + } + + /** + * Asynchronously connect to an AppRTC room URL, e.g. + * https://apprtc.appspot.com/?r=NNN and register message-handling callbacks + * on its GAE Channel. + */ + public void connectToRoom(String url) { + while (url.indexOf('?') < 0) { + // Keep redirecting until we get a room number. + (new RedirectResolver()).execute(url); + return; // RedirectResolver above calls us back with the next URL. + } + (new RoomParameterGetter()).execute(url); + } + + /** + * Disconnect from the GAE Channel. + */ + public void disconnect() { + if (channelClient != null) { + channelClient.close(); + channelClient = null; + } + } + + /** + * Queue a message for sending to the room's channel and send it if already + * connected (other wise queued messages are drained when the channel is + eventually established). + */ + public synchronized void sendMessage(String msg) { + synchronized (sendQueue) { + sendQueue.add(msg); + } + requestQueueDrainInBackground(); + } + + public boolean isInitiator() { + return appRTCSignalingParameters.initiator; + } + + public MediaConstraints pcConstraints() { + return appRTCSignalingParameters.pcConstraints; + } + + public MediaConstraints videoConstraints() { + return appRTCSignalingParameters.videoConstraints; + } + + // Struct holding the signaling parameters of an AppRTC room. + private class AppRTCSignalingParameters { + public final List iceServers; + public final String gaeBaseHref; + public final String channelToken; + public final String postMessageUrl; + public final boolean initiator; + public final MediaConstraints pcConstraints; + public final MediaConstraints videoConstraints; + + public AppRTCSignalingParameters( + List iceServers, + String gaeBaseHref, String channelToken, String postMessageUrl, + boolean initiator, MediaConstraints pcConstraints, + MediaConstraints videoConstraints) { + this.iceServers = iceServers; + this.gaeBaseHref = gaeBaseHref; + this.channelToken = channelToken; + this.postMessageUrl = postMessageUrl; + this.initiator = initiator; + this.pcConstraints = pcConstraints; + this.videoConstraints = videoConstraints; + } + } + + // Load the given URL and return the value of the Location header of the + // resulting 302 response. If the result is not a 302, throws. + private class RedirectResolver extends AsyncTask { + @Override + protected String doInBackground(String... urls) { + if (urls.length != 1) { + throw new RuntimeException("Must be called with a single URL"); + } + try { + return followRedirect(urls[0]); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected void onPostExecute(String url) { + connectToRoom(url); + } + + private String followRedirect(String url) throws IOException { + HttpURLConnection connection = (HttpURLConnection) + new URL(url).openConnection(); + connection.setInstanceFollowRedirects(false); + int code = connection.getResponseCode(); + if (code != HttpURLConnection.HTTP_MOVED_TEMP) { + throw new IOException("Unexpected response: " + code + " for " + url + + ", with contents: " + drainStream(connection.getInputStream())); + } + int n = 0; + String name, value; + while ((name = connection.getHeaderFieldKey(n)) != null) { + value = connection.getHeaderField(n); + if (name.equals("Location")) { + return value; + } + ++n; + } + throw new IOException("Didn't find Location header!"); + } + } + + // AsyncTask that converts an AppRTC room URL into the set of signaling + // parameters to use with that room. + private class RoomParameterGetter + extends AsyncTask { + @Override + protected AppRTCSignalingParameters doInBackground(String... urls) { + if (urls.length != 1) { + throw new RuntimeException("Must be called with a single URL"); + } + try { + return getParametersForRoomUrl(urls[0]); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected void onPostExecute(AppRTCSignalingParameters params) { + channelClient = + new GAEChannelClient(activity, params.channelToken, gaeHandler); + synchronized (sendQueue) { + appRTCSignalingParameters = params; + } + requestQueueDrainInBackground(); + iceServersObserver.onIceServers(appRTCSignalingParameters.iceServers); + } + + // Fetches |url| and fishes the signaling parameters out of the HTML via + // regular expressions. + // + // TODO(fischman): replace this hackery with a dedicated JSON-serving URL in + // apprtc so that this isn't necessary (here and in other future apps that + // want to interop with apprtc). + private AppRTCSignalingParameters getParametersForRoomUrl(String url) + throws IOException { + final Pattern fullRoomPattern = Pattern.compile( + ".*\n *Sorry, this room is full\\..*"); + + String roomHtml = + drainStream((new URL(url)).openConnection().getInputStream()); + + Matcher fullRoomMatcher = fullRoomPattern.matcher(roomHtml); + if (fullRoomMatcher.find()) { + throw new IOException("Room is full!"); + } + + String gaeBaseHref = url.substring(0, url.indexOf('?')); + String token = getVarValue(roomHtml, "channelToken", true); + String postMessageUrl = "/message?r=" + + getVarValue(roomHtml, "roomKey", true) + "&u=" + + getVarValue(roomHtml, "me", true); + boolean initiator = getVarValue(roomHtml, "initiator", false).equals("1"); + LinkedList iceServers = + iceServersFromPCConfigJSON(getVarValue(roomHtml, "pcConfig", false)); + + boolean isTurnPresent = false; + for (PeerConnection.IceServer server : iceServers) { + if (server.uri.startsWith("turn:")) { + isTurnPresent = true; + break; + } + } + if (!isTurnPresent) { + iceServers.add( + requestTurnServer(getVarValue(roomHtml, "turnUrl", true))); + } + + MediaConstraints pcConstraints = constraintsFromJSON( + getVarValue(roomHtml, "pcConstraints", false)); + Log.d(TAG, "pcConstraints: " + pcConstraints); + + MediaConstraints videoConstraints = constraintsFromJSON( + getVideoConstraints( + getVarValue(roomHtml, "mediaConstraints", false))); + Log.d(TAG, "videoConstraints: " + videoConstraints); + + return new AppRTCSignalingParameters( + iceServers, gaeBaseHref, token, postMessageUrl, initiator, + pcConstraints, videoConstraints); + } + + private String getVideoConstraints(String mediaConstraintsString) { + try { + JSONObject json = new JSONObject(mediaConstraintsString); + JSONObject videoJson = json.optJSONObject("video"); + if (videoJson == null) { + return ""; + } + return videoJson.toString(); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + private MediaConstraints constraintsFromJSON(String jsonString) { + try { + MediaConstraints constraints = new MediaConstraints(); + JSONObject json = new JSONObject(jsonString); + JSONObject mandatoryJSON = json.optJSONObject("mandatory"); + if (mandatoryJSON != null) { + JSONArray mandatoryKeys = mandatoryJSON.names(); + if (mandatoryKeys != null) { + for (int i = 0; i < mandatoryKeys.length(); ++i) { + String key = (String) mandatoryKeys.getString(i); + String value = mandatoryJSON.getString(key); + constraints.mandatory.add( + new MediaConstraints.KeyValuePair(key, value)); + } + } + } + JSONArray optionalJSON = json.optJSONArray("optional"); + if (optionalJSON != null) { + for (int i = 0; i < optionalJSON.length(); ++i) { + JSONObject keyValueDict = optionalJSON.getJSONObject(i); + String key = keyValueDict.names().getString(0); + String value = keyValueDict.getString(key); + constraints.optional.add( + new MediaConstraints.KeyValuePair(key, value)); + } + } + return constraints; + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + // Scan |roomHtml| for declaration & assignment of |varName| and return its + // value, optionally stripping outside quotes if |stripQuotes| requests it. + private String getVarValue( + String roomHtml, String varName, boolean stripQuotes) + throws IOException { + final Pattern pattern = Pattern.compile( + ".*\n *var " + varName + " = ([^\n]*);\n.*"); + Matcher matcher = pattern.matcher(roomHtml); + if (!matcher.find()) { + throw new IOException("Missing " + varName + " in HTML: " + roomHtml); + } + String varValue = matcher.group(1); + if (matcher.find()) { + throw new IOException("Too many " + varName + " in HTML: " + roomHtml); + } + if (stripQuotes) { + varValue = varValue.substring(1, varValue.length() - 1); + } + return varValue; + } + + // Requests & returns a TURN ICE Server based on a request URL. Must be run + // off the main thread! + private PeerConnection.IceServer requestTurnServer(String url) { + try { + URLConnection connection = (new URL(url)).openConnection(); + connection.addRequestProperty("user-agent", "Mozilla/5.0"); + connection.addRequestProperty("origin", "https://apprtc.appspot.com"); + String response = drainStream(connection.getInputStream()); + JSONObject responseJSON = new JSONObject(response); + String uri = responseJSON.getJSONArray("uris").getString(0); + String username = responseJSON.getString("username"); + String password = responseJSON.getString("password"); + return new PeerConnection.IceServer(uri, username, password); + } catch (JSONException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + // Return the list of ICE servers described by a WebRTCPeerConnection + // configuration string. + private LinkedList iceServersFromPCConfigJSON( + String pcConfig) { + try { + JSONObject json = new JSONObject(pcConfig); + JSONArray servers = json.getJSONArray("iceServers"); + LinkedList ret = + new LinkedList(); + for (int i = 0; i < servers.length(); ++i) { + JSONObject server = servers.getJSONObject(i); + String url = server.getString("url"); + String credential = + server.has("credential") ? server.getString("credential") : ""; + ret.add(new PeerConnection.IceServer(url, "", credential)); + } + return ret; + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + // Request an attempt to drain the send queue, on a background thread. + private void requestQueueDrainInBackground() { + (new AsyncTask() { + public Void doInBackground(Void... unused) { + maybeDrainQueue(); + return null; + } + }).execute(); + } + + // Send all queued messages if connected to the room. + private void maybeDrainQueue() { + synchronized (sendQueue) { + if (appRTCSignalingParameters == null) { + return; + } + try { + for (String msg : sendQueue) { + URLConnection connection = new URL( + appRTCSignalingParameters.gaeBaseHref + + appRTCSignalingParameters.postMessageUrl).openConnection(); + connection.setDoOutput(true); + connection.getOutputStream().write(msg.getBytes("UTF-8")); + if (!connection.getHeaderField(null).startsWith("HTTP/1.1 200 ")) { + throw new IOException( + "Non-200 response to POST: " + connection.getHeaderField(null) + + " for msg: " + msg); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + sendQueue.clear(); + } + } + + // Return the contents of an InputStream as a String. + private static String drainStream(InputStream in) { + Scanner s = new Scanner(in).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } +} diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java new file mode 100644 index 000000000..bd17323e1 --- /dev/null +++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java @@ -0,0 +1,499 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.appspot.apprtc; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Point; +import android.media.AudioManager; +import android.os.Bundle; +import android.os.PowerManager; +import android.util.Log; +import android.webkit.JavascriptInterface; +import android.widget.EditText; +import android.widget.Toast; + +import org.json.JSONException; +import org.json.JSONObject; +import org.webrtc.IceCandidate; +import org.webrtc.MediaConstraints; +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.SdpObserver; +import org.webrtc.SessionDescription; +import org.webrtc.StatsObserver; +import org.webrtc.StatsReport; +import org.webrtc.VideoCapturer; +import org.webrtc.VideoRenderer; +import org.webrtc.VideoRenderer.I420Frame; +import org.webrtc.VideoSource; +import org.webrtc.VideoTrack; + +import java.util.LinkedList; +import java.util.List; + +/** + * Main Activity of the AppRTCDemo Android app demonstrating interoperability + * between the Android/Java implementation of PeerConnection and the + * apprtc.appspot.com demo webapp. + */ +public class AppRTCDemoActivity extends Activity + implements AppRTCClient.IceServersObserver { + private static final String TAG = "AppRTCDemoActivity"; + private PeerConnection pc; + private final PCObserver pcObserver = new PCObserver(); + private final SDPObserver sdpObserver = new SDPObserver(); + private final GAEChannelClient.MessageHandler gaeHandler = new GAEHandler(); + private AppRTCClient appRtcClient = new AppRTCClient(this, gaeHandler, this); + private VideoStreamsView vsv; + private Toast logToast; + private LinkedList queuedRemoteCandidates = + new LinkedList(); + // Synchronize on quit[0] to avoid teardown-related crashes. + private final Boolean[] quit = new Boolean[] { false }; + private MediaConstraints sdpMediaConstraints; + private PowerManager.WakeLock wakeLock; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Since the error-handling of this demo consists of throwing + // RuntimeExceptions and we assume that'll terminate the app, we install + // this default handler so it's applied to background threads as well. + Thread.setDefaultUncaughtExceptionHandler( + new Thread.UncaughtExceptionHandler() { + public void uncaughtException(Thread t, Throwable e) { + e.printStackTrace(); + System.exit(-1); + } + }); + + PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); + wakeLock = powerManager.newWakeLock( + PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "AppRTCDemo"); + wakeLock.acquire(); + + Point displaySize = new Point(); + getWindowManager().getDefaultDisplay().getSize(displaySize); + vsv = new VideoStreamsView(this, displaySize); + setContentView(vsv); + + abortUnless(PeerConnectionFactory.initializeAndroidGlobals(this), + "Failed to initializeAndroidGlobals"); + + AudioManager audioManager = + ((AudioManager) getSystemService(AUDIO_SERVICE)); + audioManager.setMode(audioManager.isWiredHeadsetOn() ? + AudioManager.MODE_IN_CALL : AudioManager.MODE_IN_COMMUNICATION); + audioManager.setSpeakerphoneOn(!audioManager.isWiredHeadsetOn()); + + sdpMediaConstraints = new MediaConstraints(); + sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair( + "OfferToReceiveAudio", "true")); + sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair( + "OfferToReceiveVideo", "true")); + + final Intent intent = getIntent(); + if ("android.intent.action.VIEW".equals(intent.getAction())) { + connectToRoom(intent.getData().toString()); + return; + } + showGetRoomUI(); + } + + private void showGetRoomUI() { + final EditText roomInput = new EditText(this); + roomInput.setText("https://apprtc.appspot.com/?r="); + roomInput.setSelection(roomInput.getText().length()); + DialogInterface.OnClickListener listener = + new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { + abortUnless(which == DialogInterface.BUTTON_POSITIVE, "lolwat?"); + dialog.dismiss(); + connectToRoom(roomInput.getText().toString()); + } + }; + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder + .setMessage("Enter room URL").setView(roomInput) + .setPositiveButton("Go!", listener).show(); + } + + private void connectToRoom(String roomUrl) { + logAndToast("Connecting to room..."); + appRtcClient.connectToRoom(roomUrl); + } + + @Override + public void onPause() { + super.onPause(); + vsv.onPause(); + // TODO(fischman): IWBN to support pause/resume, but the WebRTC codebase + // isn't ready for that yet; e.g. + // https://code.google.com/p/webrtc/issues/detail?id=1407 + // Instead, simply exit instead of pausing (the alternative leads to + // system-borking with wedged cameras; e.g. b/8224551) + disconnectAndExit(); + } + + @Override + public void onResume() { + // The onResume() is a lie! See TODO(fischman) in onPause() above. + super.onResume(); + vsv.onResume(); + } + + @Override + public void onIceServers(List iceServers) { + PeerConnectionFactory factory = new PeerConnectionFactory(); + + pc = factory.createPeerConnection( + iceServers, appRtcClient.pcConstraints(), pcObserver); + + { + final PeerConnection finalPC = pc; + final Runnable repeatedStatsLogger = new Runnable() { + public void run() { + synchronized (quit[0]) { + if (quit[0]) { + return; + } + final Runnable runnableThis = this; + boolean success = finalPC.getStats(new StatsObserver() { + public void onComplete(StatsReport[] reports) { + for (StatsReport report : reports) { + Log.d(TAG, "Stats: " + report.toString()); + } + vsv.postDelayed(runnableThis, 10000); + } + }, null); + if (!success) { + throw new RuntimeException("getStats() return false!"); + } + } + } + }; + vsv.postDelayed(repeatedStatsLogger, 10000); + } + + { + logAndToast("Creating local video source..."); + VideoCapturer capturer = getVideoCapturer(); + VideoSource videoSource = factory.createVideoSource( + capturer, appRtcClient.videoConstraints()); + MediaStream lMS = factory.createLocalMediaStream("ARDAMS"); + VideoTrack videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource); + videoTrack.addRenderer(new VideoRenderer(new VideoCallbacks( + vsv, VideoStreamsView.Endpoint.LOCAL))); + lMS.addTrack(videoTrack); + lMS.addTrack(factory.createAudioTrack("ARDAMSa0")); + pc.addStream(lMS, new MediaConstraints()); + } + logAndToast("Waiting for ICE candidates..."); + } + + // Cycle through likely device names for the camera and return the first + // capturer that works, or crash if none do. + private VideoCapturer getVideoCapturer() { + String[] cameraFacing = { "front", "back" }; + int[] cameraIndex = { 0, 1 }; + int[] cameraOrientation = { 0, 90, 180, 270 }; + for (String facing : cameraFacing) { + for (int index : cameraIndex) { + for (int orientation : cameraOrientation) { + String name = "Camera " + index + ", Facing " + facing + + ", Orientation " + orientation; + VideoCapturer capturer = VideoCapturer.create(name); + if (capturer != null) { + logAndToast("Using camera: " + name); + return capturer; + } + } + } + } + throw new RuntimeException("Failed to open capturer"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + // Poor-man's assert(): die with |msg| unless |condition| is true. + private static void abortUnless(boolean condition, String msg) { + if (!condition) { + throw new RuntimeException(msg); + } + } + + // Log |msg| and Toast about it. + private void logAndToast(String msg) { + Log.d(TAG, msg); + if (logToast != null) { + logToast.cancel(); + } + logToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT); + logToast.show(); + } + + // Send |json| to the underlying AppEngine Channel. + private void sendMessage(JSONObject json) { + appRtcClient.sendMessage(json.toString()); + } + + // Put a |key|->|value| mapping in |json|. + private static void jsonPut(JSONObject json, String key, Object value) { + try { + json.put(key, value); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + // Implementation detail: observe ICE & stream changes and react accordingly. + private class PCObserver implements PeerConnection.Observer { + @Override public void onIceCandidate(final IceCandidate candidate){ + runOnUiThread(new Runnable() { + public void run() { + JSONObject json = new JSONObject(); + jsonPut(json, "type", "candidate"); + jsonPut(json, "label", candidate.sdpMLineIndex); + jsonPut(json, "id", candidate.sdpMid); + jsonPut(json, "candidate", candidate.sdp); + sendMessage(json); + } + }); + } + + @Override public void onError(){ + runOnUiThread(new Runnable() { + public void run() { + throw new RuntimeException("PeerConnection error!"); + } + }); + } + + @Override public void onSignalingChange( + PeerConnection.SignalingState newState) { + } + + @Override public void onIceConnectionChange( + PeerConnection.IceConnectionState newState) { + } + + @Override public void onIceGatheringChange( + PeerConnection.IceGatheringState newState) { + } + + @Override public void onAddStream(final MediaStream stream){ + runOnUiThread(new Runnable() { + public void run() { + abortUnless(stream.audioTracks.size() == 1 && + stream.videoTracks.size() == 1, + "Weird-looking stream: " + stream); + stream.videoTracks.get(0).addRenderer(new VideoRenderer( + new VideoCallbacks(vsv, VideoStreamsView.Endpoint.REMOTE))); + } + }); + } + + @Override public void onRemoveStream(final MediaStream stream){ + runOnUiThread(new Runnable() { + public void run() { + stream.videoTracks.get(0).dispose(); + } + }); + } + } + + // Implementation detail: handle offer creation/signaling and answer setting, + // as well as adding remote ICE candidates once the answer SDP is set. + private class SDPObserver implements SdpObserver { + @Override public void onCreateSuccess(final SessionDescription sdp) { + runOnUiThread(new Runnable() { + public void run() { + logAndToast("Sending " + sdp.type); + JSONObject json = new JSONObject(); + jsonPut(json, "type", sdp.type.canonicalForm()); + jsonPut(json, "sdp", sdp.description); + sendMessage(json); + pc.setLocalDescription(sdpObserver, sdp); + } + }); + } + + @Override public void onSetSuccess() { + runOnUiThread(new Runnable() { + public void run() { + if (appRtcClient.isInitiator()) { + if (pc.getRemoteDescription() != null) { + // We've set our local offer and received & set the remote + // answer, so drain candidates. + drainRemoteCandidates(); + } + } else { + if (pc.getLocalDescription() == null) { + // We just set the remote offer, time to create our answer. + logAndToast("Creating answer"); + pc.createAnswer(SDPObserver.this, sdpMediaConstraints); + } else { + // Sent our answer and set it as local description; drain + // candidates. + drainRemoteCandidates(); + } + } + } + }); + } + + @Override public void onCreateFailure(final String error) { + runOnUiThread(new Runnable() { + public void run() { + throw new RuntimeException("createSDP error: " + error); + } + }); + } + + @Override public void onSetFailure(final String error) { + runOnUiThread(new Runnable() { + public void run() { + throw new RuntimeException("setSDP error: " + error); + } + }); + } + + private void drainRemoteCandidates() { + for (IceCandidate candidate : queuedRemoteCandidates) { + pc.addIceCandidate(candidate); + } + queuedRemoteCandidates = null; + } + } + + // Implementation detail: handler for receiving GAE messages and dispatching + // them appropriately. + private class GAEHandler implements GAEChannelClient.MessageHandler { + @JavascriptInterface public void onOpen() { + if (!appRtcClient.isInitiator()) { + return; + } + logAndToast("Creating offer..."); + pc.createOffer(sdpObserver, sdpMediaConstraints); + } + + @JavascriptInterface public void onMessage(String data) { + try { + JSONObject json = new JSONObject(data); + String type = (String) json.get("type"); + if (type.equals("candidate")) { + IceCandidate candidate = new IceCandidate( + (String) json.get("id"), + json.getInt("label"), + (String) json.get("candidate")); + if (queuedRemoteCandidates != null) { + queuedRemoteCandidates.add(candidate); + } else { + pc.addIceCandidate(candidate); + } + } else if (type.equals("answer") || type.equals("offer")) { + SessionDescription sdp = new SessionDescription( + SessionDescription.Type.fromCanonicalForm(type), + (String) json.get("sdp")); + pc.setRemoteDescription(sdpObserver, sdp); + } else if (type.equals("bye")) { + logAndToast("Remote end hung up; dropping PeerConnection"); + disconnectAndExit(); + } else { + throw new RuntimeException("Unexpected message: " + data); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + @JavascriptInterface public void onClose() { + disconnectAndExit(); + } + + @JavascriptInterface public void onError(int code, String description) { + disconnectAndExit(); + } + } + + // Disconnect from remote resources, dispose of local resources, and exit. + private void disconnectAndExit() { + synchronized (quit[0]) { + if (quit[0]) { + return; + } + quit[0] = true; + wakeLock.release(); + if (pc != null) { + pc.dispose(); + pc = null; + } + if (appRtcClient != null) { + appRtcClient.sendMessage("{\"type\": \"bye\"}"); + appRtcClient.disconnect(); + appRtcClient = null; + } + finish(); + } + } + + // Implementation detail: bridge the VideoRenderer.Callbacks interface to the + // VideoStreamsView implementation. + private class VideoCallbacks implements VideoRenderer.Callbacks { + private final VideoStreamsView view; + private final VideoStreamsView.Endpoint stream; + + public VideoCallbacks( + VideoStreamsView view, VideoStreamsView.Endpoint stream) { + this.view = view; + this.stream = stream; + } + + @Override + public void setSize(final int width, final int height) { + view.queueEvent(new Runnable() { + public void run() { + view.setSize(stream, width, height); + } + }); + } + + @Override + public void renderFrame(I420Frame frame) { + view.queueFrame(stream, frame); + } + } +} diff --git a/talk/examples/android/src/org/appspot/apprtc/FramePool.java b/talk/examples/android/src/org/appspot/apprtc/FramePool.java new file mode 100644 index 000000000..6f1128650 --- /dev/null +++ b/talk/examples/android/src/org/appspot/apprtc/FramePool.java @@ -0,0 +1,104 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.appspot.apprtc; + +import org.webrtc.VideoRenderer.I420Frame; + +import java.util.HashMap; +import java.util.LinkedList; + +/** + * This class acts as an allocation pool meant to minimize GC churn caused by + * frame allocation & disposal. The public API comprises of just two methods: + * copyFrame(), which allocates as necessary and copies, and + * returnFrame(), which returns frame ownership to the pool for use by a later + * call to copyFrame(). + * + * This class is thread-safe; calls to copyFrame() and returnFrame() are allowed + * to happen on any thread. + */ +class FramePool { + // Maps each summary code (see summarizeFrameDimensions()) to a list of frames + // of that description. + private final HashMap> availableFrames = + new HashMap>(); + // Every dimension (e.g. width, height, stride) of a frame must be less than + // this value. + private static final long MAX_DIMENSION = 4096; + + public I420Frame takeFrame(I420Frame source) { + long desc = summarizeFrameDimensions(source); + I420Frame dst = null; + synchronized (availableFrames) { + LinkedList frames = availableFrames.get(desc); + if (frames == null) { + frames = new LinkedList(); + availableFrames.put(desc, frames); + } + if (!frames.isEmpty()) { + dst = frames.pop(); + } else { + dst = new I420Frame( + source.width, source.height, source.yuvStrides, null); + } + } + return dst; + } + + public void returnFrame(I420Frame frame) { + long desc = summarizeFrameDimensions(frame); + synchronized (availableFrames) { + LinkedList frames = availableFrames.get(desc); + if (frames == null) { + throw new IllegalArgumentException("Unexpected frame dimensions"); + } + frames.add(frame); + } + } + + /** Validate that |frame| can be managed by the pool. */ + public static boolean validateDimensions(I420Frame frame) { + return frame.width < MAX_DIMENSION && frame.height < MAX_DIMENSION && + frame.yuvStrides[0] < MAX_DIMENSION && + frame.yuvStrides[1] < MAX_DIMENSION && + frame.yuvStrides[2] < MAX_DIMENSION; + } + + // Return a code summarizing the dimensions of |frame|. Two frames that + // return the same summary are guaranteed to be able to store each others' + // contents. Used like Object.hashCode(), but we need all the bits of a long + // to do a good job, and hashCode() returns int, so we do this. + private static long summarizeFrameDimensions(I420Frame frame) { + long ret = frame.width; + ret = ret * MAX_DIMENSION + frame.height; + ret = ret * MAX_DIMENSION + frame.yuvStrides[0]; + ret = ret * MAX_DIMENSION + frame.yuvStrides[1]; + ret = ret * MAX_DIMENSION + frame.yuvStrides[2]; + return ret; + } +} diff --git a/talk/examples/android/src/org/appspot/apprtc/GAEChannelClient.java b/talk/examples/android/src/org/appspot/apprtc/GAEChannelClient.java new file mode 100644 index 000000000..46f638d65 --- /dev/null +++ b/talk/examples/android/src/org/appspot/apprtc/GAEChannelClient.java @@ -0,0 +1,164 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.appspot.apprtc; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.util.Log; +import android.webkit.ConsoleMessage; +import android.webkit.JavascriptInterface; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +/** + * Java-land version of Google AppEngine's JavaScript Channel API: + * https://developers.google.com/appengine/docs/python/channel/javascript + * + * Requires a hosted HTML page that opens the desired channel and dispatches JS + * on{Open,Message,Close,Error}() events to a global object named + * "androidMessageHandler". + */ +public class GAEChannelClient { + private static final String TAG = "GAEChannelClient"; + private WebView webView; + private final ProxyingMessageHandler proxyingMessageHandler; + + /** + * Callback interface for messages delivered on the Google AppEngine channel. + * + * Methods are guaranteed to be invoked on the UI thread of |activity| passed + * to GAEChannelClient's constructor. + */ + public interface MessageHandler { + public void onOpen(); + public void onMessage(String data); + public void onClose(); + public void onError(int code, String description); + } + + /** Asynchronously open an AppEngine channel. */ + @SuppressLint("SetJavaScriptEnabled") + public GAEChannelClient( + Activity activity, String token, MessageHandler handler) { + webView = new WebView(activity); + webView.getSettings().setJavaScriptEnabled(true); + webView.setWebChromeClient(new WebChromeClient() { // Purely for debugging. + public boolean onConsoleMessage (ConsoleMessage msg) { + Log.d(TAG, "console: " + msg.message() + " at " + + msg.sourceId() + ":" + msg.lineNumber()); + return false; + } + }); + webView.setWebViewClient(new WebViewClient() { // Purely for debugging. + public void onReceivedError( + WebView view, int errorCode, String description, + String failingUrl) { + Log.e(TAG, "JS error: " + errorCode + " in " + failingUrl + + ", desc: " + description); + } + }); + proxyingMessageHandler = new ProxyingMessageHandler(activity, handler); + webView.addJavascriptInterface( + proxyingMessageHandler, "androidMessageHandler"); + webView.loadUrl("file:///android_asset/channel.html?token=" + token); + } + + /** Close the connection to the AppEngine channel. */ + public void close() { + if (webView == null) { + return; + } + proxyingMessageHandler.disconnect(); + webView.removeJavascriptInterface("androidMessageHandler"); + webView.loadUrl("about:blank"); + webView = null; + } + + // Helper class for proxying callbacks from the Java<->JS interaction + // (private, background) thread to the Activity's UI thread. + private static class ProxyingMessageHandler { + private final Activity activity; + private final MessageHandler handler; + private final boolean[] disconnected = { false }; + + public ProxyingMessageHandler(Activity activity, MessageHandler handler) { + this.activity = activity; + this.handler = handler; + } + + public void disconnect() { + disconnected[0] = true; + } + + private boolean disconnected() { + return disconnected[0]; + } + + @JavascriptInterface public void onOpen() { + activity.runOnUiThread(new Runnable() { + public void run() { + if (!disconnected()) { + handler.onOpen(); + } + } + }); + } + + @JavascriptInterface public void onMessage(final String data) { + activity.runOnUiThread(new Runnable() { + public void run() { + if (!disconnected()) { + handler.onMessage(data); + } + } + }); + } + + @JavascriptInterface public void onClose() { + activity.runOnUiThread(new Runnable() { + public void run() { + if (!disconnected()) { + handler.onClose(); + } + } + }); + } + + @JavascriptInterface public void onError( + final int code, final String description) { + activity.runOnUiThread(new Runnable() { + public void run() { + if (!disconnected()) { + handler.onError(code, description); + } + } + }); + } + } +} diff --git a/talk/examples/android/src/org/appspot/apprtc/VideoStreamsView.java b/talk/examples/android/src/org/appspot/apprtc/VideoStreamsView.java new file mode 100644 index 000000000..12217d6db --- /dev/null +++ b/talk/examples/android/src/org/appspot/apprtc/VideoStreamsView.java @@ -0,0 +1,295 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +package org.appspot.apprtc; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.util.Log; + +import org.webrtc.VideoRenderer.I420Frame; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.EnumMap; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * A GLSurfaceView{,.Renderer} that efficiently renders YUV frames from local & + * remote VideoTracks using the GPU for CSC. Clients will want to call the + * constructor, setSize() and updateFrame() as appropriate, but none of the + * other public methods of this class are of interest to clients (only to system + * classes). + */ +public class VideoStreamsView + extends GLSurfaceView + implements GLSurfaceView.Renderer { + + /** Identify which of the two video streams is being addressed. */ + public static enum Endpoint { LOCAL, REMOTE }; + + private final static String TAG = "VideoStreamsView"; + private EnumMap rects = + new EnumMap(Endpoint.class); + private Point screenDimensions; + // [0] are local Y,U,V, [1] are remote Y,U,V. + private int[][] yuvTextures = { { -1, -1, -1}, {-1, -1, -1 }}; + private int posLocation = -1; + private long lastFPSLogTime = System.nanoTime(); + private long numFramesSinceLastLog = 0; + private FramePool framePool = new FramePool(); + + public VideoStreamsView(Context c, Point screenDimensions) { + super(c); + this.screenDimensions = screenDimensions; + setEGLContextClientVersion(2); + setRenderer(this); + setRenderMode(RENDERMODE_WHEN_DIRTY); + } + + /** Queue |frame| to be uploaded. */ + public void queueFrame(final Endpoint stream, I420Frame frame) { + // Paying for the copy of the YUV data here allows CSC and painting time + // to get spent on the render thread instead of the UI thread. + abortUnless(framePool.validateDimensions(frame), "Frame too large!"); + final I420Frame frameCopy = framePool.takeFrame(frame).copyFrom(frame); + queueEvent(new Runnable() { + public void run() { + updateFrame(stream, frameCopy); + } + }); + } + + // Upload the planes from |frame| to the textures owned by this View. + private void updateFrame(Endpoint stream, I420Frame frame) { + int[] textures = yuvTextures[stream == Endpoint.LOCAL ? 0 : 1]; + texImage2D(frame, textures); + framePool.returnFrame(frame); + requestRender(); + } + + /** Inform this View of the dimensions of frames coming from |stream|. */ + public void setSize(Endpoint stream, int width, int height) { + // Generate 3 texture ids for Y/U/V and place them into |textures|, + // allocating enough storage for |width|x|height| pixels. + int[] textures = yuvTextures[stream == Endpoint.LOCAL ? 0 : 1]; + GLES20.glGenTextures(3, textures, 0); + for (int i = 0; i < 3; ++i) { + int w = i == 0 ? width : width / 2; + int h = i == 0 ? height : height / 2; + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[i]); + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, w, h, 0, + GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, null); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + } + checkNoGLES2Error(); + } + + @Override + protected void onMeasure(int unusedX, int unusedY) { + // Go big or go home! + setMeasuredDimension(screenDimensions.x, screenDimensions.y); + } + + @Override + public void onSurfaceChanged(GL10 unused, int width, int height) { + GLES20.glViewport(0, 0, width, height); + checkNoGLES2Error(); + } + + @Override + public void onDrawFrame(GL10 unused) { + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + drawRectangle(yuvTextures[1], remoteVertices); + drawRectangle(yuvTextures[0], localVertices); + ++numFramesSinceLastLog; + long now = System.nanoTime(); + if (lastFPSLogTime == -1 || now - lastFPSLogTime > 1e9) { + double fps = numFramesSinceLastLog / ((now - lastFPSLogTime) / 1e9); + Log.d(TAG, "Rendered FPS: " + fps); + lastFPSLogTime = now; + numFramesSinceLastLog = 1; + } + checkNoGLES2Error(); + } + + @Override + public void onSurfaceCreated(GL10 unused, EGLConfig config) { + int program = GLES20.glCreateProgram(); + addShaderTo(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_STRING, program); + addShaderTo(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_STRING, program); + + GLES20.glLinkProgram(program); + int[] result = new int[] { GLES20.GL_FALSE }; + result[0] = GLES20.GL_FALSE; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, result, 0); + abortUnless(result[0] == GLES20.GL_TRUE, + GLES20.glGetProgramInfoLog(program)); + GLES20.glUseProgram(program); + + GLES20.glUniform1i(GLES20.glGetUniformLocation(program, "y_tex"), 0); + GLES20.glUniform1i(GLES20.glGetUniformLocation(program, "u_tex"), 1); + GLES20.glUniform1i(GLES20.glGetUniformLocation(program, "v_tex"), 2); + + // Actually set in drawRectangle(), but queried only once here. + posLocation = GLES20.glGetAttribLocation(program, "in_pos"); + + int tcLocation = GLES20.glGetAttribLocation(program, "in_tc"); + GLES20.glEnableVertexAttribArray(tcLocation); + GLES20.glVertexAttribPointer( + tcLocation, 2, GLES20.GL_FLOAT, false, 0, textureCoords); + + GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + checkNoGLES2Error(); + } + + // Wrap a float[] in a direct FloatBuffer using native byte order. + private static FloatBuffer directNativeFloatBuffer(float[] array) { + FloatBuffer buffer = ByteBuffer.allocateDirect(array.length * 4).order( + ByteOrder.nativeOrder()).asFloatBuffer(); + buffer.put(array); + buffer.flip(); + return buffer; + } + + // Upload the YUV planes from |frame| to |textures|. + private void texImage2D(I420Frame frame, int[] textures) { + for (int i = 0; i < 3; ++i) { + ByteBuffer plane = frame.yuvPlanes[i]; + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[i]); + int w = i == 0 ? frame.width : frame.width / 2; + int h = i == 0 ? frame.height : frame.height / 2; + abortUnless(w == frame.yuvStrides[i], frame.yuvStrides[i] + "!=" + w); + GLES20.glTexImage2D( + GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, w, h, 0, + GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, plane); + } + checkNoGLES2Error(); + } + + // Draw |textures| using |vertices| (X,Y coordinates). + private void drawRectangle(int[] textures, FloatBuffer vertices) { + for (int i = 0; i < 3; ++i) { + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[i]); + } + + GLES20.glVertexAttribPointer( + posLocation, 2, GLES20.GL_FLOAT, false, 0, vertices); + GLES20.glEnableVertexAttribArray(posLocation); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + checkNoGLES2Error(); + } + + // Compile & attach a |type| shader specified by |source| to |program|. + private static void addShaderTo( + int type, String source, int program) { + int[] result = new int[] { GLES20.GL_FALSE }; + int shader = GLES20.glCreateShader(type); + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, 0); + abortUnless(result[0] == GLES20.GL_TRUE, + GLES20.glGetShaderInfoLog(shader) + ", source: " + source); + GLES20.glAttachShader(program, shader); + GLES20.glDeleteShader(shader); + checkNoGLES2Error(); + } + + // Poor-man's assert(): die with |msg| unless |condition| is true. + private static void abortUnless(boolean condition, String msg) { + if (!condition) { + throw new RuntimeException(msg); + } + } + + // Assert that no OpenGL ES 2.0 error has been raised. + private static void checkNoGLES2Error() { + int error = GLES20.glGetError(); + abortUnless(error == GLES20.GL_NO_ERROR, "GLES20 error: " + error); + } + + // Remote image should span the full screen. + private static final FloatBuffer remoteVertices = directNativeFloatBuffer( + new float[] { -1, 1, -1, -1, 1, 1, 1, -1 }); + + // Local image should be thumbnailish. + private static final FloatBuffer localVertices = directNativeFloatBuffer( + new float[] { 0.6f, 0.9f, 0.6f, 0.6f, 0.9f, 0.9f, 0.9f, 0.6f }); + + // Texture Coordinates mapping the entire texture. + private static final FloatBuffer textureCoords = directNativeFloatBuffer( + new float[] { 0, 0, 0, 1, 1, 0, 1, 1 }); + + // Pass-through vertex shader. + private static final String VERTEX_SHADER_STRING = + "varying vec2 interp_tc;\n" + + "\n" + + "attribute vec4 in_pos;\n" + + "attribute vec2 in_tc;\n" + + "\n" + + "void main() {\n" + + " gl_Position = in_pos;\n" + + " interp_tc = in_tc;\n" + + "}\n"; + + // YUV to RGB pixel shader. Loads a pixel from each plane and pass through the + // matrix. + private static final String FRAGMENT_SHADER_STRING = + "precision mediump float;\n" + + "varying vec2 interp_tc;\n" + + "\n" + + "uniform sampler2D y_tex;\n" + + "uniform sampler2D u_tex;\n" + + "uniform sampler2D v_tex;\n" + + "\n" + + "void main() {\n" + + " float y = texture2D(y_tex, interp_tc).r;\n" + + " float u = texture2D(u_tex, interp_tc).r - .5;\n" + + " float v = texture2D(v_tex, interp_tc).r - .5;\n" + + // CSC according to http://www.fourcc.org/fccyvrgb.php + " gl_FragColor = vec4(y + 1.403 * v, " + + " y - 0.344 * u - 0.714 * v, " + + " y + 1.77 * u, 1);\n" + + "}\n"; +} diff --git a/talk/examples/call/Info.plist b/talk/examples/call/Info.plist new file mode 100644 index 000000000..a59cfa591 --- /dev/null +++ b/talk/examples/call/Info.plist @@ -0,0 +1,11 @@ + + + + + CFBundleIdentifier + com.google.call + CFBundleName + call + + + diff --git a/talk/examples/call/call_main.cc b/talk/examples/call/call_main.cc new file mode 100644 index 000000000..2ee796b1c --- /dev/null +++ b/talk/examples/call/call_main.cc @@ -0,0 +1,499 @@ +/* + * libjingle + * Copyright 2004--2005, 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 +#include +#include +#include +#include +#include + +#include "talk/base/flags.h" +#include "talk/base/logging.h" +#ifdef OSX +#include "talk/base/maccocoasocketserver.h" +#endif +#include "talk/base/pathutils.h" +#include "talk/base/ssladapter.h" +#include "talk/base/stream.h" +#include "talk/base/win32socketserver.h" +#include "talk/examples/call/callclient.h" +#include "talk/examples/call/console.h" +#include "talk/examples/call/mediaenginefactory.h" +#include "talk/p2p/base/constants.h" +#ifdef ANDROID +#include "talk/media/other/androidmediaengine.h" +#endif +#include "talk/session/media/mediasessionclient.h" +#include "talk/session/media/srtpfilter.h" +#include "talk/xmpp/xmppauth.h" +#include "talk/xmpp/xmppclientsettings.h" +#include "talk/xmpp/xmpppump.h" +#include "talk/xmpp/xmppsocket.h" + +class DebugLog : public sigslot::has_slots<> { + public: + DebugLog() : + debug_input_buf_(NULL), debug_input_len_(0), debug_input_alloc_(0), + debug_output_buf_(NULL), debug_output_len_(0), debug_output_alloc_(0), + censor_password_(false) + {} + char * debug_input_buf_; + int debug_input_len_; + int debug_input_alloc_; + char * debug_output_buf_; + int debug_output_len_; + int debug_output_alloc_; + bool censor_password_; + + void Input(const char * data, int len) { + if (debug_input_len_ + len > debug_input_alloc_) { + char * old_buf = debug_input_buf_; + debug_input_alloc_ = 4096; + while (debug_input_alloc_ < debug_input_len_ + len) { + debug_input_alloc_ *= 2; + } + debug_input_buf_ = new char[debug_input_alloc_]; + memcpy(debug_input_buf_, old_buf, debug_input_len_); + delete[] old_buf; + } + memcpy(debug_input_buf_ + debug_input_len_, data, len); + debug_input_len_ += len; + DebugPrint(debug_input_buf_, &debug_input_len_, false); + } + + void Output(const char * data, int len) { + if (debug_output_len_ + len > debug_output_alloc_) { + char * old_buf = debug_output_buf_; + debug_output_alloc_ = 4096; + while (debug_output_alloc_ < debug_output_len_ + len) { + debug_output_alloc_ *= 2; + } + debug_output_buf_ = new char[debug_output_alloc_]; + memcpy(debug_output_buf_, old_buf, debug_output_len_); + delete[] old_buf; + } + memcpy(debug_output_buf_ + debug_output_len_, data, len); + debug_output_len_ += len; + DebugPrint(debug_output_buf_, &debug_output_len_, true); + } + + static bool IsAuthTag(const char * str, size_t len) { + if (str[0] == '<' && str[1] == 'a' && + str[2] == 'u' && + str[3] == 't' && + str[4] == 'h' && + str[5] <= ' ') { + std::string tag(str, len); + + if (tag.find("mechanism") != std::string::npos) + return true; + } + return false; + } + + void DebugPrint(char * buf, int * plen, bool output) { + int len = *plen; + if (len > 0) { + time_t tim = time(NULL); + struct tm * now = localtime(&tim); + char *time_string = asctime(now); + if (time_string) { + size_t time_len = strlen(time_string); + if (time_len > 0) { + time_string[time_len-1] = 0; // trim off terminating \n + } + } + LOG(INFO) << (output ? "SEND >>>>>>>>>>>>>>>>" : "RECV <<<<<<<<<<<<<<<<") + << " : " << time_string; + + bool indent; + int start = 0, nest = 3; + for (int i = 0; i < len; i += 1) { + if (buf[i] == '>') { + if ((i > 0) && (buf[i-1] == '/')) { + indent = false; + } else if ((start + 1 < len) && (buf[start + 1] == '/')) { + indent = false; + nest -= 2; + } else { + indent = true; + } + + // Output a tag + LOG(INFO) << std::setw(nest) << " " + << std::string(buf + start, i + 1 - start); + + if (indent) + nest += 2; + + // Note if it's a PLAIN auth tag + if (IsAuthTag(buf + start, i + 1 - start)) { + censor_password_ = true; + } + + // incr + start = i + 1; + } + + if (buf[i] == '<' && start < i) { + if (censor_password_) { + LOG(INFO) << std::setw(nest) << " " << "## TEXT REMOVED ##"; + censor_password_ = false; + } else { + LOG(INFO) << std::setw(nest) << " " + << std::string(buf + start, i - start); + } + start = i; + } + } + len = len - start; + memcpy(buf, buf + start, len); + *plen = len; + } + } +}; + +static DebugLog debug_log_; +static const int DEFAULT_PORT = 5222; + +#ifdef ANDROID +static std::vector codecs; +static const cricket::AudioCodec ISAC(103, "ISAC", 40000, 16000, 1, 0); + +cricket::MediaEngine *AndroidMediaEngineFactory() { + cricket::FakeMediaEngine *engine = new cricket::FakeMediaEngine(); + + codecs.push_back(ISAC); + engine->SetAudioCodecs(codecs); + return engine; +} +#endif + +// TODO: Move this into Console. +void Print(const char* chars) { + printf("%s", chars); + fflush(stdout); +} + +bool GetSecurePolicy(const std::string& in, cricket::SecurePolicy* out) { + if (in == "disable") { + *out = cricket::SEC_DISABLED; + } else if (in == "enable") { + *out = cricket::SEC_ENABLED; + } else if (in == "require") { + *out = cricket::SEC_REQUIRED; + } else { + return false; + } + return true; +} + +int main(int argc, char **argv) { + // This app has three threads. The main thread will run the XMPP client, + // which will print to the screen in its own thread. A second thread + // will get input from the console, parse it, and pass the appropriate + // message back to the XMPP client's thread. A third thread is used + // by MediaSessionClient as its worker thread. + + // define options + DEFINE_string(s, "talk.google.com", "The connection server to use."); + DEFINE_string(tls, "require", + "Select connection encryption: disable, enable, require."); + DEFINE_bool(allowplain, false, "Allow plain authentication."); + DEFINE_bool(testserver, false, "Use test server."); + DEFINE_string(oauth, "", "OAuth2 access token."); + DEFINE_bool(a, false, "Turn on auto accept for incoming calls."); + DEFINE_string(signaling, "hybrid", + "Initial signaling protocol to use: jingle, gingle, or hybrid."); + DEFINE_string(transport, "hybrid", + "Initial transport protocol to use: ice, gice, or hybrid."); + DEFINE_string(sdes, "enable", + "Select SDES media encryption: disable, enable, require."); + DEFINE_string(dtls, "disable", + "Select DTLS transport encryption: disable, enable, require."); + DEFINE_int(portallocator, 0, "Filter out unwanted connection types."); + DEFINE_string(pmuc, "groupchat.google.com", "The persistant muc domain."); + DEFINE_string(capsnode, "http://code.google.com/p/libjingle/call", + "Caps node: A URI identifying the app."); + DEFINE_string(capsver, "0.6", + "Caps ver: A string identifying the version of the app."); + DEFINE_string(voiceinput, NULL, "RTP dump file for voice input."); + DEFINE_string(voiceoutput, NULL, "RTP dump file for voice output."); + DEFINE_string(videoinput, NULL, "RTP dump file for video input."); + DEFINE_string(videooutput, NULL, "RTP dump file for video output."); + DEFINE_bool(render, true, "Renders the video."); + DEFINE_string(datachannel, "", + "Enable a data channel, and choose the type: rtp or sctp."); + DEFINE_bool(d, false, "Turn on debugging."); + DEFINE_string(log, "", "Turn on debugging to a file."); + DEFINE_bool(debugsrtp, false, "Enable debugging for srtp."); + DEFINE_bool(help, false, "Prints this message"); + DEFINE_bool(multisession, false, + "Enable support for multiple sessions in calls."); + DEFINE_bool(roster, false, + "Enable roster messages printed in console."); + + // parse options + FlagList::SetFlagsFromCommandLine(&argc, argv, true); + if (FLAG_help) { + FlagList::Print(NULL, false); + return 0; + } + + bool auto_accept = FLAG_a; + bool debug = FLAG_d; + std::string log = FLAG_log; + std::string signaling = FLAG_signaling; + std::string transport = FLAG_transport; + bool test_server = FLAG_testserver; + bool allow_plain = FLAG_allowplain; + std::string tls = FLAG_tls; + std::string oauth_token = FLAG_oauth; + int32 portallocator_flags = FLAG_portallocator; + std::string pmuc_domain = FLAG_pmuc; + std::string server = FLAG_s; + std::string sdes = FLAG_sdes; + std::string dtls = FLAG_dtls; + std::string caps_node = FLAG_capsnode; + std::string caps_ver = FLAG_capsver; + bool debugsrtp = FLAG_debugsrtp; + bool render = FLAG_render; + std::string data_channel = FLAG_datachannel; + bool multisession_enabled = FLAG_multisession; + talk_base::SSLIdentity* ssl_identity = NULL; + bool show_roster_messages = FLAG_roster; + + // Set up debugging. + if (debug) { + talk_base::LogMessage::LogToDebug(talk_base::LS_VERBOSE); + } + + if (!log.empty()) { + talk_base::StreamInterface* stream = + talk_base::Filesystem::OpenFile(log, "a"); + if (stream) { + talk_base::LogMessage::LogToStream(stream, talk_base::LS_VERBOSE); + } else { + Print(("Cannot open debug log " + log + "\n").c_str()); + return 1; + } + } + + if (debugsrtp) { + cricket::EnableSrtpDebugging(); + } + + // Set up the crypto subsystem. + talk_base::InitializeSSL(); + + // Parse username and password, if present. + buzz::Jid jid; + std::string username; + talk_base::InsecureCryptStringImpl pass; + if (argc > 1) { + username = argv[1]; + if (argc > 2) { + pass.password() = argv[2]; + } + } + + if (username.empty()) { + Print("JID: "); + std::cin >> username; + } + if (username.find('@') == std::string::npos) { + username.append("@localhost"); + } + jid = buzz::Jid(username); + if (!jid.IsValid() || jid.node() == "") { + Print("Invalid JID. JIDs should be in the form user@domain\n"); + return 1; + } + if (pass.password().empty() && !test_server && oauth_token.empty()) { + Console::SetEcho(false); + Print("Password: "); + std::cin >> pass.password(); + Console::SetEcho(true); + Print("\n"); + } + + // Decide on the connection settings. + buzz::XmppClientSettings xcs; + xcs.set_user(jid.node()); + xcs.set_resource("call"); + xcs.set_host(jid.domain()); + xcs.set_allow_plain(allow_plain); + + if (tls == "disable") { + xcs.set_use_tls(buzz::TLS_DISABLED); + } else if (tls == "enable") { + xcs.set_use_tls(buzz::TLS_ENABLED); + } else if (tls == "require") { + xcs.set_use_tls(buzz::TLS_REQUIRED); + } else { + Print("Invalid TLS option, must be enable, disable, or require.\n"); + return 1; + } + + if (test_server) { + pass.password() = jid.node(); + xcs.set_allow_plain(true); + xcs.set_use_tls(buzz::TLS_DISABLED); + xcs.set_test_server_domain("google.com"); + } + xcs.set_pass(talk_base::CryptString(pass)); + if (!oauth_token.empty()) { + xcs.set_auth_token(buzz::AUTH_MECHANISM_OAUTH2, oauth_token); + } + + std::string host; + int port; + + int colon = server.find(':'); + if (colon == -1) { + host = server; + port = DEFAULT_PORT; + } else { + host = server.substr(0, colon); + port = atoi(server.substr(colon + 1).c_str()); + } + + xcs.set_server(talk_base::SocketAddress(host, port)); + + // Decide on the signaling and crypto settings. + cricket::SignalingProtocol signaling_protocol = cricket::PROTOCOL_HYBRID; + if (signaling == "jingle") { + signaling_protocol = cricket::PROTOCOL_JINGLE; + } else if (signaling == "gingle") { + signaling_protocol = cricket::PROTOCOL_GINGLE; + } else if (signaling == "hybrid") { + signaling_protocol = cricket::PROTOCOL_HYBRID; + } else { + Print("Invalid signaling protocol. Must be jingle, gingle, or hybrid.\n"); + return 1; + } + + cricket::TransportProtocol transport_protocol = cricket::ICEPROTO_HYBRID; + if (transport == "ice") { + transport_protocol = cricket::ICEPROTO_RFC5245; + } else if (transport == "gice") { + transport_protocol = cricket::ICEPROTO_GOOGLE; + } else if (transport == "hybrid") { + transport_protocol = cricket::ICEPROTO_HYBRID; + } else { + Print("Invalid transport protocol. Must be ice, gice, or hybrid.\n"); + return 1; + } + + cricket::DataChannelType data_channel_type = cricket::DCT_NONE; + if (data_channel == "rtp") { + data_channel_type = cricket::DCT_RTP; + } else if (data_channel == "sctp") { + data_channel_type = cricket::DCT_SCTP; + } else if (!data_channel.empty()) { + Print("Invalid data channel type. Must be rtp or sctp.\n"); + return 1; + } + + cricket::SecurePolicy sdes_policy, dtls_policy; + if (!GetSecurePolicy(sdes, &sdes_policy)) { + Print("Invalid SDES policy. Must be enable, disable, or require.\n"); + return 1; + } + if (!GetSecurePolicy(dtls, &dtls_policy)) { + Print("Invalid DTLS policy. Must be enable, disable, or require.\n"); + return 1; + } + if (dtls_policy != cricket::SEC_DISABLED) { + ssl_identity = talk_base::SSLIdentity::Generate(jid.Str()); + if (!ssl_identity) { + Print("Failed to generate identity for DTLS.\n"); + return 1; + } + } + +#ifdef ANDROID + InitAndroidMediaEngineFactory(AndroidMediaEngineFactory); +#endif + +#if WIN32 + // Need to pump messages on our main thread on Windows. + talk_base::Win32Thread w32_thread; + talk_base::ThreadManager::Instance()->SetCurrentThread(&w32_thread); +#endif + talk_base::Thread* main_thread = talk_base::Thread::Current(); +#ifdef OSX + talk_base::MacCocoaSocketServer ss; + talk_base::SocketServerScope ss_scope(&ss); +#endif + + buzz::XmppPump pump; + CallClient *client = new CallClient(pump.client(), caps_node, caps_ver); + + if (FLAG_voiceinput || FLAG_voiceoutput || + FLAG_videoinput || FLAG_videooutput) { + // If any dump file is specified, we use a FileMediaEngine. + cricket::MediaEngineInterface* engine = + MediaEngineFactory::CreateFileMediaEngine( + FLAG_voiceinput, FLAG_voiceoutput, + FLAG_videoinput, FLAG_videooutput); + client->SetMediaEngine(engine); + } + + Console *console = new Console(main_thread, client); + client->SetConsole(console); + client->SetAutoAccept(auto_accept); + client->SetPmucDomain(pmuc_domain); + client->SetPortAllocatorFlags(portallocator_flags); + client->SetAllowLocalIps(true); + client->SetSignalingProtocol(signaling_protocol); + client->SetTransportProtocol(transport_protocol); + client->SetSecurePolicy(sdes_policy, dtls_policy); + client->SetSslIdentity(ssl_identity); + client->SetRender(render); + client->SetDataChannelType(data_channel_type); + client->SetMultiSessionEnabled(multisession_enabled); + client->SetShowRosterMessages(show_roster_messages); + console->Start(); + + if (debug) { + pump.client()->SignalLogInput.connect(&debug_log_, &DebugLog::Input); + pump.client()->SignalLogOutput.connect(&debug_log_, &DebugLog::Output); + } + + Print(("Logging in to " + server + " as " + jid.Str() + "\n").c_str()); + pump.DoLogin(xcs, new buzz::XmppSocket(buzz::TLS_REQUIRED), new XmppAuth()); + main_thread->Run(); + pump.DoDisconnect(); + + console->Stop(); + delete console; + delete client; + + return 0; +} diff --git a/talk/examples/call/call_unittest.cc b/talk/examples/call/call_unittest.cc new file mode 100644 index 000000000..d95f1dd96 --- /dev/null +++ b/talk/examples/call/call_unittest.cc @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2008, 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. + */ + +// Main function for all unit tests in talk/examples/call + +#include "talk/base/logging.h" +#include "testing/base/public/gunit.h" + +int main(int argc, char **argv) { + talk_base::LogMessage::LogToDebug(talk_base::LogMessage::NO_LOGGING); + testing::ParseGUnitFlags(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/talk/examples/call/callclient.cc b/talk/examples/call/callclient.cc new file mode 100644 index 000000000..66c4b6fa6 --- /dev/null +++ b/talk/examples/call/callclient.cc @@ -0,0 +1,1615 @@ +/* + * libjingle + * Copyright 2004--2005, 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/examples/call/callclient.h" + +#include + +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/network.h" +#include "talk/base/socketaddress.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/base/windowpickerfactory.h" +#include "talk/examples/call/console.h" +#include "talk/examples/call/friendinvitesendtask.h" +#include "talk/examples/call/muc.h" +#include "talk/examples/call/mucinviterecvtask.h" +#include "talk/examples/call/mucinvitesendtask.h" +#include "talk/examples/call/presencepushtask.h" +#include "talk/media/base/mediacommon.h" +#include "talk/media/base/mediaengine.h" +#include "talk/media/base/rtpdataengine.h" +#include "talk/media/base/screencastid.h" +#ifdef HAVE_SCTP +#include "talk/media/sctp/sctpdataengine.h" +#endif +#include "talk/media/base/videorenderer.h" +#include "talk/media/devices/devicemanager.h" +#include "talk/media/devices/videorendererfactory.h" +#include "talk/p2p/base/sessionmanager.h" +#include "talk/p2p/client/basicportallocator.h" +#include "talk/p2p/client/sessionmanagertask.h" +#include "talk/session/media/mediamessages.h" +#include "talk/session/media/mediasessionclient.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/hangoutpubsubclient.h" +#include "talk/xmpp/mucroomconfigtask.h" +#include "talk/xmpp/mucroomlookuptask.h" +#include "talk/xmpp/presenceouttask.h" +#include "talk/xmpp/pingtask.h" + +namespace { + +// Must be period >= timeout. +const uint32 kPingPeriodMillis = 10000; +const uint32 kPingTimeoutMillis = 10000; + +const char* DescribeStatus(buzz::PresenceStatus::Show show, + const std::string& desc) { + switch (show) { + case buzz::PresenceStatus::SHOW_XA: return desc.c_str(); + case buzz::PresenceStatus::SHOW_ONLINE: return "online"; + case buzz::PresenceStatus::SHOW_AWAY: return "away"; + case buzz::PresenceStatus::SHOW_DND: return "do not disturb"; + case buzz::PresenceStatus::SHOW_CHAT: return "ready to chat"; + default: return "offline"; + } +} + +std::string GetWord(const std::vector& words, + size_t index, const std::string& def) { + if (words.size() > index) { + return words[index]; + } else { + return def; + } +} + +int GetInt(const std::vector& words, size_t index, int def) { + int val; + if (words.size() > index && talk_base::FromString(words[index], &val)) { + return val; + } else { + return def; + } +} + +} // namespace + +const char* CALL_COMMANDS = +"Available commands:\n" +"\n" +" hangup Ends the call.\n" +" hold Puts the current call on hold\n" +" calls Lists the current calls and their sessions\n" +" switch [call_id] Switch to the specified call\n" +" addsession [jid] Add a new session to the current call.\n" +" rmsession [sid] Remove specified session.\n" +" mute Stops sending voice.\n" +" unmute Re-starts sending voice.\n" +" vmute Stops sending video.\n" +" vunmute Re-starts sending video.\n" +" dtmf Sends a DTMF tone.\n" +" stats Print voice stats for the current call.\n" +" quit Quits the application.\n" +""; + +// TODO: Make present and record really work. +const char* HANGOUT_COMMANDS = +"Available MUC commands:\n" +"\n" +" present Starts presenting (just signalling; not actually presenting.)\n" +" unpresent Stops presenting (just signalling; not actually presenting.)\n" +" record Starts recording (just signalling; not actually recording.)\n" +" unrecord Stops recording (just signalling; not actually recording.)\n" +" rmute [nick] Remote mute another participant.\n" +" block [nick] Block another participant.\n" +" screencast [fps] Starts screencast. \n" +" unscreencast Stops screencast. \n" +" quit Quits the application.\n" +""; + +const char* RECEIVE_COMMANDS = +"Available commands:\n" +"\n" +" accept [bw] Accepts the incoming call and switches to it.\n" +" reject Rejects the incoming call and stays with the current call.\n" +" quit Quits the application.\n" +""; + +const char* CONSOLE_COMMANDS = +"Available commands:\n" +"\n" +" roster Prints the online friends from your roster.\n" +" friend user Request to add a user to your roster.\n" +" call [jid] [bw] Initiates a call to the user[/room] with the\n" +" given JID and with optional bandwidth.\n" +" vcall [jid] [bw] Initiates a video call to the user[/room] with\n" +" the given JID and with optional bandwidth.\n" +" calls Lists the current calls\n" +" switch [call_id] Switch to the specified call\n" +" join [room_jid] Joins a multi-user-chat with room JID.\n" +" ljoin [room_name] Joins a MUC by looking up JID from room name.\n" +" invite user [room] Invites a friend to a multi-user-chat.\n" +" leave [room] Leaves a multi-user-chat.\n" +" nick [nick] Sets the nick.\n" +" priority [int] Sets the priority.\n" +" getdevs Prints the available media devices.\n" +" quit Quits the application.\n" +""; + +void CallClient::ParseLine(const std::string& line) { + std::vector words; + int start = -1; + int state = 0; + for (int index = 0; index <= static_cast(line.size()); ++index) { + if (state == 0) { + if (!isspace(line[index])) { + start = index; + state = 1; + } + } else { + ASSERT(state == 1); + ASSERT(start >= 0); + if (isspace(line[index])) { + std::string word(line, start, index - start); + words.push_back(word); + start = -1; + state = 0; + } + } + } + + // Global commands + const std::string& command = GetWord(words, 0, ""); + if (command == "quit") { + Quit(); + } else if (call_ && incoming_call_) { + if (command == "accept") { + cricket::CallOptions options; + options.video_bandwidth = GetInt(words, 1, cricket::kAutoBandwidth); + options.has_video = true; + options.data_channel_type = data_channel_type_; + Accept(options); + } else if (command == "reject") { + Reject(); + } else { + console_->PrintLine(RECEIVE_COMMANDS); + } + } else if (call_) { + if (command == "hangup") { + call_->Terminate(); + } else if (command == "hold") { + media_client_->SetFocus(NULL); + call_ = NULL; + } else if (command == "addsession") { + std::string to = GetWord(words, 1, ""); + cricket::CallOptions options; + options.has_video = call_->has_video(); + options.video_bandwidth = cricket::kAutoBandwidth; + options.data_channel_type = data_channel_type_; + options.AddStream(cricket::MEDIA_TYPE_VIDEO, "", ""); + if (!InitiateAdditionalSession(to, options)) { + console_->PrintLine("Failed to initiate additional session."); + } + } else if (command == "rmsession") { + std::string id = GetWord(words, 1, ""); + TerminateAndRemoveSession(call_, id); + } else if (command == "calls") { + PrintCalls(); + } else if ((words.size() == 2) && (command == "switch")) { + SwitchToCall(GetInt(words, 1, -1)); + } else if (command == "mute") { + call_->Mute(true); + if (InMuc()) { + hangout_pubsub_client_->PublishAudioMuteState(true); + } + } else if (command == "unmute") { + call_->Mute(false); + if (InMuc()) { + hangout_pubsub_client_->PublishAudioMuteState(false); + } + } else if (command == "vmute") { + call_->MuteVideo(true); + if (InMuc()) { + hangout_pubsub_client_->PublishVideoMuteState(true); + } + } else if (command == "vunmute") { + call_->MuteVideo(false); + if (InMuc()) { + hangout_pubsub_client_->PublishVideoMuteState(false); + } + } else if (command == "screencast") { + if (screencast_ssrc_ != 0) { + console_->PrintLine("Can't screencast twice. Unscreencast first."); + } else { + std::string streamid = "screencast"; + screencast_ssrc_ = talk_base::CreateRandomId(); + int fps = GetInt(words, 1, 5); // Default to 5 fps. + + cricket::ScreencastId screencastid; + cricket::Session* session = GetFirstSession(); + if (session && SelectFirstDesktopScreencastId(&screencastid)) { + call_->StartScreencast( + session, streamid, screencast_ssrc_, screencastid, fps); + } + } + } else if (command == "unscreencast") { + // TODO: Use a random ssrc + std::string streamid = "screencast"; + + cricket::Session* session = GetFirstSession(); + if (session) { + call_->StopScreencast(session, streamid, screencast_ssrc_); + screencast_ssrc_ = 0; + } + } else if (command == "present") { + if (InMuc()) { + hangout_pubsub_client_->PublishPresenterState(true); + } + } else if (command == "unpresent") { + if (InMuc()) { + hangout_pubsub_client_->PublishPresenterState(false); + } + } else if (command == "record") { + if (InMuc()) { + hangout_pubsub_client_->PublishRecordingState(true); + } + } else if (command == "unrecord") { + if (InMuc()) { + hangout_pubsub_client_->PublishRecordingState(false); + } + } else if ((command == "rmute") && (words.size() == 2)) { + if (InMuc()) { + const std::string& nick = words[1]; + hangout_pubsub_client_->RemoteMute(nick); + } + } else if ((command == "block") && (words.size() == 2)) { + if (InMuc()) { + const std::string& nick = words[1]; + hangout_pubsub_client_->BlockMedia(nick); + } + } else if (command == "senddata") { + // "" is the default streamid. + SendData("", words[1]); + } else if ((command == "dtmf") && (words.size() == 2)) { + int ev = std::string("0123456789*#").find(words[1][0]); + call_->PressDTMF(ev); + } else if (command == "stats") { + PrintStats(); + } else { + console_->PrintLine(CALL_COMMANDS); + if (InMuc()) { + console_->PrintLine(HANGOUT_COMMANDS); + } + } + } else { + if (command == "roster") { + PrintRoster(); + } else if (command == "send") { + buzz::Jid jid(words[1]); + if (jid.IsValid()) { + last_sent_to_ = words[1]; + SendChat(words[1], words[2]); + } else if (!last_sent_to_.empty()) { + SendChat(last_sent_to_, words[1]); + } else { + console_->PrintLine( + "Invalid JID. JIDs should be in the form user@domain"); + } + } else if ((words.size() == 2) && (command == "friend")) { + InviteFriend(words[1]); + } else if (command == "call") { + std::string to = GetWord(words, 1, ""); + cricket::CallOptions options; + options.data_channel_type = data_channel_type_; + if (!PlaceCall(to, options)) { + console_->PrintLine("Failed to initiate call."); + } + } else if (command == "vcall") { + std::string to = GetWord(words, 1, ""); + int bandwidth = GetInt(words, 2, cricket::kAutoBandwidth); + cricket::CallOptions options; + options.has_video = true; + options.video_bandwidth = bandwidth; + options.data_channel_type = data_channel_type_; + if (!PlaceCall(to, options)) { + console_->PrintLine("Failed to initiate call."); + } + } else if (command == "calls") { + PrintCalls(); + } else if ((words.size() == 2) && (command == "switch")) { + SwitchToCall(GetInt(words, 1, -1)); + } else if (command == "join") { + JoinMuc(GetWord(words, 1, "")); + } else if (command == "ljoin") { + LookupAndJoinMuc(GetWord(words, 1, "")); + } else if ((words.size() >= 2) && (command == "invite")) { + InviteToMuc(words[1], GetWord(words, 2, "")); + } else if (command == "leave") { + LeaveMuc(GetWord(words, 1, "")); + } else if (command == "nick") { + SetNick(GetWord(words, 1, "")); + } else if (command == "priority") { + int priority = GetInt(words, 1, 0); + SetPriority(priority); + SendStatus(); + } else if (command == "getdevs") { + GetDevices(); + } else if ((words.size() == 2) && (command == "setvol")) { + SetVolume(words[1]); + } else { + console_->PrintLine(CONSOLE_COMMANDS); + } + } +} + +CallClient::CallClient(buzz::XmppClient* xmpp_client, + const std::string& caps_node, const std::string& version) + : xmpp_client_(xmpp_client), + worker_thread_(NULL), + media_engine_(NULL), + data_engine_(NULL), + media_client_(NULL), + call_(NULL), + hangout_pubsub_client_(NULL), + incoming_call_(false), + auto_accept_(false), + pmuc_domain_("groupchat.google.com"), + render_(true), + data_channel_type_(cricket::DCT_NONE), + multisession_enabled_(false), + local_renderer_(NULL), + static_views_accumulated_count_(0), + screencast_ssrc_(0), + roster_(new RosterMap), + portallocator_flags_(0), + allow_local_ips_(false), + signaling_protocol_(cricket::PROTOCOL_HYBRID), + transport_protocol_(cricket::ICEPROTO_HYBRID), + sdes_policy_(cricket::SEC_DISABLED), + dtls_policy_(cricket::SEC_DISABLED), + ssl_identity_(NULL), + show_roster_messages_(false) { + xmpp_client_->SignalStateChange.connect(this, &CallClient::OnStateChange); + my_status_.set_caps_node(caps_node); + my_status_.set_version(version); +} + +CallClient::~CallClient() { + delete media_client_; + delete roster_; + delete worker_thread_; +} + +const std::string CallClient::strerror(buzz::XmppEngine::Error err) { + switch (err) { + case buzz::XmppEngine::ERROR_NONE: + return ""; + case buzz::XmppEngine::ERROR_XML: + return "Malformed XML or encoding error"; + case buzz::XmppEngine::ERROR_STREAM: + return "XMPP stream error"; + case buzz::XmppEngine::ERROR_VERSION: + return "XMPP version error"; + case buzz::XmppEngine::ERROR_UNAUTHORIZED: + return "User is not authorized (Check your username and password)"; + case buzz::XmppEngine::ERROR_TLS: + return "TLS could not be negotiated"; + case buzz::XmppEngine::ERROR_AUTH: + return "Authentication could not be negotiated"; + case buzz::XmppEngine::ERROR_BIND: + return "Resource or session binding could not be negotiated"; + case buzz::XmppEngine::ERROR_CONNECTION_CLOSED: + return "Connection closed by output handler."; + case buzz::XmppEngine::ERROR_DOCUMENT_CLOSED: + return "Closed by "; + case buzz::XmppEngine::ERROR_SOCKET: + return "Socket error"; + default: + return "Unknown error"; + } +} + +void CallClient::OnCallDestroy(cricket::Call* call) { + RemoveCallsStaticRenderedViews(call); + if (call == call_) { + if (local_renderer_) { + delete local_renderer_; + local_renderer_ = NULL; + } + console_->PrintLine("call destroyed"); + call_ = NULL; + delete hangout_pubsub_client_; + hangout_pubsub_client_ = NULL; + } +} + +void CallClient::OnStateChange(buzz::XmppEngine::State state) { + switch (state) { + case buzz::XmppEngine::STATE_START: + console_->PrintLine("connecting..."); + break; + case buzz::XmppEngine::STATE_OPENING: + console_->PrintLine("logging in..."); + break; + case buzz::XmppEngine::STATE_OPEN: + console_->PrintLine("logged in..."); + InitMedia(); + InitPresence(); + break; + case buzz::XmppEngine::STATE_CLOSED: + { + buzz::XmppEngine::Error error = xmpp_client_->GetError(NULL); + console_->PrintLine("logged out... %s", strerror(error).c_str()); + Quit(); + } + break; + default: + break; + } +} + +void CallClient::InitMedia() { + worker_thread_ = new talk_base::Thread(); + // The worker thread must be started here since initialization of + // the ChannelManager will generate messages that need to be + // dispatched by it. + worker_thread_->Start(); + + // TODO: It looks like we are leaking many objects. E.g. + // |network_manager_| is never deleted. + network_manager_ = new talk_base::BasicNetworkManager(); + + // TODO: Decide if the relay address should be specified here. + talk_base::SocketAddress stun_addr("stun.l.google.com", 19302); + port_allocator_ = new cricket::BasicPortAllocator( + network_manager_, stun_addr, talk_base::SocketAddress(), + talk_base::SocketAddress(), talk_base::SocketAddress()); + + if (portallocator_flags_ != 0) { + port_allocator_->set_flags(portallocator_flags_); + } + session_manager_ = new cricket::SessionManager( + port_allocator_, worker_thread_); + session_manager_->set_secure(dtls_policy_); + session_manager_->set_identity(ssl_identity_.get()); + session_manager_->set_transport_protocol(transport_protocol_); + session_manager_->SignalRequestSignaling.connect( + this, &CallClient::OnRequestSignaling); + session_manager_->SignalSessionCreate.connect( + this, &CallClient::OnSessionCreate); + session_manager_->OnSignalingReady(); + + session_manager_task_ = + new cricket::SessionManagerTask(xmpp_client_, session_manager_); + session_manager_task_->EnableOutgoingMessages(); + session_manager_task_->Start(); + + if (!media_engine_) { + media_engine_ = cricket::MediaEngineFactory::Create(); + } + + if (!data_engine_) { + if (data_channel_type_ == cricket::DCT_SCTP) { +#ifdef HAVE_SCTP + data_engine_ = new cricket::SctpDataEngine(); +#else + LOG(LS_WARNING) << "SCTP Data Engine not supported."; + data_channel_type_ = cricket::DCT_NONE; + data_engine_ = new cricket::RtpDataEngine(); +#endif + } else { + // Even if we have DCT_NONE, we still have a data engine, just + // to make sure it isn't NULL. + data_engine_ = new cricket::RtpDataEngine(); + } + } + + media_client_ = new cricket::MediaSessionClient( + xmpp_client_->jid(), + session_manager_, + media_engine_, + data_engine_, + cricket::DeviceManagerFactory::Create()); + media_client_->SignalCallCreate.connect(this, &CallClient::OnCallCreate); + media_client_->SignalCallDestroy.connect(this, &CallClient::OnCallDestroy); + media_client_->SignalDevicesChange.connect(this, + &CallClient::OnDevicesChange); + media_client_->set_secure(sdes_policy_); + media_client_->set_multisession_enabled(multisession_enabled_); +} + +void CallClient::OnRequestSignaling() { + session_manager_->OnSignalingReady(); +} + +void CallClient::OnSessionCreate(cricket::Session* session, bool initiate) { + session->set_current_protocol(signaling_protocol_); +} + +void CallClient::OnCallCreate(cricket::Call* call) { + call->SignalSessionState.connect(this, &CallClient::OnSessionState); + call->SignalMediaStreamsUpdate.connect( + this, &CallClient::OnMediaStreamsUpdate); +} + +void CallClient::OnSessionState(cricket::Call* call, + cricket::Session* session, + cricket::Session::State state) { + if (state == cricket::Session::STATE_RECEIVEDINITIATE) { + buzz::Jid jid(session->remote_name()); + if (call_ == call && multisession_enabled_) { + // We've received an initiate for an existing call. This is actually a + // new session for that call. + console_->PrintLine("Incoming session from '%s'", jid.Str().c_str()); + AddSession(session); + + cricket::CallOptions options; + options.has_video = call_->has_video(); + options.data_channel_type = data_channel_type_; + call_->AcceptSession(session, options); + + if (call_->has_video() && render_) { + RenderAllStreams(call, session, true); + } + } else { + console_->PrintLine("Incoming call from '%s'", jid.Str().c_str()); + call_ = call; + AddSession(session); + incoming_call_ = true; + if (call->has_video() && render_) { + local_renderer_ = + cricket::VideoRendererFactory::CreateGuiVideoRenderer(160, 100); + } + if (auto_accept_) { + cricket::CallOptions options; + options.has_video = true; + options.data_channel_type = data_channel_type_; + Accept(options); + } + } + } else if (state == cricket::Session::STATE_SENTINITIATE) { + if (call->has_video() && render_) { + local_renderer_ = + cricket::VideoRendererFactory::CreateGuiVideoRenderer(160, 100); + } + console_->PrintLine("calling..."); + } else if (state == cricket::Session::STATE_RECEIVEDACCEPT) { + console_->PrintLine("call answered"); + SetupAcceptedCall(); + } else if (state == cricket::Session::STATE_RECEIVEDREJECT) { + console_->PrintLine("call not answered"); + } else if (state == cricket::Session::STATE_INPROGRESS) { + console_->PrintLine("call in progress"); + call->SignalSpeakerMonitor.connect(this, &CallClient::OnSpeakerChanged); + call->StartSpeakerMonitor(session); + } else if (state == cricket::Session::STATE_RECEIVEDTERMINATE) { + console_->PrintLine("other side terminated"); + TerminateAndRemoveSession(call, session->id()); + } +} + +void CallClient::OnSpeakerChanged(cricket::Call* call, + cricket::Session* session, + const cricket::StreamParams& speaker) { + if (!speaker.has_ssrcs()) { + console_->PrintLine("Session %s has no current speaker.", + session->id().c_str()); + } else if (speaker.id.empty()) { + console_->PrintLine("Session %s speaker change to unknown (%u).", + session->id().c_str(), speaker.first_ssrc()); + } else { + console_->PrintLine("Session %s speaker changed to %s (%u).", + session->id().c_str(), speaker.id.c_str(), + speaker.first_ssrc()); + } +} + +void SetMediaCaps(int media_caps, buzz::PresenceStatus* status) { + status->set_voice_capability((media_caps & cricket::AUDIO_RECV) != 0); + status->set_video_capability((media_caps & cricket::VIDEO_RECV) != 0); + status->set_camera_capability((media_caps & cricket::VIDEO_SEND) != 0); +} + +void SetCaps(int media_caps, buzz::PresenceStatus* status) { + status->set_know_capabilities(true); + status->set_pmuc_capability(true); + SetMediaCaps(media_caps, status); +} + +void SetAvailable(const buzz::Jid& jid, buzz::PresenceStatus* status) { + status->set_jid(jid); + status->set_available(true); + status->set_show(buzz::PresenceStatus::SHOW_ONLINE); +} + +void CallClient::InitPresence() { + presence_push_ = new buzz::PresencePushTask(xmpp_client_, this); + presence_push_->SignalStatusUpdate.connect( + this, &CallClient::OnStatusUpdate); + presence_push_->SignalMucJoined.connect(this, &CallClient::OnMucJoined); + presence_push_->SignalMucLeft.connect(this, &CallClient::OnMucLeft); + presence_push_->SignalMucStatusUpdate.connect( + this, &CallClient::OnMucStatusUpdate); + presence_push_->Start(); + + presence_out_ = new buzz::PresenceOutTask(xmpp_client_); + SetAvailable(xmpp_client_->jid(), &my_status_); + SetCaps(media_client_->GetCapabilities(), &my_status_); + SendStatus(my_status_); + presence_out_->Start(); + + muc_invite_recv_ = new buzz::MucInviteRecvTask(xmpp_client_); + muc_invite_recv_->SignalInviteReceived.connect(this, + &CallClient::OnMucInviteReceived); + muc_invite_recv_->Start(); + + muc_invite_send_ = new buzz::MucInviteSendTask(xmpp_client_); + muc_invite_send_->Start(); + + friend_invite_send_ = new buzz::FriendInviteSendTask(xmpp_client_); + friend_invite_send_->Start(); + + StartXmppPing(); +} + +void CallClient::StartXmppPing() { + buzz::PingTask* ping = new buzz::PingTask( + xmpp_client_, talk_base::Thread::Current(), + kPingPeriodMillis, kPingTimeoutMillis); + ping->SignalTimeout.connect(this, &CallClient::OnPingTimeout); + ping->Start(); +} + +void CallClient::OnPingTimeout() { + LOG(LS_WARNING) << "XMPP Ping timeout. Will keep trying..."; + StartXmppPing(); + + // Or should we do this instead? + // Quit(); +} + +void CallClient::SendStatus(const buzz::PresenceStatus& status) { + presence_out_->Send(status); +} + +void CallClient::OnStatusUpdate(const buzz::PresenceStatus& status) { + RosterItem item; + item.jid = status.jid(); + item.show = status.show(); + item.status = status.status(); + + std::string key = item.jid.Str(); + + if (status.available() && status.voice_capability()) { + if (show_roster_messages_) { + console_->PrintLine("Adding to roster: %s", key.c_str()); + } + (*roster_)[key] = item; + // TODO: Make some of these constants. + } else { + if (show_roster_messages_) { + console_->PrintLine("Removing from roster: %s", key.c_str()); + } + RosterMap::iterator iter = roster_->find(key); + if (iter != roster_->end()) + roster_->erase(iter); + } +} + +void CallClient::PrintRoster() { + console_->PrintLine("Roster contains %d callable", roster_->size()); + RosterMap::iterator iter = roster_->begin(); + while (iter != roster_->end()) { + console_->PrintLine("%s - %s", + iter->second.jid.BareJid().Str().c_str(), + DescribeStatus(iter->second.show, iter->second.status)); + iter++; + } +} + +void CallClient::SendChat(const std::string& to, const std::string msg) { + buzz::XmlElement* stanza = new buzz::XmlElement(buzz::QN_MESSAGE); + stanza->AddAttr(buzz::QN_TO, to); + stanza->AddAttr(buzz::QN_ID, talk_base::CreateRandomString(16)); + stanza->AddAttr(buzz::QN_TYPE, "chat"); + buzz::XmlElement* body = new buzz::XmlElement(buzz::QN_BODY); + body->SetBodyText(msg); + stanza->AddElement(body); + + xmpp_client_->SendStanza(stanza); + delete stanza; +} + +void CallClient::SendData(const std::string& streamid, + const std::string& text) { + // TODO(mylesj): Support sending data over sessions other than the first. + cricket::Session* session = GetFirstSession(); + if (!call_ || !session) { + console_->PrintLine("Must be in a call to send data."); + return; + } + if (!call_->has_data()) { + console_->PrintLine("This call doesn't have a data channel."); + return; + } + + const cricket::DataContentDescription* data = + cricket::GetFirstDataContentDescription(session->local_description()); + if (!data) { + console_->PrintLine("This call doesn't have a data content."); + return; + } + + cricket::StreamParams stream; + if (!cricket::GetStreamByIds( + data->streams(), "", streamid, &stream)) { + LOG(LS_WARNING) << "Could not send data: no such stream: " + << streamid << "."; + return; + } + + cricket::SendDataParams params; + params.ssrc = stream.first_ssrc(); + talk_base::Buffer payload(text.data(), text.length()); + cricket::SendDataResult result; + bool sent = call_->SendData(session, params, payload, &result); + if (!sent) { + if (result == cricket::SDR_BLOCK) { + LOG(LS_WARNING) << "Could not send data because it would block."; + } else { + LOG(LS_WARNING) << "Could not send data for unknown reason."; + } + } +} + +void CallClient::InviteFriend(const std::string& name) { + buzz::Jid jid(name); + if (!jid.IsValid() || jid.node() == "") { + console_->PrintLine("Invalid JID. JIDs should be in the form user@domain."); + return; + } + // Note: for some reason the Buzz backend does not forward our presence + // subscription requests to the end user when that user is another call + // client as opposed to a Smurf user. Thus, in that scenario, you must + // run the friend command as the other user too to create the linkage + // (and you won't be notified to do so). + friend_invite_send_->Send(jid); + console_->PrintLine("Requesting to befriend %s.", name.c_str()); +} + +bool CallClient::FindJid(const std::string& name, buzz::Jid* found_jid, + cricket::CallOptions* options) { + bool found = false; + options->is_muc = false; + buzz::Jid callto_jid(name); + if (name.length() == 0 && mucs_.size() > 0) { + // if no name, and in a MUC, establish audio with the MUC + *found_jid = mucs_.begin()->first; + found = true; + options->is_muc = true; + } else if (name[0] == '+') { + // if the first character is a +, assume it's a phone number + *found_jid = callto_jid; + found = true; + } else { + // otherwise, it's a friend + for (RosterMap::iterator iter = roster_->begin(); + iter != roster_->end(); ++iter) { + if (iter->second.jid.BareEquals(callto_jid)) { + found = true; + *found_jid = iter->second.jid; + break; + } + } + + if (!found) { + if (mucs_.count(callto_jid) == 1 && + mucs_[callto_jid]->state() == buzz::Muc::MUC_JOINED) { + found = true; + *found_jid = callto_jid; + options->is_muc = true; + } + } + } + + if (found) { + console_->PrintLine("Found %s '%s'", + options->is_muc ? "room" : "online friend", + found_jid->Str().c_str()); + } else { + console_->PrintLine("Could not find online friend '%s'", name.c_str()); + } + + return found; +} + +void CallClient::OnDataReceived(cricket::Call*, + const cricket::ReceiveDataParams& params, + const talk_base::Buffer& payload) { + // TODO(mylesj): Support receiving data on sessions other than the first. + cricket::Session* session = GetFirstSession(); + if (!session) + return; + + cricket::StreamParams stream; + const std::vector* data_streams = + call_->GetDataRecvStreams(session); + std::string text(payload.data(), payload.length()); + if (data_streams && GetStreamBySsrc(*data_streams, params.ssrc, &stream)) { + console_->PrintLine( + "Received data from '%s' on stream '%s' (ssrc=%u): %s", + stream.groupid.c_str(), stream.id.c_str(), + params.ssrc, text.c_str()); + } else { + console_->PrintLine( + "Received data (ssrc=%u): %s", + params.ssrc, text.c_str()); + } +} + +bool CallClient::PlaceCall(const std::string& name, + cricket::CallOptions options) { + buzz::Jid jid; + if (!FindJid(name, &jid, &options)) + return false; + + if (!call_) { + call_ = media_client_->CreateCall(); + AddSession(call_->InitiateSession(jid, media_client_->jid(), options)); + } + media_client_->SetFocus(call_); + if (call_->has_video() && render_) { + if (!options.is_muc) { + call_->SetLocalRenderer(local_renderer_); + } + } + if (options.is_muc) { + const std::string& nick = mucs_[jid]->local_jid().resource(); + hangout_pubsub_client_ = + new buzz::HangoutPubSubClient(xmpp_client_, jid, nick); + hangout_pubsub_client_->SignalPresenterStateChange.connect( + this, &CallClient::OnPresenterStateChange); + hangout_pubsub_client_->SignalAudioMuteStateChange.connect( + this, &CallClient::OnAudioMuteStateChange); + hangout_pubsub_client_->SignalRecordingStateChange.connect( + this, &CallClient::OnRecordingStateChange); + hangout_pubsub_client_->SignalRemoteMute.connect( + this, &CallClient::OnRemoteMuted); + hangout_pubsub_client_->SignalMediaBlock.connect( + this, &CallClient::OnMediaBlocked); + hangout_pubsub_client_->SignalRequestError.connect( + this, &CallClient::OnHangoutRequestError); + hangout_pubsub_client_->SignalPublishAudioMuteError.connect( + this, &CallClient::OnHangoutPublishAudioMuteError); + hangout_pubsub_client_->SignalPublishPresenterError.connect( + this, &CallClient::OnHangoutPublishPresenterError); + hangout_pubsub_client_->SignalPublishRecordingError.connect( + this, &CallClient::OnHangoutPublishRecordingError); + hangout_pubsub_client_->SignalRemoteMuteError.connect( + this, &CallClient::OnHangoutRemoteMuteError); + hangout_pubsub_client_->RequestAll(); + } + + return true; +} + +bool CallClient::InitiateAdditionalSession(const std::string& name, + cricket::CallOptions options) { + // Can't add a session if there is no call yet. + if (!call_) + return false; + + buzz::Jid jid; + if (!FindJid(name, &jid, &options)) + return false; + + std::vector& call_sessions = sessions_[call_->id()]; + call_sessions.push_back( + call_->InitiateSession(jid, + buzz::Jid(call_sessions[0]->remote_name()), + options)); + + return true; +} + +void CallClient::TerminateAndRemoveSession(cricket::Call* call, + const std::string& id) { + std::vector& call_sessions = sessions_[call->id()]; + for (std::vector::iterator iter = call_sessions.begin(); + iter != call_sessions.end(); ++iter) { + if ((*iter)->id() == id) { + RenderAllStreams(call, *iter, false); + call_->TerminateSession(*iter); + call_sessions.erase(iter); + break; + } + } +} + +void CallClient::PrintCalls() { + const std::map& calls = media_client_->calls(); + for (std::map::const_iterator i = calls.begin(); + i != calls.end(); ++i) { + console_->PrintLine("Call (id:%d), is %s", + i->first, + i->second == call_ ? "active" : "on hold"); + std::vector& sessions = sessions_[call_->id()]; + for (std::vector::const_iterator j = sessions.begin(); + j != sessions.end(); ++j) { + console_->PrintLine("|--Session (id:%s), to %s", (*j)->id().c_str(), + (*j)->remote_name().c_str()); + + std::vector::const_iterator k; + const std::vector* streams = + i->second->GetAudioRecvStreams(*j); + if (streams) + for (k = streams->begin(); k != streams->end(); ++k) { + console_->PrintLine("|----Audio Stream: %s", k->ToString().c_str()); + } + streams = i->second->GetVideoRecvStreams(*j); + if (streams) + for (k = streams->begin(); k != streams->end(); ++k) { + console_->PrintLine("|----Video Stream: %s", k->ToString().c_str()); + } + streams = i->second->GetDataRecvStreams(*j); + if (streams) + for (k = streams->begin(); k != streams->end(); ++k) { + console_->PrintLine("|----Data Stream: %s", k->ToString().c_str()); + } + } + } +} + +void CallClient::SwitchToCall(uint32 call_id) { + const std::map& calls = media_client_->calls(); + std::map::const_iterator call_iter = + calls.find(call_id); + if (call_iter != calls.end()) { + media_client_->SetFocus(call_iter->second); + call_ = call_iter->second; + } else { + console_->PrintLine("Unable to find call: %d", call_id); + } +} + +void CallClient::OnPresenterStateChange( + const std::string& nick, bool was_presenting, bool is_presenting) { + if (!was_presenting && is_presenting) { + console_->PrintLine("%s now presenting.", nick.c_str()); + } else if (was_presenting && !is_presenting) { + console_->PrintLine("%s no longer presenting.", nick.c_str()); + } else if (was_presenting && is_presenting) { + console_->PrintLine("%s still presenting.", nick.c_str()); + } else if (!was_presenting && !is_presenting) { + console_->PrintLine("%s still not presenting.", nick.c_str()); + } +} + +void CallClient::OnAudioMuteStateChange( + const std::string& nick, bool was_muted, bool is_muted) { + if (!was_muted && is_muted) { + console_->PrintLine("%s now muted.", nick.c_str()); + } else if (was_muted && !is_muted) { + console_->PrintLine("%s no longer muted.", nick.c_str()); + } +} + +void CallClient::OnRecordingStateChange( + const std::string& nick, bool was_recording, bool is_recording) { + if (!was_recording && is_recording) { + console_->PrintLine("%s now recording.", nick.c_str()); + } else if (was_recording && !is_recording) { + console_->PrintLine("%s no longer recording.", nick.c_str()); + } +} + +void CallClient::OnRemoteMuted(const std::string& mutee_nick, + const std::string& muter_nick, + bool should_mute_locally) { + if (should_mute_locally) { + call_->Mute(true); + console_->PrintLine("Remote muted by %s.", muter_nick.c_str()); + } else { + console_->PrintLine("%s remote muted by %s.", + mutee_nick.c_str(), muter_nick.c_str()); + } +} + +void CallClient::OnMediaBlocked(const std::string& blockee_nick, + const std::string& blocker_nick) { + console_->PrintLine("%s blocked by %s.", + blockee_nick.c_str(), blocker_nick.c_str()); +} + +void CallClient::OnHangoutRequestError(const std::string& node, + const buzz::XmlElement* stanza) { + console_->PrintLine("Failed request pub sub items for node %s.", + node.c_str()); +} + +void CallClient::OnHangoutPublishAudioMuteError( + const std::string& task_id, const buzz::XmlElement* stanza) { + console_->PrintLine("Failed to publish audio mute state."); +} + +void CallClient::OnHangoutPublishPresenterError( + const std::string& task_id, const buzz::XmlElement* stanza) { + console_->PrintLine("Failed to publish presenting state."); +} + +void CallClient::OnHangoutPublishRecordingError( + const std::string& task_id, const buzz::XmlElement* stanza) { + console_->PrintLine("Failed to publish recording state."); +} + +void CallClient::OnHangoutRemoteMuteError(const std::string& task_id, + const std::string& mutee_nick, + const buzz::XmlElement* stanza) { + console_->PrintLine("Failed to remote mute."); +} + +void CallClient::Accept(const cricket::CallOptions& options) { + ASSERT(call_ && incoming_call_); + ASSERT(sessions_[call_->id()].size() == 1); + cricket::Session* session = GetFirstSession(); + call_->AcceptSession(session, options); + media_client_->SetFocus(call_); + if (call_->has_video() && render_) { + call_->SetLocalRenderer(local_renderer_); + RenderAllStreams(call_, session, true); + } + SetupAcceptedCall(); + incoming_call_ = false; +} + +void CallClient::SetupAcceptedCall() { + if (call_->has_data()) { + call_->SignalDataReceived.connect(this, &CallClient::OnDataReceived); + } +} + +void CallClient::Reject() { + ASSERT(call_ && incoming_call_); + call_->RejectSession(call_->sessions()[0]); + incoming_call_ = false; +} + +void CallClient::Quit() { + talk_base::Thread::Current()->Quit(); +} + +void CallClient::SetNick(const std::string& muc_nick) { + my_status_.set_nick(muc_nick); + + // TODO: We might want to re-send presence, but right + // now, it appears to be ignored by the MUC. + // + // presence_out_->Send(my_status_); for (MucMap::const_iterator itr + // = mucs_.begin(); itr != mucs_.end(); ++itr) { + // presence_out_->SendDirected(itr->second->local_jid(), + // my_status_); } + + console_->PrintLine("Nick set to '%s'.", muc_nick.c_str()); +} + +void CallClient::LookupAndJoinMuc(const std::string& room_name) { + // The room_name can't be empty for lookup task. + if (room_name.empty()) { + console_->PrintLine("Please provide a room name or room jid."); + return; + } + + std::string room = room_name; + std::string domain = xmpp_client_->jid().domain(); + if (room_name.find("@") != std::string::npos) { + // Assume the room_name is a fully qualified room name. + // We'll find the room name string and domain name string from it. + room = room_name.substr(0, room_name.find("@")); + domain = room_name.substr(room_name.find("@") + 1); + } + + buzz::MucRoomLookupTask* lookup_query_task = + buzz::MucRoomLookupTask::CreateLookupTaskForRoomName( + xmpp_client_, buzz::Jid(buzz::STR_GOOGLE_MUC_LOOKUP_JID), room, + domain); + lookup_query_task->SignalResult.connect(this, + &CallClient::OnRoomLookupResponse); + lookup_query_task->SignalError.connect(this, + &CallClient::OnRoomLookupError); + lookup_query_task->Start(); +} + +void CallClient::JoinMuc(const std::string& room_jid_str) { + if (room_jid_str.empty()) { + buzz::Jid room_jid = GenerateRandomMucJid(); + console_->PrintLine("Generated a random room jid: %s", + room_jid.Str().c_str()); + JoinMuc(room_jid); + } else { + JoinMuc(buzz::Jid(room_jid_str)); + } +} + +void CallClient::JoinMuc(const buzz::Jid& room_jid) { + if (!room_jid.IsValid()) { + console_->PrintLine("Unable to make valid muc endpoint for %s", + room_jid.Str().c_str()); + return; + } + + std::string room_nick = room_jid.resource(); + if (room_nick.empty()) { + room_nick = (xmpp_client_->jid().node() + + "_" + xmpp_client_->jid().resource()); + } + + MucMap::iterator elem = mucs_.find(room_jid); + if (elem != mucs_.end()) { + console_->PrintLine("This MUC already exists."); + return; + } + + buzz::Muc* muc = new buzz::Muc(room_jid.BareJid(), room_nick); + mucs_[muc->jid()] = muc; + presence_out_->SendDirected(muc->local_jid(), my_status_); +} + +void CallClient::OnRoomLookupResponse(buzz::MucRoomLookupTask* task, + const buzz::MucRoomInfo& room) { + // The server requires the room be "configured" before being used. + // We only need to configure it if we create it, but rooms are + // auto-created at lookup, so there's currently no way to know if we + // created it. So, we configure it every time, just in case. + // Luckily, it appears to be safe to configure a room that's already + // configured. Our current flow is: + // 1. Lookup/auto-create + // 2. Configure + // 3. Join + // TODO: In the future, once the server supports it, we + // should: + // 1. Lookup + // 2. Create and Configure if necessary + // 3. Join + std::vector room_features; + room_features.push_back(buzz::STR_MUC_ROOM_FEATURE_ENTERPRISE); + buzz::MucRoomConfigTask* room_config_task = new buzz::MucRoomConfigTask( + xmpp_client_, room.jid, room.full_name(), room_features); + room_config_task->SignalResult.connect(this, + &CallClient::OnRoomConfigResult); + room_config_task->SignalError.connect(this, + &CallClient::OnRoomConfigError); + room_config_task->Start(); +} + +void CallClient::OnRoomLookupError(buzz::IqTask* task, + const buzz::XmlElement* stanza) { + if (stanza == NULL) { + console_->PrintLine("Room lookup failed."); + } else { + console_->PrintLine("Room lookup error: ", stanza->Str().c_str()); + } +} + +void CallClient::OnRoomConfigResult(buzz::MucRoomConfigTask* task) { + JoinMuc(task->room_jid()); +} + +void CallClient::OnRoomConfigError(buzz::IqTask* task, + const buzz::XmlElement* stanza) { + console_->PrintLine("Room config failed."); + // We join the muc anyway, because if the room is already + // configured, the configure will fail, but we still want to join. + // Idealy, we'd know why the room config failed and only do this on + // "already configured" errors. But right now all we get back is + // "not-allowed". + buzz::MucRoomConfigTask* config_task = + static_cast(task); + JoinMuc(config_task->room_jid()); +} + +void CallClient::OnMucInviteReceived(const buzz::Jid& inviter, + const buzz::Jid& room, + const std::vector& avail) { + + console_->PrintLine("Invited to join %s by %s.", room.Str().c_str(), + inviter.Str().c_str()); + console_->PrintLine("Available media:"); + if (avail.size() > 0) { + for (std::vector::const_iterator i = + avail.begin(); + i != avail.end(); + ++i) { + console_->PrintLine(" %s, %s", + buzz::AvailableMediaEntry::TypeAsString(i->type), + buzz::AvailableMediaEntry::StatusAsString(i->status)); + } + } else { + console_->PrintLine(" None"); + } + // We automatically join the room. + JoinMuc(room); +} + +void CallClient::OnMucJoined(const buzz::Jid& endpoint) { + MucMap::iterator elem = mucs_.find(endpoint); + ASSERT(elem != mucs_.end() && + elem->second->state() == buzz::Muc::MUC_JOINING); + + buzz::Muc* muc = elem->second; + muc->set_state(buzz::Muc::MUC_JOINED); + console_->PrintLine("Joined \"%s\"", muc->jid().Str().c_str()); +} + +void CallClient::OnMucStatusUpdate(const buzz::Jid& jid, + const buzz::MucPresenceStatus& status) { + + // Look up this muc. + MucMap::iterator elem = mucs_.find(jid); + ASSERT(elem != mucs_.end()); + + buzz::Muc* muc = elem->second; + + if (status.jid().IsBare() || status.jid() == muc->local_jid()) { + // We are only interested in status about other users. + return; + } + + if (!status.available()) { + // Remove them from the room. + muc->members().erase(status.jid().resource()); + } +} + +bool CallClient::InMuc() { + const buzz::Jid* muc_jid = FirstMucJid(); + if (!muc_jid) return false; + return muc_jid->IsValid(); +} + +const buzz::Jid* CallClient::FirstMucJid() { + if (mucs_.empty()) return NULL; + return &(mucs_.begin()->first); +} + +void CallClient::LeaveMuc(const std::string& room) { + buzz::Jid room_jid; + const buzz::Jid* muc_jid = FirstMucJid(); + if (room.length() > 0) { + room_jid = buzz::Jid(room); + } else if (mucs_.size() > 0) { + // leave the first MUC if no JID specified + if (muc_jid) { + room_jid = *(muc_jid); + } + } + + if (!room_jid.IsValid()) { + console_->PrintLine("Invalid MUC JID."); + return; + } + + MucMap::iterator elem = mucs_.find(room_jid); + if (elem == mucs_.end()) { + console_->PrintLine("No such MUC."); + return; + } + + buzz::Muc* muc = elem->second; + muc->set_state(buzz::Muc::MUC_LEAVING); + + buzz::PresenceStatus status; + status.set_jid(my_status_.jid()); + status.set_available(false); + status.set_priority(0); + presence_out_->SendDirected(muc->local_jid(), status); +} + +void CallClient::OnMucLeft(const buzz::Jid& endpoint, int error) { + // We could be kicked from a room from any state. We would hope this + // happens While in the MUC_LEAVING state + MucMap::iterator elem = mucs_.find(endpoint); + if (elem == mucs_.end()) + return; + + buzz::Muc* muc = elem->second; + if (muc->state() == buzz::Muc::MUC_JOINING) { + console_->PrintLine("Failed to join \"%s\", code=%d", + muc->jid().Str().c_str(), error); + } else if (muc->state() == buzz::Muc::MUC_JOINED) { + console_->PrintLine("Kicked from \"%s\"", + muc->jid().Str().c_str()); + } + + delete muc; + mucs_.erase(elem); +} + +void CallClient::InviteToMuc(const std::string& given_user, + const std::string& room) { + std::string user = given_user; + + // First find the room. + const buzz::Muc* found_muc; + if (room.length() == 0) { + if (mucs_.size() == 0) { + console_->PrintLine("Not in a room yet; can't invite."); + return; + } + // Invite to the first muc + found_muc = mucs_.begin()->second; + } else { + MucMap::iterator elem = mucs_.find(buzz::Jid(room)); + if (elem == mucs_.end()) { + console_->PrintLine("Not in room %s.", room.c_str()); + return; + } + found_muc = elem->second; + } + + buzz::Jid invite_to = found_muc->jid(); + + // Now find the user. We invite all of their resources. + bool found_user = false; + buzz::Jid user_jid(user); + for (RosterMap::iterator iter = roster_->begin(); + iter != roster_->end(); ++iter) { + if (iter->second.jid.BareEquals(user_jid)) { + buzz::Jid invitee = iter->second.jid; + muc_invite_send_->Send(invite_to, invitee); + found_user = true; + } + } + if (!found_user) { + buzz::Jid invitee = user_jid; + muc_invite_send_->Send(invite_to, invitee); + } +} + +void CallClient::GetDevices() { + std::vector names; + media_client_->GetAudioInputDevices(&names); + console_->PrintLine("Audio input devices:"); + PrintDevices(names); + media_client_->GetAudioOutputDevices(&names); + console_->PrintLine("Audio output devices:"); + PrintDevices(names); + media_client_->GetVideoCaptureDevices(&names); + console_->PrintLine("Video capture devices:"); + PrintDevices(names); +} + +void CallClient::PrintDevices(const std::vector& names) { + for (size_t i = 0; i < names.size(); ++i) { + console_->PrintLine("%d: %s", static_cast(i), names[i].c_str()); + } +} + +void CallClient::OnDevicesChange() { + console_->PrintLine("Devices changed."); + SetMediaCaps(media_client_->GetCapabilities(), &my_status_); + SendStatus(my_status_); +} + +void CallClient::SetVolume(const std::string& level) { + media_client_->SetOutputVolume(strtol(level.c_str(), NULL, 10)); +} + +void CallClient::OnMediaStreamsUpdate(cricket::Call* call, + cricket::Session* session, + const cricket::MediaStreams& added, + const cricket::MediaStreams& removed) { + if (call && call->has_video()) { + for (std::vector::const_iterator + it = removed.video().begin(); it != removed.video().end(); ++it) { + RemoveStaticRenderedView(it->first_ssrc()); + } + + if (render_) { + RenderStreams(call, session, added.video(), true); + } + SendViewRequest(call, session); + } +} + +void CallClient::RenderAllStreams(cricket::Call* call, + cricket::Session* session, + bool enable) { + const std::vector* video_streams = + call->GetVideoRecvStreams(session); + if (video_streams) { + RenderStreams(call, session, *video_streams, enable); + } +} + +void CallClient::RenderStreams( + cricket::Call* call, + cricket::Session* session, + const std::vector& video_streams, + bool enable) { + std::vector::const_iterator stream; + for (stream = video_streams.begin(); stream != video_streams.end(); + ++stream) { + RenderStream(call, session, *stream, enable); + } +} + +void CallClient::RenderStream(cricket::Call* call, + cricket::Session* session, + const cricket::StreamParams& stream, + bool enable) { + if (!stream.has_ssrcs()) { + // Nothing to see here; move along. + return; + } + + uint32 ssrc = stream.first_ssrc(); + StaticRenderedViews::iterator iter = + static_rendered_views_.find(std::make_pair(session, ssrc)); + if (enable) { + if (iter == static_rendered_views_.end()) { + // TODO(pthatcher): Make dimensions and positions more configurable. + int offset = (50 * static_views_accumulated_count_) % 300; + AddStaticRenderedView(session, ssrc, 640, 400, 30, + offset, offset); + // Should have it now. + iter = static_rendered_views_.find(std::make_pair(session, ssrc)); + } + call->SetVideoRenderer(session, ssrc, iter->second.renderer); + } else { + if (iter != static_rendered_views_.end()) { + call->SetVideoRenderer(session, ssrc, NULL); + RemoveStaticRenderedView(ssrc); + } + } +} + +// TODO: Would these methods to add and remove views make +// more sense in call.cc? Would other clients use them? +void CallClient::AddStaticRenderedView( + cricket::Session* session, + uint32 ssrc, int width, int height, int framerate, + int x_offset, int y_offset) { + StaticRenderedView rendered_view( + cricket::StaticVideoView( + cricket::StreamSelector(ssrc), width, height, framerate), + cricket::VideoRendererFactory::CreateGuiVideoRenderer( + x_offset, y_offset)); + rendered_view.renderer->SetSize(width, height, 0); + static_rendered_views_.insert(std::make_pair(std::make_pair(session, ssrc), + rendered_view)); + ++static_views_accumulated_count_; + console_->PrintLine("Added renderer for ssrc %d", ssrc); +} + +bool CallClient::RemoveStaticRenderedView(uint32 ssrc) { + for (StaticRenderedViews::iterator it = static_rendered_views_.begin(); + it != static_rendered_views_.end(); ++it) { + if (it->second.view.selector.ssrc == ssrc) { + delete it->second.renderer; + static_rendered_views_.erase(it); + console_->PrintLine("Removed renderer for ssrc %d", ssrc); + return true; + } + } + return false; +} + +void CallClient::RemoveCallsStaticRenderedViews(cricket::Call* call) { + std::vector& sessions = sessions_[call->id()]; + std::set call_sessions(sessions.begin(), sessions.end()); + for (StaticRenderedViews::iterator it = static_rendered_views_.begin(); + it != static_rendered_views_.end(); ) { + if (call_sessions.find(it->first.first) != call_sessions.end()) { + delete it->second.renderer; + static_rendered_views_.erase(it++); + } else { + ++it; + } + } +} + +void CallClient::SendViewRequest(cricket::Call* call, + cricket::Session* session) { + cricket::ViewRequest request; + for (StaticRenderedViews::iterator it = static_rendered_views_.begin(); + it != static_rendered_views_.end(); ++it) { + if (it->first.first == session) { + request.static_video_views.push_back(it->second.view); + } + } + call->SendViewRequest(session, request); +} + +buzz::Jid CallClient::GenerateRandomMucJid() { + // Generate a GUID of the form XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX, + // for an eventual JID of private-chat-@groupchat.google.com. + char guid[37], guid_room[256]; + for (size_t i = 0; i < ARRAY_SIZE(guid) - 1;) { + if (i == 8 || i == 13 || i == 18 || i == 23) { + guid[i++] = '-'; + } else { + sprintf(guid + i, "%04x", rand()); + i += 4; + } + } + + talk_base::sprintfn(guid_room, + ARRAY_SIZE(guid_room), + "private-chat-%s@%s", + guid, + pmuc_domain_.c_str()); + return buzz::Jid(guid_room); +} + +bool CallClient::SelectFirstDesktopScreencastId( + cricket::ScreencastId* screencastid) { + if (!talk_base::WindowPickerFactory::IsSupported()) { + LOG(LS_WARNING) << "Window picker not suported on this OS."; + return false; + } + + talk_base::WindowPicker* picker = + talk_base::WindowPickerFactory::CreateWindowPicker(); + if (!picker) { + LOG(LS_WARNING) << "Could not create a window picker."; + return false; + } + + talk_base::DesktopDescriptionList desktops; + if (!picker->GetDesktopList(&desktops) || desktops.empty()) { + LOG(LS_WARNING) << "Could not get a list of desktops."; + return false; + } + + *screencastid = cricket::ScreencastId(desktops[0].id()); + return true; +} + +void CallClient::PrintStats() const { + const cricket::VoiceMediaInfo& vmi = call_->last_voice_media_info(); + + for (std::vector::const_iterator it = + vmi.senders.begin(); it != vmi.senders.end(); ++it) { + console_->PrintLine("Sender: ssrc=%u codec='%s' bytes=%d packets=%d " + "rtt=%d jitter=%d", + it->ssrc, it->codec_name.c_str(), it->bytes_sent, + it->packets_sent, it->rtt_ms, it->jitter_ms); + } + + for (std::vector::const_iterator it = + vmi.receivers.begin(); it != vmi.receivers.end(); ++it) { + console_->PrintLine("Receiver: ssrc=%u bytes=%d packets=%d " + "jitter=%d loss=%.2f", + it->ssrc, it->bytes_rcvd, it->packets_rcvd, + it->jitter_ms, it->fraction_lost); + } +} diff --git a/talk/examples/call/callclient.h b/talk/examples/call/callclient.h new file mode 100644 index 000000000..39a5b11fe --- /dev/null +++ b/talk/examples/call/callclient.h @@ -0,0 +1,352 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_EXAMPLES_CALL_CALLCLIENT_H_ +#define TALK_EXAMPLES_CALL_CALLCLIENT_H_ + +#include +#include +#include + +#include "talk/base/scoped_ptr.h" +#include "talk/base/sslidentity.h" +#include "talk/examples/call/console.h" +#include "talk/media/base/mediachannel.h" +#include "talk/p2p/base/session.h" +#include "talk/session/media/mediamessages.h" +#include "talk/session/media/mediasessionclient.h" +#include "talk/xmpp/hangoutpubsubclient.h" +#include "talk/xmpp/presencestatus.h" +#include "talk/xmpp/xmppclient.h" + +namespace buzz { +class PresencePushTask; +class PresenceOutTask; +class MucInviteRecvTask; +class MucInviteSendTask; +class FriendInviteSendTask; +class DiscoInfoQueryTask; +class Muc; +class PresenceStatus; +class IqTask; +class MucRoomConfigTask; +class MucRoomLookupTask; +class MucPresenceStatus; +class XmlElement; +class HangoutPubSubClient; +struct AvailableMediaEntry; +struct MucRoomInfo; +} // namespace buzz + +namespace talk_base { +class Thread; +class NetworkManager; +} // namespace talk_base + +namespace cricket { +class PortAllocator; +class MediaEngineInterface; +class MediaSessionClient; +class Call; +class SessionManagerTask; +struct CallOptions; +struct MediaStreams; +struct StreamParams; +} // namespace cricket + +struct RosterItem { + buzz::Jid jid; + buzz::PresenceStatus::Show show; + std::string status; +}; + +struct StaticRenderedView { + StaticRenderedView(const cricket::StaticVideoView& view, + cricket::VideoRenderer* renderer) : + view(view), + renderer(renderer) { + } + + cricket::StaticVideoView view; + cricket::VideoRenderer* renderer; +}; + +// Maintain a mapping of (session, ssrc) to rendered view. +typedef std::map, + StaticRenderedView> StaticRenderedViews; + +class CallClient: public sigslot::has_slots<> { + public: + CallClient(buzz::XmppClient* xmpp_client, + const std::string& caps_node, + const std::string& version); + ~CallClient(); + + cricket::MediaSessionClient* media_client() const { return media_client_; } + void SetMediaEngine(cricket::MediaEngineInterface* media_engine) { + media_engine_ = media_engine; + } + void SetAutoAccept(bool auto_accept) { + auto_accept_ = auto_accept; + } + void SetPmucDomain(const std::string &pmuc_domain) { + pmuc_domain_ = pmuc_domain; + } + void SetRender(bool render) { + render_ = render; + } + void SetDataChannelType(cricket::DataChannelType data_channel_type) { + data_channel_type_ = data_channel_type; + } + void SetMultiSessionEnabled(bool multisession_enabled) { + multisession_enabled_ = multisession_enabled; + } + void SetConsole(Console *console) { + console_ = console; + } + void SetPriority(int priority) { + my_status_.set_priority(priority); + } + void SendStatus() { + SendStatus(my_status_); + } + void SendStatus(const buzz::PresenceStatus& status); + + void ParseLine(const std::string &str); + + void SendChat(const std::string& to, const std::string msg); + void SendData(const std::string& stream_name, + const std::string& text); + void InviteFriend(const std::string& user); + void JoinMuc(const buzz::Jid& room_jid); + void JoinMuc(const std::string& room_jid_str); + void LookupAndJoinMuc(const std::string& room_name); + void InviteToMuc(const std::string& user, const std::string& room); + bool InMuc(); + const buzz::Jid* FirstMucJid(); + void LeaveMuc(const std::string& room); + void SetNick(const std::string& muc_nick); + void SetPortAllocatorFlags(uint32 flags) { portallocator_flags_ = flags; } + void SetAllowLocalIps(bool allow_local_ips) { + allow_local_ips_ = allow_local_ips; + } + + void SetSignalingProtocol(cricket::SignalingProtocol protocol) { + signaling_protocol_ = protocol; + } + void SetTransportProtocol(cricket::TransportProtocol protocol) { + transport_protocol_ = protocol; + } + void SetSecurePolicy(cricket::SecurePolicy sdes_policy, + cricket::SecurePolicy dtls_policy) { + sdes_policy_ = sdes_policy; + dtls_policy_ = dtls_policy; + } + void SetSslIdentity(talk_base::SSLIdentity* identity) { + ssl_identity_.reset(identity); + } + + typedef std::map MucMap; + + const MucMap& mucs() const { + return mucs_; + } + + void SetShowRosterMessages(bool show_roster_messages) { + show_roster_messages_ = show_roster_messages; + } + + private: + void AddStream(uint32 audio_src_id, uint32 video_src_id); + void RemoveStream(uint32 audio_src_id, uint32 video_src_id); + void OnStateChange(buzz::XmppEngine::State state); + + void InitMedia(); + void InitPresence(); + void StartXmppPing(); + void OnPingTimeout(); + void OnRequestSignaling(); + void OnSessionCreate(cricket::Session* session, bool initiate); + void OnCallCreate(cricket::Call* call); + void OnCallDestroy(cricket::Call* call); + void OnSessionState(cricket::Call* call, + cricket::Session* session, + cricket::Session::State state); + void OnStatusUpdate(const buzz::PresenceStatus& status); + void OnMucInviteReceived(const buzz::Jid& inviter, const buzz::Jid& room, + const std::vector& avail); + void OnMucJoined(const buzz::Jid& endpoint); + void OnMucStatusUpdate(const buzz::Jid& jid, + const buzz::MucPresenceStatus& status); + void OnMucLeft(const buzz::Jid& endpoint, int error); + void OnPresenterStateChange(const std::string& nick, + bool was_presenting, bool is_presenting); + void OnAudioMuteStateChange(const std::string& nick, + bool was_muted, bool is_muted); + void OnRecordingStateChange(const std::string& nick, + bool was_recording, bool is_recording); + void OnRemoteMuted(const std::string& mutee_nick, + const std::string& muter_nick, + bool should_mute_locally); + void OnMediaBlocked(const std::string& blockee_nick, + const std::string& blocker_nick); + void OnHangoutRequestError(const std::string& node, + const buzz::XmlElement* stanza); + void OnHangoutPublishAudioMuteError(const std::string& task_id, + const buzz::XmlElement* stanza); + void OnHangoutPublishPresenterError(const std::string& task_id, + const buzz::XmlElement* stanza); + void OnHangoutPublishRecordingError(const std::string& task_id, + const buzz::XmlElement* stanza); + void OnHangoutRemoteMuteError(const std::string& task_id, + const std::string& mutee_nick, + const buzz::XmlElement* stanza); + void OnDevicesChange(); + void OnMediaStreamsUpdate(cricket::Call* call, + cricket::Session* session, + const cricket::MediaStreams& added, + const cricket::MediaStreams& removed); + void OnSpeakerChanged(cricket::Call* call, + cricket::Session* session, + const cricket::StreamParams& speaker_stream); + void OnRoomLookupResponse(buzz::MucRoomLookupTask* task, + const buzz::MucRoomInfo& room_info); + void OnRoomLookupError(buzz::IqTask* task, + const buzz::XmlElement* stanza); + void OnRoomConfigResult(buzz::MucRoomConfigTask* task); + void OnRoomConfigError(buzz::IqTask* task, + const buzz::XmlElement* stanza); + void OnDataReceived(cricket::Call*, + const cricket::ReceiveDataParams& params, + const talk_base::Buffer& payload); + buzz::Jid GenerateRandomMucJid(); + + // Depending on |enable|, render (or don't) all the streams in |session|. + void RenderAllStreams(cricket::Call* call, + cricket::Session* session, + bool enable); + + // Depending on |enable|, render (or don't) the streams in |video_streams|. + void RenderStreams(cricket::Call* call, + cricket::Session* session, + const std::vector& video_streams, + bool enable); + + // Depending on |enable|, render (or don't) the supplied |stream|. + void RenderStream(cricket::Call* call, + cricket::Session* session, + const cricket::StreamParams& stream, + bool enable); + void AddStaticRenderedView( + cricket::Session* session, + uint32 ssrc, int width, int height, int framerate, + int x_offset, int y_offset); + bool RemoveStaticRenderedView(uint32 ssrc); + void RemoveCallsStaticRenderedViews(cricket::Call* call); + void SendViewRequest(cricket::Call* call, cricket::Session* session); + bool SelectFirstDesktopScreencastId(cricket::ScreencastId* screencastid); + + static const std::string strerror(buzz::XmppEngine::Error err); + + void PrintRoster(); + bool FindJid(const std::string& name, + buzz::Jid* found_jid, + cricket::CallOptions* options); + bool PlaceCall(const std::string& name, cricket::CallOptions options); + bool InitiateAdditionalSession(const std::string& name, + cricket::CallOptions options); + void TerminateAndRemoveSession(cricket::Call* call, const std::string& id); + void PrintCalls(); + void SwitchToCall(uint32 call_id); + void Accept(const cricket::CallOptions& options); + void Reject(); + void Quit(); + + void GetDevices(); + void PrintDevices(const std::vector& names); + + void SetVolume(const std::string& level); + + cricket::Session* GetFirstSession() { return sessions_[call_->id()][0]; } + void AddSession(cricket::Session* session) { + sessions_[call_->id()].push_back(session); + } + + void PrintStats() const; + void SetupAcceptedCall(); + + typedef std::map RosterMap; + + Console *console_; + buzz::XmppClient* xmpp_client_; + talk_base::Thread* worker_thread_; + talk_base::NetworkManager* network_manager_; + cricket::PortAllocator* port_allocator_; + cricket::SessionManager* session_manager_; + cricket::SessionManagerTask* session_manager_task_; + cricket::MediaEngineInterface* media_engine_; + cricket::DataEngineInterface* data_engine_; + cricket::MediaSessionClient* media_client_; + MucMap mucs_; + + cricket::Call* call_; + typedef std::map > SessionMap; + SessionMap sessions_; + + buzz::HangoutPubSubClient* hangout_pubsub_client_; + bool incoming_call_; + bool auto_accept_; + std::string pmuc_domain_; + bool render_; + cricket::DataChannelType data_channel_type_; + bool multisession_enabled_; + cricket::VideoRenderer* local_renderer_; + StaticRenderedViews static_rendered_views_; + uint32 static_views_accumulated_count_; + uint32 screencast_ssrc_; + + buzz::PresenceStatus my_status_; + buzz::PresencePushTask* presence_push_; + buzz::PresenceOutTask* presence_out_; + buzz::MucInviteRecvTask* muc_invite_recv_; + buzz::MucInviteSendTask* muc_invite_send_; + buzz::FriendInviteSendTask* friend_invite_send_; + RosterMap* roster_; + uint32 portallocator_flags_; + + bool allow_local_ips_; + cricket::SignalingProtocol signaling_protocol_; + cricket::TransportProtocol transport_protocol_; + cricket::SecurePolicy sdes_policy_; + cricket::SecurePolicy dtls_policy_; + talk_base::scoped_ptr ssl_identity_; + std::string last_sent_to_; + + bool show_roster_messages_; +}; + +#endif // TALK_EXAMPLES_CALL_CALLCLIENT_H_ diff --git a/talk/examples/call/callclient_unittest.cc b/talk/examples/call/callclient_unittest.cc new file mode 100644 index 000000000..b0e9d8977 --- /dev/null +++ b/talk/examples/call/callclient_unittest.cc @@ -0,0 +1,47 @@ +/* + * libjingle + * Copyright 2008, 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. + */ + +// Unit tests for CallClient + +#include "talk/base/gunit.h" +#include "talk/examples/call/callclient.h" +#include "talk/media/base/filemediaengine.h" +#include "talk/media/base/mediaengine.h" +#include "talk/xmpp/xmppthread.h" + +TEST(CallClientTest, CreateCallClientWithDefaultMediaEngine) { + buzz::XmppPump pump; + CallClient *client = new CallClient(pump.client(), "app", "version"); + delete client; +} + +TEST(CallClientTest, CreateCallClientWithFileMediaEngine) { + buzz::XmppPump pump; + CallClient *client = new CallClient(pump.client(), "app", "version"); + client->SetMediaEngine(new cricket::FileMediaEngine); + delete client; +} diff --git a/talk/examples/call/console.cc b/talk/examples/call/console.cc new file mode 100644 index 000000000..dec3b4abc --- /dev/null +++ b/talk/examples/call/console.cc @@ -0,0 +1,165 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#define _CRT_SECURE_NO_DEPRECATE 1 + +#ifdef POSIX +#include +#include +#include +#endif // POSIX +#include +#include "talk/base/logging.h" +#include "talk/base/messagequeue.h" +#include "talk/base/stringutils.h" +#include "talk/examples/call/console.h" +#include "talk/examples/call/callclient.h" + +#ifdef POSIX +static void DoNothing(int unused) {} +#endif + +Console::Console(talk_base::Thread *thread, CallClient *client) : + client_(client), + client_thread_(thread), + console_thread_(new talk_base::Thread()) {} + +Console::~Console() { + Stop(); +} + +void Console::Start() { + if (!console_thread_) { + // stdin was closed in Stop(), so we can't restart. + LOG(LS_ERROR) << "Cannot re-start"; + return; + } + if (console_thread_->started()) { + LOG(LS_WARNING) << "Already started"; + return; + } + console_thread_->Start(); + console_thread_->Post(this, MSG_START); +} + +void Console::Stop() { + if (console_thread_ && console_thread_->started()) { +#ifdef WIN32 + CloseHandle(GetStdHandle(STD_INPUT_HANDLE)); +#else + close(fileno(stdin)); + // This forces the read() in fgets() to return with errno = EINTR. fgets() + // will retry the read() and fail, thus returning. + pthread_kill(console_thread_->GetPThread(), SIGUSR1); +#endif + console_thread_->Stop(); + console_thread_.reset(); + } +} + +void Console::SetEcho(bool on) { +#ifdef WIN32 + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + if ((hIn == INVALID_HANDLE_VALUE) || (hIn == NULL)) + return; + + DWORD mode; + if (!GetConsoleMode(hIn, &mode)) + return; + + if (on) { + mode = mode | ENABLE_ECHO_INPUT; + } else { + mode = mode & ~ENABLE_ECHO_INPUT; + } + + SetConsoleMode(hIn, mode); +#else + const int fd = fileno(stdin); + if (fd == -1) + return; + + struct termios tcflags; + if (tcgetattr(fd, &tcflags) == -1) + return; + + if (on) { + tcflags.c_lflag |= ECHO; + } else { + tcflags.c_lflag &= ~ECHO; + } + + tcsetattr(fd, TCSANOW, &tcflags); +#endif +} + +void Console::PrintLine(const char* format, ...) { + va_list ap; + va_start(ap, format); + + char buf[4096]; + int size = vsnprintf(buf, sizeof(buf), format, ap); + assert(size >= 0); + assert(size < static_cast(sizeof(buf))); + buf[size] = '\0'; + printf("%s\n", buf); + fflush(stdout); + + va_end(ap); +} + +void Console::RunConsole() { + char input_buffer[128]; + while (fgets(input_buffer, sizeof(input_buffer), stdin) != NULL) { + client_thread_->Post(this, MSG_INPUT, + new talk_base::TypedMessageData(input_buffer)); + } +} + +void Console::OnMessage(talk_base::Message *msg) { + switch (msg->message_id) { + case MSG_START: +#ifdef POSIX + // Install a no-op signal so that we can abort RunConsole() by raising + // SIGUSR1. + struct sigaction act; + act.sa_handler = &DoNothing; + sigemptyset(&act.sa_mask); + act.sa_flags = 0; + if (sigaction(SIGUSR1, &act, NULL) < 0) { + LOG(LS_WARNING) << "Can't install signal"; + } +#endif + RunConsole(); + break; + case MSG_INPUT: + talk_base::TypedMessageData *data = + static_cast*>(msg->pdata); + client_->ParseLine(data->data()); + break; + } +} diff --git a/talk/examples/call/console.h b/talk/examples/call/console.h new file mode 100644 index 000000000..4a90a7fe5 --- /dev/null +++ b/talk/examples/call/console.h @@ -0,0 +1,69 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_EXAMPLES_CALL_CONSOLE_H_ +#define TALK_EXAMPLES_CALL_CONSOLE_H_ + +#include + +#include "talk/base/thread.h" +#include "talk/base/messagequeue.h" +#include "talk/base/scoped_ptr.h" + +class CallClient; + +class Console : public talk_base::MessageHandler { + public: + Console(talk_base::Thread *thread, CallClient *client); + ~Console(); + + // Starts reading lines from the console and giving them to the CallClient. + void Start(); + // Stops reading lines. Cannot be restarted. + void Stop(); + + virtual void OnMessage(talk_base::Message *msg); + + void PrintLine(const char* format, ...); + + static void SetEcho(bool on); + + private: + enum { + MSG_START, + MSG_INPUT, + }; + + void RunConsole(); + void ParseLine(std::string &str); + + CallClient *client_; + talk_base::Thread *client_thread_; + talk_base::scoped_ptr console_thread_; +}; + +#endif // TALK_EXAMPLES_CALL_CONSOLE_H_ diff --git a/talk/examples/call/friendinvitesendtask.cc b/talk/examples/call/friendinvitesendtask.cc new file mode 100644 index 000000000..cdb0b2cca --- /dev/null +++ b/talk/examples/call/friendinvitesendtask.cc @@ -0,0 +1,76 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmpp/constants.h" +#include "talk/examples/call/friendinvitesendtask.h" + +namespace buzz { + +XmppReturnStatus +FriendInviteSendTask::Send(const Jid& user) { + if (GetState() != STATE_INIT && GetState() != STATE_START) + return XMPP_RETURN_BADSTATE; + + // Need to first add to roster, then subscribe to presence. + XmlElement* iq = new XmlElement(QN_IQ); + iq->AddAttr(QN_TYPE, STR_SET); + XmlElement* query = new XmlElement(QN_ROSTER_QUERY); + XmlElement* item = new XmlElement(QN_ROSTER_ITEM); + item->AddAttr(QN_JID, user.Str()); + item->AddAttr(QN_NAME, user.node()); + query->AddElement(item); + iq->AddElement(query); + QueueStanza(iq); + + // Subscribe to presence + XmlElement* presence = new XmlElement(QN_PRESENCE); + presence->AddAttr(QN_TO, user.Str()); + presence->AddAttr(QN_TYPE, STR_SUBSCRIBE); + XmlElement* invitation = new XmlElement(QN_INVITATION); + invitation->AddAttr(QN_INVITE_MESSAGE, + "I've been using Google Talk and thought you might like to try it out. " + "We can use it to call each other for free over the internet. Here's an " + "invitation to download Google Talk. Give it a try!"); + presence->AddElement(invitation); + QueueStanza(presence); + + return XMPP_RETURN_OK; +} + +int +FriendInviteSendTask::ProcessStart() { + const XmlElement* stanza = NextStanza(); + if (stanza == NULL) + return STATE_BLOCKED; + + if (SendStanza(stanza) != XMPP_RETURN_OK) + return STATE_ERROR; + + return STATE_START; +} + +} diff --git a/talk/examples/call/friendinvitesendtask.h b/talk/examples/call/friendinvitesendtask.h new file mode 100644 index 000000000..625f077f4 --- /dev/null +++ b/talk/examples/call/friendinvitesendtask.h @@ -0,0 +1,49 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _FRIENDINVITESENDTASK_H_ +#define _FRIENDINVITESENDTASK_H_ + +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmpptask.h" + +namespace buzz { + +class FriendInviteSendTask : public XmppTask { +public: + explicit FriendInviteSendTask(XmppTaskParentInterface* parent) + : XmppTask(parent) {} + virtual ~FriendInviteSendTask() {} + + XmppReturnStatus Send(const Jid& user); + + virtual int ProcessStart(); +}; + +} + +#endif diff --git a/talk/examples/call/mediaenginefactory.cc b/talk/examples/call/mediaenginefactory.cc new file mode 100644 index 000000000..983345d2f --- /dev/null +++ b/talk/examples/call/mediaenginefactory.cc @@ -0,0 +1,81 @@ +// +// libjingle +// Copyright 2004--2007, 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/examples/call/mediaenginefactory.h" + +#include "talk/base/stringutils.h" +#include "talk/media/base/fakemediaengine.h" +#include "talk/media/base/filemediaengine.h" +#include "talk/media/base/mediaengine.h" + +std::vector RequiredAudioCodecs() { + std::vector audio_codecs; + audio_codecs.push_back( + cricket::AudioCodec(9, "G722", 16000, 0, 1, 0)); + audio_codecs.push_back( + cricket::AudioCodec(0, "PCMU", 8000, 0, 1, 0)); + audio_codecs.push_back( + cricket::AudioCodec(13, "CN", 8000, 0, 1, 0)); + audio_codecs.push_back( + cricket::AudioCodec(105, "CN", 16000, 0, 1, 0)); + return audio_codecs; +} + +std::vector RequiredVideoCodecs() { + std::vector video_codecs; + video_codecs.push_back( + cricket::VideoCodec(97, "H264", 320, 240, 30, 0)); + video_codecs.push_back( + cricket::VideoCodec(99, "H264-SVC", 640, 360, 30, 0)); + return video_codecs; +} + +cricket::MediaEngineInterface* MediaEngineFactory::CreateFileMediaEngine( + const char* voice_in, const char* voice_out, + const char* video_in, const char* video_out) { + cricket::FileMediaEngine* file_media_engine = new cricket::FileMediaEngine; + // Set the RTP dump file names. + if (voice_in) { + file_media_engine->set_voice_input_filename(voice_in); + } + if (voice_out) { + file_media_engine->set_voice_output_filename(voice_out); + } + if (video_in) { + file_media_engine->set_video_input_filename(video_in); + } + if (video_out) { + file_media_engine->set_video_output_filename(video_out); + } + + // Set voice and video codecs. TODO: The codecs actually depend on + // the the input voice and video streams. + file_media_engine->set_voice_codecs(RequiredAudioCodecs()); + file_media_engine->set_video_codecs(RequiredVideoCodecs()); + + return file_media_engine; +} diff --git a/talk/examples/call/mediaenginefactory.h b/talk/examples/call/mediaenginefactory.h new file mode 100644 index 000000000..90407f9fa --- /dev/null +++ b/talk/examples/call/mediaenginefactory.h @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_EXAMPLES_CALL_MEDIAENGINEFACTORY_H_ +#define TALK_EXAMPLES_CALL_MEDIAENGINEFACTORY_H_ + +#include "talk/media/base/mediaengine.h" + +class MediaEngineFactory { + public: + static cricket::MediaEngineInterface* CreateFileMediaEngine( + const char* voice_in, const char* voice_out, + const char* video_in, const char* video_out); +}; + +#endif // TALK_EXAMPLES_CALL_MEDIAENGINEFACTORY_H_ diff --git a/talk/examples/call/muc.h b/talk/examples/call/muc.h new file mode 100644 index 000000000..0e937cabb --- /dev/null +++ b/talk/examples/call/muc.h @@ -0,0 +1,66 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _MUC_H_ +#define _MUC_H_ + +#include +#include "talk/xmpp/jid.h" +#include "talk/xmpp/presencestatus.h" + +namespace buzz { + +class Muc { + public: + Muc(const Jid& jid, const std::string& nick) : state_(MUC_JOINING), + jid_(jid), local_jid_(Jid(jid.Str() + "/" + nick)) {} + ~Muc() {}; + + enum State { MUC_JOINING, MUC_JOINED, MUC_LEAVING }; + State state() const { return state_; } + void set_state(State state) { state_ = state; } + const Jid & jid() const { return jid_; } + const Jid & local_jid() const { return local_jid_; } + + typedef std::map MemberMap; + + // All the intelligence about how to manage the members is in + // CallClient, so we completely expose the map. + MemberMap& members() { + return members_; + } + +private: + State state_; + Jid jid_; + Jid local_jid_; + MemberMap members_; +}; + +} + +#endif diff --git a/talk/examples/call/mucinviterecvtask.cc b/talk/examples/call/mucinviterecvtask.cc new file mode 100644 index 000000000..061db744e --- /dev/null +++ b/talk/examples/call/mucinviterecvtask.cc @@ -0,0 +1,124 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmpp/constants.h" +#include "talk/examples/call/mucinviterecvtask.h" + +namespace buzz { + +const char* types[] = { + "unknown", + "audio", + "video", +}; + +const char* statuses[] = { + "unknown", + "sendrecv", + "sendonly", + "recvonly", + "inactive", +}; + +const char* +AvailableMediaEntry::TypeAsString(type_t type) { + // The values of the constants have been chosen such that this is correct. + return types[type]; +} + +const char* +AvailableMediaEntry::StatusAsString(status_t status) { + // The values of the constants have been chosen such that this is correct. + return statuses[status]; +} + +int bodytext_to_array_pos(const XmlElement* elem, const char* array[], + int len, int defval = -1) { + if (elem) { + const std::string& body(elem->BodyText()); + for (int i = 0; i < len; ++i) { + if (body == array[i]) { + // Found it. + return i; + } + } + } + // If we get here, it's not any value in the array. + return defval; +} + +bool +MucInviteRecvTask::HandleStanza(const XmlElement* stanza) { + // Figuring out that we want to handle this is a lot of the work of + // actually handling it, so we handle it right here instead of queueing it. + const XmlElement* xstanza; + const XmlElement* invite; + if (stanza->Name() != QN_MESSAGE) return false; + xstanza = stanza->FirstNamed(QN_MUC_USER_X); + if (!xstanza) return false; + invite = xstanza->FirstNamed(QN_MUC_USER_INVITE); + if (!invite) return false; + // Else it's an invite and we definitely want to handle it. Parse the + // available-media, if any. + std::vector v; + const XmlElement* avail = + invite->FirstNamed(QN_GOOGLE_MUC_USER_AVAILABLE_MEDIA); + if (avail) { + for (const XmlElement* entry = avail->FirstNamed(QN_GOOGLE_MUC_USER_ENTRY); + entry; + entry = entry->NextNamed(QN_GOOGLE_MUC_USER_ENTRY)) { + AvailableMediaEntry tmp; + // In the interest of debugging, we accept as much valid-looking data + // as we can. + tmp.label = atoi(entry->Attr(QN_LABEL).c_str()); + tmp.type = static_cast( + bodytext_to_array_pos( + entry->FirstNamed(QN_GOOGLE_MUC_USER_TYPE), + types, + sizeof(types)/sizeof(const char*), + AvailableMediaEntry::TYPE_UNKNOWN)); + tmp.status = static_cast( + bodytext_to_array_pos( + entry->FirstNamed(QN_GOOGLE_MUC_USER_STATUS), + statuses, + sizeof(statuses)/sizeof(const char*), + AvailableMediaEntry::STATUS_UNKNOWN)); + v.push_back(tmp); + } + } + SignalInviteReceived(Jid(invite->Attr(QN_FROM)), Jid(stanza->Attr(QN_FROM)), + v); + return true; +} + +int +MucInviteRecvTask::ProcessStart() { + // We never queue anything so we are always blocked. + return STATE_BLOCKED; +} + +} diff --git a/talk/examples/call/mucinviterecvtask.h b/talk/examples/call/mucinviterecvtask.h new file mode 100644 index 000000000..24f05e0e4 --- /dev/null +++ b/talk/examples/call/mucinviterecvtask.h @@ -0,0 +1,82 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _MUCINVITERECVTASK_H_ +#define _MUCINVITERECVTASK_H_ + +#include + +#include "talk/base/sigslot.h" +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmpptask.h" + +namespace buzz { + +struct AvailableMediaEntry { + enum type_t { + // SIP defines other media types, but these are the only ones we use in + // multiway jingle. + // These numbers are important; see .cc file + TYPE_UNKNOWN = 0, // indicates invalid string + TYPE_AUDIO = 1, + TYPE_VIDEO = 2, + }; + + enum status_t { + // These numbers are important; see .cc file + STATUS_UNKNOWN = 0, // indicates invalid string + STATUS_SENDRECV = 1, + STATUS_SENDONLY = 2, + STATUS_RECVONLY = 3, + STATUS_INACTIVE = 4, + }; + + uint32 label; + type_t type; + status_t status; + + static const char* TypeAsString(type_t type); + static const char* StatusAsString(status_t status); +}; + +class MucInviteRecvTask : public XmppTask { + public: + explicit MucInviteRecvTask(XmppTaskParentInterface* parent) + : XmppTask(parent, XmppEngine::HL_TYPE) {} + virtual int ProcessStart(); + + // First arg is inviter's JID; second is MUC's JID. + sigslot::signal3& > SignalInviteReceived; + + protected: + virtual bool HandleStanza(const XmlElement* stanza); + +}; + +} + +#endif diff --git a/talk/examples/call/mucinvitesendtask.cc b/talk/examples/call/mucinvitesendtask.cc new file mode 100644 index 000000000..d648fef08 --- /dev/null +++ b/talk/examples/call/mucinvitesendtask.cc @@ -0,0 +1,63 @@ +/* + * libjingle + * Copyright 2004--2005, 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/examples/call/mucinvitesendtask.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/xmppclient.h" + +namespace buzz { + +XmppReturnStatus +MucInviteSendTask::Send(const Jid& to, const Jid& invitee) { + if (GetState() != STATE_INIT && GetState() != STATE_START) + return XMPP_RETURN_BADSTATE; + + XmlElement* message = new XmlElement(QN_MESSAGE); + message->AddAttr(QN_TO, to.Str()); + XmlElement* xstanza = new XmlElement(QN_MUC_USER_X); + XmlElement* invite = new XmlElement(QN_MUC_USER_INVITE); + invite->AddAttr(QN_TO, invitee.Str()); + xstanza->AddElement(invite); + message->AddElement(xstanza); + + QueueStanza(message); + return XMPP_RETURN_OK; +} + +int +MucInviteSendTask::ProcessStart() { + const XmlElement* stanza = NextStanza(); + if (stanza == NULL) + return STATE_BLOCKED; + + if (SendStanza(stanza) != XMPP_RETURN_OK) + return STATE_ERROR; + + return STATE_START; +} + +} diff --git a/talk/examples/call/mucinvitesendtask.h b/talk/examples/call/mucinvitesendtask.h new file mode 100644 index 000000000..2429b3186 --- /dev/null +++ b/talk/examples/call/mucinvitesendtask.h @@ -0,0 +1,50 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _MUCINVITESENDTASK_H_ +#define _MUCINVITESENDTASK_H_ + +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmpptask.h" +#include "talk/examples/call/muc.h" + +namespace buzz { + +class MucInviteSendTask : public XmppTask { +public: + explicit MucInviteSendTask(XmppTaskParentInterface* parent) + : XmppTask(parent) {} + virtual ~MucInviteSendTask() {} + + XmppReturnStatus Send(const Jid& to, const Jid& invitee); + + virtual int ProcessStart(); +}; + +} + +#endif diff --git a/talk/examples/call/presencepushtask.cc b/talk/examples/call/presencepushtask.cc new file mode 100644 index 000000000..af02b1f3e --- /dev/null +++ b/talk/examples/call/presencepushtask.cc @@ -0,0 +1,222 @@ +/* + * libjingle + * Copyright 2004--2005, 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/examples/call/presencepushtask.h" + +#include "talk/base/stringencode.h" +#include "talk/examples/call/muc.h" +#include "talk/xmpp/constants.h" + + + +namespace buzz { + +// string helper functions ----------------------------------------------------- + +static bool +IsXmlSpace(int ch) { + return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t'; +} + +static bool ListContainsToken(const std::string & list, + const std::string & token) { + size_t i = list.find(token); + if (i == std::string::npos || token.empty()) + return false; + bool boundary_before = (i == 0 || IsXmlSpace(list[i - 1])); + bool boundary_after = (i == list.length() - token.length() || + IsXmlSpace(list[i + token.length()])); + return boundary_before && boundary_after; +} + + +bool PresencePushTask::HandleStanza(const XmlElement * stanza) { + if (stanza->Name() != QN_PRESENCE) + return false; + QueueStanza(stanza); + return true; +} + +static bool IsUtf8FirstByte(int c) { + return (((c)&0x80)==0) || // is single byte + ((unsigned char)((c)-0xc0)<0x3e); // or is lead byte +} + +int PresencePushTask::ProcessStart() { + const XmlElement * stanza = NextStanza(); + if (stanza == NULL) + return STATE_BLOCKED; + + Jid from(stanza->Attr(QN_FROM)); + std::map::const_iterator elem = + client_->mucs().find(from.BareJid()); + if (elem == client_->mucs().end()) { + HandlePresence(from, stanza); + } else { + HandleMucPresence(elem->second, from, stanza); + } + + return STATE_START; +} + +void PresencePushTask::HandlePresence(const Jid& from, + const XmlElement* stanza) { + if (stanza->Attr(QN_TYPE) == STR_ERROR) + return; + + PresenceStatus s; + FillStatus(from, stanza, &s); + SignalStatusUpdate(s); +} + +void PresencePushTask::HandleMucPresence(buzz::Muc* muc, + const Jid& from, + const XmlElement* stanza) { + if (from == muc->local_jid()) { + if (!stanza->HasAttr(QN_TYPE)) { + // We joined the MUC. + const XmlElement* elem = stanza->FirstNamed(QN_MUC_USER_X); + // Status code=110 or 100 is not guaranteed to be present, so we + // only check the item element and Muc join status. + if (elem) { + if (elem->FirstNamed(QN_MUC_USER_ITEM) && + muc->state() == buzz::Muc::MUC_JOINING) { + SignalMucJoined(muc->jid()); + } + } + } else { + // We've been kicked. Bye. + int error = 0; + if (stanza->Attr(QN_TYPE) == STR_ERROR) { + const XmlElement* elem = stanza->FirstNamed(QN_ERROR); + if (elem && elem->HasAttr(QN_CODE)) { + error = atoi(elem->Attr(QN_CODE).c_str()); + } + } + SignalMucLeft(muc->jid(), error); + } + } else { + MucPresenceStatus s; + FillMucStatus(from, stanza, &s); + SignalMucStatusUpdate(muc->jid(), s); + } +} + +void PresencePushTask::FillStatus(const Jid& from, const XmlElement* stanza, + PresenceStatus* s) { + s->set_jid(from); + if (stanza->Attr(QN_TYPE) == STR_UNAVAILABLE) { + s->set_available(false); + } else { + s->set_available(true); + const XmlElement * status = stanza->FirstNamed(QN_STATUS); + if (status != NULL) { + s->set_status(status->BodyText()); + + // Truncate status messages longer than 300 bytes + if (s->status().length() > 300) { + size_t len = 300; + + // Be careful not to split legal utf-8 chars in half + while (!IsUtf8FirstByte(s->status()[len]) && len > 0) { + len -= 1; + } + std::string truncated(s->status(), 0, len); + s->set_status(truncated); + } + } + + const XmlElement * priority = stanza->FirstNamed(QN_PRIORITY); + if (priority != NULL) { + int pri; + if (talk_base::FromString(priority->BodyText(), &pri)) { + s->set_priority(pri); + } + } + + const XmlElement * show = stanza->FirstNamed(QN_SHOW); + if (show == NULL || show->FirstChild() == NULL) { + s->set_show(PresenceStatus::SHOW_ONLINE); + } + else { + if (show->BodyText() == "away") { + s->set_show(PresenceStatus::SHOW_AWAY); + } + else if (show->BodyText() == "xa") { + s->set_show(PresenceStatus::SHOW_XA); + } + else if (show->BodyText() == "dnd") { + s->set_show(PresenceStatus::SHOW_DND); + } + else if (show->BodyText() == "chat") { + s->set_show(PresenceStatus::SHOW_CHAT); + } + else { + s->set_show(PresenceStatus::SHOW_ONLINE); + } + } + + const XmlElement * caps = stanza->FirstNamed(QN_CAPS_C); + if (caps != NULL) { + std::string node = caps->Attr(QN_NODE); + std::string ver = caps->Attr(QN_VER); + std::string exts = caps->Attr(QN_EXT); + + s->set_know_capabilities(true); + s->set_caps_node(node); + s->set_version(ver); + + if (ListContainsToken(exts, "voice-v1")) { + s->set_voice_capability(true); + } + if (ListContainsToken(exts, "video-v1")) { + s->set_video_capability(true); + } + } + + const XmlElement* delay = stanza->FirstNamed(kQnDelayX); + if (delay != NULL) { + // Ideally we would parse this according to the Psuedo ISO-8601 rules + // that are laid out in JEP-0082: + // http://www.jabber.org/jeps/jep-0082.html + std::string stamp = delay->Attr(kQnStamp); + s->set_sent_time(stamp); + } + + const XmlElement* nick = stanza->FirstNamed(QN_NICKNAME); + if (nick) { + s->set_nick(nick->BodyText()); + } + } +} + +void PresencePushTask::FillMucStatus(const Jid& from, const XmlElement* stanza, + MucPresenceStatus* s) { + FillStatus(from, stanza, s); +} + +} diff --git a/talk/examples/call/presencepushtask.h b/talk/examples/call/presencepushtask.h new file mode 100644 index 000000000..9cd1b4298 --- /dev/null +++ b/talk/examples/call/presencepushtask.h @@ -0,0 +1,70 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _PRESENCEPUSHTASK_H_ +#define _PRESENCEPUSHTASK_H_ + +#include + +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmpptask.h" +#include "talk/xmpp/presencestatus.h" +#include "talk/base/sigslot.h" +#include "talk/examples/call/callclient.h" + +namespace buzz { + +class PresencePushTask : public XmppTask { + public: + PresencePushTask(XmppTaskParentInterface* parent, CallClient* client) + : XmppTask(parent, XmppEngine::HL_TYPE), + client_(client) {} + virtual int ProcessStart(); + + sigslot::signal1 SignalStatusUpdate; + sigslot::signal1 SignalMucJoined; + sigslot::signal2 SignalMucLeft; + sigslot::signal2 SignalMucStatusUpdate; + + protected: + virtual bool HandleStanza(const XmlElement * stanza); + void HandlePresence(const Jid& from, const XmlElement * stanza); + void HandleMucPresence(buzz::Muc* muc, + const Jid& from, const XmlElement * stanza); + static void FillStatus(const Jid& from, const XmlElement * stanza, + PresenceStatus* status); + static void FillMucStatus(const Jid& from, const XmlElement * stanza, + MucPresenceStatus* status); + + private: + CallClient* client_; +}; + + +} + +#endif diff --git a/talk/examples/chat/Info.plist b/talk/examples/chat/Info.plist new file mode 100644 index 000000000..ecd083ac6 --- /dev/null +++ b/talk/examples/chat/Info.plist @@ -0,0 +1,11 @@ + + + + + CFBundleIdentifier + com.google.call + CFBundleName + chat + + + diff --git a/talk/examples/chat/chat_main.cc b/talk/examples/chat/chat_main.cc new file mode 100644 index 000000000..09a454e2d --- /dev/null +++ b/talk/examples/chat/chat_main.cc @@ -0,0 +1,159 @@ +/* + * libjingle + * Copyright 2004--2013, 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. + */ + +// +// A simple text chat application, largely copied from examples/call. +// + +#include + +#include "talk/base/logging.h" +#include "talk/base/ssladapter.h" + +#ifdef OSX +#include "talk/base/maccocoasocketserver.h" +#elif defined(WIN32) +#include "talk/base/win32socketserver.h" +#else +#include "talk/base/physicalsocketserver.h" +#endif + +#include "talk/xmpp/constants.h" +#include "talk/xmpp/xmppauth.h" +#include "talk/xmpp/xmppclientsettings.h" +#include "talk/xmpp/xmpppump.h" +#include "talk/xmpp/xmppsocket.h" + +#include "talk/examples/chat/chatapp.h" +#include "talk/examples/chat/consoletask.h" + +static const int kDefaultPort = 5222; + +int main(int argc, char* argv[]) { + // TODO(pmclean): Remove duplication of code with examples/call. + // Set up debugging. + bool debug = true; + if (debug) { + talk_base::LogMessage::LogToDebug(talk_base::LS_VERBOSE); + } + + // Set up the crypto subsystem. + talk_base::InitializeSSL(); + + // Parse username and password, if present. + buzz::Jid jid; + std::string username; + talk_base::InsecureCryptStringImpl pass; + if (argc > 1) { + username = argv[1]; + if (argc > 2) { + pass.password() = argv[2]; + } + } + + // ... else prompt for them + if (username.empty()) { + printf("JID: "); + std::cin >> username; + } + if (username.find('@') == std::string::npos) { + username.append("@localhost"); + } + + jid = buzz::Jid(username); + if (!jid.IsValid() || jid.node() == "") { + printf("Invalid JID. JIDs should be in the form user@domain\n"); + return 1; + } + + if (pass.password().empty()) { + buzz::ConsoleTask::SetEcho(false); + printf("Password: "); + std::cin >> pass.password(); + buzz::ConsoleTask::SetEcho(true); + printf("\n"); + } + + // OTP (this can be skipped) + std::string otp_token; + printf("OTP: "); + fflush(stdin); + std::getline(std::cin, otp_token); + + // Setup the connection settings. + buzz::XmppClientSettings xcs; + xcs.set_user(jid.node()); + xcs.set_resource("chat"); + xcs.set_host(jid.domain()); + bool allow_plain = false; + xcs.set_allow_plain(allow_plain); + xcs.set_use_tls(buzz::TLS_REQUIRED); + xcs.set_pass(talk_base::CryptString(pass)); + if (!otp_token.empty() && *otp_token.c_str() != '\n') { + xcs.set_auth_token(buzz::AUTH_MECHANISM_OAUTH2, otp_token); + } + + // Build the server spec + std::string host; + int port; + + std::string server = "talk.google.com"; + int colon = server.find(':'); + if (colon == -1) { + host = server; + port = kDefaultPort; + } else { + host = server.substr(0, colon); + port = atoi(server.substr(colon + 1).c_str()); + } + xcs.set_server(talk_base::SocketAddress(host, port)); + + talk_base::Thread* main_thread = talk_base::Thread::Current(); +#if WIN32 + // Need to pump messages on our main thread on Windows. + talk_base::Win32Thread w32_thread; + talk_base::ThreadManager::Instance()->SetCurrentThread(&w32_thread); +#elif defined(OSX) + talk_base::MacCocoaSocketServer ss; + talk_base::SocketServerScope ss_scope(&ss); +#else + talk_base::PhysicalSocketServer ss; +#endif + + buzz::XmppPump* pump = new buzz::XmppPump(); + ChatApp *client = new ChatApp(pump->client(), main_thread); + + // Start pumping messages! + pump->DoLogin(xcs, new buzz::XmppSocket(buzz::TLS_REQUIRED), new XmppAuth()); + + main_thread->Run(); + pump->DoDisconnect(); + + delete client; + + return 0; +} diff --git a/talk/examples/chat/chatapp.cc b/talk/examples/chat/chatapp.cc new file mode 100644 index 000000000..1b59910b0 --- /dev/null +++ b/talk/examples/chat/chatapp.cc @@ -0,0 +1,251 @@ +/* + * libjingle + * Copyright 2004--2013, 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/examples/chat/chatapp.h" + +#include "talk/examples/chat/consoletask.h" +#include "talk/examples/chat/textchatsendtask.h" +#include "talk/examples/chat/textchatreceivetask.h" +#include "talk/xmpp/presenceouttask.h" +#include "talk/xmpp/presencereceivetask.h" + +#ifdef WIN32 +#define snprintf _snprintf +#endif + +ChatApp::ChatApp(buzz::XmppClient* xmpp_client, talk_base::Thread* main_thread) + : xmpp_client_(xmpp_client), + presence_out_task_(NULL), + presence_receive_task_(NULL), + message_send_task_(NULL), + message_received_task_(NULL), + console_task_(new buzz::ConsoleTask(main_thread)), + ui_state_(STATE_BASE) { + xmpp_client_->SignalStateChange.connect(this, &ChatApp::OnStateChange); + + console_task_->TextInputHandler.connect(this, &ChatApp::OnConsoleMessage); + console_task_->Start(); +} + +ChatApp::~ChatApp() { + if (presence_out_task_ != NULL) { + // Check out + BroadcastPresence(away); + } +} + +void ChatApp::Quit() { + talk_base::Thread::Current()->Quit(); +} + +void ChatApp::OnXmppOpen() { + presence_out_task_.reset(new buzz::PresenceOutTask(xmpp_client_)); + presence_receive_task_.reset(new buzz::PresenceReceiveTask(xmpp_client_)); + presence_receive_task_->PresenceUpdate.connect(this, + &ChatApp::OnPresenceUpdate); + message_send_task_.reset(new buzz::TextChatSendTask(xmpp_client_)); + message_received_task_.reset(new buzz::TextChatReceiveTask(xmpp_client_)); + message_received_task_->SignalTextChatReceived.connect( + this, &ChatApp::OnTextMessage); + + presence_out_task_->Start(); + presence_receive_task_->Start(); + message_send_task_->Start(); + message_received_task_->Start(); +} + +void ChatApp::BroadcastPresence(PresenceState state) { + buzz::PresenceStatus status; + status.set_jid(xmpp_client_->jid()); + status.set_available(state == online); + status.set_show(state == online ? buzz::PresenceStatus::SHOW_ONLINE + : buzz::PresenceStatus::SHOW_AWAY); + presence_out_task_->Send(status); +} + +// UI Stuff +static const char* kMenuChoiceQuit = "0"; +static const char* kMenuChoiceRoster = "1"; +static const char* kMenuChoiceChat = "2"; + +static const char* kUIStrings[3][2] = { + {kMenuChoiceQuit, "Quit"}, + {kMenuChoiceRoster, "Roster"}, + {kMenuChoiceChat, "Send"}}; + +void ChatApp::PrintMenu() { + char buff[128]; + int numMenuItems = sizeof(kUIStrings) / sizeof(kUIStrings[0]); + for (int index = 0; index < numMenuItems; ++index) { + snprintf(buff, sizeof(buff), "%s) %s\n", kUIStrings[index][0], + kUIStrings[index][1]); + console_task_->Print(buff); + } + console_task_->Print("choice:"); +} + +void ChatApp::PrintRoster() { + int index = 0; + for (RosterList::iterator iter = roster_list_.begin(); + iter != roster_list_.end(); ++iter) { + const buzz::Jid& jid = iter->second.jid(); + console_task_->Print( + "%d: (*) %s@%s [%s] \n", + index++, + jid.node().c_str(), + jid.domain().c_str(), + jid.resource().c_str()); + } +} + +void ChatApp::PromptJid() { + PrintRoster(); + console_task_->Print("choice:"); +} + +void ChatApp::PromptChatMessage() { + console_task_->Print(":"); +} + +bool ChatApp::GetRosterItem(int index, buzz::PresenceStatus* status) { + int found_index = 0; + for (RosterList::iterator iter = roster_list_.begin(); + iter != roster_list_.end() && found_index <= index; ++iter) { + if (found_index == index) { + *status = iter->second; + return true; + } + found_index++; + } + + return false; +} + +void ChatApp::HandleBaseInput(const std::string& message) { + if (message == kMenuChoiceQuit) { + Quit(); + } else if (message == kMenuChoiceRoster) { + PrintRoster(); + } else if (message == kMenuChoiceChat) { + ui_state_ = STATE_PROMPTJID; + PromptJid(); + } else if (message == "") { + PrintMenu(); + } +} + +void ChatApp::HandleJidInput(const std::string& message) { + if (isdigit(message[0])) { + // It's an index-based roster choice. + int index = 0; + buzz::PresenceStatus status; + if (!talk_base::FromString(message, &index) || + !GetRosterItem(index, &status)) { + // fail, so drop back + ui_state_ = STATE_BASE; + return; + } + + chat_dest_jid_ = status.jid(); + } else { + // It's an explicit address. + chat_dest_jid_ = buzz::Jid(message.c_str()); + } + ui_state_ = STATE_CHATTING; + PromptChatMessage(); +} + +void ChatApp::HandleChatInput(const std::string& message) { + if (message == "") { + ui_state_ = STATE_BASE; + PrintMenu(); + } else { + message_send_task_->Send(chat_dest_jid_, message); + PromptChatMessage(); + } +} + +// Connection state notifications +void ChatApp::OnStateChange(buzz::XmppEngine::State state) { + switch (state) { + // Nonexistent state + case buzz::XmppEngine::STATE_NONE: + break; + + // Nonexistent state + case buzz::XmppEngine::STATE_START: + break; + + // Exchanging stream headers, authenticating and so on. + case buzz::XmppEngine::STATE_OPENING: + break; + + // Authenticated and bound. + case buzz::XmppEngine::STATE_OPEN: + OnXmppOpen(); + BroadcastPresence(online); + PrintMenu(); + break; + + // Session closed, possibly due to error. + case buzz::XmppEngine::STATE_CLOSED: + break; + } +} + +// Presence Notifications +void ChatApp::OnPresenceUpdate(const buzz::PresenceStatus& status) { + if (status.available()) { + roster_list_[status.jid().Str()] = status; + } else { + RosterList::iterator iter = roster_list_.find(status.jid().Str()); + if (iter != roster_list_.end()) { + roster_list_.erase(iter); + } + } +} + +// Text message handlers +void ChatApp::OnTextMessage(const buzz::Jid& from, const buzz::Jid& to, + const std::string& message) { + console_task_->Print("%s says: %s\n", from.node().c_str(), message.c_str()); +} + +void ChatApp::OnConsoleMessage(const std::string &message) { + switch (ui_state_) { + case STATE_BASE: + HandleBaseInput(message); + break; + + case STATE_PROMPTJID: + HandleJidInput(message); + break; + + case STATE_CHATTING: + HandleChatInput(message); + break; + } +} diff --git a/talk/examples/chat/chatapp.h b/talk/examples/chat/chatapp.h new file mode 100644 index 000000000..cc032a67f --- /dev/null +++ b/talk/examples/chat/chatapp.h @@ -0,0 +1,171 @@ +/* + * libjingle + * Copyright 2004--2013, 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. + */ + +#ifndef TALK_EXAMPLES_CHAT_CHATAPP_H_ +#define TALK_EXAMPLES_CHAT_CHATAPP_H_ + +#include "talk/base/thread.h" +#include "talk/base/scoped_ptr.h" + +#include "talk/xmpp/jid.h" +#include "talk/xmpp/xmppclient.h" + +namespace buzz { +class XmppClient; +class PresenceOutTask; +class PresenceReceiveTask; +class TextChatSendTask; +class TextChatReceiveTask; +class ConsoleTask; +class PresenceStatus; +} + +// This is an example chat app for libjingle, showing how to use xmpp tasks, +// data, callbacks, etc. It has a simple text-based UI for logging in, +// sending and receiving messages, and printing the roster. +class ChatApp: public sigslot::has_slots<> { + public: + // Arguments: + // xmpp_client Points to the XmppClient for the communication channel + // (typically created by the XmppPump object). + // main_thread Wraps the application's main thread. Subsidiary threads + // for the various tasks will be forked off of this. + ChatApp(buzz::XmppClient* xmpp_client, talk_base::Thread* main_thread); + + // Shuts down and releases all of the contained tasks/threads + ~ChatApp(); + + // Shuts down the current thread and quits + void Quit(); + + private: + // + // Initialization + // + // Called explicitly after the connection to the chat server is established. + void OnXmppOpen(); + + // + // UI Stuff + // + // Prints the app main menu on the console. + // Called when ui_state_ == STATE_BASE. + void PrintMenu(); + + // Prints a numbered list of the logged-in user's roster on the console. + void PrintRoster(); + + // Prints a prompt for the user to enter either the index from the + // roster list of the user they wish to chat with, or a fully-qualified + // (user@server.ext) jid. + // Called when when ui_state_ == STATE_PROMPTJID. + void PromptJid(); + + // Prints a prompt on the console for the user to enter a message to send. + // Called when when ui_state_ == STATE_CHATTING. + void PromptChatMessage(); + + // Sends our presence state to the chat server (and on to your roster list). + // Arguments: + // state Specifies the presence state to show. + enum PresenceState {online, away}; + void BroadcastPresence(PresenceState state); + + // Returns the RosterItem associated with the specified index. + // Just a helper to select a roster item from a numbered list in the UI. + bool GetRosterItem(int index, buzz::PresenceStatus* status); + + // + // Input Handling + // + // Receives input when ui_state_ == STATE_BASE. Handles choices from the + // main menu. + void HandleBaseInput(const std::string& message); + + // Receives input when ui_state_ == STATE_PROMPTJID. Handles selection + // of a JID to chat to. + void HandleJidInput(const std::string& message); + + // Receives input when ui_state_ == STATE_CHATTING. Handles text messages. + void HandleChatInput(const std::string& message); + + // + // signal/slot Callbacks + // + // Connected to the XmppClient::SignalStateChange slot. Receives + // notifications of state changes of the connection. + void OnStateChange(buzz::XmppEngine::State state); + + // Connected to the PresenceReceiveTask::PresenceUpdate slot. + // Receives status messages for the logged-in user's roster (i.e. + // an initial list from the server and people coming/going). + void OnPresenceUpdate(const buzz::PresenceStatus& status); + + // Connected to the TextChatReceiveTask::SignalTextChatReceived slot. + // Called when we receive a text chat from someone else. + void OnTextMessage(const buzz::Jid& from, const buzz::Jid& to, + const std::string& message); + + // Receives text input from the console task. This is where any input + // from the user comes in. + // Arguments: + // message What the user typed. + void OnConsoleMessage(const std::string &message); + + // The XmppClient object associated with this chat application instance. + buzz::XmppClient* xmpp_client_; + + // We send presence information through this object. + talk_base::scoped_ptr presence_out_task_; + + // We receive others presence information through this object. + talk_base::scoped_ptr presence_receive_task_; + + // We send text messages though this object. + talk_base::scoped_ptr message_send_task_; + + // We receive messages through this object. + talk_base::scoped_ptr message_received_task_; + + // UI gets drawn and receives input through this task. + talk_base::scoped_ptr< buzz::ConsoleTask> console_task_; + + // The list of JIDs for the people in the logged-in users roster. + // RosterList roster_list_; + typedef std::map RosterList; + RosterList roster_list_; + + // The JID of the user currently being chatted with. + buzz::Jid chat_dest_jid_; + + // UI State constants + enum UIState { STATE_BASE, STATE_PROMPTJID, STATE_CHATTING }; + UIState ui_state_; +}; + +#endif // TALK_EXAMPLES_CHAT_CHATAPP_H_ + diff --git a/talk/examples/chat/consoletask.cc b/talk/examples/chat/consoletask.cc new file mode 100644 index 000000000..2577c79cb --- /dev/null +++ b/talk/examples/chat/consoletask.cc @@ -0,0 +1,177 @@ +/* + * libjingle + * Copyright 2004--2013, 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. + */ + +// TODO(pmclean): Perhaps this should be unified with examples/call/console.cc +// and refactor to talk/base. +#include "talk/examples/chat/consoletask.h" + +#define _CRT_SECURE_NO_DEPRECATE 1 + +#include +#ifdef POSIX +#include +#include +#include +#endif // POSIX +#include + +#include "talk/base/logging.h" + +#ifdef POSIX +static void DoNothing(int unused) {} +#endif + +namespace buzz { + +ConsoleTask::ConsoleTask(talk_base::Thread *thread) : + client_thread_(thread), + console_thread_(new talk_base::Thread()) { +} + +ConsoleTask::~ConsoleTask() { + Stop(); +} + +void ConsoleTask::Start() { + if (!console_thread_) { + // stdin was closed in Stop(), so we can't restart. + LOG(LS_ERROR) << "Cannot re-start"; + return; + } + if (console_thread_->started()) { + LOG(LS_WARNING) << "Already started"; + return; + } + console_thread_->Start(); + console_thread_->Post(this, MSG_START); +} + +void ConsoleTask::Stop() { + if (console_thread_ && console_thread_->started()) { +#ifdef WIN32 + CloseHandle(GetStdHandle(STD_INPUT_HANDLE)); +#else + close(fileno(stdin)); + // This forces the read() in fgets() to return with errno = EINTR. fgets() + // will retry the read() and fail, thus returning. + pthread_kill(console_thread_->GetPThread(), SIGUSR1); +#endif + console_thread_->Stop(); + console_thread_.reset(); + } +} + +void ConsoleTask::SetEcho(bool on) { +#ifdef WIN32 + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + if ((hIn == INVALID_HANDLE_VALUE) || (hIn == NULL)) + return; + + DWORD mode; + if (!GetConsoleMode(hIn, &mode)) + return; + + if (on) { + mode = mode | ENABLE_ECHO_INPUT; + } else { + mode = mode & ~ENABLE_ECHO_INPUT; + } + + SetConsoleMode(hIn, mode); +#else // MAC & LINUX + const int fd = fileno(stdin); + if (fd == -1) { + return; + } + + struct termios tcflags; + if (tcgetattr(fd, &tcflags) == -1) { + return; + } + + if (on) { + tcflags.c_lflag |= ECHO; + } else { + tcflags.c_lflag &= ~ECHO; + } + + tcsetattr(fd, TCSANOW, &tcflags); +#endif +} + +void ConsoleTask::Print(const char* format, ...) { + va_list ap; + va_start(ap, format); + + char buf[4096]; + int size = vsnprintf(buf, sizeof(buf), format, ap); + assert(size >= 0); + assert(size < static_cast(sizeof(buf))); + buf[size] = '\0'; + printf("%s", buf); + fflush(stdout); + + va_end(ap); +} + +void ConsoleTask::RunConsole() { + char input_buffer[128]; + while (fgets(input_buffer, sizeof(input_buffer), stdin) != NULL) { + client_thread_->Post(this, MSG_INPUT, + new talk_base::TypedMessageData(input_buffer)); + } +} + +void ConsoleTask::OnMessage(talk_base::Message *msg) { + switch (msg->message_id) { + case MSG_START: +#ifdef POSIX + // Install a no-op signal so that we can abort RunConsole() by raising + // SIGUSR1. + struct sigaction act; + act.sa_handler = &DoNothing; + sigemptyset(&act.sa_mask); + act.sa_flags = 0; + if (sigaction(SIGUSR1, &act, NULL) < 0) { + LOG(LS_WARNING) << "Can't install signal"; + } +#endif + RunConsole(); + break; + + case MSG_INPUT: + talk_base::TypedMessageData *data = + static_cast*>(msg->pdata); + // Trim off the .line-terminator to make processing easier. + std::string parsed_message = + data->data().substr(0, data->data().length() - 1); + TextInputHandler(parsed_message); + break; + } +} + +} // namespace buzz diff --git a/talk/examples/chat/consoletask.h b/talk/examples/chat/consoletask.h new file mode 100644 index 000000000..1d45b3a7f --- /dev/null +++ b/talk/examples/chat/consoletask.h @@ -0,0 +1,92 @@ +/* + * libjingle + * Copyright 2004--2013, 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. + */ + +#ifndef TALK_EXAMPLES_CHAT_CONSOLETASK_H_ +#define TALK_EXAMPLES_CHAT_CONSOLETASK_H_ + +#include + +#include "talk/base/thread.h" +#include "talk/base/sigslot.h" + +namespace buzz { + +// +// Provides properly threaded console I/O. +// +class ConsoleTask : public talk_base::MessageHandler { + public: + // Arguments: + // thread The main application thread. Input messages get posted through + // this. + explicit ConsoleTask(talk_base::Thread *thread); + + // Shuts down the thread associated with this task. + ~ConsoleTask(); + + // Slot for text inputs handler. + sigslot::signal1 TextInputHandler; + + // Starts reading lines from the console and passes them to the + // TextInputHandler. + void Start(); + + // Stops reading lines and shuts down the thread. Cannot be restarted. + void Stop(); + + // Thread messages (especialy text-input messages) come in through here. + virtual void OnMessage(talk_base::Message *msg); + + // printf() style output to the console. + void Print(const char* format, ...); + + // Turns on/off the echo of input characters on the console. + // Arguments: + // on If true turns echo on, off otherwise. + static void SetEcho(bool on); + + private: + /** Message IDs (for OnMessage()). */ + enum { + MSG_START, + MSG_INPUT, + }; + + // Starts up polling for console input + void RunConsole(); + + // The main application thread + talk_base::Thread *client_thread_; + + // The tread associated with this console object + talk_base::scoped_ptr console_thread_; +}; + +} // namespace buzz + +#endif // TALK_EXAMPLES_CHAT_CONSOLETASK_H_ + diff --git a/talk/examples/chat/textchatreceivetask.cc b/talk/examples/chat/textchatreceivetask.cc new file mode 100644 index 000000000..cbd019c7e --- /dev/null +++ b/talk/examples/chat/textchatreceivetask.cc @@ -0,0 +1,66 @@ +/* + * libjingle + * Copyright 2004--2013, 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/examples/chat/textchatreceivetask.h" + +#include "talk/xmpp/constants.h" + +namespace buzz { + +TextChatReceiveTask::TextChatReceiveTask(XmppTaskParentInterface* parent) + : XmppTask(parent, XmppEngine::HL_TYPE) { +} + +TextChatReceiveTask::~TextChatReceiveTask() { + Stop(); +} + +bool TextChatReceiveTask::HandleStanza(const XmlElement* stanza) { + // Make sure that this stanza is a message + if (stanza->Name() != QN_MESSAGE) { + return false; + } + + // see if there is any body + const XmlElement* message_body = stanza->FirstNamed(QN_BODY); + if (message_body == NULL) { + return false; + } + + // Looks good, so send the message text along. + SignalTextChatReceived(Jid(stanza->Attr(QN_FROM)), Jid(stanza->Attr(QN_TO)), + message_body->BodyText()); + + return true; +} + +int TextChatReceiveTask::ProcessStart() { + // not queuing messages, so just block. + return STATE_BLOCKED; +} + +} // namespace buzz diff --git a/talk/examples/chat/textchatreceivetask.h b/talk/examples/chat/textchatreceivetask.h new file mode 100644 index 000000000..e2776927a --- /dev/null +++ b/talk/examples/chat/textchatreceivetask.h @@ -0,0 +1,63 @@ +/* + * libjingle + * Copyright 2004--2013, 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. + */ + +#ifndef TALK_EXAMPLES_CHAT_TEXTCHATRECEIVETASK_H_ +#define TALK_EXAMPLES_CHAT_TEXTCHATRECEIVETASK_H_ + +#include "talk/base/sigslot.h" +#include "talk/xmpp/xmpptask.h" + +namespace buzz { + +// A class to receive chat messages from the XMPP server. +class TextChatReceiveTask : public XmppTask { + public: + // Arguments: + // parent a reference to task interface associated withe the XMPP client. + explicit TextChatReceiveTask(XmppTaskParentInterface* parent); + + // Shuts down the thread associated with this task. + virtual ~TextChatReceiveTask(); + + // Starts pulling queued status messages and dispatching them to the + // PresenceUpdate() callback. + virtual int ProcessStart(); + + // Slot for chat message callbacks + sigslot::signal3 + SignalTextChatReceived; + + protected: + // Called by the XMPP client when chat stanzas arrive. We pull out the + // interesting parts and send them to the SignalTextCharReceived() slot. + virtual bool HandleStanza(const XmlElement* stanza); +}; + +} // namespace buzz + +#endif // TALK_EXAMPLES_CHAT_TEXTCHATRECEIVETASK_H_ + diff --git a/talk/examples/chat/textchatsendtask.cc b/talk/examples/chat/textchatsendtask.cc new file mode 100644 index 000000000..ba1445302 --- /dev/null +++ b/talk/examples/chat/textchatsendtask.cc @@ -0,0 +1,81 @@ +/* + * libjingle + * Copyright 2004--2013, 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/examples/chat/textchatsendtask.h" + +#include "talk/xmpp/constants.h" +#include "talk/xmpp/xmppclient.h" + +namespace buzz { +TextChatSendTask::TextChatSendTask(XmppTaskParentInterface* parent) + : XmppTask(parent) { +} + +TextChatSendTask::~TextChatSendTask() { + Stop(); +} + +XmppReturnStatus TextChatSendTask::Send(const Jid& to, + const std::string& textmessage) { + // Make sure we are actually connected. + if (GetState() != STATE_INIT && GetState() != STATE_START) { + return XMPP_RETURN_BADSTATE; + } + + // Put together the chat stanza... + XmlElement* message_stanza = new XmlElement(QN_MESSAGE); + + // ... and specify the required attributes... + message_stanza->AddAttr(QN_TO, to.Str()); + message_stanza->AddAttr(QN_TYPE, "chat"); + message_stanza->AddAttr(QN_LANG, "en"); + + // ... and fill out the body. + XmlElement* message_body = new XmlElement(QN_BODY); + message_body->AddText(textmessage); + message_stanza->AddElement(message_body); + + // Now queue it up. + QueueStanza(message_stanza); + + return XMPP_RETURN_OK; +} + +int TextChatSendTask::ProcessStart() { + const XmlElement* stanza = NextStanza(); + if (stanza == NULL) { + return STATE_BLOCKED; + } + + if (SendStanza(stanza) != XMPP_RETURN_OK) { + return STATE_ERROR; + } + + return STATE_START; +} + +} // namespace buzz diff --git a/talk/examples/chat/textchatsendtask.h b/talk/examples/chat/textchatsendtask.h new file mode 100644 index 000000000..9b18923d2 --- /dev/null +++ b/talk/examples/chat/textchatsendtask.h @@ -0,0 +1,56 @@ +/* + * libjingle + * Copyright 2004--2013, 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. + */ + +#ifndef TALK_EXAMPLES_CHAT_TEXTCHATSENDTASK_H_ +#define TALK_EXAMPLES_CHAT_TEXTCHATSENDTASK_H_ + +#include "talk/xmpp/xmpptask.h" + +namespace buzz { + +// A class to send chat messages to the XMPP server. +class TextChatSendTask : public XmppTask { + public: + // Arguments: + // parent a reference to task interface associated withe the XMPP client. + explicit TextChatSendTask(XmppTaskParentInterface* parent); + + // Shuts down the thread associated with this task. + virtual ~TextChatSendTask(); + + // Forms the XMPP "chat" stanza with the specified receipient and message + // and queues it up. + XmppReturnStatus Send(const Jid& to, const std::string& message); + + // Picks up any "chat" stanzas from our queue and sends them to the server. + virtual int ProcessStart(); +}; + +} // namespace buzz + +#endif // TALK_EXAMPLES_CHAT_TEXTCHATSENDTASK_H_ + diff --git a/talk/examples/ios/AppRTCDemo.xcodeproj/project.pbxproj b/talk/examples/ios/AppRTCDemo.xcodeproj/project.pbxproj new file mode 100644 index 000000000..72de3b494 --- /dev/null +++ b/talk/examples/ios/AppRTCDemo.xcodeproj/project.pbxproj @@ -0,0 +1,570 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 4F995B31173B6937007F179A /* libaudio_coding_module.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B01173B6937007F179A /* libaudio_coding_module.a */; }; + 4F995B32173B6937007F179A /* libaudio_conference_mixer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B02173B6937007F179A /* libaudio_conference_mixer.a */; }; + 4F995B33173B6937007F179A /* libaudio_device.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B03173B6937007F179A /* libaudio_device.a */; }; + 4F995B34173B6938007F179A /* libaudio_processing.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B04173B6937007F179A /* libaudio_processing.a */; }; + 4F995B35173B6938007F179A /* libaudioproc_debug_proto.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B05173B6937007F179A /* libaudioproc_debug_proto.a */; }; + 4F995B36173B6938007F179A /* libbitrate_controller.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B06173B6937007F179A /* libbitrate_controller.a */; }; + 4F995B37173B6938007F179A /* libCNG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B07173B6937007F179A /* libCNG.a */; }; + 4F995B38173B6938007F179A /* libcommon_video.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B08173B6937007F179A /* libcommon_video.a */; }; + 4F995B39173B6938007F179A /* libexpat.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B09173B6937007F179A /* libexpat.a */; }; + 4F995B3A173B6938007F179A /* libG711.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B0A173B6937007F179A /* libG711.a */; }; + 4F995B3B173B6938007F179A /* libG722.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B0B173B6937007F179A /* libG722.a */; }; + 4F995B3C173B6938007F179A /* libgunit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B0C173B6937007F179A /* libgunit.a */; }; + 4F995B3D173B6938007F179A /* libiLBC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B0D173B6937007F179A /* libiLBC.a */; }; + 4F995B3E173B6938007F179A /* libiSAC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B0E173B6937007F179A /* libiSAC.a */; }; + 4F995B3F173B6938007F179A /* libiSACFix.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B0F173B6937007F179A /* libiSACFix.a */; }; + 4F995B40173B6938007F179A /* libjingle_media.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B10173B6937007F179A /* libjingle_media.a */; }; + 4F995B41173B6938007F179A /* libjingle_p2p.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B11173B6937007F179A /* libjingle_p2p.a */; }; + 4F995B42173B6938007F179A /* libjingle_peerconnection_objc.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B12173B6937007F179A /* libjingle_peerconnection_objc.a */; }; + 4F995B43173B6938007F179A /* libjingle_peerconnection.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B13173B6937007F179A /* libjingle_peerconnection.a */; }; + 4F995B44173B6938007F179A /* libjingle_sound.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B14173B6937007F179A /* libjingle_sound.a */; }; + 4F995B45173B6938007F179A /* libjingle.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B15173B6937007F179A /* libjingle.a */; }; + 4F995B46173B6938007F179A /* libjsoncpp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B16173B6937007F179A /* libjsoncpp.a */; }; + 4F995B47173B6938007F179A /* libmedia_file.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B17173B6937007F179A /* libmedia_file.a */; }; + 4F995B48173B6938007F179A /* libNetEq.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B18173B6937007F179A /* libNetEq.a */; }; + 4F995B49173B6938007F179A /* libopenssl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B19173B6937007F179A /* libopenssl.a */; }; + 4F995B4A173B6938007F179A /* libopus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B1A173B6937007F179A /* libopus.a */; }; + 4F995B4B173B6938007F179A /* libpaced_sender.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B1B173B6937007F179A /* libpaced_sender.a */; }; + 4F995B4C173B6938007F179A /* libPCM16B.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B1C173B6937007F179A /* libPCM16B.a */; }; + 4F995B4D173B6938007F179A /* libprotobuf_lite.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B1D173B6937007F179A /* libprotobuf_lite.a */; }; + 4F995B4E173B6938007F179A /* libremote_bitrate_estimator.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B1E173B6937007F179A /* libremote_bitrate_estimator.a */; }; + 4F995B4F173B6938007F179A /* libresampler.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B1F173B6937007F179A /* libresampler.a */; }; + 4F995B50173B6938007F179A /* librtp_rtcp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B20173B6937007F179A /* librtp_rtcp.a */; }; + 4F995B51173B6938007F179A /* libsignal_processing.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B21173B6937007F179A /* libsignal_processing.a */; }; + 4F995B52173B6938007F179A /* libsrtp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B22173B6937007F179A /* libsrtp.a */; }; + 4F995B53173B6938007F179A /* libsystem_wrappers.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B23173B6937007F179A /* libsystem_wrappers.a */; }; + 4F995B54173B6938007F179A /* libudp_transport.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B24173B6937007F179A /* libudp_transport.a */; }; + 4F995B55173B6938007F179A /* libvad.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B25173B6937007F179A /* libvad.a */; }; + 4F995B56173B6938007F179A /* libvideo_capture_module.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B26173B6937007F179A /* libvideo_capture_module.a */; }; + 4F995B57173B6938007F179A /* libvideo_coding_utility.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B27173B6937007F179A /* libvideo_coding_utility.a */; }; + 4F995B58173B6938007F179A /* libvideo_engine_core.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B28173B6937007F179A /* libvideo_engine_core.a */; }; + 4F995B59173B6938007F179A /* libvideo_processing.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B29173B6937007F179A /* libvideo_processing.a */; }; + 4F995B5A173B6938007F179A /* libvideo_render_module.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B2A173B6937007F179A /* libvideo_render_module.a */; }; + 4F995B5B173B6938007F179A /* libvoice_engine_core.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B2B173B6937007F179A /* libvoice_engine_core.a */; }; + 4F995B5C173B6938007F179A /* libwebrtc_i420.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B2C173B6937007F179A /* libwebrtc_i420.a */; }; + 4F995B5D173B6938007F179A /* libwebrtc_opus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B2D173B6937007F179A /* libwebrtc_opus.a */; }; + 4F995B5E173B6938007F179A /* libwebrtc_utility.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B2E173B6937007F179A /* libwebrtc_utility.a */; }; + 4F995B5F173B6938007F179A /* libwebrtc_video_coding.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B2F173B6937007F179A /* libwebrtc_video_coding.a */; }; + 4F995B60173B6938007F179A /* libwebrtc_vp8.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B30173B6937007F179A /* libwebrtc_vp8.a */; }; + 4F995B62173B694B007F179A /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B61173B694B007F179A /* AVFoundation.framework */; }; + 4F995B64173B6956007F179A /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B63173B6956007F179A /* CoreMedia.framework */; }; + 4F995B66173B695C007F179A /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B65173B695C007F179A /* CoreVideo.framework */; }; + 4F995B68173B6970007F179A /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B67173B6970007F179A /* CoreAudio.framework */; }; + 4F995B91173C03A1007F179A /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F995B90173C03A1007F179A /* AudioToolbox.framework */; }; + 4F995B94173C0B82007F179A /* shim.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4F995B92173C0819007F179A /* shim.mm */; }; + 4FBCC04F1728E929004C8C0B /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FBCC04E1728E929004C8C0B /* UIKit.framework */; }; + 4FBCC0511728E929004C8C0B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FBCC0501728E929004C8C0B /* Foundation.framework */; }; + 4FBCC0531728E929004C8C0B /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FBCC0521728E929004C8C0B /* CoreGraphics.framework */; }; + 4FBCC05B1728E929004C8C0B /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FBCC05A1728E929004C8C0B /* main.m */; }; + 4FBCC05F1728E929004C8C0B /* APPRTCAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FBCC05E1728E929004C8C0B /* APPRTCAppDelegate.m */; }; + 4FBCC0611728E929004C8C0B /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = 4FBCC0601728E929004C8C0B /* Default.png */; }; + 4FBCC0681728E929004C8C0B /* APPRTCViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FBCC0671728E929004C8C0B /* APPRTCViewController.m */; }; + 4FBCC06B1728E929004C8C0B /* APPRTCViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4FBCC0691728E929004C8C0B /* APPRTCViewController.xib */; }; + 4FBCC0731729B780004C8C0B /* GAEChannelClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FBCC0721729B780004C8C0B /* GAEChannelClient.m */; }; + 4FD7F5011732E1C2009295E5 /* APPRTCAppClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FD7F5001732E1C2009295E5 /* APPRTCAppClient.m */; }; + 4FEE3E531743C94D0005814A /* ios_channel.html in Resources */ = {isa = PBXBuildFile; fileRef = 4FEE3E511743C92D0005814A /* ios_channel.html */; }; + 4FEE3EB71746A3810005814A /* Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 4FEE3EB61746A3810005814A /* Icon.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 4F995B01173B6937007F179A /* libaudio_coding_module.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libaudio_coding_module.a; path = libs/libaudio_coding_module.a; sourceTree = ""; }; + 4F995B02173B6937007F179A /* libaudio_conference_mixer.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libaudio_conference_mixer.a; path = libs/libaudio_conference_mixer.a; sourceTree = ""; }; + 4F995B03173B6937007F179A /* libaudio_device.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libaudio_device.a; path = libs/libaudio_device.a; sourceTree = ""; }; + 4F995B04173B6937007F179A /* libaudio_processing.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libaudio_processing.a; path = libs/libaudio_processing.a; sourceTree = ""; }; + 4F995B05173B6937007F179A /* libaudioproc_debug_proto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libaudioproc_debug_proto.a; path = libs/libaudioproc_debug_proto.a; sourceTree = ""; }; + 4F995B06173B6937007F179A /* libbitrate_controller.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libbitrate_controller.a; path = libs/libbitrate_controller.a; sourceTree = ""; }; + 4F995B07173B6937007F179A /* libCNG.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libCNG.a; path = libs/libCNG.a; sourceTree = ""; }; + 4F995B08173B6937007F179A /* libcommon_video.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcommon_video.a; path = libs/libcommon_video.a; sourceTree = ""; }; + 4F995B09173B6937007F179A /* libexpat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libexpat.a; path = libs/libexpat.a; sourceTree = ""; }; + 4F995B0A173B6937007F179A /* libG711.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libG711.a; path = libs/libG711.a; sourceTree = ""; }; + 4F995B0B173B6937007F179A /* libG722.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libG722.a; path = libs/libG722.a; sourceTree = ""; }; + 4F995B0C173B6937007F179A /* libgunit.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgunit.a; path = libs/libgunit.a; sourceTree = ""; }; + 4F995B0D173B6937007F179A /* libiLBC.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libiLBC.a; path = libs/libiLBC.a; sourceTree = ""; }; + 4F995B0E173B6937007F179A /* libiSAC.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libiSAC.a; path = libs/libiSAC.a; sourceTree = ""; }; + 4F995B0F173B6937007F179A /* libiSACFix.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libiSACFix.a; path = libs/libiSACFix.a; sourceTree = ""; }; + 4F995B10173B6937007F179A /* libjingle_media.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libjingle_media.a; path = libs/libjingle_media.a; sourceTree = ""; }; + 4F995B11173B6937007F179A /* libjingle_p2p.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libjingle_p2p.a; path = libs/libjingle_p2p.a; sourceTree = ""; }; + 4F995B12173B6937007F179A /* libjingle_peerconnection_objc.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libjingle_peerconnection_objc.a; path = libs/libjingle_peerconnection_objc.a; sourceTree = ""; }; + 4F995B13173B6937007F179A /* libjingle_peerconnection.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libjingle_peerconnection.a; path = libs/libjingle_peerconnection.a; sourceTree = ""; }; + 4F995B14173B6937007F179A /* libjingle_sound.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libjingle_sound.a; path = libs/libjingle_sound.a; sourceTree = ""; }; + 4F995B15173B6937007F179A /* libjingle.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libjingle.a; path = libs/libjingle.a; sourceTree = ""; }; + 4F995B16173B6937007F179A /* libjsoncpp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libjsoncpp.a; path = libs/libjsoncpp.a; sourceTree = ""; }; + 4F995B17173B6937007F179A /* libmedia_file.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmedia_file.a; path = libs/libmedia_file.a; sourceTree = ""; }; + 4F995B18173B6937007F179A /* libNetEq.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libNetEq.a; path = libs/libNetEq.a; sourceTree = ""; }; + 4F995B19173B6937007F179A /* libopenssl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libopenssl.a; path = libs/libopenssl.a; sourceTree = ""; }; + 4F995B1A173B6937007F179A /* libopus.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libopus.a; path = libs/libopus.a; sourceTree = ""; }; + 4F995B1B173B6937007F179A /* libpaced_sender.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libpaced_sender.a; path = libs/libpaced_sender.a; sourceTree = ""; }; + 4F995B1C173B6937007F179A /* libPCM16B.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libPCM16B.a; path = libs/libPCM16B.a; sourceTree = ""; }; + 4F995B1D173B6937007F179A /* libprotobuf_lite.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libprotobuf_lite.a; path = libs/libprotobuf_lite.a; sourceTree = ""; }; + 4F995B1E173B6937007F179A /* libremote_bitrate_estimator.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libremote_bitrate_estimator.a; path = libs/libremote_bitrate_estimator.a; sourceTree = ""; }; + 4F995B1F173B6937007F179A /* libresampler.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libresampler.a; path = libs/libresampler.a; sourceTree = ""; }; + 4F995B20173B6937007F179A /* librtp_rtcp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = librtp_rtcp.a; path = libs/librtp_rtcp.a; sourceTree = ""; }; + 4F995B21173B6937007F179A /* libsignal_processing.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libsignal_processing.a; path = libs/libsignal_processing.a; sourceTree = ""; }; + 4F995B22173B6937007F179A /* libsrtp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libsrtp.a; path = libs/libsrtp.a; sourceTree = ""; }; + 4F995B23173B6937007F179A /* libsystem_wrappers.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libsystem_wrappers.a; path = libs/libsystem_wrappers.a; sourceTree = ""; }; + 4F995B24173B6937007F179A /* libudp_transport.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libudp_transport.a; path = libs/libudp_transport.a; sourceTree = ""; }; + 4F995B25173B6937007F179A /* libvad.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libvad.a; path = libs/libvad.a; sourceTree = ""; }; + 4F995B26173B6937007F179A /* libvideo_capture_module.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libvideo_capture_module.a; path = libs/libvideo_capture_module.a; sourceTree = ""; }; + 4F995B27173B6937007F179A /* libvideo_coding_utility.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libvideo_coding_utility.a; path = libs/libvideo_coding_utility.a; sourceTree = ""; }; + 4F995B28173B6937007F179A /* libvideo_engine_core.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libvideo_engine_core.a; path = libs/libvideo_engine_core.a; sourceTree = ""; }; + 4F995B29173B6937007F179A /* libvideo_processing.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libvideo_processing.a; path = libs/libvideo_processing.a; sourceTree = ""; }; + 4F995B2A173B6937007F179A /* libvideo_render_module.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libvideo_render_module.a; path = libs/libvideo_render_module.a; sourceTree = ""; }; + 4F995B2B173B6937007F179A /* libvoice_engine_core.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libvoice_engine_core.a; path = libs/libvoice_engine_core.a; sourceTree = ""; }; + 4F995B2C173B6937007F179A /* libwebrtc_i420.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwebrtc_i420.a; path = libs/libwebrtc_i420.a; sourceTree = ""; }; + 4F995B2D173B6937007F179A /* libwebrtc_opus.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwebrtc_opus.a; path = libs/libwebrtc_opus.a; sourceTree = ""; }; + 4F995B2E173B6937007F179A /* libwebrtc_utility.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwebrtc_utility.a; path = libs/libwebrtc_utility.a; sourceTree = ""; }; + 4F995B2F173B6937007F179A /* libwebrtc_video_coding.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwebrtc_video_coding.a; path = libs/libwebrtc_video_coding.a; sourceTree = ""; }; + 4F995B30173B6937007F179A /* libwebrtc_vp8.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwebrtc_vp8.a; path = libs/libwebrtc_vp8.a; sourceTree = ""; }; + 4F995B61173B694B007F179A /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + 4F995B63173B6956007F179A /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + 4F995B65173B695C007F179A /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; + 4F995B67173B6970007F179A /* CoreAudio.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreAudio.framework; path = System/Library/Frameworks/CoreAudio.framework; sourceTree = SDKROOT; }; + 4F995B90173C03A1007F179A /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; + 4F995B92173C0819007F179A /* shim.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = shim.mm; path = ../../../app/webrtc/objctests/ios/shim.mm; sourceTree = ""; }; + 4FBCC04B1728E929004C8C0B /* AppRTCDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppRTCDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4FBCC04E1728E929004C8C0B /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 4FBCC0501728E929004C8C0B /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 4FBCC0521728E929004C8C0B /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 4FBCC0561728E929004C8C0B /* AppRTCDemo-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "AppRTCDemo-Info.plist"; sourceTree = ""; }; + 4FBCC05A1728E929004C8C0B /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 4FBCC05C1728E929004C8C0B /* AppRTCDemo-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AppRTCDemo-Prefix.pch"; sourceTree = ""; }; + 4FBCC05D1728E929004C8C0B /* APPRTCAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = APPRTCAppDelegate.h; sourceTree = ""; }; + 4FBCC05E1728E929004C8C0B /* APPRTCAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = APPRTCAppDelegate.m; sourceTree = ""; }; + 4FBCC0601728E929004C8C0B /* Default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Default.png; sourceTree = ""; }; + 4FBCC0661728E929004C8C0B /* APPRTCViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = APPRTCViewController.h; sourceTree = ""; }; + 4FBCC0671728E929004C8C0B /* APPRTCViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = APPRTCViewController.m; sourceTree = ""; }; + 4FBCC06A1728E929004C8C0B /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/APPRTCViewController.xib; sourceTree = ""; }; + 4FBCC0711729B780004C8C0B /* GAEChannelClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GAEChannelClient.h; sourceTree = ""; }; + 4FBCC0721729B780004C8C0B /* GAEChannelClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GAEChannelClient.m; sourceTree = ""; }; + 4FD7F4FF1732E1C1009295E5 /* APPRTCAppClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = APPRTCAppClient.h; sourceTree = ""; }; + 4FD7F5001732E1C2009295E5 /* APPRTCAppClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = APPRTCAppClient.m; sourceTree = ""; }; + 4FEE3E511743C92D0005814A /* ios_channel.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = ios_channel.html; sourceTree = ""; }; + 4FEE3EB61746A3810005814A /* Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = Icon.png; path = ../Icon.png; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4FBCC0481728E929004C8C0B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4F995B91173C03A1007F179A /* AudioToolbox.framework in Frameworks */, + 4F995B62173B694B007F179A /* AVFoundation.framework in Frameworks */, + 4F995B68173B6970007F179A /* CoreAudio.framework in Frameworks */, + 4FBCC0531728E929004C8C0B /* CoreGraphics.framework in Frameworks */, + 4F995B64173B6956007F179A /* CoreMedia.framework in Frameworks */, + 4F995B66173B695C007F179A /* CoreVideo.framework in Frameworks */, + 4FBCC0511728E929004C8C0B /* Foundation.framework in Frameworks */, + 4FBCC04F1728E929004C8C0B /* UIKit.framework in Frameworks */, + 4F995B31173B6937007F179A /* libaudio_coding_module.a in Frameworks */, + 4F995B32173B6937007F179A /* libaudio_conference_mixer.a in Frameworks */, + 4F995B33173B6937007F179A /* libaudio_device.a in Frameworks */, + 4F995B34173B6938007F179A /* libaudio_processing.a in Frameworks */, + 4F995B35173B6938007F179A /* libaudioproc_debug_proto.a in Frameworks */, + 4F995B36173B6938007F179A /* libbitrate_controller.a in Frameworks */, + 4F995B37173B6938007F179A /* libCNG.a in Frameworks */, + 4F995B38173B6938007F179A /* libcommon_video.a in Frameworks */, + 4F995B39173B6938007F179A /* libexpat.a in Frameworks */, + 4F995B3A173B6938007F179A /* libG711.a in Frameworks */, + 4F995B3B173B6938007F179A /* libG722.a in Frameworks */, + 4F995B3C173B6938007F179A /* libgunit.a in Frameworks */, + 4F995B3D173B6938007F179A /* libiLBC.a in Frameworks */, + 4F995B3E173B6938007F179A /* libiSAC.a in Frameworks */, + 4F995B40173B6938007F179A /* libjingle_media.a in Frameworks */, + 4F995B41173B6938007F179A /* libjingle_p2p.a in Frameworks */, + 4F995B42173B6938007F179A /* libjingle_peerconnection_objc.a in Frameworks */, + 4F995B43173B6938007F179A /* libjingle_peerconnection.a in Frameworks */, + 4F995B44173B6938007F179A /* libjingle_sound.a in Frameworks */, + 4F995B45173B6938007F179A /* libjingle.a in Frameworks */, + 4F995B46173B6938007F179A /* libjsoncpp.a in Frameworks */, + 4F995B47173B6938007F179A /* libmedia_file.a in Frameworks */, + 4F995B48173B6938007F179A /* libNetEq.a in Frameworks */, + 4F995B49173B6938007F179A /* libopenssl.a in Frameworks */, + 4F995B4A173B6938007F179A /* libopus.a in Frameworks */, + 4F995B4B173B6938007F179A /* libpaced_sender.a in Frameworks */, + 4F995B4C173B6938007F179A /* libPCM16B.a in Frameworks */, + 4F995B4D173B6938007F179A /* libprotobuf_lite.a in Frameworks */, + 4F995B4E173B6938007F179A /* libremote_bitrate_estimator.a in Frameworks */, + 4F995B4F173B6938007F179A /* libresampler.a in Frameworks */, + 4F995B50173B6938007F179A /* librtp_rtcp.a in Frameworks */, + 4F995B51173B6938007F179A /* libsignal_processing.a in Frameworks */, + 4F995B52173B6938007F179A /* libsrtp.a in Frameworks */, + 4F995B53173B6938007F179A /* libsystem_wrappers.a in Frameworks */, + 4F995B54173B6938007F179A /* libudp_transport.a in Frameworks */, + 4F995B55173B6938007F179A /* libvad.a in Frameworks */, + 4F995B56173B6938007F179A /* libvideo_capture_module.a in Frameworks */, + 4F995B57173B6938007F179A /* libvideo_coding_utility.a in Frameworks */, + 4F995B58173B6938007F179A /* libvideo_engine_core.a in Frameworks */, + 4F995B59173B6938007F179A /* libvideo_processing.a in Frameworks */, + 4F995B5A173B6938007F179A /* libvideo_render_module.a in Frameworks */, + 4F995B5B173B6938007F179A /* libvoice_engine_core.a in Frameworks */, + 4F995B5C173B6938007F179A /* libwebrtc_i420.a in Frameworks */, + 4F995B5D173B6938007F179A /* libwebrtc_opus.a in Frameworks */, + 4F995B5E173B6938007F179A /* libwebrtc_utility.a in Frameworks */, + 4F995B5F173B6938007F179A /* libwebrtc_video_coding.a in Frameworks */, + 4F995B60173B6938007F179A /* libwebrtc_vp8.a in Frameworks */, + 4F995B3F173B6938007F179A /* libiSACFix.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4F1CC122172F52E50090479F /* libs */ = { + isa = PBXGroup; + children = ( + 4F995B01173B6937007F179A /* libaudio_coding_module.a */, + 4F995B02173B6937007F179A /* libaudio_conference_mixer.a */, + 4F995B03173B6937007F179A /* libaudio_device.a */, + 4F995B04173B6937007F179A /* libaudio_processing.a */, + 4F995B05173B6937007F179A /* libaudioproc_debug_proto.a */, + 4F995B06173B6937007F179A /* libbitrate_controller.a */, + 4F995B07173B6937007F179A /* libCNG.a */, + 4F995B08173B6937007F179A /* libcommon_video.a */, + 4F995B09173B6937007F179A /* libexpat.a */, + 4F995B0A173B6937007F179A /* libG711.a */, + 4F995B0B173B6937007F179A /* libG722.a */, + 4F995B0C173B6937007F179A /* libgunit.a */, + 4F995B0D173B6937007F179A /* libiLBC.a */, + 4F995B0E173B6937007F179A /* libiSAC.a */, + 4F995B0F173B6937007F179A /* libiSACFix.a */, + 4F995B10173B6937007F179A /* libjingle_media.a */, + 4F995B11173B6937007F179A /* libjingle_p2p.a */, + 4F995B12173B6937007F179A /* libjingle_peerconnection_objc.a */, + 4F995B13173B6937007F179A /* libjingle_peerconnection.a */, + 4F995B14173B6937007F179A /* libjingle_sound.a */, + 4F995B15173B6937007F179A /* libjingle.a */, + 4F995B16173B6937007F179A /* libjsoncpp.a */, + 4F995B17173B6937007F179A /* libmedia_file.a */, + 4F995B18173B6937007F179A /* libNetEq.a */, + 4F995B19173B6937007F179A /* libopenssl.a */, + 4F995B1A173B6937007F179A /* libopus.a */, + 4F995B1B173B6937007F179A /* libpaced_sender.a */, + 4F995B1C173B6937007F179A /* libPCM16B.a */, + 4F995B1D173B6937007F179A /* libprotobuf_lite.a */, + 4F995B1E173B6937007F179A /* libremote_bitrate_estimator.a */, + 4F995B1F173B6937007F179A /* libresampler.a */, + 4F995B20173B6937007F179A /* librtp_rtcp.a */, + 4F995B21173B6937007F179A /* libsignal_processing.a */, + 4F995B22173B6937007F179A /* libsrtp.a */, + 4F995B23173B6937007F179A /* libsystem_wrappers.a */, + 4F995B24173B6937007F179A /* libudp_transport.a */, + 4F995B25173B6937007F179A /* libvad.a */, + 4F995B26173B6937007F179A /* libvideo_capture_module.a */, + 4F995B27173B6937007F179A /* libvideo_coding_utility.a */, + 4F995B28173B6937007F179A /* libvideo_engine_core.a */, + 4F995B29173B6937007F179A /* libvideo_processing.a */, + 4F995B2A173B6937007F179A /* libvideo_render_module.a */, + 4F995B2B173B6937007F179A /* libvoice_engine_core.a */, + 4F995B2C173B6937007F179A /* libwebrtc_i420.a */, + 4F995B2D173B6937007F179A /* libwebrtc_opus.a */, + 4F995B2E173B6937007F179A /* libwebrtc_utility.a */, + 4F995B2F173B6937007F179A /* libwebrtc_video_coding.a */, + 4F995B30173B6937007F179A /* libwebrtc_vp8.a */, + ); + name = libs; + sourceTree = ""; + }; + 4FBCC0421728E929004C8C0B = { + isa = PBXGroup; + children = ( + 4FBCC0541728E929004C8C0B /* AppRTCDemo */, + 4FBCC04D1728E929004C8C0B /* Frameworks */, + 4F1CC122172F52E50090479F /* libs */, + 4FBCC04C1728E929004C8C0B /* Products */, + ); + sourceTree = ""; + }; + 4FBCC04C1728E929004C8C0B /* Products */ = { + isa = PBXGroup; + children = ( + 4FBCC04B1728E929004C8C0B /* AppRTCDemo.app */, + ); + name = Products; + sourceTree = ""; + }; + 4FBCC04D1728E929004C8C0B /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4F995B90173C03A1007F179A /* AudioToolbox.framework */, + 4F995B61173B694B007F179A /* AVFoundation.framework */, + 4F995B67173B6970007F179A /* CoreAudio.framework */, + 4FBCC0521728E929004C8C0B /* CoreGraphics.framework */, + 4F995B63173B6956007F179A /* CoreMedia.framework */, + 4F995B65173B695C007F179A /* CoreVideo.framework */, + 4FBCC0501728E929004C8C0B /* Foundation.framework */, + 4FBCC04E1728E929004C8C0B /* UIKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 4FBCC0541728E929004C8C0B /* AppRTCDemo */ = { + isa = PBXGroup; + children = ( + 4FBCC0711729B780004C8C0B /* GAEChannelClient.h */, + 4FBCC0721729B780004C8C0B /* GAEChannelClient.m */, + 4FD7F4FF1732E1C1009295E5 /* APPRTCAppClient.h */, + 4FD7F5001732E1C2009295E5 /* APPRTCAppClient.m */, + 4FBCC05D1728E929004C8C0B /* APPRTCAppDelegate.h */, + 4FBCC05E1728E929004C8C0B /* APPRTCAppDelegate.m */, + 4FBCC0661728E929004C8C0B /* APPRTCViewController.h */, + 4FBCC0671728E929004C8C0B /* APPRTCViewController.m */, + 4FBCC0691728E929004C8C0B /* APPRTCViewController.xib */, + 4FBCC0551728E929004C8C0B /* Supporting Files */, + ); + path = AppRTCDemo; + sourceTree = ""; + }; + 4FBCC0551728E929004C8C0B /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 4FBCC0561728E929004C8C0B /* AppRTCDemo-Info.plist */, + 4FBCC05C1728E929004C8C0B /* AppRTCDemo-Prefix.pch */, + 4FBCC0601728E929004C8C0B /* Default.png */, + 4FEE3EB61746A3810005814A /* Icon.png */, + 4FEE3E511743C92D0005814A /* ios_channel.html */, + 4FBCC05A1728E929004C8C0B /* main.m */, + 4F995B92173C0819007F179A /* shim.mm */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4FBCC04A1728E929004C8C0B /* AppRTCDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4FBCC06E1728E929004C8C0B /* Build configuration list for PBXNativeTarget "AppRTCDemo" */; + buildPhases = ( + 4FBCC0471728E929004C8C0B /* Sources */, + 4FBCC0481728E929004C8C0B /* Frameworks */, + 4FBCC0491728E929004C8C0B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AppRTCDemo; + productName = AppRTCDemo; + productReference = 4FBCC04B1728E929004C8C0B /* AppRTCDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4FBCC0431728E929004C8C0B /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = RTC; + LastUpgradeCheck = 0460; + ORGANIZATIONNAME = Google; + }; + buildConfigurationList = 4FBCC0461728E929004C8C0B /* Build configuration list for PBXProject "AppRTCDemo" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 4FBCC0421728E929004C8C0B; + productRefGroup = 4FBCC04C1728E929004C8C0B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4FBCC04A1728E929004C8C0B /* AppRTCDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4FBCC0491728E929004C8C0B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4FEE3E531743C94D0005814A /* ios_channel.html in Resources */, + 4FBCC0611728E929004C8C0B /* Default.png in Resources */, + 4FBCC06B1728E929004C8C0B /* APPRTCViewController.xib in Resources */, + 4FEE3EB71746A3810005814A /* Icon.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4FBCC0471728E929004C8C0B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4FBCC05B1728E929004C8C0B /* main.m in Sources */, + 4FBCC05F1728E929004C8C0B /* APPRTCAppDelegate.m in Sources */, + 4FBCC0681728E929004C8C0B /* APPRTCViewController.m in Sources */, + 4FBCC0731729B780004C8C0B /* GAEChannelClient.m in Sources */, + 4FD7F5011732E1C2009295E5 /* APPRTCAppClient.m in Sources */, + 4F995B94173C0B82007F179A /* shim.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 4FBCC0691728E929004C8C0B /* APPRTCViewController.xib */ = { + isa = PBXVariantGroup; + children = ( + 4FBCC06A1728E929004C8C0B /* en */, + ); + name = APPRTCViewController.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 4FBCC06C1728E929004C8C0B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 6.1; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 4FBCC06D1728E929004C8C0B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 6.1; + OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4FBCC06F1728E929004C8C0B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_UNIVERSAL_IPHONE_OS)"; + CLANG_CXX_LANGUAGE_STANDARD = "compiler-default"; + CLANG_CXX_LIBRARY = "compiler-default"; + GCC_CW_ASM_SYNTAX = NO; + GCC_C_LANGUAGE_STANDARD = c99; + GCC_ENABLE_CPP_EXCEPTIONS = NO; + GCC_ENABLE_CPP_RTTI = NO; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "AppRTCDemo/AppRTCDemo-Prefix.pch"; + GCC_THREADSAFE_STATICS = NO; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + HEADER_SEARCH_PATHS = ( + ../../app/webrtc/objc/public, + ../../.., + ); + INFOPLIST_FILE = "AppRTCDemo/AppRTCDemo-Info.plist"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/libs\"", + ); + ONLY_ACTIVE_ARCH = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + VALID_ARCHS = "armv7 i386"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 4FBCC0701728E929004C8C0B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_UNIVERSAL_IPHONE_OS)"; + CLANG_CXX_LANGUAGE_STANDARD = "compiler-default"; + CLANG_CXX_LIBRARY = "compiler-default"; + GCC_CW_ASM_SYNTAX = NO; + GCC_C_LANGUAGE_STANDARD = c99; + GCC_ENABLE_CPP_EXCEPTIONS = NO; + GCC_ENABLE_CPP_RTTI = NO; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "AppRTCDemo/AppRTCDemo-Prefix.pch"; + GCC_THREADSAFE_STATICS = NO; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + HEADER_SEARCH_PATHS = ( + ../../app/webrtc/objc/public, + ../../.., + ); + INFOPLIST_FILE = "AppRTCDemo/AppRTCDemo-Info.plist"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/libs\"", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + VALID_ARCHS = "armv7 i386"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4FBCC0461728E929004C8C0B /* Build configuration list for PBXProject "AppRTCDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4FBCC06C1728E929004C8C0B /* Debug */, + 4FBCC06D1728E929004C8C0B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4FBCC06E1728E929004C8C0B /* Build configuration list for PBXNativeTarget "AppRTCDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4FBCC06F1728E929004C8C0B /* Debug */, + 4FBCC0701728E929004C8C0B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 4FBCC0431728E929004C8C0B /* Project object */; +} diff --git a/talk/examples/ios/AppRTCDemo/APPRTCAppClient.h b/talk/examples/ios/AppRTCDemo/APPRTCAppClient.h new file mode 100644 index 000000000..cac2f17b4 --- /dev/null +++ b/talk/examples/ios/AppRTCDemo/APPRTCAppClient.h @@ -0,0 +1,54 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +#import "GAEChannelClient.h" + +// Called when there are RTCIceServers. +@protocol IceServerDelegate + +- (void)onIceServers:(NSArray *)servers; + +@end + +// Negotiates signaling for chatting with apprtc.appspot.com "rooms". +// Uses the client<->server specifics of the apprtc AppEngine webapp. +// +// To use: create an instance of this object (registering a message handler) and +// call connectToRoom(). apprtc.appspot.com will signal that is successful via +// onOpen through the browser channel. Then you should call sendData() and wait +// for the registered handler to be called with received messages. +@interface APPRTCAppClient : NSObject + +@property(nonatomic, assign) idiceServerDelegate; +@property(nonatomic, assign) idmessageHandler; + +- (void)connectToRoom:(NSURL *)room; +- (void)sendData:(NSData *)data; + +@end diff --git a/talk/examples/ios/AppRTCDemo/APPRTCAppClient.m b/talk/examples/ios/AppRTCDemo/APPRTCAppClient.m new file mode 100644 index 000000000..bcc2329b2 --- /dev/null +++ b/talk/examples/ios/AppRTCDemo/APPRTCAppClient.m @@ -0,0 +1,333 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "APPRTCAppClient.h" + +#import + +#import "GAEChannelClient.h" +#import "RTCIceServer.h" + +@interface APPRTCAppClient () + +@property(nonatomic, strong) dispatch_queue_t backgroundQueue; +@property(nonatomic, copy) NSString *baseURL; +@property(nonatomic, strong) GAEChannelClient *gaeChannel; +@property(nonatomic, copy) NSString *postMessageUrl; +@property(nonatomic, copy) NSString *pcConfig; +@property(nonatomic, strong) NSMutableString *receivedData; +@property(atomic, strong) NSMutableArray *sendQueue; +@property(nonatomic, copy) NSString *token; + +@property(nonatomic, assign) BOOL verboseLogging; + +@end + +@implementation APPRTCAppClient + +- (id)init { + if (self = [super init]) { + _backgroundQueue = dispatch_queue_create("RTCBackgroundQueue", NULL); + _sendQueue = [NSMutableArray array]; + // Uncomment to see Request/Response logging. + //_verboseLogging = YES; + } + return self; +} + +#pragma mark - Public methods + +- (void)connectToRoom:(NSURL *)url { + NSURLRequest *request = [self getRequestFromUrl:url]; + [NSURLConnection connectionWithRequest:request delegate:self]; +} + +- (void)sendData:(NSData *)data { + @synchronized(self) { + [self maybeLogMessage:@"Send message"]; + [self.sendQueue addObject:[data copy]]; + } + [self requestQueueDrainInBackground]; +} + +#pragma mark - Internal methods + +- (NSTextCheckingResult *)findMatch:(NSString *)regexpPattern + withString:(NSString *)string + errorMessage:(NSString *)errorMessage { + NSError *error; + NSRegularExpression *regexp = + [NSRegularExpression regularExpressionWithPattern:regexpPattern + options:0 + error:&error]; + if (error) { + [self maybeLogMessage: + [NSString stringWithFormat:@"Failed to create regexp - %@", + [error description]]]; + return nil; + } + NSRange fullRange = NSMakeRange(0, [string length]); + NSArray *matches = [regexp matchesInString:string options:0 range:fullRange]; + if ([matches count] == 0) { + if ([errorMessage length] > 0) { + [self maybeLogMessage:string]; + [self showMessage: + [NSString stringWithFormat:@"Missing %@ in HTML.", errorMessage]]; + } + return nil; + } else if ([matches count] > 1) { + if ([errorMessage length] > 0) { + [self maybeLogMessage:string]; + [self showMessage:[NSString stringWithFormat:@"Too many %@s in HTML.", + errorMessage]]; + } + return nil; + } + return matches[0]; +} + +- (NSURLRequest *)getRequestFromUrl:(NSURL *)url { + self.receivedData = [NSMutableString stringWithCapacity:20000]; + NSString *path = + [NSString stringWithFormat:@"https:%@", [url resourceSpecifier]]; + NSURLRequest *request = + [NSURLRequest requestWithURL:[NSURL URLWithString:path]]; + return request; +} + +- (void)maybeLogMessage:(NSString *)message { + if (self.verboseLogging) { + NSLog(@"%@", message); + } +} + +- (void)requestQueueDrainInBackground { + dispatch_async(self.backgroundQueue, ^(void) { + // TODO(hughv): This can block the UI thread. Fix. + @synchronized(self) { + if ([self.postMessageUrl length] < 1) { + return; + } + for (NSData *data in self.sendQueue) { + NSString *url = [NSString stringWithFormat:@"%@/%@", + self.baseURL, + self.postMessageUrl]; + [self sendData:data withUrl:url]; + } + [self.sendQueue removeAllObjects]; + } + }); +} + +- (void)sendData:(NSData *)data withUrl:(NSString *)url { + NSMutableURLRequest *request = + [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; + request.HTTPMethod = @"POST"; + [request setHTTPBody:data]; + NSURLResponse *response; + NSError *error; + NSData *responseData = [NSURLConnection sendSynchronousRequest:request + returningResponse:&response + error:&error]; + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + int status = [httpResponse statusCode]; + NSAssert(status == 200, + @"Bad response [%d] to message: %@\n\n%@", + status, + [NSString stringWithUTF8String:[data bytes]], + [NSString stringWithUTF8String:[responseData bytes]]); +} + +- (void)showMessage:(NSString *)message { + NSLog(@"%@", message); + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Unable to join" + message:message + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil]; + [alertView show]; +} + +- (void)updateIceServers:(NSMutableArray *)iceServers + withTurnServer:(NSString *)turnServerUrl { + if ([turnServerUrl length] < 1) { + [self.iceServerDelegate onIceServers:iceServers]; + return; + } + dispatch_async(self.backgroundQueue, ^(void) { + NSMutableURLRequest *request = [NSMutableURLRequest + requestWithURL:[NSURL URLWithString:turnServerUrl]]; + [request addValue:@"Mozilla/5.0" forHTTPHeaderField:@"user-agent"]; + [request addValue:@"https://apprtc.appspot.com" + forHTTPHeaderField:@"origin"]; + NSURLResponse *response; + NSError *error; + NSData *responseData = [NSURLConnection sendSynchronousRequest:request + returningResponse:&response + error:&error]; + if (!error) { + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseData + options:0 + error:&error]; + NSAssert(!error, @"Unable to parse. %@", error.localizedDescription); + NSString *username = json[@"username"]; + NSString *turnServer = json[@"turn"]; + NSString *password = json[@"password"]; + NSString *fullUrl = + [NSString stringWithFormat:@"turn:%@@%@", username, turnServer]; + RTCIceServer *iceServer = + [[RTCIceServer alloc] initWithUri:[NSURL URLWithString:fullUrl] + password:password]; + [iceServers addObject:iceServer]; + } else { + NSLog(@"Unable to get TURN server. Error: %@", error.description); + } + + dispatch_async(dispatch_get_main_queue(), ^(void) { + [self.iceServerDelegate onIceServers:iceServers]; + }); + }); +} + +#pragma mark - NSURLConnectionDataDelegate methods + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { + NSString *roomHtml = [NSString stringWithUTF8String:[data bytes]]; + [self maybeLogMessage: + [NSString stringWithFormat:@"Received %d chars", [roomHtml length]]]; + [self.receivedData appendString:roomHtml]; +} + +- (void)connection:(NSURLConnection *)connection + didReceiveResponse:(NSURLResponse *)response { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + int statusCode = [httpResponse statusCode]; + [self maybeLogMessage: + [NSString stringWithFormat: + @"Response received\nURL\n%@\nStatus [%d]\nHeaders\n%@", + [httpResponse URL], + statusCode, + [httpResponse allHeaderFields]]]; + NSAssert(statusCode == 200, @"Invalid response of %d received.", statusCode); +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection { + [self maybeLogMessage:[NSString stringWithFormat:@"finished loading %d chars", + [self.receivedData length]]]; + NSTextCheckingResult *result = + [self findMatch:@".*\n *Sorry, this room is full\\..*" + withString:self.receivedData + errorMessage:nil]; + if (result) { + [self showMessage:@"Room full"]; + return; + } + + NSString *fullUrl = [[[connection originalRequest] URL] absoluteString]; + NSRange queryRange = [fullUrl rangeOfString:@"?"]; + self.baseURL = [fullUrl substringToIndex:queryRange.location]; + [self maybeLogMessage:[NSString stringWithFormat:@"URL\n%@", self.baseURL]]; + + result = [self findMatch:@".*\n *openChannel\\('([^']*)'\\);\n.*" + withString:self.receivedData + errorMessage:@"channel token"]; + if (!result) { + return; + } + self.token = [self.receivedData substringWithRange:[result rangeAtIndex:1]]; + [self maybeLogMessage:[NSString stringWithFormat:@"Token\n%@", self.token]]; + + result = + [self findMatch:@".*\n *path = '/(message\\?r=.+)' \\+ '(&u=[0-9]+)';\n.*" + withString:self.receivedData + errorMessage:@"postMessage URL"]; + if (!result) { + return; + } + self.postMessageUrl = + [NSString stringWithFormat:@"%@%@", + [self.receivedData substringWithRange:[result rangeAtIndex:1]], + [self.receivedData substringWithRange:[result rangeAtIndex:2]]]; + [self maybeLogMessage:[NSString stringWithFormat:@"POST message URL\n%@", + self.postMessageUrl]]; + + result = [self findMatch:@".*\n *var pc_config = (\\{[^\n]*\\});\n.*" + withString:self.receivedData + errorMessage:@"pc_config"]; + if (!result) { + return; + } + NSString *pcConfig = + [self.receivedData substringWithRange:[result rangeAtIndex:1]]; + [self maybeLogMessage: + [NSString stringWithFormat:@"PC Config JSON\n%@", pcConfig]]; + + result = [self findMatch:@".*\n *requestTurn\\('([^\n]*)'\\);\n.*" + withString:self.receivedData + errorMessage:@"channel token"]; + NSString *turnServerUrl; + if (result) { + turnServerUrl = + [self.receivedData substringWithRange:[result rangeAtIndex:1]]; + [self maybeLogMessage: + [NSString stringWithFormat:@"TURN server request URL\n%@", + turnServerUrl]]; + } + + NSError *error; + NSData *pcData = [pcConfig dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *json = + [NSJSONSerialization JSONObjectWithData:pcData options:0 error:&error]; + NSAssert(!error, @"Unable to parse. %@", error.localizedDescription); + NSArray *servers = [json objectForKey:@"iceServers"]; + NSMutableArray *iceServers = [NSMutableArray array]; + for (NSDictionary *server in servers) { + NSString *url = [server objectForKey:@"url"]; + NSString *credential = [server objectForKey:@"credential"]; + if (!credential) { + credential = @""; + } + [self maybeLogMessage: + [NSString stringWithFormat:@"url [%@] - credential [%@]", + url, + credential]]; + RTCIceServer *iceServer = + [[RTCIceServer alloc] initWithUri:[NSURL URLWithString:url] + password:credential]; + [iceServers addObject:iceServer]; + } + [self updateIceServers:iceServers withTurnServer:turnServerUrl]; + + [self maybeLogMessage: + [NSString stringWithFormat:@"About to open GAE with token: %@", + self.token]]; + self.gaeChannel = + [[GAEChannelClient alloc] initWithToken:self.token + delegate:self.messageHandler]; +} + +@end diff --git a/talk/examples/ios/AppRTCDemo/APPRTCAppDelegate.h b/talk/examples/ios/AppRTCDemo/APPRTCAppDelegate.h new file mode 100644 index 000000000..82b07f044 --- /dev/null +++ b/talk/examples/ios/AppRTCDemo/APPRTCAppDelegate.h @@ -0,0 +1,53 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +#import "GAEChannelClient.h" +#import "APPRTCAppClient.h" +#import "RTCSessionDescriptonDelegate.h" + +// Used to send a message to an apprtc.appspot.com "room". +@protocol APPRTCSendMessage + +- (void)sendData:(NSData *)data; + +@end + +@class APPRTCViewController; + +// The main application class of the AppRTCDemo iOS app demonstrating +// interoperability between the Objcective C implementation of PeerConnection +// and the apprtc.appspot.com demo webapp. +@interface APPRTCAppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; +@property (strong, nonatomic) APPRTCViewController *viewController; + +@end diff --git a/talk/examples/ios/AppRTCDemo/APPRTCAppDelegate.m b/talk/examples/ios/AppRTCDemo/APPRTCAppDelegate.m new file mode 100644 index 000000000..0c429a078 --- /dev/null +++ b/talk/examples/ios/AppRTCDemo/APPRTCAppDelegate.m @@ -0,0 +1,370 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "APPRTCAppDelegate.h" + +#import "APPRTCViewController.h" +#import "RTCIceCandidate.h" +#import "RTCIceServer.h" +#import "RTCMediaConstraints.h" +#import "RTCMediaStream.h" +#import "RTCPair.h" +#import "RTCPeerConnection.h" +#import "RTCPeerConnectionDelegate.h" +#import "RTCPeerConnectionFactory.h" +#import "RTCSessionDescription.h" + +@interface PCObserver : NSObject + +- (id)initWithDelegate:(id)delegate; + +@end + +@implementation PCObserver { + id _delegate; +} + +- (id)initWithDelegate:(id)delegate { + if (self = [super init]) { + _delegate = delegate; + } + return self; +} + +- (void)peerConnectionOnError:(RTCPeerConnection *)peerConnection { + NSLog(@"PCO onError."); + NSAssert(NO, @"PeerConnection failed."); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + onSignalingStateChange:(RTCSignalingState)stateChanged { + NSLog(@"PCO onSignalingStateChange."); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + onAddStream:(RTCMediaStream *)stream { + NSLog(@"PCO onAddStream."); + dispatch_async(dispatch_get_main_queue(), ^(void) { + NSAssert([stream.audioTracks count] >= 1, + @"Expected at least 1 audio stream"); + //NSAssert([stream.videoTracks count] >= 1, + // @"Expected at least 1 video stream"); + // TODO(hughv): Add video support + }); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + onRemoveStream:(RTCMediaStream *)stream { + NSLog(@"PCO onRemoveStream."); + // TODO(hughv): Remove video track. +} + +- (void) + peerConnectionOnRenegotiationNeeded:(RTCPeerConnection *)peerConnection { + NSLog(@"PCO onRenegotiationNeeded."); + // TODO(hughv): Handle this. +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + onIceCandidate:(RTCIceCandidate *)candidate { + NSLog(@"PCO onIceCandidate.\n Mid[%@] Index[%d] Sdp[%@]", + candidate.sdpMid, + candidate.sdpMLineIndex, + candidate.sdp); + NSDictionary *json = + @{ @"type" : @"candidate", + @"label" : [NSNumber numberWithInt:candidate.sdpMLineIndex], + @"id" : candidate.sdpMid, + @"candidate" : candidate.sdp }; + NSError *error; + NSData *data = + [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; + if (!error) { + [_delegate sendData:data]; + } else { + NSAssert(NO, @"Unable to serialize JSON object with error: %@", + error.localizedDescription); + } +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + onIceGatheringChange:(RTCIceGatheringState)newState { + NSLog(@"PCO onIceGatheringChange. %d", newState); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + onIceConnectionChange:(RTCIceConnectionState)newState { + NSLog(@"PCO onIceConnectionChange. %d", newState); +} + +@end + +@interface APPRTCAppDelegate () + +@property(nonatomic, strong) APPRTCAppClient *client; +@property(nonatomic, strong) PCObserver *pcObserver; +@property(nonatomic, strong) RTCPeerConnection *peerConnection; +@property(nonatomic, strong) RTCPeerConnectionFactory *peerConnectionFactory; +@property(nonatomic, strong) NSMutableArray *queuedRemoteCandidates; + +@end + +@implementation APPRTCAppDelegate + +#pragma mark - UIApplicationDelegate methods + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.viewController = + [[APPRTCViewController alloc] initWithNibName:@"RTCViewController" + bundle:nil]; + self.window.rootViewController = self.viewController; + [self.window makeKeyAndVisible]; + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + [self displayLogMessage:@"Application lost focus, connection broken."]; + [self disconnect]; + [self.viewController resetUI]; +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { +} + +- (void)applicationWillTerminate:(UIApplication *)application { +} + +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication + annotation:(id)annotation { + if (self.client) { + return NO; + } + self.client = [[APPRTCAppClient alloc] init]; + self.client.iceServerDelegate = self; + self.client.messageHandler = self; + [self.client connectToRoom:url]; + return YES; +} + +- (void)displayLogMessage:(NSString *)message { + NSLog(@"%@", message); + [self.viewController displayText:message]; +} + +#pragma mark - RTCSendMessage method + +- (void)sendData:(NSData *)data { + [self.client sendData:data]; +} + +#pragma mark - IceServerDelegate method + +- (void)onIceServers:(NSArray *)servers { + self.queuedRemoteCandidates = [NSMutableArray array]; + self.peerConnectionFactory = [[RTCPeerConnectionFactory alloc] init]; + RTCMediaConstraints *constraints = [[RTCMediaConstraints alloc] init]; + self.pcObserver = [[PCObserver alloc] initWithDelegate:self]; + self.peerConnection = + [self.peerConnectionFactory peerConnectionWithIceServers:servers + constraints:constraints + delegate:self.pcObserver]; + RTCMediaStream *lms = + [self.peerConnectionFactory mediaStreamWithLabel:@"ARDAMS"]; + // TODO(hughv): Add video. + [lms addAudioTrack:[self.peerConnectionFactory audioTrackWithId:@"ARDAMSa0"]]; + [self.peerConnection addStream:lms withConstraints:constraints]; + [self displayLogMessage:@"onIceServers - add local stream."]; +} + +#pragma mark - GAEMessageHandler methods + +- (void)onOpen { + [self displayLogMessage:@"GAE onOpen - create offer."]; + RTCPair *audio = + [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"]; + // TODO(hughv): Add video. + // RTCPair *video = [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" + // value:@"true"]; + NSArray *mandatory = @[ audio /*, video*/ ]; + RTCMediaConstraints *constraints = + [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory + optionalConstraints:nil]; + [self.peerConnection createOfferWithDelegate:self constraints:constraints]; + [self displayLogMessage:@"PC - createOffer."]; +} + +- (void)onMessage:(NSString *)data { + NSString *message = [self unHTMLifyString:data]; + NSError *error; + NSDictionary *objects = [NSJSONSerialization + JSONObjectWithData:[message dataUsingEncoding:NSUTF8StringEncoding] + options:0 + error:&error]; + NSAssert(!error, + @"%@", + [NSString stringWithFormat:@"Error: %@", error.description]); + NSAssert([objects count] > 0, @"Invalid JSON object"); + NSString *value = [objects objectForKey:@"type"]; + [self displayLogMessage: + [NSString stringWithFormat:@"GAE onMessage type - %@", value]]; + if ([value compare:@"candidate"] == NSOrderedSame) { + NSString *mid = [objects objectForKey:@"id"]; + NSNumber *sdpLineIndex = [objects objectForKey:@"label"]; + NSString *sdp = [objects objectForKey:@"candidate"]; + RTCIceCandidate *candidate = + [[RTCIceCandidate alloc] initWithMid:mid + index:sdpLineIndex.intValue + sdp:sdp]; + if (self.queuedRemoteCandidates) { + [self.queuedRemoteCandidates addObject:candidate]; + } else { + [self.peerConnection addIceCandidate:candidate]; + } + } else if (([value compare:@"offer"] == NSOrderedSame) || + ([value compare:@"answer"] == NSOrderedSame)) { + NSString *sdpString = [objects objectForKey:@"sdp"]; + RTCSessionDescription *sdp = + [[RTCSessionDescription alloc] initWithType:value sdp:sdpString]; + [self.peerConnection setRemoteDescriptionWithDelegate:self + sessionDescription:sdp]; + [self displayLogMessage:@"PC - setRemoteDescription."]; + } else if ([value compare:@"bye"] == NSOrderedSame) { + [self disconnect]; + } else { + NSAssert(NO, @"Invalid message: %@", data); + } +} + +- (void)onClose { + [self displayLogMessage:@"GAE onClose."]; + [self disconnect]; +} + +- (void)onError:(int)code withDescription:(NSString *)description { + [self displayLogMessage: + [NSString stringWithFormat:@"GAE onError: %@", description]]; + [self disconnect]; +} + +#pragma mark - RTCSessionDescriptonDelegate methods + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + createSessionDescriptionCompleted:(RTCSessionDescription *)sdp + withError:(NSError *)error { + if (error) { + [self displayLogMessage:@"SDP onFailure."]; + NSAssert(NO, error.description); + return; + } + + [self displayLogMessage:@"SDP onSuccess(SDP) - set local description."]; + [self.peerConnection setLocalDescriptionWithDelegate:self + sessionDescription:sdp]; + [self displayLogMessage:@"PC setLocalDescription."]; + dispatch_async(dispatch_get_main_queue(), ^(void) { + NSDictionary *json = @{ @"type" : sdp.type, @"sdp" : sdp.description }; + NSError *error; + NSData *data = + [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; + NSAssert(!error, + @"%@", + [NSString stringWithFormat:@"Error: %@", error.description]); + [self sendData:data]; + }); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + setSessionDescriptionCompletedWithError:(NSError *)error { + if (error) { + [self displayLogMessage:@"SDP onFailure."]; + NSAssert(NO, error.description); + return; + } + + [self displayLogMessage:@"SDP onSuccess() - possibly drain candidates"]; + dispatch_async(dispatch_get_main_queue(), ^(void) { + // TODO(hughv): Handle non-initiator case. http://s10/46622051 + if (self.peerConnection.remoteDescription) { + [self displayLogMessage:@"SDP onSuccess - drain candidates"]; + [self drainRemoteCandidates]; + } + }); +} + +#pragma mark - internal methods + +- (void)disconnect { + [self.client + sendData:[@"{\"type\": \"bye\"}" dataUsingEncoding:NSUTF8StringEncoding]]; + self.peerConnection = nil; + self.peerConnectionFactory = nil; + self.pcObserver = nil; + self.client.iceServerDelegate = nil; + self.client.messageHandler = nil; + self.client = nil; +} + +- (void)drainRemoteCandidates { + for (RTCIceCandidate *candidate in self.queuedRemoteCandidates) { + [self.peerConnection addIceCandidate:candidate]; + } + self.queuedRemoteCandidates = nil; +} + +- (NSString *)unHTMLifyString:(NSString *)base { + // TODO(hughv): Investigate why percent escapes are being added. Removing + // them isn't necessary on Android. + // convert HTML escaped characters to UTF8. + NSString *removePercent = + [base stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + // remove leading and trailing ". + NSRange range; + range.length = [removePercent length] - 2; + range.location = 1; + NSString *removeQuotes = [removePercent substringWithRange:range]; + // convert \" to ". + NSString *removeEscapedQuotes = + [removeQuotes stringByReplacingOccurrencesOfString:@"\\\"" + withString:@"\""]; + // convert \\ to \. + NSString *removeBackslash = + [removeEscapedQuotes stringByReplacingOccurrencesOfString:@"\\\\" + withString:@"\\"]; + return removeBackslash; +} + +@end diff --git a/talk/examples/ios/AppRTCDemo/APPRTCViewController.h b/talk/examples/ios/AppRTCDemo/APPRTCViewController.h new file mode 100644 index 000000000..6b107a564 --- /dev/null +++ b/talk/examples/ios/AppRTCDemo/APPRTCViewController.h @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +// The view controller that is displayed when AppRTCDemo is loaded. +@interface APPRTCViewController : UIViewController + +@property (weak, nonatomic) IBOutlet UITextField *textField; +@property (weak, nonatomic) IBOutlet UITextView *textInstructions; +@property (weak, nonatomic) IBOutlet UITextView *textOutput; + +- (void)displayText:(NSString *)text; +- (void)resetUI; + +@end diff --git a/talk/examples/ios/AppRTCDemo/APPRTCViewController.m b/talk/examples/ios/AppRTCDemo/APPRTCViewController.m new file mode 100644 index 000000000..928686b68 --- /dev/null +++ b/talk/examples/ios/AppRTCDemo/APPRTCViewController.m @@ -0,0 +1,83 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "APPRTCViewController.h" + +@interface APPRTCViewController () + +@end + +@implementation APPRTCViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.textField.delegate = self; +} + +- (void)displayText:(NSString *)text { + dispatch_async(dispatch_get_main_queue(), ^(void) { + NSString *output = + [NSString stringWithFormat:@"%@\n%@", self.textOutput.text, text]; + self.textOutput.text = output; + }); +} + +- (void)resetUI { + self.textField.text = nil; + self.textField.hidden = NO; + self.textInstructions.hidden = NO; + self.textOutput.hidden = YES; + self.textOutput.text = nil; +} + +#pragma mark - UITextFieldDelegate + +- (void)textFieldDidEndEditing:(UITextField *)textField { + NSString *room = textField.text; + if ([room length] == 0) { + return; + } + textField.hidden = YES; + self.textInstructions.hidden = YES; + self.textOutput.hidden = NO; + // TODO(hughv): Instead of launching a URL with apprtc scheme, change to + // prepopulating the textField with a valid URL missing the room. This allows + // the user to have the simplicity of just entering the room or the ability to + // override to a custom appspot instance. Remove apprtc:// when this is done. + NSString *url = + [NSString stringWithFormat:@"apprtc://apprtc.appspot.com/?r=%@", room]; + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; +} + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + // There is no other control that can take focus, so manually resign focus + // when return (Join) is pressed to trigger |textFieldDidEndEditing|. + [textField resignFirstResponder]; + return YES; +} + +@end diff --git a/talk/examples/ios/AppRTCDemo/AppRTCDemo-Info.plist b/talk/examples/ios/AppRTCDemo/AppRTCDemo-Info.plist new file mode 100644 index 000000000..3ab57ed5f --- /dev/null +++ b/talk/examples/ios/AppRTCDemo/AppRTCDemo-Info.plist @@ -0,0 +1,71 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIcons + + CFBundlePrimaryIcon + + CFBundleIconFiles + + Icon.png + + + + CFBundleIdentifier + com.Google.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.google.apprtcdemo + CFBundleURLSchemes + + apprtc + + + + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarTintParameters + + UINavigationBar + + Style + UIBarStyleDefault + Translucent + + + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/talk/examples/ios/AppRTCDemo/AppRTCDemo-Prefix.pch b/talk/examples/ios/AppRTCDemo/AppRTCDemo-Prefix.pch new file mode 100644 index 000000000..3ac2c3b1a --- /dev/null +++ b/talk/examples/ios/AppRTCDemo/AppRTCDemo-Prefix.pch @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +// +// Prefix header for all source files of the 'AppRTCDemo' target in the +// 'AppRTCDemo' project +// + +#import + +#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_6_0 +#warning "This project uses features only available in iOS SDK 6.0 and later." +#endif + +#import +#import diff --git a/talk/examples/ios/AppRTCDemo/Default.png b/talk/examples/ios/AppRTCDemo/Default.png new file mode 100644 index 0000000000000000000000000000000000000000..4c8ca6f693f96d511e9113c0eb59eec552354e42 GIT binary patch literal 6540 zcmeAS@N?(olHy`uVBq!ia0y~yU~~ZD2OMlbkt;o0To@QwR5G2N13aCb6#|O#(=u~X z85k@CTSM>X-wqM6>&y>YB4)1;;ojbLbbV-W^iFB1wa3^zCog^LCAReC4K0-?R_2{6 zrP*)4+_uWUy3w5N52M3PW_}MFMP9a~>YLvVZ1D_k*IMQ2QT^fwzoOb(*3gH$%aYWC zkHmcab=va2<#X%jakpJ;<1@F;k__#bwtC&%^D0v(FBh9K&$sK+<}2RJS609D)17$w ztdQP8(eLM8Ka}m_IQ@3wyMKP)l=oM4-?`YS_*P?4V_ORLPxsj&7Ju#kH;>6^Kp?T7~ zl+q?{UOOqV==?+d{=)5s|M~T1mwtH@+Z^$G&eEO9JNP^AX@3jZ*J*!!>lc|1-W%fA z@AOQpXZ_Lt>rxFXrGp*zLPiW@uo_c7C{As>j zWeX)wi+LTp_)@KYZCX{j;H?|1yXT4DnlS(Fr8gyP5|uaX_gLvaW0ScZdnG7o+u{T6 zFI-%d{ls*WuCDa5UJ@|RXv&ejZe}*BMkiWY51&pnRPw(hlykSzvj6e%mYz-GdvzBD zF10?szF_~!jS=?2HyQuPCvARXAe}C}WP|yQ*>5~~=*Nxq8+HHW1~FMDRCP^TcacKuk$ z(U#REVv)D!PhJ*ecH-ELFUrfyV&*)Z)>UCOuS?yd^L@Afk>ihynYPc{^CRwu+JHX+#$@YsC4c|l0tGigsn@jy) zXD($Ouk>H+V(Mr6NQT0S9BFM~V6nkj;1OBOz`zY;a|<&v%$g$sEJPk;hD4M^`1)8S z=jZArrsOB3>Q&?x097+E*i={nnYpPYi3%0DIeEoa6}C!X6;?ntNLXJ<0j#7X+g2&U zH$cHTzbI9~RL@Y)NXd>%K|#T$C?(A*$i)q+9mum)$|xx*u+rBrFE7_CH`dE9O4m2E zw6xSWFw!?N(gmu}Ew0QfNvzP#D^`XW0yD=YwK%ybv!En1KTiQ3|)OBHVcpi zp&D%TL4k-AsNfg_g$9~9p}$+4Ynr|VULLgiakg&)DD)EWO!OHC@snXr}UI${nVUP zpr1>Mf#G6^ng~;pt%^&NvQm>vU@-wn)!_JWN=(;B61LIDR86%A1?G9U(@`={MPdPF zbOKdd`R1o&rd7HmmZaJl85kPr8kp-EnTHsfS{ayIfdU*&4N@e5WSomq6HD@oLh|!- z?7;Dr3*ssm=^5w&a}>G?yzvAH17L|`#|6|0E4}QvA~xC{V_*wu2^AHZU}H9f($4F$btFf{}TLQXUhF5fht1@YV$^ z9BUdFV+73^nIsvRXRM40U}6b7z_6}kHbY}i1LK(xT@6Mi?F5GKBfbp|ZU-3BR*6kv zXcRSQ(0-)mprD+wTr)o_4I;(%zOu)+jEgNB)_SXCVoSa}|F?cfwR!69+L=W3IX z!UiU`0@ph%94Rb33Cpq^IY*r_8XBW%V>G9XmK&p`=xCiXTEmXEH%41uqixaAmicH0 zVYIt6!aI*K%s=kP-v##6IXG + +// These methods will be called by the AppEngine chanel. The documentation +// for these methods is found here. (Yes, it is a JS API.) +// https://developers.google.com/appengine/docs/java/channel/javascript +@protocol GAEMessageHandler + +- (void)onOpen; +- (void)onMessage:(NSString *)data; +- (void)onClose; +- (void)onError:(int)code withDescription:(NSString *)description; + +@end + +// Initialize with a token for an AppRTC data channel. This will load +// ios_channel.html and use the token to establish a data channel between the +// application and AppEngine. +@interface GAEChannelClient : NSObject + +- (id)initWithToken:(NSString *)token delegate:(id)delegate; + +@end diff --git a/talk/examples/ios/AppRTCDemo/GAEChannelClient.m b/talk/examples/ios/AppRTCDemo/GAEChannelClient.m new file mode 100644 index 000000000..9126f67c7 --- /dev/null +++ b/talk/examples/ios/AppRTCDemo/GAEChannelClient.m @@ -0,0 +1,104 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import "GAEChannelClient.h" + +#import "RTCPeerConnectionFactory.h" + +@interface GAEChannelClient () + +@property(nonatomic, assign) id delegate; +@property(nonatomic, strong) UIWebView *webView; + +@end + +@implementation GAEChannelClient + +- (id)initWithToken:(NSString *)token delegate:(id)delegate { + self = [super init]; + if (self) { + _webView = [[UIWebView alloc] init]; + _webView.delegate = self; + _delegate = delegate; + NSString *htmlPath = + [[NSBundle mainBundle] pathForResource:@"ios_channel" ofType:@"html"]; + NSURL *htmlUrl = [NSURL fileURLWithPath:htmlPath]; + NSString *path = [NSString stringWithFormat:@"%@?token=%@", + [htmlUrl absoluteString], + token]; + + [_webView + loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:path]]]; + } + return self; +} + +- (void)dealloc { + _webView.delegate = nil; + [_webView stopLoading]; +} + +#pragma mark - UIWebViewDelegate method + +- (BOOL)webView:(UIWebView *)webView + shouldStartLoadWithRequest:(NSURLRequest *)request + navigationType:(UIWebViewNavigationType)navigationType { + NSString *scheme = [request.URL scheme]; + if ([scheme compare:@"js-frame"] != NSOrderedSame) { + return YES; + } + NSString *resourceSpecifier = [request.URL resourceSpecifier]; + NSRange range = [resourceSpecifier rangeOfString:@":"]; + NSString *method; + NSString *message; + if (range.length == 0 && range.location == NSNotFound) { + method = resourceSpecifier; + } else { + method = [resourceSpecifier substringToIndex:range.location]; + message = [resourceSpecifier substringFromIndex:range.location + 1]; + } + dispatch_async(dispatch_get_main_queue(), ^(void) { + if ([method compare:@"onopen"] == NSOrderedSame) { + [self.delegate onOpen]; + } else if ([method compare:@"onmessage"] == NSOrderedSame) { + [self.delegate onMessage:message]; + } else if ([method compare:@"onclose"] == NSOrderedSame) { + [self.delegate onClose]; + } else if ([method compare:@"onerror"] == NSOrderedSame) { + // TODO(hughv): Get error. + int code = -1; + NSString *description = message; + [self.delegate onError:code withDescription:description]; + } else { + NSAssert(NO, @"Invalid message sent from UIWebView: %@", + resourceSpecifier); + } + }); + return YES; +} + +@end diff --git a/talk/examples/ios/AppRTCDemo/en.lproj/APPRTCViewController.xib b/talk/examples/ios/AppRTCDemo/en.lproj/APPRTCViewController.xib new file mode 100644 index 000000000..cd73ea64e --- /dev/null +++ b/talk/examples/ios/AppRTCDemo/en.lproj/APPRTCViewController.xib @@ -0,0 +1,529 @@ + + + + 1552 + 12D78 + 3084 + 1187.37 + 626.00 + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + 2083 + + + IBNSLayoutConstraint + IBProxyObject + IBUITextField + IBUITextView + IBUIView + + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + PluginDependencyRecalculationVersion + + + + + IBFilesOwner + IBCocoaTouchFramework + + + IBFirstResponder + IBCocoaTouchFramework + + + + 274 + + + + 292 + {{20, 20}, {280, 141}} + + + _NS:9 + + 1 + MSAxIDEAA + + YES + NO + IBCocoaTouchFramework + Use Safari and open a URL with a scheme of apprtc to load the test app and connect. i.e. apprtc://apprtc.appspot.com/?r=12345678 Or just enter the room below to connect to apprtc. + + 2 + IBCocoaTouchFramework + + + 1 + 14 + + + Helvetica + 14 + 16 + + + + + 292 + {{20, 180}, {280, 30}} + + + _NS:9 + NO + YES + IBCocoaTouchFramework + 0 + + 3 + apprtc room + + 3 + MAA + + 2 + + + YES + 17 + + 2 + 3 + IBCocoaTouchFramework + + + + + + + -2147483356 + {{20, 20}, {280, 508}} + + _NS:9 + + YES + YES + IBCocoaTouchFramework + NO + + + 2 + IBCocoaTouchFramework + + + + + + {{0, 20}, {320, 548}} + + + 3 + MC43NQA + + + NO + + + IBUIScreenMetrics + + YES + + + + + + {320, 568} + {568, 320} + + + IBCocoaTouchFramework + Retina 4 Full Screen + 2 + + IBCocoaTouchFramework + + + + + + + view + + + + 7 + + + + textField + + + + 108 + + + + textInstructions + + + + 127 + + + + textOutput + + + + 138 + + + + + + 0 + + + + + + -1 + + + File's Owner + + + -2 + + + + + 6 + + + + + 6 + 0 + + 6 + 1 + + 20 + + 1000 + + 8 + 29 + 3 + + + + 3 + 0 + + 3 + 1 + + 180 + + 1000 + + 3 + 9 + 3 + + + + 5 + 0 + + 5 + 1 + + 20 + + 1000 + + 8 + 29 + 3 + + + + 3 + 0 + + 3 + 1 + + 20 + + 1000 + + 8 + 29 + 3 + + + + 6 + 0 + + 6 + 1 + + 20 + + 1000 + + 8 + 29 + 3 + + + + 4 + 0 + + 4 + 1 + + 20 + + 1000 + + 8 + 29 + 3 + + + + 5 + 0 + + 5 + 1 + + 20 + + 1000 + + 8 + 29 + 3 + + + + 6 + 0 + + 6 + 1 + + 20 + + 1000 + + 8 + 29 + 3 + + + + 3 + 0 + + 3 + 1 + + 20 + + 1000 + + 8 + 29 + 3 + + + + 5 + 0 + + 5 + 1 + + 20 + + 1000 + + 8 + 29 + 3 + + + + + + + + + 57 + + + + + 8 + 0 + + 0 + 1 + + 141 + + 1000 + + 3 + 9 + 1 + + + + + + 62 + + + + + 63 + + + + + 66 + + + + + 104 + + + + + + 107 + + + + + 123 + + + + + 124 + + + + + 126 + + + + + 128 + + + + + + 133 + + + + + 136 + + + + + 137 + + + + + 139 + + + + + + + APPRTCViewController + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + UIResponder + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + + + + + + + + + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + + + 139 + + + + + NSLayoutConstraint + NSObject + + IBProjectSource + ./Classes/NSLayoutConstraint.h + + + + + 0 + IBCocoaTouchFramework + YES + 3 + YES + 2083 + + diff --git a/talk/examples/ios/AppRTCDemo/ios_channel.html b/talk/examples/ios/AppRTCDemo/ios_channel.html new file mode 100644 index 000000000..a55b8f48b --- /dev/null +++ b/talk/examples/ios/AppRTCDemo/ios_channel.html @@ -0,0 +1,88 @@ + + + + + + + + + diff --git a/talk/examples/ios/AppRTCDemo/main.m b/talk/examples/ios/AppRTCDemo/main.m new file mode 100644 index 000000000..bf35f4cbf --- /dev/null +++ b/talk/examples/ios/AppRTCDemo/main.m @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#import + +#import "APPRTCAppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain( + argc, argv, nil, NSStringFromClass([APPRTCAppDelegate class])); + } +} diff --git a/talk/examples/ios/Icon.png b/talk/examples/ios/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..55773ca9d9967360a259821d868a62b6ccc14b47 GIT binary patch literal 62469 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 z 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↧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 literal 0 HcmV?d00001 diff --git a/talk/examples/ios/README b/talk/examples/ios/README new file mode 100644 index 000000000..9dbbd3fe0 --- /dev/null +++ b/talk/examples/ios/README @@ -0,0 +1,28 @@ +This directory contains an example iOS client for http://apprtc.appspot.com + +Example of building & using the app: + +cd /trunk/talk +- Open libjingle.xcproj. Select iPhone or iPad simulator and build everything. + Then switch to iOS device and build everything. This creates x86 and ARM + archives. +cd examples/ios +./makeLibs.sh +- This will generate fat archives containing both targets and copy them to + ./libs. +- This step must be rerun every time you run gclient sync or build the API + libraries. +- Open AppRTCDemo.xcodeproj, select your device or simulator and run. +- If you have any problems deploying for the first time, check the project + properties to ensure that the Bundle Identifier matches your phone + provisioning profile. Or use the simulator as it doesn't require a profile. + +- In desktop chrome, navigate to http://apprtc.appspot.com and note the r= + room number in the resulting URL. + +- Enter that number into the text field on the phone. + +- Alternatively, you can background the app and launch Safari. In Safari, open + the url apprtc://apprtc.appspot.com/?r= where is the room name. + Other options are to put the link in an email and send it to your self. + Clicking on it will launch AppRTCDemo and navigate to the room. diff --git a/talk/examples/ios/makeLibs.sh b/talk/examples/ios/makeLibs.sh new file mode 100755 index 000000000..b5bf3cda7 --- /dev/null +++ b/talk/examples/ios/makeLibs.sh @@ -0,0 +1,30 @@ +#!/bin/bash -e + +if [ "$(dirname $0)" != "." ]; then + echo "$(basename $0) must be run from talk/examples/ios as ./$(basename $0)" + exit 1 +fi + +rm -rf libs +mkdir libs + +cd ../../../xcodebuild/Debug-iphoneos +for f in *.a; do + if [ -f "../Debug-iphonesimulator/$f" ]; then + echo "creating fat static library $f" + lipo -create "$f" "../Debug-iphonesimulator/$f" -output "../../talk/examples/ios/libs/$f" + else + echo "" + echo "$f was not built for the simulator." + echo "" + fi +done + +cd ../Debug-iphonesimulator +for f in *.a; do + if [ ! -f "../Debug-iphoneos/$f" ]; then + echo "" + echo "$f was not built for the iPhone." + echo "" + fi +done diff --git a/talk/examples/login/login_main.cc b/talk/examples/login/login_main.cc new file mode 100644 index 000000000..55bb893ad --- /dev/null +++ b/talk/examples/login/login_main.cc @@ -0,0 +1,66 @@ +/* + * libjingle + * Copyright 2004--2005, 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 +#include + +#include "talk/base/thread.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/xmppclientsettings.h" +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmppthread.h" + +int main(int argc, char **argv) { + std::cout << "OAuth Access Token: "; + std::string auth_token; + std::getline(std::cin, auth_token); + + std::cout << "User Name: "; + std::string username; + std::getline(std::cin, username); + + // Start xmpp on a different thread + buzz::XmppThread thread; + thread.Start(); + + buzz::XmppClientSettings xcs; + xcs.set_user(username.c_str()); + xcs.set_host("gmail.com"); + xcs.set_use_tls(buzz::TLS_DISABLED); + xcs.set_auth_token(buzz::AUTH_MECHANISM_OAUTH2, + auth_token.c_str()); + xcs.set_server(talk_base::SocketAddress("talk.google.com", 5222)); + thread.Login(xcs); + + // Use main thread for console input + std::string line; + while (std::getline(std::cin, line)) { + if (line == "quit") + break; + } + return 0; +} diff --git a/talk/examples/pcp/pcp_main.cc b/talk/examples/pcp/pcp_main.cc new file mode 100644 index 000000000..1b8974dea --- /dev/null +++ b/talk/examples/pcp/pcp_main.cc @@ -0,0 +1,715 @@ +#define _CRT_SECURE_NO_DEPRECATE 1 + +#include + +#if defined(POSIX) +#include +#endif + +#include +#include +#include + +#if HAVE_CONFIG_H +#include "config.h" +#endif // HAVE_CONFIG_H + +#include "talk/base/sslconfig.h" // For SSL_USE_* + +#if SSL_USE_OPENSSL +#define USE_SSL_TUNNEL +#endif + +#include "talk/base/basicdefs.h" +#include "talk/base/common.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/ssladapter.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/sessionmanager.h" +#include "talk/p2p/client/autoportallocator.h" +#include "talk/p2p/client/sessionmanagertask.h" +#include "talk/xmpp/xmppengine.h" +#ifdef USE_SSL_TUNNEL +#include "talk/session/tunnel/securetunnelsessionclient.h" +#endif +#include "talk/session/tunnel/tunnelsessionclient.h" +#include "talk/xmpp/xmppclient.h" +#include "talk/xmpp/xmppclientsettings.h" +#include "talk/xmpp/xmpppump.h" +#include "talk/xmpp/xmppsocket.h" + +#ifndef MAX_PATH +#define MAX_PATH 256 +#endif + +#if defined(_MSC_VER) && (_MSC_VER < 1400) +// The following are necessary to properly link when compiling STL without +// /EHsc, otherwise known as C++ exceptions. +void __cdecl std::_Throw(const std::exception &) {} +std::_Prhand std::_Raise_handler = 0; +#endif + +enum { + MSG_LOGIN_COMPLETE = 1, + MSG_LOGIN_FAILED, + MSG_DONE, +}; + +buzz::Jid gUserJid; +talk_base::InsecureCryptStringImpl gUserPass; +std::string gXmppHost = "talk.google.com"; +int gXmppPort = 5222; +buzz::TlsOptions gXmppUseTls = buzz::TLS_REQUIRED; + +class DebugLog : public sigslot::has_slots<> { +public: + DebugLog() : + debug_input_buf_(NULL), debug_input_len_(0), debug_input_alloc_(0), + debug_output_buf_(NULL), debug_output_len_(0), debug_output_alloc_(0), + censor_password_(false) + {} + char * debug_input_buf_; + int debug_input_len_; + int debug_input_alloc_; + char * debug_output_buf_; + int debug_output_len_; + int debug_output_alloc_; + bool censor_password_; + + void Input(const char * data, int len) { + if (debug_input_len_ + len > debug_input_alloc_) { + char * old_buf = debug_input_buf_; + debug_input_alloc_ = 4096; + while (debug_input_alloc_ < debug_input_len_ + len) { + debug_input_alloc_ *= 2; + } + debug_input_buf_ = new char[debug_input_alloc_]; + memcpy(debug_input_buf_, old_buf, debug_input_len_); + delete[] old_buf; + } + memcpy(debug_input_buf_ + debug_input_len_, data, len); + debug_input_len_ += len; + DebugPrint(debug_input_buf_, &debug_input_len_, false); + } + + void Output(const char * data, int len) { + if (debug_output_len_ + len > debug_output_alloc_) { + char * old_buf = debug_output_buf_; + debug_output_alloc_ = 4096; + while (debug_output_alloc_ < debug_output_len_ + len) { + debug_output_alloc_ *= 2; + } + debug_output_buf_ = new char[debug_output_alloc_]; + memcpy(debug_output_buf_, old_buf, debug_output_len_); + delete[] old_buf; + } + memcpy(debug_output_buf_ + debug_output_len_, data, len); + debug_output_len_ += len; + DebugPrint(debug_output_buf_, &debug_output_len_, true); + } + + static bool + IsAuthTag(const char * str, size_t len) { + if (str[0] == '<' && str[1] == 'a' && + str[2] == 'u' && + str[3] == 't' && + str[4] == 'h' && + str[5] <= ' ') { + std::string tag(str, len); + + if (tag.find("mechanism") != std::string::npos) + return true; + + } + return false; + } + + void + DebugPrint(char * buf, int * plen, bool output) { + int len = *plen; + if (len > 0) { + time_t tim = time(NULL); + struct tm * now = localtime(&tim); + char *time_string = asctime(now); + if (time_string) { + size_t time_len = strlen(time_string); + if (time_len > 0) { + time_string[time_len-1] = 0; // trim off terminating \n + } + } + LOG(INFO) << (output ? "SEND >>>>>>>>>>>>>>>>>>>>>>>>>" : "RECV <<<<<<<<<<<<<<<<<<<<<<<<<") + << " : " << time_string; + + bool indent; + int start = 0, nest = 3; + for (int i = 0; i < len; i += 1) { + if (buf[i] == '>') { + if ((i > 0) && (buf[i-1] == '/')) { + indent = false; + } else if ((start + 1 < len) && (buf[start + 1] == '/')) { + indent = false; + nest -= 2; + } else { + indent = true; + } + + // Output a tag + LOG(INFO) << std::setw(nest) << " " << std::string(buf + start, i + 1 - start); + + if (indent) + nest += 2; + + // Note if it's a PLAIN auth tag + if (IsAuthTag(buf + start, i + 1 - start)) { + censor_password_ = true; + } + + // incr + start = i + 1; + } + + if (buf[i] == '<' && start < i) { + if (censor_password_) { + LOG(INFO) << std::setw(nest) << " " << "## TEXT REMOVED ##"; + censor_password_ = false; + } + else { + LOG(INFO) << std::setw(nest) << " " << std::string(buf + start, i - start); + } + start = i; + } + } + len = len - start; + memcpy(buf, buf + start, len); + *plen = len; + } + } + +}; + +static DebugLog debug_log_; + +// Prints out a usage message then exits. +void Usage() { + std::cerr << "Usage:" << std::endl; + std::cerr << " pcp [options] (server mode)" << std::endl; + std::cerr << " pcp [options] : (client sending)" << std::endl; + std::cerr << " pcp [options] : (client rcv'ing)" << std::endl; + std::cerr << " --verbose" << std::endl; + std::cerr << " --xmpp-host=" << std::endl; + std::cerr << " --xmpp-port=" << std::endl; + std::cerr << " --xmpp-use-tls=(true|false)" << std::endl; + exit(1); +} + +// Prints out an error message, a usage message, then exits. +void Error(const std::string& msg) { + std::cerr << "error: " << msg << std::endl; + std::cerr << std::endl; + Usage(); +} + +void FatalError(const std::string& msg) { + std::cerr << "error: " << msg << std::endl; + std::cerr << std::endl; + exit(1); +} + +// Determines whether the given string is an option. If so, the name and +// value are appended to the given strings. +bool ParseArg(const char* arg, std::string* name, std::string* value) { + if (strncmp(arg, "--", 2) != 0) + return false; + + const char* eq = strchr(arg + 2, '='); + if (eq) { + if (name) + name->append(arg + 2, eq); + if (value) + value->append(eq + 1, arg + strlen(arg)); + } else { + if (name) + name->append(arg + 2, arg + strlen(arg)); + if (value) + value->clear(); + } + + return true; +} + +int ParseIntArg(const std::string& name, const std::string& value) { + char* end; + long val = strtol(value.c_str(), &end, 10); + if (*end != '\0') + Error(std::string("value of option ") + name + " must be an integer"); + return static_cast(val); +} + +#ifdef WIN32 +#pragma warning(push) +// disable "unreachable code" warning b/c it varies between dbg and opt +#pragma warning(disable: 4702) +#endif +bool ParseBoolArg(const std::string& name, const std::string& value) { + if (value == "true") + return true; + else if (value == "false") + return false; + else { + Error(std::string("value of option ") + name + " must be true or false"); + return false; + } +} +#ifdef WIN32 +#pragma warning(pop) +#endif + +void ParseFileArg(const char* arg, buzz::Jid* jid, std::string* file) { + const char* sep = strchr(arg, ':'); + if (!sep) { + *file = arg; + } else { + buzz::Jid jid_arg(std::string(arg, sep-arg)); + if (jid_arg.IsBare()) + Error("A full JID is required for the source or destination arguments."); + *jid = jid_arg; + *file = std::string(sep+1); + } +} + + +void SetConsoleEcho(bool on) { +#ifdef WIN32 + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + if ((hIn == INVALID_HANDLE_VALUE) || (hIn == NULL)) + return; + + DWORD mode; + if (!GetConsoleMode(hIn, &mode)) + return; + + if (on) { + mode = mode | ENABLE_ECHO_INPUT; + } else { + mode = mode & ~ENABLE_ECHO_INPUT; + } + + SetConsoleMode(hIn, mode); +#else + int re; + if (on) + re = system("stty echo"); + else + re = system("stty -echo"); + if (-1 == re) + return; +#endif +} + +// Fills in a settings object with the values from the arguments. +buzz::XmppClientSettings LoginSettings() { + buzz::XmppClientSettings xcs; + xcs.set_user(gUserJid.node()); + xcs.set_host(gUserJid.domain()); + xcs.set_resource("pcp"); + xcs.set_pass(talk_base::CryptString(gUserPass)); + talk_base::SocketAddress server(gXmppHost, gXmppPort); + xcs.set_server(server); + xcs.set_use_tls(gXmppUseTls); + return xcs; +} + +// Runs the current thread until a message with the given ID is seen. +uint32 Loop(const std::vector& ids) { + talk_base::Message msg; + while (talk_base::Thread::Current()->Get(&msg)) { + if (msg.phandler == NULL) { + if (std::find(ids.begin(), ids.end(), msg.message_id) != ids.end()) + return msg.message_id; + std::cout << "orphaned message: " << msg.message_id; + continue; + } + talk_base::Thread::Current()->Dispatch(&msg); + } + return 0; +} + +#ifdef WIN32 +#pragma warning(disable:4355) +#endif + +class CustomXmppPump : public buzz::XmppPumpNotify, public buzz::XmppPump { +public: + CustomXmppPump() : XmppPump(this), server_(false) { } + + void Serve(cricket::TunnelSessionClient* client) { + client->SignalIncomingTunnel.connect(this, + &CustomXmppPump::OnIncomingTunnel); + server_ = true; + } + + void OnStateChange(buzz::XmppEngine::State state) { + switch (state) { + case buzz::XmppEngine::STATE_START: + std::cout << "connecting..." << std::endl; + break; + case buzz::XmppEngine::STATE_OPENING: + std::cout << "logging in..." << std::endl; + break; + case buzz::XmppEngine::STATE_OPEN: + std::cout << "logged in..." << std::endl; + talk_base::Thread::Current()->Post(NULL, MSG_LOGIN_COMPLETE); + break; + case buzz::XmppEngine::STATE_CLOSED: + std::cout << "logged out..." << std::endl; + talk_base::Thread::Current()->Post(NULL, MSG_LOGIN_FAILED); + break; + } + } + + void OnIncomingTunnel(cricket::TunnelSessionClient* client, buzz::Jid jid, + std::string description, cricket::Session* session) { + std::cout << "IncomingTunnel from " << jid.Str() + << ": " << description << std::endl; + if (!server_ || file_) { + client->DeclineTunnel(session); + return; + } + std::string filename; + bool send; + if (strncmp(description.c_str(), "send:", 5) == 0) { + send = true; + } else if (strncmp(description.c_str(), "recv:", 5) == 0) { + send = false; + } else { + client->DeclineTunnel(session); + return; + } + filename = description.substr(5); + talk_base::StreamInterface* stream = client->AcceptTunnel(session); + if (!ProcessStream(stream, filename, send)) + talk_base::Thread::Current()->Post(NULL, MSG_DONE); + + // TODO: There is a potential memory leak, however, since the PCP + // app doesn't work right now, I can't verify the fix actually works, so + // comment out the following line until we fix the PCP app. + + // delete stream; + } + + bool ProcessStream(talk_base::StreamInterface* stream, + const std::string& filename, bool send) { + ASSERT(file_); + sending_ = send; + file_.reset(new talk_base::FileStream); + buffer_len_ = 0; + int err; + if (!file_->Open(filename.c_str(), sending_ ? "rb" : "wb", &err)) { + std::cerr << "Error opening <" << filename << ">: " + << std::strerror(err) << std::endl; + return false; + } + stream->SignalEvent.connect(this, &CustomXmppPump::OnStreamEvent); + if (stream->GetState() == talk_base::SS_CLOSED) { + std::cerr << "Failed to establish P2P tunnel" << std::endl; + return false; + } + if (stream->GetState() == talk_base::SS_OPEN) { + OnStreamEvent(stream, + talk_base::SE_OPEN | talk_base::SE_READ | talk_base::SE_WRITE, 0); + } + return true; + } + + void OnStreamEvent(talk_base::StreamInterface* stream, int events, + int error) { + if (events & talk_base::SE_CLOSE) { + if (error == 0) { + std::cout << "Tunnel closed normally" << std::endl; + } else { + std::cout << "Tunnel closed with error: " << error << std::endl; + } + Cleanup(stream); + return; + } + if (events & talk_base::SE_OPEN) { + std::cout << "Tunnel connected" << std::endl; + } + talk_base::StreamResult result; + size_t count; + if (sending_ && (events & talk_base::SE_WRITE)) { + LOG(LS_VERBOSE) << "Tunnel SE_WRITE"; + while (true) { + size_t write_pos = 0; + while (write_pos < buffer_len_) { + result = stream->Write(buffer_ + write_pos, buffer_len_ - write_pos, + &count, &error); + if (result == talk_base::SR_SUCCESS) { + write_pos += count; + continue; + } + if (result == talk_base::SR_BLOCK) { + buffer_len_ -= write_pos; + memmove(buffer_, buffer_ + write_pos, buffer_len_); + LOG(LS_VERBOSE) << "Tunnel write block"; + return; + } + if (result == talk_base::SR_EOS) { + std::cout << "Tunnel closed unexpectedly on write" << std::endl; + } else { + std::cout << "Tunnel write error: " << error << std::endl; + } + Cleanup(stream); + return; + } + buffer_len_ = 0; + while (buffer_len_ < sizeof(buffer_)) { + result = file_->Read(buffer_ + buffer_len_, + sizeof(buffer_) - buffer_len_, + &count, &error); + if (result == talk_base::SR_SUCCESS) { + buffer_len_ += count; + continue; + } + if (result == talk_base::SR_EOS) { + if (buffer_len_ > 0) + break; + std::cout << "End of file" << std::endl; + // A hack until we have friendly shutdown + Cleanup(stream, true); + return; + } else if (result == talk_base::SR_BLOCK) { + std::cout << "File blocked unexpectedly on read" << std::endl; + } else { + std::cout << "File read error: " << error << std::endl; + } + Cleanup(stream); + return; + } + } + } + if (!sending_ && (events & talk_base::SE_READ)) { + LOG(LS_VERBOSE) << "Tunnel SE_READ"; + while (true) { + buffer_len_ = 0; + while (buffer_len_ < sizeof(buffer_)) { + result = stream->Read(buffer_ + buffer_len_, + sizeof(buffer_) - buffer_len_, + &count, &error); + if (result == talk_base::SR_SUCCESS) { + buffer_len_ += count; + continue; + } + if (result == talk_base::SR_BLOCK) { + if (buffer_len_ > 0) + break; + LOG(LS_VERBOSE) << "Tunnel read block"; + return; + } + if (result == talk_base::SR_EOS) { + std::cout << "Tunnel closed unexpectedly on read" << std::endl; + } else { + std::cout << "Tunnel read error: " << error << std::endl; + } + Cleanup(stream); + return; + } + size_t write_pos = 0; + while (write_pos < buffer_len_) { + result = file_->Write(buffer_ + write_pos, buffer_len_ - write_pos, + &count, &error); + if (result == talk_base::SR_SUCCESS) { + write_pos += count; + continue; + } + if (result == talk_base::SR_EOS) { + std::cout << "File closed unexpectedly on write" << std::endl; + } else if (result == talk_base::SR_BLOCK) { + std::cout << "File blocked unexpectedly on write" << std::endl; + } else { + std::cout << "File write error: " << error << std::endl; + } + Cleanup(stream); + return; + } + } + } + } + + void Cleanup(talk_base::StreamInterface* stream, bool delay = false) { + LOG(LS_VERBOSE) << "Closing"; + stream->Close(); + file_.reset(); + if (!server_) { + if (delay) + talk_base::Thread::Current()->PostDelayed(2000, NULL, MSG_DONE); + else + talk_base::Thread::Current()->Post(NULL, MSG_DONE); + } + } + +private: + bool server_, sending_; + talk_base::scoped_ptr file_; + char buffer_[1024 * 64]; + size_t buffer_len_; +}; + +int main(int argc, char **argv) { + talk_base::LogMessage::LogThreads(); + talk_base::LogMessage::LogTimestamps(); + + // TODO: Default the username to the current users's name. + + // Parse the arguments. + + int index = 1; + while (index < argc) { + std::string name, value; + if (!ParseArg(argv[index], &name, &value)) + break; + + if (name == "help") { + Usage(); + } else if (name == "verbose") { + talk_base::LogMessage::LogToDebug(talk_base::LS_VERBOSE); + } else if (name == "xmpp-host") { + gXmppHost = value; + } else if (name == "xmpp-port") { + gXmppPort = ParseIntArg(name, value); + } else if (name == "xmpp-use-tls") { + gXmppUseTls = ParseBoolArg(name, value)? + buzz::TLS_REQUIRED : buzz::TLS_DISABLED; + } else { + Error(std::string("unknown option: ") + name); + } + + index += 1; + } + + if (index >= argc) + Error("bad arguments"); + gUserJid = buzz::Jid(argv[index++]); + if (!gUserJid.IsValid()) + Error("bad arguments"); + + char path[MAX_PATH]; +#if WIN32 + GetCurrentDirectoryA(MAX_PATH, path); +#else + if (NULL == getcwd(path, MAX_PATH)) + Error("Unable to get current path"); +#endif + + std::cout << "Directory: " << std::string(path) << std::endl; + + buzz::Jid gSrcJid; + buzz::Jid gDstJid; + std::string gSrcFile; + std::string gDstFile; + + bool as_server = true; + if (index + 2 == argc) { + ParseFileArg(argv[index], &gSrcJid, &gSrcFile); + ParseFileArg(argv[index+1], &gDstJid, &gDstFile); + if(gSrcJid.Str().empty() == gDstJid.Str().empty()) + Error("Exactly one of source JID or destination JID must be empty."); + as_server = false; + } else if (index != argc) { + Error("bad arguments"); + } + + std::cout << "Password: "; + SetConsoleEcho(false); + std::cin >> gUserPass.password(); + SetConsoleEcho(true); + std::cout << std::endl; + + talk_base::InitializeSSL(); + // Log in. + CustomXmppPump pump; + pump.client()->SignalLogInput.connect(&debug_log_, &DebugLog::Input); + pump.client()->SignalLogOutput.connect(&debug_log_, &DebugLog::Output); + pump.DoLogin(LoginSettings(), new buzz::XmppSocket(gXmppUseTls), 0); + //new XmppAuth()); + + // Wait until login succeeds. + std::vector ids; + ids.push_back(MSG_LOGIN_COMPLETE); + ids.push_back(MSG_LOGIN_FAILED); + if (MSG_LOGIN_FAILED == Loop(ids)) + FatalError("Failed to connect"); + + { + talk_base::scoped_ptr presence( + new buzz::XmlElement(buzz::QN_PRESENCE)); + presence->AddElement(new buzz::XmlElement(buzz::QN_PRIORITY)); + presence->AddText("-1", 1); + pump.SendStanza(presence.get()); + } + + std::string user_jid_str = pump.client()->jid().Str(); + std::cout << "Logged in as " << user_jid_str << std::endl; + + // Prepare the random number generator. + talk_base::InitRandom(user_jid_str.c_str(), user_jid_str.size()); + + // Create the P2P session manager. + talk_base::BasicNetworkManager network_manager; + AutoPortAllocator allocator(&network_manager, "pcp_agent"); + allocator.SetXmppClient(pump.client()); + cricket::SessionManager session_manager(&allocator); +#ifdef USE_SSL_TUNNEL + cricket::SecureTunnelSessionClient session_client(pump.client()->jid(), + &session_manager); + if (!session_client.GenerateIdentity()) + FatalError("Failed to generate SSL identity"); +#else // !USE_SSL_TUNNEL + cricket::TunnelSessionClient session_client(pump.client()->jid(), + &session_manager); +#endif // USE_SSL_TUNNEL + cricket::SessionManagerTask *receiver = + new cricket::SessionManagerTask(pump.client(), &session_manager); + receiver->EnableOutgoingMessages(); + receiver->Start(); + + bool success = true; + + // Establish the appropriate connection. + if (as_server) { + pump.Serve(&session_client); + } else { + talk_base::StreamInterface* stream = NULL; + std::string filename; + bool sending; + if (gSrcJid.Str().empty()) { + std::string message("recv:"); + message.append(gDstFile); + stream = session_client.CreateTunnel(gDstJid, message); + filename = gSrcFile; + sending = true; + } else { + std::string message("send:"); + message.append(gSrcFile); + stream = session_client.CreateTunnel(gSrcJid, message); + filename = gDstFile; + sending = false; + } + success = pump.ProcessStream(stream, filename, sending); + } + + if (success) { + // Wait until the copy is done. + ids.clear(); + ids.push_back(MSG_DONE); + ids.push_back(MSG_LOGIN_FAILED); + Loop(ids); + } + + // Log out. + pump.DoDisconnect(); + + return 0; +} diff --git a/talk/examples/peerconnection/client/conductor.cc b/talk/examples/peerconnection/client/conductor.cc new file mode 100644 index 000000000..b35a05482 --- /dev/null +++ b/talk/examples/peerconnection/client/conductor.cc @@ -0,0 +1,494 @@ +/* + * libjingle + * Copyright 2012, 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/examples/peerconnection/client/conductor.h" + +#include + +#include "talk/app/webrtc/videosourceinterface.h" +#include "talk/base/common.h" +#include "talk/base/json.h" +#include "talk/base/logging.h" +#include "talk/examples/peerconnection/client/defaults.h" +#include "talk/media/devices/devicemanager.h" + +// Names used for a IceCandidate JSON object. +const char kCandidateSdpMidName[] = "sdpMid"; +const char kCandidateSdpMlineIndexName[] = "sdpMLineIndex"; +const char kCandidateSdpName[] = "candidate"; + +// Names used for a SessionDescription JSON object. +const char kSessionDescriptionTypeName[] = "type"; +const char kSessionDescriptionSdpName[] = "sdp"; + +class DummySetSessionDescriptionObserver + : public webrtc::SetSessionDescriptionObserver { + public: + static DummySetSessionDescriptionObserver* Create() { + return + new talk_base::RefCountedObject(); + } + virtual void OnSuccess() { + LOG(INFO) << __FUNCTION__; + } + virtual void OnFailure(const std::string& error) { + LOG(INFO) << __FUNCTION__ << " " << error; + } + + protected: + DummySetSessionDescriptionObserver() {} + ~DummySetSessionDescriptionObserver() {} +}; + +Conductor::Conductor(PeerConnectionClient* client, MainWindow* main_wnd) + : peer_id_(-1), + client_(client), + main_wnd_(main_wnd) { + client_->RegisterObserver(this); + main_wnd->RegisterObserver(this); +} + +Conductor::~Conductor() { + ASSERT(peer_connection_.get() == NULL); +} + +bool Conductor::connection_active() const { + return peer_connection_.get() != NULL; +} + +void Conductor::Close() { + client_->SignOut(); + DeletePeerConnection(); +} + +bool Conductor::InitializePeerConnection() { + ASSERT(peer_connection_factory_.get() == NULL); + ASSERT(peer_connection_.get() == NULL); + + peer_connection_factory_ = webrtc::CreatePeerConnectionFactory(); + + if (!peer_connection_factory_.get()) { + main_wnd_->MessageBox("Error", + "Failed to initialize PeerConnectionFactory", true); + DeletePeerConnection(); + return false; + } + + webrtc::PeerConnectionInterface::IceServers servers; + webrtc::PeerConnectionInterface::IceServer server; + server.uri = GetPeerConnectionString(); + servers.push_back(server); + peer_connection_ = peer_connection_factory_->CreatePeerConnection(servers, + NULL, + NULL, + this); + if (!peer_connection_.get()) { + main_wnd_->MessageBox("Error", + "CreatePeerConnection failed", true); + DeletePeerConnection(); + } + AddStreams(); + return peer_connection_.get() != NULL; +} + +void Conductor::DeletePeerConnection() { + peer_connection_ = NULL; + active_streams_.clear(); + main_wnd_->StopLocalRenderer(); + main_wnd_->StopRemoteRenderer(); + peer_connection_factory_ = NULL; + peer_id_ = -1; +} + +void Conductor::EnsureStreamingUI() { + ASSERT(peer_connection_.get() != NULL); + if (main_wnd_->IsWindow()) { + if (main_wnd_->current_ui() != MainWindow::STREAMING) + main_wnd_->SwitchToStreamingUI(); + } +} + +// +// PeerConnectionObserver implementation. +// + +void Conductor::OnError() { + LOG(LS_ERROR) << __FUNCTION__; + main_wnd_->QueueUIThreadCallback(PEER_CONNECTION_ERROR, NULL); +} + +// Called when a remote stream is added +void Conductor::OnAddStream(webrtc::MediaStreamInterface* stream) { + LOG(INFO) << __FUNCTION__ << " " << stream->label(); + + stream->AddRef(); + main_wnd_->QueueUIThreadCallback(NEW_STREAM_ADDED, + stream); +} + +void Conductor::OnRemoveStream(webrtc::MediaStreamInterface* stream) { + LOG(INFO) << __FUNCTION__ << " " << stream->label(); + stream->AddRef(); + main_wnd_->QueueUIThreadCallback(STREAM_REMOVED, + stream); +} + +void Conductor::OnIceCandidate(const webrtc::IceCandidateInterface* candidate) { + LOG(INFO) << __FUNCTION__ << " " << candidate->sdp_mline_index(); + Json::StyledWriter writer; + Json::Value jmessage; + + jmessage[kCandidateSdpMidName] = candidate->sdp_mid(); + jmessage[kCandidateSdpMlineIndexName] = candidate->sdp_mline_index(); + std::string sdp; + if (!candidate->ToString(&sdp)) { + LOG(LS_ERROR) << "Failed to serialize candidate"; + return; + } + jmessage[kCandidateSdpName] = sdp; + SendMessage(writer.write(jmessage)); +} + +// +// PeerConnectionClientObserver implementation. +// + +void Conductor::OnSignedIn() { + LOG(INFO) << __FUNCTION__; + main_wnd_->SwitchToPeerList(client_->peers()); +} + +void Conductor::OnDisconnected() { + LOG(INFO) << __FUNCTION__; + + DeletePeerConnection(); + + if (main_wnd_->IsWindow()) + main_wnd_->SwitchToConnectUI(); +} + +void Conductor::OnPeerConnected(int id, const std::string& name) { + LOG(INFO) << __FUNCTION__; + // Refresh the list if we're showing it. + if (main_wnd_->current_ui() == MainWindow::LIST_PEERS) + main_wnd_->SwitchToPeerList(client_->peers()); +} + +void Conductor::OnPeerDisconnected(int id) { + LOG(INFO) << __FUNCTION__; + if (id == peer_id_) { + LOG(INFO) << "Our peer disconnected"; + main_wnd_->QueueUIThreadCallback(PEER_CONNECTION_CLOSED, NULL); + } else { + // Refresh the list if we're showing it. + if (main_wnd_->current_ui() == MainWindow::LIST_PEERS) + main_wnd_->SwitchToPeerList(client_->peers()); + } +} + +void Conductor::OnMessageFromPeer(int peer_id, const std::string& message) { + ASSERT(peer_id_ == peer_id || peer_id_ == -1); + ASSERT(!message.empty()); + + if (!peer_connection_.get()) { + ASSERT(peer_id_ == -1); + peer_id_ = peer_id; + + if (!InitializePeerConnection()) { + LOG(LS_ERROR) << "Failed to initialize our PeerConnection instance"; + client_->SignOut(); + return; + } + } else if (peer_id != peer_id_) { + ASSERT(peer_id_ != -1); + LOG(WARNING) << "Received a message from unknown peer while already in a " + "conversation with a different peer."; + return; + } + + Json::Reader reader; + Json::Value jmessage; + if (!reader.parse(message, jmessage)) { + LOG(WARNING) << "Received unknown message. " << message; + return; + } + std::string type; + std::string json_object; + + GetStringFromJsonObject(jmessage, kSessionDescriptionTypeName, &type); + if (!type.empty()) { + std::string sdp; + if (!GetStringFromJsonObject(jmessage, kSessionDescriptionSdpName, &sdp)) { + LOG(WARNING) << "Can't parse received session description message."; + return; + } + webrtc::SessionDescriptionInterface* session_description( + webrtc::CreateSessionDescription(type, sdp)); + if (!session_description) { + LOG(WARNING) << "Can't parse received session description message."; + return; + } + LOG(INFO) << " Received session description :" << message; + peer_connection_->SetRemoteDescription( + DummySetSessionDescriptionObserver::Create(), session_description); + if (session_description->type() == + webrtc::SessionDescriptionInterface::kOffer) { + peer_connection_->CreateAnswer(this, NULL); + } + return; + } else { + std::string sdp_mid; + int sdp_mlineindex = 0; + std::string sdp; + if (!GetStringFromJsonObject(jmessage, kCandidateSdpMidName, &sdp_mid) || + !GetIntFromJsonObject(jmessage, kCandidateSdpMlineIndexName, + &sdp_mlineindex) || + !GetStringFromJsonObject(jmessage, kCandidateSdpName, &sdp)) { + LOG(WARNING) << "Can't parse received message."; + return; + } + talk_base::scoped_ptr candidate( + webrtc::CreateIceCandidate(sdp_mid, sdp_mlineindex, sdp)); + if (!candidate.get()) { + LOG(WARNING) << "Can't parse received candidate message."; + return; + } + if (!peer_connection_->AddIceCandidate(candidate.get())) { + LOG(WARNING) << "Failed to apply the received candidate"; + return; + } + LOG(INFO) << " Received candidate :" << message; + return; + } +} + +void Conductor::OnMessageSent(int err) { + // Process the next pending message if any. + main_wnd_->QueueUIThreadCallback(SEND_MESSAGE_TO_PEER, NULL); +} + +void Conductor::OnServerConnectionFailure() { + main_wnd_->MessageBox("Error", ("Failed to connect to " + server_).c_str(), + true); +} + +// +// MainWndCallback implementation. +// + +void Conductor::StartLogin(const std::string& server, int port) { + if (client_->is_connected()) + return; + server_ = server; + client_->Connect(server, port, GetPeerName()); +} + +void Conductor::DisconnectFromServer() { + if (client_->is_connected()) + client_->SignOut(); +} + +void Conductor::ConnectToPeer(int peer_id) { + ASSERT(peer_id_ == -1); + ASSERT(peer_id != -1); + + if (peer_connection_.get()) { + main_wnd_->MessageBox("Error", + "We only support connecting to one peer at a time", true); + return; + } + + if (InitializePeerConnection()) { + peer_id_ = peer_id; + peer_connection_->CreateOffer(this, NULL); + } else { + main_wnd_->MessageBox("Error", "Failed to initialize PeerConnection", true); + } +} + +cricket::VideoCapturer* Conductor::OpenVideoCaptureDevice() { + talk_base::scoped_ptr dev_manager( + cricket::DeviceManagerFactory::Create()); + if (!dev_manager->Init()) { + LOG(LS_ERROR) << "Can't create device manager"; + return NULL; + } + std::vector devs; + if (!dev_manager->GetVideoCaptureDevices(&devs)) { + LOG(LS_ERROR) << "Can't enumerate video devices"; + return NULL; + } + std::vector::iterator dev_it = devs.begin(); + cricket::VideoCapturer* capturer = NULL; + for (; dev_it != devs.end(); ++dev_it) { + capturer = dev_manager->CreateVideoCapturer(*dev_it); + if (capturer != NULL) + break; + } + return capturer; +} + +void Conductor::AddStreams() { + if (active_streams_.find(kStreamLabel) != active_streams_.end()) + return; // Already added. + + talk_base::scoped_refptr audio_track( + peer_connection_factory_->CreateAudioTrack( + kAudioLabel, peer_connection_factory_->CreateAudioSource(NULL))); + + talk_base::scoped_refptr video_track( + peer_connection_factory_->CreateVideoTrack( + kVideoLabel, + peer_connection_factory_->CreateVideoSource(OpenVideoCaptureDevice(), + NULL))); + main_wnd_->StartLocalRenderer(video_track); + + talk_base::scoped_refptr stream = + peer_connection_factory_->CreateLocalMediaStream(kStreamLabel); + + stream->AddTrack(audio_track); + stream->AddTrack(video_track); + if (!peer_connection_->AddStream(stream, NULL)) { + LOG(LS_ERROR) << "Adding stream to PeerConnection failed"; + } + typedef std::pair > + MediaStreamPair; + active_streams_.insert(MediaStreamPair(stream->label(), stream)); + main_wnd_->SwitchToStreamingUI(); +} + +void Conductor::DisconnectFromCurrentPeer() { + LOG(INFO) << __FUNCTION__; + if (peer_connection_.get()) { + client_->SendHangUp(peer_id_); + DeletePeerConnection(); + } + + if (main_wnd_->IsWindow()) + main_wnd_->SwitchToPeerList(client_->peers()); +} + +void Conductor::UIThreadCallback(int msg_id, void* data) { + switch (msg_id) { + case PEER_CONNECTION_CLOSED: + LOG(INFO) << "PEER_CONNECTION_CLOSED"; + DeletePeerConnection(); + + ASSERT(active_streams_.empty()); + + if (main_wnd_->IsWindow()) { + if (client_->is_connected()) { + main_wnd_->SwitchToPeerList(client_->peers()); + } else { + main_wnd_->SwitchToConnectUI(); + } + } else { + DisconnectFromServer(); + } + break; + + case SEND_MESSAGE_TO_PEER: { + LOG(INFO) << "SEND_MESSAGE_TO_PEER"; + std::string* msg = reinterpret_cast(data); + if (msg) { + // For convenience, we always run the message through the queue. + // This way we can be sure that messages are sent to the server + // in the same order they were signaled without much hassle. + pending_messages_.push_back(msg); + } + + if (!pending_messages_.empty() && !client_->IsSendingMessage()) { + msg = pending_messages_.front(); + pending_messages_.pop_front(); + + if (!client_->SendToPeer(peer_id_, *msg) && peer_id_ != -1) { + LOG(LS_ERROR) << "SendToPeer failed"; + DisconnectFromServer(); + } + delete msg; + } + + if (!peer_connection_.get()) + peer_id_ = -1; + + break; + } + + case PEER_CONNECTION_ERROR: + main_wnd_->MessageBox("Error", "an unknown error occurred", true); + break; + + case NEW_STREAM_ADDED: { + webrtc::MediaStreamInterface* stream = + reinterpret_cast( + data); + webrtc::VideoTrackVector tracks = stream->GetVideoTracks(); + // Only render the first track. + if (!tracks.empty()) { + webrtc::VideoTrackInterface* track = tracks[0]; + main_wnd_->StartRemoteRenderer(track); + } + stream->Release(); + break; + } + + case STREAM_REMOVED: { + // Remote peer stopped sending a stream. + webrtc::MediaStreamInterface* stream = + reinterpret_cast( + data); + stream->Release(); + break; + } + + default: + ASSERT(false); + break; + } +} + +void Conductor::OnSuccess(webrtc::SessionDescriptionInterface* desc) { + peer_connection_->SetLocalDescription( + DummySetSessionDescriptionObserver::Create(), desc); + Json::StyledWriter writer; + Json::Value jmessage; + jmessage[kSessionDescriptionTypeName] = desc->type(); + std::string sdp; + desc->ToString(&sdp); + jmessage[kSessionDescriptionSdpName] = sdp; + SendMessage(writer.write(jmessage)); +} + +void Conductor::OnFailure(const std::string& error) { + LOG(LERROR) << error; +} + +void Conductor::SendMessage(const std::string& json_object) { + std::string* msg = new std::string(json_object); + main_wnd_->QueueUIThreadCallback(SEND_MESSAGE_TO_PEER, msg); +} diff --git a/talk/examples/peerconnection/client/conductor.h b/talk/examples/peerconnection/client/conductor.h new file mode 100644 index 000000000..f9fb3937e --- /dev/null +++ b/talk/examples/peerconnection/client/conductor.h @@ -0,0 +1,144 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef PEERCONNECTION_SAMPLES_CLIENT_CONDUCTOR_H_ +#define PEERCONNECTION_SAMPLES_CLIENT_CONDUCTOR_H_ +#pragma once + +#include +#include +#include +#include + +#include "talk/examples/peerconnection/client/main_wnd.h" +#include "talk/examples/peerconnection/client/peer_connection_client.h" +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/app/webrtc/peerconnectioninterface.h" +#include "talk/base/scoped_ptr.h" + +namespace webrtc { +class VideoCaptureModule; +} // namespace webrtc + +namespace cricket { +class VideoRenderer; +} // namespace cricket + +class Conductor + : public webrtc::PeerConnectionObserver, + public webrtc::CreateSessionDescriptionObserver, + public PeerConnectionClientObserver, + public MainWndCallback { + public: + enum CallbackID { + MEDIA_CHANNELS_INITIALIZED = 1, + PEER_CONNECTION_CLOSED, + SEND_MESSAGE_TO_PEER, + PEER_CONNECTION_ERROR, + NEW_STREAM_ADDED, + STREAM_REMOVED, + }; + + Conductor(PeerConnectionClient* client, MainWindow* main_wnd); + + bool connection_active() const; + + virtual void Close(); + + protected: + ~Conductor(); + bool InitializePeerConnection(); + void DeletePeerConnection(); + void EnsureStreamingUI(); + void AddStreams(); + cricket::VideoCapturer* OpenVideoCaptureDevice(); + + // + // PeerConnectionObserver implementation. + // + virtual void OnError(); + virtual void OnStateChange( + webrtc::PeerConnectionObserver::StateType state_changed) {} + virtual void OnAddStream(webrtc::MediaStreamInterface* stream); + virtual void OnRemoveStream(webrtc::MediaStreamInterface* stream); + virtual void OnRenegotiationNeeded() {} + virtual void OnIceChange() {} + virtual void OnIceCandidate(const webrtc::IceCandidateInterface* candidate); + + // + // PeerConnectionClientObserver implementation. + // + + virtual void OnSignedIn(); + + virtual void OnDisconnected(); + + virtual void OnPeerConnected(int id, const std::string& name); + + virtual void OnPeerDisconnected(int id); + + virtual void OnMessageFromPeer(int peer_id, const std::string& message); + + virtual void OnMessageSent(int err); + + virtual void OnServerConnectionFailure(); + + // + // MainWndCallback implementation. + // + + virtual void StartLogin(const std::string& server, int port); + + virtual void DisconnectFromServer(); + + virtual void ConnectToPeer(int peer_id); + + virtual void DisconnectFromCurrentPeer(); + + virtual void UIThreadCallback(int msg_id, void* data); + + // CreateSessionDescriptionObserver implementation. + virtual void OnSuccess(webrtc::SessionDescriptionInterface* desc); + virtual void OnFailure(const std::string& error); + + protected: + // Send a message to the remote peer. + void SendMessage(const std::string& json_object); + + int peer_id_; + talk_base::scoped_refptr peer_connection_; + talk_base::scoped_refptr + peer_connection_factory_; + PeerConnectionClient* client_; + MainWindow* main_wnd_; + std::deque pending_messages_; + std::map > + active_streams_; + std::string server_; +}; + +#endif // PEERCONNECTION_SAMPLES_CLIENT_CONDUCTOR_H_ diff --git a/talk/examples/peerconnection/client/defaults.cc b/talk/examples/peerconnection/client/defaults.cc new file mode 100644 index 000000000..40f3dd171 --- /dev/null +++ b/talk/examples/peerconnection/client/defaults.cc @@ -0,0 +1,75 @@ +/* + * libjingle + * Copyright 2012, 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/examples/peerconnection/client/defaults.h" + +#include +#include + +#ifdef WIN32 +#include +#else +#include +#endif + +#include "talk/base/common.h" + +const char kAudioLabel[] = "audio_label"; +const char kVideoLabel[] = "video_label"; +const char kStreamLabel[] = "stream_label"; +const uint16 kDefaultServerPort = 8888; + +std::string GetEnvVarOrDefault(const char* env_var_name, + const char* default_value) { + std::string value; + const char* env_var = getenv(env_var_name); + if (env_var) + value = env_var; + + if (value.empty()) + value = default_value; + + return value; +} + +std::string GetPeerConnectionString() { + return GetEnvVarOrDefault("WEBRTC_CONNECT", "stun:stun.l.google.com:19302"); +} + +std::string GetDefaultServerName() { + return GetEnvVarOrDefault("WEBRTC_SERVER", "localhost"); +} + +std::string GetPeerName() { + char computer_name[256]; + if (gethostname(computer_name, ARRAY_SIZE(computer_name)) != 0) + strcpy(computer_name, "host"); + std::string ret(GetEnvVarOrDefault("USERNAME", "user")); + ret += '@'; + ret += computer_name; + return ret; +} diff --git a/talk/examples/peerconnection/client/defaults.h b/talk/examples/peerconnection/client/defaults.h new file mode 100644 index 000000000..f646149c8 --- /dev/null +++ b/talk/examples/peerconnection/client/defaults.h @@ -0,0 +1,47 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef PEERCONNECTION_SAMPLES_CLIENT_DEFAULTS_H_ +#define PEERCONNECTION_SAMPLES_CLIENT_DEFAULTS_H_ +#pragma once + +#include + +#include "talk/base/basictypes.h" + +extern const char kAudioLabel[]; +extern const char kVideoLabel[]; +extern const char kStreamLabel[]; +extern const uint16 kDefaultServerPort; + +std::string GetEnvVarOrDefault(const char* env_var_name, + const char* default_value); +std::string GetPeerConnectionString(); +std::string GetDefaultServerName(); +std::string GetPeerName(); + +#endif // PEERCONNECTION_SAMPLES_CLIENT_DEFAULTS_H_ diff --git a/talk/examples/peerconnection/client/flagdefs.h b/talk/examples/peerconnection/client/flagdefs.h new file mode 100644 index 000000000..c135bbbc3 --- /dev/null +++ b/talk/examples/peerconnection/client/flagdefs.h @@ -0,0 +1,50 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_EXAMPLES_PEERCONNECTION_CLIENT_FLAGDEFS_H_ +#define TALK_EXAMPLES_PEERCONNECTION_CLIENT_FLAGDEFS_H_ +#pragma once + +#include "talk/base/flags.h" + +extern const uint16 kDefaultServerPort; // From defaults.[h|cc] + +// Define flags for the peerconnect_client testing tool, in a separate +// header file so that they can be shared across the different main.cc's +// for each platform. + +DEFINE_bool(help, false, "Prints this message"); +DEFINE_bool(autoconnect, false, "Connect to the server without user " + "intervention."); +DEFINE_string(server, "localhost", "The server to connect to."); +DEFINE_int(port, kDefaultServerPort, + "The port on which the server is listening."); +DEFINE_bool(autocall, false, "Call the first available other client on " + "the server without user intervention. Note: this flag should only be set " + "to true on one of the two clients."); + +#endif // TALK_EXAMPLES_PEERCONNECTION_CLIENT_FLAGDEFS_H_ diff --git a/talk/examples/peerconnection/client/linux/main.cc b/talk/examples/peerconnection/client/linux/main.cc new file mode 100644 index 000000000..aee1bb100 --- /dev/null +++ b/talk/examples/peerconnection/client/linux/main.cc @@ -0,0 +1,117 @@ +/* + * libjingle + * Copyright 2012, 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 + +#include "talk/examples/peerconnection/client/conductor.h" +#include "talk/examples/peerconnection/client/flagdefs.h" +#include "talk/examples/peerconnection/client/linux/main_wnd.h" +#include "talk/examples/peerconnection/client/peer_connection_client.h" + +#include "talk/base/thread.h" + +class CustomSocketServer : public talk_base::PhysicalSocketServer { + public: + CustomSocketServer(talk_base::Thread* thread, GtkMainWnd* wnd) + : thread_(thread), wnd_(wnd), conductor_(NULL), client_(NULL) {} + virtual ~CustomSocketServer() {} + + void set_client(PeerConnectionClient* client) { client_ = client; } + void set_conductor(Conductor* conductor) { conductor_ = conductor; } + + // Override so that we can also pump the GTK message loop. + virtual bool Wait(int cms, bool process_io) { + // Pump GTK events. + // TODO: We really should move either the socket server or UI to a + // different thread. Alternatively we could look at merging the two loops + // by implementing a dispatcher for the socket server and/or use + // g_main_context_set_poll_func. + while (gtk_events_pending()) + gtk_main_iteration(); + + if (!wnd_->IsWindow() && !conductor_->connection_active() && + client_ != NULL && !client_->is_connected()) { + thread_->Quit(); + } + return talk_base::PhysicalSocketServer::Wait(0/*cms == -1 ? 1 : cms*/, + process_io); + } + + protected: + talk_base::Thread* thread_; + GtkMainWnd* wnd_; + Conductor* conductor_; + PeerConnectionClient* client_; +}; + +int main(int argc, char* argv[]) { + gtk_init(&argc, &argv); + g_type_init(); + g_thread_init(NULL); + + FlagList::SetFlagsFromCommandLine(&argc, argv, true); + if (FLAG_help) { + FlagList::Print(NULL, false); + return 0; + } + + // Abort if the user specifies a port that is outside the allowed + // range [1, 65535]. + if ((FLAG_port < 1) || (FLAG_port > 65535)) { + printf("Error: %i is not a valid port.\n", FLAG_port); + return -1; + } + + GtkMainWnd wnd(FLAG_server, FLAG_port, FLAG_autoconnect, FLAG_autocall); + wnd.Create(); + + talk_base::AutoThread auto_thread; + talk_base::Thread* thread = talk_base::Thread::Current(); + CustomSocketServer socket_server(thread, &wnd); + thread->set_socketserver(&socket_server); + + // Must be constructed after we set the socketserver. + PeerConnectionClient client; + talk_base::scoped_refptr conductor( + new talk_base::RefCountedObject(&client, &wnd)); + socket_server.set_client(&client); + socket_server.set_conductor(conductor); + + thread->Run(); + + // gtk_main(); + wnd.Destroy(); + + thread->set_socketserver(NULL); + // TODO: Run the Gtk main loop to tear down the connection. + //while (gtk_events_pending()) { + // gtk_main_iteration(); + //} + + return 0; +} + diff --git a/talk/examples/peerconnection/client/linux/main_wnd.cc b/talk/examples/peerconnection/client/linux/main_wnd.cc new file mode 100644 index 000000000..0a2e1f699 --- /dev/null +++ b/talk/examples/peerconnection/client/linux/main_wnd.cc @@ -0,0 +1,520 @@ +/* + * libjingle + * Copyright 2012, 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/examples/peerconnection/client/linux/main_wnd.h" + +#include +#include +#include + +#include "talk/examples/peerconnection/client/defaults.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/stringutils.h" + +using talk_base::sprintfn; + +namespace { + +// +// Simple static functions that simply forward the callback to the +// GtkMainWnd instance. +// + +gboolean OnDestroyedCallback(GtkWidget* widget, GdkEvent* event, + gpointer data) { + reinterpret_cast(data)->OnDestroyed(widget, event); + return FALSE; +} + +void OnClickedCallback(GtkWidget* widget, gpointer data) { + reinterpret_cast(data)->OnClicked(widget); +} + +gboolean SimulateButtonClick(gpointer button) { + g_signal_emit_by_name(button, "clicked"); + return false; +} + +gboolean OnKeyPressCallback(GtkWidget* widget, GdkEventKey* key, + gpointer data) { + reinterpret_cast(data)->OnKeyPress(widget, key); + return false; +} + +void OnRowActivatedCallback(GtkTreeView* tree_view, GtkTreePath* path, + GtkTreeViewColumn* column, gpointer data) { + reinterpret_cast(data)->OnRowActivated(tree_view, path, column); +} + +gboolean SimulateLastRowActivated(gpointer data) { + GtkTreeView* tree_view = reinterpret_cast(data); + GtkTreeModel* model = gtk_tree_view_get_model(tree_view); + + // "if iter is NULL, then the number of toplevel nodes is returned." + int rows = gtk_tree_model_iter_n_children(model, NULL); + GtkTreePath* lastpath = gtk_tree_path_new_from_indices(rows - 1, -1); + + // Select the last item in the list + GtkTreeSelection* selection = gtk_tree_view_get_selection(tree_view); + gtk_tree_selection_select_path(selection, lastpath); + + // Our TreeView only has one column, so it is column 0. + GtkTreeViewColumn* column = gtk_tree_view_get_column(tree_view, 0); + + gtk_tree_view_row_activated(tree_view, lastpath, column); + + gtk_tree_path_free(lastpath); + return false; +} + +// Creates a tree view, that we use to display the list of peers. +void InitializeList(GtkWidget* list) { + GtkCellRenderer* renderer = gtk_cell_renderer_text_new(); + GtkTreeViewColumn* column = gtk_tree_view_column_new_with_attributes( + "List Items", renderer, "text", 0, NULL); + gtk_tree_view_append_column(GTK_TREE_VIEW(list), column); + GtkListStore* store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT); + gtk_tree_view_set_model(GTK_TREE_VIEW(list), GTK_TREE_MODEL(store)); + g_object_unref(store); +} + +// Adds an entry to a tree view. +void AddToList(GtkWidget* list, const gchar* str, int value) { + GtkListStore* store = GTK_LIST_STORE( + gtk_tree_view_get_model(GTK_TREE_VIEW(list))); + + GtkTreeIter iter; + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, str, 1, value, -1); +} + +struct UIThreadCallbackData { + explicit UIThreadCallbackData(MainWndCallback* cb, int id, void* d) + : callback(cb), msg_id(id), data(d) {} + MainWndCallback* callback; + int msg_id; + void* data; +}; + +gboolean HandleUIThreadCallback(gpointer data) { + UIThreadCallbackData* cb_data = reinterpret_cast(data); + cb_data->callback->UIThreadCallback(cb_data->msg_id, cb_data->data); + delete cb_data; + return false; +} + +gboolean Redraw(gpointer data) { + GtkMainWnd* wnd = reinterpret_cast(data); + wnd->OnRedraw(); + return false; +} +} // end anonymous + +// +// GtkMainWnd implementation. +// + +GtkMainWnd::GtkMainWnd(const char* server, int port, bool autoconnect, + bool autocall) + : window_(NULL), draw_area_(NULL), vbox_(NULL), server_edit_(NULL), + port_edit_(NULL), peer_list_(NULL), callback_(NULL), + server_(server), autoconnect_(autoconnect), autocall_(autocall) { + char buffer[10]; + sprintfn(buffer, sizeof(buffer), "%i", port); + port_ = buffer; +} + +GtkMainWnd::~GtkMainWnd() { + ASSERT(!IsWindow()); +} + +void GtkMainWnd::RegisterObserver(MainWndCallback* callback) { + callback_ = callback; +} + +bool GtkMainWnd::IsWindow() { + return window_ != NULL && GTK_IS_WINDOW(window_); +} + +void GtkMainWnd::MessageBox(const char* caption, const char* text, + bool is_error) { + GtkWidget* dialog = gtk_message_dialog_new(GTK_WINDOW(window_), + GTK_DIALOG_DESTROY_WITH_PARENT, + is_error ? GTK_MESSAGE_ERROR : GTK_MESSAGE_INFO, + GTK_BUTTONS_CLOSE, "%s", text); + gtk_window_set_title(GTK_WINDOW(dialog), caption); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + +MainWindow::UI GtkMainWnd::current_ui() { + if (vbox_) + return CONNECT_TO_SERVER; + + if (peer_list_) + return LIST_PEERS; + + return STREAMING; +} + + +void GtkMainWnd::StartLocalRenderer(webrtc::VideoTrackInterface* local_video) { + local_renderer_.reset(new VideoRenderer(this, local_video)); +} + +void GtkMainWnd::StopLocalRenderer() { + local_renderer_.reset(); +} + +void GtkMainWnd::StartRemoteRenderer(webrtc::VideoTrackInterface* remote_video) { + remote_renderer_.reset(new VideoRenderer(this, remote_video)); +} + +void GtkMainWnd::StopRemoteRenderer() { + remote_renderer_.reset(); +} + +void GtkMainWnd::QueueUIThreadCallback(int msg_id, void* data) { + g_idle_add(HandleUIThreadCallback, + new UIThreadCallbackData(callback_, msg_id, data)); +} + +bool GtkMainWnd::Create() { + ASSERT(window_ == NULL); + + window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); + if (window_) { + gtk_window_set_position(GTK_WINDOW(window_), GTK_WIN_POS_CENTER); + gtk_window_set_default_size(GTK_WINDOW(window_), 640, 480); + gtk_window_set_title(GTK_WINDOW(window_), "PeerConnection client"); + g_signal_connect(G_OBJECT(window_), "delete-event", + G_CALLBACK(&OnDestroyedCallback), this); + g_signal_connect(window_, "key-press-event", G_CALLBACK(OnKeyPressCallback), + this); + + SwitchToConnectUI(); + } + + return window_ != NULL; +} + +bool GtkMainWnd::Destroy() { + if (!IsWindow()) + return false; + + gtk_widget_destroy(window_); + window_ = NULL; + + return true; +} + +void GtkMainWnd::SwitchToConnectUI() { + LOG(INFO) << __FUNCTION__; + + ASSERT(IsWindow()); + ASSERT(vbox_ == NULL); + + gtk_container_set_border_width(GTK_CONTAINER(window_), 10); + + if (peer_list_) { + gtk_widget_destroy(peer_list_); + peer_list_ = NULL; + } + + vbox_ = gtk_vbox_new(FALSE, 5); + GtkWidget* valign = gtk_alignment_new(0, 1, 0, 0); + gtk_container_add(GTK_CONTAINER(vbox_), valign); + gtk_container_add(GTK_CONTAINER(window_), vbox_); + + GtkWidget* hbox = gtk_hbox_new(FALSE, 5); + + GtkWidget* label = gtk_label_new("Server"); + gtk_container_add(GTK_CONTAINER(hbox), label); + + server_edit_ = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(server_edit_), server_.c_str()); + gtk_widget_set_size_request(server_edit_, 400, 30); + gtk_container_add(GTK_CONTAINER(hbox), server_edit_); + + port_edit_ = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(port_edit_), port_.c_str()); + gtk_widget_set_size_request(port_edit_, 70, 30); + gtk_container_add(GTK_CONTAINER(hbox), port_edit_); + + GtkWidget* button = gtk_button_new_with_label("Connect"); + gtk_widget_set_size_request(button, 70, 30); + g_signal_connect(button, "clicked", G_CALLBACK(OnClickedCallback), this); + gtk_container_add(GTK_CONTAINER(hbox), button); + + GtkWidget* halign = gtk_alignment_new(1, 0, 0, 0); + gtk_container_add(GTK_CONTAINER(halign), hbox); + gtk_box_pack_start(GTK_BOX(vbox_), halign, FALSE, FALSE, 0); + + gtk_widget_show_all(window_); + + if (autoconnect_) + g_idle_add(SimulateButtonClick, button); +} + +void GtkMainWnd::SwitchToPeerList(const Peers& peers) { + LOG(INFO) << __FUNCTION__; + + if (!peer_list_) { + gtk_container_set_border_width(GTK_CONTAINER(window_), 0); + if (vbox_) { + gtk_widget_destroy(vbox_); + vbox_ = NULL; + server_edit_ = NULL; + port_edit_ = NULL; + } else if (draw_area_) { + gtk_widget_destroy(draw_area_); + draw_area_ = NULL; + draw_buffer_.reset(); + } + + peer_list_ = gtk_tree_view_new(); + g_signal_connect(peer_list_, "row-activated", + G_CALLBACK(OnRowActivatedCallback), this); + gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(peer_list_), FALSE); + InitializeList(peer_list_); + gtk_container_add(GTK_CONTAINER(window_), peer_list_); + gtk_widget_show_all(window_); + } else { + GtkListStore* store = + GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(peer_list_))); + gtk_list_store_clear(store); + } + + AddToList(peer_list_, "List of currently connected peers:", -1); + for (Peers::const_iterator i = peers.begin(); i != peers.end(); ++i) + AddToList(peer_list_, i->second.c_str(), i->first); + + if (autocall_ && peers.begin() != peers.end()) + g_idle_add(SimulateLastRowActivated, peer_list_); +} + +void GtkMainWnd::SwitchToStreamingUI() { + LOG(INFO) << __FUNCTION__; + + ASSERT(draw_area_ == NULL); + + gtk_container_set_border_width(GTK_CONTAINER(window_), 0); + if (peer_list_) { + gtk_widget_destroy(peer_list_); + peer_list_ = NULL; + } + + draw_area_ = gtk_drawing_area_new(); + gtk_container_add(GTK_CONTAINER(window_), draw_area_); + + gtk_widget_show_all(window_); +} + +void GtkMainWnd::OnDestroyed(GtkWidget* widget, GdkEvent* event) { + callback_->Close(); + window_ = NULL; + draw_area_ = NULL; + vbox_ = NULL; + server_edit_ = NULL; + port_edit_ = NULL; + peer_list_ = NULL; +} + +void GtkMainWnd::OnClicked(GtkWidget* widget) { + // Make the connect button insensitive, so that it cannot be clicked more than + // once. Now that the connection includes auto-retry, it should not be + // necessary to click it more than once. + gtk_widget_set_sensitive(widget, false); + server_ = gtk_entry_get_text(GTK_ENTRY(server_edit_)); + port_ = gtk_entry_get_text(GTK_ENTRY(port_edit_)); + int port = port_.length() ? atoi(port_.c_str()) : 0; + callback_->StartLogin(server_, port); +} + +void GtkMainWnd::OnKeyPress(GtkWidget* widget, GdkEventKey* key) { + if (key->type == GDK_KEY_PRESS) { + switch (key->keyval) { + case GDK_Escape: + if (draw_area_) { + callback_->DisconnectFromCurrentPeer(); + } else if (peer_list_) { + callback_->DisconnectFromServer(); + } + break; + + case GDK_KP_Enter: + case GDK_Return: + if (vbox_) { + OnClicked(NULL); + } else if (peer_list_) { + // OnRowActivated will be called automatically when the user + // presses enter. + } + break; + + default: + break; + } + } +} + +void GtkMainWnd::OnRowActivated(GtkTreeView* tree_view, GtkTreePath* path, + GtkTreeViewColumn* column) { + ASSERT(peer_list_ != NULL); + GtkTreeIter iter; + GtkTreeModel* model; + GtkTreeSelection* selection = + gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view)); + if (gtk_tree_selection_get_selected(selection, &model, &iter)) { + char* text; + int id = -1; + gtk_tree_model_get(model, &iter, 0, &text, 1, &id, -1); + if (id != -1) + callback_->ConnectToPeer(id); + g_free(text); + } +} + +void GtkMainWnd::OnRedraw() { + gdk_threads_enter(); + + VideoRenderer* remote_renderer = remote_renderer_.get(); + if (remote_renderer && remote_renderer->image() != NULL && + draw_area_ != NULL) { + int width = remote_renderer->width(); + int height = remote_renderer->height(); + + if (!draw_buffer_.get()) { + draw_buffer_size_ = (width * height * 4) * 4; + draw_buffer_.reset(new uint8[draw_buffer_size_]); + gtk_widget_set_size_request(draw_area_, width * 2, height * 2); + } + + const uint32* image = reinterpret_cast( + remote_renderer->image()); + uint32* scaled = reinterpret_cast(draw_buffer_.get()); + for (int r = 0; r < height; ++r) { + for (int c = 0; c < width; ++c) { + int x = c * 2; + scaled[x] = scaled[x + 1] = image[c]; + } + + uint32* prev_line = scaled; + scaled += width * 2; + memcpy(scaled, prev_line, (width * 2) * 4); + + image += width; + scaled += width * 2; + } + + VideoRenderer* local_renderer = local_renderer_.get(); + if (local_renderer && local_renderer->image()) { + image = reinterpret_cast(local_renderer->image()); + scaled = reinterpret_cast(draw_buffer_.get()); + // Position the local preview on the right side. + scaled += (width * 2) - (local_renderer->width() / 2); + // right margin... + scaled -= 10; + // ... towards the bottom. + scaled += (height * width * 4) - + ((local_renderer->height() / 2) * + (local_renderer->width() / 2) * 4); + // bottom margin... + scaled -= (width * 2) * 5; + for (int r = 0; r < local_renderer->height(); r += 2) { + for (int c = 0; c < local_renderer->width(); c += 2) { + scaled[c / 2] = image[c + r * local_renderer->width()]; + } + scaled += width * 2; + } + } + + gdk_draw_rgb_32_image(draw_area_->window, + draw_area_->style->fg_gc[GTK_STATE_NORMAL], + 0, + 0, + width * 2, + height * 2, + GDK_RGB_DITHER_MAX, + draw_buffer_.get(), + (width * 2) * 4); + } + + gdk_threads_leave(); +} + +GtkMainWnd::VideoRenderer::VideoRenderer( + GtkMainWnd* main_wnd, + webrtc::VideoTrackInterface* track_to_render) + : width_(0), + height_(0), + main_wnd_(main_wnd), + rendered_track_(track_to_render) { + rendered_track_->AddRenderer(this); +} + +GtkMainWnd::VideoRenderer::~VideoRenderer() { + rendered_track_->RemoveRenderer(this); +} + +void GtkMainWnd::VideoRenderer::SetSize(int width, int height) { + gdk_threads_enter(); + width_ = width; + height_ = height; + image_.reset(new uint8[width * height * 4]); + gdk_threads_leave(); +} + +void GtkMainWnd::VideoRenderer::RenderFrame(const cricket::VideoFrame* frame) { + gdk_threads_enter(); + + int size = width_ * height_ * 4; + // TODO: Convert directly to RGBA + frame->ConvertToRgbBuffer(cricket::FOURCC_ARGB, + image_.get(), + size, + width_ * 4); + // Convert the B,G,R,A frame to R,G,B,A, which is accepted by GTK. + // The 'A' is just padding for GTK, so we can use it as temp. + uint8* pix = image_.get(); + uint8* end = image_.get() + size; + while (pix < end) { + pix[3] = pix[0]; // Save B to A. + pix[0] = pix[2]; // Set Red. + pix[2] = pix[3]; // Set Blue. + pix[3] = 0xFF; // Fixed Alpha. + pix += 4; + } + + gdk_threads_leave(); + + g_idle_add(Redraw, main_wnd_); +} + + diff --git a/talk/examples/peerconnection/client/linux/main_wnd.h b/talk/examples/peerconnection/client/linux/main_wnd.h new file mode 100644 index 000000000..c23c1164b --- /dev/null +++ b/talk/examples/peerconnection/client/linux/main_wnd.h @@ -0,0 +1,138 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + + +#ifndef PEERCONNECTION_SAMPLES_CLIENT_LINUX_MAIN_WND_H_ +#define PEERCONNECTION_SAMPLES_CLIENT_LINUX_MAIN_WND_H_ + +#include "talk/examples/peerconnection/client/main_wnd.h" +#include "talk/examples/peerconnection/client/peer_connection_client.h" + +// Forward declarations. +typedef struct _GtkWidget GtkWidget; +typedef union _GdkEvent GdkEvent; +typedef struct _GdkEventKey GdkEventKey; +typedef struct _GtkTreeView GtkTreeView; +typedef struct _GtkTreePath GtkTreePath; +typedef struct _GtkTreeViewColumn GtkTreeViewColumn; + +// Implements the main UI of the peer connection client. +// This is functionally equivalent to the MainWnd class in the Windows +// implementation. +class GtkMainWnd : public MainWindow { + public: + GtkMainWnd(const char* server, int port, bool autoconnect, bool autocall); + ~GtkMainWnd(); + + virtual void RegisterObserver(MainWndCallback* callback); + virtual bool IsWindow(); + virtual void SwitchToConnectUI(); + virtual void SwitchToPeerList(const Peers& peers); + virtual void SwitchToStreamingUI(); + virtual void MessageBox(const char* caption, const char* text, + bool is_error); + virtual MainWindow::UI current_ui(); + virtual void StartLocalRenderer(webrtc::VideoTrackInterface* local_video); + virtual void StopLocalRenderer(); + virtual void StartRemoteRenderer(webrtc::VideoTrackInterface* remote_video); + virtual void StopRemoteRenderer(); + + virtual void QueueUIThreadCallback(int msg_id, void* data); + + // Creates and shows the main window with the |Connect UI| enabled. + bool Create(); + + // Destroys the window. When the window is destroyed, it ends the + // main message loop. + bool Destroy(); + + // Callback for when the main window is destroyed. + void OnDestroyed(GtkWidget* widget, GdkEvent* event); + + // Callback for when the user clicks the "Connect" button. + void OnClicked(GtkWidget* widget); + + // Callback for keystrokes. Used to capture Esc and Return. + void OnKeyPress(GtkWidget* widget, GdkEventKey* key); + + // Callback when the user double clicks a peer in order to initiate a + // connection. + void OnRowActivated(GtkTreeView* tree_view, GtkTreePath* path, + GtkTreeViewColumn* column); + + void OnRedraw(); + + protected: + class VideoRenderer : public webrtc::VideoRendererInterface { + public: + VideoRenderer(GtkMainWnd* main_wnd, + webrtc::VideoTrackInterface* track_to_render); + virtual ~VideoRenderer(); + + // VideoRendererInterface implementation + virtual void SetSize(int width, int height); + virtual void RenderFrame(const cricket::VideoFrame* frame); + + const uint8* image() const { + return image_.get(); + } + + int width() const { + return width_; + } + + int height() const { + return height_; + } + + protected: + talk_base::scoped_array image_; + int width_; + int height_; + GtkMainWnd* main_wnd_; + talk_base::scoped_refptr rendered_track_; + }; + + protected: + GtkWidget* window_; // Our main window. + GtkWidget* draw_area_; // The drawing surface for rendering video streams. + GtkWidget* vbox_; // Container for the Connect UI. + GtkWidget* server_edit_; + GtkWidget* port_edit_; + GtkWidget* peer_list_; // The list of peers. + MainWndCallback* callback_; + std::string server_; + std::string port_; + bool autoconnect_; + bool autocall_; + talk_base::scoped_ptr local_renderer_; + talk_base::scoped_ptr remote_renderer_; + talk_base::scoped_ptr draw_buffer_; + int draw_buffer_size_; +}; + +#endif // PEERCONNECTION_SAMPLES_CLIENT_LINUX_MAIN_WND_H_ diff --git a/talk/examples/peerconnection/client/main.cc b/talk/examples/peerconnection/client/main.cc new file mode 100644 index 000000000..bd0a5c3c1 --- /dev/null +++ b/talk/examples/peerconnection/client/main.cc @@ -0,0 +1,72 @@ +/* + * libjingle + * Copyright 2012, 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/examples/peerconnection/client/conductor.h" +#include "talk/examples/peerconnection/client/main_wnd.h" +#include "talk/examples/peerconnection/client/peer_connection_client.h" +#include "talk/base/win32socketinit.h" +#include "talk/base/win32socketserver.h" + + +int PASCAL wWinMain(HINSTANCE instance, HINSTANCE prev_instance, + wchar_t* cmd_line, int cmd_show) { + talk_base::EnsureWinsockInit(); + talk_base::Win32Thread w32_thread; + talk_base::ThreadManager::Instance()->SetCurrentThread(&w32_thread); + + MainWnd wnd; + if (!wnd.Create()) { + ASSERT(false); + return -1; + } + + PeerConnectionClient client; + talk_base::scoped_refptr conductor( + new talk_base::RefCountedObject(&client, &wnd)); + + // Main loop. + MSG msg; + BOOL gm; + while ((gm = ::GetMessage(&msg, NULL, 0, 0)) != 0 && gm != -1) { + if (!wnd.PreTranslateMessage(&msg)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + } + + if (conductor->connection_active() || client.is_connected()) { + while ((conductor->connection_active() || client.is_connected()) && + (gm = ::GetMessage(&msg, NULL, 0, 0)) != 0 && gm != -1) { + if (!wnd.PreTranslateMessage(&msg)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + } + } + + return 0; +} diff --git a/talk/examples/peerconnection/client/main_wnd.cc b/talk/examples/peerconnection/client/main_wnd.cc new file mode 100644 index 000000000..0a22d0399 --- /dev/null +++ b/talk/examples/peerconnection/client/main_wnd.cc @@ -0,0 +1,603 @@ +/* + * libjingle + * Copyright 2012, 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/examples/peerconnection/client/main_wnd.h" + +#include + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/examples/peerconnection/client/defaults.h" + +ATOM MainWnd::wnd_class_ = 0; +const wchar_t MainWnd::kClassName[] = L"WebRTC_MainWnd"; + +namespace { + +const char kConnecting[] = "Connecting... "; +const char kNoVideoStreams[] = "(no video streams either way)"; +const char kNoIncomingStream[] = "(no incoming video)"; + +void CalculateWindowSizeForText(HWND wnd, const wchar_t* text, + size_t* width, size_t* height) { + HDC dc = ::GetDC(wnd); + RECT text_rc = {0}; + ::DrawText(dc, text, -1, &text_rc, DT_CALCRECT | DT_SINGLELINE); + ::ReleaseDC(wnd, dc); + RECT client, window; + ::GetClientRect(wnd, &client); + ::GetWindowRect(wnd, &window); + + *width = text_rc.right - text_rc.left; + *width += (window.right - window.left) - + (client.right - client.left); + *height = text_rc.bottom - text_rc.top; + *height += (window.bottom - window.top) - + (client.bottom - client.top); +} + +HFONT GetDefaultFont() { + static HFONT font = reinterpret_cast(GetStockObject(DEFAULT_GUI_FONT)); + return font; +} + +std::string GetWindowText(HWND wnd) { + char text[MAX_PATH] = {0}; + ::GetWindowTextA(wnd, &text[0], ARRAYSIZE(text)); + return text; +} + +void AddListBoxItem(HWND listbox, const std::string& str, LPARAM item_data) { + LRESULT index = ::SendMessageA(listbox, LB_ADDSTRING, 0, + reinterpret_cast(str.c_str())); + ::SendMessageA(listbox, LB_SETITEMDATA, index, item_data); +} + +} // namespace + +MainWnd::MainWnd() + : ui_(CONNECT_TO_SERVER), wnd_(NULL), edit1_(NULL), edit2_(NULL), + label1_(NULL), label2_(NULL), button_(NULL), listbox_(NULL), + destroyed_(false), callback_(NULL), nested_msg_(NULL) { +} + +MainWnd::~MainWnd() { + ASSERT(!IsWindow()); +} + +bool MainWnd::Create() { + ASSERT(wnd_ == NULL); + if (!RegisterWindowClass()) + return false; + + ui_thread_id_ = ::GetCurrentThreadId(); + wnd_ = ::CreateWindowExW(WS_EX_OVERLAPPEDWINDOW, kClassName, L"WebRTC", + WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN, + CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, + NULL, NULL, GetModuleHandle(NULL), this); + + ::SendMessage(wnd_, WM_SETFONT, reinterpret_cast(GetDefaultFont()), + TRUE); + + CreateChildWindows(); + SwitchToConnectUI(); + + return wnd_ != NULL; +} + +bool MainWnd::Destroy() { + BOOL ret = FALSE; + if (IsWindow()) { + ret = ::DestroyWindow(wnd_); + } + + return ret != FALSE; +} + +void MainWnd::RegisterObserver(MainWndCallback* callback) { + callback_ = callback; +} + +bool MainWnd::IsWindow() { + return wnd_ && ::IsWindow(wnd_) != FALSE; +} + +bool MainWnd::PreTranslateMessage(MSG* msg) { + bool ret = false; + if (msg->message == WM_CHAR) { + if (msg->wParam == VK_TAB) { + HandleTabbing(); + ret = true; + } else if (msg->wParam == VK_RETURN) { + OnDefaultAction(); + ret = true; + } else if (msg->wParam == VK_ESCAPE) { + if (callback_) { + if (ui_ == STREAMING) { + callback_->DisconnectFromCurrentPeer(); + } else { + callback_->DisconnectFromServer(); + } + } + } + } else if (msg->hwnd == NULL && msg->message == UI_THREAD_CALLBACK) { + callback_->UIThreadCallback(static_cast(msg->wParam), + reinterpret_cast(msg->lParam)); + ret = true; + } + return ret; +} + +void MainWnd::SwitchToConnectUI() { + ASSERT(IsWindow()); + LayoutPeerListUI(false); + ui_ = CONNECT_TO_SERVER; + LayoutConnectUI(true); + ::SetFocus(edit1_); +} + +void MainWnd::SwitchToPeerList(const Peers& peers) { + LayoutConnectUI(false); + + ::SendMessage(listbox_, LB_RESETCONTENT, 0, 0); + + AddListBoxItem(listbox_, "List of currently connected peers:", -1); + Peers::const_iterator i = peers.begin(); + for (; i != peers.end(); ++i) + AddListBoxItem(listbox_, i->second.c_str(), i->first); + + ui_ = LIST_PEERS; + LayoutPeerListUI(true); + ::SetFocus(listbox_); +} + +void MainWnd::SwitchToStreamingUI() { + LayoutConnectUI(false); + LayoutPeerListUI(false); + ui_ = STREAMING; +} + +void MainWnd::MessageBox(const char* caption, const char* text, bool is_error) { + DWORD flags = MB_OK; + if (is_error) + flags |= MB_ICONERROR; + + ::MessageBoxA(handle(), text, caption, flags); +} + + +void MainWnd::StartLocalRenderer(webrtc::VideoTrackInterface* local_video) { + local_renderer_.reset(new VideoRenderer(handle(), 1, 1, local_video)); +} + +void MainWnd::StopLocalRenderer() { + local_renderer_.reset(); +} + +void MainWnd::StartRemoteRenderer(webrtc::VideoTrackInterface* remote_video) { + remote_renderer_.reset(new VideoRenderer(handle(), 1, 1, remote_video)); +} + +void MainWnd::StopRemoteRenderer() { + remote_renderer_.reset(); +} + +void MainWnd::QueueUIThreadCallback(int msg_id, void* data) { + ::PostThreadMessage(ui_thread_id_, UI_THREAD_CALLBACK, + static_cast(msg_id), reinterpret_cast(data)); +} + +void MainWnd::OnPaint() { + PAINTSTRUCT ps; + ::BeginPaint(handle(), &ps); + + RECT rc; + ::GetClientRect(handle(), &rc); + + VideoRenderer* local_renderer = local_renderer_.get(); + VideoRenderer* remote_renderer = remote_renderer_.get(); + if (ui_ == STREAMING && remote_renderer && local_renderer) { + AutoLock local_lock(local_renderer); + AutoLock remote_lock(remote_renderer); + + const BITMAPINFO& bmi = remote_renderer->bmi(); + int height = abs(bmi.bmiHeader.biHeight); + int width = bmi.bmiHeader.biWidth; + + const uint8* image = remote_renderer->image(); + if (image != NULL) { + HDC dc_mem = ::CreateCompatibleDC(ps.hdc); + ::SetStretchBltMode(dc_mem, HALFTONE); + + // Set the map mode so that the ratio will be maintained for us. + HDC all_dc[] = { ps.hdc, dc_mem }; + for (int i = 0; i < ARRAY_SIZE(all_dc); ++i) { + SetMapMode(all_dc[i], MM_ISOTROPIC); + SetWindowExtEx(all_dc[i], width, height, NULL); + SetViewportExtEx(all_dc[i], rc.right, rc.bottom, NULL); + } + + HBITMAP bmp_mem = ::CreateCompatibleBitmap(ps.hdc, rc.right, rc.bottom); + HGDIOBJ bmp_old = ::SelectObject(dc_mem, bmp_mem); + + POINT logical_area = { rc.right, rc.bottom }; + DPtoLP(ps.hdc, &logical_area, 1); + + HBRUSH brush = ::CreateSolidBrush(RGB(0, 0, 0)); + RECT logical_rect = {0, 0, logical_area.x, logical_area.y }; + ::FillRect(dc_mem, &logical_rect, brush); + ::DeleteObject(brush); + + int x = (logical_area.x / 2) - (width / 2); + int y = (logical_area.y / 2) - (height / 2); + + StretchDIBits(dc_mem, x, y, width, height, + 0, 0, width, height, image, &bmi, DIB_RGB_COLORS, SRCCOPY); + + if ((rc.right - rc.left) > 200 && (rc.bottom - rc.top) > 200) { + const BITMAPINFO& bmi = local_renderer->bmi(); + image = local_renderer->image(); + int thumb_width = bmi.bmiHeader.biWidth / 4; + int thumb_height = abs(bmi.bmiHeader.biHeight) / 4; + StretchDIBits(dc_mem, + logical_area.x - thumb_width - 10, + logical_area.y - thumb_height - 10, + thumb_width, thumb_height, + 0, 0, bmi.bmiHeader.biWidth, -bmi.bmiHeader.biHeight, + image, &bmi, DIB_RGB_COLORS, SRCCOPY); + } + + BitBlt(ps.hdc, 0, 0, logical_area.x, logical_area.y, + dc_mem, 0, 0, SRCCOPY); + + // Cleanup. + ::SelectObject(dc_mem, bmp_old); + ::DeleteObject(bmp_mem); + ::DeleteDC(dc_mem); + } else { + // We're still waiting for the video stream to be initialized. + HBRUSH brush = ::CreateSolidBrush(RGB(0, 0, 0)); + ::FillRect(ps.hdc, &rc, brush); + ::DeleteObject(brush); + + HGDIOBJ old_font = ::SelectObject(ps.hdc, GetDefaultFont()); + ::SetTextColor(ps.hdc, RGB(0xff, 0xff, 0xff)); + ::SetBkMode(ps.hdc, TRANSPARENT); + + std::string text(kConnecting); + if (!local_renderer->image()) { + text += kNoVideoStreams; + } else { + text += kNoIncomingStream; + } + ::DrawTextA(ps.hdc, text.c_str(), -1, &rc, + DT_SINGLELINE | DT_CENTER | DT_VCENTER); + ::SelectObject(ps.hdc, old_font); + } + } else { + HBRUSH brush = ::CreateSolidBrush(::GetSysColor(COLOR_WINDOW)); + ::FillRect(ps.hdc, &rc, brush); + ::DeleteObject(brush); + } + + ::EndPaint(handle(), &ps); +} + +void MainWnd::OnDestroyed() { + PostQuitMessage(0); +} + +void MainWnd::OnDefaultAction() { + if (!callback_) + return; + if (ui_ == CONNECT_TO_SERVER) { + std::string server(GetWindowText(edit1_)); + std::string port_str(GetWindowText(edit2_)); + int port = port_str.length() ? atoi(port_str.c_str()) : 0; + callback_->StartLogin(server, port); + } else if (ui_ == LIST_PEERS) { + LRESULT sel = ::SendMessage(listbox_, LB_GETCURSEL, 0, 0); + if (sel != LB_ERR) { + LRESULT peer_id = ::SendMessage(listbox_, LB_GETITEMDATA, sel, 0); + if (peer_id != -1 && callback_) { + callback_->ConnectToPeer(peer_id); + } + } + } else { + MessageBoxA(wnd_, "OK!", "Yeah", MB_OK); + } +} + +bool MainWnd::OnMessage(UINT msg, WPARAM wp, LPARAM lp, LRESULT* result) { + switch (msg) { + case WM_ERASEBKGND: + *result = TRUE; + return true; + + case WM_PAINT: + OnPaint(); + return true; + + case WM_SETFOCUS: + if (ui_ == CONNECT_TO_SERVER) { + SetFocus(edit1_); + } else if (ui_ == LIST_PEERS) { + SetFocus(listbox_); + } + return true; + + case WM_SIZE: + if (ui_ == CONNECT_TO_SERVER) { + LayoutConnectUI(true); + } else if (ui_ == LIST_PEERS) { + LayoutPeerListUI(true); + } + break; + + case WM_CTLCOLORSTATIC: + *result = reinterpret_cast(GetSysColorBrush(COLOR_WINDOW)); + return true; + + case WM_COMMAND: + if (button_ == reinterpret_cast(lp)) { + if (BN_CLICKED == HIWORD(wp)) + OnDefaultAction(); + } else if (listbox_ == reinterpret_cast(lp)) { + if (LBN_DBLCLK == HIWORD(wp)) { + OnDefaultAction(); + } + } + return true; + + case WM_CLOSE: + if (callback_) + callback_->Close(); + break; + } + return false; +} + +// static +LRESULT CALLBACK MainWnd::WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { + MainWnd* me = reinterpret_cast( + ::GetWindowLongPtr(hwnd, GWLP_USERDATA)); + if (!me && WM_CREATE == msg) { + CREATESTRUCT* cs = reinterpret_cast(lp); + me = reinterpret_cast(cs->lpCreateParams); + me->wnd_ = hwnd; + ::SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast(me)); + } + + LRESULT result = 0; + if (me) { + void* prev_nested_msg = me->nested_msg_; + me->nested_msg_ = &msg; + + bool handled = me->OnMessage(msg, wp, lp, &result); + if (WM_NCDESTROY == msg) { + me->destroyed_ = true; + } else if (!handled) { + result = ::DefWindowProc(hwnd, msg, wp, lp); + } + + if (me->destroyed_ && prev_nested_msg == NULL) { + me->OnDestroyed(); + me->wnd_ = NULL; + me->destroyed_ = false; + } + + me->nested_msg_ = prev_nested_msg; + } else { + result = ::DefWindowProc(hwnd, msg, wp, lp); + } + + return result; +} + +// static +bool MainWnd::RegisterWindowClass() { + if (wnd_class_) + return true; + + WNDCLASSEX wcex = { sizeof(WNDCLASSEX) }; + wcex.style = CS_DBLCLKS; + wcex.hInstance = GetModuleHandle(NULL); + wcex.hbrBackground = reinterpret_cast(COLOR_WINDOW + 1); + wcex.hCursor = ::LoadCursor(NULL, IDC_ARROW); + wcex.lpfnWndProc = &WndProc; + wcex.lpszClassName = kClassName; + wnd_class_ = ::RegisterClassEx(&wcex); + ASSERT(wnd_class_ != 0); + return wnd_class_ != 0; +} + +void MainWnd::CreateChildWindow(HWND* wnd, MainWnd::ChildWindowID id, + const wchar_t* class_name, DWORD control_style, + DWORD ex_style) { + if (::IsWindow(*wnd)) + return; + + // Child windows are invisible at first, and shown after being resized. + DWORD style = WS_CHILD | control_style; + *wnd = ::CreateWindowEx(ex_style, class_name, L"", style, + 100, 100, 100, 100, wnd_, + reinterpret_cast(id), + GetModuleHandle(NULL), NULL); + ASSERT(::IsWindow(*wnd) != FALSE); + ::SendMessage(*wnd, WM_SETFONT, reinterpret_cast(GetDefaultFont()), + TRUE); +} + +void MainWnd::CreateChildWindows() { + // Create the child windows in tab order. + CreateChildWindow(&label1_, LABEL1_ID, L"Static", ES_CENTER | ES_READONLY, 0); + CreateChildWindow(&edit1_, EDIT_ID, L"Edit", + ES_LEFT | ES_NOHIDESEL | WS_TABSTOP, WS_EX_CLIENTEDGE); + CreateChildWindow(&label2_, LABEL2_ID, L"Static", ES_CENTER | ES_READONLY, 0); + CreateChildWindow(&edit2_, EDIT_ID, L"Edit", + ES_LEFT | ES_NOHIDESEL | WS_TABSTOP, WS_EX_CLIENTEDGE); + CreateChildWindow(&button_, BUTTON_ID, L"Button", BS_CENTER | WS_TABSTOP, 0); + + CreateChildWindow(&listbox_, LISTBOX_ID, L"ListBox", + LBS_HASSTRINGS | LBS_NOTIFY, WS_EX_CLIENTEDGE); + + ::SetWindowTextA(edit1_, GetDefaultServerName().c_str()); + ::SetWindowTextA(edit2_, "8888"); +} + +void MainWnd::LayoutConnectUI(bool show) { + struct Windows { + HWND wnd; + const wchar_t* text; + size_t width; + size_t height; + } windows[] = { + { label1_, L"Server" }, + { edit1_, L"XXXyyyYYYgggXXXyyyYYYggg" }, + { label2_, L":" }, + { edit2_, L"XyXyX" }, + { button_, L"Connect" }, + }; + + if (show) { + const size_t kSeparator = 5; + size_t total_width = (ARRAYSIZE(windows) - 1) * kSeparator; + + for (size_t i = 0; i < ARRAYSIZE(windows); ++i) { + CalculateWindowSizeForText(windows[i].wnd, windows[i].text, + &windows[i].width, &windows[i].height); + total_width += windows[i].width; + } + + RECT rc; + ::GetClientRect(wnd_, &rc); + size_t x = (rc.right / 2) - (total_width / 2); + size_t y = rc.bottom / 2; + for (size_t i = 0; i < ARRAYSIZE(windows); ++i) { + size_t top = y - (windows[i].height / 2); + ::MoveWindow(windows[i].wnd, x, top, windows[i].width, windows[i].height, + TRUE); + x += kSeparator + windows[i].width; + if (windows[i].text[0] != 'X') + ::SetWindowText(windows[i].wnd, windows[i].text); + ::ShowWindow(windows[i].wnd, SW_SHOWNA); + } + } else { + for (size_t i = 0; i < ARRAYSIZE(windows); ++i) { + ::ShowWindow(windows[i].wnd, SW_HIDE); + } + } +} + +void MainWnd::LayoutPeerListUI(bool show) { + if (show) { + RECT rc; + ::GetClientRect(wnd_, &rc); + ::MoveWindow(listbox_, 0, 0, rc.right, rc.bottom, TRUE); + ::ShowWindow(listbox_, SW_SHOWNA); + } else { + ::ShowWindow(listbox_, SW_HIDE); + InvalidateRect(wnd_, NULL, TRUE); + } +} + +void MainWnd::HandleTabbing() { + bool shift = ((::GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0); + UINT next_cmd = shift ? GW_HWNDPREV : GW_HWNDNEXT; + UINT loop_around_cmd = shift ? GW_HWNDLAST : GW_HWNDFIRST; + HWND focus = GetFocus(), next; + do { + next = ::GetWindow(focus, next_cmd); + if (IsWindowVisible(next) && + (GetWindowLong(next, GWL_STYLE) & WS_TABSTOP)) { + break; + } + + if (!next) { + next = ::GetWindow(focus, loop_around_cmd); + if (IsWindowVisible(next) && + (GetWindowLong(next, GWL_STYLE) & WS_TABSTOP)) { + break; + } + } + focus = next; + } while (true); + ::SetFocus(next); +} + +// +// MainWnd::VideoRenderer +// + +MainWnd::VideoRenderer::VideoRenderer( + HWND wnd, int width, int height, + webrtc::VideoTrackInterface* track_to_render) + : wnd_(wnd), rendered_track_(track_to_render) { + ::InitializeCriticalSection(&buffer_lock_); + ZeroMemory(&bmi_, sizeof(bmi_)); + bmi_.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi_.bmiHeader.biPlanes = 1; + bmi_.bmiHeader.biBitCount = 32; + bmi_.bmiHeader.biCompression = BI_RGB; + bmi_.bmiHeader.biWidth = width; + bmi_.bmiHeader.biHeight = -height; + bmi_.bmiHeader.biSizeImage = width * height * + (bmi_.bmiHeader.biBitCount >> 3); + rendered_track_->AddRenderer(this); +} + +MainWnd::VideoRenderer::~VideoRenderer() { + rendered_track_->RemoveRenderer(this); + ::DeleteCriticalSection(&buffer_lock_); +} + +void MainWnd::VideoRenderer::SetSize(int width, int height) { + AutoLock lock(this); + + bmi_.bmiHeader.biWidth = width; + bmi_.bmiHeader.biHeight = -height; + bmi_.bmiHeader.biSizeImage = width * height * + (bmi_.bmiHeader.biBitCount >> 3); + image_.reset(new uint8[bmi_.bmiHeader.biSizeImage]); +} + +void MainWnd::VideoRenderer::RenderFrame(const cricket::VideoFrame* frame) { + if (!frame) + return; + + { + AutoLock lock(this); + + ASSERT(image_.get() != NULL); + frame->ConvertToRgbBuffer(cricket::FOURCC_ARGB, + image_.get(), + bmi_.bmiHeader.biSizeImage, + bmi_.bmiHeader.biWidth * + bmi_.bmiHeader.biBitCount / 8); + } + InvalidateRect(wnd_, NULL, TRUE); +} diff --git a/talk/examples/peerconnection/client/main_wnd.h b/talk/examples/peerconnection/client/main_wnd.h new file mode 100644 index 000000000..0ed0f1422 --- /dev/null +++ b/talk/examples/peerconnection/client/main_wnd.h @@ -0,0 +1,213 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef PEERCONNECTION_SAMPLES_CLIENT_MAIN_WND_H_ +#define PEERCONNECTION_SAMPLES_CLIENT_MAIN_WND_H_ +#pragma once + +#include +#include + +#include "talk/app/webrtc/mediastreaminterface.h" +#include "talk/base/win32.h" +#include "talk/examples/peerconnection/client/peer_connection_client.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/videocommon.h" +#include "talk/media/base/videoframe.h" +#include "talk/media/base/videorenderer.h" + +class MainWndCallback { + public: + virtual void StartLogin(const std::string& server, int port) = 0; + virtual void DisconnectFromServer() = 0; + virtual void ConnectToPeer(int peer_id) = 0; + virtual void DisconnectFromCurrentPeer() = 0; + virtual void UIThreadCallback(int msg_id, void* data) = 0; + virtual void Close() = 0; + protected: + virtual ~MainWndCallback() {} +}; + +// Pure virtual interface for the main window. +class MainWindow { + public: + virtual ~MainWindow() {} + + enum UI { + CONNECT_TO_SERVER, + LIST_PEERS, + STREAMING, + }; + + virtual void RegisterObserver(MainWndCallback* callback) = 0; + + virtual bool IsWindow() = 0; + virtual void MessageBox(const char* caption, const char* text, + bool is_error) = 0; + + virtual UI current_ui() = 0; + + virtual void SwitchToConnectUI() = 0; + virtual void SwitchToPeerList(const Peers& peers) = 0; + virtual void SwitchToStreamingUI() = 0; + + virtual void StartLocalRenderer(webrtc::VideoTrackInterface* local_video) = 0; + virtual void StopLocalRenderer() = 0; + virtual void StartRemoteRenderer(webrtc::VideoTrackInterface* remote_video) = 0; + virtual void StopRemoteRenderer() = 0; + + virtual void QueueUIThreadCallback(int msg_id, void* data) = 0; +}; + +#ifdef WIN32 + +class MainWnd : public MainWindow { + public: + static const wchar_t kClassName[]; + + enum WindowMessages { + UI_THREAD_CALLBACK = WM_APP + 1, + }; + + MainWnd(); + ~MainWnd(); + + bool Create(); + bool Destroy(); + bool PreTranslateMessage(MSG* msg); + + virtual void RegisterObserver(MainWndCallback* callback); + virtual bool IsWindow(); + virtual void SwitchToConnectUI(); + virtual void SwitchToPeerList(const Peers& peers); + virtual void SwitchToStreamingUI(); + virtual void MessageBox(const char* caption, const char* text, + bool is_error); + virtual UI current_ui() { return ui_; } + + virtual void StartLocalRenderer(webrtc::VideoTrackInterface* local_video); + virtual void StopLocalRenderer(); + virtual void StartRemoteRenderer(webrtc::VideoTrackInterface* remote_video); + virtual void StopRemoteRenderer(); + + virtual void QueueUIThreadCallback(int msg_id, void* data); + + HWND handle() const { return wnd_; } + + class VideoRenderer : public webrtc::VideoRendererInterface { + public: + VideoRenderer(HWND wnd, int width, int height, + webrtc::VideoTrackInterface* track_to_render); + virtual ~VideoRenderer(); + + void Lock() { + ::EnterCriticalSection(&buffer_lock_); + } + + void Unlock() { + ::LeaveCriticalSection(&buffer_lock_); + } + + // VideoRendererInterface implementation + virtual void SetSize(int width, int height); + virtual void RenderFrame(const cricket::VideoFrame* frame); + + const BITMAPINFO& bmi() const { return bmi_; } + const uint8* image() const { return image_.get(); } + + protected: + enum { + SET_SIZE, + RENDER_FRAME, + }; + + HWND wnd_; + BITMAPINFO bmi_; + talk_base::scoped_array image_; + CRITICAL_SECTION buffer_lock_; + talk_base::scoped_refptr rendered_track_; + }; + + // A little helper class to make sure we always to proper locking and + // unlocking when working with VideoRenderer buffers. + template + class AutoLock { + public: + explicit AutoLock(T* obj) : obj_(obj) { obj_->Lock(); } + ~AutoLock() { obj_->Unlock(); } + protected: + T* obj_; + }; + + protected: + enum ChildWindowID { + EDIT_ID = 1, + BUTTON_ID, + LABEL1_ID, + LABEL2_ID, + LISTBOX_ID, + }; + + void OnPaint(); + void OnDestroyed(); + + void OnDefaultAction(); + + bool OnMessage(UINT msg, WPARAM wp, LPARAM lp, LRESULT* result); + + static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); + static bool RegisterWindowClass(); + + void CreateChildWindow(HWND* wnd, ChildWindowID id, const wchar_t* class_name, + DWORD control_style, DWORD ex_style); + void CreateChildWindows(); + + void LayoutConnectUI(bool show); + void LayoutPeerListUI(bool show); + + void HandleTabbing(); + + private: + talk_base::scoped_ptr local_renderer_; + talk_base::scoped_ptr remote_renderer_; + UI ui_; + HWND wnd_; + DWORD ui_thread_id_; + HWND edit1_; + HWND edit2_; + HWND label1_; + HWND label2_; + HWND button_; + HWND listbox_; + bool destroyed_; + void* nested_msg_; + MainWndCallback* callback_; + static ATOM wnd_class_; +}; +#endif // WIN32 + +#endif // PEERCONNECTION_SAMPLES_CLIENT_MAIN_WND_H_ diff --git a/talk/examples/peerconnection/client/peer_connection_client.cc b/talk/examples/peerconnection/client/peer_connection_client.cc new file mode 100644 index 000000000..dc6694694 --- /dev/null +++ b/talk/examples/peerconnection/client/peer_connection_client.cc @@ -0,0 +1,531 @@ +/* + * libjingle + * Copyright 2012, 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/examples/peerconnection/client/peer_connection_client.h" + +#include "talk/examples/peerconnection/client/defaults.h" +#include "talk/base/common.h" +#include "talk/base/nethelpers.h" +#include "talk/base/logging.h" +#include "talk/base/stringutils.h" + +#ifdef WIN32 +#include "talk/base/win32socketserver.h" +#endif + +using talk_base::sprintfn; + +namespace { + +// This is our magical hangup signal. +const char kByeMessage[] = "BYE"; +// Delay between server connection retries, in milliseconds +const int kReconnectDelay = 2000; + +talk_base::AsyncSocket* CreateClientSocket(int family) { +#ifdef WIN32 + talk_base::Win32Socket* sock = new talk_base::Win32Socket(); + sock->CreateT(family, SOCK_STREAM); + return sock; +#elif defined(POSIX) + talk_base::Thread* thread = talk_base::Thread::Current(); + ASSERT(thread != NULL); + return thread->socketserver()->CreateAsyncSocket(family, SOCK_STREAM); +#else +#error Platform not supported. +#endif +} + +} + +PeerConnectionClient::PeerConnectionClient() + : callback_(NULL), + resolver_(NULL), + state_(NOT_CONNECTED), + my_id_(-1) { +} + +PeerConnectionClient::~PeerConnectionClient() { +} + +void PeerConnectionClient::InitSocketSignals() { + ASSERT(control_socket_.get() != NULL); + ASSERT(hanging_get_.get() != NULL); + control_socket_->SignalCloseEvent.connect(this, + &PeerConnectionClient::OnClose); + hanging_get_->SignalCloseEvent.connect(this, + &PeerConnectionClient::OnClose); + control_socket_->SignalConnectEvent.connect(this, + &PeerConnectionClient::OnConnect); + hanging_get_->SignalConnectEvent.connect(this, + &PeerConnectionClient::OnHangingGetConnect); + control_socket_->SignalReadEvent.connect(this, + &PeerConnectionClient::OnRead); + hanging_get_->SignalReadEvent.connect(this, + &PeerConnectionClient::OnHangingGetRead); +} + +int PeerConnectionClient::id() const { + return my_id_; +} + +bool PeerConnectionClient::is_connected() const { + return my_id_ != -1; +} + +const Peers& PeerConnectionClient::peers() const { + return peers_; +} + +void PeerConnectionClient::RegisterObserver( + PeerConnectionClientObserver* callback) { + ASSERT(!callback_); + callback_ = callback; +} + +void PeerConnectionClient::Connect(const std::string& server, int port, + const std::string& client_name) { + ASSERT(!server.empty()); + ASSERT(!client_name.empty()); + + if (state_ != NOT_CONNECTED) { + LOG(WARNING) + << "The client must not be connected before you can call Connect()"; + callback_->OnServerConnectionFailure(); + return; + } + + if (server.empty() || client_name.empty()) { + callback_->OnServerConnectionFailure(); + return; + } + + if (port <= 0) + port = kDefaultServerPort; + + server_address_.SetIP(server); + server_address_.SetPort(port); + client_name_ = client_name; + + if (server_address_.IsUnresolved()) { + state_ = RESOLVING; + resolver_ = new talk_base::AsyncResolver(); + resolver_->SignalWorkDone.connect(this, + &PeerConnectionClient::OnResolveResult); + resolver_->set_address(server_address_); + resolver_->Start(); + } else { + DoConnect(); + } +} + +void PeerConnectionClient::OnResolveResult(talk_base::SignalThread *t) { + if (resolver_->error() != 0) { + callback_->OnServerConnectionFailure(); + resolver_->Destroy(false); + resolver_ = NULL; + state_ = NOT_CONNECTED; + } else { + server_address_ = resolver_->address(); + DoConnect(); + } +} + +void PeerConnectionClient::DoConnect() { + control_socket_.reset(CreateClientSocket(server_address_.ipaddr().family())); + hanging_get_.reset(CreateClientSocket(server_address_.ipaddr().family())); + InitSocketSignals(); + char buffer[1024]; + sprintfn(buffer, sizeof(buffer), + "GET /sign_in?%s HTTP/1.0\r\n\r\n", client_name_.c_str()); + onconnect_data_ = buffer; + + bool ret = ConnectControlSocket(); + if (ret) + state_ = SIGNING_IN; + if (!ret) { + callback_->OnServerConnectionFailure(); + } +} + +bool PeerConnectionClient::SendToPeer(int peer_id, const std::string& message) { + if (state_ != CONNECTED) + return false; + + ASSERT(is_connected()); + ASSERT(control_socket_->GetState() == talk_base::Socket::CS_CLOSED); + if (!is_connected() || peer_id == -1) + return false; + + char headers[1024]; + sprintfn(headers, sizeof(headers), + "POST /message?peer_id=%i&to=%i HTTP/1.0\r\n" + "Content-Length: %i\r\n" + "Content-Type: text/plain\r\n" + "\r\n", + my_id_, peer_id, message.length()); + onconnect_data_ = headers; + onconnect_data_ += message; + return ConnectControlSocket(); +} + +bool PeerConnectionClient::SendHangUp(int peer_id) { + return SendToPeer(peer_id, kByeMessage); +} + +bool PeerConnectionClient::IsSendingMessage() { + return state_ == CONNECTED && + control_socket_->GetState() != talk_base::Socket::CS_CLOSED; +} + +bool PeerConnectionClient::SignOut() { + if (state_ == NOT_CONNECTED || state_ == SIGNING_OUT) + return true; + + if (hanging_get_->GetState() != talk_base::Socket::CS_CLOSED) + hanging_get_->Close(); + + if (control_socket_->GetState() == talk_base::Socket::CS_CLOSED) { + state_ = SIGNING_OUT; + + if (my_id_ != -1) { + char buffer[1024]; + sprintfn(buffer, sizeof(buffer), + "GET /sign_out?peer_id=%i HTTP/1.0\r\n\r\n", my_id_); + onconnect_data_ = buffer; + return ConnectControlSocket(); + } else { + // Can occur if the app is closed before we finish connecting. + return true; + } + } else { + state_ = SIGNING_OUT_WAITING; + } + + return true; +} + +void PeerConnectionClient::Close() { + control_socket_->Close(); + hanging_get_->Close(); + onconnect_data_.clear(); + peers_.clear(); + if (resolver_ != NULL) { + resolver_->Destroy(false); + resolver_ = NULL; + } + my_id_ = -1; + state_ = NOT_CONNECTED; +} + +bool PeerConnectionClient::ConnectControlSocket() { + ASSERT(control_socket_->GetState() == talk_base::Socket::CS_CLOSED); + int err = control_socket_->Connect(server_address_); + if (err == SOCKET_ERROR) { + Close(); + return false; + } + return true; +} + +void PeerConnectionClient::OnConnect(talk_base::AsyncSocket* socket) { + ASSERT(!onconnect_data_.empty()); + size_t sent = socket->Send(onconnect_data_.c_str(), onconnect_data_.length()); + ASSERT(sent == onconnect_data_.length()); + UNUSED(sent); + onconnect_data_.clear(); +} + +void PeerConnectionClient::OnHangingGetConnect(talk_base::AsyncSocket* socket) { + char buffer[1024]; + sprintfn(buffer, sizeof(buffer), + "GET /wait?peer_id=%i HTTP/1.0\r\n\r\n", my_id_); + int len = strlen(buffer); + int sent = socket->Send(buffer, len); + ASSERT(sent == len); + UNUSED2(sent, len); +} + +void PeerConnectionClient::OnMessageFromPeer(int peer_id, + const std::string& message) { + if (message.length() == (sizeof(kByeMessage) - 1) && + message.compare(kByeMessage) == 0) { + callback_->OnPeerDisconnected(peer_id); + } else { + callback_->OnMessageFromPeer(peer_id, message); + } +} + +bool PeerConnectionClient::GetHeaderValue(const std::string& data, + size_t eoh, + const char* header_pattern, + size_t* value) { + ASSERT(value != NULL); + size_t found = data.find(header_pattern); + if (found != std::string::npos && found < eoh) { + *value = atoi(&data[found + strlen(header_pattern)]); + return true; + } + return false; +} + +bool PeerConnectionClient::GetHeaderValue(const std::string& data, size_t eoh, + const char* header_pattern, + std::string* value) { + ASSERT(value != NULL); + size_t found = data.find(header_pattern); + if (found != std::string::npos && found < eoh) { + size_t begin = found + strlen(header_pattern); + size_t end = data.find("\r\n", begin); + if (end == std::string::npos) + end = eoh; + value->assign(data.substr(begin, end - begin)); + return true; + } + return false; +} + +bool PeerConnectionClient::ReadIntoBuffer(talk_base::AsyncSocket* socket, + std::string* data, + size_t* content_length) { + char buffer[0xffff]; + do { + int bytes = socket->Recv(buffer, sizeof(buffer)); + if (bytes <= 0) + break; + data->append(buffer, bytes); + } while (true); + + bool ret = false; + size_t i = data->find("\r\n\r\n"); + if (i != std::string::npos) { + LOG(INFO) << "Headers received"; + if (GetHeaderValue(*data, i, "\r\nContent-Length: ", content_length)) { + size_t total_response_size = (i + 4) + *content_length; + if (data->length() >= total_response_size) { + ret = true; + std::string should_close; + const char kConnection[] = "\r\nConnection: "; + if (GetHeaderValue(*data, i, kConnection, &should_close) && + should_close.compare("close") == 0) { + socket->Close(); + // Since we closed the socket, there was no notification delivered + // to us. Compensate by letting ourselves know. + OnClose(socket, 0); + } + } else { + // We haven't received everything. Just continue to accept data. + } + } else { + LOG(LS_ERROR) << "No content length field specified by the server."; + } + } + return ret; +} + +void PeerConnectionClient::OnRead(talk_base::AsyncSocket* socket) { + size_t content_length = 0; + if (ReadIntoBuffer(socket, &control_data_, &content_length)) { + size_t peer_id = 0, eoh = 0; + bool ok = ParseServerResponse(control_data_, content_length, &peer_id, + &eoh); + if (ok) { + if (my_id_ == -1) { + // First response. Let's store our server assigned ID. + ASSERT(state_ == SIGNING_IN); + my_id_ = peer_id; + ASSERT(my_id_ != -1); + + // The body of the response will be a list of already connected peers. + if (content_length) { + size_t pos = eoh + 4; + while (pos < control_data_.size()) { + size_t eol = control_data_.find('\n', pos); + if (eol == std::string::npos) + break; + int id = 0; + std::string name; + bool connected; + if (ParseEntry(control_data_.substr(pos, eol - pos), &name, &id, + &connected) && id != my_id_) { + peers_[id] = name; + callback_->OnPeerConnected(id, name); + } + pos = eol + 1; + } + } + ASSERT(is_connected()); + callback_->OnSignedIn(); + } else if (state_ == SIGNING_OUT) { + Close(); + callback_->OnDisconnected(); + } else if (state_ == SIGNING_OUT_WAITING) { + SignOut(); + } + } + + control_data_.clear(); + + if (state_ == SIGNING_IN) { + ASSERT(hanging_get_->GetState() == talk_base::Socket::CS_CLOSED); + state_ = CONNECTED; + hanging_get_->Connect(server_address_); + } + } +} + +void PeerConnectionClient::OnHangingGetRead(talk_base::AsyncSocket* socket) { + LOG(INFO) << __FUNCTION__; + size_t content_length = 0; + if (ReadIntoBuffer(socket, ¬ification_data_, &content_length)) { + size_t peer_id = 0, eoh = 0; + bool ok = ParseServerResponse(notification_data_, content_length, + &peer_id, &eoh); + + if (ok) { + // Store the position where the body begins. + size_t pos = eoh + 4; + + if (my_id_ == static_cast(peer_id)) { + // A notification about a new member or a member that just + // disconnected. + int id = 0; + std::string name; + bool connected = false; + if (ParseEntry(notification_data_.substr(pos), &name, &id, + &connected)) { + if (connected) { + peers_[id] = name; + callback_->OnPeerConnected(id, name); + } else { + peers_.erase(id); + callback_->OnPeerDisconnected(id); + } + } + } else { + OnMessageFromPeer(peer_id, notification_data_.substr(pos)); + } + } + + notification_data_.clear(); + } + + if (hanging_get_->GetState() == talk_base::Socket::CS_CLOSED && + state_ == CONNECTED) { + hanging_get_->Connect(server_address_); + } +} + +bool PeerConnectionClient::ParseEntry(const std::string& entry, + std::string* name, + int* id, + bool* connected) { + ASSERT(name != NULL); + ASSERT(id != NULL); + ASSERT(connected != NULL); + ASSERT(!entry.empty()); + + *connected = false; + size_t separator = entry.find(','); + if (separator != std::string::npos) { + *id = atoi(&entry[separator + 1]); + name->assign(entry.substr(0, separator)); + separator = entry.find(',', separator + 1); + if (separator != std::string::npos) { + *connected = atoi(&entry[separator + 1]) ? true : false; + } + } + return !name->empty(); +} + +int PeerConnectionClient::GetResponseStatus(const std::string& response) { + int status = -1; + size_t pos = response.find(' '); + if (pos != std::string::npos) + status = atoi(&response[pos + 1]); + return status; +} + +bool PeerConnectionClient::ParseServerResponse(const std::string& response, + size_t content_length, + size_t* peer_id, + size_t* eoh) { + int status = GetResponseStatus(response.c_str()); + if (status != 200) { + LOG(LS_ERROR) << "Received error from server"; + Close(); + callback_->OnDisconnected(); + return false; + } + + *eoh = response.find("\r\n\r\n"); + ASSERT(*eoh != std::string::npos); + if (*eoh == std::string::npos) + return false; + + *peer_id = -1; + + // See comment in peer_channel.cc for why we use the Pragma header and + // not e.g. "X-Peer-Id". + GetHeaderValue(response, *eoh, "\r\nPragma: ", peer_id); + + return true; +} + +void PeerConnectionClient::OnClose(talk_base::AsyncSocket* socket, int err) { + LOG(INFO) << __FUNCTION__; + + socket->Close(); + +#ifdef WIN32 + if (err != WSAECONNREFUSED) { +#else + if (err != ECONNREFUSED) { +#endif + if (socket == hanging_get_.get()) { + if (state_ == CONNECTED) { + hanging_get_->Close(); + hanging_get_->Connect(server_address_); + } + } else { + callback_->OnMessageSent(err); + } + } else { + if (socket == control_socket_.get()) { + LOG(WARNING) << "Connection refused; retrying in 2 seconds"; + talk_base::Thread::Current()->PostDelayed(kReconnectDelay, this, 0); + } else { + Close(); + callback_->OnDisconnected(); + } + } +} + +void PeerConnectionClient::OnMessage(talk_base::Message* msg) { + // ignore msg; there is currently only one supported message ("retry") + DoConnect(); +} diff --git a/talk/examples/peerconnection/client/peer_connection_client.h b/talk/examples/peerconnection/client/peer_connection_client.h new file mode 100644 index 000000000..d31262b67 --- /dev/null +++ b/talk/examples/peerconnection/client/peer_connection_client.h @@ -0,0 +1,140 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef PEERCONNECTION_SAMPLES_CLIENT_PEER_CONNECTION_CLIENT_H_ +#define PEERCONNECTION_SAMPLES_CLIENT_PEER_CONNECTION_CLIENT_H_ +#pragma once + +#include +#include + +#include "talk/base/nethelpers.h" +#include "talk/base/signalthread.h" +#include "talk/base/sigslot.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/scoped_ptr.h" + +typedef std::map Peers; + +struct PeerConnectionClientObserver { + virtual void OnSignedIn() = 0; // Called when we're logged on. + virtual void OnDisconnected() = 0; + virtual void OnPeerConnected(int id, const std::string& name) = 0; + virtual void OnPeerDisconnected(int peer_id) = 0; + virtual void OnMessageFromPeer(int peer_id, const std::string& message) = 0; + virtual void OnMessageSent(int err) = 0; + virtual void OnServerConnectionFailure() = 0; + + protected: + virtual ~PeerConnectionClientObserver() {} +}; + +class PeerConnectionClient : public sigslot::has_slots<>, + public talk_base::MessageHandler { + public: + enum State { + NOT_CONNECTED, + RESOLVING, + SIGNING_IN, + CONNECTED, + SIGNING_OUT_WAITING, + SIGNING_OUT, + }; + + PeerConnectionClient(); + ~PeerConnectionClient(); + + int id() const; + bool is_connected() const; + const Peers& peers() const; + + void RegisterObserver(PeerConnectionClientObserver* callback); + + void Connect(const std::string& server, int port, + const std::string& client_name); + + bool SendToPeer(int peer_id, const std::string& message); + bool SendHangUp(int peer_id); + bool IsSendingMessage(); + + bool SignOut(); + + // implements the MessageHandler interface + void OnMessage(talk_base::Message* msg); + + protected: + void DoConnect(); + void Close(); + void InitSocketSignals(); + bool ConnectControlSocket(); + void OnConnect(talk_base::AsyncSocket* socket); + void OnHangingGetConnect(talk_base::AsyncSocket* socket); + void OnMessageFromPeer(int peer_id, const std::string& message); + + // Quick and dirty support for parsing HTTP header values. + bool GetHeaderValue(const std::string& data, size_t eoh, + const char* header_pattern, size_t* value); + + bool GetHeaderValue(const std::string& data, size_t eoh, + const char* header_pattern, std::string* value); + + // Returns true if the whole response has been read. + bool ReadIntoBuffer(talk_base::AsyncSocket* socket, std::string* data, + size_t* content_length); + + void OnRead(talk_base::AsyncSocket* socket); + + void OnHangingGetRead(talk_base::AsyncSocket* socket); + + // Parses a single line entry in the form ",," + bool ParseEntry(const std::string& entry, std::string* name, int* id, + bool* connected); + + int GetResponseStatus(const std::string& response); + + bool ParseServerResponse(const std::string& response, size_t content_length, + size_t* peer_id, size_t* eoh); + + void OnClose(talk_base::AsyncSocket* socket, int err); + + void OnResolveResult(talk_base::SignalThread *t); + + PeerConnectionClientObserver* callback_; + talk_base::SocketAddress server_address_; + talk_base::AsyncResolver* resolver_; + talk_base::scoped_ptr control_socket_; + talk_base::scoped_ptr hanging_get_; + std::string onconnect_data_; + std::string control_data_; + std::string notification_data_; + std::string client_name_; + Peers peers_; + State state_; + int my_id_; +}; + +#endif // PEERCONNECTION_SAMPLES_CLIENT_PEER_CONNECTION_CLIENT_H_ diff --git a/talk/examples/peerconnection/peerconnection.scons b/talk/examples/peerconnection/peerconnection.scons new file mode 100644 index 000000000..7a7d2f3be --- /dev/null +++ b/talk/examples/peerconnection/peerconnection.scons @@ -0,0 +1,64 @@ +# -*- Python -*- +import talk + +Import('env') + +if env.Bit('have_webrtc_voice') and env.Bit('have_webrtc_video'): + talk.App( + env, + name = 'peerconnection_client', + # TODO: Build peerconnection_client on mac. + libs = [ + 'base', + 'expat', + 'json', + 'p2p', + 'peerconnection', + 'phone', + 'srtp', + 'xmllite', + 'xmpp', + 'yuvscaler', + ], + win_srcs = [ + 'client/conductor.cc', + 'client/defaults.cc', + 'client/main.cc', + 'client/main_wnd.cc', + 'client/peer_connection_client.cc', + ], + posix_libs = [ + 'crypto', + 'securetunnel', + 'ssl', + ], + lin_srcs = [ + 'client/conductor.cc', + 'client/defaults.cc', + 'client/peer_connection_client.cc', + 'client/linux/main.cc', + 'client/linux/main_wnd.cc', + ], + lin_packages = [ + 'glib-2.0', + 'gobject-2.0', + 'gtk+-2.0', + ], + lin_libs = [ + 'sound', + ], + win_link_flags = [ + ('', '/nodefaultlib:libcmt')[env.Bit('debug')], + ], + ) + + talk.App( + env, + name = 'peerconnection_server', + srcs = [ + 'server/data_socket.cc', + 'server/main.cc', + 'server/peer_channel.cc', + 'server/utils.cc', + ], + ) diff --git a/talk/examples/peerconnection/server/data_socket.cc b/talk/examples/peerconnection/server/data_socket.cc new file mode 100644 index 000000000..58370b4ee --- /dev/null +++ b/talk/examples/peerconnection/server/data_socket.cc @@ -0,0 +1,306 @@ +/* + * libjingle + * Copyright 2011, 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/examples/peerconnection/server/data_socket.h" + +#include +#include +#include +#if defined(POSIX) +#include +#endif + +#include "talk/examples/peerconnection/server/utils.h" + +static const char kHeaderTerminator[] = "\r\n\r\n"; +static const int kHeaderTerminatorLength = sizeof(kHeaderTerminator) - 1; + +// static +const char DataSocket::kCrossOriginAllowHeaders[] = + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Credentials: true\r\n" + "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n" + "Access-Control-Allow-Headers: Content-Type, " + "Content-Length, Connection, Cache-Control\r\n" + "Access-Control-Expose-Headers: Content-Length, X-Peer-Id\r\n"; + +#if defined(WIN32) +class WinsockInitializer { + static WinsockInitializer singleton; + + WinsockInitializer() { + WSADATA data; + WSAStartup(MAKEWORD(1, 0), &data); + } + + public: + ~WinsockInitializer() { WSACleanup(); } +}; +WinsockInitializer WinsockInitializer::singleton; +#endif + +// +// SocketBase +// + +bool SocketBase::Create() { + assert(!valid()); + socket_ = ::socket(AF_INET, SOCK_STREAM, 0); + return valid(); +} + +void SocketBase::Close() { + if (socket_ != INVALID_SOCKET) { + closesocket(socket_); + socket_ = INVALID_SOCKET; + } +} + +// +// DataSocket +// + +std::string DataSocket::request_arguments() const { + size_t args = request_path_.find('?'); + if (args != std::string::npos) + return request_path_.substr(args + 1); + return ""; +} + +bool DataSocket::PathEquals(const char* path) const { + assert(path); + size_t args = request_path_.find('?'); + if (args != std::string::npos) + return request_path_.substr(0, args).compare(path) == 0; + return request_path_.compare(path) == 0; +} + +bool DataSocket::OnDataAvailable(bool* close_socket) { + assert(valid()); + char buffer[0xfff] = {0}; + int bytes = recv(socket_, buffer, sizeof(buffer), 0); + if (bytes == SOCKET_ERROR || bytes == 0) { + *close_socket = true; + return false; + } + + *close_socket = false; + + bool ret = true; + if (headers_received()) { + if (method_ != POST) { + // unexpectedly received data. + ret = false; + } else { + data_.append(buffer, bytes); + } + } else { + request_headers_.append(buffer, bytes); + size_t found = request_headers_.find(kHeaderTerminator); + if (found != std::string::npos) { + data_ = request_headers_.substr(found + kHeaderTerminatorLength); + request_headers_.resize(found + kHeaderTerminatorLength); + ret = ParseHeaders(); + } + } + return ret; +} + +bool DataSocket::Send(const std::string& data) const { + return send(socket_, data.data(), data.length(), 0) != SOCKET_ERROR; +} + +bool DataSocket::Send(const std::string& status, bool connection_close, + const std::string& content_type, + const std::string& extra_headers, + const std::string& data) const { + assert(valid()); + assert(!status.empty()); + std::string buffer("HTTP/1.1 " + status + "\r\n"); + + buffer += "Server: PeerConnectionTestServer/0.1\r\n" + "Cache-Control: no-cache\r\n"; + + if (connection_close) + buffer += "Connection: close\r\n"; + + if (!content_type.empty()) + buffer += "Content-Type: " + content_type + "\r\n"; + + buffer += "Content-Length: " + int2str(data.size()) + "\r\n"; + + if (!extra_headers.empty()) { + buffer += extra_headers; + // Extra headers are assumed to have a separator per header. + } + + buffer += kCrossOriginAllowHeaders; + + buffer += "\r\n"; + buffer += data; + + return Send(buffer); +} + +void DataSocket::Clear() { + method_ = INVALID; + content_length_ = 0; + content_type_.clear(); + request_path_.clear(); + request_headers_.clear(); + data_.clear(); +} + +bool DataSocket::ParseHeaders() { + assert(!request_headers_.empty()); + assert(method_ == INVALID); + size_t i = request_headers_.find("\r\n"); + if (i == std::string::npos) + return false; + + if (!ParseMethodAndPath(request_headers_.data(), i)) + return false; + + assert(method_ != INVALID); + assert(!request_path_.empty()); + + if (method_ == POST) { + const char* headers = request_headers_.data() + i + 2; + size_t len = request_headers_.length() - i - 2; + if (!ParseContentLengthAndType(headers, len)) + return false; + } + + return true; +} + +bool DataSocket::ParseMethodAndPath(const char* begin, size_t len) { + struct { + const char* method_name; + size_t method_name_len; + RequestMethod id; + } supported_methods[] = { + { "GET", 3, GET }, + { "POST", 4, POST }, + { "OPTIONS", 7, OPTIONS }, + }; + + const char* path = NULL; + for (size_t i = 0; i < ARRAYSIZE(supported_methods); ++i) { + if (len > supported_methods[i].method_name_len && + isspace(begin[supported_methods[i].method_name_len]) && + strncmp(begin, supported_methods[i].method_name, + supported_methods[i].method_name_len) == 0) { + method_ = supported_methods[i].id; + path = begin + supported_methods[i].method_name_len; + break; + } + } + + const char* end = begin + len; + if (!path || path >= end) + return false; + + ++path; + begin = path; + while (!isspace(*path) && path < end) + ++path; + + request_path_.assign(begin, path - begin); + + return true; +} + +bool DataSocket::ParseContentLengthAndType(const char* headers, size_t length) { + assert(content_length_ == 0); + assert(content_type_.empty()); + + const char* end = headers + length; + while (headers && headers < end) { + if (!isspace(headers[0])) { + static const char kContentLength[] = "Content-Length:"; + static const char kContentType[] = "Content-Type:"; + if ((headers + ARRAYSIZE(kContentLength)) < end && + strncmp(headers, kContentLength, + ARRAYSIZE(kContentLength) - 1) == 0) { + headers += ARRAYSIZE(kContentLength) - 1; + while (headers[0] == ' ') + ++headers; + content_length_ = atoi(headers); + } else if ((headers + ARRAYSIZE(kContentType)) < end && + strncmp(headers, kContentType, + ARRAYSIZE(kContentType) - 1) == 0) { + headers += ARRAYSIZE(kContentType) - 1; + while (headers[0] == ' ') + ++headers; + const char* type_end = strstr(headers, "\r\n"); + if (type_end == NULL) + type_end = end; + content_type_.assign(headers, type_end); + } + } else { + ++headers; + } + headers = strstr(headers, "\r\n"); + if (headers) + headers += 2; + } + + return !content_type_.empty() && content_length_ != 0; +} + +// +// ListeningSocket +// + +bool ListeningSocket::Listen(unsigned short port) { + assert(valid()); + int enabled = 1; + setsockopt(socket_, SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&enabled), sizeof(enabled)); + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(port); + if (bind(socket_, reinterpret_cast(&addr), + sizeof(addr)) == SOCKET_ERROR) { + printf("bind failed\n"); + return false; + } + return listen(socket_, 5) != SOCKET_ERROR; +} + +DataSocket* ListeningSocket::Accept() const { + assert(valid()); + struct sockaddr_in addr = {0}; + socklen_t size = sizeof(addr); + int client = accept(socket_, reinterpret_cast(&addr), &size); + if (client == INVALID_SOCKET) + return NULL; + + return new DataSocket(client); +} diff --git a/talk/examples/peerconnection/server/data_socket.h b/talk/examples/peerconnection/server/data_socket.h new file mode 100644 index 000000000..b2ba28de2 --- /dev/null +++ b/talk/examples/peerconnection/server/data_socket.h @@ -0,0 +1,168 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_EXAMPLES_PEERCONNECTION_SERVER_DATA_SOCKET_H_ +#define TALK_EXAMPLES_PEERCONNECTION_SERVER_DATA_SOCKET_H_ +#pragma once + +#ifdef WIN32 +#include +typedef int socklen_t; +#else +#include +#include +#include +#define closesocket close +#endif + +#include + +#ifndef SOCKET_ERROR +#define SOCKET_ERROR (-1) +#endif + +#ifndef INVALID_SOCKET +#define INVALID_SOCKET static_cast(~0) +#endif + +class SocketBase { + public: + SocketBase() : socket_(INVALID_SOCKET) { } + explicit SocketBase(int socket) : socket_(socket) { } + ~SocketBase() { Close(); } + + int socket() const { return socket_; } + bool valid() const { return socket_ != INVALID_SOCKET; } + + bool Create(); + void Close(); + + protected: + int socket_; +}; + +// Represents an HTTP server socket. +class DataSocket : public SocketBase { + public: + enum RequestMethod { + INVALID, + GET, + POST, + OPTIONS, + }; + + explicit DataSocket(int socket) + : SocketBase(socket), + method_(INVALID), + content_length_(0) { + } + + ~DataSocket() { + } + + static const char kCrossOriginAllowHeaders[]; + + bool headers_received() const { return method_ != INVALID; } + + RequestMethod method() const { return method_; } + + const std::string& request_path() const { return request_path_; } + std::string request_arguments() const; + + const std::string& data() const { return data_; } + + const std::string& content_type() const { return content_type_; } + + size_t content_length() const { return content_length_; } + + bool request_received() const { + return headers_received() && (method_ != POST || data_received()); + } + + bool data_received() const { + return method_ != POST || data_.length() >= content_length_; + } + + // Checks if the request path (minus arguments) matches a given path. + bool PathEquals(const char* path) const; + + // Called when we have received some data from clients. + // Returns false if an error occurred. + bool OnDataAvailable(bool* close_socket); + + // Send a raw buffer of bytes. + bool Send(const std::string& data) const; + + // Send an HTTP response. The |status| should start with a valid HTTP + // response code, followed by a string. E.g. "200 OK". + // If |connection_close| is set to true, an extra "Connection: close" HTTP + // header will be included. |content_type| is the mime content type, not + // including the "Content-Type: " string. + // |extra_headers| should be either empty or a list of headers where each + // header terminates with "\r\n". + // |data| is the body of the message. It's length will be specified via + // a "Content-Length" header. + bool Send(const std::string& status, bool connection_close, + const std::string& content_type, + const std::string& extra_headers, const std::string& data) const; + + // Clears all held state and prepares the socket for receiving a new request. + void Clear(); + + protected: + // A fairly relaxed HTTP header parser. Parses the method, path and + // content length (POST only) of a request. + // Returns true if a valid request was received and no errors occurred. + bool ParseHeaders(); + + // Figures out whether the request is a GET or POST and what path is + // being requested. + bool ParseMethodAndPath(const char* begin, size_t len); + + // Determines the length of the body and it's mime type. + bool ParseContentLengthAndType(const char* headers, size_t length); + + protected: + RequestMethod method_; + size_t content_length_; + std::string content_type_; + std::string request_path_; + std::string request_headers_; + std::string data_; +}; + +// The server socket. Accepts connections and generates DataSocket instances +// for each new connection. +class ListeningSocket : public SocketBase { + public: + ListeningSocket() {} + + bool Listen(unsigned short port); + DataSocket* Accept() const; +}; + +#endif // TALK_EXAMPLES_PEERCONNECTION_SERVER_DATA_SOCKET_H_ diff --git a/talk/examples/peerconnection/server/main.cc b/talk/examples/peerconnection/server/main.cc new file mode 100644 index 000000000..40ede93fa --- /dev/null +++ b/talk/examples/peerconnection/server/main.cc @@ -0,0 +1,190 @@ +/* + * libjingle + * Copyright 2011, 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 +#include +#include + +#include + +#include "talk/base/flags.h" +#include "talk/examples/peerconnection/server/data_socket.h" +#include "talk/examples/peerconnection/server/peer_channel.h" +#include "talk/examples/peerconnection/server/utils.h" + +DEFINE_bool(help, false, "Prints this message"); +DEFINE_int(port, 8888, "The port on which to listen."); + +static const size_t kMaxConnections = (FD_SETSIZE - 2); + +void HandleBrowserRequest(DataSocket* ds, bool* quit) { + assert(ds && ds->valid()); + assert(quit); + + const std::string& path = ds->request_path(); + + *quit = (path.compare("/quit") == 0); + + if (*quit) { + ds->Send("200 OK", true, "text/html", "", + "Quitting..."); + } else if (ds->method() == DataSocket::OPTIONS) { + // We'll get this when a browsers do cross-resource-sharing requests. + // The headers to allow cross-origin script support will be set inside + // Send. + ds->Send("200 OK", true, "", "", ""); + } else { + // Here we could write some useful output back to the browser depending on + // the path. + printf("Received an invalid request: %s\n", ds->request_path().c_str()); + ds->Send("500 Sorry", true, "text/html", "", + "Sorry, not yet implemented"); + } +} + +int main(int argc, char** argv) { + FlagList::SetFlagsFromCommandLine(&argc, argv, true); + if (FLAG_help) { + FlagList::Print(NULL, false); + return 0; + } + + // Abort if the user specifies a port that is outside the allowed + // range [1, 65535]. + if ((FLAG_port < 1) || (FLAG_port > 65535)) { + printf("Error: %i is not a valid port.\n", FLAG_port); + return -1; + } + + ListeningSocket listener; + if (!listener.Create()) { + printf("Failed to create server socket\n"); + return -1; + } else if (!listener.Listen(FLAG_port)) { + printf("Failed to listen on server socket\n"); + return -1; + } + + printf("Server listening on port %i\n", FLAG_port); + + PeerChannel clients; + typedef std::vector SocketArray; + SocketArray sockets; + bool quit = false; + while (!quit) { + fd_set socket_set; + FD_ZERO(&socket_set); + if (listener.valid()) + FD_SET(listener.socket(), &socket_set); + + for (SocketArray::iterator i = sockets.begin(); i != sockets.end(); ++i) + FD_SET((*i)->socket(), &socket_set); + + struct timeval timeout = { 10, 0 }; + if (select(FD_SETSIZE, &socket_set, NULL, NULL, &timeout) == SOCKET_ERROR) { + printf("select failed\n"); + break; + } + + for (SocketArray::iterator i = sockets.begin(); i != sockets.end(); ++i) { + DataSocket* s = *i; + bool socket_done = true; + if (FD_ISSET(s->socket(), &socket_set)) { + if (s->OnDataAvailable(&socket_done) && s->request_received()) { + ChannelMember* member = clients.Lookup(s); + if (member || PeerChannel::IsPeerConnection(s)) { + if (!member) { + if (s->PathEquals("/sign_in")) { + clients.AddMember(s); + } else { + printf("No member found for: %s\n", + s->request_path().c_str()); + s->Send("500 Error", true, "text/plain", "", + "Peer most likely gone."); + } + } else if (member->is_wait_request(s)) { + // no need to do anything. + socket_done = false; + } else { + ChannelMember* target = clients.IsTargetedRequest(s); + if (target) { + member->ForwardRequestToPeer(s, target); + } else if (s->PathEquals("/sign_out")) { + s->Send("200 OK", true, "text/plain", "", ""); + } else { + printf("Couldn't find target for request: %s\n", + s->request_path().c_str()); + s->Send("500 Error", true, "text/plain", "", + "Peer most likely gone."); + } + } + } else { + HandleBrowserRequest(s, &quit); + if (quit) { + printf("Quitting...\n"); + FD_CLR(listener.socket(), &socket_set); + listener.Close(); + clients.CloseAll(); + } + } + } + } else { + socket_done = false; + } + + if (socket_done) { + printf("Disconnecting socket\n"); + clients.OnClosing(s); + assert(s->valid()); // Close must not have been called yet. + FD_CLR(s->socket(), &socket_set); + delete (*i); + i = sockets.erase(i); + if (i == sockets.end()) + break; + } + } + + clients.CheckForTimeout(); + + if (FD_ISSET(listener.socket(), &socket_set)) { + DataSocket* s = listener.Accept(); + if (sockets.size() >= kMaxConnections) { + delete s; // sorry, that's all we can take. + printf("Connection limit reached\n"); + } else { + sockets.push_back(s); + printf("New connection...\n"); + } + } + } + + for (SocketArray::iterator i = sockets.begin(); i != sockets.end(); ++i) + delete (*i); + sockets.clear(); + + return 0; +} diff --git a/talk/examples/peerconnection/server/peer_channel.cc b/talk/examples/peerconnection/server/peer_channel.cc new file mode 100644 index 000000000..260346599 --- /dev/null +++ b/talk/examples/peerconnection/server/peer_channel.cc @@ -0,0 +1,369 @@ +/* + * libjingle + * Copyright 2011, 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/examples/peerconnection/server/peer_channel.h" + +#include +#include +#include + +#include + +#include "talk/examples/peerconnection/server/data_socket.h" +#include "talk/examples/peerconnection/server/utils.h" + +// Set to the peer id of the originator when messages are being +// exchanged between peers, but set to the id of the receiving peer +// itself when notifications are sent from the server about the state +// of other peers. +// +// WORKAROUND: Since support for CORS varies greatly from one browser to the +// next, we don't use a custom name for our peer-id header (originally it was +// "X-Peer-Id: "). Instead, we use a "simple header", "Pragma" which should +// always be exposed to CORS requests. There is a special CORS header devoted +// to exposing proprietary headers (Access-Control-Expose-Headers), however +// at this point it is not working correctly in some popular browsers. +static const char kPeerIdHeader[] = "Pragma: "; + +static const char* kRequestPaths[] = { + "/wait", "/sign_out", "/message", +}; + +enum RequestPathIndex { + kWait, + kSignOut, + kMessage, +}; + +// +// ChannelMember +// + +int ChannelMember::s_member_id_ = 0; + +ChannelMember::ChannelMember(DataSocket* socket) + : waiting_socket_(NULL), id_(++s_member_id_), + connected_(true), timestamp_(time(NULL)) { + assert(socket); + assert(socket->method() == DataSocket::GET); + assert(socket->PathEquals("/sign_in")); + name_ = socket->request_arguments(); // TODO: urldecode + if (!name_.length()) + name_ = "peer_" + int2str(id_); + std::replace(name_.begin(), name_.end(), ',', '_'); +} + +ChannelMember::~ChannelMember() { +} + +bool ChannelMember::is_wait_request(DataSocket* ds) const { + return ds && ds->PathEquals(kRequestPaths[kWait]); +} + +bool ChannelMember::TimedOut() { + return waiting_socket_ == NULL && (time(NULL) - timestamp_) > 30; +} + +std::string ChannelMember::GetPeerIdHeader() const { + std::string ret(kPeerIdHeader + int2str(id_) + "\r\n"); + return ret; +} + +bool ChannelMember::NotifyOfOtherMember(const ChannelMember& other) { + assert(&other != this); + QueueResponse("200 OK", "text/plain", GetPeerIdHeader(), + other.GetEntry()); + return true; +} + +// Returns a string in the form "name,id\n". +std::string ChannelMember::GetEntry() const { + char entry[1024] = {0}; + sprintf(entry, "%s,%i,%i\n", name_.c_str(), id_, connected_); // NOLINT + return entry; +} + +void ChannelMember::ForwardRequestToPeer(DataSocket* ds, ChannelMember* peer) { + assert(peer); + assert(ds); + + std::string extra_headers(GetPeerIdHeader()); + + if (peer == this) { + ds->Send("200 OK", true, ds->content_type(), extra_headers, + ds->data()); + } else { + printf("Client %s sending to %s\n", + name_.c_str(), peer->name().c_str()); + peer->QueueResponse("200 OK", ds->content_type(), extra_headers, + ds->data()); + ds->Send("200 OK", true, "text/plain", "", ""); + } +} + +void ChannelMember::OnClosing(DataSocket* ds) { + if (ds == waiting_socket_) { + waiting_socket_ = NULL; + timestamp_ = time(NULL); + } +} + +void ChannelMember::QueueResponse(const std::string& status, + const std::string& content_type, + const std::string& extra_headers, + const std::string& data) { + if (waiting_socket_) { + assert(queue_.size() == 0); + assert(waiting_socket_->method() == DataSocket::GET); + bool ok = waiting_socket_->Send(status, true, content_type, extra_headers, + data); + if (!ok) { + printf("Failed to deliver data to waiting socket\n"); + } + waiting_socket_ = NULL; + timestamp_ = time(NULL); + } else { + QueuedResponse qr; + qr.status = status; + qr.content_type = content_type; + qr.extra_headers = extra_headers; + qr.data = data; + queue_.push(qr); + } +} + +void ChannelMember::SetWaitingSocket(DataSocket* ds) { + assert(ds->method() == DataSocket::GET); + if (ds && !queue_.empty()) { + assert(waiting_socket_ == NULL); + const QueuedResponse& response = queue_.front(); + ds->Send(response.status, true, response.content_type, + response.extra_headers, response.data); + queue_.pop(); + } else { + waiting_socket_ = ds; + } +} + + +// +// PeerChannel +// + +// static +bool PeerChannel::IsPeerConnection(const DataSocket* ds) { + assert(ds); + return (ds->method() == DataSocket::POST && ds->content_length() > 0) || + (ds->method() == DataSocket::GET && ds->PathEquals("/sign_in")); +} + +ChannelMember* PeerChannel::Lookup(DataSocket* ds) const { + assert(ds); + + if (ds->method() != DataSocket::GET && ds->method() != DataSocket::POST) + return NULL; + + size_t i = 0; + for (; i < ARRAYSIZE(kRequestPaths); ++i) { + if (ds->PathEquals(kRequestPaths[i])) + break; + } + + if (i == ARRAYSIZE(kRequestPaths)) + return NULL; + + std::string args(ds->request_arguments()); + static const char kPeerId[] = "peer_id="; + size_t found = args.find(kPeerId); + if (found == std::string::npos) + return NULL; + + int id = atoi(&args[found + ARRAYSIZE(kPeerId) - 1]); + Members::const_iterator iter = members_.begin(); + for (; iter != members_.end(); ++iter) { + if (id == (*iter)->id()) { + if (i == kWait) + (*iter)->SetWaitingSocket(ds); + if (i == kSignOut) + (*iter)->set_disconnected(); + return *iter; + } + } + + return NULL; +} + +ChannelMember* PeerChannel::IsTargetedRequest(const DataSocket* ds) const { + assert(ds); + // Regardless of GET or POST, we look for the peer_id parameter + // only in the request_path. + const std::string& path = ds->request_path(); + size_t args = path.find('?'); + if (args == std::string::npos) + return NULL; + size_t found; + const char kTargetPeerIdParam[] = "to="; + do { + found = path.find(kTargetPeerIdParam, args); + if (found == std::string::npos) + return NULL; + if (found == (args + 1) || path[found - 1] == '&') { + found += ARRAYSIZE(kTargetPeerIdParam) - 1; + break; + } + args = found + ARRAYSIZE(kTargetPeerIdParam) - 1; + } while (true); + int id = atoi(&path[found]); + Members::const_iterator i = members_.begin(); + for (; i != members_.end(); ++i) { + if ((*i)->id() == id) { + return *i; + } + } + return NULL; +} + +bool PeerChannel::AddMember(DataSocket* ds) { + assert(IsPeerConnection(ds)); + ChannelMember* new_guy = new ChannelMember(ds); + Members failures; + BroadcastChangedState(*new_guy, &failures); + HandleDeliveryFailures(&failures); + members_.push_back(new_guy); + + printf("New member added (total=%s): %s\n", + size_t2str(members_.size()).c_str(), new_guy->name().c_str()); + + // Let the newly connected peer know about other members of the channel. + std::string content_type; + std::string response = BuildResponseForNewMember(*new_guy, &content_type); + ds->Send("200 Added", true, content_type, new_guy->GetPeerIdHeader(), + response); + return true; +} + +void PeerChannel::CloseAll() { + Members::const_iterator i = members_.begin(); + for (; i != members_.end(); ++i) { + (*i)->QueueResponse("200 OK", "text/plain", "", "Server shutting down"); + } + DeleteAll(); +} + +void PeerChannel::OnClosing(DataSocket* ds) { + for (Members::iterator i = members_.begin(); i != members_.end(); ++i) { + ChannelMember* m = (*i); + m->OnClosing(ds); + if (!m->connected()) { + i = members_.erase(i); + Members failures; + BroadcastChangedState(*m, &failures); + HandleDeliveryFailures(&failures); + delete m; + if (i == members_.end()) + break; + } + } + printf("Total connected: %s\n", size_t2str(members_.size()).c_str()); +} + +void PeerChannel::CheckForTimeout() { + for (Members::iterator i = members_.begin(); i != members_.end(); ++i) { + ChannelMember* m = (*i); + if (m->TimedOut()) { + printf("Timeout: %s\n", m->name().c_str()); + m->set_disconnected(); + i = members_.erase(i); + Members failures; + BroadcastChangedState(*m, &failures); + HandleDeliveryFailures(&failures); + delete m; + if (i == members_.end()) + break; + } + } +} + +void PeerChannel::DeleteAll() { + for (Members::iterator i = members_.begin(); i != members_.end(); ++i) + delete (*i); + members_.clear(); +} + +void PeerChannel::BroadcastChangedState(const ChannelMember& member, + Members* delivery_failures) { + // This function should be called prior to DataSocket::Close(). + assert(delivery_failures); + + if (!member.connected()) { + printf("Member disconnected: %s\n", member.name().c_str()); + } + + Members::iterator i = members_.begin(); + for (; i != members_.end(); ++i) { + if (&member != (*i)) { + if (!(*i)->NotifyOfOtherMember(member)) { + (*i)->set_disconnected(); + delivery_failures->push_back(*i); + i = members_.erase(i); + if (i == members_.end()) + break; + } + } + } +} + +void PeerChannel::HandleDeliveryFailures(Members* failures) { + assert(failures); + + while (!failures->empty()) { + Members::iterator i = failures->begin(); + ChannelMember* member = *i; + assert(!member->connected()); + failures->erase(i); + BroadcastChangedState(*member, failures); + delete member; + } +} + +// Builds a simple list of "name,id\n" entries for each member. +std::string PeerChannel::BuildResponseForNewMember(const ChannelMember& member, + std::string* content_type) { + assert(content_type); + + *content_type = "text/plain"; + // The peer itself will always be the first entry. + std::string response(member.GetEntry()); + for (Members::iterator i = members_.begin(); i != members_.end(); ++i) { + if (member.id() != (*i)->id()) { + assert((*i)->connected()); + response += (*i)->GetEntry(); + } + } + + return response; +} diff --git a/talk/examples/peerconnection/server/peer_channel.h b/talk/examples/peerconnection/server/peer_channel.h new file mode 100644 index 000000000..2ecc78911 --- /dev/null +++ b/talk/examples/peerconnection/server/peer_channel.h @@ -0,0 +1,137 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_EXAMPLES_PEERCONNECTION_SERVER_PEER_CHANNEL_H_ +#define TALK_EXAMPLES_PEERCONNECTION_SERVER_PEER_CHANNEL_H_ +#pragma once + +#include + +#include +#include +#include + +class DataSocket; + +// Represents a single peer connected to the server. +class ChannelMember { + public: + explicit ChannelMember(DataSocket* socket); + ~ChannelMember(); + + bool connected() const { return connected_; } + int id() const { return id_; } + void set_disconnected() { connected_ = false; } + bool is_wait_request(DataSocket* ds) const; + const std::string& name() const { return name_; } + + bool TimedOut(); + + std::string GetPeerIdHeader() const; + + bool NotifyOfOtherMember(const ChannelMember& other); + + // Returns a string in the form "name,id\n". + std::string GetEntry() const; + + void ForwardRequestToPeer(DataSocket* ds, ChannelMember* peer); + + void OnClosing(DataSocket* ds); + + void QueueResponse(const std::string& status, const std::string& content_type, + const std::string& extra_headers, const std::string& data); + + void SetWaitingSocket(DataSocket* ds); + + protected: + struct QueuedResponse { + std::string status, content_type, extra_headers, data; + }; + + DataSocket* waiting_socket_; + int id_; + bool connected_; + time_t timestamp_; + std::string name_; + std::queue queue_; + static int s_member_id_; +}; + +// Manages all currently connected peers. +class PeerChannel { + public: + typedef std::vector Members; + + PeerChannel() { + } + + ~PeerChannel() { + DeleteAll(); + } + + const Members& members() const { return members_; } + + // Returns true if the request should be treated as a new ChannelMember + // request. Otherwise the request is not peerconnection related. + static bool IsPeerConnection(const DataSocket* ds); + + // Finds a connected peer that's associated with the |ds| socket. + ChannelMember* Lookup(DataSocket* ds) const; + + // Checks if the request has a "peer_id" parameter and if so, looks up the + // peer for which the request is targeted at. + ChannelMember* IsTargetedRequest(const DataSocket* ds) const; + + // Adds a new ChannelMember instance to the list of connected peers and + // associates it with the socket. + bool AddMember(DataSocket* ds); + + // Closes all connections and sends a "shutting down" message to all + // connected peers. + void CloseAll(); + + // Called when a socket was determined to be closing by the peer (or if the + // connection went dead). + void OnClosing(DataSocket* ds); + + void CheckForTimeout(); + + protected: + void DeleteAll(); + void BroadcastChangedState(const ChannelMember& member, + Members* delivery_failures); + void HandleDeliveryFailures(Members* failures); + + // Builds a simple list of "name,id\n" entries for each member. + std::string BuildResponseForNewMember(const ChannelMember& member, + std::string* content_type); + + protected: + Members members_; +}; + +#endif // TALK_EXAMPLES_PEERCONNECTION_SERVER_PEER_CHANNEL_H_ diff --git a/talk/examples/peerconnection/server/server_test.html b/talk/examples/peerconnection/server/server_test.html new file mode 100644 index 000000000..01559176b --- /dev/null +++ b/talk/examples/peerconnection/server/server_test.html @@ -0,0 +1,253 @@ + + +PeerConnection server test page + + + + + +Server:
+ Loopback (just send +received messages right back)
+Your name: + + +
+
+Target peer id: +Message: + +
+ + +
+
+

+ + diff --git a/talk/examples/peerconnection/server/utils.cc b/talk/examples/peerconnection/server/utils.cc new file mode 100644 index 000000000..25ec10c89 --- /dev/null +++ b/talk/examples/peerconnection/server/utils.cc @@ -0,0 +1,47 @@ +/* + * libjingle + * Copyright 2011, 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/examples/peerconnection/server/utils.h" + +#include + +std::string int2str(int i) { + char buffer[11] = {0}; + sprintf(buffer, "%d", i); // NOLINT + return buffer; +} + +std::string size_t2str(size_t i) { + char buffer[32] = {0}; +#ifdef WIN32 + // %zu isn't supported on Windows. + sprintf(buffer, "%Iu", i); // NOLINT +#else + sprintf(buffer, "%zu", i); // NOLINT +#endif + return buffer; +} diff --git a/talk/examples/peerconnection/server/utils.h b/talk/examples/peerconnection/server/utils.h new file mode 100644 index 000000000..d05a2c343 --- /dev/null +++ b/talk/examples/peerconnection/server/utils.h @@ -0,0 +1,53 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_EXAMPLES_PEERCONNECTION_SERVER_UTILS_H_ +#define TALK_EXAMPLES_PEERCONNECTION_SERVER_UTILS_H_ +#pragma once + +#ifndef assert +#ifndef WIN32 +#include +#else +#ifndef NDEBUG +#define assert(expr) ((void)((expr) ? true : __debugbreak())) +#else +#define assert(expr) ((void)0) +#endif // NDEBUG +#endif // WIN32 +#endif // assert + +#include + +#ifndef ARRAYSIZE +#define ARRAYSIZE(x) (sizeof(x) / sizeof(x[0])) +#endif + +std::string int2str(int i); +std::string size_t2str(size_t i); + +#endif // TALK_EXAMPLES_PEERCONNECTION_SERVER_UTILS_H_ diff --git a/talk/examples/plus/libjingleplus.cc b/talk/examples/plus/libjingleplus.cc new file mode 100644 index 000000000..4d27dfd12 --- /dev/null +++ b/talk/examples/plus/libjingleplus.cc @@ -0,0 +1,736 @@ +/* + * libjingle + * Copyright 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 +#include "libjingleplus.h" +#ifdef WIN32 +#include "talk/base/win32socketserver.h" +#endif +#include "talk/base/physicalsocketserver.h" +#include "talk/base/logging.h" +#include "talk/examples/login/xmppauth.h" +#include "talk/examples/login/xmppsocket.h" +#include "talk/examples/login/xmpppump.h" +#include "presencepushtask.h" +#include "talk/app/status.h" +#include "talk/app/message.h" +#include "rostertask.h" +#include "talk/app/iqtask.h" +#include "talk/app/presenceouttask.h" +#include "talk/app/receivemessagetask.h" +#include "talk/app/rostersettask.h" +#include "talk/app/sendmessagetask.h" + +enum { + MSG_START, + + // main thread to worker + MSG_LOGIN, + MSG_DISCONNECT, + MSG_SEND_PRESENCE, + MSG_SEND_DIRECTED_PRESENCE, + MSG_SEND_DIRECTED_MUC_PRESENCE, + MSG_SEND_XMPP_MESSAGE, + MSG_SEND_XMPP_IQ, + MSG_UPDATE_ROSTER_ITEM, + MSG_REMOVE_ROSTER_ITEM, + + // worker thread to main thread + MSG_STATE_CHANGE, + MSG_STATUS_UPDATE, + MSG_STATUS_ERROR, + MSG_ROSTER_REFRESH_STARTED, + MSG_ROSTER_REFRESH_FINISHED, + MSG_ROSTER_ITEM_UPDATED, + MSG_ROSTER_ITEM_REMOVED, + MSG_ROSTER_SUBSCRIBE, + MSG_ROSTER_UNSUBSCRIBE, + MSG_ROSTER_SUBSCRIBED, + MSG_ROSTER_UNSUBSCRIBED, + MSG_INCOMING_MESSAGE, + MSG_IQ_COMPLETE, + MSG_XMPP_INPUT, + MSG_XMPP_OUTPUT +}; + +class LibjinglePlusWorker : public talk_base::MessageHandler, + public XmppPumpNotify, + public sigslot::has_slots<> { + public: + LibjinglePlusWorker(LibjinglePlus *ljp, LibjinglePlusNotify *notify) : + worker_thread_(NULL), ljp_(ljp), notify_(notify), + ppt_(NULL), rmt_(NULL), rt_(NULL), is_test_login_(false) { + + main_thread_.reset(new talk_base::AutoThread()); +#ifdef WIN32 + ss_.reset(new talk_base::Win32SocketServer(main_thread_.get())); + main_thread_->set_socketserver(ss_.get()); +#endif + + pump_.reset(new XmppPump(this)); + + pump_->client()->SignalLogInput.connect(this, &LibjinglePlusWorker::OnInputDebug); + pump_->client()->SignalLogOutput.connect(this, &LibjinglePlusWorker::OnOutputDebug); + //pump_->client()->SignalStateChange.connect(this, &LibjinglePlusWorker::OnStateChange); + } + + ~LibjinglePlusWorker() { + if (worker_thread_) { + worker_thread_->Send(this, MSG_DISCONNECT); + delete worker_thread_; + } + } + + virtual void OnMessage(talk_base::Message *msg) { + switch (msg->message_id) { + case MSG_START: + LoginW(); + break; + case MSG_DISCONNECT: + DisconnectW(); + break; + case MSG_SEND_XMPP_MESSAGE: + SendXmppMessageW(static_cast(msg->pdata)->m_); + delete msg->pdata; + break; + case MSG_SEND_XMPP_IQ: + SendXmppIqW(static_cast(msg->pdata)->to_jid_, + static_cast(msg->pdata)->is_get_, + static_cast(msg->pdata)->xml_element_); + delete msg->pdata; + break; + case MSG_SEND_PRESENCE: + SendPresenceW(static_cast(msg->pdata)->s_); + delete msg->pdata; + break; + case MSG_SEND_DIRECTED_PRESENCE: + SendDirectedPresenceW(static_cast(msg->pdata)->j_, + static_cast(msg->pdata)->s_); + delete msg->pdata; + break; + case MSG_SEND_DIRECTED_MUC_PRESENCE: + SendDirectedMUCPresenceW(static_cast(msg->pdata)->j_, + static_cast(msg->pdata)->s_, + static_cast(msg->pdata)->un_, + static_cast(msg->pdata)->ac_, + static_cast(msg->pdata)->am_, + static_cast(msg->pdata)->role_); + delete msg->pdata; + break; + case MSG_UPDATE_ROSTER_ITEM: + UpdateRosterItemW(static_cast(msg->pdata)->jid_, + static_cast(msg->pdata)->n_, + static_cast(msg->pdata)->g_, + static_cast(msg->pdata)->grt_); + delete msg->pdata; + break; + case MSG_REMOVE_ROSTER_ITEM: + RemoveRosterItemW(static_cast(msg->pdata)->jid_); + delete msg->pdata; + break; + + + + + case MSG_STATUS_UPDATE: + OnStatusUpdateW(static_cast(msg->pdata)->s_); + delete msg->pdata; + break; + case MSG_STATUS_ERROR: + OnStatusErrorW(static_cast(msg->pdata)->stanza_); + delete msg->pdata; + break; + case MSG_STATE_CHANGE: + OnStateChangeW(static_cast(msg->pdata)->s_); + delete msg->pdata; + break; + case MSG_ROSTER_REFRESH_STARTED: + OnRosterRefreshStartedW(); + break; + case MSG_ROSTER_REFRESH_FINISHED: + OnRosterRefreshFinishedW(); + break; + case MSG_ROSTER_ITEM_UPDATED: + OnRosterItemUpdatedW(static_cast(msg->pdata)->ri_); + delete msg->pdata; + break; + case MSG_ROSTER_ITEM_REMOVED: + OnRosterItemRemovedW(static_cast(msg->pdata)->ri_); + delete msg->pdata; + break; + case MSG_ROSTER_SUBSCRIBE: + OnRosterSubscribeW(static_cast(msg->pdata)->jid_); + delete msg->pdata; + break; + case MSG_ROSTER_UNSUBSCRIBE: + OnRosterUnsubscribeW(static_cast(msg->pdata)->jid_); + delete msg->pdata; + break; + case MSG_ROSTER_SUBSCRIBED: + OnRosterSubscribedW(static_cast(msg->pdata)->jid_); + delete msg->pdata; + break; + case MSG_ROSTER_UNSUBSCRIBED: + OnRosterUnsubscribedW(static_cast(msg->pdata)->jid_); + delete msg->pdata; + break; + case MSG_INCOMING_MESSAGE: + OnIncomingMessageW(static_cast(msg->pdata)->m_); + delete msg->pdata; + break; + case MSG_IQ_COMPLETE: + OnIqCompleteW(static_cast(msg->pdata)->success_, + static_cast(msg->pdata)->stanza_); + delete msg->pdata; + break; + case MSG_XMPP_OUTPUT: + OnOutputDebugW(static_cast(msg->pdata)->s_); + delete msg->pdata; + break; + case MSG_XMPP_INPUT: + OnInputDebugW(static_cast(msg->pdata)->s_); + delete msg->pdata; + break; + } + } + + void Login(const std::string &jid, const std::string &password, + const std::string &machine_address, bool is_test, bool cookie_auth) { + is_test_login_ = is_test; + + xcs_.set_user(jid); + if (cookie_auth) { + xcs_.set_auth_cookie(password); + } else { + talk_base::InsecureCryptStringImpl pass; + pass.password() = password; + xcs_.set_pass(talk_base::CryptString(pass)); + } + xcs_.set_host(is_test ? "google.com" : "gmail.com"); + xcs_.set_resource("libjingleplus"); + xcs_.set_server(talk_base::SocketAddress(machine_address, 5222)); + xcs_.set_use_tls(!is_test); + if (is_test) { + xcs_.set_allow_plain(true); + } + + worker_thread_ = new talk_base::Thread(&pss_); + worker_thread_->Start(); + worker_thread_->Send(this, MSG_START); + } + + void SendXmppMessage(const buzz::XmppMessage &m) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + worker_thread_->Post(this, MSG_SEND_XMPP_MESSAGE, new SendMessageData(m)); + } + + void SendXmppIq(const buzz::Jid &to_jid, bool is_get, + const buzz::XmlElement *xml_element) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + worker_thread_->Post(this, MSG_SEND_XMPP_IQ, + new SendIqData(to_jid, is_get, xml_element)); + } + + void SendPresence(const buzz::Status & s) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + worker_thread_->Post(this, MSG_SEND_PRESENCE, new SendPresenceData(s)); + } + + void SendDirectedPresence (const buzz::Jid &j, const buzz::Status &s) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + worker_thread_->Post(this, MSG_SEND_DIRECTED_PRESENCE, new SendDirectedPresenceData(j,s)); + } + + void SendDirectedMUCPresence(const buzz::Jid &j, const buzz::Status &s, + const std::string &un, const std::string &ac, + const std::string &am, const std::string &role) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + worker_thread_->Post(this, MSG_SEND_DIRECTED_MUC_PRESENCE, new SendDirectedMUCPresenceData(j,s,un,ac,am, role)); + } + + void UpdateRosterItem(const buzz::Jid & jid, const std::string & name, + const std::vector & groups, buzz::GrType grt) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + worker_thread_->Post(this, MSG_UPDATE_ROSTER_ITEM, new UpdateRosterItemData(jid,name,groups,grt)); + } + + void RemoveRosterItemW(const buzz::Jid &jid) { + buzz::RosterSetTask *rst = new buzz::RosterSetTask(pump_.get()->client()); + rst->Remove(jid); + rst->Start(); + } + + void RemoveRosterItem(const buzz::Jid &jid) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + worker_thread_->Post(this, MSG_REMOVE_ROSTER_ITEM, new JidData(jid)); + } + + void DoCallbacks() { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + talk_base::Message m; + while (main_thread_->Get(&m, 0)) { + main_thread_->Dispatch(&m); + } + } + + private: + + struct UpdateRosterItemData : public talk_base::MessageData { + UpdateRosterItemData(const buzz::Jid &jid, const std::string &name, + const std::vector &groups, buzz::GrType grt) : + jid_(jid), n_(name), g_(groups), grt_(grt) {} + buzz::Jid jid_; + std::string n_; + std::vector g_; + buzz::GrType grt_; + }; + + void UpdateRosterItemW(const buzz::Jid &jid, const std::string &name, + const std::vector &groups, buzz::GrType grt) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + buzz::RosterSetTask *rst = new buzz::RosterSetTask(pump_.get()->client()); + rst->Update(jid, name, groups, grt); + rst->Start(); + } + + struct StringData : public talk_base::MessageData { + StringData(std::string s) : s_(s) {} + std::string s_; + }; + + void OnInputDebugW(const std::string &data) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnXmppInput(data); + } + + void OnInputDebug(const char *data, int len) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_XMPP_INPUT, new StringData(std::string(data,len))); + if (notify_) + notify_->WakeupMainThread(); + } + + void OnOutputDebugW(const std::string &data) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnXmppOutput(data); + } + + void OnOutputDebug(const char *data, int len) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_XMPP_OUTPUT, new StringData(std::string(data,len))); + if (notify_) + notify_->WakeupMainThread(); + } + + struct StateChangeData : public talk_base::MessageData { + StateChangeData(buzz::XmppEngine::State state) : s_(state) {} + buzz::XmppEngine::State s_; + }; + + void OnStateChange(buzz::XmppEngine::State state) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + switch (state) { + case buzz::XmppEngine::STATE_OPEN: + ppt_ = new buzz::PresencePushTask(pump_.get()->client()); + ppt_->SignalStatusUpdate.connect(this, + &LibjinglePlusWorker::OnStatusUpdate); + ppt_->SignalStatusError.connect(this, + &LibjinglePlusWorker::OnStatusError); + ppt_->Start(); + + rmt_ = new buzz::ReceiveMessageTask(pump_.get()->client(), buzz::XmppEngine::HL_ALL); + rmt_->SignalIncomingMessage.connect(this, &LibjinglePlusWorker::OnIncomingMessage); + rmt_->Start(); + + rt_ = new buzz::RosterTask(pump_.get()->client()); + rt_->SignalRosterItemUpdated.connect(this, &LibjinglePlusWorker::OnRosterItemUpdated); + rt_->SignalRosterItemRemoved.connect(this, &LibjinglePlusWorker::OnRosterItemRemoved); + rt_->SignalSubscribe.connect(this, &LibjinglePlusWorker::OnRosterSubscribe); + rt_->SignalUnsubscribe.connect(this, &LibjinglePlusWorker::OnRosterUnsubscribe); + rt_->SignalSubscribed.connect(this, &LibjinglePlusWorker::OnRosterSubscribed); + rt_->SignalUnsubscribed.connect(this, &LibjinglePlusWorker::OnRosterUnsubscribed); + rt_->SignalRosterRefreshStarted.connect(this, &LibjinglePlusWorker::OnRosterRefreshStarted); + rt_->SignalRosterRefreshFinished.connect(this, &LibjinglePlusWorker::OnRosterRefreshFinished); + rt_->Start(); + rt_->RefreshRosterNow(); + + break; + } + main_thread_->Post(this, MSG_STATE_CHANGE, new StateChangeData(state)); + if (notify_) + notify_->WakeupMainThread(); + } + + void OnStateChangeW(buzz::XmppEngine::State state) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnStateChange(state); + } + + struct RosterItemData : public talk_base::MessageData { + RosterItemData(const buzz::RosterItem &ri) : ri_(ri) {} + buzz::RosterItem ri_; + }; + + void OnRosterItemUpdatedW(const buzz::RosterItem &ri) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnRosterItemUpdated(ri); + } + + void OnRosterItemUpdated(const buzz::RosterItem &ri, bool huh) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_ROSTER_ITEM_UPDATED, new RosterItemData(ri)); + if (notify_) + notify_->WakeupMainThread(); + } + + void OnRosterItemRemovedW(const buzz::RosterItem &ri) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnRosterItemRemoved(ri); + } + + void OnRosterItemRemoved(const buzz::RosterItem &ri) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_ROSTER_ITEM_REMOVED, new RosterItemData(ri)); + if (notify_) + notify_->WakeupMainThread(); + } + + struct JidData : public talk_base::MessageData { + JidData(const buzz::Jid& jid) : jid_(jid) {} + const buzz::Jid jid_; + }; + + void OnRosterSubscribeW(const buzz::Jid& jid) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnRosterSubscribe(jid); + } + + void OnRosterSubscribe(const buzz::Jid& jid) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_ROSTER_SUBSCRIBE, new JidData(jid)); + if (notify_) + notify_->WakeupMainThread(); + } + + void OnRosterUnsubscribeW(const buzz::Jid &jid) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnRosterUnsubscribe(jid); + } + + void OnRosterUnsubscribe(const buzz::Jid &jid) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_ROSTER_UNSUBSCRIBE, new JidData(jid)); + if (notify_) + notify_->WakeupMainThread(); + } + + void OnRosterSubscribedW(const buzz::Jid &jid) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnRosterSubscribed(jid); + } + + void OnRosterSubscribed(const buzz::Jid &jid) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_ROSTER_SUBSCRIBED, new JidData(jid)); + if (notify_) + notify_->WakeupMainThread(); + } + + void OnRosterUnsubscribedW(const buzz::Jid &jid) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnRosterUnsubscribed(jid); + } + + void OnRosterUnsubscribed(const buzz::Jid &jid) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_ROSTER_UNSUBSCRIBED, new JidData(jid)); + if (notify_) + notify_->WakeupMainThread(); + } + + void OnRosterRefreshStartedW() { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnRosterRefreshStarted(); + } + + void OnRosterRefreshStarted() { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_ROSTER_REFRESH_STARTED); + if (notify_) + notify_->WakeupMainThread(); + } + + void OnRosterRefreshFinishedW() { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnRosterRefreshFinished(); + } + + void OnRosterRefreshFinished() { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_ROSTER_REFRESH_FINISHED); + if (notify_) + notify_->WakeupMainThread(); + } + + struct XmppMessageData : talk_base::MessageData { + XmppMessageData(const buzz::XmppMessage &m) : m_(m) {} + buzz::XmppMessage m_; + }; + + void OnIncomingMessageW(const buzz::XmppMessage &msg) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnMessage(msg); + } + + void OnIncomingMessage(const buzz::XmppMessage &msg) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_INCOMING_MESSAGE, new XmppMessageData(msg)); + if (notify_) + notify_->WakeupMainThread(); + } + + void OnStatusUpdateW (const buzz::Status &status) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnStatusUpdate(status); + } + + void OnStatusUpdate (const buzz::Status &status) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_STATUS_UPDATE, new SendPresenceData(status)); + if (notify_) + notify_->WakeupMainThread(); + } + + struct StatusErrorData : talk_base::MessageData { + StatusErrorData(const buzz::XmlElement &stanza) : stanza_(stanza) {} + buzz::XmlElement stanza_; + }; + + void OnStatusErrorW (const buzz::XmlElement &stanza) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnStatusError(stanza); + } + + void OnStatusError (const buzz::XmlElement &stanza) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_STATUS_ERROR, new StatusErrorData(stanza)); + if (notify_) + notify_->WakeupMainThread(); + } + + void LoginW() { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + XmppSocket* socket = new XmppSocket(true); + pump_->DoLogin(xcs_, socket, is_test_login_ ? NULL : new XmppAuth()); + socket->SignalCloseEvent.connect(this, + &LibjinglePlusWorker::OnXmppSocketClose); + } + + void DisconnectW() { + assert(talk_base::ThreadManager::CurrentThread() == worker_thread_); + pump_->DoDisconnect(); + } + + void SendXmppMessageW(const buzz::XmppMessage &m) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + buzz::SendMessageTask * smt = new buzz::SendMessageTask(pump_.get()->client()); + smt->Send(m); + smt->Start(); + } + + void SendXmppIqW(const buzz::Jid &to_jid, bool is_get, + const buzz::XmlElement *xml_element) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + buzz::IqTask *iq_task = new buzz::IqTask(pump_.get()->client(), + is_get, to_jid, const_cast(xml_element)); + iq_task->SignalDone.connect(this, &LibjinglePlusWorker::OnIqComplete); + iq_task->Start(); + } + + struct IqCompleteData : public talk_base::MessageData { + IqCompleteData(bool success, const buzz::XmlElement *stanza) : + success_(success), stanza_(*stanza) {} + bool success_; + buzz::XmlElement stanza_; + }; + + void OnIqCompleteW(bool success, const buzz::XmlElement& stanza) { + assert(talk_base::ThreadManager::CurrentThread() != worker_thread_); + if (notify_) + notify_->OnIqDone(success, stanza); + } + + void OnIqComplete(bool success, const buzz::XmlElement *stanza) { + assert(talk_base::ThreadManager::CurrentThread() == worker_thread_); + main_thread_->Post(this, MSG_IQ_COMPLETE, + new IqCompleteData(success, stanza)); + if (notify_) + notify_->WakeupMainThread(); + } + + void SendPresenceW(const buzz::Status & s) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + buzz::PresenceOutTask *pot = new buzz::PresenceOutTask(pump_.get()->client()); + pot->Send(s); + pot->Start(); + } + + + void SendDirectedMUCPresenceW(const buzz::Jid & j, const buzz::Status & s, + const std::string &user_nick, const std::string &api_capability, + const std::string &api_message, const std::string &role) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + buzz::PresenceOutTask *pot = new buzz::PresenceOutTask(pump_.get()->client()); + pot->SendDirectedMUC(j,s,user_nick,api_capability,api_message, role); + pot->Start(); + } + + void SendDirectedPresenceW(const buzz::Jid & j, const buzz::Status & s) { + assert (talk_base::ThreadManager::CurrentThread() == worker_thread_); + buzz::PresenceOutTask *pot = new buzz::PresenceOutTask(pump_.get()->client()); + pot->SendDirected(j,s); + pot->Start(); + } + + void OnXmppSocketClose(int error) { + notify_->OnSocketClose(error); + } + + struct SendMessageData : public talk_base::MessageData { + SendMessageData(const buzz::XmppMessage &m) : m_(m) {} + buzz::XmppMessage m_; + }; + + struct SendIqData : public talk_base::MessageData { + SendIqData(const buzz::Jid &jid, bool is_get, const buzz::XmlElement *m) + : to_jid_(jid), is_get_(is_get), xml_element_(m) {} + buzz::Jid to_jid_; + bool is_get_; + const buzz::XmlElement *xml_element_; + }; + + struct SendPresenceData : public talk_base::MessageData { + SendPresenceData(const buzz::Status &s) : s_(s) {} + buzz::Status s_; + }; + + struct SendDirectedPresenceData : public talk_base::MessageData { + SendDirectedPresenceData(const buzz::Jid &j, const buzz::Status &s) : j_(j), s_(s) {} + buzz::Jid j_; + buzz::Status s_; + }; + + struct SendDirectedMUCPresenceData : public talk_base::MessageData { + SendDirectedMUCPresenceData(const buzz::Jid &j, const buzz::Status &s, + const std::string &un, const std::string &ac, + const std::string &am, const std::string &role) + : j_(j), s_(s), un_(un), ac_(ac), am_(am), role_(role) {} + buzz::Jid j_; + buzz::Status s_; + std::string un_; + std::string ac_; + std::string am_; + std::string role_; + }; + + talk_base::scoped_ptr ss_; + talk_base::scoped_ptr main_thread_; + talk_base::Thread *worker_thread_; + + LibjinglePlus *ljp_; + LibjinglePlusNotify *notify_; + buzz::XmppClientSettings xcs_; + talk_base::PhysicalSocketServer pss_; + + talk_base::scoped_ptr pump_; + buzz::PresencePushTask * ppt_; + buzz::ReceiveMessageTask * rmt_; + buzz::RosterTask * rt_; + + bool is_test_login_; +}; + +LibjinglePlus::LibjinglePlus(LibjinglePlusNotify *notify) +{ + worker_ = new LibjinglePlusWorker(this, notify); +} + +LibjinglePlus::~LibjinglePlus() +{ + delete worker_; + worker_ = NULL; +} + +void LibjinglePlus::Login(const std::string &jid, + const std::string &password, + const std::string &machine_address, + bool is_test, bool cookie_auth) { + worker_->Login(jid, password, machine_address, is_test, cookie_auth); +} + +void LibjinglePlus::SendPresence(const buzz::Status & s) { + worker_->SendPresence(s); +} + +void LibjinglePlus::SendDirectedPresence(const buzz::Jid & j, const buzz::Status & s) { + worker_->SendDirectedPresence(j,s); +} + +void LibjinglePlus::SendDirectedMUCPresence(const buzz::Jid & j, + const buzz::Status & s, const std::string &user_nick, + const std::string &api_capability, const std::string &api_message, + const std::string &role) { + worker_->SendDirectedMUCPresence(j,s,user_nick,api_capability,api_message, + role); +} + +void LibjinglePlus::SendXmppMessage(const buzz::XmppMessage & m) { + worker_->SendXmppMessage(m); +} + +void LibjinglePlus::SendXmppIq(const buzz::Jid &to_jid, bool is_get, + const buzz::XmlElement *iq_element) { + worker_->SendXmppIq(to_jid, is_get, iq_element); +} + +void LibjinglePlus::DoCallbacks() { + worker_->DoCallbacks(); +} diff --git a/talk/examples/plus/libjingleplus.h b/talk/examples/plus/libjingleplus.h new file mode 100644 index 000000000..a2898f51a --- /dev/null +++ b/talk/examples/plus/libjingleplus.h @@ -0,0 +1,154 @@ +/* + * libjingle + * Copyright 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. + */ + +// LibjinglePlus is a class that connects to Google Talk, creates +// some common tasks, and emits signals when things change + +#ifndef LIBJINGLEPLUS_H__ +#define LIBJINGLEPLUS_H__ + +#include "talk/base/basicdefs.h" +#include "talk/app/rosteritem.h" +#include "talk/app/message.h" +#include "talk/app/status.h" +#include "talk/xmpp/xmppengine.h" +#include "talk/base/scoped_ptr.h" + + +class LibjinglePlusWorker; + +class LibjinglePlusNotify { + public: + virtual ~LibjinglePlusNotify() {} + + /* Libjingle+ works on its own thread. It will call WakeupMainThread + * when it has something to report. The main thread should then wake up, + * and call DoCallbacks on the LibjinglePlus object. + * + * This function gets called from libjingle+'s worker thread. All other + * methods in LibjinglePlusNotify get called from the thread you call + * DoCallbacks() on. + * + * If running on Windows, libjingle+ will use Windows messages to generate + * callbacks from the main thread, and you don't need to do anything here. + */ + virtual void WakeupMainThread() = 0; + + /* Connection */ + /* Called when the connection state changes */ + virtual void OnStateChange(buzz::XmppEngine::State) = 0; + + /* Called when the socket closes */ + virtual void OnSocketClose(int error_code) = 0; + + /* Called when XMPP is being sent or received. Used for debugging */ + virtual void OnXmppOutput(const std::string &output) = 0; + virtual void OnXmppInput(const std::string &input) = 0; + + /* Presence */ + /* Called when someone's Status is updated */ + virtual void OnStatusUpdate(const buzz::Status &status) = 0; + + /* Called when a status update results in an error */ + virtual void OnStatusError(const buzz::XmlElement &stanza) = 0; + + /* Called with an IQ return code */ + virtual void OnIqDone(bool success, const buzz::XmlElement &stanza) = 0; + + /* Message */ + /* Called when a message comes in. */ + virtual void OnMessage(const buzz::XmppMessage &message) = 0; + + /* Roster */ + + /* Called when we start refreshing the roster */ + virtual void OnRosterRefreshStarted() = 0; + /* Called when we have the entire roster */ + virtual void OnRosterRefreshFinished() = 0; + /* Called when an item on the roster is created or updated */ + virtual void OnRosterItemUpdated(const buzz::RosterItem &ri) = 0; + /* Called when an item on the roster is removed */ + virtual void OnRosterItemRemoved(const buzz::RosterItem &ri) = 0; + + /* Subscriptions */ + virtual void OnRosterSubscribe(const buzz::Jid &jid) = 0; + virtual void OnRosterUnsubscribe(const buzz::Jid &jid) = 0; + virtual void OnRosterSubscribed(const buzz::Jid &jid) = 0; + virtual void OnRosterUnsubscribed(const buzz::Jid &jid) = 0; + +}; + +class LibjinglePlus +{ + public: + /* Provide the constructor with your interface. */ + LibjinglePlus(LibjinglePlusNotify *notify); + ~LibjinglePlus(); + + /* Logs in and starts doing stuff + * + * If cookie_auth is true, password must be a Gaia SID. Otherwise, + * it should be the user's password + */ + void Login(const std::string &username, const std::string &password, + const std::string &machine_address, bool is_test, bool cookie_auth); + + /* Set Presence */ + void SendPresence(const buzz::Status & s); + void SendDirectedPresence(const buzz::Jid & j, const buzz::Status & s); + void SendDirectedMUCPresence(const buzz::Jid & j, const buzz::Status & s, + const std::string &user_nick, const std::string &api_capability, + const std::string &api_message, const std::string &role); + + /* Send Message */ + void SendXmppMessage(const buzz::XmppMessage & m); + + /* Send IQ */ + void SendXmppIq(const buzz::Jid &to_jid, bool is_get, + const buzz::XmlElement *iq_element); + + /* Set Roster */ + void UpdateRosterItem(const buzz::Jid & jid, const std::string & name, + const std::vector & groups, buzz::GrType grt); + void RemoveRosterItem(const buzz::Jid &jid); + + /* Call this from the thread you want to receive callbacks on. Typically, this will be called + * after your WakeupMainThread() notify function is called. + * + * On Windows, libjingle+ will trigger its callback from the Windows message loop, and + * you needn't call this yourself. + */ + void DoCallbacks(); + + private: + void LoginInternal(const std::string &jid, const std::string &password, + const std::string &machine_address, bool is_test); + + LibjinglePlusWorker *worker_; +}; + +#endif // LIBJINGLE_PLUS_H__ diff --git a/talk/examples/plus/presencepushtask.cc b/talk/examples/plus/presencepushtask.cc new file mode 100644 index 000000000..9d5ed28db --- /dev/null +++ b/talk/examples/plus/presencepushtask.cc @@ -0,0 +1,201 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/stringencode.h" +#include "presencepushtask.h" +#include "talk/xmpp/constants.h" +#include + + +namespace buzz { + +// string helper functions ----------------------------------------------------- + +static bool +IsXmlSpace(int ch) { + return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t'; +} + +static bool +ListContainsToken(const std::string & list, const std::string & token) { + size_t i = list.find(token); + if (i == std::string::npos || token.empty()) + return false; + bool boundary_before = (i == 0 || IsXmlSpace(list[i - 1])); + bool boundary_after = (i == list.length() - token.length() || IsXmlSpace(list[i + token.length()])); + return boundary_before && boundary_after; +} + + +bool +PresencePushTask::HandleStanza(const XmlElement * stanza) { + if (stanza->Name() != QN_PRESENCE) + return false; + if (stanza->HasAttr(QN_TYPE) && stanza->Attr(QN_TYPE) != STR_UNAVAILABLE) { + if (stanza->Attr(QN_TYPE) == STR_ERROR) { + // Pass on the error. + const XmlElement* error_xml_elem = stanza->FirstNamed(QN_ERROR); + if (!error_xml_elem) { + return false; + } + SignalStatusError(*error_xml_elem); + return true; + } + } + QueueStanza(stanza); + return true; +} + +static bool IsUtf8FirstByte(int c) { + return (((c)&0x80)==0) || // is single byte + ((unsigned char)((c)-0xc0)<0x3e); // or is lead byte +} + +int +PresencePushTask::ProcessStart() { + const XmlElement * stanza = NextStanza(); + if (stanza == NULL) + return STATE_BLOCKED; + Status s; + + s.set_jid(Jid(stanza->Attr(QN_FROM))); + + if (stanza->Attr(QN_TYPE) == STR_UNAVAILABLE) { + s.set_available(false); + SignalStatusUpdate(s); + } + else { + s.set_available(true); + const XmlElement * status = stanza->FirstNamed(QN_STATUS); + if (status != NULL) { + s.set_status(status->BodyText()); + + // Truncate status messages longer than 300 bytes + if (s.status().length() > 300) { + size_t len = 300; + + // Be careful not to split legal utf-8 chars in half + while (!IsUtf8FirstByte(s.status()[len]) && len > 0) { + len -= 1; + } + std::string truncated(s.status(), 0, len); + s.set_status(truncated); + } + } + + const XmlElement * priority = stanza->FirstNamed(QN_PRIORITY); + if (priority != NULL) { + int pri; + if (talk_base::FromString(priority->BodyText(), &pri)) { + s.set_priority(pri); + } + } + + const XmlElement * show = stanza->FirstNamed(QN_SHOW); + if (show == NULL || show->FirstChild() == NULL) { + s.set_show(Status::SHOW_ONLINE); + } + else { + if (show->BodyText() == "away") { + s.set_show(Status::SHOW_AWAY); + } + else if (show->BodyText() == "xa") { + s.set_show(Status::SHOW_XA); + } + else if (show->BodyText() == "dnd") { + s.set_show(Status::SHOW_DND); + } + else if (show->BodyText() == "chat") { + s.set_show(Status::SHOW_CHAT); + } + else { + s.set_show(Status::SHOW_ONLINE); + } + } + + const XmlElement * caps = stanza->FirstNamed(QN_CAPS_C); + if (caps != NULL) { + std::string node = caps->Attr(QN_NODE); + std::string ver = caps->Attr(QN_VER); + std::string exts = caps->Attr(QN_EXT); + + s.set_know_capabilities(true); + std::string capability; + std::stringstream ss(exts); + while (ss >> capability) { + s.AddCapability(capability); + } + + s->set_caps_node(node); + s->set_version(ver); + } + + const XmlElement* delay = stanza->FirstNamed(kQnDelayX); + if (delay != NULL) { + // Ideally we would parse this according to the Psuedo ISO-8601 rules + // that are laid out in JEP-0082: + // http://www.jabber.org/jeps/jep-0082.html + std::string stamp = delay->Attr(kQnStamp); + s.set_sent_time(stamp); + } + + const XmlElement *nick = stanza->FirstNamed(kQnNickname); + if (nick) { + std::string user_nick = nick->BodyText(); + s.set_user_nick(user_nick); + } + + const XmlElement *plugin = stanza->FirstNamed(QN_PLUGIN); + if (plugin) { + const XmlElement *api_cap = plugin->FirstNamed(QN_CAPABILITY); + if (api_cap) { + const std::string &api_capability = api_cap->BodyText(); + s.set_api_capability(api_capability); + } + const XmlElement *api_msg = plugin->FirstNamed(QN_DATA); + if (api_msg) { + const std::string &api_message = api_msg->BodyText(); + s.set_api_message(api_message); + } + } + + const XmlElement* data_x = stanza->FirstNamed(QN_MUC_USER_X); + if (data_x != NULL) { + const XmlElement* item = data_x->FirstNamed(QN_MUC_USER_ITEM); + if (item != NULL) { + s.set_muc_role(item->Attr(QN_ROLE)); + } + } + + SignalStatusUpdate(s); + } + + return STATE_START; +} + + +} diff --git a/talk/examples/plus/presencepushtask.h b/talk/examples/plus/presencepushtask.h new file mode 100644 index 000000000..b9c8038b3 --- /dev/null +++ b/talk/examples/plus/presencepushtask.h @@ -0,0 +1,53 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _PRESENCEPUSHTASK_H_ +#define _PRESENCEPUSHTASK_H_ + +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmpptask.h" +#include "talk/base/sigslot.h" +#include "talk/app/status.h" + +namespace buzz { + +class PresencePushTask : public XmppTask { + +public: + PresencePushTask(Task * parent) : XmppTask(parent, XmppEngine::HL_TYPE) {} + virtual int ProcessStart(); + sigslot::signal1SignalStatusUpdate; + sigslot::signal1 SignalStatusError; + +protected: + virtual bool HandleStanza(const XmlElement * stanza); +}; + + +} + +#endif diff --git a/talk/examples/plus/rostertask.cc b/talk/examples/plus/rostertask.cc new file mode 100644 index 000000000..1344d0831 --- /dev/null +++ b/talk/examples/plus/rostertask.cc @@ -0,0 +1,218 @@ +/* + * libjingle + * Copyright 2004--2005, 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 "rostertask.h" +#include "talk/xmpp/constants.h" +#include "talk/base/stream.h" + +#undef WIN32 +#ifdef WIN32 +#include "talk/app/win32/offlineroster.h" +#endif + +namespace buzz { + +class RosterTask::RosterGetTask : public XmppTask { +public: + RosterGetTask(Task * parent) : XmppTask(parent, XmppEngine::HL_SINGLE), + done_(false) {} + + virtual int ProcessStart(); + virtual int ProcessResponse(); + +protected: + virtual bool HandleStanza(const XmlElement * stanza); + + bool done_; +}; + +//============================================================================== +// RosterTask +//============================================================================== +void RosterTask::RefreshRosterNow() { + RosterGetTask* get_task = new RosterGetTask(this); + ResumeTimeout(); + get_task->Start(); +} + +void RosterTask::TranslateItems(const XmlElement * rosterQueryResult) { +#if defined(FEATURE_ENABLE_PSTN) +#ifdef WIN32 + // We build up a list of contacts which have had information persisted offline. + // we'll remove items from this list if we get a buzz::SUBSCRIBE_REMOVE + // subscription. After updating all the items from the server, we'll then + // update (and merge) any roster items left in our map of offline items + XmlElement *el_local = OfflineRoster::RetrieveOfflineRoster(GetClient()->jid()); + std::map jid_to_item; + if (el_local) { + for (XmlElement *el_item = el_local->FirstNamed(QN_ROSTER_ITEM); + el_item != NULL; + el_item = el_item->NextNamed(QN_ROSTER_ITEM)) { + RosterItem roster_item; + roster_item.FromXml(el_item); + + jid_to_item[roster_item.jid()] = roster_item; + } + } +#endif // WIN32 +#endif // FEATURE_ENABLE_PSTN + + const XmlElement * xml_item; + for (xml_item = rosterQueryResult->FirstNamed(QN_ROSTER_ITEM); + xml_item != NULL; xml_item = xml_item->NextNamed(QN_ROSTER_ITEM)) { + RosterItem roster_item; + roster_item.FromXml(xml_item); + + if (roster_item.subscription() == buzz::SUBSCRIBE_REMOVE) { + SignalRosterItemRemoved(roster_item); + +#if defined(FEATURE_ENABLE_PSTN) +#ifdef WIN32 + std::map::iterator it = + jid_to_item.find(roster_item.jid()); + + if (it != jid_to_item.end()) + jid_to_item.erase(it); +#endif +#endif + } else { + SignalRosterItemUpdated(roster_item, false); + } + } + +#if defined(FEATURE_ENABLE_PSTN) +#ifdef WIN32 + for (std::map::iterator it = jid_to_item.begin(); + it != jid_to_item.end(); ++it) { + SignalRosterItemUpdated(it->second, true); + } +#endif +#endif +} + +int RosterTask::ProcessStart() { + const XmlElement * stanza = NextStanza(); + if (stanza == NULL) + return STATE_BLOCKED; + + if (stanza->Name() == QN_IQ) { + SuspendTimeout(); + bool result = (stanza->Attr(QN_TYPE) == STR_RESULT); + if (result) + SignalRosterRefreshStarted(); + + TranslateItems(stanza->FirstNamed(QN_ROSTER_QUERY)); + + if (result) + SignalRosterRefreshFinished(); + } else if (stanza->Name() == QN_PRESENCE) { + Jid jid(stanza->Attr(QN_FROM)); + std::string type = stanza->Attr(QN_TYPE); + if (type == "subscribe") + SignalSubscribe(jid); + else if (type == "unsubscribe") + SignalUnsubscribe(jid); + else if (type == "subscribed") + SignalSubscribed(jid); + else if (type == "unsubscribed") + SignalUnsubscribed(jid); + } + + return STATE_START; +} + +bool RosterTask::HandleStanza(const XmlElement * stanza) { + if (!MatchRequestIq(stanza, STR_SET, QN_ROSTER_QUERY)) { + // Not a roster IQ. Look for a presence instead + if (stanza->Name() != QN_PRESENCE) + return false; + if (!stanza->HasAttr(QN_TYPE)) + return false; + std::string type = stanza->Attr(QN_TYPE); + if (type == "subscribe" || type == "unsubscribe" || + type == "subscribed" || type == "unsubscribed") { + QueueStanza(stanza); + return true; + } + return false; + } + + // only respect roster push from the server + Jid from(stanza->Attr(QN_FROM)); + if (from != JID_EMPTY && + !from.BareEquals(GetClient()->jid()) && + from != Jid(GetClient()->jid().domain())) + return false; + + XmlElement * result = MakeIqResult(stanza); + result->AddElement(new XmlElement(QN_ROSTER_QUERY, true)); + SendStanza(result); + + QueueStanza(stanza); + return true; +} + + +//============================================================================== +// RosterTask::RosterGetTask +//============================================================================== +int RosterTask::RosterGetTask::ProcessStart() { + talk_base::scoped_ptr get(MakeIq(STR_GET, JID_EMPTY, task_id())); + get->AddElement(new XmlElement(QN_ROSTER_QUERY, true)); + get->AddAttr(QN_XMLNS_GR, NS_GR, 1); + get->AddAttr(QN_GR_EXT, "2", 1); + get->AddAttr(QN_GR_INCLUDE, "all", 1); + if (SendStanza(get.get()) != XMPP_RETURN_OK) { + return STATE_ERROR; + } + return STATE_RESPONSE; +} + +int RosterTask::RosterGetTask::ProcessResponse() { + if (done_) + return STATE_DONE; + return STATE_BLOCKED; +} + +bool RosterTask::RosterGetTask::HandleStanza(const XmlElement * stanza) { + if (!MatchResponseIq(stanza, JID_EMPTY, task_id())) + return false; + + if (stanza->Attr(QN_TYPE) != STR_RESULT) + return false; + + // Queue the stanza with the parent so these don't get handled out of order + RosterTask* parent = static_cast(GetParent()); + parent->QueueStanza(stanza); + + // Wake ourselves so we can go into the done state + done_ = true; + Wake(); + return true; +} + +} diff --git a/talk/examples/plus/rostertask.h b/talk/examples/plus/rostertask.h new file mode 100644 index 000000000..beab1f9a5 --- /dev/null +++ b/talk/examples/plus/rostertask.h @@ -0,0 +1,72 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _PHONE_CLIENT_ROSTERTASK_H_ +#define _PHONE_CLIENT_ROSTERTASK_H_ + +#include "talk/xmpp/xmppclient.h" +#include "talk/xmpp/xmpptask.h" +#include "talk/app/rosteritem.h" +#include "talk/base/sigslot.h" + +namespace buzz { + +class RosterTask : public XmppTask { +public: + RosterTask(Task * parent) : + XmppTask(parent, XmppEngine::HL_TYPE) {} + + // Roster items removed or updated. This can come from a push or a get + sigslot::signal2 SignalRosterItemUpdated; + sigslot::signal1 SignalRosterItemRemoved; + + // Subscription messages + sigslot::signal1 SignalSubscribe; + sigslot::signal1 SignalUnsubscribe; + sigslot::signal1 SignalSubscribed; + sigslot::signal1 SignalUnsubscribed; + + // Roster get + void RefreshRosterNow(); + sigslot::signal0<> SignalRosterRefreshStarted; + sigslot::signal0<> SignalRosterRefreshFinished; + + virtual int ProcessStart(); + +protected: + void TranslateItems(const XmlElement *rosterQueryResult); + + virtual bool HandleStanza(const XmlElement * stanza); + + // Inner class for doing the roster get + class RosterGetTask; + friend class RosterGetTask; +}; + +} + +#endif // _PHONE_CLIENT_ROSTERTASK_H_ diff --git a/talk/examples/plus/testutil/libjingleplus_main.cc b/talk/examples/plus/testutil/libjingleplus_main.cc new file mode 100644 index 000000000..b5a0686ba --- /dev/null +++ b/talk/examples/plus/testutil/libjingleplus_main.cc @@ -0,0 +1,119 @@ +/* + * libjingle + * Copyright 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 +#include + +#include "talk/base/thread.h" +#include "talk/libjingle-plus/libjingleplus.h" +#include "talk/libjingle-plus/testutil/libjingleplus_test_notifier.h" + +#if defined(_MSC_VER) && (_MSC_VER < 1400) +void __cdecl std::_Throw(const std::exception &) {} +std::_Prhand std::_Raise_handler =0; +#endif + + +void SetConsoleEcho(bool on) { +#ifdef WIN32 + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + if ((hIn == INVALID_HANDLE_VALUE) || (hIn == NULL)) + return; + + DWORD mode; + if (!GetConsoleMode(hIn, &mode)) + return; + + if (on) { + mode = mode | ENABLE_ECHO_INPUT; + } else { + mode = mode & ~ENABLE_ECHO_INPUT; + } + + SetConsoleMode(hIn, mode); +#else + if (on) + system("stty echo"); + else + system("stty -echo"); +#endif +} + +int main (int argc, char **argv) +{ + std::string username; + std::string password; + + bool gaia = false; + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "-gaia")) + gaia = true; + } + + std::cout << "Username: "; + std::cin >> username; + std::cout << (gaia ? "Gaia cookie: " : "Password: "); + SetConsoleEcho(false); + std::cin >> password; + SetConsoleEcho(true); + + // Create a LibjinglePlus object and give it the notifier interface + LibjinglePlus ljp(new Notifier); + + // Login + ljp.Login(username, password, "talk.google.com", false, gaia); + + buzz::Status s; + s.set_available(true); + s.set_show(buzz::Status::SHOW_ONLINE); + s.set_status("I'm online."); + + buzz::XmppMessage m; + m.set_to(buzz::Jid(username + "@gmail.com")); + m.set_body("What's up?"); + + // Typically, you would wait for WakeupMainThread to be called, and then call + // DoCallbacks. Because I have nothing else to do on the main thread, I'm just going + // to do a few things after 10 seconds and then poll every 2ms. + Sleep(10000); + // ljp.DoCallbacks(); + ljp.SendPresence(s); + ljp.SendXmppMessage(m); + +#ifdef WIN32 + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + DispatchMessage(&msg); + } +#else + for (;;) { + ljp.DoCallbacks(); + Sleep(2); + } +#endif +} diff --git a/talk/examples/plus/testutil/libjingleplus_test_notifier.h b/talk/examples/plus/testutil/libjingleplus_test_notifier.h new file mode 100644 index 000000000..3a1af552a --- /dev/null +++ b/talk/examples/plus/testutil/libjingleplus_test_notifier.h @@ -0,0 +1,101 @@ +/* + * libjingle + * Copyright 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 +#include + +#include "talk/libjingle-plus/libjingleplus.h" + +class Notifier : virtual public LibjinglePlusNotify { + virtual void OnStateChange(buzz::XmppEngine::State state) { + std::cout << "State change: " << state << std::endl; + } + + virtual void OnSocketClose(int error_code) { + std::cout << "Socket close: " << error_code << std::endl; + } + + virtual void OnXmppOutput(const std::string &output) { + std::cout << ">>>>>>>>" << std::endl << output << std::endl << ">>>>>>>>" << std::endl; + } + + virtual void OnXmppInput(const std::string &input) { + std::cout << "<<<<<<<<" << std::endl << input << std::endl << "<<<<<<<<" << std::endl; + } + + + virtual void OnStatusUpdate(const buzz::Status &status) { + std::string from = status.jid().Str(); + std::cout << from << " - " << status.status() << std::endl; + } + + virtual void OnStatusError(const buzz::XmlElement &stanza) { + } + + virtual void OnIqDone(bool success, const buzz::XmlElement &stanza) { + } + + virtual void OnMessage(const buzz::XmppMessage &m) { + if (m.body() != "") + std::cout << m.from().Str() << ": " << m.body() << std::endl; + } + + void OnRosterItemUpdated(const buzz::RosterItem &ri) { + std::cout << "Roster item: " << ri.jid().Str() << std::endl; + } + + virtual void OnRosterItemRemoved(const buzz::RosterItem &ri) { + std::cout << "Roster item removed: " << ri.jid().Str() << std::endl; + } + + virtual void OnRosterSubscribe(const buzz::Jid& jid) { + std::cout << "Subscribing: " << jid.Str() << std::endl; + } + + virtual void OnRosterUnsubscribe(const buzz::Jid &jid) { + std::cout << "Unsubscribing: " <Login("eaterleaver0", "Buzzt3st", "talk.google.com", false, false); + + delete libjingleplus; + } +} +} + +int main(int argc, char** argv) { + testing::ParseGUnitFlags(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/talk/libjingle.gyp b/talk/libjingle.gyp new file mode 100755 index 000000000..39ab84381 --- /dev/null +++ b/talk/libjingle.gyp @@ -0,0 +1,1174 @@ +# +# libjingle +# Copyright 2012, 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. +# + +{ + 'includes': ['build/common.gypi'], + + 'conditions': [ + ['os_posix == 1 and OS != "mac" and OS != "ios"', { + 'conditions': [ + ['sysroot!=""', { + 'variables': { + 'pkg-config': '../../../build/linux/pkg-config-wrapper "<(sysroot)" "<(target_arch)"', + }, + }, { + 'variables': { + 'pkg-config': 'pkg-config' + }, + }], + ], + }], + + ['OS=="linux" or OS=="android"', { + 'targets': [ + { + 'target_name': 'libjingle_peerconnection_so', + 'type': 'loadable_module', + 'dependencies': [ + 'libjingle_peerconnection', + '<(DEPTH)/third_party/icu/icu.gyp:icuuc', + ], + 'sources': [ + 'app/webrtc/java/jni/peerconnection_jni.cc' + ], + 'conditions': [ + ['OS=="linux"', { + 'defines': [ + 'HAVE_GTK', + ], + 'include_dirs': [ + '<(java_home)/include', + '<(java_home)/include/linux', + ], + 'link_settings': { + 'libraries': [ + ' <(PRODUCT_DIR)/libjingle_peerconnection_java_unittest && ' + 'cp <(DEPTH)/third_party/junit/junit-4.11.jar <(PRODUCT_DIR) && ' + 'chmod u+x <(PRODUCT_DIR)/libjingle_peerconnection_java_unittest' + ], + }, + ], + }, + ], + }], + ['libjingle_objc == 1', { + 'targets': [ + { + 'variables': { + 'infoplist_file': './app/webrtc/objctests/Info.plist', + }, + 'target_name': 'libjingle_peerconnection_objc_test', + 'type': 'executable', + 'mac_bundle': 1, + 'mac_bundle_resources': [ + '<(infoplist_file)', + ], + # The plist is listed above so that it appears in XCode's file list, + # but we don't actually want to bundle it. + 'mac_bundle_resources!': [ + '<(infoplist_file)', + ], + 'xcode_settings': { + 'INFOPLIST_FILE': '<(infoplist_file)', + }, + 'dependencies': [ + 'gunit', + 'libjingle.gyp:libjingle_peerconnection_objc', + ], + 'FRAMEWORK_SEARCH_PATHS': [ + '$(inherited)', + '$(SDKROOT)/Developer/Library/Frameworks', + '$(DEVELOPER_LIBRARY_DIR)/Frameworks', + ], + 'sources': [ + 'app/webrtc/objctests/RTCPeerConnectionSyncObserver.h', + 'app/webrtc/objctests/RTCPeerConnectionSyncObserver.m', + 'app/webrtc/objctests/RTCPeerConnectionTest.mm', + 'app/webrtc/objctests/RTCSessionDescriptionSyncObserver.h', + 'app/webrtc/objctests/RTCSessionDescriptionSyncObserver.m', + ], + 'include_dirs': [ + '<(DEPTH)/talk/app/webrtc/objc/public', + ], + 'conditions': [ + [ 'OS=="mac"', { + 'sources': [ + 'app/webrtc/objctests/mac/main.mm', + ], + 'xcode_settings': { + 'CLANG_ENABLE_OBJC_ARC': 'YES', + 'CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS': 'NO', + 'CLANG_LINK_OBJC_RUNTIME': 'YES', + # build/common.gypi disables ARC by default for back-compat + # reasons with OSX 10.6. Enabling OBJC runtime and clearing + # LDPLUSPLUS and CC re-enables it. Setting deployment target to + # 10.7 as there are no back-compat issues with ARC. + # https://code.google.com/p/chromium/issues/detail?id=156530 + 'CC': '', + 'LDPLUSPLUS': '', + 'macosx_deployment_target': '10.7', + }, + }], + ], + }, + ], + }], + ], +} diff --git a/talk/main.scons b/talk/main.scons new file mode 100644 index 000000000..76b24af64 --- /dev/null +++ b/talk/main.scons @@ -0,0 +1,889 @@ +# -*- Python -*- +# +# +# All the helper functions are defined in: +# - site_scons/talk.py +# Use 'import talk' in any .scons file to get access to it. +# Add any new helper functions to it; unittest are available +# in talk_unittest.py. +# +# Each 'component' that is built is defined in a .scons file. +# See talk.Components(...) for further info on file naming convention. +# +# To add a new platform clone and modify the root_env object. Remember to add +# the new environment object to the envs list otherwise it will not be included +# in the build. +# +# +# + +import talk +import os +import platform + +#------------------------------------------------------------------------------- +# The build files/directories to 'build'. +# If the name is the name of a directory then that directory shall contain a +# .scons file with the same name as the directory itself: +# Ex: The directory session/phone contains a file called phone.scons +# This list must be in order of library dependencies. e.g., if +# session/phone/phone.scons defines a target that links to a library target +# defined in sound/sound.scons, then 'sound' must come first. +# When no particular order is imposed by library dependencies, try to keep in +# mostly alphabetical order. +# +components = talk.Components("libjingle.scons") + +#------------------------------------------------------------------------------- +# Build environments +# + +# The list of build environments. +envs = [] + +# The root of all builds. +root_env = Environment( + tools = [ + 'component_bits', + 'component_setup', + 'replace_strings', + 'talk_noops', + #'talk_utils', + ], + BUILD_SCONSCRIPTS = components, + DESTINATION_ROOT = '$MAIN_DIR/build', + CPPPATH = [ + '$OBJ_ROOT', # generated headers are relative to here + '$MAIN_DIR/..', # TODO(dape): how can we use GOOGLECLIENT instead? + ], + CPPDEFINES = [ + 'LOGGING=1', + + # Feature selection + 'FEATURE_ENABLE_SSL', + 'FEATURE_ENABLE_VOICEMAIL', + 'FEATURE_ENABLE_PSTN', + 'HAVE_SRTP', + ], + # Ensure the os environment is captured for any scripts we call out to + ENV = os.environ, +) + +# This is where we set common environments +# +# Detect if building on 64-bit or 32-bit platform. +DeclareBit('build_platform_64bit', 'Platform of the build machine is 64-bit') +if platform.architecture()[0] == "64bit": + root_env.SetBits('build_platform_64bit') + +# This bit denotes that an env is for 64-bit builds. When set, all build +# artifacts will be 64-bit. When unset, all build artifacts will be 32-bit. +DeclareBit('host_platform_64bit', + 'Platform of the host machine (where artifacts will execute) is ' + '64-bit') + +# This bit denotes that we are cross-compiling using a sysroot. +DeclareBit('cross_compile', + 'Cross compiling using the SYSROOT environment variable') + +def CrossArch(env): + """Return whether or not the host platform architecture differs from the build + environment architecture.""" + if env.Bit('cross_compile'): + # The architecture of the Python process may not match the architecture of + # the sysroot, so we just assume it's not a cross-arch build or that it + # doesn't matter. Currently it only matters if you try to build a cross-arch + # Debian package, so just don't do that. + return False + else: + return env.Bit('host_platform_64bit') != env.Bit('build_platform_64bit') +root_env.AddMethod(CrossArch) + +DeclareBit('use_static_openssl', 'Build OpenSSL as a static library') + +DeclareBit('have_dbus_glib', + 'Whether the build system has the dbus-glib-1 package') +DeclareBit('have_libpulse', + 'Whether the build system has the libpulse package') + + +# List all the locales we localize to. +root_env.AppendUnique(locales = [ + 'af', 'am', 'ar', 'bg', 'bn', 'ca', 'cs', 'da', 'de', 'el', 'en', 'en-GB', + 'es', 'es-419', 'et', 'eu', 'fa', 'fi', 'fil', 'fr', 'fr-CA', 'gl', 'gu', + 'hi', 'hr', 'hu', 'id', 'is', 'it', 'iw', 'ja', 'kn', 'ko', 'lt', 'lv', + 'ml', 'mr', 'ms', 'nl', 'no', 'or', 'pl', 'pt-BR', 'pt-PT', 'ro', 'ru', + 'sk', 'sl', 'sr', 'sv', 'sw', 'ta', 'te', 'th', 'tl', 'tr', 'uk', 'ur', + 'vi', 'zh-CN', 'zh-HK', 'zh-TW', 'zu']) + +AddTargetGroup('all_breakpads', 'breakpad files can be built') + +AddTargetGroup('all_dsym', 'dsym debug packages can be built') + +#------------------------------------------------------------------------------- +# W I N D O W S +# +win_env = root_env.Clone( + tools = [ + 'atlmfc_vc80', + #'code_signing', + 'component_targets_msvs', + 'directx_9_0_c', + #'grid_builder', + 'midl', + 'target_platform_windows' + ], + # Don't use default vc80 midl.exe. It doesn't understand vista_sdk idl files. + MIDL = '$PLATFORM_SDK_VISTA_6_0_DIR/Bin/midl.exe ', + WIX_DIR = '$GOOGLECLIENT/third_party/wix/v3_0_2925/files', + # Flags for debug and optimization are added to CCFLAGS instead + CCPDBFLAGS = '', + CCFLAGS_DEBUG = '', + CCFLAGS_OPTIMIZED = '', + # We force a x86 target even when building on x64 Windows platforms. + TARGET_ARCH = 'x86', +) + + +win_env.Decider('MD5-timestamp') +win_env.Append( + COMPONENT_LIBRARY_PUBLISH = True, # Put dlls in output dir too + CCFLAGS = [ + '/Fd${TARGET}.pdb', # pdb per object allows --jobs= + '/WX', # warnings are errors + '/Zc:forScope', # handle 'for (int i = 0 ...)' right + '/EHs-c-', # disable C++ EH + '/GR-', # disable RTTI + '/Gy', # enable function level linking + '/wd4996', # ignore POSIX deprecated warnings + + # promote certain level 4 warnings + '/w14701', # potentially uninitialized var + '/w14702', # unreachable code + '/w14706', # assignment within a conditional + '/w14709', # comma operator within array index + '/w14063', # case 'identifier' is not a valid value for switch of enum + '/w14064', # switch of incomplete enum 'enumeration' + '/w14057', # 'identifier1' indirection to slightly different base + # types from 'identifier2' + '/w14263', # member function does not override any base class virtual + # member function + '/w14266', # no override available for virtual memberfunction from base + # 'type'; function is hidden + '/w14296', # expression is always false + '/w14355', # 'this' : used in base member initializer list + ], + CPPDEFINES = [ + '_ATL_CSTRING_EXPLICIT_CONSTRUCTORS', + # TODO(dape): encapsulate all string operations that are not based + # on std::string/std::wstring and make sure we use the safest versions + # available on all platforms. + '_CRT_SECURE_NO_WARNINGS', + '_USE_32BIT_TIME_T', + '_UNICODE', + 'UNICODE', + '_HAS_EXCEPTIONS=0', + 'WIN32', + # TODO(dape): remove this from logging.cc and enable here instead. + #'WIN32_LEAN_AND_MEAN', + + 'WINVER=0x0500', + '_WIN32_WINNT=0x0501', + '_WIN32_IE=0x0501', + # The Vista platform SDK 6.0 needs at least + # this NTDDI version or else the headers + # that LMI includes from it won't compile. + 'NTDDI_VERSION=NTDDI_WINXP', + + # npapi.h requires the following: + '_WINDOWS', + ], + CPPPATH = [ + '$THIRD_PARTY/wtl_71/include', + '$PLATFORM_SDK_VISTA_6_0_DIR/Include', + ], + LIBPATH = [ + '$PLATFORM_SDK_VISTA_6_0_DIR/Lib' + ], + LINKFLAGS = [ + '-manifest', # TODO(thaloun): Why do we need this? + # Some of the third-party libraries we link in don't have public symbols, so + # ignore that linker warning. + '/ignore:4221', + '/nxcompat', # Binary was tested to be be compatible with Windows DEP. + '/dynamicbase', # Use ASLR to dynamically rebase at load-time. + '/fixed:no', # Binary can be loaded at any base-address. + ], + MIDLFLAGS = [ + '/win32', + '/I$PLATFORM_SDK_VISTA_6_0_DIR/include' + ] +) + +# TODO(dape): Figure out what this does; found it in +# omaha/main.scons. This fixes the problem with redefinition +# of OS_WINDOWS symbol. +win_env.FilterOut(CPPDEFINES = ['OS_WINDOWS=OS_WINDOWS']) + +# Set up digital signing +DeclareBit('test_signing', 'Sign binaries with the test certificate') +win_env.SetBitFromOption('test_signing', False) +if win_env.Bit('test_signing'): + win_env.Replace( + CERTIFICATE_PATH = win_env.File( + '$GOOGLECLIENT/tools/test_key/testkey.pfx').abspath, + CERTIFICATE_PASSWORD = 'test', + ) +AddTargetGroup('signed_binaries', 'digitally signed binaries can be built') + +win_dbg_env = win_env.Clone( + BUILD_TYPE = 'dbg', + BUILD_TYPE_DESCRIPTION = 'Windows debug build', + BUILD_GROUPS = ['default', 'all'], + tools = ['target_debug'], +) + +win_dbg_env.Prepend( + CCFLAGS = [ + '/ZI', # enable debugging + '/Od', # disable optimizations + '/MTd', # link with LIBCMTD.LIB debug lib + '/RTC1', # enable runtime checks + ], +) + +envs.append(win_dbg_env) + +win_dbg64_env = win_dbg_env.Clone( + BUILD_TYPE = 'dbg64', + BUILD_TYPE_DESCRIPTION = 'Windows debug 64bit build', + BUILD_GROUPS = ['all'], +) + +win_dbg64_env.FilterOut(CCFLAGS = ['/ZI']) + +win_dbg64_env.Append( + CCFLAGS = [ + '/Zi', # enable debugging that is 64 bit compatible. + # TODO(fbarchard): fix warnings and remove these disables. + '/wd4244', # disable cast warning + '/wd4267', # disable cast warning + ], + ARFLAGS = [ + '/MACHINE:x64', + ], + CPPDEFINES = [ + 'WIN64', + 'ARCH_CPU_64_BITS', + ], + LIBFLAGS = [ + '/MACHINE:x64', + ], + LINKFLAGS = [ + '/MACHINE:x64', + ], +) + +win_dbg64_env.FilterOut(CPPDEFINES = ['_USE_32BIT_TIME_T']) + +win_dbg64_env.Prepend( + LIBPATH = [ + '$VC80_DIR/vc/lib/amd64', + '$ATLMFC_VC80_DIR/lib/amd64', + '$PLATFORM_SDK_VISTA_6_0_DIR/Lib/x64', + ], +) +win_dbg64_env.PrependENVPath( + 'PATH', + win_dbg64_env.Dir('$VC80_DIR/vc/bin/x86_amd64')) + +win_dbg64_env.SetBits('host_platform_64bit') + +envs.append(win_dbg64_env) + +win_coverage_env = win_dbg_env.Clone( + tools = ['code_coverage'], + BUILD_TYPE = 'coverage', + BUILD_TYPE_DESCRIPTION = 'Windows code coverage build', + BUILD_GROUPS = ['all'], +) + +win_coverage_env.Append( + CPPDEFINES = [ + 'COVERAGE_ENABLED', + ], +) + +envs.append(win_coverage_env) + +win_opt_env = win_env.Clone( + BUILD_TYPE = 'opt', + BUILD_TYPE_DESCRIPTION = 'Windows opt build', + BUILD_GROUPS = ['all'], + tools = ['target_optimized'], +) + +win_opt_env.Prepend( + CCFLAGS=[ + '/Zi', # enable debugging + '/O1', # optimize for size + '/fp:fast', # float faster but less precise + '/MT', # link with LIBCMT.LIB (multi-threaded, static linked crt) + '/GS', # enable security checks + ], + LINKFLAGS = [ + '/safeseh', # protect against attacks against exception handlers + '/opt:ref', # Remove unused references (functions/data). + ], +) + +envs.append(win_opt_env) + +#------------------------------------------------------------------------------- +# P O S I X +# +posix_env = root_env.Clone() +posix_env.Append( + CPPDEFINES = [ + 'HASHNAMESPACE=__gnu_cxx', + 'HASH_NAMESPACE=__gnu_cxx', + 'POSIX', + 'DISABLE_DYNAMIC_CAST', + # The POSIX standard says we have to define this. + '_REENTRANT', + ], + CCFLAGS = [ + '-Wall', + '-Werror', + '-Wno-switch', + '-fno-exceptions', + # Needed for a clean ABI and for link-time dead-code removal to work + # properly. + '-fvisibility=hidden', + # Generate debugging info in the DWARF2 format. + '-gdwarf-2', + # Generate maximal debugging information. (It is stripped from what we ship + # to users, so we want it for both dbg and opt.) + # Note that hammer automatically supplies "-g" for mac/linux dbg, so that + # flag must be filtered out of linux_dbg and mac_dbg envs below. + '-g3', + ], + CXXFLAGS = [ + '-Wno-non-virtual-dtor', + '-Wno-ctor-dtor-privacy', + '-fno-rtti', + ], +) + +# Switch-hit between NSS and OpenSSL +if 'NSS_BUILD_PLATFORM' in root_env['ENV']: + posix_env.AppendUnique(CPPDEFINES=['HAVE_NSS_SSL_H=1', + 'NSS_SSL_RELATIVE_PATH']) +else: + posix_env.AppendUnique(CPPDEFINES=['HAVE_OPENSSL_SSL_H=1']) + + +#------------------------------------------------------------------------------- +# M A C OSX +# +mac_env = posix_env.Clone( + tools = [ + 'target_platform_mac', + #'talk_mac', + #'fill_plist', + ], +) +# Use static OpenSSL on mac so that we can use the latest APIs on all +# supported mac platforms (10.5+). +mac_env.SetBits('use_static_openssl') + +# For libjingle we don't specify a sysroot or minimum OS version. +mac_osx_version_min_32 = "" +mac_osx_version_min_64 = "" + +# Generic mac environment common to all targets +mac_env.Append( + CPPDEFINES = [ + 'OSX', + ], + CCFLAGS = [ + '-arch', 'i386', + '-fasm-blocks', + ], + LINKFLAGS = [ + '-Wl,-search_paths_first', + # This flag makes all members of a static library be included in the + # final exe - that increases the size of the exe, but without it + # Obj-C categories aren't properly included in the exe. + # TODO(thaloun): consider only defining for libs that actually have objc. + '-ObjC', + '-arch', 'i386', + '-dead_strip', + ], + FRAMEWORKS = [ + 'CoreServices', + 'Security', + 'SystemConfiguration', + 'OpenGL', + 'CoreAudio', + 'Quartz', + 'Cocoa', + 'QTKit', + ] +) + +if 'NSS_BUILD_PLATFORM' in root_env['ENV']: + mac_env.AppendUnique(LINKFLAGS = ['-Lthird_party/mozilla/dist/' + root_env['ENV']['NSS_BUILD_PLATFORM'] + '/lib']) +else: + mac_env.AppendUnique(LINKFLAGS = ['-Lthird_party/openssl']) + + +# add debug flags to environment +def mac_debug_include(env): + env.Append( + CCFLAGS = [ + '-O0', + ], + CPPDEFINES = [ + 'DEBUG=1', + ], + ) + # Remove -g set by hammer, which is not what we want (we have set -g3 above). + env.FilterOut(CCFLAGS = ['-g']) + +# add 32/64 bit specific options to specified environment +def mac_common_include_x86_32(env): + env.Append( + CCFLAGS = [ + '-m32', + ], + LINKFLAGS = [ + '-m32', + ], + FRAMEWORKS = [ + 'Carbon', + 'QuickTime', + ], + ) + envs.append(env) + +def mac_common_include_x86_64(env): + env.Append( + CCFLAGS = [ + '-m64', + '-fPIC', + ], + CPPDEFINES = [ + 'ARCH_CPU_64_BITS', + 'CARBON_DEPRECATED', + ], + LINKFLAGS = [ + '-m64', + ], + FRAMEWORKS = [ + 'AppKit', + ], + ) + env.SetBits('host_platform_64bit') + envs.append(env) + +def mac_osx_version_min(env, ver): + if ver != "": + sdk_path = '/Developer/SDKs/MacOSX%s.sdk' % ver + env.Append( + CCFLAGS = [ + '-mmacosx-version-min=' + ver, + '-isysroot', sdk_path, + ], + LINKFLAGS = [ + '-mmacosx-version-min=' + ver, + '-isysroot', sdk_path, + ], + osx_sdk_path = sdk_path, + osx_version_min = ver, + ) + +# Create all environments +mac_dbg_env = mac_env.Clone( + BUILD_TYPE = 'dbg', + BUILD_TYPE_DESCRIPTION = 'Mac debug build', + BUILD_GROUPS = ['default', 'all'], + tools = ['target_debug'], +) + +mac_opt_env = mac_env.Clone( + BUILD_TYPE = 'opt', + BUILD_TYPE_DESCRIPTION = 'Mac opt build', + BUILD_GROUPS = ['all'], + tools = ['target_optimized'], +) + +mac_dbg64_env = mac_dbg_env.Clone( + BUILD_TYPE = 'dbg64', + BUILD_TYPE_DESCRIPTION = 'Mac debug 64bit build', + BUILD_GROUPS = ['all'], +) + +mac_opt64_env = mac_opt_env.Clone( + BUILD_TYPE = 'opt64', + BUILD_TYPE_DESCRIPTION = 'Mac opt 64bit build', + BUILD_GROUPS = ['all'], +) + +mac_debug_include(mac_dbg_env) +mac_debug_include(mac_dbg64_env) +mac_common_include_x86_32(mac_dbg_env) +mac_common_include_x86_32(mac_opt_env) +mac_common_include_x86_64(mac_dbg64_env) +mac_common_include_x86_64(mac_opt64_env) +mac_osx_version_min(mac_dbg_env, mac_osx_version_min_32) +mac_osx_version_min(mac_opt_env, mac_osx_version_min_32) +mac_osx_version_min(mac_dbg64_env, mac_osx_version_min_64) +mac_osx_version_min(mac_opt64_env, mac_osx_version_min_64) + + +#------------------------------------------------------------------------------- +# L I N U X +# +linux_common_env = posix_env.Clone( + tools = [ + 'target_platform_linux', + 'talk_linux', + ], +) + +linux_common_env.Append( + CPPDEFINES = [ + 'LINUX', + ], + CCFLAGS = [ + # Needed for link-time dead-code removal to work properly. + '-ffunction-sections', + '-fdata-sections', + ], + LINKFLAGS = [ + # Enable dead-code removal. + '-Wl,--gc-sections', + # Elide dependencies on shared libraries that we're not actually using. + '-Wl,--as-needed', + '-Wl,--start-group', + ], + _LIBFLAGS = ['-Wl,--end-group'], +) + +# Remove default rpath set by Hammer. Hammer sets it to LIB_DIR, which is wrong. +# The rpath is the _run-time_ library search path for the resulting binary, i.e. +# the one used by ld.so at load time. Setting it equal to the path to build +# output on the build machine is nonsense. +linux_common_env.Replace( + RPATH = [], +) + +# Enable the optional DBus-GLib code if the build machine has the required +# dependency. +linux_common_env.EnableFeatureWherePackagePresent('have_dbus_glib', + 'HAVE_DBUS_GLIB', + 'dbus-glib-1') + +def linux_common_include_x86_32(env): + """Include x86-32 settings into an env based on linux_common.""" + env.Append( + CCFLAGS = [ + '-m32', + ], + LINKFLAGS = [ + '-m32', + ], + ) + +def linux_common_include_x86_64(env): + """Include x86-64 settings into an env based on linux_common.""" + env.Append( + CCFLAGS = [ + '-m64', + '-fPIC', + ], + LINKFLAGS = [ + '-m64', + ], + ) + env.SetBits('host_platform_64bit') + +#------------------------------------------------------------------------------- +# L I N U X -- C R O S S -- B U I L D + +# Cross build requires the following tool names be provided by the environment: +linux_cross_common_env = linux_common_env.Clone( + AR = os.environ.get("AR"), + AS = os.environ.get("AS"), + LD = os.environ.get("LD"), + NM = os.environ.get("NM"), + RANLIB = os.environ.get("RANLIB"), + CC = str(os.environ.get("CC")) + + ' --sysroot=' + str(os.environ.get("SYSROOT")), + CXX = str(os.environ.get("CXX")) + + ' --sysroot=' + str(os.environ.get("SYSROOT")), +) +linux_cross_common_env.SetBits('cross_compile') + +# The rest of these paths and flags are optional: +if os.environ.get("CPPPATH"): + linux_cross_common_env.Append( + CPPPATH = os.environ.get("CPPPATH").split(':'), + ) +if os.environ.get("LIBPATH"): + linux_cross_common_env.Append( + LIBPATH = os.environ.get("LIBPATH").split(':'), + ) +if os.environ.get("CFLAGS"): + linux_cross_common_env.Append( + CFLAGS = os.environ.get("CFLAGS").split(' '), + ) +if os.environ.get("CCFLAGS"): + linux_cross_common_env.Append( + CCFLAGS = os.environ.get("CCFLAGS").split(' '), + ) +if os.environ.get("CXXFLAGS"): + linux_cross_common_env.Append( + CXXFLAGS = os.environ.get("CXXFLAGS").split(' '), + ) +if os.environ.get("LIBFLAGS"): + linux_cross_common_env.Append( + _LIBFLAGS = os.environ.get("LIBFLAGS").split(' '), + ) +if os.environ.get("LINKFLAGS"): + linux_cross_common_env.Prepend( + LINKFLAGS = os.environ.get("LINKFLAGS").split(' '), + ) + +#------------------------------------------------------------------------------- +# L I N U X -- T R A D I T I O N A L -- X 8 6 +# +# Settings that are specific to our desktop Linux x86 targets. +def linux_common_include_traditional(env): + """Include traditional Linux settings into an env based on linux_common.""" + # OpenSSL has infamously poor ABI stability, so that building against one + # version and running against a different one often will not work. Since our + # non-ChromeOS Linux builds are used on many different distros and distro + # versions, this means we can't safely dynamically link to OpenSSL because the + # product would end up being broken on any computer with a different version + # installed. So instead we build it ourself and statically link to it. + env.SetBits('use_static_openssl') + # Enable the optional PulseAudio code if the build machine has the required + # dependency. + # TODO(?): This belongs in linux_common_env, but we can't safely move it there + # yet because pkg-config is not being used properly with ChromeOS builds (see + # TODO below). + env.EnableFeatureWherePackagePresent('have_libpulse', + 'HAVE_LIBPULSE', + 'libpulse') + +def linux_traditional_include_dbg(env): + """Include traditional Linux dbg settings into an env based on the above.""" + # Remove -g set by hammer, which is not what we want (we have set -g3 above). + env.FilterOut(CCFLAGS = ['-g']) + +def linux_traditional_include_opt(env): + """Include traditional Linux opt settings into an env based on the above.""" + # Remove -O2 set by hammer, which is not what we want. + env.FilterOut(CCFLAGS = ['-O2']) + env.Append(CCFLAGS = ['-Os']) + +def gen_linux_nonhermetic(linux_env, type_suffix, desc_suffix): + groups = ['nonhermetic'] + if not linux_env.CrossArch(): + groups = groups + ['nonhermetic-native'] + # The non-hermetic, native-arch dbg build is the default. + dbg_groups = groups + ['default'] + native_desc = ', native ' + # No suffix for native modes. + type_suffix = '' + else: + groups = groups + ['nonhermetic-cross'] + dbg_groups = groups + native_desc = ', cross-built for ' + + linux_dbg_env = linux_env.Clone( + BUILD_TYPE = 'dbg' + type_suffix, + BUILD_TYPE_DESCRIPTION = 'Linux debug build%s%s' % (native_desc, + desc_suffix), + BUILD_GROUPS = dbg_groups, + tools = ['target_debug'], + ) + linux_traditional_include_dbg(linux_dbg_env) + envs.append(linux_dbg_env) + + linux_opt_env = linux_env.Clone( + BUILD_TYPE = 'opt' + type_suffix, + BUILD_TYPE_DESCRIPTION = 'Linux optimized build%s%s' % (native_desc, + desc_suffix), + BUILD_GROUPS = groups, + tools = ['target_optimized'], + ) + linux_traditional_include_opt(linux_opt_env) + envs.append(linux_opt_env) + +linux_nonhermetic_common_env = linux_common_env.Clone() +linux_common_include_traditional(linux_nonhermetic_common_env) + +linux_nonhermetic_x86_32_env = linux_nonhermetic_common_env.Clone() +linux_common_include_x86_32(linux_nonhermetic_x86_32_env) +gen_linux_nonhermetic(linux_nonhermetic_x86_32_env, '32', '32-bit') + +linux_nonhermetic_x86_64_env = linux_nonhermetic_common_env.Clone() +linux_common_include_x86_64(linux_nonhermetic_x86_64_env) +gen_linux_nonhermetic(linux_nonhermetic_x86_64_env, '64', '64-bit') + +def gen_linux_hermetic(linux_env, type_suffix, desc): + groups = ['hermetic'] + + linux_dbg_env = linux_env.Clone( + BUILD_TYPE = 'hermetic-dbg' + type_suffix, + BUILD_TYPE_DESCRIPTION = 'Hermetic %s Linux debug build' % desc, + BUILD_GROUPS = groups, + tools = ['target_debug'], + ) + linux_traditional_include_dbg(linux_dbg_env) + envs.append(linux_dbg_env) + + linux_opt_env = linux_env.Clone( + BUILD_TYPE = 'hermetic-opt' + type_suffix, + BUILD_TYPE_DESCRIPTION = 'Hermetic %s Linux optimized build' % desc, + BUILD_GROUPS = groups, + tools = ['target_optimized'], + ) + linux_traditional_include_opt(linux_opt_env) + envs.append(linux_opt_env) + +linux_hermetic_common_env = linux_cross_common_env.Clone() +linux_common_include_traditional(linux_hermetic_common_env) + +linux_hermetic_x86_32_env = linux_hermetic_common_env.Clone() +linux_common_include_x86_32(linux_hermetic_x86_32_env) +gen_linux_hermetic(linux_hermetic_x86_32_env, '32', '32-bit') + +linux_hermetic_x86_64_env = linux_hermetic_common_env.Clone() +linux_common_include_x86_64(linux_hermetic_x86_64_env) +gen_linux_hermetic(linux_hermetic_x86_64_env, '64', '64-bit') + +#------------------------------------------------------------------------------- +# L I N U X -- C R O S S -- B U I L D -- A R M + +# TODO(noahric): All the following Linux builds are running against a sysroot +# but improperly using the host machine's pkg-config environment. The ChromeOS +# ones should probably be using +# https://cs.corp.google.com/#chrome/src/build/linux/pkg-config-wrapper. + +linux_cross_arm_env = linux_cross_common_env.Clone() +linux_cross_arm_env.Append( + CPPDEFINES = [ + 'NACL_BUILD_ARCH=arm', + 'DISABLE_EFFECTS=1', + ], + CCFLAGS = [ + '-fPIC', + ], +) +DeclareBit('arm', 'ARM build') +linux_cross_arm_env.SetBits('arm') + +# Detect NEON support from the -mfpu build flag. +DeclareBit('arm_neon', 'ARM supporting neon') +if '-mfpu=neon' in linux_cross_arm_env['CFLAGS'] or \ + '-mfpu=neon' in linux_cross_arm_env['CCFLAGS'] or \ + '-mfpu=neon' in linux_cross_arm_env['CXXFLAGS']: + print "Building with ARM NEON support." + linux_cross_arm_env.SetBits('arm_neon') + +# Detect hardfp from the -mfloat-abi build flag +DeclareBit('arm_hardfp', 'ARM supporting hardfp') +if '-mfloat-abi=hard' in linux_cross_arm_env['CFLAGS'] or \ + '-mfloat-abi=hard' in linux_cross_arm_env['CCFLAGS'] or \ + '-mfloat-abi=hard' in linux_cross_arm_env['CXXFLAGS']: + print "Building with hard floating point support." + linux_cross_arm_env.SetBits('arm_hardfp') + +linux_cross_arm_dbg_env = linux_cross_arm_env.Clone( + BUILD_TYPE = 'arm-dbg', + BUILD_TYPE_DESCRIPTION = 'Cross-compiled ARM debug build', + BUILD_GROUPS = ['arm'], + tools = ['target_debug'], +) +envs.append(linux_cross_arm_dbg_env) + +linux_cross_arm_opt_env = linux_cross_arm_env.Clone( + BUILD_TYPE = 'arm-opt', + BUILD_TYPE_DESCRIPTION = 'Cross-compiled ARM optimized build', + BUILD_GROUPS = ['arm'], + tools = ['target_optimized'], +) +envs.append(linux_cross_arm_opt_env) + + + +# Create a group for installers +AddTargetGroup('all_installers', 'installers that can be built') + +# Parse child .scons files +BuildEnvironments(envs) + +# Explicitly set which targets to build when not stated on commandline +Default(None) +# Build the following, which excludes unit test output (ie running them) +# To run unittests, specify the test to run, or run_all_tests. See -h option. +Default(['all_libraries', 'all_programs', 'all_test_programs']) + +# .sln creation code lifted from googleclient/bar/main.scons. Must be after +# the call to BuildEnvironments for all_foo aliases to be defined. +# Run 'hammer --mode=all --vsproj' to generate +DeclareBit('vsproj', 'Generate Visual Studio projects and solution files.') +win_env.SetBitFromOption('vsproj', False) + +if win_env.Bit('vsproj'): + vs_env = win_env.Clone() + vs_env.Append( + COMPONENT_VS_SOURCE_SUFFIXES = [ + '.def', + '.grd', + '.html', + '.idl', + '.mk', + '.txt', + '.py', + '.scons', + '.wxs.template', + ] + ) + + # Source project + p = vs_env.ComponentVSDirProject( + 'flute_source', + ['$MAIN_DIR', + ], + COMPONENT_VS_SOURCE_FOLDERS = [ + # Files are assigned to first matching folder. Folder names of None + # are filters. + (None, '$DESTINATION_ROOT'), + ('flute', '$MAIN_DIR'), + ('google3', '$GOOGLE3'), + ('third_party', '$THIRD_PARTY'), + ], + # Force source project to main dir, so that Visual Studio can find the + # source files corresponding to build errors. + COMPONENT_VS_PROJECT_DIR = '$MAIN_DIR', + ) + vs_env.AlwaysBuild(p) + + # Solution and target projects + s = vs_env.ComponentVSSolution( + # 'libjingle', # Please uncomment this line if you build VS proj files. + ['all_libraries', 'all_programs', 'all_test_programs'], + projects = [p], + ) + + print '***Unfortunately the vsproj creator isn\'t smart enough to ' + print '***automatically get the correct output locations. It is very easy' + print '***though to change it in the properties pane to the following' + print '***$(SolutionDir)/build//staging/.exe' + Default(None) + Default([s]) diff --git a/talk/media/base/audioframe.h b/talk/media/base/audioframe.h new file mode 100755 index 000000000..b0c8b047d --- /dev/null +++ b/talk/media/base/audioframe.h @@ -0,0 +1,63 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_AUDIOFRAME_H_ +#define TALK_MEDIA_BASE_AUDIOFRAME_H_ + +namespace cricket { + +class AudioFrame { + public: + AudioFrame() + : audio10ms_(NULL), + length_(0), + sampling_frequency_(8000), + stereo_(false) { + } + AudioFrame(int16* audio, size_t audio_length, int sample_freq, bool stereo) + : audio10ms_(audio), + length_(audio_length), + sampling_frequency_(sample_freq), + stereo_(stereo) { + } + + int16* GetData() { return audio10ms_; } + size_t GetSize() const { return length_; } + int GetSamplingFrequency() const { return sampling_frequency_; } + bool GetStereo() const { return stereo_; } + + private: + // TODO(janahan): currently the data is not owned by this class. + // add ownership when we come up with the first use case that requires it. + int16* audio10ms_; + size_t length_; + int sampling_frequency_; + bool stereo_; +}; + +} // namespace cricket +#endif // TALK_MEDIA_BASE_AUDIOFRAME_H_ diff --git a/talk/media/base/audiorenderer.h b/talk/media/base/audiorenderer.h new file mode 100644 index 000000000..23b17da8b --- /dev/null +++ b/talk/media/base/audiorenderer.h @@ -0,0 +1,45 @@ +/* + * libjingle + * Copyright 2013 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. + */ + +#ifndef TALK_MEDIA_BASE_AUDIORENDERER_H_ +#define TALK_MEDIA_BASE_AUDIORENDERER_H_ + +namespace cricket { + +// Abstract interface for holding the voice channel ID. +class AudioRenderer { + public: + virtual void SetChannelId(int channel_id) = 0; + virtual int GetChannelId() const = 0; + + protected: + virtual ~AudioRenderer() {} +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_AUDIORENDERER_H_ diff --git a/talk/media/base/capturemanager.cc b/talk/media/base/capturemanager.cc new file mode 100644 index 000000000..5705bcd66 --- /dev/null +++ b/talk/media/base/capturemanager.cc @@ -0,0 +1,389 @@ +/* + * libjingle + * Copyright 2012 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/media/base/capturemanager.h" + +#include + +#include "talk/base/logging.h" +#include "talk/media/base/videocapturer.h" +#include "talk/media/base/videoprocessor.h" +#include "talk/media/base/videorenderer.h" + +namespace cricket { + +// CaptureManager helper class. +class VideoCapturerState { + public: + static const VideoFormatPod kDefaultCaptureFormat; + + static VideoCapturerState* Create(VideoCapturer* video_capturer); + ~VideoCapturerState() {} + + void AddCaptureResolution(const VideoFormat& desired_format); + bool RemoveCaptureResolution(const VideoFormat& format); + VideoFormat GetHighestFormat(VideoCapturer* video_capturer) const; + + int IncCaptureStartRef(); + int DecCaptureStartRef(); + CaptureRenderAdapter* adapter() { return adapter_.get(); } + VideoCapturer* GetVideoCapturer() { return adapter()->video_capturer(); } + + int start_count() const { return start_count_; } + + private: + struct CaptureResolutionInfo { + VideoFormat video_format; + int format_ref_count; + }; + typedef std::vector CaptureFormats; + + explicit VideoCapturerState(CaptureRenderAdapter* adapter); + + talk_base::scoped_ptr adapter_; + + int start_count_; + CaptureFormats capture_formats_; +}; + +const VideoFormatPod VideoCapturerState::kDefaultCaptureFormat = { + 640, 360, FPS_TO_INTERVAL(30), FOURCC_ANY +}; + +VideoCapturerState::VideoCapturerState(CaptureRenderAdapter* adapter) + : adapter_(adapter), start_count_(1) {} + +VideoCapturerState* VideoCapturerState::Create(VideoCapturer* video_capturer) { + CaptureRenderAdapter* adapter = CaptureRenderAdapter::Create(video_capturer); + if (!adapter) { + return NULL; + } + return new VideoCapturerState(adapter); +} + +void VideoCapturerState::AddCaptureResolution( + const VideoFormat& desired_format) { + for (CaptureFormats::iterator iter = capture_formats_.begin(); + iter != capture_formats_.end(); ++iter) { + if (desired_format == iter->video_format) { + ++(iter->format_ref_count); + return; + } + } + CaptureResolutionInfo capture_resolution = { desired_format, 1 }; + capture_formats_.push_back(capture_resolution); +} + +bool VideoCapturerState::RemoveCaptureResolution(const VideoFormat& format) { + for (CaptureFormats::iterator iter = capture_formats_.begin(); + iter != capture_formats_.end(); ++iter) { + if (format == iter->video_format) { + --(iter->format_ref_count); + if (iter->format_ref_count == 0) { + capture_formats_.erase(iter); + } + return true; + } + } + return false; +} + +VideoFormat VideoCapturerState::GetHighestFormat( + VideoCapturer* video_capturer) const { + VideoFormat highest_format(0, 0, VideoFormat::FpsToInterval(1), FOURCC_ANY); + if (capture_formats_.empty()) { + VideoFormat default_format(kDefaultCaptureFormat); + return default_format; + } + for (CaptureFormats::const_iterator iter = capture_formats_.begin(); + iter != capture_formats_.end(); ++iter) { + if (iter->video_format.width > highest_format.width) { + highest_format.width = iter->video_format.width; + } + if (iter->video_format.height > highest_format.height) { + highest_format.height = iter->video_format.height; + } + if (iter->video_format.interval < highest_format.interval) { + highest_format.interval = iter->video_format.interval; + } + } + return highest_format; +} + +int VideoCapturerState::IncCaptureStartRef() { return ++start_count_; } + +int VideoCapturerState::DecCaptureStartRef() { + if (start_count_ > 0) { + // Start count may be 0 if a capturer was added but never started. + --start_count_; + } + return start_count_; +} + +CaptureManager::~CaptureManager() { + while (!capture_states_.empty()) { + // There may have been multiple calls to StartVideoCapture which means that + // an equal number of calls to StopVideoCapture must be made. Note that + // StopVideoCapture will remove the element from |capture_states_| when a + // successfull stop has been made. + UnregisterVideoCapturer(capture_states_.begin()->second); + } +} + +bool CaptureManager::StartVideoCapture(VideoCapturer* video_capturer, + const VideoFormat& desired_format) { + if (desired_format.width == 0 || desired_format.height == 0) { + return false; + } + if (!video_capturer) { + return false; + } + VideoCapturerState* capture_state = GetCaptureState(video_capturer); + if (capture_state) { + const int ref_count = capture_state->IncCaptureStartRef(); + if (ref_count < 1) { + ASSERT(false); + } + // VideoCapturer has already been started. Don't start listening to + // callbacks since that has already been done. + capture_state->AddCaptureResolution(desired_format); + return true; + } + if (!RegisterVideoCapturer(video_capturer)) { + return false; + } + capture_state = GetCaptureState(video_capturer); + ASSERT(capture_state != NULL); + capture_state->AddCaptureResolution(desired_format); + if (!StartWithBestCaptureFormat(capture_state, video_capturer)) { + UnregisterVideoCapturer(capture_state); + return false; + } + return true; +} + +bool CaptureManager::StopVideoCapture(VideoCapturer* video_capturer, + const VideoFormat& format) { + VideoCapturerState* capture_state = GetCaptureState(video_capturer); + if (!capture_state) { + return false; + } + if (!capture_state->RemoveCaptureResolution(format)) { + return false; + } + + if (capture_state->DecCaptureStartRef() == 0) { + // Unregistering cannot fail as capture_state is not NULL. + UnregisterVideoCapturer(capture_state); + } + return true; +} + +bool CaptureManager::RestartVideoCapture( + VideoCapturer* video_capturer, + const VideoFormat& previous_format, + const VideoFormat& desired_format, + CaptureManager::RestartOptions options) { + if (!IsCapturerRegistered(video_capturer)) { + LOG(LS_ERROR) << "RestartVideoCapture: video_capturer is not registered."; + return false; + } + // Start the new format first. This keeps the capturer running. + if (!StartVideoCapture(video_capturer, desired_format)) { + LOG(LS_ERROR) << "RestartVideoCapture: unable to start video capture with " + "desired_format=" << desired_format.ToString(); + return false; + } + // Stop the old format. + if (!StopVideoCapture(video_capturer, previous_format)) { + LOG(LS_ERROR) << "RestartVideoCapture: unable to stop video capture with " + "previous_format=" << previous_format.ToString(); + // Undo the start request we just performed. + StopVideoCapture(video_capturer, desired_format); + return false; + } + + switch (options) { + case kForceRestart: { + VideoCapturerState* capture_state = GetCaptureState(video_capturer); + ASSERT(capture_state && capture_state->start_count() > 0); + // Try a restart using the new best resolution. + VideoFormat highest_asked_format = + capture_state->GetHighestFormat(video_capturer); + VideoFormat capture_format; + if (video_capturer->GetBestCaptureFormat(highest_asked_format, + &capture_format)) { + if (!video_capturer->Restart(capture_format)) { + LOG(LS_ERROR) << "RestartVideoCapture: Restart failed."; + } + } else { + LOG(LS_WARNING) + << "RestartVideoCapture: Couldn't find a best capture format for " + << highest_asked_format.ToString(); + } + break; + } + case kRequestRestart: + // TODO(ryanpetrie): Support restart requests. Should this + // to-be-implemented logic be used for {Start,Stop}VideoCapture as well? + break; + default: + LOG(LS_ERROR) << "Unknown/unimplemented RestartOption"; + break; + } + return true; +} + +bool CaptureManager::AddVideoRenderer(VideoCapturer* video_capturer, + VideoRenderer* video_renderer) { + if (!video_capturer || !video_renderer) { + return false; + } + CaptureRenderAdapter* adapter = GetAdapter(video_capturer); + if (!adapter) { + return false; + } + return adapter->AddRenderer(video_renderer); +} + +bool CaptureManager::RemoveVideoRenderer(VideoCapturer* video_capturer, + VideoRenderer* video_renderer) { + if (!video_capturer || !video_renderer) { + return false; + } + CaptureRenderAdapter* adapter = GetAdapter(video_capturer); + if (!adapter) { + return false; + } + return adapter->RemoveRenderer(video_renderer); +} + +bool CaptureManager::AddVideoProcessor(VideoCapturer* video_capturer, + VideoProcessor* video_processor) { + if (!video_capturer || !video_processor) { + return false; + } + if (!IsCapturerRegistered(video_capturer)) { + return false; + } + video_capturer->AddVideoProcessor(video_processor); + return true; +} + +bool CaptureManager::RemoveVideoProcessor(VideoCapturer* video_capturer, + VideoProcessor* video_processor) { + if (!video_capturer || !video_processor) { + return false; + } + if (!IsCapturerRegistered(video_capturer)) { + return false; + } + return video_capturer->RemoveVideoProcessor(video_processor); +} + +bool CaptureManager::IsCapturerRegistered(VideoCapturer* video_capturer) const { + return GetCaptureState(video_capturer) != NULL; +} + +bool CaptureManager::RegisterVideoCapturer(VideoCapturer* video_capturer) { + VideoCapturerState* capture_state = + VideoCapturerState::Create(video_capturer); + if (!capture_state) { + return false; + } + capture_states_[video_capturer] = capture_state; + SignalCapturerStateChange.repeat(video_capturer->SignalStateChange); + return true; +} + +void CaptureManager::UnregisterVideoCapturer( + VideoCapturerState* capture_state) { + VideoCapturer* video_capturer = capture_state->GetVideoCapturer(); + capture_states_.erase(video_capturer); + delete capture_state; + + // When unregistering a VideoCapturer, the CaptureManager needs to unregister + // from all state change callbacks from the VideoCapturer. E.g. to avoid + // problems with multiple callbacks if registering the same VideoCapturer + // multiple times. The VideoCapturer will update the capturer state. However, + // this is done through Post-calls which means it may happen at any time. If + // the CaptureManager no longer is listening to the VideoCapturer it will not + // receive those callbacks. Here it is made sure that the the callback is + // indeed sent by letting the ChannelManager do the signaling. The downside is + // that the callback may happen before the VideoCapturer is stopped. However, + // for the CaptureManager it doesn't matter as it will no longer receive any + // frames from the VideoCapturer. + SignalCapturerStateChange.stop(video_capturer->SignalStateChange); + video_capturer->Stop(); + SignalCapturerStateChange(video_capturer, CS_STOPPED); +} + +bool CaptureManager::StartWithBestCaptureFormat( + VideoCapturerState* capture_state, VideoCapturer* video_capturer) { + VideoFormat highest_asked_format = + capture_state->GetHighestFormat(video_capturer); + VideoFormat capture_format; + if (!video_capturer->GetBestCaptureFormat(highest_asked_format, + &capture_format)) { + LOG(LS_WARNING) << "Unsupported format:" + << " width=" << highest_asked_format.width + << " height=" << highest_asked_format.height + << ". Supported formats are:"; + const std::vector* formats = + video_capturer->GetSupportedFormats(); + ASSERT(formats != NULL); + for (std::vector::const_iterator i = formats->begin(); + i != formats->end(); ++i) { + const VideoFormat& format = *i; + LOG(LS_WARNING) << " " << GetFourccName(format.fourcc) + << ":" << format.width << "x" << format.height << "x" + << format.framerate(); + } + return false; + } + return video_capturer->StartCapturing(capture_format); +} + +VideoCapturerState* CaptureManager::GetCaptureState( + VideoCapturer* video_capturer) const { + CaptureStates::const_iterator iter = capture_states_.find(video_capturer); + if (iter == capture_states_.end()) { + return NULL; + } + return iter->second; +} + +CaptureRenderAdapter* CaptureManager::GetAdapter( + VideoCapturer* video_capturer) const { + VideoCapturerState* capture_state = GetCaptureState(video_capturer); + if (!capture_state) { + return NULL; + } + return capture_state->adapter(); +} + +} // namespace cricket diff --git a/talk/media/base/capturemanager.h b/talk/media/base/capturemanager.h new file mode 100644 index 000000000..9c443950a --- /dev/null +++ b/talk/media/base/capturemanager.h @@ -0,0 +1,114 @@ +/* + * libjingle + * Copyright 2012 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. + */ + +// The CaptureManager class manages VideoCapturers to make it possible to share +// the same VideoCapturers across multiple instances. E.g. if two instances of +// some class want to listen to same VideoCapturer they can't individually stop +// and start capturing as doing so will affect the other instance. +// The class employs reference counting on starting and stopping of capturing of +// frames such that if anyone is still listening it will not be stopped. The +// class also provides APIs for attaching VideoRenderers to a specific capturer +// such that the VideoRenderers are fed frames directly from the capturer. In +// addition, these frames can be altered before being sent to the capturers by +// way of VideoProcessors. +// CaptureManager is Thread-unsafe. This means that none of its APIs may be +// called concurrently. Note that callbacks are called by the VideoCapturer's +// thread which is normally a separate unmarshalled thread and thus normally +// require lock protection. + +#ifndef TALK_MEDIA_BASE_CAPTUREMANAGER_H_ +#define TALK_MEDIA_BASE_CAPTUREMANAGER_H_ + +#include +#include + +#include "talk/base/sigslotrepeater.h" +#include "talk/media/base/capturerenderadapter.h" +#include "talk/media/base/videocommon.h" + +namespace cricket { + +class VideoCapturer; +class VideoProcessor; +class VideoRenderer; +class VideoCapturerState; + +class CaptureManager : public sigslot::has_slots<> { + public: + enum RestartOptions { + kRequestRestart, + kForceRestart + }; + + CaptureManager() {} + virtual ~CaptureManager(); + + bool StartVideoCapture(VideoCapturer* video_capturer, + const VideoFormat& desired_format); + bool StopVideoCapture(VideoCapturer* video_capturer, + const VideoFormat& format); + + // Possibly restarts the capturer. If |options| is set to kRequestRestart, + // the CaptureManager chooses whether this request can be handled with the + // current state or if a restart is actually needed. If |options| is set to + // kForceRestart, the capturer is restarted. + bool RestartVideoCapture(VideoCapturer* video_capturer, + const VideoFormat& previous_format, + const VideoFormat& desired_format, + RestartOptions options); + + virtual bool AddVideoRenderer(VideoCapturer* video_capturer, + VideoRenderer* video_renderer); + virtual bool RemoveVideoRenderer(VideoCapturer* video_capturer, + VideoRenderer* video_renderer); + + virtual bool AddVideoProcessor(VideoCapturer* video_capturer, + VideoProcessor* video_processor); + virtual bool RemoveVideoProcessor(VideoCapturer* video_capturer, + VideoProcessor* video_processor); + + sigslot::repeater2 SignalCapturerStateChange; + + private: + typedef std::map CaptureStates; + + bool IsCapturerRegistered(VideoCapturer* video_capturer) const; + bool RegisterVideoCapturer(VideoCapturer* video_capturer); + void UnregisterVideoCapturer(VideoCapturerState* capture_state); + + bool StartWithBestCaptureFormat(VideoCapturerState* capture_info, + VideoCapturer* video_capturer); + + VideoCapturerState* GetCaptureState(VideoCapturer* video_capturer) const; + CaptureRenderAdapter* GetAdapter(VideoCapturer* video_capturer) const; + + CaptureStates capture_states_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_CAPTUREMANAGER_H_ diff --git a/talk/media/base/capturemanager_unittest.cc b/talk/media/base/capturemanager_unittest.cc new file mode 100644 index 000000000..e9d9cfb13 --- /dev/null +++ b/talk/media/base/capturemanager_unittest.cc @@ -0,0 +1,251 @@ +/* + * libjingle + * Copyright 2012, 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/media/base/capturemanager.h" + +#include "talk/base/gunit.h" +#include "talk/base/sigslot.h" +#include "talk/media/base/fakemediaprocessor.h" +#include "talk/media/base/fakevideocapturer.h" +#include "talk/media/base/fakevideorenderer.h" + +const int kMsCallbackWait = 50; + +const int kFps = 30; +const cricket::VideoFormatPod kCameraFormats[] = { + {640, 480, cricket::VideoFormat::FpsToInterval(kFps), cricket::FOURCC_I420}, + {320, 240, cricket::VideoFormat::FpsToInterval(kFps), cricket::FOURCC_I420} +}; + +class CaptureManagerTest : public ::testing::Test, public sigslot::has_slots<> { + public: + CaptureManagerTest() + : capture_manager_(), + callback_count_(0), + format_vga_(kCameraFormats[0]), + format_qvga_(kCameraFormats[1]) { + } + virtual void SetUp() { + PopulateSupportedFormats(); + capture_state_ = cricket::CS_STOPPED; + capture_manager_.SignalCapturerStateChange.connect( + this, + &CaptureManagerTest::OnCapturerStateChange); + } + void PopulateSupportedFormats() { + std::vector formats; + for (int i = 0; i < ARRAY_SIZE(kCameraFormats); ++i) { + formats.push_back(cricket::VideoFormat(kCameraFormats[i])); + } + video_capturer_.ResetSupportedFormats(formats); + } + int NumFramesProcessed() { return media_processor_.video_frame_count(); } + int NumFramesRendered() { return video_renderer_.num_rendered_frames(); } + bool WasRenderedResolution(cricket::VideoFormat format) { + return format.width == video_renderer_.width() && + format.height == video_renderer_.height(); + } + cricket::CaptureState capture_state() { return capture_state_; } + int callback_count() { return callback_count_; } + void OnCapturerStateChange(cricket::VideoCapturer* capturer, + cricket::CaptureState capture_state) { + capture_state_ = capture_state; + ++callback_count_; + } + + protected: + cricket::FakeMediaProcessor media_processor_; + cricket::FakeVideoCapturer video_capturer_; + cricket::FakeVideoRenderer video_renderer_; + + cricket::CaptureManager capture_manager_; + + cricket::CaptureState capture_state_; + int callback_count_; + cricket::VideoFormat format_vga_; + cricket::VideoFormat format_qvga_; +}; + +// Incorrect use cases. +TEST_F(CaptureManagerTest, InvalidCallOrder) { + // Capturer must be registered before any of these calls. + EXPECT_FALSE(capture_manager_.AddVideoRenderer(&video_capturer_, + &video_renderer_)); + EXPECT_FALSE(capture_manager_.AddVideoProcessor(&video_capturer_, + &media_processor_)); +} + +TEST_F(CaptureManagerTest, InvalidAddingRemoving) { + EXPECT_FALSE(capture_manager_.StopVideoCapture(&video_capturer_, + cricket::VideoFormat())); + EXPECT_TRUE(capture_manager_.StartVideoCapture(&video_capturer_, + format_vga_)); + EXPECT_EQ_WAIT(cricket::CS_RUNNING, capture_state(), kMsCallbackWait); + EXPECT_EQ(1, callback_count()); + EXPECT_FALSE(capture_manager_.AddVideoRenderer(&video_capturer_, NULL)); + EXPECT_FALSE(capture_manager_.RemoveVideoRenderer(&video_capturer_, + &video_renderer_)); + EXPECT_FALSE(capture_manager_.AddVideoProcessor(&video_capturer_, + NULL)); + EXPECT_FALSE(capture_manager_.RemoveVideoProcessor(&video_capturer_, + &media_processor_)); + EXPECT_TRUE(capture_manager_.StopVideoCapture(&video_capturer_, format_vga_)); +} + +// Valid use cases +TEST_F(CaptureManagerTest, ProcessorTest) { + EXPECT_TRUE(capture_manager_.StartVideoCapture(&video_capturer_, + format_vga_)); + EXPECT_EQ_WAIT(cricket::CS_RUNNING, capture_state(), kMsCallbackWait); + EXPECT_EQ(1, callback_count()); + EXPECT_TRUE(capture_manager_.AddVideoRenderer(&video_capturer_, + &video_renderer_)); + EXPECT_TRUE(capture_manager_.AddVideoProcessor(&video_capturer_, + &media_processor_)); + EXPECT_TRUE(video_capturer_.CaptureFrame()); + EXPECT_EQ(1, NumFramesProcessed()); + EXPECT_EQ(1, NumFramesRendered()); + EXPECT_TRUE(capture_manager_.RemoveVideoProcessor(&video_capturer_, + &media_processor_)); + // Processor has been removed so no more frames should be processed. + EXPECT_TRUE(video_capturer_.CaptureFrame()); + EXPECT_EQ(1, NumFramesProcessed()); + EXPECT_EQ(2, NumFramesRendered()); + EXPECT_TRUE(capture_manager_.StopVideoCapture(&video_capturer_, format_vga_)); + EXPECT_EQ(2, callback_count()); +} + +TEST_F(CaptureManagerTest, KeepFirstResolutionHigh) { + EXPECT_TRUE(capture_manager_.StartVideoCapture(&video_capturer_, + format_vga_)); + EXPECT_EQ_WAIT(cricket::CS_RUNNING, capture_state(), kMsCallbackWait); + EXPECT_EQ(1, callback_count()); + EXPECT_TRUE(capture_manager_.AddVideoRenderer(&video_capturer_, + &video_renderer_)); + EXPECT_TRUE(video_capturer_.CaptureFrame()); + EXPECT_EQ(1, NumFramesRendered()); + // Renderer should be fed frames with the resolution of format_vga_. + EXPECT_TRUE(WasRenderedResolution(format_vga_)); + + // Start again with one more format. + EXPECT_TRUE(capture_manager_.StartVideoCapture(&video_capturer_, + format_qvga_)); + // Existing renderers should be fed frames with the resolution of format_vga_. + EXPECT_TRUE(video_capturer_.CaptureFrame()); + EXPECT_TRUE(WasRenderedResolution(format_vga_)); + EXPECT_TRUE(capture_manager_.StopVideoCapture(&video_capturer_, format_vga_)); + EXPECT_TRUE(capture_manager_.StopVideoCapture(&video_capturer_, + format_qvga_)); + EXPECT_FALSE(capture_manager_.StopVideoCapture(&video_capturer_, + format_vga_)); + EXPECT_FALSE(capture_manager_.StopVideoCapture(&video_capturer_, + format_qvga_)); +} + +// Should pick the lowest resolution as the highest resolution is not chosen +// until after capturing has started. This ensures that no particular resolution +// is favored over others. +TEST_F(CaptureManagerTest, KeepFirstResolutionLow) { + EXPECT_TRUE(capture_manager_.StartVideoCapture(&video_capturer_, + format_qvga_)); + EXPECT_TRUE(capture_manager_.StartVideoCapture(&video_capturer_, + format_vga_)); + EXPECT_TRUE(capture_manager_.AddVideoRenderer(&video_capturer_, + &video_renderer_)); + EXPECT_EQ_WAIT(1, callback_count(), kMsCallbackWait); + EXPECT_TRUE(video_capturer_.CaptureFrame()); + EXPECT_EQ(1, NumFramesRendered()); + EXPECT_TRUE(WasRenderedResolution(format_qvga_)); +} + +// Ensure that the reference counting is working when multiple start and +// multiple stop calls are made. +TEST_F(CaptureManagerTest, MultipleStartStops) { + EXPECT_TRUE(capture_manager_.StartVideoCapture(&video_capturer_, + format_vga_)); + // Add video capturer but with different format. + EXPECT_TRUE(capture_manager_.StartVideoCapture(&video_capturer_, + format_qvga_)); + EXPECT_EQ_WAIT(cricket::CS_RUNNING, capture_state(), kMsCallbackWait); + EXPECT_EQ(1, callback_count()); + EXPECT_TRUE(capture_manager_.AddVideoRenderer(&video_capturer_, + &video_renderer_)); + // Ensure that a frame can be captured when two start calls have been made. + EXPECT_TRUE(video_capturer_.CaptureFrame()); + EXPECT_EQ(1, NumFramesRendered()); + + EXPECT_TRUE(capture_manager_.StopVideoCapture(&video_capturer_, format_vga_)); + // Video should still render since there has been two start calls but only + // one stop call. + EXPECT_TRUE(video_capturer_.CaptureFrame()); + EXPECT_EQ(2, NumFramesRendered()); + + EXPECT_TRUE(capture_manager_.StopVideoCapture(&video_capturer_, + format_qvga_)); + EXPECT_EQ_WAIT(cricket::CS_STOPPED, capture_state(), kMsCallbackWait); + EXPECT_EQ(2, callback_count()); + // Last stop call should fail as it is one more than the number of start + // calls. + EXPECT_FALSE(capture_manager_.StopVideoCapture(&video_capturer_, + format_vga_)); +} + +TEST_F(CaptureManagerTest, TestForceRestart) { + EXPECT_TRUE(capture_manager_.StartVideoCapture(&video_capturer_, + format_qvga_)); + EXPECT_TRUE(capture_manager_.AddVideoRenderer(&video_capturer_, + &video_renderer_)); + EXPECT_EQ_WAIT(1, callback_count(), kMsCallbackWait); + EXPECT_TRUE(video_capturer_.CaptureFrame()); + EXPECT_EQ(1, NumFramesRendered()); + EXPECT_TRUE(WasRenderedResolution(format_qvga_)); + // Now restart with vga. + EXPECT_TRUE(capture_manager_.RestartVideoCapture( + &video_capturer_, format_qvga_, format_vga_, + cricket::CaptureManager::kForceRestart)); + EXPECT_TRUE(video_capturer_.CaptureFrame()); + EXPECT_EQ(2, NumFramesRendered()); + EXPECT_TRUE(WasRenderedResolution(format_vga_)); +} + +TEST_F(CaptureManagerTest, TestRequestRestart) { + EXPECT_TRUE(capture_manager_.StartVideoCapture(&video_capturer_, + format_vga_)); + EXPECT_TRUE(capture_manager_.AddVideoRenderer(&video_capturer_, + &video_renderer_)); + EXPECT_EQ_WAIT(1, callback_count(), kMsCallbackWait); + EXPECT_TRUE(video_capturer_.CaptureFrame()); + EXPECT_EQ(1, NumFramesRendered()); + EXPECT_TRUE(WasRenderedResolution(format_vga_)); + // Now request restart with qvga. + EXPECT_TRUE(capture_manager_.RestartVideoCapture( + &video_capturer_, format_vga_, format_qvga_, + cricket::CaptureManager::kRequestRestart)); + EXPECT_TRUE(video_capturer_.CaptureFrame()); + EXPECT_EQ(2, NumFramesRendered()); + EXPECT_TRUE(WasRenderedResolution(format_vga_)); +} diff --git a/talk/media/base/capturerenderadapter.cc b/talk/media/base/capturerenderadapter.cc new file mode 100644 index 000000000..8fbf34ebb --- /dev/null +++ b/talk/media/base/capturerenderadapter.cc @@ -0,0 +1,137 @@ +/* + * libjingle + * Copyright 2012, 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/media/base/capturerenderadapter.h" + +#include "talk/base/logging.h" +#include "talk/media/base/videocapturer.h" +#include "talk/media/base/videoprocessor.h" +#include "talk/media/base/videorenderer.h" + +namespace cricket { + +CaptureRenderAdapter::CaptureRenderAdapter(VideoCapturer* video_capturer) + : video_capturer_(video_capturer) { +} + +CaptureRenderAdapter::~CaptureRenderAdapter() { + // Have to disconnect here since |video_capturer_| lives on past the + // destruction of this object. + if (video_capturer_) { + video_capturer_->SignalVideoFrame.disconnect(this); + } +} + +CaptureRenderAdapter* CaptureRenderAdapter::Create( + VideoCapturer* video_capturer) { + if (!video_capturer) { + return NULL; + } + CaptureRenderAdapter* return_value = new CaptureRenderAdapter(video_capturer); + return_value->Init(); // Can't fail. + return return_value; +} + +bool CaptureRenderAdapter::AddRenderer(VideoRenderer* video_renderer) { + if (!video_renderer) { + return false; + } + talk_base::CritScope cs(&capture_crit_); + if (IsRendererRegistered(*video_renderer)) { + return false; + } + video_renderers_.push_back(VideoRendererInfo(video_renderer)); + return true; +} + +bool CaptureRenderAdapter::RemoveRenderer(VideoRenderer* video_renderer) { + if (!video_renderer) { + return false; + } + talk_base::CritScope cs(&capture_crit_); + for (VideoRenderers::iterator iter = video_renderers_.begin(); + iter != video_renderers_.end(); ++iter) { + if (video_renderer == iter->renderer) { + video_renderers_.erase(iter); + return true; + } + } + return false; +} + +void CaptureRenderAdapter::Init() { + video_capturer_->SignalVideoFrame.connect( + this, + &CaptureRenderAdapter::OnVideoFrame); +} + +void CaptureRenderAdapter::OnVideoFrame(VideoCapturer* capturer, + const VideoFrame* video_frame) { + talk_base::CritScope cs(&capture_crit_); + if (video_renderers_.empty()) { + return; + } + MaybeSetRenderingSize(video_frame); + + for (VideoRenderers::iterator iter = video_renderers_.begin(); + iter != video_renderers_.end(); ++iter) { + VideoRenderer* video_renderer = iter->renderer; + video_renderer->RenderFrame(video_frame); + } +} + +// The renderer_crit_ lock needs to be taken when calling this function. +void CaptureRenderAdapter::MaybeSetRenderingSize(const VideoFrame* frame) { + for (VideoRenderers::iterator iter = video_renderers_.begin(); + iter != video_renderers_.end(); ++iter) { + const bool new_resolution = iter->render_width != frame->GetWidth() || + iter->render_height != frame->GetHeight(); + if (new_resolution) { + if (iter->renderer->SetSize(frame->GetWidth(), frame->GetHeight(), 0)) { + iter->render_width = frame->GetWidth(); + iter->render_height = frame->GetHeight(); + } else { + LOG(LS_ERROR) << "Captured frame size not supported by renderer: " << + frame->GetWidth() << " x " << frame->GetHeight(); + } + } + } +} + +// The renderer_crit_ lock needs to be taken when calling this function. +bool CaptureRenderAdapter::IsRendererRegistered( + const VideoRenderer& video_renderer) const { + for (VideoRenderers::const_iterator iter = video_renderers_.begin(); + iter != video_renderers_.end(); ++iter) { + if (&video_renderer == iter->renderer) { + return true; + } + } + return false; +} + +} // namespace cricket diff --git a/talk/media/base/capturerenderadapter.h b/talk/media/base/capturerenderadapter.h new file mode 100644 index 000000000..1df9131d6 --- /dev/null +++ b/talk/media/base/capturerenderadapter.h @@ -0,0 +1,91 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +// This file contains the class CaptureRenderAdapter. The class connects a +// VideoCapturer to any number of VideoRenders such that the former feeds the +// latter. +// CaptureRenderAdapter is Thread-unsafe. This means that none of its APIs may +// be called concurrently. + +#ifndef TALK_MEDIA_BASE_CAPTURERENDERADAPTER_H_ +#define TALK_MEDIA_BASE_CAPTURERENDERADAPTER_H_ + +#include + +#include "talk/base/criticalsection.h" +#include "talk/base/sigslot.h" +#include "talk/media/base/videocapturer.h" + +namespace cricket { + +class VideoCapturer; +class VideoProcessor; +class VideoRenderer; + +class CaptureRenderAdapter : public sigslot::has_slots<> { + public: + static CaptureRenderAdapter* Create(VideoCapturer* video_capturer); + ~CaptureRenderAdapter(); + + bool AddRenderer(VideoRenderer* video_renderer); + bool RemoveRenderer(VideoRenderer* video_renderer); + + VideoCapturer* video_capturer() { return video_capturer_; } + private: + struct VideoRendererInfo { + explicit VideoRendererInfo(VideoRenderer* r) + : renderer(r), + render_width(0), + render_height(0) { + } + VideoRenderer* renderer; + size_t render_width; + size_t render_height; + }; + + // Just pointers since ownership is not handed over to this class. + typedef std::vector VideoRenderers; + + explicit CaptureRenderAdapter(VideoCapturer* video_capturer); + void Init(); + + // Callback for frames received from the capturer. + void OnVideoFrame(VideoCapturer* capturer, const VideoFrame* video_frame); + + void MaybeSetRenderingSize(const VideoFrame* frame); + + bool IsRendererRegistered(const VideoRenderer& video_renderer) const; + + VideoRenderers video_renderers_; + VideoCapturer* video_capturer_; + // Critical section synchronizing the capture thread. + mutable talk_base::CriticalSection capture_crit_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_CAPTURERENDERADAPTER_H_ diff --git a/talk/media/base/codec.cc b/talk/media/base/codec.cc new file mode 100644 index 000000000..2d54c9907 --- /dev/null +++ b/talk/media/base/codec.cc @@ -0,0 +1,169 @@ +/* + * libjingle + * Copyright 2004 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/media/base/codec.h" + +#include +#include + +#include "talk/base/common.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" + +namespace cricket { + +static const int kMaxStaticPayloadId = 95; + +bool FeedbackParam::operator==(const FeedbackParam& other) const { + return _stricmp(other.id().c_str(), id().c_str()) == 0 && + _stricmp(other.param().c_str(), param().c_str()) == 0; +} + +bool FeedbackParams::operator==(const FeedbackParams& other) const { + return params_ == other.params_; +} + +bool FeedbackParams::Has(const FeedbackParam& param) const { + return std::find(params_.begin(), params_.end(), param) != params_.end(); +} + +void FeedbackParams::Add(const FeedbackParam& param) { + if (param.id().empty()) { + return; + } + if (Has(param)) { + // Param already in |this|. + return; + } + params_.push_back(param); + ASSERT(!HasDuplicateEntries()); +} + +void FeedbackParams::Intersect(const FeedbackParams& from) { + std::vector::iterator iter_to = params_.begin(); + while (iter_to != params_.end()) { + if (!from.Has(*iter_to)) { + iter_to = params_.erase(iter_to); + } else { + ++iter_to; + } + } +} + +bool FeedbackParams::HasDuplicateEntries() const { + for (std::vector::const_iterator iter = params_.begin(); + iter != params_.end(); ++iter) { + for (std::vector::const_iterator found = iter + 1; + found != params_.end(); ++found) { + if (*found == *iter) { + return true; + } + } + } + return false; +} + +bool Codec::Matches(const Codec& codec) const { + // Match the codec id/name based on the typical static/dynamic name rules. + // Matching is case-insensitive. + return (codec.id <= kMaxStaticPayloadId) ? + (id == codec.id) : (_stricmp(name.c_str(), codec.name.c_str()) == 0); +} + +bool Codec::GetParam(const std::string& name, std::string* out) const { + CodecParameterMap::const_iterator iter = params.find(name); + if (iter == params.end()) + return false; + *out = iter->second; + return true; +} + +bool Codec::GetParam(const std::string& name, int* out) const { + CodecParameterMap::const_iterator iter = params.find(name); + if (iter == params.end()) + return false; + return talk_base::FromString(iter->second, out); +} + +void Codec::SetParam(const std::string& name, const std::string& value) { + params[name] = value; +} + +void Codec::SetParam(const std::string& name, int value) { + params[name] = talk_base::ToString(value); +} + +void Codec::AddFeedbackParam(const FeedbackParam& param) { + feedback_params.Add(param); +} + +bool Codec::HasFeedbackParam(const FeedbackParam& param) const { + return feedback_params.Has(param); +} + +void Codec::IntersectFeedbackParams(const Codec& other) { + feedback_params.Intersect(other.feedback_params); +} + +bool AudioCodec::Matches(const AudioCodec& codec) const { + // If a nonzero clockrate is specified, it must match the actual clockrate. + // If a nonzero bitrate is specified, it must match the actual bitrate, + // unless the codec is VBR (0), where we just force the supplied value. + // The number of channels must match exactly, with the exception + // that channels=0 is treated synonymously as channels=1, per RFC + // 4566 section 6: " [The channels] parameter is OPTIONAL and may be + // omitted if the number of channels is one." + // Preference is ignored. + // TODO(juberti): Treat a zero clockrate as 8000Hz, the RTP default clockrate. + return Codec::Matches(codec) && + ((codec.clockrate == 0 /*&& clockrate == 8000*/) || + clockrate == codec.clockrate) && + (codec.bitrate == 0 || bitrate <= 0 || bitrate == codec.bitrate) && + ((codec.channels < 2 && channels < 2) || channels == codec.channels); +} + +std::string AudioCodec::ToString() const { + std::ostringstream os; + os << "AudioCodec[" << id << ":" << name << ":" << clockrate << ":" << bitrate + << ":" << channels << ":" << preference << "]"; + return os.str(); +} + +std::string VideoCodec::ToString() const { + std::ostringstream os; + os << "VideoCodec[" << id << ":" << name << ":" << width << ":" << height + << ":" << framerate << ":" << preference << "]"; + return os.str(); +} + +std::string DataCodec::ToString() const { + std::ostringstream os; + os << "DataCodec[" << id << ":" << name << "]"; + return os.str(); +} + +} // namespace cricket diff --git a/talk/media/base/codec.h b/talk/media/base/codec.h new file mode 100644 index 000000000..d7030017c --- /dev/null +++ b/talk/media/base/codec.h @@ -0,0 +1,301 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_CODEC_H_ +#define TALK_MEDIA_BASE_CODEC_H_ + +#include +#include +#include +#include + +#include "talk/media/base/constants.h" + +namespace cricket { + +typedef std::map CodecParameterMap; + +class FeedbackParam { + public: + FeedbackParam(const std::string& id, const std::string& param) + : id_(id), + param_(param) { + } + bool operator==(const FeedbackParam& other) const; + + const std::string& id() const { return id_; } + const std::string& param() const { return param_; } + + private: + std::string id_; // e.g. "nack", "ccm" + std::string param_; // e.g. "", "rpsi", "fir" +}; + +class FeedbackParams { + public: + bool operator==(const FeedbackParams& other) const; + + bool Has(const FeedbackParam& param) const; + void Add(const FeedbackParam& param); + + void Intersect(const FeedbackParams& from); + + const std::vector& params() const { return params_; } + private: + bool HasDuplicateEntries() const; + + std::vector params_; +}; + +struct Codec { + int id; + std::string name; + int clockrate; + int preference; + CodecParameterMap params; + FeedbackParams feedback_params; + + // Creates a codec with the given parameters. + Codec(int id, const std::string& name, int clockrate, int preference) + : id(id), + name(name), + clockrate(clockrate), + preference(preference) { + } + + // Creates an empty codec. + Codec() : id(0), clockrate(0), preference(0) {} + + // Indicates if this codec is compatible with the specified codec. + bool Matches(const Codec& codec) const; + + // Find the parameter for |name| and write the value to |out|. + bool GetParam(const std::string& name, std::string* out) const; + bool GetParam(const std::string& name, int* out) const; + + void SetParam(const std::string& name, const std::string& value); + void SetParam(const std::string& name, int value); + + bool HasFeedbackParam(const FeedbackParam& param) const; + void AddFeedbackParam(const FeedbackParam& param); + + static bool Preferable(const Codec& first, const Codec& other) { + return first.preference > other.preference; + } + + // Filter |this| feedbacks params such that only those shared by both |this| + // and |other| are kept. + void IntersectFeedbackParams(const Codec& other); + + Codec& operator=(const Codec& c) { + this->id = c.id; // id is reserved in objective-c + name = c.name; + clockrate = c.clockrate; + preference = c.preference; + return *this; + } + + bool operator==(const Codec& c) const { + return this->id == c.id && // id is reserved in objective-c + name == c.name && + clockrate == c.clockrate && + preference == c.preference; + } + + bool operator!=(const Codec& c) const { + return !(*this == c); + } +}; + +struct AudioCodec : public Codec { + int bitrate; + int channels; + + // Creates a codec with the given parameters. + AudioCodec(int pt, const std::string& nm, int cr, int br, int cs, int pr) + : Codec(pt, nm, cr, pr), + bitrate(br), + channels(cs) { + } + + // Creates an empty codec. + AudioCodec() : Codec(), bitrate(0), channels(0) {} + + // Indicates if this codec is compatible with the specified codec. + bool Matches(const AudioCodec& codec) const; + + static bool Preferable(const AudioCodec& first, const AudioCodec& other) { + return first.preference > other.preference; + } + + std::string ToString() const; + + AudioCodec& operator=(const AudioCodec& c) { + this->id = c.id; // id is reserved in objective-c + name = c.name; + clockrate = c.clockrate; + bitrate = c.bitrate; + channels = c.channels; + preference = c.preference; + params = c.params; + feedback_params = c.feedback_params; + return *this; + } + + bool operator==(const AudioCodec& c) const { + return this->id == c.id && // id is reserved in objective-c + name == c.name && + clockrate == c.clockrate && + bitrate == c.bitrate && + channels == c.channels && + preference == c.preference && + params == c.params && + feedback_params == c.feedback_params; + } + + bool operator!=(const AudioCodec& c) const { + return !(*this == c); + } +}; + +struct VideoCodec : public Codec { + int width; + int height; + int framerate; + + // Creates a codec with the given parameters. + VideoCodec(int pt, const std::string& nm, int w, int h, int fr, int pr) + : Codec(pt, nm, kVideoCodecClockrate, pr), + width(w), + height(h), + framerate(fr) { + } + + // Creates an empty codec. + VideoCodec() + : Codec(), + width(0), + height(0), + framerate(0) { + clockrate = kVideoCodecClockrate; + } + + static bool Preferable(const VideoCodec& first, const VideoCodec& other) { + return first.preference > other.preference; + } + + std::string ToString() const; + + VideoCodec& operator=(const VideoCodec& c) { + this->id = c.id; // id is reserved in objective-c + name = c.name; + clockrate = c.clockrate; + width = c.width; + height = c.height; + framerate = c.framerate; + preference = c.preference; + params = c.params; + feedback_params = c.feedback_params; + return *this; + } + + bool operator==(const VideoCodec& c) const { + return this->id == c.id && // id is reserved in objective-c + name == c.name && + clockrate == c.clockrate && + width == c.width && + height == c.height && + framerate == c.framerate && + preference == c.preference && + params == c.params && + feedback_params == c.feedback_params; + } + + bool operator!=(const VideoCodec& c) const { + return !(*this == c); + } +}; + +struct DataCodec : public Codec { + DataCodec(int id, const std::string& name, int preference) + : Codec(id, name, kDataCodecClockrate, preference) { + } + + DataCodec() : Codec() { + clockrate = kDataCodecClockrate; + } + + std::string ToString() const; +}; + +struct VideoEncoderConfig { + static const int kDefaultMaxThreads = -1; + static const int kDefaultCpuProfile = -1; + + VideoEncoderConfig() + : max_codec(), + num_threads(kDefaultMaxThreads), + cpu_profile(kDefaultCpuProfile) { + } + + VideoEncoderConfig(const VideoCodec& c) + : max_codec(c), + num_threads(kDefaultMaxThreads), + cpu_profile(kDefaultCpuProfile) { + } + + VideoEncoderConfig(const VideoCodec& c, int t, int p) + : max_codec(c), + num_threads(t), + cpu_profile(p) { + } + + VideoEncoderConfig& operator=(const VideoEncoderConfig& config) { + max_codec = config.max_codec; + num_threads = config.num_threads; + cpu_profile = config.cpu_profile; + return *this; + } + + bool operator==(const VideoEncoderConfig& config) const { + return max_codec == config.max_codec && + num_threads == config.num_threads && + cpu_profile == config.cpu_profile; + } + + bool operator!=(const VideoEncoderConfig& config) const { + return !(*this == config); + } + + VideoCodec max_codec; + int num_threads; + int cpu_profile; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_CODEC_H_ diff --git a/talk/media/base/codec_unittest.cc b/talk/media/base/codec_unittest.cc new file mode 100644 index 000000000..f0ffd8f5a --- /dev/null +++ b/talk/media/base/codec_unittest.cc @@ -0,0 +1,294 @@ +/* + * libjingle + * Copyright 2009 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/base/gunit.h" +#include "talk/media/base/codec.h" + +using cricket::AudioCodec; +using cricket::Codec; +using cricket::DataCodec; +using cricket::FeedbackParam; +using cricket::VideoCodec; +using cricket::VideoEncoderConfig; + +class CodecTest : public testing::Test { + public: + CodecTest() {} +}; + +TEST_F(CodecTest, TestAudioCodecOperators) { + AudioCodec c0(96, "A", 44100, 20000, 2, 3); + AudioCodec c1(95, "A", 44100, 20000, 2, 3); + AudioCodec c2(96, "x", 44100, 20000, 2, 3); + AudioCodec c3(96, "A", 48000, 20000, 2, 3); + AudioCodec c4(96, "A", 44100, 10000, 2, 3); + AudioCodec c5(96, "A", 44100, 20000, 1, 3); + AudioCodec c6(96, "A", 44100, 20000, 2, 1); + EXPECT_TRUE(c0 != c1); + EXPECT_TRUE(c0 != c2); + EXPECT_TRUE(c0 != c3); + EXPECT_TRUE(c0 != c4); + EXPECT_TRUE(c0 != c5); + EXPECT_TRUE(c0 != c6); + + AudioCodec c7; + AudioCodec c8(0, "", 0, 0, 0, 0); + AudioCodec c9 = c0; + EXPECT_TRUE(c8 == c7); + EXPECT_TRUE(c9 != c7); + EXPECT_TRUE(c9 == c0); + + AudioCodec c10(c0); + AudioCodec c11(c0); + AudioCodec c12(c0); + AudioCodec c13(c0); + c10.params["x"] = "abc"; + c11.params["x"] = "def"; + c12.params["y"] = "abc"; + c13.params["x"] = "abc"; + EXPECT_TRUE(c10 != c0); + EXPECT_TRUE(c11 != c0); + EXPECT_TRUE(c11 != c10); + EXPECT_TRUE(c12 != c0); + EXPECT_TRUE(c12 != c10); + EXPECT_TRUE(c12 != c11); + EXPECT_TRUE(c13 == c10); +} + +TEST_F(CodecTest, TestAudioCodecMatches) { + // Test a codec with a static payload type. + AudioCodec c0(95, "A", 44100, 20000, 1, 3); + EXPECT_TRUE(c0.Matches(AudioCodec(95, "", 44100, 20000, 1, 0))); + EXPECT_TRUE(c0.Matches(AudioCodec(95, "", 44100, 20000, 0, 0))); + EXPECT_TRUE(c0.Matches(AudioCodec(95, "", 44100, 0, 0, 0))); + EXPECT_TRUE(c0.Matches(AudioCodec(95, "", 0, 0, 0, 0))); + EXPECT_FALSE(c0.Matches(AudioCodec(96, "", 44100, 20000, 1, 0))); + EXPECT_FALSE(c0.Matches(AudioCodec(95, "", 55100, 20000, 1, 0))); + EXPECT_FALSE(c0.Matches(AudioCodec(95, "", 44100, 30000, 1, 0))); + EXPECT_FALSE(c0.Matches(AudioCodec(95, "", 44100, 20000, 2, 0))); + EXPECT_FALSE(c0.Matches(AudioCodec(95, "", 55100, 30000, 2, 0))); + + // Test a codec with a dynamic payload type. + AudioCodec c1(96, "A", 44100, 20000, 1, 3); + EXPECT_TRUE(c1.Matches(AudioCodec(96, "A", 0, 0, 0, 0))); + EXPECT_TRUE(c1.Matches(AudioCodec(97, "A", 0, 0, 0, 0))); + EXPECT_TRUE(c1.Matches(AudioCodec(96, "a", 0, 0, 0, 0))); + EXPECT_TRUE(c1.Matches(AudioCodec(97, "a", 0, 0, 0, 0))); + EXPECT_FALSE(c1.Matches(AudioCodec(95, "A", 0, 0, 0, 0))); + EXPECT_FALSE(c1.Matches(AudioCodec(96, "", 44100, 20000, 2, 0))); + EXPECT_FALSE(c1.Matches(AudioCodec(96, "A", 55100, 30000, 1, 0))); + + // Test a codec with a dynamic payload type, and auto bitrate. + AudioCodec c2(97, "A", 16000, 0, 1, 3); + // Use default bitrate. + EXPECT_TRUE(c2.Matches(AudioCodec(97, "A", 16000, 0, 1, 0))); + EXPECT_TRUE(c2.Matches(AudioCodec(97, "A", 16000, 0, 0, 0))); + // Use explicit bitrate. + EXPECT_TRUE(c2.Matches(AudioCodec(97, "A", 16000, 32000, 1, 0))); + // Backward compatibility with clients that might send "-1" (for default). + EXPECT_TRUE(c2.Matches(AudioCodec(97, "A", 16000, -1, 1, 0))); + + // Stereo doesn't match channels = 0. + AudioCodec c3(96, "A", 44100, 20000, 2, 3); + EXPECT_TRUE(c3.Matches(AudioCodec(96, "A", 44100, 20000, 2, 3))); + EXPECT_FALSE(c3.Matches(AudioCodec(96, "A", 44100, 20000, 1, 3))); + EXPECT_FALSE(c3.Matches(AudioCodec(96, "A", 44100, 20000, 0, 3))); +} + +TEST_F(CodecTest, TestVideoCodecOperators) { + VideoCodec c0(96, "V", 320, 200, 30, 3); + VideoCodec c1(95, "V", 320, 200, 30, 3); + VideoCodec c2(96, "x", 320, 200, 30, 3); + VideoCodec c3(96, "V", 120, 200, 30, 3); + VideoCodec c4(96, "V", 320, 100, 30, 3); + VideoCodec c5(96, "V", 320, 200, 10, 3); + VideoCodec c6(96, "V", 320, 200, 30, 1); + EXPECT_TRUE(c0 != c1); + EXPECT_TRUE(c0 != c2); + EXPECT_TRUE(c0 != c3); + EXPECT_TRUE(c0 != c4); + EXPECT_TRUE(c0 != c5); + EXPECT_TRUE(c0 != c6); + + VideoCodec c7; + VideoCodec c8(0, "", 0, 0, 0, 0); + VideoCodec c9 = c0; + EXPECT_TRUE(c8 == c7); + EXPECT_TRUE(c9 != c7); + EXPECT_TRUE(c9 == c0); + + VideoCodec c10(c0); + VideoCodec c11(c0); + VideoCodec c12(c0); + VideoCodec c13(c0); + c10.params["x"] = "abc"; + c11.params["x"] = "def"; + c12.params["y"] = "abc"; + c13.params["x"] = "abc"; + EXPECT_TRUE(c10 != c0); + EXPECT_TRUE(c11 != c0); + EXPECT_TRUE(c11 != c10); + EXPECT_TRUE(c12 != c0); + EXPECT_TRUE(c12 != c10); + EXPECT_TRUE(c12 != c11); + EXPECT_TRUE(c13 == c10); +} + +TEST_F(CodecTest, TestVideoCodecMatches) { + // Test a codec with a static payload type. + VideoCodec c0(95, "V", 320, 200, 30, 3); + EXPECT_TRUE(c0.Matches(VideoCodec(95, "", 640, 400, 15, 0))); + EXPECT_FALSE(c0.Matches(VideoCodec(96, "", 320, 200, 30, 0))); + + // Test a codec with a dynamic payload type. + VideoCodec c1(96, "V", 320, 200, 30, 3); + EXPECT_TRUE(c1.Matches(VideoCodec(96, "V", 640, 400, 15, 0))); + EXPECT_TRUE(c1.Matches(VideoCodec(97, "V", 640, 400, 15, 0))); + EXPECT_TRUE(c1.Matches(VideoCodec(96, "v", 640, 400, 15, 0))); + EXPECT_TRUE(c1.Matches(VideoCodec(97, "v", 640, 400, 15, 0))); + EXPECT_FALSE(c1.Matches(VideoCodec(96, "", 320, 200, 30, 0))); + EXPECT_FALSE(c1.Matches(VideoCodec(95, "V", 640, 400, 15, 0))); +} + +TEST_F(CodecTest, TestVideoEncoderConfigOperators) { + VideoEncoderConfig c1(VideoCodec( + 96, "SVC", 320, 200, 30, 3), 1, 2); + VideoEncoderConfig c2(VideoCodec( + 95, "SVC", 320, 200, 30, 3), 1, 2); + VideoEncoderConfig c3(VideoCodec( + 96, "xxx", 320, 200, 30, 3), 1, 2); + VideoEncoderConfig c4(VideoCodec( + 96, "SVC", 120, 200, 30, 3), 1, 2); + VideoEncoderConfig c5(VideoCodec( + 96, "SVC", 320, 100, 30, 3), 1, 2); + VideoEncoderConfig c6(VideoCodec( + 96, "SVC", 320, 200, 10, 3), 1, 2); + VideoEncoderConfig c7(VideoCodec( + 96, "SVC", 320, 200, 30, 1), 1, 2); + VideoEncoderConfig c8(VideoCodec( + 96, "SVC", 320, 200, 30, 3), 0, 2); + VideoEncoderConfig c9(VideoCodec( + 96, "SVC", 320, 200, 30, 3), 1, 1); + EXPECT_TRUE(c1 != c2); + EXPECT_TRUE(c1 != c2); + EXPECT_TRUE(c1 != c3); + EXPECT_TRUE(c1 != c4); + EXPECT_TRUE(c1 != c5); + EXPECT_TRUE(c1 != c6); + EXPECT_TRUE(c1 != c7); + EXPECT_TRUE(c1 != c8); + EXPECT_TRUE(c1 != c9); + + VideoEncoderConfig c10; + VideoEncoderConfig c11(VideoCodec( + 0, "", 0, 0, 0, 0)); + VideoEncoderConfig c12(VideoCodec( + 0, "", 0, 0, 0, 0), + VideoEncoderConfig::kDefaultMaxThreads, + VideoEncoderConfig::kDefaultCpuProfile); + VideoEncoderConfig c13 = c1; + VideoEncoderConfig c14(VideoCodec( + 0, "", 0, 0, 0, 0), 0, 0); + + EXPECT_TRUE(c11 == c10); + EXPECT_TRUE(c12 == c10); + EXPECT_TRUE(c13 != c10); + EXPECT_TRUE(c13 == c1); + EXPECT_TRUE(c14 != c11); + EXPECT_TRUE(c14 != c12); +} + +TEST_F(CodecTest, TestDataCodecMatches) { + // Test a codec with a static payload type. + DataCodec c0(95, "D", 0); + EXPECT_TRUE(c0.Matches(DataCodec(95, "", 0))); + EXPECT_FALSE(c0.Matches(DataCodec(96, "", 0))); + + // Test a codec with a dynamic payload type. + DataCodec c1(96, "D", 3); + EXPECT_TRUE(c1.Matches(DataCodec(96, "D", 0))); + EXPECT_TRUE(c1.Matches(DataCodec(97, "D", 0))); + EXPECT_TRUE(c1.Matches(DataCodec(96, "d", 0))); + EXPECT_TRUE(c1.Matches(DataCodec(97, "d", 0))); + EXPECT_FALSE(c1.Matches(DataCodec(96, "", 0))); + EXPECT_FALSE(c1.Matches(DataCodec(95, "D", 0))); +} + +TEST_F(CodecTest, TestDataCodecOperators) { + DataCodec c0(96, "D", 3); + DataCodec c1(95, "D", 3); + DataCodec c2(96, "x", 3); + DataCodec c3(96, "D", 1); + EXPECT_TRUE(c0 != c1); + EXPECT_TRUE(c0 != c2); + EXPECT_TRUE(c0 != c3); + + DataCodec c4; + DataCodec c5(0, "", 0); + DataCodec c6 = c0; + EXPECT_TRUE(c5 == c4); + EXPECT_TRUE(c6 != c4); + EXPECT_TRUE(c6 == c0); +} + +TEST_F(CodecTest, TestSetParamAndGetParam) { + AudioCodec codec; + codec.SetParam("a", "1"); + codec.SetParam("b", "x"); + + int int_value = 0; + EXPECT_TRUE(codec.GetParam("a", &int_value)); + EXPECT_EQ(1, int_value); + EXPECT_FALSE(codec.GetParam("b", &int_value)); + EXPECT_FALSE(codec.GetParam("c", &int_value)); + + std::string str_value; + EXPECT_TRUE(codec.GetParam("a", &str_value)); + EXPECT_EQ("1", str_value); + EXPECT_TRUE(codec.GetParam("b", &str_value)); + EXPECT_EQ("x", str_value); + EXPECT_FALSE(codec.GetParam("c", &str_value)); +} + +TEST_F(CodecTest, TestIntersectFeedbackParams) { + const FeedbackParam a1("a", "1"); + const FeedbackParam b2("b", "2"); + const FeedbackParam b3("b", "3"); + const FeedbackParam c3("c", "3"); + Codec c1; + c1.AddFeedbackParam(a1); // Only match with c2. + c1.AddFeedbackParam(b2); // Same param different values. + c1.AddFeedbackParam(c3); // Not in c2. + Codec c2; + c2.AddFeedbackParam(a1); + c2.AddFeedbackParam(b3); + + c1.IntersectFeedbackParams(c2); + EXPECT_TRUE(c1.HasFeedbackParam(a1)); + EXPECT_FALSE(c1.HasFeedbackParam(b2)); + EXPECT_FALSE(c1.HasFeedbackParam(c3)); +} diff --git a/talk/media/base/constants.cc b/talk/media/base/constants.cc new file mode 100644 index 000000000..8fd1bdc33 --- /dev/null +++ b/talk/media/base/constants.cc @@ -0,0 +1,95 @@ +/* + * libjingle + * Copyright 2012 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/media/base/constants.h" + +#include + +namespace cricket { + +const int kVideoCodecClockrate = 90000; +const int kDataCodecClockrate = 90000; +const int kDataMaxBandwidth = 30720; // bps + +const float kHighSystemCpuThreshold = 0.85f; +const float kLowSystemCpuThreshold = 0.65f; +const float kProcessCpuThreshold = 0.10f; + +const char* kRtxCodecName = "rtx"; + +// RTP payload type is in the 0-127 range. Use 128 to indicate "all" payload +// types. +const int kWildcardPayloadType = -1; + +const char* kCodecParamAssociatedPayloadType = "apt"; + +const char* kOpusCodecName = "opus"; + +const char* kCodecParamPTime = "ptime"; +const char* kCodecParamMaxPTime = "maxptime"; + +const char* kCodecParamMinPTime = "minptime"; +const char* kCodecParamSPropStereo = "sprop-stereo"; +const char* kCodecParamStereo = "stereo"; +const char* kCodecParamUseInbandFec = "useinbandfec"; +const char* kCodecParamSctpProtocol = "protocol"; +const char* kCodecParamSctpStreams = "streams"; + +const char* kParamValueTrue = "1"; +const char* kParamValueEmpty = ""; + +const int kOpusDefaultMaxPTime = 120; +const int kOpusDefaultPTime = 20; +const int kOpusDefaultMinPTime = 3; +const int kOpusDefaultSPropStereo = 0; +const int kOpusDefaultStereo = 0; +const int kOpusDefaultUseInbandFec = 0; + +const int kPreferredMaxPTime = 60; +const int kPreferredMinPTime = 10; +const int kPreferredSPropStereo = 0; +const int kPreferredStereo = 0; +const int kPreferredUseInbandFec = 0; + +const char* kRtcpFbParamNack = "nack"; +const char* kRtcpFbParamRemb = "goog-remb"; + +const char* kRtcpFbParamCcm = "ccm"; +const char* kRtcpFbCcmParamFir = "fir"; +const char* kCodecParamMaxBitrate = "x-google-max-bitrate"; +const char* kCodecParamMinBitrate = "x-google-min-bitrate"; +const char* kCodecParamMaxQuantization = "x-google-max-quantization"; + +const int kGoogleRtpDataCodecId = 101; +const char kGoogleRtpDataCodecName[] = "google-data"; + +const int kGoogleSctpDataCodecId = 108; +const char kGoogleSctpDataCodecName[] = "google-sctp-data"; + +const char kComfortNoiseCodecName[] = "CN"; + +} // namespace cricket diff --git a/talk/media/base/constants.h b/talk/media/base/constants.h new file mode 100644 index 000000000..8df480525 --- /dev/null +++ b/talk/media/base/constants.h @@ -0,0 +1,118 @@ +/* + * libjingle + * Copyright 2012 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. + */ + +#ifndef TALK_MEDIA_BASE_CONSTANTS_H_ +#define TALK_MEDIA_BASE_CONSTANTS_H_ + +#include + +// This file contains constants related to media. + +namespace cricket { + +extern const int kVideoCodecClockrate; +extern const int kDataCodecClockrate; +extern const int kDataMaxBandwidth; // bps + +// Default CPU thresholds. +extern const float kHighSystemCpuThreshold; +extern const float kLowSystemCpuThreshold; +extern const float kProcessCpuThreshold; + +extern const char* kRtxCodecName; + +// Codec parameters +extern const int kWildcardPayloadType; +extern const char* kCodecParamAssociatedPayloadType; + +extern const char* kOpusCodecName; + +// Attribute parameters +extern const char* kCodecParamPTime; +extern const char* kCodecParamMaxPTime; +// fmtp parameters +extern const char* kCodecParamMinPTime; +extern const char* kCodecParamSPropStereo; +extern const char* kCodecParamStereo; +extern const char* kCodecParamUseInbandFec; +extern const char* kCodecParamSctpProtocol; +extern const char* kCodecParamSctpStreams; + +extern const char* kParamValueTrue; +// Parameters are stored as parameter/value pairs. For parameters who do not +// have a value, |kParamValueEmpty| should be used as value. +extern const char* kParamValueEmpty; + +// opus parameters. +// Default value for maxptime according to +// http://tools.ietf.org/html/draft-spittka-payload-rtp-opus-03 +extern const int kOpusDefaultMaxPTime; +extern const int kOpusDefaultPTime; +extern const int kOpusDefaultMinPTime; +extern const int kOpusDefaultSPropStereo; +extern const int kOpusDefaultStereo; +extern const int kOpusDefaultUseInbandFec; +// Prefered values in this code base. Note that they may differ from the default +// values in http://tools.ietf.org/html/draft-spittka-payload-rtp-opus-03 +// Only frames larger or equal to 10 ms are currently supported in this code +// base. +extern const int kPreferredMaxPTime; +extern const int kPreferredMinPTime; +extern const int kPreferredSPropStereo; +extern const int kPreferredStereo; +extern const int kPreferredUseInbandFec; + +// rtcp-fb messages according to RFC 4585 +extern const char* kRtcpFbParamNack; +// rtcp-fb messages according to +// http://tools.ietf.org/html/draft-alvestrand-rmcat-remb-00 +extern const char* kRtcpFbParamRemb; +// ccm submessages according to RFC 5104 +extern const char* kRtcpFbParamCcm; +extern const char* kRtcpFbCcmParamFir; +// Google specific parameters +extern const char* kCodecParamMaxBitrate; +extern const char* kCodecParamMinBitrate; +extern const char* kCodecParamMaxQuantization; + +// We put the data codec names here so callers of +// DataEngine::CreateChannel don't have to import rtpdataengine.h or +// sctpdataengine.h to get the codec names they want to pass in. +extern const int kGoogleRtpDataCodecId; +extern const char kGoogleRtpDataCodecName[]; + +// TODO(pthatcher): Find an id that won't conflict with anything. On +// the other hand, it really shouldn't matter since the id won't be +// used on the wire. +extern const int kGoogleSctpDataCodecId; +extern const char kGoogleSctpDataCodecName[]; + +extern const char kComfortNoiseCodecName[]; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_CONSTANTS_H_ diff --git a/talk/media/base/cpuid.cc b/talk/media/base/cpuid.cc new file mode 100644 index 000000000..9fd786588 --- /dev/null +++ b/talk/media/base/cpuid.cc @@ -0,0 +1,88 @@ +/* + * libjingle + * Copyright 2011 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/media/base/cpuid.h" + +#if !defined(DISABLE_YUV) +#include "libyuv/cpu_id.h" +#endif + +namespace cricket { + +bool CpuInfo::TestCpuFlag(int flag) { +#if !defined(DISABLE_YUV) + return libyuv::TestCpuFlag(flag) ? true : false; +#else + return false; +#endif +} + +void CpuInfo::MaskCpuFlagsForTest(int enable_flags) { +#if !defined(DISABLE_YUV) + libyuv::MaskCpuFlags(enable_flags); +#endif +} + +// Detect an Intel Core I5 or better such as 4th generation Macbook Air. +bool IsCoreIOrBetter() { +#if !defined(DISABLE_YUV) && (defined(__i386__) || defined(__x86_64__) || \ + defined(_M_IX86) || defined(_M_X64)) + int cpu_info[4]; + libyuv::CpuId(cpu_info, 0); // Function 0: Vendor ID + if (cpu_info[1] == 0x756e6547 && cpu_info[3] == 0x49656e69 && + cpu_info[2] == 0x6c65746e) { // GenuineIntel + // Detect CPU Family and Model + // 3:0 - Stepping + // 7:4 - Model + // 11:8 - Family + // 13:12 - Processor Type + // 19:16 - Extended Model + // 27:20 - Extended Family + libyuv::CpuId(cpu_info, 1); // Function 1: Family and Model + int family = ((cpu_info[0] >> 8) & 0x0f) | ((cpu_info[0] >> 16) & 0xff0); + int model = ((cpu_info[0] >> 4) & 0x0f) | ((cpu_info[0] >> 12) & 0xf0); + // CpuFamily | CpuModel | Name + // 6 | 14 | Yonah -- Core + // 6 | 15 | Merom -- Core 2 + // 6 | 23 | Penryn -- Core 2 (most common) + // 6 | 26 | Nehalem -- Core i* + // 6 | 28 | Atom + // 6 | 30 | Lynnfield -- Core i* + // 6 | 37 | Westmere -- Core i* + const int kAtom = 28; + const int kCore2 = 23; + if (family < 6 || family == 15 || + (family == 6 && (model == kAtom || model <= kCore2))) { + return false; + } + return true; + } +#endif + return false; +} + +} // namespace cricket diff --git a/talk/media/base/cpuid.h b/talk/media/base/cpuid.h new file mode 100644 index 000000000..3b2aa76c8 --- /dev/null +++ b/talk/media/base/cpuid.h @@ -0,0 +1,76 @@ +/* + * libjingle + * Copyright 2011 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. + */ + +#ifndef TALK_MEDIA_BASE_CPUID_H_ +#define TALK_MEDIA_BASE_CPUID_H_ + +#include "talk/base/basictypes.h" // For DISALLOW_IMPLICIT_CONSTRUCTORS + +namespace cricket { + +class CpuInfo { + public: + // The following flags must match libyuv/cpu_id.h values. + // Internal flag to indicate cpuid requires initialization. + static const int kCpuInit = 0x1; + + // These flags are only valid on ARM processors. + static const int kCpuHasARM = 0x2; + static const int kCpuHasNEON = 0x4; + // 0x8 reserved for future ARM flag. + + // These flags are only valid on x86 processors. + static const int kCpuHasX86 = 0x10; + static const int kCpuHasSSE2 = 0x20; + static const int kCpuHasSSSE3 = 0x40; + static const int kCpuHasSSE41 = 0x80; + static const int kCpuHasSSE42 = 0x100; + static const int kCpuHasAVX = 0x200; + static const int kCpuHasAVX2 = 0x400; + static const int kCpuHasERMS = 0x800; + + // These flags are only valid on MIPS processors. + static const int kCpuHasMIPS = 0x1000; + static const int kCpuHasMIPS_DSP = 0x2000; + static const int kCpuHasMIPS_DSPR2 = 0x4000; + + // Detect CPU has SSE2 etc. + static bool TestCpuFlag(int flag); + + // For testing, allow CPU flags to be disabled. + static void MaskCpuFlagsForTest(int enable_flags); + + private: + DISALLOW_IMPLICIT_CONSTRUCTORS(CpuInfo); +}; + +// Detect an Intel Core I5 or better such as 4th generation Macbook Air. +bool IsCoreIOrBetter(); + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_CPUID_H_ diff --git a/talk/media/base/cpuid_unittest.cc b/talk/media/base/cpuid_unittest.cc new file mode 100644 index 000000000..e8fcc2cde --- /dev/null +++ b/talk/media/base/cpuid_unittest.cc @@ -0,0 +1,74 @@ +/* + * libjingle + * Copyright 2011 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/media/base/cpuid.h" + +#include + +#include "talk/base/basictypes.h" +#include "talk/base/gunit.h" +#include "talk/base/systeminfo.h" + +TEST(CpuInfoTest, CpuId) { + LOG(LS_INFO) << "ARM: " + << cricket::CpuInfo::TestCpuFlag(cricket::CpuInfo::kCpuHasARM); + LOG(LS_INFO) << "NEON: " + << cricket::CpuInfo::TestCpuFlag(cricket::CpuInfo::kCpuHasNEON); + LOG(LS_INFO) << "X86: " + << cricket::CpuInfo::TestCpuFlag(cricket::CpuInfo::kCpuHasX86); + LOG(LS_INFO) << "SSE2: " + << cricket::CpuInfo::TestCpuFlag(cricket::CpuInfo::kCpuHasSSE2); + LOG(LS_INFO) << "SSSE3: " + << cricket::CpuInfo::TestCpuFlag(cricket::CpuInfo::kCpuHasSSSE3); + LOG(LS_INFO) << "SSE41: " + << cricket::CpuInfo::TestCpuFlag(cricket::CpuInfo::kCpuHasSSE41); + LOG(LS_INFO) << "SSE42: " + << cricket::CpuInfo::TestCpuFlag(cricket::CpuInfo::kCpuHasSSE42); + LOG(LS_INFO) << "AVX: " + << cricket::CpuInfo::TestCpuFlag(cricket::CpuInfo::kCpuHasAVX); + bool has_arm = cricket::CpuInfo::TestCpuFlag(cricket::CpuInfo::kCpuHasARM); + bool has_x86 = cricket::CpuInfo::TestCpuFlag(cricket::CpuInfo::kCpuHasX86); + EXPECT_FALSE(has_arm && has_x86); +} + +TEST(CpuInfoTest, IsCoreIOrBetter) { + bool core_i_or_better = cricket::IsCoreIOrBetter(); + // Tests the function is callable. Run on known hardware to confirm. + LOG(LS_INFO) << "IsCoreIOrBetter: " << core_i_or_better; + + // All Core I CPUs have SSE 4.1. + if (core_i_or_better) { + EXPECT_TRUE(cricket::CpuInfo::TestCpuFlag(cricket::CpuInfo::kCpuHasSSE41)); + EXPECT_TRUE(cricket::CpuInfo::TestCpuFlag(cricket::CpuInfo::kCpuHasSSSE3)); + } + + // All CPUs that lack SSE 4.1 are not Core I CPUs. + if (!cricket::CpuInfo::TestCpuFlag(cricket::CpuInfo::kCpuHasSSE41)) { + EXPECT_FALSE(core_i_or_better); + } +} + diff --git a/talk/media/base/cryptoparams.h b/talk/media/base/cryptoparams.h new file mode 100644 index 000000000..9dd1db516 --- /dev/null +++ b/talk/media/base/cryptoparams.h @@ -0,0 +1,54 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_CRYPTOPARAMS_H_ +#define TALK_MEDIA_BASE_CRYPTOPARAMS_H_ + +#include + +namespace cricket { + +// Parameters for SRTP negotiation, as described in RFC 4568. +struct CryptoParams { + CryptoParams() : tag(0) {} + CryptoParams(int t, const std::string& cs, + const std::string& kp, const std::string& sp) + : tag(t), cipher_suite(cs), key_params(kp), session_params(sp) {} + + bool Matches(const CryptoParams& params) const { + return (tag == params.tag && cipher_suite == params.cipher_suite); + } + + int tag; + std::string cipher_suite; + std::string key_params; + std::string session_params; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_CRYPTOPARAMS_H_ diff --git a/talk/media/base/fakecapturemanager.h b/talk/media/base/fakecapturemanager.h new file mode 100644 index 000000000..64c0f521d --- /dev/null +++ b/talk/media/base/fakecapturemanager.h @@ -0,0 +1,52 @@ +/* + * libjingle + * Copyright 2012 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. + */ + +#ifndef TALK_MEDIA_BASE_FAKECAPTUREMANAGER_H_ +#define TALK_MEDIA_BASE_FAKECAPTUREMANAGER_H_ + +#include "talk/media/base/capturemanager.h" + +namespace cricket { + +class FakeCaptureManager : public CaptureManager { + public: + FakeCaptureManager() {} + ~FakeCaptureManager() {} + + virtual bool AddVideoRenderer(VideoCapturer* video_capturer, + VideoRenderer* video_renderer) { + return true; + } + virtual bool RemoveVideoRenderer(VideoCapturer* video_capturer, + VideoRenderer* video_renderer) { + return true; + } +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_FAKECAPTUREMANAGER_H_ diff --git a/talk/media/base/fakemediaengine.h b/talk/media/base/fakemediaengine.h new file mode 100644 index 000000000..f0f83a057 --- /dev/null +++ b/talk/media/base/fakemediaengine.h @@ -0,0 +1,971 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_FAKEMEDIAENGINE_H_ +#define TALK_MEDIA_BASE_FAKEMEDIAENGINE_H_ + +#include +#include +#include +#include +#include + +#include "talk/base/buffer.h" +#include "talk/base/stringutils.h" +#include "talk/media/base/mediaengine.h" +#include "talk/media/base/rtputils.h" +#include "talk/media/base/streamparams.h" +#include "talk/p2p/base/sessiondescription.h" + +namespace cricket { + +class FakeMediaEngine; +class FakeVideoEngine; +class FakeVoiceEngine; + +// A common helper class that handles sending and receiving RTP/RTCP packets. +template class RtpHelper : public Base { + public: + RtpHelper() + : sending_(false), + playout_(false), + fail_set_send_codecs_(false), + fail_set_recv_codecs_(false), + send_ssrc_(0), + ready_to_send_(false) {} + const std::vector& recv_extensions() { + return recv_extensions_; + } + const std::vector& send_extensions() { + return send_extensions_; + } + bool sending() const { return sending_; } + bool playout() const { return playout_; } + const std::list& rtp_packets() const { return rtp_packets_; } + const std::list& rtcp_packets() const { return rtcp_packets_; } + + bool SendRtp(const void* data, int len) { + if (!sending_ || !Base::network_interface_) { + return false; + } + talk_base::Buffer packet(data, len, kMaxRtpPacketLen); + return Base::network_interface_->SendPacket(&packet); + } + bool SendRtcp(const void* data, int len) { + if (!Base::network_interface_) { + return false; + } + talk_base::Buffer packet(data, len, kMaxRtpPacketLen); + return Base::network_interface_->SendRtcp(&packet); + } + + bool CheckRtp(const void* data, int len) { + bool success = !rtp_packets_.empty(); + if (success) { + std::string packet = rtp_packets_.front(); + rtp_packets_.pop_front(); + success = (packet == std::string(static_cast(data), len)); + } + return success; + } + bool CheckRtcp(const void* data, int len) { + bool success = !rtcp_packets_.empty(); + if (success) { + std::string packet = rtcp_packets_.front(); + rtcp_packets_.pop_front(); + success = (packet == std::string(static_cast(data), len)); + } + return success; + } + bool CheckNoRtp() { return rtp_packets_.empty(); } + bool CheckNoRtcp() { return rtcp_packets_.empty(); } + virtual bool SetRecvRtpHeaderExtensions( + const std::vector& extensions) { + recv_extensions_ = extensions; + return true; + } + virtual bool SetSendRtpHeaderExtensions( + const std::vector& extensions) { + send_extensions_ = extensions; + return true; + } + void set_fail_set_send_codecs(bool fail) { fail_set_send_codecs_ = fail; } + void set_fail_set_recv_codecs(bool fail) { fail_set_recv_codecs_ = fail; } + virtual bool AddSendStream(const StreamParams& sp) { + if (std::find(send_streams_.begin(), send_streams_.end(), sp) != + send_streams_.end()) { + return false; + } + send_streams_.push_back(sp); + return true; + } + virtual bool RemoveSendStream(uint32 ssrc) { + return RemoveStreamBySsrc(&send_streams_, ssrc); + } + virtual bool AddRecvStream(const StreamParams& sp) { + if (std::find(receive_streams_.begin(), receive_streams_.end(), sp) != + receive_streams_.end()) { + return false; + } + receive_streams_.push_back(sp); + return true; + } + virtual bool RemoveRecvStream(uint32 ssrc) { + return RemoveStreamBySsrc(&receive_streams_, ssrc); + } + virtual bool MuteStream(uint32 ssrc, bool on) { + if (!HasSendStream(ssrc) && ssrc != 0) + return false; + if (on) + muted_streams_.insert(ssrc); + else + muted_streams_.erase(ssrc); + return true; + } + bool IsStreamMuted(uint32 ssrc) const { + bool ret = muted_streams_.find(ssrc) != muted_streams_.end(); + // If |ssrc = 0| check if the first send stream is muted. + if (!ret && ssrc == 0 && !send_streams_.empty()) { + return muted_streams_.find(send_streams_[0].first_ssrc()) != + muted_streams_.end(); + } + return ret; + } + const std::vector& send_streams() const { + return send_streams_; + } + const std::vector& recv_streams() const { + return receive_streams_; + } + bool HasRecvStream(uint32 ssrc) const { + return GetStreamBySsrc(receive_streams_, ssrc, NULL); + } + bool HasSendStream(uint32 ssrc) const { + return GetStreamBySsrc(send_streams_, ssrc, NULL); + } + // TODO(perkj): This is to support legacy unit test that only check one + // sending stream. + const uint32 send_ssrc() { + if (send_streams_.empty()) + return 0; + return send_streams_[0].first_ssrc(); + } + + // TODO(perkj): This is to support legacy unit test that only check one + // sending stream. + const std::string rtcp_cname() { + if (send_streams_.empty()) + return ""; + return send_streams_[0].cname; + } + + bool ready_to_send() const { + return ready_to_send_; + } + + protected: + bool set_sending(bool send) { + sending_ = send; + return true; + } + void set_playout(bool playout) { playout_ = playout; } + virtual void OnPacketReceived(talk_base::Buffer* packet) { + rtp_packets_.push_back(std::string(packet->data(), packet->length())); + } + virtual void OnRtcpReceived(talk_base::Buffer* packet) { + rtcp_packets_.push_back(std::string(packet->data(), packet->length())); + } + virtual void OnReadyToSend(bool ready) { + ready_to_send_ = ready; + } + bool fail_set_send_codecs() const { return fail_set_send_codecs_; } + bool fail_set_recv_codecs() const { return fail_set_recv_codecs_; } + + private: + bool sending_; + bool playout_; + std::vector recv_extensions_; + std::vector send_extensions_; + std::list rtp_packets_; + std::list rtcp_packets_; + std::vector send_streams_; + std::vector receive_streams_; + std::set muted_streams_; + bool fail_set_send_codecs_; + bool fail_set_recv_codecs_; + uint32 send_ssrc_; + std::string rtcp_cname_; + bool ready_to_send_; +}; + +class FakeVoiceMediaChannel : public RtpHelper { + public: + struct DtmfInfo { + DtmfInfo(uint32 ssrc, int event_code, int duration, int flags) + : ssrc(ssrc), event_code(event_code), duration(duration), flags(flags) { + } + uint32 ssrc; + int event_code; + int duration; + int flags; + }; + explicit FakeVoiceMediaChannel(FakeVoiceEngine* engine) + : engine_(engine), + fail_set_send_(false), + ringback_tone_ssrc_(0), + ringback_tone_play_(false), + ringback_tone_loop_(false), + time_since_last_typing_(-1) { + output_scalings_[0] = OutputScaling(); // For default channel. + } + ~FakeVoiceMediaChannel(); + const std::vector& recv_codecs() const { return recv_codecs_; } + const std::vector& send_codecs() const { return send_codecs_; } + const std::vector& codecs() const { return send_codecs(); } + const std::vector& dtmf_info_queue() const { + return dtmf_info_queue_; + } + const AudioOptions& options() const { return options_; } + + uint32 ringback_tone_ssrc() const { return ringback_tone_ssrc_; } + bool ringback_tone_play() const { return ringback_tone_play_; } + bool ringback_tone_loop() const { return ringback_tone_loop_; } + + virtual bool SetRecvCodecs(const std::vector& codecs) { + if (fail_set_recv_codecs()) { + // Fake the failure in SetRecvCodecs. + return false; + } + recv_codecs_ = codecs; + return true; + } + virtual bool SetSendCodecs(const std::vector& codecs) { + if (fail_set_send_codecs()) { + // Fake the failure in SetSendCodecs. + return false; + } + send_codecs_ = codecs; + return true; + } + virtual bool SetPlayout(bool playout) { + set_playout(playout); + return true; + } + virtual bool SetSend(SendFlags flag) { + if (fail_set_send_) { + return false; + } + return set_sending(flag != SEND_NOTHING); + } + virtual bool SetSendBandwidth(bool autobw, int bps) { return true; } + virtual bool AddRecvStream(const StreamParams& sp) { + if (!RtpHelper::AddRecvStream(sp)) + return false; + output_scalings_[sp.first_ssrc()] = OutputScaling(); + return true; + } + virtual bool RemoveRecvStream(uint32 ssrc) { + if (!RtpHelper::RemoveRecvStream(ssrc)) + return false; + output_scalings_.erase(ssrc); + return true; + } + virtual bool SetRenderer(uint32 ssrc, AudioRenderer* renderer) { + // TODO(xians): Implement this. + return false; + } + + virtual bool GetActiveStreams(AudioInfo::StreamList* streams) { return true; } + virtual int GetOutputLevel() { return 0; } + void set_time_since_last_typing(int ms) { time_since_last_typing_ = ms; } + virtual int GetTimeSinceLastTyping() { return time_since_last_typing_; } + virtual void SetTypingDetectionParameters( + int time_window, int cost_per_typing, int reporting_threshold, + int penalty_decay, int type_event_delay) {} + + virtual bool SetRingbackTone(const char* buf, int len) { return true; } + virtual bool PlayRingbackTone(uint32 ssrc, bool play, bool loop) { + ringback_tone_ssrc_ = ssrc; + ringback_tone_play_ = play; + ringback_tone_loop_ = loop; + return true; + } + + virtual bool CanInsertDtmf() { + for (std::vector::const_iterator it = send_codecs_.begin(); + it != send_codecs_.end(); ++it) { + // Find the DTMF telephone event "codec". + if (_stricmp(it->name.c_str(), "telephone-event") == 0) { + return true; + } + } + return false; + } + virtual bool InsertDtmf(uint32 ssrc, int event_code, int duration, + int flags) { + dtmf_info_queue_.push_back(DtmfInfo(ssrc, event_code, duration, flags)); + return true; + } + + virtual bool SetOutputScaling(uint32 ssrc, double left, double right) { + if (0 == ssrc) { + std::map::iterator it; + for (it = output_scalings_.begin(); it != output_scalings_.end(); ++it) { + it->second.left = left; + it->second.right = right; + } + return true; + } else if (output_scalings_.find(ssrc) != output_scalings_.end()) { + output_scalings_[ssrc].left = left; + output_scalings_[ssrc].right = right; + return true; + } + return false; + } + virtual bool GetOutputScaling(uint32 ssrc, double* left, double* right) { + if (output_scalings_.find(ssrc) == output_scalings_.end()) + return false; + *left = output_scalings_[ssrc].left; + *right = output_scalings_[ssrc].right; + return true; + } + + virtual bool GetStats(VoiceMediaInfo* info) { return false; } + virtual void GetLastMediaError(uint32* ssrc, + VoiceMediaChannel::Error* error) { + *ssrc = 0; + *error = fail_set_send_ ? VoiceMediaChannel::ERROR_REC_DEVICE_OPEN_FAILED + : VoiceMediaChannel::ERROR_NONE; + } + + void set_fail_set_send(bool fail) { fail_set_send_ = fail; } + void TriggerError(uint32 ssrc, VoiceMediaChannel::Error error) { + VoiceMediaChannel::SignalMediaError(ssrc, error); + } + + virtual bool SetOptions(const AudioOptions& options) { + // Does a "merge" of current options and set options. + options_.SetAll(options); + return true; + } + virtual bool GetOptions(AudioOptions* options) const { + *options = options_; + return true; + } + + private: + struct OutputScaling { + OutputScaling() : left(1.0), right(1.0) {} + double left, right; + }; + + FakeVoiceEngine* engine_; + std::vector recv_codecs_; + std::vector send_codecs_; + std::map output_scalings_; + std::vector dtmf_info_queue_; + bool fail_set_send_; + uint32 ringback_tone_ssrc_; + bool ringback_tone_play_; + bool ringback_tone_loop_; + int time_since_last_typing_; + AudioOptions options_; +}; + +// A helper function to compare the FakeVoiceMediaChannel::DtmfInfo. +inline bool CompareDtmfInfo(const FakeVoiceMediaChannel::DtmfInfo& info, + uint32 ssrc, int event_code, int duration, + int flags) { + return (info.duration == duration && info.event_code == event_code && + info.flags == flags && info.ssrc == ssrc); +} + +class FakeVideoMediaChannel : public RtpHelper { + public: + explicit FakeVideoMediaChannel(FakeVideoEngine* engine) + : engine_(engine), + sent_intra_frame_(false), + requested_intra_frame_(false) {} + ~FakeVideoMediaChannel(); + + const std::vector& recv_codecs() const { return recv_codecs_; } + const std::vector& send_codecs() const { return send_codecs_; } + const std::vector& codecs() const { return send_codecs(); } + bool rendering() const { return playout(); } + const VideoOptions& options() const { return options_; } + const std::map& renderers() const { + return renderers_; + } + bool GetSendStreamFormat(uint32 ssrc, VideoFormat* format) { + if (send_formats_.find(ssrc) == send_formats_.end()) { + return false; + } + *format = send_formats_[ssrc]; + return true; + } + virtual bool SetSendStreamFormat(uint32 ssrc, const VideoFormat& format) { + if (send_formats_.find(ssrc) == send_formats_.end()) { + return false; + } + send_formats_[ssrc] = format; + return true; + } + + virtual bool AddSendStream(const StreamParams& sp) { + if (!RtpHelper::AddSendStream(sp)) { + return false; + } + SetSendStreamDefaultFormat(sp.first_ssrc()); + return true; + } + virtual bool RemoveSendStream(uint32 ssrc) { + send_formats_.erase(ssrc); + return RtpHelper::RemoveSendStream(ssrc); + } + + virtual bool SetRecvCodecs(const std::vector& codecs) { + if (fail_set_recv_codecs()) { + // Fake the failure in SetRecvCodecs. + return false; + } + recv_codecs_ = codecs; + return true; + } + virtual bool SetSendCodecs(const std::vector& codecs) { + if (fail_set_send_codecs()) { + // Fake the failure in SetSendCodecs. + return false; + } + send_codecs_ = codecs; + + for (std::vector::const_iterator it = send_streams().begin(); + it != send_streams().end(); ++it) { + SetSendStreamDefaultFormat(it->first_ssrc()); + } + return true; + } + virtual bool GetSendCodec(VideoCodec* send_codec) { + if (send_codecs_.empty()) { + return false; + } + *send_codec = send_codecs_[0]; + return true; + } + virtual bool SetRender(bool render) { + set_playout(render); + return true; + } + virtual bool SetRenderer(uint32 ssrc, VideoRenderer* r) { + if (ssrc != 0 && renderers_.find(ssrc) == renderers_.end()) { + return false; + } + if (ssrc != 0) { + renderers_[ssrc] = r; + } + return true; + } + + virtual bool SetSend(bool send) { return set_sending(send); } + virtual bool SetCapturer(uint32 ssrc, VideoCapturer* capturer) { + capturers_[ssrc] = capturer; + return true; + } + bool HasCapturer(uint32 ssrc) const { + return capturers_.find(ssrc) != capturers_.end(); + } + virtual bool SetSendBandwidth(bool autobw, int bps) { return true; } + virtual bool AddRecvStream(const StreamParams& sp) { + if (!RtpHelper::AddRecvStream(sp)) + return false; + renderers_[sp.first_ssrc()] = NULL; + return true; + } + virtual bool RemoveRecvStream(uint32 ssrc) { + if (!RtpHelper::RemoveRecvStream(ssrc)) + return false; + renderers_.erase(ssrc); + return true; + } + + virtual bool GetStats(VideoMediaInfo* info) { return false; } + virtual bool SendIntraFrame() { + sent_intra_frame_ = true; + return true; + } + virtual bool RequestIntraFrame() { + requested_intra_frame_ = true; + return true; + } + virtual bool SetOptions(const VideoOptions& options) { + options_ = options; + return true; + } + virtual bool GetOptions(VideoOptions* options) const { + *options = options_; + return true; + } + virtual void UpdateAspectRatio(int ratio_w, int ratio_h) {} + void set_sent_intra_frame(bool v) { sent_intra_frame_ = v; } + bool sent_intra_frame() const { return sent_intra_frame_; } + void set_requested_intra_frame(bool v) { requested_intra_frame_ = v; } + bool requested_intra_frame() const { return requested_intra_frame_; } + + private: + // Be default, each send stream uses the first send codec format. + void SetSendStreamDefaultFormat(uint32 ssrc) { + if (!send_codecs_.empty()) { + send_formats_[ssrc] = VideoFormat( + send_codecs_[0].width, send_codecs_[0].height, + cricket::VideoFormat::FpsToInterval(send_codecs_[0].framerate), + cricket::FOURCC_I420); + } + } + + FakeVideoEngine* engine_; + std::vector recv_codecs_; + std::vector send_codecs_; + std::map renderers_; + std::map send_formats_; + std::map capturers_; + bool sent_intra_frame_; + bool requested_intra_frame_; + VideoOptions options_; +}; + +class FakeSoundclipMedia : public SoundclipMedia { + public: + virtual bool PlaySound(const char* buf, int len, int flags) { return true; } +}; + +class FakeDataMediaChannel : public RtpHelper { + public: + explicit FakeDataMediaChannel(void* unused) + : auto_bandwidth_(false), max_bps_(-1) {} + ~FakeDataMediaChannel() {} + const std::vector& recv_codecs() const { return recv_codecs_; } + const std::vector& send_codecs() const { return send_codecs_; } + const std::vector& codecs() const { return send_codecs(); } + bool auto_bandwidth() const { return auto_bandwidth_; } + int max_bps() const { return max_bps_; } + + virtual bool SetRecvCodecs(const std::vector& codecs) { + if (fail_set_recv_codecs()) { + // Fake the failure in SetRecvCodecs. + return false; + } + recv_codecs_ = codecs; + return true; + } + virtual bool SetSendCodecs(const std::vector& codecs) { + if (fail_set_send_codecs()) { + // Fake the failure in SetSendCodecs. + return false; + } + send_codecs_ = codecs; + return true; + } + virtual bool SetSend(bool send) { return set_sending(send); } + virtual bool SetReceive(bool receive) { + set_playout(receive); + return true; + } + virtual bool SetSendBandwidth(bool autobw, int bps) { + auto_bandwidth_ = autobw; + max_bps_ = bps; + return true; + } + virtual bool AddRecvStream(const StreamParams& sp) { + if (!RtpHelper::AddRecvStream(sp)) + return false; + return true; + } + virtual bool RemoveRecvStream(uint32 ssrc) { + if (!RtpHelper::RemoveRecvStream(ssrc)) + return false; + return true; + } + + virtual bool SendData(const SendDataParams& params, + const talk_base::Buffer& payload, + SendDataResult* result) { + last_sent_data_params_ = params; + last_sent_data_ = std::string(payload.data(), payload.length()); + return true; + } + + SendDataParams last_sent_data_params() { return last_sent_data_params_; } + std::string last_sent_data() { return last_sent_data_; } + + private: + std::vector recv_codecs_; + std::vector send_codecs_; + SendDataParams last_sent_data_params_; + std::string last_sent_data_; + bool auto_bandwidth_; + int max_bps_; +}; + +// A base class for all of the shared parts between FakeVoiceEngine +// and FakeVideoEngine. +class FakeBaseEngine { + public: + FakeBaseEngine() + : loglevel_(-1), + options_(0), + options_changed_(false), + fail_create_channel_(false) {} + bool Init(talk_base::Thread* worker_thread) { return true; } + void Terminate() {} + + bool SetOptions(int options) { + options_ = options; + options_changed_ = true; + return true; + } + + void SetLogging(int level, const char* filter) { + loglevel_ = level; + logfilter_ = filter; + } + + void set_fail_create_channel(bool fail) { fail_create_channel_ = fail; } + + const std::vector& rtp_header_extensions() const { + return rtp_header_extensions_; + } + + protected: + int loglevel_; + std::string logfilter_; + int options_; + // Flag used by optionsmessagehandler_unittest for checking whether any + // relevant setting has been updated. + // TODO(thaloun): Replace with explicit checks of before & after values. + bool options_changed_; + bool fail_create_channel_; + std::vector rtp_header_extensions_; +}; + +class FakeVoiceEngine : public FakeBaseEngine { + public: + FakeVoiceEngine() + : output_volume_(-1), + delay_offset_(0), + rx_processor_(NULL), + tx_processor_(NULL) { + // Add a fake audio codec. Note that the name must not be "" as there are + // sanity checks against that. + codecs_.push_back(AudioCodec(101, "fake_audio_codec", 0, 0, 1, 0)); + } + int GetCapabilities() { return AUDIO_SEND | AUDIO_RECV; } + + VoiceMediaChannel* CreateChannel() { + if (fail_create_channel_) { + return NULL; + } + + FakeVoiceMediaChannel* ch = new FakeVoiceMediaChannel(this); + channels_.push_back(ch); + return ch; + } + FakeVoiceMediaChannel* GetChannel(size_t index) { + return (channels_.size() > index) ? channels_[index] : NULL; + } + void UnregisterChannel(VoiceMediaChannel* channel) { + channels_.erase(std::find(channels_.begin(), channels_.end(), channel)); + } + SoundclipMedia* CreateSoundclip() { return new FakeSoundclipMedia(); } + + const std::vector& codecs() { return codecs_; } + void SetCodecs(const std::vector codecs) { codecs_ = codecs; } + + bool SetDelayOffset(int offset) { + delay_offset_ = offset; + return true; + } + + bool SetDevices(const Device* in_device, const Device* out_device) { + in_device_ = (in_device) ? in_device->name : ""; + out_device_ = (out_device) ? out_device->name : ""; + options_changed_ = true; + return true; + } + + bool GetOutputVolume(int* level) { + *level = output_volume_; + return true; + } + + bool SetOutputVolume(int level) { + output_volume_ = level; + options_changed_ = true; + return true; + } + + int GetInputLevel() { return 0; } + + bool SetLocalMonitor(bool enable) { return true; } + + bool RegisterProcessor(uint32 ssrc, VoiceProcessor* voice_processor, + MediaProcessorDirection direction) { + if (direction == MPD_RX) { + rx_processor_ = voice_processor; + return true; + } else if (direction == MPD_TX) { + tx_processor_ = voice_processor; + return true; + } + return false; + } + + bool UnregisterProcessor(uint32 ssrc, VoiceProcessor* voice_processor, + MediaProcessorDirection direction) { + bool unregistered = false; + if (direction & MPD_RX) { + rx_processor_ = NULL; + unregistered = true; + } + if (direction & MPD_TX) { + tx_processor_ = NULL; + unregistered = true; + } + return unregistered; + } + + private: + std::vector channels_; + std::vector codecs_; + int output_volume_; + int delay_offset_; + std::string in_device_; + std::string out_device_; + VoiceProcessor* rx_processor_; + VoiceProcessor* tx_processor_; + + friend class FakeMediaEngine; +}; + +class FakeVideoEngine : public FakeBaseEngine { + public: + FakeVideoEngine() : renderer_(NULL), capture_(false), processor_(NULL) { + // Add a fake video codec. Note that the name must not be "" as there are + // sanity checks against that. + codecs_.push_back(VideoCodec(0, "fake_video_codec", 0, 0, 0, 0)); + } + int GetCapabilities() { return VIDEO_SEND | VIDEO_RECV; } + bool SetDefaultEncoderConfig(const VideoEncoderConfig& config) { + default_encoder_config_ = config; + return true; + } + const VideoEncoderConfig& default_encoder_config() const { + return default_encoder_config_; + } + + VideoMediaChannel* CreateChannel(VoiceMediaChannel* channel) { + if (fail_create_channel_) { + return NULL; + } + + FakeVideoMediaChannel* ch = new FakeVideoMediaChannel(this); + channels_.push_back(ch); + return ch; + } + FakeVideoMediaChannel* GetChannel(size_t index) { + return (channels_.size() > index) ? channels_[index] : NULL; + } + void UnregisterChannel(VideoMediaChannel* channel) { + channels_.erase(std::find(channels_.begin(), channels_.end(), channel)); + } + + const std::vector& codecs() const { return codecs_; } + bool FindCodec(const VideoCodec& in) { + for (size_t i = 0; i < codecs_.size(); ++i) { + if (codecs_[i].Matches(in)) { + return true; + } + } + return false; + } + void SetCodecs(const std::vector codecs) { codecs_ = codecs; } + + bool SetCaptureDevice(const Device* device) { + in_device_ = (device) ? device->name : ""; + options_changed_ = true; + return true; + } + bool SetLocalRenderer(VideoRenderer* r) { + renderer_ = r; + return true; + } + bool SetVideoCapturer(VideoCapturer* /*capturer*/) { return true; } + VideoCapturer* GetVideoCapturer() const { return NULL; } + bool SetCapture(bool capture) { + capture_ = capture; + return true; + } + VideoFormat GetStartCaptureFormat() const { + return VideoFormat(640, 480, cricket::VideoFormat::FpsToInterval(30), + FOURCC_I420); + } + + sigslot::repeater2 SignalCaptureStateChange; + + private: + std::vector channels_; + std::vector codecs_; + VideoEncoderConfig default_encoder_config_; + std::string in_device_; + VideoRenderer* renderer_; + bool capture_; + VideoProcessor* processor_; + + friend class FakeMediaEngine; +}; + +class FakeMediaEngine : + public CompositeMediaEngine { + public: + FakeMediaEngine() { + voice_ = FakeVoiceEngine(); + video_ = FakeVideoEngine(); + } + virtual ~FakeMediaEngine() {} + + virtual void SetAudioCodecs(const std::vector codecs) { + voice_.SetCodecs(codecs); + } + + virtual void SetVideoCodecs(const std::vector codecs) { + video_.SetCodecs(codecs); + } + + FakeVoiceMediaChannel* GetVoiceChannel(size_t index) { + return voice_.GetChannel(index); + } + + FakeVideoMediaChannel* GetVideoChannel(size_t index) { + return video_.GetChannel(index); + } + + int audio_options() const { return voice_.options_; } + int audio_delay_offset() const { return voice_.delay_offset_; } + int output_volume() const { return voice_.output_volume_; } + const VideoEncoderConfig& default_video_encoder_config() const { + return video_.default_encoder_config_; + } + const std::string& audio_in_device() const { return voice_.in_device_; } + const std::string& audio_out_device() const { return voice_.out_device_; } + VideoRenderer* local_renderer() { return video_.renderer_; } + int voice_loglevel() const { return voice_.loglevel_; } + const std::string& voice_logfilter() const { return voice_.logfilter_; } + int video_loglevel() const { return video_.loglevel_; } + const std::string& video_logfilter() const { return video_.logfilter_; } + bool capture() const { return video_.capture_; } + bool options_changed() const { + return voice_.options_changed_ || video_.options_changed_; + } + void clear_options_changed() { + video_.options_changed_ = false; + voice_.options_changed_ = false; + } + void set_fail_create_channel(bool fail) { + voice_.set_fail_create_channel(fail); + video_.set_fail_create_channel(fail); + } + bool voice_processor_registered(MediaProcessorDirection direction) const { + if (direction == MPD_RX) { + return voice_.rx_processor_ != NULL; + } else if (direction == MPD_TX) { + return voice_.tx_processor_ != NULL; + } + return false; + } +}; + +// CompositeMediaEngine with FakeVoiceEngine to expose SetAudioCodecs to +// establish a media connectionwith minimum set of audio codes required +template +class CompositeMediaEngineWithFakeVoiceEngine : + public CompositeMediaEngine { + public: + CompositeMediaEngineWithFakeVoiceEngine() {} + virtual ~CompositeMediaEngineWithFakeVoiceEngine() {} + + virtual void SetAudioCodecs(const std::vector& codecs) { + CompositeMediaEngine::voice_.SetCodecs(codecs); + } +}; + +// Have to come afterwards due to declaration order +inline FakeVoiceMediaChannel::~FakeVoiceMediaChannel() { + if (engine_) { + engine_->UnregisterChannel(this); + } +} + +inline FakeVideoMediaChannel::~FakeVideoMediaChannel() { + if (engine_) { + engine_->UnregisterChannel(this); + } +} + +class FakeDataEngine : public DataEngineInterface { + public: + FakeDataEngine() : last_channel_type_(DCT_NONE) {} + + virtual DataMediaChannel* CreateChannel(DataChannelType data_channel_type) { + last_channel_type_ = data_channel_type; + FakeDataMediaChannel* ch = new FakeDataMediaChannel(this); + channels_.push_back(ch); + return ch; + } + + FakeDataMediaChannel* GetChannel(size_t index) { + return (channels_.size() > index) ? channels_[index] : NULL; + } + + void UnregisterChannel(DataMediaChannel* channel) { + channels_.erase(std::find(channels_.begin(), channels_.end(), channel)); + } + + virtual void SetDataCodecs(const std::vector& data_codecs) { + data_codecs_ = data_codecs; + } + + virtual const std::vector& data_codecs() { return data_codecs_; } + + DataChannelType last_channel_type() const { return last_channel_type_; } + + private: + std::vector channels_; + std::vector data_codecs_; + DataChannelType last_channel_type_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_FAKEMEDIAENGINE_H_ diff --git a/talk/media/base/fakemediaprocessor.h b/talk/media/base/fakemediaprocessor.h new file mode 100644 index 000000000..a1f5ac9a6 --- /dev/null +++ b/talk/media/base/fakemediaprocessor.h @@ -0,0 +1,79 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_FAKEMEDIAPROCESSOR_H_ +#define TALK_MEDIA_BASE_FAKEMEDIAPROCESSOR_H_ + +#include "talk/media/base/videoprocessor.h" +#include "talk/media/base/voiceprocessor.h" + +namespace cricket { + +class AudioFrame; + +class FakeMediaProcessor : public VoiceProcessor, public VideoProcessor { + public: + FakeMediaProcessor() + : voice_frame_count_(0), + video_frame_count_(0), + drop_frames_(false), + dropped_frame_count_(0) { + } + virtual ~FakeMediaProcessor() {} + + virtual void OnFrame(uint32 ssrc, + MediaProcessorDirection direction, + AudioFrame* frame) { + ++voice_frame_count_; + } + virtual void OnFrame(uint32 ssrc, VideoFrame* frame_ptr, bool* drop_frame) { + ++video_frame_count_; + if (drop_frames_) { + *drop_frame = true; + ++dropped_frame_count_; + } + } + virtual void OnVoiceMute(uint32 ssrc, bool muted) {} + virtual void OnVideoMute(uint32 ssrc, bool muted) {} + + int voice_frame_count() const { return voice_frame_count_; } + int video_frame_count() const { return video_frame_count_; } + + void set_drop_frames(bool b) { drop_frames_ = b; } + int dropped_frame_count() const { return dropped_frame_count_; } + + private: + // TODO(janahan): make is a map so that we can multiple ssrcs + int voice_frame_count_; + int video_frame_count_; + bool drop_frames_; + int dropped_frame_count_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_FAKEMEDIAPROCESSOR_H_ diff --git a/talk/media/base/fakenetworkinterface.h b/talk/media/base/fakenetworkinterface.h new file mode 100644 index 000000000..25016844b --- /dev/null +++ b/talk/media/base/fakenetworkinterface.h @@ -0,0 +1,249 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_FAKENETWORKINTERFACE_H_ +#define TALK_MEDIA_BASE_FAKENETWORKINTERFACE_H_ + +#include +#include + +#include "talk/base/buffer.h" +#include "talk/base/byteorder.h" +#include "talk/base/criticalsection.h" +#include "talk/base/messagehandler.h" +#include "talk/base/messagequeue.h" +#include "talk/base/thread.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/rtputils.h" + +namespace cricket { + +// Fake NetworkInterface that sends/receives RTP/RTCP packets. +class FakeNetworkInterface : public MediaChannel::NetworkInterface, + public talk_base::MessageHandler { + public: + FakeNetworkInterface() + : thread_(talk_base::Thread::Current()), + dest_(NULL), + conf_(false), + sendbuf_size_(-1), + recvbuf_size_(-1) { + } + + void SetDestination(MediaChannel* dest) { dest_ = dest; } + + // Conference mode is a mode where instead of simply forwarding the packets, + // the transport will send multiple copies of the packet with the specified + // SSRCs. This allows us to simulate receiving media from multiple sources. + void SetConferenceMode(bool conf, const std::vector& ssrcs) { + talk_base::CritScope cs(&crit_); + conf_ = conf; + conf_sent_ssrcs_ = ssrcs; + } + + int NumRtpBytes() { + talk_base::CritScope cs(&crit_); + int bytes = 0; + for (size_t i = 0; i < rtp_packets_.size(); ++i) { + bytes += rtp_packets_[i].length(); + } + return bytes; + } + + int NumRtpBytes(uint32 ssrc) { + talk_base::CritScope cs(&crit_); + int bytes = 0; + GetNumRtpBytesAndPackets(ssrc, &bytes, NULL); + return bytes; + } + + int NumRtpPackets() { + talk_base::CritScope cs(&crit_); + return rtp_packets_.size(); + } + + int NumRtpPackets(uint32 ssrc) { + talk_base::CritScope cs(&crit_); + int packets = 0; + GetNumRtpBytesAndPackets(ssrc, NULL, &packets); + return packets; + } + + int NumSentSsrcs() { + talk_base::CritScope cs(&crit_); + return sent_ssrcs_.size(); + } + + // Note: callers are responsible for deleting the returned buffer. + const talk_base::Buffer* GetRtpPacket(int index) { + talk_base::CritScope cs(&crit_); + if (index >= NumRtpPackets()) { + return NULL; + } + return new talk_base::Buffer(rtp_packets_[index]); + } + + int NumRtcpPackets() { + talk_base::CritScope cs(&crit_); + return rtcp_packets_.size(); + } + + // Note: callers are responsible for deleting the returned buffer. + const talk_base::Buffer* GetRtcpPacket(int index) { + talk_base::CritScope cs(&crit_); + if (index >= NumRtcpPackets()) { + return NULL; + } + return new talk_base::Buffer(rtcp_packets_[index]); + } + + // Indicate that |n|'th packet for |ssrc| should be dropped. + void AddPacketDrop(uint32 ssrc, uint32 n) { + drop_map_[ssrc].insert(n); + } + + int sendbuf_size() const { return sendbuf_size_; } + int recvbuf_size() const { return recvbuf_size_; } + + protected: + virtual bool SendPacket(talk_base::Buffer* packet) { + talk_base::CritScope cs(&crit_); + + uint32 cur_ssrc = 0; + if (!GetRtpSsrc(packet->data(), packet->length(), &cur_ssrc)) { + return false; + } + sent_ssrcs_[cur_ssrc]++; + + // Check if we need to drop this packet. + std::map >::iterator itr = + drop_map_.find(cur_ssrc); + if (itr != drop_map_.end() && + itr->second.count(sent_ssrcs_[cur_ssrc]) > 0) { + // "Drop" the packet. + return true; + } + + rtp_packets_.push_back(*packet); + if (conf_) { + talk_base::Buffer buffer_copy(*packet); + for (size_t i = 0; i < conf_sent_ssrcs_.size(); ++i) { + if (!SetRtpSsrc(buffer_copy.data(), buffer_copy.length(), + conf_sent_ssrcs_[i])) { + return false; + } + PostMessage(ST_RTP, buffer_copy); + } + } else { + PostMessage(ST_RTP, *packet); + } + return true; + } + + virtual bool SendRtcp(talk_base::Buffer* packet) { + talk_base::CritScope cs(&crit_); + rtcp_packets_.push_back(*packet); + if (!conf_) { + // don't worry about RTCP in conf mode for now + PostMessage(ST_RTCP, *packet); + } + return true; + } + + virtual int SetOption(SocketType type, talk_base::Socket::Option opt, + int option) { + if (opt == talk_base::Socket::OPT_SNDBUF) { + sendbuf_size_ = option; + } else if (opt == talk_base::Socket::OPT_RCVBUF) { + recvbuf_size_ = option; + } + return 0; + } + + void PostMessage(int id, const talk_base::Buffer& packet) { + thread_->Post(this, id, talk_base::WrapMessageData(packet)); + } + + virtual void OnMessage(talk_base::Message* msg) { + talk_base::TypedMessageData* msg_data = + static_cast*>( + msg->pdata); + if (dest_) { + if (msg->message_id == ST_RTP) { + dest_->OnPacketReceived(&msg_data->data()); + } else { + dest_->OnRtcpReceived(&msg_data->data()); + } + } + delete msg_data; + } + + private: + void GetNumRtpBytesAndPackets(uint32 ssrc, int* bytes, int* packets) { + if (bytes) { + *bytes = 0; + } + if (packets) { + *packets = 0; + } + uint32 cur_ssrc = 0; + for (size_t i = 0; i < rtp_packets_.size(); ++i) { + if (!GetRtpSsrc(rtp_packets_[i].data(), + rtp_packets_[i].length(), &cur_ssrc)) { + return; + } + if (ssrc == cur_ssrc) { + if (bytes) { + *bytes += rtp_packets_[i].length(); + } + if (packets) { + ++(*packets); + } + } + } + } + + talk_base::Thread* thread_; + MediaChannel* dest_; + bool conf_; + // The ssrcs used in sending out packets in conference mode. + std::vector conf_sent_ssrcs_; + // Map to track counts of packets that have been sent per ssrc. + // This includes packets that are dropped. + std::map sent_ssrcs_; + // Map to track packet-number that needs to be dropped per ssrc. + std::map > drop_map_; + talk_base::CriticalSection crit_; + std::vector rtp_packets_; + std::vector rtcp_packets_; + int sendbuf_size_; + int recvbuf_size_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_FAKENETWORKINTERFACE_H_ diff --git a/talk/media/base/fakertp.h b/talk/media/base/fakertp.h new file mode 100644 index 000000000..7c56cbaa7 --- /dev/null +++ b/talk/media/base/fakertp.h @@ -0,0 +1,104 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +// Fake RTP and RTCP packets to use in unit tests. + +#ifndef TALK_MEDIA_BASE_FAKERTP_H_ +#define TALK_MEDIA_BASE_FAKERTP_H_ + +// A typical PCMU RTP packet. +// PT=0, SN=1, TS=0, SSRC=1 +// all data FF +static const unsigned char kPcmuFrame[] = { + 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, +}; + +// A typical Receiver Report RTCP packet. +// PT=RR, LN=1, SSRC=1 +// send SSRC=2, all other fields 0 +static const unsigned char kRtcpReport[] = { + 0x80, 0xc9, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +// PT = 97, TS = 0, Seq = 1, SSRC = 2 +// H264 - NRI = 1, Type = 1, bit stream = FF + +static const unsigned char kH264Packet[] = { + 0x80, 0x61, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x21, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, +}; + +// PT= 101, SN=2, TS=3, SSRC = 4 +static const unsigned char kDataPacket[] = { + 0x80, 0x65, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, +}; + +#endif // TALK_MEDIA_BASE_FAKERTP_H_ diff --git a/talk/media/base/fakevideocapturer.h b/talk/media/base/fakevideocapturer.h new file mode 100644 index 000000000..4f51b661a --- /dev/null +++ b/talk/media/base/fakevideocapturer.h @@ -0,0 +1,154 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_FAKEVIDEOCAPTURER_H_ +#define TALK_MEDIA_BASE_FAKEVIDEOCAPTURER_H_ + +#include + +#include + +#include "talk/base/timeutils.h" +#include "talk/media/base/videocapturer.h" +#include "talk/media/base/videocommon.h" +#include "talk/media/base/videoframe.h" + +namespace cricket { + +// Fake video capturer that allows the test to manually pump in frames. +class FakeVideoCapturer : public cricket::VideoCapturer { + public: + FakeVideoCapturer() + : running_(false), + initial_unix_timestamp_(time(NULL) * talk_base::kNumNanosecsPerSec), + next_timestamp_(talk_base::kNumNanosecsPerMillisec), + is_screencast_(false) { + // Default supported formats. Use ResetSupportedFormats to over write. + std::vector formats; + formats.push_back(cricket::VideoFormat(1280, 720, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(320, 240, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(160, 120, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + ResetSupportedFormats(formats); + } + ~FakeVideoCapturer() { + SignalDestroyed(this); + } + + void ResetSupportedFormats(const std::vector& formats) { + SetSupportedFormats(formats); + } + bool CaptureFrame() { + if (!GetCaptureFormat()) { + return false; + } + return CaptureCustomFrame(GetCaptureFormat()->width, + GetCaptureFormat()->height, + GetCaptureFormat()->fourcc); + } + bool CaptureCustomFrame(int width, int height, uint32 fourcc) { + if (!running_) { + return false; + } + // Currently, |fourcc| is always I420 or ARGB. + // TODO(fbarchard): Extend SizeOf to take fourcc. + uint32 size = 0u; + if (fourcc == cricket::FOURCC_ARGB) { + size = width * 4 * height; + } else if (fourcc == cricket::FOURCC_I420) { + size = cricket::VideoFrame::SizeOf(width, height); + } else { + return false; // Unsupported FOURCC. + } + if (size == 0u) { + return false; // Width and/or Height were zero. + } + + cricket::CapturedFrame frame; + frame.width = width; + frame.height = height; + frame.fourcc = fourcc; + frame.data_size = size; + frame.elapsed_time = next_timestamp_; + frame.time_stamp = initial_unix_timestamp_ + next_timestamp_; + next_timestamp_ += 33333333; // 30 fps + + talk_base::scoped_array data(new char[size]); + frame.data = data.get(); + // Copy something non-zero into the buffer so Validate wont complain that + // the frame is all duplicate. + memset(frame.data, 1, size / 2); + memset(reinterpret_cast(frame.data) + (size / 2), 2, + size - (size / 2)); + memcpy(frame.data, reinterpret_cast(&fourcc), 4); + // TODO(zhurunz): SignalFrameCaptured carry returned value to be able to + // capture results from downstream. + SignalFrameCaptured(this, &frame); + return true; + } + + sigslot::signal1 SignalDestroyed; + + virtual cricket::CaptureState Start(const cricket::VideoFormat& format) { + cricket::VideoFormat supported; + if (GetBestCaptureFormat(format, &supported)) { + SetCaptureFormat(&supported); + } + running_ = true; + SetCaptureState(cricket::CS_RUNNING); + return cricket::CS_RUNNING; + } + virtual void Stop() { + running_ = false; + SetCaptureFormat(NULL); + SetCaptureState(cricket::CS_STOPPED); + } + virtual bool IsRunning() { return running_; } + void SetScreencast(bool is_screencast) { + is_screencast_ = is_screencast; + } + virtual bool IsScreencast() const { return is_screencast_; } + bool GetPreferredFourccs(std::vector* fourccs) { + fourccs->push_back(cricket::FOURCC_I420); + fourccs->push_back(cricket::FOURCC_MJPG); + return true; + } + + private: + bool running_; + int64 initial_unix_timestamp_; + int64 next_timestamp_; + bool is_screencast_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_FAKEVIDEOCAPTURER_H_ diff --git a/talk/media/base/fakevideorenderer.h b/talk/media/base/fakevideorenderer.h new file mode 100644 index 000000000..4000d5e20 --- /dev/null +++ b/talk/media/base/fakevideorenderer.h @@ -0,0 +1,142 @@ +/* + * libjingle + * Copyright 2011 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. + */ + +#ifndef TALK_MEDIA_BASE_FAKEVIDEORENDERER_H_ +#define TALK_MEDIA_BASE_FAKEVIDEORENDERER_H_ + +#include "talk/base/sigslot.h" +#include "talk/media/base/videoframe.h" +#include "talk/media/base/videorenderer.h" + +namespace cricket { + +// Faked video renderer that has a callback for actions on rendering. +class FakeVideoRenderer : public VideoRenderer { + public: + FakeVideoRenderer() + : errors_(0), + width_(0), + height_(0), + num_set_sizes_(0), + num_rendered_frames_(0), + black_frame_(false) { + } + + virtual bool SetSize(int width, int height, int reserved) { + width_ = width; + height_ = height; + ++num_set_sizes_; + SignalSetSize(width, height, reserved); + return true; + } + + virtual bool RenderFrame(const VideoFrame* frame) { + // TODO(zhurunz) Check with VP8 team to see if we can remove this + // tolerance on Y values. + black_frame_ = CheckFrameColorYuv(6, 48, 128, 128, 128, 128, frame); + // Treat unexpected frame size as error. + if (!frame || + frame->GetWidth() != static_cast(width_) || + frame->GetHeight() != static_cast(height_)) { + ++errors_; + return false; + } + ++num_rendered_frames_; + SignalRenderFrame(frame); + return true; + } + + int errors() const { return errors_; } + int width() const { return width_; } + int height() const { return height_; } + int num_set_sizes() const { return num_set_sizes_; } + int num_rendered_frames() const { return num_rendered_frames_; } + bool black_frame() const { return black_frame_; } + + sigslot::signal3 SignalSetSize; + sigslot::signal1 SignalRenderFrame; + + private: + static bool CheckFrameColorYuv(uint8 y_min, uint8 y_max, + uint8 u_min, uint8 u_max, + uint8 v_min, uint8 v_max, + const cricket::VideoFrame* frame) { + if (!frame) { + return false; + } + // Y + size_t y_width = frame->GetWidth(); + size_t y_height = frame->GetHeight(); + const uint8* y_plane = frame->GetYPlane(); + const uint8* y_pos = y_plane; + int32 y_pitch = frame->GetYPitch(); + for (size_t i = 0; i < y_height; ++i) { + for (size_t j = 0; j < y_width; ++j) { + uint8 y_value = *(y_pos + j); + if (y_value < y_min || y_value > y_max) { + return false; + } + } + y_pos += y_pitch; + } + // U and V + size_t chroma_width = frame->GetChromaWidth(); + size_t chroma_height = frame->GetChromaHeight(); + const uint8* u_plane = frame->GetUPlane(); + const uint8* v_plane = frame->GetVPlane(); + const uint8* u_pos = u_plane; + const uint8* v_pos = v_plane; + int32 u_pitch = frame->GetUPitch(); + int32 v_pitch = frame->GetVPitch(); + for (size_t i = 0; i < chroma_height; ++i) { + for (size_t j = 0; j < chroma_width; ++j) { + uint8 u_value = *(u_pos + j); + if (u_value < u_min || u_value > u_max) { + return false; + } + uint8 v_value = *(v_pos + j); + if (v_value < v_min || v_value > v_max) { + return false; + } + } + u_pos += u_pitch; + v_pos += v_pitch; + } + return true; + } + + int errors_; + int width_; + int height_; + int num_set_sizes_; + int num_rendered_frames_; + bool black_frame_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_FAKEVIDEORENDERER_H_ diff --git a/talk/media/base/filemediaengine.cc b/talk/media/base/filemediaengine.cc new file mode 100644 index 000000000..fe4831171 --- /dev/null +++ b/talk/media/base/filemediaengine.cc @@ -0,0 +1,342 @@ +// libjingle +// Copyright 2004 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/media/base/filemediaengine.h" + +#include + +#include "talk/base/buffer.h" +#include "talk/base/event.h" +#include "talk/base/logging.h" +#include "talk/base/pathutils.h" +#include "talk/base/stream.h" +#include "talk/media/base/rtpdump.h" +#include "talk/media/base/rtputils.h" +#include "talk/media/base/streamparams.h" + +namespace cricket { + +/////////////////////////////////////////////////////////////////////////// +// Implementation of FileMediaEngine. +/////////////////////////////////////////////////////////////////////////// +int FileMediaEngine::GetCapabilities() { + int capabilities = 0; + if (!voice_input_filename_.empty()) { + capabilities |= AUDIO_SEND; + } + if (!voice_output_filename_.empty()) { + capabilities |= AUDIO_RECV; + } + if (!video_input_filename_.empty()) { + capabilities |= VIDEO_SEND; + } + if (!video_output_filename_.empty()) { + capabilities |= VIDEO_RECV; + } + return capabilities; +} + +VoiceMediaChannel* FileMediaEngine::CreateChannel() { + talk_base::FileStream* input_file_stream = NULL; + talk_base::FileStream* output_file_stream = NULL; + + if (voice_input_filename_.empty() && voice_output_filename_.empty()) + return NULL; + if (!voice_input_filename_.empty()) { + input_file_stream = talk_base::Filesystem::OpenFile( + talk_base::Pathname(voice_input_filename_), "rb"); + if (!input_file_stream) { + LOG(LS_ERROR) << "Not able to open the input audio stream file."; + return NULL; + } + } + + if (!voice_output_filename_.empty()) { + output_file_stream = talk_base::Filesystem::OpenFile( + talk_base::Pathname(voice_output_filename_), "wb"); + if (!output_file_stream) { + delete input_file_stream; + LOG(LS_ERROR) << "Not able to open the output audio stream file."; + return NULL; + } + } + + return new FileVoiceChannel(input_file_stream, output_file_stream); +} + +VideoMediaChannel* FileMediaEngine::CreateVideoChannel( + VoiceMediaChannel* voice_ch) { + talk_base::FileStream* input_file_stream = NULL; + talk_base::FileStream* output_file_stream = NULL; + + if (video_input_filename_.empty() && video_output_filename_.empty()) + return NULL; + + if (!video_input_filename_.empty()) { + input_file_stream = talk_base::Filesystem::OpenFile( + talk_base::Pathname(video_input_filename_), "rb"); + if (!input_file_stream) { + LOG(LS_ERROR) << "Not able to open the input video stream file."; + return NULL; + } + } + + if (!video_output_filename_.empty()) { + output_file_stream = talk_base::Filesystem::OpenFile( + talk_base::Pathname(video_output_filename_), "wb"); + if (!output_file_stream) { + delete input_file_stream; + LOG(LS_ERROR) << "Not able to open the output video stream file."; + return NULL; + } + } + + return new FileVideoChannel(input_file_stream, output_file_stream); +} + +/////////////////////////////////////////////////////////////////////////// +// Definition of RtpSenderReceiver. +/////////////////////////////////////////////////////////////////////////// +class RtpSenderReceiver + : public talk_base::Thread, public talk_base::MessageHandler { + public: + RtpSenderReceiver(MediaChannel* channel, + talk_base::StreamInterface* input_file_stream, + talk_base::StreamInterface* output_file_stream); + + // Called by media channel. Context: media channel thread. + bool SetSend(bool send); + void SetSendSsrc(uint32 ssrc); + void OnPacketReceived(talk_base::Buffer* packet); + + // Override virtual method of parent MessageHandler. Context: Worker Thread. + virtual void OnMessage(talk_base::Message* pmsg); + + private: + // Read the next RTP dump packet, whose RTP SSRC is the same as first_ssrc_. + // Return true if successful. + bool ReadNextPacket(RtpDumpPacket* packet); + // Send a RTP packet to the network. The input parameter data points to the + // start of the RTP packet and len is the packet size. Return true if the sent + // size is equal to len. + bool SendRtpPacket(const void* data, size_t len); + + MediaChannel* media_channel_; + talk_base::scoped_ptr input_stream_; + talk_base::scoped_ptr output_stream_; + talk_base::scoped_ptr rtp_dump_reader_; + talk_base::scoped_ptr rtp_dump_writer_; + // RTP dump packet read from the input stream. + RtpDumpPacket rtp_dump_packet_; + uint32 start_send_time_; + bool sending_; + bool first_packet_; + uint32 first_ssrc_; + + DISALLOW_COPY_AND_ASSIGN(RtpSenderReceiver); +}; + +/////////////////////////////////////////////////////////////////////////// +// Implementation of RtpSenderReceiver. +/////////////////////////////////////////////////////////////////////////// +RtpSenderReceiver::RtpSenderReceiver( + MediaChannel* channel, + talk_base::StreamInterface* input_file_stream, + talk_base::StreamInterface* output_file_stream) + : media_channel_(channel), + sending_(false), + first_packet_(true) { + input_stream_.reset(input_file_stream); + if (input_stream_) { + rtp_dump_reader_.reset(new RtpDumpLoopReader(input_stream_.get())); + // Start the sender thread, which reads rtp dump records, waits based on + // the record timestamps, and sends the RTP packets to the network. + Thread::Start(); + } + + // Create a rtp dump writer for the output RTP dump stream. + output_stream_.reset(output_file_stream); + if (output_stream_) { + rtp_dump_writer_.reset(new RtpDumpWriter(output_stream_.get())); + } +} + +bool RtpSenderReceiver::SetSend(bool send) { + bool was_sending = sending_; + sending_ = send; + if (!was_sending && sending_) { + PostDelayed(0, this); // Wake up the send thread. + start_send_time_ = talk_base::Time(); + } + return true; +} + +void RtpSenderReceiver::SetSendSsrc(uint32 ssrc) { + if (rtp_dump_reader_) { + rtp_dump_reader_->SetSsrc(ssrc); + } +} + +void RtpSenderReceiver::OnPacketReceived(talk_base::Buffer* packet) { + if (rtp_dump_writer_) { + rtp_dump_writer_->WriteRtpPacket(packet->data(), packet->length()); + } +} + +void RtpSenderReceiver::OnMessage(talk_base::Message* pmsg) { + if (!sending_) { + // If the sender thread is not sending, ignore this message. The thread goes + // to sleep until SetSend(true) wakes it up. + return; + } + + if (!first_packet_) { + // Send the previously read packet. + SendRtpPacket(&rtp_dump_packet_.data[0], rtp_dump_packet_.data.size()); + } + + if (ReadNextPacket(&rtp_dump_packet_)) { + int wait = talk_base::TimeUntil( + start_send_time_ + rtp_dump_packet_.elapsed_time); + wait = talk_base::_max(0, wait); + PostDelayed(wait, this); + } else { + Quit(); + } +} + +bool RtpSenderReceiver::ReadNextPacket(RtpDumpPacket* packet) { + while (talk_base::SR_SUCCESS == rtp_dump_reader_->ReadPacket(packet)) { + uint32 ssrc; + if (!packet->GetRtpSsrc(&ssrc)) { + return false; + } + if (first_packet_) { + first_packet_ = false; + first_ssrc_ = ssrc; + } + if (ssrc == first_ssrc_) { + return true; + } + } + return false; +} + +bool RtpSenderReceiver::SendRtpPacket(const void* data, size_t len) { + if (!media_channel_ || !media_channel_->network_interface()) { + return false; + } + + talk_base::Buffer packet(data, len, kMaxRtpPacketLen); + return media_channel_->network_interface()->SendPacket(&packet); +} + +/////////////////////////////////////////////////////////////////////////// +// Implementation of FileVoiceChannel. +/////////////////////////////////////////////////////////////////////////// +FileVoiceChannel::FileVoiceChannel( + talk_base::StreamInterface* input_file_stream, + talk_base::StreamInterface* output_file_stream) + : send_ssrc_(0), + rtp_sender_receiver_(new RtpSenderReceiver(this, input_file_stream, + output_file_stream)) {} + +FileVoiceChannel::~FileVoiceChannel() {} + +bool FileVoiceChannel::SetSendCodecs(const std::vector& codecs) { + // TODO(whyuan): Check the format of RTP dump input. + return true; +} + +bool FileVoiceChannel::SetSend(SendFlags flag) { + return rtp_sender_receiver_->SetSend(flag != SEND_NOTHING); +} + +bool FileVoiceChannel::AddSendStream(const StreamParams& sp) { + if (send_ssrc_ != 0 || sp.ssrcs.size() != 1) { + LOG(LS_ERROR) << "FileVoiceChannel only supports one send stream."; + return false; + } + send_ssrc_ = sp.ssrcs[0]; + rtp_sender_receiver_->SetSendSsrc(send_ssrc_); + return true; +} + +bool FileVoiceChannel::RemoveSendStream(uint32 ssrc) { + if (ssrc != send_ssrc_) + return false; + send_ssrc_ = 0; + rtp_sender_receiver_->SetSendSsrc(send_ssrc_); + return true; +} + +void FileVoiceChannel::OnPacketReceived(talk_base::Buffer* packet) { + rtp_sender_receiver_->OnPacketReceived(packet); +} + +/////////////////////////////////////////////////////////////////////////// +// Implementation of FileVideoChannel. +/////////////////////////////////////////////////////////////////////////// +FileVideoChannel::FileVideoChannel( + talk_base::StreamInterface* input_file_stream, + talk_base::StreamInterface* output_file_stream) + : send_ssrc_(0), + rtp_sender_receiver_(new RtpSenderReceiver(this, input_file_stream, + output_file_stream)) {} + +FileVideoChannel::~FileVideoChannel() {} + +bool FileVideoChannel::SetSendCodecs(const std::vector& codecs) { + // TODO(whyuan): Check the format of RTP dump input. + return true; +} + +bool FileVideoChannel::SetSend(bool send) { + return rtp_sender_receiver_->SetSend(send); +} + +bool FileVideoChannel::AddSendStream(const StreamParams& sp) { + if (send_ssrc_ != 0 || sp.ssrcs.size() != 1) { + LOG(LS_ERROR) << "FileVideoChannel only support one send stream."; + return false; + } + send_ssrc_ = sp.ssrcs[0]; + rtp_sender_receiver_->SetSendSsrc(send_ssrc_); + return true; +} + +bool FileVideoChannel::RemoveSendStream(uint32 ssrc) { + if (ssrc != send_ssrc_) + return false; + send_ssrc_ = 0; + rtp_sender_receiver_->SetSendSsrc(send_ssrc_); + return true; +} + +void FileVideoChannel::OnPacketReceived(talk_base::Buffer* packet) { + rtp_sender_receiver_->OnPacketReceived(packet); +} + +} // namespace cricket diff --git a/talk/media/base/filemediaengine.h b/talk/media/base/filemediaengine.h new file mode 100644 index 000000000..70335de59 --- /dev/null +++ b/talk/media/base/filemediaengine.h @@ -0,0 +1,316 @@ +// libjingle +// Copyright 2004 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. + +#ifndef TALK_MEDIA_BASE_FILEMEDIAENGINE_H_ +#define TALK_MEDIA_BASE_FILEMEDIAENGINE_H_ + +#include +#include + +#include "talk/base/scoped_ptr.h" +#include "talk/base/stream.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/mediaengine.h" + +namespace talk_base { +class StreamInterface; +} + +namespace cricket { + +// A media engine contains a capturer, an encoder, and a sender in the sender +// side and a receiver, a decoder, and a renderer in the receiver side. +// FileMediaEngine simulates the capturer and the encoder via an input RTP dump +// stream and simulates the decoder and the renderer via an output RTP dump +// stream. Depending on the parameters of the constructor, FileMediaEngine can +// act as file voice engine, file video engine, or both. Currently, we use +// only the RTP dump packets. TODO(whyuan): Enable RTCP packets. +class FileMediaEngine : public MediaEngineInterface { + public: + FileMediaEngine() {} + virtual ~FileMediaEngine() {} + + // Set the file name of the input or output RTP dump for voice or video. + // Should be called before the channel is created. + void set_voice_input_filename(const std::string& filename) { + voice_input_filename_ = filename; + } + void set_voice_output_filename(const std::string& filename) { + voice_output_filename_ = filename; + } + void set_video_input_filename(const std::string& filename) { + video_input_filename_ = filename; + } + void set_video_output_filename(const std::string& filename) { + video_output_filename_ = filename; + } + + // Should be called before codecs() and video_codecs() are called. We need to + // set the voice and video codecs; otherwise, Jingle initiation will fail. + void set_voice_codecs(const std::vector& codecs) { + voice_codecs_ = codecs; + } + void set_video_codecs(const std::vector& codecs) { + video_codecs_ = codecs; + } + + // Implement pure virtual methods of MediaEngine. + virtual bool Init(talk_base::Thread* worker_thread) { + return true; + } + virtual void Terminate() {} + virtual int GetCapabilities(); + virtual VoiceMediaChannel* CreateChannel(); + virtual VideoMediaChannel* CreateVideoChannel(VoiceMediaChannel* voice_ch); + virtual SoundclipMedia* CreateSoundclip() { return NULL; } + virtual bool SetAudioOptions(int options) { return true; } + virtual bool SetVideoOptions(int options) { return true; } + virtual bool SetAudioDelayOffset(int offset) { return true; } + virtual bool SetDefaultVideoEncoderConfig(const VideoEncoderConfig& config) { + return true; + } + virtual bool SetSoundDevices(const Device* in_dev, const Device* out_dev) { + return true; + } + virtual bool SetVideoCaptureDevice(const Device* cam_device) { return true; } + virtual bool SetVideoCapturer(VideoCapturer* /*capturer*/) { + return true; + } + virtual VideoCapturer* GetVideoCapturer() const { + return NULL; + } + virtual bool GetOutputVolume(int* level) { + *level = 0; + return true; + } + virtual bool SetOutputVolume(int level) { return true; } + virtual int GetInputLevel() { return 0; } + virtual bool SetLocalMonitor(bool enable) { return true; } + virtual bool SetLocalRenderer(VideoRenderer* renderer) { return true; } + // TODO(whyuan): control channel send? + virtual bool SetVideoCapture(bool capture) { return true; } + virtual const std::vector& audio_codecs() { + return voice_codecs_; + } + virtual const std::vector& video_codecs() { + return video_codecs_; + } + virtual const std::vector& audio_rtp_header_extensions() { + return audio_rtp_header_extensions_; + } + virtual const std::vector& video_rtp_header_extensions() { + return video_rtp_header_extensions_; + } + + virtual bool FindAudioCodec(const AudioCodec& codec) { return true; } + virtual bool FindVideoCodec(const VideoCodec& codec) { return true; } + virtual void SetVoiceLogging(int min_sev, const char* filter) {} + virtual void SetVideoLogging(int min_sev, const char* filter) {} + + virtual bool RegisterVideoProcessor(VideoProcessor* processor) { + return true; + } + virtual bool UnregisterVideoProcessor(VideoProcessor* processor) { + return true; + } + virtual bool RegisterVoiceProcessor(uint32 ssrc, + VoiceProcessor* processor, + MediaProcessorDirection direction) { + return true; + } + virtual bool UnregisterVoiceProcessor(uint32 ssrc, + VoiceProcessor* processor, + MediaProcessorDirection direction) { + return true; + } + VideoFormat GetStartCaptureFormat() const { + return VideoFormat(); + } + + virtual sigslot::repeater2& + SignalVideoCaptureStateChange() { + return signal_state_change_; + } + + private: + std::string voice_input_filename_; + std::string voice_output_filename_; + std::string video_input_filename_; + std::string video_output_filename_; + std::vector voice_codecs_; + std::vector video_codecs_; + std::vector audio_rtp_header_extensions_; + std::vector video_rtp_header_extensions_; + sigslot::repeater2 + signal_state_change_; + + DISALLOW_COPY_AND_ASSIGN(FileMediaEngine); +}; + +class RtpSenderReceiver; // Forward declaration. Defined in the .cc file. + +class FileVoiceChannel : public VoiceMediaChannel { + public: + FileVoiceChannel(talk_base::StreamInterface* input_file_stream, + talk_base::StreamInterface* output_file_stream); + virtual ~FileVoiceChannel(); + + // Implement pure virtual methods of VoiceMediaChannel. + virtual bool SetRecvCodecs(const std::vector& codecs) { + return true; + } + virtual bool SetSendCodecs(const std::vector& codecs); + virtual bool SetRecvRtpHeaderExtensions( + const std::vector& extensions) { + return true; + } + virtual bool SetSendRtpHeaderExtensions( + const std::vector& extensions) { + return true; + } + virtual bool SetPlayout(bool playout) { return true; } + virtual bool SetSend(SendFlags flag); + virtual bool SetRenderer(uint32 ssrc, AudioRenderer* renderer) { + return false; + } + virtual bool GetActiveStreams(AudioInfo::StreamList* actives) { return true; } + virtual int GetOutputLevel() { return 0; } + virtual int GetTimeSinceLastTyping() { return -1; } + virtual void SetTypingDetectionParameters(int time_window, + int cost_per_typing, int reporting_threshold, int penalty_decay, + int type_event_delay) {} + + virtual bool SetOutputScaling(uint32 ssrc, double left, double right) { + return false; + } + virtual bool GetOutputScaling(uint32 ssrc, double* left, double* right) { + return false; + } + virtual bool SetRingbackTone(const char* buf, int len) { return true; } + virtual bool PlayRingbackTone(uint32 ssrc, bool play, bool loop) { + return true; + } + virtual bool InsertDtmf(uint32 ssrc, int event, int duration, int flags) { + return false; + } + virtual bool GetStats(VoiceMediaInfo* info) { return true; } + + // Implement pure virtual methods of MediaChannel. + virtual void OnPacketReceived(talk_base::Buffer* packet); + virtual void OnRtcpReceived(talk_base::Buffer* packet) {} + virtual void OnReadyToSend(bool ready) {} + virtual bool AddSendStream(const StreamParams& sp); + virtual bool RemoveSendStream(uint32 ssrc); + virtual bool AddRecvStream(const StreamParams& sp) { return true; } + virtual bool RemoveRecvStream(uint32 ssrc) { return true; } + virtual bool MuteStream(uint32 ssrc, bool on) { return false; } + virtual bool SetSendBandwidth(bool autobw, int bps) { return true; } + virtual bool SetOptions(const AudioOptions& options) { + options_ = options; + return true; + } + virtual bool GetOptions(AudioOptions* options) const { + *options = options_; + return true; + } + + private: + uint32 send_ssrc_; + talk_base::scoped_ptr rtp_sender_receiver_; + AudioOptions options_; + + DISALLOW_COPY_AND_ASSIGN(FileVoiceChannel); +}; + +class FileVideoChannel : public VideoMediaChannel { + public: + FileVideoChannel(talk_base::StreamInterface* input_file_stream, + talk_base::StreamInterface* output_file_stream); + virtual ~FileVideoChannel(); + + // Implement pure virtual methods of VideoMediaChannel. + virtual bool SetRecvCodecs(const std::vector& codecs) { + return true; + } + virtual bool SetSendCodecs(const std::vector& codecs); + virtual bool GetSendCodec(VideoCodec* send_codec) { + *send_codec = VideoCodec(); + return true; + } + virtual bool SetSendStreamFormat(uint32 ssrc, const VideoFormat& format) { + return true; + } + virtual bool SetRecvRtpHeaderExtensions( + const std::vector& extensions) { + return true; + } + virtual bool SetSendRtpHeaderExtensions( + const std::vector& extensions) { + return true; + } + virtual bool SetRender(bool render) { return true; } + virtual bool SetSend(bool send); + virtual bool SetRenderer(uint32 ssrc, VideoRenderer* renderer) { + return true; + } + virtual bool SetCapturer(uint32 ssrc, VideoCapturer* capturer) { + return false; + } + virtual bool GetStats(VideoMediaInfo* info) { return true; } + virtual bool SendIntraFrame() { return false; } + virtual bool RequestIntraFrame() { return false; } + + // Implement pure virtual methods of MediaChannel. + virtual void OnPacketReceived(talk_base::Buffer* packet); + virtual void OnRtcpReceived(talk_base::Buffer* packet) {} + virtual void OnReadyToSend(bool ready) {} + virtual bool AddSendStream(const StreamParams& sp); + virtual bool RemoveSendStream(uint32 ssrc); + virtual bool AddRecvStream(const StreamParams& sp) { return true; } + virtual bool RemoveRecvStream(uint32 ssrc) { return true; } + virtual bool MuteStream(uint32 ssrc, bool on) { return false; } + virtual bool SetSendBandwidth(bool autobw, int bps) { return true; } + virtual bool SetOptions(const VideoOptions& options) { + options_ = options; + return true; + } + virtual bool GetOptions(VideoOptions* options) const { + *options = options_; + return true; + } + virtual void UpdateAspectRatio(int ratio_w, int ratio_h) {} + + private: + uint32 send_ssrc_; + talk_base::scoped_ptr rtp_sender_receiver_; + VideoOptions options_; + + DISALLOW_COPY_AND_ASSIGN(FileVideoChannel); +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_FILEMEDIAENGINE_H_ diff --git a/talk/media/base/filemediaengine_unittest.cc b/talk/media/base/filemediaengine_unittest.cc new file mode 100644 index 000000000..a2c91a16c --- /dev/null +++ b/talk/media/base/filemediaengine_unittest.cc @@ -0,0 +1,463 @@ +/* + * libjingle + * Copyright 2004 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 + +#include "talk/base/buffer.h" +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/pathutils.h" +#include "talk/base/stream.h" +#include "talk/media/base/filemediaengine.h" +#include "talk/media/base/rtpdump.h" +#include "talk/media/base/streamparams.h" +#include "talk/media/base/testutils.h" + +namespace cricket { + +static const int kWaitTimeMs = 100; +static const std::string kFakeFileName = "foobar"; + +////////////////////////////////////////////////////////////////////////////// +// Media channel sends RTP packets via NetworkInterface. Rather than sending +// packets to the network, FileNetworkInterface writes packets to a stream and +// feeds packets back to the channel via OnPacketReceived. +////////////////////////////////////////////////////////////////////////////// +class FileNetworkInterface : public MediaChannel::NetworkInterface { + public: + FileNetworkInterface(talk_base::StreamInterface* output, MediaChannel* ch) + : media_channel_(ch), + num_sent_packets_(0) { + if (output) { + dump_writer_.reset(new RtpDumpWriter(output)); + } + } + + // Implement pure virtual methods of NetworkInterface. + virtual bool SendPacket(talk_base::Buffer* packet) { + if (!packet) return false; + + if (media_channel_) { + media_channel_->OnPacketReceived(packet); + } + if (dump_writer_.get() && + talk_base::SR_SUCCESS != dump_writer_->WriteRtpPacket( + packet->data(), packet->length())) { + return false; + } + + ++num_sent_packets_; + return true; + } + + virtual bool SendRtcp(talk_base::Buffer* packet) { return false; } + virtual int SetOption(MediaChannel::NetworkInterface::SocketType type, + talk_base::Socket::Option opt, int option) { + return 0; + } + + size_t num_sent_packets() const { return num_sent_packets_; } + + private: + MediaChannel* media_channel_; + talk_base::scoped_ptr dump_writer_; + size_t num_sent_packets_; + + DISALLOW_COPY_AND_ASSIGN(FileNetworkInterface); +}; + +class FileMediaEngineTest : public testing::Test { + public: + virtual void SetUp() { + setup_ok_ = true; + setup_ok_ &= GetTempFilename(&voice_input_filename_); + setup_ok_ &= GetTempFilename(&voice_output_filename_); + setup_ok_ &= GetTempFilename(&video_input_filename_); + setup_ok_ &= GetTempFilename(&video_output_filename_); + } + virtual void TearDown() { + // Force to close the dump files, if opened. + voice_channel_.reset(); + video_channel_.reset(); + + DeleteTempFile(voice_input_filename_); + DeleteTempFile(voice_output_filename_); + DeleteTempFile(video_input_filename_); + DeleteTempFile(video_output_filename_); + } + + protected: + bool CreateEngineAndChannels(const std::string& voice_in, + const std::string& voice_out, + const std::string& video_in, + const std::string& video_out, + size_t ssrc_count) { + // Force to close the dump files, if opened. + voice_channel_.reset(); + video_channel_.reset(); + + bool ret = setup_ok_; + if (!voice_in.empty()) { + ret &= WriteTestPacketsToFile(voice_in, ssrc_count); + } + if (!video_in.empty()) { + ret &= WriteTestPacketsToFile(video_in, ssrc_count); + } + + engine_.reset(new FileMediaEngine); + engine_->set_voice_input_filename(voice_in); + engine_->set_voice_output_filename(voice_out); + engine_->set_video_input_filename(video_in); + engine_->set_video_output_filename(video_out); + + voice_channel_.reset(engine_->CreateChannel()); + video_channel_.reset(engine_->CreateVideoChannel(NULL)); + + return ret; + } + + bool GetTempFilename(std::string* filename) { + talk_base::Pathname temp_path; + if (!talk_base::Filesystem::GetTemporaryFolder(temp_path, true, NULL)) { + return false; + } + temp_path.SetPathname( + talk_base::Filesystem::TempFilename(temp_path, "fme-test-")); + + if (filename) { + *filename = temp_path.pathname(); + } + return true; + } + + bool WriteTestPacketsToFile(const std::string& filename, size_t ssrc_count) { + talk_base::scoped_ptr stream( + talk_base::Filesystem::OpenFile(talk_base::Pathname(filename), "wb")); + bool ret = (NULL != stream.get()); + RtpDumpWriter writer(stream.get()); + + for (size_t i = 0; i < ssrc_count; ++i) { + ret &= RtpTestUtility::WriteTestPackets( + RtpTestUtility::GetTestPacketCount(), false, + RtpTestUtility::kDefaultSsrc + i, &writer); + } + return ret; + } + + void DeleteTempFile(std::string filename) { + talk_base::Pathname pathname(filename); + if (talk_base::Filesystem::IsFile(talk_base::Pathname(pathname))) { + talk_base::Filesystem::DeleteFile(pathname); + } + } + + bool GetSsrcAndPacketCounts(talk_base::StreamInterface* stream, + size_t* ssrc_count, size_t* packet_count) { + talk_base::scoped_ptr reader(new RtpDumpReader(stream)); + size_t count = 0; + RtpDumpPacket packet; + std::set ssrcs; + while (talk_base::SR_SUCCESS == reader->ReadPacket(&packet)) { + count++; + uint32 ssrc; + if (!packet.GetRtpSsrc(&ssrc)) { + return false; + } + ssrcs.insert(ssrc); + } + if (ssrc_count) { + *ssrc_count = ssrcs.size(); + } + if (packet_count) { + *packet_count = count; + } + return true; + } + + static const uint32 kWaitTimeout = 3000; + bool setup_ok_; + std::string voice_input_filename_; + std::string voice_output_filename_; + std::string video_input_filename_; + std::string video_output_filename_; + talk_base::scoped_ptr engine_; + talk_base::scoped_ptr voice_channel_; + talk_base::scoped_ptr video_channel_; +}; + +TEST_F(FileMediaEngineTest, TestDefaultImplementation) { + EXPECT_TRUE(CreateEngineAndChannels("", "", "", "", 1)); + EXPECT_TRUE(engine_->Init(talk_base::Thread::Current())); + EXPECT_EQ(0, engine_->GetCapabilities()); + EXPECT_TRUE(NULL == voice_channel_.get()); + EXPECT_TRUE(NULL == video_channel_.get()); + EXPECT_TRUE(NULL == engine_->CreateSoundclip()); + EXPECT_TRUE(engine_->SetAudioOptions(0)); + EXPECT_TRUE(engine_->SetVideoOptions(0)); + VideoEncoderConfig video_encoder_config; + EXPECT_TRUE(engine_->SetDefaultVideoEncoderConfig(video_encoder_config)); + EXPECT_TRUE(engine_->SetSoundDevices(NULL, NULL)); + EXPECT_TRUE(engine_->SetVideoCaptureDevice(NULL)); + EXPECT_TRUE(engine_->SetOutputVolume(0)); + EXPECT_EQ(0, engine_->GetInputLevel()); + EXPECT_TRUE(engine_->SetLocalMonitor(true)); + EXPECT_TRUE(engine_->SetLocalRenderer(NULL)); + EXPECT_TRUE(engine_->SetVideoCapture(true)); + EXPECT_EQ(0U, engine_->audio_codecs().size()); + EXPECT_EQ(0U, engine_->video_codecs().size()); + AudioCodec voice_codec; + EXPECT_TRUE(engine_->FindAudioCodec(voice_codec)); + VideoCodec video_codec; + EXPECT_TRUE(engine_->FindVideoCodec(video_codec)); + engine_->Terminate(); +} + +// Test that when file path is not pointing to a valid stream file, the channel +// creation function should fail and return NULL. +TEST_F(FileMediaEngineTest, TestBadFilePath) { + engine_.reset(new FileMediaEngine); + engine_->set_voice_input_filename(kFakeFileName); + engine_->set_video_input_filename(kFakeFileName); + EXPECT_TRUE(engine_->CreateChannel() == NULL); + EXPECT_TRUE(engine_->CreateVideoChannel(NULL) == NULL); +} + +TEST_F(FileMediaEngineTest, TestCodecs) { + EXPECT_TRUE(CreateEngineAndChannels("", "", "", "", 1)); + std::vector voice_codecs = engine_->audio_codecs(); + std::vector video_codecs = engine_->video_codecs(); + EXPECT_EQ(0U, voice_codecs.size()); + EXPECT_EQ(0U, video_codecs.size()); + + AudioCodec voice_codec(103, "ISAC", 16000, 0, 1, 0); + voice_codecs.push_back(voice_codec); + engine_->set_voice_codecs(voice_codecs); + voice_codecs = engine_->audio_codecs(); + ASSERT_EQ(1U, voice_codecs.size()); + EXPECT_EQ(voice_codec, voice_codecs[0]); + + VideoCodec video_codec(96, "H264-SVC", 320, 240, 30, 0); + video_codecs.push_back(video_codec); + engine_->set_video_codecs(video_codecs); + video_codecs = engine_->video_codecs(); + ASSERT_EQ(1U, video_codecs.size()); + EXPECT_EQ(video_codec, video_codecs[0]); +} + +// Test that the capabilities and channel creation of the Filemedia engine +// depend on the stream parameters passed to its constructor. +TEST_F(FileMediaEngineTest, TestGetCapabilities) { + EXPECT_TRUE(CreateEngineAndChannels(voice_input_filename_, "", "", "", 1)); + EXPECT_EQ(AUDIO_SEND, engine_->GetCapabilities()); + EXPECT_TRUE(NULL != voice_channel_.get()); + EXPECT_TRUE(NULL == video_channel_.get()); + + EXPECT_TRUE(CreateEngineAndChannels(voice_input_filename_, + voice_output_filename_, "", "", 1)); + EXPECT_EQ(AUDIO_SEND | AUDIO_RECV, engine_->GetCapabilities()); + EXPECT_TRUE(NULL != voice_channel_.get()); + EXPECT_TRUE(NULL == video_channel_.get()); + + EXPECT_TRUE(CreateEngineAndChannels("", "", video_input_filename_, "", 1)); + EXPECT_EQ(VIDEO_SEND, engine_->GetCapabilities()); + EXPECT_TRUE(NULL == voice_channel_.get()); + EXPECT_TRUE(NULL != video_channel_.get()); + + EXPECT_TRUE(CreateEngineAndChannels(voice_input_filename_, + voice_output_filename_, + video_input_filename_, + video_output_filename_, + 1)); + EXPECT_EQ(AUDIO_SEND | AUDIO_RECV | VIDEO_SEND | VIDEO_RECV, + engine_->GetCapabilities()); + EXPECT_TRUE(NULL != voice_channel_.get()); + EXPECT_TRUE(NULL != video_channel_.get()); +} + +// FileVideoChannel is the same as FileVoiceChannel in terms of receiving and +// sending the RTP packets. We therefore test only FileVoiceChannel. + +// Test that SetSend() controls whether a voice channel sends RTP packets. +TEST_F(FileMediaEngineTest, TestVoiceChannelSetSend) { + EXPECT_TRUE(CreateEngineAndChannels(voice_input_filename_, + voice_output_filename_, "", "", 1)); + EXPECT_TRUE(NULL != voice_channel_.get()); + talk_base::MemoryStream net_dump; + FileNetworkInterface net_interface(&net_dump, voice_channel_.get()); + voice_channel_->SetInterface(&net_interface); + + // The channel is not sending yet. + talk_base::Thread::Current()->ProcessMessages(kWaitTimeMs); + EXPECT_EQ(0U, net_interface.num_sent_packets()); + + // The channel starts sending. + voice_channel_->SetSend(SEND_MICROPHONE); + EXPECT_TRUE_WAIT(net_interface.num_sent_packets() >= 1U, kWaitTimeout); + + // The channel stops sending. + voice_channel_->SetSend(SEND_NOTHING); + // Wait until packets are all delivered. + talk_base::Thread::Current()->ProcessMessages(kWaitTimeMs); + size_t old_number = net_interface.num_sent_packets(); + talk_base::Thread::Current()->ProcessMessages(kWaitTimeMs); + EXPECT_EQ(old_number, net_interface.num_sent_packets()); + + // The channel starts sending again. + voice_channel_->SetSend(SEND_MICROPHONE); + EXPECT_TRUE_WAIT(net_interface.num_sent_packets() > old_number, kWaitTimeout); + + // When the function exits, the net_interface object is released. The sender + // thread may call net_interface to send packets, which results in a segment + // fault. We hence stop sending and wait until all packets are delivered + // before we exit this function. + voice_channel_->SetSend(SEND_NOTHING); + talk_base::Thread::Current()->ProcessMessages(kWaitTimeMs); +} + +// Test the sender thread of the channel. The sender sends RTP packets +// continuously with proper sequence number, timestamp, and payload. +TEST_F(FileMediaEngineTest, TestVoiceChannelSenderThread) { + EXPECT_TRUE(CreateEngineAndChannels(voice_input_filename_, + voice_output_filename_, "", "", 1)); + EXPECT_TRUE(NULL != voice_channel_.get()); + talk_base::MemoryStream net_dump; + FileNetworkInterface net_interface(&net_dump, voice_channel_.get()); + voice_channel_->SetInterface(&net_interface); + + voice_channel_->SetSend(SEND_MICROPHONE); + // Wait until the number of sent packets is no less than 2 * kPacketNumber. + EXPECT_TRUE_WAIT( + net_interface.num_sent_packets() >= + 2 * RtpTestUtility::GetTestPacketCount(), + kWaitTimeout); + voice_channel_->SetSend(SEND_NOTHING); + // Wait until packets are all delivered. + talk_base::Thread::Current()->ProcessMessages(kWaitTimeMs); + EXPECT_TRUE(RtpTestUtility::VerifyTestPacketsFromStream( + 2 * RtpTestUtility::GetTestPacketCount(), &net_dump, + RtpTestUtility::kDefaultSsrc)); + + // Each sent packet is dumped to net_dump and is also feed to the channel + // via OnPacketReceived, which in turn writes the packets into voice_output_. + // We next verify the packets in voice_output_. + voice_channel_.reset(); // Force to close the files. + talk_base::scoped_ptr voice_output_; + voice_output_.reset(talk_base::Filesystem::OpenFile( + talk_base::Pathname(voice_output_filename_), "rb")); + EXPECT_TRUE(voice_output_.get() != NULL); + EXPECT_TRUE(RtpTestUtility::VerifyTestPacketsFromStream( + 2 * RtpTestUtility::GetTestPacketCount(), voice_output_.get(), + RtpTestUtility::kDefaultSsrc)); +} + +// Test that we can specify the ssrc for outgoing RTP packets. +TEST_F(FileMediaEngineTest, TestVoiceChannelSendSsrc) { + EXPECT_TRUE(CreateEngineAndChannels(voice_input_filename_, + voice_output_filename_, "", "", 1)); + EXPECT_TRUE(NULL != voice_channel_.get()); + const uint32 send_ssrc = RtpTestUtility::kDefaultSsrc + 1; + voice_channel_->AddSendStream(StreamParams::CreateLegacy(send_ssrc)); + + talk_base::MemoryStream net_dump; + FileNetworkInterface net_interface(&net_dump, voice_channel_.get()); + voice_channel_->SetInterface(&net_interface); + + voice_channel_->SetSend(SEND_MICROPHONE); + // Wait until the number of sent packets is no less than 2 * kPacketNumber. + EXPECT_TRUE_WAIT( + net_interface.num_sent_packets() >= + 2 * RtpTestUtility::GetTestPacketCount(), + kWaitTimeout); + voice_channel_->SetSend(SEND_NOTHING); + // Wait until packets are all delivered. + talk_base::Thread::Current()->ProcessMessages(kWaitTimeMs); + EXPECT_TRUE(RtpTestUtility::VerifyTestPacketsFromStream( + 2 * RtpTestUtility::GetTestPacketCount(), &net_dump, send_ssrc)); + + // Each sent packet is dumped to net_dump and is also feed to the channel + // via OnPacketReceived, which in turn writes the packets into voice_output_. + // We next verify the packets in voice_output_. + voice_channel_.reset(); // Force to close the files. + talk_base::scoped_ptr voice_output_; + voice_output_.reset(talk_base::Filesystem::OpenFile( + talk_base::Pathname(voice_output_filename_), "rb")); + EXPECT_TRUE(voice_output_.get() != NULL); + EXPECT_TRUE(RtpTestUtility::VerifyTestPacketsFromStream( + 2 * RtpTestUtility::GetTestPacketCount(), voice_output_.get(), + send_ssrc)); +} + +// Test the sender thread of the channel, where the input rtpdump has two SSRCs. +TEST_F(FileMediaEngineTest, TestVoiceChannelSenderThreadTwoSsrcs) { + EXPECT_TRUE(CreateEngineAndChannels(voice_input_filename_, + voice_output_filename_, "", "", 2)); + // Verify that voice_input_filename_ contains 2 * + // RtpTestUtility::GetTestPacketCount() packets + // with different SSRCs. + talk_base::scoped_ptr input_stream( + talk_base::Filesystem::OpenFile( + talk_base::Pathname(voice_input_filename_), "rb")); + ASSERT_TRUE(NULL != input_stream.get()); + size_t ssrc_count; + size_t packet_count; + EXPECT_TRUE(GetSsrcAndPacketCounts(input_stream.get(), &ssrc_count, + &packet_count)); + EXPECT_EQ(2U, ssrc_count); + EXPECT_EQ(2 * RtpTestUtility::GetTestPacketCount(), packet_count); + input_stream.reset(); + + // Send 2 * RtpTestUtility::GetTestPacketCount() packets and verify that all + // these packets have the same SSRCs (that is, the packets with different + // SSRCs are skipped by the filemediaengine). + EXPECT_TRUE(NULL != voice_channel_.get()); + talk_base::MemoryStream net_dump; + FileNetworkInterface net_interface(&net_dump, voice_channel_.get()); + voice_channel_->SetInterface(&net_interface); + voice_channel_->SetSend(SEND_MICROPHONE); + EXPECT_TRUE_WAIT( + net_interface.num_sent_packets() >= + 2 * RtpTestUtility::GetTestPacketCount(), + kWaitTimeout); + voice_channel_->SetSend(SEND_NOTHING); + // Wait until packets are all delivered. + talk_base::Thread::Current()->ProcessMessages(kWaitTimeMs); + net_dump.Rewind(); + EXPECT_TRUE(GetSsrcAndPacketCounts(&net_dump, &ssrc_count, &packet_count)); + EXPECT_EQ(1U, ssrc_count); + EXPECT_GE(packet_count, 2 * RtpTestUtility::GetTestPacketCount()); +} + +// Test SendIntraFrame() and RequestIntraFrame() of video channel. +TEST_F(FileMediaEngineTest, TestVideoChannelIntraFrame) { + EXPECT_TRUE(CreateEngineAndChannels("", "", video_input_filename_, + video_output_filename_, 1)); + EXPECT_TRUE(NULL != video_channel_.get()); + EXPECT_FALSE(video_channel_->SendIntraFrame()); + EXPECT_FALSE(video_channel_->RequestIntraFrame()); +} + +} // namespace cricket diff --git a/talk/media/base/hybriddataengine.h b/talk/media/base/hybriddataengine.h new file mode 100644 index 000000000..bece492aa --- /dev/null +++ b/talk/media/base/hybriddataengine.h @@ -0,0 +1,76 @@ +/* + * libjingle + * Copyright 2012 Google Inc, and Robin Seggelmann + * + * 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. + */ + +#ifndef TALK_MEDIA_SCTP_HYBRIDDATAENGINE_H_ +#define TALK_MEDIA_SCTP_HYBRIDDATAENGINE_H_ + +#include +#include + +#include "talk/base/scoped_ptr.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/mediaengine.h" + +namespace cricket { + +class HybridDataEngine : public DataEngineInterface { + public: + // Takes ownership. + HybridDataEngine(DataEngineInterface* first, + DataEngineInterface* second) + : first_(first), + second_(second) { + codecs_ = first_->data_codecs(); + codecs_.insert( + codecs_.end(), + second_->data_codecs().begin(), + second_->data_codecs().end()); + } + + virtual DataMediaChannel* CreateChannel(DataChannelType data_channel_type) { + DataMediaChannel* channel = NULL; + if (first_) { + channel = first_->CreateChannel(data_channel_type); + } + if (!channel && second_) { + channel = second_->CreateChannel(data_channel_type); + } + return channel; + } + + virtual const std::vector& data_codecs() { return codecs_; } + + private: + talk_base::scoped_ptr first_; + talk_base::scoped_ptr second_; + std::vector codecs_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_SCTP_HYBRIDDATAENGINE_H_ diff --git a/talk/media/base/hybridvideoengine.cc b/talk/media/base/hybridvideoengine.cc new file mode 100644 index 000000000..a405f8d28 --- /dev/null +++ b/talk/media/base/hybridvideoengine.cc @@ -0,0 +1,350 @@ +/* + * libjingle + * Copyright 2004 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/media/base/hybridvideoengine.h" + +#include "talk/base/logging.h" + +namespace cricket { + +HybridVideoMediaChannel::HybridVideoMediaChannel( + HybridVideoEngineInterface* engine, + VideoMediaChannel* channel1, + VideoMediaChannel* channel2) + : engine_(engine), + channel1_(channel1), + channel2_(channel2), + active_channel_(NULL), + sending_(false) { +} + +HybridVideoMediaChannel::~HybridVideoMediaChannel() { +} + +void HybridVideoMediaChannel::SetInterface(NetworkInterface* iface) { + if (channel1_) { + channel1_->SetInterface(iface); + } + if (channel2_) { + channel2_->SetInterface(iface); + } +} + +bool HybridVideoMediaChannel::SetOptions(const VideoOptions &options) { + bool ret = true; + if (channel1_) { + ret = channel1_->SetOptions(options); + } + if (channel2_ && ret) { + ret = channel2_->SetOptions(options); + } + return ret; +} + +bool HybridVideoMediaChannel::GetOptions(VideoOptions *options) const { + if (active_channel_) { + return active_channel_->GetOptions(options); + } + if (channel1_) { + return channel1_->GetOptions(options); + } + if (channel2_) { + return channel2_->GetOptions(options); + } + return false; +} + +bool HybridVideoMediaChannel::SetRecvCodecs( + const std::vector& codecs) { + // Only give each channel the codecs it knows about. + bool ret = true; + std::vector codecs1, codecs2; + SplitCodecs(codecs, &codecs1, &codecs2); + if (channel1_) { + ret = channel1_->SetRecvCodecs(codecs1); + } + if (channel2_ && ret) { + ret = channel2_->SetRecvCodecs(codecs2); + } + return ret; +} + +bool HybridVideoMediaChannel::SetRecvRtpHeaderExtensions( + const std::vector& extensions) { + bool ret = true; + if (channel1_) { + ret = channel1_->SetRecvRtpHeaderExtensions(extensions); + } + if (channel2_ && ret) { + ret = channel2_->SetRecvRtpHeaderExtensions(extensions); + } + return ret; +} + +bool HybridVideoMediaChannel::SetRenderer(uint32 ssrc, + VideoRenderer* renderer) { + bool ret = true; + if (channel1_) { + ret = channel1_->SetRenderer(ssrc, renderer); + } + if (channel2_ && ret) { + ret = channel2_->SetRenderer(ssrc, renderer); + } + return ret; +} + +bool HybridVideoMediaChannel::SetRender(bool render) { + bool ret = true; + if (channel1_) { + ret = channel1_->SetRender(render); + } + if (channel2_ && ret) { + ret = channel2_->SetRender(render); + } + return ret; +} + +bool HybridVideoMediaChannel::MuteStream(uint32 ssrc, bool muted) { + bool ret = true; + if (channel1_) { + ret = channel1_->MuteStream(ssrc, muted); + } + if (channel2_ && ret) { + ret = channel2_->MuteStream(ssrc, muted); + } + return ret; +} + +bool HybridVideoMediaChannel::SetSendCodecs( + const std::vector& codecs) { + // Use the input to this function to decide what impl we're going to use. + if (!active_channel_ && !SelectActiveChannel(codecs)) { + LOG(LS_WARNING) << "Failed to select active channel"; + return false; + } + // Only give the active channel the codecs it knows about. + std::vector codecs1, codecs2; + SplitCodecs(codecs, &codecs1, &codecs2); + const std::vector& codecs_to_set = + (active_channel_ == channel1_.get()) ? codecs1 : codecs2; + bool return_value = active_channel_->SetSendCodecs(codecs_to_set); + if (!return_value) { + return false; + } + VideoCodec send_codec; + return_value = active_channel_->GetSendCodec(&send_codec); + if (!return_value) { + return false; + } + engine_->OnNewSendResolution(send_codec.width, send_codec.height); + active_channel_->UpdateAspectRatio(send_codec.width, send_codec.height); + return true; +} + +bool HybridVideoMediaChannel::GetSendCodec(VideoCodec* send_codec) { + if (!active_channel_) { + return false; + } + return active_channel_->GetSendCodec(send_codec); +} + +bool HybridVideoMediaChannel::SetSendStreamFormat(uint32 ssrc, + const VideoFormat& format) { + return active_channel_ && active_channel_->SetSendStreamFormat(ssrc, format); +} + +bool HybridVideoMediaChannel::SetSendRtpHeaderExtensions( + const std::vector& extensions) { + return active_channel_ && + active_channel_->SetSendRtpHeaderExtensions(extensions); +} + +bool HybridVideoMediaChannel::SetSendBandwidth(bool autobw, int bps) { + return active_channel_ && + active_channel_->SetSendBandwidth(autobw, bps); +} + +bool HybridVideoMediaChannel::SetSend(bool send) { + if (send == sending()) { + return true; // no action required if already set. + } + + bool ret = active_channel_ && + active_channel_->SetSend(send); + + // Returns error and don't connect the signal if starting up. + // Disconnects the signal anyway if shutting down. + if (ret || !send) { + // TODO(juberti): Remove this hack that connects the WebRTC channel + // to the capturer. + if (active_channel_ == channel1_.get()) { + engine_->OnSendChange1(channel1_.get(), send); + } else { + engine_->OnSendChange2(channel2_.get(), send); + } + // If succeeded, remember the state as is. + // If failed to open, sending_ should be false. + // If failed to stop, sending_ should also be false, as we disconnect the + // capture anyway. + // The failure on SetSend(false) is a known issue in webrtc. + sending_ = send; + } + return ret; +} + +bool HybridVideoMediaChannel::SetCapturer(uint32 ssrc, + VideoCapturer* capturer) { + bool ret = true; + if (channel1_.get()) { + ret = channel1_->SetCapturer(ssrc, capturer); + } + if (channel2_.get() && ret) { + ret = channel2_->SetCapturer(ssrc, capturer); + } + return ret; +} + +bool HybridVideoMediaChannel::AddSendStream(const StreamParams& sp) { + bool ret = true; + if (channel1_) { + ret = channel1_->AddSendStream(sp); + } + if (channel2_ && ret) { + ret = channel2_->AddSendStream(sp); + } + return ret; +} + +bool HybridVideoMediaChannel::RemoveSendStream(uint32 ssrc) { + bool ret = true; + if (channel1_) { + ret = channel1_->RemoveSendStream(ssrc); + } + if (channel2_ && ret) { + ret = channel2_->RemoveSendStream(ssrc); + } + return ret; +} + +bool HybridVideoMediaChannel::AddRecvStream(const StreamParams& sp) { + return active_channel_ && + active_channel_->AddRecvStream(sp); +} + +bool HybridVideoMediaChannel::RemoveRecvStream(uint32 ssrc) { + return active_channel_ && + active_channel_->RemoveRecvStream(ssrc); +} + +bool HybridVideoMediaChannel::SendIntraFrame() { + return active_channel_ && + active_channel_->SendIntraFrame(); +} + +bool HybridVideoMediaChannel::RequestIntraFrame() { + return active_channel_ && + active_channel_->RequestIntraFrame(); +} + +bool HybridVideoMediaChannel::GetStats(VideoMediaInfo* info) { + // TODO(juberti): Ensure that returning no stats until SetSendCodecs is OK. + return active_channel_ && + active_channel_->GetStats(info); +} + +void HybridVideoMediaChannel::OnPacketReceived(talk_base::Buffer* packet) { + // Eat packets until we have an active channel; + if (active_channel_) { + active_channel_->OnPacketReceived(packet); + } else { + LOG(LS_INFO) << "HybridVideoChannel: Eating early RTP packet"; + } +} + +void HybridVideoMediaChannel::OnRtcpReceived(talk_base::Buffer* packet) { + // Eat packets until we have an active channel; + if (active_channel_) { + active_channel_->OnRtcpReceived(packet); + } else { + LOG(LS_INFO) << "HybridVideoChannel: Eating early RTCP packet"; + } +} + +void HybridVideoMediaChannel::OnReadyToSend(bool ready) { + if (channel1_) { + channel1_->OnReadyToSend(ready); + } + if (channel2_) { + channel2_->OnReadyToSend(ready); + } +} + +void HybridVideoMediaChannel::UpdateAspectRatio(int ratio_w, int ratio_h) { + if (active_channel_) active_channel_->UpdateAspectRatio(ratio_w, ratio_h); +} + +bool HybridVideoMediaChannel::SelectActiveChannel( + const std::vector& codecs) { + if (!active_channel_ && !codecs.empty()) { + if (engine_->HasCodec1(codecs[0])) { + channel2_.reset(); + active_channel_ = channel1_.get(); + } else if (engine_->HasCodec2(codecs[0])) { + channel1_.reset(); + active_channel_ = channel2_.get(); + } + } + if (NULL == active_channel_) { + return false; + } + // Connect signals from the active channel. + active_channel_->SignalMediaError.connect( + this, + &HybridVideoMediaChannel::OnMediaError); + return true; +} + +void HybridVideoMediaChannel::SplitCodecs( + const std::vector& codecs, + std::vector* codecs1, std::vector* codecs2) { + codecs1->clear(); + codecs2->clear(); + for (size_t i = 0; i < codecs.size(); ++i) { + if (engine_->HasCodec1(codecs[i])) { + codecs1->push_back(codecs[i]); + } + if (engine_->HasCodec2(codecs[i])) { + codecs2->push_back(codecs[i]); + } + } +} + +void HybridVideoMediaChannel::OnMediaError(uint32 ssrc, Error error) { + SignalMediaError(ssrc, error); +} + +} // namespace cricket diff --git a/talk/media/base/hybridvideoengine.h b/talk/media/base/hybridvideoengine.h new file mode 100644 index 000000000..1e43a30b7 --- /dev/null +++ b/talk/media/base/hybridvideoengine.h @@ -0,0 +1,275 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_HYBRIDVIDEOENGINE_H_ +#define TALK_MEDIA_BASE_HYBRIDVIDEOENGINE_H_ + +#include +#include + +#include "talk/base/logging.h" +#include "talk/base/sigslotrepeater.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/videocapturer.h" +#include "talk/media/base/videocommon.h" + +namespace cricket { + +struct Device; +struct VideoFormat; +class HybridVideoEngineInterface; +class VideoCapturer; +class VideoFrame; +class VideoRenderer; + +// HybridVideoMediaChannels work with a HybridVideoEngine to combine +// two unrelated VideoMediaChannel implementations into a single class. +class HybridVideoMediaChannel : public VideoMediaChannel { + public: + HybridVideoMediaChannel(HybridVideoEngineInterface* engine, + VideoMediaChannel* channel1, + VideoMediaChannel* channel2); + virtual ~HybridVideoMediaChannel(); + + // VideoMediaChannel methods + virtual void SetInterface(NetworkInterface* iface); + virtual bool SetOptions(const VideoOptions& options); + virtual bool GetOptions(VideoOptions* options) const; + virtual bool AddSendStream(const StreamParams& sp); + virtual bool RemoveSendStream(uint32 ssrc); + virtual bool SetRenderer(uint32 ssrc, VideoRenderer* renderer); + virtual bool SetRender(bool render); + virtual bool MuteStream(uint32 ssrc, bool muted); + + virtual bool SetRecvCodecs(const std::vector& codecs); + virtual bool SetRecvRtpHeaderExtensions( + const std::vector& extensions); + + virtual bool SetSendCodecs(const std::vector& codecs); + virtual bool GetSendCodec(VideoCodec* codec); + virtual bool SetSendStreamFormat(uint32 ssrc, const VideoFormat& format); + virtual bool SetSendRtpHeaderExtensions( + const std::vector& extensions); + virtual bool SetSendBandwidth(bool autobw, int bps); + virtual bool SetSend(bool send); + + virtual bool AddRecvStream(const StreamParams& sp); + virtual bool RemoveRecvStream(uint32 ssrc); + virtual bool SetCapturer(uint32 ssrc, VideoCapturer* capturer); + + virtual bool SendIntraFrame(); + virtual bool RequestIntraFrame(); + + virtual bool GetStats(VideoMediaInfo* info); + + virtual void OnPacketReceived(talk_base::Buffer* packet); + virtual void OnRtcpReceived(talk_base::Buffer* packet); + virtual void OnReadyToSend(bool ready); + + virtual void UpdateAspectRatio(int ratio_w, int ratio_h); + + void OnLocalFrame(VideoCapturer*, const VideoFrame*); + void OnLocalFrameFormat(VideoCapturer*, const VideoFormat*); + + bool sending() const { return sending_; } + + private: + bool SelectActiveChannel(const std::vector& codecs); + void SplitCodecs(const std::vector& codecs, + std::vector* codecs1, + std::vector* codecs2); + + void OnMediaError(uint32 ssrc, Error error); + + HybridVideoEngineInterface* engine_; + talk_base::scoped_ptr channel1_; + talk_base::scoped_ptr channel2_; + VideoMediaChannel* active_channel_; + bool sending_; +}; + +// Interface class for HybridVideoChannels to talk to the engine. +class HybridVideoEngineInterface { + public: + virtual ~HybridVideoEngineInterface() {} + virtual bool HasCodec1(const VideoCodec& codec) = 0; + virtual bool HasCodec2(const VideoCodec& codec) = 0; + virtual void OnSendChange1(VideoMediaChannel* channel1, bool send) = 0; + virtual void OnSendChange2(VideoMediaChannel* channel1, bool send) = 0; + virtual void OnNewSendResolution(int width, int height) = 0; +}; + +// The HybridVideoEngine class combines two unrelated VideoEngine impls +// into a single class. It creates HybridVideoMediaChannels that also contain +// a VideoMediaChannel implementation from each engine. Policy is then used +// during call setup to determine which VideoMediaChannel should be used. +// Currently, this policy is based on what codec the remote side wants to use. +template +class HybridVideoEngine : public HybridVideoEngineInterface { + public: + HybridVideoEngine() { + // Unify the codec lists. + codecs_ = video1_.codecs(); + codecs_.insert(codecs_.end(), video2_.codecs().begin(), + video2_.codecs().end()); + + rtp_header_extensions_ = video1_.rtp_header_extensions(); + rtp_header_extensions_.insert(rtp_header_extensions_.end(), + video2_.rtp_header_extensions().begin(), + video2_.rtp_header_extensions().end()); + + SignalCaptureStateChange.repeat(video2_.SignalCaptureStateChange); + } + + bool Init(talk_base::Thread* worker_thread) { + if (!video1_.Init(worker_thread)) { + LOG(LS_ERROR) << "Failed to init VideoEngine1"; + return false; + } + if (!video2_.Init(worker_thread)) { + LOG(LS_ERROR) << "Failed to init VideoEngine2"; + video1_.Terminate(); + return false; + } + return true; + } + void Terminate() { + video1_.Terminate(); + video2_.Terminate(); + } + + int GetCapabilities() { + return (video1_.GetCapabilities() | video2_.GetCapabilities()); + } + HybridVideoMediaChannel* CreateChannel(VoiceMediaChannel* channel) { + talk_base::scoped_ptr channel1( + video1_.CreateChannel(channel)); + if (!channel1) { + LOG(LS_ERROR) << "Failed to create VideoMediaChannel1"; + return NULL; + } + talk_base::scoped_ptr channel2( + video2_.CreateChannel(channel)); + if (!channel2) { + LOG(LS_ERROR) << "Failed to create VideoMediaChannel2"; + return NULL; + } + return new HybridVideoMediaChannel(this, + channel1.release(), channel2.release()); + } + + bool SetOptions(int o) { + return video1_.SetOptions(o) && video2_.SetOptions(o); + } + bool SetDefaultEncoderConfig(const VideoEncoderConfig& config) { + VideoEncoderConfig conf = config; + if (video1_.codecs().size() > 0) { + conf.max_codec.name = video1_.codecs()[0].name; + if (!video1_.SetDefaultEncoderConfig(conf)) { + LOG(LS_ERROR) << "Failed to SetDefaultEncoderConfig for video1"; + return false; + } + } + if (video2_.codecs().size() > 0) { + conf.max_codec.name = video2_.codecs()[0].name; + if (!video2_.SetDefaultEncoderConfig(conf)) { + LOG(LS_ERROR) << "Failed to SetDefaultEncoderConfig for video2"; + return false; + } + } + return true; + } + const std::vector& codecs() const { + return codecs_; + } + const std::vector& rtp_header_extensions() const { + return rtp_header_extensions_; + } + void SetLogging(int min_sev, const char* filter) { + video1_.SetLogging(min_sev, filter); + video2_.SetLogging(min_sev, filter); + } + + VideoFormat GetStartCaptureFormat() const { + return video2_.GetStartCaptureFormat(); + } + + // TODO(juberti): Remove these functions after we do the capturer refactoring. + // For now they are set to always use the second engine for capturing, which + // is convenient given our intended use case. + bool SetCaptureDevice(const Device* device) { + return video2_.SetCaptureDevice(device); + } + bool SetVideoCapturer(VideoCapturer* capturer) { + return video2_.SetVideoCapturer(capturer); + } + VideoCapturer* GetVideoCapturer() const { + return video2_.GetVideoCapturer(); + } + bool SetLocalRenderer(VideoRenderer* renderer) { + return video2_.SetLocalRenderer(renderer); + } + bool SetCapture(bool capture) { + return video2_.SetCapture(capture); + } + sigslot::repeater2 SignalCaptureStateChange; + + virtual bool HasCodec1(const VideoCodec& codec) { + return HasCodec(video1_, codec); + } + virtual bool HasCodec2(const VideoCodec& codec) { + return HasCodec(video2_, codec); + } + template + bool HasCodec(const VIDEO& engine, const VideoCodec& codec) const { + for (std::vector::const_iterator i = engine.codecs().begin(); + i != engine.codecs().end(); + ++i) { + if (i->Matches(codec)) { + return true; + } + } + return false; + } + virtual void OnSendChange1(VideoMediaChannel* channel1, bool send) { + } + virtual void OnSendChange2(VideoMediaChannel* channel2, bool send) { + } + virtual void OnNewSendResolution(int width, int height) { + } + + protected: + VIDEO1 video1_; + VIDEO2 video2_; + std::vector codecs_; + std::vector rtp_header_extensions_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_HYBRIDVIDEOENGINE_H_ diff --git a/talk/media/base/mediachannel.h b/talk/media/base/mediachannel.h new file mode 100644 index 000000000..b20051e72 --- /dev/null +++ b/talk/media/base/mediachannel.h @@ -0,0 +1,933 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_MEDIACHANNEL_H_ +#define TALK_MEDIA_BASE_MEDIACHANNEL_H_ + +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/buffer.h" +#include "talk/base/logging.h" +#include "talk/base/sigslot.h" +#include "talk/base/socket.h" +#include "talk/base/window.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/streamparams.h" +// TODO(juberti): re-evaluate this include +#include "talk/session/media/audiomonitor.h" + +namespace talk_base { +class Buffer; +class RateLimiter; +class Timing; +} + +namespace cricket { + +class AudioRenderer; +struct RtpHeader; +class ScreencastId; +struct VideoFormat; +class VideoCapturer; +class VideoRenderer; + +const int kMinRtpHeaderExtensionId = 1; +const int kMaxRtpHeaderExtensionId = 255; +const int kScreencastDefaultFps = 5; + +// Used in AudioOptions and VideoOptions to signify "unset" values. +template +class Settable { + public: + Settable() : set_(false), val_() {} + explicit Settable(T val) : set_(true), val_(val) {} + + bool IsSet() const { + return set_; + } + + bool Get(T* out) const { + *out = val_; + return set_; + } + + T GetWithDefaultIfUnset(const T& default_value) const { + return set_ ? val_ : default_value; + } + + virtual void Set(T val) { + set_ = true; + val_ = val; + } + + void Clear() { + Set(T()); + set_ = false; + } + + void SetFrom(const Settable& o) { + // Set this value based on the value of o, iff o is set. If this value is + // set and o is unset, the current value will be unchanged. + T val; + if (o.Get(&val)) { + Set(val); + } + } + + std::string ToString() const { + return set_ ? talk_base::ToString(val_) : ""; + } + + bool operator==(const Settable& o) const { + // Equal if both are unset with any value or both set with the same value. + return (set_ == o.set_) && (!set_ || (val_ == o.val_)); + } + + bool operator!=(const Settable& o) const { + return !operator==(o); + } + + protected: + void InitializeValue(const T &val) { + val_ = val; + } + + private: + bool set_; + T val_; +}; + +class SettablePercent : public Settable { + public: + virtual void Set(float val) { + if (val < 0) { + val = 0; + } + if (val > 1.0) { + val = 1.0; + } + Settable::Set(val); + } +}; + +template +static std::string ToStringIfSet(const char* key, const Settable& val) { + std::string str; + if (val.IsSet()) { + str = key; + str += ": "; + str += val.ToString(); + str += ", "; + } + return str; +} + +// Options that can be applied to a VoiceMediaChannel or a VoiceMediaEngine. +// Used to be flags, but that makes it hard to selectively apply options. +// We are moving all of the setting of options to structs like this, +// but some things currently still use flags. +struct AudioOptions { + void SetAll(const AudioOptions& change) { + echo_cancellation.SetFrom(change.echo_cancellation); + auto_gain_control.SetFrom(change.auto_gain_control); + noise_suppression.SetFrom(change.noise_suppression); + highpass_filter.SetFrom(change.highpass_filter); + stereo_swapping.SetFrom(change.stereo_swapping); + typing_detection.SetFrom(change.typing_detection); + conference_mode.SetFrom(change.conference_mode); + adjust_agc_delta.SetFrom(change.adjust_agc_delta); + experimental_agc.SetFrom(change.experimental_agc); + experimental_aec.SetFrom(change.experimental_aec); + aec_dump.SetFrom(change.aec_dump); + } + + bool operator==(const AudioOptions& o) const { + return echo_cancellation == o.echo_cancellation && + auto_gain_control == o.auto_gain_control && + noise_suppression == o.noise_suppression && + highpass_filter == o.highpass_filter && + stereo_swapping == o.stereo_swapping && + typing_detection == o.typing_detection && + conference_mode == o.conference_mode && + experimental_agc == o.experimental_agc && + experimental_aec == o.experimental_aec && + adjust_agc_delta == o.adjust_agc_delta && + aec_dump == o.aec_dump; + } + + std::string ToString() const { + std::ostringstream ost; + ost << "AudioOptions {"; + ost << ToStringIfSet("aec", echo_cancellation); + ost << ToStringIfSet("agc", auto_gain_control); + ost << ToStringIfSet("ns", noise_suppression); + ost << ToStringIfSet("hf", highpass_filter); + ost << ToStringIfSet("swap", stereo_swapping); + ost << ToStringIfSet("typing", typing_detection); + ost << ToStringIfSet("conference", conference_mode); + ost << ToStringIfSet("agc_delta", adjust_agc_delta); + ost << ToStringIfSet("experimental_agc", experimental_agc); + ost << ToStringIfSet("experimental_aec", experimental_aec); + ost << ToStringIfSet("aec_dump", aec_dump); + ost << "}"; + return ost.str(); + } + + // Audio processing that attempts to filter away the output signal from + // later inbound pickup. + Settable echo_cancellation; + // Audio processing to adjust the sensitivity of the local mic dynamically. + Settable auto_gain_control; + // Audio processing to filter out background noise. + Settable noise_suppression; + // Audio processing to remove background noise of lower frequencies. + Settable highpass_filter; + // Audio processing to swap the left and right channels. + Settable stereo_swapping; + // Audio processing to detect typing. + Settable typing_detection; + Settable conference_mode; + Settable adjust_agc_delta; + Settable experimental_agc; + Settable experimental_aec; + Settable aec_dump; +}; + +// Options that can be applied to a VideoMediaChannel or a VideoMediaEngine. +// Used to be flags, but that makes it hard to selectively apply options. +// We are moving all of the setting of options to structs like this, +// but some things currently still use flags. +struct VideoOptions { + VideoOptions() { + process_adaptation_threshhold.Set(kProcessCpuThreshold); + system_low_adaptation_threshhold.Set(kLowSystemCpuThreshold); + system_high_adaptation_threshhold.Set(kHighSystemCpuThreshold); + } + + void SetAll(const VideoOptions& change) { + adapt_input_to_encoder.SetFrom(change.adapt_input_to_encoder); + adapt_input_to_cpu_usage.SetFrom(change.adapt_input_to_cpu_usage); + adapt_view_switch.SetFrom(change.adapt_view_switch); + video_noise_reduction.SetFrom(change.video_noise_reduction); + video_three_layers.SetFrom(change.video_three_layers); + video_enable_camera_list.SetFrom(change.video_enable_camera_list); + video_one_layer_screencast.SetFrom(change.video_one_layer_screencast); + video_high_bitrate.SetFrom(change.video_high_bitrate); + video_watermark.SetFrom(change.video_watermark); + video_temporal_layer_screencast.SetFrom( + change.video_temporal_layer_screencast); + video_leaky_bucket.SetFrom(change.video_leaky_bucket); + conference_mode.SetFrom(change.conference_mode); + process_adaptation_threshhold.SetFrom(change.process_adaptation_threshhold); + system_low_adaptation_threshhold.SetFrom( + change.system_low_adaptation_threshhold); + system_high_adaptation_threshhold.SetFrom( + change.system_high_adaptation_threshhold); + buffered_mode_latency.SetFrom(change.buffered_mode_latency); + } + + bool operator==(const VideoOptions& o) const { + return adapt_input_to_encoder == o.adapt_input_to_encoder && + adapt_input_to_cpu_usage == o.adapt_input_to_cpu_usage && + adapt_view_switch == o.adapt_view_switch && + video_noise_reduction == o.video_noise_reduction && + video_three_layers == o.video_three_layers && + video_enable_camera_list == o.video_enable_camera_list && + video_one_layer_screencast == o.video_one_layer_screencast && + video_high_bitrate == o.video_high_bitrate && + video_watermark == o.video_watermark && + video_temporal_layer_screencast == o.video_temporal_layer_screencast && + video_leaky_bucket == o.video_leaky_bucket && + conference_mode == o.conference_mode && + process_adaptation_threshhold == o.process_adaptation_threshhold && + system_low_adaptation_threshhold == + o.system_low_adaptation_threshhold && + system_high_adaptation_threshhold == + o.system_high_adaptation_threshhold && + buffered_mode_latency == o.buffered_mode_latency; + } + + std::string ToString() const { + std::ostringstream ost; + ost << "VideoOptions {"; + ost << ToStringIfSet("encoder adaption", adapt_input_to_encoder); + ost << ToStringIfSet("cpu adaption", adapt_input_to_cpu_usage); + ost << ToStringIfSet("adapt view switch", adapt_view_switch); + ost << ToStringIfSet("noise reduction", video_noise_reduction); + ost << ToStringIfSet("3 layers", video_three_layers); + ost << ToStringIfSet("camera list", video_enable_camera_list); + ost << ToStringIfSet("1 layer screencast", + video_one_layer_screencast); + ost << ToStringIfSet("high bitrate", video_high_bitrate); + ost << ToStringIfSet("watermark", video_watermark); + ost << ToStringIfSet("video temporal layer screencast", + video_temporal_layer_screencast); + ost << ToStringIfSet("leaky bucket", video_leaky_bucket); + ost << ToStringIfSet("conference mode", conference_mode); + ost << ToStringIfSet("process", process_adaptation_threshhold); + ost << ToStringIfSet("low", system_low_adaptation_threshhold); + ost << ToStringIfSet("high", system_high_adaptation_threshhold); + ost << ToStringIfSet("buffered mode latency", buffered_mode_latency); + ost << "}"; + return ost.str(); + } + + // Encoder adaption, which is the gd callback in LMI, and TBA in WebRTC. + Settable adapt_input_to_encoder; + // Enable CPU adaptation? + Settable adapt_input_to_cpu_usage; + // Enable Adapt View Switch? + Settable adapt_view_switch; + // Enable denoising? + Settable video_noise_reduction; + // Experimental: Enable multi layer? + Settable video_three_layers; + // Experimental: Enable camera list? + Settable video_enable_camera_list; + // Experimental: Enable one layer screencast? + Settable video_one_layer_screencast; + // Experimental: Enable WebRtc higher bitrate? + Settable video_high_bitrate; + // Experimental: Add watermark to the rendered video image. + Settable video_watermark; + // Experimental: Enable WebRTC layered screencast. + Settable video_temporal_layer_screencast; + // Enable WebRTC leaky bucket when sending media packets. + Settable video_leaky_bucket; + // Use conference mode? + Settable conference_mode; + // Threshhold for process cpu adaptation. (Process limit) + SettablePercent process_adaptation_threshhold; + // Low threshhold for cpu adaptation. (Adapt up) + SettablePercent system_low_adaptation_threshhold; + // High threshhold for cpu adaptation. (Adapt down) + SettablePercent system_high_adaptation_threshhold; + // Specify buffered mode latency in milliseconds. + Settable buffered_mode_latency; +}; + +// A class for playing out soundclips. +class SoundclipMedia { + public: + enum SoundclipFlags { + SF_LOOP = 1, + }; + + virtual ~SoundclipMedia() {} + + // Plays a sound out to the speakers with the given audio stream. The stream + // must be 16-bit little-endian 16 kHz PCM. If a stream is already playing + // on this SoundclipMedia, it is stopped. If clip is NULL, nothing is played. + // Returns whether it was successful. + virtual bool PlaySound(const char *clip, int len, int flags) = 0; +}; + +struct RtpHeaderExtension { + RtpHeaderExtension() : id(0) {} + RtpHeaderExtension(const std::string& u, int i) : uri(u), id(i) {} + std::string uri; + int id; + // TODO(juberti): SendRecv direction; + + bool operator==(const RtpHeaderExtension& ext) const { + // id is a reserved word in objective-c. Therefore the id attribute has to + // be a fully qualified name in order to compile on IOS. + return this->id == ext.id && + uri == ext.uri; + } +}; + +// Returns the named header extension if found among all extensions, NULL +// otherwise. +inline const RtpHeaderExtension* FindHeaderExtension( + const std::vector& extensions, + const std::string& name) { + for (std::vector::const_iterator it = extensions.begin(); + it != extensions.end(); ++it) { + if (it->uri == name) + return &(*it); + } + return NULL; +} + +enum MediaChannelOptions { + // Tune the stream for conference mode. + OPT_CONFERENCE = 0x0001 +}; + +enum VoiceMediaChannelOptions { + // Tune the audio stream for vcs with different target levels. + OPT_AGC_MINUS_10DB = 0x80000000 +}; + +// DTMF flags to control if a DTMF tone should be played and/or sent. +enum DtmfFlags { + DF_PLAY = 0x01, + DF_SEND = 0x02, +}; + +// Special purpose DTMF event code used by the VoiceMediaChannel::InsertDtmf. +const int kDtmfDelay = -1; // Insert a delay to the end of the DTMF queue. +const int kDtmfReset = -2; // Reset the DTMF queue. +// The delay in ms when the InsertDtmf is called with kDtmfDelay. +const int kDtmfDelayInMs = 2000; + +class MediaChannel : public sigslot::has_slots<> { + public: + class NetworkInterface { + public: + enum SocketType { ST_RTP, ST_RTCP }; + virtual bool SendPacket(talk_base::Buffer* packet) = 0; + virtual bool SendRtcp(talk_base::Buffer* packet) = 0; + virtual int SetOption(SocketType type, talk_base::Socket::Option opt, + int option) = 0; + virtual ~NetworkInterface() {} + }; + + MediaChannel() : network_interface_(NULL) {} + virtual ~MediaChannel() {} + + // Gets/sets the abstract inteface class for sending RTP/RTCP data. + NetworkInterface *network_interface() { return network_interface_; } + virtual void SetInterface(NetworkInterface *iface) { + network_interface_ = iface; + } + + // Called when a RTP packet is received. + virtual void OnPacketReceived(talk_base::Buffer* packet) = 0; + // Called when a RTCP packet is received. + virtual void OnRtcpReceived(talk_base::Buffer* packet) = 0; + // Called when the socket's ability to send has changed. + virtual void OnReadyToSend(bool ready) = 0; + // Creates a new outgoing media stream with SSRCs and CNAME as described + // by sp. + virtual bool AddSendStream(const StreamParams& sp) = 0; + // Removes an outgoing media stream. + // ssrc must be the first SSRC of the media stream if the stream uses + // multiple SSRCs. + virtual bool RemoveSendStream(uint32 ssrc) = 0; + // Creates a new incoming media stream with SSRCs and CNAME as described + // by sp. + virtual bool AddRecvStream(const StreamParams& sp) = 0; + // Removes an incoming media stream. + // ssrc must be the first SSRC of the media stream if the stream uses + // multiple SSRCs. + virtual bool RemoveRecvStream(uint32 ssrc) = 0; + + // Mutes the channel. + virtual bool MuteStream(uint32 ssrc, bool on) = 0; + + // Sets the RTP extension headers and IDs to use when sending RTP. + virtual bool SetRecvRtpHeaderExtensions( + const std::vector& extensions) = 0; + virtual bool SetSendRtpHeaderExtensions( + const std::vector& extensions) = 0; + // Sets the rate control to use when sending data. + virtual bool SetSendBandwidth(bool autobw, int bps) = 0; + + protected: + NetworkInterface *network_interface_; +}; + +enum SendFlags { + SEND_NOTHING, + SEND_RINGBACKTONE, + SEND_MICROPHONE +}; + +struct VoiceSenderInfo { + VoiceSenderInfo() + : ssrc(0), + bytes_sent(0), + packets_sent(0), + packets_lost(0), + fraction_lost(0.0), + ext_seqnum(0), + rtt_ms(0), + jitter_ms(0), + audio_level(0), + aec_quality_min(0.0), + echo_delay_median_ms(0), + echo_delay_std_ms(0), + echo_return_loss(0), + echo_return_loss_enhancement(0) { + } + + uint32 ssrc; + std::string codec_name; + int64 bytes_sent; + int packets_sent; + int packets_lost; + float fraction_lost; + int ext_seqnum; + int rtt_ms; + int jitter_ms; + int audio_level; + float aec_quality_min; + int echo_delay_median_ms; + int echo_delay_std_ms; + int echo_return_loss; + int echo_return_loss_enhancement; +}; + +struct VoiceReceiverInfo { + VoiceReceiverInfo() + : ssrc(0), + bytes_rcvd(0), + packets_rcvd(0), + packets_lost(0), + fraction_lost(0.0), + ext_seqnum(0), + jitter_ms(0), + jitter_buffer_ms(0), + jitter_buffer_preferred_ms(0), + delay_estimate_ms(0), + audio_level(0), + expand_rate(0) { + } + + uint32 ssrc; + int64 bytes_rcvd; + int packets_rcvd; + int packets_lost; + float fraction_lost; + int ext_seqnum; + int jitter_ms; + int jitter_buffer_ms; + int jitter_buffer_preferred_ms; + int delay_estimate_ms; + int audio_level; + // fraction of synthesized speech inserted through pre-emptive expansion + float expand_rate; +}; + +struct VideoSenderInfo { + VideoSenderInfo() + : bytes_sent(0), + packets_sent(0), + packets_cached(0), + packets_lost(0), + fraction_lost(0.0), + firs_rcvd(0), + nacks_rcvd(0), + rtt_ms(0), + frame_width(0), + frame_height(0), + framerate_input(0), + framerate_sent(0), + nominal_bitrate(0), + preferred_bitrate(0), + adapt_reason(0) { + } + + std::vector ssrcs; + std::vector ssrc_groups; + std::string codec_name; + int64 bytes_sent; + int packets_sent; + int packets_cached; + int packets_lost; + float fraction_lost; + int firs_rcvd; + int nacks_rcvd; + int rtt_ms; + int frame_width; + int frame_height; + int framerate_input; + int framerate_sent; + int nominal_bitrate; + int preferred_bitrate; + int adapt_reason; +}; + +struct VideoReceiverInfo { + VideoReceiverInfo() + : bytes_rcvd(0), + packets_rcvd(0), + packets_lost(0), + packets_concealed(0), + fraction_lost(0.0), + firs_sent(0), + nacks_sent(0), + frame_width(0), + frame_height(0), + framerate_rcvd(0), + framerate_decoded(0), + framerate_output(0), + framerate_render_input(0), + framerate_render_output(0) { + } + + std::vector ssrcs; + std::vector ssrc_groups; + int64 bytes_rcvd; + // vector layer_bytes_rcvd; + int packets_rcvd; + int packets_lost; + int packets_concealed; + float fraction_lost; + int firs_sent; + int nacks_sent; + int frame_width; + int frame_height; + int framerate_rcvd; + int framerate_decoded; + int framerate_output; + // Framerate as sent to the renderer. + int framerate_render_input; + // Framerate that the renderer reports. + int framerate_render_output; +}; + +struct DataSenderInfo { + DataSenderInfo() + : ssrc(0), + bytes_sent(0), + packets_sent(0) { + } + + uint32 ssrc; + std::string codec_name; + int64 bytes_sent; + int packets_sent; +}; + +struct DataReceiverInfo { + DataReceiverInfo() + : ssrc(0), + bytes_rcvd(0), + packets_rcvd(0) { + } + + uint32 ssrc; + int64 bytes_rcvd; + int packets_rcvd; +}; + +struct BandwidthEstimationInfo { + BandwidthEstimationInfo() + : available_send_bandwidth(0), + available_recv_bandwidth(0), + target_enc_bitrate(0), + actual_enc_bitrate(0), + retransmit_bitrate(0), + transmit_bitrate(0), + bucket_delay(0) { + } + + int available_send_bandwidth; + int available_recv_bandwidth; + int target_enc_bitrate; + int actual_enc_bitrate; + int retransmit_bitrate; + int transmit_bitrate; + int bucket_delay; +}; + +struct VoiceMediaInfo { + void Clear() { + senders.clear(); + receivers.clear(); + } + std::vector senders; + std::vector receivers; +}; + +struct VideoMediaInfo { + void Clear() { + senders.clear(); + receivers.clear(); + bw_estimations.clear(); + } + std::vector senders; + std::vector receivers; + std::vector bw_estimations; +}; + +struct DataMediaInfo { + void Clear() { + senders.clear(); + receivers.clear(); + } + std::vector senders; + std::vector receivers; +}; + +class VoiceMediaChannel : public MediaChannel { + public: + enum Error { + ERROR_NONE = 0, // No error. + ERROR_OTHER, // Other errors. + ERROR_REC_DEVICE_OPEN_FAILED = 100, // Could not open mic. + ERROR_REC_DEVICE_MUTED, // Mic was muted by OS. + ERROR_REC_DEVICE_SILENT, // No background noise picked up. + ERROR_REC_DEVICE_SATURATION, // Mic input is clipping. + ERROR_REC_DEVICE_REMOVED, // Mic was removed while active. + ERROR_REC_RUNTIME_ERROR, // Processing is encountering errors. + ERROR_REC_SRTP_ERROR, // Generic SRTP failure. + ERROR_REC_SRTP_AUTH_FAILED, // Failed to authenticate packets. + ERROR_REC_TYPING_NOISE_DETECTED, // Typing noise is detected. + ERROR_PLAY_DEVICE_OPEN_FAILED = 200, // Could not open playout. + ERROR_PLAY_DEVICE_MUTED, // Playout muted by OS. + ERROR_PLAY_DEVICE_REMOVED, // Playout removed while active. + ERROR_PLAY_RUNTIME_ERROR, // Errors in voice processing. + ERROR_PLAY_SRTP_ERROR, // Generic SRTP failure. + ERROR_PLAY_SRTP_AUTH_FAILED, // Failed to authenticate packets. + ERROR_PLAY_SRTP_REPLAY, // Packet replay detected. + }; + + VoiceMediaChannel() {} + virtual ~VoiceMediaChannel() {} + // Sets the codecs/payload types to be used for incoming media. + virtual bool SetRecvCodecs(const std::vector& codecs) = 0; + // Sets the codecs/payload types to be used for outgoing media. + virtual bool SetSendCodecs(const std::vector& codecs) = 0; + // Starts or stops playout of received audio. + virtual bool SetPlayout(bool playout) = 0; + // Starts or stops sending (and potentially capture) of local audio. + virtual bool SetSend(SendFlags flag) = 0; + // Sets the renderer object to be used for the specified audio stream. + virtual bool SetRenderer(uint32 ssrc, AudioRenderer* renderer) = 0; + // Gets current energy levels for all incoming streams. + virtual bool GetActiveStreams(AudioInfo::StreamList* actives) = 0; + // Get the current energy level of the stream sent to the speaker. + virtual int GetOutputLevel() = 0; + // Get the time in milliseconds since last recorded keystroke, or negative. + virtual int GetTimeSinceLastTyping() = 0; + // Temporarily exposed field for tuning typing detect options. + virtual void SetTypingDetectionParameters(int time_window, + int cost_per_typing, int reporting_threshold, int penalty_decay, + int type_event_delay) = 0; + // Set left and right scale for speaker output volume of the specified ssrc. + virtual bool SetOutputScaling(uint32 ssrc, double left, double right) = 0; + // Get left and right scale for speaker output volume of the specified ssrc. + virtual bool GetOutputScaling(uint32 ssrc, double* left, double* right) = 0; + // Specifies a ringback tone to be played during call setup. + virtual bool SetRingbackTone(const char *buf, int len) = 0; + // Plays or stops the aforementioned ringback tone + virtual bool PlayRingbackTone(uint32 ssrc, bool play, bool loop) = 0; + // Returns if the telephone-event has been negotiated. + virtual bool CanInsertDtmf() { return false; } + // Send and/or play a DTMF |event| according to the |flags|. + // The DTMF out-of-band signal will be used on sending. + // The |ssrc| should be either 0 or a valid send stream ssrc. + // The valid value for the |event| are -2 to 15. + // kDtmfReset(-2) is used to reset the DTMF. + // kDtmfDelay(-1) is used to insert a delay to the end of the DTMF queue. + // 0 to 15 which corresponding to DTMF event 0-9, *, #, A-D. + virtual bool InsertDtmf(uint32 ssrc, int event, int duration, int flags) = 0; + // Gets quality stats for the channel. + virtual bool GetStats(VoiceMediaInfo* info) = 0; + // Gets last reported error for this media channel. + virtual void GetLastMediaError(uint32* ssrc, + VoiceMediaChannel::Error* error) { + ASSERT(error != NULL); + *error = ERROR_NONE; + } + // Sets the media options to use. + virtual bool SetOptions(const AudioOptions& options) = 0; + virtual bool GetOptions(AudioOptions* options) const = 0; + + // Signal errors from MediaChannel. Arguments are: + // ssrc(uint32), and error(VoiceMediaChannel::Error). + sigslot::signal2 SignalMediaError; +}; + +class VideoMediaChannel : public MediaChannel { + public: + enum Error { + ERROR_NONE = 0, // No error. + ERROR_OTHER, // Other errors. + ERROR_REC_DEVICE_OPEN_FAILED = 100, // Could not open camera. + ERROR_REC_DEVICE_NO_DEVICE, // No camera. + ERROR_REC_DEVICE_IN_USE, // Device is in already use. + ERROR_REC_DEVICE_REMOVED, // Device is removed. + ERROR_REC_SRTP_ERROR, // Generic sender SRTP failure. + ERROR_REC_SRTP_AUTH_FAILED, // Failed to authenticate packets. + ERROR_REC_CPU_MAX_CANT_DOWNGRADE, // Can't downgrade capture anymore. + ERROR_PLAY_SRTP_ERROR = 200, // Generic receiver SRTP failure. + ERROR_PLAY_SRTP_AUTH_FAILED, // Failed to authenticate packets. + ERROR_PLAY_SRTP_REPLAY, // Packet replay detected. + }; + + VideoMediaChannel() : renderer_(NULL) {} + virtual ~VideoMediaChannel() {} + // Sets the codecs/payload types to be used for incoming media. + virtual bool SetRecvCodecs(const std::vector& codecs) = 0; + // Sets the codecs/payload types to be used for outgoing media. + virtual bool SetSendCodecs(const std::vector& codecs) = 0; + // Gets the currently set codecs/payload types to be used for outgoing media. + virtual bool GetSendCodec(VideoCodec* send_codec) = 0; + // Sets the format of a specified outgoing stream. + virtual bool SetSendStreamFormat(uint32 ssrc, const VideoFormat& format) = 0; + // Starts or stops playout of received video. + virtual bool SetRender(bool render) = 0; + // Starts or stops transmission (and potentially capture) of local video. + virtual bool SetSend(bool send) = 0; + // Sets the renderer object to be used for the specified stream. + // If SSRC is 0, the renderer is used for the 'default' stream. + virtual bool SetRenderer(uint32 ssrc, VideoRenderer* renderer) = 0; + // If |ssrc| is 0, replace the default capturer (engine capturer) with + // |capturer|. If |ssrc| is non zero create a new stream with |ssrc| as SSRC. + virtual bool SetCapturer(uint32 ssrc, VideoCapturer* capturer) = 0; + // Gets quality stats for the channel. + virtual bool GetStats(VideoMediaInfo* info) = 0; + + // Send an intra frame to the receivers. + virtual bool SendIntraFrame() = 0; + // Reuqest each of the remote senders to send an intra frame. + virtual bool RequestIntraFrame() = 0; + // Sets the media options to use. + virtual bool SetOptions(const VideoOptions& options) = 0; + virtual bool GetOptions(VideoOptions* options) const = 0; + virtual void UpdateAspectRatio(int ratio_w, int ratio_h) = 0; + + // Signal errors from MediaChannel. Arguments are: + // ssrc(uint32), and error(VideoMediaChannel::Error). + sigslot::signal2 SignalMediaError; + + protected: + VideoRenderer *renderer_; +}; + +enum DataMessageType { + // TODO(pthatcher): Make this enum match the SCTP PPIDs that WebRTC uses? + DMT_CONTROL = 0, + DMT_BINARY = 1, + DMT_TEXT = 2, +}; + +// Info about data received in DataMediaChannel. For use in +// DataMediaChannel::SignalDataReceived and in all of the signals that +// signal fires, on up the chain. +struct ReceiveDataParams { + // The in-packet stream indentifier. + // For SCTP, this is really SID, not SSRC. + uint32 ssrc; + // The type of message (binary, text, or control). + DataMessageType type; + // A per-stream value incremented per packet in the stream. + int seq_num; + // A per-stream value monotonically increasing with time. + int timestamp; + + ReceiveDataParams() : + ssrc(0), + type(DMT_TEXT), + seq_num(0), + timestamp(0) { + } +}; + +struct SendDataParams { + // The in-packet stream indentifier. + // For SCTP, this is really SID, not SSRC. + uint32 ssrc; + // The type of message (binary, text, or control). + DataMessageType type; + + // For SCTP, whether to send messages flagged as ordered or not. + // If false, messages can be received out of order. + bool ordered; + // For SCTP, whether the messages are sent reliably or not. + // If false, messages may be lost. + bool reliable; + // For SCTP, if reliable == false, provide partial reliability by + // resending up to this many times. Either count or millis + // is supported, not both at the same time. + int max_rtx_count; + // For SCTP, if reliable == false, provide partial reliability by + // resending for up to this many milliseconds. Either count or millis + // is supported, not both at the same time. + int max_rtx_ms; + + SendDataParams() : + ssrc(0), + type(DMT_TEXT), + // TODO(pthatcher): Make these true by default? + ordered(false), + reliable(false), + max_rtx_count(0), + max_rtx_ms(0) { + } +}; + +enum SendDataResult { SDR_SUCCESS, SDR_ERROR, SDR_BLOCK }; + +class DataMediaChannel : public MediaChannel { + public: + enum Error { + ERROR_NONE = 0, // No error. + ERROR_OTHER, // Other errors. + ERROR_SEND_SRTP_ERROR = 200, // Generic SRTP failure. + ERROR_SEND_SRTP_AUTH_FAILED, // Failed to authenticate packets. + ERROR_RECV_SRTP_ERROR, // Generic SRTP failure. + ERROR_RECV_SRTP_AUTH_FAILED, // Failed to authenticate packets. + ERROR_RECV_SRTP_REPLAY, // Packet replay detected. + }; + + virtual ~DataMediaChannel() {} + + virtual bool SetSendBandwidth(bool autobw, int bps) = 0; + virtual bool SetSendCodecs(const std::vector& codecs) = 0; + virtual bool SetRecvCodecs(const std::vector& codecs) = 0; + virtual bool SetRecvRtpHeaderExtensions( + const std::vector& extensions) = 0; + virtual bool SetSendRtpHeaderExtensions( + const std::vector& extensions) = 0; + virtual bool AddSendStream(const StreamParams& sp) = 0; + virtual bool RemoveSendStream(uint32 ssrc) = 0; + virtual bool AddRecvStream(const StreamParams& sp) = 0; + virtual bool RemoveRecvStream(uint32 ssrc) = 0; + virtual bool MuteStream(uint32 ssrc, bool on) { return false; } + // TODO(pthatcher): Implement this. + virtual bool GetStats(DataMediaInfo* info) { return true; } + + virtual bool SetSend(bool send) = 0; + virtual bool SetReceive(bool receive) = 0; + virtual void OnPacketReceived(talk_base::Buffer* packet) = 0; + virtual void OnRtcpReceived(talk_base::Buffer* packet) = 0; + + virtual bool SendData( + const SendDataParams& params, + const talk_base::Buffer& payload, + SendDataResult* result = NULL) = 0; + // Signals when data is received (params, data, len) + sigslot::signal3 SignalDataReceived; + // Signal errors from MediaChannel. Arguments are: + // ssrc(uint32), and error(DataMediaChannel::Error). + sigslot::signal2 SignalMediaError; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_MEDIACHANNEL_H_ diff --git a/talk/media/base/mediacommon.h b/talk/media/base/mediacommon.h new file mode 100644 index 000000000..e0d7ecaa7 --- /dev/null +++ b/talk/media/base/mediacommon.h @@ -0,0 +1,44 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_MEDIACOMMON_H_ +#define TALK_MEDIA_BASE_MEDIACOMMON_H_ + +#include "talk/base/stringencode.h" + +namespace cricket { + +enum MediaCapabilities { + AUDIO_RECV = 1 << 0, + AUDIO_SEND = 1 << 1, + VIDEO_RECV = 1 << 2, + VIDEO_SEND = 1 << 3, +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_MEDIACOMMON_H_ diff --git a/talk/media/base/mediaengine.cc b/talk/media/base/mediaengine.cc new file mode 100644 index 000000000..021cf81f1 --- /dev/null +++ b/talk/media/base/mediaengine.cc @@ -0,0 +1,74 @@ +// +// libjingle +// Copyright 2004 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/media/base/mediaengine.h" + +namespace cricket { +const int MediaEngineInterface::kDefaultAudioDelayOffset = 0; +} + +#if !defined(DISABLE_MEDIA_ENGINE_FACTORY) + +#if defined(HAVE_LINPHONE) +#include "talk/media/other/linphonemediaengine.h" +#endif // HAVE_LINPHONE +#if defined(HAVE_WEBRTC_VOICE) +#include "talk/media/webrtc/webrtcvoiceengine.h" +#endif // HAVE_WEBRTC_VOICE +#if defined(HAVE_WEBRTC_VIDEO) +#include "talk/media/webrtc/webrtcvideoengine.h" +#endif // HAVE_WEBRTC_VIDEO + +namespace cricket { +#if defined(HAVE_WEBRTC_VOICE) +#define AUDIO_ENG_NAME WebRtcVoiceEngine +#else +#define AUDIO_ENG_NAME NullVoiceEngine +#endif + +#if defined(HAVE_WEBRTC_VIDEO) +template<> +CompositeMediaEngine:: + CompositeMediaEngine() { + video_.SetVoiceEngine(&voice_); +} +#define VIDEO_ENG_NAME WebRtcVideoEngine +#endif + +MediaEngineInterface* MediaEngineFactory::Create() { +#if defined(HAVE_LINPHONE) + return new LinphoneMediaEngine("", ""); +#elif defined(AUDIO_ENG_NAME) && defined(VIDEO_ENG_NAME) + return new CompositeMediaEngine(); +#else + return new NullMediaEngine(); +#endif +} + +}; // namespace cricket + +#endif // DISABLE_MEDIA_ENGINE_FACTORY diff --git a/talk/media/base/mediaengine.h b/talk/media/base/mediaengine.h new file mode 100644 index 000000000..5cfcb4d76 --- /dev/null +++ b/talk/media/base/mediaengine.h @@ -0,0 +1,400 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_MEDIAENGINE_H_ +#define TALK_MEDIA_BASE_MEDIAENGINE_H_ + +#ifdef OSX +#include +#endif + +#include +#include +#include + +#include "talk/base/sigslotrepeater.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/mediacommon.h" +#include "talk/media/base/videocapturer.h" +#include "talk/media/base/videocommon.h" +#include "talk/media/base/videoprocessor.h" +#include "talk/media/base/voiceprocessor.h" +#include "talk/media/devices/devicemanager.h" + +#if defined(GOOGLE_CHROME_BUILD) || defined(CHROMIUM_BUILD) +#define DISABLE_MEDIA_ENGINE_FACTORY +#endif + +namespace cricket { + +class VideoCapturer; + +// MediaEngineInterface is an abstraction of a media engine which can be +// subclassed to support different media componentry backends. +// It supports voice and video operations in the same class to facilitate +// proper synchronization between both media types. +class MediaEngineInterface { + public: + // Bitmask flags for options that may be supported by the media engine + // implementation. This can be converted to and from an + // AudioOptions struct for backwards compatibility with calls that + // use flags until we transition to using structs everywhere. + enum AudioFlags { + // Audio processing that attempts to filter away the output signal from + // later inbound pickup. + ECHO_CANCELLATION = 1 << 0, + // Audio processing to adjust the sensitivity of the local mic dynamically. + AUTO_GAIN_CONTROL = 1 << 1, + // Audio processing to filter out background noise. + NOISE_SUPPRESSION = 1 << 2, + // Audio processing to remove background noise of lower frequencies. + HIGHPASS_FILTER = 1 << 3, + // A switch to swap which captured signal is left and right in stereo mode. + STEREO_FLIPPING = 1 << 4, + // Controls delegation echo cancellation to use the OS' facility. + SYSTEM_AEC_MODE = 1 << 5, + + ALL_AUDIO_OPTIONS = (1 << 6) - 1, + DEFAULT_AUDIO_OPTIONS = ECHO_CANCELLATION | AUTO_GAIN_CONTROL | + NOISE_SUPPRESSION | HIGHPASS_FILTER, + }; + + // Default value to be used for SetAudioDelayOffset(). + static const int kDefaultAudioDelayOffset; + + virtual ~MediaEngineInterface() {} + + // Initialization + // Starts the engine. + virtual bool Init(talk_base::Thread* worker_thread) = 0; + // Shuts down the engine. + virtual void Terminate() = 0; + // Returns what the engine is capable of, as a set of Capabilities, above. + virtual int GetCapabilities() = 0; + + // MediaChannel creation + // Creates a voice media channel. Returns NULL on failure. + virtual VoiceMediaChannel *CreateChannel() = 0; + // Creates a video media channel, paired with the specified voice channel. + // Returns NULL on failure. + virtual VideoMediaChannel *CreateVideoChannel( + VoiceMediaChannel* voice_media_channel) = 0; + + // Creates a soundclip object for playing sounds on. Returns NULL on failure. + virtual SoundclipMedia *CreateSoundclip() = 0; + + // Configuration + // Sets global audio options. "options" are from AudioOptions, above. + virtual bool SetAudioOptions(int options) = 0; + // Sets global video options. "options" are from VideoOptions, above. + virtual bool SetVideoOptions(int options) = 0; + // Sets the value used by the echo canceller to offset delay values obtained + // from the OS. + virtual bool SetAudioDelayOffset(int offset) = 0; + // Sets the default (maximum) codec/resolution and encoder option to capture + // and encode video. + virtual bool SetDefaultVideoEncoderConfig(const VideoEncoderConfig& config) + = 0; + + // Device selection + // TODO(tschmelcher): Add method for selecting the soundclip device. + virtual bool SetSoundDevices(const Device* in_device, + const Device* out_device) = 0; + // Sets the externally provided video capturer. The ssrc is the ssrc of the + // (video) stream for which the video capturer should be set. + virtual bool SetVideoCapturer(VideoCapturer* capturer) = 0; + virtual VideoCapturer* GetVideoCapturer() const = 0; + + // Device configuration + // Gets the current speaker volume, as a value between 0 and 255. + virtual bool GetOutputVolume(int* level) = 0; + // Sets the current speaker volume, as a value between 0 and 255. + virtual bool SetOutputVolume(int level) = 0; + + // Local monitoring + // Gets the current microphone level, as a value between 0 and 10. + virtual int GetInputLevel() = 0; + // Starts or stops the local microphone. Useful if local mic info is needed + // prior to a call being connected; the mic will be started automatically + // when a VoiceMediaChannel starts sending. + virtual bool SetLocalMonitor(bool enable) = 0; + // Installs a callback for raw frames from the local camera. + virtual bool SetLocalRenderer(VideoRenderer* renderer) = 0; + // Starts/stops local camera. + virtual bool SetVideoCapture(bool capture) = 0; + + virtual const std::vector& audio_codecs() = 0; + virtual const std::vector& + audio_rtp_header_extensions() = 0; + virtual const std::vector& video_codecs() = 0; + virtual const std::vector& + video_rtp_header_extensions() = 0; + + // Logging control + virtual void SetVoiceLogging(int min_sev, const char* filter) = 0; + virtual void SetVideoLogging(int min_sev, const char* filter) = 0; + + // Voice processors for effects. + virtual bool RegisterVoiceProcessor(uint32 ssrc, + VoiceProcessor* video_processor, + MediaProcessorDirection direction) = 0; + virtual bool UnregisterVoiceProcessor(uint32 ssrc, + VoiceProcessor* video_processor, + MediaProcessorDirection direction) = 0; + + virtual VideoFormat GetStartCaptureFormat() const = 0; + + virtual sigslot::repeater2& + SignalVideoCaptureStateChange() = 0; +}; + + +#if !defined(DISABLE_MEDIA_ENGINE_FACTORY) +class MediaEngineFactory { + public: + static MediaEngineInterface* Create(); +}; +#endif + +// CompositeMediaEngine constructs a MediaEngine from separate +// voice and video engine classes. +template +class CompositeMediaEngine : public MediaEngineInterface { + public: + CompositeMediaEngine() {} + virtual ~CompositeMediaEngine() {} + virtual bool Init(talk_base::Thread* worker_thread) { + if (!voice_.Init(worker_thread)) + return false; + if (!video_.Init(worker_thread)) { + voice_.Terminate(); + return false; + } + SignalVideoCaptureStateChange().repeat(video_.SignalCaptureStateChange); + return true; + } + virtual void Terminate() { + video_.Terminate(); + voice_.Terminate(); + } + + virtual int GetCapabilities() { + return (voice_.GetCapabilities() | video_.GetCapabilities()); + } + virtual VoiceMediaChannel *CreateChannel() { + return voice_.CreateChannel(); + } + virtual VideoMediaChannel *CreateVideoChannel(VoiceMediaChannel* channel) { + return video_.CreateChannel(channel); + } + virtual SoundclipMedia *CreateSoundclip() { + return voice_.CreateSoundclip(); + } + + virtual bool SetAudioOptions(int o) { + return voice_.SetOptions(o); + } + virtual bool SetVideoOptions(int o) { + return video_.SetOptions(o); + } + virtual bool SetAudioDelayOffset(int offset) { + return voice_.SetDelayOffset(offset); + } + virtual bool SetDefaultVideoEncoderConfig(const VideoEncoderConfig& config) { + return video_.SetDefaultEncoderConfig(config); + } + + virtual bool SetSoundDevices(const Device* in_device, + const Device* out_device) { + return voice_.SetDevices(in_device, out_device); + } + virtual bool SetVideoCapturer(VideoCapturer* capturer) { + return video_.SetVideoCapturer(capturer); + } + virtual VideoCapturer* GetVideoCapturer() const { + return video_.GetVideoCapturer(); + } + + virtual bool GetOutputVolume(int* level) { + return voice_.GetOutputVolume(level); + } + virtual bool SetOutputVolume(int level) { + return voice_.SetOutputVolume(level); + } + + virtual int GetInputLevel() { + return voice_.GetInputLevel(); + } + virtual bool SetLocalMonitor(bool enable) { + return voice_.SetLocalMonitor(enable); + } + virtual bool SetLocalRenderer(VideoRenderer* renderer) { + return video_.SetLocalRenderer(renderer); + } + virtual bool SetVideoCapture(bool capture) { + return video_.SetCapture(capture); + } + + virtual const std::vector& audio_codecs() { + return voice_.codecs(); + } + virtual const std::vector& audio_rtp_header_extensions() { + return voice_.rtp_header_extensions(); + } + virtual const std::vector& video_codecs() { + return video_.codecs(); + } + virtual const std::vector& video_rtp_header_extensions() { + return video_.rtp_header_extensions(); + } + + virtual void SetVoiceLogging(int min_sev, const char* filter) { + return voice_.SetLogging(min_sev, filter); + } + virtual void SetVideoLogging(int min_sev, const char* filter) { + return video_.SetLogging(min_sev, filter); + } + + virtual bool RegisterVoiceProcessor(uint32 ssrc, + VoiceProcessor* processor, + MediaProcessorDirection direction) { + return voice_.RegisterProcessor(ssrc, processor, direction); + } + virtual bool UnregisterVoiceProcessor(uint32 ssrc, + VoiceProcessor* processor, + MediaProcessorDirection direction) { + return voice_.UnregisterProcessor(ssrc, processor, direction); + } + virtual VideoFormat GetStartCaptureFormat() const { + return video_.GetStartCaptureFormat(); + } + virtual sigslot::repeater2& + SignalVideoCaptureStateChange() { + return signal_state_change_; + } + + protected: + VOICE voice_; + VIDEO video_; + sigslot::repeater2 signal_state_change_; +}; + +// NullVoiceEngine can be used with CompositeMediaEngine in the case where only +// a video engine is desired. +class NullVoiceEngine { + public: + bool Init(talk_base::Thread* worker_thread) { return true; } + void Terminate() {} + int GetCapabilities() { return 0; } + // If you need this to return an actual channel, use FakeMediaEngine instead. + VoiceMediaChannel* CreateChannel() { + return NULL; + } + SoundclipMedia* CreateSoundclip() { + return NULL; + } + bool SetDelayOffset(int offset) { return true; } + bool SetOptions(int opts) { return true; } + bool SetDevices(const Device* in_device, const Device* out_device) { + return true; + } + bool GetOutputVolume(int* level) { + *level = 0; + return true; + } + bool SetOutputVolume(int level) { return true; } + int GetInputLevel() { return 0; } + bool SetLocalMonitor(bool enable) { return true; } + const std::vector& codecs() { return codecs_; } + const std::vector& rtp_header_extensions() { + return rtp_header_extensions_; + } + void SetLogging(int min_sev, const char* filter) {} + bool RegisterProcessor(uint32 ssrc, + VoiceProcessor* voice_processor, + MediaProcessorDirection direction) { return true; } + bool UnregisterProcessor(uint32 ssrc, + VoiceProcessor* voice_processor, + MediaProcessorDirection direction) { return true; } + + private: + std::vector codecs_; + std::vector rtp_header_extensions_; +}; + +// NullVideoEngine can be used with CompositeMediaEngine in the case where only +// a voice engine is desired. +class NullVideoEngine { + public: + bool Init(talk_base::Thread* worker_thread) { return true; } + void Terminate() {} + int GetCapabilities() { return 0; } + // If you need this to return an actual channel, use FakeMediaEngine instead. + VideoMediaChannel* CreateChannel( + VoiceMediaChannel* voice_media_channel) { + return NULL; + } + bool SetOptions(int opts) { return true; } + bool SetDefaultEncoderConfig(const VideoEncoderConfig& config) { + return true; + } + bool SetLocalRenderer(VideoRenderer* renderer) { return true; } + bool SetCapture(bool capture) { return true; } + const std::vector& codecs() { return codecs_; } + const std::vector& rtp_header_extensions() { + return rtp_header_extensions_; + } + void SetLogging(int min_sev, const char* filter) {} + VideoFormat GetStartCaptureFormat() const { return VideoFormat(); } + bool SetVideoCapturer(VideoCapturer* capturer) { return true; } + VideoCapturer* GetVideoCapturer() const { return NULL; } + + sigslot::signal2 SignalCaptureStateChange; + private: + std::vector codecs_; + std::vector rtp_header_extensions_; +}; + +typedef CompositeMediaEngine NullMediaEngine; + +enum DataChannelType { + DCT_NONE = 0, + DCT_RTP = 1, + DCT_SCTP = 2 +}; + +class DataEngineInterface { + public: + virtual ~DataEngineInterface() {} + virtual DataMediaChannel* CreateChannel(DataChannelType type) = 0; + virtual const std::vector& data_codecs() = 0; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_MEDIAENGINE_H_ diff --git a/talk/media/base/mutedvideocapturer.cc b/talk/media/base/mutedvideocapturer.cc new file mode 100644 index 000000000..0c74b9fd9 --- /dev/null +++ b/talk/media/base/mutedvideocapturer.cc @@ -0,0 +1,135 @@ +/* + * libjingle + * Copyright 2012 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/base/logging.h" +#include "talk/base/thread.h" +#include "talk/media/base/mutedvideocapturer.h" +#include "talk/media/base/videoframe.h" + +#if defined(HAVE_WEBRTC_VIDEO) +#include "talk/media/webrtc/webrtcvideoframe.h" +#endif // HAVE_WEBRTC_VIDEO + + +namespace cricket { + +const char MutedVideoCapturer::kCapturerId[] = "muted_camera"; + +class MutedFramesGenerator : public talk_base::MessageHandler { + public: + explicit MutedFramesGenerator(const VideoFormat& format); + virtual ~MutedFramesGenerator(); + + // Called every |interval| ms. From |format|.interval given in the + // constructor. + sigslot::signal1 SignalFrame; + + protected: + virtual void OnMessage(talk_base::Message* message); + + private: + talk_base::Thread capture_thread_; + talk_base::scoped_ptr muted_frame_; + const VideoFormat format_; + const int interval_; + uint32 create_time_; +}; + +MutedFramesGenerator::MutedFramesGenerator(const VideoFormat& format) + : format_(format), + interval_(static_cast(format.interval / + talk_base::kNumNanosecsPerMillisec)), + create_time_(talk_base::Time()) { + capture_thread_.Start(); + capture_thread_.PostDelayed(interval_, this); +} + +MutedFramesGenerator::~MutedFramesGenerator() { capture_thread_.Clear(this); } + +void MutedFramesGenerator::OnMessage(talk_base::Message* message) { + // Queue a new frame as soon as possible to minimize drift. + capture_thread_.PostDelayed(interval_, this); + if (!muted_frame_) { +#if defined(HAVE_WEBRTC_VIDEO) +#define VIDEO_FRAME_NAME WebRtcVideoFrame +#endif +#if defined(VIDEO_FRAME_NAME) + muted_frame_.reset(new VIDEO_FRAME_NAME()); +#else + return; +#endif + } + uint32 current_timestamp = talk_base::Time(); + // Delta between create time and current time will be correct even if there is + // a wraparound since they are unsigned integers. + uint32 elapsed_time = current_timestamp - create_time_; + if (!muted_frame_->InitToBlack(format_.width, format_.height, 1, 1, + elapsed_time, current_timestamp)) { + LOG(LS_ERROR) << "Failed to create a black frame."; + } + SignalFrame(muted_frame_.get()); +} + +MutedVideoCapturer::MutedVideoCapturer() { SetId(kCapturerId); } + +MutedVideoCapturer::~MutedVideoCapturer() { Stop(); } + +bool MutedVideoCapturer::GetBestCaptureFormat(const VideoFormat& desired, + VideoFormat* best_format) { + *best_format = desired; + return true; +} + +CaptureState MutedVideoCapturer::Start(const VideoFormat& capture_format) { + if (frame_generator_.get()) { + return CS_RUNNING; + } + frame_generator_.reset(new MutedFramesGenerator(capture_format)); + frame_generator_->SignalFrame + .connect(this, &MutedVideoCapturer::OnMutedFrame); + SetCaptureFormat(&capture_format); + return CS_RUNNING; +} + +void MutedVideoCapturer::Stop() { + frame_generator_.reset(); + SetCaptureFormat(NULL); +} + +bool MutedVideoCapturer::IsRunning() { return frame_generator_.get() != NULL; } + +bool MutedVideoCapturer::GetPreferredFourccs(std::vector* fourccs) { + fourccs->clear(); + fourccs->push_back(cricket::FOURCC_I420); + return true; +} + +void MutedVideoCapturer::OnMutedFrame(VideoFrame* muted_frame) { + SignalVideoFrame(this, muted_frame); +} + +} // namespace cricket diff --git a/talk/media/base/mutedvideocapturer.h b/talk/media/base/mutedvideocapturer.h new file mode 100644 index 000000000..fb249a94a --- /dev/null +++ b/talk/media/base/mutedvideocapturer.h @@ -0,0 +1,60 @@ +/* + * libjingle + * Copyright 2012 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. + */ + +#ifndef TALK_MEDIA_BASE_MUTEDVIDEOCAPTURER_H_ +#define TALK_MEDIA_BASE_MUTEDVIDEOCAPTURER_H_ + +#include "talk/base/thread.h" +#include "talk/media/base/videocapturer.h" + +namespace cricket { + +class MutedFramesGenerator; + +class MutedVideoCapturer : public VideoCapturer { + public: + static const char kCapturerId[]; + + MutedVideoCapturer(); + virtual ~MutedVideoCapturer(); + virtual bool GetBestCaptureFormat(const VideoFormat& desired, + VideoFormat* best_format); + virtual CaptureState Start(const VideoFormat& capture_format); + virtual void Stop(); + virtual bool IsRunning(); + virtual bool IsScreencast() const { return false; } + virtual bool GetPreferredFourccs(std::vector* fourccs); + + protected: + void OnMutedFrame(VideoFrame* muted_frame); + + talk_base::scoped_ptr frame_generator_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_MUTEDVIDEOCAPTURER_H_ diff --git a/talk/media/base/mutedvideocapturer_unittest.cc b/talk/media/base/mutedvideocapturer_unittest.cc new file mode 100644 index 000000000..dfb56dfef --- /dev/null +++ b/talk/media/base/mutedvideocapturer_unittest.cc @@ -0,0 +1,96 @@ +/* + * libjingle + * Copyright 2012 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/media/base/mutedvideocapturer.h" + +#include "talk/base/gunit.h" +#include "talk/media/base/videoframe.h" + +class MutedVideoCapturerTest : public sigslot::has_slots<>, + public testing::Test { + protected: + void SetUp() { + frames_received_ = 0; + capturer_.SignalVideoFrame + .connect(this, &MutedVideoCapturerTest::OnVideoFrame); + } + void OnVideoFrame(cricket::VideoCapturer* capturer, + const cricket::VideoFrame* muted_frame) { + EXPECT_EQ(capturer, &capturer_); + ++frames_received_; + received_width_ = muted_frame->GetWidth(); + received_height_ = muted_frame->GetHeight(); + } + int frames_received() { return frames_received_; } + bool ReceivedCorrectFormat() { + return (received_width_ == capturer_.GetCaptureFormat()->width) && + (received_height_ == capturer_.GetCaptureFormat()->height); + } + + cricket::MutedVideoCapturer capturer_; + int frames_received_; + cricket::VideoFormat capture_format_; + int received_width_; + int received_height_; +}; + +TEST_F(MutedVideoCapturerTest, GetBestCaptureFormat) { + cricket::VideoFormat format(640, 360, cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420); + cricket::VideoFormat best_format; + EXPECT_TRUE(capturer_.GetBestCaptureFormat(format, &best_format)); + EXPECT_EQ(format.width, best_format.width); + EXPECT_EQ(format.height, best_format.height); + EXPECT_EQ(format.interval, best_format.interval); + EXPECT_EQ(format.fourcc, best_format.fourcc); +} + +TEST_F(MutedVideoCapturerTest, IsScreencast) { + EXPECT_FALSE(capturer_.IsScreencast()); +} + +TEST_F(MutedVideoCapturerTest, GetPreferredFourccs) { + std::vector fourccs; + EXPECT_TRUE(capturer_.GetPreferredFourccs(&fourccs)); + EXPECT_EQ(fourccs.size(), 1u); + EXPECT_TRUE(capturer_.GetPreferredFourccs(&fourccs)); + EXPECT_EQ(fourccs.size(), 1u); + EXPECT_EQ(fourccs[0], cricket::FOURCC_I420); +} + +TEST_F(MutedVideoCapturerTest, Capturing) { + cricket::VideoFormat format(640, 360, cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420); + EXPECT_EQ(capturer_.Start(format), cricket::CS_RUNNING); + EXPECT_EQ(capturer_.Start(format), cricket::CS_RUNNING); + EXPECT_TRUE(capturer_.IsRunning()); + // 100 ms should be enough to receive 3 frames at FPS of 30. + EXPECT_EQ_WAIT(frames_received(), 1, 100); + EXPECT_TRUE(ReceivedCorrectFormat()); + capturer_.Stop(); + EXPECT_FALSE(capturer_.IsRunning()); +} diff --git a/talk/media/base/nullvideoframe.h b/talk/media/base/nullvideoframe.h new file mode 100644 index 000000000..ff2912966 --- /dev/null +++ b/talk/media/base/nullvideoframe.h @@ -0,0 +1,93 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_NULLVIDEOFRAME_H_ +#define TALK_MEDIA_BASE_NULLVIDEOFRAME_H_ + +#include "talk/media/base/videoframe.h" + +namespace cricket { + +// Simple subclass for use in mocks. +class NullVideoFrame : public VideoFrame { + public: + virtual bool Reset(uint32 format, int w, int h, int dw, int dh, uint8 *sample, + size_t sample_size, size_t pixel_width, + size_t pixel_height, int64 elapsed_time, int64 time_stamp, + int rotation) { + return false; + } + virtual bool InitToBlack(int w, int h, size_t pixel_width, + size_t pixel_height, int64 elapsed_time, + int64 time_stamp) { + return false; + } + virtual size_t GetWidth() const { return 0; } + virtual size_t GetHeight() const { return 0; } + virtual const uint8 *GetYPlane() const { return NULL; } + virtual const uint8 *GetUPlane() const { return NULL; } + virtual const uint8 *GetVPlane() const { return NULL; } + virtual uint8 *GetYPlane() { return NULL; } + virtual uint8 *GetUPlane() { return NULL; } + virtual uint8 *GetVPlane() { return NULL; } + virtual int32 GetYPitch() const { return 0; } + virtual int32 GetUPitch() const { return 0; } + virtual int32 GetVPitch() const { return 0; } + + virtual size_t GetPixelWidth() const { return 1; } + virtual size_t GetPixelHeight() const { return 1; } + virtual int64 GetElapsedTime() const { return 0; } + virtual int64 GetTimeStamp() const { return 0; } + virtual void SetElapsedTime(int64 elapsed_time) {} + virtual void SetTimeStamp(int64 time_stamp) {} + virtual int GetRotation() const { return 0; } + + virtual VideoFrame *Copy() const { return NULL; } + + virtual bool MakeExclusive() { return false; } + + virtual size_t CopyToBuffer(uint8 *buffer, size_t size) const { return 0; } + + virtual size_t ConvertToRgbBuffer(uint32 to_fourcc, uint8 *buffer, + size_t size, int stride_rgb) const { + return 0; + } + + virtual void StretchToPlanes( + uint8 *y, uint8 *u, uint8 *v, int32 pitchY, int32 pitchU, int32 pitchV, + size_t width, size_t height, bool interpolate, bool crop) const {} + + virtual VideoFrame *CreateEmptyFrame(int w, int h, size_t pixel_width, + size_t pixel_height, int64 elapsed_time, + int64 time_stamp) const { + return NULL; + } +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_NULLVIDEOFRAME_H_ diff --git a/talk/media/base/nullvideorenderer.h b/talk/media/base/nullvideorenderer.h new file mode 100644 index 000000000..4652c47bf --- /dev/null +++ b/talk/media/base/nullvideorenderer.h @@ -0,0 +1,48 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_NULLVIDEORENDERER_H_ +#define TALK_MEDIA_BASE_NULLVIDEORENDERER_H_ + +#include "talk/media/base/videorenderer.h" + +namespace cricket { + +// Simple implementation for use in tests. +class NullVideoRenderer : public VideoRenderer { + virtual bool SetSize(int width, int height, int reserved) { + return true; + } + // Called when a new frame is available for display. + virtual bool RenderFrame(const VideoFrame *frame) { + return true; + } +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_NULLVIDEORENDERER_H_ diff --git a/talk/media/base/rtpdataengine.cc b/talk/media/base/rtpdataengine.cc new file mode 100644 index 000000000..5a39a54f3 --- /dev/null +++ b/talk/media/base/rtpdataengine.cc @@ -0,0 +1,381 @@ +/* + * libjingle + * Copyright 2012 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/media/base/rtpdataengine.h" + +#include "talk/base/buffer.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/ratelimiter.h" +#include "talk/base/timing.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/rtputils.h" +#include "talk/media/base/streamparams.h" + +namespace cricket { + +// We want to avoid IP fragmentation. +static const size_t kDataMaxRtpPacketLen = 1200U; +// We reserve space after the RTP header for future wiggle room. +static const unsigned char kReservedSpace[] = { + 0x00, 0x00, 0x00, 0x00 +}; + +// Amount of overhead SRTP may take. We need to leave room in the +// buffer for it, otherwise SRTP will fail later. If SRTP ever uses +// more than this, we need to increase this number. +static const size_t kMaxSrtpHmacOverhead = 16; + +RtpDataEngine::RtpDataEngine() { + data_codecs_.push_back( + DataCodec(kGoogleRtpDataCodecId, + kGoogleRtpDataCodecName, 0)); + SetTiming(new talk_base::Timing()); +} + +DataMediaChannel* RtpDataEngine::CreateChannel( + DataChannelType data_channel_type) { + if (data_channel_type != DCT_RTP) { + return NULL; + } + return new RtpDataMediaChannel(timing_.get()); +} + +// TODO(pthatcher): Should we move these find/get functions somewhere +// common? +bool FindCodecById(const std::vector& codecs, + int id, DataCodec* codec_out) { + std::vector::const_iterator iter; + for (iter = codecs.begin(); iter != codecs.end(); ++iter) { + if (iter->id == id) { + *codec_out = *iter; + return true; + } + } + return false; +} + +bool FindCodecByName(const std::vector& codecs, + const std::string& name, DataCodec* codec_out) { + std::vector::const_iterator iter; + for (iter = codecs.begin(); iter != codecs.end(); ++iter) { + if (iter->name == name) { + *codec_out = *iter; + return true; + } + } + return false; +} + +RtpDataMediaChannel::RtpDataMediaChannel(talk_base::Timing* timing) { + Construct(timing); +} + +RtpDataMediaChannel::RtpDataMediaChannel() { + Construct(NULL); +} + +void RtpDataMediaChannel::Construct(talk_base::Timing* timing) { + sending_ = false; + receiving_ = false; + timing_ = timing; + send_limiter_.reset(new talk_base::RateLimiter(kDataMaxBandwidth / 8, 1.0)); +} + + +RtpDataMediaChannel::~RtpDataMediaChannel() { + std::map::const_iterator iter; + for (iter = rtp_clock_by_send_ssrc_.begin(); + iter != rtp_clock_by_send_ssrc_.end(); + ++iter) { + delete iter->second; + } +} + +void RtpClock::Tick( + double now, int* seq_num, uint32* timestamp) { + *seq_num = ++last_seq_num_; + *timestamp = timestamp_offset_ + static_cast(now * clockrate_); +} + +const DataCodec* FindUnknownCodec(const std::vector& codecs) { + DataCodec data_codec(kGoogleRtpDataCodecId, kGoogleRtpDataCodecName, 0); + std::vector::const_iterator iter; + for (iter = codecs.begin(); iter != codecs.end(); ++iter) { + if (!iter->Matches(data_codec)) { + return &(*iter); + } + } + return NULL; +} + +const DataCodec* FindKnownCodec(const std::vector& codecs) { + DataCodec data_codec(kGoogleRtpDataCodecId, kGoogleRtpDataCodecName, 0); + std::vector::const_iterator iter; + for (iter = codecs.begin(); iter != codecs.end(); ++iter) { + if (iter->Matches(data_codec)) { + return &(*iter); + } + } + return NULL; +} + +bool RtpDataMediaChannel::SetRecvCodecs(const std::vector& codecs) { + const DataCodec* unknown_codec = FindUnknownCodec(codecs); + if (unknown_codec) { + LOG(LS_WARNING) << "Failed to SetRecvCodecs because of unknown codec: " + << unknown_codec->ToString(); + return false; + } + + recv_codecs_ = codecs; + return true; +} + +bool RtpDataMediaChannel::SetSendCodecs(const std::vector& codecs) { + const DataCodec* known_codec = FindKnownCodec(codecs); + if (!known_codec) { + LOG(LS_WARNING) << + "Failed to SetSendCodecs because there is no known codec."; + return false; + } + + send_codecs_ = codecs; + return true; +} + +bool RtpDataMediaChannel::AddSendStream(const StreamParams& stream) { + if (!stream.has_ssrcs()) { + return false; + } + + StreamParams found_stream; + if (GetStreamBySsrc(send_streams_, stream.first_ssrc(), &found_stream)) { + LOG(LS_WARNING) << "Not adding data send stream '" << stream.id + << "' with ssrc=" << stream.first_ssrc() + << " because stream already exists."; + return false; + } + + send_streams_.push_back(stream); + // TODO(pthatcher): This should be per-stream, not per-ssrc. + // And we should probably allow more than one per stream. + rtp_clock_by_send_ssrc_[stream.first_ssrc()] = new RtpClock( + kDataCodecClockrate, + talk_base::CreateRandomNonZeroId(), talk_base::CreateRandomNonZeroId()); + + LOG(LS_INFO) << "Added data send stream '" << stream.id + << "' with ssrc=" << stream.first_ssrc(); + return true; +} + +bool RtpDataMediaChannel::RemoveSendStream(uint32 ssrc) { + StreamParams found_stream; + if (!GetStreamBySsrc(send_streams_, ssrc, &found_stream)) { + return false; + } + + RemoveStreamBySsrc(&send_streams_, ssrc); + delete rtp_clock_by_send_ssrc_[ssrc]; + rtp_clock_by_send_ssrc_.erase(ssrc); + return true; +} + +bool RtpDataMediaChannel::AddRecvStream(const StreamParams& stream) { + if (!stream.has_ssrcs()) { + return false; + } + + StreamParams found_stream; + if (GetStreamBySsrc(recv_streams_, stream.first_ssrc(), &found_stream)) { + LOG(LS_WARNING) << "Not adding data recv stream '" << stream.id + << "' with ssrc=" << stream.first_ssrc() + << " because stream already exists."; + return false; + } + + recv_streams_.push_back(stream); + LOG(LS_INFO) << "Added data recv stream '" << stream.id + << "' with ssrc=" << stream.first_ssrc(); + return true; +} + +bool RtpDataMediaChannel::RemoveRecvStream(uint32 ssrc) { + RemoveStreamBySsrc(&recv_streams_, ssrc); + return true; +} + +void RtpDataMediaChannel::OnPacketReceived(talk_base::Buffer* packet) { + RtpHeader header; + if (!GetRtpHeader(packet->data(), packet->length(), &header)) { + // Don't want to log for every corrupt packet. + // LOG(LS_WARNING) << "Could not read rtp header from packet of length " + // << packet->length() << "."; + return; + } + + size_t header_length; + if (!GetRtpHeaderLen(packet->data(), packet->length(), &header_length)) { + // Don't want to log for every corrupt packet. + // LOG(LS_WARNING) << "Could not read rtp header" + // << length from packet of length " + // << packet->length() << "."; + return; + } + const char* data = packet->data() + header_length + sizeof(kReservedSpace); + size_t data_len = packet->length() - header_length - sizeof(kReservedSpace); + + if (!receiving_) { + LOG(LS_WARNING) << "Not receiving packet " + << header.ssrc << ":" << header.seq_num + << " before SetReceive(true) called."; + return; + } + + DataCodec codec; + if (!FindCodecById(recv_codecs_, header.payload_type, &codec)) { + LOG(LS_WARNING) << "Not receiving packet " + << header.ssrc << ":" << header.seq_num + << " (" << data_len << ")" + << " because unknown payload id: " << header.payload_type; + return; + } + + StreamParams found_stream; + if (!GetStreamBySsrc(recv_streams_, header.ssrc, &found_stream)) { + LOG(LS_WARNING) << "Received packet for unknown ssrc: " << header.ssrc; + return; + } + + // Uncomment this for easy debugging. + // LOG(LS_INFO) << "Received packet" + // << " groupid=" << found_stream.groupid + // << ", ssrc=" << header.ssrc + // << ", seqnum=" << header.seq_num + // << ", timestamp=" << header.timestamp + // << ", len=" << data_len; + + ReceiveDataParams params; + params.ssrc = header.ssrc; + params.seq_num = header.seq_num; + params.timestamp = header.timestamp; + SignalDataReceived(params, data, data_len); +} + +bool RtpDataMediaChannel::SetSendBandwidth(bool autobw, int bps) { + if (autobw || bps <= 0) { + bps = kDataMaxBandwidth; + } + send_limiter_.reset(new talk_base::RateLimiter(bps / 8, 1.0)); + LOG(LS_INFO) << "RtpDataMediaChannel::SetSendBandwidth to " << bps << "bps."; + return true; +} + +bool RtpDataMediaChannel::SendData( + const SendDataParams& params, + const talk_base::Buffer& payload, + SendDataResult* result) { + if (result) { + // If we return true, we'll set this to SDR_SUCCESS. + *result = SDR_ERROR; + } + if (!sending_) { + LOG(LS_WARNING) << "Not sending packet with ssrc=" << params.ssrc + << " len=" << payload.length() << " before SetSend(true)."; + return false; + } + + if (params.type != cricket::DMT_TEXT) { + LOG(LS_WARNING) << "Not sending data because binary type is unsupported."; + return false; + } + + StreamParams found_stream; + if (!GetStreamBySsrc(send_streams_, params.ssrc, &found_stream)) { + LOG(LS_WARNING) << "Not sending data because ssrc is unknown: " + << params.ssrc; + return false; + } + + DataCodec found_codec; + if (!FindCodecByName(send_codecs_, kGoogleRtpDataCodecName, &found_codec)) { + LOG(LS_WARNING) << "Not sending data because codec is unknown: " + << kGoogleRtpDataCodecName; + return false; + } + + size_t packet_len = (kMinRtpPacketLen + sizeof(kReservedSpace) + + payload.length() + kMaxSrtpHmacOverhead); + if (packet_len > kDataMaxRtpPacketLen) { + return false; + } + + double now = timing_->TimerNow(); + + if (!send_limiter_->CanUse(packet_len, now)) { + LOG(LS_VERBOSE) << "Dropped data packet of len=" << packet_len + << "; already sent " << send_limiter_->used_in_period() + << "/" << send_limiter_->max_per_period(); + return false; + } else { + LOG(LS_VERBOSE) << "Sending data packet of len=" << packet_len + << "; already sent " << send_limiter_->used_in_period() + << "/" << send_limiter_->max_per_period(); + } + + RtpHeader header; + header.payload_type = found_codec.id; + header.ssrc = params.ssrc; + rtp_clock_by_send_ssrc_[header.ssrc]->Tick( + now, &header.seq_num, &header.timestamp); + + talk_base::Buffer packet; + packet.SetCapacity(packet_len); + packet.SetLength(kMinRtpPacketLen); + if (!SetRtpHeader(packet.data(), packet.length(), header)) { + return false; + } + packet.AppendData(&kReservedSpace, sizeof(kReservedSpace)); + packet.AppendData(payload.data(), payload.length()); + + // Uncomment this for easy debugging. + // LOG(LS_INFO) << "Sent packet: " + // << " stream=" << found_stream.id + // << ", seqnum=" << header.seq_num + // << ", timestamp=" << header.timestamp + // << ", len=" << data_len; + + network_interface()->SendPacket(&packet); + send_limiter_->Use(packet_len, now); + if (result) { + *result = SDR_SUCCESS; + } + return true; +} + +} // namespace cricket diff --git a/talk/media/base/rtpdataengine.h b/talk/media/base/rtpdataengine.h new file mode 100644 index 000000000..bc7b667ea --- /dev/null +++ b/talk/media/base/rtpdataengine.h @@ -0,0 +1,142 @@ +/* + * libjingle + * Copyright 2012 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. + */ + +#ifndef TALK_MEDIA_BASE_RTPDATAENGINE_H_ +#define TALK_MEDIA_BASE_RTPDATAENGINE_H_ + +#include +#include + +#include "talk/base/timing.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/mediaengine.h" + +namespace cricket { + +struct DataCodec; + +class RtpDataEngine : public DataEngineInterface { + public: + RtpDataEngine(); + + virtual DataMediaChannel* CreateChannel(DataChannelType data_channel_type); + + virtual const std::vector& data_codecs() { + return data_codecs_; + } + + // Mostly for testing with a fake clock. Ownership is passed in. + void SetTiming(talk_base::Timing* timing) { + timing_.reset(timing); + } + + private: + std::vector data_codecs_; + talk_base::scoped_ptr timing_; +}; + +// Keep track of sequence number and timestamp of an RTP stream. The +// sequence number starts with a "random" value and increments. The +// timestamp starts with a "random" value and increases monotonically +// according to the clockrate. +class RtpClock { + public: + RtpClock(int clockrate, uint16 first_seq_num, uint32 timestamp_offset) + : clockrate_(clockrate), + last_seq_num_(first_seq_num), + timestamp_offset_(timestamp_offset) { + } + + // Given the current time (in number of seconds which must be + // monotonically increasing), Return the next sequence number and + // timestamp. + void Tick(double now, int* seq_num, uint32* timestamp); + + private: + int clockrate_; + uint16 last_seq_num_; + uint32 timestamp_offset_; +}; + +class RtpDataMediaChannel : public DataMediaChannel { + public: + // Timing* Used for the RtpClock + explicit RtpDataMediaChannel(talk_base::Timing* timing); + // Sets Timing == NULL, so you'll need to call set_timer() before + // using it. This is needed by FakeMediaEngine. + RtpDataMediaChannel(); + virtual ~RtpDataMediaChannel(); + + void set_timing(talk_base::Timing* timing) { + timing_ = timing; + } + + virtual bool SetSendBandwidth(bool autobw, int bps); + virtual bool SetRecvRtpHeaderExtensions( + const std::vector& extensions) { return true; } + virtual bool SetSendRtpHeaderExtensions( + const std::vector& extensions) { return true; } + virtual bool SetSendCodecs(const std::vector& codecs); + virtual bool SetRecvCodecs(const std::vector& codecs); + virtual bool AddSendStream(const StreamParams& sp); + virtual bool RemoveSendStream(uint32 ssrc); + virtual bool AddRecvStream(const StreamParams& sp); + virtual bool RemoveRecvStream(uint32 ssrc); + virtual bool SetSend(bool send) { + sending_ = send; + return true; + } + virtual bool SetReceive(bool receive) { + receiving_ = receive; + return true; + } + virtual void OnPacketReceived(talk_base::Buffer* packet); + virtual void OnRtcpReceived(talk_base::Buffer* packet) {} + virtual void OnReadyToSend(bool ready) {} + virtual bool SendData( + const SendDataParams& params, + const talk_base::Buffer& payload, + SendDataResult* result); + + private: + void Construct(talk_base::Timing* timing); + + bool sending_; + bool receiving_; + talk_base::Timing* timing_; + std::vector send_codecs_; + std::vector recv_codecs_; + std::vector send_streams_; + std::vector recv_streams_; + std::map rtp_clock_by_send_ssrc_; + talk_base::scoped_ptr send_limiter_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_RTPDATAENGINE_H_ diff --git a/talk/media/base/rtpdataengine_unittest.cc b/talk/media/base/rtpdataengine_unittest.cc new file mode 100644 index 000000000..23b57d4d2 --- /dev/null +++ b/talk/media/base/rtpdataengine_unittest.cc @@ -0,0 +1,463 @@ +/* + * libjingle + * Copyright 2012 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 + +#include "talk/base/buffer.h" +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/timing.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/fakenetworkinterface.h" +#include "talk/media/base/rtpdataengine.h" +#include "talk/media/base/rtputils.h" + +class FakeTiming : public talk_base::Timing { + public: + FakeTiming() : now_(0.0) {} + + virtual double TimerNow() { + return now_; + } + + void set_now(double now) { + now_ = now; + } + + private: + double now_; +}; + +class FakeDataReceiver : public sigslot::has_slots<> { + public: + FakeDataReceiver() : has_received_data_(false) {} + + void OnDataReceived( + const cricket::ReceiveDataParams& params, + const char* data, size_t len) { + has_received_data_ = true; + last_received_data_ = std::string(data, len); + last_received_data_len_ = len; + last_received_data_params_ = params; + } + + bool has_received_data() const { return has_received_data_; } + std::string last_received_data() const { return last_received_data_; } + size_t last_received_data_len() const { return last_received_data_len_; } + cricket::ReceiveDataParams last_received_data_params() const { + return last_received_data_params_; + } + + private: + bool has_received_data_; + std::string last_received_data_; + size_t last_received_data_len_; + cricket::ReceiveDataParams last_received_data_params_; +}; + +class RtpDataMediaChannelTest : public testing::Test { + protected: + virtual void SetUp() { + // Seed needed for each test to satisfy expectations. + iface_.reset(new cricket::FakeNetworkInterface()); + timing_ = new FakeTiming(); + dme_.reset(CreateEngine(timing_)); + receiver_.reset(new FakeDataReceiver()); + } + + void SetNow(double now) { + timing_->set_now(now); + } + + cricket::RtpDataEngine* CreateEngine(FakeTiming* timing) { + cricket::RtpDataEngine* dme = new cricket::RtpDataEngine(); + dme->SetTiming(timing); + return dme; + } + + cricket::RtpDataMediaChannel* CreateChannel() { + return CreateChannel(dme_.get()); + } + + cricket::RtpDataMediaChannel* CreateChannel(cricket::RtpDataEngine* dme) { + cricket::RtpDataMediaChannel* channel = + static_cast(dme->CreateChannel( + cricket::DCT_RTP)); + channel->SetInterface(iface_.get()); + channel->SignalDataReceived.connect( + receiver_.get(), &FakeDataReceiver::OnDataReceived); + return channel; + } + + FakeDataReceiver* receiver() { + return receiver_.get(); + } + + bool HasReceivedData() { + return receiver_->has_received_data(); + } + + std::string GetReceivedData() { + return receiver_->last_received_data(); + } + + size_t GetReceivedDataLen() { + return receiver_->last_received_data_len(); + } + + cricket::ReceiveDataParams GetReceivedDataParams() { + return receiver_->last_received_data_params(); + } + + bool HasSentData(int count) { + return (iface_->NumRtpPackets() > count); + } + + std::string GetSentData(int index) { + // Assume RTP header of length 12 + const talk_base::Buffer* packet = iface_->GetRtpPacket(index); + if (packet->length() > 12) { + return std::string(packet->data() + 12, packet->length() - 12); + } else { + return ""; + } + } + + cricket::RtpHeader GetSentDataHeader(int index) { + const talk_base::Buffer* packet = iface_->GetRtpPacket(index); + cricket::RtpHeader header; + GetRtpHeader(packet->data(), packet->length(), &header); + return header; + } + + private: + talk_base::scoped_ptr dme_; + // Timing passed into dme_. Owned by dme_; + FakeTiming* timing_; + talk_base::scoped_ptr iface_; + talk_base::scoped_ptr receiver_; +}; + +TEST_F(RtpDataMediaChannelTest, SetUnknownCodecs) { + talk_base::scoped_ptr dmc(CreateChannel()); + + cricket::DataCodec known_codec; + known_codec.id = 103; + known_codec.name = "google-data"; + cricket::DataCodec unknown_codec; + unknown_codec.id = 104; + unknown_codec.name = "unknown-data"; + + std::vector known_codecs; + known_codecs.push_back(known_codec); + + std::vector unknown_codecs; + unknown_codecs.push_back(unknown_codec); + + std::vector mixed_codecs; + mixed_codecs.push_back(known_codec); + mixed_codecs.push_back(unknown_codec); + + EXPECT_TRUE(dmc->SetSendCodecs(known_codecs)); + EXPECT_FALSE(dmc->SetSendCodecs(unknown_codecs)); + EXPECT_TRUE(dmc->SetSendCodecs(mixed_codecs)); + EXPECT_TRUE(dmc->SetRecvCodecs(known_codecs)); + EXPECT_FALSE(dmc->SetRecvCodecs(unknown_codecs)); + EXPECT_FALSE(dmc->SetRecvCodecs(mixed_codecs)); +} + +TEST_F(RtpDataMediaChannelTest, AddRemoveSendStream) { + talk_base::scoped_ptr dmc(CreateChannel()); + + cricket::StreamParams stream1; + stream1.add_ssrc(41); + EXPECT_TRUE(dmc->AddSendStream(stream1)); + cricket::StreamParams stream2; + stream2.add_ssrc(42); + EXPECT_TRUE(dmc->AddSendStream(stream2)); + + EXPECT_TRUE(dmc->RemoveSendStream(41)); + EXPECT_TRUE(dmc->RemoveSendStream(42)); + EXPECT_FALSE(dmc->RemoveSendStream(43)); +} + +TEST_F(RtpDataMediaChannelTest, AddRemoveRecvStream) { + talk_base::scoped_ptr dmc(CreateChannel()); + + cricket::StreamParams stream1; + stream1.add_ssrc(41); + EXPECT_TRUE(dmc->AddRecvStream(stream1)); + cricket::StreamParams stream2; + stream2.add_ssrc(42); + EXPECT_TRUE(dmc->AddRecvStream(stream2)); + EXPECT_FALSE(dmc->AddRecvStream(stream2)); + + EXPECT_TRUE(dmc->RemoveRecvStream(41)); + EXPECT_TRUE(dmc->RemoveRecvStream(42)); +} + +TEST_F(RtpDataMediaChannelTest, SendData) { + talk_base::scoped_ptr dmc(CreateChannel()); + + cricket::SendDataParams params; + params.ssrc = 42; + unsigned char data[] = "food"; + talk_base::Buffer payload(data, 4); + unsigned char padded_data[] = { + 0x00, 0x00, 0x00, 0x00, + 'f', 'o', 'o', 'd', + }; + cricket::SendDataResult result; + + // Not sending + EXPECT_FALSE(dmc->SendData(params, payload, &result)); + EXPECT_EQ(cricket::SDR_ERROR, result); + EXPECT_FALSE(HasSentData(0)); + ASSERT_TRUE(dmc->SetSend(true)); + + // Unknown stream name. + EXPECT_FALSE(dmc->SendData(params, payload, &result)); + EXPECT_EQ(cricket::SDR_ERROR, result); + EXPECT_FALSE(HasSentData(0)); + + cricket::StreamParams stream; + stream.add_ssrc(42); + ASSERT_TRUE(dmc->AddSendStream(stream)); + + // Unknown codec; + EXPECT_FALSE(dmc->SendData(params, payload, &result)); + EXPECT_EQ(cricket::SDR_ERROR, result); + EXPECT_FALSE(HasSentData(0)); + + cricket::DataCodec codec; + codec.id = 103; + codec.name = cricket::kGoogleRtpDataCodecName; + std::vector codecs; + codecs.push_back(codec); + ASSERT_TRUE(dmc->SetSendCodecs(codecs)); + + // Length too large; + std::string x10000(10000, 'x'); + EXPECT_FALSE(dmc->SendData( + params, talk_base::Buffer(x10000.data(), x10000.length()), &result)); + EXPECT_EQ(cricket::SDR_ERROR, result); + EXPECT_FALSE(HasSentData(0)); + + // Finally works! + EXPECT_TRUE(dmc->SendData(params, payload, &result)); + EXPECT_EQ(cricket::SDR_SUCCESS, result); + ASSERT_TRUE(HasSentData(0)); + EXPECT_EQ(sizeof(padded_data), GetSentData(0).length()); + EXPECT_EQ(0, memcmp( + padded_data, GetSentData(0).data(), sizeof(padded_data))); + cricket::RtpHeader header0 = GetSentDataHeader(0); + EXPECT_NE(0, header0.seq_num); + EXPECT_NE(0U, header0.timestamp); + EXPECT_EQ(header0.ssrc, 42U); + EXPECT_EQ(header0.payload_type, 103); + + // Should bump timestamp by 180000 because the clock rate is 90khz. + SetNow(2); + + EXPECT_TRUE(dmc->SendData(params, payload, &result)); + ASSERT_TRUE(HasSentData(1)); + EXPECT_EQ(sizeof(padded_data), GetSentData(1).length()); + EXPECT_EQ(0, memcmp( + padded_data, GetSentData(1).data(), sizeof(padded_data))); + cricket::RtpHeader header1 = GetSentDataHeader(1); + EXPECT_EQ(header1.ssrc, 42U); + EXPECT_EQ(header1.payload_type, 103); + EXPECT_EQ(header0.seq_num + 1, header1.seq_num); + EXPECT_EQ(header0.timestamp + 180000, header1.timestamp); +} + +TEST_F(RtpDataMediaChannelTest, SendDataMultipleClocks) { + // Timings owned by RtpDataEngines. + FakeTiming* timing1 = new FakeTiming(); + talk_base::scoped_ptr dme1(CreateEngine(timing1)); + talk_base::scoped_ptr dmc1( + CreateChannel(dme1.get())); + FakeTiming* timing2 = new FakeTiming(); + talk_base::scoped_ptr dme2(CreateEngine(timing2)); + talk_base::scoped_ptr dmc2( + CreateChannel(dme2.get())); + + ASSERT_TRUE(dmc1->SetSend(true)); + ASSERT_TRUE(dmc2->SetSend(true)); + + cricket::StreamParams stream1; + stream1.add_ssrc(41); + ASSERT_TRUE(dmc1->AddSendStream(stream1)); + cricket::StreamParams stream2; + stream2.add_ssrc(42); + ASSERT_TRUE(dmc2->AddSendStream(stream2)); + + cricket::DataCodec codec; + codec.id = 103; + codec.name = cricket::kGoogleRtpDataCodecName; + std::vector codecs; + codecs.push_back(codec); + ASSERT_TRUE(dmc1->SetSendCodecs(codecs)); + ASSERT_TRUE(dmc2->SetSendCodecs(codecs)); + + cricket::SendDataParams params1; + params1.ssrc = 41; + cricket::SendDataParams params2; + params2.ssrc = 42; + + unsigned char data[] = "foo"; + talk_base::Buffer payload(data, 3); + cricket::SendDataResult result; + + EXPECT_TRUE(dmc1->SendData(params1, payload, &result)); + EXPECT_TRUE(dmc2->SendData(params2, payload, &result)); + + // Should bump timestamp by 90000 because the clock rate is 90khz. + timing1->set_now(1); + // Should bump timestamp by 180000 because the clock rate is 90khz. + timing2->set_now(2); + + EXPECT_TRUE(dmc1->SendData(params1, payload, &result)); + EXPECT_TRUE(dmc2->SendData(params2, payload, &result)); + + ASSERT_TRUE(HasSentData(3)); + cricket::RtpHeader header1a = GetSentDataHeader(0); + cricket::RtpHeader header2a = GetSentDataHeader(1); + cricket::RtpHeader header1b = GetSentDataHeader(2); + cricket::RtpHeader header2b = GetSentDataHeader(3); + + EXPECT_EQ(header1a.seq_num + 1, header1b.seq_num); + EXPECT_EQ(header1a.timestamp + 90000, header1b.timestamp); + EXPECT_EQ(header2a.seq_num + 1, header2b.seq_num); + EXPECT_EQ(header2a.timestamp + 180000, header2b.timestamp); +} + +TEST_F(RtpDataMediaChannelTest, SendDataRate) { + talk_base::scoped_ptr dmc(CreateChannel()); + + ASSERT_TRUE(dmc->SetSend(true)); + + cricket::DataCodec codec; + codec.id = 103; + codec.name = cricket::kGoogleRtpDataCodecName; + std::vector codecs; + codecs.push_back(codec); + ASSERT_TRUE(dmc->SetSendCodecs(codecs)); + + cricket::StreamParams stream; + stream.add_ssrc(42); + ASSERT_TRUE(dmc->AddSendStream(stream)); + + cricket::SendDataParams params; + params.ssrc = 42; + unsigned char data[] = "food"; + talk_base::Buffer payload(data, 4); + cricket::SendDataResult result; + + // With rtp overhead of 32 bytes, each one of our packets is 36 + // bytes, or 288 bits. So, a limit of 872bps will allow 3 packets, + // but not four. + dmc->SetSendBandwidth(false, 872); + + EXPECT_TRUE(dmc->SendData(params, payload, &result)); + EXPECT_TRUE(dmc->SendData(params, payload, &result)); + EXPECT_TRUE(dmc->SendData(params, payload, &result)); + EXPECT_FALSE(dmc->SendData(params, payload, &result)); + EXPECT_FALSE(dmc->SendData(params, payload, &result)); + + SetNow(0.9); + EXPECT_FALSE(dmc->SendData(params, payload, &result)); + + SetNow(1.1); + EXPECT_TRUE(dmc->SendData(params, payload, &result)); + EXPECT_TRUE(dmc->SendData(params, payload, &result)); + SetNow(1.9); + EXPECT_TRUE(dmc->SendData(params, payload, &result)); + + SetNow(2.2); + EXPECT_TRUE(dmc->SendData(params, payload, &result)); + EXPECT_TRUE(dmc->SendData(params, payload, &result)); + EXPECT_TRUE(dmc->SendData(params, payload, &result)); + EXPECT_FALSE(dmc->SendData(params, payload, &result)); +} + +TEST_F(RtpDataMediaChannelTest, ReceiveData) { + // PT= 103, SN=2, TS=3, SSRC = 4, data = "abcde" + unsigned char data[] = { + 0x80, 0x67, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x2A, + 0x00, 0x00, 0x00, 0x00, + 'a', 'b', 'c', 'd', 'e' + }; + talk_base::Buffer packet(data, sizeof(data)); + + talk_base::scoped_ptr dmc(CreateChannel()); + + // SetReceived not called. + dmc->OnPacketReceived(&packet); + EXPECT_FALSE(HasReceivedData()); + + dmc->SetReceive(true); + + // Unknown payload id + dmc->OnPacketReceived(&packet); + EXPECT_FALSE(HasReceivedData()); + + cricket::DataCodec codec; + codec.id = 103; + codec.name = cricket::kGoogleRtpDataCodecName; + std::vector codecs; + codecs.push_back(codec); + ASSERT_TRUE(dmc->SetRecvCodecs(codecs)); + + // Unknown stream + dmc->OnPacketReceived(&packet); + EXPECT_FALSE(HasReceivedData()); + + cricket::StreamParams stream; + stream.add_ssrc(42); + ASSERT_TRUE(dmc->AddRecvStream(stream)); + + // Finally works! + dmc->OnPacketReceived(&packet); + EXPECT_TRUE(HasReceivedData()); + EXPECT_EQ("abcde", GetReceivedData()); + EXPECT_EQ(5U, GetReceivedDataLen()); +} + +TEST_F(RtpDataMediaChannelTest, InvalidRtpPackets) { + unsigned char data[] = { + 0x80, 0x65, 0x00, 0x02 + }; + talk_base::Buffer packet(data, sizeof(data)); + + talk_base::scoped_ptr dmc(CreateChannel()); + + // Too short + dmc->OnPacketReceived(&packet); + EXPECT_FALSE(HasReceivedData()); +} diff --git a/talk/media/base/rtpdump.cc b/talk/media/base/rtpdump.cc new file mode 100644 index 000000000..10c835c8c --- /dev/null +++ b/talk/media/base/rtpdump.cc @@ -0,0 +1,424 @@ +/* + * libjingle + * Copyright 2010 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/media/base/rtpdump.h" + +#include + +#include + +#include "talk/base/byteorder.h" +#include "talk/base/logging.h" +#include "talk/base/timeutils.h" +#include "talk/media/base/rtputils.h" + +namespace { +static const int kRtpSsrcOffset = 8; +const int kWarnSlowWritesDelayMs = 50; +} // namespace + +namespace cricket { + +const char RtpDumpFileHeader::kFirstLine[] = "#!rtpplay1.0 0.0.0.0/0\n"; + +RtpDumpFileHeader::RtpDumpFileHeader(uint32 start_ms, uint32 s, uint16 p) + : start_sec(start_ms / 1000), + start_usec(start_ms % 1000 * 1000), + source(s), + port(p), + padding(0) { +} + +void RtpDumpFileHeader::WriteToByteBuffer(talk_base::ByteBuffer* buf) { + buf->WriteUInt32(start_sec); + buf->WriteUInt32(start_usec); + buf->WriteUInt32(source); + buf->WriteUInt16(port); + buf->WriteUInt16(padding); +} + +static const uint32 kDefaultTimeIncrease = 30; + +bool RtpDumpPacket::IsValidRtpPacket() const { + return original_data_len >= data.size() && + data.size() >= kMinRtpPacketLen; +} + +bool RtpDumpPacket::IsValidRtcpPacket() const { + return original_data_len == 0 && + data.size() >= kMinRtcpPacketLen; +} + +bool RtpDumpPacket::GetRtpPayloadType(int* pt) const { + return IsValidRtpPacket() && + cricket::GetRtpPayloadType(&data[0], data.size(), pt); +} + +bool RtpDumpPacket::GetRtpSeqNum(int* seq_num) const { + return IsValidRtpPacket() && + cricket::GetRtpSeqNum(&data[0], data.size(), seq_num); +} + +bool RtpDumpPacket::GetRtpTimestamp(uint32* ts) const { + return IsValidRtpPacket() && + cricket::GetRtpTimestamp(&data[0], data.size(), ts); +} + +bool RtpDumpPacket::GetRtpSsrc(uint32* ssrc) const { + return IsValidRtpPacket() && + cricket::GetRtpSsrc(&data[0], data.size(), ssrc); +} + +bool RtpDumpPacket::GetRtpHeaderLen(size_t* len) const { + return IsValidRtpPacket() && + cricket::GetRtpHeaderLen(&data[0], data.size(), len); +} + +bool RtpDumpPacket::GetRtcpType(int* type) const { + return IsValidRtcpPacket() && + cricket::GetRtcpType(&data[0], data.size(), type); +} + +/////////////////////////////////////////////////////////////////////////// +// Implementation of RtpDumpReader. +/////////////////////////////////////////////////////////////////////////// + +void RtpDumpReader::SetSsrc(uint32 ssrc) { + ssrc_override_ = ssrc; +} + +talk_base::StreamResult RtpDumpReader::ReadPacket(RtpDumpPacket* packet) { + if (!packet) return talk_base::SR_ERROR; + + talk_base::StreamResult res = talk_base::SR_SUCCESS; + // Read the file header if it has not been read yet. + if (!file_header_read_) { + res = ReadFileHeader(); + if (res != talk_base::SR_SUCCESS) { + return res; + } + file_header_read_ = true; + } + + // Read the RTP dump packet header. + char header[RtpDumpPacket::kHeaderLength]; + res = stream_->ReadAll(header, sizeof(header), NULL, NULL); + if (res != talk_base::SR_SUCCESS) { + return res; + } + talk_base::ByteBuffer buf(header, sizeof(header)); + uint16 dump_packet_len; + uint16 data_len; + // Read the full length of the rtpdump packet, including the rtpdump header. + buf.ReadUInt16(&dump_packet_len); + packet->data.resize(dump_packet_len - sizeof(header)); + // Read the size of the original packet, which may be larger than the size in + // the rtpdump file, in the event that only part of the packet (perhaps just + // the header) was recorded. Note that this field is set to zero for RTCP + // packets, which have their own internal length field. + buf.ReadUInt16(&data_len); + packet->original_data_len = data_len; + // Read the elapsed time for this packet (different than RTP timestamp). + buf.ReadUInt32(&packet->elapsed_time); + + // Read the actual RTP or RTCP packet. + res = stream_->ReadAll(&packet->data[0], packet->data.size(), NULL, NULL); + + // If the packet is RTP and we have specified a ssrc, replace the RTP ssrc + // with the specified ssrc. + if (res == talk_base::SR_SUCCESS && + packet->IsValidRtpPacket() && + ssrc_override_ != 0) { + talk_base::SetBE32(&packet->data[kRtpSsrcOffset], ssrc_override_); + } + + return res; +} + +talk_base::StreamResult RtpDumpReader::ReadFileHeader() { + // Read the first line. + std::string first_line; + talk_base::StreamResult res = stream_->ReadLine(&first_line); + if (res != talk_base::SR_SUCCESS) { + return res; + } + if (!CheckFirstLine(first_line)) { + return talk_base::SR_ERROR; + } + + // Read the 16 byte file header. + char header[RtpDumpFileHeader::kHeaderLength]; + res = stream_->ReadAll(header, sizeof(header), NULL, NULL); + if (res == talk_base::SR_SUCCESS) { + talk_base::ByteBuffer buf(header, sizeof(header)); + uint32 start_sec; + uint32 start_usec; + buf.ReadUInt32(&start_sec); + buf.ReadUInt32(&start_usec); + start_time_ms_ = start_sec * 1000 + start_usec / 1000; + // Increase the length by 1 since first_line does not contain the ending \n. + first_line_and_file_header_len_ = first_line.size() + 1 + sizeof(header); + } + return res; +} + +bool RtpDumpReader::CheckFirstLine(const std::string& first_line) { + // The first line is like "#!rtpplay1.0 address/port" + bool matched = (0 == first_line.find("#!rtpplay1.0 ")); + + // The address could be IP or hostname. We do not check it here. Instead, we + // check the port at the end. + size_t pos = first_line.find('/'); + matched &= (pos != std::string::npos && pos < first_line.size() - 1); + for (++pos; pos < first_line.size() && matched; ++pos) { + matched &= (0 != isdigit(first_line[pos])); + } + + return matched; +} + +/////////////////////////////////////////////////////////////////////////// +// Implementation of RtpDumpLoopReader. +/////////////////////////////////////////////////////////////////////////// +RtpDumpLoopReader::RtpDumpLoopReader(talk_base::StreamInterface* stream) + : RtpDumpReader(stream), + loop_count_(0), + elapsed_time_increases_(0), + rtp_seq_num_increase_(0), + rtp_timestamp_increase_(0), + packet_count_(0), + frame_count_(0), + first_elapsed_time_(0), + first_rtp_seq_num_(0), + first_rtp_timestamp_(0), + prev_elapsed_time_(0), + prev_rtp_seq_num_(0), + prev_rtp_timestamp_(0) { +} + +talk_base::StreamResult RtpDumpLoopReader::ReadPacket(RtpDumpPacket* packet) { + if (!packet) return talk_base::SR_ERROR; + + talk_base::StreamResult res = RtpDumpReader::ReadPacket(packet); + if (talk_base::SR_SUCCESS == res) { + if (0 == loop_count_) { + // During the first loop, we update the statistics of the input stream. + UpdateStreamStatistics(*packet); + } + } else if (talk_base::SR_EOS == res) { + if (0 == loop_count_) { + // At the end of the first loop, calculate elapsed_time_increases_, + // rtp_seq_num_increase_, and rtp_timestamp_increase_, which will be + // used during the second and later loops. + CalculateIncreases(); + } + + // Rewind the input stream to the first dump packet and read again. + ++loop_count_; + if (RewindToFirstDumpPacket()) { + res = RtpDumpReader::ReadPacket(packet); + } + } + + if (talk_base::SR_SUCCESS == res && loop_count_ > 0) { + // During the second and later loops, we update the elapsed time of the dump + // packet. If the dumped packet is a RTP packet, we also update its RTP + // sequence number and timestamp. + UpdateDumpPacket(packet); + } + + return res; +} + +void RtpDumpLoopReader::UpdateStreamStatistics(const RtpDumpPacket& packet) { + // Get the RTP sequence number and timestamp of the dump packet. + int rtp_seq_num = 0; + packet.GetRtpSeqNum(&rtp_seq_num); + uint32 rtp_timestamp = 0; + packet.GetRtpTimestamp(&rtp_timestamp); + + // Set the timestamps and sequence number for the first dump packet. + if (0 == packet_count_++) { + first_elapsed_time_ = packet.elapsed_time; + first_rtp_seq_num_ = rtp_seq_num; + first_rtp_timestamp_ = rtp_timestamp; + // The first packet belongs to a new payload frame. + ++frame_count_; + } else if (rtp_timestamp != prev_rtp_timestamp_) { + // The current and previous packets belong to different payload frames. + ++frame_count_; + } + + prev_elapsed_time_ = packet.elapsed_time; + prev_rtp_timestamp_ = rtp_timestamp; + prev_rtp_seq_num_ = rtp_seq_num; +} + +void RtpDumpLoopReader::CalculateIncreases() { + // At this time, prev_elapsed_time_, prev_rtp_seq_num_, and + // prev_rtp_timestamp_ are values of the last dump packet in the input stream. + rtp_seq_num_increase_ = prev_rtp_seq_num_ - first_rtp_seq_num_ + 1; + // If we have only one packet or frame, we use the default timestamp + // increase. Otherwise, we use the difference between the first and the last + // packets or frames. + elapsed_time_increases_ = packet_count_ <= 1 ? kDefaultTimeIncrease : + (prev_elapsed_time_ - first_elapsed_time_) * packet_count_ / + (packet_count_ - 1); + rtp_timestamp_increase_ = frame_count_ <= 1 ? kDefaultTimeIncrease : + (prev_rtp_timestamp_ - first_rtp_timestamp_) * frame_count_ / + (frame_count_ - 1); +} + +void RtpDumpLoopReader::UpdateDumpPacket(RtpDumpPacket* packet) { + // Increase the elapsed time of the dump packet. + packet->elapsed_time += loop_count_ * elapsed_time_increases_; + + if (packet->IsValidRtpPacket()) { + // Get the old RTP sequence number and timestamp. + int sequence = 0; + packet->GetRtpSeqNum(&sequence); + uint32 timestamp = 0; + packet->GetRtpTimestamp(×tamp); + // Increase the RTP sequence number and timestamp. + sequence += loop_count_ * rtp_seq_num_increase_; + timestamp += loop_count_ * rtp_timestamp_increase_; + // Write the updated sequence number and timestamp back to the RTP packet. + talk_base::ByteBuffer buffer; + buffer.WriteUInt16(sequence); + buffer.WriteUInt32(timestamp); + memcpy(&packet->data[2], buffer.Data(), buffer.Length()); + } +} + +/////////////////////////////////////////////////////////////////////////// +// Implementation of RtpDumpWriter. +/////////////////////////////////////////////////////////////////////////// + +RtpDumpWriter::RtpDumpWriter(talk_base::StreamInterface* stream) + : stream_(stream), + packet_filter_(PF_ALL), + file_header_written_(false), + start_time_ms_(talk_base::Time()), + warn_slow_writes_delay_(kWarnSlowWritesDelayMs) { +} + +void RtpDumpWriter::set_packet_filter(int filter) { + packet_filter_ = filter; + LOG(LS_INFO) << "RtpDumpWriter set_packet_filter to " << packet_filter_; +} + +uint32 RtpDumpWriter::GetElapsedTime() const { + return talk_base::TimeSince(start_time_ms_); +} + +talk_base::StreamResult RtpDumpWriter::WriteFileHeader() { + talk_base::StreamResult res = WriteToStream( + RtpDumpFileHeader::kFirstLine, + strlen(RtpDumpFileHeader::kFirstLine)); + if (res != talk_base::SR_SUCCESS) { + return res; + } + + talk_base::ByteBuffer buf; + RtpDumpFileHeader file_header(talk_base::Time(), 0, 0); + file_header.WriteToByteBuffer(&buf); + return WriteToStream(buf.Data(), buf.Length()); +} + +talk_base::StreamResult RtpDumpWriter::WritePacket( + const void* data, size_t data_len, uint32 elapsed, bool rtcp) { + if (!stream_ || !data || 0 == data_len) return talk_base::SR_ERROR; + + talk_base::StreamResult res = talk_base::SR_SUCCESS; + // Write the file header if it has not been written yet. + if (!file_header_written_) { + res = WriteFileHeader(); + if (res != talk_base::SR_SUCCESS) { + return res; + } + file_header_written_ = true; + } + + // Figure out what to write. + size_t write_len = FilterPacket(data, data_len, rtcp); + if (write_len == 0) { + return talk_base::SR_SUCCESS; + } + + // Write the dump packet header. + talk_base::ByteBuffer buf; + buf.WriteUInt16(static_cast( + RtpDumpPacket::kHeaderLength + write_len)); + buf.WriteUInt16(static_cast(rtcp ? 0 : data_len)); + buf.WriteUInt32(elapsed); + res = WriteToStream(buf.Data(), buf.Length()); + if (res != talk_base::SR_SUCCESS) { + return res; + } + + // Write the header or full packet as indicated by write_len. + return WriteToStream(data, write_len); +} + +size_t RtpDumpWriter::FilterPacket(const void* data, size_t data_len, + bool rtcp) { + size_t filtered_len = 0; + if (!rtcp) { + if ((packet_filter_ & PF_RTPPACKET) == PF_RTPPACKET) { + // RTP header + payload + filtered_len = data_len; + } else if ((packet_filter_ & PF_RTPHEADER) == PF_RTPHEADER) { + // RTP header only + size_t header_len; + if (GetRtpHeaderLen(data, data_len, &header_len)) { + filtered_len = header_len; + } + } + } else { + if ((packet_filter_ & PF_RTCPPACKET) == PF_RTCPPACKET) { + // RTCP header + payload + filtered_len = data_len; + } + } + + return filtered_len; +} + +talk_base::StreamResult RtpDumpWriter::WriteToStream( + const void* data, size_t data_len) { + uint32 before = talk_base::Time(); + talk_base::StreamResult result = + stream_->WriteAll(data, data_len, NULL, NULL); + uint32 delay = talk_base::TimeSince(before); + if (delay >= warn_slow_writes_delay_) { + LOG(LS_WARNING) << "Slow RtpDump: took " << delay << "ms to write " + << data_len << " bytes."; + } + return result; +} + +} // namespace cricket diff --git a/talk/media/base/rtpdump.h b/talk/media/base/rtpdump.h new file mode 100644 index 000000000..9d7b679d5 --- /dev/null +++ b/talk/media/base/rtpdump.h @@ -0,0 +1,232 @@ +/* + * libjingle + * Copyright 2010 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. + */ + +#ifndef TALK_MEDIA_BASE_RTPDUMP_H_ +#define TALK_MEDIA_BASE_RTPDUMP_H_ + +#include +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/bytebuffer.h" +#include "talk/base/stream.h" + +namespace cricket { + +// We use the RTP dump file format compatible to the format used by rtptools +// (http://www.cs.columbia.edu/irt/software/rtptools/) and Wireshark +// (http://wiki.wireshark.org/rtpdump). In particular, the file starts with the +// first line "#!rtpplay1.0 address/port\n", followed by a 16 byte file header. +// For each packet, the file contains a 8 byte dump packet header, followed by +// the actual RTP or RTCP packet. + +enum RtpDumpPacketFilter { + PF_NONE = 0x0, + PF_RTPHEADER = 0x1, + PF_RTPPACKET = 0x3, // includes header + // PF_RTCPHEADER = 0x4, // TODO(juberti) + PF_RTCPPACKET = 0xC, // includes header + PF_ALL = 0xF +}; + +struct RtpDumpFileHeader { + RtpDumpFileHeader(uint32 start_ms, uint32 s, uint16 p); + void WriteToByteBuffer(talk_base::ByteBuffer* buf); + + static const char kFirstLine[]; + static const size_t kHeaderLength = 16; + uint32 start_sec; // start of recording, the seconds part. + uint32 start_usec; // start of recording, the microseconds part. + uint32 source; // network source (multicast address). + uint16 port; // UDP port. + uint16 padding; // 2 bytes padding. +}; + +struct RtpDumpPacket { + RtpDumpPacket() {} + + RtpDumpPacket(const void* d, size_t s, uint32 elapsed, bool rtcp) + : elapsed_time(elapsed), + original_data_len((rtcp) ? 0 : s) { + data.resize(s); + memcpy(&data[0], d, s); + } + + // In the rtpdump file format, RTCP packets have their data len set to zero, + // since RTCP has an internal length field. + bool is_rtcp() const { return original_data_len == 0; } + bool IsValidRtpPacket() const; + bool IsValidRtcpPacket() const; + // Get the payload type, sequence number, timestampe, and SSRC of the RTP + // packet. Return true and set the output parameter if successful. + bool GetRtpPayloadType(int* pt) const; + bool GetRtpSeqNum(int* seq_num) const; + bool GetRtpTimestamp(uint32* ts) const; + bool GetRtpSsrc(uint32* ssrc) const; + bool GetRtpHeaderLen(size_t* len) const; + // Get the type of the RTCP packet. Return true and set the output parameter + // if successful. + bool GetRtcpType(int* type) const; + + static const size_t kHeaderLength = 8; + uint32 elapsed_time; // Milliseconds since the start of recording. + std::vector data; // The actual RTP or RTCP packet. + size_t original_data_len; // The original length of the packet; may be + // greater than data.size() if only part of the + // packet was recorded. +}; + +class RtpDumpReader { + public: + explicit RtpDumpReader(talk_base::StreamInterface* stream) + : stream_(stream), + file_header_read_(false), + first_line_and_file_header_len_(0), + start_time_ms_(0), + ssrc_override_(0) { + } + virtual ~RtpDumpReader() {} + + // Use the specified ssrc, rather than the ssrc from dump, for RTP packets. + void SetSsrc(uint32 ssrc); + virtual talk_base::StreamResult ReadPacket(RtpDumpPacket* packet); + + protected: + talk_base::StreamResult ReadFileHeader(); + bool RewindToFirstDumpPacket() { + return stream_->SetPosition(first_line_and_file_header_len_); + } + + private: + // Check if its matches "#!rtpplay1.0 address/port\n". + bool CheckFirstLine(const std::string& first_line); + + talk_base::StreamInterface* stream_; + bool file_header_read_; + size_t first_line_and_file_header_len_; + uint32 start_time_ms_; + uint32 ssrc_override_; + + DISALLOW_COPY_AND_ASSIGN(RtpDumpReader); +}; + +// RtpDumpLoopReader reads RTP dump packets from the input stream and rewinds +// the stream when it ends. RtpDumpLoopReader maintains the elapsed time, the +// RTP sequence number and the RTP timestamp properly. RtpDumpLoopReader can +// handle both RTP dump and RTCP dump. We assume that the dump does not mix +// RTP packets and RTCP packets. +class RtpDumpLoopReader : public RtpDumpReader { + public: + explicit RtpDumpLoopReader(talk_base::StreamInterface* stream); + virtual talk_base::StreamResult ReadPacket(RtpDumpPacket* packet); + + private: + // During the first loop, update the statistics, including packet count, frame + // count, timestamps, and sequence number, of the input stream. + void UpdateStreamStatistics(const RtpDumpPacket& packet); + + // At the end of first loop, calculate elapsed_time_increases_, + // rtp_seq_num_increase_, and rtp_timestamp_increase_. + void CalculateIncreases(); + + // During the second and later loops, update the elapsed time of the dump + // packet. If the dumped packet is a RTP packet, update its RTP sequence + // number and timestamp as well. + void UpdateDumpPacket(RtpDumpPacket* packet); + + int loop_count_; + // How much to increase the elapsed time, RTP sequence number, RTP timestampe + // for each loop. They are calcualted with the variables below during the + // first loop. + uint32 elapsed_time_increases_; + int rtp_seq_num_increase_; + uint32 rtp_timestamp_increase_; + // How many RTP packets and how many payload frames in the input stream. RTP + // packets belong to the same frame have the same RTP timestamp, different + // dump timestamp, and different RTP sequence number. + uint32 packet_count_; + uint32 frame_count_; + // The elapsed time, RTP sequence number, and RTP timestamp of the first and + // the previous dump packets in the input stream. + uint32 first_elapsed_time_; + int first_rtp_seq_num_; + uint32 first_rtp_timestamp_; + uint32 prev_elapsed_time_; + int prev_rtp_seq_num_; + uint32 prev_rtp_timestamp_; + + DISALLOW_COPY_AND_ASSIGN(RtpDumpLoopReader); +}; + +class RtpDumpWriter { + public: + explicit RtpDumpWriter(talk_base::StreamInterface* stream); + + // Filter to control what packets we actually record. + void set_packet_filter(int filter); + // Write a RTP or RTCP packet. The parameters data points to the packet and + // data_len is its length. + talk_base::StreamResult WriteRtpPacket(const void* data, size_t data_len) { + return WritePacket(data, data_len, GetElapsedTime(), false); + } + talk_base::StreamResult WriteRtcpPacket(const void* data, size_t data_len) { + return WritePacket(data, data_len, GetElapsedTime(), true); + } + talk_base::StreamResult WritePacket(const RtpDumpPacket& packet) { + return WritePacket(&packet.data[0], packet.data.size(), packet.elapsed_time, + packet.is_rtcp()); + } + uint32 GetElapsedTime() const; + + bool GetDumpSize(size_t* size) { + // Note that we use GetPosition(), rather than GetSize(), to avoid flush the + // stream per write. + return stream_ && size && stream_->GetPosition(size); + } + + protected: + talk_base::StreamResult WriteFileHeader(); + + private: + talk_base::StreamResult WritePacket(const void* data, size_t data_len, + uint32 elapsed, bool rtcp); + size_t FilterPacket(const void* data, size_t data_len, bool rtcp); + talk_base::StreamResult WriteToStream(const void* data, size_t data_len); + + talk_base::StreamInterface* stream_; + int packet_filter_; + bool file_header_written_; + uint32 start_time_ms_; // Time when the record starts. + // If writing to the stream takes longer than this many ms, log a warning. + uint32 warn_slow_writes_delay_; + DISALLOW_COPY_AND_ASSIGN(RtpDumpWriter); +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_RTPDUMP_H_ diff --git a/talk/media/base/rtpdump_unittest.cc b/talk/media/base/rtpdump_unittest.cc new file mode 100644 index 000000000..c32718953 --- /dev/null +++ b/talk/media/base/rtpdump_unittest.cc @@ -0,0 +1,297 @@ +/* + * libjingle + * Copyright 2004 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 + +#include "talk/base/bytebuffer.h" +#include "talk/base/gunit.h" +#include "talk/base/thread.h" +#include "talk/media/base/rtpdump.h" +#include "talk/media/base/rtputils.h" +#include "talk/media/base/testutils.h" + +namespace cricket { + +static const uint32 kTestSsrc = 1; + +// Test that we read the correct header fields from the RTP/RTCP packet. +TEST(RtpDumpTest, ReadRtpDumpPacket) { + talk_base::ByteBuffer rtp_buf; + RtpTestUtility::kTestRawRtpPackets[0].WriteToByteBuffer(kTestSsrc, &rtp_buf); + RtpDumpPacket rtp_packet(rtp_buf.Data(), rtp_buf.Length(), 0, false); + + int type; + int seq_num; + uint32 ts; + uint32 ssrc; + EXPECT_FALSE(rtp_packet.is_rtcp()); + EXPECT_TRUE(rtp_packet.IsValidRtpPacket()); + EXPECT_FALSE(rtp_packet.IsValidRtcpPacket()); + EXPECT_TRUE(rtp_packet.GetRtpPayloadType(&type)); + EXPECT_EQ(0, type); + EXPECT_TRUE(rtp_packet.GetRtpSeqNum(&seq_num)); + EXPECT_EQ(0, seq_num); + EXPECT_TRUE(rtp_packet.GetRtpTimestamp(&ts)); + EXPECT_EQ(0U, ts); + EXPECT_TRUE(rtp_packet.GetRtpSsrc(&ssrc)); + EXPECT_EQ(kTestSsrc, ssrc); + EXPECT_FALSE(rtp_packet.GetRtcpType(&type)); + + talk_base::ByteBuffer rtcp_buf; + RtpTestUtility::kTestRawRtcpPackets[0].WriteToByteBuffer(&rtcp_buf); + RtpDumpPacket rtcp_packet(rtcp_buf.Data(), rtcp_buf.Length(), 0, true); + + EXPECT_TRUE(rtcp_packet.is_rtcp()); + EXPECT_FALSE(rtcp_packet.IsValidRtpPacket()); + EXPECT_TRUE(rtcp_packet.IsValidRtcpPacket()); + EXPECT_TRUE(rtcp_packet.GetRtcpType(&type)); + EXPECT_EQ(0, type); +} + +// Test that we read only the RTP dump file. +TEST(RtpDumpTest, ReadRtpDumpFile) { + RtpDumpPacket packet; + talk_base::MemoryStream stream; + RtpDumpWriter writer(&stream); + talk_base::scoped_ptr reader; + + // Write a RTP packet to the stream, which is a valid RTP dump. Next, we will + // change the first line to make the RTP dump valid or invalid. + ASSERT_TRUE(RtpTestUtility::WriteTestPackets(1, false, kTestSsrc, &writer)); + stream.Rewind(); + reader.reset(new RtpDumpReader(&stream)); + EXPECT_EQ(talk_base::SR_SUCCESS, reader->ReadPacket(&packet)); + + // The first line is correct. + stream.Rewind(); + const char new_line[] = "#!rtpplay1.0 1.1.1.1/1\n"; + EXPECT_EQ(talk_base::SR_SUCCESS, + stream.WriteAll(new_line, strlen(new_line), NULL, NULL)); + stream.Rewind(); + reader.reset(new RtpDumpReader(&stream)); + EXPECT_EQ(talk_base::SR_SUCCESS, reader->ReadPacket(&packet)); + + // The first line is not correct: not started with #!rtpplay1.0. + stream.Rewind(); + const char new_line2[] = "#!rtpplaz1.0 0.0.0.0/0\n"; + EXPECT_EQ(talk_base::SR_SUCCESS, + stream.WriteAll(new_line2, strlen(new_line2), NULL, NULL)); + stream.Rewind(); + reader.reset(new RtpDumpReader(&stream)); + EXPECT_EQ(talk_base::SR_ERROR, reader->ReadPacket(&packet)); + + // The first line is not correct: no port. + stream.Rewind(); + const char new_line3[] = "#!rtpplay1.0 0.0.0.0//\n"; + EXPECT_EQ(talk_base::SR_SUCCESS, + stream.WriteAll(new_line3, strlen(new_line3), NULL, NULL)); + stream.Rewind(); + reader.reset(new RtpDumpReader(&stream)); + EXPECT_EQ(talk_base::SR_ERROR, reader->ReadPacket(&packet)); +} + +// Test that we read the same RTP packets that rtp dump writes. +TEST(RtpDumpTest, WriteReadSameRtp) { + talk_base::MemoryStream stream; + RtpDumpWriter writer(&stream); + ASSERT_TRUE(RtpTestUtility::WriteTestPackets( + RtpTestUtility::GetTestPacketCount(), false, kTestSsrc, &writer)); + EXPECT_TRUE(RtpTestUtility::VerifyTestPacketsFromStream( + RtpTestUtility::GetTestPacketCount(), &stream, kTestSsrc)); + + // Check stream has only RtpTestUtility::GetTestPacketCount() packets. + RtpDumpPacket packet; + RtpDumpReader reader(&stream); + for (size_t i = 0; i < RtpTestUtility::GetTestPacketCount(); ++i) { + EXPECT_EQ(talk_base::SR_SUCCESS, reader.ReadPacket(&packet)); + uint32 ssrc; + EXPECT_TRUE(GetRtpSsrc(&packet.data[0], packet.data.size(), &ssrc)); + EXPECT_EQ(kTestSsrc, ssrc); + } + // No more packets to read. + EXPECT_EQ(talk_base::SR_EOS, reader.ReadPacket(&packet)); + + // Rewind the stream and read again with a specified ssrc. + stream.Rewind(); + RtpDumpReader reader_w_ssrc(&stream); + const uint32 send_ssrc = kTestSsrc + 1; + reader_w_ssrc.SetSsrc(send_ssrc); + for (size_t i = 0; i < RtpTestUtility::GetTestPacketCount(); ++i) { + EXPECT_EQ(talk_base::SR_SUCCESS, reader_w_ssrc.ReadPacket(&packet)); + EXPECT_FALSE(packet.is_rtcp()); + EXPECT_EQ(packet.original_data_len, packet.data.size()); + uint32 ssrc; + EXPECT_TRUE(GetRtpSsrc(&packet.data[0], packet.data.size(), &ssrc)); + EXPECT_EQ(send_ssrc, ssrc); + } + // No more packets to read. + EXPECT_EQ(talk_base::SR_EOS, reader_w_ssrc.ReadPacket(&packet)); +} + +// Test that we read the same RTCP packets that rtp dump writes. +TEST(RtpDumpTest, WriteReadSameRtcp) { + talk_base::MemoryStream stream; + RtpDumpWriter writer(&stream); + ASSERT_TRUE(RtpTestUtility::WriteTestPackets( + RtpTestUtility::GetTestPacketCount(), true, kTestSsrc, &writer)); + EXPECT_TRUE(RtpTestUtility::VerifyTestPacketsFromStream( + RtpTestUtility::GetTestPacketCount(), &stream, kTestSsrc)); + + // Check stream has only RtpTestUtility::GetTestPacketCount() packets. + RtpDumpPacket packet; + RtpDumpReader reader(&stream); + reader.SetSsrc(kTestSsrc + 1); // Does not affect RTCP packet. + for (size_t i = 0; i < RtpTestUtility::GetTestPacketCount(); ++i) { + EXPECT_EQ(talk_base::SR_SUCCESS, reader.ReadPacket(&packet)); + EXPECT_TRUE(packet.is_rtcp()); + EXPECT_EQ(0U, packet.original_data_len); + } + // No more packets to read. + EXPECT_EQ(talk_base::SR_EOS, reader.ReadPacket(&packet)); +} + +// Test dumping only RTP packet headers. +TEST(RtpDumpTest, WriteReadRtpHeadersOnly) { + talk_base::MemoryStream stream; + RtpDumpWriter writer(&stream); + writer.set_packet_filter(PF_RTPHEADER); + + // Write some RTP and RTCP packets. RTP packets should only have headers; + // RTCP packets should be eaten. + ASSERT_TRUE(RtpTestUtility::WriteTestPackets( + RtpTestUtility::GetTestPacketCount(), false, kTestSsrc, &writer)); + ASSERT_TRUE(RtpTestUtility::WriteTestPackets( + RtpTestUtility::GetTestPacketCount(), true, kTestSsrc, &writer)); + stream.Rewind(); + + // Check that only RTP packet headers are present. + RtpDumpPacket packet; + RtpDumpReader reader(&stream); + for (size_t i = 0; i < RtpTestUtility::GetTestPacketCount(); ++i) { + EXPECT_EQ(talk_base::SR_SUCCESS, reader.ReadPacket(&packet)); + EXPECT_FALSE(packet.is_rtcp()); + size_t len = 0; + packet.GetRtpHeaderLen(&len); + EXPECT_EQ(len, packet.data.size()); + EXPECT_GT(packet.original_data_len, packet.data.size()); + } + // No more packets to read. + EXPECT_EQ(talk_base::SR_EOS, reader.ReadPacket(&packet)); +} + +// Test dumping only RTCP packets. +TEST(RtpDumpTest, WriteReadRtcpOnly) { + talk_base::MemoryStream stream; + RtpDumpWriter writer(&stream); + writer.set_packet_filter(PF_RTCPPACKET); + + // Write some RTP and RTCP packets. RTP packets should be eaten. + ASSERT_TRUE(RtpTestUtility::WriteTestPackets( + RtpTestUtility::GetTestPacketCount(), false, kTestSsrc, &writer)); + ASSERT_TRUE(RtpTestUtility::WriteTestPackets( + RtpTestUtility::GetTestPacketCount(), true, kTestSsrc, &writer)); + stream.Rewind(); + + // Check that only RTCP packets are present. + RtpDumpPacket packet; + RtpDumpReader reader(&stream); + for (size_t i = 0; i < RtpTestUtility::GetTestPacketCount(); ++i) { + EXPECT_EQ(talk_base::SR_SUCCESS, reader.ReadPacket(&packet)); + EXPECT_TRUE(packet.is_rtcp()); + EXPECT_EQ(0U, packet.original_data_len); + } + // No more packets to read. + EXPECT_EQ(talk_base::SR_EOS, reader.ReadPacket(&packet)); +} + +// Test that RtpDumpLoopReader reads RTP packets continously and the elapsed +// time, the sequence number, and timestamp are maintained properly. +TEST(RtpDumpTest, LoopReadRtp) { + talk_base::MemoryStream stream; + RtpDumpWriter writer(&stream); + ASSERT_TRUE(RtpTestUtility::WriteTestPackets( + RtpTestUtility::GetTestPacketCount(), false, kTestSsrc, &writer)); + EXPECT_TRUE(RtpTestUtility::VerifyTestPacketsFromStream( + 3 * RtpTestUtility::GetTestPacketCount(), &stream, kTestSsrc)); +} + +// Test that RtpDumpLoopReader reads RTCP packets continously and the elapsed +// time is maintained properly. +TEST(RtpDumpTest, LoopReadRtcp) { + talk_base::MemoryStream stream; + RtpDumpWriter writer(&stream); + ASSERT_TRUE(RtpTestUtility::WriteTestPackets( + RtpTestUtility::GetTestPacketCount(), true, kTestSsrc, &writer)); + EXPECT_TRUE(RtpTestUtility::VerifyTestPacketsFromStream( + 3 * RtpTestUtility::GetTestPacketCount(), &stream, kTestSsrc)); +} + +// Test that RtpDumpLoopReader reads continously from stream with a single RTP +// packets. +TEST(RtpDumpTest, LoopReadSingleRtp) { + talk_base::MemoryStream stream; + RtpDumpWriter writer(&stream); + ASSERT_TRUE(RtpTestUtility::WriteTestPackets(1, false, kTestSsrc, &writer)); + + // The regular reader can read only one packet. + RtpDumpPacket packet; + stream.Rewind(); + RtpDumpReader reader(&stream); + EXPECT_EQ(talk_base::SR_SUCCESS, reader.ReadPacket(&packet)); + EXPECT_EQ(talk_base::SR_EOS, reader.ReadPacket(&packet)); + + // The loop reader reads three packets from the input stream. + stream.Rewind(); + RtpDumpLoopReader loop_reader(&stream); + EXPECT_EQ(talk_base::SR_SUCCESS, loop_reader.ReadPacket(&packet)); + EXPECT_EQ(talk_base::SR_SUCCESS, loop_reader.ReadPacket(&packet)); + EXPECT_EQ(talk_base::SR_SUCCESS, loop_reader.ReadPacket(&packet)); +} + +// Test that RtpDumpLoopReader reads continously from stream with a single RTCP +// packets. +TEST(RtpDumpTest, LoopReadSingleRtcp) { + talk_base::MemoryStream stream; + RtpDumpWriter writer(&stream); + ASSERT_TRUE(RtpTestUtility::WriteTestPackets(1, true, kTestSsrc, &writer)); + + // The regular reader can read only one packet. + RtpDumpPacket packet; + stream.Rewind(); + RtpDumpReader reader(&stream); + EXPECT_EQ(talk_base::SR_SUCCESS, reader.ReadPacket(&packet)); + EXPECT_EQ(talk_base::SR_EOS, reader.ReadPacket(&packet)); + + // The loop reader reads three packets from the input stream. + stream.Rewind(); + RtpDumpLoopReader loop_reader(&stream); + EXPECT_EQ(talk_base::SR_SUCCESS, loop_reader.ReadPacket(&packet)); + EXPECT_EQ(talk_base::SR_SUCCESS, loop_reader.ReadPacket(&packet)); + EXPECT_EQ(talk_base::SR_SUCCESS, loop_reader.ReadPacket(&packet)); +} + +} // namespace cricket diff --git a/talk/media/base/rtputils.cc b/talk/media/base/rtputils.cc new file mode 100644 index 000000000..5215c3b76 --- /dev/null +++ b/talk/media/base/rtputils.cc @@ -0,0 +1,226 @@ +/* + * libjingle + * Copyright 2011 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/media/base/rtputils.h" + +namespace cricket { + +static const int kRtpVersion = 2; +static const size_t kRtpFlagsOffset = 0; +static const size_t kRtpPayloadTypeOffset = 1; +static const size_t kRtpSeqNumOffset = 2; +static const size_t kRtpTimestampOffset = 4; +static const size_t kRtpSsrcOffset = 8; +static const size_t kRtcpPayloadTypeOffset = 1; + +bool GetUint8(const void* data, size_t offset, int* value) { + if (!data || !value) { + return false; + } + *value = *(static_cast(data) + offset); + return true; +} + +bool GetUint16(const void* data, size_t offset, int* value) { + if (!data || !value) { + return false; + } + *value = static_cast( + talk_base::GetBE16(static_cast(data) + offset)); + return true; +} + +bool GetUint32(const void* data, size_t offset, uint32* value) { + if (!data || !value) { + return false; + } + *value = talk_base::GetBE32(static_cast(data) + offset); + return true; +} + +bool SetUint8(void* data, size_t offset, int value) { + if (!data) { + return false; + } + talk_base::Set8(data, offset, value); + return true; +} + +bool SetUint16(void* data, size_t offset, int value) { + if (!data) { + return false; + } + talk_base::SetBE16(static_cast(data) + offset, value); + return true; +} + +bool SetUint32(void* data, size_t offset, uint32 value) { + if (!data) { + return false; + } + talk_base::SetBE32(static_cast(data) + offset, value); + return true; +} + +bool GetRtpFlags(const void* data, size_t len, int* value) { + if (len < kMinRtpPacketLen) { + return false; + } + return GetUint8(data, kRtpFlagsOffset, value); +} + +bool GetRtpPayloadType(const void* data, size_t len, int* value) { + if (len < kMinRtpPacketLen) { + return false; + } + if (!GetUint8(data, kRtpPayloadTypeOffset, value)) { + return false; + } + *value &= 0x7F; + return true; +} + +bool GetRtpSeqNum(const void* data, size_t len, int* value) { + if (len < kMinRtpPacketLen) { + return false; + } + return GetUint16(data, kRtpSeqNumOffset, value); +} + +bool GetRtpTimestamp(const void* data, size_t len, uint32* value) { + if (len < kMinRtpPacketLen) { + return false; + } + return GetUint32(data, kRtpTimestampOffset, value); +} + +bool GetRtpSsrc(const void* data, size_t len, uint32* value) { + if (len < kMinRtpPacketLen) { + return false; + } + return GetUint32(data, kRtpSsrcOffset, value); +} + +bool GetRtpHeaderLen(const void* data, size_t len, size_t* value) { + if (!data || len < kMinRtpPacketLen || !value) return false; + const uint8* header = static_cast(data); + // Get base header size + length of CSRCs (not counting extension yet). + size_t header_size = kMinRtpPacketLen + (header[0] & 0xF) * sizeof(uint32); + if (len < header_size) return false; + // If there's an extension, read and add in the extension size. + if (header[0] & 0x10) { + if (len < header_size + sizeof(uint32)) return false; + header_size += ((talk_base::GetBE16(header + header_size + 2) + 1) * + sizeof(uint32)); + if (len < header_size) return false; + } + *value = header_size; + return true; +} + +bool GetRtpVersion(const void* data, size_t len, int* version) { + if (len == 0) { + return false; + } + + const uint8 first = static_cast(data)[0]; + *version = static_cast((first >> 6) & 0x3); + return true; +} + +bool GetRtpHeader(const void* data, size_t len, RtpHeader* header) { + return (GetRtpPayloadType(data, len, &(header->payload_type)) && + GetRtpSeqNum(data, len, &(header->seq_num)) && + GetRtpTimestamp(data, len, &(header->timestamp)) && + GetRtpSsrc(data, len, &(header->ssrc))); +} + +bool GetRtcpType(const void* data, size_t len, int* value) { + if (len < kMinRtcpPacketLen) { + return false; + } + return GetUint8(data, kRtcpPayloadTypeOffset, value); +} + +// This method returns SSRC first of RTCP packet, except if packet is SDES. +// TODO(mallinath) - Fully implement RFC 5506. This standard doesn't restrict +// to send non-compound packets only to feedback messages. +bool GetRtcpSsrc(const void* data, size_t len, uint32* value) { + // Packet should be at least of 8 bytes, to get SSRC from a RTCP packet. + if (!data || len < kMinRtcpPacketLen + 4 || !value) return false; + int pl_type; + if (!GetRtcpType(data, len, &pl_type)) return false; + // SDES packet parsing is not supported. + if (pl_type == kRtcpTypeSDES) return false; + *value = talk_base::GetBE32(static_cast(data) + 4); + return true; +} + +bool SetRtpHeaderFlags( + void* data, size_t len, + bool padding, bool extension, int csrc_count) { + if (csrc_count > 0x0F) { + return false; + } + int flags = 0; + flags |= (kRtpVersion << 6); + flags |= ((padding ? 1 : 0) << 5); + flags |= ((extension ? 1 : 0) << 4); + flags |= csrc_count; + return SetUint8(data, kRtpFlagsOffset, flags); +} + +// Assumes marker bit is 0. +bool SetRtpPayloadType(void* data, size_t len, int value) { + if (value >= 0x7F) { + return false; + } + return SetUint8(data, kRtpPayloadTypeOffset, value & 0x7F); +} + +bool SetRtpSeqNum(void* data, size_t len, int value) { + return SetUint16(data, kRtpSeqNumOffset, value); +} + +bool SetRtpTimestamp(void* data, size_t len, uint32 value) { + return SetUint32(data, kRtpTimestampOffset, value); +} + +bool SetRtpSsrc(void* data, size_t len, uint32 value) { + return SetUint32(data, kRtpSsrcOffset, value); +} + +// Assumes version 2, no padding, no extensions, no csrcs. +bool SetRtpHeader(void* data, size_t len, const RtpHeader& header) { + return (SetRtpHeaderFlags(data, len, false, false, 0) && + SetRtpPayloadType(data, len, header.payload_type) && + SetRtpSeqNum(data, len, header.seq_num) && + SetRtpTimestamp(data, len, header.timestamp) && + SetRtpSsrc(data, len, header.ssrc)); +} + +} // namespace cricket diff --git a/talk/media/base/rtputils.h b/talk/media/base/rtputils.h new file mode 100644 index 000000000..6f76866ab --- /dev/null +++ b/talk/media/base/rtputils.h @@ -0,0 +1,79 @@ +/* + * libjingle + * Copyright 2011 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. + */ + +#ifndef TALK_MEDIA_BASE_RTPUTILS_H_ +#define TALK_MEDIA_BASE_RTPUTILS_H_ + +#include "talk/base/byteorder.h" + +namespace cricket { + +const size_t kMinRtpPacketLen = 12; +const size_t kMaxRtpPacketLen = 2048; +const size_t kMinRtcpPacketLen = 4; + +struct RtpHeader { + int payload_type; + int seq_num; + uint32 timestamp; + uint32 ssrc; +}; + +enum RtcpTypes { + kRtcpTypeSR = 200, // Sender report payload type. + kRtcpTypeRR = 201, // Receiver report payload type. + kRtcpTypeSDES = 202, // SDES payload type. + kRtcpTypeBye = 203, // BYE payload type. + kRtcpTypeApp = 204, // APP payload type. + kRtcpTypeRTPFB = 205, // Transport layer Feedback message payload type. + kRtcpTypePSFB = 206, // Payload-specific Feedback message payload type. +}; + +bool GetRtpVersion(const void* data, size_t len, int* version); +bool GetRtpPayloadType(const void* data, size_t len, int* value); +bool GetRtpSeqNum(const void* data, size_t len, int* value); +bool GetRtpTimestamp(const void* data, size_t len, uint32* value); +bool GetRtpSsrc(const void* data, size_t len, uint32* value); +bool GetRtpHeaderLen(const void* data, size_t len, size_t* value); +bool GetRtcpType(const void* data, size_t len, int* value); +bool GetRtcpSsrc(const void* data, size_t len, uint32* value); +bool GetRtpHeader(const void* data, size_t len, RtpHeader* header); + +// Assumes marker bit is 0. +bool SetRtpHeaderFlags( + void* data, size_t len, + bool padding, bool extension, int csrc_count); +bool SetRtpPayloadType(void* data, size_t len, int value); +bool SetRtpSeqNum(void* data, size_t len, int value); +bool SetRtpTimestamp(void* data, size_t len, uint32 value); +bool SetRtpSsrc(void* data, size_t len, uint32 value); +// Assumes version 2, no padding, no extensions, no csrcs. +bool SetRtpHeader(void* data, size_t len, const RtpHeader& header); + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_RTPUTILS_H_ diff --git a/talk/media/base/rtputils_unittest.cc b/talk/media/base/rtputils_unittest.cc new file mode 100644 index 000000000..d3ea5217b --- /dev/null +++ b/talk/media/base/rtputils_unittest.cc @@ -0,0 +1,194 @@ +/* + * libjingle + * Copyright 2004 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/base/gunit.h" +#include "talk/media/base/fakertp.h" +#include "talk/media/base/rtputils.h" + +namespace cricket { + +static const unsigned char kRtpPacketWithMarker[] = { + 0x80, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 +}; +// 3 CSRCs (0x01020304, 0x12345678, 0xAABBCCDD) +// Extension (0xBEDE, 0x1122334455667788) +static const unsigned char kRtpPacketWithMarkerAndCsrcAndExtension[] = { + 0x93, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x01, 0x02, 0x03, 0x04, 0x12, 0x34, 0x56, 0x78, 0xAA, 0xBB, 0xCC, 0xDD, + 0xBE, 0xDE, 0x00, 0x02, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 +}; +static const unsigned char kInvalidPacket[] = { 0x80, 0x00 }; +static const unsigned char kInvalidPacketWithCsrc[] = { + 0x83, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x01, 0x02, 0x03, 0x04, 0x12, 0x34, 0x56, 0x78, 0xAA, 0xBB, 0xCC +}; +static const unsigned char kInvalidPacketWithCsrcAndExtension1[] = { + 0x93, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x01, 0x02, 0x03, 0x04, 0x12, 0x34, 0x56, 0x78, 0xAA, 0xBB, 0xCC, 0xDD, + 0xBE, 0xDE, 0x00 +}; +static const unsigned char kInvalidPacketWithCsrcAndExtension2[] = { + 0x93, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x01, 0x02, 0x03, 0x04, 0x12, 0x34, 0x56, 0x78, 0xAA, 0xBB, 0xCC, 0xDD, + 0xBE, 0xDE, 0x00, 0x02, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 +}; + +// PT = 206, FMT = 1, Sender SSRC = 0x1111, Media SSRC = 0x1111 +// No FCI information is needed for PLI. +static const unsigned char kNonCompoundRtcpPliFeedbackPacket[] = { + 0x81, 0xCE, 0x00, 0x0C, 0x00, 0x00, 0x11, 0x11, 0x00, 0x00, 0x11, 0x11 +}; + +// Packet has only mandatory fixed RTCP header +// PT = 204, SSRC = 0x1111 +static const unsigned char kNonCompoundRtcpAppPacket[] = { + 0x81, 0xCC, 0x00, 0x0C, 0x00, 0x00, 0x11, 0x11 +}; + +// PT = 202, Source count = 0 +static const unsigned char kNonCompoundRtcpSDESPacket[] = { + 0x80, 0xCA, 0x00, 0x00 +}; + +TEST(RtpUtilsTest, GetRtp) { + int ver; + EXPECT_TRUE(GetRtpVersion(kPcmuFrame, sizeof(kPcmuFrame), &ver)); + EXPECT_EQ(2, ver); + + int pt; + EXPECT_TRUE(GetRtpPayloadType(kPcmuFrame, sizeof(kPcmuFrame), &pt)); + EXPECT_EQ(0, pt); + EXPECT_TRUE(GetRtpPayloadType(kRtpPacketWithMarker, + sizeof(kRtpPacketWithMarker), &pt)); + EXPECT_EQ(0, pt); + + int seq_num; + EXPECT_TRUE(GetRtpSeqNum(kPcmuFrame, sizeof(kPcmuFrame), &seq_num)); + EXPECT_EQ(1, seq_num); + + uint32 ts; + EXPECT_TRUE(GetRtpTimestamp(kPcmuFrame, sizeof(kPcmuFrame), &ts)); + EXPECT_EQ(0u, ts); + + uint32 ssrc; + EXPECT_TRUE(GetRtpSsrc(kPcmuFrame, sizeof(kPcmuFrame), &ssrc)); + EXPECT_EQ(1u, ssrc); + + RtpHeader header; + EXPECT_TRUE(GetRtpHeader(kPcmuFrame, sizeof(kPcmuFrame), &header)); + EXPECT_EQ(0, header.payload_type); + EXPECT_EQ(1, header.seq_num); + EXPECT_EQ(0u, header.timestamp); + EXPECT_EQ(1u, header.ssrc); + + EXPECT_FALSE(GetRtpPayloadType(kInvalidPacket, sizeof(kInvalidPacket), &pt)); + EXPECT_FALSE(GetRtpSeqNum(kInvalidPacket, sizeof(kInvalidPacket), &seq_num)); + EXPECT_FALSE(GetRtpTimestamp(kInvalidPacket, sizeof(kInvalidPacket), &ts)); + EXPECT_FALSE(GetRtpSsrc(kInvalidPacket, sizeof(kInvalidPacket), &ssrc)); +} + +TEST(RtpUtilsTest, SetRtp) { + unsigned char packet[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + EXPECT_TRUE(SetRtpHeaderFlags(packet, sizeof(packet), false, false, 0)); + EXPECT_TRUE(SetRtpPayloadType(packet, sizeof(packet), 9u)); + EXPECT_TRUE(SetRtpSeqNum(packet, sizeof(packet), 1111u)); + EXPECT_TRUE(SetRtpTimestamp(packet, sizeof(packet), 2222u)); + EXPECT_TRUE(SetRtpSsrc(packet, sizeof(packet), 3333u)); + + // Bits: 10 0 0 0000 + EXPECT_EQ(128u, packet[0]); + size_t len; + EXPECT_TRUE(GetRtpHeaderLen(packet, sizeof(packet), &len)); + EXPECT_EQ(12U, len); + RtpHeader header; + EXPECT_TRUE(GetRtpHeader(packet, sizeof(packet), &header)); + EXPECT_EQ(9, header.payload_type); + EXPECT_EQ(1111, header.seq_num); + EXPECT_EQ(2222u, header.timestamp); + EXPECT_EQ(3333u, header.ssrc); + + unsigned char packet2[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + EXPECT_TRUE(SetRtpHeader(packet2, sizeof(packet2), header)); + + // Bits: 10 0 0 0000 + EXPECT_EQ(128u, packet2[0]); + EXPECT_TRUE(GetRtpHeaderLen(packet2, sizeof(packet2), &len)); + EXPECT_EQ(12U, len); + EXPECT_TRUE(GetRtpHeader(packet2, sizeof(packet2), &header)); + EXPECT_EQ(9, header.payload_type); + EXPECT_EQ(1111, header.seq_num); + EXPECT_EQ(2222u, header.timestamp); + EXPECT_EQ(3333u, header.ssrc); +} + +TEST(RtpUtilsTest, GetRtpHeaderLen) { + size_t len; + EXPECT_TRUE(GetRtpHeaderLen(kPcmuFrame, sizeof(kPcmuFrame), &len)); + EXPECT_EQ(12U, len); + + EXPECT_TRUE(GetRtpHeaderLen(kRtpPacketWithMarkerAndCsrcAndExtension, + sizeof(kRtpPacketWithMarkerAndCsrcAndExtension), + &len)); + EXPECT_EQ(sizeof(kRtpPacketWithMarkerAndCsrcAndExtension), len); + + EXPECT_FALSE(GetRtpHeaderLen(kInvalidPacket, sizeof(kInvalidPacket), &len)); + EXPECT_FALSE(GetRtpHeaderLen(kInvalidPacketWithCsrc, + sizeof(kInvalidPacketWithCsrc), &len)); + EXPECT_FALSE(GetRtpHeaderLen(kInvalidPacketWithCsrcAndExtension1, + sizeof(kInvalidPacketWithCsrcAndExtension1), + &len)); + EXPECT_FALSE(GetRtpHeaderLen(kInvalidPacketWithCsrcAndExtension2, + sizeof(kInvalidPacketWithCsrcAndExtension2), + &len)); +} + +TEST(RtpUtilsTest, GetRtcp) { + int pt; + EXPECT_TRUE(GetRtcpType(kRtcpReport, sizeof(kRtcpReport), &pt)); + EXPECT_EQ(0xc9, pt); + + EXPECT_FALSE(GetRtcpType(kInvalidPacket, sizeof(kInvalidPacket), &pt)); + + uint32 ssrc; + EXPECT_TRUE(GetRtcpSsrc(kNonCompoundRtcpPliFeedbackPacket, + sizeof(kNonCompoundRtcpPliFeedbackPacket), + &ssrc)); + EXPECT_TRUE(GetRtcpSsrc(kNonCompoundRtcpAppPacket, + sizeof(kNonCompoundRtcpAppPacket), + &ssrc)); + EXPECT_FALSE(GetRtcpSsrc(kNonCompoundRtcpSDESPacket, + sizeof(kNonCompoundRtcpSDESPacket), + &ssrc)); +} + +} // namespace cricket diff --git a/talk/media/base/screencastid.h b/talk/media/base/screencastid.h new file mode 100644 index 000000000..d1f84f335 --- /dev/null +++ b/talk/media/base/screencastid.h @@ -0,0 +1,88 @@ +// Copyright 2012 Google Inc. +// Author: thorcarpenter@google.com (Thor Carpenter) +// +// Defines variant class ScreencastId that combines WindowId and DesktopId. + +#ifndef TALK_MEDIA_BASE_SCREENCASTID_H_ +#define TALK_MEDIA_BASE_SCREENCASTID_H_ + +#include +#include + +#include "talk/base/window.h" +#include "talk/base/windowpicker.h" + +namespace cricket { + +class ScreencastId; +typedef std::vector ScreencastIdList; + +// Used for identifying a window or desktop to be screencast. +class ScreencastId { + public: + enum Type { INVALID, WINDOW, DESKTOP }; + + // Default constructor indicates invalid ScreencastId. + ScreencastId() : type_(INVALID) {} + explicit ScreencastId(const talk_base::WindowId& id) + : type_(WINDOW), window_(id) { + } + explicit ScreencastId(const talk_base::DesktopId& id) + : type_(DESKTOP), desktop_(id) { + } + + Type type() const { return type_; } + const talk_base::WindowId& window() const { return window_; } + const talk_base::DesktopId& desktop() const { return desktop_; } + + // Title is an optional parameter. + const std::string& title() const { return title_; } + void set_title(const std::string& desc) { title_ = desc; } + + bool IsValid() const { + if (type_ == INVALID) { + return false; + } else if (type_ == WINDOW) { + return window_.IsValid(); + } else { + return desktop_.IsValid(); + } + } + bool IsWindow() const { return type_ == WINDOW; } + bool IsDesktop() const { return type_ == DESKTOP; } + bool EqualsId(const ScreencastId& other) const { + if (type_ != other.type_) { + return false; + } + if (type_ == INVALID) { + return true; + } else if (type_ == WINDOW) { + return window_.Equals(other.window()); + } + return desktop_.Equals(other.desktop()); + } + + // T is assumed to be WindowDescription or DesktopDescription. + template + static cricket::ScreencastIdList Convert(const std::vector& list) { + ScreencastIdList screencast_list; + screencast_list.reserve(list.size()); + for (typename std::vector::const_iterator it = list.begin(); + it != list.end(); ++it) { + ScreencastId id(it->id()); + id.set_title(it->title()); + screencast_list.push_back(id); + } + return screencast_list; + } + + private: + Type type_; + talk_base::WindowId window_; + talk_base::DesktopId desktop_; + std::string title_; // Optional. +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_SCREENCASTID_H_ diff --git a/talk/media/base/streamparams.cc b/talk/media/base/streamparams.cc new file mode 100644 index 000000000..08eeea7a4 --- /dev/null +++ b/talk/media/base/streamparams.cc @@ -0,0 +1,182 @@ +/* + * libjingle + * Copyright 2011 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/media/base/streamparams.h" + +#include + +namespace cricket { + +const char kFecSsrcGroupSemantics[] = "FEC"; +const char kFidSsrcGroupSemantics[] = "FID"; + +static std::string SsrcsToString(const std::vector& ssrcs) { + std::ostringstream ost; + ost << "ssrcs:["; + for (std::vector::const_iterator it = ssrcs.begin(); + it != ssrcs.end(); ++it) { + if (it != ssrcs.begin()) { + ost << ","; + } + ost << *it; + } + ost << "]"; + return ost.str(); +} + +bool SsrcGroup::has_semantics(const std::string& semantics_in) const { + return (semantics == semantics_in && ssrcs.size() > 0); +} + +std::string SsrcGroup::ToString() const { + std::ostringstream ost; + ost << "{"; + ost << "semantics:" << semantics << ";"; + ost << SsrcsToString(ssrcs); + ost << "}"; + return ost.str(); +} + +std::string StreamParams::ToString() const { + std::ostringstream ost; + ost << "{"; + if (!groupid.empty()) { + ost << "groupid:" << groupid << ";"; + } + if (!id.empty()) { + ost << "id:" << id << ";"; + } + ost << SsrcsToString(ssrcs) << ";"; + ost << "ssrc_groups:"; + for (std::vector::const_iterator it = ssrc_groups.begin(); + it != ssrc_groups.end(); ++it) { + if (it != ssrc_groups.begin()) { + ost << ","; + } + ost << it->ToString(); + } + ost << ";"; + if (!type.empty()) { + ost << "type:" << type << ";"; + } + if (!display.empty()) { + ost << "display:" << display << ";"; + } + if (!cname.empty()) { + ost << "cname:" << cname << ";"; + } + if (!sync_label.empty()) { + ost << "sync_label:" << sync_label; + } + ost << "}"; + return ost.str(); +} + +bool StreamParams::AddSecondarySsrc(const std::string& semantics, + uint32 primary_ssrc, + uint32 secondary_ssrc) { + if (!has_ssrc(primary_ssrc)) { + return false; + } + + ssrcs.push_back(secondary_ssrc); + std::vector ssrc_vector; + ssrc_vector.push_back(primary_ssrc); + ssrc_vector.push_back(secondary_ssrc); + SsrcGroup ssrc_group = SsrcGroup(semantics, ssrc_vector); + ssrc_groups.push_back(ssrc_group); + return true; +} + +bool StreamParams::GetSecondarySsrc(const std::string& semantics, + uint32 primary_ssrc, + uint32* secondary_ssrc) const { + for (std::vector::const_iterator it = ssrc_groups.begin(); + it != ssrc_groups.end(); ++it) { + if (it->has_semantics(semantics) && + it->ssrcs.size() >= 2 && + it->ssrcs[0] == primary_ssrc) { + *secondary_ssrc = it->ssrcs[1]; + return true; + } + } + return false; +} + +bool GetStream(const StreamParamsVec& streams, + const StreamSelector& selector, + StreamParams* stream_out) { + for (StreamParamsVec::const_iterator stream = streams.begin(); + stream != streams.end(); ++stream) { + if (selector.Matches(*stream)) { + if (stream_out != NULL) { + *stream_out = *stream; + } + return true; + } + } + return false; +} + +bool GetStreamBySsrc(const StreamParamsVec& streams, uint32 ssrc, + StreamParams* stream_out) { + return GetStream(streams, StreamSelector(ssrc), stream_out); +} + +bool GetStreamByIds(const StreamParamsVec& streams, + const std::string& groupid, + const std::string& id, + StreamParams* stream_out) { + return GetStream(streams, StreamSelector(groupid, id), stream_out); +} + +bool RemoveStream(StreamParamsVec* streams, + const StreamSelector& selector) { + bool ret = false; + for (StreamParamsVec::iterator stream = streams->begin(); + stream != streams->end(); ) { + if (selector.Matches(*stream)) { + stream = streams->erase(stream); + ret = true; + } else { + ++stream; + } + } + return ret; +} + +bool RemoveStreamBySsrc(StreamParamsVec* streams, uint32 ssrc) { + return RemoveStream(streams, StreamSelector(ssrc)); +} + +bool RemoveStreamByIds(StreamParamsVec* streams, + const std::string& groupid, + const std::string& id) { + return RemoveStream(streams, StreamSelector(groupid, id)); +} + +} // namespace cricket diff --git a/talk/media/base/streamparams.h b/talk/media/base/streamparams.h new file mode 100644 index 000000000..1561d6f92 --- /dev/null +++ b/talk/media/base/streamparams.h @@ -0,0 +1,209 @@ +/* + * libjingle + * Copyright 2011 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. + */ + +// This file contains structures for describing SSRCs from a media source such +// as a MediaStreamTrack when it is sent across an RTP session. Multiple media +// sources may be sent across the same RTP session, each of them will be +// described by one StreamParams object +// SsrcGroup is used to describe the relationship between the SSRCs that +// are used for this media source. + +#ifndef TALK_MEDIA_BASE_STREAMPARAMS_H_ +#define TALK_MEDIA_BASE_STREAMPARAMS_H_ + +#include +#include +#include +#include + +#include "talk/base/basictypes.h" + +namespace cricket { + +extern const char kFecSsrcGroupSemantics[]; +extern const char kFidSsrcGroupSemantics[]; + +struct SsrcGroup { + SsrcGroup(const std::string& usage, const std::vector& ssrcs) + : semantics(usage), ssrcs(ssrcs) { + } + + bool operator==(const SsrcGroup& other) const { + return (semantics == other.semantics && ssrcs == other.ssrcs); + } + bool operator!=(const SsrcGroup &other) const { + return !(*this == other); + } + + bool has_semantics(const std::string& semantics) const; + + std::string ToString() const; + + std::string semantics; // e.g FIX, FEC, SIM. + std::vector ssrcs; // SSRCs of this type. +}; + +struct StreamParams { + static StreamParams CreateLegacy(uint32 ssrc) { + StreamParams stream; + stream.ssrcs.push_back(ssrc); + return stream; + } + + bool operator==(const StreamParams& other) const { + return (groupid == other.groupid && + id == other.id && + ssrcs == other.ssrcs && + ssrc_groups == other.ssrc_groups && + type == other.type && + display == other.display && + cname == other.cname && + sync_label == other.sync_label); + } + bool operator!=(const StreamParams &other) const { + return !(*this == other); + } + + uint32 first_ssrc() const { + if (ssrcs.empty()) { + return 0; + } + + return ssrcs[0]; + } + bool has_ssrcs() const { + return !ssrcs.empty(); + } + bool has_ssrc(uint32 ssrc) const { + return std::find(ssrcs.begin(), ssrcs.end(), ssrc) != ssrcs.end(); + } + void add_ssrc(uint32 ssrc) { + ssrcs.push_back(ssrc); + } + bool has_ssrc_groups() const { + return !ssrc_groups.empty(); + } + bool has_ssrc_group(const std::string& semantics) const { + return (get_ssrc_group(semantics) != NULL); + } + const SsrcGroup* get_ssrc_group(const std::string& semantics) const { + for (std::vector::const_iterator it = ssrc_groups.begin(); + it != ssrc_groups.end(); ++it) { + if (it->has_semantics(semantics)) { + return &(*it); + } + } + return NULL; + } + + // Convenience function to add an FID ssrc for a primary_ssrc + // that's already been added. + inline bool AddFidSsrc(uint32 primary_ssrc, uint32 fid_ssrc) { + return AddSecondarySsrc(kFidSsrcGroupSemantics, primary_ssrc, fid_ssrc); + } + + // Convenience function to lookup the FID ssrc for a primary_ssrc. + // Returns false if primary_ssrc not found or FID not defined for it. + inline bool GetFidSsrc(uint32 primary_ssrc, uint32* fid_ssrc) const { + return GetSecondarySsrc(kFidSsrcGroupSemantics, primary_ssrc, fid_ssrc); + } + + std::string ToString() const; + + // Resource of the MUC jid of the participant of with this stream. + // For 1:1 calls, should be left empty (which means remote streams + // and local streams should not be mixed together). + std::string groupid; + // Unique per-groupid, not across all groupids + std::string id; + std::vector ssrcs; // All SSRCs for this source + std::vector ssrc_groups; // e.g. FID, FEC, SIM + // Examples: "camera", "screencast" + std::string type; + // Friendly name describing stream + std::string display; + std::string cname; // RTCP CNAME + std::string sync_label; // Friendly name of cname. + + private: + bool AddSecondarySsrc(const std::string& semantics, uint32 primary_ssrc, + uint32 secondary_ssrc); + bool GetSecondarySsrc(const std::string& semantics, uint32 primary_ssrc, + uint32* secondary_ssrc) const; +}; + +// A Stream can be selected by either groupid+id or ssrc. +struct StreamSelector { + explicit StreamSelector(uint32 ssrc) : + ssrc(ssrc) { + } + + StreamSelector(const std::string& groupid, + const std::string& streamid) : + ssrc(0), + groupid(groupid), + streamid(streamid) { + } + + bool Matches(const StreamParams& stream) const { + if (ssrc == 0) { + return stream.groupid == groupid && stream.id == streamid; + } else { + return stream.has_ssrc(ssrc); + } + } + + uint32 ssrc; + std::string groupid; + std::string streamid; +}; + +typedef std::vector StreamParamsVec; + +// Finds the stream in streams. Returns true if found. +bool GetStream(const StreamParamsVec& streams, + const StreamSelector& selector, + StreamParams* stream_out); +bool GetStreamBySsrc(const StreamParamsVec& streams, uint32 ssrc, + StreamParams* stream_out); +bool GetStreamByIds(const StreamParamsVec& streams, + const std::string& groupid, + const std::string& id, + StreamParams* stream_out); + +// Removes the stream from streams. Returns true if a stream is +// found and removed. +bool RemoveStream(StreamParamsVec* streams, + const StreamSelector& selector); +bool RemoveStreamBySsrc(StreamParamsVec* streams, uint32 ssrc); +bool RemoveStreamByIds(StreamParamsVec* streams, + const std::string& groupid, + const std::string& id); + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_STREAMPARAMS_H_ diff --git a/talk/media/base/streamparams_unittest.cc b/talk/media/base/streamparams_unittest.cc new file mode 100644 index 000000000..99d1603e6 --- /dev/null +++ b/talk/media/base/streamparams_unittest.cc @@ -0,0 +1,165 @@ +/* + * libjingle + * Copyright 2012 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/base/gunit.h" +#include "talk/media/base/streamparams.h" +#include "talk/media/base/testutils.h" + +static const uint32 kSscrs1[] = {1}; +static const uint32 kSscrs2[] = {1, 2}; + +static cricket::StreamParams CreateStreamParamsWithSsrcGroup( + const std::string& semantics, const uint32 ssrcs_in[], size_t len) { + cricket::StreamParams stream; + std::vector ssrcs(ssrcs_in, ssrcs_in + len); + cricket::SsrcGroup sg(semantics, ssrcs); + stream.ssrcs = ssrcs; + stream.ssrc_groups.push_back(sg); + return stream; +} + +TEST(SsrcGroup, EqualNotEqual) { + cricket::SsrcGroup ssrc_groups[] = { + cricket::SsrcGroup("ABC", MAKE_VECTOR(kSscrs1)), + cricket::SsrcGroup("ABC", MAKE_VECTOR(kSscrs2)), + cricket::SsrcGroup("Abc", MAKE_VECTOR(kSscrs2)), + cricket::SsrcGroup("abc", MAKE_VECTOR(kSscrs2)), + }; + + for (size_t i = 0; i < ARRAY_SIZE(ssrc_groups); ++i) { + for (size_t j = 0; j < ARRAY_SIZE(ssrc_groups); ++j) { + EXPECT_EQ((ssrc_groups[i] == ssrc_groups[j]), (i == j)); + EXPECT_EQ((ssrc_groups[i] != ssrc_groups[j]), (i != j)); + } + } +} + +TEST(SsrcGroup, HasSemantics) { + cricket::SsrcGroup sg1("ABC", MAKE_VECTOR(kSscrs1)); + EXPECT_TRUE(sg1.has_semantics("ABC")); + + cricket::SsrcGroup sg2("Abc", MAKE_VECTOR(kSscrs1)); + EXPECT_FALSE(sg2.has_semantics("ABC")); + + cricket::SsrcGroup sg3("abc", MAKE_VECTOR(kSscrs1)); + EXPECT_FALSE(sg3.has_semantics("ABC")); +} + +TEST(SsrcGroup, ToString) { + cricket::SsrcGroup sg1("ABC", MAKE_VECTOR(kSscrs1)); + EXPECT_STREQ("{semantics:ABC;ssrcs:[1]}", sg1.ToString().c_str()); +} + +TEST(StreamParams, CreateLegacy) { + const uint32 ssrc = 7; + cricket::StreamParams one_sp = cricket::StreamParams::CreateLegacy(ssrc); + EXPECT_EQ(1U, one_sp.ssrcs.size()); + EXPECT_EQ(ssrc, one_sp.first_ssrc()); + EXPECT_TRUE(one_sp.has_ssrcs()); + EXPECT_TRUE(one_sp.has_ssrc(ssrc)); + EXPECT_FALSE(one_sp.has_ssrc(ssrc+1)); + EXPECT_FALSE(one_sp.has_ssrc_groups()); + EXPECT_EQ(0U, one_sp.ssrc_groups.size()); +} + +TEST(StreamParams, HasSsrcGroup) { + cricket::StreamParams sp = + CreateStreamParamsWithSsrcGroup("XYZ", kSscrs2, ARRAY_SIZE(kSscrs2)); + EXPECT_EQ(2U, sp.ssrcs.size()); + EXPECT_EQ(kSscrs2[0], sp.first_ssrc()); + EXPECT_TRUE(sp.has_ssrcs()); + EXPECT_TRUE(sp.has_ssrc(kSscrs2[0])); + EXPECT_TRUE(sp.has_ssrc(kSscrs2[1])); + EXPECT_TRUE(sp.has_ssrc_group("XYZ")); + EXPECT_EQ(1U, sp.ssrc_groups.size()); + EXPECT_EQ(2U, sp.ssrc_groups[0].ssrcs.size()); + EXPECT_EQ(kSscrs2[0], sp.ssrc_groups[0].ssrcs[0]); + EXPECT_EQ(kSscrs2[1], sp.ssrc_groups[0].ssrcs[1]); +} + +TEST(StreamParams, GetSsrcGroup) { + cricket::StreamParams sp = + CreateStreamParamsWithSsrcGroup("XYZ", kSscrs2, ARRAY_SIZE(kSscrs2)); + EXPECT_EQ(NULL, sp.get_ssrc_group("xyz")); + EXPECT_EQ(&sp.ssrc_groups[0], sp.get_ssrc_group("XYZ")); +} + +TEST(StreamParams, EqualNotEqual) { + cricket::StreamParams l1 = cricket::StreamParams::CreateLegacy(1); + cricket::StreamParams l2 = cricket::StreamParams::CreateLegacy(2); + cricket::StreamParams sg1 = + CreateStreamParamsWithSsrcGroup("ABC", kSscrs1, ARRAY_SIZE(kSscrs1)); + cricket::StreamParams sg2 = + CreateStreamParamsWithSsrcGroup("ABC", kSscrs2, ARRAY_SIZE(kSscrs2)); + cricket::StreamParams sg3 = + CreateStreamParamsWithSsrcGroup("Abc", kSscrs2, ARRAY_SIZE(kSscrs2)); + cricket::StreamParams sg4 = + CreateStreamParamsWithSsrcGroup("abc", kSscrs2, ARRAY_SIZE(kSscrs2)); + cricket::StreamParams sps[] = {l1, l2, sg1, sg2, sg3, sg4}; + + for (size_t i = 0; i < ARRAY_SIZE(sps); ++i) { + for (size_t j = 0; j < ARRAY_SIZE(sps); ++j) { + EXPECT_EQ((sps[i] == sps[j]), (i == j)); + EXPECT_EQ((sps[i] != sps[j]), (i != j)); + } + } +} + +TEST(StreamParams, FidFunctions) { + uint32 fid_ssrc; + + cricket::StreamParams sp = cricket::StreamParams::CreateLegacy(1); + EXPECT_FALSE(sp.AddFidSsrc(10, 20)); + EXPECT_TRUE(sp.AddFidSsrc(1, 2)); + EXPECT_TRUE(sp.GetFidSsrc(1, &fid_ssrc)); + EXPECT_EQ(2u, fid_ssrc); + EXPECT_FALSE(sp.GetFidSsrc(15, &fid_ssrc)); + + sp.add_ssrc(20); + sp.AddFidSsrc(20, 30); + EXPECT_TRUE(sp.GetFidSsrc(20, &fid_ssrc)); + EXPECT_EQ(30u, fid_ssrc); + + // Manually create SsrcGroup to test bounds-checking + // in GetSecondarySsrc. We construct an invalid StreamParams + // for this. + std::vector fid_vector; + fid_vector.push_back(13); + cricket::SsrcGroup invalid_fid_group(cricket::kFidSsrcGroupSemantics, + fid_vector); + cricket::StreamParams sp_invalid; + sp_invalid.add_ssrc(13); + sp_invalid.ssrc_groups.push_back(invalid_fid_group); + EXPECT_FALSE(sp_invalid.GetFidSsrc(13, &fid_ssrc)); +} + +TEST(StreamParams, ToString) { + cricket::StreamParams sp = + CreateStreamParamsWithSsrcGroup("XYZ", kSscrs2, ARRAY_SIZE(kSscrs2)); + EXPECT_STREQ("{ssrcs:[1,2];ssrc_groups:{semantics:XYZ;ssrcs:[1,2]};}", + sp.ToString().c_str()); +} diff --git a/talk/media/base/testutils.cc b/talk/media/base/testutils.cc new file mode 100644 index 000000000..a5e2df9f4 --- /dev/null +++ b/talk/media/base/testutils.cc @@ -0,0 +1,338 @@ +/* + * libjingle + * Copyright 2004 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/media/base/testutils.h" + +#include + +#include "talk/base/bytebuffer.h" +#include "talk/base/fileutils.h" +#include "talk/base/gunit.h" +#include "talk/base/pathutils.h" +#include "talk/base/stream.h" +#include "talk/base/stringutils.h" +#include "talk/media/base/rtpdump.h" +#include "talk/media/base/videocapturer.h" +#include "talk/media/base/videoframe.h" + +namespace cricket { + +///////////////////////////////////////////////////////////////////////// +// Implementation of RawRtpPacket +///////////////////////////////////////////////////////////////////////// +void RawRtpPacket::WriteToByteBuffer( + uint32 in_ssrc, talk_base::ByteBuffer *buf) const { + if (!buf) return; + + buf->WriteUInt8(ver_to_cc); + buf->WriteUInt8(m_to_pt); + buf->WriteUInt16(sequence_number); + buf->WriteUInt32(timestamp); + buf->WriteUInt32(in_ssrc); + buf->WriteBytes(payload, sizeof(payload)); +} + +bool RawRtpPacket::ReadFromByteBuffer(talk_base::ByteBuffer* buf) { + if (!buf) return false; + + bool ret = true; + ret &= buf->ReadUInt8(&ver_to_cc); + ret &= buf->ReadUInt8(&m_to_pt); + ret &= buf->ReadUInt16(&sequence_number); + ret &= buf->ReadUInt32(×tamp); + ret &= buf->ReadUInt32(&ssrc); + ret &= buf->ReadBytes(payload, sizeof(payload)); + return ret; +} + +bool RawRtpPacket::SameExceptSeqNumTimestampSsrc( + const RawRtpPacket& packet, uint16 seq, uint32 ts, uint32 ssc) const { + return sequence_number == seq && + timestamp == ts && + ver_to_cc == packet.ver_to_cc && + m_to_pt == packet.m_to_pt && + ssrc == ssc && + 0 == memcmp(payload, packet.payload, sizeof(payload)); +} + +///////////////////////////////////////////////////////////////////////// +// Implementation of RawRtcpPacket +///////////////////////////////////////////////////////////////////////// +void RawRtcpPacket::WriteToByteBuffer(talk_base::ByteBuffer *buf) const { + if (!buf) return; + + buf->WriteUInt8(ver_to_count); + buf->WriteUInt8(type); + buf->WriteUInt16(length); + buf->WriteBytes(payload, sizeof(payload)); +} + +bool RawRtcpPacket::ReadFromByteBuffer(talk_base::ByteBuffer* buf) { + if (!buf) return false; + + bool ret = true; + ret &= buf->ReadUInt8(&ver_to_count); + ret &= buf->ReadUInt8(&type); + ret &= buf->ReadUInt16(&length); + ret &= buf->ReadBytes(payload, sizeof(payload)); + return ret; +} + +bool RawRtcpPacket::EqualsTo(const RawRtcpPacket& packet) const { + return ver_to_count == packet.ver_to_count && + type == packet.type && + length == packet.length && + 0 == memcmp(payload, packet.payload, sizeof(payload)); +} + +///////////////////////////////////////////////////////////////////////// +// Implementation of class RtpTestUtility +///////////////////////////////////////////////////////////////////////// +const RawRtpPacket RtpTestUtility::kTestRawRtpPackets[] = { + {0x80, 0, 0, 0, RtpTestUtility::kDefaultSsrc, "RTP frame 0"}, + {0x80, 0, 1, 30, RtpTestUtility::kDefaultSsrc, "RTP frame 1"}, + {0x80, 0, 2, 30, RtpTestUtility::kDefaultSsrc, "RTP frame 1"}, + {0x80, 0, 3, 60, RtpTestUtility::kDefaultSsrc, "RTP frame 2"} +}; +const RawRtcpPacket RtpTestUtility::kTestRawRtcpPackets[] = { + // The Version is 2, the Length is 2, and the payload has 8 bytes. + {0x80, 0, 2, "RTCP0000"}, + {0x80, 0, 2, "RTCP0001"}, + {0x80, 0, 2, "RTCP0002"}, + {0x80, 0, 2, "RTCP0003"}, +}; + +size_t RtpTestUtility::GetTestPacketCount() { + return talk_base::_min( + ARRAY_SIZE(kTestRawRtpPackets), + ARRAY_SIZE(kTestRawRtcpPackets)); +} + +bool RtpTestUtility::WriteTestPackets( + size_t count, bool rtcp, uint32 rtp_ssrc, RtpDumpWriter* writer) { + if (!writer || count > GetTestPacketCount()) return false; + + bool result = true; + uint32 elapsed_time_ms = 0; + for (size_t i = 0; i < count && result; ++i) { + talk_base::ByteBuffer buf; + if (rtcp) { + kTestRawRtcpPackets[i].WriteToByteBuffer(&buf); + } else { + kTestRawRtpPackets[i].WriteToByteBuffer(rtp_ssrc, &buf); + } + + RtpDumpPacket dump_packet(buf.Data(), buf.Length(), elapsed_time_ms, rtcp); + elapsed_time_ms += kElapsedTimeInterval; + result &= (talk_base::SR_SUCCESS == writer->WritePacket(dump_packet)); + } + return result; +} + +bool RtpTestUtility::VerifyTestPacketsFromStream( + size_t count, talk_base::StreamInterface* stream, uint32 ssrc) { + if (!stream) return false; + + uint32 prev_elapsed_time = 0; + bool result = true; + stream->Rewind(); + RtpDumpLoopReader reader(stream); + for (size_t i = 0; i < count && result; ++i) { + // Which loop and which index in the loop are we reading now. + size_t loop = i / GetTestPacketCount(); + size_t index = i % GetTestPacketCount(); + + RtpDumpPacket packet; + result &= (talk_base::SR_SUCCESS == reader.ReadPacket(&packet)); + // Check the elapsed time of the dump packet. + result &= (packet.elapsed_time >= prev_elapsed_time); + prev_elapsed_time = packet.elapsed_time; + + // Check the RTP or RTCP packet. + talk_base::ByteBuffer buf(reinterpret_cast(&packet.data[0]), + packet.data.size()); + if (packet.is_rtcp()) { + // RTCP packet. + RawRtcpPacket rtcp_packet; + result &= rtcp_packet.ReadFromByteBuffer(&buf); + result &= rtcp_packet.EqualsTo(kTestRawRtcpPackets[index]); + } else { + // RTP packet. + RawRtpPacket rtp_packet; + result &= rtp_packet.ReadFromByteBuffer(&buf); + result &= rtp_packet.SameExceptSeqNumTimestampSsrc( + kTestRawRtpPackets[index], + kTestRawRtpPackets[index].sequence_number + + loop * GetTestPacketCount(), + kTestRawRtpPackets[index].timestamp + loop * kRtpTimestampIncrease, + ssrc); + } + } + + stream->Rewind(); + return result; +} + +bool RtpTestUtility::VerifyPacket(const RtpDumpPacket* dump, + const RawRtpPacket* raw, + bool header_only) { + if (!dump || !raw) return false; + + talk_base::ByteBuffer buf; + raw->WriteToByteBuffer(RtpTestUtility::kDefaultSsrc, &buf); + + if (header_only) { + size_t header_len = 0; + dump->GetRtpHeaderLen(&header_len); + return header_len == dump->data.size() && + buf.Length() > dump->data.size() && + 0 == memcmp(buf.Data(), &dump->data[0], dump->data.size()); + } else { + return buf.Length() == dump->data.size() && + 0 == memcmp(buf.Data(), &dump->data[0], dump->data.size()); + } +} + +// Implementation of VideoCaptureListener. +VideoCapturerListener::VideoCapturerListener(VideoCapturer* capturer) + : last_capture_state_(CS_STARTING), + frame_count_(0), + frame_fourcc_(0), + frame_width_(0), + frame_height_(0), + frame_size_(0), + resolution_changed_(false) { + capturer->SignalStateChange.connect(this, + &VideoCapturerListener::OnStateChange); + capturer->SignalFrameCaptured.connect(this, + &VideoCapturerListener::OnFrameCaptured); +} + +void VideoCapturerListener::OnStateChange(VideoCapturer* capturer, + CaptureState result) { + last_capture_state_ = result; +} + +void VideoCapturerListener::OnFrameCaptured(VideoCapturer* capturer, + const CapturedFrame* frame) { + ++frame_count_; + if (1 == frame_count_) { + frame_fourcc_ = frame->fourcc; + frame_width_ = frame->width; + frame_height_ = frame->height; + frame_size_ = frame->data_size; + } else if (frame_width_ != frame->width || frame_height_ != frame->height) { + resolution_changed_ = true; + } +} + +// Returns the absolute path to a file in the testdata/ directory. +std::string GetTestFilePath(const std::string& filename) { + // Locate test data directory. + talk_base::Pathname path = GetTalkDirectory(); + EXPECT_FALSE(path.empty()); // must be run from inside "talk" + path.AppendFolder("media"); + path.AppendFolder("testdata"); + path.SetFilename(filename); + return path.pathname(); +} + +// Loads the image with the specified prefix and size into |out|. +bool LoadPlanarYuvTestImage(const std::string& prefix, + int width, int height, uint8* out) { + std::stringstream ss; + ss << prefix << "." << width << "x" << height << "_P420.yuv"; + + talk_base::scoped_ptr stream( + talk_base::Filesystem::OpenFile(talk_base::Pathname( + GetTestFilePath(ss.str())), "rb")); + if (!stream) { + return false; + } + + talk_base::StreamResult res = + stream->ReadAll(out, I420_SIZE(width, height), NULL, NULL); + return (res == talk_base::SR_SUCCESS); +} + +// Dumps the YUV image out to a file, for visual inspection. +// PYUV tool can be used to view dump files. +void DumpPlanarYuvTestImage(const std::string& prefix, const uint8* img, + int w, int h) { + talk_base::FileStream fs; + char filename[256]; + talk_base::sprintfn(filename, sizeof(filename), "%s.%dx%d_P420.yuv", + prefix.c_str(), w, h); + fs.Open(filename, "wb", NULL); + fs.Write(img, I420_SIZE(w, h), NULL, NULL); +} + +// Dumps the ARGB image out to a file, for visual inspection. +// ffplay tool can be used to view dump files. +void DumpPlanarArgbTestImage(const std::string& prefix, const uint8* img, + int w, int h) { + talk_base::FileStream fs; + char filename[256]; + talk_base::sprintfn(filename, sizeof(filename), "%s.%dx%d_ARGB.raw", + prefix.c_str(), w, h); + fs.Open(filename, "wb", NULL); + fs.Write(img, ARGB_SIZE(w, h), NULL, NULL); +} + +bool VideoFrameEqual(const VideoFrame* frame0, const VideoFrame* frame1) { + const uint8* y0 = frame0->GetYPlane(); + const uint8* u0 = frame0->GetUPlane(); + const uint8* v0 = frame0->GetVPlane(); + const uint8* y1 = frame1->GetYPlane(); + const uint8* u1 = frame1->GetUPlane(); + const uint8* v1 = frame1->GetVPlane(); + + for (size_t i = 0; i < frame0->GetHeight(); ++i) { + if (0 != memcmp(y0, y1, frame0->GetWidth())) { + return false; + } + y0 += frame0->GetYPitch(); + y1 += frame1->GetYPitch(); + } + + for (size_t i = 0; i < frame0->GetChromaHeight(); ++i) { + if (0 != memcmp(u0, u1, frame0->GetChromaWidth())) { + return false; + } + if (0 != memcmp(v0, v1, frame0->GetChromaWidth())) { + return false; + } + u0 += frame0->GetUPitch(); + v0 += frame0->GetVPitch(); + u1 += frame1->GetUPitch(); + v1 += frame1->GetVPitch(); + } + + return true; +} + +} // namespace cricket diff --git a/talk/media/base/testutils.h b/talk/media/base/testutils.h new file mode 100644 index 000000000..7bc7dc3ae --- /dev/null +++ b/talk/media/base/testutils.h @@ -0,0 +1,242 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_TESTUTILS_H_ +#define TALK_MEDIA_BASE_TESTUTILS_H_ + +#include +#include +#if !defined(DISABLE_YUV) +#include "libyuv/compare.h" +#endif +#include "talk/base/basictypes.h" +#include "talk/base/sigslot.h" +#include "talk/base/window.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/videocapturer.h" +#include "talk/media/base/videocommon.h" + +namespace talk_base { +class ByteBuffer; +class StreamInterface; +} + +namespace cricket { + +// Returns size of 420 image with rounding on chroma for odd sizes. +#define I420_SIZE(w, h) (w * h + (((w + 1) / 2) * ((h + 1) / 2)) * 2) +// Returns size of ARGB image. +#define ARGB_SIZE(w, h) (w * h * 4) + +template inline std::vector MakeVector(const T a[], size_t s) { + return std::vector(a, a + s); +} +#define MAKE_VECTOR(a) cricket::MakeVector(a, ARRAY_SIZE(a)) + +struct RtpDumpPacket; +class RtpDumpWriter; +class VideoFrame; + +struct RawRtpPacket { + void WriteToByteBuffer(uint32 in_ssrc, talk_base::ByteBuffer* buf) const; + bool ReadFromByteBuffer(talk_base::ByteBuffer* buf); + // Check if this packet is the same as the specified packet except the + // sequence number and timestamp, which should be the same as the specified + // parameters. + bool SameExceptSeqNumTimestampSsrc( + const RawRtpPacket& packet, uint16 seq, uint32 ts, uint32 ssc) const; + int size() const { return 28; } + + uint8 ver_to_cc; + uint8 m_to_pt; + uint16 sequence_number; + uint32 timestamp; + uint32 ssrc; + char payload[16]; +}; + +struct RawRtcpPacket { + void WriteToByteBuffer(talk_base::ByteBuffer* buf) const; + bool ReadFromByteBuffer(talk_base::ByteBuffer* buf); + bool EqualsTo(const RawRtcpPacket& packet) const; + + uint8 ver_to_count; + uint8 type; + uint16 length; + char payload[16]; +}; + +class RtpTestUtility { + public: + static size_t GetTestPacketCount(); + + // Write the first count number of kTestRawRtcpPackets or kTestRawRtpPackets, + // depending on the flag rtcp. If it is RTP, use the specified SSRC. Return + // true if successful. + static bool WriteTestPackets( + size_t count, bool rtcp, uint32 rtp_ssrc, RtpDumpWriter* writer); + + // Loop read the first count number of packets from the specified stream. + // Verify the elapsed time of the dump packets increase monotonically. If the + // stream is a RTP stream, verify the RTP sequence number, timestamp, and + // payload. If the stream is a RTCP stream, verify the RTCP header and + // payload. + static bool VerifyTestPacketsFromStream( + size_t count, talk_base::StreamInterface* stream, uint32 ssrc); + + // Verify the dump packet is the same as the raw RTP packet. + static bool VerifyPacket(const RtpDumpPacket* dump, + const RawRtpPacket* raw, + bool header_only); + + static const uint32 kDefaultSsrc = 1; + static const uint32 kRtpTimestampIncrease = 90; + static const uint32 kDefaultTimeIncrease = 30; + static const uint32 kElapsedTimeInterval = 10; + static const RawRtpPacket kTestRawRtpPackets[]; + static const RawRtcpPacket kTestRawRtcpPackets[]; + + private: + RtpTestUtility() {} +}; + +// Test helper for testing VideoCapturer implementations. +class VideoCapturerListener : public sigslot::has_slots<> { + public: + explicit VideoCapturerListener(VideoCapturer* cap); + + CaptureState last_capture_state() const { return last_capture_state_; } + int frame_count() const { return frame_count_; } + uint32 frame_fourcc() const { return frame_fourcc_; } + int frame_width() const { return frame_width_; } + int frame_height() const { return frame_height_; } + uint32 frame_size() const { return frame_size_; } + bool resolution_changed() const { return resolution_changed_; } + + void OnStateChange(VideoCapturer* capturer, CaptureState state); + void OnFrameCaptured(VideoCapturer* capturer, const CapturedFrame* frame); + + private: + CaptureState last_capture_state_; + int frame_count_; + uint32 frame_fourcc_; + int frame_width_; + int frame_height_; + uint32 frame_size_; + bool resolution_changed_; +}; + +class ScreencastEventCatcher : public sigslot::has_slots<> { + public: + ScreencastEventCatcher() : ssrc_(0), ev_(talk_base::WE_RESIZE) { } + uint32 ssrc() const { return ssrc_; } + talk_base::WindowEvent event() const { return ev_; } + void OnEvent(uint32 ssrc, talk_base::WindowEvent ev) { + ssrc_ = ssrc; + ev_ = ev; + } + private: + uint32 ssrc_; + talk_base::WindowEvent ev_; +}; + +class VideoMediaErrorCatcher : public sigslot::has_slots<> { + public: + VideoMediaErrorCatcher() : ssrc_(0), error_(VideoMediaChannel::ERROR_NONE) { } + uint32 ssrc() const { return ssrc_; } + VideoMediaChannel::Error error() const { return error_; } + void OnError(uint32 ssrc, VideoMediaChannel::Error error) { + ssrc_ = ssrc; + error_ = error; + } + private: + uint32 ssrc_; + VideoMediaChannel::Error error_; +}; + +// Returns the absolute path to a file in the testdata/ directory. +std::string GetTestFilePath(const std::string& filename); + +// PSNR formula: psnr = 10 * log10 (Peak Signal^2 / mse) +// sse is set to a small number for identical frames or sse == 0 +static inline double ComputePSNR(double sse, double count) { +#if !defined(DISABLE_YUV) + return libyuv::SumSquareErrorToPsnr(static_cast(sse), + static_cast(count)); +#else + if (sse <= 0.) + sse = 65025.0 * count / pow(10., 128./10.); // produces max PSNR of 128 + return 10.0 * log10(65025.0 * count / sse); +#endif +} + +static inline double ComputeSumSquareError(const uint8 *org, const uint8 *rec, + int size) { +#if !defined(DISABLE_YUV) + return static_cast(libyuv::ComputeSumSquareError(org, rec, size)); +#else + double sse = 0.; + for (int j = 0; j < size; ++j) { + const int diff = static_cast(org[j]) - static_cast(rec[j]); + sse += static_cast(diff * diff); + } + return sse; +#endif +} + +// Loads the image with the specified prefix and size into |out|. +bool LoadPlanarYuvTestImage(const std::string& prefix, + int width, int height, uint8* out); + +// Dumps the YUV image out to a file, for visual inspection. +// PYUV tool can be used to view dump files. +void DumpPlanarYuvTestImage(const std::string& prefix, const uint8* img, + int w, int h); + +// Dumps the ARGB image out to a file, for visual inspection. +// ffplay tool can be used to view dump files. +void DumpPlanarArgbTestImage(const std::string& prefix, const uint8* img, + int w, int h); + +// Compare two I420 frames. +bool VideoFrameEqual(const VideoFrame* frame0, const VideoFrame* frame1); + +// Checks whether |codecs| contains |codec|; checks using Codec::Matches(). +template +bool ContainsMatchingCodec(const std::vector& codecs, const C& codec) { + typename std::vector::const_iterator it; + for (it = codecs.begin(); it != codecs.end(); ++it) { + if (it->Matches(codec)) { + return true; + } + } + return false; +} + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_TESTUTILS_H_ diff --git a/talk/media/base/videoadapter.cc b/talk/media/base/videoadapter.cc new file mode 100644 index 000000000..1e5918a1a --- /dev/null +++ b/talk/media/base/videoadapter.cc @@ -0,0 +1,588 @@ +// libjingle +// Copyright 2010 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/media/base/videoadapter.h" + +#include // For INT_MAX + +#include "talk/media/base/constants.h" +#include "talk/base/logging.h" +#include "talk/base/timeutils.h" +#include "talk/media/base/videoframe.h" + +namespace cricket { + +// TODO(fbarchard): Make downgrades settable +static const int kMaxCpuDowngrades = 2; // Downgrade at most 2 times for CPU. +static const int kDefaultDowngradeWaitTimeMs = 2000; + +// TODO(fbarchard): Consider making scale factor table settable, to allow +// application to select quality vs performance tradeoff. +// TODO(fbarchard): Add framerate scaling to tables for 1/2 framerate. +// List of scale factors that adapter will scale by. +#if defined(IOS) || defined(ANDROID) +// Mobile needs 1/4 scale for VGA (640 x 360) to QQVGA (160 x 90) +// or 1/4 scale for HVGA (480 x 270) to QQHVGA (120 x 67) +static const int kMinNumPixels = 120 * 67; +static float kScaleFactors[] = { + 1.f / 1.f, // Full size. + 3.f / 4.f, // 3/4 scale. + 1.f / 2.f, // 1/2 scale. + 3.f / 8.f, // 3/8 scale. + 1.f / 4.f, // 1/4 scale. +}; +#else +// Desktop needs 1/8 scale for HD (1280 x 720) to QQVGA (160 x 90) +static const int kMinNumPixels = 160 * 100; +static float kScaleFactors[] = { + 1.f / 1.f, // Full size. + 3.f / 4.f, // 3/4 scale. + 1.f / 2.f, // 1/2 scale. + 3.f / 8.f, // 3/8 scale. + 1.f / 4.f, // 1/4 scale. + 3.f / 16.f, // 3/16 scale. + 1.f / 8.f // 1/8 scale. +}; +#endif + +static const int kNumScaleFactors = ARRAY_SIZE(kScaleFactors); + +// Find the scale factor that, when applied to width and height, is closest +// to num_pixels. +float VideoAdapter::FindClosestScale(int width, int height, + int target_num_pixels) { + if (!target_num_pixels) { + return 0.f; + } + int best_distance = INT_MAX; + int best_index = kNumScaleFactors - 1; // Default to max scale. + for (int i = 0; i < kNumScaleFactors; ++i) { + int test_num_pixels = static_cast(width * kScaleFactors[i] * + height * kScaleFactors[i]); + int diff = test_num_pixels - target_num_pixels; + if (diff < 0) { + diff = -diff; + } + if (diff < best_distance) { + best_distance = diff; + best_index = i; + if (best_distance == 0) { // Found exact match. + break; + } + } + } + return kScaleFactors[best_index]; +} + +// Finds the scale factor that, when applied to width and height, produces +// fewer than num_pixels. +float VideoAdapter::FindLowerScale(int width, int height, + int target_num_pixels) { + if (!target_num_pixels) { + return 0.f; + } + int best_distance = INT_MAX; + int best_index = kNumScaleFactors - 1; // Default to max scale. + for (int i = 0; i < kNumScaleFactors; ++i) { + int test_num_pixels = static_cast(width * kScaleFactors[i] * + height * kScaleFactors[i]); + int diff = target_num_pixels - test_num_pixels; + if (diff >= 0 && diff < best_distance) { + best_distance = diff; + best_index = i; + if (best_distance == 0) { // Found exact match. + break; + } + } + } + return kScaleFactors[best_index]; +} + +// There are several frame sizes used by Adapter. This explains them +// input_format - set once by server to frame size expected from the camera. +// output_format - size that output would like to be. Includes framerate. +// output_num_pixels - size that output should be constrained to. Used to +// compute output_format from in_frame. +// in_frame - actual camera captured frame size, which is typically the same +// as input_format. This can also be rotated or cropped for aspect ratio. +// out_frame - actual frame output by adapter. Should be a direct scale of +// in_frame maintaining rotation and aspect ratio. +// OnOutputFormatRequest - server requests you send this resolution based on +// view requests. +// OnEncoderResolutionRequest - encoder requests you send this resolution based +// on bandwidth +// OnCpuLoadUpdated - cpu monitor requests you send this resolution based on +// cpu load. + +/////////////////////////////////////////////////////////////////////// +// Implementation of VideoAdapter +VideoAdapter::VideoAdapter() + : output_num_pixels_(INT_MAX), + black_output_(false), + is_black_(false), + interval_next_frame_(0) { +} + +VideoAdapter::~VideoAdapter() { +} + +void VideoAdapter::SetInputFormat(const VideoFrame& in_frame) { + talk_base::CritScope cs(&critical_section_); + input_format_.width = in_frame.GetWidth(); + input_format_.height = in_frame.GetHeight(); +} + +void VideoAdapter::SetInputFormat(const VideoFormat& format) { + talk_base::CritScope cs(&critical_section_); + input_format_ = format; + output_format_.interval = talk_base::_max( + output_format_.interval, input_format_.interval); +} + +void VideoAdapter::SetOutputFormat(const VideoFormat& format) { + talk_base::CritScope cs(&critical_section_); + output_format_ = format; + output_num_pixels_ = output_format_.width * output_format_.height; + output_format_.interval = talk_base::_max( + output_format_.interval, input_format_.interval); +} + +const VideoFormat& VideoAdapter::input_format() { + talk_base::CritScope cs(&critical_section_); + return input_format_; +} + +const VideoFormat& VideoAdapter::output_format() { + talk_base::CritScope cs(&critical_section_); + return output_format_; +} + +void VideoAdapter::SetBlackOutput(bool black) { + talk_base::CritScope cs(&critical_section_); + black_output_ = black; +} + +// Constrain output resolution to this many pixels overall +void VideoAdapter::SetOutputNumPixels(int num_pixels) { + output_num_pixels_ = num_pixels; +} + +int VideoAdapter::GetOutputNumPixels() const { + return output_num_pixels_; +} + +// TODO(fbarchard): Add AdaptFrameRate function that only drops frames but +// not resolution. +bool VideoAdapter::AdaptFrame(const VideoFrame* in_frame, + const VideoFrame** out_frame) { + talk_base::CritScope cs(&critical_section_); + if (!in_frame || !out_frame) { + return false; + } + + // Update input to actual frame dimensions. + SetInputFormat(*in_frame); + + // Drop the input frame if necessary. + bool should_drop = false; + if (!output_num_pixels_) { + // Drop all frames as the output format is 0x0. + should_drop = true; + } else { + // Drop some frames based on input fps and output fps. + // Normally output fps is less than input fps. + // TODO(fbarchard): Consider adjusting interval to reflect the adjusted + // interval between frames after dropping some frames. + interval_next_frame_ += input_format_.interval; + if (output_format_.interval > 0) { + if (interval_next_frame_ >= output_format_.interval) { + interval_next_frame_ %= output_format_.interval; + } else { + should_drop = true; + } + } + } + if (should_drop) { + *out_frame = NULL; + return true; + } + + if (output_num_pixels_) { + float scale = VideoAdapter::FindClosestScale(in_frame->GetWidth(), + in_frame->GetHeight(), + output_num_pixels_); + output_format_.width = static_cast(in_frame->GetWidth() * scale + .5f); + output_format_.height = static_cast(in_frame->GetHeight() * scale + + .5f); + } + + if (!StretchToOutputFrame(in_frame)) { + return false; + } + + *out_frame = output_frame_.get(); + return true; +} + +bool VideoAdapter::StretchToOutputFrame(const VideoFrame* in_frame) { + int output_width = output_format_.width; + int output_height = output_format_.height; + + // Create and stretch the output frame if it has not been created yet or its + // size is not same as the expected. + bool stretched = false; + if (!output_frame_ || + output_frame_->GetWidth() != static_cast(output_width) || + output_frame_->GetHeight() != static_cast(output_height)) { + output_frame_.reset( + in_frame->Stretch(output_width, output_height, true, true)); + if (!output_frame_) { + LOG(LS_WARNING) << "Adapter failed to stretch frame to " + << output_width << "x" << output_height; + return false; + } + stretched = true; + is_black_ = false; + } + + if (!black_output_) { + if (!stretched) { + // The output frame does not need to be blacken and has not been stretched + // from the input frame yet, stretch the input frame. This is the most + // common case. + in_frame->StretchToFrame(output_frame_.get(), true, true); + } + is_black_ = false; + } else { + if (!is_black_) { + output_frame_->SetToBlack(); + is_black_ = true; + } + output_frame_->SetElapsedTime(in_frame->GetElapsedTime()); + output_frame_->SetTimeStamp(in_frame->GetTimeStamp()); + } + + return true; +} + +/////////////////////////////////////////////////////////////////////// +// Implementation of CoordinatedVideoAdapter +CoordinatedVideoAdapter::CoordinatedVideoAdapter() + : cpu_adaptation_(false), + gd_adaptation_(true), + view_adaptation_(true), + view_switch_(false), + cpu_downgrade_count_(0), + cpu_downgrade_wait_time_(0), + high_system_threshold_(kHighSystemCpuThreshold), + low_system_threshold_(kLowSystemCpuThreshold), + process_threshold_(kProcessCpuThreshold), + view_desired_num_pixels_(INT_MAX), + view_desired_interval_(0), + encoder_desired_num_pixels_(INT_MAX), + cpu_desired_num_pixels_(INT_MAX), + adapt_reason_(0) { +} + +// Helper function to UPGRADE or DOWNGRADE a number of pixels +void CoordinatedVideoAdapter::StepPixelCount( + CoordinatedVideoAdapter::AdaptRequest request, + int* num_pixels) { + switch (request) { + case CoordinatedVideoAdapter::DOWNGRADE: + *num_pixels /= 2; + break; + + case CoordinatedVideoAdapter::UPGRADE: + *num_pixels *= 2; + break; + + default: // No change in pixel count + break; + } + return; +} + +// Find the adaptation request of the cpu based on the load. Return UPGRADE if +// the load is low, DOWNGRADE if the load is high, and KEEP otherwise. +CoordinatedVideoAdapter::AdaptRequest CoordinatedVideoAdapter::FindCpuRequest( + int current_cpus, int max_cpus, + float process_load, float system_load) { + // Downgrade if system is high and plugin is at least more than midrange. + if (system_load >= high_system_threshold_ * max_cpus && + process_load >= process_threshold_ * current_cpus) { + return CoordinatedVideoAdapter::DOWNGRADE; + // Upgrade if system is low. + } else if (system_load < low_system_threshold_ * max_cpus) { + return CoordinatedVideoAdapter::UPGRADE; + } + return CoordinatedVideoAdapter::KEEP; +} + +// A remote view request for a new resolution. +void CoordinatedVideoAdapter::OnOutputFormatRequest(const VideoFormat& format) { + talk_base::CritScope cs(&request_critical_section_); + if (!view_adaptation_) { + return; + } + // Set output for initial aspect ratio in mediachannel unittests. + int old_num_pixels = GetOutputNumPixels(); + SetOutputFormat(format); + SetOutputNumPixels(old_num_pixels); + view_desired_num_pixels_ = format.width * format.height; + view_desired_interval_ = format.interval; + int new_width, new_height; + bool changed = AdaptToMinimumFormat(&new_width, &new_height); + LOG(LS_INFO) << "VAdapt View Request: " + << format.width << "x" << format.height + << " Pixels: " << view_desired_num_pixels_ + << " Changed: " << (changed ? "true" : "false") + << " To: " << new_width << "x" << new_height; +} + +// A Bandwidth GD request for new resolution +void CoordinatedVideoAdapter::OnEncoderResolutionRequest( + int width, int height, AdaptRequest request) { + talk_base::CritScope cs(&request_critical_section_); + if (!gd_adaptation_) { + return; + } + int old_encoder_desired_num_pixels = encoder_desired_num_pixels_; + if (KEEP != request) { + int new_encoder_desired_num_pixels = width * height; + int old_num_pixels = GetOutputNumPixels(); + if (new_encoder_desired_num_pixels != old_num_pixels) { + LOG(LS_VERBOSE) << "VAdapt GD resolution stale. Ignored"; + } else { + // Update the encoder desired format based on the request. + encoder_desired_num_pixels_ = new_encoder_desired_num_pixels; + StepPixelCount(request, &encoder_desired_num_pixels_); + } + } + int new_width, new_height; + bool changed = AdaptToMinimumFormat(&new_width, &new_height); + + // Ignore up or keep if no change. + if (DOWNGRADE != request && view_switch_ && !changed) { + encoder_desired_num_pixels_ = old_encoder_desired_num_pixels; + LOG(LS_VERBOSE) << "VAdapt ignoring GD request."; + } + + LOG(LS_INFO) << "VAdapt GD Request: " + << (DOWNGRADE == request ? "down" : + (UPGRADE == request ? "up" : "keep")) + << " From: " << width << "x" << height + << " Pixels: " << encoder_desired_num_pixels_ + << " Changed: " << (changed ? "true" : "false") + << " To: " << new_width << "x" << new_height; +} + +// A CPU request for new resolution +void CoordinatedVideoAdapter::OnCpuLoadUpdated( + int current_cpus, int max_cpus, float process_load, float system_load) { + talk_base::CritScope cs(&request_critical_section_); + if (!cpu_adaptation_) { + return; + } + AdaptRequest request = FindCpuRequest(current_cpus, max_cpus, + process_load, system_load); + // Update how many times we have downgraded due to the cpu load. + switch (request) { + case DOWNGRADE: + if (cpu_downgrade_count_ < kMaxCpuDowngrades) { + // Ignore downgrades if we have downgraded the maximum times or we just + // downgraded in a short time. + if (cpu_downgrade_wait_time_ != 0 && + talk_base::TimeIsLater(talk_base::Time(), + cpu_downgrade_wait_time_)) { + LOG(LS_VERBOSE) << "VAdapt CPU load high but do not downgrade until " + << talk_base::TimeUntil(cpu_downgrade_wait_time_) + << " ms."; + request = KEEP; + } else { + ++cpu_downgrade_count_; + } + } else { + LOG(LS_VERBOSE) << "VAdapt CPU load high but do not downgrade " + "because maximum downgrades reached"; + SignalCpuAdaptationUnable(); + } + break; + case UPGRADE: + if (cpu_downgrade_count_ > 0) { + bool is_min = IsMinimumFormat(cpu_desired_num_pixels_); + if (is_min) { + --cpu_downgrade_count_; + } else { + LOG(LS_VERBOSE) << "VAdapt CPU load low but do not upgrade " + "because cpu is not limiting resolution"; + } + } else { + LOG(LS_VERBOSE) << "VAdapt CPU load low but do not upgrade " + "because minimum downgrades reached"; + } + break; + case KEEP: + default: + break; + } + if (KEEP != request) { + // TODO(fbarchard): compute stepping up/down from OutputNumPixels but + // clamp to inputpixels / 4 (2 steps) + cpu_desired_num_pixels_ = cpu_downgrade_count_ == 0 ? INT_MAX : + static_cast(input_format().width * input_format().height >> + cpu_downgrade_count_); + } + int new_width, new_height; + bool changed = AdaptToMinimumFormat(&new_width, &new_height); + LOG(LS_INFO) << "VAdapt CPU Request: " + << (DOWNGRADE == request ? "down" : + (UPGRADE == request ? "up" : "keep")) + << " Process: " << process_load + << " System: " << system_load + << " Steps: " << cpu_downgrade_count_ + << " Changed: " << (changed ? "true" : "false") + << " To: " << new_width << "x" << new_height; +} + +// Called by cpu adapter on up requests. +bool CoordinatedVideoAdapter::IsMinimumFormat(int pixels) { + // Find closest scale factor that matches input resolution to min_num_pixels + // and set that for output resolution. This is not needed for VideoAdapter, + // but provides feedback to unittests and users on expected resolution. + // Actual resolution is based on input frame. + VideoFormat new_output = output_format(); + VideoFormat input = input_format(); + if (input_format().IsSize0x0()) { + input = new_output; + } + float scale = 1.0f; + if (!input.IsSize0x0()) { + scale = FindClosestScale(input.width, + input.height, + pixels); + } + new_output.width = static_cast(input.width * scale + .5f); + new_output.height = static_cast(input.height * scale + .5f); + int new_pixels = new_output.width * new_output.height; + int num_pixels = GetOutputNumPixels(); + return new_pixels <= num_pixels; +} + +// Called by all coordinators when there is a change. +bool CoordinatedVideoAdapter::AdaptToMinimumFormat(int* new_width, + int* new_height) { + VideoFormat new_output = output_format(); + VideoFormat input = input_format(); + if (input_format().IsSize0x0()) { + input = new_output; + } + int old_num_pixels = GetOutputNumPixels(); + // Find resolution that respects ViewRequest or less pixels. + int view_desired_num_pixels = view_desired_num_pixels_; + int min_num_pixels = view_desired_num_pixels_; + if (!input.IsSize0x0()) { + float scale = FindLowerScale(input.width, input.height, min_num_pixels); + min_num_pixels = view_desired_num_pixels = + static_cast(input.width * input.height * scale * scale + .5f); + } + // Reduce resolution further, if necessary, based on encoder bandwidth (GD). + if (encoder_desired_num_pixels_ && + (encoder_desired_num_pixels_ < min_num_pixels)) { + min_num_pixels = encoder_desired_num_pixels_; + } + // Reduce resolution further, if necessary, based on CPU. + if (cpu_adaptation_ && cpu_desired_num_pixels_ && + (cpu_desired_num_pixels_ < min_num_pixels)) { + min_num_pixels = cpu_desired_num_pixels_; + // Update the cpu_downgrade_wait_time_ if we are going to downgrade video. + cpu_downgrade_wait_time_ = + talk_base::TimeAfter(kDefaultDowngradeWaitTimeMs); + } + + // Determine which factors are keeping adapter resolution low. + // Caveat: Does not consider framerate. + adapt_reason_ = static_cast(0); + if (view_desired_num_pixels == min_num_pixels) { + adapt_reason_ |= ADAPTREASON_VIEW; + } + if (encoder_desired_num_pixels_ == min_num_pixels) { + adapt_reason_ |= ADAPTREASON_BANDWIDTH; + } + if (cpu_desired_num_pixels_ == min_num_pixels) { + adapt_reason_ |= ADAPTREASON_CPU; + } + + // Prevent going below QQVGA. + if (min_num_pixels > 0 && min_num_pixels < kMinNumPixels) { + min_num_pixels = kMinNumPixels; + } + SetOutputNumPixels(min_num_pixels); + + // Find closest scale factor that matches input resolution to min_num_pixels + // and set that for output resolution. This is not needed for VideoAdapter, + // but provides feedback to unittests and users on expected resolution. + // Actual resolution is based on input frame. + float scale = 1.0f; + if (!input.IsSize0x0()) { + scale = FindClosestScale(input.width, input.height, min_num_pixels); + } + if (scale == 1.0f) { + adapt_reason_ = 0; + } + *new_width = new_output.width = static_cast(input.width * scale + .5f); + *new_height = new_output.height = static_cast(input.height * scale + + .5f); + new_output.interval = view_desired_interval_; + SetOutputFormat(new_output); + int new_num_pixels = GetOutputNumPixels(); + bool changed = new_num_pixels != old_num_pixels; + + static const char* kReasons[8] = { + "None", + "CPU", + "BANDWIDTH", + "CPU+BANDWIDTH", + "VIEW", + "CPU+VIEW", + "BANDWIDTH+VIEW", + "CPU+BANDWIDTH+VIEW", + }; + + LOG(LS_VERBOSE) << "VAdapt Status View: " << view_desired_num_pixels_ + << " GD: " << encoder_desired_num_pixels_ + << " CPU: " << cpu_desired_num_pixels_ + << " Pixels: " << min_num_pixels + << " Input: " << input.width + << "x" << input.height + << " Scale: " << scale + << " Resolution: " << new_output.width + << "x" << new_output.height + << " Changed: " << (changed ? "true" : "false") + << " Reason: " << kReasons[adapt_reason_]; + return changed; +} + +} // namespace cricket diff --git a/talk/media/base/videoadapter.h b/talk/media/base/videoadapter.h new file mode 100644 index 000000000..14829abab --- /dev/null +++ b/talk/media/base/videoadapter.h @@ -0,0 +1,213 @@ +// libjingle +// Copyright 2010 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. + +#ifndef TALK_MEDIA_BASE_VIDEOADAPTER_H_ // NOLINT +#define TALK_MEDIA_BASE_VIDEOADAPTER_H_ + +#include "talk/base/common.h" // For ASSERT +#include "talk/base/criticalsection.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" +#include "talk/media/base/videocommon.h" + +namespace cricket { + +class VideoFrame; + +// VideoAdapter adapts an input video frame to an output frame based on the +// specified input and output formats. The adaptation includes dropping frames +// to reduce frame rate and scaling frames. VideoAdapter is thread safe. +class VideoAdapter { + public: + VideoAdapter(); + virtual ~VideoAdapter(); + + void SetInputFormat(const VideoFrame& in_frame); + void SetInputFormat(const VideoFormat& format); + void SetOutputFormat(const VideoFormat& format); + // Constrain output resolution to this many pixels overall + void SetOutputNumPixels(int num_pixels); + int GetOutputNumPixels() const; + + const VideoFormat& input_format(); + const VideoFormat& output_format(); + // If the parameter black is true, the adapted frames will be black. + void SetBlackOutput(bool black); + + // Adapt the input frame from the input format to the output format. Return + // true and set the output frame to NULL if the input frame is dropped. Return + // true and set the out frame to output_frame_ if the input frame is adapted + // successfully. Return false otherwise. + // output_frame_ is owned by the VideoAdapter that has the best knowledge on + // the output frame. + bool AdaptFrame(const VideoFrame* in_frame, const VideoFrame** out_frame); + + protected: + float FindClosestScale(int width, int height, int target_num_pixels); + float FindLowerScale(int width, int height, int target_num_pixels); + + private: + bool StretchToOutputFrame(const VideoFrame* in_frame); + + VideoFormat input_format_; + VideoFormat output_format_; + int output_num_pixels_; + bool black_output_; // Flag to tell if we need to black output_frame_. + bool is_black_; // Flag to tell if output_frame_ is currently black. + int64 interval_next_frame_; + talk_base::scoped_ptr output_frame_; + // The critical section to protect the above variables. + talk_base::CriticalSection critical_section_; + + DISALLOW_COPY_AND_ASSIGN(VideoAdapter); +}; + +// CoordinatedVideoAdapter adapts the video input to the encoder by coordinating +// the format request from the server, the resolution request from the encoder, +// and the CPU load. +class CoordinatedVideoAdapter + : public VideoAdapter, public sigslot::has_slots<> { + public: + enum AdaptRequest { UPGRADE, KEEP, DOWNGRADE }; + enum { + ADAPTREASON_CPU = 1, + ADAPTREASON_BANDWIDTH = 2, + ADAPTREASON_VIEW = 4 + }; + typedef int AdaptReason; + + CoordinatedVideoAdapter(); + virtual ~CoordinatedVideoAdapter() {} + + // Enable or disable video adaptation due to the change of the CPU load. + void set_cpu_adaptation(bool enable) { cpu_adaptation_ = enable; } + bool cpu_adaptation() const { return cpu_adaptation_; } + // Enable or disable video adaptation due to the change of the GD + void set_gd_adaptation(bool enable) { gd_adaptation_ = enable; } + bool gd_adaptation() const { return gd_adaptation_; } + // Enable or disable video adaptation due to the change of the View + void set_view_adaptation(bool enable) { view_adaptation_ = enable; } + bool view_adaptation() const { return view_adaptation_; } + // Enable or disable video adaptation to fast switch View + void set_view_switch(bool enable) { view_switch_ = enable; } + bool view_switch() const { return view_switch_; } + + CoordinatedVideoAdapter::AdaptReason adapt_reason() const { + return adapt_reason_; + } + + // When the video is decreased, set the waiting time for CPU adaptation to + // decrease video again. + void set_cpu_downgrade_wait_time(uint32 cpu_downgrade_wait_time) { + if (cpu_downgrade_wait_time_ != static_cast(cpu_downgrade_wait_time)) { + LOG(LS_INFO) << "VAdapt Change Cpu Downgrade Wait Time from: " + << cpu_downgrade_wait_time_ << " to " + << cpu_downgrade_wait_time; + cpu_downgrade_wait_time_ = static_cast(cpu_downgrade_wait_time); + } + } + // CPU system load high threshold for reducing resolution. e.g. 0.85f + void set_high_system_threshold(float high_system_threshold) { + ASSERT(high_system_threshold <= 1.0f); + ASSERT(high_system_threshold >= 0.0f); + if (high_system_threshold_ != high_system_threshold) { + LOG(LS_INFO) << "VAdapt Change High System Threshold from: " + << high_system_threshold_ << " to " << high_system_threshold; + high_system_threshold_ = high_system_threshold; + } + } + float high_system_threshold() const { return high_system_threshold_; } + // CPU system load low threshold for increasing resolution. e.g. 0.70f + void set_low_system_threshold(float low_system_threshold) { + ASSERT(low_system_threshold <= 1.0f); + ASSERT(low_system_threshold >= 0.0f); + if (low_system_threshold_ != low_system_threshold) { + LOG(LS_INFO) << "VAdapt Change Low System Threshold from: " + << low_system_threshold_ << " to " << low_system_threshold; + low_system_threshold_ = low_system_threshold; + } + } + float low_system_threshold() const { return low_system_threshold_; } + // CPU process load threshold for reducing resolution. e.g. 0.10f + void set_process_threshold(float process_threshold) { + ASSERT(process_threshold <= 1.0f); + ASSERT(process_threshold >= 0.0f); + if (process_threshold_ != process_threshold) { + LOG(LS_INFO) << "VAdapt Change High Process Threshold from: " + << process_threshold_ << " to " << process_threshold; + process_threshold_ = process_threshold; + } + } + float process_threshold() const { return process_threshold_; } + + // Handle the format request from the server via Jingle update message. + void OnOutputFormatRequest(const VideoFormat& format); + // Handle the resolution request from the encoder due to bandwidth changes. + void OnEncoderResolutionRequest(int width, int height, AdaptRequest request); + // Handle the CPU load provided by a CPU monitor. + void OnCpuLoadUpdated(int current_cpus, int max_cpus, + float process_load, float system_load); + + sigslot::signal0<> SignalCpuAdaptationUnable; + + private: + // Adapt to the minimum of the formats the server requests, the CPU wants, and + // the encoder wants. Returns true if resolution changed. + bool AdaptToMinimumFormat(int* new_width, int* new_height); + bool IsMinimumFormat(int pixels); + void StepPixelCount(CoordinatedVideoAdapter::AdaptRequest request, + int* num_pixels); + CoordinatedVideoAdapter::AdaptRequest FindCpuRequest( + int current_cpus, int max_cpus, + float process_load, float system_load); + + bool cpu_adaptation_; // True if cpu adaptation is enabled. + bool gd_adaptation_; // True if gd adaptation is enabled. + bool view_adaptation_; // True if view adaptation is enabled. + bool view_switch_; // True if view switch is enabled. + int cpu_downgrade_count_; + int cpu_downgrade_wait_time_; + // cpu system load thresholds relative to max cpus. + float high_system_threshold_; + float low_system_threshold_; + // cpu process load thresholds relative to current cpus. + float process_threshold_; + // Video formats that the server view requests, the CPU wants, and the encoder + // wants respectively. The adapted output format is the minimum of these. + int view_desired_num_pixels_; + int64 view_desired_interval_; + int encoder_desired_num_pixels_; + int cpu_desired_num_pixels_; + CoordinatedVideoAdapter::AdaptReason adapt_reason_; + // The critical section to protect handling requests. + talk_base::CriticalSection request_critical_section_; + + DISALLOW_COPY_AND_ASSIGN(CoordinatedVideoAdapter); +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_VIDEOADAPTER_H_ // NOLINT diff --git a/talk/media/base/videocapturer.cc b/talk/media/base/videocapturer.cc new file mode 100644 index 000000000..3bc23731d --- /dev/null +++ b/talk/media/base/videocapturer.cc @@ -0,0 +1,580 @@ +// libjingle +// Copyright 2010 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. +// +// Implementation file of class VideoCapturer. + +#include "talk/media/base/videocapturer.h" + +#include + +#if !defined(DISABLE_YUV) +#include "libyuv/scale_argb.h" +#endif +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/systeminfo.h" +#include "talk/media/base/videoprocessor.h" + +#if defined(HAVE_WEBRTC_VIDEO) +#include "talk/media/webrtc/webrtcvideoframe.h" +#endif // HAVE_WEBRTC_VIDEO + + +namespace cricket { + +namespace { + +// TODO(thorcarpenter): This is a BIG hack to flush the system with black +// frames. Frontends should coordinate to update the video state of a muted +// user. When all frontends to this consider removing the black frame business. +const int kNumBlackFramesOnMute = 30; + +// MessageHandler constants. +enum { + MSG_DO_PAUSE = 0, + MSG_DO_UNPAUSE, + MSG_STATE_CHANGE +}; + +static const int64 kMaxDistance = ~(static_cast(1) << 63); +static const int kYU12Penalty = 16; // Needs to be higher than MJPG index. +static const int kDefaultScreencastFps = 5; +typedef talk_base::TypedMessageData StateChangeParams; + +} // namespace + +///////////////////////////////////////////////////////////////////// +// Implementation of struct CapturedFrame +///////////////////////////////////////////////////////////////////// +CapturedFrame::CapturedFrame() + : width(0), + height(0), + fourcc(0), + pixel_width(0), + pixel_height(0), + elapsed_time(0), + time_stamp(0), + data_size(0), + rotation(0), + data(NULL) {} + +// TODO(fbarchard): Remove this function once lmimediaengine stops using it. +bool CapturedFrame::GetDataSize(uint32* size) const { + if (!size || data_size == CapturedFrame::kUnknownDataSize) { + return false; + } + *size = data_size; + return true; +} + +///////////////////////////////////////////////////////////////////// +// Implementation of class VideoCapturer +///////////////////////////////////////////////////////////////////// +VideoCapturer::VideoCapturer() : thread_(talk_base::Thread::Current()) { + Construct(); +} + +VideoCapturer::VideoCapturer(talk_base::Thread* thread) : thread_(thread) { + Construct(); +} + +void VideoCapturer::Construct() { + ClearAspectRatio(); + enable_camera_list_ = false; + capture_state_ = CS_STOPPED; + SignalFrameCaptured.connect(this, &VideoCapturer::OnFrameCaptured); + scaled_width_ = 0; + scaled_height_ = 0; + muted_ = false; + black_frame_count_down_ = kNumBlackFramesOnMute; +} + +const std::vector* VideoCapturer::GetSupportedFormats() const { + return &filtered_supported_formats_; +} + +bool VideoCapturer::StartCapturing(const VideoFormat& capture_format) { + CaptureState result = Start(capture_format); + const bool success = (result == CS_RUNNING) || (result == CS_STARTING); + if (!success) { + return false; + } + if (result == CS_RUNNING) { + SetCaptureState(result); + } + return true; +} + +void VideoCapturer::UpdateAspectRatio(int ratio_w, int ratio_h) { + if (ratio_w == 0 || ratio_h == 0) { + LOG(LS_WARNING) << "UpdateAspectRatio ignored invalid ratio: " + << ratio_w << "x" << ratio_h; + return; + } + ratio_w_ = ratio_w; + ratio_h_ = ratio_h; +} + +void VideoCapturer::ClearAspectRatio() { + ratio_w_ = 0; + ratio_h_ = 0; +} + +// Override this to have more control of how your device is started/stopped. +bool VideoCapturer::Pause(bool pause) { + if (pause) { + if (capture_state() == CS_PAUSED) { + return true; + } + bool is_running = capture_state() == CS_STARTING || + capture_state() == CS_RUNNING; + if (!is_running) { + LOG(LS_ERROR) << "Cannot pause a stopped camera."; + return false; + } + LOG(LS_INFO) << "Pausing a camera."; + talk_base::scoped_ptr capture_format_when_paused( + capture_format_ ? new VideoFormat(*capture_format_) : NULL); + Stop(); + SetCaptureState(CS_PAUSED); + // If you override this function be sure to restore the capture format + // after calling Stop(). + SetCaptureFormat(capture_format_when_paused.get()); + } else { // Unpause. + if (capture_state() != CS_PAUSED) { + LOG(LS_WARNING) << "Cannot unpause a camera that hasn't been paused."; + return false; + } + if (!capture_format_) { + LOG(LS_ERROR) << "Missing capture_format_, cannot unpause a camera."; + return false; + } + if (muted_) { + LOG(LS_WARNING) << "Camera cannot be unpaused while muted."; + return false; + } + LOG(LS_INFO) << "Unpausing a camera."; + if (!Start(*capture_format_)) { + LOG(LS_ERROR) << "Camera failed to start when unpausing."; + return false; + } + } + return true; +} + +bool VideoCapturer::Restart(const VideoFormat& capture_format) { + if (!IsRunning()) { + return StartCapturing(capture_format); + } + + if (GetCaptureFormat() != NULL && *GetCaptureFormat() == capture_format) { + // The reqested format is the same; nothing to do. + return true; + } + + Stop(); + return StartCapturing(capture_format); +} + +bool VideoCapturer::MuteToBlackThenPause(bool muted) { + if (muted == IsMuted()) { + return true; + } + + LOG(LS_INFO) << (muted ? "Muting" : "Unmuting") << " this video capturer."; + muted_ = muted; // Do this before calling Pause(). + if (muted) { + // Reset black frame count down. + black_frame_count_down_ = kNumBlackFramesOnMute; + // Following frames will be overritten with black, then the camera will be + // paused. + return true; + } + // Start the camera. + thread_->Clear(this, MSG_DO_PAUSE); + return Pause(false); +} + +void VideoCapturer::SetSupportedFormats( + const std::vector& formats) { + supported_formats_ = formats; + UpdateFilteredSupportedFormats(); +} + +bool VideoCapturer::GetBestCaptureFormat(const VideoFormat& format, + VideoFormat* best_format) { + // TODO(fbarchard): Directly support max_format. + UpdateFilteredSupportedFormats(); + const std::vector* supported_formats = GetSupportedFormats(); + + if (supported_formats->empty()) { + return false; + } + LOG(LS_INFO) << " Capture Requested " << format.ToString(); + int64 best_distance = kMaxDistance; + std::vector::const_iterator best = supported_formats->end(); + std::vector::const_iterator i; + for (i = supported_formats->begin(); i != supported_formats->end(); ++i) { + int64 distance = GetFormatDistance(format, *i); + // TODO(fbarchard): Reduce to LS_VERBOSE if/when camera capture is + // relatively bug free. + LOG(LS_INFO) << " Supported " << i->ToString() << " distance " << distance; + if (distance < best_distance) { + best_distance = distance; + best = i; + } + } + if (supported_formats->end() == best) { + LOG(LS_ERROR) << " No acceptable camera format found"; + return false; + } + + if (best_format) { + best_format->width = best->width; + best_format->height = best->height; + best_format->fourcc = best->fourcc; + best_format->interval = talk_base::_max(format.interval, best->interval); + LOG(LS_INFO) << " Best " << best_format->ToString() << " Interval " + << best_format->interval << " distance " << best_distance; + } + return true; +} + +void VideoCapturer::AddVideoProcessor(VideoProcessor* video_processor) { + talk_base::CritScope cs(&crit_); + ASSERT(std::find(video_processors_.begin(), video_processors_.end(), + video_processor) == video_processors_.end()); + video_processors_.push_back(video_processor); +} + +bool VideoCapturer::RemoveVideoProcessor(VideoProcessor* video_processor) { + talk_base::CritScope cs(&crit_); + VideoProcessors::iterator found = std::find( + video_processors_.begin(), video_processors_.end(), video_processor); + if (found == video_processors_.end()) { + return false; + } + video_processors_.erase(found); + return true; +} + +void VideoCapturer::ConstrainSupportedFormats(const VideoFormat& max_format) { + max_format_.reset(new VideoFormat(max_format)); + LOG(LS_VERBOSE) << " ConstrainSupportedFormats " << max_format.ToString(); + UpdateFilteredSupportedFormats(); +} + +std::string VideoCapturer::ToString(const CapturedFrame* captured_frame) const { + std::string fourcc_name = GetFourccName(captured_frame->fourcc) + " "; + for (std::string::const_iterator i = fourcc_name.begin(); + i < fourcc_name.end(); ++i) { + // Test character is printable; Avoid isprint() which asserts on negatives. + if (*i < 32 || *i >= 127) { + fourcc_name = ""; + break; + } + } + + std::ostringstream ss; + ss << fourcc_name << captured_frame->width << "x" << captured_frame->height + << "x" << VideoFormat::IntervalToFps(captured_frame->elapsed_time); + return ss.str(); +} + +void VideoCapturer::OnFrameCaptured(VideoCapturer*, + const CapturedFrame* captured_frame) { + if (muted_) { + if (black_frame_count_down_ == 0) { + thread_->Post(this, MSG_DO_PAUSE, NULL); + } else { + --black_frame_count_down_; + } + } + + if (SignalVideoFrame.is_empty()) { + return; + } +#if defined(HAVE_WEBRTC_VIDEO) +#define VIDEO_FRAME_NAME WebRtcVideoFrame +#endif +#if defined(VIDEO_FRAME_NAME) +#if !defined(DISABLE_YUV) + if (IsScreencast()) { + int scaled_width, scaled_height; + int desired_screencast_fps = capture_format_.get() ? + VideoFormat::IntervalToFps(capture_format_->interval) : + kDefaultScreencastFps; + ComputeScale(captured_frame->width, captured_frame->height, + desired_screencast_fps, &scaled_width, &scaled_height); + + if (scaled_width != scaled_width_ || scaled_height != scaled_height_) { + LOG(LS_VERBOSE) << "Scaling Screencast from " + << captured_frame->width << "x" + << captured_frame->height << " to " + << scaled_width << "x" << scaled_height; + scaled_width_ = scaled_width; + scaled_height_ = scaled_height; + } + if (FOURCC_ARGB == captured_frame->fourcc && + (scaled_width != captured_frame->height || + scaled_height != captured_frame->height)) { + CapturedFrame* scaled_frame = const_cast(captured_frame); + // Compute new width such that width * height is less than maximum but + // maintains original captured frame aspect ratio. + // Round down width to multiple of 4 so odd width won't round up beyond + // maximum, and so chroma channel is even width to simplify spatial + // resampling. + libyuv::ARGBScale(reinterpret_cast(captured_frame->data), + captured_frame->width * 4, captured_frame->width, + captured_frame->height, + reinterpret_cast(scaled_frame->data), + scaled_width * 4, scaled_width, scaled_height, + libyuv::kFilterBilinear); + scaled_frame->width = scaled_width; + scaled_frame->height = scaled_height; + scaled_frame->data_size = scaled_width * 4 * scaled_height; + } + } +#endif // !DISABLE_YUV + // Size to crop captured frame to. This adjusts the captured frames + // aspect ratio to match the final view aspect ratio, considering pixel + // aspect ratio and rotation. The final size may be scaled down by video + // adapter to better match ratio_w_ x ratio_h_. + // Note that abs() of frame height is passed in, because source may be + // inverted, but output will be positive. + int desired_width = captured_frame->width; + int desired_height = captured_frame->height; + + // TODO(fbarchard): Improve logic to pad or crop. + // MJPG can crop vertically, but not horizontally. This logic disables crop. + // Alternatively we could pad the image with black, or implement a 2 step + // crop. + bool can_crop = true; + if (captured_frame->fourcc == FOURCC_MJPG) { + float cam_aspect = static_cast(captured_frame->width) / + static_cast(captured_frame->height); + float view_aspect = static_cast(ratio_w_) / + static_cast(ratio_h_); + can_crop = cam_aspect <= view_aspect; + } + if (can_crop && !IsScreencast()) { + // TODO(ronghuawu): The capturer should always produce the native + // resolution and the cropping should be done in downstream code. + ComputeCrop(ratio_w_, ratio_h_, captured_frame->width, + abs(captured_frame->height), captured_frame->pixel_width, + captured_frame->pixel_height, captured_frame->rotation, + &desired_width, &desired_height); + } + + VIDEO_FRAME_NAME i420_frame; + if (!i420_frame.Init(captured_frame, desired_width, desired_height)) { + // TODO(fbarchard): LOG more information about captured frame attributes. + LOG(LS_ERROR) << "Couldn't convert to I420! " + << "From " << ToString(captured_frame) << " To " + << desired_width << " x " << desired_height; + return; + } + if (!muted_ && !ApplyProcessors(&i420_frame)) { + // Processor dropped the frame. + return; + } + if (muted_) { + i420_frame.SetToBlack(); + } + SignalVideoFrame(this, &i420_frame); +#endif // VIDEO_FRAME_NAME +} + +void VideoCapturer::SetCaptureState(CaptureState state) { + if (state == capture_state_) { + // Don't trigger a state changed callback if the state hasn't changed. + return; + } + StateChangeParams* state_params = new StateChangeParams(state); + capture_state_ = state; + thread_->Post(this, MSG_STATE_CHANGE, state_params); +} + +void VideoCapturer::OnMessage(talk_base::Message* message) { + switch (message->message_id) { + case MSG_STATE_CHANGE: { + talk_base::scoped_ptr p( + static_cast(message->pdata)); + SignalStateChange(this, p->data()); + break; + } + case MSG_DO_PAUSE: { + Pause(true); + break; + } + case MSG_DO_UNPAUSE: { + Pause(false); + break; + } + default: { + ASSERT(false); + } + } +} + +// Get the distance between the supported and desired formats. +// Prioritization is done according to this algorithm: +// 1) Width closeness. If not same, we prefer wider. +// 2) Height closeness. If not same, we prefer higher. +// 3) Framerate closeness. If not same, we prefer faster. +// 4) Compression. If desired format has a specific fourcc, we need exact match; +// otherwise, we use preference. +int64 VideoCapturer::GetFormatDistance(const VideoFormat& desired, + const VideoFormat& supported) { + int64 distance = kMaxDistance; + + // Check fourcc. + uint32 supported_fourcc = CanonicalFourCC(supported.fourcc); + int64 delta_fourcc = kMaxDistance; + if (FOURCC_ANY == desired.fourcc) { + // Any fourcc is OK for the desired. Use preference to find best fourcc. + std::vector preferred_fourccs; + if (!GetPreferredFourccs(&preferred_fourccs)) { + return distance; + } + + for (size_t i = 0; i < preferred_fourccs.size(); ++i) { + if (supported_fourcc == CanonicalFourCC(preferred_fourccs[i])) { + delta_fourcc = i; +#ifdef LINUX + // For HD avoid YU12 which is a software conversion and has 2 bugs + // b/7326348 b/6960899. Reenable when fixed. + if (supported.height >= 720 && (supported_fourcc == FOURCC_YU12 || + supported_fourcc == FOURCC_YV12)) { + delta_fourcc += kYU12Penalty; + } +#endif + break; + } + } + } else if (supported_fourcc == CanonicalFourCC(desired.fourcc)) { + delta_fourcc = 0; // Need exact match. + } + + if (kMaxDistance == delta_fourcc) { + // Failed to match fourcc. + return distance; + } + + // Check resolution and fps. + int desired_width = desired.width; + int desired_height = desired.height; + int64 delta_w = supported.width - desired_width; + int64 supported_fps = VideoFormat::IntervalToFps(supported.interval); + int64 delta_fps = + supported_fps - VideoFormat::IntervalToFps(desired.interval); + // Check height of supported height compared to height we would like it to be. + int64 aspect_h = + desired_width ? supported.width * desired_height / desired_width + : desired_height; + int64 delta_h = supported.height - aspect_h; + + distance = 0; + // Set high penalty if the supported format is lower than the desired format. + // 3x means we would prefer down to down to 3/4, than up to double. + // But we'd prefer up to double than down to 1/2. This is conservative, + // strongly avoiding going down in resolution, similar to + // the old method, but not completely ruling it out in extreme situations. + // It also ignores framerate, which is often very low at high resolutions. + // TODO(fbarchard): Improve logic to use weighted factors. + static const int kDownPenalty = -3; + if (delta_w < 0) { + delta_w = delta_w * kDownPenalty; + } + if (delta_h < 0) { + delta_h = delta_h * kDownPenalty; + } + // Require camera fps to be at least 80% of what is requested if resolution + // matches. + // Require camera fps to be at least 96% of what is requested, or higher, + // if resolution differs. 96% allows for slight variations in fps. e.g. 29.97 + if (delta_fps < 0) { + int64 min_desirable_fps = delta_w ? + VideoFormat::IntervalToFps(desired.interval) * 29 / 30 : + VideoFormat::IntervalToFps(desired.interval) * 24 / 30; + delta_fps = -delta_fps; + if (supported_fps < min_desirable_fps) { + distance |= static_cast(1) << 62; + } else { + distance |= static_cast(1) << 15; + } + } + + // 12 bits for width and height and 8 bits for fps and fourcc. + distance |= + (delta_w << 28) | (delta_h << 16) | (delta_fps << 8) | delta_fourcc; + + return distance; +} + +bool VideoCapturer::ApplyProcessors(VideoFrame* video_frame) { + bool drop_frame = false; + talk_base::CritScope cs(&crit_); + for (VideoProcessors::iterator iter = video_processors_.begin(); + iter != video_processors_.end(); ++iter) { + (*iter)->OnFrame(kDummyVideoSsrc, video_frame, &drop_frame); + if (drop_frame) { + return false; + } + } + return true; +} + +void VideoCapturer::UpdateFilteredSupportedFormats() { + filtered_supported_formats_.clear(); + filtered_supported_formats_ = supported_formats_; + if (!max_format_) { + return; + } + std::vector::iterator iter = filtered_supported_formats_.begin(); + while (iter != filtered_supported_formats_.end()) { + if (ShouldFilterFormat(*iter)) { + iter = filtered_supported_formats_.erase(iter); + } else { + ++iter; + } + } + if (filtered_supported_formats_.empty()) { + // The device only captures at resolutions higher than |max_format_| this + // indicates that |max_format_| should be ignored as it is better to capture + // at too high a resolution than to not capture at all. + filtered_supported_formats_ = supported_formats_; + } +} + +bool VideoCapturer::ShouldFilterFormat(const VideoFormat& format) const { + if (!enable_camera_list_) { + return false; + } + return format.width > max_format_->width || + format.height > max_format_->height; +} + +} // namespace cricket diff --git a/talk/media/base/videocapturer.h b/talk/media/base/videocapturer.h new file mode 100644 index 000000000..3997976f8 --- /dev/null +++ b/talk/media/base/videocapturer.h @@ -0,0 +1,327 @@ +// libjingle +// Copyright 2010 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. +// +// Declaration of abstract class VideoCapturer + +#ifndef TALK_MEDIA_BASE_VIDEOCAPTURER_H_ +#define TALK_MEDIA_BASE_VIDEOCAPTURER_H_ + +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/criticalsection.h" +#include "talk/base/messagehandler.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" +#include "talk/base/thread.h" +#include "talk/media/base/videocommon.h" +#include "talk/media/devices/devicemanager.h" + + +namespace cricket { + +class VideoProcessor; + +// Current state of the capturer. +// TODO(hellner): CS_NO_DEVICE is an error code not a capture state. Separate +// error codes and states. +enum CaptureState { + CS_STOPPED, // The capturer has been stopped or hasn't started yet. + CS_STARTING, // The capturer is in the process of starting. Note, it may + // still fail to start. + CS_RUNNING, // The capturer has been started successfully and is now + // capturing. + CS_PAUSED, // The capturer has been paused. + CS_FAILED, // The capturer failed to start. + CS_NO_DEVICE, // The capturer has no device and consequently failed to start. +}; + +class VideoFrame; + +struct CapturedFrame { + static const uint32 kFrameHeaderSize = 40; // Size from width to data_size. + static const uint32 kUnknownDataSize = 0xFFFFFFFF; + + CapturedFrame(); + + // Get the number of bytes of the frame data. If data_size is known, return + // it directly. Otherwise, calculate the size based on width, height, and + // fourcc. Return true if succeeded. + bool GetDataSize(uint32* size) const; + + // The width and height of the captured frame could be different from those + // of VideoFormat. Once the first frame is captured, the width, height, + // fourcc, pixel_width, and pixel_height should keep the same over frames. + int width; // in number of pixels + int height; // in number of pixels + uint32 fourcc; // compression + uint32 pixel_width; // width of a pixel, default is 1 + uint32 pixel_height; // height of a pixel, default is 1 + int64 elapsed_time; // elapsed time since the creation of the frame + // source (that is, the camera), in nanoseconds. + int64 time_stamp; // timestamp of when the frame was captured, in unix + // time with nanosecond units. + uint32 data_size; // number of bytes of the frame data + int rotation; // rotation in degrees of the frame (0, 90, 180, 270) + void* data; // pointer to the frame data. This object allocates the + // memory or points to an existing memory. + + private: + DISALLOW_COPY_AND_ASSIGN(CapturedFrame); +}; + +// VideoCapturer is an abstract class that defines the interfaces for video +// capturing. The subclasses implement the video capturer for various types of +// capturers and various platforms. +// +// The captured frames may need to be adapted (for example, cropping). Adaptors +// can be registered to the capturer or applied externally to the capturer. +// If the adaptor is needed, it acts as the downstream of VideoCapturer, adapts +// the captured frames, and delivers the adapted frames to other components +// such as the encoder. Effects can also be registered to the capturer or +// applied externally. +// +// Programming model: +// Create an object of a subclass of VideoCapturer +// Initialize +// SignalStateChange.connect() +// SignalFrameCaptured.connect() +// Find the capture format for Start() by either calling GetSupportedFormats() +// and selecting one of the supported or calling GetBestCaptureFormat(). +// Start() +// GetCaptureFormat() optionally +// Stop() +// +// Assumption: +// The Start() and Stop() methods are called by a single thread (E.g., the +// media engine thread). Hence, the VideoCapture subclasses dont need to be +// thread safe. +// +class VideoCapturer + : public sigslot::has_slots<>, + public talk_base::MessageHandler { + public: + typedef std::vector VideoProcessors; + + // All signals are marshalled to |thread| or the creating thread if + // none is provided. + VideoCapturer(); + explicit VideoCapturer(talk_base::Thread* thread); + virtual ~VideoCapturer() {} + + // Gets the id of the underlying device, which is available after the capturer + // is initialized. Can be used to determine if two capturers reference the + // same device. + const std::string& GetId() const { return id_; } + + // Get the capture formats supported by the video capturer. The supported + // formats are non empty after the device has been opened successfully. + const std::vector* GetSupportedFormats() const; + + // Get the best capture format for the desired format. The best format is the + // same as one of the supported formats except that the frame interval may be + // different. If the application asks for 16x9 and the camera does not support + // 16x9 HD or the application asks for 16x10, we find the closest 4x3 and then + // crop; Otherwise, we find what the application asks for. Note that we assume + // that for HD, the desired format is always 16x9. The subclasses can override + // the default implementation. + // Parameters + // desired: the input desired format. If desired.fourcc is not kAnyFourcc, + // the best capture format has the exactly same fourcc. Otherwise, + // the best capture format uses a fourcc in GetPreferredFourccs(). + // best_format: the output of the best capture format. + // Return false if there is no such a best format, that is, the desired format + // is not supported. + virtual bool GetBestCaptureFormat(const VideoFormat& desired, + VideoFormat* best_format); + + // TODO(hellner): deprecate (make private) the Start API in favor of this one. + // Also remove CS_STARTING as it is implied by the return + // value of StartCapturing(). + bool StartCapturing(const VideoFormat& capture_format); + // Start the video capturer with the specified capture format. + // Parameter + // capture_format: The caller got this parameter by either calling + // GetSupportedFormats() and selecting one of the supported + // or calling GetBestCaptureFormat(). + // Return + // CS_STARTING: The capturer is trying to start. Success or failure will + // be notified via the |SignalStateChange| callback. + // CS_RUNNING: if the capturer is started and capturing. + // CS_PAUSED: Will never be returned. + // CS_FAILED: if the capturer failes to start.. + // CS_NO_DEVICE: if the capturer has no device and fails to start. + virtual CaptureState Start(const VideoFormat& capture_format) = 0; + // Sets the desired aspect ratio. If the capturer is capturing at another + // aspect ratio it will crop the width or the height so that asked for + // aspect ratio is acheived. Note that ratio_w and ratio_h do not need to be + // relatively prime. + void UpdateAspectRatio(int ratio_w, int ratio_h); + void ClearAspectRatio(); + + // Get the current capture format, which is set by the Start() call. + // Note that the width and height of the captured frames may differ from the + // capture format. For example, the capture format is HD but the captured + // frames may be smaller than HD. + const VideoFormat* GetCaptureFormat() const { + return capture_format_.get(); + } + + // Pause the video capturer. + virtual bool Pause(bool paused); + // Stop the video capturer. + virtual void Stop() = 0; + // Check if the video capturer is running. + virtual bool IsRunning() = 0; + // Restart the video capturer with the new |capture_format|. + // Default implementation stops and starts the capturer. + virtual bool Restart(const VideoFormat& capture_format); + // TODO(thorcarpenter): This behavior of keeping the camera open just to emit + // black frames is a total hack and should be fixed. + // When muting, produce black frames then pause the camera. + // When unmuting, start the camera. Camera starts unmuted. + virtual bool MuteToBlackThenPause(bool muted); + virtual bool IsMuted() const { + return muted_; + } + CaptureState capture_state() const { + return capture_state_; + } + + // Adds a video processor that will be applied on VideoFrames returned by + // |SignalVideoFrame|. Multiple video processors can be added. The video + // processors will be applied in the order they were added. + void AddVideoProcessor(VideoProcessor* video_processor); + // Removes the |video_processor| from the list of video processors or + // returns false. + bool RemoveVideoProcessor(VideoProcessor* video_processor); + + // Returns true if the capturer is screencasting. This can be used to + // implement screencast specific behavior. + virtual bool IsScreencast() const = 0; + + // Caps the VideoCapturer's format according to max_format. It can e.g. be + // used to prevent cameras from capturing at a resolution or framerate that + // the capturer is capable of but not performing satisfactorily at. + // The capping is an upper bound for each component of the capturing format. + // The fourcc component is ignored. + void ConstrainSupportedFormats(const VideoFormat& max_format); + + void set_enable_camera_list(bool enable_camera_list) { + enable_camera_list_ = enable_camera_list; + } + bool enable_camera_list() { + return enable_camera_list_; + } + // Signal all capture state changes that are not a direct result of calling + // Start(). + sigslot::signal2 SignalStateChange; + // TODO(hellner): rename |SignalFrameCaptured| to something like + // |SignalRawFrame| or |SignalNativeFrame|. + // Frame callbacks are multithreaded to allow disconnect and connect to be + // called concurrently. It also ensures that it is safe to call disconnect + // at any time which is needed since the signal may be called from an + // unmarshalled thread owned by the VideoCapturer. + // Signal the captured frame to downstream. + sigslot::signal2 SignalFrameCaptured; + // Signal the captured frame converted to I420 to downstream. + sigslot::signal2 SignalVideoFrame; + + const VideoProcessors& video_processors() const { return video_processors_; } + + protected: + // Callback attached to SignalFrameCaptured where SignalVideoFrames is called. + void OnFrameCaptured(VideoCapturer* video_capturer, + const CapturedFrame* captured_frame); + void SetCaptureState(CaptureState state); + + // Marshals SignalStateChange onto thread_. + void OnMessage(talk_base::Message* message); + + // subclasses override this virtual method to provide a vector of fourccs, in + // order of preference, that are expected by the media engine. + virtual bool GetPreferredFourccs(std::vector* fourccs) = 0; + + // mutators to set private attributes + void SetId(const std::string& id) { + id_ = id; + } + + void SetCaptureFormat(const VideoFormat* format) { + capture_format_.reset(format ? new VideoFormat(*format) : NULL); + } + + void SetSupportedFormats(const std::vector& formats); + + private: + void Construct(); + // Get the distance between the desired format and the supported format. + // Return the max distance if they mismatch. See the implementation for + // details. + int64 GetFormatDistance(const VideoFormat& desired, + const VideoFormat& supported); + + // Convert captured frame to readable string for LOG messages. + std::string ToString(const CapturedFrame* frame) const; + + // Applies all registered processors. If any of the processors signal that + // the frame should be dropped the return value will be false. Note that + // this frame should be dropped as it has not applied all processors. + bool ApplyProcessors(VideoFrame* video_frame); + + // Updates filtered_supported_formats_ so that it contains the formats in + // supported_formats_ that fulfill all applied restrictions. + void UpdateFilteredSupportedFormats(); + // Returns true if format doesn't fulfill all applied restrictions. + bool ShouldFilterFormat(const VideoFormat& format) const; + + talk_base::Thread* thread_; + std::string id_; + CaptureState capture_state_; + talk_base::scoped_ptr capture_format_; + std::vector supported_formats_; + talk_base::scoped_ptr max_format_; + std::vector filtered_supported_formats_; + + int ratio_w_; // View resolution. e.g. 1280 x 720. + int ratio_h_; + bool enable_camera_list_; + int scaled_width_; // Current output size from ComputeScale. + int scaled_height_; + bool muted_; + int black_frame_count_down_; + + talk_base::CriticalSection crit_; + VideoProcessors video_processors_; + + DISALLOW_COPY_AND_ASSIGN(VideoCapturer); +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_VIDEOCAPTURER_H_ diff --git a/talk/media/base/videocapturer_unittest.cc b/talk/media/base/videocapturer_unittest.cc new file mode 100644 index 000000000..a6ce3ba9b --- /dev/null +++ b/talk/media/base/videocapturer_unittest.cc @@ -0,0 +1,683 @@ +// Copyright 2008 Google Inc. + +#include +#include + +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/thread.h" +#include "talk/media/base/fakemediaprocessor.h" +#include "talk/media/base/fakevideocapturer.h" +#include "talk/media/base/fakevideorenderer.h" +#include "talk/media/base/testutils.h" +#include "talk/media/base/videocapturer.h" +#include "talk/media/base/videoprocessor.h" + +// If HAS_I420_FRAME is not defined the video capturer will not be able to +// provide OnVideoFrame-callbacks since they require cricket::CapturedFrame to +// be decoded as a cricket::VideoFrame (i.e. an I420 frame). This functionality +// only exist if HAS_I420_FRAME is defined below. I420 frames are also a +// requirement for the VideoProcessors so they will not be called either. +#if defined(HAVE_WEBRTC_VIDEO) +#define HAS_I420_FRAME +#endif + +using cricket::FakeVideoCapturer; + +namespace { + +const int kMsCallbackWait = 500; +// For HD only the height matters. +const int kMinHdHeight = 720; +const uint32 kTimeout = 5000U; + +} // namespace + +// Sets the elapsed time in the video frame to 0. +class VideoProcessor0 : public cricket::VideoProcessor { + public: + virtual void OnFrame(uint32 /*ssrc*/, cricket::VideoFrame* frame, + bool* drop_frame) { + frame->SetElapsedTime(0u); + } +}; + +// Adds one to the video frame's elapsed time. Note that VideoProcessor0 and +// VideoProcessor1 are not commutative. +class VideoProcessor1 : public cricket::VideoProcessor { + public: + virtual void OnFrame(uint32 /*ssrc*/, cricket::VideoFrame* frame, + bool* drop_frame) { + int64 elapsed_time = frame->GetElapsedTime(); + frame->SetElapsedTime(elapsed_time + 1); + } +}; + +class VideoCapturerTest + : public sigslot::has_slots<>, + public testing::Test { + public: + VideoCapturerTest() + : capture_state_(cricket::CS_STOPPED), + num_state_changes_(0), + video_frames_received_(0), + last_frame_elapsed_time_(0) { + capturer_.SignalVideoFrame.connect(this, &VideoCapturerTest::OnVideoFrame); + capturer_.SignalStateChange.connect(this, + &VideoCapturerTest::OnStateChange); + } + + protected: + void OnVideoFrame(cricket::VideoCapturer*, const cricket::VideoFrame* frame) { + ++video_frames_received_; + last_frame_elapsed_time_ = frame->GetElapsedTime(); + renderer_.RenderFrame(frame); + } + void OnStateChange(cricket::VideoCapturer*, + cricket::CaptureState capture_state) { + capture_state_ = capture_state; + ++num_state_changes_; + } + cricket::CaptureState capture_state() { return capture_state_; } + int num_state_changes() { return num_state_changes_; } + int video_frames_received() const { + return video_frames_received_; + } + int64 last_frame_elapsed_time() const { return last_frame_elapsed_time_; } + + cricket::FakeVideoCapturer capturer_; + cricket::CaptureState capture_state_; + int num_state_changes_; + int video_frames_received_; + int64 last_frame_elapsed_time_; + cricket::FakeVideoRenderer renderer_; +}; + +TEST_F(VideoCapturerTest, CaptureState) { + EXPECT_EQ(cricket::CS_RUNNING, capturer_.Start(cricket::VideoFormat( + 640, + 480, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420))); + EXPECT_TRUE(capturer_.IsRunning()); + EXPECT_EQ_WAIT(cricket::CS_RUNNING, capture_state(), kMsCallbackWait); + EXPECT_EQ(1, num_state_changes()); + capturer_.Stop(); + EXPECT_EQ_WAIT(cricket::CS_STOPPED, capture_state(), kMsCallbackWait); + EXPECT_EQ(2, num_state_changes()); + capturer_.Stop(); + talk_base::Thread::Current()->ProcessMessages(100); + EXPECT_EQ(2, num_state_changes()); +} + +TEST_F(VideoCapturerTest, TestRestart) { + EXPECT_EQ(cricket::CS_RUNNING, capturer_.Start(cricket::VideoFormat( + 640, + 480, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420))); + EXPECT_TRUE(capturer_.IsRunning()); + EXPECT_EQ_WAIT(cricket::CS_RUNNING, capture_state(), kMsCallbackWait); + EXPECT_EQ(1, num_state_changes()); + EXPECT_TRUE(capturer_.Restart(cricket::VideoFormat( + 320, + 240, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420))); + EXPECT_EQ_WAIT(cricket::CS_RUNNING, capture_state(), kMsCallbackWait); + EXPECT_TRUE(capturer_.IsRunning()); + EXPECT_GE(1, num_state_changes()); + capturer_.Stop(); + talk_base::Thread::Current()->ProcessMessages(100); + EXPECT_FALSE(capturer_.IsRunning()); +} + +TEST_F(VideoCapturerTest, TestStartingWithRestart) { + EXPECT_FALSE(capturer_.IsRunning()); + EXPECT_TRUE(capturer_.Restart(cricket::VideoFormat( + 640, + 480, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420))); + EXPECT_TRUE(capturer_.IsRunning()); + EXPECT_EQ_WAIT(cricket::CS_RUNNING, capture_state(), kMsCallbackWait); +} + +TEST_F(VideoCapturerTest, TestRestartWithSameFormat) { + cricket::VideoFormat format(640, 480, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420); + EXPECT_EQ(cricket::CS_RUNNING, capturer_.Start(format)); + EXPECT_TRUE(capturer_.IsRunning()); + EXPECT_EQ_WAIT(cricket::CS_RUNNING, capture_state(), kMsCallbackWait); + EXPECT_EQ(1, num_state_changes()); + EXPECT_TRUE(capturer_.Restart(format)); + EXPECT_EQ(cricket::CS_RUNNING, capture_state()); + EXPECT_TRUE(capturer_.IsRunning()); + EXPECT_EQ(1, num_state_changes()); +} + +TEST_F(VideoCapturerTest, CameraOffOnMute) { + EXPECT_EQ(cricket::CS_RUNNING, capturer_.Start(cricket::VideoFormat( + 640, + 480, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420))); + EXPECT_TRUE(capturer_.IsRunning()); + EXPECT_EQ(0, video_frames_received()); + EXPECT_TRUE(capturer_.CaptureFrame()); + EXPECT_EQ(1, video_frames_received()); + EXPECT_FALSE(capturer_.IsMuted()); + + // Mute the camera and expect black output frame. + capturer_.MuteToBlackThenPause(true); + EXPECT_TRUE(capturer_.IsMuted()); + for (int i = 0; i < 31; ++i) { + EXPECT_TRUE(capturer_.CaptureFrame()); + EXPECT_TRUE(renderer_.black_frame()); + } + EXPECT_EQ(32, video_frames_received()); + EXPECT_EQ_WAIT(cricket::CS_PAUSED, + capturer_.capture_state(), kTimeout); + + // Verify that the camera is off. + EXPECT_FALSE(capturer_.CaptureFrame()); + EXPECT_EQ(32, video_frames_received()); + + // Unmute the camera and expect non-black output frame. + capturer_.MuteToBlackThenPause(false); + EXPECT_FALSE(capturer_.IsMuted()); + EXPECT_EQ_WAIT(cricket::CS_RUNNING, + capturer_.capture_state(), kTimeout); + EXPECT_TRUE(capturer_.CaptureFrame()); + EXPECT_FALSE(renderer_.black_frame()); + EXPECT_EQ(33, video_frames_received()); +} + +TEST_F(VideoCapturerTest, TestFourccMatch) { + cricket::VideoFormat desired(640, 480, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_ANY); + cricket::VideoFormat best; + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(640, best.width); + EXPECT_EQ(480, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); + + desired.fourcc = cricket::FOURCC_MJPG; + EXPECT_FALSE(capturer_.GetBestCaptureFormat(desired, &best)); + + desired.fourcc = cricket::FOURCC_I420; + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); +} + +TEST_F(VideoCapturerTest, TestResolutionMatch) { + cricket::VideoFormat desired(1920, 1080, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_ANY); + cricket::VideoFormat best; + // Ask for 1920x1080. Get HD 1280x720 which is the highest. + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(1280, best.width); + EXPECT_EQ(720, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); + + desired.width = 360; + desired.height = 250; + // Ask for a little higher than QVGA. Get QVGA. + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(320, best.width); + EXPECT_EQ(240, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); + + desired.width = 480; + desired.height = 270; + // Ask for HVGA. Get VGA. + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(640, best.width); + EXPECT_EQ(480, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); + + desired.width = 320; + desired.height = 240; + // Ask for QVGA. Get QVGA. + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(320, best.width); + EXPECT_EQ(240, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); + + desired.width = 80; + desired.height = 60; + // Ask for lower than QQVGA. Get QQVGA, which is the lowest. + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(160, best.width); + EXPECT_EQ(120, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); +} + +TEST_F(VideoCapturerTest, TestHDResolutionMatch) { + // Add some HD formats typical of a mediocre HD webcam. + std::vector formats; + formats.push_back(cricket::VideoFormat(320, 240, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(960, 544, + cricket::VideoFormat::FpsToInterval(24), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(1280, 720, + cricket::VideoFormat::FpsToInterval(15), cricket::FOURCC_I420)); + formats.push_back(cricket::VideoFormat(2592, 1944, + cricket::VideoFormat::FpsToInterval(7), cricket::FOURCC_I420)); + capturer_.ResetSupportedFormats(formats); + + cricket::VideoFormat desired(960, 720, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_ANY); + cricket::VideoFormat best; + // Ask for 960x720 30 fps. Get qHD 24 fps + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(960, best.width); + EXPECT_EQ(544, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(24), best.interval); + + desired.width = 960; + desired.height = 544; + desired.interval = cricket::VideoFormat::FpsToInterval(30); + // Ask for qHD 30 fps. Get qHD 24 fps + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(960, best.width); + EXPECT_EQ(544, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(24), best.interval); + + desired.width = 360; + desired.height = 250; + desired.interval = cricket::VideoFormat::FpsToInterval(30); + // Ask for a little higher than QVGA. Get QVGA. + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(320, best.width); + EXPECT_EQ(240, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); + + desired.width = 480; + desired.height = 270; + // Ask for HVGA. Get VGA. + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(640, best.width); + EXPECT_EQ(480, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); + + desired.width = 320; + desired.height = 240; + // Ask for QVGA. Get QVGA. + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(320, best.width); + EXPECT_EQ(240, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); + + desired.width = 160; + desired.height = 120; + // Ask for lower than QVGA. Get QVGA, which is the lowest. + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(320, best.width); + EXPECT_EQ(240, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); + + desired.width = 1280; + desired.height = 720; + // Ask for HD. 720p fps is too low. Get VGA which has 30 fps. + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(640, best.width); + EXPECT_EQ(480, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); + + desired.width = 1280; + desired.height = 720; + desired.interval = cricket::VideoFormat::FpsToInterval(15); + // Ask for HD 15 fps. Fps matches. Get HD + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(1280, best.width); + EXPECT_EQ(720, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(15), best.interval); + + desired.width = 1920; + desired.height = 1080; + desired.interval = cricket::VideoFormat::FpsToInterval(30); + // Ask for 1080p. Fps of HD formats is too low. Get VGA which can do 30 fps. + EXPECT_TRUE(capturer_.GetBestCaptureFormat(desired, &best)); + EXPECT_EQ(640, best.width); + EXPECT_EQ(480, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); +} + +// Some cameras support 320x240 and 320x640. Verify we choose 320x240. +TEST_F(VideoCapturerTest, TestStrangeFormats) { + std::vector supported_formats; + supported_formats.push_back(cricket::VideoFormat(320, 240, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(320, 640, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + capturer_.ResetSupportedFormats(supported_formats); + + std::vector required_formats; + required_formats.push_back(cricket::VideoFormat(320, 240, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + required_formats.push_back(cricket::VideoFormat(320, 200, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + required_formats.push_back(cricket::VideoFormat(320, 180, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + cricket::VideoFormat best; + for (size_t i = 0; i < required_formats.size(); ++i) { + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[i], &best)); + EXPECT_EQ(320, best.width); + EXPECT_EQ(240, best.height); + } + + supported_formats.clear(); + supported_formats.push_back(cricket::VideoFormat(320, 640, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(320, 240, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + capturer_.ResetSupportedFormats(supported_formats); + + for (size_t i = 0; i < required_formats.size(); ++i) { + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[i], &best)); + EXPECT_EQ(320, best.width); + EXPECT_EQ(240, best.height); + } +} + +// Some cameras only have very low fps. Verify we choose something sensible. +TEST_F(VideoCapturerTest, TestPoorFpsFormats) { + // all formats are low framerate + std::vector supported_formats; + supported_formats.push_back(cricket::VideoFormat(320, 240, + cricket::VideoFormat::FpsToInterval(10), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(7), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(1280, 720, + cricket::VideoFormat::FpsToInterval(2), cricket::FOURCC_I420)); + capturer_.ResetSupportedFormats(supported_formats); + + std::vector required_formats; + required_formats.push_back(cricket::VideoFormat(320, 240, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + required_formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + cricket::VideoFormat best; + for (size_t i = 0; i < required_formats.size(); ++i) { + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[i], &best)); + EXPECT_EQ(required_formats[i].width, best.width); + EXPECT_EQ(required_formats[i].height, best.height); + } + + // Increase framerate of 320x240. Expect low fps VGA avoided. + supported_formats.clear(); + supported_formats.push_back(cricket::VideoFormat(320, 240, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(7), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(1280, 720, + cricket::VideoFormat::FpsToInterval(2), cricket::FOURCC_I420)); + capturer_.ResetSupportedFormats(supported_formats); + + for (size_t i = 0; i < required_formats.size(); ++i) { + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[i], &best)); + EXPECT_EQ(320, best.width); + EXPECT_EQ(240, best.height); + } +} + +// Some cameras support same size with different frame rates. Verify we choose +// the frame rate properly. +TEST_F(VideoCapturerTest, TestSameSizeDifferentFpsFormats) { + std::vector supported_formats; + supported_formats.push_back(cricket::VideoFormat(320, 240, + cricket::VideoFormat::FpsToInterval(10), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(320, 240, + cricket::VideoFormat::FpsToInterval(20), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(320, 240, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + capturer_.ResetSupportedFormats(supported_formats); + + std::vector required_formats = supported_formats; + cricket::VideoFormat best; + for (size_t i = 0; i < required_formats.size(); ++i) { + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[i], &best)); + EXPECT_EQ(320, best.width); + EXPECT_EQ(240, best.height); + EXPECT_EQ(required_formats[i].interval, best.interval); + } +} + +// Some cameras support the correct resolution but at a lower fps than +// we'd like. This tests we get the expected resolution and fps. +TEST_F(VideoCapturerTest, TestFpsFormats) { + // We have VGA but low fps. Choose VGA, not HD + std::vector supported_formats; + supported_formats.push_back(cricket::VideoFormat(1280, 720, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(15), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 400, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 360, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + capturer_.ResetSupportedFormats(supported_formats); + + std::vector required_formats; + required_formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_ANY)); + required_formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(20), cricket::FOURCC_ANY)); + required_formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(10), cricket::FOURCC_ANY)); + cricket::VideoFormat best; + + // expect 30 fps to choose 30 fps format + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[0], &best)); + EXPECT_EQ(640, best.width); + EXPECT_EQ(400, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); + + // expect 20 fps to choose 20 fps format + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[1], &best)); + EXPECT_EQ(640, best.width); + EXPECT_EQ(400, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(20), best.interval); + + // expect 10 fps to choose 15 fps format but set fps to 10 + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[2], &best)); + EXPECT_EQ(640, best.width); + EXPECT_EQ(480, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(10), best.interval); + + // We have VGA 60 fps and 15 fps. Choose best fps. + supported_formats.clear(); + supported_formats.push_back(cricket::VideoFormat(1280, 720, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(60), cricket::FOURCC_MJPG)); + supported_formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(15), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 400, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 360, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + capturer_.ResetSupportedFormats(supported_formats); + + // expect 30 fps to choose 60 fps format, but will set best fps to 30 + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[0], &best)); + EXPECT_EQ(640, best.width); + EXPECT_EQ(480, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval); + + // expect 20 fps to choose 60 fps format, but will set best fps to 20 + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[1], &best)); + EXPECT_EQ(640, best.width); + EXPECT_EQ(480, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(20), best.interval); + + // expect 10 fps to choose 10 fps + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[2], &best)); + EXPECT_EQ(640, best.width); + EXPECT_EQ(480, best.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(10), best.interval); +} + +TEST_F(VideoCapturerTest, TestRequest16x10_9) { + std::vector supported_formats; + // We do not support HD, expect 4x3 for 4x3, 16x10, and 16x9 requests. + supported_formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 400, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 360, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + capturer_.ResetSupportedFormats(supported_formats); + + std::vector required_formats = supported_formats; + cricket::VideoFormat best; + // Expect 4x3, 16x10, and 16x9 requests are respected. + for (size_t i = 0; i < required_formats.size(); ++i) { + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[i], &best)); + EXPECT_EQ(required_formats[i].width, best.width); + EXPECT_EQ(required_formats[i].height, best.height); + } + + // We do not support 16x9 HD, expect 4x3 for 4x3, 16x10, and 16x9 requests. + supported_formats.clear(); + supported_formats.push_back(cricket::VideoFormat(960, 720, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 400, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 360, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + capturer_.ResetSupportedFormats(supported_formats); + + // Expect 4x3, 16x10, and 16x9 requests are respected. + for (size_t i = 0; i < required_formats.size(); ++i) { + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[i], &best)); + EXPECT_EQ(required_formats[i].width, best.width); + EXPECT_EQ(required_formats[i].height, best.height); + } + + // We support 16x9HD, Expect 4x3, 16x10, and 16x9 requests are respected. + supported_formats.clear(); + supported_formats.push_back(cricket::VideoFormat(1280, 720, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 480, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 400, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + supported_formats.push_back(cricket::VideoFormat(640, 360, + cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420)); + capturer_.ResetSupportedFormats(supported_formats); + + // Expect 4x3 for 4x3 and 16x10 requests. + for (size_t i = 0; i < required_formats.size() - 1; ++i) { + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[i], &best)); + EXPECT_EQ(required_formats[i].width, best.width); + EXPECT_EQ(required_formats[i].height, best.height); + } + + // Expect 16x9 for 16x9 request. + EXPECT_TRUE(capturer_.GetBestCaptureFormat(required_formats[2], &best)); + EXPECT_EQ(640, best.width); + EXPECT_EQ(360, best.height); +} + +#if defined(HAS_I420_FRAME) +TEST_F(VideoCapturerTest, VideoFrame) { + EXPECT_EQ(cricket::CS_RUNNING, capturer_.Start(cricket::VideoFormat( + 640, + 480, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420))); + EXPECT_TRUE(capturer_.IsRunning()); + EXPECT_EQ(0, video_frames_received()); + EXPECT_TRUE(capturer_.CaptureFrame()); + EXPECT_EQ(1, video_frames_received()); +} + +TEST_F(VideoCapturerTest, ProcessorChainTest) { + VideoProcessor0 processor0; + VideoProcessor1 processor1; + EXPECT_EQ(cricket::CS_RUNNING, capturer_.Start(cricket::VideoFormat( + 640, + 480, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420))); + EXPECT_TRUE(capturer_.IsRunning()); + EXPECT_EQ(0, video_frames_received()); + // First processor sets elapsed time to 0. + capturer_.AddVideoProcessor(&processor0); + // Second processor adds 1 to the elapsed time. I.e. a frames elapsed time + // should now always be 1 (and not 0). + capturer_.AddVideoProcessor(&processor1); + EXPECT_TRUE(capturer_.CaptureFrame()); + EXPECT_EQ(1, video_frames_received()); + EXPECT_EQ(1u, last_frame_elapsed_time()); + capturer_.RemoveVideoProcessor(&processor1); + EXPECT_TRUE(capturer_.CaptureFrame()); + // Since processor1 has been removed the elapsed time should now be 0. + EXPECT_EQ(2, video_frames_received()); + EXPECT_EQ(0u, last_frame_elapsed_time()); +} + +TEST_F(VideoCapturerTest, ProcessorDropFrame) { + cricket::FakeMediaProcessor dropping_processor_; + dropping_processor_.set_drop_frames(true); + EXPECT_EQ(cricket::CS_RUNNING, capturer_.Start(cricket::VideoFormat( + 640, + 480, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420))); + EXPECT_TRUE(capturer_.IsRunning()); + EXPECT_EQ(0, video_frames_received()); + // Install a processor that always drop frames. + capturer_.AddVideoProcessor(&dropping_processor_); + EXPECT_TRUE(capturer_.CaptureFrame()); + EXPECT_EQ(0, video_frames_received()); +} +#endif // HAS_I420_FRAME + +bool HdFormatInList(const std::vector& formats) { + for (std::vector::const_iterator found = + formats.begin(); found != formats.end(); ++found) { + if (found->height >= kMinHdHeight) { + return true; + } + } + return false; +} + +TEST_F(VideoCapturerTest, Whitelist) { + // The definition of HD only applies to the height. Set the HD width to the + // smallest legal number to document this fact in this test. + const int kMinHdWidth = 1; + cricket::VideoFormat hd_format(kMinHdWidth, + kMinHdHeight, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420); + cricket::VideoFormat vga_format(640, 480, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420); + std::vector formats = *capturer_.GetSupportedFormats(); + formats.push_back(hd_format); + + // Enable whitelist. Expect HD not in list. + capturer_.set_enable_camera_list(true); + capturer_.ResetSupportedFormats(formats); + EXPECT_TRUE(HdFormatInList(*capturer_.GetSupportedFormats())); + capturer_.ConstrainSupportedFormats(vga_format); + EXPECT_FALSE(HdFormatInList(*capturer_.GetSupportedFormats())); + + // Disable whitelist. Expect HD in list. + capturer_.set_enable_camera_list(false); + capturer_.ResetSupportedFormats(formats); + EXPECT_TRUE(HdFormatInList(*capturer_.GetSupportedFormats())); + capturer_.ConstrainSupportedFormats(vga_format); + EXPECT_TRUE(HdFormatInList(*capturer_.GetSupportedFormats())); +} diff --git a/talk/media/base/videocommon.cc b/talk/media/base/videocommon.cc new file mode 100644 index 000000000..12f3bb368 --- /dev/null +++ b/talk/media/base/videocommon.cc @@ -0,0 +1,237 @@ +// libjingle +// Copyright 2010 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/media/base/videocommon.h" + +#include // For INT_MAX +#include +#include + +#include "talk/base/common.h" + +namespace cricket { + +struct FourCCAliasEntry { + uint32 alias; + uint32 canonical; +}; + +static const FourCCAliasEntry kFourCCAliases[] = { + {FOURCC_IYUV, FOURCC_I420}, + {FOURCC_YU16, FOURCC_I422}, + {FOURCC_YU24, FOURCC_I444}, + {FOURCC_YUYV, FOURCC_YUY2}, + {FOURCC_YUVS, FOURCC_YUY2}, + {FOURCC_HDYC, FOURCC_UYVY}, + {FOURCC_2VUY, FOURCC_UYVY}, + {FOURCC_JPEG, FOURCC_MJPG}, // Note: JPEG has DHT while MJPG does not. + {FOURCC_DMB1, FOURCC_MJPG}, + {FOURCC_BA81, FOURCC_BGGR}, + {FOURCC_RGB3, FOURCC_RAW}, + {FOURCC_BGR3, FOURCC_24BG}, + {FOURCC_CM32, FOURCC_BGRA}, + {FOURCC_CM24, FOURCC_RAW}, +}; + +uint32 CanonicalFourCC(uint32 fourcc) { + for (int i = 0; i < ARRAY_SIZE(kFourCCAliases); ++i) { + if (kFourCCAliases[i].alias == fourcc) { + return kFourCCAliases[i].canonical; + } + } + // Not an alias, so return it as-is. + return fourcc; +} + +static float kScaleFactors[] = { + 1.f / 1.f, // Full size. + 1.f / 2.f, // 1/2 scale. + 1.f / 4.f, // 1/4 scale. + 1.f / 8.f, // 1/8 scale. + 1.f / 16.f // 1/16 scale. +}; + +static const int kNumScaleFactors = ARRAY_SIZE(kScaleFactors); + +// Finds the scale factor that, when applied to width and height, produces +// fewer than num_pixels. +static float FindLowerScale(int width, int height, int target_num_pixels) { + if (!target_num_pixels) { + return 0.f; + } + int best_distance = INT_MAX; + int best_index = kNumScaleFactors - 1; // Default to max scale. + for (int i = 0; i < kNumScaleFactors; ++i) { + int test_num_pixels = static_cast(width * kScaleFactors[i] * + height * kScaleFactors[i]); + int diff = target_num_pixels - test_num_pixels; + if (diff >= 0 && diff < best_distance) { + best_distance = diff; + best_index = i; + if (best_distance == 0) { // Found exact match. + break; + } + } + } + return kScaleFactors[best_index]; +} + +// Compute a size to scale frames to that is below maximum compression +// and rendering size with the same aspect ratio. +void ComputeScale(int frame_width, int frame_height, int fps, + int* scaled_width, int* scaled_height) { + ASSERT(scaled_width != NULL); + ASSERT(scaled_height != NULL); + // For VP8 the values for max width and height can be found here + // webrtc/src/video_engine/vie_defines.h (kViEMaxCodecWidth and + // kViEMaxCodecHeight) + const int kMaxWidth = 4096; + const int kMaxHeight = 3072; + // Maximum pixels limit is set to Retina MacBookPro 15" resolution of + // 2880 x 1800 as of 4/18/2013. + // For high fps, maximum pixels limit is set based on common 24" monitor + // resolution of 2048 x 1280 as of 6/13/2013. The Retina resolution is + // therefore reduced to 1440 x 900. + int kMaxPixels = (fps > 5) ? 2048 * 1280 : 2880 * 1800; + int new_frame_width = frame_width; + int new_frame_height = frame_height; + + // Limit width. + if (new_frame_width > kMaxWidth) { + new_frame_height = new_frame_height * kMaxWidth / new_frame_width; + new_frame_width = kMaxWidth; + } + // Limit height. + if (new_frame_height > kMaxHeight) { + new_frame_width = new_frame_width * kMaxHeight / new_frame_height; + new_frame_height = kMaxHeight; + } + // Limit number of pixels. + if (new_frame_width * new_frame_height > kMaxPixels) { + // Compute new width such that width * height is less than maximum but + // maintains original captured frame aspect ratio. + new_frame_width = static_cast(sqrtf(static_cast( + kMaxPixels) * new_frame_width / new_frame_height)); + new_frame_height = kMaxPixels / new_frame_width; + } + // Snap to a scale factor that is less than or equal to target pixels. + float scale = FindLowerScale(frame_width, frame_height, + new_frame_width * new_frame_height); + *scaled_width = static_cast(frame_width * scale + .5f); + *scaled_height = static_cast(frame_height * scale + .5f); +} + +// Compute size to crop video frame to. +// If cropped_format_* is 0, return the frame_* size as is. +void ComputeCrop(int cropped_format_width, + int cropped_format_height, + int frame_width, int frame_height, + int pixel_width, int pixel_height, + int rotation, + int* cropped_width, int* cropped_height) { + ASSERT(cropped_format_width >= 0); + ASSERT(cropped_format_height >= 0); + ASSERT(frame_width > 0); + ASSERT(frame_height > 0); + ASSERT(pixel_width >= 0); + ASSERT(pixel_height >= 0); + ASSERT(rotation == 0 || rotation == 90 || rotation == 180 || rotation == 270); + ASSERT(cropped_width != NULL); + ASSERT(cropped_height != NULL); + if (!pixel_width) { + pixel_width = 1; + } + if (!pixel_height) { + pixel_height = 1; + } + // if cropped_format is 0x0 disable cropping. + if (!cropped_format_height) { + cropped_format_height = 1; + } + float frame_aspect = static_cast(frame_width * pixel_width) / + static_cast(frame_height * pixel_height); + float crop_aspect = static_cast(cropped_format_width) / + static_cast(cropped_format_height); + int new_frame_width = frame_width; + int new_frame_height = frame_height; + if (rotation == 90 || rotation == 270) { + frame_aspect = 1.0f / frame_aspect; + new_frame_width = frame_height; + new_frame_height = frame_width; + } + + // kAspectThresh is the maximum aspect ratio difference that we'll accept + // for cropping. The value 1.33 is based on 4:3 being cropped to 16:9. + // Set to zero to disable cropping entirely. + // TODO(fbarchard): crop to multiple of 16 width for better performance. + const float kAspectThresh = 16.f / 9.f / (4.f / 3.f) + 0.01f; // 1.33 + // Wide aspect - crop horizontally + if (frame_aspect > crop_aspect && + frame_aspect < crop_aspect * kAspectThresh) { + // Round width down to multiple of 4 to avoid odd chroma width. + // Width a multiple of 4 allows a half size image to have chroma channel + // that avoids rounding errors. lmi and webrtc have odd width limitations. + new_frame_width = static_cast((crop_aspect * frame_height * + pixel_height) / pixel_width + 0.5f) & ~3; + } else if (crop_aspect > frame_aspect && + crop_aspect < frame_aspect * kAspectThresh) { + new_frame_height = static_cast((frame_width * pixel_width) / + (crop_aspect * pixel_height) + 0.5f) & ~1; + } + + *cropped_width = new_frame_width; + *cropped_height = new_frame_height; + if (rotation == 90 || rotation == 270) { + *cropped_width = new_frame_height; + *cropped_height = new_frame_width; + } +} + +// The C++ standard requires a namespace-scope definition of static const +// integral types even when they are initialized in the declaration (see +// [class.static.data]/4), but MSVC with /Ze is non-conforming and treats that +// as a multiply defined symbol error. See Also: +// http://msdn.microsoft.com/en-us/library/34h23df8.aspx +#ifndef _MSC_EXTENSIONS +const int64 VideoFormat::kMinimumInterval; // Initialized in header. +#endif + +std::string VideoFormat::ToString() const { + std::string fourcc_name = GetFourccName(fourcc) + " "; + for (std::string::const_iterator i = fourcc_name.begin(); + i < fourcc_name.end(); ++i) { + // Test character is printable; Avoid isprint() which asserts on negatives. + if (*i < 32 || *i >= 127) { + fourcc_name = ""; + break; + } + } + + std::ostringstream ss; + ss << fourcc_name << width << "x" << height << "x" << IntervalToFps(interval); + return ss.str(); +} + +} // namespace cricket diff --git a/talk/media/base/videocommon.h b/talk/media/base/videocommon.h new file mode 100644 index 000000000..098651f3d --- /dev/null +++ b/talk/media/base/videocommon.h @@ -0,0 +1,242 @@ +// libjingle +// Copyright 2004 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. +// +// Common definition for video, including fourcc and VideoFormat. + +#ifndef TALK_MEDIA_BASE_VIDEOCOMMON_H_ +#define TALK_MEDIA_BASE_VIDEOCOMMON_H_ + +#include + +#include "talk/base/basictypes.h" +#include "talk/base/timeutils.h" + +namespace cricket { + +// TODO(janahan): For now, a hard-coded ssrc is used as the video ssrc. +// This is because when the video frame is passed to the mediaprocessor for +// processing, it doesn't have the correct ssrc. Since currently only Tx +// Video processing is supported, this is ok. When we switch over to trigger +// from capturer, this should be fixed and this const removed. +const uint32 kDummyVideoSsrc = 0xFFFFFFFF; + +// Minimum interval is 10k fps. +#define FPS_TO_INTERVAL(fps) \ + (fps ? talk_base::kNumNanosecsPerSec / fps : \ + talk_base::kNumNanosecsPerSec / 10000) + +////////////////////////////////////////////////////////////////////////////// +// Definition of FourCC codes +////////////////////////////////////////////////////////////////////////////// +// Convert four characters to a FourCC code. +// Needs to be a macro otherwise the OS X compiler complains when the kFormat* +// constants are used in a switch. +#define FOURCC(a, b, c, d) ( \ + (static_cast(a)) | (static_cast(b) << 8) | \ + (static_cast(c) << 16) | (static_cast(d) << 24)) +// Some pages discussing FourCC codes: +// http://www.fourcc.org/yuv.php +// http://v4l2spec.bytesex.org/spec/book1.htm +// http://developer.apple.com/quicktime/icefloe/dispatch020.html +// http://msdn.microsoft.com/library/windows/desktop/dd206750.aspx#nv12 +// http://people.xiph.org/~xiphmont/containers/nut/nut4cc.txt + +// FourCC codes grouped according to implementation efficiency. +// Primary formats should convert in 1 efficient step. +// Secondary formats are converted in 2 steps. +// Auxilliary formats call primary converters. +enum FourCC { + // 9 Primary YUV formats: 5 planar, 2 biplanar, 2 packed. + FOURCC_I420 = FOURCC('I', '4', '2', '0'), + FOURCC_I422 = FOURCC('I', '4', '2', '2'), + FOURCC_I444 = FOURCC('I', '4', '4', '4'), + FOURCC_I411 = FOURCC('I', '4', '1', '1'), + FOURCC_I400 = FOURCC('I', '4', '0', '0'), + FOURCC_NV21 = FOURCC('N', 'V', '2', '1'), + FOURCC_NV12 = FOURCC('N', 'V', '1', '2'), + FOURCC_YUY2 = FOURCC('Y', 'U', 'Y', '2'), + FOURCC_UYVY = FOURCC('U', 'Y', 'V', 'Y'), + + // 2 Secondary YUV formats: row biplanar. + FOURCC_M420 = FOURCC('M', '4', '2', '0'), + FOURCC_Q420 = FOURCC('Q', '4', '2', '0'), + + // 9 Primary RGB formats: 4 32 bpp, 2 24 bpp, 3 16 bpp. + FOURCC_ARGB = FOURCC('A', 'R', 'G', 'B'), + FOURCC_BGRA = FOURCC('B', 'G', 'R', 'A'), + FOURCC_ABGR = FOURCC('A', 'B', 'G', 'R'), + FOURCC_24BG = FOURCC('2', '4', 'B', 'G'), + FOURCC_RAW = FOURCC('r', 'a', 'w', ' '), + FOURCC_RGBA = FOURCC('R', 'G', 'B', 'A'), + FOURCC_RGBP = FOURCC('R', 'G', 'B', 'P'), // bgr565. + FOURCC_RGBO = FOURCC('R', 'G', 'B', 'O'), // abgr1555. + FOURCC_R444 = FOURCC('R', '4', '4', '4'), // argb4444. + + // 4 Secondary RGB formats: 4 Bayer Patterns. + FOURCC_RGGB = FOURCC('R', 'G', 'G', 'B'), + FOURCC_BGGR = FOURCC('B', 'G', 'G', 'R'), + FOURCC_GRBG = FOURCC('G', 'R', 'B', 'G'), + FOURCC_GBRG = FOURCC('G', 'B', 'R', 'G'), + + // 1 Primary Compressed YUV format. + FOURCC_MJPG = FOURCC('M', 'J', 'P', 'G'), + + // 5 Auxiliary YUV variations: 3 with U and V planes are swapped, 1 Alias. + FOURCC_YV12 = FOURCC('Y', 'V', '1', '2'), + FOURCC_YV16 = FOURCC('Y', 'V', '1', '6'), + FOURCC_YV24 = FOURCC('Y', 'V', '2', '4'), + FOURCC_YU12 = FOURCC('Y', 'U', '1', '2'), // Linux version of I420. + FOURCC_J420 = FOURCC('J', '4', '2', '0'), + FOURCC_J400 = FOURCC('J', '4', '0', '0'), + + // 14 Auxiliary aliases. CanonicalFourCC() maps these to canonical fourcc. + FOURCC_IYUV = FOURCC('I', 'Y', 'U', 'V'), // Alias for I420. + FOURCC_YU16 = FOURCC('Y', 'U', '1', '6'), // Alias for I422. + FOURCC_YU24 = FOURCC('Y', 'U', '2', '4'), // Alias for I444. + FOURCC_YUYV = FOURCC('Y', 'U', 'Y', 'V'), // Alias for YUY2. + FOURCC_YUVS = FOURCC('y', 'u', 'v', 's'), // Alias for YUY2 on Mac. + FOURCC_HDYC = FOURCC('H', 'D', 'Y', 'C'), // Alias for UYVY. + FOURCC_2VUY = FOURCC('2', 'v', 'u', 'y'), // Alias for UYVY on Mac. + FOURCC_JPEG = FOURCC('J', 'P', 'E', 'G'), // Alias for MJPG. + FOURCC_DMB1 = FOURCC('d', 'm', 'b', '1'), // Alias for MJPG on Mac. + FOURCC_BA81 = FOURCC('B', 'A', '8', '1'), // Alias for BGGR. + FOURCC_RGB3 = FOURCC('R', 'G', 'B', '3'), // Alias for RAW. + FOURCC_BGR3 = FOURCC('B', 'G', 'R', '3'), // Alias for 24BG. + FOURCC_CM32 = FOURCC(0, 0, 0, 32), // Alias for BGRA kCMPixelFormat_32ARGB + FOURCC_CM24 = FOURCC(0, 0, 0, 24), // Alias for RAW kCMPixelFormat_24RGB + + // 1 Auxiliary compressed YUV format set aside for capturer. + FOURCC_H264 = FOURCC('H', '2', '6', '4'), + + // Match any fourcc. + FOURCC_ANY = 0xFFFFFFFF, +}; + +// Converts fourcc aliases into canonical ones. +uint32 CanonicalFourCC(uint32 fourcc); + +// Get FourCC code as a string. +inline std::string GetFourccName(uint32 fourcc) { + std::string name; + name.push_back(static_cast(fourcc & 0xFF)); + name.push_back(static_cast((fourcc >> 8) & 0xFF)); + name.push_back(static_cast((fourcc >> 16) & 0xFF)); + name.push_back(static_cast((fourcc >> 24) & 0xFF)); + return name; +} + +void ComputeScale(int frame_width, int frame_height, int fps, + int* scaled_width, int* scaled_height); + +// Compute the frame size that conversion should crop to based on aspect ratio. +// Ensures size is multiple of 2 due to I420 and conversion limitations. +void ComputeCrop(int cropped_format_width, int cropped_format_height, + int frame_width, int frame_height, + int pixel_width, int pixel_height, + int rotation, + int* cropped_width, int* cropped_height); + +////////////////////////////////////////////////////////////////////////////// +// Definition of VideoFormat. +////////////////////////////////////////////////////////////////////////////// + +// VideoFormat with Plain Old Data for global variables. +struct VideoFormatPod { + int width; // Number of pixels. + int height; // Number of pixels. + int64 interval; // Nanoseconds. + uint32 fourcc; // Color space. FOURCC_ANY means that any color space is OK. +}; + +struct VideoFormat : VideoFormatPod { + static const int64 kMinimumInterval = + talk_base::kNumNanosecsPerSec / 10000; // 10k fps. + + VideoFormat() { + Construct(0, 0, 0, 0); + } + + VideoFormat(int w, int h, int64 interval_ns, uint32 cc) { + Construct(w, h, interval_ns, cc); + } + + explicit VideoFormat(const VideoFormatPod& format) { + Construct(format.width, format.height, format.interval, format.fourcc); + } + + void Construct(int w, int h, int64 interval_ns, uint32 cc) { + width = w; + height = h; + interval = interval_ns; + fourcc = cc; + } + + static int64 FpsToInterval(int fps) { + return fps ? talk_base::kNumNanosecsPerSec / fps : kMinimumInterval; + } + + static int IntervalToFps(int64 interval) { + // Normalize the interval first. + interval = talk_base::_max(interval, kMinimumInterval); + return static_cast(talk_base::kNumNanosecsPerSec / interval); + } + + bool operator==(const VideoFormat& format) const { + return width == format.width && height == format.height && + interval == format.interval && fourcc == format.fourcc; + } + + bool operator!=(const VideoFormat& format) const { + return !(*this == format); + } + + bool operator<(const VideoFormat& format) const { + return (fourcc < format.fourcc) || + (fourcc == format.fourcc && width < format.width) || + (fourcc == format.fourcc && width == format.width && + height < format.height) || + (fourcc == format.fourcc && width == format.width && + height == format.height && interval > format.interval); + } + + int framerate() const { return IntervalToFps(interval); } + + // Check if both width and height are 0. + bool IsSize0x0() const { return 0 == width && 0 == height; } + + // Check if this format is less than another one by comparing the resolution + // and frame rate. + bool IsPixelRateLess(const VideoFormat& format) const { + return width * height * framerate() < + format.width * format.height * format.framerate(); + } + + // Get a string presentation in the form of "fourcc width x height x fps" + std::string ToString() const; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_VIDEOCOMMON_H_ diff --git a/talk/media/base/videocommon_unittest.cc b/talk/media/base/videocommon_unittest.cc new file mode 100644 index 000000000..e9cd26a17 --- /dev/null +++ b/talk/media/base/videocommon_unittest.cc @@ -0,0 +1,290 @@ +/* + * libjingle + * Copyright 2008 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/base/gunit.h" +#include "talk/media/base/videocommon.h" + +namespace cricket { + +TEST(VideoCommonTest, TestCanonicalFourCC) { + // Canonical fourccs are not changed. + EXPECT_EQ(FOURCC_I420, CanonicalFourCC(FOURCC_I420)); + // The special FOURCC_ANY value is not changed. + EXPECT_EQ(FOURCC_ANY, CanonicalFourCC(FOURCC_ANY)); + // Aliases are translated to the canonical equivalent. + EXPECT_EQ(FOURCC_I420, CanonicalFourCC(FOURCC_IYUV)); + EXPECT_EQ(FOURCC_I422, CanonicalFourCC(FOURCC_YU16)); + EXPECT_EQ(FOURCC_I444, CanonicalFourCC(FOURCC_YU24)); + EXPECT_EQ(FOURCC_YUY2, CanonicalFourCC(FOURCC_YUYV)); + EXPECT_EQ(FOURCC_YUY2, CanonicalFourCC(FOURCC_YUVS)); + EXPECT_EQ(FOURCC_UYVY, CanonicalFourCC(FOURCC_HDYC)); + EXPECT_EQ(FOURCC_UYVY, CanonicalFourCC(FOURCC_2VUY)); + EXPECT_EQ(FOURCC_MJPG, CanonicalFourCC(FOURCC_JPEG)); + EXPECT_EQ(FOURCC_MJPG, CanonicalFourCC(FOURCC_DMB1)); + EXPECT_EQ(FOURCC_BGGR, CanonicalFourCC(FOURCC_BA81)); + EXPECT_EQ(FOURCC_RAW, CanonicalFourCC(FOURCC_RGB3)); + EXPECT_EQ(FOURCC_24BG, CanonicalFourCC(FOURCC_BGR3)); + EXPECT_EQ(FOURCC_BGRA, CanonicalFourCC(FOURCC_CM32)); + EXPECT_EQ(FOURCC_RAW, CanonicalFourCC(FOURCC_CM24)); +} + +// Test conversion between interval and fps +TEST(VideoCommonTest, TestVideoFormatFps) { + EXPECT_EQ(VideoFormat::kMinimumInterval, VideoFormat::FpsToInterval(0)); + EXPECT_EQ(talk_base::kNumNanosecsPerSec / 20, VideoFormat::FpsToInterval(20)); + EXPECT_EQ(20, VideoFormat::IntervalToFps(talk_base::kNumNanosecsPerSec / 20)); +} + +// Test IsSize0x0 +TEST(VideoCommonTest, TestVideoFormatIsSize0x0) { + VideoFormat format; + EXPECT_TRUE(format.IsSize0x0()); + format.width = 320; + EXPECT_FALSE(format.IsSize0x0()); +} + +// Test ToString: print fourcc when it is printable. +TEST(VideoCommonTest, TestVideoFormatToString) { + VideoFormat format; + EXPECT_EQ("0x0x10000", format.ToString()); + + format.fourcc = FOURCC_I420; + format.width = 640; + format.height = 480; + format.interval = VideoFormat::FpsToInterval(20); + EXPECT_EQ("I420 640x480x20", format.ToString()); + + format.fourcc = FOURCC_ANY; + format.width = 640; + format.height = 480; + format.interval = VideoFormat::FpsToInterval(20); + EXPECT_EQ("640x480x20", format.ToString()); +} + +// Test comparison. +TEST(VideoCommonTest, TestVideoFormatCompare) { + VideoFormat format(640, 480, VideoFormat::FpsToInterval(20), FOURCC_I420); + VideoFormat format2; + EXPECT_NE(format, format2); + + // Same pixelrate, different fourcc. + format2 = format; + format2.fourcc = FOURCC_YUY2; + EXPECT_NE(format, format2); + EXPECT_FALSE(format.IsPixelRateLess(format2) || + format2.IsPixelRateLess(format2)); + + format2 = format; + format2.interval /= 2; + EXPECT_TRUE(format.IsPixelRateLess(format2)); + + format2 = format; + format2.width *= 2; + EXPECT_TRUE(format.IsPixelRateLess(format2)); +} + +TEST(VideoCommonTest, TestComputeScaleWithLowFps) { + int scaled_width, scaled_height; + + // Request small enough. Expect no change. + ComputeScale(2560, 1600, 5, &scaled_width, &scaled_height); + EXPECT_EQ(2560, scaled_width); + EXPECT_EQ(1600, scaled_height); + + // Request too many pixels. Expect 1/2 size. + ComputeScale(4096, 2560, 5, &scaled_width, &scaled_height); + EXPECT_EQ(2048, scaled_width); + EXPECT_EQ(1280, scaled_height); + + // Request too many pixels and too wide and tall. Expect 1/4 size. + ComputeScale(16000, 10000, 5, &scaled_width, &scaled_height); + EXPECT_EQ(2000, scaled_width); + EXPECT_EQ(1250, scaled_height); + + // Request too wide. (two 30 inch monitors). Expect 1/2 size. + ComputeScale(5120, 1600, 5, &scaled_width, &scaled_height); + EXPECT_EQ(2560, scaled_width); + EXPECT_EQ(800, scaled_height); + + // Request too wide but not too many pixels. Expect 1/2 size. + ComputeScale(8192, 1024, 5, &scaled_width, &scaled_height); + EXPECT_EQ(4096, scaled_width); + EXPECT_EQ(512, scaled_height); + + // Request too tall. Expect 1/4 size. + ComputeScale(1024, 8192, 5, &scaled_width, &scaled_height); + EXPECT_EQ(256, scaled_width); + EXPECT_EQ(2048, scaled_height); +} + +// Same as TestComputeScale but with 15 fps instead of 5 fps. +TEST(VideoCommonTest, TestComputeScaleWithHighFps) { + int scaled_width, scaled_height; + + // Request small enough but high fps. Expect 1/2 size. + ComputeScale(2560, 1600, 15, &scaled_width, &scaled_height); + EXPECT_EQ(1280, scaled_width); + EXPECT_EQ(800, scaled_height); + + // Request too many pixels. Expect 1/2 size. + ComputeScale(4096, 2560, 15, &scaled_width, &scaled_height); + EXPECT_EQ(2048, scaled_width); + EXPECT_EQ(1280, scaled_height); + + // Request too many pixels and too wide and tall. Expect 1/16 size. + ComputeScale(64000, 40000, 15, &scaled_width, &scaled_height); + EXPECT_EQ(4000, scaled_width); + EXPECT_EQ(2500, scaled_height); + + // Request too wide. (two 30 inch monitors). Expect 1/2 size. + ComputeScale(5120, 1600, 15, &scaled_width, &scaled_height); + EXPECT_EQ(2560, scaled_width); + EXPECT_EQ(800, scaled_height); + + // Request too wide but not too many pixels. Expect 1/2 size. + ComputeScale(8192, 1024, 15, &scaled_width, &scaled_height); + EXPECT_EQ(4096, scaled_width); + EXPECT_EQ(512, scaled_height); + + // Request too tall. Expect 1/4 size. + ComputeScale(1024, 8192, 15, &scaled_width, &scaled_height); + EXPECT_EQ(256, scaled_width); + EXPECT_EQ(2048, scaled_height); +} + +TEST(VideoCommonTest, TestComputeCrop) { + int cropped_width, cropped_height; + + // Request 16:9 to 16:9. Expect no cropping. + ComputeCrop(1280, 720, // Crop size 16:9 + 640, 360, // Frame is 4:3 + 1, 1, // Normal 1:1 pixels + 0, + &cropped_width, &cropped_height); + EXPECT_EQ(640, cropped_width); + EXPECT_EQ(360, cropped_height); + + // Request 4:3 to 16:9. Expect vertical. + ComputeCrop(640, 360, // Crop size 16:9 + 640, 480, // Frame is 4:3 + 1, 1, // Normal 1:1 pixels + 0, + &cropped_width, &cropped_height); + EXPECT_EQ(640, cropped_width); + EXPECT_EQ(360, cropped_height); + + // Request 16:9 to 4:3. Expect horizontal crop. + ComputeCrop(640, 480, // Crop size 4:3 + 640, 360, // Frame is 16:9 + 1, 1, // Normal 1:1 pixels + 0, + &cropped_width, &cropped_height); + EXPECT_EQ(480, cropped_width); + EXPECT_EQ(360, cropped_height); + + // Request 16:9 but VGA has 3:8 pixel aspect ratio. Expect no crop. + // This occurs on HP4110 on OSX 10.5/10.6/10.7 + ComputeCrop(640, 360, // Crop size 16:9 + 640, 480, // Frame is 4:3 + 3, 8, // Pixel aspect ratio is tall + 0, + &cropped_width, &cropped_height); + EXPECT_EQ(640, cropped_width); + EXPECT_EQ(480, cropped_height); + + // Request 16:9 but QVGA has 15:11 pixel aspect ratio. Expect horizontal crop. + // This occurs on Logitech B910 on OSX 10.5/10.6/10.7 in Hangouts. + ComputeCrop(640, 360, // Crop size 16:9 + 320, 240, // Frame is 4:3 + 15, 11, // Pixel aspect ratio is wide + 0, + &cropped_width, &cropped_height); + EXPECT_EQ(312, cropped_width); + EXPECT_EQ(240, cropped_height); + + // Request 16:10 but QVGA has 15:11 pixel aspect ratio. + // Expect horizontal crop. + // This occurs on Logitech B910 on OSX 10.5/10.6/10.7 in gmail. + ComputeCrop(640, 400, // Crop size 16:10 + 320, 240, // Frame is 4:3 + 15, 11, // Pixel aspect ratio is wide + 0, + &cropped_width, &cropped_height); + EXPECT_EQ(280, cropped_width); + EXPECT_EQ(240, cropped_height); + + // Request 16:9 but VGA has 6:5 pixel aspect ratio. Expect vertical crop. + // This occurs on Logitech QuickCam Pro C9000 on OSX + ComputeCrop(640, 360, // Crop size 16:9 + 640, 480, // Frame is 4:3 + 6, 5, // Pixel aspect ratio is wide + 0, + &cropped_width, &cropped_height); + EXPECT_EQ(640, cropped_width); + EXPECT_EQ(432, cropped_height); + + // Request 16:10 but HD is 16:9. Expect horizontal crop. + // This occurs in settings and local preview with HD experiment. + ComputeCrop(1280, 800, // Crop size 16:10 + 1280, 720, // Frame is 4:3 + 1, 1, // Pixel aspect ratio is wide + 0, + &cropped_width, &cropped_height); + EXPECT_EQ(1152, cropped_width); + EXPECT_EQ(720, cropped_height); + + // Request 16:9 but HD has 3:4 pixel aspect ratio. Expect vertical crop. + // This occurs on Logitech B910 on OSX 10.5/10.6.7 but not OSX 10.6.8 or 10.7 + ComputeCrop(1280, 720, // Crop size 16:9 + 1280, 720, // Frame is 4:3 + 3, 4, // Pixel aspect ratio is wide + 0, + &cropped_width, &cropped_height); + EXPECT_EQ(1280, cropped_width); + EXPECT_EQ(540, cropped_height); + + // Request 16:9 to 3:4 (portrait). Expect no cropping. + ComputeCrop(640, 360, // Crop size 16:9 + 640, 480, // Frame is 3:4 portrait + 1, 1, // Normal 1:1 pixels + 90, + &cropped_width, &cropped_height); + EXPECT_EQ(640, cropped_width); + EXPECT_EQ(480, cropped_height); + + // Cropped size 0x0. Expect no cropping. + // This is used when adding multiple capturers + ComputeCrop(0, 0, // Crop size 0x0 + 1024, 768, // Frame is 3:4 portrait + 1, 1, // Normal 1:1 pixels + 0, + &cropped_width, &cropped_height); + EXPECT_EQ(1024, cropped_width); + EXPECT_EQ(768, cropped_height); +} + +} // namespace cricket diff --git a/talk/media/base/videoengine_unittest.h b/talk/media/base/videoengine_unittest.h new file mode 100644 index 000000000..dcef4ac6c --- /dev/null +++ b/talk/media/base/videoengine_unittest.h @@ -0,0 +1,1644 @@ +// libjingle +// Copyright 2004 Google Inc. All rights reserved. +// +// 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. + +#ifndef TALK_MEDIA_BASE_VIDEOENGINE_UNITTEST_H_ +#define TALK_MEDIA_BASE_VIDEOENGINE_UNITTEST_H_ + +#include +#include + +#include "talk/base/bytebuffer.h" +#include "talk/base/gunit.h" +#include "talk/base/timeutils.h" +#include "talk/media/base/fakenetworkinterface.h" +#include "talk/media/base/fakevideocapturer.h" +#include "talk/media/base/fakevideorenderer.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/streamparams.h" + +#ifdef WIN32 +#include // NOLINT +#endif + +#define EXPECT_FRAME_WAIT(c, w, h, t) \ + EXPECT_EQ_WAIT((c), renderer_.num_rendered_frames(), (t)); \ + EXPECT_EQ((w), renderer_.width()); \ + EXPECT_EQ((h), renderer_.height()); \ + EXPECT_EQ(0, renderer_.errors()); \ + +#define EXPECT_FRAME_ON_RENDERER_WAIT(r, c, w, h, t) \ + EXPECT_EQ_WAIT((c), (r).num_rendered_frames(), (t)); \ + EXPECT_EQ((w), (r).width()); \ + EXPECT_EQ((h), (r).height()); \ + EXPECT_EQ(0, (r).errors()); \ + +static const uint32 kTimeout = 5000U; +static const uint32 kSsrc = 1234u; +static const uint32 kRtxSsrc = 4321u; +static const uint32 kSsrcs4[] = {1, 2, 3, 4}; + +inline bool IsEqualRes(const cricket::VideoCodec& a, int w, int h, int fps) { + return a.width == w && a.height == h && a.framerate == fps; +} + +inline bool IsEqualCodec(const cricket::VideoCodec& a, + const cricket::VideoCodec& b) { + return a.id == b.id && a.name == b.name && + IsEqualRes(a, b.width, b.height, b.framerate); +} + +inline std::ostream& operator<<(std::ostream& s, const cricket::VideoCodec& c) { + s << "{" << c.name << "(" << c.id << "), " + << c.width << "x" << c.height << "x" << c.framerate << "}"; + return s; +} + +inline int TimeBetweenSend(const cricket::VideoCodec& codec) { + return static_cast ( + cricket::VideoFormat::FpsToInterval(codec.framerate) / + talk_base::kNumNanosecsPerMillisec); +} + +// Fake video engine that makes it possible to test enabling and disabling +// capturer (checking that the engine state is updated and that the capturer +// is indeed capturing) without having to create a channel. It also makes it +// possible to test that the media processors are indeed being called when +// registered. +template +class VideoEngineOverride : public T { + public: + VideoEngineOverride() { + } + virtual ~VideoEngineOverride() { + } + bool is_camera_on() const { return T::GetVideoCapturer()->IsRunning(); } + void set_has_senders(bool has_senders) { + if (has_senders) { + this->RegisterSender(this, + &VideoEngineOverride::OnLocalFrame, + &VideoEngineOverride::OnLocalFrameFormat); + } else { + this->UnregisterSender(this); + } + } + void OnLocalFrame(cricket::VideoCapturer*, + const cricket::VideoFrame*) { + } + void OnLocalFrameFormat(cricket::VideoCapturer*, + const cricket::VideoFormat*) { + } + + void TriggerMediaFrame( + uint32 ssrc, cricket::VideoFrame* frame, bool* drop_frame) { + T::SignalMediaFrame(ssrc, frame, drop_frame); + } +}; + +// Macroes that declare test functions for a given test class, before and after +// Init(). +// To use, define a test function called FooBody and pass Foo to the macro. +#define TEST_PRE_VIDEOENGINE_INIT(TestClass, func) \ + TEST_F(TestClass, func##PreInit) { \ + func##Body(); \ + } +#define TEST_POST_VIDEOENGINE_INIT(TestClass, func) \ + TEST_F(TestClass, func##PostInit) { \ + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); \ + func##Body(); \ + engine_.Terminate(); \ + } + +template +class VideoEngineTest : public testing::Test { + protected: + // Tests starting and stopping the engine, and creating a channel. + void StartupShutdown() { + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + cricket::VideoMediaChannel* channel = engine_.CreateChannel(NULL); + EXPECT_TRUE(channel != NULL); + delete channel; + engine_.Terminate(); + } + +#ifdef WIN32 + // Tests that the COM reference count is not munged by the engine. + // Test to make sure LMI does not munge the CoInitialize reference count. + void CheckCoInitialize() { + // Initial refcount should be 0. + EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_MULTITHREADED)); + + // Engine should start even with COM already inited. + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + engine_.Terminate(); + // Refcount after terminate should be 1; this tests if it is nonzero. + EXPECT_EQ(S_FALSE, CoInitializeEx(NULL, COINIT_MULTITHREADED)); + // Decrement refcount to (hopefully) 0. + CoUninitialize(); + CoUninitialize(); + + // Ensure refcount is 0. + EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_MULTITHREADED)); + CoUninitialize(); + } +#endif + + // Tests starting and stopping the capturer. + void SetCapture() { + EXPECT_FALSE(engine_.GetVideoCapturer()); + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + ResetCapturer(); + EXPECT_TRUE(engine_.GetVideoCapturer() != NULL); + EXPECT_FALSE(engine_.is_camera_on()); + EXPECT_TRUE(engine_.SetCapture(true)); + EXPECT_TRUE(engine_.is_camera_on()); + EXPECT_TRUE(engine_.SetCapture(false)); + EXPECT_FALSE(engine_.is_camera_on()); + engine_.set_has_senders(true); + EXPECT_TRUE(engine_.is_camera_on()); + EXPECT_TRUE(engine_.SetCapture(true)); + EXPECT_TRUE(engine_.is_camera_on()); + EXPECT_TRUE(engine_.SetCapture(false)); + EXPECT_TRUE(engine_.is_camera_on()); + engine_.set_has_senders(false); + EXPECT_FALSE(engine_.is_camera_on()); + EXPECT_TRUE(engine_.SetCapture(true)); + EXPECT_TRUE(engine_.is_camera_on()); + EXPECT_TRUE(engine_.SetCapture(false)); + EXPECT_FALSE(engine_.is_camera_on()); + EXPECT_TRUE(engine_.SetVideoCapturer(NULL)); + EXPECT_TRUE(engine_.GetVideoCapturer() == NULL); + engine_.Terminate(); + } + void ResetCapturer() { + cricket::Device device("test", "device"); + video_capturer_.reset(new cricket::FakeVideoCapturer); + EXPECT_TRUE(engine_.SetVideoCapturer(video_capturer_.get())); + } + + void ConstrainNewCodecBody() { + cricket::VideoCodec empty, in, out; + cricket::VideoCodec max_settings(engine_.codecs()[0].id, + engine_.codecs()[0].name, + 1280, 800, 30, 0); + + // set max settings of 1280x960x30 + EXPECT_TRUE(engine_.SetDefaultEncoderConfig( + cricket::VideoEncoderConfig(max_settings))); + + // don't constrain the max resolution + in = max_settings; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED2(IsEqualCodec, out, in); + + // constrain resolution greater than the max and wider aspect, + // picking best aspect (16:10) + in.width = 1380; + in.height = 800; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED4(IsEqualRes, out, 1280, 720, 30); + + // constrain resolution greater than the max and narrow aspect, + // picking best aspect (16:9) + in.width = 1280; + in.height = 740; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED4(IsEqualRes, out, 1280, 720, 30); + + // constrain resolution greater than the max, picking equal aspect (4:3) + in.width = 1280; + in.height = 960; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED4(IsEqualRes, out, 1280, 800, 30); + + // constrain resolution greater than the max, picking equal aspect (16:10) + in.width = 1280; + in.height = 800; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED4(IsEqualRes, out, 1280, 800, 30); + + // reduce max settings to 640x480x30 + max_settings.width = 640; + max_settings.height = 480; + EXPECT_TRUE(engine_.SetDefaultEncoderConfig( + cricket::VideoEncoderConfig(max_settings))); + + // don't constrain the max resolution + in = max_settings; + in.width = 640; + in.height = 480; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED2(IsEqualCodec, out, in); + + // keep 16:10 if they request it + in.height = 400; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED2(IsEqualCodec, out, in); + + // don't constrain lesser 4:3 resolutions + in.width = 320; + in.height = 240; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED2(IsEqualCodec, out, in); + + // don't constrain lesser 16:10 resolutions + in.width = 320; + in.height = 200; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED2(IsEqualCodec, out, in); + + // requested resolution of 0x0 succeeds + in.width = 0; + in.height = 0; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED2(IsEqualCodec, out, in); + + // constrain resolution lesser than the max and wider aspect, + // picking best aspect (16:9) + in.width = 350; + in.height = 201; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED4(IsEqualRes, out, 320, 180, 30); + + // constrain resolution greater than the max and narrow aspect, + // picking best aspect (4:3) + in.width = 350; + in.height = 300; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED4(IsEqualRes, out, 320, 240, 30); + + // constrain resolution greater than the max and wider aspect, + // picking best aspect (16:9) + in.width = 1380; + in.height = 800; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED4(IsEqualRes, out, 640, 360, 30); + + // constrain resolution greater than the max and narrow aspect, + // picking best aspect (4:3) + in.width = 1280; + in.height = 900; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED4(IsEqualRes, out, 640, 480, 30); + + // constrain resolution greater than the max, picking equal aspect (4:3) + in.width = 1280; + in.height = 960; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED4(IsEqualRes, out, 640, 480, 30); + + // constrain resolution greater than the max, picking equal aspect (16:10) + in.width = 1280; + in.height = 800; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED4(IsEqualRes, out, 640, 400, 30); + + // constrain res & fps greater than the max + in.framerate = 50; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED4(IsEqualRes, out, 640, 400, 30); + + // reduce max settings to 160x100x10 + max_settings.width = 160; + max_settings.height = 100; + max_settings.framerate = 10; + EXPECT_TRUE(engine_.SetDefaultEncoderConfig( + cricket::VideoEncoderConfig(max_settings))); + + // constrain res & fps to new max + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED4(IsEqualRes, out, 160, 100, 10); + + // allow 4:3 "comparable" resolutions + in.width = 160; + in.height = 120; + in.framerate = 10; + EXPECT_TRUE(engine_.CanSendCodec(in, empty, &out)); + EXPECT_PRED4(IsEqualRes, out, 160, 120, 10); + } + + void ConstrainRunningCodecBody() { + cricket::VideoCodec in, out, current; + cricket::VideoCodec max_settings(engine_.codecs()[0].id, + engine_.codecs()[0].name, + 1280, 800, 30, 0); + + // set max settings of 1280x960x30 + EXPECT_TRUE(engine_.SetDefaultEncoderConfig( + cricket::VideoEncoderConfig(max_settings))); + + // establish current call at 1280x800x30 (16:10) + current = max_settings; + current.height = 800; + + // Don't constrain current resolution + in = current; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED2(IsEqualCodec, out, in); + + // requested resolution of 0x0 succeeds + in.width = 0; + in.height = 0; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED2(IsEqualCodec, out, in); + + // Reduce an intermediate resolution down to the next lowest one, preserving + // aspect ratio. + in.width = 800; + in.height = 600; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 640, 400, 30); + + // Clamping by aspect ratio, but still never return a dimension higher than + // requested. + in.width = 1280; + in.height = 720; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 1280, 720, 30); + + in.width = 1279; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 960, 600, 30); + + in.width = 1281; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 1280, 720, 30); + + // Clamp large resolutions down, always preserving aspect + in.width = 1920; + in.height = 1080; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 1280, 800, 30); + + in.width = 1921; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 1280, 800, 30); + + in.width = 1919; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 1280, 800, 30); + + // reduce max settings to 640x480x30 + max_settings.width = 640; + max_settings.height = 480; + EXPECT_TRUE(engine_.SetDefaultEncoderConfig( + cricket::VideoEncoderConfig(max_settings))); + + // establish current call at 640x400x30 (16:10) + current = max_settings; + current.height = 400; + + // Don't constrain current resolution + in = current; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED2(IsEqualCodec, out, in); + + // requested resolution of 0x0 succeeds + in.width = 0; + in.height = 0; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED2(IsEqualCodec, out, in); + + // Reduce an intermediate resolution down to the next lowest one, preserving + // aspect ratio. + in.width = 400; + in.height = 300; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 320, 200, 30); + + // Clamping by aspect ratio, but still never return a dimension higher than + // requested. + in.width = 640; + in.height = 360; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 640, 360, 30); + + in.width = 639; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 480, 300, 30); + + in.width = 641; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 640, 360, 30); + + // Clamp large resolutions down, always preserving aspect + in.width = 1280; + in.height = 800; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 640, 400, 30); + + in.width = 1281; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 640, 400, 30); + + in.width = 1279; + EXPECT_TRUE(engine_.CanSendCodec(in, current, &out)); + EXPECT_PRED4(IsEqualRes, out, 640, 400, 30); + + // Should fail for any that are smaller than our supported formats + in.width = 80; + in.height = 80; + EXPECT_FALSE(engine_.CanSendCodec(in, current, &out)); + + in.height = 50; + EXPECT_FALSE(engine_.CanSendCodec(in, current, &out)); + } + + VideoEngineOverride engine_; + talk_base::scoped_ptr video_capturer_; +}; + +template +class VideoMediaChannelTest : public testing::Test, + public sigslot::has_slots<> { + protected: + virtual cricket::VideoCodec DefaultCodec() = 0; + + virtual cricket::StreamParams DefaultSendStreamParams() { + return cricket::StreamParams::CreateLegacy(kSsrc); + } + + virtual void SetUp() { + cricket::Device device("test", "device"); + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + video_capturer_.reset(new cricket::FakeVideoCapturer); + EXPECT_TRUE(video_capturer_.get() != NULL); + EXPECT_TRUE(engine_.SetVideoCapturer(video_capturer_.get())); + channel_.reset(engine_.CreateChannel(NULL)); + EXPECT_TRUE(channel_.get() != NULL); + ConnectVideoChannelError(); + network_interface_.SetDestination(channel_.get()); + channel_->SetInterface(&network_interface_); + SetRendererAsDefault(); + media_error_ = cricket::VideoMediaChannel::ERROR_NONE; + channel_->SetRecvCodecs(engine_.codecs()); + EXPECT_TRUE(channel_->AddSendStream(DefaultSendStreamParams())); + } + void SetUpSecondStream() { + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc))); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc+2))); + // SetUp() already added kSsrc make sure duplicate SSRCs cant be added. + EXPECT_FALSE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(kSsrc))); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(kSsrc+2))); + // Make the second renderer available for use by a new stream. + EXPECT_TRUE(channel_->SetRenderer(kSsrc+2, &renderer2_)); + } + virtual void TearDown() { + channel_.reset(); + engine_.Terminate(); + } + void ConnectVideoChannelError() { + channel_->SignalMediaError.connect(this, + &VideoMediaChannelTest::OnVideoChannelError); + } + bool SetDefaultCodec() { + return SetOneCodec(DefaultCodec()); + } + void SetRendererAsDefault() { + EXPECT_TRUE(channel_->SetRenderer(0, &renderer_)); + } + + bool SetOneCodec(int pt, const char* name, int w, int h, int fr) { + return SetOneCodec(cricket::VideoCodec(pt, name, w, h, fr, 0)); + } + bool SetOneCodec(const cricket::VideoCodec& codec) { + std::vector codecs; + codecs.push_back(codec); + bool sending = channel_->sending(); + bool success = SetSend(false); + if (success) + success = channel_->SetSendCodecs(codecs); + if (success) + success = SetSend(sending); + return success; + } + bool SetSend(bool send) { + return channel_->SetSend(send); + } + int DrainOutgoingPackets() { + int packets = 0; + do { + packets = NumRtpPackets(); + // 100 ms should be long enough. + talk_base::Thread::Current()->ProcessMessages(100); + } while (NumRtpPackets() > packets); + return NumRtpPackets(); + } + bool SendFrame() { + return video_capturer_.get() && + video_capturer_->CaptureFrame(); + } + bool WaitAndSendFrame(int wait_ms) { + bool ret = talk_base::Thread::Current()->ProcessMessages(wait_ms); + ret &= SendFrame(); + return ret; + } + // Sends frames and waits for the decoder to be fully initialized. + // Returns the number of frames that were sent. + int WaitForDecoder() { +#if defined(HAVE_OPENMAX) + // Send enough frames for the OpenMAX decoder to continue processing, and + // return the number of frames sent. + // Send frames for a full kTimeout's worth of 15fps video. + int frame_count = 0; + while (frame_count < static_cast(kTimeout) / 66) { + EXPECT_TRUE(WaitAndSendFrame(66)); + ++frame_count; + } + return frame_count; +#else + return 0; +#endif + } + bool SendCustomVideoFrame(int w, int h) { + if (!video_capturer_.get()) return false; + return video_capturer_->CaptureCustomFrame(w, h, cricket::FOURCC_I420); + } + int NumRtpBytes() { + return network_interface_.NumRtpBytes(); + } + int NumRtpBytes(uint32 ssrc) { + return network_interface_.NumRtpBytes(ssrc); + } + int NumRtpPackets() { + return network_interface_.NumRtpPackets(); + } + int NumRtpPackets(uint32 ssrc) { + return network_interface_.NumRtpPackets(ssrc); + } + int NumSentSsrcs() { + return network_interface_.NumSentSsrcs(); + } + const talk_base::Buffer* GetRtpPacket(int index) { + return network_interface_.GetRtpPacket(index); + } + int NumRtcpPackets() { + return network_interface_.NumRtcpPackets(); + } + const talk_base::Buffer* GetRtcpPacket(int index) { + return network_interface_.GetRtcpPacket(index); + } + static int GetPayloadType(const talk_base::Buffer* p) { + int pt = -1; + ParseRtpPacket(p, NULL, &pt, NULL, NULL, NULL, NULL); + return pt; + } + static bool ParseRtpPacket(const talk_base::Buffer* p, bool* x, int* pt, + int* seqnum, uint32* tstamp, uint32* ssrc, + std::string* payload) { + talk_base::ByteBuffer buf(p->data(), p->length()); + uint8 u08 = 0; + uint16 u16 = 0; + uint32 u32 = 0; + + // Read X and CC fields. + if (!buf.ReadUInt8(&u08)) return false; + bool extension = ((u08 & 0x10) != 0); + uint8 cc = (u08 & 0x0F); + if (x) *x = extension; + + // Read PT field. + if (!buf.ReadUInt8(&u08)) return false; + if (pt) *pt = (u08 & 0x7F); + + // Read Sequence Number field. + if (!buf.ReadUInt16(&u16)) return false; + if (seqnum) *seqnum = u16; + + // Read Timestamp field. + if (!buf.ReadUInt32(&u32)) return false; + if (tstamp) *tstamp = u32; + + // Read SSRC field. + if (!buf.ReadUInt32(&u32)) return false; + if (ssrc) *ssrc = u32; + + // Skip CSRCs. + for (uint8 i = 0; i < cc; ++i) { + if (!buf.ReadUInt32(&u32)) return false; + } + + // Skip extension header. + if (extension) { + // Read Profile-specific extension header ID + if (!buf.ReadUInt16(&u16)) return false; + + // Read Extension header length + if (!buf.ReadUInt16(&u16)) return false; + uint16 ext_header_len = u16; + + // Read Extension header + for (uint16 i = 0; i < ext_header_len; ++i) { + if (!buf.ReadUInt32(&u32)) return false; + } + } + + if (payload) { + return buf.ReadString(payload, buf.Length()); + } + return true; + } + + // Parse all RTCP packet, from start_index to stop_index, and count how many + // FIR (PT=206 and FMT=4 according to RFC 5104). If successful, set the count + // and return true. + bool CountRtcpFir(int start_index, int stop_index, int* fir_count) { + int count = 0; + for (int i = start_index; i < stop_index; ++i) { + talk_base::scoped_ptr p(GetRtcpPacket(i)); + talk_base::ByteBuffer buf(p->data(), p->length()); + size_t total_len = 0; + // The packet may be a compound RTCP packet. + while (total_len < p->length()) { + // Read FMT, type and length. + uint8 fmt = 0; + uint8 type = 0; + uint16 length = 0; + if (!buf.ReadUInt8(&fmt)) return false; + fmt &= 0x1F; + if (!buf.ReadUInt8(&type)) return false; + if (!buf.ReadUInt16(&length)) return false; + buf.Consume(length * 4); // Skip RTCP data. + total_len += (length + 1) * 4; + if ((192 == type) || ((206 == type) && (4 == fmt))) { + ++count; + } + } + } + + if (fir_count) { + *fir_count = count; + } + return true; + } + + void OnVideoChannelError(uint32 ssrc, + cricket::VideoMediaChannel::Error error) { + media_error_ = error; + } + + // Test that SetSend works. + void SetSend() { + EXPECT_FALSE(channel_->sending()); + EXPECT_TRUE(SetOneCodec(DefaultCodec())); + EXPECT_FALSE(channel_->sending()); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->sending()); + EXPECT_TRUE(SendFrame()); + EXPECT_TRUE_WAIT(NumRtpPackets() > 0, kTimeout); + EXPECT_TRUE(SetSend(false)); + EXPECT_FALSE(channel_->sending()); + } + // Test that SetSend fails without codecs being set. + void SetSendWithoutCodecs() { + EXPECT_FALSE(channel_->sending()); + EXPECT_FALSE(SetSend(true)); + EXPECT_FALSE(channel_->sending()); + } + // Test that we properly set the send and recv buffer sizes by the time + // SetSend is called. + void SetSendSetsTransportBufferSizes() { + EXPECT_TRUE(SetOneCodec(DefaultCodec())); + EXPECT_TRUE(SetSend(true)); + // TODO(sriniv): Remove or re-enable this. + // As part of b/8030474, send-buffer is size now controlled through + // portallocator flags. Its not set by channels. + // EXPECT_EQ(64 * 1024, network_interface_.sendbuf_size()); + EXPECT_EQ(64 * 1024, network_interface_.recvbuf_size()); + } + // Tests that we can send frames and the right payload type is used. + void Send(const cricket::VideoCodec& codec) { + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(SendFrame()); + EXPECT_TRUE_WAIT(NumRtpPackets() > 0, kTimeout); + talk_base::scoped_ptr p(GetRtpPacket(0)); + EXPECT_EQ(codec.id, GetPayloadType(p.get())); + } + // Tests that we can send and receive frames. + void SendAndReceive(const cricket::VideoCodec& codec) { + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_EQ(0, renderer_.num_rendered_frames()); + EXPECT_TRUE(SendFrame()); + EXPECT_FRAME_WAIT(1, codec.width, codec.height, kTimeout); + talk_base::scoped_ptr p(GetRtpPacket(0)); + EXPECT_EQ(codec.id, GetPayloadType(p.get())); + } + // Tests that we only get a VideoRenderer::SetSize() callback when needed. + void SendManyResizeOnce() { + cricket::VideoCodec codec(DefaultCodec()); + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_EQ(0, renderer_.num_rendered_frames()); + EXPECT_TRUE(WaitAndSendFrame(30)); + EXPECT_FRAME_WAIT(1, codec.width, codec.height, kTimeout); + EXPECT_TRUE(WaitAndSendFrame(30)); + EXPECT_FRAME_WAIT(2, codec.width, codec.height, kTimeout); + talk_base::scoped_ptr p(GetRtpPacket(0)); + EXPECT_EQ(codec.id, GetPayloadType(p.get())); + EXPECT_EQ(1, renderer_.num_set_sizes()); + + codec.width /= 2; + codec.height /= 2; + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(WaitAndSendFrame(30)); + EXPECT_FRAME_WAIT(3, codec.width, codec.height, kTimeout); + EXPECT_EQ(2, renderer_.num_set_sizes()); + } + // Test that stats work properly for a 1-1 call. + void GetStats() { + SendAndReceive(DefaultCodec()); + cricket::VideoMediaInfo info; + EXPECT_TRUE(channel_->GetStats(&info)); + + ASSERT_EQ(1U, info.senders.size()); + // TODO(whyuan): bytes_sent and bytes_rcvd are different. Are both payload? + EXPECT_GT(info.senders[0].bytes_sent, 0); + EXPECT_EQ(NumRtpPackets(), info.senders[0].packets_sent); + EXPECT_EQ(0.0, info.senders[0].fraction_lost); + EXPECT_EQ(0, info.senders[0].firs_rcvd); + EXPECT_EQ(0, info.senders[0].nacks_rcvd); + EXPECT_EQ(DefaultCodec().width, info.senders[0].frame_width); + EXPECT_EQ(DefaultCodec().height, info.senders[0].frame_height); + EXPECT_GT(info.senders[0].framerate_input, 0); + EXPECT_GT(info.senders[0].framerate_sent, 0); + + ASSERT_EQ(1U, info.receivers.size()); + EXPECT_EQ(1U, info.senders[0].ssrcs.size()); + EXPECT_EQ(1U, info.receivers[0].ssrcs.size()); + EXPECT_EQ(info.senders[0].ssrcs[0], info.receivers[0].ssrcs[0]); + EXPECT_EQ(NumRtpBytes(), info.receivers[0].bytes_rcvd); + EXPECT_EQ(NumRtpPackets(), info.receivers[0].packets_rcvd); + EXPECT_EQ(0.0, info.receivers[0].fraction_lost); + EXPECT_EQ(0, info.receivers[0].packets_lost); + EXPECT_EQ(0, info.receivers[0].packets_concealed); + EXPECT_EQ(0, info.receivers[0].firs_sent); + EXPECT_EQ(0, info.receivers[0].nacks_sent); + EXPECT_EQ(DefaultCodec().width, info.receivers[0].frame_width); + EXPECT_EQ(DefaultCodec().height, info.receivers[0].frame_height); + EXPECT_GT(info.receivers[0].framerate_rcvd, 0); + EXPECT_GT(info.receivers[0].framerate_decoded, 0); + EXPECT_GT(info.receivers[0].framerate_output, 0); + } + // Test that stats work properly for a conf call with multiple recv streams. + void GetStatsMultipleRecvStreams() { + cricket::FakeVideoRenderer renderer1, renderer2; + EXPECT_TRUE(SetOneCodec(DefaultCodec())); + cricket::VideoOptions vmo; + vmo.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(vmo)); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(1))); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(2))); + EXPECT_TRUE(channel_->SetRenderer(1, &renderer1)); + EXPECT_TRUE(channel_->SetRenderer(2, &renderer2)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_EQ(0, renderer1.num_rendered_frames()); + EXPECT_EQ(0, renderer2.num_rendered_frames()); + std::vector ssrcs; + ssrcs.push_back(1); + ssrcs.push_back(2); + network_interface_.SetConferenceMode(true, ssrcs); + EXPECT_TRUE(SendFrame()); + EXPECT_FRAME_ON_RENDERER_WAIT( + renderer1, 1, DefaultCodec().width, DefaultCodec().height, kTimeout); + EXPECT_FRAME_ON_RENDERER_WAIT( + renderer2, 1, DefaultCodec().width, DefaultCodec().height, kTimeout); + cricket::VideoMediaInfo info; + EXPECT_TRUE(channel_->GetStats(&info)); + + ASSERT_EQ(1U, info.senders.size()); + // TODO(whyuan): bytes_sent and bytes_rcvd are different. Are both payload? + EXPECT_GT(info.senders[0].bytes_sent, 0); + EXPECT_EQ(NumRtpPackets(), info.senders[0].packets_sent); + EXPECT_EQ(0.0, info.senders[0].fraction_lost); + EXPECT_EQ(0, info.senders[0].firs_rcvd); + EXPECT_EQ(0, info.senders[0].nacks_rcvd); + EXPECT_EQ(DefaultCodec().width, info.senders[0].frame_width); + EXPECT_EQ(DefaultCodec().height, info.senders[0].frame_height); + EXPECT_GT(info.senders[0].framerate_input, 0); + EXPECT_GT(info.senders[0].framerate_sent, 0); + + ASSERT_EQ(2U, info.receivers.size()); + for (size_t i = 0; i < info.receivers.size(); ++i) { + EXPECT_EQ(1U, info.receivers[i].ssrcs.size()); + EXPECT_EQ(i + 1, info.receivers[i].ssrcs[0]); + EXPECT_EQ(NumRtpBytes(), info.receivers[i].bytes_rcvd); + EXPECT_EQ(NumRtpPackets(), info.receivers[i].packets_rcvd); + EXPECT_EQ(0.0, info.receivers[i].fraction_lost); + EXPECT_EQ(0, info.receivers[i].packets_lost); + EXPECT_EQ(0, info.receivers[i].packets_concealed); + EXPECT_EQ(0, info.receivers[i].firs_sent); + EXPECT_EQ(0, info.receivers[i].nacks_sent); + EXPECT_EQ(DefaultCodec().width, info.receivers[i].frame_width); + EXPECT_EQ(DefaultCodec().height, info.receivers[i].frame_height); + EXPECT_GT(info.receivers[i].framerate_rcvd, 0); + EXPECT_GT(info.receivers[i].framerate_decoded, 0); + EXPECT_GT(info.receivers[i].framerate_output, 0); + } + } + // Test that stats work properly for a conf call with multiple send streams. + void GetStatsMultipleSendStreams() { + // Normal setup; note that we set the SSRC explicitly to ensure that + // it will come first in the senders map. + EXPECT_TRUE(SetOneCodec(DefaultCodec())); + cricket::VideoOptions vmo; + vmo.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(vmo)); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(1234))); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_TRUE(SendFrame()); + EXPECT_TRUE_WAIT(NumRtpPackets() > 0, kTimeout); + EXPECT_FRAME_WAIT(1, DefaultCodec().width, DefaultCodec().height, kTimeout); + + // Add an additional capturer, and hook up a renderer to receive it. + cricket::FakeVideoRenderer renderer1; + talk_base::scoped_ptr capturer( + new cricket::FakeVideoCapturer); + capturer->SetScreencast(true); + cricket::VideoFormat format(1024, 768, + cricket::VideoFormat::FpsToInterval(5), 0); + EXPECT_EQ(cricket::CS_RUNNING, capturer->Start(format)); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(5678))); + EXPECT_TRUE(channel_->SetCapturer(5678, capturer.get())); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(5678))); + EXPECT_TRUE(channel_->SetRenderer(5678, &renderer1)); + EXPECT_TRUE(capturer->CaptureCustomFrame(1024, 768, cricket::FOURCC_I420)); + EXPECT_FRAME_ON_RENDERER_WAIT(renderer1, 1, 1024, 768, kTimeout); + + // Get stats, and make sure they are correct for two senders. + cricket::VideoMediaInfo info; + EXPECT_TRUE(channel_->GetStats(&info)); + ASSERT_EQ(2U, info.senders.size()); + EXPECT_EQ(NumRtpPackets(), + info.senders[0].packets_sent + info.senders[1].packets_sent); + EXPECT_EQ(1U, info.senders[0].ssrcs.size()); + EXPECT_EQ(1234U, info.senders[0].ssrcs[0]); + EXPECT_EQ(DefaultCodec().width, info.senders[0].frame_width); + EXPECT_EQ(DefaultCodec().height, info.senders[0].frame_height); + EXPECT_EQ(1U, info.senders[1].ssrcs.size()); + EXPECT_EQ(5678U, info.senders[1].ssrcs[0]); + EXPECT_EQ(1024, info.senders[1].frame_width); + EXPECT_EQ(768, info.senders[1].frame_height); + // The capturer must be unregistered here as it runs out of it's scope next. + EXPECT_TRUE(channel_->SetCapturer(5678, NULL)); + } + + // Test that we can set the bandwidth to auto or a specific value. + void SetSendBandwidth() { + EXPECT_TRUE(channel_->SetSendBandwidth(true, -1)); + EXPECT_TRUE(channel_->SetSendBandwidth(true, 128 * 1024)); + EXPECT_TRUE(channel_->SetSendBandwidth(false, -1)); + EXPECT_TRUE(channel_->SetSendBandwidth(false, 128 * 1024)); + } + // Test that we can set the SSRC for the default send source. + void SetSendSsrc() { + EXPECT_TRUE(SetDefaultCodec()); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(SendFrame()); + EXPECT_TRUE_WAIT(NumRtpPackets() > 0, kTimeout); + uint32 ssrc = 0; + talk_base::scoped_ptr p(GetRtpPacket(0)); + ParseRtpPacket(p.get(), NULL, NULL, NULL, NULL, &ssrc, NULL); + EXPECT_EQ(kSsrc, ssrc); + EXPECT_EQ(NumRtpPackets(), NumRtpPackets(ssrc)); + EXPECT_EQ(NumRtpBytes(), NumRtpBytes(ssrc)); + EXPECT_EQ(1, NumSentSsrcs()); + EXPECT_EQ(0, NumRtpPackets(kSsrc - 1)); + EXPECT_EQ(0, NumRtpBytes(kSsrc - 1)); + } + // Test that we can set the SSRC even after codecs are set. + void SetSendSsrcAfterSetCodecs() { + // Remove stream added in Setup. + EXPECT_TRUE(channel_->RemoveSendStream(kSsrc)); + EXPECT_TRUE(SetDefaultCodec()); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(999))); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(WaitAndSendFrame(0)); + EXPECT_TRUE_WAIT(NumRtpPackets() > 0, kTimeout); + uint32 ssrc = 0; + talk_base::scoped_ptr p(GetRtpPacket(0)); + ParseRtpPacket(p.get(), NULL, NULL, NULL, NULL, &ssrc, NULL); + EXPECT_EQ(999u, ssrc); + EXPECT_EQ(NumRtpPackets(), NumRtpPackets(ssrc)); + EXPECT_EQ(NumRtpBytes(), NumRtpBytes(ssrc)); + EXPECT_EQ(1, NumSentSsrcs()); + EXPECT_EQ(0, NumRtpPackets(kSsrc)); + EXPECT_EQ(0, NumRtpBytes(kSsrc)); + } + // Test that we can set the default video renderer before and after + // media is received. + void SetRenderer() { + uint8 data1[] = { + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + talk_base::Buffer packet1(data1, sizeof(data1)); + talk_base::SetBE32(packet1.data() + 8, kSsrc); + channel_->SetRenderer(0, NULL); + EXPECT_TRUE(SetDefaultCodec()); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_EQ(0, renderer_.num_rendered_frames()); + channel_->OnPacketReceived(&packet1); + SetRendererAsDefault(); + EXPECT_TRUE(SendFrame()); + EXPECT_FRAME_WAIT(1, DefaultCodec().width, DefaultCodec().height, kTimeout); + } + + // Tests empty StreamParams is rejected. + void RejectEmptyStreamParams() { + // Remove the send stream that was added during Setup. + EXPECT_TRUE(channel_->RemoveSendStream(kSsrc)); + + cricket::StreamParams empty; + EXPECT_FALSE(channel_->AddSendStream(empty)); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(789u))); + } + + // Tests setting up and configuring a send stream. + void AddRemoveSendStreams() { + EXPECT_TRUE(SetOneCodec(DefaultCodec())); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_TRUE(SendFrame()); + EXPECT_FRAME_WAIT(1, DefaultCodec().width, DefaultCodec().height, kTimeout); + EXPECT_GE(2, NumRtpPackets()); + uint32 ssrc = 0; + size_t last_packet = NumRtpPackets() - 1; + talk_base::scoped_ptr p(GetRtpPacket(last_packet)); + ParseRtpPacket(p.get(), NULL, NULL, NULL, NULL, &ssrc, NULL); + EXPECT_EQ(kSsrc, ssrc); + + // Remove the send stream that was added during Setup. + EXPECT_TRUE(channel_->RemoveSendStream(kSsrc)); + int rtp_packets = NumRtpPackets(); + + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(789u))); + EXPECT_EQ(rtp_packets, NumRtpPackets()); + // Wait 30ms to guarantee the engine does not drop the frame. + EXPECT_TRUE(WaitAndSendFrame(30)); + EXPECT_TRUE_WAIT(NumRtpPackets() > rtp_packets, kTimeout); + + last_packet = NumRtpPackets() - 1; + p.reset(GetRtpPacket(last_packet)); + ParseRtpPacket(p.get(), NULL, NULL, NULL, NULL, &ssrc, NULL); + EXPECT_EQ(789u, ssrc); + } + + // Tests adding streams already exists returns false. + void AddRecvStreamsAlreadyExist() { + cricket::VideoOptions vmo; + vmo.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(vmo)); + + EXPECT_FALSE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(0))); + + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(1))); + EXPECT_FALSE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(1))); + + EXPECT_TRUE(channel_->RemoveRecvStream(1)); + EXPECT_FALSE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(0))); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(1))); + } + + // Tests setting up and configuring multiple incoming streams. + void AddRemoveRecvStreams() { + cricket::FakeVideoRenderer renderer1, renderer2; + cricket::VideoOptions vmo; + vmo.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(vmo)); + // Ensure we can't set the renderer on a non-existent stream. + EXPECT_FALSE(channel_->SetRenderer(1, &renderer1)); + EXPECT_FALSE(channel_->SetRenderer(2, &renderer2)); + cricket::VideoRenderer* renderer; + EXPECT_FALSE(channel_->GetRenderer(1, &renderer)); + EXPECT_FALSE(channel_->GetRenderer(2, &renderer)); + + // Ensure we can add streams. + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(1))); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(2))); + EXPECT_TRUE(channel_->GetRenderer(1, &renderer)); + // Verify the first AddRecvStream hook up to the default renderer. + EXPECT_EQ(&renderer_, renderer); + EXPECT_TRUE(channel_->GetRenderer(2, &renderer)); + EXPECT_TRUE(NULL == renderer); + + // Ensure we can now set the renderers. + EXPECT_TRUE(channel_->SetRenderer(1, &renderer1)); + EXPECT_TRUE(channel_->SetRenderer(2, &renderer2)); + EXPECT_TRUE(channel_->GetRenderer(1, &renderer)); + EXPECT_TRUE(&renderer1 == renderer); + EXPECT_TRUE(channel_->GetRenderer(2, &renderer)); + EXPECT_TRUE(&renderer2 == renderer); + + // Ensure we can change the renderers if needed. + EXPECT_TRUE(channel_->SetRenderer(1, &renderer2)); + EXPECT_TRUE(channel_->SetRenderer(2, &renderer1)); + EXPECT_TRUE(channel_->GetRenderer(1, &renderer)); + EXPECT_TRUE(&renderer2 == renderer); + EXPECT_TRUE(channel_->GetRenderer(2, &renderer)); + EXPECT_TRUE(&renderer1 == renderer); + + EXPECT_TRUE(channel_->RemoveRecvStream(2)); + EXPECT_TRUE(channel_->RemoveRecvStream(1)); + EXPECT_FALSE(channel_->GetRenderer(1, &renderer)); + EXPECT_FALSE(channel_->GetRenderer(2, &renderer)); + } + + // Tests setting up and configuring multiple incoming streams in a + // non-conference call. + void AddRemoveRecvStreamsNoConference() { + cricket::FakeVideoRenderer renderer1, renderer2; + // Ensure we can't set the renderer on a non-existent stream. + EXPECT_FALSE(channel_->SetRenderer(1, &renderer1)); + EXPECT_FALSE(channel_->SetRenderer(2, &renderer2)); + cricket::VideoRenderer* renderer; + EXPECT_FALSE(channel_->GetRenderer(1, &renderer)); + EXPECT_FALSE(channel_->GetRenderer(2, &renderer)); + + // Ensure we can add streams. + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(1))); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(2))); + EXPECT_TRUE(channel_->GetRenderer(1, &renderer)); + // Verify the first AddRecvStream hook up to the default renderer. + EXPECT_EQ(&renderer_, renderer); + EXPECT_TRUE(channel_->GetRenderer(2, &renderer)); + EXPECT_TRUE(NULL == renderer); + + // Ensure we can now set the renderers. + EXPECT_TRUE(channel_->SetRenderer(1, &renderer1)); + EXPECT_TRUE(channel_->SetRenderer(2, &renderer2)); + EXPECT_TRUE(channel_->GetRenderer(1, &renderer)); + EXPECT_TRUE(&renderer1 == renderer); + EXPECT_TRUE(channel_->GetRenderer(2, &renderer)); + EXPECT_TRUE(&renderer2 == renderer); + + // Ensure we can change the renderers if needed. + EXPECT_TRUE(channel_->SetRenderer(1, &renderer2)); + EXPECT_TRUE(channel_->SetRenderer(2, &renderer1)); + EXPECT_TRUE(channel_->GetRenderer(1, &renderer)); + EXPECT_TRUE(&renderer2 == renderer); + EXPECT_TRUE(channel_->GetRenderer(2, &renderer)); + EXPECT_TRUE(&renderer1 == renderer); + + EXPECT_TRUE(channel_->RemoveRecvStream(2)); + EXPECT_TRUE(channel_->RemoveRecvStream(1)); + EXPECT_FALSE(channel_->GetRenderer(1, &renderer)); + EXPECT_FALSE(channel_->GetRenderer(2, &renderer)); + } + + // Test that no frames are rendered after the receive stream have been + // removed. + void AddRemoveRecvStreamAndRender() { + cricket::FakeVideoRenderer renderer1; + EXPECT_TRUE(SetDefaultCodec()); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc))); + EXPECT_TRUE(channel_->SetRenderer(kSsrc, &renderer1)); + + EXPECT_TRUE(SendFrame()); + EXPECT_FRAME_ON_RENDERER_WAIT( + renderer1, 1, DefaultCodec().width, DefaultCodec().height, kTimeout); + EXPECT_TRUE(channel_->RemoveRecvStream(kSsrc)); + // Send three more frames. This is to avoid that the test might be flaky + // due to frame dropping. + for (size_t i = 0; i < 3; ++i) + EXPECT_TRUE(WaitAndSendFrame(100)); + + // Test that no more frames have been rendered. + EXPECT_EQ(1, renderer1.num_rendered_frames()); + + // Re-add the stream again and make sure it renders. + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc))); + // Force the next frame to be a key frame to make the receiving + // decoder happy. + EXPECT_TRUE(channel_->SendIntraFrame()); + + EXPECT_TRUE(channel_->SetRenderer(kSsrc, &renderer1)); + EXPECT_TRUE(SendFrame()); + EXPECT_FRAME_ON_RENDERER_WAIT( + renderer1, 2, DefaultCodec().width, DefaultCodec().height, kTimeout); + } + + // Tests the behavior of incoming streams in a conference scenario. + void SimulateConference() { + cricket::FakeVideoRenderer renderer1, renderer2; + EXPECT_TRUE(SetDefaultCodec()); + cricket::VideoOptions vmo; + vmo.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(vmo)); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(1))); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(2))); + EXPECT_TRUE(channel_->SetRenderer(1, &renderer1)); + EXPECT_TRUE(channel_->SetRenderer(2, &renderer2)); + EXPECT_EQ(0, renderer1.num_rendered_frames()); + EXPECT_EQ(0, renderer2.num_rendered_frames()); + std::vector ssrcs; + ssrcs.push_back(1); + ssrcs.push_back(2); + network_interface_.SetConferenceMode(true, ssrcs); + EXPECT_TRUE(SendFrame()); + EXPECT_FRAME_ON_RENDERER_WAIT( + renderer1, 1, DefaultCodec().width, DefaultCodec().height, kTimeout); + EXPECT_FRAME_ON_RENDERER_WAIT( + renderer2, 1, DefaultCodec().width, DefaultCodec().height, kTimeout); + + talk_base::scoped_ptr p(GetRtpPacket(0)); + EXPECT_EQ(DefaultCodec().id, GetPayloadType(p.get())); + EXPECT_EQ(DefaultCodec().width, renderer1.width()); + EXPECT_EQ(DefaultCodec().height, renderer1.height()); + EXPECT_EQ(DefaultCodec().width, renderer2.width()); + EXPECT_EQ(DefaultCodec().height, renderer2.height()); + EXPECT_TRUE(channel_->RemoveRecvStream(2)); + EXPECT_TRUE(channel_->RemoveRecvStream(1)); + } + + // Tests that we can add and remove capturers and frames are sent out properly + void AddRemoveCapturer() { + const cricket::VideoCodec codec(DefaultCodec()); + const int time_between_send = TimeBetweenSend(codec); + EXPECT_TRUE(SetDefaultCodec()); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_EQ(0, renderer_.num_rendered_frames()); + EXPECT_TRUE(SendFrame()); + EXPECT_FRAME_WAIT(1, codec.width, codec.height, kTimeout); + talk_base::scoped_ptr capturer( + new cricket::FakeVideoCapturer); + capturer->SetScreencast(true); + cricket::VideoFormat format(1024, 768, + cricket::VideoFormat::FpsToInterval(30), 0); + EXPECT_EQ(cricket::CS_RUNNING, capturer->Start(format)); + // All capturers start generating frames with the same timestamp. ViE does + // not allow the same timestamp to be used. Capture one frame before + // associating the capturer with the channel. + EXPECT_TRUE(capturer->CaptureCustomFrame(format.width, format.height, + cricket::FOURCC_I420)); + + int captured_frames = 1; + for (int iterations = 0; iterations < 2; ++iterations) { + EXPECT_TRUE(channel_->SetCapturer(kSsrc, capturer.get())); + talk_base::Thread::Current()->ProcessMessages(time_between_send); + EXPECT_TRUE(capturer->CaptureCustomFrame(format.width, format.height, + cricket::FOURCC_I420)); + ++captured_frames; + EXPECT_FRAME_WAIT(captured_frames, format.width, format.height, kTimeout); + EXPECT_FALSE(renderer_.black_frame()); + EXPECT_TRUE(channel_->SetCapturer(kSsrc, NULL)); + // Make sure a black frame is generated as no new frame is captured. + // A black frame should be the resolution of the send codec. + ++captured_frames; + EXPECT_FRAME_WAIT(captured_frames, codec.width, codec.height, kTimeout); + EXPECT_TRUE(renderer_.black_frame()); + + // The black frame has the same timestamp as the next frame since it's + // timestamp is set to the last frame's timestamp + interval. WebRTC will + // not render a frame with the same timestamp so capture another frame + // with the frame capturer to increment the next frame's timestamp. + EXPECT_TRUE(capturer->CaptureCustomFrame(format.width, format.height, + cricket::FOURCC_I420)); + } + } + + // Tests that if RemoveCapturer is called without a capturer ever being + // added, the plugin shouldn't crash (and no black frame should be sent). + void RemoveCapturerWithoutAdd() { + EXPECT_TRUE(SetOneCodec(DefaultCodec())); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_EQ(0, renderer_.num_rendered_frames()); + EXPECT_TRUE(SendFrame()); + EXPECT_FRAME_WAIT(1, 640, 400, kTimeout); + // No capturer was added, so this RemoveCapturer should + // fail. + EXPECT_FALSE(channel_->SetCapturer(kSsrc, NULL)); + // Wait for kTimeout, to make sure no frames are sent + WAIT(renderer_.num_rendered_frames() != 1, kTimeout); + // Still a single frame, from the original SendFrame() call. + EXPECT_EQ(1, renderer_.num_rendered_frames()); + } + + // Tests that we can add and remove capturer as unique sources. + void AddRemoveCapturerMultipleSources() { + // WebRTC implementation will drop frames if pushed to quickly. Wait the + // interval time to avoid that. + const cricket::VideoFormat send_format( + 1024, + 768, + cricket::VideoFormat::FpsToInterval(30), + 0); + // WebRTC implementation will drop frames if pushed to quickly. Wait the + // interval time to avoid that. + // Set up the stream associated with the engine. + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc))); + EXPECT_TRUE(channel_->SetRenderer(kSsrc, &renderer_)); + cricket::VideoFormat capture_format; // default format + capture_format.interval = cricket::VideoFormat::FpsToInterval(30); + // Set up additional stream 1. + cricket::FakeVideoRenderer renderer1; + EXPECT_FALSE(channel_->SetRenderer(1, &renderer1)); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(1))); + EXPECT_TRUE(channel_->SetRenderer(1, &renderer1)); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(1))); + talk_base::scoped_ptr capturer1( + new cricket::FakeVideoCapturer); + capturer1->SetScreencast(true); + EXPECT_EQ(cricket::CS_RUNNING, capturer1->Start(capture_format)); + // Set up additional stream 2. + cricket::FakeVideoRenderer renderer2; + EXPECT_FALSE(channel_->SetRenderer(2, &renderer2)); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(2))); + EXPECT_TRUE(channel_->SetRenderer(2, &renderer2)); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(2))); + talk_base::scoped_ptr capturer2( + new cricket::FakeVideoCapturer); + capturer2->SetScreencast(true); + EXPECT_EQ(cricket::CS_RUNNING, capturer2->Start(capture_format)); + // State for all the streams. + EXPECT_TRUE(SetOneCodec(DefaultCodec())); + // A limitation in the lmi implementation requires that SetCapturer() is + // called after SetOneCodec(). + // TODO(hellner): this seems like an unnecessary constraint, fix it. + EXPECT_TRUE(channel_->SetCapturer(1, capturer1.get())); + EXPECT_TRUE(channel_->SetCapturer(2, capturer2.get())); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + // Test capturer associated with engine. + EXPECT_TRUE(capturer1->CaptureCustomFrame(1024, 768, cricket::FOURCC_I420)); + EXPECT_FRAME_ON_RENDERER_WAIT(renderer1, 1, 1024, 768, kTimeout); + // Capture a frame with additional capturer2, frames should be received + EXPECT_TRUE(capturer2->CaptureCustomFrame(1024, 768, cricket::FOURCC_I420)); + EXPECT_FRAME_ON_RENDERER_WAIT(renderer2, 1, 1024, 768, kTimeout); + EXPECT_FALSE(channel_->SetCapturer(kSsrc, NULL)); + // The capturers must be unregistered here as it runs out of it's scope + // next. + EXPECT_TRUE(channel_->SetCapturer(1, NULL)); + EXPECT_TRUE(channel_->SetCapturer(2, NULL)); + } + + void HighAspectHighHeightCapturer() { + const int kWidth = 80; + const int kHeight = 10000; + const int kScaledWidth = 20; + const int kScaledHeight = 2500; + + cricket::VideoCodec codec(DefaultCodec()); + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(SetSend(true)); + + cricket::FakeVideoRenderer renderer; + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc))); + EXPECT_TRUE(channel_->SetRenderer(kSsrc, &renderer)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_EQ(0, renderer.num_rendered_frames()); + + EXPECT_TRUE(SendFrame()); + EXPECT_FRAME_ON_RENDERER_WAIT(renderer, 1, codec.width, codec.height, + kTimeout); + + // Registering an external capturer is currently the same as screen casting + // (update the test when this changes). + talk_base::scoped_ptr capturer( + new cricket::FakeVideoCapturer); + capturer->SetScreencast(true); + const std::vector* formats = + capturer->GetSupportedFormats(); + cricket::VideoFormat capture_format = (*formats)[0]; + EXPECT_EQ(cricket::CS_RUNNING, capturer->Start(capture_format)); + // Capture frame to not get same frame timestamps as previous capturer. + capturer->CaptureFrame(); + EXPECT_TRUE(channel_->SetCapturer(kSsrc, capturer.get())); + EXPECT_TRUE(talk_base::Thread::Current()->ProcessMessages(30)); + EXPECT_TRUE(capturer->CaptureCustomFrame(kWidth, kHeight, + cricket::FOURCC_ARGB)); + EXPECT_TRUE(capturer->CaptureFrame()); + EXPECT_FRAME_ON_RENDERER_WAIT(renderer, 2, kScaledWidth, kScaledHeight, + kTimeout); + EXPECT_TRUE(channel_->SetCapturer(kSsrc, NULL)); + } + + // Tests that we can adapt video resolution with 16:10 aspect ratio properly. + void AdaptResolution16x10() { + cricket::VideoCodec codec(DefaultCodec()); + codec.width = 640; + codec.height = 400; + SendAndReceive(codec); + codec.width /= 2; + codec.height /= 2; + // Adapt the resolution. + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(WaitAndSendFrame(30)); + EXPECT_FRAME_WAIT(2, codec.width, codec.height, kTimeout); + } + // Tests that we can adapt video resolution with 4:3 aspect ratio properly. + void AdaptResolution4x3() { + cricket::VideoCodec codec(DefaultCodec()); + codec.width = 640; + codec.height = 400; + SendAndReceive(codec); + codec.width /= 2; + codec.height /= 2; + // Adapt the resolution. + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(WaitAndSendFrame(30)); + EXPECT_FRAME_WAIT(2, codec.width, codec.height, kTimeout); + } + // Tests that we can drop all frames properly. + void AdaptDropAllFrames() { + // Set the channel codec's resolution to 0, which will require the adapter + // to drop all frames. + cricket::VideoCodec codec(DefaultCodec()); + codec.width = codec.height = codec.framerate = 0; + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_EQ(0, renderer_.num_rendered_frames()); + EXPECT_TRUE(SendFrame()); + EXPECT_TRUE(SendFrame()); + talk_base::Thread::Current()->ProcessMessages(500); + EXPECT_EQ(0, renderer_.num_rendered_frames()); + } + // Tests that we can reduce the frame rate on demand properly. + // TODO(fbarchard): This test is flakey on pulse. Fix and re-enable + void AdaptFramerate() { + cricket::VideoCodec codec(DefaultCodec()); + int frame_count = 0; + // The capturer runs at 30 fps. The channel requires 30 fps. + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_EQ(frame_count, renderer_.num_rendered_frames()); + EXPECT_TRUE(WaitAndSendFrame(0)); // Should be rendered. + EXPECT_TRUE(WaitAndSendFrame(30)); // Should be rendered. + frame_count += 2; + EXPECT_FRAME_WAIT(frame_count, codec.width, codec.height, kTimeout); + talk_base::scoped_ptr p(GetRtpPacket(0)); + EXPECT_EQ(codec.id, GetPayloadType(p.get())); + + // The channel requires 15 fps. + codec.framerate = 15; + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(WaitAndSendFrame(0)); // Should be rendered. + EXPECT_TRUE(WaitAndSendFrame(30)); // Should be dropped. + EXPECT_TRUE(WaitAndSendFrame(30)); // Should be rendered. + frame_count += 2; + EXPECT_EQ_WAIT(frame_count, renderer_.num_rendered_frames(), kTimeout); + + // The channel requires 10 fps. + codec.framerate = 10; + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(WaitAndSendFrame(0)); // Should be rendered. + EXPECT_TRUE(WaitAndSendFrame(30)); // Should be dropped. + EXPECT_TRUE(WaitAndSendFrame(30)); // Should be dropped. + EXPECT_TRUE(WaitAndSendFrame(30)); // Should be rendered. + frame_count += 2; + EXPECT_EQ_WAIT(frame_count, renderer_.num_rendered_frames(), kTimeout); + + // The channel requires 8 fps. The adapter adapts to 10 fps, which is the + // closest factor of 30. + codec.framerate = 8; + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(WaitAndSendFrame(0)); // Should be rendered. + EXPECT_TRUE(WaitAndSendFrame(30)); // Should be dropped. + EXPECT_TRUE(WaitAndSendFrame(30)); // Should be dropped. + EXPECT_TRUE(WaitAndSendFrame(30)); // Should be rendered. + frame_count += 2; + EXPECT_EQ_WAIT(frame_count, renderer_.num_rendered_frames(), kTimeout); + } + // Tests that we can set the send stream format properly. + void SetSendStreamFormat() { + cricket::VideoCodec codec(DefaultCodec()); + SendAndReceive(codec); + int frame_count = 1; + EXPECT_FRAME_WAIT(frame_count, codec.width, codec.height, kTimeout); + + // Adapt the resolution and frame rate to half. + cricket::VideoFormat format( + codec.width / 2, + codec.height / 2, + cricket::VideoFormat::FpsToInterval(codec.framerate / 2), + cricket::FOURCC_I420); + // The SSRC differs from the send SSRC. + EXPECT_FALSE(channel_->SetSendStreamFormat(kSsrc - 1, format)); + EXPECT_TRUE(channel_->SetSendStreamFormat(kSsrc, format)); + + EXPECT_TRUE(WaitAndSendFrame(30)); // Should be dropped. + EXPECT_TRUE(WaitAndSendFrame(30)); // Should be rendered. + EXPECT_TRUE(WaitAndSendFrame(30)); // Should be dropped. + frame_count += 1; + EXPECT_FRAME_WAIT(frame_count, format.width, format.height, kTimeout); + + // Adapt the resolution to 0x0, which should drop all frames. + format.width = 0; + format.height = 0; + EXPECT_TRUE(channel_->SetSendStreamFormat(kSsrc, format)); + EXPECT_TRUE(SendFrame()); + EXPECT_TRUE(SendFrame()); + talk_base::Thread::Current()->ProcessMessages(500); + EXPECT_EQ(frame_count, renderer_.num_rendered_frames()); + } + // Test that setting send stream format to 0x0 resolution will result in + // frames being dropped. + void SetSendStreamFormat0x0() { + EXPECT_TRUE(SetOneCodec(DefaultCodec())); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_EQ(0, renderer_.num_rendered_frames()); + // This frame should be received. + EXPECT_TRUE(SendFrame()); + EXPECT_FRAME_WAIT(1, DefaultCodec().width, DefaultCodec().height, kTimeout); + const int64 interval = cricket::VideoFormat::FpsToInterval( + DefaultCodec().framerate); + cricket::VideoFormat format( + 0, + 0, + interval, + cricket::FOURCC_I420); + EXPECT_TRUE(channel_->SetSendStreamFormat(kSsrc, format)); + // This frame should not be received. + EXPECT_TRUE(WaitAndSendFrame( + static_cast(interval/talk_base::kNumNanosecsPerMillisec))); + talk_base::Thread::Current()->ProcessMessages(500); + EXPECT_EQ(1, renderer_.num_rendered_frames()); + } + + // Tests that we can mute and unmute the channel properly. + void MuteStream() { + int frame_count = 0; + EXPECT_TRUE(SetDefaultCodec()); + cricket::FakeVideoCapturer video_capturer; + video_capturer.Start( + cricket::VideoFormat( + 640, 480, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420)); + EXPECT_TRUE(channel_->SetCapturer(kSsrc, &video_capturer)); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_EQ(frame_count, renderer_.num_rendered_frames()); + + // Mute the channel and expect black output frame. + EXPECT_TRUE(channel_->MuteStream(kSsrc, true)); + EXPECT_TRUE(video_capturer.CaptureFrame()); + ++frame_count; + EXPECT_EQ_WAIT(frame_count, renderer_.num_rendered_frames(), kTimeout); + EXPECT_TRUE(renderer_.black_frame()); + + // Unmute the channel and expect non-black output frame. + EXPECT_TRUE(channel_->MuteStream(kSsrc, false)); + EXPECT_TRUE(talk_base::Thread::Current()->ProcessMessages(30)); + EXPECT_TRUE(video_capturer.CaptureFrame()); + ++frame_count; + EXPECT_EQ_WAIT(frame_count, renderer_.num_rendered_frames(), kTimeout); + EXPECT_FALSE(renderer_.black_frame()); + + // Test that we can also Mute using the correct send stream SSRC. + EXPECT_TRUE(channel_->MuteStream(kSsrc, true)); + EXPECT_TRUE(talk_base::Thread::Current()->ProcessMessages(30)); + EXPECT_TRUE(video_capturer.CaptureFrame()); + ++frame_count; + EXPECT_EQ_WAIT(frame_count, renderer_.num_rendered_frames(), kTimeout); + EXPECT_TRUE(renderer_.black_frame()); + + EXPECT_TRUE(channel_->MuteStream(kSsrc, false)); + EXPECT_TRUE(talk_base::Thread::Current()->ProcessMessages(30)); + EXPECT_TRUE(video_capturer.CaptureFrame()); + ++frame_count; + EXPECT_EQ_WAIT(frame_count, renderer_.num_rendered_frames(), kTimeout); + EXPECT_FALSE(renderer_.black_frame()); + + // Test that muting an invalid stream fails. + EXPECT_FALSE(channel_->MuteStream(kSsrc+1, true)); + EXPECT_TRUE(channel_->SetCapturer(kSsrc, NULL)); + } + + // Test that multiple send streams can be created and deleted properly. + void MultipleSendStreams() { + // Remove stream added in Setup. I.e. remove stream corresponding to default + // channel. + EXPECT_TRUE(channel_->RemoveSendStream(kSsrc)); + const unsigned int kSsrcsSize = sizeof(kSsrcs4)/sizeof(kSsrcs4[0]); + for (unsigned int i = 0; i < kSsrcsSize; ++i) { + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(kSsrcs4[i]))); + } + // Delete one of the non default channel streams, let the destructor delete + // the remaining ones. + EXPECT_TRUE(channel_->RemoveSendStream(kSsrcs4[kSsrcsSize - 1])); + // Stream should already be deleted. + EXPECT_FALSE(channel_->RemoveSendStream(kSsrcs4[kSsrcsSize - 1])); + } + + + // Two streams one channel tests. + + // Tests that we can send and receive frames. + void TwoStreamsSendAndReceive(const cricket::VideoCodec& codec) { + SetUpSecondStream(); + // Test sending and receiving on first stream. + SendAndReceive(codec); + // Test sending and receiving on second stream. + EXPECT_EQ_WAIT(1, renderer2_.num_rendered_frames(), kTimeout); + EXPECT_EQ(2, NumRtpPackets()); + EXPECT_EQ(1, renderer2_.num_rendered_frames()); + } + + // Disconnect the first stream and re-use it with another SSRC + void TwoStreamsReUseFirstStream(const cricket::VideoCodec& codec) { + SetUpSecondStream(); + EXPECT_TRUE(channel_->RemoveRecvStream(kSsrc)); + EXPECT_FALSE(channel_->RemoveRecvStream(kSsrc)); + // SSRC 0 should map to the "default" stream. I.e. the first added stream. + EXPECT_TRUE(channel_->RemoveSendStream(0)); + // Make sure that the first added stream was indeed the "default" stream. + EXPECT_FALSE(channel_->RemoveSendStream(kSsrc)); + // Make sure that the "default" stream is indeed removed and that removing + // the default stream has an effect. + EXPECT_FALSE(channel_->RemoveSendStream(0)); + + SetRendererAsDefault(); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(kSsrc))); + EXPECT_FALSE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(kSsrc))); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc))); + EXPECT_FALSE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc))); + + SendAndReceive(codec); + EXPECT_TRUE(channel_->RemoveSendStream(0)); + } + + VideoEngineOverride engine_; + talk_base::scoped_ptr video_capturer_; + talk_base::scoped_ptr channel_; + cricket::FakeNetworkInterface network_interface_; + cricket::FakeVideoRenderer renderer_; + cricket::VideoMediaChannel::Error media_error_; + + // Used by test cases where 2 streams are run on the same channel. + cricket::FakeVideoRenderer renderer2_; +}; + +#endif // TALK_MEDIA_BASE_VIDEOENGINE_UNITTEST_H_ diff --git a/talk/media/base/videoframe.cc b/talk/media/base/videoframe.cc new file mode 100644 index 000000000..f5b0b6de5 --- /dev/null +++ b/talk/media/base/videoframe.cc @@ -0,0 +1,385 @@ +/* + * libjingle + * Copyright 2011 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/media/base/videoframe.h" + +#include + +#if !defined(DISABLE_YUV) +#include "libyuv/compare.h" +#include "libyuv/planar_functions.h" +#include "libyuv/scale.h" +#endif + +#include "talk/base/logging.h" +#include "talk/media/base/videocommon.h" + +namespace cricket { + +// Round to 2 pixels because Chroma channels are half size. +#define ROUNDTO2(v) (v & ~1) + +talk_base::StreamResult VideoFrame::Write(talk_base::StreamInterface* stream, + int* error) { + talk_base::StreamResult result = talk_base::SR_SUCCESS; + const uint8* src_y = GetYPlane(); + const uint8* src_u = GetUPlane(); + const uint8* src_v = GetVPlane(); + if (!src_y || !src_u || !src_v) { + return result; // Nothing to write. + } + const int32 y_pitch = GetYPitch(); + const int32 u_pitch = GetUPitch(); + const int32 v_pitch = GetVPitch(); + const size_t width = GetWidth(); + const size_t height = GetHeight(); + const size_t half_width = (width + 1) >> 1; + const size_t half_height = (height + 1) >> 1; + // Write Y. + for (size_t row = 0; row < height; ++row) { + result = stream->Write(src_y + row * y_pitch, width, NULL, error); + if (result != talk_base::SR_SUCCESS) { + return result; + } + } + // Write U. + for (size_t row = 0; row < half_height; ++row) { + result = stream->Write(src_u + row * u_pitch, half_width, NULL, error); + if (result != talk_base::SR_SUCCESS) { + return result; + } + } + // Write V. + for (size_t row = 0; row < half_height; ++row) { + result = stream->Write(src_v + row * v_pitch, half_width, NULL, error); + if (result != talk_base::SR_SUCCESS) { + return result; + } + } + return result; +} + +bool VideoFrame::CopyToPlanes( + uint8* dst_y, uint8* dst_u, uint8* dst_v, + int32 dst_pitch_y, int32 dst_pitch_u, int32 dst_pitch_v) const { +#if !defined(DISABLE_YUV) + int32 src_width = GetWidth(); + int32 src_height = GetHeight(); + return libyuv::I420Copy(GetYPlane(), GetYPitch(), + GetUPlane(), GetUPitch(), + GetVPlane(), GetVPitch(), + dst_y, dst_pitch_y, + dst_u, dst_pitch_u, + dst_v, dst_pitch_v, + src_width, src_height) == 0; +#else + int uv_size = GetUPitch() * GetChromaHeight(); + memcpy(dst_y, GetYPlane(), GetWidth() * GetHeight()); + memcpy(dst_u, GetUPlane(), uv_size); + memcpy(dst_v, GetVPlane(), uv_size); + return true; +#endif +} + +void VideoFrame::CopyToFrame(VideoFrame* dst) const { + if (!dst) { + LOG(LS_ERROR) << "NULL dst pointer."; + return; + } + + CopyToPlanes(dst->GetYPlane(), dst->GetUPlane(), dst->GetVPlane(), + dst->GetYPitch(), dst->GetUPitch(), dst->GetVPitch()); +} + +// TODO(fbarchard): Handle odd width/height with rounding. +void VideoFrame::StretchToPlanes( + uint8* dst_y, uint8* dst_u, uint8* dst_v, + int32 dst_pitch_y, int32 dst_pitch_u, int32 dst_pitch_v, + size_t width, size_t height, bool interpolate, bool vert_crop) const { + if (!GetYPlane() || !GetUPlane() || !GetVPlane()) { + LOG(LS_ERROR) << "NULL plane pointer."; + return; + } + + size_t src_width = GetWidth(); + size_t src_height = GetHeight(); + if (width == src_width && height == src_height) { + CopyToPlanes(dst_y, dst_u, dst_v, dst_pitch_y, dst_pitch_u, dst_pitch_v); + return; + } + const uint8* src_y = GetYPlane(); + const uint8* src_u = GetUPlane(); + const uint8* src_v = GetVPlane(); + + if (vert_crop) { + // Adjust the input width:height ratio to be the same as the output ratio. + if (src_width * height > src_height * width) { + // Reduce the input width, but keep size/position aligned for YuvScaler + src_width = ROUNDTO2(src_height * width / height); + int32 iwidth_offset = ROUNDTO2((GetWidth() - src_width) / 2); + src_y += iwidth_offset; + src_u += iwidth_offset / 2; + src_v += iwidth_offset / 2; + } else if (src_width * height < src_height * width) { + // Reduce the input height. + src_height = src_width * height / width; + int32 iheight_offset = (GetHeight() - src_height) >> 2; + iheight_offset <<= 1; // Ensure that iheight_offset is even. + src_y += iheight_offset * GetYPitch(); + src_u += iheight_offset / 2 * GetUPitch(); + src_v += iheight_offset / 2 * GetVPitch(); + } + } + + // TODO(fbarchard): Implement a simple scale for non-libyuv. +#if !defined(DISABLE_YUV) + // Scale to the output I420 frame. + libyuv::Scale(src_y, src_u, src_v, + GetYPitch(), GetUPitch(), GetVPitch(), + src_width, src_height, + dst_y, dst_u, dst_v, dst_pitch_y, dst_pitch_u, dst_pitch_v, + width, height, interpolate); +#endif +} + +size_t VideoFrame::StretchToBuffer(size_t dst_width, size_t dst_height, + uint8* dst_buffer, size_t size, + bool interpolate, bool vert_crop) const { + if (!dst_buffer) { + LOG(LS_ERROR) << "NULL dst_buffer pointer."; + return 0; + } + + size_t needed = SizeOf(dst_width, dst_height); + if (needed <= size) { + uint8* dst_y = dst_buffer; + uint8* dst_u = dst_y + dst_width * dst_height; + uint8* dst_v = dst_u + ((dst_width + 1) >> 1) * ((dst_height + 1) >> 1); + StretchToPlanes(dst_y, dst_u, dst_v, + dst_width, (dst_width + 1) >> 1, (dst_width + 1) >> 1, + dst_width, dst_height, interpolate, vert_crop); + } + return needed; +} + +void VideoFrame::StretchToFrame(VideoFrame* dst, + bool interpolate, bool vert_crop) const { + if (!dst) { + LOG(LS_ERROR) << "NULL dst pointer."; + return; + } + + StretchToPlanes(dst->GetYPlane(), dst->GetUPlane(), dst->GetVPlane(), + dst->GetYPitch(), dst->GetUPitch(), dst->GetVPitch(), + dst->GetWidth(), dst->GetHeight(), + interpolate, vert_crop); + dst->SetElapsedTime(GetElapsedTime()); + dst->SetTimeStamp(GetTimeStamp()); +} + +VideoFrame* VideoFrame::Stretch(size_t dst_width, size_t dst_height, + bool interpolate, bool vert_crop) const { + VideoFrame* dest = CreateEmptyFrame(dst_width, dst_height, + GetPixelWidth(), GetPixelHeight(), + GetElapsedTime(), GetTimeStamp()); + if (dest) { + StretchToFrame(dest, interpolate, vert_crop); + } + return dest; +} + +bool VideoFrame::SetToBlack() { +#if !defined(DISABLE_YUV) + return libyuv::I420Rect(GetYPlane(), GetYPitch(), + GetUPlane(), GetUPitch(), + GetVPlane(), GetVPitch(), + 0, 0, GetWidth(), GetHeight(), + 16, 128, 128) == 0; +#else + int uv_size = GetUPitch() * GetChromaHeight(); + memset(GetYPlane(), 16, GetWidth() * GetHeight()); + memset(GetUPlane(), 128, uv_size); + memset(GetVPlane(), 128, uv_size); + return true; +#endif +} + +static const size_t kMaxSampleSize = 1000000000u; +// Returns whether a sample is valid +bool VideoFrame::Validate(uint32 fourcc, int w, int h, + const uint8 *sample, size_t sample_size) { + if (h < 0) { + h = -h; + } + // 16384 is maximum resolution for VP8 codec. + if (w < 1 || w > 16384 || h < 1 || h > 16384) { + LOG(LS_ERROR) << "Invalid dimensions: " << w << "x" << h; + return false; + } + uint32 format = CanonicalFourCC(fourcc); + int expected_bpp = 8; + switch (format) { + case FOURCC_I400: + case FOURCC_RGGB: + case FOURCC_BGGR: + case FOURCC_GRBG: + case FOURCC_GBRG: + expected_bpp = 8; + break; + case FOURCC_I420: + case FOURCC_I411: + case FOURCC_YU12: + case FOURCC_YV12: + case FOURCC_M420: + case FOURCC_Q420: + case FOURCC_NV21: + case FOURCC_NV12: + expected_bpp = 12; + break; + case FOURCC_I422: + case FOURCC_YV16: + case FOURCC_YUY2: + case FOURCC_UYVY: + case FOURCC_RGBP: + case FOURCC_RGBO: + case FOURCC_R444: + expected_bpp = 16; + break; + case FOURCC_I444: + case FOURCC_YV24: + case FOURCC_24BG: + case FOURCC_RAW: + expected_bpp = 24; + break; + + case FOURCC_ABGR: + case FOURCC_BGRA: + case FOURCC_ARGB: + expected_bpp = 32; + break; + + case FOURCC_MJPG: + case FOURCC_H264: + expected_bpp = 0; + break; + default: + expected_bpp = 8; // Expect format is at least 8 bits per pixel. + break; + } + size_t expected_size = (w * expected_bpp + 7) / 8 * h; + // For compressed formats, expect 4 bits per 16 x 16 macro. I420 would be + // 6 bits, but grey can be 4 bits. + if (expected_bpp == 0) { + expected_size = ((w + 15) / 16) * ((h + 15) / 16) * 4 / 8; + } + if (sample == NULL) { + LOG(LS_ERROR) << "NULL sample pointer." + << " format: " << GetFourccName(format) + << " bpp: " << expected_bpp + << " size: " << w << "x" << h + << " expected: " << expected_size + << " " << sample_size; + return false; + } + if (sample_size < expected_size) { + LOG(LS_ERROR) << "Size field is too small." + << " format: " << GetFourccName(format) + << " bpp: " << expected_bpp + << " size: " << w << "x" << h + << " " << sample_size + << " expected: " << expected_size + << " sample[0..3]: " << static_cast(sample[0]) + << ", " << static_cast(sample[1]) + << ", " << static_cast(sample[2]) + << ", " << static_cast(sample[3]); + return false; + } + if (sample_size > kMaxSampleSize) { + LOG(LS_WARNING) << "Size field is invalid." + << " format: " << GetFourccName(format) + << " bpp: " << expected_bpp + << " size: " << w << "x" << h + << " " << sample_size + << " expected: " << 2 * expected_size + << " sample[0..3]: " << static_cast(sample[0]) + << ", " << static_cast(sample[1]) + << ", " << static_cast(sample[2]) + << ", " << static_cast(sample[3]); + return false; + } + // Show large size warning once every 100 frames. + static int large_warn100 = 0; + size_t large_expected_size = expected_size * 2; + if (expected_bpp >= 8 && + (sample_size > large_expected_size || sample_size > kMaxSampleSize) && + large_warn100 % 100 == 0) { + ++large_warn100; + LOG(LS_WARNING) << "Size field is too large." + << " format: " << GetFourccName(format) + << " bpp: " << expected_bpp + << " size: " << w << "x" << h + << " bytes: " << sample_size + << " expected: " << large_expected_size + << " sample[0..3]: " << static_cast(sample[0]) + << ", " << static_cast(sample[1]) + << ", " << static_cast(sample[2]) + << ", " << static_cast(sample[3]); + } + // Scan pages to ensure they are there and don't contain a single value and + // to generate an error. + if (!memcmp(sample + sample_size - 8, sample + sample_size - 4, 4) && + !memcmp(sample, sample + 4, sample_size - 4)) { + LOG(LS_WARNING) << "Duplicate value for all pixels." + << " format: " << GetFourccName(format) + << " bpp: " << expected_bpp + << " size: " << w << "x" << h + << " bytes: " << sample_size + << " expected: " << expected_size + << " sample[0..3]: " << static_cast(sample[0]) + << ", " << static_cast(sample[1]) + << ", " << static_cast(sample[2]) + << ", " << static_cast(sample[3]); + } + + static bool valid_once = true; + if (valid_once) { + valid_once = false; + LOG(LS_INFO) << "Validate frame passed." + << " format: " << GetFourccName(format) + << " bpp: " << expected_bpp + << " size: " << w << "x" << h + << " bytes: " << sample_size + << " expected: " << expected_size + << " sample[0..3]: " << static_cast(sample[0]) + << ", " << static_cast(sample[1]) + << ", " << static_cast(sample[2]) + << ", " << static_cast(sample[3]); + } + return true; +} + +} // namespace cricket diff --git a/talk/media/base/videoframe.h b/talk/media/base/videoframe.h new file mode 100644 index 000000000..2f641ffab --- /dev/null +++ b/talk/media/base/videoframe.h @@ -0,0 +1,188 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_VIDEOFRAME_H_ +#define TALK_MEDIA_BASE_VIDEOFRAME_H_ + +#include "talk/base/basictypes.h" +#include "talk/base/stream.h" + +namespace cricket { + +// Simple rotation constants. +enum { + ROTATION_0 = 0, + ROTATION_90 = 90, + ROTATION_180 = 180, + ROTATION_270 = 270 +}; + +// Represents a YUV420 (a.k.a. I420) video frame. +class VideoFrame { + public: + VideoFrame() {} + virtual ~VideoFrame() {} + + virtual bool InitToBlack(int w, int h, size_t pixel_width, + size_t pixel_height, int64 elapsed_time, + int64 time_stamp) = 0; + // Creates a frame from a raw sample with FourCC |format| and size |w| x |h|. + // |h| can be negative indicating a vertically flipped image. + // |dw| is destination width; can be less than |w| if cropping is desired. + // |dh| is destination height, like |dw|, but must be a positive number. + // Returns whether the function succeeded or failed. + virtual bool Reset(uint32 fourcc, int w, int h, int dw, int dh, uint8 *sample, + size_t sample_size, size_t pixel_width, + size_t pixel_height, int64 elapsed_time, int64 time_stamp, + int rotation) = 0; + + // Basic accessors. + virtual size_t GetWidth() const = 0; + virtual size_t GetHeight() const = 0; + size_t GetChromaWidth() const { return (GetWidth() + 1) / 2; } + size_t GetChromaHeight() const { return (GetHeight() + 1) / 2; } + size_t GetChromaSize() const { return GetUPitch() * GetChromaHeight(); } + virtual const uint8 *GetYPlane() const = 0; + virtual const uint8 *GetUPlane() const = 0; + virtual const uint8 *GetVPlane() const = 0; + virtual uint8 *GetYPlane() = 0; + virtual uint8 *GetUPlane() = 0; + virtual uint8 *GetVPlane() = 0; + virtual int32 GetYPitch() const = 0; + virtual int32 GetUPitch() const = 0; + virtual int32 GetVPitch() const = 0; + + // For retrieving the aspect ratio of each pixel. Usually this is 1x1, but + // the aspect_ratio_idc parameter of H.264 can specify non-square pixels. + virtual size_t GetPixelWidth() const = 0; + virtual size_t GetPixelHeight() const = 0; + + virtual int64 GetElapsedTime() const = 0; + virtual int64 GetTimeStamp() const = 0; + virtual void SetElapsedTime(int64 elapsed_time) = 0; + virtual void SetTimeStamp(int64 time_stamp) = 0; + + // Indicates the rotation angle in degrees. + virtual int GetRotation() const = 0; + + // Make a shallow copy of the frame. The frame buffer itself is not copied. + // Both the current and new VideoFrame will share a single reference-counted + // frame buffer. + virtual VideoFrame *Copy() const = 0; + + // Since VideoFrame supports shallow copy and the internal frame buffer might + // be shared, in case VideoFrame needs exclusive access of the frame buffer, + // user can call MakeExclusive() to make sure the frame buffer is exclusive + // accessable to the current object. This might mean a deep copy of the frame + // buffer if it is currently shared by other objects. + virtual bool MakeExclusive() = 0; + + // Writes the frame into the given frame buffer, provided that it is of + // sufficient size. Returns the frame's actual size, regardless of whether + // it was written or not (like snprintf). If there is insufficient space, + // nothing is written. + virtual size_t CopyToBuffer(uint8 *buffer, size_t size) const = 0; + + // Writes the frame into the given planes, stretched to the given width and + // height. The parameter "interpolate" controls whether to interpolate or just + // take the nearest-point. The parameter "crop" controls whether to crop this + // frame to the aspect ratio of the given dimensions before stretching. + virtual bool CopyToPlanes( + uint8* dst_y, uint8* dst_u, uint8* dst_v, + int32 dst_pitch_y, int32 dst_pitch_u, int32 dst_pitch_v) const; + + // Writes the frame into the target VideoFrame. + virtual void CopyToFrame(VideoFrame* target) const; + + // Writes the frame into the given stream and returns the StreamResult. + // See talk/base/stream.h for a description of StreamResult and error. + // Error may be NULL. If a non-success value is returned from + // StreamInterface::Write(), we immediately return with that value. + virtual talk_base::StreamResult Write(talk_base::StreamInterface *stream, + int *error); + + // Converts the I420 data to RGB of a certain type such as ARGB and ABGR. + // Returns the frame's actual size, regardless of whether it was written or + // not (like snprintf). Parameters size and stride_rgb are in units of bytes. + // If there is insufficient space, nothing is written. + virtual size_t ConvertToRgbBuffer(uint32 to_fourcc, uint8 *buffer, + size_t size, int stride_rgb) const = 0; + + // Writes the frame into the given planes, stretched to the given width and + // height. The parameter "interpolate" controls whether to interpolate or just + // take the nearest-point. The parameter "crop" controls whether to crop this + // frame to the aspect ratio of the given dimensions before stretching. + virtual void StretchToPlanes( + uint8 *y, uint8 *u, uint8 *v, int32 pitchY, int32 pitchU, int32 pitchV, + size_t width, size_t height, bool interpolate, bool crop) const; + + // Writes the frame into the given frame buffer, stretched to the given width + // and height, provided that it is of sufficient size. Returns the frame's + // actual size, regardless of whether it was written or not (like snprintf). + // If there is insufficient space, nothing is written. The parameter + // "interpolate" controls whether to interpolate or just take the + // nearest-point. The parameter "crop" controls whether to crop this frame to + // the aspect ratio of the given dimensions before stretching. + virtual size_t StretchToBuffer(size_t w, size_t h, uint8 *buffer, size_t size, + bool interpolate, bool crop) const; + + // Writes the frame into the target VideoFrame, stretched to the size of that + // frame. The parameter "interpolate" controls whether to interpolate or just + // take the nearest-point. The parameter "crop" controls whether to crop this + // frame to the aspect ratio of the target frame before stretching. + virtual void StretchToFrame(VideoFrame *target, bool interpolate, + bool crop) const; + + // Stretches the frame to the given size, creating a new VideoFrame object to + // hold it. The parameter "interpolate" controls whether to interpolate or + // just take the nearest-point. The parameter "crop" controls whether to crop + // this frame to the aspect ratio of the given dimensions before stretching. + virtual VideoFrame *Stretch(size_t w, size_t h, bool interpolate, + bool crop) const; + + // Sets the video frame to black. + bool SetToBlack(); + + // Tests if sample is valid. Returns true if valid. + static bool Validate(uint32 fourcc, int w, int h, const uint8 *sample, + size_t sample_size); + + // Size of an I420 image of given dimensions when stored as a frame buffer. + static size_t SizeOf(size_t w, size_t h) { + return w * h + ((w + 1) / 2) * ((h + 1) / 2) * 2; + } + + protected: + // Creates an empty frame. + virtual VideoFrame *CreateEmptyFrame(int w, int h, size_t pixel_width, + size_t pixel_height, int64 elapsed_time, + int64 time_stamp) const = 0; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_VIDEOFRAME_H_ diff --git a/talk/media/base/videoframe_unittest.h b/talk/media/base/videoframe_unittest.h new file mode 100644 index 000000000..f70e5675b --- /dev/null +++ b/talk/media/base/videoframe_unittest.h @@ -0,0 +1,2102 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_VIDEOFRAME_UNITTEST_H_ +#define TALK_MEDIA_BASE_VIDEOFRAME_UNITTEST_H_ + +#include + +#include "libyuv/convert.h" +#include "libyuv/convert_from.h" +#include "libyuv/format_conversion.h" +#include "libyuv/planar_functions.h" +#include "libyuv/rotate.h" +#include "talk/base/gunit.h" +#include "talk/base/pathutils.h" +#include "talk/base/stream.h" +#include "talk/base/stringutils.h" +#include "talk/media/base/testutils.h" +#include "talk/media/base/videocommon.h" +#include "talk/media/base/videoframe.h" + +#if defined(_MSC_VER) +#define ALIGN16(var) __declspec(align(16)) var +#else +#define ALIGN16(var) var __attribute__((aligned(16))) +#endif + +#define kImageFilename "faces.1280x720_P420.yuv" +#define kJpeg420Filename "faces_I420.jpg" +#define kJpeg422Filename "faces_I422.jpg" +#define kJpeg444Filename "faces_I444.jpg" +#define kJpeg411Filename "faces_I411.jpg" +#define kJpeg400Filename "faces_I400.jpg" + +// Generic test class for testing various video frame implementations. +template +class VideoFrameTest : public testing::Test { + public: + VideoFrameTest() : repeat_(1) {} + + protected: + static const int kWidth = 1280; + static const int kHeight = 720; + static const int kAlignment = 16; + static const int kMinWidthAll = 1; // Constants for ConstructYUY2AllSizes. + static const int kMinHeightAll = 1; + static const int kMaxWidthAll = 17; + static const int kMaxHeightAll = 23; + + // Load a video frame from disk. + bool LoadFrameNoRepeat(T* frame) { + int save_repeat = repeat_; // This LoadFrame disables repeat. + repeat_ = 1; + bool success = LoadFrame(kImageFilename, cricket::FOURCC_I420, + kWidth, kHeight, frame); + repeat_ = save_repeat; + return success; + } + + bool LoadFrame(const std::string& filename, uint32 format, + int32 width, int32 height, T* frame) { + return LoadFrame(filename, format, width, height, + width, abs(height), 0, frame); + } + bool LoadFrame(const std::string& filename, uint32 format, + int32 width, int32 height, int dw, int dh, int rotation, + T* frame) { + talk_base::scoped_ptr ms(LoadSample(filename)); + return LoadFrame(ms.get(), format, width, height, dw, dh, rotation, frame); + } + // Load a video frame from a memory stream. + bool LoadFrame(talk_base::MemoryStream* ms, uint32 format, + int32 width, int32 height, T* frame) { + return LoadFrame(ms, format, width, height, + width, abs(height), 0, frame); + } + bool LoadFrame(talk_base::MemoryStream* ms, uint32 format, + int32 width, int32 height, int dw, int dh, int rotation, + T* frame) { + if (!ms) { + return false; + } + size_t data_size; + bool ret = ms->GetSize(&data_size); + EXPECT_TRUE(ret); + if (ret) { + ret = LoadFrame(reinterpret_cast(ms->GetBuffer()), data_size, + format, width, height, dw, dh, rotation, frame); + } + return ret; + } + // Load a frame from a raw buffer. + bool LoadFrame(uint8* sample, size_t sample_size, uint32 format, + int32 width, int32 height, T* frame) { + return LoadFrame(sample, sample_size, format, width, height, + width, abs(height), 0, frame); + } + bool LoadFrame(uint8* sample, size_t sample_size, uint32 format, + int32 width, int32 height, int dw, int dh, int rotation, + T* frame) { + bool ret = false; + for (int i = 0; i < repeat_; ++i) { + ret = frame->Init(format, width, height, dw, dh, + sample, sample_size, 1, 1, 0, 0, rotation); + } + return ret; + } + + talk_base::MemoryStream* LoadSample(const std::string& filename) { + talk_base::Pathname path(cricket::GetTestFilePath(filename)); + talk_base::scoped_ptr fs( + talk_base::Filesystem::OpenFile(path, "rb")); + if (!fs.get()) { + return NULL; + } + + char buf[4096]; + talk_base::scoped_ptr ms( + new talk_base::MemoryStream()); + talk_base::StreamResult res = Flow(fs.get(), buf, sizeof(buf), ms.get()); + if (res != talk_base::SR_SUCCESS) { + return NULL; + } + + return ms.release(); + } + + // Write an I420 frame out to disk. + bool DumpFrame(const std::string& prefix, + const cricket::VideoFrame& frame) { + char filename[256]; + talk_base::sprintfn(filename, sizeof(filename), "%s.%dx%d_P420.yuv", + prefix.c_str(), frame.GetWidth(), frame.GetHeight()); + size_t out_size = cricket::VideoFrame::SizeOf(frame.GetWidth(), + frame.GetHeight()); + talk_base::scoped_array out(new uint8[out_size]); + frame.CopyToBuffer(out.get(), out_size); + return DumpSample(filename, out.get(), out_size); + } + + bool DumpSample(const std::string& filename, const void* buffer, int size) { + talk_base::Pathname path(filename); + talk_base::scoped_ptr fs( + talk_base::Filesystem::OpenFile(path, "wb")); + if (!fs.get()) { + return false; + } + + return (fs->Write(buffer, size, NULL, NULL) == talk_base::SR_SUCCESS); + } + + // Create a test image in the desired color space. + // The image is a checkerboard pattern with 63x63 squares, which allows + // I420 chroma artifacts to easily be seen on the square boundaries. + // The pattern is { { green, orange }, { blue, purple } } + // There is also a gradient within each square to ensure that the luma + // values are handled properly. + talk_base::MemoryStream* CreateYuv422Sample(uint32 fourcc, + uint32 width, uint32 height) { + int y1_pos, y2_pos, u_pos, v_pos; + if (!GetYuv422Packing(fourcc, &y1_pos, &y2_pos, &u_pos, &v_pos)) { + return NULL; + } + + talk_base::scoped_ptr ms( + new talk_base::MemoryStream); + int awidth = (width + 1) & ~1; + int size = awidth * 2 * height; + if (!ms->ReserveSize(size)) { + return NULL; + } + for (uint32 y = 0; y < height; ++y) { + for (int x = 0; x < awidth; x += 2) { + uint8 quad[4]; + quad[y1_pos] = (x % 63 + y % 63) + 64; + quad[y2_pos] = ((x + 1) % 63 + y % 63) + 64; + quad[u_pos] = ((x / 63) & 1) ? 192 : 64; + quad[v_pos] = ((y / 63) & 1) ? 192 : 64; + ms->Write(quad, sizeof(quad), NULL, NULL); + } + } + return ms.release(); + } + + // Create a test image for YUV 420 formats with 12 bits per pixel. + talk_base::MemoryStream* CreateYuvSample(uint32 width, uint32 height, + uint32 bpp) { + talk_base::scoped_ptr ms( + new talk_base::MemoryStream); + if (!ms->ReserveSize(width * height * bpp / 8)) { + return NULL; + } + + for (uint32 i = 0; i < width * height * bpp / 8; ++i) { + char value = ((i / 63) & 1) ? 192 : 64; + ms->Write(&value, sizeof(value), NULL, NULL); + } + return ms.release(); + } + + talk_base::MemoryStream* CreateRgbSample(uint32 fourcc, + uint32 width, uint32 height) { + int r_pos, g_pos, b_pos, bytes; + if (!GetRgbPacking(fourcc, &r_pos, &g_pos, &b_pos, &bytes)) { + return NULL; + } + + talk_base::scoped_ptr ms( + new talk_base::MemoryStream); + if (!ms->ReserveSize(width * height * bytes)) { + return NULL; + } + + for (uint32 y = 0; y < height; ++y) { + for (uint32 x = 0; x < width; ++x) { + uint8 rgb[4] = { 255, 255, 255, 255 }; + rgb[r_pos] = ((x / 63) & 1) ? 224 : 32; + rgb[g_pos] = (x % 63 + y % 63) + 96; + rgb[b_pos] = ((y / 63) & 1) ? 224 : 32; + ms->Write(rgb, bytes, NULL, NULL); + } + } + return ms.release(); + } + + // Simple conversion routines to verify the optimized VideoFrame routines. + // Converts from the specified colorspace to I420. + bool ConvertYuv422(const talk_base::MemoryStream* ms, + uint32 fourcc, uint32 width, uint32 height, + T* frame) { + int y1_pos, y2_pos, u_pos, v_pos; + if (!GetYuv422Packing(fourcc, &y1_pos, &y2_pos, &u_pos, &v_pos)) { + return false; + } + + const uint8* start = reinterpret_cast(ms->GetBuffer()); + int awidth = (width + 1) & ~1; + frame->InitToBlack(width, height, 1, 1, 0, 0); + int stride_y = frame->GetYPitch(); + int stride_u = frame->GetUPitch(); + int stride_v = frame->GetVPitch(); + for (uint32 y = 0; y < height; ++y) { + for (uint32 x = 0; x < width; x += 2) { + const uint8* quad1 = start + (y * awidth + x) * 2; + frame->GetYPlane()[stride_y * y + x] = quad1[y1_pos]; + if ((x + 1) < width) { + frame->GetYPlane()[stride_y * y + x + 1] = quad1[y2_pos]; + } + if ((y & 1) == 0) { + const uint8* quad2 = quad1 + awidth * 2; + if ((y + 1) >= height) { + quad2 = quad1; + } + frame->GetUPlane()[stride_u * (y / 2) + x / 2] = + (quad1[u_pos] + quad2[u_pos] + 1) / 2; + frame->GetVPlane()[stride_v * (y / 2) + x / 2] = + (quad1[v_pos] + quad2[v_pos] + 1) / 2; + } + } + } + return true; + } + + // Convert RGB to 420. + // A negative height inverts the image. + bool ConvertRgb(const talk_base::MemoryStream* ms, + uint32 fourcc, int32 width, int32 height, + T* frame) { + int r_pos, g_pos, b_pos, bytes; + if (!GetRgbPacking(fourcc, &r_pos, &g_pos, &b_pos, &bytes)) { + return false; + } + int pitch = width * bytes; + const uint8* start = reinterpret_cast(ms->GetBuffer()); + if (height < 0) { + height = -height; + start = start + pitch * (height - 1); + pitch = -pitch; + } + frame->InitToBlack(width, height, 1, 1, 0, 0); + int stride_y = frame->GetYPitch(); + int stride_u = frame->GetUPitch(); + int stride_v = frame->GetVPitch(); + for (int32 y = 0; y < height; y += 2) { + for (int32 x = 0; x < width; x += 2) { + const uint8* rgb[4]; + uint8 yuv[4][3]; + rgb[0] = start + y * pitch + x * bytes; + rgb[1] = rgb[0] + ((x + 1) < width ? bytes : 0); + rgb[2] = rgb[0] + ((y + 1) < height ? pitch : 0); + rgb[3] = rgb[2] + ((x + 1) < width ? bytes : 0); + for (size_t i = 0; i < 4; ++i) { + ConvertRgbPixel(rgb[i][r_pos], rgb[i][g_pos], rgb[i][b_pos], + &yuv[i][0], &yuv[i][1], &yuv[i][2]); + } + frame->GetYPlane()[stride_y * y + x] = yuv[0][0]; + if ((x + 1) < width) { + frame->GetYPlane()[stride_y * y + x + 1] = yuv[1][0]; + } + if ((y + 1) < height) { + frame->GetYPlane()[stride_y * (y + 1) + x] = yuv[2][0]; + if ((x + 1) < width) { + frame->GetYPlane()[stride_y * (y + 1) + x + 1] = yuv[3][0]; + } + } + frame->GetUPlane()[stride_u * (y / 2) + x / 2] = + (yuv[0][1] + yuv[1][1] + yuv[2][1] + yuv[3][1] + 2) / 4; + frame->GetVPlane()[stride_v * (y / 2) + x / 2] = + (yuv[0][2] + yuv[1][2] + yuv[2][2] + yuv[3][2] + 2) / 4; + } + } + return true; + } + + // Simple and slow RGB->YUV conversion. From NTSC standard, c/o Wikipedia. + void ConvertRgbPixel(uint8 r, uint8 g, uint8 b, + uint8* y, uint8* u, uint8* v) { + *y = static_cast(.257 * r + .504 * g + .098 * b) + 16; + *u = static_cast(-.148 * r - .291 * g + .439 * b) + 128; + *v = static_cast(.439 * r - .368 * g - .071 * b) + 128; + } + + bool GetYuv422Packing(uint32 fourcc, + int* y1_pos, int* y2_pos, int* u_pos, int* v_pos) { + if (fourcc == cricket::FOURCC_YUY2) { + *y1_pos = 0; *u_pos = 1; *y2_pos = 2; *v_pos = 3; + } else if (fourcc == cricket::FOURCC_UYVY) { + *u_pos = 0; *y1_pos = 1; *v_pos = 2; *y2_pos = 3; + } else { + return false; + } + return true; + } + + bool GetRgbPacking(uint32 fourcc, + int* r_pos, int* g_pos, int* b_pos, int* bytes) { + if (fourcc == cricket::FOURCC_RAW) { + *r_pos = 0; *g_pos = 1; *b_pos = 2; *bytes = 3; // RGB in memory. + } else if (fourcc == cricket::FOURCC_24BG) { + *r_pos = 2; *g_pos = 1; *b_pos = 0; *bytes = 3; // BGR in memory. + } else if (fourcc == cricket::FOURCC_ABGR) { + *r_pos = 0; *g_pos = 1; *b_pos = 2; *bytes = 4; // RGBA in memory. + } else if (fourcc == cricket::FOURCC_BGRA) { + *r_pos = 1; *g_pos = 2; *b_pos = 3; *bytes = 4; // ARGB in memory. + } else if (fourcc == cricket::FOURCC_ARGB) { + *r_pos = 2; *g_pos = 1; *b_pos = 0; *bytes = 4; // BGRA in memory. + } else { + return false; + } + return true; + } + + // Comparison functions for testing. + static bool IsNull(const cricket::VideoFrame& frame) { + return !frame.GetYPlane(); + } + + static bool IsSize(const cricket::VideoFrame& frame, + uint32 width, uint32 height) { + return !IsNull(frame) && + frame.GetYPitch() >= static_cast(width) && + frame.GetUPitch() >= static_cast(width) / 2 && + frame.GetVPitch() >= static_cast(width) / 2 && + frame.GetWidth() == width && frame.GetHeight() == height; + } + + static bool IsPlaneEqual(const std::string& name, + const uint8* plane1, uint32 pitch1, + const uint8* plane2, uint32 pitch2, + uint32 width, uint32 height, + int max_error) { + const uint8* r1 = plane1; + const uint8* r2 = plane2; + for (uint32 y = 0; y < height; ++y) { + for (uint32 x = 0; x < width; ++x) { + if (abs(static_cast(r1[x] - r2[x])) > max_error) { + LOG(LS_INFO) << "IsPlaneEqual(" << name << "): pixel[" + << x << "," << y << "] differs: " + << static_cast(r1[x]) << " vs " + << static_cast(r2[x]); + return false; + } + } + r1 += pitch1; + r2 += pitch2; + } + return true; + } + + static bool IsEqual(const cricket::VideoFrame& frame, + size_t width, size_t height, + size_t pixel_width, size_t pixel_height, + int64 elapsed_time, int64 time_stamp, + const uint8* y, uint32 ypitch, + const uint8* u, uint32 upitch, + const uint8* v, uint32 vpitch, + int max_error) { + return IsSize(frame, width, height) && + frame.GetPixelWidth() == pixel_width && + frame.GetPixelHeight() == pixel_height && + frame.GetElapsedTime() == elapsed_time && + frame.GetTimeStamp() == time_stamp && + IsPlaneEqual("y", frame.GetYPlane(), frame.GetYPitch(), y, ypitch, + width, height, max_error) && + IsPlaneEqual("u", frame.GetUPlane(), frame.GetUPitch(), u, upitch, + (width + 1) / 2, (height + 1) / 2, max_error) && + IsPlaneEqual("v", frame.GetVPlane(), frame.GetVPitch(), v, vpitch, + (width + 1) / 2, (height + 1) / 2, max_error); + } + + static bool IsEqual(const cricket::VideoFrame& frame1, + const cricket::VideoFrame& frame2, + int max_error) { + return IsEqual(frame1, + frame2.GetWidth(), frame2.GetHeight(), + frame2.GetPixelWidth(), frame2.GetPixelHeight(), + frame2.GetElapsedTime(), frame2.GetTimeStamp(), + frame2.GetYPlane(), frame2.GetYPitch(), + frame2.GetUPlane(), frame2.GetUPitch(), + frame2.GetVPlane(), frame2.GetVPitch(), + max_error); + } + + static bool IsEqualWithCrop(const cricket::VideoFrame& frame1, + const cricket::VideoFrame& frame2, + int hcrop, int vcrop, int max_error) { + return frame1.GetWidth() <= frame2.GetWidth() && + frame1.GetHeight() <= frame2.GetHeight() && + IsEqual(frame1, + frame2.GetWidth() - hcrop * 2, + frame2.GetHeight() - vcrop * 2, + frame2.GetPixelWidth(), frame2.GetPixelHeight(), + frame2.GetElapsedTime(), frame2.GetTimeStamp(), + frame2.GetYPlane() + vcrop * frame2.GetYPitch() + + hcrop, + frame2.GetYPitch(), + frame2.GetUPlane() + vcrop * frame2.GetUPitch() / 2 + + hcrop / 2, + frame2.GetUPitch(), + frame2.GetVPlane() + vcrop * frame2.GetVPitch() / 2 + + hcrop / 2, + frame2.GetVPitch(), + max_error); + } + + static bool IsBlack(const cricket::VideoFrame& frame) { + return !IsNull(frame) && + *frame.GetYPlane() == 16 && + *frame.GetUPlane() == 128 && + *frame.GetVPlane() == 128; + } + + //////////////////////// + // Construction tests // + //////////////////////// + + // Test constructing an image from a I420 buffer. + void ConstructI420() { + T frame; + EXPECT_TRUE(IsNull(frame)); + talk_base::scoped_ptr ms( + CreateYuvSample(kWidth, kHeight, 12)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_I420, + kWidth, kHeight, &frame)); + + const uint8* y = reinterpret_cast(ms.get()->GetBuffer()); + const uint8* u = y + kWidth * kHeight; + const uint8* v = u + kWidth * kHeight / 4; + EXPECT_TRUE(IsEqual(frame, kWidth, kHeight, 1, 1, 0, 0, + y, kWidth, u, kWidth / 2, v, kWidth / 2, 0)); + } + + // Test constructing an image from a YV12 buffer. + void ConstructYV12() { + T frame; + talk_base::scoped_ptr ms( + CreateYuvSample(kWidth, kHeight, 12)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_YV12, + kWidth, kHeight, &frame)); + + const uint8* y = reinterpret_cast(ms.get()->GetBuffer()); + const uint8* v = y + kWidth * kHeight; + const uint8* u = v + kWidth * kHeight / 4; + EXPECT_TRUE(IsEqual(frame, kWidth, kHeight, 1, 1, 0, 0, + y, kWidth, u, kWidth / 2, v, kWidth / 2, 0)); + } + + // Test constructing an image from a I422 buffer. + void ConstructI422() { + T frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + size_t buf_size = kWidth * kHeight * 2; + talk_base::scoped_array buf(new uint8[buf_size + kAlignment]); + uint8* y = ALIGNP(buf.get(), kAlignment); + uint8* u = y + kWidth * kHeight; + uint8* v = u + (kWidth / 2) * kHeight; + EXPECT_EQ(0, libyuv::I420ToI422(frame1.GetYPlane(), frame1.GetYPitch(), + frame1.GetUPlane(), frame1.GetUPitch(), + frame1.GetVPlane(), frame1.GetVPitch(), + y, kWidth, + u, kWidth / 2, + v, kWidth / 2, + kWidth, kHeight)); + EXPECT_TRUE(LoadFrame(y, buf_size, cricket::FOURCC_I422, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + } + + // Test constructing an image from a YUY2 buffer. + void ConstructYuy2() { + T frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + size_t buf_size = kWidth * kHeight * 2; + talk_base::scoped_array buf(new uint8[buf_size + kAlignment]); + uint8* yuy2 = ALIGNP(buf.get(), kAlignment); + EXPECT_EQ(0, libyuv::I420ToYUY2(frame1.GetYPlane(), frame1.GetYPitch(), + frame1.GetUPlane(), frame1.GetUPitch(), + frame1.GetVPlane(), frame1.GetVPitch(), + yuy2, kWidth * 2, + kWidth, kHeight)); + EXPECT_TRUE(LoadFrame(yuy2, buf_size, cricket::FOURCC_YUY2, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + } + + // Test constructing an image from a YUY2 buffer with buffer unaligned. + void ConstructYuy2Unaligned() { + T frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + size_t buf_size = kWidth * kHeight * 2; + talk_base::scoped_array buf(new uint8[buf_size + kAlignment + 1]); + uint8* yuy2 = ALIGNP(buf.get(), kAlignment) + 1; + EXPECT_EQ(0, libyuv::I420ToYUY2(frame1.GetYPlane(), frame1.GetYPitch(), + frame1.GetUPlane(), frame1.GetUPitch(), + frame1.GetVPlane(), frame1.GetVPitch(), + yuy2, kWidth * 2, + kWidth, kHeight)); + EXPECT_TRUE(LoadFrame(yuy2, buf_size, cricket::FOURCC_YUY2, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + } + + // Test constructing an image from a wide YUY2 buffer. + // Normal is 1280x720. Wide is 12800x72 + void ConstructYuy2Wide() { + T frame1, frame2; + talk_base::scoped_ptr ms( + CreateYuv422Sample(cricket::FOURCC_YUY2, kWidth * 10, kHeight / 10)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertYuv422(ms.get(), cricket::FOURCC_YUY2, + kWidth * 10, kHeight / 10, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_YUY2, + kWidth * 10, kHeight / 10, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + } + + // Test constructing an image from a UYVY buffer. + void ConstructUyvy() { + T frame1, frame2; + talk_base::scoped_ptr ms( + CreateYuv422Sample(cricket::FOURCC_UYVY, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertYuv422(ms.get(), cricket::FOURCC_UYVY, kWidth, kHeight, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_UYVY, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + } + + // Test constructing an image from a random buffer. + // We are merely verifying that the code succeeds and is free of crashes. + void ConstructM420() { + T frame; + talk_base::scoped_ptr ms( + CreateYuvSample(kWidth, kHeight, 12)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_M420, + kWidth, kHeight, &frame)); + } + + void ConstructQ420() { + T frame; + talk_base::scoped_ptr ms( + CreateYuvSample(kWidth, kHeight, 12)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_Q420, + kWidth, kHeight, &frame)); + } + + void ConstructNV21() { + T frame; + talk_base::scoped_ptr ms( + CreateYuvSample(kWidth, kHeight, 12)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_NV21, + kWidth, kHeight, &frame)); + } + + void ConstructNV12() { + T frame; + talk_base::scoped_ptr ms( + CreateYuvSample(kWidth, kHeight, 12)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_NV12, + kWidth, kHeight, &frame)); + } + + // Test constructing an image from a ABGR buffer + // Due to rounding, some pixels may differ slightly from the VideoFrame impl. + void ConstructABGR() { + T frame1, frame2; + talk_base::scoped_ptr ms( + CreateRgbSample(cricket::FOURCC_ABGR, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertRgb(ms.get(), cricket::FOURCC_ABGR, kWidth, kHeight, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_ABGR, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 2)); + } + + // Test constructing an image from a ARGB buffer + // Due to rounding, some pixels may differ slightly from the VideoFrame impl. + void ConstructARGB() { + T frame1, frame2; + talk_base::scoped_ptr ms( + CreateRgbSample(cricket::FOURCC_ARGB, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertRgb(ms.get(), cricket::FOURCC_ARGB, kWidth, kHeight, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_ARGB, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 2)); + } + + // Test constructing an image from a wide ARGB buffer + // Normal is 1280x720. Wide is 12800x72 + void ConstructARGBWide() { + T frame1, frame2; + talk_base::scoped_ptr ms( + CreateRgbSample(cricket::FOURCC_ARGB, kWidth * 10, kHeight / 10)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertRgb(ms.get(), cricket::FOURCC_ARGB, + kWidth * 10, kHeight / 10, &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_ARGB, + kWidth * 10, kHeight / 10, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 2)); + } + + // Test constructing an image from an BGRA buffer. + // Due to rounding, some pixels may differ slightly from the VideoFrame impl. + void ConstructBGRA() { + T frame1, frame2; + talk_base::scoped_ptr ms( + CreateRgbSample(cricket::FOURCC_BGRA, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertRgb(ms.get(), cricket::FOURCC_BGRA, kWidth, kHeight, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_BGRA, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 2)); + } + + // Test constructing an image from a 24BG buffer. + // Due to rounding, some pixels may differ slightly from the VideoFrame impl. + void Construct24BG() { + T frame1, frame2; + talk_base::scoped_ptr ms( + CreateRgbSample(cricket::FOURCC_24BG, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertRgb(ms.get(), cricket::FOURCC_24BG, kWidth, kHeight, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_24BG, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 2)); + } + + // Test constructing an image from a raw RGB buffer. + // Due to rounding, some pixels may differ slightly from the VideoFrame impl. + void ConstructRaw() { + T frame1, frame2; + talk_base::scoped_ptr ms( + CreateRgbSample(cricket::FOURCC_RAW, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertRgb(ms.get(), cricket::FOURCC_RAW, kWidth, kHeight, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_RAW, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 2)); + } + + // Test constructing an image from a RGB565 buffer + void ConstructRGB565() { + T frame1, frame2; + size_t out_size = kWidth * kHeight * 2; + talk_base::scoped_array outbuf(new uint8[out_size + kAlignment]); + uint8 *out = ALIGNP(outbuf.get(), kAlignment); + T frame; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + EXPECT_EQ(out_size, frame1.ConvertToRgbBuffer(cricket::FOURCC_RGBP, + out, + out_size, kWidth * 2)); + EXPECT_TRUE(LoadFrame(out, out_size, cricket::FOURCC_RGBP, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 20)); + } + + // Test constructing an image from a ARGB1555 buffer + void ConstructARGB1555() { + T frame1, frame2; + size_t out_size = kWidth * kHeight * 2; + talk_base::scoped_array outbuf(new uint8[out_size + kAlignment]); + uint8 *out = ALIGNP(outbuf.get(), kAlignment); + T frame; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + EXPECT_EQ(out_size, frame1.ConvertToRgbBuffer(cricket::FOURCC_RGBO, + out, + out_size, kWidth * 2)); + EXPECT_TRUE(LoadFrame(out, out_size, cricket::FOURCC_RGBO, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 20)); + } + + // Test constructing an image from a ARGB4444 buffer + void ConstructARGB4444() { + T frame1, frame2; + size_t out_size = kWidth * kHeight * 2; + talk_base::scoped_array outbuf(new uint8[out_size + kAlignment]); + uint8 *out = ALIGNP(outbuf.get(), kAlignment); + T frame; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + EXPECT_EQ(out_size, frame1.ConvertToRgbBuffer(cricket::FOURCC_R444, + out, + out_size, kWidth * 2)); + EXPECT_TRUE(LoadFrame(out, out_size, cricket::FOURCC_R444, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 20)); + } + + // Macro to help test different Bayer formats. + // Error threshold of 60 allows for Bayer format subsampling. + // TODO(fbarchard): Refactor this test to go from Bayer to I420 and + // back to bayer, which would be less lossy. + #define TEST_BYR(NAME, BAYER) \ + void NAME() { \ + size_t bayer_size = kWidth * kHeight; \ + talk_base::scoped_array bayerbuf(new uint8[ \ + bayer_size + kAlignment]); \ + uint8 *bayer = ALIGNP(bayerbuf.get(), kAlignment); \ + T frame1, frame2; \ + talk_base::scoped_ptr ms( \ + CreateRgbSample(cricket::FOURCC_ARGB, kWidth, kHeight)); \ + ASSERT_TRUE(ms.get() != NULL); \ + libyuv::ARGBToBayer##BAYER(reinterpret_cast(ms->GetBuffer()), \ + kWidth * 4, \ + bayer, kWidth, \ + kWidth, kHeight); \ + EXPECT_TRUE(LoadFrame(bayer, bayer_size, cricket::FOURCC_##BAYER, \ + kWidth, kHeight, &frame1)); \ + EXPECT_TRUE(ConvertRgb(ms.get(), cricket::FOURCC_ARGB, kWidth, kHeight, \ + &frame2)); \ + EXPECT_TRUE(IsEqual(frame1, frame2, 60)); \ + } + + // Test constructing an image from Bayer formats. + TEST_BYR(ConstructBayerGRBG, GRBG) + TEST_BYR(ConstructBayerGBRG, GBRG) + TEST_BYR(ConstructBayerBGGR, BGGR) + TEST_BYR(ConstructBayerRGGB, RGGB) + + +// Macro to help test different rotations +#define TEST_MIRROR(FOURCC, BPP) \ +void Construct##FOURCC##Mirror() { \ + T frame1, frame2, frame3; \ + talk_base::scoped_ptr ms( \ + CreateYuvSample(kWidth, kHeight, BPP)); \ + ASSERT_TRUE(ms.get() != NULL); \ + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_##FOURCC, \ + kWidth, -kHeight, kWidth, kHeight, \ + cricket::ROTATION_180, &frame1)); \ + size_t data_size; \ + bool ret = ms->GetSize(&data_size); \ + EXPECT_TRUE(ret); \ + EXPECT_TRUE(frame2.Init(cricket::FOURCC_##FOURCC, \ + kWidth, kHeight, kWidth, kHeight, \ + reinterpret_cast(ms->GetBuffer()), \ + data_size, \ + 1, 1, 0, 0, 0)); \ + int width_rotate = frame1.GetWidth(); \ + int height_rotate = frame1.GetHeight(); \ + EXPECT_TRUE(frame3.InitToBlack(width_rotate, height_rotate, 1, 1, 0, 0)); \ + libyuv::I420Mirror(frame2.GetYPlane(), frame2.GetYPitch(), \ + frame2.GetUPlane(), frame2.GetUPitch(), \ + frame2.GetVPlane(), frame2.GetVPitch(), \ + frame3.GetYPlane(), frame3.GetYPitch(), \ + frame3.GetUPlane(), frame3.GetUPitch(), \ + frame3.GetVPlane(), frame3.GetVPitch(), \ + kWidth, kHeight); \ + EXPECT_TRUE(IsEqual(frame1, frame3, 0)); \ + } + + TEST_MIRROR(I420, 420) + +// Macro to help test different rotations +#define TEST_ROTATE(FOURCC, BPP, ROTATE) \ +void Construct##FOURCC##Rotate##ROTATE() { \ + T frame1, frame2, frame3; \ + talk_base::scoped_ptr ms( \ + CreateYuvSample(kWidth, kHeight, BPP)); \ + ASSERT_TRUE(ms.get() != NULL); \ + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_##FOURCC, \ + kWidth, kHeight, kWidth, kHeight, \ + cricket::ROTATION_##ROTATE, &frame1)); \ + size_t data_size; \ + bool ret = ms->GetSize(&data_size); \ + EXPECT_TRUE(ret); \ + EXPECT_TRUE(frame2.Init(cricket::FOURCC_##FOURCC, \ + kWidth, kHeight, kWidth, kHeight, \ + reinterpret_cast(ms->GetBuffer()), \ + data_size, \ + 1, 1, 0, 0, 0)); \ + int width_rotate = frame1.GetWidth(); \ + int height_rotate = frame1.GetHeight(); \ + EXPECT_TRUE(frame3.InitToBlack(width_rotate, height_rotate, 1, 1, 0, 0)); \ + libyuv::I420Rotate(frame2.GetYPlane(), frame2.GetYPitch(), \ + frame2.GetUPlane(), frame2.GetUPitch(), \ + frame2.GetVPlane(), frame2.GetVPitch(), \ + frame3.GetYPlane(), frame3.GetYPitch(), \ + frame3.GetUPlane(), frame3.GetUPitch(), \ + frame3.GetVPlane(), frame3.GetVPitch(), \ + kWidth, kHeight, libyuv::kRotate##ROTATE); \ + EXPECT_TRUE(IsEqual(frame1, frame3, 0)); \ + } + + // Test constructing an image with rotation. + TEST_ROTATE(I420, 12, 0) + TEST_ROTATE(I420, 12, 90) + TEST_ROTATE(I420, 12, 180) + TEST_ROTATE(I420, 12, 270) + TEST_ROTATE(YV12, 12, 0) + TEST_ROTATE(YV12, 12, 90) + TEST_ROTATE(YV12, 12, 180) + TEST_ROTATE(YV12, 12, 270) + TEST_ROTATE(NV12, 12, 0) + TEST_ROTATE(NV12, 12, 90) + TEST_ROTATE(NV12, 12, 180) + TEST_ROTATE(NV12, 12, 270) + TEST_ROTATE(NV21, 12, 0) + TEST_ROTATE(NV21, 12, 90) + TEST_ROTATE(NV21, 12, 180) + TEST_ROTATE(NV21, 12, 270) + TEST_ROTATE(UYVY, 16, 0) + TEST_ROTATE(UYVY, 16, 90) + TEST_ROTATE(UYVY, 16, 180) + TEST_ROTATE(UYVY, 16, 270) + TEST_ROTATE(YUY2, 16, 0) + TEST_ROTATE(YUY2, 16, 90) + TEST_ROTATE(YUY2, 16, 180) + TEST_ROTATE(YUY2, 16, 270) + + // Test constructing an image from a UYVY buffer rotated 90 degrees. + void ConstructUyvyRotate90() { + T frame2; + talk_base::scoped_ptr ms( + CreateYuv422Sample(cricket::FOURCC_UYVY, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_UYVY, + kWidth, kHeight, kWidth, kHeight, + cricket::ROTATION_90, &frame2)); + } + + // Test constructing an image from a UYVY buffer rotated 180 degrees. + void ConstructUyvyRotate180() { + T frame2; + talk_base::scoped_ptr ms( + CreateYuv422Sample(cricket::FOURCC_UYVY, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_UYVY, + kWidth, kHeight, kWidth, kHeight, + cricket::ROTATION_180, &frame2)); + } + + // Test constructing an image from a UYVY buffer rotated 270 degrees. + void ConstructUyvyRotate270() { + T frame2; + talk_base::scoped_ptr ms( + CreateYuv422Sample(cricket::FOURCC_UYVY, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_UYVY, + kWidth, kHeight, kWidth, kHeight, + cricket::ROTATION_270, &frame2)); + } + + // Test constructing an image from a YUY2 buffer rotated 90 degrees. + void ConstructYuy2Rotate90() { + T frame2; + talk_base::scoped_ptr ms( + CreateYuv422Sample(cricket::FOURCC_YUY2, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_YUY2, + kWidth, kHeight, kWidth, kHeight, + cricket::ROTATION_90, &frame2)); + } + + // Test constructing an image from a YUY2 buffer rotated 180 degrees. + void ConstructYuy2Rotate180() { + T frame2; + talk_base::scoped_ptr ms( + CreateYuv422Sample(cricket::FOURCC_YUY2, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_YUY2, + kWidth, kHeight, kWidth, kHeight, + cricket::ROTATION_180, &frame2)); + } + + // Test constructing an image from a YUY2 buffer rotated 270 degrees. + void ConstructYuy2Rotate270() { + T frame2; + talk_base::scoped_ptr ms( + CreateYuv422Sample(cricket::FOURCC_YUY2, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_YUY2, + kWidth, kHeight, kWidth, kHeight, + cricket::ROTATION_270, &frame2)); + } + + // Test 1 pixel edge case image I420 buffer. + void ConstructI4201Pixel() { + T frame; + uint8 pixel[3] = { 1, 2, 3 }; + for (int i = 0; i < repeat_; ++i) { + EXPECT_TRUE(frame.Init(cricket::FOURCC_I420, 1, 1, 1, 1, + pixel, sizeof(pixel), + 1, 1, 0, 0, 0)); + } + const uint8* y = pixel; + const uint8* u = y + 1; + const uint8* v = u + 1; + EXPECT_TRUE(IsEqual(frame, 1, 1, 1, 1, 0, 0, + y, 1, u, 1, v, 1, 0)); + } + + // Test 5 pixel edge case image I420 buffer rounds down to 4. + void ConstructI4205Pixel() { + T frame; + uint8 pixels5x5[5 * 5 + ((5 + 1) / 2 * (5 + 1) / 2) * 2]; + memset(pixels5x5, 1, 5 * 5 + ((5 + 1) / 2 * (5 + 1) / 2) * 2); + for (int i = 0; i < repeat_; ++i) { + EXPECT_TRUE(frame.Init(cricket::FOURCC_I420, 5, 5, 5, 5, + pixels5x5, sizeof(pixels5x5), + 1, 1, 0, 0, 0)); + } + EXPECT_EQ(4u, frame.GetWidth()); + EXPECT_EQ(4u, frame.GetHeight()); + EXPECT_EQ(4, frame.GetYPitch()); + EXPECT_EQ(2, frame.GetUPitch()); + EXPECT_EQ(2, frame.GetVPitch()); + } + + // Test 1 pixel edge case image ARGB buffer. + void ConstructARGB1Pixel() { + T frame; + uint8 pixel[4] = { 64, 128, 192, 255 }; + for (int i = 0; i < repeat_; ++i) { + EXPECT_TRUE(frame.Init(cricket::FOURCC_ARGB, 1, 1, 1, 1, + pixel, sizeof(pixel), + 1, 1, 0, 0, 0)); + } + // Convert back to ARGB. + size_t out_size = 4; + talk_base::scoped_array outbuf(new uint8[out_size + kAlignment]); + uint8 *out = ALIGNP(outbuf.get(), kAlignment); + + EXPECT_EQ(out_size, frame.ConvertToRgbBuffer(cricket::FOURCC_ARGB, + out, + out_size, // buffer size + out_size)); // stride + #ifdef USE_LMI_CONVERT + // TODO(fbarchard): Expected to fail, but not crash. + EXPECT_FALSE(IsPlaneEqual("argb", pixel, 4, out, 4, 3, 1, 2)); + #else + // TODO(fbarchard): Check for overwrite. + EXPECT_TRUE(IsPlaneEqual("argb", pixel, 4, out, 4, 3, 1, 2)); + #endif + } + + // Test Black, White and Grey pixels. + void ConstructARGBBlackWhitePixel() { + T frame; + uint8 pixel[10 * 4] = { 0, 0, 0, 255, // Black. + 0, 0, 0, 255, + 64, 64, 64, 255, // Dark Grey. + 64, 64, 64, 255, + 128, 128, 128, 255, // Grey. + 128, 128, 128, 255, + 196, 196, 196, 255, // Light Grey. + 196, 196, 196, 255, + 255, 255, 255, 255, // White. + 255, 255, 255, 255 }; + + for (int i = 0; i < repeat_; ++i) { + EXPECT_TRUE(frame.Init(cricket::FOURCC_ARGB, 10, 1, 10, 1, + pixel, sizeof(pixel), + 1, 1, 0, 0, 0)); + } + // Convert back to ARGB + size_t out_size = 10 * 4; + talk_base::scoped_array outbuf(new uint8[out_size + kAlignment]); + uint8 *out = ALIGNP(outbuf.get(), kAlignment); + + EXPECT_EQ(out_size, frame.ConvertToRgbBuffer(cricket::FOURCC_ARGB, + out, + out_size, // buffer size. + out_size)); // stride. + EXPECT_TRUE(IsPlaneEqual("argb", pixel, out_size, + out, out_size, + out_size, 1, 2)); + } + + // Test constructing an image from an I420 buffer with horizontal cropping. + void ConstructI420CropHorizontal() { + T frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + ASSERT_TRUE(LoadFrame(kImageFilename, cricket::FOURCC_I420, kWidth, kHeight, + kWidth * 3 / 4, kHeight, 0, &frame2)); + EXPECT_TRUE(IsEqualWithCrop(frame2, frame1, kWidth / 8, 0, 0)); + } + + // Test constructing an image from a YUY2 buffer with horizontal cropping. + void ConstructYuy2CropHorizontal() { + T frame1, frame2; + talk_base::scoped_ptr ms( + CreateYuv422Sample(cricket::FOURCC_YUY2, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertYuv422(ms.get(), cricket::FOURCC_YUY2, kWidth, kHeight, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_YUY2, kWidth, kHeight, + kWidth * 3 / 4, kHeight, 0, &frame2)); + EXPECT_TRUE(IsEqualWithCrop(frame2, frame1, kWidth / 8, 0, 0)); + } + + // Test constructing an image from an ARGB buffer with horizontal cropping. + void ConstructARGBCropHorizontal() { + T frame1, frame2; + talk_base::scoped_ptr ms( + CreateRgbSample(cricket::FOURCC_ARGB, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertRgb(ms.get(), cricket::FOURCC_ARGB, kWidth, kHeight, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_ARGB, kWidth, kHeight, + kWidth * 3 / 4, kHeight, 0, &frame2)); + EXPECT_TRUE(IsEqualWithCrop(frame2, frame1, kWidth / 8, 0, 2)); + } + + // Test constructing an image from an I420 buffer, cropping top and bottom. + void ConstructI420CropVertical() { + T frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + ASSERT_TRUE(LoadFrame(kImageFilename, cricket::FOURCC_I420, kWidth, kHeight, + kWidth, kHeight * 3 / 4, 0, &frame2)); + EXPECT_TRUE(IsEqualWithCrop(frame2, frame1, 0, kHeight / 8, 0)); + } + + // Test constructing an image from I420 synonymous formats. + void ConstructI420Aliases() { + T frame1, frame2, frame3; + ASSERT_TRUE(LoadFrame(kImageFilename, cricket::FOURCC_I420, kWidth, kHeight, + &frame1)); + ASSERT_TRUE(LoadFrame(kImageFilename, cricket::FOURCC_IYUV, kWidth, kHeight, + &frame2)); + ASSERT_TRUE(LoadFrame(kImageFilename, cricket::FOURCC_YU12, kWidth, kHeight, + &frame3)); + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + EXPECT_TRUE(IsEqual(frame1, frame3, 0)); + } + + // Test constructing an image from an I420 MJPG buffer. + void ConstructMjpgI420() { + T frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + ASSERT_TRUE(LoadFrame(kJpeg420Filename, + cricket::FOURCC_MJPG, kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 32)); + } + + // Test constructing an image from an I422 MJPG buffer. + void ConstructMjpgI422() { + T frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + ASSERT_TRUE(LoadFrame(kJpeg422Filename, + cricket::FOURCC_MJPG, kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 32)); + } + + // Test constructing an image from an I444 MJPG buffer. + void ConstructMjpgI444() { + T frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + ASSERT_TRUE(LoadFrame(kJpeg444Filename, + cricket::FOURCC_MJPG, kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 32)); + } + + // Test constructing an image from an I444 MJPG buffer. + void ConstructMjpgI411() { + T frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + ASSERT_TRUE(LoadFrame(kJpeg411Filename, + cricket::FOURCC_MJPG, kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 32)); + } + + // Test constructing an image from an I400 MJPG buffer. + // TODO(fbarchard): Stronger compare on chroma. Compare agaisnt a grey image. + void ConstructMjpgI400() { + T frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + ASSERT_TRUE(LoadFrame(kJpeg400Filename, + cricket::FOURCC_MJPG, kWidth, kHeight, &frame2)); + EXPECT_TRUE(IsPlaneEqual("y", frame1.GetYPlane(), frame1.GetYPitch(), + frame2.GetYPlane(), frame2.GetYPitch(), + kWidth, kHeight, 32)); + EXPECT_TRUE(IsEqual(frame1, frame2, 128)); + } + + // Test constructing an image from an I420 MJPG buffer. + void ValidateFrame(const char* name, uint32 fourcc, int data_adjust, + int size_adjust, bool expected_result) { + T frame; + talk_base::scoped_ptr ms(LoadSample(name)); + const uint8* sample = reinterpret_cast(ms.get()->GetBuffer()); + size_t sample_size; + ms->GetSize(&sample_size); + // Optional adjust size to test invalid size. + size_t data_size = sample_size + data_adjust; + + // Allocate a buffer with end page aligned. + const int kPadToHeapSized = 16 * 1024 * 1024; + talk_base::scoped_array page_buffer( + new uint8[((data_size + kPadToHeapSized + 4095) & ~4095)]); + uint8* data_ptr = page_buffer.get(); + if (!data_ptr) { + LOG(LS_WARNING) << "Failed to allocate memory for ValidateFrame test."; + EXPECT_FALSE(expected_result); // NULL is okay if failure was expected. + return; + } + data_ptr += kPadToHeapSized + (-(static_cast(data_size)) & 4095); + memcpy(data_ptr, sample, talk_base::_min(data_size, sample_size)); + for (int i = 0; i < repeat_; ++i) { + EXPECT_EQ(expected_result, frame.Validate(fourcc, kWidth, kHeight, + data_ptr, + sample_size + size_adjust)); + } + } + + // Test validate for I420 MJPG buffer. + void ValidateMjpgI420() { + ValidateFrame(kJpeg420Filename, cricket::FOURCC_MJPG, 0, 0, true); + } + + // Test validate for I422 MJPG buffer. + void ValidateMjpgI422() { + ValidateFrame(kJpeg422Filename, cricket::FOURCC_MJPG, 0, 0, true); + } + + // Test validate for I444 MJPG buffer. + void ValidateMjpgI444() { + ValidateFrame(kJpeg444Filename, cricket::FOURCC_MJPG, 0, 0, true); + } + + // Test validate for I411 MJPG buffer. + void ValidateMjpgI411() { + ValidateFrame(kJpeg411Filename, cricket::FOURCC_MJPG, 0, 0, true); + } + + // Test validate for I400 MJPG buffer. + void ValidateMjpgI400() { + ValidateFrame(kJpeg400Filename, cricket::FOURCC_MJPG, 0, 0, true); + } + + // Test validate for I420 buffer. + void ValidateI420() { + ValidateFrame(kImageFilename, cricket::FOURCC_I420, 0, 0, true); + } + + // Test validate for I420 buffer where size is too small + void ValidateI420SmallSize() { + ValidateFrame(kImageFilename, cricket::FOURCC_I420, 0, -16384, false); + } + + // Test validate for I420 buffer where size is too large (16 MB) + // Will produce warning but pass. + void ValidateI420LargeSize() { + ValidateFrame(kImageFilename, cricket::FOURCC_I420, 16000000, 16000000, + true); + } + + // Test validate for I420 buffer where size is 1 GB (not reasonable). + void ValidateI420HugeSize() { +#ifndef WIN32 // TODO(fbarchard): Reenable when fixing bug 9603762. + ValidateFrame(kImageFilename, cricket::FOURCC_I420, 1000000000u, + 1000000000u, false); +#endif + } + + // The following test that Validate crashes if the size is greater than the + // actual buffer size. + // TODO(fbarchard): Consider moving a filter into the capturer/plugin. +#if defined(_MSC_VER) && defined(_DEBUG) + int ExceptionFilter(unsigned int code, struct _EXCEPTION_POINTERS *ep) { + if (code == EXCEPTION_ACCESS_VIOLATION) { + LOG(LS_INFO) << "Caught EXCEPTION_ACCESS_VIOLATION as expected."; + return EXCEPTION_EXECUTE_HANDLER; + } else { + LOG(LS_INFO) << "Did not catch EXCEPTION_ACCESS_VIOLATION. Unexpected."; + return EXCEPTION_CONTINUE_SEARCH; + } + } + + // Test validate fails for truncated MJPG data buffer. If ValidateFrame + // crashes the exception handler will return and unittest passes with OK. + void ValidateMjpgI420InvalidSize() { + __try { + ValidateFrame(kJpeg420Filename, cricket::FOURCC_MJPG, -16384, 0, false); + FAIL() << "Validate was expected to cause EXCEPTION_ACCESS_VIOLATION."; + } __except(ExceptionFilter(GetExceptionCode(), GetExceptionInformation())) { + return; // Successfully crashed in ValidateFrame. + } + } + + // Test validate fails for truncated I420 buffer. + void ValidateI420InvalidSize() { + __try { + ValidateFrame(kImageFilename, cricket::FOURCC_I420, -16384, 0, false); + FAIL() << "Validate was expected to cause EXCEPTION_ACCESS_VIOLATION."; + } __except(ExceptionFilter(GetExceptionCode(), GetExceptionInformation())) { + return; // Successfully crashed in ValidateFrame. + } + } +#endif + + // Test constructing an image from a YUY2 buffer (and synonymous formats). + void ConstructYuy2Aliases() { + T frame1, frame2, frame3, frame4; + talk_base::scoped_ptr ms( + CreateYuv422Sample(cricket::FOURCC_YUY2, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertYuv422(ms.get(), cricket::FOURCC_YUY2, kWidth, kHeight, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_YUY2, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_YUVS, + kWidth, kHeight, &frame3)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_YUYV, + kWidth, kHeight, &frame4)); + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + EXPECT_TRUE(IsEqual(frame1, frame3, 0)); + EXPECT_TRUE(IsEqual(frame1, frame4, 0)); + } + + // Test constructing an image from a UYVY buffer (and synonymous formats). + void ConstructUyvyAliases() { + T frame1, frame2, frame3, frame4; + talk_base::scoped_ptr ms( + CreateYuv422Sample(cricket::FOURCC_UYVY, kWidth, kHeight)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertYuv422(ms.get(), cricket::FOURCC_UYVY, kWidth, kHeight, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_UYVY, + kWidth, kHeight, &frame2)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_2VUY, + kWidth, kHeight, &frame3)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_HDYC, + kWidth, kHeight, &frame4)); + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + EXPECT_TRUE(IsEqual(frame1, frame3, 0)); + EXPECT_TRUE(IsEqual(frame1, frame4, 0)); + } + + // Test creating a copy. + void ConstructCopy() { + T frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + for (int i = 0; i < repeat_; ++i) { + EXPECT_TRUE(frame2.Init(frame1)); + } + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + } + + // Test creating a copy and check that it just increments the refcount. + void ConstructCopyIsRef() { + T frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + for (int i = 0; i < repeat_; ++i) { + EXPECT_TRUE(frame2.Init(frame1)); + } + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + EXPECT_EQ(frame1.GetYPlane(), frame2.GetYPlane()); + EXPECT_EQ(frame1.GetUPlane(), frame2.GetUPlane()); + EXPECT_EQ(frame1.GetVPlane(), frame2.GetVPlane()); + } + + // Test creating an empty image and initing it to black. + void ConstructBlack() { + T frame; + for (int i = 0; i < repeat_; ++i) { + EXPECT_TRUE(frame.InitToBlack(kWidth, kHeight, 1, 1, 0, 0)); + } + EXPECT_TRUE(IsSize(frame, kWidth, kHeight)); + EXPECT_TRUE(IsBlack(frame)); + } + + // Test constructing an image from a YUY2 buffer with a range of sizes. + // Only tests that conversion does not crash or corrupt heap. + void ConstructYuy2AllSizes() { + T frame1, frame2; + for (int height = kMinHeightAll; height <= kMaxHeightAll; ++height) { + for (int width = kMinWidthAll; width <= kMaxWidthAll; ++width) { + talk_base::scoped_ptr ms( + CreateYuv422Sample(cricket::FOURCC_YUY2, width, height)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertYuv422(ms.get(), cricket::FOURCC_YUY2, width, height, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_YUY2, + width, height, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + } + } + } + + // Test constructing an image from a ARGB buffer with a range of sizes. + // Only tests that conversion does not crash or corrupt heap. + void ConstructARGBAllSizes() { + T frame1, frame2; + for (int height = kMinHeightAll; height <= kMaxHeightAll; ++height) { + for (int width = kMinWidthAll; width <= kMaxWidthAll; ++width) { + talk_base::scoped_ptr ms( + CreateRgbSample(cricket::FOURCC_ARGB, width, height)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertRgb(ms.get(), cricket::FOURCC_ARGB, width, height, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_ARGB, + width, height, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 64)); + } + } + // Test a practical window size for screencasting usecase. + const int kOddWidth = 1228; + const int kOddHeight = 260; + for (int j = 0; j < 2; ++j) { + for (int i = 0; i < 2; ++i) { + talk_base::scoped_ptr ms( + CreateRgbSample(cricket::FOURCC_ARGB, kOddWidth + i, kOddHeight + j)); + ASSERT_TRUE(ms.get() != NULL); + EXPECT_TRUE(ConvertRgb(ms.get(), cricket::FOURCC_ARGB, + kOddWidth + i, kOddHeight + j, + &frame1)); + EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_ARGB, + kOddWidth + i, kOddHeight + j, &frame2)); + EXPECT_TRUE(IsEqual(frame1, frame2, 64)); + } + } + } + + // Tests re-initing an existing image. + void Reset() { + T frame1, frame2; + talk_base::scoped_ptr ms( + LoadSample(kImageFilename)); + size_t data_size; + ms->GetSize(&data_size); + EXPECT_TRUE(frame1.InitToBlack(kWidth, kHeight, 1, 1, 0, 0)); + EXPECT_TRUE(frame2.InitToBlack(kWidth, kHeight, 1, 1, 0, 0)); + EXPECT_TRUE(IsBlack(frame1)); + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + EXPECT_TRUE(frame1.Reset(cricket::FOURCC_I420, + kWidth, kHeight, kWidth, kHeight, + reinterpret_cast(ms->GetBuffer()), + data_size, 1, 1, 0, 0, 0)); + EXPECT_FALSE(IsBlack(frame1)); + EXPECT_FALSE(IsEqual(frame1, frame2, 0)); + } + + ////////////////////// + // Conversion tests // + ////////////////////// + + enum ToFrom { TO, FROM }; + + // Helper function for test converting from I420 to packed formats. + inline void ConvertToBuffer(int bpp, int rowpad, bool invert, ToFrom to_from, + int error, uint32 fourcc, + int (*RGBToI420)(const uint8* src_frame, int src_stride_frame, + uint8* dst_y, int dst_stride_y, + uint8* dst_u, int dst_stride_u, + uint8* dst_v, int dst_stride_v, + int width, int height)) { + T frame1, frame2; + int repeat_to = (to_from == TO) ? repeat_ : 1; + int repeat_from = (to_from == FROM) ? repeat_ : 1; + + int astride = kWidth * bpp + rowpad; + size_t out_size = astride * kHeight; + talk_base::scoped_array outbuf(new uint8[out_size + kAlignment + 1]); + memset(outbuf.get(), 0, out_size + kAlignment + 1); + uint8 *outtop = ALIGNP(outbuf.get(), kAlignment); + uint8 *out = outtop; + int stride = astride; + if (invert) { + out += (kHeight - 1) * stride; // Point to last row. + stride = -stride; + } + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + + for (int i = 0; i < repeat_to; ++i) { + EXPECT_EQ(out_size, frame1.ConvertToRgbBuffer(fourcc, + out, + out_size, stride)); + } + EXPECT_TRUE(frame2.InitToBlack(kWidth, kHeight, 1, 1, 0, 0)); + for (int i = 0; i < repeat_from; ++i) { + EXPECT_EQ(0, RGBToI420(out, stride, + frame2.GetYPlane(), frame2.GetYPitch(), + frame2.GetUPlane(), frame2.GetUPitch(), + frame2.GetVPlane(), frame2.GetVPitch(), + kWidth, kHeight)); + } + if (rowpad) { + EXPECT_EQ(0, outtop[kWidth * bpp]); // Ensure stride skipped end of row. + EXPECT_NE(0, outtop[astride]); // Ensure pixel at start of 2nd row. + } else { + EXPECT_NE(0, outtop[kWidth * bpp]); // Expect something to be here. + } + EXPECT_EQ(0, outtop[out_size]); // Ensure no overrun. + EXPECT_TRUE(IsEqual(frame1, frame2, error)); + } + + static const int kError = 20; + static const int kErrorHigh = 40; + static const int kOddStride = 23; + + // Tests ConvertToRGBBuffer formats. + void ConvertToARGBBuffer() { + ConvertToBuffer(4, 0, false, TO, kError, + cricket::FOURCC_ARGB, libyuv::ARGBToI420); + } + void ConvertToBGRABuffer() { + ConvertToBuffer(4, 0, false, TO, kError, + cricket::FOURCC_BGRA, libyuv::BGRAToI420); + } + void ConvertToABGRBuffer() { + ConvertToBuffer(4, 0, false, TO, kError, + cricket::FOURCC_ABGR, libyuv::ABGRToI420); + } + void ConvertToRGB24Buffer() { + ConvertToBuffer(3, 0, false, TO, kError, + cricket::FOURCC_24BG, libyuv::RGB24ToI420); + } + void ConvertToRAWBuffer() { + ConvertToBuffer(3, 0, false, TO, kError, + cricket::FOURCC_RAW, libyuv::RAWToI420); + } + void ConvertToRGB565Buffer() { + ConvertToBuffer(2, 0, false, TO, kError, + cricket::FOURCC_RGBP, libyuv::RGB565ToI420); + } + void ConvertToARGB1555Buffer() { + ConvertToBuffer(2, 0, false, TO, kError, + cricket::FOURCC_RGBO, libyuv::ARGB1555ToI420); + } + void ConvertToARGB4444Buffer() { + ConvertToBuffer(2, 0, false, TO, kError, + cricket::FOURCC_R444, libyuv::ARGB4444ToI420); + } + void ConvertToBayerBGGRBuffer() { + ConvertToBuffer(1, 0, false, TO, kErrorHigh, + cricket::FOURCC_BGGR, libyuv::BayerBGGRToI420); + } + void ConvertToBayerGBRGBuffer() { + ConvertToBuffer(1, 0, false, TO, kErrorHigh, + cricket::FOURCC_GBRG, libyuv::BayerGBRGToI420); + } + void ConvertToBayerGRBGBuffer() { + ConvertToBuffer(1, 0, false, TO, kErrorHigh, + cricket::FOURCC_GRBG, libyuv::BayerGRBGToI420); + } + void ConvertToBayerRGGBBuffer() { + ConvertToBuffer(1, 0, false, TO, kErrorHigh, + cricket::FOURCC_RGGB, libyuv::BayerRGGBToI420); + } + void ConvertToI400Buffer() { + ConvertToBuffer(1, 0, false, TO, 128, + cricket::FOURCC_I400, libyuv::I400ToI420); + } + void ConvertToYUY2Buffer() { + ConvertToBuffer(2, 0, false, TO, kError, + cricket::FOURCC_YUY2, libyuv::YUY2ToI420); + } + void ConvertToUYVYBuffer() { + ConvertToBuffer(2, 0, false, TO, kError, + cricket::FOURCC_UYVY, libyuv::UYVYToI420); + } + + // Tests ConvertToRGBBuffer formats with odd stride. + void ConvertToARGBBufferStride() { + ConvertToBuffer(4, kOddStride, false, TO, kError, + cricket::FOURCC_ARGB, libyuv::ARGBToI420); + } + void ConvertToBGRABufferStride() { + ConvertToBuffer(4, kOddStride, false, TO, kError, + cricket::FOURCC_BGRA, libyuv::BGRAToI420); + } + void ConvertToABGRBufferStride() { + ConvertToBuffer(4, kOddStride, false, TO, kError, + cricket::FOURCC_ABGR, libyuv::ABGRToI420); + } + void ConvertToRGB24BufferStride() { + ConvertToBuffer(3, kOddStride, false, TO, kError, + cricket::FOURCC_24BG, libyuv::RGB24ToI420); + } + void ConvertToRAWBufferStride() { + ConvertToBuffer(3, kOddStride, false, TO, kError, + cricket::FOURCC_RAW, libyuv::RAWToI420); + } + void ConvertToRGB565BufferStride() { + ConvertToBuffer(2, kOddStride, false, TO, kError, + cricket::FOURCC_RGBP, libyuv::RGB565ToI420); + } + void ConvertToARGB1555BufferStride() { + ConvertToBuffer(2, kOddStride, false, TO, kError, + cricket::FOURCC_RGBO, libyuv::ARGB1555ToI420); + } + void ConvertToARGB4444BufferStride() { + ConvertToBuffer(2, kOddStride, false, TO, kError, + cricket::FOURCC_R444, libyuv::ARGB4444ToI420); + } + void ConvertToBayerBGGRBufferStride() { + ConvertToBuffer(1, kOddStride, false, TO, kErrorHigh, + cricket::FOURCC_BGGR, libyuv::BayerBGGRToI420); + } + void ConvertToBayerGBRGBufferStride() { + ConvertToBuffer(1, kOddStride, false, TO, kErrorHigh, + cricket::FOURCC_GBRG, libyuv::BayerGBRGToI420); + } + void ConvertToBayerGRBGBufferStride() { + ConvertToBuffer(1, kOddStride, false, TO, kErrorHigh, + cricket::FOURCC_GRBG, libyuv::BayerGRBGToI420); + } + void ConvertToBayerRGGBBufferStride() { + ConvertToBuffer(1, kOddStride, false, TO, kErrorHigh, + cricket::FOURCC_RGGB, libyuv::BayerRGGBToI420); + } + void ConvertToI400BufferStride() { + ConvertToBuffer(1, kOddStride, false, TO, 128, + cricket::FOURCC_I400, libyuv::I400ToI420); + } + void ConvertToYUY2BufferStride() { + ConvertToBuffer(2, kOddStride, false, TO, kError, + cricket::FOURCC_YUY2, libyuv::YUY2ToI420); + } + void ConvertToUYVYBufferStride() { + ConvertToBuffer(2, kOddStride, false, TO, kError, + cricket::FOURCC_UYVY, libyuv::UYVYToI420); + } + + // Tests ConvertToRGBBuffer formats with negative stride to invert image. + void ConvertToARGBBufferInverted() { + ConvertToBuffer(4, 0, true, TO, kError, + cricket::FOURCC_ARGB, libyuv::ARGBToI420); + } + void ConvertToBGRABufferInverted() { + ConvertToBuffer(4, 0, true, TO, kError, + cricket::FOURCC_BGRA, libyuv::BGRAToI420); + } + void ConvertToABGRBufferInverted() { + ConvertToBuffer(4, 0, true, TO, kError, + cricket::FOURCC_ABGR, libyuv::ABGRToI420); + } + void ConvertToRGB24BufferInverted() { + ConvertToBuffer(3, 0, true, TO, kError, + cricket::FOURCC_24BG, libyuv::RGB24ToI420); + } + void ConvertToRAWBufferInverted() { + ConvertToBuffer(3, 0, true, TO, kError, + cricket::FOURCC_RAW, libyuv::RAWToI420); + } + void ConvertToRGB565BufferInverted() { + ConvertToBuffer(2, 0, true, TO, kError, + cricket::FOURCC_RGBP, libyuv::RGB565ToI420); + } + void ConvertToARGB1555BufferInverted() { + ConvertToBuffer(2, 0, true, TO, kError, + cricket::FOURCC_RGBO, libyuv::ARGB1555ToI420); + } + void ConvertToARGB4444BufferInverted() { + ConvertToBuffer(2, 0, true, TO, kError, + cricket::FOURCC_R444, libyuv::ARGB4444ToI420); + } + void ConvertToBayerBGGRBufferInverted() { + ConvertToBuffer(1, 0, true, TO, kErrorHigh, + cricket::FOURCC_BGGR, libyuv::BayerBGGRToI420); + } + void ConvertToBayerGBRGBufferInverted() { + ConvertToBuffer(1, 0, true, TO, kErrorHigh, + cricket::FOURCC_GBRG, libyuv::BayerGBRGToI420); + } + void ConvertToBayerGRBGBufferInverted() { + ConvertToBuffer(1, 0, true, TO, kErrorHigh, + cricket::FOURCC_GRBG, libyuv::BayerGRBGToI420); + } + void ConvertToBayerRGGBBufferInverted() { + ConvertToBuffer(1, 0, true, TO, kErrorHigh, + cricket::FOURCC_RGGB, libyuv::BayerRGGBToI420); + } + void ConvertToI400BufferInverted() { + ConvertToBuffer(1, 0, true, TO, 128, + cricket::FOURCC_I400, libyuv::I400ToI420); + } + void ConvertToYUY2BufferInverted() { + ConvertToBuffer(2, 0, true, TO, kError, + cricket::FOURCC_YUY2, libyuv::YUY2ToI420); + } + void ConvertToUYVYBufferInverted() { + ConvertToBuffer(2, 0, true, TO, kError, + cricket::FOURCC_UYVY, libyuv::UYVYToI420); + } + + // Tests ConvertFrom formats. + void ConvertFromARGBBuffer() { + ConvertToBuffer(4, 0, false, FROM, kError, + cricket::FOURCC_ARGB, libyuv::ARGBToI420); + } + void ConvertFromBGRABuffer() { + ConvertToBuffer(4, 0, false, FROM, kError, + cricket::FOURCC_BGRA, libyuv::BGRAToI420); + } + void ConvertFromABGRBuffer() { + ConvertToBuffer(4, 0, false, FROM, kError, + cricket::FOURCC_ABGR, libyuv::ABGRToI420); + } + void ConvertFromRGB24Buffer() { + ConvertToBuffer(3, 0, false, FROM, kError, + cricket::FOURCC_24BG, libyuv::RGB24ToI420); + } + void ConvertFromRAWBuffer() { + ConvertToBuffer(3, 0, false, FROM, kError, + cricket::FOURCC_RAW, libyuv::RAWToI420); + } + void ConvertFromRGB565Buffer() { + ConvertToBuffer(2, 0, false, FROM, kError, + cricket::FOURCC_RGBP, libyuv::RGB565ToI420); + } + void ConvertFromARGB1555Buffer() { + ConvertToBuffer(2, 0, false, FROM, kError, + cricket::FOURCC_RGBO, libyuv::ARGB1555ToI420); + } + void ConvertFromARGB4444Buffer() { + ConvertToBuffer(2, 0, false, FROM, kError, + cricket::FOURCC_R444, libyuv::ARGB4444ToI420); + } + void ConvertFromBayerBGGRBuffer() { + ConvertToBuffer(1, 0, false, FROM, kErrorHigh, + cricket::FOURCC_BGGR, libyuv::BayerBGGRToI420); + } + void ConvertFromBayerGBRGBuffer() { + ConvertToBuffer(1, 0, false, FROM, kErrorHigh, + cricket::FOURCC_GBRG, libyuv::BayerGBRGToI420); + } + void ConvertFromBayerGRBGBuffer() { + ConvertToBuffer(1, 0, false, FROM, kErrorHigh, + cricket::FOURCC_GRBG, libyuv::BayerGRBGToI420); + } + void ConvertFromBayerRGGBBuffer() { + ConvertToBuffer(1, 0, false, FROM, kErrorHigh, + cricket::FOURCC_RGGB, libyuv::BayerRGGBToI420); + } + void ConvertFromI400Buffer() { + ConvertToBuffer(1, 0, false, FROM, 128, + cricket::FOURCC_I400, libyuv::I400ToI420); + } + void ConvertFromYUY2Buffer() { + ConvertToBuffer(2, 0, false, FROM, kError, + cricket::FOURCC_YUY2, libyuv::YUY2ToI420); + } + void ConvertFromUYVYBuffer() { + ConvertToBuffer(2, 0, false, FROM, kError, + cricket::FOURCC_UYVY, libyuv::UYVYToI420); + } + + // Tests ConvertFrom formats with odd stride. + void ConvertFromARGBBufferStride() { + ConvertToBuffer(4, kOddStride, false, FROM, kError, + cricket::FOURCC_ARGB, libyuv::ARGBToI420); + } + void ConvertFromBGRABufferStride() { + ConvertToBuffer(4, kOddStride, false, FROM, kError, + cricket::FOURCC_BGRA, libyuv::BGRAToI420); + } + void ConvertFromABGRBufferStride() { + ConvertToBuffer(4, kOddStride, false, FROM, kError, + cricket::FOURCC_ABGR, libyuv::ABGRToI420); + } + void ConvertFromRGB24BufferStride() { + ConvertToBuffer(3, kOddStride, false, FROM, kError, + cricket::FOURCC_24BG, libyuv::RGB24ToI420); + } + void ConvertFromRAWBufferStride() { + ConvertToBuffer(3, kOddStride, false, FROM, kError, + cricket::FOURCC_RAW, libyuv::RAWToI420); + } + void ConvertFromRGB565BufferStride() { + ConvertToBuffer(2, kOddStride, false, FROM, kError, + cricket::FOURCC_RGBP, libyuv::RGB565ToI420); + } + void ConvertFromARGB1555BufferStride() { + ConvertToBuffer(2, kOddStride, false, FROM, kError, + cricket::FOURCC_RGBO, libyuv::ARGB1555ToI420); + } + void ConvertFromARGB4444BufferStride() { + ConvertToBuffer(2, kOddStride, false, FROM, kError, + cricket::FOURCC_R444, libyuv::ARGB4444ToI420); + } + void ConvertFromBayerBGGRBufferStride() { + ConvertToBuffer(1, kOddStride, false, FROM, kErrorHigh, + cricket::FOURCC_BGGR, libyuv::BayerBGGRToI420); + } + void ConvertFromBayerGBRGBufferStride() { + ConvertToBuffer(1, kOddStride, false, FROM, kErrorHigh, + cricket::FOURCC_GBRG, libyuv::BayerGBRGToI420); + } + void ConvertFromBayerGRBGBufferStride() { + ConvertToBuffer(1, kOddStride, false, FROM, kErrorHigh, + cricket::FOURCC_GRBG, libyuv::BayerGRBGToI420); + } + void ConvertFromBayerRGGBBufferStride() { + ConvertToBuffer(1, kOddStride, false, FROM, kErrorHigh, + cricket::FOURCC_RGGB, libyuv::BayerRGGBToI420); + } + void ConvertFromI400BufferStride() { + ConvertToBuffer(1, kOddStride, false, FROM, 128, + cricket::FOURCC_I400, libyuv::I400ToI420); + } + void ConvertFromYUY2BufferStride() { + ConvertToBuffer(2, kOddStride, false, FROM, kError, + cricket::FOURCC_YUY2, libyuv::YUY2ToI420); + } + void ConvertFromUYVYBufferStride() { + ConvertToBuffer(2, kOddStride, false, FROM, kError, + cricket::FOURCC_UYVY, libyuv::UYVYToI420); + } + + // Tests ConvertFrom formats with negative stride to invert image. + void ConvertFromARGBBufferInverted() { + ConvertToBuffer(4, 0, true, FROM, kError, + cricket::FOURCC_ARGB, libyuv::ARGBToI420); + } + void ConvertFromBGRABufferInverted() { + ConvertToBuffer(4, 0, true, FROM, kError, + cricket::FOURCC_BGRA, libyuv::BGRAToI420); + } + void ConvertFromABGRBufferInverted() { + ConvertToBuffer(4, 0, true, FROM, kError, + cricket::FOURCC_ABGR, libyuv::ABGRToI420); + } + void ConvertFromRGB24BufferInverted() { + ConvertToBuffer(3, 0, true, FROM, kError, + cricket::FOURCC_24BG, libyuv::RGB24ToI420); + } + void ConvertFromRAWBufferInverted() { + ConvertToBuffer(3, 0, true, FROM, kError, + cricket::FOURCC_RAW, libyuv::RAWToI420); + } + void ConvertFromRGB565BufferInverted() { + ConvertToBuffer(2, 0, true, FROM, kError, + cricket::FOURCC_RGBP, libyuv::RGB565ToI420); + } + void ConvertFromARGB1555BufferInverted() { + ConvertToBuffer(2, 0, true, FROM, kError, + cricket::FOURCC_RGBO, libyuv::ARGB1555ToI420); + } + void ConvertFromARGB4444BufferInverted() { + ConvertToBuffer(2, 0, true, FROM, kError, + cricket::FOURCC_R444, libyuv::ARGB4444ToI420); + } + void ConvertFromBayerBGGRBufferInverted() { + ConvertToBuffer(1, 0, true, FROM, kErrorHigh, + cricket::FOURCC_BGGR, libyuv::BayerBGGRToI420); + } + void ConvertFromBayerGBRGBufferInverted() { + ConvertToBuffer(1, 0, true, FROM, kErrorHigh, + cricket::FOURCC_GBRG, libyuv::BayerGBRGToI420); + } + void ConvertFromBayerGRBGBufferInverted() { + ConvertToBuffer(1, 0, true, FROM, kErrorHigh, + cricket::FOURCC_GRBG, libyuv::BayerGRBGToI420); + } + void ConvertFromBayerRGGBBufferInverted() { + ConvertToBuffer(1, 0, true, FROM, kErrorHigh, + cricket::FOURCC_RGGB, libyuv::BayerRGGBToI420); + } + void ConvertFromI400BufferInverted() { + ConvertToBuffer(1, 0, true, FROM, 128, + cricket::FOURCC_I400, libyuv::I400ToI420); + } + void ConvertFromYUY2BufferInverted() { + ConvertToBuffer(2, 0, true, FROM, kError, + cricket::FOURCC_YUY2, libyuv::YUY2ToI420); + } + void ConvertFromUYVYBufferInverted() { + ConvertToBuffer(2, 0, true, FROM, kError, + cricket::FOURCC_UYVY, libyuv::UYVYToI420); + } + + // Test converting from I420 to I422. + void ConvertToI422Buffer() { + T frame1, frame2; + size_t out_size = kWidth * kHeight * 2; + talk_base::scoped_array buf(new uint8[out_size + kAlignment]); + uint8* y = ALIGNP(buf.get(), kAlignment); + uint8* u = y + kWidth * kHeight; + uint8* v = u + (kWidth / 2) * kHeight; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + for (int i = 0; i < repeat_; ++i) { + EXPECT_EQ(0, libyuv::I420ToI422(frame1.GetYPlane(), frame1.GetYPitch(), + frame1.GetUPlane(), frame1.GetUPitch(), + frame1.GetVPlane(), frame1.GetVPitch(), + y, kWidth, + u, kWidth / 2, + v, kWidth / 2, + kWidth, kHeight)); + } + EXPECT_TRUE(frame2.Init(cricket::FOURCC_I422, + kWidth, kHeight, kWidth, kHeight, + y, + out_size, 1, 1, 0, 0, cricket::ROTATION_0)); + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + } + + #define TEST_TOBYR(NAME, BAYER) \ + void NAME() { \ + size_t bayer_size = kWidth * kHeight; \ + talk_base::scoped_array bayerbuf(new uint8[ \ + bayer_size + kAlignment]); \ + uint8 *bayer = ALIGNP(bayerbuf.get(), kAlignment); \ + T frame; \ + talk_base::scoped_ptr ms( \ + CreateRgbSample(cricket::FOURCC_ARGB, kWidth, kHeight)); \ + ASSERT_TRUE(ms.get() != NULL); \ + for (int i = 0; i < repeat_; ++i) { \ + libyuv::ARGBToBayer##BAYER(reinterpret_cast(ms->GetBuffer()), \ + kWidth * 4, \ + bayer, kWidth, \ + kWidth, kHeight); \ + } \ + talk_base::scoped_ptr ms2( \ + CreateRgbSample(cricket::FOURCC_ARGB, kWidth, kHeight)); \ + size_t data_size; \ + bool ret = ms2->GetSize(&data_size); \ + EXPECT_TRUE(ret); \ + libyuv::Bayer##BAYER##ToARGB(bayer, kWidth, \ + reinterpret_cast(ms2->GetBuffer()), \ + kWidth * 4, \ + kWidth, kHeight); \ + EXPECT_TRUE(IsPlaneEqual("argb", \ + reinterpret_cast(ms->GetBuffer()), kWidth * 4, \ + reinterpret_cast(ms2->GetBuffer()), kWidth * 4, \ + kWidth * 4, kHeight, 240)); \ + } \ + void NAME##Unaligned() { \ + size_t bayer_size = kWidth * kHeight; \ + talk_base::scoped_array bayerbuf(new uint8[ \ + bayer_size + 1 + kAlignment]); \ + uint8 *bayer = ALIGNP(bayerbuf.get(), kAlignment) + 1; \ + T frame; \ + talk_base::scoped_ptr ms( \ + CreateRgbSample(cricket::FOURCC_ARGB, kWidth, kHeight)); \ + ASSERT_TRUE(ms.get() != NULL); \ + for (int i = 0; i < repeat_; ++i) { \ + libyuv::ARGBToBayer##BAYER(reinterpret_cast(ms->GetBuffer()), \ + kWidth * 4, \ + bayer, kWidth, \ + kWidth, kHeight); \ + } \ + talk_base::scoped_ptr ms2( \ + CreateRgbSample(cricket::FOURCC_ARGB, kWidth, kHeight)); \ + size_t data_size; \ + bool ret = ms2->GetSize(&data_size); \ + EXPECT_TRUE(ret); \ + libyuv::Bayer##BAYER##ToARGB(bayer, kWidth, \ + reinterpret_cast(ms2->GetBuffer()), \ + kWidth * 4, \ + kWidth, kHeight); \ + EXPECT_TRUE(IsPlaneEqual("argb", \ + reinterpret_cast(ms->GetBuffer()), kWidth * 4, \ + reinterpret_cast(ms2->GetBuffer()), kWidth * 4, \ + kWidth * 4, kHeight, 240)); \ + } + + // Tests ARGB to Bayer formats. + TEST_TOBYR(ConvertARGBToBayerGRBG, GRBG) + TEST_TOBYR(ConvertARGBToBayerGBRG, GBRG) + TEST_TOBYR(ConvertARGBToBayerBGGR, BGGR) + TEST_TOBYR(ConvertARGBToBayerRGGB, RGGB) + + #define TEST_BYRTORGB(NAME, BAYER) \ + void NAME() { \ + size_t bayer_size = kWidth * kHeight; \ + talk_base::scoped_array bayerbuf(new uint8[ \ + bayer_size + kAlignment]); \ + uint8 *bayer1 = ALIGNP(bayerbuf.get(), kAlignment); \ + for (int i = 0; i < kWidth * kHeight; ++i) { \ + bayer1[i] = static_cast(i * 33u + 183u); \ + } \ + T frame; \ + talk_base::scoped_ptr ms( \ + CreateRgbSample(cricket::FOURCC_ARGB, kWidth, kHeight)); \ + ASSERT_TRUE(ms.get() != NULL); \ + for (int i = 0; i < repeat_; ++i) { \ + libyuv::Bayer##BAYER##ToARGB(bayer1, kWidth, \ + reinterpret_cast(ms->GetBuffer()), \ + kWidth * 4, \ + kWidth, kHeight); \ + } \ + talk_base::scoped_array bayer2buf(new uint8[ \ + bayer_size + kAlignment]); \ + uint8 *bayer2 = ALIGNP(bayer2buf.get(), kAlignment); \ + libyuv::ARGBToBayer##BAYER(reinterpret_cast(ms->GetBuffer()), \ + kWidth * 4, \ + bayer2, kWidth, \ + kWidth, kHeight); \ + EXPECT_TRUE(IsPlaneEqual("bayer", \ + bayer1, kWidth, \ + bayer2, kWidth, \ + kWidth, kHeight, 0)); \ + } + + // Tests Bayer formats to ARGB. + TEST_BYRTORGB(ConvertBayerGRBGToARGB, GRBG) + TEST_BYRTORGB(ConvertBayerGBRGToARGB, GBRG) + TEST_BYRTORGB(ConvertBayerBGGRToARGB, BGGR) + TEST_BYRTORGB(ConvertBayerRGGBToARGB, RGGB) + + /////////////////// + // General tests // + /////////////////// + + void Copy() { + talk_base::scoped_ptr source(new T); + talk_base::scoped_ptr target; + ASSERT_TRUE(LoadFrameNoRepeat(source.get())); + target.reset(source->Copy()); + EXPECT_TRUE(IsEqual(*source, *target, 0)); + source.reset(); + EXPECT_TRUE(target->GetYPlane() != NULL); + } + + void CopyIsRef() { + talk_base::scoped_ptr source(new T); + talk_base::scoped_ptr target; + ASSERT_TRUE(LoadFrameNoRepeat(source.get())); + target.reset(source->Copy()); + EXPECT_TRUE(IsEqual(*source, *target, 0)); + EXPECT_EQ(source->GetYPlane(), target->GetYPlane()); + EXPECT_EQ(source->GetUPlane(), target->GetUPlane()); + EXPECT_EQ(source->GetVPlane(), target->GetVPlane()); + } + + void MakeExclusive() { + talk_base::scoped_ptr source(new T); + talk_base::scoped_ptr target; + ASSERT_TRUE(LoadFrameNoRepeat(source.get())); + target.reset(source->Copy()); + EXPECT_TRUE(target->MakeExclusive()); + EXPECT_TRUE(IsEqual(*source, *target, 0)); + EXPECT_NE(target->GetYPlane(), source->GetYPlane()); + EXPECT_NE(target->GetUPlane(), source->GetUPlane()); + EXPECT_NE(target->GetVPlane(), source->GetVPlane()); + } + + void CopyToBuffer() { + T frame; + talk_base::scoped_ptr ms( + LoadSample(kImageFilename)); + ASSERT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_I420, kWidth, kHeight, + &frame)); + size_t out_size = kWidth * kHeight * 3 / 2; + talk_base::scoped_array out(new uint8[out_size]); + for (int i = 0; i < repeat_; ++i) { + EXPECT_EQ(out_size, frame.CopyToBuffer(out.get(), out_size)); + } + EXPECT_EQ(0, memcmp(out.get(), ms->GetBuffer(), out_size)); + } + + void CopyToFrame() { + T source; + talk_base::scoped_ptr ms( + LoadSample(kImageFilename)); + ASSERT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_I420, kWidth, kHeight, + &source)); + + // Create the target frame by loading from a file. + T target; + ASSERT_TRUE(LoadFrameNoRepeat(&target)); + EXPECT_FALSE(IsBlack(target)); + + // Stretch and check if the stretched target is black. + source.CopyToFrame(&target); + + EXPECT_TRUE(IsEqual(source, target, 0)); + } + + void Write() { + T frame; + talk_base::scoped_ptr ms( + LoadSample(kImageFilename)); + talk_base::MemoryStream ms2; + size_t size; + ASSERT_TRUE(ms->GetSize(&size)); + ASSERT_TRUE(ms2.ReserveSize(size)); + ASSERT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_I420, kWidth, kHeight, + &frame)); + for (int i = 0; i < repeat_; ++i) { + ms2.SetPosition(0u); // Useful when repeat_ > 1. + int error; + EXPECT_EQ(talk_base::SR_SUCCESS, frame.Write(&ms2, &error)); + } + size_t out_size = cricket::VideoFrame::SizeOf(kWidth, kHeight); + EXPECT_EQ(0, memcmp(ms2.GetBuffer(), ms->GetBuffer(), out_size)); + } + + void CopyToBuffer1Pixel() { + size_t out_size = 3; + talk_base::scoped_array out(new uint8[out_size + 1]); + memset(out.get(), 0xfb, out_size + 1); // Fill buffer + uint8 pixel[3] = { 1, 2, 3 }; + T frame; + EXPECT_TRUE(frame.Init(cricket::FOURCC_I420, 1, 1, 1, 1, + pixel, sizeof(pixel), + 1, 1, 0, 0, 0)); + for (int i = 0; i < repeat_; ++i) { + EXPECT_EQ(out_size, frame.CopyToBuffer(out.get(), out_size)); + } + EXPECT_EQ(1, out.get()[0]); // Check Y. Should be 1. + EXPECT_EQ(2, out.get()[1]); // Check U. Should be 2. + EXPECT_EQ(3, out.get()[2]); // Check V. Should be 3. + EXPECT_EQ(0xfb, out.get()[3]); // Check sentinel is still intact. + } + + void StretchToFrame() { + // Create the source frame as a black frame. + T source; + EXPECT_TRUE(source.InitToBlack(kWidth * 2, kHeight * 2, 1, 1, 0, 0)); + EXPECT_TRUE(IsSize(source, kWidth * 2, kHeight * 2)); + + // Create the target frame by loading from a file. + T target1; + ASSERT_TRUE(LoadFrameNoRepeat(&target1)); + EXPECT_FALSE(IsBlack(target1)); + + // Stretch and check if the stretched target is black. + source.StretchToFrame(&target1, true, false); + EXPECT_TRUE(IsBlack(target1)); + + // Crop and stretch and check if the stretched target is black. + T target2; + ASSERT_TRUE(LoadFrameNoRepeat(&target2)); + source.StretchToFrame(&target2, true, true); + EXPECT_TRUE(IsBlack(target2)); + EXPECT_EQ(source.GetElapsedTime(), target2.GetElapsedTime()); + EXPECT_EQ(source.GetTimeStamp(), target2.GetTimeStamp()); + } + + int repeat_; +}; + +#endif // TALK_MEDIA_BASE_VIDEOFRAME_UNITTEST_H_ diff --git a/talk/media/base/videoprocessor.h b/talk/media/base/videoprocessor.h new file mode 100755 index 000000000..412d9892a --- /dev/null +++ b/talk/media/base/videoprocessor.h @@ -0,0 +1,50 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_VIDEOPROCESSOR_H_ +#define TALK_MEDIA_BASE_VIDEOPROCESSOR_H_ + +#include "talk/base/sigslot.h" +#include "talk/media/base/videoframe.h" + +namespace cricket { + +class VideoProcessor : public sigslot::has_slots<> { + public: + virtual ~VideoProcessor() {} + // Contents of frame may be manipulated by the processor. + // The processed data is expected to be the same size as the + // original data. VideoProcessors may be chained together and may decide + // that the current frame should be dropped. If *drop_frame is true, + // the current processor should skip processing. If the current processor + // decides it cannot process the current frame in a timely manner, it may set + // *drop_frame = true and the frame will be dropped. + virtual void OnFrame(uint32 ssrc, VideoFrame* frame, bool* drop_frame) = 0; +}; + +} // namespace cricket +#endif // TALK_MEDIA_BASE_VIDEOPROCESSOR_H_ diff --git a/talk/media/base/videorenderer.h b/talk/media/base/videorenderer.h new file mode 100644 index 000000000..ccbe978ca --- /dev/null +++ b/talk/media/base/videorenderer.h @@ -0,0 +1,58 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_VIDEORENDERER_H_ +#define TALK_MEDIA_BASE_VIDEORENDERER_H_ + +#ifdef _DEBUG +#include +#endif // _DEBUG + +#include "talk/base/sigslot.h" + +namespace cricket { + +class VideoFrame; + +// Abstract interface for rendering VideoFrames. +class VideoRenderer { + public: + virtual ~VideoRenderer() {} + // Called when the video has changed size. + virtual bool SetSize(int width, int height, int reserved) = 0; + // Called when a new frame is available for display. + virtual bool RenderFrame(const VideoFrame *frame) = 0; + +#ifdef _DEBUG + // Allow renderer dumping out rendered frames. + virtual bool SetDumpPath(const std::string &path) { return true; } +#endif // _DEBUG +}; + +} // namespace cricket + +#endif // TALK_MEDIA_BASE_VIDEORENDERER_H_ diff --git a/talk/media/base/voiceprocessor.h b/talk/media/base/voiceprocessor.h new file mode 100755 index 000000000..576bdca66 --- /dev/null +++ b/talk/media/base/voiceprocessor.h @@ -0,0 +1,56 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_BASE_VOICEPROCESSOR_H_ +#define TALK_MEDIA_BASE_VOICEPROCESSOR_H_ + +#include "talk/base/basictypes.h" +#include "talk/base/sigslot.h" +#include "talk/media/base/audioframe.h" + +namespace cricket { + +enum MediaProcessorDirection { + MPD_INVALID = 0, + MPD_RX = 1 << 0, + MPD_TX = 1 << 1, + MPD_RX_AND_TX = MPD_RX | MPD_TX, +}; + +class VoiceProcessor : public sigslot::has_slots<> { + public: + virtual ~VoiceProcessor() {} + // Contents of frame may be manipulated by the processor. + // The processed data is expected to be the same size as the + // original data. + virtual void OnFrame(uint32 ssrc, + MediaProcessorDirection direction, + AudioFrame* frame) = 0; +}; + +} // namespace cricket +#endif // TALK_MEDIA_BASE_VOICEPROCESSOR_H_ diff --git a/talk/media/devices/carbonvideorenderer.cc b/talk/media/devices/carbonvideorenderer.cc new file mode 100644 index 000000000..71abf2629 --- /dev/null +++ b/talk/media/devices/carbonvideorenderer.cc @@ -0,0 +1,182 @@ +// libjingle +// Copyright 2011 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. +// +// Implementation of CarbonVideoRenderer + +#include "talk/media/devices/carbonvideorenderer.h" + +#include "talk/base/logging.h" +#include "talk/media/base/videocommon.h" +#include "talk/media/base/videoframe.h" + +namespace cricket { + +CarbonVideoRenderer::CarbonVideoRenderer(int x, int y) + : image_width_(0), + image_height_(0), + x_(x), + y_(y), + image_ref_(NULL), + window_ref_(NULL) { +} + +CarbonVideoRenderer::~CarbonVideoRenderer() { + if (window_ref_) { + DisposeWindow(window_ref_); + } +} + +// Called from the main event loop. All renderering needs to happen on +// the main thread. +OSStatus CarbonVideoRenderer::DrawEventHandler(EventHandlerCallRef handler, + EventRef event, + void* data) { + OSStatus status = noErr; + CarbonVideoRenderer* renderer = static_cast(data); + if (renderer != NULL) { + if (!renderer->DrawFrame()) { + LOG(LS_ERROR) << "Failed to draw frame."; + } + } + return status; +} + +bool CarbonVideoRenderer::DrawFrame() { + // Grab the image lock to make sure it is not changed why we'll draw it. + talk_base::CritScope cs(&image_crit_); + + if (image_.get() == NULL) { + // Nothing to draw, just return. + return true; + } + int width = image_width_; + int height = image_height_; + CGDataProviderRef provider = + CGDataProviderCreateWithData(NULL, image_.get(), width * height * 4, + NULL); + CGColorSpaceRef color_space_ref = CGColorSpaceCreateDeviceRGB(); + CGBitmapInfo bitmap_info = kCGBitmapByteOrderDefault; + CGColorRenderingIntent rendering_intent = kCGRenderingIntentDefault; + CGImageRef image_ref = CGImageCreate(width, height, 8, 32, width * 4, + color_space_ref, bitmap_info, provider, + NULL, false, rendering_intent); + CGDataProviderRelease(provider); + + if (image_ref == NULL) { + return false; + } + CGContextRef context; + SetPortWindowPort(window_ref_); + if (QDBeginCGContext(GetWindowPort(window_ref_), &context) != noErr) { + CGImageRelease(image_ref); + return false; + } + Rect window_bounds; + GetWindowPortBounds(window_ref_, &window_bounds); + + // Anchor the image to the top left corner. + int x = 0; + int y = window_bounds.bottom - CGImageGetHeight(image_ref); + CGRect dst_rect = CGRectMake(x, y, CGImageGetWidth(image_ref), + CGImageGetHeight(image_ref)); + CGContextDrawImage(context, dst_rect, image_ref); + CGContextFlush(context); + QDEndCGContext(GetWindowPort(window_ref_), &context); + CGImageRelease(image_ref); + return true; +} + +bool CarbonVideoRenderer::SetSize(int width, int height, int reserved) { + if (width != image_width_ || height != image_height_) { + // Grab the image lock while changing its size. + talk_base::CritScope cs(&image_crit_); + image_width_ = width; + image_height_ = height; + image_.reset(new uint8[width * height * 4]); + memset(image_.get(), 255, width * height * 4); + } + return true; +} + +bool CarbonVideoRenderer::RenderFrame(const VideoFrame* frame) { + if (!frame) { + return false; + } + { + // Grab the image lock so we are not trashing up the image being drawn. + talk_base::CritScope cs(&image_crit_); + frame->ConvertToRgbBuffer(cricket::FOURCC_ABGR, + image_.get(), + frame->GetWidth() * frame->GetHeight() * 4, + frame->GetWidth() * 4); + } + + // Trigger a repaint event for the whole window. + Rect bounds; + InvalWindowRect(window_ref_, GetWindowPortBounds(window_ref_, &bounds)); + return true; +} + +bool CarbonVideoRenderer::Initialize() { + OSStatus err; + WindowAttributes attributes = + kWindowStandardDocumentAttributes | + kWindowLiveResizeAttribute | + kWindowFrameworkScaledAttribute | + kWindowStandardHandlerAttribute; + + struct Rect bounds; + bounds.top = y_; + bounds.bottom = 480; + bounds.left = x_; + bounds.right = 640; + err = CreateNewWindow(kDocumentWindowClass, attributes, + &bounds, &window_ref_); + if (!window_ref_ || err != noErr) { + LOG(LS_ERROR) << "CreateNewWindow failed, error code: " << err; + return false; + } + static const EventTypeSpec event_spec = { + kEventClassWindow, + kEventWindowDrawContent + }; + + err = InstallWindowEventHandler( + window_ref_, + NewEventHandlerUPP(CarbonVideoRenderer::DrawEventHandler), + GetEventTypeCount(event_spec), + &event_spec, + this, + NULL); + if (err != noErr) { + LOG(LS_ERROR) << "Failed to install event handler, error code: " << err; + return false; + } + SelectWindow(window_ref_); + ShowWindow(window_ref_); + return true; +} + +} // namespace cricket diff --git a/talk/media/devices/carbonvideorenderer.h b/talk/media/devices/carbonvideorenderer.h new file mode 100644 index 000000000..e09118613 --- /dev/null +++ b/talk/media/devices/carbonvideorenderer.h @@ -0,0 +1,72 @@ +// libjingle +// Copyright 2011 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. +// +// Definition of class CarbonVideoRenderer that implements the abstract class +// cricket::VideoRenderer via Carbon. + +#ifndef TALK_MEDIA_DEVICES_CARBONVIDEORENDERER_H_ +#define TALK_MEDIA_DEVICES_CARBONVIDEORENDERER_H_ + +#include + +#include "talk/base/criticalsection.h" +#include "talk/base/scoped_ptr.h" +#include "talk/media/base/videorenderer.h" + +namespace cricket { + +class CarbonVideoRenderer : public VideoRenderer { + public: + CarbonVideoRenderer(int x, int y); + virtual ~CarbonVideoRenderer(); + + // Implementation of pure virtual methods of VideoRenderer. + // These two methods may be executed in different threads. + // SetSize is called before RenderFrame. + virtual bool SetSize(int width, int height, int reserved); + virtual bool RenderFrame(const VideoFrame* frame); + + // Needs to be called on the main thread. + bool Initialize(); + + private: + bool DrawFrame(); + + static OSStatus DrawEventHandler(EventHandlerCallRef handler, + EventRef event, + void* data); + talk_base::scoped_array image_; + talk_base::CriticalSection image_crit_; + int image_width_; + int image_height_; + int x_; + int y_; + CGImageRef image_ref_; + WindowRef window_ref_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_DEVICES_CARBONVIDEORENDERER_H_ diff --git a/talk/media/devices/deviceinfo.h b/talk/media/devices/deviceinfo.h new file mode 100644 index 000000000..86382f614 --- /dev/null +++ b/talk/media/devices/deviceinfo.h @@ -0,0 +1,42 @@ +/* + * libjingle + * Copyright 2012 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. + */ + +#ifndef TALK_MEDIA_DEVICES_DEVICEINFO_H_ +#define TALK_MEDIA_DEVICES_DEVICEINFO_H_ + +#include + +#include "talk/media/devices/devicemanager.h" + +namespace cricket { + +bool GetUsbId(const Device& device, std::string* usb_id); +bool GetUsbVersion(const Device& device, std::string* usb_version); + +} // namespace cricket + +#endif // TALK_MEDIA_DEVICES_DEVICEINFO_H_ diff --git a/talk/media/devices/devicemanager.cc b/talk/media/devices/devicemanager.cc new file mode 100644 index 000000000..2ce5eb08b --- /dev/null +++ b/talk/media/devices/devicemanager.cc @@ -0,0 +1,389 @@ +/* + * libjingle + * Copyright 2004 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/media/devices/devicemanager.h" + +#include "talk/base/fileutils.h" +#include "talk/base/logging.h" +#include "talk/base/pathutils.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/base/windowpicker.h" +#include "talk/base/windowpickerfactory.h" +#include "talk/media/base/mediacommon.h" +#include "talk/media/devices/deviceinfo.h" +#include "talk/media/devices/filevideocapturer.h" + +#if !defined(IOS) + +#if defined(HAVE_WEBRTC_VIDEO) +#include "talk/media/webrtc/webrtcvideocapturer.h" +#endif + + +#if defined(HAVE_WEBRTC_VIDEO) +#define VIDEO_CAPTURER_NAME WebRtcVideoCapturer +#endif + + +#endif + +namespace { + +bool StringMatchWithWildcard( + const std::pair, cricket::VideoFormat> key, + const std::string& val) { + return talk_base::string_match(val.c_str(), key.first.c_str()); +} + +} // namespace + +namespace cricket { + +// Initialize to empty string. +const char DeviceManagerInterface::kDefaultDeviceName[] = ""; + + +class DefaultVideoCapturerFactory : public VideoCapturerFactory { + public: + DefaultVideoCapturerFactory() {} + virtual ~DefaultVideoCapturerFactory() {} + + VideoCapturer* Create(const Device& device) { +#if defined(VIDEO_CAPTURER_NAME) + VIDEO_CAPTURER_NAME* return_value = new VIDEO_CAPTURER_NAME; + if (!return_value->Init(device)) { + delete return_value; + return NULL; + } + return return_value; +#else + return NULL; +#endif + } +}; + +DeviceManager::DeviceManager() + : initialized_(false), + device_video_capturer_factory_(new DefaultVideoCapturerFactory), + window_picker_(talk_base::WindowPickerFactory::CreateWindowPicker()) { +} + +DeviceManager::~DeviceManager() { + if (initialized()) { + Terminate(); + } +} + +bool DeviceManager::Init() { + if (!initialized()) { + if (!watcher()->Start()) { + return false; + } + set_initialized(true); + } + return true; +} + +void DeviceManager::Terminate() { + if (initialized()) { + watcher()->Stop(); + set_initialized(false); + } +} + +int DeviceManager::GetCapabilities() { + std::vector devices; + int caps = VIDEO_RECV; + if (GetAudioInputDevices(&devices) && !devices.empty()) { + caps |= AUDIO_SEND; + } + if (GetAudioOutputDevices(&devices) && !devices.empty()) { + caps |= AUDIO_RECV; + } + if (GetVideoCaptureDevices(&devices) && !devices.empty()) { + caps |= VIDEO_SEND; + } + return caps; +} + +bool DeviceManager::GetAudioInputDevices(std::vector* devices) { + return GetAudioDevices(true, devices); +} + +bool DeviceManager::GetAudioOutputDevices(std::vector* devices) { + return GetAudioDevices(false, devices); +} + +bool DeviceManager::GetAudioInputDevice(const std::string& name, Device* out) { + return GetAudioDevice(true, name, out); +} + +bool DeviceManager::GetAudioOutputDevice(const std::string& name, Device* out) { + return GetAudioDevice(false, name, out); +} + +bool DeviceManager::GetVideoCaptureDevices(std::vector* devices) { + devices->clear(); +#if defined(IOS) + // On iOS, we treat the camera(s) as a single device. Even if there are + // multiple cameras, that's abstracted away at a higher level. + Device dev("camera", "1"); // name and ID + devices->push_back(dev); + return true; +#else + return false; +#endif +} + +bool DeviceManager::GetVideoCaptureDevice(const std::string& name, + Device* out) { + // If the name is empty, return the default device. + if (name.empty() || name == kDefaultDeviceName) { + return GetDefaultVideoCaptureDevice(out); + } + + std::vector devices; + if (!GetVideoCaptureDevices(&devices)) { + return false; + } + + for (std::vector::const_iterator it = devices.begin(); + it != devices.end(); ++it) { + if (name == it->name) { + *out = *it; + return true; + } + } + + // If |name| is a valid name for a file, return a file video capturer device. + if (talk_base::Filesystem::IsFile(name)) { + *out = FileVideoCapturer::CreateFileVideoCapturerDevice(name); + return true; + } + + return false; +} + +void DeviceManager::SetVideoCaptureDeviceMaxFormat( + const std::string& usb_id, + const VideoFormat& max_format) { + max_formats_[usb_id] = max_format; +} + +void DeviceManager::ClearVideoCaptureDeviceMaxFormat( + const std::string& usb_id) { + max_formats_.erase(usb_id); +} + +VideoCapturer* DeviceManager::CreateVideoCapturer(const Device& device) const { +#if defined(IOS) + LOG_F(LS_ERROR) << " should never be called!"; + return NULL; +#else + // TODO(hellner): Throw out the creation of a file video capturer once the + // refactoring is completed. + if (FileVideoCapturer::IsFileVideoCapturerDevice(device)) { + FileVideoCapturer* capturer = new FileVideoCapturer; + if (!capturer->Init(device)) { + delete capturer; + return NULL; + } + LOG(LS_INFO) << "Created file video capturer " << device.name; + capturer->set_repeat(talk_base::kForever); + return capturer; + } + VideoCapturer* capturer = device_video_capturer_factory_->Create(device); + if (!capturer) { + return NULL; + } + LOG(LS_INFO) << "Created VideoCapturer for " << device.name; + VideoFormat video_format; + bool has_max = GetMaxFormat(device, &video_format); + capturer->set_enable_camera_list(has_max); + if (has_max) { + capturer->ConstrainSupportedFormats(video_format); + } + return capturer; +#endif +} + +bool DeviceManager::GetWindows( + std::vector* descriptions) { + if (!window_picker_) { + return false; + } + return window_picker_->GetWindowList(descriptions); +} + +VideoCapturer* DeviceManager::CreateWindowCapturer(talk_base::WindowId window) { +#if defined(WINDOW_CAPTURER_NAME) + WINDOW_CAPTURER_NAME* window_capturer = new WINDOW_CAPTURER_NAME(); + if (!window_capturer->Init(window)) { + delete window_capturer; + return NULL; + } + return window_capturer; +#else + return NULL; +#endif +} + +bool DeviceManager::GetDesktops( + std::vector* descriptions) { + if (!window_picker_) { + return false; + } + return window_picker_->GetDesktopList(descriptions); +} + +VideoCapturer* DeviceManager::CreateDesktopCapturer( + talk_base::DesktopId desktop) { +#if defined(DESKTOP_CAPTURER_NAME) + DESKTOP_CAPTURER_NAME* desktop_capturer = new DESKTOP_CAPTURER_NAME(); + if (!desktop_capturer->Init(desktop.index())) { + delete desktop_capturer; + return NULL; + } + return desktop_capturer; +#else + return NULL; +#endif +} + +bool DeviceManager::GetAudioDevices(bool input, + std::vector* devs) { + devs->clear(); +#if defined(IOS) || defined(ANDROID) + // Under Android, we don't access the device file directly. + // Arbitrary use 0 for the mic and 1 for the output. + // These ids are used in MediaEngine::SetSoundDevices(in, out); + // The strings are for human consumption. + if (input) { + devs->push_back(Device("audiorecord", 0)); + } else { + devs->push_back(Device("audiotrack", 1)); + } + return true; +#else + return false; +#endif +} + +bool DeviceManager::GetAudioDevice(bool is_input, const std::string& name, + Device* out) { + // If the name is empty, return the default device id. + if (name.empty() || name == kDefaultDeviceName) { + *out = Device(name, -1); + return true; + } + + std::vector devices; + bool ret = is_input ? GetAudioInputDevices(&devices) : + GetAudioOutputDevices(&devices); + if (ret) { + ret = false; + for (size_t i = 0; i < devices.size(); ++i) { + if (devices[i].name == name) { + *out = devices[i]; + ret = true; + break; + } + } + } + return ret; +} + +bool DeviceManager::GetDefaultVideoCaptureDevice(Device* device) { + bool ret = false; + // We just return the first device. + std::vector devices; + ret = (GetVideoCaptureDevices(&devices) && !devices.empty()); + if (ret) { + *device = devices[0]; + } + return ret; +} + +bool DeviceManager::IsInWhitelist(const std::string& key, + VideoFormat* video_format) const { + std::map::const_iterator found = + std::search_n(max_formats_.begin(), max_formats_.end(), 1, key, + StringMatchWithWildcard); + if (found == max_formats_.end()) { + return false; + } + *video_format = found->second; + return true; +} + +bool DeviceManager::GetMaxFormat(const Device& device, + VideoFormat* video_format) const { + // Match USB ID if available. Failing that, match device name. + std::string usb_id; + if (GetUsbId(device, &usb_id) && IsInWhitelist(usb_id, video_format)) { + return true; + } + return IsInWhitelist(device.name, video_format); +} + +bool DeviceManager::ShouldDeviceBeIgnored(const std::string& device_name, + const char* const exclusion_list[]) { + // If exclusion_list is empty return directly. + if (!exclusion_list) + return false; + + int i = 0; + while (exclusion_list[i]) { + if (strnicmp(device_name.c_str(), exclusion_list[i], + strlen(exclusion_list[i])) == 0) { + LOG(LS_INFO) << "Ignoring device " << device_name; + return true; + } + ++i; + } + return false; +} + +bool DeviceManager::FilterDevices(std::vector* devices, + const char* const exclusion_list[]) { + if (!devices) { + return false; + } + + for (std::vector::iterator it = devices->begin(); + it != devices->end(); ) { + if (ShouldDeviceBeIgnored(it->name, exclusion_list)) { + it = devices->erase(it); + } else { + ++it; + } + } + return true; +} + +} // namespace cricket diff --git a/talk/media/devices/devicemanager.h b/talk/media/devices/devicemanager.h new file mode 100644 index 000000000..90da89157 --- /dev/null +++ b/talk/media/devices/devicemanager.h @@ -0,0 +1,214 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_DEVICES_DEVICEMANAGER_H_ +#define TALK_MEDIA_DEVICES_DEVICEMANAGER_H_ + +#include +#include +#include + +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" +#include "talk/base/stringencode.h" +#include "talk/base/window.h" +#include "talk/media/base/videocommon.h" + +namespace talk_base { + +class DesktopDescription; +class WindowDescription; +class WindowPicker; + +} +namespace cricket { + +class VideoCapturer; + +// Used to represent an audio or video capture or render device. +struct Device { + Device() {} + Device(const std::string& first, int second) + : name(first), + id(talk_base::ToString(second)) { + } + Device(const std::string& first, const std::string& second) + : name(first), id(second) {} + + std::string name; + std::string id; +}; + +class VideoCapturerFactory { + public: + VideoCapturerFactory() {} + virtual ~VideoCapturerFactory() {} + + virtual VideoCapturer* Create(const Device& device) = 0; +}; + +// DeviceManagerInterface - interface to manage the audio and +// video devices on the system. +class DeviceManagerInterface { + public: + virtual ~DeviceManagerInterface() { } + + // Initialization + virtual bool Init() = 0; + virtual void Terminate() = 0; + + // Capabilities + virtual int GetCapabilities() = 0; + + // Device enumeration + virtual bool GetAudioInputDevices(std::vector* devices) = 0; + virtual bool GetAudioOutputDevices(std::vector* devices) = 0; + + virtual bool GetAudioInputDevice(const std::string& name, Device* out) = 0; + virtual bool GetAudioOutputDevice(const std::string& name, Device* out) = 0; + + virtual bool GetVideoCaptureDevices(std::vector* devs) = 0; + virtual bool GetVideoCaptureDevice(const std::string& name, Device* out) = 0; + + // Caps the capture format according to max format for capturers created + // by CreateVideoCapturer(). See ConstrainSupportedFormats() in + // videocapturer.h for more detail. + // Note that once a VideoCapturer has been created, calling this API will + // not affect it. + virtual void SetVideoCaptureDeviceMaxFormat( + const std::string& usb_id, + const VideoFormat& max_format) = 0; + virtual void ClearVideoCaptureDeviceMaxFormat(const std::string& usb_id) = 0; + + // Device creation + virtual VideoCapturer* CreateVideoCapturer(const Device& device) const = 0; + + virtual bool GetWindows( + std::vector* descriptions) = 0; + virtual VideoCapturer* CreateWindowCapturer(talk_base::WindowId window) = 0; + + virtual bool GetDesktops( + std::vector* descriptions) = 0; + virtual VideoCapturer* CreateDesktopCapturer( + talk_base::DesktopId desktop) = 0; + + sigslot::signal0<> SignalDevicesChange; + + static const char kDefaultDeviceName[]; +}; + +class DeviceWatcher { + public: + explicit DeviceWatcher(DeviceManagerInterface* dm) {} + virtual ~DeviceWatcher() {} + virtual bool Start() { return true; } + virtual void Stop() {} +}; + +class DeviceManagerFactory { + public: + static DeviceManagerInterface* Create(); + + private: + DeviceManagerFactory() {} +}; + +class DeviceManager : public DeviceManagerInterface { + public: + DeviceManager(); + virtual ~DeviceManager(); + + void set_device_video_capturer_factory( + VideoCapturerFactory* device_video_capturer_factory) { + device_video_capturer_factory_.reset(device_video_capturer_factory); + } + + // Initialization + virtual bool Init(); + virtual void Terminate(); + + // Capabilities + virtual int GetCapabilities(); + + // Device enumeration + virtual bool GetAudioInputDevices(std::vector* devices); + virtual bool GetAudioOutputDevices(std::vector* devices); + + virtual bool GetAudioInputDevice(const std::string& name, Device* out); + virtual bool GetAudioOutputDevice(const std::string& name, Device* out); + + virtual bool GetVideoCaptureDevices(std::vector* devs); + virtual bool GetVideoCaptureDevice(const std::string& name, Device* out); + + virtual void SetVideoCaptureDeviceMaxFormat(const std::string& usb_id, + const VideoFormat& max_format); + virtual void ClearVideoCaptureDeviceMaxFormat(const std::string& usb_id); + + virtual VideoCapturer* CreateVideoCapturer(const Device& device) const; + + virtual bool GetWindows( + std::vector* descriptions); + virtual VideoCapturer* CreateWindowCapturer(talk_base::WindowId window); + + virtual bool GetDesktops( + std::vector* descriptions); + virtual VideoCapturer* CreateDesktopCapturer(talk_base::DesktopId desktop); + + // The exclusion_list MUST be a NULL terminated list. + static bool FilterDevices(std::vector* devices, + const char* const exclusion_list[]); + bool initialized() const { return initialized_; } + + protected: + virtual bool GetAudioDevices(bool input, std::vector* devs); + virtual bool GetAudioDevice(bool is_input, const std::string& name, + Device* out); + virtual bool GetDefaultVideoCaptureDevice(Device* device); + bool IsInWhitelist(const std::string& key, VideoFormat* video_format) const; + virtual bool GetMaxFormat(const Device& device, + VideoFormat* video_format) const; + + void set_initialized(bool initialized) { initialized_ = initialized; } + + void set_watcher(DeviceWatcher* watcher) { watcher_.reset(watcher); } + DeviceWatcher* watcher() { return watcher_.get(); } + + private: + // The exclusion_list MUST be a NULL terminated list. + static bool ShouldDeviceBeIgnored(const std::string& device_name, + const char* const exclusion_list[]); + + bool initialized_; + talk_base::scoped_ptr device_video_capturer_factory_; + std::map max_formats_; + talk_base::scoped_ptr watcher_; + talk_base::scoped_ptr window_picker_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_DEVICES_DEVICEMANAGER_H_ diff --git a/talk/media/devices/devicemanager_unittest.cc b/talk/media/devices/devicemanager_unittest.cc new file mode 100644 index 000000000..d8564eaaf --- /dev/null +++ b/talk/media/devices/devicemanager_unittest.cc @@ -0,0 +1,452 @@ +/* + * libjingle + * Copyright 2004 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/media/devices/devicemanager.h" + +#ifdef WIN32 +#include "talk/base/win32.h" +#include +#endif +#include + +#include "talk/base/fileutils.h" +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/pathutils.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stream.h" +#include "talk/base/windowpickerfactory.h" +#include "talk/media/base/fakevideocapturer.h" +#include "talk/media/base/testutils.h" +#include "talk/media/devices/filevideocapturer.h" +#include "talk/media/devices/v4llookup.h" + +#ifdef LINUX +// TODO(juberti): Figure out why this doesn't compile on Windows. +#include "talk/base/fileutils_mock.h" +#endif // LINUX + +using talk_base::Pathname; +using talk_base::FileTimeType; +using talk_base::scoped_ptr; +using cricket::Device; +using cricket::DeviceManager; +using cricket::DeviceManagerFactory; +using cricket::DeviceManagerInterface; + +const cricket::VideoFormat kVgaFormat(640, 480, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420); +const cricket::VideoFormat kHdFormat(1280, 720, + cricket::VideoFormat::FpsToInterval(30), + cricket::FOURCC_I420); + +class FakeVideoCapturerFactory : public cricket::VideoCapturerFactory { + public: + FakeVideoCapturerFactory() {} + virtual ~FakeVideoCapturerFactory() {} + + virtual cricket::VideoCapturer* Create(const cricket::Device& device) { + return new cricket::FakeVideoCapturer; + } +}; + +class DeviceManagerTestFake : public testing::Test { + public: + virtual void SetUp() { + dm_.reset(DeviceManagerFactory::Create()); + EXPECT_TRUE(dm_->Init()); + DeviceManager* device_manager = static_cast(dm_.get()); + device_manager->set_device_video_capturer_factory( + new FakeVideoCapturerFactory()); + } + + virtual void TearDown() { + dm_->Terminate(); + } + + protected: + scoped_ptr dm_; +}; + + +// Test that we startup/shutdown properly. +TEST(DeviceManagerTest, StartupShutdown) { + scoped_ptr dm(DeviceManagerFactory::Create()); + EXPECT_TRUE(dm->Init()); + dm->Terminate(); +} + +// Test CoInitEx behavior +#ifdef WIN32 +TEST(DeviceManagerTest, CoInitialize) { + scoped_ptr dm(DeviceManagerFactory::Create()); + std::vector devices; + // Ensure that calls to video device work if COM is not yet initialized. + EXPECT_TRUE(dm->Init()); + EXPECT_TRUE(dm->GetVideoCaptureDevices(&devices)); + dm->Terminate(); + // Ensure that the ref count is correct. + EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_MULTITHREADED)); + CoUninitialize(); + // Ensure that Init works in COINIT_APARTMENTTHREADED setting. + EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)); + EXPECT_TRUE(dm->Init()); + dm->Terminate(); + CoUninitialize(); + // Ensure that the ref count is correct. + EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)); + CoUninitialize(); + // Ensure that Init works in COINIT_MULTITHREADED setting. + EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_MULTITHREADED)); + EXPECT_TRUE(dm->Init()); + dm->Terminate(); + CoUninitialize(); + // Ensure that the ref count is correct. + EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_MULTITHREADED)); + CoUninitialize(); +} +#endif + +// Test enumerating devices (although we may not find any). +TEST(DeviceManagerTest, GetDevices) { + scoped_ptr dm(DeviceManagerFactory::Create()); + std::vector audio_ins, audio_outs, video_ins; + std::vector video_in_devs; + cricket::Device def_video; + EXPECT_TRUE(dm->Init()); + EXPECT_TRUE(dm->GetAudioInputDevices(&audio_ins)); + EXPECT_TRUE(dm->GetAudioOutputDevices(&audio_outs)); + EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_ins)); + EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_in_devs)); + EXPECT_EQ(video_ins.size(), video_in_devs.size()); + // If we have any video devices, we should be able to pick a default. + EXPECT_TRUE(dm->GetVideoCaptureDevice( + cricket::DeviceManagerInterface::kDefaultDeviceName, &def_video) + != video_ins.empty()); +} + +// Test that we return correct ids for default and bogus devices. +TEST(DeviceManagerTest, GetAudioDeviceIds) { + scoped_ptr dm(DeviceManagerFactory::Create()); + Device device; + EXPECT_TRUE(dm->Init()); + EXPECT_TRUE(dm->GetAudioInputDevice( + cricket::DeviceManagerInterface::kDefaultDeviceName, &device)); + EXPECT_EQ("-1", device.id); + EXPECT_TRUE(dm->GetAudioOutputDevice( + cricket::DeviceManagerInterface::kDefaultDeviceName, &device)); + EXPECT_EQ("-1", device.id); + EXPECT_FALSE(dm->GetAudioInputDevice("_NOT A REAL DEVICE_", &device)); + EXPECT_FALSE(dm->GetAudioOutputDevice("_NOT A REAL DEVICE_", &device)); +} + +// Test that we get the video capture device by name properly. +TEST(DeviceManagerTest, GetVideoDeviceIds) { + scoped_ptr dm(DeviceManagerFactory::Create()); + Device device; + EXPECT_TRUE(dm->Init()); + EXPECT_FALSE(dm->GetVideoCaptureDevice("_NOT A REAL DEVICE_", &device)); + std::vector video_ins; + EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_ins)); + if (!video_ins.empty()) { + // Get the default device with the parameter kDefaultDeviceName. + EXPECT_TRUE(dm->GetVideoCaptureDevice( + cricket::DeviceManagerInterface::kDefaultDeviceName, &device)); + + // Get the first device with the parameter video_ins[0].name. + EXPECT_TRUE(dm->GetVideoCaptureDevice(video_ins[0].name, &device)); + EXPECT_EQ(device.name, video_ins[0].name); + EXPECT_EQ(device.id, video_ins[0].id); + } +} + +TEST(DeviceManagerTest, GetVideoDeviceIds_File) { + scoped_ptr dm(DeviceManagerFactory::Create()); + EXPECT_TRUE(dm->Init()); + Device device; + const std::string test_file = + cricket::GetTestFilePath("captured-320x240-2s-48.frames"); + EXPECT_TRUE(dm->GetVideoCaptureDevice(test_file, &device)); + EXPECT_TRUE(cricket::FileVideoCapturer::IsFileVideoCapturerDevice(device)); +} + +TEST(DeviceManagerTest, VerifyDevicesListsAreCleared) { + const std::string imaginary("_NOT A REAL DEVICE_"); + scoped_ptr dm(DeviceManagerFactory::Create()); + std::vector audio_ins, audio_outs, video_ins; + audio_ins.push_back(Device(imaginary, imaginary)); + audio_outs.push_back(Device(imaginary, imaginary)); + video_ins.push_back(Device(imaginary, imaginary)); + EXPECT_TRUE(dm->Init()); + EXPECT_TRUE(dm->GetAudioInputDevices(&audio_ins)); + EXPECT_TRUE(dm->GetAudioOutputDevices(&audio_outs)); + EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_ins)); + for (size_t i = 0; i < audio_ins.size(); ++i) { + EXPECT_NE(imaginary, audio_ins[i].name); + } + for (size_t i = 0; i < audio_outs.size(); ++i) { + EXPECT_NE(imaginary, audio_outs[i].name); + } + for (size_t i = 0; i < video_ins.size(); ++i) { + EXPECT_NE(imaginary, video_ins[i].name); + } +} + +static bool CompareDeviceList(std::vector& devices, + const char* const device_list[], int list_size) { + if (list_size != static_cast(devices.size())) { + return false; + } + for (int i = 0; i < list_size; ++i) { + if (devices[i].name.compare(device_list[i]) != 0) { + return false; + } + } + return true; +} + +TEST(DeviceManagerTest, VerifyFilterDevices) { + static const char* const kTotalDevicesName[] = { + "Google Camera Adapters are tons of fun.", + "device1", + "device2", + "device3", + "device4", + "device5", + "Google Camera Adapter 0", + "Google Camera Adapter 1", + }; + static const char* const kFilteredDevicesName[] = { + "device2", + "device4", + "Google Camera Adapter", + NULL, + }; + static const char* const kDevicesName[] = { + "device1", + "device3", + "device5", + }; + std::vector devices; + for (int i = 0; i < ARRAY_SIZE(kTotalDevicesName); ++i) { + devices.push_back(Device(kTotalDevicesName[i], i)); + } + EXPECT_TRUE(CompareDeviceList(devices, kTotalDevicesName, + ARRAY_SIZE(kTotalDevicesName))); + // Return false if given NULL as the exclusion list. + EXPECT_TRUE(DeviceManager::FilterDevices(&devices, NULL)); + // The devices should not change. + EXPECT_TRUE(CompareDeviceList(devices, kTotalDevicesName, + ARRAY_SIZE(kTotalDevicesName))); + EXPECT_TRUE(DeviceManager::FilterDevices(&devices, kFilteredDevicesName)); + EXPECT_TRUE(CompareDeviceList(devices, kDevicesName, + ARRAY_SIZE(kDevicesName))); +} + +#ifdef LINUX +class FakeV4LLookup : public cricket::V4LLookup { + public: + explicit FakeV4LLookup(std::vector device_paths) + : device_paths_(device_paths) {} + + protected: + bool CheckIsV4L2Device(const std::string& device) { + return std::find(device_paths_.begin(), device_paths_.end(), device) + != device_paths_.end(); + } + + private: + std::vector device_paths_; +}; + +TEST(DeviceManagerTest, GetVideoCaptureDevices_K2_6) { + std::vector devices; + devices.push_back("/dev/video0"); + devices.push_back("/dev/video5"); + cricket::V4LLookup::SetV4LLookup(new FakeV4LLookup(devices)); + + std::vector files; + files.push_back(talk_base::FakeFileSystem::File("/dev/video0", "")); + files.push_back(talk_base::FakeFileSystem::File("/dev/video5", "")); + files.push_back(talk_base::FakeFileSystem::File( + "/sys/class/video4linux/video0/name", "Video Device 1")); + files.push_back(talk_base::FakeFileSystem::File( + "/sys/class/video4linux/video1/model", "Bad Device")); + files.push_back( + talk_base::FakeFileSystem::File("/sys/class/video4linux/video5/model", + "Video Device 2")); + talk_base::FilesystemScope fs(new talk_base::FakeFileSystem(files)); + + scoped_ptr dm(DeviceManagerFactory::Create()); + std::vector video_ins; + EXPECT_TRUE(dm->Init()); + EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_ins)); + EXPECT_EQ(2u, video_ins.size()); + EXPECT_EQ("Video Device 1", video_ins.at(0).name); + EXPECT_EQ("Video Device 2", video_ins.at(1).name); +} + +TEST(DeviceManagerTest, GetVideoCaptureDevices_K2_4) { + std::vector devices; + devices.push_back("/dev/video0"); + devices.push_back("/dev/video5"); + cricket::V4LLookup::SetV4LLookup(new FakeV4LLookup(devices)); + + std::vector files; + files.push_back(talk_base::FakeFileSystem::File("/dev/video0", "")); + files.push_back(talk_base::FakeFileSystem::File("/dev/video5", "")); + files.push_back(talk_base::FakeFileSystem::File( + "/proc/video/dev/video0", + "param1: value1\nname: Video Device 1\n param2: value2\n")); + files.push_back(talk_base::FakeFileSystem::File( + "/proc/video/dev/video1", + "param1: value1\nname: Bad Device\n param2: value2\n")); + files.push_back(talk_base::FakeFileSystem::File( + "/proc/video/dev/video5", + "param1: value1\nname: Video Device 2\n param2: value2\n")); + talk_base::FilesystemScope fs(new talk_base::FakeFileSystem(files)); + + scoped_ptr dm(DeviceManagerFactory::Create()); + std::vector video_ins; + EXPECT_TRUE(dm->Init()); + EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_ins)); + EXPECT_EQ(2u, video_ins.size()); + EXPECT_EQ("Video Device 1", video_ins.at(0).name); + EXPECT_EQ("Video Device 2", video_ins.at(1).name); +} + +TEST(DeviceManagerTest, GetVideoCaptureDevices_KUnknown) { + std::vector devices; + devices.push_back("/dev/video0"); + devices.push_back("/dev/video5"); + cricket::V4LLookup::SetV4LLookup(new FakeV4LLookup(devices)); + + std::vector files; + files.push_back(talk_base::FakeFileSystem::File("/dev/video0", "")); + files.push_back(talk_base::FakeFileSystem::File("/dev/video1", "")); + files.push_back(talk_base::FakeFileSystem::File("/dev/video5", "")); + talk_base::FilesystemScope fs(new talk_base::FakeFileSystem(files)); + + scoped_ptr dm(DeviceManagerFactory::Create()); + std::vector video_ins; + EXPECT_TRUE(dm->Init()); + EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_ins)); + EXPECT_EQ(2u, video_ins.size()); + EXPECT_EQ("/dev/video0", video_ins.at(0).name); + EXPECT_EQ("/dev/video5", video_ins.at(1).name); +} +#endif // LINUX + +// TODO(noahric): These are flaky on windows on headless machines. +#ifndef WIN32 +TEST(DeviceManagerTest, GetWindows) { + if (!talk_base::WindowPickerFactory::IsSupported()) { + LOG(LS_INFO) << "skipping test: window capturing is not supported with " + << "current configuration."; + return; + } + scoped_ptr dm(DeviceManagerFactory::Create()); + std::vector descriptions; + EXPECT_TRUE(dm->Init()); + if (!dm->GetWindows(&descriptions) || descriptions.empty()) { + LOG(LS_INFO) << "skipping test: window capturing. Does not have any " + << "windows to capture."; + return; + } + scoped_ptr capturer(dm->CreateWindowCapturer( + descriptions.front().id())); + EXPECT_FALSE(capturer.get() == NULL); + // TODO(hellner): creating a window capturer and immediately deleting it + // results in "Continuous Build and Test Mainline - Mac opt" failure (crash). + // Remove the following line as soon as this has been resolved. + talk_base::Thread::Current()->ProcessMessages(1); +} + +TEST(DeviceManagerTest, GetDesktops) { + if (!talk_base::WindowPickerFactory::IsSupported()) { + LOG(LS_INFO) << "skipping test: desktop capturing is not supported with " + << "current configuration."; + return; + } + scoped_ptr dm(DeviceManagerFactory::Create()); + std::vector descriptions; + EXPECT_TRUE(dm->Init()); + if (!dm->GetDesktops(&descriptions) || descriptions.empty()) { + LOG(LS_INFO) << "skipping test: desktop capturing. Does not have any " + << "desktops to capture."; + return; + } + scoped_ptr capturer(dm->CreateDesktopCapturer( + descriptions.front().id())); + EXPECT_FALSE(capturer.get() == NULL); +} +#endif // !WIN32 + +TEST_F(DeviceManagerTestFake, CaptureConstraintsWhitelisted) { + const Device device("white", "white_id"); + dm_->SetVideoCaptureDeviceMaxFormat(device.name, kHdFormat); + scoped_ptr capturer( + dm_->CreateVideoCapturer(device)); + cricket::VideoFormat best_format; + capturer->set_enable_camera_list(true); + EXPECT_TRUE(capturer->GetBestCaptureFormat(kHdFormat, &best_format)); + EXPECT_EQ(kHdFormat, best_format); +} + +TEST_F(DeviceManagerTestFake, CaptureConstraintsNotWhitelisted) { + const Device device("regular", "regular_id"); + scoped_ptr capturer( + dm_->CreateVideoCapturer(device)); + cricket::VideoFormat best_format; + capturer->set_enable_camera_list(true); + EXPECT_TRUE(capturer->GetBestCaptureFormat(kHdFormat, &best_format)); + EXPECT_EQ(kHdFormat, best_format); +} + +TEST_F(DeviceManagerTestFake, CaptureConstraintsUnWhitelisted) { + const Device device("un_white", "un_white_id"); + dm_->SetVideoCaptureDeviceMaxFormat(device.name, kHdFormat); + dm_->ClearVideoCaptureDeviceMaxFormat(device.name); + scoped_ptr capturer( + dm_->CreateVideoCapturer(device)); + cricket::VideoFormat best_format; + capturer->set_enable_camera_list(true); + EXPECT_TRUE(capturer->GetBestCaptureFormat(kHdFormat, &best_format)); + EXPECT_EQ(kHdFormat, best_format); +} + +TEST_F(DeviceManagerTestFake, CaptureConstraintsWildcard) { + const Device device("any_device", "any_device"); + dm_->SetVideoCaptureDeviceMaxFormat("*", kHdFormat); + scoped_ptr capturer( + dm_->CreateVideoCapturer(device)); + cricket::VideoFormat best_format; + capturer->set_enable_camera_list(true); + EXPECT_TRUE(capturer->GetBestCaptureFormat(kHdFormat, &best_format)); + EXPECT_EQ(kHdFormat, best_format); +} diff --git a/talk/media/devices/dummydevicemanager.cc b/talk/media/devices/dummydevicemanager.cc new file mode 100644 index 000000000..736258b75 --- /dev/null +++ b/talk/media/devices/dummydevicemanager.cc @@ -0,0 +1,38 @@ +/* + * libjingle + * Copyright 2004 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/media/devices/dummydevicemanager.h" + +namespace cricket { + +const char DeviceManagerInterface::kDefaultDeviceName[] = ""; + +DeviceManagerInterface* DeviceManagerFactory::Create() { + return new DummyDeviceManager(); +} + +}; // namespace cricket diff --git a/talk/media/devices/dummydevicemanager.h b/talk/media/devices/dummydevicemanager.h new file mode 100644 index 000000000..7da81853c --- /dev/null +++ b/talk/media/devices/dummydevicemanager.h @@ -0,0 +1,51 @@ +/* + * libjingle + * Copyright 2011 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. + */ + +#ifndef TALK_MEDIA_DEVICES_DUMMYDEVICEMANAGER_H_ +#define TALK_MEDIA_DEVICES_DUMMYDEVICEMANAGER_H_ + +#include + +#include "talk/media/base/mediacommon.h" +#include "talk/media/devices/fakedevicemanager.h" + +namespace cricket { + +class DummyDeviceManager : public FakeDeviceManager { + public: + DummyDeviceManager() { + std::vector devices; + devices.push_back(DeviceManagerInterface::kDefaultDeviceName); + SetAudioInputDevices(devices); + SetAudioOutputDevices(devices); + SetVideoCaptureDevices(devices); + } +}; + +} // namespace cricket + +#endif // TALK_MEDIA_DEVICES_DUMMYDEVICEMANAGER_H_ diff --git a/talk/media/devices/dummydevicemanager_unittest.cc b/talk/media/devices/dummydevicemanager_unittest.cc new file mode 100644 index 000000000..1abf1ea7a --- /dev/null +++ b/talk/media/devices/dummydevicemanager_unittest.cc @@ -0,0 +1,104 @@ +/* + * libjingle + * Copyright 2004 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/base/gunit.h" +#include "talk/media/devices/dummydevicemanager.h" + +using cricket::Device; +using cricket::DummyDeviceManager; + +// Test that we startup/shutdown properly. +TEST(DummyDeviceManagerTest, StartupShutdown) { + DummyDeviceManager dm; + EXPECT_TRUE(dm.Init()); + dm.Terminate(); +} + +// Test enumerating capabilities. +TEST(DummyDeviceManagerTest, GetCapabilities) { + DummyDeviceManager dm; + int capabilities = dm.GetCapabilities(); + EXPECT_EQ((cricket::AUDIO_SEND | cricket::AUDIO_RECV | + cricket::VIDEO_SEND | cricket::VIDEO_RECV), capabilities); +} + +// Test enumerating devices. +TEST(DummyDeviceManagerTest, GetDevices) { + DummyDeviceManager dm; + EXPECT_TRUE(dm.Init()); + std::vector audio_ins, audio_outs, video_ins; + EXPECT_TRUE(dm.GetAudioInputDevices(&audio_ins)); + EXPECT_TRUE(dm.GetAudioOutputDevices(&audio_outs)); + EXPECT_TRUE(dm.GetVideoCaptureDevices(&video_ins)); +} + +// Test that we return correct ids for default and bogus devices. +TEST(DummyDeviceManagerTest, GetAudioDeviceIds) { + DummyDeviceManager dm; + Device device; + EXPECT_TRUE(dm.Init()); + EXPECT_TRUE(dm.GetAudioInputDevice( + cricket::DeviceManagerInterface::kDefaultDeviceName, &device)); + EXPECT_EQ("-1", device.id); + EXPECT_TRUE(dm.GetAudioOutputDevice( + cricket::DeviceManagerInterface::kDefaultDeviceName, &device)); + EXPECT_EQ("-1", device.id); + EXPECT_FALSE(dm.GetAudioInputDevice("_NOT A REAL DEVICE_", &device)); + EXPECT_FALSE(dm.GetAudioOutputDevice("_NOT A REAL DEVICE_", &device)); +} + +// Test that we get the video capture device by name properly. +TEST(DummyDeviceManagerTest, GetVideoDeviceIds) { + DummyDeviceManager dm; + Device device; + EXPECT_TRUE(dm.Init()); + EXPECT_FALSE(dm.GetVideoCaptureDevice("_NOT A REAL DEVICE_", &device)); + EXPECT_TRUE(dm.GetVideoCaptureDevice( + cricket::DeviceManagerInterface::kDefaultDeviceName, &device)); +} + +TEST(DummyDeviceManagerTest, VerifyDevicesListsAreCleared) { + const std::string imaginary("_NOT A REAL DEVICE_"); + DummyDeviceManager dm; + std::vector audio_ins, audio_outs, video_ins; + audio_ins.push_back(Device(imaginary, imaginary)); + audio_outs.push_back(Device(imaginary, imaginary)); + video_ins.push_back(Device(imaginary, imaginary)); + EXPECT_TRUE(dm.Init()); + EXPECT_TRUE(dm.GetAudioInputDevices(&audio_ins)); + EXPECT_TRUE(dm.GetAudioOutputDevices(&audio_outs)); + EXPECT_TRUE(dm.GetVideoCaptureDevices(&video_ins)); + for (size_t i = 0; i < audio_ins.size(); ++i) { + EXPECT_NE(imaginary, audio_ins[i].name); + } + for (size_t i = 0; i < audio_outs.size(); ++i) { + EXPECT_NE(imaginary, audio_outs[i].name); + } + for (size_t i = 0; i < video_ins.size(); ++i) { + EXPECT_NE(imaginary, video_ins[i].name); + } +} diff --git a/talk/media/devices/fakedevicemanager.h b/talk/media/devices/fakedevicemanager.h new file mode 100644 index 000000000..7000ef9ea --- /dev/null +++ b/talk/media/devices/fakedevicemanager.h @@ -0,0 +1,219 @@ +/* + * libjingle + * Copyright 2008 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. + */ + +#ifndef TALK_MEDIA_DEVICES_FAKEDEVICEMANAGER_H_ +#define TALK_MEDIA_DEVICES_FAKEDEVICEMANAGER_H_ + +#include +#include + +#include "talk/base/window.h" +#include "talk/base/windowpicker.h" +#include "talk/media/base/fakevideocapturer.h" +#include "talk/media/base/mediacommon.h" +#include "talk/media/devices/devicemanager.h" + +namespace cricket { + +class FakeDeviceManager : public DeviceManagerInterface { + public: + FakeDeviceManager() {} + virtual bool Init() { + return true; + } + virtual void Terminate() { + } + virtual int GetCapabilities() { + std::vector devices; + int caps = VIDEO_RECV; + if (!input_devices_.empty()) { + caps |= AUDIO_SEND; + } + if (!output_devices_.empty()) { + caps |= AUDIO_RECV; + } + if (!vidcap_devices_.empty()) { + caps |= VIDEO_SEND; + } + return caps; + } + virtual bool GetAudioInputDevices(std::vector* devs) { + *devs = input_devices_; + return true; + } + virtual bool GetAudioOutputDevices(std::vector* devs) { + *devs = output_devices_; + return true; + } + virtual bool GetAudioInputDevice(const std::string& name, Device* out) { + return GetAudioDevice(true, name, out); + } + virtual bool GetAudioOutputDevice(const std::string& name, Device* out) { + return GetAudioDevice(false, name, out); + } + virtual bool GetVideoCaptureDevices(std::vector* devs) { + *devs = vidcap_devices_; + return true; + } + virtual void SetVideoCaptureDeviceMaxFormat(const std::string& usb_id, + const VideoFormat& max_format) { + max_formats_[usb_id] = max_format; + } + bool IsMaxFormatForDevice(const std::string& usb_id, + const VideoFormat& max_format) const { + std::map::const_iterator found = + max_formats_.find(usb_id); + return (found != max_formats_.end()) ? + max_format == found->second : + false; + } + virtual void ClearVideoCaptureDeviceMaxFormat(const std::string& usb_id) { + max_formats_.erase(usb_id); + } + virtual VideoCapturer* CreateVideoCapturer(const Device& device) const { + return new FakeVideoCapturer(); + } + virtual bool GetWindows( + std::vector* descriptions) { + descriptions->clear(); + const uint32_t id = 1u; // Note that 0 is not a valid ID. + const talk_base::WindowId window_id = + talk_base::WindowId::Cast(id); + std::string title = "FakeWindow"; + talk_base::WindowDescription window_description(window_id, title); + descriptions->push_back(window_description); + return true; + } + virtual VideoCapturer* CreateWindowCapturer(talk_base::WindowId window) { + if (!window.IsValid()) { + return NULL; + } + return new FakeVideoCapturer; + } + virtual bool GetDesktops( + std::vector* descriptions) { + descriptions->clear(); + const int id = 0; + const int valid_index = 0; + const talk_base::DesktopId desktop_id = + talk_base::DesktopId::Cast(id, valid_index); + std::string title = "FakeDesktop"; + talk_base::DesktopDescription desktop_description(desktop_id, title); + descriptions->push_back(desktop_description); + return true; + } + virtual VideoCapturer* CreateDesktopCapturer(talk_base::DesktopId desktop) { + if (!desktop.IsValid()) { + return NULL; + } + return new FakeVideoCapturer; + } + + virtual bool GetDefaultVideoCaptureDevice(Device* device) { + if (vidcap_devices_.empty()) { + return false; + } + *device = vidcap_devices_[0]; + return true; + } + +#ifdef OSX + bool QtKitToSgDevice(const std::string& qtkit_name, Device* out) { + out->name = qtkit_name; + out->id = "sg:" + qtkit_name; + return true; + } +#endif + + void SetAudioInputDevices(const std::vector& devices) { + input_devices_.clear(); + for (size_t i = 0; i < devices.size(); ++i) { + input_devices_.push_back(Device(devices[i], i)); + } + SignalDevicesChange(); + } + void SetAudioOutputDevices(const std::vector& devices) { + output_devices_.clear(); + for (size_t i = 0; i < devices.size(); ++i) { + output_devices_.push_back(Device(devices[i], i)); + } + SignalDevicesChange(); + } + void SetVideoCaptureDevices(const std::vector& devices) { + vidcap_devices_.clear(); + for (size_t i = 0; i < devices.size(); ++i) { + vidcap_devices_.push_back(Device(devices[i], i)); + } + SignalDevicesChange(); + } + virtual bool GetVideoCaptureDevice(const std::string& name, + Device* out) { + if (vidcap_devices_.empty()) + return false; + + // If the name is empty, return the default device. + if (name.empty() || name == kDefaultDeviceName) { + *out = vidcap_devices_[0]; + return true; + } + + return FindDeviceByName(vidcap_devices_, name, out); + } + bool GetAudioDevice(bool is_input, const std::string& name, + Device* out) { + // If the name is empty, return the default device. + if (name.empty() || name == kDefaultDeviceName) { + *out = Device(name, -1); + return true; + } + + return FindDeviceByName((is_input ? input_devices_ : output_devices_), + name, out); + } + static bool FindDeviceByName(const std::vector& devices, + const std::string& name, + Device* out) { + for (std::vector::const_iterator it = devices.begin(); + it != devices.end(); ++it) { + if (name == it->name) { + *out = *it; + return true; + } + } + return false; + } + + private: + std::vector input_devices_; + std::vector output_devices_; + std::vector vidcap_devices_; + std::map max_formats_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_DEVICES_FAKEDEVICEMANAGER_H_ diff --git a/talk/media/devices/filevideocapturer.cc b/talk/media/devices/filevideocapturer.cc new file mode 100644 index 000000000..8946fea77 --- /dev/null +++ b/talk/media/devices/filevideocapturer.cc @@ -0,0 +1,366 @@ +// libjingle +// Copyright 2004 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. +// +// Implementation of VideoRecorder and FileVideoCapturer. + +#include "talk/media/devices/filevideocapturer.h" + +#include "talk/base/bytebuffer.h" +#include "talk/base/logging.h" +#include "talk/base/thread.h" + +namespace cricket { + +///////////////////////////////////////////////////////////////////// +// Implementation of class VideoRecorder +///////////////////////////////////////////////////////////////////// +bool VideoRecorder::Start(const std::string& filename, bool write_header) { + Stop(); + write_header_ = write_header; + int err; + if (!video_file_.Open(filename, "wb", &err)) { + LOG(LS_ERROR) << "Unable to open file " << filename << " err=" << err; + return false; + } + return true; +} + +void VideoRecorder::Stop() { + video_file_.Close(); +} + +bool VideoRecorder::RecordFrame(const CapturedFrame& frame) { + if (talk_base::SS_CLOSED == video_file_.GetState()) { + LOG(LS_ERROR) << "File not opened yet"; + return false; + } + + uint32 size = 0; + if (!frame.GetDataSize(&size)) { + LOG(LS_ERROR) << "Unable to calculate the data size of the frame"; + return false; + } + + if (write_header_) { + // Convert the frame header to bytebuffer. + talk_base::ByteBuffer buffer; + buffer.WriteUInt32(frame.width); + buffer.WriteUInt32(frame.height); + buffer.WriteUInt32(frame.fourcc); + buffer.WriteUInt32(frame.pixel_width); + buffer.WriteUInt32(frame.pixel_height); + buffer.WriteUInt64(frame.elapsed_time); + buffer.WriteUInt64(frame.time_stamp); + buffer.WriteUInt32(size); + + // Write the bytebuffer to file. + if (talk_base::SR_SUCCESS != video_file_.Write(buffer.Data(), + buffer.Length(), + NULL, + NULL)) { + LOG(LS_ERROR) << "Failed to write frame header"; + return false; + } + } + // Write the frame data to file. + if (talk_base::SR_SUCCESS != video_file_.Write(frame.data, + size, + NULL, + NULL)) { + LOG(LS_ERROR) << "Failed to write frame data"; + return false; + } + + return true; +} + +/////////////////////////////////////////////////////////////////////// +// Definition of private class FileReadThread that periodically reads +// frames from a file. +/////////////////////////////////////////////////////////////////////// +class FileVideoCapturer::FileReadThread + : public talk_base::Thread, public talk_base::MessageHandler { + public: + explicit FileReadThread(FileVideoCapturer* capturer) + : capturer_(capturer), + finished_(false) { + } + + // Override virtual method of parent Thread. Context: Worker Thread. + virtual void Run() { + // Read the first frame and start the message pump. The pump runs until + // Stop() is called externally or Quit() is called by OnMessage(). + int waiting_time_ms = 0; + if (capturer_ && capturer_->ReadFrame(true, &waiting_time_ms)) { + PostDelayed(waiting_time_ms, this); + Thread::Run(); + } + finished_ = true; + } + + // Override virtual method of parent MessageHandler. Context: Worker Thread. + virtual void OnMessage(talk_base::Message* /*pmsg*/) { + int waiting_time_ms = 0; + if (capturer_ && capturer_->ReadFrame(false, &waiting_time_ms)) { + PostDelayed(waiting_time_ms, this); + } else { + Quit(); + } + } + + // Check if Run() is finished. + bool Finished() const { return finished_; } + + private: + FileVideoCapturer* capturer_; + bool finished_; + + DISALLOW_COPY_AND_ASSIGN(FileReadThread); +}; + +///////////////////////////////////////////////////////////////////// +// Implementation of class FileVideoCapturer +///////////////////////////////////////////////////////////////////// +static const int64 kNumNanoSecsPerMilliSec = 1000000; +const char* FileVideoCapturer::kVideoFileDevicePrefix = "video-file:"; + +FileVideoCapturer::FileVideoCapturer() + : frame_buffer_size_(0), + file_read_thread_(NULL), + repeat_(0), + start_time_ns_(0), + last_frame_timestamp_ns_(0), + ignore_framerate_(false) { +} + +FileVideoCapturer::~FileVideoCapturer() { + Stop(); + delete[] static_cast (captured_frame_.data); +} + +bool FileVideoCapturer::Init(const Device& device) { + if (!FileVideoCapturer::IsFileVideoCapturerDevice(device)) { + return false; + } + std::string filename(device.name); + if (IsRunning()) { + LOG(LS_ERROR) << "The file video capturer is already running"; + return false; + } + // Open the file. + int err; + if (!video_file_.Open(filename, "rb", &err)) { + LOG(LS_ERROR) << "Unable to open the file " << filename << " err=" << err; + return false; + } + // Read the first frame's header to determine the supported format. + CapturedFrame frame; + if (talk_base::SR_SUCCESS != ReadFrameHeader(&frame)) { + LOG(LS_ERROR) << "Failed to read the first frame header"; + video_file_.Close(); + return false; + } + // Seek back to the start of the file. + if (!video_file_.SetPosition(0)) { + LOG(LS_ERROR) << "Failed to seek back to beginning of the file"; + video_file_.Close(); + return false; + } + + // Enumerate the supported formats. We have only one supported format. We set + // the frame interval to kMinimumInterval here. In Start(), if the capture + // format's interval is greater than kMinimumInterval, we use the interval; + // otherwise, we use the timestamp in the file to control the interval. + VideoFormat format(frame.width, frame.height, VideoFormat::kMinimumInterval, + frame.fourcc); + std::vector supported; + supported.push_back(format); + + SetId(device.id); + SetSupportedFormats(supported); + return true; +} + +bool FileVideoCapturer::Init(const std::string& filename) { + return Init(FileVideoCapturer::CreateFileVideoCapturerDevice(filename)); +} + +CaptureState FileVideoCapturer::Start(const VideoFormat& capture_format) { + if (IsRunning()) { + LOG(LS_ERROR) << "The file video capturer is already running"; + return CS_FAILED; + } + + if (talk_base::SS_CLOSED == video_file_.GetState()) { + LOG(LS_ERROR) << "File not opened yet"; + return CS_NO_DEVICE; + } else if (!video_file_.SetPosition(0)) { + LOG(LS_ERROR) << "Failed to seek back to beginning of the file"; + return CS_FAILED; + } + + SetCaptureFormat(&capture_format); + // Create a thread to read the file. + file_read_thread_ = new FileReadThread(this); + bool ret = file_read_thread_->Start(); + start_time_ns_ = kNumNanoSecsPerMilliSec * + static_cast(talk_base::Time()); + if (ret) { + LOG(LS_INFO) << "File video capturer '" << GetId() << "' started"; + return CS_RUNNING; + } else { + LOG(LS_ERROR) << "File video capturer '" << GetId() << "' failed to start"; + return CS_FAILED; + } +} + +bool FileVideoCapturer::IsRunning() { + return file_read_thread_ && !file_read_thread_->Finished(); +} + +void FileVideoCapturer::Stop() { + if (file_read_thread_) { + file_read_thread_->Stop(); + file_read_thread_ = NULL; + LOG(LS_INFO) << "File video capturer '" << GetId() << "' stopped"; + } + SetCaptureFormat(NULL); +} + +bool FileVideoCapturer::GetPreferredFourccs(std::vector* fourccs) { + if (!fourccs) { + return false; + } + + fourccs->push_back(GetSupportedFormats()->at(0).fourcc); + return true; +} + +talk_base::StreamResult FileVideoCapturer::ReadFrameHeader( + CapturedFrame* frame) { + // We first read kFrameHeaderSize bytes from the file stream to a memory + // buffer, then construct a bytebuffer from the memory buffer, and finally + // read the frame header from the bytebuffer. + char header[CapturedFrame::kFrameHeaderSize]; + talk_base::StreamResult sr; + size_t bytes_read; + int error; + sr = video_file_.Read(header, + CapturedFrame::kFrameHeaderSize, + &bytes_read, + &error); + LOG(LS_VERBOSE) << "Read frame header: stream_result = " << sr + << ", bytes read = " << bytes_read << ", error = " << error; + if (talk_base::SR_SUCCESS == sr) { + if (CapturedFrame::kFrameHeaderSize != bytes_read) { + return talk_base::SR_EOS; + } + talk_base::ByteBuffer buffer(header, CapturedFrame::kFrameHeaderSize); + buffer.ReadUInt32(reinterpret_cast(&frame->width)); + buffer.ReadUInt32(reinterpret_cast(&frame->height)); + buffer.ReadUInt32(&frame->fourcc); + buffer.ReadUInt32(&frame->pixel_width); + buffer.ReadUInt32(&frame->pixel_height); + buffer.ReadUInt64(reinterpret_cast(&frame->elapsed_time)); + buffer.ReadUInt64(reinterpret_cast(&frame->time_stamp)); + buffer.ReadUInt32(&frame->data_size); + } + + return sr; +} + +// Executed in the context of FileReadThread. +bool FileVideoCapturer::ReadFrame(bool first_frame, int* wait_time_ms) { + uint32 start_read_time_ms = talk_base::Time(); + + // 1. Signal the previously read frame to downstream. + if (!first_frame) { + captured_frame_.time_stamp = kNumNanoSecsPerMilliSec * + static_cast(start_read_time_ms); + captured_frame_.elapsed_time = captured_frame_.time_stamp - start_time_ns_; + SignalFrameCaptured(this, &captured_frame_); + } + + // 2. Read the next frame. + if (talk_base::SS_CLOSED == video_file_.GetState()) { + LOG(LS_ERROR) << "File not opened yet"; + return false; + } + // 2.1 Read the frame header. + talk_base::StreamResult result = ReadFrameHeader(&captured_frame_); + if (talk_base::SR_EOS == result) { // Loop back if repeat. + if (repeat_ != talk_base::kForever) { + if (repeat_ > 0) { + --repeat_; + } else { + return false; + } + } + + if (video_file_.SetPosition(0)) { + result = ReadFrameHeader(&captured_frame_); + } + } + if (talk_base::SR_SUCCESS != result) { + LOG(LS_ERROR) << "Failed to read the frame header"; + return false; + } + // 2.2 Reallocate memory for the frame data if necessary. + if (frame_buffer_size_ < captured_frame_.data_size) { + frame_buffer_size_ = captured_frame_.data_size; + delete[] static_cast (captured_frame_.data); + captured_frame_.data = new char[frame_buffer_size_]; + } + // 2.3 Read the frame adata. + if (talk_base::SR_SUCCESS != video_file_.Read(captured_frame_.data, + captured_frame_.data_size, + NULL, NULL)) { + LOG(LS_ERROR) << "Failed to read frame data"; + return false; + } + + // 3. Decide how long to wait for the next frame. + *wait_time_ms = 0; + + // If the capture format's interval is not kMinimumInterval, we use it to + // control the rate; otherwise, we use the timestamp in the file to control + // the rate. + if (!first_frame && !ignore_framerate_) { + int64 interval_ns = + GetCaptureFormat()->interval > VideoFormat::kMinimumInterval ? + GetCaptureFormat()->interval : + captured_frame_.time_stamp - last_frame_timestamp_ns_; + int interval_ms = static_cast(interval_ns / kNumNanoSecsPerMilliSec); + interval_ms -= talk_base::Time() - start_read_time_ms; + if (interval_ms > 0) { + *wait_time_ms = interval_ms; + } + } + // Keep the original timestamp read from the file. + last_frame_timestamp_ns_ = captured_frame_.time_stamp; + return true; +} + +} // namespace cricket diff --git a/talk/media/devices/filevideocapturer.h b/talk/media/devices/filevideocapturer.h new file mode 100644 index 000000000..e3e39b40d --- /dev/null +++ b/talk/media/devices/filevideocapturer.h @@ -0,0 +1,156 @@ +// libjingle +// Copyright 2004 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. +// +// This file contains two classes, VideoRecorder and FileVideoCapturer. +// VideoRecorder records the captured frames into a file. The file stores a +// sequence of captured frames; each frame has a header defined in struct +// CapturedFrame, followed by the frame data. +// +// FileVideoCapturer, a subclass of VideoCapturer, is a simulated video capturer +// that periodically reads images from a previously recorded file. + +#ifndef TALK_MEDIA_DEVICES_FILEVIDEOCAPTURER_H_ +#define TALK_MEDIA_DEVICES_FILEVIDEOCAPTURER_H_ + +#include +#include + +#include "talk/base/stream.h" +#include "talk/base/stringutils.h" +#include "talk/media/base/videocapturer.h" + +namespace talk_base { +class FileStream; +} + +namespace cricket { + +// Utility class to record the frames captured by a video capturer into a file. +class VideoRecorder { + public: + VideoRecorder() {} + ~VideoRecorder() { Stop(); } + + // Start the recorder by opening the specified file. Return true if the file + // is opened successfully. write_header should normally be true; false means + // write raw frame pixel data to file without any headers. + bool Start(const std::string& filename, bool write_header); + // Stop the recorder by closing the file. + void Stop(); + // Record a video frame to the file. Return true if the frame is written to + // the file successfully. This method needs to be called after Start() and + // before Stop(). + bool RecordFrame(const CapturedFrame& frame); + + private: + talk_base::FileStream video_file_; + bool write_header_; + + DISALLOW_COPY_AND_ASSIGN(VideoRecorder); +}; + +// Simulated video capturer that periodically reads frames from a file. +class FileVideoCapturer : public VideoCapturer { + public: + FileVideoCapturer(); + virtual ~FileVideoCapturer(); + + // Determines if the given device is actually a video file, to be captured + // with a FileVideoCapturer. + static bool IsFileVideoCapturerDevice(const Device& device) { + return talk_base::starts_with(device.id.c_str(), kVideoFileDevicePrefix); + } + + // Creates a fake device for the given filename. + static Device CreateFileVideoCapturerDevice(const std::string& filename) { + std::stringstream id; + id << kVideoFileDevicePrefix << filename; + return Device(filename, id.str()); + } + + // Set how many times to repeat reading the file. Repeat forever if the + // parameter is talk_base::kForever(-1); no repeat if the parameter is 0 or + // less than -1. + void set_repeat(int repeat) { repeat_ = repeat; } + + // If ignore_framerate is true, file is read as quickly as possible. If + // false, read rate is controlled by the timestamps in the video file + // (thus simulating camera capture). Default value set to false. + void set_ignore_framerate(bool ignore_framerate) { + ignore_framerate_ = ignore_framerate; + } + + // Initializes the capturer with the given file. + bool Init(const std::string& filename); + + // Initializes the capturer with the given device. This should only be used + // if IsFileVideoCapturerDevice returned true for the given device. + bool Init(const Device& device); + + // Override virtual methods of parent class VideoCapturer. + virtual CaptureState Start(const VideoFormat& capture_format); + virtual void Stop(); + virtual bool IsRunning(); + virtual bool IsScreencast() const { return false; } + + protected: + // Override virtual methods of parent class VideoCapturer. + virtual bool GetPreferredFourccs(std::vector* fourccs); + + // Read the frame header from the file stream, video_file_. + talk_base::StreamResult ReadFrameHeader(CapturedFrame* frame); + + // Read a frame and determine how long to wait for the next frame. If the + // frame is read successfully, Set the output parameter, wait_time_ms and + // return true. Otherwise, do not change wait_time_ms and return false. + bool ReadFrame(bool first_frame, int* wait_time_ms); + + // Return the CapturedFrame - useful for extracting contents after reading + // a frame. Should be used only while still reading a file (i.e. only while + // the CapturedFrame object still exists). + const CapturedFrame* frame() const { + return &captured_frame_; + } + + private: + class FileReadThread; // Forward declaration, defined in .cc. + + static const char* kVideoFileDevicePrefix; + talk_base::FileStream video_file_; + CapturedFrame captured_frame_; + // The number of bytes allocated buffer for captured_frame_.data. + uint32 frame_buffer_size_; + FileReadThread* file_read_thread_; + int repeat_; // How many times to repeat the file. + int64 start_time_ns_; // Time when the file video capturer starts. + int64 last_frame_timestamp_ns_; // Timestamp of last read frame. + bool ignore_framerate_; + + DISALLOW_COPY_AND_ASSIGN(FileVideoCapturer); +}; + +} // namespace cricket + +#endif // TALK_MEDIA_DEVICES_FILEVIDEOCAPTURER_H_ diff --git a/talk/media/devices/filevideocapturer_unittest.cc b/talk/media/devices/filevideocapturer_unittest.cc new file mode 100644 index 000000000..489f98d45 --- /dev/null +++ b/talk/media/devices/filevideocapturer_unittest.cc @@ -0,0 +1,203 @@ +/* + * libjingle + * Copyright 2004 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 + +#include +#include + +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/thread.h" +#include "talk/media/base/testutils.h" +#include "talk/media/devices/filevideocapturer.h" + +namespace { + +class FileVideoCapturerTest : public testing::Test { + public: + virtual void SetUp() { + capturer_.reset(new cricket::FileVideoCapturer); + } + + bool OpenFile(const std::string& filename) { + return capturer_->Init(cricket::GetTestFilePath(filename)); + } + + protected: + class VideoCapturerListener : public sigslot::has_slots<> { + public: + VideoCapturerListener() + : frame_count_(0), + frame_width_(0), + frame_height_(0), + resolution_changed_(false) { + } + + void OnFrameCaptured(cricket::VideoCapturer* capturer, + const cricket::CapturedFrame* frame) { + ++frame_count_; + if (1 == frame_count_) { + frame_width_ = frame->width; + frame_height_ = frame->height; + } else if (frame_width_ != frame->width || + frame_height_ != frame->height) { + resolution_changed_ = true; + } + } + + int frame_count() const { return frame_count_; } + int frame_width() const { return frame_width_; } + int frame_height() const { return frame_height_; } + bool resolution_changed() const { return resolution_changed_; } + + private: + int frame_count_; + int frame_width_; + int frame_height_; + bool resolution_changed_; + }; + + talk_base::scoped_ptr capturer_; + cricket::VideoFormat capture_format_; +}; + +TEST_F(FileVideoCapturerTest, TestNotOpened) { + EXPECT_EQ("", capturer_->GetId()); + EXPECT_TRUE(capturer_->GetSupportedFormats()->empty()); + EXPECT_EQ(NULL, capturer_->GetCaptureFormat()); + EXPECT_FALSE(capturer_->IsRunning()); +} + +TEST_F(FileVideoCapturerTest, TestInvalidOpen) { + EXPECT_FALSE(OpenFile("NotmeNotme")); +} + +TEST_F(FileVideoCapturerTest, TestOpen) { + EXPECT_TRUE(OpenFile("captured-320x240-2s-48.frames")); + EXPECT_NE("", capturer_->GetId()); + EXPECT_TRUE(NULL != capturer_->GetSupportedFormats()); + EXPECT_EQ(1U, capturer_->GetSupportedFormats()->size()); + EXPECT_EQ(NULL, capturer_->GetCaptureFormat()); // not started yet + EXPECT_FALSE(capturer_->IsRunning()); +} + +TEST_F(FileVideoCapturerTest, TestLargeSmallDesiredFormat) { + EXPECT_TRUE(OpenFile("captured-320x240-2s-48.frames")); + // desired format with large resolution. + cricket::VideoFormat desired( + 3200, 2400, cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_ANY); + EXPECT_TRUE(capturer_->GetBestCaptureFormat(desired, &capture_format_)); + EXPECT_EQ(320, capture_format_.width); + EXPECT_EQ(240, capture_format_.height); + + // Desired format with small resolution. + desired.width = 0; + desired.height = 0; + EXPECT_TRUE(capturer_->GetBestCaptureFormat(desired, &capture_format_)); + EXPECT_EQ(320, capture_format_.width); + EXPECT_EQ(240, capture_format_.height); +} + +TEST_F(FileVideoCapturerTest, TestSupportedAsDesiredFormat) { + EXPECT_TRUE(OpenFile("captured-320x240-2s-48.frames")); + // desired format same as the capture format supported by the file + cricket::VideoFormat desired = capturer_->GetSupportedFormats()->at(0); + EXPECT_TRUE(capturer_->GetBestCaptureFormat(desired, &capture_format_)); + EXPECT_TRUE(desired == capture_format_); + + // desired format same as the supported capture format except the fourcc + desired.fourcc = cricket::FOURCC_ANY; + EXPECT_TRUE(capturer_->GetBestCaptureFormat(desired, &capture_format_)); + EXPECT_NE(capture_format_.fourcc, desired.fourcc); + + // desired format with minimum interval + desired.interval = cricket::VideoFormat::kMinimumInterval; + EXPECT_TRUE(capturer_->GetBestCaptureFormat(desired, &capture_format_)); +} + +TEST_F(FileVideoCapturerTest, TestNoRepeat) { + EXPECT_TRUE(OpenFile("captured-320x240-2s-48.frames")); + VideoCapturerListener listener; + capturer_->SignalFrameCaptured.connect( + &listener, &VideoCapturerListener::OnFrameCaptured); + capturer_->set_repeat(0); + capture_format_ = capturer_->GetSupportedFormats()->at(0); + EXPECT_EQ(cricket::CS_RUNNING, capturer_->Start(capture_format_)); + EXPECT_TRUE_WAIT(!capturer_->IsRunning(), 20000); + EXPECT_EQ(48, listener.frame_count()); +} + +TEST_F(FileVideoCapturerTest, TestRepeatForever) { + // Start the capturer_ with 50 fps and read no less than 150 frames. + EXPECT_TRUE(OpenFile("captured-320x240-2s-48.frames")); + VideoCapturerListener listener; + capturer_->SignalFrameCaptured.connect( + &listener, &VideoCapturerListener::OnFrameCaptured); + capturer_->set_repeat(talk_base::kForever); + capture_format_ = capturer_->GetSupportedFormats()->at(0); + capture_format_.interval = cricket::VideoFormat::FpsToInterval(50); + EXPECT_EQ(cricket::CS_RUNNING, capturer_->Start(capture_format_)); + EXPECT_TRUE(NULL != capturer_->GetCaptureFormat()); + EXPECT_TRUE(capture_format_ == *capturer_->GetCaptureFormat()); + EXPECT_TRUE_WAIT(!capturer_->IsRunning() || + listener.frame_count() >= 150, 20000); + capturer_->Stop(); + EXPECT_FALSE(capturer_->IsRunning()); + EXPECT_GE(listener.frame_count(), 150); + EXPECT_FALSE(listener.resolution_changed()); + EXPECT_EQ(listener.frame_width(), capture_format_.width); + EXPECT_EQ(listener.frame_height(), capture_format_.height); +} + +TEST_F(FileVideoCapturerTest, TestPartialFrameHeader) { + EXPECT_TRUE(OpenFile("1.frame_plus_1.byte")); + VideoCapturerListener listener; + capturer_->SignalFrameCaptured.connect( + &listener, &VideoCapturerListener::OnFrameCaptured); + capturer_->set_repeat(0); + capture_format_ = capturer_->GetSupportedFormats()->at(0); + EXPECT_EQ(cricket::CS_RUNNING, capturer_->Start(capture_format_)); + EXPECT_TRUE_WAIT(!capturer_->IsRunning(), 1000); + EXPECT_EQ(1, listener.frame_count()); +} + +TEST_F(FileVideoCapturerTest, TestFileDevices) { + cricket::Device not_a_file("I'm a camera", "with an id"); + EXPECT_FALSE( + cricket::FileVideoCapturer::IsFileVideoCapturerDevice(not_a_file)); + const std::string test_file = + cricket::GetTestFilePath("captured-320x240-2s-48.frames"); + cricket::Device file_device = + cricket::FileVideoCapturer::CreateFileVideoCapturerDevice(test_file); + EXPECT_TRUE( + cricket::FileVideoCapturer::IsFileVideoCapturerDevice(file_device)); + EXPECT_TRUE(capturer_->Init(file_device)); + EXPECT_EQ(file_device.id, capturer_->GetId()); +} + +} // unnamed namespace diff --git a/talk/media/devices/gdivideorenderer.cc b/talk/media/devices/gdivideorenderer.cc new file mode 100755 index 000000000..d5edfc6e4 --- /dev/null +++ b/talk/media/devices/gdivideorenderer.cc @@ -0,0 +1,266 @@ +// libjingle +// Copyright 2004 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. +// +// Implementation of GdiVideoRenderer on Windows + +#ifdef WIN32 + +#include "talk/media/devices/gdivideorenderer.h" + +#include "talk/base/scoped_ptr.h" +#include "talk/base/thread.h" +#include "talk/base/win32window.h" +#include "talk/media/base/videocommon.h" +#include "talk/media/base/videoframe.h" + +namespace cricket { + +///////////////////////////////////////////////////////////////////////////// +// Definition of private class VideoWindow. We use a worker thread to manage +// the window. +///////////////////////////////////////////////////////////////////////////// +class GdiVideoRenderer::VideoWindow : public talk_base::Win32Window { + public: + VideoWindow(int x, int y, int width, int height); + virtual ~VideoWindow(); + + // Called when the video size changes. If it is called the first time, we + // create and start the thread. Otherwise, we send kSetSizeMsg to the thread. + // Context: non-worker thread. + bool SetSize(int width, int height); + + // Called when a new frame is available. Upon this call, we send + // kRenderFrameMsg to the window thread. Context: non-worker thread. It may be + // better to pass RGB bytes to VideoWindow. However, we pass VideoFrame to put + // all the thread synchronization within VideoWindow. + bool RenderFrame(const VideoFrame* frame); + + protected: + // Override virtual method of talk_base::Win32Window. Context: worker Thread. + virtual bool OnMessage(UINT uMsg, WPARAM wParam, LPARAM lParam, + LRESULT& result); + + private: + enum { kSetSizeMsg = WM_USER, kRenderFrameMsg}; + + class WindowThread : public talk_base::Thread { + public: + explicit WindowThread(VideoWindow* window) : window_(window) {} + + // Override virtual method of talk_base::Thread. Context: worker Thread. + virtual void Run() { + // Initialize the window + if (!window_ || !window_->Initialize()) { + return; + } + // Run the message loop + MSG msg; + while (GetMessage(&msg, NULL, 0, 0) > 0) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + private: + VideoWindow* window_; + }; + + // Context: worker Thread. + bool Initialize(); + void OnPaint(); + void OnSize(int width, int height, bool frame_changed); + void OnRenderFrame(const VideoFrame* frame); + + BITMAPINFO bmi_; + talk_base::scoped_array image_; + talk_base::scoped_ptr window_thread_; + // The initial position of the window. + int initial_x_; + int initial_y_; +}; + +///////////////////////////////////////////////////////////////////////////// +// Implementation of class VideoWindow +///////////////////////////////////////////////////////////////////////////// +GdiVideoRenderer::VideoWindow::VideoWindow( + int x, int y, int width, int height) + : initial_x_(x), + initial_y_(y) { + memset(&bmi_.bmiHeader, 0, sizeof(bmi_.bmiHeader)); + bmi_.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi_.bmiHeader.biPlanes = 1; + bmi_.bmiHeader.biBitCount = 32; + bmi_.bmiHeader.biCompression = BI_RGB; + bmi_.bmiHeader.biWidth = width; + bmi_.bmiHeader.biHeight = -height; + bmi_.bmiHeader.biSizeImage = width * height * 4; + + image_.reset(new uint8[bmi_.bmiHeader.biSizeImage]); +} + +GdiVideoRenderer::VideoWindow::~VideoWindow() { + // Context: caller Thread. We cannot call Destroy() since the window was + // created by another thread. Instead, we send WM_CLOSE message. + if (handle()) { + SendMessage(handle(), WM_CLOSE, 0, 0); + } +} + +bool GdiVideoRenderer::VideoWindow::SetSize(int width, int height) { + if (!window_thread_.get()) { + // Create and start the window thread. + window_thread_.reset(new WindowThread(this)); + return window_thread_->Start(); + } else if (width != bmi_.bmiHeader.biWidth || + height != -bmi_.bmiHeader.biHeight) { + SendMessage(handle(), kSetSizeMsg, 0, MAKELPARAM(width, height)); + } + return true; +} + +bool GdiVideoRenderer::VideoWindow::RenderFrame(const VideoFrame* frame) { + if (!handle()) { + return false; + } + + SendMessage(handle(), kRenderFrameMsg, reinterpret_cast(frame), 0); + return true; +} + +bool GdiVideoRenderer::VideoWindow::OnMessage(UINT uMsg, WPARAM wParam, + LPARAM lParam, LRESULT& result) { + switch (uMsg) { + case WM_PAINT: + OnPaint(); + return true; + + case WM_DESTROY: + PostQuitMessage(0); // post WM_QUIT to end the message loop in Run() + return false; + + case WM_SIZE: // The window UI was resized. + OnSize(LOWORD(lParam), HIWORD(lParam), false); + return true; + + case kSetSizeMsg: // The video resolution changed. + OnSize(LOWORD(lParam), HIWORD(lParam), true); + return true; + + case kRenderFrameMsg: + OnRenderFrame(reinterpret_cast(wParam)); + return true; + } + return false; +} + +bool GdiVideoRenderer::VideoWindow::Initialize() { + if (!talk_base::Win32Window::Create( + NULL, L"Video Renderer", + WS_OVERLAPPEDWINDOW | WS_SIZEBOX, + WS_EX_APPWINDOW, + initial_x_, initial_y_, + bmi_.bmiHeader.biWidth, -bmi_.bmiHeader.biHeight)) { + return false; + } + OnSize(bmi_.bmiHeader.biWidth, -bmi_.bmiHeader.biHeight, false); + return true; +} + +void GdiVideoRenderer::VideoWindow::OnPaint() { + RECT rcClient; + GetClientRect(handle(), &rcClient); + PAINTSTRUCT ps; + HDC hdc = BeginPaint(handle(), &ps); + StretchDIBits(hdc, + 0, 0, rcClient.right, rcClient.bottom, // destination rect + 0, 0, bmi_.bmiHeader.biWidth, -bmi_.bmiHeader.biHeight, // source rect + image_.get(), &bmi_, DIB_RGB_COLORS, SRCCOPY); + EndPaint(handle(), &ps); +} + +void GdiVideoRenderer::VideoWindow::OnSize(int width, int height, + bool frame_changed) { + // Get window and client sizes + RECT rcClient, rcWindow; + GetClientRect(handle(), &rcClient); + GetWindowRect(handle(), &rcWindow); + + // Find offset between window size and client size + POINT ptDiff; + ptDiff.x = (rcWindow.right - rcWindow.left) - rcClient.right; + ptDiff.y = (rcWindow.bottom - rcWindow.top) - rcClient.bottom; + + // Resize client + MoveWindow(handle(), rcWindow.left, rcWindow.top, + width + ptDiff.x, height + ptDiff.y, false); + UpdateWindow(handle()); + ShowWindow(handle(), SW_SHOW); + + if (frame_changed && (width != bmi_.bmiHeader.biWidth || + height != -bmi_.bmiHeader.biHeight)) { + // Update the bmi and image buffer + bmi_.bmiHeader.biWidth = width; + bmi_.bmiHeader.biHeight = -height; + bmi_.bmiHeader.biSizeImage = width * height * 4; + image_.reset(new uint8[bmi_.bmiHeader.biSizeImage]); + } +} + +void GdiVideoRenderer::VideoWindow::OnRenderFrame(const VideoFrame* frame) { + if (!frame) { + return; + } + // Convert frame to ARGB format, which is accepted by GDI + frame->ConvertToRgbBuffer(cricket::FOURCC_ARGB, image_.get(), + bmi_.bmiHeader.biSizeImage, + bmi_.bmiHeader.biWidth * 4); + InvalidateRect(handle(), 0, 0); +} + +///////////////////////////////////////////////////////////////////////////// +// Implementation of class GdiVideoRenderer +///////////////////////////////////////////////////////////////////////////// +GdiVideoRenderer::GdiVideoRenderer(int x, int y) + : initial_x_(x), + initial_y_(y) { +} +GdiVideoRenderer::~GdiVideoRenderer() {} + +bool GdiVideoRenderer::SetSize(int width, int height, int reserved) { + if (!window_.get()) { // Create the window for the first frame + window_.reset(new VideoWindow(initial_x_, initial_y_, width, height)); + } + return window_->SetSize(width, height); +} + +bool GdiVideoRenderer::RenderFrame(const VideoFrame* frame) { + if (!frame || !window_.get()) { + return false; + } + return window_->RenderFrame(frame); +} + +} // namespace cricket +#endif // WIN32 diff --git a/talk/media/devices/gdivideorenderer.h b/talk/media/devices/gdivideorenderer.h new file mode 100755 index 000000000..da3897d99 --- /dev/null +++ b/talk/media/devices/gdivideorenderer.h @@ -0,0 +1,60 @@ +// libjingle +// Copyright 2004 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. +// +// Definition of class GdiVideoRenderer that implements the abstract class +// cricket::VideoRenderer via GDI on Windows. + +#ifndef TALK_MEDIA_DEVICES_GDIVIDEORENDERER_H_ +#define TALK_MEDIA_DEVICES_GDIVIDEORENDERER_H_ + +#ifdef WIN32 +#include "talk/base/scoped_ptr.h" +#include "talk/media/base/videorenderer.h" + +namespace cricket { + +class GdiVideoRenderer : public VideoRenderer { + public: + GdiVideoRenderer(int x, int y); + virtual ~GdiVideoRenderer(); + + // Implementation of pure virtual methods of VideoRenderer. + // These two methods may be executed in different threads. + // SetSize is called before RenderFrame. + virtual bool SetSize(int width, int height, int reserved); + virtual bool RenderFrame(const VideoFrame* frame); + + private: + class VideoWindow; // forward declaration, defined in the .cc file + talk_base::scoped_ptr window_; + // The initial position of the window. + int initial_x_; + int initial_y_; +}; + +} // namespace cricket + +#endif // WIN32 +#endif // TALK_MEDIA_DEVICES_GDIVIDEORENDERER_H_ diff --git a/talk/media/devices/gtkvideorenderer.cc b/talk/media/devices/gtkvideorenderer.cc new file mode 100755 index 000000000..a926879be --- /dev/null +++ b/talk/media/devices/gtkvideorenderer.cc @@ -0,0 +1,156 @@ +// libjingle +// Copyright 2004 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. +// +// Implementation of GtkVideoRenderer + +#include "talk/media/devices/gtkvideorenderer.h" + +#include +#include +#include + +#include "talk/media/base/videocommon.h" +#include "talk/media/base/videoframe.h" + +namespace cricket { + +class ScopedGdkLock { + public: + ScopedGdkLock() { + gdk_threads_enter(); + } + + ~ScopedGdkLock() { + gdk_threads_leave(); + } +}; + +GtkVideoRenderer::GtkVideoRenderer(int x, int y) + : window_(NULL), + draw_area_(NULL), + initial_x_(x), + initial_y_(y) { + g_type_init(); + g_thread_init(NULL); + gdk_threads_init(); +} + +GtkVideoRenderer::~GtkVideoRenderer() { + if (window_) { + ScopedGdkLock lock; + gtk_widget_destroy(window_); + // Run the Gtk main loop to tear down the window. + Pump(); + } + // Don't need to destroy draw_area_ because it is not top-level, so it is + // implicitly destroyed by the above. +} + +bool GtkVideoRenderer::SetSize(int width, int height, int reserved) { + ScopedGdkLock lock; + + // For the first frame, initialize the GTK window + if ((!window_ && !Initialize(width, height)) || IsClosed()) { + return false; + } + + image_.reset(new uint8[width * height * 4]); + gtk_widget_set_size_request(draw_area_, width, height); + return true; +} + +bool GtkVideoRenderer::RenderFrame(const VideoFrame* frame) { + if (!frame) { + return false; + } + + // convert I420 frame to ABGR format, which is accepted by GTK + frame->ConvertToRgbBuffer(cricket::FOURCC_ABGR, + image_.get(), + frame->GetWidth() * frame->GetHeight() * 4, + frame->GetWidth() * 4); + + ScopedGdkLock lock; + + if (IsClosed()) { + return false; + } + + // draw the ABGR image + gdk_draw_rgb_32_image(draw_area_->window, + draw_area_->style->fg_gc[GTK_STATE_NORMAL], + 0, + 0, + frame->GetWidth(), + frame->GetHeight(), + GDK_RGB_DITHER_MAX, + image_.get(), + frame->GetWidth() * 4); + + // Run the Gtk main loop to refresh the window. + Pump(); + return true; +} + +bool GtkVideoRenderer::Initialize(int width, int height) { + gtk_init(NULL, NULL); + window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); + draw_area_ = gtk_drawing_area_new(); + if (!window_ || !draw_area_) { + return false; + } + + gtk_window_set_position(GTK_WINDOW(window_), GTK_WIN_POS_CENTER); + gtk_window_set_title(GTK_WINDOW(window_), "Video Renderer"); + gtk_window_set_resizable(GTK_WINDOW(window_), FALSE); + gtk_widget_set_size_request(draw_area_, width, height); + gtk_container_add(GTK_CONTAINER(window_), draw_area_); + gtk_widget_show_all(window_); + gtk_window_move(GTK_WINDOW(window_), initial_x_, initial_y_); + + image_.reset(new uint8[width * height * 4]); + return true; +} + +void GtkVideoRenderer::Pump() { + while (gtk_events_pending()) { + gtk_main_iteration(); + } +} + +bool GtkVideoRenderer::IsClosed() const { + if (!window_) { + // Not initialized yet, so hasn't been closed. + return false; + } + + if (!GTK_IS_WINDOW(window_) || !GTK_IS_DRAWING_AREA(draw_area_)) { + return true; + } + + return false; +} + +} // namespace cricket diff --git a/talk/media/devices/gtkvideorenderer.h b/talk/media/devices/gtkvideorenderer.h new file mode 100755 index 000000000..6276b51c8 --- /dev/null +++ b/talk/media/devices/gtkvideorenderer.h @@ -0,0 +1,69 @@ +// libjingle +// Copyright 2004 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. +// +// Definition of class GtkVideoRenderer that implements the abstract class +// cricket::VideoRenderer via GTK. + +#ifndef TALK_MEDIA_DEVICES_GTKVIDEORENDERER_H_ +#define TALK_MEDIA_DEVICES_GTKVIDEORENDERER_H_ + +#include "talk/base/basictypes.h" +#include "talk/base/scoped_ptr.h" +#include "talk/media/base/videorenderer.h" + +typedef struct _GtkWidget GtkWidget; // forward declaration, defined in gtk.h + +namespace cricket { + +class GtkVideoRenderer : public VideoRenderer { + public: + GtkVideoRenderer(int x, int y); + virtual ~GtkVideoRenderer(); + + // Implementation of pure virtual methods of VideoRenderer. + // These two methods may be executed in different threads. + // SetSize is called before RenderFrame. + virtual bool SetSize(int width, int height, int reserved); + virtual bool RenderFrame(const VideoFrame* frame); + + private: + // Initialize the attributes when the first frame arrives. + bool Initialize(int width, int height); + // Pump the Gtk event loop until there are no events left. + void Pump(); + // Check if the window has been closed. + bool IsClosed() const; + + talk_base::scoped_array image_; + GtkWidget* window_; + GtkWidget* draw_area_; + // The initial position of the window. + int initial_x_; + int initial_y_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_DEVICES_GTKVIDEORENDERER_H_ diff --git a/talk/media/devices/iosdeviceinfo.cc b/talk/media/devices/iosdeviceinfo.cc new file mode 100644 index 000000000..8b65c1428 --- /dev/null +++ b/talk/media/devices/iosdeviceinfo.cc @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2012 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/media/devices/deviceinfo.h" + +namespace cricket { + +bool GetUsbId(const Device& device, std::string* usb_id) { + return false; +} + +bool GetUsbVersion(const Device& device, std::string* usb_version) { + return false; +} + +} // namespace cricket diff --git a/talk/media/devices/libudevsymboltable.cc b/talk/media/devices/libudevsymboltable.cc new file mode 100644 index 000000000..b1d9d31e6 --- /dev/null +++ b/talk/media/devices/libudevsymboltable.cc @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2004 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/media/devices/libudevsymboltable.h" + +namespace cricket { + +#define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME LIBUDEV_SYMBOLS_CLASS_NAME +#define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST LIBUDEV_SYMBOLS_LIST +#define LATE_BINDING_SYMBOL_TABLE_DLL_NAME "libudev.so.0" +#include "talk/base/latebindingsymboltable.cc.def" +#undef LATE_BINDING_SYMBOL_TABLE_CLASS_NAME +#undef LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST +#undef LATE_BINDING_SYMBOL_TABLE_DLL_NAME + +} // namespace cricket diff --git a/talk/media/devices/libudevsymboltable.h b/talk/media/devices/libudevsymboltable.h new file mode 100644 index 000000000..046a4b0e8 --- /dev/null +++ b/talk/media/devices/libudevsymboltable.h @@ -0,0 +1,71 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_DEVICES_LIBUDEVSYMBOLTABLE_H_ +#define TALK_MEDIA_DEVICES_LIBUDEVSYMBOLTABLE_H_ + +#include + +#include "talk/base/latebindingsymboltable.h" + +namespace cricket { + +#define LIBUDEV_SYMBOLS_CLASS_NAME LibUDevSymbolTable +// The libudev symbols we need, as an X-Macro list. +// This list must contain precisely every libudev function that is used in +// linuxdevicemanager.cc. +#define LIBUDEV_SYMBOLS_LIST \ + X(udev_device_get_devnode) \ + X(udev_device_get_parent_with_subsystem_devtype) \ + X(udev_device_get_sysattr_value) \ + X(udev_device_new_from_syspath) \ + X(udev_device_unref) \ + X(udev_enumerate_add_match_subsystem) \ + X(udev_enumerate_get_list_entry) \ + X(udev_enumerate_new) \ + X(udev_enumerate_scan_devices) \ + X(udev_enumerate_unref) \ + X(udev_list_entry_get_name) \ + X(udev_list_entry_get_next) \ + X(udev_monitor_enable_receiving) \ + X(udev_monitor_filter_add_match_subsystem_devtype) \ + X(udev_monitor_get_fd) \ + X(udev_monitor_new_from_netlink) \ + X(udev_monitor_receive_device) \ + X(udev_monitor_unref) \ + X(udev_new) \ + X(udev_unref) + +#define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME LIBUDEV_SYMBOLS_CLASS_NAME +#define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST LIBUDEV_SYMBOLS_LIST +#include "talk/base/latebindingsymboltable.h.def" +#undef LATE_BINDING_SYMBOL_TABLE_CLASS_NAME +#undef LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST + +} // namespace cricket + +#endif // TALK_MEDIA_DEVICES_LIBUDEVSYMBOLTABLE_H_ diff --git a/talk/media/devices/linuxdeviceinfo.cc b/talk/media/devices/linuxdeviceinfo.cc new file mode 100644 index 000000000..7b5200112 --- /dev/null +++ b/talk/media/devices/linuxdeviceinfo.cc @@ -0,0 +1,173 @@ +/* + * libjingle + * Copyright 2012 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/media/devices/deviceinfo.h" + +#include "talk/base/common.h" // for ASSERT +#include "talk/media/devices/libudevsymboltable.h" + +namespace cricket { + +class ScopedLibUdev { + public: + static ScopedLibUdev* Create() { + ScopedLibUdev* ret_val = new ScopedLibUdev(); + if (!ret_val->Init()) { + delete ret_val; + return NULL; + } + return ret_val; + } + ~ScopedLibUdev() { + libudev_.Unload(); + } + + LibUDevSymbolTable* instance() { return &libudev_; } + + private: + ScopedLibUdev() {} + + bool Init() { + return libudev_.Load(); + } + + LibUDevSymbolTable libudev_; +}; + +class ScopedUdev { + public: + explicit ScopedUdev(LibUDevSymbolTable* libudev) : libudev_(libudev) { + udev_ = libudev_->udev_new()(); + } + ~ScopedUdev() { + if (udev_) libudev_->udev_unref()(udev_); + } + + udev* instance() { return udev_; } + + private: + LibUDevSymbolTable* libudev_; + udev* udev_; +}; + +class ScopedUdevEnumerate { + public: + ScopedUdevEnumerate(LibUDevSymbolTable* libudev, udev* udev) + : libudev_(libudev) { + enumerate_ = libudev_->udev_enumerate_new()(udev); + } + ~ScopedUdevEnumerate() { + if (enumerate_) libudev_->udev_enumerate_unref()(enumerate_); + } + + udev_enumerate* instance() { return enumerate_; } + + private: + LibUDevSymbolTable* libudev_; + udev_enumerate* enumerate_; +}; + +bool GetUsbProperty(const Device& device, const char* property_name, + std::string* property) { + talk_base::scoped_ptr libudev_context(ScopedLibUdev::Create()); + if (!libudev_context) { + return false; + } + ScopedUdev udev_context(libudev_context->instance()); + if (!udev_context.instance()) { + return false; + } + ScopedUdevEnumerate enumerate_context(libudev_context->instance(), + udev_context.instance()); + if (!enumerate_context.instance()) { + return false; + } + libudev_context->instance()->udev_enumerate_add_match_subsystem()( + enumerate_context.instance(), "video4linux"); + libudev_context->instance()->udev_enumerate_scan_devices()( + enumerate_context.instance()); + udev_list_entry* devices = + libudev_context->instance()->udev_enumerate_get_list_entry()( + enumerate_context.instance()); + if (!devices) { + return false; + } + udev_list_entry* dev_list_entry = NULL; + const char* property_value = NULL; + // Macro that expands to a for-loop over the devices. + for (dev_list_entry = devices; dev_list_entry != NULL; + dev_list_entry = libudev_context->instance()-> + udev_list_entry_get_next()(dev_list_entry)) { + const char* path = libudev_context->instance()->udev_list_entry_get_name()( + dev_list_entry); + if (!path) continue; + udev_device* dev = + libudev_context->instance()->udev_device_new_from_syspath()( + udev_context.instance(), path); + if (!dev) continue; + const char* device_node = + libudev_context->instance()->udev_device_get_devnode()(dev); + if (!device_node || device.id.compare(device_node) != 0) { + continue; + } + dev = libudev_context->instance()-> + udev_device_get_parent_with_subsystem_devtype()( + dev, "usb", "usb_device"); + if (!dev) continue; + property_value = libudev_context->instance()-> + udev_device_get_sysattr_value()( + dev, property_name); + break; + } + if (!property_value) { + return false; + } + property->assign(property_value); + return true; +} + +bool GetUsbId(const Device& device, std::string* usb_id) { + std::string id_vendor; + std::string id_product; + if (!GetUsbProperty(device, "idVendor", &id_vendor)) { + return false; + } + if (!GetUsbProperty(device, "idProduct", &id_product)) { + return false; + } + usb_id->clear(); + usb_id->append(id_vendor); + usb_id->append(":"); + usb_id->append(id_product); + return true; +} + +bool GetUsbVersion(const Device& device, std::string* usb_version) { + return GetUsbProperty(device, "version", usb_version); +} + +} // namespace cricket diff --git a/talk/media/devices/linuxdevicemanager.cc b/talk/media/devices/linuxdevicemanager.cc new file mode 100644 index 000000000..2096eeb27 --- /dev/null +++ b/talk/media/devices/linuxdevicemanager.cc @@ -0,0 +1,406 @@ +/* + * libjingle + * Copyright 2004 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/media/devices/linuxdevicemanager.h" + +#include +#include "talk/base/fileutils.h" +#include "talk/base/linux.h" +#include "talk/base/logging.h" +#include "talk/base/pathutils.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/stream.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/media/base/mediacommon.h" +#include "talk/media/devices/libudevsymboltable.h" +#include "talk/media/devices/v4llookup.h" +#include "talk/sound/platformsoundsystem.h" +#include "talk/sound/platformsoundsystemfactory.h" +#include "talk/sound/sounddevicelocator.h" +#include "talk/sound/soundsysteminterface.h" + +namespace cricket { + +DeviceManagerInterface* DeviceManagerFactory::Create() { + return new LinuxDeviceManager(); +} + +class LinuxDeviceWatcher + : public DeviceWatcher, + private talk_base::Dispatcher { + public: + explicit LinuxDeviceWatcher(DeviceManagerInterface* dm); + virtual ~LinuxDeviceWatcher(); + virtual bool Start(); + virtual void Stop(); + + private: + virtual uint32 GetRequestedEvents(); + virtual void OnPreEvent(uint32 ff); + virtual void OnEvent(uint32 ff, int err); + virtual int GetDescriptor(); + virtual bool IsDescriptorClosed(); + + DeviceManagerInterface* manager_; + LibUDevSymbolTable libudev_; + struct udev* udev_; + struct udev_monitor* udev_monitor_; + bool registered_; +}; + +static const char* const kFilteredAudioDevicesName[] = { +#if defined(CHROMEOS) + "surround40:", + "surround41:", + "surround50:", + "surround51:", + "surround71:", + "iec958:", // S/PDIF +#endif + NULL, +}; +static const char* kFilteredVideoDevicesName[] = { + NULL, +}; + +LinuxDeviceManager::LinuxDeviceManager() + : sound_system_(new PlatformSoundSystemFactory()) { + set_watcher(new LinuxDeviceWatcher(this)); +} + +LinuxDeviceManager::~LinuxDeviceManager() { +} + +bool LinuxDeviceManager::GetAudioDevices(bool input, + std::vector* devs) { + devs->clear(); + if (!sound_system_.get()) { + return false; + } + SoundSystemInterface::SoundDeviceLocatorList list; + bool success; + if (input) { + success = sound_system_->EnumerateCaptureDevices(&list); + } else { + success = sound_system_->EnumeratePlaybackDevices(&list); + } + if (!success) { + LOG(LS_ERROR) << "Can't enumerate devices"; + sound_system_.release(); + return false; + } + // We have to start the index at 1 because webrtc VoiceEngine puts the default + // device at index 0, but Enumerate(Capture|Playback)Devices does not include + // a locator for the default device. + int index = 1; + for (SoundSystemInterface::SoundDeviceLocatorList::iterator i = list.begin(); + i != list.end(); + ++i, ++index) { + devs->push_back(Device((*i)->name(), index)); + } + SoundSystemInterface::ClearSoundDeviceLocatorList(&list); + sound_system_.release(); + return FilterDevices(devs, kFilteredAudioDevicesName); +} + +static const std::string kVideoMetaPathK2_4("/proc/video/dev/"); +static const std::string kVideoMetaPathK2_6("/sys/class/video4linux/"); + +enum MetaType { M2_4, M2_6, NONE }; + +static void ScanDeviceDirectory(const std::string& devdir, + std::vector* devices) { + talk_base::scoped_ptr directoryIterator( + talk_base::Filesystem::IterateDirectory()); + + if (directoryIterator->Iterate(talk_base::Pathname(devdir))) { + do { + std::string filename = directoryIterator->Name(); + std::string device_name = devdir + filename; + if (!directoryIterator->IsDots()) { + if (filename.find("video") == 0 && + V4LLookup::IsV4L2Device(device_name)) { + devices->push_back(Device(device_name, device_name)); + } + } + } while (directoryIterator->Next()); + } +} + +static std::string GetVideoDeviceNameK2_6(const std::string& device_meta_path) { + std::string device_name; + + talk_base::scoped_ptr device_meta_stream( + talk_base::Filesystem::OpenFile(device_meta_path, "r")); + + if (device_meta_stream) { + if (device_meta_stream->ReadLine(&device_name) != talk_base::SR_SUCCESS) { + LOG(LS_ERROR) << "Failed to read V4L2 device meta " << device_meta_path; + } + device_meta_stream->Close(); + } + + return device_name; +} + +static std::string Trim(const std::string& s, const std::string& drop = " \t") { + std::string::size_type first = s.find_first_not_of(drop); + std::string::size_type last = s.find_last_not_of(drop); + + if (first == std::string::npos || last == std::string::npos) + return std::string(""); + + return s.substr(first, last - first + 1); +} + +static std::string GetVideoDeviceNameK2_4(const std::string& device_meta_path) { + talk_base::ConfigParser::MapVector all_values; + + talk_base::ConfigParser config_parser; + talk_base::FileStream* file_stream = + talk_base::Filesystem::OpenFile(device_meta_path, "r"); + + if (file_stream == NULL) return ""; + + config_parser.Attach(file_stream); + config_parser.Parse(&all_values); + + for (talk_base::ConfigParser::MapVector::iterator i = all_values.begin(); + i != all_values.end(); ++i) { + talk_base::ConfigParser::SimpleMap::iterator device_name_i = + i->find("name"); + + if (device_name_i != i->end()) { + return device_name_i->second; + } + } + + return ""; +} + +static std::string GetVideoDeviceName(MetaType meta, + const std::string& device_file_name) { + std::string device_meta_path; + std::string device_name; + std::string meta_file_path; + + if (meta == M2_6) { + meta_file_path = kVideoMetaPathK2_6 + device_file_name + "/name"; + + LOG(LS_INFO) << "Trying " + meta_file_path; + device_name = GetVideoDeviceNameK2_6(meta_file_path); + + if (device_name.empty()) { + meta_file_path = kVideoMetaPathK2_6 + device_file_name + "/model"; + + LOG(LS_INFO) << "Trying " << meta_file_path; + device_name = GetVideoDeviceNameK2_6(meta_file_path); + } + } else { + meta_file_path = kVideoMetaPathK2_4 + device_file_name; + LOG(LS_INFO) << "Trying " << meta_file_path; + device_name = GetVideoDeviceNameK2_4(meta_file_path); + } + + if (device_name.empty()) { + device_name = "/dev/" + device_file_name; + LOG(LS_ERROR) + << "Device name not found, defaulting to device path " << device_name; + } + + LOG(LS_INFO) << "Name for " << device_file_name << " is " << device_name; + + return Trim(device_name); +} + +static void ScanV4L2Devices(std::vector* devices) { + LOG(LS_INFO) << ("Enumerating V4L2 devices"); + + MetaType meta; + std::string metadata_dir; + + talk_base::scoped_ptr directoryIterator( + talk_base::Filesystem::IterateDirectory()); + + // Try and guess kernel version + if (directoryIterator->Iterate(kVideoMetaPathK2_6)) { + meta = M2_6; + metadata_dir = kVideoMetaPathK2_6; + } else if (directoryIterator->Iterate(kVideoMetaPathK2_4)) { + meta = M2_4; + metadata_dir = kVideoMetaPathK2_4; + } else { + meta = NONE; + } + + if (meta != NONE) { + LOG(LS_INFO) << "V4L2 device metadata found at " << metadata_dir; + + do { + std::string filename = directoryIterator->Name(); + + if (filename.find("video") == 0) { + std::string device_path = "/dev/" + filename; + + if (V4LLookup::IsV4L2Device(device_path)) { + devices->push_back( + Device(GetVideoDeviceName(meta, filename), device_path)); + } + } + } while (directoryIterator->Next()); + } else { + LOG(LS_ERROR) << "Unable to detect v4l2 metadata directory"; + } + + if (devices->size() == 0) { + LOG(LS_INFO) << "Plan B. Scanning all video devices in /dev directory"; + ScanDeviceDirectory("/dev/", devices); + } + + LOG(LS_INFO) << "Total V4L2 devices found : " << devices->size(); +} + +bool LinuxDeviceManager::GetVideoCaptureDevices(std::vector* devices) { + devices->clear(); + ScanV4L2Devices(devices); + return FilterDevices(devices, kFilteredVideoDevicesName); +} + +LinuxDeviceWatcher::LinuxDeviceWatcher(DeviceManagerInterface* dm) + : DeviceWatcher(dm), + manager_(dm), + udev_(NULL), + udev_monitor_(NULL), + registered_(false) { +} + +LinuxDeviceWatcher::~LinuxDeviceWatcher() { +} + +bool LinuxDeviceWatcher::Start() { + // We deliberately return true in the failure paths here because libudev is + // not a critical component of a Linux system so it may not be present/usable, + // and we don't want to halt LinuxDeviceManager initialization in such a case. + if (!libudev_.Load()) { + LOG(LS_WARNING) << "libudev not present/usable; LinuxDeviceWatcher disabled"; + return true; + } + udev_ = libudev_.udev_new()(); + if (!udev_) { + LOG_ERR(LS_ERROR) << "udev_new()"; + return true; + } + // The second argument here is the event source. It can be either "kernel" or + // "udev", but "udev" is the only correct choice. Apps listen on udev and the + // udev daemon in turn listens on the kernel. + udev_monitor_ = libudev_.udev_monitor_new_from_netlink()(udev_, "udev"); + if (!udev_monitor_) { + LOG_ERR(LS_ERROR) << "udev_monitor_new_from_netlink()"; + return true; + } + // We only listen for changes in the video devices. Audio devices are more or + // less unimportant because receiving device change notifications really only + // matters for broadcasting updated send/recv capabilities based on whether + // there is at least one device available, and almost all computers have at + // least one audio device. Also, PulseAudio device notifications don't come + // from the udev daemon, they come from the PulseAudio daemon, so we'd only + // want to listen for audio device changes from udev if using ALSA. For + // simplicity, we don't bother with any audio stuff at all. + if (libudev_.udev_monitor_filter_add_match_subsystem_devtype()( + udev_monitor_, "video4linux", NULL) < 0) { + LOG_ERR(LS_ERROR) << "udev_monitor_filter_add_match_subsystem_devtype()"; + return true; + } + if (libudev_.udev_monitor_enable_receiving()(udev_monitor_) < 0) { + LOG_ERR(LS_ERROR) << "udev_monitor_enable_receiving()"; + return true; + } + static_cast( + talk_base::Thread::Current()->socketserver())->Add(this); + registered_ = true; + return true; +} + +void LinuxDeviceWatcher::Stop() { + if (registered_) { + static_cast( + talk_base::Thread::Current()->socketserver())->Remove(this); + registered_ = false; + } + if (udev_monitor_) { + libudev_.udev_monitor_unref()(udev_monitor_); + udev_monitor_ = NULL; + } + if (udev_) { + libudev_.udev_unref()(udev_); + udev_ = NULL; + } + libudev_.Unload(); +} + +uint32 LinuxDeviceWatcher::GetRequestedEvents() { + return talk_base::DE_READ; +} + +void LinuxDeviceWatcher::OnPreEvent(uint32 ff) { + // Nothing to do. +} + +void LinuxDeviceWatcher::OnEvent(uint32 ff, int err) { + udev_device* device = libudev_.udev_monitor_receive_device()(udev_monitor_); + if (!device) { + // Probably the socket connection to the udev daemon was terminated (perhaps + // the daemon crashed or is being restarted?). + LOG_ERR(LS_WARNING) << "udev_monitor_receive_device()"; + // Stop listening to avoid potential livelock (an fd with EOF in it is + // always considered readable). + static_cast( + talk_base::Thread::Current()->socketserver())->Remove(this); + registered_ = false; + return; + } + // Else we read the device successfully. + + // Since we already have our own filesystem-based device enumeration code, we + // simply re-enumerate rather than inspecting the device event. + libudev_.udev_device_unref()(device); + manager_->SignalDevicesChange(); +} + +int LinuxDeviceWatcher::GetDescriptor() { + return libudev_.udev_monitor_get_fd()(udev_monitor_); +} + +bool LinuxDeviceWatcher::IsDescriptorClosed() { + // If it is closed then we will just get an error in + // udev_monitor_receive_device and unregister, so we don't need to check for + // it separately. + return false; +} + +}; // namespace cricket diff --git a/talk/media/devices/linuxdevicemanager.h b/talk/media/devices/linuxdevicemanager.h new file mode 100644 index 000000000..d8f1665de --- /dev/null +++ b/talk/media/devices/linuxdevicemanager.h @@ -0,0 +1,55 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_DEVICES_LINUXDEVICEMANAGER_H_ +#define TALK_MEDIA_DEVICES_LINUXDEVICEMANAGER_H_ + +#include +#include + +#include "talk/base/sigslot.h" +#include "talk/base/stringencode.h" +#include "talk/media/devices/devicemanager.h" +#include "talk/sound/soundsystemfactory.h" + +namespace cricket { + +class LinuxDeviceManager : public DeviceManager { + public: + LinuxDeviceManager(); + virtual ~LinuxDeviceManager(); + + virtual bool GetVideoCaptureDevices(std::vector* devs); + + private: + virtual bool GetAudioDevices(bool input, std::vector* devs); + SoundSystemHandle sound_system_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_DEVICES_LINUXDEVICEMANAGER_H_ diff --git a/talk/media/devices/macdeviceinfo.cc b/talk/media/devices/macdeviceinfo.cc new file mode 100644 index 000000000..f34932d0e --- /dev/null +++ b/talk/media/devices/macdeviceinfo.cc @@ -0,0 +1,56 @@ +/* + * libjingle + * Copyright 2012 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/media/devices/deviceinfo.h" + +namespace cricket { + +bool GetUsbId(const Device& device, std::string* usb_id) { + // Both PID and VID are 4 characters. + const int id_size = 4; + if (device.id.size() < 2 * id_size) { + return false; + } + + // The last characters of device id is a concatenation of VID and then PID. + const size_t vid_location = device.id.size() - 2 * id_size; + std::string id_vendor = device.id.substr(vid_location, id_size); + const size_t pid_location = device.id.size() - id_size; + std::string id_product = device.id.substr(pid_location, id_size); + + usb_id->clear(); + usb_id->append(id_vendor); + usb_id->append(":"); + usb_id->append(id_product); + return true; +} + +bool GetUsbVersion(const Device& device, std::string* usb_version) { + return false; +} + +} // namespace cricket diff --git a/talk/media/devices/macdevicemanager.cc b/talk/media/devices/macdevicemanager.cc new file mode 100644 index 000000000..10d85a0f7 --- /dev/null +++ b/talk/media/devices/macdevicemanager.cc @@ -0,0 +1,197 @@ +/* + * libjingle + * Copyright 2004 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/media/devices/macdevicemanager.h" + +#include +#include + +#include "talk/base/logging.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/media/base/mediacommon.h" + +class DeviceWatcherImpl; + +namespace cricket { + +DeviceManagerInterface* DeviceManagerFactory::Create() { + return new MacDeviceManager(); +} + +class MacDeviceWatcher : public DeviceWatcher { + public: + explicit MacDeviceWatcher(DeviceManagerInterface* dm); + virtual ~MacDeviceWatcher(); + virtual bool Start(); + virtual void Stop(); + + private: + DeviceManagerInterface* manager_; + DeviceWatcherImpl* impl_; +}; + +static const char* kFilteredAudioDevicesName[] = { + NULL, +}; +// TODO(tommyw): Try to get hold of a copy of Final Cut to understand why we +// crash while scanning their components on OS X. +static const char* const kFilteredVideoDevicesName[] = { + "DVCPRO HD", // Final cut + "Sonix SN9C201p", // Crashes in OpenAComponent and CloseComponent + NULL, +}; +static const int kVideoDeviceOpenAttempts = 3; +static const UInt32 kAudioDeviceNameLength = 64; +// Obj-C functions defined in macdevicemanagermm.mm +// TODO(ronghuawu): have a shared header for these function defines. +extern DeviceWatcherImpl* CreateDeviceWatcherCallback( + DeviceManagerInterface* dm); +extern void ReleaseDeviceWatcherCallback(DeviceWatcherImpl* impl); +extern bool GetQTKitVideoDevices(std::vector* out); +static bool GetAudioDeviceIDs(bool inputs, std::vector* out); +static bool GetAudioDeviceName(AudioDeviceID id, bool input, std::string* out); + +MacDeviceManager::MacDeviceManager() { + set_watcher(new MacDeviceWatcher(this)); +} + +MacDeviceManager::~MacDeviceManager() { +} + +bool MacDeviceManager::GetVideoCaptureDevices(std::vector* devices) { + devices->clear(); + if (!GetQTKitVideoDevices(devices)) { + return false; + } + return FilterDevices(devices, kFilteredVideoDevicesName); +} + +bool MacDeviceManager::GetAudioDevices(bool input, + std::vector* devs) { + devs->clear(); + std::vector dev_ids; + bool ret = GetAudioDeviceIDs(input, &dev_ids); + if (!ret) { + return false; + } + for (size_t i = 0; i < dev_ids.size(); ++i) { + std::string name; + if (GetAudioDeviceName(dev_ids[i], input, &name)) { + devs->push_back(Device(name, dev_ids[i])); + } + } + return FilterDevices(devs, kFilteredAudioDevicesName); +} + +static bool GetAudioDeviceIDs(bool input, + std::vector* out_dev_ids) { + UInt32 propsize; + OSErr err = AudioHardwareGetPropertyInfo(kAudioHardwarePropertyDevices, + &propsize, NULL); + if (0 != err) { + LOG(LS_ERROR) << "Couldn't get information about property, " + << "so no device list acquired."; + return false; + } + + size_t num_devices = propsize / sizeof(AudioDeviceID); + talk_base::scoped_array device_ids( + new AudioDeviceID[num_devices]); + + err = AudioHardwareGetProperty(kAudioHardwarePropertyDevices, + &propsize, device_ids.get()); + if (0 != err) { + LOG(LS_ERROR) << "Failed to get device ids, " + << "so no device listing acquired."; + return false; + } + + for (size_t i = 0; i < num_devices; ++i) { + AudioDeviceID an_id = device_ids[i]; + // find out the number of channels for this direction + // (input/output) on this device - + // we'll ignore anything with no channels. + err = AudioDeviceGetPropertyInfo(an_id, 0, input, + kAudioDevicePropertyStreams, + &propsize, NULL); + if (0 == err) { + unsigned num_channels = propsize / sizeof(AudioStreamID); + if (0 < num_channels) { + out_dev_ids->push_back(an_id); + } + } else { + LOG(LS_ERROR) << "No property info for stream property for device id " + << an_id << "(is_input == " << input + << "), so not including it in the list."; + } + } + + return true; +} + +static bool GetAudioDeviceName(AudioDeviceID id, + bool input, + std::string* out_name) { + UInt32 nameLength = kAudioDeviceNameLength; + char name[kAudioDeviceNameLength + 1]; + OSErr err = AudioDeviceGetProperty(id, 0, input, + kAudioDevicePropertyDeviceName, + &nameLength, name); + if (0 != err) { + LOG(LS_ERROR) << "No name acquired for device id " << id; + return false; + } + + *out_name = name; + return true; +} + +MacDeviceWatcher::MacDeviceWatcher(DeviceManagerInterface* manager) + : DeviceWatcher(manager), + manager_(manager), + impl_(NULL) { +} + +MacDeviceWatcher::~MacDeviceWatcher() { +} + +bool MacDeviceWatcher::Start() { + if (!impl_) { + impl_ = CreateDeviceWatcherCallback(manager_); + } + return impl_ != NULL; +} + +void MacDeviceWatcher::Stop() { + if (impl_) { + ReleaseDeviceWatcherCallback(impl_); + impl_ = NULL; + } +} + +}; // namespace cricket diff --git a/talk/media/devices/macdevicemanager.h b/talk/media/devices/macdevicemanager.h new file mode 100644 index 000000000..25fe4fcaf --- /dev/null +++ b/talk/media/devices/macdevicemanager.h @@ -0,0 +1,56 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_DEVICES_MACDEVICEMANAGER_H_ +#define TALK_MEDIA_DEVICES_MACDEVICEMANAGER_H_ + +#include +#include + +#include "talk/base/sigslot.h" +#include "talk/base/stringencode.h" +#include "talk/media/devices/devicemanager.h" + +namespace cricket { + +class DeviceWatcher; + +class MacDeviceManager : public DeviceManager { + public: + MacDeviceManager(); + virtual ~MacDeviceManager(); + + virtual bool GetVideoCaptureDevices(std::vector* devs); + + private: + virtual bool GetAudioDevices(bool input, std::vector* devs); + bool FilterDevice(const Device& d); +}; + +} // namespace cricket + +#endif // TALK_MEDIA_DEVICES_MACDEVICEMANAGER_H_ diff --git a/talk/media/devices/macdevicemanagermm.mm b/talk/media/devices/macdevicemanagermm.mm new file mode 100644 index 000000000..8cc77518c --- /dev/null +++ b/talk/media/devices/macdevicemanagermm.mm @@ -0,0 +1,140 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +// support GCC compiler +#ifndef __has_feature +# define __has_feature(x) 0 +#endif + +#include "talk/media/devices/devicemanager.h" + +#import +#import + +#include "talk/base/logging.h" + +@interface DeviceWatcherImpl : NSObject { + @private + cricket::DeviceManagerInterface* manager_; +} +- (id)init:(cricket::DeviceManagerInterface*)manager; +- (void)onDevicesChanged:(NSNotification *)notification; +@end + +@implementation DeviceWatcherImpl +- (id)init:(cricket::DeviceManagerInterface*)manager { + if ((self = [super init])) { + assert(manager != NULL); + manager_ = manager; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(onDevicesChanged:) + name:QTCaptureDeviceWasConnectedNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(onDevicesChanged:) + name:QTCaptureDeviceWasDisconnectedNotification + object:nil]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +#if !__has_feature(objc_arc) + [super dealloc]; +#endif +} +- (void)onDevicesChanged:(NSNotification *)notification { + manager_->SignalDevicesChange(); +} +@end + +namespace cricket { + +DeviceWatcherImpl* CreateDeviceWatcherCallback( + DeviceManagerInterface* manager) { + DeviceWatcherImpl* impl; +#if !__has_feature(objc_arc) + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; +#else + @autoreleasepool +#endif + { + impl = [[DeviceWatcherImpl alloc] init:manager]; + } +#if !__has_feature(objc_arc) + [pool drain]; +#endif + return impl; +} + +void ReleaseDeviceWatcherCallback(DeviceWatcherImpl* watcher) { +#if !__has_feature(objc_arc) + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + [watcher release]; + [pool drain]; +#endif +} + +bool GetQTKitVideoDevices(std::vector* devices) { +#if !__has_feature(objc_arc) + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; +#else + @autoreleasepool +#endif + { + NSArray* qt_capture_devices = + [QTCaptureDevice inputDevicesWithMediaType:QTMediaTypeVideo]; + NSUInteger count = [qt_capture_devices count]; + LOG(LS_INFO) << count << " capture device(s) found:"; + for (QTCaptureDevice* qt_capture_device in qt_capture_devices) { + static NSString* const kFormat = @"localizedDisplayName: \"%@\", " + @"modelUniqueID: \"%@\", uniqueID \"%@\", isConnected: %d, " + @"isOpen: %d, isInUseByAnotherApplication: %d"; + NSString* info = [NSString stringWithFormat:kFormat, + [qt_capture_device localizedDisplayName], + [qt_capture_device modelUniqueID], + [qt_capture_device uniqueID], + [qt_capture_device isConnected], + [qt_capture_device isOpen], + [qt_capture_device isInUseByAnotherApplication]]; + LOG(LS_INFO) << [info UTF8String]; + + std::string name([[qt_capture_device localizedDisplayName] + UTF8String]); + devices->push_back(Device(name, + [[qt_capture_device uniqueID] + UTF8String])); + } + } +#if !__has_feature(objc_arc) + [pool drain]; +#endif + return true; +} + +} // namespace cricket diff --git a/talk/media/devices/mobiledevicemanager.cc b/talk/media/devices/mobiledevicemanager.cc new file mode 100644 index 000000000..a08911b91 --- /dev/null +++ b/talk/media/devices/mobiledevicemanager.cc @@ -0,0 +1,76 @@ +/* + * libjingle + * Copyright 2013 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/media/devices/devicemanager.h" +#include "webrtc/modules/video_capture/include/video_capture_factory.h" + +namespace cricket { + +class MobileDeviceManager : public DeviceManager { + public: + MobileDeviceManager(); + virtual ~MobileDeviceManager(); + virtual bool GetVideoCaptureDevices(std::vector* devs); +}; + +MobileDeviceManager::MobileDeviceManager() { + // We don't expect available devices to change on Android/iOS, so use a + // do-nothing watcher. + set_watcher(new DeviceWatcher(this)); +} + +MobileDeviceManager::~MobileDeviceManager() {} + +bool MobileDeviceManager::GetVideoCaptureDevices(std::vector* devs) { + devs->clear(); + talk_base::scoped_ptr info( + webrtc::VideoCaptureFactory::CreateDeviceInfo(0)); + if (!info) + return false; + + uint32 num_cams = info->NumberOfDevices(); + char id[256]; + char name[256]; + for (uint32 i = 0; i < num_cams; ++i) { + if (info->GetDeviceName(i, name, ARRAY_SIZE(name), id, ARRAY_SIZE(id))) + continue; + devs->push_back(Device(name, id)); + } + return true; +} + +DeviceManagerInterface* DeviceManagerFactory::Create() { + return new MobileDeviceManager(); +} + +bool GetUsbId(const Device& device, std::string* usb_id) { return false; } + +bool GetUsbVersion(const Device& device, std::string* usb_version) { + return false; +} + +} // namespace cricket diff --git a/talk/media/devices/v4llookup.cc b/talk/media/devices/v4llookup.cc new file mode 100644 index 000000000..ff128a4ae --- /dev/null +++ b/talk/media/devices/v4llookup.cc @@ -0,0 +1,67 @@ +/* + * Copyright 2009 Google Inc. + * Author: lexnikitin@google.com (Alexey Nikitin) + * + * V4LLookup provides basic functionality to work with V2L2 devices in Linux + * The functionality is implemented as a class with virtual methods for + * the purpose of unit testing. + */ +#include "talk/media/devices/v4llookup.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "talk/base/logging.h" + +namespace cricket { + +V4LLookup *V4LLookup::v4l_lookup_ = NULL; + +bool V4LLookup::CheckIsV4L2Device(const std::string& device_path) { + // check device major/minor numbers are in the range for video devices. + struct stat s; + + if (lstat(device_path.c_str(), &s) != 0 || !S_ISCHR(s.st_mode)) return false; + + int video_fd = -1; + bool is_v4l2 = false; + + // check major/minur device numbers are in range for video device + if (major(s.st_rdev) == 81) { + dev_t num = minor(s.st_rdev); + if (num <= 63) { + video_fd = ::open(device_path.c_str(), O_RDONLY | O_NONBLOCK); + if ((video_fd >= 0) || (errno == EBUSY)) { + ::v4l2_capability video_caps; + memset(&video_caps, 0, sizeof(video_caps)); + + if ((errno == EBUSY) || + (::ioctl(video_fd, VIDIOC_QUERYCAP, &video_caps) >= 0 && + (video_caps.capabilities & V4L2_CAP_VIDEO_CAPTURE))) { + LOG(LS_INFO) << "Found V4L2 capture device " << device_path; + + is_v4l2 = true; + } else { + LOG(LS_ERROR) << "VIDIOC_QUERYCAP failed for " << device_path; + } + } else { + LOG(LS_ERROR) << "Failed to open " << device_path; + } + } + } + + if (video_fd >= 0) + ::close(video_fd); + + return is_v4l2; +} + +}; // namespace cricket diff --git a/talk/media/devices/v4llookup.h b/talk/media/devices/v4llookup.h new file mode 100644 index 000000000..026eb0ebc --- /dev/null +++ b/talk/media/devices/v4llookup.h @@ -0,0 +1,44 @@ +/* + * Copyright 2009 Google Inc. + * Author: lexnikitin@google.com (Alexey Nikitin) + * + * V4LLookup provides basic functionality to work with V2L2 devices in Linux + * The functionality is implemented as a class with virtual methods for + * the purpose of unit testing. + */ +#ifndef TALK_MEDIA_DEVICES_V4LLOOKUP_H_ +#define TALK_MEDIA_DEVICES_V4LLOOKUP_H_ + +#include + +#ifdef LINUX +namespace cricket { +class V4LLookup { + public: + virtual ~V4LLookup() {} + + static bool IsV4L2Device(const std::string& device_path) { + return GetV4LLookup()->CheckIsV4L2Device(device_path); + } + + static void SetV4LLookup(V4LLookup* v4l_lookup) { + v4l_lookup_ = v4l_lookup; + } + + static V4LLookup* GetV4LLookup() { + if (!v4l_lookup_) { + v4l_lookup_ = new V4LLookup(); + } + return v4l_lookup_; + } + + protected: + static V4LLookup* v4l_lookup_; + // Making virtual so it is easier to mock + virtual bool CheckIsV4L2Device(const std::string& device_path); +}; + +} // namespace cricket + +#endif // LINUX +#endif // TALK_MEDIA_DEVICES_V4LLOOKUP_H_ diff --git a/talk/media/devices/videorendererfactory.h b/talk/media/devices/videorendererfactory.h new file mode 100644 index 000000000..64033c997 --- /dev/null +++ b/talk/media/devices/videorendererfactory.h @@ -0,0 +1,66 @@ +// libjingle +// Copyright 2010 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. +// +// A factory to create a GUI video renderer. + +#ifndef TALK_MEDIA_DEVICES_VIDEORENDERERFACTORY_H_ +#define TALK_MEDIA_DEVICES_VIDEORENDERERFACTORY_H_ + +#include "talk/media/base/videorenderer.h" +#if defined(LINUX) && defined(HAVE_GTK) +#include "talk/media/devices/gtkvideorenderer.h" +#elif defined(OSX) && !defined(CARBON_DEPRECATED) +#include "talk/media/devices/carbonvideorenderer.h" +#elif defined(WIN32) +#include "talk/media/devices/gdivideorenderer.h" +#endif + +namespace cricket { + +class VideoRendererFactory { + public: + static VideoRenderer* CreateGuiVideoRenderer(int x, int y) { + #if defined(LINUX) && defined(HAVE_GTK) + return new GtkVideoRenderer(x, y); + #elif defined(OSX) && !defined(CARBON_DEPRECATED) + CarbonVideoRenderer* renderer = new CarbonVideoRenderer(x, y); + // Needs to be initialized on the main thread. + if (renderer->Initialize()) { + return renderer; + } else { + delete renderer; + return NULL; + } + #elif defined(WIN32) + return new GdiVideoRenderer(x, y); + #else + return NULL; + #endif + } +}; + +} // namespace cricket + +#endif // TALK_MEDIA_DEVICES_VIDEORENDERERFACTORY_H_ diff --git a/talk/media/devices/win32deviceinfo.cc b/talk/media/devices/win32deviceinfo.cc new file mode 100644 index 000000000..61a775903 --- /dev/null +++ b/talk/media/devices/win32deviceinfo.cc @@ -0,0 +1,62 @@ +/* + * libjingle + * Copyright 2012 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/media/devices/deviceinfo.h" + +namespace cricket { + +bool GetUsbId(const Device& device, std::string* usb_id) { + // Both PID and VID are 4 characters. + const int id_size = 4; + const char vid[] = "vid_"; // Also contains '\0'. + const size_t vid_location = device.id.find(vid); + if (vid_location == std::string::npos || + vid_location + sizeof(vid) - 1 + id_size > device.id.size()) { + return false; + } + const char pid[] = "pid_"; + const size_t pid_location = device.id.find(pid); + if (pid_location == std::string::npos || + pid_location + sizeof(pid) - 1 + id_size > device.id.size()) { + return false; + } + std::string id_vendor = device.id.substr(vid_location + sizeof(vid) - 1, + id_size); + std::string id_product = device.id.substr(pid_location + sizeof(pid) -1, + id_size); + usb_id->clear(); + usb_id->append(id_vendor); + usb_id->append(":"); + usb_id->append(id_product); + return true; +} + +bool GetUsbVersion(const Device& device, std::string* usb_version) { + return false; +} + +} // namespace cricket diff --git a/talk/media/devices/win32devicemanager.cc b/talk/media/devices/win32devicemanager.cc new file mode 100644 index 000000000..071f11179 --- /dev/null +++ b/talk/media/devices/win32devicemanager.cc @@ -0,0 +1,404 @@ +/* + * libjingle + * Copyright 2004 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/media/devices/win32devicemanager.h" + +#include +#include +#include // must come before ks.h +#include +#include +#define INITGUID // For PKEY_AudioEndpoint_GUID +#include +#include +#include +#include + +#include "talk/base/logging.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/base/win32.h" // ToUtf8 +#include "talk/base/win32window.h" +#include "talk/media/base/mediacommon.h" +#ifdef HAVE_LOGITECH_HEADERS +#include "third_party/logitech/files/logitechquickcam.h" +#endif + +namespace cricket { + +DeviceManagerInterface* DeviceManagerFactory::Create() { + return new Win32DeviceManager(); +} + +class Win32DeviceWatcher + : public DeviceWatcher, + public talk_base::Win32Window { + public: + explicit Win32DeviceWatcher(Win32DeviceManager* dm); + virtual ~Win32DeviceWatcher(); + virtual bool Start(); + virtual void Stop(); + + private: + HDEVNOTIFY Register(REFGUID guid); + void Unregister(HDEVNOTIFY notify); + virtual bool OnMessage(UINT msg, WPARAM wp, LPARAM lp, LRESULT& result); + + Win32DeviceManager* manager_; + HDEVNOTIFY audio_notify_; + HDEVNOTIFY video_notify_; +}; + +static const char* kFilteredAudioDevicesName[] = { + NULL, +}; +static const char* const kFilteredVideoDevicesName[] = { + "Asus virtual Camera", // Bad Asus desktop virtual cam + "Bluetooth Video", // Bad Sony viao bluetooth sharing driver + NULL, +}; +static const wchar_t kFriendlyName[] = L"FriendlyName"; +static const wchar_t kDevicePath[] = L"DevicePath"; +static const char kUsbDevicePathPrefix[] = "\\\\?\\usb"; +static bool GetDevices(const CLSID& catid, std::vector* out); +static bool GetCoreAudioDevices(bool input, std::vector* devs); +static bool GetWaveDevices(bool input, std::vector* devs); + +Win32DeviceManager::Win32DeviceManager() + : need_couninitialize_(false) { + set_watcher(new Win32DeviceWatcher(this)); +} + +Win32DeviceManager::~Win32DeviceManager() { + if (initialized()) { + Terminate(); + } +} + +bool Win32DeviceManager::Init() { + if (!initialized()) { + HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); + need_couninitialize_ = SUCCEEDED(hr); + if (FAILED(hr)) { + LOG(LS_ERROR) << "CoInitialize failed, hr=" << hr; + if (hr != RPC_E_CHANGED_MODE) { + return false; + } + } + if (!watcher()->Start()) { + return false; + } + set_initialized(true); + } + return true; +} + +void Win32DeviceManager::Terminate() { + if (initialized()) { + watcher()->Stop(); + if (need_couninitialize_) { + CoUninitialize(); + need_couninitialize_ = false; + } + set_initialized(false); + } +} + +bool Win32DeviceManager::GetDefaultVideoCaptureDevice(Device* device) { + bool ret = false; + // If there are multiple capture devices, we want the first USB one. + // This avoids issues with defaulting to virtual cameras or grabber cards. + std::vector devices; + ret = (GetVideoCaptureDevices(&devices) && !devices.empty()); + if (ret) { + *device = devices[0]; + for (size_t i = 0; i < devices.size(); ++i) { + if (strnicmp(devices[i].id.c_str(), kUsbDevicePathPrefix, + ARRAY_SIZE(kUsbDevicePathPrefix) - 1) == 0) { + *device = devices[i]; + break; + } + } + } + return ret; +} + +bool Win32DeviceManager::GetAudioDevices(bool input, + std::vector* devs) { + devs->clear(); + + if (talk_base::IsWindowsVistaOrLater()) { + if (!GetCoreAudioDevices(input, devs)) + return false; + } else { + if (!GetWaveDevices(input, devs)) + return false; + } + return FilterDevices(devs, kFilteredAudioDevicesName); +} + +bool Win32DeviceManager::GetVideoCaptureDevices(std::vector* devices) { + devices->clear(); + if (!GetDevices(CLSID_VideoInputDeviceCategory, devices)) { + return false; + } + return FilterDevices(devices, kFilteredVideoDevicesName); +} + +bool GetDevices(const CLSID& catid, std::vector* devices) { + HRESULT hr; + + // CComPtr is a scoped pointer that will be auto released when going + // out of scope. CoUninitialize must not be called before the + // release. + CComPtr sys_dev_enum; + CComPtr cam_enum; + if (FAILED(hr = sys_dev_enum.CoCreateInstance(CLSID_SystemDeviceEnum)) || + FAILED(hr = sys_dev_enum->CreateClassEnumerator(catid, &cam_enum, 0))) { + LOG(LS_ERROR) << "Failed to create device enumerator, hr=" << hr; + return false; + } + + // Only enum devices if CreateClassEnumerator returns S_OK. If there are no + // devices available, S_FALSE will be returned, but enumMk will be NULL. + if (hr == S_OK) { + CComPtr mk; + while (cam_enum->Next(1, &mk, NULL) == S_OK) { +#ifdef HAVE_LOGITECH_HEADERS + // Initialize Logitech device if applicable + MaybeLogitechDeviceReset(mk); +#endif + CComPtr bag; + if (SUCCEEDED(mk->BindToStorage(NULL, NULL, + __uuidof(bag), reinterpret_cast(&bag)))) { + CComVariant name, path; + std::string name_str, path_str; + if (SUCCEEDED(bag->Read(kFriendlyName, &name, 0)) && + name.vt == VT_BSTR) { + name_str = talk_base::ToUtf8(name.bstrVal); + // Get the device id if one exists. + if (SUCCEEDED(bag->Read(kDevicePath, &path, 0)) && + path.vt == VT_BSTR) { + path_str = talk_base::ToUtf8(path.bstrVal); + } + + devices->push_back(Device(name_str, path_str)); + } + } + mk = NULL; + } + } + + return true; +} + +HRESULT GetStringProp(IPropertyStore* bag, PROPERTYKEY key, std::string* out) { + out->clear(); + PROPVARIANT var; + PropVariantInit(&var); + + HRESULT hr = bag->GetValue(key, &var); + if (SUCCEEDED(hr)) { + if (var.pwszVal) + *out = talk_base::ToUtf8(var.pwszVal); + else + hr = E_FAIL; + } + + PropVariantClear(&var); + return hr; +} + +// Adapted from http://msdn.microsoft.com/en-us/library/dd370812(v=VS.85).aspx +HRESULT CricketDeviceFromImmDevice(IMMDevice* device, Device* out) { + CComPtr props; + + HRESULT hr = device->OpenPropertyStore(STGM_READ, &props); + if (FAILED(hr)) { + return hr; + } + + // Get the endpoint's name and id. + std::string name, guid; + hr = GetStringProp(props, PKEY_Device_FriendlyName, &name); + if (SUCCEEDED(hr)) { + hr = GetStringProp(props, PKEY_AudioEndpoint_GUID, &guid); + + if (SUCCEEDED(hr)) { + out->name = name; + out->id = guid; + } + } + return hr; +} + +bool GetCoreAudioDevices( + bool input, std::vector* devs) { + HRESULT hr = S_OK; + CComPtr enumerator; + + hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, + __uuidof(IMMDeviceEnumerator), reinterpret_cast(&enumerator)); + if (SUCCEEDED(hr)) { + CComPtr devices; + hr = enumerator->EnumAudioEndpoints((input ? eCapture : eRender), + DEVICE_STATE_ACTIVE, &devices); + if (SUCCEEDED(hr)) { + unsigned int count; + hr = devices->GetCount(&count); + + if (SUCCEEDED(hr)) { + for (unsigned int i = 0; i < count; i++) { + CComPtr device; + + // Get pointer to endpoint number i. + hr = devices->Item(i, &device); + if (FAILED(hr)) { + break; + } + + Device dev; + hr = CricketDeviceFromImmDevice(device, &dev); + if (SUCCEEDED(hr)) { + devs->push_back(dev); + } else { + LOG(LS_WARNING) << "Unable to query IMM Device, skipping. HR=" + << hr; + hr = S_FALSE; + } + } + } + } + } + + if (FAILED(hr)) { + LOG(LS_WARNING) << "GetCoreAudioDevices failed with hr " << hr; + return false; + } + return true; +} + +bool GetWaveDevices(bool input, std::vector* devs) { + // Note, we don't use the System Device Enumerator interface here since it + // adds lots of pseudo-devices to the list, such as DirectSound and Wave + // variants of the same device. + if (input) { + int num_devs = waveInGetNumDevs(); + for (int i = 0; i < num_devs; ++i) { + WAVEINCAPS caps; + if (waveInGetDevCaps(i, &caps, sizeof(caps)) == MMSYSERR_NOERROR && + caps.wChannels > 0) { + devs->push_back(Device(talk_base::ToUtf8(caps.szPname), + talk_base::ToString(i))); + } + } + } else { + int num_devs = waveOutGetNumDevs(); + for (int i = 0; i < num_devs; ++i) { + WAVEOUTCAPS caps; + if (waveOutGetDevCaps(i, &caps, sizeof(caps)) == MMSYSERR_NOERROR && + caps.wChannels > 0) { + devs->push_back(Device(talk_base::ToUtf8(caps.szPname), i)); + } + } + } + return true; +} + +Win32DeviceWatcher::Win32DeviceWatcher(Win32DeviceManager* manager) + : DeviceWatcher(manager), + manager_(manager), + audio_notify_(NULL), + video_notify_(NULL) { +} + +Win32DeviceWatcher::~Win32DeviceWatcher() { +} + +bool Win32DeviceWatcher::Start() { + if (!Create(NULL, _T("libjingle Win32DeviceWatcher Window"), + 0, 0, 0, 0, 0, 0)) { + return false; + } + + audio_notify_ = Register(KSCATEGORY_AUDIO); + if (!audio_notify_) { + Stop(); + return false; + } + + video_notify_ = Register(KSCATEGORY_VIDEO); + if (!video_notify_) { + Stop(); + return false; + } + + return true; +} + +void Win32DeviceWatcher::Stop() { + UnregisterDeviceNotification(video_notify_); + video_notify_ = NULL; + UnregisterDeviceNotification(audio_notify_); + audio_notify_ = NULL; + Destroy(); +} + +HDEVNOTIFY Win32DeviceWatcher::Register(REFGUID guid) { + DEV_BROADCAST_DEVICEINTERFACE dbdi; + dbdi.dbcc_size = sizeof(dbdi); + dbdi.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; + dbdi.dbcc_classguid = guid; + dbdi.dbcc_name[0] = '\0'; + return RegisterDeviceNotification(handle(), &dbdi, + DEVICE_NOTIFY_WINDOW_HANDLE); +} + +void Win32DeviceWatcher::Unregister(HDEVNOTIFY handle) { + UnregisterDeviceNotification(handle); +} + +bool Win32DeviceWatcher::OnMessage(UINT uMsg, WPARAM wParam, LPARAM lParam, + LRESULT& result) { + if (uMsg == WM_DEVICECHANGE) { + if (wParam == DBT_DEVICEARRIVAL || + wParam == DBT_DEVICEREMOVECOMPLETE) { + DEV_BROADCAST_DEVICEINTERFACE* dbdi = + reinterpret_cast(lParam); + if (dbdi->dbcc_classguid == KSCATEGORY_AUDIO || + dbdi->dbcc_classguid == KSCATEGORY_VIDEO) { + manager_->SignalDevicesChange(); + } + } + result = 0; + return true; + } + + return false; +} + +}; // namespace cricket diff --git a/talk/media/devices/win32devicemanager.h b/talk/media/devices/win32devicemanager.h new file mode 100644 index 000000000..4854ec07b --- /dev/null +++ b/talk/media/devices/win32devicemanager.h @@ -0,0 +1,60 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_DEVICES_WIN32DEVICEMANAGER_H_ +#define TALK_MEDIA_DEVICES_WIN32DEVICEMANAGER_H_ + +#include +#include + +#include "talk/base/sigslot.h" +#include "talk/base/stringencode.h" +#include "talk/media/devices/devicemanager.h" + +namespace cricket { + +class Win32DeviceManager : public DeviceManager { + public: + Win32DeviceManager(); + virtual ~Win32DeviceManager(); + + // Initialization + virtual bool Init(); + virtual void Terminate(); + + virtual bool GetVideoCaptureDevices(std::vector* devs); + + private: + virtual bool GetAudioDevices(bool input, std::vector* devs); + virtual bool GetDefaultVideoCaptureDevice(Device* device); + + bool need_couninitialize_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_DEVICES_WIN32DEVICEMANAGER_H_ diff --git a/talk/media/other/linphonemediaengine.cc b/talk/media/other/linphonemediaengine.cc new file mode 100644 index 000000000..3b97c0b9f --- /dev/null +++ b/talk/media/other/linphonemediaengine.cc @@ -0,0 +1,276 @@ +/* + * libjingle + * Copyright 2010 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. + */ + +#ifndef MSILBC_LIBRARY +#define MSILBC_LIBRARY "/usr/lib/mediastreamer/plugins/libmsilbc.so" +#endif + +// LinphoneMediaEngine is a Linphone implementation of MediaEngine +extern "C" { +#include +#include +#include +} + +#include "talk/media/other/linphonemediaengine.h" + +#include "talk/base/buffer.h" +#include "talk/base/event.h" +#include "talk/base/logging.h" +#include "talk/base/pathutils.h" +#include "talk/base/stream.h" +#include "talk/media/base/rtpdump.h" + +#ifndef WIN32 +#include +#endif + +namespace cricket { + +/////////////////////////////////////////////////////////////////////////// +// Implementation of LinphoneMediaEngine. +/////////////////////////////////////////////////////////////////////////// +LinphoneMediaEngine::LinphoneMediaEngine(const std::string& ringWav, const std::string& callWav) : ring_wav_(ringWav), call_wav_(callWav) { } + +bool LinphoneMediaEngine::Init() { + ortp_init(); + ms_init(); + +#ifdef HAVE_ILBC +#ifndef WIN32 + char * path = strdup(MSILBC_LIBRARY); + char * dirc = dirname(path); + ms_load_plugins(dirc); +#endif + if (ms_filter_codec_supported("iLBC")) + have_ilbc = 1; + else + have_ilbc = 0; +#else + have_ilbc = 0; +#endif + +#ifdef HAVE_SPEEX + voice_codecs_.push_back(AudioCodec(110, payload_type_speex_wb.mime_type, payload_type_speex_wb.clock_rate, 0, 1, 8)); + voice_codecs_.push_back(AudioCodec(111, payload_type_speex_nb.mime_type, payload_type_speex_nb.clock_rate, 0, 1, 7)); +#endif + +#ifdef HAVE_ILBC + if (have_ilbc) + voice_codecs_.push_back(AudioCodec(102, payload_type_ilbc.mime_type, payload_type_ilbc.clock_rate, 0, 1, 4)); +#endif + + voice_codecs_.push_back(AudioCodec(0, payload_type_pcmu8000.mime_type, payload_type_pcmu8000.clock_rate, 0, 1, 2)); + voice_codecs_.push_back(AudioCodec(101, payload_type_telephone_event.mime_type, payload_type_telephone_event.clock_rate, 0, 1, 1)); + return true; +} + +void LinphoneMediaEngine::Terminate() { + fflush(stdout); +} + + +int LinphoneMediaEngine::GetCapabilities() { + int capabilities = 0; + capabilities |= AUDIO_SEND; + capabilities |= AUDIO_RECV; + return capabilities; +} + +VoiceMediaChannel* LinphoneMediaEngine::CreateChannel() { + return new LinphoneVoiceChannel(this); +} + +VideoMediaChannel* LinphoneMediaEngine::CreateVideoChannel(VoiceMediaChannel* voice_ch) { + return NULL; +} + +bool LinphoneMediaEngine::FindAudioCodec(const AudioCodec &c) { + if (c.id == 0) + return true; + if (c.name == payload_type_telephone_event.mime_type) + return true; +#ifdef HAVE_SPEEX + if (c.name == payload_type_speex_wb.mime_type && c.clockrate == payload_type_speex_wb.clock_rate) + return true; + if (c.name == payload_type_speex_nb.mime_type && c.clockrate == payload_type_speex_nb.clock_rate) + return true; +#endif +#ifdef HAVE_ILBC + if (have_ilbc && c.name == payload_type_ilbc.mime_type) + return true; +#endif + return false; +} + +/////////////////////////////////////////////////////////////////////////// +// Implementation of LinphoneVoiceChannel. +/////////////////////////////////////////////////////////////////////////// +LinphoneVoiceChannel::LinphoneVoiceChannel(LinphoneMediaEngine*eng) + : pt_(-1), + audio_stream_(0), + engine_(eng), + ring_stream_(0) +{ + + talk_base::Thread *thread = talk_base::ThreadManager::CurrentThread(); + talk_base::SocketServer *ss = thread->socketserver(); + socket_.reset(ss->CreateAsyncSocket(SOCK_DGRAM)); + + socket_->Bind(talk_base::SocketAddress("localhost",3000)); + socket_->SignalReadEvent.connect(this, &LinphoneVoiceChannel::OnIncomingData); + +} + +LinphoneVoiceChannel::~LinphoneVoiceChannel() +{ + fflush(stdout); + StopRing(); + + if (audio_stream_) + audio_stream_stop(audio_stream_); +} + +bool LinphoneVoiceChannel::SetPlayout(bool playout) { + play_ = playout; + return true; +} + +bool LinphoneVoiceChannel::SetSendCodecs(const std::vector& codecs) { + + bool first = true; + std::vector::const_iterator i; + + ortp_set_log_level_mask(ORTP_MESSAGE|ORTP_WARNING|ORTP_ERROR|ORTP_FATAL); + + for (i = codecs.begin(); i < codecs.end(); i++) { + + if (!engine_->FindAudioCodec(*i)) + continue; +#ifdef HAVE_ILBC + if (engine_->have_ilbc && i->name == payload_type_ilbc.mime_type) { + rtp_profile_set_payload(&av_profile, i->id, &payload_type_ilbc); + } +#endif +#ifdef HAVE_SPEEX + if (i->name == payload_type_speex_wb.mime_type && i->clockrate == payload_type_speex_wb.clock_rate) { + rtp_profile_set_payload(&av_profile, i->id, &payload_type_speex_wb); + } else if (i->name == payload_type_speex_nb.mime_type && i->clockrate == payload_type_speex_nb.clock_rate) { + rtp_profile_set_payload(&av_profile, i->id, &payload_type_speex_nb); + } +#endif + + if (i->id == 0) + rtp_profile_set_payload(&av_profile, 0, &payload_type_pcmu8000); + + if (i->name == payload_type_telephone_event.mime_type) { + rtp_profile_set_payload(&av_profile, i->id, &payload_type_telephone_event); + } + + if (first) { + StopRing(); + LOG(LS_INFO) << "Using " << i->name << "/" << i->clockrate; + pt_ = i->id; + audio_stream_ = audio_stream_start(&av_profile, 2000, "127.0.0.1", 3000, i->id, 250, 0); + first = false; + } + } + + if (first) { + StopRing(); + // We're being asked to set an empty list of codecs. This will only happen when + // working with a buggy client; let's try PCMU. + LOG(LS_WARNING) << "Received empty list of codces; using PCMU/8000"; + audio_stream_ = audio_stream_start(&av_profile, 2000, "127.0.0.1", 3000, 0, 250, 0); + } + + return true; +} + +bool LinphoneVoiceChannel::SetSend(SendFlags flag) { + mute_ = !flag; + return true; +} + +void LinphoneVoiceChannel::OnPacketReceived(talk_base::Buffer* packet) { + const void* data = packet->data(); + int len = packet->length(); + uint8 buf[2048]; + memcpy(buf, data, len); + + /* We may receive packets with payload type 13: comfort noise. Linphone can't + * handle them, so let's ignore those packets. + */ + int payloadtype = buf[1] & 0x7f; + if (play_ && payloadtype != 13) + socket_->SendTo(buf, len, talk_base::SocketAddress("localhost",2000)); +} + +void LinphoneVoiceChannel::StartRing(bool bIncomingCall) +{ + MSSndCard *sndcard = NULL; + sndcard=ms_snd_card_manager_get_default_card(ms_snd_card_manager_get()); + if (sndcard) + { + if (bIncomingCall) + { + if (engine_->GetRingWav().size() > 0) + { + LOG(LS_VERBOSE) << "incoming ring. sound file: " << engine_->GetRingWav().c_str() << "\n"; + ring_stream_ = ring_start (engine_->GetRingWav().c_str(), 1, sndcard); + } + } + else + { + if (engine_->GetCallWav().size() > 0) + { + LOG(LS_VERBOSE) << "outgoing ring. sound file: " << engine_->GetCallWav().c_str() << "\n"; + ring_stream_ = ring_start (engine_->GetCallWav().c_str(), 1, sndcard); + } + } + } +} + +void LinphoneVoiceChannel::StopRing() +{ + if (ring_stream_) { + ring_stop(ring_stream_); + ring_stream_ = 0; + } +} + +void LinphoneVoiceChannel::OnIncomingData(talk_base::AsyncSocket *s) +{ + char *buf[2048]; + int len; + len = s->Recv(buf, sizeof(buf)); + talk_base::Buffer packet(buf, len); + if (network_interface_ && !mute_) + network_interface_->SendPacket(&packet); +} + +} diff --git a/talk/media/other/linphonemediaengine.h b/talk/media/other/linphonemediaengine.h new file mode 100644 index 000000000..69b2d2f6a --- /dev/null +++ b/talk/media/other/linphonemediaengine.h @@ -0,0 +1,173 @@ +/* + * libjingle + * Copyright 2010 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. + */ + +// LinphoneMediaEngine is a Linphone implementation of MediaEngine + +#ifndef TALK_SESSION_PHONE_LINPHONEMEDIAENGINE_H_ +#define TALK_SESSION_PHONE_LINPHONEMEDIAENGINE_H_ + +#include +#include + +extern "C" { +#include +} + +#include "talk/base/scoped_ptr.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/mediaengine.h" + +namespace talk_base { +class StreamInterface; +} + +namespace cricket { + +class LinphoneMediaEngine : public MediaEngineInterface { + public: + LinphoneMediaEngine(const std::string& ringWav, const std::string& callWav); + virtual ~LinphoneMediaEngine() {} + + // Should be called before codecs() and video_codecs() are called. We need to + // set the voice and video codecs; otherwise, Jingle initiation will fail. + void set_voice_codecs(const std::vector& codecs) { + voice_codecs_ = codecs; + } + void set_video_codecs(const std::vector& codecs) { + video_codecs_ = codecs; + } + + // Implement pure virtual methods of MediaEngine. + virtual bool Init(); + virtual void Terminate(); + virtual int GetCapabilities(); + virtual VoiceMediaChannel* CreateChannel(); + virtual VideoMediaChannel* CreateVideoChannel(VoiceMediaChannel* voice_ch); + virtual SoundclipMedia* CreateSoundclip() { return NULL; } + virtual bool SetAudioOptions(int options) { return true; } + virtual bool SetVideoOptions(int options) { return true; } + virtual bool SetDefaultVideoEncoderConfig(const VideoEncoderConfig& config) { + return true; + } + virtual bool SetSoundDevices(const Device* in_dev, const Device* out_dev) { + return true; + } + virtual bool SetVideoCaptureDevice(const Device* cam_device) { return true; } + virtual bool SetOutputVolume(int level) { return true; } + virtual int GetInputLevel() { return 0; } + virtual bool SetLocalMonitor(bool enable) { return true; } + virtual bool SetLocalRenderer(VideoRenderer* renderer) { return true; } + // TODO: control channel send? + virtual bool SetVideoCapture(bool capture) { return true; } + virtual const std::vector& audio_codecs() { + return voice_codecs_; + } + virtual const std::vector& video_codecs() { + return video_codecs_; + } + virtual bool FindAudioCodec(const AudioCodec& codec); + virtual bool FindVideoCodec(const VideoCodec& codec) { return true; } + virtual void SetVoiceLogging(int min_sev, const char* filter) {} + virtual void SetVideoLogging(int min_sev, const char* filter) {} + + std::string GetRingWav(){return ring_wav_;} + std::string GetCallWav(){return call_wav_;} + + int have_ilbc; + + private: + std::string voice_input_filename_; + std::string voice_output_filename_; + std::string video_input_filename_; + std::string video_output_filename_; + std::vector voice_codecs_; + std::vector video_codecs_; + + std::string ring_wav_; + std::string call_wav_; + + DISALLOW_COPY_AND_ASSIGN(LinphoneMediaEngine); +}; + +class LinphoneVoiceChannel : public VoiceMediaChannel { + public: + LinphoneVoiceChannel(LinphoneMediaEngine *eng); + virtual ~LinphoneVoiceChannel(); + + // Implement pure virtual methods of VoiceMediaChannel. + virtual bool SetRecvCodecs(const std::vector& codecs) { return true; } + virtual bool SetSendCodecs(const std::vector& codecs); + virtual bool SetPlayout(bool playout); + virtual bool SetSend(SendFlags flag); + virtual bool AddStream(uint32 ssrc) { return true; } + virtual bool RemoveStream(uint32 ssrc) { return true; } + virtual bool GetActiveStreams(AudioInfo::StreamList* actives) { return true; } + virtual int GetOutputLevel() { return 0; } + virtual bool SetOutputScaling(uint32 ssrc, double left, double right) { + return false; + } + virtual bool GetOutputScaling(uint32 ssrc, double* left, double* right) { + return false; + } + virtual void SetRingbackTone(const char* buf, int len) {} + virtual bool PlayRingbackTone(bool play, bool loop) { return true; } + virtual bool PressDTMF(int event, bool playout) { return true; } + virtual bool GetStats(VoiceMediaInfo* info) { return true; } + + // Implement pure virtual methods of MediaChannel. + virtual void OnPacketReceived(talk_base::Buffer* packet); + virtual void OnRtcpReceived(talk_base::Buffer* packet) {} + virtual void SetSendSsrc(uint32 id) {} // TODO: change RTP packet? + virtual bool SetRtcpCName(const std::string& cname) { return true; } + virtual bool Mute(bool on) { return mute_; } + virtual bool SetSendBandwidth(bool autobw, int bps) { return true; } + virtual bool SetOptions(int options) { return true; } + virtual bool SetRecvRtpHeaderExtensions( + const std::vector& extensions) { return true; } + virtual bool SetSendRtpHeaderExtensions( + const std::vector& extensions) { return true; } + + virtual void StartRing(bool bIncomingCall); + virtual void StopRing(); + + private: + int pt_; + bool mute_; + bool play_; + AudioStream *audio_stream_; + LinphoneMediaEngine *engine_; + RingStream* ring_stream_; + talk_base::scoped_ptr socket_; + void OnIncomingData(talk_base::AsyncSocket *s); + + DISALLOW_COPY_AND_ASSIGN(LinphoneVoiceChannel); +}; + +} // namespace cricket + +#endif // TALK_SESSION_PHONE_LINPHONEMEDIAENGINE_H_ diff --git a/talk/media/sctp/sctpdataengine.cc b/talk/media/sctp/sctpdataengine.cc new file mode 100644 index 000000000..cb70182ad --- /dev/null +++ b/talk/media/sctp/sctpdataengine.cc @@ -0,0 +1,684 @@ +/* + * libjingle SCTP + * Copyright 2012 Google Inc, and Robin Seggelmann + * + * 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/media/sctp/sctpdataengine.h" + +#include +#include +#include +#include + +#include "talk/base/buffer.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/streamparams.h" +#include "usrsctplib/usrsctp.h" + +#ifdef _WIN32 +// EINPROGRESS gets #defined to WSAEINPROGRESS in some headers above, which +// is not 112. 112 is the value defined in . usrsctp uses 112 for +// EINPROGRESS. +#undef EINPROGRESS +#define EINPROGRESS (112) +#endif + +namespace cricket { + +// This is the SCTP port to use. It is passed along the wire and the listener +// and connector must be using the same port. It is not related to the ports at +// the IP level. (Corresponds to: sockaddr_conn.sconn_port in usrsctp.h) +// +// TODO(ldixon): Allow port to be set from higher level code. +static const int kSctpDefaultPort = 5001; +// TODO(ldixon): Find where this is defined, and also check is Sctp really +// respects this. +static const size_t kSctpMtu = 1280; + +enum { + MSG_SCTPINBOUNDPACKET = 1, // MessageData is SctpInboundPacket + MSG_SCTPOUTBOUNDPACKET = 2, // MessageData is talk_base:Buffer +}; + +struct SctpInboundPacket { + talk_base::Buffer buffer; + ReceiveDataParams params; + // The |flags| parameter is used by SCTP to distinguish notification packets + // from other types of packets. + int flags; +}; + +// Helper for logging SCTP data. Given a buffer, returns a readable string. +static void debug_sctp_printf(const char *format, ...) { + char s[255]; + va_list ap; + va_start(ap, format); + vsnprintf(s, sizeof(s), format, ap); + LOG(LS_INFO) << s; + // vprintf(format, ap); + va_end(ap); +} + +// Helper for make a string dump of some SCTP data. Used for LOG +// debugging messages. +static std::string SctpDataToDebugString(void* buffer, size_t length, + int dump_type) { + char *dump_buf = usrsctp_dumppacket(buffer, length, dump_type); + if (!dump_buf) { + return ""; + } + std::string s = std::string(dump_buf); + usrsctp_freedumpbuffer(dump_buf); + return s; +} + +// This is the callback usrsctp uses when there's data to send on the network +// that has been wrapped appropriatly for the SCTP protocol. +static int OnSctpOutboundPacket(void* addr, void* data, size_t length, + uint8_t tos, uint8_t set_df) { + SctpDataMediaChannel* channel = static_cast(addr); + LOG(LS_VERBOSE) << "global OnSctpOutboundPacket():" + << "addr: " << addr << "; length: " << length + << "; tos: " << std::hex << static_cast(tos) + << "; set_df: " << std::hex << static_cast(set_df) + << "; data:" << SctpDataToDebugString(data, length, + SCTP_DUMP_OUTBOUND); + // Note: We have to copy the data; the caller will delete it. + talk_base::Buffer* buffer = new talk_base::Buffer(data, length); + channel->worker_thread()->Post(channel, MSG_SCTPOUTBOUNDPACKET, + talk_base::WrapMessageData(buffer)); + return 0; +} + +// This is the callback called from usrsctp when data has been received, after +// a packet has been interpreted and parsed by usrsctp and found to contain +// payload data. It is called by a usrsctp thread. It is assumed this function +// will free the memory used by 'data'. +static int OnSctpInboundPacket(struct socket* sock, union sctp_sockstore addr, + void* data, size_t length, + struct sctp_rcvinfo rcv, int flags, + void* ulp_info) { + LOG(LS_VERBOSE) << "global OnSctpInboundPacket... Msg of length " + << length << " received via " << addr.sconn.sconn_addr << ":" + << talk_base::NetworkToHost16(addr.sconn.sconn_port) + << " on stream " << rcv.rcv_sid + << " with SSN " << rcv.rcv_ssn + << " and TSN " << rcv.rcv_tsn << ", PPID " + << talk_base::NetworkToHost32(rcv.rcv_ppid) + << ", context " << rcv.rcv_context + << ", data: " << data + << ", ulp_info:" << ulp_info + << ", flags:" << std::hex << flags; + SctpDataMediaChannel* channel = static_cast(ulp_info); + // The second log call is useful when the defines flags are incorrect. In + // this case, ulp_info ends up being bad and the second log message will + // cause a crash. + LOG(LS_VERBOSE) << "global OnSctpInboundPacket. channel=" + << channel->debug_name() << "..."; + // Post data to the channel's receiver thread (copying it). + // TODO(ldixon): Unclear if copy is needed as this method is responsible for + // memory cleanup. But this does simplify code. + SctpInboundPacket* packet = new SctpInboundPacket(); + packet->buffer.SetData(data, length); + packet->params.ssrc = rcv.rcv_sid; + packet->params.seq_num = rcv.rcv_ssn; + packet->params.timestamp = rcv.rcv_tsn; + packet->flags = flags; + channel->worker_thread()->Post(channel, MSG_SCTPINBOUNDPACKET, + talk_base::WrapMessageData(packet)); + free(data); + return 1; +} + +// Set the initial value of the static SCTP Data Engines reference count. +int SctpDataEngine::usrsctp_engines_count = 0; + +SctpDataEngine::SctpDataEngine() { + if (usrsctp_engines_count == 0) { + // First argument is udp_encapsulation_port, which is not releveant for our + // AF_CONN use of sctp. + usrsctp_init(0, cricket::OnSctpOutboundPacket, debug_sctp_printf); + + // To turn on/off detailed SCTP debugging. You will also need to have the + // SCTP_DEBUG cpp defines flag. + // usrsctp_sysctl_set_sctp_debug_on(SCTP_DEBUG_ALL); + + // TODO(ldixon): Consider turning this on/off. + usrsctp_sysctl_set_sctp_ecn_enable(0); + + // TODO(ldixon): Consider turning this on/off. + // This is not needed right now (we don't do dynamic address changes): + // If SCTP Auto-ASCONF is enabled, the peer is informed automatically + // when a new address is added or removed. This feature is enabled by + // default. + // usrsctp_sysctl_set_sctp_auto_asconf(0); + + // TODO(ldixon): Consider turning this on/off. + // Add a blackhole sysctl. Setting it to 1 results in no ABORTs + // being sent in response to INITs, setting it to 2 results + // in no ABORTs being sent for received OOTB packets. + // This is similar to the TCP sysctl. + // + // See: http://lakerest.net/pipermail/sctp-coders/2012-January/009438.html + // See: http://svnweb.freebsd.org/base?view=revision&revision=229805 + // usrsctp_sysctl_set_sctp_blackhole(2); + } + usrsctp_engines_count++; + + // We don't put in a codec because we don't want one offered when we + // use the hybrid data engine. + // codecs_.push_back(cricket::DataCodec( kGoogleSctpDataCodecId, + // kGoogleSctpDataCodecName, 0)); +} + +SctpDataEngine::~SctpDataEngine() { + // TODO(ldixon): There is currently a bug in teardown of usrsctp that blocks + // indefintely if a finish call made too soon after close calls. So teardown + // has been skipped. Once the bug is fixed, retest and enable teardown. + // + // usrsctp_engines_count--; + // LOG(LS_VERBOSE) << "usrsctp_engines_count:" << usrsctp_engines_count; + // if (usrsctp_engines_count == 0) { + // if (usrsctp_finish() != 0) { + // LOG(LS_WARNING) << "usrsctp_finish."; + // } + // } +} + +DataMediaChannel* SctpDataEngine::CreateChannel( + DataChannelType data_channel_type) { + if (data_channel_type != DCT_SCTP) { + return NULL; + } + return new SctpDataMediaChannel(talk_base::Thread::Current()); +} + +SctpDataMediaChannel::SctpDataMediaChannel(talk_base::Thread* thread) + : worker_thread_(thread), + local_port_(kSctpDefaultPort), + remote_port_(kSctpDefaultPort), + sock_(NULL), + sending_(false), + receiving_(false), + debug_name_("SctpDataMediaChannel") { +} + +SctpDataMediaChannel::~SctpDataMediaChannel() { + CloseSctpSocket(); +} + +sockaddr_conn SctpDataMediaChannel::GetSctpSockAddr(int port) { + sockaddr_conn sconn = {0}; + sconn.sconn_family = AF_CONN; +#ifdef HAVE_SCONN_LEN + sconn.sconn_len = sizeof(sockaddr_conn); +#endif + // Note: conversion from int to uint16_t happens here. + sconn.sconn_port = talk_base::HostToNetwork16(port); + sconn.sconn_addr = this; + return sconn; +} + +bool SctpDataMediaChannel::OpenSctpSocket() { + if (sock_) { + LOG(LS_VERBOSE) << debug_name_ + << "->Ignoring attempt to re-create existing socket."; + return false; + } + sock_ = usrsctp_socket(AF_CONN, SOCK_STREAM, IPPROTO_SCTP, + cricket::OnSctpInboundPacket, NULL, 0, this); + if (!sock_) { + LOG_ERRNO(LS_ERROR) << debug_name_ << "Failed to create SCTP socket."; + return false; + } + + // Make the socket non-blocking. Connect, close, shutdown etc will not block + // the thread waiting for the socket operation to complete. + if (usrsctp_set_non_blocking(sock_, 1) < 0) { + LOG_ERRNO(LS_ERROR) << debug_name_ << "Failed to set SCTP to non blocking."; + return false; + } + + // This ensures that the usrsctp close call deletes the association. This + // prevents usrsctp from calling OnSctpOutboundPacket with references to + // this class as the address. + linger linger_opt; + linger_opt.l_onoff = 1; + linger_opt.l_linger = 0; + if (usrsctp_setsockopt(sock_, SOL_SOCKET, SO_LINGER, &linger_opt, + sizeof(linger_opt))) { + LOG_ERRNO(LS_ERROR) << debug_name_ << "Failed to set SO_LINGER."; + return false; + } + + // Subscribe to SCTP event notifications. + int event_types[] = {SCTP_ASSOC_CHANGE, + SCTP_PEER_ADDR_CHANGE, + SCTP_SEND_FAILED_EVENT}; + struct sctp_event event = {0}; + event.se_assoc_id = SCTP_ALL_ASSOC; + event.se_on = 1; + for (size_t i = 0; i < ARRAY_SIZE(event_types); i++) { + event.se_type = event_types[i]; + if (usrsctp_setsockopt(sock_, IPPROTO_SCTP, SCTP_EVENT, &event, + sizeof(event)) < 0) { + LOG_ERRNO(LS_ERROR) << debug_name_ << "Failed to set SCTP_EVENT type: " + << event.se_type; + return false; + } + } + + // Register this class as an address for usrsctp. This is used by SCTP to + // direct the packets received (by the created socket) to this class. + usrsctp_register_address(this); + sending_ = true; + return true; +} + +void SctpDataMediaChannel::CloseSctpSocket() { + sending_ = false; + if (sock_) { + // We assume that SO_LINGER option is set to close the association when + // close is called. This means that any pending packets in usrsctp will be + // discarded instead of being sent. + usrsctp_close(sock_); + sock_ = NULL; + usrsctp_deregister_address(this); + } +} + +bool SctpDataMediaChannel::Connect() { + LOG(LS_VERBOSE) << debug_name_ << "->Connect()."; + + // If we already have a socket connection, just return. + if (sock_) { + LOG(LS_WARNING) << debug_name_ << "->Connect(): Ignored as socket " + "is already established."; + return true; + } + + // If no socket (it was closed) try to start it again. This can happen when + // the socket we are connecting to closes, does an sctp shutdown handshake, + // or behaves unexpectedly causing us to perform a CloseSctpSocket. + if (!sock_ && !OpenSctpSocket()) { + return false; + } + + // Note: conversion from int to uint16_t happens on assignment. + sockaddr_conn local_sconn = GetSctpSockAddr(local_port_); + if (usrsctp_bind(sock_, reinterpret_cast(&local_sconn), + sizeof(local_sconn)) < 0) { + LOG_ERRNO(LS_ERROR) << debug_name_ << "->Connect(): " + << ("Failed usrsctp_bind"); + CloseSctpSocket(); + return false; + } + + // Note: conversion from int to uint16_t happens on assignment. + sockaddr_conn remote_sconn = GetSctpSockAddr(remote_port_); + int connect_result = usrsctp_connect( + sock_, reinterpret_cast(&remote_sconn), sizeof(remote_sconn)); + if (connect_result < 0 && errno != EINPROGRESS) { + LOG_ERRNO(LS_ERROR) << debug_name_ << "Failed usrsctp_connect"; + CloseSctpSocket(); + return false; + } + return true; +} + +void SctpDataMediaChannel::Disconnect() { + // TODO(ldixon): Consider calling |usrsctp_shutdown(sock_, ...)| to do a + // shutdown handshake and remove the association. + CloseSctpSocket(); +} + +bool SctpDataMediaChannel::SetSend(bool send) { + if (!sending_ && send) { + return Connect(); + } + if (sending_ && !send) { + Disconnect(); + } + return true; +} + +bool SctpDataMediaChannel::SetReceive(bool receive) { + receiving_ = receive; + return true; +} + +bool SctpDataMediaChannel::AddSendStream(const StreamParams& stream) { + if (!stream.has_ssrcs()) { + return false; + } + + StreamParams found_stream; + if (GetStreamBySsrc(send_streams_, stream.first_ssrc(), &found_stream)) { + LOG(LS_WARNING) << debug_name_ << "->AddSendStream(...): " + << "Not adding data send stream '" << stream.id + << "' with ssrc=" << stream.first_ssrc() + << " because stream already exists."; + return false; + } + + send_streams_.push_back(stream); + return true; +} + +bool SctpDataMediaChannel::RemoveSendStream(uint32 ssrc) { + StreamParams found_stream; + if (!GetStreamBySsrc(send_streams_, ssrc, &found_stream)) { + return false; + } + + RemoveStreamBySsrc(&send_streams_, ssrc); + return true; +} + +// Note: expects exactly one ssrc. If none are given, it will fail. If more +// than one are given, it will use the first. +bool SctpDataMediaChannel::AddRecvStream(const StreamParams& stream) { + if (!stream.has_ssrcs()) { + return false; + } + + StreamParams found_stream; + if (GetStreamBySsrc(recv_streams_, stream.first_ssrc(), &found_stream)) { + LOG(LS_WARNING) << debug_name_ << "->AddRecvStream(...): " + << "Not adding data recv stream '" << stream.id + << "' with ssrc=" << stream.first_ssrc() + << " because stream already exists."; + return false; + } + + recv_streams_.push_back(stream); + LOG(LS_VERBOSE) << debug_name_ << "->AddRecvStream(...): " + << "Added data recv stream '" << stream.id + << "' with ssrc=" << stream.first_ssrc(); + return true; +} + +bool SctpDataMediaChannel::RemoveRecvStream(uint32 ssrc) { + RemoveStreamBySsrc(&recv_streams_, ssrc); + return true; +} + +bool SctpDataMediaChannel::SendData( + const SendDataParams& params, + const talk_base::Buffer& payload, + SendDataResult* result) { + if (result) { + // If we return true, we'll set this to SDR_SUCCESS. + *result = SDR_ERROR; + } + + if (!sending_) { + LOG(LS_WARNING) << debug_name_ << "->SendData(...): " + << "Not sending packet with ssrc=" << params.ssrc + << " len=" << payload.length() << " before SetSend(true)."; + return false; + } + + StreamParams found_stream; + if (!GetStreamBySsrc(send_streams_, params.ssrc, &found_stream)) { + LOG(LS_WARNING) << debug_name_ << "->SendData(...): " + << "Not sending data because ssrc is unknown: " + << params.ssrc; + return false; + } + + // TODO(ldixon): Experiment with sctp_sendv_spa instead of sctp_sndinfo. e.g. + // struct sctp_sendv_spa spa = {0}; + // spa.sendv_flags |= SCTP_SEND_SNDINFO_VALID; + // spa.sendv_sndinfo.snd_sid = params.ssrc; + // spa.sendv_sndinfo.snd_context = 0; + // spa.sendv_sndinfo.snd_assoc_id = 0; + // TODO(pthatcher): Support different types of protocols (e.g. SSL) and + // messages (e.g. Binary) via SendDataParams. + // spa.sendv_sndinfo.snd_ppid = htonl(PPID_NONE); + // TODO(pthatcher): Support different reliability semantics. + // For reliable: Remove SCTP_UNORDERED. + // For partially-reliable: Add rtx or ttl. + // spa.sendv_sndinfo.snd_flags = SCTP_UNORDERED; + // TODO(phatcher): Try some of these things. + // spa.sendv_flags |= SCTP_SEND_PRINFO_VALID; + // spa.sendv_prinfo.pr_policy = SCTP_PR_SCTP_RTX; + // spa.sendv_prinfo.pr_value = htons(max_retransmit_count); + // spa.sendv_prinfo.pr_policy = SCTP_PR_SCTP_TTL; + // spa.sendv_prinfo.pr_value = htons(max_retransmit_time); + // + // Send data using SCTP. + sctp_sndinfo sndinfo = {0}; + sndinfo.snd_sid = params.ssrc; + sndinfo.snd_flags = 0; + // TODO(pthatcher): Once data types are added to SendParams, this can be set + // from SendParams. + sndinfo.snd_ppid = talk_base::HostToNetwork32(PPID_NONE); + sndinfo.snd_context = 0; + sndinfo.snd_assoc_id = 0; + ssize_t res = usrsctp_sendv(sock_, payload.data(), + static_cast(payload.length()), + NULL, 0, &sndinfo, + static_cast(sizeof(sndinfo)), + SCTP_SENDV_SNDINFO, 0); + if (res < 0) { + LOG_ERRNO(LS_ERROR) << "ERROR:" << debug_name_ + << "SendData->(...): " + << " usrsctp_sendv: "; + // TODO(pthatcher): Make result SDR_BLOCK if the error is because + // it would block. + return false; + } + if (result) { + // If we return true, we'll set this to SDR_SUCCESS. + *result = SDR_SUCCESS; + } + return true; +} + +// Called by network interface when a packet has been received. +void SctpDataMediaChannel::OnPacketReceived(talk_base::Buffer* packet) { + LOG(LS_VERBOSE) << debug_name_ << "->OnPacketReceived(...): " + << " length=" << packet->length() << "; data=" + << SctpDataToDebugString(packet->data(), packet->length(), + SCTP_DUMP_INBOUND); + // Only give receiving packets to usrsctp after if connected. This enables two + // peers to each make a connect call, but for them not to receive an INIT + // packet before they have called connect; least the last receiver of the INIT + // packet will have called connect, and a connection will be established. + if (sending_) { + LOG(LS_VERBOSE) << debug_name_ << "->OnPacketReceived(...):" + << " Passed packet to sctp."; + // Pass received packet to SCTP stack. Once processed by usrsctp, the data + // will be will be given to the global OnSctpInboundData, and then, + // marshalled by a Post and handled with OnMessage. + usrsctp_conninput(this, packet->data(), packet->length(), 0); + } else { + // TODO(ldixon): Consider caching the packet for very slightly better + // reliability. + LOG(LS_INFO) << debug_name_ << "->OnPacketReceived(...):" + << " Threw packet (probably an INIT) away."; + } +} + +void SctpDataMediaChannel::OnInboundPacketFromSctpToChannel( + SctpInboundPacket* packet) { + LOG(LS_VERBOSE) << debug_name_ << "->OnInboundPacketFromSctpToChannel(...): " + << "Received SCTP data:" + << " ssrc=" << packet->params.ssrc + << " data='" << std::string(packet->buffer.data(), + packet->buffer.length()) + << " notification: " << (packet->flags & MSG_NOTIFICATION) + << "' length=" << packet->buffer.length(); + // Sending a packet with data == NULL (no data) is SCTPs "close the + // connection" message. This sets sock_ = NULL; + if (!packet->buffer.length() || !packet->buffer.data()) { + LOG(LS_INFO) << debug_name_ << "->OnInboundPacketFromSctpToChannel(...): " + "No data, closing."; + return; + } + if (packet->flags & MSG_NOTIFICATION) { + OnNotificationFromSctp(&packet->buffer); + } else { + OnDataFromSctpToChannel(packet->params, &packet->buffer); + } +} + +void SctpDataMediaChannel::OnDataFromSctpToChannel( + const ReceiveDataParams& params, talk_base::Buffer* buffer) { + StreamParams found_stream; + if (!GetStreamBySsrc(recv_streams_, params.ssrc, &found_stream)) { + LOG(LS_WARNING) << debug_name_ << "->OnDataFromSctpToChannel(...): " + << "Received packet for unknown ssrc: " << params.ssrc; + return; + } + + if (receiving_) { + LOG(LS_VERBOSE) << debug_name_ << "->OnDataFromSctpToChannel(...): " + << "Posting with length: " << buffer->length(); + SignalDataReceived(params, buffer->data(), buffer->length()); + } else { + LOG(LS_WARNING) << debug_name_ << "->OnDataFromSctpToChannel(...): " + << "Not receiving packet with sid=" << params.ssrc + << " len=" << buffer->length() + << " before SetReceive(true)."; + } +} + +void SctpDataMediaChannel::OnNotificationFromSctp( + talk_base::Buffer* buffer) { + const sctp_notification& notification = + reinterpret_cast(*buffer->data()); + ASSERT(notification.sn_header.sn_length == buffer->length()); + + // TODO(ldixon): handle notifications appropriately. + switch (notification.sn_header.sn_type) { + case SCTP_ASSOC_CHANGE: + LOG(LS_VERBOSE) << "SCTP_ASSOC_CHANGE"; + OnNotificationAssocChange(notification.sn_assoc_change); + break; + case SCTP_REMOTE_ERROR: + LOG(LS_INFO) << "SCTP_REMOTE_ERROR"; + break; + case SCTP_SHUTDOWN_EVENT: + LOG(LS_INFO) << "SCTP_SHUTDOWN_EVENT"; + break; + case SCTP_ADAPTATION_INDICATION: + LOG(LS_INFO) << "SCTP_ADAPTATION_INIDICATION"; + break; + case SCTP_PARTIAL_DELIVERY_EVENT: + LOG(LS_INFO) << "SCTP_PARTIAL_DELIVERY_EVENT"; + break; + case SCTP_AUTHENTICATION_EVENT: + LOG(LS_INFO) << "SCTP_AUTHENTICATION_EVENT"; + break; + case SCTP_SENDER_DRY_EVENT: + LOG(LS_INFO) << "SCTP_SENDER_DRY_EVENT"; + break; + // TODO(ldixon): Unblock after congestion. + case SCTP_NOTIFICATIONS_STOPPED_EVENT: + LOG(LS_INFO) << "SCTP_NOTIFICATIONS_STOPPED_EVENT"; + break; + case SCTP_SEND_FAILED_EVENT: + LOG(LS_INFO) << "SCTP_SEND_FAILED_EVENT"; + break; + case SCTP_STREAM_RESET_EVENT: + LOG(LS_INFO) << "SCTP_STREAM_RESET_EVENT"; + // TODO(ldixon): Notify up to channel that stream resent has happened, + // and write unit test for this case. + break; + case SCTP_ASSOC_RESET_EVENT: + LOG(LS_INFO) << "SCTP_ASSOC_RESET_EVENT"; + break; + case SCTP_STREAM_CHANGE_EVENT: + LOG(LS_INFO) << "SCTP_STREAM_CHANGE_EVENT"; + break; + default: + LOG(LS_WARNING) << "Unknown SCTP event: " + << notification.sn_header.sn_type; + break; + } +} + +void SctpDataMediaChannel::OnNotificationAssocChange( + const sctp_assoc_change& change) { + switch (change.sac_state) { + case SCTP_COMM_UP: + LOG(LS_VERBOSE) << "Association change SCTP_COMM_UP"; + break; + case SCTP_COMM_LOST: + LOG(LS_INFO) << "Association change SCTP_COMM_LOST"; + break; + case SCTP_RESTART: + LOG(LS_INFO) << "Association change SCTP_RESTART"; + break; + case SCTP_SHUTDOWN_COMP: + LOG(LS_INFO) << "Association change SCTP_SHUTDOWN_COMP"; + break; + case SCTP_CANT_STR_ASSOC: + LOG(LS_INFO) << "Association change SCTP_CANT_STR_ASSOC"; + break; + default: + LOG(LS_INFO) << "Association change UNKNOWN"; + break; + } +} + + +void SctpDataMediaChannel::OnPacketFromSctpToNetwork( + talk_base::Buffer* buffer) { + if (buffer->length() > kSctpMtu) { + LOG(LS_ERROR) << debug_name_ << "->OnPacketFromSctpToNetwork(...): " + << "SCTP seems to have made a poacket that is bigger " + "than its official MTU."; + } + network_interface()->SendPacket(buffer); +} + +void SctpDataMediaChannel::OnMessage(talk_base::Message* msg) { + switch (msg->message_id) { + case MSG_SCTPINBOUNDPACKET: { + SctpInboundPacket* packet = + static_cast*>( + msg->pdata)->data(); + OnInboundPacketFromSctpToChannel(packet); + delete packet; + break; + } + case MSG_SCTPOUTBOUNDPACKET: { + talk_base::Buffer* buffer = + static_cast*>( + msg->pdata)->data(); + OnPacketFromSctpToNetwork(buffer); + delete buffer; + break; + } + } +} + +} // namespace cricket diff --git a/talk/media/sctp/sctpdataengine.h b/talk/media/sctp/sctpdataengine.h new file mode 100644 index 000000000..9f9566645 --- /dev/null +++ b/talk/media/sctp/sctpdataengine.h @@ -0,0 +1,220 @@ +/* + * libjingle SCTP + * Copyright 2012 Google Inc, and Robin Seggelmann + * + * 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. + */ + +#ifndef TALK_MEDIA_SCTP_SCTPDATAENGINE_H_ +#define TALK_MEDIA_SCTP_SCTPDATAENGINE_H_ + +#include +#include + +#include "talk/base/buffer.h" +#include "talk/base/scoped_ptr.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/mediaengine.h" + +// Defined by "usrsctplib/usrsctp.h" +struct sockaddr_conn; +struct sctp_assoc_change; +// Defined by +struct socket; + +namespace cricket { +// A DataEngine that interacts with usrsctp. +// +// From channel calls, data flows like this: +// [worker thread (although it can in princple be another thread)] +// 1. SctpDataMediaChannel::SendData(data) +// 2. usrsctp_sendv(data) +// [worker thread returns; sctp thread then calls the following] +// 3. OnSctpOutboundPacket(wrapped_data) +// [sctp thread returns having posted a message for the worker thread] +// 4. SctpDataMediaChannel::OnMessage(wrapped_data) +// 5. SctpDataMediaChannel::OnPacketFromSctpToNetwork(wrapped_data) +// 6. NetworkInterface::SendPacket(wrapped_data) +// 7. ... across network ... a packet is sent back ... +// 8. SctpDataMediaChannel::OnPacketReceived(wrapped_data) +// 9. usrsctp_conninput(wrapped_data) +// [worker thread returns; sctp thread then calls the following] +// 10. OnSctpInboundData(data) +// [sctp thread returns having posted a message fot the worker thread] +// 11. SctpDataMediaChannel::OnMessage(inboundpacket) +// 12. SctpDataMediaChannel::OnInboundPacketFromSctpToChannel(inboundpacket) +// 13. SctpDataMediaChannel::OnDataFromSctpToChannel(data) +// 14. SctpDataMediaChannel::SignalDataReceived(data) +// [from the same thread, methods registered/connected to +// SctpDataMediaChannel are called with the recieved data] +class SctpDataEngine : public DataEngineInterface { + public: + SctpDataEngine(); + virtual ~SctpDataEngine(); + + virtual DataMediaChannel* CreateChannel(DataChannelType data_channel_type); + + virtual const std::vector& data_codecs() { return codecs_; } + + private: + static int usrsctp_engines_count; + std::vector codecs_; +}; + +// TODO(ldixon): Make into a special type of TypedMessageData. +// Holds data to be passed on to a channel. +struct SctpInboundPacket; + +class SctpDataMediaChannel : public DataMediaChannel, + public talk_base::MessageHandler { + public: + // DataMessageType is used for the SCTP "Payload Protocol Identifier", as + // defined in http://tools.ietf.org/html/rfc4960#section-14.4 + // + // For the list of IANA approved values see: + // http://www.iana.org/assignments/sctp-parameters/sctp-parameters.xml + // The value is not used by SCTP itself. It indicates the protocol running + // on top of SCTP. + enum PayloadProtocolIdentifier { + PPID_NONE = 0, // No protocol is specified. + // Specified by Mozilla. Not clear that this is actually part of the + // standard. Use with caution! + // http://mxr.mozilla.org/mozilla-central/source/netwerk/sctp/datachannel/DataChannelProtocol.h#22 + PPID_CONTROL = 50, + PPID_TEXT = 51, + PPID_BINARY = 52, + }; + + // Given a thread which will be used to post messages (received data) to this + // SctpDataMediaChannel instance. + explicit SctpDataMediaChannel(talk_base::Thread* thread); + virtual ~SctpDataMediaChannel(); + + // When SetSend is set to true, connects. When set to false, disconnects. + // Calling: "SetSend(true); SetSend(false); SetSend(true);" will connect, + // disconnect, and reconnect. + virtual bool SetSend(bool send); + // Unless SetReceive(true) is called, received packets will be discarded. + virtual bool SetReceive(bool receive); + + virtual bool AddSendStream(const StreamParams& sp); + virtual bool RemoveSendStream(uint32 ssrc); + virtual bool AddRecvStream(const StreamParams& sp); + virtual bool RemoveRecvStream(uint32 ssrc); + + // Called when Sctp gets data. The data may be a notification or data for + // OnSctpInboundData. Called from the worker thread. + virtual void OnMessage(talk_base::Message* msg); + // Send data down this channel (will be wrapped as SCTP packets then given to + // sctp that will then post the network interface by OnMessage). + // Returns true iff successful data somewhere on the send-queue/network. + virtual bool SendData(const SendDataParams& params, + const talk_base::Buffer& payload, + SendDataResult* result = NULL); + // A packet is received from the network interface. Posted to OnMessage. + virtual void OnPacketReceived(talk_base::Buffer* packet); + + // Exposed to allow Post call from c-callbacks. + talk_base::Thread* worker_thread() const { return worker_thread_; } + + // TODO(ldixon): add a DataOptions class to mediachannel.h + virtual bool SetOptions(int options) { return false; } + virtual int GetOptions() const { return 0; } + + // Many of these things are unused by SCTP, but are needed to fulfill + // the MediaChannel interface. + // TODO(pthatcher): Cleanup MediaChannel interface, or at least + // don't try calling these and return false. Right now, things + // don't work if we return false. + virtual bool SetSendBandwidth(bool autobw, int bps) { return true; } + virtual bool SetRecvRtpHeaderExtensions( + const std::vector& extensions) { return true; } + virtual bool SetSendRtpHeaderExtensions( + const std::vector& extensions) { return true; } + virtual bool SetSendCodecs(const std::vector& codecs) { + return true; + } + virtual bool SetRecvCodecs(const std::vector& codecs) { + return true; + } + virtual void OnRtcpReceived(talk_base::Buffer* packet) {} + virtual void OnReadyToSend(bool ready) {} + + // Helper for debugging. + void set_debug_name(const std::string& debug_name) { + debug_name_ = debug_name; + } + const std::string& debug_name() const { return debug_name_; } + + private: + sockaddr_conn GetSctpSockAddr(int port); + + // Creates the socket and connects. Sets sending_ to true. + bool Connect(); + // Closes the socket. Sets sending_ to false. + void Disconnect(); + + // Returns false when openning the socket failed; when successfull sets + // sending_ to true + bool OpenSctpSocket(); + // Sets sending_ to false and sock_ to NULL. + void CloseSctpSocket(); + + // Called by OnMessage to send packet on the network. + void OnPacketFromSctpToNetwork(talk_base::Buffer* buffer); + // Called by OnMessage to decide what to do with the packet. + void OnInboundPacketFromSctpToChannel(SctpInboundPacket* packet); + void OnDataFromSctpToChannel(const ReceiveDataParams& params, + talk_base::Buffer* buffer); + void OnNotificationFromSctp(talk_base::Buffer* buffer); + void OnNotificationAssocChange(const sctp_assoc_change& change); + + // Responsible for marshalling incoming data to the channels listeners, and + // outgoing data to the network interface. + talk_base::Thread* worker_thread_; + // The local and remote SCTP port to use. These are passed along the wire + // and the listener and connector must be using the same port. It is not + // related to the ports at the IP level. + int local_port_; + int remote_port_; + // TODO(ldixon): investigate why removing 'struct' makes the compiler + // complain. + // + // The socket created by usrsctp_socket(...). + struct socket* sock_; + + // sending_ is true iff there is a connected socket. + bool sending_; + // receiving_ controls whether inbound packets are thrown away. + bool receiving_; + std::vector send_streams_; + std::vector recv_streams_; + + // A human-readable name for debugging messages. + std::string debug_name_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_SCTP_SCTPDATAENGINE_H_ diff --git a/talk/media/sctp/sctpdataengine_unittest.cc b/talk/media/sctp/sctpdataengine_unittest.cc new file mode 100644 index 000000000..071fbbb02 --- /dev/null +++ b/talk/media/sctp/sctpdataengine_unittest.cc @@ -0,0 +1,260 @@ +/* + * libjingle SCTP + * Copyright 2013 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 +#include +#include +#include + +#include "talk/base/buffer.h" +#include "talk/base/criticalsection.h" +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/messagehandler.h" +#include "talk/base/messagequeue.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/thread.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/sctp/sctpdataengine.h" + +enum { + MSG_PACKET = 1, +}; + +// Fake NetworkInterface that sends/receives sctp packets. The one in +// talk/media/base/fakenetworkinterface.h only works with rtp/rtcp. +class SctpFakeNetworkInterface : public cricket::MediaChannel::NetworkInterface, + public talk_base::MessageHandler { + public: + explicit SctpFakeNetworkInterface(talk_base::Thread* thread) + : thread_(thread), + dest_(NULL) { + } + + void SetDestination(cricket::DataMediaChannel* dest) { dest_ = dest; } + + protected: + // Called to send raw packet down the wire (e.g. SCTP an packet). + virtual bool SendPacket(talk_base::Buffer* packet) { + LOG(LS_VERBOSE) << "SctpFakeNetworkInterface::SendPacket"; + + // TODO(ldixon): Can/should we use Buffer.TransferTo here? + // Note: this assignment does a deep copy of data from packet. + talk_base::Buffer* buffer = new talk_base::Buffer(packet->data(), + packet->length()); + thread_->Post(this, MSG_PACKET, talk_base::WrapMessageData(buffer)); + LOG(LS_VERBOSE) << "SctpFakeNetworkInterface::SendPacket, Posted message."; + return true; + } + + // Called when a raw packet has been recieved. This passes the data to the + // code that will interpret the packet. e.g. to get the content payload from + // an SCTP packet. + virtual void OnMessage(talk_base::Message* msg) { + LOG(LS_VERBOSE) << "SctpFakeNetworkInterface::OnMessage"; + talk_base::Buffer* buffer = + static_cast*>( + msg->pdata)->data(); + if (dest_) { + dest_->OnPacketReceived(buffer); + } + delete buffer; + } + + // Unsupported functions required to exist by NetworkInterface. + // TODO(ldixon): Refactor parent NetworkInterface class so these are not + // required. They are RTC specific and should be in an appropriate subclass. + virtual bool SendRtcp(talk_base::Buffer* packet) { + LOG(LS_WARNING) << "Unsupported: SctpFakeNetworkInterface::SendRtcp."; + return false; + } + virtual int SetOption(SocketType type, talk_base::Socket::Option opt, + int option) { + LOG(LS_WARNING) << "Unsupported: SctpFakeNetworkInterface::SetOption."; + return 0; + } + + private: + // Not owned by this class. + talk_base::Thread* thread_; + cricket::DataMediaChannel* dest_; +}; + +// This is essentially a buffer to hold recieved data. It stores only the last +// received data. Calling OnDataReceived twice overwrites old data with the +// newer one. +// TODO(ldixon): Implement constraints, and allow new data to be added to old +// instead of replacing it. +class SctpFakeDataReceiver : public sigslot::has_slots<> { + public: + SctpFakeDataReceiver() : received_(false) {} + + void Clear() { + received_ = false; + last_data_ = ""; + last_params_ = cricket::ReceiveDataParams(); + } + + virtual void OnDataReceived(const cricket::ReceiveDataParams& params, + const char* data, size_t length) { + received_ = true; + last_data_ = std::string(data, length); + last_params_ = params; + } + + bool received() const { return received_; } + std::string last_data() const { return last_data_; } + cricket::ReceiveDataParams last_params() const { return last_params_; } + + private: + bool received_; + std::string last_data_; + cricket::ReceiveDataParams last_params_; +}; + +// SCTP Data Engine testing framework. +class SctpDataMediaChannelTest : public testing::Test { + protected: + virtual void SetUp() { + engine_.reset(new cricket::SctpDataEngine()); + } + + cricket::SctpDataMediaChannel* CreateChannel( + SctpFakeNetworkInterface* net, SctpFakeDataReceiver* recv) { + cricket::SctpDataMediaChannel* channel = + static_cast(engine_->CreateChannel( + cricket::DCT_SCTP)); + channel->SetInterface(net); + // When data is received, pass it to the SctpFakeDataReceiver. + channel->SignalDataReceived.connect( + recv, &SctpFakeDataReceiver::OnDataReceived); + return channel; + } + + bool SendData(cricket::SctpDataMediaChannel* chan, uint32 ssrc, + const std::string& msg, + cricket::SendDataResult* result) { + cricket::SendDataParams params; + params.ssrc = ssrc; + return chan->SendData(params, talk_base::Buffer(msg.data(), msg.length()), result); + } + + bool ReceivedData(const SctpFakeDataReceiver* recv, uint32 ssrc, + const std::string& msg ) { + return (recv->received() && + recv->last_params().ssrc == ssrc && + recv->last_data() == msg); + } + + bool ProcessMessagesUntilIdle() { + talk_base::Thread* thread = talk_base::Thread::Current(); + while (!thread->empty()) { + talk_base::Message msg; + if (thread->Get(&msg, talk_base::kForever)) { + thread->Dispatch(&msg); + } + } + return !thread->IsQuitting(); + } + + private: + talk_base::scoped_ptr engine_; +}; + +TEST_F(SctpDataMediaChannelTest, SendData) { + talk_base::scoped_ptr net1( + new SctpFakeNetworkInterface(talk_base::Thread::Current())); + talk_base::scoped_ptr net2( + new SctpFakeNetworkInterface(talk_base::Thread::Current())); + talk_base::scoped_ptr recv1( + new SctpFakeDataReceiver()); + talk_base::scoped_ptr recv2( + new SctpFakeDataReceiver()); + talk_base::scoped_ptr chan1( + CreateChannel(net1.get(), recv1.get())); + chan1->set_debug_name("chan1/connector"); + talk_base::scoped_ptr chan2( + CreateChannel(net2.get(), recv2.get())); + chan2->set_debug_name("chan2/listener"); + + net1->SetDestination(chan2.get()); + net2->SetDestination(chan1.get()); + + LOG(LS_VERBOSE) << "Channel setup ----------------------------- "; + chan1->AddSendStream(cricket::StreamParams::CreateLegacy(1)); + chan2->AddRecvStream(cricket::StreamParams::CreateLegacy(1)); + + chan2->AddSendStream(cricket::StreamParams::CreateLegacy(2)); + chan1->AddRecvStream(cricket::StreamParams::CreateLegacy(2)); + + LOG(LS_VERBOSE) << "Connect the channels -----------------------------"; + // chan1 wants to setup a data connection. + chan1->SetReceive(true); + // chan1 will have sent chan2 a request to setup a data connection. After + // chan2 accepts the offer, chan2 connects to chan1 with the following. + chan2->SetReceive(true); + chan2->SetSend(true); + // Makes sure that network packets are delivered and simulates a + // deterministic and realistic small timing delay between the SetSend calls. + ProcessMessagesUntilIdle(); + + // chan1 and chan2 are now connected so chan1 enables sending to complete + // the creation of the connection. + chan1->SetSend(true); + + cricket::SendDataResult result; + LOG(LS_VERBOSE) << "chan1 sending: 'hello?' -----------------------------"; + ASSERT_TRUE(SendData(chan1.get(), 1, "hello?", &result)); + EXPECT_EQ(cricket::SDR_SUCCESS, result); + EXPECT_TRUE_WAIT(ReceivedData(recv2.get(), 1, "hello?"), 1000); + LOG(LS_VERBOSE) << "recv2.received=" << recv2->received() + << "recv2.last_params.ssrc=" << recv2->last_params().ssrc + << "recv2.last_params.timestamp=" + << recv2->last_params().ssrc + << "recv2.last_params.seq_num=" + << recv2->last_params().seq_num + << "recv2.last_data=" << recv2->last_data(); + + LOG(LS_VERBOSE) << "chan2 sending: 'hi chan1' -----------------------------"; + ASSERT_TRUE(SendData(chan2.get(), 2, "hi chan1", &result)); + EXPECT_EQ(cricket::SDR_SUCCESS, result); + EXPECT_TRUE_WAIT(ReceivedData(recv1.get(), 2, "hi chan1"), 1000); + LOG(LS_VERBOSE) << "recv1.received=" << recv1->received() + << "recv1.last_params.ssrc=" << recv1->last_params().ssrc + << "recv1.last_params.timestamp=" + << recv1->last_params().ssrc + << "recv1.last_params.seq_num=" + << recv1->last_params().seq_num + << "recv1.last_data=" << recv1->last_data(); + + LOG(LS_VERBOSE) << "Closing down. -----------------------------"; + // Disconnects and closes socket, including setting receiving to false. + chan1->SetSend(false); + chan2->SetSend(false); + LOG(LS_VERBOSE) << "Cleaning up. -----------------------------"; +} diff --git a/talk/media/testdata/1.frame_plus_1.byte b/talk/media/testdata/1.frame_plus_1.byte new file mode 100644 index 0000000000000000000000000000000000000000..b619edeed237ffe4f448820b412b70297ab4818e GIT binary patch literal 153641 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 EAKZJw>Hq)$ literal 0 HcmV?d00001 diff --git a/talk/media/testdata/captured-320x240-2s-48.frames b/talk/media/testdata/captured-320x240-2s-48.frames new file mode 100644 index 0000000000000000000000000000000000000000..292a1700f154d1763ccb574cd9d3f135789c85a0 GIT binary patch literal 7374720 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 (k+mu%4=0M6}ji+bxVFP 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

>uhQ)|u+1(pWWNO@qG$ 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

    <#?dlI7oamx!vM!4)KrMFBP|Q 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%0AD_z#Nz?4T||Du4F&`GfoL!{7`0_aOJhm1n#Ww! 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

    K9o;T;fu^u7=V0Dnb#C#{p- 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
      -C3iKmC09-*5jN z`p7%_RsSLRAJc#4|F7HMuLtjjPoBN_a_Ph8KVAKc=f7I}^!bn0lMlu>=U?Qxt3J)W z&tB*~(wAmt=rohX6I(Oo2hX%sfI16$LiHSjIGS2ljeTy|!6E!_zi{XB5OP3l8`rIw zT5=Qh7gQ`_;qQ%jEWWGWyK2+KcjO+{H&70Fh3h~qN3jsLN^ujql=K>yLxm%HE&S)` zVrUQ2uj5b4&%(qnM-}fy@{$^6uXmz)4L_+|275%Tbw~61F4i+FsxE2RlKkS<9*iA4)tw!;mX7Q-QP=Dvo$T8G=mj6}03-+*o=C3<=F4-^5E1RF^ePi2r zE%GXAgf2HW8)>+gO{G81->2?mc%yg6BcAOQ{uY4CW{d_~l~cn8!g0pt!Gm_#D}X~` z5xeK`R|Rj_K~MPGDlukj7zBI1@CR$u9lmEXsd%nB*uQwvn@lFllL;zh$r#h-BkbM; zgAvsUYJRA9`6$TwU{Fh;^dx-xAEuktud0ueuPX~*e)Hpn^yQ03FTQ@Z zSos*8j!!e}UiqxYSwMQ(`kb+60Z^N%l-Vu1*J2?@qFTr?1#6`zAkA z&v#X87?iFWG2Z|)=PuW~$F=UnK3wHomX>;Of*HMiunG>OgK;-IwWY)6YR}j`*+9!9 z`~9JcG&~d!iA$qC$~ERlF8N22$>6_A{!RIBEB|fjud9Did|6@VdiGQQXX&5(B;BoE zF4s^wfoa*5JJ35)&e4s2n{r*{A;;BDa^4ubr<34!v3p}+kVkwljEGo@Nx~qL19tk$>O|9MI^+kzg=)$k1j;r_eY|KR-%I|2XU*4O#ZvQNB^lTU*$ z(%&$T+r&Oa*$_1A>``@;4tQ5+0afn?mTwrIW&5ys_+I>->x*D__;>i#9eb!*am8ZH z8yo(ZF9(0E6?BHI2jcP+{I1uPTAf;Prg9v^xO}_*eFwkO#VY(7>?Zse=ktm0R8zFR zowIumd%~Z+pWE2c5fAG}9}cbnYL6VEahv^M{sa8sXXRf@^0)G{*f=jo+?V5L@T~j| zY~`l;UliJ9>ty%L1`30(@HbVSB;UOs-!G5HqvRaQJJ@!vM|S88MTS3=yZTYml$xeH z;EzeZ?&MwYm*%QJ-Co}Q`MVFFzpOk=zeyifzj>FFK8(QM+E=p=dp;g`I!vu(d}{

      ZIn_Cm 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

      0CXt`_(IoO>b4Ald*HAO*;4sQf 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%0DK<8^c)oBdPX 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%0DcGZn_pwB3?D)J4~1ePpH8qvAhop~9bNW{BMr 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 z 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�l6x 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
    1. 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�i){--i*V-K7Y66s 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 z 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

      V~ssavc#ZBUR@KHBtP$>RCb1?c4u3dQ?zF<5F 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%0DsMnl#&nb`1{z~T;y6M6uH)j>$pvd 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"&am1Za0aIPP4etfuFG3Cbqg8 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�~0`rxpb7N$h9!@1y7=)jLm5Ge>nwI8lx;d4hR2 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%0D!rmywJwqujO+TlMxaZUDTj1W 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 SFz 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

      qP8{=~)K_jsnR=1uki3sgqg0<8k%zoL{B-rZ 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

      ~>BDXV?Yd!O=PF7GqanKlHEn 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 wKDR*g=SMk%y8<> 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

      eHvF`Mpg2RFLo2K7q(I&gZ2 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

    2. @<(oe~*hWG@W$p>uqC@LRTXH(pV?X#JG{2#G@ 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%0DE;1W;Bl~3XWcFr!D|;)tnR_BZ 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%0ASxKz%LqBsf&fhY6`XVU)kpB$e&;3$* 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

      Bk!2j~Q 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 z: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

      b!JgD| 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 r0hIe?i`l`L9 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

      fmZqrtf5$_gXp_t>K_E5qsu#n8edi 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

      H<_DmJ_aS`#afFs~fs4+UEcJ;h8 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 literal 0 HcmV?d00001 diff --git a/talk/media/testdata/h264-svc-99-640x360.rtpdump b/talk/media/testdata/h264-svc-99-640x360.rtpdump new file mode 100644 index 0000000000000000000000000000000000000000..ffa521df06694c2a0587ba87d3dc5605747edf40 GIT binary patch literal 1058252 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 z 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ċ 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

      )Fs$QJ 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]B%Ve?U0#X$?CNK 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 z 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ϡqf-sI$ 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

      ;qrbnq41ten$vUwVau&EJ z`v$7Fqc$}*w}qypyF!+N_y9s}XhKu9>l4g&f&jfQ8l;YaHi7jEPS7Ia%+n*tJ5Z7R z@rdvP+|Xf#%CYAKP4BWJ(B`Ur0DjS}>fZNSVXmL$B@ja>Hy6vZroh`@!{(pF=BL7D zL4FM8#s-Y$LEFkHsykqpZjaV{AmB)c-4SZn6U@!ME%-DYFo;8u?i&j*_z9I`%m$w8 z&l&b8&{g+5NpT>B^(2@gZCI2mC<{pecyii(0^kWbce_fDFk6GhLmApcC>-FH?iURk z1kGbs4>lS?@v&`=XWgZ@pJMYjRwMmGVKaD3z26b(XAgJCMGZUUAEhGZfZ89 za4uRMJR%k((fA~Or&ATKt~U_BvP0ne(1h`2yV@Q*8;0mT>e^hU;(RuK1)F~gn>WH{ z<^j^)FGP7l?INfV(X@tMvGd?HKS2PSM)`8KFf{|>WL`%xDQ!4hO#t5zl~Hx*8ZFgQ2+$;EQf*7nqa^*I{q5MMf2)d|hof zy&A-uxax-@-W}oXY~u5r=Gy}Q{L`zE@u{%6(%HTiA8C~h-%!JnXqS!2lafJV$E(t3 zgan-rks?f$`QfBbC4y3L#!E#99yfqxb3@zI!uT$cZfwZJQKR8-c`n|zFFHq~T!*X< zpkrI=2~?Uxw^W-$3z@mKlm`IgEqr+@crkhD9`=nilU1WXq@)Rz^M&7dz~-M>jm!^( z%}^p%!0CW4w%oNEcFpS4Lrco2LzvgTmmjUEo@_Is1x`kh1&CiDqvAn9Z8`NW0TIkk z=%A711|z~yWDQufbaxbBxdf)rBFf>wCZ_S>q^+oo$?OYE^OCpgtCnjz!6KX zi(1%3^3{t=E8Yfk8OKx+CTt!ub!IUi+(1rg7a8L3{N?1}$W|Kd&$>(R{C;fy>)8BM z*bH-|1HWl}w{^xONy)&YvQM1JJc!l_s!)i{dri>-fA6%(RB39a6*LEea)lScF`BG< zLs#7rgg_^ZegKrauHO<=A2Qz#jwFYBo!5vVQ$qX$T0FkLAaBO8gRvh1V;LUO#&l5+ z!!-scvL6n4pn?ZJrp@A9%YEmMV)NfvjhqjK%~Oa^g?c^IpG`%L)2R=;IM7uaZIIqD zh%CGO01KwZvgl^WctUl-AA=KMC(#j0J>(RXwwCxokoX_(9$A2a{+}j zR~IFDEx`9f54*3s^os$?WmOHcYH*edi<#Ydx7{S6-yU}bSwON)4av%n{rNmU!sfq; z%^P7e^U;C7kgIKEFD>LPDkLUy%3YAO)(`7W(b<2G*S9Pj*fn8BCpS`2oB%2;DOL~U zSAsr0hAlQHy<#Gc39M6!s2DI^VrSdsMVvEBLyX>PiwWf02pHrJ46#YKkmCnJ8Kduz zhGK#Xv5s0@kb#=g5Jf!%@A?_f|Bi@}{?=;bekyG4)tfPRV|6}Z-%X+VMs@&Ux*_-R z?aMnRZMWO?h8Qb5-r?`+8nhh#Ckj5JP^2&I5 zigHzh_uR!vB=>izhk8!#t4Our_L1ds%c@|klT?_Y$f_>ehTykcK6xTJh&n0rv%b|k zKY-1Ddo}Vt5H@Q>oNk>ImdT)mq^kyj-t_7UN^0^S+g-P`Yg1V`b!*}pk8wfBf`s07AJ|J}IMq5;ZCPf>!$p}*+dV|21)H=+ z&PX+2Wgd8Rb;mZ3>pP(Fd6F^(qf`lEeer&Cdp;ZAz~;Y$&7TRIw{3MK!p{d|>E&cr zIs!0cwVtSiPCxnRLUJ9|&V*FneFW;h*KLvVPmqn04D?>mrbAjzoj7|uqNdAESHpLp zkTx^LaAG~HyguALf4vV3L_ZWJD129;T{B~#cQLui*ROW^9GRK{`+czL$Gg~=&XT*t zr%uZ1pWl>sejb}wtC2quHg{aZK4Acep`{iZ==_3S>T2a9TC#qX<} zGD{u%l$Dl;3QBPK$(5~KJUX9^-^Avp*!)!3{NxH{L6gxQF!clhvN2ywRk!XaQPjz1 z_xi>2m(O0`pGcAVV|*~iPbPKL&9CHaO+ww;4B(N=z@}~L%5wJjlTWUn%$jY~nCU%S zC2@lM)gF##bvU)ke8Hs8T(5UXfH9lmm5^ePnJ2+~vLM^$UNXQ`g?ak$;x*;Y=;%zZ zCuf@l{`u!tqwqsvvx)*FbpWJ%l=Q`S))h|nRP-UU=2MBcLt8Km)I(}+lUXj|51^Kz zu`6VCK2fQgcuRtP8|}twd!H1$SFi5xUw-!`PG)m|ux~CXtxHn3%iLgl{}P41L&Y1) z+?c}@Ser+;uOgWJdqA3Cm z@_h{S9DQ>6hHs+ikP-w$Gle+nW58B_IlNMJes>4`p|i zN0d6_hb?|`y@6AtpHHVZAN}#~MV%TSGt(HIf)0Sb0;7> zhulI`cns5kX)rg61jIfk$119h=t){kmRb|amscKt;U2}9Y_p+L`S@ZyMMDkkR;RU? z9lU#>H4~wgd3#%4efeWa;{d4!>T>ml()#H{l@Jz=V_G&Elc@B9^`WTMn`yV%NuQi; zeuB+EzZ!kzEn)MM`2f=lcnX6+xR8_%Fa;7BLasG)mkXouT?)wV{{D6YS3LRw9P8S* z(1fvr-VKm{>5;>|-L=cF*0*!)!3%$%&eAkMFmP0DSD zsD2^o5cXWmEkrPSbV^Rqi&xwI_HMtJ6fd7A)Qop&LvHIKMF_ZxzaZO#Z^>Sr9-Gki zWZ`R&|Y44usF+@7hBdbIiOHP0b}abfQ;Xj-@oxh^y-JyCOnkpCzW%@*0VBKOL4 zvrBkN-Od2Ov|tRL8OgSz7p9xkc~H7UiUIPajH8c^Z^DyNZq{d;zlqKN1e-U)=F~#k zfwYoQJVirp6j5TPxRuFRYX((;Z(p{M5+TTS7@g}VM+`NinL?nUgbe0*I;L;*2~i|q zK9X`T0ve0)qxv%o+9)79wDekyD>4!JzXC)kFFgD4l2Nt$a=OJzN5NeAcZT9I!) zemtd`L9%j6_9bGU{KJ4bt`~hqx!gmDEud0sdu7)h=*dV{aUj{R_`KSaAwFQXe{wUW zkmoh=?W=8(?{05*`F^|J*Hyg9oRFa4>dHFA!Q^Ic?(VmlO;}8~=4Nv4556n#&%dx5 z{kac>&9gC}rpGg+$Ecs}qO9HDzP!)vGTNe_eM%j5u|QK~etEU10^EmP)Kzt2SU-ki z?fH8Boo_uqC`P(B#$%+DrOB7Se;h~7#V2!OP5d*057zSXqet*+k@(1Nx;yM}*DqeY zTu1p}H*1gvh-g~yAuKe6X0C6T`SYg0w{HEJd3g2ei?i`B!~bBd74R(ykD zM%0B2G@z*ko_lTY?y>owuSP#G5jHccT_O8JGQ{+0+mLeWLyPv^E-9BY6dtAPiBwYu zyS`TG=AH63y?^nbKE4jtYpTf}#gyqnA!I`)$4{Bt#y>fB59}x1)rzt};$>mhb&KvU zsu$Ba0x8Q1(wLNA$T(<_&{yBpr|9MD9aXXTKC;k7rS)_I|G~3dopadtej7Ibi`D4Q zza?x|^{!$6U6N5qTqKP{iE5VT_R4gQz?>{AeskPDY*4hH`su!vTsKV*ZGD7AV=#i8 zRYTyLCgXJa!#>}vx9B190y_K* zdrI4J#=A~m*73V&dkAQ*xs3QGnJ+80O+^DS#(*6lzK0|(D)5=LsB?JA!k{cF1r&<{ zzeI4#_s9m>6KQ;L-5{KofdosoFP`05Pd>h!m;huJc7dQX#8}Us-EFpHgKYJgGvvMB zjm^Kf8vTV2h0X3@6QfatV+cLa)cJ)FKPTT?D#T`p)(jyvMFeyxg*p}k-5TMoTaZ-T zKYQ`~*~3mK0iEnQOi9OW%nbcLN*lC2KY2o2_2|*#tLc#@bDr+jciW6Q-X=AnJ-S+u zV|#Y`@bbO_rGVIxN=0KAm)-$FoKuHpnSVXg5h_3H@V@s4vH6#68T!}w~|%vnx$EmU2@paFVsxj|8%j;JBi;knOm^?t>M2{|7M+Is$QwlV)wBSk;t3mYO zI0Yi6Qt@wyIe-7;GhqN}kQB8Oa7sh_UFm%;?_#d#L^=U-lp{^AG1 zX4K76j?jZ|m|0pXa0zq{6{2P^renQK&>#zZmls!|Vh*4P>bE?1OnFe7KC9Ka8Q<^Z zEBG|fSch8Sm?JnABzT|>ahOVxy%x@fLYnpW+lD^uwlw@^kYw%D&n_3y{ei&Bq(H|I z_Ms}IxePgn>In*lU6Ik~Z1?ANcCqg78kTU4U(&rA9AaC`JR z>aKf&_E+irf*@mVw`iUOKpok4N1Gt!m816EmFwN1E%$?G?>9SgJwu(1#clI;h^x~~ z5*=A%!Sr^_Ihd0sSgdxBB7L2|c=_z*%R98R6;+;JPh|%p&6_y{906!1svPII?!B*K z^S@q=esCgergz}gt>@MbP^~^>%-!odg$~N@0QFH!ZL)%6Q!nML2~=BDYfN^!UY<(R*yg~qx4hQ zt?(9s-X*uMNih$O#pAZnVLp<94X8G~-SavN{PVwAjsDVG!e$WR&FLY^$&NQ$6wn_s z^2&#Lp$bYzv|4AEA3s?RT{!3f$){G4QT9Rda~Lw{wR!}SjJ;^rWJKMaUp_D z)!h{Z2q^a(O>>h*t{Srm_i*j z_ZBkmpkI+G?BOv?Qm9sro7jc#3Wh2lG)x*laQwDg6!G$+JD%$K43#+Yw;9xnb32Q& zD&8dyl8Lh?k3M-kxt>j7;Gv~RgZw1R#X70Cub;nu@!}cGLvwk0eFb~p1)5GBuAV(Td(KF-JJPJ~iX>oog^td0dG58nC-Bez zel_|lp9-4;c|LO})=~XDm6ZUi`SLK7K}GRb_Nm4TfJ;YnA|;`PLqxLCiq&M_L=T=O z3W}tJ?Hm{?2k3<->%j@45v~hohovO2G>uU=>BVfaEEKmB!`Cqxw}uyUw&>8ggk+ zLw9MmJSG4Q4BBFzVmTU}bR#(5!S{bVHvfm!=!ZTKHn%Vo(a9uiIQphvoVrdAT8oe( za1FQZblEHl3o+!z0T@BP-FNM&38?Tpp00^LLLD#o%!^5ue9RL6Xt8kf8 z@b?kK1P?(T#Sdd5n% z=kOiZB_N|$=J5F#*dsaLz|CSepgZdZ;6aDR9~>+VIlpLh`XRpqFocItVh`U66&K($ zoFvFb5aw(>->3IYN3yJYzO_Z)mLu6wb%IZZECJfQkt=zwSVBEjw;J*iRNZ5uMkK&+MH0W1+c>l$bErb*61 zc|HI3!LwN)SiEzs;QjB#=FhPCsj%5+wdfl+b;n1b!d&}Z$Ade-CB)hhYXq&JR0%>B z+hqL#?R1^^Z9`3O^UW7GzyrXSwITo0X6ypur-DQfQY6Ze?cnhja34KuNKcQ5+Yj6> z>|M`CK{({Yv6OEg*b07s*HIt^q++7837ygTYzX}GudPNu@}aPq{ATb2TDk!XIDq7G z_$U1!2;O{rSqD@Oc^CY$GLfln0ROSh$VSrC4{ZJ+E`7-QbsPd8K}_8Z8yrF1vqdek zuY6iw4t`jnuFaVN^Z^h6aagvD)(bll7NMoPgE)*Gryp{(f!-tQ;SNI$LDSjh5S#xC zHgAN@mFQ@ezBz#BvcNX6y)3lD-thr7pX@lC0h9a4cQI4J(WS^57C^Mnz^*A%7XS%p zS)&^Zr~;~_*7ZDk?^X|NKxk^){3c+IfVoVNS+oY|yP>E`j%5Hbq2g<>W_-zsItAFs zpr~@!?G7cjx80e8{e2N5{nyp#M?ZaYHXOFYw^$FlFAtsx$plhHvhNP=*K<~Hq*@K z^0ox|pojVZl!wuCI=#RNL^D<`%8)8m?zxKNwH#@WJHv76eTvK+h#=2c*8tBA(hjsZ z%ZHpJE(jM15(N;dpkU6$eJcvw*MO2J&aGuaj?dtrRF2uE6;3Zp1E&EJ+dEt)I)JhzLQC}dC3mp)J;GR7nMkv>HtL)f;Gy?bIgF2}OP4B^F`l3PL2tnuu z(k#p8Kb0=CWXbIs%i- zllO47J086bX22$F>*9c!0VVvvLs+x2bSI-rY*5zdnX{SBP zTf*il4B7)6Yys^(_Jenpt!A3gHeexiT99wD6@pu^s|smiXQ&2sq4zCARy-dR26<(9 z0D8KW75@jww5s8}R!tkS?6`AKbWH>7Z3v1u?If=_P^Sc&1CchB%D(Br^|fVFj za~hsx5#RI{h^x>u&w2Oz1vdW%Ha~r{&aS|Y85W-acVmkKqGV`WKrR)Zk{3RJQ^Ls_ z2HuT;Lt?qS6@h7R=3FNfZ#V9oh`RPP@U#K+mZui^@SLDFY`LI$OMlZRJmB%>sqowo z*KheiWng{B}Z45kH95$XX%;S5_*?Ca5#_iT=%?!Tr&Pr$y3-+@HYi`0awTJcr0-!qNk4Fk6RuR zMD(7(kQ1m3K2M#;F&PFvk9uWW2mBk*9=@|GGUS}mMsA5W_yN3md{@IK!5GM2)BwqOyDmjn&qFL=P&&}Z2sS?(NBCTY_<&sGU}dJsOM^V7;WGm2iz5VOOVsxwOTMj zZ+?_u3FOCAJ;$l;2p~GgVSCG-2!Su;B82>jpHAoo7=vJfz^N)YwrqR{^uHTA-f4~* zzboBuF2|DV<9CqEE2dtjq*KLMw#t@&3? zeHpJ>f;g<}hLg(6%Z^o@YY|f5_SgZAga~U0bE>xI74}8cXWtxW?>M&K<^P!bA43VPCllM0*6fxI&J35U_SwLo|aw@OckH7LNZ04-vW+@f-io zkfZ52uFIH$ZDNQ%S2KClG*OMJ8^?u}TpP+Sp#rtAj4<m-GeVM01N3BxI>l`SK&FfL)I}2 zSUunvSZIzVhp1}t4wOY(o(u<1fdlIC6`UcFL_2}cW5~X6b%hTZnwFQd!_x-5Anaj3 zutJ_gB4819o3_URqV8}$8(+cZ|F;_b)LX*lfaUP`!9l=L5NEY@x%?Fg7oLba*wrGe zC&u;p6MxKU?Xc4<5X<23JJ<<9P3wogD)5cOW4;@Rm$Cse*b(N`1o{Dr?s$lReH?g9 zoiN}TU>gDppW|2@(5?>HF!A$6j?#t=yU!P#&RX@t`KJz zhpxcUutXs)1SV~pqrvjW4x0NHyp)?Gg?-Ra4I1dWWgV(b5P3z$=35)TCYQ9i6a`Ky<-fjv0#jZX#hHbXiVVP8d!W) z!$Uota;O57Oj1lQ^huU8NMOrjnx2}W$95;*o^K0ge}7^gA$tf z@NBc-fBx=j^vw^1&D38K#$9xvQ>x6o_>~qtsE;o5b#OiP``NX+TjyZlefaltuS<@G zWM_$^cxgBr``fh#sNnuKa)2}&!Rv25w?3ZjXxN|P)BJMMtdqr~OR27bu^B=Yg^!So p{A59}a97W!)x$36w|A$-)Z5+1zM-)SCZQ2N#(A{|7}5od*B_ literal 0 HcmV?d00001 diff --git a/talk/media/webrtc/fakewebrtccommon.h b/talk/media/webrtc/fakewebrtccommon.h new file mode 100644 index 000000000..026ad10b4 --- /dev/null +++ b/talk/media/webrtc/fakewebrtccommon.h @@ -0,0 +1,66 @@ +/* + * libjingle + * Copyright 2011 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. + */ + +#ifndef TALK_SESSION_PHONE_FAKEWEBRTCCOMMON_H_ +#define TALK_SESSION_PHONE_FAKEWEBRTCCOMMON_H_ + +#include "talk/base/common.h" + +namespace cricket { + +#define WEBRTC_STUB(method, args) \ + virtual int method args OVERRIDE { return 0; } + +#define WEBRTC_STUB_CONST(method, args) \ + virtual int method args const OVERRIDE { return 0; } + +#define WEBRTC_BOOL_STUB(method, args) \ + virtual bool method args OVERRIDE { return true; } + +#define WEBRTC_VOID_STUB(method, args) \ + virtual void method args OVERRIDE {} + +#define WEBRTC_FUNC(method, args) \ + virtual int method args OVERRIDE + +#define WEBRTC_FUNC_CONST(method, args) \ + virtual int method args const OVERRIDE + +#define WEBRTC_BOOL_FUNC(method, args) \ + virtual bool method args OVERRIDE + +#define WEBRTC_VOID_FUNC(method, args) \ + virtual void method args OVERRIDE + +#define WEBRTC_CHECK_CHANNEL(channel) \ + if (channels_.find(channel) == channels_.end()) return -1; + +#define WEBRTC_ASSERT_CHANNEL(channel) \ + ASSERT(channels_.find(channel) != channels_.end()); +} // namespace cricket + +#endif // TALK_SESSION_PHONE_FAKEWEBRTCCOMMON_H_ diff --git a/talk/media/webrtc/fakewebrtcdeviceinfo.h b/talk/media/webrtc/fakewebrtcdeviceinfo.h new file mode 100644 index 000000000..210792a49 --- /dev/null +++ b/talk/media/webrtc/fakewebrtcdeviceinfo.h @@ -0,0 +1,123 @@ +// libjingle +// Copyright 2004 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. + +#ifndef TALK_SESSION_PHONE_FAKEWEBRTCDEVICEINFO_H_ +#define TALK_SESSION_PHONE_FAKEWEBRTCDEVICEINFO_H_ + +#include + +#include "talk/base/stringutils.h" +#include "talk/media/webrtc/webrtcvideocapturer.h" + +// Fake class for mocking out webrtc::VideoCaptureModule::DeviceInfo. +class FakeWebRtcDeviceInfo : public webrtc::VideoCaptureModule::DeviceInfo { + public: + struct Device { + Device(const std::string& n, const std::string& i) : name(n), id(i) {} + std::string name; + std::string id; + std::string product; + std::vector caps; + }; + FakeWebRtcDeviceInfo() {} + void AddDevice(const std::string& device_name, const std::string& device_id) { + devices_.push_back(Device(device_name, device_id)); + } + void AddCapability(const std::string& device_id, + const webrtc::VideoCaptureCapability& cap) { + Device* dev = GetDeviceById( + reinterpret_cast(device_id.c_str())); + if (!dev) return; + dev->caps.push_back(cap); + } + virtual uint32_t NumberOfDevices() { + return devices_.size(); + } + virtual int32_t GetDeviceName(uint32_t device_num, + char* device_name, + uint32_t device_name_len, + char* device_id, + uint32_t device_id_len, + char* product_id, + uint32_t product_id_len) { + Device* dev = GetDeviceByIndex(device_num); + if (!dev) return -1; + talk_base::strcpyn(reinterpret_cast(device_name), device_name_len, + dev->name.c_str()); + talk_base::strcpyn(reinterpret_cast(device_id), device_id_len, + dev->id.c_str()); + if (product_id) { + talk_base::strcpyn(reinterpret_cast(product_id), product_id_len, + dev->product.c_str()); + } + return 0; + } + virtual int32_t NumberOfCapabilities(const char* device_id) { + Device* dev = GetDeviceById(device_id); + if (!dev) return -1; + return dev->caps.size(); + } + virtual int32_t GetCapability(const char* device_id, + const uint32_t device_cap_num, + webrtc::VideoCaptureCapability& cap) { + Device* dev = GetDeviceById(device_id); + if (!dev) return -1; + if (device_cap_num >= dev->caps.size()) return -1; + cap = dev->caps[device_cap_num]; + return 0; + } + virtual int32_t GetOrientation(const char* device_id, + webrtc::VideoCaptureRotation& rotation) { + return -1; // not implemented + } + virtual int32_t GetBestMatchedCapability( + const char* device_id, + const webrtc::VideoCaptureCapability& requested, + webrtc::VideoCaptureCapability& resulting) { + return -1; // not implemented + } + virtual int32_t DisplayCaptureSettingsDialogBox( + const char* device_id, const char* dialog_title, + void* parent, uint32_t x, uint32_t y) { + return -1; // not implemented + } + + Device* GetDeviceByIndex(size_t num) { + return (num < devices_.size()) ? &devices_[num] : NULL; + } + Device* GetDeviceById(const char* device_id) { + for (size_t i = 0; i < devices_.size(); ++i) { + if (devices_[i].id == reinterpret_cast(device_id)) { + return &devices_[i]; + } + } + return NULL; + } + + private: + std::vector devices_; +}; + +#endif // TALK_SESSION_PHONE_FAKEWEBRTCDEVICEINFO_H_ diff --git a/talk/media/webrtc/fakewebrtcvcmfactory.h b/talk/media/webrtc/fakewebrtcvcmfactory.h new file mode 100644 index 000000000..38643f9c2 --- /dev/null +++ b/talk/media/webrtc/fakewebrtcvcmfactory.h @@ -0,0 +1,63 @@ +// libjingle +// Copyright 2004 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. + +#ifndef TALK_SESSION_PHONE_FAKEWEBRTCVCMFACTORY_H_ +#define TALK_SESSION_PHONE_FAKEWEBRTCVCMFACTORY_H_ + +#include + +#include "talk/media/webrtc/fakewebrtcvideocapturemodule.h" +#include "talk/media/webrtc/webrtcvideocapturer.h" + +// Factory class to allow the fakes above to be injected into +// WebRtcVideoCapturer. +class FakeWebRtcVcmFactory : public cricket::WebRtcVcmFactoryInterface { + public: + virtual webrtc::VideoCaptureModule* Create(int module_id, + const char* device_id) { + if (!device_info.GetDeviceById(device_id)) return NULL; + FakeWebRtcVideoCaptureModule* module = + new FakeWebRtcVideoCaptureModule(this, module_id); + modules.push_back(module); + return module; + } + virtual webrtc::VideoCaptureModule::DeviceInfo* CreateDeviceInfo(int id) { + return &device_info; + } + virtual void DestroyDeviceInfo(webrtc::VideoCaptureModule::DeviceInfo* info) { + } + void OnDestroyed(webrtc::VideoCaptureModule* module) { + std::remove(modules.begin(), modules.end(), module); + } + FakeWebRtcDeviceInfo device_info; + std::vector modules; +}; + +FakeWebRtcVideoCaptureModule::~FakeWebRtcVideoCaptureModule() { + if (factory_) + factory_->OnDestroyed(this); +} + +#endif // TALK_SESSION_PHONE_FAKEWEBRTCVCMFACTORY_H_ diff --git a/talk/media/webrtc/fakewebrtcvideocapturemodule.h b/talk/media/webrtc/fakewebrtcvideocapturemodule.h new file mode 100644 index 000000000..b823bc18f --- /dev/null +++ b/talk/media/webrtc/fakewebrtcvideocapturemodule.h @@ -0,0 +1,159 @@ +// libjingle +// Copyright 2004 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. + +#ifndef TALK_SESSION_PHONE_FAKEWEBRTCVIDEOCAPTUREMODULE_H_ +#define TALK_SESSION_PHONE_FAKEWEBRTCVIDEOCAPTUREMODULE_H_ + +#include + +#include "talk/media/base/testutils.h" +#include "talk/media/webrtc/fakewebrtcdeviceinfo.h" +#include "talk/media/webrtc/webrtcvideocapturer.h" + +class FakeWebRtcVcmFactory; + +// Fake class for mocking out webrtc::VideoCaptureModule. +class FakeWebRtcVideoCaptureModule : public webrtc::VideoCaptureModule { + public: + FakeWebRtcVideoCaptureModule(FakeWebRtcVcmFactory* factory, int32_t id) + : factory_(factory), + id_(id), + callback_(NULL), + running_(false), + delay_(0) { + } + virtual int32_t Version(char* version, + uint32_t& remaining_buffer_in_bytes, + uint32_t& position) const { + return 0; + } + virtual int32_t TimeUntilNextProcess() { + return 0; + } + virtual int32_t Process() { + return 0; + } + virtual int32_t ChangeUniqueId(const int32_t id) { + id_ = id; + return 0; + } + virtual int32_t RegisterCaptureDataCallback( + webrtc::VideoCaptureDataCallback& callback) { + callback_ = &callback; + return 0; + } + virtual int32_t DeRegisterCaptureDataCallback() { + callback_ = NULL; + return 0; + } + virtual int32_t RegisterCaptureCallback( + webrtc::VideoCaptureFeedBack& callback) { + return -1; // not implemented + } + virtual int32_t DeRegisterCaptureCallback() { + return 0; + } + virtual int32_t StartCapture( + const webrtc::VideoCaptureCapability& cap) { + if (running_) return -1; + cap_ = cap; + running_ = true; + return 0; + } + virtual int32_t StopCapture() { + running_ = false; + return 0; + } + virtual const char* CurrentDeviceName() const { + return NULL; // not implemented + } + virtual bool CaptureStarted() { + return running_; + } + virtual int32_t CaptureSettings( + webrtc::VideoCaptureCapability& settings) { + if (!running_) return -1; + settings = cap_; + return 0; + } + virtual int32_t SetCaptureDelay(int32_t delay) { + delay_ = delay; + return 0; + } + virtual int32_t CaptureDelay() { + return delay_; + } + virtual int32_t SetCaptureRotation( + webrtc::VideoCaptureRotation rotation) { + return -1; // not implemented + } + virtual VideoCaptureEncodeInterface* GetEncodeInterface( + const webrtc::VideoCodec& codec) { + return NULL; // not implemented + } + virtual int32_t EnableFrameRateCallback(const bool enable) { + return -1; // not implemented + } + virtual int32_t EnableNoPictureAlarm(const bool enable) { + return -1; // not implemented + } + virtual int32_t AddRef() { + return 0; + } + virtual int32_t Release() { + delete this; + return 0; + } + + bool SendFrame(int w, int h) { + if (!running_) return false; + webrtc::I420VideoFrame sample; + // Setting stride based on width. + if (sample.CreateEmptyFrame(w, h, w, (w + 1) / 2, (w + 1) / 2) < 0) { + return false; + } + if (callback_) { + callback_->OnIncomingCapturedFrame(id_, sample); + } + return true; + } + + const webrtc::VideoCaptureCapability& cap() const { + return cap_; + } + + private: + // Ref-counted, use Release() instead. + ~FakeWebRtcVideoCaptureModule(); + + FakeWebRtcVcmFactory* factory_; + int id_; + webrtc::VideoCaptureDataCallback* callback_; + bool running_; + webrtc::VideoCaptureCapability cap_; + int delay_; +}; + +#endif // TALK_SESSION_PHONE_FAKEWEBRTCVIDEOCAPTUREMODULE_H_ diff --git a/talk/media/webrtc/fakewebrtcvideoengine.h b/talk/media/webrtc/fakewebrtcvideoengine.h new file mode 100644 index 000000000..5b7b6cb0c --- /dev/null +++ b/talk/media/webrtc/fakewebrtcvideoengine.h @@ -0,0 +1,1100 @@ +/* + * libjingle + * Copyright 2010 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. + */ + +#ifndef TALK_MEDIA_WEBRTC_FAKEWEBRTCVIDEOENGINE_H_ +#define TALK_MEDIA_WEBRTC_FAKEWEBRTCVIDEOENGINE_H_ + +#include +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/stringutils.h" +#include "talk/media/base/codec.h" +#include "talk/media/webrtc/fakewebrtccommon.h" +#include "talk/media/webrtc/webrtcvideodecoderfactory.h" +#include "talk/media/webrtc/webrtcvideoencoderfactory.h" +#include "talk/media/webrtc/webrtcvie.h" + +namespace webrtc { + +bool operator==(const webrtc::VideoCodec& c1, const webrtc::VideoCodec& c2) { + return memcmp(&c1, &c2, sizeof(c1)) == 0; +} + +} + +namespace cricket { + +#define WEBRTC_CHECK_CAPTURER(capturer) \ + if (capturers_.find(capturer) == capturers_.end()) return -1; + +#define WEBRTC_ASSERT_CAPTURER(capturer) \ + ASSERT(capturers_.find(capturer) != capturers_.end()); + +static const int kMinVideoBitrate = 100; +static const int kStartVideoBitrate = 300; +static const int kMaxVideoBitrate = 1000; + +// WebRtc channel id and capture id share the same number space. +// This is how AddRenderer(renderId, ...) is able to tell if it is adding a +// renderer for a channel or it is adding a renderer for a capturer. +static const int kViEChannelIdBase = 0; +static const int kViEChannelIdMax = 1000; +static const int kViECaptureIdBase = 10000; // Make sure there is a gap. +static const int kViECaptureIdMax = 11000; + +// Fake class for mocking out webrtc::VideoDecoder +class FakeWebRtcVideoDecoder : public webrtc::VideoDecoder { + public: + FakeWebRtcVideoDecoder() + : num_frames_received_(0) { + } + + virtual int32 InitDecode(const webrtc::VideoCodec*, int32) { + return WEBRTC_VIDEO_CODEC_OK; + } + + virtual int32 Decode( + const webrtc::EncodedImage&, bool, const webrtc::RTPFragmentationHeader*, + const webrtc::CodecSpecificInfo*, int64) { + num_frames_received_++; + return WEBRTC_VIDEO_CODEC_OK; + } + + virtual int32 RegisterDecodeCompleteCallback( + webrtc::DecodedImageCallback*) { + return WEBRTC_VIDEO_CODEC_OK; + } + + virtual int32 Release() { + return WEBRTC_VIDEO_CODEC_OK; + } + + virtual int32 Reset() { + return WEBRTC_VIDEO_CODEC_OK; + } + + int GetNumFramesReceived() const { + return num_frames_received_; + } + + private: + int num_frames_received_; +}; + +// Fake class for mocking out WebRtcVideoDecoderFactory. +class FakeWebRtcVideoDecoderFactory : public WebRtcVideoDecoderFactory { + public: + FakeWebRtcVideoDecoderFactory() + : num_created_decoders_(0) { + } + + virtual webrtc::VideoDecoder* CreateVideoDecoder( + webrtc::VideoCodecType type) { + if (supported_codec_types_.count(type) == 0) { + return NULL; + } + FakeWebRtcVideoDecoder* decoder = new FakeWebRtcVideoDecoder(); + decoders_.push_back(decoder); + num_created_decoders_++; + return decoder; + } + + virtual void DestroyVideoDecoder(webrtc::VideoDecoder* decoder) { + decoders_.erase( + std::remove(decoders_.begin(), decoders_.end(), decoder), + decoders_.end()); + delete decoder; + } + + void AddSupportedVideoCodecType(webrtc::VideoCodecType type) { + supported_codec_types_.insert(type); + } + + int GetNumCreatedDecoders() { + return num_created_decoders_; + } + + const std::vector& decoders() { + return decoders_; + } + + private: + std::set supported_codec_types_; + std::vector decoders_; + int num_created_decoders_; +}; + +// Fake class for mocking out webrtc::VideoEnoder +class FakeWebRtcVideoEncoder : public webrtc::VideoEncoder { + public: + FakeWebRtcVideoEncoder() {} + + virtual int32 InitEncode(const webrtc::VideoCodec* codecSettings, + int32 numberOfCores, + uint32 maxPayloadSize) { + return WEBRTC_VIDEO_CODEC_OK; + } + + virtual int32 Encode( + const webrtc::I420VideoFrame& inputImage, + const webrtc::CodecSpecificInfo* codecSpecificInfo, + const std::vector* frame_types) { + return WEBRTC_VIDEO_CODEC_OK; + } + + virtual int32 RegisterEncodeCompleteCallback( + webrtc::EncodedImageCallback* callback) { + return WEBRTC_VIDEO_CODEC_OK; + } + + virtual int32 Release() { + return WEBRTC_VIDEO_CODEC_OK; + } + + virtual int32 SetChannelParameters(uint32 packetLoss, + int rtt) { + return WEBRTC_VIDEO_CODEC_OK; + } + + virtual int32 SetRates(uint32 newBitRate, + uint32 frameRate) { + return WEBRTC_VIDEO_CODEC_OK; + } +}; + +// Fake class for mocking out WebRtcVideoEncoderFactory. +class FakeWebRtcVideoEncoderFactory : public WebRtcVideoEncoderFactory { + public: + FakeWebRtcVideoEncoderFactory() + : num_created_encoders_(0) { + } + + virtual webrtc::VideoEncoder* CreateVideoEncoder( + webrtc::VideoCodecType type) { + if (supported_codec_types_.count(type) == 0) { + return NULL; + } + FakeWebRtcVideoEncoder* encoder = new FakeWebRtcVideoEncoder(); + encoders_.push_back(encoder); + num_created_encoders_++; + return encoder; + } + + virtual void DestroyVideoEncoder(webrtc::VideoEncoder* encoder) { + encoders_.erase( + std::remove(encoders_.begin(), encoders_.end(), encoder), + encoders_.end()); + delete encoder; + } + + virtual void AddObserver(WebRtcVideoEncoderFactory::Observer* observer) { + bool inserted = observers_.insert(observer).second; + EXPECT_TRUE(inserted); + } + + virtual void RemoveObserver(WebRtcVideoEncoderFactory::Observer* observer) { + size_t erased = observers_.erase(observer); + EXPECT_EQ(erased, 1UL); + } + + virtual const std::vector& codecs() + const { + return codecs_; + } + + void AddSupportedVideoCodecType(webrtc::VideoCodecType type, + const std::string& name) { + supported_codec_types_.insert(type); + codecs_.push_back( + WebRtcVideoEncoderFactory::VideoCodec(type, name, 1280, 720, 30)); + } + + void NotifyCodecsAvailable() { + std::set::iterator it; + for (it = observers_.begin(); it != observers_.end(); ++it) + (*it)->OnCodecsAvailable(); + } + + int GetNumCreatedEncoders() { + return num_created_encoders_; + } + + const std::vector& encoders() { + return encoders_; + } + + private: + std::set supported_codec_types_; + std::vector codecs_; + std::vector encoders_; + std::set observers_; + int num_created_encoders_; +}; + +class FakeWebRtcVideoEngine + : public webrtc::ViEBase, + public webrtc::ViECodec, + public webrtc::ViECapture, + public webrtc::ViENetwork, + public webrtc::ViERender, + public webrtc::ViERTP_RTCP, + public webrtc::ViEImageProcess, + public webrtc::ViEExternalCodec { + public: + struct Channel { + Channel() + : capture_id_(-1), + original_channel_id_(-1), + has_renderer_(false), + render_started_(false), + send(false), + receive_(false), + can_transmit_(true), + rtcp_status_(webrtc::kRtcpNone), + key_frame_request_method_(webrtc::kViEKeyFrameRequestNone), + tmmbr_(false), + remb_contribute_(false), + remb_bw_partition_(false), + rtp_offset_send_id_(0), + rtp_offset_receive_id_(0), + rtp_absolute_send_time_send_id_(0), + rtp_absolute_send_time_receive_id_(0), + sender_target_delay_(0), + receiver_target_delay_(0), + transmission_smoothing_(false), + nack_(false), + hybrid_nack_fec_(false), + send_video_bitrate_(0), + send_fec_bitrate_(0), + send_nack_bitrate_(0), + send_bandwidth_(0), + receive_bandwidth_(0) { + ssrcs_[0] = 0; // default ssrc. + memset(&send_codec, 0, sizeof(send_codec)); + } + int capture_id_; + int original_channel_id_; + bool has_renderer_; + bool render_started_; + bool send; + bool receive_; + bool can_transmit_; + std::map ssrcs_; + std::string cname_; + webrtc::ViERTCPMode rtcp_status_; + webrtc::ViEKeyFrameRequestMethod key_frame_request_method_; + bool tmmbr_; + bool remb_contribute_; // This channel contributes to the remb report. + bool remb_bw_partition_; // This channel is allocated part of total bw. + int rtp_offset_send_id_; + int rtp_offset_receive_id_; + int rtp_absolute_send_time_send_id_; + int rtp_absolute_send_time_receive_id_; + int sender_target_delay_; + int receiver_target_delay_; + bool transmission_smoothing_; + bool nack_; + bool hybrid_nack_fec_; + std::vector recv_codecs; + std::set ext_decoder_pl_types_; + std::set ext_encoder_pl_types_; + webrtc::VideoCodec send_codec; + unsigned int send_video_bitrate_; + unsigned int send_fec_bitrate_; + unsigned int send_nack_bitrate_; + unsigned int send_bandwidth_; + unsigned int receive_bandwidth_; + }; + class Capturer : public webrtc::ViEExternalCapture { + public: + Capturer() : channel_id_(-1), denoising_(false), last_capture_time_(0) { } + int channel_id() const { return channel_id_; } + void set_channel_id(int channel_id) { channel_id_ = channel_id; } + bool denoising() const { return denoising_; } + void set_denoising(bool denoising) { denoising_ = denoising; } + int64 last_capture_time() { return last_capture_time_; } + + // From ViEExternalCapture + virtual int IncomingFrame(unsigned char* videoFrame, + unsigned int videoFrameLength, + unsigned short width, + unsigned short height, + webrtc::RawVideoType videoType, + unsigned long long captureTime) { + return 0; + } + virtual int IncomingFrameI420( + const webrtc::ViEVideoFrameI420& video_frame, + unsigned long long captureTime) { + last_capture_time_ = captureTime; + return 0; + } + + private: + int channel_id_; + bool denoising_; + int64 last_capture_time_; + }; + + FakeWebRtcVideoEngine(const cricket::VideoCodec* const* codecs, + int num_codecs) + : inited_(false), + last_channel_(kViEChannelIdBase - 1), + fail_create_channel_(false), + last_capturer_(kViECaptureIdBase - 1), + fail_alloc_capturer_(false), + codecs_(codecs), + num_codecs_(num_codecs), + num_set_send_codecs_(0) { + } + + ~FakeWebRtcVideoEngine() { + ASSERT(0 == channels_.size()); + ASSERT(0 == capturers_.size()); + } + bool IsInited() const { return inited_; } + + int GetLastChannel() const { return last_channel_; } + int GetChannelFromLocalSsrc(int local_ssrc) const { + // ssrcs_[0] is the default local ssrc. + for (std::map::const_iterator iter = channels_.begin(); + iter != channels_.end(); ++iter) { + if (local_ssrc == iter->second->ssrcs_[0]) { + return iter->first; + } + } + return -1; + } + + int GetNumChannels() const { return channels_.size(); } + bool IsChannel(int channel) const { + return (channels_.find(channel) != channels_.end()); + } + void set_fail_create_channel(bool fail_create_channel) { + fail_create_channel_ = fail_create_channel; + } + + int GetLastCapturer() const { return last_capturer_; } + int GetNumCapturers() const { return capturers_.size(); } + void set_fail_alloc_capturer(bool fail_alloc_capturer) { + fail_alloc_capturer_ = fail_alloc_capturer; + } + int num_set_send_codecs() const { return num_set_send_codecs_; } + + int GetCaptureId(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->capture_id_; + } + int GetOriginalChannelId(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->original_channel_id_; + } + bool GetHasRenderer(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->has_renderer_; + } + bool GetRenderStarted(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->render_started_; + } + bool GetSend(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->send; + } + int GetCaptureChannelId(int capture_id) const { + WEBRTC_ASSERT_CAPTURER(capture_id); + return capturers_.find(capture_id)->second->channel_id(); + } + bool GetCaptureDenoising(int capture_id) const { + WEBRTC_ASSERT_CAPTURER(capture_id); + return capturers_.find(capture_id)->second->denoising(); + } + int64 GetCaptureLastTimestamp(int capture_id) const { + WEBRTC_ASSERT_CAPTURER(capture_id); + return capturers_.find(capture_id)->second->last_capture_time(); + } + webrtc::ViERTCPMode GetRtcpStatus(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->rtcp_status_; + } + webrtc::ViEKeyFrameRequestMethod GetKeyFrameRequestMethod(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->key_frame_request_method_; + } + bool GetTmmbrStatus(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->tmmbr_; + } + bool GetRembStatusBwPartition(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->remb_bw_partition_; + } + bool GetRembStatusContribute(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->remb_contribute_; + } + int GetSendRtpTimestampOffsetExtensionId(int channel) { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->rtp_offset_send_id_; + } + int GetReceiveRtpTimestampOffsetExtensionId(int channel) { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->rtp_offset_receive_id_; + } + int GetSendAbsoluteSendTimeExtensionId(int channel) { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->rtp_absolute_send_time_send_id_; + } + int GetReceiveAbsoluteSendTimeExtensionId(int channel) { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->rtp_absolute_send_time_receive_id_; + } + bool GetTransmissionSmoothingStatus(int channel) { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->transmission_smoothing_; + } + int GetSenderTargetDelay(int channel) { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->sender_target_delay_; + } + int GetReceiverTargetDelay(int channel) { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->receiver_target_delay_; + } + bool GetNackStatus(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->nack_; + } + bool GetHybridNackFecStatus(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->hybrid_nack_fec_; + } + int GetNumSsrcs(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->ssrcs_.size(); + } + bool GetIsTransmitting(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->can_transmit_; + } + bool ReceiveCodecRegistered(int channel, + const webrtc::VideoCodec& codec) const { + WEBRTC_ASSERT_CHANNEL(channel); + const std::vector& codecs = + channels_.find(channel)->second->recv_codecs; + return std::find(codecs.begin(), codecs.end(), codec) != codecs.end(); + }; + bool ExternalDecoderRegistered(int channel, + unsigned int pl_type) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second-> + ext_decoder_pl_types_.count(pl_type) != 0; + }; + int GetNumExternalDecoderRegistered(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->ext_decoder_pl_types_.size(); + }; + bool ExternalEncoderRegistered(int channel, + unsigned int pl_type) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second-> + ext_encoder_pl_types_.count(pl_type) != 0; + }; + int GetNumExternalEncoderRegistered(int channel) const { + WEBRTC_ASSERT_CHANNEL(channel); + return channels_.find(channel)->second->ext_encoder_pl_types_.size(); + }; + int GetTotalNumExternalEncoderRegistered() const { + std::map::const_iterator it; + int total_num_registered = 0; + for (it = channels_.begin(); it != channels_.end(); ++it) + total_num_registered += it->second->ext_encoder_pl_types_.size(); + return total_num_registered; + } + void SetSendBitrates(int channel, unsigned int video_bitrate, + unsigned int fec_bitrate, unsigned int nack_bitrate) { + WEBRTC_ASSERT_CHANNEL(channel); + channels_[channel]->send_video_bitrate_ = video_bitrate; + channels_[channel]->send_fec_bitrate_ = fec_bitrate; + channels_[channel]->send_nack_bitrate_ = nack_bitrate; + } + void SetSendBandwidthEstimate(int channel, unsigned int send_bandwidth) { + WEBRTC_ASSERT_CHANNEL(channel); + channels_[channel]->send_bandwidth_ = send_bandwidth; + } + void SetReceiveBandwidthEstimate(int channel, + unsigned int receive_bandwidth) { + WEBRTC_ASSERT_CHANNEL(channel); + channels_[channel]->receive_bandwidth_ = receive_bandwidth; + }; + + WEBRTC_STUB(Release, ()); + + // webrtc::ViEBase + WEBRTC_FUNC(Init, ()) { + inited_ = true; + return 0; + }; + WEBRTC_STUB(SetVoiceEngine, (webrtc::VoiceEngine*)); + WEBRTC_FUNC(CreateChannel, (int& channel)) { // NOLINT + if (fail_create_channel_) { + return -1; + } + if (kViEChannelIdMax == last_channel_) { + return -1; + } + Channel* ch = new Channel(); + channels_[++last_channel_] = ch; + channel = last_channel_; + return 0; + }; + WEBRTC_FUNC(CreateChannel, (int& channel, int original_channel)) { + WEBRTC_CHECK_CHANNEL(original_channel); + if (CreateChannel(channel) != 0) { + return -1; + } + channels_[channel]->original_channel_id_ = original_channel; + return 0; + } + WEBRTC_FUNC(CreateReceiveChannel, (int& channel, int original_channel)) { + return CreateChannel(channel, original_channel); + } + WEBRTC_FUNC(DeleteChannel, (const int channel)) { + WEBRTC_CHECK_CHANNEL(channel); + // Make sure we deregister all the decoders before deleting a channel. + EXPECT_EQ(0, GetNumExternalDecoderRegistered(channel)); + delete channels_[channel]; + channels_.erase(channel); + return 0; + } + WEBRTC_STUB(ConnectAudioChannel, (const int, const int)); + WEBRTC_STUB(DisconnectAudioChannel, (const int)); + WEBRTC_FUNC(StartSend, (const int channel)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->send = true; + return 0; + } + WEBRTC_FUNC(StopSend, (const int channel)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->send = false; + return 0; + } + WEBRTC_FUNC(StartReceive, (const int channel)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->receive_ = true; + return 0; + } + WEBRTC_FUNC(StopReceive, (const int channel)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->receive_ = false; + return 0; + } + WEBRTC_STUB(GetVersion, (char version[1024])); + WEBRTC_STUB(LastError, ()); + + // webrtc::ViECodec + WEBRTC_FUNC_CONST(NumberOfCodecs, ()) { + return num_codecs_; + }; + WEBRTC_FUNC_CONST(GetCodec, (const unsigned char list_number, + webrtc::VideoCodec& out_codec)) { + if (list_number >= NumberOfCodecs()) { + return -1; + } + memset(&out_codec, 0, sizeof(out_codec)); + const cricket::VideoCodec& c(*codecs_[list_number]); + if ("I420" == c.name) { + out_codec.codecType = webrtc::kVideoCodecI420; + } else if ("VP8" == c.name) { + out_codec.codecType = webrtc::kVideoCodecVP8; + } else if ("red" == c.name) { + out_codec.codecType = webrtc::kVideoCodecRED; + } else if ("ulpfec" == c.name) { + out_codec.codecType = webrtc::kVideoCodecULPFEC; + } else { + out_codec.codecType = webrtc::kVideoCodecUnknown; + } + talk_base::strcpyn(out_codec.plName, sizeof(out_codec.plName), + c.name.c_str()); + out_codec.plType = c.id; + out_codec.width = c.width; + out_codec.height = c.height; + out_codec.startBitrate = kStartVideoBitrate; + out_codec.maxBitrate = kMaxVideoBitrate; + out_codec.minBitrate = kMinVideoBitrate; + out_codec.maxFramerate = c.framerate; + return 0; + }; + WEBRTC_FUNC(SetSendCodec, (const int channel, + const webrtc::VideoCodec& codec)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->send_codec = codec; + ++num_set_send_codecs_; + return 0; + }; + WEBRTC_FUNC_CONST(GetSendCodec, (const int channel, + webrtc::VideoCodec& codec)) { // NOLINT + WEBRTC_CHECK_CHANNEL(channel); + codec = channels_.find(channel)->second->send_codec; + return 0; + }; + WEBRTC_FUNC(SetReceiveCodec, (const int channel, + const webrtc::VideoCodec& codec)) { // NOLINT + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->recv_codecs.push_back(codec); + return 0; + }; + WEBRTC_STUB_CONST(GetReceiveCodec, (const int, webrtc::VideoCodec&)); + WEBRTC_STUB_CONST(GetCodecConfigParameters, (const int, + unsigned char*, unsigned char&)); + WEBRTC_STUB(SetImageScaleStatus, (const int, const bool)); + WEBRTC_STUB_CONST(GetSendCodecStastistics, (const int, + unsigned int&, unsigned int&)); + WEBRTC_STUB_CONST(GetReceiveCodecStastistics, (const int, + unsigned int&, unsigned int&)); + WEBRTC_STUB_CONST(GetReceiveSideDelay, (const int video_channel, + int* delay_ms)); + WEBRTC_FUNC_CONST(GetCodecTargetBitrate, (const int channel, + unsigned int* codec_target_bitrate)) { + WEBRTC_CHECK_CHANNEL(channel); + + std::map::const_iterator it = channels_.find(channel); + if (it->second->send) { + // Assume the encoder produces the expected rate. + *codec_target_bitrate = it->second->send_video_bitrate_; + } else { + *codec_target_bitrate = 0; + } + return 0; + } + virtual unsigned int GetDiscardedPackets(const int channel) const { + return 0; + } + + WEBRTC_STUB(SetKeyFrameRequestCallbackStatus, (const int, const bool)); + WEBRTC_STUB(SetSignalKeyPacketLossStatus, (const int, const bool, + const bool)); + WEBRTC_STUB(RegisterEncoderObserver, (const int, + webrtc::ViEEncoderObserver&)); + WEBRTC_STUB(DeregisterEncoderObserver, (const int)); + WEBRTC_STUB(RegisterDecoderObserver, (const int, + webrtc::ViEDecoderObserver&)); + WEBRTC_STUB(DeregisterDecoderObserver, (const int)); + WEBRTC_STUB(SendKeyFrame, (const int)); + WEBRTC_STUB(WaitForFirstKeyFrame, (const int, const bool)); +#ifdef USE_WEBRTC_DEV_BRANCH + WEBRTC_STUB(StartDebugRecording, (int, const char*)); + WEBRTC_STUB(StopDebugRecording, (int)); +#endif + + // webrtc::ViECapture + WEBRTC_STUB(NumberOfCaptureDevices, ()); + WEBRTC_STUB(GetCaptureDevice, (unsigned int, char*, + const unsigned int, char*, const unsigned int)); + WEBRTC_STUB(AllocateCaptureDevice, (const char*, const unsigned int, int&)); + WEBRTC_FUNC(AllocateExternalCaptureDevice, + (int& capture_id, webrtc::ViEExternalCapture*& capture)) { + if (fail_alloc_capturer_) { + return -1; + } + if (kViECaptureIdMax == last_capturer_) { + return -1; + } + Capturer* cap = new Capturer(); + capturers_[++last_capturer_] = cap; + capture_id = last_capturer_; + capture = cap; + return 0; + } + WEBRTC_STUB(AllocateCaptureDevice, (webrtc::VideoCaptureModule&, int&)); + WEBRTC_FUNC(ReleaseCaptureDevice, (const int capture_id)) { + WEBRTC_CHECK_CAPTURER(capture_id); + delete capturers_[capture_id]; + capturers_.erase(capture_id); + return 0; + } + WEBRTC_FUNC(ConnectCaptureDevice, (const int capture_id, + const int channel)) { + WEBRTC_CHECK_CHANNEL(channel); + WEBRTC_CHECK_CAPTURER(capture_id); + channels_[channel]->capture_id_ = capture_id; + capturers_[capture_id]->set_channel_id(channel); + return 0; + } + WEBRTC_FUNC(DisconnectCaptureDevice, (const int channel)) { + WEBRTC_CHECK_CHANNEL(channel); + int capture_id = channels_[channel]->capture_id_; + WEBRTC_CHECK_CAPTURER(capture_id); + channels_[channel]->capture_id_ = -1; + capturers_[capture_id]->set_channel_id(-1); + return 0; + } + WEBRTC_STUB(StartCapture, (const int, const webrtc::CaptureCapability&)); + WEBRTC_STUB(StopCapture, (const int)); + WEBRTC_STUB(SetRotateCapturedFrames, (const int, + const webrtc::RotateCapturedFrame)); + WEBRTC_STUB(SetCaptureDelay, (const int, const unsigned int)); + WEBRTC_STUB(NumberOfCapabilities, (const char*, const unsigned int)); + WEBRTC_STUB(GetCaptureCapability, (const char*, const unsigned int, + const unsigned int, webrtc::CaptureCapability&)); + WEBRTC_STUB(ShowCaptureSettingsDialogBox, (const char*, const unsigned int, + const char*, void*, const unsigned int, const unsigned int)); + WEBRTC_STUB(GetOrientation, (const char*, webrtc::RotateCapturedFrame&)); + WEBRTC_STUB(EnableBrightnessAlarm, (const int, const bool)); + WEBRTC_STUB(RegisterObserver, (const int, webrtc::ViECaptureObserver&)); + WEBRTC_STUB(DeregisterObserver, (const int)); + + // webrtc::ViENetwork + WEBRTC_VOID_FUNC(SetNetworkTransmissionState, (const int channel, + const bool is_transmitting)) { + WEBRTC_ASSERT_CHANNEL(channel); + channels_[channel]->can_transmit_ = is_transmitting; + } + WEBRTC_STUB(RegisterSendTransport, (const int, webrtc::Transport&)); + WEBRTC_STUB(DeregisterSendTransport, (const int)); + WEBRTC_STUB(ReceivedRTPPacket, (const int, const void*, const int)); + WEBRTC_STUB(ReceivedRTCPPacket, (const int, const void*, const int)); + // Not using WEBRTC_STUB due to bool return value + virtual bool IsIPv6Enabled(int channel) { return true; } + WEBRTC_STUB(SetMTU, (int, unsigned int)); + WEBRTC_STUB(SetPacketTimeoutNotification, (const int, bool, int)); + WEBRTC_STUB(RegisterObserver, (const int, webrtc::ViENetworkObserver&)); + WEBRTC_STUB(SetPeriodicDeadOrAliveStatus, (const int, const bool, + const unsigned int)); + + // webrtc::ViERender + WEBRTC_STUB(RegisterVideoRenderModule, (webrtc::VideoRender&)); + WEBRTC_STUB(DeRegisterVideoRenderModule, (webrtc::VideoRender&)); + WEBRTC_STUB(AddRenderer, (const int, void*, const unsigned int, const float, + const float, const float, const float)); + WEBRTC_FUNC(RemoveRenderer, (const int render_id)) { + if (IsCapturerId(render_id)) { + WEBRTC_CHECK_CAPTURER(render_id); + return 0; + } else if (IsChannelId(render_id)) { + WEBRTC_CHECK_CHANNEL(render_id); + channels_[render_id]->has_renderer_ = false; + return 0; + } + return -1; + } + WEBRTC_FUNC(StartRender, (const int render_id)) { + if (IsCapturerId(render_id)) { + WEBRTC_CHECK_CAPTURER(render_id); + return 0; + } else if (IsChannelId(render_id)) { + WEBRTC_CHECK_CHANNEL(render_id); + channels_[render_id]->render_started_ = true; + return 0; + } + return -1; + } + WEBRTC_FUNC(StopRender, (const int render_id)) { + if (IsCapturerId(render_id)) { + WEBRTC_CHECK_CAPTURER(render_id); + return 0; + } else if (IsChannelId(render_id)) { + WEBRTC_CHECK_CHANNEL(render_id); + channels_[render_id]->render_started_ = false; + return 0; + } + return -1; + } + WEBRTC_STUB(SetExpectedRenderDelay, (int render_id, int render_delay)); + WEBRTC_STUB(ConfigureRender, (int, const unsigned int, const float, + const float, const float, const float)); + WEBRTC_STUB(MirrorRenderStream, (const int, const bool, const bool, + const bool)); + WEBRTC_FUNC(AddRenderer, (const int render_id, + webrtc::RawVideoType video_type, + webrtc::ExternalRenderer* renderer)) { + if (IsCapturerId(render_id)) { + WEBRTC_CHECK_CAPTURER(render_id); + return 0; + } else if (IsChannelId(render_id)) { + WEBRTC_CHECK_CHANNEL(render_id); + channels_[render_id]->has_renderer_ = true; + return 0; + } + return -1; + } + + // webrtc::ViERTP_RTCP + WEBRTC_FUNC(SetLocalSSRC, (const int channel, + const unsigned int ssrc, + const webrtc::StreamType usage, + const unsigned char idx)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->ssrcs_[idx] = ssrc; + return 0; + } + WEBRTC_STUB_CONST(SetRemoteSSRCType, (const int, + const webrtc::StreamType, const unsigned int)); + + WEBRTC_FUNC_CONST(GetLocalSSRC, (const int channel, + unsigned int& ssrc)) { + // ssrcs_[0] is the default local ssrc. + WEBRTC_CHECK_CHANNEL(channel); + ssrc = channels_.find(channel)->second->ssrcs_[0]; + return 0; + } + WEBRTC_STUB_CONST(GetRemoteSSRC, (const int, unsigned int&)); + WEBRTC_STUB_CONST(GetRemoteCSRCs, (const int, unsigned int*)); + + WEBRTC_STUB(SetRtxSendPayloadType, (const int, const uint8)); + WEBRTC_STUB(SetRtxReceivePayloadType, (const int, const uint8)); + + WEBRTC_STUB(SetStartSequenceNumber, (const int, unsigned short)); + WEBRTC_FUNC(SetRTCPStatus, + (const int channel, const webrtc::ViERTCPMode mode)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->rtcp_status_ = mode; + return 0; + } + WEBRTC_STUB_CONST(GetRTCPStatus, (const int, webrtc::ViERTCPMode&)); + WEBRTC_FUNC(SetRTCPCName, (const int channel, + const char rtcp_cname[KMaxRTCPCNameLength])) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->cname_.assign(rtcp_cname); + return 0; + } + WEBRTC_FUNC_CONST(GetRTCPCName, (const int channel, + char rtcp_cname[KMaxRTCPCNameLength])) { + WEBRTC_CHECK_CHANNEL(channel); + talk_base::strcpyn(rtcp_cname, KMaxRTCPCNameLength, + channels_.find(channel)->second->cname_.c_str()); + return 0; + } + WEBRTC_STUB_CONST(GetRemoteRTCPCName, (const int, char*)); + WEBRTC_STUB(SendApplicationDefinedRTCPPacket, (const int, const unsigned char, + unsigned int, const char*, unsigned short)); + WEBRTC_FUNC(SetNACKStatus, (const int channel, const bool enable)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->nack_ = enable; + channels_[channel]->hybrid_nack_fec_ = false; + return 0; + } + WEBRTC_STUB(SetFECStatus, (const int, const bool, const unsigned char, + const unsigned char)); + WEBRTC_FUNC(SetHybridNACKFECStatus, (const int channel, const bool enable, + const unsigned char red_type, const unsigned char fec_type)) { + WEBRTC_CHECK_CHANNEL(channel); + if (red_type == fec_type || + red_type == channels_[channel]->send_codec.plType || + fec_type == channels_[channel]->send_codec.plType) { + return -1; + } + channels_[channel]->nack_ = false; + channels_[channel]->hybrid_nack_fec_ = enable; + return 0; + } + WEBRTC_FUNC(SetKeyFrameRequestMethod, + (const int channel, + const webrtc::ViEKeyFrameRequestMethod method)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->key_frame_request_method_ = method; + return 0; + } + WEBRTC_FUNC(SetSenderBufferingMode, (int channel, int target_delay)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->sender_target_delay_ = target_delay; + return 0; + } + WEBRTC_FUNC(SetReceiverBufferingMode, (int channel, int target_delay)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->receiver_target_delay_ = target_delay; + return 0; + } + // |Send| and |receive| are stored locally in variables that more clearly + // explain what they mean. + WEBRTC_FUNC(SetRembStatus, (int channel, bool send, bool receive)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->remb_contribute_ = receive; + channels_[channel]->remb_bw_partition_ = send; + return 0; + } + WEBRTC_FUNC(SetTMMBRStatus, (const int channel, const bool enable)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->tmmbr_ = enable; + return 0; + } + WEBRTC_FUNC(SetSendTimestampOffsetStatus, (int channel, bool enable, + int id)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->rtp_offset_send_id_ = (enable) ? id : 0; + return 0; + } + WEBRTC_FUNC(SetReceiveTimestampOffsetStatus, (int channel, bool enable, + int id)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->rtp_offset_receive_id_ = (enable) ? id : 0; + return 0; + } + WEBRTC_FUNC(SetSendAbsoluteSendTimeStatus, (int channel, bool enable, + int id)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->rtp_absolute_send_time_send_id_ = (enable) ? id : 0; + return 0; + } + WEBRTC_FUNC(SetReceiveAbsoluteSendTimeStatus, (int channel, bool enable, + int id)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->rtp_absolute_send_time_receive_id_ = (enable) ? id : 0; + return 0; + } + WEBRTC_FUNC(SetTransmissionSmoothingStatus, (int channel, bool enable)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->transmission_smoothing_ = enable; + return 0; + } + WEBRTC_STUB_CONST(GetReceivedRTCPStatistics, (const int, unsigned short&, + unsigned int&, unsigned int&, unsigned int&, int&)); + WEBRTC_STUB_CONST(GetSentRTCPStatistics, (const int, unsigned short&, + unsigned int&, unsigned int&, unsigned int&, int&)); + WEBRTC_STUB_CONST(GetRTPStatistics, (const int, unsigned int&, unsigned int&, + unsigned int&, unsigned int&)); + WEBRTC_FUNC_CONST(GetBandwidthUsage, (const int channel, + unsigned int& total_bitrate, unsigned int& video_bitrate, + unsigned int& fec_bitrate, unsigned int& nack_bitrate)) { + WEBRTC_CHECK_CHANNEL(channel); + std::map::const_iterator it = channels_.find(channel); + if (it->second->send) { + video_bitrate = it->second->send_video_bitrate_; + fec_bitrate = it->second->send_fec_bitrate_; + nack_bitrate = it->second->send_nack_bitrate_; + total_bitrate = video_bitrate + fec_bitrate + nack_bitrate; + } else { + total_bitrate = 0; + video_bitrate = 0; + fec_bitrate = 0; + nack_bitrate = 0; + } + return 0; + } + WEBRTC_FUNC_CONST(GetEstimatedSendBandwidth, (const int channel, + unsigned int* send_bandwidth_estimate)) { + WEBRTC_CHECK_CHANNEL(channel); + std::map::const_iterator it = channels_.find(channel); + // Assume the current video, fec and nack bitrate sums up to our estimate. + if (it->second->send) { + *send_bandwidth_estimate = it->second->send_bandwidth_; + } else { + *send_bandwidth_estimate = 0; + } + return 0; + } + WEBRTC_FUNC_CONST(GetEstimatedReceiveBandwidth, (const int channel, + unsigned int* receive_bandwidth_estimate)) { + WEBRTC_CHECK_CHANNEL(channel); + std::map::const_iterator it = channels_.find(channel); + if (it->second->receive_) { + // For simplicity, assume all channels receive half of max send rate. + *receive_bandwidth_estimate = it->second->receive_bandwidth_; + } else { + *receive_bandwidth_estimate = 0; + } + return 0; + } + + WEBRTC_STUB(StartRTPDump, (const int, const char*, webrtc::RTPDirections)); + WEBRTC_STUB(StopRTPDump, (const int, webrtc::RTPDirections)); + WEBRTC_STUB(RegisterRTPObserver, (const int, webrtc::ViERTPObserver&)); + WEBRTC_STUB(DeregisterRTPObserver, (const int)); + WEBRTC_STUB(RegisterRTCPObserver, (const int, webrtc::ViERTCPObserver&)); + WEBRTC_STUB(DeregisterRTCPObserver, (const int)); + + // webrtc::ViEImageProcess + WEBRTC_STUB(RegisterCaptureEffectFilter, (const int, + webrtc::ViEEffectFilter&)); + WEBRTC_STUB(DeregisterCaptureEffectFilter, (const int)); + WEBRTC_STUB(RegisterSendEffectFilter, (const int, + webrtc::ViEEffectFilter&)); + WEBRTC_STUB(DeregisterSendEffectFilter, (const int)); + WEBRTC_STUB(RegisterRenderEffectFilter, (const int, + webrtc::ViEEffectFilter&)); + WEBRTC_STUB(DeregisterRenderEffectFilter, (const int)); + WEBRTC_STUB(EnableDeflickering, (const int, const bool)); + WEBRTC_FUNC(EnableDenoising, (const int capture_id, const bool denoising)) { + WEBRTC_CHECK_CAPTURER(capture_id); + capturers_[capture_id]->set_denoising(denoising); + return 0; + } + WEBRTC_STUB(EnableColorEnhancement, (const int, const bool)); + + // webrtc::ViEExternalCodec + WEBRTC_FUNC(RegisterExternalSendCodec, + (const int channel, const unsigned char pl_type, webrtc::VideoEncoder*, + bool)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->ext_encoder_pl_types_.insert(pl_type); + return 0; + } + WEBRTC_FUNC(DeRegisterExternalSendCodec, + (const int channel, const unsigned char pl_type)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->ext_encoder_pl_types_.erase(pl_type); + return 0; + } + WEBRTC_FUNC(RegisterExternalReceiveCodec, + (const int channel, const unsigned int pl_type, webrtc::VideoDecoder*, + bool, int)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->ext_decoder_pl_types_.insert(pl_type); + return 0; + } + WEBRTC_FUNC(DeRegisterExternalReceiveCodec, + (const int channel, const unsigned char pl_type)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->ext_decoder_pl_types_.erase(pl_type); + return 0; + } + + private: + bool IsChannelId(int id) const { + return (id >= kViEChannelIdBase && id <= kViEChannelIdMax); + } + bool IsCapturerId(int id) const { + return (id >= kViECaptureIdBase && id <= kViECaptureIdMax); + } + + bool inited_; + int last_channel_; + std::map channels_; + bool fail_create_channel_; + int last_capturer_; + std::map capturers_; + bool fail_alloc_capturer_; + const cricket::VideoCodec* const* codecs_; + int num_codecs_; + int num_set_send_codecs_; // how many times we call SetSendCodec(). +}; + +} // namespace cricket + +#endif // TALK_MEDIA_WEBRTC_FAKEWEBRTCVIDEOENGINE_H_ diff --git a/talk/media/webrtc/fakewebrtcvoiceengine.h b/talk/media/webrtc/fakewebrtcvoiceengine.h new file mode 100644 index 000000000..7202e1520 --- /dev/null +++ b/talk/media/webrtc/fakewebrtcvoiceengine.h @@ -0,0 +1,1010 @@ +/* + * libjingle + * Copyright 2010 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. + */ + +#ifndef TALK_SESSION_PHONE_FAKEWEBRTCVOICEENGINE_H_ +#define TALK_SESSION_PHONE_FAKEWEBRTCVOICEENGINE_H_ + +#include +#include +#include + + +#include "talk/base/basictypes.h" +#include "talk/base/stringutils.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/voiceprocessor.h" +#include "talk/media/webrtc/fakewebrtccommon.h" +#include "talk/media/webrtc/webrtcvoe.h" + +namespace cricket { + +// Function returning stats will return these values +// for all values based on type. +const int kIntStatValue = 123; +const float kFractionLostStatValue = 0.5; + +static const char kFakeDefaultDeviceName[] = "Fake Default"; +static const int kFakeDefaultDeviceId = -1; +static const char kFakeDeviceName[] = "Fake Device"; +#ifdef WIN32 +static const int kFakeDeviceId = 0; +#else +static const int kFakeDeviceId = 1; +#endif + + +class FakeWebRtcVoiceEngine + : public webrtc::VoEAudioProcessing, + public webrtc::VoEBase, public webrtc::VoECodec, public webrtc::VoEDtmf, + public webrtc::VoEFile, public webrtc::VoEHardware, + public webrtc::VoEExternalMedia, public webrtc::VoENetEqStats, + public webrtc::VoENetwork, public webrtc::VoERTP_RTCP, + public webrtc::VoEVideoSync, public webrtc::VoEVolumeControl { + public: + struct DtmfInfo { + DtmfInfo() + : dtmf_event_code(-1), + dtmf_out_of_band(false), + dtmf_length_ms(-1) {} + int dtmf_event_code; + bool dtmf_out_of_band; + int dtmf_length_ms; + }; + struct Channel { + Channel() + : external_transport(false), + send(false), + playout(false), + volume_scale(1.0), + volume_pan_left(1.0), + volume_pan_right(1.0), + file(false), + vad(false), + fec(false), + nack(false), + media_processor_registered(false), + cn8_type(13), + cn16_type(105), + dtmf_type(106), + fec_type(117), + nack_max_packets(0), + send_ssrc(0), + level_header_ext_(-1) { + memset(&send_codec, 0, sizeof(send_codec)); + } + bool external_transport; + bool send; + bool playout; + float volume_scale; + float volume_pan_left; + float volume_pan_right; + bool file; + bool vad; + bool fec; + bool nack; + bool media_processor_registered; + int cn8_type; + int cn16_type; + int dtmf_type; + int fec_type; + int nack_max_packets; + uint32 send_ssrc; + int level_header_ext_; + DtmfInfo dtmf_info; + std::vector recv_codecs; + webrtc::CodecInst send_codec; + std::list packets; + }; + + FakeWebRtcVoiceEngine(const cricket::AudioCodec* const* codecs, + int num_codecs) + : inited_(false), + last_channel_(-1), + fail_create_channel_(false), + codecs_(codecs), + num_codecs_(num_codecs), + ec_enabled_(false), + ec_metrics_enabled_(false), + cng_enabled_(false), + ns_enabled_(false), + agc_enabled_(false), + highpass_filter_enabled_(false), + stereo_swapping_enabled_(false), + typing_detection_enabled_(false), + ec_mode_(webrtc::kEcDefault), + aecm_mode_(webrtc::kAecmSpeakerphone), + ns_mode_(webrtc::kNsDefault), + agc_mode_(webrtc::kAgcDefault), + observer_(NULL), + playout_fail_channel_(-1), + send_fail_channel_(-1), + fail_start_recording_microphone_(false), + recording_microphone_(false), + media_processor_(NULL) { + memset(&agc_config_, 0, sizeof(agc_config_)); + } + ~FakeWebRtcVoiceEngine() { + // Ought to have all been deleted by the WebRtcVoiceMediaChannel + // destructors, but just in case ... + for (std::map::const_iterator i = channels_.begin(); + i != channels_.end(); ++i) { + delete i->second; + } + } + + bool IsExternalMediaProcessorRegistered() const { + return media_processor_ != NULL; + } + bool IsInited() const { return inited_; } + int GetLastChannel() const { return last_channel_; } + int GetNumChannels() const { return channels_.size(); } + bool GetPlayout(int channel) { + return channels_[channel]->playout; + } + bool GetSend(int channel) { + return channels_[channel]->send; + } + bool GetRecordingMicrophone() { + return recording_microphone_; + } + bool GetVAD(int channel) { + return channels_[channel]->vad; + } + bool GetFEC(int channel) { + return channels_[channel]->fec; + } + bool GetNACK(int channel) { + return channels_[channel]->nack; + } + int GetNACKMaxPackets(int channel) { + return channels_[channel]->nack_max_packets; + } + int GetSendCNPayloadType(int channel, bool wideband) { + return (wideband) ? + channels_[channel]->cn16_type : + channels_[channel]->cn8_type; + } + int GetSendTelephoneEventPayloadType(int channel) { + return channels_[channel]->dtmf_type; + } + int GetSendFECPayloadType(int channel) { + return channels_[channel]->fec_type; + } + bool CheckPacket(int channel, const void* data, size_t len) { + bool result = !CheckNoPacket(channel); + if (result) { + std::string packet = channels_[channel]->packets.front(); + result = (packet == std::string(static_cast(data), len)); + channels_[channel]->packets.pop_front(); + } + return result; + } + bool CheckNoPacket(int channel) { + return channels_[channel]->packets.empty(); + } + void TriggerCallbackOnError(int channel_num, int err_code) { + ASSERT(observer_ != NULL); + observer_->CallbackOnError(channel_num, err_code); + } + void set_playout_fail_channel(int channel) { + playout_fail_channel_ = channel; + } + void set_send_fail_channel(int channel) { + send_fail_channel_ = channel; + } + void set_fail_start_recording_microphone( + bool fail_start_recording_microphone) { + fail_start_recording_microphone_ = fail_start_recording_microphone; + } + void set_fail_create_channel(bool fail_create_channel) { + fail_create_channel_ = fail_create_channel; + } + void TriggerProcessPacket(MediaProcessorDirection direction) { + webrtc::ProcessingTypes pt = + (direction == cricket::MPD_TX) ? + webrtc::kRecordingPerChannel : webrtc::kPlaybackAllChannelsMixed; + if (media_processor_ != NULL) { + media_processor_->Process(0, + pt, + NULL, + 0, + 0, + true); + } + } + + WEBRTC_STUB(Release, ()); + + // webrtc::VoEBase + WEBRTC_FUNC(RegisterVoiceEngineObserver, ( + webrtc::VoiceEngineObserver& observer)) { + observer_ = &observer; + return 0; + } + WEBRTC_STUB(DeRegisterVoiceEngineObserver, ()); + WEBRTC_FUNC(Init, (webrtc::AudioDeviceModule* adm, + webrtc::AudioProcessing* audioproc)) { + inited_ = true; + return 0; + } + WEBRTC_FUNC(Terminate, ()) { + inited_ = false; + return 0; + } + virtual webrtc::AudioProcessing* audio_processing() OVERRIDE { + return NULL; + } + WEBRTC_STUB(MaxNumOfChannels, ()); + WEBRTC_FUNC(CreateChannel, ()) { + if (fail_create_channel_) { + return -1; + } + Channel* ch = new Channel(); + for (int i = 0; i < NumOfCodecs(); ++i) { + webrtc::CodecInst codec; + GetCodec(i, codec); + ch->recv_codecs.push_back(codec); + } + channels_[++last_channel_] = ch; + return last_channel_; + } + WEBRTC_FUNC(DeleteChannel, (int channel)) { + WEBRTC_CHECK_CHANNEL(channel); + delete channels_[channel]; + channels_.erase(channel); + return 0; + } + WEBRTC_STUB(StartReceive, (int channel)); + WEBRTC_FUNC(StartPlayout, (int channel)) { + if (playout_fail_channel_ != channel) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->playout = true; + return 0; + } else { + // When playout_fail_channel_ == channel, fail the StartPlayout on this + // channel. + return -1; + } + } + WEBRTC_FUNC(StartSend, (int channel)) { + if (send_fail_channel_ != channel) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->send = true; + return 0; + } else { + // When send_fail_channel_ == channel, fail the StartSend on this + // channel. + return -1; + } + } + WEBRTC_STUB(StopReceive, (int channel)); + WEBRTC_FUNC(StopPlayout, (int channel)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->playout = false; + return 0; + } + WEBRTC_FUNC(StopSend, (int channel)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->send = false; + return 0; + } + WEBRTC_STUB(GetVersion, (char version[1024])); + WEBRTC_STUB(LastError, ()); + WEBRTC_STUB(SetOnHoldStatus, (int, bool, webrtc::OnHoldModes)); + WEBRTC_STUB(GetOnHoldStatus, (int, bool&, webrtc::OnHoldModes&)); + WEBRTC_STUB(SetNetEQPlayoutMode, (int, webrtc::NetEqModes)); + WEBRTC_STUB(GetNetEQPlayoutMode, (int, webrtc::NetEqModes&)); + + // webrtc::VoECodec + WEBRTC_FUNC(NumOfCodecs, ()) { + return num_codecs_; + } + WEBRTC_FUNC(GetCodec, (int index, webrtc::CodecInst& codec)) { + if (index < 0 || index >= NumOfCodecs()) { + return -1; + } + const cricket::AudioCodec& c(*codecs_[index]); + codec.pltype = c.id; + talk_base::strcpyn(codec.plname, sizeof(codec.plname), c.name.c_str()); + codec.plfreq = c.clockrate; + codec.pacsize = 0; + codec.channels = c.channels; + codec.rate = c.bitrate; + return 0; + } + WEBRTC_FUNC(SetSendCodec, (int channel, const webrtc::CodecInst& codec)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->send_codec = codec; + return 0; + } + WEBRTC_FUNC(GetSendCodec, (int channel, webrtc::CodecInst& codec)) { + WEBRTC_CHECK_CHANNEL(channel); + codec = channels_[channel]->send_codec; + return 0; + } + WEBRTC_STUB(SetSecondarySendCodec, (int channel, + const webrtc::CodecInst& codec, + int red_payload_type)); + WEBRTC_STUB(RemoveSecondarySendCodec, (int channel)); + WEBRTC_STUB(GetSecondarySendCodec, (int channel, + webrtc::CodecInst& codec)); + WEBRTC_STUB(GetRecCodec, (int channel, webrtc::CodecInst& codec)); + WEBRTC_STUB(SetAMREncFormat, (int channel, webrtc::AmrMode mode)); + WEBRTC_STUB(SetAMRDecFormat, (int channel, webrtc::AmrMode mode)); + WEBRTC_STUB(SetAMRWbEncFormat, (int channel, webrtc::AmrMode mode)); + WEBRTC_STUB(SetAMRWbDecFormat, (int channel, webrtc::AmrMode mode)); + WEBRTC_STUB(SetISACInitTargetRate, (int channel, int rateBps, + bool useFixedFrameSize)); + WEBRTC_STUB(SetISACMaxRate, (int channel, int rateBps)); + WEBRTC_STUB(SetISACMaxPayloadSize, (int channel, int sizeBytes)); + WEBRTC_FUNC(SetRecPayloadType, (int channel, + const webrtc::CodecInst& codec)) { + WEBRTC_CHECK_CHANNEL(channel); + Channel* ch = channels_[channel]; + if (ch->playout) + return -1; // Channel is in use. + // Check if something else already has this slot. + if (codec.pltype != -1) { + for (std::vector::iterator it = + ch->recv_codecs.begin(); it != ch->recv_codecs.end(); ++it) { + if (it->pltype == codec.pltype && + _stricmp(it->plname, codec.plname) != 0) { + return -1; + } + } + } + // Otherwise try to find this codec and update its payload type. + for (std::vector::iterator it = ch->recv_codecs.begin(); + it != ch->recv_codecs.end(); ++it) { + if (strcmp(it->plname, codec.plname) == 0 && + it->plfreq == codec.plfreq) { + it->pltype = codec.pltype; + it->channels = codec.channels; + return 0; + } + } + return -1; // not found + } + WEBRTC_FUNC(SetSendCNPayloadType, (int channel, int type, + webrtc::PayloadFrequencies frequency)) { + WEBRTC_CHECK_CHANNEL(channel); + if (frequency == webrtc::kFreq8000Hz) { + channels_[channel]->cn8_type = type; + } else if (frequency == webrtc::kFreq16000Hz) { + channels_[channel]->cn16_type = type; + } + return 0; + } + WEBRTC_FUNC(GetRecPayloadType, (int channel, webrtc::CodecInst& codec)) { + WEBRTC_CHECK_CHANNEL(channel); + Channel* ch = channels_[channel]; + for (std::vector::iterator it = ch->recv_codecs.begin(); + it != ch->recv_codecs.end(); ++it) { + if (strcmp(it->plname, codec.plname) == 0 && + it->plfreq == codec.plfreq && + it->channels == codec.channels && + it->pltype != -1) { + codec.pltype = it->pltype; + return 0; + } + } + return -1; // not found + } + WEBRTC_FUNC(SetVADStatus, (int channel, bool enable, webrtc::VadModes mode, + bool disableDTX)) { + WEBRTC_CHECK_CHANNEL(channel); + if (channels_[channel]->send_codec.channels == 2) { + // Replicating VoE behavior; VAD cannot be enabled for stereo. + return -1; + } + channels_[channel]->vad = enable; + return 0; + } + WEBRTC_STUB(GetVADStatus, (int channel, bool& enabled, + webrtc::VadModes& mode, bool& disabledDTX)); + + // webrtc::VoEDtmf + WEBRTC_FUNC(SendTelephoneEvent, (int channel, int event_code, + bool out_of_band = true, int length_ms = 160, int attenuation_db = 10)) { + channels_[channel]->dtmf_info.dtmf_event_code = event_code; + channels_[channel]->dtmf_info.dtmf_out_of_band = out_of_band; + channels_[channel]->dtmf_info.dtmf_length_ms = length_ms; + return 0; + } + + WEBRTC_FUNC(SetSendTelephoneEventPayloadType, + (int channel, unsigned char type)) { + channels_[channel]->dtmf_type = type; + return 0; + }; + WEBRTC_STUB(GetSendTelephoneEventPayloadType, + (int channel, unsigned char& type)); + + WEBRTC_STUB(SetDtmfFeedbackStatus, (bool enable, bool directFeedback)); + WEBRTC_STUB(GetDtmfFeedbackStatus, (bool& enabled, bool& directFeedback)); + WEBRTC_STUB(SetDtmfPlayoutStatus, (int channel, bool enable)); + WEBRTC_STUB(GetDtmfPlayoutStatus, (int channel, bool& enabled)); + + + WEBRTC_FUNC(PlayDtmfTone, + (int event_code, int length_ms = 200, int attenuation_db = 10)) { + dtmf_info_.dtmf_event_code = event_code; + dtmf_info_.dtmf_length_ms = length_ms; + return 0; + } + WEBRTC_STUB(StartPlayingDtmfTone, + (int eventCode, int attenuationDb = 10)); + WEBRTC_STUB(StopPlayingDtmfTone, ()); + + // webrtc::VoEFile + WEBRTC_FUNC(StartPlayingFileLocally, (int channel, const char* fileNameUTF8, + bool loop, webrtc::FileFormats format, + float volumeScaling, int startPointMs, + int stopPointMs)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->file = true; + return 0; + } + WEBRTC_FUNC(StartPlayingFileLocally, (int channel, webrtc::InStream* stream, + webrtc::FileFormats format, + float volumeScaling, int startPointMs, + int stopPointMs)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->file = true; + return 0; + } + WEBRTC_FUNC(StopPlayingFileLocally, (int channel)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->file = false; + return 0; + } + WEBRTC_FUNC(IsPlayingFileLocally, (int channel)) { + WEBRTC_CHECK_CHANNEL(channel); + return (channels_[channel]->file) ? 1 : 0; + } + WEBRTC_STUB(ScaleLocalFilePlayout, (int channel, float scale)); + WEBRTC_STUB(StartPlayingFileAsMicrophone, (int channel, + const char* fileNameUTF8, + bool loop, + bool mixWithMicrophone, + webrtc::FileFormats format, + float volumeScaling)); + WEBRTC_STUB(StartPlayingFileAsMicrophone, (int channel, + webrtc::InStream* stream, + bool mixWithMicrophone, + webrtc::FileFormats format, + float volumeScaling)); + WEBRTC_STUB(StopPlayingFileAsMicrophone, (int channel)); + WEBRTC_STUB(IsPlayingFileAsMicrophone, (int channel)); + WEBRTC_STUB(ScaleFileAsMicrophonePlayout, (int channel, float scale)); + WEBRTC_STUB(StartRecordingPlayout, (int channel, const char* fileNameUTF8, + webrtc::CodecInst* compression, + int maxSizeBytes)); + WEBRTC_STUB(StartRecordingPlayout, (int channel, webrtc::OutStream* stream, + webrtc::CodecInst* compression)); + WEBRTC_STUB(StopRecordingPlayout, (int channel)); + WEBRTC_FUNC(StartRecordingMicrophone, (const char* fileNameUTF8, + webrtc::CodecInst* compression, + int maxSizeBytes)) { + if (fail_start_recording_microphone_) { + return -1; + } + recording_microphone_ = true; + return 0; + } + WEBRTC_FUNC(StartRecordingMicrophone, (webrtc::OutStream* stream, + webrtc::CodecInst* compression)) { + if (fail_start_recording_microphone_) { + return -1; + } + recording_microphone_ = true; + return 0; + } + WEBRTC_FUNC(StopRecordingMicrophone, ()) { + if (!recording_microphone_) { + return -1; + } + recording_microphone_ = false; + return 0; + } + WEBRTC_STUB(ConvertPCMToWAV, (const char* fileNameInUTF8, + const char* fileNameOutUTF8)); + WEBRTC_STUB(ConvertPCMToWAV, (webrtc::InStream* streamIn, + webrtc::OutStream* streamOut)); + WEBRTC_STUB(ConvertWAVToPCM, (const char* fileNameInUTF8, + const char* fileNameOutUTF8)); + WEBRTC_STUB(ConvertWAVToPCM, (webrtc::InStream* streamIn, + webrtc::OutStream* streamOut)); + WEBRTC_STUB(ConvertPCMToCompressed, (const char* fileNameInUTF8, + const char* fileNameOutUTF8, + webrtc::CodecInst* compression)); + WEBRTC_STUB(ConvertPCMToCompressed, (webrtc::InStream* streamIn, + webrtc::OutStream* streamOut, + webrtc::CodecInst* compression)); + WEBRTC_STUB(ConvertCompressedToPCM, (const char* fileNameInUTF8, + const char* fileNameOutUTF8)); + WEBRTC_STUB(ConvertCompressedToPCM, (webrtc::InStream* streamIn, + webrtc::OutStream* streamOut)); + WEBRTC_STUB(GetFileDuration, (const char* fileNameUTF8, int& durationMs, + webrtc::FileFormats format)); + WEBRTC_STUB(GetPlaybackPosition, (int channel, int& positionMs)); + + // webrtc::VoEHardware + WEBRTC_STUB(GetCPULoad, (int&)); + WEBRTC_FUNC(GetNumOfRecordingDevices, (int& num)) { + return GetNumDevices(num); + } + WEBRTC_FUNC(GetNumOfPlayoutDevices, (int& num)) { + return GetNumDevices(num); + } + WEBRTC_FUNC(GetRecordingDeviceName, (int i, char* name, char* guid)) { + return GetDeviceName(i, name, guid); + } + WEBRTC_FUNC(GetPlayoutDeviceName, (int i, char* name, char* guid)) { + return GetDeviceName(i, name, guid); + } + WEBRTC_STUB(SetRecordingDevice, (int, webrtc::StereoChannel)); + WEBRTC_STUB(SetPlayoutDevice, (int)); + WEBRTC_STUB(SetAudioDeviceLayer, (webrtc::AudioLayers)); + WEBRTC_STUB(GetAudioDeviceLayer, (webrtc::AudioLayers&)); + WEBRTC_STUB(GetPlayoutDeviceStatus, (bool&)); + WEBRTC_STUB(GetRecordingDeviceStatus, (bool&)); + WEBRTC_STUB(ResetAudioDevice, ()); + WEBRTC_STUB(AudioDeviceControl, (unsigned int, unsigned int, unsigned int)); + WEBRTC_STUB(SetLoudspeakerStatus, (bool enable)); + WEBRTC_STUB(GetLoudspeakerStatus, (bool& enabled)); + WEBRTC_STUB(SetRecordingSampleRate, (unsigned int samples_per_sec)); + WEBRTC_STUB_CONST(RecordingSampleRate, (unsigned int* samples_per_sec)); + WEBRTC_STUB(SetPlayoutSampleRate, (unsigned int samples_per_sec)); + WEBRTC_STUB_CONST(PlayoutSampleRate, (unsigned int* samples_per_sec)); + WEBRTC_STUB(EnableBuiltInAEC, (bool enable)); + virtual bool BuiltInAECIsEnabled() const { return true; } + + // webrtc::VoENetEqStats + WEBRTC_STUB(GetNetworkStatistics, (int, webrtc::NetworkStatistics&)); + + // webrtc::VoENetwork + WEBRTC_FUNC(RegisterExternalTransport, (int channel, + webrtc::Transport& transport)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->external_transport = true; + return 0; + } + WEBRTC_FUNC(DeRegisterExternalTransport, (int channel)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->external_transport = false; + return 0; + } + WEBRTC_FUNC(ReceivedRTPPacket, (int channel, const void* data, + unsigned int length)) { + WEBRTC_CHECK_CHANNEL(channel); + if (!channels_[channel]->external_transport) return -1; + channels_[channel]->packets.push_back( + std::string(static_cast(data), length)); + return 0; + } + WEBRTC_STUB(ReceivedRTCPPacket, (int channel, const void* data, + unsigned int length)); + // Not using WEBRTC_STUB due to bool return value + WEBRTC_STUB(SetPacketTimeoutNotification, (int channel, bool enable, + int timeoutSeconds)); + WEBRTC_STUB(GetPacketTimeoutNotification, (int channel, bool& enable, + int& timeoutSeconds)); + WEBRTC_STUB(RegisterDeadOrAliveObserver, (int channel, + webrtc::VoEConnectionObserver& observer)); + WEBRTC_STUB(DeRegisterDeadOrAliveObserver, (int channel)); + WEBRTC_STUB(GetPeriodicDeadOrAliveStatus, (int channel, bool& enabled, + int& sampleTimeSeconds)); + WEBRTC_STUB(SetPeriodicDeadOrAliveStatus, (int channel, bool enable, + int sampleTimeSeconds)); + + // webrtc::VoERTP_RTCP + WEBRTC_STUB(RegisterRTPObserver, (int channel, + webrtc::VoERTPObserver& observer)); + WEBRTC_STUB(DeRegisterRTPObserver, (int channel)); + WEBRTC_STUB(RegisterRTCPObserver, (int channel, + webrtc::VoERTCPObserver& observer)); + WEBRTC_STUB(DeRegisterRTCPObserver, (int channel)); + WEBRTC_FUNC(SetLocalSSRC, (int channel, unsigned int ssrc)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->send_ssrc = ssrc; + return 0; + } + WEBRTC_FUNC(GetLocalSSRC, (int channel, unsigned int& ssrc)) { + WEBRTC_CHECK_CHANNEL(channel); + ssrc = channels_[channel]->send_ssrc; + return 0; + } + WEBRTC_STUB(GetRemoteSSRC, (int channel, unsigned int& ssrc)); + WEBRTC_FUNC(SetRTPAudioLevelIndicationStatus, (int channel, bool enable, + unsigned char id)) { + WEBRTC_CHECK_CHANNEL(channel); + if (enable && (id < 1 || id > 14)) { + // [RFC5285] The 4-bit ID is the local identifier of this element in + // the range 1-14 inclusive. + return -1; + } + channels_[channel]->level_header_ext_ = (enable) ? id : -1; + return 0; + } + WEBRTC_FUNC(GetRTPAudioLevelIndicationStatus, (int channel, bool& enabled, + unsigned char& id)) { + WEBRTC_CHECK_CHANNEL(channel); + enabled = (channels_[channel]->level_header_ext_ != -1); + id = channels_[channel]->level_header_ext_; + return 0; + } + WEBRTC_STUB(GetRemoteCSRCs, (int channel, unsigned int arrCSRC[15])); + WEBRTC_STUB(SetRTCPStatus, (int channel, bool enable)); + WEBRTC_STUB(GetRTCPStatus, (int channel, bool& enabled)); + WEBRTC_STUB(SetRTCP_CNAME, (int channel, const char cname[256])); + WEBRTC_STUB(GetRTCP_CNAME, (int channel, char cname[256])); + WEBRTC_STUB(GetRemoteRTCP_CNAME, (int channel, char* cname)); + WEBRTC_STUB(GetRemoteRTCPData, (int channel, unsigned int& NTPHigh, + unsigned int& NTPLow, + unsigned int& timestamp, + unsigned int& playoutTimestamp, + unsigned int* jitter, + unsigned short* fractionLost)); + WEBRTC_STUB(GetRemoteRTCPSenderInfo, (int channel, + webrtc::SenderInfo* sender_info)); + WEBRTC_FUNC(GetRemoteRTCPReportBlocks, + (int channel, std::vector* receive_blocks)) { + WEBRTC_CHECK_CHANNEL(channel); + webrtc::ReportBlock block; + block.source_SSRC = channels_[channel]->send_ssrc; + webrtc::CodecInst send_codec = channels_[channel]->send_codec; + if (send_codec.pltype >= 0) { + block.fraction_lost = (unsigned char)(kFractionLostStatValue * 256); + if (send_codec.plfreq / 1000 > 0) { + block.interarrival_jitter = kIntStatValue * (send_codec.plfreq / 1000); + } + block.cumulative_num_packets_lost = kIntStatValue; + block.extended_highest_sequence_number = kIntStatValue; + receive_blocks->push_back(block); + } + return 0; + } + WEBRTC_STUB(SendApplicationDefinedRTCPPacket, (int channel, + unsigned char subType, + unsigned int name, + const char* data, + unsigned short dataLength)); + WEBRTC_STUB(GetRTPStatistics, (int channel, unsigned int& averageJitterMs, + unsigned int& maxJitterMs, + unsigned int& discardedPackets)); + WEBRTC_FUNC(GetRTCPStatistics, (int channel, webrtc::CallStatistics& stats)) { + WEBRTC_CHECK_CHANNEL(channel); + stats.fractionLost = static_cast(kIntStatValue); + stats.cumulativeLost = kIntStatValue; + stats.extendedMax = kIntStatValue; + stats.jitterSamples = kIntStatValue; + stats.rttMs = kIntStatValue; + stats.bytesSent = kIntStatValue; + stats.packetsSent = kIntStatValue; + stats.bytesReceived = kIntStatValue; + stats.packetsReceived = kIntStatValue; + return 0; + } + WEBRTC_FUNC(SetFECStatus, (int channel, bool enable, int redPayloadtype)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->fec = enable; + channels_[channel]->fec_type = redPayloadtype; + return 0; + } + WEBRTC_FUNC(GetFECStatus, (int channel, bool& enable, int& redPayloadtype)) { + WEBRTC_CHECK_CHANNEL(channel); + enable = channels_[channel]->fec; + redPayloadtype = channels_[channel]->fec_type; + return 0; + } + WEBRTC_FUNC(SetNACKStatus, (int channel, bool enable, int maxNoPackets)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->nack = enable; + channels_[channel]->nack_max_packets = maxNoPackets; + return 0; + } + WEBRTC_STUB(StartRTPDump, (int channel, const char* fileNameUTF8, + webrtc::RTPDirections direction)); + WEBRTC_STUB(StopRTPDump, (int channel, webrtc::RTPDirections direction)); + WEBRTC_STUB(RTPDumpIsActive, (int channel, webrtc::RTPDirections direction)); + WEBRTC_STUB(InsertExtraRTPPacket, (int channel, unsigned char payloadType, + bool markerBit, const char* payloadData, + unsigned short payloadSize)); + WEBRTC_STUB(GetLastRemoteTimeStamp, (int channel, + uint32_t* lastRemoteTimeStamp)); + + // webrtc::VoEVideoSync + WEBRTC_STUB(GetPlayoutBufferSize, (int& bufferMs)); + WEBRTC_STUB(GetPlayoutTimestamp, (int channel, unsigned int& timestamp)); + WEBRTC_STUB(GetRtpRtcp, (int, webrtc::RtpRtcp*&)); + WEBRTC_STUB(SetInitTimestamp, (int channel, unsigned int timestamp)); + WEBRTC_STUB(SetInitSequenceNumber, (int channel, short sequenceNumber)); + WEBRTC_STUB(SetMinimumPlayoutDelay, (int channel, int delayMs)); + WEBRTC_STUB(SetInitialPlayoutDelay, (int channel, int delay_ms)); + WEBRTC_STUB(GetDelayEstimate, (int channel, int* jitter_buffer_delay_ms, + int* playout_buffer_delay_ms)); + WEBRTC_STUB_CONST(GetLeastRequiredDelayMs, (int channel)); + + // webrtc::VoEVolumeControl + WEBRTC_STUB(SetSpeakerVolume, (unsigned int)); + WEBRTC_STUB(GetSpeakerVolume, (unsigned int&)); + WEBRTC_STUB(SetSystemOutputMute, (bool)); + WEBRTC_STUB(GetSystemOutputMute, (bool&)); + WEBRTC_STUB(SetMicVolume, (unsigned int)); + WEBRTC_STUB(GetMicVolume, (unsigned int&)); + WEBRTC_STUB(SetInputMute, (int, bool)); + WEBRTC_STUB(GetInputMute, (int, bool&)); + WEBRTC_STUB(SetSystemInputMute, (bool)); + WEBRTC_STUB(GetSystemInputMute, (bool&)); + WEBRTC_STUB(GetSpeechInputLevel, (unsigned int&)); + WEBRTC_STUB(GetSpeechOutputLevel, (int, unsigned int&)); + WEBRTC_STUB(GetSpeechInputLevelFullRange, (unsigned int&)); + WEBRTC_STUB(GetSpeechOutputLevelFullRange, (int, unsigned int&)); + WEBRTC_FUNC(SetChannelOutputVolumeScaling, (int channel, float scale)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->volume_scale= scale; + return 0; + } + WEBRTC_FUNC(GetChannelOutputVolumeScaling, (int channel, float& scale)) { + WEBRTC_CHECK_CHANNEL(channel); + scale = channels_[channel]->volume_scale; + return 0; + } + WEBRTC_FUNC(SetOutputVolumePan, (int channel, float left, float right)) { + WEBRTC_CHECK_CHANNEL(channel); + channels_[channel]->volume_pan_left = left; + channels_[channel]->volume_pan_right = right; + return 0; + } + WEBRTC_FUNC(GetOutputVolumePan, (int channel, float& left, float& right)) { + WEBRTC_CHECK_CHANNEL(channel); + left = channels_[channel]->volume_pan_left; + right = channels_[channel]->volume_pan_right; + return 0; + } + + // webrtc::VoEAudioProcessing + WEBRTC_FUNC(SetNsStatus, (bool enable, webrtc::NsModes mode)) { + ns_enabled_ = enable; + ns_mode_ = mode; + return 0; + } + WEBRTC_FUNC(GetNsStatus, (bool& enabled, webrtc::NsModes& mode)) { + enabled = ns_enabled_; + mode = ns_mode_; + return 0; + } + + WEBRTC_FUNC(SetAgcStatus, (bool enable, webrtc::AgcModes mode)) { + agc_enabled_ = enable; + agc_mode_ = mode; + return 0; + } + WEBRTC_FUNC(GetAgcStatus, (bool& enabled, webrtc::AgcModes& mode)) { + enabled = agc_enabled_; + mode = agc_mode_; + return 0; + } + + WEBRTC_FUNC(SetAgcConfig, (webrtc::AgcConfig config)) { + agc_config_ = config; + return 0; + } + WEBRTC_FUNC(GetAgcConfig, (webrtc::AgcConfig& config)) { + config = agc_config_; + return 0; + } + WEBRTC_FUNC(SetEcStatus, (bool enable, webrtc::EcModes mode)) { + ec_enabled_ = enable; + ec_mode_ = mode; + return 0; + } + WEBRTC_FUNC(GetEcStatus, (bool& enabled, webrtc::EcModes& mode)) { + enabled = ec_enabled_; + mode = ec_mode_; + return 0; + } + WEBRTC_STUB(EnableDriftCompensation, (bool enable)) + WEBRTC_BOOL_STUB(DriftCompensationEnabled, ()) + WEBRTC_VOID_STUB(SetDelayOffsetMs, (int offset)) + WEBRTC_STUB(DelayOffsetMs, ()); + WEBRTC_FUNC(SetAecmMode, (webrtc::AecmModes mode, bool enableCNG)) { + aecm_mode_ = mode; + cng_enabled_ = enableCNG; + return 0; + } + WEBRTC_FUNC(GetAecmMode, (webrtc::AecmModes& mode, bool& enabledCNG)) { + mode = aecm_mode_; + enabledCNG = cng_enabled_; + return 0; + } + WEBRTC_STUB(SetRxNsStatus, (int channel, bool enable, webrtc::NsModes mode)); + WEBRTC_STUB(GetRxNsStatus, (int channel, bool& enabled, + webrtc::NsModes& mode)); + WEBRTC_STUB(SetRxAgcStatus, (int channel, bool enable, + webrtc::AgcModes mode)); + WEBRTC_STUB(GetRxAgcStatus, (int channel, bool& enabled, + webrtc::AgcModes& mode)); + WEBRTC_STUB(SetRxAgcConfig, (int channel, webrtc::AgcConfig config)); + WEBRTC_STUB(GetRxAgcConfig, (int channel, webrtc::AgcConfig& config)); + + WEBRTC_STUB(RegisterRxVadObserver, (int, webrtc::VoERxVadCallback&)); + WEBRTC_STUB(DeRegisterRxVadObserver, (int channel)); + WEBRTC_STUB(VoiceActivityIndicator, (int channel)); + WEBRTC_FUNC(SetEcMetricsStatus, (bool enable)) { + ec_metrics_enabled_ = enable; + return 0; + } + WEBRTC_FUNC(GetEcMetricsStatus, (bool& enabled)) { + enabled = ec_metrics_enabled_; + return 0; + } + WEBRTC_STUB(GetEchoMetrics, (int& ERL, int& ERLE, int& RERL, int& A_NLP)); + WEBRTC_STUB(GetEcDelayMetrics, (int& delay_median, int& delay_std)); + + WEBRTC_STUB(StartDebugRecording, (const char* fileNameUTF8)); + WEBRTC_STUB(StopDebugRecording, ()); + + WEBRTC_FUNC(SetTypingDetectionStatus, (bool enable)) { + typing_detection_enabled_ = enable; + return 0; + } + WEBRTC_FUNC(GetTypingDetectionStatus, (bool& enabled)) { + enabled = typing_detection_enabled_; + return 0; + } + + WEBRTC_STUB(TimeSinceLastTyping, (int& seconds)); + WEBRTC_STUB(SetTypingDetectionParameters, (int timeWindow, + int costPerTyping, + int reportingThreshold, + int penaltyDecay, + int typeEventDelay)); + int EnableHighPassFilter(bool enable) { + highpass_filter_enabled_ = enable; + return 0; + } + bool IsHighPassFilterEnabled() { + return highpass_filter_enabled_; + } + bool IsStereoChannelSwappingEnabled() { + return stereo_swapping_enabled_; + } + void EnableStereoChannelSwapping(bool enable) { + stereo_swapping_enabled_ = enable; + } + bool WasSendTelephoneEventCalled(int channel, int event_code, int length_ms) { + return (channels_[channel]->dtmf_info.dtmf_event_code == event_code && + channels_[channel]->dtmf_info.dtmf_out_of_band == true && + channels_[channel]->dtmf_info.dtmf_length_ms == length_ms); + } + bool WasPlayDtmfToneCalled(int event_code, int length_ms) { + return (dtmf_info_.dtmf_event_code == event_code && + dtmf_info_.dtmf_length_ms == length_ms); + } + // webrtc::VoEExternalMedia + WEBRTC_FUNC(RegisterExternalMediaProcessing, + (int channel, webrtc::ProcessingTypes type, + webrtc::VoEMediaProcess& processObject)) { + WEBRTC_CHECK_CHANNEL(channel); + if (channels_[channel]->media_processor_registered) { + return -1; + } + channels_[channel]->media_processor_registered = true; + media_processor_ = &processObject; + return 0; + } + WEBRTC_FUNC(DeRegisterExternalMediaProcessing, + (int channel, webrtc::ProcessingTypes type)) { + WEBRTC_CHECK_CHANNEL(channel); + if (!channels_[channel]->media_processor_registered) { + return -1; + } + channels_[channel]->media_processor_registered = false; + media_processor_ = NULL; + return 0; + } + WEBRTC_STUB(SetExternalRecordingStatus, (bool enable)); + WEBRTC_STUB(SetExternalPlayoutStatus, (bool enable)); + WEBRTC_STUB(ExternalRecordingInsertData, + (const int16_t speechData10ms[], int lengthSamples, + int samplingFreqHz, int current_delay_ms)); + WEBRTC_STUB(ExternalPlayoutGetData, + (int16_t speechData10ms[], int samplingFreqHz, + int current_delay_ms, int& lengthSamples)); + WEBRTC_STUB(GetAudioFrame, (int channel, int desired_sample_rate_hz, + webrtc::AudioFrame* frame)); + WEBRTC_STUB(SetExternalMixing, (int channel, bool enable)); + + private: + int GetNumDevices(int& num) { +#ifdef WIN32 + num = 1; +#else + // On non-Windows platforms VE adds a special entry for the default device, + // so if there is one physical device then there are two entries in the + // list. + num = 2; +#endif + return 0; + } + + int GetDeviceName(int i, char* name, char* guid) { + const char *s; +#ifdef WIN32 + if (0 == i) { + s = kFakeDeviceName; + } else { + return -1; + } +#else + // See comment above. + if (0 == i) { + s = kFakeDefaultDeviceName; + } else if (1 == i) { + s = kFakeDeviceName; + } else { + return -1; + } +#endif + strcpy(name, s); + guid[0] = '\0'; + return 0; + } + + bool inited_; + int last_channel_; + std::map channels_; + bool fail_create_channel_; + const cricket::AudioCodec* const* codecs_; + int num_codecs_; + bool ec_enabled_; + bool ec_metrics_enabled_; + bool cng_enabled_; + bool ns_enabled_; + bool agc_enabled_; + bool highpass_filter_enabled_; + bool stereo_swapping_enabled_; + bool typing_detection_enabled_; + webrtc::EcModes ec_mode_; + webrtc::AecmModes aecm_mode_; + webrtc::NsModes ns_mode_; + webrtc::AgcModes agc_mode_; + webrtc::AgcConfig agc_config_; + webrtc::VoiceEngineObserver* observer_; + int playout_fail_channel_; + int send_fail_channel_; + bool fail_start_recording_microphone_; + bool recording_microphone_; + DtmfInfo dtmf_info_; + webrtc::VoEMediaProcess* media_processor_; +}; + +} // namespace cricket + +#endif // TALK_SESSION_PHONE_FAKEWEBRTCVOICEENGINE_H_ diff --git a/talk/media/webrtc/webrtccommon.h b/talk/media/webrtc/webrtccommon.h new file mode 100644 index 000000000..3a557f11d --- /dev/null +++ b/talk/media/webrtc/webrtccommon.h @@ -0,0 +1,76 @@ +/* + * libjingle + * Copyright 2004 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. + */ + + +#ifndef TALK_MEDIA_WEBRTCCOMMON_H_ +#define TALK_MEDIA_WEBRTCCOMMON_H_ + +#include "webrtc/common_types.h" + +namespace cricket { + +// Tracing helpers, for easy logging when WebRTC calls fail. +// Example: "LOG_RTCERR1(StartSend, channel);" produces the trace +// "StartSend(1) failed, err=XXXX" +// The method GetLastEngineError must be defined in the calling scope. +#define LOG_RTCERR0(func) \ + LOG_RTCERR0_EX(func, GetLastEngineError()) +#define LOG_RTCERR1(func, a1) \ + LOG_RTCERR1_EX(func, a1, GetLastEngineError()) +#define LOG_RTCERR2(func, a1, a2) \ + LOG_RTCERR2_EX(func, a1, a2, GetLastEngineError()) +#define LOG_RTCERR3(func, a1, a2, a3) \ + LOG_RTCERR3_EX(func, a1, a2, a3, GetLastEngineError()) +#define LOG_RTCERR4(func, a1, a2, a3, a4) \ + LOG_RTCERR4_EX(func, a1, a2, a3, a4, GetLastEngineError()) +#define LOG_RTCERR5(func, a1, a2, a3, a4, a5) \ + LOG_RTCERR5_EX(func, a1, a2, a3, a4, a5, GetLastEngineError()) +#define LOG_RTCERR6(func, a1, a2, a3, a4, a5, a6) \ + LOG_RTCERR6_EX(func, a1, a2, a3, a4, a5, a6, GetLastEngineError()) +#define LOG_RTCERR0_EX(func, err) LOG(LS_WARNING) \ + << "" << #func << "() failed, err=" << err +#define LOG_RTCERR1_EX(func, a1, err) LOG(LS_WARNING) \ + << "" << #func << "(" << a1 << ") failed, err=" << err +#define LOG_RTCERR2_EX(func, a1, a2, err) LOG(LS_WARNING) \ + << "" << #func << "(" << a1 << ", " << a2 << ") failed, err=" \ + << err +#define LOG_RTCERR3_EX(func, a1, a2, a3, err) LOG(LS_WARNING) \ + << "" << #func << "(" << a1 << ", " << a2 << ", " << a3 \ + << ") failed, err=" << err +#define LOG_RTCERR4_EX(func, a1, a2, a3, a4, err) LOG(LS_WARNING) \ + << "" << #func << "(" << a1 << ", " << a2 << ", " << a3 \ + << ", " << a4 << ") failed, err=" << err +#define LOG_RTCERR5_EX(func, a1, a2, a3, a4, a5, err) LOG(LS_WARNING) \ + << "" << #func << "(" << a1 << ", " << a2 << ", " << a3 \ + << ", " << a4 << ", " << a5 << ") failed, err=" << err +#define LOG_RTCERR6_EX(func, a1, a2, a3, a4, a5, a6, err) LOG(LS_WARNING) \ + << "" << #func << "(" << a1 << ", " << a2 << ", " << a3 \ + << ", " << a4 << ", " << a5 << ", " << a6 << ") failed, err=" << err + +} // namespace cricket + +#endif // TALK_MEDIA_WEBRTCCOMMON_H_ diff --git a/talk/media/webrtc/webrtcexport.h b/talk/media/webrtc/webrtcexport.h new file mode 100644 index 000000000..71ebe4e8e --- /dev/null +++ b/talk/media/webrtc/webrtcexport.h @@ -0,0 +1,79 @@ +/* + * libjingle + * Copyright 2004--2013, 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. + */ +#ifndef TALK_MEDIA_WEBRTC_WEBRTCEXPORT_H_ +#define TALK_MEDIA_WEBRTC_WEBRTCEXPORT_H_ + +#if !defined(GOOGLE_CHROME_BUILD) && !defined(CHROMIUM_BUILD) +#define LIBPEERCONNECTION_LIB 1 +#endif + +#ifndef NON_EXPORTED_BASE +#ifdef WIN32 + +// MSVC_SUPPRESS_WARNING disables warning |n| for the remainder of the line and +// for the next line of the source file. +#define MSVC_SUPPRESS_WARNING(n) __pragma(warning(suppress:n)) + +// Allows exporting a class that inherits from a non-exported base class. +// This uses suppress instead of push/pop because the delimiter after the +// declaration (either "," or "{") has to be placed before the pop macro. +// +// Example usage: +// class EXPORT_API Foo : NON_EXPORTED_BASE(public Bar) { +// +// MSVC Compiler warning C4275: +// non dll-interface class 'Bar' used as base for dll-interface class 'Foo'. +// Note that this is intended to be used only when no access to the base class' +// static data is done through derived classes or inline methods. For more info, +// see http://msdn.microsoft.com/en-us/library/3tdb471s(VS.80).aspx +#define NON_EXPORTED_BASE(code) MSVC_SUPPRESS_WARNING(4275) \ + code + +#else // Not WIN32 +#define NON_EXPORTED_BASE(code) code +#endif // WIN32 +#endif // NON_EXPORTED_BASE + +#if defined (LIBPEERCONNECTION_LIB) + #define WRME_EXPORT +#else + #if defined(WIN32) + #if defined(LIBPEERCONNECTION_IMPLEMENTATION) + #define WRME_EXPORT __declspec(dllexport) + #else + #define WRME_EXPORT __declspec(dllimport) + #endif + #else // defined(WIN32) + #if defined(LIBPEERCONNECTION_IMPLEMENTATION) + #define WRME_EXPORT __attribute__((visibility("default"))) + #else + #define WRME_EXPORT + #endif + #endif +#endif // LIBPEERCONNECTION_LIB + +#endif // TALK_MEDIA_WEBRTC_WEBRTCEXPORT_H_ diff --git a/talk/media/webrtc/webrtcmediaengine.h b/talk/media/webrtc/webrtcmediaengine.h new file mode 100644 index 000000000..1a2de8e1b --- /dev/null +++ b/talk/media/webrtc/webrtcmediaengine.h @@ -0,0 +1,203 @@ +/* + * libjingle + * Copyright 2011 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. + */ + +#ifndef TALK_MEDIA_WEBRTCMEDIAENGINE_H_ +#define TALK_MEDIA_WEBRTCMEDIAENGINE_H_ + +#include "talk/media/base/mediaengine.h" +#include "talk/media/webrtc/webrtcexport.h" + +namespace webrtc { +class AudioDeviceModule; +class VideoCaptureModule; +} +namespace cricket { +class WebRtcVideoDecoderFactory; +class WebRtcVideoEncoderFactory; +} + + +#if !defined(LIBPEERCONNECTION_LIB) && \ + !defined(LIBPEERCONNECTION_IMPLEMENTATION) + +WRME_EXPORT +cricket::MediaEngineInterface* CreateWebRtcMediaEngine( + webrtc::AudioDeviceModule* adm, webrtc::AudioDeviceModule* adm_sc, + cricket::WebRtcVideoEncoderFactory* encoder_factory, + cricket::WebRtcVideoDecoderFactory* decoder_factory); + +WRME_EXPORT +void DestroyWebRtcMediaEngine(cricket::MediaEngineInterface* media_engine); + +namespace cricket { + +class WebRtcMediaEngine : public cricket::MediaEngineInterface { + public: + WebRtcMediaEngine( + webrtc::AudioDeviceModule* adm, + webrtc::AudioDeviceModule* adm_sc, + cricket::WebRtcVideoEncoderFactory* encoder_factory, + cricket::WebRtcVideoDecoderFactory* decoder_factory) + : delegate_(CreateWebRtcMediaEngine( + adm, adm_sc, encoder_factory, decoder_factory)) { + } + virtual ~WebRtcMediaEngine() { + DestroyWebRtcMediaEngine(delegate_); + } + virtual bool Init(talk_base::Thread* worker_thread) OVERRIDE { + return delegate_->Init(worker_thread); + } + virtual void Terminate() OVERRIDE { + delegate_->Terminate(); + } + virtual int GetCapabilities() OVERRIDE { + return delegate_->GetCapabilities(); + } + virtual VoiceMediaChannel* CreateChannel() OVERRIDE { + return delegate_->CreateChannel(); + } + virtual VideoMediaChannel* CreateVideoChannel( + VoiceMediaChannel* voice_media_channel) OVERRIDE { + return delegate_->CreateVideoChannel(voice_media_channel); + } + virtual SoundclipMedia* CreateSoundclip() OVERRIDE { + return delegate_->CreateSoundclip(); + } + virtual bool SetAudioOptions(int options) OVERRIDE { + return delegate_->SetAudioOptions(options); + } + virtual bool SetVideoOptions(int options) OVERRIDE { + return delegate_->SetVideoOptions(options); + } + virtual bool SetAudioDelayOffset(int offset) OVERRIDE { + return delegate_->SetAudioDelayOffset(offset); + } + virtual bool SetDefaultVideoEncoderConfig( + const VideoEncoderConfig& config) OVERRIDE { + return delegate_->SetDefaultVideoEncoderConfig(config); + } + virtual bool SetSoundDevices( + const Device* in_device, const Device* out_device) OVERRIDE { + return delegate_->SetSoundDevices(in_device, out_device); + } + virtual bool SetVideoCapturer(VideoCapturer* capturer) OVERRIDE { + return delegate_->SetVideoCapturer(capturer); + } + virtual VideoCapturer* GetVideoCapturer() const { + return delegate_->GetVideoCapturer(); + } + virtual bool GetOutputVolume(int* level) OVERRIDE { + return delegate_->GetOutputVolume(level); + } + virtual bool SetOutputVolume(int level) OVERRIDE { + return delegate_->SetOutputVolume(level); + } + virtual int GetInputLevel() OVERRIDE { + return delegate_->GetInputLevel(); + } + virtual bool SetLocalMonitor(bool enable) OVERRIDE { + return delegate_->SetLocalMonitor(enable); + } + virtual bool SetLocalRenderer(VideoRenderer* renderer) OVERRIDE { + return delegate_->SetLocalRenderer(renderer); + } + virtual bool SetVideoCapture(bool capture) OVERRIDE { + return delegate_->SetVideoCapture(capture); + } + virtual const std::vector& audio_codecs() OVERRIDE { + return delegate_->audio_codecs(); + } + virtual const std::vector& + audio_rtp_header_extensions() OVERRIDE { + return delegate_->audio_rtp_header_extensions(); + } + virtual const std::vector& video_codecs() OVERRIDE { + return delegate_->video_codecs(); + } + virtual const std::vector& + video_rtp_header_extensions() OVERRIDE { + return delegate_->video_rtp_header_extensions(); + } + virtual void SetVoiceLogging(int min_sev, const char* filter) OVERRIDE { + delegate_->SetVoiceLogging(min_sev, filter); + } + virtual void SetVideoLogging(int min_sev, const char* filter) OVERRIDE { + delegate_->SetVideoLogging(min_sev, filter); + } + virtual bool RegisterVoiceProcessor( + uint32 ssrc, VoiceProcessor* video_processor, + MediaProcessorDirection direction) OVERRIDE { + return delegate_->RegisterVoiceProcessor(ssrc, video_processor, direction); + } + virtual bool UnregisterVoiceProcessor( + uint32 ssrc, VoiceProcessor* video_processor, + MediaProcessorDirection direction) OVERRIDE { + return delegate_->UnregisterVoiceProcessor(ssrc, video_processor, + direction); + } + virtual VideoFormat GetStartCaptureFormat() const OVERRIDE { + return delegate_->GetStartCaptureFormat(); + } + virtual sigslot::repeater2& + SignalVideoCaptureStateChange() { + return delegate_->SignalVideoCaptureStateChange(); + } + + private: + cricket::MediaEngineInterface* delegate_; +}; + +} // namespace cricket +#else + +#include "talk/media/webrtc/webrtcvideoengine.h" +#include "talk/media/webrtc/webrtcvoiceengine.h" + +namespace cricket { +typedef CompositeMediaEngine + WebRtcCompositeMediaEngine; + +class WebRtcMediaEngine : public WebRtcCompositeMediaEngine { + public: + WebRtcMediaEngine(webrtc::AudioDeviceModule* adm, + webrtc::AudioDeviceModule* adm_sc, + WebRtcVideoEncoderFactory* encoder_factory, + WebRtcVideoDecoderFactory* decoder_factory) { + voice_.SetAudioDeviceModule(adm, adm_sc); + video_.SetVoiceEngine(&voice_); + video_.EnableTimedRender(); + video_.SetExternalEncoderFactory(encoder_factory); + video_.SetExternalDecoderFactory(decoder_factory); + } +}; + +} // namespace cricket + +#endif // !defined(LIBPEERCONNECTION_LIB) && + // !defined(LIBPEERCONNECTION_IMPLEMENTATION) + +#endif // TALK_MEDIA_WEBRTCMEDIAENGINE_H_ diff --git a/talk/media/webrtc/webrtcpassthroughrender.cc b/talk/media/webrtc/webrtcpassthroughrender.cc new file mode 100644 index 000000000..b4e78b44e --- /dev/null +++ b/talk/media/webrtc/webrtcpassthroughrender.cc @@ -0,0 +1,176 @@ +/* + * libjingle + * Copyright 2004 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/media/webrtc/webrtcpassthroughrender.h" + +#include "talk/base/common.h" +#include "talk/base/logging.h" + +namespace cricket { + +#define LOG_FIND_STREAM_ERROR(func, id) LOG(LS_ERROR) \ + << "" << func << " - Failed to find stream: " << id + +class PassthroughStream: public webrtc::VideoRenderCallback { + public: + explicit PassthroughStream(const uint32_t stream_id) + : stream_id_(stream_id), + running_(false) { + } + virtual ~PassthroughStream() { + } + virtual int32_t RenderFrame(const uint32_t stream_id, + webrtc::I420VideoFrame& videoFrame) { + talk_base::CritScope cs(&stream_critical_); + // Send frame for rendering directly + if (running_ && renderer_) { + renderer_->RenderFrame(stream_id, videoFrame); + } + return 0; + } + int32_t SetRenderer(VideoRenderCallback* renderer) { + talk_base::CritScope cs(&stream_critical_); + renderer_ = renderer; + return 0; + } + + int32_t StartRender() { + talk_base::CritScope cs(&stream_critical_); + running_ = true; + return 0; + } + + int32_t StopRender() { + talk_base::CritScope cs(&stream_critical_); + running_ = false; + return 0; + } + + private: + uint32_t stream_id_; + VideoRenderCallback* renderer_; + talk_base::CriticalSection stream_critical_; + bool running_; +}; + +WebRtcPassthroughRender::WebRtcPassthroughRender() + : window_(NULL) { +} + +WebRtcPassthroughRender::~WebRtcPassthroughRender() { + while (!stream_render_map_.empty()) { + PassthroughStream* stream = stream_render_map_.begin()->second; + stream_render_map_.erase(stream_render_map_.begin()); + delete stream; + } +} + +webrtc::VideoRenderCallback* WebRtcPassthroughRender::AddIncomingRenderStream( + const uint32_t stream_id, + const uint32_t zOrder, + const float left, const float top, + const float right, const float bottom) { + talk_base::CritScope cs(&render_critical_); + // Stream already exist. + if (FindStream(stream_id) != NULL) { + LOG(LS_ERROR) << "AddIncomingRenderStream - Stream already exists: " + << stream_id; + return NULL; + } + + PassthroughStream* stream = new PassthroughStream(stream_id); + // Store the stream + stream_render_map_[stream_id] = stream; + return stream; +} + +int32_t WebRtcPassthroughRender::DeleteIncomingRenderStream( + const uint32_t stream_id) { + talk_base::CritScope cs(&render_critical_); + PassthroughStream* stream = FindStream(stream_id); + if (stream == NULL) { + LOG_FIND_STREAM_ERROR("DeleteIncomingRenderStream", stream_id); + return -1; + } + delete stream; + stream_render_map_.erase(stream_id); + return 0; +} + +int32_t WebRtcPassthroughRender::AddExternalRenderCallback( + const uint32_t stream_id, + webrtc::VideoRenderCallback* render_object) { + talk_base::CritScope cs(&render_critical_); + PassthroughStream* stream = FindStream(stream_id); + if (stream == NULL) { + LOG_FIND_STREAM_ERROR("AddExternalRenderCallback", stream_id); + return -1; + } + return stream->SetRenderer(render_object); +} + +bool WebRtcPassthroughRender::HasIncomingRenderStream( + const uint32_t stream_id) const { + return (FindStream(stream_id) != NULL); +} + +webrtc::RawVideoType WebRtcPassthroughRender::PreferredVideoType() const { + return webrtc::kVideoI420; +} + +int32_t WebRtcPassthroughRender::StartRender(const uint32_t stream_id) { + talk_base::CritScope cs(&render_critical_); + PassthroughStream* stream = FindStream(stream_id); + if (stream == NULL) { + LOG_FIND_STREAM_ERROR("StartRender", stream_id); + return -1; + } + return stream->StartRender(); +} + +int32_t WebRtcPassthroughRender::StopRender(const uint32_t stream_id) { + talk_base::CritScope cs(&render_critical_); + PassthroughStream* stream = FindStream(stream_id); + if (stream == NULL) { + LOG_FIND_STREAM_ERROR("StopRender", stream_id); + return -1; + } + return stream->StopRender(); +} + +// TODO(ronghuawu): Is it ok to return non-const pointer to PassthroughStream +// from this const function FindStream. +PassthroughStream* WebRtcPassthroughRender::FindStream( + const uint32_t stream_id) const { + StreamMap::const_iterator it = stream_render_map_.find(stream_id); + if (it == stream_render_map_.end()) { + return NULL; + } + return it->second; +} + +} // namespace cricket diff --git a/talk/media/webrtc/webrtcpassthroughrender.h b/talk/media/webrtc/webrtcpassthroughrender.h new file mode 100644 index 000000000..967a29ba0 --- /dev/null +++ b/talk/media/webrtc/webrtcpassthroughrender.h @@ -0,0 +1,211 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_WEBRTCPASSTHROUGHRENDER_H_ +#define TALK_MEDIA_WEBRTCPASSTHROUGHRENDER_H_ + +#include + +#include "talk/base/criticalsection.h" +#include "webrtc/modules/video_render/include/video_render.h" + +namespace cricket { +class PassthroughStream; + +class WebRtcPassthroughRender : public webrtc::VideoRender { + public: + WebRtcPassthroughRender(); + virtual ~WebRtcPassthroughRender(); + + virtual int32_t Version(int8_t* version, + uint32_t& remainingBufferInBytes, + uint32_t& position) const { + return 0; + } + + virtual int32_t ChangeUniqueId(const int32_t id) { + return 0; + } + + virtual int32_t TimeUntilNextProcess() { return 0; } + + virtual int32_t Process() { return 0; } + + virtual void* Window() { + talk_base::CritScope cs(&render_critical_); + return window_; + } + + virtual int32_t ChangeWindow(void* window) { + talk_base::CritScope cs(&render_critical_); + window_ = window; + return 0; + } + + virtual webrtc::VideoRenderCallback* AddIncomingRenderStream( + const uint32_t stream_id, + const uint32_t zOrder, + const float left, const float top, + const float right, const float bottom); + + virtual int32_t DeleteIncomingRenderStream(const uint32_t stream_id); + + virtual int32_t AddExternalRenderCallback( + const uint32_t stream_id, + webrtc::VideoRenderCallback* render_object); + + virtual int32_t GetIncomingRenderStreamProperties( + const uint32_t stream_id, + uint32_t& zOrder, + float& left, float& top, + float& right, float& bottom) const { + return -1; + } + + virtual uint32_t GetIncomingFrameRate( + const uint32_t stream_id) { + return 0; + } + + virtual uint32_t GetNumIncomingRenderStreams() const { + return stream_render_map_.size(); + } + + virtual bool HasIncomingRenderStream(const uint32_t stream_id) const; + + virtual int32_t RegisterRawFrameCallback( + const uint32_t stream_id, + webrtc::VideoRenderCallback* callback_obj) { + return -1; + } + + virtual int32_t GetLastRenderedFrame( + const uint32_t stream_id, + webrtc::I420VideoFrame &frame) const { + return -1; + } + + virtual int32_t StartRender( + const uint32_t stream_id); + + virtual int32_t StopRender( + const uint32_t stream_id); + + virtual int32_t ResetRender() { return 0; } + + virtual webrtc::RawVideoType PreferredVideoType() const; + + virtual bool IsFullScreen() { return false; } + + virtual int32_t GetScreenResolution(uint32_t& screenWidth, + uint32_t& screenHeight) const { + return -1; + } + + virtual uint32_t RenderFrameRate( + const uint32_t stream_id) { + return 0; + } + + virtual int32_t SetStreamCropping( + const uint32_t stream_id, + const float left, const float top, + const float right, + const float bottom) { + return -1; + } + + virtual int32_t SetExpectedRenderDelay(uint32_t stream_id, int32_t delay_ms) { + return -1; + } + + virtual int32_t ConfigureRenderer( + const uint32_t stream_id, + const unsigned int zOrder, + const float left, const float top, + const float right, + const float bottom) { + return -1; + } + + virtual int32_t SetTransparentBackground(const bool enable) { + return -1; + } + + virtual int32_t FullScreenRender(void* window, const bool enable) { + return -1; + } + + virtual int32_t SetBitmap(const void* bitMap, + const uint8_t pictureId, const void* colorKey, + const float left, const float top, + const float right, const float bottom) { + return -1; + } + + virtual int32_t SetText(const uint8_t textId, + const uint8_t* text, + const int32_t textLength, + const uint32_t textColorRef, + const uint32_t backgroundColorRef, + const float left, const float top, + const float right, const float bottom) { + return -1; + } + + virtual int32_t SetStartImage( + const uint32_t stream_id, + const webrtc::I420VideoFrame& videoFrame) { + return -1; + } + + virtual int32_t SetTimeoutImage( + const uint32_t stream_id, + const webrtc::I420VideoFrame& videoFrame, + const uint32_t timeout) { + return -1; + } + + virtual int32_t MirrorRenderStream(const int renderId, + const bool enable, + const bool mirrorXAxis, + const bool mirrorYAxis) { + return -1; + } + + private: + typedef std::map StreamMap; + + PassthroughStream* FindStream(const uint32_t stream_id) const; + + void* window_; + StreamMap stream_render_map_; + talk_base::CriticalSection render_critical_; +}; +} // namespace cricket + +#endif // TALK_MEDIA_WEBRTCPASSTHROUGHRENDER_H_ diff --git a/talk/media/webrtc/webrtcpassthroughrender_unittest.cc b/talk/media/webrtc/webrtcpassthroughrender_unittest.cc new file mode 100644 index 000000000..4eb289251 --- /dev/null +++ b/talk/media/webrtc/webrtcpassthroughrender_unittest.cc @@ -0,0 +1,147 @@ +// Copyright 2008 Google Inc. +// +// Author: Ronghua Wu (ronghuawu@google.com) + +#include + +#include "talk/base/gunit.h" +#include "talk/media/base/testutils.h" +#include "talk/media/webrtc/webrtcpassthroughrender.h" + +class WebRtcPassthroughRenderTest : public testing::Test { + public: + class ExternalRenderer : public webrtc::VideoRenderCallback { + public: + ExternalRenderer() : frame_num_(0) { + } + + virtual ~ExternalRenderer() { + } + + virtual int32_t RenderFrame(const uint32_t stream_id, + webrtc::I420VideoFrame& videoFrame) { + ++frame_num_; + LOG(INFO) << "RenderFrame stream_id: " << stream_id + << " frame_num: " << frame_num_; + return 0; + } + + int frame_num() const { + return frame_num_; + } + + private: + int frame_num_; + }; + + WebRtcPassthroughRenderTest() + : renderer_(new cricket::WebRtcPassthroughRender()) { + } + + ~WebRtcPassthroughRenderTest() { + } + + webrtc::VideoRenderCallback* AddIncomingRenderStream(int stream_id) { + return renderer_->AddIncomingRenderStream(stream_id, 0, 0, 0, 0, 0); + } + + bool HasIncomingRenderStream(int stream_id) { + return renderer_->HasIncomingRenderStream(stream_id); + } + + bool DeleteIncomingRenderStream(int stream_id) { + return (renderer_->DeleteIncomingRenderStream(stream_id) == 0); + } + + bool AddExternalRenderCallback(int stream_id, + webrtc::VideoRenderCallback* renderer) { + return (renderer_->AddExternalRenderCallback(stream_id, renderer) == 0); + } + + bool StartRender(int stream_id) { + return (renderer_->StartRender(stream_id) == 0); + } + + bool StopRender(int stream_id) { + return (renderer_->StopRender(stream_id) == 0); + } + + private: + talk_base::scoped_ptr renderer_; +}; + +TEST_F(WebRtcPassthroughRenderTest, Streams) { + const int stream_id1 = 1234; + const int stream_id2 = 5678; + const int stream_id3 = 9012; // A stream that doesn't exist. + webrtc::VideoRenderCallback* stream = NULL; + // Add a new stream + stream = AddIncomingRenderStream(stream_id1); + EXPECT_TRUE(stream != NULL); + EXPECT_TRUE(HasIncomingRenderStream(stream_id1)); + // Tried to add a already existed stream should return null + stream =AddIncomingRenderStream(stream_id1); + EXPECT_TRUE(stream == NULL); + stream = AddIncomingRenderStream(stream_id2); + EXPECT_TRUE(stream != NULL); + EXPECT_TRUE(HasIncomingRenderStream(stream_id2)); + // Remove the stream + EXPECT_FALSE(DeleteIncomingRenderStream(stream_id3)); + EXPECT_TRUE(DeleteIncomingRenderStream(stream_id2)); + EXPECT_TRUE(!HasIncomingRenderStream(stream_id2)); + // Add back the removed stream + stream = AddIncomingRenderStream(stream_id2); + EXPECT_TRUE(stream != NULL); + EXPECT_TRUE(HasIncomingRenderStream(stream_id2)); +} + +TEST_F(WebRtcPassthroughRenderTest, Renderer) { + webrtc::I420VideoFrame frame; + const int stream_id1 = 1234; + const int stream_id2 = 5678; + const int stream_id3 = 9012; // A stream that doesn't exist. + webrtc::VideoRenderCallback* stream1 = NULL; + webrtc::VideoRenderCallback* stream2 = NULL; + // Add two new stream + stream1 = AddIncomingRenderStream(stream_id1); + EXPECT_TRUE(stream1 != NULL); + EXPECT_TRUE(HasIncomingRenderStream(stream_id1)); + stream2 = AddIncomingRenderStream(stream_id2); + EXPECT_TRUE(stream2 != NULL); + EXPECT_TRUE(HasIncomingRenderStream(stream_id2)); + // Register the external renderer + WebRtcPassthroughRenderTest::ExternalRenderer renderer1; + WebRtcPassthroughRenderTest::ExternalRenderer renderer2; + EXPECT_FALSE(AddExternalRenderCallback(stream_id3, &renderer1)); + EXPECT_TRUE(AddExternalRenderCallback(stream_id1, &renderer1)); + EXPECT_TRUE(AddExternalRenderCallback(stream_id2, &renderer2)); + int test_frame_num = 10; + // RenderFrame without starting the render + for (int i = 0; i < test_frame_num; ++i) { + stream1->RenderFrame(stream_id1, frame); + } + EXPECT_EQ(0, renderer1.frame_num()); + // Start the render and test again. + EXPECT_FALSE(StartRender(stream_id3)); + EXPECT_TRUE(StartRender(stream_id1)); + for (int i = 0; i < test_frame_num; ++i) { + stream1->RenderFrame(stream_id1, frame); + } + EXPECT_EQ(test_frame_num, renderer1.frame_num()); + // Stop the render and test again. + EXPECT_FALSE(StopRender(stream_id3)); + EXPECT_TRUE(StopRender(stream_id1)); + for (int i = 0; i < test_frame_num; ++i) { + stream1->RenderFrame(stream_id1, frame); + } + // The frame number should not have changed. + EXPECT_EQ(test_frame_num, renderer1.frame_num()); + + // Test on stream2 with a differnt number. + EXPECT_TRUE(StartRender(stream_id2)); + test_frame_num = 30; + for (int i = 0; i < test_frame_num; ++i) { + stream2->RenderFrame(stream_id2, frame); + } + EXPECT_EQ(test_frame_num, renderer2.frame_num()); +} diff --git a/talk/media/webrtc/webrtcvideocapturer.cc b/talk/media/webrtc/webrtcvideocapturer.cc new file mode 100644 index 000000000..bcfda4e84 --- /dev/null +++ b/talk/media/webrtc/webrtcvideocapturer.cc @@ -0,0 +1,366 @@ +// libjingle +// Copyright 2011 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. +// +// Implementation of class WebRtcVideoCapturer. + +#include "talk/media/webrtc/webrtcvideocapturer.h" + +#ifdef HAVE_CONFIG_H +#include +#endif + +#ifdef HAVE_WEBRTC_VIDEO +#include "talk/base/logging.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" +#include "talk/media/webrtc/webrtcvideoframe.h" + +#include "talk/base/win32.h" // Need this to #include the impl files. +#include "webrtc/modules/video_capture/include/video_capture_factory.h" + +namespace cricket { + +struct kVideoFourCCEntry { + uint32 fourcc; + webrtc::RawVideoType webrtc_type; +}; + +// This indicates our format preferences and defines a mapping between +// webrtc::RawVideoType (from video_capture_defines.h) to our FOURCCs. +static kVideoFourCCEntry kSupportedFourCCs[] = { + { FOURCC_I420, webrtc::kVideoI420 }, // 12 bpp, no conversion. + { FOURCC_YV12, webrtc::kVideoYV12 }, // 12 bpp, no conversion. + { FOURCC_NV12, webrtc::kVideoNV12 }, // 12 bpp, fast conversion. + { FOURCC_NV21, webrtc::kVideoNV21 }, // 12 bpp, fast conversion. + { FOURCC_YUY2, webrtc::kVideoYUY2 }, // 16 bpp, fast conversion. + { FOURCC_UYVY, webrtc::kVideoUYVY }, // 16 bpp, fast conversion. + { FOURCC_MJPG, webrtc::kVideoMJPEG }, // compressed, slow conversion. + { FOURCC_ARGB, webrtc::kVideoARGB }, // 32 bpp, slow conversion. + { FOURCC_24BG, webrtc::kVideoRGB24 }, // 24 bpp, slow conversion. +}; + +class WebRtcVcmFactory : public WebRtcVcmFactoryInterface { + public: + virtual webrtc::VideoCaptureModule* Create(int id, const char* device) { + return webrtc::VideoCaptureFactory::Create(id, device); + } + virtual webrtc::VideoCaptureModule::DeviceInfo* CreateDeviceInfo(int id) { + return webrtc::VideoCaptureFactory::CreateDeviceInfo(id); + } + virtual void DestroyDeviceInfo(webrtc::VideoCaptureModule::DeviceInfo* info) { + delete info; + } +}; + +static bool CapabilityToFormat(const webrtc::VideoCaptureCapability& cap, + VideoFormat* format) { + uint32 fourcc = 0; + for (size_t i = 0; i < ARRAY_SIZE(kSupportedFourCCs); ++i) { + if (kSupportedFourCCs[i].webrtc_type == cap.rawType) { + fourcc = kSupportedFourCCs[i].fourcc; + break; + } + } + if (fourcc == 0) { + return false; + } + + format->fourcc = fourcc; + format->width = cap.width; + format->height = cap.height; + format->interval = VideoFormat::FpsToInterval(cap.maxFPS); + return true; +} + +static bool FormatToCapability(const VideoFormat& format, + webrtc::VideoCaptureCapability* cap) { + webrtc::RawVideoType webrtc_type = webrtc::kVideoUnknown; + for (size_t i = 0; i < ARRAY_SIZE(kSupportedFourCCs); ++i) { + if (kSupportedFourCCs[i].fourcc == format.fourcc) { + webrtc_type = kSupportedFourCCs[i].webrtc_type; + break; + } + } + if (webrtc_type == webrtc::kVideoUnknown) { + return false; + } + + cap->width = format.width; + cap->height = format.height; + cap->maxFPS = VideoFormat::IntervalToFps(format.interval); + cap->expectedCaptureDelay = 0; + cap->rawType = webrtc_type; + cap->codecType = webrtc::kVideoCodecUnknown; + cap->interlaced = false; + return true; +} + +/////////////////////////////////////////////////////////////////////////// +// Implementation of class WebRtcVideoCapturer +/////////////////////////////////////////////////////////////////////////// + +WebRtcVideoCapturer::WebRtcVideoCapturer() + : factory_(new WebRtcVcmFactory), + module_(NULL), + captured_frames_(0) { +} + +WebRtcVideoCapturer::WebRtcVideoCapturer(WebRtcVcmFactoryInterface* factory) + : factory_(factory), + module_(NULL), + captured_frames_(0) { +} + +WebRtcVideoCapturer::~WebRtcVideoCapturer() { + if (module_) { + module_->Release(); + } +} + +bool WebRtcVideoCapturer::Init(const Device& device) { + if (module_) { + LOG(LS_ERROR) << "The capturer is already initialized"; + return false; + } + + webrtc::VideoCaptureModule::DeviceInfo* info = factory_->CreateDeviceInfo(0); + if (!info) { + return false; + } + + // Find the desired camera, by name. + // In the future, comparing IDs will be more robust. + // TODO(juberti): Figure what's needed to allow this. + int num_cams = info->NumberOfDevices(); + char vcm_id[256] = ""; + bool found = false; + for (int index = 0; index < num_cams; ++index) { + char vcm_name[256]; + if (info->GetDeviceName(index, vcm_name, ARRAY_SIZE(vcm_name), + vcm_id, ARRAY_SIZE(vcm_id)) != -1) { + if (device.name == reinterpret_cast(vcm_name)) { + found = true; + break; + } + } + } + if (!found) { + LOG(LS_WARNING) << "Failed to find capturer for id: " << device.id; + factory_->DestroyDeviceInfo(info); + return false; + } + + // Enumerate the supported formats. + // TODO(juberti): Find out why this starts/stops the camera... + std::vector supported; + int32_t num_caps = info->NumberOfCapabilities(vcm_id); + for (int32_t i = 0; i < num_caps; ++i) { + webrtc::VideoCaptureCapability cap; + if (info->GetCapability(vcm_id, i, cap) != -1) { + VideoFormat format; + if (CapabilityToFormat(cap, &format)) { + supported.push_back(format); + } else { + LOG(LS_WARNING) << "Ignoring unsupported WebRTC capture format " + << cap.rawType; + } + } + } + factory_->DestroyDeviceInfo(info); + if (supported.empty()) { + LOG(LS_ERROR) << "Failed to find usable formats for id: " << device.id; + return false; + } + + module_ = factory_->Create(0, vcm_id); + if (!module_) { + LOG(LS_ERROR) << "Failed to create capturer for id: " << device.id; + return false; + } + + // It is safe to change member attributes now. + module_->AddRef(); + SetId(device.id); + SetSupportedFormats(supported); + return true; +} + +bool WebRtcVideoCapturer::Init(webrtc::VideoCaptureModule* module) { + if (module_) { + LOG(LS_ERROR) << "The capturer is already initialized"; + return false; + } + if (!module) { + LOG(LS_ERROR) << "Invalid VCM supplied"; + return false; + } + // TODO(juberti): Set id and formats. + (module_ = module)->AddRef(); + return true; +} + +bool WebRtcVideoCapturer::GetBestCaptureFormat(const VideoFormat& desired, + VideoFormat* best_format) { + if (!best_format) { + return false; + } + + if (!VideoCapturer::GetBestCaptureFormat(desired, best_format)) { + // We maybe using a manually injected VCM which doesn't support enum. + // Use the desired format as the best format. + best_format->width = desired.width; + best_format->height = desired.height; + best_format->fourcc = FOURCC_I420; + best_format->interval = desired.interval; + LOG(LS_INFO) << "Failed to find best capture format," + << " fall back to the requested format " + << best_format->ToString(); + } + return true; +} + +CaptureState WebRtcVideoCapturer::Start(const VideoFormat& capture_format) { + if (!module_) { + LOG(LS_ERROR) << "The capturer has not been initialized"; + return CS_NO_DEVICE; + } + + // TODO(hellner): weird to return failure when it is in fact actually running. + if (IsRunning()) { + LOG(LS_ERROR) << "The capturer is already running"; + return CS_FAILED; + } + + SetCaptureFormat(&capture_format); + + webrtc::VideoCaptureCapability cap; + if (!FormatToCapability(capture_format, &cap)) { + LOG(LS_ERROR) << "Invalid capture format specified"; + return CS_FAILED; + } + + std::string camera_id(GetId()); + uint32 start = talk_base::Time(); + if (module_->RegisterCaptureDataCallback(*this) != 0 || + module_->StartCapture(cap) != 0) { + LOG(LS_ERROR) << "Camera '" << camera_id << "' failed to start"; + return CS_FAILED; + } + + LOG(LS_INFO) << "Camera '" << camera_id << "' started with format " + << capture_format.ToString() << ", elapsed time " + << talk_base::TimeSince(start) << " ms"; + + captured_frames_ = 0; + SetCaptureState(CS_RUNNING); + return CS_STARTING; +} + +void WebRtcVideoCapturer::Stop() { + if (IsRunning()) { + talk_base::Thread::Current()->Clear(this); + module_->StopCapture(); + module_->DeRegisterCaptureDataCallback(); + + // TODO(juberti): Determine if the VCM exposes any drop stats we can use. + double drop_ratio = 0.0; + std::string camera_id(GetId()); + LOG(LS_INFO) << "Camera '" << camera_id << "' stopped after capturing " + << captured_frames_ << " frames and dropping " + << drop_ratio << "%"; + } + SetCaptureFormat(NULL); +} + +bool WebRtcVideoCapturer::IsRunning() { + return (module_ != NULL && module_->CaptureStarted()); +} + +bool WebRtcVideoCapturer::GetPreferredFourccs( + std::vector* fourccs) { + if (!fourccs) { + return false; + } + + fourccs->clear(); + for (size_t i = 0; i < ARRAY_SIZE(kSupportedFourCCs); ++i) { + fourccs->push_back(kSupportedFourCCs[i].fourcc); + } + return true; +} + +void WebRtcVideoCapturer::OnIncomingCapturedFrame(const int32_t id, + webrtc::I420VideoFrame& sample) { + ASSERT(IsRunning()); + + ++captured_frames_; + // Log the size and pixel aspect ratio of the first captured frame. + if (1 == captured_frames_) { + LOG(LS_INFO) << "Captured frame size " + << sample.width() << "x" << sample.height() + << ". Expected format " << GetCaptureFormat()->ToString(); + } + + // Signal down stream components on captured frame. + // The CapturedFrame class doesn't support planes. We have to ExtractBuffer + // to one block for it. + int length = webrtc::CalcBufferSize(webrtc::kI420, + sample.width(), sample.height()); + if (!captured_frame_.get() || + captured_frame_->length() != static_cast(length)) { + captured_frame_.reset(new FrameBuffer(length)); + } + // TODO(ronghuawu): Refactor the WebRtcVideoFrame to avoid memory copy. + webrtc::ExtractBuffer(sample, length, + reinterpret_cast(captured_frame_->data())); + WebRtcCapturedFrame frame(sample, captured_frame_->data(), length); + SignalFrameCaptured(this, &frame); +} + +void WebRtcVideoCapturer::OnCaptureDelayChanged(const int32_t id, + const int32_t delay) { + LOG(LS_INFO) << "Capture delay changed to " << delay << " ms"; +} + +// WebRtcCapturedFrame +WebRtcCapturedFrame::WebRtcCapturedFrame(const webrtc::I420VideoFrame& sample, + void* buffer, + int length) { + width = sample.width(); + height = sample.height(); + fourcc = FOURCC_I420; + // TODO(hellner): Support pixel aspect ratio (for OSX). + pixel_width = 1; + pixel_height = 1; + // Convert units from VideoFrame RenderTimeMs to CapturedFrame (nanoseconds). + elapsed_time = sample.render_time_ms() * talk_base::kNumNanosecsPerMillisec; + time_stamp = elapsed_time; + data_size = length; + data = buffer; +} + +} // namespace cricket + +#endif // HAVE_WEBRTC_VIDEO diff --git a/talk/media/webrtc/webrtcvideocapturer.h b/talk/media/webrtc/webrtcvideocapturer.h new file mode 100644 index 000000000..eb9995644 --- /dev/null +++ b/talk/media/webrtc/webrtcvideocapturer.h @@ -0,0 +1,103 @@ +// libjingle +// Copyright 2004 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. + +#ifndef TALK_MEDIA_WEBRTCVIDEOCAPTURER_H_ +#define TALK_MEDIA_WEBRTCVIDEOCAPTURER_H_ + +#ifdef HAVE_WEBRTC_VIDEO + +#include +#include + +#include "talk/base/messagehandler.h" +#include "talk/media/base/videocapturer.h" +#include "talk/media/webrtc/webrtcvideoframe.h" +#include "webrtc/common_video/libyuv/include/webrtc_libyuv.h" +#include "webrtc/modules/video_capture/include/video_capture.h" + +namespace cricket { + +// Factory to allow injection of a VCM impl into WebRtcVideoCapturer. +// DeviceInfos do not have a Release() and therefore need an explicit Destroy(). +class WebRtcVcmFactoryInterface { + public: + virtual ~WebRtcVcmFactoryInterface() {} + virtual webrtc::VideoCaptureModule* Create( + int id, const char* device) = 0; + virtual webrtc::VideoCaptureModule::DeviceInfo* CreateDeviceInfo(int id) = 0; + virtual void DestroyDeviceInfo( + webrtc::VideoCaptureModule::DeviceInfo* info) = 0; +}; + +// WebRTC-based implementation of VideoCapturer. +class WebRtcVideoCapturer : public VideoCapturer, + public webrtc::VideoCaptureDataCallback { + public: + WebRtcVideoCapturer(); + explicit WebRtcVideoCapturer(WebRtcVcmFactoryInterface* factory); + virtual ~WebRtcVideoCapturer(); + + bool Init(const Device& device); + bool Init(webrtc::VideoCaptureModule* module); + + // Override virtual methods of the parent class VideoCapturer. + virtual bool GetBestCaptureFormat(const VideoFormat& desired, + VideoFormat* best_format); + virtual CaptureState Start(const VideoFormat& capture_format); + virtual void Stop(); + virtual bool IsRunning(); + virtual bool IsScreencast() const { return false; } + + protected: + // Override virtual methods of the parent class VideoCapturer. + virtual bool GetPreferredFourccs(std::vector* fourccs); + + private: + // Callback when a frame is captured by camera. + virtual void OnIncomingCapturedFrame(const int32_t id, + webrtc::I420VideoFrame& frame); + virtual void OnIncomingCapturedEncodedFrame(const int32_t id, + webrtc::VideoFrame& frame, + webrtc::VideoCodecType codec_type) { + } + virtual void OnCaptureDelayChanged(const int32_t id, + const int32_t delay); + + talk_base::scoped_ptr factory_; + webrtc::VideoCaptureModule* module_; + int captured_frames_; + talk_base::scoped_ptr captured_frame_; +}; + +struct WebRtcCapturedFrame : public CapturedFrame { + public: + WebRtcCapturedFrame(const webrtc::I420VideoFrame& frame, + void* buffer, int length); +}; + +} // namespace cricket + +#endif // HAVE_WEBRTC_VIDEO +#endif // TALK_MEDIA_WEBRTCVIDEOCAPTURER_H_ diff --git a/talk/media/webrtc/webrtcvideocapturer_unittest.cc b/talk/media/webrtc/webrtcvideocapturer_unittest.cc new file mode 100644 index 000000000..226aa4b33 --- /dev/null +++ b/talk/media/webrtc/webrtcvideocapturer_unittest.cc @@ -0,0 +1,145 @@ +// libjingle +// Copyright 2004 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 +#include +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/media/base/testutils.h" +#include "talk/media/base/videocommon.h" +#include "talk/media/webrtc/fakewebrtcvcmfactory.h" +#include "talk/media/webrtc/webrtcvideocapturer.h" + +using cricket::VideoFormat; + +static const std::string kTestDeviceName = "JuberTech FakeCam Q123"; +static const std::string kTestDeviceId = "foo://bar/baz"; +const VideoFormat kDefaultVideoFormat = + VideoFormat(640, 400, VideoFormat::FpsToInterval(30), cricket::FOURCC_ANY); + +class WebRtcVideoCapturerTest : public testing::Test { + public: + WebRtcVideoCapturerTest() + : factory_(new FakeWebRtcVcmFactory), + capturer_(new cricket::WebRtcVideoCapturer(factory_)), + listener_(capturer_.get()) { + factory_->device_info.AddDevice(kTestDeviceName, kTestDeviceId); + // add a VGA/I420 capability + webrtc::VideoCaptureCapability vga; + vga.width = 640; + vga.height = 480; + vga.maxFPS = 30; + vga.rawType = webrtc::kVideoI420; + factory_->device_info.AddCapability(kTestDeviceId, vga); + } + + protected: + FakeWebRtcVcmFactory* factory_; // owned by capturer_ + talk_base::scoped_ptr capturer_; + cricket::VideoCapturerListener listener_; +}; + +TEST_F(WebRtcVideoCapturerTest, TestNotOpened) { + EXPECT_EQ("", capturer_->GetId()); + EXPECT_TRUE(capturer_->GetSupportedFormats()->empty()); + EXPECT_TRUE(capturer_->GetCaptureFormat() == NULL); + EXPECT_FALSE(capturer_->IsRunning()); +} + +TEST_F(WebRtcVideoCapturerTest, TestBadInit) { + EXPECT_FALSE(capturer_->Init(cricket::Device("bad-name", "bad-id"))); + EXPECT_FALSE(capturer_->IsRunning()); +} + +TEST_F(WebRtcVideoCapturerTest, TestInit) { + EXPECT_TRUE(capturer_->Init(cricket::Device(kTestDeviceName, kTestDeviceId))); + EXPECT_EQ(kTestDeviceId, capturer_->GetId()); + EXPECT_TRUE(NULL != capturer_->GetSupportedFormats()); + ASSERT_EQ(1U, capturer_->GetSupportedFormats()->size()); + EXPECT_EQ(640, (*capturer_->GetSupportedFormats())[0].width); + EXPECT_EQ(480, (*capturer_->GetSupportedFormats())[0].height); + EXPECT_TRUE(capturer_->GetCaptureFormat() == NULL); // not started yet + EXPECT_FALSE(capturer_->IsRunning()); +} + +TEST_F(WebRtcVideoCapturerTest, TestInitVcm) { + EXPECT_TRUE(capturer_->Init(factory_->Create(0, + reinterpret_cast(kTestDeviceId.c_str())))); +} + +TEST_F(WebRtcVideoCapturerTest, TestCapture) { + EXPECT_TRUE(capturer_->Init(cricket::Device(kTestDeviceName, kTestDeviceId))); + cricket::VideoFormat format( + capturer_->GetSupportedFormats()->at(0)); + EXPECT_EQ(cricket::CS_STARTING, capturer_->Start(format)); + EXPECT_TRUE(capturer_->IsRunning()); + ASSERT_TRUE(capturer_->GetCaptureFormat() != NULL); + EXPECT_EQ(format, *capturer_->GetCaptureFormat()); + EXPECT_EQ_WAIT(cricket::CS_RUNNING, listener_.last_capture_state(), 1000); + EXPECT_TRUE(factory_->modules[0]->SendFrame(640, 480)); + EXPECT_TRUE_WAIT(listener_.frame_count() > 0, 5000); + EXPECT_EQ(capturer_->GetCaptureFormat()->fourcc, listener_.frame_fourcc()); + EXPECT_EQ(640, listener_.frame_width()); + EXPECT_EQ(480, listener_.frame_height()); + EXPECT_EQ(cricket::CS_FAILED, capturer_->Start(format)); + capturer_->Stop(); + EXPECT_FALSE(capturer_->IsRunning()); + EXPECT_TRUE(capturer_->GetCaptureFormat() == NULL); +} + +TEST_F(WebRtcVideoCapturerTest, TestCaptureVcm) { + EXPECT_TRUE(capturer_->Init(factory_->Create(0, + reinterpret_cast(kTestDeviceId.c_str())))); + EXPECT_TRUE(capturer_->GetSupportedFormats()->empty()); + VideoFormat format; + EXPECT_TRUE(capturer_->GetBestCaptureFormat(kDefaultVideoFormat, &format)); + EXPECT_EQ(kDefaultVideoFormat.width, format.width); + EXPECT_EQ(kDefaultVideoFormat.height, format.height); + EXPECT_EQ(kDefaultVideoFormat.interval, format.interval); + EXPECT_EQ(cricket::FOURCC_I420, format.fourcc); + EXPECT_EQ(cricket::CS_STARTING, capturer_->Start(format)); + EXPECT_TRUE(capturer_->IsRunning()); + ASSERT_TRUE(capturer_->GetCaptureFormat() != NULL); + EXPECT_EQ(format, *capturer_->GetCaptureFormat()); + EXPECT_EQ_WAIT(cricket::CS_RUNNING, listener_.last_capture_state(), 1000); + EXPECT_TRUE(factory_->modules[0]->SendFrame(640, 480)); + EXPECT_TRUE_WAIT(listener_.frame_count() > 0, 5000); + EXPECT_EQ(capturer_->GetCaptureFormat()->fourcc, listener_.frame_fourcc()); + EXPECT_EQ(640, listener_.frame_width()); + EXPECT_EQ(480, listener_.frame_height()); + EXPECT_EQ(cricket::CS_FAILED, capturer_->Start(format)); + capturer_->Stop(); + EXPECT_FALSE(capturer_->IsRunning()); + EXPECT_TRUE(capturer_->GetCaptureFormat() == NULL); +} + +TEST_F(WebRtcVideoCapturerTest, TestCaptureWithoutInit) { + cricket::VideoFormat format; + EXPECT_EQ(cricket::CS_NO_DEVICE, capturer_->Start(format)); + EXPECT_TRUE(capturer_->GetCaptureFormat() == NULL); + EXPECT_FALSE(capturer_->IsRunning()); +} diff --git a/talk/media/webrtc/webrtcvideodecoderfactory.h b/talk/media/webrtc/webrtcvideodecoderfactory.h new file mode 100644 index 000000000..483bca7d3 --- /dev/null +++ b/talk/media/webrtc/webrtcvideodecoderfactory.h @@ -0,0 +1,53 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#ifndef TALK_MEDIA_WEBRTC_WEBRTCVIDEODECODERFACTORY_H_ +#define TALK_MEDIA_WEBRTC_WEBRTCVIDEODECODERFACTORY_H_ + +#include "talk/base/refcount.h" +#include "webrtc/common_types.h" + +namespace webrtc { +class VideoDecoder; +} + +namespace cricket { + +class WebRtcVideoDecoderFactory { + public: + // Caller takes the ownership of the returned object and it should be released + // by calling DestroyVideoDecoder(). + virtual webrtc::VideoDecoder* CreateVideoDecoder( + webrtc::VideoCodecType type) = 0; + virtual ~WebRtcVideoDecoderFactory() {} + + virtual void DestroyVideoDecoder(webrtc::VideoDecoder* decoder) = 0; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_WEBRTC_WEBRTCVIDEODECODERFACTORY_H_ diff --git a/talk/media/webrtc/webrtcvideoencoderfactory.h b/talk/media/webrtc/webrtcvideoencoderfactory.h new file mode 100644 index 000000000..a84430963 --- /dev/null +++ b/talk/media/webrtc/webrtcvideoencoderfactory.h @@ -0,0 +1,89 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#ifndef TALK_MEDIA_WEBRTC_WEBRTCVIDEOENCODERFACTORY_H_ +#define TALK_MEDIA_WEBRTC_WEBRTCVIDEOENCODERFACTORY_H_ + +#include "talk/base/refcount.h" +#include "talk/media/base/codec.h" +#include "webrtc/common_types.h" + +namespace webrtc { +class VideoEncoder; +} + +namespace cricket { + +class WebRtcVideoEncoderFactory { + public: + struct VideoCodec { + webrtc::VideoCodecType type; + std::string name; + int max_width; + int max_height; + int max_fps; + + VideoCodec(webrtc::VideoCodecType t, const std::string& nm, int w, int h, + int fr) + : type(t), name(nm), max_width(w), max_height(h), max_fps(fr) { + } + }; + + class Observer { + public: + // Invoked when the list of supported codecs becomes available. + // This will not be invoked if the list of codecs is already available when + // the factory is installed. Otherwise this will be invoked only once if the + // list of codecs is not yet available when the factory is installed. + virtual void OnCodecsAvailable() = 0; + + protected: + virtual ~Observer() {} + }; + + // Caller takes the ownership of the returned object and it should be released + // by calling DestroyVideoEncoder(). + virtual webrtc::VideoEncoder* CreateVideoEncoder( + webrtc::VideoCodecType type) = 0; + virtual ~WebRtcVideoEncoderFactory() {} + + // Adds/removes observer to receive OnCodecsChanged notifications. + // Factory must outlive Observer. Observer is responsible for removing itself + // from the Factory by the time its dtor is done. + virtual void AddObserver(Observer* observer) = 0; + virtual void RemoveObserver(Observer* observer) = 0; + + // Returns a list of supported codecs in order of preference. + // The list is empty if the list of codecs is not yet available. + virtual const std::vector& codecs() const = 0; + + virtual void DestroyVideoEncoder(webrtc::VideoEncoder* encoder) = 0; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_WEBRTC_WEBRTCVIDEOENCODERFACTORY_H_ diff --git a/talk/media/webrtc/webrtcvideoengine.cc b/talk/media/webrtc/webrtcvideoengine.cc new file mode 100644 index 000000000..5047974a9 --- /dev/null +++ b/talk/media/webrtc/webrtcvideoengine.cc @@ -0,0 +1,3626 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifdef HAVE_WEBRTC_VIDEO +#include "talk/media/webrtc/webrtcvideoengine.h" + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/buffer.h" +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/cpumonitor.h" +#include "talk/base/logging.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/rtputils.h" +#include "talk/media/base/streamparams.h" +#include "talk/media/base/videoadapter.h" +#include "talk/media/base/videocapturer.h" +#include "talk/media/base/videorenderer.h" +#include "talk/media/devices/filevideocapturer.h" +#include "talk/media/webrtc/webrtcvideodecoderfactory.h" +#include "talk/media/webrtc/webrtcvideoencoderfactory.h" +#include "talk/media/webrtc/webrtcpassthroughrender.h" +#include "talk/media/webrtc/webrtcvideocapturer.h" +#include "talk/media/webrtc/webrtcvideoframe.h" +#include "talk/media/webrtc/webrtcvie.h" +#include "talk/media/webrtc/webrtcvoe.h" +#include "talk/media/webrtc/webrtcvoiceengine.h" + +#if !defined(LIBPEERCONNECTION_LIB) +#ifndef HAVE_WEBRTC_VIDEO +#error Need webrtc video +#endif +#include "talk/media/webrtc/webrtcmediaengine.h" + +WRME_EXPORT +cricket::MediaEngineInterface* CreateWebRtcMediaEngine( + webrtc::AudioDeviceModule* adm, webrtc::AudioDeviceModule* adm_sc, + cricket::WebRtcVideoEncoderFactory* encoder_factory, + cricket::WebRtcVideoDecoderFactory* decoder_factory) { + return new cricket::WebRtcMediaEngine(adm, adm_sc, encoder_factory, + decoder_factory); +} + +WRME_EXPORT +void DestroyWebRtcMediaEngine(cricket::MediaEngineInterface* media_engine) { + delete static_cast(media_engine); +} +#endif + + +namespace cricket { + + +static const int kDefaultLogSeverity = talk_base::LS_WARNING; + +static const int kMinVideoBitrate = 50; +static const int kStartVideoBitrate = 300; +static const int kMaxVideoBitrate = 2000; +static const int kDefaultConferenceModeMaxVideoBitrate = 500; + +static const int kVideoMtu = 1200; + +static const int kVideoRtpBufferSize = 65536; + +static const char kVp8PayloadName[] = "VP8"; +static const char kRedPayloadName[] = "red"; +static const char kFecPayloadName[] = "ulpfec"; + +static const int kDefaultNumberOfTemporalLayers = 1; // 1:1 + +static const int kTimestampDeltaInSecondsForWarning = 2; + +static const int kMaxExternalVideoCodecs = 8; +static const int kExternalVideoPayloadTypeBase = 120; + +// Static allocation of payload type values for external video codec. +static int GetExternalVideoPayloadType(int index) { + ASSERT(index >= 0 && index < kMaxExternalVideoCodecs); + return kExternalVideoPayloadTypeBase + index; +} + +static void LogMultiline(talk_base::LoggingSeverity sev, char* text) { + const char* delim = "\r\n"; + // TODO(fbarchard): Fix strtok lint warning. + for (char* tok = strtok(text, delim); tok; tok = strtok(NULL, delim)) { + LOG_V(sev) << tok; + } +} + +// Severity is an integer because it comes is assumed to be from command line. +static int SeverityToFilter(int severity) { + int filter = webrtc::kTraceNone; + switch (severity) { + case talk_base::LS_VERBOSE: + filter |= webrtc::kTraceAll; + case talk_base::LS_INFO: + filter |= (webrtc::kTraceStateInfo | webrtc::kTraceInfo); + case talk_base::LS_WARNING: + filter |= (webrtc::kTraceTerseInfo | webrtc::kTraceWarning); + case talk_base::LS_ERROR: + filter |= (webrtc::kTraceError | webrtc::kTraceCritical); + } + return filter; +} + +static const int kCpuMonitorPeriodMs = 2000; // 2 seconds. + +static const bool kNotSending = false; + +// Extension header for RTP timestamp offset, see RFC 5450 for details: +// http://tools.ietf.org/html/rfc5450 +static const char kRtpTimestampOffsetHeaderExtension[] = + "urn:ietf:params:rtp-hdrext:toffset"; +static const int kRtpTimeOffsetExtensionId = 2; + +// Extension header for absolute send time, see url for details: +// http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +static const char kRtpAbsoluteSendTimeHeaderExtension[] = + "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"; +static const int kRtpAbsoluteSendTimeExtensionId = 3; + +static bool IsNackEnabled(const VideoCodec& codec) { + return codec.HasFeedbackParam(FeedbackParam(kRtcpFbParamNack, + kParamValueEmpty)); +} + +// Returns true if Receiver Estimated Max Bitrate is enabled. +static bool IsRembEnabled(const VideoCodec& codec) { + return codec.HasFeedbackParam(FeedbackParam(kRtcpFbParamRemb, + kParamValueEmpty)); +} + +struct FlushBlackFrameData : public talk_base::MessageData { + FlushBlackFrameData(uint32 s, int64 t) : ssrc(s), timestamp(t) { + } + uint32 ssrc; + int64 timestamp; +}; + +class WebRtcRenderAdapter : public webrtc::ExternalRenderer { + public: + explicit WebRtcRenderAdapter(VideoRenderer* renderer) + : renderer_(renderer), width_(0), height_(0), watermark_enabled_(false) { + } + virtual ~WebRtcRenderAdapter() { + } + void set_watermark_enabled(bool enable) { + talk_base::CritScope cs(&crit_); + watermark_enabled_ = enable; + } + void SetRenderer(VideoRenderer* renderer) { + talk_base::CritScope cs(&crit_); + renderer_ = renderer; + // FrameSizeChange may have already been called when renderer was not set. + // If so we should call SetSize here. + // TODO(ronghuawu): Add unit test for this case. Didn't do it now + // because the WebRtcRenderAdapter is currently hiding in cc file. No + // good way to get access to it from the unit test. + if (width_ > 0 && height_ > 0 && renderer_ != NULL) { + if (!renderer_->SetSize(width_, height_, 0)) { + LOG(LS_ERROR) + << "WebRtcRenderAdapter SetRenderer failed to SetSize to: " + << width_ << "x" << height_; + } + } + } + // Implementation of webrtc::ExternalRenderer. + virtual int FrameSizeChange(unsigned int width, unsigned int height, + unsigned int /*number_of_streams*/) { + talk_base::CritScope cs(&crit_); + width_ = width; + height_ = height; + LOG(LS_INFO) << "WebRtcRenderAdapter frame size changed to: " + << width << "x" << height; + if (renderer_ == NULL) { + LOG(LS_VERBOSE) << "WebRtcRenderAdapter the renderer has not been set. " + << "SetSize will be called later in SetRenderer."; + return 0; + } + return renderer_->SetSize(width_, height_, 0) ? 0 : -1; + } + virtual int DeliverFrame(unsigned char* buffer, int buffer_size, + uint32_t time_stamp, int64_t render_time) { + talk_base::CritScope cs(&crit_); + frame_rate_tracker_.Update(1); + if (renderer_ == NULL) { + return 0; + } + WebRtcVideoFrame video_frame; + // Convert 90K rtp timestamp to ns timestamp. + int64 rtp_time_stamp_in_ns = (time_stamp / 90) * + talk_base::kNumNanosecsPerMillisec; + // Convert milisecond render time to ns timestamp. + int64 render_time_stamp_in_ns = render_time * + talk_base::kNumNanosecsPerMillisec; + // Send the rtp timestamp to renderer as the VideoFrame timestamp. + // and the render timestamp as the VideoFrame elapsed_time. + video_frame.Attach(buffer, buffer_size, width_, height_, + 1, 1, render_time_stamp_in_ns, + rtp_time_stamp_in_ns, 0); + + + // Sanity check on decoded frame size. + if (buffer_size != static_cast(VideoFrame::SizeOf(width_, height_))) { + LOG(LS_WARNING) << "WebRtcRenderAdapter received a strange frame size: " + << buffer_size; + } + + int ret = renderer_->RenderFrame(&video_frame) ? 0 : -1; + uint8* buffer_temp; + size_t buffer_size_temp; + video_frame.Detach(&buffer_temp, &buffer_size_temp); + return ret; + } + + unsigned int width() { + talk_base::CritScope cs(&crit_); + return width_; + } + unsigned int height() { + talk_base::CritScope cs(&crit_); + return height_; + } + int framerate() { + talk_base::CritScope cs(&crit_); + return frame_rate_tracker_.units_second(); + } + VideoRenderer* renderer() { + talk_base::CritScope cs(&crit_); + return renderer_; + } + + private: + talk_base::CriticalSection crit_; + VideoRenderer* renderer_; + unsigned int width_; + unsigned int height_; + talk_base::RateTracker frame_rate_tracker_; + bool watermark_enabled_; +}; + +class WebRtcDecoderObserver : public webrtc::ViEDecoderObserver { + public: + explicit WebRtcDecoderObserver(int video_channel) + : video_channel_(video_channel), + framerate_(0), + bitrate_(0), + firs_requested_(0) { + } + + // virtual functions from VieDecoderObserver. + virtual void IncomingCodecChanged(const int videoChannel, + const webrtc::VideoCodec& videoCodec) {} + virtual void IncomingRate(const int videoChannel, + const unsigned int framerate, + const unsigned int bitrate) { + ASSERT(video_channel_ == videoChannel); + framerate_ = framerate; + bitrate_ = bitrate; + } + virtual void RequestNewKeyFrame(const int videoChannel) { + ASSERT(video_channel_ == videoChannel); + ++firs_requested_; + } + + int framerate() const { return framerate_; } + int bitrate() const { return bitrate_; } + int firs_requested() const { return firs_requested_; } + + private: + int video_channel_; + int framerate_; + int bitrate_; + int firs_requested_; +}; + +class WebRtcEncoderObserver : public webrtc::ViEEncoderObserver { + public: + explicit WebRtcEncoderObserver(int video_channel) + : video_channel_(video_channel), + framerate_(0), + bitrate_(0) { + } + + // virtual functions from VieEncoderObserver. + virtual void OutgoingRate(const int videoChannel, + const unsigned int framerate, + const unsigned int bitrate) { + ASSERT(video_channel_ == videoChannel); + framerate_ = framerate; + bitrate_ = bitrate; + } + + int framerate() const { return framerate_; } + int bitrate() const { return bitrate_; } + + private: + int video_channel_; + int framerate_; + int bitrate_; +}; + +class WebRtcLocalStreamInfo { + public: + WebRtcLocalStreamInfo() + : width_(0), height_(0), elapsed_time_(-1), time_stamp_(-1) {} + size_t width() const { + talk_base::CritScope cs(&crit_); + return width_; + } + size_t height() const { + talk_base::CritScope cs(&crit_); + return height_; + } + int64 elapsed_time() const { + talk_base::CritScope cs(&crit_); + return elapsed_time_; + } + int64 time_stamp() const { + talk_base::CritScope cs(&crit_); + return time_stamp_; + } + int framerate() { + talk_base::CritScope cs(&crit_); + return rate_tracker_.units_second(); + } + void GetLastFrameInfo( + size_t* width, size_t* height, int64* elapsed_time) const { + talk_base::CritScope cs(&crit_); + *width = width_; + *height = height_; + *elapsed_time = elapsed_time_; + } + + void UpdateFrame(const VideoFrame* frame) { + talk_base::CritScope cs(&crit_); + + width_ = frame->GetWidth(); + height_ = frame->GetHeight(); + elapsed_time_ = frame->GetElapsedTime(); + time_stamp_ = frame->GetTimeStamp(); + + rate_tracker_.Update(1); + } + + private: + mutable talk_base::CriticalSection crit_; + size_t width_; + size_t height_; + int64 elapsed_time_; + int64 time_stamp_; + talk_base::RateTracker rate_tracker_; + + DISALLOW_COPY_AND_ASSIGN(WebRtcLocalStreamInfo); +}; + +// WebRtcVideoChannelRecvInfo is a container class with members such as renderer +// and a decoder observer that is used by receive channels. +// It must exist as long as the receive channel is connected to renderer or a +// decoder observer in this class and methods in the class should only be called +// from the worker thread. +class WebRtcVideoChannelRecvInfo { + public: + typedef std::map DecoderMap; // key: payload type + explicit WebRtcVideoChannelRecvInfo(int channel_id) + : channel_id_(channel_id), + render_adapter_(NULL), + decoder_observer_(channel_id) { + } + int channel_id() { return channel_id_; } + void SetRenderer(VideoRenderer* renderer) { + render_adapter_.SetRenderer(renderer); + } + WebRtcRenderAdapter* render_adapter() { return &render_adapter_; } + WebRtcDecoderObserver* decoder_observer() { return &decoder_observer_; } + void RegisterDecoder(int pl_type, webrtc::VideoDecoder* decoder) { + ASSERT(!IsDecoderRegistered(pl_type)); + registered_decoders_[pl_type] = decoder; + } + bool IsDecoderRegistered(int pl_type) { + return registered_decoders_.count(pl_type) != 0; + } + const DecoderMap& registered_decoders() { + return registered_decoders_; + } + void ClearRegisteredDecoders() { + registered_decoders_.clear(); + } + + private: + int channel_id_; // Webrtc video channel number. + // Renderer for this channel. + WebRtcRenderAdapter render_adapter_; + WebRtcDecoderObserver decoder_observer_; + DecoderMap registered_decoders_; +}; + +class WebRtcVideoChannelSendInfo { + public: + typedef std::map EncoderMap; // key: payload type + WebRtcVideoChannelSendInfo(int channel_id, int capture_id, + webrtc::ViEExternalCapture* external_capture, + talk_base::CpuMonitor* cpu_monitor) + : channel_id_(channel_id), + capture_id_(capture_id), + sending_(false), + muted_(false), + video_capturer_(NULL), + encoder_observer_(channel_id), + external_capture_(external_capture), + capturer_updated_(false), + interval_(0), + video_adapter_(new CoordinatedVideoAdapter) { + // TODO(asapersson): + // video_adapter_->SignalCpuAdaptationUnable.connect( + // this, &WebRtcVideoChannelSendInfo::OnCpuAdaptationUnable); + if (cpu_monitor) { + cpu_monitor->SignalUpdate.connect( + video_adapter_.get(), &CoordinatedVideoAdapter::OnCpuLoadUpdated); + } + } + + int channel_id() const { return channel_id_; } + int capture_id() const { return capture_id_; } + void set_sending(bool sending) { sending_ = sending; } + bool sending() const { return sending_; } + void set_muted(bool on) { + // TODO(asapersson): add support. + // video_adapter_->SetBlackOutput(on); + muted_ = on; + } + bool muted() {return muted_; } + + WebRtcEncoderObserver* encoder_observer() { return &encoder_observer_; } + webrtc::ViEExternalCapture* external_capture() { return external_capture_; } + const VideoFormat& video_format() const { + return video_format_; + } + void set_video_format(const VideoFormat& video_format) { + video_format_ = video_format; + if (video_format_ != cricket::VideoFormat()) { + interval_ = video_format_.interval; + } + video_adapter_->OnOutputFormatRequest(video_format_); + } + void set_interval(int64 interval) { + if (video_format() == cricket::VideoFormat()) { + interval_ = interval; + } + } + int64 interval() { return interval_; } + + void InitializeAdapterOutputFormat(const webrtc::VideoCodec& codec) { + VideoFormat format(codec.width, codec.height, + VideoFormat::FpsToInterval(codec.maxFramerate), + FOURCC_I420); + if (video_adapter_->output_format().IsSize0x0()) { + video_adapter_->SetOutputFormat(format); + } + } + + bool AdaptFrame(const VideoFrame* in_frame, const VideoFrame** out_frame) { + *out_frame = NULL; + return video_adapter_->AdaptFrame(in_frame, out_frame); + } + int CurrentAdaptReason() const { + return video_adapter_->adapt_reason(); + } + + StreamParams* stream_params() { return stream_params_.get(); } + void set_stream_params(const StreamParams& sp) { + stream_params_.reset(new StreamParams(sp)); + } + void ClearStreamParams() { stream_params_.reset(); } + bool has_ssrc(uint32 local_ssrc) const { + return !stream_params_ ? false : + stream_params_->has_ssrc(local_ssrc); + } + WebRtcLocalStreamInfo* local_stream_info() { + return &local_stream_info_; + } + VideoCapturer* video_capturer() { + return video_capturer_; + } + void set_video_capturer(VideoCapturer* video_capturer) { + if (video_capturer == video_capturer_) { + return; + } + capturer_updated_ = true; + video_capturer_ = video_capturer; + if (video_capturer && !video_capturer->IsScreencast()) { + const VideoFormat* capture_format = video_capturer->GetCaptureFormat(); + if (capture_format) { + video_adapter_->SetInputFormat(*capture_format); + } + } + } + + void ApplyCpuOptions(const VideoOptions& options) { + bool cpu_adapt; + float low, med, high; + if (options.adapt_input_to_cpu_usage.Get(&cpu_adapt)) { + video_adapter_->set_cpu_adaptation(cpu_adapt); + } + if (options.process_adaptation_threshhold.Get(&med)) { + video_adapter_->set_process_threshold(med); + } + if (options.system_low_adaptation_threshhold.Get(&low)) { + video_adapter_->set_low_system_threshold(low); + } + if (options.system_high_adaptation_threshhold.Get(&high)) { + video_adapter_->set_high_system_threshold(high); + } + } + void ProcessFrame(const VideoFrame& original_frame, bool mute, + VideoFrame** processed_frame) { + if (!mute) { + *processed_frame = original_frame.Copy(); + } else { + WebRtcVideoFrame* black_frame = new WebRtcVideoFrame(); + black_frame->InitToBlack(original_frame.GetWidth(), + original_frame.GetHeight(), 1, 1, + original_frame.GetElapsedTime(), + original_frame.GetTimeStamp()); + *processed_frame = black_frame; + } + local_stream_info_.UpdateFrame(*processed_frame); + } + void RegisterEncoder(int pl_type, webrtc::VideoEncoder* encoder) { + ASSERT(!IsEncoderRegistered(pl_type)); + registered_encoders_[pl_type] = encoder; + } + bool IsEncoderRegistered(int pl_type) { + return registered_encoders_.count(pl_type) != 0; + } + const EncoderMap& registered_encoders() { + return registered_encoders_; + } + void ClearRegisteredEncoders() { + registered_encoders_.clear(); + } + + private: + int channel_id_; + int capture_id_; + bool sending_; + bool muted_; + VideoCapturer* video_capturer_; + WebRtcEncoderObserver encoder_observer_; + webrtc::ViEExternalCapture* external_capture_; + EncoderMap registered_encoders_; + + VideoFormat video_format_; + + talk_base::scoped_ptr stream_params_; + + WebRtcLocalStreamInfo local_stream_info_; + + bool capturer_updated_; + + int64 interval_; + + talk_base::scoped_ptr video_adapter_; +}; + +const WebRtcVideoEngine::VideoCodecPref + WebRtcVideoEngine::kVideoCodecPrefs[] = { + {kVp8PayloadName, 100, 0}, + {kRedPayloadName, 116, 1}, + {kFecPayloadName, 117, 2}, +}; + +// The formats are sorted by the descending order of width. We use the order to +// find the next format for CPU and bandwidth adaptation. +const VideoFormatPod WebRtcVideoEngine::kVideoFormats[] = { + {1280, 800, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {1280, 720, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {960, 600, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {960, 540, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {640, 400, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {640, 360, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {640, 480, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {480, 300, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {480, 270, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {480, 360, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {320, 200, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {320, 180, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {320, 240, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {240, 150, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {240, 135, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {240, 180, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {160, 100, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {160, 90, FPS_TO_INTERVAL(30), FOURCC_ANY}, + {160, 120, FPS_TO_INTERVAL(30), FOURCC_ANY}, +}; + +const VideoFormatPod WebRtcVideoEngine::kDefaultVideoFormat = + {640, 400, FPS_TO_INTERVAL(30), FOURCC_ANY}; + +static void UpdateVideoCodec(const cricket::VideoFormat& video_format, + webrtc::VideoCodec* target_codec) { + if ((target_codec == NULL) || (video_format == cricket::VideoFormat())) { + return; + } + target_codec->width = video_format.width; + target_codec->height = video_format.height; + target_codec->maxFramerate = cricket::VideoFormat::IntervalToFps( + video_format.interval); +} + +WebRtcVideoEngine::WebRtcVideoEngine() { + Construct(new ViEWrapper(), new ViETraceWrapper(), NULL, + new talk_base::CpuMonitor(NULL)); +} + +WebRtcVideoEngine::WebRtcVideoEngine(WebRtcVoiceEngine* voice_engine, + ViEWrapper* vie_wrapper, + talk_base::CpuMonitor* cpu_monitor) { + Construct(vie_wrapper, new ViETraceWrapper(), voice_engine, cpu_monitor); +} + +WebRtcVideoEngine::WebRtcVideoEngine(WebRtcVoiceEngine* voice_engine, + ViEWrapper* vie_wrapper, + ViETraceWrapper* tracing, + talk_base::CpuMonitor* cpu_monitor) { + Construct(vie_wrapper, tracing, voice_engine, cpu_monitor); +} + +void WebRtcVideoEngine::Construct(ViEWrapper* vie_wrapper, + ViETraceWrapper* tracing, + WebRtcVoiceEngine* voice_engine, + talk_base::CpuMonitor* cpu_monitor) { + LOG(LS_INFO) << "WebRtcVideoEngine::WebRtcVideoEngine"; + worker_thread_ = NULL; + vie_wrapper_.reset(vie_wrapper); + vie_wrapper_base_initialized_ = false; + tracing_.reset(tracing); + voice_engine_ = voice_engine; + initialized_ = false; + SetTraceFilter(SeverityToFilter(kDefaultLogSeverity)); + render_module_.reset(new WebRtcPassthroughRender()); + local_renderer_w_ = local_renderer_h_ = 0; + local_renderer_ = NULL; + video_capturer_ = NULL; + frame_listeners_ = 0; + capture_started_ = false; + decoder_factory_ = NULL; + encoder_factory_ = NULL; + cpu_monitor_.reset(cpu_monitor); + + SetTraceOptions(""); + if (tracing_->SetTraceCallback(this) != 0) { + LOG_RTCERR1(SetTraceCallback, this); + } + + // Set default quality levels for our supported codecs. We override them here + // if we know your cpu performance is low, and they can be updated explicitly + // by calling SetDefaultCodec. For example by a flute preference setting, or + // by the server with a jec in response to our reported system info. + VideoCodec max_codec(kVideoCodecPrefs[0].payload_type, + kVideoCodecPrefs[0].name, + kDefaultVideoFormat.width, + kDefaultVideoFormat.height, + VideoFormat::IntervalToFps(kDefaultVideoFormat.interval), + 0); + if (!SetDefaultCodec(max_codec)) { + LOG(LS_ERROR) << "Failed to initialize list of supported codec types"; + } + + + // Load our RTP Header extensions. + rtp_header_extensions_.push_back( + RtpHeaderExtension(kRtpTimestampOffsetHeaderExtension, + kRtpTimeOffsetExtensionId)); + rtp_header_extensions_.push_back( + RtpHeaderExtension(kRtpAbsoluteSendTimeHeaderExtension, + kRtpAbsoluteSendTimeExtensionId)); +} + +WebRtcVideoEngine::~WebRtcVideoEngine() { + ClearCapturer(); + LOG(LS_INFO) << "WebRtcVideoEngine::~WebRtcVideoEngine"; + if (initialized_) { + Terminate(); + } + if (encoder_factory_) { + encoder_factory_->RemoveObserver(this); + } + tracing_->SetTraceCallback(NULL); + // Test to see if the media processor was deregistered properly. + ASSERT(SignalMediaFrame.is_empty()); +} + +bool WebRtcVideoEngine::Init(talk_base::Thread* worker_thread) { + LOG(LS_INFO) << "WebRtcVideoEngine::Init"; + worker_thread_ = worker_thread; + ASSERT(worker_thread_ != NULL); + + cpu_monitor_->set_thread(worker_thread_); + if (!cpu_monitor_->Start(kCpuMonitorPeriodMs)) { + LOG(LS_ERROR) << "Failed to start CPU monitor."; + cpu_monitor_.reset(); + } + + bool result = InitVideoEngine(); + if (result) { + LOG(LS_INFO) << "VideoEngine Init done"; + } else { + LOG(LS_ERROR) << "VideoEngine Init failed, releasing"; + Terminate(); + } + return result; +} + +bool WebRtcVideoEngine::InitVideoEngine() { + LOG(LS_INFO) << "WebRtcVideoEngine::InitVideoEngine"; + + // Init WebRTC VideoEngine. + if (!vie_wrapper_base_initialized_) { + if (vie_wrapper_->base()->Init() != 0) { + LOG_RTCERR0(Init); + return false; + } + vie_wrapper_base_initialized_ = true; + } + + // Log the VoiceEngine version info. + char buffer[1024] = ""; + if (vie_wrapper_->base()->GetVersion(buffer) != 0) { + LOG_RTCERR0(GetVersion); + return false; + } + + LOG(LS_INFO) << "WebRtc VideoEngine Version:"; + LogMultiline(talk_base::LS_INFO, buffer); + + // Hook up to VoiceEngine for sync purposes, if supplied. + if (!voice_engine_) { + LOG(LS_WARNING) << "NULL voice engine"; + } else if ((vie_wrapper_->base()->SetVoiceEngine( + voice_engine_->voe()->engine())) != 0) { + LOG_RTCERR0(SetVoiceEngine); + return false; + } + + // Register our custom render module. + if (vie_wrapper_->render()->RegisterVideoRenderModule( + *render_module_.get()) != 0) { + LOG_RTCERR0(RegisterVideoRenderModule); + return false; + } + + initialized_ = true; + return true; +} + +void WebRtcVideoEngine::Terminate() { + LOG(LS_INFO) << "WebRtcVideoEngine::Terminate"; + initialized_ = false; + SetCapture(false); + + if (vie_wrapper_->render()->DeRegisterVideoRenderModule( + *render_module_.get()) != 0) { + LOG_RTCERR0(DeRegisterVideoRenderModule); + } + + if (vie_wrapper_->base()->SetVoiceEngine(NULL) != 0) { + LOG_RTCERR0(SetVoiceEngine); + } + + cpu_monitor_->Stop(); +} + +int WebRtcVideoEngine::GetCapabilities() { + return VIDEO_RECV | VIDEO_SEND; +} + +bool WebRtcVideoEngine::SetOptions(int options) { + return true; +} + +bool WebRtcVideoEngine::SetDefaultEncoderConfig( + const VideoEncoderConfig& config) { + return SetDefaultCodec(config.max_codec); +} + +// SetDefaultCodec may be called while the capturer is running. For example, a +// test call is started in a page with QVGA default codec, and then a real call +// is started in another page with VGA default codec. This is the corner case +// and happens only when a session is started. We ignore this case currently. +bool WebRtcVideoEngine::SetDefaultCodec(const VideoCodec& codec) { + if (!RebuildCodecList(codec)) { + LOG(LS_WARNING) << "Failed to RebuildCodecList"; + return false; + } + + default_codec_format_ = VideoFormat( + video_codecs_[0].width, + video_codecs_[0].height, + VideoFormat::FpsToInterval(video_codecs_[0].framerate), + FOURCC_ANY); + return true; +} + +WebRtcVideoMediaChannel* WebRtcVideoEngine::CreateChannel( + VoiceMediaChannel* voice_channel) { + WebRtcVideoMediaChannel* channel = + new WebRtcVideoMediaChannel(this, voice_channel); + if (!channel->Init()) { + delete channel; + channel = NULL; + } + return channel; +} + +bool WebRtcVideoEngine::SetVideoCapturer(VideoCapturer* capturer) { + return SetCapturer(capturer); +} + +VideoCapturer* WebRtcVideoEngine::GetVideoCapturer() const { + return video_capturer_; +} + +bool WebRtcVideoEngine::SetLocalRenderer(VideoRenderer* renderer) { + local_renderer_w_ = local_renderer_h_ = 0; + local_renderer_ = renderer; + return true; +} + +bool WebRtcVideoEngine::SetCapture(bool capture) { + bool old_capture = capture_started_; + capture_started_ = capture; + CaptureState result = UpdateCapturingState(); + if (result == CS_FAILED || result == CS_NO_DEVICE) { + capture_started_ = old_capture; + return false; + } + return true; +} + +CaptureState WebRtcVideoEngine::UpdateCapturingState() { + bool capture = capture_started_ && frame_listeners_; + CaptureState result = CS_RUNNING; + if (!IsCapturing() && capture) { // Start capturing. + if (video_capturer_ == NULL) { + return CS_NO_DEVICE; + } + + VideoFormat capture_format; + if (!video_capturer_->GetBestCaptureFormat(default_codec_format_, + &capture_format)) { + LOG(LS_WARNING) << "Unsupported format:" + << " width=" << default_codec_format_.width + << " height=" << default_codec_format_.height + << ". Supported formats are:"; + const std::vector* formats = + video_capturer_->GetSupportedFormats(); + if (formats) { + for (std::vector::const_iterator i = formats->begin(); + i != formats->end(); ++i) { + const VideoFormat& format = *i; + LOG(LS_WARNING) << " " << GetFourccName(format.fourcc) << ":" + << format.width << "x" << format.height << "x" + << format.framerate(); + } + } + return CS_FAILED; + } + + // Start the video capturer. + result = video_capturer_->Start(capture_format); + if (CS_RUNNING != result && CS_STARTING != result) { + LOG(LS_ERROR) << "Failed to start the video capturer"; + return result; + } + } else if (IsCapturing() && !capture) { // Stop capturing. + video_capturer_->Stop(); + result = CS_STOPPED; + } + + return result; +} + +bool WebRtcVideoEngine::IsCapturing() const { + return (video_capturer_ != NULL) && video_capturer_->IsRunning(); +} + +// TODO(thorcarpenter): Remove this fn, it's only used for unittests! +void WebRtcVideoEngine::OnFrameCaptured(VideoCapturer* capturer, + const CapturedFrame* frame) { + // Crop to desired aspect ratio. + int cropped_width, cropped_height; + ComputeCrop(default_codec_format_.width, default_codec_format_.height, + frame->width, abs(frame->height), + frame->pixel_width, frame->pixel_height, + frame->rotation, &cropped_width, &cropped_height); + + // This CapturedFrame* will already be in I420. In the future, when + // WebRtcVideoFrame has support for independent planes, we can just attach + // to it and update the pointers when cropping. + WebRtcVideoFrame i420_frame; + if (!i420_frame.Init(frame, cropped_width, cropped_height)) { + LOG(LS_ERROR) << "Couldn't convert to I420! " + << cropped_width << " x " << cropped_height; + return; + } + + // TODO(janahan): This is the trigger point for Tx video processing. + // Once the capturer refactoring is done, we will move this into the + // capturer...it's not there right now because that image is in not in the + // I420 color space. + // The clients that subscribe will obtain meta info from the frame. + // When this trigger is switched over to capturer, need to pass in the real + // ssrc. + bool drop_frame = false; + { + talk_base::CritScope cs(&signal_media_critical_); + SignalMediaFrame(kDummyVideoSsrc, &i420_frame, &drop_frame); + } + if (drop_frame) { + LOG(LS_VERBOSE) << "Media Effects dropped a frame."; + return; + } + + // Send I420 frame to the local renderer. + if (local_renderer_) { + if (local_renderer_w_ != static_cast(i420_frame.GetWidth()) || + local_renderer_h_ != static_cast(i420_frame.GetHeight())) { + local_renderer_->SetSize(local_renderer_w_ = i420_frame.GetWidth(), + local_renderer_h_ = i420_frame.GetHeight(), 0); + } + local_renderer_->RenderFrame(&i420_frame); + } + // Send I420 frame to the registered senders. + talk_base::CritScope cs(&channels_crit_); + for (VideoChannels::iterator it = channels_.begin(); + it != channels_.end(); ++it) { + if ((*it)->sending()) (*it)->SendFrame(capturer, &i420_frame); + } +} + +const std::vector& WebRtcVideoEngine::codecs() const { + return video_codecs_; +} + +const std::vector& +WebRtcVideoEngine::rtp_header_extensions() const { + return rtp_header_extensions_; +} + +void WebRtcVideoEngine::SetLogging(int min_sev, const char* filter) { + // if min_sev == -1, we keep the current log level. + if (min_sev >= 0) { + SetTraceFilter(SeverityToFilter(min_sev)); + } + SetTraceOptions(filter); +} + +int WebRtcVideoEngine::GetLastEngineError() { + return vie_wrapper_->error(); +} + +// Checks to see whether we comprehend and could receive a particular codec +bool WebRtcVideoEngine::FindCodec(const VideoCodec& in) { + for (int i = 0; i < ARRAY_SIZE(kVideoFormats); ++i) { + const VideoFormat fmt(kVideoFormats[i]); + if ((in.width == 0 && in.height == 0) || + (fmt.width == in.width && fmt.height == in.height)) { + if (encoder_factory_) { + const std::vector& codecs = + encoder_factory_->codecs(); + for (size_t j = 0; j < codecs.size(); ++j) { + VideoCodec codec(GetExternalVideoPayloadType(j), + codecs[j].name, 0, 0, 0, 0); + if (codec.Matches(in)) + return true; + } + } + for (size_t j = 0; j < ARRAY_SIZE(kVideoCodecPrefs); ++j) { + VideoCodec codec(kVideoCodecPrefs[j].payload_type, + kVideoCodecPrefs[j].name, 0, 0, 0, 0); + if (codec.Matches(in)) { + return true; + } + } + } + } + return false; +} + +// Given the requested codec, returns true if we can send that codec type and +// updates out with the best quality we could send for that codec. If current is +// not empty, we constrain out so that its aspect ratio matches current's. +bool WebRtcVideoEngine::CanSendCodec(const VideoCodec& requested, + const VideoCodec& current, + VideoCodec* out) { + if (!out) { + return false; + } + + std::vector::const_iterator local_max; + for (local_max = video_codecs_.begin(); + local_max < video_codecs_.end(); + ++local_max) { + // First match codecs by payload type + if (!requested.Matches(*local_max)) { + continue; + } + + out->id = requested.id; + out->name = requested.name; + out->preference = requested.preference; + out->params = requested.params; + out->framerate = talk_base::_min(requested.framerate, local_max->framerate); + out->width = 0; + out->height = 0; + out->params = requested.params; + out->feedback_params = requested.feedback_params; + + if (0 == requested.width && 0 == requested.height) { + // Special case with resolution 0. The channel should not send frames. + return true; + } else if (0 == requested.width || 0 == requested.height) { + // 0xn and nx0 are invalid resolutions. + return false; + } + + // Pick the best quality that is within their and our bounds and has the + // correct aspect ratio. + for (int j = 0; j < ARRAY_SIZE(kVideoFormats); ++j) { + const VideoFormat format(kVideoFormats[j]); + + // Skip any format that is larger than the local or remote maximums, or + // smaller than the current best match + if (format.width > requested.width || format.height > requested.height || + format.width > local_max->width || + (format.width < out->width && format.height < out->height)) { + continue; + } + + bool better = false; + + // Check any further constraints on this prospective format + if (!out->width || !out->height) { + // If we don't have any matches yet, this is the best so far. + better = true; + } else if (current.width && current.height) { + // current is set so format must match its ratio exactly. + better = + (format.width * current.height == format.height * current.width); + } else { + // Prefer closer aspect ratios i.e + // format.aspect - requested.aspect < out.aspect - requested.aspect + better = abs(format.width * requested.height * out->height - + requested.width * format.height * out->height) < + abs(out->width * format.height * requested.height - + requested.width * format.height * out->height); + } + + if (better) { + out->width = format.width; + out->height = format.height; + } + } + if (out->width > 0) { + return true; + } + } + return false; +} + +static void ConvertToCricketVideoCodec( + const webrtc::VideoCodec& in_codec, VideoCodec* out_codec) { + out_codec->id = in_codec.plType; + out_codec->name = in_codec.plName; + out_codec->width = in_codec.width; + out_codec->height = in_codec.height; + out_codec->framerate = in_codec.maxFramerate; + out_codec->SetParam(kCodecParamMinBitrate, in_codec.minBitrate); + out_codec->SetParam(kCodecParamMaxBitrate, in_codec.maxBitrate); + if (in_codec.qpMax) { + out_codec->SetParam(kCodecParamMaxQuantization, in_codec.qpMax); + } +} + +bool WebRtcVideoEngine::ConvertFromCricketVideoCodec( + const VideoCodec& in_codec, webrtc::VideoCodec* out_codec) { + bool found = false; + int ncodecs = vie_wrapper_->codec()->NumberOfCodecs(); + for (int i = 0; i < ncodecs; ++i) { + if (vie_wrapper_->codec()->GetCodec(i, *out_codec) == 0 && + _stricmp(in_codec.name.c_str(), out_codec->plName) == 0) { + found = true; + break; + } + } + + // If not found, check if this is supported by external encoder factory. + if (!found && encoder_factory_) { + const std::vector& codecs = + encoder_factory_->codecs(); + for (size_t i = 0; i < codecs.size(); ++i) { + if (_stricmp(in_codec.name.c_str(), codecs[i].name.c_str()) == 0) { + out_codec->codecType = codecs[i].type; + out_codec->plType = GetExternalVideoPayloadType(i); + talk_base::strcpyn(out_codec->plName, sizeof(out_codec->plName), + codecs[i].name.c_str(), codecs[i].name.length()); + found = true; + break; + } + } + } + + if (!found) { + LOG(LS_ERROR) << "invalid codec type"; + return false; + } + + if (in_codec.id != 0) + out_codec->plType = in_codec.id; + + if (in_codec.width != 0) + out_codec->width = in_codec.width; + + if (in_codec.height != 0) + out_codec->height = in_codec.height; + + if (in_codec.framerate != 0) + out_codec->maxFramerate = in_codec.framerate; + + // Convert bitrate parameters. + int max_bitrate = kMaxVideoBitrate; + int min_bitrate = kMinVideoBitrate; + int start_bitrate = kStartVideoBitrate; + + in_codec.GetParam(kCodecParamMinBitrate, &min_bitrate); + in_codec.GetParam(kCodecParamMaxBitrate, &max_bitrate); + + if (max_bitrate < min_bitrate) { + return false; + } + start_bitrate = talk_base::_max(start_bitrate, min_bitrate); + start_bitrate = talk_base::_min(start_bitrate, max_bitrate); + + out_codec->minBitrate = min_bitrate; + out_codec->startBitrate = start_bitrate; + out_codec->maxBitrate = max_bitrate; + + // Convert general codec parameters. + int max_quantization = 0; + if (in_codec.GetParam(kCodecParamMaxQuantization, &max_quantization)) { + if (max_quantization < 0) { + return false; + } + out_codec->qpMax = max_quantization; + } + return true; +} + +void WebRtcVideoEngine::RegisterChannel(WebRtcVideoMediaChannel *channel) { + talk_base::CritScope cs(&channels_crit_); + channels_.push_back(channel); +} + +void WebRtcVideoEngine::UnregisterChannel(WebRtcVideoMediaChannel *channel) { + talk_base::CritScope cs(&channels_crit_); + channels_.erase(std::remove(channels_.begin(), channels_.end(), channel), + channels_.end()); +} + +bool WebRtcVideoEngine::SetVoiceEngine(WebRtcVoiceEngine* voice_engine) { + if (initialized_) { + LOG(LS_WARNING) << "SetVoiceEngine can not be called after Init"; + return false; + } + voice_engine_ = voice_engine; + return true; +} + +bool WebRtcVideoEngine::EnableTimedRender() { + if (initialized_) { + LOG(LS_WARNING) << "EnableTimedRender can not be called after Init"; + return false; + } + render_module_.reset(webrtc::VideoRender::CreateVideoRender(0, NULL, + false, webrtc::kRenderExternal)); + return true; +} + +void WebRtcVideoEngine::SetTraceFilter(int filter) { + tracing_->SetTraceFilter(filter); +} + +// See https://sites.google.com/a/google.com/wavelet/ +// Home/Magic-Flute--RTC-Engine-/Magic-Flute-Command-Line-Parameters +// for all supported command line setttings. +void WebRtcVideoEngine::SetTraceOptions(const std::string& options) { + // Set WebRTC trace file. + std::vector opts; + talk_base::tokenize(options, ' ', '"', '"', &opts); + std::vector::iterator tracefile = + std::find(opts.begin(), opts.end(), "tracefile"); + if (tracefile != opts.end() && ++tracefile != opts.end()) { + // Write WebRTC debug output (at same loglevel) to file + if (tracing_->SetTraceFile(tracefile->c_str()) == -1) { + LOG_RTCERR1(SetTraceFile, *tracefile); + } + } +} + +static void AddDefaultFeedbackParams(VideoCodec* codec) { + const FeedbackParam kFir(kRtcpFbParamCcm, kRtcpFbCcmParamFir); + codec->AddFeedbackParam(kFir); + const FeedbackParam kNack(kRtcpFbParamNack, kParamValueEmpty); + codec->AddFeedbackParam(kNack); + const FeedbackParam kRemb(kRtcpFbParamRemb, kParamValueEmpty); + codec->AddFeedbackParam(kRemb); +} + +// Rebuilds the codec list to be only those that are less intensive +// than the specified codec. +bool WebRtcVideoEngine::RebuildCodecList(const VideoCodec& in_codec) { + if (!FindCodec(in_codec)) + return false; + + video_codecs_.clear(); + + bool found = false; + std::set external_codec_names; + if (encoder_factory_) { + const std::vector& codecs = + encoder_factory_->codecs(); + for (size_t i = 0; i < codecs.size(); ++i) { + if (!found) + found = (in_codec.name == codecs[i].name); + VideoCodec codec(GetExternalVideoPayloadType(i), + codecs[i].name, + codecs[i].max_width, + codecs[i].max_height, + codecs[i].max_fps, + codecs.size() + ARRAY_SIZE(kVideoCodecPrefs) - i); + AddDefaultFeedbackParams(&codec); + video_codecs_.push_back(codec); + external_codec_names.insert(codecs[i].name); + } + } + for (size_t i = 0; i < ARRAY_SIZE(kVideoCodecPrefs); ++i) { + const VideoCodecPref& pref(kVideoCodecPrefs[i]); + if (!found) + found = (in_codec.name == pref.name); + bool is_external_codec = external_codec_names.find(pref.name) != + external_codec_names.end(); + if (found && !is_external_codec) { + VideoCodec codec(pref.payload_type, pref.name, + in_codec.width, in_codec.height, in_codec.framerate, + ARRAY_SIZE(kVideoCodecPrefs) - i); + if (_stricmp(kVp8PayloadName, codec.name.c_str()) == 0) { + AddDefaultFeedbackParams(&codec); + } + video_codecs_.push_back(codec); + } + } + ASSERT(found); + return true; +} + +bool WebRtcVideoEngine::SetCapturer(VideoCapturer* capturer) { + if (capturer == NULL) { + // Stop capturing before clearing the capturer. + if (!SetCapture(false)) { + LOG(LS_WARNING) << "Camera failed to stop"; + return false; + } + ClearCapturer(); + return true; + } + + // Hook up signals and install the supplied capturer. + SignalCaptureStateChange.repeat(capturer->SignalStateChange); + capturer->SignalFrameCaptured.connect(this, + &WebRtcVideoEngine::OnFrameCaptured); + ClearCapturer(); + video_capturer_ = capturer; + // Possibly restart the capturer if it is supposed to be running. + CaptureState result = UpdateCapturingState(); + if (result == CS_FAILED || result == CS_NO_DEVICE) { + LOG(LS_WARNING) << "Camera failed to restart"; + return false; + } + return true; +} + +// Ignore spammy trace messages, mostly from the stats API when we haven't +// gotten RTCP info yet from the remote side. +bool WebRtcVideoEngine::ShouldIgnoreTrace(const std::string& trace) { + static const char* const kTracesToIgnore[] = { + NULL + }; + for (const char* const* p = kTracesToIgnore; *p; ++p) { + if (trace.find(*p) == 0) { + return true; + } + } + return false; +} + +int WebRtcVideoEngine::GetNumOfChannels() { + talk_base::CritScope cs(&channels_crit_); + return channels_.size(); +} + +void WebRtcVideoEngine::IncrementFrameListeners() { + if (++frame_listeners_ == 1) { + UpdateCapturingState(); + } + // In the unlikely event of wrapparound. + ASSERT(frame_listeners_ >= 0); +} + +void WebRtcVideoEngine::DecrementFrameListeners() { + if (--frame_listeners_ == 0) { + UpdateCapturingState(); + } + ASSERT(frame_listeners_ >= 0); +} + +void WebRtcVideoEngine::Print(webrtc::TraceLevel level, const char* trace, + int length) { + talk_base::LoggingSeverity sev = talk_base::LS_VERBOSE; + if (level == webrtc::kTraceError || level == webrtc::kTraceCritical) + sev = talk_base::LS_ERROR; + else if (level == webrtc::kTraceWarning) + sev = talk_base::LS_WARNING; + else if (level == webrtc::kTraceStateInfo || level == webrtc::kTraceInfo) + sev = talk_base::LS_INFO; + else if (level == webrtc::kTraceTerseInfo) + sev = talk_base::LS_INFO; + + // Skip past boilerplate prefix text + if (length < 72) { + std::string msg(trace, length); + LOG(LS_ERROR) << "Malformed webrtc log message: "; + LOG_V(sev) << msg; + } else { + std::string msg(trace + 71, length - 72); + if (!ShouldIgnoreTrace(msg) && + (!voice_engine_ || !voice_engine_->ShouldIgnoreTrace(msg))) { + LOG_V(sev) << "webrtc: " << msg; + } + } +} + +bool WebRtcVideoEngine::RegisterProcessor( + VideoProcessor* video_processor) { + talk_base::CritScope cs(&signal_media_critical_); + SignalMediaFrame.connect(video_processor, + &VideoProcessor::OnFrame); + return true; +} +bool WebRtcVideoEngine::UnregisterProcessor( + VideoProcessor* video_processor) { + talk_base::CritScope cs(&signal_media_critical_); + SignalMediaFrame.disconnect(video_processor); + return true; +} + +webrtc::VideoDecoder* WebRtcVideoEngine::CreateExternalDecoder( + webrtc::VideoCodecType type) { + if (decoder_factory_ == NULL) { + return NULL; + } + return decoder_factory_->CreateVideoDecoder(type); +} + +void WebRtcVideoEngine::DestroyExternalDecoder(webrtc::VideoDecoder* decoder) { + ASSERT(decoder_factory_ != NULL); + if (decoder_factory_ == NULL) + return; + decoder_factory_->DestroyVideoDecoder(decoder); +} + +webrtc::VideoEncoder* WebRtcVideoEngine::CreateExternalEncoder( + webrtc::VideoCodecType type) { + if (encoder_factory_ == NULL) { + return NULL; + } + return encoder_factory_->CreateVideoEncoder(type); +} + +void WebRtcVideoEngine::DestroyExternalEncoder(webrtc::VideoEncoder* encoder) { + ASSERT(encoder_factory_ != NULL); + if (encoder_factory_ == NULL) + return; + encoder_factory_->DestroyVideoEncoder(encoder); +} + +bool WebRtcVideoEngine::IsExternalEncoderCodecType( + webrtc::VideoCodecType type) const { + if (!encoder_factory_) + return false; + const std::vector& codecs = + encoder_factory_->codecs(); + std::vector::const_iterator it; + for (it = codecs.begin(); it != codecs.end(); ++it) { + if (it->type == type) + return true; + } + return false; +} + +void WebRtcVideoEngine::ClearCapturer() { + video_capturer_ = NULL; +} + +void WebRtcVideoEngine::SetExternalDecoderFactory( + WebRtcVideoDecoderFactory* decoder_factory) { + decoder_factory_ = decoder_factory; +} + +void WebRtcVideoEngine::SetExternalEncoderFactory( + WebRtcVideoEncoderFactory* encoder_factory) { + if (encoder_factory_ == encoder_factory) + return; + + if (encoder_factory_) { + encoder_factory_->RemoveObserver(this); + } + encoder_factory_ = encoder_factory; + if (encoder_factory_) { + encoder_factory_->AddObserver(this); + } + + // Invoke OnCodecAvailable() here in case the list of codecs is already + // available when the encoder factory is installed. If not the encoder + // factory will invoke the callback later when the codecs become available. + OnCodecsAvailable(); +} + +void WebRtcVideoEngine::OnCodecsAvailable() { + // Rebuild codec list while reapplying the current default codec format. + VideoCodec max_codec(kVideoCodecPrefs[0].payload_type, + kVideoCodecPrefs[0].name, + video_codecs_[0].width, + video_codecs_[0].height, + video_codecs_[0].framerate, + 0); + if (!RebuildCodecList(max_codec)) { + LOG(LS_ERROR) << "Failed to initialize list of supported codec types"; + } +} + +// WebRtcVideoMediaChannel + +WebRtcVideoMediaChannel::WebRtcVideoMediaChannel( + WebRtcVideoEngine* engine, + VoiceMediaChannel* channel) + : engine_(engine), + voice_channel_(channel), + vie_channel_(-1), + nack_enabled_(true), + remb_enabled_(false), + render_started_(false), + first_receive_ssrc_(0), + send_red_type_(-1), + send_fec_type_(-1), + send_min_bitrate_(kMinVideoBitrate), + send_start_bitrate_(kStartVideoBitrate), + send_max_bitrate_(kMaxVideoBitrate), + sending_(false), + ratio_w_(0), + ratio_h_(0) { + engine->RegisterChannel(this); +} + +bool WebRtcVideoMediaChannel::Init() { + const uint32 ssrc_key = 0; + return CreateChannel(ssrc_key, MD_SENDRECV, &vie_channel_); +} + +WebRtcVideoMediaChannel::~WebRtcVideoMediaChannel() { + const bool send = false; + SetSend(send); + const bool render = false; + SetRender(render); + + while (!send_channels_.empty()) { + if (!DeleteSendChannel(send_channels_.begin()->first)) { + LOG(LS_ERROR) << "Unable to delete channel with ssrc key " + << send_channels_.begin()->first; + ASSERT(false); + break; + } + } + + // Remove all receive streams and the default channel. + while (!recv_channels_.empty()) { + RemoveRecvStream(recv_channels_.begin()->first); + } + + // Unregister the channel from the engine. + engine()->UnregisterChannel(this); + if (worker_thread()) { + worker_thread()->Clear(this); + } +} + +bool WebRtcVideoMediaChannel::SetRecvCodecs( + const std::vector& codecs) { + receive_codecs_.clear(); + for (std::vector::const_iterator iter = codecs.begin(); + iter != codecs.end(); ++iter) { + if (engine()->FindCodec(*iter)) { + webrtc::VideoCodec wcodec; + if (engine()->ConvertFromCricketVideoCodec(*iter, &wcodec)) { + receive_codecs_.push_back(wcodec); + } + } else { + LOG(LS_INFO) << "Unknown codec " << iter->name; + return false; + } + } + + for (RecvChannelMap::iterator it = recv_channels_.begin(); + it != recv_channels_.end(); ++it) { + if (!SetReceiveCodecs(it->second)) + return false; + } + return true; +} + +bool WebRtcVideoMediaChannel::SetSendCodecs( + const std::vector& codecs) { + // Match with local video codec list. + std::vector send_codecs; + VideoCodec checked_codec; + VideoCodec current; // defaults to 0x0 + if (sending_) { + ConvertToCricketVideoCodec(*send_codec_, ¤t); + } + for (std::vector::const_iterator iter = codecs.begin(); + iter != codecs.end(); ++iter) { + if (_stricmp(iter->name.c_str(), kRedPayloadName) == 0) { + send_red_type_ = iter->id; + } else if (_stricmp(iter->name.c_str(), kFecPayloadName) == 0) { + send_fec_type_ = iter->id; + } else if (engine()->CanSendCodec(*iter, current, &checked_codec)) { + webrtc::VideoCodec wcodec; + if (engine()->ConvertFromCricketVideoCodec(checked_codec, &wcodec)) { + if (send_codecs.empty()) { + nack_enabled_ = IsNackEnabled(checked_codec); + remb_enabled_ = IsRembEnabled(checked_codec); + } + send_codecs.push_back(wcodec); + } + } else { + LOG(LS_WARNING) << "Unknown codec " << iter->name; + } + } + + // Fail if we don't have a match. + if (send_codecs.empty()) { + LOG(LS_WARNING) << "No matching codecs available"; + return false; + } + + // Recv protection. + for (RecvChannelMap::iterator it = recv_channels_.begin(); + it != recv_channels_.end(); ++it) { + int channel_id = it->second->channel_id(); + if (!SetNackFec(channel_id, send_red_type_, send_fec_type_, + nack_enabled_)) { + return false; + } + if (engine_->vie()->rtp()->SetRembStatus(channel_id, + kNotSending, + remb_enabled_) != 0) { + LOG_RTCERR3(SetRembStatus, channel_id, kNotSending, remb_enabled_); + return false; + } + } + + // Send settings. + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + int channel_id = iter->second->channel_id(); + if (!SetNackFec(channel_id, send_red_type_, send_fec_type_, + nack_enabled_)) { + return false; + } + if (engine_->vie()->rtp()->SetRembStatus(channel_id, + remb_enabled_, + remb_enabled_) != 0) { + LOG_RTCERR3(SetRembStatus, channel_id, remb_enabled_, remb_enabled_); + return false; + } + } + + // Select the first matched codec. + webrtc::VideoCodec& codec(send_codecs[0]); + + if (!SetSendCodec( + codec, codec.minBitrate, codec.startBitrate, codec.maxBitrate)) { + return false; + } + + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + send_channel->InitializeAdapterOutputFormat(codec); + } + + LogSendCodecChange("SetSendCodecs()"); + + return true; +} + +bool WebRtcVideoMediaChannel::GetSendCodec(VideoCodec* send_codec) { + if (!send_codec_) { + return false; + } + ConvertToCricketVideoCodec(*send_codec_, send_codec); + return true; +} + +bool WebRtcVideoMediaChannel::SetSendStreamFormat(uint32 ssrc, + const VideoFormat& format) { + if (!send_codec_) { + LOG(LS_ERROR) << "The send codec has not been set yet."; + return false; + } + WebRtcVideoChannelSendInfo* send_channel = GetSendChannel(ssrc); + if (!send_channel) { + LOG(LS_ERROR) << "The specified ssrc " << ssrc << " is not in use."; + return false; + } + send_channel->set_video_format(format); + return true; +} + +bool WebRtcVideoMediaChannel::SetRender(bool render) { + if (render == render_started_) { + return true; // no action required + } + + bool ret = true; + for (RecvChannelMap::iterator it = recv_channels_.begin(); + it != recv_channels_.end(); ++it) { + if (render) { + if (engine()->vie()->render()->StartRender( + it->second->channel_id()) != 0) { + LOG_RTCERR1(StartRender, it->second->channel_id()); + ret = false; + } + } else { + if (engine()->vie()->render()->StopRender( + it->second->channel_id()) != 0) { + LOG_RTCERR1(StopRender, it->second->channel_id()); + ret = false; + } + } + } + if (ret) { + render_started_ = render; + } + + return ret; +} + +bool WebRtcVideoMediaChannel::SetSend(bool send) { + if (!HasReadySendChannels() && send) { + LOG(LS_ERROR) << "No stream added"; + return false; + } + if (send == sending()) { + return true; // No action required. + } + + if (send) { + // We've been asked to start sending. + // SetSendCodecs must have been called already. + if (!send_codec_) { + return false; + } + // Start send now. + if (!StartSend()) { + return false; + } + } else { + // We've been asked to stop sending. + if (!StopSend()) { + return false; + } + } + sending_ = send; + + return true; +} + +bool WebRtcVideoMediaChannel::AddSendStream(const StreamParams& sp) { + LOG(LS_INFO) << "AddSendStream " << sp.ToString(); + + if (!IsOneSsrcStream(sp)) { + LOG(LS_ERROR) << "AddSendStream: bad local stream parameters"; + return false; + } + + uint32 ssrc_key; + if (!CreateSendChannelKey(sp.first_ssrc(), &ssrc_key)) { + LOG(LS_ERROR) << "Trying to register duplicate ssrc: " << sp.first_ssrc(); + return false; + } + // If the default channel is already used for sending create a new channel + // otherwise use the default channel for sending. + int channel_id = -1; + if (send_channels_[0]->stream_params() == NULL) { + channel_id = vie_channel_; + } else { + if (!CreateChannel(ssrc_key, MD_SEND, &channel_id)) { + LOG(LS_ERROR) << "AddSendStream: unable to create channel"; + return false; + } + } + WebRtcVideoChannelSendInfo* send_channel = send_channels_[ssrc_key]; + // Set the send (local) SSRC. + // If there are multiple send SSRCs, we can only set the first one here, and + // the rest of the SSRC(s) need to be set after SetSendCodec has been called + // (with a codec requires multiple SSRC(s)). + if (engine()->vie()->rtp()->SetLocalSSRC(channel_id, + sp.first_ssrc()) != 0) { + LOG_RTCERR2(SetLocalSSRC, channel_id, sp.first_ssrc()); + return false; + } + + // Set RTCP CName. + if (engine()->vie()->rtp()->SetRTCPCName(channel_id, + sp.cname.c_str()) != 0) { + LOG_RTCERR2(SetRTCPCName, channel_id, sp.cname.c_str()); + return false; + } + + // At this point the channel's local SSRC has been updated. If the channel is + // the default channel make sure that all the receive channels are updated as + // well. Receive channels have to have the same SSRC as the default channel in + // order to send receiver reports with this SSRC. + if (IsDefaultChannel(channel_id)) { + for (RecvChannelMap::const_iterator it = recv_channels_.begin(); + it != recv_channels_.end(); ++it) { + WebRtcVideoChannelRecvInfo* info = it->second; + int channel_id = info->channel_id(); + if (engine()->vie()->rtp()->SetLocalSSRC(channel_id, + sp.first_ssrc()) != 0) { + LOG_RTCERR1(SetLocalSSRC, it->first); + return false; + } + } + } + + send_channel->set_stream_params(sp); + + // Reset send codec after stream parameters changed. + if (send_codec_) { + if (!SetSendCodec(send_channel, *send_codec_, send_min_bitrate_, + send_start_bitrate_, send_max_bitrate_)) { + return false; + } + LogSendCodecChange("SetSendStreamFormat()"); + } + + if (sending_) { + return StartSend(send_channel); + } + return true; +} + +bool WebRtcVideoMediaChannel::RemoveSendStream(uint32 ssrc) { + uint32 ssrc_key; + if (!GetSendChannelKey(ssrc, &ssrc_key)) { + LOG(LS_WARNING) << "Try to remove stream with ssrc " << ssrc + << " which doesn't exist."; + return false; + } + WebRtcVideoChannelSendInfo* send_channel = send_channels_[ssrc_key]; + int channel_id = send_channel->channel_id(); + if (IsDefaultChannel(channel_id) && (send_channel->stream_params() == NULL)) { + // Default channel will still exist. However, if stream_params() is NULL + // there is no stream to remove. + return false; + } + if (sending_) { + StopSend(send_channel); + } + + const WebRtcVideoChannelSendInfo::EncoderMap& encoder_map = + send_channel->registered_encoders(); + for (WebRtcVideoChannelSendInfo::EncoderMap::const_iterator it = + encoder_map.begin(); it != encoder_map.end(); ++it) { + if (engine()->vie()->ext_codec()->DeRegisterExternalSendCodec( + channel_id, it->first) != 0) { + LOG_RTCERR1(DeregisterEncoderObserver, channel_id); + } + engine()->DestroyExternalEncoder(it->second); + } + send_channel->ClearRegisteredEncoders(); + + // The receive channels depend on the default channel, recycle it instead. + if (IsDefaultChannel(channel_id)) { + SetCapturer(GetDefaultChannelSsrc(), NULL); + send_channel->ClearStreamParams(); + } else { + return DeleteSendChannel(ssrc_key); + } + return true; +} + +bool WebRtcVideoMediaChannel::AddRecvStream(const StreamParams& sp) { + // TODO(zhurunz) Remove this once BWE works properly across different send + // and receive channels. + // Reuse default channel for recv stream in 1:1 call. + if (!InConferenceMode() && first_receive_ssrc_ == 0) { + LOG(LS_INFO) << "Recv stream " << sp.first_ssrc() + << " reuse default channel #" + << vie_channel_; + first_receive_ssrc_ = sp.first_ssrc(); + if (render_started_) { + if (engine()->vie()->render()->StartRender(vie_channel_) !=0) { + LOG_RTCERR1(StartRender, vie_channel_); + } + } + return true; + } + + if (recv_channels_.find(sp.first_ssrc()) != recv_channels_.end() || + first_receive_ssrc_ == sp.first_ssrc()) { + LOG(LS_ERROR) << "Stream already exists"; + return false; + } + + // TODO(perkj): Implement recv media from multiple SSRCs per stream. + if (sp.ssrcs.size() != 1) { + LOG(LS_ERROR) << "WebRtcVideoMediaChannel supports one receiving SSRC per" + << " stream"; + return false; + } + + // Create a new channel for receiving video data. + // In order to get the bandwidth estimation work fine for + // receive only channels, we connect all receiving channels + // to our master send channel. + int channel_id = -1; + if (!CreateChannel(sp.first_ssrc(), MD_RECV, &channel_id)) { + return false; + } + + // Get the default renderer. + VideoRenderer* default_renderer = NULL; + if (InConferenceMode()) { + // The recv_channels_ size start out being 1, so if it is two here this + // is the first receive channel created (vie_channel_ is not used for + // receiving in a conference call). This means that the renderer stored + // inside vie_channel_ should be used for the just created channel. + if (recv_channels_.size() == 2 && + recv_channels_.find(0) != recv_channels_.end()) { + GetRenderer(0, &default_renderer); + } + } + + // The first recv stream reuses the default renderer (if a default renderer + // has been set). + if (default_renderer) { + SetRenderer(sp.first_ssrc(), default_renderer); + } + + LOG(LS_INFO) << "New video stream " << sp.first_ssrc() + << " registered to VideoEngine channel #" + << channel_id << " and connected to channel #" << vie_channel_; + + return true; +} + +bool WebRtcVideoMediaChannel::RemoveRecvStream(uint32 ssrc) { + RecvChannelMap::iterator it = recv_channels_.find(ssrc); + + if (it == recv_channels_.end()) { + // TODO(perkj): Remove this once BWE works properly across different send + // and receive channels. + // The default channel is reused for recv stream in 1:1 call. + if (first_receive_ssrc_ == ssrc) { + first_receive_ssrc_ = 0; + // Need to stop the renderer and remove it since the render window can be + // deleted after this. + if (render_started_) { + if (engine()->vie()->render()->StopRender(vie_channel_) !=0) { + LOG_RTCERR1(StopRender, it->second->channel_id()); + } + } + recv_channels_[0]->SetRenderer(NULL); + return true; + } + return false; + } + WebRtcVideoChannelRecvInfo* info = it->second; + int channel_id = info->channel_id(); + if (engine()->vie()->render()->RemoveRenderer(channel_id) != 0) { + LOG_RTCERR1(RemoveRenderer, channel_id); + } + + if (engine()->vie()->network()->DeregisterSendTransport(channel_id) !=0) { + LOG_RTCERR1(DeRegisterSendTransport, channel_id); + } + + if (engine()->vie()->codec()->DeregisterDecoderObserver( + channel_id) != 0) { + LOG_RTCERR1(DeregisterDecoderObserver, channel_id); + } + + const WebRtcVideoChannelRecvInfo::DecoderMap& decoder_map = + info->registered_decoders(); + for (WebRtcVideoChannelRecvInfo::DecoderMap::const_iterator it = + decoder_map.begin(); it != decoder_map.end(); ++it) { + if (engine()->vie()->ext_codec()->DeRegisterExternalReceiveCodec( + channel_id, it->first) != 0) { + LOG_RTCERR1(DeregisterDecoderObserver, channel_id); + } + engine()->DestroyExternalDecoder(it->second); + } + info->ClearRegisteredDecoders(); + + LOG(LS_INFO) << "Removing video stream " << ssrc + << " with VideoEngine channel #" + << channel_id; + if (engine()->vie()->base()->DeleteChannel(channel_id) == -1) { + LOG_RTCERR1(DeleteChannel, channel_id); + // Leak the WebRtcVideoChannelRecvInfo owned by |it| but remove the channel + // from recv_channels_. + recv_channels_.erase(it); + return false; + } + // Delete the WebRtcVideoChannelRecvInfo pointed to by it->second. + delete info; + recv_channels_.erase(it); + return true; +} + +bool WebRtcVideoMediaChannel::StartSend() { + bool success = true; + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + if (!StartSend(send_channel)) { + success = false; + } + } + return success; +} + +bool WebRtcVideoMediaChannel::StartSend( + WebRtcVideoChannelSendInfo* send_channel) { + const int channel_id = send_channel->channel_id(); + if (engine()->vie()->base()->StartSend(channel_id) != 0) { + LOG_RTCERR1(StartSend, channel_id); + return false; + } + + send_channel->set_sending(true); + if (!send_channel->video_capturer()) { + engine_->IncrementFrameListeners(); + } + return true; +} + +bool WebRtcVideoMediaChannel::StopSend() { + bool success = true; + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + if (!StopSend(send_channel)) { + success = false; + } + } + return success; +} + +bool WebRtcVideoMediaChannel::StopSend( + WebRtcVideoChannelSendInfo* send_channel) { + const int channel_id = send_channel->channel_id(); + if (engine()->vie()->base()->StopSend(channel_id) != 0) { + LOG_RTCERR1(StopSend, channel_id); + return false; + } + send_channel->set_sending(false); + if (!send_channel->video_capturer()) { + engine_->DecrementFrameListeners(); + } + return true; +} + +bool WebRtcVideoMediaChannel::SendIntraFrame() { + bool success = true; + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); + ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + const int channel_id = send_channel->channel_id(); + if (engine()->vie()->codec()->SendKeyFrame(channel_id) != 0) { + LOG_RTCERR1(SendKeyFrame, channel_id); + success = false; + } + } + return success; +} + +bool WebRtcVideoMediaChannel::IsOneSsrcStream(const StreamParams& sp) { + return (sp.ssrcs.size() == 1 && sp.ssrc_groups.size() == 0); +} + +bool WebRtcVideoMediaChannel::HasReadySendChannels() { + return !send_channels_.empty() && + ((send_channels_.size() > 1) || + (send_channels_[0]->stream_params() != NULL)); +} + +bool WebRtcVideoMediaChannel::GetSendChannelKey(uint32 local_ssrc, + uint32* key) { + *key = 0; + // If a send channel is not ready to send it will not have local_ssrc + // registered to it. + if (!HasReadySendChannels()) { + return false; + } + // The default channel is stored with key 0. The key therefore does not match + // the SSRC associated with the default channel. Check if the SSRC provided + // corresponds to the default channel's SSRC. + if (local_ssrc == GetDefaultChannelSsrc()) { + return true; + } + if (send_channels_.find(local_ssrc) == send_channels_.end()) { + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + if (send_channel->has_ssrc(local_ssrc)) { + *key = iter->first; + return true; + } + } + return false; + } + // The key was found in the above std::map::find call. This means that the + // ssrc is the key. + *key = local_ssrc; + return true; +} + +WebRtcVideoChannelSendInfo* WebRtcVideoMediaChannel::GetSendChannel( + VideoCapturer* video_capturer) { + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + if (send_channel->video_capturer() == video_capturer) { + return send_channel; + } + } + return NULL; +} + +WebRtcVideoChannelSendInfo* WebRtcVideoMediaChannel::GetSendChannel( + uint32 local_ssrc) { + uint32 key; + if (!GetSendChannelKey(local_ssrc, &key)) { + return NULL; + } + return send_channels_[key]; +} + +bool WebRtcVideoMediaChannel::CreateSendChannelKey(uint32 local_ssrc, + uint32* key) { + if (GetSendChannelKey(local_ssrc, key)) { + // If there is a key corresponding to |local_ssrc|, the SSRC is already in + // use. SSRCs need to be unique in a session and at this point a duplicate + // SSRC has been detected. + return false; + } + if (send_channels_[0]->stream_params() == NULL) { + // key should be 0 here as the default channel should be re-used whenever it + // is not used. + *key = 0; + return true; + } + // SSRC is currently not in use and the default channel is already in use. Use + // the SSRC as key since it is supposed to be unique in a session. + *key = local_ssrc; + return true; +} + +uint32 WebRtcVideoMediaChannel::GetDefaultChannelSsrc() { + WebRtcVideoChannelSendInfo* send_channel = send_channels_[0]; + const StreamParams* sp = send_channel->stream_params(); + if (sp == NULL) { + // This happens if no send stream is currently registered. + return 0; + } + return sp->first_ssrc(); +} + +bool WebRtcVideoMediaChannel::DeleteSendChannel(uint32 ssrc_key) { + if (send_channels_.find(ssrc_key) == send_channels_.end()) { + return false; + } + WebRtcVideoChannelSendInfo* send_channel = send_channels_[ssrc_key]; + VideoCapturer* capturer = send_channel->video_capturer(); + if (capturer != NULL) { + capturer->SignalVideoFrame.disconnect(this); + send_channel->set_video_capturer(NULL); + } + + int channel_id = send_channel->channel_id(); + int capture_id = send_channel->capture_id(); + if (engine()->vie()->codec()->DeregisterEncoderObserver( + channel_id) != 0) { + LOG_RTCERR1(DeregisterEncoderObserver, channel_id); + } + + // Destroy the external capture interface. + if (engine()->vie()->capture()->DisconnectCaptureDevice( + channel_id) != 0) { + LOG_RTCERR1(DisconnectCaptureDevice, channel_id); + } + if (engine()->vie()->capture()->ReleaseCaptureDevice( + capture_id) != 0) { + LOG_RTCERR1(ReleaseCaptureDevice, capture_id); + } + + // The default channel is stored in both |send_channels_| and + // |recv_channels_|. To make sure it is only deleted once from vie let the + // delete call happen when tearing down |recv_channels_| and not here. + if (!IsDefaultChannel(channel_id)) { + engine_->vie()->base()->DeleteChannel(channel_id); + } + delete send_channel; + send_channels_.erase(ssrc_key); + return true; +} + +bool WebRtcVideoMediaChannel::RemoveCapturer(uint32 ssrc) { + WebRtcVideoChannelSendInfo* send_channel = GetSendChannel(ssrc); + if (!send_channel) { + return false; + } + VideoCapturer* capturer = send_channel->video_capturer(); + if (capturer == NULL) { + return false; + } + capturer->SignalVideoFrame.disconnect(this); + send_channel->set_video_capturer(NULL); + if (send_channel->sending()) { + engine_->IncrementFrameListeners(); + } + const int64 timestamp = send_channel->local_stream_info()->time_stamp(); + if (send_codec_) { + QueueBlackFrame(ssrc, timestamp, send_codec_->maxFramerate); + } + return true; +} + +bool WebRtcVideoMediaChannel::SetRenderer(uint32 ssrc, + VideoRenderer* renderer) { + if (recv_channels_.find(ssrc) == recv_channels_.end()) { + // TODO(perkj): Remove this once BWE works properly across different send + // and receive channels. + // The default channel is reused for recv stream in 1:1 call. + if (first_receive_ssrc_ == ssrc && + recv_channels_.find(0) != recv_channels_.end()) { + LOG(LS_INFO) << "SetRenderer " << ssrc + << " reuse default channel #" + << vie_channel_; + recv_channels_[0]->SetRenderer(renderer); + return true; + } + return false; + } + + recv_channels_[ssrc]->SetRenderer(renderer); + return true; +} + +bool WebRtcVideoMediaChannel::GetStats(VideoMediaInfo* info) { + // Get sender statistics and build VideoSenderInfo. + unsigned int total_bitrate_sent = 0; + unsigned int video_bitrate_sent = 0; + unsigned int fec_bitrate_sent = 0; + unsigned int nack_bitrate_sent = 0; + unsigned int estimated_send_bandwidth = 0; + unsigned int target_enc_bitrate = 0; + if (send_codec_) { + for (SendChannelMap::const_iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + const int channel_id = send_channel->channel_id(); + VideoSenderInfo sinfo; + const StreamParams* send_params = send_channel->stream_params(); + if (send_params == NULL) { + // This should only happen if the default vie channel is not in use. + // This can happen if no streams have ever been added or the stream + // corresponding to the default channel has been removed. Note that + // there may be non-default vie channels in use when this happen so + // asserting send_channels_.size() == 1 is not correct and neither is + // breaking out of the loop. + ASSERT(channel_id == vie_channel_); + continue; + } + unsigned int bytes_sent, packets_sent, bytes_recv, packets_recv; + if (engine_->vie()->rtp()->GetRTPStatistics(channel_id, bytes_sent, + packets_sent, bytes_recv, + packets_recv) != 0) { + LOG_RTCERR1(GetRTPStatistics, vie_channel_); + continue; + } + WebRtcLocalStreamInfo* channel_stream_info = + send_channel->local_stream_info(); + + sinfo.ssrcs = send_params->ssrcs; + sinfo.codec_name = send_codec_->plName; + sinfo.bytes_sent = bytes_sent; + sinfo.packets_sent = packets_sent; + sinfo.packets_cached = -1; + sinfo.packets_lost = -1; + sinfo.fraction_lost = -1; + sinfo.firs_rcvd = -1; + sinfo.nacks_rcvd = -1; + sinfo.rtt_ms = -1; + sinfo.frame_width = channel_stream_info->width(); + sinfo.frame_height = channel_stream_info->height(); + sinfo.framerate_input = channel_stream_info->framerate(); + sinfo.framerate_sent = send_channel->encoder_observer()->framerate(); + sinfo.nominal_bitrate = send_channel->encoder_observer()->bitrate(); + sinfo.preferred_bitrate = send_max_bitrate_; + sinfo.adapt_reason = send_channel->CurrentAdaptReason(); + + // Get received RTCP statistics for the sender, if available. + // It's not a fatal error if we can't, since RTCP may not have arrived + // yet. + uint16 r_fraction_lost; + unsigned int r_cumulative_lost; + unsigned int r_extended_max; + unsigned int r_jitter; + int r_rtt_ms; + + if (engine_->vie()->rtp()->GetSentRTCPStatistics( + channel_id, + r_fraction_lost, + r_cumulative_lost, + r_extended_max, + r_jitter, r_rtt_ms) == 0) { + // Convert Q8 to float. + sinfo.packets_lost = r_cumulative_lost; + sinfo.fraction_lost = static_cast(r_fraction_lost) / (1 << 8); + sinfo.rtt_ms = r_rtt_ms; + } + info->senders.push_back(sinfo); + + unsigned int channel_total_bitrate_sent = 0; + unsigned int channel_video_bitrate_sent = 0; + unsigned int channel_fec_bitrate_sent = 0; + unsigned int channel_nack_bitrate_sent = 0; + if (engine_->vie()->rtp()->GetBandwidthUsage( + channel_id, channel_total_bitrate_sent, channel_video_bitrate_sent, + channel_fec_bitrate_sent, channel_nack_bitrate_sent) == 0) { + total_bitrate_sent += channel_total_bitrate_sent; + video_bitrate_sent += channel_video_bitrate_sent; + fec_bitrate_sent += channel_fec_bitrate_sent; + nack_bitrate_sent += channel_nack_bitrate_sent; + } else { + LOG_RTCERR1(GetBandwidthUsage, channel_id); + } + + unsigned int estimated_stream_send_bandwidth = 0; + if (engine_->vie()->rtp()->GetEstimatedSendBandwidth( + channel_id, &estimated_stream_send_bandwidth) == 0) { + estimated_send_bandwidth += estimated_stream_send_bandwidth; + } else { + LOG_RTCERR1(GetEstimatedSendBandwidth, channel_id); + } + unsigned int target_enc_stream_bitrate = 0; + if (engine_->vie()->codec()->GetCodecTargetBitrate( + channel_id, &target_enc_stream_bitrate) == 0) { + target_enc_bitrate += target_enc_stream_bitrate; + } else { + LOG_RTCERR1(GetCodecTargetBitrate, channel_id); + } + } + } else { + LOG(LS_WARNING) << "GetStats: sender information not ready."; + } + + // Get the SSRC and stats for each receiver, based on our own calculations. + unsigned int estimated_recv_bandwidth = 0; + for (RecvChannelMap::const_iterator it = recv_channels_.begin(); + it != recv_channels_.end(); ++it) { + // Don't report receive statistics from the default channel if we have + // specified receive channels. + if (it->first == 0 && recv_channels_.size() > 1) + continue; + WebRtcVideoChannelRecvInfo* channel = it->second; + + unsigned int ssrc; + // Get receiver statistics and build VideoReceiverInfo, if we have data. + if (engine_->vie()->rtp()->GetRemoteSSRC(channel->channel_id(), ssrc) != 0) + continue; + + unsigned int bytes_sent, packets_sent, bytes_recv, packets_recv; + if (engine_->vie()->rtp()->GetRTPStatistics( + channel->channel_id(), bytes_sent, packets_sent, bytes_recv, + packets_recv) != 0) { + LOG_RTCERR1(GetRTPStatistics, channel->channel_id()); + return false; + } + VideoReceiverInfo rinfo; + rinfo.ssrcs.push_back(ssrc); + rinfo.bytes_rcvd = bytes_recv; + rinfo.packets_rcvd = packets_recv; + rinfo.packets_lost = -1; + rinfo.packets_concealed = -1; + rinfo.fraction_lost = -1; // from SentRTCP + rinfo.firs_sent = channel->decoder_observer()->firs_requested(); + rinfo.nacks_sent = -1; + rinfo.frame_width = channel->render_adapter()->width(); + rinfo.frame_height = channel->render_adapter()->height(); + rinfo.framerate_rcvd = channel->decoder_observer()->framerate(); + int fps = channel->render_adapter()->framerate(); + rinfo.framerate_decoded = fps; + rinfo.framerate_output = fps; + + // Get sent RTCP statistics. + uint16 s_fraction_lost; + unsigned int s_cumulative_lost; + unsigned int s_extended_max; + unsigned int s_jitter; + int s_rtt_ms; + if (engine_->vie()->rtp()->GetReceivedRTCPStatistics(channel->channel_id(), + s_fraction_lost, s_cumulative_lost, s_extended_max, + s_jitter, s_rtt_ms) == 0) { + // Convert Q8 to float. + rinfo.packets_lost = s_cumulative_lost; + rinfo.fraction_lost = static_cast(s_fraction_lost) / (1 << 8); + } + info->receivers.push_back(rinfo); + + unsigned int estimated_recv_stream_bandwidth = 0; + if (engine_->vie()->rtp()->GetEstimatedReceiveBandwidth( + channel->channel_id(), &estimated_recv_stream_bandwidth) == 0) { + estimated_recv_bandwidth += estimated_recv_stream_bandwidth; + } else { + LOG_RTCERR1(GetEstimatedReceiveBandwidth, channel->channel_id()); + } + } + + // Build BandwidthEstimationInfo. + // TODO(zhurunz): Add real unittest for this. + BandwidthEstimationInfo bwe; + + // Calculations done above per send/receive stream. + bwe.actual_enc_bitrate = video_bitrate_sent; + bwe.transmit_bitrate = total_bitrate_sent; + bwe.retransmit_bitrate = nack_bitrate_sent; + bwe.available_send_bandwidth = estimated_send_bandwidth; + bwe.available_recv_bandwidth = estimated_recv_bandwidth; + bwe.target_enc_bitrate = target_enc_bitrate; + + info->bw_estimations.push_back(bwe); + + return true; +} + +bool WebRtcVideoMediaChannel::SetCapturer(uint32 ssrc, + VideoCapturer* capturer) { + ASSERT(ssrc != 0); + if (!capturer) { + return RemoveCapturer(ssrc); + } + WebRtcVideoChannelSendInfo* send_channel = GetSendChannel(ssrc); + if (!send_channel) { + return false; + } + VideoCapturer* old_capturer = send_channel->video_capturer(); + if (send_channel->sending() && !old_capturer) { + engine_->DecrementFrameListeners(); + } + if (old_capturer) { + old_capturer->SignalVideoFrame.disconnect(this); + } + + send_channel->set_video_capturer(capturer); + capturer->SignalVideoFrame.connect( + this, + &WebRtcVideoMediaChannel::AdaptAndSendFrame); + if (!capturer->IsScreencast() && ratio_w_ != 0 && ratio_h_ != 0) { + capturer->UpdateAspectRatio(ratio_w_, ratio_h_); + } + const int64 timestamp = send_channel->local_stream_info()->time_stamp(); + if (send_codec_) { + QueueBlackFrame(ssrc, timestamp, send_codec_->maxFramerate); + } + return true; +} + +bool WebRtcVideoMediaChannel::RequestIntraFrame() { + // There is no API exposed to application to request a key frame + // ViE does this internally when there are errors from decoder + return false; +} + +void WebRtcVideoMediaChannel::OnPacketReceived(talk_base::Buffer* packet) { + // Pick which channel to send this packet to. If this packet doesn't match + // any multiplexed streams, just send it to the default channel. Otherwise, + // send it to the specific decoder instance for that stream. + uint32 ssrc = 0; + if (!GetRtpSsrc(packet->data(), packet->length(), &ssrc)) + return; + int which_channel = GetRecvChannelNum(ssrc); + if (which_channel == -1) { + which_channel = video_channel(); + } + + engine()->vie()->network()->ReceivedRTPPacket(which_channel, + packet->data(), + packet->length()); +} + +void WebRtcVideoMediaChannel::OnRtcpReceived(talk_base::Buffer* packet) { +// Sending channels need all RTCP packets with feedback information. +// Even sender reports can contain attached report blocks. +// Receiving channels need sender reports in order to create +// correct receiver reports. + + uint32 ssrc = 0; + if (!GetRtcpSsrc(packet->data(), packet->length(), &ssrc)) { + LOG(LS_WARNING) << "Failed to parse SSRC from received RTCP packet"; + return; + } + int type = 0; + if (!GetRtcpType(packet->data(), packet->length(), &type)) { + LOG(LS_WARNING) << "Failed to parse type from received RTCP packet"; + return; + } + + // If it is a sender report, find the channel that is listening. + if (type == kRtcpTypeSR) { + int which_channel = GetRecvChannelNum(ssrc); + if (which_channel != -1 && !IsDefaultChannel(which_channel)) { + engine_->vie()->network()->ReceivedRTCPPacket(which_channel, + packet->data(), + packet->length()); + } + } + // SR may continue RR and any RR entry may correspond to any one of the send + // channels. So all RTCP packets must be forwarded all send channels. ViE + // will filter out RR internally. + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + int channel_id = send_channel->channel_id(); + engine_->vie()->network()->ReceivedRTCPPacket(channel_id, + packet->data(), + packet->length()); + } +} + +void WebRtcVideoMediaChannel::OnReadyToSend(bool ready) { + SetNetworkTransmissionState(ready); +} + +bool WebRtcVideoMediaChannel::MuteStream(uint32 ssrc, bool muted) { + WebRtcVideoChannelSendInfo* send_channel = GetSendChannel(ssrc); + if (!send_channel) { + LOG(LS_ERROR) << "The specified ssrc " << ssrc << " is not in use."; + return false; + } + send_channel->set_muted(muted); + return true; +} + +bool WebRtcVideoMediaChannel::SetRecvRtpHeaderExtensions( + const std::vector& extensions) { + if (receive_extensions_ == extensions) { + return true; + } + receive_extensions_ = extensions; + + const RtpHeaderExtension* offset_extension = + FindHeaderExtension(extensions, kRtpTimestampOffsetHeaderExtension); + const RtpHeaderExtension* send_time_extension = + FindHeaderExtension(extensions, kRtpAbsoluteSendTimeHeaderExtension); + + // Loop through all receive channels and enable/disable the extensions. + for (RecvChannelMap::iterator channel_it = recv_channels_.begin(); + channel_it != recv_channels_.end(); ++channel_it) { + int channel_id = channel_it->second->channel_id(); + if (!SetHeaderExtension( + &webrtc::ViERTP_RTCP::SetReceiveTimestampOffsetStatus, channel_id, + offset_extension)) { + return false; + } + if (!SetHeaderExtension( + &webrtc::ViERTP_RTCP::SetReceiveAbsoluteSendTimeStatus, channel_id, + send_time_extension)) { + return false; + } + } + return true; +} + +bool WebRtcVideoMediaChannel::SetSendRtpHeaderExtensions( + const std::vector& extensions) { + send_extensions_ = extensions; + + const RtpHeaderExtension* offset_extension = + FindHeaderExtension(extensions, kRtpTimestampOffsetHeaderExtension); + const RtpHeaderExtension* send_time_extension = + FindHeaderExtension(extensions, kRtpAbsoluteSendTimeHeaderExtension); + + // Loop through all send channels and enable/disable the extensions. + for (SendChannelMap::iterator channel_it = send_channels_.begin(); + channel_it != send_channels_.end(); ++channel_it) { + int channel_id = channel_it->second->channel_id(); + if (!SetHeaderExtension( + &webrtc::ViERTP_RTCP::SetSendTimestampOffsetStatus, channel_id, + offset_extension)) { + return false; + } + if (!SetHeaderExtension( + &webrtc::ViERTP_RTCP::SetSendAbsoluteSendTimeStatus, channel_id, + send_time_extension)) { + return false; + } + } + return true; +} + +bool WebRtcVideoMediaChannel::SetSendBandwidth(bool autobw, int bps) { + LOG(LS_INFO) << "WebRtcVideoMediaChanne::SetSendBandwidth"; + + if (InConferenceMode()) { + LOG(LS_INFO) << "Conference mode ignores SetSendBandWidth"; + return true; + } + + if (!send_codec_) { + LOG(LS_INFO) << "The send codec has not been set up yet"; + return true; + } + + int min_bitrate; + int start_bitrate; + int max_bitrate; + if (autobw) { + // Use the default values for min bitrate. + min_bitrate = kMinVideoBitrate; + // Use the default value or the bps for the max + max_bitrate = (bps <= 0) ? send_max_bitrate_ : (bps / 1000); + // Maximum start bitrate can be kStartVideoBitrate. + start_bitrate = talk_base::_min(kStartVideoBitrate, max_bitrate); + } else { + // Use the default start or the bps as the target bitrate. + int target_bitrate = (bps <= 0) ? kStartVideoBitrate : (bps / 1000); + min_bitrate = target_bitrate; + start_bitrate = target_bitrate; + max_bitrate = target_bitrate; + } + + if (!SetSendCodec(*send_codec_, min_bitrate, start_bitrate, max_bitrate)) { + return false; + } + LogSendCodecChange("SetSendBandwidth()"); + + return true; +} + +bool WebRtcVideoMediaChannel::SetOptions(const VideoOptions &options) { + // Always accept options that are unchanged. + if (options_ == options) { + return true; + } + + // Trigger SetSendCodec to set correct noise reduction state if the option has + // changed. + bool denoiser_changed = options.video_noise_reduction.IsSet() && + (options_.video_noise_reduction != options.video_noise_reduction); + + bool leaky_bucket_changed = options.video_leaky_bucket.IsSet() && + (options_.video_leaky_bucket != options.video_leaky_bucket); + + bool buffer_latency_changed = options.buffered_mode_latency.IsSet() && + (options_.buffered_mode_latency != options.buffered_mode_latency); + + bool conference_mode_turned_off = false; + if (options_.conference_mode.IsSet() && options.conference_mode.IsSet() && + options_.conference_mode.GetWithDefaultIfUnset(false) && + !options.conference_mode.GetWithDefaultIfUnset(false)) { + conference_mode_turned_off = true; + } + + // Save the options, to be interpreted where appropriate. + // Use options_.SetAll() instead of assignment so that unset value in options + // will not overwrite the previous option value. + options_.SetAll(options); + + // Set CPU options for all send channels. + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + send_channel->ApplyCpuOptions(options_); + } + + // Adjust send codec bitrate if needed. + int conf_max_bitrate = kDefaultConferenceModeMaxVideoBitrate; + + int expected_bitrate = send_max_bitrate_; + if (InConferenceMode()) { + expected_bitrate = conf_max_bitrate; + } else if (conference_mode_turned_off) { + // This is a special case for turning conference mode off. + // Max bitrate should go back to the default maximum value instead + // of the current maximum. + expected_bitrate = kMaxVideoBitrate; + } + + if (send_codec_ && + (send_max_bitrate_ != expected_bitrate || denoiser_changed)) { + // On success, SetSendCodec() will reset send_max_bitrate_ to + // expected_bitrate. + if (!SetSendCodec(*send_codec_, + send_min_bitrate_, + send_start_bitrate_, + expected_bitrate)) { + return false; + } + LogSendCodecChange("SetOptions()"); + } + if (leaky_bucket_changed) { + bool enable_leaky_bucket = + options_.video_leaky_bucket.GetWithDefaultIfUnset(false); + for (SendChannelMap::iterator it = send_channels_.begin(); + it != send_channels_.end(); ++it) { + if (engine()->vie()->rtp()->SetTransmissionSmoothingStatus( + it->second->channel_id(), enable_leaky_bucket) != 0) { + LOG_RTCERR2(SetTransmissionSmoothingStatus, it->second->channel_id(), + enable_leaky_bucket); + } + } + } + if (buffer_latency_changed) { + int buffer_latency = + options_.buffered_mode_latency.GetWithDefaultIfUnset( + cricket::kBufferedModeDisabled); + for (SendChannelMap::iterator it = send_channels_.begin(); + it != send_channels_.end(); ++it) { + if (engine()->vie()->rtp()->SetSenderBufferingMode( + it->second->channel_id(), buffer_latency) != 0) { + LOG_RTCERR2(SetSenderBufferingMode, it->second->channel_id(), + buffer_latency); + } + } + for (RecvChannelMap::iterator it = recv_channels_.begin(); + it != recv_channels_.end(); ++it) { + if (engine()->vie()->rtp()->SetReceiverBufferingMode( + it->second->channel_id(), buffer_latency) != 0) { + LOG_RTCERR2(SetReceiverBufferingMode, it->second->channel_id(), + buffer_latency); + } + } + } + return true; +} + +void WebRtcVideoMediaChannel::SetInterface(NetworkInterface* iface) { + MediaChannel::SetInterface(iface); + // Set the RTP recv/send buffer to a bigger size + if (network_interface_) { + network_interface_->SetOption(NetworkInterface::ST_RTP, + talk_base::Socket::OPT_RCVBUF, + kVideoRtpBufferSize); + + // TODO(sriniv): Remove or re-enable this. + // As part of b/8030474, send-buffer is size now controlled through + // portallocator flags. + // network_interface_->SetOption(NetworkInterface::ST_RTP, + // talk_base::Socket::OPT_SNDBUF, + // kVideoRtpBufferSize); + } +} + +void WebRtcVideoMediaChannel::UpdateAspectRatio(int ratio_w, int ratio_h) { + ASSERT(ratio_w != 0); + ASSERT(ratio_h != 0); + ratio_w_ = ratio_w; + ratio_h_ = ratio_h; + // For now assume that all streams want the same aspect ratio. + // TODO(hellner): remove the need for this assumption. + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + VideoCapturer* capturer = send_channel->video_capturer(); + if (capturer) { + capturer->UpdateAspectRatio(ratio_w, ratio_h); + } + } +} + +bool WebRtcVideoMediaChannel::GetRenderer(uint32 ssrc, + VideoRenderer** renderer) { + RecvChannelMap::const_iterator it = recv_channels_.find(ssrc); + if (it == recv_channels_.end()) { + if (first_receive_ssrc_ == ssrc && + recv_channels_.find(0) != recv_channels_.end()) { + LOG(LS_INFO) << " GetRenderer " << ssrc + << " reuse default renderer #" + << vie_channel_; + *renderer = recv_channels_[0]->render_adapter()->renderer(); + return true; + } + return false; + } + + *renderer = it->second->render_adapter()->renderer(); + return true; +} + +void WebRtcVideoMediaChannel::AdaptAndSendFrame(VideoCapturer* capturer, + const VideoFrame* frame) { + if (capturer->IsScreencast()) { + // Do not adapt frames that are screencast. + SendFrame(capturer, frame); + return; + } + WebRtcVideoChannelSendInfo* send_channel = GetSendChannel(capturer); + if (!send_channel) { + SendFrame(capturer, frame); + return; + } + const VideoFrame* output_frame = NULL; + send_channel->AdaptFrame(frame, &output_frame); + if (output_frame) { + SendFrame(send_channel, output_frame, capturer->IsScreencast()); + } +} + +// TODO(zhurunz): Add unittests to test this function. +void WebRtcVideoMediaChannel::SendFrame(VideoCapturer* capturer, + const VideoFrame* frame) { + // If there's send channel registers to the |capturer|, then only send the + // frame to that channel and return. Otherwise send the frame to the default + // channel, which currently taking frames from the engine. + WebRtcVideoChannelSendInfo* send_channel = GetSendChannel(capturer); + if (send_channel) { + SendFrame(send_channel, frame, capturer->IsScreencast()); + return; + } + // TODO(hellner): Remove below for loop once the captured frame no longer + // come from the engine, i.e. the engine no longer owns a capturer. + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + if (send_channel->video_capturer() == NULL) { + SendFrame(send_channel, frame, capturer->IsScreencast()); + } + } +} + +bool WebRtcVideoMediaChannel::SendFrame( + WebRtcVideoChannelSendInfo* send_channel, + const VideoFrame* frame, + bool is_screencast) { + if (!send_channel) { + return false; + } + if (!send_codec_) { + // Send codec has not been set. No reason to process the frame any further. + return false; + } + const VideoFormat& video_format = send_channel->video_format(); + // If the frame should be dropped. + const bool video_format_set = video_format != cricket::VideoFormat(); + if (video_format_set && + (video_format.width == 0 && video_format.height == 0)) { + return true; + } + + // Checks if we need to reset vie send codec. + if (!MaybeResetVieSendCodec(send_channel, frame->GetWidth(), + frame->GetHeight(), is_screencast, NULL)) { + LOG(LS_ERROR) << "MaybeResetVieSendCodec failed with " + << frame->GetWidth() << "x" << frame->GetHeight(); + return false; + } + const VideoFrame* frame_out = frame; + talk_base::scoped_ptr processed_frame; + // Disable muting for screencast. + const bool mute = (send_channel->muted() && !is_screencast); + send_channel->ProcessFrame(*frame_out, mute, processed_frame.use()); + if (processed_frame) { + frame_out = processed_frame.get(); + } + + webrtc::ViEVideoFrameI420 frame_i420; + // TODO(ronghuawu): Update the webrtc::ViEVideoFrameI420 + // to use const unsigned char* + frame_i420.y_plane = const_cast(frame_out->GetYPlane()); + frame_i420.u_plane = const_cast(frame_out->GetUPlane()); + frame_i420.v_plane = const_cast(frame_out->GetVPlane()); + frame_i420.y_pitch = frame_out->GetYPitch(); + frame_i420.u_pitch = frame_out->GetUPitch(); + frame_i420.v_pitch = frame_out->GetVPitch(); + frame_i420.width = frame_out->GetWidth(); + frame_i420.height = frame_out->GetHeight(); + + int64 timestamp_ntp_ms = 0; + // TODO(justinlin): Reenable after Windows issues with clock drift are fixed. + // Currently reverted to old behavior of discarding capture timestamp. +#if 0 + // If the frame timestamp is 0, we will use the deliver time. + const int64 frame_timestamp = frame->GetTimeStamp(); + if (frame_timestamp != 0) { + if (abs(time(NULL) - frame_timestamp / talk_base::kNumNanosecsPerSec) > + kTimestampDeltaInSecondsForWarning) { + LOG(LS_WARNING) << "Frame timestamp differs by more than " + << kTimestampDeltaInSecondsForWarning << " seconds from " + << "current Unix timestamp."; + } + + timestamp_ntp_ms = + talk_base::UnixTimestampNanosecsToNtpMillisecs(frame_timestamp); + } +#endif + + return send_channel->external_capture()->IncomingFrameI420( + frame_i420, timestamp_ntp_ms) == 0; +} + +bool WebRtcVideoMediaChannel::CreateChannel(uint32 ssrc_key, + MediaDirection direction, + int* channel_id) { + // There are 3 types of channels. Sending only, receiving only and + // sending and receiving. The sending and receiving channel is the + // default channel and there is only one. All other channels that are created + // are associated with the default channel which must exist. The default + // channel id is stored in |vie_channel_|. All channels need to know about + // the default channel to properly handle remb which is why there are + // different ViE create channel calls. + // For this channel the local and remote ssrc key is 0. However, it may + // have a non-zero local and/or remote ssrc depending on if it is currently + // sending and/or receiving. + if ((vie_channel_ == -1 || direction == MD_SENDRECV) && + (!send_channels_.empty() || !recv_channels_.empty())) { + ASSERT(false); + return false; + } + + *channel_id = -1; + if (direction == MD_RECV) { + // All rec channels are associated with the default channel |vie_channel_| + if (engine_->vie()->base()->CreateReceiveChannel(*channel_id, + vie_channel_) != 0) { + LOG_RTCERR2(CreateReceiveChannel, *channel_id, vie_channel_); + return false; + } + } else if (direction == MD_SEND) { + if (engine_->vie()->base()->CreateChannel(*channel_id, + vie_channel_) != 0) { + LOG_RTCERR2(CreateChannel, *channel_id, vie_channel_); + return false; + } + } else { + ASSERT(direction == MD_SENDRECV); + if (engine_->vie()->base()->CreateChannel(*channel_id) != 0) { + LOG_RTCERR1(CreateChannel, *channel_id); + return false; + } + } + if (!ConfigureChannel(*channel_id, direction, ssrc_key)) { + engine_->vie()->base()->DeleteChannel(*channel_id); + *channel_id = -1; + return false; + } + + return true; +} + +bool WebRtcVideoMediaChannel::ConfigureChannel(int channel_id, + MediaDirection direction, + uint32 ssrc_key) { + const bool receiving = (direction == MD_RECV) || (direction == MD_SENDRECV); + const bool sending = (direction == MD_SEND) || (direction == MD_SENDRECV); + // Register external transport. + if (engine_->vie()->network()->RegisterSendTransport( + channel_id, *this) != 0) { + LOG_RTCERR1(RegisterSendTransport, channel_id); + return false; + } + + // Set MTU. + if (engine_->vie()->network()->SetMTU(channel_id, kVideoMtu) != 0) { + LOG_RTCERR2(SetMTU, channel_id, kVideoMtu); + return false; + } + // Turn on RTCP and loss feedback reporting. + if (engine()->vie()->rtp()->SetRTCPStatus( + channel_id, webrtc::kRtcpCompound_RFC4585) != 0) { + LOG_RTCERR2(SetRTCPStatus, channel_id, webrtc::kRtcpCompound_RFC4585); + return false; + } + // Enable pli as key frame request method. + if (engine_->vie()->rtp()->SetKeyFrameRequestMethod( + channel_id, webrtc::kViEKeyFrameRequestPliRtcp) != 0) { + LOG_RTCERR2(SetKeyFrameRequestMethod, + channel_id, webrtc::kViEKeyFrameRequestPliRtcp); + return false; + } + if (!SetNackFec(channel_id, send_red_type_, send_fec_type_, nack_enabled_)) { + // Logged in SetNackFec. Don't spam the logs. + return false; + } + // Note that receiving must always be configured before sending to ensure + // that send and receive channel is configured correctly (ConfigureReceiving + // assumes no sending). + if (receiving) { + if (!ConfigureReceiving(channel_id, ssrc_key)) { + return false; + } + } + if (sending) { + if (!ConfigureSending(channel_id, ssrc_key)) { + return false; + } + } + + return true; +} + +bool WebRtcVideoMediaChannel::ConfigureReceiving(int channel_id, + uint32 remote_ssrc_key) { + // Make sure that an SSRC/key isn't registered more than once. + if (recv_channels_.find(remote_ssrc_key) != recv_channels_.end()) { + return false; + } + // Connect the voice channel, if there is one. + // TODO(perkj): The A/V is synched by the receiving channel. So we need to + // know the SSRC of the remote audio channel in order to fetch the correct + // webrtc VoiceEngine channel. For now- only sync the default channel used + // in 1-1 calls. + if (remote_ssrc_key == 0 && voice_channel_) { + WebRtcVoiceMediaChannel* voice_channel = + static_cast(voice_channel_); + if (engine_->vie()->base()->ConnectAudioChannel( + vie_channel_, voice_channel->voe_channel()) != 0) { + LOG_RTCERR2(ConnectAudioChannel, channel_id, + voice_channel->voe_channel()); + LOG(LS_WARNING) << "A/V not synchronized"; + // Not a fatal error. + } + } + + talk_base::scoped_ptr channel_info( + new WebRtcVideoChannelRecvInfo(channel_id)); + + // Install a render adapter. + if (engine_->vie()->render()->AddRenderer(channel_id, + webrtc::kVideoI420, channel_info->render_adapter()) != 0) { + LOG_RTCERR3(AddRenderer, channel_id, webrtc::kVideoI420, + channel_info->render_adapter()); + return false; + } + + + if (engine_->vie()->rtp()->SetRembStatus(channel_id, + kNotSending, + remb_enabled_) != 0) { + LOG_RTCERR3(SetRembStatus, channel_id, kNotSending, remb_enabled_); + return false; + } + + if (!SetHeaderExtension(&webrtc::ViERTP_RTCP::SetReceiveTimestampOffsetStatus, + channel_id, receive_extensions_, kRtpTimestampOffsetHeaderExtension)) { + return false; + } + + if (!SetHeaderExtension( + &webrtc::ViERTP_RTCP::SetReceiveAbsoluteSendTimeStatus, channel_id, + receive_extensions_, kRtpAbsoluteSendTimeHeaderExtension)) { + return false; + } + + if (remote_ssrc_key != 0) { + // Use the same SSRC as our default channel + // (so the RTCP reports are correct). + unsigned int send_ssrc = 0; + webrtc::ViERTP_RTCP* rtp = engine()->vie()->rtp(); + if (rtp->GetLocalSSRC(vie_channel_, send_ssrc) == -1) { + LOG_RTCERR2(GetLocalSSRC, vie_channel_, send_ssrc); + return false; + } + if (rtp->SetLocalSSRC(channel_id, send_ssrc) == -1) { + LOG_RTCERR2(SetLocalSSRC, channel_id, send_ssrc); + return false; + } + } // Else this is the the default channel and we don't change the SSRC. + + // Disable color enhancement since it is a bit too aggressive. + if (engine()->vie()->image()->EnableColorEnhancement(channel_id, + false) != 0) { + LOG_RTCERR1(EnableColorEnhancement, channel_id); + return false; + } + + if (!SetReceiveCodecs(channel_info.get())) { + return false; + } + + int buffer_latency = + options_.buffered_mode_latency.GetWithDefaultIfUnset( + cricket::kBufferedModeDisabled); + if (buffer_latency != cricket::kBufferedModeDisabled) { + if (engine()->vie()->rtp()->SetReceiverBufferingMode( + channel_id, buffer_latency) != 0) { + LOG_RTCERR2(SetReceiverBufferingMode, channel_id, buffer_latency); + } + } + + if (render_started_) { + if (engine_->vie()->render()->StartRender(channel_id) != 0) { + LOG_RTCERR1(StartRender, channel_id); + return false; + } + } + + // Register decoder observer for incoming framerate and bitrate. + if (engine()->vie()->codec()->RegisterDecoderObserver( + channel_id, *channel_info->decoder_observer()) != 0) { + LOG_RTCERR1(RegisterDecoderObserver, channel_info->decoder_observer()); + return false; + } + + recv_channels_[remote_ssrc_key] = channel_info.release(); + return true; +} + +bool WebRtcVideoMediaChannel::ConfigureSending(int channel_id, + uint32 local_ssrc_key) { + // The ssrc key can be zero or correspond to an SSRC. + // Make sure the default channel isn't configured more than once. + if (local_ssrc_key == 0 && send_channels_.find(0) != send_channels_.end()) { + return false; + } + // Make sure that the SSRC is not already in use. + uint32 dummy_key; + if (GetSendChannelKey(local_ssrc_key, &dummy_key)) { + return false; + } + int vie_capture = 0; + webrtc::ViEExternalCapture* external_capture = NULL; + // Register external capture. + if (engine()->vie()->capture()->AllocateExternalCaptureDevice( + vie_capture, external_capture) != 0) { + LOG_RTCERR0(AllocateExternalCaptureDevice); + return false; + } + + // Connect external capture. + if (engine()->vie()->capture()->ConnectCaptureDevice( + vie_capture, channel_id) != 0) { + LOG_RTCERR2(ConnectCaptureDevice, vie_capture, channel_id); + return false; + } + talk_base::scoped_ptr send_channel( + new WebRtcVideoChannelSendInfo(channel_id, vie_capture, + external_capture, + engine()->cpu_monitor())); + send_channel->ApplyCpuOptions(options_); + + // Register encoder observer for outgoing framerate and bitrate. + if (engine()->vie()->codec()->RegisterEncoderObserver( + channel_id, *send_channel->encoder_observer()) != 0) { + LOG_RTCERR1(RegisterEncoderObserver, send_channel->encoder_observer()); + return false; + } + + if (!SetHeaderExtension(&webrtc::ViERTP_RTCP::SetSendTimestampOffsetStatus, + channel_id, send_extensions_, kRtpTimestampOffsetHeaderExtension)) { + return false; + } + + if (!SetHeaderExtension(&webrtc::ViERTP_RTCP::SetSendAbsoluteSendTimeStatus, + channel_id, send_extensions_, kRtpAbsoluteSendTimeHeaderExtension)) { + return false; + } + + if (options_.video_leaky_bucket.GetWithDefaultIfUnset(false)) { + if (engine()->vie()->rtp()->SetTransmissionSmoothingStatus(channel_id, + true) != 0) { + LOG_RTCERR2(SetTransmissionSmoothingStatus, channel_id, true); + return false; + } + } + + int buffer_latency = + options_.buffered_mode_latency.GetWithDefaultIfUnset( + cricket::kBufferedModeDisabled); + if (buffer_latency != cricket::kBufferedModeDisabled) { + if (engine()->vie()->rtp()->SetSenderBufferingMode( + channel_id, buffer_latency) != 0) { + LOG_RTCERR2(SetSenderBufferingMode, channel_id, buffer_latency); + } + } + // The remb status direction correspond to the RTP stream (and not the RTCP + // stream). I.e. if send remb is enabled it means it is receiving remote + // rembs and should use them to estimate bandwidth. Receive remb mean that + // remb packets will be generated and that the channel should be included in + // it. If remb is enabled all channels are allowed to contribute to the remb + // but only receive channels will ever end up actually contributing. This + // keeps the logic simple. + if (engine_->vie()->rtp()->SetRembStatus(channel_id, + remb_enabled_, + remb_enabled_) != 0) { + LOG_RTCERR3(SetRembStatus, channel_id, remb_enabled_, remb_enabled_); + return false; + } + if (!SetNackFec(channel_id, send_red_type_, send_fec_type_, nack_enabled_)) { + // Logged in SetNackFec. Don't spam the logs. + return false; + } + + send_channels_[local_ssrc_key] = send_channel.release(); + + return true; +} + +bool WebRtcVideoMediaChannel::SetNackFec(int channel_id, + int red_payload_type, + int fec_payload_type, + bool nack_enabled) { + bool enable = (red_payload_type != -1 && fec_payload_type != -1 && + !InConferenceMode()); + if (enable) { + if (engine_->vie()->rtp()->SetHybridNACKFECStatus( + channel_id, nack_enabled, red_payload_type, fec_payload_type) != 0) { + LOG_RTCERR4(SetHybridNACKFECStatus, + channel_id, nack_enabled, red_payload_type, fec_payload_type); + return false; + } + LOG(LS_INFO) << "Hybrid NACK/FEC enabled for channel " << channel_id; + } else { + if (engine_->vie()->rtp()->SetNACKStatus(channel_id, nack_enabled) != 0) { + LOG_RTCERR1(SetNACKStatus, channel_id); + return false; + } + LOG(LS_INFO) << "NACK enabled for channel " << channel_id; + } + return true; +} + +bool WebRtcVideoMediaChannel::SetSendCodec(const webrtc::VideoCodec& codec, + int min_bitrate, + int start_bitrate, + int max_bitrate) { + bool ret_val = true; + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + ret_val = SetSendCodec(send_channel, codec, min_bitrate, start_bitrate, + max_bitrate) && ret_val; + } + if (ret_val) { + // All SetSendCodec calls were successful. Update the global state + // accordingly. + send_codec_.reset(new webrtc::VideoCodec(codec)); + send_min_bitrate_ = min_bitrate; + send_start_bitrate_ = start_bitrate; + send_max_bitrate_ = max_bitrate; + } else { + // At least one SetSendCodec call failed, rollback. + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + if (send_codec_) { + SetSendCodec(send_channel, *send_codec_.get(), send_min_bitrate_, + send_start_bitrate_, send_max_bitrate_); + } + } + } + return ret_val; +} + +bool WebRtcVideoMediaChannel::SetSendCodec( + WebRtcVideoChannelSendInfo* send_channel, + const webrtc::VideoCodec& codec, + int min_bitrate, + int start_bitrate, + int max_bitrate) { + if (!send_channel) { + return false; + } + const int channel_id = send_channel->channel_id(); + // Make a copy of the codec + webrtc::VideoCodec target_codec = codec; + target_codec.startBitrate = start_bitrate; + target_codec.minBitrate = min_bitrate; + target_codec.maxBitrate = max_bitrate; + + // Set the default number of temporal layers for VP8. + if (webrtc::kVideoCodecVP8 == codec.codecType) { + target_codec.codecSpecific.VP8.numberOfTemporalLayers = + kDefaultNumberOfTemporalLayers; + + // Turn off the VP8 error resilience + target_codec.codecSpecific.VP8.resilience = webrtc::kResilienceOff; + + bool enable_denoising = + options_.video_noise_reduction.GetWithDefaultIfUnset(false); + target_codec.codecSpecific.VP8.denoisingOn = enable_denoising; + } + + // Register external encoder if codec type is supported by encoder factory. + if (engine()->IsExternalEncoderCodecType(codec.codecType) && + !send_channel->IsEncoderRegistered(target_codec.plType)) { + webrtc::VideoEncoder* encoder = + engine()->CreateExternalEncoder(codec.codecType); + if (encoder) { + if (engine()->vie()->ext_codec()->RegisterExternalSendCodec( + channel_id, target_codec.plType, encoder, false) == 0) { + send_channel->RegisterEncoder(target_codec.plType, encoder); + } else { + LOG_RTCERR2(RegisterExternalSendCodec, channel_id, target_codec.plName); + engine()->DestroyExternalEncoder(encoder); + } + } + } + + // Resolution and framerate may vary for different send channels. + const VideoFormat& video_format = send_channel->video_format(); + UpdateVideoCodec(video_format, &target_codec); + + if (target_codec.width == 0 && target_codec.height == 0) { + const uint32 ssrc = send_channel->stream_params()->first_ssrc(); + LOG(LS_INFO) << "0x0 resolution selected. Captured frames will be dropped " + << "for ssrc: " << ssrc << "."; + } else { + MaybeChangeStartBitrate(channel_id, &target_codec); + if (0 != engine()->vie()->codec()->SetSendCodec(channel_id, target_codec)) { + LOG_RTCERR2(SetSendCodec, channel_id, target_codec.plName); + return false; + } + + } + send_channel->set_interval( + cricket::VideoFormat::FpsToInterval(target_codec.maxFramerate)); + return true; +} + + +static std::string ToString(webrtc::VideoCodecComplexity complexity) { + switch (complexity) { + case webrtc::kComplexityNormal: + return "normal"; + case webrtc::kComplexityHigh: + return "high"; + case webrtc::kComplexityHigher: + return "higher"; + case webrtc::kComplexityMax: + return "max"; + default: + return "unknown"; + } +} + +static std::string ToString(webrtc::VP8ResilienceMode resilience) { + switch (resilience) { + case webrtc::kResilienceOff: + return "off"; + case webrtc::kResilientStream: + return "stream"; + case webrtc::kResilientFrames: + return "frames"; + default: + return "unknown"; + } +} + +void WebRtcVideoMediaChannel::LogSendCodecChange(const std::string& reason) { + webrtc::VideoCodec vie_codec; + if (engine()->vie()->codec()->GetSendCodec(vie_channel_, vie_codec) != 0) { + LOG_RTCERR1(GetSendCodec, vie_channel_); + return; + } + + LOG(LS_INFO) << reason << " : selected video codec " + << vie_codec.plName << "/" + << vie_codec.width << "x" << vie_codec.height << "x" + << static_cast(vie_codec.maxFramerate) << "fps" + << "@" << vie_codec.maxBitrate << "kbps" + << " (min=" << vie_codec.minBitrate << "kbps," + << " start=" << vie_codec.startBitrate << "kbps)"; + LOG(LS_INFO) << "Video max quantization: " << vie_codec.qpMax; + if (webrtc::kVideoCodecVP8 == vie_codec.codecType) { + LOG(LS_INFO) << "VP8 number of temporal layers: " + << static_cast( + vie_codec.codecSpecific.VP8.numberOfTemporalLayers); + LOG(LS_INFO) << "VP8 options : " + << "picture loss indication = " + << vie_codec.codecSpecific.VP8.pictureLossIndicationOn + << ", feedback mode = " + << vie_codec.codecSpecific.VP8.feedbackModeOn + << ", complexity = " + << ToString(vie_codec.codecSpecific.VP8.complexity) + << ", resilience = " + << ToString(vie_codec.codecSpecific.VP8.resilience) + << ", denoising = " + << vie_codec.codecSpecific.VP8.denoisingOn + << ", error concealment = " + << vie_codec.codecSpecific.VP8.errorConcealmentOn + << ", automatic resize = " + << vie_codec.codecSpecific.VP8.automaticResizeOn + << ", frame dropping = " + << vie_codec.codecSpecific.VP8.frameDroppingOn + << ", key frame interval = " + << vie_codec.codecSpecific.VP8.keyFrameInterval; + } + +} + +bool WebRtcVideoMediaChannel::SetReceiveCodecs( + WebRtcVideoChannelRecvInfo* info) { + int red_type = -1; + int fec_type = -1; + int channel_id = info->channel_id(); + for (std::vector::iterator it = receive_codecs_.begin(); + it != receive_codecs_.end(); ++it) { + if (it->codecType == webrtc::kVideoCodecRED) { + red_type = it->plType; + } else if (it->codecType == webrtc::kVideoCodecULPFEC) { + fec_type = it->plType; + } + if (engine()->vie()->codec()->SetReceiveCodec(channel_id, *it) != 0) { + LOG_RTCERR2(SetReceiveCodec, channel_id, it->plName); + return false; + } + if (!info->IsDecoderRegistered(it->plType) && + it->codecType != webrtc::kVideoCodecRED && + it->codecType != webrtc::kVideoCodecULPFEC) { + webrtc::VideoDecoder* decoder = + engine()->CreateExternalDecoder(it->codecType); + if (decoder) { + if (engine()->vie()->ext_codec()->RegisterExternalReceiveCodec( + channel_id, it->plType, decoder) == 0) { + info->RegisterDecoder(it->plType, decoder); + } else { + LOG_RTCERR2(RegisterExternalReceiveCodec, channel_id, it->plName); + engine()->DestroyExternalDecoder(decoder); + } + } + } + } + + // Start receiving packets if at least one receive codec has been set. + if (!receive_codecs_.empty()) { + if (engine()->vie()->base()->StartReceive(channel_id) != 0) { + LOG_RTCERR1(StartReceive, channel_id); + return false; + } + } + return true; +} + +int WebRtcVideoMediaChannel::GetRecvChannelNum(uint32 ssrc) { + if (ssrc == first_receive_ssrc_) { + return vie_channel_; + } + RecvChannelMap::iterator it = recv_channels_.find(ssrc); + return (it != recv_channels_.end()) ? it->second->channel_id() : -1; +} + +// If the new frame size is different from the send codec size we set on vie, +// we need to reset the send codec on vie. +// The new send codec size should not exceed send_codec_ which is controlled +// only by the 'jec' logic. +bool WebRtcVideoMediaChannel::MaybeResetVieSendCodec( + WebRtcVideoChannelSendInfo* send_channel, + int new_width, + int new_height, + bool is_screencast, + bool* reset) { + if (reset) { + *reset = false; + } + ASSERT(send_codec_.get() != NULL); + + webrtc::VideoCodec target_codec = *send_codec_.get(); + const VideoFormat& video_format = send_channel->video_format(); + UpdateVideoCodec(video_format, &target_codec); + + // Vie send codec size should not exceed target_codec. + int target_width = new_width; + int target_height = new_height; + if (!is_screencast && + (new_width > target_codec.width || new_height > target_codec.height)) { + target_width = target_codec.width; + target_height = target_codec.height; + } + + // Get current vie codec. + webrtc::VideoCodec vie_codec; + const int channel_id = send_channel->channel_id(); + if (engine()->vie()->codec()->GetSendCodec(channel_id, vie_codec) != 0) { + LOG_RTCERR1(GetSendCodec, channel_id); + return false; + } + const int cur_width = vie_codec.width; + const int cur_height = vie_codec.height; + + // Only reset send codec when there is a size change. Additionally, + // automatic resize needs to be turned off when screencasting and on when + // not screencasting. + // Don't allow automatic resizing for screencasting. + bool automatic_resize = !is_screencast; + // Turn off VP8 frame dropping when screensharing as the current model does + // not work well at low fps. + bool vp8_frame_dropping = !is_screencast; + // Disable denoising for screencasting. + bool enable_denoising = + options_.video_noise_reduction.GetWithDefaultIfUnset(false); + bool denoising = !is_screencast && enable_denoising; + bool reset_send_codec = + target_width != cur_width || target_height != cur_height || + automatic_resize != vie_codec.codecSpecific.VP8.automaticResizeOn || + denoising != vie_codec.codecSpecific.VP8.denoisingOn || + vp8_frame_dropping != vie_codec.codecSpecific.VP8.frameDroppingOn; + + if (reset_send_codec) { + // Set the new codec on vie. + vie_codec.width = target_width; + vie_codec.height = target_height; + vie_codec.maxFramerate = target_codec.maxFramerate; + vie_codec.startBitrate = target_codec.startBitrate; + vie_codec.codecSpecific.VP8.automaticResizeOn = automatic_resize; + vie_codec.codecSpecific.VP8.denoisingOn = denoising; + vie_codec.codecSpecific.VP8.frameDroppingOn = vp8_frame_dropping; + // TODO(mflodman): Remove 'is_screencast' check when screen cast settings + // are treated correctly in WebRTC. + if (!is_screencast) + MaybeChangeStartBitrate(channel_id, &vie_codec); + + if (engine()->vie()->codec()->SetSendCodec(channel_id, vie_codec) != 0) { + LOG_RTCERR1(SetSendCodec, channel_id); + return false; + } + if (reset) { + *reset = true; + } + LogSendCodecChange("Capture size changed"); + } + + return true; +} + +void WebRtcVideoMediaChannel::MaybeChangeStartBitrate( + int channel_id, webrtc::VideoCodec* video_codec) { + if (video_codec->startBitrate < video_codec->minBitrate) { + video_codec->startBitrate = video_codec->minBitrate; + } else if (video_codec->startBitrate > video_codec->maxBitrate) { + video_codec->startBitrate = video_codec->maxBitrate; + } + + // Use a previous target bitrate, if there is one. + unsigned int current_target_bitrate = 0; + if (engine()->vie()->codec()->GetCodecTargetBitrate( + channel_id, ¤t_target_bitrate) == 0) { + // Convert to kbps. + current_target_bitrate /= 1000; + if (current_target_bitrate > video_codec->maxBitrate) { + current_target_bitrate = video_codec->maxBitrate; + } + if (current_target_bitrate > video_codec->startBitrate) { + video_codec->startBitrate = current_target_bitrate; + } + } +} + +void WebRtcVideoMediaChannel::OnMessage(talk_base::Message* msg) { + FlushBlackFrameData* black_frame_data = + static_cast (msg->pdata); + FlushBlackFrame(black_frame_data->ssrc, black_frame_data->timestamp); + delete black_frame_data; +} + +int WebRtcVideoMediaChannel::SendPacket(int channel, const void* data, + int len) { + if (!network_interface_) { + return -1; + } + talk_base::Buffer packet(data, len, kMaxRtpPacketLen); + return network_interface_->SendPacket(&packet) ? len : -1; +} + +int WebRtcVideoMediaChannel::SendRTCPPacket(int channel, + const void* data, + int len) { + if (!network_interface_) { + return -1; + } + talk_base::Buffer packet(data, len, kMaxRtpPacketLen); + return network_interface_->SendRtcp(&packet) ? len : -1; +} + +void WebRtcVideoMediaChannel::QueueBlackFrame(uint32 ssrc, int64 timestamp, + int framerate) { + if (timestamp) { + FlushBlackFrameData* black_frame_data = new FlushBlackFrameData( + ssrc, + timestamp); + const int delay_ms = static_cast ( + 2 * cricket::VideoFormat::FpsToInterval(framerate) * + talk_base::kNumMillisecsPerSec / talk_base::kNumNanosecsPerSec); + worker_thread()->PostDelayed(delay_ms, this, 0, black_frame_data); + } +} + +void WebRtcVideoMediaChannel::FlushBlackFrame(uint32 ssrc, int64 timestamp) { + WebRtcVideoChannelSendInfo* send_channel = GetSendChannel(ssrc); + if (!send_channel) { + return; + } + talk_base::scoped_ptr black_frame_ptr; + + const WebRtcLocalStreamInfo* channel_stream_info = + send_channel->local_stream_info(); + int64 last_frame_time_stamp = channel_stream_info->time_stamp(); + if (last_frame_time_stamp == timestamp) { + size_t last_frame_width = 0; + size_t last_frame_height = 0; + int64 last_frame_elapsed_time = 0; + channel_stream_info->GetLastFrameInfo(&last_frame_width, &last_frame_height, + &last_frame_elapsed_time); + if (!last_frame_width || !last_frame_height) { + return; + } + WebRtcVideoFrame black_frame; + // Black frame is not screencast. + const bool screencasting = false; + const int64 timestamp_delta = send_channel->interval(); + if (!black_frame.InitToBlack(send_codec_->width, send_codec_->height, 1, 1, + last_frame_elapsed_time + timestamp_delta, + last_frame_time_stamp + timestamp_delta) || + !SendFrame(send_channel, &black_frame, screencasting)) { + LOG(LS_ERROR) << "Failed to send black frame."; + } + } +} + +void WebRtcVideoMediaChannel::SetNetworkTransmissionState( + bool is_transmitting) { + LOG(LS_INFO) << "SetNetworkTransmissionState: " << is_transmitting; + for (SendChannelMap::iterator iter = send_channels_.begin(); + iter != send_channels_.end(); ++iter) { + WebRtcVideoChannelSendInfo* send_channel = iter->second; + int channel_id = send_channel->channel_id(); + engine_->vie()->network()->SetNetworkTransmissionState(channel_id, + is_transmitting); + } +} + +bool WebRtcVideoMediaChannel::SetHeaderExtension(ExtensionSetterFunction setter, + int channel_id, const RtpHeaderExtension* extension) { + bool enable = false; + int id = 0; + if (extension) { + enable = true; + id = extension->id; + } + if ((engine_->vie()->rtp()->*setter)(channel_id, enable, id) != 0) { + LOG_RTCERR4(*setter, extension->uri, channel_id, enable, id); + return false; + } + return true; +} + +bool WebRtcVideoMediaChannel::SetHeaderExtension(ExtensionSetterFunction setter, + int channel_id, const std::vector& extensions, + const char header_extension_uri[]) { + const RtpHeaderExtension* extension = FindHeaderExtension(extensions, + header_extension_uri); + return SetHeaderExtension(setter, channel_id, extension); +} +} // namespace cricket + +#endif // HAVE_WEBRTC_VIDEO diff --git a/talk/media/webrtc/webrtcvideoengine.h b/talk/media/webrtc/webrtcvideoengine.h new file mode 100644 index 000000000..f2dc18c78 --- /dev/null +++ b/talk/media/webrtc/webrtcvideoengine.h @@ -0,0 +1,456 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_WEBRTCVIDEOENGINE_H_ +#define TALK_MEDIA_WEBRTCVIDEOENGINE_H_ + +#include +#include + +#include "talk/base/scoped_ptr.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/videocommon.h" +#include "talk/media/webrtc/webrtccommon.h" +#include "talk/media/webrtc/webrtcexport.h" +#include "talk/media/webrtc/webrtcvideoencoderfactory.h" +#include "talk/session/media/channel.h" +#include "webrtc/video_engine/include/vie_base.h" + +#if !defined(LIBPEERCONNECTION_LIB) && \ + !defined(LIBPEERCONNECTION_IMPLEMENTATION) +#error "Bogus include." +#endif + +namespace webrtc { +class VideoCaptureModule; +class VideoDecoder; +class VideoEncoder; +class VideoRender; +class ViEExternalCapture; +class ViERTP_RTCP; +} + +namespace talk_base { +class CpuMonitor; +} // namespace talk_base + +namespace cricket { + +class VideoCapturer; +class VideoFrame; +class VideoProcessor; +class VideoRenderer; +class ViETraceWrapper; +class ViEWrapper; +class VoiceMediaChannel; +class WebRtcDecoderObserver; +class WebRtcEncoderObserver; +class WebRtcLocalStreamInfo; +class WebRtcRenderAdapter; +class WebRtcVideoChannelRecvInfo; +class WebRtcVideoChannelSendInfo; +class WebRtcVideoDecoderFactory; +class WebRtcVideoEncoderFactory; +class WebRtcVideoMediaChannel; +class WebRtcVoiceEngine; + +struct CapturedFrame; +struct Device; + +class WebRtcVideoEngine : public sigslot::has_slots<>, + public webrtc::TraceCallback, + public WebRtcVideoEncoderFactory::Observer { + public: + // Creates the WebRtcVideoEngine with internal VideoCaptureModule. + WebRtcVideoEngine(); + // For testing purposes. Allows the WebRtcVoiceEngine, + // ViEWrapper and CpuMonitor to be mocks. + // TODO(juberti): Remove the 3-arg ctor once fake tracing is implemented. + WebRtcVideoEngine(WebRtcVoiceEngine* voice_engine, + ViEWrapper* vie_wrapper, + talk_base::CpuMonitor* cpu_monitor); + WebRtcVideoEngine(WebRtcVoiceEngine* voice_engine, + ViEWrapper* vie_wrapper, + ViETraceWrapper* tracing, + talk_base::CpuMonitor* cpu_monitor); + ~WebRtcVideoEngine(); + + // Basic video engine implementation. + bool Init(talk_base::Thread* worker_thread); + void Terminate(); + + int GetCapabilities(); + bool SetOptions(int options); + bool SetDefaultEncoderConfig(const VideoEncoderConfig& config); + + WebRtcVideoMediaChannel* CreateChannel(VoiceMediaChannel* voice_channel); + + const std::vector& codecs() const; + const std::vector& rtp_header_extensions() const; + void SetLogging(int min_sev, const char* filter); + + // If capturer is NULL, unregisters the capturer and stops capturing. + // Otherwise sets the capturer and starts capturing. + bool SetVideoCapturer(VideoCapturer* capturer); + VideoCapturer* GetVideoCapturer() const; + bool SetLocalRenderer(VideoRenderer* renderer); + bool SetCapture(bool capture); + sigslot::repeater2 SignalCaptureStateChange; + CaptureState UpdateCapturingState(); + bool IsCapturing() const; + void OnFrameCaptured(VideoCapturer* capturer, const CapturedFrame* frame); + + // Set the VoiceEngine for A/V sync. This can only be called before Init. + bool SetVoiceEngine(WebRtcVoiceEngine* voice_engine); + // Set a WebRtcVideoDecoderFactory for external decoding. Video engine does + // not take the ownership of |decoder_factory|. The caller needs to make sure + // that |decoder_factory| outlives the video engine. + void SetExternalDecoderFactory(WebRtcVideoDecoderFactory* decoder_factory); + // Set a WebRtcVideoEncoderFactory for external encoding. Video engine does + // not take the ownership of |encoder_factory|. The caller needs to make sure + // that |encoder_factory| outlives the video engine. + void SetExternalEncoderFactory(WebRtcVideoEncoderFactory* encoder_factory); + // Enable the render module with timing control. + bool EnableTimedRender(); + + bool RegisterProcessor(VideoProcessor* video_processor); + bool UnregisterProcessor(VideoProcessor* video_processor); + + // Returns an external decoder for the given codec type. The return value + // can be NULL if decoder factory is not given or it does not support the + // codec type. The caller takes the ownership of the returned object. + webrtc::VideoDecoder* CreateExternalDecoder(webrtc::VideoCodecType type); + // Releases the decoder instance created by CreateExternalDecoder(). + void DestroyExternalDecoder(webrtc::VideoDecoder* decoder); + + // Returns an external encoder for the given codec type. The return value + // can be NULL if encoder factory is not given or it does not support the + // codec type. The caller takes the ownership of the returned object. + webrtc::VideoEncoder* CreateExternalEncoder(webrtc::VideoCodecType type); + // Releases the encoder instance created by CreateExternalEncoder(). + void DestroyExternalEncoder(webrtc::VideoEncoder* encoder); + + // Returns true if the codec type is supported by the external encoder. + bool IsExternalEncoderCodecType(webrtc::VideoCodecType type) const; + + // Functions called by WebRtcVideoMediaChannel. + talk_base::Thread* worker_thread() { return worker_thread_; } + ViEWrapper* vie() { return vie_wrapper_.get(); } + const VideoFormat& default_codec_format() const { + return default_codec_format_; + } + int GetLastEngineError(); + bool FindCodec(const VideoCodec& in); + bool CanSendCodec(const VideoCodec& in, const VideoCodec& current, + VideoCodec* out); + void RegisterChannel(WebRtcVideoMediaChannel* channel); + void UnregisterChannel(WebRtcVideoMediaChannel* channel); + bool ConvertFromCricketVideoCodec(const VideoCodec& in_codec, + webrtc::VideoCodec* out_codec); + // Check whether the supplied trace should be ignored. + bool ShouldIgnoreTrace(const std::string& trace); + int GetNumOfChannels(); + + void IncrementFrameListeners(); + void DecrementFrameListeners(); + + VideoFormat GetStartCaptureFormat() const { return default_codec_format_; } + + talk_base::CpuMonitor* cpu_monitor() { return cpu_monitor_.get(); } + + protected: + // When a video processor registers with the engine. + // SignalMediaFrame will be invoked for every video frame. + // See videoprocessor.h for param reference. + sigslot::signal3 SignalMediaFrame; + + private: + typedef std::vector VideoChannels; + struct VideoCodecPref { + const char* name; + int payload_type; + int pref; + }; + + static const VideoCodecPref kVideoCodecPrefs[]; + static const VideoFormatPod kVideoFormats[]; + static const VideoFormatPod kDefaultVideoFormat; + + void Construct(ViEWrapper* vie_wrapper, + ViETraceWrapper* tracing, + WebRtcVoiceEngine* voice_engine, + talk_base::CpuMonitor* cpu_monitor); + bool SetDefaultCodec(const VideoCodec& codec); + bool RebuildCodecList(const VideoCodec& max_codec); + void SetTraceFilter(int filter); + void SetTraceOptions(const std::string& options); + bool InitVideoEngine(); + bool SetCapturer(VideoCapturer* capturer); + + // webrtc::TraceCallback implementation. + virtual void Print(webrtc::TraceLevel level, const char* trace, int length); + void ClearCapturer(); + + // WebRtcVideoEncoderFactory::Observer implementation. + virtual void OnCodecsAvailable(); + + talk_base::Thread* worker_thread_; + talk_base::scoped_ptr vie_wrapper_; + bool vie_wrapper_base_initialized_; + talk_base::scoped_ptr tracing_; + WebRtcVoiceEngine* voice_engine_; + talk_base::scoped_ptr render_module_; + WebRtcVideoEncoderFactory* encoder_factory_; + WebRtcVideoDecoderFactory* decoder_factory_; + std::vector video_codecs_; + std::vector rtp_header_extensions_; + VideoFormat default_codec_format_; + + bool initialized_; + talk_base::CriticalSection channels_crit_; + VideoChannels channels_; + + VideoCapturer* video_capturer_; + int frame_listeners_; + bool capture_started_; + int local_renderer_w_; + int local_renderer_h_; + VideoRenderer* local_renderer_; + + // Critical section to protect the media processor register/unregister + // while processing a frame + talk_base::CriticalSection signal_media_critical_; + + talk_base::scoped_ptr cpu_monitor_; +}; + +class WebRtcVideoMediaChannel : public talk_base::MessageHandler, + public VideoMediaChannel, + public webrtc::Transport { + public: + WebRtcVideoMediaChannel(WebRtcVideoEngine* engine, + VoiceMediaChannel* voice_channel); + ~WebRtcVideoMediaChannel(); + bool Init(); + + WebRtcVideoEngine* engine() { return engine_; } + VoiceMediaChannel* voice_channel() { return voice_channel_; } + int video_channel() const { return vie_channel_; } + bool sending() const { return sending_; } + + // VideoMediaChannel implementation + virtual bool SetRecvCodecs(const std::vector &codecs); + virtual bool SetSendCodecs(const std::vector &codecs); + virtual bool GetSendCodec(VideoCodec* send_codec); + virtual bool SetSendStreamFormat(uint32 ssrc, const VideoFormat& format); + virtual bool SetRender(bool render); + virtual bool SetSend(bool send); + + virtual bool AddSendStream(const StreamParams& sp); + virtual bool RemoveSendStream(uint32 ssrc); + virtual bool AddRecvStream(const StreamParams& sp); + virtual bool RemoveRecvStream(uint32 ssrc); + virtual bool SetRenderer(uint32 ssrc, VideoRenderer* renderer); + virtual bool GetStats(VideoMediaInfo* info); + virtual bool SetCapturer(uint32 ssrc, VideoCapturer* capturer); + virtual bool SendIntraFrame(); + virtual bool RequestIntraFrame(); + + virtual void OnPacketReceived(talk_base::Buffer* packet); + virtual void OnRtcpReceived(talk_base::Buffer* packet); + virtual void OnReadyToSend(bool ready); + virtual bool MuteStream(uint32 ssrc, bool on); + virtual bool SetRecvRtpHeaderExtensions( + const std::vector& extensions); + virtual bool SetSendRtpHeaderExtensions( + const std::vector& extensions); + virtual bool SetSendBandwidth(bool autobw, int bps); + virtual bool SetOptions(const VideoOptions &options); + virtual bool GetOptions(VideoOptions *options) const { + *options = options_; + return true; + } + virtual void SetInterface(NetworkInterface* iface); + virtual void UpdateAspectRatio(int ratio_w, int ratio_h); + + // Public functions for use by tests and other specialized code. + uint32 send_ssrc() const { return 0; } + bool GetRenderer(uint32 ssrc, VideoRenderer** renderer); + void SendFrame(VideoCapturer* capturer, const VideoFrame* frame); + bool SendFrame(WebRtcVideoChannelSendInfo* channel_info, + const VideoFrame* frame, bool is_screencast); + + void AdaptAndSendFrame(VideoCapturer* capturer, const VideoFrame* frame); + + // Thunk functions for use with HybridVideoEngine + void OnLocalFrame(VideoCapturer* capturer, const VideoFrame* frame) { + SendFrame(0u, frame, capturer->IsScreencast()); + } + void OnLocalFrameFormat(VideoCapturer* capturer, const VideoFormat* format) { + } + + virtual void OnMessage(talk_base::Message* msg); + + protected: + int GetLastEngineError() { return engine()->GetLastEngineError(); } + virtual int SendPacket(int channel, const void* data, int len); + virtual int SendRTCPPacket(int channel, const void* data, int len); + + private: + typedef std::map RecvChannelMap; + typedef std::map SendChannelMap; + typedef int (webrtc::ViERTP_RTCP::* ExtensionSetterFunction)(int, bool, int); + + enum MediaDirection { MD_RECV, MD_SEND, MD_SENDRECV }; + + // Creates and initializes a ViE channel. When successful |channel_id| will + // contain the new channel's ID. If |receiving| is true |ssrc| is the + // remote ssrc. If |sending| is true the ssrc is local ssrc. If both + // |receiving| and |sending| is true the ssrc must be 0 and the channel will + // be created as a default channel. The ssrc must be different for receive + // channels and it must be different for send channels. If the same SSRC is + // being used for creating channel more than once, this function will fail + // returning false. + bool CreateChannel(uint32 ssrc_key, MediaDirection direction, + int* channel_id); + bool ConfigureChannel(int channel_id, MediaDirection direction, + uint32 ssrc_key); + bool ConfigureReceiving(int channel_id, uint32 remote_ssrc_key); + bool ConfigureSending(int channel_id, uint32 local_ssrc_key); + bool SetNackFec(int channel_id, int red_payload_type, int fec_payload_type, + bool nack_enabled); + bool SetSendCodec(const webrtc::VideoCodec& codec, int min_bitrate, + int start_bitrate, int max_bitrate); + bool SetSendCodec(WebRtcVideoChannelSendInfo* send_channel, + const webrtc::VideoCodec& codec, int min_bitrate, + int start_bitrate, int max_bitrate); + void LogSendCodecChange(const std::string& reason); + // Prepares the channel with channel id |info->channel_id()| to receive all + // codecs in |receive_codecs_| and start receive packets. + bool SetReceiveCodecs(WebRtcVideoChannelRecvInfo* info); + // Returns the channel number that receives the stream with SSRC |ssrc|. + int GetRecvChannelNum(uint32 ssrc); + // Given captured video frame size, checks if we need to reset vie send codec. + // |reset| is set to whether resetting has happened on vie or not. + // Returns false on error. + bool MaybeResetVieSendCodec(WebRtcVideoChannelSendInfo* send_channel, + int new_width, int new_height, bool is_screencast, + bool* reset); + // Checks the current bitrate estimate and modifies the start bitrate + // accordingly. + void MaybeChangeStartBitrate(int channel_id, webrtc::VideoCodec* video_codec); + // Helper function for starting the sending of media on all channels or + // |channel_id|. Note that these two function do not change |sending_|. + bool StartSend(); + bool StartSend(WebRtcVideoChannelSendInfo* send_channel); + // Helper function for stop the sending of media on all channels or + // |channel_id|. Note that these two function do not change |sending_|. + bool StopSend(); + bool StopSend(WebRtcVideoChannelSendInfo* send_channel); + bool SendIntraFrame(int channel_id); + + // Send with one local SSRC. Normal case. + bool IsOneSsrcStream(const StreamParams& sp); + + bool HasReadySendChannels(); + + // Send channel key returns the key corresponding to the provided local SSRC + // in |key|. The return value is true upon success. + // If the local ssrc correspond to that of the default channel the key is 0. + // For all other channels the returned key will be the same as the local ssrc. + bool GetSendChannelKey(uint32 local_ssrc, uint32* key); + WebRtcVideoChannelSendInfo* GetSendChannel(VideoCapturer* video_capturer); + WebRtcVideoChannelSendInfo* GetSendChannel(uint32 local_ssrc); + // Creates a new unique key that can be used for inserting a new send channel + // into |send_channels_| + bool CreateSendChannelKey(uint32 local_ssrc, uint32* key); + + bool IsDefaultChannel(int channel_id) const { + return channel_id == vie_channel_; + } + uint32 GetDefaultChannelSsrc(); + + bool DeleteSendChannel(uint32 ssrc_key); + + bool InConferenceMode() const { + return options_.conference_mode.GetWithDefaultIfUnset(false); + } + bool RemoveCapturer(uint32 ssrc); + + + talk_base::MessageQueue* worker_thread() { return engine_->worker_thread(); } + void QueueBlackFrame(uint32 ssrc, int64 timestamp, int framerate); + void FlushBlackFrame(uint32 ssrc, int64 timestamp); + + void SetNetworkTransmissionState(bool is_transmitting); + + bool SetHeaderExtension(ExtensionSetterFunction setter, int channel_id, + const RtpHeaderExtension* extension); + bool SetHeaderExtension(ExtensionSetterFunction setter, int channel_id, + const std::vector& extensions, + const char header_extension_uri[]); + + // Global state. + WebRtcVideoEngine* engine_; + VoiceMediaChannel* voice_channel_; + int vie_channel_; + bool nack_enabled_; + // Receiver Estimated Max Bitrate + bool remb_enabled_; + VideoOptions options_; + + // Global recv side state. + // Note the default channel (vie_channel_), i.e. the send channel + // corresponding to all the receive channels (this must be done for REMB to + // work properly), resides in both recv_channels_ and send_channels_ with the + // ssrc key 0. + RecvChannelMap recv_channels_; // Contains all receive channels. + std::vector receive_codecs_; + bool render_started_; + uint32 first_receive_ssrc_; + std::vector receive_extensions_; + + // Global send side state. + SendChannelMap send_channels_; + talk_base::scoped_ptr send_codec_; + int send_red_type_; + int send_fec_type_; + int send_min_bitrate_; + int send_start_bitrate_; + int send_max_bitrate_; + bool sending_; + std::vector send_extensions_; + + // The aspect ratio that the channel desires. 0 means there is no desired + // aspect ratio + int ratio_w_; + int ratio_h_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_WEBRTCVIDEOENGINE_H_ diff --git a/talk/media/webrtc/webrtcvideoengine_unittest.cc b/talk/media/webrtc/webrtcvideoengine_unittest.cc new file mode 100644 index 000000000..37b212f9a --- /dev/null +++ b/talk/media/webrtc/webrtcvideoengine_unittest.cc @@ -0,0 +1,1882 @@ +/* + * libjingle + * Copyright 2004 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/base/fakecpumonitor.h" +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stream.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/fakemediaprocessor.h" +#include "talk/media/base/fakenetworkinterface.h" +#include "talk/media/base/fakevideorenderer.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/testutils.h" +#include "talk/media/base/videoengine_unittest.h" +#include "talk/media/webrtc/fakewebrtcvideocapturemodule.h" +#include "talk/media/webrtc/fakewebrtcvideoengine.h" +#include "talk/media/webrtc/fakewebrtcvoiceengine.h" +#include "talk/media/webrtc/webrtcvideocapturer.h" +#include "talk/media/webrtc/webrtcvideoengine.h" +#include "talk/media/webrtc/webrtcvideoframe.h" +#include "talk/media/webrtc/webrtcvoiceengine.h" +#include "talk/session/media/mediasession.h" +#include "webrtc/system_wrappers/interface/trace.h" + +// Tests for the WebRtcVideoEngine/VideoChannel code. + +static const cricket::VideoCodec kVP8Codec720p(100, "VP8", 1280, 720, 30, 0); +static const cricket::VideoCodec kVP8Codec360p(100, "VP8", 640, 360, 30, 0); +static const cricket::VideoCodec kVP8Codec270p(100, "VP8", 480, 270, 30, 0); +static const cricket::VideoCodec kVP8Codec180p(100, "VP8", 320, 180, 30, 0); + +static const cricket::VideoCodec kVP8Codec(100, "VP8", 640, 400, 30, 0); +static const cricket::VideoCodec kRedCodec(101, "red", 0, 0, 0, 0); +static const cricket::VideoCodec kUlpFecCodec(102, "ulpfec", 0, 0, 0, 0); +static const cricket::VideoCodec* const kVideoCodecs[] = { + &kVP8Codec, + &kRedCodec, + &kUlpFecCodec +}; + +static const unsigned int kMinBandwidthKbps = 50; +static const unsigned int kStartBandwidthKbps = 300; +static const unsigned int kMaxBandwidthKbps = 2000; + +static const unsigned int kNumberOfTemporalLayers = 1; + + +class FakeViEWrapper : public cricket::ViEWrapper { + public: + explicit FakeViEWrapper(cricket::FakeWebRtcVideoEngine* engine) + : cricket::ViEWrapper(engine, // base + engine, // codec + engine, // capture + engine, // network + engine, // render + engine, // rtp + engine, // image + engine) { // external decoder + } +}; + +// Test fixture to test WebRtcVideoEngine with a fake webrtc::VideoEngine. +// Useful for testing failure paths. +class WebRtcVideoEngineTestFake : public testing::Test { + public: + WebRtcVideoEngineTestFake() + : vie_(kVideoCodecs, ARRAY_SIZE(kVideoCodecs)), + cpu_monitor_(new talk_base::FakeCpuMonitor( + talk_base::Thread::Current())), + engine_(NULL, // cricket::WebRtcVoiceEngine + new FakeViEWrapper(&vie_), cpu_monitor_), + channel_(NULL), + voice_channel_(NULL) { + } + bool SetupEngine() { + bool result = engine_.Init(talk_base::Thread::Current()); + if (result) { + channel_ = engine_.CreateChannel(voice_channel_); + result = (channel_ != NULL); + } + return result; + } + bool SendI420Frame(int width, int height) { + if (NULL == channel_) { + return false; + } + cricket::WebRtcVideoFrame frame; + size_t size = width * height * 3 / 2; // I420 + talk_base::scoped_array pixel(new uint8[size]); + if (!frame.Init(cricket::FOURCC_I420, + width, height, width, height, + pixel.get(), size, 1, 1, 0, 0, 0)) { + return false; + } + cricket::FakeVideoCapturer capturer; + channel_->SendFrame(&capturer, &frame); + return true; + } + bool SendI420ScreencastFrame(int width, int height) { + return SendI420ScreencastFrameWithTimestamp(width, height, 0); + } + bool SendI420ScreencastFrameWithTimestamp( + int width, int height, int64 timestamp) { + if (NULL == channel_) { + return false; + } + cricket::WebRtcVideoFrame frame; + size_t size = width * height * 3 / 2; // I420 + talk_base::scoped_array pixel(new uint8[size]); + if (!frame.Init(cricket::FOURCC_I420, + width, height, width, height, + pixel.get(), size, 1, 1, 0, timestamp, 0)) { + return false; + } + cricket::FakeVideoCapturer capturer; + capturer.SetScreencast(true); + channel_->SendFrame(&capturer, &frame); + return true; + } + void VerifyVP8SendCodec(int channel_num, + unsigned int width, + unsigned int height, + unsigned int layers = 0, + unsigned int max_bitrate = kMaxBandwidthKbps, + unsigned int min_bitrate = kMinBandwidthKbps, + unsigned int start_bitrate = kStartBandwidthKbps, + unsigned int fps = 30, + unsigned int max_quantization = 0 + ) { + webrtc::VideoCodec gcodec; + EXPECT_EQ(0, vie_.GetSendCodec(channel_num, gcodec)); + + // Video codec properties. + EXPECT_EQ(webrtc::kVideoCodecVP8, gcodec.codecType); + EXPECT_STREQ("VP8", gcodec.plName); + EXPECT_EQ(100, gcodec.plType); + EXPECT_EQ(width, gcodec.width); + EXPECT_EQ(height, gcodec.height); + EXPECT_EQ(talk_base::_min(start_bitrate, max_bitrate), gcodec.startBitrate); + EXPECT_EQ(max_bitrate, gcodec.maxBitrate); + EXPECT_EQ(min_bitrate, gcodec.minBitrate); + EXPECT_EQ(fps, gcodec.maxFramerate); + // VP8 specific. + EXPECT_FALSE(gcodec.codecSpecific.VP8.pictureLossIndicationOn); + EXPECT_FALSE(gcodec.codecSpecific.VP8.feedbackModeOn); + EXPECT_EQ(webrtc::kComplexityNormal, gcodec.codecSpecific.VP8.complexity); + EXPECT_EQ(webrtc::kResilienceOff, gcodec.codecSpecific.VP8.resilience); + EXPECT_EQ(max_quantization, gcodec.qpMax); + } + virtual void TearDown() { + delete channel_; + engine_.Terminate(); + } + + protected: + cricket::FakeWebRtcVideoEngine vie_; + cricket::FakeWebRtcVideoDecoderFactory decoder_factory_; + cricket::FakeWebRtcVideoEncoderFactory encoder_factory_; + talk_base::FakeCpuMonitor* cpu_monitor_; + cricket::WebRtcVideoEngine engine_; + cricket::WebRtcVideoMediaChannel* channel_; + cricket::WebRtcVoiceMediaChannel* voice_channel_; +}; + +// Test fixtures to test WebRtcVideoEngine with a real webrtc::VideoEngine. +class WebRtcVideoEngineTest + : public VideoEngineTest { + protected: + typedef VideoEngineTest Base; +}; +class WebRtcVideoMediaChannelTest + : public VideoMediaChannelTest< + cricket::WebRtcVideoEngine, cricket::WebRtcVideoMediaChannel> { + protected: + typedef VideoMediaChannelTest Base; + virtual cricket::VideoCodec DefaultCodec() { return kVP8Codec; } + virtual void SetUp() { + Base::SetUp(); + // Need to start the capturer to allow us to pump in frames. + engine_.SetCapture(true); + } + virtual void TearDown() { + engine_.SetCapture(false); + Base::TearDown(); + } +}; + +///////////////////////// +// Tests with fake ViE // +///////////////////////// + +// Tests that our stub library "works". +TEST_F(WebRtcVideoEngineTestFake, StartupShutdown) { + EXPECT_FALSE(vie_.IsInited()); + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + EXPECT_TRUE(vie_.IsInited()); + engine_.Terminate(); +} + +// Tests that webrtc logs are logged when they should be. +TEST_F(WebRtcVideoEngineTest, WebRtcShouldLog) { + const char webrtc_log[] = "WebRtcVideoEngineTest.WebRtcShouldLog"; + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + engine_.SetLogging(talk_base::LS_INFO, ""); + std::string str; + talk_base::StringStream stream(str); + talk_base::LogMessage::AddLogToStream(&stream, talk_base::LS_INFO); + EXPECT_EQ(talk_base::LS_INFO, talk_base::LogMessage::GetLogToStream(&stream)); + webrtc::Trace::Add(webrtc::kTraceStateInfo, webrtc::kTraceUndefined, 0, + webrtc_log); + EXPECT_TRUE_WAIT(std::string::npos != str.find(webrtc_log), 10); + talk_base::LogMessage::RemoveLogToStream(&stream); +} + +// Tests that webrtc logs are not logged when they should't be. +TEST_F(WebRtcVideoEngineTest, WebRtcShouldNotLog) { + const char webrtc_log[] = "WebRtcVideoEngineTest.WebRtcShouldNotLog"; + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + // WebRTC should never be logged lower than LS_INFO. + engine_.SetLogging(talk_base::LS_WARNING, ""); + std::string str; + talk_base::StringStream stream(str); + // Make sure that WebRTC is not logged, even at lowest severity + talk_base::LogMessage::AddLogToStream(&stream, talk_base::LS_SENSITIVE); + EXPECT_EQ(talk_base::LS_SENSITIVE, + talk_base::LogMessage::GetLogToStream(&stream)); + webrtc::Trace::Add(webrtc::kTraceStateInfo, webrtc::kTraceUndefined, 0, + webrtc_log); + talk_base::Thread::Current()->ProcessMessages(10); + EXPECT_EQ(std::string::npos, str.find(webrtc_log)); + talk_base::LogMessage::RemoveLogToStream(&stream); +} + +// Tests that we can create and destroy a channel. +TEST_F(WebRtcVideoEngineTestFake, CreateChannel) { + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + channel_ = engine_.CreateChannel(voice_channel_); + EXPECT_TRUE(channel_ != NULL); + EXPECT_EQ(1, engine_.GetNumOfChannels()); + delete channel_; + channel_ = NULL; + EXPECT_EQ(0, engine_.GetNumOfChannels()); +} + +// Tests that we properly handle failures in CreateChannel. +TEST_F(WebRtcVideoEngineTestFake, CreateChannelFail) { + vie_.set_fail_create_channel(true); + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + channel_ = engine_.CreateChannel(voice_channel_); + EXPECT_TRUE(channel_ == NULL); +} + +// Tests that we properly handle failures in AllocateExternalCaptureDevice. +TEST_F(WebRtcVideoEngineTestFake, AllocateExternalCaptureDeviceFail) { + vie_.set_fail_alloc_capturer(true); + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + channel_ = engine_.CreateChannel(voice_channel_); + EXPECT_TRUE(channel_ == NULL); +} + +// Test that we apply our default codecs properly. +TEST_F(WebRtcVideoEngineTestFake, SetSendCodecs) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + std::vector codecs(engine_.codecs()); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + VerifyVP8SendCodec(channel_num, kVP8Codec.width, kVP8Codec.height); + EXPECT_TRUE(vie_.GetHybridNackFecStatus(channel_num)); + EXPECT_FALSE(vie_.GetNackStatus(channel_num)); + // TODO(juberti): Check RTCP, PLI, TMMBR. +} + +TEST_F(WebRtcVideoEngineTestFake, SetSendCodecsWithMinMaxBitrate) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + std::vector codecs(engine_.codecs()); + codecs[0].params[cricket::kCodecParamMinBitrate] = "10"; + codecs[0].params[cricket::kCodecParamMaxBitrate] = "20"; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + VerifyVP8SendCodec( + channel_num, kVP8Codec.width, kVP8Codec.height, 0, 20, 10, 20); + + cricket::VideoCodec codec; + EXPECT_TRUE(channel_->GetSendCodec(&codec)); + EXPECT_EQ("10", codec.params[cricket::kCodecParamMinBitrate]); + EXPECT_EQ("20", codec.params[cricket::kCodecParamMaxBitrate]); +} + +TEST_F(WebRtcVideoEngineTestFake, SetSendCodecsWithMinMaxBitrateInvalid) { + EXPECT_TRUE(SetupEngine()); + std::vector codecs(engine_.codecs()); + codecs[0].params[cricket::kCodecParamMinBitrate] = "30"; + codecs[0].params[cricket::kCodecParamMaxBitrate] = "20"; + EXPECT_FALSE(channel_->SetSendCodecs(codecs)); +} + +TEST_F(WebRtcVideoEngineTestFake, SetSendCodecsWithLargeMinMaxBitrate) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + std::vector codecs(engine_.codecs()); + codecs[0].params[cricket::kCodecParamMinBitrate] = "1000"; + codecs[0].params[cricket::kCodecParamMaxBitrate] = "2000"; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + VerifyVP8SendCodec( + channel_num, kVP8Codec.width, kVP8Codec.height, 0, 2000, 1000, + 1000); +} + +TEST_F(WebRtcVideoEngineTestFake, SetSendCodecsWithMaxQuantization) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + std::vector codecs(engine_.codecs()); + codecs[0].params[cricket::kCodecParamMaxQuantization] = "21"; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + VerifyVP8SendCodec( + channel_num, kVP8Codec.width, kVP8Codec.height, 0, 2000, 50, 300, + 30, 21); + + cricket::VideoCodec codec; + EXPECT_TRUE(channel_->GetSendCodec(&codec)); + EXPECT_EQ("21", codec.params[cricket::kCodecParamMaxQuantization]); +} + +TEST_F(WebRtcVideoEngineTestFake, SetOptionsWithMaxBitrate) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + std::vector codecs(engine_.codecs()); + codecs[0].params[cricket::kCodecParamMinBitrate] = "10"; + codecs[0].params[cricket::kCodecParamMaxBitrate] = "20"; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + VerifyVP8SendCodec( + channel_num, kVP8Codec.width, kVP8Codec.height, 0, 20, 10, 20); + + // Verify that max bitrate doesn't change after SetOptions(). + cricket::VideoOptions options; + options.video_noise_reduction.Set(true); + EXPECT_TRUE(channel_->SetOptions(options)); + VerifyVP8SendCodec( + channel_num, kVP8Codec.width, kVP8Codec.height, 0, 20, 10, 20); + + options.video_noise_reduction.Set(false); + options.conference_mode.Set(false); + EXPECT_TRUE(channel_->SetOptions(options)); + VerifyVP8SendCodec( + channel_num, kVP8Codec.width, kVP8Codec.height, 0, 20, 10, 20); +} + +TEST_F(WebRtcVideoEngineTestFake, MaxBitrateResetWithConferenceMode) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + std::vector codecs(engine_.codecs()); + codecs[0].params[cricket::kCodecParamMinBitrate] = "10"; + codecs[0].params[cricket::kCodecParamMaxBitrate] = "20"; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + VerifyVP8SendCodec( + channel_num, kVP8Codec.width, kVP8Codec.height, 0, 20, 10, 20); + + cricket::VideoOptions options; + options.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(options)); + options.conference_mode.Set(false); + EXPECT_TRUE(channel_->SetOptions(options)); + VerifyVP8SendCodec( + channel_num, kVP8Codec.width, kVP8Codec.height, 0, + kMaxBandwidthKbps, 10, 20); +} + +// Verify the current send bitrate is used as start bitrate when reconfiguring +// the send codec. +TEST_F(WebRtcVideoEngineTestFake, StartSendBitrate) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(1))); + int send_channel = vie_.GetLastChannel(); + cricket::VideoCodec codec(kVP8Codec); + std::vector codec_list; + codec_list.push_back(codec); + EXPECT_TRUE(channel_->SetSendCodecs(codec_list)); + const unsigned int kVideoMaxSendBitrateKbps = 2000; + const unsigned int kVideoMinSendBitrateKbps = 50; + const unsigned int kVideoDefaultStartSendBitrateKbps = 300; + VerifyVP8SendCodec(send_channel, kVP8Codec.width, kVP8Codec.height, 0, + kVideoMaxSendBitrateKbps, kVideoMinSendBitrateKbps, + kVideoDefaultStartSendBitrateKbps); + EXPECT_EQ(0, vie_.StartSend(send_channel)); + + // Increase the send bitrate and verify it is used as start bitrate. + const unsigned int kVideoSendBitrateBps = 768000; + vie_.SetSendBitrates(send_channel, kVideoSendBitrateBps, 0, 0); + EXPECT_TRUE(channel_->SetSendCodecs(codec_list)); + VerifyVP8SendCodec(send_channel, kVP8Codec.width, kVP8Codec.height, 0, + kVideoMaxSendBitrateKbps, kVideoMinSendBitrateKbps, + kVideoSendBitrateBps / 1000); + + // Never set a start bitrate higher than the max bitrate. + vie_.SetSendBitrates(send_channel, kVideoMaxSendBitrateKbps + 500, 0, 0); + EXPECT_TRUE(channel_->SetSendCodecs(codec_list)); + VerifyVP8SendCodec(send_channel, kVP8Codec.width, kVP8Codec.height, 0, + kVideoMaxSendBitrateKbps, kVideoMinSendBitrateKbps, + kVideoDefaultStartSendBitrateKbps); + + // Use the default start bitrate if the send bitrate is lower. + vie_.SetSendBitrates(send_channel, kVideoDefaultStartSendBitrateKbps - 50, 0, + 0); + EXPECT_TRUE(channel_->SetSendCodecs(codec_list)); + VerifyVP8SendCodec(send_channel, kVP8Codec.width, kVP8Codec.height, 0, + kVideoMaxSendBitrateKbps, kVideoMinSendBitrateKbps, + kVideoDefaultStartSendBitrateKbps); +} + + +// Test that we constrain send codecs properly. +TEST_F(WebRtcVideoEngineTestFake, ConstrainSendCodecs) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + // Set max settings of 640x400x30. + EXPECT_TRUE(engine_.SetDefaultEncoderConfig( + cricket::VideoEncoderConfig(kVP8Codec))); + + // Send codec format bigger than max setting. + cricket::VideoCodec codec(kVP8Codec); + codec.width = 1280; + codec.height = 800; + codec.framerate = 60; + std::vector codec_list; + codec_list.push_back(codec); + + // Set send codec and verify codec has been constrained. + EXPECT_TRUE(channel_->SetSendCodecs(codec_list)); + VerifyVP8SendCodec(channel_num, kVP8Codec.width, kVP8Codec.height); +} + +// Test that SetSendCodecs rejects bad format. +TEST_F(WebRtcVideoEngineTestFake, SetSendCodecsRejectBadFormat) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + // Set w = 0. + cricket::VideoCodec codec(kVP8Codec); + codec.width = 0; + std::vector codec_list; + codec_list.push_back(codec); + + // Verify SetSendCodecs failed and send codec is not changed on engine. + EXPECT_FALSE(channel_->SetSendCodecs(codec_list)); + webrtc::VideoCodec gcodec; + // Set plType to something other than the value to test against ensuring + // that failure will happen if it is not changed. + gcodec.plType = 1; + EXPECT_EQ(0, vie_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(0, gcodec.plType); + + // Set h = 0. + codec_list[0].width = 640; + codec_list[0].height = 0; + + // Verify SetSendCodecs failed and send codec is not changed on engine. + EXPECT_FALSE(channel_->SetSendCodecs(codec_list)); + // Set plType to something other than the value to test against ensuring + // that failure will happen if it is not changed. + gcodec.plType = 1; + EXPECT_EQ(0, vie_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(0, gcodec.plType); +} + +// Test that SetSendCodecs rejects bad codec. +TEST_F(WebRtcVideoEngineTestFake, SetSendCodecsRejectBadCodec) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + // Set bad codec name. + cricket::VideoCodec codec(kVP8Codec); + codec.name = "bad"; + std::vector codec_list; + codec_list.push_back(codec); + + // Verify SetSendCodecs failed and send codec is not changed on engine. + EXPECT_FALSE(channel_->SetSendCodecs(codec_list)); + webrtc::VideoCodec gcodec; + // Set plType to something other than the value to test against ensuring + // that failure will happen if it is not changed. + gcodec.plType = 1; + EXPECT_EQ(0, vie_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(0, gcodec.plType); +} + +// Test that vie send codec is reset on new video frame size. +TEST_F(WebRtcVideoEngineTestFake, ResetVieSendCodecOnNewFrameSize) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + // Set send codec. + std::vector codec_list; + codec_list.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetSendCodecs(codec_list)); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(123))); + EXPECT_TRUE(channel_->SetSend(true)); + + // Capture a smaller frame and verify vie send codec has been reset to + // the new size. + SendI420Frame(kVP8Codec.width / 2, kVP8Codec.height / 2); + VerifyVP8SendCodec(channel_num, kVP8Codec.width / 2, kVP8Codec.height / 2); + + // Capture a frame bigger than send_codec_ and verify vie send codec has been + // reset (and clipped) to send_codec_. + SendI420Frame(kVP8Codec.width * 2, kVP8Codec.height * 2); + VerifyVP8SendCodec(channel_num, kVP8Codec.width, kVP8Codec.height); +} + +// Test that we set our inbound codecs properly. +TEST_F(WebRtcVideoEngineTestFake, SetRecvCodecs) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + + webrtc::VideoCodec wcodec; + EXPECT_TRUE(engine_.ConvertFromCricketVideoCodec(kVP8Codec, &wcodec)); + EXPECT_TRUE(vie_.ReceiveCodecRegistered(channel_num, wcodec)); +} + +// Test that channel connects and disconnects external capturer correctly. +TEST_F(WebRtcVideoEngineTestFake, HasExternalCapturer) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + EXPECT_EQ(1, vie_.GetNumCapturers()); + int capture_id = vie_.GetCaptureId(channel_num); + EXPECT_EQ(channel_num, vie_.GetCaptureChannelId(capture_id)); + + // Delete the channel should disconnect the capturer. + delete channel_; + channel_ = NULL; + EXPECT_EQ(0, vie_.GetNumCapturers()); +} + +// Test that channel adds and removes renderer correctly. +TEST_F(WebRtcVideoEngineTestFake, HasRenderer) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + EXPECT_TRUE(vie_.GetHasRenderer(channel_num)); + EXPECT_FALSE(vie_.GetRenderStarted(channel_num)); +} + +// Test that rtcp is enabled on the channel. +TEST_F(WebRtcVideoEngineTestFake, RtcpEnabled) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + EXPECT_EQ(webrtc::kRtcpCompound_RFC4585, vie_.GetRtcpStatus(channel_num)); +} + +// Test that key frame request method is set on the channel. +TEST_F(WebRtcVideoEngineTestFake, KeyFrameRequestEnabled) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + EXPECT_EQ(webrtc::kViEKeyFrameRequestPliRtcp, + vie_.GetKeyFrameRequestMethod(channel_num)); +} + +// Test that remb receive and send is enabled for the default channel in a 1:1 +// call. +TEST_F(WebRtcVideoEngineTestFake, RembEnabled) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(1))); + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + EXPECT_TRUE(vie_.GetRembStatusBwPartition(channel_num)); + EXPECT_TRUE(channel_->SetSend(true)); + EXPECT_TRUE(vie_.GetRembStatusBwPartition(channel_num)); + EXPECT_TRUE(vie_.GetRembStatusContribute(channel_num)); +} + +// When in conference mode, test that remb is enabled on a receive channel but +// not for the default channel and that it uses the default channel for sending +// remb packets. +TEST_F(WebRtcVideoEngineTestFake, RembEnabledOnReceiveChannels) { + EXPECT_TRUE(SetupEngine()); + int default_channel = vie_.GetLastChannel(); + cricket::VideoOptions options; + options.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(options)); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(1))); + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + EXPECT_TRUE(vie_.GetRembStatusBwPartition(default_channel)); + EXPECT_TRUE(vie_.GetRembStatusContribute(default_channel)); + EXPECT_TRUE(channel_->SetSend(true)); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(1))); + int new_channel_num = vie_.GetLastChannel(); + EXPECT_NE(default_channel, new_channel_num); + + EXPECT_TRUE(vie_.GetRembStatusBwPartition(default_channel)); + EXPECT_TRUE(vie_.GetRembStatusContribute(default_channel)); + EXPECT_FALSE(vie_.GetRembStatusBwPartition(new_channel_num)); + EXPECT_TRUE(vie_.GetRembStatusContribute(new_channel_num)); +} + +// Test support for RTP timestamp offset header extension. +TEST_F(WebRtcVideoEngineTestFake, RtpTimestampOffsetHeaderExtensions) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + cricket::VideoOptions options; + options.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(options)); + + // Verify extensions are off by default. + EXPECT_EQ(0, vie_.GetSendRtpTimestampOffsetExtensionId(channel_num)); + EXPECT_EQ(0, vie_.GetReceiveRtpTimestampOffsetExtensionId(channel_num)); + + // Enable RTP timestamp extension. + const int id = 14; + std::vector extensions; + extensions.push_back(cricket::RtpHeaderExtension( + "urn:ietf:params:rtp-hdrext:toffset", id)); + + // Verify the send extension id. + EXPECT_TRUE(channel_->SetSendRtpHeaderExtensions(extensions)); + EXPECT_EQ(id, vie_.GetSendRtpTimestampOffsetExtensionId(channel_num)); + + // Remove the extension id. + std::vector empty_extensions; + EXPECT_TRUE(channel_->SetSendRtpHeaderExtensions(empty_extensions)); + EXPECT_EQ(0, vie_.GetSendRtpTimestampOffsetExtensionId(channel_num)); + + // Verify receive extension id. + EXPECT_TRUE(channel_->SetRecvRtpHeaderExtensions(extensions)); + EXPECT_EQ(id, vie_.GetReceiveRtpTimestampOffsetExtensionId(channel_num)); + + // Add a new receive stream and verify the extension is set. + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + int new_channel_num = vie_.GetLastChannel(); + EXPECT_NE(channel_num, new_channel_num); + EXPECT_EQ(id, vie_.GetReceiveRtpTimestampOffsetExtensionId(new_channel_num)); + + // Remove the extension id. + EXPECT_TRUE(channel_->SetRecvRtpHeaderExtensions(empty_extensions)); + EXPECT_EQ(0, vie_.GetReceiveRtpTimestampOffsetExtensionId(channel_num)); + EXPECT_EQ(0, vie_.GetReceiveRtpTimestampOffsetExtensionId(new_channel_num)); +} + +// Test support for absolute send time header extension. +TEST_F(WebRtcVideoEngineTestFake, AbsoluteSendTimeHeaderExtensions) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + cricket::VideoOptions options; + options.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(options)); + + // Verify extensions are off by default. + EXPECT_EQ(0, vie_.GetSendAbsoluteSendTimeExtensionId(channel_num)); + EXPECT_EQ(0, vie_.GetReceiveAbsoluteSendTimeExtensionId(channel_num)); + + // Enable RTP timestamp extension. + const int id = 12; + std::vector extensions; + extensions.push_back(cricket::RtpHeaderExtension( + "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", id)); + + // Verify the send extension id. + EXPECT_TRUE(channel_->SetSendRtpHeaderExtensions(extensions)); + EXPECT_EQ(id, vie_.GetSendAbsoluteSendTimeExtensionId(channel_num)); + + // Remove the extension id. + std::vector empty_extensions; + EXPECT_TRUE(channel_->SetSendRtpHeaderExtensions(empty_extensions)); + EXPECT_EQ(0, vie_.GetSendAbsoluteSendTimeExtensionId(channel_num)); + + // Verify receive extension id. + EXPECT_TRUE(channel_->SetRecvRtpHeaderExtensions(extensions)); + EXPECT_EQ(id, vie_.GetReceiveAbsoluteSendTimeExtensionId(channel_num)); + + // Add a new receive stream and verify the extension is set. + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + int new_channel_num = vie_.GetLastChannel(); + EXPECT_NE(channel_num, new_channel_num); + EXPECT_EQ(id, vie_.GetReceiveAbsoluteSendTimeExtensionId(new_channel_num)); + + // Remove the extension id. + EXPECT_TRUE(channel_->SetRecvRtpHeaderExtensions(empty_extensions)); + EXPECT_EQ(0, vie_.GetReceiveAbsoluteSendTimeExtensionId(channel_num)); + EXPECT_EQ(0, vie_.GetReceiveAbsoluteSendTimeExtensionId(new_channel_num)); +} + +TEST_F(WebRtcVideoEngineTestFake, LeakyBucketTest) { + EXPECT_TRUE(SetupEngine()); + + // Verify this is off by default. + EXPECT_TRUE(channel_->AddSendStream(cricket::StreamParams::CreateLegacy(1))); + int first_send_channel = vie_.GetLastChannel(); + EXPECT_FALSE(vie_.GetTransmissionSmoothingStatus(first_send_channel)); + + // Enable the experiment and verify. + cricket::VideoOptions options; + options.conference_mode.Set(true); + options.video_leaky_bucket.Set(true); + EXPECT_TRUE(channel_->SetOptions(options)); + EXPECT_TRUE(vie_.GetTransmissionSmoothingStatus(first_send_channel)); + + // Add a receive channel and verify leaky bucket isn't enabled. + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + int recv_channel_num = vie_.GetLastChannel(); + EXPECT_NE(first_send_channel, recv_channel_num); + EXPECT_FALSE(vie_.GetTransmissionSmoothingStatus(recv_channel_num)); + + // Add a new send stream and verify leaky bucket is enabled from start. + EXPECT_TRUE(channel_->AddSendStream(cricket::StreamParams::CreateLegacy(3))); + int second_send_channel = vie_.GetLastChannel(); + EXPECT_NE(first_send_channel, second_send_channel); + EXPECT_TRUE(vie_.GetTransmissionSmoothingStatus(second_send_channel)); +} + +TEST_F(WebRtcVideoEngineTestFake, BufferedModeLatency) { + EXPECT_TRUE(SetupEngine()); + + // Verify this is off by default. + EXPECT_TRUE(channel_->AddSendStream(cricket::StreamParams::CreateLegacy(1))); + int first_send_channel = vie_.GetLastChannel(); + EXPECT_EQ(0, vie_.GetSenderTargetDelay(first_send_channel)); + EXPECT_EQ(0, vie_.GetReceiverTargetDelay(first_send_channel)); + + // Enable the experiment and verify. The default channel will have both + // sender and receiver buffered mode enabled. + cricket::VideoOptions options; + options.conference_mode.Set(true); + options.buffered_mode_latency.Set(100); + EXPECT_TRUE(channel_->SetOptions(options)); + EXPECT_EQ(100, vie_.GetSenderTargetDelay(first_send_channel)); + EXPECT_EQ(100, vie_.GetReceiverTargetDelay(first_send_channel)); + + // Add a receive channel and verify sender buffered mode isn't enabled. + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + int recv_channel_num = vie_.GetLastChannel(); + EXPECT_NE(first_send_channel, recv_channel_num); + EXPECT_EQ(0, vie_.GetSenderTargetDelay(recv_channel_num)); + EXPECT_EQ(100, vie_.GetReceiverTargetDelay(recv_channel_num)); + + // Add a new send stream and verify sender buffered mode is enabled. + EXPECT_TRUE(channel_->AddSendStream(cricket::StreamParams::CreateLegacy(3))); + int second_send_channel = vie_.GetLastChannel(); + EXPECT_NE(first_send_channel, second_send_channel); + EXPECT_EQ(100, vie_.GetSenderTargetDelay(second_send_channel)); + EXPECT_EQ(0, vie_.GetReceiverTargetDelay(second_send_channel)); + + // Disable sender buffered mode and verify. + options.buffered_mode_latency.Set(cricket::kBufferedModeDisabled); + EXPECT_TRUE(channel_->SetOptions(options)); + EXPECT_EQ(0, vie_.GetSenderTargetDelay(first_send_channel)); + EXPECT_EQ(0, vie_.GetReceiverTargetDelay(first_send_channel)); + EXPECT_EQ(0, vie_.GetSenderTargetDelay(second_send_channel)); + EXPECT_EQ(0, vie_.GetReceiverTargetDelay(second_send_channel)); + EXPECT_EQ(0, vie_.GetSenderTargetDelay(recv_channel_num)); + EXPECT_EQ(0, vie_.GetReceiverTargetDelay(recv_channel_num)); +} + +TEST_F(WebRtcVideoEngineTestFake, AdditiveVideoOptions) { + EXPECT_TRUE(SetupEngine()); + + EXPECT_TRUE(channel_->AddSendStream(cricket::StreamParams::CreateLegacy(1))); + int first_send_channel = vie_.GetLastChannel(); + EXPECT_EQ(0, vie_.GetSenderTargetDelay(first_send_channel)); + EXPECT_EQ(0, vie_.GetReceiverTargetDelay(first_send_channel)); + + cricket::VideoOptions options1; + options1.buffered_mode_latency.Set(100); + EXPECT_TRUE(channel_->SetOptions(options1)); + EXPECT_EQ(100, vie_.GetSenderTargetDelay(first_send_channel)); + EXPECT_EQ(100, vie_.GetReceiverTargetDelay(first_send_channel)); + EXPECT_FALSE(vie_.GetTransmissionSmoothingStatus(first_send_channel)); + + cricket::VideoOptions options2; + options2.video_leaky_bucket.Set(true); + EXPECT_TRUE(channel_->SetOptions(options2)); + EXPECT_TRUE(vie_.GetTransmissionSmoothingStatus(first_send_channel)); + // The buffered_mode_latency still takes effect. + EXPECT_EQ(100, vie_.GetSenderTargetDelay(first_send_channel)); + EXPECT_EQ(100, vie_.GetReceiverTargetDelay(first_send_channel)); + + options1.buffered_mode_latency.Set(50); + EXPECT_TRUE(channel_->SetOptions(options1)); + EXPECT_EQ(50, vie_.GetSenderTargetDelay(first_send_channel)); + EXPECT_EQ(50, vie_.GetReceiverTargetDelay(first_send_channel)); + // The video_leaky_bucket still takes effect. + EXPECT_TRUE(vie_.GetTransmissionSmoothingStatus(first_send_channel)); +} + +// Test that AddRecvStream doesn't create new channel for 1:1 call. +TEST_F(WebRtcVideoEngineTestFake, AddRecvStream1On1) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(1))); + EXPECT_EQ(channel_num, vie_.GetLastChannel()); +} + +// Test that AddRecvStream doesn't change remb for 1:1 call. +TEST_F(WebRtcVideoEngineTestFake, NoRembChangeAfterAddRecvStream) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(1))); + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + EXPECT_TRUE(vie_.GetRembStatusBwPartition(channel_num)); + EXPECT_TRUE(vie_.GetRembStatusContribute(channel_num)); + EXPECT_TRUE(channel_->SetSend(true)); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(1))); + EXPECT_TRUE(vie_.GetRembStatusBwPartition(channel_num)); + EXPECT_TRUE(vie_.GetRembStatusContribute(channel_num)); +} + +// Verify default REMB setting and that it can be turned on and off. +TEST_F(WebRtcVideoEngineTestFake, RembOnOff) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + // Verify REMB sending is always off by default. + EXPECT_FALSE(vie_.GetRembStatusBwPartition(channel_num)); + + // Verify that REMB is turned on when setting default codecs since the + // default codecs have REMB enabled. + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + EXPECT_TRUE(vie_.GetRembStatusBwPartition(channel_num)); + + // Verify that REMB is turned off when codecs without REMB are set. + std::vector codecs = engine_.codecs(); + // Clearing the codecs' FeedbackParams and setting send codecs should disable + // REMB. + for (std::vector::iterator iter = codecs.begin(); + iter != codecs.end(); ++iter) { + // Intersecting with empty will clear the FeedbackParams. + cricket::FeedbackParams empty_params; + iter->feedback_params.Intersect(empty_params); + EXPECT_TRUE(iter->feedback_params.params().empty()); + } + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_FALSE(vie_.GetRembStatusBwPartition(channel_num)); +} + +// Test that nack is enabled on the channel if we don't offer red/fec. +TEST_F(WebRtcVideoEngineTestFake, NackEnabled) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + std::vector codecs(engine_.codecs()); + codecs.resize(1); // toss out red and ulpfec + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(vie_.GetNackStatus(channel_num)); +} + +// Test that we enable hybrid NACK FEC mode. +TEST_F(WebRtcVideoEngineTestFake, HybridNackFec) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + EXPECT_TRUE(channel_->SetRecvCodecs(engine_.codecs())); + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + EXPECT_TRUE(vie_.GetHybridNackFecStatus(channel_num)); + EXPECT_FALSE(vie_.GetNackStatus(channel_num)); +} + +// Test that we enable hybrid NACK FEC mode when calling SetSendCodecs and +// SetReceiveCodecs in reversed order. +TEST_F(WebRtcVideoEngineTestFake, HybridNackFecReversedOrder) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + EXPECT_TRUE(channel_->SetRecvCodecs(engine_.codecs())); + EXPECT_TRUE(vie_.GetHybridNackFecStatus(channel_num)); + EXPECT_FALSE(vie_.GetNackStatus(channel_num)); +} + +// Test NACK vs Hybrid NACK/FEC interop call setup, i.e. only use NACK even if +// red/fec is offered as receive codec. +TEST_F(WebRtcVideoEngineTestFake, VideoProtectionInterop) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + std::vector recv_codecs(engine_.codecs()); + std::vector send_codecs(engine_.codecs()); + // Only add VP8 as send codec. + send_codecs.resize(1); + EXPECT_TRUE(channel_->SetRecvCodecs(recv_codecs)); + EXPECT_TRUE(channel_->SetSendCodecs(send_codecs)); + EXPECT_FALSE(vie_.GetHybridNackFecStatus(channel_num)); + EXPECT_TRUE(vie_.GetNackStatus(channel_num)); +} + +// Test NACK vs Hybrid NACK/FEC interop call setup, i.e. only use NACK even if +// red/fec is offered as receive codec. Call order reversed compared to +// VideoProtectionInterop. +TEST_F(WebRtcVideoEngineTestFake, VideoProtectionInteropReversed) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + std::vector recv_codecs(engine_.codecs()); + std::vector send_codecs(engine_.codecs()); + // Only add VP8 as send codec. + send_codecs.resize(1); + EXPECT_TRUE(channel_->SetSendCodecs(send_codecs)); + EXPECT_TRUE(channel_->SetRecvCodecs(recv_codecs)); + EXPECT_FALSE(vie_.GetHybridNackFecStatus(channel_num)); + EXPECT_TRUE(vie_.GetNackStatus(channel_num)); +} + +// Test that NACK, not hybrid mode, is enabled in conference mode. +TEST_F(WebRtcVideoEngineTestFake, HybridNackFecConference) { + EXPECT_TRUE(SetupEngine()); + // Setup the send channel. + int send_channel_num = vie_.GetLastChannel(); + cricket::VideoOptions options; + options.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(options)); + EXPECT_TRUE(channel_->SetRecvCodecs(engine_.codecs())); + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + EXPECT_FALSE(vie_.GetHybridNackFecStatus(send_channel_num)); + EXPECT_TRUE(vie_.GetNackStatus(send_channel_num)); + // Add a receive stream. + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(1))); + int receive_channel_num = vie_.GetLastChannel(); + EXPECT_FALSE(vie_.GetHybridNackFecStatus(receive_channel_num)); + EXPECT_TRUE(vie_.GetNackStatus(receive_channel_num)); +} + +// Test that when AddRecvStream in conference mode, a new channel is created +// for receiving. And the new channel's "original channel" is the send channel. +TEST_F(WebRtcVideoEngineTestFake, AddRemoveRecvStreamConference) { + EXPECT_TRUE(SetupEngine()); + // Setup the send channel. + int send_channel_num = vie_.GetLastChannel(); + cricket::VideoOptions options; + options.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(options)); + // Add a receive stream. + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(1))); + int receive_channel_num = vie_.GetLastChannel(); + EXPECT_EQ(send_channel_num, vie_.GetOriginalChannelId(receive_channel_num)); + EXPECT_TRUE(channel_->RemoveRecvStream(1)); + EXPECT_FALSE(vie_.IsChannel(receive_channel_num)); +} + +// Test that we can create a channel and start/stop rendering out on it. +TEST_F(WebRtcVideoEngineTestFake, SetRender) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + // Verify we can start/stop/start/stop rendering. + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_TRUE(vie_.GetRenderStarted(channel_num)); + EXPECT_TRUE(channel_->SetRender(false)); + EXPECT_FALSE(vie_.GetRenderStarted(channel_num)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_TRUE(vie_.GetRenderStarted(channel_num)); + EXPECT_TRUE(channel_->SetRender(false)); + EXPECT_FALSE(vie_.GetRenderStarted(channel_num)); +} + +// Test that we can create a channel and start/stop sending out on it. +TEST_F(WebRtcVideoEngineTestFake, SetSend) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + // Set send codecs on the channel. + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(123))); + + // Verify we can start/stop/start/stop sending. + EXPECT_TRUE(channel_->SetSend(true)); + EXPECT_TRUE(vie_.GetSend(channel_num)); + EXPECT_TRUE(channel_->SetSend(false)); + EXPECT_FALSE(vie_.GetSend(channel_num)); + EXPECT_TRUE(channel_->SetSend(true)); + EXPECT_TRUE(vie_.GetSend(channel_num)); + EXPECT_TRUE(channel_->SetSend(false)); + EXPECT_FALSE(vie_.GetSend(channel_num)); +} + +// Test that we set bandwidth properly when using full auto bandwidth mode. +TEST_F(WebRtcVideoEngineTestFake, SetBandwidthAuto) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + EXPECT_TRUE(channel_->SetSendBandwidth(true, cricket::kAutoBandwidth)); + VerifyVP8SendCodec(channel_num, kVP8Codec.width, kVP8Codec.height); +} + +// Test that we set bandwidth properly when using auto with upper bound. +TEST_F(WebRtcVideoEngineTestFake, SetBandwidthAutoCapped) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + EXPECT_TRUE(channel_->SetSendBandwidth(true, 768000)); + VerifyVP8SendCodec(channel_num, kVP8Codec.width, kVP8Codec.height, 0, 768U); +} + +// Test that we set bandwidth properly when using a fixed bandwidth. +TEST_F(WebRtcVideoEngineTestFake, SetBandwidthFixed) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + EXPECT_TRUE(channel_->SetSendBandwidth(false, 768000)); + VerifyVP8SendCodec(channel_num, kVP8Codec.width, kVP8Codec.height, 0, + 768U, 768U, 768U); +} + +// Test that SetSendBandwidth is ignored in conference mode. +TEST_F(WebRtcVideoEngineTestFake, SetBandwidthInConference) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + cricket::VideoOptions options; + options.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(options)); + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + VerifyVP8SendCodec(channel_num, kVP8Codec.width, kVP8Codec.height); + + // Set send bandwidth. + EXPECT_TRUE(channel_->SetSendBandwidth(false, 768000)); + + // Verify bitrate not changed. + webrtc::VideoCodec gcodec; + EXPECT_EQ(0, vie_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(kMinBandwidthKbps, gcodec.minBitrate); + EXPECT_EQ(kStartBandwidthKbps, gcodec.startBitrate); + EXPECT_EQ(kMaxBandwidthKbps, gcodec.maxBitrate); + EXPECT_NE(768U, gcodec.minBitrate); + EXPECT_NE(768U, gcodec.startBitrate); + EXPECT_NE(768U, gcodec.maxBitrate); +} + +// Test that sending screencast frames doesn't change bitrate. +TEST_F(WebRtcVideoEngineTestFake, SetBandwidthScreencast) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + // Set send codec. + cricket::VideoCodec codec(kVP8Codec); + std::vector codec_list; + codec_list.push_back(codec); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(123))); + EXPECT_TRUE(channel_->SetSendCodecs(codec_list)); + EXPECT_TRUE(channel_->SetSendBandwidth(false, 111000)); + EXPECT_TRUE(channel_->SetSend(true)); + + SendI420ScreencastFrame(kVP8Codec.width, kVP8Codec.height); + VerifyVP8SendCodec(channel_num, kVP8Codec.width, kVP8Codec.height, 0, + 111, 111, 111); +} + + +// Test SetSendSsrc. +TEST_F(WebRtcVideoEngineTestFake, SetSendSsrcAndCname) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + cricket::StreamParams stream; + stream.ssrcs.push_back(1234); + stream.cname = "cname"; + channel_->AddSendStream(stream); + + unsigned int ssrc = 0; + EXPECT_EQ(0, vie_.GetLocalSSRC(channel_num, ssrc)); + EXPECT_EQ(1234U, ssrc); + EXPECT_EQ(1, vie_.GetNumSsrcs(channel_num)); + + char rtcp_cname[256]; + EXPECT_EQ(0, vie_.GetRTCPCName(channel_num, rtcp_cname)); + EXPECT_STREQ("cname", rtcp_cname); +} + + +// Test that the local SSRC is the same on sending and receiving channels if the +// receive channel is created before the send channel. +TEST_F(WebRtcVideoEngineTestFake, SetSendSsrcAfterCreatingReceiveChannel) { + EXPECT_TRUE(SetupEngine()); + + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(1))); + int receive_channel_num = vie_.GetLastChannel(); + cricket::StreamParams stream = cricket::StreamParams::CreateLegacy(1234); + EXPECT_TRUE(channel_->AddSendStream(stream)); + int send_channel_num = vie_.GetLastChannel(); + unsigned int ssrc = 0; + EXPECT_EQ(0, vie_.GetLocalSSRC(send_channel_num, ssrc)); + EXPECT_EQ(1234U, ssrc); + EXPECT_EQ(1, vie_.GetNumSsrcs(send_channel_num)); + ssrc = 0; + EXPECT_EQ(0, vie_.GetLocalSSRC(receive_channel_num, ssrc)); + EXPECT_EQ(1234U, ssrc); + EXPECT_EQ(1, vie_.GetNumSsrcs(receive_channel_num)); +} + + +// Test SetOptions with denoising flag. +TEST_F(WebRtcVideoEngineTestFake, SetOptionsWithDenoising) { + EXPECT_TRUE(SetupEngine()); + EXPECT_EQ(1, vie_.GetNumCapturers()); + int channel_num = vie_.GetLastChannel(); + int capture_id = vie_.GetCaptureId(channel_num); + // Set send codecs on the channel. + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + // Set options with OPT_VIDEO_NOISE_REDUCTION flag. + cricket::VideoOptions options; + options.video_noise_reduction.Set(true); + EXPECT_TRUE(channel_->SetOptions(options)); + + // Verify capture has denoising turned on. + webrtc::VideoCodec send_codec; + memset(&send_codec, 0, sizeof(send_codec)); // avoid uninitialized warning + EXPECT_EQ(0, vie_.GetSendCodec(channel_num, send_codec)); + EXPECT_TRUE(send_codec.codecSpecific.VP8.denoisingOn); + EXPECT_FALSE(vie_.GetCaptureDenoising(capture_id)); + + // Set options back to zero. + options.video_noise_reduction.Set(false); + EXPECT_TRUE(channel_->SetOptions(options)); + + // Verify capture has denoising turned off. + EXPECT_EQ(0, vie_.GetSendCodec(channel_num, send_codec)); + EXPECT_FALSE(send_codec.codecSpecific.VP8.denoisingOn); + EXPECT_FALSE(vie_.GetCaptureDenoising(capture_id)); +} + + +TEST_F(WebRtcVideoEngineTestFake, SendReceiveBitratesStats) { + EXPECT_TRUE(SetupEngine()); + cricket::VideoOptions options; + options.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(options)); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(1))); + int send_channel = vie_.GetLastChannel(); + cricket::VideoCodec codec(kVP8Codec720p); + std::vector codec_list; + codec_list.push_back(codec); + EXPECT_TRUE(channel_->SetSendCodecs(codec_list)); + + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(2))); + int first_receive_channel = vie_.GetLastChannel(); + EXPECT_NE(send_channel, first_receive_channel); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(3))); + int second_receive_channel = vie_.GetLastChannel(); + EXPECT_NE(first_receive_channel, second_receive_channel); + + cricket::VideoMediaInfo info; + EXPECT_TRUE(channel_->GetStats(&info)); + ASSERT_EQ(1U, info.bw_estimations.size()); + ASSERT_EQ(0, info.bw_estimations[0].actual_enc_bitrate); + ASSERT_EQ(0, info.bw_estimations[0].transmit_bitrate); + ASSERT_EQ(0, info.bw_estimations[0].retransmit_bitrate); + ASSERT_EQ(0, info.bw_estimations[0].available_send_bandwidth); + ASSERT_EQ(0, info.bw_estimations[0].available_recv_bandwidth); + ASSERT_EQ(0, info.bw_estimations[0].target_enc_bitrate); + + // Start sending and receiving on one of the channels and verify bitrates. + EXPECT_EQ(0, vie_.StartSend(send_channel)); + int send_video_bitrate = 800; + int send_fec_bitrate = 100; + int send_nack_bitrate = 20; + int send_total_bitrate = send_video_bitrate + send_fec_bitrate + + send_nack_bitrate; + int send_bandwidth = 950; + vie_.SetSendBitrates(send_channel, send_video_bitrate, send_fec_bitrate, + send_nack_bitrate); + vie_.SetSendBandwidthEstimate(send_channel, send_bandwidth); + + EXPECT_EQ(0, vie_.StartReceive(first_receive_channel)); + int first_channel_receive_bandwidth = 600; + vie_.SetReceiveBandwidthEstimate(first_receive_channel, + first_channel_receive_bandwidth); + + info.Clear(); + EXPECT_TRUE(channel_->GetStats(&info)); + ASSERT_EQ(1U, info.bw_estimations.size()); + ASSERT_EQ(send_video_bitrate, info.bw_estimations[0].actual_enc_bitrate); + ASSERT_EQ(send_total_bitrate, info.bw_estimations[0].transmit_bitrate); + ASSERT_EQ(send_nack_bitrate, info.bw_estimations[0].retransmit_bitrate); + ASSERT_EQ(send_bandwidth, info.bw_estimations[0].available_send_bandwidth); + ASSERT_EQ(first_channel_receive_bandwidth, + info.bw_estimations[0].available_recv_bandwidth); + ASSERT_EQ(send_video_bitrate, info.bw_estimations[0].target_enc_bitrate); + + // Start receiving on the second channel and verify received rate. + EXPECT_EQ(0, vie_.StartReceive(second_receive_channel)); + int second_channel_receive_bandwidth = 100; + vie_.SetReceiveBandwidthEstimate(second_receive_channel, + second_channel_receive_bandwidth); + + info.Clear(); + EXPECT_TRUE(channel_->GetStats(&info)); + ASSERT_EQ(1U, info.bw_estimations.size()); + ASSERT_EQ(send_video_bitrate, info.bw_estimations[0].actual_enc_bitrate); + ASSERT_EQ(send_total_bitrate, info.bw_estimations[0].transmit_bitrate); + ASSERT_EQ(send_nack_bitrate, info.bw_estimations[0].retransmit_bitrate); + ASSERT_EQ(send_bandwidth, info.bw_estimations[0].available_send_bandwidth); + ASSERT_EQ(first_channel_receive_bandwidth + second_channel_receive_bandwidth, + info.bw_estimations[0].available_recv_bandwidth); + ASSERT_EQ(send_video_bitrate, info.bw_estimations[0].target_enc_bitrate); +} + +TEST_F(WebRtcVideoEngineTestFake, TestSetAdaptInputToCpuUsage) { + EXPECT_TRUE(SetupEngine()); + cricket::VideoOptions options_in, options_out; + bool cpu_adapt = false; + channel_->SetOptions(options_in); + EXPECT_TRUE(channel_->GetOptions(&options_out)); + EXPECT_FALSE(options_out.adapt_input_to_cpu_usage.Get(&cpu_adapt)); + // Set adapt input CPU usage option. + options_in.adapt_input_to_cpu_usage.Set(true); + EXPECT_TRUE(channel_->SetOptions(options_in)); + EXPECT_TRUE(channel_->GetOptions(&options_out)); + EXPECT_TRUE(options_out.adapt_input_to_cpu_usage.Get(&cpu_adapt)); + EXPECT_TRUE(cpu_adapt); +} + +TEST_F(WebRtcVideoEngineTestFake, TestSetCpuThreshold) { + EXPECT_TRUE(SetupEngine()); + float low, high; + cricket::VideoOptions options_in, options_out; + // Verify that initial values are set. + EXPECT_TRUE(channel_->GetOptions(&options_out)); + EXPECT_TRUE(options_out.system_low_adaptation_threshhold.Get(&low)); + EXPECT_EQ(low, 0.65f); + EXPECT_TRUE(options_out.system_high_adaptation_threshhold.Get(&high)); + EXPECT_EQ(high, 0.85f); + // Set new CPU threshold values. + options_in.system_low_adaptation_threshhold.Set(0.45f); + options_in.system_high_adaptation_threshhold.Set(0.95f); + EXPECT_TRUE(channel_->SetOptions(options_in)); + EXPECT_TRUE(channel_->GetOptions(&options_out)); + EXPECT_TRUE(options_out.system_low_adaptation_threshhold.Get(&low)); + EXPECT_EQ(low, 0.45f); + EXPECT_TRUE(options_out.system_high_adaptation_threshhold.Get(&high)); + EXPECT_EQ(high, 0.95f); +} + +TEST_F(WebRtcVideoEngineTestFake, TestSetInvalidCpuThreshold) { + EXPECT_TRUE(SetupEngine()); + float low, high; + cricket::VideoOptions options_in, options_out; + // Valid range is [0, 1]. + options_in.system_low_adaptation_threshhold.Set(-1.5f); + options_in.system_high_adaptation_threshhold.Set(1.5f); + EXPECT_TRUE(channel_->SetOptions(options_in)); + EXPECT_TRUE(channel_->GetOptions(&options_out)); + EXPECT_TRUE(options_out.system_low_adaptation_threshhold.Get(&low)); + EXPECT_EQ(low, 0.0f); + EXPECT_TRUE(options_out.system_high_adaptation_threshhold.Get(&high)); + EXPECT_EQ(high, 1.0f); +} + + +///////////////////////// +// Tests with real ViE // +///////////////////////// + +// Tests that we can find codecs by name or id. +TEST_F(WebRtcVideoEngineTest, FindCodec) { + // We should not need to init engine in order to get codecs. + const std::vector& c = engine_.codecs(); + EXPECT_EQ(3U, c.size()); + + cricket::VideoCodec vp8(104, "VP8", 320, 200, 30, 0); + EXPECT_TRUE(engine_.FindCodec(vp8)); + + cricket::VideoCodec vp8_ci(104, "vp8", 320, 200, 30, 0); + EXPECT_TRUE(engine_.FindCodec(vp8)); + + cricket::VideoCodec vp8_diff_fr_diff_pref(104, "VP8", 320, 200, 50, 50); + EXPECT_TRUE(engine_.FindCodec(vp8_diff_fr_diff_pref)); + + cricket::VideoCodec vp8_diff_id(95, "VP8", 320, 200, 30, 0); + EXPECT_FALSE(engine_.FindCodec(vp8_diff_id)); + vp8_diff_id.id = 97; + EXPECT_TRUE(engine_.FindCodec(vp8_diff_id)); + + cricket::VideoCodec vp8_diff_res(104, "VP8", 320, 111, 30, 0); + EXPECT_FALSE(engine_.FindCodec(vp8_diff_res)); + + // PeerConnection doesn't negotiate the resolution at this point. + // Test that FindCodec can handle the case when width/height is 0. + cricket::VideoCodec vp8_zero_res(104, "VP8", 0, 0, 30, 0); + EXPECT_TRUE(engine_.FindCodec(vp8_zero_res)); + + cricket::VideoCodec red(101, "RED", 0, 0, 30, 0); + EXPECT_TRUE(engine_.FindCodec(red)); + + cricket::VideoCodec red_ci(101, "red", 0, 0, 30, 0); + EXPECT_TRUE(engine_.FindCodec(red)); + + cricket::VideoCodec fec(102, "ULPFEC", 0, 0, 30, 0); + EXPECT_TRUE(engine_.FindCodec(fec)); + + cricket::VideoCodec fec_ci(102, "ulpfec", 0, 0, 30, 0); + EXPECT_TRUE(engine_.FindCodec(fec)); +} + +TEST_F(WebRtcVideoEngineTest, StartupShutdown) { + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + engine_.Terminate(); +} + +TEST_PRE_VIDEOENGINE_INIT(WebRtcVideoEngineTest, ConstrainNewCodec) +TEST_POST_VIDEOENGINE_INIT(WebRtcVideoEngineTest, ConstrainNewCodec) + +TEST_PRE_VIDEOENGINE_INIT(WebRtcVideoEngineTest, ConstrainRunningCodec) +TEST_POST_VIDEOENGINE_INIT(WebRtcVideoEngineTest, ConstrainRunningCodec) + +// TODO(juberti): Figure out why ViE is munging the COM refcount. +#ifdef WIN32 +TEST_F(WebRtcVideoEngineTest, DISABLED_CheckCoInitialize) { + Base::CheckCoInitialize(); +} +#endif + +TEST_F(WebRtcVideoEngineTest, CreateChannel) { + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + cricket::VideoMediaChannel* channel = engine_.CreateChannel(NULL); + EXPECT_TRUE(channel != NULL); + delete channel; +} + +TEST_F(WebRtcVideoMediaChannelTest, TestVideoProcessor_DropFrames) { + // Connect a video processor. + cricket::FakeMediaProcessor vp; + vp.set_drop_frames(false); + EXPECT_TRUE(engine_.RegisterProcessor(&vp)); + EXPECT_EQ(0, vp.dropped_frame_count()); + // Send the first frame with default codec. + int packets = NumRtpPackets(); + cricket::VideoCodec codec(DefaultCodec()); + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_EQ(0, renderer_.num_rendered_frames()); + EXPECT_TRUE(WaitAndSendFrame(30)); + EXPECT_FRAME_WAIT(1, codec.width, codec.height, kTimeout); + // Verify frame was sent. + EXPECT_TRUE_WAIT(NumRtpPackets() > packets, kTimeout); + packets = NumRtpPackets(); + EXPECT_EQ(0, vp.dropped_frame_count()); + // Send another frame and expect it to be sent. + EXPECT_TRUE(WaitAndSendFrame(30)); + EXPECT_FRAME_WAIT(2, codec.width, codec.height, kTimeout); + EXPECT_TRUE_WAIT(NumRtpPackets() > packets, kTimeout); + packets = NumRtpPackets(); + EXPECT_EQ(0, vp.dropped_frame_count()); + // Attempt to send a frame and expect it to be dropped. + vp.set_drop_frames(true); + EXPECT_TRUE(WaitAndSendFrame(30)); + DrainOutgoingPackets(); + EXPECT_FRAME_WAIT(2, codec.width, codec.height, kTimeout); + EXPECT_EQ(packets, NumRtpPackets()); + EXPECT_EQ(1, vp.dropped_frame_count()); + // Disconnect video processor. + EXPECT_TRUE(engine_.UnregisterProcessor(&vp)); +} +TEST_F(WebRtcVideoMediaChannelTest, SetRecvCodecs) { + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); +} +TEST_F(WebRtcVideoMediaChannelTest, SetRecvCodecsWrongPayloadType) { + std::vector codecs; + codecs.push_back(kVP8Codec); + codecs[0].id = 99; + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); +} +TEST_F(WebRtcVideoMediaChannelTest, SetRecvCodecsUnsupportedCodec) { + std::vector codecs; + codecs.push_back(kVP8Codec); + codecs.push_back(cricket::VideoCodec(101, "VP1", 640, 400, 30, 0)); + EXPECT_FALSE(channel_->SetRecvCodecs(codecs)); +} + +TEST_F(WebRtcVideoMediaChannelTest, SetSend) { + Base::SetSend(); +} +TEST_F(WebRtcVideoMediaChannelTest, SetSendWithoutCodecs) { + Base::SetSendWithoutCodecs(); +} +TEST_F(WebRtcVideoMediaChannelTest, SetSendSetsTransportBufferSizes) { + Base::SetSendSetsTransportBufferSizes(); +} + +TEST_F(WebRtcVideoMediaChannelTest, SendAndReceiveVp8Vga) { + SendAndReceive(cricket::VideoCodec(100, "VP8", 640, 400, 30, 0)); +} +TEST_F(WebRtcVideoMediaChannelTest, SendAndReceiveVp8Qvga) { + SendAndReceive(cricket::VideoCodec(100, "VP8", 320, 200, 30, 0)); +} +TEST_F(WebRtcVideoMediaChannelTest, SendAndReceiveH264SvcQqvga) { + SendAndReceive(cricket::VideoCodec(100, "VP8", 160, 100, 30, 0)); +} +TEST_F(WebRtcVideoMediaChannelTest, SendManyResizeOnce) { + SendManyResizeOnce(); +} + +TEST_F(WebRtcVideoMediaChannelTest, SendVp8HdAndReceiveAdaptedVp8Vga) { + EXPECT_TRUE(engine_.SetVideoCapturer(NULL)); + channel_->UpdateAspectRatio(1280, 720); + video_capturer_.reset(new cricket::FakeVideoCapturer); + const std::vector* formats = + video_capturer_->GetSupportedFormats(); + cricket::VideoFormat capture_format_hd = (*formats)[0]; + EXPECT_EQ(cricket::CS_RUNNING, video_capturer_->Start(capture_format_hd)); + EXPECT_TRUE(channel_->SetCapturer(kSsrc, video_capturer_.get())); + + // Capture format HD -> adapt (OnOutputFormatRequest VGA) -> VGA. + cricket::VideoCodec codec(100, "VP8", 1280, 720, 30, 0); + EXPECT_TRUE(SetOneCodec(codec)); + codec.width /= 2; + codec.height /= 2; + EXPECT_TRUE(channel_->SetSendStreamFormat(kSsrc, cricket::VideoFormat( + codec.width, codec.height, + cricket::VideoFormat::FpsToInterval(codec.framerate), + cricket::FOURCC_ANY))); + EXPECT_TRUE(SetSend(true)); + EXPECT_TRUE(channel_->SetRender(true)); + EXPECT_EQ(0, renderer_.num_rendered_frames()); + EXPECT_TRUE(SendFrame()); + EXPECT_FRAME_WAIT(1, codec.width, codec.height, kTimeout); +} + +// TODO(juberti): Fix this test to tolerate missing stats. +TEST_F(WebRtcVideoMediaChannelTest, DISABLED_GetStats) { + Base::GetStats(); +} + +// TODO(juberti): Fix this test to tolerate missing stats. +TEST_F(WebRtcVideoMediaChannelTest, DISABLED_GetStatsMultipleRecvStreams) { + Base::GetStatsMultipleRecvStreams(); +} + +TEST_F(WebRtcVideoMediaChannelTest, GetStatsMultipleSendStreams) { + Base::GetStatsMultipleSendStreams(); +} + +TEST_F(WebRtcVideoMediaChannelTest, SetSendBandwidth) { + Base::SetSendBandwidth(); +} +TEST_F(WebRtcVideoMediaChannelTest, SetSendSsrc) { + Base::SetSendSsrc(); +} +TEST_F(WebRtcVideoMediaChannelTest, SetSendSsrcAfterSetCodecs) { + Base::SetSendSsrcAfterSetCodecs(); +} + +TEST_F(WebRtcVideoMediaChannelTest, SetRenderer) { + Base::SetRenderer(); +} + +TEST_F(WebRtcVideoMediaChannelTest, AddRemoveRecvStreams) { + Base::AddRemoveRecvStreams(); +} + +TEST_F(WebRtcVideoMediaChannelTest, AddRemoveRecvStreamAndRender) { + Base::AddRemoveRecvStreamAndRender(); +} + +TEST_F(WebRtcVideoMediaChannelTest, AddRemoveRecvStreamsNoConference) { + Base::AddRemoveRecvStreamsNoConference(); +} + +TEST_F(WebRtcVideoMediaChannelTest, AddRemoveSendStreams) { + Base::AddRemoveSendStreams(); +} + +TEST_F(WebRtcVideoMediaChannelTest, SetVideoCapturer) { + // Use 123 to verify there's no assumption to the module id + FakeWebRtcVideoCaptureModule* vcm = + new FakeWebRtcVideoCaptureModule(NULL, 123); + talk_base::scoped_ptr capturer( + new cricket::WebRtcVideoCapturer); + EXPECT_TRUE(capturer->Init(vcm)); + EXPECT_TRUE(engine_.SetVideoCapturer(capturer.get())); + EXPECT_FALSE(engine_.IsCapturing()); + EXPECT_TRUE(engine_.SetCapture(true)); + cricket::VideoCodec codec(DefaultCodec()); + EXPECT_TRUE(SetOneCodec(codec)); + EXPECT_TRUE(channel_->SetSend(true)); + EXPECT_TRUE(engine_.IsCapturing()); + + EXPECT_EQ(engine_.default_codec_format().width, vcm->cap().width); + EXPECT_EQ(engine_.default_codec_format().height, vcm->cap().height); + EXPECT_EQ(cricket::VideoFormat::IntervalToFps( + engine_.default_codec_format().interval), + vcm->cap().maxFPS); + EXPECT_EQ(webrtc::kVideoI420, vcm->cap().rawType); + EXPECT_EQ(webrtc::kVideoCodecUnknown, vcm->cap().codecType); + + EXPECT_TRUE(engine_.SetVideoCapturer(NULL)); + EXPECT_FALSE(engine_.IsCapturing()); +} + +TEST_F(WebRtcVideoMediaChannelTest, SimulateConference) { + Base::SimulateConference(); +} + +TEST_F(WebRtcVideoMediaChannelTest, AddRemoveCapturer) { + Base::AddRemoveCapturer(); +} + +TEST_F(WebRtcVideoMediaChannelTest, RemoveCapturerWithoutAdd) { + Base::RemoveCapturerWithoutAdd(); +} + +TEST_F(WebRtcVideoMediaChannelTest, AddRemoveCapturerMultipleSources) { + Base::AddRemoveCapturerMultipleSources(); +} + + +TEST_F(WebRtcVideoMediaChannelTest, SetOptionsSucceedsWhenSending) { + cricket::VideoOptions options; + options.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(options)); + + // Verify SetOptions returns true on a different options. + cricket::VideoOptions options2; + options2.adapt_input_to_cpu_usage.Set(true); + EXPECT_TRUE(channel_->SetOptions(options2)); + + // Set send codecs on the channel and start sending. + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(channel_->SetSend(true)); + + // Verify SetOptions returns true if channel is already sending. + cricket::VideoOptions options3; + options3.conference_mode.Set(true); + EXPECT_TRUE(channel_->SetOptions(options3)); +} + +// Tests empty StreamParams is rejected. +TEST_F(WebRtcVideoMediaChannelTest, RejectEmptyStreamParams) { + Base::RejectEmptyStreamParams(); +} + + +TEST_F(WebRtcVideoMediaChannelTest, AdaptResolution16x10) { + Base::AdaptResolution16x10(); +} + +TEST_F(WebRtcVideoMediaChannelTest, AdaptResolution4x3) { + Base::AdaptResolution4x3(); +} + +TEST_F(WebRtcVideoMediaChannelTest, MuteStream) { + Base::MuteStream(); +} + +TEST_F(WebRtcVideoMediaChannelTest, MultipleSendStreams) { + Base::MultipleSendStreams(); +} + +// TODO(juberti): Restore this test once we support sending 0 fps. +TEST_F(WebRtcVideoMediaChannelTest, DISABLED_AdaptDropAllFrames) { + Base::AdaptDropAllFrames(); +} +// TODO(juberti): Understand why we get decode errors on this test. +TEST_F(WebRtcVideoMediaChannelTest, DISABLED_AdaptFramerate) { + Base::AdaptFramerate(); +} + +TEST_F(WebRtcVideoMediaChannelTest, SetSendStreamFormat0x0) { + Base::SetSendStreamFormat0x0(); +} + +// TODO(zhurunz): Fix the flakey test. +TEST_F(WebRtcVideoMediaChannelTest, DISABLED_SetSendStreamFormat) { + Base::SetSendStreamFormat(); +} + +TEST_F(WebRtcVideoMediaChannelTest, TwoStreamsSendAndReceive) { + Base::TwoStreamsSendAndReceive(cricket::VideoCodec(100, "VP8", 640, 400, 30, + 0)); +} + +TEST_F(WebRtcVideoMediaChannelTest, TwoStreamsReUseFirstStream) { + Base::TwoStreamsReUseFirstStream(cricket::VideoCodec(100, "VP8", 640, 400, 30, + 0)); +} + +TEST_F(WebRtcVideoEngineTestFake, ResetCodecOnScreencast) { + EXPECT_TRUE(SetupEngine()); + cricket::VideoOptions options; + options.video_noise_reduction.Set(true); + EXPECT_TRUE(channel_->SetOptions(options)); + + // Set send codec. + cricket::VideoCodec codec(kVP8Codec); + std::vector codec_list; + codec_list.push_back(codec); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(123))); + EXPECT_TRUE(channel_->SetSendCodecs(codec_list)); + EXPECT_TRUE(channel_->SetSend(true)); + EXPECT_EQ(1, vie_.num_set_send_codecs()); + + webrtc::VideoCodec gcodec; + memset(&gcodec, 0, sizeof(gcodec)); + int channel_num = vie_.GetLastChannel(); + EXPECT_EQ(0, vie_.GetSendCodec(channel_num, gcodec)); + EXPECT_TRUE(gcodec.codecSpecific.VP8.denoisingOn); + + // Send a screencast frame with the same size. + // Verify that denoising is turned off. + SendI420ScreencastFrame(kVP8Codec.width, kVP8Codec.height); + EXPECT_EQ(2, vie_.num_set_send_codecs()); + EXPECT_EQ(0, vie_.GetSendCodec(channel_num, gcodec)); + EXPECT_FALSE(gcodec.codecSpecific.VP8.denoisingOn); +} + + +TEST_F(WebRtcVideoEngineTestFake, DontRegisterDecoderIfFactoryIsNotGiven) { + engine_.SetExternalDecoderFactory(NULL); + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + + EXPECT_EQ(0, vie_.GetNumExternalDecoderRegistered(channel_num)); +} + +TEST_F(WebRtcVideoEngineTestFake, RegisterDecoderIfFactoryIsGiven) { + decoder_factory_.AddSupportedVideoCodecType(webrtc::kVideoCodecVP8); + engine_.SetExternalDecoderFactory(&decoder_factory_); + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + + EXPECT_TRUE(vie_.ExternalDecoderRegistered(channel_num, 100)); + EXPECT_EQ(1, vie_.GetNumExternalDecoderRegistered(channel_num)); +} + +TEST_F(WebRtcVideoEngineTestFake, DontRegisterDecoderMultipleTimes) { + decoder_factory_.AddSupportedVideoCodecType(webrtc::kVideoCodecVP8); + engine_.SetExternalDecoderFactory(&decoder_factory_); + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + + EXPECT_TRUE(vie_.ExternalDecoderRegistered(channel_num, 100)); + EXPECT_EQ(1, vie_.GetNumExternalDecoderRegistered(channel_num)); + EXPECT_EQ(1, decoder_factory_.GetNumCreatedDecoders()); + + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + EXPECT_EQ(1, vie_.GetNumExternalDecoderRegistered(channel_num)); + EXPECT_EQ(1, decoder_factory_.GetNumCreatedDecoders()); +} + +TEST_F(WebRtcVideoEngineTestFake, DontRegisterDecoderForNonVP8) { + decoder_factory_.AddSupportedVideoCodecType(webrtc::kVideoCodecVP8); + engine_.SetExternalDecoderFactory(&decoder_factory_); + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + std::vector codecs; + codecs.push_back(kRedCodec); + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + + EXPECT_EQ(0, vie_.GetNumExternalDecoderRegistered(channel_num)); +} + +TEST_F(WebRtcVideoEngineTestFake, DontRegisterEncoderIfFactoryIsNotGiven) { + engine_.SetExternalEncoderFactory(NULL); + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + EXPECT_EQ(0, vie_.GetNumExternalEncoderRegistered(channel_num)); +} + +TEST_F(WebRtcVideoEngineTestFake, RegisterEncoderIfFactoryIsGiven) { + encoder_factory_.AddSupportedVideoCodecType(webrtc::kVideoCodecVP8, "VP8"); + engine_.SetExternalEncoderFactory(&encoder_factory_); + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + EXPECT_TRUE(vie_.ExternalEncoderRegistered(channel_num, 100)); + EXPECT_EQ(1, vie_.GetNumExternalEncoderRegistered(channel_num)); +} + +TEST_F(WebRtcVideoEngineTestFake, DontRegisterEncoderMultipleTimes) { + encoder_factory_.AddSupportedVideoCodecType(webrtc::kVideoCodecVP8, "VP8"); + engine_.SetExternalEncoderFactory(&encoder_factory_); + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + EXPECT_TRUE(vie_.ExternalEncoderRegistered(channel_num, 100)); + EXPECT_EQ(1, vie_.GetNumExternalEncoderRegistered(channel_num)); + EXPECT_EQ(1, encoder_factory_.GetNumCreatedEncoders()); + + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_EQ(1, vie_.GetNumExternalEncoderRegistered(channel_num)); + EXPECT_EQ(1, encoder_factory_.GetNumCreatedEncoders()); +} + +TEST_F(WebRtcVideoEngineTestFake, RegisterEncoderWithMultipleSendStreams) { + encoder_factory_.AddSupportedVideoCodecType(webrtc::kVideoCodecVP8, "VP8"); + engine_.SetExternalEncoderFactory(&encoder_factory_); + EXPECT_TRUE(SetupEngine()); + + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_EQ(1, vie_.GetTotalNumExternalEncoderRegistered()); + + // When we add the first stream (1234), it reuses the default send channel, + // so it doesn't increase the registration count of external encoders. + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(1234))); + EXPECT_EQ(1, vie_.GetTotalNumExternalEncoderRegistered()); + + // When we add the second stream (2345), it creates a new channel and + // increments the registration count. + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(2345))); + EXPECT_EQ(2, vie_.GetTotalNumExternalEncoderRegistered()); + + // At this moment the total registration count is two, but only one encoder + // is registered per channel. + int channel_num = vie_.GetLastChannel(); + EXPECT_EQ(1, vie_.GetNumExternalEncoderRegistered(channel_num)); + + // Removing send streams decrements the registration count. + EXPECT_TRUE(channel_->RemoveSendStream(1234)); + EXPECT_EQ(1, vie_.GetTotalNumExternalEncoderRegistered()); + + // When we remove the last send stream, it also destroys the last send + // channel and causes the registration count to drop to zero. It is a little + // weird, but not a bug. + EXPECT_TRUE(channel_->RemoveSendStream(2345)); + EXPECT_EQ(0, vie_.GetTotalNumExternalEncoderRegistered()); +} + +TEST_F(WebRtcVideoEngineTestFake, DontRegisterEncoderForNonVP8) { + encoder_factory_.AddSupportedVideoCodecType(webrtc::kVideoCodecGeneric, + "GENERIC"); + engine_.SetExternalEncoderFactory(&encoder_factory_); + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + // Note: unlike the SetRecvCodecs, we must set a valid video codec for + // channel_->SetSendCodecs() to succeed. + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + EXPECT_EQ(0, vie_.GetNumExternalEncoderRegistered(channel_num)); +} + +// Test that NACK and REMB are enabled for external codec. +TEST_F(WebRtcVideoEngineTestFake, FeedbackParamsForNonVP8) { + encoder_factory_.AddSupportedVideoCodecType(webrtc::kVideoCodecGeneric, + "GENERIC"); + engine_.SetExternalEncoderFactory(&encoder_factory_); + encoder_factory_.NotifyCodecsAvailable(); + EXPECT_TRUE(SetupEngine()); + + std::vector codecs(engine_.codecs()); + EXPECT_EQ("GENERIC", codecs[0].name); + EXPECT_TRUE(codecs[0].HasFeedbackParam( + cricket::FeedbackParam(cricket::kRtcpFbParamNack, + cricket::kParamValueEmpty))); + EXPECT_TRUE(codecs[0].HasFeedbackParam( + cricket::FeedbackParam(cricket::kRtcpFbParamRemb, + cricket::kParamValueEmpty))); + EXPECT_TRUE(codecs[0].HasFeedbackParam( + cricket::FeedbackParam(cricket::kRtcpFbParamCcm, + cricket::kRtcpFbCcmParamFir))); +} + +TEST_F(WebRtcVideoEngineTestFake, UpdateEncoderCodecsAfterSetFactory) { + engine_.SetExternalEncoderFactory(&encoder_factory_); + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + + encoder_factory_.AddSupportedVideoCodecType(webrtc::kVideoCodecVP8, "VP8"); + encoder_factory_.NotifyCodecsAvailable(); + std::vector codecs; + codecs.push_back(kVP8Codec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + EXPECT_TRUE(vie_.ExternalEncoderRegistered(channel_num, 100)); + EXPECT_EQ(1, vie_.GetNumExternalEncoderRegistered(channel_num)); + EXPECT_EQ(1, encoder_factory_.GetNumCreatedEncoders()); +} + +// Tests that OnReadyToSend will be propagated into ViE. +TEST_F(WebRtcVideoEngineTestFake, OnReadyToSend) { + EXPECT_TRUE(SetupEngine()); + int channel_num = vie_.GetLastChannel(); + EXPECT_TRUE(vie_.GetIsTransmitting(channel_num)); + + channel_->OnReadyToSend(false); + EXPECT_FALSE(vie_.GetIsTransmitting(channel_num)); + + channel_->OnReadyToSend(true); + EXPECT_TRUE(vie_.GetIsTransmitting(channel_num)); +} + +#if 0 +TEST_F(WebRtcVideoEngineTestFake, CaptureFrameTimestampToNtpTimestamp) { + EXPECT_TRUE(SetupEngine()); + int capture_id = vie_.GetCaptureId(vie_.GetLastChannel()); + + // Set send codec. + cricket::VideoCodec codec(kVP8Codec); + std::vector codec_list; + codec_list.push_back(codec); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(123))); + EXPECT_TRUE(channel_->SetSendCodecs(codec_list)); + EXPECT_TRUE(channel_->SetSend(true)); + + int64 timestamp = time(NULL) * talk_base::kNumNanosecsPerSec; + SendI420ScreencastFrameWithTimestamp( + kVP8Codec.width, kVP8Codec.height, timestamp); + EXPECT_EQ(talk_base::UnixTimestampNanosecsToNtpMillisecs(timestamp), + vie_.GetCaptureLastTimestamp(capture_id)); + + SendI420ScreencastFrameWithTimestamp(kVP8Codec.width, kVP8Codec.height, 0); + EXPECT_EQ(0, vie_.GetCaptureLastTimestamp(capture_id)); +} +#endif diff --git a/talk/media/webrtc/webrtcvideoframe.cc b/talk/media/webrtc/webrtcvideoframe.cc new file mode 100644 index 000000000..80f24817a --- /dev/null +++ b/talk/media/webrtc/webrtcvideoframe.cc @@ -0,0 +1,355 @@ +/* + * libjingle + * Copyright 2011 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/media/webrtc/webrtcvideoframe.h" + +#include "libyuv/convert.h" +#include "libyuv/convert_from.h" +#include "libyuv/planar_functions.h" +#include "talk/base/logging.h" +#include "talk/media/base/videocapturer.h" +#include "talk/media/base/videocommon.h" + +namespace cricket { + +static const int kWatermarkWidth = 8; +static const int kWatermarkHeight = 8; +static const int kWatermarkOffsetFromLeft = 8; +static const int kWatermarkOffsetFromBottom = 8; +static const unsigned char kWatermarkMaxYValue = 64; + +FrameBuffer::FrameBuffer() : length_(0) {} + +FrameBuffer::FrameBuffer(size_t length) : length_(0) { + char* buffer = new char[length]; + SetData(buffer, length); +} + +FrameBuffer::~FrameBuffer() { + // Make sure that the video_frame_ doesn't delete the buffer as it may be + // shared between multiple WebRtcVideoFrame. + uint8_t* new_memory = NULL; + uint32_t new_length = 0; + uint32_t new_size = 0; + video_frame_.Swap(new_memory, new_length, new_size); +} + +void FrameBuffer::SetData(char* data, size_t length) { + data_.reset(data); + length_ = length; + uint8_t* new_memory = reinterpret_cast(data); + uint32_t new_length = length; + uint32_t new_size = length; + video_frame_.Swap(new_memory, new_length, new_size); +} + +void FrameBuffer::ReturnData(char** data, size_t* length) { + uint8_t* old_memory = NULL; + uint32_t old_length = 0; + uint32_t old_size = 0; + video_frame_.Swap(old_memory, old_length, old_size); + data_.release(); + length_ = 0; + *length = old_length; + *data = reinterpret_cast(old_memory); +} + +char* FrameBuffer::data() { return data_.get(); } + +size_t FrameBuffer::length() const { return length_; } + +webrtc::VideoFrame* FrameBuffer::frame() { return &video_frame_; } + +const webrtc::VideoFrame* FrameBuffer::frame() const { return &video_frame_; } + +WebRtcVideoFrame::WebRtcVideoFrame() + : video_buffer_(new RefCountedBuffer()), is_black_(false) {} + +WebRtcVideoFrame::~WebRtcVideoFrame() {} + +bool WebRtcVideoFrame::Init( + uint32 format, int w, int h, int dw, int dh, uint8* sample, + size_t sample_size, size_t pixel_width, size_t pixel_height, + int64 elapsed_time, int64 time_stamp, int rotation) { + return Reset(format, w, h, dw, dh, sample, sample_size, pixel_width, + pixel_height, elapsed_time, time_stamp, rotation); +} + +bool WebRtcVideoFrame::Init(const CapturedFrame* frame, int dw, int dh) { + return Reset(frame->fourcc, frame->width, frame->height, dw, dh, + static_cast(frame->data), frame->data_size, + frame->pixel_width, frame->pixel_height, frame->elapsed_time, + frame->time_stamp, frame->rotation); +} + +bool WebRtcVideoFrame::InitToBlack(int w, int h, size_t pixel_width, + size_t pixel_height, int64 elapsed_time, + int64 time_stamp) { + InitToEmptyBuffer(w, h, pixel_width, pixel_height, elapsed_time, time_stamp); + if (!is_black_) { + return SetToBlack(); + } + return true; +} + +void WebRtcVideoFrame::Attach( + uint8* buffer, size_t buffer_size, int w, int h, size_t pixel_width, + size_t pixel_height, int64 elapsed_time, int64 time_stamp, int rotation) { + talk_base::scoped_refptr video_buffer( + new RefCountedBuffer()); + video_buffer->SetData(reinterpret_cast(buffer), buffer_size); + Attach(video_buffer.get(), buffer_size, w, h, pixel_width, pixel_height, + elapsed_time, time_stamp, rotation); +} + +void WebRtcVideoFrame::Detach(uint8** data, size_t* length) { + video_buffer_->ReturnData(reinterpret_cast(data), length); +} + +size_t WebRtcVideoFrame::GetWidth() const { return frame()->Width(); } + +size_t WebRtcVideoFrame::GetHeight() const { return frame()->Height(); } + +const uint8* WebRtcVideoFrame::GetYPlane() const { + uint8_t* buffer = frame()->Buffer(); + return buffer; +} + +const uint8* WebRtcVideoFrame::GetUPlane() const { + uint8_t* buffer = frame()->Buffer(); + if (buffer) { + buffer += (frame()->Width() * frame()->Height()); + } + return buffer; +} + +const uint8* WebRtcVideoFrame::GetVPlane() const { + uint8_t* buffer = frame()->Buffer(); + if (buffer) { + int uv_size = GetChromaSize(); + buffer += frame()->Width() * frame()->Height() + uv_size; + } + return buffer; +} + +uint8* WebRtcVideoFrame::GetYPlane() { + uint8_t* buffer = frame()->Buffer(); + return buffer; +} + +uint8* WebRtcVideoFrame::GetUPlane() { + uint8_t* buffer = frame()->Buffer(); + if (buffer) { + buffer += (frame()->Width() * frame()->Height()); + } + return buffer; +} + +uint8* WebRtcVideoFrame::GetVPlane() { + uint8_t* buffer = frame()->Buffer(); + if (buffer) { + int uv_size = GetChromaSize(); + buffer += frame()->Width() * frame()->Height() + uv_size; + } + return buffer; +} + +VideoFrame* WebRtcVideoFrame::Copy() const { + const char* old_buffer = video_buffer_->data(); + if (!old_buffer) + return NULL; + size_t new_buffer_size = video_buffer_->length(); + + WebRtcVideoFrame* ret_val = new WebRtcVideoFrame(); + ret_val->Attach(video_buffer_.get(), new_buffer_size, frame()->Width(), + frame()->Height(), pixel_width_, pixel_height_, elapsed_time_, + time_stamp_, rotation_); + return ret_val; +} + +bool WebRtcVideoFrame::MakeExclusive() { + const int length = video_buffer_->length(); + RefCountedBuffer* exclusive_buffer = new RefCountedBuffer(length); + memcpy(exclusive_buffer->data(), video_buffer_->data(), length); + Attach(exclusive_buffer, length, frame()->Width(), frame()->Height(), + pixel_width_, pixel_height_, elapsed_time_, time_stamp_, rotation_); + return true; +} + +size_t WebRtcVideoFrame::CopyToBuffer(uint8* buffer, size_t size) const { + if (!frame()->Buffer()) { + return 0; + } + + size_t needed = frame()->Length(); + if (needed <= size) { + memcpy(buffer, frame()->Buffer(), needed); + } + return needed; +} + +// TODO(fbarchard): Refactor into base class and share with lmi +size_t WebRtcVideoFrame::ConvertToRgbBuffer(uint32 to_fourcc, uint8* buffer, + size_t size, int stride_rgb) const { + if (!frame()->Buffer()) { + return 0; + } + size_t width = frame()->Width(); + size_t height = frame()->Height(); + size_t needed = (stride_rgb >= 0 ? stride_rgb : -stride_rgb) * height; + if (size < needed) { + LOG(LS_WARNING) << "RGB buffer is not large enough"; + return needed; + } + + if (libyuv::ConvertFromI420(GetYPlane(), GetYPitch(), GetUPlane(), + GetUPitch(), GetVPlane(), GetVPitch(), buffer, + stride_rgb, width, height, to_fourcc)) { + LOG(LS_WARNING) << "RGB type not supported: " << to_fourcc; + return 0; // 0 indicates error + } + return needed; +} + +void WebRtcVideoFrame::Attach( + RefCountedBuffer* video_buffer, size_t buffer_size, int w, int h, + size_t pixel_width, size_t pixel_height, int64 elapsed_time, + int64 time_stamp, int rotation) { + if (video_buffer_.get() == video_buffer) { + return; + } + is_black_ = false; + video_buffer_ = video_buffer; + frame()->SetWidth(w); + frame()->SetHeight(h); + pixel_width_ = pixel_width; + pixel_height_ = pixel_height; + elapsed_time_ = elapsed_time; + time_stamp_ = time_stamp; + rotation_ = rotation; +} + +// Add a square watermark near the left-low corner. clamp Y. +// Returns false on error. +bool WebRtcVideoFrame::AddWatermark() { + size_t w = GetWidth(); + size_t h = GetHeight(); + + if (w < kWatermarkWidth + kWatermarkOffsetFromLeft || + h < kWatermarkHeight + kWatermarkOffsetFromBottom) { + return false; + } + + uint8* buffer = GetYPlane(); + for (size_t x = kWatermarkOffsetFromLeft; + x < kWatermarkOffsetFromLeft + kWatermarkWidth; ++x) { + for (size_t y = h - kWatermarkOffsetFromBottom - kWatermarkHeight; + y < h - kWatermarkOffsetFromBottom; ++y) { + buffer[y * w + x] = + talk_base::_min(buffer[y * w + x], kWatermarkMaxYValue); + } + } + return true; +} + +bool WebRtcVideoFrame::Reset( + uint32 format, int w, int h, int dw, int dh, uint8* sample, + size_t sample_size, size_t pixel_width, size_t pixel_height, + int64 elapsed_time, int64 time_stamp, int rotation) { + if (!Validate(format, w, h, sample, sample_size)) { + return false; + } + // Translate aliases to standard enums (e.g., IYUV -> I420). + format = CanonicalFourCC(format); + + // Round display width and height down to multiple of 4, to avoid webrtc + // size calculation error on odd sizes. + // TODO(Ronghua): Remove this once the webrtc allocator is fixed. + dw = (dw > 4) ? (dw & ~3) : dw; + dh = (dh > 4) ? (dh & ~3) : dh; + + // Set up a new buffer. + // TODO(fbarchard): Support lazy allocation. + int new_width = dw; + int new_height = dh; + if (rotation == 90 || rotation == 270) { // If rotated swap width, height. + new_width = dh; + new_height = dw; + } + + size_t desired_size = SizeOf(new_width, new_height); + talk_base::scoped_refptr video_buffer( + new RefCountedBuffer(desired_size)); + // Since the libyuv::ConvertToI420 will handle the rotation, so the + // new frame's rotation should always be 0. + Attach(video_buffer.get(), desired_size, new_width, new_height, pixel_width, + pixel_height, elapsed_time, time_stamp, 0); + + int horiz_crop = ((w - dw) / 2) & ~1; + // ARGB on Windows has negative height. + // The sample's layout in memory is normal, so just correct crop. + int vert_crop = ((abs(h) - dh) / 2) & ~1; + // Conversion functions expect negative height to flip the image. + int idh = (h < 0) ? -dh : dh; + uint8* y = GetYPlane(); + int y_stride = GetYPitch(); + uint8* u = GetUPlane(); + int u_stride = GetUPitch(); + uint8* v = GetVPlane(); + int v_stride = GetVPitch(); + int r = libyuv::ConvertToI420( + sample, sample_size, y, y_stride, u, u_stride, v, v_stride, horiz_crop, + vert_crop, w, h, dw, idh, static_cast(rotation), + format); + if (r) { + LOG(LS_ERROR) << "Error parsing format: " << GetFourccName(format) + << " return code : " << r; + return false; + } + return true; +} + +VideoFrame* WebRtcVideoFrame::CreateEmptyFrame( + int w, int h, size_t pixel_width, size_t pixel_height, int64 elapsed_time, + int64 time_stamp) const { + WebRtcVideoFrame* frame = new WebRtcVideoFrame(); + frame->InitToEmptyBuffer(w, h, pixel_width, pixel_height, elapsed_time, + time_stamp); + return frame; +} + +void WebRtcVideoFrame::InitToEmptyBuffer(int w, int h, size_t pixel_width, + size_t pixel_height, + int64 elapsed_time, int64 time_stamp) { + size_t buffer_size = VideoFrame::SizeOf(w, h); + talk_base::scoped_refptr video_buffer( + new RefCountedBuffer(buffer_size)); + Attach(video_buffer.get(), buffer_size, w, h, pixel_width, pixel_height, + elapsed_time, time_stamp, 0); +} + +} // namespace cricket diff --git a/talk/media/webrtc/webrtcvideoframe.h b/talk/media/webrtc/webrtcvideoframe.h new file mode 100644 index 000000000..03a31966e --- /dev/null +++ b/talk/media/webrtc/webrtcvideoframe.h @@ -0,0 +1,149 @@ +/* + * libjingle + * Copyright 2011 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. + */ + +#ifndef TALK_MEDIA_WEBRTCVIDEOFRAME_H_ +#define TALK_MEDIA_WEBRTCVIDEOFRAME_H_ + +#include "talk/base/buffer.h" +#include "talk/base/refcount.h" +#include "talk/base/scoped_ref_ptr.h" +#include "talk/media/base/videoframe.h" +#include "webrtc/common_types.h" +#include "webrtc/modules/interface/module_common_types.h" + +namespace cricket { + +struct CapturedFrame; + +// Class that takes ownership of the frame passed to it. +class FrameBuffer { + public: + FrameBuffer(); + explicit FrameBuffer(size_t length); + ~FrameBuffer(); + + void SetData(char* data, size_t length); + void ReturnData(char** data, size_t* length); + char* data(); + size_t length() const; + + webrtc::VideoFrame* frame(); + const webrtc::VideoFrame* frame() const; + + private: + talk_base::scoped_array data_; + size_t length_; + webrtc::VideoFrame video_frame_; +}; + +class WebRtcVideoFrame : public VideoFrame { + public: + typedef talk_base::RefCountedObject RefCountedBuffer; + + WebRtcVideoFrame(); + ~WebRtcVideoFrame(); + + // Creates a frame from a raw sample with FourCC "format" and size "w" x "h". + // "h" can be negative indicating a vertically flipped image. + // "dh" is destination height if cropping is desired and is always positive. + // Returns "true" if successful. + bool Init(uint32 format, int w, int h, int dw, int dh, uint8* sample, + size_t sample_size, size_t pixel_width, size_t pixel_height, + int64 elapsed_time, int64 time_stamp, int rotation); + + bool Init(const CapturedFrame* frame, int dw, int dh); + + bool InitToBlack(int w, int h, size_t pixel_width, size_t pixel_height, + int64 elapsed_time, int64 time_stamp); + + void Attach(uint8* buffer, size_t buffer_size, int w, int h, + size_t pixel_width, size_t pixel_height, int64 elapsed_time, + int64 time_stamp, int rotation); + + void Detach(uint8** data, size_t* length); + bool AddWatermark(); + webrtc::VideoFrame* frame() { return video_buffer_->frame(); } + webrtc::VideoFrame* frame() const { return video_buffer_->frame(); } + + // From base class VideoFrame. + virtual bool Reset(uint32 format, int w, int h, int dw, int dh, uint8* sample, + size_t sample_size, size_t pixel_width, + size_t pixel_height, int64 elapsed_time, int64 time_stamp, + int rotation); + + virtual size_t GetWidth() const; + virtual size_t GetHeight() const; + virtual const uint8* GetYPlane() const; + virtual const uint8* GetUPlane() const; + virtual const uint8* GetVPlane() const; + virtual uint8* GetYPlane(); + virtual uint8* GetUPlane(); + virtual uint8* GetVPlane(); + virtual int32 GetYPitch() const { return frame()->Width(); } + virtual int32 GetUPitch() const { return (frame()->Width() + 1) / 2; } + virtual int32 GetVPitch() const { return (frame()->Width() + 1) / 2; } + + virtual size_t GetPixelWidth() const { return pixel_width_; } + virtual size_t GetPixelHeight() const { return pixel_height_; } + virtual int64 GetElapsedTime() const { return elapsed_time_; } + virtual int64 GetTimeStamp() const { return time_stamp_; } + virtual void SetElapsedTime(int64 elapsed_time) { + elapsed_time_ = elapsed_time; + } + virtual void SetTimeStamp(int64 time_stamp) { time_stamp_ = time_stamp; } + + virtual int GetRotation() const { return rotation_; } + + virtual VideoFrame* Copy() const; + virtual bool MakeExclusive(); + virtual size_t CopyToBuffer(uint8* buffer, size_t size) const; + virtual size_t ConvertToRgbBuffer(uint32 to_fourcc, uint8* buffer, + size_t size, int stride_rgb) const; + + private: + void Attach(RefCountedBuffer* video_buffer, size_t buffer_size, int w, int h, + size_t pixel_width, size_t pixel_height, int64 elapsed_time, + int64 time_stamp, int rotation); + + virtual VideoFrame* CreateEmptyFrame(int w, int h, size_t pixel_width, + size_t pixel_height, int64 elapsed_time, + int64 time_stamp) const; + void InitToEmptyBuffer(int w, int h, size_t pixel_width, size_t pixel_height, + int64 elapsed_time, int64 time_stamp); + + talk_base::scoped_refptr video_buffer_; + bool is_black_; + size_t pixel_width_; + size_t pixel_height_; + int64 elapsed_time_; + int64 time_stamp_; + int rotation_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_WEBRTCVIDEOFRAME_H_ diff --git a/talk/media/webrtc/webrtcvideoframe_unittest.cc b/talk/media/webrtc/webrtcvideoframe_unittest.cc new file mode 100644 index 000000000..2f0decb28 --- /dev/null +++ b/talk/media/webrtc/webrtcvideoframe_unittest.cc @@ -0,0 +1,313 @@ +/* + * libjingle + * Copyright 2011 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/base/flags.h" +#include "talk/media/base/videoframe_unittest.h" +#include "talk/media/webrtc/webrtcvideoframe.h" + +extern int FLAG_yuvconverter_repeat; // From lmivideoframe_unittest.cc. + +class WebRtcVideoFrameTest : public VideoFrameTest { + public: + WebRtcVideoFrameTest() { + repeat_ = FLAG_yuvconverter_repeat; + } + + void TestInit(int cropped_width, int cropped_height) { + const int frame_width = 1920; + const int frame_height = 1080; + + // Build the CapturedFrame. + cricket::CapturedFrame captured_frame; + captured_frame.fourcc = cricket::FOURCC_I420; + captured_frame.pixel_width = 1; + captured_frame.pixel_height = 1; + captured_frame.elapsed_time = 1234; + captured_frame.time_stamp = 5678; + captured_frame.rotation = 0; + captured_frame.width = frame_width; + captured_frame.height = frame_height; + captured_frame.data_size = (frame_width * frame_height) + + ((frame_width + 1) / 2) * ((frame_height + 1) / 2) * 2; + talk_base::scoped_array captured_frame_buffer( + new uint8[captured_frame.data_size]); + captured_frame.data = captured_frame_buffer.get(); + + // Create the new frame from the CapturedFrame. + cricket::WebRtcVideoFrame frame; + EXPECT_TRUE(frame.Init(&captured_frame, cropped_width, cropped_height)); + + // Verify the new frame. + EXPECT_EQ(1u, frame.GetPixelWidth()); + EXPECT_EQ(1u, frame.GetPixelHeight()); + EXPECT_EQ(1234, frame.GetElapsedTime()); + EXPECT_EQ(5678, frame.GetTimeStamp()); + EXPECT_EQ(0, frame.GetRotation()); + // The size of the new frame should have been cropped to multiple of 4. + EXPECT_EQ(static_cast(cropped_width & ~3), frame.GetWidth()); + EXPECT_EQ(static_cast(cropped_height & ~3), frame.GetHeight()); + } +}; + +#define TEST_WEBRTCVIDEOFRAME(X) TEST_F(WebRtcVideoFrameTest, X) { \ + VideoFrameTest::X(); \ +} + +TEST_WEBRTCVIDEOFRAME(ConstructI420) +TEST_WEBRTCVIDEOFRAME(ConstructI422) +TEST_WEBRTCVIDEOFRAME(ConstructYuy2) +TEST_WEBRTCVIDEOFRAME(ConstructYuy2Unaligned) +TEST_WEBRTCVIDEOFRAME(ConstructYuy2Wide) +TEST_WEBRTCVIDEOFRAME(ConstructYV12) +TEST_WEBRTCVIDEOFRAME(ConstructUyvy) +TEST_WEBRTCVIDEOFRAME(ConstructM420) +TEST_WEBRTCVIDEOFRAME(ConstructQ420) +TEST_WEBRTCVIDEOFRAME(ConstructNV21) +TEST_WEBRTCVIDEOFRAME(ConstructNV12) +TEST_WEBRTCVIDEOFRAME(ConstructABGR) +TEST_WEBRTCVIDEOFRAME(ConstructARGB) +TEST_WEBRTCVIDEOFRAME(ConstructARGBWide) +TEST_WEBRTCVIDEOFRAME(ConstructBGRA) +TEST_WEBRTCVIDEOFRAME(Construct24BG) +TEST_WEBRTCVIDEOFRAME(ConstructRaw) +TEST_WEBRTCVIDEOFRAME(ConstructRGB565) +TEST_WEBRTCVIDEOFRAME(ConstructARGB1555) +TEST_WEBRTCVIDEOFRAME(ConstructARGB4444) + +TEST_WEBRTCVIDEOFRAME(ConstructI420Mirror) +TEST_WEBRTCVIDEOFRAME(ConstructI420Rotate0) +TEST_WEBRTCVIDEOFRAME(ConstructI420Rotate90) +TEST_WEBRTCVIDEOFRAME(ConstructI420Rotate180) +TEST_WEBRTCVIDEOFRAME(ConstructI420Rotate270) +TEST_WEBRTCVIDEOFRAME(ConstructYV12Rotate0) +TEST_WEBRTCVIDEOFRAME(ConstructYV12Rotate90) +TEST_WEBRTCVIDEOFRAME(ConstructYV12Rotate180) +TEST_WEBRTCVIDEOFRAME(ConstructYV12Rotate270) +TEST_WEBRTCVIDEOFRAME(ConstructNV12Rotate0) +TEST_WEBRTCVIDEOFRAME(ConstructNV12Rotate90) +TEST_WEBRTCVIDEOFRAME(ConstructNV12Rotate180) +TEST_WEBRTCVIDEOFRAME(ConstructNV12Rotate270) +TEST_WEBRTCVIDEOFRAME(ConstructNV21Rotate0) +TEST_WEBRTCVIDEOFRAME(ConstructNV21Rotate90) +TEST_WEBRTCVIDEOFRAME(ConstructNV21Rotate180) +TEST_WEBRTCVIDEOFRAME(ConstructNV21Rotate270) +TEST_WEBRTCVIDEOFRAME(ConstructUYVYRotate0) +TEST_WEBRTCVIDEOFRAME(ConstructUYVYRotate90) +TEST_WEBRTCVIDEOFRAME(ConstructUYVYRotate180) +TEST_WEBRTCVIDEOFRAME(ConstructUYVYRotate270) +TEST_WEBRTCVIDEOFRAME(ConstructYUY2Rotate0) +TEST_WEBRTCVIDEOFRAME(ConstructYUY2Rotate90) +TEST_WEBRTCVIDEOFRAME(ConstructYUY2Rotate180) +TEST_WEBRTCVIDEOFRAME(ConstructYUY2Rotate270) +TEST_WEBRTCVIDEOFRAME(ConstructI4201Pixel) +TEST_WEBRTCVIDEOFRAME(ConstructI4205Pixel) +// TODO(juberti): WebRtcVideoFrame does not support horizontal crop. +// Re-evaluate once it supports 3 independent planes, since we might want to +// just Init normally and then crop by adjusting pointers. +// TEST_WEBRTCVIDEOFRAME(ConstructI420CropHorizontal) +TEST_WEBRTCVIDEOFRAME(ConstructI420CropVertical) +// TODO(juberti): WebRtcVideoFrame is not currently refcounted. +// TEST_WEBRTCVIDEOFRAME(ConstructCopy) +// TEST_WEBRTCVIDEOFRAME(ConstructCopyIsRef) +TEST_WEBRTCVIDEOFRAME(ConstructBlack) +// TODO(fbarchard): Implement Jpeg +// TEST_WEBRTCVIDEOFRAME(ConstructMjpgI420) +// TEST_WEBRTCVIDEOFRAME(ConstructMjpgI422) +// TEST_WEBRTCVIDEOFRAME(ConstructMjpgI444) +// TEST_WEBRTCVIDEOFRAME(ConstructMjpgI411) +// TEST_WEBRTCVIDEOFRAME(ConstructMjpgI400) +// TEST_WEBRTCVIDEOFRAME(ValidateMjpgI420) +// TEST_WEBRTCVIDEOFRAME(ValidateMjpgI422) +// TEST_WEBRTCVIDEOFRAME(ValidateMjpgI444) +// TEST_WEBRTCVIDEOFRAME(ValidateMjpgI411) +// TEST_WEBRTCVIDEOFRAME(ValidateMjpgI400) +TEST_WEBRTCVIDEOFRAME(ValidateI420) +TEST_WEBRTCVIDEOFRAME(ValidateI420SmallSize) +TEST_WEBRTCVIDEOFRAME(ValidateI420LargeSize) +TEST_WEBRTCVIDEOFRAME(ValidateI420HugeSize) +// TEST_WEBRTCVIDEOFRAME(ValidateMjpgI420InvalidSize) +// TEST_WEBRTCVIDEOFRAME(ValidateI420InvalidSize) + +// TODO(fbarchard): WebRtcVideoFrame does not support odd sizes. +// Re-evaluate once WebRTC switches to libyuv +// TEST_WEBRTCVIDEOFRAME(ConstructYuy2AllSizes) +// TEST_WEBRTCVIDEOFRAME(ConstructARGBAllSizes) +TEST_WEBRTCVIDEOFRAME(Reset) +TEST_WEBRTCVIDEOFRAME(ConvertToABGRBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertToABGRBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToABGRBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToARGB1555Buffer) +TEST_WEBRTCVIDEOFRAME(ConvertToARGB1555BufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToARGB1555BufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToARGB4444Buffer) +TEST_WEBRTCVIDEOFRAME(ConvertToARGB4444BufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToARGB4444BufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToARGBBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertToARGBBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToARGBBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToBGRABuffer) +TEST_WEBRTCVIDEOFRAME(ConvertToBGRABufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToBGRABufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToRAWBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertToRAWBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToRAWBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToRGB24Buffer) +TEST_WEBRTCVIDEOFRAME(ConvertToRGB24BufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToRGB24BufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToRGB565Buffer) +TEST_WEBRTCVIDEOFRAME(ConvertToRGB565BufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToRGB565BufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToBayerBGGRBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertToBayerBGGRBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToBayerBGGRBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToBayerGRBGBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertToBayerGRBGBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToBayerGRBGBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToBayerGBRGBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertToBayerGBRGBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToBayerGBRGBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToBayerRGGBBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertToBayerRGGBBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToBayerRGGBBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToI400Buffer) +TEST_WEBRTCVIDEOFRAME(ConvertToI400BufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToI400BufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToYUY2Buffer) +TEST_WEBRTCVIDEOFRAME(ConvertToYUY2BufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToYUY2BufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertToUYVYBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertToUYVYBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertToUYVYBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromABGRBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromABGRBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromABGRBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromARGB1555Buffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromARGB1555BufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromARGB1555BufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromARGB4444Buffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromARGB4444BufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromARGB4444BufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromARGBBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromARGBBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromARGBBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromBGRABuffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromBGRABufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromBGRABufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromRAWBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromRAWBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromRAWBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromRGB24Buffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromRGB24BufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromRGB24BufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromRGB565Buffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromRGB565BufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromRGB565BufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromBayerBGGRBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromBayerBGGRBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromBayerBGGRBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromBayerGRBGBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromBayerGRBGBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromBayerGRBGBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromBayerGBRGBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromBayerGBRGBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromBayerGBRGBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromBayerRGGBBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromBayerRGGBBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromBayerRGGBBufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromI400Buffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromI400BufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromI400BufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromYUY2Buffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromYUY2BufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromYUY2BufferInverted) +TEST_WEBRTCVIDEOFRAME(ConvertFromUYVYBuffer) +TEST_WEBRTCVIDEOFRAME(ConvertFromUYVYBufferStride) +TEST_WEBRTCVIDEOFRAME(ConvertFromUYVYBufferInverted) +// TEST_WEBRTCVIDEOFRAME(ConvertToI422Buffer) +TEST_WEBRTCVIDEOFRAME(ConvertARGBToBayerGRBG) +TEST_WEBRTCVIDEOFRAME(ConvertARGBToBayerGBRG) +TEST_WEBRTCVIDEOFRAME(ConvertARGBToBayerBGGR) +TEST_WEBRTCVIDEOFRAME(ConvertARGBToBayerRGGB) +TEST_WEBRTCVIDEOFRAME(CopyToBuffer) +TEST_WEBRTCVIDEOFRAME(CopyToFrame) +TEST_WEBRTCVIDEOFRAME(Write) +TEST_WEBRTCVIDEOFRAME(CopyToBuffer1Pixel) +// TEST_WEBRTCVIDEOFRAME(ConstructARGBBlackWhitePixel) + +TEST_WEBRTCVIDEOFRAME(StretchToFrame) +TEST_WEBRTCVIDEOFRAME(Copy) +TEST_WEBRTCVIDEOFRAME(CopyIsRef) +TEST_WEBRTCVIDEOFRAME(MakeExclusive) + +// These functions test implementation-specific details. +TEST_F(WebRtcVideoFrameTest, AttachAndRelease) { + cricket::WebRtcVideoFrame frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + const int64 time_stamp = 0x7FFFFFFFFFFFFFF0LL; + frame1.SetTimeStamp(time_stamp); + EXPECT_EQ(time_stamp, frame1.GetTimeStamp()); + frame2.Attach(frame1.frame()->Buffer(), frame1.frame()->Size(), + kWidth, kHeight, 1, 1, + frame1.GetElapsedTime(), frame1.GetTimeStamp(), 0); + EXPECT_TRUE(IsEqual(frame1, frame2, 0)); + uint8* buffer; + size_t size; + frame2.Detach(&buffer, &size); + EXPECT_EQ(frame1.frame()->Buffer(), buffer); + EXPECT_EQ(frame1.frame()->Size(), size); + EXPECT_TRUE(IsNull(frame2)); + EXPECT_TRUE(IsSize(frame1, kWidth, kHeight)); +} + +TEST_F(WebRtcVideoFrameTest, Transfer) { + cricket::WebRtcVideoFrame frame1, frame2; + ASSERT_TRUE(LoadFrameNoRepeat(&frame1)); + uint8* buffer; + size_t size; + frame1.Detach(&buffer, &size); + frame2.Attach(buffer, size, kWidth, kHeight, 1, 1, + frame1.GetElapsedTime(), frame1.GetTimeStamp(), 0); + EXPECT_TRUE(IsNull(frame1)); + EXPECT_TRUE(IsSize(frame2, kWidth, kHeight)); +} + +// Tests the Init function with different cropped size. +TEST_F(WebRtcVideoFrameTest, InitEvenSize) { + TestInit(640, 360); +} + +TEST_F(WebRtcVideoFrameTest, InitOddWidth) { + TestInit(601, 480); +} + +TEST_F(WebRtcVideoFrameTest, InitOddHeight) { + TestInit(360, 765); +} + +TEST_F(WebRtcVideoFrameTest, InitOddWidthHeight) { + TestInit(355, 1021); +} diff --git a/talk/media/webrtc/webrtcvie.h b/talk/media/webrtc/webrtcvie.h new file mode 100644 index 000000000..9550962e5 --- /dev/null +++ b/talk/media/webrtc/webrtcvie.h @@ -0,0 +1,151 @@ +/* + * libjingle + * Copyright 2004 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. + */ + + +#ifndef TALK_MEDIA_WEBRTCVIE_H_ +#define TALK_MEDIA_WEBRTCVIE_H_ + +#include "talk/base/common.h" +#include "talk/media/webrtc/webrtccommon.h" +#include "webrtc/common_types.h" +#include "webrtc/modules/interface/module_common_types.h" +#include "webrtc/modules/video_capture/include/video_capture.h" +#include "webrtc/modules/video_coding/codecs/interface/video_codec_interface.h" +#include "webrtc/modules/video_render/include/video_render.h" +#include "webrtc/video_engine/include/vie_base.h" +#include "webrtc/video_engine/include/vie_capture.h" +#include "webrtc/video_engine/include/vie_codec.h" +#include "webrtc/video_engine/include/vie_errors.h" +#include "webrtc/video_engine/include/vie_external_codec.h" +#include "webrtc/video_engine/include/vie_image_process.h" +#include "webrtc/video_engine/include/vie_network.h" +#include "webrtc/video_engine/include/vie_render.h" +#include "webrtc/video_engine/include/vie_rtp_rtcp.h" + +namespace cricket { + +// all tracing macros should go to a common file + +// automatically handles lifetime of VideoEngine +class scoped_vie_engine { + public: + explicit scoped_vie_engine(webrtc::VideoEngine* e) : ptr(e) {} + // VERIFY, to ensure that there are no leaks at shutdown + ~scoped_vie_engine() { + if (ptr) { + webrtc::VideoEngine::Delete(ptr); + } + } + webrtc::VideoEngine* get() const { return ptr; } + private: + webrtc::VideoEngine* ptr; +}; + +// scoped_ptr class to handle obtaining and releasing VideoEngine +// interface pointers +template class scoped_vie_ptr { + public: + explicit scoped_vie_ptr(const scoped_vie_engine& e) + : ptr(T::GetInterface(e.get())) {} + explicit scoped_vie_ptr(T* p) : ptr(p) {} + ~scoped_vie_ptr() { if (ptr) ptr->Release(); } + T* operator->() const { return ptr; } + T* get() const { return ptr; } + private: + T* ptr; +}; + +// Utility class for aggregating the various WebRTC interface. +// Fake implementations can also be injected for testing. +class ViEWrapper { + public: + ViEWrapper() + : engine_(webrtc::VideoEngine::Create()), + base_(engine_), codec_(engine_), capture_(engine_), + network_(engine_), render_(engine_), rtp_(engine_), + image_(engine_), ext_codec_(engine_) { + } + + ViEWrapper(webrtc::ViEBase* base, webrtc::ViECodec* codec, + webrtc::ViECapture* capture, webrtc::ViENetwork* network, + webrtc::ViERender* render, webrtc::ViERTP_RTCP* rtp, + webrtc::ViEImageProcess* image, + webrtc::ViEExternalCodec* ext_codec) + : engine_(NULL), + base_(base), + codec_(codec), + capture_(capture), + network_(network), + render_(render), + rtp_(rtp), + image_(image), + ext_codec_(ext_codec) { + } + + virtual ~ViEWrapper() {} + webrtc::VideoEngine* engine() { return engine_.get(); } + webrtc::ViEBase* base() { return base_.get(); } + webrtc::ViECodec* codec() { return codec_.get(); } + webrtc::ViECapture* capture() { return capture_.get(); } + webrtc::ViENetwork* network() { return network_.get(); } + webrtc::ViERender* render() { return render_.get(); } + webrtc::ViERTP_RTCP* rtp() { return rtp_.get(); } + webrtc::ViEImageProcess* image() { return image_.get(); } + webrtc::ViEExternalCodec* ext_codec() { return ext_codec_.get(); } + int error() { return base_->LastError(); } + + private: + scoped_vie_engine engine_; + scoped_vie_ptr base_; + scoped_vie_ptr codec_; + scoped_vie_ptr capture_; + scoped_vie_ptr network_; + scoped_vie_ptr render_; + scoped_vie_ptr rtp_; + scoped_vie_ptr image_; + scoped_vie_ptr ext_codec_; +}; + +// Adds indirection to static WebRtc functions, allowing them to be mocked. +class ViETraceWrapper { + public: + virtual ~ViETraceWrapper() {} + + virtual int SetTraceFilter(const unsigned int filter) { + return webrtc::VideoEngine::SetTraceFilter(filter); + } + virtual int SetTraceFile(const char* fileNameUTF8) { + return webrtc::VideoEngine::SetTraceFile(fileNameUTF8); + } + virtual int SetTraceCallback(webrtc::TraceCallback* callback) { + return webrtc::VideoEngine::SetTraceCallback(callback); + } +}; + +} // namespace cricket + +#endif // TALK_MEDIA_WEBRTCVIE_H_ diff --git a/talk/media/webrtc/webrtcvoe.h b/talk/media/webrtc/webrtcvoe.h new file mode 100644 index 000000000..bc8358d9b --- /dev/null +++ b/talk/media/webrtc/webrtcvoe.h @@ -0,0 +1,179 @@ +/* + * libjingle + * Copyright 2004 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. + */ + + +#ifndef TALK_MEDIA_WEBRTCVOE_H_ +#define TALK_MEDIA_WEBRTCVOE_H_ + +#include "talk/base/common.h" +#include "talk/media/webrtc/webrtccommon.h" + +#include "webrtc/common_types.h" +#include "webrtc/modules/audio_device/include/audio_device.h" +#include "webrtc/voice_engine/include/voe_audio_processing.h" +#include "webrtc/voice_engine/include/voe_base.h" +#include "webrtc/voice_engine/include/voe_codec.h" +#include "webrtc/voice_engine/include/voe_dtmf.h" +#include "webrtc/voice_engine/include/voe_errors.h" +#include "webrtc/voice_engine/include/voe_external_media.h" +#include "webrtc/voice_engine/include/voe_file.h" +#include "webrtc/voice_engine/include/voe_hardware.h" +#include "webrtc/voice_engine/include/voe_neteq_stats.h" +#include "webrtc/voice_engine/include/voe_network.h" +#include "webrtc/voice_engine/include/voe_rtp_rtcp.h" +#include "webrtc/voice_engine/include/voe_video_sync.h" +#include "webrtc/voice_engine/include/voe_volume_control.h" + +namespace cricket { +// automatically handles lifetime of WebRtc VoiceEngine +class scoped_voe_engine { + public: + explicit scoped_voe_engine(webrtc::VoiceEngine* e) : ptr(e) {} + // VERIFY, to ensure that there are no leaks at shutdown + ~scoped_voe_engine() { if (ptr) VERIFY(webrtc::VoiceEngine::Delete(ptr)); } + // Releases the current pointer. + void reset() { + if (ptr) { + VERIFY(webrtc::VoiceEngine::Delete(ptr)); + ptr = NULL; + } + } + webrtc::VoiceEngine* get() const { return ptr; } + private: + webrtc::VoiceEngine* ptr; +}; + +// scoped_ptr class to handle obtaining and releasing WebRTC interface pointers +template +class scoped_voe_ptr { + public: + explicit scoped_voe_ptr(const scoped_voe_engine& e) + : ptr(T::GetInterface(e.get())) {} + explicit scoped_voe_ptr(T* p) : ptr(p) {} + ~scoped_voe_ptr() { if (ptr) ptr->Release(); } + T* operator->() const { return ptr; } + T* get() const { return ptr; } + + // Releases the current pointer. + void reset() { + if (ptr) { + ptr->Release(); + ptr = NULL; + } + } + + private: + T* ptr; +}; + +// Utility class for aggregating the various WebRTC interface. +// Fake implementations can also be injected for testing. +class VoEWrapper { + public: + VoEWrapper() + : engine_(webrtc::VoiceEngine::Create()), processing_(engine_), + base_(engine_), codec_(engine_), dtmf_(engine_), file_(engine_), + hw_(engine_), media_(engine_), neteq_(engine_), network_(engine_), + rtp_(engine_), sync_(engine_), volume_(engine_) { + } + VoEWrapper(webrtc::VoEAudioProcessing* processing, + webrtc::VoEBase* base, + webrtc::VoECodec* codec, + webrtc::VoEDtmf* dtmf, + webrtc::VoEFile* file, + webrtc::VoEHardware* hw, + webrtc::VoEExternalMedia* media, + webrtc::VoENetEqStats* neteq, + webrtc::VoENetwork* network, + webrtc::VoERTP_RTCP* rtp, + webrtc::VoEVideoSync* sync, + webrtc::VoEVolumeControl* volume) + : engine_(NULL), + processing_(processing), + base_(base), + codec_(codec), + dtmf_(dtmf), + file_(file), + hw_(hw), + media_(media), + neteq_(neteq), + network_(network), + rtp_(rtp), + sync_(sync), + volume_(volume) { + } + ~VoEWrapper() {} + webrtc::VoiceEngine* engine() const { return engine_.get(); } + webrtc::VoEAudioProcessing* processing() const { return processing_.get(); } + webrtc::VoEBase* base() const { return base_.get(); } + webrtc::VoECodec* codec() const { return codec_.get(); } + webrtc::VoEDtmf* dtmf() const { return dtmf_.get(); } + webrtc::VoEFile* file() const { return file_.get(); } + webrtc::VoEHardware* hw() const { return hw_.get(); } + webrtc::VoEExternalMedia* media() const { return media_.get(); } + webrtc::VoENetEqStats* neteq() const { return neteq_.get(); } + webrtc::VoENetwork* network() const { return network_.get(); } + webrtc::VoERTP_RTCP* rtp() const { return rtp_.get(); } + webrtc::VoEVideoSync* sync() const { return sync_.get(); } + webrtc::VoEVolumeControl* volume() const { return volume_.get(); } + int error() { return base_->LastError(); } + + private: + scoped_voe_engine engine_; + scoped_voe_ptr processing_; + scoped_voe_ptr base_; + scoped_voe_ptr codec_; + scoped_voe_ptr dtmf_; + scoped_voe_ptr file_; + scoped_voe_ptr hw_; + scoped_voe_ptr media_; + scoped_voe_ptr neteq_; + scoped_voe_ptr network_; + scoped_voe_ptr rtp_; + scoped_voe_ptr sync_; + scoped_voe_ptr volume_; +}; + +// Adds indirection to static WebRtc functions, allowing them to be mocked. +class VoETraceWrapper { + public: + virtual ~VoETraceWrapper() {} + + virtual int SetTraceFilter(const unsigned int filter) { + return webrtc::VoiceEngine::SetTraceFilter(filter); + } + virtual int SetTraceFile(const char* fileNameUTF8) { + return webrtc::VoiceEngine::SetTraceFile(fileNameUTF8); + } + virtual int SetTraceCallback(webrtc::TraceCallback* callback) { + return webrtc::VoiceEngine::SetTraceCallback(callback); + } +}; + +} // namespace cricket + +#endif // TALK_MEDIA_WEBRTCVOE_H_ diff --git a/talk/media/webrtc/webrtcvoiceengine.cc b/talk/media/webrtc/webrtcvoiceengine.cc new file mode 100644 index 000000000..266cefc5c --- /dev/null +++ b/talk/media/webrtc/webrtcvoiceengine.cc @@ -0,0 +1,2777 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#ifdef HAVE_WEBRTC_VOICE + +#include "talk/media/webrtc/webrtcvoiceengine.h" + +#include +#include +#include +#include + +#include "talk/base/base64.h" +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" +#include "talk/media/base/audiorenderer.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/streamparams.h" +#include "talk/media/base/voiceprocessor.h" +#include "talk/media/webrtc/webrtcvoe.h" +#include "webrtc/modules/audio_processing/include/audio_processing.h" + +#ifdef WIN32 +#include // NOLINT +#endif + +namespace cricket { + +struct CodecPref { + const char* name; + int clockrate; + int channels; + int payload_type; + bool is_multi_rate; +}; + +static const CodecPref kCodecPrefs[] = { + { "OPUS", 48000, 2, 111, true }, + { "ISAC", 16000, 1, 103, true }, + { "ISAC", 32000, 1, 104, true }, + { "CELT", 32000, 1, 109, true }, + { "CELT", 32000, 2, 110, true }, + { "G722", 16000, 1, 9, false }, + { "ILBC", 8000, 1, 102, false }, + { "PCMU", 8000, 1, 0, false }, + { "PCMA", 8000, 1, 8, false }, + { "CN", 48000, 1, 107, false }, + { "CN", 32000, 1, 106, false }, + { "CN", 16000, 1, 105, false }, + { "CN", 8000, 1, 13, false }, + { "red", 8000, 1, 127, false }, + { "telephone-event", 8000, 1, 126, false }, +}; + +// For Linux/Mac, using the default device is done by specifying index 0 for +// VoE 4.0 and not -1 (which was the case for VoE 3.5). +// +// On Windows Vista and newer, Microsoft introduced the concept of "Default +// Communications Device". This means that there are two types of default +// devices (old Wave Audio style default and Default Communications Device). +// +// On Windows systems which only support Wave Audio style default, uses either +// -1 or 0 to select the default device. +// +// On Windows systems which support both "Default Communication Device" and +// old Wave Audio style default, use -1 for Default Communications Device and +// -2 for Wave Audio style default, which is what we want to use for clips. +// It's not clear yet whether the -2 index is handled properly on other OSes. + +#ifdef WIN32 +static const int kDefaultAudioDeviceId = -1; +static const int kDefaultSoundclipDeviceId = -2; +#else +static const int kDefaultAudioDeviceId = 0; +#endif + +// extension header for audio levels, as defined in +// http://tools.ietf.org/html/draft-ietf-avtext-client-to-mixer-audio-level-03 +static const char kRtpAudioLevelHeaderExtension[] = + "urn:ietf:params:rtp-hdrext:ssrc-audio-level"; +static const int kRtpAudioLevelHeaderExtensionId = 1; + +static const char kIsacCodecName[] = "ISAC"; +static const char kL16CodecName[] = "L16"; +// Codec parameters for Opus. +static const int kOpusMonoBitrate = 32000; +// Parameter used for NACK. +// This value is equivalent to 5 seconds of audio data at 20 ms per packet. +static const int kNackMaxPackets = 250; +static const int kOpusStereoBitrate = 64000; + +// Dumps an AudioCodec in RFC 2327-ish format. +static std::string ToString(const AudioCodec& codec) { + std::stringstream ss; + ss << codec.name << "/" << codec.clockrate << "/" << codec.channels + << " (" << codec.id << ")"; + return ss.str(); +} +static std::string ToString(const webrtc::CodecInst& codec) { + std::stringstream ss; + ss << codec.plname << "/" << codec.plfreq << "/" << codec.channels + << " (" << codec.pltype << ")"; + return ss.str(); +} + +static void LogMultiline(talk_base::LoggingSeverity sev, char* text) { + const char* delim = "\r\n"; + for (char* tok = strtok(text, delim); tok; tok = strtok(NULL, delim)) { + LOG_V(sev) << tok; + } +} + +// Severity is an integer because it comes is assumed to be from command line. +static int SeverityToFilter(int severity) { + int filter = webrtc::kTraceNone; + switch (severity) { + case talk_base::LS_VERBOSE: + filter |= webrtc::kTraceAll; + case talk_base::LS_INFO: + filter |= (webrtc::kTraceStateInfo | webrtc::kTraceInfo); + case talk_base::LS_WARNING: + filter |= (webrtc::kTraceTerseInfo | webrtc::kTraceWarning); + case talk_base::LS_ERROR: + filter |= (webrtc::kTraceError | webrtc::kTraceCritical); + } + return filter; +} + +static bool IsCodecMultiRate(const webrtc::CodecInst& codec) { + for (size_t i = 0; i < ARRAY_SIZE(kCodecPrefs); ++i) { + if (_stricmp(kCodecPrefs[i].name, codec.plname) == 0 && + kCodecPrefs[i].clockrate == codec.plfreq) { + return kCodecPrefs[i].is_multi_rate; + } + } + return false; +} + +static bool FindCodec(const std::vector& codecs, + const AudioCodec& codec, + AudioCodec* found_codec) { + for (std::vector::const_iterator it = codecs.begin(); + it != codecs.end(); ++it) { + if (it->Matches(codec)) { + if (found_codec != NULL) { + *found_codec = *it; + } + return true; + } + } + return false; +} +static bool IsNackEnabled(const AudioCodec& codec) { + return codec.HasFeedbackParam(FeedbackParam(kRtcpFbParamNack, + kParamValueEmpty)); +} + + +class WebRtcSoundclipMedia : public SoundclipMedia { + public: + explicit WebRtcSoundclipMedia(WebRtcVoiceEngine *engine) + : engine_(engine), webrtc_channel_(-1) { + engine_->RegisterSoundclip(this); + } + + virtual ~WebRtcSoundclipMedia() { + engine_->UnregisterSoundclip(this); + if (webrtc_channel_ != -1) { + // We shouldn't have to call Disable() here. DeleteChannel() should call + // StopPlayout() while deleting the channel. We should fix the bug + // inside WebRTC and remove the Disable() call bellow. This work is + // tracked by bug http://b/issue?id=5382855. + PlaySound(NULL, 0, 0); + Disable(); + if (engine_->voe_sc()->base()->DeleteChannel(webrtc_channel_) + == -1) { + LOG_RTCERR1(DeleteChannel, webrtc_channel_); + } + } + } + + bool Init() { + webrtc_channel_ = engine_->voe_sc()->base()->CreateChannel(); + if (webrtc_channel_ == -1) { + LOG_RTCERR0(CreateChannel); + return false; + } + return true; + } + + bool Enable() { + if (engine_->voe_sc()->base()->StartPlayout(webrtc_channel_) == -1) { + LOG_RTCERR1(StartPlayout, webrtc_channel_); + return false; + } + return true; + } + + bool Disable() { + if (engine_->voe_sc()->base()->StopPlayout(webrtc_channel_) == -1) { + LOG_RTCERR1(StopPlayout, webrtc_channel_); + return false; + } + return true; + } + + virtual bool PlaySound(const char *buf, int len, int flags) { + // The voe file api is not available in chrome. + if (!engine_->voe_sc()->file()) { + return false; + } + // Must stop playing the current sound (if any), because we are about to + // modify the stream. + if (engine_->voe_sc()->file()->StopPlayingFileLocally(webrtc_channel_) + == -1) { + LOG_RTCERR1(StopPlayingFileLocally, webrtc_channel_); + return false; + } + + if (buf) { + stream_.reset(new WebRtcSoundclipStream(buf, len)); + stream_->set_loop((flags & SF_LOOP) != 0); + stream_->Rewind(); + + // Play it. + if (engine_->voe_sc()->file()->StartPlayingFileLocally( + webrtc_channel_, stream_.get()) == -1) { + LOG_RTCERR2(StartPlayingFileLocally, webrtc_channel_, stream_.get()); + LOG(LS_ERROR) << "Unable to start soundclip"; + return false; + } + } else { + stream_.reset(); + } + return true; + } + + int GetLastEngineError() const { return engine_->voe_sc()->error(); } + + private: + WebRtcVoiceEngine *engine_; + int webrtc_channel_; + talk_base::scoped_ptr stream_; +}; + +WebRtcVoiceEngine::WebRtcVoiceEngine() + : voe_wrapper_(new VoEWrapper()), + voe_wrapper_sc_(new VoEWrapper()), + tracing_(new VoETraceWrapper()), + adm_(NULL), + adm_sc_(NULL), + log_filter_(SeverityToFilter(kDefaultLogSeverity)), + is_dumping_aec_(false), + desired_local_monitor_enable_(false), + tx_processor_ssrc_(0), + rx_processor_ssrc_(0) { + Construct(); +} + +WebRtcVoiceEngine::WebRtcVoiceEngine(VoEWrapper* voe_wrapper, + VoEWrapper* voe_wrapper_sc, + VoETraceWrapper* tracing) + : voe_wrapper_(voe_wrapper), + voe_wrapper_sc_(voe_wrapper_sc), + tracing_(tracing), + adm_(NULL), + adm_sc_(NULL), + log_filter_(SeverityToFilter(kDefaultLogSeverity)), + is_dumping_aec_(false), + desired_local_monitor_enable_(false), + tx_processor_ssrc_(0), + rx_processor_ssrc_(0) { + Construct(); +} + +void WebRtcVoiceEngine::Construct() { + SetTraceFilter(log_filter_); + initialized_ = false; + LOG(LS_VERBOSE) << "WebRtcVoiceEngine::WebRtcVoiceEngine"; + SetTraceOptions(""); + if (tracing_->SetTraceCallback(this) == -1) { + LOG_RTCERR0(SetTraceCallback); + } + if (voe_wrapper_->base()->RegisterVoiceEngineObserver(*this) == -1) { + LOG_RTCERR0(RegisterVoiceEngineObserver); + } + // Clear the default agc state. + memset(&default_agc_config_, 0, sizeof(default_agc_config_)); + + // Load our audio codec list. + ConstructCodecs(); + + // Load our RTP Header extensions. + rtp_header_extensions_.push_back( + RtpHeaderExtension(kRtpAudioLevelHeaderExtension, + kRtpAudioLevelHeaderExtensionId)); +} + +static bool IsOpus(const AudioCodec& codec) { + return (_stricmp(codec.name.c_str(), kOpusCodecName) == 0); +} + +static bool IsIsac(const AudioCodec& codec) { + return (_stricmp(codec.name.c_str(), kIsacCodecName) == 0); +} + +// True if params["stereo"] == "1" +static bool IsOpusStereoEnabled(const AudioCodec& codec) { + CodecParameterMap::const_iterator param = + codec.params.find(kCodecParamStereo); + if (param == codec.params.end()) { + return false; + } + return param->second == kParamValueTrue; +} + +void WebRtcVoiceEngine::ConstructCodecs() { + LOG(LS_INFO) << "WebRtc VoiceEngine codecs:"; + int ncodecs = voe_wrapper_->codec()->NumOfCodecs(); + for (int i = 0; i < ncodecs; ++i) { + webrtc::CodecInst voe_codec; + if (voe_wrapper_->codec()->GetCodec(i, voe_codec) != -1) { + // Skip uncompressed formats. + if (_stricmp(voe_codec.plname, kL16CodecName) == 0) { + continue; + } + + const CodecPref* pref = NULL; + for (size_t j = 0; j < ARRAY_SIZE(kCodecPrefs); ++j) { + if (_stricmp(kCodecPrefs[j].name, voe_codec.plname) == 0 && + kCodecPrefs[j].clockrate == voe_codec.plfreq && + kCodecPrefs[j].channels == voe_codec.channels) { + pref = &kCodecPrefs[j]; + break; + } + } + + if (pref) { + // Use the payload type that we've configured in our pref table; + // use the offset in our pref table to determine the sort order. + AudioCodec codec(pref->payload_type, voe_codec.plname, voe_codec.plfreq, + voe_codec.rate, voe_codec.channels, + ARRAY_SIZE(kCodecPrefs) - (pref - kCodecPrefs)); + LOG(LS_INFO) << ToString(codec); + if (IsIsac(codec)) { + // Indicate auto-bandwidth in signaling. + codec.bitrate = 0; + } + if (IsOpus(codec)) { + // Only add fmtp parameters that differ from the spec. + if (kPreferredMinPTime != kOpusDefaultMinPTime) { + codec.params[kCodecParamMinPTime] = + talk_base::ToString(kPreferredMinPTime); + } + if (kPreferredMaxPTime != kOpusDefaultMaxPTime) { + codec.params[kCodecParamMaxPTime] = + talk_base::ToString(kPreferredMaxPTime); + } + // TODO(hellner): Add ptime, sprop-stereo, stereo and useinbandfec + // when they can be set to values other than the default. + } + codecs_.push_back(codec); + } else { + LOG(LS_WARNING) << "Unexpected codec: " << ToString(voe_codec); + } + } + } + // Make sure they are in local preference order. + std::sort(codecs_.begin(), codecs_.end(), &AudioCodec::Preferable); +} + +WebRtcVoiceEngine::~WebRtcVoiceEngine() { + LOG(LS_VERBOSE) << "WebRtcVoiceEngine::~WebRtcVoiceEngine"; + if (voe_wrapper_->base()->DeRegisterVoiceEngineObserver() == -1) { + LOG_RTCERR0(DeRegisterVoiceEngineObserver); + } + if (adm_) { + voe_wrapper_.reset(); + adm_->Release(); + adm_ = NULL; + } + if (adm_sc_) { + voe_wrapper_sc_.reset(); + adm_sc_->Release(); + adm_sc_ = NULL; + } + + // Test to see if the media processor was deregistered properly + ASSERT(SignalRxMediaFrame.is_empty()); + ASSERT(SignalTxMediaFrame.is_empty()); + + tracing_->SetTraceCallback(NULL); +} + +bool WebRtcVoiceEngine::Init(talk_base::Thread* worker_thread) { + LOG(LS_INFO) << "WebRtcVoiceEngine::Init"; + bool res = InitInternal(); + if (res) { + LOG(LS_INFO) << "WebRtcVoiceEngine::Init Done!"; + } else { + LOG(LS_ERROR) << "WebRtcVoiceEngine::Init failed"; + Terminate(); + } + return res; +} + +bool WebRtcVoiceEngine::InitInternal() { + // Temporarily turn logging level up for the Init call + int old_filter = log_filter_; + int extended_filter = log_filter_ | SeverityToFilter(talk_base::LS_INFO); + SetTraceFilter(extended_filter); + SetTraceOptions(""); + + // Init WebRtc VoiceEngine. + if (voe_wrapper_->base()->Init(adm_) == -1) { + LOG_RTCERR0_EX(Init, voe_wrapper_->error()); + SetTraceFilter(old_filter); + return false; + } + + SetTraceFilter(old_filter); + SetTraceOptions(log_options_); + + // Log the VoiceEngine version info + char buffer[1024] = ""; + voe_wrapper_->base()->GetVersion(buffer); + LOG(LS_INFO) << "WebRtc VoiceEngine Version:"; + LogMultiline(talk_base::LS_INFO, buffer); + + // Save the default AGC configuration settings. This must happen before + // calling SetOptions or the default will be overwritten. + if (voe_wrapper_->processing()->GetAgcConfig(default_agc_config_) == -1) { + LOG_RTCERR0(GetAGCConfig); + return false; + } + + if (!SetOptions(MediaEngineInterface::DEFAULT_AUDIO_OPTIONS)) { + return false; + } + + // Print our codec list again for the call diagnostic log + LOG(LS_INFO) << "WebRtc VoiceEngine codecs:"; + for (std::vector::const_iterator it = codecs_.begin(); + it != codecs_.end(); ++it) { + LOG(LS_INFO) << ToString(*it); + } + +#if defined(LINUX) && !defined(HAVE_LIBPULSE) + voe_wrapper_sc_->hw()->SetAudioDeviceLayer(webrtc::kAudioLinuxAlsa); +#endif + + // Initialize the VoiceEngine instance that we'll use to play out sound clips. + if (voe_wrapper_sc_->base()->Init(adm_sc_) == -1) { + LOG_RTCERR0_EX(Init, voe_wrapper_sc_->error()); + return false; + } + + // On Windows, tell it to use the default sound (not communication) devices. + // First check whether there is a valid sound device for playback. + // TODO(juberti): Clean this up when we support setting the soundclip device. +#ifdef WIN32 + // The SetPlayoutDevice may not be implemented in the case of external ADM. + // TODO(ronghuawu): We should only check the adm_sc_ here, but current + // PeerConnection interface never set the adm_sc_, so need to check both + // in order to determine if the external adm is used. + if (!adm_ && !adm_sc_) { + int num_of_devices = 0; + if (voe_wrapper_sc_->hw()->GetNumOfPlayoutDevices(num_of_devices) != -1 && + num_of_devices > 0) { + if (voe_wrapper_sc_->hw()->SetPlayoutDevice(kDefaultSoundclipDeviceId) + == -1) { + LOG_RTCERR1_EX(SetPlayoutDevice, kDefaultSoundclipDeviceId, + voe_wrapper_sc_->error()); + return false; + } + } else { + LOG(LS_WARNING) << "No valid sound playout device found."; + } + } +#endif + + initialized_ = true; + return true; +} + +void WebRtcVoiceEngine::Terminate() { + LOG(LS_INFO) << "WebRtcVoiceEngine::Terminate"; + initialized_ = false; + + StopAecDump(); + + voe_wrapper_sc_->base()->Terminate(); + voe_wrapper_->base()->Terminate(); + desired_local_monitor_enable_ = false; +} + +int WebRtcVoiceEngine::GetCapabilities() { + return AUDIO_SEND | AUDIO_RECV; +} + +VoiceMediaChannel *WebRtcVoiceEngine::CreateChannel() { + WebRtcVoiceMediaChannel* ch = new WebRtcVoiceMediaChannel(this); + if (!ch->valid()) { + delete ch; + ch = NULL; + } + return ch; +} + +SoundclipMedia *WebRtcVoiceEngine::CreateSoundclip() { + WebRtcSoundclipMedia *soundclip = new WebRtcSoundclipMedia(this); + if (!soundclip->Init() || !soundclip->Enable()) { + delete soundclip; + return NULL; + } + return soundclip; +} + +// TODO(zhurunz): Add a comprehensive unittests for SetOptions(). +bool WebRtcVoiceEngine::SetOptions(int flags) { + AudioOptions options; + + // Convert flags to AudioOptions. + options.echo_cancellation.Set( + ((flags & MediaEngineInterface::ECHO_CANCELLATION) != 0)); + options.auto_gain_control.Set( + ((flags & MediaEngineInterface::AUTO_GAIN_CONTROL) != 0)); + options.noise_suppression.Set( + ((flags & MediaEngineInterface::NOISE_SUPPRESSION) != 0)); + options.highpass_filter.Set( + ((flags & MediaEngineInterface::HIGHPASS_FILTER) != 0)); + options.stereo_swapping.Set( + ((flags & MediaEngineInterface::STEREO_FLIPPING) != 0)); + + // Set defaults for flagless options here. Make sure they are all set so that + // ApplyOptions applies all of them when we clear overrides. + options.typing_detection.Set(true); + options.conference_mode.Set(false); + options.adjust_agc_delta.Set(0); + options.experimental_agc.Set(false); + options.experimental_aec.Set(false); + options.aec_dump.Set(false); + + return SetAudioOptions(options); +} + +bool WebRtcVoiceEngine::SetAudioOptions(const AudioOptions& options) { + if (!ApplyOptions(options)) { + return false; + } + options_ = options; + return true; +} + +bool WebRtcVoiceEngine::SetOptionOverrides(const AudioOptions& overrides) { + LOG(LS_INFO) << "Setting option overrides: " << overrides.ToString(); + if (!ApplyOptions(overrides)) { + return false; + } + option_overrides_ = overrides; + return true; +} + +bool WebRtcVoiceEngine::ClearOptionOverrides() { + LOG(LS_INFO) << "Clearing option overrides."; + AudioOptions options = options_; + // Only call ApplyOptions if |options_overrides_| contains overrided options. + // ApplyOptions affects NS, AGC other options that is shared between + // all WebRtcVoiceEngineChannels. + if (option_overrides_ == AudioOptions()) { + return true; + } + + if (!ApplyOptions(options)) { + return false; + } + option_overrides_ = AudioOptions(); + return true; +} + +// AudioOptions defaults are set in InitInternal (for options with corresponding +// MediaEngineInterface flags) and in SetOptions(int) for flagless options. +bool WebRtcVoiceEngine::ApplyOptions(const AudioOptions& options_in) { + AudioOptions options = options_in; // The options are modified below. + // kEcConference is AEC with high suppression. + webrtc::EcModes ec_mode = webrtc::kEcConference; + webrtc::AecmModes aecm_mode = webrtc::kAecmSpeakerphone; + webrtc::AgcModes agc_mode = webrtc::kAgcAdaptiveAnalog; + webrtc::NsModes ns_mode = webrtc::kNsHighSuppression; + bool aecm_comfort_noise = false; + +#if defined(IOS) + // On iOS, VPIO provides built-in EC and AGC. + options.echo_cancellation.Set(false); + options.auto_gain_control.Set(false); +#elif defined(ANDROID) + ec_mode = webrtc::kEcAecm; +#endif + +#if defined(IOS) || defined(ANDROID) + // Set the AGC mode for iOS as well despite disabling it above, to avoid + // unsupported configuration errors from webrtc. + agc_mode = webrtc::kAgcFixedDigital; + options.typing_detection.Set(false); + options.experimental_agc.Set(false); + options.experimental_aec.Set(false); +#endif + + LOG(LS_INFO) << "Applying audio options: " << options.ToString(); + + webrtc::VoEAudioProcessing* voep = voe_wrapper_->processing(); + + bool echo_cancellation; + if (options.echo_cancellation.Get(&echo_cancellation)) { + if (voep->SetEcStatus(echo_cancellation, ec_mode) == -1) { + LOG_RTCERR2(SetEcStatus, echo_cancellation, ec_mode); + return false; + } +#if !defined(ANDROID) + // TODO(ajm): Remove the error return on Android from webrtc. + if (voep->SetEcMetricsStatus(echo_cancellation) == -1) { + LOG_RTCERR1(SetEcMetricsStatus, echo_cancellation); + return false; + } +#endif + if (ec_mode == webrtc::kEcAecm) { + if (voep->SetAecmMode(aecm_mode, aecm_comfort_noise) != 0) { + LOG_RTCERR2(SetAecmMode, aecm_mode, aecm_comfort_noise); + return false; + } + } + } + + bool auto_gain_control; + if (options.auto_gain_control.Get(&auto_gain_control)) { + if (voep->SetAgcStatus(auto_gain_control, agc_mode) == -1) { + LOG_RTCERR2(SetAgcStatus, auto_gain_control, agc_mode); + return false; + } + } + + bool noise_suppression; + if (options.noise_suppression.Get(&noise_suppression)) { + if (voep->SetNsStatus(noise_suppression, ns_mode) == -1) { + LOG_RTCERR2(SetNsStatus, noise_suppression, ns_mode); + return false; + } + } + + bool highpass_filter; + if (options.highpass_filter.Get(&highpass_filter)) { + if (voep->EnableHighPassFilter(highpass_filter) == -1) { + LOG_RTCERR1(SetHighpassFilterStatus, highpass_filter); + return false; + } + } + + bool stereo_swapping; + if (options.stereo_swapping.Get(&stereo_swapping)) { + voep->EnableStereoChannelSwapping(stereo_swapping); + if (voep->IsStereoChannelSwappingEnabled() != stereo_swapping) { + LOG_RTCERR1(EnableStereoChannelSwapping, stereo_swapping); + return false; + } + } + + bool typing_detection; + if (options.typing_detection.Get(&typing_detection)) { + if (voep->SetTypingDetectionStatus(typing_detection) == -1) { + // In case of error, log the info and continue + LOG_RTCERR1(SetTypingDetectionStatus, typing_detection); + } + } + + int adjust_agc_delta; + if (options.adjust_agc_delta.Get(&adjust_agc_delta)) { + if (!AdjustAgcLevel(adjust_agc_delta)) { + return false; + } + } + + bool aec_dump; + if (options.aec_dump.Get(&aec_dump)) { + // TODO(grunell): Use a string in the options instead and let the embedder + // choose the filename. + if (aec_dump) + StartAecDump("audio.aecdump"); + else + StopAecDump(); + } + + + return true; +} + +bool WebRtcVoiceEngine::SetDelayOffset(int offset) { + voe_wrapper_->processing()->SetDelayOffsetMs(offset); + if (voe_wrapper_->processing()->DelayOffsetMs() != offset) { + LOG_RTCERR1(SetDelayOffsetMs, offset); + return false; + } + + return true; +} + +struct ResumeEntry { + ResumeEntry(WebRtcVoiceMediaChannel *c, bool p, SendFlags s) + : channel(c), + playout(p), + send(s) { + } + + WebRtcVoiceMediaChannel *channel; + bool playout; + SendFlags send; +}; + +// TODO(juberti): Refactor this so that the core logic can be used to set the +// soundclip device. At that time, reinstate the soundclip pause/resume code. +bool WebRtcVoiceEngine::SetDevices(const Device* in_device, + const Device* out_device) { +#if !defined(IOS) && !defined(ANDROID) + int in_id = in_device ? talk_base::FromString(in_device->id) : + kDefaultAudioDeviceId; + int out_id = out_device ? talk_base::FromString(out_device->id) : + kDefaultAudioDeviceId; + // The device manager uses -1 as the default device, which was the case for + // VoE 3.5. VoE 4.0, however, uses 0 as the default in Linux and Mac. +#ifndef WIN32 + if (-1 == in_id) { + in_id = kDefaultAudioDeviceId; + } + if (-1 == out_id) { + out_id = kDefaultAudioDeviceId; + } +#endif + + std::string in_name = (in_id != kDefaultAudioDeviceId) ? + in_device->name : "Default device"; + std::string out_name = (out_id != kDefaultAudioDeviceId) ? + out_device->name : "Default device"; + LOG(LS_INFO) << "Setting microphone to (id=" << in_id << ", name=" << in_name + << ") and speaker to (id=" << out_id << ", name=" << out_name + << ")"; + + // If we're running the local monitor, we need to stop it first. + bool ret = true; + if (!PauseLocalMonitor()) { + LOG(LS_WARNING) << "Failed to pause local monitor"; + ret = false; + } + + // Must also pause all audio playback and capture. + for (ChannelList::const_iterator i = channels_.begin(); + i != channels_.end(); ++i) { + WebRtcVoiceMediaChannel *channel = *i; + if (!channel->PausePlayout()) { + LOG(LS_WARNING) << "Failed to pause playout"; + ret = false; + } + if (!channel->PauseSend()) { + LOG(LS_WARNING) << "Failed to pause send"; + ret = false; + } + } + + // Find the recording device id in VoiceEngine and set recording device. + if (!FindWebRtcAudioDeviceId(true, in_name, in_id, &in_id)) { + ret = false; + } + if (ret) { + if (voe_wrapper_->hw()->SetRecordingDevice(in_id) == -1) { + LOG_RTCERR2(SetRecordingDevice, in_device->name, in_id); + ret = false; + } + } + + // Find the playout device id in VoiceEngine and set playout device. + if (!FindWebRtcAudioDeviceId(false, out_name, out_id, &out_id)) { + LOG(LS_WARNING) << "Failed to find VoiceEngine device id for " << out_name; + ret = false; + } + if (ret) { + if (voe_wrapper_->hw()->SetPlayoutDevice(out_id) == -1) { + LOG_RTCERR2(SetPlayoutDevice, out_device->name, out_id); + ret = false; + } + } + + // Resume all audio playback and capture. + for (ChannelList::const_iterator i = channels_.begin(); + i != channels_.end(); ++i) { + WebRtcVoiceMediaChannel *channel = *i; + if (!channel->ResumePlayout()) { + LOG(LS_WARNING) << "Failed to resume playout"; + ret = false; + } + if (!channel->ResumeSend()) { + LOG(LS_WARNING) << "Failed to resume send"; + ret = false; + } + } + + // Resume local monitor. + if (!ResumeLocalMonitor()) { + LOG(LS_WARNING) << "Failed to resume local monitor"; + ret = false; + } + + if (ret) { + LOG(LS_INFO) << "Set microphone to (id=" << in_id <<" name=" << in_name + << ") and speaker to (id="<< out_id << " name=" << out_name + << ")"; + } + + return ret; +#else + return true; +#endif // !IOS && !ANDROID +} + +bool WebRtcVoiceEngine::FindWebRtcAudioDeviceId( + bool is_input, const std::string& dev_name, int dev_id, int* rtc_id) { + // In Linux, VoiceEngine uses the same device dev_id as the device manager. +#ifdef LINUX + *rtc_id = dev_id; + return true; +#else + // In Windows and Mac, we need to find the VoiceEngine device id by name + // unless the input dev_id is the default device id. + if (kDefaultAudioDeviceId == dev_id) { + *rtc_id = dev_id; + return true; + } + + // Get the number of VoiceEngine audio devices. + int count = 0; + if (is_input) { + if (-1 == voe_wrapper_->hw()->GetNumOfRecordingDevices(count)) { + LOG_RTCERR0(GetNumOfRecordingDevices); + return false; + } + } else { + if (-1 == voe_wrapper_->hw()->GetNumOfPlayoutDevices(count)) { + LOG_RTCERR0(GetNumOfPlayoutDevices); + return false; + } + } + + for (int i = 0; i < count; ++i) { + char name[128]; + char guid[128]; + if (is_input) { + voe_wrapper_->hw()->GetRecordingDeviceName(i, name, guid); + LOG(LS_VERBOSE) << "VoiceEngine microphone " << i << ": " << name; + } else { + voe_wrapper_->hw()->GetPlayoutDeviceName(i, name, guid); + LOG(LS_VERBOSE) << "VoiceEngine speaker " << i << ": " << name; + } + + std::string webrtc_name(name); + if (dev_name.compare(0, webrtc_name.size(), webrtc_name) == 0) { + *rtc_id = i; + return true; + } + } + LOG(LS_WARNING) << "VoiceEngine cannot find device: " << dev_name; + return false; +#endif +} + +bool WebRtcVoiceEngine::GetOutputVolume(int* level) { + unsigned int ulevel; + if (voe_wrapper_->volume()->GetSpeakerVolume(ulevel) == -1) { + LOG_RTCERR1(GetSpeakerVolume, level); + return false; + } + *level = ulevel; + return true; +} + +bool WebRtcVoiceEngine::SetOutputVolume(int level) { + ASSERT(level >= 0 && level <= 255); + if (voe_wrapper_->volume()->SetSpeakerVolume(level) == -1) { + LOG_RTCERR1(SetSpeakerVolume, level); + return false; + } + return true; +} + +int WebRtcVoiceEngine::GetInputLevel() { + unsigned int ulevel; + return (voe_wrapper_->volume()->GetSpeechInputLevel(ulevel) != -1) ? + static_cast(ulevel) : -1; +} + +bool WebRtcVoiceEngine::SetLocalMonitor(bool enable) { + desired_local_monitor_enable_ = enable; + return ChangeLocalMonitor(desired_local_monitor_enable_); +} + +bool WebRtcVoiceEngine::ChangeLocalMonitor(bool enable) { + // The voe file api is not available in chrome. + if (!voe_wrapper_->file()) { + return false; + } + if (enable && !monitor_) { + monitor_.reset(new WebRtcMonitorStream); + if (voe_wrapper_->file()->StartRecordingMicrophone(monitor_.get()) == -1) { + LOG_RTCERR1(StartRecordingMicrophone, monitor_.get()); + // Must call Stop() because there are some cases where Start will report + // failure but still change the state, and if we leave VE in the on state + // then it could crash later when trying to invoke methods on our monitor. + voe_wrapper_->file()->StopRecordingMicrophone(); + monitor_.reset(); + return false; + } + } else if (!enable && monitor_) { + voe_wrapper_->file()->StopRecordingMicrophone(); + monitor_.reset(); + } + return true; +} + +bool WebRtcVoiceEngine::PauseLocalMonitor() { + return ChangeLocalMonitor(false); +} + +bool WebRtcVoiceEngine::ResumeLocalMonitor() { + return ChangeLocalMonitor(desired_local_monitor_enable_); +} + +const std::vector& WebRtcVoiceEngine::codecs() { + return codecs_; +} + +bool WebRtcVoiceEngine::FindCodec(const AudioCodec& in) { + return FindWebRtcCodec(in, NULL); +} + +// Get the VoiceEngine codec that matches |in|, with the supplied settings. +bool WebRtcVoiceEngine::FindWebRtcCodec(const AudioCodec& in, + webrtc::CodecInst* out) { + int ncodecs = voe_wrapper_->codec()->NumOfCodecs(); + for (int i = 0; i < ncodecs; ++i) { + webrtc::CodecInst voe_codec; + if (voe_wrapper_->codec()->GetCodec(i, voe_codec) != -1) { + AudioCodec codec(voe_codec.pltype, voe_codec.plname, voe_codec.plfreq, + voe_codec.rate, voe_codec.channels, 0); + bool multi_rate = IsCodecMultiRate(voe_codec); + // Allow arbitrary rates for ISAC to be specified. + if (multi_rate) { + // Set codec.bitrate to 0 so the check for codec.Matches() passes. + codec.bitrate = 0; + } + if (codec.Matches(in)) { + if (out) { + // Fixup the payload type. + voe_codec.pltype = in.id; + + // Set bitrate if specified. + if (multi_rate && in.bitrate != 0) { + voe_codec.rate = in.bitrate; + } + + // Apply codec-specific settings. + if (IsIsac(codec)) { + // If ISAC and an explicit bitrate is not specified, + // enable auto bandwidth adjustment. + voe_codec.rate = (in.bitrate > 0) ? in.bitrate : -1; + } + *out = voe_codec; + } + return true; + } + } + } + return false; +} +const std::vector& +WebRtcVoiceEngine::rtp_header_extensions() const { + return rtp_header_extensions_; +} + +void WebRtcVoiceEngine::SetLogging(int min_sev, const char* filter) { + // if min_sev == -1, we keep the current log level. + if (min_sev >= 0) { + SetTraceFilter(SeverityToFilter(min_sev)); + } + log_options_ = filter; + SetTraceOptions(initialized_ ? log_options_ : ""); +} + +int WebRtcVoiceEngine::GetLastEngineError() { + return voe_wrapper_->error(); +} + +void WebRtcVoiceEngine::SetTraceFilter(int filter) { + log_filter_ = filter; + tracing_->SetTraceFilter(filter); +} + +// We suppport three different logging settings for VoiceEngine: +// 1. Observer callback that goes into talk diagnostic logfile. +// Use --logfile and --loglevel +// +// 2. Encrypted VoiceEngine log for debugging VoiceEngine. +// Use --voice_loglevel --voice_logfilter "tracefile file_name" +// +// 3. EC log and dump for debugging QualityEngine. +// Use --voice_loglevel --voice_logfilter "recordEC file_name" +// +// For more details see: "https://sites.google.com/a/google.com/wavelet/Home/ +// Magic-Flute--RTC-Engine-/Magic-Flute-Command-Line-Parameters" +void WebRtcVoiceEngine::SetTraceOptions(const std::string& options) { + // Set encrypted trace file. + std::vector opts; + talk_base::tokenize(options, ' ', '"', '"', &opts); + std::vector::iterator tracefile = + std::find(opts.begin(), opts.end(), "tracefile"); + if (tracefile != opts.end() && ++tracefile != opts.end()) { + // Write encrypted debug output (at same loglevel) to file + // EncryptedTraceFile no longer supported. + if (tracing_->SetTraceFile(tracefile->c_str()) == -1) { + LOG_RTCERR1(SetTraceFile, *tracefile); + } + } + + // Set AEC dump file + std::vector::iterator recordEC = + std::find(opts.begin(), opts.end(), "recordEC"); + if (recordEC != opts.end()) { + ++recordEC; + if (recordEC != opts.end()) + StartAecDump(recordEC->c_str()); + else + StopAecDump(); + } +} + +// Ignore spammy trace messages, mostly from the stats API when we haven't +// gotten RTCP info yet from the remote side. +bool WebRtcVoiceEngine::ShouldIgnoreTrace(const std::string& trace) { + static const char* kTracesToIgnore[] = { + "\tfailed to GetReportBlockInformation", + "GetRecCodec() failed to get received codec", + "GetReceivedRtcpStatistics: Could not get received RTP statistics", + "GetRemoteRTCPData() failed to measure statistics due to lack of received RTP and/or RTCP packets", // NOLINT + "GetRemoteRTCPData() failed to retrieve sender info for remote side", + "GetRTPStatistics() failed to measure RTT since no RTP packets have been received yet", // NOLINT + "GetRTPStatistics() failed to read RTP statistics from the RTP/RTCP module", + "GetRTPStatistics() failed to retrieve RTT from the RTP/RTCP module", + "SenderInfoReceived No received SR", + "StatisticsRTP() no statistics available", + "TransmitMixer::TypingDetection() VE_TYPING_NOISE_WARNING message has been posted", // NOLINT + "TransmitMixer::TypingDetection() pending noise-saturation warning exists", // NOLINT + "GetRecPayloadType() failed to retrieve RX payload type (error=10026)", // NOLINT + "StopPlayingFileAsMicrophone() isnot playing (error=8088)", + NULL + }; + for (const char* const* p = kTracesToIgnore; *p; ++p) { + if (trace.find(*p) != std::string::npos) { + return true; + } + } + return false; +} + +void WebRtcVoiceEngine::Print(webrtc::TraceLevel level, const char* trace, + int length) { + talk_base::LoggingSeverity sev = talk_base::LS_VERBOSE; + if (level == webrtc::kTraceError || level == webrtc::kTraceCritical) + sev = talk_base::LS_ERROR; + else if (level == webrtc::kTraceWarning) + sev = talk_base::LS_WARNING; + else if (level == webrtc::kTraceStateInfo || level == webrtc::kTraceInfo) + sev = talk_base::LS_INFO; + else if (level == webrtc::kTraceTerseInfo) + sev = talk_base::LS_INFO; + + // Skip past boilerplate prefix text + if (length < 72) { + std::string msg(trace, length); + LOG(LS_ERROR) << "Malformed webrtc log message: "; + LOG_V(sev) << msg; + } else { + std::string msg(trace + 71, length - 72); + if (!ShouldIgnoreTrace(msg)) { + LOG_V(sev) << "webrtc: " << msg; + } + } +} + +void WebRtcVoiceEngine::CallbackOnError(int channel_num, int err_code) { + talk_base::CritScope lock(&channels_cs_); + WebRtcVoiceMediaChannel* channel = NULL; + uint32 ssrc = 0; + LOG(LS_WARNING) << "VoiceEngine error " << err_code << " reported on channel " + << channel_num << "."; + if (FindChannelAndSsrc(channel_num, &channel, &ssrc)) { + ASSERT(channel != NULL); + channel->OnError(ssrc, err_code); + } else { + LOG(LS_ERROR) << "VoiceEngine channel " << channel_num + << " could not be found in channel list when error reported."; + } +} + +bool WebRtcVoiceEngine::FindChannelAndSsrc( + int channel_num, WebRtcVoiceMediaChannel** channel, uint32* ssrc) const { + ASSERT(channel != NULL && ssrc != NULL); + + *channel = NULL; + *ssrc = 0; + // Find corresponding channel and ssrc + for (ChannelList::const_iterator it = channels_.begin(); + it != channels_.end(); ++it) { + ASSERT(*it != NULL); + if ((*it)->FindSsrc(channel_num, ssrc)) { + *channel = *it; + return true; + } + } + + return false; +} + +// This method will search through the WebRtcVoiceMediaChannels and +// obtain the voice engine's channel number. +bool WebRtcVoiceEngine::FindChannelNumFromSsrc( + uint32 ssrc, MediaProcessorDirection direction, int* channel_num) { + ASSERT(channel_num != NULL); + ASSERT(direction == MPD_RX || direction == MPD_TX); + + *channel_num = -1; + // Find corresponding channel for ssrc. + for (ChannelList::const_iterator it = channels_.begin(); + it != channels_.end(); ++it) { + ASSERT(*it != NULL); + if (direction & MPD_RX) { + *channel_num = (*it)->GetReceiveChannelNum(ssrc); + } + if (*channel_num == -1 && (direction & MPD_TX)) { + *channel_num = (*it)->GetSendChannelNum(ssrc); + } + if (*channel_num != -1) { + return true; + } + } + LOG(LS_WARNING) << "FindChannelFromSsrc. No Channel Found for Ssrc: " << ssrc; + return false; +} + +void WebRtcVoiceEngine::RegisterChannel(WebRtcVoiceMediaChannel *channel) { + talk_base::CritScope lock(&channels_cs_); + channels_.push_back(channel); +} + +void WebRtcVoiceEngine::UnregisterChannel(WebRtcVoiceMediaChannel *channel) { + talk_base::CritScope lock(&channels_cs_); + ChannelList::iterator i = std::find(channels_.begin(), + channels_.end(), + channel); + if (i != channels_.end()) { + channels_.erase(i); + } +} + +void WebRtcVoiceEngine::RegisterSoundclip(WebRtcSoundclipMedia *soundclip) { + soundclips_.push_back(soundclip); +} + +void WebRtcVoiceEngine::UnregisterSoundclip(WebRtcSoundclipMedia *soundclip) { + SoundclipList::iterator i = std::find(soundclips_.begin(), + soundclips_.end(), + soundclip); + if (i != soundclips_.end()) { + soundclips_.erase(i); + } +} + +// Adjusts the default AGC target level by the specified delta. +// NB: If we start messing with other config fields, we'll want +// to save the current webrtc::AgcConfig as well. +bool WebRtcVoiceEngine::AdjustAgcLevel(int delta) { + webrtc::AgcConfig config = default_agc_config_; + config.targetLeveldBOv -= delta; + + LOG(LS_INFO) << "Adjusting AGC level from default -" + << default_agc_config_.targetLeveldBOv << "dB to -" + << config.targetLeveldBOv << "dB"; + + if (voe_wrapper_->processing()->SetAgcConfig(config) == -1) { + LOG_RTCERR1(SetAgcConfig, config.targetLeveldBOv); + return false; + } + return true; +} + +bool WebRtcVoiceEngine::SetAudioDeviceModule(webrtc::AudioDeviceModule* adm, + webrtc::AudioDeviceModule* adm_sc) { + if (initialized_) { + LOG(LS_WARNING) << "SetAudioDeviceModule can not be called after Init."; + return false; + } + if (adm_) { + adm_->Release(); + adm_ = NULL; + } + if (adm) { + adm_ = adm; + adm_->AddRef(); + } + + if (adm_sc_) { + adm_sc_->Release(); + adm_sc_ = NULL; + } + if (adm_sc) { + adm_sc_ = adm_sc; + adm_sc_->AddRef(); + } + return true; +} + +bool WebRtcVoiceEngine::RegisterProcessor( + uint32 ssrc, + VoiceProcessor* voice_processor, + MediaProcessorDirection direction) { + bool register_with_webrtc = false; + int channel_id = -1; + bool success = false; + uint32* processor_ssrc = NULL; + bool found_channel = FindChannelNumFromSsrc(ssrc, direction, &channel_id); + if (voice_processor == NULL || !found_channel) { + LOG(LS_WARNING) << "Media Processing Registration Failed. ssrc: " << ssrc + << " foundChannel: " << found_channel; + return false; + } + + webrtc::ProcessingTypes processing_type; + { + talk_base::CritScope cs(&signal_media_critical_); + if (direction == MPD_RX) { + processing_type = webrtc::kPlaybackAllChannelsMixed; + if (SignalRxMediaFrame.is_empty()) { + register_with_webrtc = true; + processor_ssrc = &rx_processor_ssrc_; + } + SignalRxMediaFrame.connect(voice_processor, + &VoiceProcessor::OnFrame); + } else { + processing_type = webrtc::kRecordingPerChannel; + if (SignalTxMediaFrame.is_empty()) { + register_with_webrtc = true; + processor_ssrc = &tx_processor_ssrc_; + } + SignalTxMediaFrame.connect(voice_processor, + &VoiceProcessor::OnFrame); + } + } + if (register_with_webrtc) { + // TODO(janahan): when registering consider instantiating a + // a VoeMediaProcess object and not make the engine extend the interface. + if (voe()->media() && voe()->media()-> + RegisterExternalMediaProcessing(channel_id, + processing_type, + *this) != -1) { + LOG(LS_INFO) << "Media Processing Registration Succeeded. channel:" + << channel_id; + *processor_ssrc = ssrc; + success = true; + } else { + LOG_RTCERR2(RegisterExternalMediaProcessing, + channel_id, + processing_type); + success = false; + } + } else { + // If we don't have to register with the engine, we just needed to + // connect a new processor, set success to true; + success = true; + } + return success; +} + +bool WebRtcVoiceEngine::UnregisterProcessorChannel( + MediaProcessorDirection channel_direction, + uint32 ssrc, + VoiceProcessor* voice_processor, + MediaProcessorDirection processor_direction) { + bool success = true; + FrameSignal* signal; + webrtc::ProcessingTypes processing_type; + uint32* processor_ssrc = NULL; + if (channel_direction == MPD_RX) { + signal = &SignalRxMediaFrame; + processing_type = webrtc::kPlaybackAllChannelsMixed; + processor_ssrc = &rx_processor_ssrc_; + } else { + signal = &SignalTxMediaFrame; + processing_type = webrtc::kRecordingPerChannel; + processor_ssrc = &tx_processor_ssrc_; + } + + int deregister_id = -1; + { + talk_base::CritScope cs(&signal_media_critical_); + if ((processor_direction & channel_direction) != 0 && !signal->is_empty()) { + signal->disconnect(voice_processor); + int channel_id = -1; + bool found_channel = FindChannelNumFromSsrc(ssrc, + channel_direction, + &channel_id); + if (signal->is_empty() && found_channel) { + deregister_id = channel_id; + } + } + } + if (deregister_id != -1) { + if (voe()->media() && + voe()->media()->DeRegisterExternalMediaProcessing(deregister_id, + processing_type) != -1) { + *processor_ssrc = 0; + LOG(LS_INFO) << "Media Processing DeRegistration Succeeded. channel:" + << deregister_id; + } else { + LOG_RTCERR2(DeRegisterExternalMediaProcessing, + deregister_id, + processing_type); + success = false; + } + } + return success; +} + +bool WebRtcVoiceEngine::UnregisterProcessor( + uint32 ssrc, + VoiceProcessor* voice_processor, + MediaProcessorDirection direction) { + bool success = true; + if (voice_processor == NULL) { + LOG(LS_WARNING) << "Media Processing Deregistration Failed. ssrc: " + << ssrc; + return false; + } + if (!UnregisterProcessorChannel(MPD_RX, ssrc, voice_processor, direction)) { + success = false; + } + if (!UnregisterProcessorChannel(MPD_TX, ssrc, voice_processor, direction)) { + success = false; + } + return success; +} + +// Implementing method from WebRtc VoEMediaProcess interface +// Do not lock mux_channel_cs_ in this callback. +void WebRtcVoiceEngine::Process(int channel, + webrtc::ProcessingTypes type, + int16_t audio10ms[], + int length, + int sampling_freq, + bool is_stereo) { + talk_base::CritScope cs(&signal_media_critical_); + AudioFrame frame(audio10ms, length, sampling_freq, is_stereo); + if (type == webrtc::kPlaybackAllChannelsMixed) { + SignalRxMediaFrame(rx_processor_ssrc_, MPD_RX, &frame); + } else if (type == webrtc::kRecordingPerChannel) { + SignalTxMediaFrame(tx_processor_ssrc_, MPD_TX, &frame); + } else { + LOG(LS_WARNING) << "Media Processing invoked unexpectedly." + << " channel: " << channel << " type: " << type + << " tx_ssrc: " << tx_processor_ssrc_ + << " rx_ssrc: " << rx_processor_ssrc_; + } +} + +void WebRtcVoiceEngine::StartAecDump(const std::string& filename) { + if (!is_dumping_aec_) { + // Start dumping AEC when we are not dumping. + if (voe_wrapper_->processing()->StartDebugRecording( + filename.c_str()) != webrtc::AudioProcessing::kNoError) { + LOG_RTCERR0(StartDebugRecording); + } else { + is_dumping_aec_ = true; + } + } +} + +void WebRtcVoiceEngine::StopAecDump() { + if (is_dumping_aec_) { + // Stop dumping AEC when we are dumping. + if (voe_wrapper_->processing()->StopDebugRecording() != + webrtc::AudioProcessing::kNoError) { + LOG_RTCERR0(StopDebugRecording); + } + is_dumping_aec_ = false; + } +} + +// WebRtcVoiceMediaChannel +WebRtcVoiceMediaChannel::WebRtcVoiceMediaChannel(WebRtcVoiceEngine *engine) + : WebRtcMediaChannel( + engine, + engine->voe()->base()->CreateChannel()), + options_(), + dtmf_allowed_(false), + desired_playout_(false), + nack_enabled_(false), + playout_(false), + desired_send_(SEND_NOTHING), + send_(SEND_NOTHING), + send_ssrc_(0), + default_receive_ssrc_(0) { + engine->RegisterChannel(this); + LOG(LS_VERBOSE) << "WebRtcVoiceMediaChannel::WebRtcVoiceMediaChannel " + << voe_channel(); + + // Register external transport + if (engine->voe()->network()->RegisterExternalTransport( + voe_channel(), *static_cast(this)) == -1) { + LOG_RTCERR2(RegisterExternalTransport, voe_channel(), this); + } + + // Enable RTCP (for quality stats and feedback messages) + EnableRtcp(voe_channel()); + + // Reset all recv codecs; they will be enabled via SetRecvCodecs. + ResetRecvCodecs(voe_channel()); + + // Disable the DTMF playout when a tone is sent. + // PlayDtmfTone will be used if local playout is needed. + if (engine->voe()->dtmf()->SetDtmfFeedbackStatus(false) == -1) { + LOG_RTCERR1(SetDtmfFeedbackStatus, false); + } +} + +WebRtcVoiceMediaChannel::~WebRtcVoiceMediaChannel() { + LOG(LS_VERBOSE) << "WebRtcVoiceMediaChannel::~WebRtcVoiceMediaChannel " + << voe_channel(); + + // DeRegister external transport + if (engine()->voe()->network()->DeRegisterExternalTransport( + voe_channel()) == -1) { + LOG_RTCERR1(DeRegisterExternalTransport, voe_channel()); + } + + // Unregister ourselves from the engine. + engine()->UnregisterChannel(this); + // Remove any remaining streams. + while (!mux_channels_.empty()) { + RemoveRecvStream(mux_channels_.begin()->first); + } + + // Delete the primary channel. + if (engine()->voe()->base()->DeleteChannel(voe_channel()) == -1) { + LOG_RTCERR1(DeleteChannel, voe_channel()); + } +} + +bool WebRtcVoiceMediaChannel::SetOptions(const AudioOptions& options) { + LOG(LS_INFO) << "Setting voice channel options: " + << options.ToString(); + + // We retain all of the existing options, and apply the given ones + // on top. This means there is no way to "clear" options such that + // they go back to the engine default. + options_.SetAll(options); + + if (send_ != SEND_NOTHING) { + if (!engine()->SetOptionOverrides(options_)) { + LOG(LS_WARNING) << + "Failed to engine SetOptionOverrides during channel SetOptions."; + return false; + } + } else { + // Will be interpreted when appropriate. + } + + LOG(LS_INFO) << "Set voice channel options. Current options: " + << options_.ToString(); + return true; +} + +bool WebRtcVoiceMediaChannel::SetRecvCodecs( + const std::vector& codecs) { + // Set the payload types to be used for incoming media. + bool ret = true; + LOG(LS_INFO) << "Setting receive voice codecs:"; + + std::vector new_codecs; + // Find all new codecs. We allow adding new codecs but don't allow changing + // the payload type of codecs that is already configured since we might + // already be receiving packets with that payload type. + for (std::vector::const_iterator it = codecs.begin(); + it != codecs.end() && ret; ++it) { + AudioCodec old_codec; + if (FindCodec(recv_codecs_, *it, &old_codec)) { + if (old_codec.id != it->id) { + LOG(LS_ERROR) << it->name << " payload type changed."; + return false; + } + } else { + new_codecs.push_back(*it); + } + } + if (new_codecs.empty()) { + // There are no new codecs to configure. Already configured codecs are + // never removed. + return true; + } + + if (playout_) { + // Receive codecs can not be changed while playing. So we temporarily + // pause playout. + PausePlayout(); + } + + for (std::vector::const_iterator it = new_codecs.begin(); + it != new_codecs.end() && ret; ++it) { + webrtc::CodecInst voe_codec; + if (engine()->FindWebRtcCodec(*it, &voe_codec)) { + LOG(LS_INFO) << ToString(*it); + voe_codec.pltype = it->id; + if (engine()->voe()->codec()->SetRecPayloadType( + voe_channel(), voe_codec) == -1) { + LOG_RTCERR2(SetRecPayloadType, voe_channel(), ToString(voe_codec)); + ret = false; + } + + // Set the receive codecs on all receiving channels. + for (ChannelMap::iterator it = mux_channels_.begin(); + it != mux_channels_.end() && ret; ++it) { + if (engine()->voe()->codec()->SetRecPayloadType( + it->second, voe_codec) == -1) { + LOG_RTCERR2(SetRecPayloadType, it->second, ToString(voe_codec)); + ret = false; + } + } + } else { + LOG(LS_WARNING) << "Unknown codec " << ToString(*it); + ret = false; + } + } + if (ret) { + recv_codecs_ = codecs; + } + + if (desired_playout_ && !playout_) { + ResumePlayout(); + } + return ret; +} + +bool WebRtcVoiceMediaChannel::SetSendCodecs( + const std::vector& codecs) { + // Disable DTMF, VAD, and FEC unless we know the other side wants them. + dtmf_allowed_ = false; + engine()->voe()->codec()->SetVADStatus(voe_channel(), false); + engine()->voe()->rtp()->SetNACKStatus(voe_channel(), false, 0); + engine()->voe()->rtp()->SetFECStatus(voe_channel(), false); + + // Scan through the list to figure out the codec to use for sending, along + // with the proper configuration for VAD and DTMF. + bool first = true; + webrtc::CodecInst send_codec; + memset(&send_codec, 0, sizeof(send_codec)); + + for (std::vector::const_iterator it = codecs.begin(); + it != codecs.end(); ++it) { + // Ignore codecs we don't know about. The negotiation step should prevent + // this, but double-check to be sure. + webrtc::CodecInst voe_codec; + if (!engine()->FindWebRtcCodec(*it, &voe_codec)) { + LOG(LS_WARNING) << "Unknown codec " << ToString(voe_codec); + continue; + } + + // If OPUS, change what we send according to the "stereo" codec + // parameter, and not the "channels" parameter. We set + // voe_codec.channels to 2 if "stereo=1" and 1 otherwise. If + // the bitrate is not specified, i.e. is zero, we set it to the + // appropriate default value for mono or stereo Opus. + if (IsOpus(*it)) { + if (IsOpusStereoEnabled(*it)) { + voe_codec.channels = 2; + if (it->bitrate == 0) { + voe_codec.rate = kOpusStereoBitrate; + } + } else { + voe_codec.channels = 1; + if (it->bitrate == 0) { + voe_codec.rate = kOpusMonoBitrate; + } + } + } + + // Find the DTMF telephone event "codec" and tell VoiceEngine about it. + if (_stricmp(it->name.c_str(), "telephone-event") == 0 || + _stricmp(it->name.c_str(), "audio/telephone-event") == 0) { + if (engine()->voe()->dtmf()->SetSendTelephoneEventPayloadType( + voe_channel(), it->id) == -1) { + LOG_RTCERR2(SetSendTelephoneEventPayloadType, voe_channel(), it->id); + return false; + } + dtmf_allowed_ = true; + } + + // Turn voice activity detection/comfort noise on if supported. + // Set the wideband CN payload type appropriately. + // (narrowband always uses the static payload type 13). + if (_stricmp(it->name.c_str(), "CN") == 0) { + webrtc::PayloadFrequencies cn_freq; + switch (it->clockrate) { + case 8000: + cn_freq = webrtc::kFreq8000Hz; + break; + case 16000: + cn_freq = webrtc::kFreq16000Hz; + break; + case 32000: + cn_freq = webrtc::kFreq32000Hz; + break; + default: + LOG(LS_WARNING) << "CN frequency " << it->clockrate + << " not supported."; + continue; + } + // The CN payload type for 8000 Hz clockrate is fixed at 13. + if (cn_freq != webrtc::kFreq8000Hz) { + if (engine()->voe()->codec()->SetSendCNPayloadType(voe_channel(), + it->id, cn_freq) == -1) { + LOG_RTCERR3(SetSendCNPayloadType, voe_channel(), it->id, cn_freq); + // TODO(ajm): This failure condition will be removed from VoE. + // Restore the return here when we update to a new enough webrtc. + // + // Not returning false because the SetSendCNPayloadType will fail if + // the channel is already sending. + // This can happen if the remote description is applied twice, for + // example in the case of ROAP on top of JSEP, where both side will + // send the offer. + } + } + // Only turn on VAD if we have a CN payload type that matches the + // clockrate for the codec we are going to use. + if (it->clockrate == send_codec.plfreq) { + LOG(LS_INFO) << "Enabling VAD"; + if (engine()->voe()->codec()->SetVADStatus(voe_channel(), true) == -1) { + LOG_RTCERR2(SetVADStatus, voe_channel(), true); + return false; + } + } + } + + // We'll use the first codec in the list to actually send audio data. + // Be sure to use the payload type requested by the remote side. + // "red", for FEC audio, is a special case where the actual codec to be + // used is specified in params. + if (first) { + if (_stricmp(it->name.c_str(), "red") == 0) { + // Parse out the RED parameters. If we fail, just ignore RED; + // we don't support all possible params/usage scenarios. + if (!GetRedSendCodec(*it, codecs, &send_codec)) { + continue; + } + + // Enable redundant encoding of the specified codec. Treat any + // failure as a fatal internal error. + LOG(LS_INFO) << "Enabling FEC"; + if (engine()->voe()->rtp()->SetFECStatus(voe_channel(), + true, it->id) == -1) { + LOG_RTCERR3(SetFECStatus, voe_channel(), true, it->id); + return false; + } + } else { + send_codec = voe_codec; + nack_enabled_ = IsNackEnabled(*it); + SetNack(send_ssrc_, voe_channel(), nack_enabled_); + } + first = false; + // Set the codec immediately, since SetVADStatus() depends on whether + // the current codec is mono or stereo. + if (!SetSendCodec(send_codec)) + return false; + } + } + for (ChannelMap::iterator it = mux_channels_.begin(); + it != mux_channels_.end(); ++it) { + SetNack(it->first, it->second, nack_enabled_); + } + + + // If we're being asked to set an empty list of codecs, due to a buggy client, + // choose the most common format: PCMU + if (first) { + LOG(LS_WARNING) << "Received empty list of codecs; using PCMU/8000"; + AudioCodec codec(0, "PCMU", 8000, 0, 1, 0); + engine()->FindWebRtcCodec(codec, &send_codec); + if (!SetSendCodec(send_codec)) + return false; + } + + return true; +} +void WebRtcVoiceMediaChannel::SetNack(uint32 ssrc, int channel, + bool nack_enabled) { + if (nack_enabled) { + LOG(LS_INFO) << "Enabling NACK for stream " << ssrc; + engine()->voe()->rtp()->SetNACKStatus(channel, true, kNackMaxPackets); + } else { + LOG(LS_INFO) << "Disabling NACK for stream " << ssrc; + engine()->voe()->rtp()->SetNACKStatus(channel, false, 0); + } +} + + +bool WebRtcVoiceMediaChannel::SetSendCodec( + const webrtc::CodecInst& send_codec) { + LOG(LS_INFO) << "Selected voice codec " << ToString(send_codec) + << ", bitrate=" << send_codec.rate; + if (engine()->voe()->codec()->SetSendCodec(voe_channel(), + send_codec) == -1) { + LOG_RTCERR2(SetSendCodec, voe_channel(), ToString(send_codec)); + return false; + } + send_codec_.reset(new webrtc::CodecInst(send_codec)); + return true; +} + +bool WebRtcVoiceMediaChannel::SetRecvRtpHeaderExtensions( + const std::vector& extensions) { + // We don't support any incoming extensions headers right now. + return true; +} + +bool WebRtcVoiceMediaChannel::SetSendRtpHeaderExtensions( + const std::vector& extensions) { + // Enable the audio level extension header if requested. + std::vector::const_iterator it; + for (it = extensions.begin(); it != extensions.end(); ++it) { + if (it->uri == kRtpAudioLevelHeaderExtension) { + break; + } + } + + bool enable = (it != extensions.end()); + int id = 0; + + if (enable) { + id = it->id; + if (id < kMinRtpHeaderExtensionId || + id > kMaxRtpHeaderExtensionId) { + LOG(LS_WARNING) << "Invalid RTP header extension id " << id; + return false; + } + } + + LOG(LS_INFO) << "Enabling audio level header extension with ID " << id; + if (engine()->voe()->rtp()->SetRTPAudioLevelIndicationStatus( + voe_channel(), enable, id) == -1) { + LOG_RTCERR3(SetRTPAudioLevelIndicationStatus, voe_channel(), enable, id); + return false; + } + + return true; +} + +bool WebRtcVoiceMediaChannel::SetPlayout(bool playout) { + desired_playout_ = playout; + return ChangePlayout(desired_playout_); +} + +bool WebRtcVoiceMediaChannel::PausePlayout() { + return ChangePlayout(false); +} + +bool WebRtcVoiceMediaChannel::ResumePlayout() { + return ChangePlayout(desired_playout_); +} + +bool WebRtcVoiceMediaChannel::ChangePlayout(bool playout) { + if (playout_ == playout) { + return true; + } + + bool result = true; + if (mux_channels_.empty()) { + // Only toggle the default channel if we don't have any other channels. + result = SetPlayout(voe_channel(), playout); + } + for (ChannelMap::iterator it = mux_channels_.begin(); + it != mux_channels_.end() && result; ++it) { + if (!SetPlayout(it->second, playout)) { + LOG(LS_ERROR) << "SetPlayout " << playout << " on channel " << it->second + << " failed"; + result = false; + } + } + + if (result) { + playout_ = playout; + } + return result; +} + +bool WebRtcVoiceMediaChannel::SetSend(SendFlags send) { + desired_send_ = send; + if (send_ssrc_ != 0) + return ChangeSend(desired_send_); + return true; +} + +bool WebRtcVoiceMediaChannel::PauseSend() { + return ChangeSend(SEND_NOTHING); +} + +bool WebRtcVoiceMediaChannel::ResumeSend() { + return ChangeSend(desired_send_); +} + +bool WebRtcVoiceMediaChannel::ChangeSend(SendFlags send) { + if (send_ == send) { + return true; + } + + if (send == SEND_MICROPHONE) { + engine()->SetOptionOverrides(options_); + + // VoiceEngine resets sequence number when StopSend is called. This + // sometimes causes libSRTP to complain about packets being + // replayed. To get around this we store the last sent sequence + // number and initializes the channel with the next to continue on + // the same sequence. + if (sequence_number() != -1) { + LOG(LS_INFO) << "WebRtcVoiceMediaChannel restores seqnum=" + << sequence_number() + 1; + if (engine()->voe()->sync()->SetInitSequenceNumber( + voe_channel(), sequence_number() + 1) == -1) { + LOG_RTCERR2(SetInitSequenceNumber, voe_channel(), + sequence_number() + 1); + } + } + if (engine()->voe()->base()->StartSend(voe_channel()) == -1) { + LOG_RTCERR1(StartSend, voe_channel()); + return false; + } + // It's OK not to have file() here, since we don't need to call Stop if + // no file is playing. + if (engine()->voe()->file() && + engine()->voe()->file()->StopPlayingFileAsMicrophone( + voe_channel()) == -1) { + LOG_RTCERR1(StopPlayingFileAsMicrophone, voe_channel()); + return false; + } + } else if (send == SEND_RINGBACKTONE) { + ASSERT(ringback_tone_); + if (!ringback_tone_) { + return false; + } + if (engine()->voe()->file() && + engine()->voe()->file()->StartPlayingFileAsMicrophone( + voe_channel(), ringback_tone_.get(), false) != -1) { + LOG(LS_INFO) << "File StartPlayingFileAsMicrophone Succeeded. channel:" + << voe_channel(); + } else { + LOG_RTCERR3(StartPlayingFileAsMicrophone, voe_channel(), + ringback_tone_.get(), false); + return false; + } + // VoiceEngine resets sequence number when StopSend is called. This + // sometimes causes libSRTP to complain about packets being + // replayed. To get around this we store the last sent sequence + // number and initializes the channel with the next to continue on + // the same sequence. + if (sequence_number() != -1) { + LOG(LS_INFO) << "WebRtcVoiceMediaChannel restores seqnum=" + << sequence_number() + 1; + if (engine()->voe()->sync()->SetInitSequenceNumber( + voe_channel(), sequence_number() + 1) == -1) { + LOG_RTCERR2(SetInitSequenceNumber, voe_channel(), + sequence_number() + 1); + } + } + if (engine()->voe()->base()->StartSend(voe_channel()) == -1) { + LOG_RTCERR1(StartSend, voe_channel()); + return false; + } + } else { // SEND_NOTHING + if (engine()->voe()->base()->StopSend(voe_channel()) == -1) { + LOG_RTCERR1(StopSend, voe_channel()); + } + + engine()->ClearOptionOverrides(); + } + send_ = send; + return true; +} + +bool WebRtcVoiceMediaChannel::AddSendStream(const StreamParams& sp) { + if (send_ssrc_ != 0) { + LOG(LS_ERROR) << "WebRtcVoiceMediaChannel supports one sending channel."; + return false; + } + + if (engine()->voe()->rtp()->SetLocalSSRC(voe_channel(), sp.first_ssrc()) + == -1) { + LOG_RTCERR2(SetSendSSRC, voe_channel(), sp.first_ssrc()); + return false; + } + // Set the SSRC on the receive channels. + // Receive channels have to have the same SSRC in order to send receiver + // reports with this SSRC. + for (ChannelMap::const_iterator it = mux_channels_.begin(); + it != mux_channels_.end(); ++it) { + int channel_id = it->second; + if (engine()->voe()->rtp()->SetLocalSSRC(channel_id, + sp.first_ssrc()) != 0) { + LOG_RTCERR1(SetLocalSSRC, it->first); + return false; + } + } + + if (engine()->voe()->rtp()->SetRTCP_CNAME(voe_channel(), + sp.cname.c_str()) == -1) { + LOG_RTCERR2(SetRTCP_CNAME, voe_channel(), sp.cname); + return false; + } + + send_ssrc_ = sp.first_ssrc(); + if (desired_send_ != send_) + return ChangeSend(desired_send_); + return true; +} + +bool WebRtcVoiceMediaChannel::RemoveSendStream(uint32 ssrc) { + if (ssrc != send_ssrc_) { + return false; + } + send_ssrc_ = 0; + ChangeSend(SEND_NOTHING); + return true; +} + +bool WebRtcVoiceMediaChannel::AddRecvStream(const StreamParams& sp) { + talk_base::CritScope lock(&mux_channels_cs_); + + // Reuse default channel for recv stream in 1:1 call. + bool conference_mode; + if (!options_.conference_mode.Get(&conference_mode) || !conference_mode) { + LOG(LS_INFO) << "Recv stream " << sp.first_ssrc() + << " reuse default channel"; + default_receive_ssrc_ = sp.first_ssrc(); + return true; + } + + if (!VERIFY(sp.ssrcs.size() == 1)) + return false; + uint32 ssrc = sp.first_ssrc(); + + if (mux_channels_.find(ssrc) != mux_channels_.end()) { + return false; + } + + // Create a new channel for receiving audio data. + int channel = engine()->voe()->base()->CreateChannel(); + if (channel == -1) { + LOG_RTCERR0(CreateChannel); + return false; + } + + // Configure to use external transport, like our default channel. + if (engine()->voe()->network()->RegisterExternalTransport( + channel, *this) == -1) { + LOG_RTCERR2(SetExternalTransport, channel, this); + return false; + } + + // Use the same SSRC as our default channel (so the RTCP reports are correct). + unsigned int send_ssrc; + webrtc::VoERTP_RTCP* rtp = engine()->voe()->rtp(); + if (rtp->GetLocalSSRC(voe_channel(), send_ssrc) == -1) { + LOG_RTCERR2(GetSendSSRC, channel, send_ssrc); + return false; + } + if (rtp->SetLocalSSRC(channel, send_ssrc) == -1) { + LOG_RTCERR2(SetSendSSRC, channel, send_ssrc); + return false; + } + + // Use the same recv payload types as our default channel. + ResetRecvCodecs(channel); + if (!recv_codecs_.empty()) { + for (std::vector::const_iterator it = recv_codecs_.begin(); + it != recv_codecs_.end(); ++it) { + webrtc::CodecInst voe_codec; + if (engine()->FindWebRtcCodec(*it, &voe_codec)) { + voe_codec.pltype = it->id; + voe_codec.rate = 0; // Needed to make GetRecPayloadType work for ISAC + if (engine()->voe()->codec()->GetRecPayloadType( + voe_channel(), voe_codec) != -1) { + if (engine()->voe()->codec()->SetRecPayloadType( + channel, voe_codec) == -1) { + LOG_RTCERR2(SetRecPayloadType, channel, ToString(voe_codec)); + return false; + } + } + } + } + } + + if (mux_channels_.empty() && playout_) { + // This is the first stream in a multi user meeting. We can now + // disable playback of the default stream. This since the default + // stream will probably have received some initial packets before + // the new stream was added. This will mean that the CN state from + // the default channel will be mixed in with the other streams + // throughout the whole meeting, which might be disturbing. + LOG(LS_INFO) << "Disabling playback on the default voice channel"; + SetPlayout(voe_channel(), false); + } + SetNack(ssrc, channel, nack_enabled_); + + mux_channels_[ssrc] = channel; + + // TODO(juberti): We should rollback the add if SetPlayout fails. + LOG(LS_INFO) << "New audio stream " << ssrc + << " registered to VoiceEngine channel #" + << channel << "."; + return SetPlayout(channel, playout_); +} + +bool WebRtcVoiceMediaChannel::RemoveRecvStream(uint32 ssrc) { + talk_base::CritScope lock(&mux_channels_cs_); + ChannelMap::iterator it = mux_channels_.find(ssrc); + + if (it != mux_channels_.end()) { + if (engine()->voe()->network()->DeRegisterExternalTransport( + it->second) == -1) { + LOG_RTCERR1(DeRegisterExternalTransport, it->second); + } + + LOG(LS_INFO) << "Removing audio stream " << ssrc + << " with VoiceEngine channel #" + << it->second << "."; + if (engine()->voe()->base()->DeleteChannel(it->second) == -1) { + LOG_RTCERR1(DeleteChannel, voe_channel()); + return false; + } + + mux_channels_.erase(it); + if (mux_channels_.empty() && playout_) { + // The last stream was removed. We can now enable the default + // channel for new channels to be played out immediately without + // waiting for AddStream messages. + // TODO(oja): Does the default channel still have it's CN state? + LOG(LS_INFO) << "Enabling playback on the default voice channel"; + SetPlayout(voe_channel(), true); + } + } + return true; +} + +bool WebRtcVoiceMediaChannel::SetRenderer(uint32 ssrc, + AudioRenderer* renderer) { + ASSERT(renderer != NULL); + int channel = GetReceiveChannelNum(ssrc); + if (channel == -1) + return false; + + renderer->SetChannelId(channel); + return true; +} + +bool WebRtcVoiceMediaChannel::GetActiveStreams( + AudioInfo::StreamList* actives) { + actives->clear(); + for (ChannelMap::iterator it = mux_channels_.begin(); + it != mux_channels_.end(); ++it) { + int level = GetOutputLevel(it->second); + if (level > 0) { + actives->push_back(std::make_pair(it->first, level)); + } + } + return true; +} + +int WebRtcVoiceMediaChannel::GetOutputLevel() { + // return the highest output level of all streams + int highest = GetOutputLevel(voe_channel()); + for (ChannelMap::iterator it = mux_channels_.begin(); + it != mux_channels_.end(); ++it) { + int level = GetOutputLevel(it->second); + highest = talk_base::_max(level, highest); + } + return highest; +} + +int WebRtcVoiceMediaChannel::GetTimeSinceLastTyping() { + int ret; + if (engine()->voe()->processing()->TimeSinceLastTyping(ret) == -1) { + // In case of error, log the info and continue + LOG_RTCERR0(TimeSinceLastTyping); + ret = -1; + } else { + ret *= 1000; // We return ms, webrtc returns seconds. + } + return ret; +} + +void WebRtcVoiceMediaChannel::SetTypingDetectionParameters(int time_window, + int cost_per_typing, int reporting_threshold, int penalty_decay, + int type_event_delay) { + if (engine()->voe()->processing()->SetTypingDetectionParameters( + time_window, cost_per_typing, + reporting_threshold, penalty_decay, type_event_delay) == -1) { + // In case of error, log the info and continue + LOG_RTCERR5(SetTypingDetectionParameters, time_window, + cost_per_typing, reporting_threshold, penalty_decay, + type_event_delay); + } +} + +bool WebRtcVoiceMediaChannel::SetOutputScaling( + uint32 ssrc, double left, double right) { + talk_base::CritScope lock(&mux_channels_cs_); + // Collect the channels to scale the output volume. + std::vector channels; + if (0 == ssrc) { // Collect all channels, including the default one. + channels.push_back(voe_channel()); + for (ChannelMap::const_iterator it = mux_channels_.begin(); + it != mux_channels_.end(); ++it) { + channels.push_back(it->second); + } + } else { // Collect only the channel of the specified ssrc. + int channel = GetReceiveChannelNum(ssrc); + if (-1 == channel) { + LOG(LS_WARNING) << "Cannot find channel for ssrc:" << ssrc; + return false; + } + channels.push_back(channel); + } + + // Scale the output volume for the collected channels. We first normalize to + // scale the volume and then set the left and right pan. + float scale = static_cast(talk_base::_max(left, right)); + if (scale > 0.0001f) { + left /= scale; + right /= scale; + } + for (std::vector::const_iterator it = channels.begin(); + it != channels.end(); ++it) { + if (-1 == engine()->voe()->volume()->SetChannelOutputVolumeScaling( + *it, scale)) { + LOG_RTCERR2(SetChannelOutputVolumeScaling, *it, scale); + return false; + } + if (-1 == engine()->voe()->volume()->SetOutputVolumePan( + *it, static_cast(left), static_cast(right))) { + LOG_RTCERR3(SetOutputVolumePan, *it, left, right); + // Do not return if fails. SetOutputVolumePan is not available for all + // pltforms. + } + LOG(LS_INFO) << "SetOutputScaling to left=" << left * scale + << " right=" << right * scale + << " for channel " << *it << " and ssrc " << ssrc; + } + return true; +} + +bool WebRtcVoiceMediaChannel::GetOutputScaling( + uint32 ssrc, double* left, double* right) { + if (!left || !right) return false; + + talk_base::CritScope lock(&mux_channels_cs_); + // Determine which channel based on ssrc. + int channel = (0 == ssrc) ? voe_channel() : GetReceiveChannelNum(ssrc); + if (channel == -1) { + LOG(LS_WARNING) << "Cannot find channel for ssrc:" << ssrc; + return false; + } + + float scaling; + if (-1 == engine()->voe()->volume()->GetChannelOutputVolumeScaling( + channel, scaling)) { + LOG_RTCERR2(GetChannelOutputVolumeScaling, channel, scaling); + return false; + } + + float left_pan; + float right_pan; + if (-1 == engine()->voe()->volume()->GetOutputVolumePan( + channel, left_pan, right_pan)) { + LOG_RTCERR3(GetOutputVolumePan, channel, left_pan, right_pan); + // If GetOutputVolumePan fails, we use the default left and right pan. + left_pan = 1.0f; + right_pan = 1.0f; + } + + *left = scaling * left_pan; + *right = scaling * right_pan; + return true; +} + +bool WebRtcVoiceMediaChannel::SetRingbackTone(const char *buf, int len) { + ringback_tone_.reset(new WebRtcSoundclipStream(buf, len)); + return true; +} + +bool WebRtcVoiceMediaChannel::PlayRingbackTone(uint32 ssrc, + bool play, bool loop) { + if (!ringback_tone_) { + return false; + } + + // The voe file api is not available in chrome. + if (!engine()->voe()->file()) { + return false; + } + + // Determine which VoiceEngine channel to play on. + int channel = (ssrc == 0) ? voe_channel() : GetReceiveChannelNum(ssrc); + if (channel == -1) { + return false; + } + + // Make sure the ringtone is cued properly, and play it out. + if (play) { + ringback_tone_->set_loop(loop); + ringback_tone_->Rewind(); + if (engine()->voe()->file()->StartPlayingFileLocally(channel, + ringback_tone_.get()) == -1) { + LOG_RTCERR2(StartPlayingFileLocally, channel, ringback_tone_.get()); + LOG(LS_ERROR) << "Unable to start ringback tone"; + return false; + } + ringback_channels_.insert(channel); + LOG(LS_INFO) << "Started ringback on channel " << channel; + } else { + if (engine()->voe()->file()->IsPlayingFileLocally(channel) == 1 && + engine()->voe()->file()->StopPlayingFileLocally(channel) == -1) { + LOG_RTCERR1(StopPlayingFileLocally, channel); + return false; + } + LOG(LS_INFO) << "Stopped ringback on channel " << channel; + ringback_channels_.erase(channel); + } + + return true; +} + +bool WebRtcVoiceMediaChannel::CanInsertDtmf() { + return dtmf_allowed_; +} + +bool WebRtcVoiceMediaChannel::InsertDtmf(uint32 ssrc, int event, + int duration, int flags) { + if (!dtmf_allowed_) { + return false; + } + + // TODO(ronghuawu): Remove this once the reset and delay are supported by VoE. + // https://code.google.com/p/webrtc/issues/detail?id=747 + if (event == kDtmfReset || event == kDtmfDelay) { + return true; + } + + // Send the event. + if (flags & cricket::DF_SEND) { + if (send_ssrc_ != ssrc && ssrc != 0) { + LOG(LS_WARNING) << "InsertDtmf - The specified ssrc " + << ssrc << " is not in use."; + return false; + } + // Send DTMF using out-of-band DTMF. ("true", as 3rd arg) + if (engine()->voe()->dtmf()->SendTelephoneEvent(voe_channel(), + event, true, duration) == -1) { + LOG_RTCERR4(SendTelephoneEvent, voe_channel(), event, true, duration); + return false; + } + } + + // Play the event. + if (flags & cricket::DF_PLAY) { + // Play DTMF tone locally. + if (engine()->voe()->dtmf()->PlayDtmfTone(event, duration) == -1) { + LOG_RTCERR2(PlayDtmfTone, event, duration); + return false; + } + } + + return true; +} + +void WebRtcVoiceMediaChannel::OnPacketReceived(talk_base::Buffer* packet) { + // Pick which channel to send this packet to. If this packet doesn't match + // any multiplexed streams, just send it to the default channel. Otherwise, + // send it to the specific decoder instance for that stream. + int which_channel = GetReceiveChannelNum( + ParseSsrc(packet->data(), packet->length(), false)); + if (which_channel == -1) { + which_channel = voe_channel(); + } + + // Stop any ringback that might be playing on the channel. + // It's possible the ringback has already stopped, ih which case we'll just + // use the opportunity to remove the channel from ringback_channels_. + if (engine()->voe()->file()) { + const std::set::iterator it = ringback_channels_.find(which_channel); + if (it != ringback_channels_.end()) { + if (engine()->voe()->file()->IsPlayingFileLocally( + which_channel) == 1) { + engine()->voe()->file()->StopPlayingFileLocally(which_channel); + LOG(LS_INFO) << "Stopped ringback on channel " << which_channel + << " due to incoming media"; + } + ringback_channels_.erase(which_channel); + } + } + + // Pass it off to the decoder. + engine()->voe()->network()->ReceivedRTPPacket(which_channel, + packet->data(), + packet->length()); +} + +void WebRtcVoiceMediaChannel::OnRtcpReceived(talk_base::Buffer* packet) { + // See above. + int which_channel = GetReceiveChannelNum( + ParseSsrc(packet->data(), packet->length(), true)); + if (which_channel == -1) { + which_channel = voe_channel(); + } + + engine()->voe()->network()->ReceivedRTCPPacket(which_channel, + packet->data(), + packet->length()); +} + +bool WebRtcVoiceMediaChannel::MuteStream(uint32 ssrc, bool muted) { + if (send_ssrc_ != ssrc && ssrc != 0) { + LOG(LS_WARNING) << "The specified ssrc " << ssrc << " is not in use."; + return false; + } + if (engine()->voe()->volume()->SetInputMute(voe_channel(), + muted) == -1) { + LOG_RTCERR2(SetInputMute, voe_channel(), muted); + return false; + } + return true; +} + +bool WebRtcVoiceMediaChannel::SetSendBandwidth(bool autobw, int bps) { + LOG(LS_INFO) << "WebRtcVoiceMediaChanne::SetSendBandwidth."; + + if (!send_codec_) { + LOG(LS_INFO) << "The send codec has not been set up yet."; + return false; + } + + // Bandwidth is auto by default. + if (autobw || bps <= 0) + return true; + + webrtc::CodecInst codec = *send_codec_; + bool is_multi_rate = IsCodecMultiRate(codec); + + if (is_multi_rate) { + // If codec is multi-rate then just set the bitrate. + codec.rate = bps; + if (!SetSendCodec(codec)) { + LOG(LS_INFO) << "Failed to set codec " << codec.plname + << " to bitrate " << bps << " bps."; + return false; + } + return true; + } else { + // If codec is not multi-rate and |bps| is less than the fixed bitrate + // then fail. If codec is not multi-rate and |bps| exceeds or equal the + // fixed bitrate then ignore. + if (bps < codec.rate) { + LOG(LS_INFO) << "Failed to set codec " << codec.plname + << " to bitrate " << bps << " bps" + << ", requires at least " << codec.rate << " bps."; + return false; + } + return true; + } +} + +bool WebRtcVoiceMediaChannel::GetStats(VoiceMediaInfo* info) { + // In VoiceEngine 3.5, GetRTCPStatistics will return 0 even when it fails, + // causing the stats to contain garbage information. To prevent this, we + // zero the stats structure before calling this API. + // TODO(juberti): Remove this workaround. + webrtc::CallStatistics cs; + unsigned int ssrc; + webrtc::CodecInst codec; + unsigned int level; + + // Fill in the sender info, based on what we know, and what the + // remote side told us it got from its RTCP report. + VoiceSenderInfo sinfo; + + // Data we obtain locally. + memset(&cs, 0, sizeof(cs)); + if (engine()->voe()->rtp()->GetRTCPStatistics(voe_channel(), cs) == -1 || + engine()->voe()->rtp()->GetLocalSSRC(voe_channel(), ssrc) == -1) { + return false; + } + + sinfo.ssrc = ssrc; + sinfo.codec_name = send_codec_.get() ? send_codec_->plname : ""; + sinfo.bytes_sent = cs.bytesSent; + sinfo.packets_sent = cs.packetsSent; + // RTT isn't known until a RTCP report is received. Until then, VoiceEngine + // returns 0 to indicate an error value. + sinfo.rtt_ms = (cs.rttMs > 0) ? cs.rttMs : -1; + + // Get data from the last remote RTCP report. Use default values if no data + // available. + sinfo.fraction_lost = -1.0; + sinfo.jitter_ms = -1; + sinfo.packets_lost = -1; + sinfo.ext_seqnum = -1; + std::vector receive_blocks; + if (engine()->voe()->rtp()->GetRemoteRTCPReportBlocks( + voe_channel(), &receive_blocks) != -1 && + engine()->voe()->codec()->GetSendCodec(voe_channel(), + codec) != -1) { + std::vector::iterator iter; + for (iter = receive_blocks.begin(); iter != receive_blocks.end(); ++iter) { + // Lookup report for send ssrc only. + if (iter->source_SSRC == sinfo.ssrc) { + // Convert Q8 to floating point. + sinfo.fraction_lost = static_cast(iter->fraction_lost) / 256; + // Convert samples to milliseconds. + if (codec.plfreq / 1000 > 0) { + sinfo.jitter_ms = iter->interarrival_jitter / (codec.plfreq / 1000); + } + sinfo.packets_lost = iter->cumulative_num_packets_lost; + sinfo.ext_seqnum = iter->extended_highest_sequence_number; + break; + } + } + } + + // Local speech level. + sinfo.audio_level = (engine()->voe()->volume()-> + GetSpeechInputLevelFullRange(level) != -1) ? level : -1; + + bool echo_metrics_on = false; + // These can take on valid negative values, so use the lowest possible level + // as default rather than -1. + sinfo.echo_return_loss = -100; + sinfo.echo_return_loss_enhancement = -100; + // These can also be negative, but in practice -1 is only used to signal + // insufficient data, since the resolution is limited to multiples of 4 ms. + sinfo.echo_delay_median_ms = -1; + sinfo.echo_delay_std_ms = -1; + if (engine()->voe()->processing()->GetEcMetricsStatus(echo_metrics_on) != + -1 && echo_metrics_on) { + // TODO(ajm): we may want to use VoECallReport::GetEchoMetricsSummary + // here, but it appears to be unsuitable currently. Revisit after this is + // investigated: http://b/issue?id=5666755 + int erl, erle, rerl, anlp; + if (engine()->voe()->processing()->GetEchoMetrics(erl, erle, rerl, anlp) != + -1) { + sinfo.echo_return_loss = erl; + sinfo.echo_return_loss_enhancement = erle; + } + + int median, std; + if (engine()->voe()->processing()->GetEcDelayMetrics(median, std) != -1) { + sinfo.echo_delay_median_ms = median; + sinfo.echo_delay_std_ms = std; + } + } + + info->senders.push_back(sinfo); + + // Build the list of receivers, one for each mux channel, or 1 in a 1:1 call. + std::vector channels; + for (ChannelMap::const_iterator it = mux_channels_.begin(); + it != mux_channels_.end(); ++it) { + channels.push_back(it->second); + } + if (channels.empty()) { + channels.push_back(voe_channel()); + } + + // Get the SSRC and stats for each receiver, based on our own calculations. + for (std::vector::const_iterator it = channels.begin(); + it != channels.end(); ++it) { + memset(&cs, 0, sizeof(cs)); + if (engine()->voe()->rtp()->GetRemoteSSRC(*it, ssrc) != -1 && + engine()->voe()->rtp()->GetRTCPStatistics(*it, cs) != -1 && + engine()->voe()->codec()->GetRecCodec(*it, codec) != -1) { + VoiceReceiverInfo rinfo; + rinfo.ssrc = ssrc; + rinfo.bytes_rcvd = cs.bytesReceived; + rinfo.packets_rcvd = cs.packetsReceived; + // The next four fields are from the most recently sent RTCP report. + // Convert Q8 to floating point. + rinfo.fraction_lost = static_cast(cs.fractionLost) / (1 << 8); + rinfo.packets_lost = cs.cumulativeLost; + rinfo.ext_seqnum = cs.extendedMax; + // Convert samples to milliseconds. + if (codec.plfreq / 1000 > 0) { + rinfo.jitter_ms = cs.jitterSamples / (codec.plfreq / 1000); + } + + // Get jitter buffer and total delay (alg + jitter + playout) stats. + webrtc::NetworkStatistics ns; + if (engine()->voe()->neteq() && + engine()->voe()->neteq()->GetNetworkStatistics( + *it, ns) != -1) { + rinfo.jitter_buffer_ms = ns.currentBufferSize; + rinfo.jitter_buffer_preferred_ms = ns.preferredBufferSize; + rinfo.expand_rate = + static_cast (ns.currentExpandRate) / (1 << 14); + } + if (engine()->voe()->sync()) { + int playout_buffer_delay_ms = 0; + engine()->voe()->sync()->GetDelayEstimate( + *it, &rinfo.delay_estimate_ms, &playout_buffer_delay_ms); + } + + // Get speech level. + rinfo.audio_level = (engine()->voe()->volume()-> + GetSpeechOutputLevelFullRange(*it, level) != -1) ? level : -1; + info->receivers.push_back(rinfo); + } + } + + return true; +} + +void WebRtcVoiceMediaChannel::GetLastMediaError( + uint32* ssrc, VoiceMediaChannel::Error* error) { + ASSERT(ssrc != NULL); + ASSERT(error != NULL); + FindSsrc(voe_channel(), ssrc); + *error = WebRtcErrorToChannelError(GetLastEngineError()); +} + +bool WebRtcVoiceMediaChannel::FindSsrc(int channel_num, uint32* ssrc) { + talk_base::CritScope lock(&mux_channels_cs_); + ASSERT(ssrc != NULL); + if (channel_num == voe_channel()) { + unsigned local_ssrc = 0; + // This is a sending channel. + if (engine()->voe()->rtp()->GetLocalSSRC( + channel_num, local_ssrc) != -1) { + *ssrc = local_ssrc; + } + return true; + } else if (channel_num == -1 && send_ != SEND_NOTHING) { + // Sometimes the VoiceEngine core will throw error with channel_num = -1. + // This means the error is not limited to a specific channel. Signal the + // message using ssrc=0. If the current channel is sending, use this + // channel for sending the message. + *ssrc = 0; + return true; + } else { + // Check whether this is a receiving channel. + for (ChannelMap::const_iterator it = mux_channels_.begin(); + it != mux_channels_.end(); ++it) { + if (it->second == channel_num) { + *ssrc = it->first; + return true; + } + } + } + return false; +} + +void WebRtcVoiceMediaChannel::OnError(uint32 ssrc, int error) { + SignalMediaError(ssrc, WebRtcErrorToChannelError(error)); +} + +int WebRtcVoiceMediaChannel::GetOutputLevel(int channel) { + unsigned int ulevel; + int ret = + engine()->voe()->volume()->GetSpeechOutputLevel(channel, ulevel); + return (ret == 0) ? static_cast(ulevel) : -1; +} + +int WebRtcVoiceMediaChannel::GetReceiveChannelNum(uint32 ssrc) { + ChannelMap::iterator it = mux_channels_.find(ssrc); + if (it != mux_channels_.end()) + return it->second; + return (ssrc == default_receive_ssrc_) ? voe_channel() : -1; +} + +int WebRtcVoiceMediaChannel::GetSendChannelNum(uint32 ssrc) { + return (ssrc == send_ssrc_) ? voe_channel() : -1; +} + +bool WebRtcVoiceMediaChannel::GetRedSendCodec(const AudioCodec& red_codec, + const std::vector& all_codecs, webrtc::CodecInst* send_codec) { + // Get the RED encodings from the parameter with no name. This may + // change based on what is discussed on the Jingle list. + // The encoding parameter is of the form "a/b"; we only support where + // a == b. Verify this and parse out the value into red_pt. + // If the parameter value is absent (as it will be until we wire up the + // signaling of this message), use the second codec specified (i.e. the + // one after "red") as the encoding parameter. + int red_pt = -1; + std::string red_params; + CodecParameterMap::const_iterator it = red_codec.params.find(""); + if (it != red_codec.params.end()) { + red_params = it->second; + std::vector red_pts; + if (talk_base::split(red_params, '/', &red_pts) != 2 || + red_pts[0] != red_pts[1] || + !talk_base::FromString(red_pts[0], &red_pt)) { + LOG(LS_WARNING) << "RED params " << red_params << " not supported."; + return false; + } + } else if (red_codec.params.empty()) { + LOG(LS_WARNING) << "RED params not present, using defaults"; + if (all_codecs.size() > 1) { + red_pt = all_codecs[1].id; + } + } + + // Try to find red_pt in |codecs|. + std::vector::const_iterator codec; + for (codec = all_codecs.begin(); codec != all_codecs.end(); ++codec) { + if (codec->id == red_pt) + break; + } + + // If we find the right codec, that will be the codec we pass to + // SetSendCodec, with the desired payload type. + if (codec != all_codecs.end() && + engine()->FindWebRtcCodec(*codec, send_codec)) { + } else { + LOG(LS_WARNING) << "RED params " << red_params << " are invalid."; + return false; + } + + return true; +} + +bool WebRtcVoiceMediaChannel::EnableRtcp(int channel) { + if (engine()->voe()->rtp()->SetRTCPStatus(channel, true) == -1) { + LOG_RTCERR2(SetRTCPStatus, voe_channel(), 1); + return false; + } + // TODO(juberti): Enable VQMon and RTCP XR reports, once we know what + // what we want to do with them. + // engine()->voe().EnableVQMon(voe_channel(), true); + // engine()->voe().EnableRTCP_XR(voe_channel(), true); + return true; +} + +bool WebRtcVoiceMediaChannel::ResetRecvCodecs(int channel) { + int ncodecs = engine()->voe()->codec()->NumOfCodecs(); + for (int i = 0; i < ncodecs; ++i) { + webrtc::CodecInst voe_codec; + if (engine()->voe()->codec()->GetCodec(i, voe_codec) != -1) { + voe_codec.pltype = -1; + if (engine()->voe()->codec()->SetRecPayloadType( + channel, voe_codec) == -1) { + LOG_RTCERR2(SetRecPayloadType, channel, ToString(voe_codec)); + return false; + } + } + } + return true; +} + +bool WebRtcVoiceMediaChannel::SetPlayout(int channel, bool playout) { + if (playout) { + LOG(LS_INFO) << "Starting playout for channel #" << channel; + if (engine()->voe()->base()->StartPlayout(channel) == -1) { + LOG_RTCERR1(StartPlayout, channel); + return false; + } + } else { + LOG(LS_INFO) << "Stopping playout for channel #" << channel; + engine()->voe()->base()->StopPlayout(channel); + } + return true; +} + +uint32 WebRtcVoiceMediaChannel::ParseSsrc(const void* data, size_t len, + bool rtcp) { + size_t ssrc_pos = (!rtcp) ? 8 : 4; + uint32 ssrc = 0; + if (len >= (ssrc_pos + sizeof(ssrc))) { + ssrc = talk_base::GetBE32(static_cast(data) + ssrc_pos); + } + return ssrc; +} + +// Convert VoiceEngine error code into VoiceMediaChannel::Error enum. +VoiceMediaChannel::Error + WebRtcVoiceMediaChannel::WebRtcErrorToChannelError(int err_code) { + switch (err_code) { + case 0: + return ERROR_NONE; + case VE_CANNOT_START_RECORDING: + case VE_MIC_VOL_ERROR: + case VE_GET_MIC_VOL_ERROR: + case VE_CANNOT_ACCESS_MIC_VOL: + return ERROR_REC_DEVICE_OPEN_FAILED; + case VE_SATURATION_WARNING: + return ERROR_REC_DEVICE_SATURATION; + case VE_REC_DEVICE_REMOVED: + return ERROR_REC_DEVICE_REMOVED; + case VE_RUNTIME_REC_WARNING: + case VE_RUNTIME_REC_ERROR: + return ERROR_REC_RUNTIME_ERROR; + case VE_CANNOT_START_PLAYOUT: + case VE_SPEAKER_VOL_ERROR: + case VE_GET_SPEAKER_VOL_ERROR: + case VE_CANNOT_ACCESS_SPEAKER_VOL: + return ERROR_PLAY_DEVICE_OPEN_FAILED; + case VE_RUNTIME_PLAY_WARNING: + case VE_RUNTIME_PLAY_ERROR: + return ERROR_PLAY_RUNTIME_ERROR; + case VE_TYPING_NOISE_WARNING: + return ERROR_REC_TYPING_NOISE_DETECTED; + default: + return VoiceMediaChannel::ERROR_OTHER; + } +} + +int WebRtcSoundclipStream::Read(void *buf, int len) { + size_t res = 0; + mem_.Read(buf, len, &res, NULL); + return res; +} + +int WebRtcSoundclipStream::Rewind() { + mem_.Rewind(); + // Return -1 to keep VoiceEngine from looping. + return (loop_) ? 0 : -1; +} + +} // namespace cricket + +#endif // HAVE_WEBRTC_VOICE diff --git a/talk/media/webrtc/webrtcvoiceengine.h b/talk/media/webrtc/webrtcvoiceengine.h new file mode 100644 index 000000000..edf07f42b --- /dev/null +++ b/talk/media/webrtc/webrtcvoiceengine.h @@ -0,0 +1,428 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_MEDIA_WEBRTCVOICEENGINE_H_ +#define TALK_MEDIA_WEBRTCVOICEENGINE_H_ + +#include +#include +#include +#include + +#include "talk/base/buffer.h" +#include "talk/base/byteorder.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stream.h" +#include "talk/media/base/rtputils.h" +#include "talk/media/webrtc/webrtccommon.h" +#include "talk/media/webrtc/webrtcexport.h" +#include "talk/media/webrtc/webrtcvoe.h" +#include "talk/session/media/channel.h" + +#if !defined(LIBPEERCONNECTION_LIB) && \ + !defined(LIBPEERCONNECTION_IMPLEMENTATION) +#error "Bogus include." +#endif + + +namespace cricket { + +// WebRtcSoundclipStream is an adapter object that allows a memory stream to be +// passed into WebRtc, and support looping. +class WebRtcSoundclipStream : public webrtc::InStream { + public: + WebRtcSoundclipStream(const char* buf, size_t len) + : mem_(buf, len), loop_(true) { + } + void set_loop(bool loop) { loop_ = loop; } + virtual int Read(void* buf, int len); + virtual int Rewind(); + + private: + talk_base::MemoryStream mem_; + bool loop_; +}; + +// WebRtcMonitorStream is used to monitor a stream coming from WebRtc. +// For now we just dump the data. +class WebRtcMonitorStream : public webrtc::OutStream { + virtual bool Write(const void *buf, int len) { + return true; + } +}; + +class AudioDeviceModule; +class VoETraceWrapper; +class VoEWrapper; +class VoiceProcessor; +class WebRtcSoundclipMedia; +class WebRtcVoiceMediaChannel; + +// WebRtcVoiceEngine is a class to be used with CompositeMediaEngine. +// It uses the WebRtc VoiceEngine library for audio handling. +class WebRtcVoiceEngine + : public webrtc::VoiceEngineObserver, + public webrtc::TraceCallback, + public webrtc::VoEMediaProcess { + public: + WebRtcVoiceEngine(); + // Dependency injection for testing. + WebRtcVoiceEngine(VoEWrapper* voe_wrapper, + VoEWrapper* voe_wrapper_sc, + VoETraceWrapper* tracing); + ~WebRtcVoiceEngine(); + bool Init(talk_base::Thread* worker_thread); + void Terminate(); + + int GetCapabilities(); + VoiceMediaChannel* CreateChannel(); + + SoundclipMedia* CreateSoundclip(); + + // TODO(pthatcher): Rename to SetOptions and replace the old + // flags-based SetOptions. + bool SetAudioOptions(const AudioOptions& options); + // Eventually, we will replace them with AudioOptions. + // In the meantime, we leave this here for backwards compat. + bool SetOptions(int flags); + // Overrides, when set, take precedence over the options on a + // per-option basis. For example, if AGC is set in options and AEC + // is set in overrides, AGC and AEC will be both be set. Overrides + // can also turn off options. For example, if AGC is set to "on" in + // options and AGC is set to "off" in overrides, the result is that + // AGC will be off until different overrides are applied or until + // the overrides are cleared. Only one set of overrides is present + // at a time (they do not "stack"). And when the overrides are + // cleared, the media engine's state reverts back to the options set + // via SetOptions. This allows us to have both "persistent options" + // (the normal options) and "temporary options" (overrides). + bool SetOptionOverrides(const AudioOptions& options); + bool ClearOptionOverrides(); + bool SetDelayOffset(int offset); + bool SetDevices(const Device* in_device, const Device* out_device); + bool GetOutputVolume(int* level); + bool SetOutputVolume(int level); + int GetInputLevel(); + bool SetLocalMonitor(bool enable); + + const std::vector& codecs(); + bool FindCodec(const AudioCodec& codec); + bool FindWebRtcCodec(const AudioCodec& codec, webrtc::CodecInst* gcodec); + + const std::vector& rtp_header_extensions() const; + + void SetLogging(int min_sev, const char* filter); + + bool RegisterProcessor(uint32 ssrc, + VoiceProcessor* voice_processor, + MediaProcessorDirection direction); + bool UnregisterProcessor(uint32 ssrc, + VoiceProcessor* voice_processor, + MediaProcessorDirection direction); + + // Method from webrtc::VoEMediaProcess + virtual void Process(int channel, + webrtc::ProcessingTypes type, + int16_t audio10ms[], + int length, + int sampling_freq, + bool is_stereo); + + // For tracking WebRtc channels. Needed because we have to pause them + // all when switching devices. + // May only be called by WebRtcVoiceMediaChannel. + void RegisterChannel(WebRtcVoiceMediaChannel *channel); + void UnregisterChannel(WebRtcVoiceMediaChannel *channel); + + // May only be called by WebRtcSoundclipMedia. + void RegisterSoundclip(WebRtcSoundclipMedia *channel); + void UnregisterSoundclip(WebRtcSoundclipMedia *channel); + + // Called by WebRtcVoiceMediaChannel to set a gain offset from + // the default AGC target level. + bool AdjustAgcLevel(int delta); + + VoEWrapper* voe() { return voe_wrapper_.get(); } + VoEWrapper* voe_sc() { return voe_wrapper_sc_.get(); } + int GetLastEngineError(); + + // Set the external ADMs. This can only be called before Init. + bool SetAudioDeviceModule(webrtc::AudioDeviceModule* adm, + webrtc::AudioDeviceModule* adm_sc); + + // Check whether the supplied trace should be ignored. + bool ShouldIgnoreTrace(const std::string& trace); + + private: + typedef std::vector SoundclipList; + typedef std::vector ChannelList; + typedef sigslot:: + signal3 FrameSignal; + + void Construct(); + void ConstructCodecs(); + bool InitInternal(); + void SetTraceFilter(int filter); + void SetTraceOptions(const std::string& options); + // Applies either options or overrides. Every option that is "set" + // will be applied. Every option not "set" will be ignored. This + // allows us to selectively turn on and off different options easily + // at any time. + bool ApplyOptions(const AudioOptions& options); + virtual void Print(webrtc::TraceLevel level, const char* trace, int length); + virtual void CallbackOnError(int channel, int errCode); + // Given the device type, name, and id, find device id. Return true and + // set the output parameter rtc_id if successful. + bool FindWebRtcAudioDeviceId( + bool is_input, const std::string& dev_name, int dev_id, int* rtc_id); + bool FindChannelAndSsrc(int channel_num, + WebRtcVoiceMediaChannel** channel, + uint32* ssrc) const; + bool FindChannelNumFromSsrc(uint32 ssrc, + MediaProcessorDirection direction, + int* channel_num); + bool ChangeLocalMonitor(bool enable); + bool PauseLocalMonitor(); + bool ResumeLocalMonitor(); + + bool UnregisterProcessorChannel(MediaProcessorDirection channel_direction, + uint32 ssrc, + VoiceProcessor* voice_processor, + MediaProcessorDirection processor_direction); + + void StartAecDump(const std::string& filename); + void StopAecDump(); + + // When a voice processor registers with the engine, it is connected + // to either the Rx or Tx signals, based on the direction parameter. + // SignalXXMediaFrame will be invoked for every audio packet. + FrameSignal SignalRxMediaFrame; + FrameSignal SignalTxMediaFrame; + + static const int kDefaultLogSeverity = talk_base::LS_WARNING; + + // The primary instance of WebRtc VoiceEngine. + talk_base::scoped_ptr voe_wrapper_; + // A secondary instance, for playing out soundclips (on the 'ring' device). + talk_base::scoped_ptr voe_wrapper_sc_; + talk_base::scoped_ptr tracing_; + // The external audio device manager + webrtc::AudioDeviceModule* adm_; + webrtc::AudioDeviceModule* adm_sc_; + int log_filter_; + std::string log_options_; + bool is_dumping_aec_; + std::vector codecs_; + std::vector rtp_header_extensions_; + bool desired_local_monitor_enable_; + talk_base::scoped_ptr monitor_; + SoundclipList soundclips_; + ChannelList channels_; + // channels_ can be read from WebRtc callback thread. We need a lock on that + // callback as well as the RegisterChannel/UnregisterChannel. + talk_base::CriticalSection channels_cs_; + webrtc::AgcConfig default_agc_config_; + bool initialized_; + // See SetOptions and SetOptionOverrides for a description of the + // difference between options and overrides. + // options_ are the base options, which combined with the + // option_overrides_, create the current options being used. + // options_ is stored so that when option_overrides_ is cleared, we + // can restore the options_ without the option_overrides. + AudioOptions options_; + AudioOptions option_overrides_; + + // When the media processor registers with the engine, the ssrc is cached + // here so that a look up need not be made when the callback is invoked. + // This is necessary because the lookup results in mux_channels_cs lock being + // held and if a remote participant leaves the hangout at the same time + // we hit a deadlock. + uint32 tx_processor_ssrc_; + uint32 rx_processor_ssrc_; + + talk_base::CriticalSection signal_media_critical_; +}; + +// WebRtcMediaChannel is a class that implements the common WebRtc channel +// functionality. +template +class WebRtcMediaChannel : public T, public webrtc::Transport { + public: + WebRtcMediaChannel(E *engine, int channel) + : engine_(engine), voe_channel_(channel), sequence_number_(-1) {} + E *engine() { return engine_; } + int voe_channel() const { return voe_channel_; } + bool valid() const { return voe_channel_ != -1; } + + protected: + // implements Transport interface + virtual int SendPacket(int channel, const void *data, int len) { + if (!T::network_interface_) { + return -1; + } + + // We need to store the sequence number to be able to pick up + // the same sequence when the device is restarted. + // TODO(oja): Remove when WebRtc has fixed the problem. + int seq_num; + if (!GetRtpSeqNum(data, len, &seq_num)) { + return -1; + } + if (sequence_number() == -1) { + LOG(INFO) << "WebRtcVoiceMediaChannel sends first packet seqnum=" + << seq_num; + } + sequence_number_ = seq_num; + + talk_base::Buffer packet(data, len, kMaxRtpPacketLen); + return T::network_interface_->SendPacket(&packet) ? len : -1; + } + virtual int SendRTCPPacket(int channel, const void *data, int len) { + if (!T::network_interface_) { + return -1; + } + + talk_base::Buffer packet(data, len, kMaxRtpPacketLen); + return T::network_interface_->SendRtcp(&packet) ? len : -1; + } + int sequence_number() const { + return sequence_number_; + } + + private: + E *engine_; + int voe_channel_; + int sequence_number_; +}; + +// WebRtcVoiceMediaChannel is an implementation of VoiceMediaChannel that uses +// WebRtc Voice Engine. +class WebRtcVoiceMediaChannel + : public WebRtcMediaChannel { + public: + explicit WebRtcVoiceMediaChannel(WebRtcVoiceEngine *engine); + virtual ~WebRtcVoiceMediaChannel(); + virtual bool SetOptions(const AudioOptions& options); + virtual bool GetOptions(AudioOptions* options) const { + *options = options_; + return true; + } + virtual bool SetRecvCodecs(const std::vector &codecs); + virtual bool SetSendCodecs(const std::vector &codecs); + virtual bool SetRecvRtpHeaderExtensions( + const std::vector& extensions); + virtual bool SetSendRtpHeaderExtensions( + const std::vector& extensions); + virtual bool SetPlayout(bool playout); + bool PausePlayout(); + bool ResumePlayout(); + virtual bool SetSend(SendFlags send); + bool PauseSend(); + bool ResumeSend(); + virtual bool AddSendStream(const StreamParams& sp); + virtual bool RemoveSendStream(uint32 ssrc); + virtual bool AddRecvStream(const StreamParams& sp); + virtual bool RemoveRecvStream(uint32 ssrc); + virtual bool SetRenderer(uint32 ssrc, AudioRenderer* renderer); + virtual bool GetActiveStreams(AudioInfo::StreamList* actives); + virtual int GetOutputLevel(); + virtual int GetTimeSinceLastTyping(); + virtual void SetTypingDetectionParameters(int time_window, + int cost_per_typing, int reporting_threshold, int penalty_decay, + int type_event_delay); + virtual bool SetOutputScaling(uint32 ssrc, double left, double right); + virtual bool GetOutputScaling(uint32 ssrc, double* left, double* right); + + virtual bool SetRingbackTone(const char *buf, int len); + virtual bool PlayRingbackTone(uint32 ssrc, bool play, bool loop); + virtual bool CanInsertDtmf(); + virtual bool InsertDtmf(uint32 ssrc, int event, int duration, int flags); + + virtual void OnPacketReceived(talk_base::Buffer* packet); + virtual void OnRtcpReceived(talk_base::Buffer* packet); + virtual void OnReadyToSend(bool ready) {} + virtual bool MuteStream(uint32 ssrc, bool on); + virtual bool SetSendBandwidth(bool autobw, int bps); + virtual bool GetStats(VoiceMediaInfo* info); + // Gets last reported error from WebRtc voice engine. This should be only + // called in response a failure. + virtual void GetLastMediaError(uint32* ssrc, + VoiceMediaChannel::Error* error); + bool FindSsrc(int channel_num, uint32* ssrc); + void OnError(uint32 ssrc, int error); + + bool sending() const { return send_ != SEND_NOTHING; } + int GetReceiveChannelNum(uint32 ssrc); + int GetSendChannelNum(uint32 ssrc); + + protected: + int GetLastEngineError() { return engine()->GetLastEngineError(); } + int GetOutputLevel(int channel); + bool GetRedSendCodec(const AudioCodec& red_codec, + const std::vector& all_codecs, + webrtc::CodecInst* send_codec); + bool EnableRtcp(int channel); + bool ResetRecvCodecs(int channel); + bool SetPlayout(int channel, bool playout); + static uint32 ParseSsrc(const void* data, size_t len, bool rtcp); + static Error WebRtcErrorToChannelError(int err_code); + + private: + void SetNack(uint32 ssrc, int channel, bool nack_enabled); + bool SetSendCodec(const webrtc::CodecInst& send_codec); + bool ChangePlayout(bool playout); + bool ChangeSend(SendFlags send); + + typedef std::map ChannelMap; + talk_base::scoped_ptr ringback_tone_; + std::set ringback_channels_; // channels playing ringback + std::vector recv_codecs_; + talk_base::scoped_ptr send_codec_; + AudioOptions options_; + bool dtmf_allowed_; + bool desired_playout_; + bool nack_enabled_; + bool playout_; + SendFlags desired_send_; + SendFlags send_; + + uint32 send_ssrc_; + uint32 default_receive_ssrc_; + ChannelMap mux_channels_; // for multiple sources + // mux_channels_ can be read from WebRtc callback thread. Accesses off the + // WebRtc thread must be synchronized with edits on the worker thread. Reads + // on the worker thread are ok. + // + // Do not lock this on the VoE media processor thread; potential for deadlock + // exists. + mutable talk_base::CriticalSection mux_channels_cs_; +}; + +} // namespace cricket + +#endif // TALK_MEDIA_WEBRTCVOICEENGINE_H_ diff --git a/talk/media/webrtc/webrtcvoiceengine_unittest.cc b/talk/media/webrtc/webrtcvoiceengine_unittest.cc new file mode 100644 index 000000000..41b81fc0a --- /dev/null +++ b/talk/media/webrtc/webrtcvoiceengine_unittest.cc @@ -0,0 +1,2584 @@ +// Copyright 2008 Google Inc. +// +// Author: Justin Uberti (juberti@google.com) + +#ifdef WIN32 +#include "talk/base/win32.h" +#include +#endif + +#include "talk/base/byteorder.h" +#include "talk/base/gunit.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/fakemediaengine.h" +#include "talk/media/base/fakemediaprocessor.h" +#include "talk/media/base/fakertp.h" +#include "talk/media/webrtc/fakewebrtcvoiceengine.h" +#include "talk/media/webrtc/webrtcvoiceengine.h" +#include "talk/p2p/base/fakesession.h" +#include "talk/session/media/channel.h" + +// Tests for the WebRtcVoiceEngine/VoiceChannel code. + +static const cricket::AudioCodec kPcmuCodec(0, "PCMU", 8000, 64000, 1, 0); +static const cricket::AudioCodec kIsacCodec(103, "ISAC", 16000, 32000, 1, 0); +static const cricket::AudioCodec kCeltCodec(110, "CELT", 32000, 64000, 2, 0); +static const cricket::AudioCodec kOpusCodec(111, "opus", 48000, 64000, 2, 0); +static const cricket::AudioCodec kRedCodec(117, "red", 8000, 0, 1, 0); +static const cricket::AudioCodec kCn8000Codec(13, "CN", 8000, 0, 1, 0); +static const cricket::AudioCodec kCn16000Codec(105, "CN", 16000, 0, 1, 0); +static const cricket::AudioCodec + kTelephoneEventCodec(106, "telephone-event", 8000, 0, 1, 0); +static const cricket::AudioCodec* const kAudioCodecs[] = { + &kPcmuCodec, &kIsacCodec, &kCeltCodec, &kOpusCodec, &kRedCodec, + &kCn8000Codec, &kCn16000Codec, &kTelephoneEventCodec, +}; +const char kRingbackTone[] = "RIFF____WAVE____ABCD1234"; +static uint32 kSsrc1 = 0x99; +static uint32 kSsrc2 = 0x98; + +class FakeVoEWrapper : public cricket::VoEWrapper { + public: + explicit FakeVoEWrapper(cricket::FakeWebRtcVoiceEngine* engine) + : cricket::VoEWrapper(engine, // processing + engine, // base + engine, // codec + engine, // dtmf + engine, // file + engine, // hw + engine, // media + engine, // neteq + engine, // network + engine, // rtp + engine, // sync + engine) { // volume + } +}; + +class NullVoETraceWrapper : public cricket::VoETraceWrapper { + public: + virtual int SetTraceFilter(const unsigned int filter) { + return 0; + } + virtual int SetTraceFile(const char* fileNameUTF8) { + return 0; + } + virtual int SetTraceCallback(webrtc::TraceCallback* callback) { + return 0; + } +}; + +class WebRtcVoiceEngineTestFake : public testing::Test { + public: + class ChannelErrorListener : public sigslot::has_slots<> { + public: + explicit ChannelErrorListener(cricket::VoiceMediaChannel* channel) + : ssrc_(0), error_(cricket::VoiceMediaChannel::ERROR_NONE) { + ASSERT(channel != NULL); + channel->SignalMediaError.connect( + this, &ChannelErrorListener::OnVoiceChannelError); + } + void OnVoiceChannelError(uint32 ssrc, + cricket::VoiceMediaChannel::Error error) { + ssrc_ = ssrc; + error_ = error; + } + void Reset() { + ssrc_ = 0; + error_ = cricket::VoiceMediaChannel::ERROR_NONE; + } + uint32 ssrc() const { + return ssrc_; + } + cricket::VoiceMediaChannel::Error error() const { + return error_; + } + + private: + uint32 ssrc_; + cricket::VoiceMediaChannel::Error error_; + }; + + WebRtcVoiceEngineTestFake() + : voe_(kAudioCodecs, ARRAY_SIZE(kAudioCodecs)), + voe_sc_(kAudioCodecs, ARRAY_SIZE(kAudioCodecs)), + engine_(new FakeVoEWrapper(&voe_), + new FakeVoEWrapper(&voe_sc_), + new NullVoETraceWrapper()), + channel_(NULL), soundclip_(NULL) { + options_conference_.conference_mode.Set(true); + options_adjust_agc_.adjust_agc_delta.Set(-10); + } + bool SetupEngine() { + bool result = engine_.Init(talk_base::Thread::Current()); + if (result) { + channel_ = engine_.CreateChannel(); + result = (channel_ != NULL); + } + if (result) { + result = channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(kSsrc1)); + } + return result; + } + void DeliverPacket(const void* data, int len) { + talk_base::Buffer packet(data, len); + channel_->OnPacketReceived(&packet); + } + virtual void TearDown() { + delete soundclip_; + delete channel_; + engine_.Terminate(); + } + + void TestInsertDtmf(uint32 ssrc, int channel_id) { + // Test we can only InsertDtmf when the other side supports telephone-event. + std::vector codecs; + codecs.push_back(kPcmuCodec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(channel_->SetSend(cricket::SEND_MICROPHONE)); + EXPECT_FALSE(channel_->CanInsertDtmf()); + EXPECT_FALSE(channel_->InsertDtmf(ssrc, 1, 111, cricket::DF_SEND)); + codecs.push_back(kTelephoneEventCodec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(channel_->CanInsertDtmf()); + // Check we fail if the ssrc is invalid. + EXPECT_FALSE(channel_->InsertDtmf(-1, 1, 111, cricket::DF_SEND)); + + // Test send + EXPECT_FALSE(voe_.WasSendTelephoneEventCalled(channel_id, 2, 123)); + EXPECT_TRUE(channel_->InsertDtmf(ssrc, 2, 123, cricket::DF_SEND)); + EXPECT_TRUE(voe_.WasSendTelephoneEventCalled(channel_id, 2, 123)); + + // Test play + EXPECT_FALSE(voe_.WasPlayDtmfToneCalled(3, 134)); + EXPECT_TRUE(channel_->InsertDtmf(ssrc, 3, 134, cricket::DF_PLAY)); + EXPECT_TRUE(voe_.WasPlayDtmfToneCalled(3, 134)); + + // Test send and play + EXPECT_FALSE(voe_.WasSendTelephoneEventCalled(channel_id, 4, 145)); + EXPECT_FALSE(voe_.WasPlayDtmfToneCalled(4, 145)); + EXPECT_TRUE(channel_->InsertDtmf(ssrc, 4, 145, + cricket::DF_PLAY | cricket::DF_SEND)); + EXPECT_TRUE(voe_.WasSendTelephoneEventCalled(channel_id, 4, 145)); + EXPECT_TRUE(voe_.WasPlayDtmfToneCalled(4, 145)); + } + + // Test that send bandwidth is set correctly. + // |codec| is the codec under test. + // |default_bitrate| is the default bitrate for the codec. + // |auto_bitrate| is a parameter to set to SetSendBandwidth(). + // |desired_bitrate| is a parameter to set to SetSendBandwidth(). + // |expected_result| is expected results from SetSendBandwidth(). + void TestSendBandwidth(const cricket::AudioCodec& codec, + int default_bitrate, + bool auto_bitrate, + int desired_bitrate, + bool expected_result) { + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + + codecs.push_back(codec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + webrtc::CodecInst temp_codec; + EXPECT_FALSE(voe_.GetSendCodec(channel_num, temp_codec)); + EXPECT_EQ(default_bitrate, temp_codec.rate); + + bool result = channel_->SetSendBandwidth(auto_bitrate, desired_bitrate); + EXPECT_EQ(expected_result, result); + + EXPECT_FALSE(voe_.GetSendCodec(channel_num, temp_codec)); + + if (result) { + // If SetSendBandwidth() returns true then bitrate is set correctly. + if (auto_bitrate) { + EXPECT_EQ(default_bitrate, temp_codec.rate); + } else { + EXPECT_EQ(desired_bitrate, temp_codec.rate); + } + } else { + // If SetSendBandwidth() returns false then bitrate is set to the + // default value. + EXPECT_EQ(default_bitrate, temp_codec.rate); + } + } + + + protected: + cricket::FakeWebRtcVoiceEngine voe_; + cricket::FakeWebRtcVoiceEngine voe_sc_; + cricket::WebRtcVoiceEngine engine_; + cricket::VoiceMediaChannel* channel_; + cricket::SoundclipMedia* soundclip_; + + cricket::AudioOptions options_conference_; + cricket::AudioOptions options_adjust_agc_; +}; + +// Tests that our stub library "works". +TEST_F(WebRtcVoiceEngineTestFake, StartupShutdown) { + EXPECT_FALSE(voe_.IsInited()); + EXPECT_FALSE(voe_sc_.IsInited()); + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + EXPECT_TRUE(voe_.IsInited()); + EXPECT_TRUE(voe_sc_.IsInited()); + engine_.Terminate(); + EXPECT_FALSE(voe_.IsInited()); + EXPECT_FALSE(voe_sc_.IsInited()); +} + +// Tests that we can create and destroy a channel. +TEST_F(WebRtcVoiceEngineTestFake, CreateChannel) { + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + channel_ = engine_.CreateChannel(); + EXPECT_TRUE(channel_ != NULL); +} + +// Tests that we properly handle failures in CreateChannel. +TEST_F(WebRtcVoiceEngineTestFake, CreateChannelFail) { + voe_.set_fail_create_channel(true); + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + channel_ = engine_.CreateChannel(); + EXPECT_TRUE(channel_ == NULL); +} + +// Tests that the list of supported codecs is created properly and ordered +// correctly +TEST_F(WebRtcVoiceEngineTestFake, CodecPreference) { + const std::vector& codecs = engine_.codecs(); + ASSERT_FALSE(codecs.empty()); + EXPECT_STRCASEEQ("opus", codecs[0].name.c_str()); + EXPECT_EQ(48000, codecs[0].clockrate); + EXPECT_EQ(2, codecs[0].channels); + EXPECT_EQ(64000, codecs[0].bitrate); + int pref = codecs[0].preference; + for (size_t i = 1; i < codecs.size(); ++i) { + EXPECT_GT(pref, codecs[i].preference); + pref = codecs[i].preference; + } +} + +// Tests that we can find codecs by name or id, and that we interpret the +// clockrate and bitrate fields properly. +TEST_F(WebRtcVoiceEngineTestFake, FindCodec) { + cricket::AudioCodec codec; + webrtc::CodecInst codec_inst; + // Find PCMU with explicit clockrate and bitrate. + EXPECT_TRUE(engine_.FindWebRtcCodec(kPcmuCodec, &codec_inst)); + // Find ISAC with explicit clockrate and 0 bitrate. + EXPECT_TRUE(engine_.FindWebRtcCodec(kIsacCodec, &codec_inst)); + // Find telephone-event with explicit clockrate and 0 bitrate. + EXPECT_TRUE(engine_.FindWebRtcCodec(kTelephoneEventCodec, &codec_inst)); + // Find ISAC with a different payload id. + codec = kIsacCodec; + codec.id = 127; + EXPECT_TRUE(engine_.FindWebRtcCodec(codec, &codec_inst)); + EXPECT_EQ(codec.id, codec_inst.pltype); + // Find PCMU with a 0 clockrate. + codec = kPcmuCodec; + codec.clockrate = 0; + EXPECT_TRUE(engine_.FindWebRtcCodec(codec, &codec_inst)); + EXPECT_EQ(codec.id, codec_inst.pltype); + EXPECT_EQ(8000, codec_inst.plfreq); + // Find PCMU with a 0 bitrate. + codec = kPcmuCodec; + codec.bitrate = 0; + EXPECT_TRUE(engine_.FindWebRtcCodec(codec, &codec_inst)); + EXPECT_EQ(codec.id, codec_inst.pltype); + EXPECT_EQ(64000, codec_inst.rate); + // Find ISAC with an explicit bitrate. + codec = kIsacCodec; + codec.bitrate = 32000; + EXPECT_TRUE(engine_.FindWebRtcCodec(codec, &codec_inst)); + EXPECT_EQ(codec.id, codec_inst.pltype); + EXPECT_EQ(32000, codec_inst.rate); +} + +// Test that we set our inbound codecs properly, including changing PT. +TEST_F(WebRtcVoiceEngineTestFake, SetRecvCodecs) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs.push_back(kTelephoneEventCodec); + codecs[0].id = 106; // collide with existing telephone-event + codecs[2].id = 126; + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + webrtc::CodecInst gcodec; + talk_base::strcpyn(gcodec.plname, ARRAY_SIZE(gcodec.plname), "ISAC"); + gcodec.plfreq = 16000; + gcodec.channels = 1; + EXPECT_EQ(0, voe_.GetRecPayloadType(channel_num, gcodec)); + EXPECT_EQ(106, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + talk_base::strcpyn(gcodec.plname, ARRAY_SIZE(gcodec.plname), + "telephone-event"); + gcodec.plfreq = 8000; + EXPECT_EQ(0, voe_.GetRecPayloadType(channel_num, gcodec)); + EXPECT_EQ(126, gcodec.pltype); + EXPECT_STREQ("telephone-event", gcodec.plname); +} + +// Test that we fail to set an unknown inbound codec. +TEST_F(WebRtcVoiceEngineTestFake, SetRecvCodecsUnsupportedCodec) { + EXPECT_TRUE(SetupEngine()); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs.push_back(cricket::AudioCodec(127, "XYZ", 32000, 0, 1, 0)); + EXPECT_FALSE(channel_->SetRecvCodecs(codecs)); +} + +// Test that we fail if we have duplicate types in the inbound list. +TEST_F(WebRtcVoiceEngineTestFake, SetRecvCodecsDuplicatePayloadType) { + EXPECT_TRUE(SetupEngine()); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs.push_back(kCn16000Codec); + codecs[1].id = kIsacCodec.id; + EXPECT_FALSE(channel_->SetRecvCodecs(codecs)); +} + +// Test that we can decode OPUS without stereo parameters. +TEST_F(WebRtcVoiceEngineTestFake, SetRecvCodecsWithOpusNoStereo) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs.push_back(kOpusCodec); + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc1))); + int channel_num2 = voe_.GetLastChannel(); + webrtc::CodecInst opus; + engine_.FindWebRtcCodec(kOpusCodec, &opus); + // Even without stereo parameters, recv codecs still specify channels = 2. + EXPECT_EQ(2, opus.channels); + EXPECT_EQ(111, opus.pltype); + EXPECT_STREQ("opus", opus.plname); + opus.pltype = 0; + EXPECT_EQ(0, voe_.GetRecPayloadType(channel_num2, opus)); + EXPECT_EQ(111, opus.pltype); +} + +// Test that we can decode OPUS with stereo = 0. +TEST_F(WebRtcVoiceEngineTestFake, SetRecvCodecsWithOpus0Stereo) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs.push_back(kOpusCodec); + codecs[2].params["stereo"] = "0"; + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc1))); + int channel_num2 = voe_.GetLastChannel(); + webrtc::CodecInst opus; + engine_.FindWebRtcCodec(kOpusCodec, &opus); + // Even when stereo is off, recv codecs still specify channels = 2. + EXPECT_EQ(2, opus.channels); + EXPECT_EQ(111, opus.pltype); + EXPECT_STREQ("opus", opus.plname); + opus.pltype = 0; + EXPECT_EQ(0, voe_.GetRecPayloadType(channel_num2, opus)); + EXPECT_EQ(111, opus.pltype); +} + +// Test that we can decode OPUS with stereo = 1. +TEST_F(WebRtcVoiceEngineTestFake, SetRecvCodecsWithOpus1Stereo) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs.push_back(kOpusCodec); + codecs[2].params["stereo"] = "1"; + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc1))); + int channel_num2 = voe_.GetLastChannel(); + webrtc::CodecInst opus; + engine_.FindWebRtcCodec(kOpusCodec, &opus); + EXPECT_EQ(2, opus.channels); + EXPECT_EQ(111, opus.pltype); + EXPECT_STREQ("opus", opus.plname); + opus.pltype = 0; + EXPECT_EQ(0, voe_.GetRecPayloadType(channel_num2, opus)); + EXPECT_EQ(111, opus.pltype); +} + +// Test that changes to recv codecs are applied to all streams. +TEST_F(WebRtcVoiceEngineTestFake, SetRecvCodecsWithMultipleStreams) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs.push_back(kTelephoneEventCodec); + codecs[0].id = 106; // collide with existing telephone-event + codecs[2].id = 126; + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc1))); + int channel_num2 = voe_.GetLastChannel(); + webrtc::CodecInst gcodec; + talk_base::strcpyn(gcodec.plname, ARRAY_SIZE(gcodec.plname), "ISAC"); + gcodec.plfreq = 16000; + gcodec.channels = 1; + EXPECT_EQ(0, voe_.GetRecPayloadType(channel_num2, gcodec)); + EXPECT_EQ(106, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + talk_base::strcpyn(gcodec.plname, ARRAY_SIZE(gcodec.plname), + "telephone-event"); + gcodec.plfreq = 8000; + gcodec.channels = 1; + EXPECT_EQ(0, voe_.GetRecPayloadType(channel_num2, gcodec)); + EXPECT_EQ(126, gcodec.pltype); + EXPECT_STREQ("telephone-event", gcodec.plname); +} + +TEST_F(WebRtcVoiceEngineTestFake, SetRecvCodecsAfterAddingStreams) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs[0].id = 106; // collide with existing telephone-event + + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc1))); + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + + int channel_num2 = voe_.GetLastChannel(); + webrtc::CodecInst gcodec; + talk_base::strcpyn(gcodec.plname, ARRAY_SIZE(gcodec.plname), "ISAC"); + gcodec.plfreq = 16000; + gcodec.channels = 1; + EXPECT_EQ(0, voe_.GetRecPayloadType(channel_num2, gcodec)); + EXPECT_EQ(106, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); +} + +// Test that we can apply the same set of codecs again while playing. +TEST_F(WebRtcVoiceEngineTestFake, SetRecvCodecsWhilePlaying) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs.push_back(kCn16000Codec); + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + EXPECT_TRUE(channel_->SetPlayout(true)); + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + + // Changing the payload type of a codec should fail. + codecs[0].id = 127; + EXPECT_FALSE(channel_->SetRecvCodecs(codecs)); + EXPECT_TRUE(voe_.GetPlayout(channel_num)); +} + +// Test that we can add a codec while playing. +TEST_F(WebRtcVoiceEngineTestFake, AddRecvCodecsWhilePlaying) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs.push_back(kCn16000Codec); + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + EXPECT_TRUE(channel_->SetPlayout(true)); + + codecs.push_back(kOpusCodec); + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + EXPECT_TRUE(voe_.GetPlayout(channel_num)); + webrtc::CodecInst gcodec; + EXPECT_TRUE(engine_.FindWebRtcCodec(kOpusCodec, &gcodec)); + EXPECT_EQ(kOpusCodec.id, gcodec.pltype); +} + +TEST_F(WebRtcVoiceEngineTestFake, SetSendBandwidthAuto) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + + // Test that when autobw is true, bitrate is kept as the default + // value. autobw is true for the following tests. + + // ISAC, default bitrate == 32000. + TestSendBandwidth(kIsacCodec, 32000, true, 96000, true); + + // PCMU, default bitrate == 64000. + TestSendBandwidth(kPcmuCodec, 64000, true, 96000, true); + + // CELT, default bitrate == 64000. + TestSendBandwidth(kCeltCodec, 64000, true, 96000, true); + + // opus, default bitrate == 64000. + TestSendBandwidth(kOpusCodec, 64000, true, 96000, true); +} + +TEST_F(WebRtcVoiceEngineTestFake, SetSendBandwidthFixedMultiRate) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + + // Test that we can set bitrate if a multi-rate codec is used. + // autobw is false for the following tests. + + // ISAC, default bitrate == 32000. + TestSendBandwidth(kIsacCodec, 32000, false, 128000, true); + + // CELT, default bitrate == 64000. + TestSendBandwidth(kCeltCodec, 64000, false, 96000, true); + + // opus, default bitrate == 64000. + TestSendBandwidth(kOpusCodec, 64000, false, 96000, true); +} + +// Test that bitrate cannot be set for CBR codecs. +// Bitrate is ignored if it is higher than the fixed bitrate. +// Bitrate less then the fixed bitrate is an error. +TEST_F(WebRtcVoiceEngineTestFake, SetSendBandwidthFixedCbr) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetSendCodecs(engine_.codecs())); + + webrtc::CodecInst codec; + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + + // PCMU, default bitrate == 64000. + codecs.push_back(kPcmuCodec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, codec)); + EXPECT_EQ(64000, codec.rate); + EXPECT_TRUE(channel_->SetSendBandwidth(false, 128000)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, codec)); + EXPECT_EQ(64000, codec.rate); + EXPECT_FALSE(channel_->SetSendBandwidth(false, 128)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, codec)); + EXPECT_EQ(64000, codec.rate); +} + +// Test that we apply codecs properly. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecs) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs.push_back(kRedCodec); + codecs[0].id = 96; + codecs[0].bitrate = 48000; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(96, gcodec.pltype); + EXPECT_EQ(48000, gcodec.rate); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_FALSE(voe_.GetVAD(channel_num)); + EXPECT_FALSE(voe_.GetFEC(channel_num)); + EXPECT_EQ(13, voe_.GetSendCNPayloadType(channel_num, false)); + EXPECT_EQ(105, voe_.GetSendCNPayloadType(channel_num, true)); + EXPECT_EQ(106, voe_.GetSendTelephoneEventPayloadType(channel_num)); +} + +// TODO(pthatcher): Change failure behavior to returning false rather +// than defaulting to PCMU. +// Test that if clockrate is not 48000 for opus, we fail by fallback to PCMU. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecOpusBadClockrate) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].bitrate = 0; + codecs[0].clockrate = 50000; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_STREQ("PCMU", gcodec.plname); +} + +// Test that if channels=0 for opus, we fail by falling back to PCMU. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecOpusBad0ChannelsNoStereo) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].bitrate = 0; + codecs[0].channels = 0; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_STREQ("PCMU", gcodec.plname); +} + +// Test that if channels=0 for opus, we fail by falling back to PCMU. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecOpusBad0Channels1Stereo) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].bitrate = 0; + codecs[0].channels = 0; + codecs[0].params["stereo"] = "1"; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_STREQ("PCMU", gcodec.plname); +} + +// Test that if channel is 1 for opus and there's no stereo, we fail. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecOpus1ChannelNoStereo) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].bitrate = 0; + codecs[0].channels = 1; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_STREQ("PCMU", gcodec.plname); +} + +// Test that if channel is 1 for opus and stereo=0, we fail. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecOpusBad1Channel0Stereo) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].bitrate = 0; + codecs[0].channels = 1; + codecs[0].params["stereo"] = "0"; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_STREQ("PCMU", gcodec.plname); +} + +// Test that if channel is 1 for opus and stereo=1, we fail. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecOpusBad1Channel1Stereo) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].bitrate = 0; + codecs[0].channels = 1; + codecs[0].params["stereo"] = "1"; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_STREQ("PCMU", gcodec.plname); +} + +// Test that with bitrate=0 and no stereo, +// channels and bitrate are 1 and 32000. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecOpusGood0BitrateNoStereo) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].bitrate = 0; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_STREQ("opus", gcodec.plname); + EXPECT_EQ(1, gcodec.channels); + EXPECT_EQ(32000, gcodec.rate); +} + +// Test that with bitrate=0 and stereo=0, +// channels and bitrate are 1 and 32000. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecOpusGood0Bitrate0Stereo) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].bitrate = 0; + codecs[0].params["stereo"] = "0"; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_STREQ("opus", gcodec.plname); + EXPECT_EQ(1, gcodec.channels); + EXPECT_EQ(32000, gcodec.rate); +} + +// Test that with bitrate=0 and stereo=1, +// channels and bitrate are 2 and 64000. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecOpusGood0Bitrate1Stereo) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].bitrate = 0; + codecs[0].params["stereo"] = "1"; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_STREQ("opus", gcodec.plname); + EXPECT_EQ(2, gcodec.channels); + EXPECT_EQ(64000, gcodec.rate); +} + +// Test that with bitrate=N and stereo unset, +// channels and bitrate are 1 and N. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecOpusGoodNBitrateNoStereo) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].bitrate = 96000; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(111, gcodec.pltype); + EXPECT_EQ(96000, gcodec.rate); + EXPECT_STREQ("opus", gcodec.plname); + EXPECT_EQ(1, gcodec.channels); + EXPECT_EQ(48000, gcodec.plfreq); +} + +// Test that with bitrate=N and stereo=0, +// channels and bitrate are 1 and N. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecOpusGoodNBitrate0Stereo) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].bitrate = 30000; + codecs[0].params["stereo"] = "0"; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(1, gcodec.channels); + EXPECT_EQ(30000, gcodec.rate); + EXPECT_STREQ("opus", gcodec.plname); +} + +// Test that with bitrate=N and without any parameters, +// channels and bitrate are 1 and N. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecOpusGoodNBitrateNoParameters) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].bitrate = 30000; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(1, gcodec.channels); + EXPECT_EQ(30000, gcodec.rate); + EXPECT_STREQ("opus", gcodec.plname); +} + +// Test that with bitrate=N and stereo=1, +// channels and bitrate are 2 and N. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecOpusGoodNBitrate1Stereo) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].bitrate = 30000; + codecs[0].params["stereo"] = "1"; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(2, gcodec.channels); + EXPECT_EQ(30000, gcodec.rate); + EXPECT_STREQ("opus", gcodec.plname); +} + +// Test that we can enable NACK with opus. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecEnableNack) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].AddFeedbackParam(cricket::FeedbackParam(cricket::kRtcpFbParamNack, + cricket::kParamValueEmpty)); + EXPECT_FALSE(voe_.GetNACK(channel_num)); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(voe_.GetNACK(channel_num)); +} + +// Test that we can enable NACK on receive streams. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecEnableNackRecvStreams) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + int channel_num1 = voe_.GetLastChannel(); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + int channel_num2 = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].AddFeedbackParam(cricket::FeedbackParam(cricket::kRtcpFbParamNack, + cricket::kParamValueEmpty)); + EXPECT_FALSE(voe_.GetNACK(channel_num1)); + EXPECT_FALSE(voe_.GetNACK(channel_num2)); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(voe_.GetNACK(channel_num1)); + EXPECT_TRUE(voe_.GetNACK(channel_num2)); +} + +// Test that we can disable NACK. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecDisableNack) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].AddFeedbackParam(cricket::FeedbackParam(cricket::kRtcpFbParamNack, + cricket::kParamValueEmpty)); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(voe_.GetNACK(channel_num)); + + codecs.clear(); + codecs.push_back(kOpusCodec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_FALSE(voe_.GetNACK(channel_num)); +} + +// Test that we can disable NACK on receive streams. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecDisableNackRecvStreams) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + int channel_num1 = voe_.GetLastChannel(); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + int channel_num2 = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kOpusCodec); + codecs[0].AddFeedbackParam(cricket::FeedbackParam(cricket::kRtcpFbParamNack, + cricket::kParamValueEmpty)); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(voe_.GetNACK(channel_num1)); + EXPECT_TRUE(voe_.GetNACK(channel_num2)); + + codecs.clear(); + codecs.push_back(kOpusCodec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_FALSE(voe_.GetNACK(channel_num1)); + EXPECT_FALSE(voe_.GetNACK(channel_num2)); +} + +// Test that NACK is enabled on a new receive stream. +TEST_F(WebRtcVoiceEngineTestFake, AddRecvStreamEnableNack) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs[0].AddFeedbackParam(cricket::FeedbackParam(cricket::kRtcpFbParamNack, + cricket::kParamValueEmpty)); + codecs.push_back(kCn16000Codec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(voe_.GetNACK(channel_num)); + + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + channel_num = voe_.GetLastChannel(); + EXPECT_TRUE(voe_.GetNACK(channel_num)); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(3))); + channel_num = voe_.GetLastChannel(); + EXPECT_TRUE(voe_.GetNACK(channel_num)); +} + +// Test that we can apply CELT with stereo mode but fail with mono mode. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsCelt) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kCeltCodec); + codecs.push_back(kPcmuCodec); + codecs[0].id = 96; + codecs[0].channels = 2; + codecs[0].bitrate = 96000; + codecs[1].bitrate = 96000; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(96, gcodec.pltype); + EXPECT_EQ(96000, gcodec.rate); + EXPECT_EQ(2, gcodec.channels); + EXPECT_STREQ("CELT", gcodec.plname); + // Doesn't support mono, expect it to fall back to the next codec in the list. + codecs[0].channels = 1; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(0, gcodec.pltype); + EXPECT_EQ(1, gcodec.channels); + EXPECT_EQ(64000, gcodec.rate); + EXPECT_STREQ("PCMU", gcodec.plname); +} + +// Test that we can switch back and forth between CELT and ISAC with CN. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsIsacCeltSwitching) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector celt_codecs; + celt_codecs.push_back(kCeltCodec); + EXPECT_TRUE(channel_->SetSendCodecs(celt_codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(110, gcodec.pltype); + EXPECT_STREQ("CELT", gcodec.plname); + + std::vector isac_codecs; + isac_codecs.push_back(kIsacCodec); + isac_codecs.push_back(kCn16000Codec); + isac_codecs.push_back(kCeltCodec); + EXPECT_TRUE(channel_->SetSendCodecs(isac_codecs)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(103, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + + EXPECT_TRUE(channel_->SetSendCodecs(celt_codecs)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(110, gcodec.pltype); + EXPECT_STREQ("CELT", gcodec.plname); +} + +// Test that we handle various ways of specifying bitrate. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsBitrate) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kIsacCodec); // bitrate == 32000 + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(103, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_EQ(32000, gcodec.rate); + + codecs[0].bitrate = 0; // bitrate == default + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(103, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_EQ(-1, gcodec.rate); + + codecs[0].bitrate = 28000; // bitrate == 28000 + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(103, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_EQ(28000, gcodec.rate); + + codecs[0] = kPcmuCodec; // bitrate == 64000 + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(0, gcodec.pltype); + EXPECT_STREQ("PCMU", gcodec.plname); + EXPECT_EQ(64000, gcodec.rate); + + codecs[0].bitrate = 0; // bitrate == default + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(0, gcodec.pltype); + EXPECT_STREQ("PCMU", gcodec.plname); + EXPECT_EQ(64000, gcodec.rate); + + codecs[0] = kOpusCodec; + codecs[0].bitrate = 0; // bitrate == default + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(111, gcodec.pltype); + EXPECT_STREQ("opus", gcodec.plname); + EXPECT_EQ(32000, gcodec.rate); +} + +// Test that we fall back to PCMU if no codecs are specified. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsNoCodecs) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(0, gcodec.pltype); + EXPECT_STREQ("PCMU", gcodec.plname); + EXPECT_FALSE(voe_.GetVAD(channel_num)); + EXPECT_FALSE(voe_.GetFEC(channel_num)); + EXPECT_EQ(13, voe_.GetSendCNPayloadType(channel_num, false)); + EXPECT_EQ(105, voe_.GetSendCNPayloadType(channel_num, true)); + EXPECT_EQ(106, voe_.GetSendTelephoneEventPayloadType(channel_num)); +} + +// Test that we set VAD and DTMF types correctly. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsCNandDTMF) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + // TODO(juberti): cn 32000 + codecs.push_back(kCn16000Codec); + codecs.push_back(kCn8000Codec); + codecs.push_back(kTelephoneEventCodec); + codecs.push_back(kRedCodec); + codecs[0].id = 96; + codecs[2].id = 97; // wideband CN + codecs[4].id = 98; // DTMF + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(96, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_TRUE(voe_.GetVAD(channel_num)); + EXPECT_FALSE(voe_.GetFEC(channel_num)); + EXPECT_EQ(13, voe_.GetSendCNPayloadType(channel_num, false)); + EXPECT_EQ(97, voe_.GetSendCNPayloadType(channel_num, true)); + EXPECT_EQ(98, voe_.GetSendTelephoneEventPayloadType(channel_num)); +} + +// Test that we only apply VAD if we have a CN codec that matches the +// send codec clockrate. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsCNNoMatch) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + // Set ISAC(16K) and CN(16K). VAD should be activated. + codecs.push_back(kIsacCodec); + codecs.push_back(kCn16000Codec); + codecs[1].id = 97; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_TRUE(voe_.GetVAD(channel_num)); + EXPECT_EQ(97, voe_.GetSendCNPayloadType(channel_num, true)); + // Set PCMU(8K) and CN(16K). VAD should not be activated. + codecs[0] = kPcmuCodec; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_STREQ("PCMU", gcodec.plname); + EXPECT_FALSE(voe_.GetVAD(channel_num)); + // Set PCMU(8K) and CN(8K). VAD should be activated. + codecs[1] = kCn8000Codec; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_STREQ("PCMU", gcodec.plname); + EXPECT_TRUE(voe_.GetVAD(channel_num)); + EXPECT_EQ(13, voe_.GetSendCNPayloadType(channel_num, false)); + // Set ISAC(16K) and CN(8K). VAD should not be activated. + codecs[0] = kIsacCodec; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_FALSE(voe_.GetVAD(channel_num)); +} + +// Test that we perform case-insensitive matching of codec names. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsCaseInsensitive) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs.push_back(kCn16000Codec); + codecs.push_back(kCn8000Codec); + codecs.push_back(kTelephoneEventCodec); + codecs.push_back(kRedCodec); + codecs[0].name = "iSaC"; + codecs[0].id = 96; + codecs[2].id = 97; // wideband CN + codecs[4].id = 98; // DTMF + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(96, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_TRUE(voe_.GetVAD(channel_num)); + EXPECT_FALSE(voe_.GetFEC(channel_num)); + EXPECT_EQ(13, voe_.GetSendCNPayloadType(channel_num, false)); + EXPECT_EQ(97, voe_.GetSendCNPayloadType(channel_num, true)); + EXPECT_EQ(98, voe_.GetSendTelephoneEventPayloadType(channel_num)); +} + +// Test that we set up FEC correctly. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsRED) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kRedCodec); + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs[0].id = 127; + codecs[0].params[""] = "96/96"; + codecs[1].id = 96; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(96, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_TRUE(voe_.GetFEC(channel_num)); + EXPECT_EQ(127, voe_.GetSendFECPayloadType(channel_num)); +} + +// Test that we set up FEC correctly if params are omitted. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsREDNoParams) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kRedCodec); + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs[0].id = 127; + codecs[1].id = 96; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(96, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_TRUE(voe_.GetFEC(channel_num)); + EXPECT_EQ(127, voe_.GetSendFECPayloadType(channel_num)); +} + +// Test that we ignore RED if the parameters aren't named the way we expect. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsBadRED1) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kRedCodec); + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs[0].id = 127; + codecs[0].params["ABC"] = "96/96"; + codecs[1].id = 96; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(96, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_FALSE(voe_.GetFEC(channel_num)); +} + +// Test that we ignore RED if it uses different primary/secondary encoding. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsBadRED2) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kRedCodec); + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs[0].id = 127; + codecs[0].params[""] = "96/0"; + codecs[1].id = 96; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(96, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_FALSE(voe_.GetFEC(channel_num)); +} + +// Test that we ignore RED if it uses more than 2 encodings. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsBadRED3) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kRedCodec); + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs[0].id = 127; + codecs[0].params[""] = "96/96/96"; + codecs[1].id = 96; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(96, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_FALSE(voe_.GetFEC(channel_num)); +} + +// Test that we ignore RED if it has bogus codec ids. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsBadRED4) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kRedCodec); + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs[0].id = 127; + codecs[0].params[""] = "ABC/ABC"; + codecs[1].id = 96; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(96, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_FALSE(voe_.GetFEC(channel_num)); +} + +// Test that we ignore RED if it refers to a codec that is not present. +TEST_F(WebRtcVoiceEngineTestFake, SetSendCodecsBadRED5) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kRedCodec); + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + codecs[0].id = 127; + codecs[0].params[""] = "97/97"; + codecs[1].id = 96; + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + webrtc::CodecInst gcodec; + EXPECT_EQ(0, voe_.GetSendCodec(channel_num, gcodec)); + EXPECT_EQ(96, gcodec.pltype); + EXPECT_STREQ("ISAC", gcodec.plname); + EXPECT_FALSE(voe_.GetFEC(channel_num)); +} + +// Test that we support setting an empty list of recv header extensions. +TEST_F(WebRtcVoiceEngineTestFake, SetRecvRtpHeaderExtensions) { + EXPECT_TRUE(SetupEngine()); + std::vector extensions; + int channel_num = voe_.GetLastChannel(); + bool enable = false; + unsigned char id = 0; + + // An empty list shouldn't cause audio-level headers to be enabled. + EXPECT_TRUE(channel_->SetRecvRtpHeaderExtensions(extensions)); + EXPECT_EQ(0, voe_.GetRTPAudioLevelIndicationStatus( + channel_num, enable, id)); + EXPECT_FALSE(enable); + + // Nor should indicating we can receive the audio-level header. + extensions.push_back(cricket::RtpHeaderExtension( + "urn:ietf:params:rtp-hdrext:ssrc-audio-level", 8)); + EXPECT_TRUE(channel_->SetRecvRtpHeaderExtensions(extensions)); + EXPECT_EQ(0, voe_.GetRTPAudioLevelIndicationStatus( + channel_num, enable, id)); + EXPECT_FALSE(enable); +} + +// Test that we support setting certain send header extensions. +TEST_F(WebRtcVoiceEngineTestFake, SetSendRtpHeaderExtensions) { + EXPECT_TRUE(SetupEngine()); + std::vector extensions; + int channel_num = voe_.GetLastChannel(); + bool enable = false; + unsigned char id = 0; + + // Ensure audio levels are off by default. + EXPECT_EQ(0, voe_.GetRTPAudioLevelIndicationStatus( + channel_num, enable, id)); + EXPECT_FALSE(enable); + + // Ensure unknown extentions won't cause an error. + extensions.push_back(cricket::RtpHeaderExtension( + "urn:ietf:params:unknowextention", 1)); + EXPECT_TRUE(channel_->SetSendRtpHeaderExtensions(extensions)); + EXPECT_EQ(0, voe_.GetRTPAudioLevelIndicationStatus( + channel_num, enable, id)); + EXPECT_FALSE(enable); + + // Ensure audio levels stay off with an empty list of headers. + EXPECT_TRUE(channel_->SetSendRtpHeaderExtensions(extensions)); + EXPECT_EQ(0, voe_.GetRTPAudioLevelIndicationStatus( + channel_num, enable, id)); + EXPECT_FALSE(enable); + + // Ensure audio levels are enabled if the audio-level header is specified. + extensions.push_back(cricket::RtpHeaderExtension( + "urn:ietf:params:rtp-hdrext:ssrc-audio-level", 8)); + EXPECT_TRUE(channel_->SetSendRtpHeaderExtensions(extensions)); + EXPECT_EQ(0, voe_.GetRTPAudioLevelIndicationStatus( + channel_num, enable, id)); + EXPECT_TRUE(enable); + EXPECT_EQ(8, id); + + // Ensure audio levels go back off with an empty list. + extensions.clear(); + EXPECT_TRUE(channel_->SetSendRtpHeaderExtensions(extensions)); + EXPECT_EQ(0, voe_.GetRTPAudioLevelIndicationStatus( + channel_num, enable, id)); + EXPECT_FALSE(enable); +} + +// Test that we can create a channel and start sending/playing out on it. +TEST_F(WebRtcVoiceEngineTestFake, SendAndPlayout) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kPcmuCodec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(channel_->SetSend(cricket::SEND_MICROPHONE)); + EXPECT_TRUE(voe_.GetSend(channel_num)); + EXPECT_TRUE(channel_->SetPlayout(true)); + EXPECT_TRUE(voe_.GetPlayout(channel_num)); + EXPECT_TRUE(channel_->SetSend(cricket::SEND_NOTHING)); + EXPECT_FALSE(voe_.GetSend(channel_num)); + EXPECT_TRUE(channel_->SetPlayout(false)); + EXPECT_FALSE(voe_.GetPlayout(channel_num)); +} + +// Test that we can add and remove streams, and do proper send/playout. +// We can receive on multiple streams, but will only send on one. +TEST_F(WebRtcVoiceEngineTestFake, SendAndPlayoutWithMultipleStreams) { + EXPECT_TRUE(SetupEngine()); + int channel_num1 = voe_.GetLastChannel(); + + // Start playout on the default channel. + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + EXPECT_TRUE(channel_->SetPlayout(true)); + EXPECT_TRUE(voe_.GetPlayout(channel_num1)); + + // Adding another stream should disable playout on the default channel. + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + int channel_num2 = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kPcmuCodec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(channel_->SetSend(cricket::SEND_MICROPHONE)); + EXPECT_TRUE(voe_.GetSend(channel_num1)); + EXPECT_FALSE(voe_.GetSend(channel_num2)); + + // Make sure only the new channel is played out. + EXPECT_FALSE(voe_.GetPlayout(channel_num1)); + EXPECT_TRUE(voe_.GetPlayout(channel_num2)); + + // Adding yet another stream should have stream 2 and 3 enabled for playout. + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(3))); + int channel_num3 = voe_.GetLastChannel(); + EXPECT_FALSE(voe_.GetPlayout(channel_num1)); + EXPECT_TRUE(voe_.GetPlayout(channel_num2)); + EXPECT_TRUE(voe_.GetPlayout(channel_num3)); + EXPECT_FALSE(voe_.GetSend(channel_num3)); + + // Stop sending. + EXPECT_TRUE(channel_->SetSend(cricket::SEND_NOTHING)); + EXPECT_FALSE(voe_.GetSend(channel_num1)); + EXPECT_FALSE(voe_.GetSend(channel_num2)); + EXPECT_FALSE(voe_.GetSend(channel_num3)); + + // Stop playout. + EXPECT_TRUE(channel_->SetPlayout(false)); + EXPECT_FALSE(voe_.GetPlayout(channel_num1)); + EXPECT_FALSE(voe_.GetPlayout(channel_num2)); + EXPECT_FALSE(voe_.GetPlayout(channel_num3)); + + // Restart playout and make sure the default channel still is not played out. + EXPECT_TRUE(channel_->SetPlayout(true)); + EXPECT_FALSE(voe_.GetPlayout(channel_num1)); + EXPECT_TRUE(voe_.GetPlayout(channel_num2)); + EXPECT_TRUE(voe_.GetPlayout(channel_num3)); + + // Now remove the new streams and verify that the default channel is + // played out again. + EXPECT_TRUE(channel_->RemoveRecvStream(3)); + EXPECT_TRUE(channel_->RemoveRecvStream(2)); + + EXPECT_TRUE(voe_.GetPlayout(channel_num1)); +} + +// Test that we can set the devices to use. +TEST_F(WebRtcVoiceEngineTestFake, SetDevices) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kPcmuCodec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + cricket::Device default_dev(cricket::kFakeDefaultDeviceName, + cricket::kFakeDefaultDeviceId); + cricket::Device dev(cricket::kFakeDeviceName, + cricket::kFakeDeviceId); + + // Test SetDevices() while not sending or playing. + EXPECT_TRUE(engine_.SetDevices(&default_dev, &default_dev)); + + // Test SetDevices() while sending and playing. + EXPECT_TRUE(engine_.SetLocalMonitor(true)); + EXPECT_TRUE(channel_->SetSend(cricket::SEND_MICROPHONE)); + EXPECT_TRUE(channel_->SetPlayout(true)); + EXPECT_TRUE(voe_.GetRecordingMicrophone()); + EXPECT_TRUE(voe_.GetSend(channel_num)); + EXPECT_TRUE(voe_.GetPlayout(channel_num)); + + EXPECT_TRUE(engine_.SetDevices(&dev, &dev)); + + EXPECT_TRUE(voe_.GetRecordingMicrophone()); + EXPECT_TRUE(voe_.GetSend(channel_num)); + EXPECT_TRUE(voe_.GetPlayout(channel_num)); + + // Test that failure to open newly selected devices does not prevent opening + // ones after that. + voe_.set_fail_start_recording_microphone(true); + voe_.set_playout_fail_channel(channel_num); + voe_.set_send_fail_channel(channel_num); + + EXPECT_FALSE(engine_.SetDevices(&default_dev, &default_dev)); + + EXPECT_FALSE(voe_.GetRecordingMicrophone()); + EXPECT_FALSE(voe_.GetSend(channel_num)); + EXPECT_FALSE(voe_.GetPlayout(channel_num)); + + voe_.set_fail_start_recording_microphone(false); + voe_.set_playout_fail_channel(-1); + voe_.set_send_fail_channel(-1); + + EXPECT_TRUE(engine_.SetDevices(&dev, &dev)); + + EXPECT_TRUE(voe_.GetRecordingMicrophone()); + EXPECT_TRUE(voe_.GetSend(channel_num)); + EXPECT_TRUE(voe_.GetPlayout(channel_num)); +} + +// Test that we can set the devices to use even if we failed to +// open the initial ones. +TEST_F(WebRtcVoiceEngineTestFake, SetDevicesWithInitiallyBadDevices) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + std::vector codecs; + codecs.push_back(kPcmuCodec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + cricket::Device default_dev(cricket::kFakeDefaultDeviceName, + cricket::kFakeDefaultDeviceId); + cricket::Device dev(cricket::kFakeDeviceName, + cricket::kFakeDeviceId); + + // Test that failure to open devices selected before starting + // send/play does not prevent opening newly selected ones after that. + voe_.set_fail_start_recording_microphone(true); + voe_.set_playout_fail_channel(channel_num); + voe_.set_send_fail_channel(channel_num); + + EXPECT_TRUE(engine_.SetDevices(&default_dev, &default_dev)); + + EXPECT_FALSE(engine_.SetLocalMonitor(true)); + EXPECT_FALSE(channel_->SetSend(cricket::SEND_MICROPHONE)); + EXPECT_FALSE(channel_->SetPlayout(true)); + EXPECT_FALSE(voe_.GetRecordingMicrophone()); + EXPECT_FALSE(voe_.GetSend(channel_num)); + EXPECT_FALSE(voe_.GetPlayout(channel_num)); + + voe_.set_fail_start_recording_microphone(false); + voe_.set_playout_fail_channel(-1); + voe_.set_send_fail_channel(-1); + + EXPECT_TRUE(engine_.SetDevices(&dev, &dev)); + + EXPECT_TRUE(voe_.GetRecordingMicrophone()); + EXPECT_TRUE(voe_.GetSend(channel_num)); + EXPECT_TRUE(voe_.GetPlayout(channel_num)); +} + +// Test that we can create a channel configured for multi-point conferences, +// and start sending/playing out on it. +TEST_F(WebRtcVoiceEngineTestFake, ConferenceSendAndPlayout) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + std::vector codecs; + codecs.push_back(kPcmuCodec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(channel_->SetSend(cricket::SEND_MICROPHONE)); + EXPECT_TRUE(voe_.GetSend(channel_num)); +} + +// Test that we can create a channel configured for Codian bridges, +// and start sending/playing out on it. +TEST_F(WebRtcVoiceEngineTestFake, CodianSendAndPlayout) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + webrtc::AgcConfig agc_config; + EXPECT_EQ(0, voe_.GetAgcConfig(agc_config)); + EXPECT_EQ(0, agc_config.targetLeveldBOv); + EXPECT_TRUE(channel_->SetOptions(options_adjust_agc_)); + std::vector codecs; + codecs.push_back(kPcmuCodec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(channel_->SetSend(cricket::SEND_MICROPHONE)); + EXPECT_TRUE(voe_.GetSend(channel_num)); + EXPECT_EQ(0, voe_.GetAgcConfig(agc_config)); + EXPECT_EQ(agc_config.targetLeveldBOv, 10); // level was attenuated + EXPECT_TRUE(channel_->SetPlayout(true)); + EXPECT_TRUE(voe_.GetPlayout(channel_num)); + EXPECT_TRUE(channel_->SetSend(cricket::SEND_NOTHING)); + EXPECT_FALSE(voe_.GetSend(channel_num)); + EXPECT_EQ(0, voe_.GetAgcConfig(agc_config)); + EXPECT_EQ(0, agc_config.targetLeveldBOv); // level was restored + EXPECT_TRUE(channel_->SetPlayout(false)); + EXPECT_FALSE(voe_.GetPlayout(channel_num)); +} + +// Test that we can set the outgoing SSRC properly. +// SSRC is set in SetupEngine by calling AddSendStream. +TEST_F(WebRtcVoiceEngineTestFake, SetSendSsrc) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + unsigned int send_ssrc; + EXPECT_EQ(0, voe_.GetLocalSSRC(channel_num, send_ssrc)); + EXPECT_NE(0U, send_ssrc); + EXPECT_EQ(0, voe_.GetLocalSSRC(channel_num, send_ssrc)); + EXPECT_EQ(kSsrc1, send_ssrc); +} + +TEST_F(WebRtcVoiceEngineTestFake, GetStats) { + // Setup. We need send codec to be set to get all stats. + EXPECT_TRUE(SetupEngine()); + std::vector codecs; + codecs.push_back(kPcmuCodec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + + cricket::VoiceMediaInfo info; + EXPECT_EQ(true, channel_->GetStats(&info)); + EXPECT_EQ(1u, info.senders.size()); + EXPECT_EQ(kSsrc1, info.senders[0].ssrc); + EXPECT_EQ(kPcmuCodec.name, info.senders[0].codec_name); + EXPECT_EQ(cricket::kIntStatValue, info.senders[0].bytes_sent); + EXPECT_EQ(cricket::kIntStatValue, info.senders[0].packets_sent); + EXPECT_EQ(cricket::kIntStatValue, info.senders[0].packets_lost); + EXPECT_EQ(cricket::kFractionLostStatValue, info.senders[0].fraction_lost); + EXPECT_EQ(cricket::kIntStatValue, info.senders[0].ext_seqnum); + EXPECT_EQ(cricket::kIntStatValue, info.senders[0].rtt_ms); + EXPECT_EQ(cricket::kIntStatValue, info.senders[0].jitter_ms); + // TODO(sriniv): Add testing for more fields. These are not populated + // in FakeWebrtcVoiceEngine yet. + // EXPECT_EQ(cricket::kIntStatValue, info.senders[0].audio_level); + // EXPECT_EQ(cricket::kIntStatValue, info.senders[0].echo_delay_median_ms); + // EXPECT_EQ(cricket::kIntStatValue, info.senders[0].echo_delay_std_ms); + // EXPECT_EQ(cricket::kIntStatValue, info.senders[0].echo_return_loss); + // EXPECT_EQ(cricket::kIntStatValue, + // info.senders[0].echo_return_loss_enhancement); + + EXPECT_EQ(1u, info.receivers.size()); + // TODO(sriniv): Add testing for receiver fields. +} + +// Test that we can set the outgoing SSRC properly with multiple streams. +// SSRC is set in SetupEngine by calling AddSendStream. +TEST_F(WebRtcVoiceEngineTestFake, SetSendSsrcWithMultipleStreams) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + int channel_num1 = voe_.GetLastChannel(); + unsigned int send_ssrc; + EXPECT_EQ(0, voe_.GetLocalSSRC(channel_num1, send_ssrc)); + EXPECT_EQ(kSsrc1, send_ssrc); + + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + int channel_num2 = voe_.GetLastChannel(); + EXPECT_EQ(0, voe_.GetLocalSSRC(channel_num2, send_ssrc)); + EXPECT_EQ(kSsrc1, send_ssrc); +} + +// Test that the local SSRC is the same on sending and receiving channels if the +// receive channel is created before the send channel. +TEST_F(WebRtcVoiceEngineTestFake, SetSendSsrcAfterCreatingReceiveChannel) { + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + channel_ = engine_.CreateChannel(); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(1))); + int receive_channel_num = voe_.GetLastChannel(); + EXPECT_TRUE(channel_->AddSendStream( + cricket::StreamParams::CreateLegacy(1234))); + int send_channel_num = voe_.GetLastChannel(); + + unsigned int ssrc = 0; + EXPECT_EQ(0, voe_.GetLocalSSRC(send_channel_num, ssrc)); + EXPECT_EQ(1234U, ssrc); + ssrc = 0; + EXPECT_EQ(0, voe_.GetLocalSSRC(receive_channel_num, ssrc)); + EXPECT_EQ(1234U, ssrc); +} + +// Test that we can properly receive packets. +TEST_F(WebRtcVoiceEngineTestFake, Recv) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + DeliverPacket(kPcmuFrame, sizeof(kPcmuFrame)); + EXPECT_TRUE(voe_.CheckPacket(channel_num, kPcmuFrame, + sizeof(kPcmuFrame))); +} + +// Test that we can properly receive packets on multiple streams. +TEST_F(WebRtcVoiceEngineTestFake, RecvWithMultipleStreams) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(1))); + int channel_num1 = voe_.GetLastChannel(); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + int channel_num2 = voe_.GetLastChannel(); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(3))); + int channel_num3 = voe_.GetLastChannel(); + // Create packets with the right SSRCs. + char packets[4][sizeof(kPcmuFrame)]; + for (size_t i = 0; i < ARRAY_SIZE(packets); ++i) { + memcpy(packets[i], kPcmuFrame, sizeof(kPcmuFrame)); + talk_base::SetBE32(packets[i] + 8, i); + } + EXPECT_TRUE(voe_.CheckNoPacket(channel_num1)); + EXPECT_TRUE(voe_.CheckNoPacket(channel_num2)); + EXPECT_TRUE(voe_.CheckNoPacket(channel_num3)); + DeliverPacket(packets[0], sizeof(packets[0])); + EXPECT_TRUE(voe_.CheckNoPacket(channel_num1)); + EXPECT_TRUE(voe_.CheckNoPacket(channel_num2)); + EXPECT_TRUE(voe_.CheckNoPacket(channel_num3)); + DeliverPacket(packets[1], sizeof(packets[1])); + EXPECT_TRUE(voe_.CheckPacket(channel_num1, packets[1], + sizeof(packets[1]))); + EXPECT_TRUE(voe_.CheckNoPacket(channel_num2)); + EXPECT_TRUE(voe_.CheckNoPacket(channel_num3)); + DeliverPacket(packets[2], sizeof(packets[2])); + EXPECT_TRUE(voe_.CheckNoPacket(channel_num1)); + EXPECT_TRUE(voe_.CheckPacket(channel_num2, packets[2], + sizeof(packets[2]))); + EXPECT_TRUE(voe_.CheckNoPacket(channel_num3)); + DeliverPacket(packets[3], sizeof(packets[3])); + EXPECT_TRUE(voe_.CheckNoPacket(channel_num1)); + EXPECT_TRUE(voe_.CheckNoPacket(channel_num2)); + EXPECT_TRUE(voe_.CheckPacket(channel_num3, packets[3], + sizeof(packets[3]))); + EXPECT_TRUE(channel_->RemoveRecvStream(3)); + EXPECT_TRUE(channel_->RemoveRecvStream(2)); + EXPECT_TRUE(channel_->RemoveRecvStream(1)); +} + +// Test that we properly handle failures to add a stream. +TEST_F(WebRtcVoiceEngineTestFake, AddStreamFail) { + EXPECT_TRUE(SetupEngine()); + voe_.set_fail_create_channel(true); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + EXPECT_FALSE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + + // In 1:1 call, we should not try to create a new channel. + cricket::AudioOptions options_no_conference_; + options_no_conference_.conference_mode.Set(false); + EXPECT_TRUE(channel_->SetOptions(options_no_conference_)); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); +} + +// Test that AddRecvStream doesn't create new channel for 1:1 call. +TEST_F(WebRtcVoiceEngineTestFake, AddRecvStream1On1) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(1))); + EXPECT_EQ(channel_num, voe_.GetLastChannel()); +} + +// Test that after adding a recv stream, we do not decode more codecs than +// those previously passed into SetRecvCodecs. +TEST_F(WebRtcVoiceEngineTestFake, AddRecvStreamUnsupportedCodec) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + std::vector codecs; + codecs.push_back(kIsacCodec); + codecs.push_back(kPcmuCodec); + EXPECT_TRUE(channel_->SetRecvCodecs(codecs)); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc1))); + int channel_num2 = voe_.GetLastChannel(); + webrtc::CodecInst gcodec; + talk_base::strcpyn(gcodec.plname, ARRAY_SIZE(gcodec.plname), "CELT"); + gcodec.plfreq = 32000; + gcodec.channels = 2; + EXPECT_EQ(-1, voe_.GetRecPayloadType(channel_num2, gcodec)); +} + +// Test that we properly clean up any streams that were added, even if +// not explicitly removed. +TEST_F(WebRtcVoiceEngineTestFake, StreamCleanup) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(1))); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + EXPECT_EQ(3, voe_.GetNumChannels()); // default channel + 2 added + delete channel_; + channel_ = NULL; + EXPECT_EQ(0, voe_.GetNumChannels()); +} + +// Test the InsertDtmf on default send stream. +TEST_F(WebRtcVoiceEngineTestFake, InsertDtmfOnDefaultSendStream) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + TestInsertDtmf(0, channel_num); +} + +// Test the InsertDtmf on specified send stream. +TEST_F(WebRtcVoiceEngineTestFake, InsertDtmfOnSendStream) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + TestInsertDtmf(kSsrc1, channel_num); +} + +// Test that we can play a ringback tone properly in a single-stream call. +TEST_F(WebRtcVoiceEngineTestFake, PlayRingback) { + EXPECT_TRUE(SetupEngine()); + int channel_num = voe_.GetLastChannel(); + EXPECT_EQ(0, voe_.IsPlayingFileLocally(channel_num)); + // Check we fail if no ringback tone specified. + EXPECT_FALSE(channel_->PlayRingbackTone(0, true, true)); + EXPECT_EQ(0, voe_.IsPlayingFileLocally(channel_num)); + // Check we can set and play a ringback tone. + EXPECT_TRUE(channel_->SetRingbackTone(kRingbackTone, strlen(kRingbackTone))); + EXPECT_TRUE(channel_->PlayRingbackTone(0, true, true)); + EXPECT_EQ(1, voe_.IsPlayingFileLocally(channel_num)); + // Check we can stop the tone manually. + EXPECT_TRUE(channel_->PlayRingbackTone(0, false, false)); + EXPECT_EQ(0, voe_.IsPlayingFileLocally(channel_num)); + // Check we stop the tone if a packet arrives. + EXPECT_TRUE(channel_->PlayRingbackTone(0, true, true)); + EXPECT_EQ(1, voe_.IsPlayingFileLocally(channel_num)); + DeliverPacket(kPcmuFrame, sizeof(kPcmuFrame)); + EXPECT_EQ(0, voe_.IsPlayingFileLocally(channel_num)); +} + +// Test that we can play a ringback tone properly in a multi-stream call. +TEST_F(WebRtcVoiceEngineTestFake, PlayRingbackWithMultipleStreams) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(1))); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + int channel_num = voe_.GetLastChannel(); + EXPECT_EQ(0, voe_.IsPlayingFileLocally(channel_num)); + // Check we fail if no ringback tone specified. + EXPECT_FALSE(channel_->PlayRingbackTone(2, true, true)); + EXPECT_EQ(0, voe_.IsPlayingFileLocally(channel_num)); + // Check we can set and play a ringback tone on the correct ssrc. + EXPECT_TRUE(channel_->SetRingbackTone(kRingbackTone, strlen(kRingbackTone))); + EXPECT_FALSE(channel_->PlayRingbackTone(77, true, true)); + EXPECT_TRUE(channel_->PlayRingbackTone(2, true, true)); + EXPECT_EQ(1, voe_.IsPlayingFileLocally(channel_num)); + // Check we can stop the tone manually. + EXPECT_TRUE(channel_->PlayRingbackTone(2, false, false)); + EXPECT_EQ(0, voe_.IsPlayingFileLocally(channel_num)); + // Check we stop the tone if a packet arrives, but only with the right SSRC. + EXPECT_TRUE(channel_->PlayRingbackTone(2, true, true)); + EXPECT_EQ(1, voe_.IsPlayingFileLocally(channel_num)); + // Send a packet with SSRC 1; the tone should not stop. + DeliverPacket(kPcmuFrame, sizeof(kPcmuFrame)); + EXPECT_EQ(1, voe_.IsPlayingFileLocally(channel_num)); + // Send a packet with SSRC 2; the tone should stop. + char packet[sizeof(kPcmuFrame)]; + memcpy(packet, kPcmuFrame, sizeof(kPcmuFrame)); + talk_base::SetBE32(packet + 8, 2); + DeliverPacket(packet, sizeof(packet)); + EXPECT_EQ(0, voe_.IsPlayingFileLocally(channel_num)); +} + +// Tests creating soundclips, and make sure they come from the right engine. +TEST_F(WebRtcVoiceEngineTestFake, CreateSoundclip) { + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + soundclip_ = engine_.CreateSoundclip(); + ASSERT_TRUE(soundclip_ != NULL); + EXPECT_EQ(0, voe_.GetNumChannels()); + EXPECT_EQ(1, voe_sc_.GetNumChannels()); + int channel_num = voe_sc_.GetLastChannel(); + EXPECT_TRUE(voe_sc_.GetPlayout(channel_num)); + delete soundclip_; + soundclip_ = NULL; + EXPECT_EQ(0, voe_sc_.GetNumChannels()); +} + +// Tests playing out a fake sound. +TEST_F(WebRtcVoiceEngineTestFake, PlaySoundclip) { + static const char kZeroes[16000] = {}; + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + soundclip_ = engine_.CreateSoundclip(); + ASSERT_TRUE(soundclip_ != NULL); + EXPECT_TRUE(soundclip_->PlaySound(kZeroes, sizeof(kZeroes), 0)); +} + +TEST_F(WebRtcVoiceEngineTestFake, MediaEngineCallbackOnError) { + talk_base::scoped_ptr listener; + cricket::WebRtcVoiceMediaChannel* media_channel; + unsigned int ssrc = 0; + + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + EXPECT_TRUE(channel_->SetSend(cricket::SEND_MICROPHONE)); + + media_channel = static_cast(channel_); + listener.reset(new ChannelErrorListener(channel_)); + + // Test on WebRtc VoE channel. + voe_.TriggerCallbackOnError(media_channel->voe_channel(), + VE_SATURATION_WARNING); + EXPECT_EQ(cricket::VoiceMediaChannel::ERROR_REC_DEVICE_SATURATION, + listener->error()); + EXPECT_NE(-1, voe_.GetLocalSSRC(voe_.GetLastChannel(), ssrc)); + EXPECT_EQ(ssrc, listener->ssrc()); + + listener->Reset(); + voe_.TriggerCallbackOnError(-1, VE_TYPING_NOISE_WARNING); + EXPECT_EQ(cricket::VoiceMediaChannel::ERROR_REC_TYPING_NOISE_DETECTED, + listener->error()); + EXPECT_EQ(0U, listener->ssrc()); + + // Add another stream and test on that. + ++ssrc; + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy( + ssrc))); + listener->Reset(); + voe_.TriggerCallbackOnError(voe_.GetLastChannel(), + VE_SATURATION_WARNING); + EXPECT_EQ(cricket::VoiceMediaChannel::ERROR_REC_DEVICE_SATURATION, + listener->error()); + EXPECT_EQ(ssrc, listener->ssrc()); + + // Testing a non-existing channel. + listener->Reset(); + voe_.TriggerCallbackOnError(voe_.GetLastChannel() + 2, + VE_SATURATION_WARNING); + EXPECT_EQ(0, listener->error()); +} + +TEST_F(WebRtcVoiceEngineTestFake, TestSetPlayoutError) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + std::vector codecs; + codecs.push_back(kPcmuCodec); + EXPECT_TRUE(channel_->SetSendCodecs(codecs)); + EXPECT_TRUE(channel_->SetSend(cricket::SEND_MICROPHONE)); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(2))); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(3))); + EXPECT_TRUE(channel_->SetPlayout(true)); + voe_.set_playout_fail_channel(voe_.GetLastChannel() - 1); + EXPECT_TRUE(channel_->SetPlayout(false)); + EXPECT_FALSE(channel_->SetPlayout(true)); +} + +// Test that the Registering/Unregistering with the +// webrtcvoiceengine works as expected +TEST_F(WebRtcVoiceEngineTestFake, RegisterVoiceProcessor) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + EXPECT_TRUE(channel_->AddRecvStream( + cricket::StreamParams::CreateLegacy(kSsrc2))); + cricket::FakeMediaProcessor vp_1; + cricket::FakeMediaProcessor vp_2; + + EXPECT_FALSE(engine_.RegisterProcessor(kSsrc2, &vp_1, cricket::MPD_TX)); + EXPECT_TRUE(engine_.RegisterProcessor(kSsrc2, &vp_1, cricket::MPD_RX)); + EXPECT_TRUE(engine_.RegisterProcessor(kSsrc2, &vp_2, cricket::MPD_RX)); + voe_.TriggerProcessPacket(cricket::MPD_RX); + voe_.TriggerProcessPacket(cricket::MPD_TX); + + EXPECT_TRUE(voe_.IsExternalMediaProcessorRegistered()); + EXPECT_EQ(1, vp_1.voice_frame_count()); + EXPECT_EQ(1, vp_2.voice_frame_count()); + + EXPECT_TRUE(engine_.UnregisterProcessor(kSsrc2, + &vp_2, + cricket::MPD_RX)); + voe_.TriggerProcessPacket(cricket::MPD_RX); + EXPECT_TRUE(voe_.IsExternalMediaProcessorRegistered()); + EXPECT_EQ(1, vp_2.voice_frame_count()); + EXPECT_EQ(2, vp_1.voice_frame_count()); + + EXPECT_TRUE(engine_.UnregisterProcessor(kSsrc2, + &vp_1, + cricket::MPD_RX)); + voe_.TriggerProcessPacket(cricket::MPD_RX); + EXPECT_FALSE(voe_.IsExternalMediaProcessorRegistered()); + EXPECT_EQ(2, vp_1.voice_frame_count()); + + EXPECT_FALSE(engine_.RegisterProcessor(kSsrc1, &vp_1, cricket::MPD_RX)); + EXPECT_TRUE(engine_.RegisterProcessor(kSsrc1, &vp_1, cricket::MPD_TX)); + voe_.TriggerProcessPacket(cricket::MPD_RX); + voe_.TriggerProcessPacket(cricket::MPD_TX); + EXPECT_TRUE(voe_.IsExternalMediaProcessorRegistered()); + EXPECT_EQ(3, vp_1.voice_frame_count()); + + EXPECT_TRUE(engine_.UnregisterProcessor(kSsrc1, + &vp_1, + cricket::MPD_RX_AND_TX)); + voe_.TriggerProcessPacket(cricket::MPD_TX); + EXPECT_FALSE(voe_.IsExternalMediaProcessorRegistered()); + EXPECT_EQ(3, vp_1.voice_frame_count()); + EXPECT_TRUE(channel_->RemoveRecvStream(kSsrc2)); + EXPECT_FALSE(engine_.RegisterProcessor(kSsrc2, &vp_1, cricket::MPD_RX)); + EXPECT_FALSE(voe_.IsExternalMediaProcessorRegistered()); + + // Test that we can register a processor on the receive channel on SSRC 0. + // This tests the 1:1 case when the receive SSRC is unknown. + EXPECT_TRUE(engine_.RegisterProcessor(0, &vp_1, cricket::MPD_RX)); + voe_.TriggerProcessPacket(cricket::MPD_RX); + EXPECT_TRUE(voe_.IsExternalMediaProcessorRegistered()); + EXPECT_EQ(4, vp_1.voice_frame_count()); + EXPECT_TRUE(engine_.UnregisterProcessor(0, + &vp_1, + cricket::MPD_RX)); + + // The following tests test that FindChannelNumFromSsrc is doing + // what we expect. + // pick an invalid ssrc and make sure we can't register + EXPECT_FALSE(engine_.RegisterProcessor(99, + &vp_1, + cricket::MPD_RX)); + EXPECT_TRUE(channel_->AddRecvStream(cricket::StreamParams::CreateLegacy(1))); + EXPECT_TRUE(engine_.RegisterProcessor(1, + &vp_1, + cricket::MPD_RX)); + EXPECT_TRUE(engine_.UnregisterProcessor(1, + &vp_1, + cricket::MPD_RX)); + EXPECT_FALSE(engine_.RegisterProcessor(1, + &vp_1, + cricket::MPD_TX)); + EXPECT_TRUE(channel_->RemoveRecvStream(1)); +} + +TEST_F(WebRtcVoiceEngineTestFake, SetAudioOptions) { + EXPECT_TRUE(SetupEngine()); + + bool ec_enabled; + webrtc::EcModes ec_mode; + bool ec_metrics_enabled; + webrtc::AecmModes aecm_mode; + bool cng_enabled; + bool agc_enabled; + webrtc::AgcModes agc_mode; + webrtc::AgcConfig agc_config; + bool ns_enabled; + webrtc::NsModes ns_mode; + bool highpass_filter_enabled; + bool stereo_swapping_enabled; + bool typing_detection_enabled; + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetEcMetricsStatus(ec_metrics_enabled); + voe_.GetAecmMode(aecm_mode, cng_enabled); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetAgcConfig(agc_config); + voe_.GetNsStatus(ns_enabled, ns_mode); + highpass_filter_enabled = voe_.IsHighPassFilterEnabled(); + stereo_swapping_enabled = voe_.IsStereoChannelSwappingEnabled(); + voe_.GetTypingDetectionStatus(typing_detection_enabled); + EXPECT_TRUE(ec_enabled); + EXPECT_TRUE(ec_metrics_enabled); + EXPECT_FALSE(cng_enabled); + EXPECT_TRUE(agc_enabled); + EXPECT_EQ(0, agc_config.targetLeveldBOv); + EXPECT_TRUE(ns_enabled); + EXPECT_TRUE(highpass_filter_enabled); + EXPECT_FALSE(stereo_swapping_enabled); + EXPECT_TRUE(typing_detection_enabled); + EXPECT_EQ(ec_mode, webrtc::kEcConference); + EXPECT_EQ(ns_mode, webrtc::kNsHighSuppression); + + // Nothing set, so all ignored. + cricket::AudioOptions options; + ASSERT_TRUE(engine_.SetAudioOptions(options)); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetEcMetricsStatus(ec_metrics_enabled); + voe_.GetAecmMode(aecm_mode, cng_enabled); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetAgcConfig(agc_config); + voe_.GetNsStatus(ns_enabled, ns_mode); + highpass_filter_enabled = voe_.IsHighPassFilterEnabled(); + stereo_swapping_enabled = voe_.IsStereoChannelSwappingEnabled(); + voe_.GetTypingDetectionStatus(typing_detection_enabled); + EXPECT_TRUE(ec_enabled); + EXPECT_TRUE(ec_metrics_enabled); + EXPECT_FALSE(cng_enabled); + EXPECT_TRUE(agc_enabled); + EXPECT_EQ(0, agc_config.targetLeveldBOv); + EXPECT_TRUE(ns_enabled); + EXPECT_TRUE(highpass_filter_enabled); + EXPECT_FALSE(stereo_swapping_enabled); + EXPECT_TRUE(typing_detection_enabled); + EXPECT_EQ(ec_mode, webrtc::kEcConference); + EXPECT_EQ(ns_mode, webrtc::kNsHighSuppression); + + // Turn echo cancellation off + options.echo_cancellation.Set(false); + ASSERT_TRUE(engine_.SetAudioOptions(options)); + voe_.GetEcStatus(ec_enabled, ec_mode); + EXPECT_FALSE(ec_enabled); + + // Turn echo cancellation back on, with settings, and make sure + // nothing else changed. + options.echo_cancellation.Set(true); + ASSERT_TRUE(engine_.SetAudioOptions(options)); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetEcMetricsStatus(ec_metrics_enabled); + voe_.GetAecmMode(aecm_mode, cng_enabled); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetAgcConfig(agc_config); + voe_.GetNsStatus(ns_enabled, ns_mode); + highpass_filter_enabled = voe_.IsHighPassFilterEnabled(); + stereo_swapping_enabled = voe_.IsStereoChannelSwappingEnabled(); + voe_.GetTypingDetectionStatus(typing_detection_enabled); + EXPECT_TRUE(ec_enabled); + EXPECT_TRUE(ec_metrics_enabled); + EXPECT_TRUE(agc_enabled); + EXPECT_EQ(0, agc_config.targetLeveldBOv); + EXPECT_TRUE(ns_enabled); + EXPECT_TRUE(highpass_filter_enabled); + EXPECT_FALSE(stereo_swapping_enabled); + EXPECT_TRUE(typing_detection_enabled); + EXPECT_EQ(ec_mode, webrtc::kEcConference); + EXPECT_EQ(ns_mode, webrtc::kNsHighSuppression); + + // Turn off AGC + options.auto_gain_control.Set(false); + ASSERT_TRUE(engine_.SetAudioOptions(options)); + voe_.GetAgcStatus(agc_enabled, agc_mode); + EXPECT_FALSE(agc_enabled); + + // Turn AGC back on + options.auto_gain_control.Set(true); + options.adjust_agc_delta.Clear(); + ASSERT_TRUE(engine_.SetAudioOptions(options)); + voe_.GetAgcStatus(agc_enabled, agc_mode); + EXPECT_TRUE(agc_enabled); + voe_.GetAgcConfig(agc_config); + EXPECT_EQ(0, agc_config.targetLeveldBOv); + + // Turn off other options (and stereo swapping on). + options.noise_suppression.Set(false); + options.highpass_filter.Set(false); + options.typing_detection.Set(false); + options.stereo_swapping.Set(true); + ASSERT_TRUE(engine_.SetAudioOptions(options)); + voe_.GetNsStatus(ns_enabled, ns_mode); + highpass_filter_enabled = voe_.IsHighPassFilterEnabled(); + stereo_swapping_enabled = voe_.IsStereoChannelSwappingEnabled(); + voe_.GetTypingDetectionStatus(typing_detection_enabled); + EXPECT_FALSE(ns_enabled); + EXPECT_FALSE(highpass_filter_enabled); + EXPECT_FALSE(typing_detection_enabled); + EXPECT_TRUE(stereo_swapping_enabled); + + // Turn on "conference mode" to ensure it has no impact. + options.conference_mode.Set(true); + ASSERT_TRUE(engine_.SetAudioOptions(options)); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + EXPECT_TRUE(ec_enabled); + EXPECT_EQ(webrtc::kEcConference, ec_mode); + EXPECT_FALSE(ns_enabled); + EXPECT_EQ(webrtc::kNsHighSuppression, ns_mode); +} + +TEST_F(WebRtcVoiceEngineTestFake, SetOptions) { + EXPECT_TRUE(SetupEngine()); + + bool ec_enabled; + webrtc::EcModes ec_mode; + bool ec_metrics_enabled; + bool agc_enabled; + webrtc::AgcModes agc_mode; + bool ns_enabled; + webrtc::NsModes ns_mode; + bool highpass_filter_enabled; + bool stereo_swapping_enabled; + bool typing_detection_enabled; + + ASSERT_TRUE(engine_.SetOptions(0)); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetEcMetricsStatus(ec_metrics_enabled); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + highpass_filter_enabled = voe_.IsHighPassFilterEnabled(); + stereo_swapping_enabled = voe_.IsStereoChannelSwappingEnabled(); + voe_.GetTypingDetectionStatus(typing_detection_enabled); + EXPECT_FALSE(ec_enabled); + EXPECT_FALSE(agc_enabled); + EXPECT_FALSE(ns_enabled); + EXPECT_FALSE(highpass_filter_enabled); + EXPECT_FALSE(stereo_swapping_enabled); + EXPECT_TRUE(typing_detection_enabled); + + ASSERT_TRUE(engine_.SetOptions( + cricket::MediaEngineInterface::ECHO_CANCELLATION)); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetEcMetricsStatus(ec_metrics_enabled); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + highpass_filter_enabled = voe_.IsHighPassFilterEnabled(); + stereo_swapping_enabled = voe_.IsStereoChannelSwappingEnabled(); + voe_.GetTypingDetectionStatus(typing_detection_enabled); + EXPECT_TRUE(ec_enabled); + EXPECT_FALSE(agc_enabled); + EXPECT_FALSE(ns_enabled); + EXPECT_FALSE(highpass_filter_enabled); + EXPECT_FALSE(stereo_swapping_enabled); + EXPECT_TRUE(typing_detection_enabled); + + ASSERT_TRUE(engine_.SetOptions( + cricket::MediaEngineInterface::AUTO_GAIN_CONTROL)); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetEcMetricsStatus(ec_metrics_enabled); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + highpass_filter_enabled = voe_.IsHighPassFilterEnabled(); + stereo_swapping_enabled = voe_.IsStereoChannelSwappingEnabled(); + voe_.GetTypingDetectionStatus(typing_detection_enabled); + EXPECT_FALSE(ec_enabled); + EXPECT_TRUE(agc_enabled); + EXPECT_FALSE(ns_enabled); + EXPECT_FALSE(highpass_filter_enabled); + EXPECT_FALSE(stereo_swapping_enabled); + EXPECT_TRUE(typing_detection_enabled); + + ASSERT_TRUE(engine_.SetOptions( + cricket::MediaEngineInterface::NOISE_SUPPRESSION)); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetEcMetricsStatus(ec_metrics_enabled); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + highpass_filter_enabled = voe_.IsHighPassFilterEnabled(); + stereo_swapping_enabled = voe_.IsStereoChannelSwappingEnabled(); + voe_.GetTypingDetectionStatus(typing_detection_enabled); + EXPECT_FALSE(ec_enabled); + EXPECT_FALSE(agc_enabled); + EXPECT_TRUE(ns_enabled); + EXPECT_FALSE(highpass_filter_enabled); + EXPECT_FALSE(stereo_swapping_enabled); + EXPECT_TRUE(typing_detection_enabled); + + ASSERT_TRUE(engine_.SetOptions( + cricket::MediaEngineInterface::HIGHPASS_FILTER)); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetEcMetricsStatus(ec_metrics_enabled); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + highpass_filter_enabled = voe_.IsHighPassFilterEnabled(); + stereo_swapping_enabled = voe_.IsStereoChannelSwappingEnabled(); + voe_.GetTypingDetectionStatus(typing_detection_enabled); + EXPECT_FALSE(ec_enabled); + EXPECT_FALSE(agc_enabled); + EXPECT_FALSE(ns_enabled); + EXPECT_TRUE(highpass_filter_enabled); + EXPECT_FALSE(stereo_swapping_enabled); + EXPECT_TRUE(typing_detection_enabled); + + ASSERT_TRUE(engine_.SetOptions( + cricket::MediaEngineInterface::STEREO_FLIPPING)); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetEcMetricsStatus(ec_metrics_enabled); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + highpass_filter_enabled = voe_.IsHighPassFilterEnabled(); + stereo_swapping_enabled = voe_.IsStereoChannelSwappingEnabled(); + voe_.GetTypingDetectionStatus(typing_detection_enabled); + EXPECT_FALSE(ec_enabled); + EXPECT_FALSE(agc_enabled); + EXPECT_FALSE(ns_enabled); + EXPECT_FALSE(highpass_filter_enabled); + EXPECT_TRUE(stereo_swapping_enabled); + EXPECT_TRUE(typing_detection_enabled); + + ASSERT_TRUE(engine_.SetOptions( + cricket::MediaEngineInterface::DEFAULT_AUDIO_OPTIONS)); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetEcMetricsStatus(ec_metrics_enabled); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + highpass_filter_enabled = voe_.IsHighPassFilterEnabled(); + stereo_swapping_enabled = voe_.IsStereoChannelSwappingEnabled(); + voe_.GetTypingDetectionStatus(typing_detection_enabled); + EXPECT_TRUE(ec_enabled); + EXPECT_TRUE(agc_enabled); + EXPECT_TRUE(ns_enabled); + EXPECT_TRUE(highpass_filter_enabled); + EXPECT_FALSE(stereo_swapping_enabled); + EXPECT_TRUE(typing_detection_enabled); + + ASSERT_TRUE(engine_.SetOptions( + cricket::MediaEngineInterface::ALL_AUDIO_OPTIONS)); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetEcMetricsStatus(ec_metrics_enabled); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + highpass_filter_enabled = voe_.IsHighPassFilterEnabled(); + stereo_swapping_enabled = voe_.IsStereoChannelSwappingEnabled(); + voe_.GetTypingDetectionStatus(typing_detection_enabled); + EXPECT_TRUE(ec_enabled); + EXPECT_TRUE(agc_enabled); + EXPECT_TRUE(ns_enabled); + EXPECT_TRUE(highpass_filter_enabled); + EXPECT_TRUE(stereo_swapping_enabled); + EXPECT_TRUE(typing_detection_enabled); +} + +TEST_F(WebRtcVoiceEngineTestFake, InitDoesNotOverwriteDefaultAgcConfig) { + webrtc::AgcConfig set_config = {0}; + set_config.targetLeveldBOv = 3; + set_config.digitalCompressionGaindB = 9; + set_config.limiterEnable = true; + EXPECT_EQ(0, voe_.SetAgcConfig(set_config)); + EXPECT_TRUE(engine_.Init(talk_base::Thread::Current())); + + webrtc::AgcConfig config = {0}; + EXPECT_EQ(0, voe_.GetAgcConfig(config)); + EXPECT_EQ(set_config.targetLeveldBOv, config.targetLeveldBOv); + EXPECT_EQ(set_config.digitalCompressionGaindB, + config.digitalCompressionGaindB); + EXPECT_EQ(set_config.limiterEnable, config.limiterEnable); +} + + +TEST_F(WebRtcVoiceEngineTestFake, SetOptionOverridesViaChannels) { + EXPECT_TRUE(SetupEngine()); + talk_base::scoped_ptr channel1( + engine_.CreateChannel()); + talk_base::scoped_ptr channel2( + engine_.CreateChannel()); + + // Have to add a stream to make SetSend work. + cricket::StreamParams stream1; + stream1.ssrcs.push_back(1); + channel1->AddSendStream(stream1); + cricket::StreamParams stream2; + stream2.ssrcs.push_back(2); + channel2->AddSendStream(stream2); + + // AEC and AGC and NS + cricket::AudioOptions options_all; + options_all.echo_cancellation.Set(true); + options_all.auto_gain_control.Set(true); + options_all.noise_suppression.Set(true); + + ASSERT_TRUE(channel1->SetOptions(options_all)); + cricket::AudioOptions expected_options = options_all; + cricket::AudioOptions actual_options; + ASSERT_TRUE(channel1->GetOptions(&actual_options)); + EXPECT_EQ(expected_options, actual_options); + ASSERT_TRUE(channel2->SetOptions(options_all)); + ASSERT_TRUE(channel2->GetOptions(&actual_options)); + EXPECT_EQ(expected_options, actual_options); + + // unset NS + cricket::AudioOptions options_no_ns; + options_no_ns.noise_suppression.Set(false); + ASSERT_TRUE(channel1->SetOptions(options_no_ns)); + + expected_options.echo_cancellation.Set(true); + expected_options.auto_gain_control.Set(true); + expected_options.noise_suppression.Set(false); + ASSERT_TRUE(channel1->GetOptions(&actual_options)); + EXPECT_EQ(expected_options, actual_options); + + // unset AGC + cricket::AudioOptions options_no_agc; + options_no_agc.auto_gain_control.Set(false); + ASSERT_TRUE(channel2->SetOptions(options_no_agc)); + + expected_options.echo_cancellation.Set(true); + expected_options.auto_gain_control.Set(false); + expected_options.noise_suppression.Set(true); + ASSERT_TRUE(channel2->GetOptions(&actual_options)); + EXPECT_EQ(expected_options, actual_options); + + ASSERT_TRUE(engine_.SetAudioOptions(options_all)); + bool ec_enabled; + webrtc::EcModes ec_mode; + bool agc_enabled; + webrtc::AgcModes agc_mode; + bool ns_enabled; + webrtc::NsModes ns_mode; + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + EXPECT_TRUE(ec_enabled); + EXPECT_TRUE(agc_enabled); + EXPECT_TRUE(ns_enabled); + + channel1->SetSend(cricket::SEND_MICROPHONE); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + EXPECT_TRUE(ec_enabled); + EXPECT_TRUE(agc_enabled); + EXPECT_FALSE(ns_enabled); + + channel1->SetSend(cricket::SEND_NOTHING); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + EXPECT_TRUE(ec_enabled); + EXPECT_TRUE(agc_enabled); + EXPECT_TRUE(ns_enabled); + + channel2->SetSend(cricket::SEND_MICROPHONE); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + EXPECT_TRUE(ec_enabled); + EXPECT_FALSE(agc_enabled); + EXPECT_TRUE(ns_enabled); + + channel2->SetSend(cricket::SEND_NOTHING); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + EXPECT_TRUE(ec_enabled); + EXPECT_TRUE(agc_enabled); + EXPECT_TRUE(ns_enabled); + + // Make sure settings take effect while we are sending. + ASSERT_TRUE(engine_.SetAudioOptions(options_all)); + cricket::AudioOptions options_no_agc_nor_ns; + options_no_agc_nor_ns.auto_gain_control.Set(false); + options_no_agc_nor_ns.noise_suppression.Set(false); + channel2->SetSend(cricket::SEND_MICROPHONE); + channel2->SetOptions(options_no_agc_nor_ns); + + expected_options.echo_cancellation.Set(true); + expected_options.auto_gain_control.Set(false); + expected_options.noise_suppression.Set(false); + ASSERT_TRUE(channel2->GetOptions(&actual_options)); + EXPECT_EQ(expected_options, actual_options); + voe_.GetEcStatus(ec_enabled, ec_mode); + voe_.GetAgcStatus(agc_enabled, agc_mode); + voe_.GetNsStatus(ns_enabled, ns_mode); + EXPECT_TRUE(ec_enabled); + EXPECT_FALSE(agc_enabled); + EXPECT_FALSE(ns_enabled); +} + +// Test that GetReceiveChannelNum returns the default channel for the first +// recv stream in 1-1 calls. +TEST_F(WebRtcVoiceEngineTestFake, TestGetReceiveChannelNumIn1To1Calls) { + EXPECT_TRUE(SetupEngine()); + cricket::WebRtcVoiceMediaChannel* media_channel = + static_cast(channel_); + // Test that GetChannelNum returns the default channel if the SSRC is unknown. + EXPECT_EQ(media_channel->voe_channel(), + media_channel->GetReceiveChannelNum(0)); + cricket::StreamParams stream; + stream.ssrcs.push_back(kSsrc2); + EXPECT_TRUE(channel_->AddRecvStream(stream)); + EXPECT_EQ(media_channel->voe_channel(), + media_channel->GetReceiveChannelNum(kSsrc2)); +} + +// Test that GetReceiveChannelNum doesn't return the default channel for the +// first recv stream in conference calls. +TEST_F(WebRtcVoiceEngineTestFake, TestGetChannelNumInConferenceCalls) { + EXPECT_TRUE(SetupEngine()); + EXPECT_TRUE(channel_->SetOptions(options_conference_)); + cricket::StreamParams stream; + stream.ssrcs.push_back(kSsrc2); + EXPECT_TRUE(channel_->AddRecvStream(stream)); + cricket::WebRtcVoiceMediaChannel* media_channel = + static_cast(channel_); + EXPECT_LT(media_channel->voe_channel(), + media_channel->GetReceiveChannelNum(kSsrc2)); +} + +TEST_F(WebRtcVoiceEngineTestFake, SetOutputScaling) { + EXPECT_TRUE(SetupEngine()); + double left, right; + EXPECT_TRUE(channel_->SetOutputScaling(0, 1, 2)); + EXPECT_TRUE(channel_->GetOutputScaling(0, &left, &right)); + EXPECT_DOUBLE_EQ(1, left); + EXPECT_DOUBLE_EQ(2, right); + + EXPECT_FALSE(channel_->SetOutputScaling(kSsrc2, 1, 2)); + cricket::StreamParams stream; + stream.ssrcs.push_back(kSsrc2); + EXPECT_TRUE(channel_->AddRecvStream(stream)); + + EXPECT_TRUE(channel_->SetOutputScaling(kSsrc2, 2, 1)); + EXPECT_TRUE(channel_->GetOutputScaling(kSsrc2, &left, &right)); + EXPECT_DOUBLE_EQ(2, left); + EXPECT_DOUBLE_EQ(1, right); +} + + +// Tests for the actual WebRtc VoE library. + +// Tests that the library initializes and shuts down properly. +TEST(WebRtcVoiceEngineTest, StartupShutdown) { + cricket::WebRtcVoiceEngine engine; + EXPECT_TRUE(engine.Init(talk_base::Thread::Current())); + cricket::VoiceMediaChannel* channel = engine.CreateChannel(); + EXPECT_TRUE(channel != NULL); + delete channel; + engine.Terminate(); + + // Reinit to catch regression where VoiceEngineObserver reference is lost + EXPECT_TRUE(engine.Init(talk_base::Thread::Current())); + engine.Terminate(); +} + +// Tests that the logging from the library is cleartext. +TEST(WebRtcVoiceEngineTest, DISABLED_HasUnencryptedLogging) { + cricket::WebRtcVoiceEngine engine; + talk_base::scoped_ptr stream( + new talk_base::MemoryStream); + size_t size = 0; + bool cleartext = true; + talk_base::LogMessage::AddLogToStream(stream.get(), talk_base::LS_VERBOSE); + engine.SetLogging(talk_base::LS_VERBOSE, ""); + EXPECT_TRUE(engine.Init(talk_base::Thread::Current())); + EXPECT_TRUE(stream->GetSize(&size)); + EXPECT_GT(size, 0U); + engine.Terminate(); + talk_base::LogMessage::RemoveLogToStream(stream.get()); + const char* buf = stream->GetBuffer(); + for (size_t i = 0; i < size && cleartext; ++i) { + int ch = static_cast(buf[i]); + ASSERT_GE(ch, 0) << "Out of bounds character in WebRtc VoE log: " + << std::hex << ch; + cleartext = (isprint(ch) || isspace(ch)); + } + EXPECT_TRUE(cleartext); +} + +// Tests we do not see any references to a monitor thread being spun up +// when initiating the engine. +TEST(WebRtcVoiceEngineTest, HasNoMonitorThread) { + cricket::WebRtcVoiceEngine engine; + talk_base::scoped_ptr stream( + new talk_base::MemoryStream); + talk_base::LogMessage::AddLogToStream(stream.get(), talk_base::LS_VERBOSE); + engine.SetLogging(talk_base::LS_VERBOSE, ""); + EXPECT_TRUE(engine.Init(talk_base::Thread::Current())); + engine.Terminate(); + talk_base::LogMessage::RemoveLogToStream(stream.get()); + + size_t size = 0; + EXPECT_TRUE(stream->GetSize(&size)); + EXPECT_GT(size, 0U); + const std::string logs(stream->GetBuffer()); + EXPECT_NE(std::string::npos, logs.find("ProcessThread")); +} + +// Tests that the library is configured with the codecs we want. +TEST(WebRtcVoiceEngineTest, HasCorrectCodecs) { + cricket::WebRtcVoiceEngine engine; + // Check codecs by name. + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "OPUS", 48000, 0, 2, 0))); + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "ISAC", 16000, 0, 1, 0))); + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "ISAC", 32000, 0, 1, 0))); + // Check that name matching is case-insensitive. + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "ILBC", 8000, 0, 1, 0))); + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "iLBC", 8000, 0, 1, 0))); + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "PCMU", 8000, 0, 1, 0))); + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "PCMA", 8000, 0, 1, 0))); + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "G722", 16000, 0, 1, 0))); + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "red", 8000, 0, 1, 0))); + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "CN", 48000, 0, 1, 0))); + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "CN", 32000, 0, 1, 0))); + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "CN", 16000, 0, 1, 0))); + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "CN", 8000, 0, 1, 0))); + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(96, "telephone-event", 8000, 0, 1, 0))); + // Check codecs with an id by id. + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(0, "", 8000, 0, 1, 0))); // PCMU + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(8, "", 8000, 0, 1, 0))); // PCMA + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(9, "", 16000, 0, 1, 0))); // G722 + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(13, "", 8000, 0, 1, 0))); // CN + // Check sample/bitrate matching. + EXPECT_TRUE(engine.FindCodec( + cricket::AudioCodec(0, "PCMU", 8000, 64000, 1, 0))); + // Check that bad codecs fail. + EXPECT_FALSE(engine.FindCodec(cricket::AudioCodec(99, "ABCD", 0, 0, 1, 0))); + EXPECT_FALSE(engine.FindCodec(cricket::AudioCodec(88, "", 0, 0, 1, 0))); + EXPECT_FALSE(engine.FindCodec(cricket::AudioCodec(0, "", 0, 0, 2, 0))); + EXPECT_FALSE(engine.FindCodec(cricket::AudioCodec(0, "", 5000, 0, 1, 0))); + EXPECT_FALSE(engine.FindCodec(cricket::AudioCodec(0, "", 0, 5000, 1, 0))); + // Check that there aren't any extra codecs lying around. + EXPECT_EQ(13U, engine.codecs().size()); + // Verify the payload id of common audio codecs, including CN, ISAC, and G722. + for (std::vector::const_iterator it = + engine.codecs().begin(); it != engine.codecs().end(); ++it) { + if (it->name == "CN" && it->clockrate == 16000) { + EXPECT_EQ(105, it->id); + } else if (it->name == "CN" && it->clockrate == 32000) { + EXPECT_EQ(106, it->id); + } else if (it->name == "ISAC" && it->clockrate == 16000) { + EXPECT_EQ(103, it->id); + } else if (it->name == "ISAC" && it->clockrate == 32000) { + EXPECT_EQ(104, it->id); + } else if (it->name == "G722" && it->clockrate == 16000) { + EXPECT_EQ(9, it->id); + } else if (it->name == "telephone-event") { + EXPECT_EQ(126, it->id); + } else if (it->name == "red") { + EXPECT_EQ(127, it->id); + } else if (it->name == "opus") { + EXPECT_EQ(111, it->id); + ASSERT_NE(it->params.find("minptime"), it->params.end()); + EXPECT_EQ("10", it->params.find("minptime")->second); + ASSERT_NE(it->params.find("maxptime"), it->params.end()); + EXPECT_EQ("60", it->params.find("maxptime")->second); + } + } + + engine.Terminate(); +} + +// Tests that VoE supports at least 32 channels +TEST(WebRtcVoiceEngineTest, Has32Channels) { + cricket::WebRtcVoiceEngine engine; + EXPECT_TRUE(engine.Init(talk_base::Thread::Current())); + + cricket::VoiceMediaChannel* channels[32]; + int num_channels = 0; + + while (num_channels < ARRAY_SIZE(channels)) { + cricket::VoiceMediaChannel* channel = engine.CreateChannel(); + if (!channel) + break; + + channels[num_channels++] = channel; + } + + int expected = ARRAY_SIZE(channels); + EXPECT_EQ(expected, num_channels); + + while (num_channels > 0) { + delete channels[--num_channels]; + } + + engine.Terminate(); +} + +// Test that we set our preferred codecs properly. +TEST(WebRtcVoiceEngineTest, SetRecvCodecs) { + cricket::WebRtcVoiceEngine engine; + EXPECT_TRUE(engine.Init(talk_base::Thread::Current())); + cricket::WebRtcVoiceMediaChannel channel(&engine); + EXPECT_TRUE(channel.SetRecvCodecs(engine.codecs())); +} + +#ifdef WIN32 +// Test our workarounds to WebRtc VoE' munging of the coinit count +TEST(WebRtcVoiceEngineTest, CoInitialize) { + cricket::WebRtcVoiceEngine* engine = new cricket::WebRtcVoiceEngine(); + + // Initial refcount should be 0. + EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_MULTITHREADED)); + + // Engine should start even with COM already inited. + EXPECT_TRUE(engine->Init(talk_base::Thread::Current())); + engine->Terminate(); + EXPECT_TRUE(engine->Init(talk_base::Thread::Current())); + engine->Terminate(); + + // Refcount after terminate should be 1 (in reality 3); test if it is nonzero. + EXPECT_EQ(S_FALSE, CoInitializeEx(NULL, COINIT_MULTITHREADED)); + // Decrement refcount to (hopefully) 0. + CoUninitialize(); + CoUninitialize(); + delete engine; + + // Ensure refcount is 0. + EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_MULTITHREADED)); + CoUninitialize(); +} +#endif + + diff --git a/talk/p2p/base/asyncstuntcpsocket.cc b/talk/p2p/base/asyncstuntcpsocket.cc new file mode 100644 index 000000000..2f616410f --- /dev/null +++ b/talk/p2p/base/asyncstuntcpsocket.cc @@ -0,0 +1,168 @@ +/* + * libjingle + * Copyright 2013, 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/p2p/base/asyncstuntcpsocket.h" + +#include + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/p2p/base/stun.h" + +namespace cricket { + +static const size_t kMaxPacketSize = 64 * 1024; + +typedef uint16 PacketLength; +static const size_t kPacketLenSize = sizeof(PacketLength); +static const size_t kPacketLenOffset = 2; +static const size_t kBufSize = kMaxPacketSize + kStunHeaderSize; +static const size_t kTurnChannelDataHdrSize = 4; + +inline bool IsStunMessage(uint16 msg_type) { + // The first two bits of a channel data message are 0b01. + return (msg_type & 0xC000) ? false : true; +} + +// AsyncStunTCPSocket +// Binds and connects |socket| and creates AsyncTCPSocket for +// it. Takes ownership of |socket|. Returns NULL if bind() or +// connect() fail (|socket| is destroyed in that case). +AsyncStunTCPSocket* AsyncStunTCPSocket::Create( + talk_base::AsyncSocket* socket, + const talk_base::SocketAddress& bind_address, + const talk_base::SocketAddress& remote_address) { + return new AsyncStunTCPSocket(AsyncTCPSocketBase::ConnectSocket( + socket, bind_address, remote_address), false); +} + +AsyncStunTCPSocket::AsyncStunTCPSocket( + talk_base::AsyncSocket* socket, bool listen) + : talk_base::AsyncTCPSocketBase(socket, listen, kBufSize) { +} + +int AsyncStunTCPSocket::Send(const void *pv, size_t cb) { + if (cb > kBufSize || cb < kPacketLenSize + kPacketLenOffset) { + SetError(EMSGSIZE); + return -1; + } + + // If we are blocking on send, then silently drop this packet + if (!IsOutBufferEmpty()) + return static_cast(cb); + + int pad_bytes; + size_t expected_pkt_len = GetExpectedLength(pv, cb, &pad_bytes); + + // Accepts only complete STUN/ChannelData packets. + if (cb != expected_pkt_len) + return -1; + + AppendToOutBuffer(pv, cb); + + ASSERT(pad_bytes < 4); + char padding[4] = {0}; + AppendToOutBuffer(padding, pad_bytes); + + int res = FlushOutBuffer(); + if (res <= 0) { + // drop packet if we made no progress + ClearOutBuffer(); + return res; + } + + // We claim to have sent the whole thing, even if we only sent partial + return static_cast(cb); +} + +void AsyncStunTCPSocket::ProcessInput(char* data, size_t* len) { + talk_base::SocketAddress remote_addr(GetRemoteAddress()); + // STUN packet - First 4 bytes. Total header size is 20 bytes. + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // |0 0| STUN Message Type | Message Length | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + // TURN ChannelData + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | Channel Number | Length | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + while (true) { + // We need at least 4 bytes to read the STUN or ChannelData packet length. + if (*len < kPacketLenOffset + kPacketLenSize) + return; + + int pad_bytes; + size_t expected_pkt_len = GetExpectedLength(data, *len, &pad_bytes); + size_t actual_length = expected_pkt_len + pad_bytes; + + if (*len < actual_length) { + return; + } + + SignalReadPacket(this, data, expected_pkt_len, remote_addr); + + *len -= actual_length; + if (*len > 0) { + memmove(data, data + actual_length, *len); + } + } +} + +void AsyncStunTCPSocket::HandleIncomingConnection( + talk_base::AsyncSocket* socket) { + SignalNewConnection(this, new AsyncStunTCPSocket(socket, false)); +} + +size_t AsyncStunTCPSocket::GetExpectedLength(const void* data, size_t len, + int* pad_bytes) { + *pad_bytes = 0; + PacketLength pkt_len = + talk_base::GetBE16(static_cast(data) + kPacketLenOffset); + size_t expected_pkt_len; + uint16 msg_type = talk_base::GetBE16(data); + if (IsStunMessage(msg_type)) { + // STUN message. + expected_pkt_len = kStunHeaderSize + pkt_len; + } else { + // TURN ChannelData message. + expected_pkt_len = kTurnChannelDataHdrSize + pkt_len; + // From RFC 5766 section 11.5 + // Over TCP and TLS-over-TCP, the ChannelData message MUST be padded to + // a multiple of four bytes in order to ensure the alignment of + // subsequent messages. The padding is not reflected in the length + // field of the ChannelData message, so the actual size of a ChannelData + // message (including padding) is (4 + Length) rounded up to the nearest + // multiple of 4. Over UDP, the padding is not required but MAY be + // included. + if (expected_pkt_len % 4) + *pad_bytes = 4 - (expected_pkt_len % 4); + } + return expected_pkt_len; +} + +} // namespace cricket diff --git a/talk/p2p/base/asyncstuntcpsocket.h b/talk/p2p/base/asyncstuntcpsocket.h new file mode 100644 index 000000000..2380c4c0d --- /dev/null +++ b/talk/p2p/base/asyncstuntcpsocket.h @@ -0,0 +1,66 @@ +/* + * libjingle + * Copyright 2013, 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. + */ + +#ifndef TALK_BASE_ASYNCSTUNTCPSOCKET_H_ +#define TALK_BASE_ASYNCSTUNTCPSOCKET_H_ + +#include "talk/base/asynctcpsocket.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/socketfactory.h" + +namespace cricket { + +class AsyncStunTCPSocket : public talk_base::AsyncTCPSocketBase { + public: + // Binds and connects |socket| and creates AsyncTCPSocket for + // it. Takes ownership of |socket|. Returns NULL if bind() or + // connect() fail (|socket| is destroyed in that case). + static AsyncStunTCPSocket* Create( + talk_base::AsyncSocket* socket, + const talk_base::SocketAddress& bind_address, + const talk_base::SocketAddress& remote_address); + + AsyncStunTCPSocket(talk_base::AsyncSocket* socket, bool listen); + virtual ~AsyncStunTCPSocket() {} + + virtual int Send(const void* pv, size_t cb); + virtual void ProcessInput(char* data, size_t* len); + virtual void HandleIncomingConnection(talk_base::AsyncSocket* socket); + + private: + // This method returns the message hdr + length written in the header. + // This method also returns the number of padding bytes needed/added to the + // turn message. |pad_bytes| should be used only when |is_turn| is true. + size_t GetExpectedLength(const void* data, size_t len, + int* pad_bytes); + + DISALLOW_EVIL_CONSTRUCTORS(AsyncStunTCPSocket); +}; + +} // namespace cricket + +#endif // TALK_BASE_ASYNCSTUNTCPSOCKET_H_ diff --git a/talk/p2p/base/asyncstuntcpsocket_unittest.cc b/talk/p2p/base/asyncstuntcpsocket_unittest.cc new file mode 100644 index 000000000..a67571264 --- /dev/null +++ b/talk/p2p/base/asyncstuntcpsocket_unittest.cc @@ -0,0 +1,277 @@ +/* + * libjingle + * Copyright 2013, 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/base/asyncsocket.h" +#include "talk/base/gunit.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/virtualsocketserver.h" +#include "talk/p2p/base/asyncstuntcpsocket.h" + +namespace cricket { + +static unsigned char kStunMessageWithZeroLength[] = { + 0x00, 0x01, 0x00, 0x00, // length of 0 (last 2 bytes) + 0x21, 0x12, 0xA4, 0x42, + '0', '1', '2', '3', + '4', '5', '6', '7', + '8', '9', 'a', 'b', +}; + + +static unsigned char kTurnChannelDataMessageWithZeroLength[] = { + 0x40, 0x00, 0x00, 0x00, // length of 0 (last 2 bytes) +}; + +static unsigned char kTurnChannelDataMessage[] = { + 0x40, 0x00, 0x00, 0x10, + 0x21, 0x12, 0xA4, 0x42, + '0', '1', '2', '3', + '4', '5', '6', '7', + '8', '9', 'a', 'b', +}; + +static unsigned char kStunMessageWithInvalidLength[] = { + 0x00, 0x01, 0x00, 0x10, + 0x21, 0x12, 0xA4, 0x42, + '0', '1', '2', '3', + '4', '5', '6', '7', + '8', '9', 'a', 'b', +}; + +static unsigned char kTurnChannelDataMessageWithInvalidLength[] = { + 0x80, 0x00, 0x00, 0x20, + 0x21, 0x12, 0xA4, 0x42, + '0', '1', '2', '3', + '4', '5', '6', '7', + '8', '9', 'a', 'b', +}; + +static unsigned char kTurnChannelDataMessageWithOddLength[] = { + 0x40, 0x00, 0x00, 0x05, + 0x21, 0x12, 0xA4, 0x42, + '0', +}; + + +static const talk_base::SocketAddress kClientAddr("11.11.11.11", 0); +static const talk_base::SocketAddress kServerAddr("22.22.22.22", 0); + +class AsyncStunTCPSocketTest : public testing::Test, + public sigslot::has_slots<> { + protected: + AsyncStunTCPSocketTest() + : vss_(new talk_base::VirtualSocketServer(NULL)), + ss_scope_(vss_.get()) { + } + + virtual void SetUp() { + CreateSockets(); + } + + void CreateSockets() { + talk_base::AsyncSocket* server = vss_->CreateAsyncSocket( + kServerAddr.family(), SOCK_STREAM); + server->Bind(kServerAddr); + recv_socket_.reset(new AsyncStunTCPSocket(server, true)); + recv_socket_->SignalNewConnection.connect( + this, &AsyncStunTCPSocketTest::OnNewConnection); + + talk_base::AsyncSocket* client = vss_->CreateAsyncSocket( + kClientAddr.family(), SOCK_STREAM); + send_socket_.reset(AsyncStunTCPSocket::Create( + client, kClientAddr, recv_socket_->GetLocalAddress())); + ASSERT_TRUE(send_socket_.get() != NULL); + vss_->ProcessMessagesUntilIdle(); + } + + void OnReadPacket(talk_base::AsyncPacketSocket* socket, const char* data, + size_t len, const talk_base::SocketAddress& remote_addr) { + recv_packets_.push_back(std::string(data, len)); + } + + void OnNewConnection(talk_base::AsyncPacketSocket* server, + talk_base::AsyncPacketSocket* new_socket) { + listen_socket_.reset(new_socket); + new_socket->SignalReadPacket.connect( + this, &AsyncStunTCPSocketTest::OnReadPacket); + } + + bool Send(const void* data, size_t len) { + size_t ret = send_socket_->Send(reinterpret_cast(data), len); + vss_->ProcessMessagesUntilIdle(); + return (ret == len); + } + + bool CheckData(const void* data, int len) { + bool ret = false; + if (recv_packets_.size()) { + std::string packet = recv_packets_.front(); + recv_packets_.pop_front(); + ret = (memcmp(data, packet.c_str(), len) == 0); + } + return ret; + } + + talk_base::scoped_ptr vss_; + talk_base::SocketServerScope ss_scope_; + talk_base::scoped_ptr send_socket_; + talk_base::scoped_ptr recv_socket_; + talk_base::scoped_ptr listen_socket_; + std::list recv_packets_; +}; + +// Testing a stun packet sent/recv properly. +TEST_F(AsyncStunTCPSocketTest, TestSingleStunPacket) { + EXPECT_TRUE(Send(kStunMessageWithZeroLength, + sizeof(kStunMessageWithZeroLength))); + EXPECT_EQ(1u, recv_packets_.size()); + EXPECT_TRUE(CheckData(kStunMessageWithZeroLength, + sizeof(kStunMessageWithZeroLength))); +} + +// Verify sending multiple packets. +TEST_F(AsyncStunTCPSocketTest, TestMultipleStunPackets) { + EXPECT_TRUE(Send(kStunMessageWithZeroLength, + sizeof(kStunMessageWithZeroLength))); + EXPECT_TRUE(Send(kStunMessageWithZeroLength, + sizeof(kStunMessageWithZeroLength))); + EXPECT_TRUE(Send(kStunMessageWithZeroLength, + sizeof(kStunMessageWithZeroLength))); + EXPECT_TRUE(Send(kStunMessageWithZeroLength, + sizeof(kStunMessageWithZeroLength))); + EXPECT_EQ(4u, recv_packets_.size()); +} + +// Verifying TURN channel data message with zero length. +TEST_F(AsyncStunTCPSocketTest, TestTurnChannelDataWithZeroLength) { + EXPECT_TRUE(Send(kTurnChannelDataMessageWithZeroLength, + sizeof(kTurnChannelDataMessageWithZeroLength))); + EXPECT_EQ(1u, recv_packets_.size()); + EXPECT_TRUE(CheckData(kTurnChannelDataMessageWithZeroLength, + sizeof(kTurnChannelDataMessageWithZeroLength))); +} + +// Verifying TURN channel data message. +TEST_F(AsyncStunTCPSocketTest, TestTurnChannelData) { + EXPECT_TRUE(Send(kTurnChannelDataMessage, + sizeof(kTurnChannelDataMessage))); + EXPECT_EQ(1u, recv_packets_.size()); + EXPECT_TRUE(CheckData(kTurnChannelDataMessage, + sizeof(kTurnChannelDataMessage))); +} + +// Verifying TURN channel messages which needs padding handled properly. +TEST_F(AsyncStunTCPSocketTest, TestTurnChannelDataPadding) { + EXPECT_TRUE(Send(kTurnChannelDataMessageWithOddLength, + sizeof(kTurnChannelDataMessageWithOddLength))); + EXPECT_EQ(1u, recv_packets_.size()); + EXPECT_TRUE(CheckData(kTurnChannelDataMessageWithOddLength, + sizeof(kTurnChannelDataMessageWithOddLength))); +} + +// Verifying stun message with invalid length. +TEST_F(AsyncStunTCPSocketTest, TestStunInvalidLength) { + EXPECT_FALSE(Send(kStunMessageWithInvalidLength, + sizeof(kStunMessageWithInvalidLength))); + EXPECT_EQ(0u, recv_packets_.size()); + + // Modify the message length to larger value. + kStunMessageWithInvalidLength[2] = 0xFF; + kStunMessageWithInvalidLength[3] = 0xFF; + EXPECT_FALSE(Send(kStunMessageWithInvalidLength, + sizeof(kStunMessageWithInvalidLength))); + + // Modify the message length to smaller value. + kStunMessageWithInvalidLength[2] = 0x00; + kStunMessageWithInvalidLength[3] = 0x01; + EXPECT_FALSE(Send(kStunMessageWithInvalidLength, + sizeof(kStunMessageWithInvalidLength))); +} + +// Verifying TURN channel data message with invalid length. +TEST_F(AsyncStunTCPSocketTest, TestTurnChannelDataWithInvalidLength) { + EXPECT_FALSE(Send(kTurnChannelDataMessageWithInvalidLength, + sizeof(kTurnChannelDataMessageWithInvalidLength))); + // Modify the length to larger value. + kTurnChannelDataMessageWithInvalidLength[2] = 0xFF; + kTurnChannelDataMessageWithInvalidLength[3] = 0xF0; + EXPECT_FALSE(Send(kTurnChannelDataMessageWithInvalidLength, + sizeof(kTurnChannelDataMessageWithInvalidLength))); + + // Modify the length to smaller value. + kTurnChannelDataMessageWithInvalidLength[2] = 0x00; + kTurnChannelDataMessageWithInvalidLength[3] = 0x00; + EXPECT_FALSE(Send(kTurnChannelDataMessageWithInvalidLength, + sizeof(kTurnChannelDataMessageWithInvalidLength))); +} + +// Verifying a small buffer handled (dropped) properly. This will be +// a common one for both stun and turn. +TEST_F(AsyncStunTCPSocketTest, TestTooSmallMessageBuffer) { + char data[1]; + EXPECT_FALSE(Send(data, sizeof(data))); +} + +// Verifying a legal large turn message. +TEST_F(AsyncStunTCPSocketTest, TestMaximumSizeTurnPacket) { + // We have problem in getting the SignalWriteEvent from the virtual socket + // server. So increasing the send buffer to 64k. + // TODO(mallinath) - Remove this setting after we fix vss issue. + vss_->set_send_buffer_capacity(64 * 1024); + unsigned char packet[65539]; + packet[0] = 0x40; + packet[1] = 0x00; + packet[2] = 0xFF; + packet[3] = 0xFF; + EXPECT_TRUE(Send(packet, sizeof(packet))); +} + +// Verifying a legal large stun message. +TEST_F(AsyncStunTCPSocketTest, TestMaximumSizeStunPacket) { + // We have problem in getting the SignalWriteEvent from the virtual socket + // server. So increasing the send buffer to 64k. + // TODO(mallinath) - Remove this setting after we fix vss issue. + vss_->set_send_buffer_capacity(64 * 1024); + unsigned char packet[65552]; + packet[0] = 0x00; + packet[1] = 0x01; + packet[2] = 0xFF; + packet[3] = 0xFC; + EXPECT_TRUE(Send(packet, sizeof(packet))); +} + +// Investigate why WriteEvent is not signaled from VSS. +TEST_F(AsyncStunTCPSocketTest, DISABLED_TestWithSmallSendBuffer) { + vss_->set_send_buffer_capacity(1); + Send(kTurnChannelDataMessageWithOddLength, + sizeof(kTurnChannelDataMessageWithOddLength)); + EXPECT_EQ(1u, recv_packets_.size()); + EXPECT_TRUE(CheckData(kTurnChannelDataMessageWithOddLength, + sizeof(kTurnChannelDataMessageWithOddLength))); +} + +} // namespace cricket diff --git a/talk/p2p/base/basicpacketsocketfactory.cc b/talk/p2p/base/basicpacketsocketfactory.cc new file mode 100644 index 000000000..714210470 --- /dev/null +++ b/talk/p2p/base/basicpacketsocketfactory.cc @@ -0,0 +1,187 @@ +/* + * libjingle + * Copyright 2011, 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/p2p/base/basicpacketsocketfactory.h" + +#include "talk/base/asyncudpsocket.h" +#include "talk/base/asynctcpsocket.h" +#include "talk/base/logging.h" +#include "talk/base/socketadapters.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/asyncstuntcpsocket.h" +#include "talk/p2p/base/stun.h" + +namespace talk_base { + +BasicPacketSocketFactory::BasicPacketSocketFactory() + : thread_(Thread::Current()), + socket_factory_(NULL) { +} + +BasicPacketSocketFactory::BasicPacketSocketFactory(Thread* thread) + : thread_(thread), + socket_factory_(NULL) { +} + +BasicPacketSocketFactory::BasicPacketSocketFactory( + SocketFactory* socket_factory) + : thread_(NULL), + socket_factory_(socket_factory) { +} + +BasicPacketSocketFactory::~BasicPacketSocketFactory() { +} + +AsyncPacketSocket* BasicPacketSocketFactory::CreateUdpSocket( + const SocketAddress& address, int min_port, int max_port) { + // UDP sockets are simple. + talk_base::AsyncSocket* socket = + socket_factory()->CreateAsyncSocket( + address.family(), SOCK_DGRAM); + if (!socket) { + return NULL; + } + if (BindSocket(socket, address, min_port, max_port) < 0) { + LOG(LS_ERROR) << "UDP bind failed with error " + << socket->GetError(); + delete socket; + return NULL; + } + return new talk_base::AsyncUDPSocket(socket); +} + +AsyncPacketSocket* BasicPacketSocketFactory::CreateServerTcpSocket( + const SocketAddress& local_address, int min_port, int max_port, int opts) { + talk_base::AsyncSocket* socket = + socket_factory()->CreateAsyncSocket(local_address.family(), + SOCK_STREAM); + if (!socket) { + return NULL; + } + + if (BindSocket(socket, local_address, min_port, max_port) < 0) { + LOG(LS_ERROR) << "TCP bind failed with error " + << socket->GetError(); + delete socket; + return NULL; + } + + // If using SSLTCP, wrap the TCP socket in a pseudo-SSL socket. + if (opts & PacketSocketFactory::OPT_SSLTCP) { + socket = new talk_base::AsyncSSLSocket(socket); + } + + // Set TCP_NODELAY (via OPT_NODELAY) for improved performance. + // See http://go/gtalktcpnodelayexperiment + socket->SetOption(talk_base::Socket::OPT_NODELAY, 1); + + if (opts & PacketSocketFactory::OPT_STUN) + return new cricket::AsyncStunTCPSocket(socket, true); + + return new talk_base::AsyncTCPSocket(socket, true); +} + +AsyncPacketSocket* BasicPacketSocketFactory::CreateClientTcpSocket( + const SocketAddress& local_address, const SocketAddress& remote_address, + const ProxyInfo& proxy_info, const std::string& user_agent, int opts) { + talk_base::AsyncSocket* socket = + socket_factory()->CreateAsyncSocket(local_address.family(), SOCK_STREAM); + if (!socket) { + return NULL; + } + + if (BindSocket(socket, local_address, 0, 0) < 0) { + LOG(LS_ERROR) << "TCP bind failed with error " + << socket->GetError(); + delete socket; + return NULL; + } + + // If using a proxy, wrap the socket in a proxy socket. + if (proxy_info.type == talk_base::PROXY_SOCKS5) { + socket = new talk_base::AsyncSocksProxySocket( + socket, proxy_info.address, proxy_info.username, proxy_info.password); + } else if (proxy_info.type == talk_base::PROXY_HTTPS) { + socket = new talk_base::AsyncHttpsProxySocket( + socket, user_agent, proxy_info.address, + proxy_info.username, proxy_info.password); + } + + // If using SSLTCP, wrap the TCP socket in a pseudo-SSL socket. + if (opts & PacketSocketFactory::OPT_SSLTCP) { + socket = new talk_base::AsyncSSLSocket(socket); + } + + if (socket->Connect(remote_address) < 0) { + LOG(LS_ERROR) << "TCP connect failed with error " + << socket->GetError(); + delete socket; + return NULL; + } + + // Finally, wrap that socket in a TCP or STUN TCP packet socket. + AsyncPacketSocket* tcp_socket; + if (opts & PacketSocketFactory::OPT_STUN) { + tcp_socket = new cricket::AsyncStunTCPSocket(socket, false); + } else { + tcp_socket = new talk_base::AsyncTCPSocket(socket, false); + } + + // Set TCP_NODELAY (via OPT_NODELAY) for improved performance. + // See http://go/gtalktcpnodelayexperiment + tcp_socket->SetOption(talk_base::Socket::OPT_NODELAY, 1); + + return tcp_socket; +} + +int BasicPacketSocketFactory::BindSocket( + AsyncSocket* socket, const SocketAddress& local_address, + int min_port, int max_port) { + int ret = -1; + if (min_port == 0 && max_port == 0) { + // If there's no port range, let the OS pick a port for us. + ret = socket->Bind(local_address); + } else { + // Otherwise, try to find a port in the provided range. + for (int port = min_port; ret < 0 && port <= max_port; ++port) { + ret = socket->Bind(talk_base::SocketAddress(local_address.ipaddr(), + port)); + } + } + return ret; +} + +SocketFactory* BasicPacketSocketFactory::socket_factory() { + if (thread_) { + ASSERT(thread_ == Thread::Current()); + return thread_->socketserver(); + } else { + return socket_factory_; + } +} + +} // namespace talk_base diff --git a/talk/p2p/base/basicpacketsocketfactory.h b/talk/p2p/base/basicpacketsocketfactory.h new file mode 100644 index 000000000..d4e76e714 --- /dev/null +++ b/talk/p2p/base/basicpacketsocketfactory.h @@ -0,0 +1,66 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_BASE_BASICPACKETSOCKETFACTORY_H_ +#define TALK_BASE_BASICPACKETSOCKETFACTORY_H_ + +#include "talk/p2p/base/packetsocketfactory.h" + +namespace talk_base { + +class AsyncSocket; +class SocketFactory; +class Thread; + +class BasicPacketSocketFactory : public PacketSocketFactory { + public: + BasicPacketSocketFactory(); + explicit BasicPacketSocketFactory(Thread* thread); + explicit BasicPacketSocketFactory(SocketFactory* socket_factory); + virtual ~BasicPacketSocketFactory(); + + virtual AsyncPacketSocket* CreateUdpSocket( + const SocketAddress& local_address, int min_port, int max_port); + virtual AsyncPacketSocket* CreateServerTcpSocket( + const SocketAddress& local_address, int min_port, int max_port, int opts); + virtual AsyncPacketSocket* CreateClientTcpSocket( + const SocketAddress& local_address, const SocketAddress& remote_address, + const ProxyInfo& proxy_info, const std::string& user_agent, int opts); + + private: + int BindSocket(AsyncSocket* socket, const SocketAddress& local_address, + int min_port, int max_port); + + SocketFactory* socket_factory(); + + Thread* thread_; + SocketFactory* socket_factory_; +}; + +} // namespace talk_base + +#endif // TALK_BASE_BASICPACKETSOCKETFACTORY_H_ diff --git a/talk/p2p/base/candidate.h b/talk/p2p/base/candidate.h new file mode 100644 index 000000000..19eed8cc3 --- /dev/null +++ b/talk/p2p/base/candidate.h @@ -0,0 +1,203 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_CANDIDATE_H_ +#define TALK_P2P_BASE_CANDIDATE_H_ + +#include +#include +#include +#include +#include +#include "talk/base/basictypes.h" +#include "talk/base/socketaddress.h" +#include "talk/p2p/base/constants.h" + +namespace cricket { + +// Candidate for ICE based connection discovery. + +class Candidate { + public: + // TODO: Match the ordering and param list as per RFC 5245 + // candidate-attribute syntax. http://tools.ietf.org/html/rfc5245#section-15.1 + Candidate() : component_(0), priority_(0), generation_(0) {} + Candidate(const std::string& id, int component, const std::string& protocol, + const talk_base::SocketAddress& address, uint32 priority, + const std::string& username, const std::string& password, + const std::string& type, const std::string& network_name, + uint32 generation, const std::string& foundation) + : id_(id), component_(component), protocol_(protocol), address_(address), + priority_(priority), username_(username), password_(password), + type_(type), network_name_(network_name), generation_(generation), + foundation_(foundation) { + } + + const std::string & id() const { return id_; } + void set_id(const std::string & id) { id_ = id; } + + int component() const { return component_; } + void set_component(int component) { component_ = component; } + + const std::string & protocol() const { return protocol_; } + void set_protocol(const std::string & protocol) { protocol_ = protocol; } + + const talk_base::SocketAddress & address() const { return address_; } + void set_address(const talk_base::SocketAddress & address) { + address_ = address; + } + + uint32 priority() const { return priority_; } + void set_priority(const uint32 priority) { priority_ = priority; } + +// void set_type_preference(uint32 type_preference) { +// priority_ = GetPriority(type_preference); +// } + + // Maps old preference (which was 0.0-1.0) to match priority (which + // is 0-2^32-1) to to match RFC 5245, section 4.1.2.1. Also see + // https://docs.google.com/a/google.com/document/d/ + // 1iNQDiwDKMh0NQOrCqbj3DKKRT0Dn5_5UJYhmZO-t7Uc/edit + float preference() const { + // The preference value is clamped to two decimal precision. + return static_cast(((priority_ >> 24) * 100 / 127) / 100.0); + } + + void set_preference(float preference) { + // Limiting priority to UINT_MAX when value exceeds uint32 max. + // This can happen for e.g. when preference = 3. + uint64 prio_val = static_cast(preference * 127) << 24; + priority_ = static_cast( + talk_base::_min(prio_val, static_cast(UINT_MAX))); + } + + const std::string & username() const { return username_; } + void set_username(const std::string & username) { username_ = username; } + + const std::string & password() const { return password_; } + void set_password(const std::string & password) { password_ = password; } + + const std::string & type() const { return type_; } + void set_type(const std::string & type) { type_ = type; } + + const std::string & network_name() const { return network_name_; } + void set_network_name(const std::string & network_name) { + network_name_ = network_name; + } + + // Candidates in a new generation replace those in the old generation. + uint32 generation() const { return generation_; } + void set_generation(uint32 generation) { generation_ = generation; } + const std::string generation_str() const { + std::ostringstream ost; + ost << generation_; + return ost.str(); + } + void set_generation_str(const std::string& str) { + std::istringstream ist(str); + ist >> generation_; + } + + const std::string& foundation() const { + return foundation_; + } + + void set_foundation(const std::string& foundation) { + foundation_ = foundation; + } + + const talk_base::SocketAddress & related_address() const { + return related_address_; + } + void set_related_address( + const talk_base::SocketAddress & related_address) { + related_address_ = related_address; + } + + // Determines whether this candidate is equivalent to the given one. + bool IsEquivalent(const Candidate& c) const { + // We ignore the network name, since that is just debug information, and + // the priority, since that should be the same if the rest is (and it's + // a float so equality checking is always worrisome). + return (id_ == c.id_) && + (component_ == c.component_) && + (protocol_ == c.protocol_) && + (address_ == c.address_) && + (username_ == c.username_) && + (password_ == c.password_) && + (type_ == c.type_) && + (generation_ == c.generation_) && + (foundation_ == c.foundation_) && + (related_address_ == c.related_address_); + } + + std::string ToString() const { + return ToStringInternal(false); + } + + std::string ToSensitiveString() const { + return ToStringInternal(true); + } + + uint32 GetPriority(uint32 type_preference) const { + // RFC 5245 - 4.1.2.1. + // priority = (2^24)*(type preference) + + // (2^8)*(local preference) + + // (2^0)*(256 - component ID) + int addr_pref = IPAddressPrecedence(address_.ipaddr()); + return (type_preference << 24) | (addr_pref << 8) | (256 - component_); + } + + private: + std::string ToStringInternal(bool sensitive) const { + std::ostringstream ost; + std::string address = sensitive ? address_.ToSensitiveString() : + address_.ToString(); + ost << "Cand[" << id_ << ":" << component_ << ":" + << type_ << ":" << protocol_ << ":" + << network_name_ << ":" << address << ":" + << username_ << ":" << password_ << "]"; + return ost.str(); + } + + std::string id_; + int component_; + std::string protocol_; + talk_base::SocketAddress address_; + uint32 priority_; + std::string username_; + std::string password_; + std::string type_; + std::string network_name_; + uint32 generation_; + std::string foundation_; + talk_base::SocketAddress related_address_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_CANDIDATE_H_ diff --git a/talk/p2p/base/common.h b/talk/p2p/base/common.h new file mode 100644 index 000000000..5a38180d6 --- /dev/null +++ b/talk/p2p/base/common.h @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_COMMON_H_ +#define TALK_P2P_BASE_COMMON_H_ + +#include "talk/base/logging.h" + +// Common log description format for jingle messages +#define LOG_J(sev, obj) LOG(sev) << "Jingle:" << obj->ToString() << ": " +#define LOG_JV(sev, obj) LOG_V(sev) << "Jingle:" << obj->ToString() << ": " + +#endif // TALK_P2P_BASE_COMMON_H_ diff --git a/talk/p2p/base/constants.cc b/talk/p2p/base/constants.cc new file mode 100644 index 000000000..992fe2dbf --- /dev/null +++ b/talk/p2p/base/constants.cc @@ -0,0 +1,263 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/constants.h" + +#include + +#include "talk/xmllite/qname.h" + +namespace cricket { + +const char NS_EMPTY[] = ""; +const char NS_JINGLE[] = "urn:xmpp:jingle:1"; +const char NS_JINGLE_DRAFT[] = "google:jingle"; +const char NS_GINGLE[] = "http://www.google.com/session"; + +// actions (aka or ) +const buzz::StaticQName QN_ACTION = { NS_EMPTY, "action" }; +const char LN_INITIATOR[] = "initiator"; +const buzz::StaticQName QN_INITIATOR = { NS_EMPTY, LN_INITIATOR }; +const buzz::StaticQName QN_CREATOR = { NS_EMPTY, "creator" }; + +const buzz::StaticQName QN_JINGLE = { NS_JINGLE, "jingle" }; +const buzz::StaticQName QN_JINGLE_CONTENT = { NS_JINGLE, "content" }; +const buzz::StaticQName QN_JINGLE_CONTENT_NAME = { NS_EMPTY, "name" }; +const buzz::StaticQName QN_JINGLE_CONTENT_MEDIA = { NS_EMPTY, "media" }; +const buzz::StaticQName QN_JINGLE_REASON = { NS_JINGLE, "reason" }; +const buzz::StaticQName QN_JINGLE_DRAFT_GROUP = { NS_JINGLE_DRAFT, "group" }; +const buzz::StaticQName QN_JINGLE_DRAFT_GROUP_TYPE = { NS_EMPTY, "type" }; +const char JINGLE_CONTENT_MEDIA_AUDIO[] = "audio"; +const char JINGLE_CONTENT_MEDIA_VIDEO[] = "video"; +const char JINGLE_CONTENT_MEDIA_DATA[] = "data"; +const char JINGLE_ACTION_SESSION_INITIATE[] = "session-initiate"; +const char JINGLE_ACTION_SESSION_INFO[] = "session-info"; +const char JINGLE_ACTION_SESSION_ACCEPT[] = "session-accept"; +const char JINGLE_ACTION_SESSION_TERMINATE[] = "session-terminate"; +const char JINGLE_ACTION_TRANSPORT_INFO[] = "transport-info"; +const char JINGLE_ACTION_TRANSPORT_ACCEPT[] = "transport-accept"; +const char JINGLE_ACTION_DESCRIPTION_INFO[] = "description-info"; + +const buzz::StaticQName QN_GINGLE_SESSION = { NS_GINGLE, "session" }; +const char GINGLE_ACTION_INITIATE[] = "initiate"; +const char GINGLE_ACTION_INFO[] = "info"; +const char GINGLE_ACTION_ACCEPT[] = "accept"; +const char GINGLE_ACTION_REJECT[] = "reject"; +const char GINGLE_ACTION_TERMINATE[] = "terminate"; +const char GINGLE_ACTION_CANDIDATES[] = "candidates"; +const char GINGLE_ACTION_UPDATE[] = "update"; + +const char LN_ERROR[] = "error"; +const buzz::StaticQName QN_GINGLE_REDIRECT = { NS_GINGLE, "redirect" }; +const char STR_REDIRECT_PREFIX[] = "xmpp:"; + +// Session Contents (aka Gingle +// or Jingle ) +const char LN_DESCRIPTION[] = "description"; +const char LN_PAYLOADTYPE[] = "payload-type"; +const buzz::StaticQName QN_ID = { NS_EMPTY, "id" }; +const buzz::StaticQName QN_SID = { NS_EMPTY, "sid" }; +const buzz::StaticQName QN_NAME = { NS_EMPTY, "name" }; +const buzz::StaticQName QN_CLOCKRATE = { NS_EMPTY, "clockrate" }; +const buzz::StaticQName QN_BITRATE = { NS_EMPTY, "bitrate" }; +const buzz::StaticQName QN_CHANNELS = { NS_EMPTY, "channels" }; +const buzz::StaticQName QN_WIDTH = { NS_EMPTY, "width" }; +const buzz::StaticQName QN_HEIGHT = { NS_EMPTY, "height" }; +const buzz::StaticQName QN_FRAMERATE = { NS_EMPTY, "framerate" }; +const char LN_NAME[] = "name"; +const char LN_VALUE[] = "value"; +const buzz::StaticQName QN_PAYLOADTYPE_PARAMETER_NAME = { NS_EMPTY, LN_NAME }; +const buzz::StaticQName QN_PAYLOADTYPE_PARAMETER_VALUE = { NS_EMPTY, LN_VALUE }; +const char PAYLOADTYPE_PARAMETER_BITRATE[] = "bitrate"; +const char PAYLOADTYPE_PARAMETER_HEIGHT[] = "height"; +const char PAYLOADTYPE_PARAMETER_WIDTH[] = "width"; +const char PAYLOADTYPE_PARAMETER_FRAMERATE[] = "framerate"; +const char LN_BANDWIDTH[] = "bandwidth"; + +const char CN_AUDIO[] = "audio"; +const char CN_VIDEO[] = "video"; +const char CN_DATA[] = "data"; +const char CN_OTHER[] = "main"; +// other SDP related strings +const char GROUP_TYPE_BUNDLE[] = "BUNDLE"; + +const char NS_JINGLE_RTP[] = "urn:xmpp:jingle:apps:rtp:1"; +const buzz::StaticQName QN_JINGLE_RTP_CONTENT = + { NS_JINGLE_RTP, LN_DESCRIPTION }; +const buzz::StaticQName QN_SSRC = { NS_EMPTY, "ssrc" }; +const buzz::StaticQName QN_JINGLE_RTP_PAYLOADTYPE = + { NS_JINGLE_RTP, LN_PAYLOADTYPE }; +const buzz::StaticQName QN_JINGLE_RTP_BANDWIDTH = + { NS_JINGLE_RTP, LN_BANDWIDTH }; +const buzz::StaticQName QN_JINGLE_RTCP_MUX = { NS_JINGLE_RTP, "rtcp-mux" }; +const buzz::StaticQName QN_PARAMETER = { NS_JINGLE_RTP, "parameter" }; +const buzz::StaticQName QN_JINGLE_RTP_HDREXT = + { NS_JINGLE_RTP, "rtp-hdrext" }; +const buzz::StaticQName QN_URI = { NS_EMPTY, "uri" }; + +const char NS_JINGLE_DRAFT_SCTP[] = "google:jingle:sctp"; +const buzz::StaticQName QN_JINGLE_DRAFT_SCTP_CONTENT = + { NS_JINGLE_DRAFT_SCTP, LN_DESCRIPTION }; +const buzz::StaticQName QN_JINGLE_DRAFT_SCTP_STREAM = + { NS_JINGLE_DRAFT_SCTP, "stream" }; + +const char NS_GINGLE_AUDIO[] = "http://www.google.com/session/phone"; +const buzz::StaticQName QN_GINGLE_AUDIO_CONTENT = + { NS_GINGLE_AUDIO, LN_DESCRIPTION }; +const buzz::StaticQName QN_GINGLE_AUDIO_PAYLOADTYPE = + { NS_GINGLE_AUDIO, LN_PAYLOADTYPE }; +const buzz::StaticQName QN_GINGLE_AUDIO_SRCID = { NS_GINGLE_AUDIO, "src-id" }; +const char NS_GINGLE_VIDEO[] = "http://www.google.com/session/video"; +const buzz::StaticQName QN_GINGLE_VIDEO_CONTENT = + { NS_GINGLE_VIDEO, LN_DESCRIPTION }; +const buzz::StaticQName QN_GINGLE_VIDEO_PAYLOADTYPE = + { NS_GINGLE_VIDEO, LN_PAYLOADTYPE }; +const buzz::StaticQName QN_GINGLE_VIDEO_SRCID = { NS_GINGLE_VIDEO, "src-id" }; +const buzz::StaticQName QN_GINGLE_VIDEO_BANDWIDTH = + { NS_GINGLE_VIDEO, LN_BANDWIDTH }; + +// Crypto support. +const buzz::StaticQName QN_ENCRYPTION = { NS_JINGLE_RTP, "encryption" }; +const buzz::StaticQName QN_ENCRYPTION_REQUIRED = { NS_EMPTY, "required" }; +const buzz::StaticQName QN_CRYPTO = { NS_JINGLE_RTP, "crypto" }; +const buzz::StaticQName QN_GINGLE_AUDIO_CRYPTO_USAGE = + { NS_GINGLE_AUDIO, "usage" }; +const buzz::StaticQName QN_GINGLE_VIDEO_CRYPTO_USAGE = + { NS_GINGLE_VIDEO, "usage" }; +const buzz::StaticQName QN_CRYPTO_SUITE = { NS_EMPTY, "crypto-suite" }; +const buzz::StaticQName QN_CRYPTO_KEY_PARAMS = { NS_EMPTY, "key-params" }; +const buzz::StaticQName QN_CRYPTO_TAG = { NS_EMPTY, "tag" }; +const buzz::StaticQName QN_CRYPTO_SESSION_PARAMS = + { NS_EMPTY, "session-params" }; + +// Transports and candidates. +const char LN_TRANSPORT[] = "transport"; +const char LN_CANDIDATE[] = "candidate"; +const buzz::StaticQName QN_UFRAG = { cricket::NS_EMPTY, "ufrag" }; +const buzz::StaticQName QN_PWD = { cricket::NS_EMPTY, "pwd" }; +const buzz::StaticQName QN_COMPONENT = { cricket::NS_EMPTY, "component" }; +const buzz::StaticQName QN_IP = { cricket::NS_EMPTY, "ip" }; +const buzz::StaticQName QN_PORT = { cricket::NS_EMPTY, "port" }; +const buzz::StaticQName QN_NETWORK = { cricket::NS_EMPTY, "network" }; +const buzz::StaticQName QN_GENERATION = { cricket::NS_EMPTY, "generation" }; +const buzz::StaticQName QN_PRIORITY = { cricket::NS_EMPTY, "priority" }; +const buzz::StaticQName QN_PROTOCOL = { cricket::NS_EMPTY, "protocol" }; +const char ICE_CANDIDATE_TYPE_PEER_STUN[] = "prflx"; +const char ICE_CANDIDATE_TYPE_SERVER_STUN[] = "srflx"; +// Minimum ufrag length is 4 characters as per RFC5245. We chose 16 because +// some internal systems expect username to be 16 bytes. +const int ICE_UFRAG_LENGTH = 16; +// Minimum password length of 22 characters as per RFC5245. We chose 24 because +// some internal systems expect password to be multiple of 4. +const int ICE_PWD_LENGTH = 24; +// TODO: This is media-specific, so might belong +// somewhere like media/base/constants.h +const int ICE_CANDIDATE_COMPONENT_RTP = 1; +const int ICE_CANDIDATE_COMPONENT_RTCP = 2; +const int ICE_CANDIDATE_COMPONENT_DEFAULT = 1; + +const buzz::StaticQName QN_FINGERPRINT = { cricket::NS_EMPTY, "fingerprint" }; +const buzz::StaticQName QN_FINGERPRINT_ALGORITHM = + { cricket::NS_EMPTY, "algorithm" }; +const buzz::StaticQName QN_FINGERPRINT_DIGEST = { cricket::NS_EMPTY, "digest" }; + +const char NS_JINGLE_ICE_UDP[] = "urn:xmpp:jingle:transports:ice-udp:1"; + +const char ICE_OPTION_GICE[] = "google-ice"; +const char NS_GINGLE_P2P[] = "http://www.google.com/transport/p2p"; +const buzz::StaticQName QN_GINGLE_P2P_TRANSPORT = + { NS_GINGLE_P2P, LN_TRANSPORT }; +const buzz::StaticQName QN_GINGLE_P2P_CANDIDATE = + { NS_GINGLE_P2P, LN_CANDIDATE }; +const buzz::StaticQName QN_GINGLE_P2P_UNKNOWN_CHANNEL_NAME = + { NS_GINGLE_P2P, "unknown-channel-name" }; +const buzz::StaticQName QN_GINGLE_CANDIDATE = { NS_GINGLE, LN_CANDIDATE }; +const buzz::StaticQName QN_ADDRESS = { cricket::NS_EMPTY, "address" }; +const buzz::StaticQName QN_USERNAME = { cricket::NS_EMPTY, "username" }; +const buzz::StaticQName QN_PASSWORD = { cricket::NS_EMPTY, "password" }; +const buzz::StaticQName QN_PREFERENCE = { cricket::NS_EMPTY, "preference" }; +const char GICE_CANDIDATE_TYPE_STUN[] = "stun"; +const char GICE_CHANNEL_NAME_RTP[] = "rtp"; +const char GICE_CHANNEL_NAME_RTCP[] = "rtcp"; +const char GICE_CHANNEL_NAME_VIDEO_RTP[] = "video_rtp"; +const char GICE_CHANNEL_NAME_VIDEO_RTCP[] = "video_rtcp"; +const char GICE_CHANNEL_NAME_DATA_RTP[] = "data_rtp"; +const char GICE_CHANNEL_NAME_DATA_RTCP[] = "data_rtcp"; + +// terminate reasons and errors +const char JINGLE_ERROR_BAD_REQUEST[] = "bad-request"; +const char JINGLE_ERROR_OUT_OF_ORDER[] = "out-of-order"; +const char JINGLE_ERROR_UNKNOWN_SESSION[] = "unknown-session"; + +// Call terminate reasons from XEP-166 +const char STR_TERMINATE_DECLINE[] = "decline"; +const char STR_TERMINATE_SUCCESS[] = "success"; +const char STR_TERMINATE_ERROR[] = "general-error"; +const char STR_TERMINATE_INCOMPATIBLE_PARAMETERS[] = "incompatible-parameters"; + +// Old terminate reasons used by cricket +const char STR_TERMINATE_CALL_ENDED[] = "call-ended"; +const char STR_TERMINATE_RECIPIENT_UNAVAILABLE[] = "recipient-unavailable"; +const char STR_TERMINATE_RECIPIENT_BUSY[] = "recipient-busy"; +const char STR_TERMINATE_INSUFFICIENT_FUNDS[] = "insufficient-funds"; +const char STR_TERMINATE_NUMBER_MALFORMED[] = "number-malformed"; +const char STR_TERMINATE_NUMBER_DISALLOWED[] = "number-disallowed"; +const char STR_TERMINATE_PROTOCOL_ERROR[] = "protocol-error"; +const char STR_TERMINATE_INTERNAL_SERVER_ERROR[] = "internal-server-error"; +const char STR_TERMINATE_UNKNOWN_ERROR[] = "unknown-error"; + +// Draft view and notify messages. +const char STR_JINGLE_DRAFT_CONTENT_NAME_VIDEO[] = "video"; +const char STR_JINGLE_DRAFT_CONTENT_NAME_AUDIO[] = "audio"; +const buzz::StaticQName QN_NICK = { cricket::NS_EMPTY, "nick" }; +const buzz::StaticQName QN_TYPE = { cricket::NS_EMPTY, "type" }; +const buzz::StaticQName QN_JINGLE_DRAFT_VIEW = { NS_JINGLE_DRAFT, "view" }; +const char STR_JINGLE_DRAFT_VIEW_TYPE_NONE[] = "none"; +const char STR_JINGLE_DRAFT_VIEW_TYPE_STATIC[] = "static"; +const buzz::StaticQName QN_JINGLE_DRAFT_PARAMS = { NS_JINGLE_DRAFT, "params" }; +const buzz::StaticQName QN_JINGLE_DRAFT_STREAMS = { NS_JINGLE_DRAFT, "streams" }; +const buzz::StaticQName QN_JINGLE_DRAFT_STREAM = { NS_JINGLE_DRAFT, "stream" }; +const buzz::StaticQName QN_DISPLAY = { cricket::NS_EMPTY, "display" }; +const buzz::StaticQName QN_CNAME = { cricket::NS_EMPTY, "cname" }; +const buzz::StaticQName QN_JINGLE_DRAFT_SSRC = { NS_JINGLE_DRAFT, "ssrc" }; +const buzz::StaticQName QN_JINGLE_DRAFT_SSRC_GROUP = + { NS_JINGLE_DRAFT, "ssrc-group" }; +const buzz::StaticQName QN_SEMANTICS = { cricket::NS_EMPTY, "semantics" }; +const buzz::StaticQName QN_JINGLE_LEGACY_NOTIFY = { NS_JINGLE_DRAFT, "notify" }; +const buzz::StaticQName QN_JINGLE_LEGACY_SOURCE = { NS_JINGLE_DRAFT, "source" }; + +const char NS_GINGLE_RAW[] = "http://www.google.com/transport/raw-udp"; +const buzz::StaticQName QN_GINGLE_RAW_TRANSPORT = { NS_GINGLE_RAW, "transport" }; +const buzz::StaticQName QN_GINGLE_RAW_CHANNEL = { NS_GINGLE_RAW, "channel" }; + +// old stuff +#ifdef FEATURE_ENABLE_VOICEMAIL +const char NS_VOICEMAIL[] = "http://www.google.com/session/voicemail"; +const buzz::StaticQName QN_VOICEMAIL_REGARDING = { NS_VOICEMAIL, "regarding" }; +#endif + +} // namespace cricket diff --git a/talk/p2p/base/constants.h b/talk/p2p/base/constants.h new file mode 100644 index 000000000..14558014e --- /dev/null +++ b/talk/p2p/base/constants.h @@ -0,0 +1,264 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_CONSTANTS_H_ +#define TALK_P2P_BASE_CONSTANTS_H_ + +#include +#include "talk/xmllite/qname.h" + +// This file contains constants related to signaling that are used in various +// classes in this directory. + +namespace cricket { + +// NS_ == namespace +// QN_ == buzz::QName (namespace + name) +// LN_ == "local name" == QName::LocalPart() +// these are useful when you need to find a tag +// that has different namespaces (like or ) + +extern const char NS_EMPTY[]; +extern const char NS_JINGLE[]; +extern const char NS_JINGLE_DRAFT[]; +extern const char NS_GINGLE[]; + +enum SignalingProtocol { + PROTOCOL_JINGLE, + PROTOCOL_GINGLE, + PROTOCOL_HYBRID, +}; + +// actions (aka Gingle or Jingle ) +extern const buzz::StaticQName QN_ACTION; +extern const char LN_INITIATOR[]; +extern const buzz::StaticQName QN_INITIATOR; +extern const buzz::StaticQName QN_CREATOR; + +extern const buzz::StaticQName QN_JINGLE; +extern const buzz::StaticQName QN_JINGLE_CONTENT; +extern const buzz::StaticQName QN_JINGLE_CONTENT_NAME; +extern const buzz::StaticQName QN_JINGLE_CONTENT_MEDIA; +extern const buzz::StaticQName QN_JINGLE_REASON; +extern const buzz::StaticQName QN_JINGLE_DRAFT_GROUP; +extern const buzz::StaticQName QN_JINGLE_DRAFT_GROUP_TYPE; +extern const char JINGLE_CONTENT_MEDIA_AUDIO[]; +extern const char JINGLE_CONTENT_MEDIA_VIDEO[]; +extern const char JINGLE_CONTENT_MEDIA_DATA[]; +extern const char JINGLE_ACTION_SESSION_INITIATE[]; +extern const char JINGLE_ACTION_SESSION_INFO[]; +extern const char JINGLE_ACTION_SESSION_ACCEPT[]; +extern const char JINGLE_ACTION_SESSION_TERMINATE[]; +extern const char JINGLE_ACTION_TRANSPORT_INFO[]; +extern const char JINGLE_ACTION_TRANSPORT_ACCEPT[]; +extern const char JINGLE_ACTION_DESCRIPTION_INFO[]; + +extern const buzz::StaticQName QN_GINGLE_SESSION; +extern const char GINGLE_ACTION_INITIATE[]; +extern const char GINGLE_ACTION_INFO[]; +extern const char GINGLE_ACTION_ACCEPT[]; +extern const char GINGLE_ACTION_REJECT[]; +extern const char GINGLE_ACTION_TERMINATE[]; +extern const char GINGLE_ACTION_CANDIDATES[]; +extern const char GINGLE_ACTION_UPDATE[]; + +extern const char LN_ERROR[]; +extern const buzz::StaticQName QN_GINGLE_REDIRECT; +extern const char STR_REDIRECT_PREFIX[]; + +// Session Contents (aka Gingle +// or Jingle ) +extern const char LN_DESCRIPTION[]; +extern const char LN_PAYLOADTYPE[]; +extern const buzz::StaticQName QN_ID; +extern const buzz::StaticQName QN_SID; +extern const buzz::StaticQName QN_NAME; +extern const buzz::StaticQName QN_CLOCKRATE; +extern const buzz::StaticQName QN_BITRATE; +extern const buzz::StaticQName QN_CHANNELS; +extern const buzz::StaticQName QN_PARAMETER; +extern const char LN_NAME[]; +extern const char LN_VALUE[]; +extern const buzz::StaticQName QN_PAYLOADTYPE_PARAMETER_NAME; +extern const buzz::StaticQName QN_PAYLOADTYPE_PARAMETER_VALUE; +extern const char PAYLOADTYPE_PARAMETER_BITRATE[]; +extern const char PAYLOADTYPE_PARAMETER_HEIGHT[]; +extern const char PAYLOADTYPE_PARAMETER_WIDTH[]; +extern const char PAYLOADTYPE_PARAMETER_FRAMERATE[]; +extern const char LN_BANDWIDTH[]; + +// CN_ == "content name". When we initiate a session, we choose the +// name, and when we receive a Gingle session, we provide default +// names (since Gingle has no content names). But when we receive a +// Jingle call, the content name can be anything, so don't rely on +// these values being the same as the ones received. +extern const char CN_AUDIO[]; +extern const char CN_VIDEO[]; +extern const char CN_DATA[]; +extern const char CN_OTHER[]; +// other SDP related strings +// GN stands for group name +extern const char GROUP_TYPE_BUNDLE[]; + +extern const char NS_JINGLE_RTP[]; +extern const buzz::StaticQName QN_JINGLE_RTP_CONTENT; +extern const buzz::StaticQName QN_SSRC; +extern const buzz::StaticQName QN_JINGLE_RTP_PAYLOADTYPE; +extern const buzz::StaticQName QN_JINGLE_RTP_BANDWIDTH; +extern const buzz::StaticQName QN_JINGLE_RTCP_MUX; +extern const buzz::StaticQName QN_JINGLE_RTP_HDREXT; +extern const buzz::StaticQName QN_URI; + +extern const char NS_JINGLE_DRAFT_SCTP[]; +extern const buzz::StaticQName QN_JINGLE_DRAFT_SCTP_CONTENT; +extern const buzz::StaticQName QN_JINGLE_DRAFT_SCTP_STREAM; + +extern const char NS_GINGLE_AUDIO[]; +extern const buzz::StaticQName QN_GINGLE_AUDIO_CONTENT; +extern const buzz::StaticQName QN_GINGLE_AUDIO_PAYLOADTYPE; +extern const buzz::StaticQName QN_GINGLE_AUDIO_SRCID; +extern const char NS_GINGLE_VIDEO[]; +extern const buzz::StaticQName QN_GINGLE_VIDEO_CONTENT; +extern const buzz::StaticQName QN_GINGLE_VIDEO_PAYLOADTYPE; +extern const buzz::StaticQName QN_GINGLE_VIDEO_SRCID; +extern const buzz::StaticQName QN_GINGLE_VIDEO_BANDWIDTH; + +// Crypto support. +extern const buzz::StaticQName QN_ENCRYPTION; +extern const buzz::StaticQName QN_ENCRYPTION_REQUIRED; +extern const buzz::StaticQName QN_CRYPTO; +extern const buzz::StaticQName QN_GINGLE_AUDIO_CRYPTO_USAGE; +extern const buzz::StaticQName QN_GINGLE_VIDEO_CRYPTO_USAGE; +extern const buzz::StaticQName QN_CRYPTO_SUITE; +extern const buzz::StaticQName QN_CRYPTO_KEY_PARAMS; +extern const buzz::StaticQName QN_CRYPTO_TAG; +extern const buzz::StaticQName QN_CRYPTO_SESSION_PARAMS; + +// Transports and candidates. +extern const char LN_TRANSPORT[]; +extern const char LN_CANDIDATE[]; +extern const buzz::StaticQName QN_JINGLE_P2P_TRANSPORT; +extern const buzz::StaticQName QN_JINGLE_P2P_CANDIDATE; +extern const buzz::StaticQName QN_UFRAG; +extern const buzz::StaticQName QN_COMPONENT; +extern const buzz::StaticQName QN_PWD; +extern const buzz::StaticQName QN_IP; +extern const buzz::StaticQName QN_PORT; +extern const buzz::StaticQName QN_NETWORK; +extern const buzz::StaticQName QN_GENERATION; +extern const buzz::StaticQName QN_PRIORITY; +extern const buzz::StaticQName QN_PROTOCOL; +extern const char ICE_CANDIDATE_TYPE_PEER_STUN[]; +extern const char ICE_CANDIDATE_TYPE_SERVER_STUN[]; +extern const int ICE_UFRAG_LENGTH; +extern const int ICE_PWD_LENGTH; +extern const int ICE_CANDIDATE_COMPONENT_RTP; +extern const int ICE_CANDIDATE_COMPONENT_RTCP; +extern const int ICE_CANDIDATE_COMPONENT_DEFAULT; + +extern const buzz::StaticQName QN_FINGERPRINT; +extern const buzz::StaticQName QN_FINGERPRINT_ALGORITHM; +extern const buzz::StaticQName QN_FINGERPRINT_DIGEST; + +extern const char NS_JINGLE_ICE_UDP[]; + +extern const char ICE_OPTION_GICE[]; +extern const char NS_GINGLE_P2P[]; +extern const buzz::StaticQName QN_GINGLE_P2P_TRANSPORT; +extern const buzz::StaticQName QN_GINGLE_P2P_CANDIDATE; +extern const buzz::StaticQName QN_GINGLE_P2P_UNKNOWN_CHANNEL_NAME; +extern const buzz::StaticQName QN_GINGLE_CANDIDATE; +extern const buzz::StaticQName QN_ADDRESS; +extern const buzz::StaticQName QN_USERNAME; +extern const buzz::StaticQName QN_PASSWORD; +extern const buzz::StaticQName QN_PREFERENCE; +extern const char GINGLE_CANDIDATE_TYPE_STUN[]; +extern const char GICE_CHANNEL_NAME_RTP[]; +extern const char GICE_CHANNEL_NAME_RTCP[]; +extern const char GICE_CHANNEL_NAME_VIDEO_RTP[]; +extern const char GICE_CHANNEL_NAME_VIDEO_RTCP[]; +extern const char GICE_CHANNEL_NAME_DATA_RTP[]; +extern const char GICE_CHANNEL_NAME_DATA_RTCP[]; + +extern const char NS_GINGLE_RAW[]; +extern const buzz::StaticQName QN_GINGLE_RAW_TRANSPORT; +extern const buzz::StaticQName QN_GINGLE_RAW_CHANNEL; + +// terminate reasons and errors: see http://xmpp.org/extensions/xep-0166.html +extern const char JINGLE_ERROR_BAD_REQUEST[]; // like parse error +// got transport-info before session-initiate, for example +extern const char JINGLE_ERROR_OUT_OF_ORDER[]; +extern const char JINGLE_ERROR_UNKNOWN_SESSION[]; + +// Call terminate reasons from XEP-166 +extern const char STR_TERMINATE_DECLINE[]; // polite reject +extern const char STR_TERMINATE_SUCCESS[]; // polite hangup +extern const char STR_TERMINATE_ERROR[]; // something bad happened +extern const char STR_TERMINATE_INCOMPATIBLE_PARAMETERS[]; // no codecs? + +// Old terminate reasons used by cricket +extern const char STR_TERMINATE_CALL_ENDED[]; +extern const char STR_TERMINATE_RECIPIENT_UNAVAILABLE[]; +extern const char STR_TERMINATE_RECIPIENT_BUSY[]; +extern const char STR_TERMINATE_INSUFFICIENT_FUNDS[]; +extern const char STR_TERMINATE_NUMBER_MALFORMED[]; +extern const char STR_TERMINATE_NUMBER_DISALLOWED[]; +extern const char STR_TERMINATE_PROTOCOL_ERROR[]; +extern const char STR_TERMINATE_INTERNAL_SERVER_ERROR[]; +extern const char STR_TERMINATE_UNKNOWN_ERROR[]; + +// Draft view and notify messages. +extern const char STR_JINGLE_DRAFT_CONTENT_NAME_VIDEO[]; +extern const char STR_JINGLE_DRAFT_CONTENT_NAME_AUDIO[]; +extern const buzz::StaticQName QN_NICK; +extern const buzz::StaticQName QN_TYPE; +extern const buzz::StaticQName QN_JINGLE_DRAFT_VIEW; +extern const char STR_JINGLE_DRAFT_VIEW_TYPE_NONE[]; +extern const char STR_JINGLE_DRAFT_VIEW_TYPE_STATIC[]; +extern const buzz::StaticQName QN_JINGLE_DRAFT_PARAMS; +extern const buzz::StaticQName QN_WIDTH; +extern const buzz::StaticQName QN_HEIGHT; +extern const buzz::StaticQName QN_FRAMERATE; +extern const buzz::StaticQName QN_JINGLE_DRAFT_STREAM; +extern const buzz::StaticQName QN_JINGLE_DRAFT_STREAMS; +extern const buzz::StaticQName QN_DISPLAY; +extern const buzz::StaticQName QN_CNAME; +extern const buzz::StaticQName QN_JINGLE_DRAFT_SSRC; +extern const buzz::StaticQName QN_JINGLE_DRAFT_SSRC_GROUP; +extern const buzz::StaticQName QN_SEMANTICS; +extern const buzz::StaticQName QN_JINGLE_LEGACY_NOTIFY; +extern const buzz::StaticQName QN_JINGLE_LEGACY_SOURCE; + +// old stuff +#ifdef FEATURE_ENABLE_VOICEMAIL +extern const char NS_VOICEMAIL[]; +extern const buzz::StaticQName QN_VOICEMAIL_REGARDING; +#endif + +} // namespace cricket + +#endif // TALK_P2P_BASE_CONSTANTS_H_ diff --git a/talk/p2p/base/dtlstransport.h b/talk/p2p/base/dtlstransport.h new file mode 100644 index 000000000..5c5253d66 --- /dev/null +++ b/talk/p2p/base/dtlstransport.h @@ -0,0 +1,144 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_P2P_BASE_DTLSTRANSPORT_H_ +#define TALK_P2P_BASE_DTLSTRANSPORT_H_ + +#include "talk/p2p/base/dtlstransportchannel.h" +#include "talk/p2p/base/transport.h" + +namespace talk_base { +class SSLIdentity; +} + +namespace cricket { + +class PortAllocator; + +// Base should be a descendant of cricket::Transport +template +class DtlsTransport : public Base { + public: + DtlsTransport(talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread, + const std::string& content_name, + PortAllocator* allocator, + talk_base::SSLIdentity* identity) + : Base(signaling_thread, worker_thread, content_name, allocator), + identity_(identity) { + } + + ~DtlsTransport() { + Base::DestroyAllChannels(); + } + + virtual bool ApplyLocalTransportDescription_w(TransportChannelImpl* + channel) { + talk_base::SSLFingerprint* local_fp = + Base::local_description()->identity_fingerprint.get(); + + if (local_fp) { + // Sanity check local fingerprint. + if (identity_) { + talk_base::scoped_ptr local_fp_tmp( + talk_base::SSLFingerprint::Create(local_fp->algorithm, + identity_)); + ASSERT(local_fp_tmp.get() != NULL); + if (!(*local_fp_tmp == *local_fp)) { + LOG(LS_WARNING) << "Local fingerprint does not match identity"; + return false; + } + } else { + LOG(LS_WARNING) + << "Local fingerprint provided but no identity available"; + return false; + } + } else { + identity_ = NULL; + } + + if (!channel->SetLocalIdentity(identity_)) + return false; + + // Apply the description in the base class. + return Base::ApplyLocalTransportDescription_w(channel); + } + + virtual bool NegotiateTransportDescription_w(ContentAction local_role) { + talk_base::SSLFingerprint* local_fp = + Base::local_description()->identity_fingerprint.get(); + talk_base::SSLFingerprint* remote_fp = + Base::remote_description()->identity_fingerprint.get(); + + if (remote_fp && local_fp) { + remote_fingerprint_.reset(new talk_base::SSLFingerprint(*remote_fp)); + } else if (local_fp && (local_role == CA_ANSWER)) { + LOG(LS_ERROR) + << "Local fingerprint supplied when caller didn't offer DTLS"; + return false; + } else { + // We are not doing DTLS + remote_fingerprint_.reset(new talk_base::SSLFingerprint( + "", NULL, 0)); + } + + // Now run the negotiation for the base class. + return Base::NegotiateTransportDescription_w(local_role); + } + + virtual DtlsTransportChannelWrapper* CreateTransportChannel(int component) { + return new DtlsTransportChannelWrapper( + this, Base::CreateTransportChannel(component)); + } + + virtual void DestroyTransportChannel(TransportChannelImpl* channel) { + // Kind of ugly, but this lets us do the exact inverse of the create. + DtlsTransportChannelWrapper* dtls_channel = + static_cast(channel); + TransportChannelImpl* base_channel = dtls_channel->channel(); + delete dtls_channel; + Base::DestroyTransportChannel(base_channel); + } + + private: + virtual void ApplyNegotiatedTransportDescription_w( + TransportChannelImpl* channel) { + channel->SetRemoteFingerprint( + remote_fingerprint_->algorithm, + reinterpret_cast(remote_fingerprint_-> + digest.data()), + remote_fingerprint_->digest.length()); + Base::ApplyNegotiatedTransportDescription_w(channel); + } + + talk_base::SSLIdentity* identity_; + talk_base::scoped_ptr remote_fingerprint_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_DTLSTRANSPORT_H_ diff --git a/talk/p2p/base/dtlstransportchannel.cc b/talk/p2p/base/dtlstransportchannel.cc new file mode 100644 index 000000000..9d6b92e70 --- /dev/null +++ b/talk/p2p/base/dtlstransportchannel.cc @@ -0,0 +1,563 @@ +/* + * libjingle + * Copyright 2011, Google Inc. + * Copyright 2011, RTFM, 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/p2p/base/dtlstransportchannel.h" + +#include "talk/base/buffer.h" +#include "talk/base/messagequeue.h" +#include "talk/base/stream.h" +#include "talk/base/sslstreamadapter.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/common.h" + +namespace cricket { + +// We don't pull the RTP constants from rtputils.h, to avoid a layer violation. +static const size_t kDtlsRecordHeaderLen = 13; +static const size_t kMaxDtlsPacketLen = 2048; +static const size_t kMinRtpPacketLen = 12; + +static bool IsDtlsPacket(const char* data, size_t len) { + const uint8* u = reinterpret_cast(data); + return (len >= kDtlsRecordHeaderLen && (u[0] > 19 && u[0] < 64)); +} +static bool IsRtpPacket(const char* data, size_t len) { + const uint8* u = reinterpret_cast(data); + return (len >= kMinRtpPacketLen && (u[0] & 0xC0) == 0x80); +} + +talk_base::StreamResult StreamInterfaceChannel::Read(void* buffer, + size_t buffer_len, + size_t* read, + int* error) { + if (state_ == talk_base::SS_CLOSED) + return talk_base::SR_EOS; + if (state_ == talk_base::SS_OPENING) + return talk_base::SR_BLOCK; + + return fifo_.Read(buffer, buffer_len, read, error); +} + +talk_base::StreamResult StreamInterfaceChannel::Write(const void* data, + size_t data_len, + size_t* written, + int* error) { + // Always succeeds, since this is an unreliable transport anyway. + // TODO: Should this block if channel_'s temporarily unwritable? + channel_->SendPacket(static_cast(data), data_len); + if (written) { + *written = data_len; + } + return talk_base::SR_SUCCESS; +} + +bool StreamInterfaceChannel::OnPacketReceived(const char* data, size_t size) { + // We force a read event here to ensure that we don't overflow our FIFO. + // Under high packet rate this can occur if we wait for the FIFO to post its + // own SE_READ. + bool ret = (fifo_.WriteAll(data, size, NULL, NULL) == talk_base::SR_SUCCESS); + if (ret) { + SignalEvent(this, talk_base::SE_READ, 0); + } + return ret; +} + +void StreamInterfaceChannel::OnEvent(talk_base::StreamInterface* stream, + int sig, int err) { + SignalEvent(this, sig, err); +} + +DtlsTransportChannelWrapper::DtlsTransportChannelWrapper( + Transport* transport, + TransportChannelImpl* channel) + : TransportChannelImpl(channel->content_name(), channel->component()), + transport_(transport), + worker_thread_(talk_base::Thread::Current()), + channel_(channel), + downward_(NULL), + dtls_state_(STATE_NONE), + local_identity_(NULL), + dtls_role_(talk_base::SSL_CLIENT) { + channel_->SignalReadableState.connect(this, + &DtlsTransportChannelWrapper::OnReadableState); + channel_->SignalWritableState.connect(this, + &DtlsTransportChannelWrapper::OnWritableState); + channel_->SignalReadPacket.connect(this, + &DtlsTransportChannelWrapper::OnReadPacket); + channel_->SignalReadyToSend.connect(this, + &DtlsTransportChannelWrapper::OnReadyToSend); + channel_->SignalRequestSignaling.connect(this, + &DtlsTransportChannelWrapper::OnRequestSignaling); + channel_->SignalCandidateReady.connect(this, + &DtlsTransportChannelWrapper::OnCandidateReady); + channel_->SignalCandidatesAllocationDone.connect(this, + &DtlsTransportChannelWrapper::OnCandidatesAllocationDone); + channel_->SignalRoleConflict.connect(this, + &DtlsTransportChannelWrapper::OnRoleConflict); + channel_->SignalRouteChange.connect(this, + &DtlsTransportChannelWrapper::OnRouteChange); +} + +DtlsTransportChannelWrapper::~DtlsTransportChannelWrapper() { +} + +void DtlsTransportChannelWrapper::Connect() { + // We should only get a single call to Connect. + ASSERT(dtls_state_ == STATE_NONE || + dtls_state_ == STATE_OFFERED || + dtls_state_ == STATE_ACCEPTED); + channel_->Connect(); +} + +void DtlsTransportChannelWrapper::Reset() { + channel_->Reset(); + set_writable(false); + set_readable(false); + + // Re-call SetupDtls() + if (!SetupDtls()) { + LOG_J(LS_ERROR, this) << "Error re-initializing DTLS"; + dtls_state_ = STATE_CLOSED; + return; + } + + dtls_state_ = STATE_ACCEPTED; +} + +bool DtlsTransportChannelWrapper::SetLocalIdentity(talk_base::SSLIdentity* + identity) { + // TODO(ekr@rtfm.com): Forbid this if Connect() has been called. + if (dtls_state_ != STATE_NONE) { + LOG_J(LS_ERROR, this) << "Can't set DTLS local identity in this state"; + return false; + } + + if (identity) { + local_identity_ = identity; + dtls_state_ = STATE_OFFERED; + } else { + LOG_J(LS_INFO, this) << "NULL DTLS identity supplied. Not doing DTLS"; + } + + return true; +} + +void DtlsTransportChannelWrapper::SetRole(TransportRole role) { + // TODO(ekr@rtfm.com): Forbid this if Connect() has been called. + ASSERT(dtls_state_ < STATE_ACCEPTED); + + // Set the role that is most conformant with RFC 5763, Section 5, bullet 1: + // The endpoint that is the offerer MUST [...] be prepared to receive + // a client_hello before it receives the answer. + // (IOW, the offerer is the server, and the answerer is the client). + dtls_role_ = (role == ROLE_CONTROLLING) ? + talk_base::SSL_SERVER : talk_base::SSL_CLIENT; + + channel_->SetRole(role); +} + +bool DtlsTransportChannelWrapper::SetRemoteFingerprint(const std::string& + digest_alg, + const uint8* digest, + size_t digest_len) { + // Allow SetRemoteFingerprint with a NULL digest even if SetLocalIdentity + // hasn't been called. + if (dtls_state_ > STATE_OFFERED || + (dtls_state_ == STATE_NONE && !digest_alg.empty())) { + LOG_J(LS_ERROR, this) << "Can't set DTLS remote settings in this state"; + return false; + } + + if (digest_alg.empty()) { + LOG_J(LS_INFO, this) << "Other side didn't support DTLS"; + dtls_state_ = STATE_NONE; + return true; + } + + // At this point we know we are doing DTLS + remote_fingerprint_value_.SetData(digest, digest_len); + remote_fingerprint_algorithm_ = digest_alg; + + if (!SetupDtls()) { + dtls_state_ = STATE_CLOSED; + return false; + } + + dtls_state_ = STATE_ACCEPTED; + return true; +} + +bool DtlsTransportChannelWrapper::SetupDtls() { + StreamInterfaceChannel* downward = + new StreamInterfaceChannel(worker_thread_, channel_); + + dtls_.reset(talk_base::SSLStreamAdapter::Create(downward)); + if (!dtls_) { + LOG_J(LS_ERROR, this) << "Failed to create DTLS adapter"; + delete downward; + return false; + } + + downward_ = downward; + + dtls_->SetIdentity(local_identity_->GetReference()); + dtls_->SetMode(talk_base::SSL_MODE_DTLS); + dtls_->SetServerRole(dtls_role_); + dtls_->SignalEvent.connect(this, &DtlsTransportChannelWrapper::OnDtlsEvent); + if (!dtls_->SetPeerCertificateDigest( + remote_fingerprint_algorithm_, + reinterpret_cast(remote_fingerprint_value_.data()), + remote_fingerprint_value_.length())) { + LOG_J(LS_ERROR, this) << "Couldn't set DTLS certificate digest"; + return false; + } + + // Set up DTLS-SRTP, if it's been enabled. + if (!srtp_ciphers_.empty()) { + if (!dtls_->SetDtlsSrtpCiphers(srtp_ciphers_)) { + LOG_J(LS_ERROR, this) << "Couldn't set DTLS-SRTP ciphers"; + return false; + } + } else { + LOG_J(LS_INFO, this) << "Not using DTLS"; + } + + LOG_J(LS_INFO, this) << "DTLS setup complete"; + return true; +} + +bool DtlsTransportChannelWrapper::SetSrtpCiphers(const std::vector& + ciphers) { + // SRTP ciphers must be set before the DTLS handshake starts. + // TODO(juberti): In multiplex situations, we may end up calling this function + // once for each muxed channel. Depending on the order of calls, this may + // result in slightly undesired results, e.g. 32 vs 80-bit MAC. The right way to + // fix this would be for the TransportProxyChannels to intersect the ciphers + // instead of overwriting, so that "80" followed by "32, 80" results in "80". + if (dtls_state_ != STATE_NONE && + dtls_state_ != STATE_OFFERED && + dtls_state_ != STATE_ACCEPTED) { + ASSERT(false); + return false; + } + + srtp_ciphers_ = ciphers; + return true; +} + +bool DtlsTransportChannelWrapper::GetSrtpCipher(std::string* cipher) { + if (dtls_state_ != STATE_OPEN) { + return false; + } + + return dtls_->GetDtlsSrtpCipher(cipher); +} + + +// Called from upper layers to send a media packet. +int DtlsTransportChannelWrapper::SendPacket(const char* data, size_t size, + int flags) { + int result = -1; + + switch (dtls_state_) { + case STATE_OFFERED: + // We don't know if we are doing DTLS yet, so we can't send a packet. + // TODO(ekr@rtfm.com): assert here? + result = -1; + break; + + case STATE_STARTED: + case STATE_ACCEPTED: + // Can't send data until the connection is active + result = -1; + break; + + case STATE_OPEN: + if (flags & PF_SRTP_BYPASS) { + ASSERT(!srtp_ciphers_.empty()); + if (!IsRtpPacket(data, size)) { + result = false; + break; + } + + result = channel_->SendPacket(data, size); + } else { + result = (dtls_->WriteAll(data, size, NULL, NULL) == + talk_base::SR_SUCCESS) ? static_cast(size) : -1; + } + break; + // Not doing DTLS. + case STATE_NONE: + result = channel_->SendPacket(data, size); + break; + + case STATE_CLOSED: // Can't send anything when we're closed. + return -1; + } + + return result; +} + +// The state transition logic here is as follows: +// (1) If we're not doing DTLS-SRTP, then the state is just the +// state of the underlying impl() +// (2) If we're doing DTLS-SRTP: +// - Prior to the DTLS handshake, the state is neither readable or +// writable +// - When the impl goes writable for the first time we +// start the DTLS handshake +// - Once the DTLS handshake completes, the state is that of the +// impl again +void DtlsTransportChannelWrapper::OnReadableState(TransportChannel* channel) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + ASSERT(channel == channel_); + LOG_J(LS_VERBOSE, this) + << "DTLSTransportChannelWrapper: channel readable state changed"; + + if (dtls_state_ == STATE_NONE || dtls_state_ == STATE_OPEN) { + set_readable(channel_->readable()); + // Note: SignalReadableState fired by set_readable. + } +} + +void DtlsTransportChannelWrapper::OnWritableState(TransportChannel* channel) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + ASSERT(channel == channel_); + LOG_J(LS_VERBOSE, this) + << "DTLSTransportChannelWrapper: channel writable state changed"; + + switch (dtls_state_) { + case STATE_NONE: + case STATE_OPEN: + set_writable(channel_->writable()); + // Note: SignalWritableState fired by set_writable. + break; + + case STATE_OFFERED: + // Do nothing + break; + + case STATE_ACCEPTED: + if (!MaybeStartDtls()) { + // This should never happen: + // Because we are operating in a nonblocking mode and all + // incoming packets come in via OnReadPacket(), which rejects + // packets in this state, the incoming queue must be empty. We + // ignore write errors, thus any errors must be because of + // configuration and therefore are our fault. + // Note that in non-debug configurations, failure in + // MaybeStartDtls() changes the state to STATE_CLOSED. + ASSERT(false); + } + break; + + case STATE_STARTED: + // Do nothing + break; + + case STATE_CLOSED: + // Should not happen. Do nothing + break; + } +} + +void DtlsTransportChannelWrapper::OnReadPacket(TransportChannel* channel, + const char* data, size_t size, + int flags) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + ASSERT(channel == channel_); + ASSERT(flags == 0); + + switch (dtls_state_) { + case STATE_NONE: + // We are not doing DTLS + SignalReadPacket(this, data, size, 0); + break; + + case STATE_OFFERED: + // Currently drop the packet, but we might in future + // decide to take this as evidence that the other + // side is ready to do DTLS and start the handshake + // on our end + LOG_J(LS_WARNING, this) << "Received packet before we know if we are doing " + << "DTLS or not; dropping"; + break; + + case STATE_ACCEPTED: + // Drop packets received before DTLS has actually started + LOG_J(LS_INFO, this) << "Dropping packet received before DTLS started"; + break; + + case STATE_STARTED: + case STATE_OPEN: + // We should only get DTLS or SRTP packets; STUN's already been demuxed. + // Is this potentially a DTLS packet? + if (IsDtlsPacket(data, size)) { + if (!HandleDtlsPacket(data, size)) { + LOG_J(LS_ERROR, this) << "Failed to handle DTLS packet"; + return; + } + } else { + // Not a DTLS packet; our handshake should be complete by now. + if (dtls_state_ != STATE_OPEN) { + LOG_J(LS_ERROR, this) << "Received non-DTLS packet before DTLS complete"; + return; + } + + // And it had better be a SRTP packet. + if (!IsRtpPacket(data, size)) { + LOG_J(LS_ERROR, this) << "Received unexpected non-DTLS packet"; + return; + } + + // Sanity check. + ASSERT(!srtp_ciphers_.empty()); + + // Signal this upwards as a bypass packet. + SignalReadPacket(this, data, size, PF_SRTP_BYPASS); + } + break; + case STATE_CLOSED: + // This shouldn't be happening. Drop the packet + break; + } +} + +void DtlsTransportChannelWrapper::OnReadyToSend(TransportChannel* channel) { + if (writable()) { + SignalReadyToSend(this); + } +} + +void DtlsTransportChannelWrapper::OnDtlsEvent(talk_base::StreamInterface* dtls, + int sig, int err) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + ASSERT(dtls == dtls_.get()); + if (sig & talk_base::SE_OPEN) { + // This is the first time. + LOG_J(LS_INFO, this) << "DTLS handshake complete"; + if (dtls_->GetState() == talk_base::SS_OPEN) { + // The check for OPEN shouldn't be necessary but let's make + // sure we don't accidentally frob the state if it's closed. + dtls_state_ = STATE_OPEN; + + set_readable(true); + set_writable(true); + } + } + if (sig & talk_base::SE_READ) { + char buf[kMaxDtlsPacketLen]; + size_t read; + if (dtls_->Read(buf, sizeof(buf), &read, NULL) == talk_base::SR_SUCCESS) { + SignalReadPacket(this, buf, read, 0); + } + } + if (sig & talk_base::SE_CLOSE) { + ASSERT(sig == talk_base::SE_CLOSE); // SE_CLOSE should be by itself. + if (!err) { + LOG_J(LS_INFO, this) << "DTLS channel closed"; + } else { + LOG_J(LS_INFO, this) << "DTLS channel error, code=" << err; + } + + set_readable(false); + set_writable(false); + dtls_state_ = STATE_CLOSED; + } +} + +bool DtlsTransportChannelWrapper::MaybeStartDtls() { + if (channel_->writable()) { + if (dtls_->StartSSLWithPeer()) { + LOG_J(LS_ERROR, this) << "Couldn't start DTLS handshake"; + dtls_state_ = STATE_CLOSED; + return false; + } + LOG_J(LS_INFO, this) + << "DtlsTransportChannelWrapper: Started DTLS handshake"; + + dtls_state_ = STATE_STARTED; + } + return true; +} + +// Called from OnReadPacket when a DTLS packet is received. +bool DtlsTransportChannelWrapper::HandleDtlsPacket(const char* data, + size_t size) { + // Sanity check we're not passing junk that + // just looks like DTLS. + const uint8* tmp_data = reinterpret_cast(data); + size_t tmp_size = size; + while (tmp_size > 0) { + if (tmp_size < kDtlsRecordHeaderLen) + return false; // Too short for the header + + size_t record_len = (tmp_data[11] << 8) | (tmp_data[12]); + if ((record_len + kDtlsRecordHeaderLen) > tmp_size) + return false; // Body too short + + tmp_data += record_len + kDtlsRecordHeaderLen; + tmp_size -= record_len + kDtlsRecordHeaderLen; + } + + // Looks good. Pass to the SIC which ends up being passed to + // the DTLS stack. + return downward_->OnPacketReceived(data, size); +} + +void DtlsTransportChannelWrapper::OnRequestSignaling( + TransportChannelImpl* channel) { + ASSERT(channel == channel_); + SignalRequestSignaling(this); +} + +void DtlsTransportChannelWrapper::OnCandidateReady( + TransportChannelImpl* channel, const Candidate& c) { + ASSERT(channel == channel_); + SignalCandidateReady(this, c); +} + +void DtlsTransportChannelWrapper::OnCandidatesAllocationDone( + TransportChannelImpl* channel) { + ASSERT(channel == channel_); + SignalCandidatesAllocationDone(this); +} + +void DtlsTransportChannelWrapper::OnRoleConflict( + TransportChannelImpl* channel) { + ASSERT(channel == channel_); + SignalRoleConflict(this); +} + +void DtlsTransportChannelWrapper::OnRouteChange( + TransportChannel* channel, const Candidate& candidate) { + ASSERT(channel == channel_); + SignalRouteChange(this, candidate); +} + +} // namespace cricket diff --git a/talk/p2p/base/dtlstransportchannel.h b/talk/p2p/base/dtlstransportchannel.h new file mode 100644 index 000000000..395df9ba0 --- /dev/null +++ b/talk/p2p/base/dtlstransportchannel.h @@ -0,0 +1,249 @@ +/* + * libjingle + * Copyright 2011, Google Inc. + * Copyright 2011, RTFM, 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. + */ + +#ifndef TALK_P2P_BASE_DTLSTRANSPORTCHANNEL_H_ +#define TALK_P2P_BASE_DTLSTRANSPORTCHANNEL_H_ + +#include +#include + +#include "talk/base/buffer.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sslstreamadapter.h" +#include "talk/base/stream.h" +#include "talk/p2p/base/transportchannelimpl.h" + +namespace cricket { + +// A bridge between a packet-oriented/channel-type interface on +// the bottom and a StreamInterface on the top. +class StreamInterfaceChannel : public talk_base::StreamInterface, + public sigslot::has_slots<> { + public: + StreamInterfaceChannel(talk_base::Thread* owner, TransportChannel* channel) + : channel_(channel), + state_(talk_base::SS_OPEN), + fifo_(kFifoSize, owner) { + fifo_.SignalEvent.connect(this, &StreamInterfaceChannel::OnEvent); + } + + // Push in a packet; this gets pulled out from Read(). + bool OnPacketReceived(const char* data, size_t size); + + // Implementations of StreamInterface + virtual talk_base::StreamState GetState() const { return state_; } + virtual void Close() { state_ = talk_base::SS_CLOSED; } + virtual talk_base::StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error); + virtual talk_base::StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + + private: + static const size_t kFifoSize = 8192; + + // Forward events + virtual void OnEvent(talk_base::StreamInterface* stream, int sig, int err); + + TransportChannel* channel_; // owned by DtlsTransportChannelWrapper + talk_base::StreamState state_; + talk_base::FifoBuffer fifo_; + + DISALLOW_COPY_AND_ASSIGN(StreamInterfaceChannel); +}; + + +// This class provides a DTLS SSLStreamAdapter inside a TransportChannel-style +// packet-based interface, wrapping an existing TransportChannel instance +// (e.g a P2PTransportChannel) +// Here's the way this works: +// +// DtlsTransportChannelWrapper { +// SSLStreamAdapter* dtls_ { +// StreamInterfaceChannel downward_ { +// TransportChannelImpl* channel_; +// } +// } +// } +// +// - Data which comes into DtlsTransportChannelWrapper from the underlying +// channel_ via OnReadPacket() is checked for whether it is DTLS +// or not, and if it is, is passed to DtlsTransportChannelWrapper:: +// HandleDtlsPacket, which pushes it into to downward_. +// dtls_ is listening for events on downward_, so it immediately calls +// downward_->Read(). +// +// - Data written to DtlsTransportChannelWrapper is passed either to +// downward_ or directly to channel_, depending on whether DTLS is +// negotiated and whether the flags include PF_SRTP_BYPASS +// +// - The SSLStreamAdapter writes to downward_->Write() +// which translates it into packet writes on channel_. +class DtlsTransportChannelWrapper : public TransportChannelImpl { + public: + enum State { + STATE_NONE, // No state or rejected. + STATE_OFFERED, // Our identity has been set. + STATE_ACCEPTED, // The other side sent a fingerprint. + STATE_STARTED, // We are negotiating. + STATE_OPEN, // Negotiation complete. + STATE_CLOSED // Connection closed. + }; + + // The parameters here are: + // transport -- the DtlsTransport that created us + // channel -- the TransportChannel we are wrapping + DtlsTransportChannelWrapper(Transport* transport, + TransportChannelImpl* channel); + virtual ~DtlsTransportChannelWrapper(); + + virtual void SetRole(TransportRole role); + // Returns current transport role of the channel. + virtual TransportRole GetRole() const { + return channel_->GetRole(); + } + virtual bool SetLocalIdentity(talk_base::SSLIdentity *identity); + + virtual bool SetRemoteFingerprint(const std::string& digest_alg, + const uint8* digest, + size_t digest_len); + virtual bool IsDtlsActive() const { return dtls_state_ != STATE_NONE; } + + // Called to send a packet (via DTLS, if turned on). + virtual int SendPacket(const char* data, size_t size, int flags); + + // TransportChannel calls that we forward to the wrapped transport. + virtual int SetOption(talk_base::Socket::Option opt, int value) { + return channel_->SetOption(opt, value); + } + virtual int GetError() { + return channel_->GetError(); + } + virtual bool GetStats(ConnectionInfos* infos) { + return channel_->GetStats(infos); + } + virtual void SetSessionId(const std::string& session_id) { + channel_->SetSessionId(session_id); + } + virtual const std::string& SessionId() const { + return channel_->SessionId(); + } + + // Set up the ciphers to use for DTLS-SRTP. If this method is not called + // before DTLS starts, or |ciphers| is empty, SRTP keys won't be negotiated. + // This method should be called before SetupDtls. + virtual bool SetSrtpCiphers(const std::vector& ciphers); + + // Find out which DTLS-SRTP cipher was negotiated + virtual bool GetSrtpCipher(std::string* cipher); + + // Once DTLS has established (i.e., this channel is writable), this method + // extracts the keys negotiated during the DTLS handshake, for use in external + // encryption. DTLS-SRTP uses this to extract the needed SRTP keys. + // See the SSLStreamAdapter documentation for info on the specific parameters. + virtual bool ExportKeyingMaterial(const std::string& label, + const uint8* context, + size_t context_len, + bool use_context, + uint8* result, + size_t result_len) { + return (dtls_.get()) ? dtls_->ExportKeyingMaterial(label, context, + context_len, + use_context, + result, result_len) + : false; + } + + // TransportChannelImpl calls. + virtual Transport* GetTransport() { + return transport_; + } + virtual void SetTiebreaker(uint64 tiebreaker) { + channel_->SetTiebreaker(tiebreaker); + } + virtual void SetIceProtocolType(IceProtocolType type) { + channel_->SetIceProtocolType(type); + } + virtual void SetIceCredentials(const std::string& ice_ufrag, + const std::string& ice_pwd) { + channel_->SetIceCredentials(ice_ufrag, ice_pwd); + } + virtual void SetRemoteIceCredentials(const std::string& ice_ufrag, + const std::string& ice_pwd) { + channel_->SetRemoteIceCredentials(ice_ufrag, ice_pwd); + } + virtual void SetRemoteIceMode(IceMode mode) { + channel_->SetRemoteIceMode(mode); + } + + virtual void Connect(); + virtual void Reset(); + + virtual void OnSignalingReady() { + channel_->OnSignalingReady(); + } + virtual void OnCandidate(const Candidate& candidate) { + channel_->OnCandidate(candidate); + } + + // Needed by DtlsTransport. + TransportChannelImpl* channel() { return channel_; } + + private: + void OnReadableState(TransportChannel* channel); + void OnWritableState(TransportChannel* channel); + void OnReadPacket(TransportChannel* channel, const char* data, size_t size, + int flags); + void OnReadyToSend(TransportChannel* channel); + void OnDtlsEvent(talk_base::StreamInterface* stream_, int sig, int err); + bool SetupDtls(); + bool MaybeStartDtls(); + bool HandleDtlsPacket(const char* data, size_t size); + void OnRequestSignaling(TransportChannelImpl* channel); + void OnCandidateReady(TransportChannelImpl* channel, const Candidate& c); + void OnCandidatesAllocationDone(TransportChannelImpl* channel); + void OnRoleConflict(TransportChannelImpl* channel); + void OnRouteChange(TransportChannel* channel, const Candidate& candidate); + + Transport* transport_; // The transport_ that created us. + talk_base::Thread* worker_thread_; // Everything should occur on this thread. + TransportChannelImpl* channel_; // Underlying channel, owned by transport_. + talk_base::scoped_ptr dtls_; // The DTLS stream + StreamInterfaceChannel* downward_; // Wrapper for channel_, owned by dtls_. + std::vector srtp_ciphers_; // SRTP ciphers to use with DTLS. + State dtls_state_; + talk_base::SSLIdentity* local_identity_; + talk_base::SSLRole dtls_role_; + talk_base::Buffer remote_fingerprint_value_; + std::string remote_fingerprint_algorithm_; + + DISALLOW_COPY_AND_ASSIGN(DtlsTransportChannelWrapper); +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_DTLSTRANSPORTCHANNEL_H_ diff --git a/talk/p2p/base/dtlstransportchannel_unittest.cc b/talk/p2p/base/dtlstransportchannel_unittest.cc new file mode 100644 index 000000000..8de3b0736 --- /dev/null +++ b/talk/p2p/base/dtlstransportchannel_unittest.cc @@ -0,0 +1,564 @@ +/* + * libjingle + * Copyright 2011, Google Inc. + * Copyright 2011, RTFM, 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 + +#include "talk/base/common.h" +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/fakesession.h" +#include "talk/base/ssladapter.h" +#include "talk/base/sslidentity.h" +#include "talk/base/sslstreamadapter.h" +#include "talk/p2p/base/dtlstransport.h" + +#define MAYBE_SKIP_TEST(feature) \ + if (!(talk_base::SSLStreamAdapter::feature())) { \ + LOG(LS_INFO) << "Feature disabled... skipping"; \ + return; \ + } + +static const char AES_CM_128_HMAC_SHA1_80[] = "AES_CM_128_HMAC_SHA1_80"; +static const char kIceUfrag1[] = "TESTICEUFRAG0001"; +static const char kIcePwd1[] = "TESTICEPWD00000000000001"; +static const size_t kPacketNumOffset = 8; +static const size_t kPacketHeaderLen = 12; + +static bool IsRtpLeadByte(uint8 b) { + return ((b & 0xC0) == 0x80); +} + +class DtlsTestClient : public sigslot::has_slots<> { + public: + DtlsTestClient(const std::string& name, + talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread) : + name_(name), + signaling_thread_(signaling_thread), + worker_thread_(worker_thread), + protocol_(cricket::ICEPROTO_GOOGLE), + packet_size_(0), + use_dtls_srtp_(false), + negotiated_dtls_(false), + received_dtls_client_hello_(false), + received_dtls_server_hello_(false) { + } + void SetIceProtocol(cricket::TransportProtocol proto) { + protocol_ = proto; + } + void CreateIdentity() { + identity_.reset(talk_base::SSLIdentity::Generate(name_)); + } + void SetupSrtp() { + ASSERT(identity_.get() != NULL); + use_dtls_srtp_ = true; + } + void SetupChannels(int count, cricket::TransportRole role) { + transport_.reset(new cricket::DtlsTransport( + signaling_thread_, worker_thread_, "dtls content name", NULL, + identity_.get())); + transport_->SetAsync(true); + transport_->SetRole(role); + transport_->SetTiebreaker((role == cricket::ROLE_CONTROLLING) ? 1 : 2); + transport_->SignalWritableState.connect(this, + &DtlsTestClient::OnTransportWritableState); + + for (int i = 0; i < count; ++i) { + cricket::DtlsTransportChannelWrapper* channel = + static_cast( + transport_->CreateChannel(i)); + ASSERT_TRUE(channel != NULL); + channel->SignalWritableState.connect(this, + &DtlsTestClient::OnTransportChannelWritableState); + channel->SignalReadPacket.connect(this, + &DtlsTestClient::OnTransportChannelReadPacket); + channels_.push_back(channel); + + // Hook the raw packets so that we can verify they are encrypted. + channel->channel()->SignalReadPacket.connect( + this, &DtlsTestClient::OnFakeTransportChannelReadPacket); + } + } + cricket::FakeTransportChannel* GetFakeChannel(int component) { + cricket::TransportChannelImpl* ch = transport_->GetChannel(component); + cricket::DtlsTransportChannelWrapper* wrapper = + static_cast(ch); + return (wrapper) ? + static_cast(wrapper->channel()) : NULL; + } + + // Offer DTLS if we have an identity; pass in a remote fingerprint only if + // both sides support DTLS. + void Negotiate(DtlsTestClient* peer) { + Negotiate(identity_.get(), (identity_) ? peer->identity_.get() : NULL); + } + + // Allow any DTLS configuration to be specified (including invalid ones). + void Negotiate(talk_base::SSLIdentity* local_identity, + talk_base::SSLIdentity* remote_identity) { + talk_base::scoped_ptr local_fingerprint; + talk_base::scoped_ptr remote_fingerprint; + if (local_identity) { + local_fingerprint.reset(talk_base::SSLFingerprint::Create( + talk_base::DIGEST_SHA_1, local_identity)); + ASSERT_TRUE(local_fingerprint.get() != NULL); + } + if (remote_identity) { + remote_fingerprint.reset(talk_base::SSLFingerprint::Create( + talk_base::DIGEST_SHA_1, remote_identity)); + ASSERT_TRUE(remote_fingerprint.get() != NULL); + } + if (use_dtls_srtp_) { + for (std::vector::iterator it = + channels_.begin(); it != channels_.end(); ++it) { + std::vector ciphers; + ciphers.push_back(AES_CM_128_HMAC_SHA1_80); + ASSERT_TRUE((*it)->SetSrtpCiphers(ciphers)); + } + } + + std::string transport_type = (protocol_ == cricket::ICEPROTO_GOOGLE) ? + cricket::NS_GINGLE_P2P : cricket::NS_JINGLE_ICE_UDP; + cricket::TransportDescription local_desc( + transport_type, std::vector(), kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, local_fingerprint.release(), + cricket::Candidates()); + ASSERT_TRUE(transport_->SetLocalTransportDescription(local_desc, + cricket::CA_OFFER)); + cricket::TransportDescription remote_desc( + transport_type, std::vector(), kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, remote_fingerprint.release(), + cricket::Candidates()); + ASSERT_TRUE(transport_->SetRemoteTransportDescription(remote_desc, + cricket::CA_ANSWER)); + + negotiated_dtls_ = (local_identity && remote_identity); + } + + bool Connect(DtlsTestClient* peer) { + transport_->ConnectChannels(); + transport_->SetDestination(peer->transport_.get()); + return true; + } + + bool writable() const { return transport_->writable(); } + + void CheckRole(talk_base::SSLRole role) { + if (role == talk_base::SSL_CLIENT) { + ASSERT_FALSE(received_dtls_client_hello_); + ASSERT_TRUE(received_dtls_server_hello_); + } else { + ASSERT_TRUE(received_dtls_client_hello_); + ASSERT_FALSE(received_dtls_server_hello_); + } + } + + void CheckSrtp(const std::string& expected_cipher) { + for (std::vector::iterator it = + channels_.begin(); it != channels_.end(); ++it) { + std::string cipher; + + bool rv = (*it)->GetSrtpCipher(&cipher); + if (negotiated_dtls_ && !expected_cipher.empty()) { + ASSERT_TRUE(rv); + + ASSERT_EQ(cipher, expected_cipher); + } else { + ASSERT_FALSE(rv); + } + } + } + + void SendPackets(size_t channel, size_t size, size_t count, bool srtp) { + ASSERT(channel < channels_.size()); + talk_base::scoped_array packet(new char[size]); + size_t sent = 0; + do { + // Fill the packet with a known value and a sequence number to check + // against, and make sure that it doesn't look like DTLS. + memset(packet.get(), sent & 0xff, size); + packet[0] = (srtp) ? 0x80 : 0x00; + talk_base::SetBE32(packet.get() + kPacketNumOffset, sent); + + // Only set the bypass flag if we've activated DTLS. + int flags = (identity_.get() && srtp) ? cricket::PF_SRTP_BYPASS : 0; + int rv = channels_[channel]->SendPacket(packet.get(), size, flags); + ASSERT_GT(rv, 0); + ASSERT_EQ(size, static_cast(rv)); + ++sent; + } while (sent < count); + } + + void ExpectPackets(size_t channel, size_t size) { + packet_size_ = size; + received_.clear(); + } + + size_t NumPacketsReceived() { + return received_.size(); + } + + bool VerifyPacket(const char* data, size_t size, uint32* out_num) { + if (size != packet_size_ || + (data[0] != 0 && static_cast(data[0]) != 0x80)) { + return false; + } + uint32 packet_num = talk_base::GetBE32(data + kPacketNumOffset); + for (size_t i = kPacketHeaderLen; i < size; ++i) { + if (static_cast(data[i]) != (packet_num & 0xff)) { + return false; + } + } + if (out_num) { + *out_num = packet_num; + } + return true; + } + bool VerifyEncryptedPacket(const char* data, size_t size) { + // This is an encrypted data packet; let's make sure it's mostly random; + // less than 10% of the bytes should be equal to the cleartext packet. + if (size <= packet_size_) { + return false; + } + uint32 packet_num = talk_base::GetBE32(data + kPacketNumOffset); + int num_matches = 0; + for (size_t i = kPacketNumOffset; i < size; ++i) { + if (static_cast(data[i]) == (packet_num & 0xff)) { + ++num_matches; + } + } + return (num_matches < ((static_cast(size) - 5) / 10)); + } + + // Transport callbacks + void OnTransportWritableState(cricket::Transport* transport) { + LOG(LS_INFO) << name_ << ": is writable"; + } + + // Transport channel callbacks + void OnTransportChannelWritableState(cricket::TransportChannel* channel) { + LOG(LS_INFO) << name_ << ": Channel '" << channel->component() + << "' is writable"; + } + + void OnTransportChannelReadPacket(cricket::TransportChannel* channel, + const char* data, size_t size, + int flags) { + uint32 packet_num = 0; + ASSERT_TRUE(VerifyPacket(data, size, &packet_num)); + received_.insert(packet_num); + // Only DTLS-SRTP packets should have the bypass flag set. + int expected_flags = (identity_.get() && IsRtpLeadByte(data[0])) ? + cricket::PF_SRTP_BYPASS : 0; + ASSERT_EQ(expected_flags, flags); + } + + // Hook into the raw packet stream to make sure DTLS packets are encrypted. + void OnFakeTransportChannelReadPacket(cricket::TransportChannel* channel, + const char* data, size_t size, + int flags) { + // Flags shouldn't be set on the underlying TransportChannel packets. + ASSERT_EQ(0, flags); + + // Look at the handshake packets to see what role we played. + // Check that non-handshake packets are DTLS data or SRTP bypass. + if (negotiated_dtls_) { + if (data[0] == 22 && size > 17) { + if (data[13] == 1) { + received_dtls_client_hello_ = true; + } else if (data[13] == 2) { + received_dtls_server_hello_ = true; + } + } else if (!(data[0] >= 20 && data[0] <= 22)) { + ASSERT_TRUE(data[0] == 23 || IsRtpLeadByte(data[0])); + if (data[0] == 23) { + ASSERT_TRUE(VerifyEncryptedPacket(data, size)); + } else if (IsRtpLeadByte(data[0])) { + ASSERT_TRUE(VerifyPacket(data, size, NULL)); + } + } + } + } + + private: + std::string name_; + talk_base::Thread* signaling_thread_; + talk_base::Thread* worker_thread_; + cricket::TransportProtocol protocol_; + talk_base::scoped_ptr identity_; + talk_base::scoped_ptr transport_; + std::vector channels_; + size_t packet_size_; + std::set received_; + bool use_dtls_srtp_; + bool negotiated_dtls_; + bool received_dtls_client_hello_; + bool received_dtls_server_hello_; +}; + + +class DtlsTransportChannelTest : public testing::Test { + public: + static void SetUpTestCase() { + talk_base::InitializeSSL(); + } + + DtlsTransportChannelTest() : + client1_("P1", talk_base::Thread::Current(), + talk_base::Thread::Current()), + client2_("P2", talk_base::Thread::Current(), + talk_base::Thread::Current()), + channel_ct_(1), + use_dtls_(false), + use_dtls_srtp_(false) { + } + + void SetChannelCount(size_t channel_ct) { + channel_ct_ = channel_ct; + } + void PrepareDtls(bool c1, bool c2) { + if (c1) { + client1_.CreateIdentity(); + } + if (c2) { + client2_.CreateIdentity(); + } + if (c1 && c2) + use_dtls_ = true; + } + void PrepareDtlsSrtp(bool c1, bool c2) { + if (!use_dtls_) + return; + + if (c1) + client1_.SetupSrtp(); + if (c2) + client2_.SetupSrtp(); + + if (c1 && c2) + use_dtls_srtp_ = true; + } + + bool Connect() { + Negotiate(); + + bool rv = client1_.Connect(&client2_); + EXPECT_TRUE(rv); + if (!rv) + return false; + + EXPECT_TRUE_WAIT(client1_.writable() && client2_.writable(), 10000); + if (!client1_.writable() || !client2_.writable()) + return false; + + // Check that we used the right roles. + if (use_dtls_) { + client1_.CheckRole(talk_base::SSL_SERVER); + client2_.CheckRole(talk_base::SSL_CLIENT); + } + + // Check that we negotiated the right ciphers. + if (use_dtls_srtp_) { + client1_.CheckSrtp(AES_CM_128_HMAC_SHA1_80); + client2_.CheckSrtp(AES_CM_128_HMAC_SHA1_80); + } else { + client1_.CheckSrtp(""); + client2_.CheckSrtp(""); + } + + return true; + } + void Negotiate() { + client1_.SetupChannels(channel_ct_, cricket::ROLE_CONTROLLING); + client2_.SetupChannels(channel_ct_, cricket::ROLE_CONTROLLED); + client2_.Negotiate(&client1_); + client1_.Negotiate(&client2_); + } + + void TestTransfer(size_t channel, size_t size, size_t count, bool srtp) { + LOG(LS_INFO) << "Expect packets, size=" << size; + client2_.ExpectPackets(channel, size); + client1_.SendPackets(channel, size, count, srtp); + EXPECT_EQ_WAIT(count, client2_.NumPacketsReceived(), 10000); + } + + protected: + DtlsTestClient client1_; + DtlsTestClient client2_; + int channel_ct_; + bool use_dtls_; + bool use_dtls_srtp_; +}; + +// Test that transport negotiation of ICE, no DTLS works properly. +TEST_F(DtlsTransportChannelTest, TestChannelSetupIce) { + client1_.SetIceProtocol(cricket::ICEPROTO_RFC5245); + client2_.SetIceProtocol(cricket::ICEPROTO_RFC5245); + Negotiate(); + cricket::FakeTransportChannel* channel1 = client1_.GetFakeChannel(0); + cricket::FakeTransportChannel* channel2 = client2_.GetFakeChannel(0); + ASSERT_TRUE(channel1 != NULL); + ASSERT_TRUE(channel2 != NULL); + EXPECT_EQ(cricket::ROLE_CONTROLLING, channel1->GetRole()); + EXPECT_EQ(1U, channel1->tiebreaker()); + EXPECT_EQ(cricket::ICEPROTO_RFC5245, channel1->protocol()); + EXPECT_EQ(kIceUfrag1, channel1->ice_ufrag()); + EXPECT_EQ(kIcePwd1, channel1->ice_pwd()); + EXPECT_EQ(cricket::ROLE_CONTROLLED, channel2->GetRole()); + EXPECT_EQ(2U, channel2->tiebreaker()); + EXPECT_EQ(cricket::ICEPROTO_RFC5245, channel2->protocol()); +} + +// Test that transport negotiation of GICE, no DTLS works properly. +TEST_F(DtlsTransportChannelTest, TestChannelSetupGice) { + client1_.SetIceProtocol(cricket::ICEPROTO_GOOGLE); + client2_.SetIceProtocol(cricket::ICEPROTO_GOOGLE); + Negotiate(); + cricket::FakeTransportChannel* channel1 = client1_.GetFakeChannel(0); + cricket::FakeTransportChannel* channel2 = client2_.GetFakeChannel(0); + ASSERT_TRUE(channel1 != NULL); + ASSERT_TRUE(channel2 != NULL); + EXPECT_EQ(cricket::ROLE_CONTROLLING, channel1->GetRole()); + EXPECT_EQ(1U, channel1->tiebreaker()); + EXPECT_EQ(cricket::ICEPROTO_GOOGLE, channel1->protocol()); + EXPECT_EQ(kIceUfrag1, channel1->ice_ufrag()); + EXPECT_EQ(kIcePwd1, channel1->ice_pwd()); + EXPECT_EQ(cricket::ROLE_CONTROLLED, channel2->GetRole()); + EXPECT_EQ(2U, channel2->tiebreaker()); + EXPECT_EQ(cricket::ICEPROTO_GOOGLE, channel2->protocol()); +} + +// Connect without DTLS, and transfer some data. +TEST_F(DtlsTransportChannelTest, TestTransfer) { + ASSERT_TRUE(Connect()); + TestTransfer(0, 1000, 100, false); +} + +// Create two channels without DTLS, and transfer some data. +TEST_F(DtlsTransportChannelTest, TestTransferTwoChannels) { + SetChannelCount(2); + ASSERT_TRUE(Connect()); + TestTransfer(0, 1000, 100, false); + TestTransfer(1, 1000, 100, false); +} + +// Connect without DTLS, and transfer SRTP data. +TEST_F(DtlsTransportChannelTest, TestTransferSrtp) { + ASSERT_TRUE(Connect()); + TestTransfer(0, 1000, 100, true); +} + +// Create two channels without DTLS, and transfer SRTP data. +TEST_F(DtlsTransportChannelTest, TestTransferSrtpTwoChannels) { + SetChannelCount(2); + ASSERT_TRUE(Connect()); + TestTransfer(0, 1000, 100, true); + TestTransfer(1, 1000, 100, true); +} + +// Connect with DTLS, and transfer some data. +TEST_F(DtlsTransportChannelTest, TestTransferDtls) { + MAYBE_SKIP_TEST(HaveDtls); + PrepareDtls(true, true); + ASSERT_TRUE(Connect()); + TestTransfer(0, 1000, 100, false); +} + +// Create two channels with DTLS, and transfer some data. +TEST_F(DtlsTransportChannelTest, TestTransferDtlsTwoChannels) { + MAYBE_SKIP_TEST(HaveDtls); + SetChannelCount(2); + PrepareDtls(true, true); + ASSERT_TRUE(Connect()); + TestTransfer(0, 1000, 100, false); + TestTransfer(1, 1000, 100, false); +} + +// Connect with A doing DTLS and B not, and transfer some data. +TEST_F(DtlsTransportChannelTest, TestTransferDtlsRejected) { + PrepareDtls(true, false); + ASSERT_TRUE(Connect()); + TestTransfer(0, 1000, 100, false); +} + +// Connect with B doing DTLS and A not, and transfer some data. +TEST_F(DtlsTransportChannelTest, TestTransferDtlsNotOffered) { + PrepareDtls(false, true); + ASSERT_TRUE(Connect()); + TestTransfer(0, 1000, 100, false); +} + +// Connect with DTLS, negotiate DTLS-SRTP, and transfer SRTP using bypass. +TEST_F(DtlsTransportChannelTest, TestTransferDtlsSrtp) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + PrepareDtls(true, true); + PrepareDtlsSrtp(true, true); + ASSERT_TRUE(Connect()); + TestTransfer(0, 1000, 100, true); +} + + +// Connect with DTLS. A does DTLS-SRTP but B does not. +TEST_F(DtlsTransportChannelTest, TestTransferDtlsSrtpRejected) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + PrepareDtls(true, true); + PrepareDtlsSrtp(true, false); + ASSERT_TRUE(Connect()); +} + +// Connect with DTLS. B does DTLS-SRTP but A does not. +TEST_F(DtlsTransportChannelTest, TestTransferDtlsSrtpNotOffered) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + PrepareDtls(true, true); + PrepareDtlsSrtp(false, true); + ASSERT_TRUE(Connect()); +} + +// Create two channels with DTLS, negotiate DTLS-SRTP, and transfer bypass SRTP. +TEST_F(DtlsTransportChannelTest, TestTransferDtlsSrtpTwoChannels) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + SetChannelCount(2); + PrepareDtls(true, true); + PrepareDtlsSrtp(true, true); + ASSERT_TRUE(Connect()); + TestTransfer(0, 1000, 100, true); + TestTransfer(1, 1000, 100, true); +} + +// Create a single channel with DTLS, and send normal data and SRTP data on it. +TEST_F(DtlsTransportChannelTest, TestTransferDtlsSrtpDemux) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + PrepareDtls(true, true); + PrepareDtlsSrtp(true, true); + ASSERT_TRUE(Connect()); + TestTransfer(0, 1000, 100, false); + TestTransfer(0, 1000, 100, true); +} diff --git a/talk/p2p/base/fakesession.h b/talk/p2p/base/fakesession.h new file mode 100644 index 000000000..3a825dd61 --- /dev/null +++ b/talk/p2p/base/fakesession.h @@ -0,0 +1,445 @@ +/* + * libjingle + * Copyright 2009, 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. + */ + +#ifndef TALK_P2P_BASE_FAKESESSION_H_ +#define TALK_P2P_BASE_FAKESESSION_H_ + +#include +#include +#include + +#include "talk/base/buffer.h" +#include "talk/base/sigslot.h" +#include "talk/base/sslfingerprint.h" +#include "talk/base/messagequeue.h" +#include "talk/p2p/base/session.h" +#include "talk/p2p/base/transport.h" +#include "talk/p2p/base/transportchannel.h" +#include "talk/p2p/base/transportchannelimpl.h" + +namespace cricket { + +class FakeTransport; + +struct PacketMessageData : public talk_base::MessageData { + PacketMessageData(const char* data, size_t len) : packet(data, len) { + } + talk_base::Buffer packet; +}; + +// Fake transport channel class, which can be passed to anything that needs a +// transport channel. Can be informed of another FakeTransportChannel via +// SetDestination. +class FakeTransportChannel : public TransportChannelImpl, + public talk_base::MessageHandler { + public: + explicit FakeTransportChannel(Transport* transport, + const std::string& content_name, + int component) + : TransportChannelImpl(content_name, component), + transport_(transport), + dest_(NULL), + state_(STATE_INIT), + async_(false), + identity_(NULL), + do_dtls_(false), + role_(ROLE_UNKNOWN), + tiebreaker_(0), + ice_proto_(ICEPROTO_HYBRID), + remote_ice_mode_(ICEMODE_FULL), + dtls_fingerprint_("", NULL, 0) { + } + ~FakeTransportChannel() { + Reset(); + } + + uint64 tiebreaker() const { return tiebreaker_; } + TransportProtocol protocol() const { return ice_proto_; } + IceMode remote_ice_mode() const { return remote_ice_mode_; } + const std::string& ice_ufrag() const { return ice_ufrag_; } + const std::string& ice_pwd() const { return ice_pwd_; } + const std::string& remote_ice_ufrag() const { return remote_ice_ufrag_; } + const std::string& remote_ice_pwd() const { return remote_ice_pwd_; } + const talk_base::SSLFingerprint& dtls_fingerprint() const { + return dtls_fingerprint_; + } + + void SetAsync(bool async) { + async_ = async; + } + + virtual Transport* GetTransport() { + return transport_; + } + + virtual void SetRole(TransportRole role) { role_ = role; } + virtual TransportRole GetRole() const { return role_; } + virtual void SetTiebreaker(uint64 tiebreaker) { tiebreaker_ = tiebreaker; } + virtual void SetIceProtocolType(IceProtocolType type) { ice_proto_ = type; } + virtual void SetIceCredentials(const std::string& ice_ufrag, + const std::string& ice_pwd) { + ice_ufrag_ = ice_ufrag; + ice_pwd_ = ice_pwd; + } + virtual void SetRemoteIceCredentials(const std::string& ice_ufrag, + const std::string& ice_pwd) { + remote_ice_ufrag_ = ice_ufrag; + remote_ice_pwd_ = ice_pwd; + } + + virtual void SetRemoteIceMode(IceMode mode) { remote_ice_mode_ = mode; } + virtual bool SetRemoteFingerprint(const std::string& alg, const uint8* digest, + size_t digest_len) { + dtls_fingerprint_ = talk_base::SSLFingerprint(alg, digest, digest_len); + return true; + } + + virtual void Connect() { + if (state_ == STATE_INIT) { + state_ = STATE_CONNECTING; + } + } + virtual void Reset() { + if (state_ != STATE_INIT) { + state_ = STATE_INIT; + if (dest_) { + dest_->state_ = STATE_INIT; + dest_->dest_ = NULL; + dest_ = NULL; + } + } + } + + void SetWritable(bool writable) { + set_writable(writable); + } + + void SetDestination(FakeTransportChannel* dest) { + if (state_ == STATE_CONNECTING && dest) { + // This simulates the delivery of candidates. + dest_ = dest; + dest_->dest_ = this; + if (identity_ && dest_->identity_) { + do_dtls_ = true; + dest_->do_dtls_ = true; + NegotiateSrtpCiphers(); + } + state_ = STATE_CONNECTED; + dest_->state_ = STATE_CONNECTED; + set_writable(true); + dest_->set_writable(true); + } else if (state_ == STATE_CONNECTED && !dest) { + // Simulates loss of connectivity, by asymmetrically forgetting dest_. + dest_ = NULL; + state_ = STATE_CONNECTING; + set_writable(false); + } + } + + virtual int SendPacket(const char* data, size_t len, int flags) { + if (state_ != STATE_CONNECTED) { + return -1; + } + + if (flags != PF_SRTP_BYPASS && flags != 0) { + return -1; + } + + PacketMessageData* packet = new PacketMessageData(data, len); + if (async_) { + talk_base::Thread::Current()->Post(this, 0, packet); + } else { + talk_base::Thread::Current()->Send(this, 0, packet); + } + return len; + } + virtual int SetOption(talk_base::Socket::Option opt, int value) { + return true; + } + virtual int GetError() { + return 0; + } + + virtual void OnSignalingReady() { + } + virtual void OnCandidate(const Candidate& candidate) { + } + + virtual void OnMessage(talk_base::Message* msg) { + PacketMessageData* data = static_cast( + msg->pdata); + dest_->SignalReadPacket(dest_, data->packet.data(), + data->packet.length(), 0); + delete data; + } + + bool SetLocalIdentity(talk_base::SSLIdentity* identity) { + identity_ = identity; + + return true; + } + + bool IsDtlsActive() const { + return do_dtls_; + } + + bool SetSrtpCiphers(const std::vector& ciphers) { + srtp_ciphers_ = ciphers; + return true; + } + + virtual bool GetSrtpCipher(std::string* cipher) { + if (!chosen_srtp_cipher_.empty()) { + *cipher = chosen_srtp_cipher_; + return true; + } + return false; + } + + virtual bool ExportKeyingMaterial(const std::string& label, + const uint8* context, + size_t context_len, + bool use_context, + uint8* result, + size_t result_len) { + if (!chosen_srtp_cipher_.empty()) { + memset(result, 0xff, result_len); + return true; + } + + return false; + } + + virtual void NegotiateSrtpCiphers() { + for (std::vector::const_iterator it1 = srtp_ciphers_.begin(); + it1 != srtp_ciphers_.end(); ++it1) { + for (std::vector::const_iterator it2 = + dest_->srtp_ciphers_.begin(); + it2 != dest_->srtp_ciphers_.end(); ++it2) { + if (*it1 == *it2) { + chosen_srtp_cipher_ = *it1; + dest_->chosen_srtp_cipher_ = *it2; + return; + } + } + } + } + + virtual bool GetStats(ConnectionInfos* infos) OVERRIDE { + ConnectionInfo info; + infos->clear(); + infos->push_back(info); + return true; + } + + private: + enum State { STATE_INIT, STATE_CONNECTING, STATE_CONNECTED }; + Transport* transport_; + FakeTransportChannel* dest_; + State state_; + bool async_; + talk_base::SSLIdentity* identity_; + bool do_dtls_; + std::vector srtp_ciphers_; + std::string chosen_srtp_cipher_; + TransportRole role_; + uint64 tiebreaker_; + IceProtocolType ice_proto_; + std::string ice_ufrag_; + std::string ice_pwd_; + std::string remote_ice_ufrag_; + std::string remote_ice_pwd_; + IceMode remote_ice_mode_; + talk_base::SSLFingerprint dtls_fingerprint_; +}; + +// Fake transport class, which can be passed to anything that needs a Transport. +// Can be informed of another FakeTransport via SetDestination (low-tech way +// of doing candidates) +class FakeTransport : public Transport { + public: + typedef std::map ChannelMap; + FakeTransport(talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread, + const std::string& content_name, + PortAllocator* alllocator = NULL) + : Transport(signaling_thread, worker_thread, + content_name, "test_type", NULL), + dest_(NULL), + async_(false), + identity_(NULL) { + } + ~FakeTransport() { + DestroyAllChannels(); + } + + const ChannelMap& channels() const { return channels_; } + + void SetAsync(bool async) { async_ = async; } + void SetDestination(FakeTransport* dest) { + dest_ = dest; + for (ChannelMap::iterator it = channels_.begin(); it != channels_.end(); + ++it) { + it->second->SetLocalIdentity(identity_); + SetChannelDestination(it->first, it->second); + } + } + + void SetWritable(bool writable) { + for (ChannelMap::iterator it = channels_.begin(); it != channels_.end(); + ++it) { + it->second->SetWritable(writable); + } + } + + void set_identity(talk_base::SSLIdentity* identity) { + identity_ = identity; + } + + using Transport::local_description; + using Transport::remote_description; + + protected: + virtual TransportChannelImpl* CreateTransportChannel(int component) { + if (channels_.find(component) != channels_.end()) { + return NULL; + } + FakeTransportChannel* channel = + new FakeTransportChannel(this, content_name(), component); + channel->SetAsync(async_); + SetChannelDestination(component, channel); + channels_[component] = channel; + return channel; + } + virtual void DestroyTransportChannel(TransportChannelImpl* channel) { + channels_.erase(channel->component()); + delete channel; + } + + private: + FakeTransportChannel* GetFakeChannel(int component) { + ChannelMap::iterator it = channels_.find(component); + return (it != channels_.end()) ? it->second : NULL; + } + void SetChannelDestination(int component, + FakeTransportChannel* channel) { + FakeTransportChannel* dest_channel = NULL; + if (dest_) { + dest_channel = dest_->GetFakeChannel(component); + if (dest_channel) { + dest_channel->SetLocalIdentity(dest_->identity_); + } + } + channel->SetDestination(dest_channel); + } + + // Note, this is distinct from the Channel map owned by Transport. + // This map just tracks the FakeTransportChannels created by this class. + ChannelMap channels_; + FakeTransport* dest_; + bool async_; + talk_base::SSLIdentity* identity_; +}; + +// Fake session class, which can be passed into a BaseChannel object for +// test purposes. Can be connected to other FakeSessions via Connect(). +class FakeSession : public BaseSession { + public: + explicit FakeSession() + : BaseSession(talk_base::Thread::Current(), + talk_base::Thread::Current(), + NULL, "", "", true), + fail_create_channel_(false) { + } + explicit FakeSession(bool initiator) + : BaseSession(talk_base::Thread::Current(), + talk_base::Thread::Current(), + NULL, "", "", initiator), + fail_create_channel_(false) { + } + + FakeTransport* GetTransport(const std::string& content_name) { + return static_cast( + BaseSession::GetTransport(content_name)); + } + + void Connect(FakeSession* dest) { + // Simulate the exchange of candidates. + CompleteNegotiation(); + dest->CompleteNegotiation(); + for (TransportMap::const_iterator it = transport_proxies().begin(); + it != transport_proxies().end(); ++it) { + static_cast(it->second->impl())->SetDestination( + dest->GetTransport(it->first)); + } + } + + virtual TransportChannel* CreateChannel( + const std::string& content_name, + const std::string& channel_name, + int component) { + if (fail_create_channel_) { + return NULL; + } + return BaseSession::CreateChannel(content_name, channel_name, component); + } + + void set_fail_channel_creation(bool fail_channel_creation) { + fail_create_channel_ = fail_channel_creation; + } + + // TODO: Hoist this into Session when we re-work the Session code. + void set_ssl_identity(talk_base::SSLIdentity* identity) { + for (TransportMap::const_iterator it = transport_proxies().begin(); + it != transport_proxies().end(); ++it) { + // We know that we have a FakeTransport* + + static_cast(it->second->impl())->set_identity + (identity); + } + } + + protected: + virtual Transport* CreateTransport(const std::string& content_name) { + return new FakeTransport(signaling_thread(), worker_thread(), content_name); + } + + void CompleteNegotiation() { + for (TransportMap::const_iterator it = transport_proxies().begin(); + it != transport_proxies().end(); ++it) { + it->second->CompleteNegotiation(); + it->second->ConnectChannels(); + } + } + + private: + bool fail_create_channel_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_FAKESESSION_H_ diff --git a/talk/p2p/base/p2ptransport.cc b/talk/p2p/base/p2ptransport.cc new file mode 100644 index 000000000..7f53cff49 --- /dev/null +++ b/talk/p2p/base/p2ptransport.cc @@ -0,0 +1,263 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/p2ptransport.h" + +#include +#include + +#include "talk/base/base64.h" +#include "talk/base/common.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/p2ptransportchannel.h" +#include "talk/p2p/base/parsing.h" +#include "talk/p2p/base/sessionmanager.h" +#include "talk/p2p/base/sessionmessages.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" + +namespace { + +// Limits for GICE and ICE username sizes. +const size_t kMaxGiceUsernameSize = 16; +const size_t kMaxIceUsernameSize = 512; + +} // namespace + +namespace cricket { + +static buzz::XmlElement* NewTransportElement(const std::string& name) { + return new buzz::XmlElement(buzz::QName(name, LN_TRANSPORT), true); +} + +P2PTransport::P2PTransport(talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread, + const std::string& content_name, + PortAllocator* allocator) + : Transport(signaling_thread, worker_thread, + content_name, NS_GINGLE_P2P, allocator) { +} + +P2PTransport::~P2PTransport() { + DestroyAllChannels(); +} + +TransportChannelImpl* P2PTransport::CreateTransportChannel(int component) { + return new P2PTransportChannel(content_name(), component, this, + port_allocator()); +} + +void P2PTransport::DestroyTransportChannel(TransportChannelImpl* channel) { + delete channel; +} + +bool P2PTransportParser::ParseTransportDescription( + const buzz::XmlElement* elem, + const CandidateTranslator* translator, + TransportDescription* desc, + ParseError* error) { + ASSERT(elem->Name().LocalPart() == LN_TRANSPORT); + desc->transport_type = elem->Name().Namespace(); + if (desc->transport_type != NS_GINGLE_P2P) + return BadParse("Unsupported transport type", error); + + for (const buzz::XmlElement* candidate_elem = elem->FirstElement(); + candidate_elem != NULL; + candidate_elem = candidate_elem->NextElement()) { + // Only look at local part because the namespace might (eventually) + // be NS_GINGLE_P2P or NS_JINGLE_ICE_UDP. + if (candidate_elem->Name().LocalPart() == LN_CANDIDATE) { + Candidate candidate; + if (!ParseCandidate(ICEPROTO_GOOGLE, candidate_elem, translator, + &candidate, error)) { + return false; + } + + desc->candidates.push_back(candidate); + } + } + return true; +} + +bool P2PTransportParser::WriteTransportDescription( + const TransportDescription& desc, + const CandidateTranslator* translator, + buzz::XmlElement** out_elem, + WriteError* error) { + TransportProtocol proto = TransportProtocolFromDescription(&desc); + talk_base::scoped_ptr trans_elem( + NewTransportElement(desc.transport_type)); + + // Fail if we get HYBRID or ICE right now. + // TODO(juberti): Add ICE and HYBRID serialization. + if (proto != ICEPROTO_GOOGLE) { + LOG(LS_ERROR) << "Failed to serialize non-GICE TransportDescription"; + return false; + } + + for (std::vector::const_iterator iter = desc.candidates.begin(); + iter != desc.candidates.end(); ++iter) { + talk_base::scoped_ptr cand_elem( + new buzz::XmlElement(QN_GINGLE_P2P_CANDIDATE)); + if (!WriteCandidate(proto, *iter, translator, cand_elem.get(), error)) { + return false; + } + trans_elem->AddElement(cand_elem.release()); + } + + *out_elem = trans_elem.release(); + return true; +} + +bool P2PTransportParser::ParseGingleCandidate( + const buzz::XmlElement* elem, + const CandidateTranslator* translator, + Candidate* candidate, + ParseError* error) { + return ParseCandidate(ICEPROTO_GOOGLE, elem, translator, candidate, error); +} + +bool P2PTransportParser::WriteGingleCandidate( + const Candidate& candidate, + const CandidateTranslator* translator, + buzz::XmlElement** out_elem, + WriteError* error) { + talk_base::scoped_ptr elem( + new buzz::XmlElement(QN_GINGLE_CANDIDATE)); + bool ret = WriteCandidate(ICEPROTO_GOOGLE, candidate, translator, elem.get(), + error); + if (ret) { + *out_elem = elem.release(); + } + return ret; +} + +bool P2PTransportParser::VerifyUsernameFormat(TransportProtocol proto, + const std::string& username, + ParseError* error) { + if (proto == ICEPROTO_GOOGLE || proto == ICEPROTO_HYBRID) { + if (username.size() > kMaxGiceUsernameSize) + return BadParse("candidate username is too long", error); + if (!talk_base::Base64::IsBase64Encoded(username)) + return BadParse("candidate username has non-base64 encoded characters", + error); + } else if (proto == ICEPROTO_RFC5245) { + if (username.size() > kMaxIceUsernameSize) + return BadParse("candidate username is too long", error); + } + return true; +} + +bool P2PTransportParser::ParseCandidate(TransportProtocol proto, + const buzz::XmlElement* elem, + const CandidateTranslator* translator, + Candidate* candidate, + ParseError* error) { + ASSERT(proto == ICEPROTO_GOOGLE); + ASSERT(translator != NULL); + + if (!elem->HasAttr(buzz::QN_NAME) || + !elem->HasAttr(QN_ADDRESS) || + !elem->HasAttr(QN_PORT) || + !elem->HasAttr(QN_USERNAME) || + !elem->HasAttr(QN_PROTOCOL) || + !elem->HasAttr(QN_GENERATION)) { + return BadParse("candidate missing required attribute", error); + } + + talk_base::SocketAddress address; + if (!ParseAddress(elem, QN_ADDRESS, QN_PORT, &address, error)) + return false; + + std::string channel_name = elem->Attr(buzz::QN_NAME); + int component = 0; + if (!translator || + !translator->GetComponentFromChannelName(channel_name, &component)) { + return BadParse("candidate has unknown channel name " + channel_name, + error); + } + + float preference = 0.0; + if (!GetXmlAttr(elem, QN_PREFERENCE, 0.0f, &preference)) { + return BadParse("candidate has unknown preference", error); + } + + candidate->set_component(component); + candidate->set_address(address); + candidate->set_username(elem->Attr(QN_USERNAME)); + candidate->set_preference(preference); + candidate->set_protocol(elem->Attr(QN_PROTOCOL)); + candidate->set_generation_str(elem->Attr(QN_GENERATION)); + if (elem->HasAttr(QN_PASSWORD)) + candidate->set_password(elem->Attr(QN_PASSWORD)); + if (elem->HasAttr(buzz::QN_TYPE)) + candidate->set_type(elem->Attr(buzz::QN_TYPE)); + if (elem->HasAttr(QN_NETWORK)) + candidate->set_network_name(elem->Attr(QN_NETWORK)); + + if (!VerifyUsernameFormat(proto, candidate->username(), error)) + return false; + + return true; +} + +bool P2PTransportParser::WriteCandidate(TransportProtocol proto, + const Candidate& candidate, + const CandidateTranslator* translator, + buzz::XmlElement* elem, + WriteError* error) { + ASSERT(proto == ICEPROTO_GOOGLE); + ASSERT(translator != NULL); + + std::string channel_name; + if (!translator || + !translator->GetChannelNameFromComponent( + candidate.component(), &channel_name)) { + return BadWrite("Cannot write candidate because of unknown component.", + error); + } + + elem->SetAttr(buzz::QN_NAME, channel_name); + elem->SetAttr(QN_ADDRESS, candidate.address().ipaddr().ToString()); + elem->SetAttr(QN_PORT, candidate.address().PortAsString()); + AddXmlAttr(elem, QN_PREFERENCE, candidate.preference()); + elem->SetAttr(QN_USERNAME, candidate.username()); + elem->SetAttr(QN_PROTOCOL, candidate.protocol()); + elem->SetAttr(QN_GENERATION, candidate.generation_str()); + if (!candidate.password().empty()) + elem->SetAttr(QN_PASSWORD, candidate.password()); + elem->SetAttr(buzz::QN_TYPE, candidate.type()); + if (!candidate.network_name().empty()) + elem->SetAttr(QN_NETWORK, candidate.network_name()); + + return true; +} + +} // namespace cricket diff --git a/talk/p2p/base/p2ptransport.h b/talk/p2p/base/p2ptransport.h new file mode 100644 index 000000000..f2b10f89b --- /dev/null +++ b/talk/p2p/base/p2ptransport.h @@ -0,0 +1,103 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_P2PTRANSPORT_H_ +#define TALK_P2P_BASE_P2PTRANSPORT_H_ + +#include +#include +#include "talk/p2p/base/transport.h" + +namespace cricket { + +class P2PTransport : public Transport { + public: + P2PTransport(talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread, + const std::string& content_name, + PortAllocator* allocator); + virtual ~P2PTransport(); + + protected: + // Creates and destroys P2PTransportChannel. + virtual TransportChannelImpl* CreateTransportChannel(int component); + virtual void DestroyTransportChannel(TransportChannelImpl* channel); + + friend class P2PTransportChannel; + + DISALLOW_EVIL_CONSTRUCTORS(P2PTransport); +}; + +class P2PTransportParser : public TransportParser { + public: + P2PTransportParser() {} + // Translator may be null, in which case ParseCandidates should + // return false if there are candidates to parse. We can't not call + // ParseCandidates because there's no way to know ahead of time if + // there are candidates or not. + + // Jingle-specific functions; can be used with either ICE, GICE, or HYBRID. + virtual bool ParseTransportDescription(const buzz::XmlElement* elem, + const CandidateTranslator* translator, + TransportDescription* desc, + ParseError* error); + virtual bool WriteTransportDescription(const TransportDescription& desc, + const CandidateTranslator* translator, + buzz::XmlElement** elem, + WriteError* error); + + // Legacy Gingle functions; only can be used with GICE. + virtual bool ParseGingleCandidate(const buzz::XmlElement* elem, + const CandidateTranslator* translator, + Candidate* candidate, + ParseError* error); + virtual bool WriteGingleCandidate(const Candidate& candidate, + const CandidateTranslator* translator, + buzz::XmlElement** elem, + WriteError* error); + + private: + bool ParseCandidate(TransportProtocol proto, + const buzz::XmlElement* elem, + const CandidateTranslator* translator, + Candidate* candidate, + ParseError* error); + bool WriteCandidate(TransportProtocol proto, + const Candidate& candidate, + const CandidateTranslator* translator, + buzz::XmlElement* elem, + WriteError* error); + bool VerifyUsernameFormat(TransportProtocol proto, + const std::string& username, + ParseError* error); + + DISALLOW_EVIL_CONSTRUCTORS(P2PTransportParser); +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_P2PTRANSPORT_H_ diff --git a/talk/p2p/base/p2ptransportchannel.cc b/talk/p2p/base/p2ptransportchannel.cc new file mode 100644 index 000000000..95a61985c --- /dev/null +++ b/talk/p2p/base/p2ptransportchannel.cc @@ -0,0 +1,1222 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/p2ptransportchannel.h" + +#include +#include "talk/base/common.h" +#include "talk/base/crc32.h" +#include "talk/base/logging.h" +#include "talk/base/stringencode.h" +#include "talk/p2p/base/common.h" +#include "talk/p2p/base/relayport.h" // For RELAY_PORT_TYPE. +#include "talk/p2p/base/stunport.h" // For STUN_PORT_TYPE. + +namespace { + +// messages for queuing up work for ourselves +enum { + MSG_SORT = 1, + MSG_PING, +}; + +// When the socket is unwritable, we will use 10 Kbps (ignoring IP+UDP headers) +// for pinging. When the socket is writable, we will use only 1 Kbps because +// we don't want to degrade the quality on a modem. These numbers should work +// well on a 28.8K modem, which is the slowest connection on which the voice +// quality is reasonable at all. +static const uint32 PING_PACKET_SIZE = 60 * 8; +static const uint32 WRITABLE_DELAY = 1000 * PING_PACKET_SIZE / 1000; // 480ms +static const uint32 UNWRITABLE_DELAY = 1000 * PING_PACKET_SIZE / 10000; // 50ms + +// If there is a current writable connection, then we will also try hard to +// make sure it is pinged at this rate. +static const uint32 MAX_CURRENT_WRITABLE_DELAY = 900; // 2*WRITABLE_DELAY - bit + +// The minimum improvement in RTT that justifies a switch. +static const double kMinImprovement = 10; + +cricket::PortInterface::CandidateOrigin GetOrigin(cricket::PortInterface* port, + cricket::PortInterface* origin_port) { + if (!origin_port) + return cricket::PortInterface::ORIGIN_MESSAGE; + else if (port == origin_port) + return cricket::PortInterface::ORIGIN_THIS_PORT; + else + return cricket::PortInterface::ORIGIN_OTHER_PORT; +} + +// Compares two connections based only on static information about them. +int CompareConnectionCandidates(cricket::Connection* a, + cricket::Connection* b) { + // Compare connection priority. Lower values get sorted last. + if (a->priority() > b->priority()) + return 1; + if (a->priority() < b->priority()) + return -1; + + // If we're still tied at this point, prefer a younger generation. + return (a->remote_candidate().generation() + a->port()->generation()) - + (b->remote_candidate().generation() + b->port()->generation()); +} + +// Compare two connections based on their writability and static preferences. +int CompareConnections(cricket::Connection *a, cricket::Connection *b) { + // Sort based on write-state. Better states have lower values. + if (a->write_state() < b->write_state()) + return 1; + if (a->write_state() > b->write_state()) + return -1; + + // Compare the candidate information. + return CompareConnectionCandidates(a, b); +} + +// Wraps the comparison connection into a less than operator that puts higher +// priority writable connections first. +class ConnectionCompare { + public: + bool operator()(const cricket::Connection *ca, + const cricket::Connection *cb) { + cricket::Connection* a = const_cast(ca); + cricket::Connection* b = const_cast(cb); + + ASSERT(a->port()->IceProtocol() == b->port()->IceProtocol()); + + // Compare first on writability and static preferences. + int cmp = CompareConnections(a, b); + if (cmp > 0) + return true; + if (cmp < 0) + return false; + + // Otherwise, sort based on latency estimate. + return a->rtt() < b->rtt(); + + // Should we bother checking for the last connection that last received + // data? It would help rendezvous on the connection that is also receiving + // packets. + // + // TODO: Yes we should definitely do this. The TCP protocol gains + // efficiency by being used bidirectionally, as opposed to two separate + // unidirectional streams. This test should probably occur before + // comparison of local prefs (assuming combined prefs are the same). We + // need to be careful though, not to bounce back and forth with both sides + // trying to rendevous with the other. + } +}; + +// Determines whether we should switch between two connections, based first on +// static preferences and then (if those are equal) on latency estimates. +bool ShouldSwitch(cricket::Connection* a_conn, cricket::Connection* b_conn) { + if (a_conn == b_conn) + return false; + + if (!a_conn || !b_conn) // don't think the latter should happen + return true; + + int prefs_cmp = CompareConnections(a_conn, b_conn); + if (prefs_cmp < 0) + return true; + if (prefs_cmp > 0) + return false; + + return b_conn->rtt() <= a_conn->rtt() + kMinImprovement; +} + +} // unnamed namespace + +namespace cricket { + +P2PTransportChannel::P2PTransportChannel(const std::string& content_name, + int component, + P2PTransport* transport, + PortAllocator *allocator) : + TransportChannelImpl(content_name, component), + transport_(transport), + allocator_(allocator), + worker_thread_(talk_base::Thread::Current()), + incoming_only_(false), + waiting_for_signaling_(false), + error_(0), + best_connection_(NULL), + pending_best_connection_(NULL), + sort_dirty_(false), + was_writable_(false), + protocol_type_(ICEPROTO_GOOGLE), + remote_ice_mode_(ICEMODE_FULL), + role_(ROLE_UNKNOWN), + tiebreaker_(0), + remote_candidate_generation_(0) { +} + +P2PTransportChannel::~P2PTransportChannel() { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + for (uint32 i = 0; i < allocator_sessions_.size(); ++i) + delete allocator_sessions_[i]; +} + +// Add the allocator session to our list so that we know which sessions +// are still active. +void P2PTransportChannel::AddAllocatorSession(PortAllocatorSession* session) { + session->set_generation(static_cast(allocator_sessions_.size())); + allocator_sessions_.push_back(session); + + // We now only want to apply new candidates that we receive to the ports + // created by this new session because these are replacing those of the + // previous sessions. + ports_.clear(); + + session->SignalPortReady.connect(this, &P2PTransportChannel::OnPortReady); + session->SignalCandidatesReady.connect( + this, &P2PTransportChannel::OnCandidatesReady); + session->SignalCandidatesAllocationDone.connect( + this, &P2PTransportChannel::OnCandidatesAllocationDone); + session->StartGettingPorts(); +} + +void P2PTransportChannel::AddConnection(Connection* connection) { + connections_.push_back(connection); + connection->set_remote_ice_mode(remote_ice_mode_); + connection->SignalReadPacket.connect( + this, &P2PTransportChannel::OnReadPacket); + connection->SignalReadyToSend.connect( + this, &P2PTransportChannel::OnReadyToSend); + connection->SignalStateChange.connect( + this, &P2PTransportChannel::OnConnectionStateChange); + connection->SignalDestroyed.connect( + this, &P2PTransportChannel::OnConnectionDestroyed); + connection->SignalUseCandidate.connect( + this, &P2PTransportChannel::OnUseCandidate); +} + +void P2PTransportChannel::SetRole(TransportRole role) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + if (role_ != role) { + role_ = role; + for (std::vector::iterator it = ports_.begin(); + it != ports_.end(); ++it) { + (*it)->SetRole(role_); + } + } +} + +void P2PTransportChannel::SetTiebreaker(uint64 tiebreaker) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + if (!ports_.empty()) { + LOG(LS_ERROR) + << "Attempt to change tiebreaker after Port has been allocated."; + return; + } + + tiebreaker_ = tiebreaker; +} + +void P2PTransportChannel::SetIceProtocolType(IceProtocolType type) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + protocol_type_ = type; + for (std::vector::iterator it = ports_.begin(); + it != ports_.end(); ++it) { + (*it)->SetIceProtocolType(protocol_type_); + } +} + +void P2PTransportChannel::SetIceCredentials(const std::string& ice_ufrag, + const std::string& ice_pwd) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + bool ice_restart = false; + if (!ice_ufrag_.empty() && !ice_pwd_.empty()) { + // Restart candidate allocation if there is any change in either + // ice ufrag or password. + ice_restart = (ice_ufrag_ != ice_ufrag) || (ice_pwd_!= ice_pwd); + } + + ice_ufrag_ = ice_ufrag; + ice_pwd_ = ice_pwd; + + if (ice_restart) { + // Restart candidate gathering. + Allocate(); + } +} + +void P2PTransportChannel::SetRemoteIceCredentials(const std::string& ice_ufrag, + const std::string& ice_pwd) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + bool ice_restart = false; + if (!remote_ice_ufrag_.empty() && !remote_ice_pwd_.empty()) { + ice_restart = (remote_ice_ufrag_ != ice_ufrag) || + (remote_ice_pwd_!= ice_pwd); + } + + remote_ice_ufrag_ = ice_ufrag; + remote_ice_pwd_ = ice_pwd; + + if (ice_restart) { + // |candidate.generation()| is not signaled in ICEPROTO_RFC5245. + // Therefore we need to keep track of the remote ice restart so + // newer connections are prioritized over the older. + ++remote_candidate_generation_; + } +} + +void P2PTransportChannel::SetRemoteIceMode(IceMode mode) { + remote_ice_mode_ = mode; +} + +// Go into the state of processing candidates, and running in general +void P2PTransportChannel::Connect() { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + if (ice_ufrag_.empty() || ice_pwd_.empty()) { + ASSERT(false); + LOG(LS_ERROR) << "P2PTransportChannel::Connect: The ice_ufrag_ and the " + << "ice_pwd_ are not set."; + return; + } + + // Kick off an allocator session + Allocate(); + + // Start pinging as the ports come in. + thread()->Post(this, MSG_PING); +} + +// Reset the socket, clear up any previous allocations and start over +void P2PTransportChannel::Reset() { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + // Get rid of all the old allocators. This should clean up everything. + for (uint32 i = 0; i < allocator_sessions_.size(); ++i) + delete allocator_sessions_[i]; + + allocator_sessions_.clear(); + ports_.clear(); + connections_.clear(); + best_connection_ = NULL; + + // Forget about all of the candidates we got before. + remote_candidates_.clear(); + + // Revert to the initial state. + set_readable(false); + set_writable(false); + + // Reinitialize the rest of our state. + waiting_for_signaling_ = false; + sort_dirty_ = false; + + // If we allocated before, start a new one now. + if (transport_->connect_requested()) + Allocate(); + + // Start pinging as the ports come in. + thread()->Clear(this); + thread()->Post(this, MSG_PING); +} + +// A new port is available, attempt to make connections for it +void P2PTransportChannel::OnPortReady(PortAllocatorSession *session, + PortInterface* port) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + // Set in-effect options on the new port + for (OptionMap::const_iterator it = options_.begin(); + it != options_.end(); + ++it) { + int val = port->SetOption(it->first, it->second); + if (val < 0) { + LOG_J(LS_WARNING, port) << "SetOption(" << it->first + << ", " << it->second + << ") failed: " << port->GetError(); + } + } + + // Remember the ports and candidates, and signal that candidates are ready. + // The session will handle this, and send an initiate/accept/modify message + // if one is pending. + + port->SetIceProtocolType(protocol_type_); + port->SetRole(role_); + port->SetTiebreaker(tiebreaker_); + ports_.push_back(port); + port->SignalUnknownAddress.connect( + this, &P2PTransportChannel::OnUnknownAddress); + port->SignalDestroyed.connect(this, &P2PTransportChannel::OnPortDestroyed); + port->SignalRoleConflict.connect( + this, &P2PTransportChannel::OnRoleConflict); + + // Attempt to create a connection from this new port to all of the remote + // candidates that we were given so far. + + std::vector::iterator iter; + for (iter = remote_candidates_.begin(); iter != remote_candidates_.end(); + ++iter) { + CreateConnection(port, *iter, iter->origin_port(), false); + } + + SortConnections(); +} + +// A new candidate is available, let listeners know +void P2PTransportChannel::OnCandidatesReady( + PortAllocatorSession *session, const std::vector& candidates) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + for (size_t i = 0; i < candidates.size(); ++i) { + SignalCandidateReady(this, candidates[i]); + } +} + +void P2PTransportChannel::OnCandidatesAllocationDone( + PortAllocatorSession* session) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + SignalCandidatesAllocationDone(this); +} + +// Handle stun packets +void P2PTransportChannel::OnUnknownAddress( + PortInterface* port, + const talk_base::SocketAddress& address, ProtocolType proto, + IceMessage* stun_msg, const std::string &remote_username, + bool port_muxed) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + // Port has received a valid stun packet from an address that no Connection + // is currently available for. See if we already have a candidate with the + // address. If it isn't we need to create new candidate for it. + + // Determine if the remote candidates use shared ufrag. + bool ufrag_per_port = false; + std::vector::iterator it; + if (remote_candidates_.size() > 0) { + it = remote_candidates_.begin(); + std::string username = it->username(); + for (; it != remote_candidates_.end(); ++it) { + if (it->username() != username) { + ufrag_per_port = true; + break; + } + } + } + + const Candidate* candidate = NULL; + bool known_username = false; + std::string remote_password; + for (it = remote_candidates_.begin(); it != remote_candidates_.end(); ++it) { + if (it->username() == remote_username) { + remote_password = it->password(); + known_username = true; + if (ufrag_per_port || + (it->address() == address && + it->protocol() == ProtoToString(proto))) { + candidate = &(*it); + break; + } + // We don't want to break here because we may find a match of the address + // later. + } + } + + if (!known_username) { + if (port_muxed) { + // When Ports are muxed, SignalUnknownAddress is delivered to all + // P2PTransportChannel belong to a session. Return from here will + // save us from sending stun binding error message from incorrect channel. + return; + } + // Don't know about this username, the request is bogus + // This sometimes happens if a binding response comes in before the ACCEPT + // message. It is totally valid; the retry state machine will try again. + port->SendBindingErrorResponse(stun_msg, address, + STUN_ERROR_STALE_CREDENTIALS, STUN_ERROR_REASON_STALE_CREDENTIALS); + return; + } + + Candidate new_remote_candidate; + if (candidate != NULL) { + new_remote_candidate = *candidate; + if (ufrag_per_port) { + new_remote_candidate.set_address(address); + } + } else { + // Create a new candidate with this address. + + std::string type; + if (protocol_type_ == ICEPROTO_RFC5245) { + type = PRFLX_PORT_TYPE; + } else { + // G-ICE doesn't support prflx candidate. + // We set candidate type to STUN_PORT_TYPE if the binding request comes + // from a relay port or the shared socket is used. Otherwise we use the + // port's type as the candidate type. + if (port->Type() == RELAY_PORT_TYPE || port->SharedSocket()) { + type = STUN_PORT_TYPE; + } else { + type = port->Type(); + } + } + + std::string id = talk_base::CreateRandomString(8); + new_remote_candidate = Candidate( + id, component(), ProtoToString(proto), address, + 0, remote_username, remote_password, type, + port->Network()->name(), 0U, + talk_base::ToString(talk_base::ComputeCrc32(id))); + new_remote_candidate.set_priority( + new_remote_candidate.GetPriority(ICE_TYPE_PREFERENCE_SRFLX)); + } + + if (protocol_type_ == ICEPROTO_RFC5245) { + // RFC 5245 + // If the source transport address of the request does not match any + // existing remote candidates, it represents a new peer reflexive remote + // candidate. + + // The priority of the candidate is set to the PRIORITY attribute + // from the request. + const StunUInt32Attribute* priority_attr = + stun_msg->GetUInt32(STUN_ATTR_PRIORITY); + if (!priority_attr) { + LOG(LS_WARNING) << "P2PTransportChannel::OnUnknownAddress - " + << "No STUN_ATTR_PRIORITY found in the " + << "stun request message"; + port->SendBindingErrorResponse(stun_msg, address, + STUN_ERROR_BAD_REQUEST, + STUN_ERROR_REASON_BAD_REQUEST); + return; + } + new_remote_candidate.set_priority(priority_attr->value()); + + // RFC5245, the agent constructs a pair whose local candidate is equal to + // the transport address on which the STUN request was received, and a + // remote candidate equal to the source transport address where the + // request came from. + + // There shouldn't be an existing connection with this remote address. + ASSERT(port->GetConnection(new_remote_candidate.address()) == NULL); + + Connection* connection = port->CreateConnection( + new_remote_candidate, cricket::PortInterface::ORIGIN_THIS_PORT); + if (!connection) { + ASSERT(false); + port->SendBindingErrorResponse(stun_msg, address, + STUN_ERROR_SERVER_ERROR, + STUN_ERROR_REASON_SERVER_ERROR); + return; + } + + AddConnection(connection); + connection->ReceivedPing(); + + // Send the pinger a successful stun response. + port->SendBindingResponse(stun_msg, address); + + // Update the list of connections since we just added another. We do this + // after sending the response since it could (in principle) delete the + // connection in question. + SortConnections(); + } else { + // Check for connectivity to this address. Create connections + // to this address across all local ports. First, add this as a new remote + // address + if (!CreateConnections(new_remote_candidate, port, true)) { + // Hopefully this won't occur, because changing a destination address + // shouldn't cause a new connection to fail + ASSERT(false); + port->SendBindingErrorResponse(stun_msg, address, STUN_ERROR_SERVER_ERROR, + STUN_ERROR_REASON_SERVER_ERROR); + return; + } + + // Send the pinger a successful stun response. + port->SendBindingResponse(stun_msg, address); + + // Update the list of connections since we just added another. We do this + // after sending the response since it could (in principle) delete the + // connection in question. + SortConnections(); + } +} + +void P2PTransportChannel::OnRoleConflict(PortInterface* port) { + SignalRoleConflict(this); // STUN ping will be sent when SetRole is called + // from Transport. +} + +// When the signalling channel is ready, we can really kick off the allocator +void P2PTransportChannel::OnSignalingReady() { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + if (waiting_for_signaling_) { + waiting_for_signaling_ = false; + AddAllocatorSession(allocator_->CreateSession( + SessionId(), content_name(), component(), ice_ufrag_, ice_pwd_)); + } +} + +void P2PTransportChannel::OnUseCandidate(Connection* conn) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + ASSERT(role_ == ROLE_CONTROLLED); + ASSERT(protocol_type_ == ICEPROTO_RFC5245); + if (conn->write_state() == Connection::STATE_WRITABLE) { + if (best_connection_ != conn) { + pending_best_connection_ = NULL; + SwitchBestConnectionTo(conn); + // Now we have selected the best connection, time to prune other existing + // connections and update the read/write state of the channel. + RequestSort(); + } + } else { + pending_best_connection_ = conn; + } +} + +void P2PTransportChannel::OnCandidate(const Candidate& candidate) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + // Create connections to this remote candidate. + CreateConnections(candidate, NULL, false); + + // Resort the connections list, which may have new elements. + SortConnections(); +} + +// Creates connections from all of the ports that we care about to the given +// remote candidate. The return value is true if we created a connection from +// the origin port. +bool P2PTransportChannel::CreateConnections(const Candidate &remote_candidate, + PortInterface* origin_port, + bool readable) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + Candidate new_remote_candidate(remote_candidate); + new_remote_candidate.set_generation( + GetRemoteCandidateGeneration(remote_candidate)); + // ICE candidates don't need to have username and password set, but + // the code below this (specifically, ConnectionRequest::Prepare in + // port.cc) uses the remote candidates's username. So, we set it + // here. + if (remote_candidate.username().empty()) { + new_remote_candidate.set_username(remote_ice_ufrag_); + } + if (remote_candidate.password().empty()) { + new_remote_candidate.set_password(remote_ice_pwd_); + } + + // Add a new connection for this candidate to every port that allows such a + // connection (i.e., if they have compatible protocols) and that does not + // already have a connection to an equivalent candidate. We must be careful + // to make sure that the origin port is included, even if it was pruned, + // since that may be the only port that can create this connection. + + bool created = false; + + std::vector::reverse_iterator it; + for (it = ports_.rbegin(); it != ports_.rend(); ++it) { + if (CreateConnection(*it, new_remote_candidate, origin_port, readable)) { + if (*it == origin_port) + created = true; + } + } + + if ((origin_port != NULL) && + std::find(ports_.begin(), ports_.end(), origin_port) == ports_.end()) { + if (CreateConnection( + origin_port, new_remote_candidate, origin_port, readable)) + created = true; + } + + // Remember this remote candidate so that we can add it to future ports. + RememberRemoteCandidate(new_remote_candidate, origin_port); + + return created; +} + +// Setup a connection object for the local and remote candidate combination. +// And then listen to connection object for changes. +bool P2PTransportChannel::CreateConnection(PortInterface* port, + const Candidate& remote_candidate, + PortInterface* origin_port, + bool readable) { + // Look for an existing connection with this remote address. If one is not + // found, then we can create a new connection for this address. + Connection* connection = port->GetConnection(remote_candidate.address()); + if (connection != NULL) { + // It is not legal to try to change any of the parameters of an existing + // connection; however, the other side can send a duplicate candidate. + if (!remote_candidate.IsEquivalent(connection->remote_candidate())) { + LOG(INFO) << "Attempt to change a remote candidate"; + return false; + } + } else { + PortInterface::CandidateOrigin origin = GetOrigin(port, origin_port); + + // Don't create connection if this is a candidate we received in a + // message and we are not allowed to make outgoing connections. + if (origin == cricket::PortInterface::ORIGIN_MESSAGE && incoming_only_) + return false; + + connection = port->CreateConnection(remote_candidate, origin); + if (!connection) + return false; + + AddConnection(connection); + + LOG_J(LS_INFO, this) << "Created connection with origin=" << origin << ", (" + << connections_.size() << " total)"; + } + + // If we are readable, it is because we are creating this in response to a + // ping from the other side. This will cause the state to become readable. + if (readable) + connection->ReceivedPing(); + + return true; +} + +bool P2PTransportChannel::FindConnection( + cricket::Connection* connection) const { + std::vector::const_iterator citer = + std::find(connections_.begin(), connections_.end(), connection); + return citer != connections_.end(); +} + +uint32 P2PTransportChannel::GetRemoteCandidateGeneration( + const Candidate& candidate) { + if (protocol_type_ == ICEPROTO_GOOGLE) { + // The Candidate.generation() can be trusted. Nothing needs to be done. + return candidate.generation(); + } + // |candidate.generation()| is not signaled in ICEPROTO_RFC5245. + // Therefore we need to keep track of the remote ice restart so + // newer connections are prioritized over the older. + ASSERT(candidate.generation() == 0 || + candidate.generation() == remote_candidate_generation_); + return remote_candidate_generation_; +} + +// Maintain our remote candidate list, adding this new remote one. +void P2PTransportChannel::RememberRemoteCandidate( + const Candidate& remote_candidate, PortInterface* origin_port) { + // Remove any candidates whose generation is older than this one. The + // presence of a new generation indicates that the old ones are not useful. + uint32 i = 0; + while (i < remote_candidates_.size()) { + if (remote_candidates_[i].generation() < remote_candidate.generation()) { + LOG(INFO) << "Pruning candidate from old generation: " + << remote_candidates_[i].address().ToSensitiveString(); + remote_candidates_.erase(remote_candidates_.begin() + i); + } else { + i += 1; + } + } + + // Make sure this candidate is not a duplicate. + for (uint32 i = 0; i < remote_candidates_.size(); ++i) { + if (remote_candidates_[i].IsEquivalent(remote_candidate)) { + LOG(INFO) << "Duplicate candidate: " + << remote_candidate.address().ToSensitiveString(); + return; + } + } + + // Try this candidate for all future ports. + remote_candidates_.push_back(RemoteCandidate(remote_candidate, origin_port)); +} + +// Set options on ourselves is simply setting options on all of our available +// port objects. +int P2PTransportChannel::SetOption(talk_base::Socket::Option opt, int value) { + OptionMap::iterator it = options_.find(opt); + if (it == options_.end()) { + options_.insert(std::make_pair(opt, value)); + } else if (it->second == value) { + return 0; + } else { + it->second = value; + } + + for (uint32 i = 0; i < ports_.size(); ++i) { + int val = ports_[i]->SetOption(opt, value); + if (val < 0) { + // Because this also occurs deferred, probably no point in reporting an + // error + LOG(WARNING) << "SetOption(" << opt << ", " << value << ") failed: " + << ports_[i]->GetError(); + } + } + return 0; +} + +// Send data to the other side, using our best connection. +int P2PTransportChannel::SendPacket(const char *data, size_t len, int flags) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + if (flags != 0) { + error_ = EINVAL; + return -1; + } + if (best_connection_ == NULL) { + error_ = EWOULDBLOCK; + return -1; + } + int sent = best_connection_->Send(data, len); + if (sent <= 0) { + ASSERT(sent < 0); + error_ = best_connection_->GetError(); + } + return sent; +} + +bool P2PTransportChannel::GetStats(ConnectionInfos *infos) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + // Gather connection infos. + infos->clear(); + + std::vector::const_iterator it; + for (it = connections_.begin(); it != connections_.end(); ++it) { + Connection *connection = *it; + ConnectionInfo info; + info.best_connection = (best_connection_ == connection); + info.readable = + (connection->read_state() == Connection::STATE_READABLE); + info.writable = + (connection->write_state() == Connection::STATE_WRITABLE); + info.timeout = + (connection->write_state() == Connection::STATE_WRITE_TIMEOUT); + info.new_connection = !connection->reported(); + connection->set_reported(true); + info.rtt = connection->rtt(); + info.sent_total_bytes = connection->sent_total_bytes(); + info.sent_bytes_second = connection->sent_bytes_second(); + info.recv_total_bytes = connection->recv_total_bytes(); + info.recv_bytes_second = connection->recv_bytes_second(); + info.local_candidate = connection->local_candidate(); + info.remote_candidate = connection->remote_candidate(); + info.key = connection; + infos->push_back(info); + } + + return true; +} + +// Begin allocate (or immediately re-allocate, if MSG_ALLOCATE pending) +void P2PTransportChannel::Allocate() { + // Time for a new allocator, lets make sure we have a signalling channel + // to communicate candidates through first. + waiting_for_signaling_ = true; + SignalRequestSignaling(this); +} + +// Monitor connection states. +void P2PTransportChannel::UpdateConnectionStates() { + uint32 now = talk_base::Time(); + + // We need to copy the list of connections since some may delete themselves + // when we call UpdateState. + for (uint32 i = 0; i < connections_.size(); ++i) + connections_[i]->UpdateState(now); +} + +// Prepare for best candidate sorting. +void P2PTransportChannel::RequestSort() { + if (!sort_dirty_) { + worker_thread_->Post(this, MSG_SORT); + sort_dirty_ = true; + } +} + +// Sort the available connections to find the best one. We also monitor +// the number of available connections and the current state. +void P2PTransportChannel::SortConnections() { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + // Make sure the connection states are up-to-date since this affects how they + // will be sorted. + UpdateConnectionStates(); + + // Any changes after this point will require a re-sort. + sort_dirty_ = false; + + // Get a list of the networks that we are using. + std::set networks; + for (uint32 i = 0; i < connections_.size(); ++i) + networks.insert(connections_[i]->port()->Network()); + + // Find the best alternative connection by sorting. It is important to note + // that amongst equal preference, writable connections, this will choose the + // one whose estimated latency is lowest. So it is the only one that we + // need to consider switching to. + + ConnectionCompare cmp; + std::stable_sort(connections_.begin(), connections_.end(), cmp); + LOG(LS_VERBOSE) << "Sorting available connections:"; + for (uint32 i = 0; i < connections_.size(); ++i) { + LOG(LS_VERBOSE) << connections_[i]->ToString(); + } + + Connection* top_connection = NULL; + if (connections_.size() > 0) + top_connection = connections_[0]; + + // We don't want to pick the best connections if channel is using RFC5245 + // and it's mode is CONTROLLED, as connections will be selected by the + // CONTROLLING agent. + + // If necessary, switch to the new choice. + if (protocol_type_ != ICEPROTO_RFC5245 || role_ == ROLE_CONTROLLING) { + if (ShouldSwitch(best_connection_, top_connection)) + SwitchBestConnectionTo(top_connection); + } + + // We can prune any connection for which there is a writable connection on + // the same network with better or equal priority. We leave those with + // better priority just in case they become writable later (at which point, + // we would prune out the current best connection). We leave connections on + // other networks because they may not be using the same resources and they + // may represent very distinct paths over which we can switch. + std::set::iterator network; + for (network = networks.begin(); network != networks.end(); ++network) { + Connection* primier = GetBestConnectionOnNetwork(*network); + if (!primier || (primier->write_state() != Connection::STATE_WRITABLE)) + continue; + + for (uint32 i = 0; i < connections_.size(); ++i) { + if ((connections_[i] != primier) && + (connections_[i]->port()->Network() == *network) && + (CompareConnectionCandidates(primier, connections_[i]) >= 0)) { + connections_[i]->Prune(); + } + } + } + + // Check if all connections are timedout. + bool all_connections_timedout = true; + for (uint32 i = 0; i < connections_.size(); ++i) { + if (connections_[i]->write_state() != Connection::STATE_WRITE_TIMEOUT) { + all_connections_timedout = false; + break; + } + } + + // Now update the writable state of the channel with the information we have + // so far. + if (best_connection_ && best_connection_->writable()) { + HandleWritable(); + } else if (all_connections_timedout) { + HandleAllTimedOut(); + } else { + HandleNotWritable(); + } + + // Update the state of this channel. This method is called whenever the + // state of any connection changes, so this is a good place to do this. + UpdateChannelState(); +} + + +// Track the best connection, and let listeners know +void P2PTransportChannel::SwitchBestConnectionTo(Connection* conn) { + // Note: if conn is NULL, the previous best_connection_ has been destroyed, + // so don't use it. + Connection* old_best_connection = best_connection_; + best_connection_ = conn; + if (best_connection_) { + if (old_best_connection) { + LOG_J(LS_INFO, this) << "Previous best connection: " + << old_best_connection->ToString(); + } + LOG_J(LS_INFO, this) << "New best connection: " + << best_connection_->ToString(); + SignalRouteChange(this, best_connection_->remote_candidate()); + } else { + LOG_J(LS_INFO, this) << "No best connection"; + } +} + +void P2PTransportChannel::UpdateChannelState() { + // The Handle* functions already set the writable state. We'll just double- + // check it here. + bool writable = ((best_connection_ != NULL) && + (best_connection_->write_state() == + Connection::STATE_WRITABLE)); + ASSERT(writable == this->writable()); + if (writable != this->writable()) + LOG(LS_ERROR) << "UpdateChannelState: writable state mismatch"; + + bool readable = false; + for (uint32 i = 0; i < connections_.size(); ++i) { + if (connections_[i]->read_state() == Connection::STATE_READABLE) + readable = true; + } + set_readable(readable); +} + +// We checked the status of our connections and we had at least one that +// was writable, go into the writable state. +void P2PTransportChannel::HandleWritable() { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + if (!writable()) { + for (uint32 i = 0; i < allocator_sessions_.size(); ++i) { + if (allocator_sessions_[i]->IsGettingPorts()) { + allocator_sessions_[i]->StopGettingPorts(); + } + } + } + + was_writable_ = true; + set_writable(true); +} + +// Notify upper layer about channel not writable state, if it was before. +void P2PTransportChannel::HandleNotWritable() { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + if (was_writable_) { + was_writable_ = false; + set_writable(false); + } +} + +void P2PTransportChannel::HandleAllTimedOut() { + // Currently we are treating this as channel not writable. + HandleNotWritable(); +} + +// If we have a best connection, return it, otherwise return top one in the +// list (later we will mark it best). +Connection* P2PTransportChannel::GetBestConnectionOnNetwork( + talk_base::Network* network) { + // If the best connection is on this network, then it wins. + if (best_connection_ && (best_connection_->port()->Network() == network)) + return best_connection_; + + // Otherwise, we return the top-most in sorted order. + for (uint32 i = 0; i < connections_.size(); ++i) { + if (connections_[i]->port()->Network() == network) + return connections_[i]; + } + + return NULL; +} + +// Handle any queued up requests +void P2PTransportChannel::OnMessage(talk_base::Message *pmsg) { + switch (pmsg->message_id) { + case MSG_SORT: + OnSort(); + break; + case MSG_PING: + OnPing(); + break; + default: + ASSERT(false); + break; + } +} + +// Handle queued up sort request +void P2PTransportChannel::OnSort() { + // Resort the connections based on the new statistics. + SortConnections(); +} + +// Handle queued up ping request +void P2PTransportChannel::OnPing() { + // Make sure the states of the connections are up-to-date (since this affects + // which ones are pingable). + UpdateConnectionStates(); + + // Find the oldest pingable connection and have it do a ping. + Connection* conn = FindNextPingableConnection(); + if (conn) + PingConnection(conn); + + // Post ourselves a message to perform the next ping. + uint32 delay = writable() ? WRITABLE_DELAY : UNWRITABLE_DELAY; + thread()->PostDelayed(delay, this, MSG_PING); +} + +// Is the connection in a state for us to even consider pinging the other side? +bool P2PTransportChannel::IsPingable(Connection* conn) { + // An unconnected connection cannot be written to at all, so pinging is out + // of the question. + if (!conn->connected()) + return false; + + if (writable()) { + // If we are writable, then we only want to ping connections that could be + // better than this one, i.e., the ones that were not pruned. + return (conn->write_state() != Connection::STATE_WRITE_TIMEOUT); + } else { + // If we are not writable, then we need to try everything that might work. + // This includes both connections that do not have write timeout as well as + // ones that do not have read timeout. A connection could be readable but + // be in write-timeout if we pruned it before. Since the other side is + // still pinging it, it very well might still work. + return (conn->write_state() != Connection::STATE_WRITE_TIMEOUT) || + (conn->read_state() != Connection::STATE_READ_TIMEOUT); + } +} + +// Returns the next pingable connection to ping. This will be the oldest +// pingable connection unless we have a writable connection that is past the +// maximum acceptable ping delay. +Connection* P2PTransportChannel::FindNextPingableConnection() { + uint32 now = talk_base::Time(); + if (best_connection_ && + (best_connection_->write_state() == Connection::STATE_WRITABLE) && + (best_connection_->last_ping_sent() + + MAX_CURRENT_WRITABLE_DELAY <= now)) { + return best_connection_; + } + + Connection* oldest_conn = NULL; + uint32 oldest_time = 0xFFFFFFFF; + for (uint32 i = 0; i < connections_.size(); ++i) { + if (IsPingable(connections_[i])) { + if (connections_[i]->last_ping_sent() < oldest_time) { + oldest_time = connections_[i]->last_ping_sent(); + oldest_conn = connections_[i]; + } + } + } + return oldest_conn; +} + +// Apart from sending ping from |conn| this method also updates +// |use_candidate_attr| flag. The criteria to update this flag is +// explained below. +// Set USE-CANDIDATE if doing ICE AND this channel is in CONTROLLING AND +// a) Channel is in FULL ICE AND +// a.1) |conn| is the best connection OR +// a.2) there is no best connection OR +// a.3) the best connection is unwritable OR +// a.4) |conn| has higher priority than best_connection. +// b) we're doing LITE ICE AND +// b.1) |conn| is the best_connection AND +// b.2) |conn| is writable. +void P2PTransportChannel::PingConnection(Connection* conn) { + bool use_candidate = false; + if (protocol_type_ == ICEPROTO_RFC5245) { + if (remote_ice_mode_ == ICEMODE_FULL && role_ == ROLE_CONTROLLING) { + use_candidate = (conn == best_connection_) || + (best_connection_ == NULL) || + (!best_connection_->writable()) || + (conn->priority() > best_connection_->priority()); + } else if (remote_ice_mode_ == ICEMODE_LITE && conn == best_connection_) { + use_candidate = best_connection_->writable(); + } + } + conn->set_use_candidate_attr(use_candidate); + conn->Ping(talk_base::Time()); +} + +// When a connection's state changes, we need to figure out who to use as +// the best connection again. It could have become usable, or become unusable. +void P2PTransportChannel::OnConnectionStateChange(Connection* connection) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + // Update the best connection if the state change is from pending best + // connection and role is controlled. + if (protocol_type_ == ICEPROTO_RFC5245 && role_ == ROLE_CONTROLLED) { + if (connection == pending_best_connection_ && connection->writable()) { + pending_best_connection_ = NULL; + SwitchBestConnectionTo(connection); + } + } + + // We have to unroll the stack before doing this because we may be changing + // the state of connections while sorting. + RequestSort(); +} + +// When a connection is removed, edit it out, and then update our best +// connection. +void P2PTransportChannel::OnConnectionDestroyed(Connection* connection) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + // Note: the previous best_connection_ may be destroyed by now, so don't + // use it. + + // Remove this connection from the list. + std::vector::iterator iter = + std::find(connections_.begin(), connections_.end(), connection); + ASSERT(iter != connections_.end()); + connections_.erase(iter); + + LOG_J(LS_INFO, this) << "Removed connection (" + << static_cast(connections_.size()) << " remaining)"; + + if (pending_best_connection_ == connection) { + pending_best_connection_ = NULL; + } + + // If this is currently the best connection, then we need to pick a new one. + // The call to SortConnections will pick a new one. It looks at the current + // best connection in order to avoid switching between fairly similar ones. + // Since this connection is no longer an option, we can just set best to NULL + // and re-choose a best assuming that there was no best connection. + if (best_connection_ == connection) { + SwitchBestConnectionTo(NULL); + RequestSort(); + } +} + +// When a port is destroyed remove it from our list of ports to use for +// connection attempts. +void P2PTransportChannel::OnPortDestroyed(PortInterface* port) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + // Remove this port from the list (if we didn't drop it already). + std::vector::iterator iter = + std::find(ports_.begin(), ports_.end(), port); + if (iter != ports_.end()) + ports_.erase(iter); + + LOG(INFO) << "Removed port from p2p socket: " + << static_cast(ports_.size()) << " remaining"; +} + +// We data is available, let listeners know +void P2PTransportChannel::OnReadPacket(Connection *connection, const char *data, + size_t len) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + // Do not deliver, if packet doesn't belong to the correct transport channel. + if (!FindConnection(connection)) + return; + + // Let the client know of an incoming packet + SignalReadPacket(this, data, len, 0); +} + +void P2PTransportChannel::OnReadyToSend(Connection* connection) { + if (connection == best_connection_ && writable()) { + SignalReadyToSend(this); + } +} + +} // namespace cricket diff --git a/talk/p2p/base/p2ptransportchannel.h b/talk/p2p/base/p2ptransportchannel.h new file mode 100644 index 000000000..420769d3f --- /dev/null +++ b/talk/p2p/base/p2ptransportchannel.h @@ -0,0 +1,196 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +// P2PTransportChannel wraps up the state management of the connection between +// two P2P clients. Clients have candidate ports for connecting, and +// connections which are combinations of candidates from each end (Alice and +// Bob each have candidates, one candidate from Alice and one candidate from +// Bob are used to make a connection, repeat to make many connections). +// +// When all of the available connections become invalid (non-writable), we +// kick off a process of determining more candidates and more connections. +// +#ifndef TALK_P2P_BASE_P2PTRANSPORTCHANNEL_H_ +#define TALK_P2P_BASE_P2PTRANSPORTCHANNEL_H_ + +#include +#include +#include +#include "talk/base/sigslot.h" +#include "talk/p2p/base/candidate.h" +#include "talk/p2p/base/portinterface.h" +#include "talk/p2p/base/portallocator.h" +#include "talk/p2p/base/transport.h" +#include "talk/p2p/base/transportchannelimpl.h" +#include "talk/p2p/base/p2ptransport.h" + +namespace cricket { + +// Adds the port on which the candidate originated. +class RemoteCandidate : public Candidate { + public: + RemoteCandidate(const Candidate& c, PortInterface* origin_port) + : Candidate(c), origin_port_(origin_port) {} + + PortInterface* origin_port() { return origin_port_; } + + private: + PortInterface* origin_port_; +}; + +// P2PTransportChannel manages the candidates and connection process to keep +// two P2P clients connected to each other. +class P2PTransportChannel : public TransportChannelImpl, + public talk_base::MessageHandler { + public: + P2PTransportChannel(const std::string& content_name, + int component, + P2PTransport* transport, + PortAllocator *allocator); + virtual ~P2PTransportChannel(); + + // From TransportChannelImpl: + virtual Transport* GetTransport() { return transport_; } + virtual void SetRole(TransportRole role); + virtual TransportRole GetRole() const { return role_; } + virtual void SetTiebreaker(uint64 tiebreaker); + virtual void SetIceProtocolType(IceProtocolType type); + virtual void SetIceCredentials(const std::string& ice_ufrag, + const std::string& ice_pwd); + virtual void SetRemoteIceCredentials(const std::string& ice_ufrag, + const std::string& ice_pwd); + virtual void SetRemoteIceMode(IceMode mode); + virtual void Connect(); + virtual void Reset(); + virtual void OnSignalingReady(); + virtual void OnCandidate(const Candidate& candidate); + + // From TransportChannel: + virtual int SendPacket(const char *data, size_t len, int flags); + virtual int SetOption(talk_base::Socket::Option opt, int value); + virtual int GetError() { return error_; } + virtual bool GetStats(std::vector* stats); + + const Connection* best_connection() const { return best_connection_; } + void set_incoming_only(bool value) { incoming_only_ = value; } + + // Note: This is only for testing purpose. + // |ports_| should not be changed from outside. + const std::vector& ports() { return ports_; } + + IceMode remote_ice_mode() const { return remote_ice_mode_; } + + private: + talk_base::Thread* thread() { return worker_thread_; } + PortAllocatorSession* allocator_session() { + return allocator_sessions_.back(); + } + + void Allocate(); + void UpdateConnectionStates(); + void RequestSort(); + void SortConnections(); + void SwitchBestConnectionTo(Connection* conn); + void UpdateChannelState(); + void HandleWritable(); + void HandleNotWritable(); + void HandleAllTimedOut(); + + Connection* GetBestConnectionOnNetwork(talk_base::Network* network); + bool CreateConnections(const Candidate &remote_candidate, + PortInterface* origin_port, bool readable); + bool CreateConnection(PortInterface* port, const Candidate& remote_candidate, + PortInterface* origin_port, bool readable); + bool FindConnection(cricket::Connection* connection) const; + + uint32 GetRemoteCandidateGeneration(const Candidate& candidate); + void RememberRemoteCandidate(const Candidate& remote_candidate, + PortInterface* origin_port); + bool IsPingable(Connection* conn); + Connection* FindNextPingableConnection(); + void PingConnection(Connection* conn); + void AddAllocatorSession(PortAllocatorSession* session); + void AddConnection(Connection* connection); + + void OnPortReady(PortAllocatorSession *session, PortInterface* port); + void OnCandidatesReady(PortAllocatorSession *session, + const std::vector& candidates); + void OnCandidatesAllocationDone(PortAllocatorSession* session); + void OnUnknownAddress(PortInterface* port, + const talk_base::SocketAddress& addr, + ProtocolType proto, + IceMessage* stun_msg, + const std::string& remote_username, + bool port_muxed); + void OnPortDestroyed(PortInterface* port); + void OnRoleConflict(PortInterface* port); + + void OnConnectionStateChange(Connection *connection); + void OnReadPacket(Connection *connection, const char *data, size_t len); + void OnReadyToSend(Connection* connection); + void OnConnectionDestroyed(Connection *connection); + + void OnUseCandidate(Connection* conn); + + virtual void OnMessage(talk_base::Message *pmsg); + void OnSort(); + void OnPing(); + + P2PTransport* transport_; + PortAllocator *allocator_; + talk_base::Thread *worker_thread_; + bool incoming_only_; + bool waiting_for_signaling_; + int error_; + std::vector allocator_sessions_; + std::vector ports_; + std::vector connections_; + Connection* best_connection_; + // Connection selected by the controlling agent. This should be used only + // at controlled side when protocol type is RFC5245. + Connection* pending_best_connection_; + std::vector remote_candidates_; + bool sort_dirty_; // indicates whether another sort is needed right now + bool was_writable_; + typedef std::map OptionMap; + OptionMap options_; + std::string ice_ufrag_; + std::string ice_pwd_; + std::string remote_ice_ufrag_; + std::string remote_ice_pwd_; + IceProtocolType protocol_type_; + IceMode remote_ice_mode_; + TransportRole role_; + uint64 tiebreaker_; + uint32 remote_candidate_generation_; + + DISALLOW_EVIL_CONSTRUCTORS(P2PTransportChannel); +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_P2PTRANSPORTCHANNEL_H_ diff --git a/talk/p2p/base/p2ptransportchannel_unittest.cc b/talk/p2p/base/p2ptransportchannel_unittest.cc new file mode 100644 index 000000000..04961092a --- /dev/null +++ b/talk/p2p/base/p2ptransportchannel_unittest.cc @@ -0,0 +1,1461 @@ +/* + * libjingle + * Copyright 2009 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/base/fakenetwork.h" +#include "talk/base/firewallsocketserver.h" +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/natserver.h" +#include "talk/base/natsocketfactory.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/proxyserver.h" +#include "talk/base/socketaddress.h" +#include "talk/base/thread.h" +#include "talk/base/virtualsocketserver.h" +#include "talk/p2p/base/p2ptransportchannel.h" +#include "talk/p2p/base/testrelayserver.h" +#include "talk/p2p/base/teststunserver.h" +#include "talk/p2p/client/basicportallocator.h" + +using cricket::kDefaultPortAllocatorFlags; +using cricket::kMinimumStepDelay; +using cricket::kDefaultStepDelay; +using cricket::PORTALLOCATOR_ENABLE_SHARED_UFRAG; +using cricket::PORTALLOCATOR_ENABLE_SHARED_SOCKET; +using talk_base::SocketAddress; + +static const int kDefaultTimeout = 1000; +static const int kOnlyLocalPorts = cricket::PORTALLOCATOR_DISABLE_STUN | + cricket::PORTALLOCATOR_DISABLE_RELAY | + cricket::PORTALLOCATOR_DISABLE_TCP; +// Addresses on the public internet. +static const SocketAddress kPublicAddrs[2] = + { SocketAddress("11.11.11.11", 0), SocketAddress("22.22.22.22", 0) }; +// For configuring multihomed clients. +static const SocketAddress kAlternateAddrs[2] = + { SocketAddress("11.11.11.101", 0), SocketAddress("22.22.22.202", 0) }; +// Addresses for HTTP proxy servers. +static const SocketAddress kHttpsProxyAddrs[2] = + { SocketAddress("11.11.11.1", 443), SocketAddress("22.22.22.1", 443) }; +// Addresses for SOCKS proxy servers. +static const SocketAddress kSocksProxyAddrs[2] = + { SocketAddress("11.11.11.1", 1080), SocketAddress("22.22.22.1", 1080) }; +// Internal addresses for NAT boxes. +static const SocketAddress kNatAddrs[2] = + { SocketAddress("192.168.1.1", 0), SocketAddress("192.168.2.1", 0) }; +// Private addresses inside the NAT private networks. +static const SocketAddress kPrivateAddrs[2] = + { SocketAddress("192.168.1.11", 0), SocketAddress("192.168.2.22", 0) }; +// For cascaded NATs, the internal addresses of the inner NAT boxes. +static const SocketAddress kCascadedNatAddrs[2] = + { SocketAddress("192.168.10.1", 0), SocketAddress("192.168.20.1", 0) }; +// For cascaded NATs, private addresses inside the inner private networks. +static const SocketAddress kCascadedPrivateAddrs[2] = + { SocketAddress("192.168.10.11", 0), SocketAddress("192.168.20.22", 0) }; +// The address of the public STUN server. +static const SocketAddress kStunAddr("99.99.99.1", cricket::STUN_SERVER_PORT); +// The addresses for the public relay server. +static const SocketAddress kRelayUdpIntAddr("99.99.99.2", 5000); +static const SocketAddress kRelayUdpExtAddr("99.99.99.3", 5001); +static const SocketAddress kRelayTcpIntAddr("99.99.99.2", 5002); +static const SocketAddress kRelayTcpExtAddr("99.99.99.3", 5003); +static const SocketAddress kRelaySslTcpIntAddr("99.99.99.2", 5004); +static const SocketAddress kRelaySslTcpExtAddr("99.99.99.3", 5005); + +// Based on ICE_UFRAG_LENGTH +static const char* kIceUfrag[4] = {"TESTICEUFRAG0000", "TESTICEUFRAG0001", + "TESTICEUFRAG0002", "TESTICEUFRAG0003"}; +// Based on ICE_PWD_LENGTH +static const char* kIcePwd[4] = {"TESTICEPWD00000000000000", + "TESTICEPWD00000000000001", + "TESTICEPWD00000000000002", + "TESTICEPWD00000000000003"}; + +static const uint64 kTiebreaker1 = 11111; +static const uint64 kTiebreaker2 = 22222; + +// This test simulates 2 P2P endpoints that want to establish connectivity +// with each other over various network topologies and conditions, which can be +// specified in each individial test. +// A virtual network (via VirtualSocketServer) along with virtual firewalls and +// NATs (via Firewall/NATSocketServer) are used to simulate the various network +// conditions. We can configure the IP addresses of the endpoints, +// block various types of connectivity, or add arbitrary levels of NAT. +// We also run a STUN server and a relay server on the virtual network to allow +// our typical P2P mechanisms to do their thing. +// For each case, we expect the P2P stack to eventually settle on a specific +// form of connectivity to the other side. The test checks that the P2P +// negotiation successfully establishes connectivity within a certain time, +// and that the result is what we expect. +// Note that this class is a base class for use by other tests, who will provide +// specialized test behavior. +class P2PTransportChannelTestBase : public testing::Test, + public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + P2PTransportChannelTestBase() + : main_(talk_base::Thread::Current()), + pss_(new talk_base::PhysicalSocketServer), + vss_(new talk_base::VirtualSocketServer(pss_.get())), + nss_(new talk_base::NATSocketServer(vss_.get())), + ss_(new talk_base::FirewallSocketServer(nss_.get())), + ss_scope_(ss_.get()), + stun_server_(main_, kStunAddr), + relay_server_(main_, kRelayUdpIntAddr, kRelayUdpExtAddr, + kRelayTcpIntAddr, kRelayTcpExtAddr, + kRelaySslTcpIntAddr, kRelaySslTcpExtAddr), + socks_server1_(ss_.get(), kSocksProxyAddrs[0], + ss_.get(), kSocksProxyAddrs[0]), + socks_server2_(ss_.get(), kSocksProxyAddrs[1], + ss_.get(), kSocksProxyAddrs[1]), + clear_remote_candidates_ufrag_pwd_(false) { + ep1_.role_ = cricket::ROLE_CONTROLLING; + ep2_.role_ = cricket::ROLE_CONTROLLED; + ep1_.allocator_.reset(new cricket::BasicPortAllocator( + &ep1_.network_manager_, kStunAddr, kRelayUdpIntAddr, + kRelayTcpIntAddr, kRelaySslTcpIntAddr)); + ep2_.allocator_.reset(new cricket::BasicPortAllocator( + &ep2_.network_manager_, kStunAddr, kRelayUdpIntAddr, + kRelayTcpIntAddr, kRelaySslTcpIntAddr)); + } + + protected: + enum Config { + OPEN, // Open to the Internet + NAT_FULL_CONE, // NAT, no filtering + NAT_ADDR_RESTRICTED, // NAT, must send to an addr to recv + NAT_PORT_RESTRICTED, // NAT, must send to an addr+port to recv + NAT_SYMMETRIC, // NAT, endpoint-dependent bindings + NAT_DOUBLE_CONE, // Double NAT, both cone + NAT_SYMMETRIC_THEN_CONE, // Double NAT, symmetric outer, cone inner + BLOCK_UDP, // Firewall, UDP in/out blocked + BLOCK_UDP_AND_INCOMING_TCP, // Firewall, UDP in/out and TCP in blocked + BLOCK_ALL_BUT_OUTGOING_HTTP, // Firewall, only TCP out on 80/443 + PROXY_HTTPS, // All traffic through HTTPS proxy + PROXY_SOCKS, // All traffic through SOCKS proxy + NUM_CONFIGS + }; + + struct Result { + Result(const std::string& lt, const std::string& lp, + const std::string& rt, const std::string& rp, + const std::string& lt2, const std::string& lp2, + const std::string& rt2, const std::string& rp2, int wait) + : local_type(lt), local_proto(lp), remote_type(rt), remote_proto(rp), + local_type2(lt2), local_proto2(lp2), remote_type2(rt2), + remote_proto2(rp2), connect_wait(wait) { + } + std::string local_type; + std::string local_proto; + std::string remote_type; + std::string remote_proto; + std::string local_type2; + std::string local_proto2; + std::string remote_type2; + std::string remote_proto2; + int connect_wait; + }; + + struct ChannelData { + bool CheckData(const char* data, int len) { + bool ret = false; + if (!ch_packets_.empty()) { + std::string packet = ch_packets_.front(); + ret = (packet == std::string(data, len)); + ch_packets_.pop_front(); + } + return ret; + } + + std::string name_; // TODO - Currently not used. + std::list ch_packets_; + talk_base::scoped_ptr ch_; + }; + + struct Endpoint { + Endpoint() : signaling_delay_(0), role_(cricket::ROLE_UNKNOWN), + tiebreaker_(0), role_conflict_(false), + protocol_type_(cricket::ICEPROTO_GOOGLE) {} + bool HasChannel(cricket::TransportChannel* ch) { + return (ch == cd1_.ch_.get() || ch == cd2_.ch_.get()); + } + ChannelData* GetChannelData(cricket::TransportChannel* ch) { + if (!HasChannel(ch)) return NULL; + if (cd1_.ch_.get() == ch) + return &cd1_; + else + return &cd2_; + } + void SetSignalingDelay(int delay) { signaling_delay_ = delay; } + + void SetRole(cricket::TransportRole role) { role_ = role; } + cricket::TransportRole role() { return role_; } + void SetIceProtocolType(cricket::IceProtocolType type) { + protocol_type_ = type; + } + cricket::IceProtocolType protocol_type() { return protocol_type_; } + void SetTiebreaker(uint64 tiebreaker) { tiebreaker_ = tiebreaker; } + uint64 GetTiebreaker() { return tiebreaker_; } + void OnRoleConflict(bool role_conflict) { role_conflict_ = role_conflict; } + bool role_conflict() { return role_conflict_; } + void SetAllocationStepDelay(uint32 delay) { + allocator_->set_step_delay(delay); + } + + talk_base::FakeNetworkManager network_manager_; + talk_base::scoped_ptr allocator_; + ChannelData cd1_; + ChannelData cd2_; + int signaling_delay_; + cricket::TransportRole role_; + uint64 tiebreaker_; + bool role_conflict_; + cricket::IceProtocolType protocol_type_; + }; + + struct CandidateData : public talk_base::MessageData { + CandidateData(cricket::TransportChannel* ch, const cricket::Candidate& c) + : channel(ch), candidate(c) { + } + cricket::TransportChannel* channel; + cricket::Candidate candidate; + }; + + ChannelData* GetChannelData(cricket::TransportChannel* channel) { + if (ep1_.HasChannel(channel)) + return ep1_.GetChannelData(channel); + else + return ep2_.GetChannelData(channel); + } + + void CreateChannels(int num) { + std::string ice_ufrag_ep1_cd1_ch = kIceUfrag[0]; + std::string ice_pwd_ep1_cd1_ch = kIcePwd[0]; + std::string ice_ufrag_ep2_cd1_ch = kIceUfrag[1]; + std::string ice_pwd_ep2_cd1_ch = kIcePwd[1]; + ep1_.cd1_.ch_.reset(CreateChannel( + 0, cricket::ICE_CANDIDATE_COMPONENT_DEFAULT, + ice_ufrag_ep1_cd1_ch, ice_pwd_ep1_cd1_ch, + ice_ufrag_ep2_cd1_ch, ice_pwd_ep2_cd1_ch)); + ep2_.cd1_.ch_.reset(CreateChannel( + 1, cricket::ICE_CANDIDATE_COMPONENT_DEFAULT, + ice_ufrag_ep2_cd1_ch, ice_pwd_ep2_cd1_ch, + ice_ufrag_ep1_cd1_ch, ice_pwd_ep1_cd1_ch)); + if (num == 2) { + std::string ice_ufrag_ep1_cd2_ch = kIceUfrag[2]; + std::string ice_pwd_ep1_cd2_ch = kIcePwd[2]; + std::string ice_ufrag_ep2_cd2_ch = kIceUfrag[3]; + std::string ice_pwd_ep2_cd2_ch = kIcePwd[3]; + // In BUNDLE each endpoint must share common ICE credentials. + if (ep1_.allocator_->flags() & cricket::PORTALLOCATOR_ENABLE_BUNDLE) { + ice_ufrag_ep1_cd2_ch = ice_ufrag_ep1_cd1_ch; + ice_pwd_ep1_cd2_ch = ice_pwd_ep1_cd1_ch; + } + if (ep2_.allocator_->flags() & cricket::PORTALLOCATOR_ENABLE_BUNDLE) { + ice_ufrag_ep2_cd2_ch = ice_ufrag_ep2_cd1_ch; + ice_pwd_ep2_cd2_ch = ice_pwd_ep2_cd1_ch; + } + ep1_.cd2_.ch_.reset(CreateChannel( + 0, cricket::ICE_CANDIDATE_COMPONENT_DEFAULT, + ice_ufrag_ep1_cd2_ch, ice_pwd_ep1_cd2_ch, + ice_ufrag_ep2_cd2_ch, ice_pwd_ep2_cd2_ch)); + ep2_.cd2_.ch_.reset(CreateChannel( + 1, cricket::ICE_CANDIDATE_COMPONENT_DEFAULT, + ice_ufrag_ep2_cd2_ch, ice_pwd_ep2_cd2_ch, + ice_ufrag_ep1_cd2_ch, ice_pwd_ep1_cd2_ch)); + } + } + cricket::P2PTransportChannel* CreateChannel( + int endpoint, + int component, + const std::string& local_ice_ufrag, + const std::string& local_ice_pwd, + const std::string& remote_ice_ufrag, + const std::string& remote_ice_pwd) { + cricket::P2PTransportChannel* channel = new cricket::P2PTransportChannel( + "test content name", component, NULL, GetAllocator(endpoint)); + channel->SignalRequestSignaling.connect( + this, &P2PTransportChannelTestBase::OnChannelRequestSignaling); + channel->SignalCandidateReady.connect(this, + &P2PTransportChannelTestBase::OnCandidate); + channel->SignalReadPacket.connect( + this, &P2PTransportChannelTestBase::OnReadPacket); + channel->SignalRoleConflict.connect( + this, &P2PTransportChannelTestBase::OnRoleConflict); + channel->SetIceProtocolType(GetEndpoint(endpoint)->protocol_type()); + channel->SetIceCredentials(local_ice_ufrag, local_ice_pwd); + if (clear_remote_candidates_ufrag_pwd_) { + // This only needs to be set if we're clearing them from the + // candidates. Some unit tests rely on this not being set. + channel->SetRemoteIceCredentials(remote_ice_ufrag, remote_ice_pwd); + } + channel->SetRole(GetEndpoint(endpoint)->role()); + channel->SetTiebreaker(GetEndpoint(endpoint)->GetTiebreaker()); + channel->Connect(); + return channel; + } + void DestroyChannels() { + ep1_.cd1_.ch_.reset(); + ep2_.cd1_.ch_.reset(); + ep1_.cd2_.ch_.reset(); + ep2_.cd2_.ch_.reset(); + } + cricket::P2PTransportChannel* ep1_ch1() { return ep1_.cd1_.ch_.get(); } + cricket::P2PTransportChannel* ep1_ch2() { return ep1_.cd2_.ch_.get(); } + cricket::P2PTransportChannel* ep2_ch1() { return ep2_.cd1_.ch_.get(); } + cricket::P2PTransportChannel* ep2_ch2() { return ep2_.cd2_.ch_.get(); } + + // Common results. + static const Result kLocalUdpToLocalUdp; + static const Result kLocalUdpToStunUdp; + static const Result kLocalUdpToPrflxUdp; + static const Result kPrflxUdpToLocalUdp; + static const Result kStunUdpToLocalUdp; + static const Result kStunUdpToStunUdp; + static const Result kPrflxUdpToStunUdp; + static const Result kLocalUdpToRelayUdp; + static const Result kPrflxUdpToRelayUdp; + static const Result kLocalTcpToLocalTcp; + static const Result kLocalTcpToPrflxTcp; + static const Result kPrflxTcpToLocalTcp; + + static void SetUpTestCase() { + // Ensure the RNG is inited. + talk_base::InitRandom(NULL, 0); + } + + talk_base::NATSocketServer* nat() { return nss_.get(); } + talk_base::FirewallSocketServer* fw() { return ss_.get(); } + + Endpoint* GetEndpoint(int endpoint) { + if (endpoint == 0) { + return &ep1_; + } else if (endpoint == 1) { + return &ep2_; + } else { + return NULL; + } + } + cricket::PortAllocator* GetAllocator(int endpoint) { + return GetEndpoint(endpoint)->allocator_.get(); + } + void AddAddress(int endpoint, const SocketAddress& addr) { + GetEndpoint(endpoint)->network_manager_.AddInterface(addr); + } + void RemoveAddress(int endpoint, const SocketAddress& addr) { + GetEndpoint(endpoint)->network_manager_.RemoveInterface(addr); + } + void SetProxy(int endpoint, talk_base::ProxyType type) { + talk_base::ProxyInfo info; + info.type = type; + info.address = (type == talk_base::PROXY_HTTPS) ? + kHttpsProxyAddrs[endpoint] : kSocksProxyAddrs[endpoint]; + GetAllocator(endpoint)->set_proxy("unittest/1.0", info); + } + void SetAllocatorFlags(int endpoint, int flags) { + GetAllocator(endpoint)->set_flags(flags); + } + void SetSignalingDelay(int endpoint, int delay) { + GetEndpoint(endpoint)->SetSignalingDelay(delay); + } + void SetIceProtocol(int endpoint, cricket::IceProtocolType type) { + GetEndpoint(endpoint)->SetIceProtocolType(type); + } + void SetIceRole(int endpoint, cricket::TransportRole role) { + GetEndpoint(endpoint)->SetRole(role); + } + void SetTiebreaker(int endpoint, uint64 tiebreaker) { + GetEndpoint(endpoint)->SetTiebreaker(tiebreaker); + } + bool GetRoleConflict(int endpoint) { + return GetEndpoint(endpoint)->role_conflict(); + } + void SetAllocationStepDelay(int endpoint, uint32 delay) { + return GetEndpoint(endpoint)->SetAllocationStepDelay(delay); + } + + void Test(const Result& expected) { + int32 connect_start = talk_base::Time(), connect_time; + + // Create the channels and wait for them to connect. + CreateChannels(1); + EXPECT_TRUE_WAIT_MARGIN(ep1_ch1() != NULL && + ep2_ch1() != NULL && + ep1_ch1()->readable() && + ep1_ch1()->writable() && + ep2_ch1()->readable() && + ep2_ch1()->writable(), + expected.connect_wait, + 1000); + connect_time = talk_base::TimeSince(connect_start); + if (connect_time < expected.connect_wait) { + LOG(LS_INFO) << "Connect time: " << connect_time << " ms"; + } else { + LOG(LS_INFO) << "Connect time: " << "TIMEOUT (" + << expected.connect_wait << " ms)"; + } + + // Allow a few turns of the crank for the best connections to emerge. + // This may take up to 2 seconds. + if (ep1_ch1()->best_connection() && + ep2_ch1()->best_connection()) { + int32 converge_start = talk_base::Time(), converge_time; + int converge_wait = 2000; + EXPECT_TRUE_WAIT_MARGIN( + LocalCandidate(ep1_ch1())->type() == expected.local_type && + LocalCandidate(ep1_ch1())->protocol() == expected.local_proto && + RemoteCandidate(ep1_ch1())->type() == expected.remote_type && + RemoteCandidate(ep1_ch1())->protocol() == expected.remote_proto, + converge_wait, + converge_wait); + + // Also do EXPECT_EQ on each part so that failures are more verbose. + EXPECT_EQ(expected.local_type, LocalCandidate(ep1_ch1())->type()); + EXPECT_EQ(expected.local_proto, LocalCandidate(ep1_ch1())->protocol()); + EXPECT_EQ(expected.remote_type, RemoteCandidate(ep1_ch1())->type()); + EXPECT_EQ(expected.remote_proto, RemoteCandidate(ep1_ch1())->protocol()); + + // Verifying remote channel best connection information. This is done + // only for the RFC 5245 as controlled agent will use USE-CANDIDATE + // from controlling (ep1) agent. We can easily predict from EP1 result + // matrix. + if (ep2_.protocol_type_ == cricket::ICEPROTO_RFC5245) { + // Checking for best connection candidates information at remote. + EXPECT_TRUE_WAIT( + LocalCandidate(ep2_ch1())->type() == expected.local_type2 && + LocalCandidate(ep2_ch1())->protocol() == expected.local_proto2 && + RemoteCandidate(ep2_ch1())->protocol() == expected.remote_proto2, + kDefaultTimeout); + + // For verbose + EXPECT_EQ(expected.local_type2, LocalCandidate(ep2_ch1())->type()); + EXPECT_EQ(expected.local_proto2, LocalCandidate(ep2_ch1())->protocol()); + EXPECT_EQ(expected.remote_proto2, + RemoteCandidate(ep2_ch1())->protocol()); + // Removed remote_type comparision aginst best connection remote + // candidate. This is done to handle remote type discrepancy from + // local to stun based on the test type. + // For example in case of Open -> NAT, ep2 channels will have LULU + // and in other cases like NAT -> NAT it will be LUSU. To avoid these + // mismatches and we are doing comparision in different way. + // i.e. when don't match its remote type is either local or stun. + // TODO(ronghuawu): Refine the test criteria. + // https://code.google.com/p/webrtc/issues/detail?id=1953 + if (expected.remote_type2 != RemoteCandidate(ep2_ch1())->type()) + EXPECT_TRUE(expected.remote_type2 == cricket::LOCAL_PORT_TYPE || + expected.remote_type2 == cricket::STUN_PORT_TYPE); + EXPECT_TRUE( + RemoteCandidate(ep2_ch1())->type() == cricket::LOCAL_PORT_TYPE || + RemoteCandidate(ep2_ch1())->type() == cricket::STUN_PORT_TYPE || + RemoteCandidate(ep2_ch1())->type() == cricket::PRFLX_PORT_TYPE); + } + + converge_time = talk_base::TimeSince(converge_start); + if (converge_time < converge_wait) { + LOG(LS_INFO) << "Converge time: " << converge_time << " ms"; + } else { + LOG(LS_INFO) << "Converge time: " << "TIMEOUT (" + << converge_wait << " ms)"; + } + } + // Try sending some data to other end. + TestSendRecv(1); + + // Destroy the channels, and wait for them to be fully cleaned up. + DestroyChannels(); + } + + void TestSendRecv(int channels) { + for (int i = 0; i < 10; ++i) { + const char* data = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + int len = static_cast(strlen(data)); + // local_channel1 <==> remote_channel1 + EXPECT_EQ_WAIT(len, SendData(ep1_ch1(), data, len), 1000); + EXPECT_TRUE_WAIT(CheckDataOnChannel(ep2_ch1(), data, len), 1000); + EXPECT_EQ_WAIT(len, SendData(ep2_ch1(), data, len), 1000); + EXPECT_TRUE_WAIT(CheckDataOnChannel(ep1_ch1(), data, len), 1000); + if (channels == 2 && ep1_ch2() && ep2_ch2()) { + // local_channel2 <==> remote_channel2 + EXPECT_EQ_WAIT(len, SendData(ep1_ch2(), data, len), 1000); + EXPECT_TRUE_WAIT(CheckDataOnChannel(ep2_ch2(), data, len), 1000); + EXPECT_EQ_WAIT(len, SendData(ep2_ch2(), data, len), 1000); + EXPECT_TRUE_WAIT(CheckDataOnChannel(ep1_ch2(), data, len), 1000); + } + } + } + + // This test waits for the transport to become readable and writable on both + // end points. Once they are, the end points set new local ice credentials to + // restart the ice gathering. Finally it waits for the transport to select a + // new connection using the newly generated ice candidates. + // Before calling this function the end points must be configured. + void TestHandleIceUfragPasswordChanged() { + ep1_ch1()->SetRemoteIceCredentials(kIceUfrag[1], kIcePwd[1]); + ep2_ch1()->SetRemoteIceCredentials(kIceUfrag[0], kIcePwd[0]); + EXPECT_TRUE_WAIT_MARGIN(ep1_ch1()->readable() && ep1_ch1()->writable() && + ep2_ch1()->readable() && ep2_ch1()->writable(), + 1000, 1000); + + const cricket::Candidate* old_local_candidate1 = LocalCandidate(ep1_ch1()); + const cricket::Candidate* old_local_candidate2 = LocalCandidate(ep2_ch1()); + const cricket::Candidate* old_remote_candidate1 = + RemoteCandidate(ep1_ch1()); + const cricket::Candidate* old_remote_candidate2 = + RemoteCandidate(ep2_ch1()); + + ep1_ch1()->SetIceCredentials(kIceUfrag[2], kIcePwd[2]); + ep1_ch1()->SetRemoteIceCredentials(kIceUfrag[3], kIcePwd[3]); + ep2_ch1()->SetIceCredentials(kIceUfrag[3], kIcePwd[3]); + ep2_ch1()->SetRemoteIceCredentials(kIceUfrag[2], kIcePwd[2]); + + EXPECT_TRUE_WAIT_MARGIN(LocalCandidate(ep1_ch1())->generation() != + old_local_candidate1->generation(), + 1000, 1000); + EXPECT_TRUE_WAIT_MARGIN(LocalCandidate(ep2_ch1())->generation() != + old_local_candidate2->generation(), + 1000, 1000); + EXPECT_TRUE_WAIT_MARGIN(RemoteCandidate(ep1_ch1())->generation() != + old_remote_candidate1->generation(), + 1000, 1000); + EXPECT_TRUE_WAIT_MARGIN(RemoteCandidate(ep2_ch1())->generation() != + old_remote_candidate2->generation(), + 1000, 1000); + EXPECT_EQ(1u, RemoteCandidate(ep2_ch1())->generation()); + EXPECT_EQ(1u, RemoteCandidate(ep1_ch1())->generation()); + } + + void TestSignalRoleConflict() { + SetIceProtocol(0, cricket::ICEPROTO_RFC5245); + SetTiebreaker(0, kTiebreaker1); // Default EP1 is in controlling state. + + SetIceProtocol(1, cricket::ICEPROTO_RFC5245); + SetIceRole(1, cricket::ROLE_CONTROLLING); + SetTiebreaker(1, kTiebreaker2); + + // Creating channels with both channels role set to CONTROLLING. + CreateChannels(1); + // Since both the channels initiated with controlling state and channel2 + // has higher tiebreaker value, channel1 should receive SignalRoleConflict. + EXPECT_TRUE_WAIT(GetRoleConflict(0), 1000); + EXPECT_FALSE(GetRoleConflict(1)); + + EXPECT_TRUE_WAIT(ep1_ch1()->readable() && + ep1_ch1()->writable() && + ep2_ch1()->readable() && + ep2_ch1()->writable(), + 1000); + + EXPECT_TRUE(ep1_ch1()->best_connection() && + ep2_ch1()->best_connection()); + + TestSendRecv(1); + } + + void OnChannelRequestSignaling(cricket::TransportChannelImpl* channel) { + channel->OnSignalingReady(); + } + // We pass the candidates directly to the other side. + void OnCandidate(cricket::TransportChannelImpl* ch, + const cricket::Candidate& c) { + main_->PostDelayed(GetEndpoint(ch)->signaling_delay_, this, 0, + new CandidateData(ch, c)); + } + void OnMessage(talk_base::Message* msg) { + talk_base::scoped_ptr data( + static_cast(msg->pdata)); + cricket::P2PTransportChannel* rch = GetRemoteChannel(data->channel); + cricket::Candidate c = data->candidate; + if (clear_remote_candidates_ufrag_pwd_) { + c.set_username(""); + c.set_password(""); + } + LOG(LS_INFO) << "Candidate(" << data->channel->component() << "->" + << rch->component() << "): " << c.type() << ", " << c.protocol() + << ", " << c.address().ToString() << ", " << c.username() + << ", " << c.generation(); + rch->OnCandidate(c); + } + void OnReadPacket(cricket::TransportChannel* channel, const char* data, + size_t len, int flags) { + std::list& packets = GetPacketList(channel); + packets.push_front(std::string(data, len)); + } + void OnRoleConflict(cricket::TransportChannelImpl* channel) { + GetEndpoint(channel)->OnRoleConflict(true); + cricket::TransportRole new_role = + GetEndpoint(channel)->role() == cricket::ROLE_CONTROLLING ? + cricket::ROLE_CONTROLLED : cricket::ROLE_CONTROLLING; + channel->SetRole(new_role); + } + int SendData(cricket::TransportChannel* channel, + const char* data, size_t len) { + return channel->SendPacket(data, len, 0); + } + bool CheckDataOnChannel(cricket::TransportChannel* channel, + const char* data, int len) { + return GetChannelData(channel)->CheckData(data, len); + } + static const cricket::Candidate* LocalCandidate( + cricket::P2PTransportChannel* ch) { + return (ch && ch->best_connection()) ? + &ch->best_connection()->local_candidate() : NULL; + } + static const cricket::Candidate* RemoteCandidate( + cricket::P2PTransportChannel* ch) { + return (ch && ch->best_connection()) ? + &ch->best_connection()->remote_candidate() : NULL; + } + Endpoint* GetEndpoint(cricket::TransportChannel* ch) { + if (ep1_.HasChannel(ch)) { + return &ep1_; + } else if (ep2_.HasChannel(ch)) { + return &ep2_; + } else { + return NULL; + } + } + cricket::P2PTransportChannel* GetRemoteChannel( + cricket::TransportChannel* ch) { + if (ch == ep1_ch1()) + return ep2_ch1(); + else if (ch == ep1_ch2()) + return ep2_ch2(); + else if (ch == ep2_ch1()) + return ep1_ch1(); + else if (ch == ep2_ch2()) + return ep1_ch2(); + else + return NULL; + } + std::list& GetPacketList(cricket::TransportChannel* ch) { + return GetChannelData(ch)->ch_packets_; + } + + void set_clear_remote_candidates_ufrag_pwd(bool clear) { + clear_remote_candidates_ufrag_pwd_ = clear; + } + + private: + talk_base::Thread* main_; + talk_base::scoped_ptr pss_; + talk_base::scoped_ptr vss_; + talk_base::scoped_ptr nss_; + talk_base::scoped_ptr ss_; + talk_base::SocketServerScope ss_scope_; + cricket::TestStunServer stun_server_; + cricket::TestRelayServer relay_server_; + talk_base::SocksProxyServer socks_server1_; + talk_base::SocksProxyServer socks_server2_; + Endpoint ep1_; + Endpoint ep2_; + bool clear_remote_candidates_ufrag_pwd_; +}; + +// The tests have only a few outcomes, which we predefine. +const P2PTransportChannelTestBase::Result P2PTransportChannelTestBase:: + kLocalUdpToLocalUdp("local", "udp", "local", "udp", + "local", "udp", "local", "udp", 1000); +const P2PTransportChannelTestBase::Result P2PTransportChannelTestBase:: + kLocalUdpToStunUdp("local", "udp", "stun", "udp", + "local", "udp", "stun", "udp", 1000); +const P2PTransportChannelTestBase::Result P2PTransportChannelTestBase:: + kLocalUdpToPrflxUdp("local", "udp", "prflx", "udp", + "prflx", "udp", "local", "udp", 1000); +const P2PTransportChannelTestBase::Result P2PTransportChannelTestBase:: + kPrflxUdpToLocalUdp("prflx", "udp", "local", "udp", + "local", "udp", "prflx", "udp", 1000); +const P2PTransportChannelTestBase::Result P2PTransportChannelTestBase:: + kStunUdpToLocalUdp("stun", "udp", "local", "udp", + "local", "udp", "stun", "udp", 1000); +const P2PTransportChannelTestBase::Result P2PTransportChannelTestBase:: + kStunUdpToStunUdp("stun", "udp", "stun", "udp", + "stun", "udp", "stun", "udp", 1000); +const P2PTransportChannelTestBase::Result P2PTransportChannelTestBase:: + kPrflxUdpToStunUdp("prflx", "udp", "stun", "udp", + "local", "udp", "prflx", "udp", 1000); +const P2PTransportChannelTestBase::Result P2PTransportChannelTestBase:: + kLocalUdpToRelayUdp("local", "udp", "relay", "udp", + "relay", "udp", "local", "udp", 2000); +const P2PTransportChannelTestBase::Result P2PTransportChannelTestBase:: + kPrflxUdpToRelayUdp("prflx", "udp", "relay", "udp", + "relay", "udp", "prflx", "udp", 2000); +const P2PTransportChannelTestBase::Result P2PTransportChannelTestBase:: + kLocalTcpToLocalTcp("local", "tcp", "local", "tcp", + "local", "tcp", "local", "tcp", 3000); +const P2PTransportChannelTestBase::Result P2PTransportChannelTestBase:: + kLocalTcpToPrflxTcp("local", "tcp", "prflx", "tcp", + "prflx", "tcp", "local", "tcp", 3000); +const P2PTransportChannelTestBase::Result P2PTransportChannelTestBase:: + kPrflxTcpToLocalTcp("prflx", "tcp", "local", "tcp", + "local", "tcp", "prflx", "tcp", 3000); + +// Test the matrix of all the connectivity types we expect to see in the wild. +// Just test every combination of the configs in the Config enum. +class P2PTransportChannelTest : public P2PTransportChannelTestBase { + protected: + static const Result* kMatrix[NUM_CONFIGS][NUM_CONFIGS]; + static const Result* kMatrixSharedUfrag[NUM_CONFIGS][NUM_CONFIGS]; + static const Result* kMatrixSharedSocketAsGice[NUM_CONFIGS][NUM_CONFIGS]; + static const Result* kMatrixSharedSocketAsIce[NUM_CONFIGS][NUM_CONFIGS]; + void ConfigureEndpoints(Config config1, Config config2, + int allocator_flags1, int allocator_flags2, + int delay1, int delay2, + cricket::IceProtocolType type) { + ConfigureEndpoint(0, config1); + SetIceProtocol(0, type); + SetAllocatorFlags(0, allocator_flags1); + SetAllocationStepDelay(0, delay1); + ConfigureEndpoint(1, config2); + SetIceProtocol(1, type); + SetAllocatorFlags(1, allocator_flags2); + SetAllocationStepDelay(1, delay2); + } + void ConfigureEndpoint(int endpoint, Config config) { + switch (config) { + case OPEN: + AddAddress(endpoint, kPublicAddrs[endpoint]); + break; + case NAT_FULL_CONE: + case NAT_ADDR_RESTRICTED: + case NAT_PORT_RESTRICTED: + case NAT_SYMMETRIC: + AddAddress(endpoint, kPrivateAddrs[endpoint]); + // Add a single NAT of the desired type + nat()->AddTranslator(kPublicAddrs[endpoint], kNatAddrs[endpoint], + static_cast(config - NAT_FULL_CONE))-> + AddClient(kPrivateAddrs[endpoint]); + break; + case NAT_DOUBLE_CONE: + case NAT_SYMMETRIC_THEN_CONE: + AddAddress(endpoint, kCascadedPrivateAddrs[endpoint]); + // Add a two cascaded NATs of the desired types + nat()->AddTranslator(kPublicAddrs[endpoint], kNatAddrs[endpoint], + (config == NAT_DOUBLE_CONE) ? + talk_base::NAT_OPEN_CONE : talk_base::NAT_SYMMETRIC)-> + AddTranslator(kPrivateAddrs[endpoint], kCascadedNatAddrs[endpoint], + talk_base::NAT_OPEN_CONE)-> + AddClient(kCascadedPrivateAddrs[endpoint]); + break; + case BLOCK_UDP: + case BLOCK_UDP_AND_INCOMING_TCP: + case BLOCK_ALL_BUT_OUTGOING_HTTP: + case PROXY_HTTPS: + case PROXY_SOCKS: + AddAddress(endpoint, kPublicAddrs[endpoint]); + // Block all UDP + fw()->AddRule(false, talk_base::FP_UDP, talk_base::FD_ANY, + kPublicAddrs[endpoint]); + if (config == BLOCK_UDP_AND_INCOMING_TCP) { + // Block TCP inbound to the endpoint + fw()->AddRule(false, talk_base::FP_TCP, SocketAddress(), + kPublicAddrs[endpoint]); + } else if (config == BLOCK_ALL_BUT_OUTGOING_HTTP) { + // Block all TCP to/from the endpoint except 80/443 out + fw()->AddRule(true, talk_base::FP_TCP, kPublicAddrs[endpoint], + SocketAddress(talk_base::IPAddress(INADDR_ANY), 80)); + fw()->AddRule(true, talk_base::FP_TCP, kPublicAddrs[endpoint], + SocketAddress(talk_base::IPAddress(INADDR_ANY), 443)); + fw()->AddRule(false, talk_base::FP_TCP, talk_base::FD_ANY, + kPublicAddrs[endpoint]); + } else if (config == PROXY_HTTPS) { + // Block all TCP to/from the endpoint except to the proxy server + fw()->AddRule(true, talk_base::FP_TCP, kPublicAddrs[endpoint], + kHttpsProxyAddrs[endpoint]); + fw()->AddRule(false, talk_base::FP_TCP, talk_base::FD_ANY, + kPublicAddrs[endpoint]); + SetProxy(endpoint, talk_base::PROXY_HTTPS); + } else if (config == PROXY_SOCKS) { + // Block all TCP to/from the endpoint except to the proxy server + fw()->AddRule(true, talk_base::FP_TCP, kPublicAddrs[endpoint], + kSocksProxyAddrs[endpoint]); + fw()->AddRule(false, talk_base::FP_TCP, talk_base::FD_ANY, + kPublicAddrs[endpoint]); + SetProxy(endpoint, talk_base::PROXY_SOCKS5); + } + break; + default: + break; + } + } +}; + +// Shorthands for use in the test matrix. +#define LULU &kLocalUdpToLocalUdp +#define LUSU &kLocalUdpToStunUdp +#define LUPU &kLocalUdpToPrflxUdp +#define PULU &kPrflxUdpToLocalUdp +#define SULU &kStunUdpToLocalUdp +#define SUSU &kStunUdpToStunUdp +#define PUSU &kPrflxUdpToStunUdp +#define LURU &kLocalUdpToRelayUdp +#define PURU &kPrflxUdpToRelayUdp +#define LTLT &kLocalTcpToLocalTcp +#define LTPT &kLocalTcpToPrflxTcp +#define PTLT &kPrflxTcpToLocalTcp +// TODO: Enable these once TestRelayServer can accept external TCP. +#define LTRT NULL +#define LSRS NULL + +// Test matrix. Originator behavior defined by rows, receiever by columns. + +// Currently the p2ptransportchannel.cc (specifically the +// P2PTransportChannel::OnUnknownAddress) operates in 2 modes depend on the +// remote candidates - ufrag per port or shared ufrag. +// For example, if the remote candidates have the shared ufrag, for the unknown +// address reaches the OnUnknownAddress, we will try to find the matched +// remote candidate based on the address and protocol, if not found, a new +// remote candidate will be created for this address. But if the remote +// candidates have different ufrags, we will try to find the matched remote +// candidate by comparing the ufrag. If not found, an error will be returned. +// Because currently the shared ufrag feature is under the experiment and will +// be rolled out gradually. We want to test the different combinations of peers +// with/without the shared ufrag enabled. And those different combinations have +// different expectation of the best connection. For example in the OpenToCONE +// case, an unknown address will be updated to a "host" remote candidate if the +// remote peer uses different ufrag per port. But in the shared ufrag case, +// a "stun" (should be peer-reflexive eventually) candidate will be created for +// that. So the expected best candidate will be LUSU instead of LULU. +// With all these, we have to keep 2 test matrixes for the tests: +// kMatrix - for the tests that the remote peer uses different ufrag per port. +// kMatrixSharedUfrag - for the tests that remote peer uses shared ufrag. +// The different between the two matrixes are on: +// OPToCONE, OPTo2CON, +// COToCONE, COToADDR, COToPORT, COToSYMM, COTo2CON, COToSCON, +// ADToCONE, ADToADDR, ADTo2CON, +// POToADDR, +// SYToADDR, +// 2CToCONE, 2CToADDR, 2CToPORT, 2CToSYMM, 2CTo2CON, 2CToSCON, +// SCToADDR, + +// TODO: Fix NULLs caused by lack of TCP support in NATSocket. +// TODO: Fix NULLs caused by no HTTP proxy support. +// TODO: Rearrange rows/columns from best to worst. +// TODO(ronghuawu): Keep only one test matrix once the shared ufrag is enabled. +const P2PTransportChannelTest::Result* + P2PTransportChannelTest::kMatrix[NUM_CONFIGS][NUM_CONFIGS] = { +// OPEN CONE ADDR PORT SYMM 2CON SCON !UDP !TCP HTTP PRXH PRXS +/*OP*/ {LULU, LULU, LULU, LULU, LULU, LULU, LULU, LTLT, LTLT, LSRS, NULL, LTLT}, +/*CO*/ {LULU, LULU, LULU, SULU, SULU, LULU, SULU, NULL, NULL, LSRS, NULL, LTRT}, +/*AD*/ {LULU, LULU, LULU, SUSU, SUSU, LULU, SUSU, NULL, NULL, LSRS, NULL, LTRT}, +/*PO*/ {LULU, LUSU, LUSU, SUSU, LURU, LUSU, LURU, NULL, NULL, LSRS, NULL, LTRT}, +/*SY*/ {LULU, LUSU, LUSU, LURU, LURU, LUSU, LURU, NULL, NULL, LSRS, NULL, LTRT}, +/*2C*/ {LULU, LULU, LULU, SULU, SULU, LULU, SULU, NULL, NULL, LSRS, NULL, LTRT}, +/*SC*/ {LULU, LUSU, LUSU, LURU, LURU, LUSU, LURU, NULL, NULL, LSRS, NULL, LTRT}, +/*!U*/ {LTLT, NULL, NULL, NULL, NULL, NULL, NULL, LTLT, LTLT, LSRS, NULL, LTRT}, +/*!T*/ {LTRT, NULL, NULL, NULL, NULL, NULL, NULL, LTLT, LTRT, LSRS, NULL, LTRT}, +/*HT*/ {LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, NULL, LSRS}, +/*PR*/ {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}, +/*PR*/ {LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LSRS, NULL, LTRT}, +}; +const P2PTransportChannelTest::Result* + P2PTransportChannelTest::kMatrixSharedUfrag[NUM_CONFIGS][NUM_CONFIGS] = { +// OPEN CONE ADDR PORT SYMM 2CON SCON !UDP !TCP HTTP PRXH PRXS +/*OP*/ {LULU, LUSU, LULU, LULU, LULU, LUSU, LULU, LTLT, LTLT, LSRS, NULL, LTLT}, +/*CO*/ {LULU, LUSU, LUSU, SUSU, SUSU, LUSU, SUSU, NULL, NULL, LSRS, NULL, LTRT}, +/*AD*/ {LULU, LUSU, LUSU, SUSU, SUSU, LUSU, SUSU, NULL, NULL, LSRS, NULL, LTRT}, +/*PO*/ {LULU, LUSU, LUSU, SUSU, LURU, LUSU, LURU, NULL, NULL, LSRS, NULL, LTRT}, +/*SY*/ {LULU, LUSU, LUSU, LURU, LURU, LUSU, LURU, NULL, NULL, LSRS, NULL, LTRT}, +/*2C*/ {LULU, LUSU, LUSU, SUSU, SUSU, LUSU, SUSU, NULL, NULL, LSRS, NULL, LTRT}, +/*SC*/ {LULU, LUSU, LUSU, LURU, LURU, LUSU, LURU, NULL, NULL, LSRS, NULL, LTRT}, +/*!U*/ {LTLT, NULL, NULL, NULL, NULL, NULL, NULL, LTLT, LTLT, LSRS, NULL, LTRT}, +/*!T*/ {LTRT, NULL, NULL, NULL, NULL, NULL, NULL, LTLT, LTRT, LSRS, NULL, LTRT}, +/*HT*/ {LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, NULL, LSRS}, +/*PR*/ {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}, +/*PR*/ {LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LSRS, NULL, LTRT}, +}; +const P2PTransportChannelTest::Result* + P2PTransportChannelTest::kMatrixSharedSocketAsGice + [NUM_CONFIGS][NUM_CONFIGS] = { +// OPEN CONE ADDR PORT SYMM 2CON SCON !UDP !TCP HTTP PRXH PRXS +/*OP*/ {LULU, LUSU, LUSU, LUSU, LUSU, LUSU, LUSU, LTLT, LTLT, LSRS, NULL, LTLT}, +/*CO*/ {LULU, LUSU, LUSU, LUSU, LUSU, LUSU, LUSU, NULL, NULL, LSRS, NULL, LTRT}, +/*AD*/ {LULU, LUSU, LUSU, LUSU, LUSU, LUSU, LUSU, NULL, NULL, LSRS, NULL, LTRT}, +/*PO*/ {LULU, LUSU, LUSU, LUSU, LURU, LUSU, LURU, NULL, NULL, LSRS, NULL, LTRT}, +/*SY*/ {LULU, LUSU, LUSU, LURU, LURU, LUSU, LURU, NULL, NULL, LSRS, NULL, LTRT}, +/*2C*/ {LULU, LUSU, LUSU, LUSU, LUSU, LUSU, LUSU, NULL, NULL, LSRS, NULL, LTRT}, +/*SC*/ {LULU, LUSU, LUSU, LURU, LURU, LUSU, LURU, NULL, NULL, LSRS, NULL, LTRT}, +/*!U*/ {LTLT, NULL, NULL, NULL, NULL, NULL, NULL, LTLT, LTLT, LSRS, NULL, LTRT}, +/*!T*/ {LTRT, NULL, NULL, NULL, NULL, NULL, NULL, LTLT, LTRT, LSRS, NULL, LTRT}, +/*HT*/ {LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, NULL, LSRS}, +/*PR*/ {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}, +/*PR*/ {LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LSRS, NULL, LTRT}, +}; +const P2PTransportChannelTest::Result* + P2PTransportChannelTest::kMatrixSharedSocketAsIce + [NUM_CONFIGS][NUM_CONFIGS] = { +// OPEN CONE ADDR PORT SYMM 2CON SCON !UDP !TCP HTTP PRXH PRXS +/*OP*/ {LULU, LUSU, LUSU, LUSU, LUPU, LUSU, LUPU, PTLT, LTPT, LSRS, NULL, PTLT}, +/*CO*/ {LULU, LUSU, LUSU, LUSU, LUPU, LUSU, LUPU, NULL, NULL, LSRS, NULL, LTRT}, +/*AD*/ {LULU, LUSU, LUSU, LUSU, LUPU, LUSU, LUPU, NULL, NULL, LSRS, NULL, LTRT}, +/*PO*/ {LULU, LUSU, LUSU, LUSU, LURU, LUSU, LURU, NULL, NULL, LSRS, NULL, LTRT}, +/*SY*/ {PULU, PUSU, PUSU, PURU, PURU, PUSU, PURU, NULL, NULL, LSRS, NULL, LTRT}, +/*2C*/ {LULU, LUSU, LUSU, LUSU, LUPU, LUSU, LUPU, NULL, NULL, LSRS, NULL, LTRT}, +/*SC*/ {PULU, PUSU, PUSU, PURU, PURU, PUSU, PURU, NULL, NULL, LSRS, NULL, LTRT}, +/*!U*/ {PTLT, NULL, NULL, NULL, NULL, NULL, NULL, PTLT, LTPT, LSRS, NULL, LTRT}, +/*!T*/ {LTRT, NULL, NULL, NULL, NULL, NULL, NULL, PTLT, LTRT, LSRS, NULL, LTRT}, +/*HT*/ {LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, NULL, LSRS}, +/*PR*/ {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}, +/*PR*/ {LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LSRS, NULL, LTRT}, +}; + +// The actual tests that exercise all the various configurations. +// Test names are of the form P2PTransportChannelTest_TestOPENToNAT_FULL_CONE +// Same test case is run in both GICE and ICE mode. +// kDefaultStepDelay - is used for all Gice cases. +// kMinimumStepDelay - is used when both end points have +// PORTALLOCATOR_ENABLE_SHARED_UFRAG flag enabled. +// Technically we should be able to use kMinimumStepDelay irrespective of +// protocol type. But which might need modifications to current result matrices +// for tests in this file. +#define P2P_TEST_DECLARATION(x, y, z) \ + TEST_F(P2PTransportChannelTest, z##Test##x##To##y##AsGiceNoneSharedUfrag) { \ + ConfigureEndpoints(x, y, kDefaultPortAllocatorFlags, \ + kDefaultPortAllocatorFlags, \ + kDefaultStepDelay, kDefaultStepDelay, \ + cricket::ICEPROTO_GOOGLE); \ + if (kMatrix[x][y] != NULL) \ + Test(*kMatrix[x][y]); \ + else \ + LOG(LS_WARNING) << "Not yet implemented"; \ + } \ + TEST_F(P2PTransportChannelTest, z##Test##x##To##y##AsGiceP0SharedUfrag) { \ + ConfigureEndpoints(x, y, PORTALLOCATOR_ENABLE_SHARED_UFRAG, \ + kDefaultPortAllocatorFlags, \ + kDefaultStepDelay, kDefaultStepDelay, \ + cricket::ICEPROTO_GOOGLE); \ + if (kMatrix[x][y] != NULL) \ + Test(*kMatrix[x][y]); \ + else \ + LOG(LS_WARNING) << "Not yet implemented"; \ + } \ + TEST_F(P2PTransportChannelTest, z##Test##x##To##y##AsGiceP1SharedUfrag) { \ + ConfigureEndpoints(x, y, kDefaultPortAllocatorFlags, \ + PORTALLOCATOR_ENABLE_SHARED_UFRAG, \ + kDefaultStepDelay, kDefaultStepDelay, \ + cricket::ICEPROTO_GOOGLE); \ + if (kMatrixSharedUfrag[x][y] != NULL) \ + Test(*kMatrixSharedUfrag[x][y]); \ + else \ + LOG(LS_WARNING) << "Not yet implemented"; \ + } \ + TEST_F(P2PTransportChannelTest, z##Test##x##To##y##AsGiceBothSharedUfrag) { \ + ConfigureEndpoints(x, y, PORTALLOCATOR_ENABLE_SHARED_UFRAG, \ + PORTALLOCATOR_ENABLE_SHARED_UFRAG, \ + kDefaultStepDelay, kDefaultStepDelay, \ + cricket::ICEPROTO_GOOGLE); \ + if (kMatrixSharedUfrag[x][y] != NULL) \ + Test(*kMatrixSharedUfrag[x][y]); \ + else \ + LOG(LS_WARNING) << "Not yet implemented"; \ + } \ + TEST_F(P2PTransportChannelTest, \ + z##Test##x##To##y##AsGiceBothSharedUfragWithMinimumStepDelay) { \ + ConfigureEndpoints(x, y, PORTALLOCATOR_ENABLE_SHARED_UFRAG, \ + PORTALLOCATOR_ENABLE_SHARED_UFRAG, \ + kMinimumStepDelay, kMinimumStepDelay, \ + cricket::ICEPROTO_GOOGLE); \ + if (kMatrixSharedUfrag[x][y] != NULL) \ + Test(*kMatrixSharedUfrag[x][y]); \ + else \ + LOG(LS_WARNING) << "Not yet implemented"; \ + } \ + TEST_F(P2PTransportChannelTest, \ + z##Test##x##To##y##AsGiceBothSharedUfragSocket) { \ + ConfigureEndpoints(x, y, PORTALLOCATOR_ENABLE_SHARED_UFRAG | \ + PORTALLOCATOR_ENABLE_SHARED_SOCKET, \ + PORTALLOCATOR_ENABLE_SHARED_UFRAG | \ + PORTALLOCATOR_ENABLE_SHARED_SOCKET, \ + kMinimumStepDelay, kMinimumStepDelay, \ + cricket::ICEPROTO_GOOGLE); \ + if (kMatrixSharedSocketAsGice[x][y] != NULL) \ + Test(*kMatrixSharedSocketAsGice[x][y]); \ + else \ + LOG(LS_WARNING) << "Not yet implemented"; \ + } \ + TEST_F(P2PTransportChannelTest, z##Test##x##To##y##AsIce) { \ + ConfigureEndpoints(x, y, PORTALLOCATOR_ENABLE_SHARED_UFRAG | \ + PORTALLOCATOR_ENABLE_SHARED_SOCKET, \ + PORTALLOCATOR_ENABLE_SHARED_UFRAG | \ + PORTALLOCATOR_ENABLE_SHARED_SOCKET, \ + kMinimumStepDelay, kMinimumStepDelay, \ + cricket::ICEPROTO_RFC5245); \ + if (kMatrixSharedSocketAsIce[x][y] != NULL) \ + Test(*kMatrixSharedSocketAsIce[x][y]); \ + else \ + LOG(LS_WARNING) << "Not yet implemented"; \ + } + +#define P2P_TEST(x, y) \ + P2P_TEST_DECLARATION(x, y,) + +#define FLAKY_P2P_TEST(x, y) \ + P2P_TEST_DECLARATION(x, y, DISABLED_) + +#define P2P_TEST_SET(x) \ + P2P_TEST(x, OPEN) \ + P2P_TEST(x, NAT_FULL_CONE) \ + P2P_TEST(x, NAT_ADDR_RESTRICTED) \ + P2P_TEST(x, NAT_PORT_RESTRICTED) \ + P2P_TEST(x, NAT_SYMMETRIC) \ + P2P_TEST(x, NAT_DOUBLE_CONE) \ + P2P_TEST(x, NAT_SYMMETRIC_THEN_CONE) \ + P2P_TEST(x, BLOCK_UDP) \ + P2P_TEST(x, BLOCK_UDP_AND_INCOMING_TCP) \ + P2P_TEST(x, BLOCK_ALL_BUT_OUTGOING_HTTP) \ + P2P_TEST(x, PROXY_HTTPS) \ + P2P_TEST(x, PROXY_SOCKS) + +#define FLAKY_P2P_TEST_SET(x) \ + P2P_TEST(x, OPEN) \ + P2P_TEST(x, NAT_FULL_CONE) \ + P2P_TEST(x, NAT_ADDR_RESTRICTED) \ + P2P_TEST(x, NAT_PORT_RESTRICTED) \ + P2P_TEST(x, NAT_SYMMETRIC) \ + P2P_TEST(x, NAT_DOUBLE_CONE) \ + P2P_TEST(x, NAT_SYMMETRIC_THEN_CONE) \ + P2P_TEST(x, BLOCK_UDP) \ + P2P_TEST(x, BLOCK_UDP_AND_INCOMING_TCP) \ + P2P_TEST(x, BLOCK_ALL_BUT_OUTGOING_HTTP) \ + P2P_TEST(x, PROXY_HTTPS) \ + P2P_TEST(x, PROXY_SOCKS) + +P2P_TEST_SET(OPEN) +P2P_TEST_SET(NAT_FULL_CONE) +P2P_TEST_SET(NAT_ADDR_RESTRICTED) +P2P_TEST_SET(NAT_PORT_RESTRICTED) +P2P_TEST_SET(NAT_SYMMETRIC) +P2P_TEST_SET(NAT_DOUBLE_CONE) +P2P_TEST_SET(NAT_SYMMETRIC_THEN_CONE) +P2P_TEST_SET(BLOCK_UDP) +P2P_TEST_SET(BLOCK_UDP_AND_INCOMING_TCP) +P2P_TEST_SET(BLOCK_ALL_BUT_OUTGOING_HTTP) +P2P_TEST_SET(PROXY_HTTPS) +P2P_TEST_SET(PROXY_SOCKS) + +// Test that we restart candidate allocation when local ufrag&pwd changed. +// Standard Ice protocol is used. +TEST_F(P2PTransportChannelTest, HandleUfragPwdChangeAsIce) { + ConfigureEndpoints(OPEN, OPEN, + PORTALLOCATOR_ENABLE_SHARED_UFRAG, + PORTALLOCATOR_ENABLE_SHARED_UFRAG, + kMinimumStepDelay, kMinimumStepDelay, + cricket::ICEPROTO_RFC5245); + CreateChannels(1); + TestHandleIceUfragPasswordChanged(); +} + +// Test that we restart candidate allocation when local ufrag&pwd changed. +// Standard Ice protocol is used. +TEST_F(P2PTransportChannelTest, HandleUfragPwdChangeBundleAsIce) { + ConfigureEndpoints(OPEN, OPEN, + PORTALLOCATOR_ENABLE_SHARED_UFRAG, + PORTALLOCATOR_ENABLE_SHARED_UFRAG, + kMinimumStepDelay, kMinimumStepDelay, + cricket::ICEPROTO_RFC5245); + SetAllocatorFlags(0, cricket::PORTALLOCATOR_ENABLE_BUNDLE); + SetAllocatorFlags(1, cricket::PORTALLOCATOR_ENABLE_BUNDLE); + + CreateChannels(2); + TestHandleIceUfragPasswordChanged(); +} + +// Test that we restart candidate allocation when local ufrag&pwd changed. +// Google Ice protocol is used. +TEST_F(P2PTransportChannelTest, HandleUfragPwdChangeAsGice) { + ConfigureEndpoints(OPEN, OPEN, + PORTALLOCATOR_ENABLE_SHARED_UFRAG, + PORTALLOCATOR_ENABLE_SHARED_UFRAG, + kDefaultStepDelay, kDefaultStepDelay, + cricket::ICEPROTO_GOOGLE); + CreateChannels(1); + TestHandleIceUfragPasswordChanged(); +} + +// Test that ICE restart works when bundle is enabled. +// Google Ice protocol is used. +TEST_F(P2PTransportChannelTest, HandleUfragPwdChangeBundleAsGice) { + ConfigureEndpoints(OPEN, OPEN, + PORTALLOCATOR_ENABLE_SHARED_UFRAG, + PORTALLOCATOR_ENABLE_SHARED_UFRAG, + kDefaultStepDelay, kDefaultStepDelay, + cricket::ICEPROTO_GOOGLE); + SetAllocatorFlags(0, cricket::PORTALLOCATOR_ENABLE_BUNDLE); + SetAllocatorFlags(1, cricket::PORTALLOCATOR_ENABLE_BUNDLE); + + CreateChannels(2); + TestHandleIceUfragPasswordChanged(); +} + +// Test the operation of GetStats. +TEST_F(P2PTransportChannelTest, GetStats) { + ConfigureEndpoints(OPEN, OPEN, + kDefaultPortAllocatorFlags, + kDefaultPortAllocatorFlags, + kDefaultStepDelay, kDefaultStepDelay, + cricket::ICEPROTO_GOOGLE); + CreateChannels(1); + EXPECT_TRUE_WAIT_MARGIN(ep1_ch1()->readable() && ep1_ch1()->writable() && + ep2_ch1()->readable() && ep2_ch1()->writable(), + 1000, 1000); + TestSendRecv(1); + cricket::ConnectionInfos infos; + ASSERT_TRUE(ep1_ch1()->GetStats(&infos)); + ASSERT_EQ(1U, infos.size()); + EXPECT_TRUE(infos[0].new_connection); + EXPECT_TRUE(infos[0].best_connection); + EXPECT_TRUE(infos[0].readable); + EXPECT_TRUE(infos[0].writable); + EXPECT_FALSE(infos[0].timeout); + EXPECT_EQ(10 * 36U, infos[0].sent_total_bytes); + EXPECT_EQ(10 * 36U, infos[0].recv_total_bytes); + EXPECT_GT(infos[0].rtt, 0U); + DestroyChannels(); +} + +// Test that we properly handle getting a STUN error due to slow signaling. +TEST_F(P2PTransportChannelTest, SlowSignaling) { + ConfigureEndpoints(OPEN, NAT_SYMMETRIC, + kDefaultPortAllocatorFlags, + kDefaultPortAllocatorFlags, + kDefaultStepDelay, kDefaultStepDelay, + cricket::ICEPROTO_GOOGLE); + // Make signaling from the callee take 500ms, so that the initial STUN pings + // from the callee beat the signaling, and so the caller responds with a + // unknown username error. We should just eat that and carry on; mishandling + // this will instead cause all the callee's connections to be discarded. + SetSignalingDelay(1, 1000); + CreateChannels(1); + const cricket::Connection* best_connection = NULL; + // Wait until the callee's connections are created. + WAIT((best_connection = ep2_ch1()->best_connection()) != NULL, 1000); + // Wait to see if they get culled; they shouldn't. + WAIT(ep2_ch1()->best_connection() != best_connection, 1000); + EXPECT_TRUE(ep2_ch1()->best_connection() == best_connection); + DestroyChannels(); +} + +// Test that if remote candidates don't have ufrag and pwd, we still work. +TEST_F(P2PTransportChannelTest, RemoteCandidatesWithoutUfragPwd) { + set_clear_remote_candidates_ufrag_pwd(true); + ConfigureEndpoints(OPEN, OPEN, + PORTALLOCATOR_ENABLE_SHARED_UFRAG, + PORTALLOCATOR_ENABLE_SHARED_UFRAG, + kMinimumStepDelay, kMinimumStepDelay, + cricket::ICEPROTO_GOOGLE); + CreateChannels(1); + const cricket::Connection* best_connection = NULL; + // Wait until the callee's connections are created. + WAIT((best_connection = ep2_ch1()->best_connection()) != NULL, 1000); + // Wait to see if they get culled; they shouldn't. + WAIT(ep2_ch1()->best_connection() != best_connection, 1000); + EXPECT_TRUE(ep2_ch1()->best_connection() == best_connection); + DestroyChannels(); +} + +// Test that a host behind NAT cannot be reached when incoming_only +// is set to true. +TEST_F(P2PTransportChannelTest, IncomingOnlyBlocked) { + ConfigureEndpoints(NAT_FULL_CONE, OPEN, + kDefaultPortAllocatorFlags, + kDefaultPortAllocatorFlags, + kDefaultStepDelay, kDefaultStepDelay, + cricket::ICEPROTO_GOOGLE); + + SetAllocatorFlags(0, kOnlyLocalPorts); + CreateChannels(1); + ep1_ch1()->set_incoming_only(true); + + // Pump for 1 second and verify that the channels are not connected. + talk_base::Thread::Current()->ProcessMessages(1000); + + EXPECT_FALSE(ep1_ch1()->readable()); + EXPECT_FALSE(ep1_ch1()->writable()); + EXPECT_FALSE(ep2_ch1()->readable()); + EXPECT_FALSE(ep2_ch1()->writable()); + + DestroyChannels(); +} + +// Test that a peer behind NAT can connect to a peer that has +// incoming_only flag set. +TEST_F(P2PTransportChannelTest, IncomingOnlyOpen) { + ConfigureEndpoints(OPEN, NAT_FULL_CONE, + kDefaultPortAllocatorFlags, + kDefaultPortAllocatorFlags, + kDefaultStepDelay, kDefaultStepDelay, + cricket::ICEPROTO_GOOGLE); + + SetAllocatorFlags(0, kOnlyLocalPorts); + CreateChannels(1); + ep1_ch1()->set_incoming_only(true); + + EXPECT_TRUE_WAIT_MARGIN(ep1_ch1() != NULL && ep2_ch1() != NULL && + ep1_ch1()->readable() && ep1_ch1()->writable() && + ep2_ch1()->readable() && ep2_ch1()->writable(), + 1000, 1000); + + DestroyChannels(); +} + +// Test what happens when we have 2 users behind the same NAT. This can lead +// to interesting behavior because the STUN server will only give out the +// address of the outermost NAT. +class P2PTransportChannelSameNatTest : public P2PTransportChannelTestBase { + protected: + void ConfigureEndpoints(Config nat_type, Config config1, Config config2) { + ASSERT(nat_type >= NAT_FULL_CONE && nat_type <= NAT_SYMMETRIC); + talk_base::NATSocketServer::Translator* outer_nat = + nat()->AddTranslator(kPublicAddrs[0], kNatAddrs[0], + static_cast(nat_type - NAT_FULL_CONE)); + ConfigureEndpoint(outer_nat, 0, config1); + ConfigureEndpoint(outer_nat, 1, config2); + } + void ConfigureEndpoint(talk_base::NATSocketServer::Translator* nat, + int endpoint, Config config) { + ASSERT(config <= NAT_SYMMETRIC); + if (config == OPEN) { + AddAddress(endpoint, kPrivateAddrs[endpoint]); + nat->AddClient(kPrivateAddrs[endpoint]); + } else { + AddAddress(endpoint, kCascadedPrivateAddrs[endpoint]); + nat->AddTranslator(kPrivateAddrs[endpoint], kCascadedNatAddrs[endpoint], + static_cast(config - NAT_FULL_CONE))->AddClient( + kCascadedPrivateAddrs[endpoint]); + } + } +}; + +TEST_F(P2PTransportChannelSameNatTest, TestConesBehindSameCone) { + ConfigureEndpoints(NAT_FULL_CONE, NAT_FULL_CONE, NAT_FULL_CONE); + Test(kLocalUdpToStunUdp); +} + +// Test what happens when we have multiple available pathways. +// In the future we will try different RTTs and configs for the different +// interfaces, so that we can simulate a user with Ethernet and VPN networks. +class P2PTransportChannelMultihomedTest : public P2PTransportChannelTestBase { +}; + +// Test that we can establish connectivity when both peers are multihomed. +TEST_F(P2PTransportChannelMultihomedTest, TestBasic) { + AddAddress(0, kPublicAddrs[0]); + AddAddress(0, kAlternateAddrs[0]); + AddAddress(1, kPublicAddrs[1]); + AddAddress(1, kAlternateAddrs[1]); + Test(kLocalUdpToLocalUdp); +} + +// Test that we can quickly switch links if an interface goes down. +TEST_F(P2PTransportChannelMultihomedTest, TestFailover) { + AddAddress(0, kPublicAddrs[0]); + AddAddress(1, kPublicAddrs[1]); + AddAddress(1, kAlternateAddrs[1]); + // Use only local ports for simplicity. + SetAllocatorFlags(0, kOnlyLocalPorts); + SetAllocatorFlags(1, kOnlyLocalPorts); + + // Create channels and let them go writable, as usual. + CreateChannels(1); + EXPECT_TRUE_WAIT(ep1_ch1()->readable() && ep1_ch1()->writable() && + ep2_ch1()->readable() && ep2_ch1()->writable(), + 1000); + EXPECT_TRUE( + ep1_ch1()->best_connection() && ep2_ch1()->best_connection() && + LocalCandidate(ep1_ch1())->address().EqualIPs(kPublicAddrs[0]) && + RemoteCandidate(ep1_ch1())->address().EqualIPs(kPublicAddrs[1])); + + // Blackhole any traffic to or from the public addrs. + LOG(LS_INFO) << "Failing over..."; + fw()->AddRule(false, talk_base::FP_ANY, talk_base::FD_ANY, + kPublicAddrs[1]); + + // We should detect loss of connectivity within 5 seconds or so. + EXPECT_TRUE_WAIT(!ep1_ch1()->writable(), 7000); + + // We should switch over to use the alternate addr immediately + // when we lose writability. + EXPECT_TRUE_WAIT( + ep1_ch1()->best_connection() && ep2_ch1()->best_connection() && + LocalCandidate(ep1_ch1())->address().EqualIPs(kPublicAddrs[0]) && + RemoteCandidate(ep1_ch1())->address().EqualIPs(kAlternateAddrs[1]), + 3000); + + DestroyChannels(); +} + +// Test that we can switch links in a coordinated fashion. +TEST_F(P2PTransportChannelMultihomedTest, TestDrain) { + AddAddress(0, kPublicAddrs[0]); + AddAddress(1, kPublicAddrs[1]); + // Use only local ports for simplicity. + SetAllocatorFlags(0, kOnlyLocalPorts); + SetAllocatorFlags(1, kOnlyLocalPorts); + + // Create channels and let them go writable, as usual. + CreateChannels(1); + EXPECT_TRUE_WAIT(ep1_ch1()->readable() && ep1_ch1()->writable() && + ep2_ch1()->readable() && ep2_ch1()->writable(), + 1000); + EXPECT_TRUE( + ep1_ch1()->best_connection() && ep2_ch1()->best_connection() && + LocalCandidate(ep1_ch1())->address().EqualIPs(kPublicAddrs[0]) && + RemoteCandidate(ep1_ch1())->address().EqualIPs(kPublicAddrs[1])); + + // Remove the public interface, add the alternate interface, and allocate + // a new generation of candidates for the new interface (via Connect()). + LOG(LS_INFO) << "Draining..."; + AddAddress(1, kAlternateAddrs[1]); + RemoveAddress(1, kPublicAddrs[1]); + ep2_ch1()->Connect(); + + // We should switch over to use the alternate address after + // an exchange of pings. + EXPECT_TRUE_WAIT( + ep1_ch1()->best_connection() && ep2_ch1()->best_connection() && + LocalCandidate(ep1_ch1())->address().EqualIPs(kPublicAddrs[0]) && + RemoteCandidate(ep1_ch1())->address().EqualIPs(kAlternateAddrs[1]), + 3000); + + DestroyChannels(); +} + +TEST_F(P2PTransportChannelTest, TestBundleAllocatorToBundleAllocator) { + AddAddress(0, kPublicAddrs[0]); + AddAddress(1, kPublicAddrs[1]); + SetAllocatorFlags(0, cricket::PORTALLOCATOR_ENABLE_BUNDLE); + SetAllocatorFlags(1, cricket::PORTALLOCATOR_ENABLE_BUNDLE); + + CreateChannels(2); + + EXPECT_TRUE_WAIT(ep1_ch1()->readable() && + ep1_ch1()->writable() && + ep2_ch1()->readable() && + ep2_ch1()->writable(), + 1000); + EXPECT_TRUE(ep1_ch1()->best_connection() && + ep2_ch1()->best_connection()); + + EXPECT_FALSE(ep1_ch2()->readable()); + EXPECT_FALSE(ep1_ch2()->writable()); + EXPECT_FALSE(ep2_ch2()->readable()); + EXPECT_FALSE(ep2_ch2()->writable()); + + TestSendRecv(1); // Only 1 channel is writable per Endpoint. + DestroyChannels(); +} + +TEST_F(P2PTransportChannelTest, TestBundleAllocatorToNonBundleAllocator) { + AddAddress(0, kPublicAddrs[0]); + AddAddress(1, kPublicAddrs[1]); + // Enable BUNDLE flag at one side. + SetAllocatorFlags(0, cricket::PORTALLOCATOR_ENABLE_BUNDLE); + + CreateChannels(2); + + EXPECT_TRUE_WAIT(ep1_ch1()->readable() && + ep1_ch1()->writable() && + ep2_ch1()->readable() && + ep2_ch1()->writable(), + 1000); + EXPECT_TRUE_WAIT(ep1_ch2()->readable() && + ep1_ch2()->writable() && + ep2_ch2()->readable() && + ep2_ch2()->writable(), + 1000); + + EXPECT_TRUE(ep1_ch1()->best_connection() && + ep2_ch1()->best_connection()); + EXPECT_TRUE(ep1_ch2()->best_connection() && + ep2_ch2()->best_connection()); + + TestSendRecv(2); + DestroyChannels(); +} + +TEST_F(P2PTransportChannelTest, TestIceRoleConflictWithoutBundle) { + AddAddress(0, kPublicAddrs[0]); + AddAddress(1, kPublicAddrs[1]); + TestSignalRoleConflict(); +} + +TEST_F(P2PTransportChannelTest, TestIceRoleConflictWithBundle) { + AddAddress(0, kPublicAddrs[0]); + AddAddress(1, kPublicAddrs[1]); + SetAllocatorFlags(0, cricket::PORTALLOCATOR_ENABLE_BUNDLE); + SetAllocatorFlags(1, cricket::PORTALLOCATOR_ENABLE_BUNDLE); + TestSignalRoleConflict(); +} + +// Tests that the ice configs (protocol, tiebreaker and role can be passed down +// to ports. +TEST_F(P2PTransportChannelTest, TestIceConfigWillPassDownToPort) { + AddAddress(0, kPublicAddrs[0]); + AddAddress(1, kPublicAddrs[1]); + + SetIceRole(0, cricket::ROLE_CONTROLLING); + SetIceProtocol(0, cricket::ICEPROTO_GOOGLE); + SetTiebreaker(0, kTiebreaker1); + SetIceRole(1, cricket::ROLE_CONTROLLING); + SetIceProtocol(1, cricket::ICEPROTO_RFC5245); + SetTiebreaker(1, kTiebreaker2); + + CreateChannels(1); + + EXPECT_EQ_WAIT(2u, ep1_ch1()->ports().size(), 1000); + + const std::vector ports_before = ep1_ch1()->ports(); + for (size_t i = 0; i < ports_before.size(); ++i) { + EXPECT_EQ(cricket::ROLE_CONTROLLING, ports_before[i]->Role()); + EXPECT_EQ(cricket::ICEPROTO_GOOGLE, ports_before[i]->IceProtocol()); + EXPECT_EQ(kTiebreaker1, ports_before[i]->Tiebreaker()); + } + + ep1_ch1()->SetRole(cricket::ROLE_CONTROLLED); + ep1_ch1()->SetIceProtocolType(cricket::ICEPROTO_RFC5245); + ep1_ch1()->SetTiebreaker(kTiebreaker2); + + const std::vector ports_after = ep1_ch1()->ports(); + for (size_t i = 0; i < ports_after.size(); ++i) { + EXPECT_EQ(cricket::ROLE_CONTROLLED, ports_before[i]->Role()); + EXPECT_EQ(cricket::ICEPROTO_RFC5245, ports_before[i]->IceProtocol()); + // SetTiebreaker after Connect() has been called will fail. So expect the + // original value. + EXPECT_EQ(kTiebreaker1, ports_before[i]->Tiebreaker()); + } + + EXPECT_TRUE_WAIT(ep1_ch1()->readable() && + ep1_ch1()->writable() && + ep2_ch1()->readable() && + ep2_ch1()->writable(), + 1000); + + EXPECT_TRUE(ep1_ch1()->best_connection() && + ep2_ch1()->best_connection()); + + TestSendRecv(1); +} diff --git a/talk/p2p/base/packetsocketfactory.h b/talk/p2p/base/packetsocketfactory.h new file mode 100644 index 000000000..b8a984635 --- /dev/null +++ b/talk/p2p/base/packetsocketfactory.h @@ -0,0 +1,65 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_BASE_PACKETSOCKETFACTORY_H_ +#define TALK_BASE_PACKETSOCKETFACTORY_H_ + +#include "talk/base/proxyinfo.h" + +namespace talk_base { + +class AsyncPacketSocket; + +class PacketSocketFactory { + public: + enum Options { + OPT_SSLTCP = 0x01, + OPT_STUN = 0x02, + }; + + PacketSocketFactory() { } + virtual ~PacketSocketFactory() { } + + virtual AsyncPacketSocket* CreateUdpSocket( + const SocketAddress& address, int min_port, int max_port) = 0; + virtual AsyncPacketSocket* CreateServerTcpSocket( + const SocketAddress& local_address, int min_port, int max_port, + int opts) = 0; + + // TODO: |proxy_info| and |user_agent| should be set + // per-factory and not when socket is created. + virtual AsyncPacketSocket* CreateClientTcpSocket( + const SocketAddress& local_address, const SocketAddress& remote_address, + const ProxyInfo& proxy_info, const std::string& user_agent, int opts) = 0; + + private: + DISALLOW_EVIL_CONSTRUCTORS(PacketSocketFactory); +}; + +} // namespace talk_base + +#endif // TALK_BASE_PACKETSOCKETFACTORY_H_ diff --git a/talk/p2p/base/parsing.cc b/talk/p2p/base/parsing.cc new file mode 100644 index 000000000..ebe05968f --- /dev/null +++ b/talk/p2p/base/parsing.cc @@ -0,0 +1,158 @@ +/* + * libjingle + * Copyright 2010, 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/p2p/base/parsing.h" + +#include +#include +#include "talk/base/stringutils.h" + +namespace { +static const char kTrue[] = "true"; +static const char kOne[] = "1"; +} + +namespace cricket { + +bool BadParse(const std::string& text, ParseError* err) { + if (err != NULL) { + err->text = text; + } + return false; +} + +bool BadWrite(const std::string& text, WriteError* err) { + if (err != NULL) { + err->text = text; + } + return false; +} + +std::string GetXmlAttr(const buzz::XmlElement* elem, + const buzz::QName& name, + const std::string& def) { + std::string val = elem->Attr(name); + return val.empty() ? def : val; +} + +std::string GetXmlAttr(const buzz::XmlElement* elem, + const buzz::QName& name, + const char* def) { + return GetXmlAttr(elem, name, std::string(def)); +} + +bool GetXmlAttr(const buzz::XmlElement* elem, + const buzz::QName& name, bool def) { + std::string val = elem->Attr(name); + std::transform(val.begin(), val.end(), val.begin(), tolower); + + return val.empty() ? def : (val == kTrue || val == kOne); +} + +int GetXmlAttr(const buzz::XmlElement* elem, + const buzz::QName& name, int def) { + std::string val = elem->Attr(name); + return val.empty() ? def : atoi(val.c_str()); +} + +const buzz::XmlElement* GetXmlChild(const buzz::XmlElement* parent, + const std::string& name) { + for (const buzz::XmlElement* child = parent->FirstElement(); + child != NULL; + child = child->NextElement()) { + if (child->Name().LocalPart() == name) { + return child; + } + } + return NULL; +} + +bool RequireXmlChild(const buzz::XmlElement* parent, + const std::string& name, + const buzz::XmlElement** child, + ParseError* error) { + *child = GetXmlChild(parent, name); + if (*child == NULL) { + return BadParse("element '" + parent->Name().Merged() + + "' missing required child '" + name, + error); + } else { + return true; + } +} + +bool RequireXmlAttr(const buzz::XmlElement* elem, + const buzz::QName& name, + std::string* value, + ParseError* error) { + if (!elem->HasAttr(name)) { + return BadParse("element '" + elem->Name().Merged() + + "' missing required attribute '" + + name.Merged() + "'", + error); + } else { + *value = elem->Attr(name); + return true; + } +} + +void AddXmlAttrIfNonEmpty(buzz::XmlElement* elem, + const buzz::QName name, + const std::string& value) { + if (!value.empty()) { + elem->AddAttr(name, value); + } +} + +void AddXmlChildren(buzz::XmlElement* parent, + const std::vector& children) { + for (std::vector::const_iterator iter = children.begin(); + iter != children.end(); + iter++) { + parent->AddElement(*iter); + } +} + +void CopyXmlChildren(const buzz::XmlElement* source, buzz::XmlElement* dest) { + for (const buzz::XmlElement* child = source->FirstElement(); + child != NULL; + child = child->NextElement()) { + dest->AddElement(new buzz::XmlElement(*child)); + } +} + +std::vector CopyOfXmlChildren(const buzz::XmlElement* elem) { + std::vector children; + for (const buzz::XmlElement* child = elem->FirstElement(); + child != NULL; + child = child->NextElement()) { + children.push_back(new buzz::XmlElement(*child)); + } + return children; +} + +} // namespace cricket diff --git a/talk/p2p/base/parsing.h b/talk/p2p/base/parsing.h new file mode 100644 index 000000000..c82005660 --- /dev/null +++ b/talk/p2p/base/parsing.h @@ -0,0 +1,157 @@ +/* + * libjingle + * Copyright 2010, 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. + */ + +#ifndef TALK_P2P_BASE_PARSING_H_ +#define TALK_P2P_BASE_PARSING_H_ + +#include +#include +#include "talk/base/basictypes.h" +#include "talk/base/stringencode.h" +#include "talk/xmllite/xmlelement.h" // Needed to delete ParseError.extra. + +namespace cricket { + +typedef std::vector XmlElements; + +// We decided "bool Parse(in, out*, error*)" is generally the best +// parse signature. "out Parse(in)" doesn't allow for errors. +// "error* Parse(in, out*)" doesn't allow flexible memory management. + +// The error type for parsing. +struct ParseError { + public: + // explains the error + std::string text; + // provide details about what wasn't parsable + const buzz::XmlElement* extra; + + ParseError() : extra(NULL) {} + + ~ParseError() { + delete extra; + } + + void SetText(const std::string& text) { + this->text = text; + } +}; + +// The error type for writing. +struct WriteError { + std::string text; + + void SetText(const std::string& text) { + this->text = text; + } +}; + +// Convenience method for returning a message when parsing fails. +bool BadParse(const std::string& text, ParseError* err); + +// Convenience method for returning a message when writing fails. +bool BadWrite(const std::string& text, WriteError* error); + +// helper XML functions +std::string GetXmlAttr(const buzz::XmlElement* elem, + const buzz::QName& name, + const std::string& def); +std::string GetXmlAttr(const buzz::XmlElement* elem, + const buzz::QName& name, + const char* def); +// Return true if the value is "true" or "1". +bool GetXmlAttr(const buzz::XmlElement* elem, + const buzz::QName& name, bool def); +int GetXmlAttr(const buzz::XmlElement* elem, + const buzz::QName& name, int def); + +template +bool GetXmlAttr(const buzz::XmlElement* elem, + const buzz::QName& name, + T* val_out) { + if (!elem->HasAttr(name)) { + return false; + } + std::string unparsed = elem->Attr(name); + return talk_base::FromString(unparsed, val_out); +} + +template +bool GetXmlAttr(const buzz::XmlElement* elem, + const buzz::QName& name, + const T& def, + T* val_out) { + if (!elem->HasAttr(name)) { + *val_out = def; + return true; + } + return GetXmlAttr(elem, name, val_out); +} + +template +bool AddXmlAttr(buzz::XmlElement* elem, + const buzz::QName& name, const T& val) { + std::string buf; + if (!talk_base::ToString(val, &buf)) { + return false; + } + elem->AddAttr(name, buf); + return true; +} + +template +bool SetXmlBody(buzz::XmlElement* elem, const T& val) { + std::string buf; + if (!talk_base::ToString(val, &buf)) { + return false; + } + elem->SetBodyText(buf); + return true; +} + +const buzz::XmlElement* GetXmlChild(const buzz::XmlElement* parent, + const std::string& name); + +bool RequireXmlChild(const buzz::XmlElement* parent, + const std::string& name, + const buzz::XmlElement** child, + ParseError* error); +bool RequireXmlAttr(const buzz::XmlElement* elem, + const buzz::QName& name, + std::string* value, + ParseError* error); +void AddXmlAttrIfNonEmpty(buzz::XmlElement* elem, + const buzz::QName name, + const std::string& value); +void AddXmlChildren(buzz::XmlElement* parent, + const std::vector& children); +void CopyXmlChildren(const buzz::XmlElement* source, buzz::XmlElement* dest); +std::vector CopyOfXmlChildren(const buzz::XmlElement* elem); + +} // namespace cricket + +#endif // TALK_P2P_BASE_PARSING_H_ diff --git a/talk/p2p/base/port.cc b/talk/p2p/base/port.cc new file mode 100644 index 000000000..b310fea07 --- /dev/null +++ b/talk/p2p/base/port.cc @@ -0,0 +1,1400 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/port.h" + +#include +#include + +#include "talk/base/base64.h" +#include "talk/base/crc32.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/messagedigest.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" +#include "talk/p2p/base/common.h" + +namespace { + +// Determines whether we have seen at least the given maximum number of +// pings fail to have a response. +inline bool TooManyFailures( + const std::vector& pings_since_last_response, + uint32 maximum_failures, + uint32 rtt_estimate, + uint32 now) { + + // If we haven't sent that many pings, then we can't have failed that many. + if (pings_since_last_response.size() < maximum_failures) + return false; + + // Check if the window in which we would expect a response to the ping has + // already elapsed. + return pings_since_last_response[maximum_failures - 1] + rtt_estimate < now; +} + +// Determines whether we have gone too long without seeing any response. +inline bool TooLongWithoutResponse( + const std::vector& pings_since_last_response, + uint32 maximum_time, + uint32 now) { + + if (pings_since_last_response.size() == 0) + return false; + + return pings_since_last_response[0] + maximum_time < now; +} + +// GICE(ICEPROTO_GOOGLE) requires different username for RTP and RTCP. +// This function generates a different username by +1 on the last character of +// the given username (|rtp_ufrag|). +std::string GetRtcpUfragFromRtpUfrag(const std::string& rtp_ufrag) { + ASSERT(!rtp_ufrag.empty()); + if (rtp_ufrag.empty()) { + return rtp_ufrag; + } + // Change the last character to the one next to it in the base64 table. + char new_last_char; + if (!talk_base::Base64::GetNextBase64Char(rtp_ufrag[rtp_ufrag.size() - 1], + &new_last_char)) { + // Should not be here. + ASSERT(false); + } + std::string rtcp_ufrag = rtp_ufrag; + rtcp_ufrag[rtcp_ufrag.size() - 1] = new_last_char; + ASSERT(rtcp_ufrag != rtp_ufrag); + return rtcp_ufrag; +} + +// We will restrict RTT estimates (when used for determining state) to be +// within a reasonable range. +const uint32 MINIMUM_RTT = 100; // 0.1 seconds +const uint32 MAXIMUM_RTT = 3000; // 3 seconds + +// When we don't have any RTT data, we have to pick something reasonable. We +// use a large value just in case the connection is really slow. +const uint32 DEFAULT_RTT = MAXIMUM_RTT; + +// Computes our estimate of the RTT given the current estimate. +inline uint32 ConservativeRTTEstimate(uint32 rtt) { + return talk_base::_max(MINIMUM_RTT, talk_base::_min(MAXIMUM_RTT, 2 * rtt)); +} + +// Weighting of the old rtt value to new data. +const int RTT_RATIO = 3; // 3 : 1 + +// The delay before we begin checking if this port is useless. +const int kPortTimeoutDelay = 30 * 1000; // 30 seconds + +const uint32 MSG_CHECKTIMEOUT = 1; +const uint32 MSG_DELETE = 1; +} + +namespace cricket { + +// TODO(ronghuawu): Use "host", "srflx", "prflx" and "relay". But this requires +// the signaling part be updated correspondingly as well. +const char LOCAL_PORT_TYPE[] = "local"; +const char STUN_PORT_TYPE[] = "stun"; +const char PRFLX_PORT_TYPE[] = "prflx"; +const char RELAY_PORT_TYPE[] = "relay"; + +const char UDP_PROTOCOL_NAME[] = "udp"; +const char TCP_PROTOCOL_NAME[] = "tcp"; +const char SSLTCP_PROTOCOL_NAME[] = "ssltcp"; + +static const char* const PROTO_NAMES[] = { UDP_PROTOCOL_NAME, + TCP_PROTOCOL_NAME, + SSLTCP_PROTOCOL_NAME }; + +const char* ProtoToString(ProtocolType proto) { + return PROTO_NAMES[proto]; +} + +bool StringToProto(const char* value, ProtocolType* proto) { + for (size_t i = 0; i <= PROTO_LAST; ++i) { + if (_stricmp(PROTO_NAMES[i], value) == 0) { + *proto = static_cast(i); + return true; + } + } + return false; +} + +// Foundation: An arbitrary string that is the same for two candidates +// that have the same type, base IP address, protocol (UDP, TCP, +// etc.), and STUN or TURN server. If any of these are different, +// then the foundation will be different. Two candidate pairs with +// the same foundation pairs are likely to have similar network +// characteristics. Foundations are used in the frozen algorithm. +static std::string ComputeFoundation( + const std::string& type, + const std::string& protocol, + const talk_base::SocketAddress& base_address) { + std::ostringstream ost; + ost << type << base_address.ipaddr().ToString() << protocol; + return talk_base::ToString(talk_base::ComputeCrc32(ost.str())); +} + +Port::Port(talk_base::Thread* thread, talk_base::Network* network, + const talk_base::IPAddress& ip, + const std::string& username_fragment, const std::string& password) + : thread_(thread), + factory_(NULL), + send_retransmit_count_attribute_(false), + network_(network), + ip_(ip), + min_port_(0), + max_port_(0), + component_(ICE_CANDIDATE_COMPONENT_DEFAULT), + generation_(0), + ice_username_fragment_(username_fragment), + password_(password), + lifetime_(LT_PRESTART), + enable_port_packets_(false), + ice_protocol_(ICEPROTO_GOOGLE), + role_(ROLE_UNKNOWN), + tiebreaker_(0), + shared_socket_(true) { + Construct(); +} + +Port::Port(talk_base::Thread* thread, const std::string& type, + talk_base::PacketSocketFactory* factory, + talk_base::Network* network, const talk_base::IPAddress& ip, + int min_port, int max_port, const std::string& username_fragment, + const std::string& password) + : thread_(thread), + factory_(factory), + type_(type), + send_retransmit_count_attribute_(false), + network_(network), + ip_(ip), + min_port_(min_port), + max_port_(max_port), + component_(ICE_CANDIDATE_COMPONENT_DEFAULT), + generation_(0), + ice_username_fragment_(username_fragment), + password_(password), + lifetime_(LT_PRESTART), + enable_port_packets_(false), + ice_protocol_(ICEPROTO_GOOGLE), + role_(ROLE_UNKNOWN), + tiebreaker_(0), + shared_socket_(false) { + ASSERT(factory_ != NULL); + Construct(); +} + +void Port::Construct() { + // If the username_fragment and password are empty, we should just create one. + if (ice_username_fragment_.empty()) { + ASSERT(password_.empty()); + ice_username_fragment_ = talk_base::CreateRandomString(ICE_UFRAG_LENGTH); + password_ = talk_base::CreateRandomString(ICE_PWD_LENGTH); + } + LOG_J(LS_INFO, this) << "Port created"; +} + +Port::~Port() { + // Delete all of the remaining connections. We copy the list up front + // because each deletion will cause it to be modified. + + std::vector list; + + AddressMap::iterator iter = connections_.begin(); + while (iter != connections_.end()) { + list.push_back(iter->second); + ++iter; + } + + for (uint32 i = 0; i < list.size(); i++) + delete list[i]; +} + +Connection* Port::GetConnection(const talk_base::SocketAddress& remote_addr) { + AddressMap::const_iterator iter = connections_.find(remote_addr); + if (iter != connections_.end()) + return iter->second; + else + return NULL; +} + +void Port::AddAddress(const talk_base::SocketAddress& address, + const talk_base::SocketAddress& base_address, + const std::string& protocol, + const std::string& type, + uint32 type_preference, + bool final) { + Candidate c; + c.set_id(talk_base::CreateRandomString(8)); + c.set_component(component_); + c.set_type(type); + c.set_protocol(protocol); + c.set_address(address); + c.set_priority(c.GetPriority(type_preference)); + c.set_username(username_fragment()); + c.set_password(password_); + c.set_network_name(network_->name()); + c.set_generation(generation_); + c.set_related_address(related_address_); + c.set_foundation(ComputeFoundation(type, protocol, base_address)); + candidates_.push_back(c); + SignalCandidateReady(this, c); + + if (final) { + SignalPortComplete(this); + } +} + +void Port::AddConnection(Connection* conn) { + connections_[conn->remote_candidate().address()] = conn; + conn->SignalDestroyed.connect(this, &Port::OnConnectionDestroyed); + SignalConnectionCreated(this, conn); +} + +void Port::OnReadPacket( + const char* data, size_t size, const talk_base::SocketAddress& addr, + ProtocolType proto) { + // If the user has enabled port packets, just hand this over. + if (enable_port_packets_) { + SignalReadPacket(this, data, size, addr); + return; + } + + // If this is an authenticated STUN request, then signal unknown address and + // send back a proper binding response. + talk_base::scoped_ptr msg; + std::string remote_username; + if (!GetStunMessage(data, size, addr, msg.accept(), &remote_username)) { + LOG_J(LS_ERROR, this) << "Received non-STUN packet from unknown address (" + << addr.ToSensitiveString() << ")"; + } else if (!msg) { + // STUN message handled already + } else if (msg->type() == STUN_BINDING_REQUEST) { + // Check for role conflicts. + if (IsStandardIce() && + !MaybeIceRoleConflict(addr, msg.get(), remote_username)) { + LOG(LS_INFO) << "Received conflicting role from the peer."; + return; + } + + SignalUnknownAddress(this, addr, proto, msg.get(), remote_username, false); + } else { + // NOTE(tschmelcher): STUN_BINDING_RESPONSE is benign. It occurs if we + // pruned a connection for this port while it had STUN requests in flight, + // because we then get back responses for them, which this code correctly + // does not handle. + if (msg->type() != STUN_BINDING_RESPONSE) { + LOG_J(LS_ERROR, this) << "Received unexpected STUN message type (" + << msg->type() << ") from unknown address (" + << addr.ToSensitiveString() << ")"; + } + } +} + +void Port::OnReadyToSend() { + AddressMap::iterator iter = connections_.begin(); + for (; iter != connections_.end(); ++iter) { + iter->second->OnReadyToSend(); + } +} + +size_t Port::AddPrflxCandidate(const Candidate& local) { + candidates_.push_back(local); + return (candidates_.size() - 1); +} + +bool Port::IsStandardIce() const { + return (ice_protocol_ == ICEPROTO_RFC5245); +} + +bool Port::IsGoogleIce() const { + return (ice_protocol_ == ICEPROTO_GOOGLE); +} + +bool Port::GetStunMessage(const char* data, size_t size, + const talk_base::SocketAddress& addr, + IceMessage** out_msg, std::string* out_username) { + // NOTE: This could clearly be optimized to avoid allocating any memory. + // However, at the data rates we'll be looking at on the client side, + // this probably isn't worth worrying about. + ASSERT(out_msg != NULL); + ASSERT(out_username != NULL); + *out_msg = NULL; + out_username->clear(); + + // Don't bother parsing the packet if we can tell it's not STUN. + // In ICE mode, all STUN packets will have a valid fingerprint. + if (IsStandardIce() && !StunMessage::ValidateFingerprint(data, size)) { + return false; + } + + // Parse the request message. If the packet is not a complete and correct + // STUN message, then ignore it. + talk_base::scoped_ptr stun_msg(new IceMessage()); + talk_base::ByteBuffer buf(data, size); + if (!stun_msg->Read(&buf) || (buf.Length() > 0)) { + return false; + } + + if (stun_msg->type() == STUN_BINDING_REQUEST) { + // Check for the presence of USERNAME and MESSAGE-INTEGRITY (if ICE) first. + // If not present, fail with a 400 Bad Request. + if (!stun_msg->GetByteString(STUN_ATTR_USERNAME) || + (IsStandardIce() && + !stun_msg->GetByteString(STUN_ATTR_MESSAGE_INTEGRITY))) { + LOG_J(LS_ERROR, this) << "Received STUN request without username/M-I " + << "from " << addr.ToSensitiveString(); + SendBindingErrorResponse(stun_msg.get(), addr, STUN_ERROR_BAD_REQUEST, + STUN_ERROR_REASON_BAD_REQUEST); + return true; + } + + // If the username is bad or unknown, fail with a 401 Unauthorized. + std::string local_ufrag; + std::string remote_ufrag; + if (!ParseStunUsername(stun_msg.get(), &local_ufrag, &remote_ufrag) || + local_ufrag != username_fragment()) { + LOG_J(LS_ERROR, this) << "Received STUN request with bad local username " + << local_ufrag << " from " + << addr.ToSensitiveString(); + SendBindingErrorResponse(stun_msg.get(), addr, STUN_ERROR_UNAUTHORIZED, + STUN_ERROR_REASON_UNAUTHORIZED); + return true; + } + + // If ICE, and the MESSAGE-INTEGRITY is bad, fail with a 401 Unauthorized + if (IsStandardIce() && + !stun_msg->ValidateMessageIntegrity(data, size, password_)) { + LOG_J(LS_ERROR, this) << "Received STUN request with bad M-I " + << "from " << addr.ToSensitiveString(); + SendBindingErrorResponse(stun_msg.get(), addr, STUN_ERROR_UNAUTHORIZED, + STUN_ERROR_REASON_UNAUTHORIZED); + return true; + } + out_username->assign(remote_ufrag); + } else if ((stun_msg->type() == STUN_BINDING_RESPONSE) || + (stun_msg->type() == STUN_BINDING_ERROR_RESPONSE)) { + if (stun_msg->type() == STUN_BINDING_ERROR_RESPONSE) { + if (const StunErrorCodeAttribute* error_code = stun_msg->GetErrorCode()) { + LOG_J(LS_ERROR, this) << "Received STUN binding error:" + << " class=" << error_code->eclass() + << " number=" << error_code->number() + << " reason='" << error_code->reason() << "'" + << " from " << addr.ToSensitiveString(); + // Return message to allow error-specific processing + } else { + LOG_J(LS_ERROR, this) << "Received STUN binding error without a error " + << "code from " << addr.ToSensitiveString(); + return true; + } + } + // NOTE: Username should not be used in verifying response messages. + out_username->clear(); + } else if (stun_msg->type() == STUN_BINDING_INDICATION) { + LOG_J(LS_VERBOSE, this) << "Received STUN binding indication:" + << " from " << addr.ToSensitiveString(); + out_username->clear(); + // No stun attributes will be verified, if it's stun indication message. + // Returning from end of the this method. + } else { + LOG_J(LS_ERROR, this) << "Received STUN packet with invalid type (" + << stun_msg->type() << ") from " + << addr.ToSensitiveString(); + return true; + } + + // Return the STUN message found. + *out_msg = stun_msg.release(); + return true; +} + +bool Port::IsCompatibleAddress(const talk_base::SocketAddress& addr) { + int family = ip().family(); + // We use single-stack sockets, so families must match. + if (addr.family() != family) { + return false; + } + // Link-local IPv6 ports can only connect to other link-local IPv6 ports. + if (family == AF_INET6 && (IPIsPrivate(ip()) != IPIsPrivate(addr.ipaddr()))) { + return false; + } + return true; +} + +bool Port::ParseStunUsername(const StunMessage* stun_msg, + std::string* local_ufrag, + std::string* remote_ufrag) const { + // The packet must include a username that either begins or ends with our + // fragment. It should begin with our fragment if it is a request and it + // should end with our fragment if it is a response. + local_ufrag->clear(); + remote_ufrag->clear(); + const StunByteStringAttribute* username_attr = + stun_msg->GetByteString(STUN_ATTR_USERNAME); + if (username_attr == NULL) + return false; + + const std::string username_attr_str = username_attr->GetString(); + if (IsStandardIce()) { + size_t colon_pos = username_attr_str.find(":"); + if (colon_pos != std::string::npos) { // RFRAG:LFRAG + *local_ufrag = username_attr_str.substr(0, colon_pos); + *remote_ufrag = username_attr_str.substr( + colon_pos + 1, username_attr_str.size()); + } else { + return false; + } + } else if (IsGoogleIce()) { + int remote_frag_len = username_attr_str.size(); + remote_frag_len -= static_cast(username_fragment().size()); + if (remote_frag_len < 0) + return false; + + *local_ufrag = username_attr_str.substr(0, username_fragment().size()); + *remote_ufrag = username_attr_str.substr( + username_fragment().size(), username_attr_str.size()); + } + return true; +} + +bool Port::MaybeIceRoleConflict( + const talk_base::SocketAddress& addr, IceMessage* stun_msg, + const std::string& remote_ufrag) { + // Validate ICE_CONTROLLING or ICE_CONTROLLED attributes. + bool ret = true; + TransportRole remote_ice_role = ROLE_UNKNOWN; + uint64 remote_tiebreaker = 0; + const StunUInt64Attribute* stun_attr = + stun_msg->GetUInt64(STUN_ATTR_ICE_CONTROLLING); + if (stun_attr) { + remote_ice_role = ROLE_CONTROLLING; + remote_tiebreaker = stun_attr->value(); + } + + // If |remote_ufrag| is same as port local username fragment and + // tie breaker value received in the ping message matches port + // tiebreaker value this must be a loopback call. + // We will treat this as valid scenario. + if (remote_ice_role == ROLE_CONTROLLING && + username_fragment() == remote_ufrag && + remote_tiebreaker == Tiebreaker()) { + return true; + } + + stun_attr = stun_msg->GetUInt64(STUN_ATTR_ICE_CONTROLLED); + if (stun_attr) { + remote_ice_role = ROLE_CONTROLLED; + remote_tiebreaker = stun_attr->value(); + } + + switch (role_) { + case ROLE_CONTROLLING: + if (ROLE_CONTROLLING == remote_ice_role) { + if (remote_tiebreaker >= tiebreaker_) { + SignalRoleConflict(this); + } else { + // Send Role Conflict (487) error response. + SendBindingErrorResponse(stun_msg, addr, + STUN_ERROR_ROLE_CONFLICT, STUN_ERROR_REASON_ROLE_CONFLICT); + ret = false; + } + } + break; + case ROLE_CONTROLLED: + if (ROLE_CONTROLLED == remote_ice_role) { + if (remote_tiebreaker < tiebreaker_) { + SignalRoleConflict(this); + } else { + // Send Role Conflict (487) error response. + SendBindingErrorResponse(stun_msg, addr, + STUN_ERROR_ROLE_CONFLICT, STUN_ERROR_REASON_ROLE_CONFLICT); + ret = false; + } + } + break; + default: + ASSERT(false); + } + return ret; +} + +void Port::CreateStunUsername(const std::string& remote_username, + std::string* stun_username_attr_str) const { + stun_username_attr_str->clear(); + *stun_username_attr_str = remote_username; + if (IsStandardIce()) { + // Connectivity checks from L->R will have username RFRAG:LFRAG. + stun_username_attr_str->append(":"); + } + stun_username_attr_str->append(username_fragment()); +} + +void Port::SendBindingResponse(StunMessage* request, + const talk_base::SocketAddress& addr) { + ASSERT(request->type() == STUN_BINDING_REQUEST); + + // Retrieve the username from the request. + const StunByteStringAttribute* username_attr = + request->GetByteString(STUN_ATTR_USERNAME); + ASSERT(username_attr != NULL); + if (username_attr == NULL) { + // No valid username, skip the response. + return; + } + + // Fill in the response message. + StunMessage response; + response.SetType(STUN_BINDING_RESPONSE); + response.SetTransactionID(request->transaction_id()); + const StunUInt32Attribute* retransmit_attr = + request->GetUInt32(STUN_ATTR_RETRANSMIT_COUNT); + if (retransmit_attr) { + // Inherit the incoming retransmit value in the response so the other side + // can see our view of lost pings. + response.AddAttribute(new StunUInt32Attribute( + STUN_ATTR_RETRANSMIT_COUNT, retransmit_attr->value())); + + if (retransmit_attr->value() > CONNECTION_WRITE_CONNECT_FAILURES) { + LOG_J(LS_INFO, this) + << "Received a remote ping with high retransmit count: " + << retransmit_attr->value(); + } + } + + // Only GICE messages have USERNAME and MAPPED-ADDRESS in the response. + // ICE messages use XOR-MAPPED-ADDRESS, and add MESSAGE-INTEGRITY. + if (IsStandardIce()) { + response.AddAttribute( + new StunXorAddressAttribute(STUN_ATTR_XOR_MAPPED_ADDRESS, addr)); + response.AddMessageIntegrity(password_); + response.AddFingerprint(); + } else if (IsGoogleIce()) { + response.AddAttribute( + new StunAddressAttribute(STUN_ATTR_MAPPED_ADDRESS, addr)); + response.AddAttribute(new StunByteStringAttribute( + STUN_ATTR_USERNAME, username_attr->GetString())); + } + + // Send the response message. + talk_base::ByteBuffer buf; + response.Write(&buf); + if (SendTo(buf.Data(), buf.Length(), addr, false) < 0) { + LOG_J(LS_ERROR, this) << "Failed to send STUN ping response to " + << addr.ToSensitiveString(); + } + + // The fact that we received a successful request means that this connection + // (if one exists) should now be readable. + Connection* conn = GetConnection(addr); + ASSERT(conn != NULL); + if (conn) + conn->ReceivedPing(); +} + +void Port::SendBindingErrorResponse(StunMessage* request, + const talk_base::SocketAddress& addr, + int error_code, const std::string& reason) { + ASSERT(request->type() == STUN_BINDING_REQUEST); + + // Fill in the response message. + StunMessage response; + response.SetType(STUN_BINDING_ERROR_RESPONSE); + response.SetTransactionID(request->transaction_id()); + + // When doing GICE, we need to write out the error code incorrectly to + // maintain backwards compatiblility. + StunErrorCodeAttribute* error_attr = StunAttribute::CreateErrorCode(); + if (IsStandardIce()) { + error_attr->SetCode(error_code); + } else if (IsGoogleIce()) { + error_attr->SetClass(error_code / 256); + error_attr->SetNumber(error_code % 256); + } + error_attr->SetReason(reason); + response.AddAttribute(error_attr); + + if (IsStandardIce()) { + // Per Section 10.1.2, certain error cases don't get a MESSAGE-INTEGRITY, + // because we don't have enough information to determine the shared secret. + if (error_code != STUN_ERROR_BAD_REQUEST && + error_code != STUN_ERROR_UNAUTHORIZED) + response.AddMessageIntegrity(password_); + response.AddFingerprint(); + } else if (IsGoogleIce()) { + // GICE responses include a username, if one exists. + const StunByteStringAttribute* username_attr = + request->GetByteString(STUN_ATTR_USERNAME); + if (username_attr) + response.AddAttribute(new StunByteStringAttribute( + STUN_ATTR_USERNAME, username_attr->GetString())); + } + + // Send the response message. + talk_base::ByteBuffer buf; + response.Write(&buf); + SendTo(buf.Data(), buf.Length(), addr, false); + LOG_J(LS_INFO, this) << "Sending STUN binding error: reason=" << reason + << " to " << addr.ToSensitiveString(); +} + +void Port::OnMessage(talk_base::Message *pmsg) { + ASSERT(pmsg->message_id == MSG_CHECKTIMEOUT); + ASSERT(lifetime_ == LT_PRETIMEOUT); + lifetime_ = LT_POSTTIMEOUT; + CheckTimeout(); +} + +std::string Port::ToString() const { + std::stringstream ss; + ss << "Port[" << content_name_ << ":" << component_ + << ":" << generation_ << ":" << type_ + << ":" << network_->ToString() << "]"; + return ss.str(); +} + +void Port::EnablePortPackets() { + enable_port_packets_ = true; +} + +void Port::Start() { + // The port sticks around for a minimum lifetime, after which + // we destroy it when it drops to zero connections. + if (lifetime_ == LT_PRESTART) { + lifetime_ = LT_PRETIMEOUT; + thread_->PostDelayed(kPortTimeoutDelay, this, MSG_CHECKTIMEOUT); + } else { + LOG_J(LS_WARNING, this) << "Port restart attempted"; + } +} + +void Port::OnConnectionDestroyed(Connection* conn) { + AddressMap::iterator iter = + connections_.find(conn->remote_candidate().address()); + ASSERT(iter != connections_.end()); + connections_.erase(iter); + + CheckTimeout(); +} + +void Port::Destroy() { + ASSERT(connections_.empty()); + LOG_J(LS_INFO, this) << "Port deleted"; + SignalDestroyed(this); + delete this; +} + +void Port::CheckTimeout() { + // If this port has no connections, then there's no reason to keep it around. + // When the connections time out (both read and write), they will delete + // themselves, so if we have any connections, they are either readable or + // writable (or still connecting). + if ((lifetime_ == LT_POSTTIMEOUT) && connections_.empty()) { + Destroy(); + } +} + +const std::string Port::username_fragment() const { + if (IsGoogleIce() && + component_ == ICE_CANDIDATE_COMPONENT_RTCP) { + // In GICE mode, we should adjust username fragment for rtcp component. + return GetRtcpUfragFromRtpUfrag(ice_username_fragment_); + } else { + return ice_username_fragment_; + } +} + +// A ConnectionRequest is a simple STUN ping used to determine writability. +class ConnectionRequest : public StunRequest { + public: + explicit ConnectionRequest(Connection* connection) + : StunRequest(new IceMessage()), + connection_(connection) { + } + + virtual ~ConnectionRequest() { + } + + virtual void Prepare(StunMessage* request) { + request->SetType(STUN_BINDING_REQUEST); + std::string username; + connection_->port()->CreateStunUsername( + connection_->remote_candidate().username(), &username); + request->AddAttribute( + new StunByteStringAttribute(STUN_ATTR_USERNAME, username)); + + // connection_ already holds this ping, so subtract one from count. + if (connection_->port()->send_retransmit_count_attribute()) { + request->AddAttribute(new StunUInt32Attribute(STUN_ATTR_RETRANSMIT_COUNT, + connection_->pings_since_last_response_.size() - 1)); + } + + // Adding ICE-specific attributes to the STUN request message. + if (connection_->port()->IsStandardIce()) { + // Adding ICE_CONTROLLED or ICE_CONTROLLING attribute based on the role. + if (connection_->port()->Role() == ROLE_CONTROLLING) { + request->AddAttribute(new StunUInt64Attribute( + STUN_ATTR_ICE_CONTROLLING, connection_->port()->Tiebreaker())); + // Since we are trying aggressive nomination, sending USE-CANDIDATE + // attribute in every ping. + // If we are dealing with a ice-lite end point, nomination flag + // in Connection will be set to false by default. Once the connection + // becomes "best connection", nomination flag will be turned on. + if (connection_->use_candidate_attr()) { + request->AddAttribute(new StunByteStringAttribute( + STUN_ATTR_USE_CANDIDATE)); + } + } else if (connection_->port()->Role() == ROLE_CONTROLLED) { + request->AddAttribute(new StunUInt64Attribute( + STUN_ATTR_ICE_CONTROLLED, connection_->port()->Tiebreaker())); + } else { + ASSERT(false); + } + + // Adding PRIORITY Attribute. + // Changing the type preference to Peer Reflexive and local preference + // and component id information is unchanged from the original priority. + // priority = (2^24)*(type preference) + + // (2^8)*(local preference) + + // (2^0)*(256 - component ID) + uint32 prflx_priority = ICE_TYPE_PREFERENCE_PRFLX << 24 | + (connection_->local_candidate().priority() & 0x00FFFFFF); + request->AddAttribute( + new StunUInt32Attribute(STUN_ATTR_PRIORITY, prflx_priority)); + + // Adding Message Integrity attribute. + request->AddMessageIntegrity(connection_->remote_candidate().password()); + // Adding Fingerprint. + request->AddFingerprint(); + } + } + + virtual void OnResponse(StunMessage* response) { + connection_->OnConnectionRequestResponse(this, response); + } + + virtual void OnErrorResponse(StunMessage* response) { + connection_->OnConnectionRequestErrorResponse(this, response); + } + + virtual void OnTimeout() { + connection_->OnConnectionRequestTimeout(this); + } + + virtual int GetNextDelay() { + // Each request is sent only once. After a single delay , the request will + // time out. + timeout_ = true; + return CONNECTION_RESPONSE_TIMEOUT; + } + + private: + Connection* connection_; +}; + +// +// Connection +// + +Connection::Connection(Port* port, size_t index, + const Candidate& remote_candidate) + : port_(port), local_candidate_index_(index), + remote_candidate_(remote_candidate), read_state_(STATE_READ_INIT), + write_state_(STATE_WRITE_INIT), connected_(true), pruned_(false), + use_candidate_attr_(false), remote_ice_mode_(ICEMODE_FULL), + requests_(port->thread()), rtt_(DEFAULT_RTT), last_ping_sent_(0), + last_ping_received_(0), last_data_received_(0), + last_ping_response_received_(0), reported_(false), state_(STATE_WAITING) { + // All of our connections start in WAITING state. + // TODO(mallinath) - Start connections from STATE_FROZEN. + // Wire up to send stun packets + requests_.SignalSendPacket.connect(this, &Connection::OnSendStunPacket); + LOG_J(LS_INFO, this) << "Connection created"; +} + +Connection::~Connection() { +} + +const Candidate& Connection::local_candidate() const { + ASSERT(local_candidate_index_ < port_->Candidates().size()); + return port_->Candidates()[local_candidate_index_]; +} + +uint64 Connection::priority() const { + uint64 priority = 0; + // RFC 5245 - 5.7.2. Computing Pair Priority and Ordering Pairs + // Let G be the priority for the candidate provided by the controlling + // agent. Let D be the priority for the candidate provided by the + // controlled agent. + // pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0) + TransportRole role = port_->Role(); + if (role != ROLE_UNKNOWN) { + uint32 g = 0; + uint32 d = 0; + if (role == ROLE_CONTROLLING) { + g = local_candidate().priority(); + d = remote_candidate_.priority(); + } else { + g = remote_candidate_.priority(); + d = local_candidate().priority(); + } + priority = talk_base::_min(g, d); + priority = priority << 32; + priority += 2 * talk_base::_max(g, d) + (g > d ? 1 : 0); + } + return priority; +} + +void Connection::set_read_state(ReadState value) { + ReadState old_value = read_state_; + read_state_ = value; + if (value != old_value) { + LOG_J(LS_VERBOSE, this) << "set_read_state"; + SignalStateChange(this); + CheckTimeout(); + } +} + +void Connection::set_write_state(WriteState value) { + WriteState old_value = write_state_; + write_state_ = value; + if (value != old_value) { + LOG_J(LS_VERBOSE, this) << "set_write_state"; + SignalStateChange(this); + CheckTimeout(); + } +} + +void Connection::set_state(State state) { + State old_state = state_; + state_ = state; + if (state != old_state) { + LOG_J(LS_VERBOSE, this) << "set_state"; + } +} + +void Connection::set_connected(bool value) { + bool old_value = connected_; + connected_ = value; + if (value != old_value) { + LOG_J(LS_VERBOSE, this) << "set_connected"; + } +} + +void Connection::set_use_candidate_attr(bool enable) { + use_candidate_attr_ = enable; +} + +void Connection::OnSendStunPacket(const void* data, size_t size, + StunRequest* req) { + if (port_->SendTo(data, size, remote_candidate_.address(), false) < 0) { + LOG_J(LS_WARNING, this) << "Failed to send STUN ping " << req->id(); + } +} + +void Connection::OnReadPacket(const char* data, size_t size) { + talk_base::scoped_ptr msg; + std::string remote_ufrag; + const talk_base::SocketAddress& addr(remote_candidate_.address()); + if (!port_->GetStunMessage(data, size, addr, msg.accept(), &remote_ufrag)) { + // The packet did not parse as a valid STUN message + + // If this connection is readable, then pass along the packet. + if (read_state_ == STATE_READABLE) { + // readable means data from this address is acceptable + // Send it on! + + last_data_received_ = talk_base::Time(); + recv_rate_tracker_.Update(size); + SignalReadPacket(this, data, size); + + // If timed out sending writability checks, start up again + if (!pruned_ && (write_state_ == STATE_WRITE_TIMEOUT)) { + LOG(LS_WARNING) << "Received a data packet on a timed-out Connection. " + << "Resetting state to STATE_WRITE_INIT."; + set_write_state(STATE_WRITE_INIT); + } + } else { + // Not readable means the remote address hasn't sent a valid + // binding request yet. + + LOG_J(LS_WARNING, this) + << "Received non-STUN packet from an unreadable connection."; + } + } else if (!msg) { + // The packet was STUN, but failed a check and was handled internally. + } else { + // The packet is STUN and passed the Port checks. + // Perform our own checks to ensure this packet is valid. + // If this is a STUN request, then update the readable bit and respond. + // If this is a STUN response, then update the writable bit. + switch (msg->type()) { + case STUN_BINDING_REQUEST: + if (remote_ufrag == remote_candidate_.username()) { + // Check for role conflicts. + if (port_->IsStandardIce() && + !port_->MaybeIceRoleConflict(addr, msg.get(), remote_ufrag)) { + // Received conflicting role from the peer. + LOG(LS_INFO) << "Received conflicting role from the peer."; + return; + } + + // Incoming, validated stun request from remote peer. + // This call will also set the connection readable. + port_->SendBindingResponse(msg.get(), addr); + + // If timed out sending writability checks, start up again + if (!pruned_ && (write_state_ == STATE_WRITE_TIMEOUT)) + set_write_state(STATE_WRITE_INIT); + + if ((port_->IsStandardIce()) && + (port_->Role() == ROLE_CONTROLLED)) { + const StunByteStringAttribute* use_candidate_attr = + msg->GetByteString(STUN_ATTR_USE_CANDIDATE); + if (use_candidate_attr) + SignalUseCandidate(this); + } + } else { + // The packet had the right local username, but the remote username + // was not the right one for the remote address. + LOG_J(LS_ERROR, this) + << "Received STUN request with bad remote username " + << remote_ufrag; + port_->SendBindingErrorResponse(msg.get(), addr, + STUN_ERROR_UNAUTHORIZED, + STUN_ERROR_REASON_UNAUTHORIZED); + + } + break; + + // Response from remote peer. Does it match request sent? + // This doesn't just check, it makes callbacks if transaction + // id's match. + case STUN_BINDING_RESPONSE: + case STUN_BINDING_ERROR_RESPONSE: + if (port_->IceProtocol() == ICEPROTO_GOOGLE || + msg->ValidateMessageIntegrity( + data, size, remote_candidate().password())) { + requests_.CheckResponse(msg.get()); + } + // Otherwise silently discard the response message. + break; + + // Remote end point sent an STUN indication instead of regular + // binding request. In this case |last_ping_received_| will be updated. + // Otherwise we can mark connection to read timeout. No response will be + // sent in this scenario. + case STUN_BINDING_INDICATION: + if (port_->IsStandardIce() && read_state_ == STATE_READABLE) { + ReceivedPing(); + } else { + LOG_J(LS_WARNING, this) << "Received STUN binding indication " + << "from an unreadable connection."; + } + break; + + default: + ASSERT(false); + break; + } + } +} + +void Connection::OnReadyToSend() { + if (write_state_ == STATE_WRITABLE) { + SignalReadyToSend(this); + } +} + +void Connection::Prune() { + if (!pruned_) { + LOG_J(LS_VERBOSE, this) << "Connection pruned"; + pruned_ = true; + requests_.Clear(); + set_write_state(STATE_WRITE_TIMEOUT); + } +} + +void Connection::Destroy() { + LOG_J(LS_VERBOSE, this) << "Connection destroyed"; + set_read_state(STATE_READ_TIMEOUT); + set_write_state(STATE_WRITE_TIMEOUT); +} + +void Connection::UpdateState(uint32 now) { + uint32 rtt = ConservativeRTTEstimate(rtt_); + + std::string pings; + for (size_t i = 0; i < pings_since_last_response_.size(); ++i) { + char buf[32]; + talk_base::sprintfn(buf, sizeof(buf), "%u", + pings_since_last_response_[i]); + pings.append(buf).append(" "); + } + LOG_J(LS_VERBOSE, this) << "UpdateState(): pings_since_last_response_=" << + pings << ", rtt=" << rtt << ", now=" << now; + + // Check the readable state. + // + // Since we don't know how many pings the other side has attempted, the best + // test we can do is a simple window. + // If other side has not sent ping after connection has become readable, use + // |last_data_received_| as the indication. + if ((read_state_ == STATE_READABLE) && + (last_ping_received_ + CONNECTION_READ_TIMEOUT <= now) && + (last_data_received_ + CONNECTION_READ_TIMEOUT <= now)) { + LOG_J(LS_INFO, this) << "Unreadable after " + << now - last_ping_received_ + << " ms without a ping," + << " ms since last received response=" + << now - last_ping_response_received_ + << " ms since last received data=" + << now - last_data_received_ + << " rtt=" << rtt; + set_read_state(STATE_READ_TIMEOUT); + } + + // Check the writable state. (The order of these checks is important.) + // + // Before becoming unwritable, we allow for a fixed number of pings to fail + // (i.e., receive no response). We also have to give the response time to + // get back, so we include a conservative estimate of this. + // + // Before timing out writability, we give a fixed amount of time. This is to + // allow for changes in network conditions. + + if ((write_state_ == STATE_WRITABLE) && + TooManyFailures(pings_since_last_response_, + CONNECTION_WRITE_CONNECT_FAILURES, + rtt, + now) && + TooLongWithoutResponse(pings_since_last_response_, + CONNECTION_WRITE_CONNECT_TIMEOUT, + now)) { + uint32 max_pings = CONNECTION_WRITE_CONNECT_FAILURES; + LOG_J(LS_INFO, this) << "Unwritable after " << max_pings + << " ping failures and " + << now - pings_since_last_response_[0] + << " ms without a response," + << " ms since last received ping=" + << now - last_ping_received_ + << " ms since last received data=" + << now - last_data_received_ + << " rtt=" << rtt; + set_write_state(STATE_WRITE_UNRELIABLE); + } + + if ((write_state_ == STATE_WRITE_UNRELIABLE || + write_state_ == STATE_WRITE_INIT) && + TooLongWithoutResponse(pings_since_last_response_, + CONNECTION_WRITE_TIMEOUT, + now)) { + LOG_J(LS_INFO, this) << "Timed out after " + << now - pings_since_last_response_[0] + << " ms without a response, rtt=" << rtt; + set_write_state(STATE_WRITE_TIMEOUT); + } +} + +void Connection::Ping(uint32 now) { + ASSERT(connected_); + last_ping_sent_ = now; + pings_since_last_response_.push_back(now); + ConnectionRequest *req = new ConnectionRequest(this); + LOG_J(LS_VERBOSE, this) << "Sending STUN ping " << req->id() << " at " << now; + requests_.Send(req); + state_ = STATE_INPROGRESS; +} + +void Connection::ReceivedPing() { + last_ping_received_ = talk_base::Time(); + set_read_state(STATE_READABLE); +} + +std::string Connection::ToString() const { + const char CONNECT_STATE_ABBREV[2] = { + '-', // not connected (false) + 'C', // connected (true) + }; + const char READ_STATE_ABBREV[3] = { + '-', // STATE_READ_INIT + 'R', // STATE_READABLE + 'x', // STATE_READ_TIMEOUT + }; + const char WRITE_STATE_ABBREV[4] = { + 'W', // STATE_WRITABLE + 'w', // STATE_WRITE_UNRELIABLE + '-', // STATE_WRITE_INIT + 'x', // STATE_WRITE_TIMEOUT + }; + const std::string ICESTATE[4] = { + "W", // STATE_WAITING + "I", // STATE_INPROGRESS + "S", // STATE_SUCCEEDED + "F" // STATE_FAILED + }; + const Candidate& local = local_candidate(); + const Candidate& remote = remote_candidate(); + std::stringstream ss; + ss << "Conn[" << port_->content_name() + << ":" << local.id() << ":" << local.component() + << ":" << local.generation() + << ":" << local.type() << ":" << local.protocol() + << ":" << local.address().ToSensitiveString() + << "->" << remote.id() << ":" << remote.component() + << ":" << remote.generation() + << ":" << remote.type() << ":" + << remote.protocol() << ":" << remote.address().ToSensitiveString() + << "|" + << CONNECT_STATE_ABBREV[connected()] + << READ_STATE_ABBREV[read_state()] + << WRITE_STATE_ABBREV[write_state()] + << ICESTATE[state()] + << "|"; + if (rtt_ < DEFAULT_RTT) { + ss << rtt_ << "]"; + } else { + ss << "-]"; + } + return ss.str(); +} + +std::string Connection::ToSensitiveString() const { + return ToString(); +} + +void Connection::OnConnectionRequestResponse(ConnectionRequest* request, + StunMessage* response) { + // We've already validated that this is a STUN binding response with + // the correct local and remote username for this connection. + // So if we're not already, become writable. We may be bringing a pruned + // connection back to life, but if we don't really want it, we can always + // prune it again. + uint32 rtt = request->Elapsed(); + set_write_state(STATE_WRITABLE); + set_state(STATE_SUCCEEDED); + + if (remote_ice_mode_ == ICEMODE_LITE) { + // A ice-lite end point never initiates ping requests. This will allow + // us to move to STATE_READABLE. + ReceivedPing(); + } + + std::string pings; + for (size_t i = 0; i < pings_since_last_response_.size(); ++i) { + char buf[32]; + talk_base::sprintfn(buf, sizeof(buf), "%u", + pings_since_last_response_[i]); + pings.append(buf).append(" "); + } + + talk_base::LoggingSeverity level = + (pings_since_last_response_.size() > CONNECTION_WRITE_CONNECT_FAILURES) ? + talk_base::LS_INFO : talk_base::LS_VERBOSE; + + LOG_JV(level, this) << "Received STUN ping response " << request->id() + << ", pings_since_last_response_=" << pings + << ", rtt=" << rtt; + + pings_since_last_response_.clear(); + last_ping_response_received_ = talk_base::Time(); + rtt_ = (RTT_RATIO * rtt_ + rtt) / (RTT_RATIO + 1); + + // Peer reflexive candidate is only for RFC 5245 ICE. + if (port_->IsStandardIce()) { + MaybeAddPrflxCandidate(request, response); + } +} + +void Connection::OnConnectionRequestErrorResponse(ConnectionRequest* request, + StunMessage* response) { + const StunErrorCodeAttribute* error_attr = response->GetErrorCode(); + int error_code = STUN_ERROR_GLOBAL_FAILURE; + if (error_attr) { + if (port_->IsGoogleIce()) { + // When doing GICE, the error code is written out incorrectly, so we need + // to unmunge it here. + error_code = error_attr->eclass() * 256 + error_attr->number(); + } else { + error_code = error_attr->code(); + } + } + + if (error_code == STUN_ERROR_UNKNOWN_ATTRIBUTE || + error_code == STUN_ERROR_SERVER_ERROR || + error_code == STUN_ERROR_UNAUTHORIZED) { + // Recoverable error, retry + } else if (error_code == STUN_ERROR_STALE_CREDENTIALS) { + // Race failure, retry + } else if (error_code == STUN_ERROR_ROLE_CONFLICT) { + HandleRoleConflictFromPeer(); + } else { + // This is not a valid connection. + LOG_J(LS_ERROR, this) << "Received STUN error response, code=" + << error_code << "; killing connection"; + set_state(STATE_FAILED); + set_write_state(STATE_WRITE_TIMEOUT); + } +} + +void Connection::OnConnectionRequestTimeout(ConnectionRequest* request) { + // Log at LS_INFO if we miss a ping on a writable connection. + talk_base::LoggingSeverity sev = (write_state_ == STATE_WRITABLE) ? + talk_base::LS_INFO : talk_base::LS_VERBOSE; + LOG_JV(sev, this) << "Timing-out STUN ping " << request->id() + << " after " << request->Elapsed() << " ms"; +} + +void Connection::CheckTimeout() { + // If both read and write have timed out or read has never initialized, then + // this connection can contribute no more to p2p socket unless at some later + // date readability were to come back. However, we gave readability a long + // time to timeout, so at this point, it seems fair to get rid of this + // connection. + if ((read_state_ == STATE_READ_TIMEOUT || + read_state_ == STATE_READ_INIT) && + write_state_ == STATE_WRITE_TIMEOUT) { + port_->thread()->Post(this, MSG_DELETE); + } +} + +void Connection::HandleRoleConflictFromPeer() { + port_->SignalRoleConflict(port_); +} + +void Connection::OnMessage(talk_base::Message *pmsg) { + ASSERT(pmsg->message_id == MSG_DELETE); + + LOG_J(LS_INFO, this) << "Connection deleted"; + SignalDestroyed(this); + delete this; +} + +size_t Connection::recv_bytes_second() { + return recv_rate_tracker_.units_second(); +} + +size_t Connection::recv_total_bytes() { + return recv_rate_tracker_.total_units(); +} + +size_t Connection::sent_bytes_second() { + return send_rate_tracker_.units_second(); +} + +size_t Connection::sent_total_bytes() { + return send_rate_tracker_.total_units(); +} + +void Connection::MaybeAddPrflxCandidate(ConnectionRequest* request, + StunMessage* response) { + // RFC 5245 + // The agent checks the mapped address from the STUN response. If the + // transport address does not match any of the local candidates that the + // agent knows about, the mapped address represents a new candidate -- a + // peer reflexive candidate. + const StunAddressAttribute* addr = + response->GetAddress(STUN_ATTR_XOR_MAPPED_ADDRESS); + if (!addr) { + LOG(LS_WARNING) << "Connection::OnConnectionRequestResponse - " + << "No MAPPED-ADDRESS or XOR-MAPPED-ADDRESS found in the " + << "stun response message"; + return; + } + + bool known_addr = false; + for (size_t i = 0; i < port_->Candidates().size(); ++i) { + if (port_->Candidates()[i].address() == addr->GetAddress()) { + known_addr = true; + break; + } + } + if (known_addr) { + return; + } + + // RFC 5245 + // Its priority is set equal to the value of the PRIORITY attribute + // in the Binding request. + const StunUInt32Attribute* priority_attr = + request->msg()->GetUInt32(STUN_ATTR_PRIORITY); + if (!priority_attr) { + LOG(LS_WARNING) << "Connection::OnConnectionRequestResponse - " + << "No STUN_ATTR_PRIORITY found in the " + << "stun response message"; + return; + } + const uint32 priority = priority_attr->value(); + std::string id = talk_base::CreateRandomString(8); + + Candidate new_local_candidate; + new_local_candidate.set_id(id); + new_local_candidate.set_component(local_candidate().component()); + new_local_candidate.set_type(PRFLX_PORT_TYPE); + new_local_candidate.set_protocol(local_candidate().protocol()); + new_local_candidate.set_address(addr->GetAddress()); + new_local_candidate.set_priority(priority); + new_local_candidate.set_username(local_candidate().username()); + new_local_candidate.set_password(local_candidate().password()); + new_local_candidate.set_network_name(local_candidate().network_name()); + new_local_candidate.set_related_address(local_candidate().address()); + new_local_candidate.set_foundation( + ComputeFoundation(PRFLX_PORT_TYPE, local_candidate().protocol(), + local_candidate().address())); + + // Change the local candidate of this Connection to the new prflx candidate. + local_candidate_index_ = port_->AddPrflxCandidate(new_local_candidate); + + // SignalStateChange to force a re-sort in P2PTransportChannel as this + // Connection's local candidate has changed. + SignalStateChange(this); +} + +ProxyConnection::ProxyConnection(Port* port, size_t index, + const Candidate& candidate) + : Connection(port, index, candidate), error_(0) { +} + +int ProxyConnection::Send(const void* data, size_t size) { + if (write_state_ == STATE_WRITE_INIT || write_state_ == STATE_WRITE_TIMEOUT) { + error_ = EWOULDBLOCK; + return SOCKET_ERROR; + } + int sent = port_->SendTo(data, size, remote_candidate_.address(), true); + if (sent <= 0) { + ASSERT(sent < 0); + error_ = port_->GetError(); + } else { + send_rate_tracker_.Update(sent); + } + return sent; +} + +} // namespace cricket diff --git a/talk/p2p/base/port.h b/talk/p2p/base/port.h new file mode 100644 index 000000000..5e795ece8 --- /dev/null +++ b/talk/p2p/base/port.h @@ -0,0 +1,585 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_PORT_H_ +#define TALK_P2P_BASE_PORT_H_ + +#include +#include +#include + +#include "talk/base/network.h" +#include "talk/base/proxyinfo.h" +#include "talk/base/ratetracker.h" +#include "talk/base/sigslot.h" +#include "talk/base/socketaddress.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/candidate.h" +#include "talk/p2p/base/packetsocketfactory.h" +#include "talk/p2p/base/portinterface.h" +#include "talk/p2p/base/stun.h" +#include "talk/p2p/base/stunrequest.h" +#include "talk/p2p/base/transport.h" + +namespace talk_base { +class AsyncPacketSocket; +} + +namespace cricket { + +class Connection; +class ConnectionRequest; + +extern const char LOCAL_PORT_TYPE[]; +extern const char STUN_PORT_TYPE[]; +extern const char PRFLX_PORT_TYPE[]; +extern const char RELAY_PORT_TYPE[]; + +extern const char UDP_PROTOCOL_NAME[]; +extern const char TCP_PROTOCOL_NAME[]; +extern const char SSLTCP_PROTOCOL_NAME[]; + +// The length of time we wait before timing out readability on a connection. +const uint32 CONNECTION_READ_TIMEOUT = 30 * 1000; // 30 seconds + +// The length of time we wait before timing out writability on a connection. +const uint32 CONNECTION_WRITE_TIMEOUT = 15 * 1000; // 15 seconds + +// The length of time we wait before we become unwritable. +const uint32 CONNECTION_WRITE_CONNECT_TIMEOUT = 5 * 1000; // 5 seconds + +// The number of pings that must fail to respond before we become unwritable. +const uint32 CONNECTION_WRITE_CONNECT_FAILURES = 5; + +// This is the length of time that we wait for a ping response to come back. +const int CONNECTION_RESPONSE_TIMEOUT = 5 * 1000; // 5 seconds + +enum RelayType { + RELAY_GTURN, // Legacy google relay service. + RELAY_TURN // Standard (TURN) relay service. +}; + +enum IcePriorityValue { + // The reason we are choosing Relay preference 2 is because, we can run + // Relay from client to server on UDP/TCP/TLS. To distinguish the transport + // protocol, we prefer UDP over TCP over TLS. + // For UDP ICE_TYPE_PREFERENCE_RELAY will be 2. + // For TCP ICE_TYPE_PREFERENCE_RELAY will be 1. + // For TLS ICE_TYPE_PREFERENCE_RELAY will be 0. + // Check turnport.cc for setting these values. + ICE_TYPE_PREFERENCE_RELAY = 2, + ICE_TYPE_PREFERENCE_HOST_TCP = 90, + ICE_TYPE_PREFERENCE_SRFLX = 100, + ICE_TYPE_PREFERENCE_PRFLX = 110, + ICE_TYPE_PREFERENCE_HOST = 126 +}; + +const char* ProtoToString(ProtocolType proto); +bool StringToProto(const char* value, ProtocolType* proto); + +struct ProtocolAddress { + talk_base::SocketAddress address; + ProtocolType proto; + + ProtocolAddress(const talk_base::SocketAddress& a, ProtocolType p) + : address(a), proto(p) { } +}; + +// Represents a local communication mechanism that can be used to create +// connections to similar mechanisms of the other client. Subclasses of this +// one add support for specific mechanisms like local UDP ports. +class Port : public PortInterface, public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + Port(talk_base::Thread* thread, talk_base::Network* network, + const talk_base::IPAddress& ip, + const std::string& username_fragment, const std::string& password); + Port(talk_base::Thread* thread, const std::string& type, + talk_base::PacketSocketFactory* factory, + talk_base::Network* network, const talk_base::IPAddress& ip, + int min_port, int max_port, const std::string& username_fragment, + const std::string& password); + virtual ~Port(); + + virtual const std::string& Type() const { return type_; } + virtual talk_base::Network* Network() const { return network_; } + + // This method will set the flag which enables standard ICE/STUN procedures + // in STUN connectivity checks. Currently this method does + // 1. Add / Verify MI attribute in STUN binding requests. + // 2. Username attribute in STUN binding request will be RFRAF:LFRAG, + // as opposed to RFRAGLFRAG. + virtual void SetIceProtocolType(IceProtocolType protocol) { + ice_protocol_ = protocol; + } + virtual IceProtocolType IceProtocol() const { return ice_protocol_; } + + // Methods to set/get ICE role and tiebreaker values. + void SetRole(TransportRole role) { role_ = role; } + TransportRole Role() const { return role_; } + + void SetTiebreaker(uint64 tiebreaker) { tiebreaker_ = tiebreaker; } + uint64 Tiebreaker() const { return tiebreaker_; } + + virtual bool SharedSocket() const { return shared_socket_; } + + // The thread on which this port performs its I/O. + talk_base::Thread* thread() { return thread_; } + + // The factory used to create the sockets of this port. + talk_base::PacketSocketFactory* socket_factory() const { return factory_; } + void set_socket_factory(talk_base::PacketSocketFactory* factory) { + factory_ = factory; + } + + // For debugging purposes. + const std::string& content_name() const { return content_name_; } + void set_content_name(const std::string& content_name) { + content_name_ = content_name; + } + + int component() const { return component_; } + void set_component(int component) { component_ = component; } + + bool send_retransmit_count_attribute() const { + return send_retransmit_count_attribute_; + } + void set_send_retransmit_count_attribute(bool enable) { + send_retransmit_count_attribute_ = enable; + } + + const talk_base::SocketAddress& related_address() const { + return related_address_; + } + void set_related_address(const talk_base::SocketAddress& address) { + related_address_ = address; + } + + // Identifies the generation that this port was created in. + uint32 generation() { return generation_; } + void set_generation(uint32 generation) { generation_ = generation; } + + // ICE requires a single username/password per content/media line. So the + // |ice_username_fragment_| of the ports that belongs to the same content will + // be the same. However this causes a small complication with our relay + // server, which expects different username for RTP and RTCP. + // + // To resolve this problem, we implemented the username_fragment(), + // which returns a different username (calculated from + // |ice_username_fragment_|) for RTCP in the case of ICEPROTO_GOOGLE. And the + // username_fragment() simply returns |ice_username_fragment_| when running + // in ICEPROTO_RFC5245. + // + // As a result the ICEPROTO_GOOGLE will use different usernames for RTP and + // RTCP. And the ICEPROTO_RFC5245 will use same username for both RTP and + // RTCP. + const std::string username_fragment() const; + const std::string& password() const { return password_; } + + // Fired when candidates are discovered by the port. When all candidates + // are discovered that belong to port SignalAddressReady is fired. + sigslot::signal2 SignalCandidateReady; + + // Provides all of the above information in one handy object. + virtual const std::vector& Candidates() const { + return candidates_; + } + + // SignalPortComplete is sent when port completes the task of candidates + // allocation. + sigslot::signal1 SignalPortComplete; + // This signal sent when port fails to allocate candidates and this port + // can't be used in establishing the connections. When port is in shared mode + // and port fails to allocate one of the candidates, port shouldn't send + // this signal as other candidates might be usefull in establishing the + // connection. + sigslot::signal1 SignalPortError; + + // Returns a map containing all of the connections of this port, keyed by the + // remote address. + typedef std::map AddressMap; + const AddressMap& connections() { return connections_; } + + // Returns the connection to the given address or NULL if none exists. + virtual Connection* GetConnection( + const talk_base::SocketAddress& remote_addr); + + // Called each time a connection is created. + sigslot::signal2 SignalConnectionCreated; + + // In a shared socket mode each port which shares the socket will decide + // to accept the packet based on the |remote_addr|. Currently only UDP + // port implemented this method. + // TODO(mallinath) - Make it pure virtual. + virtual bool HandleIncomingPacket( + talk_base::AsyncPacketSocket* socket, const char* data, size_t size, + const talk_base::SocketAddress& remote_addr) { + ASSERT(false); + return false; + } + + // Sends a response message (normal or error) to the given request. One of + // these methods should be called as a response to SignalUnknownAddress. + // NOTE: You MUST call CreateConnection BEFORE SendBindingResponse. + virtual void SendBindingResponse(StunMessage* request, + const talk_base::SocketAddress& addr); + virtual void SendBindingErrorResponse( + StunMessage* request, const talk_base::SocketAddress& addr, + int error_code, const std::string& reason); + + void set_proxy(const std::string& user_agent, + const talk_base::ProxyInfo& proxy) { + user_agent_ = user_agent; + proxy_ = proxy; + } + const std::string& user_agent() { return user_agent_; } + const talk_base::ProxyInfo& proxy() { return proxy_; } + + virtual void EnablePortPackets(); + + // Indicates to the port that its official use has now begun. This will + // start the timer that checks to see if the port is being used. + void Start(); + + // Called if the port has no connections and is no longer useful. + void Destroy(); + + virtual void OnMessage(talk_base::Message *pmsg); + + // Debugging description of this port + virtual std::string ToString() const; + talk_base::IPAddress& ip() { return ip_; } + int min_port() { return min_port_; } + int max_port() { return max_port_; } + + // This method will return local and remote username fragements from the + // stun username attribute if present. + bool ParseStunUsername(const StunMessage* stun_msg, + std::string* local_username, + std::string* remote_username) const; + void CreateStunUsername(const std::string& remote_username, + std::string* stun_username_attr_str) const; + + bool MaybeIceRoleConflict(const talk_base::SocketAddress& addr, + IceMessage* stun_msg, + const std::string& remote_ufrag); + + // Called when the socket is currently able to send. + void OnReadyToSend(); + + // Called when the Connection discovers a local peer reflexive candidate. + // Returns the index of the new local candidate. + size_t AddPrflxCandidate(const Candidate& local); + + // Returns if RFC 5245 ICE protocol is used. + bool IsStandardIce() const; + + // Returns if Google ICE protocol is used. + bool IsGoogleIce() const; + + protected: + void set_type(const std::string& type) { type_ = type; } + // Fills in the local address of the port. + void AddAddress(const talk_base::SocketAddress& address, + const talk_base::SocketAddress& base_address, + const std::string& protocol, const std::string& type, + uint32 type_preference, bool final); + + // Adds the given connection to the list. (Deleting removes them.) + void AddConnection(Connection* conn); + + // Called when a packet is received from an unknown address that is not + // currently a connection. If this is an authenticated STUN binding request, + // then we will signal the client. + void OnReadPacket(const char* data, size_t size, + const talk_base::SocketAddress& addr, + ProtocolType proto); + + // If the given data comprises a complete and correct STUN message then the + // return value is true, otherwise false. If the message username corresponds + // with this port's username fragment, msg will contain the parsed STUN + // message. Otherwise, the function may send a STUN response internally. + // remote_username contains the remote fragment of the STUN username. + bool GetStunMessage(const char* data, size_t size, + const talk_base::SocketAddress& addr, + IceMessage** out_msg, std::string* out_username); + + // Checks if the address in addr is compatible with the port's ip. + bool IsCompatibleAddress(const talk_base::SocketAddress& addr); + + private: + void Construct(); + // Called when one of our connections deletes itself. + void OnConnectionDestroyed(Connection* conn); + + // Checks if this port is useless, and hence, should be destroyed. + void CheckTimeout(); + + talk_base::Thread* thread_; + talk_base::PacketSocketFactory* factory_; + std::string type_; + bool send_retransmit_count_attribute_; + talk_base::Network* network_; + talk_base::IPAddress ip_; + int min_port_; + int max_port_; + std::string content_name_; + int component_; + uint32 generation_; + talk_base::SocketAddress related_address_; + // In order to establish a connection to this Port (so that real data can be + // sent through), the other side must send us a STUN binding request that is + // authenticated with this username_fragment and password. + // PortAllocatorSession will provide these username_fragment and password. + // + // Note: we should always use username_fragment() instead of using + // |ice_username_fragment_| directly. For the details see the comment on + // username_fragment(). + std::string ice_username_fragment_; + std::string password_; + std::vector candidates_; + AddressMap connections_; + enum Lifetime { LT_PRESTART, LT_PRETIMEOUT, LT_POSTTIMEOUT } lifetime_; + bool enable_port_packets_; + IceProtocolType ice_protocol_; + TransportRole role_; + uint64 tiebreaker_; + bool shared_socket_; + + // Information to use when going through a proxy. + std::string user_agent_; + talk_base::ProxyInfo proxy_; + + friend class Connection; +}; + +// Represents a communication link between a port on the local client and a +// port on the remote client. +class Connection : public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + // States are from RFC 5245. http://tools.ietf.org/html/rfc5245#section-5.7.4 + enum State { + STATE_WAITING = 0, // Check has not been performed, Waiting pair on CL. + STATE_INPROGRESS, // Check has been sent, transaction is in progress. + STATE_SUCCEEDED, // Check already done, produced a successful result. + STATE_FAILED // Check for this connection failed. + }; + + virtual ~Connection(); + + // The local port where this connection sends and receives packets. + Port* port() { return port_; } + const Port* port() const { return port_; } + + // Returns the description of the local port + virtual const Candidate& local_candidate() const; + + // Returns the description of the remote port to which we communicate. + const Candidate& remote_candidate() const { return remote_candidate_; } + + // Returns the pair priority. + uint64 priority() const; + + enum ReadState { + STATE_READ_INIT = 0, // we have yet to receive a ping + STATE_READABLE = 1, // we have received pings recently + STATE_READ_TIMEOUT = 2, // we haven't received pings in a while + }; + + ReadState read_state() const { return read_state_; } + bool readable() const { return read_state_ == STATE_READABLE; } + + enum WriteState { + STATE_WRITABLE = 0, // we have received ping responses recently + STATE_WRITE_UNRELIABLE = 1, // we have had a few ping failures + STATE_WRITE_INIT = 2, // we have yet to receive a ping response + STATE_WRITE_TIMEOUT = 3, // we have had a large number of ping failures + }; + + WriteState write_state() const { return write_state_; } + bool writable() const { return write_state_ == STATE_WRITABLE; } + + // Determines whether the connection has finished connecting. This can only + // be false for TCP connections. + bool connected() const { return connected_; } + + // Estimate of the round-trip time over this connection. + uint32 rtt() const { return rtt_; } + + size_t sent_total_bytes(); + size_t sent_bytes_second(); + size_t recv_total_bytes(); + size_t recv_bytes_second(); + sigslot::signal1 SignalStateChange; + + // Sent when the connection has decided that it is no longer of value. It + // will delete itself immediately after this call. + sigslot::signal1 SignalDestroyed; + + // The connection can send and receive packets asynchronously. This matches + // the interface of AsyncPacketSocket, which may use UDP or TCP under the + // covers. + virtual int Send(const void* data, size_t size) = 0; + + // Error if Send() returns < 0 + virtual int GetError() = 0; + + sigslot::signal3 SignalReadPacket; + + sigslot::signal1 SignalReadyToSend; + + // Called when a packet is received on this connection. + void OnReadPacket(const char* data, size_t size); + + // Called when the socket is currently able to send. + void OnReadyToSend(); + + // Called when a connection is determined to be no longer useful to us. We + // still keep it around in case the other side wants to use it. But we can + // safely stop pinging on it and we can allow it to time out if the other + // side stops using it as well. + bool pruned() const { return pruned_; } + void Prune(); + + bool use_candidate_attr() const { return use_candidate_attr_; } + void set_use_candidate_attr(bool enable); + + void set_remote_ice_mode(IceMode mode) { + remote_ice_mode_ = mode; + } + + // Makes the connection go away. + void Destroy(); + + // Checks that the state of this connection is up-to-date. The argument is + // the current time, which is compared against various timeouts. + void UpdateState(uint32 now); + + // Called when this connection should try checking writability again. + uint32 last_ping_sent() const { return last_ping_sent_; } + void Ping(uint32 now); + + // Called whenever a valid ping is received on this connection. This is + // public because the connection intercepts the first ping for us. + uint32 last_ping_received() const { return last_ping_received_; } + void ReceivedPing(); + + // Debugging description of this connection + std::string ToString() const; + std::string ToSensitiveString() const; + + bool reported() const { return reported_; } + void set_reported(bool reported) { reported_ = reported;} + + // This flag will be set if this connection is the chosen one for media + // transmission. This connection will send STUN ping with USE-CANDIDATE + // attribute. + sigslot::signal1 SignalUseCandidate; + // Invoked when Connection receives STUN error response with 487 code. + void HandleRoleConflictFromPeer(); + + State state() const { return state_; } + + IceMode remote_ice_mode() const { return remote_ice_mode_; } + + protected: + // Constructs a new connection to the given remote port. + Connection(Port* port, size_t index, const Candidate& candidate); + + // Called back when StunRequestManager has a stun packet to send + void OnSendStunPacket(const void* data, size_t size, StunRequest* req); + + // Callbacks from ConnectionRequest + void OnConnectionRequestResponse(ConnectionRequest* req, + StunMessage* response); + void OnConnectionRequestErrorResponse(ConnectionRequest* req, + StunMessage* response); + void OnConnectionRequestTimeout(ConnectionRequest* req); + + // Changes the state and signals if necessary. + void set_read_state(ReadState value); + void set_write_state(WriteState value); + void set_state(State state); + void set_connected(bool value); + + // Checks if this connection is useless, and hence, should be destroyed. + void CheckTimeout(); + + void OnMessage(talk_base::Message *pmsg); + + Port* port_; + size_t local_candidate_index_; + Candidate remote_candidate_; + ReadState read_state_; + WriteState write_state_; + bool connected_; + bool pruned_; + // By default |use_candidate_attr_| flag will be true, + // as we will be using agrressive nomination. + // But when peer is ice-lite, this flag "must" be initialized to false and + // turn on when connection becomes "best connection". + bool use_candidate_attr_; + IceMode remote_ice_mode_; + StunRequestManager requests_; + uint32 rtt_; + uint32 last_ping_sent_; // last time we sent a ping to the other side + uint32 last_ping_received_; // last time we received a ping from the other + // side + uint32 last_data_received_; + uint32 last_ping_response_received_; + std::vector pings_since_last_response_; + + talk_base::RateTracker recv_rate_tracker_; + talk_base::RateTracker send_rate_tracker_; + + private: + void MaybeAddPrflxCandidate(ConnectionRequest* request, + StunMessage* response); + + bool reported_; + State state_; + + friend class Port; + friend class ConnectionRequest; +}; + +// ProxyConnection defers all the interesting work to the port +class ProxyConnection : public Connection { + public: + ProxyConnection(Port* port, size_t index, const Candidate& candidate); + + virtual int Send(const void* data, size_t size); + virtual int GetError() { return error_; } + + private: + int error_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_PORT_H_ diff --git a/talk/p2p/base/port_unittest.cc b/talk/p2p/base/port_unittest.cc new file mode 100644 index 000000000..d6aa92d66 --- /dev/null +++ b/talk/p2p/base/port_unittest.cc @@ -0,0 +1,2261 @@ +/* + * libjingle + * Copyright 2004 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/base/crc32.h" +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/host.h" +#include "talk/base/logging.h" +#include "talk/base/natserver.h" +#include "talk/base/natsocketfactory.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/socketaddress.h" +#include "talk/base/stringutils.h" +#include "talk/base/thread.h" +#include "talk/base/virtualsocketserver.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/base/portproxy.h" +#include "talk/p2p/base/relayport.h" +#include "talk/p2p/base/stunport.h" +#include "talk/p2p/base/tcpport.h" +#include "talk/p2p/base/testrelayserver.h" +#include "talk/p2p/base/teststunserver.h" +#include "talk/p2p/base/testturnserver.h" +#include "talk/p2p/base/transport.h" +#include "talk/p2p/base/turnport.h" + +using talk_base::AsyncPacketSocket; +using talk_base::ByteBuffer; +using talk_base::NATType; +using talk_base::NAT_OPEN_CONE; +using talk_base::NAT_ADDR_RESTRICTED; +using talk_base::NAT_PORT_RESTRICTED; +using talk_base::NAT_SYMMETRIC; +using talk_base::PacketSocketFactory; +using talk_base::scoped_ptr; +using talk_base::Socket; +using talk_base::SocketAddress; +using namespace cricket; + +static const int kTimeout = 1000; +static const SocketAddress kLocalAddr1("192.168.1.2", 0); +static const SocketAddress kLocalAddr2("192.168.1.3", 0); +static const SocketAddress kNatAddr1("77.77.77.77", talk_base::NAT_SERVER_PORT); +static const SocketAddress kNatAddr2("88.88.88.88", talk_base::NAT_SERVER_PORT); +static const SocketAddress kStunAddr("99.99.99.1", STUN_SERVER_PORT); +static const SocketAddress kRelayUdpIntAddr("99.99.99.2", 5000); +static const SocketAddress kRelayUdpExtAddr("99.99.99.3", 5001); +static const SocketAddress kRelayTcpIntAddr("99.99.99.2", 5002); +static const SocketAddress kRelayTcpExtAddr("99.99.99.3", 5003); +static const SocketAddress kRelaySslTcpIntAddr("99.99.99.2", 5004); +static const SocketAddress kRelaySslTcpExtAddr("99.99.99.3", 5005); +static const SocketAddress kTurnUdpIntAddr("99.99.99.4", STUN_SERVER_PORT); +static const SocketAddress kTurnUdpExtAddr("99.99.99.5", 0); +static const RelayCredentials kRelayCredentials("test", "test"); + +// TODO: Update these when RFC5245 is completely supported. +// Magic value of 30 is from RFC3484, for IPv4 addresses. +static const uint32 kDefaultPrflxPriority = ICE_TYPE_PREFERENCE_PRFLX << 24 | + 30 << 8 | (256 - ICE_CANDIDATE_COMPONENT_DEFAULT); +static const int STUN_ERROR_BAD_REQUEST_AS_GICE = + STUN_ERROR_BAD_REQUEST / 256 * 100 + STUN_ERROR_BAD_REQUEST % 256; +static const int STUN_ERROR_UNAUTHORIZED_AS_GICE = + STUN_ERROR_UNAUTHORIZED / 256 * 100 + STUN_ERROR_UNAUTHORIZED % 256; +static const int STUN_ERROR_SERVER_ERROR_AS_GICE = + STUN_ERROR_SERVER_ERROR / 256 * 100 + STUN_ERROR_SERVER_ERROR % 256; + +static const int kTiebreaker1 = 11111; +static const int kTiebreaker2 = 22222; + +static Candidate GetCandidate(Port* port) { + assert(port->Candidates().size() == 1); + return port->Candidates()[0]; +} + +static SocketAddress GetAddress(Port* port) { + return GetCandidate(port).address(); +} + +static IceMessage* CopyStunMessage(const IceMessage* src) { + IceMessage* dst = new IceMessage(); + ByteBuffer buf; + src->Write(&buf); + dst->Read(&buf); + return dst; +} + +static bool WriteStunMessage(const StunMessage* msg, ByteBuffer* buf) { + buf->Resize(0); // clear out any existing buffer contents + return msg->Write(buf); +} + +// Stub port class for testing STUN generation and processing. +class TestPort : public Port { + public: + TestPort(talk_base::Thread* thread, const std::string& type, + talk_base::PacketSocketFactory* factory, talk_base::Network* network, + const talk_base::IPAddress& ip, int min_port, int max_port, + const std::string& username_fragment, const std::string& password) + : Port(thread, type, factory, network, ip, + min_port, max_port, username_fragment, password) { + } + ~TestPort() {} + + // Expose GetStunMessage so that we can test it. + using cricket::Port::GetStunMessage; + + // The last StunMessage that was sent on this Port. + // TODO: Make these const; requires changes to SendXXXXResponse. + ByteBuffer* last_stun_buf() { return last_stun_buf_.get(); } + IceMessage* last_stun_msg() { return last_stun_msg_.get(); } + int last_stun_error_code() { + int code = 0; + if (last_stun_msg_) { + const StunErrorCodeAttribute* error_attr = last_stun_msg_->GetErrorCode(); + if (error_attr) { + code = error_attr->code(); + } + } + return code; + } + + virtual void PrepareAddress() { + talk_base::SocketAddress addr(ip(), min_port()); + AddAddress(addr, addr, "udp", Type(), ICE_TYPE_PREFERENCE_HOST, true); + } + + // Exposed for testing candidate building. + void AddCandidateAddress(const talk_base::SocketAddress& addr) { + AddAddress(addr, addr, "udp", Type(), type_preference_, false); + } + void AddCandidateAddress(const talk_base::SocketAddress& addr, + const talk_base::SocketAddress& base_address, + const std::string& type, + int type_preference, + bool final) { + AddAddress(addr, base_address, "udp", type, + type_preference, final); + } + + virtual Connection* CreateConnection(const Candidate& remote_candidate, + CandidateOrigin origin) { + Connection* conn = new ProxyConnection(this, 0, remote_candidate); + AddConnection(conn); + // Set use-candidate attribute flag as this will add USE-CANDIDATE attribute + // in STUN binding requests. + conn->set_use_candidate_attr(true); + return conn; + } + virtual int SendTo( + const void* data, size_t size, const talk_base::SocketAddress& addr, + bool payload) { + if (!payload) { + IceMessage* msg = new IceMessage; + ByteBuffer* buf = new ByteBuffer(static_cast(data), size); + ByteBuffer::ReadPosition pos(buf->GetReadPosition()); + if (!msg->Read(buf)) { + delete msg; + delete buf; + return -1; + } + buf->SetReadPosition(pos); + last_stun_buf_.reset(buf); + last_stun_msg_.reset(msg); + } + return size; + } + virtual int SetOption(talk_base::Socket::Option opt, int value) { + return 0; + } + virtual int GetOption(talk_base::Socket::Option opt, int* value) { + return -1; + } + virtual int GetError() { + return 0; + } + void Reset() { + last_stun_buf_.reset(); + last_stun_msg_.reset(); + } + void set_type_preference(int type_preference) { + type_preference_ = type_preference; + } + + private: + talk_base::scoped_ptr last_stun_buf_; + talk_base::scoped_ptr last_stun_msg_; + int type_preference_; +}; + +class TestChannel : public sigslot::has_slots<> { + public: + TestChannel(Port* p1, Port* p2) + : ice_mode_(ICEMODE_FULL), src_(p1), dst_(p2), complete_count_(0), + conn_(NULL), remote_request_(NULL), nominated_(false) { + src_->SignalPortComplete.connect( + this, &TestChannel::OnPortComplete); + src_->SignalUnknownAddress.connect(this, &TestChannel::OnUnknownAddress); + } + + int complete_count() { return complete_count_; } + Connection* conn() { return conn_; } + const SocketAddress& remote_address() { return remote_address_; } + const std::string remote_fragment() { return remote_frag_; } + + void Start() { + src_->PrepareAddress(); + } + void CreateConnection() { + conn_ = src_->CreateConnection(GetCandidate(dst_), Port::ORIGIN_MESSAGE); + IceMode remote_ice_mode = + (ice_mode_ == ICEMODE_FULL) ? ICEMODE_LITE : ICEMODE_FULL; + conn_->set_remote_ice_mode(remote_ice_mode); + conn_->set_use_candidate_attr(remote_ice_mode == ICEMODE_FULL); + conn_->SignalStateChange.connect( + this, &TestChannel::OnConnectionStateChange); + } + void OnConnectionStateChange(Connection* conn) { + if (conn->write_state() == Connection::STATE_WRITABLE) { + conn->set_use_candidate_attr(true); + nominated_ = true; + } + } + void AcceptConnection() { + ASSERT_TRUE(remote_request_.get() != NULL); + Candidate c = GetCandidate(dst_); + c.set_address(remote_address_); + conn_ = src_->CreateConnection(c, Port::ORIGIN_MESSAGE); + src_->SendBindingResponse(remote_request_.get(), remote_address_); + remote_request_.reset(); + } + void Ping() { + Ping(0); + } + void Ping(uint32 now) { + conn_->Ping(now); + } + void Stop() { + conn_->SignalDestroyed.connect(this, &TestChannel::OnDestroyed); + conn_->Destroy(); + } + + void OnPortComplete(Port* port) { + complete_count_++; + } + void SetIceMode(IceMode ice_mode) { + ice_mode_ = ice_mode; + } + + void OnUnknownAddress(PortInterface* port, const SocketAddress& addr, + ProtocolType proto, + IceMessage* msg, const std::string& rf, + bool /*port_muxed*/) { + ASSERT_EQ(src_.get(), port); + if (!remote_address_.IsNil()) { + ASSERT_EQ(remote_address_, addr); + } + // MI and PRIORITY attribute should be present in ping requests when port + // is in ICEPROTO_RFC5245 mode. + const cricket::StunUInt32Attribute* priority_attr = + msg->GetUInt32(STUN_ATTR_PRIORITY); + const cricket::StunByteStringAttribute* mi_attr = + msg->GetByteString(STUN_ATTR_MESSAGE_INTEGRITY); + const cricket::StunUInt32Attribute* fingerprint_attr = + msg->GetUInt32(STUN_ATTR_FINGERPRINT); + if (src_->IceProtocol() == cricket::ICEPROTO_RFC5245) { + EXPECT_TRUE(priority_attr != NULL); + EXPECT_TRUE(mi_attr != NULL); + EXPECT_TRUE(fingerprint_attr != NULL); + } else { + EXPECT_TRUE(priority_attr == NULL); + EXPECT_TRUE(mi_attr == NULL); + EXPECT_TRUE(fingerprint_attr == NULL); + } + remote_address_ = addr; + remote_request_.reset(CopyStunMessage(msg)); + remote_frag_ = rf; + } + + void OnDestroyed(Connection* conn) { + ASSERT_EQ(conn_, conn); + conn_ = NULL; + } + + bool nominated() const { return nominated_; } + + private: + IceMode ice_mode_; + talk_base::scoped_ptr src_; + Port* dst_; + + int complete_count_; + Connection* conn_; + SocketAddress remote_address_; + talk_base::scoped_ptr remote_request_; + std::string remote_frag_; + bool nominated_; +}; + +class PortTest : public testing::Test, public sigslot::has_slots<> { + public: + PortTest() + : main_(talk_base::Thread::Current()), + pss_(new talk_base::PhysicalSocketServer), + ss_(new talk_base::VirtualSocketServer(pss_.get())), + ss_scope_(ss_.get()), + network_("unittest", "unittest", talk_base::IPAddress(INADDR_ANY), 32), + socket_factory_(talk_base::Thread::Current()), + nat_factory1_(ss_.get(), kNatAddr1), + nat_factory2_(ss_.get(), kNatAddr2), + nat_socket_factory1_(&nat_factory1_), + nat_socket_factory2_(&nat_factory2_), + stun_server_(main_, kStunAddr), + turn_server_(main_, kTurnUdpIntAddr, kTurnUdpExtAddr), + relay_server_(main_, kRelayUdpIntAddr, kRelayUdpExtAddr, + kRelayTcpIntAddr, kRelayTcpExtAddr, + kRelaySslTcpIntAddr, kRelaySslTcpExtAddr), + username_(talk_base::CreateRandomString(ICE_UFRAG_LENGTH)), + password_(talk_base::CreateRandomString(ICE_PWD_LENGTH)), + ice_protocol_(cricket::ICEPROTO_GOOGLE), + role_conflict_(false) { + network_.AddIP(talk_base::IPAddress(INADDR_ANY)); + } + + protected: + static void SetUpTestCase() { + // Ensure the RNG is inited. + talk_base::InitRandom(NULL, 0); + } + + void TestLocalToLocal() { + Port* port1 = CreateUdpPort(kLocalAddr1); + Port* port2 = CreateUdpPort(kLocalAddr2); + TestConnectivity("udp", port1, "udp", port2, true, true, true, true); + } + void TestLocalToStun(NATType ntype) { + Port* port1 = CreateUdpPort(kLocalAddr1); + nat_server2_.reset(CreateNatServer(kNatAddr2, ntype)); + Port* port2 = CreateStunPort(kLocalAddr2, &nat_socket_factory2_); + TestConnectivity("udp", port1, StunName(ntype), port2, + ntype == NAT_OPEN_CONE, true, + ntype != NAT_SYMMETRIC, true); + } + void TestLocalToRelay(RelayType rtype, ProtocolType proto) { + Port* port1 = CreateUdpPort(kLocalAddr1); + Port* port2 = CreateRelayPort(kLocalAddr2, rtype, proto, PROTO_UDP); + TestConnectivity("udp", port1, RelayName(rtype, proto), port2, + rtype == RELAY_GTURN, true, true, true); + } + void TestStunToLocal(NATType ntype) { + nat_server1_.reset(CreateNatServer(kNatAddr1, ntype)); + Port* port1 = CreateStunPort(kLocalAddr1, &nat_socket_factory1_); + Port* port2 = CreateUdpPort(kLocalAddr2); + TestConnectivity(StunName(ntype), port1, "udp", port2, + true, ntype != NAT_SYMMETRIC, true, true); + } + void TestStunToStun(NATType ntype1, NATType ntype2) { + nat_server1_.reset(CreateNatServer(kNatAddr1, ntype1)); + Port* port1 = CreateStunPort(kLocalAddr1, &nat_socket_factory1_); + nat_server2_.reset(CreateNatServer(kNatAddr2, ntype2)); + Port* port2 = CreateStunPort(kLocalAddr2, &nat_socket_factory2_); + TestConnectivity(StunName(ntype1), port1, StunName(ntype2), port2, + ntype2 == NAT_OPEN_CONE, + ntype1 != NAT_SYMMETRIC, ntype2 != NAT_SYMMETRIC, + ntype1 + ntype2 < (NAT_PORT_RESTRICTED + NAT_SYMMETRIC)); + } + void TestStunToRelay(NATType ntype, RelayType rtype, ProtocolType proto) { + nat_server1_.reset(CreateNatServer(kNatAddr1, ntype)); + Port* port1 = CreateStunPort(kLocalAddr1, &nat_socket_factory1_); + Port* port2 = CreateRelayPort(kLocalAddr2, rtype, proto, PROTO_UDP); + TestConnectivity(StunName(ntype), port1, RelayName(rtype, proto), port2, + rtype == RELAY_GTURN, ntype != NAT_SYMMETRIC, true, true); + } + void TestTcpToTcp() { + Port* port1 = CreateTcpPort(kLocalAddr1); + Port* port2 = CreateTcpPort(kLocalAddr2); + TestConnectivity("tcp", port1, "tcp", port2, true, false, true, true); + } + void TestTcpToRelay(RelayType rtype, ProtocolType proto) { + Port* port1 = CreateTcpPort(kLocalAddr1); + Port* port2 = CreateRelayPort(kLocalAddr2, rtype, proto, PROTO_TCP); + TestConnectivity("tcp", port1, RelayName(rtype, proto), port2, + rtype == RELAY_GTURN, false, true, true); + } + void TestSslTcpToRelay(RelayType rtype, ProtocolType proto) { + Port* port1 = CreateTcpPort(kLocalAddr1); + Port* port2 = CreateRelayPort(kLocalAddr2, rtype, proto, PROTO_SSLTCP); + TestConnectivity("ssltcp", port1, RelayName(rtype, proto), port2, + rtype == RELAY_GTURN, false, true, true); + } + + // helpers for above functions + UDPPort* CreateUdpPort(const SocketAddress& addr) { + return CreateUdpPort(addr, &socket_factory_); + } + UDPPort* CreateUdpPort(const SocketAddress& addr, + PacketSocketFactory* socket_factory) { + UDPPort* port = UDPPort::Create(main_, socket_factory, &network_, + addr.ipaddr(), 0, 0, username_, password_); + port->SetIceProtocolType(ice_protocol_); + return port; + } + TCPPort* CreateTcpPort(const SocketAddress& addr) { + TCPPort* port = CreateTcpPort(addr, &socket_factory_); + port->SetIceProtocolType(ice_protocol_); + return port; + } + TCPPort* CreateTcpPort(const SocketAddress& addr, + PacketSocketFactory* socket_factory) { + TCPPort* port = TCPPort::Create(main_, socket_factory, &network_, + addr.ipaddr(), 0, 0, username_, password_, + true); + port->SetIceProtocolType(ice_protocol_); + return port; + } + StunPort* CreateStunPort(const SocketAddress& addr, + talk_base::PacketSocketFactory* factory) { + StunPort* port = StunPort::Create(main_, factory, &network_, + addr.ipaddr(), 0, 0, + username_, password_, kStunAddr); + port->SetIceProtocolType(ice_protocol_); + return port; + } + Port* CreateRelayPort(const SocketAddress& addr, RelayType rtype, + ProtocolType int_proto, ProtocolType ext_proto) { + if (rtype == RELAY_TURN) { + return CreateTurnPort(addr, &socket_factory_, int_proto, ext_proto); + } else { + return CreateGturnPort(addr, int_proto, ext_proto); + } + } + TurnPort* CreateTurnPort(const SocketAddress& addr, + PacketSocketFactory* socket_factory, + ProtocolType int_proto, ProtocolType ext_proto) { + TurnPort* port = TurnPort::Create(main_, socket_factory, &network_, + addr.ipaddr(), 0, 0, + username_, password_, ProtocolAddress( + kTurnUdpIntAddr, PROTO_UDP), + kRelayCredentials); + port->SetIceProtocolType(ice_protocol_); + return port; + } + RelayPort* CreateGturnPort(const SocketAddress& addr, + ProtocolType int_proto, ProtocolType ext_proto) { + RelayPort* port = CreateGturnPort(addr); + SocketAddress addrs[] = + { kRelayUdpIntAddr, kRelayTcpIntAddr, kRelaySslTcpIntAddr }; + port->AddServerAddress(ProtocolAddress(addrs[int_proto], int_proto)); + return port; + } + RelayPort* CreateGturnPort(const SocketAddress& addr) { + RelayPort* port = RelayPort::Create(main_, &socket_factory_, &network_, + addr.ipaddr(), 0, 0, + username_, password_); + // TODO: Add an external address for ext_proto, so that the + // other side can connect to this port using a non-UDP protocol. + port->SetIceProtocolType(ice_protocol_); + return port; + } + talk_base::NATServer* CreateNatServer(const SocketAddress& addr, + talk_base::NATType type) { + return new talk_base::NATServer(type, ss_.get(), addr, ss_.get(), addr); + } + static const char* StunName(NATType type) { + switch (type) { + case NAT_OPEN_CONE: return "stun(open cone)"; + case NAT_ADDR_RESTRICTED: return "stun(addr restricted)"; + case NAT_PORT_RESTRICTED: return "stun(port restricted)"; + case NAT_SYMMETRIC: return "stun(symmetric)"; + default: return "stun(?)"; + } + } + static const char* RelayName(RelayType type, ProtocolType proto) { + if (type == RELAY_TURN) { + switch (proto) { + case PROTO_UDP: return "turn(udp)"; + case PROTO_TCP: return "turn(tcp)"; + case PROTO_SSLTCP: return "turn(ssltcp)"; + default: return "turn(?)"; + } + } else { + switch (proto) { + case PROTO_UDP: return "gturn(udp)"; + case PROTO_TCP: return "gturn(tcp)"; + case PROTO_SSLTCP: return "gturn(ssltcp)"; + default: return "gturn(?)"; + } + } + } + + void TestCrossFamilyPorts(int type); + + // this does all the work + void TestConnectivity(const char* name1, Port* port1, + const char* name2, Port* port2, + bool accept, bool same_addr1, + bool same_addr2, bool possible); + + void SetIceProtocolType(cricket::IceProtocolType protocol) { + ice_protocol_ = protocol; + } + + IceMessage* CreateStunMessage(int type) { + IceMessage* msg = new IceMessage(); + msg->SetType(type); + msg->SetTransactionID("TESTTESTTEST"); + return msg; + } + IceMessage* CreateStunMessageWithUsername(int type, + const std::string& username) { + IceMessage* msg = CreateStunMessage(type); + msg->AddAttribute( + new StunByteStringAttribute(STUN_ATTR_USERNAME, username)); + return msg; + } + TestPort* CreateTestPort(const talk_base::SocketAddress& addr, + const std::string& username, + const std::string& password) { + TestPort* port = new TestPort(main_, "test", &socket_factory_, &network_, + addr.ipaddr(), 0, 0, username, password); + port->SignalRoleConflict.connect(this, &PortTest::OnRoleConflict); + return port; + } + TestPort* CreateTestPort(const talk_base::SocketAddress& addr, + const std::string& username, + const std::string& password, + cricket::IceProtocolType type, + cricket::TransportRole role, + int tiebreaker) { + TestPort* port = CreateTestPort(addr, username, password); + port->SetIceProtocolType(type); + port->SetRole(role); + port->SetTiebreaker(tiebreaker); + return port; + } + + void OnRoleConflict(PortInterface* port) { + role_conflict_ = true; + } + bool role_conflict() const { return role_conflict_; } + + talk_base::BasicPacketSocketFactory* nat_socket_factory1() { + return &nat_socket_factory1_; + } + + private: + talk_base::Thread* main_; + talk_base::scoped_ptr pss_; + talk_base::scoped_ptr ss_; + talk_base::SocketServerScope ss_scope_; + talk_base::Network network_; + talk_base::BasicPacketSocketFactory socket_factory_; + talk_base::scoped_ptr nat_server1_; + talk_base::scoped_ptr nat_server2_; + talk_base::NATSocketFactory nat_factory1_; + talk_base::NATSocketFactory nat_factory2_; + talk_base::BasicPacketSocketFactory nat_socket_factory1_; + talk_base::BasicPacketSocketFactory nat_socket_factory2_; + TestStunServer stun_server_; + TestTurnServer turn_server_; + TestRelayServer relay_server_; + std::string username_; + std::string password_; + cricket::IceProtocolType ice_protocol_; + bool role_conflict_; +}; + +void PortTest::TestConnectivity(const char* name1, Port* port1, + const char* name2, Port* port2, + bool accept, bool same_addr1, + bool same_addr2, bool possible) { + LOG(LS_INFO) << "Test: " << name1 << " to " << name2 << ": "; + port1->set_component(cricket::ICE_CANDIDATE_COMPONENT_DEFAULT); + port2->set_component(cricket::ICE_CANDIDATE_COMPONENT_DEFAULT); + + // Set up channels. + TestChannel ch1(port1, port2); + TestChannel ch2(port2, port1); + EXPECT_EQ(0, ch1.complete_count()); + EXPECT_EQ(0, ch2.complete_count()); + + // Acquire addresses. + ch1.Start(); + ch2.Start(); + ASSERT_EQ_WAIT(1, ch1.complete_count(), kTimeout); + ASSERT_EQ_WAIT(1, ch2.complete_count(), kTimeout); + + // Send a ping from src to dst. This may or may not make it. + ch1.CreateConnection(); + ASSERT_TRUE(ch1.conn() != NULL); + EXPECT_TRUE_WAIT(ch1.conn()->connected(), kTimeout); // for TCP connect + ch1.Ping(); + WAIT(!ch2.remote_address().IsNil(), kTimeout); + + if (accept) { + // We are able to send a ping from src to dst. This is the case when + // sending to UDP ports and cone NATs. + EXPECT_TRUE(ch1.remote_address().IsNil()); + EXPECT_EQ(ch2.remote_fragment(), port1->username_fragment()); + + // Ensure the ping came from the same address used for src. + // This is the case unless the source NAT was symmetric. + if (same_addr1) EXPECT_EQ(ch2.remote_address(), GetAddress(port1)); + EXPECT_TRUE(same_addr2); + + // Send a ping from dst to src. + ch2.AcceptConnection(); + ASSERT_TRUE(ch2.conn() != NULL); + ch2.Ping(); + EXPECT_EQ_WAIT(Connection::STATE_WRITABLE, ch2.conn()->write_state(), + kTimeout); + } else { + // We can't send a ping from src to dst, so flip it around. This will happen + // when the destination NAT is addr/port restricted or symmetric. + EXPECT_TRUE(ch1.remote_address().IsNil()); + EXPECT_TRUE(ch2.remote_address().IsNil()); + + // Send a ping from dst to src. Again, this may or may not make it. + ch2.CreateConnection(); + ASSERT_TRUE(ch2.conn() != NULL); + ch2.Ping(); + WAIT(ch2.conn()->write_state() == Connection::STATE_WRITABLE, kTimeout); + + if (same_addr1 && same_addr2) { + // The new ping got back to the source. + EXPECT_EQ(Connection::STATE_READABLE, ch1.conn()->read_state()); + EXPECT_EQ(Connection::STATE_WRITABLE, ch2.conn()->write_state()); + + // First connection may not be writable if the first ping did not get + // through. So we will have to do another. + if (ch1.conn()->write_state() == Connection::STATE_WRITE_INIT) { + ch1.Ping(); + EXPECT_EQ_WAIT(Connection::STATE_WRITABLE, ch1.conn()->write_state(), + kTimeout); + } + } else if (!same_addr1 && possible) { + // The new ping went to the candidate address, but that address was bad. + // This will happen when the source NAT is symmetric. + EXPECT_TRUE(ch1.remote_address().IsNil()); + EXPECT_TRUE(ch2.remote_address().IsNil()); + + // However, since we have now sent a ping to the source IP, we should be + // able to get a ping from it. This gives us the real source address. + ch1.Ping(); + EXPECT_TRUE_WAIT(!ch2.remote_address().IsNil(), kTimeout); + EXPECT_EQ(Connection::STATE_READ_INIT, ch2.conn()->read_state()); + EXPECT_TRUE(ch1.remote_address().IsNil()); + + // Pick up the actual address and establish the connection. + ch2.AcceptConnection(); + ASSERT_TRUE(ch2.conn() != NULL); + ch2.Ping(); + EXPECT_EQ_WAIT(Connection::STATE_WRITABLE, ch2.conn()->write_state(), + kTimeout); + } else if (!same_addr2 && possible) { + // The new ping came in, but from an unexpected address. This will happen + // when the destination NAT is symmetric. + EXPECT_FALSE(ch1.remote_address().IsNil()); + EXPECT_EQ(Connection::STATE_READ_INIT, ch1.conn()->read_state()); + + // Update our address and complete the connection. + ch1.AcceptConnection(); + ch1.Ping(); + EXPECT_EQ_WAIT(Connection::STATE_WRITABLE, ch1.conn()->write_state(), + kTimeout); + } else { // (!possible) + // There should be s no way for the pings to reach each other. Check it. + EXPECT_TRUE(ch1.remote_address().IsNil()); + EXPECT_TRUE(ch2.remote_address().IsNil()); + ch1.Ping(); + WAIT(!ch2.remote_address().IsNil(), kTimeout); + EXPECT_TRUE(ch1.remote_address().IsNil()); + EXPECT_TRUE(ch2.remote_address().IsNil()); + } + } + + // Everything should be good, unless we know the situation is impossible. + ASSERT_TRUE(ch1.conn() != NULL); + ASSERT_TRUE(ch2.conn() != NULL); + if (possible) { + EXPECT_EQ(Connection::STATE_READABLE, ch1.conn()->read_state()); + EXPECT_EQ(Connection::STATE_WRITABLE, ch1.conn()->write_state()); + EXPECT_EQ(Connection::STATE_READABLE, ch2.conn()->read_state()); + EXPECT_EQ(Connection::STATE_WRITABLE, ch2.conn()->write_state()); + } else { + EXPECT_NE(Connection::STATE_READABLE, ch1.conn()->read_state()); + EXPECT_NE(Connection::STATE_WRITABLE, ch1.conn()->write_state()); + EXPECT_NE(Connection::STATE_READABLE, ch2.conn()->read_state()); + EXPECT_NE(Connection::STATE_WRITABLE, ch2.conn()->write_state()); + } + + // Tear down and ensure that goes smoothly. + ch1.Stop(); + ch2.Stop(); + EXPECT_TRUE_WAIT(ch1.conn() == NULL, kTimeout); + EXPECT_TRUE_WAIT(ch2.conn() == NULL, kTimeout); +} + +class FakePacketSocketFactory : public talk_base::PacketSocketFactory { + public: + FakePacketSocketFactory() + : next_udp_socket_(NULL), + next_server_tcp_socket_(NULL), + next_client_tcp_socket_(NULL) { + } + virtual ~FakePacketSocketFactory() { } + + virtual AsyncPacketSocket* CreateUdpSocket( + const SocketAddress& address, int min_port, int max_port) { + EXPECT_TRUE(next_udp_socket_ != NULL); + AsyncPacketSocket* result = next_udp_socket_; + next_udp_socket_ = NULL; + return result; + } + + virtual AsyncPacketSocket* CreateServerTcpSocket( + const SocketAddress& local_address, int min_port, int max_port, + int opts) { + EXPECT_TRUE(next_server_tcp_socket_ != NULL); + AsyncPacketSocket* result = next_server_tcp_socket_; + next_server_tcp_socket_ = NULL; + return result; + } + + // TODO: |proxy_info| and |user_agent| should be set + // per-factory and not when socket is created. + virtual AsyncPacketSocket* CreateClientTcpSocket( + const SocketAddress& local_address, const SocketAddress& remote_address, + const talk_base::ProxyInfo& proxy_info, + const std::string& user_agent, int opts) { + EXPECT_TRUE(next_client_tcp_socket_ != NULL); + AsyncPacketSocket* result = next_client_tcp_socket_; + next_client_tcp_socket_ = NULL; + return result; + } + + void set_next_udp_socket(AsyncPacketSocket* next_udp_socket) { + next_udp_socket_ = next_udp_socket; + } + void set_next_server_tcp_socket(AsyncPacketSocket* next_server_tcp_socket) { + next_server_tcp_socket_ = next_server_tcp_socket; + } + void set_next_client_tcp_socket(AsyncPacketSocket* next_client_tcp_socket) { + next_client_tcp_socket_ = next_client_tcp_socket; + } + + private: + AsyncPacketSocket* next_udp_socket_; + AsyncPacketSocket* next_server_tcp_socket_; + AsyncPacketSocket* next_client_tcp_socket_; +}; + +class FakeAsyncPacketSocket : public AsyncPacketSocket { + public: + // Returns current local address. Address may be set to NULL if the + // socket is not bound yet (GetState() returns STATE_BINDING). + virtual SocketAddress GetLocalAddress() const { + return SocketAddress(); + } + + // Returns remote address. Returns zeroes if this is not a client TCP socket. + virtual SocketAddress GetRemoteAddress() const { + return SocketAddress(); + } + + // Send a packet. + virtual int Send(const void *pv, size_t cb) { + return cb; + } + virtual int SendTo(const void *pv, size_t cb, const SocketAddress& addr) { + return cb; + } + virtual int Close() { + return 0; + } + + virtual State GetState() const { return state_; } + virtual int GetOption(Socket::Option opt, int* value) { return 0; } + virtual int SetOption(Socket::Option opt, int value) { return 0; } + virtual int GetError() const { return 0; } + virtual void SetError(int error) { } + + void set_state(State state) { state_ = state; } + + private: + State state_; +}; + +// Local -> XXXX +TEST_F(PortTest, TestLocalToLocal) { + TestLocalToLocal(); +} + +TEST_F(PortTest, TestLocalToConeNat) { + TestLocalToStun(NAT_OPEN_CONE); +} + +TEST_F(PortTest, TestLocalToARNat) { + TestLocalToStun(NAT_ADDR_RESTRICTED); +} + +TEST_F(PortTest, TestLocalToPRNat) { + TestLocalToStun(NAT_PORT_RESTRICTED); +} + +TEST_F(PortTest, TestLocalToSymNat) { + TestLocalToStun(NAT_SYMMETRIC); +} + +TEST_F(PortTest, TestLocalToTurn) { + TestLocalToRelay(RELAY_TURN, PROTO_UDP); +} + +TEST_F(PortTest, TestLocalToGturn) { + TestLocalToRelay(RELAY_GTURN, PROTO_UDP); +} + +TEST_F(PortTest, TestLocalToTcpGturn) { + TestLocalToRelay(RELAY_GTURN, PROTO_TCP); +} + +TEST_F(PortTest, TestLocalToSslTcpGturn) { + TestLocalToRelay(RELAY_GTURN, PROTO_SSLTCP); +} + +// Cone NAT -> XXXX +TEST_F(PortTest, TestConeNatToLocal) { + TestStunToLocal(NAT_OPEN_CONE); +} + +TEST_F(PortTest, TestConeNatToConeNat) { + TestStunToStun(NAT_OPEN_CONE, NAT_OPEN_CONE); +} + +TEST_F(PortTest, TestConeNatToARNat) { + TestStunToStun(NAT_OPEN_CONE, NAT_ADDR_RESTRICTED); +} + +TEST_F(PortTest, TestConeNatToPRNat) { + TestStunToStun(NAT_OPEN_CONE, NAT_PORT_RESTRICTED); +} + +TEST_F(PortTest, TestConeNatToSymNat) { + TestStunToStun(NAT_OPEN_CONE, NAT_SYMMETRIC); +} + +TEST_F(PortTest, TestConeNatToTurn) { + TestStunToRelay(NAT_OPEN_CONE, RELAY_TURN, PROTO_UDP); +} + +TEST_F(PortTest, TestConeNatToGturn) { + TestStunToRelay(NAT_OPEN_CONE, RELAY_GTURN, PROTO_UDP); +} + +TEST_F(PortTest, TestConeNatToTcpGturn) { + TestStunToRelay(NAT_OPEN_CONE, RELAY_GTURN, PROTO_TCP); +} + +// Address-restricted NAT -> XXXX +TEST_F(PortTest, TestARNatToLocal) { + TestStunToLocal(NAT_ADDR_RESTRICTED); +} + +TEST_F(PortTest, TestARNatToConeNat) { + TestStunToStun(NAT_ADDR_RESTRICTED, NAT_OPEN_CONE); +} + +TEST_F(PortTest, TestARNatToARNat) { + TestStunToStun(NAT_ADDR_RESTRICTED, NAT_ADDR_RESTRICTED); +} + +TEST_F(PortTest, TestARNatToPRNat) { + TestStunToStun(NAT_ADDR_RESTRICTED, NAT_PORT_RESTRICTED); +} + +TEST_F(PortTest, TestARNatToSymNat) { + TestStunToStun(NAT_ADDR_RESTRICTED, NAT_SYMMETRIC); +} + +TEST_F(PortTest, TestARNatToTurn) { + TestStunToRelay(NAT_ADDR_RESTRICTED, RELAY_TURN, PROTO_UDP); +} + +TEST_F(PortTest, TestARNatToGturn) { + TestStunToRelay(NAT_ADDR_RESTRICTED, RELAY_GTURN, PROTO_UDP); +} + +TEST_F(PortTest, TestARNATNatToTcpGturn) { + TestStunToRelay(NAT_ADDR_RESTRICTED, RELAY_GTURN, PROTO_TCP); +} + +// Port-restricted NAT -> XXXX +TEST_F(PortTest, TestPRNatToLocal) { + TestStunToLocal(NAT_PORT_RESTRICTED); +} + +TEST_F(PortTest, TestPRNatToConeNat) { + TestStunToStun(NAT_PORT_RESTRICTED, NAT_OPEN_CONE); +} + +TEST_F(PortTest, TestPRNatToARNat) { + TestStunToStun(NAT_PORT_RESTRICTED, NAT_ADDR_RESTRICTED); +} + +TEST_F(PortTest, TestPRNatToPRNat) { + TestStunToStun(NAT_PORT_RESTRICTED, NAT_PORT_RESTRICTED); +} + +TEST_F(PortTest, TestPRNatToSymNat) { + // Will "fail" + TestStunToStun(NAT_PORT_RESTRICTED, NAT_SYMMETRIC); +} + +TEST_F(PortTest, TestPRNatToTurn) { + TestStunToRelay(NAT_PORT_RESTRICTED, RELAY_TURN, PROTO_UDP); +} + +TEST_F(PortTest, TestPRNatToGturn) { + TestStunToRelay(NAT_PORT_RESTRICTED, RELAY_GTURN, PROTO_UDP); +} + +TEST_F(PortTest, TestPRNatToTcpGturn) { + TestStunToRelay(NAT_PORT_RESTRICTED, RELAY_GTURN, PROTO_TCP); +} + +// Symmetric NAT -> XXXX +TEST_F(PortTest, TestSymNatToLocal) { + TestStunToLocal(NAT_SYMMETRIC); +} + +TEST_F(PortTest, TestSymNatToConeNat) { + TestStunToStun(NAT_SYMMETRIC, NAT_OPEN_CONE); +} + +TEST_F(PortTest, TestSymNatToARNat) { + TestStunToStun(NAT_SYMMETRIC, NAT_ADDR_RESTRICTED); +} + +TEST_F(PortTest, TestSymNatToPRNat) { + // Will "fail" + TestStunToStun(NAT_SYMMETRIC, NAT_PORT_RESTRICTED); +} + +TEST_F(PortTest, TestSymNatToSymNat) { + // Will "fail" + TestStunToStun(NAT_SYMMETRIC, NAT_SYMMETRIC); +} + +TEST_F(PortTest, TestSymNatToTurn) { + TestStunToRelay(NAT_SYMMETRIC, RELAY_TURN, PROTO_UDP); +} + +TEST_F(PortTest, TestSymNatToGturn) { + TestStunToRelay(NAT_SYMMETRIC, RELAY_GTURN, PROTO_UDP); +} + +TEST_F(PortTest, TestSymNatToTcpGturn) { + TestStunToRelay(NAT_SYMMETRIC, RELAY_GTURN, PROTO_TCP); +} + +// Outbound TCP -> XXXX +TEST_F(PortTest, TestTcpToTcp) { + TestTcpToTcp(); +} + +/* TODO: Enable these once testrelayserver can accept external TCP. +TEST_F(PortTest, TestTcpToTcpRelay) { + TestTcpToRelay(PROTO_TCP); +} + +TEST_F(PortTest, TestTcpToSslTcpRelay) { + TestTcpToRelay(PROTO_SSLTCP); +} +*/ + +// Outbound SSLTCP -> XXXX +/* TODO: Enable these once testrelayserver can accept external SSL. +TEST_F(PortTest, TestSslTcpToTcpRelay) { + TestSslTcpToRelay(PROTO_TCP); +} + +TEST_F(PortTest, TestSslTcpToSslTcpRelay) { + TestSslTcpToRelay(PROTO_SSLTCP); +} +*/ + +// This test case verifies standard ICE features in STUN messages. Currently it +// verifies Message Integrity attribute in STUN messages and username in STUN +// binding request will have colon (":") between remote and local username. +TEST_F(PortTest, TestLocalToLocalAsIce) { + SetIceProtocolType(cricket::ICEPROTO_RFC5245); + UDPPort* port1 = CreateUdpPort(kLocalAddr1); + port1->SetRole(cricket::ROLE_CONTROLLING); + port1->SetTiebreaker(kTiebreaker1); + ASSERT_EQ(cricket::ICEPROTO_RFC5245, port1->IceProtocol()); + UDPPort* port2 = CreateUdpPort(kLocalAddr2); + port2->SetRole(cricket::ROLE_CONTROLLED); + port2->SetTiebreaker(kTiebreaker2); + ASSERT_EQ(cricket::ICEPROTO_RFC5245, port2->IceProtocol()); + // Same parameters as TestLocalToLocal above. + TestConnectivity("udp", port1, "udp", port2, true, true, true, true); +} + +// This test is trying to validate a successful and failure scenario in a +// loopback test when protocol is RFC5245. For success tiebreaker, username +// should remain equal to the request generated by the port and role of port +// must be in controlling. +TEST_F(PortTest, TestLoopbackCallAsIce) { + talk_base::scoped_ptr lport( + CreateTestPort(kLocalAddr1, "lfrag", "lpass")); + lport->SetIceProtocolType(ICEPROTO_RFC5245); + lport->SetRole(cricket::ROLE_CONTROLLING); + lport->SetTiebreaker(kTiebreaker1); + lport->PrepareAddress(); + ASSERT_FALSE(lport->Candidates().empty()); + Connection* conn = lport->CreateConnection(lport->Candidates()[0], + Port::ORIGIN_MESSAGE); + conn->Ping(0); + + ASSERT_TRUE_WAIT(lport->last_stun_msg() != NULL, 1000); + IceMessage* msg = lport->last_stun_msg(); + EXPECT_EQ(STUN_BINDING_REQUEST, msg->type()); + conn->OnReadPacket(lport->last_stun_buf()->Data(), + lport->last_stun_buf()->Length()); + ASSERT_TRUE_WAIT(lport->last_stun_msg() != NULL, 1000); + msg = lport->last_stun_msg(); + EXPECT_EQ(STUN_BINDING_RESPONSE, msg->type()); + + // If the tiebreaker value is different from port, we expect a error response. + lport->Reset(); + lport->AddCandidateAddress(kLocalAddr2); + // Creating a different connection as |conn| is in STATE_READABLE. + Connection* conn1 = lport->CreateConnection(lport->Candidates()[1], + Port::ORIGIN_MESSAGE); + conn1->Ping(0); + + ASSERT_TRUE_WAIT(lport->last_stun_msg() != NULL, 1000); + msg = lport->last_stun_msg(); + EXPECT_EQ(STUN_BINDING_REQUEST, msg->type()); + IceMessage* modified_req = CreateStunMessage(STUN_BINDING_REQUEST); + const StunByteStringAttribute* username_attr = msg->GetByteString( + STUN_ATTR_USERNAME); + modified_req->AddAttribute(new StunByteStringAttribute( + STUN_ATTR_USERNAME, username_attr->GetString())); + // To make sure we receive error response, adding tiebreaker less than + // what's present in request. + modified_req->AddAttribute(new StunUInt64Attribute( + STUN_ATTR_ICE_CONTROLLING, kTiebreaker1 - 1)); + modified_req->AddMessageIntegrity("lpass"); + modified_req->AddFingerprint(); + + lport->Reset(); + talk_base::scoped_ptr buf(new ByteBuffer()); + WriteStunMessage(modified_req, buf.get()); + conn1->OnReadPacket(buf->Data(), buf->Length()); + ASSERT_TRUE_WAIT(lport->last_stun_msg() != NULL, 1000); + msg = lport->last_stun_msg(); + EXPECT_EQ(STUN_BINDING_ERROR_RESPONSE, msg->type()); +} + +// This test verifies role conflict signal is received when there is +// conflict in the role. In this case both ports are in controlling and +// |rport| has higher tiebreaker value than |lport|. Since |lport| has lower +// value of tiebreaker, when it receives ping request from |rport| it will +// send role conflict signal. +TEST_F(PortTest, TestIceRoleConflict) { + talk_base::scoped_ptr lport( + CreateTestPort(kLocalAddr1, "lfrag", "lpass")); + lport->SetIceProtocolType(ICEPROTO_RFC5245); + lport->SetRole(cricket::ROLE_CONTROLLING); + lport->SetTiebreaker(kTiebreaker1); + talk_base::scoped_ptr rport( + CreateTestPort(kLocalAddr2, "rfrag", "rpass")); + rport->SetIceProtocolType(ICEPROTO_RFC5245); + rport->SetRole(cricket::ROLE_CONTROLLING); + rport->SetTiebreaker(kTiebreaker2); + + lport->PrepareAddress(); + rport->PrepareAddress(); + ASSERT_FALSE(lport->Candidates().empty()); + ASSERT_FALSE(rport->Candidates().empty()); + Connection* lconn = lport->CreateConnection(rport->Candidates()[0], + Port::ORIGIN_MESSAGE); + Connection* rconn = rport->CreateConnection(lport->Candidates()[0], + Port::ORIGIN_MESSAGE); + rconn->Ping(0); + + ASSERT_TRUE_WAIT(rport->last_stun_msg() != NULL, 1000); + IceMessage* msg = rport->last_stun_msg(); + EXPECT_EQ(STUN_BINDING_REQUEST, msg->type()); + // Send rport binding request to lport. + lconn->OnReadPacket(rport->last_stun_buf()->Data(), + rport->last_stun_buf()->Length()); + + ASSERT_TRUE_WAIT(lport->last_stun_msg() != NULL, 1000); + EXPECT_EQ(STUN_BINDING_RESPONSE, lport->last_stun_msg()->type()); + EXPECT_TRUE(role_conflict()); +} + +TEST_F(PortTest, TestTcpNoDelay) { + TCPPort* port1 = CreateTcpPort(kLocalAddr1); + int option_value = -1; + int success = port1->GetOption(talk_base::Socket::OPT_NODELAY, + &option_value); + ASSERT_EQ(0, success); // GetOption() should complete successfully w/ 0 + ASSERT_EQ(1, option_value); + delete port1; +} + +TEST_F(PortTest, TestDelayedBindingUdp) { + FakeAsyncPacketSocket *socket = new FakeAsyncPacketSocket(); + FakePacketSocketFactory socket_factory; + + socket_factory.set_next_udp_socket(socket); + scoped_ptr port( + CreateUdpPort(kLocalAddr1, &socket_factory)); + + socket->set_state(AsyncPacketSocket::STATE_BINDING); + port->PrepareAddress(); + + EXPECT_EQ(0U, port->Candidates().size()); + socket->SignalAddressReady(socket, kLocalAddr2); + + EXPECT_EQ(1U, port->Candidates().size()); +} + +TEST_F(PortTest, TestDelayedBindingTcp) { + FakeAsyncPacketSocket *socket = new FakeAsyncPacketSocket(); + FakePacketSocketFactory socket_factory; + + socket_factory.set_next_server_tcp_socket(socket); + scoped_ptr port( + CreateTcpPort(kLocalAddr1, &socket_factory)); + + socket->set_state(AsyncPacketSocket::STATE_BINDING); + port->PrepareAddress(); + + EXPECT_EQ(0U, port->Candidates().size()); + socket->SignalAddressReady(socket, kLocalAddr2); + + EXPECT_EQ(1U, port->Candidates().size()); +} + +void PortTest::TestCrossFamilyPorts(int type) { + FakePacketSocketFactory factory; + scoped_ptr ports[4]; + SocketAddress addresses[4] = {SocketAddress("192.168.1.3", 0), + SocketAddress("192.168.1.4", 0), + SocketAddress("2001:db8::1", 0), + SocketAddress("2001:db8::2", 0)}; + for (int i = 0; i < 4; i++) { + FakeAsyncPacketSocket *socket = new FakeAsyncPacketSocket(); + if (type == SOCK_DGRAM) { + factory.set_next_udp_socket(socket); + ports[i].reset(CreateUdpPort(addresses[i], &factory)); + } else if (type == SOCK_STREAM) { + factory.set_next_server_tcp_socket(socket); + ports[i].reset(CreateTcpPort(addresses[i], &factory)); + } + socket->set_state(AsyncPacketSocket::STATE_BINDING); + socket->SignalAddressReady(socket, addresses[i]); + ports[i]->PrepareAddress(); + } + + // IPv4 Port, connects to IPv6 candidate and then to IPv4 candidate. + if (type == SOCK_STREAM) { + FakeAsyncPacketSocket* clientsocket = new FakeAsyncPacketSocket(); + factory.set_next_client_tcp_socket(clientsocket); + } + Connection* c = ports[0]->CreateConnection(GetCandidate(ports[2].get()), + Port::ORIGIN_MESSAGE); + EXPECT_TRUE(NULL == c); + EXPECT_EQ(0U, ports[0]->connections().size()); + c = ports[0]->CreateConnection(GetCandidate(ports[1].get()), + Port::ORIGIN_MESSAGE); + EXPECT_FALSE(NULL == c); + EXPECT_EQ(1U, ports[0]->connections().size()); + + // IPv6 Port, connects to IPv4 candidate and to IPv6 candidate. + if (type == SOCK_STREAM) { + FakeAsyncPacketSocket* clientsocket = new FakeAsyncPacketSocket(); + factory.set_next_client_tcp_socket(clientsocket); + } + c = ports[2]->CreateConnection(GetCandidate(ports[0].get()), + Port::ORIGIN_MESSAGE); + EXPECT_TRUE(NULL == c); + EXPECT_EQ(0U, ports[2]->connections().size()); + c = ports[2]->CreateConnection(GetCandidate(ports[3].get()), + Port::ORIGIN_MESSAGE); + EXPECT_FALSE(NULL == c); + EXPECT_EQ(1U, ports[2]->connections().size()); +} + +TEST_F(PortTest, TestSkipCrossFamilyTcp) { + TestCrossFamilyPorts(SOCK_STREAM); +} + +TEST_F(PortTest, TestSkipCrossFamilyUdp) { + TestCrossFamilyPorts(SOCK_DGRAM); +} + +// Test sending STUN messages in GICE format. +TEST_F(PortTest, TestSendStunMessageAsGice) { + talk_base::scoped_ptr lport( + CreateTestPort(kLocalAddr1, "lfrag", "lpass")); + talk_base::scoped_ptr rport( + CreateTestPort(kLocalAddr2, "rfrag", "rpass")); + lport->SetIceProtocolType(ICEPROTO_GOOGLE); + rport->SetIceProtocolType(ICEPROTO_GOOGLE); + + // Send a fake ping from lport to rport. + lport->PrepareAddress(); + rport->PrepareAddress(); + ASSERT_FALSE(rport->Candidates().empty()); + Connection* conn = lport->CreateConnection(rport->Candidates()[0], + Port::ORIGIN_MESSAGE); + rport->CreateConnection(lport->Candidates()[0], Port::ORIGIN_MESSAGE); + conn->Ping(0); + + // Check that it's a proper BINDING-REQUEST. + ASSERT_TRUE_WAIT(lport->last_stun_msg() != NULL, 1000); + IceMessage* msg = lport->last_stun_msg(); + EXPECT_EQ(STUN_BINDING_REQUEST, msg->type()); + EXPECT_FALSE(msg->IsLegacy()); + const StunByteStringAttribute* username_attr = msg->GetByteString( + STUN_ATTR_USERNAME); + ASSERT_TRUE(username_attr != NULL); + EXPECT_EQ("rfraglfrag", username_attr->GetString()); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_MESSAGE_INTEGRITY) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_PRIORITY) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_FINGERPRINT) == NULL); + + // Save a copy of the BINDING-REQUEST for use below. + talk_base::scoped_ptr request(CopyStunMessage(msg)); + + // Respond with a BINDING-RESPONSE. + rport->SendBindingResponse(request.get(), lport->Candidates()[0].address()); + msg = rport->last_stun_msg(); + ASSERT_TRUE(msg != NULL); + EXPECT_EQ(STUN_BINDING_RESPONSE, msg->type()); + EXPECT_FALSE(msg->IsLegacy()); + username_attr = msg->GetByteString(STUN_ATTR_USERNAME); + ASSERT_TRUE(username_attr != NULL); // GICE has a username in the response. + EXPECT_EQ("rfraglfrag", username_attr->GetString()); + const StunAddressAttribute* addr_attr = msg->GetAddress( + STUN_ATTR_MAPPED_ADDRESS); + ASSERT_TRUE(addr_attr != NULL); + EXPECT_EQ(lport->Candidates()[0].address(), addr_attr->GetAddress()); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_XOR_MAPPED_ADDRESS) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_MESSAGE_INTEGRITY) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_PRIORITY) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_FINGERPRINT) == NULL); + + // Respond with a BINDING-ERROR-RESPONSE. This wouldn't happen in real life, + // but we can do it here. + rport->SendBindingErrorResponse(request.get(), + rport->Candidates()[0].address(), + STUN_ERROR_SERVER_ERROR, + STUN_ERROR_REASON_SERVER_ERROR); + msg = rport->last_stun_msg(); + ASSERT_TRUE(msg != NULL); + EXPECT_EQ(STUN_BINDING_ERROR_RESPONSE, msg->type()); + EXPECT_FALSE(msg->IsLegacy()); + username_attr = msg->GetByteString(STUN_ATTR_USERNAME); + ASSERT_TRUE(username_attr != NULL); // GICE has a username in the response. + EXPECT_EQ("rfraglfrag", username_attr->GetString()); + const StunErrorCodeAttribute* error_attr = msg->GetErrorCode(); + ASSERT_TRUE(error_attr != NULL); + // The GICE wire format for error codes is incorrect. + EXPECT_EQ(STUN_ERROR_SERVER_ERROR_AS_GICE, error_attr->code()); + EXPECT_EQ(STUN_ERROR_SERVER_ERROR / 256, error_attr->eclass()); + EXPECT_EQ(STUN_ERROR_SERVER_ERROR % 256, error_attr->number()); + EXPECT_EQ(std::string(STUN_ERROR_REASON_SERVER_ERROR), error_attr->reason()); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_PRIORITY) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_MESSAGE_INTEGRITY) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_FINGERPRINT) == NULL); +} + +// Test sending STUN messages in ICE format. +TEST_F(PortTest, TestSendStunMessageAsIce) { + talk_base::scoped_ptr lport( + CreateTestPort(kLocalAddr1, "lfrag", "lpass")); + talk_base::scoped_ptr rport( + CreateTestPort(kLocalAddr2, "rfrag", "rpass")); + lport->SetIceProtocolType(ICEPROTO_RFC5245); + lport->SetRole(cricket::ROLE_CONTROLLING); + lport->SetTiebreaker(kTiebreaker1); + rport->SetIceProtocolType(ICEPROTO_RFC5245); + rport->SetRole(cricket::ROLE_CONTROLLED); + rport->SetTiebreaker(kTiebreaker2); + + // Send a fake ping from lport to rport. + lport->PrepareAddress(); + rport->PrepareAddress(); + ASSERT_FALSE(rport->Candidates().empty()); + Connection* lconn = lport->CreateConnection( + rport->Candidates()[0], Port::ORIGIN_MESSAGE); + Connection* rconn = rport->CreateConnection( + lport->Candidates()[0], Port::ORIGIN_MESSAGE); + lconn->Ping(0); + + // Check that it's a proper BINDING-REQUEST. + ASSERT_TRUE_WAIT(lport->last_stun_msg() != NULL, 1000); + IceMessage* msg = lport->last_stun_msg(); + EXPECT_EQ(STUN_BINDING_REQUEST, msg->type()); + EXPECT_FALSE(msg->IsLegacy()); + const StunByteStringAttribute* username_attr = + msg->GetByteString(STUN_ATTR_USERNAME); + ASSERT_TRUE(username_attr != NULL); + const StunUInt32Attribute* priority_attr = msg->GetUInt32(STUN_ATTR_PRIORITY); + ASSERT_TRUE(priority_attr != NULL); + EXPECT_EQ(kDefaultPrflxPriority, priority_attr->value()); + EXPECT_EQ("rfrag:lfrag", username_attr->GetString()); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_MESSAGE_INTEGRITY) != NULL); + EXPECT_TRUE(StunMessage::ValidateMessageIntegrity( + lport->last_stun_buf()->Data(), lport->last_stun_buf()->Length(), + "rpass")); + const StunUInt64Attribute* ice_controlling_attr = + msg->GetUInt64(STUN_ATTR_ICE_CONTROLLING); + ASSERT_TRUE(ice_controlling_attr != NULL); + EXPECT_EQ(lport->Tiebreaker(), ice_controlling_attr->value()); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_ICE_CONTROLLED) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_USE_CANDIDATE) != NULL); + EXPECT_TRUE(msg->GetUInt32(STUN_ATTR_FINGERPRINT) != NULL); + EXPECT_TRUE(StunMessage::ValidateFingerprint( + lport->last_stun_buf()->Data(), lport->last_stun_buf()->Length())); + + // Request should not include ping count. + ASSERT_TRUE(msg->GetUInt32(STUN_ATTR_RETRANSMIT_COUNT) == NULL); + + // Save a copy of the BINDING-REQUEST for use below. + talk_base::scoped_ptr request(CopyStunMessage(msg)); + + // Respond with a BINDING-RESPONSE. + rport->SendBindingResponse(request.get(), lport->Candidates()[0].address()); + msg = rport->last_stun_msg(); + ASSERT_TRUE(msg != NULL); + EXPECT_EQ(STUN_BINDING_RESPONSE, msg->type()); + + + EXPECT_FALSE(msg->IsLegacy()); + const StunAddressAttribute* addr_attr = msg->GetAddress( + STUN_ATTR_XOR_MAPPED_ADDRESS); + ASSERT_TRUE(addr_attr != NULL); + EXPECT_EQ(lport->Candidates()[0].address(), addr_attr->GetAddress()); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_MESSAGE_INTEGRITY) != NULL); + EXPECT_TRUE(StunMessage::ValidateMessageIntegrity( + rport->last_stun_buf()->Data(), rport->last_stun_buf()->Length(), + "rpass")); + EXPECT_TRUE(msg->GetUInt32(STUN_ATTR_FINGERPRINT) != NULL); + EXPECT_TRUE(StunMessage::ValidateFingerprint( + lport->last_stun_buf()->Data(), lport->last_stun_buf()->Length())); + // No USERNAME or PRIORITY in ICE responses. + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_USERNAME) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_PRIORITY) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_MAPPED_ADDRESS) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_ICE_CONTROLLING) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_ICE_CONTROLLED) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_USE_CANDIDATE) == NULL); + + // Response should not include ping count. + ASSERT_TRUE(msg->GetUInt32(STUN_ATTR_RETRANSMIT_COUNT) == NULL); + + // Respond with a BINDING-ERROR-RESPONSE. This wouldn't happen in real life, + // but we can do it here. + rport->SendBindingErrorResponse(request.get(), + lport->Candidates()[0].address(), + STUN_ERROR_SERVER_ERROR, + STUN_ERROR_REASON_SERVER_ERROR); + msg = rport->last_stun_msg(); + ASSERT_TRUE(msg != NULL); + EXPECT_EQ(STUN_BINDING_ERROR_RESPONSE, msg->type()); + EXPECT_FALSE(msg->IsLegacy()); + const StunErrorCodeAttribute* error_attr = msg->GetErrorCode(); + ASSERT_TRUE(error_attr != NULL); + EXPECT_EQ(STUN_ERROR_SERVER_ERROR, error_attr->code()); + EXPECT_EQ(std::string(STUN_ERROR_REASON_SERVER_ERROR), error_attr->reason()); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_MESSAGE_INTEGRITY) != NULL); + EXPECT_TRUE(StunMessage::ValidateMessageIntegrity( + rport->last_stun_buf()->Data(), rport->last_stun_buf()->Length(), + "rpass")); + EXPECT_TRUE(msg->GetUInt32(STUN_ATTR_FINGERPRINT) != NULL); + EXPECT_TRUE(StunMessage::ValidateFingerprint( + lport->last_stun_buf()->Data(), lport->last_stun_buf()->Length())); + // No USERNAME with ICE. + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_USERNAME) == NULL); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_PRIORITY) == NULL); + + // Testing STUN binding requests from rport --> lport, having ICE_CONTROLLED + // and (incremented) RETRANSMIT_COUNT attributes. + rport->Reset(); + rport->set_send_retransmit_count_attribute(true); + rconn->Ping(0); + rconn->Ping(0); + rconn->Ping(0); + ASSERT_TRUE_WAIT(rport->last_stun_msg() != NULL, 1000); + msg = rport->last_stun_msg(); + EXPECT_EQ(STUN_BINDING_REQUEST, msg->type()); + const StunUInt64Attribute* ice_controlled_attr = + msg->GetUInt64(STUN_ATTR_ICE_CONTROLLED); + ASSERT_TRUE(ice_controlled_attr != NULL); + EXPECT_EQ(rport->Tiebreaker(), ice_controlled_attr->value()); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_USE_CANDIDATE) == NULL); + + // Request should include ping count. + const StunUInt32Attribute* retransmit_attr = + msg->GetUInt32(STUN_ATTR_RETRANSMIT_COUNT); + ASSERT_TRUE(retransmit_attr != NULL); + EXPECT_EQ(2U, retransmit_attr->value()); + + // Respond with a BINDING-RESPONSE. + request.reset(CopyStunMessage(msg)); + lport->SendBindingResponse(request.get(), rport->Candidates()[0].address()); + msg = lport->last_stun_msg(); + + // Response should include same ping count. + retransmit_attr = msg->GetUInt32(STUN_ATTR_RETRANSMIT_COUNT); + ASSERT_TRUE(retransmit_attr != NULL); + EXPECT_EQ(2U, retransmit_attr->value()); +} + +TEST_F(PortTest, TestUseCandidateAttribute) { + talk_base::scoped_ptr lport( + CreateTestPort(kLocalAddr1, "lfrag", "lpass")); + talk_base::scoped_ptr rport( + CreateTestPort(kLocalAddr2, "rfrag", "rpass")); + lport->SetIceProtocolType(ICEPROTO_RFC5245); + lport->SetRole(cricket::ROLE_CONTROLLING); + lport->SetTiebreaker(kTiebreaker1); + rport->SetIceProtocolType(ICEPROTO_RFC5245); + rport->SetRole(cricket::ROLE_CONTROLLED); + rport->SetTiebreaker(kTiebreaker2); + + // Send a fake ping from lport to rport. + lport->PrepareAddress(); + rport->PrepareAddress(); + ASSERT_FALSE(rport->Candidates().empty()); + Connection* lconn = lport->CreateConnection( + rport->Candidates()[0], Port::ORIGIN_MESSAGE); + lconn->Ping(0); + ASSERT_TRUE_WAIT(lport->last_stun_msg() != NULL, 1000); + IceMessage* msg = lport->last_stun_msg(); + const StunUInt64Attribute* ice_controlling_attr = + msg->GetUInt64(STUN_ATTR_ICE_CONTROLLING); + ASSERT_TRUE(ice_controlling_attr != NULL); + const StunByteStringAttribute* use_candidate_attr = msg->GetByteString( + STUN_ATTR_USE_CANDIDATE); + ASSERT_TRUE(use_candidate_attr != NULL); +} + +// Test handling STUN messages in GICE format. +TEST_F(PortTest, TestHandleStunMessageAsGice) { + // Our port will act as the "remote" port. + talk_base::scoped_ptr port( + CreateTestPort(kLocalAddr2, "rfrag", "rpass")); + port->SetIceProtocolType(ICEPROTO_GOOGLE); + + talk_base::scoped_ptr in_msg, out_msg; + talk_base::scoped_ptr buf(new ByteBuffer()); + talk_base::SocketAddress addr(kLocalAddr1); + std::string username; + + // BINDING-REQUEST from local to remote with valid GICE username and no M-I. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_REQUEST, + "rfraglfrag")); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() != NULL); // Succeeds, since this is GICE. + EXPECT_EQ("lfrag", username); + + // Add M-I; should be ignored and rest of message parsed normally. + in_msg->AddMessageIntegrity("password"); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() != NULL); + EXPECT_EQ("lfrag", username); + + // BINDING-RESPONSE with username, as done in GICE. Should succeed. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_RESPONSE, + "rfraglfrag")); + in_msg->AddAttribute( + new StunAddressAttribute(STUN_ATTR_MAPPED_ADDRESS, kLocalAddr2)); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() != NULL); + EXPECT_EQ("", username); + + // BINDING-RESPONSE without username. Should be tolerated as well. + in_msg.reset(CreateStunMessage(STUN_BINDING_RESPONSE)); + in_msg->AddAttribute( + new StunAddressAttribute(STUN_ATTR_MAPPED_ADDRESS, kLocalAddr2)); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() != NULL); + EXPECT_EQ("", username); + + // BINDING-ERROR-RESPONSE with username and error code. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_ERROR_RESPONSE, + "rfraglfrag")); + in_msg->AddAttribute(new StunErrorCodeAttribute(STUN_ATTR_ERROR_CODE, + STUN_ERROR_SERVER_ERROR_AS_GICE, STUN_ERROR_REASON_SERVER_ERROR)); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + ASSERT_TRUE(out_msg.get() != NULL); + EXPECT_EQ("", username); + ASSERT_TRUE(out_msg->GetErrorCode() != NULL); + // GetStunMessage doesn't unmunge the GICE error code (happens downstream). + EXPECT_EQ(STUN_ERROR_SERVER_ERROR_AS_GICE, out_msg->GetErrorCode()->code()); + EXPECT_EQ(std::string(STUN_ERROR_REASON_SERVER_ERROR), + out_msg->GetErrorCode()->reason()); +} + +// Test handling STUN messages in ICE format. +TEST_F(PortTest, TestHandleStunMessageAsIce) { + // Our port will act as the "remote" port. + talk_base::scoped_ptr port( + CreateTestPort(kLocalAddr2, "rfrag", "rpass")); + port->SetIceProtocolType(ICEPROTO_RFC5245); + + talk_base::scoped_ptr in_msg, out_msg; + talk_base::scoped_ptr buf(new ByteBuffer()); + talk_base::SocketAddress addr(kLocalAddr1); + std::string username; + + // BINDING-REQUEST from local to remote with valid ICE username, + // MESSAGE-INTEGRITY, and FINGERPRINT. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_REQUEST, + "rfrag:lfrag")); + in_msg->AddMessageIntegrity("rpass"); + in_msg->AddFingerprint(); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() != NULL); + EXPECT_EQ("lfrag", username); + + // BINDING-RESPONSE without username, with MESSAGE-INTEGRITY and FINGERPRINT. + in_msg.reset(CreateStunMessage(STUN_BINDING_RESPONSE)); + in_msg->AddAttribute( + new StunXorAddressAttribute(STUN_ATTR_XOR_MAPPED_ADDRESS, kLocalAddr2)); + in_msg->AddMessageIntegrity("rpass"); + in_msg->AddFingerprint(); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() != NULL); + EXPECT_EQ("", username); + + // BINDING-ERROR-RESPONSE without username, with error, M-I, and FINGERPRINT. + in_msg.reset(CreateStunMessage(STUN_BINDING_ERROR_RESPONSE)); + in_msg->AddAttribute(new StunErrorCodeAttribute(STUN_ATTR_ERROR_CODE, + STUN_ERROR_SERVER_ERROR, STUN_ERROR_REASON_SERVER_ERROR)); + in_msg->AddFingerprint(); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() != NULL); + EXPECT_EQ("", username); + ASSERT_TRUE(out_msg->GetErrorCode() != NULL); + EXPECT_EQ(STUN_ERROR_SERVER_ERROR, out_msg->GetErrorCode()->code()); + EXPECT_EQ(std::string(STUN_ERROR_REASON_SERVER_ERROR), + out_msg->GetErrorCode()->reason()); +} + +// Tests handling of GICE binding requests with missing or incorrect usernames. +TEST_F(PortTest, TestHandleStunMessageAsGiceBadUsername) { + talk_base::scoped_ptr port( + CreateTestPort(kLocalAddr2, "rfrag", "rpass")); + port->SetIceProtocolType(ICEPROTO_GOOGLE); + + talk_base::scoped_ptr in_msg, out_msg; + talk_base::scoped_ptr buf(new ByteBuffer()); + talk_base::SocketAddress addr(kLocalAddr1); + std::string username; + + // BINDING-REQUEST with no username. + in_msg.reset(CreateStunMessage(STUN_BINDING_REQUEST)); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() == NULL); + EXPECT_EQ("", username); + EXPECT_EQ(STUN_ERROR_BAD_REQUEST_AS_GICE, port->last_stun_error_code()); + + // BINDING-REQUEST with empty username. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_REQUEST, "")); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() == NULL); + EXPECT_EQ("", username); + EXPECT_EQ(STUN_ERROR_UNAUTHORIZED_AS_GICE, port->last_stun_error_code()); + + // BINDING-REQUEST with too-short username. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_REQUEST, "lfra")); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() == NULL); + EXPECT_EQ("", username); + EXPECT_EQ(STUN_ERROR_UNAUTHORIZED_AS_GICE, port->last_stun_error_code()); + + // BINDING-REQUEST with reversed username. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_REQUEST, + "lfragrfrag")); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() == NULL); + EXPECT_EQ("", username); + EXPECT_EQ(STUN_ERROR_UNAUTHORIZED_AS_GICE, port->last_stun_error_code()); + + // BINDING-REQUEST with garbage username. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_REQUEST, + "abcdefgh")); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() == NULL); + EXPECT_EQ("", username); + EXPECT_EQ(STUN_ERROR_UNAUTHORIZED_AS_GICE, port->last_stun_error_code()); +} + +// Tests handling of ICE binding requests with missing or incorrect usernames. +TEST_F(PortTest, TestHandleStunMessageAsIceBadUsername) { + talk_base::scoped_ptr port( + CreateTestPort(kLocalAddr2, "rfrag", "rpass")); + port->SetIceProtocolType(ICEPROTO_RFC5245); + + talk_base::scoped_ptr in_msg, out_msg; + talk_base::scoped_ptr buf(new ByteBuffer()); + talk_base::SocketAddress addr(kLocalAddr1); + std::string username; + + // BINDING-REQUEST with no username. + in_msg.reset(CreateStunMessage(STUN_BINDING_REQUEST)); + in_msg->AddMessageIntegrity("rpass"); + in_msg->AddFingerprint(); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() == NULL); + EXPECT_EQ("", username); + EXPECT_EQ(STUN_ERROR_BAD_REQUEST, port->last_stun_error_code()); + + // BINDING-REQUEST with empty username. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_REQUEST, "")); + in_msg->AddMessageIntegrity("rpass"); + in_msg->AddFingerprint(); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() == NULL); + EXPECT_EQ("", username); + EXPECT_EQ(STUN_ERROR_UNAUTHORIZED, port->last_stun_error_code()); + + // BINDING-REQUEST with too-short username. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_REQUEST, "rfra")); + in_msg->AddMessageIntegrity("rpass"); + in_msg->AddFingerprint(); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() == NULL); + EXPECT_EQ("", username); + EXPECT_EQ(STUN_ERROR_UNAUTHORIZED, port->last_stun_error_code()); + + // BINDING-REQUEST with reversed username. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_REQUEST, + "lfrag:rfrag")); + in_msg->AddMessageIntegrity("rpass"); + in_msg->AddFingerprint(); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() == NULL); + EXPECT_EQ("", username); + EXPECT_EQ(STUN_ERROR_UNAUTHORIZED, port->last_stun_error_code()); + + // BINDING-REQUEST with garbage username. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_REQUEST, + "abcd:efgh")); + in_msg->AddMessageIntegrity("rpass"); + in_msg->AddFingerprint(); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() == NULL); + EXPECT_EQ("", username); + EXPECT_EQ(STUN_ERROR_UNAUTHORIZED, port->last_stun_error_code()); +} + +// Test handling STUN messages (as ICE) with missing or malformed M-I. +TEST_F(PortTest, TestHandleStunMessageAsIceBadMessageIntegrity) { + // Our port will act as the "remote" port. + talk_base::scoped_ptr port( + CreateTestPort(kLocalAddr2, "rfrag", "rpass")); + port->SetIceProtocolType(ICEPROTO_RFC5245); + + talk_base::scoped_ptr in_msg, out_msg; + talk_base::scoped_ptr buf(new ByteBuffer()); + talk_base::SocketAddress addr(kLocalAddr1); + std::string username; + + // BINDING-REQUEST from local to remote with valid ICE username and + // FINGERPRINT, but no MESSAGE-INTEGRITY. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_REQUEST, + "rfrag:lfrag")); + in_msg->AddFingerprint(); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() == NULL); + EXPECT_EQ("", username); + EXPECT_EQ(STUN_ERROR_BAD_REQUEST, port->last_stun_error_code()); + + // BINDING-REQUEST from local to remote with valid ICE username and + // FINGERPRINT, but invalid MESSAGE-INTEGRITY. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_REQUEST, + "rfrag:lfrag")); + in_msg->AddMessageIntegrity("invalid"); + in_msg->AddFingerprint(); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() == NULL); + EXPECT_EQ("", username); + EXPECT_EQ(STUN_ERROR_UNAUTHORIZED, port->last_stun_error_code()); + + // TODO: BINDING-RESPONSES and BINDING-ERROR-RESPONSES are checked + // by the Connection, not the Port, since they require the remote username. + // Change this test to pass in data via Connection::OnReadPacket instead. +} + +// Test handling STUN messages (as ICE) with missing or malformed FINGERPRINT. +TEST_F(PortTest, TestHandleStunMessageAsIceBadFingerprint) { + // Our port will act as the "remote" port. + talk_base::scoped_ptr port( + CreateTestPort(kLocalAddr2, "rfrag", "rpass")); + port->SetIceProtocolType(ICEPROTO_RFC5245); + + talk_base::scoped_ptr in_msg, out_msg; + talk_base::scoped_ptr buf(new ByteBuffer()); + talk_base::SocketAddress addr(kLocalAddr1); + std::string username; + + // BINDING-REQUEST from local to remote with valid ICE username and + // MESSAGE-INTEGRITY, but no FINGERPRINT; GetStunMessage should fail. + in_msg.reset(CreateStunMessageWithUsername(STUN_BINDING_REQUEST, + "rfrag:lfrag")); + in_msg->AddMessageIntegrity("rpass"); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_FALSE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_EQ(0, port->last_stun_error_code()); + + // Now, add a fingerprint, but munge the message so it's not valid. + in_msg->AddFingerprint(); + in_msg->SetTransactionID("TESTTESTBADD"); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_FALSE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_EQ(0, port->last_stun_error_code()); + + // Valid BINDING-RESPONSE, except no FINGERPRINT. + in_msg.reset(CreateStunMessage(STUN_BINDING_RESPONSE)); + in_msg->AddAttribute( + new StunXorAddressAttribute(STUN_ATTR_XOR_MAPPED_ADDRESS, kLocalAddr2)); + in_msg->AddMessageIntegrity("rpass"); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_FALSE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_EQ(0, port->last_stun_error_code()); + + // Now, add a fingerprint, but munge the message so it's not valid. + in_msg->AddFingerprint(); + in_msg->SetTransactionID("TESTTESTBADD"); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_FALSE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_EQ(0, port->last_stun_error_code()); + + // Valid BINDING-ERROR-RESPONSE, except no FINGERPRINT. + in_msg.reset(CreateStunMessage(STUN_BINDING_ERROR_RESPONSE)); + in_msg->AddAttribute(new StunErrorCodeAttribute(STUN_ATTR_ERROR_CODE, + STUN_ERROR_SERVER_ERROR, STUN_ERROR_REASON_SERVER_ERROR)); + in_msg->AddMessageIntegrity("rpass"); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_FALSE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_EQ(0, port->last_stun_error_code()); + + // Now, add a fingerprint, but munge the message so it's not valid. + in_msg->AddFingerprint(); + in_msg->SetTransactionID("TESTTESTBADD"); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_FALSE(port->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_EQ(0, port->last_stun_error_code()); +} + +// Test handling of STUN binding indication messages (as ICE). STUN binding +// indications are allowed only to the connection which is in read mode. +TEST_F(PortTest, TestHandleStunBindingIndication) { + talk_base::scoped_ptr lport( + CreateTestPort(kLocalAddr2, "lfrag", "lpass")); + lport->SetIceProtocolType(ICEPROTO_RFC5245); + lport->SetRole(cricket::ROLE_CONTROLLING); + lport->SetTiebreaker(kTiebreaker1); + + // Verifying encoding and decoding STUN indication message. + talk_base::scoped_ptr in_msg, out_msg; + talk_base::scoped_ptr buf(new ByteBuffer()); + talk_base::SocketAddress addr(kLocalAddr1); + std::string username; + + in_msg.reset(CreateStunMessage(STUN_BINDING_INDICATION)); + in_msg->AddFingerprint(); + WriteStunMessage(in_msg.get(), buf.get()); + EXPECT_TRUE(lport->GetStunMessage(buf->Data(), buf->Length(), addr, + out_msg.accept(), &username)); + EXPECT_TRUE(out_msg.get() != NULL); + EXPECT_EQ(out_msg->type(), STUN_BINDING_INDICATION); + EXPECT_EQ("", username); + + // Verify connection can handle STUN indication and updates + // last_ping_received. + talk_base::scoped_ptr rport( + CreateTestPort(kLocalAddr2, "rfrag", "rpass")); + rport->SetIceProtocolType(ICEPROTO_RFC5245); + rport->SetRole(cricket::ROLE_CONTROLLED); + rport->SetTiebreaker(kTiebreaker2); + + lport->PrepareAddress(); + rport->PrepareAddress(); + ASSERT_FALSE(lport->Candidates().empty()); + ASSERT_FALSE(rport->Candidates().empty()); + + Connection* lconn = lport->CreateConnection(rport->Candidates()[0], + Port::ORIGIN_MESSAGE); + Connection* rconn = rport->CreateConnection(lport->Candidates()[0], + Port::ORIGIN_MESSAGE); + rconn->Ping(0); + + ASSERT_TRUE_WAIT(rport->last_stun_msg() != NULL, 1000); + IceMessage* msg = rport->last_stun_msg(); + EXPECT_EQ(STUN_BINDING_REQUEST, msg->type()); + // Send rport binding request to lport. + lconn->OnReadPacket(rport->last_stun_buf()->Data(), + rport->last_stun_buf()->Length()); + ASSERT_TRUE_WAIT(lport->last_stun_msg() != NULL, 1000); + EXPECT_EQ(STUN_BINDING_RESPONSE, lport->last_stun_msg()->type()); + uint32 last_ping_received1 = lconn->last_ping_received(); + + // Adding a delay of 100ms. + talk_base::Thread::Current()->ProcessMessages(100); + // Pinging lconn using stun indication message. + lconn->OnReadPacket(buf->Data(), buf->Length()); + uint32 last_ping_received2 = lconn->last_ping_received(); + EXPECT_GT(last_ping_received2, last_ping_received1); +} + +TEST_F(PortTest, TestComputeCandidatePriority) { + talk_base::scoped_ptr port( + CreateTestPort(kLocalAddr1, "name", "pass")); + port->set_type_preference(90); + port->set_component(177); + port->AddCandidateAddress(SocketAddress("192.168.1.4", 1234)); + port->AddCandidateAddress(SocketAddress("2001:db8::1234", 1234)); + port->AddCandidateAddress(SocketAddress("fc12:3456::1234", 1234)); + port->AddCandidateAddress(SocketAddress("::ffff:192.168.1.4", 1234)); + port->AddCandidateAddress(SocketAddress("::192.168.1.4", 1234)); + port->AddCandidateAddress(SocketAddress("2002::1234:5678", 1234)); + port->AddCandidateAddress(SocketAddress("2001::1234:5678", 1234)); + port->AddCandidateAddress(SocketAddress("fecf::1234:5678", 1234)); + port->AddCandidateAddress(SocketAddress("3ffe::1234:5678", 1234)); + // These should all be: + // (90 << 24) | ([rfc3484 pref value] << 8) | (256 - 177) + uint32 expected_priority_v4 = 1509957199U; + uint32 expected_priority_v6 = 1509959759U; + uint32 expected_priority_ula = 1509962319U; + uint32 expected_priority_v4mapped = expected_priority_v4; + uint32 expected_priority_v4compat = 1509949775U; + uint32 expected_priority_6to4 = 1509954639U; + uint32 expected_priority_teredo = 1509952079U; + uint32 expected_priority_sitelocal = 1509949775U; + uint32 expected_priority_6bone = 1509949775U; + ASSERT_EQ(expected_priority_v4, port->Candidates()[0].priority()); + ASSERT_EQ(expected_priority_v6, port->Candidates()[1].priority()); + ASSERT_EQ(expected_priority_ula, port->Candidates()[2].priority()); + ASSERT_EQ(expected_priority_v4mapped, port->Candidates()[3].priority()); + ASSERT_EQ(expected_priority_v4compat, port->Candidates()[4].priority()); + ASSERT_EQ(expected_priority_6to4, port->Candidates()[5].priority()); + ASSERT_EQ(expected_priority_teredo, port->Candidates()[6].priority()); + ASSERT_EQ(expected_priority_sitelocal, port->Candidates()[7].priority()); + ASSERT_EQ(expected_priority_6bone, port->Candidates()[8].priority()); +} + +TEST_F(PortTest, TestPortProxyProperties) { + talk_base::scoped_ptr port( + CreateTestPort(kLocalAddr1, "name", "pass")); + port->SetRole(cricket::ROLE_CONTROLLING); + port->SetTiebreaker(kTiebreaker1); + + // Create a proxy port. + talk_base::scoped_ptr proxy(new PortProxy()); + proxy->set_impl(port.get()); + EXPECT_EQ(port->Type(), proxy->Type()); + EXPECT_EQ(port->Network(), proxy->Network()); + EXPECT_EQ(port->Role(), proxy->Role()); + EXPECT_EQ(port->Tiebreaker(), proxy->Tiebreaker()); +} + +// In the case of shared socket, one port may be shared by local and stun. +// Test that candidates with different types will have different foundation. +TEST_F(PortTest, TestFoundation) { + talk_base::scoped_ptr testport( + CreateTestPort(kLocalAddr1, "name", "pass")); + testport->AddCandidateAddress(kLocalAddr1, kLocalAddr1, + LOCAL_PORT_TYPE, + cricket::ICE_TYPE_PREFERENCE_HOST, false); + testport->AddCandidateAddress(kLocalAddr2, kLocalAddr1, + STUN_PORT_TYPE, + cricket::ICE_TYPE_PREFERENCE_SRFLX, true); + EXPECT_NE(testport->Candidates()[0].foundation(), + testport->Candidates()[1].foundation()); +} + +// This test verifies the foundation of different types of ICE candidates. +TEST_F(PortTest, TestCandidateFoundation) { + talk_base::scoped_ptr nat_server( + CreateNatServer(kNatAddr1, NAT_OPEN_CONE)); + talk_base::scoped_ptr udpport1(CreateUdpPort(kLocalAddr1)); + udpport1->PrepareAddress(); + talk_base::scoped_ptr udpport2(CreateUdpPort(kLocalAddr1)); + udpport2->PrepareAddress(); + EXPECT_EQ(udpport1->Candidates()[0].foundation(), + udpport2->Candidates()[0].foundation()); + talk_base::scoped_ptr tcpport1(CreateTcpPort(kLocalAddr1)); + tcpport1->PrepareAddress(); + talk_base::scoped_ptr tcpport2(CreateTcpPort(kLocalAddr1)); + tcpport2->PrepareAddress(); + EXPECT_EQ(tcpport1->Candidates()[0].foundation(), + tcpport2->Candidates()[0].foundation()); + talk_base::scoped_ptr stunport( + CreateStunPort(kLocalAddr1, nat_socket_factory1())); + stunport->PrepareAddress(); + ASSERT_EQ_WAIT(1U, stunport->Candidates().size(), kTimeout); + EXPECT_NE(tcpport1->Candidates()[0].foundation(), + stunport->Candidates()[0].foundation()); + EXPECT_NE(tcpport2->Candidates()[0].foundation(), + stunport->Candidates()[0].foundation()); + EXPECT_NE(udpport1->Candidates()[0].foundation(), + stunport->Candidates()[0].foundation()); + EXPECT_NE(udpport2->Candidates()[0].foundation(), + stunport->Candidates()[0].foundation()); + // Verify GTURN candidate foundation. + talk_base::scoped_ptr relayport( + CreateGturnPort(kLocalAddr1)); + relayport->AddServerAddress( + cricket::ProtocolAddress(kRelayUdpIntAddr, cricket::PROTO_UDP)); + relayport->PrepareAddress(); + ASSERT_EQ_WAIT(1U, relayport->Candidates().size(), kTimeout); + EXPECT_NE(udpport1->Candidates()[0].foundation(), + relayport->Candidates()[0].foundation()); + EXPECT_NE(udpport2->Candidates()[0].foundation(), + relayport->Candidates()[0].foundation()); + // Verifying TURN candidate foundation. + talk_base::scoped_ptr turnport(CreateTurnPort( + kLocalAddr1, nat_socket_factory1(), PROTO_UDP, PROTO_UDP)); + turnport->PrepareAddress(); + ASSERT_EQ_WAIT(1U, turnport->Candidates().size(), kTimeout); + EXPECT_NE(udpport1->Candidates()[0].foundation(), + turnport->Candidates()[0].foundation()); + EXPECT_NE(udpport2->Candidates()[0].foundation(), + turnport->Candidates()[0].foundation()); + EXPECT_NE(stunport->Candidates()[0].foundation(), + turnport->Candidates()[0].foundation()); +} + +// This test verifies the related addresses of different types of +// ICE candiates. +TEST_F(PortTest, TestCandidateRelatedAddress) { + talk_base::scoped_ptr nat_server( + CreateNatServer(kNatAddr1, NAT_OPEN_CONE)); + talk_base::scoped_ptr udpport(CreateUdpPort(kLocalAddr1)); + udpport->PrepareAddress(); + // For UDPPort, related address will be empty. + EXPECT_TRUE(udpport->Candidates()[0].related_address().IsNil()); + // Testing related address for stun candidates. + // For stun candidate related address must be equal to the base + // socket address. + talk_base::scoped_ptr stunport( + CreateStunPort(kLocalAddr1, nat_socket_factory1())); + stunport->PrepareAddress(); + ASSERT_EQ_WAIT(1U, stunport->Candidates().size(), kTimeout); + // Check STUN candidate address. + EXPECT_EQ(stunport->Candidates()[0].address().ipaddr(), + kNatAddr1.ipaddr()); + // Check STUN candidate related address. + EXPECT_EQ(stunport->Candidates()[0].related_address(), + stunport->GetLocalAddress()); + // Verifying the related address for the GTURN candidates. + // NOTE: In case of GTURN related address will be equal to the mapped + // address, but address(mapped) will not be XOR. + talk_base::scoped_ptr relayport( + CreateGturnPort(kLocalAddr1)); + relayport->AddServerAddress( + cricket::ProtocolAddress(kRelayUdpIntAddr, cricket::PROTO_UDP)); + relayport->PrepareAddress(); + ASSERT_EQ_WAIT(1U, relayport->Candidates().size(), kTimeout); + // For Gturn related address is set to "0.0.0.0:0" + EXPECT_EQ(talk_base::SocketAddress(), + relayport->Candidates()[0].related_address()); + // Verifying the related address for TURN candidate. + // For TURN related address must be equal to the mapped address. + talk_base::scoped_ptr turnport(CreateTurnPort( + kLocalAddr1, nat_socket_factory1(), PROTO_UDP, PROTO_UDP)); + turnport->PrepareAddress(); + ASSERT_EQ_WAIT(1U, turnport->Candidates().size(), kTimeout); + EXPECT_EQ(kTurnUdpExtAddr.ipaddr(), + turnport->Candidates()[0].address().ipaddr()); + EXPECT_EQ(kNatAddr1.ipaddr(), + turnport->Candidates()[0].related_address().ipaddr()); +} + +// Test priority value overflow handling when preference is set to 3. +TEST_F(PortTest, TestCandidatePreference) { + cricket::Candidate cand1; + cand1.set_preference(3); + cricket::Candidate cand2; + cand2.set_preference(1); + EXPECT_TRUE(cand1.preference() > cand2.preference()); +} + +// Test the Connection priority is calculated correctly. +TEST_F(PortTest, TestConnectionPriority) { + talk_base::scoped_ptr lport( + CreateTestPort(kLocalAddr1, "lfrag", "lpass")); + lport->set_type_preference(cricket::ICE_TYPE_PREFERENCE_HOST); + talk_base::scoped_ptr rport( + CreateTestPort(kLocalAddr2, "rfrag", "rpass")); + rport->set_type_preference(cricket::ICE_TYPE_PREFERENCE_RELAY); + lport->set_component(123); + lport->AddCandidateAddress(SocketAddress("192.168.1.4", 1234)); + rport->set_component(23); + rport->AddCandidateAddress(SocketAddress("10.1.1.100", 1234)); + + EXPECT_EQ(0x7E001E85U, lport->Candidates()[0].priority()); + EXPECT_EQ(0x2001EE9U, rport->Candidates()[0].priority()); + + // RFC 5245 + // pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0) + lport->SetRole(cricket::ROLE_CONTROLLING); + rport->SetRole(cricket::ROLE_CONTROLLED); + Connection* lconn = lport->CreateConnection( + rport->Candidates()[0], Port::ORIGIN_MESSAGE); +#if defined(WIN32) + EXPECT_EQ(0x2001EE9FC003D0BU, lconn->priority()); +#else + EXPECT_EQ(0x2001EE9FC003D0BLLU, lconn->priority()); +#endif + + lport->SetRole(cricket::ROLE_CONTROLLED); + rport->SetRole(cricket::ROLE_CONTROLLING); + Connection* rconn = rport->CreateConnection( + lport->Candidates()[0], Port::ORIGIN_MESSAGE); +#if defined(WIN32) + EXPECT_EQ(0x2001EE9FC003D0AU, rconn->priority()); +#else + EXPECT_EQ(0x2001EE9FC003D0ALLU, rconn->priority()); +#endif +} + +TEST_F(PortTest, TestWritableState) { + UDPPort* port1 = CreateUdpPort(kLocalAddr1); + UDPPort* port2 = CreateUdpPort(kLocalAddr2); + + // Set up channels. + TestChannel ch1(port1, port2); + TestChannel ch2(port2, port1); + + // Acquire addresses. + ch1.Start(); + ch2.Start(); + ASSERT_EQ_WAIT(1, ch1.complete_count(), kTimeout); + ASSERT_EQ_WAIT(1, ch2.complete_count(), kTimeout); + + // Send a ping from src to dst. + ch1.CreateConnection(); + ASSERT_TRUE(ch1.conn() != NULL); + EXPECT_EQ(Connection::STATE_WRITE_INIT, ch1.conn()->write_state()); + EXPECT_TRUE_WAIT(ch1.conn()->connected(), kTimeout); // for TCP connect + ch1.Ping(); + WAIT(!ch2.remote_address().IsNil(), kTimeout); + + // Data should be unsendable until the connection is accepted. + char data[] = "abcd"; + int data_size = ARRAY_SIZE(data); + EXPECT_EQ(SOCKET_ERROR, ch1.conn()->Send(data, data_size)); + + // Accept the connection to return the binding response, transition to + // writable, and allow data to be sent. + ch2.AcceptConnection(); + EXPECT_EQ_WAIT(Connection::STATE_WRITABLE, ch1.conn()->write_state(), + kTimeout); + EXPECT_EQ(data_size, ch1.conn()->Send(data, data_size)); + + // Ask the connection to update state as if enough time has passed to lose + // full writability and 5 pings went unresponded to. We'll accomplish the + // latter by sending pings but not pumping messages. + for (uint32 i = 1; i <= CONNECTION_WRITE_CONNECT_FAILURES; ++i) { + ch1.Ping(i); + } + uint32 unreliable_timeout_delay = CONNECTION_WRITE_CONNECT_TIMEOUT + 500u; + ch1.conn()->UpdateState(unreliable_timeout_delay); + EXPECT_EQ(Connection::STATE_WRITE_UNRELIABLE, ch1.conn()->write_state()); + + // Data should be able to be sent in this state. + EXPECT_EQ(data_size, ch1.conn()->Send(data, data_size)); + + // And now allow the other side to process the pings and send binding + // responses. + EXPECT_EQ_WAIT(Connection::STATE_WRITABLE, ch1.conn()->write_state(), + kTimeout); + + // Wait long enough for a full timeout (past however long we've already + // waited). + for (uint32 i = 1; i <= CONNECTION_WRITE_CONNECT_FAILURES; ++i) { + ch1.Ping(unreliable_timeout_delay + i); + } + ch1.conn()->UpdateState(unreliable_timeout_delay + CONNECTION_WRITE_TIMEOUT + + 500u); + EXPECT_EQ(Connection::STATE_WRITE_TIMEOUT, ch1.conn()->write_state()); + + // Now that the connection has completely timed out, data send should fail. + EXPECT_EQ(SOCKET_ERROR, ch1.conn()->Send(data, data_size)); + + ch1.Stop(); + ch2.Stop(); +} + +TEST_F(PortTest, TestTimeoutForNeverWritable) { + UDPPort* port1 = CreateUdpPort(kLocalAddr1); + UDPPort* port2 = CreateUdpPort(kLocalAddr2); + + // Set up channels. + TestChannel ch1(port1, port2); + TestChannel ch2(port2, port1); + + // Acquire addresses. + ch1.Start(); + ch2.Start(); + + ch1.CreateConnection(); + ASSERT_TRUE(ch1.conn() != NULL); + EXPECT_EQ(Connection::STATE_WRITE_INIT, ch1.conn()->write_state()); + + // Attempt to go directly to write timeout. + for (uint32 i = 1; i <= CONNECTION_WRITE_CONNECT_FAILURES; ++i) { + ch1.Ping(i); + } + ch1.conn()->UpdateState(CONNECTION_WRITE_TIMEOUT + 500u); + EXPECT_EQ(Connection::STATE_WRITE_TIMEOUT, ch1.conn()->write_state()); +} + +// This test verifies the connection setup between ICEMODE_FULL +// and ICEMODE_LITE. +// In this test |ch1| behaves like FULL mode client and we have created +// port which responds to the ping message just like LITE client. +TEST_F(PortTest, TestIceLiteConnectivity) { + TestPort* ice_full_port = CreateTestPort( + kLocalAddr1, "lfrag", "lpass", cricket::ICEPROTO_RFC5245, + cricket::ROLE_CONTROLLING, kTiebreaker1); + + talk_base::scoped_ptr ice_lite_port(CreateTestPort( + kLocalAddr2, "rfrag", "rpass", cricket::ICEPROTO_RFC5245, + cricket::ROLE_CONTROLLED, kTiebreaker2)); + // Setup TestChannel. This behaves like FULL mode client. + TestChannel ch1(ice_full_port, ice_lite_port.get()); + ch1.SetIceMode(ICEMODE_FULL); + + // Start gathering candidates. + ch1.Start(); + ice_lite_port->PrepareAddress(); + + ASSERT_EQ_WAIT(1, ch1.complete_count(), kTimeout); + ASSERT_FALSE(ice_lite_port->Candidates().empty()); + + ch1.CreateConnection(); + ASSERT_TRUE(ch1.conn() != NULL); + EXPECT_EQ(Connection::STATE_WRITE_INIT, ch1.conn()->write_state()); + + // Send ping from full mode client. + // This ping must not have USE_CANDIDATE_ATTR. + ch1.Ping(); + + // Verify stun ping is without USE_CANDIDATE_ATTR. Getting message directly + // from port. + ASSERT_TRUE_WAIT(ice_full_port->last_stun_msg() != NULL, 1000); + IceMessage* msg = ice_full_port->last_stun_msg(); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_USE_CANDIDATE) == NULL); + + // Respond with a BINDING-RESPONSE from litemode client. + // NOTE: Ideally we should't create connection at this stage from lite + // port, as it should be done only after receiving ping with USE_CANDIDATE. + // But we need a connection to send a response message. + ice_lite_port->CreateConnection( + ice_full_port->Candidates()[0], cricket::Port::ORIGIN_MESSAGE); + talk_base::scoped_ptr request(CopyStunMessage(msg)); + ice_lite_port->SendBindingResponse( + request.get(), ice_full_port->Candidates()[0].address()); + + // Feeding the respone message from litemode to the full mode connection. + ch1.conn()->OnReadPacket(ice_lite_port->last_stun_buf()->Data(), + ice_lite_port->last_stun_buf()->Length()); + // Verifying full mode connection becomes writable from the response. + EXPECT_EQ_WAIT(Connection::STATE_WRITABLE, ch1.conn()->write_state(), + kTimeout); + EXPECT_TRUE_WAIT(ch1.nominated(), kTimeout); + + // Clear existing stun messsages. Otherwise we will process old stun + // message right after we send ping. + ice_full_port->Reset(); + // Send ping. This must have USE_CANDIDATE_ATTR. + ch1.Ping(); + ASSERT_TRUE_WAIT(ice_full_port->last_stun_msg() != NULL, 1000); + msg = ice_full_port->last_stun_msg(); + EXPECT_TRUE(msg->GetByteString(STUN_ATTR_USE_CANDIDATE) != NULL); + ch1.Stop(); +} + diff --git a/talk/p2p/base/portallocator.cc b/talk/p2p/base/portallocator.cc new file mode 100644 index 000000000..00ba4d8ce --- /dev/null +++ b/talk/p2p/base/portallocator.cc @@ -0,0 +1,108 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/portallocator.h" + +#include "talk/p2p/base/portallocatorsessionproxy.h" + +namespace cricket { + +PortAllocatorSession::PortAllocatorSession(const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd, + uint32 flags) + : content_name_(content_name), + component_(component), + flags_(flags), + // If PORTALLOCATOR_ENABLE_SHARED_UFRAG flag is not enabled, ignore the + // incoming ufrag and pwd, which will cause each Port to generate one + // by itself. + username_(flags_ & PORTALLOCATOR_ENABLE_SHARED_UFRAG ? ice_ufrag : ""), + password_(flags_ & PORTALLOCATOR_ENABLE_SHARED_UFRAG ? ice_pwd : "") { +} + +PortAllocator::~PortAllocator() { + for (SessionMuxerMap::iterator iter = muxers_.begin(); + iter != muxers_.end(); ++iter) { + delete iter->second; + } +} + +PortAllocatorSession* PortAllocator::CreateSession( + const std::string& sid, + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd) { + if (flags_ & PORTALLOCATOR_ENABLE_BUNDLE) { + // If we just use |sid| as key in identifying PortAllocatorSessionMuxer, + // ICE restart will not result in different candidates, as |sid| will + // be same. To yield different candiates we are using combination of + // |ice_ufrag| and |ice_pwd|. + // Ideally |ice_ufrag| and |ice_pwd| should change together, but + // there can be instances where only ice_pwd will be changed. + std::string key_str = ice_ufrag + ":" + ice_pwd; + PortAllocatorSessionMuxer* muxer = GetSessionMuxer(key_str); + if (!muxer) { + PortAllocatorSession* session_impl = CreateSessionInternal( + content_name, component, ice_ufrag, ice_pwd); + // Create PortAllocatorSessionMuxer object for |session_impl|. + muxer = new PortAllocatorSessionMuxer(session_impl); + muxer->SignalDestroyed.connect( + this, &PortAllocator::OnSessionMuxerDestroyed); + // Add PortAllocatorSession to the map. + muxers_[key_str] = muxer; + } + PortAllocatorSessionProxy* proxy = + new PortAllocatorSessionProxy(content_name, component, flags_); + muxer->RegisterSessionProxy(proxy); + return proxy; + } + return CreateSessionInternal(content_name, component, ice_ufrag, ice_pwd); +} + +PortAllocatorSessionMuxer* PortAllocator::GetSessionMuxer( + const std::string& key) const { + SessionMuxerMap::const_iterator iter = muxers_.find(key); + if (iter != muxers_.end()) + return iter->second; + return NULL; +} + +void PortAllocator::OnSessionMuxerDestroyed( + PortAllocatorSessionMuxer* session) { + SessionMuxerMap::iterator iter; + for (iter = muxers_.begin(); iter != muxers_.end(); ++iter) { + if (iter->second == session) + break; + } + if (iter != muxers_.end()) + muxers_.erase(iter); +} + +} // namespace cricket diff --git a/talk/p2p/base/portallocator.h b/talk/p2p/base/portallocator.h new file mode 100644 index 000000000..7568d45f2 --- /dev/null +++ b/talk/p2p/base/portallocator.h @@ -0,0 +1,184 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_PORTALLOCATOR_H_ +#define TALK_P2P_BASE_PORTALLOCATOR_H_ + +#include +#include + +#include "talk/base/helpers.h" +#include "talk/base/proxyinfo.h" +#include "talk/base/sigslot.h" +#include "talk/p2p/base/portinterface.h" + +namespace cricket { + +// PortAllocator is responsible for allocating Port types for a given +// P2PSocket. It also handles port freeing. +// +// Clients can override this class to control port allocation, including +// what kinds of ports are allocated. + +const uint32 PORTALLOCATOR_DISABLE_UDP = 0x01; +const uint32 PORTALLOCATOR_DISABLE_STUN = 0x02; +const uint32 PORTALLOCATOR_DISABLE_RELAY = 0x04; +const uint32 PORTALLOCATOR_DISABLE_TCP = 0x08; +const uint32 PORTALLOCATOR_ENABLE_SHAKER = 0x10; +const uint32 PORTALLOCATOR_ENABLE_BUNDLE = 0x20; +const uint32 PORTALLOCATOR_ENABLE_IPV6 = 0x40; +const uint32 PORTALLOCATOR_ENABLE_SHARED_UFRAG = 0x80; +const uint32 PORTALLOCATOR_ENABLE_SHARED_SOCKET = 0x100; +const uint32 PORTALLOCATOR_ENABLE_STUN_RETRANSMIT_ATTRIBUTE = 0x200; +const uint32 PORTALLOCATOR_USE_LARGE_SOCKET_SEND_BUFFERS = 0x400; + +const uint32 kDefaultPortAllocatorFlags = 0; + +const uint32 kDefaultStepDelay = 1000; // 1 sec step delay. +// As per RFC 5245 Appendix B.1, STUN transactions need to be paced at certain +// internal. Less than 20ms is not acceptable. We choose 50ms as our default. +const uint32 kMinimumStepDelay = 50; + +class PortAllocatorSessionMuxer; + +class PortAllocatorSession : public sigslot::has_slots<> { + public: + // Content name passed in mostly for logging and debugging. + // TODO(mallinath) - Change username and password to ice_ufrag and ice_pwd. + PortAllocatorSession(const std::string& content_name, + int component, + const std::string& username, + const std::string& password, + uint32 flags); + + // Subclasses should clean up any ports created. + virtual ~PortAllocatorSession() {} + + uint32 flags() const { return flags_; } + void set_flags(uint32 flags) { flags_ = flags; } + std::string content_name() const { return content_name_; } + int component() const { return component_; } + + // Starts gathering STUN and Relay configurations. + virtual void StartGettingPorts() = 0; + virtual void StopGettingPorts() = 0; + virtual bool IsGettingPorts() = 0; + + sigslot::signal2 SignalPortReady; + sigslot::signal2&> SignalCandidatesReady; + sigslot::signal1 SignalCandidatesAllocationDone; + + virtual uint32 generation() { return generation_; } + virtual void set_generation(uint32 generation) { generation_ = generation; } + sigslot::signal1 SignalDestroyed; + + protected: + const std::string& username() const { return username_; } + const std::string& password() const { return password_; } + + std::string content_name_; + int component_; + + private: + uint32 flags_; + uint32 generation_; + std::string username_; + std::string password_; +}; + +class PortAllocator : public sigslot::has_slots<> { + public: + PortAllocator() : + flags_(kDefaultPortAllocatorFlags), + min_port_(0), + max_port_(0), + step_delay_(kDefaultStepDelay) { + // This will allow us to have old behavior on non webrtc clients. + } + virtual ~PortAllocator(); + + PortAllocatorSession* CreateSession( + const std::string& sid, + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd); + + PortAllocatorSessionMuxer* GetSessionMuxer(const std::string& key) const; + void OnSessionMuxerDestroyed(PortAllocatorSessionMuxer* session); + + uint32 flags() const { return flags_; } + void set_flags(uint32 flags) { flags_ = flags; } + + const std::string& user_agent() const { return agent_; } + const talk_base::ProxyInfo& proxy() const { return proxy_; } + void set_proxy(const std::string& agent, const talk_base::ProxyInfo& proxy) { + agent_ = agent; + proxy_ = proxy; + } + + // Gets/Sets the port range to use when choosing client ports. + int min_port() const { return min_port_; } + int max_port() const { return max_port_; } + bool SetPortRange(int min_port, int max_port) { + if (min_port > max_port) { + return false; + } + + min_port_ = min_port; + max_port_ = max_port; + return true; + } + + void set_step_delay(uint32 delay) { + ASSERT(delay >= kMinimumStepDelay); + step_delay_ = delay; + } + uint32 step_delay() const { return step_delay_; } + + protected: + virtual PortAllocatorSession* CreateSessionInternal( + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd) = 0; + + typedef std::map SessionMuxerMap; + + uint32 flags_; + std::string agent_; + talk_base::ProxyInfo proxy_; + int min_port_; + int max_port_; + uint32 step_delay_; + SessionMuxerMap muxers_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_PORTALLOCATOR_H_ diff --git a/talk/p2p/base/portallocatorsessionproxy.cc b/talk/p2p/base/portallocatorsessionproxy.cc new file mode 100644 index 000000000..1a201d3fa --- /dev/null +++ b/talk/p2p/base/portallocatorsessionproxy.cc @@ -0,0 +1,239 @@ +/* + * libjingle + * Copyright 2004--2011, 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/p2p/base/portallocatorsessionproxy.h" + +#include "talk/base/thread.h" +#include "talk/p2p/base/portallocator.h" +#include "talk/p2p/base/portproxy.h" + +namespace cricket { + +enum { + MSG_SEND_ALLOCATION_DONE = 1, + MSG_SEND_ALLOCATED_PORTS, +}; + +typedef talk_base::TypedMessageData ProxyObjData; + +PortAllocatorSessionMuxer::PortAllocatorSessionMuxer( + PortAllocatorSession* session) + : worker_thread_(talk_base::Thread::Current()), + session_(session), + candidate_done_signal_received_(false) { + session_->SignalPortReady.connect( + this, &PortAllocatorSessionMuxer::OnPortReady); + session_->SignalCandidatesAllocationDone.connect( + this, &PortAllocatorSessionMuxer::OnCandidatesAllocationDone); +} + +PortAllocatorSessionMuxer::~PortAllocatorSessionMuxer() { + for (size_t i = 0; i < session_proxies_.size(); ++i) + delete session_proxies_[i]; + + SignalDestroyed(this); +} + +void PortAllocatorSessionMuxer::RegisterSessionProxy( + PortAllocatorSessionProxy* session_proxy) { + session_proxies_.push_back(session_proxy); + session_proxy->SignalDestroyed.connect( + this, &PortAllocatorSessionMuxer::OnSessionProxyDestroyed); + session_proxy->set_impl(session_.get()); + + // Populate new proxy session with the information available in the actual + // implementation. + if (!ports_.empty()) { + worker_thread_->Post( + this, MSG_SEND_ALLOCATED_PORTS, new ProxyObjData(session_proxy)); + } + + if (candidate_done_signal_received_) { + worker_thread_->Post( + this, MSG_SEND_ALLOCATION_DONE, new ProxyObjData(session_proxy)); + } +} + +void PortAllocatorSessionMuxer::OnCandidatesAllocationDone( + PortAllocatorSession* session) { + candidate_done_signal_received_ = true; +} + +void PortAllocatorSessionMuxer::OnPortReady(PortAllocatorSession* session, + PortInterface* port) { + ASSERT(session == session_.get()); + ports_.push_back(port); + port->SignalDestroyed.connect( + this, &PortAllocatorSessionMuxer::OnPortDestroyed); +} + +void PortAllocatorSessionMuxer::OnPortDestroyed(PortInterface* port) { + std::vector::iterator it = + std::find(ports_.begin(), ports_.end(), port); + if (it != ports_.end()) + ports_.erase(it); +} + +void PortAllocatorSessionMuxer::OnSessionProxyDestroyed( + PortAllocatorSession* proxy) { + + std::vector::iterator it = + std::find(session_proxies_.begin(), session_proxies_.end(), proxy); + if (it != session_proxies_.end()) { + session_proxies_.erase(it); + } + + if (session_proxies_.empty()) { + // Destroy PortAllocatorSession and its associated muxer object if all + // proxies belonging to this session are already destroyed. + delete this; + } +} + +void PortAllocatorSessionMuxer::OnMessage(talk_base::Message *pmsg) { + ProxyObjData* proxy = static_cast (pmsg->pdata); + switch (pmsg->message_id) { + case MSG_SEND_ALLOCATION_DONE: + SendAllocationDone_w(proxy->data()); + delete proxy; + break; + case MSG_SEND_ALLOCATED_PORTS: + SendAllocatedPorts_w(proxy->data()); + delete proxy; + break; + default: + ASSERT(false); + break; + } +} + +void PortAllocatorSessionMuxer::SendAllocationDone_w( + PortAllocatorSessionProxy* proxy) { + std::vector::iterator iter = + std::find(session_proxies_.begin(), session_proxies_.end(), proxy); + if (iter != session_proxies_.end()) { + proxy->OnCandidatesAllocationDone(session_.get()); + } +} + +void PortAllocatorSessionMuxer::SendAllocatedPorts_w( + PortAllocatorSessionProxy* proxy) { + std::vector::iterator iter = + std::find(session_proxies_.begin(), session_proxies_.end(), proxy); + if (iter != session_proxies_.end()) { + for (size_t i = 0; i < ports_.size(); ++i) { + PortInterface* port = ports_[i]; + proxy->OnPortReady(session_.get(), port); + // If port already has candidates, send this to the clients of proxy + // session. This can happen if proxy is created later than the actual + // implementation. + if (!port->Candidates().empty()) { + proxy->OnCandidatesReady(session_.get(), port->Candidates()); + } + } + } +} + +PortAllocatorSessionProxy::~PortAllocatorSessionProxy() { + std::map::iterator it; + for (it = proxy_ports_.begin(); it != proxy_ports_.end(); it++) + delete it->second; + + SignalDestroyed(this); +} + +void PortAllocatorSessionProxy::set_impl( + PortAllocatorSession* session) { + impl_ = session; + + impl_->SignalCandidatesReady.connect( + this, &PortAllocatorSessionProxy::OnCandidatesReady); + impl_->SignalPortReady.connect( + this, &PortAllocatorSessionProxy::OnPortReady); + impl_->SignalCandidatesAllocationDone.connect( + this, &PortAllocatorSessionProxy::OnCandidatesAllocationDone); +} + +void PortAllocatorSessionProxy::StartGettingPorts() { + ASSERT(impl_ != NULL); + // Since all proxies share a common PortAllocatorSession, this check will + // prohibit sending multiple STUN ping messages to the stun server, which + // is a problem on Chrome. GetInitialPorts() and StartGetAllPorts() called + // from the worker thread and are called together from TransportChannel, + // checking for IsGettingAllPorts() for GetInitialPorts() will not be a + // problem. + if (!impl_->IsGettingPorts()) { + impl_->StartGettingPorts(); + } +} + +void PortAllocatorSessionProxy::StopGettingPorts() { + ASSERT(impl_ != NULL); + if (impl_->IsGettingPorts()) { + impl_->StopGettingPorts(); + } +} + +bool PortAllocatorSessionProxy::IsGettingPorts() { + ASSERT(impl_ != NULL); + return impl_->IsGettingPorts(); +} + +void PortAllocatorSessionProxy::OnPortReady(PortAllocatorSession* session, + PortInterface* port) { + ASSERT(session == impl_); + + PortProxy* proxy_port = new PortProxy(); + proxy_port->set_impl(port); + proxy_ports_[port] = proxy_port; + SignalPortReady(this, proxy_port); +} + +void PortAllocatorSessionProxy::OnCandidatesReady( + PortAllocatorSession* session, + const std::vector& candidates) { + ASSERT(session == impl_); + + // Since all proxy sessions share a common PortAllocatorSession, + // all Candidates will have name associated with the common PAS. + // Change Candidate name with the PortAllocatorSessionProxy name. + std::vector our_candidates; + for (size_t i = 0; i < candidates.size(); ++i) { + Candidate new_local_candidate = candidates[i]; + new_local_candidate.set_component(component_); + our_candidates.push_back(new_local_candidate); + } + SignalCandidatesReady(this, our_candidates); +} + +void PortAllocatorSessionProxy::OnCandidatesAllocationDone( + PortAllocatorSession* session) { + ASSERT(session == impl_); + SignalCandidatesAllocationDone(this); +} + +} // namespace cricket diff --git a/talk/p2p/base/portallocatorsessionproxy.h b/talk/p2p/base/portallocatorsessionproxy.h new file mode 100644 index 000000000..990ea8a03 --- /dev/null +++ b/talk/p2p/base/portallocatorsessionproxy.h @@ -0,0 +1,123 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifndef TALK_P2P_BASE_PORTALLOCATORSESSIONPROXY_H_ +#define TALK_P2P_BASE_PORTALLOCATORSESSIONPROXY_H_ + +#include + +#include "talk/p2p/base/candidate.h" +#include "talk/p2p/base/portallocator.h" + +namespace cricket { +class PortAllocator; +class PortAllocatorSessionProxy; +class PortProxy; + +// This class maintains the list of cricket::Port* objects. Ports will be +// deleted upon receiving SignalDestroyed signal. This class is used when +// PORTALLOCATOR_ENABLE_BUNDLE flag is set. + +class PortAllocatorSessionMuxer : public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + explicit PortAllocatorSessionMuxer(PortAllocatorSession* session); + virtual ~PortAllocatorSessionMuxer(); + + void RegisterSessionProxy(PortAllocatorSessionProxy* session_proxy); + + void OnPortReady(PortAllocatorSession* session, PortInterface* port); + void OnPortDestroyed(PortInterface* port); + void OnCandidatesAllocationDone(PortAllocatorSession* session); + + const std::vector& ports() { return ports_; } + + sigslot::signal1 SignalDestroyed; + + private: + virtual void OnMessage(talk_base::Message *pmsg); + void OnSessionProxyDestroyed(PortAllocatorSession* proxy); + void SendAllocationDone_w(PortAllocatorSessionProxy* proxy); + void SendAllocatedPorts_w(PortAllocatorSessionProxy* proxy); + + // Port will be deleted when SignalDestroyed received, otherwise delete + // happens when PortAllocatorSession dtor is called. + talk_base::Thread* worker_thread_; + std::vector ports_; + talk_base::scoped_ptr session_; + std::vector session_proxies_; + bool candidate_done_signal_received_; +}; + +class PortAllocatorSessionProxy : public PortAllocatorSession { + public: + PortAllocatorSessionProxy(const std::string& content_name, + int component, + uint32 flags) + // Use empty string as the ufrag and pwd because the proxy always uses + // the ufrag and pwd from the underlying implementation. + : PortAllocatorSession(content_name, component, "", "", flags), + impl_(NULL) { + } + + virtual ~PortAllocatorSessionProxy(); + + PortAllocatorSession* impl() { return impl_; } + void set_impl(PortAllocatorSession* session); + + // Forwards call to the actual PortAllocatorSession. + virtual void StartGettingPorts(); + virtual void StopGettingPorts(); + virtual bool IsGettingPorts(); + + virtual void set_generation(uint32 generation) { + ASSERT(impl_ != NULL); + impl_->set_generation(generation); + } + + virtual uint32 generation() { + ASSERT(impl_ != NULL); + return impl_->generation(); + } + + private: + void OnPortReady(PortAllocatorSession* session, PortInterface* port); + void OnCandidatesReady(PortAllocatorSession* session, + const std::vector& candidates); + void OnPortDestroyed(PortInterface* port); + void OnCandidatesAllocationDone(PortAllocatorSession* session); + + // This is the actual PortAllocatorSession, owned by PortAllocator. + PortAllocatorSession* impl_; + std::map proxy_ports_; + + friend class PortAllocatorSessionMuxer; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_PORTALLOCATORSESSIONPROXY_H_ diff --git a/talk/p2p/base/portallocatorsessionproxy_unittest.cc b/talk/p2p/base/portallocatorsessionproxy_unittest.cc new file mode 100644 index 000000000..fc6dc590b --- /dev/null +++ b/talk/p2p/base/portallocatorsessionproxy_unittest.cc @@ -0,0 +1,160 @@ +/* + * libjingle + * Copyright 2012 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 + +#include "talk/base/fakenetwork.h" +#include "talk/base/gunit.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/base/portallocatorsessionproxy.h" +#include "talk/p2p/client/basicportallocator.h" +#include "talk/p2p/client/fakeportallocator.h" + +using cricket::Candidate; +using cricket::PortAllocatorSession; +using cricket::PortAllocatorSessionMuxer; +using cricket::PortAllocatorSessionProxy; + +// Based on ICE_UFRAG_LENGTH +static const char kIceUfrag0[] = "TESTICEUFRAG0000"; +// Based on ICE_PWD_LENGTH +static const char kIcePwd0[] = "TESTICEPWD00000000000000"; + +class TestSessionChannel : public sigslot::has_slots<> { + public: + explicit TestSessionChannel(PortAllocatorSessionProxy* proxy) + : proxy_session_(proxy), + candidates_count_(0), + allocation_complete_(false), + ports_count_(0) { + proxy_session_->SignalCandidatesAllocationDone.connect( + this, &TestSessionChannel::OnCandidatesAllocationDone); + proxy_session_->SignalCandidatesReady.connect( + this, &TestSessionChannel::OnCandidatesReady); + proxy_session_->SignalPortReady.connect( + this, &TestSessionChannel::OnPortReady); + } + virtual ~TestSessionChannel() {} + void OnCandidatesReady(PortAllocatorSession* session, + const std::vector& candidates) { + EXPECT_EQ(proxy_session_, session); + candidates_count_ += candidates.size(); + } + void OnCandidatesAllocationDone(PortAllocatorSession* session) { + EXPECT_EQ(proxy_session_, session); + allocation_complete_ = true; + } + void OnPortReady(PortAllocatorSession* session, + cricket::PortInterface* port) { + EXPECT_EQ(proxy_session_, session); + ++ports_count_; + } + int candidates_count() { return candidates_count_; } + bool allocation_complete() { return allocation_complete_; } + int ports_count() { return ports_count_; } + + void StartGettingPorts() { + proxy_session_->StartGettingPorts(); + } + + void StopGettingPorts() { + proxy_session_->StopGettingPorts(); + } + + bool IsGettingPorts() { + return proxy_session_->IsGettingPorts(); + } + + private: + PortAllocatorSessionProxy* proxy_session_; + int candidates_count_; + bool allocation_complete_; + int ports_count_; +}; + +class PortAllocatorSessionProxyTest : public testing::Test { + public: + PortAllocatorSessionProxyTest() + : socket_factory_(talk_base::Thread::Current()), + allocator_(talk_base::Thread::Current(), NULL), + session_(talk_base::Thread::Current(), &socket_factory_, + "test content", 1, + kIceUfrag0, kIcePwd0), + session_muxer_(new PortAllocatorSessionMuxer(&session_)) { + } + virtual ~PortAllocatorSessionProxyTest() {} + void RegisterSessionProxy(PortAllocatorSessionProxy* proxy) { + session_muxer_->RegisterSessionProxy(proxy); + } + + TestSessionChannel* CreateChannel() { + PortAllocatorSessionProxy* proxy = + new PortAllocatorSessionProxy("test content", 1, 0); + TestSessionChannel* channel = new TestSessionChannel(proxy); + session_muxer_->RegisterSessionProxy(proxy); + channel->StartGettingPorts(); + return channel; + } + + protected: + talk_base::BasicPacketSocketFactory socket_factory_; + cricket::FakePortAllocator allocator_; + cricket::FakePortAllocatorSession session_; + // Muxer object will be delete itself after all registered session proxies + // are deleted. + PortAllocatorSessionMuxer* session_muxer_; +}; + +TEST_F(PortAllocatorSessionProxyTest, TestBasic) { + TestSessionChannel* channel = CreateChannel(); + EXPECT_EQ_WAIT(1, channel->candidates_count(), 1000); + EXPECT_EQ(1, channel->ports_count()); + EXPECT_TRUE(channel->allocation_complete()); + delete channel; +} + +TEST_F(PortAllocatorSessionProxyTest, TestLateBinding) { + TestSessionChannel* channel1 = CreateChannel(); + EXPECT_EQ_WAIT(1, channel1->candidates_count(), 1000); + EXPECT_EQ(1, channel1->ports_count()); + EXPECT_TRUE(channel1->allocation_complete()); + EXPECT_EQ(1, session_.port_config_count()); + // Creating another PortAllocatorSessionProxy and it also should receive + // already happened events. + PortAllocatorSessionProxy* proxy = + new PortAllocatorSessionProxy("test content", 2, 0); + TestSessionChannel* channel2 = new TestSessionChannel(proxy); + session_muxer_->RegisterSessionProxy(proxy); + EXPECT_TRUE(channel2->IsGettingPorts()); + EXPECT_EQ_WAIT(1, channel2->candidates_count(), 1000); + EXPECT_EQ(1, channel2->ports_count()); + EXPECT_TRUE_WAIT(channel2->allocation_complete(), 1000); + EXPECT_EQ(1, session_.port_config_count()); + delete channel1; + delete channel2; +} diff --git a/talk/p2p/base/portinterface.h b/talk/p2p/base/portinterface.h new file mode 100644 index 000000000..ec34bf135 --- /dev/null +++ b/talk/p2p/base/portinterface.h @@ -0,0 +1,143 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_P2P_BASE_PORTINTERFACE_H_ +#define TALK_P2P_BASE_PORTINTERFACE_H_ + +#include + +#include "talk/base/socketaddress.h" +#include "talk/p2p/base/transport.h" + +namespace talk_base { +class Network; +class PacketSocketFactory; +} + +namespace cricket { +class Connection; +class IceMessage; +class StunMessage; + +enum ProtocolType { + PROTO_UDP, + PROTO_TCP, + PROTO_SSLTCP, + PROTO_LAST = PROTO_SSLTCP +}; + +// Defines the interface for a port, which represents a local communication +// mechanism that can be used to create connections to similar mechanisms of +// the other client. Various types of ports will implement this interface. +class PortInterface { + public: + virtual ~PortInterface() {} + + virtual const std::string& Type() const = 0; + virtual talk_base::Network* Network() const = 0; + + virtual void SetIceProtocolType(IceProtocolType protocol) = 0; + virtual IceProtocolType IceProtocol() const = 0; + + // Methods to set/get ICE role and tiebreaker values. + virtual void SetRole(TransportRole role) = 0; + virtual TransportRole Role() const = 0; + + virtual void SetTiebreaker(uint64 tiebreaker) = 0; + virtual uint64 Tiebreaker() const = 0; + + virtual bool SharedSocket() const = 0; + + // PrepareAddress will attempt to get an address for this port that other + // clients can send to. It may take some time before the address is ready. + // Once it is ready, we will send SignalAddressReady. If errors are + // preventing the port from getting an address, it may send + // SignalAddressError. + virtual void PrepareAddress() = 0; + + // Returns the connection to the given address or NULL if none exists. + virtual Connection* GetConnection( + const talk_base::SocketAddress& remote_addr) = 0; + + // Creates a new connection to the given address. + enum CandidateOrigin { ORIGIN_THIS_PORT, ORIGIN_OTHER_PORT, ORIGIN_MESSAGE }; + virtual Connection* CreateConnection( + const Candidate& remote_candidate, CandidateOrigin origin) = 0; + + // Functions on the underlying socket(s). + virtual int SetOption(talk_base::Socket::Option opt, int value) = 0; + virtual int GetError() = 0; + + virtual int GetOption(talk_base::Socket::Option opt, int* value) = 0; + + virtual const std::vector& Candidates() const = 0; + + // Sends the given packet to the given address, provided that the address is + // that of a connection or an address that has sent to us already. + virtual int SendTo(const void* data, size_t size, + const talk_base::SocketAddress& addr, bool payload) = 0; + + // Indicates that we received a successful STUN binding request from an + // address that doesn't correspond to any current connection. To turn this + // into a real connection, call CreateConnection. + sigslot::signal6 SignalUnknownAddress; + + // Sends a response message (normal or error) to the given request. One of + // these methods should be called as a response to SignalUnknownAddress. + // NOTE: You MUST call CreateConnection BEFORE SendBindingResponse. + virtual void SendBindingResponse(StunMessage* request, + const talk_base::SocketAddress& addr) = 0; + virtual void SendBindingErrorResponse( + StunMessage* request, const talk_base::SocketAddress& addr, + int error_code, const std::string& reason) = 0; + + // Signaled when this port decides to delete itself because it no longer has + // any usefulness. + sigslot::signal1 SignalDestroyed; + + // Signaled when Port discovers ice role conflict with the peer. + sigslot::signal1 SignalRoleConflict; + + // Normally, packets arrive through a connection (or they result signaling of + // unknown address). Calling this method turns off delivery of packets + // through their respective connection and instead delivers every packet + // through this port. + virtual void EnablePortPackets() = 0; + sigslot::signal4 SignalReadPacket; + + virtual std::string ToString() const = 0; + + protected: + PortInterface() {} +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_PORTINTERFACE_H_ diff --git a/talk/p2p/base/portproxy.cc b/talk/p2p/base/portproxy.cc new file mode 100644 index 000000000..63fd02653 --- /dev/null +++ b/talk/p2p/base/portproxy.cc @@ -0,0 +1,180 @@ +/* + * libjingle + * Copyright 2004--2011, 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/p2p/base/portproxy.h" + +namespace cricket { + +void PortProxy::set_impl(PortInterface* port) { + impl_ = port; + impl_->SignalUnknownAddress.connect( + this, &PortProxy::OnUnknownAddress); + impl_->SignalDestroyed.connect(this, &PortProxy::OnPortDestroyed); + impl_->SignalRoleConflict.connect(this, &PortProxy::OnRoleConflict); +} + +const std::string& PortProxy::Type() const { + ASSERT(impl_ != NULL); + return impl_->Type(); +} + +talk_base::Network* PortProxy::Network() const { + ASSERT(impl_ != NULL); + return impl_->Network(); +} + +void PortProxy::SetIceProtocolType(IceProtocolType protocol) { + ASSERT(impl_ != NULL); + impl_->SetIceProtocolType(protocol); +} + +IceProtocolType PortProxy::IceProtocol() const { + ASSERT(impl_ != NULL); + return impl_->IceProtocol(); +} + +// Methods to set/get ICE role and tiebreaker values. +void PortProxy::SetRole(TransportRole role) { + ASSERT(impl_ != NULL); + impl_->SetRole(role); +} + +TransportRole PortProxy::Role() const { + ASSERT(impl_ != NULL); + return impl_->Role(); +} + +void PortProxy::SetTiebreaker(uint64 tiebreaker) { + ASSERT(impl_ != NULL); + impl_->SetTiebreaker(tiebreaker); +} + +uint64 PortProxy::Tiebreaker() const { + ASSERT(impl_ != NULL); + return impl_->Tiebreaker(); +} + +bool PortProxy::SharedSocket() const { + ASSERT(impl_ != NULL); + return impl_->SharedSocket(); +} + +void PortProxy::PrepareAddress() { + ASSERT(impl_ != NULL); + impl_->PrepareAddress(); +} + +Connection* PortProxy::CreateConnection(const Candidate& remote_candidate, + CandidateOrigin origin) { + ASSERT(impl_ != NULL); + return impl_->CreateConnection(remote_candidate, origin); +} + +int PortProxy::SendTo(const void* data, + size_t size, + const talk_base::SocketAddress& addr, + bool payload) { + ASSERT(impl_ != NULL); + return impl_->SendTo(data, size, addr, payload); +} + +int PortProxy::SetOption(talk_base::Socket::Option opt, + int value) { + ASSERT(impl_ != NULL); + return impl_->SetOption(opt, value); +} + +int PortProxy::GetOption(talk_base::Socket::Option opt, + int* value) { + ASSERT(impl_ != NULL); + return impl_->GetOption(opt, value); +} + + +int PortProxy::GetError() { + ASSERT(impl_ != NULL); + return impl_->GetError(); +} + +const std::vector& PortProxy::Candidates() const { + ASSERT(impl_ != NULL); + return impl_->Candidates(); +} + +void PortProxy::SendBindingResponse( + StunMessage* request, const talk_base::SocketAddress& addr) { + ASSERT(impl_ != NULL); + impl_->SendBindingResponse(request, addr); +} + +Connection* PortProxy::GetConnection( + const talk_base::SocketAddress& remote_addr) { + ASSERT(impl_ != NULL); + return impl_->GetConnection(remote_addr); +} + +void PortProxy::SendBindingErrorResponse( + StunMessage* request, const talk_base::SocketAddress& addr, + int error_code, const std::string& reason) { + ASSERT(impl_ != NULL); + impl_->SendBindingErrorResponse(request, addr, error_code, reason); +} + +void PortProxy::EnablePortPackets() { + ASSERT(impl_ != NULL); + impl_->EnablePortPackets(); +} + +std::string PortProxy::ToString() const { + ASSERT(impl_ != NULL); + return impl_->ToString(); +} + +void PortProxy::OnUnknownAddress( + PortInterface *port, + const talk_base::SocketAddress &addr, + ProtocolType proto, + IceMessage *stun_msg, + const std::string &remote_username, + bool port_muxed) { + ASSERT(port == impl_); + ASSERT(!port_muxed); + SignalUnknownAddress(this, addr, proto, stun_msg, remote_username, true); +} + +void PortProxy::OnRoleConflict(PortInterface* port) { + ASSERT(port == impl_); + SignalRoleConflict(this); +} + +void PortProxy::OnPortDestroyed(PortInterface* port) { + ASSERT(port == impl_); + // |port| will be destroyed in PortAllocatorSessionMuxer. + SignalDestroyed(this); +} + +} // namespace cricket diff --git a/talk/p2p/base/portproxy.h b/talk/p2p/base/portproxy.h new file mode 100644 index 000000000..8653c0972 --- /dev/null +++ b/talk/p2p/base/portproxy.h @@ -0,0 +1,102 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifndef TALK_P2P_BASE_PORTPROXY_H_ +#define TALK_P2P_BASE_PORTPROXY_H_ + +#include "talk/base/sigslot.h" +#include "talk/p2p/base/portinterface.h" + +namespace talk_base { +class Network; +} + +namespace cricket { + +class PortProxy : public PortInterface, public sigslot::has_slots<> { + public: + PortProxy() {} + virtual ~PortProxy() {} + + PortInterface* impl() { return impl_; } + void set_impl(PortInterface* port); + + virtual const std::string& Type() const; + virtual talk_base::Network* Network() const; + + virtual void SetIceProtocolType(IceProtocolType protocol); + virtual IceProtocolType IceProtocol() const; + + // Methods to set/get ICE role and tiebreaker values. + virtual void SetRole(TransportRole role); + virtual TransportRole Role() const; + + virtual void SetTiebreaker(uint64 tiebreaker); + virtual uint64 Tiebreaker() const; + + virtual bool SharedSocket() const; + + // Forwards call to the actual Port. + virtual void PrepareAddress(); + virtual Connection* CreateConnection(const Candidate& remote_candidate, + CandidateOrigin origin); + virtual Connection* GetConnection( + const talk_base::SocketAddress& remote_addr); + + virtual int SendTo(const void* data, size_t size, + const talk_base::SocketAddress& addr, bool payload); + virtual int SetOption(talk_base::Socket::Option opt, int value); + virtual int GetOption(talk_base::Socket::Option opt, int* value); + virtual int GetError(); + + virtual const std::vector& Candidates() const; + + virtual void SendBindingResponse(StunMessage* request, + const talk_base::SocketAddress& addr); + virtual void SendBindingErrorResponse( + StunMessage* request, const talk_base::SocketAddress& addr, + int error_code, const std::string& reason); + + virtual void EnablePortPackets(); + virtual std::string ToString() const; + + private: + void OnUnknownAddress(PortInterface *port, + const talk_base::SocketAddress &addr, + ProtocolType proto, + IceMessage *stun_msg, + const std::string &remote_username, + bool port_muxed); + void OnRoleConflict(PortInterface* port); + void OnPortDestroyed(PortInterface* port); + + PortInterface* impl_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_PORTPROXY_H_ diff --git a/talk/p2p/base/pseudotcp.cc b/talk/p2p/base/pseudotcp.cc new file mode 100644 index 000000000..2cf2799c0 --- /dev/null +++ b/talk/p2p/base/pseudotcp.cc @@ -0,0 +1,1296 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/pseudotcp.h" + +#include +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/bytebuffer.h" +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/socket.h" +#include "talk/base/stringutils.h" +#include "talk/base/timeutils.h" + +// The following logging is for detailed (packet-level) analysis only. +#define _DBG_NONE 0 +#define _DBG_NORMAL 1 +#define _DBG_VERBOSE 2 +#define _DEBUGMSG _DBG_NONE + +namespace cricket { + +////////////////////////////////////////////////////////////////////// +// Network Constants +////////////////////////////////////////////////////////////////////// + +// Standard MTUs +const uint16 PACKET_MAXIMUMS[] = { + 65535, // Theoretical maximum, Hyperchannel + 32000, // Nothing + 17914, // 16Mb IBM Token Ring + 8166, // IEEE 802.4 + //4464, // IEEE 802.5 (4Mb max) + 4352, // FDDI + //2048, // Wideband Network + 2002, // IEEE 802.5 (4Mb recommended) + //1536, // Expermental Ethernet Networks + //1500, // Ethernet, Point-to-Point (default) + 1492, // IEEE 802.3 + 1006, // SLIP, ARPANET + //576, // X.25 Networks + //544, // DEC IP Portal + //512, // NETBIOS + 508, // IEEE 802/Source-Rt Bridge, ARCNET + 296, // Point-to-Point (low delay) + //68, // Official minimum + 0, // End of list marker +}; + +const uint32 MAX_PACKET = 65535; +// Note: we removed lowest level because packet overhead was larger! +const uint32 MIN_PACKET = 296; + +const uint32 IP_HEADER_SIZE = 20; // (+ up to 40 bytes of options?) +const uint32 ICMP_HEADER_SIZE = 8; +const uint32 UDP_HEADER_SIZE = 8; +// TODO: Make JINGLE_HEADER_SIZE transparent to this code? +const uint32 JINGLE_HEADER_SIZE = 64; // when relay framing is in use + +// Default size for receive and send buffer. +const uint32 DEFAULT_RCV_BUF_SIZE = 60 * 1024; +const uint32 DEFAULT_SND_BUF_SIZE = 90 * 1024; + +////////////////////////////////////////////////////////////////////// +// Global Constants and Functions +////////////////////////////////////////////////////////////////////// +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// 0 | Conversation Number | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// 4 | Sequence Number | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// 8 | Acknowledgment Number | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | | |U|A|P|R|S|F| | +// 12 | Control | |R|C|S|S|Y|I| Window | +// | | |G|K|H|T|N|N| | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// 16 | Timestamp sending | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// 20 | Timestamp receiving | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// 24 | data | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +////////////////////////////////////////////////////////////////////// + +#define PSEUDO_KEEPALIVE 0 + +const uint32 MAX_SEQ = 0xFFFFFFFF; +const uint32 HEADER_SIZE = 24; +const uint32 PACKET_OVERHEAD = HEADER_SIZE + UDP_HEADER_SIZE + IP_HEADER_SIZE + JINGLE_HEADER_SIZE; + +const uint32 MIN_RTO = 250; // 250 ms (RFC1122, Sec 4.2.3.1 "fractions of a second") +const uint32 DEF_RTO = 3000; // 3 seconds (RFC1122, Sec 4.2.3.1) +const uint32 MAX_RTO = 60000; // 60 seconds +const uint32 DEF_ACK_DELAY = 100; // 100 milliseconds + +const uint8 FLAG_CTL = 0x02; +const uint8 FLAG_RST = 0x04; + +const uint8 CTL_CONNECT = 0; +//const uint8 CTL_REDIRECT = 1; +const uint8 CTL_EXTRA = 255; + +// TCP options. +const uint8 TCP_OPT_EOL = 0; // End of list. +const uint8 TCP_OPT_NOOP = 1; // No-op. +const uint8 TCP_OPT_MSS = 2; // Maximum segment size. +const uint8 TCP_OPT_WND_SCALE = 3; // Window scale factor. + +/* +const uint8 FLAG_FIN = 0x01; +const uint8 FLAG_SYN = 0x02; +const uint8 FLAG_ACK = 0x10; +*/ + +const uint32 CTRL_BOUND = 0x80000000; + +const long DEFAULT_TIMEOUT = 4000; // If there are no pending clocks, wake up every 4 seconds +const long CLOSED_TIMEOUT = 60 * 1000; // If the connection is closed, once per minute + +#if PSEUDO_KEEPALIVE +// !?! Rethink these times +const uint32 IDLE_PING = 20 * 1000; // 20 seconds (note: WinXP SP2 firewall udp timeout is 90 seconds) +const uint32 IDLE_TIMEOUT = 90 * 1000; // 90 seconds; +#endif // PSEUDO_KEEPALIVE + +////////////////////////////////////////////////////////////////////// +// Helper Functions +////////////////////////////////////////////////////////////////////// + +inline void long_to_bytes(uint32 val, void* buf) { + *static_cast(buf) = talk_base::HostToNetwork32(val); +} + +inline void short_to_bytes(uint16 val, void* buf) { + *static_cast(buf) = talk_base::HostToNetwork16(val); +} + +inline uint32 bytes_to_long(const void* buf) { + return talk_base::NetworkToHost32(*static_cast(buf)); +} + +inline uint16 bytes_to_short(const void* buf) { + return talk_base::NetworkToHost16(*static_cast(buf)); +} + +uint32 bound(uint32 lower, uint32 middle, uint32 upper) { + return talk_base::_min(talk_base::_max(lower, middle), upper); +} + +////////////////////////////////////////////////////////////////////// +// Debugging Statistics +////////////////////////////////////////////////////////////////////// + +#if 0 // Not used yet + +enum Stat { + S_SENT_PACKET, // All packet sends + S_RESENT_PACKET, // All packet sends that are retransmits + S_RECV_PACKET, // All packet receives + S_RECV_NEW, // All packet receives that are too new + S_RECV_OLD, // All packet receives that are too old + S_NUM_STATS +}; + +const char* const STAT_NAMES[S_NUM_STATS] = { + "snt", + "snt-r", + "rcv" + "rcv-n", + "rcv-o" +}; + +int g_stats[S_NUM_STATS]; +inline void Incr(Stat s) { ++g_stats[s]; } +void ReportStats() { + char buffer[256]; + size_t len = 0; + for (int i = 0; i < S_NUM_STATS; ++i) { + len += talk_base::sprintfn(buffer, ARRAY_SIZE(buffer), "%s%s:%d", + (i == 0) ? "" : ",", STAT_NAMES[i], g_stats[i]); + g_stats[i] = 0; + } + LOG(LS_INFO) << "Stats[" << buffer << "]"; +} + +#endif + +////////////////////////////////////////////////////////////////////// +// PseudoTcp +////////////////////////////////////////////////////////////////////// + +uint32 PseudoTcp::Now() { +#if 0 // Use this to synchronize timers with logging timestamps (easier debug) + return talk_base::TimeSince(StartTime()); +#else + return talk_base::Time(); +#endif +} + +PseudoTcp::PseudoTcp(IPseudoTcpNotify* notify, uint32 conv) + : m_notify(notify), + m_shutdown(SD_NONE), + m_error(0), + m_rbuf_len(DEFAULT_RCV_BUF_SIZE), + m_rbuf(m_rbuf_len), + m_sbuf_len(DEFAULT_SND_BUF_SIZE), + m_sbuf(m_sbuf_len) { + + // Sanity check on buffer sizes (needed for OnTcpWriteable notification logic) + ASSERT(m_rbuf_len + MIN_PACKET < m_sbuf_len); + + uint32 now = Now(); + + m_state = TCP_LISTEN; + m_conv = conv; + m_rcv_wnd = m_rbuf_len; + m_rwnd_scale = m_swnd_scale = 0; + m_snd_nxt = 0; + m_snd_wnd = 1; + m_snd_una = m_rcv_nxt = 0; + m_bReadEnable = true; + m_bWriteEnable = false; + m_t_ack = 0; + + m_msslevel = 0; + m_largest = 0; + ASSERT(MIN_PACKET > PACKET_OVERHEAD); + m_mss = MIN_PACKET - PACKET_OVERHEAD; + m_mtu_advise = MAX_PACKET; + + m_rto_base = 0; + + m_cwnd = 2 * m_mss; + m_ssthresh = m_rbuf_len; + m_lastrecv = m_lastsend = m_lasttraffic = now; + m_bOutgoing = false; + + m_dup_acks = 0; + m_recover = 0; + + m_ts_recent = m_ts_lastack = 0; + + m_rx_rto = DEF_RTO; + m_rx_srtt = m_rx_rttvar = 0; + + m_use_nagling = true; + m_ack_delay = DEF_ACK_DELAY; + m_support_wnd_scale = true; +} + +PseudoTcp::~PseudoTcp() { +} + +int PseudoTcp::Connect() { + if (m_state != TCP_LISTEN) { + m_error = EINVAL; + return -1; + } + + m_state = TCP_SYN_SENT; + LOG(LS_INFO) << "State: TCP_SYN_SENT"; + + queueConnectMessage(); + attemptSend(); + + return 0; +} + +void PseudoTcp::NotifyMTU(uint16 mtu) { + m_mtu_advise = mtu; + if (m_state == TCP_ESTABLISHED) { + adjustMTU(); + } +} + +void PseudoTcp::NotifyClock(uint32 now) { + if (m_state == TCP_CLOSED) + return; + + // Check if it's time to retransmit a segment + if (m_rto_base && (talk_base::TimeDiff(m_rto_base + m_rx_rto, now) <= 0)) { + if (m_slist.empty()) { + ASSERT(false); + } else { + // Note: (m_slist.front().xmit == 0)) { + // retransmit segments +#if _DEBUGMSG >= _DBG_NORMAL + LOG(LS_INFO) << "timeout retransmit (rto: " << m_rx_rto + << ") (rto_base: " << m_rto_base + << ") (now: " << now + << ") (dup_acks: " << static_cast(m_dup_acks) + << ")"; +#endif // _DEBUGMSG + if (!transmit(m_slist.begin(), now)) { + closedown(ECONNABORTED); + return; + } + + uint32 nInFlight = m_snd_nxt - m_snd_una; + m_ssthresh = talk_base::_max(nInFlight / 2, 2 * m_mss); + //LOG(LS_INFO) << "m_ssthresh: " << m_ssthresh << " nInFlight: " << nInFlight << " m_mss: " << m_mss; + m_cwnd = m_mss; + + // Back off retransmit timer. Note: the limit is lower when connecting. + uint32 rto_limit = (m_state < TCP_ESTABLISHED) ? DEF_RTO : MAX_RTO; + m_rx_rto = talk_base::_min(rto_limit, m_rx_rto * 2); + m_rto_base = now; + } + } + + // Check if it's time to probe closed windows + if ((m_snd_wnd == 0) + && (talk_base::TimeDiff(m_lastsend + m_rx_rto, now) <= 0)) { + if (talk_base::TimeDiff(now, m_lastrecv) >= 15000) { + closedown(ECONNABORTED); + return; + } + + // probe the window + packet(m_snd_nxt - 1, 0, 0, 0); + m_lastsend = now; + + // back off retransmit timer + m_rx_rto = talk_base::_min(MAX_RTO, m_rx_rto * 2); + } + + // Check if it's time to send delayed acks + if (m_t_ack && (talk_base::TimeDiff(m_t_ack + m_ack_delay, now) <= 0)) { + packet(m_snd_nxt, 0, 0, 0); + } + +#if PSEUDO_KEEPALIVE + // Check for idle timeout + if ((m_state == TCP_ESTABLISHED) && (TimeDiff(m_lastrecv + IDLE_TIMEOUT, now) <= 0)) { + closedown(ECONNABORTED); + return; + } + + // Check for ping timeout (to keep udp mapping open) + if ((m_state == TCP_ESTABLISHED) && (TimeDiff(m_lasttraffic + (m_bOutgoing ? IDLE_PING * 3/2 : IDLE_PING), now) <= 0)) { + packet(m_snd_nxt, 0, 0, 0); + } +#endif // PSEUDO_KEEPALIVE +} + +bool PseudoTcp::NotifyPacket(const char* buffer, size_t len) { + if (len > MAX_PACKET) { + LOG_F(WARNING) << "packet too large"; + return false; + } + return parse(reinterpret_cast(buffer), uint32(len)); +} + +bool PseudoTcp::GetNextClock(uint32 now, long& timeout) { + return clock_check(now, timeout); +} + +void PseudoTcp::GetOption(Option opt, int* value) { + if (opt == OPT_NODELAY) { + *value = m_use_nagling ? 0 : 1; + } else if (opt == OPT_ACKDELAY) { + *value = m_ack_delay; + } else if (opt == OPT_SNDBUF) { + *value = m_sbuf_len; + } else if (opt == OPT_RCVBUF) { + *value = m_rbuf_len; + } else { + ASSERT(false); + } +} +void PseudoTcp::SetOption(Option opt, int value) { + if (opt == OPT_NODELAY) { + m_use_nagling = value == 0; + } else if (opt == OPT_ACKDELAY) { + m_ack_delay = value; + } else if (opt == OPT_SNDBUF) { + ASSERT(m_state == TCP_LISTEN); + resizeSendBuffer(value); + } else if (opt == OPT_RCVBUF) { + ASSERT(m_state == TCP_LISTEN); + resizeReceiveBuffer(value); + } else { + ASSERT(false); + } +} + +uint32 PseudoTcp::GetCongestionWindow() const { + return m_cwnd; +} + +uint32 PseudoTcp::GetBytesInFlight() const { + return m_snd_nxt - m_snd_una; +} + +uint32 PseudoTcp::GetBytesBufferedNotSent() const { + size_t buffered_bytes = 0; + m_sbuf.GetBuffered(&buffered_bytes); + return m_snd_una + buffered_bytes - m_snd_nxt; +} + +uint32 PseudoTcp::GetRoundTripTimeEstimateMs() const { + return m_rx_srtt; +} + +// +// IPStream Implementation +// + +int PseudoTcp::Recv(char* buffer, size_t len) { + if (m_state != TCP_ESTABLISHED) { + m_error = ENOTCONN; + return SOCKET_ERROR; + } + + size_t read = 0; + talk_base::StreamResult result = m_rbuf.Read(buffer, len, &read, NULL); + + // If there's no data in |m_rbuf|. + if (result == talk_base::SR_BLOCK) { + m_bReadEnable = true; + m_error = EWOULDBLOCK; + return SOCKET_ERROR; + } + ASSERT(result == talk_base::SR_SUCCESS); + + size_t available_space = 0; + m_rbuf.GetWriteRemaining(&available_space); + + if (uint32(available_space) - m_rcv_wnd >= + talk_base::_min(m_rbuf_len / 2, m_mss)) { + bool bWasClosed = (m_rcv_wnd == 0); // !?! Not sure about this was closed business + m_rcv_wnd = available_space; + + if (bWasClosed) { + attemptSend(sfImmediateAck); + } + } + + return read; +} + +int PseudoTcp::Send(const char* buffer, size_t len) { + if (m_state != TCP_ESTABLISHED) { + m_error = ENOTCONN; + return SOCKET_ERROR; + } + + size_t available_space = 0; + m_sbuf.GetWriteRemaining(&available_space); + + if (!available_space) { + m_bWriteEnable = true; + m_error = EWOULDBLOCK; + return SOCKET_ERROR; + } + + int written = queue(buffer, uint32(len), false); + attemptSend(); + return written; +} + +void PseudoTcp::Close(bool force) { + LOG_F(LS_VERBOSE) << "(" << (force ? "true" : "false") << ")"; + m_shutdown = force ? SD_FORCEFUL : SD_GRACEFUL; +} + +int PseudoTcp::GetError() { + return m_error; +} + +// +// Internal Implementation +// + +uint32 PseudoTcp::queue(const char* data, uint32 len, bool bCtrl) { + size_t available_space = 0; + m_sbuf.GetWriteRemaining(&available_space); + + if (len > static_cast(available_space)) { + ASSERT(!bCtrl); + len = static_cast(available_space); + } + + // We can concatenate data if the last segment is the same type + // (control v. regular data), and has not been transmitted yet + if (!m_slist.empty() && (m_slist.back().bCtrl == bCtrl) && (m_slist.back().xmit == 0)) { + m_slist.back().len += len; + } else { + size_t snd_buffered = 0; + m_sbuf.GetBuffered(&snd_buffered); + SSegment sseg(m_snd_una + snd_buffered, len, bCtrl); + m_slist.push_back(sseg); + } + + size_t written = 0; + m_sbuf.Write(data, len, &written, NULL); + return written; +} + +IPseudoTcpNotify::WriteResult PseudoTcp::packet(uint32 seq, uint8 flags, + uint32 offset, uint32 len) { + ASSERT(HEADER_SIZE + len <= MAX_PACKET); + + uint32 now = Now(); + + uint8 buffer[MAX_PACKET]; + long_to_bytes(m_conv, buffer); + long_to_bytes(seq, buffer + 4); + long_to_bytes(m_rcv_nxt, buffer + 8); + buffer[12] = 0; + buffer[13] = flags; + short_to_bytes(static_cast(m_rcv_wnd >> m_rwnd_scale), buffer + 14); + + // Timestamp computations + long_to_bytes(now, buffer + 16); + long_to_bytes(m_ts_recent, buffer + 20); + m_ts_lastack = m_rcv_nxt; + + if (len) { + size_t bytes_read = 0; + talk_base::StreamResult result = m_sbuf.ReadOffset(buffer + HEADER_SIZE, + len, + offset, + &bytes_read); + UNUSED(result); + ASSERT(result == talk_base::SR_SUCCESS); + ASSERT(static_cast(bytes_read) == len); + } + +#if _DEBUGMSG >= _DBG_VERBOSE + LOG(LS_INFO) << "<-- (flags) + << ">"; +#endif // _DEBUGMSG + + IPseudoTcpNotify::WriteResult wres = m_notify->TcpWritePacket(this, reinterpret_cast(buffer), len + HEADER_SIZE); + // Note: When len is 0, this is an ACK packet. We don't read the return value for those, + // and thus we won't retry. So go ahead and treat the packet as a success (basically simulate + // as if it were dropped), which will prevent our timers from being messed up. + if ((wres != IPseudoTcpNotify::WR_SUCCESS) && (0 != len)) + return wres; + + m_t_ack = 0; + if (len > 0) { + m_lastsend = now; + } + m_lasttraffic = now; + m_bOutgoing = true; + + return IPseudoTcpNotify::WR_SUCCESS; +} + +bool PseudoTcp::parse(const uint8* buffer, uint32 size) { + if (size < 12) + return false; + + Segment seg; + seg.conv = bytes_to_long(buffer); + seg.seq = bytes_to_long(buffer + 4); + seg.ack = bytes_to_long(buffer + 8); + seg.flags = buffer[13]; + seg.wnd = bytes_to_short(buffer + 14); + + seg.tsval = bytes_to_long(buffer + 16); + seg.tsecr = bytes_to_long(buffer + 20); + + seg.data = reinterpret_cast(buffer) + HEADER_SIZE; + seg.len = size - HEADER_SIZE; + +#if _DEBUGMSG >= _DBG_VERBOSE + LOG(LS_INFO) << "--> (seg.flags) + << ">"; +#endif // _DEBUGMSG + + return process(seg); +} + +bool PseudoTcp::clock_check(uint32 now, long& nTimeout) { + if (m_shutdown == SD_FORCEFUL) + return false; + + size_t snd_buffered = 0; + m_sbuf.GetBuffered(&snd_buffered); + if ((m_shutdown == SD_GRACEFUL) + && ((m_state != TCP_ESTABLISHED) + || ((snd_buffered == 0) && (m_t_ack == 0)))) { + return false; + } + + if (m_state == TCP_CLOSED) { + nTimeout = CLOSED_TIMEOUT; + return true; + } + + nTimeout = DEFAULT_TIMEOUT; + + if (m_t_ack) { + nTimeout = talk_base::_min(nTimeout, + talk_base::TimeDiff(m_t_ack + m_ack_delay, now)); + } + if (m_rto_base) { + nTimeout = talk_base::_min(nTimeout, + talk_base::TimeDiff(m_rto_base + m_rx_rto, now)); + } + if (m_snd_wnd == 0) { + nTimeout = talk_base::_min(nTimeout, talk_base::TimeDiff(m_lastsend + m_rx_rto, now)); + } +#if PSEUDO_KEEPALIVE + if (m_state == TCP_ESTABLISHED) { + nTimeout = talk_base::_min(nTimeout, + talk_base::TimeDiff(m_lasttraffic + (m_bOutgoing ? IDLE_PING * 3/2 : IDLE_PING), now)); + } +#endif // PSEUDO_KEEPALIVE + return true; +} + +bool PseudoTcp::process(Segment& seg) { + // If this is the wrong conversation, send a reset!?! (with the correct conversation?) + if (seg.conv != m_conv) { + //if ((seg.flags & FLAG_RST) == 0) { + // packet(tcb, seg.ack, 0, FLAG_RST, 0, 0); + //} + LOG_F(LS_ERROR) << "wrong conversation"; + return false; + } + + uint32 now = Now(); + m_lasttraffic = m_lastrecv = now; + m_bOutgoing = false; + + if (m_state == TCP_CLOSED) { + // !?! send reset? + LOG_F(LS_ERROR) << "closed"; + return false; + } + + // Check if this is a reset segment + if (seg.flags & FLAG_RST) { + closedown(ECONNRESET); + return false; + } + + // Check for control data + bool bConnect = false; + if (seg.flags & FLAG_CTL) { + if (seg.len == 0) { + LOG_F(LS_ERROR) << "Missing control code"; + return false; + } else if (seg.data[0] == CTL_CONNECT) { + bConnect = true; + + // TCP options are in the remainder of the payload after CTL_CONNECT. + parseOptions(&seg.data[1], seg.len - 1); + + if (m_state == TCP_LISTEN) { + m_state = TCP_SYN_RECEIVED; + LOG(LS_INFO) << "State: TCP_SYN_RECEIVED"; + //m_notify->associate(addr); + queueConnectMessage(); + } else if (m_state == TCP_SYN_SENT) { + m_state = TCP_ESTABLISHED; + LOG(LS_INFO) << "State: TCP_ESTABLISHED"; + adjustMTU(); + if (m_notify) { + m_notify->OnTcpOpen(this); + } + //notify(evOpen); + } + } else { + LOG_F(LS_WARNING) << "Unknown control code: " << seg.data[0]; + return false; + } + } + + // Update timestamp + if ((seg.seq <= m_ts_lastack) && (m_ts_lastack < seg.seq + seg.len)) { + m_ts_recent = seg.tsval; + } + + // Check if this is a valuable ack + if ((seg.ack > m_snd_una) && (seg.ack <= m_snd_nxt)) { + // Calculate round-trip time + if (seg.tsecr) { + long rtt = talk_base::TimeDiff(now, seg.tsecr); + if (rtt >= 0) { + if (m_rx_srtt == 0) { + m_rx_srtt = rtt; + m_rx_rttvar = rtt / 2; + } else { + m_rx_rttvar = (3 * m_rx_rttvar + abs(long(rtt - m_rx_srtt))) / 4; + m_rx_srtt = (7 * m_rx_srtt + rtt) / 8; + } + m_rx_rto = bound(MIN_RTO, m_rx_srtt + + talk_base::_max(1, 4 * m_rx_rttvar), MAX_RTO); +#if _DEBUGMSG >= _DBG_VERBOSE + LOG(LS_INFO) << "rtt: " << rtt + << " srtt: " << m_rx_srtt + << " rto: " << m_rx_rto; +#endif // _DEBUGMSG + } else { + ASSERT(false); + } + } + + m_snd_wnd = static_cast(seg.wnd) << m_swnd_scale; + + uint32 nAcked = seg.ack - m_snd_una; + m_snd_una = seg.ack; + + m_rto_base = (m_snd_una == m_snd_nxt) ? 0 : now; + + m_sbuf.ConsumeReadData(nAcked); + + for (uint32 nFree = nAcked; nFree > 0; ) { + ASSERT(!m_slist.empty()); + if (nFree < m_slist.front().len) { + m_slist.front().len -= nFree; + nFree = 0; + } else { + if (m_slist.front().len > m_largest) { + m_largest = m_slist.front().len; + } + nFree -= m_slist.front().len; + m_slist.pop_front(); + } + } + + if (m_dup_acks >= 3) { + if (m_snd_una >= m_recover) { // NewReno + uint32 nInFlight = m_snd_nxt - m_snd_una; + m_cwnd = talk_base::_min(m_ssthresh, nInFlight + m_mss); // (Fast Retransmit) +#if _DEBUGMSG >= _DBG_NORMAL + LOG(LS_INFO) << "exit recovery"; +#endif // _DEBUGMSG + m_dup_acks = 0; + } else { +#if _DEBUGMSG >= _DBG_NORMAL + LOG(LS_INFO) << "recovery retransmit"; +#endif // _DEBUGMSG + if (!transmit(m_slist.begin(), now)) { + closedown(ECONNABORTED); + return false; + } + m_cwnd += m_mss - talk_base::_min(nAcked, m_cwnd); + } + } else { + m_dup_acks = 0; + // Slow start, congestion avoidance + if (m_cwnd < m_ssthresh) { + m_cwnd += m_mss; + } else { + m_cwnd += talk_base::_max(1, m_mss * m_mss / m_cwnd); + } + } + } else if (seg.ack == m_snd_una) { + // !?! Note, tcp says don't do this... but otherwise how does a closed window become open? + m_snd_wnd = static_cast(seg.wnd) << m_swnd_scale; + + // Check duplicate acks + if (seg.len > 0) { + // it's a dup ack, but with a data payload, so don't modify m_dup_acks + } else if (m_snd_una != m_snd_nxt) { + m_dup_acks += 1; + if (m_dup_acks == 3) { // (Fast Retransmit) +#if _DEBUGMSG >= _DBG_NORMAL + LOG(LS_INFO) << "enter recovery"; + LOG(LS_INFO) << "recovery retransmit"; +#endif // _DEBUGMSG + if (!transmit(m_slist.begin(), now)) { + closedown(ECONNABORTED); + return false; + } + m_recover = m_snd_nxt; + uint32 nInFlight = m_snd_nxt - m_snd_una; + m_ssthresh = talk_base::_max(nInFlight / 2, 2 * m_mss); + //LOG(LS_INFO) << "m_ssthresh: " << m_ssthresh << " nInFlight: " << nInFlight << " m_mss: " << m_mss; + m_cwnd = m_ssthresh + 3 * m_mss; + } else if (m_dup_acks > 3) { + m_cwnd += m_mss; + } + } else { + m_dup_acks = 0; + } + } + + // !?! A bit hacky + if ((m_state == TCP_SYN_RECEIVED) && !bConnect) { + m_state = TCP_ESTABLISHED; + LOG(LS_INFO) << "State: TCP_ESTABLISHED"; + adjustMTU(); + if (m_notify) { + m_notify->OnTcpOpen(this); + } + //notify(evOpen); + } + + // If we make room in the send queue, notify the user + // The goal it to make sure we always have at least enough data to fill the + // window. We'd like to notify the app when we are halfway to that point. + const uint32 kIdealRefillSize = (m_sbuf_len + m_rbuf_len) / 2; + size_t snd_buffered = 0; + m_sbuf.GetBuffered(&snd_buffered); + if (m_bWriteEnable && static_cast(snd_buffered) < kIdealRefillSize) { + m_bWriteEnable = false; + if (m_notify) { + m_notify->OnTcpWriteable(this); + } + //notify(evWrite); + } + + // Conditions were acks must be sent: + // 1) Segment is too old (they missed an ACK) (immediately) + // 2) Segment is too new (we missed a segment) (immediately) + // 3) Segment has data (so we need to ACK!) (delayed) + // ... so the only time we don't need to ACK, is an empty segment that points to rcv_nxt! + + SendFlags sflags = sfNone; + if (seg.seq != m_rcv_nxt) { + sflags = sfImmediateAck; // (Fast Recovery) + } else if (seg.len != 0) { + if (m_ack_delay == 0) { + sflags = sfImmediateAck; + } else { + sflags = sfDelayedAck; + } + } +#if _DEBUGMSG >= _DBG_NORMAL + if (sflags == sfImmediateAck) { + if (seg.seq > m_rcv_nxt) { + LOG_F(LS_INFO) << "too new"; + } else if (seg.seq + seg.len <= m_rcv_nxt) { + LOG_F(LS_INFO) << "too old"; + } + } +#endif // _DEBUGMSG + + // Adjust the incoming segment to fit our receive buffer + if (seg.seq < m_rcv_nxt) { + uint32 nAdjust = m_rcv_nxt - seg.seq; + if (nAdjust < seg.len) { + seg.seq += nAdjust; + seg.data += nAdjust; + seg.len -= nAdjust; + } else { + seg.len = 0; + } + } + + size_t available_space = 0; + m_rbuf.GetWriteRemaining(&available_space); + + if ((seg.seq + seg.len - m_rcv_nxt) > static_cast(available_space)) { + uint32 nAdjust = seg.seq + seg.len - m_rcv_nxt - static_cast(available_space); + if (nAdjust < seg.len) { + seg.len -= nAdjust; + } else { + seg.len = 0; + } + } + + bool bIgnoreData = (seg.flags & FLAG_CTL) || (m_shutdown != SD_NONE); + bool bNewData = false; + + if (seg.len > 0) { + if (bIgnoreData) { + if (seg.seq == m_rcv_nxt) { + m_rcv_nxt += seg.len; + } + } else { + uint32 nOffset = seg.seq - m_rcv_nxt; + + talk_base::StreamResult result = m_rbuf.WriteOffset(seg.data, seg.len, + nOffset, NULL); + ASSERT(result == talk_base::SR_SUCCESS); + UNUSED(result); + + if (seg.seq == m_rcv_nxt) { + m_rbuf.ConsumeWriteBuffer(seg.len); + m_rcv_nxt += seg.len; + m_rcv_wnd -= seg.len; + bNewData = true; + + RList::iterator it = m_rlist.begin(); + while ((it != m_rlist.end()) && (it->seq <= m_rcv_nxt)) { + if (it->seq + it->len > m_rcv_nxt) { + sflags = sfImmediateAck; // (Fast Recovery) + uint32 nAdjust = (it->seq + it->len) - m_rcv_nxt; +#if _DEBUGMSG >= _DBG_NORMAL + LOG(LS_INFO) << "Recovered " << nAdjust << " bytes (" << m_rcv_nxt << " -> " << m_rcv_nxt + nAdjust << ")"; +#endif // _DEBUGMSG + m_rbuf.ConsumeWriteBuffer(nAdjust); + m_rcv_nxt += nAdjust; + m_rcv_wnd -= nAdjust; + } + it = m_rlist.erase(it); + } + } else { +#if _DEBUGMSG >= _DBG_NORMAL + LOG(LS_INFO) << "Saving " << seg.len << " bytes (" << seg.seq << " -> " << seg.seq + seg.len << ")"; +#endif // _DEBUGMSG + RSegment rseg; + rseg.seq = seg.seq; + rseg.len = seg.len; + RList::iterator it = m_rlist.begin(); + while ((it != m_rlist.end()) && (it->seq < rseg.seq)) { + ++it; + } + m_rlist.insert(it, rseg); + } + } + } + + attemptSend(sflags); + + // If we have new data, notify the user + if (bNewData && m_bReadEnable) { + m_bReadEnable = false; + if (m_notify) { + m_notify->OnTcpReadable(this); + } + //notify(evRead); + } + + return true; +} + +bool PseudoTcp::transmit(const SList::iterator& seg, uint32 now) { + if (seg->xmit >= ((m_state == TCP_ESTABLISHED) ? 15 : 30)) { + LOG_F(LS_VERBOSE) << "too many retransmits"; + return false; + } + + uint32 nTransmit = talk_base::_min(seg->len, m_mss); + + while (true) { + uint32 seq = seg->seq; + uint8 flags = (seg->bCtrl ? FLAG_CTL : 0); + IPseudoTcpNotify::WriteResult wres = packet(seq, + flags, + seg->seq - m_snd_una, + nTransmit); + + if (wres == IPseudoTcpNotify::WR_SUCCESS) + break; + + if (wres == IPseudoTcpNotify::WR_FAIL) { + LOG_F(LS_VERBOSE) << "packet failed"; + return false; + } + + ASSERT(wres == IPseudoTcpNotify::WR_TOO_LARGE); + + while (true) { + if (PACKET_MAXIMUMS[m_msslevel + 1] == 0) { + LOG_F(LS_VERBOSE) << "MTU too small"; + return false; + } + // !?! We need to break up all outstanding and pending packets and then retransmit!?! + + m_mss = PACKET_MAXIMUMS[++m_msslevel] - PACKET_OVERHEAD; + m_cwnd = 2 * m_mss; // I added this... haven't researched actual formula + if (m_mss < nTransmit) { + nTransmit = m_mss; + break; + } + } +#if _DEBUGMSG >= _DBG_NORMAL + LOG(LS_INFO) << "Adjusting mss to " << m_mss << " bytes"; +#endif // _DEBUGMSG + } + + if (nTransmit < seg->len) { + LOG_F(LS_VERBOSE) << "mss reduced to " << m_mss; + + SSegment subseg(seg->seq + nTransmit, seg->len - nTransmit, seg->bCtrl); + //subseg.tstamp = seg->tstamp; + subseg.xmit = seg->xmit; + seg->len = nTransmit; + + SList::iterator next = seg; + m_slist.insert(++next, subseg); + } + + if (seg->xmit == 0) { + m_snd_nxt += seg->len; + } + seg->xmit += 1; + //seg->tstamp = now; + if (m_rto_base == 0) { + m_rto_base = now; + } + + return true; +} + +void PseudoTcp::attemptSend(SendFlags sflags) { + uint32 now = Now(); + + if (talk_base::TimeDiff(now, m_lastsend) > static_cast(m_rx_rto)) { + m_cwnd = m_mss; + } + +#if _DEBUGMSG + bool bFirst = true; + UNUSED(bFirst); +#endif // _DEBUGMSG + + while (true) { + uint32 cwnd = m_cwnd; + if ((m_dup_acks == 1) || (m_dup_acks == 2)) { // Limited Transmit + cwnd += m_dup_acks * m_mss; + } + uint32 nWindow = talk_base::_min(m_snd_wnd, cwnd); + uint32 nInFlight = m_snd_nxt - m_snd_una; + uint32 nUseable = (nInFlight < nWindow) ? (nWindow - nInFlight) : 0; + + size_t snd_buffered = 0; + m_sbuf.GetBuffered(&snd_buffered); + uint32 nAvailable = + talk_base::_min(static_cast(snd_buffered) - nInFlight, m_mss); + + if (nAvailable > nUseable) { + if (nUseable * 4 < nWindow) { + // RFC 813 - avoid SWS + nAvailable = 0; + } else { + nAvailable = nUseable; + } + } + +#if _DEBUGMSG >= _DBG_VERBOSE + if (bFirst) { + size_t available_space = 0; + m_sbuf.GetWriteRemaining(&available_space); + + bFirst = false; + LOG(LS_INFO) << "[cwnd: " << m_cwnd + << " nWindow: " << nWindow + << " nInFlight: " << nInFlight + << " nAvailable: " << nAvailable + << " nQueued: " << snd_buffered + << " nEmpty: " << available_space + << " ssthresh: " << m_ssthresh << "]"; + } +#endif // _DEBUGMSG + + if (nAvailable == 0) { + if (sflags == sfNone) + return; + + // If this is an immediate ack, or the second delayed ack + if ((sflags == sfImmediateAck) || m_t_ack) { + packet(m_snd_nxt, 0, 0, 0); + } else { + m_t_ack = Now(); + } + return; + } + + // Nagle's algorithm. + // If there is data already in-flight, and we haven't a full segment of + // data ready to send then hold off until we get more to send, or the + // in-flight data is acknowledged. + if (m_use_nagling && (m_snd_nxt > m_snd_una) && (nAvailable < m_mss)) { + return; + } + + // Find the next segment to transmit + SList::iterator it = m_slist.begin(); + while (it->xmit > 0) { + ++it; + ASSERT(it != m_slist.end()); + } + SList::iterator seg = it; + + // If the segment is too large, break it into two + if (seg->len > nAvailable) { + SSegment subseg(seg->seq + nAvailable, seg->len - nAvailable, seg->bCtrl); + seg->len = nAvailable; + m_slist.insert(++it, subseg); + } + + if (!transmit(seg, now)) { + LOG_F(LS_VERBOSE) << "transmit failed"; + // TODO: consider closing socket + return; + } + + sflags = sfNone; + } +} + +void +PseudoTcp::closedown(uint32 err) { + LOG(LS_INFO) << "State: TCP_CLOSED"; + m_state = TCP_CLOSED; + if (m_notify) { + m_notify->OnTcpClosed(this, err); + } + //notify(evClose, err); +} + +void +PseudoTcp::adjustMTU() { + // Determine our current mss level, so that we can adjust appropriately later + for (m_msslevel = 0; PACKET_MAXIMUMS[m_msslevel + 1] > 0; ++m_msslevel) { + if (static_cast(PACKET_MAXIMUMS[m_msslevel]) <= m_mtu_advise) { + break; + } + } + m_mss = m_mtu_advise - PACKET_OVERHEAD; + // !?! Should we reset m_largest here? +#if _DEBUGMSG >= _DBG_NORMAL + LOG(LS_INFO) << "Adjusting mss to " << m_mss << " bytes"; +#endif // _DEBUGMSG + // Enforce minimums on ssthresh and cwnd + m_ssthresh = talk_base::_max(m_ssthresh, 2 * m_mss); + m_cwnd = talk_base::_max(m_cwnd, m_mss); +} + +bool +PseudoTcp::isReceiveBufferFull() const { + size_t available_space = 0; + m_rbuf.GetWriteRemaining(&available_space); + return !available_space; +} + +void +PseudoTcp::disableWindowScale() { + m_support_wnd_scale = false; +} + +void +PseudoTcp::queueConnectMessage() { + talk_base::ByteBuffer buf(talk_base::ByteBuffer::ORDER_NETWORK); + + buf.WriteUInt8(CTL_CONNECT); + if (m_support_wnd_scale) { + buf.WriteUInt8(TCP_OPT_WND_SCALE); + buf.WriteUInt8(1); + buf.WriteUInt8(m_rwnd_scale); + } + m_snd_wnd = buf.Length(); + queue(buf.Data(), buf.Length(), true); +} + +void +PseudoTcp::parseOptions(const char* data, uint32 len) { + std::set options_specified; + + // See http://www.freesoft.org/CIE/Course/Section4/8.htm for + // parsing the options list. + talk_base::ByteBuffer buf(data, len); + while (buf.Length()) { + uint8 kind = TCP_OPT_EOL; + buf.ReadUInt8(&kind); + + if (kind == TCP_OPT_EOL) { + // End of option list. + break; + } else if (kind == TCP_OPT_NOOP) { + // No op. + continue; + } + + // Length of this option. + ASSERT(len != 0); + UNUSED(len); + uint8 opt_len = 0; + buf.ReadUInt8(&opt_len); + + // Content of this option. + if (opt_len <= buf.Length()) { + applyOption(kind, buf.Data(), opt_len); + buf.Consume(opt_len); + } else { + LOG(LS_ERROR) << "Invalid option length received."; + return; + } + options_specified.insert(kind); + } + + if (options_specified.find(TCP_OPT_WND_SCALE) == options_specified.end()) { + LOG(LS_WARNING) << "Peer doesn't support window scaling"; + + if (m_rwnd_scale > 0) { + // Peer doesn't support TCP options and window scaling. + // Revert receive buffer size to default value. + resizeReceiveBuffer(DEFAULT_RCV_BUF_SIZE); + m_swnd_scale = 0; + } + } +} + +void +PseudoTcp::applyOption(char kind, const char* data, uint32 len) { + if (kind == TCP_OPT_MSS) { + LOG(LS_WARNING) << "Peer specified MSS option which is not supported."; + // TODO: Implement. + } else if (kind == TCP_OPT_WND_SCALE) { + // Window scale factor. + // http://www.ietf.org/rfc/rfc1323.txt + if (len != 1) { + LOG_F(WARNING) << "Invalid window scale option received."; + return; + } + applyWindowScaleOption(data[0]); + } +} + +void +PseudoTcp::applyWindowScaleOption(uint8 scale_factor) { + m_swnd_scale = scale_factor; +} + +void +PseudoTcp::resizeSendBuffer(uint32 new_size) { + m_sbuf_len = new_size; + m_sbuf.SetCapacity(new_size); +} + +void +PseudoTcp::resizeReceiveBuffer(uint32 new_size) { + uint8 scale_factor = 0; + + // Determine the scale factor such that the scaled window size can fit + // in a 16-bit unsigned integer. + while (new_size > 0xFFFF) { + ++scale_factor; + new_size >>= 1; + } + + // Determine the proper size of the buffer. + new_size <<= scale_factor; + bool result = m_rbuf.SetCapacity(new_size); + + // Make sure the new buffer is large enough to contain data in the old + // buffer. This should always be true because this method is called either + // before connection is established or when peers are exchanging connect + // messages. + ASSERT(result); + UNUSED(result); + m_rbuf_len = new_size; + m_rwnd_scale = scale_factor; + m_ssthresh = new_size; + + size_t available_space = 0; + m_rbuf.GetWriteRemaining(&available_space); + m_rcv_wnd = available_space; +} + +} // namespace cricket diff --git a/talk/p2p/base/pseudotcp.h b/talk/p2p/base/pseudotcp.h new file mode 100644 index 000000000..edd861b1c --- /dev/null +++ b/talk/p2p/base/pseudotcp.h @@ -0,0 +1,258 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_PSEUDOTCP_H_ +#define TALK_P2P_BASE_PSEUDOTCP_H_ + +#include + +#include "talk/base/basictypes.h" +#include "talk/base/stream.h" + +namespace cricket { + +////////////////////////////////////////////////////////////////////// +// IPseudoTcpNotify +////////////////////////////////////////////////////////////////////// + +class PseudoTcp; + +class IPseudoTcpNotify { + public: + // Notification of tcp events + virtual void OnTcpOpen(PseudoTcp* tcp) = 0; + virtual void OnTcpReadable(PseudoTcp* tcp) = 0; + virtual void OnTcpWriteable(PseudoTcp* tcp) = 0; + virtual void OnTcpClosed(PseudoTcp* tcp, uint32 error) = 0; + + // Write the packet onto the network + enum WriteResult { WR_SUCCESS, WR_TOO_LARGE, WR_FAIL }; + virtual WriteResult TcpWritePacket(PseudoTcp* tcp, + const char* buffer, size_t len) = 0; + + protected: + virtual ~IPseudoTcpNotify() {} +}; + +////////////////////////////////////////////////////////////////////// +// PseudoTcp +////////////////////////////////////////////////////////////////////// + +class PseudoTcp { + public: + static uint32 Now(); + + PseudoTcp(IPseudoTcpNotify* notify, uint32 conv); + virtual ~PseudoTcp(); + + int Connect(); + int Recv(char* buffer, size_t len); + int Send(const char* buffer, size_t len); + void Close(bool force); + int GetError(); + + enum TcpState { + TCP_LISTEN, TCP_SYN_SENT, TCP_SYN_RECEIVED, TCP_ESTABLISHED, TCP_CLOSED + }; + TcpState State() const { return m_state; } + + // Call this when the PMTU changes. + void NotifyMTU(uint16 mtu); + + // Call this based on timeout value returned from GetNextClock. + // It's ok to call this too frequently. + void NotifyClock(uint32 now); + + // Call this whenever a packet arrives. + // Returns true if the packet was processed successfully. + bool NotifyPacket(const char * buffer, size_t len); + + // Call this to determine the next time NotifyClock should be called. + // Returns false if the socket is ready to be destroyed. + bool GetNextClock(uint32 now, long& timeout); + + // Call these to get/set option values to tailor this PseudoTcp + // instance's behaviour for the kind of data it will carry. + // If an unrecognized option is set or got, an assertion will fire. + // + // Setting options for OPT_RCVBUF or OPT_SNDBUF after Connect() is called + // will result in an assertion. + enum Option { + OPT_NODELAY, // Whether to enable Nagle's algorithm (0 == off) + OPT_ACKDELAY, // The Delayed ACK timeout (0 == off). + OPT_RCVBUF, // Set the receive buffer size, in bytes. + OPT_SNDBUF, // Set the send buffer size, in bytes. + }; + void GetOption(Option opt, int* value); + void SetOption(Option opt, int value); + + // Returns current congestion window in bytes. + uint32 GetCongestionWindow() const; + + // Returns amount of data in bytes that has been sent, but haven't + // been acknowledged. + uint32 GetBytesInFlight() const; + + // Returns number of bytes that were written in buffer and haven't + // been sent. + uint32 GetBytesBufferedNotSent() const; + + // Returns current round-trip time estimate in milliseconds. + uint32 GetRoundTripTimeEstimateMs() const; + + protected: + enum SendFlags { sfNone, sfDelayedAck, sfImmediateAck }; + + struct Segment { + uint32 conv, seq, ack; + uint8 flags; + uint16 wnd; + const char * data; + uint32 len; + uint32 tsval, tsecr; + }; + + struct SSegment { + SSegment(uint32 s, uint32 l, bool c) + : seq(s), len(l), /*tstamp(0),*/ xmit(0), bCtrl(c) { + } + uint32 seq, len; + //uint32 tstamp; + uint8 xmit; + bool bCtrl; + }; + typedef std::list SList; + + struct RSegment { + uint32 seq, len; + }; + + uint32 queue(const char* data, uint32 len, bool bCtrl); + + // Creates a packet and submits it to the network. This method can either + // send payload or just an ACK packet. + // + // |seq| is the sequence number of this packet. + // |flags| is the flags for sending this packet. + // |offset| is the offset to read from |m_sbuf|. + // |len| is the number of bytes to read from |m_sbuf| as payload. If this + // value is 0 then this is an ACK packet, otherwise this packet has payload. + IPseudoTcpNotify::WriteResult packet(uint32 seq, uint8 flags, + uint32 offset, uint32 len); + bool parse(const uint8* buffer, uint32 size); + + void attemptSend(SendFlags sflags = sfNone); + + void closedown(uint32 err = 0); + + bool clock_check(uint32 now, long& nTimeout); + + bool process(Segment& seg); + bool transmit(const SList::iterator& seg, uint32 now); + + void adjustMTU(); + + protected: + // This method is used in test only to query receive buffer state. + bool isReceiveBufferFull() const; + + // This method is only used in tests, to disable window scaling + // support for testing backward compatibility. + void disableWindowScale(); + + private: + // Queue the connect message with TCP options. + void queueConnectMessage(); + + // Parse TCP options in the header. + void parseOptions(const char* data, uint32 len); + + // Apply a TCP option that has been read from the header. + void applyOption(char kind, const char* data, uint32 len); + + // Apply window scale option. + void applyWindowScaleOption(uint8 scale_factor); + + // Resize the send buffer with |new_size| in bytes. + void resizeSendBuffer(uint32 new_size); + + // Resize the receive buffer with |new_size| in bytes. This call adjusts + // window scale factor |m_swnd_scale| accordingly. + void resizeReceiveBuffer(uint32 new_size); + + IPseudoTcpNotify* m_notify; + enum Shutdown { SD_NONE, SD_GRACEFUL, SD_FORCEFUL } m_shutdown; + int m_error; + + // TCB data + TcpState m_state; + uint32 m_conv; + bool m_bReadEnable, m_bWriteEnable, m_bOutgoing; + uint32 m_lasttraffic; + + // Incoming data + typedef std::list RList; + RList m_rlist; + uint32 m_rbuf_len, m_rcv_nxt, m_rcv_wnd, m_lastrecv; + uint8 m_rwnd_scale; // Window scale factor. + talk_base::FifoBuffer m_rbuf; + + // Outgoing data + SList m_slist; + uint32 m_sbuf_len, m_snd_nxt, m_snd_wnd, m_lastsend, m_snd_una; + uint8 m_swnd_scale; // Window scale factor. + talk_base::FifoBuffer m_sbuf; + + // Maximum segment size, estimated protocol level, largest segment sent + uint32 m_mss, m_msslevel, m_largest, m_mtu_advise; + // Retransmit timer + uint32 m_rto_base; + + // Timestamp tracking + uint32 m_ts_recent, m_ts_lastack; + + // Round-trip calculation + uint32 m_rx_rttvar, m_rx_srtt, m_rx_rto; + + // Congestion avoidance, Fast retransmit/recovery, Delayed ACKs + uint32 m_ssthresh, m_cwnd; + uint8 m_dup_acks; + uint32 m_recover; + uint32 m_t_ack; + + // Configuration options + bool m_use_nagling; + uint32 m_ack_delay; + + // This is used by unit tests to test backward compatibility of + // PseudoTcp implementations that don't support window scaling. + bool m_support_wnd_scale; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_PSEUDOTCP_H_ diff --git a/talk/p2p/base/pseudotcp_unittest.cc b/talk/p2p/base/pseudotcp_unittest.cc new file mode 100644 index 000000000..09eac1621 --- /dev/null +++ b/talk/p2p/base/pseudotcp_unittest.cc @@ -0,0 +1,857 @@ +/* + * libjingle + * Copyright 2011 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 + +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/messagehandler.h" +#include "talk/base/stream.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" +#include "talk/p2p/base/pseudotcp.h" + +using cricket::PseudoTcp; + +static const int kConnectTimeoutMs = 10000; // ~3 * default RTO of 3000ms +static const int kTransferTimeoutMs = 15000; +static const int kBlockSize = 4096; + +class PseudoTcpForTest : public cricket::PseudoTcp { + public: + PseudoTcpForTest(cricket::IPseudoTcpNotify* notify, uint32 conv) + : PseudoTcp(notify, conv) { + } + + bool isReceiveBufferFull() const { + return PseudoTcp::isReceiveBufferFull(); + } + + void disableWindowScale() { + PseudoTcp::disableWindowScale(); + } +}; + +class PseudoTcpTestBase : public testing::Test, + public talk_base::MessageHandler, + public cricket::IPseudoTcpNotify { + public: + PseudoTcpTestBase() + : local_(this, 1), + remote_(this, 1), + have_connected_(false), + have_disconnected_(false), + local_mtu_(65535), + remote_mtu_(65535), + delay_(0), + loss_(0) { + // Set use of the test RNG to get predictable loss patterns. + talk_base::SetRandomTestMode(true); + } + ~PseudoTcpTestBase() { + // Put it back for the next test. + talk_base::SetRandomTestMode(false); + } + void SetLocalMtu(int mtu) { + local_.NotifyMTU(mtu); + local_mtu_ = mtu; + } + void SetRemoteMtu(int mtu) { + remote_.NotifyMTU(mtu); + remote_mtu_ = mtu; + } + void SetDelay(int delay) { + delay_ = delay; + } + void SetLoss(int percent) { + loss_ = percent; + } + void SetOptNagling(bool enable_nagles) { + local_.SetOption(PseudoTcp::OPT_NODELAY, !enable_nagles); + remote_.SetOption(PseudoTcp::OPT_NODELAY, !enable_nagles); + } + void SetOptAckDelay(int ack_delay) { + local_.SetOption(PseudoTcp::OPT_ACKDELAY, ack_delay); + remote_.SetOption(PseudoTcp::OPT_ACKDELAY, ack_delay); + } + void SetOptSndBuf(int size) { + local_.SetOption(PseudoTcp::OPT_SNDBUF, size); + remote_.SetOption(PseudoTcp::OPT_SNDBUF, size); + } + void SetRemoteOptRcvBuf(int size) { + remote_.SetOption(PseudoTcp::OPT_RCVBUF, size); + } + void SetLocalOptRcvBuf(int size) { + local_.SetOption(PseudoTcp::OPT_RCVBUF, size); + } + void DisableRemoteWindowScale() { + remote_.disableWindowScale(); + } + void DisableLocalWindowScale() { + local_.disableWindowScale(); + } + + protected: + int Connect() { + int ret = local_.Connect(); + if (ret == 0) { + UpdateLocalClock(); + } + return ret; + } + void Close() { + local_.Close(false); + UpdateLocalClock(); + } + + enum { MSG_LPACKET, MSG_RPACKET, MSG_LCLOCK, MSG_RCLOCK, MSG_IOCOMPLETE, + MSG_WRITE}; + virtual void OnTcpOpen(PseudoTcp* tcp) { + // Consider ourselves connected when the local side gets OnTcpOpen. + // OnTcpWriteable isn't fired at open, so we trigger it now. + LOG(LS_VERBOSE) << "Opened"; + if (tcp == &local_) { + have_connected_ = true; + OnTcpWriteable(tcp); + } + } + // Test derived from the base should override + // virtual void OnTcpReadable(PseudoTcp* tcp) + // and + // virtual void OnTcpWritable(PseudoTcp* tcp) + virtual void OnTcpClosed(PseudoTcp* tcp, uint32 error) { + // Consider ourselves closed when the remote side gets OnTcpClosed. + // TODO: OnTcpClosed is only ever notified in case of error in + // the current implementation. Solicited close is not (yet) supported. + LOG(LS_VERBOSE) << "Closed"; + EXPECT_EQ(0U, error); + if (tcp == &remote_) { + have_disconnected_ = true; + } + } + virtual WriteResult TcpWritePacket(PseudoTcp* tcp, + const char* buffer, size_t len) { + // Randomly drop the desired percentage of packets. + // Also drop packets that are larger than the configured MTU. + if (talk_base::CreateRandomId() % 100 < static_cast(loss_)) { + LOG(LS_VERBOSE) << "Randomly dropping packet, size=" << len; + } else if (len > static_cast( + talk_base::_min(local_mtu_, remote_mtu_))) { + LOG(LS_VERBOSE) << "Dropping packet that exceeds path MTU, size=" << len; + } else { + int id = (tcp == &local_) ? MSG_RPACKET : MSG_LPACKET; + std::string packet(buffer, len); + talk_base::Thread::Current()->PostDelayed(delay_, this, id, + talk_base::WrapMessageData(packet)); + } + return WR_SUCCESS; + } + + void UpdateLocalClock() { UpdateClock(&local_, MSG_LCLOCK); } + void UpdateRemoteClock() { UpdateClock(&remote_, MSG_RCLOCK); } + void UpdateClock(PseudoTcp* tcp, uint32 message) { + long interval; // NOLINT + tcp->GetNextClock(PseudoTcp::Now(), interval); + interval = talk_base::_max(interval, 0L); // sometimes interval is < 0 + talk_base::Thread::Current()->Clear(this, message); + talk_base::Thread::Current()->PostDelayed(interval, this, message); + } + + virtual void OnMessage(talk_base::Message* message) { + switch (message->message_id) { + case MSG_LPACKET: { + const std::string& s( + talk_base::UseMessageData(message->pdata)); + local_.NotifyPacket(s.c_str(), s.size()); + UpdateLocalClock(); + break; + } + case MSG_RPACKET: { + const std::string& s( + talk_base::UseMessageData(message->pdata)); + remote_.NotifyPacket(s.c_str(), s.size()); + UpdateRemoteClock(); + break; + } + case MSG_LCLOCK: + local_.NotifyClock(PseudoTcp::Now()); + UpdateLocalClock(); + break; + case MSG_RCLOCK: + remote_.NotifyClock(PseudoTcp::Now()); + UpdateRemoteClock(); + break; + default: + break; + } + delete message->pdata; + } + + PseudoTcpForTest local_; + PseudoTcpForTest remote_; + talk_base::MemoryStream send_stream_; + talk_base::MemoryStream recv_stream_; + bool have_connected_; + bool have_disconnected_; + int local_mtu_; + int remote_mtu_; + int delay_; + int loss_; +}; + +class PseudoTcpTest : public PseudoTcpTestBase { + public: + void TestTransfer(int size) { + uint32 start, elapsed; + size_t received; + // Create some dummy data to send. + send_stream_.ReserveSize(size); + for (int i = 0; i < size; ++i) { + char ch = static_cast(i); + send_stream_.Write(&ch, 1, NULL, NULL); + } + send_stream_.Rewind(); + // Prepare the receive stream. + recv_stream_.ReserveSize(size); + // Connect and wait until connected. + start = talk_base::Time(); + EXPECT_EQ(0, Connect()); + EXPECT_TRUE_WAIT(have_connected_, kConnectTimeoutMs); + // Sending will start from OnTcpWriteable and complete when all data has + // been received. + EXPECT_TRUE_WAIT(have_disconnected_, kTransferTimeoutMs); + elapsed = talk_base::TimeSince(start); + recv_stream_.GetSize(&received); + // Ensure we closed down OK and we got the right data. + // TODO: Ensure the errors are cleared properly. + //EXPECT_EQ(0, local_.GetError()); + //EXPECT_EQ(0, remote_.GetError()); + EXPECT_EQ(static_cast(size), received); + EXPECT_EQ(0, memcmp(send_stream_.GetBuffer(), + recv_stream_.GetBuffer(), size)); + LOG(LS_INFO) << "Transferred " << received << " bytes in " << elapsed + << " ms (" << size * 8 / elapsed << " Kbps)"; + } + + private: + // IPseudoTcpNotify interface + + virtual void OnTcpReadable(PseudoTcp* tcp) { + // Stream bytes to the recv stream as they arrive. + if (tcp == &remote_) { + ReadData(); + + // TODO: OnTcpClosed() is currently only notified on error - + // there is no on-the-wire equivalent of TCP FIN. + // So we fake the notification when all the data has been read. + size_t received, required; + recv_stream_.GetPosition(&received); + send_stream_.GetSize(&required); + if (received == required) + OnTcpClosed(&remote_, 0); + } + } + virtual void OnTcpWriteable(PseudoTcp* tcp) { + // Write bytes from the send stream when we can. + // Shut down when we've sent everything. + if (tcp == &local_) { + LOG(LS_VERBOSE) << "Flow Control Lifted"; + bool done; + WriteData(&done); + if (done) { + Close(); + } + } + } + + void ReadData() { + char block[kBlockSize]; + size_t position; + int rcvd; + do { + rcvd = remote_.Recv(block, sizeof(block)); + if (rcvd != -1) { + recv_stream_.Write(block, rcvd, NULL, NULL); + recv_stream_.GetPosition(&position); + LOG(LS_VERBOSE) << "Received: " << position; + } + } while (rcvd > 0); + } + void WriteData(bool* done) { + size_t position, tosend; + int sent; + char block[kBlockSize]; + do { + send_stream_.GetPosition(&position); + if (send_stream_.Read(block, sizeof(block), &tosend, NULL) != + talk_base::SR_EOS) { + sent = local_.Send(block, tosend); + UpdateLocalClock(); + if (sent != -1) { + send_stream_.SetPosition(position + sent); + LOG(LS_VERBOSE) << "Sent: " << position + sent; + } else { + send_stream_.SetPosition(position); + LOG(LS_VERBOSE) << "Flow Controlled"; + } + } else { + sent = tosend = 0; + } + } while (sent > 0); + *done = (tosend == 0); + } + + private: + talk_base::MemoryStream send_stream_; + talk_base::MemoryStream recv_stream_; +}; + + +class PseudoTcpTestPingPong : public PseudoTcpTestBase { + public: + PseudoTcpTestPingPong() + : iterations_remaining_(0), + sender_(NULL), + receiver_(NULL), + bytes_per_send_(0) { + } + void SetBytesPerSend(int bytes) { + bytes_per_send_ = bytes; + } + void TestPingPong(int size, int iterations) { + uint32 start, elapsed; + iterations_remaining_ = iterations; + receiver_ = &remote_; + sender_ = &local_; + // Create some dummy data to send. + send_stream_.ReserveSize(size); + for (int i = 0; i < size; ++i) { + char ch = static_cast(i); + send_stream_.Write(&ch, 1, NULL, NULL); + } + send_stream_.Rewind(); + // Prepare the receive stream. + recv_stream_.ReserveSize(size); + // Connect and wait until connected. + start = talk_base::Time(); + EXPECT_EQ(0, Connect()); + EXPECT_TRUE_WAIT(have_connected_, kConnectTimeoutMs); + // Sending will start from OnTcpWriteable and stop when the required + // number of iterations have completed. + EXPECT_TRUE_WAIT(have_disconnected_, kTransferTimeoutMs); + elapsed = talk_base::TimeSince(start); + LOG(LS_INFO) << "Performed " << iterations << " pings in " + << elapsed << " ms"; + } + + private: + // IPseudoTcpNotify interface + + virtual void OnTcpReadable(PseudoTcp* tcp) { + if (tcp != receiver_) { + LOG_F(LS_ERROR) << "unexpected OnTcpReadable"; + return; + } + // Stream bytes to the recv stream as they arrive. + ReadData(); + // If we've received the desired amount of data, rewind things + // and send it back the other way! + size_t position, desired; + recv_stream_.GetPosition(&position); + send_stream_.GetSize(&desired); + if (position == desired) { + if (receiver_ == &local_ && --iterations_remaining_ == 0) { + Close(); + // TODO: Fake OnTcpClosed() on the receiver for now. + OnTcpClosed(&remote_, 0); + return; + } + PseudoTcp* tmp = receiver_; + receiver_ = sender_; + sender_ = tmp; + recv_stream_.Rewind(); + send_stream_.Rewind(); + OnTcpWriteable(sender_); + } + } + virtual void OnTcpWriteable(PseudoTcp* tcp) { + if (tcp != sender_) + return; + // Write bytes from the send stream when we can. + // Shut down when we've sent everything. + LOG(LS_VERBOSE) << "Flow Control Lifted"; + WriteData(); + } + + void ReadData() { + char block[kBlockSize]; + size_t position; + int rcvd; + do { + rcvd = receiver_->Recv(block, sizeof(block)); + if (rcvd != -1) { + recv_stream_.Write(block, rcvd, NULL, NULL); + recv_stream_.GetPosition(&position); + LOG(LS_VERBOSE) << "Received: " << position; + } + } while (rcvd > 0); + } + void WriteData() { + size_t position, tosend; + int sent; + char block[kBlockSize]; + do { + send_stream_.GetPosition(&position); + tosend = bytes_per_send_ ? bytes_per_send_ : sizeof(block); + if (send_stream_.Read(block, tosend, &tosend, NULL) != + talk_base::SR_EOS) { + sent = sender_->Send(block, tosend); + UpdateLocalClock(); + if (sent != -1) { + send_stream_.SetPosition(position + sent); + LOG(LS_VERBOSE) << "Sent: " << position + sent; + } else { + send_stream_.SetPosition(position); + LOG(LS_VERBOSE) << "Flow Controlled"; + } + } else { + sent = tosend = 0; + } + } while (sent > 0); + } + + private: + int iterations_remaining_; + PseudoTcp* sender_; + PseudoTcp* receiver_; + int bytes_per_send_; +}; + +// Fill the receiver window until it is full, drain it and then +// fill it with the same amount. This is to test that receiver window +// contracts and enlarges correctly. +class PseudoTcpTestReceiveWindow : public PseudoTcpTestBase { + public: + // Not all the data are transfered, |size| just need to be big enough + // to fill up the receiver window twice. + void TestTransfer(int size) { + // Create some dummy data to send. + send_stream_.ReserveSize(size); + for (int i = 0; i < size; ++i) { + char ch = static_cast(i); + send_stream_.Write(&ch, 1, NULL, NULL); + } + send_stream_.Rewind(); + + // Prepare the receive stream. + recv_stream_.ReserveSize(size); + + // Connect and wait until connected. + EXPECT_EQ(0, Connect()); + EXPECT_TRUE_WAIT(have_connected_, kConnectTimeoutMs); + + talk_base::Thread::Current()->Post(this, MSG_WRITE); + EXPECT_TRUE_WAIT(have_disconnected_, kTransferTimeoutMs); + + ASSERT_EQ(2u, send_position_.size()); + ASSERT_EQ(2u, recv_position_.size()); + + const size_t estimated_recv_window = EstimateReceiveWindowSize(); + + // The difference in consecutive send positions should equal the + // receive window size or match very closely. This verifies that receive + // window is open after receiver drained all the data. + const size_t send_position_diff = send_position_[1] - send_position_[0]; + EXPECT_GE(1024u, estimated_recv_window - send_position_diff); + + // Receiver drained the receive window twice. + EXPECT_EQ(2 * estimated_recv_window, recv_position_[1]); + } + + virtual void OnMessage(talk_base::Message* message) { + int message_id = message->message_id; + PseudoTcpTestBase::OnMessage(message); + + switch (message_id) { + case MSG_WRITE: { + WriteData(); + break; + } + default: + break; + } + } + + uint32 EstimateReceiveWindowSize() const { + return recv_position_[0]; + } + + uint32 EstimateSendWindowSize() const { + return send_position_[0] - recv_position_[0]; + } + + private: + // IPseudoTcpNotify interface + virtual void OnTcpReadable(PseudoTcp* tcp) { + } + + virtual void OnTcpWriteable(PseudoTcp* tcp) { + } + + void ReadUntilIOPending() { + char block[kBlockSize]; + size_t position; + int rcvd; + + do { + rcvd = remote_.Recv(block, sizeof(block)); + if (rcvd != -1) { + recv_stream_.Write(block, rcvd, NULL, NULL); + recv_stream_.GetPosition(&position); + LOG(LS_VERBOSE) << "Received: " << position; + } + } while (rcvd > 0); + + recv_stream_.GetPosition(&position); + recv_position_.push_back(position); + + // Disconnect if we have done two transfers. + if (recv_position_.size() == 2u) { + Close(); + OnTcpClosed(&remote_, 0); + } else { + WriteData(); + } + } + + void WriteData() { + size_t position, tosend; + int sent; + char block[kBlockSize]; + do { + send_stream_.GetPosition(&position); + if (send_stream_.Read(block, sizeof(block), &tosend, NULL) != + talk_base::SR_EOS) { + sent = local_.Send(block, tosend); + UpdateLocalClock(); + if (sent != -1) { + send_stream_.SetPosition(position + sent); + LOG(LS_VERBOSE) << "Sent: " << position + sent; + } else { + send_stream_.SetPosition(position); + LOG(LS_VERBOSE) << "Flow Controlled"; + } + } else { + sent = tosend = 0; + } + } while (sent > 0); + // At this point, we've filled up the available space in the send queue. + + int message_queue_size = talk_base::Thread::Current()->size(); + // The message queue will always have at least 2 messages, an RCLOCK and + // an LCLOCK, since they are added back on the delay queue at the same time + // they are pulled off and therefore are never really removed. + if (message_queue_size > 2) { + // If there are non-clock messages remaining, attempt to continue sending + // after giving those messages time to process, which should free up the + // send buffer. + talk_base::Thread::Current()->PostDelayed(10, this, MSG_WRITE); + } else { + if (!remote_.isReceiveBufferFull()) { + LOG(LS_ERROR) << "This shouldn't happen - the send buffer is full, " + << "the receive buffer is not, and there are no " + << "remaining messages to process."; + } + send_stream_.GetPosition(&position); + send_position_.push_back(position); + + // Drain the receiver buffer. + ReadUntilIOPending(); + } + } + + private: + talk_base::MemoryStream send_stream_; + talk_base::MemoryStream recv_stream_; + + std::vector send_position_; + std::vector recv_position_; +}; + +// Basic end-to-end data transfer tests + +// Test the normal case of sending data from one side to the other. +TEST_F(PseudoTcpTest, TestSend) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + TestTransfer(1000000); +} + +// Test sending data with a 50 ms RTT. Transmission should take longer due +// to a slower ramp-up in send rate. +TEST_F(PseudoTcpTest, TestSendWithDelay) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetDelay(50); + TestTransfer(1000000); +} + +// Test sending data with packet loss. Transmission should take much longer due +// to send back-off when loss occurs. +TEST_F(PseudoTcpTest, TestSendWithLoss) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetLoss(10); + TestTransfer(100000); // less data so test runs faster +} + +// Test sending data with a 50 ms RTT and 10% packet loss. Transmission should +// take much longer due to send back-off and slower detection of loss. +TEST_F(PseudoTcpTest, TestSendWithDelayAndLoss) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetDelay(50); + SetLoss(10); + TestTransfer(100000); // less data so test runs faster +} + +// Test sending data with 10% packet loss and Nagling disabled. Transmission +// should take about the same time as with Nagling enabled. +TEST_F(PseudoTcpTest, TestSendWithLossAndOptNaglingOff) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetLoss(10); + SetOptNagling(false); + TestTransfer(100000); // less data so test runs faster +} + +// Test sending data with 10% packet loss and Delayed ACK disabled. +// Transmission should be slightly faster than with it enabled. +TEST_F(PseudoTcpTest, TestSendWithLossAndOptAckDelayOff) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetLoss(10); + SetOptAckDelay(0); + TestTransfer(100000); +} + +// Test sending data with 50ms delay and Nagling disabled. +TEST_F(PseudoTcpTest, TestSendWithDelayAndOptNaglingOff) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetDelay(50); + SetOptNagling(false); + TestTransfer(100000); // less data so test runs faster +} + +// Test sending data with 50ms delay and Delayed ACK disabled. +TEST_F(PseudoTcpTest, TestSendWithDelayAndOptAckDelayOff) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetDelay(50); + SetOptAckDelay(0); + TestTransfer(100000); // less data so test runs faster +} + +// Test a large receive buffer with a sender that doesn't support scaling. +TEST_F(PseudoTcpTest, TestSendRemoteNoWindowScale) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetLocalOptRcvBuf(100000); + DisableRemoteWindowScale(); + TestTransfer(1000000); +} + +// Test a large sender-side receive buffer with a receiver that doesn't support +// scaling. +TEST_F(PseudoTcpTest, TestSendLocalNoWindowScale) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetRemoteOptRcvBuf(100000); + DisableLocalWindowScale(); + TestTransfer(1000000); +} + +// Test when both sides use window scaling. +TEST_F(PseudoTcpTest, TestSendBothUseWindowScale) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetRemoteOptRcvBuf(100000); + SetLocalOptRcvBuf(100000); + TestTransfer(1000000); +} + +// Test using a large window scale value. +TEST_F(PseudoTcpTest, TestSendLargeInFlight) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetRemoteOptRcvBuf(100000); + SetLocalOptRcvBuf(100000); + SetOptSndBuf(150000); + TestTransfer(1000000); +} + +TEST_F(PseudoTcpTest, TestSendBothUseLargeWindowScale) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetRemoteOptRcvBuf(1000000); + SetLocalOptRcvBuf(1000000); + TestTransfer(10000000); +} + +// Test using a small receive buffer. +TEST_F(PseudoTcpTest, TestSendSmallReceiveBuffer) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetRemoteOptRcvBuf(10000); + SetLocalOptRcvBuf(10000); + TestTransfer(1000000); +} + +// Test using a very small receive buffer. +TEST_F(PseudoTcpTest, TestSendVerySmallReceiveBuffer) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetRemoteOptRcvBuf(100); + SetLocalOptRcvBuf(100); + TestTransfer(100000); +} + +// Ping-pong (request/response) tests + +// Test sending <= 1x MTU of data in each ping/pong. Should take <10ms. +TEST_F(PseudoTcpTestPingPong, TestPingPong1xMtu) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + TestPingPong(100, 100); +} + +// Test sending 2x-3x MTU of data in each ping/pong. Should take <10ms. +TEST_F(PseudoTcpTestPingPong, TestPingPong3xMtu) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + TestPingPong(400, 100); +} + +// Test sending 1x-2x MTU of data in each ping/pong. +// Should take ~1s, due to interaction between Nagling and Delayed ACK. +TEST_F(PseudoTcpTestPingPong, TestPingPong2xMtu) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + TestPingPong(2000, 5); +} + +// Test sending 1x-2x MTU of data in each ping/pong with Delayed ACK off. +// Should take <10ms. +TEST_F(PseudoTcpTestPingPong, TestPingPong2xMtuWithAckDelayOff) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetOptAckDelay(0); + TestPingPong(2000, 100); +} + +// Test sending 1x-2x MTU of data in each ping/pong with Nagling off. +// Should take <10ms. +TEST_F(PseudoTcpTestPingPong, TestPingPong2xMtuWithNaglingOff) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetOptNagling(false); + TestPingPong(2000, 5); +} + +// Test sending a ping as pair of short (non-full) segments. +// Should take ~1s, due to Delayed ACK interaction with Nagling. +TEST_F(PseudoTcpTestPingPong, TestPingPongShortSegments) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetOptAckDelay(5000); + SetBytesPerSend(50); // i.e. two Send calls per payload + TestPingPong(100, 5); +} + +// Test sending ping as a pair of short (non-full) segments, with Nagling off. +// Should take <10ms. +TEST_F(PseudoTcpTestPingPong, TestPingPongShortSegmentsWithNaglingOff) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetOptNagling(false); + SetBytesPerSend(50); // i.e. two Send calls per payload + TestPingPong(100, 5); +} + +// Test sending <= 1x MTU of data ping/pong, in two segments, no Delayed ACK. +// Should take ~1s. +TEST_F(PseudoTcpTestPingPong, TestPingPongShortSegmentsWithAckDelayOff) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetBytesPerSend(50); // i.e. two Send calls per payload + SetOptAckDelay(0); + TestPingPong(100, 5); +} + +// Test that receive window expands and contract correctly. +TEST_F(PseudoTcpTestReceiveWindow, TestReceiveWindow) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetOptNagling(false); + SetOptAckDelay(0); + TestTransfer(1024 * 1000); +} + +// Test setting send window size to a very small value. +TEST_F(PseudoTcpTestReceiveWindow, TestSetVerySmallSendWindowSize) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetOptNagling(false); + SetOptAckDelay(0); + SetOptSndBuf(900); + TestTransfer(1024 * 1000); + EXPECT_EQ(900u, EstimateSendWindowSize()); +} + +// Test setting receive window size to a value other than default. +TEST_F(PseudoTcpTestReceiveWindow, TestSetReceiveWindowSize) { + SetLocalMtu(1500); + SetRemoteMtu(1500); + SetOptNagling(false); + SetOptAckDelay(0); + SetRemoteOptRcvBuf(100000); + SetLocalOptRcvBuf(100000); + TestTransfer(1024 * 1000); + EXPECT_EQ(100000u, EstimateReceiveWindowSize()); +} + +/* Test sending data with mismatched MTUs. We should detect this and reduce +// our packet size accordingly. +// TODO: This doesn't actually work right now. The current code +// doesn't detect if the MTU is set too high on either side. +TEST_F(PseudoTcpTest, TestSendWithMismatchedMtus) { + SetLocalMtu(1500); + SetRemoteMtu(1280); + TestTransfer(1000000); +} +*/ diff --git a/talk/p2p/base/rawtransport.cc b/talk/p2p/base/rawtransport.cc new file mode 100644 index 000000000..fe4f3a2bb --- /dev/null +++ b/talk/p2p/base/rawtransport.cc @@ -0,0 +1,132 @@ +/* + * libjingle + * Copyright 2004--2005, 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 +#include +#include "talk/p2p/base/rawtransport.h" +#include "talk/base/common.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/parsing.h" +#include "talk/p2p/base/sessionmanager.h" +#include "talk/p2p/base/rawtransportchannel.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" + +#if defined(FEATURE_ENABLE_PSTN) +namespace cricket { + +RawTransport::RawTransport(talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread, + const std::string& content_name, + PortAllocator* allocator) + : Transport(signaling_thread, worker_thread, + content_name, NS_GINGLE_RAW, allocator) { +} + +RawTransport::~RawTransport() { + DestroyAllChannels(); +} + +bool RawTransport::ParseCandidates(SignalingProtocol protocol, + const buzz::XmlElement* elem, + const CandidateTranslator* translator, + Candidates* candidates, + ParseError* error) { + for (const buzz::XmlElement* cand_elem = elem->FirstElement(); + cand_elem != NULL; + cand_elem = cand_elem->NextElement()) { + if (cand_elem->Name() == QN_GINGLE_RAW_CHANNEL) { + if (!cand_elem->HasAttr(buzz::QN_NAME)) { + return BadParse("no channel name given", error); + } + if (type() != cand_elem->Attr(buzz::QN_NAME)) { + return BadParse("channel named does not exist", error); + } + talk_base::SocketAddress addr; + if (!ParseRawAddress(cand_elem, &addr, error)) + return false; + + Candidate candidate; + candidate.set_component(1); + candidate.set_address(addr); + candidates->push_back(candidate); + } + } + return true; +} + +bool RawTransport::WriteCandidates(SignalingProtocol protocol, + const Candidates& candidates, + const CandidateTranslator* translator, + XmlElements* candidate_elems, + WriteError* error) { + for (std::vector::const_iterator + cand = candidates.begin(); + cand != candidates.end(); + ++cand) { + ASSERT(cand->component() == 1); + ASSERT(cand->protocol() == "udp"); + talk_base::SocketAddress addr = cand->address(); + + buzz::XmlElement* elem = new buzz::XmlElement(QN_GINGLE_RAW_CHANNEL); + elem->SetAttr(buzz::QN_NAME, type()); + elem->SetAttr(QN_ADDRESS, addr.ipaddr().ToString()); + elem->SetAttr(QN_PORT, addr.PortAsString()); + candidate_elems->push_back(elem); + } + return true; +} + +bool RawTransport::ParseRawAddress(const buzz::XmlElement* elem, + talk_base::SocketAddress* addr, + ParseError* error) { + // Make sure the required attributes exist + if (!elem->HasAttr(QN_ADDRESS) || + !elem->HasAttr(QN_PORT)) { + return BadParse("channel missing required attribute", error); + } + + // Parse the address. + if (!ParseAddress(elem, QN_ADDRESS, QN_PORT, addr, error)) + return false; + + return true; +} + +TransportChannelImpl* RawTransport::CreateTransportChannel(int component) { + return new RawTransportChannel(content_name(), component, this, + worker_thread(), + port_allocator()); +} + +void RawTransport::DestroyTransportChannel(TransportChannelImpl* channel) { + delete channel; +} + +} // namespace cricket +#endif // defined(FEATURE_ENABLE_PSTN) diff --git a/talk/p2p/base/rawtransport.h b/talk/p2p/base/rawtransport.h new file mode 100644 index 000000000..6bb04fe06 --- /dev/null +++ b/talk/p2p/base/rawtransport.h @@ -0,0 +1,81 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_RAWTRANSPORT_H_ +#define TALK_P2P_BASE_RAWTRANSPORT_H_ + +#include +#include "talk/p2p/base/transport.h" + +#if defined(FEATURE_ENABLE_PSTN) +namespace cricket { + +// Implements a transport that only sends raw packets, no STUN. As a result, +// it cannot do pings to determine connectivity, so it only uses a single port +// that it thinks will work. +class RawTransport : public Transport, public TransportParser { + public: + RawTransport(talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread, + const std::string& content_name, + PortAllocator* allocator); + virtual ~RawTransport(); + + virtual bool ParseCandidates(SignalingProtocol protocol, + const buzz::XmlElement* elem, + const CandidateTranslator* translator, + Candidates* candidates, + ParseError* error); + virtual bool WriteCandidates(SignalingProtocol protocol, + const Candidates& candidates, + const CandidateTranslator* translator, + XmlElements* candidate_elems, + WriteError* error); + + protected: + // Creates and destroys raw channels. + virtual TransportChannelImpl* CreateTransportChannel(int component); + virtual void DestroyTransportChannel(TransportChannelImpl* channel); + + private: + // Parses the given element, which should describe the address to use for a + // given channel. This will return false and signal an error if the address + // or channel name is bad. + bool ParseRawAddress(const buzz::XmlElement* elem, + talk_base::SocketAddress* addr, + ParseError* error); + + friend class RawTransportChannel; // For ParseAddress. + + DISALLOW_EVIL_CONSTRUCTORS(RawTransport); +}; + +} // namespace cricket + +#endif // defined(FEATURE_ENABLE_PSTN) + +#endif // TALK_P2P_BASE_RAWTRANSPORT_H_ diff --git a/talk/p2p/base/rawtransportchannel.cc b/talk/p2p/base/rawtransportchannel.cc new file mode 100644 index 000000000..54adab13c --- /dev/null +++ b/talk/p2p/base/rawtransportchannel.cc @@ -0,0 +1,275 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/rawtransportchannel.h" + +#include +#include +#include "talk/base/common.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/portallocator.h" +#include "talk/p2p/base/portinterface.h" +#include "talk/p2p/base/rawtransport.h" +#include "talk/p2p/base/relayport.h" +#include "talk/p2p/base/sessionmanager.h" +#include "talk/p2p/base/stunport.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" + +#if defined(FEATURE_ENABLE_PSTN) + +namespace { + +const uint32 MSG_DESTROY_UNUSED_PORTS = 1; + +} // namespace + +namespace cricket { + +RawTransportChannel::RawTransportChannel(const std::string& content_name, + int component, + RawTransport* transport, + talk_base::Thread *worker_thread, + PortAllocator *allocator) + : TransportChannelImpl(content_name, component), + raw_transport_(transport), + allocator_(allocator), + allocator_session_(NULL), + stun_port_(NULL), + relay_port_(NULL), + port_(NULL), + use_relay_(false) { + if (worker_thread == NULL) + worker_thread_ = raw_transport_->worker_thread(); + else + worker_thread_ = worker_thread; +} + +RawTransportChannel::~RawTransportChannel() { + delete allocator_session_; +} + +int RawTransportChannel::SendPacket(const char *data, size_t size, int flags) { + if (port_ == NULL) + return -1; + if (remote_address_.IsNil()) + return -1; + if (flags != 0) + return -1; + return port_->SendTo(data, size, remote_address_, true); +} + +int RawTransportChannel::SetOption(talk_base::Socket::Option opt, int value) { + // TODO: allow these to be set before we have a port + if (port_ == NULL) + return -1; + return port_->SetOption(opt, value); +} + +int RawTransportChannel::GetError() { + return (port_ != NULL) ? port_->GetError() : 0; +} + +void RawTransportChannel::Connect() { + // Create an allocator that only returns stun and relay ports. + // Use empty string for ufrag and pwd here. There won't be any STUN or relay + // interactions when using RawTC. + // TODO: Change raw to only use local udp ports. + allocator_session_ = allocator_->CreateSession( + SessionId(), content_name(), component(), "", ""); + + uint32 flags = PORTALLOCATOR_DISABLE_UDP | PORTALLOCATOR_DISABLE_TCP; + +#if !defined(FEATURE_ENABLE_STUN_CLASSIFICATION) + flags |= PORTALLOCATOR_DISABLE_RELAY; +#endif + allocator_session_->set_flags(flags); + allocator_session_->SignalPortReady.connect( + this, &RawTransportChannel::OnPortReady); + allocator_session_->SignalCandidatesReady.connect( + this, &RawTransportChannel::OnCandidatesReady); + + // The initial ports will include stun. + allocator_session_->StartGettingPorts(); +} + +void RawTransportChannel::Reset() { + set_readable(false); + set_writable(false); + + delete allocator_session_; + + allocator_session_ = NULL; + stun_port_ = NULL; + relay_port_ = NULL; + port_ = NULL; + remote_address_ = talk_base::SocketAddress(); +} + +void RawTransportChannel::OnCandidate(const Candidate& candidate) { + remote_address_ = candidate.address(); + ASSERT(!remote_address_.IsNil()); + set_readable(true); + + // We can write once we have a port and a remote address. + if (port_ != NULL) + SetWritable(); +} + +void RawTransportChannel::OnRemoteAddress( + const talk_base::SocketAddress& remote_address) { + remote_address_ = remote_address; + set_readable(true); + + if (port_ != NULL) + SetWritable(); +} + +// Note about stun classification +// Code to classify our NAT type and use the relay port if we are behind an +// asymmetric NAT is under a FEATURE_ENABLE_STUN_CLASSIFICATION #define. +// To turn this one we will have to enable a second stun address and make sure +// that the relay server works for raw UDP. +// +// Another option is to classify the NAT type early and not offer the raw +// transport type at all if we can't support it. + +void RawTransportChannel::OnPortReady( + PortAllocatorSession* session, PortInterface* port) { + ASSERT(session == allocator_session_); + + if (port->Type() == STUN_PORT_TYPE) { + stun_port_ = static_cast(port); + } else if (port->Type() == RELAY_PORT_TYPE) { + relay_port_ = static_cast(port); + } else { + ASSERT(false); + } +} + +void RawTransportChannel::OnCandidatesReady( + PortAllocatorSession *session, const std::vector& candidates) { + ASSERT(session == allocator_session_); + ASSERT(candidates.size() >= 1); + + // The most recent candidate is the one we haven't seen yet. + Candidate c = candidates[candidates.size() - 1]; + + if (c.type() == STUN_PORT_TYPE) { + ASSERT(stun_port_ != NULL); + +#if defined(FEATURE_ENABLE_STUN_CLASSIFICATION) + // We need to wait until we have two addresses. + if (stun_port_->candidates().size() < 2) + return; + + // This is the second address. If these addresses are the same, then we + // are not behind a symmetric NAT. Hence, a stun port should be sufficient. + if (stun_port_->candidates()[0].address() == + stun_port_->candidates()[1].address()) { + SetPort(stun_port_); + return; + } + + // We will need to use relay. + use_relay_ = true; + + // If we already have a relay address, we're good. Otherwise, we will need + // to wait until one arrives. + if (relay_port_->candidates().size() > 0) + SetPort(relay_port_); +#else // defined(FEATURE_ENABLE_STUN_CLASSIFICATION) + // Always use the stun port. We don't classify right now so just assume it + // will work fine. + SetPort(stun_port_); +#endif + } else if (c.type() == RELAY_PORT_TYPE) { + if (use_relay_) + SetPort(relay_port_); + } else { + ASSERT(false); + } +} + +void RawTransportChannel::SetPort(PortInterface* port) { + ASSERT(port_ == NULL); + port_ = port; + + // We don't need any ports other than the one we picked. + allocator_session_->StopGettingPorts(); + worker_thread_->Post( + this, MSG_DESTROY_UNUSED_PORTS, NULL); + + // Send a message to the other client containing our address. + + ASSERT(port_->Candidates().size() >= 1); + ASSERT(port_->Candidates()[0].protocol() == "udp"); + SignalCandidateReady(this, port_->Candidates()[0]); + + // Read all packets from this port. + port_->EnablePortPackets(); + port_->SignalReadPacket.connect(this, &RawTransportChannel::OnReadPacket); + + // We can write once we have a port and a remote address. + if (!remote_address_.IsAny()) + SetWritable(); +} + +void RawTransportChannel::SetWritable() { + ASSERT(port_ != NULL); + ASSERT(!remote_address_.IsAny()); + + set_writable(true); + + Candidate remote_candidate; + remote_candidate.set_address(remote_address_); + SignalRouteChange(this, remote_candidate); +} + +void RawTransportChannel::OnReadPacket( + PortInterface* port, const char* data, size_t size, + const talk_base::SocketAddress& addr) { + ASSERT(port_ == port); + SignalReadPacket(this, data, size, 0); +} + +void RawTransportChannel::OnMessage(talk_base::Message* msg) { + ASSERT(msg->message_id == MSG_DESTROY_UNUSED_PORTS); + ASSERT(port_ != NULL); + if (port_ != stun_port_) { + stun_port_->Destroy(); + stun_port_ = NULL; + } + if (port_ != relay_port_ && relay_port_ != NULL) { + relay_port_->Destroy(); + relay_port_ = NULL; + } +} + +} // namespace cricket +#endif // defined(FEATURE_ENABLE_PSTN) diff --git a/talk/p2p/base/rawtransportchannel.h b/talk/p2p/base/rawtransportchannel.h new file mode 100644 index 000000000..3c9c33088 --- /dev/null +++ b/talk/p2p/base/rawtransportchannel.h @@ -0,0 +1,143 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_RAWTRANSPORTCHANNEL_H_ +#define TALK_P2P_BASE_RAWTRANSPORTCHANNEL_H_ + +#include +#include +#include "talk/base/messagequeue.h" +#include "talk/p2p/base/transportchannelimpl.h" +#include "talk/p2p/base/rawtransport.h" +#include "talk/p2p/base/candidate.h" + +#if defined(FEATURE_ENABLE_PSTN) + +namespace talk_base { +class Thread; +} + +namespace cricket { + +class Connection; +class PortAllocator; +class PortAllocatorSession; +class PortInterface; +class RelayPort; +class StunPort; + +// Implements a channel that just sends bare packets once we have received the +// address of the other side. We pick a single address to send them based on +// a simple investigation of NAT type. +class RawTransportChannel : public TransportChannelImpl, + public talk_base::MessageHandler { + public: + RawTransportChannel(const std::string& content_name, + int component, + RawTransport* transport, + talk_base::Thread *worker_thread, + PortAllocator *allocator); + virtual ~RawTransportChannel(); + + // Implementation of normal channel packet sending. + virtual int SendPacket(const char *data, size_t len, int flags); + virtual int SetOption(talk_base::Socket::Option opt, int value); + virtual int GetError(); + + // Implements TransportChannelImpl. + virtual Transport* GetTransport() { return raw_transport_; } + virtual void SetIceCredentials(const std::string& ice_ufrag, + const std::string& ice_pwd) {} + virtual void SetRemoteIceCredentials(const std::string& ice_ufrag, + const std::string& ice_pwd) {} + virtual TransportRole GetRole() const { return ROLE_UNKNOWN; } + + // Creates an allocator session to start figuring out which type of + // port we should send to the other client. This will send + // SignalAvailableCandidate once we have decided. + virtual void Connect(); + + // Resets state back to unconnected. + virtual void Reset(); + + // We don't actually worry about signaling since we can't send new candidates. + virtual void OnSignalingReady() {} + + // Handles a message setting the remote address. We are writable once we + // have this since we now know where to send. + virtual void OnCandidate(const Candidate& candidate); + + void OnRemoteAddress(const talk_base::SocketAddress& remote_address); + + // Below ICE specific virtual methods not implemented. + virtual void SetRole(TransportRole role) {} + virtual void SetTiebreaker(uint64 tiebreaker) {} + virtual void SetIceProtocolType(IceProtocolType type) {} + virtual void SetIceUfrag(const std::string& ice_ufrag) {} + virtual void SetIcePwd(const std::string& ice_pwd) {} + virtual void SetRemoteIceMode(IceMode mode) {} + + private: + RawTransport* raw_transport_; + talk_base::Thread *worker_thread_; + PortAllocator* allocator_; + PortAllocatorSession* allocator_session_; + StunPort* stun_port_; + RelayPort* relay_port_; + PortInterface* port_; + bool use_relay_; + talk_base::SocketAddress remote_address_; + + // Called when the allocator creates another port. + void OnPortReady(PortAllocatorSession* session, PortInterface* port); + + // Called when one of the ports we are using has determined its address. + void OnCandidatesReady(PortAllocatorSession *session, + const std::vector& candidates); + + // Called once we have chosen the port to use for communication with the + // other client. This will send its address and prepare the port for use. + void SetPort(PortInterface* port); + + // Called once we have a port and a remote address. This will set mark the + // channel as writable and signal the route to the client. + void SetWritable(); + + // Called when we receive a packet from the other client. + void OnReadPacket(PortInterface* port, const char* data, size_t size, + const talk_base::SocketAddress& addr); + + // Handles a message to destroy unused ports. + virtual void OnMessage(talk_base::Message *msg); + + DISALLOW_EVIL_CONSTRUCTORS(RawTransportChannel); +}; + +} // namespace cricket + +#endif // defined(FEATURE_ENABLE_PSTN) +#endif // TALK_P2P_BASE_RAWTRANSPORTCHANNEL_H_ diff --git a/talk/p2p/base/relayport.cc b/talk/p2p/base/relayport.cc new file mode 100644 index 000000000..7abe942f1 --- /dev/null +++ b/talk/p2p/base/relayport.cc @@ -0,0 +1,820 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/asyncpacketsocket.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/p2p/base/relayport.h" + +namespace cricket { + +static const uint32 kMessageConnectTimeout = 1; +static const int kKeepAliveDelay = 10 * 60 * 1000; +static const int kRetryTimeout = 50 * 1000; // ICE says 50 secs +// How long to wait for a socket to connect to remote host in milliseconds +// before trying another connection. +static const int kSoftConnectTimeoutMs = 3 * 1000; + +// Handles a connection to one address/port/protocol combination for a +// particular RelayEntry. +class RelayConnection : public sigslot::has_slots<> { + public: + RelayConnection(const ProtocolAddress* protocol_address, + talk_base::AsyncPacketSocket* socket, + talk_base::Thread* thread); + ~RelayConnection(); + talk_base::AsyncPacketSocket* socket() const { return socket_; } + + const ProtocolAddress* protocol_address() { + return protocol_address_; + } + + talk_base::SocketAddress GetAddress() const { + return protocol_address_->address; + } + + ProtocolType GetProtocol() const { + return protocol_address_->proto; + } + + int SetSocketOption(talk_base::Socket::Option opt, int value); + + // Validates a response to a STUN allocate request. + bool CheckResponse(StunMessage* msg); + + // Sends data to the relay server. + int Send(const void* pv, size_t cb); + + // Sends a STUN allocate request message to the relay server. + void SendAllocateRequest(RelayEntry* entry, int delay); + + // Return the latest error generated by the socket. + int GetError() { return socket_->GetError(); } + + // Called on behalf of a StunRequest to write data to the socket. This is + // already STUN intended for the server, so no wrapping is necessary. + void OnSendPacket(const void* data, size_t size, StunRequest* req); + + private: + talk_base::AsyncPacketSocket* socket_; + const ProtocolAddress* protocol_address_; + StunRequestManager *request_manager_; +}; + +// Manages a number of connections to the relayserver, one for each +// available protocol. We aim to use each connection for only a +// specific destination address so that we can avoid wrapping every +// packet in a STUN send / data indication. +class RelayEntry : public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + RelayEntry(RelayPort* port, const talk_base::SocketAddress& ext_addr); + ~RelayEntry(); + + RelayPort* port() { return port_; } + + const talk_base::SocketAddress& address() const { return ext_addr_; } + void set_address(const talk_base::SocketAddress& addr) { ext_addr_ = addr; } + + bool connected() const { return connected_; } + bool locked() const { return locked_; } + + // Returns the last error on the socket of this entry. + int GetError(); + + // Returns the most preferred connection of the given + // ones. Connections are rated based on protocol in the order of: + // UDP, TCP and SSLTCP, where UDP is the most preferred protocol + static RelayConnection* GetBestConnection(RelayConnection* conn1, + RelayConnection* conn2); + + // Sends the STUN requests to the server to initiate this connection. + void Connect(); + + // Called when this entry becomes connected. The address given is the one + // exposed to the outside world on the relay server. + void OnConnect(const talk_base::SocketAddress& mapped_addr, + RelayConnection* socket); + + // Sends a packet to the given destination address using the socket of this + // entry. This will wrap the packet in STUN if necessary. + int SendTo(const void* data, size_t size, + const talk_base::SocketAddress& addr); + + // Schedules a keep-alive allocate request. + void ScheduleKeepAlive(); + + void SetServerIndex(size_t sindex) { server_index_ = sindex; } + + // Sets this option on the socket of each connection. + int SetSocketOption(talk_base::Socket::Option opt, int value); + + size_t ServerIndex() const { return server_index_; } + + // Try a different server address + void HandleConnectFailure(talk_base::AsyncPacketSocket* socket); + + // Implementation of the MessageHandler Interface. + virtual void OnMessage(talk_base::Message *pmsg); + + private: + RelayPort* port_; + talk_base::SocketAddress ext_addr_; + size_t server_index_; + bool connected_; + bool locked_; + RelayConnection* current_connection_; + + // Called when a TCP connection is established or fails + void OnSocketConnect(talk_base::AsyncPacketSocket* socket); + void OnSocketClose(talk_base::AsyncPacketSocket* socket, int error); + + // Called when a packet is received on this socket. + void OnReadPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& remote_addr); + + // Called when the socket is currently able to send. + void OnReadyToSend(talk_base::AsyncPacketSocket* socket); + + // Sends the given data on the socket to the server with no wrapping. This + // returns the number of bytes written or -1 if an error occurred. + int SendPacket(const void* data, size_t size); +}; + +// Handles an allocate request for a particular RelayEntry. +class AllocateRequest : public StunRequest { + public: + AllocateRequest(RelayEntry* entry, RelayConnection* connection); + virtual ~AllocateRequest() {} + + virtual void Prepare(StunMessage* request); + + virtual int GetNextDelay(); + + virtual void OnResponse(StunMessage* response); + virtual void OnErrorResponse(StunMessage* response); + virtual void OnTimeout(); + + private: + RelayEntry* entry_; + RelayConnection* connection_; + uint32 start_time_; +}; + +RelayPort::RelayPort( + talk_base::Thread* thread, talk_base::PacketSocketFactory* factory, + talk_base::Network* network, const talk_base::IPAddress& ip, + int min_port, int max_port, const std::string& username, + const std::string& password) + : Port(thread, RELAY_PORT_TYPE, factory, network, ip, min_port, max_port, + username, password), + ready_(false), + error_(0) { + entries_.push_back( + new RelayEntry(this, talk_base::SocketAddress())); + // TODO: set local preference value for TCP based candidates. +} + +RelayPort::~RelayPort() { + for (size_t i = 0; i < entries_.size(); ++i) + delete entries_[i]; + thread()->Clear(this); +} + +void RelayPort::AddServerAddress(const ProtocolAddress& addr) { + // Since HTTP proxies usually only allow 443, + // let's up the priority on PROTO_SSLTCP + if (addr.proto == PROTO_SSLTCP && + (proxy().type == talk_base::PROXY_HTTPS || + proxy().type == talk_base::PROXY_UNKNOWN)) { + server_addr_.push_front(addr); + } else { + server_addr_.push_back(addr); + } +} + +void RelayPort::AddExternalAddress(const ProtocolAddress& addr) { + std::string proto_name = ProtoToString(addr.proto); + for (std::vector::iterator it = external_addr_.begin(); + it != external_addr_.end(); ++it) { + if ((it->address == addr.address) && (it->proto == addr.proto)) { + LOG(INFO) << "Redundant relay address: " << proto_name + << " @ " << addr.address.ToSensitiveString(); + return; + } + } + external_addr_.push_back(addr); +} + +void RelayPort::SetReady() { + if (!ready_) { + std::vector::iterator iter; + for (iter = external_addr_.begin(); + iter != external_addr_.end(); ++iter) { + std::string proto_name = ProtoToString(iter->proto); + AddAddress(iter->address, iter->address, proto_name, + RELAY_PORT_TYPE, ICE_TYPE_PREFERENCE_RELAY, false); + } + ready_ = true; + SignalPortComplete(this); + } +} + +const ProtocolAddress * RelayPort::ServerAddress(size_t index) const { + if (index < server_addr_.size()) + return &server_addr_[index]; + return NULL; +} + +bool RelayPort::HasMagicCookie(const char* data, size_t size) { + if (size < 24 + sizeof(TURN_MAGIC_COOKIE_VALUE)) { + return false; + } else { + return 0 == std::memcmp(data + 24, TURN_MAGIC_COOKIE_VALUE, + sizeof(TURN_MAGIC_COOKIE_VALUE)); + } +} + +void RelayPort::PrepareAddress() { + // We initiate a connect on the first entry. If this completes, it will fill + // in the server address as the address of this port. + ASSERT(entries_.size() == 1); + entries_[0]->Connect(); + ready_ = false; +} + +Connection* RelayPort::CreateConnection(const Candidate& address, + CandidateOrigin origin) { + // We only create conns to non-udp sockets if they are incoming on this port + if ((address.protocol() != UDP_PROTOCOL_NAME) && + (origin != ORIGIN_THIS_PORT)) { + return 0; + } + + // We don't support loopback on relays + if (address.type() == Type()) { + return 0; + } + + if (!IsCompatibleAddress(address.address())) { + return 0; + } + + size_t index = 0; + for (size_t i = 0; i < Candidates().size(); ++i) { + const Candidate& local = Candidates()[i]; + if (local.protocol() == address.protocol()) { + index = i; + break; + } + } + + Connection * conn = new ProxyConnection(this, index, address); + AddConnection(conn); + return conn; +} + +int RelayPort::SendTo(const void* data, size_t size, + const talk_base::SocketAddress& addr, bool payload) { + // Try to find an entry for this specific address. Note that the first entry + // created was not given an address initially, so it can be set to the first + // address that comes along. + RelayEntry* entry = 0; + + for (size_t i = 0; i < entries_.size(); ++i) { + if (entries_[i]->address().IsNil() && payload) { + entry = entries_[i]; + entry->set_address(addr); + break; + } else if (entries_[i]->address() == addr) { + entry = entries_[i]; + break; + } + } + + // If we did not find one, then we make a new one. This will not be useable + // until it becomes connected, however. + if (!entry && payload) { + entry = new RelayEntry(this, addr); + if (!entries_.empty()) { + entry->SetServerIndex(entries_[0]->ServerIndex()); + } + entry->Connect(); + entries_.push_back(entry); + } + + // If the entry is connected, then we can send on it (though wrapping may + // still be necessary). Otherwise, we can't yet use this connection, so we + // default to the first one. + if (!entry || !entry->connected()) { + ASSERT(!entries_.empty()); + entry = entries_[0]; + if (!entry->connected()) { + error_ = EWOULDBLOCK; + return SOCKET_ERROR; + } + } + + // Send the actual contents to the server using the usual mechanism. + int sent = entry->SendTo(data, size, addr); + if (sent <= 0) { + ASSERT(sent < 0); + error_ = entry->GetError(); + return SOCKET_ERROR; + } + // The caller of the function is expecting the number of user data bytes, + // rather than the size of the packet. + return size; +} + +int RelayPort::SetOption(talk_base::Socket::Option opt, int value) { + int result = 0; + for (size_t i = 0; i < entries_.size(); ++i) { + if (entries_[i]->SetSocketOption(opt, value) < 0) { + result = -1; + error_ = entries_[i]->GetError(); + } + } + options_.push_back(OptionValue(opt, value)); + return result; +} + +int RelayPort::GetOption(talk_base::Socket::Option opt, int* value) { + std::vector::iterator it; + for (it = options_.begin(); it < options_.end(); ++it) { + if (it->first == opt) { + *value = it->second; + return 0; + } + } + return SOCKET_ERROR; +} + +int RelayPort::GetError() { + return error_; +} + +void RelayPort::OnReadPacket( + const char* data, size_t size, + const talk_base::SocketAddress& remote_addr, ProtocolType proto) { + if (Connection* conn = GetConnection(remote_addr)) { + conn->OnReadPacket(data, size); + } else { + Port::OnReadPacket(data, size, remote_addr, proto); + } +} + +RelayConnection::RelayConnection(const ProtocolAddress* protocol_address, + talk_base::AsyncPacketSocket* socket, + talk_base::Thread* thread) + : socket_(socket), + protocol_address_(protocol_address) { + request_manager_ = new StunRequestManager(thread); + request_manager_->SignalSendPacket.connect(this, + &RelayConnection::OnSendPacket); +} + +RelayConnection::~RelayConnection() { + delete request_manager_; + delete socket_; +} + +int RelayConnection::SetSocketOption(talk_base::Socket::Option opt, + int value) { + if (socket_) { + return socket_->SetOption(opt, value); + } + return 0; +} + +bool RelayConnection::CheckResponse(StunMessage* msg) { + return request_manager_->CheckResponse(msg); +} + +void RelayConnection::OnSendPacket(const void* data, size_t size, + StunRequest* req) { + int sent = socket_->SendTo(data, size, GetAddress()); + if (sent <= 0) { + LOG(LS_VERBOSE) << "OnSendPacket: failed sending to " << GetAddress() << + std::strerror(socket_->GetError()); + ASSERT(sent < 0); + } +} + +int RelayConnection::Send(const void* pv, size_t cb) { + return socket_->SendTo(pv, cb, GetAddress()); +} + +void RelayConnection::SendAllocateRequest(RelayEntry* entry, int delay) { + request_manager_->SendDelayed(new AllocateRequest(entry, this), delay); +} + +RelayEntry::RelayEntry(RelayPort* port, + const talk_base::SocketAddress& ext_addr) + : port_(port), ext_addr_(ext_addr), + server_index_(0), connected_(false), locked_(false), + current_connection_(NULL) { +} + +RelayEntry::~RelayEntry() { + // Remove all RelayConnections and dispose sockets. + delete current_connection_; + current_connection_ = NULL; +} + +void RelayEntry::Connect() { + // If we're already connected, return. + if (connected_) + return; + + // If we've exhausted all options, bail out. + const ProtocolAddress* ra = port()->ServerAddress(server_index_); + if (!ra) { + LOG(LS_WARNING) << "No more relay addresses left to try"; + return; + } + + // Remove any previous connection. + if (current_connection_) { + port()->thread()->Dispose(current_connection_); + current_connection_ = NULL; + } + + // Try to set up our new socket. + LOG(LS_INFO) << "Connecting to relay via " << ProtoToString(ra->proto) << + " @ " << ra->address.ToSensitiveString(); + + talk_base::AsyncPacketSocket* socket = NULL; + + if (ra->proto == PROTO_UDP) { + // UDP sockets are simple. + socket = port_->socket_factory()->CreateUdpSocket( + talk_base::SocketAddress(port_->ip(), 0), + port_->min_port(), port_->max_port()); + } else if (ra->proto == PROTO_TCP || ra->proto == PROTO_SSLTCP) { + int opts = (ra->proto == PROTO_SSLTCP) ? + talk_base::PacketSocketFactory::OPT_SSLTCP : 0; + socket = port_->socket_factory()->CreateClientTcpSocket( + talk_base::SocketAddress(port_->ip(), 0), ra->address, + port_->proxy(), port_->user_agent(), opts); + } else { + LOG(LS_WARNING) << "Unknown protocol (" << ra->proto << ")"; + } + + if (!socket) { + LOG(LS_WARNING) << "Socket creation failed"; + } + + // If we failed to get a socket, move on to the next protocol. + if (!socket) { + port()->thread()->Post(this, kMessageConnectTimeout); + return; + } + + // Otherwise, create the new connection and configure any socket options. + socket->SignalReadPacket.connect(this, &RelayEntry::OnReadPacket); + socket->SignalReadyToSend.connect(this, &RelayEntry::OnReadyToSend); + current_connection_ = new RelayConnection(ra, socket, port()->thread()); + for (size_t i = 0; i < port_->options().size(); ++i) { + current_connection_->SetSocketOption(port_->options()[i].first, + port_->options()[i].second); + } + + // If we're trying UDP, start binding requests. + // If we're trying TCP, wait for connection with a fixed timeout. + if ((ra->proto == PROTO_TCP) || (ra->proto == PROTO_SSLTCP)) { + socket->SignalClose.connect(this, &RelayEntry::OnSocketClose); + socket->SignalConnect.connect(this, &RelayEntry::OnSocketConnect); + port()->thread()->PostDelayed(kSoftConnectTimeoutMs, this, + kMessageConnectTimeout); + } else { + current_connection_->SendAllocateRequest(this, 0); + } +} + +int RelayEntry::GetError() { + if (current_connection_ != NULL) { + return current_connection_->GetError(); + } + return 0; +} + +RelayConnection* RelayEntry::GetBestConnection(RelayConnection* conn1, + RelayConnection* conn2) { + return conn1->GetProtocol() <= conn2->GetProtocol() ? conn1 : conn2; +} + +void RelayEntry::OnConnect(const talk_base::SocketAddress& mapped_addr, + RelayConnection* connection) { + // We are connected, notify our parent. + ProtocolType proto = PROTO_UDP; + LOG(INFO) << "Relay allocate succeeded: " << ProtoToString(proto) + << " @ " << mapped_addr.ToSensitiveString(); + connected_ = true; + + // In case of Gturn related address is set to null socket address. + // This is due to mapped address stun attribute is used for allocated + // address. + port_->set_related_address(talk_base::SocketAddress()); + port_->AddExternalAddress(ProtocolAddress(mapped_addr, proto)); + port_->SetReady(); +} + +int RelayEntry::SendTo(const void* data, size_t size, + const talk_base::SocketAddress& addr) { + // If this connection is locked to the address given, then we can send the + // packet with no wrapper. + if (locked_ && (ext_addr_ == addr)) + return SendPacket(data, size); + + // Otherwise, we must wrap the given data in a STUN SEND request so that we + // can communicate the destination address to the server. + // + // Note that we do not use a StunRequest here. This is because there is + // likely no reason to resend this packet. If it is late, we just drop it. + // The next send to this address will try again. + + RelayMessage request; + request.SetType(STUN_SEND_REQUEST); + + StunByteStringAttribute* magic_cookie_attr = + StunAttribute::CreateByteString(STUN_ATTR_MAGIC_COOKIE); + magic_cookie_attr->CopyBytes(TURN_MAGIC_COOKIE_VALUE, + sizeof(TURN_MAGIC_COOKIE_VALUE)); + VERIFY(request.AddAttribute(magic_cookie_attr)); + + StunByteStringAttribute* username_attr = + StunAttribute::CreateByteString(STUN_ATTR_USERNAME); + username_attr->CopyBytes(port_->username_fragment().c_str(), + port_->username_fragment().size()); + VERIFY(request.AddAttribute(username_attr)); + + StunAddressAttribute* addr_attr = + StunAttribute::CreateAddress(STUN_ATTR_DESTINATION_ADDRESS); + addr_attr->SetIP(addr.ipaddr()); + addr_attr->SetPort(addr.port()); + VERIFY(request.AddAttribute(addr_attr)); + + // Attempt to lock + if (ext_addr_ == addr) { + StunUInt32Attribute* options_attr = + StunAttribute::CreateUInt32(STUN_ATTR_OPTIONS); + options_attr->SetValue(0x1); + VERIFY(request.AddAttribute(options_attr)); + } + + StunByteStringAttribute* data_attr = + StunAttribute::CreateByteString(STUN_ATTR_DATA); + data_attr->CopyBytes(data, size); + VERIFY(request.AddAttribute(data_attr)); + + // TODO: compute the HMAC. + + talk_base::ByteBuffer buf; + request.Write(&buf); + + return SendPacket(buf.Data(), buf.Length()); +} + +void RelayEntry::ScheduleKeepAlive() { + if (current_connection_) { + current_connection_->SendAllocateRequest(this, kKeepAliveDelay); + } +} + +int RelayEntry::SetSocketOption(talk_base::Socket::Option opt, int value) { + // Set the option on all available sockets. + int socket_error = 0; + if (current_connection_) { + socket_error = current_connection_->SetSocketOption(opt, value); + } + return socket_error; +} + +void RelayEntry::HandleConnectFailure( + talk_base::AsyncPacketSocket* socket) { + // Make sure it's the current connection that has failed, it might + // be an old socked that has not yet been disposed. + if (!socket || + (current_connection_ && socket == current_connection_->socket())) { + if (current_connection_) + port()->SignalConnectFailure(current_connection_->protocol_address()); + + // Try to connect to the next server address. + server_index_ += 1; + Connect(); + } +} + +void RelayEntry::OnMessage(talk_base::Message *pmsg) { + ASSERT(pmsg->message_id == kMessageConnectTimeout); + if (current_connection_) { + const ProtocolAddress* ra = current_connection_->protocol_address(); + LOG(LS_WARNING) << "Relay " << ra->proto << " connection to " << + ra->address << " timed out"; + + // Currently we connect to each server address in sequence. If we + // have more addresses to try, treat this is an error and move on to + // the next address, otherwise give this connection more time and + // await the real timeout. + // + // TODO: Connect to servers in parallel to speed up connect time + // and to avoid giving up too early. + port_->SignalSoftTimeout(ra); + HandleConnectFailure(current_connection_->socket()); + } else { + HandleConnectFailure(NULL); + } +} + +void RelayEntry::OnSocketConnect(talk_base::AsyncPacketSocket* socket) { + LOG(INFO) << "relay tcp connected to " << + socket->GetRemoteAddress().ToSensitiveString(); + if (current_connection_ != NULL) { + current_connection_->SendAllocateRequest(this, 0); + } +} + +void RelayEntry::OnSocketClose(talk_base::AsyncPacketSocket* socket, + int error) { + PLOG(LERROR, error) << "Relay connection failed: socket closed"; + HandleConnectFailure(socket); +} + +void RelayEntry::OnReadPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& remote_addr) { + // ASSERT(remote_addr == port_->server_addr()); + // TODO: are we worried about this? + + if (current_connection_ == NULL || socket != current_connection_->socket()) { + // This packet comes from an unknown address. + LOG(WARNING) << "Dropping packet: unknown address"; + return; + } + + // If the magic cookie is not present, then this is an unwrapped packet sent + // by the server, The actual remote address is the one we recorded. + if (!port_->HasMagicCookie(data, size)) { + if (locked_) { + port_->OnReadPacket(data, size, ext_addr_, PROTO_UDP); + } else { + LOG(WARNING) << "Dropping packet: entry not locked"; + } + return; + } + + talk_base::ByteBuffer buf(data, size); + RelayMessage msg; + if (!msg.Read(&buf)) { + LOG(INFO) << "Incoming packet was not STUN"; + return; + } + + // The incoming packet should be a STUN ALLOCATE response, SEND response, or + // DATA indication. + if (current_connection_->CheckResponse(&msg)) { + return; + } else if (msg.type() == STUN_SEND_RESPONSE) { + if (const StunUInt32Attribute* options_attr = + msg.GetUInt32(STUN_ATTR_OPTIONS)) { + if (options_attr->value() & 0x1) { + locked_ = true; + } + } + return; + } else if (msg.type() != STUN_DATA_INDICATION) { + LOG(INFO) << "Received BAD stun type from server: " << msg.type(); + return; + } + + // This must be a data indication. + + const StunAddressAttribute* addr_attr = + msg.GetAddress(STUN_ATTR_SOURCE_ADDRESS2); + if (!addr_attr) { + LOG(INFO) << "Data indication has no source address"; + return; + } else if (addr_attr->family() != 1) { + LOG(INFO) << "Source address has bad family"; + return; + } + + talk_base::SocketAddress remote_addr2(addr_attr->ipaddr(), addr_attr->port()); + + const StunByteStringAttribute* data_attr = msg.GetByteString(STUN_ATTR_DATA); + if (!data_attr) { + LOG(INFO) << "Data indication has no data"; + return; + } + + // Process the actual data and remote address in the normal manner. + port_->OnReadPacket(data_attr->bytes(), data_attr->length(), remote_addr2, + PROTO_UDP); +} + +void RelayEntry::OnReadyToSend(talk_base::AsyncPacketSocket* socket) { + if (connected()) { + port_->OnReadyToSend(); + } +} + +int RelayEntry::SendPacket(const void* data, size_t size) { + int sent = 0; + if (current_connection_) { + // We are connected, no need to send packets anywere else than to + // the current connection. + sent = current_connection_->Send(data, size); + } + return sent; +} + +AllocateRequest::AllocateRequest(RelayEntry* entry, + RelayConnection* connection) + : StunRequest(new RelayMessage()), + entry_(entry), + connection_(connection) { + start_time_ = talk_base::Time(); +} + +void AllocateRequest::Prepare(StunMessage* request) { + request->SetType(STUN_ALLOCATE_REQUEST); + + StunByteStringAttribute* username_attr = + StunAttribute::CreateByteString(STUN_ATTR_USERNAME); + username_attr->CopyBytes( + entry_->port()->username_fragment().c_str(), + entry_->port()->username_fragment().size()); + VERIFY(request->AddAttribute(username_attr)); +} + +int AllocateRequest::GetNextDelay() { + int delay = 100 * talk_base::_max(1 << count_, 2); + count_ += 1; + if (count_ == 5) + timeout_ = true; + return delay; +} + +void AllocateRequest::OnResponse(StunMessage* response) { + const StunAddressAttribute* addr_attr = + response->GetAddress(STUN_ATTR_MAPPED_ADDRESS); + if (!addr_attr) { + LOG(INFO) << "Allocate response missing mapped address."; + } else if (addr_attr->family() != 1) { + LOG(INFO) << "Mapped address has bad family"; + } else { + talk_base::SocketAddress addr(addr_attr->ipaddr(), addr_attr->port()); + entry_->OnConnect(addr, connection_); + } + + // We will do a keep-alive regardless of whether this request suceeds. + // This should have almost no impact on network usage. + entry_->ScheduleKeepAlive(); +} + +void AllocateRequest::OnErrorResponse(StunMessage* response) { + const StunErrorCodeAttribute* attr = response->GetErrorCode(); + if (!attr) { + LOG(INFO) << "Bad allocate response error code"; + } else { + LOG(INFO) << "Allocate error response:" + << " code=" << attr->code() + << " reason='" << attr->reason() << "'"; + } + + if (talk_base::TimeSince(start_time_) <= kRetryTimeout) + entry_->ScheduleKeepAlive(); +} + +void AllocateRequest::OnTimeout() { + LOG(INFO) << "Allocate request timed out"; + entry_->HandleConnectFailure(connection_->socket()); +} + +} // namespace cricket diff --git a/talk/p2p/base/relayport.h b/talk/p2p/base/relayport.h new file mode 100644 index 000000000..a2bfb7442 --- /dev/null +++ b/talk/p2p/base/relayport.h @@ -0,0 +1,115 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_RELAYPORT_H_ +#define TALK_P2P_BASE_RELAYPORT_H_ + +#include +#include +#include +#include + +#include "talk/p2p/base/port.h" +#include "talk/p2p/base/stunrequest.h" + +namespace cricket { + +class RelayEntry; +class RelayConnection; + +// Communicates using an allocated port on the relay server. For each +// remote candidate that we try to send data to a RelayEntry instance +// is created. The RelayEntry will try to reach the remote destination +// by connecting to all available server addresses in a pre defined +// order with a small delay in between. When a connection is +// successful all other connection attemts are aborted. +class RelayPort : public Port { + public: + typedef std::pair OptionValue; + + // RelayPort doesn't yet do anything fancy in the ctor. + static RelayPort* Create( + talk_base::Thread* thread, talk_base::PacketSocketFactory* factory, + talk_base::Network* network, const talk_base::IPAddress& ip, + int min_port, int max_port, const std::string& username, + const std::string& password) { + return new RelayPort(thread, factory, network, ip, min_port, max_port, + username, password); + } + virtual ~RelayPort(); + + void AddServerAddress(const ProtocolAddress& addr); + void AddExternalAddress(const ProtocolAddress& addr); + + const std::vector& options() const { return options_; } + bool HasMagicCookie(const char* data, size_t size); + + virtual void PrepareAddress(); + virtual Connection* CreateConnection(const Candidate& address, + CandidateOrigin origin); + virtual int SetOption(talk_base::Socket::Option opt, int value); + virtual int GetOption(talk_base::Socket::Option opt, int* value); + virtual int GetError(); + + const ProtocolAddress * ServerAddress(size_t index) const; + bool IsReady() { return ready_; } + + // Used for testing. + sigslot::signal1 SignalConnectFailure; + sigslot::signal1 SignalSoftTimeout; + + protected: + RelayPort(talk_base::Thread* thread, talk_base::PacketSocketFactory* factory, + talk_base::Network*, const talk_base::IPAddress& ip, + int min_port, int max_port, const std::string& username, + const std::string& password); + bool Init(); + + void SetReady(); + + virtual int SendTo(const void* data, size_t size, + const talk_base::SocketAddress& addr, bool payload); + + // Dispatches the given packet to the port or connection as appropriate. + void OnReadPacket(const char* data, size_t size, + const talk_base::SocketAddress& remote_addr, + ProtocolType proto); + + private: + friend class RelayEntry; + + std::deque server_addr_; + std::vector external_addr_; + bool ready_; + std::vector entries_; + std::vector options_; + int error_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_RELAYPORT_H_ diff --git a/talk/p2p/base/relayport_unittest.cc b/talk/p2p/base/relayport_unittest.cc new file mode 100644 index 000000000..ced8c589b --- /dev/null +++ b/talk/p2p/base/relayport_unittest.cc @@ -0,0 +1,292 @@ +/* + * libjingle + * Copyright 2009 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/base/logging.h" +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/socketadapters.h" +#include "talk/base/socketaddress.h" +#include "talk/base/thread.h" +#include "talk/base/virtualsocketserver.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/base/relayport.h" +#include "talk/p2p/base/relayserver.h" + +using talk_base::SocketAddress; + +static const SocketAddress kLocalAddress = SocketAddress("192.168.1.2", 0); +static const SocketAddress kRelayUdpAddr = SocketAddress("99.99.99.1", 5000); +static const SocketAddress kRelayTcpAddr = SocketAddress("99.99.99.2", 5001); +static const SocketAddress kRelaySslAddr = SocketAddress("99.99.99.3", 443); +static const SocketAddress kRelayExtAddr = SocketAddress("99.99.99.3", 5002); + +static const int kTimeoutMs = 1000; +static const int kMaxTimeoutMs = 5000; + +// Tests connecting a RelayPort to a fake relay server +// (cricket::RelayServer) using all currently available protocols. The +// network layer is faked out by using a VirtualSocketServer for +// creating sockets. The test will monitor the current state of the +// RelayPort and created sockets by listening for signals such as, +// SignalConnectFailure, SignalConnectTimeout, SignalSocketClosed and +// SignalReadPacket. +class RelayPortTest : public testing::Test, + public sigslot::has_slots<> { + public: + RelayPortTest() + : main_(talk_base::Thread::Current()), + physical_socket_server_(new talk_base::PhysicalSocketServer), + virtual_socket_server_(new talk_base::VirtualSocketServer( + physical_socket_server_.get())), + ss_scope_(virtual_socket_server_.get()), + network_("unittest", "unittest", talk_base::IPAddress(INADDR_ANY), 32), + socket_factory_(talk_base::Thread::Current()), + username_(talk_base::CreateRandomString(16)), + password_(talk_base::CreateRandomString(16)), + relay_port_(cricket::RelayPort::Create(main_, &socket_factory_, + &network_, + kLocalAddress.ipaddr(), + 0, 0, username_, password_)), + relay_server_(new cricket::RelayServer(main_)) { + } + + void OnReadPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& remote_addr) { + received_packet_count_[socket]++; + } + + void OnConnectFailure(const cricket::ProtocolAddress* addr) { + failed_connections_.push_back(*addr); + } + + void OnSoftTimeout(const cricket::ProtocolAddress* addr) { + soft_timedout_connections_.push_back(*addr); + } + + protected: + static void SetUpTestCase() { + // Ensure the RNG is inited. + talk_base::InitRandom(NULL, 0); + } + + virtual void SetUp() { + // The relay server needs an external socket to work properly. + talk_base::AsyncUDPSocket* ext_socket = + CreateAsyncUdpSocket(kRelayExtAddr); + relay_server_->AddExternalSocket(ext_socket); + + // Listen for failures. + relay_port_->SignalConnectFailure. + connect(this, &RelayPortTest::OnConnectFailure); + + // Listen for soft timeouts. + relay_port_->SignalSoftTimeout. + connect(this, &RelayPortTest::OnSoftTimeout); + } + + // Udp has the highest 'goodness' value of the three different + // protocols used for connecting to the relay server. As soon as + // PrepareAddress is called, the RelayPort will start trying to + // connect to the given UDP address. As soon as a response to the + // sent STUN allocate request message has been received, the + // RelayPort will consider the connection to be complete and will + // abort any other connection attempts. + void TestConnectUdp() { + // Add a UDP socket to the relay server. + talk_base::AsyncUDPSocket* internal_udp_socket = + CreateAsyncUdpSocket(kRelayUdpAddr); + talk_base::AsyncSocket* server_socket = CreateServerSocket(kRelayTcpAddr); + + relay_server_->AddInternalSocket(internal_udp_socket); + relay_server_->AddInternalServerSocket(server_socket, cricket::PROTO_TCP); + + // Now add our relay addresses to the relay port and let it start. + relay_port_->AddServerAddress( + cricket::ProtocolAddress(kRelayUdpAddr, cricket::PROTO_UDP)); + relay_port_->AddServerAddress( + cricket::ProtocolAddress(kRelayTcpAddr, cricket::PROTO_TCP)); + relay_port_->PrepareAddress(); + + // Should be connected. + EXPECT_TRUE_WAIT(relay_port_->IsReady(), kTimeoutMs); + + // Make sure that we are happy with UDP, ie. not continuing with + // TCP, SSLTCP, etc. + WAIT(relay_server_->HasConnection(kRelayTcpAddr), kTimeoutMs); + + // Should have only one connection. + EXPECT_EQ(1, relay_server_->GetConnectionCount()); + + // Should be the UDP address. + EXPECT_TRUE(relay_server_->HasConnection(kRelayUdpAddr)); + } + + // TCP has the second best 'goodness' value, and as soon as UDP + // connection has failed, the RelayPort will attempt to connect via + // TCP. Here we add a fake UDP address together with a real TCP + // address to simulate an UDP failure. As soon as UDP has failed the + // RelayPort will try the TCP adress and succed. + void TestConnectTcp() { + // Create a fake UDP address for relay port to simulate a failure. + cricket::ProtocolAddress fake_protocol_address = + cricket::ProtocolAddress(kRelayUdpAddr, cricket::PROTO_UDP); + + // Create a server socket for the RelayServer. + talk_base::AsyncSocket* server_socket = CreateServerSocket(kRelayTcpAddr); + relay_server_->AddInternalServerSocket(server_socket, cricket::PROTO_TCP); + + // Add server addresses to the relay port and let it start. + relay_port_->AddServerAddress( + cricket::ProtocolAddress(fake_protocol_address)); + relay_port_->AddServerAddress( + cricket::ProtocolAddress(kRelayTcpAddr, cricket::PROTO_TCP)); + relay_port_->PrepareAddress(); + + EXPECT_FALSE(relay_port_->IsReady()); + + // Should have timed out in 200 + 200 + 400 + 800 + 1600 ms. + EXPECT_TRUE_WAIT(HasFailed(&fake_protocol_address), 3600); + + // Wait until relayport is ready. + EXPECT_TRUE_WAIT(relay_port_->IsReady(), kMaxTimeoutMs); + + // Should have only one connection. + EXPECT_EQ(1, relay_server_->GetConnectionCount()); + + // Should be the TCP address. + EXPECT_TRUE(relay_server_->HasConnection(kRelayTcpAddr)); + } + + void TestConnectSslTcp() { + // Create a fake TCP address for relay port to simulate a failure. + // We skip UDP here since transition from UDP to TCP has been + // tested above. + cricket::ProtocolAddress fake_protocol_address = + cricket::ProtocolAddress(kRelayTcpAddr, cricket::PROTO_TCP); + + // Create a ssl server socket for the RelayServer. + talk_base::AsyncSocket* ssl_server_socket = + CreateServerSocket(kRelaySslAddr); + relay_server_->AddInternalServerSocket(ssl_server_socket, + cricket::PROTO_SSLTCP); + + // Create a tcp server socket that listens on the fake address so + // the relay port can attempt to connect to it. + talk_base::scoped_ptr tcp_server_socket( + CreateServerSocket(kRelayTcpAddr)); + + // Add server addresses to the relay port and let it start. + relay_port_->AddServerAddress(fake_protocol_address); + relay_port_->AddServerAddress( + cricket::ProtocolAddress(kRelaySslAddr, cricket::PROTO_SSLTCP)); + relay_port_->PrepareAddress(); + EXPECT_FALSE(relay_port_->IsReady()); + + // Should have timed out in 3000 ms(relayport.cc, kSoftConnectTimeoutMs). + EXPECT_TRUE_WAIT_MARGIN(HasTimedOut(&fake_protocol_address), 3000, 100); + + // Wait until relayport is ready. + EXPECT_TRUE_WAIT(relay_port_->IsReady(), kMaxTimeoutMs); + + // Should have only one connection. + EXPECT_EQ(1, relay_server_->GetConnectionCount()); + + // Should be the SSLTCP address. + EXPECT_TRUE(relay_server_->HasConnection(kRelaySslAddr)); + } + + private: + talk_base::AsyncUDPSocket* CreateAsyncUdpSocket(const SocketAddress addr) { + talk_base::AsyncSocket* socket = + virtual_socket_server_->CreateAsyncSocket(SOCK_DGRAM); + talk_base::AsyncUDPSocket* packet_socket = + talk_base::AsyncUDPSocket::Create(socket, addr); + EXPECT_TRUE(packet_socket != NULL); + packet_socket->SignalReadPacket.connect(this, &RelayPortTest::OnReadPacket); + return packet_socket; + } + + talk_base::AsyncSocket* CreateServerSocket(const SocketAddress addr) { + talk_base::AsyncSocket* socket = + virtual_socket_server_->CreateAsyncSocket(SOCK_STREAM); + EXPECT_GE(socket->Bind(addr), 0); + EXPECT_GE(socket->Listen(5), 0); + return socket; + } + + bool HasFailed(cricket::ProtocolAddress* addr) { + for (size_t i = 0; i < failed_connections_.size(); i++) { + if (failed_connections_[i].address == addr->address && + failed_connections_[i].proto == addr->proto) { + return true; + } + } + return false; + } + + bool HasTimedOut(cricket::ProtocolAddress* addr) { + for (size_t i = 0; i < soft_timedout_connections_.size(); i++) { + if (soft_timedout_connections_[i].address == addr->address && + soft_timedout_connections_[i].proto == addr->proto) { + return true; + } + } + return false; + } + + typedef std::map PacketMap; + + talk_base::Thread* main_; + talk_base::scoped_ptr + physical_socket_server_; + talk_base::scoped_ptr virtual_socket_server_; + talk_base::SocketServerScope ss_scope_; + talk_base::Network network_; + talk_base::BasicPacketSocketFactory socket_factory_; + std::string username_; + std::string password_; + talk_base::scoped_ptr relay_port_; + talk_base::scoped_ptr relay_server_; + std::vector failed_connections_; + std::vector soft_timedout_connections_; + PacketMap received_packet_count_; +}; + +TEST_F(RelayPortTest, ConnectUdp) { + TestConnectUdp(); +} + +TEST_F(RelayPortTest, ConnectTcp) { + TestConnectTcp(); +} + +TEST_F(RelayPortTest, ConnectSslTcp) { + TestConnectSslTcp(); +} diff --git a/talk/p2p/base/relayserver.cc b/talk/p2p/base/relayserver.cc new file mode 100644 index 000000000..0470e9e7b --- /dev/null +++ b/talk/p2p/base/relayserver.cc @@ -0,0 +1,756 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/relayserver.h" + +#ifdef POSIX +#include +#endif // POSIX + +#include + +#include "talk/base/asynctcpsocket.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/socketadapters.h" + +namespace cricket { + +// By default, we require a ping every 90 seconds. +const int MAX_LIFETIME = 15 * 60 * 1000; + +// The number of bytes in each of the usernames we use. +const uint32 USERNAME_LENGTH = 16; + +static const uint32 kMessageAcceptConnection = 1; + +// Calls SendTo on the given socket and logs any bad results. +void Send(talk_base::AsyncPacketSocket* socket, const char* bytes, size_t size, + const talk_base::SocketAddress& addr) { + int result = socket->SendTo(bytes, size, addr); + if (result < static_cast(size)) { + LOG(LS_ERROR) << "SendTo wrote only " << result << " of " << size + << " bytes"; + } else if (result < 0) { + LOG_ERR(LS_ERROR) << "SendTo"; + } +} + +// Sends the given STUN message on the given socket. +void SendStun(const StunMessage& msg, + talk_base::AsyncPacketSocket* socket, + const talk_base::SocketAddress& addr) { + talk_base::ByteBuffer buf; + msg.Write(&buf); + Send(socket, buf.Data(), buf.Length(), addr); +} + +// Constructs a STUN error response and sends it on the given socket. +void SendStunError(const StunMessage& msg, talk_base::AsyncPacketSocket* socket, + const talk_base::SocketAddress& remote_addr, int error_code, + const char* error_desc, const std::string& magic_cookie) { + RelayMessage err_msg; + err_msg.SetType(GetStunErrorResponseType(msg.type())); + err_msg.SetTransactionID(msg.transaction_id()); + + StunByteStringAttribute* magic_cookie_attr = + StunAttribute::CreateByteString(cricket::STUN_ATTR_MAGIC_COOKIE); + if (magic_cookie.size() == 0) { + magic_cookie_attr->CopyBytes(cricket::TURN_MAGIC_COOKIE_VALUE, + sizeof(cricket::TURN_MAGIC_COOKIE_VALUE)); + } else { + magic_cookie_attr->CopyBytes(magic_cookie.c_str(), magic_cookie.size()); + } + err_msg.AddAttribute(magic_cookie_attr); + + StunErrorCodeAttribute* err_code = StunAttribute::CreateErrorCode(); + err_code->SetClass(error_code / 100); + err_code->SetNumber(error_code % 100); + err_code->SetReason(error_desc); + err_msg.AddAttribute(err_code); + + SendStun(err_msg, socket, remote_addr); +} + +RelayServer::RelayServer(talk_base::Thread* thread) + : thread_(thread), log_bindings_(true) { +} + +RelayServer::~RelayServer() { + // Deleting the binding will cause it to be removed from the map. + while (!bindings_.empty()) + delete bindings_.begin()->second; + for (size_t i = 0; i < internal_sockets_.size(); ++i) + delete internal_sockets_[i]; + for (size_t i = 0; i < external_sockets_.size(); ++i) + delete external_sockets_[i]; + while (!server_sockets_.empty()) { + talk_base::AsyncSocket* socket = server_sockets_.begin()->first; + server_sockets_.erase(server_sockets_.begin()->first); + delete socket; + } +} + +void RelayServer::AddInternalSocket(talk_base::AsyncPacketSocket* socket) { + ASSERT(internal_sockets_.end() == + std::find(internal_sockets_.begin(), internal_sockets_.end(), socket)); + internal_sockets_.push_back(socket); + socket->SignalReadPacket.connect(this, &RelayServer::OnInternalPacket); +} + +void RelayServer::RemoveInternalSocket(talk_base::AsyncPacketSocket* socket) { + SocketList::iterator iter = + std::find(internal_sockets_.begin(), internal_sockets_.end(), socket); + ASSERT(iter != internal_sockets_.end()); + internal_sockets_.erase(iter); + socket->SignalReadPacket.disconnect(this); +} + +void RelayServer::AddExternalSocket(talk_base::AsyncPacketSocket* socket) { + ASSERT(external_sockets_.end() == + std::find(external_sockets_.begin(), external_sockets_.end(), socket)); + external_sockets_.push_back(socket); + socket->SignalReadPacket.connect(this, &RelayServer::OnExternalPacket); +} + +void RelayServer::RemoveExternalSocket(talk_base::AsyncPacketSocket* socket) { + SocketList::iterator iter = + std::find(external_sockets_.begin(), external_sockets_.end(), socket); + ASSERT(iter != external_sockets_.end()); + external_sockets_.erase(iter); + socket->SignalReadPacket.disconnect(this); +} + +void RelayServer::AddInternalServerSocket(talk_base::AsyncSocket* socket, + cricket::ProtocolType proto) { + ASSERT(server_sockets_.end() == + server_sockets_.find(socket)); + server_sockets_[socket] = proto; + socket->SignalReadEvent.connect(this, &RelayServer::OnReadEvent); +} + +void RelayServer::RemoveInternalServerSocket( + talk_base::AsyncSocket* socket) { + ServerSocketMap::iterator iter = server_sockets_.find(socket); + ASSERT(iter != server_sockets_.end()); + server_sockets_.erase(iter); + socket->SignalReadEvent.disconnect(this); +} + +int RelayServer::GetConnectionCount() const { + return connections_.size(); +} + +talk_base::SocketAddressPair RelayServer::GetConnection(int connection) const { + int i = 0; + for (ConnectionMap::const_iterator it = connections_.begin(); + it != connections_.end(); ++it) { + if (i == connection) { + return it->second->addr_pair(); + } + ++i; + } + return talk_base::SocketAddressPair(); +} + +bool RelayServer::HasConnection(const talk_base::SocketAddress& address) const { + for (ConnectionMap::const_iterator it = connections_.begin(); + it != connections_.end(); ++it) { + if (it->second->addr_pair().destination() == address) { + return true; + } + } + return false; +} + +void RelayServer::OnReadEvent(talk_base::AsyncSocket* socket) { + ASSERT(server_sockets_.find(socket) != server_sockets_.end()); + AcceptConnection(socket); +} + +void RelayServer::OnInternalPacket( + talk_base::AsyncPacketSocket* socket, const char* bytes, size_t size, + const talk_base::SocketAddress& remote_addr) { + + // Get the address of the connection we just received on. + talk_base::SocketAddressPair ap(remote_addr, socket->GetLocalAddress()); + ASSERT(!ap.destination().IsNil()); + + // If this did not come from an existing connection, it should be a STUN + // allocate request. + ConnectionMap::iterator piter = connections_.find(ap); + if (piter == connections_.end()) { + HandleStunAllocate(bytes, size, ap, socket); + return; + } + + RelayServerConnection* int_conn = piter->second; + + // Handle STUN requests to the server itself. + if (int_conn->binding()->HasMagicCookie(bytes, size)) { + HandleStun(int_conn, bytes, size); + return; + } + + // Otherwise, this is a non-wrapped packet that we are to forward. Make sure + // that this connection has been locked. (Otherwise, we would not know what + // address to forward to.) + if (!int_conn->locked()) { + LOG(LS_WARNING) << "Dropping packet: connection not locked"; + return; + } + + // Forward this to the destination address into the connection. + RelayServerConnection* ext_conn = int_conn->binding()->GetExternalConnection( + int_conn->default_destination()); + if (ext_conn && ext_conn->locked()) { + // TODO: Check the HMAC. + ext_conn->Send(bytes, size); + } else { + // This happens very often and is not an error. + LOG(LS_INFO) << "Dropping packet: no external connection"; + } +} + +void RelayServer::OnExternalPacket( + talk_base::AsyncPacketSocket* socket, const char* bytes, size_t size, + const talk_base::SocketAddress& remote_addr) { + + // Get the address of the connection we just received on. + talk_base::SocketAddressPair ap(remote_addr, socket->GetLocalAddress()); + ASSERT(!ap.destination().IsNil()); + + // If this connection already exists, then forward the traffic. + ConnectionMap::iterator piter = connections_.find(ap); + if (piter != connections_.end()) { + // TODO: Check the HMAC. + RelayServerConnection* ext_conn = piter->second; + RelayServerConnection* int_conn = + ext_conn->binding()->GetInternalConnection( + ext_conn->addr_pair().source()); + ASSERT(int_conn != NULL); + int_conn->Send(bytes, size, ext_conn->addr_pair().source()); + ext_conn->Lock(); // allow outgoing packets + return; + } + + // The first packet should always be a STUN / TURN packet. If it isn't, then + // we should just ignore this packet. + RelayMessage msg; + talk_base::ByteBuffer buf(bytes, size); + if (!msg.Read(&buf)) { + LOG(LS_WARNING) << "Dropping packet: first packet not STUN"; + return; + } + + // The initial packet should have a username (which identifies the binding). + const StunByteStringAttribute* username_attr = + msg.GetByteString(STUN_ATTR_USERNAME); + if (!username_attr) { + LOG(LS_WARNING) << "Dropping packet: no username"; + return; + } + + uint32 length = talk_base::_min(static_cast(username_attr->length()), + USERNAME_LENGTH); + std::string username(username_attr->bytes(), length); + // TODO: Check the HMAC. + + // The binding should already be present. + BindingMap::iterator biter = bindings_.find(username); + if (biter == bindings_.end()) { + LOG(LS_WARNING) << "Dropping packet: no binding with username"; + return; + } + + // Add this authenticted connection to the binding. + RelayServerConnection* ext_conn = + new RelayServerConnection(biter->second, ap, socket); + ext_conn->binding()->AddExternalConnection(ext_conn); + AddConnection(ext_conn); + + // We always know where external packets should be forwarded, so we can lock + // them from the beginning. + ext_conn->Lock(); + + // Send this message on the appropriate internal connection. + RelayServerConnection* int_conn = ext_conn->binding()->GetInternalConnection( + ext_conn->addr_pair().source()); + ASSERT(int_conn != NULL); + int_conn->Send(bytes, size, ext_conn->addr_pair().source()); +} + +bool RelayServer::HandleStun( + const char* bytes, size_t size, const talk_base::SocketAddress& remote_addr, + talk_base::AsyncPacketSocket* socket, std::string* username, + StunMessage* msg) { + + // Parse this into a stun message. Eat the message if this fails. + talk_base::ByteBuffer buf(bytes, size); + if (!msg->Read(&buf)) { + return false; + } + + // The initial packet should have a username (which identifies the binding). + const StunByteStringAttribute* username_attr = + msg->GetByteString(STUN_ATTR_USERNAME); + if (!username_attr) { + SendStunError(*msg, socket, remote_addr, 432, "Missing Username", ""); + return false; + } + + // Record the username if requested. + if (username) + username->append(username_attr->bytes(), username_attr->length()); + + // TODO: Check for unknown attributes (<= 0x7fff) + + return true; +} + +void RelayServer::HandleStunAllocate( + const char* bytes, size_t size, const talk_base::SocketAddressPair& ap, + talk_base::AsyncPacketSocket* socket) { + + // Make sure this is a valid STUN request. + RelayMessage request; + std::string username; + if (!HandleStun(bytes, size, ap.source(), socket, &username, &request)) + return; + + // Make sure this is a an allocate request. + if (request.type() != STUN_ALLOCATE_REQUEST) { + SendStunError(request, + socket, + ap.source(), + 600, + "Operation Not Supported", + ""); + return; + } + + // TODO: Check the HMAC. + + // Find or create the binding for this username. + + RelayServerBinding* binding; + + BindingMap::iterator biter = bindings_.find(username); + if (biter != bindings_.end()) { + binding = biter->second; + } else { + // NOTE: In the future, bindings will be created by the bot only. This + // else-branch will then disappear. + + // Compute the appropriate lifetime for this binding. + uint32 lifetime = MAX_LIFETIME; + const StunUInt32Attribute* lifetime_attr = + request.GetUInt32(STUN_ATTR_LIFETIME); + if (lifetime_attr) + lifetime = talk_base::_min(lifetime, lifetime_attr->value() * 1000); + + binding = new RelayServerBinding(this, username, "0", lifetime); + binding->SignalTimeout.connect(this, &RelayServer::OnTimeout); + bindings_[username] = binding; + + if (log_bindings_) { + LOG(LS_INFO) << "Added new binding " << username << ", " + << bindings_.size() << " total"; + } + } + + // Add this connection to the binding. It starts out unlocked. + RelayServerConnection* int_conn = + new RelayServerConnection(binding, ap, socket); + binding->AddInternalConnection(int_conn); + AddConnection(int_conn); + + // Now that we have a connection, this other method takes over. + HandleStunAllocate(int_conn, request); +} + +void RelayServer::HandleStun( + RelayServerConnection* int_conn, const char* bytes, size_t size) { + + // Make sure this is a valid STUN request. + RelayMessage request; + std::string username; + if (!HandleStun(bytes, size, int_conn->addr_pair().source(), + int_conn->socket(), &username, &request)) + return; + + // Make sure the username is the one were were expecting. + if (username != int_conn->binding()->username()) { + int_conn->SendStunError(request, 430, "Stale Credentials"); + return; + } + + // TODO: Check the HMAC. + + // Send this request to the appropriate handler. + if (request.type() == STUN_SEND_REQUEST) + HandleStunSend(int_conn, request); + else if (request.type() == STUN_ALLOCATE_REQUEST) + HandleStunAllocate(int_conn, request); + else + int_conn->SendStunError(request, 600, "Operation Not Supported"); +} + +void RelayServer::HandleStunAllocate( + RelayServerConnection* int_conn, const StunMessage& request) { + + // Create a response message that includes an address with which external + // clients can communicate. + + RelayMessage response; + response.SetType(STUN_ALLOCATE_RESPONSE); + response.SetTransactionID(request.transaction_id()); + + StunByteStringAttribute* magic_cookie_attr = + StunAttribute::CreateByteString(cricket::STUN_ATTR_MAGIC_COOKIE); + magic_cookie_attr->CopyBytes(int_conn->binding()->magic_cookie().c_str(), + int_conn->binding()->magic_cookie().size()); + response.AddAttribute(magic_cookie_attr); + + size_t index = rand() % external_sockets_.size(); + talk_base::SocketAddress ext_addr = + external_sockets_[index]->GetLocalAddress(); + + StunAddressAttribute* addr_attr = + StunAttribute::CreateAddress(STUN_ATTR_MAPPED_ADDRESS); + addr_attr->SetIP(ext_addr.ipaddr()); + addr_attr->SetPort(ext_addr.port()); + response.AddAttribute(addr_attr); + + StunUInt32Attribute* res_lifetime_attr = + StunAttribute::CreateUInt32(STUN_ATTR_LIFETIME); + res_lifetime_attr->SetValue(int_conn->binding()->lifetime() / 1000); + response.AddAttribute(res_lifetime_attr); + + // TODO: Support transport-prefs (preallocate RTCP port). + // TODO: Support bandwidth restrictions. + // TODO: Add message integrity check. + + // Send a response to the caller. + int_conn->SendStun(response); +} + +void RelayServer::HandleStunSend( + RelayServerConnection* int_conn, const StunMessage& request) { + + const StunAddressAttribute* addr_attr = + request.GetAddress(STUN_ATTR_DESTINATION_ADDRESS); + if (!addr_attr) { + int_conn->SendStunError(request, 400, "Bad Request"); + return; + } + + const StunByteStringAttribute* data_attr = + request.GetByteString(STUN_ATTR_DATA); + if (!data_attr) { + int_conn->SendStunError(request, 400, "Bad Request"); + return; + } + + talk_base::SocketAddress ext_addr(addr_attr->ipaddr(), addr_attr->port()); + RelayServerConnection* ext_conn = + int_conn->binding()->GetExternalConnection(ext_addr); + if (!ext_conn) { + // Create a new connection to establish the relationship with this binding. + ASSERT(external_sockets_.size() == 1); + talk_base::AsyncPacketSocket* socket = external_sockets_[0]; + talk_base::SocketAddressPair ap(ext_addr, socket->GetLocalAddress()); + ext_conn = new RelayServerConnection(int_conn->binding(), ap, socket); + ext_conn->binding()->AddExternalConnection(ext_conn); + AddConnection(ext_conn); + } + + // If this connection has pinged us, then allow outgoing traffic. + if (ext_conn->locked()) + ext_conn->Send(data_attr->bytes(), data_attr->length()); + + const StunUInt32Attribute* options_attr = + request.GetUInt32(STUN_ATTR_OPTIONS); + if (options_attr && (options_attr->value() & 0x01)) { + int_conn->set_default_destination(ext_addr); + int_conn->Lock(); + + RelayMessage response; + response.SetType(STUN_SEND_RESPONSE); + response.SetTransactionID(request.transaction_id()); + + StunByteStringAttribute* magic_cookie_attr = + StunAttribute::CreateByteString(cricket::STUN_ATTR_MAGIC_COOKIE); + magic_cookie_attr->CopyBytes(int_conn->binding()->magic_cookie().c_str(), + int_conn->binding()->magic_cookie().size()); + response.AddAttribute(magic_cookie_attr); + + StunUInt32Attribute* options2_attr = + StunAttribute::CreateUInt32(cricket::STUN_ATTR_OPTIONS); + options2_attr->SetValue(0x01); + response.AddAttribute(options2_attr); + + int_conn->SendStun(response); + } +} + +void RelayServer::AddConnection(RelayServerConnection* conn) { + ASSERT(connections_.find(conn->addr_pair()) == connections_.end()); + connections_[conn->addr_pair()] = conn; +} + +void RelayServer::RemoveConnection(RelayServerConnection* conn) { + ConnectionMap::iterator iter = connections_.find(conn->addr_pair()); + ASSERT(iter != connections_.end()); + connections_.erase(iter); +} + +void RelayServer::RemoveBinding(RelayServerBinding* binding) { + BindingMap::iterator iter = bindings_.find(binding->username()); + ASSERT(iter != bindings_.end()); + bindings_.erase(iter); + + if (log_bindings_) { + LOG(LS_INFO) << "Removed binding " << binding->username() << ", " + << bindings_.size() << " remaining"; + } +} + +void RelayServer::OnMessage(talk_base::Message *pmsg) { + ASSERT(pmsg->message_id == kMessageAcceptConnection); + talk_base::MessageData* data = pmsg->pdata; + talk_base::AsyncSocket* socket = + static_cast *> + (data)->data(); + AcceptConnection(socket); + delete data; +} + +void RelayServer::OnTimeout(RelayServerBinding* binding) { + // This call will result in all of the necessary clean-up. We can't call + // delete here, because you can't delete an object that is signaling you. + thread_->Dispose(binding); +} + +void RelayServer::AcceptConnection(talk_base::AsyncSocket* server_socket) { + // Check if someone is trying to connect to us. + talk_base::SocketAddress accept_addr; + talk_base::AsyncSocket* accepted_socket = + server_socket->Accept(&accept_addr); + if (accepted_socket != NULL) { + // We had someone trying to connect, now check which protocol to + // use and create a packet socket. + ASSERT(server_sockets_[server_socket] == cricket::PROTO_TCP || + server_sockets_[server_socket] == cricket::PROTO_SSLTCP); + if (server_sockets_[server_socket] == cricket::PROTO_SSLTCP) { + accepted_socket = new talk_base::AsyncSSLServerSocket(accepted_socket); + } + talk_base::AsyncTCPSocket* tcp_socket = + new talk_base::AsyncTCPSocket(accepted_socket, false); + + // Finally add the socket so it can start communicating with the client. + AddInternalSocket(tcp_socket); + } +} + +RelayServerConnection::RelayServerConnection( + RelayServerBinding* binding, const talk_base::SocketAddressPair& addrs, + talk_base::AsyncPacketSocket* socket) + : binding_(binding), addr_pair_(addrs), socket_(socket), locked_(false) { + // The creation of a new connection constitutes a use of the binding. + binding_->NoteUsed(); +} + +RelayServerConnection::~RelayServerConnection() { + // Remove this connection from the server's map (if it exists there). + binding_->server()->RemoveConnection(this); +} + +void RelayServerConnection::Send(const char* data, size_t size) { + // Note that the binding has been used again. + binding_->NoteUsed(); + + cricket::Send(socket_, data, size, addr_pair_.source()); +} + +void RelayServerConnection::Send( + const char* data, size_t size, const talk_base::SocketAddress& from_addr) { + // If the from address is known to the client, we don't need to send it. + if (locked() && (from_addr == default_dest_)) { + Send(data, size); + return; + } + + // Wrap the given data in a data-indication packet. + + RelayMessage msg; + msg.SetType(STUN_DATA_INDICATION); + + StunByteStringAttribute* magic_cookie_attr = + StunAttribute::CreateByteString(cricket::STUN_ATTR_MAGIC_COOKIE); + magic_cookie_attr->CopyBytes(binding_->magic_cookie().c_str(), + binding_->magic_cookie().size()); + msg.AddAttribute(magic_cookie_attr); + + StunAddressAttribute* addr_attr = + StunAttribute::CreateAddress(STUN_ATTR_SOURCE_ADDRESS2); + addr_attr->SetIP(from_addr.ipaddr()); + addr_attr->SetPort(from_addr.port()); + msg.AddAttribute(addr_attr); + + StunByteStringAttribute* data_attr = + StunAttribute::CreateByteString(STUN_ATTR_DATA); + ASSERT(size <= 65536); + data_attr->CopyBytes(data, uint16(size)); + msg.AddAttribute(data_attr); + + SendStun(msg); +} + +void RelayServerConnection::SendStun(const StunMessage& msg) { + // Note that the binding has been used again. + binding_->NoteUsed(); + + cricket::SendStun(msg, socket_, addr_pair_.source()); +} + +void RelayServerConnection::SendStunError( + const StunMessage& request, int error_code, const char* error_desc) { + // An error does not indicate use. If no legitimate use off the binding + // occurs, we want it to be cleaned up even if errors are still occuring. + + cricket::SendStunError( + request, socket_, addr_pair_.source(), error_code, error_desc, + binding_->magic_cookie()); +} + +void RelayServerConnection::Lock() { + locked_ = true; +} + +void RelayServerConnection::Unlock() { + locked_ = false; +} + +// IDs used for posted messages: +const uint32 MSG_LIFETIME_TIMER = 1; + +RelayServerBinding::RelayServerBinding( + RelayServer* server, const std::string& username, + const std::string& password, uint32 lifetime) + : server_(server), username_(username), password_(password), + lifetime_(lifetime) { + // For now, every connection uses the standard magic cookie value. + magic_cookie_.append( + reinterpret_cast(TURN_MAGIC_COOKIE_VALUE), + sizeof(TURN_MAGIC_COOKIE_VALUE)); + + // Initialize the last-used time to now. + NoteUsed(); + + // Set the first timeout check. + server_->thread()->PostDelayed(lifetime_, this, MSG_LIFETIME_TIMER); +} + +RelayServerBinding::~RelayServerBinding() { + // Clear the outstanding timeout check. + server_->thread()->Clear(this); + + // Clean up all of the connections. + for (size_t i = 0; i < internal_connections_.size(); ++i) + delete internal_connections_[i]; + for (size_t i = 0; i < external_connections_.size(); ++i) + delete external_connections_[i]; + + // Remove this binding from the server's map. + server_->RemoveBinding(this); +} + +void RelayServerBinding::AddInternalConnection(RelayServerConnection* conn) { + internal_connections_.push_back(conn); +} + +void RelayServerBinding::AddExternalConnection(RelayServerConnection* conn) { + external_connections_.push_back(conn); +} + +void RelayServerBinding::NoteUsed() { + last_used_ = talk_base::Time(); +} + +bool RelayServerBinding::HasMagicCookie(const char* bytes, size_t size) const { + if (size < 24 + magic_cookie_.size()) { + return false; + } else { + return 0 == std::memcmp( + bytes + 24, magic_cookie_.c_str(), magic_cookie_.size()); + } +} + +RelayServerConnection* RelayServerBinding::GetInternalConnection( + const talk_base::SocketAddress& ext_addr) { + + // Look for an internal connection that is locked to this address. + for (size_t i = 0; i < internal_connections_.size(); ++i) { + if (internal_connections_[i]->locked() && + (ext_addr == internal_connections_[i]->default_destination())) + return internal_connections_[i]; + } + + // If one was not found, we send to the first connection. + ASSERT(internal_connections_.size() > 0); + return internal_connections_[0]; +} + +RelayServerConnection* RelayServerBinding::GetExternalConnection( + const talk_base::SocketAddress& ext_addr) { + for (size_t i = 0; i < external_connections_.size(); ++i) { + if (ext_addr == external_connections_[i]->addr_pair().source()) + return external_connections_[i]; + } + return 0; +} + +void RelayServerBinding::OnMessage(talk_base::Message *pmsg) { + if (pmsg->message_id == MSG_LIFETIME_TIMER) { + ASSERT(!pmsg->pdata); + + // If the lifetime timeout has been exceeded, then send a signal. + // Otherwise, just keep waiting. + if (talk_base::Time() >= last_used_ + lifetime_) { + LOG(LS_INFO) << "Expiring binding " << username_; + SignalTimeout(this); + } else { + server_->thread()->PostDelayed(lifetime_, this, MSG_LIFETIME_TIMER); + } + + } else { + ASSERT(false); + } +} + +} // namespace cricket diff --git a/talk/p2p/base/relayserver.h b/talk/p2p/base/relayserver.h new file mode 100644 index 000000000..a5fcc243d --- /dev/null +++ b/talk/p2p/base/relayserver.h @@ -0,0 +1,249 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_RELAYSERVER_H_ +#define TALK_P2P_BASE_RELAYSERVER_H_ + +#include +#include +#include + +#include "talk/base/asyncudpsocket.h" +#include "talk/base/socketaddresspair.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" +#include "talk/p2p/base/port.h" +#include "talk/p2p/base/stun.h" + +namespace cricket { + +class RelayServerBinding; +class RelayServerConnection; + +// Relays traffic between connections to the server that are "bound" together. +// All connections created with the same username/password are bound together. +class RelayServer : public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + // Creates a server, which will use this thread to post messages to itself. + explicit RelayServer(talk_base::Thread* thread); + ~RelayServer(); + + talk_base::Thread* thread() { return thread_; } + + // Indicates whether we will print updates of the number of bindings. + bool log_bindings() const { return log_bindings_; } + void set_log_bindings(bool log_bindings) { log_bindings_ = log_bindings; } + + // Updates the set of sockets that the server uses to talk to "internal" + // clients. These are clients that do the "port allocations". + void AddInternalSocket(talk_base::AsyncPacketSocket* socket); + void RemoveInternalSocket(talk_base::AsyncPacketSocket* socket); + + // Updates the set of sockets that the server uses to talk to "external" + // clients. These are the clients that do not do allocations. They do not + // know that these addresses represent a relay server. + void AddExternalSocket(talk_base::AsyncPacketSocket* socket); + void RemoveExternalSocket(talk_base::AsyncPacketSocket* socket); + + // Starts listening for connections on this sockets. When someone + // tries to connect, the connection will be accepted and a new + // internal socket will be added. + void AddInternalServerSocket(talk_base::AsyncSocket* socket, + cricket::ProtocolType proto); + + // Removes this server socket from the list. + void RemoveInternalServerSocket(talk_base::AsyncSocket* socket); + + // Methods for testing and debuging. + int GetConnectionCount() const; + talk_base::SocketAddressPair GetConnection(int connection) const; + bool HasConnection(const talk_base::SocketAddress& address) const; + + private: + typedef std::vector SocketList; + typedef std::map ServerSocketMap; + typedef std::map BindingMap; + typedef std::map ConnectionMap; + + talk_base::Thread* thread_; + bool log_bindings_; + SocketList internal_sockets_; + SocketList external_sockets_; + ServerSocketMap server_sockets_; + BindingMap bindings_; + ConnectionMap connections_; + + // Called when a packet is received by the server on one of its sockets. + void OnInternalPacket(talk_base::AsyncPacketSocket* socket, + const char* bytes, size_t size, + const talk_base::SocketAddress& remote_addr); + void OnExternalPacket(talk_base::AsyncPacketSocket* socket, + const char* bytes, size_t size, + const talk_base::SocketAddress& remote_addr); + + void OnReadEvent(talk_base::AsyncSocket* socket); + + // Processes the relevant STUN request types from the client. + bool HandleStun(const char* bytes, size_t size, + const talk_base::SocketAddress& remote_addr, + talk_base::AsyncPacketSocket* socket, + std::string* username, StunMessage* msg); + void HandleStunAllocate(const char* bytes, size_t size, + const talk_base::SocketAddressPair& ap, + talk_base::AsyncPacketSocket* socket); + void HandleStun(RelayServerConnection* int_conn, const char* bytes, + size_t size); + void HandleStunAllocate(RelayServerConnection* int_conn, + const StunMessage& msg); + void HandleStunSend(RelayServerConnection* int_conn, const StunMessage& msg); + + // Adds/Removes the a connection or binding. + void AddConnection(RelayServerConnection* conn); + void RemoveConnection(RelayServerConnection* conn); + void RemoveBinding(RelayServerBinding* binding); + + // Handle messages in our worker thread. + void OnMessage(talk_base::Message *pmsg); + + // Called when the timer for checking lifetime times out. + void OnTimeout(RelayServerBinding* binding); + + // Accept connections on this server socket. + void AcceptConnection(talk_base::AsyncSocket* server_socket); + + friend class RelayServerConnection; + friend class RelayServerBinding; +}; + +// Maintains information about a connection to the server. Each connection is +// part of one and only one binding. +class RelayServerConnection { + public: + RelayServerConnection(RelayServerBinding* binding, + const talk_base::SocketAddressPair& addrs, + talk_base::AsyncPacketSocket* socket); + ~RelayServerConnection(); + + RelayServerBinding* binding() { return binding_; } + talk_base::AsyncPacketSocket* socket() { return socket_; } + + // Returns a pair where the source is the remote address and the destination + // is the local address. + const talk_base::SocketAddressPair& addr_pair() { return addr_pair_; } + + // Sends a packet to the connected client. If an address is provided, then + // we make sure the internal client receives it, wrapping if necessary. + void Send(const char* data, size_t size); + void Send(const char* data, size_t size, + const talk_base::SocketAddress& ext_addr); + + // Sends a STUN message to the connected client with no wrapping. + void SendStun(const StunMessage& msg); + void SendStunError(const StunMessage& request, int code, const char* desc); + + // A locked connection is one for which we know the intended destination of + // any raw packet received. + bool locked() const { return locked_; } + void Lock(); + void Unlock(); + + // Records the address that raw packets should be forwarded to (for internal + // packets only; for external, we already know where they go). + const talk_base::SocketAddress& default_destination() const { + return default_dest_; + } + void set_default_destination(const talk_base::SocketAddress& addr) { + default_dest_ = addr; + } + + private: + RelayServerBinding* binding_; + talk_base::SocketAddressPair addr_pair_; + talk_base::AsyncPacketSocket* socket_; + bool locked_; + talk_base::SocketAddress default_dest_; +}; + +// Records a set of internal and external connections that we relay between, +// or in other words, that are "bound" together. +class RelayServerBinding : public talk_base::MessageHandler { + public: + RelayServerBinding( + RelayServer* server, const std::string& username, + const std::string& password, uint32 lifetime); + virtual ~RelayServerBinding(); + + RelayServer* server() { return server_; } + uint32 lifetime() { return lifetime_; } + const std::string& username() { return username_; } + const std::string& password() { return password_; } + const std::string& magic_cookie() { return magic_cookie_; } + + // Adds/Removes a connection into the binding. + void AddInternalConnection(RelayServerConnection* conn); + void AddExternalConnection(RelayServerConnection* conn); + + // We keep track of the use of each binding. If we detect that it was not + // used for longer than the lifetime, then we send a signal. + void NoteUsed(); + sigslot::signal1 SignalTimeout; + + // Determines whether the given packet has the magic cookie present (in the + // right place). + bool HasMagicCookie(const char* bytes, size_t size) const; + + // Determines the connection to use to send packets to or from the given + // external address. + RelayServerConnection* GetInternalConnection( + const talk_base::SocketAddress& ext_addr); + RelayServerConnection* GetExternalConnection( + const talk_base::SocketAddress& ext_addr); + + // MessageHandler: + void OnMessage(talk_base::Message *pmsg); + + private: + RelayServer* server_; + + std::string username_; + std::string password_; + std::string magic_cookie_; + + std::vector internal_connections_; + std::vector external_connections_; + + uint32 lifetime_; + uint32 last_used_; + // TODO: bandwidth +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_RELAYSERVER_H_ diff --git a/talk/p2p/base/relayserver_main.cc b/talk/p2p/base/relayserver_main.cc new file mode 100644 index 000000000..11e8a5bf1 --- /dev/null +++ b/talk/p2p/base/relayserver_main.cc @@ -0,0 +1,80 @@ +/* + * libjingle + * Copyright 2004--2005, 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 // NOLINT + +#include "talk/base/thread.h" +#include "talk/base/scoped_ptr.h" +#include "talk/p2p/base/relayserver.h" + +int main(int argc, char **argv) { + if (argc != 3) { + std::cerr << "usage: relayserver internal-address external-address" + << std::endl; + return 1; + } + + talk_base::SocketAddress int_addr; + if (!int_addr.FromString(argv[1])) { + std::cerr << "Unable to parse IP address: " << argv[1]; + return 1; + } + + talk_base::SocketAddress ext_addr; + if (!ext_addr.FromString(argv[2])) { + std::cerr << "Unable to parse IP address: " << argv[2]; + return 1; + } + + talk_base::Thread *pthMain = talk_base::Thread::Current(); + + talk_base::scoped_ptr int_socket( + talk_base::AsyncUDPSocket::Create(pthMain->socketserver(), int_addr)); + if (!int_socket) { + std::cerr << "Failed to create a UDP socket bound at" + << int_addr.ToString() << std::endl; + return 1; + } + + talk_base::scoped_ptr ext_socket( + talk_base::AsyncUDPSocket::Create(pthMain->socketserver(), ext_addr)); + if (!ext_socket) { + std::cerr << "Failed to create a UDP socket bound at" + << ext_addr.ToString() << std::endl; + return 1; + } + + cricket::RelayServer server(pthMain); + server.AddInternalSocket(int_socket.get()); + server.AddExternalSocket(ext_socket.get()); + + std::cout << "Listening internally at " << int_addr.ToString() << std::endl; + std::cout << "Listening externally at " << ext_addr.ToString() << std::endl; + + pthMain->Run(); + return 0; +} diff --git a/talk/p2p/base/relayserver_unittest.cc b/talk/p2p/base/relayserver_unittest.cc new file mode 100644 index 000000000..349fe08c2 --- /dev/null +++ b/talk/p2p/base/relayserver_unittest.cc @@ -0,0 +1,539 @@ +/* + * libjingle + * Copyright 2004 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 + +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/host.h" +#include "talk/base/logging.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/socketaddress.h" +#include "talk/base/testclient.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/relayserver.h" + +using talk_base::SocketAddress; +using namespace cricket; + +static const uint32 LIFETIME = 4; // seconds +static const SocketAddress server_int_addr("127.0.0.1", 5000); +static const SocketAddress server_ext_addr("127.0.0.1", 5001); +static const SocketAddress client1_addr("127.0.0.1", 6000 + (rand() % 1000)); +static const SocketAddress client2_addr("127.0.0.1", 7000 + (rand() % 1000)); +static const char* bad = "this is a completely nonsensical message whose only " + "purpose is to make the parser go 'ack'. it doesn't " + "look anything like a normal stun message"; +static const char* msg1 = "spamspamspamspamspamspamspambakedbeansspam"; +static const char* msg2 = "Lobster Thermidor a Crevette with a mornay sauce..."; + +class RelayServerTest : public testing::Test { + public: + static void SetUpTestCase() { + talk_base::InitRandom(NULL, 0); + } + RelayServerTest() + : main_(talk_base::Thread::Current()), ss_(main_->socketserver()), + username_(talk_base::CreateRandomString(12)), + password_(talk_base::CreateRandomString(12)) { + } + protected: + virtual void SetUp() { + server_.reset(new RelayServer(main_)); + + server_->AddInternalSocket( + talk_base::AsyncUDPSocket::Create(ss_, server_int_addr)); + server_->AddExternalSocket( + talk_base::AsyncUDPSocket::Create(ss_, server_ext_addr)); + + client1_.reset(new talk_base::TestClient( + talk_base::AsyncUDPSocket::Create(ss_, client1_addr))); + client2_.reset(new talk_base::TestClient( + talk_base::AsyncUDPSocket::Create(ss_, client2_addr))); + } + + void Allocate() { + talk_base::scoped_ptr req( + CreateStunMessage(STUN_ALLOCATE_REQUEST)); + AddUsernameAttr(req.get(), username_); + AddLifetimeAttr(req.get(), LIFETIME); + Send1(req.get()); + delete Receive1(); + } + void Bind() { + talk_base::scoped_ptr req( + CreateStunMessage(STUN_BINDING_REQUEST)); + AddUsernameAttr(req.get(), username_); + Send2(req.get()); + delete Receive1(); + } + + void Send1(const StunMessage* msg) { + talk_base::ByteBuffer buf; + msg->Write(&buf); + SendRaw1(buf.Data(), buf.Length()); + } + void Send2(const StunMessage* msg) { + talk_base::ByteBuffer buf; + msg->Write(&buf); + SendRaw2(buf.Data(), buf.Length()); + } + void SendRaw1(const char* data, int len) { + return Send(client1_.get(), data, len, server_int_addr); + } + void SendRaw2(const char* data, int len) { + return Send(client2_.get(), data, len, server_ext_addr); + } + void Send(talk_base::TestClient* client, const char* data, + int len, const SocketAddress& addr) { + client->SendTo(data, len, addr); + } + + StunMessage* Receive1() { + return Receive(client1_.get()); + } + StunMessage* Receive2() { + return Receive(client2_.get()); + } + std::string ReceiveRaw1() { + return ReceiveRaw(client1_.get()); + } + std::string ReceiveRaw2() { + return ReceiveRaw(client2_.get()); + } + StunMessage* Receive(talk_base::TestClient* client) { + StunMessage* msg = NULL; + talk_base::TestClient::Packet* packet = client->NextPacket(); + if (packet) { + talk_base::ByteBuffer buf(packet->buf, packet->size); + msg = new RelayMessage(); + msg->Read(&buf); + delete packet; + } + return msg; + } + std::string ReceiveRaw(talk_base::TestClient* client) { + std::string raw; + talk_base::TestClient::Packet* packet = client->NextPacket(); + if (packet) { + raw = std::string(packet->buf, packet->size); + delete packet; + } + return raw; + } + + static StunMessage* CreateStunMessage(int type) { + StunMessage* msg = new RelayMessage(); + msg->SetType(type); + msg->SetTransactionID( + talk_base::CreateRandomString(kStunTransactionIdLength)); + return msg; + } + static void AddMagicCookieAttr(StunMessage* msg) { + StunByteStringAttribute* attr = + StunAttribute::CreateByteString(STUN_ATTR_MAGIC_COOKIE); + attr->CopyBytes(TURN_MAGIC_COOKIE_VALUE, sizeof(TURN_MAGIC_COOKIE_VALUE)); + msg->AddAttribute(attr); + } + static void AddUsernameAttr(StunMessage* msg, const std::string& val) { + StunByteStringAttribute* attr = + StunAttribute::CreateByteString(STUN_ATTR_USERNAME); + attr->CopyBytes(val.c_str(), val.size()); + msg->AddAttribute(attr); + } + static void AddLifetimeAttr(StunMessage* msg, int val) { + StunUInt32Attribute* attr = + StunAttribute::CreateUInt32(STUN_ATTR_LIFETIME); + attr->SetValue(val); + msg->AddAttribute(attr); + } + static void AddDestinationAttr(StunMessage* msg, const SocketAddress& addr) { + StunAddressAttribute* attr = + StunAttribute::CreateAddress(STUN_ATTR_DESTINATION_ADDRESS); + attr->SetIP(addr.ipaddr()); + attr->SetPort(addr.port()); + msg->AddAttribute(attr); + } + + talk_base::Thread* main_; + talk_base::SocketServer* ss_; + talk_base::scoped_ptr server_; + talk_base::scoped_ptr client1_; + talk_base::scoped_ptr client2_; + std::string username_; + std::string password_; +}; + +// Send a complete nonsense message and verify that it is eaten. +TEST_F(RelayServerTest, TestBadRequest) { + talk_base::scoped_ptr res; + + SendRaw1(bad, std::strlen(bad)); + res.reset(Receive1()); + + ASSERT_TRUE(!res); +} + +// Send an allocate request without a username and verify it is rejected. +TEST_F(RelayServerTest, TestAllocateNoUsername) { + talk_base::scoped_ptr req( + CreateStunMessage(STUN_ALLOCATE_REQUEST)), res; + + Send1(req.get()); + res.reset(Receive1()); + + ASSERT_TRUE(res); + EXPECT_EQ(STUN_ALLOCATE_ERROR_RESPONSE, res->type()); + EXPECT_EQ(req->transaction_id(), res->transaction_id()); + + const StunErrorCodeAttribute* err = res->GetErrorCode(); + ASSERT_TRUE(err != NULL); + EXPECT_EQ(4, err->eclass()); + EXPECT_EQ(32, err->number()); + EXPECT_EQ("Missing Username", err->reason()); +} + +// Send a binding request and verify that it is rejected. +TEST_F(RelayServerTest, TestBindingRequest) { + talk_base::scoped_ptr req( + CreateStunMessage(STUN_BINDING_REQUEST)), res; + AddUsernameAttr(req.get(), username_); + + Send1(req.get()); + res.reset(Receive1()); + + ASSERT_TRUE(res); + EXPECT_EQ(STUN_BINDING_ERROR_RESPONSE, res->type()); + EXPECT_EQ(req->transaction_id(), res->transaction_id()); + + const StunErrorCodeAttribute* err = res->GetErrorCode(); + ASSERT_TRUE(err != NULL); + EXPECT_EQ(6, err->eclass()); + EXPECT_EQ(0, err->number()); + EXPECT_EQ("Operation Not Supported", err->reason()); +} + +// Send an allocate request and verify that it is accepted. +TEST_F(RelayServerTest, TestAllocate) { + talk_base::scoped_ptr req( + CreateStunMessage(STUN_ALLOCATE_REQUEST)), res; + AddUsernameAttr(req.get(), username_); + AddLifetimeAttr(req.get(), LIFETIME); + + Send1(req.get()); + res.reset(Receive1()); + + ASSERT_TRUE(res); + EXPECT_EQ(STUN_ALLOCATE_RESPONSE, res->type()); + EXPECT_EQ(req->transaction_id(), res->transaction_id()); + + const StunAddressAttribute* mapped_addr = + res->GetAddress(STUN_ATTR_MAPPED_ADDRESS); + ASSERT_TRUE(mapped_addr != NULL); + EXPECT_EQ(1, mapped_addr->family()); + EXPECT_EQ(server_ext_addr.port(), mapped_addr->port()); + EXPECT_EQ(server_ext_addr.ipaddr(), mapped_addr->ipaddr()); + + const StunUInt32Attribute* res_lifetime_attr = + res->GetUInt32(STUN_ATTR_LIFETIME); + ASSERT_TRUE(res_lifetime_attr != NULL); + EXPECT_EQ(LIFETIME, res_lifetime_attr->value()); +} + +// Send a second allocate request and verify that it is also accepted, though +// the lifetime should be ignored. +TEST_F(RelayServerTest, TestReallocate) { + Allocate(); + + talk_base::scoped_ptr req( + CreateStunMessage(STUN_ALLOCATE_REQUEST)), res; + AddMagicCookieAttr(req.get()); + AddUsernameAttr(req.get(), username_); + + Send1(req.get()); + res.reset(Receive1()); + + ASSERT_TRUE(res); + EXPECT_EQ(STUN_ALLOCATE_RESPONSE, res->type()); + EXPECT_EQ(req->transaction_id(), res->transaction_id()); + + const StunAddressAttribute* mapped_addr = + res->GetAddress(STUN_ATTR_MAPPED_ADDRESS); + ASSERT_TRUE(mapped_addr != NULL); + EXPECT_EQ(1, mapped_addr->family()); + EXPECT_EQ(server_ext_addr.port(), mapped_addr->port()); + EXPECT_EQ(server_ext_addr.ipaddr(), mapped_addr->ipaddr()); + + const StunUInt32Attribute* lifetime_attr = + res->GetUInt32(STUN_ATTR_LIFETIME); + ASSERT_TRUE(lifetime_attr != NULL); + EXPECT_EQ(LIFETIME, lifetime_attr->value()); +} + +// Send a request from another client and see that it arrives at the first +// client in the binding. +TEST_F(RelayServerTest, TestRemoteBind) { + Allocate(); + + talk_base::scoped_ptr req( + CreateStunMessage(STUN_BINDING_REQUEST)), res; + AddUsernameAttr(req.get(), username_); + + Send2(req.get()); + res.reset(Receive1()); + + ASSERT_TRUE(res); + EXPECT_EQ(STUN_DATA_INDICATION, res->type()); + + const StunByteStringAttribute* recv_data = + res->GetByteString(STUN_ATTR_DATA); + ASSERT_TRUE(recv_data != NULL); + + talk_base::ByteBuffer buf(recv_data->bytes(), recv_data->length()); + talk_base::scoped_ptr res2(new StunMessage()); + EXPECT_TRUE(res2->Read(&buf)); + EXPECT_EQ(STUN_BINDING_REQUEST, res2->type()); + EXPECT_EQ(req->transaction_id(), res2->transaction_id()); + + const StunAddressAttribute* src_addr = + res->GetAddress(STUN_ATTR_SOURCE_ADDRESS2); + ASSERT_TRUE(src_addr != NULL); + EXPECT_EQ(1, src_addr->family()); + EXPECT_EQ(client2_addr.ipaddr(), src_addr->ipaddr()); + EXPECT_EQ(client2_addr.port(), src_addr->port()); + + EXPECT_TRUE(Receive2() == NULL); +} + +// Send a complete nonsense message to the established connection and verify +// that it is dropped by the server. +TEST_F(RelayServerTest, TestRemoteBadRequest) { + Allocate(); + Bind(); + + SendRaw1(bad, std::strlen(bad)); + EXPECT_TRUE(Receive1() == NULL); + EXPECT_TRUE(Receive2() == NULL); +} + +// Send a send request without a username and verify it is rejected. +TEST_F(RelayServerTest, TestSendRequestMissingUsername) { + Allocate(); + Bind(); + + talk_base::scoped_ptr req( + CreateStunMessage(STUN_SEND_REQUEST)), res; + AddMagicCookieAttr(req.get()); + + Send1(req.get()); + res.reset(Receive1()); + + ASSERT_TRUE(res); + EXPECT_EQ(STUN_SEND_ERROR_RESPONSE, res->type()); + EXPECT_EQ(req->transaction_id(), res->transaction_id()); + + const StunErrorCodeAttribute* err = res->GetErrorCode(); + ASSERT_TRUE(err != NULL); + EXPECT_EQ(4, err->eclass()); + EXPECT_EQ(32, err->number()); + EXPECT_EQ("Missing Username", err->reason()); +} + +// Send a send request with the wrong username and verify it is rejected. +TEST_F(RelayServerTest, TestSendRequestBadUsername) { + Allocate(); + Bind(); + + talk_base::scoped_ptr req( + CreateStunMessage(STUN_SEND_REQUEST)), res; + AddMagicCookieAttr(req.get()); + AddUsernameAttr(req.get(), "foobarbizbaz"); + + Send1(req.get()); + res.reset(Receive1()); + + ASSERT_TRUE(res); + EXPECT_EQ(STUN_SEND_ERROR_RESPONSE, res->type()); + EXPECT_EQ(req->transaction_id(), res->transaction_id()); + + const StunErrorCodeAttribute* err = res->GetErrorCode(); + ASSERT_TRUE(err != NULL); + EXPECT_EQ(4, err->eclass()); + EXPECT_EQ(30, err->number()); + EXPECT_EQ("Stale Credentials", err->reason()); +} + +// Send a send request without a destination address and verify that it is +// rejected. +TEST_F(RelayServerTest, TestSendRequestNoDestinationAddress) { + Allocate(); + Bind(); + + talk_base::scoped_ptr req( + CreateStunMessage(STUN_SEND_REQUEST)), res; + AddMagicCookieAttr(req.get()); + AddUsernameAttr(req.get(), username_); + + Send1(req.get()); + res.reset(Receive1()); + + ASSERT_TRUE(res); + EXPECT_EQ(STUN_SEND_ERROR_RESPONSE, res->type()); + EXPECT_EQ(req->transaction_id(), res->transaction_id()); + + const StunErrorCodeAttribute* err = res->GetErrorCode(); + ASSERT_TRUE(err != NULL); + EXPECT_EQ(4, err->eclass()); + EXPECT_EQ(0, err->number()); + EXPECT_EQ("Bad Request", err->reason()); +} + +// Send a send request without data and verify that it is rejected. +TEST_F(RelayServerTest, TestSendRequestNoData) { + Allocate(); + Bind(); + + talk_base::scoped_ptr req( + CreateStunMessage(STUN_SEND_REQUEST)), res; + AddMagicCookieAttr(req.get()); + AddUsernameAttr(req.get(), username_); + AddDestinationAttr(req.get(), client2_addr); + + Send1(req.get()); + res.reset(Receive1()); + + ASSERT_TRUE(res); + EXPECT_EQ(STUN_SEND_ERROR_RESPONSE, res->type()); + EXPECT_EQ(req->transaction_id(), res->transaction_id()); + + const StunErrorCodeAttribute* err = res->GetErrorCode(); + ASSERT_TRUE(err != NULL); + EXPECT_EQ(4, err->eclass()); + EXPECT_EQ(00, err->number()); + EXPECT_EQ("Bad Request", err->reason()); +} + +// Send a binding request after an allocate and verify that it is rejected. +TEST_F(RelayServerTest, TestSendRequestWrongType) { + Allocate(); + Bind(); + + talk_base::scoped_ptr req( + CreateStunMessage(STUN_BINDING_REQUEST)), res; + AddMagicCookieAttr(req.get()); + AddUsernameAttr(req.get(), username_); + + Send1(req.get()); + res.reset(Receive1()); + + ASSERT_TRUE(res); + EXPECT_EQ(STUN_BINDING_ERROR_RESPONSE, res->type()); + EXPECT_EQ(req->transaction_id(), res->transaction_id()); + + const StunErrorCodeAttribute* err = res->GetErrorCode(); + ASSERT_TRUE(err != NULL); + EXPECT_EQ(6, err->eclass()); + EXPECT_EQ(0, err->number()); + EXPECT_EQ("Operation Not Supported", err->reason()); +} + +// Verify that we can send traffic back and forth between the clients after a +// successful allocate and bind. +TEST_F(RelayServerTest, TestSendRaw) { + Allocate(); + Bind(); + + for (int i = 0; i < 10; i++) { + talk_base::scoped_ptr req( + CreateStunMessage(STUN_SEND_REQUEST)), res; + AddMagicCookieAttr(req.get()); + AddUsernameAttr(req.get(), username_); + AddDestinationAttr(req.get(), client2_addr); + + StunByteStringAttribute* send_data = + StunAttribute::CreateByteString(STUN_ATTR_DATA); + send_data->CopyBytes(msg1); + req->AddAttribute(send_data); + + Send1(req.get()); + EXPECT_EQ(msg1, ReceiveRaw2()); + SendRaw2(msg2, std::strlen(msg2)); + res.reset(Receive1()); + + ASSERT_TRUE(res); + EXPECT_EQ(STUN_DATA_INDICATION, res->type()); + + const StunAddressAttribute* src_addr = + res->GetAddress(STUN_ATTR_SOURCE_ADDRESS2); + ASSERT_TRUE(src_addr != NULL); + EXPECT_EQ(1, src_addr->family()); + EXPECT_EQ(client2_addr.ipaddr(), src_addr->ipaddr()); + EXPECT_EQ(client2_addr.port(), src_addr->port()); + + const StunByteStringAttribute* recv_data = + res->GetByteString(STUN_ATTR_DATA); + ASSERT_TRUE(recv_data != NULL); + EXPECT_EQ(strlen(msg2), recv_data->length()); + EXPECT_EQ(0, memcmp(msg2, recv_data->bytes(), recv_data->length())); + } +} + +// Verify that a binding expires properly, and rejects send requests. +TEST_F(RelayServerTest, TestExpiration) { + Allocate(); + Bind(); + + // Wait twice the lifetime to make sure the server has expired the binding. + talk_base::Thread::Current()->ProcessMessages((LIFETIME * 2) * 1000); + + talk_base::scoped_ptr req( + CreateStunMessage(STUN_SEND_REQUEST)), res; + AddMagicCookieAttr(req.get()); + AddUsernameAttr(req.get(), username_); + AddDestinationAttr(req.get(), client2_addr); + + StunByteStringAttribute* data_attr = + StunAttribute::CreateByteString(STUN_ATTR_DATA); + data_attr->CopyBytes(msg1); + req->AddAttribute(data_attr); + + Send1(req.get()); + res.reset(Receive1()); + + ASSERT_TRUE(res.get() != NULL); + EXPECT_EQ(STUN_SEND_ERROR_RESPONSE, res->type()); + + const StunErrorCodeAttribute* err = res->GetErrorCode(); + ASSERT_TRUE(err != NULL); + EXPECT_EQ(6, err->eclass()); + EXPECT_EQ(0, err->number()); + EXPECT_EQ("Operation Not Supported", err->reason()); + + // Also verify that traffic from the external client is ignored. + SendRaw2(msg2, std::strlen(msg2)); + EXPECT_TRUE(ReceiveRaw1().empty()); +} diff --git a/talk/p2p/base/session.cc b/talk/p2p/base/session.cc new file mode 100644 index 000000000..3478a33aa --- /dev/null +++ b/talk/p2p/base/session.cc @@ -0,0 +1,1659 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/session.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/helpers.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sslstreamadapter.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/jid.h" +#include "talk/p2p/base/dtlstransport.h" +#include "talk/p2p/base/p2ptransport.h" +#include "talk/p2p/base/sessionclient.h" +#include "talk/p2p/base/transport.h" +#include "talk/p2p/base/transportchannelproxy.h" +#include "talk/p2p/base/transportinfo.h" + +#include "talk/p2p/base/constants.h" + +namespace cricket { + +bool BadMessage(const buzz::QName type, + const std::string& text, + MessageError* err) { + err->SetType(type); + err->SetText(text); + return false; +} + +TransportProxy::~TransportProxy() { + for (ChannelMap::iterator iter = channels_.begin(); + iter != channels_.end(); ++iter) { + iter->second->SignalDestroyed(iter->second); + delete iter->second; + } +} + +std::string TransportProxy::type() const { + return transport_->get()->type(); +} + +TransportChannel* TransportProxy::GetChannel(int component) { + return GetChannelProxy(component); +} + +TransportChannel* TransportProxy::CreateChannel( + const std::string& name, int component) { + ASSERT(GetChannel(component) == NULL); + ASSERT(!transport_->get()->HasChannel(component)); + + // We always create a proxy in case we need to change out the transport later. + TransportChannelProxy* channel = + new TransportChannelProxy(content_name(), name, component); + channels_[component] = channel; + + // If we're already negotiated, create an impl and hook it up to the proxy + // channel. If we're connecting, create an impl but don't hook it up yet. + if (negotiated_) { + SetChannelProxyImpl(component, channel); + } else if (connecting_) { + GetOrCreateChannelProxyImpl(component); + } + return channel; +} + +bool TransportProxy::HasChannel(int component) { + return transport_->get()->HasChannel(component); +} + +void TransportProxy::DestroyChannel(int component) { + TransportChannel* channel = GetChannel(component); + if (channel) { + // If the state of TransportProxy is not NEGOTIATED + // then TransportChannelProxy and its impl are not + // connected. Both must be connected before + // deletion. + if (!negotiated_) { + SetChannelProxyImpl(component, GetChannelProxy(component)); + } + + channels_.erase(component); + channel->SignalDestroyed(channel); + delete channel; + } +} + +void TransportProxy::ConnectChannels() { + if (!connecting_) { + if (!negotiated_) { + for (ChannelMap::iterator iter = channels_.begin(); + iter != channels_.end(); ++iter) { + GetOrCreateChannelProxyImpl(iter->first); + } + } + connecting_ = true; + } + // TODO(juberti): Right now Transport::ConnectChannels doesn't work if we + // don't have any channels yet, so we need to allow this method to be called + // multiple times. Once we fix Transport, we can move this call inside the + // if (!connecting_) block. + transport_->get()->ConnectChannels(); +} + +void TransportProxy::CompleteNegotiation() { + if (!negotiated_) { + for (ChannelMap::iterator iter = channels_.begin(); + iter != channels_.end(); ++iter) { + SetChannelProxyImpl(iter->first, iter->second); + } + negotiated_ = true; + } +} + +void TransportProxy::AddSentCandidates(const Candidates& candidates) { + for (Candidates::const_iterator cand = candidates.begin(); + cand != candidates.end(); ++cand) { + sent_candidates_.push_back(*cand); + } +} + +void TransportProxy::AddUnsentCandidates(const Candidates& candidates) { + for (Candidates::const_iterator cand = candidates.begin(); + cand != candidates.end(); ++cand) { + unsent_candidates_.push_back(*cand); + } +} + +bool TransportProxy::GetChannelNameFromComponent( + int component, std::string* channel_name) const { + const TransportChannelProxy* channel = GetChannelProxy(component); + if (channel == NULL) { + return false; + } + + *channel_name = channel->name(); + return true; +} + +bool TransportProxy::GetComponentFromChannelName( + const std::string& channel_name, int* component) const { + const TransportChannelProxy* channel = GetChannelProxyByName(channel_name); + if (channel == NULL) { + return false; + } + + *component = channel->component(); + return true; +} + +TransportChannelProxy* TransportProxy::GetChannelProxy(int component) const { + ChannelMap::const_iterator iter = channels_.find(component); + return (iter != channels_.end()) ? iter->second : NULL; +} + +TransportChannelProxy* TransportProxy::GetChannelProxyByName( + const std::string& name) const { + for (ChannelMap::const_iterator iter = channels_.begin(); + iter != channels_.end(); + ++iter) { + if (iter->second->name() == name) { + return iter->second; + } + } + return NULL; +} + +TransportChannelImpl* TransportProxy::GetOrCreateChannelProxyImpl( + int component) { + TransportChannelImpl* impl = transport_->get()->GetChannel(component); + if (impl == NULL) { + impl = transport_->get()->CreateChannel(component); + impl->SetSessionId(sid_); + } + return impl; +} + +void TransportProxy::SetChannelProxyImpl( + int component, TransportChannelProxy* transproxy) { + TransportChannelImpl* impl = GetOrCreateChannelProxyImpl(component); + ASSERT(impl != NULL); + transproxy->SetImplementation(impl); +} + +// This function muxes |this| onto |target| by repointing |this| at +// |target|'s transport and setting our TransportChannelProxies +// to point to |target|'s underlying implementations. +bool TransportProxy::SetupMux(TransportProxy* target) { + // Bail out if there's nothing to do. + if (transport_ == target->transport_) { + return true; + } + + // Run through all channels and remove any non-rtp transport channels before + // setting target transport channels. + for (ChannelMap::const_iterator iter = channels_.begin(); + iter != channels_.end(); ++iter) { + if (!target->transport_->get()->HasChannel(iter->first)) { + // Remove if channel doesn't exist in |transport_|. + iter->second->SetImplementation(NULL); + } else { + // Replace the impl for all the TransportProxyChannels with the channels + // from |target|'s transport. Fail if there's not an exact match. + iter->second->SetImplementation( + target->transport_->get()->CreateChannel(iter->first)); + } + } + + // Now replace our transport. Must happen afterwards because + // it deletes all impls as a side effect. + transport_ = target->transport_; + transport_->get()->SignalCandidatesReady.connect( + this, &TransportProxy::OnTransportCandidatesReady); + set_candidates_allocated(target->candidates_allocated()); + return true; +} + +void TransportProxy::SetRole(TransportRole role) { + transport_->get()->SetRole(role); +} + +bool TransportProxy::SetLocalTransportDescription( + const TransportDescription& description, ContentAction action) { + // If this is an answer, finalize the negotiation. + if (action == CA_ANSWER) { + CompleteNegotiation(); + } + return transport_->get()->SetLocalTransportDescription(description, action); +} + +bool TransportProxy::SetRemoteTransportDescription( + const TransportDescription& description, ContentAction action) { + // If this is an answer, finalize the negotiation. + if (action == CA_ANSWER) { + CompleteNegotiation(); + } + return transport_->get()->SetRemoteTransportDescription(description, action); +} + +void TransportProxy::OnSignalingReady() { + // If we're starting a new allocation sequence, reset our state. + set_candidates_allocated(false); + transport_->get()->OnSignalingReady(); +} + +bool TransportProxy::OnRemoteCandidates(const Candidates& candidates, + std::string* error) { + // Ensure the transport is negotiated before handling candidates. + // TODO(juberti): Remove this once everybody calls SetLocalTD. + CompleteNegotiation(); + + // Verify each candidate before passing down to transport layer. + for (Candidates::const_iterator cand = candidates.begin(); + cand != candidates.end(); ++cand) { + if (!transport_->get()->VerifyCandidate(*cand, error)) + return false; + if (!HasChannel(cand->component())) { + *error = "Candidate has unknown component: " + cand->ToString() + + " for content: " + content_name_; + return false; + } + } + transport_->get()->OnRemoteCandidates(candidates); + return true; +} + +std::string BaseSession::StateToString(State state) { + switch (state) { + case Session::STATE_INIT: + return "STATE_INIT"; + case Session::STATE_SENTINITIATE: + return "STATE_SENTINITIATE"; + case Session::STATE_RECEIVEDINITIATE: + return "STATE_RECEIVEDINITIATE"; + case Session::STATE_SENTPRACCEPT: + return "STATE_SENTPRACCEPT"; + case Session::STATE_SENTACCEPT: + return "STATE_SENTACCEPT"; + case Session::STATE_RECEIVEDPRACCEPT: + return "STATE_RECEIVEDPRACCEPT"; + case Session::STATE_RECEIVEDACCEPT: + return "STATE_RECEIVEDACCEPT"; + case Session::STATE_SENTMODIFY: + return "STATE_SENTMODIFY"; + case Session::STATE_RECEIVEDMODIFY: + return "STATE_RECEIVEDMODIFY"; + case Session::STATE_SENTREJECT: + return "STATE_SENTREJECT"; + case Session::STATE_RECEIVEDREJECT: + return "STATE_RECEIVEDREJECT"; + case Session::STATE_SENTREDIRECT: + return "STATE_SENTREDIRECT"; + case Session::STATE_SENTTERMINATE: + return "STATE_SENTTERMINATE"; + case Session::STATE_RECEIVEDTERMINATE: + return "STATE_RECEIVEDTERMINATE"; + case Session::STATE_INPROGRESS: + return "STATE_INPROGRESS"; + case Session::STATE_DEINIT: + return "STATE_DEINIT"; + default: + break; + } + return "STATE_" + talk_base::ToString(state); +} + +BaseSession::BaseSession(talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread, + PortAllocator* port_allocator, + const std::string& sid, + const std::string& content_type, + bool initiator) + : state_(STATE_INIT), + error_(ERROR_NONE), + signaling_thread_(signaling_thread), + worker_thread_(worker_thread), + port_allocator_(port_allocator), + sid_(sid), + content_type_(content_type), + transport_type_(NS_GINGLE_P2P), + initiator_(initiator), + identity_(NULL), + local_description_(NULL), + remote_description_(NULL), + ice_tiebreaker_(talk_base::CreateRandomId64()), + role_switch_(false) { + ASSERT(signaling_thread->IsCurrent()); +} + +BaseSession::~BaseSession() { + ASSERT(signaling_thread()->IsCurrent()); + + ASSERT(state_ != STATE_DEINIT); + LogState(state_, STATE_DEINIT); + state_ = STATE_DEINIT; + SignalState(this, state_); + + for (TransportMap::iterator iter = transports_.begin(); + iter != transports_.end(); ++iter) { + delete iter->second; + } + + delete remote_description_; + delete local_description_; +} + +bool BaseSession::PushdownTransportDescription(ContentSource source, + ContentAction action) { + if (source == CS_LOCAL) { + return PushdownLocalTransportDescription(local_description_, action); + } + return PushdownRemoteTransportDescription(remote_description_, action); +} + +bool BaseSession::PushdownLocalTransportDescription( + const SessionDescription* sdesc, + ContentAction action) { + // Update the Transports with the right information, and trigger them to + // start connecting. + for (TransportMap::iterator iter = transports_.begin(); + iter != transports_.end(); ++iter) { + // If no transport info was in this session description, ret == false + // and we just skip this one. + TransportDescription tdesc; + bool ret = GetTransportDescription( + sdesc, iter->second->content_name(), &tdesc); + if (ret) { + if (!iter->second->SetLocalTransportDescription(tdesc, action)) { + return false; + } + + iter->second->ConnectChannels(); + } + } + + return true; +} + +bool BaseSession::PushdownRemoteTransportDescription( + const SessionDescription* sdesc, + ContentAction action) { + // Update the Transports with the right information. + for (TransportMap::iterator iter = transports_.begin(); + iter != transports_.end(); ++iter) { + TransportDescription tdesc; + + // If no transport info was in this session description, ret == false + // and we just skip this one. + bool ret = GetTransportDescription( + sdesc, iter->second->content_name(), &tdesc); + if (ret) { + if (!iter->second->SetRemoteTransportDescription(tdesc, action)) { + return false; + } + } + } + + return true; +} + +TransportChannel* BaseSession::CreateChannel(const std::string& content_name, + const std::string& channel_name, + int component) { + // We create the proxy "on demand" here because we need to support + // creating channels at any time, even before we send or receive + // initiate messages, which is before we create the transports. + TransportProxy* transproxy = GetOrCreateTransportProxy(content_name); + return transproxy->CreateChannel(channel_name, component); +} + +TransportChannel* BaseSession::GetChannel(const std::string& content_name, + int component) { + TransportProxy* transproxy = GetTransportProxy(content_name); + if (transproxy == NULL) + return NULL; + else + return transproxy->GetChannel(component); +} + +void BaseSession::DestroyChannel(const std::string& content_name, + int component) { + TransportProxy* transproxy = GetTransportProxy(content_name); + ASSERT(transproxy != NULL); + transproxy->DestroyChannel(component); +} + +TransportProxy* BaseSession::GetOrCreateTransportProxy( + const std::string& content_name) { + TransportProxy* transproxy = GetTransportProxy(content_name); + if (transproxy) + return transproxy; + + Transport* transport = CreateTransport(content_name); + transport->SetRole(initiator_ ? ROLE_CONTROLLING : ROLE_CONTROLLED); + transport->SetTiebreaker(ice_tiebreaker_); + // TODO: Connect all the Transport signals to TransportProxy + // then to the BaseSession. + transport->SignalConnecting.connect( + this, &BaseSession::OnTransportConnecting); + transport->SignalWritableState.connect( + this, &BaseSession::OnTransportWritable); + transport->SignalRequestSignaling.connect( + this, &BaseSession::OnTransportRequestSignaling); + transport->SignalTransportError.connect( + this, &BaseSession::OnTransportSendError); + transport->SignalRouteChange.connect( + this, &BaseSession::OnTransportRouteChange); + transport->SignalCandidatesAllocationDone.connect( + this, &BaseSession::OnTransportCandidatesAllocationDone); + transport->SignalRoleConflict.connect( + this, &BaseSession::OnRoleConflict); + + transproxy = new TransportProxy(sid_, content_name, + new TransportWrapper(transport)); + transproxy->SignalCandidatesReady.connect( + this, &BaseSession::OnTransportProxyCandidatesReady); + transports_[content_name] = transproxy; + + return transproxy; +} + +Transport* BaseSession::GetTransport(const std::string& content_name) { + TransportProxy* transproxy = GetTransportProxy(content_name); + if (transproxy == NULL) + return NULL; + return transproxy->impl(); +} + +TransportProxy* BaseSession::GetTransportProxy( + const std::string& content_name) { + TransportMap::iterator iter = transports_.find(content_name); + return (iter != transports_.end()) ? iter->second : NULL; +} + +TransportProxy* BaseSession::GetTransportProxy(const Transport* transport) { + for (TransportMap::iterator iter = transports_.begin(); + iter != transports_.end(); ++iter) { + TransportProxy* transproxy = iter->second; + if (transproxy->impl() == transport) { + return transproxy; + } + } + return NULL; +} + +TransportProxy* BaseSession::GetFirstTransportProxy() { + if (transports_.empty()) + return NULL; + return transports_.begin()->second; +} + +void BaseSession::DestroyTransportProxy( + const std::string& content_name) { + TransportMap::iterator iter = transports_.find(content_name); + if (iter != transports_.end()) { + delete iter->second; + transports_.erase(content_name); + } +} + +cricket::Transport* BaseSession::CreateTransport( + const std::string& content_name) { + ASSERT(transport_type_ == NS_GINGLE_P2P); + return new cricket::DtlsTransport( + signaling_thread(), worker_thread(), content_name, + port_allocator(), identity_); +} + +bool BaseSession::GetStats(SessionStats* stats) { + for (TransportMap::iterator iter = transports_.begin(); + iter != transports_.end(); ++iter) { + std::string proxy_id = iter->second->content_name(); + // We are ignoring not-yet-instantiated transports. + if (iter->second->impl()) { + std::string transport_id = iter->second->impl()->content_name(); + stats->proxy_to_transport[proxy_id] = transport_id; + if (stats->transport_stats.find(transport_id) + == stats->transport_stats.end()) { + TransportStats subinfos; + if (!iter->second->impl()->GetStats(&subinfos)) { + return false; + } + stats->transport_stats[transport_id] = subinfos; + } + } + } + return true; +} + +void BaseSession::SetState(State state) { + ASSERT(signaling_thread_->IsCurrent()); + if (state != state_) { + LogState(state_, state); + state_ = state; + SignalState(this, state_); + signaling_thread_->Post(this, MSG_STATE); + } + SignalNewDescription(); +} + +void BaseSession::SetError(Error error) { + ASSERT(signaling_thread_->IsCurrent()); + if (error != error_) { + error_ = error; + SignalError(this, error); + } +} + +void BaseSession::OnSignalingReady() { + ASSERT(signaling_thread()->IsCurrent()); + for (TransportMap::iterator iter = transports_.begin(); + iter != transports_.end(); ++iter) { + iter->second->OnSignalingReady(); + } +} + +// TODO(juberti): Since PushdownLocalTD now triggers the connection process to +// start, remove this method once everyone calls PushdownLocalTD. +void BaseSession::SpeculativelyConnectAllTransportChannels() { + // Put all transports into the connecting state. + for (TransportMap::iterator iter = transports_.begin(); + iter != transports_.end(); ++iter) { + iter->second->ConnectChannels(); + } +} + +bool BaseSession::OnRemoteCandidates(const std::string& content_name, + const Candidates& candidates, + std::string* error) { + // Give candidates to the appropriate transport, and tell that transport + // to start connecting, if it's not already doing so. + TransportProxy* transproxy = GetTransportProxy(content_name); + if (!transproxy) { + *error = "Unknown content name " + content_name; + return false; + } + if (!transproxy->OnRemoteCandidates(candidates, error)) { + return false; + } + // TODO(juberti): Remove this call once we can be sure that we always have + // a local transport description (which will trigger the connection). + transproxy->ConnectChannels(); + return true; +} + +bool BaseSession::MaybeEnableMuxingSupport() { + // We need both a local and remote description to decide if we should mux. + if ((state_ == STATE_SENTINITIATE || + state_ == STATE_RECEIVEDINITIATE) && + ((local_description_ == NULL) || + (remote_description_ == NULL))) { + return false; + } + + // In order to perform the multiplexing, we need all proxies to be in the + // negotiated state, i.e. to have implementations underneath. + // Ensure that this is the case, regardless of whether we are going to mux. + for (TransportMap::iterator iter = transports_.begin(); + iter != transports_.end(); ++iter) { + ASSERT(iter->second->negotiated()); + if (!iter->second->negotiated()) + return false; + } + + // If both sides agree to BUNDLE, mux all the specified contents onto the + // transport belonging to the first content name in the BUNDLE group. + // If the contents are already muxed, this will be a no-op. + // TODO(juberti): Should this check that local and remote have configured + // BUNDLE the same way? + bool candidates_allocated = IsCandidateAllocationDone(); + const ContentGroup* local_bundle_group = + local_description()->GetGroupByName(GROUP_TYPE_BUNDLE); + const ContentGroup* remote_bundle_group = + remote_description()->GetGroupByName(GROUP_TYPE_BUNDLE); + if (local_bundle_group && remote_bundle_group && + local_bundle_group->FirstContentName()) { + const std::string* content_name = local_bundle_group->FirstContentName(); + const ContentInfo* content = + local_description_->GetContentByName(*content_name); + ASSERT(content != NULL); + if (!SetSelectedProxy(content->name, local_bundle_group)) { + LOG(LS_WARNING) << "Failed to set up BUNDLE"; + return false; + } + + // If we weren't done gathering before, we might be done now, as a result + // of enabling mux. + LOG(LS_INFO) << "Enabling BUNDLE, bundling onto transport: " + << *content_name; + if (!candidates_allocated) { + MaybeCandidateAllocationDone(); + } + } else { + LOG(LS_INFO) << "No BUNDLE information, not bundling."; + } + return true; +} + +bool BaseSession::SetSelectedProxy(const std::string& content_name, + const ContentGroup* muxed_group) { + TransportProxy* selected_proxy = GetTransportProxy(content_name); + if (!selected_proxy) { + return false; + } + + ASSERT(selected_proxy->negotiated()); + for (TransportMap::iterator iter = transports_.begin(); + iter != transports_.end(); ++iter) { + // If content is part of the mux group, then repoint its proxy at the + // transport object that we have chosen to mux onto. If the proxy + // is already pointing at the right object, it will be a no-op. + if (muxed_group->HasContentName(iter->first) && + !iter->second->SetupMux(selected_proxy)) { + return false; + } + } + return true; +} + +void BaseSession::OnTransportCandidatesAllocationDone(Transport* transport) { + // TODO(juberti): This is a clunky way of processing the done signal. Instead, + // TransportProxy should receive the done signal directly, set its allocated + // flag internally, and then reissue the done signal to Session. + // Overall we should make TransportProxy receive *all* the signals from + // Transport, since this removes the need to manually iterate over all + // the transports, as is needed to make sure signals are handled properly + // when BUNDLEing. +#if 0 + ASSERT(!IsCandidateAllocationDone()); +#endif + for (TransportMap::iterator iter = transports_.begin(); + iter != transports_.end(); ++iter) { + if (iter->second->impl() == transport) { + iter->second->set_candidates_allocated(true); + } + } + MaybeCandidateAllocationDone(); +} + +bool BaseSession::IsCandidateAllocationDone() const { + for (TransportMap::const_iterator iter = transports_.begin(); + iter != transports_.end(); ++iter) { + if (!iter->second->candidates_allocated()) + return false; + } + return true; +} + +void BaseSession::MaybeCandidateAllocationDone() { + if (IsCandidateAllocationDone()) { + LOG(LS_INFO) << "Candidate gathering is complete."; + OnCandidatesAllocationDone(); + } +} + +void BaseSession::OnRoleConflict() { + if (role_switch_) { + LOG(LS_WARNING) << "Repeat of role conflict signal from Transport."; + return; + } + + role_switch_ = true; + for (TransportMap::iterator iter = transports_.begin(); + iter != transports_.end(); ++iter) { + // Role will be reverse of initial role setting. + TransportRole role = initiator_ ? ROLE_CONTROLLED : ROLE_CONTROLLING; + iter->second->SetRole(role); + } +} + +void BaseSession::LogState(State old_state, State new_state) { + LOG(LS_INFO) << "Session:" << id() + << " Old state:" << StateToString(old_state) + << " New state:" << StateToString(new_state) + << " Type:" << content_type() + << " Transport:" << transport_type(); +} + +bool BaseSession::GetTransportDescription(const SessionDescription* description, + const std::string& content_name, + TransportDescription* tdesc) { + if (!description || !tdesc) { + return false; + } + const TransportInfo* transport_info = + description->GetTransportInfoByName(content_name); + if (!transport_info) { + return false; + } + *tdesc = transport_info->description; + return true; +} + +void BaseSession::SignalNewDescription() { + ContentAction action; + ContentSource source; + if (!GetContentAction(&action, &source)) { + return; + } + if (source == CS_LOCAL) { + SignalNewLocalDescription(this, action); + } else { + SignalNewRemoteDescription(this, action); + } +} + +bool BaseSession::GetContentAction(ContentAction* action, + ContentSource* source) { + switch (state_) { + // new local description + case STATE_SENTINITIATE: + *action = CA_OFFER; + *source = CS_LOCAL; + break; + case STATE_SENTPRACCEPT: + *action = CA_PRANSWER; + *source = CS_LOCAL; + break; + case STATE_SENTACCEPT: + *action = CA_ANSWER; + *source = CS_LOCAL; + break; + // new remote description + case STATE_RECEIVEDINITIATE: + *action = CA_OFFER; + *source = CS_REMOTE; + break; + case STATE_RECEIVEDPRACCEPT: + *action = CA_PRANSWER; + *source = CS_REMOTE; + break; + case STATE_RECEIVEDACCEPT: + *action = CA_ANSWER; + *source = CS_REMOTE; + break; + default: + return false; + } + return true; +} + +void BaseSession::OnMessage(talk_base::Message *pmsg) { + switch (pmsg->message_id) { + case MSG_TIMEOUT: + // Session timeout has occured. + SetError(ERROR_TIME); + break; + + case MSG_STATE: + switch (state_) { + case STATE_SENTACCEPT: + case STATE_RECEIVEDACCEPT: + SetState(STATE_INPROGRESS); + break; + + default: + // Explicitly ignoring some states here. + break; + } + break; + } +} + +Session::Session(SessionManager* session_manager, + const std::string& local_name, + const std::string& initiator_name, + const std::string& sid, + const std::string& content_type, + SessionClient* client) + : BaseSession(session_manager->signaling_thread(), + session_manager->worker_thread(), + session_manager->port_allocator(), + sid, content_type, initiator_name == local_name) { + ASSERT(client != NULL); + session_manager_ = session_manager; + local_name_ = local_name; + initiator_name_ = initiator_name; + transport_parser_ = new P2PTransportParser(); + client_ = client; + initiate_acked_ = false; + current_protocol_ = PROTOCOL_HYBRID; +} + +Session::~Session() { + delete transport_parser_; +} + +bool Session::Initiate(const std::string &to, + const SessionDescription* sdesc) { + ASSERT(signaling_thread()->IsCurrent()); + SessionError error; + + // Only from STATE_INIT + if (state() != STATE_INIT) + return false; + + // Setup for signaling. + set_remote_name(to); + set_local_description(sdesc); + if (!CreateTransportProxies(GetEmptyTransportInfos(sdesc->contents()), + &error)) { + LOG(LS_ERROR) << "Could not create transports: " << error.text; + return false; + } + + if (!SendInitiateMessage(sdesc, &error)) { + LOG(LS_ERROR) << "Could not send initiate message: " << error.text; + return false; + } + + // We need to connect transport proxy and impl here so that we can process + // the TransportDescriptions. + SpeculativelyConnectAllTransportChannels(); + + PushdownTransportDescription(CS_LOCAL, CA_OFFER); + SetState(Session::STATE_SENTINITIATE); + return true; +} + +bool Session::Accept(const SessionDescription* sdesc) { + ASSERT(signaling_thread()->IsCurrent()); + + // Only if just received initiate + if (state() != STATE_RECEIVEDINITIATE) + return false; + + // Setup for signaling. + set_local_description(sdesc); + + SessionError error; + if (!SendAcceptMessage(sdesc, &error)) { + LOG(LS_ERROR) << "Could not send accept message: " << error.text; + return false; + } + // TODO(juberti): Add BUNDLE support to transport-info messages. + PushdownTransportDescription(CS_LOCAL, CA_ANSWER); + MaybeEnableMuxingSupport(); // Enable transport channel mux if supported. + SetState(Session::STATE_SENTACCEPT); + return true; +} + +bool Session::Reject(const std::string& reason) { + ASSERT(signaling_thread()->IsCurrent()); + + // Reject is sent in response to an initiate or modify, to reject the + // request + if (state() != STATE_RECEIVEDINITIATE && state() != STATE_RECEIVEDMODIFY) + return false; + + SessionError error; + if (!SendRejectMessage(reason, &error)) { + LOG(LS_ERROR) << "Could not send reject message: " << error.text; + return false; + } + + SetState(STATE_SENTREJECT); + return true; +} + +bool Session::TerminateWithReason(const std::string& reason) { + ASSERT(signaling_thread()->IsCurrent()); + + // Either side can terminate, at any time. + switch (state()) { + case STATE_SENTTERMINATE: + case STATE_RECEIVEDTERMINATE: + return false; + + case STATE_SENTREJECT: + case STATE_RECEIVEDREJECT: + // We don't need to send terminate if we sent or received a reject... + // it's implicit. + break; + + default: + SessionError error; + if (!SendTerminateMessage(reason, &error)) { + LOG(LS_ERROR) << "Could not send terminate message: " << error.text; + return false; + } + break; + } + + SetState(STATE_SENTTERMINATE); + return true; +} + +bool Session::SendInfoMessage(const XmlElements& elems) { + ASSERT(signaling_thread()->IsCurrent()); + SessionError error; + if (!SendMessage(ACTION_SESSION_INFO, elems, &error)) { + LOG(LS_ERROR) << "Could not send info message " << error.text; + return false; + } + return true; +} + +bool Session::SendDescriptionInfoMessage(const ContentInfos& contents) { + XmlElements elems; + WriteError write_error; + if (!WriteDescriptionInfo(current_protocol_, + contents, + GetContentParsers(), + &elems, &write_error)) { + LOG(LS_ERROR) << "Could not write description info message: " + << write_error.text; + return false; + } + SessionError error; + if (!SendMessage(ACTION_DESCRIPTION_INFO, elems, &error)) { + LOG(LS_ERROR) << "Could not send description info message: " + << error.text; + return false; + } + return true; +} + +TransportInfos Session::GetEmptyTransportInfos( + const ContentInfos& contents) const { + TransportInfos tinfos; + for (ContentInfos::const_iterator content = contents.begin(); + content != contents.end(); ++content) { + tinfos.push_back( + TransportInfo(content->name, + TransportDescription(transport_type(), Candidates()))); + } + return tinfos; +} + +bool Session::OnRemoteCandidates( + const TransportInfos& tinfos, ParseError* error) { + for (TransportInfos::const_iterator tinfo = tinfos.begin(); + tinfo != tinfos.end(); ++tinfo) { + std::string str_error; + if (!BaseSession::OnRemoteCandidates( + tinfo->content_name, tinfo->description.candidates, &str_error)) { + return BadParse(str_error, error); + } + } + return true; +} + +bool Session::CreateTransportProxies(const TransportInfos& tinfos, + SessionError* error) { + for (TransportInfos::const_iterator tinfo = tinfos.begin(); + tinfo != tinfos.end(); ++tinfo) { + if (tinfo->description.transport_type != transport_type()) { + error->SetText("No supported transport in offer."); + return false; + } + + GetOrCreateTransportProxy(tinfo->content_name); + } + return true; +} + +TransportParserMap Session::GetTransportParsers() { + TransportParserMap parsers; + parsers[transport_type()] = transport_parser_; + return parsers; +} + +CandidateTranslatorMap Session::GetCandidateTranslators() { + CandidateTranslatorMap translators; + // NOTE: This technique makes it impossible to parse G-ICE + // candidates in session-initiate messages because the channels + // aren't yet created at that point. Since we don't use candidates + // in session-initiate messages, we should be OK. Once we switch to + // ICE, this translation shouldn't be necessary. + for (TransportMap::const_iterator iter = transport_proxies().begin(); + iter != transport_proxies().end(); ++iter) { + translators[iter->first] = iter->second; + } + return translators; +} + +ContentParserMap Session::GetContentParsers() { + ContentParserMap parsers; + parsers[content_type()] = client_; + // We need to be able parse both RTP-based and SCTP-based Jingle + // with the same client. + if (content_type() == NS_JINGLE_RTP) { + parsers[NS_JINGLE_DRAFT_SCTP] = client_; + } + return parsers; +} + +void Session::OnTransportRequestSignaling(Transport* transport) { + ASSERT(signaling_thread()->IsCurrent()); + TransportProxy* transproxy = GetTransportProxy(transport); + ASSERT(transproxy != NULL); + if (transproxy) { + // Reset candidate allocation status for the transport proxy. + transproxy->set_candidates_allocated(false); + } + SignalRequestSignaling(this); +} + +void Session::OnTransportConnecting(Transport* transport) { + // This is an indication that we should begin watching the writability + // state of the transport. + OnTransportWritable(transport); +} + +void Session::OnTransportWritable(Transport* transport) { + ASSERT(signaling_thread()->IsCurrent()); + + // If the transport is not writable, start a timer to make sure that it + // becomes writable within a reasonable amount of time. If it does not, we + // terminate since we can't actually send data. If the transport is writable, + // cancel the timer. Note that writability transitions may occur repeatedly + // during the lifetime of the session. + signaling_thread()->Clear(this, MSG_TIMEOUT); + if (transport->HasChannels() && !transport->writable()) { + signaling_thread()->PostDelayed( + session_manager_->session_timeout() * 1000, this, MSG_TIMEOUT); + } +} + +void Session::OnTransportProxyCandidatesReady(TransportProxy* transproxy, + const Candidates& candidates) { + ASSERT(signaling_thread()->IsCurrent()); + if (transproxy != NULL) { + if (initiator() && !initiate_acked_) { + // TODO: This is to work around server re-ordering + // messages. We send the candidates once the session-initiate + // is acked. Once we have fixed the server to guarantee message + // order, we can remove this case. + transproxy->AddUnsentCandidates(candidates); + } else { + if (!transproxy->negotiated()) { + transproxy->AddSentCandidates(candidates); + } + SessionError error; + if (!SendTransportInfoMessage(transproxy, candidates, &error)) { + LOG(LS_ERROR) << "Could not send transport info message: " + << error.text; + return; + } + } + } +} + +void Session::OnTransportSendError(Transport* transport, + const buzz::XmlElement* stanza, + const buzz::QName& name, + const std::string& type, + const std::string& text, + const buzz::XmlElement* extra_info) { + ASSERT(signaling_thread()->IsCurrent()); + SignalErrorMessage(this, stanza, name, type, text, extra_info); +} + +void Session::OnIncomingMessage(const SessionMessage& msg) { + ASSERT(signaling_thread()->IsCurrent()); + ASSERT(state() == STATE_INIT || msg.from == remote_name()); + + if (current_protocol_== PROTOCOL_HYBRID) { + if (msg.protocol == PROTOCOL_GINGLE) { + current_protocol_ = PROTOCOL_GINGLE; + } else { + current_protocol_ = PROTOCOL_JINGLE; + } + } + + bool valid = false; + MessageError error; + switch (msg.type) { + case ACTION_SESSION_INITIATE: + valid = OnInitiateMessage(msg, &error); + break; + case ACTION_SESSION_INFO: + valid = OnInfoMessage(msg); + break; + case ACTION_SESSION_ACCEPT: + valid = OnAcceptMessage(msg, &error); + break; + case ACTION_SESSION_REJECT: + valid = OnRejectMessage(msg, &error); + break; + case ACTION_SESSION_TERMINATE: + valid = OnTerminateMessage(msg, &error); + break; + case ACTION_TRANSPORT_INFO: + valid = OnTransportInfoMessage(msg, &error); + break; + case ACTION_TRANSPORT_ACCEPT: + valid = OnTransportAcceptMessage(msg, &error); + break; + case ACTION_DESCRIPTION_INFO: + valid = OnDescriptionInfoMessage(msg, &error); + break; + default: + valid = BadMessage(buzz::QN_STANZA_BAD_REQUEST, + "unknown session message type", + &error); + } + + if (valid) { + SendAcknowledgementMessage(msg.stanza); + } else { + SignalErrorMessage(this, msg.stanza, error.type, + "modify", error.text, NULL); + } +} + +void Session::OnIncomingResponse(const buzz::XmlElement* orig_stanza, + const buzz::XmlElement* response_stanza, + const SessionMessage& msg) { + ASSERT(signaling_thread()->IsCurrent()); + + if (msg.type == ACTION_SESSION_INITIATE) { + OnInitiateAcked(); + } +} + +void Session::OnInitiateAcked() { + // TODO: This is to work around server re-ordering + // messages. We send the candidates once the session-initiate + // is acked. Once we have fixed the server to guarantee message + // order, we can remove this case. + if (!initiate_acked_) { + initiate_acked_ = true; + SessionError error; + SendAllUnsentTransportInfoMessages(&error); + } +} + +void Session::OnFailedSend(const buzz::XmlElement* orig_stanza, + const buzz::XmlElement* error_stanza) { + ASSERT(signaling_thread()->IsCurrent()); + + SessionMessage msg; + ParseError parse_error; + if (!ParseSessionMessage(orig_stanza, &msg, &parse_error)) { + LOG(LS_ERROR) << "Error parsing failed send: " << parse_error.text + << ":" << orig_stanza; + return; + } + + // If the error is a session redirect, call OnRedirectError, which will + // continue the session with a new remote JID. + SessionRedirect redirect; + if (FindSessionRedirect(error_stanza, &redirect)) { + SessionError error; + if (!OnRedirectError(redirect, &error)) { + // TODO: Should we send a message back? The standard + // says nothing about it. + LOG(LS_ERROR) << "Failed to redirect: " << error.text; + SetError(ERROR_RESPONSE); + } + return; + } + + std::string error_type = "cancel"; + + const buzz::XmlElement* error = error_stanza->FirstNamed(buzz::QN_ERROR); + if (error) { + error_type = error->Attr(buzz::QN_TYPE); + + LOG(LS_ERROR) << "Session error:\n" << error->Str() << "\n" + << "in response to:\n" << orig_stanza->Str(); + } else { + // don't crash if is missing + LOG(LS_ERROR) << "Session error without element, ignoring"; + return; + } + + if (msg.type == ACTION_TRANSPORT_INFO) { + // Transport messages frequently generate errors because they are sent right + // when we detect a network failure. For that reason, we ignore such + // errors, because if we do not establish writability again, we will + // terminate anyway. The exceptions are transport-specific error tags, + // which we pass on to the respective transport. + } else if ((error_type != "continue") && (error_type != "wait")) { + // We do not set an error if the other side said it is okay to continue + // (possibly after waiting). These errors can be ignored. + SetError(ERROR_RESPONSE); + } +} + +bool Session::OnInitiateMessage(const SessionMessage& msg, + MessageError* error) { + if (!CheckState(STATE_INIT, error)) + return false; + + SessionInitiate init; + if (!ParseSessionInitiate(msg.protocol, msg.action_elem, + GetContentParsers(), GetTransportParsers(), + GetCandidateTranslators(), + &init, error)) + return false; + + SessionError session_error; + if (!CreateTransportProxies(init.transports, &session_error)) { + return BadMessage(buzz::QN_STANZA_NOT_ACCEPTABLE, + session_error.text, error); + } + + set_remote_name(msg.from); + set_initiator_name(msg.initiator); + set_remote_description(new SessionDescription(init.ClearContents(), + init.transports, + init.groups)); + // Updating transport with TransportDescription. + PushdownTransportDescription(CS_REMOTE, CA_OFFER); + SetState(STATE_RECEIVEDINITIATE); + + // Users of Session may listen to state change and call Reject(). + if (state() != STATE_SENTREJECT) { + if (!OnRemoteCandidates(init.transports, error)) + return false; + + // TODO(juberti): Auto-generate and push down the local transport answer. + // This is necessary for trickling to work with RFC 5245 ICE. + } + return true; +} + +bool Session::OnAcceptMessage(const SessionMessage& msg, MessageError* error) { + if (!CheckState(STATE_SENTINITIATE, error)) + return false; + + SessionAccept accept; + if (!ParseSessionAccept(msg.protocol, msg.action_elem, + GetContentParsers(), GetTransportParsers(), + GetCandidateTranslators(), + &accept, error)) { + return false; + } + + // If we get an accept, we can assume the initiate has been + // received, even if we haven't gotten an IQ response. + OnInitiateAcked(); + + set_remote_description(new SessionDescription(accept.ClearContents(), + accept.transports, + accept.groups)); + // Updating transport with TransportDescription. + PushdownTransportDescription(CS_REMOTE, CA_ANSWER); + MaybeEnableMuxingSupport(); // Enable transport channel mux if supported. + SetState(STATE_RECEIVEDACCEPT); + + if (!OnRemoteCandidates(accept.transports, error)) + return false; + + return true; +} + +bool Session::OnRejectMessage(const SessionMessage& msg, MessageError* error) { + if (!CheckState(STATE_SENTINITIATE, error)) + return false; + + SetState(STATE_RECEIVEDREJECT); + return true; +} + +bool Session::OnInfoMessage(const SessionMessage& msg) { + SignalInfoMessage(this, msg.action_elem); + return true; +} + +bool Session::OnTerminateMessage(const SessionMessage& msg, + MessageError* error) { + SessionTerminate term; + if (!ParseSessionTerminate(msg.protocol, msg.action_elem, &term, error)) + return false; + + SignalReceivedTerminateReason(this, term.reason); + if (term.debug_reason != buzz::STR_EMPTY) { + LOG(LS_VERBOSE) << "Received error on call: " << term.debug_reason; + } + + SetState(STATE_RECEIVEDTERMINATE); + return true; +} + +bool Session::OnTransportInfoMessage(const SessionMessage& msg, + MessageError* error) { + TransportInfos tinfos; + if (!ParseTransportInfos(msg.protocol, msg.action_elem, + initiator_description()->contents(), + GetTransportParsers(), GetCandidateTranslators(), + &tinfos, error)) + return false; + + if (!OnRemoteCandidates(tinfos, error)) + return false; + + return true; +} + +bool Session::OnTransportAcceptMessage(const SessionMessage& msg, + MessageError* error) { + // TODO: Currently here only for compatibility with + // Gingle 1.1 clients (notably, Google Voice). + return true; +} + +bool Session::OnDescriptionInfoMessage(const SessionMessage& msg, + MessageError* error) { + if (!CheckState(STATE_INPROGRESS, error)) + return false; + + DescriptionInfo description_info; + if (!ParseDescriptionInfo(msg.protocol, msg.action_elem, + GetContentParsers(), GetTransportParsers(), + GetCandidateTranslators(), + &description_info, error)) { + return false; + } + + ContentInfos updated_contents = description_info.ClearContents(); + + // TODO: Currently, reflector sends back + // video stream updates even for an audio-only call, which causes + // this to fail. Put this back once reflector is fixed. + // + // ContentInfos::iterator it; + // First, ensure all updates are valid before modifying remote_description_. + // for (it = updated_contents.begin(); it != updated_contents.end(); ++it) { + // if (remote_description()->GetContentByName(it->name) == NULL) { + // return false; + // } + // } + + // TODO: We used to replace contents from an update, but + // that no longer works with partial updates. We need to figure out + // a way to merge patial updates into contents. For now, users of + // Session should listen to SignalRemoteDescriptionUpdate and handle + // updates. They should not expect remote_description to be the + // latest value. + // + // for (it = updated_contents.begin(); it != updated_contents.end(); ++it) { + // remote_description()->RemoveContentByName(it->name); + // remote_description()->AddContent(it->name, it->type, it->description); + // } + // } + + SignalRemoteDescriptionUpdate(this, updated_contents); + return true; +} + +bool BareJidsEqual(const std::string& name1, + const std::string& name2) { + buzz::Jid jid1(name1); + buzz::Jid jid2(name2); + + return jid1.IsValid() && jid2.IsValid() && jid1.BareEquals(jid2); +} + +bool Session::OnRedirectError(const SessionRedirect& redirect, + SessionError* error) { + MessageError message_error; + if (!CheckState(STATE_SENTINITIATE, &message_error)) { + return BadWrite(message_error.text, error); + } + + if (!BareJidsEqual(remote_name(), redirect.target)) + return BadWrite("Redirection not allowed: must be the same bare jid.", + error); + + // When we receive a redirect, we point the session at the new JID + // and resend the candidates. + set_remote_name(redirect.target); + return (SendInitiateMessage(local_description(), error) && + ResendAllTransportInfoMessages(error)); +} + +bool Session::CheckState(State expected, MessageError* error) { + if (state() != expected) { + // The server can deliver messages out of order/repeated for various + // reasons. For example, if the server does not recive our iq response, + // it could assume that the iq it sent was lost, and will then send + // it again. Ideally, we should implement reliable messaging with + // duplicate elimination. + return BadMessage(buzz::QN_STANZA_NOT_ALLOWED, + "message not allowed in current state", + error); + } + return true; +} + +void Session::SetError(Error error) { + BaseSession::SetError(error); + if (error != ERROR_NONE) + signaling_thread()->Post(this, MSG_ERROR); +} + +void Session::OnMessage(talk_base::Message* pmsg) { + // preserve this because BaseSession::OnMessage may modify it + State orig_state = state(); + + BaseSession::OnMessage(pmsg); + + switch (pmsg->message_id) { + case MSG_ERROR: + TerminateWithReason(STR_TERMINATE_ERROR); + break; + + case MSG_STATE: + switch (orig_state) { + case STATE_SENTREJECT: + case STATE_RECEIVEDREJECT: + // Assume clean termination. + Terminate(); + break; + + case STATE_SENTTERMINATE: + case STATE_RECEIVEDTERMINATE: + session_manager_->DestroySession(this); + break; + + default: + // Explicitly ignoring some states here. + break; + } + break; + } +} + +bool Session::SendInitiateMessage(const SessionDescription* sdesc, + SessionError* error) { + SessionInitiate init; + init.contents = sdesc->contents(); + init.transports = GetEmptyTransportInfos(init.contents); + init.groups = sdesc->groups(); + return SendMessage(ACTION_SESSION_INITIATE, init, error); +} + +bool Session::WriteSessionAction( + SignalingProtocol protocol, const SessionInitiate& init, + XmlElements* elems, WriteError* error) { + return WriteSessionInitiate(protocol, init.contents, init.transports, + GetContentParsers(), GetTransportParsers(), + GetCandidateTranslators(), init.groups, + elems, error); +} + +bool Session::SendAcceptMessage(const SessionDescription* sdesc, + SessionError* error) { + XmlElements elems; + if (!WriteSessionAccept(current_protocol_, + sdesc->contents(), + GetEmptyTransportInfos(sdesc->contents()), + GetContentParsers(), GetTransportParsers(), + GetCandidateTranslators(), sdesc->groups(), + &elems, error)) { + return false; + } + return SendMessage(ACTION_SESSION_ACCEPT, elems, error); +} + +bool Session::SendRejectMessage(const std::string& reason, + SessionError* error) { + SessionTerminate term(reason); + return SendMessage(ACTION_SESSION_REJECT, term, error); +} + +bool Session::SendTerminateMessage(const std::string& reason, + SessionError* error) { + SessionTerminate term(reason); + return SendMessage(ACTION_SESSION_TERMINATE, term, error); +} + +bool Session::WriteSessionAction(SignalingProtocol protocol, + const SessionTerminate& term, + XmlElements* elems, WriteError* error) { + WriteSessionTerminate(protocol, term, elems); + return true; +} + +bool Session::SendTransportInfoMessage(const TransportInfo& tinfo, + SessionError* error) { + return SendMessage(ACTION_TRANSPORT_INFO, tinfo, error); +} + +bool Session::SendTransportInfoMessage(const TransportProxy* transproxy, + const Candidates& candidates, + SessionError* error) { + return SendTransportInfoMessage(TransportInfo(transproxy->content_name(), + TransportDescription(transproxy->type(), candidates)), error); +} + +bool Session::WriteSessionAction(SignalingProtocol protocol, + const TransportInfo& tinfo, + XmlElements* elems, WriteError* error) { + TransportInfos tinfos; + tinfos.push_back(tinfo); + return WriteTransportInfos(protocol, tinfos, + GetTransportParsers(), GetCandidateTranslators(), + elems, error); +} + +bool Session::ResendAllTransportInfoMessages(SessionError* error) { + for (TransportMap::const_iterator iter = transport_proxies().begin(); + iter != transport_proxies().end(); ++iter) { + TransportProxy* transproxy = iter->second; + if (transproxy->sent_candidates().size() > 0) { + if (!SendTransportInfoMessage( + transproxy, transproxy->sent_candidates(), error)) { + LOG(LS_ERROR) << "Could not resend transport info messages: " + << error->text; + return false; + } + transproxy->ClearSentCandidates(); + } + } + return true; +} + +bool Session::SendAllUnsentTransportInfoMessages(SessionError* error) { + for (TransportMap::const_iterator iter = transport_proxies().begin(); + iter != transport_proxies().end(); ++iter) { + TransportProxy* transproxy = iter->second; + if (transproxy->unsent_candidates().size() > 0) { + if (!SendTransportInfoMessage( + transproxy, transproxy->unsent_candidates(), error)) { + LOG(LS_ERROR) << "Could not send unsent transport info messages: " + << error->text; + return false; + } + transproxy->ClearUnsentCandidates(); + } + } + return true; +} + +bool Session::SendMessage(ActionType type, const XmlElements& action_elems, + SessionError* error) { + talk_base::scoped_ptr stanza( + new buzz::XmlElement(buzz::QN_IQ)); + + SessionMessage msg(current_protocol_, type, id(), initiator_name()); + msg.to = remote_name(); + WriteSessionMessage(msg, action_elems, stanza.get()); + + SignalOutgoingMessage(this, stanza.get()); + return true; +} + +template +bool Session::SendMessage(ActionType type, const Action& action, + SessionError* error) { + talk_base::scoped_ptr stanza( + new buzz::XmlElement(buzz::QN_IQ)); + if (!WriteActionMessage(type, action, stanza.get(), error)) + return false; + + SignalOutgoingMessage(this, stanza.get()); + return true; +} + +template +bool Session::WriteActionMessage(ActionType type, const Action& action, + buzz::XmlElement* stanza, + WriteError* error) { + if (current_protocol_ == PROTOCOL_HYBRID) { + if (!WriteActionMessage(PROTOCOL_JINGLE, type, action, stanza, error)) + return false; + if (!WriteActionMessage(PROTOCOL_GINGLE, type, action, stanza, error)) + return false; + } else { + if (!WriteActionMessage(current_protocol_, type, action, stanza, error)) + return false; + } + return true; +} + +template +bool Session::WriteActionMessage(SignalingProtocol protocol, + ActionType type, const Action& action, + buzz::XmlElement* stanza, WriteError* error) { + XmlElements action_elems; + if (!WriteSessionAction(protocol, action, &action_elems, error)) + return false; + + SessionMessage msg(protocol, type, id(), initiator_name()); + msg.to = remote_name(); + + WriteSessionMessage(msg, action_elems, stanza); + return true; +} + +void Session::SendAcknowledgementMessage(const buzz::XmlElement* stanza) { + talk_base::scoped_ptr ack( + new buzz::XmlElement(buzz::QN_IQ)); + ack->SetAttr(buzz::QN_TO, remote_name()); + ack->SetAttr(buzz::QN_ID, stanza->Attr(buzz::QN_ID)); + ack->SetAttr(buzz::QN_TYPE, "result"); + + SignalOutgoingMessage(this, ack.get()); +} + +} // namespace cricket diff --git a/talk/p2p/base/session.h b/talk/p2p/base/session.h new file mode 100644 index 000000000..5f0651815 --- /dev/null +++ b/talk/p2p/base/session.h @@ -0,0 +1,723 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_SESSION_H_ +#define TALK_P2P_BASE_SESSION_H_ + +#include +#include +#include +#include + +#include "talk/base/refcount.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/scoped_ref_ptr.h" +#include "talk/base/socketaddress.h" +#include "talk/p2p/base/parsing.h" +#include "talk/p2p/base/port.h" +#include "talk/p2p/base/sessionclient.h" +#include "talk/p2p/base/sessionmanager.h" +#include "talk/p2p/base/sessionmessages.h" +#include "talk/p2p/base/transport.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" + +namespace cricket { + +class BaseSession; +class P2PTransportChannel; +class Transport; +class TransportChannel; +class TransportChannelProxy; +class TransportChannelImpl; + +typedef talk_base::RefCountedObject > +TransportWrapper; + +// Used for errors that will send back a specific error message to the +// remote peer. We add "type" to the errors because it's needed for +// SignalErrorMessage. +struct MessageError : ParseError { + buzz::QName type; + + // if unset, assume type is a parse error + MessageError() : ParseError(), type(buzz::QN_STANZA_BAD_REQUEST) {} + + void SetType(const buzz::QName type) { + this->type = type; + } +}; + +// Used for errors that may be returned by public session methods that +// can fail. +// TODO: Use this error in Session::Initiate and +// Session::Accept. +struct SessionError : WriteError { +}; + +// Bundles a Transport and ChannelMap together. ChannelMap is used to +// create transport channels before receiving or sending a session +// initiate, and for speculatively connecting channels. Previously, a +// session had one ChannelMap and transport. Now, with multiple +// transports per session, we need multiple ChannelMaps as well. + +typedef std::map ChannelMap; + +class TransportProxy : public sigslot::has_slots<>, + public CandidateTranslator { + public: + TransportProxy( + const std::string& sid, + const std::string& content_name, + TransportWrapper* transport) + : sid_(sid), + content_name_(content_name), + transport_(transport), + connecting_(false), + negotiated_(false), + sent_candidates_(false), + candidates_allocated_(false) { + transport_->get()->SignalCandidatesReady.connect( + this, &TransportProxy::OnTransportCandidatesReady); + } + ~TransportProxy(); + + std::string content_name() const { return content_name_; } + // TODO(juberti): It's not good form to expose the object you're wrapping, + // since callers can mutate it. Can we make this return a const Transport*? + Transport* impl() const { return transport_->get(); } + + std::string type() const; + bool negotiated() const { return negotiated_; } + const Candidates& sent_candidates() const { return sent_candidates_; } + const Candidates& unsent_candidates() const { return unsent_candidates_; } + bool candidates_allocated() const { return candidates_allocated_; } + void set_candidates_allocated(bool allocated) { + candidates_allocated_ = allocated; + } + + TransportChannel* GetChannel(int component); + TransportChannel* CreateChannel(const std::string& channel_name, + int component); + bool HasChannel(int component); + void DestroyChannel(int component); + + void AddSentCandidates(const Candidates& candidates); + void AddUnsentCandidates(const Candidates& candidates); + void ClearSentCandidates() { sent_candidates_.clear(); } + void ClearUnsentCandidates() { unsent_candidates_.clear(); } + + // Start the connection process for any channels, creating impls if needed. + void ConnectChannels(); + // Hook up impls to the proxy channels. Doesn't change connect state. + void CompleteNegotiation(); + + // Mux this proxy onto the specified proxy's transport. + bool SetupMux(TransportProxy* proxy); + + // Simple functions that thunk down to the same functions on Transport. + void SetRole(TransportRole role); + bool SetLocalTransportDescription(const TransportDescription& description, + ContentAction action); + bool SetRemoteTransportDescription(const TransportDescription& description, + ContentAction action); + void OnSignalingReady(); + bool OnRemoteCandidates(const Candidates& candidates, std::string* error); + + // CandidateTranslator methods. + virtual bool GetChannelNameFromComponent( + int component, std::string* channel_name) const; + virtual bool GetComponentFromChannelName( + const std::string& channel_name, int* component) const; + + // Called when a transport signals that it has new candidates. + void OnTransportCandidatesReady(cricket::Transport* transport, + const Candidates& candidates) { + SignalCandidatesReady(this, candidates); + } + + // Handles sending of ready candidates and receiving of remote candidates. + sigslot::signal2&> SignalCandidatesReady; + + private: + TransportChannelProxy* GetChannelProxy(int component) const; + TransportChannelProxy* GetChannelProxyByName(const std::string& name) const; + void ReplaceChannelProxyImpl(TransportChannelProxy* channel_proxy, + size_t index); + TransportChannelImpl* GetOrCreateChannelProxyImpl(int component); + void SetChannelProxyImpl(int component, + TransportChannelProxy* proxy); + + std::string sid_; + std::string content_name_; + talk_base::scoped_refptr transport_; + bool connecting_; + bool negotiated_; + ChannelMap channels_; + Candidates sent_candidates_; + Candidates unsent_candidates_; + bool candidates_allocated_; +}; + +typedef std::map TransportMap; + +// Statistics for all the transports of this session. +typedef std::map TransportStatsMap; +typedef std::map ProxyTransportMap; + +struct SessionStats { + ProxyTransportMap proxy_to_transport; + TransportStatsMap transport_stats; +}; + +// A BaseSession manages general session state. This includes negotiation +// of both the application-level and network-level protocols: the former +// defines what will be sent and the latter defines how it will be sent. Each +// network-level protocol is represented by a Transport object. Each Transport +// participates in the network-level negotiation. The individual streams of +// packets are represented by TransportChannels. The application-level protocol +// is represented by SessionDecription objects. +class BaseSession : public sigslot::has_slots<>, + public talk_base::MessageHandler { + public: + enum { + MSG_TIMEOUT = 0, + MSG_ERROR, + MSG_STATE, + }; + + enum State { + STATE_INIT = 0, + STATE_SENTINITIATE, // sent initiate, waiting for Accept or Reject + STATE_RECEIVEDINITIATE, // received an initiate. Call Accept or Reject + STATE_SENTPRACCEPT, // sent provisional Accept + STATE_SENTACCEPT, // sent accept. begin connecting transport + STATE_RECEIVEDPRACCEPT, // received provisional Accept, waiting for Accept + STATE_RECEIVEDACCEPT, // received accept. begin connecting transport + STATE_SENTMODIFY, // sent modify, waiting for Accept or Reject + STATE_RECEIVEDMODIFY, // received modify, call Accept or Reject + STATE_SENTREJECT, // sent reject after receiving initiate + STATE_RECEIVEDREJECT, // received reject after sending initiate + STATE_SENTREDIRECT, // sent direct after receiving initiate + STATE_SENTTERMINATE, // sent terminate (any time / either side) + STATE_RECEIVEDTERMINATE, // received terminate (any time / either side) + STATE_INPROGRESS, // session accepted and in progress + STATE_DEINIT, // session is being destroyed + }; + + enum Error { + ERROR_NONE = 0, // no error + ERROR_TIME = 1, // no response to signaling + ERROR_RESPONSE = 2, // error during signaling + ERROR_NETWORK = 3, // network error, could not allocate network resources + ERROR_CONTENT = 4, // channel errors in SetLocalContent/SetRemoteContent + ERROR_TRANSPORT = 5, // transport error of some kind + }; + + // Convert State to a readable string. + static std::string StateToString(State state); + + BaseSession(talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread, + PortAllocator* port_allocator, + const std::string& sid, + const std::string& content_type, + bool initiator); + virtual ~BaseSession(); + + talk_base::Thread* signaling_thread() { return signaling_thread_; } + talk_base::Thread* worker_thread() { return worker_thread_; } + PortAllocator* port_allocator() { return port_allocator_; } + + // The ID of this session. + const std::string& id() const { return sid_; } + + // TODO(juberti): This data is largely redundant, as it can now be obtained + // from local/remote_description(). Remove these functions and members. + // Returns the XML namespace identifying the type of this session. + const std::string& content_type() const { return content_type_; } + // Returns the XML namespace identifying the transport used for this session. + const std::string& transport_type() const { return transport_type_; } + + // Indicates whether we initiated this session. + bool initiator() const { return initiator_; } + + // Returns the application-level description given by our client. + // If we are the recipient, this will be NULL until we send an accept. + const SessionDescription* local_description() const { + return local_description_; + } + // Returns the application-level description given by the other client. + // If we are the initiator, this will be NULL until we receive an accept. + const SessionDescription* remote_description() const { + return remote_description_; + } + SessionDescription* remote_description() { + return remote_description_; + } + + // Takes ownership of SessionDescription* + bool set_local_description(const SessionDescription* sdesc) { + if (sdesc != local_description_) { + delete local_description_; + local_description_ = sdesc; + } + return true; + } + + // Takes ownership of SessionDescription* + bool set_remote_description(SessionDescription* sdesc) { + if (sdesc != remote_description_) { + delete remote_description_; + remote_description_ = sdesc; + } + return true; + } + + const SessionDescription* initiator_description() const { + if (initiator_) { + return local_description_; + } else { + return remote_description_; + } + } + + // Returns the current state of the session. See the enum above for details. + // Each time the state changes, we will fire this signal. + State state() const { return state_; } + sigslot::signal2 SignalState; + + // Returns the last error in the session. See the enum above for details. + // Each time the an error occurs, we will fire this signal. + Error error() const { return error_; } + sigslot::signal2 SignalError; + + // Updates the state, signaling if necessary. + virtual void SetState(State state); + + // Updates the error state, signaling if necessary. + virtual void SetError(Error error); + + // Fired when the remote description is updated, with the updated + // contents. + sigslot::signal2 + SignalRemoteDescriptionUpdate; + + // Fired when SetState is called (regardless if there's a state change), which + // indicates the session description might have be updated. + sigslot::signal2 SignalNewLocalDescription; + + // Fired when SetState is called (regardless if there's a state change), which + // indicates the session description might have be updated. + sigslot::signal2 SignalNewRemoteDescription; + + // Returns the transport that has been negotiated or NULL if + // negotiation is still in progress. + Transport* GetTransport(const std::string& content_name); + + // Creates a new channel with the given names. This method may be called + // immediately after creating the session. However, the actual + // implementation may not be fixed until transport negotiation completes. + // This will usually be called from the worker thread, but that + // shouldn't be an issue since the main thread will be blocked in + // Send when doing so. + virtual TransportChannel* CreateChannel(const std::string& content_name, + const std::string& channel_name, + int component); + + // Returns the channel with the given names. + virtual TransportChannel* GetChannel(const std::string& content_name, + int component); + + // Destroys the channel with the given names. + // This will usually be called from the worker thread, but that + // shouldn't be an issue since the main thread will be blocked in + // Send when doing so. + virtual void DestroyChannel(const std::string& content_name, + int component); + + // Returns stats for all channels of all transports. + // This avoids exposing the internal structures used to track them. + virtual bool GetStats(SessionStats* stats); + + protected: + bool PushdownTransportDescription(ContentSource source, + ContentAction action); + void set_initiator(bool initiator) { initiator_ = initiator; } + + talk_base::SSLIdentity* identity() { return identity_; } + // Specifies the identity to use in this session. + void set_identity(talk_base::SSLIdentity* identity) { identity_ = identity; } + + const TransportMap& transport_proxies() const { return transports_; } + // Get a TransportProxy by content_name or transport. NULL if not found. + TransportProxy* GetTransportProxy(const std::string& content_name); + TransportProxy* GetTransportProxy(const Transport* transport); + TransportProxy* GetFirstTransportProxy(); + void DestroyTransportProxy(const std::string& content_name); + // TransportProxy is owned by session. Return proxy just for convenience. + TransportProxy* GetOrCreateTransportProxy(const std::string& content_name); + // Creates the actual transport object. Overridable for testing. + virtual Transport* CreateTransport(const std::string& content_name); + + void OnSignalingReady(); + void SpeculativelyConnectAllTransportChannels(); + // Helper method to provide remote candidates to the transport. + bool OnRemoteCandidates(const std::string& content_name, + const Candidates& candidates, + std::string* error); + + // This method will mux transport channels by content_name. + // First content is used for muxing. + bool MaybeEnableMuxingSupport(); + + // Called when a transport requests signaling. + virtual void OnTransportRequestSignaling(Transport* transport) { + } + + // Called when the first channel of a transport begins connecting. We use + // this to start a timer, to make sure that the connection completes in a + // reasonable amount of time. + virtual void OnTransportConnecting(Transport* transport) { + } + + // Called when a transport changes its writable state. We track this to make + // sure that the transport becomes writable within a reasonable amount of + // time. If this does not occur, we signal an error. + virtual void OnTransportWritable(Transport* transport) { + } + virtual void OnTransportReadable(Transport* transport) { + } + + // Called when a transport signals that it has new candidates. + virtual void OnTransportProxyCandidatesReady(TransportProxy* proxy, + const Candidates& candidates) { + } + + // Called when a transport signals that it found an error in an incoming + // message. + virtual void OnTransportSendError(Transport* transport, + const buzz::XmlElement* stanza, + const buzz::QName& name, + const std::string& type, + const std::string& text, + const buzz::XmlElement* extra_info) { + } + + virtual void OnTransportRouteChange( + Transport* transport, + int component, + const cricket::Candidate& remote_candidate) { + } + + virtual void OnTransportCandidatesAllocationDone(Transport* transport); + + // Called when all transport channels allocated required candidates. + // This method should be used as an indication of candidates gathering process + // is completed and application can now send local candidates list to remote. + virtual void OnCandidatesAllocationDone() { + } + + // Handles the ice role change callback from Transport. This must be + // propagated to all the transports. + virtual void OnRoleConflict(); + + // Handles messages posted to us. + virtual void OnMessage(talk_base::Message *pmsg); + + protected: + State state_; + Error error_; + + private: + // Helper methods to push local and remote transport descriptions. + bool PushdownLocalTransportDescription( + const SessionDescription* sdesc, ContentAction action); + bool PushdownRemoteTransportDescription( + const SessionDescription* sdesc, ContentAction action); + + bool IsCandidateAllocationDone() const; + void MaybeCandidateAllocationDone(); + + // This method will delete the Transport and TransportChannelImpls and + // replace those with the selected Transport objects. Selection is done + // based on the content_name and in this case first MediaContent information + // is used for mux. + bool SetSelectedProxy(const std::string& content_name, + const ContentGroup* muxed_group); + // Log session state. + void LogState(State old_state, State new_state); + + // Returns true and the TransportInfo of the given |content_name| + // from |description|. Returns false if it's not available. + bool GetTransportDescription(const SessionDescription* description, + const std::string& content_name, + TransportDescription* info); + + // Fires the new description signal according to the current state. + void SignalNewDescription(); + + // Gets the ContentAction and ContentSource according to the session state. + bool GetContentAction(ContentAction* action, ContentSource* source); + + talk_base::Thread* signaling_thread_; + talk_base::Thread* worker_thread_; + PortAllocator* port_allocator_; + std::string sid_; + std::string content_type_; + std::string transport_type_; + bool initiator_; + talk_base::SSLIdentity* identity_; + const SessionDescription* local_description_; + SessionDescription* remote_description_; + uint64 ice_tiebreaker_; + // This flag will be set to true after the first role switch. This flag + // will enable us to stop any role switch during the call. + bool role_switch_; + TransportMap transports_; +}; + +// A specific Session created by the SessionManager, using XMPP for protocol. +class Session : public BaseSession { + public: + // Returns the manager that created and owns this session. + SessionManager* session_manager() const { return session_manager_; } + + // Returns the client that is handling the application data of this session. + SessionClient* client() const { return client_; } + + // Returns the JID of this client. + const std::string& local_name() const { return local_name_; } + + // Returns the JID of the other peer in this session. + const std::string& remote_name() const { return remote_name_; } + + // Set the JID of the other peer in this session. + // Typically the remote_name_ is set when the session is initiated. + // However, sometimes (e.g when a proxy is used) the peer name is + // known after the BaseSession has been initiated and it must be updated + // explicitly. + void set_remote_name(const std::string& name) { remote_name_ = name; } + + // Set the JID of the initiator of this session. Allows for the overriding + // of the initiator to be a third-party, eg. the MUC JID when creating p2p + // sessions. + void set_initiator_name(const std::string& name) { initiator_name_ = name; } + + // Indicates the JID of the entity who initiated this session. + // In special cases, may be different than both local_name and remote_name. + const std::string& initiator_name() const { return initiator_name_; } + + SignalingProtocol current_protocol() const { return current_protocol_; } + + void set_current_protocol(SignalingProtocol protocol) { + current_protocol_ = protocol; + } + + // Updates the error state, signaling if necessary. + virtual void SetError(Error error); + + // When the session needs to send signaling messages, it beings by requesting + // signaling. The client should handle this by calling OnSignalingReady once + // it is ready to send the messages. + // (These are called only by SessionManager.) + sigslot::signal1 SignalRequestSignaling; + void OnSignalingReady() { BaseSession::OnSignalingReady(); } + + // Takes ownership of session description. + // TODO: Add an error argument to pass back to the caller. + bool Initiate(const std::string& to, + const SessionDescription* sdesc); + + // When we receive an initiate, we create a session in the + // RECEIVEDINITIATE state and respond by accepting or rejecting. + // Takes ownership of session description. + // TODO: Add an error argument to pass back to the caller. + bool Accept(const SessionDescription* sdesc); + bool Reject(const std::string& reason); + bool Terminate() { + return TerminateWithReason(STR_TERMINATE_SUCCESS); + } + bool TerminateWithReason(const std::string& reason); + // Fired whenever we receive a terminate message along with a reason + sigslot::signal2 SignalReceivedTerminateReason; + + // The two clients in the session may also send one another + // arbitrary XML messages, which are called "info" messages. Sending + // takes ownership of the given elements. The signal does not; the + // parent element will be deleted after the signal. + bool SendInfoMessage(const XmlElements& elems); + bool SendDescriptionInfoMessage(const ContentInfos& contents); + sigslot::signal2 SignalInfoMessage; + + private: + // Creates or destroys a session. (These are called only SessionManager.) + Session(SessionManager *session_manager, + const std::string& local_name, const std::string& initiator_name, + const std::string& sid, const std::string& content_type, + SessionClient* client); + ~Session(); + // For each transport info, create a transport proxy. Can fail for + // incompatible transport types. + bool CreateTransportProxies(const TransportInfos& tinfos, + SessionError* error); + bool OnRemoteCandidates(const TransportInfos& tinfos, + ParseError* error); + // Returns a TransportInfo without candidates for each content name. + // Uses the transport_type_ of the session. + TransportInfos GetEmptyTransportInfos(const ContentInfos& contents) const; + + // Maps passed to serialization functions. + TransportParserMap GetTransportParsers(); + ContentParserMap GetContentParsers(); + CandidateTranslatorMap GetCandidateTranslators(); + + virtual void OnTransportRequestSignaling(Transport* transport); + virtual void OnTransportConnecting(Transport* transport); + virtual void OnTransportWritable(Transport* transport); + virtual void OnTransportProxyCandidatesReady(TransportProxy* proxy, + const Candidates& candidates); + virtual void OnTransportSendError(Transport* transport, + const buzz::XmlElement* stanza, + const buzz::QName& name, + const std::string& type, + const std::string& text, + const buzz::XmlElement* extra_info); + virtual void OnMessage(talk_base::Message *pmsg); + + // Send various kinds of session messages. + bool SendInitiateMessage(const SessionDescription* sdesc, + SessionError* error); + bool SendAcceptMessage(const SessionDescription* sdesc, SessionError* error); + bool SendRejectMessage(const std::string& reason, SessionError* error); + bool SendTerminateMessage(const std::string& reason, SessionError* error); + bool SendTransportInfoMessage(const TransportInfo& tinfo, + SessionError* error); + bool SendTransportInfoMessage(const TransportProxy* transproxy, + const Candidates& candidates, + SessionError* error); + + bool ResendAllTransportInfoMessages(SessionError* error); + bool SendAllUnsentTransportInfoMessages(SessionError* error); + + // Both versions of SendMessage send a message of the given type to + // the other client. Can pass either a set of elements or an + // "action", which must have a WriteSessionAction method to go along + // with it. Sending with an action supports sending a "hybrid" + // message. Sending with elements must be sent as Jingle or Gingle. + + // When passing elems, must be either Jingle or Gingle protocol. + // Takes ownership of action_elems. + bool SendMessage(ActionType type, const XmlElements& action_elems, + SessionError* error); + // When passing an action, may be Hybrid protocol. + template + bool SendMessage(ActionType type, const Action& action, + SessionError* error); + + // Helper methods to write the session message stanza. + template + bool WriteActionMessage(ActionType type, const Action& action, + buzz::XmlElement* stanza, WriteError* error); + template + bool WriteActionMessage(SignalingProtocol protocol, + ActionType type, const Action& action, + buzz::XmlElement* stanza, WriteError* error); + + // Sending messages in hybrid form requires being able to write them + // on a per-protocol basis with a common method signature, which all + // of these have. + bool WriteSessionAction(SignalingProtocol protocol, + const SessionInitiate& init, + XmlElements* elems, WriteError* error); + bool WriteSessionAction(SignalingProtocol protocol, + const TransportInfo& tinfo, + XmlElements* elems, WriteError* error); + bool WriteSessionAction(SignalingProtocol protocol, + const SessionTerminate& term, + XmlElements* elems, WriteError* error); + + // Sends a message back to the other client indicating that we have received + // and accepted their message. + void SendAcknowledgementMessage(const buzz::XmlElement* stanza); + + // Once signaling is ready, the session will use this signal to request the + // sending of each message. When messages are received by the other client, + // they should be handed to OnIncomingMessage. + // (These are called only by SessionManager.) + sigslot::signal2 SignalOutgoingMessage; + void OnIncomingMessage(const SessionMessage& msg); + + void OnIncomingResponse(const buzz::XmlElement* orig_stanza, + const buzz::XmlElement* response_stanza, + const SessionMessage& msg); + void OnInitiateAcked(); + void OnFailedSend(const buzz::XmlElement* orig_stanza, + const buzz::XmlElement* error_stanza); + + // Invoked when an error is found in an incoming message. This is translated + // into the appropriate XMPP response by SessionManager. + sigslot::signal6 SignalErrorMessage; + + // Handlers for the various types of messages. These functions may take + // pointers to the whole stanza or to just the session element. + bool OnInitiateMessage(const SessionMessage& msg, MessageError* error); + bool OnAcceptMessage(const SessionMessage& msg, MessageError* error); + bool OnRejectMessage(const SessionMessage& msg, MessageError* error); + bool OnInfoMessage(const SessionMessage& msg); + bool OnTerminateMessage(const SessionMessage& msg, MessageError* error); + bool OnTransportInfoMessage(const SessionMessage& msg, MessageError* error); + bool OnTransportAcceptMessage(const SessionMessage& msg, MessageError* error); + bool OnDescriptionInfoMessage(const SessionMessage& msg, MessageError* error); + bool OnRedirectError(const SessionRedirect& redirect, SessionError* error); + + // Verifies that we are in the appropriate state to receive this message. + bool CheckState(State state, MessageError* error); + + SessionManager* session_manager_; + bool initiate_acked_; + std::string local_name_; + std::string initiator_name_; + std::string remote_name_; + SessionClient* client_; + TransportParser* transport_parser_; + // Keeps track of what protocol we are speaking. + SignalingProtocol current_protocol_; + + friend class SessionManager; // For access to constructor, destructor, + // and signaling related methods. +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_SESSION_H_ diff --git a/talk/p2p/base/session_unittest.cc b/talk/p2p/base/session_unittest.cc new file mode 100644 index 000000000..73933bbc8 --- /dev/null +++ b/talk/p2p/base/session_unittest.cc @@ -0,0 +1,2464 @@ +/* + * libjingle + * Copyright 2004 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 +#include +#include +#include + +#include "talk/base/base64.h" +#include "talk/base/common.h" +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/host.h" +#include "talk/base/logging.h" +#include "talk/base/natserver.h" +#include "talk/base/natsocketfactory.h" +#include "talk/base/stringencode.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/parsing.h" +#include "talk/p2p/base/portallocator.h" +#include "talk/p2p/base/p2ptransport.h" +#include "talk/p2p/base/relayport.h" +#include "talk/p2p/base/relayserver.h" +#include "talk/p2p/base/session.h" +#include "talk/p2p/base/sessionclient.h" +#include "talk/p2p/base/sessionmanager.h" +#include "talk/p2p/base/stunport.h" +#include "talk/p2p/base/stunserver.h" +#include "talk/p2p/base/transportchannel.h" +#include "talk/p2p/base/transportchannelproxy.h" +#include "talk/p2p/base/udpport.h" +#include "talk/xmpp/constants.h" + +using cricket::SignalingProtocol; +using cricket::PROTOCOL_HYBRID; +using cricket::PROTOCOL_JINGLE; +using cricket::PROTOCOL_GINGLE; + +static const std::string kInitiator = "init@init.com"; +static const std::string kResponder = "resp@resp.com"; +// Expected from test random number generator. +static const std::string kSessionId = "9254631414740579489"; +// TODO: When we need to test more than one transport type, +// allow this to be injected like the content types are. +static const std::string kTransportType = "http://www.google.com/transport/p2p"; + +// Controls how long we wait for a session to send messages that we +// expect, in milliseconds. We put it high to avoid flaky tests. +static const int kEventTimeout = 5000; + +static const int kNumPorts = 2; +static const int kPort0 = 28653; +static const int kPortStep = 5; + +static const std::string kNotifyNick1 = "derekcheng_google.com^59422C27"; +static const std::string kNotifyNick2 = "someoneelses_google.com^7abd6a7a20"; +static const uint32 kNotifyAudioSsrc1 = 2625839801U; +static const uint32 kNotifyAudioSsrc2 = 2529430427U; +static const uint32 kNotifyVideoSsrc1 = 3; +static const uint32 kNotifyVideoSsrc2 = 2; + +static const std::string kViewRequestNick = "param_google.com^16A3CDBE"; +static const uint32 kViewRequestSsrc = 4; +static const int kViewRequestWidth = 320; +static const int kViewRequestHeight = 200; +static const int kViewRequestFrameRate = 15; + +int GetPort(int port_index) { + return kPort0 + (port_index * kPortStep); +} + +std::string GetPortString(int port_index) { + return talk_base::ToString(GetPort(port_index)); +} + +// Only works for port_index < 10, which is fine for our purposes. +std::string GetUsername(int port_index) { + return "username" + std::string(8, talk_base::ToString(port_index)[0]); +} + +// Only works for port_index < 10, which is fine for our purposes. +std::string GetPassword(int port_index) { + return "password" + std::string(8, talk_base::ToString(port_index)[0]); +} + +std::string IqAck(const std::string& id, + const std::string& from, + const std::string& to) { + return ""; +} + +std::string IqSet(const std::string& id, + const std::string& from, + const std::string& to, + const std::string& content) { + return "" + + content + + ""; +} + +std::string IqError(const std::string& id, + const std::string& from, + const std::string& to, + const std::string& content) { + return "" + + content + + ""; +} + +std::string GingleSessionXml(const std::string& type, + const std::string& content) { + return "" + + content + + ""; +} + +std::string GingleDescriptionXml(const std::string& content_type) { + return ""; +} + +std::string P2pCandidateXml(const std::string& name, int port_index) { + // Port will update the rtcp username by +1 on the last character. So we need + // to compensate here. See Port::username_fragment() for detail. + std::string username = GetUsername(port_index); + // TODO: Use the component id instead of the channel name to + // determinte if we need to covert the username here. + if (name == "rtcp" || name == "video_rtcp" || name == "chanb") { + char next_ch = username[username.size() - 1]; + ASSERT(username.size() > 0); + talk_base::Base64::GetNextBase64Char(next_ch, &next_ch); + username[username.size() - 1] = next_ch; + } + return ""; +} + +std::string JingleActionXml(const std::string& action, + const std::string& content) { + return "" + + content + + ""; +} + +std::string JingleInitiateActionXml(const std::string& content) { + return "" + + content + + ""; +} + +std::string JingleGroupInfoXml(const std::string& content_name_a, + const std::string& content_name_b) { + std::string group_info = ""; + if (!content_name_a.empty()) + group_info += ""; + if (!content_name_b.empty()) + group_info += ""; + group_info += ""; + return group_info; +} + + +std::string JingleEmptyContentXml(const std::string& content_name, + const std::string& content_type, + const std::string& transport_type) { + return "" + "" + "" + ""; +} + +std::string JingleContentXml(const std::string& content_name, + const std::string& content_type, + const std::string& transport_type, + const std::string& transport_main) { + std::string transport = transport_type.empty() ? "" : + "" + + transport_main + + ""; + + return"" + "" + + transport + + ""; +} + +std::string JingleTransportContentXml(const std::string& content_name, + const std::string& transport_type, + const std::string& content) { + return "" + "" + + content + + "" + ""; +} + +std::string GingleInitiateXml(const std::string& content_type) { + return GingleSessionXml( + "initiate", + GingleDescriptionXml(content_type)); +} + +std::string JingleInitiateXml(const std::string& content_name_a, + const std::string& content_type_a, + const std::string& content_name_b, + const std::string& content_type_b, + bool bundle = false) { + std::string content_xml; + if (content_name_b.empty()) { + content_xml = JingleEmptyContentXml( + content_name_a, content_type_a, kTransportType); + } else { + content_xml = JingleEmptyContentXml( + content_name_a, content_type_a, kTransportType) + + JingleEmptyContentXml( + content_name_b, content_type_b, kTransportType); + if (bundle) { + content_xml += JingleGroupInfoXml(content_name_a, content_name_b); + } + } + return JingleInitiateActionXml(content_xml); +} + +std::string GingleAcceptXml(const std::string& content_type) { + return GingleSessionXml( + "accept", + GingleDescriptionXml(content_type)); +} + +std::string JingleAcceptXml(const std::string& content_name_a, + const std::string& content_type_a, + const std::string& content_name_b, + const std::string& content_type_b, + bool bundle = false) { + std::string content_xml; + if (content_name_b.empty()) { + content_xml = JingleEmptyContentXml( + content_name_a, content_type_a, kTransportType); + } else { + content_xml = JingleEmptyContentXml( + content_name_a, content_type_a, kTransportType) + + JingleEmptyContentXml( + content_name_b, content_type_b, kTransportType); + } + if (bundle) { + content_xml += JingleGroupInfoXml(content_name_a, content_name_b); + } + + return JingleActionXml("session-accept", content_xml); +} + +std::string Gingle2CandidatesXml(const std::string& channel_name, + int port_index0, + int port_index1) { + return GingleSessionXml( + "candidates", + P2pCandidateXml(channel_name, port_index0) + + P2pCandidateXml(channel_name, port_index1)); +} + +std::string Gingle4CandidatesXml(const std::string& channel_name_a, + int port_index0, + int port_index1, + const std::string& channel_name_b, + int port_index2, + int port_index3) { + return GingleSessionXml( + "candidates", + P2pCandidateXml(channel_name_a, port_index0) + + P2pCandidateXml(channel_name_a, port_index1) + + P2pCandidateXml(channel_name_b, port_index2) + + P2pCandidateXml(channel_name_b, port_index3)); +} + +std::string Jingle2TransportInfoXml(const std::string& content_name, + const std::string& channel_name, + int port_index0, + int port_index1) { + return JingleActionXml( + "transport-info", + JingleTransportContentXml( + content_name, kTransportType, + P2pCandidateXml(channel_name, port_index0) + + P2pCandidateXml(channel_name, port_index1))); +} + +std::string Jingle4TransportInfoXml(const std::string& content_name, + const std::string& channel_name_a, + int port_index0, + int port_index1, + const std::string& channel_name_b, + int port_index2, + int port_index3) { + return JingleActionXml( + "transport-info", + JingleTransportContentXml( + content_name, kTransportType, + P2pCandidateXml(channel_name_a, port_index0) + + P2pCandidateXml(channel_name_a, port_index1) + + P2pCandidateXml(channel_name_b, port_index2) + + P2pCandidateXml(channel_name_b, port_index3))); +} + +std::string JingleDescriptionInfoXml(const std::string& content_name, + const std::string& content_type) { + return JingleActionXml( + "description-info", + JingleContentXml(content_name, content_type, "", "")); +} + +std::string GingleRejectXml(const std::string& reason) { + return GingleSessionXml( + "reject", + "<" + reason + "/>"); +} + +std::string JingleTerminateXml(const std::string& reason) { + return JingleActionXml( + "session-terminate", + "<" + reason + "/>"); +} + +std::string GingleTerminateXml(const std::string& reason) { + return GingleSessionXml( + "terminate", + "<" + reason + "/>"); +} + +std::string GingleRedirectXml(const std::string& intitiate, + const std::string& target) { + return intitiate + + "" + "" + "xmpp:" + target + + "" + ""; +} + +std::string JingleRedirectXml(const std::string& intitiate, + const std::string& target) { + return intitiate + + "" + "" + "xmpp:" + target + + "" + ""; +} + +std::string InitiateXml(SignalingProtocol protocol, + const std::string& gingle_content_type, + const std::string& content_name_a, + const std::string& content_type_a, + const std::string& content_name_b, + const std::string& content_type_b, + bool bundle = false) { + switch (protocol) { + case PROTOCOL_JINGLE: + return JingleInitiateXml(content_name_a, content_type_a, + content_name_b, content_type_b, + bundle); + case PROTOCOL_GINGLE: + return GingleInitiateXml(gingle_content_type); + case PROTOCOL_HYBRID: + return JingleInitiateXml(content_name_a, content_type_a, + content_name_b, content_type_b) + + GingleInitiateXml(gingle_content_type); + } + return ""; +} + +std::string InitiateXml(SignalingProtocol protocol, + const std::string& content_name, + const std::string& content_type) { + return InitiateXml(protocol, + content_type, + content_name, content_type, + "", ""); +} + +std::string AcceptXml(SignalingProtocol protocol, + const std::string& gingle_content_type, + const std::string& content_name_a, + const std::string& content_type_a, + const std::string& content_name_b, + const std::string& content_type_b, + bool bundle = false) { + switch (protocol) { + case PROTOCOL_JINGLE: + return JingleAcceptXml(content_name_a, content_type_a, + content_name_b, content_type_b, bundle); + case PROTOCOL_GINGLE: + return GingleAcceptXml(gingle_content_type); + case PROTOCOL_HYBRID: + return + JingleAcceptXml(content_name_a, content_type_a, + content_name_b, content_type_b) + + GingleAcceptXml(gingle_content_type); + } + return ""; +} + + +std::string AcceptXml(SignalingProtocol protocol, + const std::string& content_name, + const std::string& content_type, + bool bundle = false) { + return AcceptXml(protocol, + content_type, + content_name, content_type, + "", ""); +} + +std::string TransportInfo2Xml(SignalingProtocol protocol, + const std::string& content_name, + const std::string& channel_name, + int port_index0, + int port_index1) { + switch (protocol) { + case PROTOCOL_JINGLE: + return Jingle2TransportInfoXml( + content_name, + channel_name, port_index0, port_index1); + case PROTOCOL_GINGLE: + return Gingle2CandidatesXml( + channel_name, port_index0, port_index1); + case PROTOCOL_HYBRID: + return + Jingle2TransportInfoXml( + content_name, + channel_name, port_index0, port_index1) + + Gingle2CandidatesXml( + channel_name, port_index0, port_index1); + } + return ""; +} + +std::string TransportInfo4Xml(SignalingProtocol protocol, + const std::string& content_name, + const std::string& channel_name_a, + int port_index0, + int port_index1, + const std::string& channel_name_b, + int port_index2, + int port_index3) { + switch (protocol) { + case PROTOCOL_JINGLE: + return Jingle4TransportInfoXml( + content_name, + channel_name_a, port_index0, port_index1, + channel_name_b, port_index2, port_index3); + case PROTOCOL_GINGLE: + return Gingle4CandidatesXml( + channel_name_a, port_index0, port_index1, + channel_name_b, port_index2, port_index3); + case PROTOCOL_HYBRID: + return + Jingle4TransportInfoXml( + content_name, + channel_name_a, port_index0, port_index1, + channel_name_b, port_index2, port_index3) + + Gingle4CandidatesXml( + channel_name_a, port_index0, port_index1, + channel_name_b, port_index2, port_index3); + } + return ""; +} + +std::string RejectXml(SignalingProtocol protocol, + const std::string& reason) { + switch (protocol) { + case PROTOCOL_JINGLE: + return JingleTerminateXml(reason); + case PROTOCOL_GINGLE: + return GingleRejectXml(reason); + case PROTOCOL_HYBRID: + return JingleTerminateXml(reason) + + GingleRejectXml(reason); + } + return ""; +} + +std::string TerminateXml(SignalingProtocol protocol, + const std::string& reason) { + switch (protocol) { + case PROTOCOL_JINGLE: + return JingleTerminateXml(reason); + case PROTOCOL_GINGLE: + return GingleTerminateXml(reason); + case PROTOCOL_HYBRID: + return JingleTerminateXml(reason) + + GingleTerminateXml(reason); + } + return ""; +} + +std::string RedirectXml(SignalingProtocol protocol, + const std::string& initiate, + const std::string& target) { + switch (protocol) { + case PROTOCOL_JINGLE: + return JingleRedirectXml(initiate, target); + case PROTOCOL_GINGLE: + return GingleRedirectXml(initiate, target); + default: + break; + } + return ""; +} + +// TODO: Break out and join with fakeportallocator.h +class TestPortAllocatorSession : public cricket::PortAllocatorSession { + public: + TestPortAllocatorSession(const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd, + const int port_offset) + : PortAllocatorSession(content_name, component, ice_ufrag, ice_pwd, 0), + port_offset_(port_offset), + ports_(kNumPorts), + address_("127.0.0.1", 0), + network_("network", "unittest", + talk_base::IPAddress(INADDR_LOOPBACK), 8), + socket_factory_(talk_base::Thread::Current()), + running_(false), + port_(28653) { + network_.AddIP(address_.ipaddr()); + } + + ~TestPortAllocatorSession() { + for (size_t i = 0; i < ports_.size(); i++) + delete ports_[i]; + } + + virtual void StartGettingPorts() { + for (int i = 0; i < kNumPorts; i++) { + int index = port_offset_ + i; + ports_[i] = cricket::UDPPort::Create( + talk_base::Thread::Current(), &socket_factory_, + &network_, address_.ipaddr(), GetPort(index), GetPort(index), + GetUsername(index), GetPassword(index)); + AddPort(ports_[i]); + } + running_ = true; + } + + virtual void StopGettingPorts() { running_ = false; } + virtual bool IsGettingPorts() { return running_; } + + void AddPort(cricket::Port* port) { + port->set_component(component_); + port->set_generation(0); + port->SignalDestroyed.connect( + this, &TestPortAllocatorSession::OnPortDestroyed); + port->SignalPortComplete.connect( + this, &TestPortAllocatorSession::OnPortComplete); + port->PrepareAddress(); + SignalPortReady(this, port); + } + + void OnPortDestroyed(cricket::PortInterface* port) { + for (size_t i = 0; i < ports_.size(); i++) { + if (ports_[i] == port) + ports_[i] = NULL; + } + } + + void OnPortComplete(cricket::Port* port) { + SignalCandidatesReady(this, port->Candidates()); + } + + private: + int port_offset_; + std::vector ports_; + talk_base::SocketAddress address_; + talk_base::Network network_; + talk_base::BasicPacketSocketFactory socket_factory_; + bool running_; + int port_; +}; + +class TestPortAllocator : public cricket::PortAllocator { + public: + TestPortAllocator() : port_offset_(0) {} + + virtual cricket::PortAllocatorSession* + CreateSessionInternal( + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd) { + port_offset_ += 2; + return new TestPortAllocatorSession(content_name, component, + ice_ufrag, ice_pwd, port_offset_ - 2); + } + + int port_offset_; +}; + +class TestContentDescription : public cricket::ContentDescription { + public: + explicit TestContentDescription(const std::string& gingle_content_type, + const std::string& content_type) + : gingle_content_type(gingle_content_type), + content_type(content_type) { + } + virtual ContentDescription* Copy() const { + return new TestContentDescription(*this); + } + + std::string gingle_content_type; + std::string content_type; +}; + +cricket::SessionDescription* NewTestSessionDescription( + const std::string gingle_content_type, + const std::string& content_name_a, const std::string& content_type_a, + const std::string& content_name_b, const std::string& content_type_b) { + + cricket::SessionDescription* offer = new cricket::SessionDescription(); + offer->AddContent(content_name_a, content_type_a, + new TestContentDescription(gingle_content_type, + content_type_a)); + cricket::TransportDescription desc(cricket::NS_GINGLE_P2P, + cricket::Candidates()); + offer->AddTransportInfo(cricket::TransportInfo(content_name_a, desc)); + + if (content_name_a != content_name_b) { + offer->AddContent(content_name_b, content_type_b, + new TestContentDescription(gingle_content_type, + content_type_b)); + offer->AddTransportInfo(cricket::TransportInfo(content_name_b, desc)); + } + return offer; +} + +cricket::SessionDescription* NewTestSessionDescription( + const std::string& content_name, const std::string& content_type) { + + cricket::SessionDescription* offer = new cricket::SessionDescription(); + offer->AddContent(content_name, content_type, + new TestContentDescription(content_type, + content_type)); + offer->AddTransportInfo(cricket::TransportInfo + (content_name, cricket::TransportDescription( + cricket::NS_GINGLE_P2P, + cricket::Candidates()))); + return offer; +} + +struct TestSessionClient: public cricket::SessionClient, + public sigslot::has_slots<> { + public: + TestSessionClient() { + } + + ~TestSessionClient() { + } + + virtual bool ParseContent(SignalingProtocol protocol, + const buzz::XmlElement* elem, + cricket::ContentDescription** content, + cricket::ParseError* error) { + std::string content_type; + std::string gingle_content_type; + if (protocol == PROTOCOL_GINGLE) { + gingle_content_type = elem->Name().Namespace(); + } else { + content_type = elem->Name().Namespace(); + } + + *content = new TestContentDescription(gingle_content_type, content_type); + return true; + } + + virtual bool WriteContent(SignalingProtocol protocol, + const cricket::ContentDescription* untyped_content, + buzz::XmlElement** elem, + cricket::WriteError* error) { + const TestContentDescription* content = + static_cast(untyped_content); + std::string content_type = (protocol == PROTOCOL_GINGLE ? + content->gingle_content_type : + content->content_type); + *elem = new buzz::XmlElement( + buzz::QName(content_type, "description"), true); + return true; + } + + void OnSessionCreate(cricket::Session* session, bool initiate) { + } + + void OnSessionDestroy(cricket::Session* session) { + } +}; + +struct ChannelHandler : sigslot::has_slots<> { + explicit ChannelHandler(cricket::TransportChannel* p, const std::string& name) + : channel(p), last_readable(false), last_writable(false), data_count(0), + last_size(0), name(name) { + p->SignalReadableState.connect(this, &ChannelHandler::OnReadableState); + p->SignalWritableState.connect(this, &ChannelHandler::OnWritableState); + p->SignalReadPacket.connect(this, &ChannelHandler::OnReadPacket); + } + + bool writable() const { + return last_writable && channel->writable(); + } + + bool readable() const { + return last_readable && channel->readable(); + } + + void OnReadableState(cricket::TransportChannel* p) { + EXPECT_EQ(channel, p); + last_readable = channel->readable(); + } + + void OnWritableState(cricket::TransportChannel* p) { + EXPECT_EQ(channel, p); + last_writable = channel->writable(); + } + + void OnReadPacket(cricket::TransportChannel* p, const char* buf, + size_t size, int flags) { + if (memcmp(buf, name.c_str(), name.size()) != 0) + return; // drop packet if packet doesn't belong to this channel. This + // can happen when transport channels are muxed together. + buf += name.size(); // Remove channel name from the message. + size -= name.size(); // Decrement size by channel name string size. + EXPECT_EQ(channel, p); + EXPECT_LE(size, sizeof(last_data)); + data_count += 1; + last_size = size; + std::memcpy(last_data, buf, size); + } + + void Send(const char* data, size_t size) { + std::string data_with_id(name); + data_with_id += data; + int result = channel->SendPacket(data_with_id.c_str(), data_with_id.size(), + 0); + EXPECT_EQ(static_cast(data_with_id.size()), result); + } + + cricket::TransportChannel* channel; + bool last_readable, last_writable; + int data_count; + char last_data[4096]; + size_t last_size; + std::string name; +}; + +void PrintStanza(const std::string& message, + const buzz::XmlElement* stanza) { + printf("%s: %s\n", message.c_str(), stanza->Str().c_str()); +} + +class TestClient : public sigslot::has_slots<> { + public: + // TODO: Add channel_component_a/b as inputs to the ctor. + TestClient(cricket::PortAllocator* port_allocator, + int* next_message_id, + const std::string& local_name, + SignalingProtocol start_protocol, + const std::string& content_type, + const std::string& content_name_a, + const std::string& channel_name_a, + const std::string& content_name_b, + const std::string& channel_name_b) { + Construct(port_allocator, next_message_id, local_name, start_protocol, + content_type, content_name_a, channel_name_a, + content_name_b, channel_name_b); + } + + ~TestClient() { + if (session) { + session_manager->DestroySession(session); + EXPECT_EQ(1U, session_destroyed_count); + } + delete session_manager; + delete client; + } + + void Construct(cricket::PortAllocator* pa, + int* message_id, + const std::string& lname, + SignalingProtocol protocol, + const std::string& cont_type, + const std::string& cont_name_a, + const std::string& chan_name_a, + const std::string& cont_name_b, + const std::string& chan_name_b) { + port_allocator_ = pa; + next_message_id = message_id; + local_name = lname; + start_protocol = protocol; + content_type = cont_type; + content_name_a = cont_name_a; + channel_name_a = chan_name_a; + content_name_b = cont_name_b; + channel_name_b = chan_name_b; + session_created_count = 0; + session_destroyed_count = 0; + session_remote_description_update_count = 0; + new_local_description = false; + new_remote_description = false; + last_content_action = cricket::CA_OFFER; + last_content_source = cricket::CS_LOCAL; + last_expected_sent_stanza = NULL; + session = NULL; + last_session_state = cricket::BaseSession::STATE_INIT; + chan_a = NULL; + chan_b = NULL; + blow_up_on_error = true; + error_count = 0; + + session_manager = new cricket::SessionManager(port_allocator_); + session_manager->SignalSessionCreate.connect( + this, &TestClient::OnSessionCreate); + session_manager->SignalSessionDestroy.connect( + this, &TestClient::OnSessionDestroy); + session_manager->SignalOutgoingMessage.connect( + this, &TestClient::OnOutgoingMessage); + + client = new TestSessionClient(); + session_manager->AddClient(content_type, client); + EXPECT_EQ(client, session_manager->GetClient(content_type)); + } + + uint32 sent_stanza_count() const { + return sent_stanzas.size(); + } + + const buzz::XmlElement* stanza() const { + return last_expected_sent_stanza; + } + + cricket::BaseSession::State session_state() const { + EXPECT_EQ(last_session_state, session->state()); + return session->state(); + } + + void SetSessionState(cricket::BaseSession::State state) { + session->SetState(state); + EXPECT_EQ_WAIT(last_session_state, session->state(), kEventTimeout); + } + + void CreateSession() { + session_manager->CreateSession(local_name, content_type); + } + + void DeliverStanza(const buzz::XmlElement* stanza) { + session_manager->OnIncomingMessage(stanza); + } + + void DeliverStanza(const std::string& str) { + buzz::XmlElement* stanza = buzz::XmlElement::ForStr(str); + session_manager->OnIncomingMessage(stanza); + delete stanza; + } + + void DeliverAckToLastStanza() { + const buzz::XmlElement* orig_stanza = stanza(); + const buzz::XmlElement* response_stanza = + buzz::XmlElement::ForStr(IqAck(orig_stanza->Attr(buzz::QN_IQ), "", "")); + session_manager->OnIncomingResponse(orig_stanza, response_stanza); + delete response_stanza; + } + + void ExpectSentStanza(const std::string& expected) { + EXPECT_TRUE(!sent_stanzas.empty()) << + "Found no stanza when expected " << expected; + + last_expected_sent_stanza = sent_stanzas.front(); + sent_stanzas.pop_front(); + + std::string actual = last_expected_sent_stanza->Str(); + EXPECT_EQ(expected, actual); + } + + void SkipUnsentStanza() { + GetNextOutgoingMessageID(); + } + + bool HasTransport(const std::string& content_name) const { + ASSERT(session != NULL); + const cricket::Transport* transport = session->GetTransport(content_name); + return transport != NULL && (kTransportType == transport->type()); + } + + bool HasChannel(const std::string& content_name, + int component) const { + ASSERT(session != NULL); + const cricket::TransportChannel* channel = + session->GetChannel(content_name, component); + return channel != NULL && (component == channel->component()); + } + + cricket::TransportChannel* GetChannel(const std::string& content_name, + int component) const { + ASSERT(session != NULL); + return session->GetChannel(content_name, component); + } + + void OnSessionCreate(cricket::Session* created_session, bool initiate) { + session_created_count += 1; + + session = created_session; + session->set_current_protocol(start_protocol); + session->SignalState.connect(this, &TestClient::OnSessionState); + session->SignalError.connect(this, &TestClient::OnSessionError); + session->SignalRemoteDescriptionUpdate.connect( + this, &TestClient::OnSessionRemoteDescriptionUpdate); + session->SignalNewLocalDescription.connect( + this, &TestClient::OnNewLocalDescription); + session->SignalNewRemoteDescription.connect( + this, &TestClient::OnNewRemoteDescription); + + CreateChannels(); + } + + void OnSessionDestroy(cricket::Session *session) { + session_destroyed_count += 1; + } + + void OnSessionState(cricket::BaseSession* session, + cricket::BaseSession::State state) { + // EXPECT_EQ does not allow use of this, hence the tmp variable. + cricket::BaseSession* tmp = this->session; + EXPECT_EQ(tmp, session); + last_session_state = state; + } + + void OnSessionError(cricket::BaseSession* session, + cricket::BaseSession::Error error) { + // EXPECT_EQ does not allow use of this, hence the tmp variable. + cricket::BaseSession* tmp = this->session; + EXPECT_EQ(tmp, session); + if (blow_up_on_error) { + EXPECT_TRUE(false); + } else { + error_count++; + } + } + + void OnSessionRemoteDescriptionUpdate(cricket::BaseSession* session, + const cricket::ContentInfos& contents) { + session_remote_description_update_count++; + } + + void OnNewLocalDescription(cricket::BaseSession* session, + cricket::ContentAction action) { + new_local_description = true; + last_content_action = action; + last_content_source = cricket::CS_LOCAL; + } + + void OnNewRemoteDescription(cricket::BaseSession* session, + cricket::ContentAction action) { + new_remote_description = true; + last_content_action = action; + last_content_source = cricket::CS_REMOTE; + } + + void PrepareCandidates() { + session_manager->OnSignalingReady(); + } + + void OnOutgoingMessage(cricket::SessionManager* manager, + const buzz::XmlElement* stanza) { + buzz::XmlElement* elem = new buzz::XmlElement(*stanza); + EXPECT_TRUE(elem->Name() == buzz::QN_IQ); + EXPECT_TRUE(elem->HasAttr(buzz::QN_TO)); + EXPECT_FALSE(elem->HasAttr(buzz::QN_FROM)); + EXPECT_TRUE(elem->HasAttr(buzz::QN_TYPE)); + EXPECT_TRUE((elem->Attr(buzz::QN_TYPE) == "set") || + (elem->Attr(buzz::QN_TYPE) == "result") || + (elem->Attr(buzz::QN_TYPE) == "error")); + + elem->SetAttr(buzz::QN_FROM, local_name); + if (elem->Attr(buzz::QN_TYPE) == "set") { + EXPECT_FALSE(elem->HasAttr(buzz::QN_ID)); + elem->SetAttr(buzz::QN_ID, GetNextOutgoingMessageID()); + } + + // Uncommenting this is useful for debugging. + // PrintStanza("OutgoingMessage", elem); + sent_stanzas.push_back(elem); + } + + std::string GetNextOutgoingMessageID() { + int message_id = (*next_message_id)++; + std::ostringstream ost; + ost << message_id; + return ost.str(); + } + + void CreateChannels() { + ASSERT(session != NULL); + // We either have a single content with multiple components (RTP/RTCP), or + // multiple contents with single components, but not both. + int component_a = 1; + int component_b = (content_name_a == content_name_b) ? 2 : 1; + chan_a = new ChannelHandler( + session->CreateChannel(content_name_a, channel_name_a, component_a), + channel_name_a); + chan_b = new ChannelHandler( + session->CreateChannel(content_name_b, channel_name_b, component_b), + channel_name_b); + } + + int* next_message_id; + std::string local_name; + SignalingProtocol start_protocol; + std::string content_type; + std::string content_name_a; + std::string channel_name_a; + std::string content_name_b; + std::string channel_name_b; + + uint32 session_created_count; + uint32 session_destroyed_count; + uint32 session_remote_description_update_count; + bool new_local_description; + bool new_remote_description; + cricket::ContentAction last_content_action; + cricket::ContentSource last_content_source; + std::deque sent_stanzas; + buzz::XmlElement* last_expected_sent_stanza; + + cricket::SessionManager* session_manager; + TestSessionClient* client; + cricket::PortAllocator* port_allocator_; + cricket::Session* session; + cricket::BaseSession::State last_session_state; + ChannelHandler* chan_a; + ChannelHandler* chan_b; + bool blow_up_on_error; + int error_count; +}; + +class SessionTest : public testing::Test { + protected: + virtual void SetUp() { + // Seed needed for each test to satisfy expectations. + talk_base::SetRandomTestMode(true); + } + + virtual void TearDown() { + talk_base::SetRandomTestMode(false); + } + + // Tests sending data between two clients, over two channels. + void TestSendRecv(ChannelHandler* chan1a, + ChannelHandler* chan1b, + ChannelHandler* chan2a, + ChannelHandler* chan2b) { + const char* dat1a = "spamspamspamspamspamspamspambakedbeansspam"; + const char* dat2a = "mapssnaebdekabmapsmapsmapsmapsmapsmapsmaps"; + const char* dat1b = "Lobster Thermidor a Crevette with a mornay sauce..."; + const char* dat2b = "...ecuas yanrom a htiw etteverC a rodimrehT retsboL"; + + for (int i = 0; i < 20; i++) { + chan1a->Send(dat1a, strlen(dat1a)); + chan1b->Send(dat1b, strlen(dat1b)); + chan2a->Send(dat2a, strlen(dat2a)); + chan2b->Send(dat2b, strlen(dat2b)); + + EXPECT_EQ_WAIT(i + 1, chan1a->data_count, kEventTimeout); + EXPECT_EQ_WAIT(i + 1, chan1b->data_count, kEventTimeout); + EXPECT_EQ_WAIT(i + 1, chan2a->data_count, kEventTimeout); + EXPECT_EQ_WAIT(i + 1, chan2b->data_count, kEventTimeout); + + EXPECT_EQ(strlen(dat2a), chan1a->last_size); + EXPECT_EQ(strlen(dat2b), chan1b->last_size); + EXPECT_EQ(strlen(dat1a), chan2a->last_size); + EXPECT_EQ(strlen(dat1b), chan2b->last_size); + + EXPECT_EQ(0, std::memcmp(chan1a->last_data, dat2a, + strlen(dat2a))); + EXPECT_EQ(0, std::memcmp(chan1b->last_data, dat2b, + strlen(dat2b))); + EXPECT_EQ(0, std::memcmp(chan2a->last_data, dat1a, + strlen(dat1a))); + EXPECT_EQ(0, std::memcmp(chan2b->last_data, dat1b, + strlen(dat1b))); + } + } + + // Test an initiate from one client to another, each with + // independent initial protocols. Checks for the correct initiates, + // candidates, and accept messages, and tests that working network + // channels are established. + void TestSession(SignalingProtocol initiator_protocol, + SignalingProtocol responder_protocol, + SignalingProtocol resulting_protocol, + const std::string& gingle_content_type, + const std::string& content_type, + const std::string& content_name_a, + const std::string& channel_name_a, + const std::string& content_name_b, + const std::string& channel_name_b, + const std::string& initiate_xml, + const std::string& transport_info_a_xml, + const std::string& transport_info_b_xml, + const std::string& transport_info_reply_a_xml, + const std::string& transport_info_reply_b_xml, + const std::string& accept_xml, + bool bundle = false) { + talk_base::scoped_ptr allocator( + new TestPortAllocator()); + int next_message_id = 0; + + talk_base::scoped_ptr initiator( + new TestClient(allocator.get(), &next_message_id, + kInitiator, initiator_protocol, + content_type, + content_name_a, channel_name_a, + content_name_b, channel_name_b)); + talk_base::scoped_ptr responder( + new TestClient(allocator.get(), &next_message_id, + kResponder, responder_protocol, + content_type, + content_name_a, channel_name_a, + content_name_b, channel_name_b)); + + // Create Session and check channels and state. + initiator->CreateSession(); + EXPECT_EQ(1U, initiator->session_created_count); + EXPECT_EQ(kSessionId, initiator->session->id()); + EXPECT_EQ(initiator->session->local_name(), kInitiator); + EXPECT_EQ(cricket::BaseSession::STATE_INIT, + initiator->session_state()); + + // See comment in CreateChannels about how we choose component IDs. + int component_a = 1; + int component_b = (content_name_a == content_name_b) ? 2 : 1; + EXPECT_TRUE(initiator->HasTransport(content_name_a)); + EXPECT_TRUE(initiator->HasChannel(content_name_a, component_a)); + EXPECT_TRUE(initiator->HasTransport(content_name_b)); + EXPECT_TRUE(initiator->HasChannel(content_name_b, component_b)); + + // Initiate and expect initiate message sent. + cricket::SessionDescription* offer = NewTestSessionDescription( + gingle_content_type, + content_name_a, content_type, + content_name_b, content_type); + if (bundle) { + cricket::ContentGroup group(cricket::GROUP_TYPE_BUNDLE); + group.AddContentName(content_name_a); + group.AddContentName(content_name_b); + EXPECT_TRUE(group.HasContentName(content_name_a)); + EXPECT_TRUE(group.HasContentName(content_name_b)); + offer->AddGroup(group); + } + EXPECT_TRUE(initiator->session->Initiate(kResponder, offer)); + EXPECT_EQ(initiator->session->remote_name(), kResponder); + EXPECT_EQ(initiator->session->local_description(), offer); + + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + EXPECT_EQ(cricket::BaseSession::STATE_SENTINITIATE, + initiator->session_state()); + + initiator->ExpectSentStanza( + IqSet("0", kInitiator, kResponder, initiate_xml)); + + // Deliver the initiate. Expect ack and session created with + // transports. + responder->DeliverStanza(initiator->stanza()); + responder->ExpectSentStanza( + IqAck("0", kResponder, kInitiator)); + EXPECT_EQ(0U, responder->sent_stanza_count()); + + EXPECT_EQ(1U, responder->session_created_count); + EXPECT_EQ(kSessionId, responder->session->id()); + EXPECT_EQ(responder->session->local_name(), kResponder); + EXPECT_EQ(responder->session->remote_name(), kInitiator); + EXPECT_EQ(cricket::BaseSession::STATE_RECEIVEDINITIATE, + responder->session_state()); + + EXPECT_TRUE(responder->HasTransport(content_name_a)); + EXPECT_TRUE(responder->HasChannel(content_name_a, component_a)); + EXPECT_TRUE(responder->HasTransport(content_name_b)); + EXPECT_TRUE(responder->HasChannel(content_name_b, component_b)); + + // Expect transport-info message from initiator. + // But don't send candidates until initiate ack is received. + initiator->PrepareCandidates(); + WAIT(initiator->sent_stanza_count() > 0, 100); + EXPECT_EQ(0U, initiator->sent_stanza_count()); + initiator->DeliverAckToLastStanza(); + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + initiator->ExpectSentStanza( + IqSet("1", kInitiator, kResponder, transport_info_a_xml)); + + // Deliver transport-info and expect ack. + responder->DeliverStanza(initiator->stanza()); + responder->ExpectSentStanza( + IqAck("1", kResponder, kInitiator)); + + if (!transport_info_b_xml.empty()) { + // Expect second transport-info message from initiator. + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + initiator->ExpectSentStanza( + IqSet("2", kInitiator, kResponder, transport_info_b_xml)); + EXPECT_EQ(0U, initiator->sent_stanza_count()); + + // Deliver second transport-info message and expect ack. + responder->DeliverStanza(initiator->stanza()); + responder->ExpectSentStanza( + IqAck("2", kResponder, kInitiator)); + } else { + EXPECT_EQ(0U, initiator->sent_stanza_count()); + EXPECT_EQ(0U, responder->sent_stanza_count()); + initiator->SkipUnsentStanza(); + } + + // Expect reply transport-info message from responder. + responder->PrepareCandidates(); + EXPECT_TRUE_WAIT(responder->sent_stanza_count() > 0, kEventTimeout); + responder->ExpectSentStanza( + IqSet("3", kResponder, kInitiator, transport_info_reply_a_xml)); + + // Deliver reply transport-info and expect ack. + initiator->DeliverStanza(responder->stanza()); + initiator->ExpectSentStanza( + IqAck("3", kInitiator, kResponder)); + + if (!transport_info_reply_b_xml.empty()) { + // Expect second reply transport-info message from responder. + EXPECT_TRUE_WAIT(responder->sent_stanza_count() > 0, kEventTimeout); + responder->ExpectSentStanza( + IqSet("4", kResponder, kInitiator, transport_info_reply_b_xml)); + EXPECT_EQ(0U, responder->sent_stanza_count()); + + // Deliver second reply transport-info message and expect ack. + initiator->DeliverStanza(responder->stanza()); + initiator->ExpectSentStanza( + IqAck("4", kInitiator, kResponder)); + EXPECT_EQ(0U, initiator->sent_stanza_count()); + } else { + EXPECT_EQ(0U, initiator->sent_stanza_count()); + EXPECT_EQ(0U, responder->sent_stanza_count()); + responder->SkipUnsentStanza(); + } + + // The channels should be able to become writable at this point. This + // requires pinging, so it may take a little while. + EXPECT_TRUE_WAIT(initiator->chan_a->writable() && + initiator->chan_a->readable(), kEventTimeout); + EXPECT_TRUE_WAIT(initiator->chan_b->writable() && + initiator->chan_b->readable(), kEventTimeout); + EXPECT_TRUE_WAIT(responder->chan_a->writable() && + responder->chan_a->readable(), kEventTimeout); + EXPECT_TRUE_WAIT(responder->chan_b->writable() && + responder->chan_b->readable(), kEventTimeout); + + // Accept the session and expect accept stanza. + cricket::SessionDescription* answer = NewTestSessionDescription( + gingle_content_type, + content_name_a, content_type, + content_name_b, content_type); + if (bundle) { + cricket::ContentGroup group(cricket::GROUP_TYPE_BUNDLE); + group.AddContentName(content_name_a); + group.AddContentName(content_name_b); + EXPECT_TRUE(group.HasContentName(content_name_a)); + EXPECT_TRUE(group.HasContentName(content_name_b)); + answer->AddGroup(group); + } + EXPECT_TRUE(responder->session->Accept(answer)); + EXPECT_EQ(responder->session->local_description(), answer); + + responder->ExpectSentStanza( + IqSet("5", kResponder, kInitiator, accept_xml)); + + EXPECT_EQ(0U, responder->sent_stanza_count()); + + // Deliver the accept message and expect an ack. + initiator->DeliverStanza(responder->stanza()); + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + initiator->ExpectSentStanza( + IqAck("5", kInitiator, kResponder)); + EXPECT_EQ(0U, initiator->sent_stanza_count()); + + // Both sessions should be in progress and have functioning + // channels. + EXPECT_EQ(resulting_protocol, initiator->session->current_protocol()); + EXPECT_EQ(resulting_protocol, responder->session->current_protocol()); + EXPECT_EQ_WAIT(cricket::BaseSession::STATE_INPROGRESS, + initiator->session_state(), kEventTimeout); + EXPECT_EQ_WAIT(cricket::BaseSession::STATE_INPROGRESS, + responder->session_state(), kEventTimeout); + if (bundle) { + cricket::TransportChannel* initiator_chan_a = initiator->chan_a->channel; + cricket::TransportChannel* initiator_chan_b = initiator->chan_b->channel; + + // Since we know these are TransportChannelProxy, type cast it. + cricket::TransportChannelProxy* initiator_proxy_chan_a = + static_cast(initiator_chan_a); + cricket::TransportChannelProxy* initiator_proxy_chan_b = + static_cast(initiator_chan_b); + EXPECT_TRUE(initiator_proxy_chan_a->impl() != NULL); + EXPECT_TRUE(initiator_proxy_chan_b->impl() != NULL); + EXPECT_EQ(initiator_proxy_chan_a->impl(), initiator_proxy_chan_b->impl()); + + cricket::TransportChannel* responder_chan_a = responder->chan_a->channel; + cricket::TransportChannel* responder_chan_b = responder->chan_b->channel; + + // Since we know these are TransportChannelProxy, type cast it. + cricket::TransportChannelProxy* responder_proxy_chan_a = + static_cast(responder_chan_a); + cricket::TransportChannelProxy* responder_proxy_chan_b = + static_cast(responder_chan_b); + EXPECT_TRUE(responder_proxy_chan_a->impl() != NULL); + EXPECT_TRUE(responder_proxy_chan_b->impl() != NULL); + EXPECT_EQ(responder_proxy_chan_a->impl(), responder_proxy_chan_b->impl()); + } + TestSendRecv(initiator->chan_a, initiator->chan_b, + responder->chan_a, responder->chan_b); + + if (resulting_protocol == PROTOCOL_JINGLE) { + // Deliver a description-info message to the initiator and check if the + // content description changes. + EXPECT_EQ(0U, initiator->session_remote_description_update_count); + + const cricket::SessionDescription* old_session_desc = + initiator->session->remote_description(); + const cricket::ContentInfo* old_content_a = + old_session_desc->GetContentByName(content_name_a); + const cricket::ContentDescription* old_content_desc_a = + old_content_a->description; + const cricket::ContentInfo* old_content_b = + old_session_desc->GetContentByName(content_name_b); + const cricket::ContentDescription* old_content_desc_b = + old_content_b->description; + EXPECT_TRUE(old_content_desc_a != NULL); + EXPECT_TRUE(old_content_desc_b != NULL); + + LOG(LS_INFO) << "A " << old_content_a->name; + LOG(LS_INFO) << "B " << old_content_b->name; + + std::string description_info_xml = + JingleDescriptionInfoXml(content_name_a, content_type); + initiator->DeliverStanza( + IqSet("6", kResponder, kInitiator, description_info_xml)); + responder->SkipUnsentStanza(); + EXPECT_EQ(1U, initiator->session_remote_description_update_count); + + const cricket::SessionDescription* new_session_desc = + initiator->session->remote_description(); + const cricket::ContentInfo* new_content_a = + new_session_desc->GetContentByName(content_name_a); + const cricket::ContentDescription* new_content_desc_a = + new_content_a->description; + const cricket::ContentInfo* new_content_b = + new_session_desc->GetContentByName(content_name_b); + const cricket::ContentDescription* new_content_desc_b = + new_content_b->description; + EXPECT_TRUE(new_content_desc_a != NULL); + EXPECT_TRUE(new_content_desc_b != NULL); + + // TODO: We used to replace contents from an update, but + // that no longer works with partial updates. We need to figure out + // a way to merge patial updates into contents. For now, users of + // Session should listen to SignalRemoteDescriptionUpdate and handle + // updates. They should not expect remote_description to be the + // latest value. + // See session.cc OnDescriptionInfoMessage. + + // EXPECT_NE(old_content_desc_a, new_content_desc_a); + + // if (content_name_a != content_name_b) { + // // If content_name_a != content_name_b, then b's content description + // // should not have changed since the description-info message only + // // contained an update for content_name_a. + // EXPECT_EQ(old_content_desc_b, new_content_desc_b); + // } + + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + initiator->ExpectSentStanza( + IqAck("6", kInitiator, kResponder)); + EXPECT_EQ(0U, initiator->sent_stanza_count()); + } else { + responder->SkipUnsentStanza(); + } + + initiator->session->Terminate(); + initiator->ExpectSentStanza( + IqSet("7", kInitiator, kResponder, + TerminateXml(resulting_protocol, + cricket::STR_TERMINATE_SUCCESS))); + + responder->DeliverStanza(initiator->stanza()); + responder->ExpectSentStanza( + IqAck("7", kResponder, kInitiator)); + EXPECT_EQ(cricket::BaseSession::STATE_SENTTERMINATE, + initiator->session_state()); + EXPECT_EQ(cricket::BaseSession::STATE_RECEIVEDTERMINATE, + responder->session_state()); + } + + // Test an initiate with other content, called "main". + void TestOtherContent(SignalingProtocol initiator_protocol, + SignalingProtocol responder_protocol, + SignalingProtocol resulting_protocol) { + std::string content_name = "main"; + std::string content_type = "http://oink.splat/session"; + std::string content_name_a = content_name; + std::string channel_name_a = "rtp"; + std::string content_name_b = content_name; + std::string channel_name_b = "rtcp"; + std::string initiate_xml = InitiateXml( + initiator_protocol, + content_name_a, content_type); + std::string transport_info_a_xml = TransportInfo4Xml( + initiator_protocol, content_name, + channel_name_a, 0, 1, + channel_name_b, 2, 3); + std::string transport_info_b_xml = ""; + std::string transport_info_reply_a_xml = TransportInfo4Xml( + resulting_protocol, content_name, + channel_name_a, 4, 5, + channel_name_b, 6, 7); + std::string transport_info_reply_b_xml = ""; + std::string accept_xml = AcceptXml( + resulting_protocol, + content_name_a, content_type); + + + TestSession(initiator_protocol, responder_protocol, resulting_protocol, + content_type, + content_type, + content_name_a, channel_name_a, + content_name_b, channel_name_b, + initiate_xml, + transport_info_a_xml, transport_info_b_xml, + transport_info_reply_a_xml, transport_info_reply_b_xml, + accept_xml); + } + + // Test an initiate with audio content. + void TestAudioContent(SignalingProtocol initiator_protocol, + SignalingProtocol responder_protocol, + SignalingProtocol resulting_protocol) { + std::string gingle_content_type = cricket::NS_GINGLE_AUDIO; + std::string content_name = cricket::CN_AUDIO; + std::string content_type = cricket::NS_JINGLE_RTP; + std::string channel_name_a = "rtp"; + std::string channel_name_b = "rtcp"; + std::string initiate_xml = InitiateXml( + initiator_protocol, + gingle_content_type, + content_name, content_type, + "", ""); + std::string transport_info_a_xml = TransportInfo4Xml( + initiator_protocol, content_name, + channel_name_a, 0, 1, + channel_name_b, 2, 3); + std::string transport_info_b_xml = ""; + std::string transport_info_reply_a_xml = TransportInfo4Xml( + resulting_protocol, content_name, + channel_name_a, 4, 5, + channel_name_b, 6, 7); + std::string transport_info_reply_b_xml = ""; + std::string accept_xml = AcceptXml( + resulting_protocol, + gingle_content_type, + content_name, content_type, + "", ""); + + + TestSession(initiator_protocol, responder_protocol, resulting_protocol, + gingle_content_type, + content_type, + content_name, channel_name_a, + content_name, channel_name_b, + initiate_xml, + transport_info_a_xml, transport_info_b_xml, + transport_info_reply_a_xml, transport_info_reply_b_xml, + accept_xml); + } + + // Since media content is "split" into two contents (audio and + // video), we need to treat it special. + void TestVideoContents(SignalingProtocol initiator_protocol, + SignalingProtocol responder_protocol, + SignalingProtocol resulting_protocol) { + std::string content_type = cricket::NS_JINGLE_RTP; + std::string gingle_content_type = cricket::NS_GINGLE_VIDEO; + std::string content_name_a = cricket::CN_AUDIO; + std::string channel_name_a = "rtp"; + std::string content_name_b = cricket::CN_VIDEO; + std::string channel_name_b = "video_rtp"; + + std::string initiate_xml = InitiateXml( + initiator_protocol, + gingle_content_type, + content_name_a, content_type, + content_name_b, content_type); + std::string transport_info_a_xml = TransportInfo2Xml( + initiator_protocol, content_name_a, + channel_name_a, 0, 1); + std::string transport_info_b_xml = TransportInfo2Xml( + initiator_protocol, content_name_b, + channel_name_b, 2, 3); + std::string transport_info_reply_a_xml = TransportInfo2Xml( + resulting_protocol, content_name_a, + channel_name_a, 4, 5); + std::string transport_info_reply_b_xml = TransportInfo2Xml( + resulting_protocol, content_name_b, + channel_name_b, 6, 7); + std::string accept_xml = AcceptXml( + resulting_protocol, + gingle_content_type, + content_name_a, content_type, + content_name_b, content_type); + + TestSession(initiator_protocol, responder_protocol, resulting_protocol, + gingle_content_type, + content_type, + content_name_a, channel_name_a, + content_name_b, channel_name_b, + initiate_xml, + transport_info_a_xml, transport_info_b_xml, + transport_info_reply_a_xml, transport_info_reply_b_xml, + accept_xml); + } + + void TestBadRedirect(SignalingProtocol protocol) { + std::string content_name = "main"; + std::string content_type = "http://oink.splat/session"; + std::string channel_name_a = "chana"; + std::string channel_name_b = "chanb"; + std::string initiate_xml = InitiateXml( + protocol, content_name, content_type); + std::string transport_info_xml = TransportInfo4Xml( + protocol, content_name, + channel_name_a, 0, 1, + channel_name_b, 2, 3); + std::string transport_info_reply_xml = TransportInfo4Xml( + protocol, content_name, + channel_name_a, 4, 5, + channel_name_b, 6, 7); + std::string accept_xml = AcceptXml( + protocol, content_name, content_type); + std::string responder_full = kResponder + "/full"; + + talk_base::scoped_ptr allocator( + new TestPortAllocator()); + int next_message_id = 0; + + talk_base::scoped_ptr initiator( + new TestClient(allocator.get(), &next_message_id, + kInitiator, protocol, + content_type, + content_name, channel_name_a, + content_name, channel_name_b)); + + talk_base::scoped_ptr responder( + new TestClient(allocator.get(), &next_message_id, + responder_full, protocol, + content_type, + content_name, channel_name_a, + content_name, channel_name_b)); + + // Create Session and check channels and state. + initiator->CreateSession(); + EXPECT_EQ(1U, initiator->session_created_count); + EXPECT_EQ(kSessionId, initiator->session->id()); + EXPECT_EQ(initiator->session->local_name(), kInitiator); + EXPECT_EQ(cricket::BaseSession::STATE_INIT, + initiator->session_state()); + + EXPECT_TRUE(initiator->HasChannel(content_name, 1)); + EXPECT_TRUE(initiator->HasChannel(content_name, 2)); + + // Initiate and expect initiate message sent. + cricket::SessionDescription* offer = NewTestSessionDescription( + content_name, content_type); + EXPECT_TRUE(initiator->session->Initiate(kResponder, offer)); + EXPECT_EQ(initiator->session->remote_name(), kResponder); + EXPECT_EQ(initiator->session->local_description(), offer); + + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + EXPECT_EQ(cricket::BaseSession::STATE_SENTINITIATE, + initiator->session_state()); + initiator->ExpectSentStanza( + IqSet("0", kInitiator, kResponder, initiate_xml)); + + // Expect transport-info message from initiator. + initiator->DeliverAckToLastStanza(); + initiator->PrepareCandidates(); + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + initiator->ExpectSentStanza( + IqSet("1", kInitiator, kResponder, transport_info_xml)); + + // Send an unauthorized redirect to the initiator and expect it be ignored. + initiator->blow_up_on_error = false; + const buzz::XmlElement* initiate_stanza = initiator->stanza(); + talk_base::scoped_ptr redirect_stanza( + buzz::XmlElement::ForStr( + IqError("ER", kResponder, kInitiator, + RedirectXml(protocol, initiate_xml, "not@allowed.com")))); + initiator->session_manager->OnFailedSend( + initiate_stanza, redirect_stanza.get()); + EXPECT_EQ(initiator->session->remote_name(), kResponder); + initiator->blow_up_on_error = true; + EXPECT_EQ(initiator->error_count, 1); + } + + void TestGoodRedirect(SignalingProtocol protocol) { + std::string content_name = "main"; + std::string content_type = "http://oink.splat/session"; + std::string channel_name_a = "chana"; + std::string channel_name_b = "chanb"; + std::string initiate_xml = InitiateXml( + protocol, content_name, content_type); + std::string transport_info_xml = TransportInfo4Xml( + protocol, content_name, + channel_name_a, 0, 1, + channel_name_b, 2, 3); + std::string transport_info_reply_xml = TransportInfo4Xml( + protocol, content_name, + channel_name_a, 4, 5, + channel_name_b, 6, 7); + std::string accept_xml = AcceptXml( + protocol, content_name, content_type); + std::string responder_full = kResponder + "/full"; + + talk_base::scoped_ptr allocator( + new TestPortAllocator()); + int next_message_id = 0; + + talk_base::scoped_ptr initiator( + new TestClient(allocator.get(), &next_message_id, + kInitiator, protocol, + content_type, + content_name, channel_name_a, + content_name, channel_name_b)); + + talk_base::scoped_ptr responder( + new TestClient(allocator.get(), &next_message_id, + responder_full, protocol, + content_type, + content_name, channel_name_a, + content_name, channel_name_b)); + + // Create Session and check channels and state. + initiator->CreateSession(); + EXPECT_EQ(1U, initiator->session_created_count); + EXPECT_EQ(kSessionId, initiator->session->id()); + EXPECT_EQ(initiator->session->local_name(), kInitiator); + EXPECT_EQ(cricket::BaseSession::STATE_INIT, + initiator->session_state()); + + EXPECT_TRUE(initiator->HasChannel(content_name, 1)); + EXPECT_TRUE(initiator->HasChannel(content_name, 2)); + + // Initiate and expect initiate message sent. + cricket::SessionDescription* offer = NewTestSessionDescription( + content_name, content_type); + EXPECT_TRUE(initiator->session->Initiate(kResponder, offer)); + EXPECT_EQ(initiator->session->remote_name(), kResponder); + EXPECT_EQ(initiator->session->local_description(), offer); + + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + EXPECT_EQ(cricket::BaseSession::STATE_SENTINITIATE, + initiator->session_state()); + initiator->ExpectSentStanza( + IqSet("0", kInitiator, kResponder, initiate_xml)); + + // Expect transport-info message from initiator. + initiator->DeliverAckToLastStanza(); + initiator->PrepareCandidates(); + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + initiator->ExpectSentStanza( + IqSet("1", kInitiator, kResponder, transport_info_xml)); + + // Send a redirect to the initiator and expect all of the message + // to be resent. + const buzz::XmlElement* initiate_stanza = initiator->stanza(); + talk_base::scoped_ptr redirect_stanza( + buzz::XmlElement::ForStr( + IqError("ER2", kResponder, kInitiator, + RedirectXml(protocol, initiate_xml, responder_full)))); + initiator->session_manager->OnFailedSend( + initiate_stanza, redirect_stanza.get()); + EXPECT_EQ(initiator->session->remote_name(), responder_full); + + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + initiator->ExpectSentStanza( + IqSet("2", kInitiator, responder_full, initiate_xml)); + initiator->ExpectSentStanza( + IqSet("3", kInitiator, responder_full, transport_info_xml)); + + // Deliver the initiate. Expect ack and session created with + // transports. + responder->DeliverStanza( + IqSet("2", kInitiator, responder_full, initiate_xml)); + responder->ExpectSentStanza( + IqAck("2", responder_full, kInitiator)); + EXPECT_EQ(0U, responder->sent_stanza_count()); + + EXPECT_EQ(1U, responder->session_created_count); + EXPECT_EQ(kSessionId, responder->session->id()); + EXPECT_EQ(responder->session->local_name(), responder_full); + EXPECT_EQ(responder->session->remote_name(), kInitiator); + EXPECT_EQ(cricket::BaseSession::STATE_RECEIVEDINITIATE, + responder->session_state()); + + EXPECT_TRUE(responder->HasChannel(content_name, 1)); + EXPECT_TRUE(responder->HasChannel(content_name, 2)); + + // Deliver transport-info and expect ack. + responder->DeliverStanza( + IqSet("3", kInitiator, responder_full, transport_info_xml)); + responder->ExpectSentStanza( + IqAck("3", responder_full, kInitiator)); + + // Expect reply transport-infos sent to new remote JID + responder->PrepareCandidates(); + EXPECT_TRUE_WAIT(responder->sent_stanza_count() > 0, kEventTimeout); + responder->ExpectSentStanza( + IqSet("4", responder_full, kInitiator, transport_info_reply_xml)); + + initiator->DeliverStanza(responder->stanza()); + initiator->ExpectSentStanza( + IqAck("4", kInitiator, responder_full)); + + // The channels should be able to become writable at this point. This + // requires pinging, so it may take a little while. + EXPECT_TRUE_WAIT(initiator->chan_a->writable() && + initiator->chan_a->readable(), kEventTimeout); + EXPECT_TRUE_WAIT(initiator->chan_b->writable() && + initiator->chan_b->readable(), kEventTimeout); + EXPECT_TRUE_WAIT(responder->chan_a->writable() && + responder->chan_a->readable(), kEventTimeout); + EXPECT_TRUE_WAIT(responder->chan_b->writable() && + responder->chan_b->readable(), kEventTimeout); + + // Accept the session and expect accept stanza. + cricket::SessionDescription* answer = NewTestSessionDescription( + content_name, content_type); + EXPECT_TRUE(responder->session->Accept(answer)); + EXPECT_EQ(responder->session->local_description(), answer); + + responder->ExpectSentStanza( + IqSet("5", responder_full, kInitiator, accept_xml)); + EXPECT_EQ(0U, responder->sent_stanza_count()); + + // Deliver the accept message and expect an ack. + initiator->DeliverStanza(responder->stanza()); + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + initiator->ExpectSentStanza( + IqAck("5", kInitiator, responder_full)); + EXPECT_EQ(0U, initiator->sent_stanza_count()); + + // Both sessions should be in progress and have functioning + // channels. + EXPECT_EQ_WAIT(cricket::BaseSession::STATE_INPROGRESS, + initiator->session_state(), kEventTimeout); + EXPECT_EQ_WAIT(cricket::BaseSession::STATE_INPROGRESS, + responder->session_state(), kEventTimeout); + TestSendRecv(initiator->chan_a, initiator->chan_b, + responder->chan_a, responder->chan_b); + } + + void TestCandidatesInInitiateAndAccept(const std::string& test_name) { + std::string content_name = "main"; + std::string content_type = "http://oink.splat/session"; + std::string channel_name_a = "rtp"; + std::string channel_name_b = "rtcp"; + cricket::SignalingProtocol protocol = PROTOCOL_JINGLE; + + talk_base::scoped_ptr allocator( + new TestPortAllocator()); + int next_message_id = 0; + + talk_base::scoped_ptr initiator( + new TestClient(allocator.get(), &next_message_id, + kInitiator, protocol, + content_type, + content_name, channel_name_a, + content_name, channel_name_b)); + + talk_base::scoped_ptr responder( + new TestClient(allocator.get(), &next_message_id, + kResponder, protocol, + content_type, + content_name, channel_name_a, + content_name, channel_name_b)); + + // Create Session and check channels and state. + initiator->CreateSession(); + EXPECT_TRUE(initiator->HasTransport(content_name)); + EXPECT_TRUE(initiator->HasChannel(content_name, 1)); + EXPECT_TRUE(initiator->HasTransport(content_name)); + EXPECT_TRUE(initiator->HasChannel(content_name, 2)); + + // Initiate and expect initiate message sent. + cricket::SessionDescription* offer = NewTestSessionDescription( + content_name, content_type); + EXPECT_TRUE(initiator->session->Initiate(kResponder, offer)); + + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + EXPECT_EQ(cricket::BaseSession::STATE_SENTINITIATE, + initiator->session_state()); + initiator->ExpectSentStanza( + IqSet("0", kInitiator, kResponder, + InitiateXml(protocol, content_name, content_type))); + + // Fake the delivery the initiate and candidates together. + responder->DeliverStanza( + IqSet("A", kInitiator, kResponder, + JingleInitiateActionXml( + JingleContentXml( + content_name, content_type, kTransportType, + P2pCandidateXml(channel_name_a, 0) + + P2pCandidateXml(channel_name_a, 1) + + P2pCandidateXml(channel_name_b, 2) + + P2pCandidateXml(channel_name_b, 3))))); + responder->ExpectSentStanza( + IqAck("A", kResponder, kInitiator)); + EXPECT_EQ(0U, responder->sent_stanza_count()); + + EXPECT_EQ(1U, responder->session_created_count); + EXPECT_EQ(kSessionId, responder->session->id()); + EXPECT_EQ(responder->session->local_name(), kResponder); + EXPECT_EQ(responder->session->remote_name(), kInitiator); + EXPECT_EQ(cricket::BaseSession::STATE_RECEIVEDINITIATE, + responder->session_state()); + + EXPECT_TRUE(responder->HasTransport(content_name)); + EXPECT_TRUE(responder->HasChannel(content_name, 1)); + EXPECT_TRUE(responder->HasTransport(content_name)); + EXPECT_TRUE(responder->HasChannel(content_name, 2)); + + // Expect transport-info message from initiator. + // But don't send candidates until initiate ack is received. + initiator->DeliverAckToLastStanza(); + initiator->PrepareCandidates(); + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + initiator->ExpectSentStanza( + IqSet("1", kInitiator, kResponder, + TransportInfo4Xml(protocol, content_name, + channel_name_a, 0, 1, + channel_name_b, 2, 3))); + + responder->PrepareCandidates(); + EXPECT_TRUE_WAIT(responder->sent_stanza_count() > 0, kEventTimeout); + responder->ExpectSentStanza( + IqSet("2", kResponder, kInitiator, + TransportInfo4Xml(protocol, content_name, + channel_name_a, 4, 5, + channel_name_b, 6, 7))); + + // Accept the session and expect accept stanza. + cricket::SessionDescription* answer = NewTestSessionDescription( + content_name, content_type); + EXPECT_TRUE(responder->session->Accept(answer)); + + responder->ExpectSentStanza( + IqSet("3", kResponder, kInitiator, + AcceptXml(protocol, content_name, content_type))); + EXPECT_EQ(0U, responder->sent_stanza_count()); + + // Fake the delivery the accept and candidates together. + initiator->DeliverStanza( + IqSet("B", kResponder, kInitiator, + JingleActionXml("session-accept", + JingleContentXml( + content_name, content_type, kTransportType, + P2pCandidateXml(channel_name_a, 4) + + P2pCandidateXml(channel_name_a, 5) + + P2pCandidateXml(channel_name_b, 6) + + P2pCandidateXml(channel_name_b, 7))))); + EXPECT_TRUE_WAIT(initiator->sent_stanza_count() > 0, kEventTimeout); + initiator->ExpectSentStanza( + IqAck("B", kInitiator, kResponder)); + EXPECT_EQ(0U, initiator->sent_stanza_count()); + + // The channels should be able to become writable at this point. This + // requires pinging, so it may take a little while. + EXPECT_TRUE_WAIT(initiator->chan_a->writable() && + initiator->chan_a->readable(), kEventTimeout); + EXPECT_TRUE_WAIT(initiator->chan_b->writable() && + initiator->chan_b->readable(), kEventTimeout); + EXPECT_TRUE_WAIT(responder->chan_a->writable() && + responder->chan_a->readable(), kEventTimeout); + EXPECT_TRUE_WAIT(responder->chan_b->writable() && + responder->chan_b->readable(), kEventTimeout); + + + // Both sessions should be in progress and have functioning + // channels. + EXPECT_EQ(protocol, initiator->session->current_protocol()); + EXPECT_EQ(protocol, responder->session->current_protocol()); + EXPECT_EQ_WAIT(cricket::BaseSession::STATE_INPROGRESS, + initiator->session_state(), kEventTimeout); + EXPECT_EQ_WAIT(cricket::BaseSession::STATE_INPROGRESS, + responder->session_state(), kEventTimeout); + TestSendRecv(initiator->chan_a, initiator->chan_b, + responder->chan_a, responder->chan_b); + } + + // Tests that when an initiator terminates right after initiate, + // everything behaves correctly. + void TestEarlyTerminationFromInitiator(SignalingProtocol protocol) { + std::string content_name = "main"; + std::string content_type = "http://oink.splat/session"; + + talk_base::scoped_ptr allocator( + new TestPortAllocator()); + int next_message_id = 0; + + talk_base::scoped_ptr initiator( + new TestClient(allocator.get(), &next_message_id, + kInitiator, protocol, + content_type, + content_name, "a", + content_name, "b")); + + talk_base::scoped_ptr responder( + new TestClient(allocator.get(), &next_message_id, + kResponder, protocol, + content_type, + content_name, "a", + content_name, "b")); + + // Send initiate + initiator->CreateSession(); + EXPECT_TRUE(initiator->session->Initiate( + kResponder, NewTestSessionDescription(content_name, content_type))); + initiator->ExpectSentStanza( + IqSet("0", kInitiator, kResponder, + InitiateXml(protocol, content_name, content_type))); + EXPECT_EQ(cricket::BaseSession::STATE_SENTINITIATE, + initiator->session_state()); + + responder->DeliverStanza(initiator->stanza()); + responder->ExpectSentStanza( + IqAck("0", kResponder, kInitiator)); + EXPECT_EQ(cricket::BaseSession::STATE_RECEIVEDINITIATE, + responder->session_state()); + + initiator->session->TerminateWithReason(cricket::STR_TERMINATE_ERROR); + initiator->ExpectSentStanza( + IqSet("1", kInitiator, kResponder, + TerminateXml(protocol, cricket::STR_TERMINATE_ERROR))); + EXPECT_EQ(cricket::BaseSession::STATE_SENTTERMINATE, + initiator->session_state()); + + responder->DeliverStanza(initiator->stanza()); + responder->ExpectSentStanza( + IqAck("1", kResponder, kInitiator)); + EXPECT_EQ(cricket::BaseSession::STATE_RECEIVEDTERMINATE, + responder->session_state()); + } + + // Tests that when the responder rejects, everything behaves + // correctly. + void TestRejection(SignalingProtocol protocol) { + std::string content_name = "main"; + std::string content_type = "http://oink.splat/session"; + + talk_base::scoped_ptr allocator( + new TestPortAllocator()); + int next_message_id = 0; + + talk_base::scoped_ptr initiator( + new TestClient(allocator.get(), &next_message_id, + kInitiator, protocol, + content_type, + content_name, "a", + content_name, "b")); + + // Send initiate + initiator->CreateSession(); + EXPECT_TRUE(initiator->session->Initiate( + kResponder, NewTestSessionDescription(content_name, content_type))); + initiator->ExpectSentStanza( + IqSet("0", kInitiator, kResponder, + InitiateXml(protocol, content_name, content_type))); + EXPECT_EQ(cricket::BaseSession::STATE_SENTINITIATE, + initiator->session_state()); + + initiator->DeliverStanza( + IqSet("1", kResponder, kInitiator, + RejectXml(protocol, cricket::STR_TERMINATE_ERROR))); + initiator->ExpectSentStanza( + IqAck("1", kInitiator, kResponder)); + if (protocol == PROTOCOL_JINGLE) { + EXPECT_EQ(cricket::BaseSession::STATE_RECEIVEDTERMINATE, + initiator->session_state()); + } else { + EXPECT_EQ(cricket::BaseSession::STATE_RECEIVEDREJECT, + initiator->session_state()); + } + } + + void TestTransportMux() { + SignalingProtocol initiator_protocol = PROTOCOL_JINGLE; + SignalingProtocol responder_protocol = PROTOCOL_JINGLE; + SignalingProtocol resulting_protocol = PROTOCOL_JINGLE; + std::string content_type = cricket::NS_JINGLE_RTP; + std::string gingle_content_type = cricket::NS_GINGLE_VIDEO; + std::string content_name_a = cricket::CN_AUDIO; + std::string channel_name_a = "rtp"; + std::string content_name_b = cricket::CN_VIDEO; + std::string channel_name_b = "video_rtp"; + + std::string initiate_xml = InitiateXml( + initiator_protocol, + gingle_content_type, + content_name_a, content_type, + content_name_b, content_type, true); + std::string transport_info_a_xml = TransportInfo2Xml( + initiator_protocol, content_name_a, + channel_name_a, 0, 1); + std::string transport_info_b_xml = TransportInfo2Xml( + initiator_protocol, content_name_b, + channel_name_b, 2, 3); + std::string transport_info_reply_a_xml = TransportInfo2Xml( + resulting_protocol, content_name_a, + channel_name_a, 4, 5); + std::string transport_info_reply_b_xml = TransportInfo2Xml( + resulting_protocol, content_name_b, + channel_name_b, 6, 7); + std::string accept_xml = AcceptXml( + resulting_protocol, + gingle_content_type, + content_name_a, content_type, + content_name_b, content_type, true); + + TestSession(initiator_protocol, responder_protocol, resulting_protocol, + gingle_content_type, + content_type, + content_name_a, channel_name_a, + content_name_b, channel_name_b, + initiate_xml, + transport_info_a_xml, transport_info_b_xml, + transport_info_reply_a_xml, transport_info_reply_b_xml, + accept_xml, + true); + } + + void TestSendDescriptionInfo() { + talk_base::scoped_ptr allocator( + new TestPortAllocator()); + int next_message_id = 0; + + std::string content_name = "content-name"; + std::string content_type = "content-type"; + talk_base::scoped_ptr initiator( + new TestClient(allocator.get(), &next_message_id, + kInitiator, PROTOCOL_JINGLE, + content_type, + content_name, "", + "", "")); + + initiator->CreateSession(); + cricket::SessionDescription* offer = NewTestSessionDescription( + content_name, content_type); + std::string initiate_xml = InitiateXml( + PROTOCOL_JINGLE, content_name, content_type); + + cricket::ContentInfos contents; + TestContentDescription content(content_type, content_type); + contents.push_back( + cricket::ContentInfo(content_name, content_type, &content)); + std::string description_info_xml = JingleDescriptionInfoXml( + content_name, content_type); + + EXPECT_TRUE(initiator->session->Initiate(kResponder, offer)); + initiator->ExpectSentStanza( + IqSet("0", kInitiator, kResponder, initiate_xml)); + + EXPECT_TRUE(initiator->session->SendDescriptionInfoMessage(contents)); + initiator->ExpectSentStanza( + IqSet("1", kInitiator, kResponder, description_info_xml)); + } + + void DoTestSignalNewDescription( + TestClient* client, + cricket::BaseSession::State state, + cricket::ContentAction expected_content_action, + cricket::ContentSource expected_content_source) { + // Clean up before the new test. + client->new_local_description = false; + client->new_remote_description = false; + + client->SetSessionState(state); + EXPECT_EQ((expected_content_source == cricket::CS_LOCAL), + client->new_local_description); + EXPECT_EQ((expected_content_source == cricket::CS_REMOTE), + client->new_remote_description); + EXPECT_EQ(expected_content_action, client->last_content_action); + EXPECT_EQ(expected_content_source, client->last_content_source); + } + + void TestCallerSignalNewDescription() { + talk_base::scoped_ptr allocator( + new TestPortAllocator()); + int next_message_id = 0; + + std::string content_name = "content-name"; + std::string content_type = "content-type"; + talk_base::scoped_ptr initiator( + new TestClient(allocator.get(), &next_message_id, + kInitiator, PROTOCOL_JINGLE, + content_type, + content_name, "", + "", "")); + + initiator->CreateSession(); + + // send offer -> send update offer -> + // receive pr answer -> receive update pr answer -> + // receive answer + DoTestSignalNewDescription( + initiator.get(), cricket::BaseSession::STATE_SENTINITIATE, + cricket::CA_OFFER, cricket::CS_LOCAL); + + DoTestSignalNewDescription( + initiator.get(), cricket::BaseSession::STATE_SENTINITIATE, + cricket::CA_OFFER, cricket::CS_LOCAL); + + DoTestSignalNewDescription( + initiator.get(), cricket::BaseSession::STATE_RECEIVEDPRACCEPT, + cricket::CA_PRANSWER, cricket::CS_REMOTE); + + DoTestSignalNewDescription( + initiator.get(), cricket::BaseSession::STATE_RECEIVEDPRACCEPT, + cricket::CA_PRANSWER, cricket::CS_REMOTE); + + DoTestSignalNewDescription( + initiator.get(), cricket::BaseSession::STATE_RECEIVEDACCEPT, + cricket::CA_ANSWER, cricket::CS_REMOTE); + } + + void TestCalleeSignalNewDescription() { + talk_base::scoped_ptr allocator( + new TestPortAllocator()); + int next_message_id = 0; + + std::string content_name = "content-name"; + std::string content_type = "content-type"; + talk_base::scoped_ptr initiator( + new TestClient(allocator.get(), &next_message_id, + kInitiator, PROTOCOL_JINGLE, + content_type, + content_name, "", + "", "")); + + initiator->CreateSession(); + + // receive offer -> receive update offer -> + // send pr answer -> send update pr answer -> + // send answer + DoTestSignalNewDescription( + initiator.get(), cricket::BaseSession::STATE_RECEIVEDINITIATE, + cricket::CA_OFFER, cricket::CS_REMOTE); + + DoTestSignalNewDescription( + initiator.get(), cricket::BaseSession::STATE_RECEIVEDINITIATE, + cricket::CA_OFFER, cricket::CS_REMOTE); + + DoTestSignalNewDescription( + initiator.get(), cricket::BaseSession::STATE_SENTPRACCEPT, + cricket::CA_PRANSWER, cricket::CS_LOCAL); + + DoTestSignalNewDescription( + initiator.get(), cricket::BaseSession::STATE_SENTPRACCEPT, + cricket::CA_PRANSWER, cricket::CS_LOCAL); + + DoTestSignalNewDescription( + initiator.get(), cricket::BaseSession::STATE_SENTACCEPT, + cricket::CA_ANSWER, cricket::CS_LOCAL); + } + + void TestGetTransportStats() { + talk_base::scoped_ptr allocator( + new TestPortAllocator()); + int next_message_id = 0; + + std::string content_name = "content-name"; + std::string content_type = "content-type"; + talk_base::scoped_ptr initiator( + new TestClient(allocator.get(), &next_message_id, + kInitiator, PROTOCOL_JINGLE, + content_type, + content_name, "", + "", "")); + initiator->CreateSession(); + + cricket::SessionStats stats; + EXPECT_TRUE(initiator->session->GetStats(&stats)); + // At initiation, there are 2 transports. + EXPECT_EQ(2ul, stats.proxy_to_transport.size()); + EXPECT_EQ(2ul, stats.transport_stats.size()); + } +}; + +// For each of these, "X => Y = Z" means "if a client with protocol X +// initiates to a client with protocol Y, they end up speaking protocol Z. + +// Gingle => Gingle = Gingle (with other content) +TEST_F(SessionTest, GingleToGingleOtherContent) { + TestOtherContent(PROTOCOL_GINGLE, PROTOCOL_GINGLE, PROTOCOL_GINGLE); +} + +// Gingle => Gingle = Gingle (with audio content) +TEST_F(SessionTest, GingleToGingleAudioContent) { + TestAudioContent(PROTOCOL_GINGLE, PROTOCOL_GINGLE, PROTOCOL_GINGLE); +} + +// Gingle => Gingle = Gingle (with video contents) +TEST_F(SessionTest, GingleToGingleVideoContents) { + TestVideoContents(PROTOCOL_GINGLE, PROTOCOL_GINGLE, PROTOCOL_GINGLE); +} + +// Jingle => Jingle = Jingle (with other content) +TEST_F(SessionTest, JingleToJingleOtherContent) { + TestOtherContent(PROTOCOL_JINGLE, PROTOCOL_JINGLE, PROTOCOL_JINGLE); +} + +// Jingle => Jingle = Jingle (with audio content) +TEST_F(SessionTest, JingleToJingleAudioContent) { + TestAudioContent(PROTOCOL_JINGLE, PROTOCOL_JINGLE, PROTOCOL_JINGLE); +} + +// Jingle => Jingle = Jingle (with video contents) +TEST_F(SessionTest, JingleToJingleVideoContents) { + TestVideoContents(PROTOCOL_JINGLE, PROTOCOL_JINGLE, PROTOCOL_JINGLE); +} + +// Hybrid => Hybrid = Jingle (with other content) +TEST_F(SessionTest, HybridToHybridOtherContent) { + TestOtherContent(PROTOCOL_HYBRID, PROTOCOL_HYBRID, PROTOCOL_JINGLE); +} + +// Hybrid => Hybrid = Jingle (with audio content) +TEST_F(SessionTest, HybridToHybridAudioContent) { + TestAudioContent(PROTOCOL_HYBRID, PROTOCOL_HYBRID, PROTOCOL_JINGLE); +} + +// Hybrid => Hybrid = Jingle (with video contents) +TEST_F(SessionTest, HybridToHybridVideoContents) { + TestVideoContents(PROTOCOL_HYBRID, PROTOCOL_HYBRID, PROTOCOL_JINGLE); +} + +// Gingle => Hybrid = Gingle (with other content) +TEST_F(SessionTest, GingleToHybridOtherContent) { + TestOtherContent(PROTOCOL_GINGLE, PROTOCOL_HYBRID, PROTOCOL_GINGLE); +} + +// Gingle => Hybrid = Gingle (with audio content) +TEST_F(SessionTest, GingleToHybridAudioContent) { + TestAudioContent(PROTOCOL_GINGLE, PROTOCOL_HYBRID, PROTOCOL_GINGLE); +} + +// Gingle => Hybrid = Gingle (with video contents) +TEST_F(SessionTest, GingleToHybridVideoContents) { + TestVideoContents(PROTOCOL_GINGLE, PROTOCOL_HYBRID, PROTOCOL_GINGLE); +} + +// Jingle => Hybrid = Jingle (with other content) +TEST_F(SessionTest, JingleToHybridOtherContent) { + TestOtherContent(PROTOCOL_JINGLE, PROTOCOL_HYBRID, PROTOCOL_JINGLE); +} + +// Jingle => Hybrid = Jingle (with audio content) +TEST_F(SessionTest, JingleToHybridAudioContent) { + TestAudioContent(PROTOCOL_JINGLE, PROTOCOL_HYBRID, PROTOCOL_JINGLE); +} + +// Jingle => Hybrid = Jingle (with video contents) +TEST_F(SessionTest, JingleToHybridVideoContents) { + TestVideoContents(PROTOCOL_JINGLE, PROTOCOL_HYBRID, PROTOCOL_JINGLE); +} + +// Hybrid => Gingle = Gingle (with other content) +TEST_F(SessionTest, HybridToGingleOtherContent) { + TestOtherContent(PROTOCOL_HYBRID, PROTOCOL_GINGLE, PROTOCOL_GINGLE); +} + +// Hybrid => Gingle = Gingle (with audio content) +TEST_F(SessionTest, HybridToGingleAudioContent) { + TestAudioContent(PROTOCOL_HYBRID, PROTOCOL_GINGLE, PROTOCOL_GINGLE); +} + +// Hybrid => Gingle = Gingle (with video contents) +TEST_F(SessionTest, HybridToGingleVideoContents) { + TestVideoContents(PROTOCOL_HYBRID, PROTOCOL_GINGLE, PROTOCOL_GINGLE); +} + +// Hybrid => Jingle = Jingle (with other content) +TEST_F(SessionTest, HybridToJingleOtherContent) { + TestOtherContent(PROTOCOL_HYBRID, PROTOCOL_JINGLE, PROTOCOL_JINGLE); +} + +// Hybrid => Jingle = Jingle (with audio content) +TEST_F(SessionTest, HybridToJingleAudioContent) { + TestAudioContent(PROTOCOL_HYBRID, PROTOCOL_JINGLE, PROTOCOL_JINGLE); +} + +// Hybrid => Jingle = Jingle (with video contents) +TEST_F(SessionTest, HybridToJingleVideoContents) { + TestVideoContents(PROTOCOL_HYBRID, PROTOCOL_JINGLE, PROTOCOL_JINGLE); +} + +TEST_F(SessionTest, GingleEarlyTerminationFromInitiator) { + TestEarlyTerminationFromInitiator(PROTOCOL_GINGLE); +} + +TEST_F(SessionTest, JingleEarlyTerminationFromInitiator) { + TestEarlyTerminationFromInitiator(PROTOCOL_JINGLE); +} + +TEST_F(SessionTest, HybridEarlyTerminationFromInitiator) { + TestEarlyTerminationFromInitiator(PROTOCOL_HYBRID); +} + +TEST_F(SessionTest, GingleRejection) { + TestRejection(PROTOCOL_GINGLE); +} + +TEST_F(SessionTest, JingleRejection) { + TestRejection(PROTOCOL_JINGLE); +} + +TEST_F(SessionTest, GingleGoodRedirect) { + TestGoodRedirect(PROTOCOL_GINGLE); +} + +TEST_F(SessionTest, JingleGoodRedirect) { + TestGoodRedirect(PROTOCOL_JINGLE); +} + +TEST_F(SessionTest, GingleBadRedirect) { + TestBadRedirect(PROTOCOL_GINGLE); +} + +TEST_F(SessionTest, JingleBadRedirect) { + TestBadRedirect(PROTOCOL_JINGLE); +} + +TEST_F(SessionTest, TestCandidatesInInitiateAndAccept) { + TestCandidatesInInitiateAndAccept("Candidates in initiate/accept"); +} + +TEST_F(SessionTest, TestTransportMux) { + TestTransportMux(); +} + +TEST_F(SessionTest, TestSendDescriptionInfo) { + TestSendDescriptionInfo(); +} + +TEST_F(SessionTest, TestCallerSignalNewDescription) { + TestCallerSignalNewDescription(); +} + +TEST_F(SessionTest, TestCalleeSignalNewDescription) { + TestCalleeSignalNewDescription(); +} + +TEST_F(SessionTest, TestGetTransportStats) { + TestGetTransportStats(); +} diff --git a/talk/p2p/base/sessionclient.h b/talk/p2p/base/sessionclient.h new file mode 100644 index 000000000..10b0c9236 --- /dev/null +++ b/talk/p2p/base/sessionclient.h @@ -0,0 +1,95 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_SESSIONCLIENT_H_ +#define TALK_P2P_BASE_SESSIONCLIENT_H_ + +#include "talk/p2p/base/constants.h" + +namespace buzz { +class XmlElement; +} + +namespace cricket { + +struct ParseError; +class Session; +class ContentDescription; + +class ContentParser { + public: + virtual bool ParseContent(SignalingProtocol protocol, + const buzz::XmlElement* elem, + ContentDescription** content, + ParseError* error) = 0; + // If not IsWriteable, then a given content should be "skipped" when + // writing in the given protocol, as if it didn't exist. We assume + // most things are writeable. We do this to avoid strange cases + // like data contents in Gingle, which aren't writable. + virtual bool IsWritable(SignalingProtocol protocol, + const ContentDescription* content) { + return true; + } + virtual bool WriteContent(SignalingProtocol protocol, + const ContentDescription* content, + buzz::XmlElement** elem, + WriteError* error) = 0; + virtual ~ContentParser() {} +}; + +// A SessionClient exists in 1-1 relation with each session. The implementor +// of this interface is the one that understands *what* the two sides are +// trying to send to one another. The lower-level layers only know how to send +// data; they do not know what is being sent. +class SessionClient : public ContentParser { + public: + // Notifies the client of the creation / destruction of sessions of this type. + // + // IMPORTANT: The SessionClient, in its handling of OnSessionCreate, must + // create whatever channels are indicate in the description. This is because + // the remote client may already be attempting to connect those channels. If + // we do not create our channel right away, then connection may fail or be + // delayed. + virtual void OnSessionCreate(Session* session, bool received_initiate) = 0; + virtual void OnSessionDestroy(Session* session) = 0; + + virtual bool ParseContent(SignalingProtocol protocol, + const buzz::XmlElement* elem, + ContentDescription** content, + ParseError* error) = 0; + virtual bool WriteContent(SignalingProtocol protocol, + const ContentDescription* content, + buzz::XmlElement** elem, + WriteError* error) = 0; + protected: + // The SessionClient interface explicitly does not include destructor + virtual ~SessionClient() { } +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_SESSIONCLIENT_H_ diff --git a/talk/p2p/base/sessiondescription.cc b/talk/p2p/base/sessiondescription.cc new file mode 100644 index 000000000..7009aa87a --- /dev/null +++ b/talk/p2p/base/sessiondescription.cc @@ -0,0 +1,239 @@ +/* + * libjingle + * Copyright 2010, 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/p2p/base/sessiondescription.h" + +#include "talk/xmllite/xmlelement.h" + +namespace cricket { + +ContentInfo* FindContentInfoByName( + ContentInfos& contents, const std::string& name) { + for (ContentInfos::iterator content = contents.begin(); + content != contents.end(); ++content) { + if (content->name == name) { + return &(*content); + } + } + return NULL; +} + +const ContentInfo* FindContentInfoByName( + const ContentInfos& contents, const std::string& name) { + for (ContentInfos::const_iterator content = contents.begin(); + content != contents.end(); ++content) { + if (content->name == name) { + return &(*content); + } + } + return NULL; +} + +const ContentInfo* FindContentInfoByType( + const ContentInfos& contents, const std::string& type) { + for (ContentInfos::const_iterator content = contents.begin(); + content != contents.end(); ++content) { + if (content->type == type) { + return &(*content); + } + } + return NULL; +} + +const std::string* ContentGroup::FirstContentName() const { + return (!content_names_.empty()) ? &(*content_names_.begin()) : NULL; +} + +bool ContentGroup::HasContentName(const std::string& content_name) const { + return (std::find(content_names_.begin(), content_names_.end(), + content_name) != content_names_.end()); +} + +void ContentGroup::AddContentName(const std::string& content_name) { + if (!HasContentName(content_name)) { + content_names_.push_back(content_name); + } +} + +bool ContentGroup::RemoveContentName(const std::string& content_name) { + ContentNames::iterator iter = std::find( + content_names_.begin(), content_names_.end(), content_name); + if (iter == content_names_.end()) { + return false; + } + content_names_.erase(iter); + return true; +} + +SessionDescription* SessionDescription::Copy() const { + SessionDescription* copy = new SessionDescription(*this); + // Copy all ContentDescriptions. + for (ContentInfos::iterator content = copy->contents_.begin(); + content != copy->contents().end(); ++content) { + content->description = content->description->Copy(); + } + return copy; +} + +const ContentInfo* SessionDescription::GetContentByName( + const std::string& name) const { + return FindContentInfoByName(contents_, name); +} + +ContentInfo* SessionDescription::GetContentByName( + const std::string& name) { + return FindContentInfoByName(contents_, name); +} + +const ContentDescription* SessionDescription::GetContentDescriptionByName( + const std::string& name) const { + const ContentInfo* cinfo = FindContentInfoByName(contents_, name); + if (cinfo == NULL) { + return NULL; + } + + return cinfo->description; +} + +ContentDescription* SessionDescription::GetContentDescriptionByName( + const std::string& name) { + ContentInfo* cinfo = FindContentInfoByName(contents_, name); + if (cinfo == NULL) { + return NULL; + } + + return cinfo->description; +} + +const ContentInfo* SessionDescription::FirstContentByType( + const std::string& type) const { + return FindContentInfoByType(contents_, type); +} + +const ContentInfo* SessionDescription::FirstContent() const { + return (contents_.empty()) ? NULL : &(*contents_.begin()); +} + +void SessionDescription::AddContent(const std::string& name, + const std::string& type, + ContentDescription* description) { + contents_.push_back(ContentInfo(name, type, description)); +} + +void SessionDescription::AddContent(const std::string& name, + const std::string& type, + bool rejected, + ContentDescription* description) { + contents_.push_back(ContentInfo(name, type, rejected, description)); +} + +bool SessionDescription::RemoveContentByName(const std::string& name) { + for (ContentInfos::iterator content = contents_.begin(); + content != contents_.end(); ++content) { + if (content->name == name) { + delete content->description; + contents_.erase(content); + return true; + } + } + + return false; +} + +bool SessionDescription::AddTransportInfo(const TransportInfo& transport_info) { + if (GetTransportInfoByName(transport_info.content_name) != NULL) { + return false; + } + transport_infos_.push_back(transport_info); + return true; +} + +bool SessionDescription::RemoveTransportInfoByName(const std::string& name) { + for (TransportInfos::iterator transport_info = transport_infos_.begin(); + transport_info != transport_infos_.end(); ++transport_info) { + if (transport_info->content_name == name) { + transport_infos_.erase(transport_info); + return true; + } + } + return false; +} + +const TransportInfo* SessionDescription::GetTransportInfoByName( + const std::string& name) const { + for (TransportInfos::const_iterator iter = transport_infos_.begin(); + iter != transport_infos_.end(); ++iter) { + if (iter->content_name == name) { + return &(*iter); + } + } + return NULL; +} + +TransportInfo* SessionDescription::GetTransportInfoByName( + const std::string& name) { + for (TransportInfos::iterator iter = transport_infos_.begin(); + iter != transport_infos_.end(); ++iter) { + if (iter->content_name == name) { + return &(*iter); + } + } + return NULL; +} + +void SessionDescription::RemoveGroupByName(const std::string& name) { + for (ContentGroups::iterator iter = content_groups_.begin(); + iter != content_groups_.end(); ++iter) { + if (iter->semantics() == name) { + content_groups_.erase(iter); + break; + } + } +} + +bool SessionDescription::HasGroup(const std::string& name) const { + for (ContentGroups::const_iterator iter = content_groups_.begin(); + iter != content_groups_.end(); ++iter) { + if (iter->semantics() == name) { + return true; + } + } + return false; +} + +const ContentGroup* SessionDescription::GetGroupByName( + const std::string& name) const { + for (ContentGroups::const_iterator iter = content_groups_.begin(); + iter != content_groups_.end(); ++iter) { + if (iter->semantics() == name) { + return &(*iter); + } + } + return NULL; +} + +} // namespace cricket diff --git a/talk/p2p/base/sessiondescription.h b/talk/p2p/base/sessiondescription.h new file mode 100644 index 000000000..d33b4c366 --- /dev/null +++ b/talk/p2p/base/sessiondescription.h @@ -0,0 +1,202 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_SESSIONDESCRIPTION_H_ +#define TALK_P2P_BASE_SESSIONDESCRIPTION_H_ + +#include +#include + +#include "talk/base/constructormagic.h" +#include "talk/p2p/base/transportinfo.h" + +namespace cricket { + +// Describes a session content. Individual content types inherit from +// this class. Analagous to a or +// . +class ContentDescription { + public: + virtual ~ContentDescription() {} + virtual ContentDescription* Copy() const = 0; +}; + +// Analagous to a or . +// name = name of +// type = xmlns of +struct ContentInfo { + ContentInfo() : description(NULL) {} + ContentInfo(const std::string& name, + const std::string& type, + ContentDescription* description) : + name(name), type(type), rejected(false), description(description) {} + ContentInfo(const std::string& name, + const std::string& type, + bool rejected, + ContentDescription* description) : + name(name), type(type), rejected(rejected), description(description) {} + std::string name; + std::string type; + bool rejected; + ContentDescription* description; +}; + +typedef std::vector ContentNames; + +// This class provides a mechanism to aggregate different media contents into a +// group. This group can also be shared with the peers in a pre-defined format. +// GroupInfo should be populated only with the |content_name| of the +// MediaDescription. +class ContentGroup { + public: + explicit ContentGroup(const std::string& semantics) : + semantics_(semantics) {} + + const std::string& semantics() const { return semantics_; } + const ContentNames& content_names() const { return content_names_; } + + const std::string* FirstContentName() const; + bool HasContentName(const std::string& content_name) const; + void AddContentName(const std::string& content_name); + bool RemoveContentName(const std::string& content_name); + + private: + std::string semantics_; + ContentNames content_names_; +}; + +typedef std::vector ContentInfos; +typedef std::vector ContentGroups; + +const ContentInfo* FindContentInfoByName( + const ContentInfos& contents, const std::string& name); +const ContentInfo* FindContentInfoByType( + const ContentInfos& contents, const std::string& type); + +// Describes a collection of contents, each with its own name and +// type. Analogous to a or stanza. Assumes that +// contents are unique be name, but doesn't enforce that. +class SessionDescription { + public: + SessionDescription() {} + explicit SessionDescription(const ContentInfos& contents) : + contents_(contents) {} + SessionDescription(const ContentInfos& contents, + const ContentGroups& groups) : + contents_(contents), + content_groups_(groups) {} + SessionDescription(const ContentInfos& contents, + const TransportInfos& transports, + const ContentGroups& groups) : + contents_(contents), + transport_infos_(transports), + content_groups_(groups) {} + ~SessionDescription() { + for (ContentInfos::iterator content = contents_.begin(); + content != contents_.end(); ++content) { + delete content->description; + } + } + + SessionDescription* Copy() const; + + // Content accessors. + const ContentInfos& contents() const { return contents_; } + ContentInfos& contents() { return contents_; } + const ContentInfo* GetContentByName(const std::string& name) const; + ContentInfo* GetContentByName(const std::string& name); + const ContentDescription* GetContentDescriptionByName( + const std::string& name) const; + ContentDescription* GetContentDescriptionByName(const std::string& name); + const ContentInfo* FirstContentByType(const std::string& type) const; + const ContentInfo* FirstContent() const; + + // Content mutators. + // Adds a content to this description. Takes ownership of ContentDescription*. + void AddContent(const std::string& name, + const std::string& type, + ContentDescription* description); + void AddContent(const std::string& name, + const std::string& type, + bool rejected, + ContentDescription* description); + bool RemoveContentByName(const std::string& name); + + // Transport accessors. + const TransportInfos& transport_infos() const { return transport_infos_; } + TransportInfos& transport_infos() { return transport_infos_; } + const TransportInfo* GetTransportInfoByName( + const std::string& name) const; + TransportInfo* GetTransportInfoByName(const std::string& name); + const TransportDescription* GetTransportDescriptionByName( + const std::string& name) const { + const TransportInfo* tinfo = GetTransportInfoByName(name); + return tinfo ? &tinfo->description : NULL; + } + + // Transport mutators. + void set_transport_infos(const TransportInfos& transport_infos) { + transport_infos_ = transport_infos; + } + // Adds a TransportInfo to this description. + // Returns false if a TransportInfo with the same name already exists. + bool AddTransportInfo(const TransportInfo& transport_info); + bool RemoveTransportInfoByName(const std::string& name); + + // Group accessors. + const ContentGroups& groups() const { return content_groups_; } + const ContentGroup* GetGroupByName(const std::string& name) const; + bool HasGroup(const std::string& name) const; + + // Group mutators. + void AddGroup(const ContentGroup& group) { content_groups_.push_back(group); } + // Remove the first group with the same semantics specified by |name|. + void RemoveGroupByName(const std::string& name); + + private: + ContentInfos contents_; + TransportInfos transport_infos_; + ContentGroups content_groups_; +}; + +// Indicates whether a ContentDescription was an offer or an answer, as +// described in http://www.ietf.org/rfc/rfc3264.txt. CA_UPDATE +// indicates a jingle update message which contains a subset of a full +// session description +enum ContentAction { + CA_OFFER, CA_PRANSWER, CA_ANSWER, CA_UPDATE +}; + +// Indicates whether a ContentDescription was sent by the local client +// or received from the remote client. +enum ContentSource { + CS_LOCAL, CS_REMOTE +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_SESSIONDESCRIPTION_H_ diff --git a/talk/p2p/base/sessionid.h b/talk/p2p/base/sessionid.h new file mode 100644 index 000000000..6942942fb --- /dev/null +++ b/talk/p2p/base/sessionid.h @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_SESSIONID_H_ +#define TALK_P2P_BASE_SESSIONID_H_ + +// TODO: Remove this file. + +namespace cricket { + +} // namespace cricket + +#endif // TALK_P2P_BASE_SESSIONID_H_ diff --git a/talk/p2p/base/sessionmanager.cc b/talk/p2p/base/sessionmanager.cc new file mode 100644 index 000000000..6b3d60a53 --- /dev/null +++ b/talk/p2p/base/sessionmanager.cc @@ -0,0 +1,313 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/sessionmanager.h" + +#include "talk/base/common.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringencode.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/session.h" +#include "talk/p2p/base/sessionmessages.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/jid.h" + +namespace cricket { + +SessionManager::SessionManager(PortAllocator *allocator, + talk_base::Thread *worker) { + allocator_ = allocator; + signaling_thread_ = talk_base::Thread::Current(); + if (worker == NULL) { + worker_thread_ = talk_base::Thread::Current(); + } else { + worker_thread_ = worker; + } + timeout_ = 50; +} + +SessionManager::~SessionManager() { + // Note: Session::Terminate occurs asynchronously, so it's too late to + // delete them now. They better be all gone. + ASSERT(session_map_.empty()); + // TerminateAll(); + SignalDestroyed(); +} + +void SessionManager::AddClient(const std::string& content_type, + SessionClient* client) { + ASSERT(client_map_.find(content_type) == client_map_.end()); + client_map_[content_type] = client; +} + +void SessionManager::RemoveClient(const std::string& content_type) { + ClientMap::iterator iter = client_map_.find(content_type); + ASSERT(iter != client_map_.end()); + client_map_.erase(iter); +} + +SessionClient* SessionManager::GetClient(const std::string& content_type) { + ClientMap::iterator iter = client_map_.find(content_type); + return (iter != client_map_.end()) ? iter->second : NULL; +} + +Session* SessionManager::CreateSession(const std::string& local_name, + const std::string& content_type) { + return CreateSession(local_name, local_name, + talk_base::ToString(talk_base::CreateRandomId64()), + content_type, false); +} + +Session* SessionManager::CreateSession( + const std::string& local_name, const std::string& initiator_name, + const std::string& sid, const std::string& content_type, + bool received_initiate) { + SessionClient* client = GetClient(content_type); + ASSERT(client != NULL); + + Session* session = new Session(this, local_name, initiator_name, + sid, content_type, client); + session->set_identity(transport_desc_factory_.identity()); + session_map_[session->id()] = session; + session->SignalRequestSignaling.connect( + this, &SessionManager::OnRequestSignaling); + session->SignalOutgoingMessage.connect( + this, &SessionManager::OnOutgoingMessage); + session->SignalErrorMessage.connect(this, &SessionManager::OnErrorMessage); + SignalSessionCreate(session, received_initiate); + session->client()->OnSessionCreate(session, received_initiate); + return session; +} + +void SessionManager::DestroySession(Session* session) { + if (session != NULL) { + SessionMap::iterator it = session_map_.find(session->id()); + if (it != session_map_.end()) { + SignalSessionDestroy(session); + session->client()->OnSessionDestroy(session); + session_map_.erase(it); + delete session; + } + } +} + +Session* SessionManager::GetSession(const std::string& sid) { + SessionMap::iterator it = session_map_.find(sid); + if (it != session_map_.end()) + return it->second; + return NULL; +} + +void SessionManager::TerminateAll() { + while (session_map_.begin() != session_map_.end()) { + Session* session = session_map_.begin()->second; + session->Terminate(); + } +} + +bool SessionManager::IsSessionMessage(const buzz::XmlElement* stanza) { + return cricket::IsSessionMessage(stanza); +} + +Session* SessionManager::FindSession(const std::string& sid, + const std::string& remote_name) { + SessionMap::iterator iter = session_map_.find(sid); + if (iter == session_map_.end()) + return NULL; + + Session* session = iter->second; + if (buzz::Jid(remote_name) != buzz::Jid(session->remote_name())) + return NULL; + + return session; +} + +void SessionManager::OnIncomingMessage(const buzz::XmlElement* stanza) { + SessionMessage msg; + ParseError error; + + if (!ParseSessionMessage(stanza, &msg, &error)) { + SendErrorMessage(stanza, buzz::QN_STANZA_BAD_REQUEST, "modify", + error.text, NULL); + return; + } + + Session* session = FindSession(msg.sid, msg.from); + if (session) { + session->OnIncomingMessage(msg); + return; + } + if (msg.type != ACTION_SESSION_INITIATE) { + SendErrorMessage(stanza, buzz::QN_STANZA_BAD_REQUEST, "modify", + "unknown session", NULL); + return; + } + + std::string content_type; + if (!ParseContentType(msg.protocol, msg.action_elem, + &content_type, &error)) { + SendErrorMessage(stanza, buzz::QN_STANZA_BAD_REQUEST, "modify", + error.text, NULL); + return; + } + + if (!GetClient(content_type)) { + SendErrorMessage(stanza, buzz::QN_STANZA_BAD_REQUEST, "modify", + "unknown content type: " + content_type, NULL); + return; + } + + session = CreateSession(msg.to, msg.initiator, msg.sid, + content_type, true); + session->OnIncomingMessage(msg); +} + +void SessionManager::OnIncomingResponse(const buzz::XmlElement* orig_stanza, + const buzz::XmlElement* response_stanza) { + if (orig_stanza == NULL || response_stanza == NULL) { + return; + } + + SessionMessage msg; + ParseError error; + if (!ParseSessionMessage(orig_stanza, &msg, &error)) { + LOG(LS_WARNING) << "Error parsing incoming response: " << error.text + << ":" << orig_stanza; + return; + } + + Session* session = FindSession(msg.sid, msg.to); + if (session) { + session->OnIncomingResponse(orig_stanza, response_stanza, msg); + } +} + +void SessionManager::OnFailedSend(const buzz::XmlElement* orig_stanza, + const buzz::XmlElement* error_stanza) { + SessionMessage msg; + ParseError error; + if (!ParseSessionMessage(orig_stanza, &msg, &error)) { + return; // TODO: log somewhere? + } + + Session* session = FindSession(msg.sid, msg.to); + if (session) { + talk_base::scoped_ptr synthetic_error; + if (!error_stanza) { + // A failed send is semantically equivalent to an error response, so we + // can just turn the former into the latter. + synthetic_error.reset( + CreateErrorMessage(orig_stanza, buzz::QN_STANZA_ITEM_NOT_FOUND, + "cancel", "Recipient did not respond", NULL)); + error_stanza = synthetic_error.get(); + } + + session->OnFailedSend(orig_stanza, error_stanza); + } +} + +void SessionManager::SendErrorMessage(const buzz::XmlElement* stanza, + const buzz::QName& name, + const std::string& type, + const std::string& text, + const buzz::XmlElement* extra_info) { + talk_base::scoped_ptr msg( + CreateErrorMessage(stanza, name, type, text, extra_info)); + SignalOutgoingMessage(this, msg.get()); +} + +buzz::XmlElement* SessionManager::CreateErrorMessage( + const buzz::XmlElement* stanza, + const buzz::QName& name, + const std::string& type, + const std::string& text, + const buzz::XmlElement* extra_info) { + buzz::XmlElement* iq = new buzz::XmlElement(buzz::QN_IQ); + iq->SetAttr(buzz::QN_TO, stanza->Attr(buzz::QN_FROM)); + iq->SetAttr(buzz::QN_ID, stanza->Attr(buzz::QN_ID)); + iq->SetAttr(buzz::QN_TYPE, "error"); + + CopyXmlChildren(stanza, iq); + + buzz::XmlElement* error = new buzz::XmlElement(buzz::QN_ERROR); + error->SetAttr(buzz::QN_TYPE, type); + iq->AddElement(error); + + // If the error name is not in the standard namespace, we have to first add + // some error from that namespace. + if (name.Namespace() != buzz::NS_STANZA) { + error->AddElement( + new buzz::XmlElement(buzz::QN_STANZA_UNDEFINED_CONDITION)); + } + error->AddElement(new buzz::XmlElement(name)); + + if (extra_info) + error->AddElement(new buzz::XmlElement(*extra_info)); + + if (text.size() > 0) { + // It's okay to always use English here. This text is for debugging + // purposes only. + buzz::XmlElement* text_elem = new buzz::XmlElement(buzz::QN_STANZA_TEXT); + text_elem->SetAttr(buzz::QN_XML_LANG, "en"); + text_elem->SetBodyText(text); + error->AddElement(text_elem); + } + + // TODO: Should we include error codes as well for SIP compatibility? + + return iq; +} + +void SessionManager::OnOutgoingMessage(Session* session, + const buzz::XmlElement* stanza) { + SignalOutgoingMessage(this, stanza); +} + +void SessionManager::OnErrorMessage(BaseSession* session, + const buzz::XmlElement* stanza, + const buzz::QName& name, + const std::string& type, + const std::string& text, + const buzz::XmlElement* extra_info) { + SendErrorMessage(stanza, name, type, text, extra_info); +} + +void SessionManager::OnSignalingReady() { + for (SessionMap::iterator it = session_map_.begin(); + it != session_map_.end(); + ++it) { + it->second->OnSignalingReady(); + } +} + +void SessionManager::OnRequestSignaling(Session* session) { + SignalRequestSignaling(); +} + +} // namespace cricket diff --git a/talk/p2p/base/sessionmanager.h b/talk/p2p/base/sessionmanager.h new file mode 100644 index 000000000..dcdf1ed47 --- /dev/null +++ b/talk/p2p/base/sessionmanager.h @@ -0,0 +1,207 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_SESSIONMANAGER_H_ +#define TALK_P2P_BASE_SESSIONMANAGER_H_ + +#include +#include +#include +#include + +#include "talk/base/sigslot.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/portallocator.h" +#include "talk/p2p/base/transportdescriptionfactory.h" + +namespace buzz { +class QName; +class XmlElement; +} + +namespace cricket { + +class Session; +class BaseSession; +class SessionClient; + +// SessionManager manages session instances. +class SessionManager : public sigslot::has_slots<> { + public: + SessionManager(PortAllocator *allocator, + talk_base::Thread *worker_thread = NULL); + virtual ~SessionManager(); + + PortAllocator *port_allocator() const { return allocator_; } + talk_base::Thread *worker_thread() const { return worker_thread_; } + talk_base::Thread *signaling_thread() const { return signaling_thread_; } + + int session_timeout() const { return timeout_; } + void set_session_timeout(int timeout) { timeout_ = timeout; } + + // Set what transport protocol we want to default to. + void set_transport_protocol(TransportProtocol proto) { + transport_desc_factory_.set_protocol(proto); + } + + // Control use of DTLS. An identity must be supplied if DTLS is enabled. + void set_secure(SecurePolicy policy) { + transport_desc_factory_.set_secure(policy); + } + void set_identity(talk_base::SSLIdentity* identity) { + transport_desc_factory_.set_identity(identity); + } + const TransportDescriptionFactory* transport_desc_factory() const { + return &transport_desc_factory_; + } + + // Registers support for the given client. If we receive an initiate + // describing a session of the given type, we will automatically create a + // Session object and notify this client. The client may then accept or + // reject the session. + void AddClient(const std::string& content_type, SessionClient* client); + void RemoveClient(const std::string& content_type); + SessionClient* GetClient(const std::string& content_type); + + // Creates a new session. The given name is the JID of the client on whose + // behalf we initiate the session. + Session *CreateSession(const std::string& local_name, + const std::string& content_type); + + // Destroys the given session. + void DestroySession(Session *session); + + // Returns the session with the given ID or NULL if none exists. + Session *GetSession(const std::string& sid); + + // Terminates all of the sessions created by this manager. + void TerminateAll(); + + // These are signaled whenever the set of existing sessions changes. + sigslot::signal2 SignalSessionCreate; + sigslot::signal1 SignalSessionDestroy; + + // Determines whether the given stanza is intended for some session. + bool IsSessionMessage(const buzz::XmlElement* stanza); + + // Given a sid, initiator, and remote_name, this finds the matching Session + Session* FindSession(const std::string& sid, + const std::string& remote_name); + + // Called when we receive a stanza for which IsSessionMessage is true. + void OnIncomingMessage(const buzz::XmlElement* stanza); + + // Called when we get a response to a message that we sent. + void OnIncomingResponse(const buzz::XmlElement* orig_stanza, + const buzz::XmlElement* response_stanza); + + // Called if an attempted to send times out or an error is returned. In the + // timeout case error_stanza will be NULL + void OnFailedSend(const buzz::XmlElement* orig_stanza, + const buzz::XmlElement* error_stanza); + + // Signalled each time a session generates a signaling message to send. + // Also signalled on errors, but with a NULL session. + sigslot::signal2 SignalOutgoingMessage; + + // Signaled before sessions try to send certain signaling messages. The + // client should call OnSignalingReady once it is safe to send them. These + // steps are taken so that we don't send signaling messages trying to + // re-establish the connectivity of a session when the client cannot send + // the messages (and would probably just drop them on the floor). + // + // Note: you can connect this directly to OnSignalingReady(), if a signalling + // check is not supported. + sigslot::signal0<> SignalRequestSignaling; + void OnSignalingReady(); + + // Signaled when this SessionManager is deleted. + sigslot::signal0<> SignalDestroyed; + + private: + typedef std::map SessionMap; + typedef std::map ClientMap; + + // Helper function for CreateSession. This is also invoked when we receive + // a message attempting to initiate a session with this client. + Session *CreateSession(const std::string& local_name, + const std::string& initiator, + const std::string& sid, + const std::string& content_type, + bool received_initiate); + + // Attempts to find a registered session type whose description appears as + // a child of the session element. Such a child should be present indicating + // the application they hope to initiate. + std::string FindClient(const buzz::XmlElement* session); + + // Sends a message back to the other client indicating that we found an error + // in the stanza they sent. name identifies the error, type is one of the + // standard XMPP types (cancel, continue, modify, auth, wait), and text is a + // description for debugging purposes. + void SendErrorMessage(const buzz::XmlElement* stanza, + const buzz::QName& name, + const std::string& type, + const std::string& text, + const buzz::XmlElement* extra_info); + + // Creates and returns an error message from the given components. The + // caller is responsible for deleting this. + buzz::XmlElement* CreateErrorMessage( + const buzz::XmlElement* stanza, + const buzz::QName& name, + const std::string& type, + const std::string& text, + const buzz::XmlElement* extra_info); + + // Called each time a session requests signaling. + void OnRequestSignaling(Session* session); + + // Called each time a session has an outgoing message. + void OnOutgoingMessage(Session* session, const buzz::XmlElement* stanza); + + // Called each time a session has an error to send. + void OnErrorMessage(BaseSession* session, + const buzz::XmlElement* stanza, + const buzz::QName& name, + const std::string& type, + const std::string& text, + const buzz::XmlElement* extra_info); + + PortAllocator *allocator_; + talk_base::Thread *signaling_thread_; + talk_base::Thread *worker_thread_; + int timeout_; + TransportDescriptionFactory transport_desc_factory_; + SessionMap session_map_; + ClientMap client_map_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_SESSIONMANAGER_H_ diff --git a/talk/p2p/base/sessionmessages.cc b/talk/p2p/base/sessionmessages.cc new file mode 100644 index 000000000..031c3d6f6 --- /dev/null +++ b/talk/p2p/base/sessionmessages.cc @@ -0,0 +1,1147 @@ +/* + * libjingle + * Copyright 2010, 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/p2p/base/sessionmessages.h" + +#include +#include + +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringutils.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/p2ptransport.h" +#include "talk/p2p/base/parsing.h" +#include "talk/p2p/base/sessionclient.h" +#include "talk/p2p/base/sessiondescription.h" +#include "talk/p2p/base/transport.h" +#include "talk/xmllite/xmlconstants.h" +#include "talk/xmpp/constants.h" + +namespace cricket { + +ActionType ToActionType(const std::string& type) { + if (type == GINGLE_ACTION_INITIATE) + return ACTION_SESSION_INITIATE; + if (type == GINGLE_ACTION_INFO) + return ACTION_SESSION_INFO; + if (type == GINGLE_ACTION_ACCEPT) + return ACTION_SESSION_ACCEPT; + if (type == GINGLE_ACTION_REJECT) + return ACTION_SESSION_REJECT; + if (type == GINGLE_ACTION_TERMINATE) + return ACTION_SESSION_TERMINATE; + if (type == GINGLE_ACTION_CANDIDATES) + return ACTION_TRANSPORT_INFO; + if (type == JINGLE_ACTION_SESSION_INITIATE) + return ACTION_SESSION_INITIATE; + if (type == JINGLE_ACTION_TRANSPORT_INFO) + return ACTION_TRANSPORT_INFO; + if (type == JINGLE_ACTION_TRANSPORT_ACCEPT) + return ACTION_TRANSPORT_ACCEPT; + if (type == JINGLE_ACTION_SESSION_INFO) + return ACTION_SESSION_INFO; + if (type == JINGLE_ACTION_SESSION_ACCEPT) + return ACTION_SESSION_ACCEPT; + if (type == JINGLE_ACTION_SESSION_TERMINATE) + return ACTION_SESSION_TERMINATE; + if (type == JINGLE_ACTION_TRANSPORT_INFO) + return ACTION_TRANSPORT_INFO; + if (type == JINGLE_ACTION_TRANSPORT_ACCEPT) + return ACTION_TRANSPORT_ACCEPT; + if (type == JINGLE_ACTION_DESCRIPTION_INFO) + return ACTION_DESCRIPTION_INFO; + if (type == GINGLE_ACTION_UPDATE) + return ACTION_DESCRIPTION_INFO; + + return ACTION_UNKNOWN; +} + +std::string ToJingleString(ActionType type) { + switch (type) { + case ACTION_SESSION_INITIATE: + return JINGLE_ACTION_SESSION_INITIATE; + case ACTION_SESSION_INFO: + return JINGLE_ACTION_SESSION_INFO; + case ACTION_DESCRIPTION_INFO: + return JINGLE_ACTION_DESCRIPTION_INFO; + case ACTION_SESSION_ACCEPT: + return JINGLE_ACTION_SESSION_ACCEPT; + // Notice that reject and terminate both go to + // "session-terminate", but there is no "session-reject". + case ACTION_SESSION_REJECT: + case ACTION_SESSION_TERMINATE: + return JINGLE_ACTION_SESSION_TERMINATE; + case ACTION_TRANSPORT_INFO: + return JINGLE_ACTION_TRANSPORT_INFO; + case ACTION_TRANSPORT_ACCEPT: + return JINGLE_ACTION_TRANSPORT_ACCEPT; + default: + return ""; + } +} + +std::string ToGingleString(ActionType type) { + switch (type) { + case ACTION_SESSION_INITIATE: + return GINGLE_ACTION_INITIATE; + case ACTION_SESSION_INFO: + return GINGLE_ACTION_INFO; + case ACTION_SESSION_ACCEPT: + return GINGLE_ACTION_ACCEPT; + case ACTION_SESSION_REJECT: + return GINGLE_ACTION_REJECT; + case ACTION_SESSION_TERMINATE: + return GINGLE_ACTION_TERMINATE; + case ACTION_TRANSPORT_INFO: + return GINGLE_ACTION_CANDIDATES; + default: + return ""; + } +} + + +bool IsJingleMessage(const buzz::XmlElement* stanza) { + const buzz::XmlElement* jingle = stanza->FirstNamed(QN_JINGLE); + if (jingle == NULL) + return false; + + return (jingle->HasAttr(buzz::QN_ACTION) && jingle->HasAttr(QN_SID)); +} + +bool IsGingleMessage(const buzz::XmlElement* stanza) { + const buzz::XmlElement* session = stanza->FirstNamed(QN_GINGLE_SESSION); + if (session == NULL) + return false; + + return (session->HasAttr(buzz::QN_TYPE) && + session->HasAttr(buzz::QN_ID) && + session->HasAttr(QN_INITIATOR)); +} + +bool IsSessionMessage(const buzz::XmlElement* stanza) { + return (stanza->Name() == buzz::QN_IQ && + stanza->Attr(buzz::QN_TYPE) == buzz::STR_SET && + (IsJingleMessage(stanza) || + IsGingleMessage(stanza))); +} + +bool ParseGingleSessionMessage(const buzz::XmlElement* session, + SessionMessage* msg, + ParseError* error) { + msg->protocol = PROTOCOL_GINGLE; + std::string type_string = session->Attr(buzz::QN_TYPE); + msg->type = ToActionType(type_string); + msg->sid = session->Attr(buzz::QN_ID); + msg->initiator = session->Attr(QN_INITIATOR); + msg->action_elem = session; + + if (msg->type == ACTION_UNKNOWN) + return BadParse("unknown action: " + type_string, error); + + return true; +} + +bool ParseJingleSessionMessage(const buzz::XmlElement* jingle, + SessionMessage* msg, + ParseError* error) { + msg->protocol = PROTOCOL_JINGLE; + std::string type_string = jingle->Attr(buzz::QN_ACTION); + msg->type = ToActionType(type_string); + msg->sid = jingle->Attr(QN_SID); + msg->initiator = GetXmlAttr(jingle, QN_INITIATOR, buzz::STR_EMPTY); + msg->action_elem = jingle; + + if (msg->type == ACTION_UNKNOWN) + return BadParse("unknown action: " + type_string, error); + + return true; +} + +bool ParseHybridSessionMessage(const buzz::XmlElement* jingle, + SessionMessage* msg, + ParseError* error) { + if (!ParseJingleSessionMessage(jingle, msg, error)) + return false; + msg->protocol = PROTOCOL_HYBRID; + + return true; +} + +bool ParseSessionMessage(const buzz::XmlElement* stanza, + SessionMessage* msg, + ParseError* error) { + msg->id = stanza->Attr(buzz::QN_ID); + msg->from = stanza->Attr(buzz::QN_FROM); + msg->to = stanza->Attr(buzz::QN_TO); + msg->stanza = stanza; + + const buzz::XmlElement* jingle = stanza->FirstNamed(QN_JINGLE); + const buzz::XmlElement* session = stanza->FirstNamed(QN_GINGLE_SESSION); + if (jingle && session) + return ParseHybridSessionMessage(jingle, msg, error); + if (jingle != NULL) + return ParseJingleSessionMessage(jingle, msg, error); + if (session != NULL) + return ParseGingleSessionMessage(session, msg, error); + return false; +} + +buzz::XmlElement* WriteGingleAction(const SessionMessage& msg, + const XmlElements& action_elems) { + buzz::XmlElement* session = new buzz::XmlElement(QN_GINGLE_SESSION, true); + session->AddAttr(buzz::QN_TYPE, ToGingleString(msg.type)); + session->AddAttr(buzz::QN_ID, msg.sid); + session->AddAttr(QN_INITIATOR, msg.initiator); + AddXmlChildren(session, action_elems); + return session; +} + +buzz::XmlElement* WriteJingleAction(const SessionMessage& msg, + const XmlElements& action_elems) { + buzz::XmlElement* jingle = new buzz::XmlElement(QN_JINGLE, true); + jingle->AddAttr(buzz::QN_ACTION, ToJingleString(msg.type)); + jingle->AddAttr(QN_SID, msg.sid); + if (msg.type == ACTION_SESSION_INITIATE) { + jingle->AddAttr(QN_INITIATOR, msg.initiator); + } + AddXmlChildren(jingle, action_elems); + return jingle; +} + +void WriteSessionMessage(const SessionMessage& msg, + const XmlElements& action_elems, + buzz::XmlElement* stanza) { + stanza->SetAttr(buzz::QN_TO, msg.to); + stanza->SetAttr(buzz::QN_TYPE, buzz::STR_SET); + + if (msg.protocol == PROTOCOL_GINGLE) { + stanza->AddElement(WriteGingleAction(msg, action_elems)); + } else { + stanza->AddElement(WriteJingleAction(msg, action_elems)); + } +} + + +TransportParser* GetTransportParser(const TransportParserMap& trans_parsers, + const std::string& transport_type) { + TransportParserMap::const_iterator map = trans_parsers.find(transport_type); + if (map == trans_parsers.end()) { + return NULL; + } else { + return map->second; + } +} + +CandidateTranslator* GetCandidateTranslator( + const CandidateTranslatorMap& translators, + const std::string& content_name) { + CandidateTranslatorMap::const_iterator map = translators.find(content_name); + if (map == translators.end()) { + return NULL; + } else { + return map->second; + } +} + +bool GetParserAndTranslator(const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + const std::string& transport_type, + const std::string& content_name, + TransportParser** parser, + CandidateTranslator** translator, + ParseError* error) { + *parser = GetTransportParser(trans_parsers, transport_type); + if (*parser == NULL) { + return BadParse("unknown transport type: " + transport_type, error); + } + // Not having a translator isn't fatal when parsing. If this is called for an + // initiate message, we won't have our proxies set up to do the translation. + // Fortunately, for the cases where translation is needed, candidates are + // never sent in initiates. + *translator = GetCandidateTranslator(translators, content_name); + return true; +} + +bool GetParserAndTranslator(const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + const std::string& transport_type, + const std::string& content_name, + TransportParser** parser, + CandidateTranslator** translator, + WriteError* error) { + *parser = GetTransportParser(trans_parsers, transport_type); + if (*parser == NULL) { + return BadWrite("unknown transport type: " + transport_type, error); + } + *translator = GetCandidateTranslator(translators, content_name); + if (*translator == NULL) { + return BadWrite("unknown content name: " + content_name, error); + } + return true; +} + +bool ParseGingleCandidate(const buzz::XmlElement* candidate_elem, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + const std::string& content_name, + Candidates* candidates, + ParseError* error) { + TransportParser* trans_parser; + CandidateTranslator* translator; + if (!GetParserAndTranslator(trans_parsers, translators, + NS_GINGLE_P2P, content_name, + &trans_parser, &translator, error)) + return false; + + Candidate candidate; + if (!trans_parser->ParseGingleCandidate( + candidate_elem, translator, &candidate, error)) { + return false; + } + + candidates->push_back(candidate); + return true; +} + +bool ParseGingleCandidates(const buzz::XmlElement* parent, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + const std::string& content_name, + Candidates* candidates, + ParseError* error) { + for (const buzz::XmlElement* candidate_elem = parent->FirstElement(); + candidate_elem != NULL; + candidate_elem = candidate_elem->NextElement()) { + if (candidate_elem->Name().LocalPart() == LN_CANDIDATE) { + if (!ParseGingleCandidate(candidate_elem, trans_parsers, translators, + content_name, candidates, error)) { + return false; + } + } + } + return true; +} + +bool ParseGingleTransportInfos(const buzz::XmlElement* action_elem, + const ContentInfos& contents, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + TransportInfos* tinfos, + ParseError* error) { + bool has_audio = FindContentInfoByName(contents, CN_AUDIO) != NULL; + bool has_video = FindContentInfoByName(contents, CN_VIDEO) != NULL; + + // If we don't have media, no need to separate the candidates. + if (!has_audio && !has_video) { + TransportInfo tinfo(CN_OTHER, + TransportDescription(NS_GINGLE_P2P, Candidates())); + if (!ParseGingleCandidates(action_elem, trans_parsers, translators, + CN_OTHER, &tinfo.description.candidates, + error)) { + return false; + } + + tinfos->push_back(tinfo); + return true; + } + + // If we have media, separate the candidates. + TransportInfo audio_tinfo(CN_AUDIO, + TransportDescription(NS_GINGLE_P2P, Candidates())); + TransportInfo video_tinfo(CN_VIDEO, + TransportDescription(NS_GINGLE_P2P, Candidates())); + for (const buzz::XmlElement* candidate_elem = action_elem->FirstElement(); + candidate_elem != NULL; + candidate_elem = candidate_elem->NextElement()) { + if (candidate_elem->Name().LocalPart() == LN_CANDIDATE) { + const std::string& channel_name = candidate_elem->Attr(buzz::QN_NAME); + if (has_audio && + (channel_name == GICE_CHANNEL_NAME_RTP || + channel_name == GICE_CHANNEL_NAME_RTCP)) { + if (!ParseGingleCandidate( + candidate_elem, trans_parsers, + translators, CN_AUDIO, + &audio_tinfo.description.candidates, error)) { + return false; + } + } else if (has_video && + (channel_name == GICE_CHANNEL_NAME_VIDEO_RTP || + channel_name == GICE_CHANNEL_NAME_VIDEO_RTCP)) { + if (!ParseGingleCandidate( + candidate_elem, trans_parsers, + translators, CN_VIDEO, + &video_tinfo.description.candidates, error)) { + return false; + } + } else { + return BadParse("Unknown channel name: " + channel_name, error); + } + } + } + + if (has_audio) { + tinfos->push_back(audio_tinfo); + } + if (has_video) { + tinfos->push_back(video_tinfo); + } + return true; +} + +bool ParseJingleTransportInfo(const buzz::XmlElement* trans_elem, + const std::string& content_name, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + TransportInfo* tinfo, + ParseError* error) { + TransportParser* trans_parser; + CandidateTranslator* translator; + if (!GetParserAndTranslator(trans_parsers, translators, + trans_elem->Name().Namespace(), content_name, + &trans_parser, &translator, error)) + return false; + + TransportDescription tdesc; + if (!trans_parser->ParseTransportDescription(trans_elem, translator, + &tdesc, error)) + return false; + + *tinfo = TransportInfo(content_name, tdesc); + return true; +} + +bool ParseJingleTransportInfos(const buzz::XmlElement* jingle, + const ContentInfos& contents, + const TransportParserMap trans_parsers, + const CandidateTranslatorMap& translators, + TransportInfos* tinfos, + ParseError* error) { + for (const buzz::XmlElement* pair_elem + = jingle->FirstNamed(QN_JINGLE_CONTENT); + pair_elem != NULL; + pair_elem = pair_elem->NextNamed(QN_JINGLE_CONTENT)) { + std::string content_name; + if (!RequireXmlAttr(pair_elem, QN_JINGLE_CONTENT_NAME, + &content_name, error)) + return false; + + const ContentInfo* content = FindContentInfoByName(contents, content_name); + if (!content) + return BadParse("Unknown content name: " + content_name, error); + + const buzz::XmlElement* trans_elem; + if (!RequireXmlChild(pair_elem, LN_TRANSPORT, &trans_elem, error)) + return false; + + TransportInfo tinfo; + if (!ParseJingleTransportInfo(trans_elem, content->name, + trans_parsers, translators, + &tinfo, error)) + return false; + + tinfos->push_back(tinfo); + } + + return true; +} + +buzz::XmlElement* NewTransportElement(const std::string& name) { + return new buzz::XmlElement(buzz::QName(name, LN_TRANSPORT), true); +} + +bool WriteGingleCandidates(const Candidates& candidates, + const TransportParserMap& trans_parsers, + const std::string& transport_type, + const CandidateTranslatorMap& translators, + const std::string& content_name, + XmlElements* elems, + WriteError* error) { + TransportParser* trans_parser; + CandidateTranslator* translator; + if (!GetParserAndTranslator(trans_parsers, translators, + transport_type, content_name, + &trans_parser, &translator, error)) + return false; + + for (size_t i = 0; i < candidates.size(); ++i) { + talk_base::scoped_ptr element; + if (!trans_parser->WriteGingleCandidate(candidates[i], translator, + element.accept(), error)) { + return false; + } + + elems->push_back(element.release()); + } + + return true; +} + +bool WriteGingleTransportInfos(const TransportInfos& tinfos, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + XmlElements* elems, + WriteError* error) { + for (TransportInfos::const_iterator tinfo = tinfos.begin(); + tinfo != tinfos.end(); ++tinfo) { + if (!WriteGingleCandidates(tinfo->description.candidates, + trans_parsers, tinfo->description.transport_type, + translators, tinfo->content_name, + elems, error)) + return false; + } + + return true; +} + +bool WriteJingleTransportInfo(const TransportInfo& tinfo, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + XmlElements* elems, + WriteError* error) { + std::string transport_type = tinfo.description.transport_type; + TransportParser* trans_parser; + CandidateTranslator* translator; + if (!GetParserAndTranslator(trans_parsers, translators, + transport_type, tinfo.content_name, + &trans_parser, &translator, error)) + return false; + + buzz::XmlElement* trans_elem; + if (!trans_parser->WriteTransportDescription(tinfo.description, translator, + &trans_elem, error)) { + return false; + } + + elems->push_back(trans_elem); + return true; +} + +void WriteJingleContent(const std::string name, + const XmlElements& child_elems, + XmlElements* elems) { + buzz::XmlElement* content_elem = new buzz::XmlElement(QN_JINGLE_CONTENT); + content_elem->SetAttr(QN_JINGLE_CONTENT_NAME, name); + content_elem->SetAttr(QN_CREATOR, LN_INITIATOR); + AddXmlChildren(content_elem, child_elems); + + elems->push_back(content_elem); +} + +bool WriteJingleTransportInfos(const TransportInfos& tinfos, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + XmlElements* elems, + WriteError* error) { + for (TransportInfos::const_iterator tinfo = tinfos.begin(); + tinfo != tinfos.end(); ++tinfo) { + XmlElements content_child_elems; + if (!WriteJingleTransportInfo(*tinfo, trans_parsers, translators, + &content_child_elems, error)) + + return false; + + WriteJingleContent(tinfo->content_name, content_child_elems, elems); + } + + return true; +} + +ContentParser* GetContentParser(const ContentParserMap& content_parsers, + const std::string& type) { + ContentParserMap::const_iterator map = content_parsers.find(type); + if (map == content_parsers.end()) { + return NULL; + } else { + return map->second; + } +} + +bool ParseContentInfo(SignalingProtocol protocol, + const std::string& name, + const std::string& type, + const buzz::XmlElement* elem, + const ContentParserMap& parsers, + ContentInfos* contents, + ParseError* error) { + ContentParser* parser = GetContentParser(parsers, type); + if (parser == NULL) + return BadParse("unknown application content: " + type, error); + + ContentDescription* desc; + if (!parser->ParseContent(protocol, elem, &desc, error)) + return false; + + contents->push_back(ContentInfo(name, type, desc)); + return true; +} + +bool ParseContentType(const buzz::XmlElement* parent_elem, + std::string* content_type, + const buzz::XmlElement** content_elem, + ParseError* error) { + if (!RequireXmlChild(parent_elem, LN_DESCRIPTION, content_elem, error)) + return false; + + *content_type = (*content_elem)->Name().Namespace(); + return true; +} + +bool ParseGingleContentInfos(const buzz::XmlElement* session, + const ContentParserMap& content_parsers, + ContentInfos* contents, + ParseError* error) { + std::string content_type; + const buzz::XmlElement* content_elem; + if (!ParseContentType(session, &content_type, &content_elem, error)) + return false; + + if (content_type == NS_GINGLE_VIDEO) { + // A parser parsing audio or video content should look at the + // namespace and only parse the codecs relevant to that namespace. + // We use this to control which codecs get parsed: first audio, + // then video. + talk_base::scoped_ptr audio_elem( + new buzz::XmlElement(QN_GINGLE_AUDIO_CONTENT)); + CopyXmlChildren(content_elem, audio_elem.get()); + if (!ParseContentInfo(PROTOCOL_GINGLE, CN_AUDIO, NS_JINGLE_RTP, + audio_elem.get(), content_parsers, + contents, error)) + return false; + + if (!ParseContentInfo(PROTOCOL_GINGLE, CN_VIDEO, NS_JINGLE_RTP, + content_elem, content_parsers, + contents, error)) + return false; + } else if (content_type == NS_GINGLE_AUDIO) { + if (!ParseContentInfo(PROTOCOL_GINGLE, CN_AUDIO, NS_JINGLE_RTP, + content_elem, content_parsers, + contents, error)) + return false; + } else { + if (!ParseContentInfo(PROTOCOL_GINGLE, CN_OTHER, content_type, + content_elem, content_parsers, + contents, error)) + return false; + } + return true; +} + +bool ParseJingleContentInfos(const buzz::XmlElement* jingle, + const ContentParserMap& content_parsers, + ContentInfos* contents, + ParseError* error) { + for (const buzz::XmlElement* pair_elem + = jingle->FirstNamed(QN_JINGLE_CONTENT); + pair_elem != NULL; + pair_elem = pair_elem->NextNamed(QN_JINGLE_CONTENT)) { + std::string content_name; + if (!RequireXmlAttr(pair_elem, QN_JINGLE_CONTENT_NAME, + &content_name, error)) + return false; + + std::string content_type; + const buzz::XmlElement* content_elem; + if (!ParseContentType(pair_elem, &content_type, &content_elem, error)) + return false; + + if (!ParseContentInfo(PROTOCOL_JINGLE, content_name, content_type, + content_elem, content_parsers, + contents, error)) + return false; + } + return true; +} + +bool ParseJingleGroupInfos(const buzz::XmlElement* jingle, + ContentGroups* groups, + ParseError* error) { + for (const buzz::XmlElement* pair_elem + = jingle->FirstNamed(QN_JINGLE_DRAFT_GROUP); + pair_elem != NULL; + pair_elem = pair_elem->NextNamed(QN_JINGLE_DRAFT_GROUP)) { + std::string group_name; + if (!RequireXmlAttr(pair_elem, QN_JINGLE_DRAFT_GROUP_TYPE, + &group_name, error)) + return false; + + ContentGroup group(group_name); + for (const buzz::XmlElement* child_elem + = pair_elem->FirstNamed(QN_JINGLE_CONTENT); + child_elem != NULL; + child_elem = child_elem->NextNamed(QN_JINGLE_CONTENT)) { + std::string content_name; + if (!RequireXmlAttr(child_elem, QN_JINGLE_CONTENT_NAME, + &content_name, error)) + return false; + group.AddContentName(content_name); + } + groups->push_back(group); + } + return true; +} + +buzz::XmlElement* WriteContentInfo(SignalingProtocol protocol, + const ContentInfo& content, + const ContentParserMap& parsers, + WriteError* error) { + ContentParser* parser = GetContentParser(parsers, content.type); + if (parser == NULL) { + BadWrite("unknown content type: " + content.type, error); + return NULL; + } + + buzz::XmlElement* elem = NULL; + if (!parser->WriteContent(protocol, content.description, &elem, error)) + return NULL; + + return elem; +} + +bool IsWritable(SignalingProtocol protocol, + const ContentInfo& content, + const ContentParserMap& parsers) { + ContentParser* parser = GetContentParser(parsers, content.type); + if (parser == NULL) { + return false; + } + + return parser->IsWritable(protocol, content.description); +} + +bool WriteGingleContentInfos(const ContentInfos& contents, + const ContentParserMap& parsers, + XmlElements* elems, + WriteError* error) { + if (contents.size() == 1 || + (contents.size() == 2 && + !IsWritable(PROTOCOL_GINGLE, contents.at(1), parsers))) { + if (contents.front().rejected) { + return BadWrite("Gingle protocol may not reject individual contents.", + error); + } + buzz::XmlElement* elem = WriteContentInfo( + PROTOCOL_GINGLE, contents.front(), parsers, error); + if (!elem) + return false; + + elems->push_back(elem); + } else if (contents.size() >= 2 && + contents.at(0).type == NS_JINGLE_RTP && + contents.at(1).type == NS_JINGLE_RTP) { + // Special-case audio + video contents so that they are "merged" + // into one "video" content. + if (contents.at(0).rejected || contents.at(1).rejected) { + return BadWrite("Gingle protocol may not reject individual contents.", + error); + } + buzz::XmlElement* audio = WriteContentInfo( + PROTOCOL_GINGLE, contents.at(0), parsers, error); + if (!audio) + return false; + + buzz::XmlElement* video = WriteContentInfo( + PROTOCOL_GINGLE, contents.at(1), parsers, error); + if (!video) { + delete audio; + return false; + } + + CopyXmlChildren(audio, video); + elems->push_back(video); + delete audio; + } else { + return BadWrite("Gingle protocol may only have one content.", error); + } + + return true; +} + +const TransportInfo* GetTransportInfoByContentName( + const TransportInfos& tinfos, const std::string& content_name) { + for (TransportInfos::const_iterator tinfo = tinfos.begin(); + tinfo != tinfos.end(); ++tinfo) { + if (content_name == tinfo->content_name) { + return &*tinfo; + } + } + return NULL; +} + +bool WriteJingleContents(const ContentInfos& contents, + const ContentParserMap& content_parsers, + const TransportInfos& tinfos, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + XmlElements* elems, + WriteError* error) { + for (ContentInfos::const_iterator content = contents.begin(); + content != contents.end(); ++content) { + if (content->rejected) { + continue; + } + const TransportInfo* tinfo = + GetTransportInfoByContentName(tinfos, content->name); + if (!tinfo) + return BadWrite("No transport for content: " + content->name, error); + + XmlElements pair_elems; + buzz::XmlElement* elem = WriteContentInfo( + PROTOCOL_JINGLE, *content, content_parsers, error); + if (!elem) + return false; + pair_elems.push_back(elem); + + if (!WriteJingleTransportInfo(*tinfo, trans_parsers, translators, + &pair_elems, error)) + return false; + + WriteJingleContent(content->name, pair_elems, elems); + } + return true; +} + +bool WriteJingleContentInfos(const ContentInfos& contents, + const ContentParserMap& content_parsers, + XmlElements* elems, + WriteError* error) { + for (ContentInfos::const_iterator content = contents.begin(); + content != contents.end(); ++content) { + if (content->rejected) { + continue; + } + XmlElements content_child_elems; + buzz::XmlElement* elem = WriteContentInfo( + PROTOCOL_JINGLE, *content, content_parsers, error); + if (!elem) + return false; + content_child_elems.push_back(elem); + WriteJingleContent(content->name, content_child_elems, elems); + } + return true; +} + +bool WriteJingleGroupInfo(const ContentInfos& contents, + const ContentGroups& groups, + XmlElements* elems, + WriteError* error) { + if (!groups.empty()) { + buzz::XmlElement* pair_elem = new buzz::XmlElement(QN_JINGLE_DRAFT_GROUP); + pair_elem->SetAttr(QN_JINGLE_DRAFT_GROUP_TYPE, GROUP_TYPE_BUNDLE); + + XmlElements pair_elems; + for (ContentInfos::const_iterator content = contents.begin(); + content != contents.end(); ++content) { + buzz::XmlElement* child_elem = + new buzz::XmlElement(QN_JINGLE_CONTENT, false); + child_elem->SetAttr(QN_JINGLE_CONTENT_NAME, content->name); + pair_elems.push_back(child_elem); + } + AddXmlChildren(pair_elem, pair_elems); + elems->push_back(pair_elem); + } + return true; +} + +bool ParseContentType(SignalingProtocol protocol, + const buzz::XmlElement* action_elem, + std::string* content_type, + ParseError* error) { + const buzz::XmlElement* content_elem; + if (protocol == PROTOCOL_GINGLE) { + if (!ParseContentType(action_elem, content_type, &content_elem, error)) + return false; + + // Internally, we only use NS_JINGLE_RTP. + if (*content_type == NS_GINGLE_AUDIO || + *content_type == NS_GINGLE_VIDEO) + *content_type = NS_JINGLE_RTP; + } else { + const buzz::XmlElement* pair_elem + = action_elem->FirstNamed(QN_JINGLE_CONTENT); + if (pair_elem == NULL) + return BadParse("No contents found", error); + + if (!ParseContentType(pair_elem, content_type, &content_elem, error)) + return false; + } + + return true; +} + +static bool ParseContentMessage( + SignalingProtocol protocol, + const buzz::XmlElement* action_elem, + bool expect_transports, + const ContentParserMap& content_parsers, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + SessionInitiate* init, + ParseError* error) { + init->owns_contents = true; + if (protocol == PROTOCOL_GINGLE) { + if (!ParseGingleContentInfos(action_elem, content_parsers, + &init->contents, error)) + return false; + + if (expect_transports && + !ParseGingleTransportInfos(action_elem, init->contents, + trans_parsers, translators, + &init->transports, error)) + return false; + } else { + if (!ParseJingleContentInfos(action_elem, content_parsers, + &init->contents, error)) + return false; + if (!ParseJingleGroupInfos(action_elem, &init->groups, error)) + return false; + + if (expect_transports && + !ParseJingleTransportInfos(action_elem, init->contents, + trans_parsers, translators, + &init->transports, error)) + return false; + } + + return true; +} + +static bool WriteContentMessage( + SignalingProtocol protocol, + const ContentInfos& contents, + const TransportInfos& tinfos, + const ContentParserMap& content_parsers, + const TransportParserMap& transport_parsers, + const CandidateTranslatorMap& translators, + const ContentGroups& groups, + XmlElements* elems, + WriteError* error) { + if (protocol == PROTOCOL_GINGLE) { + if (!WriteGingleContentInfos(contents, content_parsers, elems, error)) + return false; + + if (!WriteGingleTransportInfos(tinfos, transport_parsers, translators, + elems, error)) + return false; + } else { + if (!WriteJingleContents(contents, content_parsers, + tinfos, transport_parsers, translators, + elems, error)) + return false; + if (!WriteJingleGroupInfo(contents, groups, elems, error)) + return false; + } + + return true; +} + +bool ParseSessionInitiate(SignalingProtocol protocol, + const buzz::XmlElement* action_elem, + const ContentParserMap& content_parsers, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + SessionInitiate* init, + ParseError* error) { + bool expect_transports = true; + return ParseContentMessage(protocol, action_elem, expect_transports, + content_parsers, trans_parsers, translators, + init, error); +} + + +bool WriteSessionInitiate(SignalingProtocol protocol, + const ContentInfos& contents, + const TransportInfos& tinfos, + const ContentParserMap& content_parsers, + const TransportParserMap& transport_parsers, + const CandidateTranslatorMap& translators, + const ContentGroups& groups, + XmlElements* elems, + WriteError* error) { + return WriteContentMessage(protocol, contents, tinfos, + content_parsers, transport_parsers, translators, + groups, + elems, error); +} + +bool ParseSessionAccept(SignalingProtocol protocol, + const buzz::XmlElement* action_elem, + const ContentParserMap& content_parsers, + const TransportParserMap& transport_parsers, + const CandidateTranslatorMap& translators, + SessionAccept* accept, + ParseError* error) { + bool expect_transports = true; + return ParseContentMessage(protocol, action_elem, expect_transports, + content_parsers, transport_parsers, translators, + accept, error); +} + +bool WriteSessionAccept(SignalingProtocol protocol, + const ContentInfos& contents, + const TransportInfos& tinfos, + const ContentParserMap& content_parsers, + const TransportParserMap& transport_parsers, + const CandidateTranslatorMap& translators, + const ContentGroups& groups, + XmlElements* elems, + WriteError* error) { + return WriteContentMessage(protocol, contents, tinfos, + content_parsers, transport_parsers, translators, + groups, + elems, error); +} + +bool ParseSessionTerminate(SignalingProtocol protocol, + const buzz::XmlElement* action_elem, + SessionTerminate* term, + ParseError* error) { + if (protocol == PROTOCOL_GINGLE) { + const buzz::XmlElement* reason_elem = action_elem->FirstElement(); + if (reason_elem != NULL) { + term->reason = reason_elem->Name().LocalPart(); + const buzz::XmlElement *debug_elem = reason_elem->FirstElement(); + if (debug_elem != NULL) { + term->debug_reason = debug_elem->Name().LocalPart(); + } + } + return true; + } else { + const buzz::XmlElement* reason_elem = + action_elem->FirstNamed(QN_JINGLE_REASON); + if (reason_elem) { + reason_elem = reason_elem->FirstElement(); + if (reason_elem) { + term->reason = reason_elem->Name().LocalPart(); + } + } + return true; + } +} + +void WriteSessionTerminate(SignalingProtocol protocol, + const SessionTerminate& term, + XmlElements* elems) { + if (protocol == PROTOCOL_GINGLE) { + elems->push_back(new buzz::XmlElement(buzz::QName(NS_GINGLE, term.reason))); + } else { + if (!term.reason.empty()) { + buzz::XmlElement* reason_elem = new buzz::XmlElement(QN_JINGLE_REASON); + reason_elem->AddElement(new buzz::XmlElement( + buzz::QName(NS_JINGLE, term.reason))); + elems->push_back(reason_elem); + } + } +} + +bool ParseDescriptionInfo(SignalingProtocol protocol, + const buzz::XmlElement* action_elem, + const ContentParserMap& content_parsers, + const TransportParserMap& transport_parsers, + const CandidateTranslatorMap& translators, + DescriptionInfo* description_info, + ParseError* error) { + bool expect_transports = false; + return ParseContentMessage(protocol, action_elem, expect_transports, + content_parsers, transport_parsers, translators, + description_info, error); +} + +bool WriteDescriptionInfo(SignalingProtocol protocol, + const ContentInfos& contents, + const ContentParserMap& content_parsers, + XmlElements* elems, + WriteError* error) { + if (protocol == PROTOCOL_GINGLE) { + return WriteGingleContentInfos(contents, content_parsers, elems, error); + } else { + return WriteJingleContentInfos(contents, content_parsers, elems, error); + } +} + +bool ParseTransportInfos(SignalingProtocol protocol, + const buzz::XmlElement* action_elem, + const ContentInfos& contents, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + TransportInfos* tinfos, + ParseError* error) { + if (protocol == PROTOCOL_GINGLE) { + return ParseGingleTransportInfos( + action_elem, contents, trans_parsers, translators, tinfos, error); + } else { + return ParseJingleTransportInfos( + action_elem, contents, trans_parsers, translators, tinfos, error); + } +} + +bool WriteTransportInfos(SignalingProtocol protocol, + const TransportInfos& tinfos, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + XmlElements* elems, + WriteError* error) { + if (protocol == PROTOCOL_GINGLE) { + return WriteGingleTransportInfos(tinfos, trans_parsers, translators, + elems, error); + } else { + return WriteJingleTransportInfos(tinfos, trans_parsers, translators, + elems, error); + } +} + +bool GetUriTarget(const std::string& prefix, const std::string& str, + std::string* after) { + size_t pos = str.find(prefix); + if (pos == std::string::npos) + return false; + + *after = str.substr(pos + prefix.size(), std::string::npos); + return true; +} + +bool FindSessionRedirect(const buzz::XmlElement* stanza, + SessionRedirect* redirect) { + const buzz::XmlElement* error_elem = GetXmlChild(stanza, LN_ERROR); + if (error_elem == NULL) + return false; + + const buzz::XmlElement* redirect_elem = + error_elem->FirstNamed(QN_GINGLE_REDIRECT); + if (redirect_elem == NULL) + redirect_elem = error_elem->FirstNamed(buzz::QN_STANZA_REDIRECT); + if (redirect_elem == NULL) + return false; + + if (!GetUriTarget(STR_REDIRECT_PREFIX, redirect_elem->BodyText(), + &redirect->target)) + return false; + + return true; +} + +} // namespace cricket diff --git a/talk/p2p/base/sessionmessages.h b/talk/p2p/base/sessionmessages.h new file mode 100644 index 000000000..5cd565c42 --- /dev/null +++ b/talk/p2p/base/sessionmessages.h @@ -0,0 +1,243 @@ +/* + * libjingle + * Copyright 2010, 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. + */ + +#ifndef TALK_P2P_BASE_SESSIONMESSAGES_H_ +#define TALK_P2P_BASE_SESSIONMESSAGES_H_ + +#include +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/parsing.h" +#include "talk/p2p/base/sessiondescription.h" // Needed to delete contents. +#include "talk/p2p/base/transportinfo.h" +#include "talk/xmllite/xmlelement.h" + +namespace cricket { + +struct ParseError; +struct WriteError; +class Candidate; +class ContentParser; +class TransportParser; + +typedef std::vector Candidates; +typedef std::map ContentParserMap; +typedef std::map TransportParserMap; + +enum ActionType { + ACTION_UNKNOWN, + + ACTION_SESSION_INITIATE, + ACTION_SESSION_INFO, + ACTION_SESSION_ACCEPT, + ACTION_SESSION_REJECT, + ACTION_SESSION_TERMINATE, + + ACTION_TRANSPORT_INFO, + ACTION_TRANSPORT_ACCEPT, + + ACTION_DESCRIPTION_INFO, +}; + +// Abstraction of a element within an stanza, per XMPP +// standard XEP-166. Can be serialized into multiple protocols, +// including the standard (Jingle) and the draft standard (Gingle). +// In general, used to communicate actions related to a p2p session, +// such accept, initiate, terminate, etc. + +struct SessionMessage { + SessionMessage() : action_elem(NULL), stanza(NULL) {} + + SessionMessage(SignalingProtocol protocol, ActionType type, + const std::string& sid, const std::string& initiator) : + protocol(protocol), type(type), sid(sid), initiator(initiator), + action_elem(NULL), stanza(NULL) {} + + std::string id; + std::string from; + std::string to; + SignalingProtocol protocol; + ActionType type; + std::string sid; // session id + std::string initiator; + + // Used for further parsing when necessary. + // Represents or . + const buzz::XmlElement* action_elem; + // Mostly used for debugging. + const buzz::XmlElement* stanza; +}; + +// TODO: Break up this class so we don't have to typedef it into +// different classes. +struct ContentMessage { + ContentMessage() : owns_contents(false) {} + + ~ContentMessage() { + if (owns_contents) { + for (ContentInfos::iterator content = contents.begin(); + content != contents.end(); content++) { + delete content->description; + } + } + } + + // Caller takes ownership of contents. + ContentInfos ClearContents() { + ContentInfos out; + contents.swap(out); + owns_contents = false; + return out; + } + + bool owns_contents; + ContentInfos contents; + TransportInfos transports; + ContentGroups groups; +}; + +typedef ContentMessage SessionInitiate; +typedef ContentMessage SessionAccept; +// Note that a DescriptionInfo does not have TransportInfos. +typedef ContentMessage DescriptionInfo; + +struct SessionTerminate { + SessionTerminate() {} + + explicit SessionTerminate(const std::string& reason) : + reason(reason) {} + + std::string reason; + std::string debug_reason; +}; + +struct SessionRedirect { + std::string target; +}; + +// Used during parsing and writing to map component to channel name +// and back. This is primarily for converting old G-ICE candidate +// signalling to new ICE candidate classes. +class CandidateTranslator { + public: + virtual bool GetChannelNameFromComponent( + int component, std::string* channel_name) const = 0; + virtual bool GetComponentFromChannelName( + const std::string& channel_name, int* component) const = 0; +}; + +// Content name => translator +typedef std::map CandidateTranslatorMap; + +bool IsSessionMessage(const buzz::XmlElement* stanza); +bool ParseSessionMessage(const buzz::XmlElement* stanza, + SessionMessage* msg, + ParseError* error); +// Will return an error if there is more than one content type. +bool ParseContentType(SignalingProtocol protocol, + const buzz::XmlElement* action_elem, + std::string* content_type, + ParseError* error); +void WriteSessionMessage(const SessionMessage& msg, + const XmlElements& action_elems, + buzz::XmlElement* stanza); +bool ParseSessionInitiate(SignalingProtocol protocol, + const buzz::XmlElement* action_elem, + const ContentParserMap& content_parsers, + const TransportParserMap& transport_parsers, + const CandidateTranslatorMap& translators, + SessionInitiate* init, + ParseError* error); +bool WriteSessionInitiate(SignalingProtocol protocol, + const ContentInfos& contents, + const TransportInfos& tinfos, + const ContentParserMap& content_parsers, + const TransportParserMap& transport_parsers, + const CandidateTranslatorMap& translators, + const ContentGroups& groups, + XmlElements* elems, + WriteError* error); +bool ParseSessionAccept(SignalingProtocol protocol, + const buzz::XmlElement* action_elem, + const ContentParserMap& content_parsers, + const TransportParserMap& transport_parsers, + const CandidateTranslatorMap& translators, + SessionAccept* accept, + ParseError* error); +bool WriteSessionAccept(SignalingProtocol protocol, + const ContentInfos& contents, + const TransportInfos& tinfos, + const ContentParserMap& content_parsers, + const TransportParserMap& transport_parsers, + const CandidateTranslatorMap& translators, + const ContentGroups& groups, + XmlElements* elems, + WriteError* error); +bool ParseSessionTerminate(SignalingProtocol protocol, + const buzz::XmlElement* action_elem, + SessionTerminate* term, + ParseError* error); +void WriteSessionTerminate(SignalingProtocol protocol, + const SessionTerminate& term, + XmlElements* elems); +bool ParseDescriptionInfo(SignalingProtocol protocol, + const buzz::XmlElement* action_elem, + const ContentParserMap& content_parsers, + const TransportParserMap& transport_parsers, + const CandidateTranslatorMap& translators, + DescriptionInfo* description_info, + ParseError* error); +bool WriteDescriptionInfo(SignalingProtocol protocol, + const ContentInfos& contents, + const ContentParserMap& content_parsers, + XmlElements* elems, + WriteError* error); +// Since a TransportInfo is not a transport-info message, and a +// transport-info message is just a collection of TransportInfos, we +// say Parse/Write TransportInfos for transport-info messages. +bool ParseTransportInfos(SignalingProtocol protocol, + const buzz::XmlElement* action_elem, + const ContentInfos& contents, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + TransportInfos* tinfos, + ParseError* error); +bool WriteTransportInfos(SignalingProtocol protocol, + const TransportInfos& tinfos, + const TransportParserMap& trans_parsers, + const CandidateTranslatorMap& translators, + XmlElements* elems, + WriteError* error); +// Handles both Gingle and Jingle syntax. +bool FindSessionRedirect(const buzz::XmlElement* stanza, + SessionRedirect* redirect); +} // namespace cricket + +#endif // TALK_P2P_BASE_SESSIONMESSAGES_H_ diff --git a/talk/p2p/base/stun.cc b/talk/p2p/base/stun.cc new file mode 100644 index 000000000..06a71a15a --- /dev/null +++ b/talk/p2p/base/stun.cc @@ -0,0 +1,928 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/stun.h" + +#include + +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/crc32.h" +#include "talk/base/logging.h" +#include "talk/base/messagedigest.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringencode.h" + +using talk_base::ByteBuffer; + +namespace cricket { + +const char STUN_ERROR_REASON_BAD_REQUEST[] = "Bad Request"; +const char STUN_ERROR_REASON_UNAUTHORIZED[] = "Unauthorized"; +const char STUN_ERROR_REASON_FORBIDDEN[] = "Forbidden"; +const char STUN_ERROR_REASON_STALE_CREDENTIALS[] = "Stale Credentials"; +const char STUN_ERROR_REASON_ALLOCATION_MISMATCH[] = "Allocation Mismatch"; +const char STUN_ERROR_REASON_STALE_NONCE[] = "Stale Nonce"; +const char STUN_ERROR_REASON_WRONG_CREDENTIALS[] = "Wrong Credentials"; +const char STUN_ERROR_REASON_UNSUPPORTED_PROTOCOL[] = "Unsupported Protocol"; +const char STUN_ERROR_REASON_ROLE_CONFLICT[] = "Role Conflict"; +const char STUN_ERROR_REASON_SERVER_ERROR[] = "Server Error"; + +const char TURN_MAGIC_COOKIE_VALUE[] = { '\x72', '\xC6', '\x4B', '\xC6' }; +const char EMPTY_TRANSACTION_ID[] = "0000000000000000"; +const uint32 STUN_FINGERPRINT_XOR_VALUE = 0x5354554E; + +// StunMessage + +StunMessage::StunMessage() + : type_(0), + length_(0), + transaction_id_(EMPTY_TRANSACTION_ID) { + ASSERT(IsValidTransactionId(transaction_id_)); + attrs_ = new std::vector(); +} + +StunMessage::~StunMessage() { + for (size_t i = 0; i < attrs_->size(); i++) + delete (*attrs_)[i]; + delete attrs_; +} + +bool StunMessage::IsLegacy() const { + if (transaction_id_.size() == kStunLegacyTransactionIdLength) + return true; + ASSERT(transaction_id_.size() == kStunTransactionIdLength); + return false; +} + +bool StunMessage::SetTransactionID(const std::string& str) { + if (!IsValidTransactionId(str)) { + return false; + } + transaction_id_ = str; + return true; +} + +bool StunMessage::AddAttribute(StunAttribute* attr) { + // Fail any attributes that aren't valid for this type of message. + if (attr->value_type() != GetAttributeValueType(attr->type())) { + return false; + } + attrs_->push_back(attr); + attr->SetOwner(this); + size_t attr_length = attr->length(); + if (attr_length % 4 != 0) { + attr_length += (4 - (attr_length % 4)); + } + length_ += attr_length + 4; + return true; +} + +const StunAddressAttribute* StunMessage::GetAddress(int type) const { + switch (type) { + case STUN_ATTR_MAPPED_ADDRESS: { + // Return XOR-MAPPED-ADDRESS when MAPPED-ADDRESS attribute is + // missing. + const StunAttribute* mapped_address = + GetAttribute(STUN_ATTR_MAPPED_ADDRESS); + if (!mapped_address) + mapped_address = GetAttribute(STUN_ATTR_XOR_MAPPED_ADDRESS); + return reinterpret_cast(mapped_address); + } + + default: + return static_cast(GetAttribute(type)); + } +} + +const StunUInt32Attribute* StunMessage::GetUInt32(int type) const { + return static_cast(GetAttribute(type)); +} + +const StunUInt64Attribute* StunMessage::GetUInt64(int type) const { + return static_cast(GetAttribute(type)); +} + +const StunByteStringAttribute* StunMessage::GetByteString(int type) const { + return static_cast(GetAttribute(type)); +} + +const StunErrorCodeAttribute* StunMessage::GetErrorCode() const { + return static_cast( + GetAttribute(STUN_ATTR_ERROR_CODE)); +} + +const StunUInt16ListAttribute* StunMessage::GetUnknownAttributes() const { + return static_cast( + GetAttribute(STUN_ATTR_UNKNOWN_ATTRIBUTES)); +} + +// Verifies a STUN message has a valid MESSAGE-INTEGRITY attribute, using the +// procedure outlined in RFC 5389, section 15.4. +bool StunMessage::ValidateMessageIntegrity(const char* data, size_t size, + const std::string& password) { + // Verifying the size of the message. + if ((size % 4) != 0) { + return false; + } + + // Getting the message length from the STUN header. + uint16 msg_length = talk_base::GetBE16(&data[2]); + if (size != (msg_length + kStunHeaderSize)) { + return false; + } + + // Finding Message Integrity attribute in stun message. + size_t current_pos = kStunHeaderSize; + bool has_message_integrity_attr = false; + while (current_pos < size) { + uint16 attr_type, attr_length; + // Getting attribute type and length. + attr_type = talk_base::GetBE16(&data[current_pos]); + attr_length = talk_base::GetBE16(&data[current_pos + sizeof(attr_type)]); + + // If M-I, sanity check it, and break out. + if (attr_type == STUN_ATTR_MESSAGE_INTEGRITY) { + if (attr_length != kStunMessageIntegritySize || + current_pos + attr_length > size) { + return false; + } + has_message_integrity_attr = true; + break; + } + + // Otherwise, skip to the next attribute. + current_pos += sizeof(attr_type) + sizeof(attr_length) + attr_length; + if ((attr_length % 4) != 0) { + current_pos += (4 - (attr_length % 4)); + } + } + + if (!has_message_integrity_attr) { + return false; + } + + // Getting length of the message to calculate Message Integrity. + size_t mi_pos = current_pos; + talk_base::scoped_array temp_data(new char[current_pos]); + memcpy(temp_data.get(), data, current_pos); + if (size > mi_pos + kStunAttributeHeaderSize + kStunMessageIntegritySize) { + // Stun message has other attributes after message integrity. + // Adjust the length parameter in stun message to calculate HMAC. + size_t extra_offset = size - + (mi_pos + kStunAttributeHeaderSize + kStunMessageIntegritySize); + size_t new_adjusted_len = size - extra_offset - kStunHeaderSize; + + // Writing new length of the STUN message @ Message Length in temp buffer. + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // |0 0| STUN Message Type | Message Length | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + talk_base::SetBE16(temp_data.get() + 2, new_adjusted_len); + } + + char hmac[kStunMessageIntegritySize]; + size_t ret = talk_base::ComputeHmac(talk_base::DIGEST_SHA_1, + password.c_str(), password.size(), + temp_data.get(), mi_pos, + hmac, sizeof(hmac)); + ASSERT(ret == sizeof(hmac)); + if (ret != sizeof(hmac)) + return false; + + // Comparing the calculated HMAC with the one present in the message. + return (std::memcmp(data + current_pos + kStunAttributeHeaderSize, + hmac, sizeof(hmac)) == 0); +} + +bool StunMessage::AddMessageIntegrity(const std::string& password) { + return AddMessageIntegrity(password.c_str(), password.size()); +} + +bool StunMessage::AddMessageIntegrity(const char* key, + size_t keylen) { + // Add the attribute with a dummy value. Since this is a known attribute, it + // can't fail. + StunByteStringAttribute* msg_integrity_attr = + new StunByteStringAttribute(STUN_ATTR_MESSAGE_INTEGRITY, + std::string(kStunMessageIntegritySize, '0')); + VERIFY(AddAttribute(msg_integrity_attr)); + + // Calculate the HMAC for the message. + talk_base::ByteBuffer buf; + if (!Write(&buf)) + return false; + + int msg_len_for_hmac = buf.Length() - + kStunAttributeHeaderSize - msg_integrity_attr->length(); + char hmac[kStunMessageIntegritySize]; + size_t ret = talk_base::ComputeHmac(talk_base::DIGEST_SHA_1, + key, keylen, + buf.Data(), msg_len_for_hmac, + hmac, sizeof(hmac)); + ASSERT(ret == sizeof(hmac)); + if (ret != sizeof(hmac)) { + LOG(LS_ERROR) << "HMAC computation failed. Message-Integrity " + << "has dummy value."; + return false; + } + + // Insert correct HMAC into the attribute. + msg_integrity_attr->CopyBytes(hmac, sizeof(hmac)); + return true; +} + +// Verifies a message is in fact a STUN message, by performing the checks +// outlined in RFC 5389, section 7.3, including the FINGERPRINT check detailed +// in section 15.5. +bool StunMessage::ValidateFingerprint(const char* data, size_t size) { + // Check the message length. + size_t fingerprint_attr_size = + kStunAttributeHeaderSize + StunUInt32Attribute::SIZE; + if (size % 4 != 0 || size < kStunHeaderSize + fingerprint_attr_size) + return false; + + // Skip the rest if the magic cookie isn't present. + const char* magic_cookie = + data + kStunTransactionIdOffset - kStunMagicCookieLength; + if (talk_base::GetBE32(magic_cookie) != kStunMagicCookie) + return false; + + // Check the fingerprint type and length. + const char* fingerprint_attr_data = data + size - fingerprint_attr_size; + if (talk_base::GetBE16(fingerprint_attr_data) != STUN_ATTR_FINGERPRINT || + talk_base::GetBE16(fingerprint_attr_data + sizeof(uint16)) != + StunUInt32Attribute::SIZE) + return false; + + // Check the fingerprint value. + uint32 fingerprint = + talk_base::GetBE32(fingerprint_attr_data + kStunAttributeHeaderSize); + return ((fingerprint ^ STUN_FINGERPRINT_XOR_VALUE) == + talk_base::ComputeCrc32(data, size - fingerprint_attr_size)); +} + +bool StunMessage::AddFingerprint() { + // Add the attribute with a dummy value. Since this is a known attribute, + // it can't fail. + StunUInt32Attribute* fingerprint_attr = + new StunUInt32Attribute(STUN_ATTR_FINGERPRINT, 0); + VERIFY(AddAttribute(fingerprint_attr)); + + // Calculate the CRC-32 for the message and insert it. + talk_base::ByteBuffer buf; + if (!Write(&buf)) + return false; + + int msg_len_for_crc32 = buf.Length() - + kStunAttributeHeaderSize - fingerprint_attr->length(); + uint32 c = talk_base::ComputeCrc32(buf.Data(), msg_len_for_crc32); + + // Insert the correct CRC-32, XORed with a constant, into the attribute. + fingerprint_attr->SetValue(c ^ STUN_FINGERPRINT_XOR_VALUE); + return true; +} + +bool StunMessage::Read(ByteBuffer* buf) { + if (!buf->ReadUInt16(&type_)) + return false; + + if (type_ & 0x8000) { + // RTP and RTCP set the MSB of first byte, since first two bits are version, + // and version is always 2 (10). If set, this is not a STUN packet. + return false; + } + + if (!buf->ReadUInt16(&length_)) + return false; + + std::string magic_cookie; + if (!buf->ReadString(&magic_cookie, kStunMagicCookieLength)) + return false; + + std::string transaction_id; + if (!buf->ReadString(&transaction_id, kStunTransactionIdLength)) + return false; + + uint32 magic_cookie_int = + *reinterpret_cast(magic_cookie.data()); + if (talk_base::NetworkToHost32(magic_cookie_int) != kStunMagicCookie) { + // If magic cookie is invalid it means that the peer implements + // RFC3489 instead of RFC5389. + transaction_id.insert(0, magic_cookie); + } + ASSERT(IsValidTransactionId(transaction_id)); + transaction_id_ = transaction_id; + + if (length_ != buf->Length()) + return false; + + attrs_->resize(0); + + size_t rest = buf->Length() - length_; + while (buf->Length() > rest) { + uint16 attr_type, attr_length; + if (!buf->ReadUInt16(&attr_type)) + return false; + if (!buf->ReadUInt16(&attr_length)) + return false; + + StunAttribute* attr = CreateAttribute(attr_type, attr_length); + if (!attr) { + // Skip any unknown or malformed attributes. + if ((attr_length % 4) != 0) { + attr_length += (4 - (attr_length % 4)); + } + if (!buf->Consume(attr_length)) + return false; + } else { + if (!attr->Read(buf)) + return false; + attrs_->push_back(attr); + } + } + + ASSERT(buf->Length() == rest); + return true; +} + +bool StunMessage::Write(ByteBuffer* buf) const { + buf->WriteUInt16(type_); + buf->WriteUInt16(length_); + if (!IsLegacy()) + buf->WriteUInt32(kStunMagicCookie); + buf->WriteString(transaction_id_); + + for (size_t i = 0; i < attrs_->size(); ++i) { + buf->WriteUInt16((*attrs_)[i]->type()); + buf->WriteUInt16((*attrs_)[i]->length()); + if (!(*attrs_)[i]->Write(buf)) + return false; + } + + return true; +} + +StunAttributeValueType StunMessage::GetAttributeValueType(int type) const { + switch (type) { + case STUN_ATTR_MAPPED_ADDRESS: return STUN_VALUE_ADDRESS; + case STUN_ATTR_USERNAME: return STUN_VALUE_BYTE_STRING; + case STUN_ATTR_MESSAGE_INTEGRITY: return STUN_VALUE_BYTE_STRING; + case STUN_ATTR_ERROR_CODE: return STUN_VALUE_ERROR_CODE; + case STUN_ATTR_UNKNOWN_ATTRIBUTES: return STUN_VALUE_UINT16_LIST; + case STUN_ATTR_REALM: return STUN_VALUE_BYTE_STRING; + case STUN_ATTR_NONCE: return STUN_VALUE_BYTE_STRING; + case STUN_ATTR_XOR_MAPPED_ADDRESS: return STUN_VALUE_XOR_ADDRESS; + case STUN_ATTR_SOFTWARE: return STUN_VALUE_BYTE_STRING; + case STUN_ATTR_ALTERNATE_SERVER: return STUN_VALUE_BYTE_STRING; + case STUN_ATTR_FINGERPRINT: return STUN_VALUE_UINT32; + case STUN_ATTR_RETRANSMIT_COUNT: return STUN_VALUE_UINT32; + default: return STUN_VALUE_UNKNOWN; + } +} + +StunAttribute* StunMessage::CreateAttribute(int type, size_t length) /*const*/ { + StunAttributeValueType value_type = GetAttributeValueType(type); + return StunAttribute::Create(value_type, type, length, this); +} + +const StunAttribute* StunMessage::GetAttribute(int type) const { + for (size_t i = 0; i < attrs_->size(); ++i) { + if ((*attrs_)[i]->type() == type) + return (*attrs_)[i]; + } + return NULL; +} + +bool StunMessage::IsValidTransactionId(const std::string& transaction_id) { + return transaction_id.size() == kStunTransactionIdLength || + transaction_id.size() == kStunLegacyTransactionIdLength; +} + +// StunAttribute + +StunAttribute::StunAttribute(uint16 type, uint16 length) + : type_(type), length_(length) { +} + +void StunAttribute::ConsumePadding(talk_base::ByteBuffer* buf) const { + int remainder = length_ % 4; + if (remainder > 0) { + buf->Consume(4 - remainder); + } +} + +void StunAttribute::WritePadding(talk_base::ByteBuffer* buf) const { + int remainder = length_ % 4; + if (remainder > 0) { + char zeroes[4] = {0}; + buf->WriteBytes(zeroes, 4 - remainder); + } +} + +StunAttribute* StunAttribute::Create(StunAttributeValueType value_type, + uint16 type, uint16 length, + StunMessage* owner) { + switch (value_type) { + case STUN_VALUE_ADDRESS: + return new StunAddressAttribute(type, length); + case STUN_VALUE_XOR_ADDRESS: + return new StunXorAddressAttribute(type, length, owner); + case STUN_VALUE_UINT32: + return new StunUInt32Attribute(type); + case STUN_VALUE_UINT64: + return new StunUInt64Attribute(type); + case STUN_VALUE_BYTE_STRING: + return new StunByteStringAttribute(type, length); + case STUN_VALUE_ERROR_CODE: + return new StunErrorCodeAttribute(type, length); + case STUN_VALUE_UINT16_LIST: + return new StunUInt16ListAttribute(type, length); + default: + return NULL; + } +} + +StunAddressAttribute* StunAttribute::CreateAddress(uint16 type) { + return new StunAddressAttribute(type, 0); +} + +StunXorAddressAttribute* StunAttribute::CreateXorAddress(uint16 type) { + return new StunXorAddressAttribute(type, 0, NULL); +} + +StunUInt64Attribute* StunAttribute::CreateUInt64(uint16 type) { + return new StunUInt64Attribute(type); +} + +StunUInt32Attribute* StunAttribute::CreateUInt32(uint16 type) { + return new StunUInt32Attribute(type); +} + +StunByteStringAttribute* StunAttribute::CreateByteString(uint16 type) { + return new StunByteStringAttribute(type, 0); +} + +StunErrorCodeAttribute* StunAttribute::CreateErrorCode() { + return new StunErrorCodeAttribute( + STUN_ATTR_ERROR_CODE, StunErrorCodeAttribute::MIN_SIZE); +} + +StunUInt16ListAttribute* StunAttribute::CreateUnknownAttributes() { + return new StunUInt16ListAttribute(STUN_ATTR_UNKNOWN_ATTRIBUTES, 0); +} + +StunAddressAttribute::StunAddressAttribute(uint16 type, + const talk_base::SocketAddress& addr) + : StunAttribute(type, 0) { + SetAddress(addr); +} + +StunAddressAttribute::StunAddressAttribute(uint16 type, uint16 length) + : StunAttribute(type, length) { +} + +bool StunAddressAttribute::Read(ByteBuffer* buf) { + uint8 dummy; + if (!buf->ReadUInt8(&dummy)) + return false; + + uint8 stun_family; + if (!buf->ReadUInt8(&stun_family)) { + return false; + } + uint16 port; + if (!buf->ReadUInt16(&port)) + return false; + if (stun_family == STUN_ADDRESS_IPV4) { + in_addr v4addr; + if (length() != SIZE_IP4) { + return false; + } + if (!buf->ReadBytes(reinterpret_cast(&v4addr), sizeof(v4addr))) { + return false; + } + talk_base::IPAddress ipaddr(v4addr); + SetAddress(talk_base::SocketAddress(ipaddr, port)); + } else if (stun_family == STUN_ADDRESS_IPV6) { + in6_addr v6addr; + if (length() != SIZE_IP6) { + return false; + } + if (!buf->ReadBytes(reinterpret_cast(&v6addr), sizeof(v6addr))) { + return false; + } + talk_base::IPAddress ipaddr(v6addr); + SetAddress(talk_base::SocketAddress(ipaddr, port)); + } else { + return false; + } + return true; +} + +bool StunAddressAttribute::Write(ByteBuffer* buf) const { + StunAddressFamily address_family = family(); + if (address_family == STUN_ADDRESS_UNDEF) { + LOG(LS_ERROR) << "Error writing address attribute: unknown family."; + return false; + } + buf->WriteUInt8(0); + buf->WriteUInt8(address_family); + buf->WriteUInt16(address_.port()); + switch (address_.family()) { + case AF_INET: { + in_addr v4addr = address_.ipaddr().ipv4_address(); + buf->WriteBytes(reinterpret_cast(&v4addr), sizeof(v4addr)); + break; + } + case AF_INET6: { + in6_addr v6addr = address_.ipaddr().ipv6_address(); + buf->WriteBytes(reinterpret_cast(&v6addr), sizeof(v6addr)); + break; + } + } + return true; +} + +StunXorAddressAttribute::StunXorAddressAttribute(uint16 type, + const talk_base::SocketAddress& addr) + : StunAddressAttribute(type, addr), owner_(NULL) { +} + +StunXorAddressAttribute::StunXorAddressAttribute(uint16 type, + uint16 length, + StunMessage* owner) + : StunAddressAttribute(type, length), owner_(owner) {} + +talk_base::IPAddress StunXorAddressAttribute::GetXoredIP() const { + if (owner_) { + talk_base::IPAddress ip = ipaddr(); + switch (ip.family()) { + case AF_INET: { + in_addr v4addr = ip.ipv4_address(); + v4addr.s_addr = + (v4addr.s_addr ^ talk_base::HostToNetwork32(kStunMagicCookie)); + return talk_base::IPAddress(v4addr); + } + case AF_INET6: { + in6_addr v6addr = ip.ipv6_address(); + const std::string& transaction_id = owner_->transaction_id(); + if (transaction_id.length() == kStunTransactionIdLength) { + uint32 transactionid_as_ints[3]; + memcpy(&transactionid_as_ints[0], transaction_id.c_str(), + transaction_id.length()); + uint32* ip_as_ints = reinterpret_cast(&v6addr.s6_addr); + // Transaction ID is in network byte order, but magic cookie + // is stored in host byte order. + ip_as_ints[0] = + (ip_as_ints[0] ^ talk_base::HostToNetwork32(kStunMagicCookie)); + ip_as_ints[1] = (ip_as_ints[1] ^ transactionid_as_ints[0]); + ip_as_ints[2] = (ip_as_ints[2] ^ transactionid_as_ints[1]); + ip_as_ints[3] = (ip_as_ints[3] ^ transactionid_as_ints[2]); + return talk_base::IPAddress(v6addr); + } + break; + } + } + } + // Invalid ip family or transaction ID, or missing owner. + // Return an AF_UNSPEC address. + return talk_base::IPAddress(); +} + +bool StunXorAddressAttribute::Read(ByteBuffer* buf) { + if (!StunAddressAttribute::Read(buf)) + return false; + uint16 xoredport = port() ^ (kStunMagicCookie >> 16); + talk_base::IPAddress xored_ip = GetXoredIP(); + SetAddress(talk_base::SocketAddress(xored_ip, xoredport)); + return true; +} + +bool StunXorAddressAttribute::Write(ByteBuffer* buf) const { + StunAddressFamily address_family = family(); + if (address_family == STUN_ADDRESS_UNDEF) { + LOG(LS_ERROR) << "Error writing xor-address attribute: unknown family."; + return false; + } + talk_base::IPAddress xored_ip = GetXoredIP(); + if (xored_ip.family() == AF_UNSPEC) { + return false; + } + buf->WriteUInt8(0); + buf->WriteUInt8(family()); + buf->WriteUInt16(port() ^ (kStunMagicCookie >> 16)); + switch (xored_ip.family()) { + case AF_INET: { + in_addr v4addr = xored_ip.ipv4_address(); + buf->WriteBytes(reinterpret_cast(&v4addr), sizeof(v4addr)); + break; + } + case AF_INET6: { + in6_addr v6addr = xored_ip.ipv6_address(); + buf->WriteBytes(reinterpret_cast(&v6addr), sizeof(v6addr)); + break; + } + } + return true; +} + +StunUInt32Attribute::StunUInt32Attribute(uint16 type, uint32 value) + : StunAttribute(type, SIZE), bits_(value) { +} + +StunUInt32Attribute::StunUInt32Attribute(uint16 type) + : StunAttribute(type, SIZE), bits_(0) { +} + +bool StunUInt32Attribute::GetBit(size_t index) const { + ASSERT(index < 32); + return static_cast((bits_ >> index) & 0x1); +} + +void StunUInt32Attribute::SetBit(size_t index, bool value) { + ASSERT(index < 32); + bits_ &= ~(1 << index); + bits_ |= value ? (1 << index) : 0; +} + +bool StunUInt32Attribute::Read(ByteBuffer* buf) { + if (length() != SIZE || !buf->ReadUInt32(&bits_)) + return false; + return true; +} + +bool StunUInt32Attribute::Write(ByteBuffer* buf) const { + buf->WriteUInt32(bits_); + return true; +} + +StunUInt64Attribute::StunUInt64Attribute(uint16 type, uint64 value) + : StunAttribute(type, SIZE), bits_(value) { +} + +StunUInt64Attribute::StunUInt64Attribute(uint16 type) + : StunAttribute(type, SIZE), bits_(0) { +} + +bool StunUInt64Attribute::Read(ByteBuffer* buf) { + if (length() != SIZE || !buf->ReadUInt64(&bits_)) + return false; + return true; +} + +bool StunUInt64Attribute::Write(ByteBuffer* buf) const { + buf->WriteUInt64(bits_); + return true; +} + +StunByteStringAttribute::StunByteStringAttribute(uint16 type) + : StunAttribute(type, 0), bytes_(NULL) { +} + +StunByteStringAttribute::StunByteStringAttribute(uint16 type, + const std::string& str) + : StunAttribute(type, 0), bytes_(NULL) { + CopyBytes(str.c_str(), str.size()); +} + +StunByteStringAttribute::StunByteStringAttribute(uint16 type, + const void* bytes, + size_t length) + : StunAttribute(type, 0), bytes_(NULL) { + CopyBytes(bytes, length); +} + +StunByteStringAttribute::StunByteStringAttribute(uint16 type, uint16 length) + : StunAttribute(type, length), bytes_(NULL) { +} + +StunByteStringAttribute::~StunByteStringAttribute() { + delete [] bytes_; +} + +void StunByteStringAttribute::CopyBytes(const char* bytes) { + CopyBytes(bytes, strlen(bytes)); +} + +void StunByteStringAttribute::CopyBytes(const void* bytes, size_t length) { + char* new_bytes = new char[length]; + std::memcpy(new_bytes, bytes, length); + SetBytes(new_bytes, length); +} + +uint8 StunByteStringAttribute::GetByte(size_t index) const { + ASSERT(bytes_ != NULL); + ASSERT(index < length()); + return static_cast(bytes_[index]); +} + +void StunByteStringAttribute::SetByte(size_t index, uint8 value) { + ASSERT(bytes_ != NULL); + ASSERT(index < length()); + bytes_[index] = value; +} + +bool StunByteStringAttribute::Read(ByteBuffer* buf) { + bytes_ = new char[length()]; + if (!buf->ReadBytes(bytes_, length())) { + return false; + } + + ConsumePadding(buf); + return true; +} + +bool StunByteStringAttribute::Write(ByteBuffer* buf) const { + buf->WriteBytes(bytes_, length()); + WritePadding(buf); + return true; +} + +void StunByteStringAttribute::SetBytes(char* bytes, size_t length) { + delete [] bytes_; + bytes_ = bytes; + SetLength(length); +} + +StunErrorCodeAttribute::StunErrorCodeAttribute(uint16 type, int code, + const std::string& reason) + : StunAttribute(type, 0) { + SetCode(code); + SetReason(reason); +} + +StunErrorCodeAttribute::StunErrorCodeAttribute(uint16 type, uint16 length) + : StunAttribute(type, length), class_(0), number_(0) { +} + +StunErrorCodeAttribute::~StunErrorCodeAttribute() { +} + +int StunErrorCodeAttribute::code() const { + return class_ * 100 + number_; +} + +void StunErrorCodeAttribute::SetCode(int code) { + class_ = static_cast(code / 100); + number_ = static_cast(code % 100); +} + +void StunErrorCodeAttribute::SetReason(const std::string& reason) { + SetLength(MIN_SIZE + static_cast(reason.size())); + reason_ = reason; +} + +bool StunErrorCodeAttribute::Read(ByteBuffer* buf) { + uint32 val; + if (length() < MIN_SIZE || !buf->ReadUInt32(&val)) + return false; + + if ((val >> 11) != 0) + LOG(LS_ERROR) << "error-code bits not zero"; + + class_ = ((val >> 8) & 0x7); + number_ = (val & 0xff); + + if (!buf->ReadString(&reason_, length() - 4)) + return false; + + ConsumePadding(buf); + return true; +} + +bool StunErrorCodeAttribute::Write(ByteBuffer* buf) const { + buf->WriteUInt32(class_ << 8 | number_); + buf->WriteString(reason_); + WritePadding(buf); + return true; +} + +StunUInt16ListAttribute::StunUInt16ListAttribute(uint16 type, uint16 length) + : StunAttribute(type, length) { + attr_types_ = new std::vector(); +} + +StunUInt16ListAttribute::~StunUInt16ListAttribute() { + delete attr_types_; +} + +size_t StunUInt16ListAttribute::Size() const { + return attr_types_->size(); +} + +uint16 StunUInt16ListAttribute::GetType(int index) const { + return (*attr_types_)[index]; +} + +void StunUInt16ListAttribute::SetType(int index, uint16 value) { + (*attr_types_)[index] = value; +} + +void StunUInt16ListAttribute::AddType(uint16 value) { + attr_types_->push_back(value); + SetLength(static_cast(attr_types_->size() * 2)); +} + +bool StunUInt16ListAttribute::Read(ByteBuffer* buf) { + if (length() % 2) + return false; + + for (size_t i = 0; i < length() / 2; i++) { + uint16 attr; + if (!buf->ReadUInt16(&attr)) + return false; + attr_types_->push_back(attr); + } + // Padding of these attributes is done in RFC 5389 style. This is + // slightly different from RFC3489, but it shouldn't be important. + // RFC3489 pads out to a 32 bit boundary by duplicating one of the + // entries in the list (not necessarily the last one - it's unspecified). + // RFC5389 pads on the end, and the bytes are always ignored. + ConsumePadding(buf); + return true; +} + +bool StunUInt16ListAttribute::Write(ByteBuffer* buf) const { + for (size_t i = 0; i < attr_types_->size(); ++i) { + buf->WriteUInt16((*attr_types_)[i]); + } + WritePadding(buf); + return true; +} + +int GetStunSuccessResponseType(int req_type) { + return IsStunRequestType(req_type) ? (req_type | 0x100) : -1; +} + +int GetStunErrorResponseType(int req_type) { + return IsStunRequestType(req_type) ? (req_type | 0x110) : -1; +} + +bool IsStunRequestType(int msg_type) { + return ((msg_type & kStunTypeMask) == 0x000); +} + +bool IsStunIndicationType(int msg_type) { + return ((msg_type & kStunTypeMask) == 0x010); +} + +bool IsStunSuccessResponseType(int msg_type) { + return ((msg_type & kStunTypeMask) == 0x100); +} + +bool IsStunErrorResponseType(int msg_type) { + return ((msg_type & kStunTypeMask) == 0x110); +} + +bool ComputeStunCredentialHash(const std::string& username, + const std::string& realm, + const std::string& password, + std::string* hash) { + // http://tools.ietf.org/html/rfc5389#section-15.4 + // long-term credentials will be calculated using the key and key is + // key = MD5(username ":" realm ":" SASLprep(password)) + std::string input = username; + input += ':'; + input += realm; + input += ':'; + input += password; + + char digest[talk_base::MessageDigest::kMaxSize]; + size_t size = talk_base::ComputeDigest( + talk_base::DIGEST_MD5, input.c_str(), input.size(), + digest, sizeof(digest)); + if (size == 0) { + return false; + } + + *hash = std::string(digest, size); + return true; +} + +} // namespace cricket diff --git a/talk/p2p/base/stun.h b/talk/p2p/base/stun.h new file mode 100644 index 000000000..6416e5156 --- /dev/null +++ b/talk/p2p/base/stun.h @@ -0,0 +1,648 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_STUN_H_ +#define TALK_P2P_BASE_STUN_H_ + +// This file contains classes for dealing with the STUN protocol, as specified +// in RFC 5389, and its descendants. + +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/bytebuffer.h" +#include "talk/base/socketaddress.h" + +namespace cricket { + +// These are the types of STUN messages defined in RFC 5389. +enum StunMessageType { + STUN_BINDING_REQUEST = 0x0001, + STUN_BINDING_INDICATION = 0x0011, + STUN_BINDING_RESPONSE = 0x0101, + STUN_BINDING_ERROR_RESPONSE = 0x0111, +}; + +// These are all known STUN attributes, defined in RFC 5389 and elsewhere. +// Next to each is the name of the class (T is StunTAttribute) that implements +// that type. +// RETRANSMIT_COUNT is the number of outstanding pings without a response at +// the time the packet is generated. +enum StunAttributeType { + STUN_ATTR_MAPPED_ADDRESS = 0x0001, // Address + STUN_ATTR_USERNAME = 0x0006, // ByteString + STUN_ATTR_MESSAGE_INTEGRITY = 0x0008, // ByteString, 20 bytes + STUN_ATTR_ERROR_CODE = 0x0009, // ErrorCode + STUN_ATTR_UNKNOWN_ATTRIBUTES = 0x000a, // UInt16List + STUN_ATTR_REALM = 0x0014, // ByteString + STUN_ATTR_NONCE = 0x0015, // ByteString + STUN_ATTR_XOR_MAPPED_ADDRESS = 0x0020, // XorAddress + STUN_ATTR_SOFTWARE = 0x8022, // ByteString + STUN_ATTR_ALTERNATE_SERVER = 0x8023, // ByteString + STUN_ATTR_FINGERPRINT = 0x8028, // UInt32 + STUN_ATTR_RETRANSMIT_COUNT = 0xFF00 // UInt32 +}; + +// These are the types of the values associated with the attributes above. +// This allows us to perform some basic validation when reading or adding +// attributes. Note that these values are for our own use, and not defined in +// RFC 5389. +enum StunAttributeValueType { + STUN_VALUE_UNKNOWN = 0, + STUN_VALUE_ADDRESS = 1, + STUN_VALUE_XOR_ADDRESS = 2, + STUN_VALUE_UINT32 = 3, + STUN_VALUE_UINT64 = 4, + STUN_VALUE_BYTE_STRING = 5, + STUN_VALUE_ERROR_CODE = 6, + STUN_VALUE_UINT16_LIST = 7 +}; + +// These are the types of STUN addresses defined in RFC 5389. +enum StunAddressFamily { + // NB: UNDEF is not part of the STUN spec. + STUN_ADDRESS_UNDEF = 0, + STUN_ADDRESS_IPV4 = 1, + STUN_ADDRESS_IPV6 = 2 +}; + +// These are the types of STUN error codes defined in RFC 5389. +enum StunErrorCode { + STUN_ERROR_TRY_ALTERNATE = 300, + STUN_ERROR_BAD_REQUEST = 400, + STUN_ERROR_UNAUTHORIZED = 401, + STUN_ERROR_UNKNOWN_ATTRIBUTE = 420, + STUN_ERROR_STALE_CREDENTIALS = 430, // GICE only + STUN_ERROR_STALE_NONCE = 438, + STUN_ERROR_SERVER_ERROR = 500, + STUN_ERROR_GLOBAL_FAILURE = 600 +}; + +// Strings for the error codes above. +extern const char STUN_ERROR_REASON_BAD_REQUEST[]; +extern const char STUN_ERROR_REASON_UNAUTHORIZED[]; +extern const char STUN_ERROR_REASON_UNKNOWN_ATTRIBUTE[]; +extern const char STUN_ERROR_REASON_STALE_CREDENTIALS[]; +extern const char STUN_ERROR_REASON_STALE_NONCE[]; +extern const char STUN_ERROR_REASON_SERVER_ERROR[]; + +// The mask used to determine whether a STUN message is a request/response etc. +const uint32 kStunTypeMask = 0x0110; + +// STUN Attribute header length. +const size_t kStunAttributeHeaderSize = 4; + +// Following values correspond to RFC5389. +const size_t kStunHeaderSize = 20; +const size_t kStunTransactionIdOffset = 8; +const size_t kStunTransactionIdLength = 12; +const uint32 kStunMagicCookie = 0x2112A442; +const size_t kStunMagicCookieLength = sizeof(kStunMagicCookie); + +// Following value corresponds to an earlier version of STUN from +// RFC3489. +const size_t kStunLegacyTransactionIdLength = 16; + +// STUN Message Integrity HMAC length. +const size_t kStunMessageIntegritySize = 20; + +class StunAttribute; +class StunAddressAttribute; +class StunXorAddressAttribute; +class StunUInt32Attribute; +class StunUInt64Attribute; +class StunByteStringAttribute; +class StunErrorCodeAttribute; +class StunUInt16ListAttribute; + +// Records a complete STUN/TURN message. Each message consists of a type and +// any number of attributes. Each attribute is parsed into an instance of an +// appropriate class (see above). The Get* methods will return instances of +// that attribute class. +class StunMessage { + public: + StunMessage(); + virtual ~StunMessage(); + + int type() const { return type_; } + size_t length() const { return length_; } + const std::string& transaction_id() const { return transaction_id_; } + + // Returns true if the message confirms to RFC3489 rather than + // RFC5389. The main difference between two version of the STUN + // protocol is the presence of the magic cookie and different length + // of transaction ID. For outgoing packets version of the protocol + // is determined by the lengths of the transaction ID. + bool IsLegacy() const; + + void SetType(int type) { type_ = static_cast(type); } + bool SetTransactionID(const std::string& str); + + // Gets the desired attribute value, or NULL if no such attribute type exists. + const StunAddressAttribute* GetAddress(int type) const; + const StunUInt32Attribute* GetUInt32(int type) const; + const StunUInt64Attribute* GetUInt64(int type) const; + const StunByteStringAttribute* GetByteString(int type) const; + + // Gets these specific attribute values. + const StunErrorCodeAttribute* GetErrorCode() const; + const StunUInt16ListAttribute* GetUnknownAttributes() const; + + // Takes ownership of the specified attribute, verifies it is of the correct + // type, and adds it to the message. The return value indicates whether this + // was successful. + bool AddAttribute(StunAttribute* attr); + + // Validates that a raw STUN message has a correct MESSAGE-INTEGRITY value. + // This can't currently be done on a StunMessage, since it is affected by + // padding data (which we discard when reading a StunMessage). + static bool ValidateMessageIntegrity(const char* data, size_t size, + const std::string& password); + // Adds a MESSAGE-INTEGRITY attribute that is valid for the current message. + bool AddMessageIntegrity(const std::string& password); + bool AddMessageIntegrity(const char* key, size_t keylen); + + // Verifies that a given buffer is STUN by checking for a correct FINGERPRINT. + static bool ValidateFingerprint(const char* data, size_t size); + + // Adds a FINGERPRINT attribute that is valid for the current message. + bool AddFingerprint(); + + // Parses the STUN packet in the given buffer and records it here. The + // return value indicates whether this was successful. + bool Read(talk_base::ByteBuffer* buf); + + // Writes this object into a STUN packet. The return value indicates whether + // this was successful. + bool Write(talk_base::ByteBuffer* buf) const; + + // Creates an empty message. Overridable by derived classes. + virtual StunMessage* CreateNew() const { return new StunMessage(); } + + protected: + // Verifies that the given attribute is allowed for this message. + virtual StunAttributeValueType GetAttributeValueType(int type) const; + + private: + StunAttribute* CreateAttribute(int type, size_t length) /* const*/; + const StunAttribute* GetAttribute(int type) const; + static bool IsValidTransactionId(const std::string& transaction_id); + + uint16 type_; + uint16 length_; + std::string transaction_id_; + std::vector* attrs_; +}; + +// Base class for all STUN/TURN attributes. +class StunAttribute { + public: + virtual ~StunAttribute() { + } + + int type() const { return type_; } + size_t length() const { return length_; } + + // Return the type of this attribute. + virtual StunAttributeValueType value_type() const = 0; + + // Only XorAddressAttribute needs this so far. + virtual void SetOwner(StunMessage* owner) {} + + // Reads the body (not the type or length) for this type of attribute from + // the given buffer. Return value is true if successful. + virtual bool Read(talk_base::ByteBuffer* buf) = 0; + + // Writes the body (not the type or length) to the given buffer. Return + // value is true if successful. + virtual bool Write(talk_base::ByteBuffer* buf) const = 0; + + // Creates an attribute object with the given type and smallest length. + static StunAttribute* Create(StunAttributeValueType value_type, uint16 type, + uint16 length, StunMessage* owner); + // TODO: Allow these create functions to take parameters, to reduce + // the amount of work callers need to do to initialize attributes. + static StunAddressAttribute* CreateAddress(uint16 type); + static StunXorAddressAttribute* CreateXorAddress(uint16 type); + static StunUInt32Attribute* CreateUInt32(uint16 type); + static StunUInt64Attribute* CreateUInt64(uint16 type); + static StunByteStringAttribute* CreateByteString(uint16 type); + static StunErrorCodeAttribute* CreateErrorCode(); + static StunUInt16ListAttribute* CreateUnknownAttributes(); + + protected: + StunAttribute(uint16 type, uint16 length); + void SetLength(uint16 length) { length_ = length; } + void WritePadding(talk_base::ByteBuffer* buf) const; + void ConsumePadding(talk_base::ByteBuffer* buf) const; + + private: + uint16 type_; + uint16 length_; +}; + +// Implements STUN attributes that record an Internet address. +class StunAddressAttribute : public StunAttribute { + public: + static const uint16 SIZE_UNDEF = 0; + static const uint16 SIZE_IP4 = 8; + static const uint16 SIZE_IP6 = 20; + StunAddressAttribute(uint16 type, const talk_base::SocketAddress& addr); + StunAddressAttribute(uint16 type, uint16 length); + + virtual StunAttributeValueType value_type() const { + return STUN_VALUE_ADDRESS; + } + + StunAddressFamily family() const { + switch (address_.ipaddr().family()) { + case AF_INET: + return STUN_ADDRESS_IPV4; + case AF_INET6: + return STUN_ADDRESS_IPV6; + } + return STUN_ADDRESS_UNDEF; + } + + const talk_base::SocketAddress& GetAddress() const { return address_; } + const talk_base::IPAddress& ipaddr() const { return address_.ipaddr(); } + uint16 port() const { return address_.port(); } + + void SetAddress(const talk_base::SocketAddress& addr) { + address_ = addr; + EnsureAddressLength(); + } + void SetIP(const talk_base::IPAddress& ip) { + address_.SetIP(ip); + EnsureAddressLength(); + } + void SetPort(uint16 port) { address_.SetPort(port); } + + virtual bool Read(talk_base::ByteBuffer* buf); + virtual bool Write(talk_base::ByteBuffer* buf) const; + + private: + void EnsureAddressLength() { + switch (family()) { + case STUN_ADDRESS_IPV4: { + SetLength(SIZE_IP4); + break; + } + case STUN_ADDRESS_IPV6: { + SetLength(SIZE_IP6); + break; + } + default: { + SetLength(SIZE_UNDEF); + break; + } + } + } + talk_base::SocketAddress address_; +}; + +// Implements STUN attributes that record an Internet address. When encoded +// in a STUN message, the address contained in this attribute is XORed with the +// transaction ID of the message. +class StunXorAddressAttribute : public StunAddressAttribute { + public: + StunXorAddressAttribute(uint16 type, const talk_base::SocketAddress& addr); + StunXorAddressAttribute(uint16 type, uint16 length, + StunMessage* owner); + + virtual StunAttributeValueType value_type() const { + return STUN_VALUE_XOR_ADDRESS; + } + virtual void SetOwner(StunMessage* owner) { + owner_ = owner; + } + virtual bool Read(talk_base::ByteBuffer* buf); + virtual bool Write(talk_base::ByteBuffer* buf) const; + + private: + talk_base::IPAddress GetXoredIP() const; + StunMessage* owner_; +}; + +// Implements STUN attributes that record a 32-bit integer. +class StunUInt32Attribute : public StunAttribute { + public: + static const uint16 SIZE = 4; + StunUInt32Attribute(uint16 type, uint32 value); + explicit StunUInt32Attribute(uint16 type); + + virtual StunAttributeValueType value_type() const { + return STUN_VALUE_UINT32; + } + + uint32 value() const { return bits_; } + void SetValue(uint32 bits) { bits_ = bits; } + + bool GetBit(size_t index) const; + void SetBit(size_t index, bool value); + + virtual bool Read(talk_base::ByteBuffer* buf); + virtual bool Write(talk_base::ByteBuffer* buf) const; + + private: + uint32 bits_; +}; + +class StunUInt64Attribute : public StunAttribute { + public: + static const uint16 SIZE = 8; + StunUInt64Attribute(uint16 type, uint64 value); + explicit StunUInt64Attribute(uint16 type); + + virtual StunAttributeValueType value_type() const { + return STUN_VALUE_UINT64; + } + + uint64 value() const { return bits_; } + void SetValue(uint64 bits) { bits_ = bits; } + + virtual bool Read(talk_base::ByteBuffer* buf); + virtual bool Write(talk_base::ByteBuffer* buf) const; + + private: + uint64 bits_; +}; + +// Implements STUN attributes that record an arbitrary byte string. +class StunByteStringAttribute : public StunAttribute { + public: + explicit StunByteStringAttribute(uint16 type); + StunByteStringAttribute(uint16 type, const std::string& str); + StunByteStringAttribute(uint16 type, const void* bytes, size_t length); + StunByteStringAttribute(uint16 type, uint16 length); + ~StunByteStringAttribute(); + + virtual StunAttributeValueType value_type() const { + return STUN_VALUE_BYTE_STRING; + } + + const char* bytes() const { return bytes_; } + std::string GetString() const { return std::string(bytes_, length()); } + + void CopyBytes(const char* bytes); // uses strlen + void CopyBytes(const void* bytes, size_t length); + + uint8 GetByte(size_t index) const; + void SetByte(size_t index, uint8 value); + + virtual bool Read(talk_base::ByteBuffer* buf); + virtual bool Write(talk_base::ByteBuffer* buf) const; + + private: + void SetBytes(char* bytes, size_t length); + + char* bytes_; +}; + +// Implements STUN attributes that record an error code. +class StunErrorCodeAttribute : public StunAttribute { + public: + static const uint16 MIN_SIZE = 4; + StunErrorCodeAttribute(uint16 type, int code, const std::string& reason); + StunErrorCodeAttribute(uint16 type, uint16 length); + ~StunErrorCodeAttribute(); + + virtual StunAttributeValueType value_type() const { + return STUN_VALUE_ERROR_CODE; + } + + // The combined error and class, e.g. 0x400. + int code() const; + void SetCode(int code); + + // The individual error components. + int eclass() const { return class_; } + int number() const { return number_; } + const std::string& reason() const { return reason_; } + void SetClass(uint8 eclass) { class_ = eclass; } + void SetNumber(uint8 number) { number_ = number; } + void SetReason(const std::string& reason); + + bool Read(talk_base::ByteBuffer* buf); + bool Write(talk_base::ByteBuffer* buf) const; + + private: + uint8 class_; + uint8 number_; + std::string reason_; +}; + +// Implements STUN attributes that record a list of attribute names. +class StunUInt16ListAttribute : public StunAttribute { + public: + StunUInt16ListAttribute(uint16 type, uint16 length); + ~StunUInt16ListAttribute(); + + virtual StunAttributeValueType value_type() const { + return STUN_VALUE_UINT16_LIST; + } + + size_t Size() const; + uint16 GetType(int index) const; + void SetType(int index, uint16 value); + void AddType(uint16 value); + + bool Read(talk_base::ByteBuffer* buf); + bool Write(talk_base::ByteBuffer* buf) const; + + private: + std::vector* attr_types_; +}; + +// Returns the (successful) response type for the given request type. +// Returns -1 if |request_type| is not a valid request type. +int GetStunSuccessResponseType(int request_type); + +// Returns the error response type for the given request type. +// Returns -1 if |request_type| is not a valid request type. +int GetStunErrorResponseType(int request_type); + +// Returns whether a given message is a request type. +bool IsStunRequestType(int msg_type); + +// Returns whether a given message is an indication type. +bool IsStunIndicationType(int msg_type); + +// Returns whether a given response is a success type. +bool IsStunSuccessResponseType(int msg_type); + +// Returns whether a given response is an error type. +bool IsStunErrorResponseType(int msg_type); + +// Computes the STUN long-term credential hash. +bool ComputeStunCredentialHash(const std::string& username, + const std::string& realm, const std::string& password, std::string* hash); + +// TODO: Move the TURN/ICE stuff below out to separate files. +extern const char TURN_MAGIC_COOKIE_VALUE[4]; + +// "GTURN" STUN methods. +// TODO: Rename these methods to GTURN_ to make it clear they aren't +// part of standard STUN/TURN. +enum RelayMessageType { + // For now, using the same defs from TurnMessageType below. + // STUN_ALLOCATE_REQUEST = 0x0003, + // STUN_ALLOCATE_RESPONSE = 0x0103, + // STUN_ALLOCATE_ERROR_RESPONSE = 0x0113, + STUN_SEND_REQUEST = 0x0004, + STUN_SEND_RESPONSE = 0x0104, + STUN_SEND_ERROR_RESPONSE = 0x0114, + STUN_DATA_INDICATION = 0x0115, +}; + +// "GTURN"-specific STUN attributes. +// TODO: Rename these attributes to GTURN_ to avoid conflicts. +enum RelayAttributeType { + STUN_ATTR_LIFETIME = 0x000d, // UInt32 + STUN_ATTR_MAGIC_COOKIE = 0x000f, // ByteString, 4 bytes + STUN_ATTR_BANDWIDTH = 0x0010, // UInt32 + STUN_ATTR_DESTINATION_ADDRESS = 0x0011, // Address + STUN_ATTR_SOURCE_ADDRESS2 = 0x0012, // Address + STUN_ATTR_DATA = 0x0013, // ByteString + STUN_ATTR_OPTIONS = 0x8001, // UInt32 +}; + +// A "GTURN" STUN message. +class RelayMessage : public StunMessage { + protected: + virtual StunAttributeValueType GetAttributeValueType(int type) const { + switch (type) { + case STUN_ATTR_LIFETIME: return STUN_VALUE_UINT32; + case STUN_ATTR_MAGIC_COOKIE: return STUN_VALUE_BYTE_STRING; + case STUN_ATTR_BANDWIDTH: return STUN_VALUE_UINT32; + case STUN_ATTR_DESTINATION_ADDRESS: return STUN_VALUE_ADDRESS; + case STUN_ATTR_SOURCE_ADDRESS2: return STUN_VALUE_ADDRESS; + case STUN_ATTR_DATA: return STUN_VALUE_BYTE_STRING; + case STUN_ATTR_OPTIONS: return STUN_VALUE_UINT32; + default: return StunMessage::GetAttributeValueType(type); + } + } + virtual StunMessage* CreateNew() const { return new RelayMessage(); } +}; + +// Defined in TURN RFC 5766. +enum TurnMessageType { + STUN_ALLOCATE_REQUEST = 0x0003, + STUN_ALLOCATE_RESPONSE = 0x0103, + STUN_ALLOCATE_ERROR_RESPONSE = 0x0113, + TURN_REFRESH_REQUEST = 0x0004, + TURN_REFRESH_RESPONSE = 0x0104, + TURN_REFRESH_ERROR_RESPONSE = 0x0114, + TURN_SEND_INDICATION = 0x0016, + TURN_DATA_INDICATION = 0x0017, + TURN_CREATE_PERMISSION_REQUEST = 0x0008, + TURN_CREATE_PERMISSION_RESPONSE = 0x0108, + TURN_CREATE_PERMISSION_ERROR_RESPONSE = 0x0118, + TURN_CHANNEL_BIND_REQUEST = 0x0009, + TURN_CHANNEL_BIND_RESPONSE = 0x0109, + TURN_CHANNEL_BIND_ERROR_RESPONSE = 0x0119, +}; + +enum TurnAttributeType { + STUN_ATTR_CHANNEL_NUMBER = 0x000C, // UInt32 + STUN_ATTR_TURN_LIFETIME = 0x000d, // UInt32 + STUN_ATTR_XOR_PEER_ADDRESS = 0x0012, // XorAddress + // TODO(mallinath) - Uncomment after RelayAttributes are renamed. + // STUN_ATTR_DATA = 0x0013, // ByteString + STUN_ATTR_XOR_RELAYED_ADDRESS = 0x0016, // XorAddress + STUN_ATTR_EVEN_PORT = 0x0018, // ByteString, 1 byte. + STUN_ATTR_REQUESTED_TRANSPORT = 0x0019, // UInt32 + STUN_ATTR_DONT_FRAGMENT = 0x001A, // No content, Length = 0 + STUN_ATTR_RESERVATION_TOKEN = 0x0022, // ByteString, 8 bytes. + // TODO(mallinath) - Rename STUN_ATTR_TURN_LIFETIME to STUN_ATTR_LIFETIME and + // STUN_ATTR_TURN_DATA to STUN_ATTR_DATA. Also rename RelayMessage attributes + // by appending G to attribute name. +}; + +// RFC 5766-defined errors. +enum TurnErrorType { + STUN_ERROR_FORBIDDEN = 403, + STUN_ERROR_ALLOCATION_MISMATCH = 437, + STUN_ERROR_WRONG_CREDENTIALS = 441, + STUN_ERROR_UNSUPPORTED_PROTOCOL = 442 +}; +extern const char STUN_ERROR_REASON_FORBIDDEN[]; +extern const char STUN_ERROR_REASON_ALLOCATION_MISMATCH[]; +extern const char STUN_ERROR_REASON_WRONG_CREDENTIALS[]; +extern const char STUN_ERROR_REASON_UNSUPPORTED_PROTOCOL[]; +class TurnMessage : public StunMessage { + protected: + virtual StunAttributeValueType GetAttributeValueType(int type) const { + switch (type) { + case STUN_ATTR_CHANNEL_NUMBER: return STUN_VALUE_UINT32; + case STUN_ATTR_TURN_LIFETIME: return STUN_VALUE_UINT32; + case STUN_ATTR_XOR_PEER_ADDRESS: return STUN_VALUE_XOR_ADDRESS; + case STUN_ATTR_DATA: return STUN_VALUE_BYTE_STRING; + case STUN_ATTR_XOR_RELAYED_ADDRESS: return STUN_VALUE_XOR_ADDRESS; + case STUN_ATTR_EVEN_PORT: return STUN_VALUE_BYTE_STRING; + case STUN_ATTR_REQUESTED_TRANSPORT: return STUN_VALUE_UINT32; + case STUN_ATTR_DONT_FRAGMENT: return STUN_VALUE_BYTE_STRING; + case STUN_ATTR_RESERVATION_TOKEN: return STUN_VALUE_BYTE_STRING; + default: return StunMessage::GetAttributeValueType(type); + } + } + virtual StunMessage* CreateNew() const { return new TurnMessage(); } +}; + +// RFC 5245 ICE STUN attributes. +enum IceAttributeType { + STUN_ATTR_PRIORITY = 0x0024, // UInt32 + STUN_ATTR_USE_CANDIDATE = 0x0025, // No content, Length = 0 + STUN_ATTR_ICE_CONTROLLED = 0x8029, // UInt64 + STUN_ATTR_ICE_CONTROLLING = 0x802A // UInt64 +}; + +// RFC 5245-defined errors. +enum IceErrorCode { + STUN_ERROR_ROLE_CONFLICT = 487, +}; +extern const char STUN_ERROR_REASON_ROLE_CONFLICT[]; + +// A RFC 5245 ICE STUN message. +class IceMessage : public StunMessage { + protected: + virtual StunAttributeValueType GetAttributeValueType(int type) const { + switch (type) { + case STUN_ATTR_PRIORITY: return STUN_VALUE_UINT32; + case STUN_ATTR_USE_CANDIDATE: return STUN_VALUE_BYTE_STRING; + case STUN_ATTR_ICE_CONTROLLED: return STUN_VALUE_UINT64; + case STUN_ATTR_ICE_CONTROLLING: return STUN_VALUE_UINT64; + default: return StunMessage::GetAttributeValueType(type); + } + } + virtual StunMessage* CreateNew() const { return new IceMessage(); } +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_STUN_H_ diff --git a/talk/p2p/base/stun_unittest.cc b/talk/p2p/base/stun_unittest.cc new file mode 100644 index 000000000..43db959d6 --- /dev/null +++ b/talk/p2p/base/stun_unittest.cc @@ -0,0 +1,1468 @@ +/* + * libjingle + * Copyright 2004 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 + +#include "talk/base/bytebuffer.h" +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/messagedigest.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/socketaddress.h" +#include "talk/p2p/base/stun.h" + +namespace cricket { + +class StunTest : public ::testing::Test { + protected: + void CheckStunHeader(const StunMessage& msg, StunMessageType expected_type, + size_t expected_length) { + ASSERT_EQ(expected_type, msg.type()); + ASSERT_EQ(expected_length, msg.length()); + } + + void CheckStunTransactionID(const StunMessage& msg, + const unsigned char* expectedID, size_t length) { + ASSERT_EQ(length, msg.transaction_id().size()); + ASSERT_EQ(length == kStunTransactionIdLength + 4, msg.IsLegacy()); + ASSERT_EQ(length == kStunTransactionIdLength, !msg.IsLegacy()); + ASSERT_EQ(0, std::memcmp(msg.transaction_id().c_str(), + expectedID, length)); + } + + void CheckStunAddressAttribute(const StunAddressAttribute* addr, + StunAddressFamily expected_family, + int expected_port, + talk_base::IPAddress expected_address) { + ASSERT_EQ(expected_family, addr->family()); + ASSERT_EQ(expected_port, addr->port()); + + if (addr->family() == STUN_ADDRESS_IPV4) { + in_addr v4_address = expected_address.ipv4_address(); + in_addr stun_address = addr->ipaddr().ipv4_address(); + ASSERT_EQ(0, std::memcmp(&v4_address, &stun_address, + sizeof(stun_address))); + } else if (addr->family() == STUN_ADDRESS_IPV6) { + in6_addr v6_address = expected_address.ipv6_address(); + in6_addr stun_address = addr->ipaddr().ipv6_address(); + ASSERT_EQ(0, std::memcmp(&v6_address, &stun_address, + sizeof(stun_address))); + } else { + ASSERT_TRUE(addr->family() == STUN_ADDRESS_IPV6 || + addr->family() == STUN_ADDRESS_IPV4); + } + } + + size_t ReadStunMessageTestCase(StunMessage* msg, + const unsigned char* testcase, + size_t size) { + const char* input = reinterpret_cast(testcase); + talk_base::ByteBuffer buf(input, size); + if (msg->Read(&buf)) { + // Returns the size the stun message should report itself as being + return (size - 20); + } else { + return 0; + } + } +}; + + +// Sample STUN packets with various attributes +// Gathered by wiresharking pjproject's pjnath test programs +// pjproject available at www.pjsip.org + +static const unsigned char kStunMessageWithIPv6MappedAddress[] = { + 0x00, 0x01, 0x00, 0x18, // message header + 0x21, 0x12, 0xa4, 0x42, // transaction id + 0x29, 0x1f, 0xcd, 0x7c, + 0xba, 0x58, 0xab, 0xd7, + 0xf2, 0x41, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x14, // Address type (mapped), length + 0x00, 0x02, 0xb8, 0x81, // family (IPv6), port + 0x24, 0x01, 0xfa, 0x00, // an IPv6 address + 0x00, 0x04, 0x10, 0x00, + 0xbe, 0x30, 0x5b, 0xff, + 0xfe, 0xe5, 0x00, 0xc3 +}; + +static const unsigned char kStunMessageWithIPv4MappedAddress[] = { + 0x01, 0x01, 0x00, 0x0c, // binding response, length 12 + 0x21, 0x12, 0xa4, 0x42, // magic cookie + 0x29, 0x1f, 0xcd, 0x7c, // transaction ID + 0xba, 0x58, 0xab, 0xd7, + 0xf2, 0x41, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x08, // Mapped, 8 byte length + 0x00, 0x01, 0x9d, 0xfc, // AF_INET, unxor-ed port + 0xac, 0x17, 0x44, 0xe6 // IPv4 address +}; + +// Test XOR-mapped IP addresses: +static const unsigned char kStunMessageWithIPv6XorMappedAddress[] = { + 0x01, 0x01, 0x00, 0x18, // message header (binding response) + 0x21, 0x12, 0xa4, 0x42, // magic cookie (rfc5389) + 0xe3, 0xa9, 0x46, 0xe1, // transaction ID + 0x7c, 0x00, 0xc2, 0x62, + 0x54, 0x08, 0x01, 0x00, + 0x00, 0x20, 0x00, 0x14, // Address Type (XOR), length + 0x00, 0x02, 0xcb, 0x5b, // family, XOR-ed port + 0x05, 0x13, 0x5e, 0x42, // XOR-ed IPv6 address + 0xe3, 0xad, 0x56, 0xe1, + 0xc2, 0x30, 0x99, 0x9d, + 0xaa, 0xed, 0x01, 0xc3 +}; + +static const unsigned char kStunMessageWithIPv4XorMappedAddress[] = { + 0x01, 0x01, 0x00, 0x0c, // message header (binding response) + 0x21, 0x12, 0xa4, 0x42, // magic cookie + 0x29, 0x1f, 0xcd, 0x7c, // transaction ID + 0xba, 0x58, 0xab, 0xd7, + 0xf2, 0x41, 0x01, 0x00, + 0x00, 0x20, 0x00, 0x08, // address type (xor), length + 0x00, 0x01, 0xfc, 0xb5, // family (AF_INET), XOR-ed port + 0x8d, 0x05, 0xe0, 0xa4 // IPv4 address +}; + +// ByteString Attribute (username) +static const unsigned char kStunMessageWithByteStringAttribute[] = { + 0x00, 0x01, 0x00, 0x0c, + 0x21, 0x12, 0xa4, 0x42, + 0xe3, 0xa9, 0x46, 0xe1, + 0x7c, 0x00, 0xc2, 0x62, + 0x54, 0x08, 0x01, 0x00, + 0x00, 0x06, 0x00, 0x08, // username attribute (length 8) + 0x61, 0x62, 0x63, 0x64, // abcdefgh + 0x65, 0x66, 0x67, 0x68 +}; + +// Message with an unknown but comprehensible optional attribute. +// Parsing should succeed despite this unknown attribute. +static const unsigned char kStunMessageWithUnknownAttribute[] = { + 0x00, 0x01, 0x00, 0x14, + 0x21, 0x12, 0xa4, 0x42, + 0xe3, 0xa9, 0x46, 0xe1, + 0x7c, 0x00, 0xc2, 0x62, + 0x54, 0x08, 0x01, 0x00, + 0x00, 0xaa, 0x00, 0x07, // Unknown attribute, length 7 (needs padding!) + 0x61, 0x62, 0x63, 0x64, // abcdefg + padding + 0x65, 0x66, 0x67, 0x00, + 0x00, 0x06, 0x00, 0x03, // Followed by a known attribute we can + 0x61, 0x62, 0x63, 0x00 // check for (username of length 3) +}; + +// ByteString Attribute (username) with padding byte +static const unsigned char kStunMessageWithPaddedByteStringAttribute[] = { + 0x00, 0x01, 0x00, 0x08, + 0x21, 0x12, 0xa4, 0x42, + 0xe3, 0xa9, 0x46, 0xe1, + 0x7c, 0x00, 0xc2, 0x62, + 0x54, 0x08, 0x01, 0x00, + 0x00, 0x06, 0x00, 0x03, // username attribute (length 3) + 0x61, 0x62, 0x63, 0xcc // abc +}; + +// Message with an Unknown Attributes (uint16 list) attribute. +static const unsigned char kStunMessageWithUInt16ListAttribute[] = { + 0x00, 0x01, 0x00, 0x0c, + 0x21, 0x12, 0xa4, 0x42, + 0xe3, 0xa9, 0x46, 0xe1, + 0x7c, 0x00, 0xc2, 0x62, + 0x54, 0x08, 0x01, 0x00, + 0x00, 0x0a, 0x00, 0x06, // username attribute (length 6) + 0x00, 0x01, 0x10, 0x00, // three attributes plus padding + 0xAB, 0xCU, 0xBE, 0xEF +}; + +// Error response message (unauthorized) +static const unsigned char kStunMessageWithErrorAttribute[] = { + 0x01, 0x11, 0x00, 0x14, + 0x21, 0x12, 0xa4, 0x42, + 0x29, 0x1f, 0xcd, 0x7c, + 0xba, 0x58, 0xab, 0xd7, + 0xf2, 0x41, 0x01, 0x00, + 0x00, 0x09, 0x00, 0x10, + 0x00, 0x00, 0x04, 0x01, + 0x55, 0x6e, 0x61, 0x75, + 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x65, 0x64 +}; + +// Message with an address attribute with an unknown address family, +// and a byte string attribute. Check that we quit reading after the +// bogus address family and don't read the username attribute. +static const unsigned char kStunMessageWithInvalidAddressFamily[] = { + 0x01, 0x01, 0x00, 0x18, // binding response, length 24 + 0x21, 0x12, 0xa4, 0x42, // magic cookie + 0x29, 0x1f, 0xcd, 0x7c, // transaction ID + 0xba, 0x58, 0xab, 0xd7, + 0xf2, 0x41, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x08, // Mapped address, 4 byte length + 0x00, 0x09, 0xfe, 0xed, // Bogus address family (port unimportant). + 0xac, 0x17, 0x44, 0xe6, // Should be skipped. + 0x00, 0x06, 0x00, 0x08, // Username attribute (length 8) + 0x61, 0x62, 0x63, 0x64, // abcdefgh + 0x65, 0x66, 0x67, 0x68 +}; + +// Message with an address attribute with an invalid address length. +// Should fail to be read. +static const unsigned char kStunMessageWithInvalidAddressLength[] = { + 0x01, 0x01, 0x00, 0x18, // binding response, length 24 + 0x21, 0x12, 0xa4, 0x42, // magic cookie + 0x29, 0x1f, 0xcd, 0x7c, // transaction ID + 0xba, 0x58, 0xab, 0xd7, + 0xf2, 0x41, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x0c, // Mapped address, 12 byte length + 0x00, 0x01, 0xfe, 0xed, // Claims to be AF_INET. + 0xac, 0x17, 0x44, 0xe6, + 0x00, 0x06, 0x00, 0x08 +}; + +// Sample messages with an invalid length Field + +// The actual length in bytes of the invalid messages (including STUN header) +static const int kRealLengthOfInvalidLengthTestCases = 32; + +static const unsigned char kStunMessageWithZeroLength[] = { + 0x00, 0x01, 0x00, 0x00, // length of 0 (last 2 bytes) + 0x21, 0x12, 0xA4, 0x42, // magic cookie + '0', '1', '2', '3', // transaction id + '4', '5', '6', '7', + '8', '9', 'a', 'b', + 0x00, 0x20, 0x00, 0x08, // xor mapped address + 0x00, 0x01, 0x21, 0x1F, + 0x21, 0x12, 0xA4, 0x53, +}; + +static const unsigned char kStunMessageWithExcessLength[] = { + 0x00, 0x01, 0x00, 0x55, // length of 85 + 0x21, 0x12, 0xA4, 0x42, // magic cookie + '0', '1', '2', '3', // transaction id + '4', '5', '6', '7', + '8', '9', 'a', 'b', + 0x00, 0x20, 0x00, 0x08, // xor mapped address + 0x00, 0x01, 0x21, 0x1F, + 0x21, 0x12, 0xA4, 0x53, +}; + +static const unsigned char kStunMessageWithSmallLength[] = { + 0x00, 0x01, 0x00, 0x03, // length of 3 + 0x21, 0x12, 0xA4, 0x42, // magic cookie + '0', '1', '2', '3', // transaction id + '4', '5', '6', '7', + '8', '9', 'a', 'b', + 0x00, 0x20, 0x00, 0x08, // xor mapped address + 0x00, 0x01, 0x21, 0x1F, + 0x21, 0x12, 0xA4, 0x53, +}; + +// RTCP packet, for testing we correctly ignore non stun packet types. +// V=2, P=false, RC=0, Type=200, Len=6, Sender-SSRC=85, etc +static const unsigned char kRtcpPacket[] = { + 0x80, 0xc8, 0x00, 0x06, 0x00, 0x00, 0x00, 0x55, + 0xce, 0xa5, 0x18, 0x3a, 0x39, 0xcc, 0x7d, 0x09, + 0x23, 0xed, 0x19, 0x07, 0x00, 0x00, 0x01, 0x56, + 0x00, 0x03, 0x73, 0x50, +}; + +// RFC5769 Test Vectors +// Software name (request): "STUN test client" (without quotes) +// Software name (response): "test vector" (without quotes) +// Username: "evtj:h6vY" (without quotes) +// Password: "VOkJxbRl1RmTxUk/WvJxBt" (without quotes) +static const unsigned char kRfc5769SampleMsgTransactionId[] = { + 0xb7, 0xe7, 0xa7, 0x01, 0xbc, 0x34, 0xd6, 0x86, 0xfa, 0x87, 0xdf, 0xae +}; +static const char kRfc5769SampleMsgClientSoftware[] = "STUN test client"; +static const char kRfc5769SampleMsgServerSoftware[] = "test vector"; +static const char kRfc5769SampleMsgUsername[] = "evtj:h6vY"; +static const char kRfc5769SampleMsgPassword[] = "VOkJxbRl1RmTxUk/WvJxBt"; +static const talk_base::SocketAddress kRfc5769SampleMsgMappedAddress( + "192.0.2.1", 32853); +static const talk_base::SocketAddress kRfc5769SampleMsgIPv6MappedAddress( + "2001:db8:1234:5678:11:2233:4455:6677", 32853); + +static const unsigned char kRfc5769SampleMsgWithAuthTransactionId[] = { + 0x78, 0xad, 0x34, 0x33, 0xc6, 0xad, 0x72, 0xc0, 0x29, 0xda, 0x41, 0x2e +}; +static const char kRfc5769SampleMsgWithAuthUsername[] = + "\xe3\x83\x9e\xe3\x83\x88\xe3\x83\xaa\xe3\x83\x83\xe3\x82\xaf\xe3\x82\xb9"; +static const char kRfc5769SampleMsgWithAuthPassword[] = "TheMatrIX"; +static const char kRfc5769SampleMsgWithAuthNonce[] = + "f//499k954d6OL34oL9FSTvy64sA"; +static const char kRfc5769SampleMsgWithAuthRealm[] = "example.org"; + +// 2.1. Sample Request +static const unsigned char kRfc5769SampleRequest[] = { + 0x00, 0x01, 0x00, 0x58, // Request type and message length + 0x21, 0x12, 0xa4, 0x42, // Magic cookie + 0xb7, 0xe7, 0xa7, 0x01, // } + 0xbc, 0x34, 0xd6, 0x86, // } Transaction ID + 0xfa, 0x87, 0xdf, 0xae, // } + 0x80, 0x22, 0x00, 0x10, // SOFTWARE attribute header + 0x53, 0x54, 0x55, 0x4e, // } + 0x20, 0x74, 0x65, 0x73, // } User-agent... + 0x74, 0x20, 0x63, 0x6c, // } ...name + 0x69, 0x65, 0x6e, 0x74, // } + 0x00, 0x24, 0x00, 0x04, // PRIORITY attribute header + 0x6e, 0x00, 0x01, 0xff, // ICE priority value + 0x80, 0x29, 0x00, 0x08, // ICE-CONTROLLED attribute header + 0x93, 0x2f, 0xf9, 0xb1, // } Pseudo-random tie breaker... + 0x51, 0x26, 0x3b, 0x36, // } ...for ICE control + 0x00, 0x06, 0x00, 0x09, // USERNAME attribute header + 0x65, 0x76, 0x74, 0x6a, // } + 0x3a, 0x68, 0x36, 0x76, // } Username (9 bytes) and padding (3 bytes) + 0x59, 0x20, 0x20, 0x20, // } + 0x00, 0x08, 0x00, 0x14, // MESSAGE-INTEGRITY attribute header + 0x9a, 0xea, 0xa7, 0x0c, // } + 0xbf, 0xd8, 0xcb, 0x56, // } + 0x78, 0x1e, 0xf2, 0xb5, // } HMAC-SHA1 fingerprint + 0xb2, 0xd3, 0xf2, 0x49, // } + 0xc1, 0xb5, 0x71, 0xa2, // } + 0x80, 0x28, 0x00, 0x04, // FINGERPRINT attribute header + 0xe5, 0x7a, 0x3b, 0xcf // CRC32 fingerprint +}; + +// 2.2. Sample IPv4 Response +static const unsigned char kRfc5769SampleResponse[] = { + 0x01, 0x01, 0x00, 0x3c, // Response type and message length + 0x21, 0x12, 0xa4, 0x42, // Magic cookie + 0xb7, 0xe7, 0xa7, 0x01, // } + 0xbc, 0x34, 0xd6, 0x86, // } Transaction ID + 0xfa, 0x87, 0xdf, 0xae, // } + 0x80, 0x22, 0x00, 0x0b, // SOFTWARE attribute header + 0x74, 0x65, 0x73, 0x74, // } + 0x20, 0x76, 0x65, 0x63, // } UTF-8 server name + 0x74, 0x6f, 0x72, 0x20, // } + 0x00, 0x20, 0x00, 0x08, // XOR-MAPPED-ADDRESS attribute header + 0x00, 0x01, 0xa1, 0x47, // Address family (IPv4) and xor'd mapped port + 0xe1, 0x12, 0xa6, 0x43, // Xor'd mapped IPv4 address + 0x00, 0x08, 0x00, 0x14, // MESSAGE-INTEGRITY attribute header + 0x2b, 0x91, 0xf5, 0x99, // } + 0xfd, 0x9e, 0x90, 0xc3, // } + 0x8c, 0x74, 0x89, 0xf9, // } HMAC-SHA1 fingerprint + 0x2a, 0xf9, 0xba, 0x53, // } + 0xf0, 0x6b, 0xe7, 0xd7, // } + 0x80, 0x28, 0x00, 0x04, // FINGERPRINT attribute header + 0xc0, 0x7d, 0x4c, 0x96 // CRC32 fingerprint +}; + +// 2.3. Sample IPv6 Response +static const unsigned char kRfc5769SampleResponseIPv6[] = { + 0x01, 0x01, 0x00, 0x48, // Response type and message length + 0x21, 0x12, 0xa4, 0x42, // Magic cookie + 0xb7, 0xe7, 0xa7, 0x01, // } + 0xbc, 0x34, 0xd6, 0x86, // } Transaction ID + 0xfa, 0x87, 0xdf, 0xae, // } + 0x80, 0x22, 0x00, 0x0b, // SOFTWARE attribute header + 0x74, 0x65, 0x73, 0x74, // } + 0x20, 0x76, 0x65, 0x63, // } UTF-8 server name + 0x74, 0x6f, 0x72, 0x20, // } + 0x00, 0x20, 0x00, 0x14, // XOR-MAPPED-ADDRESS attribute header + 0x00, 0x02, 0xa1, 0x47, // Address family (IPv6) and xor'd mapped port. + 0x01, 0x13, 0xa9, 0xfa, // } + 0xa5, 0xd3, 0xf1, 0x79, // } Xor'd mapped IPv6 address + 0xbc, 0x25, 0xf4, 0xb5, // } + 0xbe, 0xd2, 0xb9, 0xd9, // } + 0x00, 0x08, 0x00, 0x14, // MESSAGE-INTEGRITY attribute header + 0xa3, 0x82, 0x95, 0x4e, // } + 0x4b, 0xe6, 0x7b, 0xf1, // } + 0x17, 0x84, 0xc9, 0x7c, // } HMAC-SHA1 fingerprint + 0x82, 0x92, 0xc2, 0x75, // } + 0xbf, 0xe3, 0xed, 0x41, // } + 0x80, 0x28, 0x00, 0x04, // FINGERPRINT attribute header + 0xc8, 0xfb, 0x0b, 0x4c // CRC32 fingerprint +}; + +// 2.4. Sample Request with Long-Term Authentication +static const unsigned char kRfc5769SampleRequestLongTermAuth[] = { + 0x00, 0x01, 0x00, 0x60, // Request type and message length + 0x21, 0x12, 0xa4, 0x42, // Magic cookie + 0x78, 0xad, 0x34, 0x33, // } + 0xc6, 0xad, 0x72, 0xc0, // } Transaction ID + 0x29, 0xda, 0x41, 0x2e, // } + 0x00, 0x06, 0x00, 0x12, // USERNAME attribute header + 0xe3, 0x83, 0x9e, 0xe3, // } + 0x83, 0x88, 0xe3, 0x83, // } + 0xaa, 0xe3, 0x83, 0x83, // } Username value (18 bytes) and padding (2 bytes) + 0xe3, 0x82, 0xaf, 0xe3, // } + 0x82, 0xb9, 0x00, 0x00, // } + 0x00, 0x15, 0x00, 0x1c, // NONCE attribute header + 0x66, 0x2f, 0x2f, 0x34, // } + 0x39, 0x39, 0x6b, 0x39, // } + 0x35, 0x34, 0x64, 0x36, // } + 0x4f, 0x4c, 0x33, 0x34, // } Nonce value + 0x6f, 0x4c, 0x39, 0x46, // } + 0x53, 0x54, 0x76, 0x79, // } + 0x36, 0x34, 0x73, 0x41, // } + 0x00, 0x14, 0x00, 0x0b, // REALM attribute header + 0x65, 0x78, 0x61, 0x6d, // } + 0x70, 0x6c, 0x65, 0x2e, // } Realm value (11 bytes) and padding (1 byte) + 0x6f, 0x72, 0x67, 0x00, // } + 0x00, 0x08, 0x00, 0x14, // MESSAGE-INTEGRITY attribute header + 0xf6, 0x70, 0x24, 0x65, // } + 0x6d, 0xd6, 0x4a, 0x3e, // } + 0x02, 0xb8, 0xe0, 0x71, // } HMAC-SHA1 fingerprint + 0x2e, 0x85, 0xc9, 0xa2, // } + 0x8c, 0xa8, 0x96, 0x66 // } +}; + +// Length parameter is changed to 0x38 from 0x58. +// AddMessageIntegrity will add MI information and update the length param +// accordingly. +static const unsigned char kRfc5769SampleRequestWithoutMI[] = { + 0x00, 0x01, 0x00, 0x38, // Request type and message length + 0x21, 0x12, 0xa4, 0x42, // Magic cookie + 0xb7, 0xe7, 0xa7, 0x01, // } + 0xbc, 0x34, 0xd6, 0x86, // } Transaction ID + 0xfa, 0x87, 0xdf, 0xae, // } + 0x80, 0x22, 0x00, 0x10, // SOFTWARE attribute header + 0x53, 0x54, 0x55, 0x4e, // } + 0x20, 0x74, 0x65, 0x73, // } User-agent... + 0x74, 0x20, 0x63, 0x6c, // } ...name + 0x69, 0x65, 0x6e, 0x74, // } + 0x00, 0x24, 0x00, 0x04, // PRIORITY attribute header + 0x6e, 0x00, 0x01, 0xff, // ICE priority value + 0x80, 0x29, 0x00, 0x08, // ICE-CONTROLLED attribute header + 0x93, 0x2f, 0xf9, 0xb1, // } Pseudo-random tie breaker... + 0x51, 0x26, 0x3b, 0x36, // } ...for ICE control + 0x00, 0x06, 0x00, 0x09, // USERNAME attribute header + 0x65, 0x76, 0x74, 0x6a, // } + 0x3a, 0x68, 0x36, 0x76, // } Username (9 bytes) and padding (3 bytes) + 0x59, 0x20, 0x20, 0x20 // } +}; + +// This HMAC differs from the RFC 5769 SampleRequest message. This differs +// because spec uses 0x20 for the padding where as our implementation uses 0. +static const unsigned char kCalculatedHmac1[] = { + 0x79, 0x07, 0xc2, 0xd2, // } + 0xed, 0xbf, 0xea, 0x48, // } + 0x0e, 0x4c, 0x76, 0xd8, // } HMAC-SHA1 fingerprint + 0x29, 0x62, 0xd5, 0xc3, // } + 0x74, 0x2a, 0xf9, 0xe3 // } +}; + +// Length parameter is changed to 0x1c from 0x3c. +// AddMessageIntegrity will add MI information and update the length param +// accordingly. +static const unsigned char kRfc5769SampleResponseWithoutMI[] = { + 0x01, 0x01, 0x00, 0x1c, // Response type and message length + 0x21, 0x12, 0xa4, 0x42, // Magic cookie + 0xb7, 0xe7, 0xa7, 0x01, // } + 0xbc, 0x34, 0xd6, 0x86, // } Transaction ID + 0xfa, 0x87, 0xdf, 0xae, // } + 0x80, 0x22, 0x00, 0x0b, // SOFTWARE attribute header + 0x74, 0x65, 0x73, 0x74, // } + 0x20, 0x76, 0x65, 0x63, // } UTF-8 server name + 0x74, 0x6f, 0x72, 0x20, // } + 0x00, 0x20, 0x00, 0x08, // XOR-MAPPED-ADDRESS attribute header + 0x00, 0x01, 0xa1, 0x47, // Address family (IPv4) and xor'd mapped port + 0xe1, 0x12, 0xa6, 0x43 // Xor'd mapped IPv4 address +}; + +// This HMAC differs from the RFC 5769 SampleResponse message. This differs +// because spec uses 0x20 for the padding where as our implementation uses 0. +static const unsigned char kCalculatedHmac2[] = { + 0x5d, 0x6b, 0x58, 0xbe, // } + 0xad, 0x94, 0xe0, 0x7e, // } + 0xef, 0x0d, 0xfc, 0x12, // } HMAC-SHA1 fingerprint + 0x82, 0xa2, 0xbd, 0x08, // } + 0x43, 0x14, 0x10, 0x28 // } +}; + +// A transaction ID without the 'magic cookie' portion +// pjnat's test programs use this transaction ID a lot. +const unsigned char kTestTransactionId1[] = { 0x029, 0x01f, 0x0cd, 0x07c, + 0x0ba, 0x058, 0x0ab, 0x0d7, + 0x0f2, 0x041, 0x001, 0x000 }; + +// They use this one sometimes too. +const unsigned char kTestTransactionId2[] = { 0x0e3, 0x0a9, 0x046, 0x0e1, + 0x07c, 0x000, 0x0c2, 0x062, + 0x054, 0x008, 0x001, 0x000 }; + +const in6_addr kIPv6TestAddress1 = { { { 0x24, 0x01, 0xfa, 0x00, + 0x00, 0x04, 0x10, 0x00, + 0xbe, 0x30, 0x5b, 0xff, + 0xfe, 0xe5, 0x00, 0xc3 } } }; +const in6_addr kIPv6TestAddress2 = { { { 0x24, 0x01, 0xfa, 0x00, + 0x00, 0x04, 0x10, 0x12, + 0x06, 0x0c, 0xce, 0xff, + 0xfe, 0x1f, 0x61, 0xa4 } } }; + +// This is kIPv6TestAddress1 xor-ed with kTestTransactionID2. +const in6_addr kIPv6XoredTestAddress = { { { 0x05, 0x13, 0x5e, 0x42, + 0xe3, 0xad, 0x56, 0xe1, + 0xc2, 0x30, 0x99, 0x9d, + 0xaa, 0xed, 0x01, 0xc3 } } }; + +#ifdef POSIX +const in_addr kIPv4TestAddress1 = { 0xe64417ac }; +// This is kIPv4TestAddress xored with the STUN magic cookie. +const in_addr kIPv4XoredTestAddress = { 0x8d05e0a4 }; +#elif defined WIN32 +// Windows in_addr has a union with a uchar[] array first. +const in_addr kIPv4XoredTestAddress = { { 0x8d, 0x05, 0xe0, 0xa4 } }; +const in_addr kIPv4TestAddress1 = { { 0x0ac, 0x017, 0x044, 0x0e6 } }; +#endif +const char kTestUserName1[] = "abcdefgh"; +const char kTestUserName2[] = "abc"; +const char kTestErrorReason[] = "Unauthorized"; +const int kTestErrorClass = 4; +const int kTestErrorNumber = 1; +const int kTestErrorCode = 401; + +const int kTestMessagePort1 = 59977; +const int kTestMessagePort2 = 47233; +const int kTestMessagePort3 = 56743; +const int kTestMessagePort4 = 40444; + +#define ReadStunMessage(X, Y) ReadStunMessageTestCase(X, Y, sizeof(Y)); + +// Test that the GetStun*Type and IsStun*Type methods work as expected. +TEST_F(StunTest, MessageTypes) { + EXPECT_EQ(STUN_BINDING_RESPONSE, + GetStunSuccessResponseType(STUN_BINDING_REQUEST)); + EXPECT_EQ(STUN_BINDING_ERROR_RESPONSE, + GetStunErrorResponseType(STUN_BINDING_REQUEST)); + EXPECT_EQ(-1, GetStunSuccessResponseType(STUN_BINDING_INDICATION)); + EXPECT_EQ(-1, GetStunSuccessResponseType(STUN_BINDING_RESPONSE)); + EXPECT_EQ(-1, GetStunSuccessResponseType(STUN_BINDING_ERROR_RESPONSE)); + EXPECT_EQ(-1, GetStunErrorResponseType(STUN_BINDING_INDICATION)); + EXPECT_EQ(-1, GetStunErrorResponseType(STUN_BINDING_RESPONSE)); + EXPECT_EQ(-1, GetStunErrorResponseType(STUN_BINDING_ERROR_RESPONSE)); + + int types[] = { + STUN_BINDING_REQUEST, STUN_BINDING_INDICATION, + STUN_BINDING_RESPONSE, STUN_BINDING_ERROR_RESPONSE + }; + for (int i = 0; i < ARRAY_SIZE(types); ++i) { + EXPECT_EQ(i == 0, IsStunRequestType(types[i])); + EXPECT_EQ(i == 1, IsStunIndicationType(types[i])); + EXPECT_EQ(i == 2, IsStunSuccessResponseType(types[i])); + EXPECT_EQ(i == 3, IsStunErrorResponseType(types[i])); + EXPECT_EQ(1, types[i] & 0xFEEF); + } +} + +TEST_F(StunTest, ReadMessageWithIPv4AddressAttribute) { + StunMessage msg; + size_t size = ReadStunMessage(&msg, kStunMessageWithIPv4MappedAddress); + CheckStunHeader(msg, STUN_BINDING_RESPONSE, size); + CheckStunTransactionID(msg, kTestTransactionId1, kStunTransactionIdLength); + + const StunAddressAttribute* addr = msg.GetAddress(STUN_ATTR_MAPPED_ADDRESS); + talk_base::IPAddress test_address(kIPv4TestAddress1); + CheckStunAddressAttribute(addr, STUN_ADDRESS_IPV4, + kTestMessagePort4, test_address); +} + +TEST_F(StunTest, ReadMessageWithIPv4XorAddressAttribute) { + StunMessage msg; + StunMessage msg2; + size_t size = ReadStunMessage(&msg, kStunMessageWithIPv4XorMappedAddress); + CheckStunHeader(msg, STUN_BINDING_RESPONSE, size); + CheckStunTransactionID(msg, kTestTransactionId1, kStunTransactionIdLength); + + const StunAddressAttribute* addr = + msg.GetAddress(STUN_ATTR_XOR_MAPPED_ADDRESS); + talk_base::IPAddress test_address(kIPv4TestAddress1); + CheckStunAddressAttribute(addr, STUN_ADDRESS_IPV4, + kTestMessagePort3, test_address); +} + +TEST_F(StunTest, ReadMessageWithIPv6AddressAttribute) { + StunMessage msg; + size_t size = ReadStunMessage(&msg, kStunMessageWithIPv6MappedAddress); + CheckStunHeader(msg, STUN_BINDING_REQUEST, size); + CheckStunTransactionID(msg, kTestTransactionId1, kStunTransactionIdLength); + + talk_base::IPAddress test_address(kIPv6TestAddress1); + + const StunAddressAttribute* addr = msg.GetAddress(STUN_ATTR_MAPPED_ADDRESS); + CheckStunAddressAttribute(addr, STUN_ADDRESS_IPV6, + kTestMessagePort2, test_address); +} + +TEST_F(StunTest, ReadMessageWithInvalidAddressAttribute) { + StunMessage msg; + size_t size = ReadStunMessage(&msg, kStunMessageWithIPv6MappedAddress); + CheckStunHeader(msg, STUN_BINDING_REQUEST, size); + CheckStunTransactionID(msg, kTestTransactionId1, kStunTransactionIdLength); + + talk_base::IPAddress test_address(kIPv6TestAddress1); + + const StunAddressAttribute* addr = msg.GetAddress(STUN_ATTR_MAPPED_ADDRESS); + CheckStunAddressAttribute(addr, STUN_ADDRESS_IPV6, + kTestMessagePort2, test_address); +} + +TEST_F(StunTest, ReadMessageWithIPv6XorAddressAttribute) { + StunMessage msg; + size_t size = ReadStunMessage(&msg, kStunMessageWithIPv6XorMappedAddress); + + talk_base::IPAddress test_address(kIPv6TestAddress1); + + CheckStunHeader(msg, STUN_BINDING_RESPONSE, size); + CheckStunTransactionID(msg, kTestTransactionId2, kStunTransactionIdLength); + + const StunAddressAttribute* addr = + msg.GetAddress(STUN_ATTR_XOR_MAPPED_ADDRESS); + CheckStunAddressAttribute(addr, STUN_ADDRESS_IPV6, + kTestMessagePort1, test_address); +} + +// Read the RFC5389 fields from the RFC5769 sample STUN request. +TEST_F(StunTest, ReadRfc5769RequestMessage) { + StunMessage msg; + size_t size = ReadStunMessage(&msg, kRfc5769SampleRequest); + CheckStunHeader(msg, STUN_BINDING_REQUEST, size); + CheckStunTransactionID(msg, kRfc5769SampleMsgTransactionId, + kStunTransactionIdLength); + + const StunByteStringAttribute* software = + msg.GetByteString(STUN_ATTR_SOFTWARE); + ASSERT_TRUE(software != NULL); + EXPECT_EQ(kRfc5769SampleMsgClientSoftware, software->GetString()); + + const StunByteStringAttribute* username = + msg.GetByteString(STUN_ATTR_USERNAME); + ASSERT_TRUE(username != NULL); + EXPECT_EQ(kRfc5769SampleMsgUsername, username->GetString()); + + // Actual M-I value checked in a later test. + ASSERT_TRUE(msg.GetByteString(STUN_ATTR_MESSAGE_INTEGRITY) != NULL); + + // Fingerprint checked in a later test, but double-check the value here. + const StunUInt32Attribute* fingerprint = + msg.GetUInt32(STUN_ATTR_FINGERPRINT); + ASSERT_TRUE(fingerprint != NULL); + EXPECT_EQ(0xe57a3bcf, fingerprint->value()); +} + +// Read the RFC5389 fields from the RFC5769 sample STUN response. +TEST_F(StunTest, ReadRfc5769ResponseMessage) { + StunMessage msg; + size_t size = ReadStunMessage(&msg, kRfc5769SampleResponse); + CheckStunHeader(msg, STUN_BINDING_RESPONSE, size); + CheckStunTransactionID(msg, kRfc5769SampleMsgTransactionId, + kStunTransactionIdLength); + + const StunByteStringAttribute* software = + msg.GetByteString(STUN_ATTR_SOFTWARE); + ASSERT_TRUE(software != NULL); + EXPECT_EQ(kRfc5769SampleMsgServerSoftware, software->GetString()); + + const StunAddressAttribute* mapped_address = + msg.GetAddress(STUN_ATTR_XOR_MAPPED_ADDRESS); + ASSERT_TRUE(mapped_address != NULL); + EXPECT_EQ(kRfc5769SampleMsgMappedAddress, mapped_address->GetAddress()); + + // Actual M-I and fingerprint checked in later tests. + ASSERT_TRUE(msg.GetByteString(STUN_ATTR_MESSAGE_INTEGRITY) != NULL); + ASSERT_TRUE(msg.GetUInt32(STUN_ATTR_FINGERPRINT) != NULL); +} + +// Read the RFC5389 fields from the RFC5769 sample STUN response for IPv6. +TEST_F(StunTest, ReadRfc5769ResponseMessageIPv6) { + StunMessage msg; + size_t size = ReadStunMessage(&msg, kRfc5769SampleResponseIPv6); + CheckStunHeader(msg, STUN_BINDING_RESPONSE, size); + CheckStunTransactionID(msg, kRfc5769SampleMsgTransactionId, + kStunTransactionIdLength); + + const StunByteStringAttribute* software = + msg.GetByteString(STUN_ATTR_SOFTWARE); + ASSERT_TRUE(software != NULL); + EXPECT_EQ(kRfc5769SampleMsgServerSoftware, software->GetString()); + + const StunAddressAttribute* mapped_address = + msg.GetAddress(STUN_ATTR_XOR_MAPPED_ADDRESS); + ASSERT_TRUE(mapped_address != NULL); + EXPECT_EQ(kRfc5769SampleMsgIPv6MappedAddress, mapped_address->GetAddress()); + + // Actual M-I and fingerprint checked in later tests. + ASSERT_TRUE(msg.GetByteString(STUN_ATTR_MESSAGE_INTEGRITY) != NULL); + ASSERT_TRUE(msg.GetUInt32(STUN_ATTR_FINGERPRINT) != NULL); +} + +// Read the RFC5389 fields from the RFC5769 sample STUN response with auth. +TEST_F(StunTest, ReadRfc5769RequestMessageLongTermAuth) { + StunMessage msg; + size_t size = ReadStunMessage(&msg, kRfc5769SampleRequestLongTermAuth); + CheckStunHeader(msg, STUN_BINDING_REQUEST, size); + CheckStunTransactionID(msg, kRfc5769SampleMsgWithAuthTransactionId, + kStunTransactionIdLength); + + const StunByteStringAttribute* username = + msg.GetByteString(STUN_ATTR_USERNAME); + ASSERT_TRUE(username != NULL); + EXPECT_EQ(kRfc5769SampleMsgWithAuthUsername, username->GetString()); + + const StunByteStringAttribute* nonce = + msg.GetByteString(STUN_ATTR_NONCE); + ASSERT_TRUE(nonce != NULL); + EXPECT_EQ(kRfc5769SampleMsgWithAuthNonce, nonce->GetString()); + + const StunByteStringAttribute* realm = + msg.GetByteString(STUN_ATTR_REALM); + ASSERT_TRUE(realm != NULL); + EXPECT_EQ(kRfc5769SampleMsgWithAuthRealm, realm->GetString()); + + // No fingerprint, actual M-I checked in later tests. + ASSERT_TRUE(msg.GetByteString(STUN_ATTR_MESSAGE_INTEGRITY) != NULL); + ASSERT_TRUE(msg.GetUInt32(STUN_ATTR_FINGERPRINT) == NULL); +} + +// The RFC3489 packet in this test is the same as +// kStunMessageWithIPv4MappedAddress, but with a different value where the +// magic cookie was. +TEST_F(StunTest, ReadLegacyMessage) { + unsigned char rfc3489_packet[sizeof(kStunMessageWithIPv4MappedAddress)]; + memcpy(rfc3489_packet, kStunMessageWithIPv4MappedAddress, + sizeof(kStunMessageWithIPv4MappedAddress)); + // Overwrite the magic cookie here. + memcpy(&rfc3489_packet[4], "ABCD", 4); + + StunMessage msg; + size_t size = ReadStunMessage(&msg, rfc3489_packet); + CheckStunHeader(msg, STUN_BINDING_RESPONSE, size); + CheckStunTransactionID(msg, &rfc3489_packet[4], kStunTransactionIdLength + 4); + + const StunAddressAttribute* addr = msg.GetAddress(STUN_ATTR_MAPPED_ADDRESS); + talk_base::IPAddress test_address(kIPv4TestAddress1); + CheckStunAddressAttribute(addr, STUN_ADDRESS_IPV4, + kTestMessagePort4, test_address); +} + +TEST_F(StunTest, SetIPv6XorAddressAttributeOwner) { + StunMessage msg; + StunMessage msg2; + size_t size = ReadStunMessage(&msg, kStunMessageWithIPv6XorMappedAddress); + + talk_base::IPAddress test_address(kIPv6TestAddress1); + + CheckStunHeader(msg, STUN_BINDING_RESPONSE, size); + CheckStunTransactionID(msg, kTestTransactionId2, kStunTransactionIdLength); + + const StunAddressAttribute* addr = + msg.GetAddress(STUN_ATTR_XOR_MAPPED_ADDRESS); + CheckStunAddressAttribute(addr, STUN_ADDRESS_IPV6, + kTestMessagePort1, test_address); + + // Owner with a different transaction ID. + msg2.SetTransactionID("ABCDABCDABCD"); + StunXorAddressAttribute addr2(STUN_ATTR_XOR_MAPPED_ADDRESS, 20, NULL); + addr2.SetIP(addr->ipaddr()); + addr2.SetPort(addr->port()); + addr2.SetOwner(&msg2); + // The internal IP address shouldn't change. + ASSERT_EQ(addr2.ipaddr(), addr->ipaddr()); + + talk_base::ByteBuffer correct_buf; + talk_base::ByteBuffer wrong_buf; + EXPECT_TRUE(addr->Write(&correct_buf)); + EXPECT_TRUE(addr2.Write(&wrong_buf)); + // But when written out, the buffers should look different. + ASSERT_NE(0, std::memcmp(correct_buf.Data(), + wrong_buf.Data(), + wrong_buf.Length())); + // And when reading a known good value, the address should be wrong. + addr2.Read(&correct_buf); + ASSERT_NE(addr->ipaddr(), addr2.ipaddr()); + addr2.SetIP(addr->ipaddr()); + addr2.SetPort(addr->port()); + // Try writing with no owner at all, should fail and write nothing. + addr2.SetOwner(NULL); + ASSERT_EQ(addr2.ipaddr(), addr->ipaddr()); + wrong_buf.Consume(wrong_buf.Length()); + EXPECT_FALSE(addr2.Write(&wrong_buf)); + ASSERT_EQ(0U, wrong_buf.Length()); +} + +TEST_F(StunTest, SetIPv4XorAddressAttributeOwner) { + // Unlike the IPv6XorAddressAttributeOwner test, IPv4 XOR address attributes + // should _not_ be affected by a change in owner. IPv4 XOR address uses the + // magic cookie value which is fixed. + StunMessage msg; + StunMessage msg2; + size_t size = ReadStunMessage(&msg, kStunMessageWithIPv4XorMappedAddress); + + talk_base::IPAddress test_address(kIPv4TestAddress1); + + CheckStunHeader(msg, STUN_BINDING_RESPONSE, size); + CheckStunTransactionID(msg, kTestTransactionId1, kStunTransactionIdLength); + + const StunAddressAttribute* addr = + msg.GetAddress(STUN_ATTR_XOR_MAPPED_ADDRESS); + CheckStunAddressAttribute(addr, STUN_ADDRESS_IPV4, + kTestMessagePort3, test_address); + + // Owner with a different transaction ID. + msg2.SetTransactionID("ABCDABCDABCD"); + StunXorAddressAttribute addr2(STUN_ATTR_XOR_MAPPED_ADDRESS, 20, NULL); + addr2.SetIP(addr->ipaddr()); + addr2.SetPort(addr->port()); + addr2.SetOwner(&msg2); + // The internal IP address shouldn't change. + ASSERT_EQ(addr2.ipaddr(), addr->ipaddr()); + + talk_base::ByteBuffer correct_buf; + talk_base::ByteBuffer wrong_buf; + EXPECT_TRUE(addr->Write(&correct_buf)); + EXPECT_TRUE(addr2.Write(&wrong_buf)); + // The same address data should be written. + ASSERT_EQ(0, std::memcmp(correct_buf.Data(), + wrong_buf.Data(), + wrong_buf.Length())); + // And an attribute should be able to un-XOR an address belonging to a message + // with a different transaction ID. + EXPECT_TRUE(addr2.Read(&correct_buf)); + ASSERT_EQ(addr->ipaddr(), addr2.ipaddr()); + + // However, no owner is still an error, should fail and write nothing. + addr2.SetOwner(NULL); + ASSERT_EQ(addr2.ipaddr(), addr->ipaddr()); + wrong_buf.Consume(wrong_buf.Length()); + EXPECT_FALSE(addr2.Write(&wrong_buf)); +} + +TEST_F(StunTest, CreateIPv6AddressAttribute) { + talk_base::IPAddress test_ip(kIPv6TestAddress2); + + StunAddressAttribute* addr = + StunAttribute::CreateAddress(STUN_ATTR_MAPPED_ADDRESS); + talk_base::SocketAddress test_addr(test_ip, kTestMessagePort2); + addr->SetAddress(test_addr); + + CheckStunAddressAttribute(addr, STUN_ADDRESS_IPV6, + kTestMessagePort2, test_ip); + delete addr; +} + +TEST_F(StunTest, CreateIPv4AddressAttribute) { + struct in_addr test_in_addr; + test_in_addr.s_addr = 0xBEB0B0BE; + talk_base::IPAddress test_ip(test_in_addr); + + StunAddressAttribute* addr = + StunAttribute::CreateAddress(STUN_ATTR_MAPPED_ADDRESS); + talk_base::SocketAddress test_addr(test_ip, kTestMessagePort2); + addr->SetAddress(test_addr); + + CheckStunAddressAttribute(addr, STUN_ADDRESS_IPV4, + kTestMessagePort2, test_ip); + delete addr; +} + +// Test that we don't care what order we set the parts of an address +TEST_F(StunTest, CreateAddressInArbitraryOrder) { + StunAddressAttribute* addr = + StunAttribute::CreateAddress(STUN_ATTR_DESTINATION_ADDRESS); + // Port first + addr->SetPort(kTestMessagePort1); + addr->SetIP(talk_base::IPAddress(kIPv4TestAddress1)); + ASSERT_EQ(kTestMessagePort1, addr->port()); + ASSERT_EQ(talk_base::IPAddress(kIPv4TestAddress1), addr->ipaddr()); + + StunAddressAttribute* addr2 = + StunAttribute::CreateAddress(STUN_ATTR_DESTINATION_ADDRESS); + // IP first + addr2->SetIP(talk_base::IPAddress(kIPv4TestAddress1)); + addr2->SetPort(kTestMessagePort2); + ASSERT_EQ(kTestMessagePort2, addr2->port()); + ASSERT_EQ(talk_base::IPAddress(kIPv4TestAddress1), addr2->ipaddr()); + + delete addr; + delete addr2; +} + +TEST_F(StunTest, WriteMessageWithIPv6AddressAttribute) { + StunMessage msg; + size_t size = sizeof(kStunMessageWithIPv6MappedAddress); + + talk_base::IPAddress test_ip(kIPv6TestAddress1); + + msg.SetType(STUN_BINDING_REQUEST); + msg.SetTransactionID( + std::string(reinterpret_cast(kTestTransactionId1), + kStunTransactionIdLength)); + CheckStunTransactionID(msg, kTestTransactionId1, kStunTransactionIdLength); + + StunAddressAttribute* addr = + StunAttribute::CreateAddress(STUN_ATTR_MAPPED_ADDRESS); + talk_base::SocketAddress test_addr(test_ip, kTestMessagePort2); + addr->SetAddress(test_addr); + EXPECT_TRUE(msg.AddAttribute(addr)); + + CheckStunHeader(msg, STUN_BINDING_REQUEST, (size - 20)); + + talk_base::ByteBuffer out; + EXPECT_TRUE(msg.Write(&out)); + ASSERT_EQ(out.Length(), sizeof(kStunMessageWithIPv6MappedAddress)); + int len1 = out.Length(); + std::string bytes; + out.ReadString(&bytes, len1); + ASSERT_EQ(0, std::memcmp(bytes.c_str(), + kStunMessageWithIPv6MappedAddress, + len1)); +} + +TEST_F(StunTest, WriteMessageWithIPv4AddressAttribute) { + StunMessage msg; + size_t size = sizeof(kStunMessageWithIPv4MappedAddress); + + talk_base::IPAddress test_ip(kIPv4TestAddress1); + + msg.SetType(STUN_BINDING_RESPONSE); + msg.SetTransactionID( + std::string(reinterpret_cast(kTestTransactionId1), + kStunTransactionIdLength)); + CheckStunTransactionID(msg, kTestTransactionId1, kStunTransactionIdLength); + + StunAddressAttribute* addr = + StunAttribute::CreateAddress(STUN_ATTR_MAPPED_ADDRESS); + talk_base::SocketAddress test_addr(test_ip, kTestMessagePort4); + addr->SetAddress(test_addr); + EXPECT_TRUE(msg.AddAttribute(addr)); + + CheckStunHeader(msg, STUN_BINDING_RESPONSE, (size - 20)); + + talk_base::ByteBuffer out; + EXPECT_TRUE(msg.Write(&out)); + ASSERT_EQ(out.Length(), sizeof(kStunMessageWithIPv4MappedAddress)); + int len1 = out.Length(); + std::string bytes; + out.ReadString(&bytes, len1); + ASSERT_EQ(0, std::memcmp(bytes.c_str(), + kStunMessageWithIPv4MappedAddress, + len1)); +} + +TEST_F(StunTest, WriteMessageWithIPv6XorAddressAttribute) { + StunMessage msg; + size_t size = sizeof(kStunMessageWithIPv6XorMappedAddress); + + talk_base::IPAddress test_ip(kIPv6TestAddress1); + + msg.SetType(STUN_BINDING_RESPONSE); + msg.SetTransactionID( + std::string(reinterpret_cast(kTestTransactionId2), + kStunTransactionIdLength)); + CheckStunTransactionID(msg, kTestTransactionId2, kStunTransactionIdLength); + + StunAddressAttribute* addr = + StunAttribute::CreateXorAddress(STUN_ATTR_XOR_MAPPED_ADDRESS); + talk_base::SocketAddress test_addr(test_ip, kTestMessagePort1); + addr->SetAddress(test_addr); + EXPECT_TRUE(msg.AddAttribute(addr)); + + CheckStunHeader(msg, STUN_BINDING_RESPONSE, (size - 20)); + + talk_base::ByteBuffer out; + EXPECT_TRUE(msg.Write(&out)); + ASSERT_EQ(out.Length(), sizeof(kStunMessageWithIPv6XorMappedAddress)); + int len1 = out.Length(); + std::string bytes; + out.ReadString(&bytes, len1); + ASSERT_EQ(0, std::memcmp(bytes.c_str(), + kStunMessageWithIPv6XorMappedAddress, + len1)); +} + +TEST_F(StunTest, WriteMessageWithIPv4XoreAddressAttribute) { + StunMessage msg; + size_t size = sizeof(kStunMessageWithIPv4XorMappedAddress); + + talk_base::IPAddress test_ip(kIPv4TestAddress1); + + msg.SetType(STUN_BINDING_RESPONSE); + msg.SetTransactionID( + std::string(reinterpret_cast(kTestTransactionId1), + kStunTransactionIdLength)); + CheckStunTransactionID(msg, kTestTransactionId1, kStunTransactionIdLength); + + StunAddressAttribute* addr = + StunAttribute::CreateXorAddress(STUN_ATTR_XOR_MAPPED_ADDRESS); + talk_base::SocketAddress test_addr(test_ip, kTestMessagePort3); + addr->SetAddress(test_addr); + EXPECT_TRUE(msg.AddAttribute(addr)); + + CheckStunHeader(msg, STUN_BINDING_RESPONSE, (size - 20)); + + talk_base::ByteBuffer out; + EXPECT_TRUE(msg.Write(&out)); + ASSERT_EQ(out.Length(), sizeof(kStunMessageWithIPv4XorMappedAddress)); + int len1 = out.Length(); + std::string bytes; + out.ReadString(&bytes, len1); + ASSERT_EQ(0, std::memcmp(bytes.c_str(), + kStunMessageWithIPv4XorMappedAddress, + len1)); +} + +TEST_F(StunTest, ReadByteStringAttribute) { + StunMessage msg; + size_t size = ReadStunMessage(&msg, kStunMessageWithByteStringAttribute); + + CheckStunHeader(msg, STUN_BINDING_REQUEST, size); + CheckStunTransactionID(msg, kTestTransactionId2, kStunTransactionIdLength); + const StunByteStringAttribute* username = + msg.GetByteString(STUN_ATTR_USERNAME); + ASSERT_TRUE(username != NULL); + EXPECT_EQ(kTestUserName1, username->GetString()); +} + +TEST_F(StunTest, ReadPaddedByteStringAttribute) { + StunMessage msg; + size_t size = ReadStunMessage(&msg, + kStunMessageWithPaddedByteStringAttribute); + ASSERT_NE(0U, size); + CheckStunHeader(msg, STUN_BINDING_REQUEST, size); + CheckStunTransactionID(msg, kTestTransactionId2, kStunTransactionIdLength); + const StunByteStringAttribute* username = + msg.GetByteString(STUN_ATTR_USERNAME); + ASSERT_TRUE(username != NULL); + EXPECT_EQ(kTestUserName2, username->GetString()); +} + +TEST_F(StunTest, ReadErrorCodeAttribute) { + StunMessage msg; + size_t size = ReadStunMessage(&msg, kStunMessageWithErrorAttribute); + + CheckStunHeader(msg, STUN_BINDING_ERROR_RESPONSE, size); + CheckStunTransactionID(msg, kTestTransactionId1, kStunTransactionIdLength); + const StunErrorCodeAttribute* errorcode = msg.GetErrorCode(); + ASSERT_TRUE(errorcode != NULL); + EXPECT_EQ(kTestErrorClass, errorcode->eclass()); + EXPECT_EQ(kTestErrorNumber, errorcode->number()); + EXPECT_EQ(kTestErrorReason, errorcode->reason()); + EXPECT_EQ(kTestErrorCode, errorcode->code()); +} + +TEST_F(StunTest, ReadMessageWithAUInt16ListAttribute) { + StunMessage msg; + size_t size = ReadStunMessage(&msg, kStunMessageWithUInt16ListAttribute); + CheckStunHeader(msg, STUN_BINDING_REQUEST, size); + const StunUInt16ListAttribute* types = msg.GetUnknownAttributes(); + ASSERT_TRUE(types != NULL); + EXPECT_EQ(3U, types->Size()); + EXPECT_EQ(0x1U, types->GetType(0)); + EXPECT_EQ(0x1000U, types->GetType(1)); + EXPECT_EQ(0xAB0CU, types->GetType(2)); +} + +TEST_F(StunTest, ReadMessageWithAnUnknownAttribute) { + StunMessage msg; + size_t size = ReadStunMessage(&msg, kStunMessageWithUnknownAttribute); + CheckStunHeader(msg, STUN_BINDING_REQUEST, size); + + // Parsing should have succeeded and there should be a USERNAME attribute + const StunByteStringAttribute* username = + msg.GetByteString(STUN_ATTR_USERNAME); + ASSERT_TRUE(username != NULL); + EXPECT_EQ(kTestUserName2, username->GetString()); +} + +TEST_F(StunTest, WriteMessageWithAnErrorCodeAttribute) { + StunMessage msg; + size_t size = sizeof(kStunMessageWithErrorAttribute); + + msg.SetType(STUN_BINDING_ERROR_RESPONSE); + msg.SetTransactionID( + std::string(reinterpret_cast(kTestTransactionId1), + kStunTransactionIdLength)); + CheckStunTransactionID(msg, kTestTransactionId1, kStunTransactionIdLength); + StunErrorCodeAttribute* errorcode = StunAttribute::CreateErrorCode(); + errorcode->SetCode(kTestErrorCode); + errorcode->SetReason(kTestErrorReason); + EXPECT_TRUE(msg.AddAttribute(errorcode)); + CheckStunHeader(msg, STUN_BINDING_ERROR_RESPONSE, (size - 20)); + + talk_base::ByteBuffer out; + EXPECT_TRUE(msg.Write(&out)); + ASSERT_EQ(size, out.Length()); + // No padding. + ASSERT_EQ(0, std::memcmp(out.Data(), kStunMessageWithErrorAttribute, size)); +} + +TEST_F(StunTest, WriteMessageWithAUInt16ListAttribute) { + StunMessage msg; + size_t size = sizeof(kStunMessageWithUInt16ListAttribute); + + msg.SetType(STUN_BINDING_REQUEST); + msg.SetTransactionID( + std::string(reinterpret_cast(kTestTransactionId2), + kStunTransactionIdLength)); + CheckStunTransactionID(msg, kTestTransactionId2, kStunTransactionIdLength); + StunUInt16ListAttribute* list = StunAttribute::CreateUnknownAttributes(); + list->AddType(0x1U); + list->AddType(0x1000U); + list->AddType(0xAB0CU); + EXPECT_TRUE(msg.AddAttribute(list)); + CheckStunHeader(msg, STUN_BINDING_REQUEST, (size - 20)); + + talk_base::ByteBuffer out; + EXPECT_TRUE(msg.Write(&out)); + ASSERT_EQ(size, out.Length()); + // Check everything up to the padding. + ASSERT_EQ(0, std::memcmp(out.Data(), kStunMessageWithUInt16ListAttribute, + size - 2)); +} + +// Test that we fail to read messages with invalid lengths. +void CheckFailureToRead(const unsigned char* testcase, size_t length) { + StunMessage msg; + const char* input = reinterpret_cast(testcase); + talk_base::ByteBuffer buf(input, length); + ASSERT_FALSE(msg.Read(&buf)); +} + +TEST_F(StunTest, FailToReadInvalidMessages) { + CheckFailureToRead(kStunMessageWithZeroLength, + kRealLengthOfInvalidLengthTestCases); + CheckFailureToRead(kStunMessageWithSmallLength, + kRealLengthOfInvalidLengthTestCases); + CheckFailureToRead(kStunMessageWithExcessLength, + kRealLengthOfInvalidLengthTestCases); +} + +// Test that we properly fail to read a non-STUN message. +TEST_F(StunTest, FailToReadRtcpPacket) { + CheckFailureToRead(kRtcpPacket, sizeof(kRtcpPacket)); +} + +// Check our STUN message validation code against the RFC5769 test messages. +TEST_F(StunTest, ValidateMessageIntegrity) { + // Try the messages from RFC 5769. + EXPECT_TRUE(StunMessage::ValidateMessageIntegrity( + reinterpret_cast(kRfc5769SampleRequest), + sizeof(kRfc5769SampleRequest), + kRfc5769SampleMsgPassword)); + EXPECT_FALSE(StunMessage::ValidateMessageIntegrity( + reinterpret_cast(kRfc5769SampleRequest), + sizeof(kRfc5769SampleRequest), + "InvalidPassword")); + + EXPECT_TRUE(StunMessage::ValidateMessageIntegrity( + reinterpret_cast(kRfc5769SampleResponse), + sizeof(kRfc5769SampleResponse), + kRfc5769SampleMsgPassword)); + EXPECT_FALSE(StunMessage::ValidateMessageIntegrity( + reinterpret_cast(kRfc5769SampleResponse), + sizeof(kRfc5769SampleResponse), + "InvalidPassword")); + + EXPECT_TRUE(StunMessage::ValidateMessageIntegrity( + reinterpret_cast(kRfc5769SampleResponseIPv6), + sizeof(kRfc5769SampleResponseIPv6), + kRfc5769SampleMsgPassword)); + EXPECT_FALSE(StunMessage::ValidateMessageIntegrity( + reinterpret_cast(kRfc5769SampleResponseIPv6), + sizeof(kRfc5769SampleResponseIPv6), + "InvalidPassword")); + + // We first need to compute the key for the long-term authentication HMAC. + std::string key; + ComputeStunCredentialHash(kRfc5769SampleMsgWithAuthUsername, + kRfc5769SampleMsgWithAuthRealm, kRfc5769SampleMsgWithAuthPassword, &key); + EXPECT_TRUE(StunMessage::ValidateMessageIntegrity( + reinterpret_cast(kRfc5769SampleRequestLongTermAuth), + sizeof(kRfc5769SampleRequestLongTermAuth), key)); + EXPECT_FALSE(StunMessage::ValidateMessageIntegrity( + reinterpret_cast(kRfc5769SampleRequestLongTermAuth), + sizeof(kRfc5769SampleRequestLongTermAuth), + "InvalidPassword")); + + // Try some edge cases. + EXPECT_FALSE(StunMessage::ValidateMessageIntegrity( + reinterpret_cast(kStunMessageWithZeroLength), + sizeof(kStunMessageWithZeroLength), + kRfc5769SampleMsgPassword)); + EXPECT_FALSE(StunMessage::ValidateMessageIntegrity( + reinterpret_cast(kStunMessageWithExcessLength), + sizeof(kStunMessageWithExcessLength), + kRfc5769SampleMsgPassword)); + EXPECT_FALSE(StunMessage::ValidateMessageIntegrity( + reinterpret_cast(kStunMessageWithSmallLength), + sizeof(kStunMessageWithSmallLength), + kRfc5769SampleMsgPassword)); + + // Test that munging a single bit anywhere in the message causes the + // message-integrity check to fail, unless it is after the M-I attribute. + char buf[sizeof(kRfc5769SampleRequest)]; + memcpy(buf, kRfc5769SampleRequest, sizeof(kRfc5769SampleRequest)); + for (size_t i = 0; i < sizeof(buf); ++i) { + buf[i] ^= 0x01; + if (i > 0) + buf[i - 1] ^= 0x01; + EXPECT_EQ(i >= sizeof(buf) - 8, StunMessage::ValidateMessageIntegrity( + buf, sizeof(buf), kRfc5769SampleMsgPassword)); + } +} + +// Validate that we generate correct MESSAGE-INTEGRITY attributes. +// Note the use of IceMessage instead of StunMessage; this is necessary because +// the RFC5769 test messages used include attributes not found in basic STUN. +TEST_F(StunTest, AddMessageIntegrity) { + IceMessage msg; + talk_base::ByteBuffer buf( + reinterpret_cast(kRfc5769SampleRequestWithoutMI), + sizeof(kRfc5769SampleRequestWithoutMI)); + EXPECT_TRUE(msg.Read(&buf)); + EXPECT_TRUE(msg.AddMessageIntegrity(kRfc5769SampleMsgPassword)); + const StunByteStringAttribute* mi_attr = + msg.GetByteString(STUN_ATTR_MESSAGE_INTEGRITY); + EXPECT_EQ(20U, mi_attr->length()); + EXPECT_EQ(0, std::memcmp( + mi_attr->bytes(), kCalculatedHmac1, sizeof(kCalculatedHmac1))); + + talk_base::ByteBuffer buf1; + EXPECT_TRUE(msg.Write(&buf1)); + EXPECT_TRUE(StunMessage::ValidateMessageIntegrity( + reinterpret_cast(buf1.Data()), buf1.Length(), + kRfc5769SampleMsgPassword)); + + IceMessage msg2; + talk_base::ByteBuffer buf2( + reinterpret_cast(kRfc5769SampleResponseWithoutMI), + sizeof(kRfc5769SampleResponseWithoutMI)); + EXPECT_TRUE(msg2.Read(&buf2)); + EXPECT_TRUE(msg2.AddMessageIntegrity(kRfc5769SampleMsgPassword)); + const StunByteStringAttribute* mi_attr2 = + msg2.GetByteString(STUN_ATTR_MESSAGE_INTEGRITY); + EXPECT_EQ(20U, mi_attr2->length()); + EXPECT_EQ(0, std::memcmp( + mi_attr2->bytes(), kCalculatedHmac2, sizeof(kCalculatedHmac2))); + + talk_base::ByteBuffer buf3; + EXPECT_TRUE(msg2.Write(&buf3)); + EXPECT_TRUE(StunMessage::ValidateMessageIntegrity( + reinterpret_cast(buf3.Data()), buf3.Length(), + kRfc5769SampleMsgPassword)); +} + +// Check our STUN message validation code against the RFC5769 test messages. +TEST_F(StunTest, ValidateFingerprint) { + EXPECT_TRUE(StunMessage::ValidateFingerprint( + reinterpret_cast(kRfc5769SampleRequest), + sizeof(kRfc5769SampleRequest))); + EXPECT_TRUE(StunMessage::ValidateFingerprint( + reinterpret_cast(kRfc5769SampleResponse), + sizeof(kRfc5769SampleResponse))); + EXPECT_TRUE(StunMessage::ValidateFingerprint( + reinterpret_cast(kRfc5769SampleResponseIPv6), + sizeof(kRfc5769SampleResponseIPv6))); + + EXPECT_FALSE(StunMessage::ValidateFingerprint( + reinterpret_cast(kStunMessageWithZeroLength), + sizeof(kStunMessageWithZeroLength))); + EXPECT_FALSE(StunMessage::ValidateFingerprint( + reinterpret_cast(kStunMessageWithExcessLength), + sizeof(kStunMessageWithExcessLength))); + EXPECT_FALSE(StunMessage::ValidateFingerprint( + reinterpret_cast(kStunMessageWithSmallLength), + sizeof(kStunMessageWithSmallLength))); + + // Test that munging a single bit anywhere in the message causes the + // fingerprint check to fail. + char buf[sizeof(kRfc5769SampleRequest)]; + memcpy(buf, kRfc5769SampleRequest, sizeof(kRfc5769SampleRequest)); + for (size_t i = 0; i < sizeof(buf); ++i) { + buf[i] ^= 0x01; + if (i > 0) + buf[i - 1] ^= 0x01; + EXPECT_FALSE(StunMessage::ValidateFingerprint(buf, sizeof(buf))); + } + // Put them all back to normal and the check should pass again. + buf[sizeof(buf) - 1] ^= 0x01; + EXPECT_TRUE(StunMessage::ValidateFingerprint(buf, sizeof(buf))); +} + +TEST_F(StunTest, AddFingerprint) { + IceMessage msg; + talk_base::ByteBuffer buf( + reinterpret_cast(kRfc5769SampleRequestWithoutMI), + sizeof(kRfc5769SampleRequestWithoutMI)); + EXPECT_TRUE(msg.Read(&buf)); + EXPECT_TRUE(msg.AddFingerprint()); + + talk_base::ByteBuffer buf1; + EXPECT_TRUE(msg.Write(&buf1)); + EXPECT_TRUE(StunMessage::ValidateFingerprint( + reinterpret_cast(buf1.Data()), buf1.Length())); +} + +// Sample "GTURN" relay message. +static const unsigned char kRelayMessage[] = { + 0x00, 0x01, 0x00, 88, // message header + 0x21, 0x12, 0xA4, 0x42, // magic cookie + '0', '1', '2', '3', // transaction id + '4', '5', '6', '7', + '8', '9', 'a', 'b', + 0x00, 0x01, 0x00, 8, // mapped address + 0x00, 0x01, 0x00, 13, + 0x00, 0x00, 0x00, 17, + 0x00, 0x06, 0x00, 12, // username + 'a', 'b', 'c', 'd', + 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', + 0x00, 0x0d, 0x00, 4, // lifetime + 0x00, 0x00, 0x00, 11, + 0x00, 0x0f, 0x00, 4, // magic cookie + 0x72, 0xc6, 0x4b, 0xc6, + 0x00, 0x10, 0x00, 4, // bandwidth + 0x00, 0x00, 0x00, 6, + 0x00, 0x11, 0x00, 8, // destination address + 0x00, 0x01, 0x00, 13, + 0x00, 0x00, 0x00, 17, + 0x00, 0x12, 0x00, 8, // source address 2 + 0x00, 0x01, 0x00, 13, + 0x00, 0x00, 0x00, 17, + 0x00, 0x13, 0x00, 7, // data + 'a', 'b', 'c', 'd', + 'e', 'f', 'g', 0 // DATA must be padded per rfc5766. +}; + +// Test that we can read the GTURN-specific fields. +TEST_F(StunTest, ReadRelayMessage) { + RelayMessage msg, msg2; + + const char* input = reinterpret_cast(kRelayMessage); + size_t size = sizeof(kRelayMessage); + talk_base::ByteBuffer buf(input, size); + EXPECT_TRUE(msg.Read(&buf)); + + EXPECT_EQ(STUN_BINDING_REQUEST, msg.type()); + EXPECT_EQ(size - 20, msg.length()); + EXPECT_EQ("0123456789ab", msg.transaction_id()); + + msg2.SetType(STUN_BINDING_REQUEST); + msg2.SetTransactionID("0123456789ab"); + + in_addr legacy_in_addr; + legacy_in_addr.s_addr = htonl(17U); + talk_base::IPAddress legacy_ip(legacy_in_addr); + + const StunAddressAttribute* addr = msg.GetAddress(STUN_ATTR_MAPPED_ADDRESS); + ASSERT_TRUE(addr != NULL); + EXPECT_EQ(1, addr->family()); + EXPECT_EQ(13, addr->port()); + EXPECT_EQ(legacy_ip, addr->ipaddr()); + + StunAddressAttribute* addr2 = + StunAttribute::CreateAddress(STUN_ATTR_MAPPED_ADDRESS); + addr2->SetPort(13); + addr2->SetIP(legacy_ip); + EXPECT_TRUE(msg2.AddAttribute(addr2)); + + const StunByteStringAttribute* bytes = msg.GetByteString(STUN_ATTR_USERNAME); + ASSERT_TRUE(bytes != NULL); + EXPECT_EQ(12U, bytes->length()); + EXPECT_EQ("abcdefghijkl", bytes->GetString()); + + StunByteStringAttribute* bytes2 = + StunAttribute::CreateByteString(STUN_ATTR_USERNAME); + bytes2->CopyBytes("abcdefghijkl"); + EXPECT_TRUE(msg2.AddAttribute(bytes2)); + + const StunUInt32Attribute* uval = msg.GetUInt32(STUN_ATTR_LIFETIME); + ASSERT_TRUE(uval != NULL); + EXPECT_EQ(11U, uval->value()); + + StunUInt32Attribute* uval2 = StunAttribute::CreateUInt32(STUN_ATTR_LIFETIME); + uval2->SetValue(11); + EXPECT_TRUE(msg2.AddAttribute(uval2)); + + bytes = msg.GetByteString(STUN_ATTR_MAGIC_COOKIE); + ASSERT_TRUE(bytes != NULL); + EXPECT_EQ(4U, bytes->length()); + EXPECT_EQ(0, std::memcmp(bytes->bytes(), TURN_MAGIC_COOKIE_VALUE, + sizeof(TURN_MAGIC_COOKIE_VALUE))); + + bytes2 = StunAttribute::CreateByteString(STUN_ATTR_MAGIC_COOKIE); + bytes2->CopyBytes(reinterpret_cast(TURN_MAGIC_COOKIE_VALUE), + sizeof(TURN_MAGIC_COOKIE_VALUE)); + EXPECT_TRUE(msg2.AddAttribute(bytes2)); + + uval = msg.GetUInt32(STUN_ATTR_BANDWIDTH); + ASSERT_TRUE(uval != NULL); + EXPECT_EQ(6U, uval->value()); + + uval2 = StunAttribute::CreateUInt32(STUN_ATTR_BANDWIDTH); + uval2->SetValue(6); + EXPECT_TRUE(msg2.AddAttribute(uval2)); + + addr = msg.GetAddress(STUN_ATTR_DESTINATION_ADDRESS); + ASSERT_TRUE(addr != NULL); + EXPECT_EQ(1, addr->family()); + EXPECT_EQ(13, addr->port()); + EXPECT_EQ(legacy_ip, addr->ipaddr()); + + addr2 = StunAttribute::CreateAddress(STUN_ATTR_DESTINATION_ADDRESS); + addr2->SetPort(13); + addr2->SetIP(legacy_ip); + EXPECT_TRUE(msg2.AddAttribute(addr2)); + + addr = msg.GetAddress(STUN_ATTR_SOURCE_ADDRESS2); + ASSERT_TRUE(addr != NULL); + EXPECT_EQ(1, addr->family()); + EXPECT_EQ(13, addr->port()); + EXPECT_EQ(legacy_ip, addr->ipaddr()); + + addr2 = StunAttribute::CreateAddress(STUN_ATTR_SOURCE_ADDRESS2); + addr2->SetPort(13); + addr2->SetIP(legacy_ip); + EXPECT_TRUE(msg2.AddAttribute(addr2)); + + bytes = msg.GetByteString(STUN_ATTR_DATA); + ASSERT_TRUE(bytes != NULL); + EXPECT_EQ(7U, bytes->length()); + EXPECT_EQ("abcdefg", bytes->GetString()); + + bytes2 = StunAttribute::CreateByteString(STUN_ATTR_DATA); + bytes2->CopyBytes("abcdefg"); + EXPECT_TRUE(msg2.AddAttribute(bytes2)); + + talk_base::ByteBuffer out; + EXPECT_TRUE(msg.Write(&out)); + EXPECT_EQ(size, out.Length()); + size_t len1 = out.Length(); + std::string outstring; + out.ReadString(&outstring, len1); + EXPECT_EQ(0, std::memcmp(outstring.c_str(), input, len1)); + + talk_base::ByteBuffer out2; + EXPECT_TRUE(msg2.Write(&out2)); + EXPECT_EQ(size, out2.Length()); + size_t len2 = out2.Length(); + std::string outstring2; + out2.ReadString(&outstring2, len2); + EXPECT_EQ(0, std::memcmp(outstring2.c_str(), input, len2)); +} + +} // namespace cricket diff --git a/talk/p2p/base/stunport.cc b/talk/p2p/base/stunport.cc new file mode 100644 index 000000000..e182a51cb --- /dev/null +++ b/talk/p2p/base/stunport.cc @@ -0,0 +1,353 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/stunport.h" + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/helpers.h" +#include "talk/base/nethelpers.h" +#include "talk/p2p/base/common.h" +#include "talk/p2p/base/stun.h" + +namespace cricket { + +// TODO: Move these to a common place (used in relayport too) +const int KEEPALIVE_DELAY = 10 * 1000; // 10 seconds - sort timeouts +const int RETRY_DELAY = 50; // 50ms, from ICE spec +const int RETRY_TIMEOUT = 50 * 1000; // ICE says 50 secs + +// Handles a binding request sent to the STUN server. +class StunBindingRequest : public StunRequest { + public: + StunBindingRequest(UDPPort* port, bool keep_alive, + const talk_base::SocketAddress& addr) + : port_(port), keep_alive_(keep_alive), server_addr_(addr) { + start_time_ = talk_base::Time(); + } + + virtual ~StunBindingRequest() { + } + + const talk_base::SocketAddress& server_addr() const { return server_addr_; } + + virtual void Prepare(StunMessage* request) { + request->SetType(STUN_BINDING_REQUEST); + } + + virtual void OnResponse(StunMessage* response) { + const StunAddressAttribute* addr_attr = + response->GetAddress(STUN_ATTR_MAPPED_ADDRESS); + if (!addr_attr) { + LOG(LS_ERROR) << "Binding response missing mapped address."; + } else if (addr_attr->family() != STUN_ADDRESS_IPV4 && + addr_attr->family() != STUN_ADDRESS_IPV6) { + LOG(LS_ERROR) << "Binding address has bad family"; + } else { + talk_base::SocketAddress addr(addr_attr->ipaddr(), addr_attr->port()); + port_->OnStunBindingRequestSucceeded(addr); + } + + // We will do a keep-alive regardless of whether this request suceeds. + // This should have almost no impact on network usage. + if (keep_alive_) { + port_->requests_.SendDelayed( + new StunBindingRequest(port_, true, server_addr_), + port_->stun_keepalive_delay()); + } + } + + virtual void OnErrorResponse(StunMessage* response) { + const StunErrorCodeAttribute* attr = response->GetErrorCode(); + if (!attr) { + LOG(LS_ERROR) << "Bad allocate response error code"; + } else { + LOG(LS_ERROR) << "Binding error response:" + << " class=" << attr->eclass() + << " number=" << attr->number() + << " reason='" << attr->reason() << "'"; + } + + port_->OnStunBindingOrResolveRequestFailed(); + + if (keep_alive_ + && (talk_base::TimeSince(start_time_) <= RETRY_TIMEOUT)) { + port_->requests_.SendDelayed( + new StunBindingRequest(port_, true, server_addr_), + port_->stun_keepalive_delay()); + } + } + + virtual void OnTimeout() { + LOG(LS_ERROR) << "Binding request timed out from " + << port_->GetLocalAddress().ToSensitiveString() + << " (" << port_->Network()->name() << ")"; + + port_->OnStunBindingOrResolveRequestFailed(); + + if (keep_alive_ + && (talk_base::TimeSince(start_time_) <= RETRY_TIMEOUT)) { + port_->requests_.SendDelayed( + new StunBindingRequest(port_, true, server_addr_), + RETRY_DELAY); + } + } + + private: + UDPPort* port_; + bool keep_alive_; + talk_base::SocketAddress server_addr_; + uint32 start_time_; +}; + +UDPPort::UDPPort(talk_base::Thread* thread, + talk_base::Network* network, + talk_base::AsyncPacketSocket* socket, + const std::string& username, const std::string& password) + : Port(thread, network, socket->GetLocalAddress().ipaddr(), + username, password), + requests_(thread), + socket_(socket), + error_(0), + resolver_(NULL), + ready_(false), + stun_keepalive_delay_(KEEPALIVE_DELAY) { +} + +UDPPort::UDPPort(talk_base::Thread* thread, + talk_base::PacketSocketFactory* factory, + talk_base::Network* network, + const talk_base::IPAddress& ip, int min_port, int max_port, + const std::string& username, const std::string& password) + : Port(thread, LOCAL_PORT_TYPE, factory, network, ip, min_port, max_port, + username, password), + requests_(thread), + socket_(NULL), + error_(0), + resolver_(NULL), + ready_(false), + stun_keepalive_delay_(KEEPALIVE_DELAY) { +} + +bool UDPPort::Init() { + if (!SharedSocket()) { + ASSERT(socket_ == NULL); + socket_ = socket_factory()->CreateUdpSocket( + talk_base::SocketAddress(ip(), 0), min_port(), max_port()); + if (!socket_) { + LOG_J(LS_WARNING, this) << "UDP socket creation failed"; + return false; + } + socket_->SignalReadPacket.connect(this, &UDPPort::OnReadPacket); + } + socket_->SignalReadyToSend.connect(this, &UDPPort::OnReadyToSend); + socket_->SignalAddressReady.connect(this, &UDPPort::OnLocalAddressReady); + requests_.SignalSendPacket.connect(this, &UDPPort::OnSendPacket); + return true; +} + +UDPPort::~UDPPort() { + if (resolver_) { + resolver_->Destroy(false); + } + if (!SharedSocket()) + delete socket_; +} + +void UDPPort::PrepareAddress() { + ASSERT(requests_.empty()); + if (socket_->GetState() == talk_base::AsyncPacketSocket::STATE_BOUND) { + OnLocalAddressReady(socket_, socket_->GetLocalAddress()); + } +} + +void UDPPort::MaybePrepareStunCandidate() { + // Sending binding request to the STUN server if address is available to + // prepare STUN candidate. + if (!server_addr_.IsNil()) { + SendStunBindingRequest(); + } else { + // Processing host candidate address. + SetResult(true); + } +} + +Connection* UDPPort::CreateConnection(const Candidate& address, + CandidateOrigin origin) { + if (address.protocol() != "udp") + return NULL; + + if (!IsCompatibleAddress(address.address())) { + return NULL; + } + + if (SharedSocket() && Candidates()[0].type() != LOCAL_PORT_TYPE) { + ASSERT(false); + return NULL; + } + + Connection* conn = new ProxyConnection(this, 0, address); + AddConnection(conn); + return conn; +} + +int UDPPort::SendTo(const void* data, size_t size, + const talk_base::SocketAddress& addr, bool payload) { + int sent = socket_->SendTo(data, size, addr); + if (sent < 0) { + error_ = socket_->GetError(); + LOG_J(LS_ERROR, this) << "UDP send of " << size + << " bytes failed with error " << error_; + } + return sent; +} + +int UDPPort::SetOption(talk_base::Socket::Option opt, int value) { + return socket_->SetOption(opt, value); +} + +int UDPPort::GetOption(talk_base::Socket::Option opt, int* value) { + return socket_->GetOption(opt, value); +} + +int UDPPort::GetError() { + return error_; +} + +void UDPPort::OnLocalAddressReady(talk_base::AsyncPacketSocket* socket, + const talk_base::SocketAddress& address) { + AddAddress(address, address, UDP_PROTOCOL_NAME, LOCAL_PORT_TYPE, + ICE_TYPE_PREFERENCE_HOST, false); + MaybePrepareStunCandidate(); +} + +void UDPPort::OnReadPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& remote_addr) { + ASSERT(socket == socket_); + + // Look for a response from the STUN server. + // Even if the response doesn't match one of our outstanding requests, we + // will eat it because it might be a response to a retransmitted packet, and + // we already cleared the request when we got the first response. + ASSERT(!server_addr_.IsUnresolved()); + if (remote_addr == server_addr_) { + requests_.CheckResponse(data, size); + return; + } + + if (Connection* conn = GetConnection(remote_addr)) { + conn->OnReadPacket(data, size); + } else { + Port::OnReadPacket(data, size, remote_addr, PROTO_UDP); + } +} + +void UDPPort::OnReadyToSend(talk_base::AsyncPacketSocket* socket) { + Port::OnReadyToSend(); +} + +void UDPPort::SendStunBindingRequest() { + // We will keep pinging the stun server to make sure our NAT pin-hole stays + // open during the call. + // TODO: Support multiple stun servers, or make ResolveStunAddress find a + // server with the correct family, or something similar. + ASSERT(requests_.empty()); + if (server_addr_.IsUnresolved()) { + ResolveStunAddress(); + } else if (socket_->GetState() == talk_base::AsyncPacketSocket::STATE_BOUND) { + if (server_addr_.family() == ip().family()) { + requests_.Send(new StunBindingRequest(this, true, server_addr_)); + } + } +} + +void UDPPort::ResolveStunAddress() { + if (resolver_) + return; + + resolver_ = new talk_base::AsyncResolver(); + resolver_->SignalWorkDone.connect(this, &UDPPort::OnResolveResult); + resolver_->set_address(server_addr_); + resolver_->Start(); +} + +void UDPPort::OnResolveResult(talk_base::SignalThread* t) { + ASSERT(t == resolver_); + if (resolver_->error() != 0) { + LOG_J(LS_WARNING, this) << "StunPort: stun host lookup received error " + << resolver_->error(); + OnStunBindingOrResolveRequestFailed(); + } + + server_addr_ = resolver_->address(); + SendStunBindingRequest(); +} + +void UDPPort::OnStunBindingRequestSucceeded( + const talk_base::SocketAddress& stun_addr) { + if (ready_) // Discarding the binding response if port is already enabled. + return; + + if (!SharedSocket() || stun_addr != socket_->GetLocalAddress()) { + // If socket is shared and |stun_addr| is equal to local socket + // address then discarding the stun address. + // Setting related address before STUN candidate is added. For STUN + // related address is local socket address. + set_related_address(socket_->GetLocalAddress()); + AddAddress(stun_addr, socket_->GetLocalAddress(), UDP_PROTOCOL_NAME, + STUN_PORT_TYPE, ICE_TYPE_PREFERENCE_PRFLX, false); + } + SetResult(true); +} + +void UDPPort::OnStunBindingOrResolveRequestFailed() { + if (ready_) // Discarding failure response if port is already enabled. + return; + + // If socket is shared, we should process local udp candidate. + SetResult(SharedSocket()); +} + +void UDPPort::SetResult(bool success) { + // Setting ready status. + ready_ = true; + if (success) { + SignalPortComplete(this); + } else { + SignalPortError(this); + } +} + +// TODO: merge this with SendTo above. +void UDPPort::OnSendPacket(const void* data, size_t size, StunRequest* req) { + StunBindingRequest* sreq = static_cast(req); + if (socket_->SendTo(data, size, sreq->server_addr()) < 0) + PLOG(LERROR, socket_->GetError()) << "sendto"; +} + +} // namespace cricket diff --git a/talk/p2p/base/stunport.h b/talk/p2p/base/stunport.h new file mode 100644 index 000000000..3f982d57b --- /dev/null +++ b/talk/p2p/base/stunport.h @@ -0,0 +1,208 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_STUNPORT_H_ +#define TALK_P2P_BASE_STUNPORT_H_ + +#include + +#include "talk/base/asyncpacketsocket.h" +#include "talk/p2p/base/port.h" +#include "talk/p2p/base/stunrequest.h" + +// TODO(mallinath) - Rename stunport.cc|h to udpport.cc|h. +namespace talk_base { +class AsyncResolver; +class SignalThread; +} + +namespace cricket { + +// Communicates using the address on the outside of a NAT. +class UDPPort : public Port { + public: + static UDPPort* Create(talk_base::Thread* thread, + talk_base::Network* network, + talk_base::AsyncPacketSocket* socket, + const std::string& username, + const std::string& password) { + UDPPort* port = new UDPPort(thread, network, socket, username, password); + if (!port->Init()) { + delete port; + port = NULL; + } + return port; + } + + static UDPPort* Create(talk_base::Thread* thread, + talk_base::PacketSocketFactory* factory, + talk_base::Network* network, + const talk_base::IPAddress& ip, + int min_port, int max_port, + const std::string& username, + const std::string& password) { + UDPPort* port = new UDPPort(thread, factory, network, + ip, min_port, max_port, + username, password); + if (!port->Init()) { + delete port; + port = NULL; + } + return port; + } + virtual ~UDPPort(); + + talk_base::SocketAddress GetLocalAddress() const { + return socket_->GetLocalAddress(); + } + + const talk_base::SocketAddress& server_addr() const { return server_addr_; } + void set_server_addr(const talk_base::SocketAddress& addr) { + server_addr_ = addr; + } + + virtual void PrepareAddress(); + + virtual Connection* CreateConnection(const Candidate& address, + CandidateOrigin origin); + virtual int SetOption(talk_base::Socket::Option opt, int value); + virtual int GetOption(talk_base::Socket::Option opt, int* value); + virtual int GetError(); + + virtual bool HandleIncomingPacket( + talk_base::AsyncPacketSocket* socket, const char* data, size_t size, + const talk_base::SocketAddress& remote_addr) { + // All packets given to UDP port will be consumed. + OnReadPacket(socket, data, size, remote_addr); + return true; + } + + void set_stun_keepalive_delay(int delay) { + stun_keepalive_delay_ = delay; + } + int stun_keepalive_delay() const { + return stun_keepalive_delay_; + } + + protected: + UDPPort(talk_base::Thread* thread, talk_base::PacketSocketFactory* factory, + talk_base::Network* network, const talk_base::IPAddress& ip, + int min_port, int max_port, + const std::string& username, const std::string& password); + + UDPPort(talk_base::Thread* thread, talk_base::Network* network, + talk_base::AsyncPacketSocket* socket, + const std::string& username, const std::string& password); + + bool Init(); + + virtual int SendTo(const void* data, size_t size, + const talk_base::SocketAddress& addr, bool payload); + + void OnLocalAddressReady(talk_base::AsyncPacketSocket* socket, + const talk_base::SocketAddress& address); + void OnReadPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& remote_addr); + void OnReadyToSend(talk_base::AsyncPacketSocket* socket); + + // This method will send STUN binding request if STUN server address is set. + void MaybePrepareStunCandidate(); + + void SendStunBindingRequest(); + + + private: + // DNS resolution of the STUN server. + void ResolveStunAddress(); + void OnResolveResult(talk_base::SignalThread* thread); + + // Below methods handles binding request responses. + void OnStunBindingRequestSucceeded(const talk_base::SocketAddress& stun_addr); + void OnStunBindingOrResolveRequestFailed(); + + // Sends STUN requests to the server. + void OnSendPacket(const void* data, size_t size, StunRequest* req); + + // TODO(mallinaht) - Move this up to cricket::Port when SignalAddressReady is + // changed to SignalPortReady. + void SetResult(bool success); + + talk_base::SocketAddress server_addr_; + StunRequestManager requests_; + talk_base::AsyncPacketSocket* socket_; + int error_; + talk_base::AsyncResolver* resolver_; + bool ready_; + int stun_keepalive_delay_; + + friend class StunBindingRequest; +}; + +class StunPort : public UDPPort { + public: + static StunPort* Create(talk_base::Thread* thread, + talk_base::PacketSocketFactory* factory, + talk_base::Network* network, + const talk_base::IPAddress& ip, + int min_port, int max_port, + const std::string& username, + const std::string& password, + const talk_base::SocketAddress& server_addr) { + StunPort* port = new StunPort(thread, factory, network, + ip, min_port, max_port, + username, password, server_addr); + if (!port->Init()) { + delete port; + port = NULL; + } + return port; + } + + virtual ~StunPort() {} + + virtual void PrepareAddress() { + SendStunBindingRequest(); + } + + protected: + StunPort(talk_base::Thread* thread, talk_base::PacketSocketFactory* factory, + talk_base::Network* network, const talk_base::IPAddress& ip, + int min_port, int max_port, + const std::string& username, const std::string& password, + const talk_base::SocketAddress& server_address) + : UDPPort(thread, factory, network, ip, min_port, max_port, username, + password) { + // UDPPort will set these to local udp, updating these to STUN. + set_type(STUN_PORT_TYPE); + set_server_addr(server_address); + } +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_STUNPORT_H_ diff --git a/talk/p2p/base/stunport_unittest.cc b/talk/p2p/base/stunport_unittest.cc new file mode 100644 index 000000000..ba36c480e --- /dev/null +++ b/talk/p2p/base/stunport_unittest.cc @@ -0,0 +1,166 @@ +/* + * libjingle + * Copyright 2009 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/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/socketaddress.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/base/stunport.h" +#include "talk/p2p/base/teststunserver.h" + +using talk_base::SocketAddress; + +static const SocketAddress kLocalAddr("127.0.0.1", 0); +static const SocketAddress kStunAddr("127.0.0.1", 5000); +static const SocketAddress kBadAddr("0.0.0.1", 5000); +static const SocketAddress kStunHostnameAddr("localhost", 5000); +static const SocketAddress kBadHostnameAddr("not-a-real-hostname", 5000); +static const int kTimeoutMs = 10000; + +// Tests connecting a StunPort to a fake STUN server (cricket::StunServer) +// TODO: Use a VirtualSocketServer here. We have to use a +// PhysicalSocketServer right now since DNS is not part of SocketServer yet. +class StunPortTest : public testing::Test, + public sigslot::has_slots<> { + public: + StunPortTest() + : network_("unittest", "unittest", talk_base::IPAddress(INADDR_ANY), 32), + socket_factory_(talk_base::Thread::Current()), + stun_server_(new cricket::TestStunServer( + talk_base::Thread::Current(), kStunAddr)), + done_(false), error_(false), stun_keepalive_delay_(0) { + } + + const cricket::Port* port() const { return stun_port_.get(); } + bool done() const { return done_; } + bool error() const { return error_; } + + void CreateStunPort(const talk_base::SocketAddress& server_addr) { + stun_port_.reset(cricket::StunPort::Create( + talk_base::Thread::Current(), &socket_factory_, &network_, + kLocalAddr.ipaddr(), 0, 0, talk_base::CreateRandomString(16), + talk_base::CreateRandomString(22), server_addr)); + stun_port_->set_stun_keepalive_delay(stun_keepalive_delay_); + stun_port_->SignalPortComplete.connect(this, + &StunPortTest::OnPortComplete); + stun_port_->SignalPortError.connect(this, + &StunPortTest::OnPortError); + } + + void PrepareAddress() { + stun_port_->PrepareAddress(); + } + + protected: + static void SetUpTestCase() { + // Ensure the RNG is inited. + talk_base::InitRandom(NULL, 0); + } + + void OnPortComplete(cricket::Port* port) { + done_ = true; + error_ = false; + } + void OnPortError(cricket::Port* port) { + done_ = true; + error_ = true; + } + void SetKeepaliveDelay(int delay) { + stun_keepalive_delay_ = delay; + } + + private: + talk_base::Network network_; + talk_base::BasicPacketSocketFactory socket_factory_; + talk_base::scoped_ptr stun_port_; + talk_base::scoped_ptr stun_server_; + bool done_; + bool error_; + int stun_keepalive_delay_; +}; + +// Test that we can create a STUN port +TEST_F(StunPortTest, TestBasic) { + CreateStunPort(kStunAddr); + EXPECT_EQ("stun", port()->Type()); + EXPECT_EQ(0U, port()->Candidates().size()); +} + +// Test that we can get an address from a STUN server. +TEST_F(StunPortTest, TestPrepareAddress) { + CreateStunPort(kStunAddr); + PrepareAddress(); + EXPECT_TRUE_WAIT(done(), kTimeoutMs); + ASSERT_EQ(1U, port()->Candidates().size()); + EXPECT_TRUE(kLocalAddr.EqualIPs(port()->Candidates()[0].address())); + + // TODO: Add IPv6 tests here, once either physicalsocketserver supports + // IPv6, or this test is changed to use VirtualSocketServer. +} + +// Test that we fail properly if we can't get an address. +TEST_F(StunPortTest, TestPrepareAddressFail) { + CreateStunPort(kBadAddr); + PrepareAddress(); + EXPECT_TRUE_WAIT(done(), kTimeoutMs); + EXPECT_TRUE(error()); + EXPECT_EQ(0U, port()->Candidates().size()); +} + +// Test that we can get an address from a STUN server specified by a hostname. +TEST_F(StunPortTest, TestPrepareAddressHostname) { + CreateStunPort(kStunHostnameAddr); + PrepareAddress(); + EXPECT_TRUE_WAIT(done(), kTimeoutMs); + ASSERT_EQ(1U, port()->Candidates().size()); + EXPECT_TRUE(kLocalAddr.EqualIPs(port()->Candidates()[0].address())); +} + +// Test that we handle hostname lookup failures properly. +TEST_F(StunPortTest, TestPrepareAddressHostnameFail) { + CreateStunPort(kBadHostnameAddr); + PrepareAddress(); + EXPECT_TRUE_WAIT(done(), kTimeoutMs); + EXPECT_TRUE(error()); + EXPECT_EQ(0U, port()->Candidates().size()); +} + +// This test verifies keepalive response messages don't result in +// additional candidate generation. +TEST_F(StunPortTest, TestKeepAliveResponse) { + SetKeepaliveDelay(500); // 500ms of keepalive delay. + CreateStunPort(kStunHostnameAddr); + PrepareAddress(); + EXPECT_TRUE_WAIT(done(), kTimeoutMs); + ASSERT_EQ(1U, port()->Candidates().size()); + EXPECT_TRUE(kLocalAddr.EqualIPs(port()->Candidates()[0].address())); + // Waiting for 1 seond, which will allow us to process + // response for keepalive binding request. 500 ms is the keepalive delay. + talk_base::Thread::Current()->ProcessMessages(1000); + ASSERT_EQ(1U, port()->Candidates().size()); +} + diff --git a/talk/p2p/base/stunrequest.cc b/talk/p2p/base/stunrequest.cc new file mode 100644 index 000000000..b3b1118c4 --- /dev/null +++ b/talk/p2p/base/stunrequest.cc @@ -0,0 +1,210 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/stunrequest.h" + +#include "talk/base/common.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" + +namespace cricket { + +const uint32 MSG_STUN_SEND = 1; + +const int MAX_SENDS = 9; +const int DELAY_UNIT = 100; // 100 milliseconds +const int DELAY_MAX_FACTOR = 16; + +StunRequestManager::StunRequestManager(talk_base::Thread* thread) + : thread_(thread) { +} + +StunRequestManager::~StunRequestManager() { + while (requests_.begin() != requests_.end()) { + StunRequest *request = requests_.begin()->second; + requests_.erase(requests_.begin()); + delete request; + } +} + +void StunRequestManager::Send(StunRequest* request) { + SendDelayed(request, 0); +} + +void StunRequestManager::SendDelayed(StunRequest* request, int delay) { + request->set_manager(this); + ASSERT(requests_.find(request->id()) == requests_.end()); + request->Construct(); + requests_[request->id()] = request; + thread_->PostDelayed(delay, request, MSG_STUN_SEND, NULL); +} + +void StunRequestManager::Remove(StunRequest* request) { + ASSERT(request->manager() == this); + RequestMap::iterator iter = requests_.find(request->id()); + if (iter != requests_.end()) { + ASSERT(iter->second == request); + requests_.erase(iter); + thread_->Clear(request); + } +} + +void StunRequestManager::Clear() { + std::vector requests; + for (RequestMap::iterator i = requests_.begin(); i != requests_.end(); ++i) + requests.push_back(i->second); + + for (uint32 i = 0; i < requests.size(); ++i) { + // StunRequest destructor calls Remove() which deletes requests + // from |requests_|. + delete requests[i]; + } +} + +bool StunRequestManager::CheckResponse(StunMessage* msg) { + RequestMap::iterator iter = requests_.find(msg->transaction_id()); + if (iter == requests_.end()) + return false; + + StunRequest* request = iter->second; + if (msg->type() == GetStunSuccessResponseType(request->type())) { + request->OnResponse(msg); + } else if (msg->type() == GetStunErrorResponseType(request->type())) { + request->OnErrorResponse(msg); + } else { + LOG(LERROR) << "Received response with wrong type: " << msg->type() + << " (expecting " + << GetStunSuccessResponseType(request->type()) << ")"; + return false; + } + + delete request; + return true; +} + +bool StunRequestManager::CheckResponse(const char* data, size_t size) { + // Check the appropriate bytes of the stream to see if they match the + // transaction ID of a response we are expecting. + + if (size < 20) + return false; + + std::string id; + id.append(data + kStunTransactionIdOffset, kStunTransactionIdLength); + + RequestMap::iterator iter = requests_.find(id); + if (iter == requests_.end()) + return false; + + // Parse the STUN message and continue processing as usual. + + talk_base::ByteBuffer buf(data, size); + talk_base::scoped_ptr response(iter->second->msg_->CreateNew()); + if (!response->Read(&buf)) + return false; + + return CheckResponse(response.get()); +} + +StunRequest::StunRequest() + : count_(0), timeout_(false), manager_(0), + msg_(new StunMessage()), tstamp_(0) { + msg_->SetTransactionID( + talk_base::CreateRandomString(kStunTransactionIdLength)); +} + +StunRequest::StunRequest(StunMessage* request) + : count_(0), timeout_(false), manager_(0), + msg_(request), tstamp_(0) { + msg_->SetTransactionID( + talk_base::CreateRandomString(kStunTransactionIdLength)); +} + +StunRequest::~StunRequest() { + ASSERT(manager_ != NULL); + if (manager_) { + manager_->Remove(this); + manager_->thread_->Clear(this); + } + delete msg_; +} + +void StunRequest::Construct() { + if (msg_->type() == 0) { + Prepare(msg_); + ASSERT(msg_->type() != 0); + } +} + +int StunRequest::type() { + ASSERT(msg_ != NULL); + return msg_->type(); +} + +const StunMessage* StunRequest::msg() const { + return msg_; +} + +uint32 StunRequest::Elapsed() const { + return talk_base::TimeSince(tstamp_); +} + + +void StunRequest::set_manager(StunRequestManager* manager) { + ASSERT(!manager_); + manager_ = manager; +} + +void StunRequest::OnMessage(talk_base::Message* pmsg) { + ASSERT(manager_ != NULL); + ASSERT(pmsg->message_id == MSG_STUN_SEND); + + if (timeout_) { + OnTimeout(); + delete this; + return; + } + + tstamp_ = talk_base::Time(); + + talk_base::ByteBuffer buf; + msg_->Write(&buf); + manager_->SignalSendPacket(buf.Data(), buf.Length(), this); + + int delay = GetNextDelay(); + manager_->thread_->PostDelayed(delay, this, MSG_STUN_SEND, NULL); +} + +int StunRequest::GetNextDelay() { + int delay = DELAY_UNIT * talk_base::_min(1 << count_, DELAY_MAX_FACTOR); + count_ += 1; + if (count_ == MAX_SENDS) + timeout_ = true; + return delay; +} + +} // namespace cricket diff --git a/talk/p2p/base/stunrequest.h b/talk/p2p/base/stunrequest.h new file mode 100644 index 000000000..f2c85b3c1 --- /dev/null +++ b/talk/p2p/base/stunrequest.h @@ -0,0 +1,133 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_STUNREQUEST_H_ +#define TALK_P2P_BASE_STUNREQUEST_H_ + +#include "talk/base/sigslot.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/stun.h" +#include +#include + +namespace cricket { + +class StunRequest; + +// Manages a set of STUN requests, sending and resending until we receive a +// response or determine that the request has timed out. +class StunRequestManager { +public: + StunRequestManager(talk_base::Thread* thread); + ~StunRequestManager(); + + // Starts sending the given request (perhaps after a delay). + void Send(StunRequest* request); + void SendDelayed(StunRequest* request, int delay); + + // Removes a stun request that was added previously. This will happen + // automatically when a request succeeds, fails, or times out. + void Remove(StunRequest* request); + + // Removes all stun requests that were added previously. + void Clear(); + + // Determines whether the given message is a response to one of the + // outstanding requests, and if so, processes it appropriately. + bool CheckResponse(StunMessage* msg); + bool CheckResponse(const char* data, size_t size); + + bool empty() { return requests_.empty(); } + + // Raised when there are bytes to be sent. + sigslot::signal3 SignalSendPacket; + +private: + typedef std::map RequestMap; + + talk_base::Thread* thread_; + RequestMap requests_; + + friend class StunRequest; +}; + +// Represents an individual request to be sent. The STUN message can either be +// constructed beforehand or built on demand. +class StunRequest : public talk_base::MessageHandler { +public: + StunRequest(); + StunRequest(StunMessage* request); + virtual ~StunRequest(); + + // Causes our wrapped StunMessage to be Prepared + void Construct(); + + // The manager handling this request (if it has been scheduled for sending). + StunRequestManager* manager() { return manager_; } + + // Returns the transaction ID of this request. + const std::string& id() { return msg_->transaction_id(); } + + // Returns the STUN type of the request message. + int type(); + + // Returns a const pointer to |msg_|. + const StunMessage* msg() const; + + // Time elapsed since last send (in ms) + uint32 Elapsed() const; + +protected: + int count_; + bool timeout_; + + // Fills in a request object to be sent. Note that request's transaction ID + // will already be set and cannot be changed. + virtual void Prepare(StunMessage* request) {} + + // Called when the message receives a response or times out. + virtual void OnResponse(StunMessage* response) {} + virtual void OnErrorResponse(StunMessage* response) {} + virtual void OnTimeout() {} + virtual int GetNextDelay(); + +private: + void set_manager(StunRequestManager* manager); + + // Handles messages for sending and timeout. + void OnMessage(talk_base::Message* pmsg); + + StunRequestManager* manager_; + StunMessage* msg_; + uint32 tstamp_; + + friend class StunRequestManager; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_STUNREQUEST_H_ diff --git a/talk/p2p/base/stunrequest_unittest.cc b/talk/p2p/base/stunrequest_unittest.cc new file mode 100644 index 000000000..b641585a7 --- /dev/null +++ b/talk/p2p/base/stunrequest_unittest.cc @@ -0,0 +1,222 @@ +/* + * libjingle + * Copyright 2004 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/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/timeutils.h" +#include "talk/p2p/base/stunrequest.h" + +using namespace cricket; + +class StunRequestTest : public testing::Test, + public sigslot::has_slots<> { + public: + static void SetUpTestCase() { + talk_base::InitRandom(NULL, 0); + } + StunRequestTest() + : manager_(talk_base::Thread::Current()), + request_count_(0), response_(NULL), + success_(false), failure_(false), timeout_(false) { + manager_.SignalSendPacket.connect(this, &StunRequestTest::OnSendPacket); + } + + void OnSendPacket(const void* data, size_t size, StunRequest* req) { + request_count_++; + } + + void OnResponse(StunMessage* res) { + response_ = res; + success_ = true; + } + void OnErrorResponse(StunMessage* res) { + response_ = res; + failure_ = true; + } + void OnTimeout() { + timeout_ = true; + } + + protected: + static StunMessage* CreateStunMessage(StunMessageType type, + StunMessage* req) { + StunMessage* msg = new StunMessage(); + msg->SetType(type); + if (req) { + msg->SetTransactionID(req->transaction_id()); + } + return msg; + } + static int TotalDelay(int sends) { + int total = 0; + for (int i = 0; i < sends; i++) { + if (i < 4) + total += 100 << i; + else + total += 1600; + } + return total; + } + + StunRequestManager manager_; + int request_count_; + StunMessage* response_; + bool success_; + bool failure_; + bool timeout_; +}; + +// Forwards results to the test class. +class StunRequestThunker : public StunRequest { + public: + StunRequestThunker(StunMessage* msg, StunRequestTest* test) + : StunRequest(msg), test_(test) {} + explicit StunRequestThunker(StunRequestTest* test) : test_(test) {} + private: + virtual void OnResponse(StunMessage* res) { + test_->OnResponse(res); + } + virtual void OnErrorResponse(StunMessage* res) { + test_->OnErrorResponse(res); + } + virtual void OnTimeout() { + test_->OnTimeout(); + } + + virtual void Prepare(StunMessage* request) { + request->SetType(STUN_BINDING_REQUEST); + } + + StunRequestTest* test_; +}; + +// Test handling of a normal binding response. +TEST_F(StunRequestTest, TestSuccess) { + StunMessage* req = CreateStunMessage(STUN_BINDING_REQUEST, NULL); + + manager_.Send(new StunRequestThunker(req, this)); + StunMessage* res = CreateStunMessage(STUN_BINDING_RESPONSE, req); + EXPECT_TRUE(manager_.CheckResponse(res)); + + EXPECT_TRUE(response_ == res); + EXPECT_TRUE(success_); + EXPECT_FALSE(failure_); + EXPECT_FALSE(timeout_); + delete res; +} + +// Test handling of an error binding response. +TEST_F(StunRequestTest, TestError) { + StunMessage* req = CreateStunMessage(STUN_BINDING_REQUEST, NULL); + + manager_.Send(new StunRequestThunker(req, this)); + StunMessage* res = CreateStunMessage(STUN_BINDING_ERROR_RESPONSE, req); + EXPECT_TRUE(manager_.CheckResponse(res)); + + EXPECT_TRUE(response_ == res); + EXPECT_FALSE(success_); + EXPECT_TRUE(failure_); + EXPECT_FALSE(timeout_); + delete res; +} + +// Test handling of a binding response with the wrong transaction id. +TEST_F(StunRequestTest, TestUnexpected) { + StunMessage* req = CreateStunMessage(STUN_BINDING_REQUEST, NULL); + + manager_.Send(new StunRequestThunker(req, this)); + StunMessage* res = CreateStunMessage(STUN_BINDING_RESPONSE, NULL); + EXPECT_FALSE(manager_.CheckResponse(res)); + + EXPECT_TRUE(response_ == NULL); + EXPECT_FALSE(success_); + EXPECT_FALSE(failure_); + EXPECT_FALSE(timeout_); + delete res; +} + +// Test that requests are sent at the right times, and that the 9th request +// (sent at 7900 ms) can be properly replied to. +TEST_F(StunRequestTest, TestBackoff) { + StunMessage* req = CreateStunMessage(STUN_BINDING_REQUEST, NULL); + + uint32 start = talk_base::Time(); + manager_.Send(new StunRequestThunker(req, this)); + StunMessage* res = CreateStunMessage(STUN_BINDING_RESPONSE, req); + for (int i = 0; i < 9; ++i) { + while (request_count_ == i) + talk_base::Thread::Current()->ProcessMessages(1); + int32 elapsed = talk_base::TimeSince(start); + LOG(LS_INFO) << "STUN request #" << (i + 1) + << " sent at " << elapsed << " ms"; + EXPECT_GE(TotalDelay(i + 1), elapsed); + } + EXPECT_TRUE(manager_.CheckResponse(res)); + + EXPECT_TRUE(response_ == res); + EXPECT_TRUE(success_); + EXPECT_FALSE(failure_); + EXPECT_FALSE(timeout_); + delete res; +} + +// Test that we timeout properly if no response is received in 9500 ms. +TEST_F(StunRequestTest, TestTimeout) { + StunMessage* req = CreateStunMessage(STUN_BINDING_REQUEST, NULL); + StunMessage* res = CreateStunMessage(STUN_BINDING_RESPONSE, req); + + manager_.Send(new StunRequestThunker(req, this)); + talk_base::Thread::Current()->ProcessMessages(10000); // > STUN timeout + EXPECT_FALSE(manager_.CheckResponse(res)); + + EXPECT_TRUE(response_ == NULL); + EXPECT_FALSE(success_); + EXPECT_FALSE(failure_); + EXPECT_TRUE(timeout_); + delete res; +} + +// Regression test for specific crash where we receive a response with the +// same id as a request that doesn't have an underlying StunMessage yet. +TEST_F(StunRequestTest, TestNoEmptyRequest) { + StunRequestThunker* request = new StunRequestThunker(this); + + manager_.SendDelayed(request, 100); + + StunMessage dummy_req; + dummy_req.SetTransactionID(request->id()); + StunMessage* res = CreateStunMessage(STUN_BINDING_RESPONSE, &dummy_req); + + EXPECT_TRUE(manager_.CheckResponse(res)); + + EXPECT_TRUE(response_ == res); + EXPECT_TRUE(success_); + EXPECT_FALSE(failure_); + EXPECT_FALSE(timeout_); + delete res; +} diff --git a/talk/p2p/base/stunserver.cc b/talk/p2p/base/stunserver.cc new file mode 100644 index 000000000..05292e8e1 --- /dev/null +++ b/talk/p2p/base/stunserver.cc @@ -0,0 +1,109 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/stunserver.h" + +#include "talk/base/bytebuffer.h" +#include "talk/base/logging.h" + +namespace cricket { + +StunServer::StunServer(talk_base::AsyncUDPSocket* socket) : socket_(socket) { + socket_->SignalReadPacket.connect(this, &StunServer::OnPacket); +} + +StunServer::~StunServer() { + socket_->SignalReadPacket.disconnect(this); +} + +void StunServer::OnPacket( + talk_base::AsyncPacketSocket* socket, const char* buf, size_t size, + const talk_base::SocketAddress& remote_addr) { + // Parse the STUN message; eat any messages that fail to parse. + talk_base::ByteBuffer bbuf(buf, size); + StunMessage msg; + if (!msg.Read(&bbuf)) { + return; + } + + // TODO: If unknown non-optional (<= 0x7fff) attributes are found, send a + // 420 "Unknown Attribute" response. + + // Send the message to the appropriate handler function. + switch (msg.type()) { + case STUN_BINDING_REQUEST: + OnBindingRequest(&msg, remote_addr); + break; + + default: + SendErrorResponse(msg, remote_addr, 600, "Operation Not Supported"); + } +} + +void StunServer::OnBindingRequest( + StunMessage* msg, const talk_base::SocketAddress& remote_addr) { + StunMessage response; + response.SetType(STUN_BINDING_RESPONSE); + response.SetTransactionID(msg->transaction_id()); + + // Tell the user the address that we received their request from. + StunAddressAttribute* mapped_addr; + if (!msg->IsLegacy()) { + mapped_addr = StunAttribute::CreateAddress(STUN_ATTR_MAPPED_ADDRESS); + } else { + mapped_addr = StunAttribute::CreateXorAddress(STUN_ATTR_XOR_MAPPED_ADDRESS); + } + mapped_addr->SetAddress(remote_addr); + response.AddAttribute(mapped_addr); + + SendResponse(response, remote_addr); +} + +void StunServer::SendErrorResponse( + const StunMessage& msg, const talk_base::SocketAddress& addr, + int error_code, const char* error_desc) { + StunMessage err_msg; + err_msg.SetType(GetStunErrorResponseType(msg.type())); + err_msg.SetTransactionID(msg.transaction_id()); + + StunErrorCodeAttribute* err_code = StunAttribute::CreateErrorCode(); + err_code->SetCode(error_code); + err_code->SetReason(error_desc); + err_msg.AddAttribute(err_code); + + SendResponse(err_msg, addr); +} + +void StunServer::SendResponse( + const StunMessage& msg, const talk_base::SocketAddress& addr) { + talk_base::ByteBuffer buf; + msg.Write(&buf); + if (socket_->SendTo(buf.Data(), buf.Length(), addr) < 0) + LOG_ERR(LS_ERROR) << "sendto"; +} + +} // namespace cricket diff --git a/talk/p2p/base/stunserver.h b/talk/p2p/base/stunserver.h new file mode 100644 index 000000000..6e51ad184 --- /dev/null +++ b/talk/p2p/base/stunserver.h @@ -0,0 +1,77 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_STUNSERVER_H_ +#define TALK_P2P_BASE_STUNSERVER_H_ + +#include "talk/base/asyncudpsocket.h" +#include "talk/base/scoped_ptr.h" +#include "talk/p2p/base/stun.h" + +namespace cricket { + +const int STUN_SERVER_PORT = 3478; + +class StunServer : public sigslot::has_slots<> { + public: + // Creates a STUN server, which will listen on the given socket. + explicit StunServer(talk_base::AsyncUDPSocket* socket); + // Removes the STUN server from the socket and deletes the socket. + ~StunServer(); + + protected: + // Slot for AsyncSocket.PacketRead: + void OnPacket( + talk_base::AsyncPacketSocket* socket, const char* buf, size_t size, + const talk_base::SocketAddress& remote_addr); + + // Handlers for the different types of STUN/TURN requests: + void OnBindingRequest(StunMessage* msg, + const talk_base::SocketAddress& addr); + void OnAllocateRequest(StunMessage* msg, + const talk_base::SocketAddress& addr); + void OnSharedSecretRequest(StunMessage* msg, + const talk_base::SocketAddress& addr); + void OnSendRequest(StunMessage* msg, + const talk_base::SocketAddress& addr); + + // Sends an error response to the given message back to the user. + void SendErrorResponse( + const StunMessage& msg, const talk_base::SocketAddress& addr, + int error_code, const char* error_desc); + + // Sends the given message to the appropriate destination. + void SendResponse(const StunMessage& msg, + const talk_base::SocketAddress& addr); + + private: + talk_base::scoped_ptr socket_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_STUNSERVER_H_ diff --git a/talk/p2p/base/stunserver_main.cc b/talk/p2p/base/stunserver_main.cc new file mode 100644 index 000000000..e6977288d --- /dev/null +++ b/talk/p2p/base/stunserver_main.cc @@ -0,0 +1,69 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifdef POSIX +#include +#endif // POSIX + +#include + +#include "talk/base/host.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/stunserver.h" + +using namespace cricket; + +int main(int argc, char* argv[]) { + if (argc != 2) { + std::cerr << "usage: stunserver address" << std::endl; + return 1; + } + + talk_base::SocketAddress server_addr; + if (!server_addr.FromString(argv[1])) { + std::cerr << "Unable to parse IP address: " << argv[1]; + return 1; + } + + talk_base::Thread *pthMain = talk_base::Thread::Current(); + + talk_base::AsyncUDPSocket* server_socket = + talk_base::AsyncUDPSocket::Create(pthMain->socketserver(), server_addr); + if (!server_socket) { + std::cerr << "Failed to create a UDP socket" << std::endl; + return 1; + } + + StunServer* server = new StunServer(server_socket); + + std::cout << "Listening at " << server_addr.ToString() << std::endl; + + pthMain->Run(); + + delete server; + return 0; +} diff --git a/talk/p2p/base/stunserver_unittest.cc b/talk/p2p/base/stunserver_unittest.cc new file mode 100644 index 000000000..1d2cd0d16 --- /dev/null +++ b/talk/p2p/base/stunserver_unittest.cc @@ -0,0 +1,120 @@ +/* + * libjingle + * Copyright 2004 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 + +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/virtualsocketserver.h" +#include "talk/base/testclient.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/stunserver.h" + +using namespace cricket; + +static const talk_base::SocketAddress server_addr("99.99.99.1", 3478); +static const talk_base::SocketAddress client_addr("1.2.3.4", 1234); + +class StunServerTest : public testing::Test { + public: + StunServerTest() + : pss_(new talk_base::PhysicalSocketServer), + ss_(new talk_base::VirtualSocketServer(pss_.get())), + worker_(ss_.get()) { + } + virtual void SetUp() { + server_.reset(new StunServer( + talk_base::AsyncUDPSocket::Create(ss_.get(), server_addr))); + client_.reset(new talk_base::TestClient( + talk_base::AsyncUDPSocket::Create(ss_.get(), client_addr))); + + worker_.Start(); + } + void Send(const StunMessage& msg) { + talk_base::ByteBuffer buf; + msg.Write(&buf); + Send(buf.Data(), buf.Length()); + } + void Send(const char* buf, int len) { + client_->SendTo(buf, len, server_addr); + } + StunMessage* Receive() { + StunMessage* msg = NULL; + talk_base::TestClient::Packet* packet = client_->NextPacket(); + if (packet) { + talk_base::ByteBuffer buf(packet->buf, packet->size); + msg = new StunMessage(); + msg->Read(&buf); + delete packet; + } + return msg; + } + private: + talk_base::scoped_ptr pss_; + talk_base::scoped_ptr ss_; + talk_base::Thread worker_; + talk_base::scoped_ptr server_; + talk_base::scoped_ptr client_; +}; + +TEST_F(StunServerTest, TestGood) { + StunMessage req; + std::string transaction_id = "0123456789ab"; + req.SetType(STUN_BINDING_REQUEST); + req.SetTransactionID(transaction_id); + Send(req); + + StunMessage* msg = Receive(); + ASSERT_TRUE(msg != NULL); + EXPECT_EQ(STUN_BINDING_RESPONSE, msg->type()); + EXPECT_EQ(req.transaction_id(), msg->transaction_id()); + + const StunAddressAttribute* mapped_addr = + msg->GetAddress(STUN_ATTR_MAPPED_ADDRESS); + EXPECT_TRUE(mapped_addr != NULL); + EXPECT_EQ(1, mapped_addr->family()); + EXPECT_EQ(client_addr.port(), mapped_addr->port()); + if (mapped_addr->ipaddr() != client_addr.ipaddr()) { + LOG(LS_WARNING) << "Warning: mapped IP (" + << mapped_addr->ipaddr() + << ") != local IP (" << client_addr.ipaddr() + << ")"; + } + + delete msg; +} + +TEST_F(StunServerTest, TestBad) { + const char* bad = "this is a completely nonsensical message whose only " + "purpose is to make the parser go 'ack'. it doesn't " + "look anything like a normal stun message"; + Send(bad, std::strlen(bad)); + + StunMessage* msg = Receive(); + ASSERT_TRUE(msg == NULL); +} diff --git a/talk/p2p/base/tcpport.cc b/talk/p2p/base/tcpport.cc new file mode 100644 index 000000000..356dd673d --- /dev/null +++ b/talk/p2p/base/tcpport.cc @@ -0,0 +1,307 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/tcpport.h" + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/p2p/base/common.h" + +namespace cricket { + +TCPPort::TCPPort(talk_base::Thread* thread, + talk_base::PacketSocketFactory* factory, + talk_base::Network* network, const talk_base::IPAddress& ip, + int min_port, int max_port, const std::string& username, + const std::string& password, bool allow_listen) + : Port(thread, LOCAL_PORT_TYPE, factory, network, ip, min_port, max_port, + username, password), + incoming_only_(false), + allow_listen_(allow_listen), + socket_(NULL), + error_(0) { + // TODO(mallinath) - Set preference value as per RFC 6544. + // http://b/issue?id=7141794 +} + +bool TCPPort::Init() { + if (allow_listen_) { + // Treat failure to create or bind a TCP socket as fatal. This + // should never happen. + socket_ = socket_factory()->CreateServerTcpSocket( + talk_base::SocketAddress(ip(), 0), min_port(), max_port(), + false /* ssl */); + if (!socket_) { + LOG_J(LS_ERROR, this) << "TCP socket creation failed."; + return false; + } + socket_->SignalNewConnection.connect(this, &TCPPort::OnNewConnection); + socket_->SignalAddressReady.connect(this, &TCPPort::OnAddressReady); + } + return true; +} + +TCPPort::~TCPPort() { + delete socket_; +} + +Connection* TCPPort::CreateConnection(const Candidate& address, + CandidateOrigin origin) { + // We only support TCP protocols + if ((address.protocol() != TCP_PROTOCOL_NAME) && + (address.protocol() != SSLTCP_PROTOCOL_NAME)) { + return NULL; + } + + // We can't accept TCP connections incoming on other ports + if (origin == ORIGIN_OTHER_PORT) + return NULL; + + // Check if we are allowed to make outgoing TCP connections + if (incoming_only_ && (origin == ORIGIN_MESSAGE)) + return NULL; + + // We don't know how to act as an ssl server yet + if ((address.protocol() == SSLTCP_PROTOCOL_NAME) && + (origin == ORIGIN_THIS_PORT)) { + return NULL; + } + + if (!IsCompatibleAddress(address.address())) { + return NULL; + } + + TCPConnection* conn = NULL; + if (talk_base::AsyncPacketSocket* socket = + GetIncoming(address.address(), true)) { + socket->SignalReadPacket.disconnect(this); + conn = new TCPConnection(this, address, socket); + } else { + conn = new TCPConnection(this, address); + } + AddConnection(conn); + return conn; +} + +void TCPPort::PrepareAddress() { + if (socket_) { + // If socket isn't bound yet the address will be added in + // OnAddressReady(). Socket may be in the CLOSED state if Listen() + // failed, we still want ot add the socket address. + LOG(LS_VERBOSE) << "Preparing TCP address, current state: " + << socket_->GetState(); + if (socket_->GetState() == talk_base::AsyncPacketSocket::STATE_BOUND || + socket_->GetState() == talk_base::AsyncPacketSocket::STATE_CLOSED) + AddAddress(socket_->GetLocalAddress(), socket_->GetLocalAddress(), + TCP_PROTOCOL_NAME, LOCAL_PORT_TYPE, + ICE_TYPE_PREFERENCE_HOST_TCP, true); + } else { + LOG_J(LS_INFO, this) << "Not listening due to firewall restrictions."; + // Sending error signal as we can't allocate tcp candidate. + SignalPortError(this); + } +} + +int TCPPort::SendTo(const void* data, size_t size, + const talk_base::SocketAddress& addr, bool payload) { + talk_base::AsyncPacketSocket * socket = NULL; + if (TCPConnection * conn = static_cast(GetConnection(addr))) { + socket = conn->socket(); + } else { + socket = GetIncoming(addr); + } + if (!socket) { + LOG_J(LS_ERROR, this) << "Attempted to send to an unknown destination, " + << addr.ToSensitiveString(); + return -1; // TODO: Set error_ + } + + int sent = socket->Send(data, size); + if (sent < 0) { + error_ = socket->GetError(); + LOG_J(LS_ERROR, this) << "TCP send of " << size + << " bytes failed with error " << error_; + } + return sent; +} + +int TCPPort::GetOption(talk_base::Socket::Option opt, int* value) { + if (socket_) { + return socket_->GetOption(opt, value); + } else { + return SOCKET_ERROR; + } +} + +int TCPPort::SetOption(talk_base::Socket::Option opt, int value) { + if (socket_) { + return socket_->SetOption(opt, value); + } else { + return SOCKET_ERROR; + } +} + +int TCPPort::GetError() { + return error_; +} + +void TCPPort::OnNewConnection(talk_base::AsyncPacketSocket* socket, + talk_base::AsyncPacketSocket* new_socket) { + ASSERT(socket == socket_); + + Incoming incoming; + incoming.addr = new_socket->GetRemoteAddress(); + incoming.socket = new_socket; + incoming.socket->SignalReadPacket.connect(this, &TCPPort::OnReadPacket); + incoming.socket->SignalReadyToSend.connect(this, &TCPPort::OnReadyToSend); + + LOG_J(LS_VERBOSE, this) << "Accepted connection from " + << incoming.addr.ToSensitiveString(); + incoming_.push_back(incoming); +} + +talk_base::AsyncPacketSocket* TCPPort::GetIncoming( + const talk_base::SocketAddress& addr, bool remove) { + talk_base::AsyncPacketSocket* socket = NULL; + for (std::list::iterator it = incoming_.begin(); + it != incoming_.end(); ++it) { + if (it->addr == addr) { + socket = it->socket; + if (remove) + incoming_.erase(it); + break; + } + } + return socket; +} + +void TCPPort::OnReadPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& remote_addr) { + Port::OnReadPacket(data, size, remote_addr, PROTO_TCP); +} + +void TCPPort::OnReadyToSend(talk_base::AsyncPacketSocket* socket) { + Port::OnReadyToSend(); +} + +void TCPPort::OnAddressReady(talk_base::AsyncPacketSocket* socket, + const talk_base::SocketAddress& address) { + AddAddress(address, address, "tcp", + LOCAL_PORT_TYPE, ICE_TYPE_PREFERENCE_HOST_TCP, + true); +} + +TCPConnection::TCPConnection(TCPPort* port, const Candidate& candidate, + talk_base::AsyncPacketSocket* socket) + : Connection(port, 0, candidate), socket_(socket), error_(0) { + bool outgoing = (socket_ == NULL); + if (outgoing) { + // TODO: Handle failures here (unlikely since TCP). + int opts = (candidate.protocol() == SSLTCP_PROTOCOL_NAME) ? + talk_base::PacketSocketFactory::OPT_SSLTCP : 0; + socket_ = port->socket_factory()->CreateClientTcpSocket( + talk_base::SocketAddress(port_->Network()->ip(), 0), + candidate.address(), port->proxy(), port->user_agent(), opts); + if (socket_) { + LOG_J(LS_VERBOSE, this) << "Connecting from " + << socket_->GetLocalAddress().ToSensitiveString() + << " to " + << candidate.address().ToSensitiveString(); + set_connected(false); + socket_->SignalConnect.connect(this, &TCPConnection::OnConnect); + } else { + LOG_J(LS_WARNING, this) << "Failed to create connection to " + << candidate.address().ToSensitiveString(); + } + } else { + // Incoming connections should match the network address. + ASSERT(socket_->GetLocalAddress().ipaddr() == port->ip()); + } + + if (socket_) { + socket_->SignalReadPacket.connect(this, &TCPConnection::OnReadPacket); + socket_->SignalReadyToSend.connect(this, &TCPConnection::OnReadyToSend); + socket_->SignalClose.connect(this, &TCPConnection::OnClose); + } +} + +TCPConnection::~TCPConnection() { + delete socket_; +} + +int TCPConnection::Send(const void* data, size_t size) { + if (!socket_) { + error_ = ENOTCONN; + return SOCKET_ERROR; + } + + if (write_state() != STATE_WRITABLE) { + // TODO: Should STATE_WRITE_TIMEOUT return a non-blocking error? + error_ = EWOULDBLOCK; + return SOCKET_ERROR; + } + int sent = socket_->Send(data, size); + if (sent < 0) { + error_ = socket_->GetError(); + } else { + send_rate_tracker_.Update(sent); + } + return sent; +} + +int TCPConnection::GetError() { + return error_; +} + +void TCPConnection::OnConnect(talk_base::AsyncPacketSocket* socket) { + ASSERT(socket == socket_); + LOG_J(LS_VERBOSE, this) << "Connection established to " + << socket->GetRemoteAddress().ToSensitiveString(); + set_connected(true); +} + +void TCPConnection::OnClose(talk_base::AsyncPacketSocket* socket, int error) { + ASSERT(socket == socket_); + LOG_J(LS_VERBOSE, this) << "Connection closed with error " << error; + set_connected(false); + set_write_state(STATE_WRITE_TIMEOUT); +} + +void TCPConnection::OnReadPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& remote_addr) { + ASSERT(socket == socket_); + Connection::OnReadPacket(data, size); +} + +void TCPConnection::OnReadyToSend(talk_base::AsyncPacketSocket* socket) { + ASSERT(socket == socket_); + Connection::OnReadyToSend(); +} + +} // namespace cricket diff --git a/talk/p2p/base/tcpport.h b/talk/p2p/base/tcpport.h new file mode 100644 index 000000000..813617666 --- /dev/null +++ b/talk/p2p/base/tcpport.h @@ -0,0 +1,148 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_TCPPORT_H_ +#define TALK_P2P_BASE_TCPPORT_H_ + +#include +#include +#include "talk/base/asyncpacketsocket.h" +#include "talk/p2p/base/port.h" + +namespace cricket { + +class TCPConnection; + +// Communicates using a local TCP port. +// +// This class is designed to allow subclasses to take advantage of the +// connection management provided by this class. A subclass should take of all +// packet sending and preparation, but when a packet is received, it should +// call this TCPPort::OnReadPacket (3 arg) to dispatch to a connection. +class TCPPort : public Port { + public: + static TCPPort* Create(talk_base::Thread* thread, + talk_base::PacketSocketFactory* factory, + talk_base::Network* network, + const talk_base::IPAddress& ip, + int min_port, int max_port, + const std::string& username, + const std::string& password, + bool allow_listen) { + TCPPort* port = new TCPPort(thread, factory, network, + ip, min_port, max_port, + username, password, allow_listen); + if (!port->Init()) { + delete port; + port = NULL; + } + return port; + } + virtual ~TCPPort(); + + virtual Connection* CreateConnection(const Candidate& address, + CandidateOrigin origin); + + virtual void PrepareAddress(); + + virtual int GetOption(talk_base::Socket::Option opt, int* value); + virtual int SetOption(talk_base::Socket::Option opt, int value); + virtual int GetError(); + + protected: + TCPPort(talk_base::Thread* thread, talk_base::PacketSocketFactory* factory, + talk_base::Network* network, const talk_base::IPAddress& ip, + int min_port, int max_port, const std::string& username, + const std::string& password, bool allow_listen); + bool Init(); + + // Handles sending using the local TCP socket. + virtual int SendTo(const void* data, size_t size, + const talk_base::SocketAddress& addr, bool payload); + + // Accepts incoming TCP connection. + void OnNewConnection(talk_base::AsyncPacketSocket* socket, + talk_base::AsyncPacketSocket* new_socket); + + private: + struct Incoming { + talk_base::SocketAddress addr; + talk_base::AsyncPacketSocket* socket; + }; + + talk_base::AsyncPacketSocket* GetIncoming( + const talk_base::SocketAddress& addr, bool remove = false); + + // Receives packet signal from the local TCP Socket. + void OnReadPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& remote_addr); + + void OnReadyToSend(talk_base::AsyncPacketSocket* socket); + + void OnAddressReady(talk_base::AsyncPacketSocket* socket, + const talk_base::SocketAddress& address); + + // TODO: Is this still needed? + bool incoming_only_; + bool allow_listen_; + talk_base::AsyncPacketSocket* socket_; + int error_; + std::list incoming_; + + friend class TCPConnection; +}; + +class TCPConnection : public Connection { + public: + // Connection is outgoing unless socket is specified + TCPConnection(TCPPort* port, const Candidate& candidate, + talk_base::AsyncPacketSocket* socket = 0); + virtual ~TCPConnection(); + + virtual int Send(const void* data, size_t size); + virtual int GetError(); + + talk_base::AsyncPacketSocket* socket() { return socket_; } + + private: + void OnConnect(talk_base::AsyncPacketSocket* socket); + void OnClose(talk_base::AsyncPacketSocket* socket, int error); + void OnReadPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& remote_addr); + void OnReadyToSend(talk_base::AsyncPacketSocket* socket); + + talk_base::AsyncPacketSocket* socket_; + int error_; + + friend class TCPPort; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_TCPPORT_H_ diff --git a/talk/p2p/base/testrelayserver.h b/talk/p2p/base/testrelayserver.h new file mode 100644 index 000000000..29e9fe42e --- /dev/null +++ b/talk/p2p/base/testrelayserver.h @@ -0,0 +1,118 @@ +/* + * libjingle + * Copyright 2008 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. + */ + +#ifndef TALK_P2P_BASE_TESTRELAYSERVER_H_ +#define TALK_P2P_BASE_TESTRELAYSERVER_H_ + +#include "talk/base/asynctcpsocket.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/socketadapters.h" +#include "talk/base/sigslot.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/relayserver.h" + +namespace cricket { + +// A test relay server. Useful for unit tests. +class TestRelayServer : public sigslot::has_slots<> { + public: + TestRelayServer(talk_base::Thread* thread, + const talk_base::SocketAddress& udp_int_addr, + const talk_base::SocketAddress& udp_ext_addr, + const talk_base::SocketAddress& tcp_int_addr, + const talk_base::SocketAddress& tcp_ext_addr, + const talk_base::SocketAddress& ssl_int_addr, + const talk_base::SocketAddress& ssl_ext_addr) + : server_(thread) { + server_.AddInternalSocket(talk_base::AsyncUDPSocket::Create( + thread->socketserver(), udp_int_addr)); + server_.AddExternalSocket(talk_base::AsyncUDPSocket::Create( + thread->socketserver(), udp_ext_addr)); + + tcp_int_socket_.reset(CreateListenSocket(thread, tcp_int_addr)); + tcp_ext_socket_.reset(CreateListenSocket(thread, tcp_ext_addr)); + ssl_int_socket_.reset(CreateListenSocket(thread, ssl_int_addr)); + ssl_ext_socket_.reset(CreateListenSocket(thread, ssl_ext_addr)); + } + int GetConnectionCount() const { + return server_.GetConnectionCount(); + } + talk_base::SocketAddressPair GetConnection(int connection) const { + return server_.GetConnection(connection); + } + bool HasConnection(const talk_base::SocketAddress& address) const { + return server_.HasConnection(address); + } + + private: + talk_base::AsyncSocket* CreateListenSocket(talk_base::Thread* thread, + const talk_base::SocketAddress& addr) { + talk_base::AsyncSocket* socket = + thread->socketserver()->CreateAsyncSocket(addr.family(), SOCK_STREAM); + socket->Bind(addr); + socket->Listen(5); + socket->SignalReadEvent.connect(this, &TestRelayServer::OnAccept); + return socket; + } + void OnAccept(talk_base::AsyncSocket* socket) { + bool external = (socket == tcp_ext_socket_.get() || + socket == ssl_ext_socket_.get()); + bool ssl = (socket == ssl_int_socket_.get() || + socket == ssl_ext_socket_.get()); + talk_base::AsyncSocket* raw_socket = socket->Accept(NULL); + if (raw_socket) { + talk_base::AsyncTCPSocket* packet_socket = new talk_base::AsyncTCPSocket( + (!ssl) ? raw_socket : + new talk_base::AsyncSSLServerSocket(raw_socket), false); + if (!external) { + packet_socket->SignalClose.connect(this, + &TestRelayServer::OnInternalClose); + server_.AddInternalSocket(packet_socket); + } else { + packet_socket->SignalClose.connect(this, + &TestRelayServer::OnExternalClose); + server_.AddExternalSocket(packet_socket); + } + } + } + void OnInternalClose(talk_base::AsyncPacketSocket* socket, int error) { + server_.RemoveInternalSocket(socket); + } + void OnExternalClose(talk_base::AsyncPacketSocket* socket, int error) { + server_.RemoveExternalSocket(socket); + } + private: + cricket::RelayServer server_; + talk_base::scoped_ptr tcp_int_socket_; + talk_base::scoped_ptr tcp_ext_socket_; + talk_base::scoped_ptr ssl_int_socket_; + talk_base::scoped_ptr ssl_ext_socket_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_TESTRELAYSERVER_H_ diff --git a/talk/p2p/base/teststunserver.h b/talk/p2p/base/teststunserver.h new file mode 100644 index 000000000..67bac21c0 --- /dev/null +++ b/talk/p2p/base/teststunserver.h @@ -0,0 +1,55 @@ +/* + * libjingle + * Copyright 2008 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. + */ + +#ifndef TALK_P2P_BASE_TESTSTUNSERVER_H_ +#define TALK_P2P_BASE_TESTSTUNSERVER_H_ + +#include "talk/base/socketaddress.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/stunserver.h" + +namespace cricket { + +// A test STUN server. Useful for unit tests. +class TestStunServer { + public: + TestStunServer(talk_base::Thread* thread, + const talk_base::SocketAddress& addr) + : socket_(thread->socketserver()->CreateAsyncSocket(addr.family(), + SOCK_DGRAM)), + udp_socket_(talk_base::AsyncUDPSocket::Create(socket_, addr)), + server_(udp_socket_) { + } + private: + talk_base::AsyncSocket* socket_; + talk_base::AsyncUDPSocket* udp_socket_; + cricket::StunServer server_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_TESTSTUNSERVER_H_ diff --git a/talk/p2p/base/testturnserver.h b/talk/p2p/base/testturnserver.h new file mode 100644 index 000000000..32251c82c --- /dev/null +++ b/talk/p2p/base/testturnserver.h @@ -0,0 +1,78 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_P2P_BASE_TESTTURNSERVER_H_ +#define TALK_P2P_BASE_TESTTURNSERVER_H_ + +#include + +#include "talk/base/asyncudpsocket.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/base/stun.h" +#include "talk/p2p/base/turnserver.h" + +namespace cricket { + +static const char kTestRealm[] = "example.org"; +static const char kTestSoftware[] = "TestTurnServer"; + +class TestTurnServer : public TurnAuthInterface { + public: + TestTurnServer(talk_base::Thread* thread, + const talk_base::SocketAddress& udp_int_addr, + const talk_base::SocketAddress& udp_ext_addr) + : server_(thread) { + server_.AddInternalSocket(talk_base::AsyncUDPSocket::Create( + thread->socketserver(), udp_int_addr), PROTO_UDP); + server_.SetExternalSocketFactory(new talk_base::BasicPacketSocketFactory(), + udp_ext_addr); + server_.set_realm(kTestRealm); + server_.set_software(kTestSoftware); + server_.set_auth_hook(this); + } + + void set_enable_otu_nonce(bool enable) { + server_.set_enable_otu_nonce(enable); + } + + TurnServer* server() { return &server_; } + + private: + // For this test server, succeed if the password is the same as the username. + // Obviously, do not use this in a production environment. + virtual bool GetKey(const std::string& username, const std::string& realm, + std::string* key) { + return ComputeStunCredentialHash(username, realm, username, key); + } + + TurnServer server_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_TESTTURNSERVER_H_ diff --git a/talk/p2p/base/transport.cc b/talk/p2p/base/transport.cc new file mode 100644 index 000000000..d2b7965fc --- /dev/null +++ b/talk/p2p/base/transport.cc @@ -0,0 +1,838 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/transport.h" + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/p2p/base/candidate.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/sessionmanager.h" +#include "talk/p2p/base/parsing.h" +#include "talk/p2p/base/transportchannelimpl.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" + +namespace cricket { + +enum { + MSG_CREATECHANNEL = 1, + MSG_DESTROYCHANNEL = 2, + MSG_DESTROYALLCHANNELS = 3, + MSG_CONNECTCHANNELS = 4, + MSG_RESETCHANNELS = 5, + MSG_ONSIGNALINGREADY = 6, + MSG_ONREMOTECANDIDATE = 7, + MSG_READSTATE = 8, + MSG_WRITESTATE = 9, + MSG_REQUESTSIGNALING = 10, + MSG_CANDIDATEREADY = 11, + MSG_ROUTECHANGE = 12, + MSG_CONNECTING = 13, + MSG_CANDIDATEALLOCATIONCOMPLETE = 14, + MSG_ROLECONFLICT = 15, + MSG_SETROLE = 16, + MSG_SETLOCALDESCRIPTION = 17, + MSG_SETREMOTEDESCRIPTION = 18, + MSG_GETSTATS = 19 +}; + +struct ChannelParams : public talk_base::MessageData { + ChannelParams() : channel(NULL), candidate(NULL) {} + explicit ChannelParams(int component) + : component(component), channel(NULL), candidate(NULL) {} + explicit ChannelParams(Candidate* candidate) + : channel(NULL), candidate(candidate) { + } + + ~ChannelParams() { + delete candidate; + } + + std::string name; + int component; + TransportChannelImpl* channel; + Candidate* candidate; +}; + +struct TransportDescriptionParams : public talk_base::MessageData { + TransportDescriptionParams(const TransportDescription& desc, + ContentAction action) + : desc(desc), action(action), result(false) {} + const TransportDescription& desc; + ContentAction action; + bool result; +}; + +struct TransportRoleParam : public talk_base::MessageData { + explicit TransportRoleParam(TransportRole role) : role(role) {} + + TransportRole role; +}; + +struct StatsParam : public talk_base::MessageData { + explicit StatsParam(TransportStats* stats) + : stats(stats), result(false) {} + + TransportStats* stats; + bool result; +}; + +Transport::Transport(talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread, + const std::string& content_name, + const std::string& type, + PortAllocator* allocator) + : signaling_thread_(signaling_thread), + worker_thread_(worker_thread), + content_name_(content_name), + type_(type), + allocator_(allocator), + destroyed_(false), + readable_(TRANSPORT_STATE_NONE), + writable_(TRANSPORT_STATE_NONE), + was_writable_(false), + connect_requested_(false), + role_(ROLE_UNKNOWN), + tiebreaker_(0), + protocol_(ICEPROTO_HYBRID), + remote_ice_mode_(ICEMODE_FULL) { +} + +Transport::~Transport() { + ASSERT(signaling_thread_->IsCurrent()); + ASSERT(destroyed_); +} + +void Transport::SetRole(TransportRole role) { + TransportRoleParam param(role); + worker_thread()->Send(this, MSG_SETROLE, ¶m); +} + +bool Transport::SetLocalTransportDescription( + const TransportDescription& description, ContentAction action) { + TransportDescriptionParams params(description, action); + worker_thread()->Send(this, MSG_SETLOCALDESCRIPTION, ¶ms); + return params.result; +} + +bool Transport::SetRemoteTransportDescription( + const TransportDescription& description, ContentAction action) { + TransportDescriptionParams params(description, action); + worker_thread()->Send(this, MSG_SETREMOTEDESCRIPTION, ¶ms); + return params.result; +} + +TransportChannelImpl* Transport::CreateChannel(int component) { + ChannelParams params(component); + worker_thread()->Send(this, MSG_CREATECHANNEL, ¶ms); + return params.channel; +} + +TransportChannelImpl* Transport::CreateChannel_w(int component) { + ASSERT(worker_thread()->IsCurrent()); + TransportChannelImpl *impl; + talk_base::CritScope cs(&crit_); + + // Create the entry if it does not exist. + bool impl_exists = false; + if (channels_.find(component) == channels_.end()) { + impl = CreateTransportChannel(component); + channels_[component] = ChannelMapEntry(impl); + } else { + impl = channels_[component].get(); + impl_exists = true; + } + + // Increase the ref count. + channels_[component].AddRef(); + destroyed_ = false; + + if (impl_exists) { + // If this is an existing channel, we should just return it without + // connecting to all the signal again. + return impl; + } + + // Push down our transport state to the new channel. + impl->SetRole(role_); + impl->SetTiebreaker(tiebreaker_); + if (local_description_) { + ApplyLocalTransportDescription_w(impl); + if (remote_description_) { + ApplyRemoteTransportDescription_w(impl); + ApplyNegotiatedTransportDescription_w(impl); + } + } + + impl->SignalReadableState.connect(this, &Transport::OnChannelReadableState); + impl->SignalWritableState.connect(this, &Transport::OnChannelWritableState); + impl->SignalRequestSignaling.connect( + this, &Transport::OnChannelRequestSignaling); + impl->SignalCandidateReady.connect(this, &Transport::OnChannelCandidateReady); + impl->SignalRouteChange.connect(this, &Transport::OnChannelRouteChange); + impl->SignalCandidatesAllocationDone.connect( + this, &Transport::OnChannelCandidatesAllocationDone); + impl->SignalRoleConflict.connect(this, &Transport::OnRoleConflict); + + if (connect_requested_) { + impl->Connect(); + if (channels_.size() == 1) { + // If this is the first channel, then indicate that we have started + // connecting. + signaling_thread()->Post(this, MSG_CONNECTING, NULL); + } + } + return impl; +} + +TransportChannelImpl* Transport::GetChannel(int component) { + talk_base::CritScope cs(&crit_); + ChannelMap::iterator iter = channels_.find(component); + return (iter != channels_.end()) ? iter->second.get() : NULL; +} + +bool Transport::HasChannels() { + talk_base::CritScope cs(&crit_); + return !channels_.empty(); +} + +void Transport::DestroyChannel(int component) { + ChannelParams params(component); + worker_thread()->Send(this, MSG_DESTROYCHANNEL, ¶ms); +} + +void Transport::DestroyChannel_w(int component) { + ASSERT(worker_thread()->IsCurrent()); + + TransportChannelImpl* impl = NULL; + { + talk_base::CritScope cs(&crit_); + ChannelMap::iterator iter = channels_.find(component); + if (iter == channels_.end()) + return; + + iter->second.DecRef(); + if (!iter->second.ref()) { + impl = iter->second.get(); + channels_.erase(iter); + } + } + + if (connect_requested_ && channels_.empty()) { + // We're no longer attempting to connect. + signaling_thread()->Post(this, MSG_CONNECTING, NULL); + } + + if (impl) { + // Check in case the deleted channel was the only non-writable channel. + OnChannelWritableState(impl); + DestroyTransportChannel(impl); + } +} + +void Transport::ConnectChannels() { + ASSERT(signaling_thread()->IsCurrent()); + worker_thread()->Send(this, MSG_CONNECTCHANNELS, NULL); +} + +void Transport::ConnectChannels_w() { + ASSERT(worker_thread()->IsCurrent()); + if (connect_requested_ || channels_.empty()) + return; + connect_requested_ = true; + signaling_thread()->Post( + this, MSG_CANDIDATEREADY, NULL); + + if (!local_description_) { + // TOOD(mallinath) : TransportDescription(TD) shouldn't be generated here. + // As Transport must know TD is offer or answer and cricket::Transport + // doesn't have the capability to decide it. This should be set by the + // Session. + // Session must generate local TD before remote candidates pushed when + // initiate request initiated by the remote. + LOG(LS_INFO) << "Transport::ConnectChannels_w: No local description has " + << "been set. Will generate one."; + TransportDescription desc(NS_GINGLE_P2P, std::vector(), + talk_base::CreateRandomString(ICE_UFRAG_LENGTH), + talk_base::CreateRandomString(ICE_PWD_LENGTH), + ICEMODE_FULL, NULL, Candidates()); + SetLocalTransportDescription_w(desc, CA_OFFER); + } + + CallChannels_w(&TransportChannelImpl::Connect); + if (!channels_.empty()) { + signaling_thread()->Post(this, MSG_CONNECTING, NULL); + } +} + +void Transport::OnConnecting_s() { + ASSERT(signaling_thread()->IsCurrent()); + SignalConnecting(this); +} + +void Transport::DestroyAllChannels() { + ASSERT(signaling_thread()->IsCurrent()); + worker_thread()->Send(this, MSG_DESTROYALLCHANNELS, NULL); + worker_thread()->Clear(this); + signaling_thread()->Clear(this); + destroyed_ = true; +} + +void Transport::DestroyAllChannels_w() { + ASSERT(worker_thread()->IsCurrent()); + std::vector impls; + { + talk_base::CritScope cs(&crit_); + for (ChannelMap::iterator iter = channels_.begin(); + iter != channels_.end(); + ++iter) { + iter->second.DecRef(); + if (!iter->second.ref()) + impls.push_back(iter->second.get()); + } + } + channels_.clear(); + + + for (size_t i = 0; i < impls.size(); ++i) + DestroyTransportChannel(impls[i]); +} + +void Transport::ResetChannels() { + ASSERT(signaling_thread()->IsCurrent()); + worker_thread()->Send(this, MSG_RESETCHANNELS, NULL); +} + +void Transport::ResetChannels_w() { + ASSERT(worker_thread()->IsCurrent()); + + // We are no longer attempting to connect + connect_requested_ = false; + + // Clear out the old messages, they aren't relevant + talk_base::CritScope cs(&crit_); + ready_candidates_.clear(); + + // Reset all of the channels + CallChannels_w(&TransportChannelImpl::Reset); +} + +void Transport::OnSignalingReady() { + ASSERT(signaling_thread()->IsCurrent()); + if (destroyed_) return; + + worker_thread()->Post(this, MSG_ONSIGNALINGREADY, NULL); + + // Notify the subclass. + OnTransportSignalingReady(); +} + +void Transport::CallChannels_w(TransportChannelFunc func) { + ASSERT(worker_thread()->IsCurrent()); + talk_base::CritScope cs(&crit_); + for (ChannelMap::iterator iter = channels_.begin(); + iter != channels_.end(); + ++iter) { + ((iter->second.get())->*func)(); + } +} + +bool Transport::VerifyCandidate(const Candidate& cand, std::string* error) { + // No address zero. + if (cand.address().IsNil() || cand.address().IsAny()) { + *error = "candidate has address of zero"; + return false; + } + + // Disallow all ports below 1024, except for 80 and 443 on public addresses. + int port = cand.address().port(); + if (port < 1024) { + if ((port != 80) && (port != 443)) { + *error = "candidate has port below 1024, but not 80 or 443"; + return false; + } + + if (cand.address().IsPrivateIP()) { + *error = "candidate has port of 80 or 443 with private IP address"; + return false; + } + } + + return true; +} + + +bool Transport::GetStats(TransportStats* stats) { + ASSERT(signaling_thread()->IsCurrent()); + StatsParam params(stats); + worker_thread()->Send(this, MSG_GETSTATS, ¶ms); + return params.result; +} + +bool Transport::GetStats_w(TransportStats* stats) { + ASSERT(worker_thread()->IsCurrent()); + stats->content_name = content_name(); + stats->channel_stats.clear(); + for (ChannelMap::iterator iter = channels_.begin(); + iter != channels_.end(); + ++iter) { + TransportChannelStats substats; + substats.component = iter->second->component(); + if (!iter->second->GetStats(&substats.connection_infos)) { + return false; + } + stats->channel_stats.push_back(substats); + } + return true; +} + +void Transport::OnRemoteCandidates(const std::vector& candidates) { + for (std::vector::const_iterator iter = candidates.begin(); + iter != candidates.end(); + ++iter) { + OnRemoteCandidate(*iter); + } +} + +void Transport::OnRemoteCandidate(const Candidate& candidate) { + ASSERT(signaling_thread()->IsCurrent()); + if (destroyed_) return; + + if (!HasChannel(candidate.component())) { + LOG(LS_WARNING) << "Ignoring candidate for unknown component " + << candidate.component(); + return; + } + + ChannelParams* params = new ChannelParams(new Candidate(candidate)); + worker_thread()->Post(this, MSG_ONREMOTECANDIDATE, params); +} + +void Transport::OnRemoteCandidate_w(const Candidate& candidate) { + ASSERT(worker_thread()->IsCurrent()); + ChannelMap::iterator iter = channels_.find(candidate.component()); + // It's ok for a channel to go away while this message is in transit. + if (iter != channels_.end()) { + iter->second->OnCandidate(candidate); + } +} + +void Transport::OnChannelReadableState(TransportChannel* channel) { + ASSERT(worker_thread()->IsCurrent()); + signaling_thread()->Post(this, MSG_READSTATE, NULL); +} + +void Transport::OnChannelReadableState_s() { + ASSERT(signaling_thread()->IsCurrent()); + TransportState readable = GetTransportState_s(true); + if (readable_ != readable) { + readable_ = readable; + SignalReadableState(this); + } +} + +void Transport::OnChannelWritableState(TransportChannel* channel) { + ASSERT(worker_thread()->IsCurrent()); + signaling_thread()->Post(this, MSG_WRITESTATE, NULL); +} + +void Transport::OnChannelWritableState_s() { + ASSERT(signaling_thread()->IsCurrent()); + TransportState writable = GetTransportState_s(false); + if (writable_ != writable) { + was_writable_ = (writable_ == TRANSPORT_STATE_ALL); + writable_ = writable; + SignalWritableState(this); + } +} + +TransportState Transport::GetTransportState_s(bool read) { + ASSERT(signaling_thread()->IsCurrent()); + talk_base::CritScope cs(&crit_); + bool any = false; + bool all = !channels_.empty(); + for (ChannelMap::iterator iter = channels_.begin(); + iter != channels_.end(); + ++iter) { + bool b = (read ? iter->second->readable() : + iter->second->writable()); + any = any || b; + all = all && b; + } + if (all) { + return TRANSPORT_STATE_ALL; + } else if (any) { + return TRANSPORT_STATE_SOME; + } else { + return TRANSPORT_STATE_NONE; + } +} + +void Transport::OnChannelRequestSignaling(TransportChannelImpl* channel) { + ASSERT(worker_thread()->IsCurrent()); + ChannelParams* params = new ChannelParams(channel->component()); + signaling_thread()->Post(this, MSG_REQUESTSIGNALING, params); +} + +void Transport::OnChannelRequestSignaling_s(int component) { + ASSERT(signaling_thread()->IsCurrent()); + LOG(LS_INFO) << "Transport: " << content_name_ << ", allocating candidates"; + // Resetting ICE state for the channel. + { + talk_base::CritScope cs(&crit_); + ChannelMap::iterator iter = channels_.find(component); + if (iter != channels_.end()) + iter->second.set_candidates_allocated(false); + } + SignalRequestSignaling(this); +} + +void Transport::OnChannelCandidateReady(TransportChannelImpl* channel, + const Candidate& candidate) { + ASSERT(worker_thread()->IsCurrent()); + talk_base::CritScope cs(&crit_); + ready_candidates_.push_back(candidate); + + // We hold any messages until the client lets us connect. + if (connect_requested_) { + signaling_thread()->Post( + this, MSG_CANDIDATEREADY, NULL); + } +} + +void Transport::OnChannelCandidateReady_s() { + ASSERT(signaling_thread()->IsCurrent()); + ASSERT(connect_requested_); + + std::vector candidates; + { + talk_base::CritScope cs(&crit_); + candidates.swap(ready_candidates_); + } + + // we do the deleting of Candidate* here to keep the new above and + // delete below close to each other + if (!candidates.empty()) { + SignalCandidatesReady(this, candidates); + } +} + +void Transport::OnChannelRouteChange(TransportChannel* channel, + const Candidate& remote_candidate) { + ASSERT(worker_thread()->IsCurrent()); + ChannelParams* params = new ChannelParams(new Candidate(remote_candidate)); + params->channel = static_cast(channel); + signaling_thread()->Post(this, MSG_ROUTECHANGE, params); +} + +void Transport::OnChannelRouteChange_s(const TransportChannel* channel, + const Candidate& remote_candidate) { + ASSERT(signaling_thread()->IsCurrent()); + SignalRouteChange(this, remote_candidate.component(), remote_candidate); +} + +void Transport::OnChannelCandidatesAllocationDone( + TransportChannelImpl* channel) { + ASSERT(worker_thread()->IsCurrent()); + talk_base::CritScope cs(&crit_); + ChannelMap::iterator iter = channels_.find(channel->component()); + ASSERT(iter != channels_.end()); + LOG(LS_INFO) << "Transport: " << content_name_ << ", component " + << channel->component() << " allocation complete"; + iter->second.set_candidates_allocated(true); + + // If all channels belonging to this Transport got signal, then + // forward this signal to upper layer. + // Can this signal arrive before all transport channels are created? + for (iter = channels_.begin(); iter != channels_.end(); ++iter) { + if (!iter->second.candidates_allocated()) + return; + } + signaling_thread_->Post(this, MSG_CANDIDATEALLOCATIONCOMPLETE); +} + +void Transport::OnChannelCandidatesAllocationDone_s() { + ASSERT(signaling_thread()->IsCurrent()); + LOG(LS_INFO) << "Transport: " << content_name_ << " allocation complete"; + SignalCandidatesAllocationDone(this); +} + +void Transport::OnRoleConflict(TransportChannelImpl* channel) { + signaling_thread_->Post(this, MSG_ROLECONFLICT); +} + +void Transport::SetRole_w(TransportRole role) { + talk_base::CritScope cs(&crit_); + role_ = role; + for (ChannelMap::iterator iter = channels_.begin(); + iter != channels_.end(); ++iter) { + iter->second->SetRole(role_); + } +} + +void Transport::SetRemoteIceMode_w(IceMode mode) { + talk_base::CritScope cs(&crit_); + remote_ice_mode_ = mode; + // Shouldn't channels be created after this method executed? + for (ChannelMap::iterator iter = channels_.begin(); + iter != channels_.end(); ++iter) { + iter->second->SetRemoteIceMode(remote_ice_mode_); + } +} + +bool Transport::SetLocalTransportDescription_w( + const TransportDescription& desc, ContentAction action) { + bool ret = true; + talk_base::CritScope cs(&crit_); + local_description_.reset(new TransportDescription(desc)); + + for (ChannelMap::iterator iter = channels_.begin(); + iter != channels_.end(); ++iter) { + ret &= ApplyLocalTransportDescription_w(iter->second.get()); + } + if (!ret) + return false; + + // If PRANSWER/ANSWER is set, we should decide transport protocol type. + if (action == CA_PRANSWER || action == CA_ANSWER) { + ret &= NegotiateTransportDescription_w(action); + } + return ret; +} + +bool Transport::SetRemoteTransportDescription_w( + const TransportDescription& desc, ContentAction action) { + bool ret = true; + talk_base::CritScope cs(&crit_); + remote_description_.reset(new TransportDescription(desc)); + + for (ChannelMap::iterator iter = channels_.begin(); + iter != channels_.end(); ++iter) { + ret &= ApplyRemoteTransportDescription_w(iter->second.get()); + } + + // If PRANSWER/ANSWER is set, we should decide transport protocol type. + if (action == CA_PRANSWER || action == CA_ANSWER) { + ret = NegotiateTransportDescription_w(CA_OFFER); + } + return ret; +} + +bool Transport::ApplyLocalTransportDescription_w(TransportChannelImpl* ch) { + ch->SetIceCredentials(local_description_->ice_ufrag, + local_description_->ice_pwd); + return true; +} + +bool Transport::ApplyRemoteTransportDescription_w(TransportChannelImpl* ch) { + ch->SetRemoteIceCredentials(remote_description_->ice_ufrag, + remote_description_->ice_pwd); + return true; +} + +void Transport::ApplyNegotiatedTransportDescription_w( + TransportChannelImpl* channel) { + channel->SetIceProtocolType(protocol_); + channel->SetRemoteIceMode(remote_ice_mode_); +} + +bool Transport::NegotiateTransportDescription_w(ContentAction local_role_) { + // TODO(ekr@rtfm.com): This is ICE-specific stuff. Refactor into + // P2PTransport. + const TransportDescription* offer; + const TransportDescription* answer; + + if (local_role_ == CA_OFFER) { + offer = local_description_.get(); + answer = remote_description_.get(); + } else { + offer = remote_description_.get(); + answer = local_description_.get(); + } + + TransportProtocol offer_proto = TransportProtocolFromDescription(offer); + TransportProtocol answer_proto = TransportProtocolFromDescription(answer); + + // If offered protocol is gice/ice, then we expect to receive matching + // protocol in answer, anything else is treated as an error. + // HYBRID is not an option when offered specific protocol. + // If offered protocol is HYBRID and answered protocol is HYBRID then + // gice is preferred protocol. + // TODO(mallinath) - Answer from local or remote should't have both ice + // and gice support. It should always pick which protocol it wants to use. + // Once WebRTC stops supporting gice (for backward compatibility), HYBRID in + // answer must be treated as error. + if ((offer_proto == ICEPROTO_GOOGLE || offer_proto == ICEPROTO_RFC5245) && + (offer_proto != answer_proto)) { + return false; + } + protocol_ = answer_proto == ICEPROTO_HYBRID ? ICEPROTO_GOOGLE : answer_proto; + + // If transport is in ROLE_CONTROLLED and remote end point supports only + // ice_lite, this local end point should take CONTROLLING role. + if (role_ == ROLE_CONTROLLED && + remote_description_->ice_mode == ICEMODE_LITE) { + SetRole_w(ROLE_CONTROLLING); + } + + // Update remote ice_mode to all existing channels. + remote_ice_mode_ = remote_description_->ice_mode; + + // Now that we have negotiated everything, push it downward. + // Note that we cache the result so that if we have race conditions + // between future SetRemote/SetLocal invocations and new channel + // creation, we have the negotiation state saved until a new + // negotiation happens. + for (ChannelMap::iterator iter = channels_.begin(); + iter != channels_.end(); + ++iter) { + ApplyNegotiatedTransportDescription_w(iter->second.get()); + } + return true; +} + +void Transport::OnMessage(talk_base::Message* msg) { + switch (msg->message_id) { + case MSG_CREATECHANNEL: { + ChannelParams* params = static_cast(msg->pdata); + params->channel = CreateChannel_w(params->component); + } + break; + case MSG_DESTROYCHANNEL: { + ChannelParams* params = static_cast(msg->pdata); + DestroyChannel_w(params->component); + } + break; + case MSG_CONNECTCHANNELS: + ConnectChannels_w(); + break; + case MSG_RESETCHANNELS: + ResetChannels_w(); + break; + case MSG_DESTROYALLCHANNELS: + DestroyAllChannels_w(); + break; + case MSG_ONSIGNALINGREADY: + CallChannels_w(&TransportChannelImpl::OnSignalingReady); + break; + case MSG_ONREMOTECANDIDATE: { + ChannelParams* params = static_cast(msg->pdata); + OnRemoteCandidate_w(*params->candidate); + delete params; + } + break; + case MSG_CONNECTING: + OnConnecting_s(); + break; + case MSG_READSTATE: + OnChannelReadableState_s(); + break; + case MSG_WRITESTATE: + OnChannelWritableState_s(); + break; + case MSG_REQUESTSIGNALING: { + ChannelParams* params = static_cast(msg->pdata); + OnChannelRequestSignaling_s(params->component); + delete params; + } + break; + case MSG_CANDIDATEREADY: + OnChannelCandidateReady_s(); + break; + case MSG_ROUTECHANGE: { + ChannelParams* params = static_cast(msg->pdata); + OnChannelRouteChange_s(params->channel, *params->candidate); + delete params; + } + break; + case MSG_CANDIDATEALLOCATIONCOMPLETE: + OnChannelCandidatesAllocationDone_s(); + break; + case MSG_ROLECONFLICT: + SignalRoleConflict(); + break; + case MSG_SETROLE: { + TransportRoleParam* param = + static_cast(msg->pdata); + SetRole_w(param->role); + } + break; + case MSG_SETLOCALDESCRIPTION: { + TransportDescriptionParams* params = + static_cast(msg->pdata); + params->result = SetLocalTransportDescription_w(params->desc, + params->action); + } + break; + case MSG_SETREMOTEDESCRIPTION: { + TransportDescriptionParams* params = + static_cast(msg->pdata); + params->result = SetRemoteTransportDescription_w(params->desc, + params->action); + } + break; + case MSG_GETSTATS: { + StatsParam* params = static_cast(msg->pdata); + params->result = GetStats_w(params->stats); + } + break; + } +} + +bool TransportParser::ParseAddress(const buzz::XmlElement* elem, + const buzz::QName& address_name, + const buzz::QName& port_name, + talk_base::SocketAddress* address, + ParseError* error) { + if (!elem->HasAttr(address_name)) + return BadParse("address does not have " + address_name.LocalPart(), error); + if (!elem->HasAttr(port_name)) + return BadParse("address does not have " + port_name.LocalPart(), error); + + address->SetIP(elem->Attr(address_name)); + std::istringstream ist(elem->Attr(port_name)); + int port = 0; + ist >> port; + address->SetPort(port); + + return true; +} + +// We're GICE if the namespace is NS_GOOGLE_P2P, or if NS_JINGLE_ICE_UDP is +// used and the GICE ice-option is set. +TransportProtocol TransportProtocolFromDescription( + const TransportDescription* desc) { + ASSERT(desc != NULL); + if (desc->transport_type == NS_JINGLE_ICE_UDP) { + return (desc->HasOption(ICE_OPTION_GICE)) ? + ICEPROTO_HYBRID : ICEPROTO_RFC5245; + } + return ICEPROTO_GOOGLE; +} + +} // namespace cricket diff --git a/talk/p2p/base/transport.h b/talk/p2p/base/transport.h new file mode 100644 index 000000000..b93513e00 --- /dev/null +++ b/talk/p2p/base/transport.h @@ -0,0 +1,481 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +// A Transport manages a set of named channels of the same type. +// +// Subclasses choose the appropriate class to instantiate for each channel; +// however, this base class keeps track of the channels by name, watches their +// state changes (in order to update the manager's state), and forwards +// requests to begin connecting or to reset to each of the channels. +// +// On Threading: Transport performs work on both the signaling and worker +// threads. For subclasses, the rule is that all signaling related calls will +// be made on the signaling thread and all channel related calls (including +// signaling for a channel) will be made on the worker thread. When +// information needs to be sent between the two threads, this class should do +// the work (e.g., OnRemoteCandidate). +// +// Note: Subclasses must call DestroyChannels() in their own constructors. +// It is not possible to do so here because the subclass constructor will +// already have run. + +#ifndef TALK_P2P_BASE_TRANSPORT_H_ +#define TALK_P2P_BASE_TRANSPORT_H_ + +#include +#include +#include +#include "talk/base/criticalsection.h" +#include "talk/base/messagequeue.h" +#include "talk/base/sigslot.h" +#include "talk/p2p/base/candidate.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/sessiondescription.h" +#include "talk/p2p/base/transportinfo.h" + +namespace talk_base { +class Thread; +} + +namespace buzz { +class QName; +class XmlElement; +} + +namespace cricket { + +struct ParseError; +struct WriteError; +class CandidateTranslator; +class PortAllocator; +class SessionManager; +class Session; +class TransportChannel; +class TransportChannelImpl; + +typedef std::vector XmlElements; +typedef std::vector Candidates; + +// Used to parse and serialize (write) transport candidates. For +// convenience of old code, Transports will implement TransportParser. +// Parse/Write seems better than Serialize/Deserialize or +// Create/Translate. +class TransportParser { + public: + // The incoming Translator value may be null, in which case + // ParseCandidates should return false if there are candidates to + // parse (indicating a failure to parse). If the Translator is null + // and there are no candidates to parse, then return true, + // indicating a successful parse of 0 candidates. + + // Parse or write a transport description, including ICE credentials and + // any DTLS fingerprint. Since only Jingle has transport descriptions, these + // functions are only used when serializing to Jingle. + virtual bool ParseTransportDescription(const buzz::XmlElement* elem, + const CandidateTranslator* translator, + TransportDescription* tdesc, + ParseError* error) = 0; + virtual bool WriteTransportDescription(const TransportDescription& tdesc, + const CandidateTranslator* translator, + buzz::XmlElement** tdesc_elem, + WriteError* error) = 0; + + + // Parse a single candidate. This must be used when parsing Gingle + // candidates, since there is no enclosing transport description. + virtual bool ParseGingleCandidate(const buzz::XmlElement* elem, + const CandidateTranslator* translator, + Candidate* candidates, + ParseError* error) = 0; + virtual bool WriteGingleCandidate(const Candidate& candidate, + const CandidateTranslator* translator, + buzz::XmlElement** candidate_elem, + WriteError* error) = 0; + + // Helper function to parse an element describing an address. This + // retrieves the IP and port from the given element and verifies + // that they look like plausible values. + bool ParseAddress(const buzz::XmlElement* elem, + const buzz::QName& address_name, + const buzz::QName& port_name, + talk_base::SocketAddress* address, + ParseError* error); + + virtual ~TransportParser() {} +}; + +// Whether our side of the call is driving the negotiation, or the other side. +enum TransportRole { + ROLE_CONTROLLING = 0, + ROLE_CONTROLLED, + ROLE_UNKNOWN +}; + +// For "writable" and "readable", we need to differentiate between +// none, all, and some. +enum TransportState { + TRANSPORT_STATE_NONE = 0, + TRANSPORT_STATE_SOME, + TRANSPORT_STATE_ALL +}; + +// Stats that we can return about the connections for a transport channel. +// TODO(hta): Rename to ConnectionStats +struct ConnectionInfo { + bool best_connection; // Is this the best connection we have? + bool writable; // Has this connection received a STUN response? + bool readable; // Has this connection received a STUN request? + bool timeout; // Has this connection timed out? + bool new_connection; // Is this a newly created connection? + size_t rtt; // The STUN RTT for this connection. + size_t sent_total_bytes; // Total bytes sent on this connection. + size_t sent_bytes_second; // Bps over the last measurement interval. + size_t recv_total_bytes; // Total bytes received on this connection. + size_t recv_bytes_second; // Bps over the last measurement interval. + Candidate local_candidate; // The local candidate for this connection. + Candidate remote_candidate; // The remote candidate for this connection. + void* key; // A static value that identifies this conn. +}; + +// Information about all the connections of a channel. +typedef std::vector ConnectionInfos; + +// Information about a specific channel +struct TransportChannelStats { + int component; + ConnectionInfos connection_infos; +}; + +// Information about all the channels of a transport. +// TODO(hta): Consider if a simple vector is as good as a map. +typedef std::vector TransportChannelStatsList; + +// Information about the stats of a transport. +struct TransportStats { + std::string content_name; + TransportChannelStatsList channel_stats; +}; + +class Transport : public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + Transport(talk_base::Thread* signaling_thread, + talk_base::Thread* worker_thread, + const std::string& content_name, + const std::string& type, + PortAllocator* allocator); + virtual ~Transport(); + + // Returns the signaling thread. The app talks to Transport on this thread. + talk_base::Thread* signaling_thread() { return signaling_thread_; } + // Returns the worker thread. The actual networking is done on this thread. + talk_base::Thread* worker_thread() { return worker_thread_; } + + // Returns the content_name of this transport. + const std::string& content_name() const { return content_name_; } + // Returns the type of this transport. + const std::string& type() const { return type_; } + + // Returns the port allocator object for this transport. + PortAllocator* port_allocator() { return allocator_; } + + // Returns the readable and states of this manager. These bits are the ORs + // of the corresponding bits on the managed channels. Each time one of these + // states changes, a signal is raised. + // TODO: Replace uses of readable() and writable() with + // any_channels_readable() and any_channels_writable(). + bool readable() const { return any_channels_readable(); } + bool writable() const { return any_channels_writable(); } + bool was_writable() const { return was_writable_; } + bool any_channels_readable() const { + return (readable_ == TRANSPORT_STATE_SOME || + readable_ == TRANSPORT_STATE_ALL); + } + bool any_channels_writable() const { + return (writable_ == TRANSPORT_STATE_SOME || + writable_ == TRANSPORT_STATE_ALL); + } + bool all_channels_readable() const { + return (readable_ == TRANSPORT_STATE_ALL); + } + bool all_channels_writable() const { + return (writable_ == TRANSPORT_STATE_ALL); + } + sigslot::signal1 SignalReadableState; + sigslot::signal1 SignalWritableState; + + // Returns whether the client has requested the channels to connect. + bool connect_requested() const { return connect_requested_; } + + void SetRole(TransportRole role); + TransportRole role() const { return role_; } + + void SetTiebreaker(uint64 tiebreaker) { tiebreaker_ = tiebreaker; } + uint64 tiebreaker() { return tiebreaker_; } + + TransportProtocol protocol() const { return protocol_; } + + // Create, destroy, and lookup the channels of this type by their components. + TransportChannelImpl* CreateChannel(int component); + // Note: GetChannel may lead to race conditions, since the mutex is not held + // after the pointer is returned. + TransportChannelImpl* GetChannel(int component); + // Note: HasChannel does not lead to race conditions, unlike GetChannel. + bool HasChannel(int component) { + return (NULL != GetChannel(component)); + } + bool HasChannels(); + void DestroyChannel(int component); + + // Set the local TransportDescription to be used by TransportChannels. + // This should be called before ConnectChannels(). + bool SetLocalTransportDescription(const TransportDescription& description, + ContentAction action); + + // Set the remote TransportDescription to be used by TransportChannels. + bool SetRemoteTransportDescription(const TransportDescription& description, + ContentAction action); + + // Tells all current and future channels to start connecting. When the first + // channel begins connecting, the following signal is raised. + void ConnectChannels(); + sigslot::signal1 SignalConnecting; + + // Resets all of the channels back to their initial state. They are no + // longer connecting. + void ResetChannels(); + + // Destroys every channel created so far. + void DestroyAllChannels(); + + bool GetStats(TransportStats* stats); + + // Before any stanza is sent, the manager will request signaling. Once + // signaling is available, the client should call OnSignalingReady. Once + // this occurs, the transport (or its channels) can send any waiting stanzas. + // OnSignalingReady invokes OnTransportSignalingReady and then forwards this + // signal to each channel. + sigslot::signal1 SignalRequestSignaling; + void OnSignalingReady(); + + // Handles sending of ready candidates and receiving of remote candidates. + sigslot::signal2&> SignalCandidatesReady; + + sigslot::signal1 SignalCandidatesAllocationDone; + void OnRemoteCandidates(const std::vector& candidates); + + // If candidate is not acceptable, returns false and sets error. + // Call this before calling OnRemoteCandidates. + virtual bool VerifyCandidate(const Candidate& candidate, + std::string* error); + + // Signals when the best connection for a channel changes. + sigslot::signal3 SignalRouteChange; + + // A transport message has generated an transport-specific error. The + // stanza that caused the error is available in session_msg. If false is + // returned, the error is considered unrecoverable, and the session is + // terminated. + // TODO(juberti): Remove these obsolete functions once Session no longer + // references them. + virtual void OnTransportError(const buzz::XmlElement* error) {} + sigslot::signal6 + SignalTransportError; + + // Forwards the signal from TransportChannel to BaseSession. + sigslot::signal0<> SignalRoleConflict; + + protected: + // These are called by Create/DestroyChannel above in order to create or + // destroy the appropriate type of channel. + virtual TransportChannelImpl* CreateTransportChannel(int component) = 0; + virtual void DestroyTransportChannel(TransportChannelImpl* channel) = 0; + + // Informs the subclass that we received the signaling ready message. + virtual void OnTransportSignalingReady() {} + + // The current local transport description, for use by derived classes + // when performing transport description negotiation. + const TransportDescription* local_description() const { + return local_description_.get(); + } + + // The current remote transport description, for use by derived classes + // when performing transport description negotiation. + const TransportDescription* remote_description() const { + return remote_description_.get(); + } + + // Pushes down the transport parameters from the local description, such + // as the ICE ufrag and pwd. + // Derived classes can override, but must call the base as well. + virtual bool ApplyLocalTransportDescription_w(TransportChannelImpl* + channel); + + // Pushes down remote ice credentials from the remote description to the + // transport channel. + virtual bool ApplyRemoteTransportDescription_w(TransportChannelImpl* ch); + + // Negotiates the transport parameters based on the current local and remote + // transport description, such at the version of ICE to use, and whether DTLS + // should be activated. + // Derived classes can negotiate their specific parameters here, but must call + // the base as well. + virtual bool NegotiateTransportDescription_w(ContentAction local_role); + + // Pushes down the transport parameters obtained via negotiation. + // Derived classes can set their specific parameters here, but must call the + // base as well. + virtual void ApplyNegotiatedTransportDescription_w( + TransportChannelImpl* channel); + + private: + struct ChannelMapEntry { + ChannelMapEntry() : impl_(NULL), candidates_allocated_(false), ref_(0) {} + explicit ChannelMapEntry(TransportChannelImpl *impl) + : impl_(impl), + candidates_allocated_(false), + ref_(0) { + } + + void AddRef() { ++ref_; } + void DecRef() { + ASSERT(ref_ > 0); + --ref_; + } + int ref() const { return ref_; } + + TransportChannelImpl* get() const { return impl_; } + TransportChannelImpl* operator->() const { return impl_; } + void set_candidates_allocated(bool status) { + candidates_allocated_ = status; + } + bool candidates_allocated() const { return candidates_allocated_; } + + private: + TransportChannelImpl *impl_; + bool candidates_allocated_; + int ref_; + }; + + // Candidate component => ChannelMapEntry + typedef std::map ChannelMap; + + // Called when the state of a channel changes. + void OnChannelReadableState(TransportChannel* channel); + void OnChannelWritableState(TransportChannel* channel); + + // Called when a channel requests signaling. + void OnChannelRequestSignaling(TransportChannelImpl* channel); + + // Called when a candidate is ready from remote peer. + void OnRemoteCandidate(const Candidate& candidate); + // Called when a candidate is ready from channel. + void OnChannelCandidateReady(TransportChannelImpl* channel, + const Candidate& candidate); + void OnChannelRouteChange(TransportChannel* channel, + const Candidate& remote_candidate); + void OnChannelCandidatesAllocationDone(TransportChannelImpl* channel); + // Called when there is ICE role change. + void OnRoleConflict(TransportChannelImpl* channel); + + // Dispatches messages to the appropriate handler (below). + void OnMessage(talk_base::Message* msg); + + // These are versions of the above methods that are called only on a + // particular thread (s = signaling, w = worker). The above methods post or + // send a message to invoke this version. + TransportChannelImpl* CreateChannel_w(int component); + void DestroyChannel_w(int component); + void ConnectChannels_w(); + void ResetChannels_w(); + void DestroyAllChannels_w(); + void OnRemoteCandidate_w(const Candidate& candidate); + void OnChannelReadableState_s(); + void OnChannelWritableState_s(); + void OnChannelRequestSignaling_s(int component); + void OnConnecting_s(); + void OnChannelRouteChange_s(const TransportChannel* channel, + const Candidate& remote_candidate); + void OnChannelCandidatesAllocationDone_s(); + + // Helper function that invokes the given function on every channel. + typedef void (TransportChannelImpl::* TransportChannelFunc)(); + void CallChannels_w(TransportChannelFunc func); + + // Computes the OR of the channel's read or write state (argument picks). + TransportState GetTransportState_s(bool read); + + void OnChannelCandidateReady_s(); + + void SetRole_w(TransportRole role); + void SetRemoteIceMode_w(IceMode mode); + bool SetLocalTransportDescription_w(const TransportDescription& desc, + ContentAction action); + bool SetRemoteTransportDescription_w(const TransportDescription& desc, + ContentAction action); + bool GetStats_w(TransportStats* infos); + + talk_base::Thread* signaling_thread_; + talk_base::Thread* worker_thread_; + std::string content_name_; + std::string type_; + PortAllocator* allocator_; + bool destroyed_; + TransportState readable_; + TransportState writable_; + bool was_writable_; + bool connect_requested_; + TransportRole role_; + uint64 tiebreaker_; + TransportProtocol protocol_; + IceMode remote_ice_mode_; + talk_base::scoped_ptr local_description_; + talk_base::scoped_ptr remote_description_; + + ChannelMap channels_; + // Buffers the ready_candidates so that SignalCanidatesReady can + // provide them in multiples. + std::vector ready_candidates_; + // Protects changes to channels and messages + talk_base::CriticalSection crit_; + + DISALLOW_EVIL_CONSTRUCTORS(Transport); +}; + +// Extract a TransportProtocol from a TransportDescription. +TransportProtocol TransportProtocolFromDescription( + const TransportDescription* desc); + +} // namespace cricket + +#endif // TALK_P2P_BASE_TRANSPORT_H_ diff --git a/talk/p2p/base/transport_unittest.cc b/talk/p2p/base/transport_unittest.cc new file mode 100644 index 000000000..446fda581 --- /dev/null +++ b/talk/p2p/base/transport_unittest.cc @@ -0,0 +1,309 @@ +/* + * libjingle + * Copyright 2011 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/base/fakesslidentity.h" +#include "talk/base/gunit.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/fakesession.h" +#include "talk/p2p/base/parsing.h" +#include "talk/p2p/base/p2ptransport.h" +#include "talk/p2p/base/rawtransport.h" +#include "talk/p2p/base/sessionmessages.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" + +using cricket::Candidate; +using cricket::Candidates; +using cricket::Transport; +using cricket::FakeTransport; +using cricket::TransportChannel; +using cricket::FakeTransportChannel; +using cricket::TransportRole; +using cricket::TransportDescription; +using cricket::WriteError; +using cricket::ParseError; +using talk_base::SocketAddress; + +static const char kIceUfrag1[] = "TESTICEUFRAG0001"; +static const char kIcePwd1[] = "TESTICEPWD00000000000001"; + +class TransportTest : public testing::Test, + public sigslot::has_slots<> { + public: + TransportTest() + : thread_(talk_base::Thread::Current()), + transport_(new FakeTransport( + thread_, thread_, "test content name", NULL)), + channel_(NULL), + connecting_signalled_(false) { + transport_->SignalConnecting.connect(this, &TransportTest::OnConnecting); + } + ~TransportTest() { + transport_->DestroyAllChannels(); + } + bool SetupChannel() { + channel_ = CreateChannel(1); + return (channel_ != NULL); + } + FakeTransportChannel* CreateChannel(int component) { + return static_cast( + transport_->CreateChannel(component)); + } + void DestroyChannel() { + transport_->DestroyChannel(1); + channel_ = NULL; + } + + protected: + void OnConnecting(Transport* transport) { + connecting_signalled_ = true; + } + + talk_base::Thread* thread_; + talk_base::scoped_ptr transport_; + FakeTransportChannel* channel_; + bool connecting_signalled_; +}; + +class FakeCandidateTranslator : public cricket::CandidateTranslator { + public: + void AddMapping(int component, const std::string& channel_name) { + name_to_component[channel_name] = component; + component_to_name[component] = channel_name; + } + + bool GetChannelNameFromComponent( + int component, std::string* channel_name) const { + if (component_to_name.find(component) == component_to_name.end()) { + return false; + } + *channel_name = component_to_name.find(component)->second; + return true; + } + bool GetComponentFromChannelName( + const std::string& channel_name, int* component) const { + if (name_to_component.find(channel_name) == name_to_component.end()) { + return false; + } + *component = name_to_component.find(channel_name)->second; + return true; + } + + std::map name_to_component; + std::map component_to_name; +}; + +// Test that calling ConnectChannels triggers an OnConnecting signal. +TEST_F(TransportTest, TestConnectChannelsDoesSignal) { + EXPECT_TRUE(SetupChannel()); + transport_->ConnectChannels(); + EXPECT_FALSE(connecting_signalled_); + + EXPECT_TRUE_WAIT(connecting_signalled_, 100); +} + +// Test that DestroyAllChannels kills any pending OnConnecting signals. +TEST_F(TransportTest, TestDestroyAllClearsPosts) { + EXPECT_TRUE(transport_->CreateChannel(1) != NULL); + + transport_->ConnectChannels(); + transport_->DestroyAllChannels(); + + thread_->ProcessMessages(0); + EXPECT_FALSE(connecting_signalled_); +} + +// This test verifies channels are created with proper ICE +// role, tiebreaker and remote ice mode and credentials after offer and answer +// negotiations. +TEST_F(TransportTest, TestChannelIceParameters) { + transport_->SetRole(cricket::ROLE_CONTROLLING); + transport_->SetTiebreaker(99U); + cricket::TransportDescription local_desc( + cricket::NS_JINGLE_ICE_UDP, std::vector(), + kIceUfrag1, kIcePwd1, cricket::ICEMODE_FULL, NULL, cricket::Candidates()); + ASSERT_TRUE(transport_->SetLocalTransportDescription(local_desc, + cricket::CA_OFFER)); + EXPECT_EQ(cricket::ROLE_CONTROLLING, transport_->role()); + EXPECT_TRUE(SetupChannel()); + EXPECT_EQ(cricket::ROLE_CONTROLLING, channel_->GetRole()); + EXPECT_EQ(cricket::ICEMODE_FULL, channel_->remote_ice_mode()); + EXPECT_EQ(kIceUfrag1, channel_->ice_ufrag()); + EXPECT_EQ(kIcePwd1, channel_->ice_pwd()); + + cricket::TransportDescription remote_desc( + cricket::NS_JINGLE_ICE_UDP, std::vector(), + kIceUfrag1, kIcePwd1, cricket::ICEMODE_FULL, NULL, cricket::Candidates()); + ASSERT_TRUE(transport_->SetRemoteTransportDescription(remote_desc, + cricket::CA_ANSWER)); + EXPECT_EQ(cricket::ROLE_CONTROLLING, channel_->GetRole()); + EXPECT_EQ(99U, channel_->tiebreaker()); + EXPECT_EQ(cricket::ICEMODE_FULL, channel_->remote_ice_mode()); + // Changing the transport role from CONTROLLING to CONTROLLED. + transport_->SetRole(cricket::ROLE_CONTROLLED); + EXPECT_EQ(cricket::ROLE_CONTROLLED, channel_->GetRole()); + EXPECT_EQ(cricket::ICEMODE_FULL, channel_->remote_ice_mode()); + EXPECT_EQ(kIceUfrag1, channel_->remote_ice_ufrag()); + EXPECT_EQ(kIcePwd1, channel_->remote_ice_pwd()); +} + +// Tests channel role is reversed after receiving ice-lite from remote. +TEST_F(TransportTest, TestSetRemoteIceLiteInOffer) { + transport_->SetRole(cricket::ROLE_CONTROLLED); + cricket::TransportDescription remote_desc( + cricket::NS_JINGLE_ICE_UDP, std::vector(), + kIceUfrag1, kIcePwd1, cricket::ICEMODE_LITE, NULL, cricket::Candidates()); + ASSERT_TRUE(transport_->SetRemoteTransportDescription(remote_desc, + cricket::CA_OFFER)); + cricket::TransportDescription local_desc( + cricket::NS_JINGLE_ICE_UDP, std::vector(), + kIceUfrag1, kIcePwd1, cricket::ICEMODE_FULL, NULL, cricket::Candidates()); + ASSERT_TRUE(transport_->SetLocalTransportDescription(local_desc, + cricket::CA_ANSWER)); + EXPECT_EQ(cricket::ROLE_CONTROLLING, transport_->role()); + EXPECT_TRUE(SetupChannel()); + EXPECT_EQ(cricket::ROLE_CONTROLLING, channel_->GetRole()); + EXPECT_EQ(cricket::ICEMODE_LITE, channel_->remote_ice_mode()); +} + +// Tests ice-lite in remote answer. +TEST_F(TransportTest, TestSetRemoteIceLiteInAnswer) { + transport_->SetRole(cricket::ROLE_CONTROLLING); + cricket::TransportDescription local_desc( + cricket::NS_JINGLE_ICE_UDP, std::vector(), + kIceUfrag1, kIcePwd1, cricket::ICEMODE_FULL, NULL, cricket::Candidates()); + ASSERT_TRUE(transport_->SetLocalTransportDescription(local_desc, + cricket::CA_OFFER)); + EXPECT_EQ(cricket::ROLE_CONTROLLING, transport_->role()); + EXPECT_TRUE(SetupChannel()); + EXPECT_EQ(cricket::ROLE_CONTROLLING, channel_->GetRole()); + // Channels will be created in ICEFULL_MODE. + EXPECT_EQ(cricket::ICEMODE_FULL, channel_->remote_ice_mode()); + cricket::TransportDescription remote_desc( + cricket::NS_JINGLE_ICE_UDP, std::vector(), + kIceUfrag1, kIcePwd1, cricket::ICEMODE_LITE, NULL, cricket::Candidates()); + ASSERT_TRUE(transport_->SetRemoteTransportDescription(remote_desc, + cricket::CA_ANSWER)); + EXPECT_EQ(cricket::ROLE_CONTROLLING, channel_->GetRole()); + // After receiving remote description with ICEMODE_LITE, channel should + // have mode set to ICEMODE_LITE. + EXPECT_EQ(cricket::ICEMODE_LITE, channel_->remote_ice_mode()); +} + +// Tests that we can properly serialize/deserialize candidates. +TEST_F(TransportTest, TestP2PTransportWriteAndParseCandidate) { + Candidate test_candidate( + "", 1, "udp", + talk_base::SocketAddress("2001:db8:fefe::1", 9999), + 738197504, "abcdef", "ghijkl", "foo", "testnet", 50, ""); + Candidate test_candidate2( + "", 2, "tcp", + talk_base::SocketAddress("192.168.7.1", 9999), + 1107296256, "mnopqr", "stuvwx", "bar", "testnet2", 100, ""); + talk_base::SocketAddress host_address("www.google.com", 24601); + host_address.SetResolvedIP(talk_base::IPAddress(0x0A000001)); + Candidate test_candidate3( + "", 3, "spdy", host_address, 1476395008, "yzabcd", + "efghij", "baz", "testnet3", 150, ""); + WriteError write_error; + ParseError parse_error; + talk_base::scoped_ptr elem; + cricket::Candidate parsed_candidate; + cricket::P2PTransportParser parser; + + FakeCandidateTranslator translator; + translator.AddMapping(1, "test"); + translator.AddMapping(2, "test2"); + translator.AddMapping(3, "test3"); + + EXPECT_TRUE(parser.WriteGingleCandidate(test_candidate, &translator, + elem.accept(), &write_error)); + EXPECT_EQ("", write_error.text); + EXPECT_EQ("test", elem->Attr(buzz::QN_NAME)); + EXPECT_EQ("udp", elem->Attr(cricket::QN_PROTOCOL)); + EXPECT_EQ("2001:db8:fefe::1", elem->Attr(cricket::QN_ADDRESS)); + EXPECT_EQ("9999", elem->Attr(cricket::QN_PORT)); + EXPECT_EQ("0.34", elem->Attr(cricket::QN_PREFERENCE)); + EXPECT_EQ("abcdef", elem->Attr(cricket::QN_USERNAME)); + EXPECT_EQ("ghijkl", elem->Attr(cricket::QN_PASSWORD)); + EXPECT_EQ("foo", elem->Attr(cricket::QN_TYPE)); + EXPECT_EQ("testnet", elem->Attr(cricket::QN_NETWORK)); + EXPECT_EQ("50", elem->Attr(cricket::QN_GENERATION)); + + EXPECT_TRUE(parser.ParseGingleCandidate(elem.get(), &translator, + &parsed_candidate, &parse_error)); + EXPECT_TRUE(test_candidate.IsEquivalent(parsed_candidate)); + + EXPECT_TRUE(parser.WriteGingleCandidate(test_candidate2, &translator, + elem.accept(), &write_error)); + EXPECT_EQ("test2", elem->Attr(buzz::QN_NAME)); + EXPECT_EQ("tcp", elem->Attr(cricket::QN_PROTOCOL)); + EXPECT_EQ("192.168.7.1", elem->Attr(cricket::QN_ADDRESS)); + EXPECT_EQ("9999", elem->Attr(cricket::QN_PORT)); + EXPECT_EQ("0.51", elem->Attr(cricket::QN_PREFERENCE)); + EXPECT_EQ("mnopqr", elem->Attr(cricket::QN_USERNAME)); + EXPECT_EQ("stuvwx", elem->Attr(cricket::QN_PASSWORD)); + EXPECT_EQ("bar", elem->Attr(cricket::QN_TYPE)); + EXPECT_EQ("testnet2", elem->Attr(cricket::QN_NETWORK)); + EXPECT_EQ("100", elem->Attr(cricket::QN_GENERATION)); + + EXPECT_TRUE(parser.ParseGingleCandidate(elem.get(), &translator, + &parsed_candidate, &parse_error)); + EXPECT_TRUE(test_candidate2.IsEquivalent(parsed_candidate)); + + // Check that an ip is preferred over hostname. + EXPECT_TRUE(parser.WriteGingleCandidate(test_candidate3, &translator, + elem.accept(), &write_error)); + EXPECT_EQ("test3", elem->Attr(cricket::QN_NAME)); + EXPECT_EQ("spdy", elem->Attr(cricket::QN_PROTOCOL)); + EXPECT_EQ("10.0.0.1", elem->Attr(cricket::QN_ADDRESS)); + EXPECT_EQ("24601", elem->Attr(cricket::QN_PORT)); + EXPECT_EQ("0.69", elem->Attr(cricket::QN_PREFERENCE)); + EXPECT_EQ("yzabcd", elem->Attr(cricket::QN_USERNAME)); + EXPECT_EQ("efghij", elem->Attr(cricket::QN_PASSWORD)); + EXPECT_EQ("baz", elem->Attr(cricket::QN_TYPE)); + EXPECT_EQ("testnet3", elem->Attr(cricket::QN_NETWORK)); + EXPECT_EQ("150", elem->Attr(cricket::QN_GENERATION)); + + EXPECT_TRUE(parser.ParseGingleCandidate(elem.get(), &translator, + &parsed_candidate, &parse_error)); + EXPECT_TRUE(test_candidate3.IsEquivalent(parsed_candidate)); +} + +TEST_F(TransportTest, TestGetStats) { + EXPECT_TRUE(SetupChannel()); + cricket::TransportStats stats; + EXPECT_TRUE(transport_->GetStats(&stats)); + // Note that this tests the behavior of a FakeTransportChannel. + ASSERT_EQ(1U, stats.channel_stats.size()); + EXPECT_EQ(1, stats.channel_stats[0].component); + transport_->ConnectChannels(); + EXPECT_TRUE(transport_->GetStats(&stats)); + ASSERT_EQ(1U, stats.channel_stats.size()); + EXPECT_EQ(1, stats.channel_stats[0].component); +} diff --git a/talk/p2p/base/transportchannel.cc b/talk/p2p/base/transportchannel.cc new file mode 100644 index 000000000..50ebfb991 --- /dev/null +++ b/talk/p2p/base/transportchannel.cc @@ -0,0 +1,60 @@ +/* + * libjingle + * Copyright 2004--2005, 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 +#include "talk/p2p/base/transportchannel.h" + +namespace cricket { + +std::string TransportChannel::ToString() const { + const char READABLE_ABBREV[2] = { '_', 'R' }; + const char WRITABLE_ABBREV[2] = { '_', 'W' }; + std::stringstream ss; + ss << "Channel[" << content_name_ + << "|" << component_ + << "|" << READABLE_ABBREV[readable_] << WRITABLE_ABBREV[writable_] << "]"; + return ss.str(); +} + +void TransportChannel::set_readable(bool readable) { + if (readable_ != readable) { + readable_ = readable; + SignalReadableState(this); + } +} + +void TransportChannel::set_writable(bool writable) { + if (writable_ != writable) { + writable_ = writable; + if (writable_) { + SignalReadyToSend(this); + } + SignalWritableState(this); + } +} + +} // namespace cricket diff --git a/talk/p2p/base/transportchannel.h b/talk/p2p/base/transportchannel.h new file mode 100644 index 000000000..4027e76f8 --- /dev/null +++ b/talk/p2p/base/transportchannel.h @@ -0,0 +1,164 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_TRANSPORTCHANNEL_H_ +#define TALK_P2P_BASE_TRANSPORTCHANNEL_H_ + +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/sigslot.h" +#include "talk/base/socket.h" +#include "talk/base/sslidentity.h" +#include "talk/base/sslstreamadapter.h" +#include "talk/p2p/base/candidate.h" +#include "talk/p2p/base/transport.h" +#include "talk/p2p/base/transportdescription.h" + +namespace cricket { + +class Candidate; + +// Flags for SendPacket/SignalReadPacket. +enum PacketFlags { + PF_NORMAL = 0x00, // A normal packet. + PF_SRTP_BYPASS = 0x01, // An encrypted SRTP packet; bypass any additional + // crypto provided by the transport (e.g. DTLS) +}; + +// A TransportChannel represents one logical stream of packets that are sent +// between the two sides of a session. +class TransportChannel : public sigslot::has_slots<> { + public: + explicit TransportChannel(const std::string& content_name, int component) + : content_name_(content_name), + component_(component), + readable_(false), writable_(false) {} + virtual ~TransportChannel() {} + + // Returns the session id of this channel. + virtual const std::string& SessionId() const { return session_id_; } + // Sets session id which created this transport channel. + // This is called from TransportProxy::GetOrCreateImpl. + virtual void SetSessionId(const std::string& session_id) { + session_id_ = session_id; + } + const std::string& content_name() const { return content_name_; } + int component() const { return component_; } + + // Returns the readable and states of this channel. Each time one of these + // states changes, a signal is raised. These states are aggregated by the + // TransportManager. + bool readable() const { return readable_; } + bool writable() const { return writable_; } + sigslot::signal1 SignalReadableState; + sigslot::signal1 SignalWritableState; + // Emitted when the TransportChannel's ability to send has changed. + sigslot::signal1 SignalReadyToSend; + + // Attempts to send the given packet. The return value is < 0 on failure. + // TODO: Remove the default argument once channel code is updated. + virtual int SendPacket(const char* data, size_t len, int flags = 0) = 0; + + // Sets a socket option on this channel. Note that not all options are + // supported by all transport types. + virtual int SetOption(talk_base::Socket::Option opt, int value) = 0; + + // Returns the most recent error that occurred on this channel. + virtual int GetError() = 0; + + // Returns current transportchannel ICE role. + virtual TransportRole GetRole() const = 0; + + // Returns the current stats for this connection. + virtual bool GetStats(ConnectionInfos* infos) { + return false; + } + + // Is DTLS active? + virtual bool IsDtlsActive() const { + return false; + } + + // Set up the ciphers to use for DTLS-SRTP. + virtual bool SetSrtpCiphers(const std::vector& ciphers) { + return false; + } + + // Find out which DTLS-SRTP cipher was negotiated + virtual bool GetSrtpCipher(std::string* cipher) { + return false; + } + + // Allows key material to be extracted for external encryption. + virtual bool ExportKeyingMaterial(const std::string& label, + const uint8* context, + size_t context_len, + bool use_context, + uint8* result, + size_t result_len) { + return false; + } + + // Signalled each time a packet is received on this channel. + sigslot::signal4 SignalReadPacket; + + // This signal occurs when there is a change in the way that packets are + // being routed, i.e. to a different remote location. The candidate + // indicates where and how we are currently sending media. + sigslot::signal2 SignalRouteChange; + + // Invoked when the channel is being destroyed. + sigslot::signal1 SignalDestroyed; + + // Debugging description of this transport channel. + std::string ToString() const; + + protected: + // Sets the readable state, signaling if necessary. + void set_readable(bool readable); + + // Sets the writable state, signaling if necessary. + void set_writable(bool writable); + + + private: + std::string session_id_; + // Used mostly for debugging. + std::string content_name_; + int component_; + bool readable_; + bool writable_; + + DISALLOW_EVIL_CONSTRUCTORS(TransportChannel); +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_TRANSPORTCHANNEL_H_ diff --git a/talk/p2p/base/transportchannelimpl.h b/talk/p2p/base/transportchannelimpl.h new file mode 100644 index 000000000..f1b84cbaa --- /dev/null +++ b/talk/p2p/base/transportchannelimpl.h @@ -0,0 +1,120 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_TRANSPORTCHANNELIMPL_H_ +#define TALK_P2P_BASE_TRANSPORTCHANNELIMPL_H_ + +#include +#include "talk/p2p/base/transport.h" +#include "talk/p2p/base/transportchannel.h" + +namespace buzz { class XmlElement; } + +namespace cricket { + +class Candidate; + +// Base class for real implementations of TransportChannel. This includes some +// methods called only by Transport, which do not need to be exposed to the +// client. +class TransportChannelImpl : public TransportChannel { + public: + explicit TransportChannelImpl(const std::string& content_name, int component) + : TransportChannel(content_name, component) {} + + // Returns the transport that created this channel. + virtual Transport* GetTransport() = 0; + + // For ICE channels. + virtual void SetRole(TransportRole role) = 0; + virtual void SetTiebreaker(uint64 tiebreaker) = 0; + // To toggle G-ICE/ICE. + virtual void SetIceProtocolType(IceProtocolType type) = 0; + // SetIceCredentials only need to be implemented by the ICE + // transport channels. Non-ICE transport channels can just ignore. + // The ufrag and pwd should be set before the Connect() is called. + virtual void SetIceCredentials(const std::string& ice_ufrag, + const std::string& ice_pwd) = 0; + // SetRemoteIceCredentials only need to be implemented by the ICE + // transport channels. Non-ICE transport channels can just ignore. + virtual void SetRemoteIceCredentials(const std::string& ice_ufrag, + const std::string& ice_pwd) = 0; + + // SetRemoteIceMode must be implemented only by the ICE transport channels. + virtual void SetRemoteIceMode(IceMode mode) = 0; + + // Begins the process of attempting to make a connection to the other client. + virtual void Connect() = 0; + + // Resets this channel back to the initial state (i.e., not connecting). + virtual void Reset() = 0; + + // Allows an individual channel to request signaling and be notified when it + // is ready. This is useful if the individual named channels have need to + // send their own transport-info stanzas. + sigslot::signal1 SignalRequestSignaling; + virtual void OnSignalingReady() = 0; + + // Handles sending and receiving of candidates. The Transport + // receives the candidates and may forward them to the relevant + // channel. + // + // Note: Since candidates are delivered asynchronously to the + // channel, they cannot return an error if the message is invalid. + // It is assumed that the Transport will have checked validity + // before forwarding. + sigslot::signal2 SignalCandidateReady; + virtual void OnCandidate(const Candidate& candidate) = 0; + + // DTLS methods + // Set DTLS local identity. + virtual bool SetLocalIdentity(talk_base::SSLIdentity* identity) { + return false; + } + + // Set DTLS Remote fingerprint. Must be after local identity set. + virtual bool SetRemoteFingerprint(const std::string& digest_alg, + const uint8* digest, + size_t digest_len) { + return false; + } + + // TransportChannel is forwarding this signal from PortAllocatorSession. + sigslot::signal1 SignalCandidatesAllocationDone; + + // Invoked when there is conflict in the ICE role between local and remote + // agents. + sigslot::signal1 SignalRoleConflict; + + private: + DISALLOW_EVIL_CONSTRUCTORS(TransportChannelImpl); +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_TRANSPORTCHANNELIMPL_H_ diff --git a/talk/p2p/base/transportchannelproxy.cc b/talk/p2p/base/transportchannelproxy.cc new file mode 100644 index 000000000..fd8fe6a69 --- /dev/null +++ b/talk/p2p/base/transportchannelproxy.cc @@ -0,0 +1,223 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/base/transportchannelproxy.h" +#include "talk/base/common.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/transport.h" +#include "talk/p2p/base/transportchannelimpl.h" + +namespace cricket { + +enum { + MSG_UPDATESTATE, +}; + +TransportChannelProxy::TransportChannelProxy(const std::string& content_name, + const std::string& name, + int component) + : TransportChannel(content_name, component), + name_(name), + impl_(NULL) { + worker_thread_ = talk_base::Thread::Current(); +} + +TransportChannelProxy::~TransportChannelProxy() { + // Clearing any pending signal. + worker_thread_->Clear(this); + if (impl_) + impl_->GetTransport()->DestroyChannel(impl_->component()); +} + +void TransportChannelProxy::SetImplementation(TransportChannelImpl* impl) { + // TODO(juberti): Fix this to occur on the correct thread. + // ASSERT(talk_base::Thread::Current() == worker_thread_); + + // Destroy any existing impl_. + if (impl_) { + impl_->GetTransport()->DestroyChannel(impl_->component()); + } + + // Adopt the supplied impl, and connect to its signals. + impl_ = impl; + + if (impl_) { + impl_->SignalReadableState.connect( + this, &TransportChannelProxy::OnReadableState); + impl_->SignalWritableState.connect( + this, &TransportChannelProxy::OnWritableState); + impl_->SignalReadPacket.connect( + this, &TransportChannelProxy::OnReadPacket); + impl_->SignalReadyToSend.connect( + this, &TransportChannelProxy::OnReadyToSend); + impl_->SignalRouteChange.connect( + this, &TransportChannelProxy::OnRouteChange); + for (OptionList::iterator it = pending_options_.begin(); + it != pending_options_.end(); + ++it) { + impl_->SetOption(it->first, it->second); + } + + // Push down the SRTP ciphers, if any were set. + if (!pending_srtp_ciphers_.empty()) { + impl_->SetSrtpCiphers(pending_srtp_ciphers_); + } + pending_options_.clear(); + } + + // Post ourselves a message to see if we need to fire state callbacks. + worker_thread_->Post(this, MSG_UPDATESTATE); +} + +int TransportChannelProxy::SendPacket(const char* data, size_t len, int flags) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + // Fail if we don't have an impl yet. + if (!impl_) { + return -1; + } + return impl_->SendPacket(data, len, flags); +} + +int TransportChannelProxy::SetOption(talk_base::Socket::Option opt, int value) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + if (!impl_) { + pending_options_.push_back(OptionPair(opt, value)); + return 0; + } + return impl_->SetOption(opt, value); +} + +int TransportChannelProxy::GetError() { + ASSERT(talk_base::Thread::Current() == worker_thread_); + if (!impl_) { + return 0; + } + return impl_->GetError(); +} + +bool TransportChannelProxy::GetStats(ConnectionInfos* infos) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + if (!impl_) { + return false; + } + return impl_->GetStats(infos); +} + +bool TransportChannelProxy::IsDtlsActive() const { + ASSERT(talk_base::Thread::Current() == worker_thread_); + if (!impl_) { + return false; + } + return impl_->IsDtlsActive(); +} + +bool TransportChannelProxy::SetSrtpCiphers(const std::vector& + ciphers) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + pending_srtp_ciphers_ = ciphers; // Cache so we can send later, but always + // set so it stays consistent. + if (impl_) { + return impl_->SetSrtpCiphers(ciphers); + } + return true; +} + +bool TransportChannelProxy::GetSrtpCipher(std::string* cipher) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + if (!impl_) { + return false; + } + return impl_->GetSrtpCipher(cipher); +} + +bool TransportChannelProxy::ExportKeyingMaterial(const std::string& label, + const uint8* context, + size_t context_len, + bool use_context, + uint8* result, + size_t result_len) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + if (!impl_) { + return false; + } + return impl_->ExportKeyingMaterial(label, context, context_len, use_context, + result, result_len); +} + +TransportRole TransportChannelProxy::GetRole() const { + ASSERT(talk_base::Thread::Current() == worker_thread_); + if (!impl_) { + return ROLE_UNKNOWN; + } + return impl_->GetRole(); +} + +void TransportChannelProxy::OnReadableState(TransportChannel* channel) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + ASSERT(channel == impl_); + set_readable(impl_->readable()); + // Note: SignalReadableState fired by set_readable. +} + +void TransportChannelProxy::OnWritableState(TransportChannel* channel) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + ASSERT(channel == impl_); + set_writable(impl_->writable()); + // Note: SignalWritableState fired by set_readable. +} + +void TransportChannelProxy::OnReadPacket(TransportChannel* channel, + const char* data, size_t size, + int flags) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + ASSERT(channel == impl_); + SignalReadPacket(this, data, size, flags); +} + +void TransportChannelProxy::OnReadyToSend(TransportChannel* channel) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + ASSERT(channel == impl_); + SignalReadyToSend(this); +} + +void TransportChannelProxy::OnRouteChange(TransportChannel* channel, + const Candidate& candidate) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + ASSERT(channel == impl_); + SignalRouteChange(this, candidate); +} + +void TransportChannelProxy::OnMessage(talk_base::Message* msg) { + ASSERT(talk_base::Thread::Current() == worker_thread_); + if (msg->message_id == MSG_UPDATESTATE) { + // If impl_ is already readable or writable, push up those signals. + set_readable(impl_ ? impl_->readable() : false); + set_writable(impl_ ? impl_->writable() : false); + } +} + +} // namespace cricket diff --git a/talk/p2p/base/transportchannelproxy.h b/talk/p2p/base/transportchannelproxy.h new file mode 100644 index 000000000..03d95b447 --- /dev/null +++ b/talk/p2p/base/transportchannelproxy.h @@ -0,0 +1,106 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_TRANSPORTCHANNELPROXY_H_ +#define TALK_P2P_BASE_TRANSPORTCHANNELPROXY_H_ + +#include +#include +#include + +#include "talk/base/messagehandler.h" +#include "talk/p2p/base/transportchannel.h" + +namespace talk_base { +class Thread; +} + +namespace cricket { + +class TransportChannelImpl; + +// Proxies calls between the client and the transport channel implementation. +// This is needed because clients are allowed to create channels before the +// network negotiation is complete. Hence, we create a proxy up front, and +// when negotiation completes, connect the proxy to the implementaiton. +class TransportChannelProxy : public TransportChannel, + public talk_base::MessageHandler { + public: + TransportChannelProxy(const std::string& content_name, + const std::string& name, + int component); + virtual ~TransportChannelProxy(); + + const std::string& name() const { return name_; } + TransportChannelImpl* impl() { return impl_; } + + // Sets the implementation to which we will proxy. + void SetImplementation(TransportChannelImpl* impl); + + // Implementation of the TransportChannel interface. These simply forward to + // the implementation. + virtual int SendPacket(const char* data, size_t len, int flags); + virtual int SetOption(talk_base::Socket::Option opt, int value); + virtual int GetError(); + virtual TransportRole GetRole() const; + virtual bool GetStats(ConnectionInfos* infos); + virtual bool IsDtlsActive() const; + virtual bool SetSrtpCiphers(const std::vector& ciphers); + virtual bool GetSrtpCipher(std::string* cipher); + virtual bool ExportKeyingMaterial(const std::string& label, + const uint8* context, + size_t context_len, + bool use_context, + uint8* result, + size_t result_len); + + private: + // Catch signals from the implementation channel. These just forward to the + // client (after updating our state to match). + void OnReadableState(TransportChannel* channel); + void OnWritableState(TransportChannel* channel); + void OnReadPacket(TransportChannel* channel, const char* data, size_t size, + int flags); + void OnReadyToSend(TransportChannel* channel); + void OnRouteChange(TransportChannel* channel, const Candidate& candidate); + + void OnMessage(talk_base::Message* message); + + typedef std::pair OptionPair; + typedef std::vector OptionList; + std::string name_; + talk_base::Thread* worker_thread_; + TransportChannelImpl* impl_; + OptionList pending_options_; + std::vector pending_srtp_ciphers_; + + DISALLOW_EVIL_CONSTRUCTORS(TransportChannelProxy); +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_TRANSPORTCHANNELPROXY_H_ diff --git a/talk/p2p/base/transportdescription.h b/talk/p2p/base/transportdescription.h new file mode 100644 index 000000000..bbca29509 --- /dev/null +++ b/talk/p2p/base/transportdescription.h @@ -0,0 +1,150 @@ +/* + * libjingle + * Copyright 2012, The Libjingle Authors. + * + * 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. + */ + +#ifndef TALK_P2P_BASE_TRANSPORTDESCRIPTION_H_ +#define TALK_P2P_BASE_TRANSPORTDESCRIPTION_H_ + +#include +#include +#include + +#include "talk/base/scoped_ptr.h" +#include "talk/base/sslfingerprint.h" +#include "talk/p2p/base/candidate.h" +#include "talk/p2p/base/constants.h" + +namespace cricket { + +// SEC_ENABLED and SEC_REQUIRED should only be used if the session +// was negotiated over TLS, to protect the inline crypto material +// exchange. +// SEC_DISABLED: No crypto in outgoing offer, ignore any supplied crypto. +// SEC_ENABLED: Crypto in outgoing offer and answer (if supplied in offer). +// SEC_REQUIRED: Crypto in outgoing offer and answer. Fail any offer with absent +// or unsupported crypto. +enum SecurePolicy { + SEC_DISABLED, + SEC_ENABLED, + SEC_REQUIRED +}; + +// The transport protocol we've elected to use. +enum TransportProtocol { + ICEPROTO_GOOGLE, // Google version of ICE protocol. + ICEPROTO_HYBRID, // ICE, but can fall back to the Google version. + ICEPROTO_RFC5245 // Standard RFC 5245 version of ICE. +}; +// The old name for TransportProtocol. +// TODO(juberti): remove this. +typedef TransportProtocol IceProtocolType; + +// ICE RFC 5245 implementation type. +enum IceMode { + ICEMODE_FULL, // As defined in http://tools.ietf.org/html/rfc5245#section-4.1 + ICEMODE_LITE // As defined in http://tools.ietf.org/html/rfc5245#section-4.2 +}; + +typedef std::vector Candidates; + +struct TransportDescription { + TransportDescription() {} + + TransportDescription(const std::string& transport_type, + const std::vector& transport_options, + const std::string& ice_ufrag, + const std::string& ice_pwd, + IceMode ice_mode, + const talk_base::SSLFingerprint* identity_fingerprint, + const Candidates& candidates) + : transport_type(transport_type), + transport_options(transport_options), + ice_ufrag(ice_ufrag), + ice_pwd(ice_pwd), + ice_mode(ice_mode), + identity_fingerprint(CopyFingerprint(identity_fingerprint)), + candidates(candidates) {} + TransportDescription(const std::string& transport_type, + const Candidates& candidates) + : transport_type(transport_type), + ice_mode(ICEMODE_FULL), + candidates(candidates) {} + TransportDescription(const TransportDescription& from) + : transport_type(from.transport_type), + transport_options(from.transport_options), + ice_ufrag(from.ice_ufrag), + ice_pwd(from.ice_pwd), + ice_mode(from.ice_mode), + identity_fingerprint(CopyFingerprint(from.identity_fingerprint.get())), + candidates(from.candidates) {} + + TransportDescription& operator=(const TransportDescription& from) { + // Self-assignment + if (this == &from) + return *this; + + transport_type = from.transport_type; + transport_options = from.transport_options; + ice_ufrag = from.ice_ufrag; + ice_pwd = from.ice_pwd; + ice_mode = from.ice_mode; + + identity_fingerprint.reset(CopyFingerprint( + from.identity_fingerprint.get())); + candidates = from.candidates; + return *this; + } + + bool HasOption(const std::string& option) const { + return (std::find(transport_options.begin(), transport_options.end(), + option) != transport_options.end()); + } + void AddOption(const std::string& option) { + transport_options.push_back(option); + } + bool secure() { return identity_fingerprint != NULL; } + + static talk_base::SSLFingerprint* CopyFingerprint( + const talk_base::SSLFingerprint* from) { + if (!from) + return NULL; + + return new talk_base::SSLFingerprint(*from); + } + + std::string transport_type; // xmlns of + std::vector transport_options; + std::string ice_ufrag; + std::string ice_pwd; + IceMode ice_mode; + + talk_base::scoped_ptr identity_fingerprint; + Candidates candidates; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_TRANSPORTDESCRIPTION_H_ diff --git a/talk/p2p/base/transportdescriptionfactory.cc b/talk/p2p/base/transportdescriptionfactory.cc new file mode 100644 index 000000000..8fbfff144 --- /dev/null +++ b/talk/p2p/base/transportdescriptionfactory.cc @@ -0,0 +1,161 @@ +/* + * libjingle + * Copyright 2012 Google Inc. All rights reserved. + * + * 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/p2p/base/transportdescriptionfactory.h" + +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/messagedigest.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sslfingerprint.h" +#include "talk/p2p/base/transportdescription.h" + +namespace cricket { + +static TransportProtocol kDefaultProtocol = ICEPROTO_GOOGLE; +static const char* kDefaultDigestAlg = talk_base::DIGEST_SHA_1; + +TransportDescriptionFactory::TransportDescriptionFactory() + : protocol_(kDefaultProtocol), + secure_(SEC_DISABLED), + identity_(NULL), + digest_alg_(kDefaultDigestAlg) { +} + +TransportDescription* TransportDescriptionFactory::CreateOffer( + const TransportOptions& options, + const TransportDescription* current_description) const { + talk_base::scoped_ptr desc(new TransportDescription()); + + // Set the transport type depending on the selected protocol. + if (protocol_ == ICEPROTO_RFC5245) { + desc->transport_type = NS_JINGLE_ICE_UDP; + } else if (protocol_ == ICEPROTO_HYBRID) { + desc->transport_type = NS_JINGLE_ICE_UDP; + desc->AddOption(ICE_OPTION_GICE); + } else if (protocol_ == ICEPROTO_GOOGLE) { + desc->transport_type = NS_GINGLE_P2P; + } + + // Generate the ICE credentials if we don't already have them. + if (!current_description || options.ice_restart) { + desc->ice_ufrag = talk_base::CreateRandomString(ICE_UFRAG_LENGTH); + desc->ice_pwd = talk_base::CreateRandomString(ICE_PWD_LENGTH); + } else { + desc->ice_ufrag = current_description->ice_ufrag; + desc->ice_pwd = current_description->ice_pwd; + } + + // If we are trying to establish a secure transport, add a fingerprint. + if (secure_ == SEC_ENABLED || secure_ == SEC_REQUIRED) { + // Fail if we can't create the fingerprint. + if (!CreateIdentityDigest(desc.get())) { + return NULL; + } + } + return desc.release(); +} + +TransportDescription* TransportDescriptionFactory::CreateAnswer( + const TransportDescription* offer, + const TransportOptions& options, + const TransportDescription* current_description) const { + // A NULL offer is treated as a GICE transport description. + // TODO(juberti): Figure out why we get NULL offers, and fix this upstream. + talk_base::scoped_ptr desc(new TransportDescription()); + + // Figure out which ICE variant to negotiate; prefer RFC 5245 ICE, but fall + // back to G-ICE if needed. Note that we never create a hybrid answer, since + // we know what the other side can support already. + if (offer && offer->transport_type == NS_JINGLE_ICE_UDP && + (protocol_ == ICEPROTO_RFC5245 || protocol_ == ICEPROTO_HYBRID)) { + // Offer is ICE or hybrid, we support ICE or hybrid: use ICE. + desc->transport_type = NS_JINGLE_ICE_UDP; + } else if (offer && offer->transport_type == NS_JINGLE_ICE_UDP && + offer->HasOption(ICE_OPTION_GICE) && + protocol_ == ICEPROTO_GOOGLE) { + desc->transport_type = NS_GINGLE_P2P; + // Offer is hybrid, we support GICE: use GICE. + } else if ((!offer || offer->transport_type == NS_GINGLE_P2P) && + (protocol_ == ICEPROTO_HYBRID || protocol_ == ICEPROTO_GOOGLE)) { + // Offer is GICE, we support hybrid or GICE: use GICE. + desc->transport_type = NS_GINGLE_P2P; + } else { + // Mismatch. + LOG(LS_WARNING) << "Failed to create TransportDescription answer " + "because of incompatible transport types"; + return NULL; + } + + // Generate the ICE credentials if we don't already have them or ice is + // being restarted. + if (!current_description || options.ice_restart) { + desc->ice_ufrag = talk_base::CreateRandomString(ICE_UFRAG_LENGTH); + desc->ice_pwd = talk_base::CreateRandomString(ICE_PWD_LENGTH); + } else { + desc->ice_ufrag = current_description->ice_ufrag; + desc->ice_pwd = current_description->ice_pwd; + } + + // Negotiate security params. + if (offer && offer->identity_fingerprint.get()) { + // The offer supports DTLS, so answer with DTLS, as long as we support it. + if (secure_ == SEC_ENABLED || secure_ == SEC_REQUIRED) { + // Fail if we can't create the fingerprint. + if (!CreateIdentityDigest(desc.get())) { + return NULL; + } + } + } else if (secure_ == SEC_REQUIRED) { + // We require DTLS, but the other side didn't offer it. Fail. + LOG(LS_WARNING) << "Failed to create TransportDescription answer " + "because of incompatible security settings"; + return NULL; + } + + return desc.release(); +} + +bool TransportDescriptionFactory::CreateIdentityDigest( + TransportDescription* desc) const { + if (!identity_) { + LOG(LS_ERROR) << "Cannot create identity digest with no identity"; + return false; + } + + desc->identity_fingerprint.reset( + talk_base::SSLFingerprint::Create(digest_alg_, identity_)); + if (!desc->identity_fingerprint.get()) { + LOG(LS_ERROR) << "Failed to create identity digest, alg=" << digest_alg_; + return false; + } + + return true; +} + +} // namespace cricket + diff --git a/talk/p2p/base/transportdescriptionfactory.h b/talk/p2p/base/transportdescriptionfactory.h new file mode 100644 index 000000000..32836f3e8 --- /dev/null +++ b/talk/p2p/base/transportdescriptionfactory.h @@ -0,0 +1,84 @@ +/* + * libjingle + * Copyright 2012 Google Inc. All rights reserved. + * + * 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. + */ + +#ifndef TALK_P2P_BASE_TRANSPORTDESCRIPTIONFACTORY_H_ +#define TALK_P2P_BASE_TRANSPORTDESCRIPTIONFACTORY_H_ + +#include "talk/p2p/base/transportdescription.h" + +namespace talk_base { +class SSLIdentity; +} + +namespace cricket { + +struct TransportOptions { + TransportOptions() : ice_restart(false) {} + bool ice_restart; +}; + +// Creates transport descriptions according to the supplied configuration. +// When creating answers, performs the appropriate negotiation +// of the various fields to determine the proper result. +class TransportDescriptionFactory { + public: + // Default ctor; use methods below to set configuration. + TransportDescriptionFactory(); + SecurePolicy secure() const { return secure_; } + // The identity to use when setting up DTLS. + talk_base::SSLIdentity* identity() const { return identity_; } + + // Specifies the transport protocol to be use. + void set_protocol(TransportProtocol protocol) { protocol_ = protocol; } + // Specifies the transport security policy to use. + void set_secure(SecurePolicy s) { secure_ = s; } + // Specifies the identity to use (only used when secure is not SEC_DISABLED). + void set_identity(talk_base::SSLIdentity* identity) { identity_ = identity; } + // Specifies the algorithm to use when creating an identity digest. + void set_digest_algorithm(const std::string& alg) { digest_alg_ = alg; } + + // Creates a transport description suitable for use in an offer. + TransportDescription* CreateOffer(const TransportOptions& options, + const TransportDescription* current_description) const; + // Create a transport description that is a response to an offer. + TransportDescription* CreateAnswer( + const TransportDescription* offer, + const TransportOptions& options, + const TransportDescription* current_description) const; + + private: + bool CreateIdentityDigest(TransportDescription* description) const; + + TransportProtocol protocol_; + SecurePolicy secure_; + talk_base::SSLIdentity* identity_; + std::string digest_alg_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_TRANSPORTDESCRIPTIONFACTORY_H_ diff --git a/talk/p2p/base/transportdescriptionfactory_unittest.cc b/talk/p2p/base/transportdescriptionfactory_unittest.cc new file mode 100644 index 000000000..c0c88297f --- /dev/null +++ b/talk/p2p/base/transportdescriptionfactory_unittest.cc @@ -0,0 +1,388 @@ +/* + * libjingle + * Copyright 2012, 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 +#include + +#include "talk/base/fakesslidentity.h" +#include "talk/base/gunit.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/transportdescription.h" +#include "talk/p2p/base/transportdescriptionfactory.h" + +using talk_base::scoped_ptr; +using cricket::TransportDescriptionFactory; +using cricket::TransportDescription; +using cricket::TransportOptions; + +// TODO(juberti): Change this to SHA-256 once we have Win32 using OpenSSL. +static const char* kDefaultDigestAlg = talk_base::DIGEST_SHA_1; + +class TransportDescriptionFactoryTest : public testing::Test { + public: + TransportDescriptionFactoryTest() + : id1_(new talk_base::FakeSSLIdentity("User1")), + id2_(new talk_base::FakeSSLIdentity("User2")) { + f1_.set_digest_algorithm(kDefaultDigestAlg); + f2_.set_digest_algorithm(kDefaultDigestAlg); + } + void CheckDesc(const TransportDescription* desc, const std::string& type, + const std::string& opt, const std::string& ice_ufrag, + const std::string& ice_pwd, const std::string& dtls_alg) { + ASSERT_TRUE(desc != NULL); + EXPECT_EQ(type, desc->transport_type); + EXPECT_EQ(!opt.empty(), desc->HasOption(opt)); + if (ice_ufrag.empty() && ice_pwd.empty()) { + EXPECT_EQ(static_cast(cricket::ICE_UFRAG_LENGTH), + desc->ice_ufrag.size()); + EXPECT_EQ(static_cast(cricket::ICE_PWD_LENGTH), + desc->ice_pwd.size()); + } else { + EXPECT_EQ(ice_ufrag, desc->ice_ufrag); + EXPECT_EQ(ice_pwd, desc->ice_pwd); + } + if (dtls_alg.empty()) { + EXPECT_TRUE(desc->identity_fingerprint.get() == NULL); + } else { + ASSERT_TRUE(desc->identity_fingerprint.get() != NULL); + EXPECT_EQ(desc->identity_fingerprint->algorithm, dtls_alg); + EXPECT_GT(desc->identity_fingerprint->digest.length(), 0U); + } + } + + // This test ice restart by doing two offer answer exchanges. On the second + // exchange ice is restarted. The test verifies that the ufrag and password + // in the offer and answer is changed. + // If |dtls| is true, the test verifies that the finger print is not changed. + void TestIceRestart(bool dtls) { + if (dtls) { + f1_.set_secure(cricket::SEC_ENABLED); + f2_.set_secure(cricket::SEC_ENABLED); + f1_.set_identity(id1_.get()); + f2_.set_identity(id2_.get()); + } else { + f1_.set_secure(cricket::SEC_DISABLED); + f2_.set_secure(cricket::SEC_DISABLED); + } + + cricket::TransportOptions options; + // The initial offer / answer exchange. + talk_base::scoped_ptr offer(f1_.CreateOffer( + options, NULL)); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), + options, NULL)); + + // Create an updated offer where we restart ice. + options.ice_restart = true; + talk_base::scoped_ptr restart_offer(f1_.CreateOffer( + options, offer.get())); + + VerifyUfragAndPasswordChanged(dtls, offer.get(), restart_offer.get()); + + // Create a new answer. The transport ufrag and password is changed since + // |options.ice_restart == true| + talk_base::scoped_ptr restart_answer( + f2_.CreateAnswer(restart_offer.get(), options, answer.get())); + ASSERT_TRUE(restart_answer.get() != NULL); + + VerifyUfragAndPasswordChanged(dtls, answer.get(), restart_answer.get()); + } + + void VerifyUfragAndPasswordChanged(bool dtls, + const TransportDescription* org_desc, + const TransportDescription* restart_desc) { + EXPECT_NE(org_desc->ice_pwd, restart_desc->ice_pwd); + EXPECT_NE(org_desc->ice_ufrag, restart_desc->ice_ufrag); + EXPECT_EQ(static_cast(cricket::ICE_UFRAG_LENGTH), + restart_desc->ice_ufrag.size()); + EXPECT_EQ(static_cast(cricket::ICE_PWD_LENGTH), + restart_desc->ice_pwd.size()); + // If DTLS is enabled, make sure the finger print is unchanged. + if (dtls) { + EXPECT_FALSE( + org_desc->identity_fingerprint->GetRfc4572Fingerprint().empty()); + EXPECT_EQ(org_desc->identity_fingerprint->GetRfc4572Fingerprint(), + restart_desc->identity_fingerprint->GetRfc4572Fingerprint()); + } + } + + protected: + TransportDescriptionFactory f1_; + TransportDescriptionFactory f2_; + scoped_ptr id1_; + scoped_ptr id2_; +}; + +// Test that in the default case, we generate the expected G-ICE offer. +TEST_F(TransportDescriptionFactoryTest, TestOfferGice) { + f1_.set_protocol(cricket::ICEPROTO_GOOGLE); + scoped_ptr desc(f1_.CreateOffer( + TransportOptions(), NULL)); + CheckDesc(desc.get(), cricket::NS_GINGLE_P2P, "", "", "", ""); +} + +// Test generating a hybrid offer. +TEST_F(TransportDescriptionFactoryTest, TestOfferHybrid) { + f1_.set_protocol(cricket::ICEPROTO_HYBRID); + scoped_ptr desc(f1_.CreateOffer( + TransportOptions(), NULL)); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "google-ice", "", "", ""); +} + +// Test generating an ICE-only offer. +TEST_F(TransportDescriptionFactoryTest, TestOfferIce) { + f1_.set_protocol(cricket::ICEPROTO_RFC5245); + scoped_ptr desc(f1_.CreateOffer( + TransportOptions(), NULL)); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "", "", "", ""); +} + +// Test generating a hybrid offer with DTLS. +TEST_F(TransportDescriptionFactoryTest, TestOfferHybridDtls) { + f1_.set_protocol(cricket::ICEPROTO_HYBRID); + f1_.set_secure(cricket::SEC_ENABLED); + f1_.set_identity(id1_.get()); + scoped_ptr desc(f1_.CreateOffer( + TransportOptions(), NULL)); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "google-ice", "", "", + kDefaultDigestAlg); + // Ensure it also works with SEC_REQUIRED. + f1_.set_secure(cricket::SEC_REQUIRED); + desc.reset(f1_.CreateOffer(TransportOptions(), NULL)); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "google-ice", "", "", + kDefaultDigestAlg); +} + +// Test generating a hybrid offer with DTLS fails with no identity. +TEST_F(TransportDescriptionFactoryTest, TestOfferHybridDtlsWithNoIdentity) { + f1_.set_protocol(cricket::ICEPROTO_HYBRID); + f1_.set_secure(cricket::SEC_ENABLED); + scoped_ptr desc(f1_.CreateOffer( + TransportOptions(), NULL)); + ASSERT_TRUE(desc.get() == NULL); +} + +// Test generating a hybrid offer with DTLS fails with an unsupported digest. +TEST_F(TransportDescriptionFactoryTest, TestOfferHybridDtlsWithBadDigestAlg) { + f1_.set_protocol(cricket::ICEPROTO_HYBRID); + f1_.set_secure(cricket::SEC_ENABLED); + f1_.set_identity(id1_.get()); + f1_.set_digest_algorithm("bogus"); + scoped_ptr desc(f1_.CreateOffer( + TransportOptions(), NULL)); + ASSERT_TRUE(desc.get() == NULL); +} + +// Test updating a hybrid offer with DTLS to pick ICE. +// The ICE credentials should stay the same in the new offer. +TEST_F(TransportDescriptionFactoryTest, TestOfferHybridDtlsReofferIceDtls) { + f1_.set_protocol(cricket::ICEPROTO_HYBRID); + f1_.set_secure(cricket::SEC_ENABLED); + f1_.set_identity(id1_.get()); + scoped_ptr old_desc(f1_.CreateOffer( + TransportOptions(), NULL)); + ASSERT_TRUE(old_desc.get() != NULL); + f1_.set_protocol(cricket::ICEPROTO_RFC5245); + scoped_ptr desc( + f1_.CreateOffer(TransportOptions(), old_desc.get())); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "", + old_desc->ice_ufrag, old_desc->ice_pwd, kDefaultDigestAlg); +} + +// Test that we can answer a GICE offer with GICE. +TEST_F(TransportDescriptionFactoryTest, TestAnswerGiceToGice) { + f1_.set_protocol(cricket::ICEPROTO_GOOGLE); + f2_.set_protocol(cricket::ICEPROTO_GOOGLE); + scoped_ptr offer(f1_.CreateOffer( + TransportOptions(), NULL)); + ASSERT_TRUE(offer.get() != NULL); + scoped_ptr desc(f2_.CreateAnswer( + offer.get(), TransportOptions(), NULL)); + CheckDesc(desc.get(), cricket::NS_GINGLE_P2P, "", "", "", ""); + // Should get the same result when answering as hybrid. + f2_.set_protocol(cricket::ICEPROTO_HYBRID); + desc.reset(f2_.CreateAnswer(offer.get(), TransportOptions(), + NULL)); + CheckDesc(desc.get(), cricket::NS_GINGLE_P2P, "", "", "", ""); +} + +// Test that we can answer a hybrid offer with GICE. +TEST_F(TransportDescriptionFactoryTest, TestAnswerGiceToHybrid) { + f1_.set_protocol(cricket::ICEPROTO_HYBRID); + f2_.set_protocol(cricket::ICEPROTO_GOOGLE); + scoped_ptr offer(f1_.CreateOffer( + TransportOptions(), NULL)); + ASSERT_TRUE(offer.get() != NULL); + scoped_ptr desc( + f2_.CreateAnswer(offer.get(), TransportOptions(), NULL)); + CheckDesc(desc.get(), cricket::NS_GINGLE_P2P, "", "", "", ""); +} + +// Test that we can answer a hybrid offer with ICE. +TEST_F(TransportDescriptionFactoryTest, TestAnswerIceToHybrid) { + f1_.set_protocol(cricket::ICEPROTO_HYBRID); + f2_.set_protocol(cricket::ICEPROTO_RFC5245); + scoped_ptr offer(f1_.CreateOffer( + TransportOptions(), NULL)); + ASSERT_TRUE(offer.get() != NULL); + scoped_ptr desc( + f2_.CreateAnswer(offer.get(), TransportOptions(), NULL)); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "", "", "", ""); + // Should get the same result when answering as hybrid. + f2_.set_protocol(cricket::ICEPROTO_HYBRID); + desc.reset(f2_.CreateAnswer(offer.get(), TransportOptions(), + NULL)); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "", "", "", ""); +} + +// Test that we can answer an ICE offer with ICE. +TEST_F(TransportDescriptionFactoryTest, TestAnswerIceToIce) { + f1_.set_protocol(cricket::ICEPROTO_RFC5245); + f2_.set_protocol(cricket::ICEPROTO_RFC5245); + scoped_ptr offer(f1_.CreateOffer( + TransportOptions(), NULL)); + ASSERT_TRUE(offer.get() != NULL); + scoped_ptr desc(f2_.CreateAnswer( + offer.get(), TransportOptions(), NULL)); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "", "", "", ""); + // Should get the same result when answering as hybrid. + f2_.set_protocol(cricket::ICEPROTO_HYBRID); + desc.reset(f2_.CreateAnswer(offer.get(), TransportOptions(), + NULL)); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "", "", "", ""); +} + +// Test that we can't answer a GICE offer with ICE. +TEST_F(TransportDescriptionFactoryTest, TestAnswerIceToGice) { + f1_.set_protocol(cricket::ICEPROTO_GOOGLE); + f2_.set_protocol(cricket::ICEPROTO_RFC5245); + scoped_ptr offer( + f1_.CreateOffer(TransportOptions(), NULL)); + ASSERT_TRUE(offer.get() != NULL); + scoped_ptr desc( + f2_.CreateAnswer(offer.get(), TransportOptions(), NULL)); + ASSERT_TRUE(desc.get() == NULL); +} + +// Test that we can't answer an ICE offer with GICE. +TEST_F(TransportDescriptionFactoryTest, TestAnswerGiceToIce) { + f1_.set_protocol(cricket::ICEPROTO_RFC5245); + f2_.set_protocol(cricket::ICEPROTO_GOOGLE); + scoped_ptr offer( + f1_.CreateOffer(TransportOptions(), NULL)); + ASSERT_TRUE(offer.get() != NULL); + scoped_ptr desc(f2_.CreateAnswer( + offer.get(), TransportOptions(), NULL)); + ASSERT_TRUE(desc.get() == NULL); +} + +// Test that we can update an answer properly; ICE credentials shouldn't change. +TEST_F(TransportDescriptionFactoryTest, TestAnswerIceToIceReanswer) { + f1_.set_protocol(cricket::ICEPROTO_RFC5245); + f2_.set_protocol(cricket::ICEPROTO_RFC5245); + scoped_ptr offer( + f1_.CreateOffer(TransportOptions(), NULL)); + ASSERT_TRUE(offer.get() != NULL); + scoped_ptr old_desc( + f2_.CreateAnswer(offer.get(), TransportOptions(), NULL)); + ASSERT_TRUE(old_desc.get() != NULL); + scoped_ptr desc( + f2_.CreateAnswer(offer.get(), TransportOptions(), + old_desc.get())); + ASSERT_TRUE(desc.get() != NULL); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "", + old_desc->ice_ufrag, old_desc->ice_pwd, ""); +} + +// Test that we handle answering an offer with DTLS with no DTLS. +TEST_F(TransportDescriptionFactoryTest, TestAnswerHybridToHybridDtls) { + f1_.set_protocol(cricket::ICEPROTO_HYBRID); + f1_.set_secure(cricket::SEC_ENABLED); + f1_.set_identity(id1_.get()); + f2_.set_protocol(cricket::ICEPROTO_HYBRID); + scoped_ptr offer( + f1_.CreateOffer(TransportOptions(), NULL)); + ASSERT_TRUE(offer.get() != NULL); + scoped_ptr desc( + f2_.CreateAnswer(offer.get(), TransportOptions(), NULL)); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "", "", "", ""); +} + +// Test that we handle answering an offer without DTLS if we have DTLS enabled, +// but fail if we require DTLS. +TEST_F(TransportDescriptionFactoryTest, TestAnswerHybridDtlsToHybrid) { + f1_.set_protocol(cricket::ICEPROTO_HYBRID); + f2_.set_protocol(cricket::ICEPROTO_HYBRID); + f2_.set_secure(cricket::SEC_ENABLED); + f2_.set_identity(id2_.get()); + scoped_ptr offer( + f1_.CreateOffer(TransportOptions(), NULL)); + ASSERT_TRUE(offer.get() != NULL); + scoped_ptr desc( + f2_.CreateAnswer(offer.get(), TransportOptions(), NULL)); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "", "", "", ""); + f2_.set_secure(cricket::SEC_REQUIRED); + desc.reset(f2_.CreateAnswer(offer.get(), TransportOptions(), + NULL)); + ASSERT_TRUE(desc.get() == NULL); +} + +// Test that we handle answering an DTLS offer with DTLS, both if we have +// DTLS enabled and required. +TEST_F(TransportDescriptionFactoryTest, TestAnswerHybridDtlsToHybridDtls) { + f1_.set_protocol(cricket::ICEPROTO_HYBRID); + f1_.set_secure(cricket::SEC_ENABLED); + f1_.set_identity(id1_.get()); + f2_.set_protocol(cricket::ICEPROTO_HYBRID); + f2_.set_secure(cricket::SEC_ENABLED); + f2_.set_identity(id2_.get()); + scoped_ptr offer( + f1_.CreateOffer(TransportOptions(), NULL)); + ASSERT_TRUE(offer.get() != NULL); + scoped_ptr desc( + f2_.CreateAnswer(offer.get(), TransportOptions(), NULL)); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "", "", "", + kDefaultDigestAlg); + f2_.set_secure(cricket::SEC_REQUIRED); + desc.reset(f2_.CreateAnswer(offer.get(), TransportOptions(), + NULL)); + CheckDesc(desc.get(), cricket::NS_JINGLE_ICE_UDP, "", "", "", + kDefaultDigestAlg); +} + +// Test that ice ufrag and password is changed in an updated offer and answer +// if |TransportDescriptionOptions::ice_restart| is true. +TEST_F(TransportDescriptionFactoryTest, TestIceRestart) { + TestIceRestart(false); +} + +// Test that ice ufrag and password is changed in an updated offer and answer +// if |TransportDescriptionOptions::ice_restart| is true and DTLS is enabled. +TEST_F(TransportDescriptionFactoryTest, TestIceRestartWithDtls) { + TestIceRestart(true); +} diff --git a/talk/p2p/base/transportinfo.h b/talk/p2p/base/transportinfo.h new file mode 100644 index 000000000..ad8b6a29b --- /dev/null +++ b/talk/p2p/base/transportinfo.h @@ -0,0 +1,60 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_P2P_BASE_TRANSPORTINFO_H_ +#define TALK_P2P_BASE_TRANSPORTINFO_H_ + +#include +#include + +#include "talk/base/helpers.h" +#include "talk/p2p/base/candidate.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/transportdescription.h" + +namespace cricket { + +// A TransportInfo is NOT a transport-info message. It is comparable +// to a "ContentInfo". A transport-infos message is basically just a +// collection of TransportInfos. +struct TransportInfo { + TransportInfo() {} + + TransportInfo(const std::string& content_name, + const TransportDescription& description) + : content_name(content_name), + description(description) {} + + std::string content_name; + TransportDescription description; +}; + +typedef std::vector TransportInfos; + +} // namespace cricket + +#endif // TALK_P2P_BASE_TRANSPORTINFO_H_ diff --git a/talk/p2p/base/turnport.cc b/talk/p2p/base/turnport.cc new file mode 100644 index 000000000..9fad27464 --- /dev/null +++ b/talk/p2p/base/turnport.cc @@ -0,0 +1,953 @@ +/* + * libjingle + * Copyright 2012, 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/p2p/base/turnport.h" + +#include + +#include "talk/base/asyncpacketsocket.h" +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/nethelpers.h" +#include "talk/base/socketaddress.h" +#include "talk/base/stringencode.h" +#include "talk/p2p/base/common.h" +#include "talk/p2p/base/stun.h" + +namespace cricket { + +// TODO(juberti): Move to stun.h when relay messages have been renamed. +static const int TURN_ALLOCATE_REQUEST = STUN_ALLOCATE_REQUEST; +static const int TURN_ALLOCATE_ERROR_RESPONSE = STUN_ALLOCATE_ERROR_RESPONSE; + +// TODO(juberti): Extract to turnmessage.h +static const int TURN_DEFAULT_PORT = 3478; +static const int TURN_CHANNEL_NUMBER_START = 0x4000; +static const int TURN_PERMISSION_TIMEOUT = 5 * 60 * 1000; // 5 minutes + +static const size_t TURN_CHANNEL_HEADER_SIZE = 4U; + +enum { + MSG_PORT_ERROR = 1 +}; + +inline bool IsTurnChannelData(uint16 msg_type) { + return ((msg_type & 0xC000) == 0x4000); // MSB are 0b01 +} + +static int GetRelayPreference(cricket::ProtocolType proto) { + int relay_preference = ICE_TYPE_PREFERENCE_RELAY; + if (proto == cricket::PROTO_TCP) + relay_preference -= 1; + else if (proto == cricket::PROTO_SSLTCP) + relay_preference -= 2; + + ASSERT(relay_preference >= 0); + return relay_preference; +} + +class TurnAllocateRequest : public StunRequest { + public: + explicit TurnAllocateRequest(TurnPort* port); + virtual void Prepare(StunMessage* request); + virtual void OnResponse(StunMessage* response); + virtual void OnErrorResponse(StunMessage* response); + virtual void OnTimeout(); + + private: + // Handles authentication challenge from the server. + void OnAuthChallenge(StunMessage* response, int code); + void OnUnknownAttribute(StunMessage* response); + + TurnPort* port_; +}; + +class TurnRefreshRequest : public StunRequest { + public: + explicit TurnRefreshRequest(TurnPort* port); + virtual void Prepare(StunMessage* request); + virtual void OnResponse(StunMessage* response); + virtual void OnErrorResponse(StunMessage* response); + virtual void OnTimeout(); + + private: + TurnPort* port_; +}; + +class TurnCreatePermissionRequest : public StunRequest, + public sigslot::has_slots<> { + public: + TurnCreatePermissionRequest(TurnPort* port, TurnEntry* entry, + const talk_base::SocketAddress& ext_addr); + virtual void Prepare(StunMessage* request); + virtual void OnResponse(StunMessage* response); + virtual void OnErrorResponse(StunMessage* response); + virtual void OnTimeout(); + + private: + void OnEntryDestroyed(TurnEntry* entry); + + TurnPort* port_; + TurnEntry* entry_; + talk_base::SocketAddress ext_addr_; +}; + +class TurnChannelBindRequest : public StunRequest, + public sigslot::has_slots<> { + public: + TurnChannelBindRequest(TurnPort* port, TurnEntry* entry, int channel_id, + const talk_base::SocketAddress& ext_addr); + virtual void Prepare(StunMessage* request); + virtual void OnResponse(StunMessage* response); + virtual void OnErrorResponse(StunMessage* response); + virtual void OnTimeout(); + + private: + void OnEntryDestroyed(TurnEntry* entry); + + TurnPort* port_; + TurnEntry* entry_; + int channel_id_; + talk_base::SocketAddress ext_addr_; +}; + +// Manages a "connection" to a remote destination. We will attempt to bring up +// a channel for this remote destination to reduce the overhead of sending data. +class TurnEntry : public sigslot::has_slots<> { + public: + enum BindState { STATE_UNBOUND, STATE_BINDING, STATE_BOUND }; + TurnEntry(TurnPort* port, int channel_id, + const talk_base::SocketAddress& ext_addr); + + TurnPort* port() { return port_; } + + int channel_id() const { return channel_id_; } + const talk_base::SocketAddress& address() const { return ext_addr_; } + BindState state() const { return state_; } + + // Helper methods to send permission and channel bind requests. + void SendCreatePermissionRequest(); + void SendChannelBindRequest(int delay); + // Sends a packet to the given destination address. + // This will wrap the packet in STUN if necessary. + int Send(const void* data, size_t size, bool payload); + + void OnCreatePermissionSuccess(); + void OnCreatePermissionError(StunMessage* response, int code); + void OnChannelBindSuccess(); + void OnChannelBindError(StunMessage* response, int code); + // Signal sent when TurnEntry is destroyed. + sigslot::signal1 SignalDestroyed; + + private: + TurnPort* port_; + int channel_id_; + talk_base::SocketAddress ext_addr_; + BindState state_; +}; + +TurnPort::TurnPort(talk_base::Thread* thread, + talk_base::PacketSocketFactory* factory, + talk_base::Network* network, + const talk_base::IPAddress& ip, + int min_port, int max_port, + const std::string& username, + const std::string& password, + const ProtocolAddress& server_address, + const RelayCredentials& credentials) + : Port(thread, RELAY_PORT_TYPE, factory, network, ip, min_port, max_port, + username, password), + server_address_(server_address), + credentials_(credentials), + resolver_(NULL), + error_(0), + request_manager_(thread), + next_channel_number_(TURN_CHANNEL_NUMBER_START), + connected_(false) { + request_manager_.SignalSendPacket.connect(this, &TurnPort::OnSendStunPacket); +} + +TurnPort::~TurnPort() { + // TODO(juberti): Should this even be necessary? + while (!entries_.empty()) { + DestroyEntry(entries_.front()->address()); + } +} + +void TurnPort::PrepareAddress() { + if (credentials_.username.empty() || + credentials_.password.empty()) { + LOG(LS_ERROR) << "Allocation can't be started without setting the" + << " TURN server credentials for the user."; + OnAllocateError(); + return; + } + + if (!server_address_.address.port()) { + // We will set default TURN port, if no port is set in the address. + server_address_.address.SetPort(TURN_DEFAULT_PORT); + } + + if (server_address_.address.IsUnresolved()) { + ResolveTurnAddress(server_address_.address); + } else { + LOG_J(LS_INFO, this) << "Trying to connect to TURN server via " + << ProtoToString(server_address_.proto) << " @ " + << server_address_.address.ToSensitiveString(); + if (server_address_.proto == PROTO_UDP) { + socket_.reset(socket_factory()->CreateUdpSocket( + talk_base::SocketAddress(ip(), 0), min_port(), max_port())); + } else if (server_address_.proto == PROTO_TCP) { + socket_.reset(socket_factory()->CreateClientTcpSocket( + talk_base::SocketAddress(ip(), 0), server_address_.address, + proxy(), user_agent(), talk_base::PacketSocketFactory::OPT_STUN)); + } + + if (!socket_) { + OnAllocateError(); + return; + } + + // Apply options if any. + for (SocketOptionsMap::iterator iter = socket_options_.begin(); + iter != socket_options_.end(); ++iter) { + socket_->SetOption(iter->first, iter->second); + } + + socket_->SignalReadPacket.connect(this, &TurnPort::OnReadPacket); + socket_->SignalReadyToSend.connect(this, &TurnPort::OnReadyToSend); + + if (server_address_.proto == PROTO_TCP) { + socket_->SignalConnect.connect(this, &TurnPort::OnSocketConnect); + socket_->SignalClose.connect(this, &TurnPort::OnSocketClose); + } else { + // If its UDP, send AllocateRequest now. + // For TCP and TLS AllcateRequest will be sent by OnSocketConnect. + SendRequest(new TurnAllocateRequest(this), 0); + } + } +} + +void TurnPort::OnSocketConnect(talk_base::AsyncPacketSocket* socket) { + LOG(LS_INFO) << "TurnPort connected to " << socket->GetRemoteAddress() + << " using tcp."; + SendRequest(new TurnAllocateRequest(this), 0); +} + +void TurnPort::OnSocketClose(talk_base::AsyncPacketSocket* socket, int error) { + LOG_J(LS_WARNING, this) << "Connection with server failed, error=" << error; + if (!connected_) { + OnAllocateError(); + } +} + +Connection* TurnPort::CreateConnection(const Candidate& address, + CandidateOrigin origin) { + // TURN-UDP can only connect to UDP candidates. + if (address.protocol() != UDP_PROTOCOL_NAME) { + return NULL; + } + + if (!IsCompatibleAddress(address.address())) { + return NULL; + } + + // Create an entry, if needed, so we can get our permissions set up correctly. + CreateEntry(address.address()); + + // TODO(juberti): The '0' index will need to change if we start gathering STUN + // candidates on this port. + ProxyConnection* conn = new ProxyConnection(this, 0, address); + conn->SignalDestroyed.connect(this, &TurnPort::OnConnectionDestroyed); + AddConnection(conn); + return conn; +} + +int TurnPort::SetOption(talk_base::Socket::Option opt, int value) { + if (!socket_) { + // If socket is not created yet, these options will be applied during socket + // creation. + socket_options_[opt] = value; + return 0; + } + return socket_->SetOption(opt, value); +} + +int TurnPort::GetOption(talk_base::Socket::Option opt, int* value) { + if (!socket_) + return -1; + + return socket_->GetOption(opt, value); +} + +int TurnPort::GetError() { + return error_; +} + +int TurnPort::SendTo(const void* data, size_t size, + const talk_base::SocketAddress& addr, + bool payload) { + // Try to find an entry for this specific address; we should have one. + TurnEntry* entry = FindEntry(addr); + ASSERT(entry != NULL); + if (!entry) { + return 0; + } + + if (!connected()) { + error_ = EWOULDBLOCK; + return SOCKET_ERROR; + } + + // Send the actual contents to the server using the usual mechanism. + int sent = entry->Send(data, size, payload); + if (sent <= 0) { + return SOCKET_ERROR; + } + + // The caller of the function is expecting the number of user data bytes, + // rather than the size of the packet. + return size; +} + +void TurnPort::OnReadPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& remote_addr) { + ASSERT(socket == socket_.get()); + ASSERT(remote_addr == server_address_.address); + + // The message must be at least the size of a channel header. + if (size < TURN_CHANNEL_HEADER_SIZE) { + LOG_J(LS_WARNING, this) << "Received TURN message that was too short"; + return; + } + + // Check the message type, to see if is a Channel Data message. + // The message will either be channel data, a TURN data indication, or + // a response to a previous request. + uint16 msg_type = talk_base::GetBE16(data); + if (IsTurnChannelData(msg_type)) { + HandleChannelData(msg_type, data, size); + } else if (msg_type == TURN_DATA_INDICATION) { + HandleDataIndication(data, size); + } else { + // This must be a response for one of our requests. + // Check success responses, but not errors, for MESSAGE-INTEGRITY. + if (IsStunSuccessResponseType(msg_type) && + !StunMessage::ValidateMessageIntegrity(data, size, hash())) { + LOG_J(LS_WARNING, this) << "Received TURN message with invalid " + << "message integrity, msg_type=" << msg_type; + return; + } + request_manager_.CheckResponse(data, size); + } +} + +void TurnPort::OnReadyToSend(talk_base::AsyncPacketSocket* socket) { + if (connected_) { + Port::OnReadyToSend(); + } +} + +void TurnPort::ResolveTurnAddress(const talk_base::SocketAddress& address) { + if (resolver_) + return; + + resolver_ = new talk_base::AsyncResolver(); + resolver_->SignalWorkDone.connect(this, &TurnPort::OnResolveResult); + resolver_->set_address(address); + resolver_->Start(); +} + +void TurnPort::OnResolveResult(talk_base::SignalThread* signal_thread) { + ASSERT(signal_thread == resolver_); + if (resolver_->error() != 0) { + LOG_J(LS_WARNING, this) << "TURN host lookup received error " + << resolver_->error(); + OnAllocateError(); + return; + } + + server_address_.address = resolver_->address(); + PrepareAddress(); +} + +void TurnPort::OnSendStunPacket(const void* data, size_t size, + StunRequest* request) { + if (Send(data, size) < 0) { + LOG_J(LS_ERROR, this) << "Failed to send TURN message, err=" + << socket_->GetError(); + } +} + +void TurnPort::OnStunAddress(const talk_base::SocketAddress& address) { + // For relay, mapped address is rel-addr. + set_related_address(address); +} + +void TurnPort::OnAllocateSuccess(const talk_base::SocketAddress& address) { + connected_ = true; + AddAddress(address, socket_->GetLocalAddress(), "udp", + RELAY_PORT_TYPE, GetRelayPreference(server_address_.proto), true); +} + +void TurnPort::OnAllocateError() { + // We will send SignalPortError asynchronously as this can be sent during + // port initialization. This way it will not be blocking other port + // creation. + thread()->Post(this, MSG_PORT_ERROR); +} + +void TurnPort::OnMessage(talk_base::Message* message) { + if (message->message_id == MSG_PORT_ERROR) { + SignalPortError(this); + } else { + Port::OnMessage(message); + } +} + +void TurnPort::OnAllocateRequestTimeout() { + OnAllocateError(); +} + +void TurnPort::HandleDataIndication(const char* data, size_t size) { + // Read in the message, and process according to RFC5766, Section 10.4. + talk_base::ByteBuffer buf(data, size); + TurnMessage msg; + if (!msg.Read(&buf)) { + LOG_J(LS_WARNING, this) << "Received invalid TURN data indication"; + return; + } + + // Check mandatory attributes. + const StunAddressAttribute* addr_attr = + msg.GetAddress(STUN_ATTR_XOR_PEER_ADDRESS); + if (!addr_attr) { + LOG_J(LS_WARNING, this) << "Missing STUN_ATTR_XOR_PEER_ADDRESS attribute " + << "in data indication."; + return; + } + + const StunByteStringAttribute* data_attr = + msg.GetByteString(STUN_ATTR_DATA); + if (!data_attr) { + LOG_J(LS_WARNING, this) << "Missing STUN_ATTR_DATA attribute in " + << "data indication."; + return; + } + + // Verify that the data came from somewhere we think we have a permission for. + talk_base::SocketAddress ext_addr(addr_attr->GetAddress()); + if (!HasPermission(ext_addr.ipaddr())) { + LOG_J(LS_WARNING, this) << "Received TURN data indication with invalid " + << "peer address, addr=" + << ext_addr.ToSensitiveString(); + return; + } + + DispatchPacket(data_attr->bytes(), data_attr->length(), ext_addr, PROTO_UDP); +} + +void TurnPort::HandleChannelData(int channel_id, const char* data, + size_t size) { + // Read the message, and process according to RFC5766, Section 11.6. + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | Channel Number | Length | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | | + // / Application Data / + // / / + // | | + // | +-------------------------------+ + // | | + // +-------------------------------+ + + // Extract header fields from the message. + uint16 len = talk_base::GetBE16(data + 2); + if (len > size - TURN_CHANNEL_HEADER_SIZE) { + LOG_J(LS_WARNING, this) << "Received TURN channel data message with " + << "incorrect length, len=" << len; + return; + } + // Allowing messages larger than |len|, as ChannelData can be padded. + + TurnEntry* entry = FindEntry(channel_id); + if (!entry) { + LOG_J(LS_WARNING, this) << "Received TURN channel data message for invalid " + << "channel, channel_id=" << channel_id; + return; + } + + DispatchPacket(data + TURN_CHANNEL_HEADER_SIZE, len, entry->address(), + PROTO_UDP); +} + +void TurnPort::DispatchPacket(const char* data, size_t size, + const talk_base::SocketAddress& remote_addr, ProtocolType proto) { + if (Connection* conn = GetConnection(remote_addr)) { + conn->OnReadPacket(data, size); + } else { + Port::OnReadPacket(data, size, remote_addr, proto); + } +} + +bool TurnPort::ScheduleRefresh(int lifetime) { + // Lifetime is in seconds; we schedule a refresh for one minute less. + if (lifetime < 2 * 60) { + LOG_J(LS_WARNING, this) << "Received response with lifetime that was " + << "too short, lifetime=" << lifetime; + return false; + } + + SendRequest(new TurnRefreshRequest(this), (lifetime - 60) * 1000); + return true; +} + +void TurnPort::SendRequest(StunRequest* req, int delay) { + request_manager_.SendDelayed(req, delay); +} + +void TurnPort::AddRequestAuthInfo(StunMessage* msg) { + // If we've gotten the necessary data from the server, add it to our request. + VERIFY(!hash_.empty()); + VERIFY(msg->AddAttribute(new StunByteStringAttribute( + STUN_ATTR_USERNAME, credentials_.username))); + VERIFY(msg->AddAttribute(new StunByteStringAttribute( + STUN_ATTR_REALM, realm_))); + VERIFY(msg->AddAttribute(new StunByteStringAttribute( + STUN_ATTR_NONCE, nonce_))); + VERIFY(msg->AddMessageIntegrity(hash())); +} + +int TurnPort::Send(const void* data, size_t len) { + return socket_->SendTo(data, len, server_address_.address); +} + +void TurnPort::UpdateHash() { + VERIFY(ComputeStunCredentialHash(credentials_.username, realm_, + credentials_.password, &hash_)); +} + +bool TurnPort::UpdateNonce(StunMessage* response) { + // When stale nonce error received, we should update + // hash and store realm and nonce. + // Check the mandatory attributes. + const StunByteStringAttribute* realm_attr = + response->GetByteString(STUN_ATTR_REALM); + if (!realm_attr) { + LOG(LS_ERROR) << "Missing STUN_ATTR_REALM attribute in " + << "stale nonce error response."; + return false; + } + set_realm(realm_attr->GetString()); + + const StunByteStringAttribute* nonce_attr = + response->GetByteString(STUN_ATTR_NONCE); + if (!nonce_attr) { + LOG(LS_ERROR) << "Missing STUN_ATTR_NONCE attribute in " + << "stale nonce error response."; + return false; + } + set_nonce(nonce_attr->GetString()); + return true; +} + +static bool MatchesIP(TurnEntry* e, talk_base::IPAddress ipaddr) { + return e->address().ipaddr() == ipaddr; +} +bool TurnPort::HasPermission(const talk_base::IPAddress& ipaddr) const { + return (std::find_if(entries_.begin(), entries_.end(), + std::bind2nd(std::ptr_fun(MatchesIP), ipaddr)) != entries_.end()); +} + +static bool MatchesAddress(TurnEntry* e, talk_base::SocketAddress addr) { + return e->address() == addr; +} +TurnEntry* TurnPort::FindEntry(const talk_base::SocketAddress& addr) const { + EntryList::const_iterator it = std::find_if(entries_.begin(), entries_.end(), + std::bind2nd(std::ptr_fun(MatchesAddress), addr)); + return (it != entries_.end()) ? *it : NULL; +} + +static bool MatchesChannelId(TurnEntry* e, int id) { + return e->channel_id() == id; +} +TurnEntry* TurnPort::FindEntry(int channel_id) const { + EntryList::const_iterator it = std::find_if(entries_.begin(), entries_.end(), + std::bind2nd(std::ptr_fun(MatchesChannelId), channel_id)); + return (it != entries_.end()) ? *it : NULL; +} + +TurnEntry* TurnPort::CreateEntry(const talk_base::SocketAddress& addr) { + ASSERT(FindEntry(addr) == NULL); + TurnEntry* entry = new TurnEntry(this, next_channel_number_++, addr); + entries_.push_back(entry); + return entry; +} + +void TurnPort::DestroyEntry(const talk_base::SocketAddress& addr) { + TurnEntry* entry = FindEntry(addr); + ASSERT(entry != NULL); + entry->SignalDestroyed(entry); + entries_.remove(entry); + delete entry; +} + +void TurnPort::OnConnectionDestroyed(Connection* conn) { + // Destroying TurnEntry for the connection, which is already destroyed. + DestroyEntry(conn->remote_candidate().address()); +} + +TurnAllocateRequest::TurnAllocateRequest(TurnPort* port) + : StunRequest(new TurnMessage()), + port_(port) { +} + +void TurnAllocateRequest::Prepare(StunMessage* request) { + // Create the request as indicated in RFC 5766, Section 6.1. + request->SetType(TURN_ALLOCATE_REQUEST); + StunUInt32Attribute* transport_attr = StunAttribute::CreateUInt32( + STUN_ATTR_REQUESTED_TRANSPORT); + transport_attr->SetValue(IPPROTO_UDP << 24); + VERIFY(request->AddAttribute(transport_attr)); + if (!port_->hash().empty()) { + port_->AddRequestAuthInfo(request); + } +} + +void TurnAllocateRequest::OnResponse(StunMessage* response) { + // Check mandatory attributes as indicated in RFC5766, Section 6.3. + const StunAddressAttribute* mapped_attr = + response->GetAddress(STUN_ATTR_XOR_MAPPED_ADDRESS); + if (!mapped_attr) { + LOG_J(LS_WARNING, port_) << "Missing STUN_ATTR_XOR_MAPPED_ADDRESS " + << "attribute in allocate success response"; + return; + } + + // TODO(mallinath) - Use mapped address for STUN candidate. + port_->OnStunAddress(mapped_attr->GetAddress()); + + const StunAddressAttribute* relayed_attr = + response->GetAddress(STUN_ATTR_XOR_RELAYED_ADDRESS); + if (!relayed_attr) { + LOG_J(LS_WARNING, port_) << "Missing STUN_ATTR_XOR_RELAYED_ADDRESS " + << "attribute in allocate success response"; + return; + } + + const StunUInt32Attribute* lifetime_attr = + response->GetUInt32(STUN_ATTR_TURN_LIFETIME); + if (!lifetime_attr) { + LOG_J(LS_WARNING, port_) << "Missing STUN_ATTR_TURN_LIFETIME attribute in " + << "allocate success response"; + return; + } + // Notify the port the allocate succeeded, and schedule a refresh request. + port_->OnAllocateSuccess(relayed_attr->GetAddress()); + port_->ScheduleRefresh(lifetime_attr->value()); +} + +void TurnAllocateRequest::OnErrorResponse(StunMessage* response) { + // Process error response according to RFC5766, Section 6.4. + const StunErrorCodeAttribute* error_code = response->GetErrorCode(); + switch (error_code->code()) { + case STUN_ERROR_UNAUTHORIZED: // Unauthrorized. + OnAuthChallenge(response, error_code->code()); + break; + default: + LOG_J(LS_WARNING, port_) << "Allocate response error, code=" + << error_code->code(); + port_->OnAllocateError(); + } +} + +void TurnAllocateRequest::OnTimeout() { + LOG_J(LS_WARNING, port_) << "Allocate request timeout"; + port_->OnAllocateRequestTimeout(); +} + +void TurnAllocateRequest::OnAuthChallenge(StunMessage* response, int code) { + // If we failed to authenticate even after we sent our credentials, fail hard. + if (code == STUN_ERROR_UNAUTHORIZED && !port_->hash().empty()) { + LOG_J(LS_WARNING, port_) << "Failed to authenticate with the server " + << "after challenge."; + port_->OnAllocateError(); + return; + } + + // Check the mandatory attributes. + const StunByteStringAttribute* realm_attr = + response->GetByteString(STUN_ATTR_REALM); + if (!realm_attr) { + LOG_J(LS_WARNING, port_) << "Missing STUN_ATTR_REALM attribute in " + << "allocate unauthorized response."; + return; + } + port_->set_realm(realm_attr->GetString()); + + const StunByteStringAttribute* nonce_attr = + response->GetByteString(STUN_ATTR_NONCE); + if (!nonce_attr) { + LOG_J(LS_WARNING, port_) << "Missing STUN_ATTR_NONCE attribute in " + << "allocate unauthorized response."; + return; + } + port_->set_nonce(nonce_attr->GetString()); + + // Send another allocate request, with the received realm and nonce values. + port_->SendRequest(new TurnAllocateRequest(port_), 0); +} + +TurnRefreshRequest::TurnRefreshRequest(TurnPort* port) + : StunRequest(new TurnMessage()), + port_(port) { +} + +void TurnRefreshRequest::Prepare(StunMessage* request) { + // Create the request as indicated in RFC 5766, Section 7.1. + // No attributes need to be included. + request->SetType(TURN_REFRESH_REQUEST); + port_->AddRequestAuthInfo(request); +} + +void TurnRefreshRequest::OnResponse(StunMessage* response) { + // Check mandatory attributes as indicated in RFC5766, Section 7.3. + const StunUInt32Attribute* lifetime_attr = + response->GetUInt32(STUN_ATTR_TURN_LIFETIME); + if (!lifetime_attr) { + LOG_J(LS_WARNING, port_) << "Missing STUN_ATTR_TURN_LIFETIME attribute in " + << "refresh success response."; + return; + } + + // Schedule a refresh based on the returned lifetime value. + port_->ScheduleRefresh(lifetime_attr->value()); +} + +void TurnRefreshRequest::OnErrorResponse(StunMessage* response) { + // TODO(juberti): Handle 437 error response as a success. + const StunErrorCodeAttribute* error_code = response->GetErrorCode(); + LOG_J(LS_WARNING, port_) << "Refresh response error, code=" + << error_code->code(); + + if (error_code->code() == STUN_ERROR_STALE_NONCE) { + if (port_->UpdateNonce(response)) { + // Send RefreshRequest immediately. + port_->SendRequest(new TurnRefreshRequest(port_), 0); + } + } +} + +void TurnRefreshRequest::OnTimeout() { +} + +TurnCreatePermissionRequest::TurnCreatePermissionRequest( + TurnPort* port, TurnEntry* entry, + const talk_base::SocketAddress& ext_addr) + : StunRequest(new TurnMessage()), + port_(port), + entry_(entry), + ext_addr_(ext_addr) { + entry_->SignalDestroyed.connect( + this, &TurnCreatePermissionRequest::OnEntryDestroyed); +} + +void TurnCreatePermissionRequest::Prepare(StunMessage* request) { + // Create the request as indicated in RFC5766, Section 9.1. + request->SetType(TURN_CREATE_PERMISSION_REQUEST); + VERIFY(request->AddAttribute(new StunXorAddressAttribute( + STUN_ATTR_XOR_PEER_ADDRESS, ext_addr_))); + port_->AddRequestAuthInfo(request); +} + +void TurnCreatePermissionRequest::OnResponse(StunMessage* response) { + if (entry_) { + entry_->OnCreatePermissionSuccess(); + } +} + +void TurnCreatePermissionRequest::OnErrorResponse(StunMessage* response) { + if (entry_) { + const StunErrorCodeAttribute* error_code = response->GetErrorCode(); + entry_->OnCreatePermissionError(response, error_code->code()); + } +} + +void TurnCreatePermissionRequest::OnTimeout() { + LOG_J(LS_WARNING, port_) << "Create permission timeout"; +} + +void TurnCreatePermissionRequest::OnEntryDestroyed(TurnEntry* entry) { + ASSERT(entry_ == entry); + entry_ = NULL; +} + +TurnChannelBindRequest::TurnChannelBindRequest( + TurnPort* port, TurnEntry* entry, + int channel_id, const talk_base::SocketAddress& ext_addr) + : StunRequest(new TurnMessage()), + port_(port), + entry_(entry), + channel_id_(channel_id), + ext_addr_(ext_addr) { + entry_->SignalDestroyed.connect( + this, &TurnChannelBindRequest::OnEntryDestroyed); +} + +void TurnChannelBindRequest::Prepare(StunMessage* request) { + // Create the request as indicated in RFC5766, Section 11.1. + request->SetType(TURN_CHANNEL_BIND_REQUEST); + VERIFY(request->AddAttribute(new StunUInt32Attribute( + STUN_ATTR_CHANNEL_NUMBER, channel_id_ << 16))); + VERIFY(request->AddAttribute(new StunXorAddressAttribute( + STUN_ATTR_XOR_PEER_ADDRESS, ext_addr_))); + port_->AddRequestAuthInfo(request); +} + +void TurnChannelBindRequest::OnResponse(StunMessage* response) { + if (entry_) { + entry_->OnChannelBindSuccess(); + // Refresh the channel binding just under the permission timeout + // threshold. The channel binding has a longer lifetime, but + // this is the easiest way to keep both the channel and the + // permission from expiring. + entry_->SendChannelBindRequest(TURN_PERMISSION_TIMEOUT - 60 * 1000); + } +} + +void TurnChannelBindRequest::OnErrorResponse(StunMessage* response) { + if (entry_) { + const StunErrorCodeAttribute* error_code = response->GetErrorCode(); + entry_->OnChannelBindError(response, error_code->code()); + } +} + +void TurnChannelBindRequest::OnTimeout() { + LOG_J(LS_WARNING, port_) << "Channel bind timeout"; +} + +void TurnChannelBindRequest::OnEntryDestroyed(TurnEntry* entry) { + ASSERT(entry_ == entry); + entry_ = NULL; +} + +TurnEntry::TurnEntry(TurnPort* port, int channel_id, + const talk_base::SocketAddress& ext_addr) + : port_(port), + channel_id_(channel_id), + ext_addr_(ext_addr), + state_(STATE_UNBOUND) { + // Creating permission for |ext_addr_|. + SendCreatePermissionRequest(); +} + +void TurnEntry::SendCreatePermissionRequest() { + port_->SendRequest(new TurnCreatePermissionRequest( + port_, this, ext_addr_), 0); +} + +void TurnEntry::SendChannelBindRequest(int delay) { + port_->SendRequest(new TurnChannelBindRequest( + port_, this, channel_id_, ext_addr_), delay); +} + +int TurnEntry::Send(const void* data, size_t size, bool payload) { + talk_base::ByteBuffer buf; + if (state_ != STATE_BOUND) { + // If we haven't bound the channel yet, we have to use a Send Indication. + TurnMessage msg; + msg.SetType(TURN_SEND_INDICATION); + msg.SetTransactionID( + talk_base::CreateRandomString(kStunTransactionIdLength)); + VERIFY(msg.AddAttribute(new StunXorAddressAttribute( + STUN_ATTR_XOR_PEER_ADDRESS, ext_addr_))); + VERIFY(msg.AddAttribute(new StunByteStringAttribute( + STUN_ATTR_DATA, data, size))); + VERIFY(msg.Write(&buf)); + + // If we're sending real data, request a channel bind that we can use later. + if (state_ == STATE_UNBOUND && payload) { + SendChannelBindRequest(0); + state_ = STATE_BINDING; + } + } else { + // If the channel is bound, we can send the data as a Channel Message. + buf.WriteUInt16(channel_id_); + buf.WriteUInt16(size); + buf.WriteBytes(reinterpret_cast(data), size); + } + return port_->Send(buf.Data(), buf.Length()); +} + +void TurnEntry::OnCreatePermissionSuccess() { + LOG_J(LS_INFO, port_) << "Create permission for " + << ext_addr_.ToSensitiveString() + << " succeeded"; + // For success result code will be 0. + port_->SignalCreatePermissionResult(port_, ext_addr_, 0); +} + +void TurnEntry::OnCreatePermissionError(StunMessage* response, int code) { + LOG_J(LS_WARNING, port_) << "Create permission for " + << ext_addr_.ToSensitiveString() + << " failed, code=" << code; + if (code == STUN_ERROR_STALE_NONCE) { + if (port_->UpdateNonce(response)) { + SendCreatePermissionRequest(); + } + } else { + // Send signal with error code. + port_->SignalCreatePermissionResult(port_, ext_addr_, code); + } +} + +void TurnEntry::OnChannelBindSuccess() { + LOG_J(LS_INFO, port_) << "Channel bind for " << ext_addr_.ToSensitiveString() + << " succeeded"; + ASSERT(state_ == STATE_BINDING || state_ == STATE_BOUND); + state_ = STATE_BOUND; +} + +void TurnEntry::OnChannelBindError(StunMessage* response, int code) { + // TODO(mallinath) - Implement handling of error response for channel + // bind request as per http://tools.ietf.org/html/rfc5766#section-11.3 + LOG_J(LS_WARNING, port_) << "Channel bind for " + << ext_addr_.ToSensitiveString() + << " failed, code=" << code; + if (code == STUN_ERROR_STALE_NONCE) { + if (port_->UpdateNonce(response)) { + // Send channel bind request with fresh nonce. + SendChannelBindRequest(0); + } + } +} + +} // namespace cricket diff --git a/talk/p2p/base/turnport.h b/talk/p2p/base/turnport.h new file mode 100644 index 000000000..fa23d5347 --- /dev/null +++ b/talk/p2p/base/turnport.h @@ -0,0 +1,179 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_P2P_BASE_TURNPORT_H_ +#define TALK_P2P_BASE_TURNPORT_H_ + +#include +#include +#include + +#include "talk/p2p/base/port.h" +#include "talk/p2p/client/basicportallocator.h" + +namespace talk_base { +class AsyncPacketSocket; +class AsyncResolver; +class SignalThread; +} + +namespace cricket { + +extern const char TURN_PORT_TYPE[]; +class TurnAllocateRequest; +class TurnEntry; + +class TurnPort : public Port { + public: + static TurnPort* Create(talk_base::Thread* thread, + talk_base::PacketSocketFactory* factory, + talk_base::Network* network, + const talk_base::IPAddress& ip, + int min_port, int max_port, + const std::string& username, // ice username. + const std::string& password, // ice password. + const ProtocolAddress& server_address, + const RelayCredentials& credentials) { + return new TurnPort(thread, factory, network, ip, min_port, max_port, + username, password, server_address, credentials); + } + + virtual ~TurnPort(); + + const ProtocolAddress& server_address() const { return server_address_; } + + bool connected() const { return connected_; } + const RelayCredentials& credentials() const { return credentials_; } + + virtual void PrepareAddress(); + virtual Connection* CreateConnection( + const Candidate& c, PortInterface::CandidateOrigin origin); + virtual int SendTo(const void* data, size_t size, + const talk_base::SocketAddress& addr, + bool payload); + virtual int SetOption(talk_base::Socket::Option opt, int value); + virtual int GetOption(talk_base::Socket::Option opt, int* value); + virtual int GetError(); + virtual void OnReadPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& remote_addr); + virtual void OnReadyToSend(talk_base::AsyncPacketSocket* socket); + + void OnSocketConnect(talk_base::AsyncPacketSocket* socket); + void OnSocketClose(talk_base::AsyncPacketSocket* socket, int error); + + + const std::string& hash() const { return hash_; } + const std::string& nonce() const { return nonce_; } + + // This signal is only for testing purpose. + sigslot::signal3 + SignalCreatePermissionResult; + + protected: + TurnPort(talk_base::Thread* thread, + talk_base::PacketSocketFactory* factory, + talk_base::Network* network, + const talk_base::IPAddress& ip, + int min_port, int max_port, + const std::string& username, + const std::string& password, + const ProtocolAddress& server_address, + const RelayCredentials& credentials); + + private: + typedef std::list EntryList; + typedef std::map SocketOptionsMap; + + virtual void OnMessage(talk_base::Message* pmsg); + + void set_nonce(const std::string& nonce) { nonce_ = nonce; } + void set_realm(const std::string& realm) { + if (realm != realm_) { + realm_ = realm; + UpdateHash(); + } + } + + void ResolveTurnAddress(const talk_base::SocketAddress& address); + void OnResolveResult(talk_base::SignalThread* signal_thread); + + void AddRequestAuthInfo(StunMessage* msg); + void OnSendStunPacket(const void* data, size_t size, StunRequest* request); + // Stun address from allocate success response. + // Currently used only for testing. + void OnStunAddress(const talk_base::SocketAddress& address); + void OnAllocateSuccess(const talk_base::SocketAddress& address); + void OnAllocateError(); + void OnAllocateRequestTimeout(); + + void HandleDataIndication(const char* data, size_t size); + void HandleChannelData(int channel_id, const char* data, size_t size); + void DispatchPacket(const char* data, size_t size, + const talk_base::SocketAddress& remote_addr, ProtocolType proto); + + bool ScheduleRefresh(int lifetime); + void SendRequest(StunRequest* request, int delay); + int Send(const void* data, size_t size); + void UpdateHash(); + bool UpdateNonce(StunMessage* response); + + bool HasPermission(const talk_base::IPAddress& ipaddr) const; + TurnEntry* FindEntry(const talk_base::SocketAddress& address) const; + TurnEntry* FindEntry(int channel_id) const; + TurnEntry* CreateEntry(const talk_base::SocketAddress& address); + void DestroyEntry(const talk_base::SocketAddress& address); + void OnConnectionDestroyed(Connection* conn); + + ProtocolAddress server_address_; + RelayCredentials credentials_; + + talk_base::scoped_ptr socket_; + SocketOptionsMap socket_options_; + talk_base::AsyncResolver* resolver_; + int error_; + + StunRequestManager request_manager_; + std::string realm_; // From 401/438 response message. + std::string nonce_; // From 401/438 response message. + std::string hash_; // Digest of username:realm:password + + int next_channel_number_; + EntryList entries_; + + bool connected_; + + friend class TurnEntry; + friend class TurnAllocateRequest; + friend class TurnRefreshRequest; + friend class TurnCreatePermissionRequest; + friend class TurnChannelBindRequest; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_TURNPORT_H_ diff --git a/talk/p2p/base/turnport_unittest.cc b/talk/p2p/base/turnport_unittest.cc new file mode 100644 index 000000000..42ae312b7 --- /dev/null +++ b/talk/p2p/base/turnport_unittest.cc @@ -0,0 +1,331 @@ +/* + * libjingle + * Copyright 2012, 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/base/asynctcpsocket.h" +#include "talk/base/buffer.h" +#include "talk/base/firewallsocketserver.h" +#include "talk/base/logging.h" +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/socketaddress.h" +#include "talk/base/thread.h" +#include "talk/base/virtualsocketserver.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/tcpport.h" +#include "talk/p2p/base/testturnserver.h" +#include "talk/p2p/base/turnport.h" +#include "talk/p2p/base/udpport.h" + +using talk_base::SocketAddress; +using cricket::Connection; +using cricket::Port; +using cricket::PortInterface; +using cricket::TurnPort; +using cricket::UDPPort; + +static const SocketAddress kLocalAddr1("11.11.11.11", 0); +static const SocketAddress kLocalAddr2("22.22.22.22", 0); +static const SocketAddress kTurnUdpIntAddr("99.99.99.3", + cricket::TURN_SERVER_PORT); +static const SocketAddress kTurnTcpIntAddr("99.99.99.4", + cricket::TURN_SERVER_PORT); +static const SocketAddress kTurnUdpExtAddr("99.99.99.5", 0); + +static const char kIceUfrag1[] = "TESTICEUFRAG0001"; +static const char kIceUfrag2[] = "TESTICEUFRAG0002"; +static const char kIcePwd1[] = "TESTICEPWD00000000000001"; +static const char kIcePwd2[] = "TESTICEPWD00000000000002"; +static const char kTurnUsername[] = "test"; +static const char kTurnPassword[] = "test"; +static const int kTimeout = 1000; + +static const cricket::ProtocolAddress kTurnUdpProtoAddr(kTurnUdpIntAddr, + cricket::PROTO_UDP); +static const cricket::ProtocolAddress kTurnTcpProtoAddr(kTurnTcpIntAddr, + cricket::PROTO_TCP); + +class TurnPortTest : public testing::Test, + public sigslot::has_slots<> { + public: + TurnPortTest() + : main_(talk_base::Thread::Current()), + pss_(new talk_base::PhysicalSocketServer), + ss_(new talk_base::VirtualSocketServer(pss_.get())), + ss_scope_(ss_.get()), + network_("unittest", "unittest", talk_base::IPAddress(INADDR_ANY), 32), + socket_factory_(talk_base::Thread::Current()), + turn_server_(main_, kTurnUdpIntAddr, kTurnUdpExtAddr), + turn_ready_(false), + turn_error_(false), + turn_unknown_address_(false), + turn_create_permission_success_(false), + udp_ready_(false) { + network_.AddIP(talk_base::IPAddress(INADDR_ANY)); + } + + void OnTurnPortComplete(Port* port) { + turn_ready_ = true; + } + void OnTurnPortError(Port* port) { + turn_error_ = true; + } + void OnTurnUnknownAddress(PortInterface* port, const SocketAddress& addr, + cricket::ProtocolType proto, + cricket::IceMessage* msg, const std::string& rf, + bool /*port_muxed*/) { + turn_unknown_address_ = true; + } + void OnTurnCreatePermissionResult(TurnPort* port, const SocketAddress& addr, + int code) { + // Ignoring the address. + if (code == 0) { + turn_create_permission_success_ = true; + } + } + void OnTurnReadPacket(Connection* conn, const char* data, size_t size) { + turn_packets_.push_back(talk_base::Buffer(data, size)); + } + void OnUdpPortComplete(Port* port) { + udp_ready_ = true; + } + void OnUdpReadPacket(Connection* conn, const char* data, size_t size) { + udp_packets_.push_back(talk_base::Buffer(data, size)); + } + + talk_base::AsyncSocket* CreateServerSocket(const SocketAddress addr) { + talk_base::AsyncSocket* socket = ss_->CreateAsyncSocket(SOCK_STREAM); + EXPECT_GE(socket->Bind(addr), 0); + EXPECT_GE(socket->Listen(5), 0); + return socket; + } + + void CreateTurnPort(const std::string& username, + const std::string& password, + const cricket::ProtocolAddress& server_address) { + cricket::RelayCredentials credentials(username, password); + turn_port_.reset(TurnPort::Create(main_, &socket_factory_, &network_, + kLocalAddr1.ipaddr(), 0, 0, + kIceUfrag1, kIcePwd1, + server_address, credentials)); + turn_port_->SignalPortComplete.connect(this, + &TurnPortTest::OnTurnPortComplete); + turn_port_->SignalPortError.connect(this, + &TurnPortTest::OnTurnPortError); + turn_port_->SignalUnknownAddress.connect(this, + &TurnPortTest::OnTurnUnknownAddress); + turn_port_->SignalCreatePermissionResult.connect(this, + &TurnPortTest::OnTurnCreatePermissionResult); + } + void CreateUdpPort() { + udp_port_.reset(UDPPort::Create(main_, &socket_factory_, &network_, + kLocalAddr2.ipaddr(), 0, 0, + kIceUfrag2, kIcePwd2)); + udp_port_->SignalPortComplete.connect( + this, &TurnPortTest::OnUdpPortComplete); + } + + void TestTurnConnection() { + // Create ports and prepare addresses. + ASSERT_TRUE(turn_port_ != NULL); + turn_port_->PrepareAddress(); + ASSERT_TRUE_WAIT(turn_ready_, kTimeout); + CreateUdpPort(); + udp_port_->PrepareAddress(); + ASSERT_TRUE_WAIT(udp_ready_, kTimeout); + + // Send ping from UDP to TURN. + Connection* conn1 = udp_port_->CreateConnection( + turn_port_->Candidates()[0], Port::ORIGIN_MESSAGE); + ASSERT_TRUE(conn1 != NULL); + conn1->Ping(0); + WAIT(!turn_unknown_address_, kTimeout); + EXPECT_FALSE(turn_unknown_address_); + EXPECT_EQ(Connection::STATE_READ_INIT, conn1->read_state()); + EXPECT_EQ(Connection::STATE_WRITE_INIT, conn1->write_state()); + + // Send ping from TURN to UDP. + Connection* conn2 = turn_port_->CreateConnection( + udp_port_->Candidates()[0], Port::ORIGIN_MESSAGE); + ASSERT_TRUE(conn2 != NULL); + ASSERT_TRUE_WAIT(turn_create_permission_success_, kTimeout); + conn2->Ping(0); + + EXPECT_EQ_WAIT(Connection::STATE_WRITABLE, conn2->write_state(), kTimeout); + EXPECT_EQ(Connection::STATE_READABLE, conn1->read_state()); + EXPECT_EQ(Connection::STATE_READ_INIT, conn2->read_state()); + EXPECT_EQ(Connection::STATE_WRITE_INIT, conn1->write_state()); + + // Send another ping from UDP to TURN. + conn1->Ping(0); + EXPECT_EQ_WAIT(Connection::STATE_WRITABLE, conn1->write_state(), kTimeout); + EXPECT_EQ(Connection::STATE_READABLE, conn2->read_state()); + } + + void TestTurnSendData() { + turn_port_->PrepareAddress(); + EXPECT_TRUE_WAIT(turn_ready_, kTimeout); + CreateUdpPort(); + udp_port_->PrepareAddress(); + EXPECT_TRUE_WAIT(udp_ready_, kTimeout); + // Create connections and send pings. + Connection* conn1 = turn_port_->CreateConnection( + udp_port_->Candidates()[0], Port::ORIGIN_MESSAGE); + Connection* conn2 = udp_port_->CreateConnection( + turn_port_->Candidates()[0], Port::ORIGIN_MESSAGE); + ASSERT_TRUE(conn1 != NULL); + ASSERT_TRUE(conn2 != NULL); + conn1->SignalReadPacket.connect(static_cast(this), + &TurnPortTest::OnTurnReadPacket); + conn2->SignalReadPacket.connect(static_cast(this), + &TurnPortTest::OnUdpReadPacket); + conn1->Ping(0); + EXPECT_EQ_WAIT(Connection::STATE_WRITABLE, conn1->write_state(), kTimeout); + conn2->Ping(0); + EXPECT_EQ_WAIT(Connection::STATE_WRITABLE, conn2->write_state(), kTimeout); + + // Send some data. + size_t num_packets = 256; + for (size_t i = 0; i < num_packets; ++i) { + char buf[256]; + for (size_t j = 0; j < i + 1; ++j) { + buf[j] = 0xFF - j; + } + conn1->Send(buf, i + 1); + conn2->Send(buf, i + 1); + main_->ProcessMessages(0); + } + + // Check the data. + ASSERT_EQ_WAIT(num_packets, turn_packets_.size(), kTimeout); + ASSERT_EQ_WAIT(num_packets, udp_packets_.size(), kTimeout); + for (size_t i = 0; i < num_packets; ++i) { + EXPECT_EQ(i + 1, turn_packets_[i].length()); + EXPECT_EQ(i + 1, udp_packets_[i].length()); + EXPECT_EQ(turn_packets_[i], udp_packets_[i]); + } + } + + protected: + talk_base::Thread* main_; + talk_base::scoped_ptr pss_; + talk_base::scoped_ptr ss_; + talk_base::SocketServerScope ss_scope_; + talk_base::Network network_; + talk_base::BasicPacketSocketFactory socket_factory_; + cricket::TestTurnServer turn_server_; + talk_base::scoped_ptr turn_port_; + talk_base::scoped_ptr udp_port_; + bool turn_ready_; + bool turn_error_; + bool turn_unknown_address_; + bool turn_create_permission_success_; + bool udp_ready_; + std::vector turn_packets_; + std::vector udp_packets_; +}; + +// Do a normal TURN allocation. +TEST_F(TurnPortTest, TestTurnAllocate) { + CreateTurnPort(kTurnUsername, kTurnPassword, kTurnUdpProtoAddr); + EXPECT_EQ(0, turn_port_->SetOption(talk_base::Socket::OPT_SNDBUF, 10*1024)); + turn_port_->PrepareAddress(); + EXPECT_TRUE_WAIT(turn_ready_, kTimeout); + ASSERT_EQ(1U, turn_port_->Candidates().size()); + EXPECT_EQ(kTurnUdpExtAddr.ipaddr(), + turn_port_->Candidates()[0].address().ipaddr()); + EXPECT_NE(0, turn_port_->Candidates()[0].address().port()); +} + +TEST_F(TurnPortTest, TestTurnTcpAllocate) { + talk_base::AsyncSocket* tcp_server_socket = + CreateServerSocket(kTurnTcpIntAddr); + turn_server_.server()->AddInternalServerSocket( + tcp_server_socket, cricket::PROTO_TCP); + CreateTurnPort(kTurnUsername, kTurnPassword, kTurnTcpProtoAddr); + EXPECT_EQ(0, turn_port_->SetOption(talk_base::Socket::OPT_SNDBUF, 10*1024)); + turn_port_->PrepareAddress(); + EXPECT_TRUE_WAIT(turn_ready_, kTimeout); + ASSERT_EQ(1U, turn_port_->Candidates().size()); + EXPECT_EQ(kTurnUdpExtAddr.ipaddr(), + turn_port_->Candidates()[0].address().ipaddr()); + EXPECT_NE(0, turn_port_->Candidates()[0].address().port()); +} + +// Try to do a TURN allocation with an invalid password. +TEST_F(TurnPortTest, TestTurnAllocateBadPassword) { + CreateTurnPort(kTurnUsername, "bad", kTurnUdpProtoAddr); + turn_port_->PrepareAddress(); + EXPECT_TRUE_WAIT(turn_error_, kTimeout); + ASSERT_EQ(0U, turn_port_->Candidates().size()); +} + +// Do a TURN allocation and try to send a packet to it from the outside. +// The packet should be dropped. Then, try to send a packet from TURN to the +// outside. It should reach its destination. Finally, try again from the +// outside. It should now work as well. +TEST_F(TurnPortTest, TestTurnConnection) { + CreateTurnPort(kTurnUsername, kTurnPassword, kTurnUdpProtoAddr); + TestTurnConnection(); +} + +TEST_F(TurnPortTest, TestTurnTcpConnection) { + talk_base::AsyncSocket* tcp_server_socket = + CreateServerSocket(kTurnTcpIntAddr); + turn_server_.server()->AddInternalServerSocket( + tcp_server_socket, cricket::PROTO_TCP); + CreateTurnPort(kTurnUsername, kTurnPassword, kTurnTcpProtoAddr); + TestTurnConnection(); +} + +// Run TurnConnectionTest with one-time-use nonce feature. +// Here server will send a 438 STALE_NONCE error message for +// every TURN transaction. +TEST_F(TurnPortTest, TestTurnConnectionUsingOTUNonce) { + turn_server_.set_enable_otu_nonce(true); + CreateTurnPort(kTurnUsername, kTurnPassword, kTurnUdpProtoAddr); + TestTurnConnection(); +} + +// Do a TURN allocation, establish a connection, and send some data. +TEST_F(TurnPortTest, TestTurnSendDataTurnUdpToUdp) { + // Create ports and prepare addresses. + CreateTurnPort(kTurnUsername, kTurnPassword, kTurnUdpProtoAddr); + TestTurnSendData(); +} + +TEST_F(TurnPortTest, TestTurnSendDataTurnTcpToUdp) { + talk_base::AsyncSocket* tcp_server_socket = + CreateServerSocket(kTurnTcpIntAddr); + turn_server_.server()->AddInternalServerSocket( + tcp_server_socket, cricket::PROTO_TCP); + // Create ports and prepare addresses. + CreateTurnPort(kTurnUsername, kTurnPassword, kTurnTcpProtoAddr); + TestTurnSendData(); +} diff --git a/talk/p2p/base/turnserver.cc b/talk/p2p/base/turnserver.cc new file mode 100644 index 000000000..e82455a87 --- /dev/null +++ b/talk/p2p/base/turnserver.cc @@ -0,0 +1,1006 @@ +/* + * libjingle + * Copyright 2012, 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/p2p/base/turnserver.h" + +#include "talk/base/bytebuffer.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/messagedigest.h" +#include "talk/base/socketadapters.h" +#include "talk/base/stringencode.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/asyncstuntcpsocket.h" +#include "talk/p2p/base/common.h" +#include "talk/p2p/base/packetsocketfactory.h" +#include "talk/p2p/base/stun.h" + +namespace cricket { + +// TODO(juberti): Move this all to a future turnmessage.h +//static const int IPPROTO_UDP = 17; +static const int kNonceTimeout = 60 * 60 * 1000; // 60 minutes +static const int kDefaultAllocationTimeout = 10 * 60 * 1000; // 10 minutes +static const int kPermissionTimeout = 5 * 60 * 1000; // 5 minutes +static const int kChannelTimeout = 10 * 60 * 1000; // 10 minutes + +static const int kMinChannelNumber = 0x4000; +static const int kMaxChannelNumber = 0x7FFF; + +static const size_t kNonceKeySize = 16; +static const size_t kNonceSize = 40; + +static const size_t TURN_CHANNEL_HEADER_SIZE = 4U; + +// TODO(mallinath) - Move these to a common place. +static const size_t kMaxPacketSize = 64 * 1024; + +inline bool IsTurnChannelData(uint16 msg_type) { + // The first two bits of a channel data message are 0b01. + return ((msg_type & 0xC000) == 0x4000); +} + +// IDs used for posted messages. +enum { + MSG_TIMEOUT, +}; + +// Encapsulates a TURN allocation. +// The object is created when an allocation request is received, and then +// handles TURN messages (via HandleTurnMessage) and channel data messages +// (via HandleChannelData) for this allocation when received by the server. +// The object self-deletes and informs the server if its lifetime timer expires. +class TurnServer::Allocation : public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + Allocation(TurnServer* server_, + talk_base::Thread* thread, const Connection& conn, + talk_base::AsyncPacketSocket* server_socket, + const std::string& key); + virtual ~Allocation(); + + Connection* conn() { return &conn_; } + const std::string& key() const { return key_; } + const std::string& transaction_id() const { return transaction_id_; } + const std::string& username() const { return username_; } + const std::string& last_nonce() const { return last_nonce_; } + void set_last_nonce(const std::string& nonce) { last_nonce_ = nonce; } + + std::string ToString() const; + + void HandleTurnMessage(const TurnMessage* msg); + void HandleChannelData(const char* data, size_t size); + + sigslot::signal1 SignalDestroyed; + + private: + typedef std::list PermissionList; + typedef std::list ChannelList; + + void HandleAllocateRequest(const TurnMessage* msg); + void HandleRefreshRequest(const TurnMessage* msg); + void HandleSendIndication(const TurnMessage* msg); + void HandleCreatePermissionRequest(const TurnMessage* msg); + void HandleChannelBindRequest(const TurnMessage* msg); + + void OnExternalPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& addr); + + static int ComputeLifetime(const TurnMessage* msg); + bool HasPermission(const talk_base::IPAddress& addr); + void AddPermission(const talk_base::IPAddress& addr); + Permission* FindPermission(const talk_base::IPAddress& addr) const; + Channel* FindChannel(int channel_id) const; + Channel* FindChannel(const talk_base::SocketAddress& addr) const; + + void SendResponse(TurnMessage* msg); + void SendBadRequestResponse(const TurnMessage* req); + void SendErrorResponse(const TurnMessage* req, int code, + const std::string& reason); + void SendExternal(const void* data, size_t size, + const talk_base::SocketAddress& peer); + + void OnPermissionDestroyed(Permission* perm); + void OnChannelDestroyed(Channel* channel); + virtual void OnMessage(talk_base::Message* msg); + + TurnServer* server_; + talk_base::Thread* thread_; + Connection conn_; + talk_base::scoped_ptr external_socket_; + std::string key_; + std::string transaction_id_; + std::string username_; + std::string last_nonce_; + PermissionList perms_; + ChannelList channels_; +}; + +// Encapsulates a TURN permission. +// The object is created when a create permission request is received by an +// allocation, and self-deletes when its lifetime timer expires. +class TurnServer::Permission : public talk_base::MessageHandler { + public: + Permission(talk_base::Thread* thread, const talk_base::IPAddress& peer); + ~Permission(); + + const talk_base::IPAddress& peer() const { return peer_; } + void Refresh(); + + sigslot::signal1 SignalDestroyed; + + private: + virtual void OnMessage(talk_base::Message* msg); + + talk_base::Thread* thread_; + talk_base::IPAddress peer_; +}; + +// Encapsulates a TURN channel binding. +// The object is created when a channel bind request is received by an +// allocation, and self-deletes when its lifetime timer expires. +class TurnServer::Channel : public talk_base::MessageHandler { + public: + Channel(talk_base::Thread* thread, int id, + const talk_base::SocketAddress& peer); + ~Channel(); + + int id() const { return id_; } + const talk_base::SocketAddress& peer() const { return peer_; } + void Refresh(); + + sigslot::signal1 SignalDestroyed; + + private: + virtual void OnMessage(talk_base::Message* msg); + + talk_base::Thread* thread_; + int id_; + talk_base::SocketAddress peer_; +}; + +static bool InitResponse(const StunMessage* req, StunMessage* resp) { + int resp_type = (req) ? GetStunSuccessResponseType(req->type()) : -1; + if (resp_type == -1) + return false; + resp->SetType(resp_type); + resp->SetTransactionID(req->transaction_id()); + return true; +} + +static bool InitErrorResponse(const StunMessage* req, int code, + const std::string& reason, StunMessage* resp) { + int resp_type = (req) ? GetStunErrorResponseType(req->type()) : -1; + if (resp_type == -1) + return false; + resp->SetType(resp_type); + resp->SetTransactionID(req->transaction_id()); + VERIFY(resp->AddAttribute(new cricket::StunErrorCodeAttribute( + STUN_ATTR_ERROR_CODE, code, reason))); + return true; +} + +TurnServer::TurnServer(talk_base::Thread* thread) + : thread_(thread), + nonce_key_(talk_base::CreateRandomString(kNonceKeySize)), + auth_hook_(NULL), + enable_otu_nonce_(false) { +} + +TurnServer::~TurnServer() { + for (AllocationMap::iterator it = allocations_.begin(); + it != allocations_.end(); ++it) { + delete it->second; + } + + for (InternalSocketMap::iterator it = server_sockets_.begin(); + it != server_sockets_.end(); ++it) { + talk_base::AsyncPacketSocket* socket = it->first; + delete socket; + } + + for (ServerSocketMap::iterator it = server_listen_sockets_.begin(); + it != server_listen_sockets_.end(); ++it) { + talk_base::AsyncSocket* socket = it->first; + delete socket; + } +} + +void TurnServer::AddInternalSocket(talk_base::AsyncPacketSocket* socket, + ProtocolType proto) { + ASSERT(server_sockets_.end() == server_sockets_.find(socket)); + server_sockets_[socket] = proto; + socket->SignalReadPacket.connect(this, &TurnServer::OnInternalPacket); +} + +void TurnServer::AddInternalServerSocket(talk_base::AsyncSocket* socket, + ProtocolType proto) { + ASSERT(server_listen_sockets_.end() == + server_listen_sockets_.find(socket)); + server_listen_sockets_[socket] = proto; + socket->SignalReadEvent.connect(this, &TurnServer::OnNewInternalConnection); +} + +void TurnServer::SetExternalSocketFactory( + talk_base::PacketSocketFactory* factory, + const talk_base::SocketAddress& external_addr) { + external_socket_factory_.reset(factory); + external_addr_ = external_addr; +} + +void TurnServer::OnNewInternalConnection(talk_base::AsyncSocket* socket) { + ASSERT(server_listen_sockets_.find(socket) != server_listen_sockets_.end()); + AcceptConnection(socket); +} + +void TurnServer::AcceptConnection(talk_base::AsyncSocket* server_socket) { + // Check if someone is trying to connect to us. + talk_base::SocketAddress accept_addr; + talk_base::AsyncSocket* accepted_socket = server_socket->Accept(&accept_addr); + if (accepted_socket != NULL) { + ProtocolType proto = server_listen_sockets_[server_socket]; + if (proto == PROTO_SSLTCP) { + accepted_socket = new talk_base::AsyncSSLServerSocket(accepted_socket); + } + cricket::AsyncStunTCPSocket* tcp_socket = + new cricket::AsyncStunTCPSocket(accepted_socket, false); + + tcp_socket->SignalClose.connect(this, &TurnServer::OnInternalSocketClose); + // Finally add the socket so it can start communicating with the client. + AddInternalSocket(tcp_socket, proto); + } +} + +void TurnServer::OnInternalSocketClose(talk_base::AsyncPacketSocket* socket, + int err) { + DestroyInternalSocket(socket); +} + +void TurnServer::OnInternalPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& addr) { + // Fail if the packet is too small to even contain a channel header. + if (size < TURN_CHANNEL_HEADER_SIZE) { + return; + } + InternalSocketMap::iterator iter = server_sockets_.find(socket); + ASSERT(iter != server_sockets_.end()); + Connection conn(addr, iter->second, socket); + uint16 msg_type = talk_base::GetBE16(data); + if (!IsTurnChannelData(msg_type)) { + // This is a STUN message. + HandleStunMessage(&conn, data, size); + } else { + // This is a channel message; let the allocation handle it. + Allocation* allocation = FindAllocation(&conn); + if (allocation) { + allocation->HandleChannelData(data, size); + } + } +} + +void TurnServer::HandleStunMessage(Connection* conn, const char* data, + size_t size) { + TurnMessage msg; + talk_base::ByteBuffer buf(data, size); + if (!msg.Read(&buf) || (buf.Length() > 0)) { + LOG(LS_WARNING) << "Received invalid STUN message"; + return; + } + + // If it's a STUN binding request, handle that specially. + if (msg.type() == STUN_BINDING_REQUEST) { + HandleBindingRequest(conn, &msg); + return; + } + + // Look up the key that we'll use to validate the M-I. If we have an + // existing allocation, the key will already be cached. + Allocation* allocation = FindAllocation(conn); + std::string key; + if (!allocation) { + GetKey(&msg, &key); + } else { + key = allocation->key(); + } + + // Ensure the message is authorized; only needed for requests. + if (IsStunRequestType(msg.type())) { + if (!CheckAuthorization(conn, &msg, data, size, key)) { + return; + } + } + + if (!allocation && msg.type() == STUN_ALLOCATE_REQUEST) { + // This is a new allocate request. + HandleAllocateRequest(conn, &msg, key); + } else if (allocation && + (msg.type() != STUN_ALLOCATE_REQUEST || + msg.transaction_id() == allocation->transaction_id())) { + // This is a non-allocate request, or a retransmit of an allocate. + // Check that the username matches the previous username used. + if (IsStunRequestType(msg.type()) && + msg.GetByteString(STUN_ATTR_USERNAME)->GetString() != + allocation->username()) { + SendErrorResponse(conn, &msg, STUN_ERROR_WRONG_CREDENTIALS, + STUN_ERROR_REASON_WRONG_CREDENTIALS); + return; + } + allocation->HandleTurnMessage(&msg); + } else { + // Allocation mismatch. + SendErrorResponse(conn, &msg, STUN_ERROR_ALLOCATION_MISMATCH, + STUN_ERROR_REASON_ALLOCATION_MISMATCH); + } +} + +bool TurnServer::GetKey(const StunMessage* msg, std::string* key) { + const StunByteStringAttribute* username_attr = + msg->GetByteString(STUN_ATTR_USERNAME); + if (!username_attr) { + return false; + } + + std::string username = username_attr->GetString(); + return (auth_hook_ != NULL && auth_hook_->GetKey(username, realm_, key)); +} + +bool TurnServer::CheckAuthorization(Connection* conn, + const StunMessage* msg, + const char* data, size_t size, + const std::string& key) { + // RFC 5389, 10.2.2. + ASSERT(IsStunRequestType(msg->type())); + const StunByteStringAttribute* mi_attr = + msg->GetByteString(STUN_ATTR_MESSAGE_INTEGRITY); + const StunByteStringAttribute* username_attr = + msg->GetByteString(STUN_ATTR_USERNAME); + const StunByteStringAttribute* realm_attr = + msg->GetByteString(STUN_ATTR_REALM); + const StunByteStringAttribute* nonce_attr = + msg->GetByteString(STUN_ATTR_NONCE); + + // Fail if no M-I. + if (!mi_attr) { + SendErrorResponseWithRealmAndNonce(conn, msg, STUN_ERROR_UNAUTHORIZED, + STUN_ERROR_REASON_UNAUTHORIZED); + return false; + } + + // Fail if there is M-I but no username, nonce, or realm. + if (!username_attr || !realm_attr || !nonce_attr) { + SendErrorResponse(conn, msg, STUN_ERROR_BAD_REQUEST, + STUN_ERROR_REASON_BAD_REQUEST); + return false; + } + + // Fail if bad nonce. + if (!ValidateNonce(nonce_attr->GetString())) { + SendErrorResponseWithRealmAndNonce(conn, msg, STUN_ERROR_STALE_NONCE, + STUN_ERROR_REASON_STALE_NONCE); + return false; + } + + // Fail if bad username or M-I. + // We need |data| and |size| for the call to ValidateMessageIntegrity. + if (key.empty() || !StunMessage::ValidateMessageIntegrity(data, size, key)) { + SendErrorResponseWithRealmAndNonce(conn, msg, STUN_ERROR_UNAUTHORIZED, + STUN_ERROR_REASON_UNAUTHORIZED); + return false; + } + + // Fail if one-time-use nonce feature is enabled. + Allocation* allocation = FindAllocation(conn); + if (enable_otu_nonce_ && allocation && + allocation->last_nonce() == nonce_attr->GetString()) { + SendErrorResponseWithRealmAndNonce(conn, msg, STUN_ERROR_STALE_NONCE, + STUN_ERROR_REASON_STALE_NONCE); + return false; + } + + if (allocation) { + allocation->set_last_nonce(nonce_attr->GetString()); + } + // Success. + return true; +} + +void TurnServer::HandleBindingRequest(Connection* conn, + const StunMessage* req) { + StunMessage response; + InitResponse(req, &response); + + // Tell the user the address that we received their request from. + StunAddressAttribute* mapped_addr_attr; + mapped_addr_attr = new StunXorAddressAttribute( + STUN_ATTR_XOR_MAPPED_ADDRESS, conn->src()); + VERIFY(response.AddAttribute(mapped_addr_attr)); + + SendStun(conn, &response); +} + +void TurnServer::HandleAllocateRequest(Connection* conn, + const TurnMessage* msg, + const std::string& key) { + // Check the parameters in the request. + const StunUInt32Attribute* transport_attr = + msg->GetUInt32(STUN_ATTR_REQUESTED_TRANSPORT); + if (!transport_attr) { + SendErrorResponse(conn, msg, STUN_ERROR_BAD_REQUEST, + STUN_ERROR_REASON_BAD_REQUEST); + return; + } + + // Only UDP is supported right now. + int proto = transport_attr->value() >> 24; + if (proto != IPPROTO_UDP) { + SendErrorResponse(conn, msg, STUN_ERROR_UNSUPPORTED_PROTOCOL, + STUN_ERROR_REASON_UNSUPPORTED_PROTOCOL); + return; + } + + // Create the allocation and let it send the success response. + // If the actual socket allocation fails, send an internal error. + Allocation* alloc = CreateAllocation(conn, proto, key); + if (alloc) { + alloc->HandleTurnMessage(msg); + } else { + SendErrorResponse(conn, msg, STUN_ERROR_SERVER_ERROR, + "Failed to allocate socket"); + } +} + +std::string TurnServer::GenerateNonce() const { + // Generate a nonce of the form hex(now + HMAC-MD5(nonce_key_, now)) + uint32 now = talk_base::Time(); + std::string input(reinterpret_cast(&now), sizeof(now)); + std::string nonce = talk_base::hex_encode(input.c_str(), input.size()); + nonce += talk_base::ComputeHmac(talk_base::DIGEST_MD5, nonce_key_, input); + ASSERT(nonce.size() == kNonceSize); + return nonce; +} + +bool TurnServer::ValidateNonce(const std::string& nonce) const { + // Check the size. + if (nonce.size() != kNonceSize) { + return false; + } + + // Decode the timestamp. + uint32 then; + char* p = reinterpret_cast(&then); + size_t len = talk_base::hex_decode(p, sizeof(then), + nonce.substr(0, sizeof(then) * 2)); + if (len != sizeof(then)) { + return false; + } + + // Verify the HMAC. + if (nonce.substr(sizeof(then) * 2) != talk_base::ComputeHmac( + talk_base::DIGEST_MD5, nonce_key_, std::string(p, sizeof(then)))) { + return false; + } + + // Validate the timestamp. + return talk_base::TimeSince(then) < kNonceTimeout; +} + +TurnServer::Allocation* TurnServer::FindAllocation(Connection* conn) { + AllocationMap::const_iterator it = allocations_.find(*conn); + return (it != allocations_.end()) ? it->second : NULL; +} + +TurnServer::Allocation* TurnServer::CreateAllocation(Connection* conn, + int proto, + const std::string& key) { + talk_base::AsyncPacketSocket* external_socket = (external_socket_factory_) ? + external_socket_factory_->CreateUdpSocket(external_addr_, 0, 0) : NULL; + if (!external_socket) { + return NULL; + } + + // The Allocation takes ownership of the socket. + Allocation* allocation = new Allocation(this, + thread_, *conn, external_socket, key); + allocation->SignalDestroyed.connect(this, &TurnServer::OnAllocationDestroyed); + allocations_[*conn] = allocation; + return allocation; +} + +void TurnServer::SendErrorResponse(Connection* conn, + const StunMessage* req, + int code, const std::string& reason) { + TurnMessage resp; + InitErrorResponse(req, code, reason, &resp); + LOG(LS_INFO) << "Sending error response, type=" << resp.type() + << ", code=" << code << ", reason=" << reason; + SendStun(conn, &resp); +} + +void TurnServer::SendErrorResponseWithRealmAndNonce( + Connection* conn, const StunMessage* msg, + int code, const std::string& reason) { + TurnMessage resp; + InitErrorResponse(msg, code, reason, &resp); + VERIFY(resp.AddAttribute(new StunByteStringAttribute( + STUN_ATTR_NONCE, GenerateNonce()))); + VERIFY(resp.AddAttribute(new StunByteStringAttribute( + STUN_ATTR_REALM, realm_))); + SendStun(conn, &resp); +} + +void TurnServer::SendStun(Connection* conn, StunMessage* msg) { + talk_base::ByteBuffer buf; + // Add a SOFTWARE attribute if one is set. + if (!software_.empty()) { + VERIFY(msg->AddAttribute( + new StunByteStringAttribute(STUN_ATTR_SOFTWARE, software_))); + } + msg->Write(&buf); + Send(conn, buf); +} + +void TurnServer::Send(Connection* conn, + const talk_base::ByteBuffer& buf) { + conn->socket()->SendTo(buf.Data(), buf.Length(), conn->src()); +} + +void TurnServer::OnAllocationDestroyed(Allocation* allocation) { + // Removing the internal socket if the connection is not udp. + talk_base::AsyncPacketSocket* socket = allocation->conn()->socket(); + InternalSocketMap::iterator iter = server_sockets_.find(socket); + ASSERT(iter != server_sockets_.end()); + // Skip if the socket serving this allocation is UDP, as this will be shared + // by all allocations. + if (iter->second != cricket::PROTO_UDP) { + DestroyInternalSocket(socket); + } + + AllocationMap::iterator it = allocations_.find(*(allocation->conn())); + if (it != allocations_.end()) + allocations_.erase(it); +} + +void TurnServer::DestroyInternalSocket(talk_base::AsyncPacketSocket* socket) { + InternalSocketMap::iterator iter = server_sockets_.find(socket); + if (iter != server_sockets_.end()) { + talk_base::AsyncPacketSocket* socket = iter->first; + delete socket; + server_sockets_.erase(iter); + } +} + +TurnServer::Connection::Connection(const talk_base::SocketAddress& src, + ProtocolType proto, + talk_base::AsyncPacketSocket* socket) + : src_(src), + dst_(socket->GetRemoteAddress()), + proto_(proto), + socket_(socket) { +} + +bool TurnServer::Connection::operator==(const Connection& c) const { + return src_ == c.src_ && dst_ == c.dst_ && proto_ == c.proto_; +} + +bool TurnServer::Connection::operator<(const Connection& c) const { + return src_ < c.src_ || dst_ < c.dst_ || proto_ < c.proto_; +} + +std::string TurnServer::Connection::ToString() const { + const char* const kProtos[] = { + "unknown", "udp", "tcp", "ssltcp" + }; + std::ostringstream ost; + ost << src_.ToString() << "-" << dst_.ToString() << ":"<< kProtos[proto_]; + return ost.str(); +} + +TurnServer::Allocation::Allocation(TurnServer* server, + talk_base::Thread* thread, + const Connection& conn, + talk_base::AsyncPacketSocket* socket, + const std::string& key) + : server_(server), + thread_(thread), + conn_(conn), + external_socket_(socket), + key_(key) { + external_socket_->SignalReadPacket.connect( + this, &TurnServer::Allocation::OnExternalPacket); +} + +TurnServer::Allocation::~Allocation() { + for (ChannelList::iterator it = channels_.begin(); + it != channels_.end(); ++it) { + delete *it; + } + for (PermissionList::iterator it = perms_.begin(); + it != perms_.end(); ++it) { + delete *it; + } + thread_->Clear(this, MSG_TIMEOUT); + LOG_J(LS_INFO, this) << "Allocation destroyed"; +} + +std::string TurnServer::Allocation::ToString() const { + std::ostringstream ost; + ost << "Alloc[" << conn_.ToString() << "]"; + return ost.str(); +} + +void TurnServer::Allocation::HandleTurnMessage(const TurnMessage* msg) { + ASSERT(msg != NULL); + switch (msg->type()) { + case STUN_ALLOCATE_REQUEST: + HandleAllocateRequest(msg); + break; + case TURN_REFRESH_REQUEST: + HandleRefreshRequest(msg); + break; + case TURN_SEND_INDICATION: + HandleSendIndication(msg); + break; + case TURN_CREATE_PERMISSION_REQUEST: + HandleCreatePermissionRequest(msg); + break; + case TURN_CHANNEL_BIND_REQUEST: + HandleChannelBindRequest(msg); + break; + default: + // Not sure what to do with this, just eat it. + LOG_J(LS_WARNING, this) << "Invalid TURN message type received: " + << msg->type(); + } +} + +void TurnServer::Allocation::HandleAllocateRequest(const TurnMessage* msg) { + // Copy the important info from the allocate request. + transaction_id_ = msg->transaction_id(); + const StunByteStringAttribute* username_attr = + msg->GetByteString(STUN_ATTR_USERNAME); + ASSERT(username_attr != NULL); + username_ = username_attr->GetString(); + + // Figure out the lifetime and start the allocation timer. + int lifetime_secs = ComputeLifetime(msg); + thread_->PostDelayed(lifetime_secs * 1000, this, MSG_TIMEOUT); + + LOG_J(LS_INFO, this) << "Created allocation, lifetime=" << lifetime_secs; + + // We've already validated all the important bits; just send a response here. + TurnMessage response; + InitResponse(msg, &response); + + StunAddressAttribute* mapped_addr_attr = + new StunXorAddressAttribute(STUN_ATTR_XOR_MAPPED_ADDRESS, conn_.src()); + StunAddressAttribute* relayed_addr_attr = + new StunXorAddressAttribute(STUN_ATTR_XOR_RELAYED_ADDRESS, + external_socket_->GetLocalAddress()); + StunUInt32Attribute* lifetime_attr = + new StunUInt32Attribute(STUN_ATTR_LIFETIME, lifetime_secs); + VERIFY(response.AddAttribute(mapped_addr_attr)); + VERIFY(response.AddAttribute(relayed_addr_attr)); + VERIFY(response.AddAttribute(lifetime_attr)); + + SendResponse(&response); +} + +void TurnServer::Allocation::HandleRefreshRequest(const TurnMessage* msg) { + // Figure out the new lifetime. + int lifetime_secs = ComputeLifetime(msg); + + // Reset the expiration timer. + thread_->Clear(this, MSG_TIMEOUT); + thread_->PostDelayed(lifetime_secs * 1000, this, MSG_TIMEOUT); + + LOG_J(LS_INFO, this) << "Refreshed allocation, lifetime=" << lifetime_secs; + + // Send a success response with a LIFETIME attribute. + TurnMessage response; + InitResponse(msg, &response); + + StunUInt32Attribute* lifetime_attr = + new StunUInt32Attribute(STUN_ATTR_LIFETIME, lifetime_secs); + VERIFY(response.AddAttribute(lifetime_attr)); + + SendResponse(&response); +} + +void TurnServer::Allocation::HandleSendIndication(const TurnMessage* msg) { + // Check mandatory attributes. + const StunByteStringAttribute* data_attr = msg->GetByteString(STUN_ATTR_DATA); + const StunAddressAttribute* peer_attr = + msg->GetAddress(STUN_ATTR_XOR_PEER_ADDRESS); + if (!data_attr || !peer_attr) { + LOG_J(LS_WARNING, this) << "Received invalid send indication"; + return; + } + + // If a permission exists, send the data on to the peer. + if (HasPermission(peer_attr->GetAddress().ipaddr())) { + SendExternal(data_attr->bytes(), data_attr->length(), + peer_attr->GetAddress()); + } else { + LOG_J(LS_WARNING, this) << "Received send indication without permission" + << "peer=" << peer_attr->GetAddress(); + } +} + +void TurnServer::Allocation::HandleCreatePermissionRequest( + const TurnMessage* msg) { + // Check mandatory attributes. + const StunAddressAttribute* peer_attr = + msg->GetAddress(STUN_ATTR_XOR_PEER_ADDRESS); + if (!peer_attr) { + SendBadRequestResponse(msg); + return; + } + + // Add this permission. + AddPermission(peer_attr->GetAddress().ipaddr()); + + LOG_J(LS_INFO, this) << "Created permission, peer=" + << peer_attr->GetAddress(); + + // Send a success response. + TurnMessage response; + InitResponse(msg, &response); + SendResponse(&response); +} + +void TurnServer::Allocation::HandleChannelBindRequest(const TurnMessage* msg) { + // Check mandatory attributes. + const StunUInt32Attribute* channel_attr = + msg->GetUInt32(STUN_ATTR_CHANNEL_NUMBER); + const StunAddressAttribute* peer_attr = + msg->GetAddress(STUN_ATTR_XOR_PEER_ADDRESS); + if (!channel_attr || !peer_attr) { + SendBadRequestResponse(msg); + return; + } + + // Check that channel id is valid. + int channel_id = channel_attr->value() >> 16; + if (channel_id < kMinChannelNumber || channel_id > kMaxChannelNumber) { + SendBadRequestResponse(msg); + return; + } + + // Check that this channel id isn't bound to another transport address, and + // that this transport address isn't bound to another channel id. + Channel* channel1 = FindChannel(channel_id); + Channel* channel2 = FindChannel(peer_attr->GetAddress()); + if (channel1 != channel2) { + SendBadRequestResponse(msg); + return; + } + + // Add or refresh this channel. + if (!channel1) { + channel1 = new Channel(thread_, channel_id, peer_attr->GetAddress()); + channel1->SignalDestroyed.connect(this, + &TurnServer::Allocation::OnChannelDestroyed); + channels_.push_back(channel1); + } else { + channel1->Refresh(); + } + + // Channel binds also refresh permissions. + AddPermission(peer_attr->GetAddress().ipaddr()); + + LOG_J(LS_INFO, this) << "Bound channel, id=" << channel_id + << ", peer=" << peer_attr->GetAddress(); + + // Send a success response. + TurnMessage response; + InitResponse(msg, &response); + SendResponse(&response); +} + +void TurnServer::Allocation::HandleChannelData(const char* data, size_t size) { + // Extract the channel number from the data. + uint16 channel_id = talk_base::GetBE16(data); + Channel* channel = FindChannel(channel_id); + if (channel) { + // Send the data to the peer address. + SendExternal(data + TURN_CHANNEL_HEADER_SIZE, + size - TURN_CHANNEL_HEADER_SIZE, channel->peer()); + } else { + LOG_J(LS_WARNING, this) << "Received channel data for invalid channel, id=" + << channel_id; + } +} + +void TurnServer::Allocation::OnExternalPacket( + talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& addr) { + ASSERT(external_socket_.get() == socket); + Channel* channel = FindChannel(addr); + if (channel) { + // There is a channel bound to this address. Send as a channel message. + talk_base::ByteBuffer buf; + buf.WriteUInt16(channel->id()); + buf.WriteUInt16(size); + buf.WriteBytes(data, size); + server_->Send(&conn_, buf); + } else if (HasPermission(addr.ipaddr())) { + // No channel, but a permission exists. Send as a data indication. + TurnMessage msg; + msg.SetType(TURN_DATA_INDICATION); + msg.SetTransactionID( + talk_base::CreateRandomString(kStunTransactionIdLength)); + VERIFY(msg.AddAttribute(new StunXorAddressAttribute( + STUN_ATTR_XOR_PEER_ADDRESS, addr))); + VERIFY(msg.AddAttribute(new StunByteStringAttribute( + STUN_ATTR_DATA, data, size))); + server_->SendStun(&conn_, &msg); + } else { + LOG_J(LS_WARNING, this) << "Received external packet without permission, " + << "peer=" << addr; + } +} + +int TurnServer::Allocation::ComputeLifetime(const TurnMessage* msg) { + // Return the smaller of our default lifetime and the requested lifetime. + uint32 lifetime = kDefaultAllocationTimeout / 1000; // convert to seconds + const StunUInt32Attribute* lifetime_attr = msg->GetUInt32(STUN_ATTR_LIFETIME); + if (lifetime_attr && lifetime_attr->value() < lifetime) { + lifetime = lifetime_attr->value(); + } + return lifetime; +} + +bool TurnServer::Allocation::HasPermission(const talk_base::IPAddress& addr) { + return (FindPermission(addr) != NULL); +} + +void TurnServer::Allocation::AddPermission(const talk_base::IPAddress& addr) { + Permission* perm = FindPermission(addr); + if (!perm) { + perm = new Permission(thread_, addr); + perm->SignalDestroyed.connect( + this, &TurnServer::Allocation::OnPermissionDestroyed); + perms_.push_back(perm); + } else { + perm->Refresh(); + } +} + +TurnServer::Permission* TurnServer::Allocation::FindPermission( + const talk_base::IPAddress& addr) const { + for (PermissionList::const_iterator it = perms_.begin(); + it != perms_.end(); ++it) { + if ((*it)->peer() == addr) + return *it; + } + return NULL; +} + +TurnServer::Channel* TurnServer::Allocation::FindChannel(int channel_id) const { + for (ChannelList::const_iterator it = channels_.begin(); + it != channels_.end(); ++it) { + if ((*it)->id() == channel_id) + return *it; + } + return NULL; +} + +TurnServer::Channel* TurnServer::Allocation::FindChannel( + const talk_base::SocketAddress& addr) const { + for (ChannelList::const_iterator it = channels_.begin(); + it != channels_.end(); ++it) { + if ((*it)->peer() == addr) + return *it; + } + return NULL; +} + +void TurnServer::Allocation::SendResponse(TurnMessage* msg) { + // Success responses always have M-I. + msg->AddMessageIntegrity(key_); + server_->SendStun(&conn_, msg); +} + +void TurnServer::Allocation::SendBadRequestResponse(const TurnMessage* req) { + SendErrorResponse(req, STUN_ERROR_BAD_REQUEST, STUN_ERROR_REASON_BAD_REQUEST); +} + +void TurnServer::Allocation::SendErrorResponse(const TurnMessage* req, int code, + const std::string& reason) { + server_->SendErrorResponse(&conn_, req, code, reason); +} + +void TurnServer::Allocation::SendExternal(const void* data, size_t size, + const talk_base::SocketAddress& peer) { + external_socket_->SendTo(data, size, peer); +} + +void TurnServer::Allocation::OnMessage(talk_base::Message* msg) { + ASSERT(msg->message_id == MSG_TIMEOUT); + SignalDestroyed(this); + delete this; +} + +void TurnServer::Allocation::OnPermissionDestroyed(Permission* perm) { + PermissionList::iterator it = std::find(perms_.begin(), perms_.end(), perm); + ASSERT(it != perms_.end()); + perms_.erase(it); +} + +void TurnServer::Allocation::OnChannelDestroyed(Channel* channel) { + ChannelList::iterator it = + std::find(channels_.begin(), channels_.end(), channel); + ASSERT(it != channels_.end()); + channels_.erase(it); +} + +TurnServer::Permission::Permission(talk_base::Thread* thread, + const talk_base::IPAddress& peer) + : thread_(thread), peer_(peer) { + Refresh(); +} + +TurnServer::Permission::~Permission() { + thread_->Clear(this, MSG_TIMEOUT); +} + +void TurnServer::Permission::Refresh() { + thread_->Clear(this, MSG_TIMEOUT); + thread_->PostDelayed(kPermissionTimeout, this, MSG_TIMEOUT); +} + +void TurnServer::Permission::OnMessage(talk_base::Message* msg) { + ASSERT(msg->message_id == MSG_TIMEOUT); + SignalDestroyed(this); + delete this; +} + +TurnServer::Channel::Channel(talk_base::Thread* thread, int id, + const talk_base::SocketAddress& peer) + : thread_(thread), id_(id), peer_(peer) { + Refresh(); +} + +TurnServer::Channel::~Channel() { + thread_->Clear(this, MSG_TIMEOUT); +} + +void TurnServer::Channel::Refresh() { + thread_->Clear(this, MSG_TIMEOUT); + thread_->PostDelayed(kChannelTimeout, this, MSG_TIMEOUT); +} + +void TurnServer::Channel::OnMessage(talk_base::Message* msg) { + ASSERT(msg->message_id == MSG_TIMEOUT); + SignalDestroyed(this); + delete this; +} + +} // namespace cricket diff --git a/talk/p2p/base/turnserver.h b/talk/p2p/base/turnserver.h new file mode 100644 index 000000000..56ce2fcb0 --- /dev/null +++ b/talk/p2p/base/turnserver.h @@ -0,0 +1,186 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_P2P_BASE_TURNSERVER_H_ +#define TALK_P2P_BASE_TURNSERVER_H_ + +#include +#include +#include +#include + +#include "talk/base/messagequeue.h" +#include "talk/base/sigslot.h" +#include "talk/base/socketaddress.h" +#include "talk/p2p/base/portinterface.h" + +namespace talk_base { +class AsyncPacketSocket; +class ByteBuffer; +class PacketSocketFactory; +class Thread; +} + +namespace cricket { + +class StunMessage; +class TurnMessage; + +// The default server port for TURN, as specified in RFC5766. +const int TURN_SERVER_PORT = 3478; + +// An interface through which the MD5 credential hash can be retrieved. +class TurnAuthInterface { + public: + // Gets HA1 for the specified user and realm. + // HA1 = MD5(A1) = MD5(username:realm:password). + // Return true if the given username and realm are valid, or false if not. + virtual bool GetKey(const std::string& username, const std::string& realm, + std::string* key) = 0; +}; + +// The core TURN server class. Give it a socket to listen on via +// AddInternalServerSocket, and a factory to create external sockets via +// SetExternalSocketFactory, and it's ready to go. +// Not yet wired up: TCP support. +class TurnServer : public sigslot::has_slots<> { + public: + explicit TurnServer(talk_base::Thread* thread); + ~TurnServer(); + + // Gets/sets the realm value to use for the server. + const std::string& realm() const { return realm_; } + void set_realm(const std::string& realm) { realm_ = realm; } + + // Gets/sets the value for the SOFTWARE attribute for TURN messages. + const std::string& software() const { return software_; } + void set_software(const std::string& software) { software_ = software; } + + // Sets the authentication callback; does not take ownership. + void set_auth_hook(TurnAuthInterface* auth_hook) { auth_hook_ = auth_hook; } + + void set_enable_otu_nonce(bool enable) { enable_otu_nonce_ = enable; } + + // Starts listening for packets from internal clients. + void AddInternalSocket(talk_base::AsyncPacketSocket* socket, + ProtocolType proto); + // Starts listening for the connections on this socket. When someone tries + // to connect, the connection will be accepted and a new internal socket + // will be added. + void AddInternalServerSocket(talk_base::AsyncSocket* socket, + ProtocolType proto); + // Specifies the factory to use for creating external sockets. + void SetExternalSocketFactory(talk_base::PacketSocketFactory* factory, + const talk_base::SocketAddress& address); + + private: + // Encapsulates the client's connection to the server. + class Connection { + public: + Connection() : proto_(PROTO_UDP), socket_(NULL) {} + Connection(const talk_base::SocketAddress& src, + ProtocolType proto, + talk_base::AsyncPacketSocket* socket); + const talk_base::SocketAddress& src() const { return src_; } + talk_base::AsyncPacketSocket* socket() { return socket_; } + bool operator==(const Connection& t) const; + bool operator<(const Connection& t) const; + std::string ToString() const; + + private: + talk_base::SocketAddress src_; + talk_base::SocketAddress dst_; + cricket::ProtocolType proto_; + talk_base::AsyncPacketSocket* socket_; + }; + class Allocation; + class Permission; + class Channel; + typedef std::map AllocationMap; + + void OnInternalPacket(talk_base::AsyncPacketSocket* socket, const char* data, + size_t size, const talk_base::SocketAddress& address); + + void OnNewInternalConnection(talk_base::AsyncSocket* socket); + + // Accept connections on this server socket. + void AcceptConnection(talk_base::AsyncSocket* server_socket); + void OnInternalSocketClose(talk_base::AsyncPacketSocket* socket, int err); + + void HandleStunMessage(Connection* conn, const char* data, size_t size); + void HandleBindingRequest(Connection* conn, const StunMessage* msg); + void HandleAllocateRequest(Connection* conn, const TurnMessage* msg, + const std::string& key); + + bool GetKey(const StunMessage* msg, std::string* key); + bool CheckAuthorization(Connection* conn, const StunMessage* msg, + const char* data, size_t size, + const std::string& key); + std::string GenerateNonce() const; + bool ValidateNonce(const std::string& nonce) const; + + Allocation* FindAllocation(Connection* conn); + Allocation* CreateAllocation(Connection* conn, int proto, + const std::string& key); + + void SendErrorResponse(Connection* conn, const StunMessage* req, + int code, const std::string& reason); + + void SendErrorResponseWithRealmAndNonce(Connection* conn, + const StunMessage* req, + int code, + const std::string& reason); + void SendStun(Connection* conn, StunMessage* msg); + void Send(Connection* conn, const talk_base::ByteBuffer& buf); + + void OnAllocationDestroyed(Allocation* allocation); + void DestroyInternalSocket(talk_base::AsyncPacketSocket* socket); + + typedef std::map InternalSocketMap; + typedef std::map ServerSocketMap; + + talk_base::Thread* thread_; + std::string nonce_key_; + std::string realm_; + std::string software_; + TurnAuthInterface* auth_hook_; + // otu - one-time-use. Server will respond with 438 if it's + // sees the same nonce in next transaction. + bool enable_otu_nonce_; + InternalSocketMap server_sockets_; + ServerSocketMap server_listen_sockets_; + talk_base::scoped_ptr + external_socket_factory_; + talk_base::SocketAddress external_addr_; + AllocationMap allocations_; +}; + +} // namespace cricket + +#endif // TALK_P2P_BASE_TURNSERVER_H_ diff --git a/talk/p2p/base/turnserver_main.cc b/talk/p2p/base/turnserver_main.cc new file mode 100644 index 000000000..d40fede03 --- /dev/null +++ b/talk/p2p/base/turnserver_main.cc @@ -0,0 +1,102 @@ +/* + * libjingle + * Copyright 2012, 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 // NOLINT + +#include "talk/base/asyncudpsocket.h" +#include "talk/base/optionsfile.h" +#include "talk/base/thread.h" +#include "talk/base/stringencode.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/base/turnserver.h" + +static const char kSoftware[] = "libjingle TurnServer"; + +class TurnFileAuth : public cricket::TurnAuthInterface { + public: + explicit TurnFileAuth(const std::string& path) : file_(path) { + file_.Load(); + } + virtual bool GetKey(const std::string& username, const std::string& realm, + std::string* key) { + // File is stored as lines of =. + // Generate HA1 via "echo -n "::" | md5sum" + std::string hex; + bool ret = file_.GetStringValue(username, &hex); + if (ret) { + char buf[32]; + size_t len = talk_base::hex_decode(buf, sizeof(buf), hex); + *key = std::string(buf, len); + } + return ret; + } + private: + talk_base::OptionsFile file_; +}; + +int main(int argc, char **argv) { + if (argc != 5) { + std::cerr << "usage: turnserver int-addr ext-ip realm auth-file" + << std::endl; + return 1; + } + + talk_base::SocketAddress int_addr; + if (!int_addr.FromString(argv[1])) { + std::cerr << "Unable to parse IP address: " << argv[1] << std::endl; + return 1; + } + + talk_base::IPAddress ext_addr; + if (!IPFromString(argv[2], &ext_addr)) { + std::cerr << "Unable to parse IP address: " << argv[2] << std::endl; + return 1; + } + + talk_base::Thread* main = talk_base::Thread::Current(); + talk_base::AsyncUDPSocket* int_socket = + talk_base::AsyncUDPSocket::Create(main->socketserver(), int_addr); + if (!int_socket) { + std::cerr << "Failed to create a UDP socket bound at" + << int_addr.ToString() << std::endl; + return 1; + } + + cricket::TurnServer server(main); + TurnFileAuth auth(argv[4]); + server.set_realm(argv[3]); + server.set_software(kSoftware); + server.set_auth_hook(&auth); + server.AddInternalSocket(int_socket, cricket::PROTO_UDP); + server.SetExternalSocketFactory(new talk_base::BasicPacketSocketFactory(), + talk_base::SocketAddress(ext_addr, 0)); + + std::cout << "Listening internally at " << int_addr.ToString() << std::endl; + + main->Run(); + return 0; +} diff --git a/talk/p2p/base/udpport.h b/talk/p2p/base/udpport.h new file mode 100644 index 000000000..fc981fdad --- /dev/null +++ b/talk/p2p/base/udpport.h @@ -0,0 +1,34 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_BASE_UDPPORT_H_ +#define TALK_P2P_BASE_UDPPORT_H_ + +// StunPort will be handling UDPPort functionality. +#include "talk/p2p/base/stunport.h" + +#endif // TALK_P2P_BASE_UDPPORT_H_ diff --git a/talk/p2p/client/autoportallocator.h b/talk/p2p/client/autoportallocator.h new file mode 100644 index 000000000..4ec324bc1 --- /dev/null +++ b/talk/p2p/client/autoportallocator.h @@ -0,0 +1,69 @@ +/* + * libjingle + * Copyright 2010, 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. + */ + +#ifndef TALK_EXAMPLES_LOGIN_AUTOPORTALLOCATOR_H_ +#define TALK_EXAMPLES_LOGIN_AUTOPORTALLOCATOR_H_ + +#include +#include + +#include "talk/base/sigslot.h" +#include "talk/p2p/client/httpportallocator.h" +#include "talk/xmpp/jingleinfotask.h" +#include "talk/xmpp/xmppclient.h" + +// This class sets the relay and stun servers using XmppClient. +// It enables the client to traverse Proxy and NAT. +class AutoPortAllocator : public cricket::HttpPortAllocator { + public: + AutoPortAllocator(talk_base::NetworkManager* network_manager, + const std::string& user_agent) + : cricket::HttpPortAllocator(network_manager, user_agent) { + } + + // Creates and initiates a task to get relay token from XmppClient and set + // it appropriately. + void SetXmppClient(buzz::XmppClient* client) { + // The JingleInfoTask is freed by the task-runner. + buzz::JingleInfoTask* jit = new buzz::JingleInfoTask(client); + jit->SignalJingleInfo.connect(this, &AutoPortAllocator::OnJingleInfo); + jit->Start(); + jit->RefreshJingleInfoNow(); + } + + private: + void OnJingleInfo( + const std::string& token, + const std::vector& relay_hosts, + const std::vector& stun_hosts) { + SetRelayToken(token); + SetStunHosts(stun_hosts); + SetRelayHosts(relay_hosts); + } +}; + +#endif // TALK_EXAMPLES_LOGIN_AUTOPORTALLOCATOR_H_ diff --git a/talk/p2p/client/basicportallocator.cc b/talk/p2p/client/basicportallocator.cc new file mode 100644 index 000000000..7a61093c9 --- /dev/null +++ b/talk/p2p/client/basicportallocator.cc @@ -0,0 +1,1072 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/client/basicportallocator.h" + +#include +#include + +#include "talk/base/common.h" +#include "talk/base/helpers.h" +#include "talk/base/host.h" +#include "talk/base/logging.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/base/common.h" +#include "talk/p2p/base/port.h" +#include "talk/p2p/base/relayport.h" +#include "talk/p2p/base/stunport.h" +#include "talk/p2p/base/tcpport.h" +#include "talk/p2p/base/turnport.h" +#include "talk/p2p/base/udpport.h" + +using talk_base::CreateRandomId; +using talk_base::CreateRandomString; + +namespace { + +const uint32 MSG_CONFIG_START = 1; +const uint32 MSG_CONFIG_READY = 2; +const uint32 MSG_ALLOCATE = 3; +const uint32 MSG_ALLOCATION_PHASE = 4; +const uint32 MSG_SHAKE = 5; +const uint32 MSG_SEQUENCEOBJECTS_CREATED = 6; +const uint32 MSG_CONFIG_STOP = 7; + +const uint32 ALLOCATE_DELAY = 250; +const uint32 ALLOCATION_STEP_DELAY = 1 * 1000; + +const int PHASE_UDP = 0; +const int PHASE_RELAY = 1; +const int PHASE_TCP = 2; +const int PHASE_SSLTCP = 3; + +const int kNumPhases = 4; + +// Both these values are in bytes. +const int kLargeSocketSendBufferSize = 128 * 1024; +const int kNormalSocketSendBufferSize = 64 * 1024; + +const int SHAKE_MIN_DELAY = 45 * 1000; // 45 seconds +const int SHAKE_MAX_DELAY = 90 * 1000; // 90 seconds + +int ShakeDelay() { + int range = SHAKE_MAX_DELAY - SHAKE_MIN_DELAY + 1; + return SHAKE_MIN_DELAY + CreateRandomId() % range; +} + +} // namespace + +namespace cricket { + +const uint32 DISABLE_ALL_PHASES = + PORTALLOCATOR_DISABLE_UDP + | PORTALLOCATOR_DISABLE_TCP + | PORTALLOCATOR_DISABLE_STUN + | PORTALLOCATOR_DISABLE_RELAY; + +// Performs the allocation of ports, in a sequenced (timed) manner, for a given +// network and IP address. +class AllocationSequence : public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + enum State { + kInit, // Initial state. + kRunning, // Started allocating ports. + kStopped, // Stopped from running. + kCompleted, // All ports are allocated. + + // kInit --> kRunning --> {kCompleted|kStopped} + }; + + AllocationSequence(BasicPortAllocatorSession* session, + talk_base::Network* network, + PortConfiguration* config, + uint32 flags); + ~AllocationSequence(); + bool Init(); + + State state() const { return state_; } + + // Disables the phases for a new sequence that this one already covers for an + // equivalent network setup. + void DisableEquivalentPhases(talk_base::Network* network, + PortConfiguration* config, uint32* flags); + + // Starts and stops the sequence. When started, it will continue allocating + // new ports on its own timed schedule. + void Start(); + void Stop(); + + // MessageHandler + void OnMessage(talk_base::Message* msg); + + void EnableProtocol(ProtocolType proto); + bool ProtocolEnabled(ProtocolType proto) const; + + // Signal from AllocationSequence, when it's done with allocating ports. + // This signal is useful, when port allocation fails which doesn't result + // in any candidates. Using this signal BasicPortAllocatorSession can send + // its candidate discovery conclusion signal. Without this signal, + // BasicPortAllocatorSession doesn't have any event to trigger signal. This + // can also be achieved by starting timer in BPAS. + sigslot::signal1 SignalPortAllocationComplete; + + private: + typedef std::vector ProtocolList; + + bool IsFlagSet(uint32 flag) { + return ((flags_ & flag) != 0); + } + void CreateUDPPorts(); + void CreateTCPPorts(); + void CreateStunPorts(); + void CreateRelayPorts(); + void CreateGturnPort(const RelayServerConfig& config); + void CreateTurnPort(const RelayServerConfig& config); + + void OnReadPacket(talk_base::AsyncPacketSocket* socket, + const char* data, size_t size, + const talk_base::SocketAddress& remote_addr); + void OnPortDestroyed(PortInterface* port); + + BasicPortAllocatorSession* session_; + talk_base::Network* network_; + talk_base::IPAddress ip_; + PortConfiguration* config_; + State state_; + uint32 flags_; + ProtocolList protocols_; + talk_base::scoped_ptr udp_socket_; + // Keeping a list of all UDP based ports. + std::deque ports; + int phase_; +}; + +// BasicPortAllocator +BasicPortAllocator::BasicPortAllocator( + talk_base::NetworkManager* network_manager, + talk_base::PacketSocketFactory* socket_factory) + : network_manager_(network_manager), + socket_factory_(socket_factory) { + ASSERT(socket_factory_ != NULL); + Construct(); +} + +BasicPortAllocator::BasicPortAllocator( + talk_base::NetworkManager* network_manager) + : network_manager_(network_manager), + socket_factory_(NULL) { + Construct(); +} + +BasicPortAllocator::BasicPortAllocator( + talk_base::NetworkManager* network_manager, + talk_base::PacketSocketFactory* socket_factory, + const talk_base::SocketAddress& stun_address) + : network_manager_(network_manager), + socket_factory_(socket_factory), + stun_address_(stun_address) { + ASSERT(socket_factory_ != NULL); + Construct(); +} + +BasicPortAllocator::BasicPortAllocator( + talk_base::NetworkManager* network_manager, + const talk_base::SocketAddress& stun_address, + const talk_base::SocketAddress& relay_address_udp, + const talk_base::SocketAddress& relay_address_tcp, + const talk_base::SocketAddress& relay_address_ssl) + : network_manager_(network_manager), + socket_factory_(NULL), + stun_address_(stun_address) { + + RelayServerConfig config(RELAY_GTURN); + if (!relay_address_udp.IsAny()) + config.ports.push_back(ProtocolAddress(relay_address_udp, PROTO_UDP)); + if (!relay_address_tcp.IsAny()) + config.ports.push_back(ProtocolAddress(relay_address_tcp, PROTO_TCP)); + if (!relay_address_ssl.IsAny()) + config.ports.push_back(ProtocolAddress(relay_address_ssl, PROTO_SSLTCP)); + AddRelay(config); + + Construct(); +} + +void BasicPortAllocator::Construct() { + allow_tcp_listen_ = true; +} + +BasicPortAllocator::~BasicPortAllocator() { +} + +PortAllocatorSession *BasicPortAllocator::CreateSessionInternal( + const std::string& content_name, int component, + const std::string& ice_ufrag, const std::string& ice_pwd) { + return new BasicPortAllocatorSession(this, content_name, component, + ice_ufrag, ice_pwd); +} + +// BasicPortAllocatorSession +BasicPortAllocatorSession::BasicPortAllocatorSession( + BasicPortAllocator *allocator, + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd) + : PortAllocatorSession(content_name, component, + ice_ufrag, ice_pwd, allocator->flags()), + allocator_(allocator), network_thread_(NULL), + socket_factory_(allocator->socket_factory()), + configuration_done_(false), + allocation_started_(false), + network_manager_started_(false), + running_(false), + allocation_sequences_created_(false) { + allocator_->network_manager()->SignalNetworksChanged.connect( + this, &BasicPortAllocatorSession::OnNetworksChanged); + allocator_->network_manager()->StartUpdating(); +} + +BasicPortAllocatorSession::~BasicPortAllocatorSession() { + allocator_->network_manager()->StopUpdating(); + if (network_thread_ != NULL) + network_thread_->Clear(this); + + std::vector::iterator it; + for (it = ports_.begin(); it != ports_.end(); it++) + delete it->port(); + + for (uint32 i = 0; i < configs_.size(); ++i) + delete configs_[i]; + + for (uint32 i = 0; i < sequences_.size(); ++i) + delete sequences_[i]; +} + +void BasicPortAllocatorSession::StartGettingPorts() { + network_thread_ = talk_base::Thread::Current(); + if (!socket_factory_) { + owned_socket_factory_.reset( + new talk_base::BasicPacketSocketFactory(network_thread_)); + socket_factory_ = owned_socket_factory_.get(); + } + + running_ = true; + network_thread_->Post(this, MSG_CONFIG_START); + + if (flags() & PORTALLOCATOR_ENABLE_SHAKER) + network_thread_->PostDelayed(ShakeDelay(), this, MSG_SHAKE); +} + +void BasicPortAllocatorSession::StopGettingPorts() { + ASSERT(talk_base::Thread::Current() == network_thread_); + running_ = false; + network_thread_->Clear(this, MSG_ALLOCATE); + for (uint32 i = 0; i < sequences_.size(); ++i) + sequences_[i]->Stop(); + network_thread_->Post(this, MSG_CONFIG_STOP); +} + +void BasicPortAllocatorSession::OnMessage(talk_base::Message *message) { + switch (message->message_id) { + case MSG_CONFIG_START: + ASSERT(talk_base::Thread::Current() == network_thread_); + GetPortConfigurations(); + break; + + case MSG_CONFIG_READY: + ASSERT(talk_base::Thread::Current() == network_thread_); + OnConfigReady(static_cast(message->pdata)); + break; + + case MSG_ALLOCATE: + ASSERT(talk_base::Thread::Current() == network_thread_); + OnAllocate(); + break; + + case MSG_SHAKE: + ASSERT(talk_base::Thread::Current() == network_thread_); + OnShake(); + break; + case MSG_SEQUENCEOBJECTS_CREATED: + ASSERT(talk_base::Thread::Current() == network_thread_); + OnAllocationSequenceObjectsCreated(); + break; + case MSG_CONFIG_STOP: + ASSERT(talk_base::Thread::Current() == network_thread_); + OnConfigStop(); + break; + default: + ASSERT(false); + } +} + +void BasicPortAllocatorSession::GetPortConfigurations() { + PortConfiguration* config = new PortConfiguration(allocator_->stun_address(), + username(), + password()); + + for (size_t i = 0; i < allocator_->relays().size(); ++i) { + config->AddRelay(allocator_->relays()[i]); + } + ConfigReady(config); +} + +void BasicPortAllocatorSession::ConfigReady(PortConfiguration* config) { + network_thread_->Post(this, MSG_CONFIG_READY, config); +} + +// Adds a configuration to the list. +void BasicPortAllocatorSession::OnConfigReady(PortConfiguration* config) { + if (config) + configs_.push_back(config); + + AllocatePorts(); +} + +void BasicPortAllocatorSession::OnConfigStop() { + ASSERT(talk_base::Thread::Current() == network_thread_); + + // If any of the allocated ports have not completed the candidates allocation, + // mark those as error. Since session doesn't need any new candidates + // at this stage of the allocation, it's safe to discard any new candidates. + bool send_signal = false; + for (std::vector::iterator it = ports_.begin(); + it != ports_.end(); ++it) { + if (!it->complete()) { + // Updating port state to error, which didn't finish allocating candidates + // yet. + it->set_error(); + send_signal = true; + } + } + + // Did we stop any running sequences? + for (std::vector::iterator it = sequences_.begin(); + it != sequences_.end() && !send_signal; ++it) { + if ((*it)->state() == AllocationSequence::kStopped) { + send_signal = true; + } + } + + // If we stopped anything that was running, send a done signal now. + if (send_signal) { + MaybeSignalCandidatesAllocationDone(); + } +} + +void BasicPortAllocatorSession::AllocatePorts() { + ASSERT(talk_base::Thread::Current() == network_thread_); + network_thread_->Post(this, MSG_ALLOCATE); +} + +void BasicPortAllocatorSession::OnAllocate() { + if (network_manager_started_) + DoAllocate(); + + allocation_started_ = true; + if (running_) + network_thread_->PostDelayed(ALLOCATE_DELAY, this, MSG_ALLOCATE); +} + +// For each network, see if we have a sequence that covers it already. If not, +// create a new sequence to create the appropriate ports. +void BasicPortAllocatorSession::DoAllocate() { + bool done_signal_needed = false; + std::vector networks; + allocator_->network_manager()->GetNetworks(&networks); + if (networks.empty()) { + LOG(LS_WARNING) << "Machine has no networks; no ports will be allocated"; + done_signal_needed = true; + } else { + for (uint32 i = 0; i < networks.size(); ++i) { + PortConfiguration* config = NULL; + if (configs_.size() > 0) + config = configs_.back(); + + uint32 sequence_flags = flags(); + if ((sequence_flags & DISABLE_ALL_PHASES) == DISABLE_ALL_PHASES) { + // If all the ports are disabled we should just fire the allocation + // done event and return. + done_signal_needed = true; + break; + } + + // Disables phases that are not specified in this config. + if (!config || config->stun_address.IsNil()) { + // No STUN ports specified in this config. + sequence_flags |= PORTALLOCATOR_DISABLE_STUN; + } + if (!config || config->relays.empty()) { + // No relay ports specified in this config. + sequence_flags |= PORTALLOCATOR_DISABLE_RELAY; + } + + if (!(sequence_flags & PORTALLOCATOR_ENABLE_IPV6) && + networks[i]->ip().family() == AF_INET6) { + // Skip IPv6 networks unless the flag's been set. + continue; + } + + // Disable phases that would only create ports equivalent to + // ones that we have already made. + DisableEquivalentPhases(networks[i], config, &sequence_flags); + + if ((sequence_flags & DISABLE_ALL_PHASES) == DISABLE_ALL_PHASES) { + // New AllocationSequence would have nothing to do, so don't make it. + continue; + } + + AllocationSequence* sequence = + new AllocationSequence(this, networks[i], config, sequence_flags); + if (!sequence->Init()) { + delete sequence; + continue; + } + done_signal_needed = true; + sequence->SignalPortAllocationComplete.connect( + this, &BasicPortAllocatorSession::OnPortAllocationComplete); + if (running_) + sequence->Start(); + sequences_.push_back(sequence); + } + } + if (done_signal_needed) { + network_thread_->Post(this, MSG_SEQUENCEOBJECTS_CREATED); + } +} + +void BasicPortAllocatorSession::OnNetworksChanged() { + network_manager_started_ = true; + if (allocation_started_) + DoAllocate(); +} + +void BasicPortAllocatorSession::DisableEquivalentPhases( + talk_base::Network* network, PortConfiguration* config, uint32* flags) { + for (uint32 i = 0; i < sequences_.size() && + (*flags & DISABLE_ALL_PHASES) != DISABLE_ALL_PHASES; ++i) { + sequences_[i]->DisableEquivalentPhases(network, config, flags); + } +} + +void BasicPortAllocatorSession::AddAllocatedPort(Port* port, + AllocationSequence * seq, + bool prepare_address) { + if (!port) + return; + + LOG(LS_INFO) << "Adding allocated port for " << content_name(); + port->set_content_name(content_name()); + port->set_component(component_); + port->set_generation(generation()); + if (allocator_->proxy().type != talk_base::PROXY_NONE) + port->set_proxy(allocator_->user_agent(), allocator_->proxy()); + port->set_send_retransmit_count_attribute((allocator_->flags() & + PORTALLOCATOR_ENABLE_STUN_RETRANSMIT_ATTRIBUTE) != 0); + + if (content_name().compare(CN_VIDEO) == 0 && + component_ == cricket::ICE_CANDIDATE_COMPONENT_RTP) { + // For video RTP alone, we set send-buffer sizes. This used to be set in the + // engines/channels. + int sendBufSize = (flags() & PORTALLOCATOR_USE_LARGE_SOCKET_SEND_BUFFERS) + ? kLargeSocketSendBufferSize + : kNormalSocketSendBufferSize; + port->SetOption(talk_base::Socket::OPT_SNDBUF, sendBufSize); + } + + PortData data(port, seq); + ports_.push_back(data); + + port->SignalCandidateReady.connect( + this, &BasicPortAllocatorSession::OnCandidateReady); + port->SignalPortComplete.connect(this, + &BasicPortAllocatorSession::OnPortComplete); + port->SignalDestroyed.connect(this, + &BasicPortAllocatorSession::OnPortDestroyed); + port->SignalPortError.connect( + this, &BasicPortAllocatorSession::OnPortError); + LOG_J(LS_INFO, port) << "Added port to allocator"; + + if (prepare_address) + port->PrepareAddress(); + if (running_) + port->Start(); +} + +void BasicPortAllocatorSession::OnAllocationSequenceObjectsCreated() { + allocation_sequences_created_ = true; + // Send candidate allocation complete signal if we have no sequences. + MaybeSignalCandidatesAllocationDone(); +} + +void BasicPortAllocatorSession::OnCandidateReady( + Port* port, const Candidate& c) { + ASSERT(talk_base::Thread::Current() == network_thread_); + PortData* data = FindPort(port); + ASSERT(data != NULL); + // Discarding any candidate signal if port allocation status is + // already in completed state. + if (data->complete()) + return; + + // Send candidates whose protocol is enabled. + std::vector candidates; + ProtocolType pvalue; + if (StringToProto(c.protocol().c_str(), &pvalue) && + data->sequence()->ProtocolEnabled(pvalue)) { + candidates.push_back(c); + } + + if (!candidates.empty()) { + SignalCandidatesReady(this, candidates); + } + + // Moving to READY state as we have atleast one candidate from the port. + // Since this port has atleast one candidate we should forward this port + // to listners, to allow connections from this port. + if (!data->ready()) { + data->set_ready(); + SignalPortReady(this, port); + } +} + +void BasicPortAllocatorSession::OnPortComplete(Port* port) { + ASSERT(talk_base::Thread::Current() == network_thread_); + PortData* data = FindPort(port); + ASSERT(data != NULL); + + // Ignore any late signals. + if (data->complete()) + return; + + // Moving to COMPLETE state. + data->set_complete(); + // Send candidate allocation complete signal if this was the last port. + MaybeSignalCandidatesAllocationDone(); +} + +void BasicPortAllocatorSession::OnPortError(Port* port) { + ASSERT(talk_base::Thread::Current() == network_thread_); + PortData* data = FindPort(port); + ASSERT(data != NULL); + // We might have already given up on this port and stopped it. + if (data->complete()) + return; + + // SignalAddressError is currently sent from StunPort/TurnPort. + // But this signal itself is generic. + data->set_error(); + // Send candidate allocation complete signal if this was the last port. + MaybeSignalCandidatesAllocationDone(); +} + +void BasicPortAllocatorSession::OnProtocolEnabled(AllocationSequence* seq, + ProtocolType proto) { + std::vector candidates; + for (std::vector::iterator it = ports_.begin(); + it != ports_.end(); ++it) { + if (it->sequence() != seq) + continue; + + const std::vector& potentials = it->port()->Candidates(); + for (size_t i = 0; i < potentials.size(); ++i) { + ProtocolType pvalue; + if (!StringToProto(potentials[i].protocol().c_str(), &pvalue)) + continue; + if (pvalue == proto) { + candidates.push_back(potentials[i]); + } + } + } + + if (!candidates.empty()) { + SignalCandidatesReady(this, candidates); + } +} + +void BasicPortAllocatorSession::OnPortAllocationComplete( + AllocationSequence* seq) { + // Send candidate allocation complete signal if all ports are done. + MaybeSignalCandidatesAllocationDone(); +} + +void BasicPortAllocatorSession::MaybeSignalCandidatesAllocationDone() { + // Send signal only if all required AllocationSequence objects + // are created. + if (!allocation_sequences_created_) + return; + + // Check that all port allocation sequences are complete. + for (std::vector::iterator it = sequences_.begin(); + it != sequences_.end(); ++it) { + if ((*it)->state() == AllocationSequence::kRunning) + return; + } + + // If all allocated ports are in complete state, session must have got all + // expected candidates. Session will trigger candidates allocation complete + // signal. + for (std::vector::iterator it = ports_.begin(); + it != ports_.end(); ++it) { + if (!it->complete()) + return; + } + LOG(LS_INFO) << "All candidates gathered for " << content_name_ << ":" + << component_ << ":" << generation(); + SignalCandidatesAllocationDone(this); +} + +void BasicPortAllocatorSession::OnPortDestroyed( + PortInterface* port) { + ASSERT(talk_base::Thread::Current() == network_thread_); + for (std::vector::iterator iter = ports_.begin(); + iter != ports_.end(); ++iter) { + if (port == iter->port()) { + ports_.erase(iter); + LOG_J(LS_INFO, port) << "Removed port from allocator (" + << static_cast(ports_.size()) << " remaining)"; + return; + } + } + ASSERT(false); +} + +void BasicPortAllocatorSession::OnShake() { + LOG(INFO) << ">>>>> SHAKE <<<<< >>>>> SHAKE <<<<< >>>>> SHAKE <<<<<"; + + std::vector ports; + std::vector connections; + + for (size_t i = 0; i < ports_.size(); ++i) { + if (ports_[i].ready()) + ports.push_back(ports_[i].port()); + } + + for (size_t i = 0; i < ports.size(); ++i) { + Port::AddressMap::const_iterator iter; + for (iter = ports[i]->connections().begin(); + iter != ports[i]->connections().end(); + ++iter) { + connections.push_back(iter->second); + } + } + + LOG(INFO) << ">>>>> Destroying " << ports.size() << " ports and " + << connections.size() << " connections"; + + for (size_t i = 0; i < connections.size(); ++i) + connections[i]->Destroy(); + + if (running_ || (ports.size() > 0) || (connections.size() > 0)) + network_thread_->PostDelayed(ShakeDelay(), this, MSG_SHAKE); +} + +BasicPortAllocatorSession::PortData* BasicPortAllocatorSession::FindPort( + Port* port) { + for (std::vector::iterator it = ports_.begin(); + it != ports_.end(); ++it) { + if (it->port() == port) { + return &*it; + } + } + return NULL; +} + +// AllocationSequence + +AllocationSequence::AllocationSequence(BasicPortAllocatorSession* session, + talk_base::Network* network, + PortConfiguration* config, + uint32 flags) + : session_(session), + network_(network), + ip_(network->ip()), + config_(config), + state_(kInit), + flags_(flags), + udp_socket_(NULL), + phase_(0) { +} + +bool AllocationSequence::Init() { + if (IsFlagSet(PORTALLOCATOR_ENABLE_SHARED_SOCKET) && + !IsFlagSet(PORTALLOCATOR_ENABLE_SHARED_UFRAG)) { + LOG(LS_ERROR) << "Shared socket option can't be set without " + << "shared ufrag."; + ASSERT(false); + return false; + } + + if (IsFlagSet(PORTALLOCATOR_ENABLE_SHARED_SOCKET)) { + udp_socket_.reset(session_->socket_factory()->CreateUdpSocket( + talk_base::SocketAddress(ip_, 0), session_->allocator()->min_port(), + session_->allocator()->max_port())); + if (udp_socket_) { + udp_socket_->SignalReadPacket.connect( + this, &AllocationSequence::OnReadPacket); + } + // Continuing if |udp_socket_| is NULL, as local TCP and RelayPort using TCP + // are next available options to setup a communication channel. + } + return true; +} + +AllocationSequence::~AllocationSequence() { + session_->network_thread()->Clear(this); +} + +void AllocationSequence::DisableEquivalentPhases(talk_base::Network* network, + PortConfiguration* config, uint32* flags) { + if (!((network == network_) && (ip_ == network->ip()))) { + // Different network setup; nothing is equivalent. + return; + } + + // Else turn off the stuff that we've already got covered. + + // Every config implicitly specifies local, so turn that off right away. + *flags |= PORTALLOCATOR_DISABLE_UDP; + *flags |= PORTALLOCATOR_DISABLE_TCP; + + if (config_ && config) { + if (config_->stun_address == config->stun_address) { + // Already got this STUN server covered. + *flags |= PORTALLOCATOR_DISABLE_STUN; + } + if (!config_->relays.empty()) { + // Already got relays covered. + // NOTE: This will even skip a _different_ set of relay servers if we + // were to be given one, but that never happens in our codebase. Should + // probably get rid of the list in PortConfiguration and just keep a + // single relay server in each one. + *flags |= PORTALLOCATOR_DISABLE_RELAY; + } + } +} + +void AllocationSequence::Start() { + state_ = kRunning; + session_->network_thread()->Post(this, MSG_ALLOCATION_PHASE); +} + +void AllocationSequence::Stop() { + // If the port is completed, don't set it to stopped. + if (state_ == kRunning) { + state_ = kStopped; + session_->network_thread()->Clear(this, MSG_ALLOCATION_PHASE); + } +} + +void AllocationSequence::OnMessage(talk_base::Message* msg) { + ASSERT(talk_base::Thread::Current() == session_->network_thread()); + ASSERT(msg->message_id == MSG_ALLOCATION_PHASE); + + const char* const PHASE_NAMES[kNumPhases] = { + "Udp", "Relay", "Tcp", "SslTcp" + }; + + // Perform all of the phases in the current step. + LOG_J(LS_INFO, network_) << "Allocation Phase=" + << PHASE_NAMES[phase_]; + + switch (phase_) { + case PHASE_UDP: + CreateUDPPorts(); + CreateStunPorts(); + EnableProtocol(PROTO_UDP); + break; + + case PHASE_RELAY: + CreateRelayPorts(); + break; + + case PHASE_TCP: + CreateTCPPorts(); + EnableProtocol(PROTO_TCP); + break; + + case PHASE_SSLTCP: + state_ = kCompleted; + EnableProtocol(PROTO_SSLTCP); + break; + + default: + ASSERT(false); + } + + if (state() == kRunning) { + ++phase_; + session_->network_thread()->PostDelayed( + session_->allocator()->step_delay(), + this, MSG_ALLOCATION_PHASE); + } else { + // If all phases in AllocationSequence are completed, no allocation + // steps needed further. Canceling pending signal. + session_->network_thread()->Clear(this, MSG_ALLOCATION_PHASE); + SignalPortAllocationComplete(this); + } +} + +void AllocationSequence::EnableProtocol(ProtocolType proto) { + if (!ProtocolEnabled(proto)) { + protocols_.push_back(proto); + session_->OnProtocolEnabled(this, proto); + } +} + +bool AllocationSequence::ProtocolEnabled(ProtocolType proto) const { + for (ProtocolList::const_iterator it = protocols_.begin(); + it != protocols_.end(); ++it) { + if (*it == proto) + return true; + } + return false; +} + +void AllocationSequence::CreateUDPPorts() { + if (IsFlagSet(PORTALLOCATOR_DISABLE_UDP)) { + LOG(LS_VERBOSE) << "AllocationSequence: UDP ports disabled, skipping."; + return; + } + + // TODO(mallinath) - Remove UDPPort creating socket after shared socket + // is enabled completely. + UDPPort* port = NULL; + if (IsFlagSet(PORTALLOCATOR_ENABLE_SHARED_SOCKET) && udp_socket_) { + port = UDPPort::Create(session_->network_thread(), network_, + udp_socket_.get(), + session_->username(), session_->password()); + } else { + port = UDPPort::Create(session_->network_thread(), + session_->socket_factory(), + network_, ip_, + session_->allocator()->min_port(), + session_->allocator()->max_port(), + session_->username(), session_->password()); + } + + if (port) { + ports.push_back(port); + // If shared socket is enabled, STUN candidate will be allocated by the + // UDPPort. + if (IsFlagSet(PORTALLOCATOR_ENABLE_SHARED_SOCKET) && + !IsFlagSet(PORTALLOCATOR_DISABLE_STUN)) { + ASSERT(config_ && !config_->stun_address.IsNil()); + if (!(config_ && !config_->stun_address.IsNil())) { + LOG(LS_WARNING) + << "AllocationSequence: No STUN server configured, skipping."; + return; + } + port->set_server_addr(config_->stun_address); + } + + session_->AddAllocatedPort(port, this, true); + port->SignalDestroyed.connect(this, &AllocationSequence::OnPortDestroyed); + } +} + +void AllocationSequence::CreateTCPPorts() { + if (IsFlagSet(PORTALLOCATOR_DISABLE_TCP)) { + LOG(LS_VERBOSE) << "AllocationSequence: TCP ports disabled, skipping."; + return; + } + + Port* port = TCPPort::Create(session_->network_thread(), + session_->socket_factory(), + network_, ip_, + session_->allocator()->min_port(), + session_->allocator()->max_port(), + session_->username(), session_->password(), + session_->allocator()->allow_tcp_listen()); + if (port) { + session_->AddAllocatedPort(port, this, true); + // Since TCPPort is not created using shared socket, |port| will not be + // added to the dequeue. + } +} + +void AllocationSequence::CreateStunPorts() { + if (IsFlagSet(PORTALLOCATOR_DISABLE_STUN)) { + LOG(LS_VERBOSE) << "AllocationSequence: STUN ports disabled, skipping."; + return; + } + + if (IsFlagSet(PORTALLOCATOR_ENABLE_SHARED_SOCKET)) { + LOG(LS_INFO) << "AllocationSequence: " + << "UDPPort will be handling the STUN candidate generation."; + return; + } + + // If BasicPortAllocatorSession::OnAllocate left STUN ports enabled then we + // ought to have an address for them here. + ASSERT(config_ && !config_->stun_address.IsNil()); + if (!(config_ && !config_->stun_address.IsNil())) { + LOG(LS_WARNING) + << "AllocationSequence: No STUN server configured, skipping."; + return; + } + + StunPort* port = StunPort::Create(session_->network_thread(), + session_->socket_factory(), + network_, ip_, + session_->allocator()->min_port(), + session_->allocator()->max_port(), + session_->username(), session_->password(), + config_->stun_address); + if (port) { + session_->AddAllocatedPort(port, this, true); + // Since StunPort is not created using shared socket, |port| will not be + // added to the dequeue. + } +} + +void AllocationSequence::CreateRelayPorts() { + if (IsFlagSet(PORTALLOCATOR_DISABLE_RELAY)) { + LOG(LS_VERBOSE) << "AllocationSequence: Relay ports disabled, skipping."; + return; + } + + // If BasicPortAllocatorSession::OnAllocate left relay ports enabled then we + // ought to have a relay list for them here. + ASSERT(config_ && !config_->relays.empty()); + if (!(config_ && !config_->relays.empty())) { + LOG(LS_WARNING) + << "AllocationSequence: No relay server configured, skipping."; + return; + } + + PortConfiguration::RelayList::const_iterator relay; + for (relay = config_->relays.begin(); + relay != config_->relays.end(); ++relay) { + if (relay->type == RELAY_GTURN) { + CreateGturnPort(*relay); + } else if (relay->type == RELAY_TURN) { + CreateTurnPort(*relay); + } else { + ASSERT(false); + } + } +} + +void AllocationSequence::CreateGturnPort(const RelayServerConfig& config) { + // TODO(mallinath) - Rename RelayPort to GTurnPort. + RelayPort* port = RelayPort::Create(session_->network_thread(), + session_->socket_factory(), + network_, ip_, + session_->allocator()->min_port(), + session_->allocator()->max_port(), + config_->username, config_->password); + if (port) { + // Since RelayPort is not created using shared socket, |port| will not be + // added to the dequeue. + // Note: We must add the allocated port before we add addresses because + // the latter will create candidates that need name and preference + // settings. However, we also can't prepare the address (normally + // done by AddAllocatedPort) until we have these addresses. So we + // wait to do that until below. + session_->AddAllocatedPort(port, this, false); + + // Add the addresses of this protocol. + PortList::const_iterator relay_port; + for (relay_port = config.ports.begin(); + relay_port != config.ports.end(); + ++relay_port) { + port->AddServerAddress(*relay_port); + port->AddExternalAddress(*relay_port); + } + // Start fetching an address for this port. + port->PrepareAddress(); + } +} + +void AllocationSequence::CreateTurnPort(const RelayServerConfig& config) { + PortList::const_iterator relay_port; + for (relay_port = config.ports.begin(); + relay_port != config.ports.end(); ++relay_port) { + TurnPort* port = TurnPort::Create(session_->network_thread(), + session_->socket_factory(), + network_, ip_, + session_->allocator()->min_port(), + session_->allocator()->max_port(), + session_->username(), + session_->password(), + *relay_port, config.credentials); + if (port) { + session_->AddAllocatedPort(port, this, true); + } + } +} + +void AllocationSequence::OnReadPacket( + talk_base::AsyncPacketSocket* socket, const char* data, size_t size, + const talk_base::SocketAddress& remote_addr) { + ASSERT(socket == udp_socket_.get()); + for (std::deque::iterator iter = ports.begin(); + iter != ports.end(); ++iter) { + // We have only one port in the queue. + // TODO(mallinath) - Add shared socket support to Relay and Turn ports. + if ((*iter)->HandleIncomingPacket(socket, data, size, remote_addr)) { + break; + } + } +} + +void AllocationSequence::OnPortDestroyed(PortInterface* port) { + std::deque::iterator iter = + std::find(ports.begin(), ports.end(), port); + ASSERT(iter != ports.end()); + ports.erase(iter); +} + +// PortConfiguration +PortConfiguration::PortConfiguration( + const talk_base::SocketAddress& stun_address, + const std::string& username, + const std::string& password) + : stun_address(stun_address), + username(username), + password(password) { +} + +void PortConfiguration::AddRelay(const RelayServerConfig& config) { + relays.push_back(config); +} + +bool PortConfiguration::SupportsProtocol( + const RelayServerConfig& relay, ProtocolType type) { + PortList::const_iterator relay_port; + for (relay_port = relay.ports.begin(); + relay_port != relay.ports.end(); + ++relay_port) { + if (relay_port->proto == type) + return true; + } + return false; +} + +} // namespace cricket diff --git a/talk/p2p/client/basicportallocator.h b/talk/p2p/client/basicportallocator.h new file mode 100644 index 000000000..0d5f64285 --- /dev/null +++ b/talk/p2p/client/basicportallocator.h @@ -0,0 +1,249 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_CLIENT_BASICPORTALLOCATOR_H_ +#define TALK_P2P_CLIENT_BASICPORTALLOCATOR_H_ + +#include +#include + +#include "talk/base/messagequeue.h" +#include "talk/base/network.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/port.h" +#include "talk/p2p/base/portallocator.h" + +namespace cricket { + +struct RelayCredentials { + RelayCredentials() {} + RelayCredentials(const std::string& username, + const std::string& password) + : username(username), + password(password) { + } + + std::string username; + std::string password; +}; + +typedef std::vector PortList; +struct RelayServerConfig { + RelayServerConfig(RelayType type) : type(type) {} + + RelayType type; + PortList ports; + RelayCredentials credentials; +}; + +class BasicPortAllocator : public PortAllocator { + public: + BasicPortAllocator(talk_base::NetworkManager* network_manager, + talk_base::PacketSocketFactory* socket_factory); + explicit BasicPortAllocator(talk_base::NetworkManager* network_manager); + BasicPortAllocator(talk_base::NetworkManager* network_manager, + talk_base::PacketSocketFactory* socket_factory, + const talk_base::SocketAddress& stun_server); + BasicPortAllocator(talk_base::NetworkManager* network_manager, + const talk_base::SocketAddress& stun_server, + const talk_base::SocketAddress& relay_server_udp, + const talk_base::SocketAddress& relay_server_tcp, + const talk_base::SocketAddress& relay_server_ssl); + virtual ~BasicPortAllocator(); + + talk_base::NetworkManager* network_manager() { return network_manager_; } + + // If socket_factory() is set to NULL each PortAllocatorSession + // creates its own socket factory. + talk_base::PacketSocketFactory* socket_factory() { return socket_factory_; } + + const talk_base::SocketAddress& stun_address() const { + return stun_address_; + } + + const std::vector& relays() const { + return relays_; + } + virtual void AddRelay(const RelayServerConfig& relay) { + relays_.push_back(relay); + } + + virtual PortAllocatorSession* CreateSessionInternal( + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd); + + bool allow_tcp_listen() const { + return allow_tcp_listen_; + } + void set_allow_tcp_listen(bool allow_tcp_listen) { + allow_tcp_listen_ = allow_tcp_listen; + } + + private: + void Construct(); + + talk_base::NetworkManager* network_manager_; + talk_base::PacketSocketFactory* socket_factory_; + const talk_base::SocketAddress stun_address_; + std::vector relays_; + bool allow_tcp_listen_; +}; + +struct PortConfiguration; +class AllocationSequence; + +class BasicPortAllocatorSession : public PortAllocatorSession, + public talk_base::MessageHandler { + public: + BasicPortAllocatorSession(BasicPortAllocator* allocator, + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd); + ~BasicPortAllocatorSession(); + + virtual BasicPortAllocator* allocator() { return allocator_; } + talk_base::Thread* network_thread() { return network_thread_; } + talk_base::PacketSocketFactory* socket_factory() { return socket_factory_; } + + virtual void StartGettingPorts(); + virtual void StopGettingPorts(); + virtual bool IsGettingPorts() { return running_; } + + protected: + // Starts the process of getting the port configurations. + virtual void GetPortConfigurations(); + + // Adds a port configuration that is now ready. Once we have one for each + // network (or a timeout occurs), we will start allocating ports. + virtual void ConfigReady(PortConfiguration* config); + + // MessageHandler. Can be overriden if message IDs do not conflict. + virtual void OnMessage(talk_base::Message *message); + + private: + class PortData { + public: + PortData() : port_(NULL), sequence_(NULL), state_(STATE_INIT) {} + PortData(Port* port, AllocationSequence* seq) + : port_(port), sequence_(seq), state_(STATE_INIT) { + } + + Port* port() { return port_; } + AllocationSequence* sequence() { return sequence_; } + bool ready() const { return state_ == STATE_READY; } + bool complete() const { + // Returns true if candidate allocation has completed one way or another. + return ((state_ == STATE_COMPLETE) || (state_ == STATE_ERROR)); + } + + void set_ready() { ASSERT(state_ == STATE_INIT); state_ = STATE_READY; } + void set_complete() { + ASSERT(state_ == STATE_READY); + state_ = STATE_COMPLETE; + } + void set_error() { + ASSERT(state_ == STATE_INIT || state_ == STATE_READY); + state_ = STATE_ERROR; + } + + private: + enum State { + STATE_INIT, // No candidates allocated yet. + STATE_READY, // At least one candidate is ready for process. + STATE_COMPLETE, // All candidates allocated and ready for process. + STATE_ERROR // Error in gathering candidates. + }; + Port* port_; + AllocationSequence* sequence_; + State state_; + }; + + void OnConfigReady(PortConfiguration* config); + void OnConfigStop(); + void AllocatePorts(); + void OnAllocate(); + void DoAllocate(); + void OnNetworksChanged(); + void OnAllocationSequenceObjectsCreated(); + void DisableEquivalentPhases(talk_base::Network* network, + PortConfiguration* config, uint32* flags); + void AddAllocatedPort(Port* port, AllocationSequence* seq, + bool prepare_address); + void OnCandidateReady(Port* port, const Candidate& c); + void OnPortComplete(Port* port); + void OnPortError(Port* port); + void OnProtocolEnabled(AllocationSequence* seq, ProtocolType proto); + void OnPortDestroyed(PortInterface* port); + void OnShake(); + void MaybeSignalCandidatesAllocationDone(); + void OnPortAllocationComplete(AllocationSequence* seq); + PortData* FindPort(Port* port); + + BasicPortAllocator* allocator_; + talk_base::Thread* network_thread_; + talk_base::scoped_ptr owned_socket_factory_; + talk_base::PacketSocketFactory* socket_factory_; + bool configuration_done_; + bool allocation_started_; + bool network_manager_started_; + bool running_; // set when StartGetAllPorts is called + bool allocation_sequences_created_; + std::vector configs_; + std::vector sequences_; + std::vector ports_; + + friend class AllocationSequence; +}; + +// Records configuration information useful in creating ports. +struct PortConfiguration : public talk_base::MessageData { + talk_base::SocketAddress stun_address; + std::string username; + std::string password; + + typedef std::vector RelayList; + RelayList relays; + + PortConfiguration(const talk_base::SocketAddress& stun_address, + const std::string& username, + const std::string& password); + + // Adds another relay server, with the given ports and modifier, to the list. + void AddRelay(const RelayServerConfig& config); + + // Determines whether the given relay server supports the given protocol. + static bool SupportsProtocol(const RelayServerConfig& relay, + ProtocolType type); +}; + +} // namespace cricket + +#endif // TALK_P2P_CLIENT_BASICPORTALLOCATOR_H_ diff --git a/talk/p2p/client/connectivitychecker.cc b/talk/p2p/client/connectivitychecker.cc new file mode 100644 index 000000000..4cc73af76 --- /dev/null +++ b/talk/p2p/client/connectivitychecker.cc @@ -0,0 +1,516 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + + +#include + +#include "talk/p2p/client/connectivitychecker.h" + +#include "talk/base/asynchttprequest.h" +#include "talk/base/autodetectproxy.h" +#include "talk/base/helpers.h" +#include "talk/base/httpcommon.h" +#include "talk/base/httpcommon-inl.h" +#include "talk/base/logging.h" +#include "talk/base/proxydetect.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/candidate.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/common.h" +#include "talk/p2p/base/port.h" +#include "talk/p2p/base/relayport.h" +#include "talk/p2p/base/stunport.h" + +namespace cricket { + +static const char kSessionTypeVideo[] = + "http://www.google.com/session/video"; +static const char kSessionNameRtp[] = "rtp"; + +static const char kDefaultStunHostname[] = "stun.l.google.com"; +static const int kDefaultStunPort = 19302; + +// Default maximum time in milliseconds we will wait for connections. +static const uint32 kDefaultTimeoutMs = 3000; + +enum { + MSG_START = 1, + MSG_STOP = 2, + MSG_TIMEOUT = 3, + MSG_SIGNAL_RESULTS = 4 +}; + +class TestHttpPortAllocator : public HttpPortAllocator { + public: + TestHttpPortAllocator(talk_base::NetworkManager* network_manager, + const std::string& user_agent, + const std::string& relay_token) : + HttpPortAllocator(network_manager, user_agent) { + SetRelayToken(relay_token); + } + PortAllocatorSession* CreateSessionInternal( + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd) { + return new TestHttpPortAllocatorSession(this, content_name, component, + ice_ufrag, ice_pwd, + stun_hosts(), relay_hosts(), + relay_token(), user_agent()); + } +}; + +void TestHttpPortAllocatorSession::ConfigReady(PortConfiguration* config) { + SignalConfigReady(username(), password(), config, proxy_); +} + +void TestHttpPortAllocatorSession::OnRequestDone( + talk_base::SignalThread* data) { + talk_base::AsyncHttpRequest* request = + static_cast(data); + + // Tell the checker that the request is complete. + SignalRequestDone(request); + + // Pass on the response to super class. + HttpPortAllocatorSession::OnRequestDone(data); +} + +ConnectivityChecker::ConnectivityChecker( + talk_base::Thread* worker, + const std::string& jid, + const std::string& session_id, + const std::string& user_agent, + const std::string& relay_token, + const std::string& connection) + : worker_(worker), + jid_(jid), + session_id_(session_id), + user_agent_(user_agent), + relay_token_(relay_token), + connection_(connection), + proxy_detect_(NULL), + timeout_ms_(kDefaultTimeoutMs), + stun_address_(kDefaultStunHostname, kDefaultStunPort), + started_(false) { +} + +ConnectivityChecker::~ConnectivityChecker() { + if (started_) { + // We try to clear the TIMEOUT below. But worker may still handle it and + // cause SignalCheckDone to happen on main-thread. So we finally clear any + // pending SIGNAL_RESULTS. + worker_->Clear(this, MSG_TIMEOUT); + worker_->Send(this, MSG_STOP); + nics_.clear(); + main_->Clear(this, MSG_SIGNAL_RESULTS); + } +} + +bool ConnectivityChecker::Initialize() { + network_manager_.reset(CreateNetworkManager()); + socket_factory_.reset(CreateSocketFactory(worker_)); + port_allocator_.reset(CreatePortAllocator(network_manager_.get(), + user_agent_, relay_token_)); + uint32 new_allocator_flags = port_allocator_->flags(); + new_allocator_flags |= cricket::PORTALLOCATOR_ENABLE_SHARED_UFRAG; + port_allocator_->set_flags(new_allocator_flags); + return true; +} + +void ConnectivityChecker::Start() { + main_ = talk_base::Thread::Current(); + worker_->Post(this, MSG_START); + started_ = true; +} + +void ConnectivityChecker::CleanUp() { + ASSERT(worker_ == talk_base::Thread::Current()); + if (proxy_detect_) { + proxy_detect_->Release(); + proxy_detect_ = NULL; + } + + for (uint32 i = 0; i < sessions_.size(); ++i) { + delete sessions_[i]; + } + sessions_.clear(); + for (uint32 i = 0; i < ports_.size(); ++i) { + delete ports_[i]; + } + ports_.clear(); +} + +bool ConnectivityChecker::AddNic(const talk_base::IPAddress& ip, + const talk_base::SocketAddress& proxy_addr) { + NicMap::iterator i = nics_.find(NicId(ip, proxy_addr)); + if (i != nics_.end()) { + // Already have it. + return false; + } + uint32 now = talk_base::Time(); + NicInfo info; + info.ip = ip; + info.proxy_info = GetProxyInfo(); + info.stun.start_time_ms = now; + nics_.insert(std::pair(NicId(ip, proxy_addr), info)); + return true; +} + +void ConnectivityChecker::SetProxyInfo(const talk_base::ProxyInfo& proxy_info) { + port_allocator_->set_proxy(user_agent_, proxy_info); + AllocatePorts(); +} + +talk_base::ProxyInfo ConnectivityChecker::GetProxyInfo() const { + talk_base::ProxyInfo proxy_info; + if (proxy_detect_) { + proxy_info = proxy_detect_->proxy(); + } + return proxy_info; +} + +void ConnectivityChecker::CheckNetworks() { + network_manager_->SignalNetworksChanged.connect( + this, &ConnectivityChecker::OnNetworksChanged); + network_manager_->StartUpdating(); +} + +void ConnectivityChecker::OnMessage(talk_base::Message *msg) { + switch (msg->message_id) { + case MSG_START: + ASSERT(worker_ == talk_base::Thread::Current()); + worker_->PostDelayed(timeout_ms_, this, MSG_TIMEOUT); + CheckNetworks(); + break; + case MSG_STOP: + // We're being stopped, free resources. + CleanUp(); + break; + case MSG_TIMEOUT: + // We need to signal results on the main thread. + main_->Post(this, MSG_SIGNAL_RESULTS); + break; + case MSG_SIGNAL_RESULTS: + ASSERT(main_ == talk_base::Thread::Current()); + SignalCheckDone(this); + break; + default: + LOG(LS_ERROR) << "Unknown message: " << msg->message_id; + } +} + +void ConnectivityChecker::OnProxyDetect(talk_base::SignalThread* thread) { + ASSERT(worker_ == talk_base::Thread::Current()); + if (proxy_detect_->proxy().type != talk_base::PROXY_NONE) { + SetProxyInfo(proxy_detect_->proxy()); + } +} + +void ConnectivityChecker::OnRequestDone(talk_base::AsyncHttpRequest* request) { + ASSERT(worker_ == talk_base::Thread::Current()); + // Since we don't know what nic were actually used for the http request, + // for now, just use the first one. + std::vector networks; + network_manager_->GetNetworks(&networks); + if (networks.empty()) { + LOG(LS_ERROR) << "No networks while registering http start."; + return; + } + talk_base::ProxyInfo proxy_info = request->proxy(); + NicMap::iterator i = nics_.find(NicId(networks[0]->ip(), proxy_info.address)); + if (i != nics_.end()) { + int port = request->port(); + uint32 now = talk_base::Time(); + NicInfo* nic_info = &i->second; + if (port == talk_base::HTTP_DEFAULT_PORT) { + nic_info->http.rtt = now - nic_info->http.start_time_ms; + } else if (port == talk_base::HTTP_SECURE_PORT) { + nic_info->https.rtt = now - nic_info->https.start_time_ms; + } else { + LOG(LS_ERROR) << "Got response with unknown port: " << port; + } + } else { + LOG(LS_ERROR) << "No nic info found while receiving response."; + } +} + +void ConnectivityChecker::OnConfigReady( + const std::string& username, const std::string& password, + const PortConfiguration* config, const talk_base::ProxyInfo& proxy_info) { + ASSERT(worker_ == talk_base::Thread::Current()); + + // Since we send requests on both HTTP and HTTPS we will get two + // configs per nic. Results from the second will overwrite the + // result from the first. + // TODO: Handle multiple pings on one nic. + CreateRelayPorts(username, password, config, proxy_info); +} + +void ConnectivityChecker::OnRelayPortComplete(Port* port) { + ASSERT(worker_ == talk_base::Thread::Current()); + RelayPort* relay_port = reinterpret_cast(port); + const ProtocolAddress* address = relay_port->ServerAddress(0); + talk_base::IPAddress ip = port->Network()->ip(); + NicMap::iterator i = nics_.find(NicId(ip, port->proxy().address)); + if (i != nics_.end()) { + // We have it already, add the new information. + NicInfo* nic_info = &i->second; + ConnectInfo* connect_info = NULL; + if (address) { + switch (address->proto) { + case PROTO_UDP: + connect_info = &nic_info->udp; + break; + case PROTO_TCP: + connect_info = &nic_info->tcp; + break; + case PROTO_SSLTCP: + connect_info = &nic_info->ssltcp; + break; + default: + LOG(LS_ERROR) << " relay address with bad protocol added"; + } + if (connect_info) { + connect_info->rtt = + talk_base::TimeSince(connect_info->start_time_ms); + } + } + } else { + LOG(LS_ERROR) << " got relay address for non-existing nic"; + } +} + +void ConnectivityChecker::OnStunPortComplete(Port* port) { + ASSERT(worker_ == talk_base::Thread::Current()); + const std::vector candidates = port->Candidates(); + Candidate c = candidates[0]; + talk_base::IPAddress ip = port->Network()->ip(); + NicMap::iterator i = nics_.find(NicId(ip, port->proxy().address)); + if (i != nics_.end()) { + // We have it already, add the new information. + uint32 now = talk_base::Time(); + NicInfo* nic_info = &i->second; + nic_info->external_address = c.address(); + nic_info->stun_server_address = static_cast(port)->server_addr(); + nic_info->stun.rtt = now - nic_info->stun.start_time_ms; + } else { + LOG(LS_ERROR) << "Got stun address for non-existing nic"; + } +} + +void ConnectivityChecker::OnStunPortError(Port* port) { + ASSERT(worker_ == talk_base::Thread::Current()); + LOG(LS_ERROR) << "Stun address error."; + talk_base::IPAddress ip = port->Network()->ip(); + NicMap::iterator i = nics_.find(NicId(ip, port->proxy().address)); + if (i != nics_.end()) { + // We have it already, add the new information. + NicInfo* nic_info = &i->second; + nic_info->stun_server_address = static_cast(port)->server_addr(); + } +} + +void ConnectivityChecker::OnRelayPortError(Port* port) { + ASSERT(worker_ == talk_base::Thread::Current()); + LOG(LS_ERROR) << "Relay address error."; +} + +void ConnectivityChecker::OnNetworksChanged() { + ASSERT(worker_ == talk_base::Thread::Current()); + std::vector networks; + network_manager_->GetNetworks(&networks); + if (networks.empty()) { + LOG(LS_ERROR) << "Machine has no networks; nothing to do"; + return; + } + AllocatePorts(); +} + +HttpPortAllocator* ConnectivityChecker::CreatePortAllocator( + talk_base::NetworkManager* network_manager, + const std::string& user_agent, + const std::string& relay_token) { + return new TestHttpPortAllocator(network_manager, user_agent, relay_token); +} + +StunPort* ConnectivityChecker::CreateStunPort( + const std::string& username, const std::string& password, + const PortConfiguration* config, talk_base::Network* network) { + return StunPort::Create(worker_, socket_factory_.get(), + network, network->ip(), 0, 0, + username, password, config->stun_address); +} + +RelayPort* ConnectivityChecker::CreateRelayPort( + const std::string& username, const std::string& password, + const PortConfiguration* config, talk_base::Network* network) { + return RelayPort::Create(worker_, socket_factory_.get(), + network, network->ip(), + port_allocator_->min_port(), + port_allocator_->max_port(), + username, password); +} + +void ConnectivityChecker::CreateRelayPorts( + const std::string& username, const std::string& password, + const PortConfiguration* config, const talk_base::ProxyInfo& proxy_info) { + PortConfiguration::RelayList::const_iterator relay; + std::vector networks; + network_manager_->GetNetworks(&networks); + if (networks.empty()) { + LOG(LS_ERROR) << "Machine has no networks; no relay ports created."; + return; + } + for (relay = config->relays.begin(); + relay != config->relays.end(); ++relay) { + for (uint32 i = 0; i < networks.size(); ++i) { + NicMap::iterator iter = nics_.find(NicId(networks[i]->ip(), + proxy_info.address)); + if (iter != nics_.end()) { + // TODO: Now setting the same start time for all protocols. + // This might affect accuracy, but since we are mainly looking for + // connect failures or number that stick out, this is good enough. + uint32 now = talk_base::Time(); + NicInfo* nic_info = &iter->second; + nic_info->udp.start_time_ms = now; + nic_info->tcp.start_time_ms = now; + nic_info->ssltcp.start_time_ms = now; + + // Add the addresses of this protocol. + PortList::const_iterator relay_port; + for (relay_port = relay->ports.begin(); + relay_port != relay->ports.end(); + ++relay_port) { + RelayPort* port = CreateRelayPort(username, password, + config, networks[i]); + port->AddServerAddress(*relay_port); + port->AddExternalAddress(*relay_port); + + nic_info->media_server_address = port->ServerAddress(0)->address; + + // Listen to network events. + port->SignalPortComplete.connect( + this, &ConnectivityChecker::OnRelayPortComplete); + port->SignalPortError.connect( + this, &ConnectivityChecker::OnRelayPortError); + + port->set_proxy(user_agent_, proxy_info); + + // Start fetching an address for this port. + port->PrepareAddress(); + ports_.push_back(port); + } + } else { + LOG(LS_ERROR) << "Failed to find nic info when creating relay ports."; + } + } + } +} + +void ConnectivityChecker::AllocatePorts() { + const std::string username = talk_base::CreateRandomString(ICE_UFRAG_LENGTH); + const std::string password = talk_base::CreateRandomString(ICE_PWD_LENGTH); + PortConfiguration config(stun_address_, username, password); + std::vector networks; + network_manager_->GetNetworks(&networks); + if (networks.empty()) { + LOG(LS_ERROR) << "Machine has no networks; no ports will be allocated"; + return; + } + talk_base::ProxyInfo proxy_info = GetProxyInfo(); + bool allocate_relay_ports = false; + for (uint32 i = 0; i < networks.size(); ++i) { + if (AddNic(networks[i]->ip(), proxy_info.address)) { + Port* port = CreateStunPort(username, password, &config, networks[i]); + if (port) { + + // Listen to network events. + port->SignalPortComplete.connect( + this, &ConnectivityChecker::OnStunPortComplete); + port->SignalPortError.connect( + this, &ConnectivityChecker::OnStunPortError); + + port->set_proxy(user_agent_, proxy_info); + port->PrepareAddress(); + ports_.push_back(port); + allocate_relay_ports = true; + } + } + } + + // If any new ip/proxy combinations were added, send a relay allocate. + if (allocate_relay_ports) { + AllocateRelayPorts(); + } + + // Initiate proxy detection. + InitiateProxyDetection(); +} + +void ConnectivityChecker::InitiateProxyDetection() { + // Only start if we haven't been started before. + if (!proxy_detect_) { + proxy_detect_ = new talk_base::AutoDetectProxy(user_agent_); + talk_base::Url host_url("/", "relay.google.com", + talk_base::HTTP_DEFAULT_PORT); + host_url.set_secure(true); + proxy_detect_->set_server_url(host_url.url()); + proxy_detect_->SignalWorkDone.connect( + this, &ConnectivityChecker::OnProxyDetect); + proxy_detect_->Start(); + } +} + +void ConnectivityChecker::AllocateRelayPorts() { + // Currently we are using the 'default' nic for http(s) requests. + TestHttpPortAllocatorSession* allocator_session = + reinterpret_cast( + port_allocator_->CreateSessionInternal( + "connectivity checker test content", + ICE_CANDIDATE_COMPONENT_RTP, + talk_base::CreateRandomString(ICE_UFRAG_LENGTH), + talk_base::CreateRandomString(ICE_PWD_LENGTH))); + allocator_session->set_proxy(port_allocator_->proxy()); + allocator_session->SignalConfigReady.connect( + this, &ConnectivityChecker::OnConfigReady); + allocator_session->SignalRequestDone.connect( + this, &ConnectivityChecker::OnRequestDone); + + // Try both http and https. + RegisterHttpStart(talk_base::HTTP_SECURE_PORT); + allocator_session->SendSessionRequest("relay.l.google.com", + talk_base::HTTP_SECURE_PORT); + RegisterHttpStart(talk_base::HTTP_DEFAULT_PORT); + allocator_session->SendSessionRequest("relay.l.google.com", + talk_base::HTTP_DEFAULT_PORT); + + sessions_.push_back(allocator_session); +} + +void ConnectivityChecker::RegisterHttpStart(int port) { + // Since we don't know what nic were actually used for the http request, + // for now, just use the first one. + std::vector networks; + network_manager_->GetNetworks(&networks); + if (networks.empty()) { + LOG(LS_ERROR) << "No networks while registering http start."; + return; + } + talk_base::ProxyInfo proxy_info = GetProxyInfo(); + NicMap::iterator i = nics_.find(NicId(networks[0]->ip(), proxy_info.address)); + if (i != nics_.end()) { + uint32 now = talk_base::Time(); + NicInfo* nic_info = &i->second; + if (port == talk_base::HTTP_DEFAULT_PORT) { + nic_info->http.start_time_ms = now; + } else if (port == talk_base::HTTP_SECURE_PORT) { + nic_info->https.start_time_ms = now; + } else { + LOG(LS_ERROR) << "Registering start time for unknown port: " << port; + } + } else { + LOG(LS_ERROR) << "Error, no nic info found while registering http start."; + } +} + +} // namespace talk_base diff --git a/talk/p2p/client/connectivitychecker.h b/talk/p2p/client/connectivitychecker.h new file mode 100644 index 000000000..95b736d09 --- /dev/null +++ b/talk/p2p/client/connectivitychecker.h @@ -0,0 +1,274 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + + +#ifndef TALK_P2P_CLIENT_CONNECTIVITYCHECKER_H_ +#define TALK_P2P_CLIENT_CONNECTIVITYCHECKER_H_ + +#include +#include + +#include "talk/base/network.h" +#include "talk/base/basictypes.h" +#include "talk/base/messagehandler.h" +#include "talk/base/proxyinfo.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" +#include "talk/base/socketaddress.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/client/httpportallocator.h" + +namespace talk_base { +class AsyncHttpRequest; +class AutoDetectProxy; +class BasicPacketSocketFactory; +class NetworkManager; +class PacketSocketFactory; +class SignalThread; +class TestHttpPortAllocatorSession; +class Thread; +} + +namespace cricket { +class HttpPortAllocator; +class Port; +class PortAllocatorSession; +struct PortConfiguration; +class RelayPort; +class StunPort; + +// Contains details about a discovered firewall that are of interest +// when debugging call failures. +struct FirewallInfo { + std::string brand; + std::string model; + + // TODO: List of current port mappings. +}; + +// Contains details about a specific connect attempt. +struct ConnectInfo { + ConnectInfo() + : rtt(-1), error(0) {} + // Time when the connection was initiated. Needed for calculating + // the round trip time. + uint32 start_time_ms; + // Round trip time in milliseconds or -1 for failed connection. + int32 rtt; + // Error code representing low level errors like socket errors. + int error; +}; + +// Identifier for a network interface and proxy address pair. +struct NicId { + NicId(const talk_base::IPAddress& ip, + const talk_base::SocketAddress& proxy_address) + : ip(ip), + proxy_address(proxy_address) { + } + talk_base::IPAddress ip; + talk_base::SocketAddress proxy_address; +}; + +// Comparator implementation identifying unique network interface and +// proxy address pairs. +class NicIdComparator { + public: + int compare(const NicId &first, const NicId &second) const { + if (first.ip == second.ip) { + // Compare proxy address. + if (first.proxy_address == second.proxy_address) { + return 0; + } else { + return first.proxy_address < second.proxy_address? -1 : 1; + } + } + return first.ip < second.ip ? -1 : 1; + } + + bool operator()(const NicId &first, const NicId &second) const { + return (compare(first, second) < 0); + } +}; + +// Contains information of a network interface and proxy address pair. +struct NicInfo { + NicInfo() {} + talk_base::IPAddress ip; + talk_base::ProxyInfo proxy_info; + talk_base::SocketAddress external_address; + talk_base::SocketAddress stun_server_address; + talk_base::SocketAddress media_server_address; + ConnectInfo stun; + ConnectInfo http; + ConnectInfo https; + ConnectInfo udp; + ConnectInfo tcp; + ConnectInfo ssltcp; + FirewallInfo firewall; +}; + +// Holds the result of the connectivity check. +class NicMap : public std::map { +}; + +class TestHttpPortAllocatorSession : public HttpPortAllocatorSession { + public: + TestHttpPortAllocatorSession( + HttpPortAllocator* allocator, + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd, + const std::vector& stun_hosts, + const std::vector& relay_hosts, + const std::string& relay_token, + const std::string& user_agent) + : HttpPortAllocatorSession( + allocator, content_name, component, ice_ufrag, ice_pwd, stun_hosts, + relay_hosts, relay_token, user_agent) { + } + void set_proxy(const talk_base::ProxyInfo& proxy) { + proxy_ = proxy; + } + + void ConfigReady(PortConfiguration* config); + + void OnRequestDone(talk_base::SignalThread* data); + + sigslot::signal4 SignalConfigReady; + sigslot::signal1 SignalRequestDone; + + private: + talk_base::ProxyInfo proxy_; +}; + +// Runs a request/response check on all network interface and proxy +// address combinations. The check is considered done either when all +// checks has been successful or when the check times out. +class ConnectivityChecker + : public talk_base::MessageHandler, public sigslot::has_slots<> { + public: + ConnectivityChecker(talk_base::Thread* worker, + const std::string& jid, + const std::string& session_id, + const std::string& user_agent, + const std::string& relay_token, + const std::string& connection); + virtual ~ConnectivityChecker(); + + // Virtual for gMock. + virtual bool Initialize(); + virtual void Start(); + + // MessageHandler implementation. + virtual void OnMessage(talk_base::Message *msg); + + // Instruct checker to stop and wait until that's done. + // Virtual for gMock. + virtual void Stop() { + worker_->Stop(); + } + + const NicMap& GetResults() const { + return nics_; + } + + void set_timeout_ms(uint32 timeout) { + timeout_ms_ = timeout; + } + + void set_stun_address(const talk_base::SocketAddress& stun_address) { + stun_address_ = stun_address; + } + + const std::string& connection() const { + return connection_; + } + + const std::string& jid() const { + return jid_; + } + + const std::string& session_id() const { + return session_id_; + } + + // Context: Main Thread. Signalled when the connectivity check is complete. + sigslot::signal1 SignalCheckDone; + + protected: + // Can be overridden for test. + virtual talk_base::NetworkManager* CreateNetworkManager() { + return new talk_base::BasicNetworkManager(); + } + virtual talk_base::BasicPacketSocketFactory* CreateSocketFactory( + talk_base::Thread* thread) { + return new talk_base::BasicPacketSocketFactory(thread); + } + virtual HttpPortAllocator* CreatePortAllocator( + talk_base::NetworkManager* network_manager, + const std::string& user_agent, + const std::string& relay_token); + virtual StunPort* CreateStunPort( + const std::string& username, const std::string& password, + const PortConfiguration* config, talk_base::Network* network); + virtual RelayPort* CreateRelayPort( + const std::string& username, const std::string& password, + const PortConfiguration* config, talk_base::Network* network); + virtual void InitiateProxyDetection(); + virtual void SetProxyInfo(const talk_base::ProxyInfo& info); + virtual talk_base::ProxyInfo GetProxyInfo() const; + + talk_base::Thread* worker() { + return worker_; + } + + private: + bool AddNic(const talk_base::IPAddress& ip, + const talk_base::SocketAddress& proxy_address); + void AllocatePorts(); + void AllocateRelayPorts(); + void CheckNetworks(); + void CreateRelayPorts( + const std::string& username, const std::string& password, + const PortConfiguration* config, const talk_base::ProxyInfo& proxy_info); + + // Must be called by the worker thread. + void CleanUp(); + + void OnRequestDone(talk_base::AsyncHttpRequest* request); + void OnRelayPortComplete(Port* port); + void OnStunPortComplete(Port* port); + void OnRelayPortError(Port* port); + void OnStunPortError(Port* port); + void OnNetworksChanged(); + void OnProxyDetect(talk_base::SignalThread* thread); + void OnConfigReady( + const std::string& username, const std::string& password, + const PortConfiguration* config, const talk_base::ProxyInfo& proxy); + void OnConfigWithProxyReady(const PortConfiguration*); + void RegisterHttpStart(int port); + talk_base::Thread* worker_; + std::string jid_; + std::string session_id_; + std::string user_agent_; + std::string relay_token_; + std::string connection_; + talk_base::AutoDetectProxy* proxy_detect_; + talk_base::scoped_ptr network_manager_; + talk_base::scoped_ptr socket_factory_; + talk_base::scoped_ptr port_allocator_; + NicMap nics_; + std::vector ports_; + std::vector sessions_; + uint32 timeout_ms_; + talk_base::SocketAddress stun_address_; + talk_base::Thread* main_; + bool started_; +}; + +} // namespace cricket + +#endif // TALK_P2P_CLIENT_CONNECTIVITYCHECKER_H_ diff --git a/talk/p2p/client/connectivitychecker_unittest.cc b/talk/p2p/client/connectivitychecker_unittest.cc new file mode 100644 index 000000000..fe1cb9b53 --- /dev/null +++ b/talk/p2p/client/connectivitychecker_unittest.cc @@ -0,0 +1,353 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + + +#include + +#include "talk/base/asynchttprequest.h" +#include "talk/base/gunit.h" +#include "talk/base/fakenetwork.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/socketaddress.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/base/relayport.h" +#include "talk/p2p/base/stunport.h" +#include "talk/p2p/client/connectivitychecker.h" +#include "talk/p2p/client/httpportallocator.h" + +namespace cricket { + +static const talk_base::SocketAddress kClientAddr1("11.11.11.11", 0); +static const talk_base::SocketAddress kClientAddr2("22.22.22.22", 0); +static const talk_base::SocketAddress kExternalAddr("33.33.33.33", 3333); +static const talk_base::SocketAddress kStunAddr("44.44.44.44", 4444); +static const talk_base::SocketAddress kRelayAddr("55.55.55.55", 5555); +static const talk_base::SocketAddress kProxyAddr("66.66.66.66", 6666); +static const talk_base::ProxyType kProxyType = talk_base::PROXY_HTTPS; +static const char kChannelName[] = "rtp_test"; +static const int kComponent = 1; +static const char kRelayHost[] = "relay.google.com"; +static const char kRelayToken[] = + "CAESFwoOb2phQGdvb2dsZS5jb20Q043h47MmGhBTB1rbfIXkhuarDCZe+xF6"; +static const char kBrowserAgent[] = "browser_test"; +static const char kJid[] = "a.b@c"; +static const char kUserName[] = "testuser"; +static const char kPassword[] = "testpassword"; +static const char kMagicCookie[] = "testcookie"; +static const char kRelayUdpPort[] = "4444"; +static const char kRelayTcpPort[] = "5555"; +static const char kRelaySsltcpPort[] = "6666"; +static const char kSessionId[] = "testsession"; +static const char kConnection[] = "testconnection"; +static const int kMinPort = 1000; +static const int kMaxPort = 2000; + +// Fake implementation to mock away real network usage. +class FakeRelayPort : public RelayPort { + public: + FakeRelayPort(talk_base::Thread* thread, + talk_base::PacketSocketFactory* factory, + talk_base::Network* network, const talk_base::IPAddress& ip, + int min_port, int max_port, + const std::string& username, const std::string& password) + : RelayPort(thread, factory, network, ip, min_port, max_port, + username, password) { + } + + // Just signal that we are done. + virtual void PrepareAddress() { + SignalPortComplete(this); + } +}; + +// Fake implementation to mock away real network usage. +class FakeStunPort : public StunPort { + public: + FakeStunPort(talk_base::Thread* thread, + talk_base::PacketSocketFactory* factory, + talk_base::Network* network, + const talk_base::IPAddress& ip, + int min_port, int max_port, + const std::string& username, const std::string& password, + const talk_base::SocketAddress& server_addr) + : StunPort(thread, factory, network, ip, min_port, max_port, + username, password, server_addr) { + } + + // Just set external address and signal that we are done. + virtual void PrepareAddress() { + AddAddress(kExternalAddr, kExternalAddr, "udp", + STUN_PORT_TYPE, ICE_TYPE_PREFERENCE_SRFLX, true); + SignalPortComplete(this); + } +}; + +// Fake implementation to mock away real network usage by responding +// to http requests immediately. +class FakeHttpPortAllocatorSession : public TestHttpPortAllocatorSession { + public: + FakeHttpPortAllocatorSession( + HttpPortAllocator* allocator, + const std::string& content_name, + int component, + const std::string& ice_ufrag, const std::string& ice_pwd, + const std::vector& stun_hosts, + const std::vector& relay_hosts, + const std::string& relay_token, + const std::string& agent) + : TestHttpPortAllocatorSession(allocator, + content_name, + component, + ice_ufrag, + ice_pwd, + stun_hosts, + relay_hosts, + relay_token, + agent) { + } + virtual void SendSessionRequest(const std::string& host, int port) { + FakeReceiveSessionResponse(host, port); + } + + // Pass results to the real implementation. + void FakeReceiveSessionResponse(const std::string& host, int port) { + talk_base::AsyncHttpRequest* response = CreateAsyncHttpResponse(port); + TestHttpPortAllocatorSession::OnRequestDone(response); + response->Destroy(true); + } + + private: + // Helper method for creating a response to a relay session request. + talk_base::AsyncHttpRequest* CreateAsyncHttpResponse(int port) { + talk_base::AsyncHttpRequest* request = + new talk_base::AsyncHttpRequest(kBrowserAgent); + std::stringstream ss; + ss << "username=" << kUserName << std::endl + << "password=" << kPassword << std::endl + << "magic_cookie=" << kMagicCookie << std::endl + << "relay.ip=" << kRelayAddr.ipaddr().ToString() << std::endl + << "relay.udp_port=" << kRelayUdpPort << std::endl + << "relay.tcp_port=" << kRelayTcpPort << std::endl + << "relay.ssltcp_port=" << kRelaySsltcpPort << std::endl; + request->response().document.reset( + new talk_base::MemoryStream(ss.str().c_str())); + request->response().set_success(); + request->set_port(port); + request->set_secure(port == talk_base::HTTP_SECURE_PORT); + return request; + } +}; + +// Fake implementation for creating fake http sessions. +class FakeHttpPortAllocator : public HttpPortAllocator { + public: + FakeHttpPortAllocator(talk_base::NetworkManager* network_manager, + const std::string& user_agent) + : HttpPortAllocator(network_manager, user_agent) { + } + + virtual PortAllocatorSession* CreateSessionInternal( + const std::string& content_name, int component, + const std::string& ice_ufrag, const std::string& ice_pwd) { + std::vector stun_hosts; + stun_hosts.push_back(kStunAddr); + std::vector relay_hosts; + relay_hosts.push_back(kRelayHost); + return new FakeHttpPortAllocatorSession(this, + content_name, + component, + ice_ufrag, + ice_pwd, + stun_hosts, + relay_hosts, + kRelayToken, + kBrowserAgent); + } +}; + +class ConnectivityCheckerForTest : public ConnectivityChecker { + public: + ConnectivityCheckerForTest(talk_base::Thread* worker, + const std::string& jid, + const std::string& session_id, + const std::string& user_agent, + const std::string& relay_token, + const std::string& connection) + : ConnectivityChecker(worker, + jid, + session_id, + user_agent, + relay_token, + connection), + proxy_initiated_(false) { + } + + talk_base::FakeNetworkManager* network_manager() const { + return network_manager_; + } + + FakeHttpPortAllocator* port_allocator() const { + return fake_port_allocator_; + } + + protected: + // Overridden methods for faking a real network. + virtual talk_base::NetworkManager* CreateNetworkManager() { + network_manager_ = new talk_base::FakeNetworkManager(); + return network_manager_; + } + virtual talk_base::BasicPacketSocketFactory* CreateSocketFactory( + talk_base::Thread* thread) { + // Create socket factory, for simplicity, let it run on the current thread. + socket_factory_ = + new talk_base::BasicPacketSocketFactory(talk_base::Thread::Current()); + return socket_factory_; + } + virtual HttpPortAllocator* CreatePortAllocator( + talk_base::NetworkManager* network_manager, + const std::string& user_agent, + const std::string& relay_token) { + fake_port_allocator_ = + new FakeHttpPortAllocator(network_manager, user_agent); + return fake_port_allocator_; + } + virtual StunPort* CreateStunPort( + const std::string& username, const std::string& password, + const PortConfiguration* config, talk_base::Network* network) { + return new FakeStunPort(worker(), socket_factory_, + network, network->ip(), + kMinPort, kMaxPort, + username, password, + config->stun_address); + } + virtual RelayPort* CreateRelayPort( + const std::string& username, const std::string& password, + const PortConfiguration* config, talk_base::Network* network) { + return new FakeRelayPort(worker(), socket_factory_, + network, network->ip(), + kMinPort, kMaxPort, + username, password); + } + virtual void InitiateProxyDetection() { + if (!proxy_initiated_) { + proxy_initiated_ = true; + proxy_info_.address = kProxyAddr; + proxy_info_.type = kProxyType; + SetProxyInfo(proxy_info_); + } + } + + virtual talk_base::ProxyInfo GetProxyInfo() const { + return proxy_info_; + } + + private: + talk_base::BasicPacketSocketFactory* socket_factory_; + FakeHttpPortAllocator* fake_port_allocator_; + talk_base::FakeNetworkManager* network_manager_; + talk_base::ProxyInfo proxy_info_; + bool proxy_initiated_; +}; + +class ConnectivityCheckerTest : public testing::Test { + protected: + void VerifyNic(const NicInfo& info, + const talk_base::SocketAddress& local_address) { + // Verify that the external address has been set. + EXPECT_EQ(kExternalAddr, info.external_address); + + // Verify that the stun server address has been set. + EXPECT_EQ(kStunAddr, info.stun_server_address); + + // Verify that the media server address has been set. Don't care + // about port since it is different for different protocols. + EXPECT_EQ(kRelayAddr.ipaddr(), info.media_server_address.ipaddr()); + + // Verify that local ip matches. + EXPECT_EQ(local_address.ipaddr(), info.ip); + + // Verify that we have received responses for our + // pings. Unsuccessful ping has rtt value -1, successful >= 0. + EXPECT_GE(info.stun.rtt, 0); + EXPECT_GE(info.udp.rtt, 0); + EXPECT_GE(info.tcp.rtt, 0); + EXPECT_GE(info.ssltcp.rtt, 0); + + // If proxy has been set, verify address and type. + if (!info.proxy_info.address.IsNil()) { + EXPECT_EQ(kProxyAddr, info.proxy_info.address); + EXPECT_EQ(kProxyType, info.proxy_info.type); + } + } +}; + +// Tests a configuration with two network interfaces. Verifies that 4 +// combinations of ip/proxy are created and that all protocols are +// tested on each combination. +TEST_F(ConnectivityCheckerTest, TestStart) { + ConnectivityCheckerForTest connectivity_checker(talk_base::Thread::Current(), + kJid, + kSessionId, + kBrowserAgent, + kRelayToken, + kConnection); + connectivity_checker.Initialize(); + connectivity_checker.set_stun_address(kStunAddr); + connectivity_checker.network_manager()->AddInterface(kClientAddr1); + connectivity_checker.network_manager()->AddInterface(kClientAddr2); + + connectivity_checker.Start(); + talk_base::Thread::Current()->ProcessMessages(1000); + + NicMap nics = connectivity_checker.GetResults(); + + // There should be 4 nics in our map. 2 for each interface added, + // one with proxy set and one without. + EXPECT_EQ(4U, nics.size()); + + // First verify interfaces without proxy. + talk_base::SocketAddress nilAddress; + + // First lookup the address of the first nic combined with no proxy. + NicMap::iterator i = nics.find(NicId(kClientAddr1.ipaddr(), nilAddress)); + ASSERT(i != nics.end()); + NicInfo info = i->second; + VerifyNic(info, kClientAddr1); + + // Then make sure the second device has been tested without proxy. + i = nics.find(NicId(kClientAddr2.ipaddr(), nilAddress)); + ASSERT(i != nics.end()); + info = i->second; + VerifyNic(info, kClientAddr2); + + // Now verify both interfaces with proxy. + i = nics.find(NicId(kClientAddr1.ipaddr(), kProxyAddr)); + ASSERT(i != nics.end()); + info = i->second; + VerifyNic(info, kClientAddr1); + + i = nics.find(NicId(kClientAddr2.ipaddr(), kProxyAddr)); + ASSERT(i != nics.end()); + info = i->second; + VerifyNic(info, kClientAddr2); +}; + +// Tests that nothing bad happens if thera are no network interfaces +// available to check. +TEST_F(ConnectivityCheckerTest, TestStartNoNetwork) { + ConnectivityCheckerForTest connectivity_checker(talk_base::Thread::Current(), + kJid, + kSessionId, + kBrowserAgent, + kRelayToken, + kConnection); + connectivity_checker.Initialize(); + connectivity_checker.Start(); + talk_base::Thread::Current()->ProcessMessages(1000); + + NicMap nics = connectivity_checker.GetResults(); + + // Verify that no nics where checked. + EXPECT_EQ(0U, nics.size()); +} + +} // namespace cricket diff --git a/talk/p2p/client/fakeportallocator.h b/talk/p2p/client/fakeportallocator.h new file mode 100644 index 000000000..2368948b0 --- /dev/null +++ b/talk/p2p/client/fakeportallocator.h @@ -0,0 +1,107 @@ +// Copyright 2010 Google Inc. All Rights Reserved, +// +// Author: Justin Uberti (juberti@google.com) + +#ifndef TALK_P2P_CLIENT_FAKEPORTALLOCATOR_H_ +#define TALK_P2P_CLIENT_FAKEPORTALLOCATOR_H_ + +#include +#include "talk/base/scoped_ptr.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/base/portallocator.h" +#include "talk/p2p/base/udpport.h" + +namespace talk_base { +class SocketFactory; +class Thread; +} + +namespace cricket { + +class FakePortAllocatorSession : public PortAllocatorSession { + public: + FakePortAllocatorSession(talk_base::Thread* worker_thread, + talk_base::PacketSocketFactory* factory, + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd) + : PortAllocatorSession(content_name, component, ice_ufrag, ice_pwd, + cricket::PORTALLOCATOR_ENABLE_SHARED_UFRAG), + worker_thread_(worker_thread), + factory_(factory), + network_("network", "unittest", + talk_base::IPAddress(INADDR_LOOPBACK), 8), + port_(NULL), running_(false), + port_config_count_(0) { + network_.AddIP(talk_base::IPAddress(INADDR_LOOPBACK)); + } + + virtual void StartGettingPorts() { + if (!port_) { + port_.reset(cricket::UDPPort::Create(worker_thread_, factory_, + &network_, network_.ip(), 0, 0, + username(), + password())); + AddPort(port_.get()); + } + ++port_config_count_; + running_ = true; + } + + virtual void StopGettingPorts() { running_ = false; } + virtual bool IsGettingPorts() { return running_; } + int port_config_count() { return port_config_count_; } + + void AddPort(cricket::Port* port) { + port->set_component(component_); + port->set_generation(0); + port->SignalPortComplete.connect( + this, &FakePortAllocatorSession::OnPortComplete); + port->PrepareAddress(); + SignalPortReady(this, port); + } + void OnPortComplete(cricket::Port* port) { + SignalCandidatesReady(this, port->Candidates()); + SignalCandidatesAllocationDone(this); + } + + private: + talk_base::Thread* worker_thread_; + talk_base::PacketSocketFactory* factory_; + talk_base::Network network_; + talk_base::scoped_ptr port_; + bool running_; + int port_config_count_; +}; + +class FakePortAllocator : public cricket::PortAllocator { + public: + FakePortAllocator(talk_base::Thread* worker_thread, + talk_base::PacketSocketFactory* factory) + : worker_thread_(worker_thread), factory_(factory) { + if (factory_ == NULL) { + owned_factory_.reset(new talk_base::BasicPacketSocketFactory( + worker_thread_)); + factory_ = owned_factory_.get(); + } + } + + virtual cricket::PortAllocatorSession* CreateSessionInternal( + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd) { + return new FakePortAllocatorSession( + worker_thread_, factory_, content_name, component, ice_ufrag, ice_pwd); + } + + private: + talk_base::Thread* worker_thread_; + talk_base::PacketSocketFactory* factory_; + talk_base::scoped_ptr owned_factory_; +}; + +} // namespace cricket + +#endif // TALK_P2P_CLIENT_FAKEPORTALLOCATOR_H_ diff --git a/talk/p2p/client/httpportallocator.cc b/talk/p2p/client/httpportallocator.cc new file mode 100644 index 000000000..e54acba5c --- /dev/null +++ b/talk/p2p/client/httpportallocator.cc @@ -0,0 +1,334 @@ +/* + * libjingle + * Copyright 2004--2008, 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/p2p/client/httpportallocator.h" + +#include +#include + +#include "talk/base/asynchttprequest.h" +#include "talk/base/basicdefs.h" +#include "talk/base/common.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/nethelpers.h" +#include "talk/base/signalthread.h" +#include "talk/base/stringencode.h" + +namespace { + +const uint32 MSG_TIMEOUT = 100; // must not conflict + // with BasicPortAllocator.cpp + +// Helper routine to remove whitespace from the ends of a string. +void Trim(std::string& str) { + size_t first = str.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) { + str.clear(); + return; + } + + ASSERT(str.find_last_not_of(" \t\r\n") != std::string::npos); +} + +// Parses the lines in the result of the HTTP request that are of the form +// 'a=b' and returns them in a map. +typedef std::map StringMap; +void ParseMap(const std::string& string, StringMap& map) { + size_t start_of_line = 0; + size_t end_of_line = 0; + + for (;;) { // for each line + start_of_line = string.find_first_not_of("\r\n", end_of_line); + if (start_of_line == std::string::npos) + break; + + end_of_line = string.find_first_of("\r\n", start_of_line); + if (end_of_line == std::string::npos) { + end_of_line = string.length(); + } + + size_t equals = string.find('=', start_of_line); + if ((equals >= end_of_line) || (equals == std::string::npos)) + continue; + + std::string key(string, start_of_line, equals - start_of_line); + std::string value(string, equals + 1, end_of_line - equals - 1); + + Trim(key); + Trim(value); + + if ((key.size() > 0) && (value.size() > 0)) + map[key] = value; + } +} + +} // namespace + +namespace cricket { + +// HttpPortAllocatorBase + +const int HttpPortAllocatorBase::kNumRetries = 5; + +const char HttpPortAllocatorBase::kCreateSessionURL[] = "/create_session"; + +HttpPortAllocatorBase::HttpPortAllocatorBase( + talk_base::NetworkManager* network_manager, + talk_base::PacketSocketFactory* socket_factory, + const std::string &user_agent) + : BasicPortAllocator(network_manager, socket_factory), agent_(user_agent) { + relay_hosts_.push_back("relay.google.com"); + stun_hosts_.push_back( + talk_base::SocketAddress("stun.l.google.com", 19302)); +} + +HttpPortAllocatorBase::HttpPortAllocatorBase( + talk_base::NetworkManager* network_manager, + const std::string &user_agent) + : BasicPortAllocator(network_manager), agent_(user_agent) { + relay_hosts_.push_back("relay.google.com"); + stun_hosts_.push_back( + talk_base::SocketAddress("stun.l.google.com", 19302)); +} + +HttpPortAllocatorBase::~HttpPortAllocatorBase() { +} + +// HttpPortAllocatorSessionBase + +HttpPortAllocatorSessionBase::HttpPortAllocatorSessionBase( + HttpPortAllocatorBase* allocator, + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd, + const std::vector& stun_hosts, + const std::vector& relay_hosts, + const std::string& relay_token, + const std::string& user_agent) + : BasicPortAllocatorSession(allocator, content_name, component, + ice_ufrag, ice_pwd), + relay_hosts_(relay_hosts), stun_hosts_(stun_hosts), + relay_token_(relay_token), agent_(user_agent), attempts_(0) { +} + +HttpPortAllocatorSessionBase::~HttpPortAllocatorSessionBase() {} + +void HttpPortAllocatorSessionBase::GetPortConfigurations() { + // Creating relay sessions can take time and is done asynchronously. + // Creating stun sessions could also take time and could be done aysnc also, + // but for now is done here and added to the initial config. Note any later + // configs will have unresolved stun ips and will be discarded by the + // AllocationSequence. + PortConfiguration* config = new PortConfiguration(stun_hosts_[0], + username(), + password()); + ConfigReady(config); + TryCreateRelaySession(); +} + +void HttpPortAllocatorSessionBase::TryCreateRelaySession() { + if (allocator()->flags() & PORTALLOCATOR_DISABLE_RELAY) { + LOG(LS_VERBOSE) << "HttpPortAllocator: Relay ports disabled, skipping."; + return; + } + + if (attempts_ == HttpPortAllocator::kNumRetries) { + LOG(LS_ERROR) << "HttpPortAllocator: maximum number of requests reached; " + << "giving up on relay."; + return; + } + + if (relay_hosts_.size() == 0) { + LOG(LS_ERROR) << "HttpPortAllocator: no relay hosts configured."; + return; + } + + // Choose the next host to try. + std::string host = relay_hosts_[attempts_ % relay_hosts_.size()]; + attempts_++; + LOG(LS_INFO) << "HTTPPortAllocator: sending to relay host " << host; + if (relay_token_.empty()) { + LOG(LS_WARNING) << "No relay auth token found."; + } + + SendSessionRequest(host, talk_base::HTTP_SECURE_PORT); +} + +std::string HttpPortAllocatorSessionBase::GetSessionRequestUrl() { + std::string url = std::string(HttpPortAllocator::kCreateSessionURL); + if (allocator()->flags() & PORTALLOCATOR_ENABLE_SHARED_UFRAG) { + ASSERT(!username().empty()); + ASSERT(!password().empty()); + url = url + "?username=" + talk_base::s_url_encode(username()) + + "&password=" + talk_base::s_url_encode(password()); + } + return url; +} + +void HttpPortAllocatorSessionBase::ReceiveSessionResponse( + const std::string& response) { + + StringMap map; + ParseMap(response, map); + + if (!username().empty() && map["username"] != username()) { + LOG(LS_WARNING) << "Received unexpected username value from relay server."; + } + if (!password().empty() && map["password"] != password()) { + LOG(LS_WARNING) << "Received unexpected password value from relay server."; + } + + std::string relay_ip = map["relay.ip"]; + std::string relay_udp_port = map["relay.udp_port"]; + std::string relay_tcp_port = map["relay.tcp_port"]; + std::string relay_ssltcp_port = map["relay.ssltcp_port"]; + + PortConfiguration* config = new PortConfiguration(stun_hosts_[0], + map["username"], + map["password"]); + + RelayServerConfig relay_config(RELAY_GTURN); + if (!relay_udp_port.empty()) { + talk_base::SocketAddress address(relay_ip, atoi(relay_udp_port.c_str())); + relay_config.ports.push_back(ProtocolAddress(address, PROTO_UDP)); + } + if (!relay_tcp_port.empty()) { + talk_base::SocketAddress address(relay_ip, atoi(relay_tcp_port.c_str())); + relay_config.ports.push_back(ProtocolAddress(address, PROTO_TCP)); + } + if (!relay_ssltcp_port.empty()) { + talk_base::SocketAddress address(relay_ip, atoi(relay_ssltcp_port.c_str())); + relay_config.ports.push_back(ProtocolAddress(address, PROTO_SSLTCP)); + } + config->AddRelay(relay_config); + ConfigReady(config); +} + +// HttpPortAllocator + +HttpPortAllocator::HttpPortAllocator( + talk_base::NetworkManager* network_manager, + talk_base::PacketSocketFactory* socket_factory, + const std::string &user_agent) + : HttpPortAllocatorBase(network_manager, socket_factory, user_agent) { +} + +HttpPortAllocator::HttpPortAllocator( + talk_base::NetworkManager* network_manager, + const std::string &user_agent) + : HttpPortAllocatorBase(network_manager, user_agent) { +} +HttpPortAllocator::~HttpPortAllocator() {} + +PortAllocatorSession* HttpPortAllocator::CreateSessionInternal( + const std::string& content_name, + int component, + const std::string& ice_ufrag, const std::string& ice_pwd) { + return new HttpPortAllocatorSession(this, content_name, component, + ice_ufrag, ice_pwd, stun_hosts(), + relay_hosts(), relay_token(), + user_agent()); +} + +// HttpPortAllocatorSession + +HttpPortAllocatorSession::HttpPortAllocatorSession( + HttpPortAllocator* allocator, + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd, + const std::vector& stun_hosts, + const std::vector& relay_hosts, + const std::string& relay, + const std::string& agent) + : HttpPortAllocatorSessionBase(allocator, content_name, component, + ice_ufrag, ice_pwd, stun_hosts, + relay_hosts, relay, agent) { +} + +HttpPortAllocatorSession::~HttpPortAllocatorSession() { + for (std::list::iterator it = requests_.begin(); + it != requests_.end(); ++it) { + (*it)->Destroy(true); + } +} + +void HttpPortAllocatorSession::SendSessionRequest(const std::string& host, + int port) { + // Initiate an HTTP request to create a session through the chosen host. + talk_base::AsyncHttpRequest* request = + new talk_base::AsyncHttpRequest(user_agent()); + request->SignalWorkDone.connect(this, + &HttpPortAllocatorSession::OnRequestDone); + + request->set_secure(port == talk_base::HTTP_SECURE_PORT); + request->set_proxy(allocator()->proxy()); + request->response().document.reset(new talk_base::MemoryStream); + request->request().verb = talk_base::HV_GET; + request->request().path = GetSessionRequestUrl(); + request->request().addHeader("X-Talk-Google-Relay-Auth", relay_token(), true); + request->request().addHeader("X-Stream-Type", "video_rtp", true); + request->set_host(host); + request->set_port(port); + request->Start(); + request->Release(); + + requests_.push_back(request); +} + +void HttpPortAllocatorSession::OnRequestDone(talk_base::SignalThread* data) { + talk_base::AsyncHttpRequest* request = + static_cast(data); + + // Remove the request from the list of active requests. + std::list::iterator it = + std::find(requests_.begin(), requests_.end(), request); + if (it != requests_.end()) { + requests_.erase(it); + } + + if (request->response().scode != 200) { + LOG(LS_WARNING) << "HTTPPortAllocator: request " + << " received error " << request->response().scode; + TryCreateRelaySession(); + return; + } + LOG(LS_INFO) << "HTTPPortAllocator: request succeeded"; + + talk_base::MemoryStream* stream = + static_cast(request->response().document.get()); + stream->Rewind(); + size_t length; + stream->GetSize(&length); + std::string resp = std::string(stream->GetBuffer(), length); + ReceiveSessionResponse(resp); +} + +} // namespace cricket diff --git a/talk/p2p/client/httpportallocator.h b/talk/p2p/client/httpportallocator.h new file mode 100644 index 000000000..cb4c8f82b --- /dev/null +++ b/talk/p2p/client/httpportallocator.h @@ -0,0 +1,192 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +#ifndef TALK_P2P_CLIENT_HTTPPORTALLOCATOR_H_ +#define TALK_P2P_CLIENT_HTTPPORTALLOCATOR_H_ + +#include +#include +#include + +#include "talk/base/gunit_prod.h" +#include "talk/p2p/client/basicportallocator.h" + +class HttpPortAllocatorTest_TestSessionRequestUrl_Test; + +namespace talk_base { +class AsyncHttpRequest; +class SignalThread; +} + +namespace cricket { + +class HttpPortAllocatorBase : public BasicPortAllocator { + public: + // The number of HTTP requests we should attempt before giving up. + static const int kNumRetries; + + // Records the URL that we will GET in order to create a session. + static const char kCreateSessionURL[]; + + HttpPortAllocatorBase(talk_base::NetworkManager* network_manager, + const std::string& user_agent); + HttpPortAllocatorBase(talk_base::NetworkManager* network_manager, + talk_base::PacketSocketFactory* socket_factory, + const std::string& user_agent); + virtual ~HttpPortAllocatorBase(); + + // CreateSession is defined in BasicPortAllocator but is + // redefined here as pure virtual. + virtual PortAllocatorSession* CreateSessionInternal( + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd) = 0; + + void SetStunHosts(const std::vector& hosts) { + if (!hosts.empty()) { + stun_hosts_ = hosts; + } + } + void SetRelayHosts(const std::vector& hosts) { + if (!hosts.empty()) { + relay_hosts_ = hosts; + } + } + void SetRelayToken(const std::string& relay) { relay_token_ = relay; } + + const std::vector& stun_hosts() const { + return stun_hosts_; + } + + const std::vector& relay_hosts() const { + return relay_hosts_; + } + + const std::string& relay_token() const { + return relay_token_; + } + + const std::string& user_agent() const { + return agent_; + } + + private: + std::vector stun_hosts_; + std::vector relay_hosts_; + std::string relay_token_; + std::string agent_; +}; + +class RequestData; + +class HttpPortAllocatorSessionBase : public BasicPortAllocatorSession { + public: + HttpPortAllocatorSessionBase( + HttpPortAllocatorBase* allocator, + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd, + const std::vector& stun_hosts, + const std::vector& relay_hosts, + const std::string& relay, + const std::string& agent); + virtual ~HttpPortAllocatorSessionBase(); + + const std::string& relay_token() const { + return relay_token_; + } + + const std::string& user_agent() const { + return agent_; + } + + virtual void SendSessionRequest(const std::string& host, int port) = 0; + virtual void ReceiveSessionResponse(const std::string& response); + + protected: + virtual void GetPortConfigurations(); + void TryCreateRelaySession(); + virtual HttpPortAllocatorBase* allocator() { + return static_cast( + BasicPortAllocatorSession::allocator()); + } + + std::string GetSessionRequestUrl(); + + private: + FRIEND_TEST(::HttpPortAllocatorTest, TestSessionRequestUrl); + + std::vector relay_hosts_; + std::vector stun_hosts_; + std::string relay_token_; + std::string agent_; + int attempts_; +}; + +class HttpPortAllocator : public HttpPortAllocatorBase { + public: + HttpPortAllocator(talk_base::NetworkManager* network_manager, + const std::string& user_agent); + HttpPortAllocator(talk_base::NetworkManager* network_manager, + talk_base::PacketSocketFactory* socket_factory, + const std::string& user_agent); + virtual ~HttpPortAllocator(); + virtual PortAllocatorSession* CreateSessionInternal( + const std::string& content_name, + int component, + const std::string& ice_ufrag, const std::string& ice_pwd); +}; + +class HttpPortAllocatorSession : public HttpPortAllocatorSessionBase { + public: + HttpPortAllocatorSession( + HttpPortAllocator* allocator, + const std::string& content_name, + int component, + const std::string& ice_ufrag, + const std::string& ice_pwd, + const std::vector& stun_hosts, + const std::vector& relay_hosts, + const std::string& relay, + const std::string& agent); + virtual ~HttpPortAllocatorSession(); + + virtual void SendSessionRequest(const std::string& host, int port); + + protected: + // Protected for diagnostics. + virtual void OnRequestDone(talk_base::SignalThread* request); + + private: + std::list requests_; +}; + +} // namespace cricket + +#endif // TALK_P2P_CLIENT_HTTPPORTALLOCATOR_H_ diff --git a/talk/p2p/client/portallocator_unittest.cc b/talk/p2p/client/portallocator_unittest.cc new file mode 100644 index 000000000..21131036c --- /dev/null +++ b/talk/p2p/client/portallocator_unittest.cc @@ -0,0 +1,822 @@ +/* + * libjingle + * Copyright 2009 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/base/fakenetwork.h" +#include "talk/base/firewallsocketserver.h" +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/natserver.h" +#include "talk/base/natsocketfactory.h" +#include "talk/base/network.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/socketaddress.h" +#include "talk/base/thread.h" +#include "talk/base/virtualsocketserver.h" +#include "talk/p2p/base/basicpacketsocketfactory.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/p2ptransportchannel.h" +#include "talk/p2p/base/portallocatorsessionproxy.h" +#include "talk/p2p/base/testrelayserver.h" +#include "talk/p2p/base/teststunserver.h" +#include "talk/p2p/client/basicportallocator.h" +#include "talk/p2p/client/httpportallocator.h" + +using talk_base::SocketAddress; +using talk_base::Thread; + +static const SocketAddress kClientAddr("11.11.11.11", 0); +static const SocketAddress kNatAddr("77.77.77.77", talk_base::NAT_SERVER_PORT); +static const SocketAddress kRemoteClientAddr("22.22.22.22", 0); +static const SocketAddress kStunAddr("99.99.99.1", cricket::STUN_SERVER_PORT); +static const SocketAddress kRelayUdpIntAddr("99.99.99.2", 5000); +static const SocketAddress kRelayUdpExtAddr("99.99.99.3", 5001); +static const SocketAddress kRelayTcpIntAddr("99.99.99.2", 5002); +static const SocketAddress kRelayTcpExtAddr("99.99.99.3", 5003); +static const SocketAddress kRelaySslTcpIntAddr("99.99.99.2", 5004); +static const SocketAddress kRelaySslTcpExtAddr("99.99.99.3", 5005); + +// Minimum and maximum port for port range tests. +static const int kMinPort = 10000; +static const int kMaxPort = 10099; + +// Based on ICE_UFRAG_LENGTH +static const char kIceUfrag0[] = "TESTICEUFRAG0000"; +// Based on ICE_PWD_LENGTH +static const char kIcePwd0[] = "TESTICEPWD00000000000000"; + +static const char kContentName[] = "test content"; + +static const int kDefaultAllocationTimeout = 1000; + +namespace cricket { + +// Helper for dumping candidates +std::ostream& operator<<(std::ostream& os, const cricket::Candidate& c) { + os << c.ToString(); + return os; +} + +} // namespace cricket + +class PortAllocatorTest : public testing::Test, public sigslot::has_slots<> { + public: + static void SetUpTestCase() { + // Ensure the RNG is inited. + talk_base::InitRandom(NULL, 0); + } + PortAllocatorTest() + : pss_(new talk_base::PhysicalSocketServer), + vss_(new talk_base::VirtualSocketServer(pss_.get())), + fss_(new talk_base::FirewallSocketServer(vss_.get())), + ss_scope_(fss_.get()), + nat_factory_(vss_.get(), kNatAddr), + nat_socket_factory_(&nat_factory_), + stun_server_(Thread::Current(), kStunAddr), + relay_server_(Thread::Current(), kRelayUdpIntAddr, kRelayUdpExtAddr, + kRelayTcpIntAddr, kRelayTcpExtAddr, + kRelaySslTcpIntAddr, kRelaySslTcpExtAddr), + allocator_(new cricket::BasicPortAllocator( + &network_manager_, kStunAddr, + kRelayUdpIntAddr, kRelayTcpIntAddr, kRelaySslTcpIntAddr)), + candidate_allocation_done_(false) { + allocator_->set_step_delay(cricket::kMinimumStepDelay); + } + + void AddInterface(const SocketAddress& addr) { + network_manager_.AddInterface(addr); + } + bool SetPortRange(int min_port, int max_port) { + return allocator_->SetPortRange(min_port, max_port); + } + talk_base::NATServer* CreateNatServer(const SocketAddress& addr, + talk_base::NATType type) { + return new talk_base::NATServer(type, vss_.get(), addr, vss_.get(), addr); + } + + bool CreateSession(int component) { + session_.reset(CreateSession("session", component)); + if (!session_) + return false; + return true; + } + + bool CreateSession(int component, const std::string& content_name) { + session_.reset(CreateSession("session", content_name, component)); + if (!session_) + return false; + return true; + } + + cricket::PortAllocatorSession* CreateSession( + const std::string& sid, int component) { + return CreateSession(sid, kContentName, component); + } + + cricket::PortAllocatorSession* CreateSession( + const std::string& sid, const std::string& content_name, int component) { + return CreateSession(sid, content_name, component, kIceUfrag0, kIcePwd0); + } + + cricket::PortAllocatorSession* CreateSession( + const std::string& sid, const std::string& content_name, int component, + const std::string& ice_ufrag, const std::string& ice_pwd) { + cricket::PortAllocatorSession* session = + allocator_->CreateSession( + sid, content_name, component, ice_ufrag, ice_pwd); + session->SignalPortReady.connect(this, + &PortAllocatorTest::OnPortReady); + session->SignalCandidatesReady.connect(this, + &PortAllocatorTest::OnCandidatesReady); + session->SignalCandidatesAllocationDone.connect(this, + &PortAllocatorTest::OnCandidatesAllocationDone); + return session; + } + + static bool CheckCandidate(const cricket::Candidate& c, + int component, const std::string& type, + const std::string& proto, + const SocketAddress& addr) { + return (c.component() == component && c.type() == type && + c.protocol() == proto && c.address().ipaddr() == addr.ipaddr() && + ((addr.port() == 0 && (c.address().port() != 0)) || + (c.address().port() == addr.port()))); + } + static bool CheckPort(const talk_base::SocketAddress& addr, + int min_port, int max_port) { + return (addr.port() >= min_port && addr.port() <= max_port); + } + + void OnCandidatesAllocationDone(cricket::PortAllocatorSession* session) { + // We should only get this callback once, except in the mux test where + // we have multiple port allocation sessions. + if (session == session_.get()) { + ASSERT_FALSE(candidate_allocation_done_); + candidate_allocation_done_ = true; + } + } + + // Check if all ports allocated have send-buffer size |expected|. If + // |expected| == -1, check if GetOptions returns SOCKET_ERROR. + void CheckSendBufferSizesOfAllPorts(int expected) { + std::vector::iterator it; + for (it = ports_.begin(); it < ports_.end(); ++it) { + int send_buffer_size; + if (expected == -1) { + EXPECT_EQ(SOCKET_ERROR, + (*it)->GetOption(talk_base::Socket::OPT_SNDBUF, + &send_buffer_size)); + } else { + EXPECT_EQ(0, (*it)->GetOption(talk_base::Socket::OPT_SNDBUF, + &send_buffer_size)); + ASSERT_EQ(expected, send_buffer_size); + } + } + } + + protected: + cricket::BasicPortAllocator& allocator() { + return *allocator_; + } + + void OnPortReady(cricket::PortAllocatorSession* ses, + cricket::PortInterface* port) { + LOG(LS_INFO) << "OnPortReady: " << port->ToString(); + ports_.push_back(port); + } + void OnCandidatesReady(cricket::PortAllocatorSession* ses, + const std::vector& candidates) { + for (size_t i = 0; i < candidates.size(); ++i) { + LOG(LS_INFO) << "OnCandidatesReady: " << candidates[i].ToString(); + candidates_.push_back(candidates[i]); + } + } + + bool HasRelayAddress(const cricket::ProtocolAddress& proto_addr) { + for (size_t i = 0; i < allocator_->relays().size(); ++i) { + cricket::RelayServerConfig server_config = allocator_->relays()[i]; + cricket::PortList::const_iterator relay_port; + for (relay_port = server_config.ports.begin(); + relay_port != server_config.ports.end(); ++relay_port) { + if (proto_addr.address == relay_port->address && + proto_addr.proto == relay_port->proto) + return true; + } + } + return false; + } + + talk_base::scoped_ptr pss_; + talk_base::scoped_ptr vss_; + talk_base::scoped_ptr fss_; + talk_base::SocketServerScope ss_scope_; + talk_base::NATSocketFactory nat_factory_; + talk_base::BasicPacketSocketFactory nat_socket_factory_; + cricket::TestStunServer stun_server_; + cricket::TestRelayServer relay_server_; + talk_base::FakeNetworkManager network_manager_; + talk_base::scoped_ptr allocator_; + talk_base::scoped_ptr session_; + std::vector ports_; + std::vector candidates_; + bool candidate_allocation_done_; +}; + +// Tests that we can init the port allocator and create a session. +TEST_F(PortAllocatorTest, TestBasic) { + EXPECT_EQ(&network_manager_, allocator().network_manager()); + EXPECT_EQ(kStunAddr, allocator().stun_address()); + ASSERT_EQ(1u, allocator().relays().size()); + EXPECT_EQ(cricket::RELAY_GTURN, allocator().relays()[0].type); + // Empty relay credentials are used for GTURN. + EXPECT_TRUE(allocator().relays()[0].credentials.username.empty()); + EXPECT_TRUE(allocator().relays()[0].credentials.password.empty()); + EXPECT_TRUE(HasRelayAddress(cricket::ProtocolAddress( + kRelayUdpIntAddr, cricket::PROTO_UDP))); + EXPECT_TRUE(HasRelayAddress(cricket::ProtocolAddress( + kRelayTcpIntAddr, cricket::PROTO_TCP))); + EXPECT_TRUE(HasRelayAddress(cricket::ProtocolAddress( + kRelaySslTcpIntAddr, cricket::PROTO_SSLTCP))); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); +} + +// Tests that we can get all the desired addresses successfully. +TEST_F(PortAllocatorTest, TestGetAllPortsWithMinimumStepDelay) { + AddInterface(kClientAddr); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(7U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(4U, ports_.size()); + EXPECT_PRED5(CheckCandidate, candidates_[0], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "local", "udp", kClientAddr); + EXPECT_PRED5(CheckCandidate, candidates_[1], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "stun", "udp", kClientAddr); + EXPECT_PRED5(CheckCandidate, candidates_[2], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "relay", "udp", kRelayUdpIntAddr); + EXPECT_PRED5(CheckCandidate, candidates_[3], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "relay", "udp", kRelayUdpExtAddr); + EXPECT_PRED5(CheckCandidate, candidates_[4], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "relay", "tcp", kRelayTcpIntAddr); + EXPECT_PRED5(CheckCandidate, candidates_[5], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "local", "tcp", kClientAddr); + EXPECT_PRED5(CheckCandidate, candidates_[6], + cricket::ICE_CANDIDATE_COMPONENT_RTP, + "relay", "ssltcp", kRelaySslTcpIntAddr); + EXPECT_TRUE(candidate_allocation_done_); +} + +// Verify candidates with default step delay of 1sec. +TEST_F(PortAllocatorTest, TestGetAllPortsWithOneSecondStepDelay) { + AddInterface(kClientAddr); + allocator_->set_step_delay(cricket::kDefaultStepDelay); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(2U, candidates_.size(), 1000); + EXPECT_EQ(2U, ports_.size()); + ASSERT_EQ_WAIT(4U, candidates_.size(), 2000); + EXPECT_EQ(3U, ports_.size()); + EXPECT_PRED5(CheckCandidate, candidates_[2], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "relay", "udp", kRelayUdpIntAddr); + EXPECT_PRED5(CheckCandidate, candidates_[3], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "relay", "udp", kRelayUdpExtAddr); + ASSERT_EQ_WAIT(6U, candidates_.size(), 1500); + EXPECT_PRED5(CheckCandidate, candidates_[4], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "relay", "tcp", kRelayTcpIntAddr); + EXPECT_PRED5(CheckCandidate, candidates_[5], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "local", "tcp", kClientAddr); + EXPECT_EQ(4U, ports_.size()); + ASSERT_EQ_WAIT(7U, candidates_.size(), 2000); + EXPECT_PRED5(CheckCandidate, candidates_[6], + cricket::ICE_CANDIDATE_COMPONENT_RTP, + "relay", "ssltcp", kRelaySslTcpIntAddr); + EXPECT_EQ(4U, ports_.size()); + EXPECT_TRUE(candidate_allocation_done_); + // If we Stop gathering now, we shouldn't get a second "done" callback. + session_->StopGettingPorts(); +} + +TEST_F(PortAllocatorTest, TestSetupVideoRtpPortsWithNormalSendBuffers) { + AddInterface(kClientAddr); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP, + cricket::CN_VIDEO)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(7U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_TRUE(candidate_allocation_done_); + // If we Stop gathering now, we shouldn't get a second "done" callback. + session_->StopGettingPorts(); + + // All ports should have normal send-buffer sizes (64KB). + CheckSendBufferSizesOfAllPorts(64 * 1024); +} + +TEST_F(PortAllocatorTest, TestSetupVideoRtpPortsWithLargeSendBuffers) { + AddInterface(kClientAddr); + allocator_->set_flags(allocator_->flags() | + cricket::PORTALLOCATOR_USE_LARGE_SOCKET_SEND_BUFFERS); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP, + cricket::CN_VIDEO)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(7U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_TRUE(candidate_allocation_done_); + // If we Stop gathering now, we shouldn't get a second "done" callback. + session_->StopGettingPorts(); + + // All ports should have large send-buffer sizes (128KB). + CheckSendBufferSizesOfAllPorts(128 * 1024); +} + +TEST_F(PortAllocatorTest, TestSetupVideoRtcpPortsAndCheckSendBuffers) { + AddInterface(kClientAddr); + allocator_->set_flags(allocator_->flags() | + cricket::PORTALLOCATOR_USE_LARGE_SOCKET_SEND_BUFFERS); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTCP, + cricket::CN_DATA)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(7U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_TRUE(candidate_allocation_done_); + // If we Stop gathering now, we shouldn't get a second "done" callback. + session_->StopGettingPorts(); + + // No ports should have send-buffer size set. + CheckSendBufferSizesOfAllPorts(-1); +} + + +TEST_F(PortAllocatorTest, TestSetupNonVideoPortsAndCheckSendBuffers) { + AddInterface(kClientAddr); + allocator_->set_flags(allocator_->flags() | + cricket::PORTALLOCATOR_USE_LARGE_SOCKET_SEND_BUFFERS); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP, + cricket::CN_DATA)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(7U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_TRUE(candidate_allocation_done_); + // If we Stop gathering now, we shouldn't get a second "done" callback. + session_->StopGettingPorts(); + + // No ports should have send-buffer size set. + CheckSendBufferSizesOfAllPorts(-1); +} + +// Tests that we can get callback after StopGetAllPorts. +TEST_F(PortAllocatorTest, TestStopGetAllPorts) { + AddInterface(kClientAddr); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(2U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(2U, ports_.size()); + session_->StopGettingPorts(); + EXPECT_TRUE_WAIT(candidate_allocation_done_, kDefaultAllocationTimeout); +} + +// Test that we restrict client ports appropriately when a port range is set. +// We check the candidates for udp/stun/tcp ports, and the from address +// for relay ports. +TEST_F(PortAllocatorTest, TestGetAllPortsPortRange) { + AddInterface(kClientAddr); + // Check that an invalid port range fails. + EXPECT_FALSE(SetPortRange(kMaxPort, kMinPort)); + // Check that a null port range succeeds. + EXPECT_TRUE(SetPortRange(0, 0)); + // Check that a valid port range succeeds. + EXPECT_TRUE(SetPortRange(kMinPort, kMaxPort)); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(7U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(4U, ports_.size()); + // Check the port number for the UDP port object. + EXPECT_PRED3(CheckPort, candidates_[0].address(), kMinPort, kMaxPort); + // Check the port number for the STUN port object. + EXPECT_PRED3(CheckPort, candidates_[1].address(), kMinPort, kMaxPort); + // Check the port number used to connect to the relay server. + EXPECT_PRED3(CheckPort, relay_server_.GetConnection(0).source(), + kMinPort, kMaxPort); + // Check the port number for the TCP port object. + EXPECT_PRED3(CheckPort, candidates_[5].address(), kMinPort, kMaxPort); + EXPECT_TRUE(candidate_allocation_done_); +} + +// Test that we don't crash or malfunction if we have no network adapters. +TEST_F(PortAllocatorTest, TestGetAllPortsNoAdapters) { + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->StartGettingPorts(); + talk_base::Thread::Current()->ProcessMessages(100); + // Without network adapter, we should not get any candidate. + EXPECT_EQ(0U, candidates_.size()); + EXPECT_TRUE(candidate_allocation_done_); +} + +// Test that we can get OnCandidatesAllocationDone callback when all the ports +// are disabled. +TEST_F(PortAllocatorTest, TestDisableAllPorts) { + AddInterface(kClientAddr); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->set_flags(cricket::PORTALLOCATOR_DISABLE_UDP | + cricket::PORTALLOCATOR_DISABLE_STUN | + cricket::PORTALLOCATOR_DISABLE_RELAY | + cricket::PORTALLOCATOR_DISABLE_TCP); + session_->StartGettingPorts(); + talk_base::Thread::Current()->ProcessMessages(100); + EXPECT_EQ(0U, candidates_.size()); + EXPECT_TRUE(candidate_allocation_done_); +} + +// Test that we don't crash or malfunction if we can't create UDP sockets. +TEST_F(PortAllocatorTest, TestGetAllPortsNoUdpSockets) { + AddInterface(kClientAddr); + fss_->set_udp_sockets_enabled(false); + EXPECT_TRUE(CreateSession(1)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(5U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(2U, ports_.size()); + EXPECT_PRED5(CheckCandidate, candidates_[0], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "relay", "udp", kRelayUdpIntAddr); + EXPECT_PRED5(CheckCandidate, candidates_[1], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "relay", "udp", kRelayUdpExtAddr); + EXPECT_PRED5(CheckCandidate, candidates_[2], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "relay", "tcp", kRelayTcpIntAddr); + EXPECT_PRED5(CheckCandidate, candidates_[3], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "local", "tcp", kClientAddr); + EXPECT_PRED5(CheckCandidate, candidates_[4], + cricket::ICE_CANDIDATE_COMPONENT_RTP, + "relay", "ssltcp", kRelaySslTcpIntAddr); + EXPECT_TRUE(candidate_allocation_done_); +} + +// Test that we don't crash or malfunction if we can't create UDP sockets or +// listen on TCP sockets. We still give out a local TCP address, since +// apparently this is needed for the remote side to accept our connection. +TEST_F(PortAllocatorTest, TestGetAllPortsNoUdpSocketsNoTcpListen) { + AddInterface(kClientAddr); + fss_->set_udp_sockets_enabled(false); + fss_->set_tcp_listen_enabled(false); + EXPECT_TRUE(CreateSession(1)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(5U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(2U, ports_.size()); + EXPECT_PRED5(CheckCandidate, candidates_[0], + 1, "relay", "udp", kRelayUdpIntAddr); + EXPECT_PRED5(CheckCandidate, candidates_[1], + 1, "relay", "udp", kRelayUdpExtAddr); + EXPECT_PRED5(CheckCandidate, candidates_[2], + 1, "relay", "tcp", kRelayTcpIntAddr); + EXPECT_PRED5(CheckCandidate, candidates_[3], + 1, "local", "tcp", kClientAddr); + EXPECT_PRED5(CheckCandidate, candidates_[4], + 1, "relay", "ssltcp", kRelaySslTcpIntAddr); + EXPECT_TRUE(candidate_allocation_done_); +} + +// Test that we don't crash or malfunction if we can't create any sockets. +// TODO: Find a way to exit early here. +TEST_F(PortAllocatorTest, TestGetAllPortsNoSockets) { + AddInterface(kClientAddr); + fss_->set_tcp_sockets_enabled(false); + fss_->set_udp_sockets_enabled(false); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->StartGettingPorts(); + WAIT(candidates_.size() > 0, 2000); + // TODO - Check candidate_allocation_done signal. + // In case of Relay, ports creation will succeed but sockets will fail. + // There is no error reporting from RelayEntry to handle this failure. +} + +TEST_F(PortAllocatorTest, TestTcpPortNoListenAllowed) { + AddInterface(kClientAddr); + allocator().set_flags(cricket::PORTALLOCATOR_DISABLE_UDP | + cricket::PORTALLOCATOR_DISABLE_STUN | + cricket::PORTALLOCATOR_DISABLE_RELAY); + allocator().set_allow_tcp_listen(false); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->StartGettingPorts(); + EXPECT_TRUE_WAIT(candidate_allocation_done_, kDefaultAllocationTimeout); + EXPECT_TRUE(candidates_.empty()); +} + +// Testing STUN timeout. +TEST_F(PortAllocatorTest, TestGetAllPortsNoUdpAllowed) { + fss_->AddRule(false, talk_base::FP_UDP, talk_base::FD_ANY, kClientAddr); + AddInterface(kClientAddr); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->StartGettingPorts(); + EXPECT_EQ_WAIT(2U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(2U, ports_.size()); + EXPECT_PRED5(CheckCandidate, candidates_[0], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "local", "udp", kClientAddr); + EXPECT_PRED5(CheckCandidate, candidates_[1], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "local", "tcp", kClientAddr); + // RelayPort connection timeout is 3sec. TCP connection with RelayServer + // will be tried after 3 seconds. + EXPECT_EQ_WAIT(6U, candidates_.size(), 4000); + EXPECT_EQ(3U, ports_.size()); + EXPECT_PRED5(CheckCandidate, candidates_[2], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "relay", "udp", kRelayUdpIntAddr); + EXPECT_PRED5(CheckCandidate, candidates_[3], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "relay", "tcp", kRelayTcpIntAddr); + EXPECT_PRED5(CheckCandidate, candidates_[4], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "relay", "ssltcp", + kRelaySslTcpIntAddr); + EXPECT_PRED5(CheckCandidate, candidates_[5], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "relay", "udp", kRelayUdpExtAddr); + // Stun Timeout is 9sec. + EXPECT_TRUE_WAIT(candidate_allocation_done_, 9000); +} + +// Test to verify ICE restart process. +TEST_F(PortAllocatorTest, TestGetAllPortsRestarts) { + AddInterface(kClientAddr); + EXPECT_TRUE(CreateSession(1)); + session_->StartGettingPorts(); + EXPECT_EQ_WAIT(7U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(4U, ports_.size()); + EXPECT_TRUE(candidate_allocation_done_); + // TODO - Extend this to verify ICE restart. +} + +TEST_F(PortAllocatorTest, TestBasicMuxFeatures) { + AddInterface(kClientAddr); + allocator().set_flags(cricket::PORTALLOCATOR_ENABLE_BUNDLE); + // Session ID - session1. + talk_base::scoped_ptr session1( + CreateSession("session1", cricket::ICE_CANDIDATE_COMPONENT_RTP)); + talk_base::scoped_ptr session2( + CreateSession("session1", cricket::ICE_CANDIDATE_COMPONENT_RTCP)); + session1->StartGettingPorts(); + session2->StartGettingPorts(); + // Each session should receive two proxy ports of local and stun. + ASSERT_EQ_WAIT(14U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(8U, ports_.size()); + + talk_base::scoped_ptr session3( + CreateSession("session1", cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session3->StartGettingPorts(); + // Already allocated candidates and ports will be sent to the newly + // allocated proxy session. + ASSERT_EQ_WAIT(21U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(12U, ports_.size()); +} + +// This test verifies by changing ice_ufrag and/or ice_pwd +// will result in different set of candidates when BUNDLE is enabled. +// If BUNDLE is disabled, CreateSession will always allocate new +// set of candidates. +TEST_F(PortAllocatorTest, TestBundleIceRestart) { + AddInterface(kClientAddr); + allocator().set_flags(cricket::PORTALLOCATOR_ENABLE_BUNDLE); + // Session ID - session1. + talk_base::scoped_ptr session1( + CreateSession("session1", kContentName, + cricket::ICE_CANDIDATE_COMPONENT_RTP, + kIceUfrag0, kIcePwd0)); + session1->StartGettingPorts(); + ASSERT_EQ_WAIT(7U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(4U, ports_.size()); + + // Allocate a different session with sid |session1| and different ice_ufrag. + talk_base::scoped_ptr session2( + CreateSession("session1", kContentName, + cricket::ICE_CANDIDATE_COMPONENT_RTP, + "TestIceUfrag", kIcePwd0)); + session2->StartGettingPorts(); + ASSERT_EQ_WAIT(14U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(8U, ports_.size()); + // Verifying the candidate address different from previously allocated + // address. + // Skipping verification of component id and candidate type. + EXPECT_NE(candidates_[0].address(), candidates_[7].address()); + EXPECT_NE(candidates_[1].address(), candidates_[8].address()); + + // Allocating a different session with sid |session1| and + // different ice_pwd. + talk_base::scoped_ptr session3( + CreateSession("session1", kContentName, + cricket::ICE_CANDIDATE_COMPONENT_RTP, + kIceUfrag0, "TestIcePwd")); + session3->StartGettingPorts(); + ASSERT_EQ_WAIT(21U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(12U, ports_.size()); + // Verifying the candidate address different from previously + // allocated address. + EXPECT_NE(candidates_[7].address(), candidates_[14].address()); + EXPECT_NE(candidates_[8].address(), candidates_[15].address()); + + // Allocating a session with by changing both ice_ufrag and ice_pwd. + talk_base::scoped_ptr session4( + CreateSession("session1", kContentName, + cricket::ICE_CANDIDATE_COMPONENT_RTP, + "TestIceUfrag", "TestIcePwd")); + session4->StartGettingPorts(); + ASSERT_EQ_WAIT(28U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(16U, ports_.size()); + // Verifying the candidate address different from previously + // allocated address. + EXPECT_NE(candidates_[14].address(), candidates_[21].address()); + EXPECT_NE(candidates_[15].address(), candidates_[22].address()); +} + +// Test that when the PORTALLOCATOR_ENABLE_SHARED_UFRAG is enabled we got same +// ufrag and pwd for the collected candidates. +TEST_F(PortAllocatorTest, TestEnableSharedUfrag) { + allocator().set_flags(allocator().flags() | + cricket::PORTALLOCATOR_ENABLE_SHARED_UFRAG); + AddInterface(kClientAddr); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(7U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_PRED5(CheckCandidate, candidates_[0], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "local", "udp", kClientAddr); + EXPECT_PRED5(CheckCandidate, candidates_[1], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "stun", "udp", kClientAddr); + EXPECT_PRED5(CheckCandidate, candidates_[5], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "local", "tcp", kClientAddr); + EXPECT_EQ(4U, ports_.size()); + EXPECT_EQ(kIceUfrag0, candidates_[0].username()); + EXPECT_EQ(kIceUfrag0, candidates_[1].username()); + EXPECT_EQ(kIceUfrag0, candidates_[2].username()); + EXPECT_EQ(kIcePwd0, candidates_[0].password()); + EXPECT_EQ(kIcePwd0, candidates_[1].password()); + EXPECT_TRUE(candidate_allocation_done_); +} + +// Test that when the PORTALLOCATOR_ENABLE_SHARED_UFRAG isn't enabled we got +// different ufrag and pwd for the collected candidates. +TEST_F(PortAllocatorTest, TestDisableSharedUfrag) { + allocator().set_flags(allocator().flags() & + ~cricket::PORTALLOCATOR_ENABLE_SHARED_UFRAG); + AddInterface(kClientAddr); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(7U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_PRED5(CheckCandidate, candidates_[0], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "local", "udp", kClientAddr); + EXPECT_PRED5(CheckCandidate, candidates_[1], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "stun", "udp", kClientAddr); + EXPECT_EQ(4U, ports_.size()); + // Port should generate random ufrag and pwd. + EXPECT_NE(kIceUfrag0, candidates_[0].username()); + EXPECT_NE(kIceUfrag0, candidates_[1].username()); + EXPECT_NE(candidates_[0].username(), candidates_[1].username()); + EXPECT_NE(kIcePwd0, candidates_[0].password()); + EXPECT_NE(kIcePwd0, candidates_[1].password()); + EXPECT_NE(candidates_[0].password(), candidates_[1].password()); + EXPECT_TRUE(candidate_allocation_done_); +} + +// Test that when PORTALLOCATOR_ENABLE_SHARED_SOCKET is enabled only one port +// is allocated for udp and stun. Also verify there is only one candidate +// (local) if stun candidate is same as local candidate, which will be the case +// in a public network like the below test. +TEST_F(PortAllocatorTest, TestEnableSharedSocketWithoutNat) { + AddInterface(kClientAddr); + allocator_->set_flags(allocator().flags() | + cricket::PORTALLOCATOR_ENABLE_SHARED_UFRAG | + cricket::PORTALLOCATOR_ENABLE_SHARED_SOCKET); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(6U, candidates_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(3U, ports_.size()); + EXPECT_PRED5(CheckCandidate, candidates_[0], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "local", "udp", kClientAddr); + EXPECT_TRUE_WAIT(candidate_allocation_done_, kDefaultAllocationTimeout); +} + +// Test that when PORTALLOCATOR_ENABLE_SHARED_SOCKET is enabled only one port +// is allocated for udp and stun. In this test we should expect both stun and +// local candidates as client behind a nat. +TEST_F(PortAllocatorTest, TestEnableSharedSocketWithNat) { + AddInterface(kClientAddr); + talk_base::scoped_ptr nat_server( + CreateNatServer(kNatAddr, talk_base::NAT_OPEN_CONE)); + allocator_.reset(new cricket::BasicPortAllocator( + &network_manager_, &nat_socket_factory_, kStunAddr)); + allocator_->set_step_delay(cricket::kMinimumStepDelay); + allocator_->set_flags(allocator().flags() | + cricket::PORTALLOCATOR_ENABLE_SHARED_UFRAG | + cricket::PORTALLOCATOR_ENABLE_SHARED_SOCKET); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(3U, candidates_.size(), kDefaultAllocationTimeout); + ASSERT_EQ(2U, ports_.size()); + EXPECT_PRED5(CheckCandidate, candidates_[0], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "local", "udp", kClientAddr); + EXPECT_PRED5(CheckCandidate, candidates_[1], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "stun", "udp", + talk_base::SocketAddress(kNatAddr.ipaddr(), 0)); + EXPECT_TRUE_WAIT(candidate_allocation_done_, kDefaultAllocationTimeout); + EXPECT_EQ(3U, candidates_.size()); +} + +// This test verifies when PORTALLOCATOR_ENABLE_SHARED_SOCKET flag is enabled +// and fail to generate STUN candidate, local UDP candidate is generated +// properly. +TEST_F(PortAllocatorTest, TestEnableSharedSocketNoUdpAllowed) { + allocator().set_flags(allocator().flags() | + cricket::PORTALLOCATOR_DISABLE_RELAY | + cricket::PORTALLOCATOR_DISABLE_TCP | + cricket::PORTALLOCATOR_ENABLE_SHARED_UFRAG | + cricket::PORTALLOCATOR_ENABLE_SHARED_SOCKET); + fss_->AddRule(false, talk_base::FP_UDP, talk_base::FD_ANY, kClientAddr); + AddInterface(kClientAddr); + EXPECT_TRUE(CreateSession(cricket::ICE_CANDIDATE_COMPONENT_RTP)); + session_->StartGettingPorts(); + ASSERT_EQ_WAIT(1U, ports_.size(), kDefaultAllocationTimeout); + EXPECT_EQ(1U, candidates_.size()); + EXPECT_PRED5(CheckCandidate, candidates_[0], + cricket::ICE_CANDIDATE_COMPONENT_RTP, "local", "udp", kClientAddr); + // STUN timeout is 9sec. We need to wait to get candidate done signal. + EXPECT_TRUE_WAIT(candidate_allocation_done_, 10000); + EXPECT_EQ(1U, candidates_.size()); +} + +// Test that the httpportallocator correctly maintains its lists of stun and +// relay servers, by never allowing an empty list. +TEST(HttpPortAllocatorTest, TestHttpPortAllocatorHostLists) { + talk_base::FakeNetworkManager network_manager; + cricket::HttpPortAllocator alloc(&network_manager, "unit test agent"); + EXPECT_EQ(1U, alloc.relay_hosts().size()); + EXPECT_EQ(1U, alloc.stun_hosts().size()); + + std::vector relay_servers; + std::vector stun_servers; + + alloc.SetRelayHosts(relay_servers); + alloc.SetStunHosts(stun_servers); + EXPECT_EQ(1U, alloc.relay_hosts().size()); + EXPECT_EQ(1U, alloc.stun_hosts().size()); + + relay_servers.push_back("1.unittest.corp.google.com"); + relay_servers.push_back("2.unittest.corp.google.com"); + stun_servers.push_back( + talk_base::SocketAddress("1.unittest.corp.google.com", 0)); + stun_servers.push_back( + talk_base::SocketAddress("2.unittest.corp.google.com", 0)); + alloc.SetRelayHosts(relay_servers); + alloc.SetStunHosts(stun_servers); + EXPECT_EQ(2U, alloc.relay_hosts().size()); + EXPECT_EQ(2U, alloc.stun_hosts().size()); +} + +// Test that the HttpPortAllocator uses correct URL to create sessions. +TEST(HttpPortAllocatorTest, TestSessionRequestUrl) { + talk_base::FakeNetworkManager network_manager; + cricket::HttpPortAllocator alloc(&network_manager, "unit test agent"); + + // Disable PORTALLOCATOR_ENABLE_SHARED_UFRAG. + alloc.set_flags(alloc.flags() & ~cricket::PORTALLOCATOR_ENABLE_SHARED_UFRAG); + talk_base::scoped_ptr session( + static_cast( + alloc.CreateSessionInternal( + "test content", 0, kIceUfrag0, kIcePwd0))); + std::string url = session->GetSessionRequestUrl(); + LOG(LS_INFO) << "url: " << url; + EXPECT_EQ(std::string(cricket::HttpPortAllocator::kCreateSessionURL), url); + + // Enable PORTALLOCATOR_ENABLE_SHARED_UFRAG. + alloc.set_flags(alloc.flags() | cricket::PORTALLOCATOR_ENABLE_SHARED_UFRAG); + session.reset(static_cast( + alloc.CreateSessionInternal("test content", 0, kIceUfrag0, kIcePwd0))); + url = session->GetSessionRequestUrl(); + LOG(LS_INFO) << "url: " << url; + std::vector parts; + talk_base::split(url, '?', &parts); + ASSERT_EQ(2U, parts.size()); + + std::vector args_parts; + talk_base::split(parts[1], '&', &args_parts); + + std::map args; + for (std::vector::iterator it = args_parts.begin(); + it != args_parts.end(); ++it) { + std::vector parts; + talk_base::split(*it, '=', &parts); + ASSERT_EQ(2U, parts.size()); + args[talk_base::s_url_decode(parts[0])] = talk_base::s_url_decode(parts[1]); + } + + EXPECT_EQ(kIceUfrag0, args["username"]); + EXPECT_EQ(kIcePwd0, args["password"]); +} diff --git a/talk/p2p/client/sessionmanagertask.h b/talk/p2p/client/sessionmanagertask.h new file mode 100644 index 000000000..d7d9733e6 --- /dev/null +++ b/talk/p2p/client/sessionmanagertask.h @@ -0,0 +1,93 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _SESSIONMANAGERTASK_H_ +#define _SESSIONMANAGERTASK_H_ + +#include "talk/p2p/base/sessionmanager.h" +#include "talk/p2p/client/sessionsendtask.h" +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmpptask.h" + +namespace cricket { + +// This class handles sending and receiving XMPP messages on behalf of the +// SessionManager. The sending part is handed over to SessionSendTask. + +class SessionManagerTask : public buzz::XmppTask { + public: + SessionManagerTask(buzz::XmppTaskParentInterface* parent, + SessionManager* session_manager) + : buzz::XmppTask(parent, buzz::XmppEngine::HL_SINGLE), + session_manager_(session_manager) { + } + + ~SessionManagerTask() { + } + + // Turns on simple support for sending messages, using SessionSendTask. + void EnableOutgoingMessages() { + session_manager_->SignalOutgoingMessage.connect( + this, &SessionManagerTask::OnOutgoingMessage); + session_manager_->SignalRequestSignaling.connect( + session_manager_, &SessionManager::OnSignalingReady); + } + + virtual int ProcessStart() { + const buzz::XmlElement *stanza = NextStanza(); + if (stanza == NULL) + return STATE_BLOCKED; + session_manager_->OnIncomingMessage(stanza); + return STATE_START; + } + + protected: + virtual bool HandleStanza(const buzz::XmlElement *stanza) { + if (!session_manager_->IsSessionMessage(stanza)) + return false; + // Responses are handled by the SessionSendTask that sent the request. + //if (stanza->Attr(buzz::QN_TYPE) != buzz::STR_SET) + // return false; + QueueStanza(stanza); + return true; + } + + private: + void OnOutgoingMessage(SessionManager* manager, + const buzz::XmlElement* stanza) { + cricket::SessionSendTask* sender = + new cricket::SessionSendTask(parent_, session_manager_); + sender->Send(stanza); + sender->Start(); + } + + SessionManager* session_manager_; +}; + +} // namespace cricket + +#endif // _SESSIONMANAGERTASK_H_ diff --git a/talk/p2p/client/sessionsendtask.h b/talk/p2p/client/sessionsendtask.h new file mode 100644 index 000000000..6c7508a57 --- /dev/null +++ b/talk/p2p/client/sessionsendtask.h @@ -0,0 +1,145 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_CLIENT_SESSIONSENDTASK_H_ +#define TALK_P2P_CLIENT_SESSIONSENDTASK_H_ + +#include "talk/base/common.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/xmppclient.h" +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmpptask.h" +#include "talk/p2p/base/sessionmanager.h" + +namespace cricket { + +// The job of this task is to send an IQ stanza out (after stamping it with +// an ID attribute) and then wait for a response. If not response happens +// within 5 seconds, it will signal failure on a SessionManager. If an error +// happens it will also signal failure. If, however, the send succeeds this +// task will quietly go away. + +class SessionSendTask : public buzz::XmppTask { + public: + SessionSendTask(buzz::XmppTaskParentInterface* parent, + SessionManager* session_manager) + : buzz::XmppTask(parent, buzz::XmppEngine::HL_SINGLE), + session_manager_(session_manager) { + set_timeout_seconds(15); + session_manager_->SignalDestroyed.connect( + this, &SessionSendTask::OnSessionManagerDestroyed); + } + + virtual ~SessionSendTask() { + SignalDone(this); + } + + void Send(const buzz::XmlElement* stanza) { + ASSERT(stanza_.get() == NULL); + + // This should be an IQ of type set, result, or error. In the first case, + // we supply an ID. In the others, it should be present. + ASSERT(stanza->Name() == buzz::QN_IQ); + ASSERT(stanza->HasAttr(buzz::QN_TYPE)); + if (stanza->Attr(buzz::QN_TYPE) == "set") { + ASSERT(!stanza->HasAttr(buzz::QN_ID)); + } else { + ASSERT((stanza->Attr(buzz::QN_TYPE) == "result") || + (stanza->Attr(buzz::QN_TYPE) == "error")); + ASSERT(stanza->HasAttr(buzz::QN_ID)); + } + + stanza_.reset(new buzz::XmlElement(*stanza)); + if (stanza_->HasAttr(buzz::QN_ID)) { + set_task_id(stanza_->Attr(buzz::QN_ID)); + } else { + stanza_->SetAttr(buzz::QN_ID, task_id()); + } + } + + void OnSessionManagerDestroyed() { + // If the session manager doesn't exist anymore, we should still try to + // send the message, but avoid calling back into the SessionManager. + session_manager_ = NULL; + } + + sigslot::signal1 SignalDone; + + protected: + virtual int OnTimeout() { + if (session_manager_ != NULL) { + session_manager_->OnFailedSend(stanza_.get(), NULL); + } + + return XmppTask::OnTimeout(); + } + + virtual int ProcessStart() { + SendStanza(stanza_.get()); + if (stanza_->Attr(buzz::QN_TYPE) == buzz::STR_SET) { + return STATE_RESPONSE; + } else { + return STATE_DONE; + } + } + + virtual int ProcessResponse() { + const buzz::XmlElement* next = NextStanza(); + if (next == NULL) + return STATE_BLOCKED; + + if (session_manager_ != NULL) { + if (next->Attr(buzz::QN_TYPE) == buzz::STR_RESULT) { + session_manager_->OnIncomingResponse(stanza_.get(), next); + } else { + session_manager_->OnFailedSend(stanza_.get(), next); + } + } + + return STATE_DONE; + } + + virtual bool HandleStanza(const buzz::XmlElement *stanza) { + if (!MatchResponseIq(stanza, + buzz::Jid(stanza_->Attr(buzz::QN_TO)), task_id())) + return false; + if (stanza->Attr(buzz::QN_TYPE) == buzz::STR_RESULT || + stanza->Attr(buzz::QN_TYPE) == buzz::STR_ERROR) { + QueueStanza(stanza); + return true; + } + return false; + } + + private: + SessionManager *session_manager_; + talk_base::scoped_ptr stanza_; +}; + +} + +#endif // TALK_P2P_CLIENT_SESSIONSENDTASK_H_ diff --git a/talk/p2p/client/socketmonitor.cc b/talk/p2p/client/socketmonitor.cc new file mode 100644 index 000000000..e0c75d48c --- /dev/null +++ b/talk/p2p/client/socketmonitor.cc @@ -0,0 +1,114 @@ +/* + * libjingle + * Copyright 2004--2005, 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/p2p/client/socketmonitor.h" + +#include "talk/base/common.h" + +namespace cricket { + +enum { + MSG_MONITOR_POLL, + MSG_MONITOR_START, + MSG_MONITOR_STOP, + MSG_MONITOR_SIGNAL +}; + +SocketMonitor::SocketMonitor(TransportChannel* channel, + talk_base::Thread* worker_thread, + talk_base::Thread* monitor_thread) { + channel_ = channel; + channel_thread_ = worker_thread; + monitoring_thread_ = monitor_thread; + monitoring_ = false; +} + +SocketMonitor::~SocketMonitor() { + channel_thread_->Clear(this); + monitoring_thread_->Clear(this); +} + +void SocketMonitor::Start(int milliseconds) { + rate_ = milliseconds; + if (rate_ < 250) + rate_ = 250; + channel_thread_->Post(this, MSG_MONITOR_START); +} + +void SocketMonitor::Stop() { + channel_thread_->Post(this, MSG_MONITOR_STOP); +} + +void SocketMonitor::OnMessage(talk_base::Message *message) { + talk_base::CritScope cs(&crit_); + switch (message->message_id) { + case MSG_MONITOR_START: + ASSERT(talk_base::Thread::Current() == channel_thread_); + if (!monitoring_) { + monitoring_ = true; + PollSocket(true); + } + break; + + case MSG_MONITOR_STOP: + ASSERT(talk_base::Thread::Current() == channel_thread_); + if (monitoring_) { + monitoring_ = false; + channel_thread_->Clear(this); + } + break; + + case MSG_MONITOR_POLL: + ASSERT(talk_base::Thread::Current() == channel_thread_); + PollSocket(true); + break; + + case MSG_MONITOR_SIGNAL: { + ASSERT(talk_base::Thread::Current() == monitoring_thread_); + std::vector infos = connection_infos_; + crit_.Leave(); + SignalUpdate(this, infos); + crit_.Enter(); + break; + } + } +} + +void SocketMonitor::PollSocket(bool poll) { + ASSERT(talk_base::Thread::Current() == channel_thread_); + talk_base::CritScope cs(&crit_); + + // Gather connection infos + channel_->GetStats(&connection_infos_); + + // Signal the monitoring thread, start another poll timer + monitoring_thread_->Post(this, MSG_MONITOR_SIGNAL); + if (poll) + channel_thread_->PostDelayed(rate_, this, MSG_MONITOR_POLL); +} + +} // namespace cricket diff --git a/talk/p2p/client/socketmonitor.h b/talk/p2p/client/socketmonitor.h new file mode 100644 index 000000000..f24ad663d --- /dev/null +++ b/talk/p2p/client/socketmonitor.h @@ -0,0 +1,71 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_P2P_CLIENT_SOCKETMONITOR_H_ +#define TALK_P2P_CLIENT_SOCKETMONITOR_H_ + +#include + +#include "talk/base/criticalsection.h" +#include "talk/base/sigslot.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/transportchannel.h" + +namespace cricket { + +class SocketMonitor : public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + SocketMonitor(TransportChannel* channel, + talk_base::Thread* worker_thread, + talk_base::Thread* monitor_thread); + ~SocketMonitor(); + + void Start(int cms); + void Stop(); + + talk_base::Thread* monitor_thread() { return monitoring_thread_; } + + sigslot::signal2&> SignalUpdate; + + protected: + void OnMessage(talk_base::Message* message); + void PollSocket(bool poll); + + std::vector connection_infos_; + TransportChannel* channel_; + talk_base::Thread* channel_thread_; + talk_base::Thread* monitoring_thread_; + talk_base::CriticalSection crit_; + uint32 rate_; + bool monitoring_; +}; + +} // namespace cricket + +#endif // TALK_P2P_CLIENT_SOCKETMONITOR_H_ diff --git a/talk/session/media/audiomonitor.cc b/talk/session/media/audiomonitor.cc new file mode 100644 index 000000000..385702f75 --- /dev/null +++ b/talk/session/media/audiomonitor.cc @@ -0,0 +1,121 @@ +/* + * libjingle + * Copyright 2004 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/media/audiomonitor.h" +#include "talk/session/media/voicechannel.h" +#include + +namespace cricket { + +const uint32 MSG_MONITOR_POLL = 1; +const uint32 MSG_MONITOR_START = 2; +const uint32 MSG_MONITOR_STOP = 3; +const uint32 MSG_MONITOR_SIGNAL = 4; + +AudioMonitor::AudioMonitor(VoiceChannel *voice_channel, + talk_base::Thread *monitor_thread) { + voice_channel_ = voice_channel; + monitoring_thread_ = monitor_thread; + monitoring_ = false; +} + +AudioMonitor::~AudioMonitor() { + voice_channel_->worker_thread()->Clear(this); + monitoring_thread_->Clear(this); +} + +void AudioMonitor::Start(int milliseconds) { + rate_ = milliseconds; + if (rate_ < 100) + rate_ = 100; + voice_channel_->worker_thread()->Post(this, MSG_MONITOR_START); +} + +void AudioMonitor::Stop() { + voice_channel_->worker_thread()->Post(this, MSG_MONITOR_STOP); +} + +void AudioMonitor::OnMessage(talk_base::Message *message) { + talk_base::CritScope cs(&crit_); + + switch (message->message_id) { + case MSG_MONITOR_START: + assert(talk_base::Thread::Current() == voice_channel_->worker_thread()); + if (!monitoring_) { + monitoring_ = true; + PollVoiceChannel(); + } + break; + + case MSG_MONITOR_STOP: + assert(talk_base::Thread::Current() == voice_channel_->worker_thread()); + if (monitoring_) { + monitoring_ = false; + voice_channel_->worker_thread()->Clear(this); + } + break; + + case MSG_MONITOR_POLL: + assert(talk_base::Thread::Current() == voice_channel_->worker_thread()); + PollVoiceChannel(); + break; + + case MSG_MONITOR_SIGNAL: + { + assert(talk_base::Thread::Current() == monitoring_thread_); + AudioInfo info = audio_info_; + crit_.Leave(); + SignalUpdate(this, info); + crit_.Enter(); + } + break; + } +} + +void AudioMonitor::PollVoiceChannel() { + talk_base::CritScope cs(&crit_); + assert(talk_base::Thread::Current() == voice_channel_->worker_thread()); + + // Gather connection infos + audio_info_.input_level = voice_channel_->GetInputLevel_w(); + audio_info_.output_level = voice_channel_->GetOutputLevel_w(); + voice_channel_->GetActiveStreams_w(&audio_info_.active_streams); + + // Signal the monitoring thread, start another poll timer + monitoring_thread_->Post(this, MSG_MONITOR_SIGNAL); + voice_channel_->worker_thread()->PostDelayed(rate_, this, MSG_MONITOR_POLL); +} + +VoiceChannel *AudioMonitor::voice_channel() { + return voice_channel_; +} + +talk_base::Thread *AudioMonitor::monitor_thread() { + return monitoring_thread_; +} + +} diff --git a/talk/session/media/audiomonitor.h b/talk/session/media/audiomonitor.h new file mode 100644 index 000000000..5aff8fd1e --- /dev/null +++ b/talk/session/media/audiomonitor.h @@ -0,0 +1,75 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_SESSION_MEDIA_AUDIOMONITOR_H_ +#define TALK_SESSION_MEDIA_AUDIOMONITOR_H_ + +#include "talk/base/sigslot.h" +#include "talk/base/thread.h" +#include "talk/p2p/base/port.h" +#include + +namespace cricket { + +class VoiceChannel; + +struct AudioInfo { + int input_level; + int output_level; + typedef std::vector > StreamList; + StreamList active_streams; // ssrcs contributing to output_level +}; + +class AudioMonitor : public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + AudioMonitor(VoiceChannel* voice_channel, talk_base::Thread *monitor_thread); + ~AudioMonitor(); + + void Start(int cms); + void Stop(); + + VoiceChannel* voice_channel(); + talk_base::Thread *monitor_thread(); + + sigslot::signal2 SignalUpdate; + + protected: + void OnMessage(talk_base::Message *message); + void PollVoiceChannel(); + + AudioInfo audio_info_; + VoiceChannel* voice_channel_; + talk_base::Thread* monitoring_thread_; + talk_base::CriticalSection crit_; + uint32 rate_; + bool monitoring_; +}; + +} + +#endif // TALK_SESSION_MEDIA_AUDIOMONITOR_H_ diff --git a/talk/session/media/call.cc b/talk/session/media/call.cc new file mode 100644 index 000000000..bd22aab85 --- /dev/null +++ b/talk/session/media/call.cc @@ -0,0 +1,1027 @@ +/* + * libjingle + * Copyright 2004 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 +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/thread.h" +#include "talk/base/window.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/screencastid.h" +#include "talk/p2p/base/parsing.h" +#include "talk/session/media/call.h" +#include "talk/session/media/mediasessionclient.h" + +namespace cricket { + +const uint32 MSG_CHECKAUTODESTROY = 1; +const uint32 MSG_TERMINATECALL = 2; +const uint32 MSG_PLAYDTMF = 3; + +namespace { +const int kDTMFDelay = 300; // msec +const size_t kMaxDTMFDigits = 30; +const int kSendToVoicemailTimeout = 1000*20; +const int kNoVoicemailTimeout = 1000*180; +const int kMediaMonitorInterval = 1000*15; +// In order to be the same as the server-side switching, this must be 100. +const int kAudioMonitorPollPeriodMillis = 100; + +// V is a pointer type. +template +V FindOrNull(const std::map& map, + const K& key) { + typename std::map::const_iterator it = map.find(key); + return (it != map.end()) ? it->second : NULL; +} + +} + +Call::Call(MediaSessionClient* session_client) + : id_(talk_base::CreateRandomId()), + session_client_(session_client), + local_renderer_(NULL), + has_video_(false), + has_data_(false), + muted_(false), + video_muted_(false), + send_to_voicemail_(true), + playing_dtmf_(false) { +} + +Call::~Call() { + while (media_session_map_.begin() != media_session_map_.end()) { + Session* session = media_session_map_.begin()->second.session; + RemoveSession(session); + session_client_->session_manager()->DestroySession(session); + } + talk_base::Thread::Current()->Clear(this); +} + +Session* Call::InitiateSession(const buzz::Jid& to, + const buzz::Jid& initiator, + const CallOptions& options) { + const SessionDescription* offer = session_client_->CreateOffer(options); + + Session* session = session_client_->CreateSession(this); + session->set_initiator_name(initiator.Str()); + + AddSession(session, offer); + session->Initiate(to.Str(), offer); + + // After this timeout, terminate the call because the callee isn't + // answering + session_client_->session_manager()->signaling_thread()->Clear(this, + MSG_TERMINATECALL); + session_client_->session_manager()->signaling_thread()->PostDelayed( + send_to_voicemail_ ? kSendToVoicemailTimeout : kNoVoicemailTimeout, + this, MSG_TERMINATECALL); + return session; +} + +void Call::IncomingSession(Session* session, const SessionDescription* offer) { + AddSession(session, offer); + + // Make sure the session knows about the incoming ssrcs. This needs to be done + // prior to the SignalSessionState call, because that may trigger handling of + // these new SSRCs, so they need to be registered before then. + UpdateRemoteMediaStreams(session, offer->contents(), false); + + // Missed the first state, the initiate, which is needed by + // call_client. + SignalSessionState(this, session, Session::STATE_RECEIVEDINITIATE); +} + +void Call::AcceptSession(Session* session, + const cricket::CallOptions& options) { + MediaSessionMap::iterator it = media_session_map_.find(session->id()); + if (it != media_session_map_.end()) { + const SessionDescription* answer = session_client_->CreateAnswer( + session->remote_description(), options); + it->second.session->Accept(answer); + } +} + +void Call::RejectSession(Session* session) { + // Assume polite decline. + MediaSessionMap::iterator it = media_session_map_.find(session->id()); + if (it != media_session_map_.end()) + it->second.session->Reject(STR_TERMINATE_DECLINE); +} + +void Call::TerminateSession(Session* session) { + MediaSessionMap::iterator it = media_session_map_.find(session->id()); + if (it != media_session_map_.end()) { + // Assume polite terminations. + it->second.session->Terminate(); + } +} + +void Call::Terminate() { + // Copy the list so that we can iterate over it in a stable way + std::vector sessions = this->sessions(); + + // There may be more than one session to terminate + std::vector::iterator it; + for (it = sessions.begin(); it != sessions.end(); ++it) { + TerminateSession(*it); + } +} + +bool Call::SendViewRequest(Session* session, + const ViewRequest& view_request) { + StaticVideoViews::const_iterator it; + for (it = view_request.static_video_views.begin(); + it != view_request.static_video_views.end(); ++it) { + StreamParams found_stream; + bool found = false; + MediaStreams* recv_streams = GetMediaStreams(session); + if (recv_streams) + found = recv_streams->GetVideoStream(it->selector, &found_stream); + if (!found) { + LOG(LS_WARNING) << "Trying to send view request for (" + << it->selector.ssrc << ", '" + << it->selector.groupid << "', '" + << it->selector.streamid << "'" + << ") is not in the local streams."; + return false; + } + } + + XmlElements elems; + WriteError error; + if (!WriteJingleViewRequest(CN_VIDEO, view_request, &elems, &error)) { + LOG(LS_ERROR) << "Couldn't write out view request: " << error.text; + return false; + } + + return session->SendInfoMessage(elems); +} + +void Call::SetLocalRenderer(VideoRenderer* renderer) { + local_renderer_ = renderer; + if (session_client_->GetFocus() == this) { + session_client_->channel_manager()->SetLocalRenderer(renderer); + } +} + +void Call::SetVideoRenderer(Session* session, uint32 ssrc, + VideoRenderer* renderer) { + VideoChannel* video_channel = GetVideoChannel(session); + if (video_channel) { + video_channel->SetRenderer(ssrc, renderer); + LOG(LS_INFO) << "Set renderer of ssrc " << ssrc + << " to " << renderer << "."; + } else { + LOG(LS_INFO) << "Failed to set renderer of ssrc " << ssrc << "."; + } +} + +void Call::OnMessage(talk_base::Message* message) { + switch (message->message_id) { + case MSG_CHECKAUTODESTROY: + // If no more sessions for this call, delete it + if (media_session_map_.empty()) + session_client_->DestroyCall(this); + break; + case MSG_TERMINATECALL: + // Signal to the user that a timeout has happened and the call should + // be sent to voicemail. + if (send_to_voicemail_) { + SignalSetupToCallVoicemail(); + } + + // Callee didn't answer - terminate call + Terminate(); + break; + case MSG_PLAYDTMF: + ContinuePlayDTMF(); + } +} + +std::vector Call::sessions() { + std::vector sessions; + MediaSessionMap::iterator it; + for (it = media_session_map_.begin(); it != media_session_map_.end(); ++it) + sessions.push_back(it->second.session); + + return sessions; +} + +bool Call::AddSession(Session* session, const SessionDescription* offer) { + bool succeeded = true; + MediaSession media_session; + media_session.session = session; + media_session.voice_channel = NULL; + media_session.video_channel = NULL; + media_session.data_channel = NULL; + media_session.recv_streams = NULL; + + const ContentInfo* audio_offer = GetFirstAudioContent(offer); + const ContentInfo* video_offer = GetFirstVideoContent(offer); + const ContentInfo* data_offer = GetFirstDataContent(offer); + has_video_ = (video_offer != NULL); + has_data_ = (data_offer != NULL); + + ASSERT(audio_offer != NULL); + // Create voice channel and start a media monitor. + media_session.voice_channel = + session_client_->channel_manager()->CreateVoiceChannel( + session, audio_offer->name, has_video_); + // voice_channel can be NULL in case of NullVoiceEngine. + if (media_session.voice_channel) { + media_session.voice_channel->SignalMediaMonitor.connect( + this, &Call::OnMediaMonitor); + media_session.voice_channel->StartMediaMonitor(kMediaMonitorInterval); + } else { + succeeded = false; + } + + // If desired, create video channel and start a media monitor. + if (has_video_ && succeeded) { + media_session.video_channel = + session_client_->channel_manager()->CreateVideoChannel( + session, video_offer->name, true, media_session.voice_channel); + // video_channel can be NULL in case of NullVideoEngine. + if (media_session.video_channel) { + media_session.video_channel->SignalMediaMonitor.connect( + this, &Call::OnMediaMonitor); + media_session.video_channel->StartMediaMonitor(kMediaMonitorInterval); + } else { + succeeded = false; + } + } + + // If desired, create data channel. + if (has_data_ && succeeded) { + const DataContentDescription* data = GetFirstDataContentDescription(offer); + if (data == NULL) { + succeeded = false; + } else { + DataChannelType data_channel_type = DCT_RTP; + if ((data->protocol() == kMediaProtocolSctp) || + (data->protocol() == kMediaProtocolDtlsSctp)) { + data_channel_type = DCT_SCTP; + } + + bool rtcp = false; + media_session.data_channel = + session_client_->channel_manager()->CreateDataChannel( + session, data_offer->name, rtcp, data_channel_type); + if (media_session.data_channel) { + media_session.data_channel->SignalDataReceived.connect( + this, &Call::OnDataReceived); + } else { + succeeded = false; + } + } + } + + if (succeeded) { + // Add session to list, create channels for this session. + media_session.recv_streams = new MediaStreams; + media_session_map_[session->id()] = media_session; + session->SignalState.connect(this, &Call::OnSessionState); + session->SignalError.connect(this, &Call::OnSessionError); + session->SignalInfoMessage.connect( + this, &Call::OnSessionInfoMessage); + session->SignalRemoteDescriptionUpdate.connect( + this, &Call::OnRemoteDescriptionUpdate); + session->SignalReceivedTerminateReason + .connect(this, &Call::OnReceivedTerminateReason); + + // If this call has the focus, enable this session's channels. + if (session_client_->GetFocus() == this) { + EnableSessionChannels(session, true); + } + + // Signal client. + SignalAddSession(this, session); + } + + return succeeded; +} + +void Call::RemoveSession(Session* session) { + MediaSessionMap::iterator it = media_session_map_.find(session->id()); + if (it == media_session_map_.end()) + return; + + // Remove all the screencasts, if they haven't been already. + while (!it->second.started_screencasts.empty()) { + uint32 ssrc = it->second.started_screencasts.begin()->first; + if (!StopScreencastWithoutSendingUpdate(it->second.session, ssrc)) { + LOG(LS_ERROR) << "Unable to stop screencast with ssrc " << ssrc; + ASSERT(false); + } + } + + // Destroy video channel + VideoChannel* video_channel = it->second.video_channel; + if (video_channel != NULL) + session_client_->channel_manager()->DestroyVideoChannel(video_channel); + + // Destroy voice channel + VoiceChannel* voice_channel = it->second.voice_channel; + if (voice_channel != NULL) + session_client_->channel_manager()->DestroyVoiceChannel(voice_channel); + + // Destroy data channel + DataChannel* data_channel = it->second.data_channel; + if (data_channel != NULL) + session_client_->channel_manager()->DestroyDataChannel(data_channel); + + delete it->second.recv_streams; + media_session_map_.erase(it); + + // Destroy speaker monitor + StopSpeakerMonitor(session); + + // Signal client + SignalRemoveSession(this, session); + + // The call auto destroys when the last session is removed + talk_base::Thread::Current()->Post(this, MSG_CHECKAUTODESTROY); +} + +VoiceChannel* Call::GetVoiceChannel(Session* session) const { + MediaSessionMap::const_iterator it = media_session_map_.find(session->id()); + return (it != media_session_map_.end()) ? it->second.voice_channel : NULL; +} + +VideoChannel* Call::GetVideoChannel(Session* session) const { + MediaSessionMap::const_iterator it = media_session_map_.find(session->id()); + return (it != media_session_map_.end()) ? it->second.video_channel : NULL; +} + +DataChannel* Call::GetDataChannel(Session* session) const { + MediaSessionMap::const_iterator it = media_session_map_.find(session->id()); + return (it != media_session_map_.end()) ? it->second.data_channel : NULL; +} + +MediaStreams* Call::GetMediaStreams(Session* session) const { + MediaSessionMap::const_iterator it = media_session_map_.find(session->id()); + return (it != media_session_map_.end()) ? it->second.recv_streams : NULL; +} + +void Call::EnableChannels(bool enable) { + MediaSessionMap::iterator it; + for (it = media_session_map_.begin(); it != media_session_map_.end(); ++it) { + EnableSessionChannels(it->second.session, enable); + } + session_client_->channel_manager()->SetLocalRenderer( + (enable) ? local_renderer_ : NULL); +} + +void Call::EnableSessionChannels(Session* session, bool enable) { + MediaSessionMap::iterator it = media_session_map_.find(session->id()); + if (it == media_session_map_.end()) + return; + + VoiceChannel* voice_channel = it->second.voice_channel; + VideoChannel* video_channel = it->second.video_channel; + DataChannel* data_channel = it->second.data_channel; + if (voice_channel != NULL) + voice_channel->Enable(enable); + if (video_channel != NULL) + video_channel->Enable(enable); + if (data_channel != NULL) + data_channel->Enable(enable); +} + +void Call::Mute(bool mute) { + muted_ = mute; + MediaSessionMap::iterator it; + for (it = media_session_map_.begin(); it != media_session_map_.end(); ++it) { + if (it->second.voice_channel != NULL) + it->second.voice_channel->MuteStream(0, mute); + } +} + +void Call::MuteVideo(bool mute) { + video_muted_ = mute; + MediaSessionMap::iterator it; + for (it = media_session_map_.begin(); it != media_session_map_.end(); ++it) { + if (it->second.video_channel != NULL) + it->second.video_channel->MuteStream(0, mute); + } +} + +bool Call::SendData(Session* session, + const SendDataParams& params, + const talk_base::Buffer& payload, + SendDataResult* result) { + DataChannel* data_channel = GetDataChannel(session); + if (!data_channel) { + LOG(LS_WARNING) << "Could not send data: no data channel."; + return false; + } + + return data_channel->SendData(params, payload, result); +} + +void Call::PressDTMF(int event) { + // Queue up this digit + if (queued_dtmf_.size() < kMaxDTMFDigits) { + LOG(LS_INFO) << "Call::PressDTMF(" << event << ")"; + + queued_dtmf_.push_back(event); + + if (!playing_dtmf_) { + ContinuePlayDTMF(); + } + } +} + +cricket::VideoFormat ScreencastFormatFromFps(int fps) { + // The capturer pretty much ignore this, but just in case we give it + // a resolution big enough to cover any expected desktop. In any + // case, it can't be 0x0, or the CaptureManager will fail to use it. + return cricket::VideoFormat( + 1, 1, + cricket::VideoFormat::FpsToInterval(fps), cricket::FOURCC_ANY); +} + +bool Call::StartScreencast(Session* session, + const std::string& streamid, uint32 ssrc, + const ScreencastId& screencastid, int fps) { + MediaSessionMap::iterator it = media_session_map_.find(session->id()); + if (it == media_session_map_.end()) { + return false; + } + + VideoChannel *video_channel = GetVideoChannel(session); + if (!video_channel) { + LOG(LS_WARNING) << "Cannot add screencast" + << " because there is no video channel."; + return false; + } + + VideoCapturer *capturer = video_channel->AddScreencast(ssrc, screencastid); + if (capturer == NULL) { + LOG(LS_WARNING) << "Could not create screencast capturer."; + return false; + } + + VideoFormat format = ScreencastFormatFromFps(fps); + if (!session_client_->channel_manager()->StartVideoCapture( + capturer, format)) { + LOG(LS_WARNING) << "Could not start video capture."; + video_channel->RemoveScreencast(ssrc); + return false; + } + + if (!video_channel->SetCapturer(ssrc, capturer)) { + LOG(LS_WARNING) << "Could not start sending screencast."; + session_client_->channel_manager()->StopVideoCapture( + capturer, ScreencastFormatFromFps(fps)); + video_channel->RemoveScreencast(ssrc); + } + + // TODO(pthatcher): Once the CaptureManager has a nicer interface + // for removing captures (such as having StartCapture return a + // handle), remove this StartedCapture stuff. + it->second.started_screencasts.insert( + std::make_pair(ssrc, StartedCapture(capturer, format))); + + // TODO(pthatcher): Verify we aren't re-using an existing id or + // ssrc. + StreamParams stream; + stream.id = streamid; + stream.ssrcs.push_back(ssrc); + VideoContentDescription* video = CreateVideoStreamUpdate(stream); + + // TODO(pthatcher): Wait until view request before sending video. + video_channel->SetLocalContent(video, CA_UPDATE); + SendVideoStreamUpdate(session, video); + return true; +} + +bool Call::StopScreencast(Session* session, + const std::string& streamid, uint32 ssrc) { + if (!StopScreencastWithoutSendingUpdate(session, ssrc)) { + return false; + } + + VideoChannel *video_channel = GetVideoChannel(session); + if (!video_channel) { + LOG(LS_WARNING) << "Cannot add screencast" + << " because there is no video channel."; + return false; + } + + StreamParams stream; + stream.id = streamid; + // No ssrcs + VideoContentDescription* video = CreateVideoStreamUpdate(stream); + + video_channel->SetLocalContent(video, CA_UPDATE); + SendVideoStreamUpdate(session, video); + return true; +} + +bool Call::StopScreencastWithoutSendingUpdate( + Session* session, uint32 ssrc) { + MediaSessionMap::iterator it = media_session_map_.find(session->id()); + if (it == media_session_map_.end()) { + return false; + } + + VideoChannel *video_channel = GetVideoChannel(session); + if (!video_channel) { + LOG(LS_WARNING) << "Cannot remove screencast" + << " because there is no video channel."; + return false; + } + + StartedScreencastMap::const_iterator screencast_iter = + it->second.started_screencasts.find(ssrc); + if (screencast_iter == it->second.started_screencasts.end()) { + LOG(LS_WARNING) << "Could not stop screencast " << ssrc + << " because there is no capturer."; + return false; + } + + VideoCapturer* capturer = screencast_iter->second.capturer; + VideoFormat format = screencast_iter->second.format; + video_channel->SetCapturer(ssrc, NULL); + if (!session_client_->channel_manager()->StopVideoCapture( + capturer, format)) { + LOG(LS_WARNING) << "Could not stop screencast " << ssrc + << " because could not stop capture."; + return false; + } + video_channel->RemoveScreencast(ssrc); + it->second.started_screencasts.erase(ssrc); + return true; +} + +VideoContentDescription* Call::CreateVideoStreamUpdate( + const StreamParams& stream) { + VideoContentDescription* video = new VideoContentDescription(); + video->set_multistream(true); + video->set_partial(true); + video->AddStream(stream); + return video; +} + +void Call::SendVideoStreamUpdate( + Session* session, VideoContentDescription* video) { + const ContentInfo* video_info = + GetFirstVideoContent(session->local_description()); + if (video_info == NULL) { + LOG(LS_WARNING) << "Cannot send stream update for video."; + delete video; + return; + } + + std::vector contents; + contents.push_back( + ContentInfo(video_info->name, video_info->type, video)); + + session->SendDescriptionInfoMessage(contents); +} + +void Call::ContinuePlayDTMF() { + playing_dtmf_ = false; + + // Check to see if we have a queued tone + if (queued_dtmf_.size() > 0) { + playing_dtmf_ = true; + + int tone = queued_dtmf_.front(); + queued_dtmf_.pop_front(); + + LOG(LS_INFO) << "Call::ContinuePlayDTMF(" << tone << ")"; + for (MediaSessionMap::iterator it = media_session_map_.begin(); + it != media_session_map_.end(); ++it) { + if (it->second.voice_channel != NULL) { + it->second.voice_channel->PressDTMF(tone, true); + } + } + + // Post a message to play the next tone or at least clear the playing_dtmf_ + // bit. + talk_base::Thread::Current()->PostDelayed(kDTMFDelay, this, MSG_PLAYDTMF); + } +} + +void Call::Join(Call* call, bool enable) { + for (MediaSessionMap::iterator it = call->media_session_map_.begin(); + it != call->media_session_map_.end(); ++it) { + // Shouldn't already exist. + ASSERT(media_session_map_.find(it->first) == media_session_map_.end()); + media_session_map_[it->first] = it->second; + + it->second.session->SignalState.connect(this, &Call::OnSessionState); + it->second.session->SignalError.connect(this, &Call::OnSessionError); + it->second.session->SignalReceivedTerminateReason + .connect(this, &Call::OnReceivedTerminateReason); + + EnableSessionChannels(it->second.session, enable); + } + + // Moved all the sessions over, so the other call should no longer have any. + call->media_session_map_.clear(); +} + +void Call::StartConnectionMonitor(Session* session, int cms) { + VoiceChannel* voice_channel = GetVoiceChannel(session); + if (voice_channel) { + voice_channel->SignalConnectionMonitor.connect(this, + &Call::OnConnectionMonitor); + voice_channel->StartConnectionMonitor(cms); + } + + VideoChannel* video_channel = GetVideoChannel(session); + if (video_channel) { + video_channel->SignalConnectionMonitor.connect(this, + &Call::OnConnectionMonitor); + video_channel->StartConnectionMonitor(cms); + } +} + +void Call::StopConnectionMonitor(Session* session) { + VoiceChannel* voice_channel = GetVoiceChannel(session); + if (voice_channel) { + voice_channel->StopConnectionMonitor(); + voice_channel->SignalConnectionMonitor.disconnect(this); + } + + VideoChannel* video_channel = GetVideoChannel(session); + if (video_channel) { + video_channel->StopConnectionMonitor(); + video_channel->SignalConnectionMonitor.disconnect(this); + } +} + +void Call::StartAudioMonitor(Session* session, int cms) { + VoiceChannel* voice_channel = GetVoiceChannel(session); + if (voice_channel) { + voice_channel->SignalAudioMonitor.connect(this, &Call::OnAudioMonitor); + voice_channel->StartAudioMonitor(cms); + } +} + +void Call::StopAudioMonitor(Session* session) { + VoiceChannel* voice_channel = GetVoiceChannel(session); + if (voice_channel) { + voice_channel->StopAudioMonitor(); + voice_channel->SignalAudioMonitor.disconnect(this); + } +} + +bool Call::IsAudioMonitorRunning(Session* session) { + VoiceChannel* voice_channel = GetVoiceChannel(session); + if (voice_channel) { + return voice_channel->IsAudioMonitorRunning(); + } else { + return false; + } +} + +void Call::StartSpeakerMonitor(Session* session) { + if (speaker_monitor_map_.find(session->id()) == speaker_monitor_map_.end()) { + if (!IsAudioMonitorRunning(session)) { + StartAudioMonitor(session, kAudioMonitorPollPeriodMillis); + } + CurrentSpeakerMonitor* speaker_monitor = + new cricket::CurrentSpeakerMonitor(this, session); + speaker_monitor->SignalUpdate.connect(this, &Call::OnSpeakerMonitor); + speaker_monitor->Start(); + speaker_monitor_map_[session->id()] = speaker_monitor; + } else { + LOG(LS_WARNING) << "Already started speaker monitor for session " + << session->id() << "."; + } +} + +void Call::StopSpeakerMonitor(Session* session) { + if (speaker_monitor_map_.find(session->id()) == speaker_monitor_map_.end()) { + LOG(LS_WARNING) << "Speaker monitor for session " + << session->id() << " already stopped."; + } else { + CurrentSpeakerMonitor* monitor = speaker_monitor_map_[session->id()]; + monitor->Stop(); + speaker_monitor_map_.erase(session->id()); + delete monitor; + } +} + +void Call::OnConnectionMonitor(VoiceChannel* channel, + const std::vector &infos) { + SignalConnectionMonitor(this, infos); +} + +void Call::OnMediaMonitor(VoiceChannel* channel, const VoiceMediaInfo& info) { + last_voice_media_info_ = info; + SignalMediaMonitor(this, info); +} + +void Call::OnAudioMonitor(VoiceChannel* channel, const AudioInfo& info) { + SignalAudioMonitor(this, info); +} + +void Call::OnSpeakerMonitor(CurrentSpeakerMonitor* monitor, uint32 ssrc) { + Session* session = static_cast(monitor->session()); + MediaStreams* recv_streams = GetMediaStreams(session); + if (recv_streams) { + StreamParams stream; + recv_streams->GetAudioStream(StreamSelector(ssrc), &stream); + SignalSpeakerMonitor(this, session, stream); + } +} + +void Call::OnConnectionMonitor(VideoChannel* channel, + const std::vector &infos) { + SignalVideoConnectionMonitor(this, infos); +} + +void Call::OnMediaMonitor(VideoChannel* channel, const VideoMediaInfo& info) { + SignalVideoMediaMonitor(this, info); +} + +void Call::OnDataReceived(DataChannel* channel, + const ReceiveDataParams& params, + const talk_base::Buffer& payload) { + SignalDataReceived(this, params, payload); +} + +uint32 Call::id() { + return id_; +} + +void Call::OnSessionState(BaseSession* base_session, BaseSession::State state) { + Session* session = static_cast(base_session); + switch (state) { + case Session::STATE_RECEIVEDACCEPT: + UpdateRemoteMediaStreams(session, + session->remote_description()->contents(), false); + session_client_->session_manager()->signaling_thread()->Clear(this, + MSG_TERMINATECALL); + break; + case Session::STATE_RECEIVEDREJECT: + case Session::STATE_RECEIVEDTERMINATE: + session_client_->session_manager()->signaling_thread()->Clear(this, + MSG_TERMINATECALL); + break; + default: + break; + } + SignalSessionState(this, session, state); +} + +void Call::OnSessionError(BaseSession* base_session, Session::Error error) { + session_client_->session_manager()->signaling_thread()->Clear(this, + MSG_TERMINATECALL); + SignalSessionError(this, static_cast(base_session), error); +} + +void Call::OnSessionInfoMessage(Session* session, + const buzz::XmlElement* action_elem) { + if (!IsJingleViewRequest(action_elem)) { + return; + } + + ViewRequest view_request; + ParseError error; + if (!ParseJingleViewRequest(action_elem, &view_request, &error)) { + LOG(LS_WARNING) << "Failed to parse view request: " << error.text; + return; + } + + VideoChannel* video_channel = GetVideoChannel(session); + if (video_channel == NULL) { + LOG(LS_WARNING) << "Ignore view request since we have no video channel."; + return; + } + + if (!video_channel->ApplyViewRequest(view_request)) { + LOG(LS_WARNING) << "Failed to ApplyViewRequest."; + } +} + +void Call::OnRemoteDescriptionUpdate(BaseSession* base_session, + const ContentInfos& updated_contents) { + Session* session = static_cast(base_session); + + const ContentInfo* audio_content = GetFirstAudioContent(updated_contents); + if (audio_content) { + const AudioContentDescription* audio_update = + static_cast(audio_content->description); + if (!audio_update->codecs().empty()) { + UpdateVoiceChannelRemoteContent(session, audio_update); + } + } + + const ContentInfo* video_content = GetFirstVideoContent(updated_contents); + if (video_content) { + const VideoContentDescription* video_update = + static_cast(video_content->description); + if (!video_update->codecs().empty()) { + UpdateVideoChannelRemoteContent(session, video_update); + } + } + + const ContentInfo* data_content = GetFirstDataContent(updated_contents); + if (data_content) { + const DataContentDescription* data_update = + static_cast(data_content->description); + if (!data_update->codecs().empty()) { + UpdateDataChannelRemoteContent(session, data_update); + } + } + + UpdateRemoteMediaStreams(session, updated_contents, true); +} + +bool Call::UpdateVoiceChannelRemoteContent( + Session* session, const AudioContentDescription* audio) { + VoiceChannel* voice_channel = GetVoiceChannel(session); + if (!voice_channel->SetRemoteContent(audio, CA_UPDATE)) { + LOG(LS_ERROR) << "Failure in audio SetRemoteContent with CA_UPDATE"; + session->SetError(BaseSession::ERROR_CONTENT); + return false; + } + return true; +} + +bool Call::UpdateVideoChannelRemoteContent( + Session* session, const VideoContentDescription* video) { + VideoChannel* video_channel = GetVideoChannel(session); + if (!video_channel->SetRemoteContent(video, CA_UPDATE)) { + LOG(LS_ERROR) << "Failure in video SetRemoteContent with CA_UPDATE"; + session->SetError(BaseSession::ERROR_CONTENT); + return false; + } + return true; +} + +bool Call::UpdateDataChannelRemoteContent( + Session* session, const DataContentDescription* data) { + DataChannel* data_channel = GetDataChannel(session); + if (!data_channel->SetRemoteContent(data, CA_UPDATE)) { + LOG(LS_ERROR) << "Failure in data SetRemoteContent with CA_UPDATE"; + session->SetError(BaseSession::ERROR_CONTENT); + return false; + } + return true; +} + +void Call::UpdateRemoteMediaStreams(Session* session, + const ContentInfos& updated_contents, + bool update_channels) { + MediaStreams* recv_streams = GetMediaStreams(session); + if (!recv_streams) + return; + + cricket::MediaStreams added_streams; + cricket::MediaStreams removed_streams; + + const ContentInfo* audio_content = GetFirstAudioContent(updated_contents); + if (audio_content) { + const AudioContentDescription* audio_update = + static_cast(audio_content->description); + UpdateRecvStreams(audio_update->streams(), + update_channels ? GetVoiceChannel(session) : NULL, + recv_streams->mutable_audio(), + added_streams.mutable_audio(), + removed_streams.mutable_audio()); + } + + const ContentInfo* video_content = GetFirstVideoContent(updated_contents); + if (video_content) { + const VideoContentDescription* video_update = + static_cast(video_content->description); + UpdateRecvStreams(video_update->streams(), + update_channels ? GetVideoChannel(session) : NULL, + recv_streams->mutable_video(), + added_streams.mutable_video(), + removed_streams.mutable_video()); + } + + const ContentInfo* data_content = GetFirstDataContent(updated_contents); + if (data_content) { + const DataContentDescription* data_update = + static_cast(data_content->description); + UpdateRecvStreams(data_update->streams(), + update_channels ? GetDataChannel(session) : NULL, + recv_streams->mutable_data(), + added_streams.mutable_data(), + removed_streams.mutable_data()); + } + + if (!added_streams.empty() || !removed_streams.empty()) { + SignalMediaStreamsUpdate(this, session, added_streams, removed_streams); + } +} + +void FindStreamChanges(const std::vector& streams, + const std::vector& updates, + std::vector* added_streams, + std::vector* removed_streams) { + for (std::vector::const_iterator update = updates.begin(); + update != updates.end(); ++update) { + StreamParams stream; + if (GetStreamByIds(streams, update->groupid, update->id, &stream)) { + if (!update->has_ssrcs()) { + removed_streams->push_back(stream); + } + } else { + // There's a bug on reflector that will send s even + // though there is not ssrc (which means there isn't really a + // stream). To work around it, we simply ignore new s + // that don't have any ssrcs. + if (update->has_ssrcs()) { + added_streams->push_back(*update); + } + } + } +} + +void Call::UpdateRecvStreams(const std::vector& update_streams, + BaseChannel* channel, + std::vector* recv_streams, + std::vector* added_streams, + std::vector* removed_streams) { + FindStreamChanges(*recv_streams, + update_streams, added_streams, removed_streams); + AddRecvStreams(*added_streams, + channel, recv_streams); + RemoveRecvStreams(*removed_streams, + channel, recv_streams); +} + +void Call::AddRecvStreams(const std::vector& added_streams, + BaseChannel* channel, + std::vector* recv_streams) { + std::vector::const_iterator stream; + for (stream = added_streams.begin(); + stream != added_streams.end(); + ++stream) { + AddRecvStream(*stream, channel, recv_streams); + } +} + +void Call::AddRecvStream(const StreamParams& stream, + BaseChannel* channel, + std::vector* recv_streams) { + if (channel && stream.has_ssrcs()) { + channel->AddRecvStream(stream); + } + recv_streams->push_back(stream); +} + +void Call::RemoveRecvStreams(const std::vector& removed_streams, + BaseChannel* channel, + std::vector* recv_streams) { + std::vector::const_iterator stream; + for (stream = removed_streams.begin(); + stream != removed_streams.end(); + ++stream) { + RemoveRecvStream(*stream, channel, recv_streams); + } +} + +void Call::RemoveRecvStream(const StreamParams& stream, + BaseChannel* channel, + std::vector* recv_streams) { + if (channel && stream.has_ssrcs()) { + // TODO(pthatcher): Change RemoveRecvStream to take a stream argument. + channel->RemoveRecvStream(stream.first_ssrc()); + } + RemoveStreamByIds(recv_streams, stream.groupid, stream.id); +} + +void Call::OnReceivedTerminateReason(Session* session, + const std::string& reason) { + session_client_->session_manager()->signaling_thread()->Clear(this, + MSG_TERMINATECALL); + SignalReceivedTerminateReason(this, session, reason); +} + +} // namespace cricket diff --git a/talk/session/media/call.h b/talk/session/media/call.h new file mode 100644 index 000000000..a604a2b5e --- /dev/null +++ b/talk/session/media/call.h @@ -0,0 +1,275 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_SESSION_MEDIA_CALL_H_ +#define TALK_SESSION_MEDIA_CALL_H_ + +#include +#include +#include +#include + +#include "talk/base/messagequeue.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/screencastid.h" +#include "talk/media/base/streamparams.h" +#include "talk/media/base/videocommon.h" +#include "talk/p2p/base/session.h" +#include "talk/p2p/client/socketmonitor.h" +#include "talk/session/media/audiomonitor.h" +#include "talk/session/media/currentspeakermonitor.h" +#include "talk/session/media/mediamessages.h" +#include "talk/session/media/mediasession.h" +#include "talk/xmpp/jid.h" + +namespace cricket { + +class MediaSessionClient; +class BaseChannel; +class VoiceChannel; +class VideoChannel; +class DataChannel; + +// Can't typedef this easily since it's forward declared as struct elsewhere. +struct CallOptions : public MediaSessionOptions { +}; + +class Call : public talk_base::MessageHandler, public sigslot::has_slots<> { + public: + explicit Call(MediaSessionClient* session_client); + ~Call(); + + // |initiator| can be empty. + Session* InitiateSession(const buzz::Jid& to, const buzz::Jid& initiator, + const CallOptions& options); + void AcceptSession(Session* session, const CallOptions& options); + void RejectSession(Session* session); + void TerminateSession(Session* session); + void Terminate(); + bool SendViewRequest(Session* session, + const ViewRequest& view_request); + void SetLocalRenderer(VideoRenderer* renderer); + void SetVideoRenderer(Session* session, uint32 ssrc, + VideoRenderer* renderer); + void StartConnectionMonitor(Session* session, int cms); + void StopConnectionMonitor(Session* session); + void StartAudioMonitor(Session* session, int cms); + void StopAudioMonitor(Session* session); + bool IsAudioMonitorRunning(Session* session); + void StartSpeakerMonitor(Session* session); + void StopSpeakerMonitor(Session* session); + void Mute(bool mute); + void MuteVideo(bool mute); + bool SendData(Session* session, + const SendDataParams& params, + const talk_base::Buffer& payload, + SendDataResult* result); + void PressDTMF(int event); + bool StartScreencast(Session* session, + const std::string& stream_name, uint32 ssrc, + const ScreencastId& screencastid, int fps); + bool StopScreencast(Session* session, + const std::string& stream_name, uint32 ssrc); + + std::vector sessions(); + uint32 id(); + bool has_video() const { return has_video_; } + bool has_data() const { return has_data_; } + bool muted() const { return muted_; } + bool video_muted() const { return video_muted_; } + const std::vector* GetDataRecvStreams(Session* session) const { + MediaStreams* recv_streams = GetMediaStreams(session); + return recv_streams ? &recv_streams->data() : NULL; + } + const std::vector* GetVideoRecvStreams(Session* session) const { + MediaStreams* recv_streams = GetMediaStreams(session); + return recv_streams ? &recv_streams->video() : NULL; + } + const std::vector* GetAudioRecvStreams(Session* session) const { + MediaStreams* recv_streams = GetMediaStreams(session); + return recv_streams ? &recv_streams->audio() : NULL; + } + // Public just for unit tests + VideoContentDescription* CreateVideoStreamUpdate(const StreamParams& stream); + // Takes ownership of video. + void SendVideoStreamUpdate(Session* session, VideoContentDescription* video); + + // Setting this to false will cause the call to have a longer timeout and + // for the SignalSetupToCallVoicemail to never fire. + void set_send_to_voicemail(bool send_to_voicemail) { + send_to_voicemail_ = send_to_voicemail; + } + bool send_to_voicemail() { return send_to_voicemail_; } + const VoiceMediaInfo& last_voice_media_info() const { + return last_voice_media_info_; + } + + // Sets a flag on the chatapp that will redirect the call to voicemail once + // the call has been terminated + sigslot::signal0<> SignalSetupToCallVoicemail; + sigslot::signal2 SignalAddSession; + sigslot::signal2 SignalRemoveSession; + sigslot::signal3 + SignalSessionState; + sigslot::signal3 + SignalSessionError; + sigslot::signal3 + SignalReceivedTerminateReason; + sigslot::signal2 &> + SignalConnectionMonitor; + sigslot::signal2 SignalMediaMonitor; + sigslot::signal2 SignalAudioMonitor; + // Empty nick on StreamParams means "unknown". + // No ssrcs in StreamParams means "no current speaker". + sigslot::signal3 SignalSpeakerMonitor; + sigslot::signal2 &> + SignalVideoConnectionMonitor; + sigslot::signal2 SignalVideoMediaMonitor; + // Gives added streams and removed streams, in that order. + sigslot::signal4 SignalMediaStreamsUpdate; + sigslot::signal3 SignalDataReceived; + + private: + void OnMessage(talk_base::Message* message); + void OnSessionState(BaseSession* base_session, BaseSession::State state); + void OnSessionError(BaseSession* base_session, Session::Error error); + void OnSessionInfoMessage( + Session* session, const buzz::XmlElement* action_elem); + void OnViewRequest( + Session* session, const ViewRequest& view_request); + void OnRemoteDescriptionUpdate( + BaseSession* base_session, const ContentInfos& updated_contents); + void OnReceivedTerminateReason(Session* session, const std::string &reason); + void IncomingSession(Session* session, const SessionDescription* offer); + // Returns true on success. + bool AddSession(Session* session, const SessionDescription* offer); + void RemoveSession(Session* session); + void EnableChannels(bool enable); + void EnableSessionChannels(Session* session, bool enable); + void Join(Call* call, bool enable); + void OnConnectionMonitor(VoiceChannel* channel, + const std::vector &infos); + void OnMediaMonitor(VoiceChannel* channel, const VoiceMediaInfo& info); + void OnAudioMonitor(VoiceChannel* channel, const AudioInfo& info); + void OnSpeakerMonitor(CurrentSpeakerMonitor* monitor, uint32 ssrc); + void OnConnectionMonitor(VideoChannel* channel, + const std::vector &infos); + void OnMediaMonitor(VideoChannel* channel, const VideoMediaInfo& info); + void OnDataReceived(DataChannel* channel, + const ReceiveDataParams& params, + const talk_base::Buffer& payload); + VoiceChannel* GetVoiceChannel(Session* session) const; + VideoChannel* GetVideoChannel(Session* session) const; + DataChannel* GetDataChannel(Session* session) const; + MediaStreams* GetMediaStreams(Session* session) const; + void UpdateRemoteMediaStreams(Session* session, + const ContentInfos& updated_contents, + bool update_channels); + bool UpdateVoiceChannelRemoteContent(Session* session, + const AudioContentDescription* audio); + bool UpdateVideoChannelRemoteContent(Session* session, + const VideoContentDescription* video); + bool UpdateDataChannelRemoteContent(Session* session, + const DataContentDescription* data); + void UpdateRecvStreams(const std::vector& update_streams, + BaseChannel* channel, + std::vector* recv_streams, + std::vector* added_streams, + std::vector* removed_streams); + void AddRecvStreams(const std::vector& added_streams, + BaseChannel* channel, + std::vector* recv_streams); + void AddRecvStream(const StreamParams& stream, + BaseChannel* channel, + std::vector* recv_streams); + void RemoveRecvStreams(const std::vector& removed_streams, + BaseChannel* channel, + std::vector* recv_streams); + void RemoveRecvStream(const StreamParams& stream, + BaseChannel* channel, + std::vector* recv_streams); + void ContinuePlayDTMF(); + bool StopScreencastWithoutSendingUpdate(Session* session, uint32 ssrc); + bool StopAllScreencastsWithoutSendingUpdate(Session* session); + + uint32 id_; + MediaSessionClient* session_client_; + + struct StartedCapture { + StartedCapture(cricket::VideoCapturer* capturer, + const cricket::VideoFormat& format) : + capturer(capturer), + format(format) { + } + cricket::VideoCapturer* capturer; + cricket::VideoFormat format; + }; + typedef std::map StartedScreencastMap; + + struct MediaSession { + Session* session; + VoiceChannel* voice_channel; + VideoChannel* video_channel; + DataChannel* data_channel; + MediaStreams* recv_streams; + StartedScreencastMap started_screencasts; + }; + + // Create a map of media sessions, keyed off session->id(). + typedef std::map MediaSessionMap; + MediaSessionMap media_session_map_; + + std::map speaker_monitor_map_; + VideoRenderer* local_renderer_; + bool has_video_; + bool has_data_; + bool muted_; + bool video_muted_; + bool send_to_voicemail_; + + // DTMF tones have to be queued up so that we don't flood the call. We + // keep a deque (doubely ended queue) of them around. While one is playing we + // set the playing_dtmf_ bit and schedule a message in XX msec to clear that + // bit or start the next tone playing. + std::deque queued_dtmf_; + bool playing_dtmf_; + + VoiceMediaInfo last_voice_media_info_; + + friend class MediaSessionClient; +}; + +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_CALL_H_ diff --git a/talk/session/media/channel.cc b/talk/session/media/channel.cc new file mode 100644 index 000000000..1048cdf7f --- /dev/null +++ b/talk/session/media/channel.cc @@ -0,0 +1,2704 @@ +/* + * libjingle + * Copyright 2004 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/media/channel.h" + +#include "talk/base/buffer.h" +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/media/base/rtputils.h" +#include "talk/p2p/base/transportchannel.h" +#include "talk/session/media/channelmanager.h" +#include "talk/session/media/mediamessages.h" +#include "talk/session/media/rtcpmuxfilter.h" +#include "talk/session/media/typingmonitor.h" + + +namespace cricket { + +enum { + MSG_ENABLE = 1, + MSG_DISABLE, + MSG_MUTESTREAM, + MSG_ISSTREAMMUTED, + MSG_SETREMOTECONTENT, + MSG_SETLOCALCONTENT, + MSG_EARLYMEDIATIMEOUT, + MSG_CANINSERTDTMF, + MSG_INSERTDTMF, + MSG_GETSTATS, + MSG_SETRENDERER, + MSG_ADDRECVSTREAM, + MSG_REMOVERECVSTREAM, + MSG_SETRINGBACKTONE, + MSG_PLAYRINGBACKTONE, + MSG_SETMAXSENDBANDWIDTH, + MSG_ADDSCREENCAST, + MSG_REMOVESCREENCAST, + MSG_SENDINTRAFRAME, + MSG_REQUESTINTRAFRAME, + MSG_SCREENCASTWINDOWEVENT, + MSG_RTPPACKET, + MSG_RTCPPACKET, + MSG_CHANNEL_ERROR, + MSG_SETCHANNELOPTIONS, + MSG_SCALEVOLUME, + MSG_HANDLEVIEWREQUEST, + MSG_READYTOSENDDATA, + MSG_SENDDATA, + MSG_DATARECEIVED, + MSG_SETCAPTURER, + MSG_ISSCREENCASTING, + MSG_SCREENCASTFPS, + MSG_SETSCREENCASTFACTORY, + MSG_FIRSTPACKETRECEIVED, + MSG_SESSION_ERROR, +}; + +// Value specified in RFC 5764. +static const char kDtlsSrtpExporterLabel[] = "EXTRACTOR-dtls_srtp"; + +static const int kAgcMinus10db = -10; + +// TODO(hellner): use the device manager for creation of screen capturers when +// the cl enabling it has landed. +class NullScreenCapturerFactory : public VideoChannel::ScreenCapturerFactory { + public: + VideoCapturer* CreateScreenCapturer(const ScreencastId& window) { + return NULL; + } +}; + + +VideoChannel::ScreenCapturerFactory* CreateScreenCapturerFactory() { + return new NullScreenCapturerFactory(); +} + +struct SetContentData : public talk_base::MessageData { + SetContentData(const MediaContentDescription* content, ContentAction action) + : content(content), + action(action), + result(false) { + } + const MediaContentDescription* content; + ContentAction action; + bool result; +}; + +struct SetBandwidthData : public talk_base::MessageData { + explicit SetBandwidthData(int value) : value(value), result(false) {} + int value; + bool result; +}; + +struct SetRingbackToneMessageData : public talk_base::MessageData { + SetRingbackToneMessageData(const void* b, int l) + : buf(b), + len(l), + result(false) { + } + const void* buf; + int len; + bool result; +}; + +struct PlayRingbackToneMessageData : public talk_base::MessageData { + PlayRingbackToneMessageData(uint32 s, bool p, bool l) + : ssrc(s), + play(p), + loop(l), + result(false) { + } + uint32 ssrc; + bool play; + bool loop; + bool result; +}; +typedef talk_base::TypedMessageData BoolMessageData; +struct DtmfMessageData : public talk_base::MessageData { + DtmfMessageData(uint32 ssrc, int event, int duration, int flags) + : ssrc(ssrc), + event(event), + duration(duration), + flags(flags), + result(false) { + } + uint32 ssrc; + int event; + int duration; + int flags; + bool result; +}; +struct ScaleVolumeMessageData : public talk_base::MessageData { + ScaleVolumeMessageData(uint32 s, double l, double r) + : ssrc(s), + left(l), + right(r), + result(false) { + } + uint32 ssrc; + double left; + double right; + bool result; +}; + +struct VoiceStatsMessageData : public talk_base::MessageData { + explicit VoiceStatsMessageData(VoiceMediaInfo* stats) + : result(false), + stats(stats) { + } + bool result; + VoiceMediaInfo* stats; +}; + +struct VideoStatsMessageData : public talk_base::MessageData { + explicit VideoStatsMessageData(VideoMediaInfo* stats) + : result(false), + stats(stats) { + } + bool result; + VideoMediaInfo* stats; +}; + +struct PacketMessageData : public talk_base::MessageData { + talk_base::Buffer packet; +}; + +struct AudioRenderMessageData: public talk_base::MessageData { + AudioRenderMessageData(uint32 s, AudioRenderer* r) + : ssrc(s), renderer(r), result(false) {} + uint32 ssrc; + AudioRenderer* renderer; + bool result; +}; + +struct VideoRenderMessageData : public talk_base::MessageData { + VideoRenderMessageData(uint32 s, VideoRenderer* r) : ssrc(s), renderer(r) {} + uint32 ssrc; + VideoRenderer* renderer; +}; + +struct AddScreencastMessageData : public talk_base::MessageData { + AddScreencastMessageData(uint32 s, const ScreencastId& id) + : ssrc(s), + window_id(id), + result(NULL) { + } + uint32 ssrc; + ScreencastId window_id; + VideoCapturer* result; +}; + +struct RemoveScreencastMessageData : public talk_base::MessageData { + explicit RemoveScreencastMessageData(uint32 s) : ssrc(s), result(false) {} + uint32 ssrc; + bool result; +}; + +struct ScreencastEventMessageData : public talk_base::MessageData { + ScreencastEventMessageData(uint32 s, talk_base::WindowEvent we) + : ssrc(s), + event(we) { + } + uint32 ssrc; + talk_base::WindowEvent event; +}; + +struct ViewRequestMessageData : public talk_base::MessageData { + explicit ViewRequestMessageData(const ViewRequest& r) + : request(r), + result(false) { + } + ViewRequest request; + bool result; +}; + +struct VoiceChannelErrorMessageData : public talk_base::MessageData { + VoiceChannelErrorMessageData(uint32 in_ssrc, + VoiceMediaChannel::Error in_error) + : ssrc(in_ssrc), + error(in_error) { + } + uint32 ssrc; + VoiceMediaChannel::Error error; +}; + +struct VideoChannelErrorMessageData : public talk_base::MessageData { + VideoChannelErrorMessageData(uint32 in_ssrc, + VideoMediaChannel::Error in_error) + : ssrc(in_ssrc), + error(in_error) { + } + uint32 ssrc; + VideoMediaChannel::Error error; +}; + +struct DataChannelErrorMessageData : public talk_base::MessageData { + DataChannelErrorMessageData(uint32 in_ssrc, + DataMediaChannel::Error in_error) + : ssrc(in_ssrc), + error(in_error) {} + uint32 ssrc; + DataMediaChannel::Error error; +}; + +struct SessionErrorMessageData : public talk_base::MessageData { + explicit SessionErrorMessageData(cricket::BaseSession::Error error) + : error_(error) {} + + BaseSession::Error error_; +}; + +struct SsrcMessageData : public talk_base::MessageData { + explicit SsrcMessageData(uint32 ssrc) : ssrc(ssrc), result(false) {} + uint32 ssrc; + bool result; +}; + +struct StreamMessageData : public talk_base::MessageData { + explicit StreamMessageData(const StreamParams& in_sp) + : sp(in_sp), + result(false) { + } + StreamParams sp; + bool result; +}; + +struct MuteStreamData : public talk_base::MessageData { + MuteStreamData(uint32 ssrc, bool mute) + : ssrc(ssrc), mute(mute), result(false) {} + uint32 ssrc; + bool mute; + bool result; +}; + +struct AudioOptionsMessageData : public talk_base::MessageData { + explicit AudioOptionsMessageData(const AudioOptions& options) + : options(options), + result(false) { + } + AudioOptions options; + bool result; +}; + +struct VideoOptionsMessageData : public talk_base::MessageData { + explicit VideoOptionsMessageData(const VideoOptions& options) + : options(options), + result(false) { + } + VideoOptions options; + bool result; +}; + +struct SetCapturerMessageData : public talk_base::MessageData { + SetCapturerMessageData(uint32 s, VideoCapturer* c) + : ssrc(s), + capturer(c), + result(false) { + } + uint32 ssrc; + VideoCapturer* capturer; + bool result; +}; + +struct IsScreencastingMessageData : public talk_base::MessageData { + IsScreencastingMessageData() + : result(false) { + } + bool result; +}; + +struct ScreencastFpsMessageData : public talk_base::MessageData { + explicit ScreencastFpsMessageData(uint32 s) + : ssrc(s), result(0) { + } + uint32 ssrc; + int result; +}; + +struct SetScreenCaptureFactoryMessageData : public talk_base::MessageData { + explicit SetScreenCaptureFactoryMessageData( + VideoChannel::ScreenCapturerFactory* f) + : screencapture_factory(f) { + } + VideoChannel::ScreenCapturerFactory* screencapture_factory; +}; + +static const char* PacketType(bool rtcp) { + return (!rtcp) ? "RTP" : "RTCP"; +} + +static bool ValidPacket(bool rtcp, const talk_base::Buffer* packet) { + // Check the packet size. We could check the header too if needed. + return (packet && + packet->length() >= (!rtcp ? kMinRtpPacketLen : kMinRtcpPacketLen) && + packet->length() <= kMaxRtpPacketLen); +} + +static bool IsReceiveContentDirection(MediaContentDirection direction) { + return direction == MD_SENDRECV || direction == MD_RECVONLY; +} + +static bool IsSendContentDirection(MediaContentDirection direction) { + return direction == MD_SENDRECV || direction == MD_SENDONLY; +} + +static const MediaContentDescription* GetContentDescription( + const ContentInfo* cinfo) { + if (cinfo == NULL) + return NULL; + return static_cast(cinfo->description); +} + +BaseChannel::BaseChannel(talk_base::Thread* thread, + MediaEngineInterface* media_engine, + MediaChannel* media_channel, BaseSession* session, + const std::string& content_name, bool rtcp) + : worker_thread_(thread), + media_engine_(media_engine), + session_(session), + media_channel_(media_channel), + content_name_(content_name), + rtcp_(rtcp), + transport_channel_(NULL), + rtcp_transport_channel_(NULL), + enabled_(false), + writable_(false), + rtp_ready_to_send_(false), + rtcp_ready_to_send_(false), + optimistic_data_send_(false), + was_ever_writable_(false), + local_content_direction_(MD_INACTIVE), + remote_content_direction_(MD_INACTIVE), + has_received_packet_(false), + dtls_keyed_(false), + secure_required_(false) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + LOG(LS_INFO) << "Created channel for " << content_name; +} + +BaseChannel::~BaseChannel() { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + StopConnectionMonitor(); + FlushRtcpMessages(); // Send any outstanding RTCP packets. + Clear(); // eats any outstanding messages or packets + // We must destroy the media channel before the transport channel, otherwise + // the media channel may try to send on the dead transport channel. NULLing + // is not an effective strategy since the sends will come on another thread. + delete media_channel_; + set_rtcp_transport_channel(NULL); + if (transport_channel_ != NULL) + session_->DestroyChannel(content_name_, transport_channel_->component()); + LOG(LS_INFO) << "Destroyed channel"; +} + +bool BaseChannel::Init(TransportChannel* transport_channel, + TransportChannel* rtcp_transport_channel) { + if (transport_channel == NULL) { + return false; + } + if (rtcp() && rtcp_transport_channel == NULL) { + return false; + } + transport_channel_ = transport_channel; + + if (!SetDtlsSrtpCiphers(transport_channel_, false)) { + return false; + } + + media_channel_->SetInterface(this); + transport_channel_->SignalWritableState.connect( + this, &BaseChannel::OnWritableState); + transport_channel_->SignalReadPacket.connect( + this, &BaseChannel::OnChannelRead); + transport_channel_->SignalReadyToSend.connect( + this, &BaseChannel::OnReadyToSend); + + session_->SignalNewLocalDescription.connect( + this, &BaseChannel::OnNewLocalDescription); + session_->SignalNewRemoteDescription.connect( + this, &BaseChannel::OnNewRemoteDescription); + + set_rtcp_transport_channel(rtcp_transport_channel); + return true; +} + +// Can be called from thread other than worker thread +bool BaseChannel::Enable(bool enable) { + Send(enable ? MSG_ENABLE : MSG_DISABLE); + return true; +} + +// Can be called from thread other than worker thread +bool BaseChannel::MuteStream(uint32 ssrc, bool mute) { + MuteStreamData data(ssrc, mute); + Send(MSG_MUTESTREAM, &data); + return data.result; +} + +bool BaseChannel::IsStreamMuted(uint32 ssrc) { + SsrcMessageData data(ssrc); + Send(MSG_ISSTREAMMUTED, &data); + return data.result; +} + +bool BaseChannel::AddRecvStream(const StreamParams& sp) { + StreamMessageData data(sp); + Send(MSG_ADDRECVSTREAM, &data); + return data.result; +} + +bool BaseChannel::RemoveRecvStream(uint32 ssrc) { + SsrcMessageData data(ssrc); + Send(MSG_REMOVERECVSTREAM, &data); + return data.result; +} + +bool BaseChannel::SetLocalContent(const MediaContentDescription* content, + ContentAction action) { + SetContentData data(content, action); + Send(MSG_SETLOCALCONTENT, &data); + return data.result; +} + +bool BaseChannel::SetRemoteContent(const MediaContentDescription* content, + ContentAction action) { + SetContentData data(content, action); + Send(MSG_SETREMOTECONTENT, &data); + return data.result; +} + +bool BaseChannel::SetMaxSendBandwidth(int max_bandwidth) { + SetBandwidthData data(max_bandwidth); + Send(MSG_SETMAXSENDBANDWIDTH, &data); + return data.result; +} + +void BaseChannel::StartConnectionMonitor(int cms) { + socket_monitor_.reset(new SocketMonitor(transport_channel_, + worker_thread(), + talk_base::Thread::Current())); + socket_monitor_->SignalUpdate.connect( + this, &BaseChannel::OnConnectionMonitorUpdate); + socket_monitor_->Start(cms); +} + +void BaseChannel::StopConnectionMonitor() { + if (socket_monitor_) { + socket_monitor_->Stop(); + socket_monitor_.reset(); + } +} + +void BaseChannel::set_rtcp_transport_channel(TransportChannel* channel) { + if (rtcp_transport_channel_ != channel) { + if (rtcp_transport_channel_) { + session_->DestroyChannel( + content_name_, rtcp_transport_channel_->component()); + } + rtcp_transport_channel_ = channel; + if (rtcp_transport_channel_) { + // TODO(juberti): Propagate this error code + VERIFY(SetDtlsSrtpCiphers(rtcp_transport_channel_, true)); + rtcp_transport_channel_->SignalWritableState.connect( + this, &BaseChannel::OnWritableState); + rtcp_transport_channel_->SignalReadPacket.connect( + this, &BaseChannel::OnChannelRead); + rtcp_transport_channel_->SignalReadyToSend.connect( + this, &BaseChannel::OnReadyToSend); + } + } +} + +bool BaseChannel::IsReadyToReceive() const { + // Receive data if we are enabled and have local content, + return enabled() && IsReceiveContentDirection(local_content_direction_); +} + +bool BaseChannel::IsReadyToSend() const { + // Send outgoing data if we are enabled, have local and remote content, + // and we have had some form of connectivity. + return enabled() && + IsReceiveContentDirection(remote_content_direction_) && + IsSendContentDirection(local_content_direction_) && + was_ever_writable(); +} + +bool BaseChannel::SendPacket(talk_base::Buffer* packet) { + return SendPacket(false, packet); +} + +bool BaseChannel::SendRtcp(talk_base::Buffer* packet) { + return SendPacket(true, packet); +} + +int BaseChannel::SetOption(SocketType type, talk_base::Socket::Option opt, + int value) { + switch (type) { + case ST_RTP: return transport_channel_->SetOption(opt, value); + case ST_RTCP: return rtcp_transport_channel_->SetOption(opt, value); + default: return -1; + } +} + +void BaseChannel::OnWritableState(TransportChannel* channel) { + ASSERT(channel == transport_channel_ || channel == rtcp_transport_channel_); + if (transport_channel_->writable() + && (!rtcp_transport_channel_ || rtcp_transport_channel_->writable())) { + ChannelWritable_w(); + } else { + ChannelNotWritable_w(); + } +} + +void BaseChannel::OnChannelRead(TransportChannel* channel, + const char* data, size_t len, int flags) { + // OnChannelRead gets called from P2PSocket; now pass data to MediaEngine + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + // When using RTCP multiplexing we might get RTCP packets on the RTP + // transport. We feed RTP traffic into the demuxer to determine if it is RTCP. + bool rtcp = PacketIsRtcp(channel, data, len); + talk_base::Buffer packet(data, len); + HandlePacket(rtcp, &packet); +} + +void BaseChannel::OnReadyToSend(TransportChannel* channel) { + SetReadyToSend(channel, true); +} + +void BaseChannel::SetReadyToSend(TransportChannel* channel, bool ready) { + ASSERT(channel == transport_channel_ || channel == rtcp_transport_channel_); + if (channel == transport_channel_) { + rtp_ready_to_send_ = ready; + } + if (channel == rtcp_transport_channel_) { + rtcp_ready_to_send_ = ready; + } + + if (!ready) { + // Notify the MediaChannel when either rtp or rtcp channel can't send. + media_channel_->OnReadyToSend(false); + } else if (rtp_ready_to_send_ && + // In the case of rtcp mux |rtcp_transport_channel_| will be null. + (rtcp_ready_to_send_ || !rtcp_transport_channel_)) { + // Notify the MediaChannel when both rtp and rtcp channel can send. + media_channel_->OnReadyToSend(true); + } +} + +bool BaseChannel::PacketIsRtcp(const TransportChannel* channel, + const char* data, size_t len) { + return (channel == rtcp_transport_channel_ || + rtcp_mux_filter_.DemuxRtcp(data, len)); +} + +bool BaseChannel::SendPacket(bool rtcp, talk_base::Buffer* packet) { + // Unless we're sending optimistically, we only allow packets through when we + // are completely writable. + if (!optimistic_data_send_ && !writable_) { + return false; + } + + // SendPacket gets called from MediaEngine, typically on an encoder thread. + // If the thread is not our worker thread, we will post to our worker + // so that the real work happens on our worker. This avoids us having to + // synchronize access to all the pieces of the send path, including + // SRTP and the inner workings of the transport channels. + // The only downside is that we can't return a proper failure code if + // needed. Since UDP is unreliable anyway, this should be a non-issue. + if (talk_base::Thread::Current() != worker_thread_) { + // Avoid a copy by transferring the ownership of the packet data. + int message_id = (!rtcp) ? MSG_RTPPACKET : MSG_RTCPPACKET; + PacketMessageData* data = new PacketMessageData; + packet->TransferTo(&data->packet); + worker_thread_->Post(this, message_id, data); + return true; + } + + // Now that we are on the correct thread, ensure we have a place to send this + // packet before doing anything. (We might get RTCP packets that we don't + // intend to send.) If we've negotiated RTCP mux, send RTCP over the RTP + // transport. + TransportChannel* channel = (!rtcp || rtcp_mux_filter_.IsActive()) ? + transport_channel_ : rtcp_transport_channel_; + if (!channel || (!optimistic_data_send_ && !channel->writable())) { + return false; + } + + // Protect ourselves against crazy data. + if (!ValidPacket(rtcp, packet)) { + LOG(LS_ERROR) << "Dropping outgoing " << content_name_ << " " + << PacketType(rtcp) << " packet: wrong size=" + << packet->length(); + return false; + } + + // Signal to the media sink before protecting the packet. + { + talk_base::CritScope cs(&signal_send_packet_cs_); + SignalSendPacketPreCrypto(packet->data(), packet->length(), rtcp); + } + + // Protect if needed. + if (srtp_filter_.IsActive()) { + bool res; + char* data = packet->data(); + int len = packet->length(); + if (!rtcp) { + res = srtp_filter_.ProtectRtp(data, len, packet->capacity(), &len); + if (!res) { + int seq_num = -1; + uint32 ssrc = 0; + GetRtpSeqNum(data, len, &seq_num); + GetRtpSsrc(data, len, &ssrc); + LOG(LS_ERROR) << "Failed to protect " << content_name_ + << " RTP packet: size=" << len + << ", seqnum=" << seq_num << ", SSRC=" << ssrc; + return false; + } + } else { + res = srtp_filter_.ProtectRtcp(data, len, packet->capacity(), &len); + if (!res) { + int type = -1; + GetRtcpType(data, len, &type); + LOG(LS_ERROR) << "Failed to protect " << content_name_ + << " RTCP packet: size=" << len << ", type=" << type; + return false; + } + } + + // Update the length of the packet now that we've added the auth tag. + packet->SetLength(len); + } else if (secure_required_) { + // This is a double check for something that supposedly can't happen. + LOG(LS_ERROR) << "Can't send outgoing " << PacketType(rtcp) + << " packet when SRTP is inactive and crypto is required"; + + ASSERT(false); + return false; + } + + // Signal to the media sink after protecting the packet. + { + talk_base::CritScope cs(&signal_send_packet_cs_); + SignalSendPacketPostCrypto(packet->data(), packet->length(), rtcp); + } + + // Bon voyage. + int ret = channel->SendPacket(packet->data(), packet->length(), + (secure() && secure_dtls()) ? PF_SRTP_BYPASS : 0); + if (ret != static_cast(packet->length())) { + if (channel->GetError() == EWOULDBLOCK) { + LOG(LS_WARNING) << "Got EWOULDBLOCK from socket."; + SetReadyToSend(channel, false); + } + return false; + } + return true; +} + +bool BaseChannel::WantsPacket(bool rtcp, talk_base::Buffer* packet) { + // Protect ourselves against crazy data. + if (!ValidPacket(rtcp, packet)) { + LOG(LS_ERROR) << "Dropping incoming " << content_name_ << " " + << PacketType(rtcp) << " packet: wrong size=" + << packet->length(); + return false; + } + // If this channel is suppose to handle RTP data, that is determined by + // checking against ssrc filter. This is necessary to do it here to avoid + // double decryption. + if (ssrc_filter_.IsActive() && + !ssrc_filter_.DemuxPacket(packet->data(), packet->length(), rtcp)) { + return false; + } + + return true; +} + +void BaseChannel::HandlePacket(bool rtcp, talk_base::Buffer* packet) { + if (!WantsPacket(rtcp, packet)) { + return; + } + + if (!has_received_packet_) { + has_received_packet_ = true; + signaling_thread()->Post(this, MSG_FIRSTPACKETRECEIVED); + } + + // Signal to the media sink before unprotecting the packet. + { + talk_base::CritScope cs(&signal_recv_packet_cs_); + SignalRecvPacketPostCrypto(packet->data(), packet->length(), rtcp); + } + + // Unprotect the packet, if needed. + if (srtp_filter_.IsActive()) { + char* data = packet->data(); + int len = packet->length(); + bool res; + if (!rtcp) { + res = srtp_filter_.UnprotectRtp(data, len, &len); + if (!res) { + int seq_num = -1; + uint32 ssrc = 0; + GetRtpSeqNum(data, len, &seq_num); + GetRtpSsrc(data, len, &ssrc); + LOG(LS_ERROR) << "Failed to unprotect " << content_name_ + << " RTP packet: size=" << len + << ", seqnum=" << seq_num << ", SSRC=" << ssrc; + return; + } + } else { + res = srtp_filter_.UnprotectRtcp(data, len, &len); + if (!res) { + int type = -1; + GetRtcpType(data, len, &type); + LOG(LS_ERROR) << "Failed to unprotect " << content_name_ + << " RTCP packet: size=" << len << ", type=" << type; + return; + } + } + + packet->SetLength(len); + } else if (secure_required_) { + // Our session description indicates that SRTP is required, but we got a + // packet before our SRTP filter is active. This means either that + // a) we got SRTP packets before we received the SDES keys, in which case + // we can't decrypt it anyway, or + // b) we got SRTP packets before DTLS completed on both the RTP and RTCP + // channels, so we haven't yet extracted keys, even if DTLS did complete + // on the channel that the packets are being sent on. It's really good + // practice to wait for both RTP and RTCP to be good to go before sending + // media, to prevent weird failure modes, so it's fine for us to just eat + // packets here. This is all sidestepped if RTCP mux is used anyway. + LOG(LS_WARNING) << "Can't process incoming " << PacketType(rtcp) + << " packet when SRTP is inactive and crypto is required"; + return; + } + + // Signal to the media sink after unprotecting the packet. + { + talk_base::CritScope cs(&signal_recv_packet_cs_); + SignalRecvPacketPreCrypto(packet->data(), packet->length(), rtcp); + } + + // Push it down to the media channel. + if (!rtcp) { + media_channel_->OnPacketReceived(packet); + } else { + media_channel_->OnRtcpReceived(packet); + } +} + +void BaseChannel::OnNewLocalDescription( + BaseSession* session, ContentAction action) { + const ContentInfo* content_info = + GetFirstContent(session->local_description()); + const MediaContentDescription* content_desc = + GetContentDescription(content_info); + if (content_desc && content_info && !content_info->rejected && + !SetLocalContent(content_desc, action)) { + LOG(LS_ERROR) << "Failure in SetLocalContent with action " << action; + session->SetError(BaseSession::ERROR_CONTENT); + } +} + +void BaseChannel::OnNewRemoteDescription( + BaseSession* session, ContentAction action) { + const ContentInfo* content_info = + GetFirstContent(session->remote_description()); + const MediaContentDescription* content_desc = + GetContentDescription(content_info); + if (content_desc && content_info && !content_info->rejected && + !SetRemoteContent(content_desc, action)) { + LOG(LS_ERROR) << "Failure in SetRemoteContent with action " << action; + session->SetError(BaseSession::ERROR_CONTENT); + } +} + +void BaseChannel::EnableMedia_w() { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + if (enabled_) + return; + + LOG(LS_INFO) << "Channel enabled"; + enabled_ = true; + ChangeState(); +} + +void BaseChannel::DisableMedia_w() { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + if (!enabled_) + return; + + LOG(LS_INFO) << "Channel disabled"; + enabled_ = false; + ChangeState(); +} + +bool BaseChannel::MuteStream_w(uint32 ssrc, bool mute) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + bool ret = media_channel()->MuteStream(ssrc, mute); + if (ret) { + if (mute) + muted_streams_.insert(ssrc); + else + muted_streams_.erase(ssrc); + } + return ret; +} + +bool BaseChannel::IsStreamMuted_w(uint32 ssrc) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + return muted_streams_.find(ssrc) != muted_streams_.end(); +} + +void BaseChannel::ChannelWritable_w() { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + if (writable_) + return; + + LOG(LS_INFO) << "Channel socket writable (" + << transport_channel_->content_name() << ", " + << transport_channel_->component() << ")" + << (was_ever_writable_ ? "" : " for the first time"); + + std::vector infos; + transport_channel_->GetStats(&infos); + for (std::vector::const_iterator it = infos.begin(); + it != infos.end(); ++it) { + if (it->best_connection) { + LOG(LS_INFO) << "Using " << it->local_candidate.ToSensitiveString() + << "->" << it->remote_candidate.ToSensitiveString(); + break; + } + } + + // If we're doing DTLS-SRTP, now is the time. + if (!was_ever_writable_ && ShouldSetupDtlsSrtp()) { + if (!SetupDtlsSrtp(false)) { + LOG(LS_ERROR) << "Couldn't finish DTLS-SRTP on RTP channel"; + SessionErrorMessageData data(BaseSession::ERROR_TRANSPORT); + // Sent synchronously. + signaling_thread()->Send(this, MSG_SESSION_ERROR, &data); + return; + } + + if (rtcp_transport_channel_) { + if (!SetupDtlsSrtp(true)) { + LOG(LS_ERROR) << "Couldn't finish DTLS-SRTP on RTCP channel"; + SessionErrorMessageData data(BaseSession::ERROR_TRANSPORT); + // Sent synchronously. + signaling_thread()->Send(this, MSG_SESSION_ERROR, &data); + return; + } + } + } + + was_ever_writable_ = true; + writable_ = true; + ChangeState(); +} + +bool BaseChannel::SetDtlsSrtpCiphers(TransportChannel *tc, bool rtcp) { + std::vector ciphers; + // We always use the default SRTP ciphers for RTCP, but we may use different + // ciphers for RTP depending on the media type. + if (!rtcp) { + GetSrtpCiphers(&ciphers); + } else { + GetSupportedDefaultCryptoSuites(&ciphers); + } + return tc->SetSrtpCiphers(ciphers); +} + +bool BaseChannel::ShouldSetupDtlsSrtp() const { + return true; +} + +// This function returns true if either DTLS-SRTP is not in use +// *or* DTLS-SRTP is successfully set up. +bool BaseChannel::SetupDtlsSrtp(bool rtcp_channel) { + bool ret = false; + + TransportChannel *channel = rtcp_channel ? + rtcp_transport_channel_ : transport_channel_; + + // No DTLS + if (!channel->IsDtlsActive()) + return true; + + std::string selected_cipher; + + if (!channel->GetSrtpCipher(&selected_cipher)) { + LOG(LS_ERROR) << "No DTLS-SRTP selected cipher"; + return false; + } + + LOG(LS_INFO) << "Installing keys from DTLS-SRTP on " + << content_name() << " " + << PacketType(rtcp_channel); + + // OK, we're now doing DTLS (RFC 5764) + std::vector dtls_buffer(SRTP_MASTER_KEY_KEY_LEN * 2 + + SRTP_MASTER_KEY_SALT_LEN * 2); + + // RFC 5705 exporter using the RFC 5764 parameters + if (!channel->ExportKeyingMaterial( + kDtlsSrtpExporterLabel, + NULL, 0, false, + &dtls_buffer[0], dtls_buffer.size())) { + LOG(LS_WARNING) << "DTLS-SRTP key export failed"; + ASSERT(false); // This should never happen + return false; + } + + // Sync up the keys with the DTLS-SRTP interface + std::vector client_write_key(SRTP_MASTER_KEY_KEY_LEN + + SRTP_MASTER_KEY_SALT_LEN); + std::vector server_write_key(SRTP_MASTER_KEY_KEY_LEN + + SRTP_MASTER_KEY_SALT_LEN); + size_t offset = 0; + memcpy(&client_write_key[0], &dtls_buffer[offset], + SRTP_MASTER_KEY_KEY_LEN); + offset += SRTP_MASTER_KEY_KEY_LEN; + memcpy(&server_write_key[0], &dtls_buffer[offset], + SRTP_MASTER_KEY_KEY_LEN); + offset += SRTP_MASTER_KEY_KEY_LEN; + memcpy(&client_write_key[SRTP_MASTER_KEY_KEY_LEN], + &dtls_buffer[offset], SRTP_MASTER_KEY_SALT_LEN); + offset += SRTP_MASTER_KEY_SALT_LEN; + memcpy(&server_write_key[SRTP_MASTER_KEY_KEY_LEN], + &dtls_buffer[offset], SRTP_MASTER_KEY_SALT_LEN); + + std::vector *send_key, *recv_key; + + if (channel->GetRole() == ROLE_CONTROLLING) { + send_key = &server_write_key; + recv_key = &client_write_key; + } else { + send_key = &client_write_key; + recv_key = &server_write_key; + } + + if (rtcp_channel) { + ret = srtp_filter_.SetRtcpParams(selected_cipher, + &(*send_key)[0], send_key->size(), + selected_cipher, + &(*recv_key)[0], recv_key->size()); + } else { + ret = srtp_filter_.SetRtpParams(selected_cipher, + &(*send_key)[0], send_key->size(), + selected_cipher, + &(*recv_key)[0], recv_key->size()); + } + + if (!ret) + LOG(LS_WARNING) << "DTLS-SRTP key installation failed"; + else + dtls_keyed_ = true; + + return ret; +} + +void BaseChannel::ChannelNotWritable_w() { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + if (!writable_) + return; + + LOG(LS_INFO) << "Channel socket not writable (" + << transport_channel_->content_name() << ", " + << transport_channel_->component() << ")"; + writable_ = false; + ChangeState(); +} + +// Sets the maximum video bandwidth for automatic bandwidth adjustment. +bool BaseChannel::SetMaxSendBandwidth_w(int max_bandwidth) { + return media_channel()->SetSendBandwidth(true, max_bandwidth); +} + +bool BaseChannel::SetSrtp_w(const std::vector& cryptos, + ContentAction action, ContentSource src) { + bool ret = false; + switch (action) { + case CA_OFFER: + ret = srtp_filter_.SetOffer(cryptos, src); + break; + case CA_PRANSWER: + // If we're doing DTLS-SRTP, we don't want to update the filter + // with an answer, because we already have SRTP parameters. + if (transport_channel_->IsDtlsActive()) { + LOG(LS_INFO) << + "Ignoring SDES answer parameters because we are using DTLS-SRTP"; + ret = true; + } else { + ret = srtp_filter_.SetProvisionalAnswer(cryptos, src); + } + break; + case CA_ANSWER: + // If we're doing DTLS-SRTP, we don't want to update the filter + // with an answer, because we already have SRTP parameters. + if (transport_channel_->IsDtlsActive()) { + LOG(LS_INFO) << + "Ignoring SDES answer parameters because we are using DTLS-SRTP"; + ret = true; + } else { + ret = srtp_filter_.SetAnswer(cryptos, src); + } + break; + case CA_UPDATE: + // no crypto params. + ret = true; + break; + default: + break; + } + return ret; +} + +bool BaseChannel::SetRtcpMux_w(bool enable, ContentAction action, + ContentSource src) { + bool ret = false; + switch (action) { + case CA_OFFER: + ret = rtcp_mux_filter_.SetOffer(enable, src); + break; + case CA_PRANSWER: + ret = rtcp_mux_filter_.SetProvisionalAnswer(enable, src); + break; + case CA_ANSWER: + ret = rtcp_mux_filter_.SetAnswer(enable, src); + if (ret && rtcp_mux_filter_.IsActive()) { + // We activated RTCP mux, close down the RTCP transport. + set_rtcp_transport_channel(NULL); + } + break; + case CA_UPDATE: + // No RTCP mux info. + ret = true; + default: + break; + } + // |rtcp_mux_filter_| can be active if |action| is CA_PRANSWER or + // CA_ANSWER, but we only want to tear down the RTCP transport channel if we + // received a final answer. + if (ret && rtcp_mux_filter_.IsActive()) { + // If the RTP transport is already writable, then so are we. + if (transport_channel_->writable()) { + ChannelWritable_w(); + } + } + + return ret; +} + +bool BaseChannel::AddRecvStream_w(const StreamParams& sp) { + ASSERT(worker_thread() == talk_base::Thread::Current()); + if (!media_channel()->AddRecvStream(sp)) + return false; + + return ssrc_filter_.AddStream(sp); +} + +bool BaseChannel::RemoveRecvStream_w(uint32 ssrc) { + ASSERT(worker_thread() == talk_base::Thread::Current()); + ssrc_filter_.RemoveStream(ssrc); + return media_channel()->RemoveRecvStream(ssrc); +} + +bool BaseChannel::UpdateLocalStreams_w(const std::vector& streams, + ContentAction action) { + if (!VERIFY(action == CA_OFFER || action == CA_ANSWER || + action == CA_PRANSWER || action == CA_UPDATE)) + return false; + + // If this is an update, streams only contain streams that have changed. + if (action == CA_UPDATE) { + for (StreamParamsVec::const_iterator it = streams.begin(); + it != streams.end(); ++it) { + StreamParams existing_stream; + bool stream_exist = GetStreamByIds(local_streams_, it->groupid, + it->id, &existing_stream); + if (!stream_exist && it->has_ssrcs()) { + if (media_channel()->AddSendStream(*it)) { + local_streams_.push_back(*it); + LOG(LS_INFO) << "Add send stream ssrc: " << it->first_ssrc(); + } else { + LOG(LS_INFO) << "Failed to add send stream ssrc: " + << it->first_ssrc(); + return false; + } + } else if (stream_exist && !it->has_ssrcs()) { + if (!media_channel()->RemoveSendStream(existing_stream.first_ssrc())) { + LOG(LS_ERROR) << "Failed to remove send stream with ssrc " + << it->first_ssrc() << "."; + return false; + } + RemoveStreamBySsrc(&local_streams_, existing_stream.first_ssrc()); + } else { + LOG(LS_WARNING) << "Ignore unsupported stream update"; + } + } + return true; + } + // Else streams are all the streams we want to send. + + // Check for streams that have been removed. + bool ret = true; + for (StreamParamsVec::const_iterator it = local_streams_.begin(); + it != local_streams_.end(); ++it) { + if (!GetStreamBySsrc(streams, it->first_ssrc(), NULL)) { + if (!media_channel()->RemoveSendStream(it->first_ssrc())) { + LOG(LS_ERROR) << "Failed to remove send stream with ssrc " + << it->first_ssrc() << "."; + ret = false; + } + } + } + // Check for new streams. + for (StreamParamsVec::const_iterator it = streams.begin(); + it != streams.end(); ++it) { + if (!GetStreamBySsrc(local_streams_, it->first_ssrc(), NULL)) { + if (media_channel()->AddSendStream(*it)) { + LOG(LS_INFO) << "Add send ssrc: " << it->ssrcs[0]; + } else { + LOG(LS_INFO) << "Failed to add send stream ssrc: " << it->first_ssrc(); + ret = false; + } + } + } + local_streams_ = streams; + return ret; +} + +bool BaseChannel::UpdateRemoteStreams_w( + const std::vector& streams, + ContentAction action) { + if (!VERIFY(action == CA_OFFER || action == CA_ANSWER || + action == CA_PRANSWER || action == CA_UPDATE)) + return false; + + // If this is an update, streams only contain streams that have changed. + if (action == CA_UPDATE) { + for (StreamParamsVec::const_iterator it = streams.begin(); + it != streams.end(); ++it) { + StreamParams existing_stream; + bool stream_exists = GetStreamByIds(remote_streams_, it->groupid, + it->id, &existing_stream); + if (!stream_exists && it->has_ssrcs()) { + if (AddRecvStream_w(*it)) { + remote_streams_.push_back(*it); + LOG(LS_INFO) << "Add remote stream ssrc: " << it->first_ssrc(); + } else { + LOG(LS_INFO) << "Failed to add remote stream ssrc: " + << it->first_ssrc(); + return false; + } + } else if (stream_exists && !it->has_ssrcs()) { + if (!RemoveRecvStream_w(existing_stream.first_ssrc())) { + LOG(LS_ERROR) << "Failed to remove remote stream with ssrc " + << it->first_ssrc() << "."; + return false; + } + RemoveStreamBySsrc(&remote_streams_, existing_stream.first_ssrc()); + } else { + LOG(LS_WARNING) << "Ignore unsupported stream update." + << " Stream exists? " << stream_exists + << " existing stream = " << existing_stream.ToString() + << " new stream = " << it->ToString(); + } + } + return true; + } + // Else streams are all the streams we want to receive. + + // Check for streams that have been removed. + bool ret = true; + for (StreamParamsVec::const_iterator it = remote_streams_.begin(); + it != remote_streams_.end(); ++it) { + if (!GetStreamBySsrc(streams, it->first_ssrc(), NULL)) { + if (!RemoveRecvStream_w(it->first_ssrc())) { + LOG(LS_ERROR) << "Failed to remove remote stream with ssrc " + << it->first_ssrc() << "."; + ret = false; + } + } + } + // Check for new streams. + for (StreamParamsVec::const_iterator it = streams.begin(); + it != streams.end(); ++it) { + if (!GetStreamBySsrc(remote_streams_, it->first_ssrc(), NULL)) { + if (AddRecvStream_w(*it)) { + LOG(LS_INFO) << "Add remote ssrc: " << it->ssrcs[0]; + } else { + LOG(LS_INFO) << "Failed to add remote stream ssrc: " + << it->first_ssrc(); + ret = false; + } + } + } + remote_streams_ = streams; + return ret; +} + +bool BaseChannel::SetBaseLocalContent_w(const MediaContentDescription* content, + ContentAction action) { + // Cache secure_required_ for belt and suspenders check on SendPacket + secure_required_ = content->crypto_required(); + bool ret = UpdateLocalStreams_w(content->streams(), action); + // Set local SRTP parameters (what we will encrypt with). + ret &= SetSrtp_w(content->cryptos(), action, CS_LOCAL); + // Set local RTCP mux parameters. + ret &= SetRtcpMux_w(content->rtcp_mux(), action, CS_LOCAL); + // Set local RTP header extensions. + if (content->rtp_header_extensions_set()) { + ret &= media_channel()->SetRecvRtpHeaderExtensions( + content->rtp_header_extensions()); + } + set_local_content_direction(content->direction()); + return ret; +} + +bool BaseChannel::SetBaseRemoteContent_w(const MediaContentDescription* content, + ContentAction action) { + bool ret = UpdateRemoteStreams_w(content->streams(), action); + // Set remote SRTP parameters (what the other side will encrypt with). + ret &= SetSrtp_w(content->cryptos(), action, CS_REMOTE); + // Set remote RTCP mux parameters. + ret &= SetRtcpMux_w(content->rtcp_mux(), action, CS_REMOTE); + // Set remote RTP header extensions. + if (content->rtp_header_extensions_set()) { + ret &= media_channel()->SetSendRtpHeaderExtensions( + content->rtp_header_extensions()); + } + if (content->bandwidth() != kAutoBandwidth) { + ret &= media_channel()->SetSendBandwidth(false, content->bandwidth()); + } + set_remote_content_direction(content->direction()); + return ret; +} + +void BaseChannel::OnMessage(talk_base::Message *pmsg) { + switch (pmsg->message_id) { + case MSG_ENABLE: + EnableMedia_w(); + break; + case MSG_DISABLE: + DisableMedia_w(); + break; + case MSG_MUTESTREAM: { + MuteStreamData* data = static_cast(pmsg->pdata); + data->result = MuteStream_w(data->ssrc, data->mute); + break; + } + case MSG_ISSTREAMMUTED: { + SsrcMessageData* data = static_cast(pmsg->pdata); + data->result = IsStreamMuted_w(data->ssrc); + break; + } + case MSG_SETLOCALCONTENT: { + SetContentData* data = static_cast(pmsg->pdata); + data->result = SetLocalContent_w(data->content, data->action); + break; + } + case MSG_SETREMOTECONTENT: { + SetContentData* data = static_cast(pmsg->pdata); + data->result = SetRemoteContent_w(data->content, data->action); + break; + } + case MSG_ADDRECVSTREAM: { + StreamMessageData* data = static_cast(pmsg->pdata); + data->result = AddRecvStream_w(data->sp); + break; + } + case MSG_REMOVERECVSTREAM: { + SsrcMessageData* data = static_cast(pmsg->pdata); + data->result = RemoveRecvStream_w(data->ssrc); + break; + } + case MSG_SETMAXSENDBANDWIDTH: { + SetBandwidthData* data = static_cast(pmsg->pdata); + data->result = SetMaxSendBandwidth_w(data->value); + break; + } + + case MSG_RTPPACKET: + case MSG_RTCPPACKET: { + PacketMessageData* data = static_cast(pmsg->pdata); + SendPacket(pmsg->message_id == MSG_RTCPPACKET, &data->packet); + delete data; // because it is Posted + break; + } + case MSG_FIRSTPACKETRECEIVED: { + SignalFirstPacketReceived(this); + break; + } + case MSG_SESSION_ERROR: { + SessionErrorMessageData* data = static_cast + (pmsg->pdata); + session_->SetError(data->error_); + break; + } + } +} + +void BaseChannel::Send(uint32 id, talk_base::MessageData *pdata) { + worker_thread_->Send(this, id, pdata); +} + +void BaseChannel::Post(uint32 id, talk_base::MessageData *pdata) { + worker_thread_->Post(this, id, pdata); +} + +void BaseChannel::PostDelayed(int cmsDelay, uint32 id, + talk_base::MessageData *pdata) { + worker_thread_->PostDelayed(cmsDelay, this, id, pdata); +} + +void BaseChannel::Clear(uint32 id, talk_base::MessageList* removed) { + worker_thread_->Clear(this, id, removed); +} + +void BaseChannel::FlushRtcpMessages() { + // Flush all remaining RTCP messages. This should only be called in + // destructor. + ASSERT(talk_base::Thread::Current() == worker_thread_); + talk_base::MessageList rtcp_messages; + Clear(MSG_RTCPPACKET, &rtcp_messages); + for (talk_base::MessageList::iterator it = rtcp_messages.begin(); + it != rtcp_messages.end(); ++it) { + Send(MSG_RTCPPACKET, it->pdata); + } +} + +VoiceChannel::VoiceChannel(talk_base::Thread* thread, + MediaEngineInterface* media_engine, + VoiceMediaChannel* media_channel, + BaseSession* session, + const std::string& content_name, + bool rtcp) + : BaseChannel(thread, media_engine, media_channel, session, content_name, + rtcp), + received_media_(false) { +} + +VoiceChannel::~VoiceChannel() { + StopAudioMonitor(); + StopMediaMonitor(); + // this can't be done in the base class, since it calls a virtual + DisableMedia_w(); +} + +bool VoiceChannel::Init() { + TransportChannel* rtcp_channel = rtcp() ? session()->CreateChannel( + content_name(), "rtcp", ICE_CANDIDATE_COMPONENT_RTCP) : NULL; + if (!BaseChannel::Init(session()->CreateChannel( + content_name(), "rtp", ICE_CANDIDATE_COMPONENT_RTP), + rtcp_channel)) { + return false; + } + media_channel()->SignalMediaError.connect( + this, &VoiceChannel::OnVoiceChannelError); + srtp_filter()->SignalSrtpError.connect( + this, &VoiceChannel::OnSrtpError); + return true; +} + +bool VoiceChannel::SetRenderer(uint32 ssrc, AudioRenderer* renderer) { + AudioRenderMessageData data(ssrc, renderer); + Send(MSG_SETRENDERER, &data); + return data.result; +} + +bool VoiceChannel::SetRingbackTone(const void* buf, int len) { + SetRingbackToneMessageData data(buf, len); + Send(MSG_SETRINGBACKTONE, &data); + return data.result; +} + +// TODO(juberti): Handle early media the right way. We should get an explicit +// ringing message telling us to start playing local ringback, which we cancel +// if any early media actually arrives. For now, we do the opposite, which is +// to wait 1 second for early media, and start playing local ringback if none +// arrives. +void VoiceChannel::SetEarlyMedia(bool enable) { + if (enable) { + // Start the early media timeout + PostDelayed(kEarlyMediaTimeout, MSG_EARLYMEDIATIMEOUT); + } else { + // Stop the timeout if currently going. + Clear(MSG_EARLYMEDIATIMEOUT); + } +} + +bool VoiceChannel::PlayRingbackTone(uint32 ssrc, bool play, bool loop) { + PlayRingbackToneMessageData data(ssrc, play, loop); + Send(MSG_PLAYRINGBACKTONE, &data); + return data.result; +} + +bool VoiceChannel::PressDTMF(int digit, bool playout) { + int flags = DF_SEND; + if (playout) { + flags |= DF_PLAY; + } + int duration_ms = 160; + return InsertDtmf(0, digit, duration_ms, flags); +} + +bool VoiceChannel::CanInsertDtmf() { + BoolMessageData data(false); + Send(MSG_CANINSERTDTMF, &data); + return data.data(); +} + +bool VoiceChannel::InsertDtmf(uint32 ssrc, int event_code, int duration, + int flags) { + DtmfMessageData data(ssrc, event_code, duration, flags); + Send(MSG_INSERTDTMF, &data); + return data.result; +} + +bool VoiceChannel::SetOutputScaling(uint32 ssrc, double left, double right) { + ScaleVolumeMessageData data(ssrc, left, right); + Send(MSG_SCALEVOLUME, &data); + return data.result; +} +bool VoiceChannel::GetStats(VoiceMediaInfo* stats) { + VoiceStatsMessageData data(stats); + Send(MSG_GETSTATS, &data); + return data.result; +} + +void VoiceChannel::StartMediaMonitor(int cms) { + media_monitor_.reset(new VoiceMediaMonitor(media_channel(), worker_thread(), + talk_base::Thread::Current())); + media_monitor_->SignalUpdate.connect( + this, &VoiceChannel::OnMediaMonitorUpdate); + media_monitor_->Start(cms); +} + +void VoiceChannel::StopMediaMonitor() { + if (media_monitor_) { + media_monitor_->Stop(); + media_monitor_->SignalUpdate.disconnect(this); + media_monitor_.reset(); + } +} + +void VoiceChannel::StartAudioMonitor(int cms) { + audio_monitor_.reset(new AudioMonitor(this, talk_base::Thread::Current())); + audio_monitor_ + ->SignalUpdate.connect(this, &VoiceChannel::OnAudioMonitorUpdate); + audio_monitor_->Start(cms); +} + +void VoiceChannel::StopAudioMonitor() { + if (audio_monitor_) { + audio_monitor_->Stop(); + audio_monitor_.reset(); + } +} + +bool VoiceChannel::IsAudioMonitorRunning() const { + return (audio_monitor_.get() != NULL); +} + +void VoiceChannel::StartTypingMonitor(const TypingMonitorOptions& settings) { + typing_monitor_.reset(new TypingMonitor(this, worker_thread(), settings)); + SignalAutoMuted.repeat(typing_monitor_->SignalMuted); +} + +void VoiceChannel::StopTypingMonitor() { + typing_monitor_.reset(); +} + +bool VoiceChannel::IsTypingMonitorRunning() const { + return typing_monitor_; +} + +bool VoiceChannel::MuteStream_w(uint32 ssrc, bool mute) { + bool ret = BaseChannel::MuteStream_w(ssrc, mute); + if (typing_monitor_ && mute) + typing_monitor_->OnChannelMuted(); + return ret; +} + +int VoiceChannel::GetInputLevel_w() { + return media_engine()->GetInputLevel(); +} + +int VoiceChannel::GetOutputLevel_w() { + return media_channel()->GetOutputLevel(); +} + +void VoiceChannel::GetActiveStreams_w(AudioInfo::StreamList* actives) { + media_channel()->GetActiveStreams(actives); +} + +void VoiceChannel::OnChannelRead(TransportChannel* channel, + const char* data, size_t len, int flags) { + BaseChannel::OnChannelRead(channel, data, len, flags); + + // Set a flag when we've received an RTP packet. If we're waiting for early + // media, this will disable the timeout. + if (!received_media_ && !PacketIsRtcp(channel, data, len)) { + received_media_ = true; + } +} + +void VoiceChannel::ChangeState() { + // Render incoming data if we're the active call, and we have the local + // content. We receive data on the default channel and multiplexed streams. + bool recv = IsReadyToReceive(); + if (!media_channel()->SetPlayout(recv)) { + SendLastMediaError(); + } + + // Send outgoing data if we're the active call, we have the remote content, + // and we have had some form of connectivity. + bool send = IsReadyToSend(); + SendFlags send_flag = send ? SEND_MICROPHONE : SEND_NOTHING; + if (!media_channel()->SetSend(send_flag)) { + LOG(LS_ERROR) << "Failed to SetSend " << send_flag << " on voice channel"; + SendLastMediaError(); + } + + LOG(LS_INFO) << "Changing voice state, recv=" << recv << " send=" << send; +} + +const ContentInfo* VoiceChannel::GetFirstContent( + const SessionDescription* sdesc) { + return GetFirstAudioContent(sdesc); +} + +bool VoiceChannel::SetLocalContent_w(const MediaContentDescription* content, + ContentAction action) { + ASSERT(worker_thread() == talk_base::Thread::Current()); + LOG(LS_INFO) << "Setting local voice description"; + + const AudioContentDescription* audio = + static_cast(content); + ASSERT(audio != NULL); + if (!audio) return false; + + bool ret = SetBaseLocalContent_w(content, action); + // Set local audio codecs (what we want to receive). + // TODO(whyuan): Change action != CA_UPDATE to !audio->partial() when partial + // is set properly. + if (action != CA_UPDATE || audio->has_codecs()) { + ret &= media_channel()->SetRecvCodecs(audio->codecs()); + } + + // If everything worked, see if we can start receiving. + if (ret) { + ChangeState(); + } else { + LOG(LS_WARNING) << "Failed to set local voice description"; + } + return ret; +} + +bool VoiceChannel::SetRemoteContent_w(const MediaContentDescription* content, + ContentAction action) { + ASSERT(worker_thread() == talk_base::Thread::Current()); + LOG(LS_INFO) << "Setting remote voice description"; + + const AudioContentDescription* audio = + static_cast(content); + ASSERT(audio != NULL); + if (!audio) return false; + + bool ret = true; + // Set remote video codecs (what the other side wants to receive). + if (action != CA_UPDATE || audio->has_codecs()) { + ret &= media_channel()->SetSendCodecs(audio->codecs()); + } + + ret &= SetBaseRemoteContent_w(content, action); + + if (action != CA_UPDATE) { + // Tweak our audio processing settings, if needed. + AudioOptions audio_options; + if (!media_channel()->GetOptions(&audio_options)) { + LOG(LS_WARNING) << "Can not set audio options from on remote content."; + } else { + if (audio->conference_mode()) { + audio_options.conference_mode.Set(true); + } + if (audio->agc_minus_10db()) { + audio_options.adjust_agc_delta.Set(kAgcMinus10db); + } + if (!media_channel()->SetOptions(audio_options)) { + // Log an error on failure, but don't abort the call. + LOG(LS_ERROR) << "Failed to set voice channel options"; + } + } + } + + // If everything worked, see if we can start sending. + if (ret) { + ChangeState(); + } else { + LOG(LS_WARNING) << "Failed to set remote voice description"; + } + return ret; +} + +bool VoiceChannel::SetRingbackTone_w(const void* buf, int len) { + ASSERT(worker_thread() == talk_base::Thread::Current()); + return media_channel()->SetRingbackTone(static_cast(buf), len); +} + +bool VoiceChannel::PlayRingbackTone_w(uint32 ssrc, bool play, bool loop) { + ASSERT(worker_thread() == talk_base::Thread::Current()); + if (play) { + LOG(LS_INFO) << "Playing ringback tone, loop=" << loop; + } else { + LOG(LS_INFO) << "Stopping ringback tone"; + } + return media_channel()->PlayRingbackTone(ssrc, play, loop); +} + +void VoiceChannel::HandleEarlyMediaTimeout() { + // This occurs on the main thread, not the worker thread. + if (!received_media_) { + LOG(LS_INFO) << "No early media received before timeout"; + SignalEarlyMediaTimeout(this); + } +} + +bool VoiceChannel::CanInsertDtmf_w() { + return media_channel()->CanInsertDtmf(); +} + +bool VoiceChannel::InsertDtmf_w(uint32 ssrc, int event, int duration, + int flags) { + if (!enabled()) { + return false; + } + + return media_channel()->InsertDtmf(ssrc, event, duration, flags); +} + +bool VoiceChannel::SetOutputScaling_w(uint32 ssrc, double left, double right) { + return media_channel()->SetOutputScaling(ssrc, left, right); +} + +bool VoiceChannel::GetStats_w(VoiceMediaInfo* stats) { + return media_channel()->GetStats(stats); +} + +bool VoiceChannel::SetChannelOptions(const AudioOptions& options) { + AudioOptionsMessageData data(options); + Send(MSG_SETCHANNELOPTIONS, &data); + return data.result; +} + +bool VoiceChannel::SetChannelOptions_w(const AudioOptions& options) { + return media_channel()->SetOptions(options); +} + +bool VoiceChannel::SetRenderer_w(uint32 ssrc, AudioRenderer* renderer) { + return media_channel()->SetRenderer(ssrc, renderer); +} + +void VoiceChannel::OnMessage(talk_base::Message *pmsg) { + switch (pmsg->message_id) { + case MSG_SETRINGBACKTONE: { + SetRingbackToneMessageData* data = + static_cast(pmsg->pdata); + data->result = SetRingbackTone_w(data->buf, data->len); + break; + } + case MSG_PLAYRINGBACKTONE: { + PlayRingbackToneMessageData* data = + static_cast(pmsg->pdata); + data->result = PlayRingbackTone_w(data->ssrc, data->play, data->loop); + break; + } + case MSG_EARLYMEDIATIMEOUT: + HandleEarlyMediaTimeout(); + break; + case MSG_CANINSERTDTMF: { + BoolMessageData* data = + static_cast(pmsg->pdata); + data->data() = CanInsertDtmf_w(); + break; + } + case MSG_INSERTDTMF: { + DtmfMessageData* data = + static_cast(pmsg->pdata); + data->result = InsertDtmf_w(data->ssrc, data->event, data->duration, + data->flags); + break; + } + case MSG_SCALEVOLUME: { + ScaleVolumeMessageData* data = + static_cast(pmsg->pdata); + data->result = SetOutputScaling_w(data->ssrc, data->left, data->right); + break; + } + case MSG_GETSTATS: { + VoiceStatsMessageData* data = + static_cast(pmsg->pdata); + data->result = GetStats_w(data->stats); + break; + } + case MSG_CHANNEL_ERROR: { + VoiceChannelErrorMessageData* data = + static_cast(pmsg->pdata); + SignalMediaError(this, data->ssrc, data->error); + delete data; + break; + } + case MSG_SETCHANNELOPTIONS: { + AudioOptionsMessageData* data = + static_cast(pmsg->pdata); + data->result = SetChannelOptions_w(data->options); + break; + } + case MSG_SETRENDERER: { + AudioRenderMessageData* data = + static_cast(pmsg->pdata); + data->result = SetRenderer_w(data->ssrc, data->renderer); + break; + } + default: + BaseChannel::OnMessage(pmsg); + break; + } +} + +void VoiceChannel::OnConnectionMonitorUpdate( + SocketMonitor* monitor, const std::vector& infos) { + SignalConnectionMonitor(this, infos); +} + +void VoiceChannel::OnMediaMonitorUpdate( + VoiceMediaChannel* media_channel, const VoiceMediaInfo& info) { + ASSERT(media_channel == this->media_channel()); + SignalMediaMonitor(this, info); +} + +void VoiceChannel::OnAudioMonitorUpdate(AudioMonitor* monitor, + const AudioInfo& info) { + SignalAudioMonitor(this, info); +} + +void VoiceChannel::OnVoiceChannelError( + uint32 ssrc, VoiceMediaChannel::Error err) { + VoiceChannelErrorMessageData* data = new VoiceChannelErrorMessageData( + ssrc, err); + signaling_thread()->Post(this, MSG_CHANNEL_ERROR, data); +} + +void VoiceChannel::OnSrtpError(uint32 ssrc, SrtpFilter::Mode mode, + SrtpFilter::Error error) { + switch (error) { + case SrtpFilter::ERROR_FAIL: + OnVoiceChannelError(ssrc, (mode == SrtpFilter::PROTECT) ? + VoiceMediaChannel::ERROR_REC_SRTP_ERROR : + VoiceMediaChannel::ERROR_PLAY_SRTP_ERROR); + break; + case SrtpFilter::ERROR_AUTH: + OnVoiceChannelError(ssrc, (mode == SrtpFilter::PROTECT) ? + VoiceMediaChannel::ERROR_REC_SRTP_AUTH_FAILED : + VoiceMediaChannel::ERROR_PLAY_SRTP_AUTH_FAILED); + break; + case SrtpFilter::ERROR_REPLAY: + // Only receving channel should have this error. + ASSERT(mode == SrtpFilter::UNPROTECT); + OnVoiceChannelError(ssrc, VoiceMediaChannel::ERROR_PLAY_SRTP_REPLAY); + break; + default: + break; + } +} + +void VoiceChannel::GetSrtpCiphers(std::vector* ciphers) const { + GetSupportedAudioCryptoSuites(ciphers); +} + +VideoChannel::VideoChannel(talk_base::Thread* thread, + MediaEngineInterface* media_engine, + VideoMediaChannel* media_channel, + BaseSession* session, + const std::string& content_name, + bool rtcp, + VoiceChannel* voice_channel) + : BaseChannel(thread, media_engine, media_channel, session, content_name, + rtcp), + voice_channel_(voice_channel), + renderer_(NULL), + screencapture_factory_(CreateScreenCapturerFactory()), + previous_we_(talk_base::WE_CLOSE) { +} + +bool VideoChannel::Init() { + TransportChannel* rtcp_channel = rtcp() ? session()->CreateChannel( + content_name(), "video_rtcp", ICE_CANDIDATE_COMPONENT_RTCP) : NULL; + if (!BaseChannel::Init(session()->CreateChannel( + content_name(), "video_rtp", ICE_CANDIDATE_COMPONENT_RTP), + rtcp_channel)) { + return false; + } + media_channel()->SignalMediaError.connect( + this, &VideoChannel::OnVideoChannelError); + srtp_filter()->SignalSrtpError.connect( + this, &VideoChannel::OnSrtpError); + return true; +} + +void VoiceChannel::SendLastMediaError() { + uint32 ssrc; + VoiceMediaChannel::Error error; + media_channel()->GetLastMediaError(&ssrc, &error); + SignalMediaError(this, ssrc, error); +} + +VideoChannel::~VideoChannel() { + std::vector screencast_ssrcs; + ScreencastMap::iterator iter; + while (!screencast_capturers_.empty()) { + if (!RemoveScreencast(screencast_capturers_.begin()->first)) { + LOG(LS_ERROR) << "Unable to delete screencast with ssrc " + << screencast_capturers_.begin()->first; + ASSERT(false); + break; + } + } + + StopMediaMonitor(); + // this can't be done in the base class, since it calls a virtual + DisableMedia_w(); +} + +bool VideoChannel::SetRenderer(uint32 ssrc, VideoRenderer* renderer) { + VideoRenderMessageData data(ssrc, renderer); + Send(MSG_SETRENDERER, &data); + return true; +} + +bool VideoChannel::ApplyViewRequest(const ViewRequest& request) { + ViewRequestMessageData data(request); + Send(MSG_HANDLEVIEWREQUEST, &data); + return data.result; +} + +VideoCapturer* VideoChannel::AddScreencast( + uint32 ssrc, const ScreencastId& id) { + AddScreencastMessageData data(ssrc, id); + Send(MSG_ADDSCREENCAST, &data); + return data.result; +} + +bool VideoChannel::SetCapturer(uint32 ssrc, VideoCapturer* capturer) { + SetCapturerMessageData data(ssrc, capturer); + Send(MSG_SETCAPTURER, &data); + return data.result; +} + +bool VideoChannel::RemoveScreencast(uint32 ssrc) { + RemoveScreencastMessageData data(ssrc); + Send(MSG_REMOVESCREENCAST, &data); + return data.result; +} + +bool VideoChannel::IsScreencasting() { + IsScreencastingMessageData data; + Send(MSG_ISSCREENCASTING, &data); + return data.result; +} + +int VideoChannel::ScreencastFps(uint32 ssrc) { + ScreencastFpsMessageData data(ssrc); + Send(MSG_SCREENCASTFPS, &data); + return data.result; +} + +bool VideoChannel::SendIntraFrame() { + Send(MSG_SENDINTRAFRAME); + return true; +} + +bool VideoChannel::RequestIntraFrame() { + Send(MSG_REQUESTINTRAFRAME); + return true; +} + +void VideoChannel::SetScreenCaptureFactory( + ScreenCapturerFactory* screencapture_factory) { + SetScreenCaptureFactoryMessageData data(screencapture_factory); + Send(MSG_SETSCREENCASTFACTORY, &data); +} + +void VideoChannel::ChangeState() { + // Render incoming data if we're the active call, and we have the local + // content. We receive data on the default channel and multiplexed streams. + bool recv = IsReadyToReceive(); + if (!media_channel()->SetRender(recv)) { + LOG(LS_ERROR) << "Failed to SetRender on video channel"; + // TODO(gangji): Report error back to server. + } + + // Send outgoing data if we're the active call, we have the remote content, + // and we have had some form of connectivity. + bool send = IsReadyToSend(); + if (!media_channel()->SetSend(send)) { + LOG(LS_ERROR) << "Failed to SetSend on video channel"; + // TODO(gangji): Report error back to server. + } + + LOG(LS_INFO) << "Changing video state, recv=" << recv << " send=" << send; +} + +bool VideoChannel::GetStats(VideoMediaInfo* stats) { + VideoStatsMessageData data(stats); + Send(MSG_GETSTATS, &data); + return data.result; +} + +void VideoChannel::StartMediaMonitor(int cms) { + media_monitor_.reset(new VideoMediaMonitor(media_channel(), worker_thread(), + talk_base::Thread::Current())); + media_monitor_->SignalUpdate.connect( + this, &VideoChannel::OnMediaMonitorUpdate); + media_monitor_->Start(cms); +} + +void VideoChannel::StopMediaMonitor() { + if (media_monitor_) { + media_monitor_->Stop(); + media_monitor_.reset(); + } +} + +const ContentInfo* VideoChannel::GetFirstContent( + const SessionDescription* sdesc) { + return GetFirstVideoContent(sdesc); +} + +bool VideoChannel::SetLocalContent_w(const MediaContentDescription* content, + ContentAction action) { + ASSERT(worker_thread() == talk_base::Thread::Current()); + LOG(LS_INFO) << "Setting local video description"; + + const VideoContentDescription* video = + static_cast(content); + ASSERT(video != NULL); + if (!video) return false; + + bool ret = SetBaseLocalContent_w(content, action); + // Set local video codecs (what we want to receive). + if (action != CA_UPDATE || video->has_codecs()) { + ret &= media_channel()->SetRecvCodecs(video->codecs()); + } + + if (action != CA_UPDATE) { + VideoOptions video_options; + media_channel()->GetOptions(&video_options); + video_options.buffered_mode_latency.Set(video->buffered_mode_latency()); + + if (!media_channel()->SetOptions(video_options)) { + // Log an error on failure, but don't abort the call. + LOG(LS_ERROR) << "Failed to set video channel options"; + } + } + + // If everything worked, see if we can start receiving. + if (ret) { + ChangeState(); + } else { + LOG(LS_WARNING) << "Failed to set local video description"; + } + return ret; +} + +bool VideoChannel::SetRemoteContent_w(const MediaContentDescription* content, + ContentAction action) { + ASSERT(worker_thread() == talk_base::Thread::Current()); + LOG(LS_INFO) << "Setting remote video description"; + + const VideoContentDescription* video = + static_cast(content); + ASSERT(video != NULL); + if (!video) return false; + + bool ret = true; + // Set remote video codecs (what the other side wants to receive). + if (action != CA_UPDATE || video->has_codecs()) { + ret &= media_channel()->SetSendCodecs(video->codecs()); + } + + ret &= SetBaseRemoteContent_w(content, action); + + if (action != CA_UPDATE) { + // Tweak our video processing settings, if needed. + VideoOptions video_options; + media_channel()->GetOptions(&video_options); + video_options.conference_mode.Set(video->conference_mode()); + video_options.buffered_mode_latency.Set(video->buffered_mode_latency()); + + if (!media_channel()->SetOptions(video_options)) { + // Log an error on failure, but don't abort the call. + LOG(LS_ERROR) << "Failed to set video channel options"; + } + } + + // If everything worked, see if we can start sending. + if (ret) { + ChangeState(); + } else { + LOG(LS_WARNING) << "Failed to set remote video description"; + } + return ret; +} + +bool VideoChannel::ApplyViewRequest_w(const ViewRequest& request) { + bool ret = true; + // Set the send format for each of the local streams. If the view request + // does not contain a local stream, set its send format to 0x0, which will + // drop all frames. + for (std::vector::const_iterator it = local_streams().begin(); + it != local_streams().end(); ++it) { + VideoFormat format(0, 0, 0, cricket::FOURCC_I420); + StaticVideoViews::const_iterator view; + for (view = request.static_video_views.begin(); + view != request.static_video_views.end(); ++view) { + if (view->selector.Matches(*it)) { + format.width = view->width; + format.height = view->height; + format.interval = cricket::VideoFormat::FpsToInterval(view->framerate); + break; + } + } + + ret &= media_channel()->SetSendStreamFormat(it->first_ssrc(), format); + } + + // Check if the view request has invalid streams. + for (StaticVideoViews::const_iterator it = request.static_video_views.begin(); + it != request.static_video_views.end(); ++it) { + if (!GetStream(local_streams(), it->selector, NULL)) { + LOG(LS_WARNING) << "View request for (" + << it->selector.ssrc << ", '" + << it->selector.groupid << "', '" + << it->selector.streamid << "'" + << ") is not in the local streams."; + } + } + + return ret; +} + +void VideoChannel::SetRenderer_w(uint32 ssrc, VideoRenderer* renderer) { + media_channel()->SetRenderer(ssrc, renderer); +} + +VideoCapturer* VideoChannel::AddScreencast_w( + uint32 ssrc, const ScreencastId& id) { + if (screencast_capturers_.find(ssrc) != screencast_capturers_.end()) { + return NULL; + } + VideoCapturer* screen_capturer = + screencapture_factory_->CreateScreenCapturer(id); + if (!screen_capturer) { + return NULL; + } + screen_capturer->SignalStateChange.connect(this, + &VideoChannel::OnStateChange); + screencast_capturers_[ssrc] = screen_capturer; + return screen_capturer; +} + +bool VideoChannel::SetCapturer_w(uint32 ssrc, VideoCapturer* capturer) { + return media_channel()->SetCapturer(ssrc, capturer); +} + +bool VideoChannel::RemoveScreencast_w(uint32 ssrc) { + ScreencastMap::iterator iter = screencast_capturers_.find(ssrc); + if (iter == screencast_capturers_.end()) { + return false; + } + // Clean up VideoCapturer. + delete iter->second; + screencast_capturers_.erase(iter); + return true; +} + +bool VideoChannel::IsScreencasting_w() const { + return !screencast_capturers_.empty(); +} + +int VideoChannel::ScreencastFps_w(uint32 ssrc) const { + ScreencastMap::const_iterator iter = screencast_capturers_.find(ssrc); + if (iter == screencast_capturers_.end()) { + return 0; + } + VideoCapturer* capturer = iter->second; + const VideoFormat* video_format = capturer->GetCaptureFormat(); + return VideoFormat::IntervalToFps(video_format->interval); +} + +void VideoChannel::SetScreenCaptureFactory_w( + ScreenCapturerFactory* screencapture_factory) { + if (screencapture_factory == NULL) { + screencapture_factory_.reset(CreateScreenCapturerFactory()); + } else { + screencapture_factory_.reset(screencapture_factory); + } +} + +bool VideoChannel::GetStats_w(VideoMediaInfo* stats) { + return media_channel()->GetStats(stats); +} + +void VideoChannel::OnScreencastWindowEvent_s(uint32 ssrc, + talk_base::WindowEvent we) { + ASSERT(signaling_thread() == talk_base::Thread::Current()); + SignalScreencastWindowEvent(ssrc, we); +} + +bool VideoChannel::SetChannelOptions(const VideoOptions &options) { + VideoOptionsMessageData data(options); + Send(MSG_SETCHANNELOPTIONS, &data); + return data.result; +} + +bool VideoChannel::SetChannelOptions_w(const VideoOptions &options) { + return media_channel()->SetOptions(options); +} + +void VideoChannel::OnMessage(talk_base::Message *pmsg) { + switch (pmsg->message_id) { + case MSG_SETRENDERER: { + const VideoRenderMessageData* data = + static_cast(pmsg->pdata); + SetRenderer_w(data->ssrc, data->renderer); + break; + } + case MSG_ADDSCREENCAST: { + AddScreencastMessageData* data = + static_cast(pmsg->pdata); + data->result = AddScreencast_w(data->ssrc, data->window_id); + break; + } + case MSG_SETCAPTURER: { + SetCapturerMessageData* data = + static_cast(pmsg->pdata); + data->result = SetCapturer_w(data->ssrc, data->capturer); + break; + } + case MSG_REMOVESCREENCAST: { + RemoveScreencastMessageData* data = + static_cast(pmsg->pdata); + data->result = RemoveScreencast_w(data->ssrc); + break; + } + case MSG_SCREENCASTWINDOWEVENT: { + const ScreencastEventMessageData* data = + static_cast(pmsg->pdata); + OnScreencastWindowEvent_s(data->ssrc, data->event); + delete data; + break; + } + case MSG_ISSCREENCASTING: { + IsScreencastingMessageData* data = + static_cast(pmsg->pdata); + data->result = IsScreencasting_w(); + break; + } + case MSG_SCREENCASTFPS: { + ScreencastFpsMessageData* data = + static_cast(pmsg->pdata); + data->result = ScreencastFps_w(data->ssrc); + break; + } + case MSG_SENDINTRAFRAME: { + SendIntraFrame_w(); + break; + } + case MSG_REQUESTINTRAFRAME: { + RequestIntraFrame_w(); + break; + } + case MSG_SETCHANNELOPTIONS: { + VideoOptionsMessageData* data = + static_cast(pmsg->pdata); + data->result = SetChannelOptions_w(data->options); + break; + } + case MSG_CHANNEL_ERROR: { + const VideoChannelErrorMessageData* data = + static_cast(pmsg->pdata); + SignalMediaError(this, data->ssrc, data->error); + delete data; + break; + } + case MSG_HANDLEVIEWREQUEST: { + ViewRequestMessageData* data = + static_cast(pmsg->pdata); + data->result = ApplyViewRequest_w(data->request); + break; + } + case MSG_SETSCREENCASTFACTORY: { + SetScreenCaptureFactoryMessageData* data = + static_cast(pmsg->pdata); + SetScreenCaptureFactory_w(data->screencapture_factory); + } + case MSG_GETSTATS: { + VideoStatsMessageData* data = + static_cast(pmsg->pdata); + data->result = GetStats_w(data->stats); + break; + } + default: + BaseChannel::OnMessage(pmsg); + break; + } +} + +void VideoChannel::OnConnectionMonitorUpdate( + SocketMonitor *monitor, const std::vector &infos) { + SignalConnectionMonitor(this, infos); +} + +// TODO(pthatcher): Look into removing duplicate code between +// audio, video, and data, perhaps by using templates. +void VideoChannel::OnMediaMonitorUpdate( + VideoMediaChannel* media_channel, const VideoMediaInfo &info) { + ASSERT(media_channel == this->media_channel()); + SignalMediaMonitor(this, info); +} + +void VideoChannel::OnScreencastWindowEvent(uint32 ssrc, + talk_base::WindowEvent event) { + ScreencastEventMessageData* pdata = + new ScreencastEventMessageData(ssrc, event); + signaling_thread()->Post(this, MSG_SCREENCASTWINDOWEVENT, pdata); +} + +void VideoChannel::OnStateChange(VideoCapturer* capturer, CaptureState ev) { + // Map capturer events to window events. In the future we may want to simply + // pass these events up directly. + talk_base::WindowEvent we; + if (ev == CS_STOPPED) { + we = talk_base::WE_CLOSE; + } else if (ev == CS_PAUSED) { + we = talk_base::WE_MINIMIZE; + } else if (ev == CS_RUNNING && previous_we_ == talk_base::WE_MINIMIZE) { + we = talk_base::WE_RESTORE; + } else { + return; + } + previous_we_ = we; + + uint32 ssrc = 0; + if (!GetLocalSsrc(capturer, &ssrc)) { + return; + } + ScreencastEventMessageData* pdata = + new ScreencastEventMessageData(ssrc, we); + signaling_thread()->Post(this, MSG_SCREENCASTWINDOWEVENT, pdata); +} + +bool VideoChannel::GetLocalSsrc(const VideoCapturer* capturer, uint32* ssrc) { + *ssrc = 0; + for (ScreencastMap::iterator iter = screencast_capturers_.begin(); + iter != screencast_capturers_.end(); ++iter) { + if (iter->second == capturer) { + *ssrc = iter->first; + return true; + } + } + return false; +} + +void VideoChannel::OnVideoChannelError(uint32 ssrc, + VideoMediaChannel::Error error) { + VideoChannelErrorMessageData* data = new VideoChannelErrorMessageData( + ssrc, error); + signaling_thread()->Post(this, MSG_CHANNEL_ERROR, data); +} + +void VideoChannel::OnSrtpError(uint32 ssrc, SrtpFilter::Mode mode, + SrtpFilter::Error error) { + switch (error) { + case SrtpFilter::ERROR_FAIL: + OnVideoChannelError(ssrc, (mode == SrtpFilter::PROTECT) ? + VideoMediaChannel::ERROR_REC_SRTP_ERROR : + VideoMediaChannel::ERROR_PLAY_SRTP_ERROR); + break; + case SrtpFilter::ERROR_AUTH: + OnVideoChannelError(ssrc, (mode == SrtpFilter::PROTECT) ? + VideoMediaChannel::ERROR_REC_SRTP_AUTH_FAILED : + VideoMediaChannel::ERROR_PLAY_SRTP_AUTH_FAILED); + break; + case SrtpFilter::ERROR_REPLAY: + // Only receving channel should have this error. + ASSERT(mode == SrtpFilter::UNPROTECT); + // TODO(gangji): Turn on the signaling of replay error once we have + // switched to the new mechanism for doing video retransmissions. + // OnVideoChannelError(ssrc, VideoMediaChannel::ERROR_PLAY_SRTP_REPLAY); + break; + default: + break; + } +} + + +void VideoChannel::GetSrtpCiphers(std::vector* ciphers) const { + GetSupportedVideoCryptoSuites(ciphers); +} + +DataChannel::DataChannel(talk_base::Thread* thread, + DataMediaChannel* media_channel, + BaseSession* session, + const std::string& content_name, + bool rtcp) + // MediaEngine is NULL + : BaseChannel(thread, NULL, media_channel, session, content_name, rtcp), + data_channel_type_(cricket::DCT_NONE) { +} + +DataChannel::~DataChannel() { + StopMediaMonitor(); + // this can't be done in the base class, since it calls a virtual + DisableMedia_w(); +} + +bool DataChannel::Init() { + TransportChannel* rtcp_channel = rtcp() ? session()->CreateChannel( + content_name(), "data_rtcp", ICE_CANDIDATE_COMPONENT_RTCP) : NULL; + if (!BaseChannel::Init(session()->CreateChannel( + content_name(), "data_rtp", ICE_CANDIDATE_COMPONENT_RTP), + rtcp_channel)) { + return false; + } + media_channel()->SignalDataReceived.connect( + this, &DataChannel::OnDataReceived); + media_channel()->SignalMediaError.connect( + this, &DataChannel::OnDataChannelError); + srtp_filter()->SignalSrtpError.connect( + this, &DataChannel::OnSrtpError); + return true; +} + +bool DataChannel::SendData(const SendDataParams& params, + const talk_base::Buffer& payload, + SendDataResult* result) { + SendDataMessageData message_data(params, &payload, result); + Send(MSG_SENDDATA, &message_data); + return message_data.succeeded; +} + +const ContentInfo* DataChannel::GetFirstContent( + const SessionDescription* sdesc) { + return GetFirstDataContent(sdesc); +} + + +static bool IsRtpPacket(const talk_base::Buffer* packet) { + int version; + if (!GetRtpVersion(packet->data(), packet->length(), &version)) { + return false; + } + + return version == 2; +} + +bool DataChannel::WantsPacket(bool rtcp, talk_base::Buffer* packet) { + if (data_channel_type_ == DCT_SCTP) { + // TODO(pthatcher): Do this in a more robust way by checking for + // SCTP or DTLS. + return !IsRtpPacket(packet); + } else if (data_channel_type_ == DCT_RTP) { + return BaseChannel::WantsPacket(rtcp, packet); + } + return false; +} + +// Sets the maximum bandwidth. Anything over this will be dropped. +bool DataChannel::SetMaxSendBandwidth_w(int max_bps) { + LOG(LS_INFO) << "DataChannel: Setting max bandwidth to " << max_bps; + return media_channel()->SetSendBandwidth(false, max_bps); +} + +bool DataChannel::SetDataChannelType(DataChannelType new_data_channel_type) { + // It hasn't been set before, so set it now. + if (data_channel_type_ == DCT_NONE) { + data_channel_type_ = new_data_channel_type; + return true; + } + + // It's been set before, but doesn't match. That's bad. + if (data_channel_type_ != new_data_channel_type) { + LOG(LS_WARNING) << "Data channel type mismatch." + << " Expected " << data_channel_type_ + << " Got " << new_data_channel_type; + return false; + } + + // It's hasn't changed. Nothing to do. + return true; +} + +bool DataChannel::SetDataChannelTypeFromContent( + const DataContentDescription* content) { + bool is_sctp = ((content->protocol() == kMediaProtocolSctp) || + (content->protocol() == kMediaProtocolDtlsSctp)); + DataChannelType data_channel_type = is_sctp ? DCT_SCTP : DCT_RTP; + return SetDataChannelType(data_channel_type); +} + +bool DataChannel::SetLocalContent_w(const MediaContentDescription* content, + ContentAction action) { + ASSERT(worker_thread() == talk_base::Thread::Current()); + LOG(LS_INFO) << "Setting local data description"; + + const DataContentDescription* data = + static_cast(content); + ASSERT(data != NULL); + if (!data) return false; + + bool ret = false; + if (!SetDataChannelTypeFromContent(data)) { + return false; + } + + if (data_channel_type_ == DCT_SCTP) { + // SCTP data channels don't need the rest of the stuff. + ret = UpdateLocalStreams_w(data->streams(), action); + if (ret) { + set_local_content_direction(content->direction()); + } + } else { + ret = SetBaseLocalContent_w(content, action); + + if (action != CA_UPDATE || data->has_codecs()) { + ret &= media_channel()->SetRecvCodecs(data->codecs()); + } + } + + // If everything worked, see if we can start receiving. + if (ret) { + ChangeState(); + } else { + LOG(LS_WARNING) << "Failed to set local data description"; + } + return ret; +} + +bool DataChannel::SetRemoteContent_w(const MediaContentDescription* content, + ContentAction action) { + ASSERT(worker_thread() == talk_base::Thread::Current()); + + const DataContentDescription* data = + static_cast(content); + ASSERT(data != NULL); + if (!data) return false; + + bool ret = true; + if (!SetDataChannelTypeFromContent(data)) { + return false; + } + + if (data_channel_type_ == DCT_SCTP) { + LOG(LS_INFO) << "Setting SCTP remote data description"; + // SCTP data channels don't need the rest of the stuff. + ret = UpdateRemoteStreams_w(content->streams(), action); + if (ret) { + set_remote_content_direction(content->direction()); + } + } else { + // If the remote data doesn't have codecs and isn't an update, it + // must be empty, so ignore it. + if (action != CA_UPDATE && !data->has_codecs()) { + return true; + } + LOG(LS_INFO) << "Setting remote data description"; + + // Set remote video codecs (what the other side wants to receive). + if (action != CA_UPDATE || data->has_codecs()) { + ret &= media_channel()->SetSendCodecs(data->codecs()); + } + + if (ret) { + ret &= SetBaseRemoteContent_w(content, action); + } + + if (action != CA_UPDATE) { + int bandwidth_bps = data->bandwidth(); + bool auto_bandwidth = (bandwidth_bps == kAutoBandwidth); + ret &= media_channel()->SetSendBandwidth(auto_bandwidth, bandwidth_bps); + } + } + + // If everything worked, see if we can start sending. + if (ret) { + ChangeState(); + } else { + LOG(LS_WARNING) << "Failed to set remote data description"; + } + return ret; +} + +void DataChannel::ChangeState() { + // Render incoming data if we're the active call, and we have the local + // content. We receive data on the default channel and multiplexed streams. + bool recv = IsReadyToReceive(); + if (!media_channel()->SetReceive(recv)) { + LOG(LS_ERROR) << "Failed to SetReceive on data channel"; + } + + // Send outgoing data if we're the active call, we have the remote content, + // and we have had some form of connectivity. + bool send = IsReadyToSend(); + if (!media_channel()->SetSend(send)) { + LOG(LS_ERROR) << "Failed to SetSend on data channel"; + } + + // Post to trigger SignalReadyToSendData. + signaling_thread()->Post(this, MSG_READYTOSENDDATA, + new BoolMessageData(send)); + + LOG(LS_INFO) << "Changing data state, recv=" << recv << " send=" << send; +} + +void DataChannel::OnMessage(talk_base::Message *pmsg) { + switch (pmsg->message_id) { + case MSG_READYTOSENDDATA: { + BoolMessageData* data = static_cast(pmsg->pdata); + SignalReadyToSendData(data->data()); + delete data; + break; + } + case MSG_SENDDATA: { + SendDataMessageData* msg = + static_cast(pmsg->pdata); + msg->succeeded = media_channel()->SendData( + msg->params, *(msg->payload), msg->result); + break; + } + case MSG_DATARECEIVED: { + DataReceivedMessageData* data = + static_cast(pmsg->pdata); + SignalDataReceived(this, data->params, data->payload); + delete data; + break; + } + case MSG_CHANNEL_ERROR: { + const DataChannelErrorMessageData* data = + static_cast(pmsg->pdata); + SignalMediaError(this, data->ssrc, data->error); + delete data; + break; + } + default: + BaseChannel::OnMessage(pmsg); + break; + } +} + +void DataChannel::OnConnectionMonitorUpdate( + SocketMonitor* monitor, const std::vector& infos) { + SignalConnectionMonitor(this, infos); +} + +void DataChannel::StartMediaMonitor(int cms) { + media_monitor_.reset(new DataMediaMonitor(media_channel(), worker_thread(), + talk_base::Thread::Current())); + media_monitor_->SignalUpdate.connect( + this, &DataChannel::OnMediaMonitorUpdate); + media_monitor_->Start(cms); +} + +void DataChannel::StopMediaMonitor() { + if (media_monitor_) { + media_monitor_->Stop(); + media_monitor_->SignalUpdate.disconnect(this); + media_monitor_.reset(); + } +} + +void DataChannel::OnMediaMonitorUpdate( + DataMediaChannel* media_channel, const DataMediaInfo& info) { + ASSERT(media_channel == this->media_channel()); + SignalMediaMonitor(this, info); +} + +void DataChannel::OnDataReceived( + const ReceiveDataParams& params, const char* data, size_t len) { + DataReceivedMessageData* msg = new DataReceivedMessageData( + params, data, len); + signaling_thread()->Post(this, MSG_DATARECEIVED, msg); +} + +void DataChannel::OnDataChannelError( + uint32 ssrc, DataMediaChannel::Error err) { + DataChannelErrorMessageData* data = new DataChannelErrorMessageData( + ssrc, err); + signaling_thread()->Post(this, MSG_CHANNEL_ERROR, data); +} + +void DataChannel::OnSrtpError(uint32 ssrc, SrtpFilter::Mode mode, + SrtpFilter::Error error) { + switch (error) { + case SrtpFilter::ERROR_FAIL: + OnDataChannelError(ssrc, (mode == SrtpFilter::PROTECT) ? + DataMediaChannel::ERROR_SEND_SRTP_ERROR : + DataMediaChannel::ERROR_RECV_SRTP_ERROR); + break; + case SrtpFilter::ERROR_AUTH: + OnDataChannelError(ssrc, (mode == SrtpFilter::PROTECT) ? + DataMediaChannel::ERROR_SEND_SRTP_AUTH_FAILED : + DataMediaChannel::ERROR_RECV_SRTP_AUTH_FAILED); + break; + case SrtpFilter::ERROR_REPLAY: + // Only receving channel should have this error. + ASSERT(mode == SrtpFilter::UNPROTECT); + OnDataChannelError(ssrc, DataMediaChannel::ERROR_RECV_SRTP_REPLAY); + break; + default: + break; + } +} + +void DataChannel::GetSrtpCiphers(std::vector* ciphers) const { + GetSupportedDataCryptoSuites(ciphers); +} + +bool DataChannel::ShouldSetupDtlsSrtp() const { + return (data_channel_type_ == DCT_RTP); +} + +} // namespace cricket diff --git a/talk/session/media/channel.h b/talk/session/media/channel.h new file mode 100644 index 000000000..ddf7c67ab --- /dev/null +++ b/talk/session/media/channel.h @@ -0,0 +1,688 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_SESSION_MEDIA_CHANNEL_H_ +#define TALK_SESSION_MEDIA_CHANNEL_H_ + +#include +#include + +#include "talk/base/asyncudpsocket.h" +#include "talk/base/criticalsection.h" +#include "talk/base/network.h" +#include "talk/base/sigslot.h" +#include "talk/base/window.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/mediaengine.h" +#include "talk/media/base/screencastid.h" +#include "talk/media/base/streamparams.h" +#include "talk/media/base/videocapturer.h" +#include "talk/p2p/base/session.h" +#include "talk/p2p/client/socketmonitor.h" +#include "talk/session/media/audiomonitor.h" +#include "talk/session/media/mediamonitor.h" +#include "talk/session/media/mediasession.h" +#include "talk/session/media/rtcpmuxfilter.h" +#include "talk/session/media/srtpfilter.h" +#include "talk/session/media/ssrcmuxfilter.h" + +namespace cricket { + +struct CryptoParams; +class MediaContentDescription; +struct TypingMonitorOptions; +class TypingMonitor; +struct ViewRequest; + +enum SinkType { + SINK_PRE_CRYPTO, // Sink packets before encryption or after decryption. + SINK_POST_CRYPTO // Sink packets after encryption or before decryption. +}; + +// BaseChannel contains logic common to voice and video, including +// enable/mute, marshaling calls to a worker thread, and +// connection and media monitors. +class BaseChannel + : public talk_base::MessageHandler, public sigslot::has_slots<>, + public MediaChannel::NetworkInterface { + public: + BaseChannel(talk_base::Thread* thread, MediaEngineInterface* media_engine, + MediaChannel* channel, BaseSession* session, + const std::string& content_name, bool rtcp); + virtual ~BaseChannel(); + bool Init(TransportChannel* transport_channel, + TransportChannel* rtcp_transport_channel); + + talk_base::Thread* worker_thread() const { return worker_thread_; } + BaseSession* session() const { return session_; } + const std::string& content_name() { return content_name_; } + TransportChannel* transport_channel() const { + return transport_channel_; + } + TransportChannel* rtcp_transport_channel() const { + return rtcp_transport_channel_; + } + bool enabled() const { return enabled_; } + // Set to true to have the channel optimistically allow data to be sent even + // when the channel isn't fully writable. + void set_optimistic_data_send(bool value) { optimistic_data_send_ = value; } + bool optimistic_data_send() const { return optimistic_data_send_; } + + // This function returns true if we are using SRTP. + bool secure() const { return srtp_filter_.IsActive(); } + // The following function returns true if we are using + // DTLS-based keying. If you turned off SRTP later, however + // you could have secure() == false and dtls_secure() == true. + bool secure_dtls() const { return dtls_keyed_; } + // This function returns true if we require secure channel for call setup. + bool secure_required() const { return secure_required_; } + + bool writable() const { return writable_; } + bool IsStreamMuted(uint32 ssrc); + + // Channel control + bool SetLocalContent(const MediaContentDescription* content, + ContentAction action); + bool SetRemoteContent(const MediaContentDescription* content, + ContentAction action); + bool SetMaxSendBandwidth(int max_bandwidth); + + bool Enable(bool enable); + // Mute sending media on the stream with SSRC |ssrc| + // If there is only one sending stream SSRC 0 can be used. + bool MuteStream(uint32 ssrc, bool mute); + + // Multiplexing + bool AddRecvStream(const StreamParams& sp); + bool RemoveRecvStream(uint32 ssrc); + + // Monitoring + void StartConnectionMonitor(int cms); + void StopConnectionMonitor(); + + void set_srtp_signal_silent_time(uint32 silent_time) { + srtp_filter_.set_signal_silent_time(silent_time); + } + + void set_content_name(const std::string& content_name) { + ASSERT(signaling_thread()->IsCurrent()); + ASSERT(!writable_); + if (session_->state() != BaseSession::STATE_INIT) { + LOG(LS_ERROR) << "Content name for a channel can be changed only " + << "when BaseSession is in STATE_INIT state."; + return; + } + content_name_ = content_name; + } + + template + void RegisterSendSink(T* sink, + void (T::*OnPacket)(const void*, size_t, bool), + SinkType type) { + talk_base::CritScope cs(&signal_send_packet_cs_); + if (SINK_POST_CRYPTO == type) { + SignalSendPacketPostCrypto.disconnect(sink); + SignalSendPacketPostCrypto.connect(sink, OnPacket); + } else { + SignalSendPacketPreCrypto.disconnect(sink); + SignalSendPacketPreCrypto.connect(sink, OnPacket); + } + } + + void UnregisterSendSink(sigslot::has_slots<>* sink, + SinkType type) { + talk_base::CritScope cs(&signal_send_packet_cs_); + if (SINK_POST_CRYPTO == type) { + SignalSendPacketPostCrypto.disconnect(sink); + } else { + SignalSendPacketPreCrypto.disconnect(sink); + } + } + + bool HasSendSinks(SinkType type) { + talk_base::CritScope cs(&signal_send_packet_cs_); + if (SINK_POST_CRYPTO == type) { + return !SignalSendPacketPostCrypto.is_empty(); + } else { + return !SignalSendPacketPreCrypto.is_empty(); + } + } + + template + void RegisterRecvSink(T* sink, + void (T::*OnPacket)(const void*, size_t, bool), + SinkType type) { + talk_base::CritScope cs(&signal_recv_packet_cs_); + if (SINK_POST_CRYPTO == type) { + SignalRecvPacketPostCrypto.disconnect(sink); + SignalRecvPacketPostCrypto.connect(sink, OnPacket); + } else { + SignalRecvPacketPreCrypto.disconnect(sink); + SignalRecvPacketPreCrypto.connect(sink, OnPacket); + } + } + + void UnregisterRecvSink(sigslot::has_slots<>* sink, + SinkType type) { + talk_base::CritScope cs(&signal_recv_packet_cs_); + if (SINK_POST_CRYPTO == type) { + SignalRecvPacketPostCrypto.disconnect(sink); + } else { + SignalRecvPacketPreCrypto.disconnect(sink); + } + } + + bool HasRecvSinks(SinkType type) { + talk_base::CritScope cs(&signal_recv_packet_cs_); + if (SINK_POST_CRYPTO == type) { + return !SignalRecvPacketPostCrypto.is_empty(); + } else { + return !SignalRecvPacketPreCrypto.is_empty(); + } + } + + SsrcMuxFilter* ssrc_filter() { return &ssrc_filter_; } + + const std::vector& local_streams() const { + return local_streams_; + } + const std::vector& remote_streams() const { + return remote_streams_; + } + + // Used for latency measurements. + sigslot::signal1 SignalFirstPacketReceived; + + // Used to alert UI when the muted status changes, perhaps autonomously. + sigslot::repeater2 SignalAutoMuted; + + // Made public for easier testing. + void SetReadyToSend(TransportChannel* channel, bool ready); + + protected: + MediaEngineInterface* media_engine() const { return media_engine_; } + virtual MediaChannel* media_channel() const { return media_channel_; } + void set_rtcp_transport_channel(TransportChannel* transport); + bool was_ever_writable() const { return was_ever_writable_; } + void set_local_content_direction(MediaContentDirection direction) { + local_content_direction_ = direction; + } + void set_remote_content_direction(MediaContentDirection direction) { + remote_content_direction_ = direction; + } + bool IsReadyToReceive() const; + bool IsReadyToSend() const; + talk_base::Thread* signaling_thread() { return session_->signaling_thread(); } + SrtpFilter* srtp_filter() { return &srtp_filter_; } + bool rtcp() const { return rtcp_; } + + void Send(uint32 id, talk_base::MessageData* pdata = NULL); + void Post(uint32 id, talk_base::MessageData* pdata = NULL); + void PostDelayed(int cmsDelay, uint32 id = 0, + talk_base::MessageData* pdata = NULL); + void Clear(uint32 id = talk_base::MQID_ANY, + talk_base::MessageList* removed = NULL); + void FlushRtcpMessages(); + + // NetworkInterface implementation, called by MediaEngine + virtual bool SendPacket(talk_base::Buffer* packet); + virtual bool SendRtcp(talk_base::Buffer* packet); + virtual int SetOption(SocketType type, talk_base::Socket::Option o, int val); + + // From TransportChannel + void OnWritableState(TransportChannel* channel); + virtual void OnChannelRead(TransportChannel* channel, const char* data, + size_t len, int flags); + void OnReadyToSend(TransportChannel* channel); + + bool PacketIsRtcp(const TransportChannel* channel, const char* data, + size_t len); + bool SendPacket(bool rtcp, talk_base::Buffer* packet); + virtual bool WantsPacket(bool rtcp, talk_base::Buffer* packet); + void HandlePacket(bool rtcp, talk_base::Buffer* packet); + + // Apply the new local/remote session description. + void OnNewLocalDescription(BaseSession* session, ContentAction action); + void OnNewRemoteDescription(BaseSession* session, ContentAction action); + + void EnableMedia_w(); + void DisableMedia_w(); + virtual bool MuteStream_w(uint32 ssrc, bool mute); + bool IsStreamMuted_w(uint32 ssrc); + void ChannelWritable_w(); + void ChannelNotWritable_w(); + bool AddRecvStream_w(const StreamParams& sp); + bool RemoveRecvStream_w(uint32 ssrc); + virtual bool ShouldSetupDtlsSrtp() const; + // Do the DTLS key expansion and impose it on the SRTP/SRTCP filters. + // |rtcp_channel| indicates whether to set up the RTP or RTCP filter. + bool SetupDtlsSrtp(bool rtcp_channel); + // Set the DTLS-SRTP cipher policy on this channel as appropriate. + bool SetDtlsSrtpCiphers(TransportChannel *tc, bool rtcp); + + virtual void ChangeState() = 0; + + // Gets the content info appropriate to the channel (audio or video). + virtual const ContentInfo* GetFirstContent( + const SessionDescription* sdesc) = 0; + bool UpdateLocalStreams_w(const std::vector& streams, + ContentAction action); + bool UpdateRemoteStreams_w(const std::vector& streams, + ContentAction action); + bool SetBaseLocalContent_w(const MediaContentDescription* content, + ContentAction action); + virtual bool SetLocalContent_w(const MediaContentDescription* content, + ContentAction action) = 0; + bool SetBaseRemoteContent_w(const MediaContentDescription* content, + ContentAction action); + virtual bool SetRemoteContent_w(const MediaContentDescription* content, + ContentAction action) = 0; + + bool SetSrtp_w(const std::vector& params, ContentAction action, + ContentSource src); + bool SetRtcpMux_w(bool enable, ContentAction action, ContentSource src); + + virtual bool SetMaxSendBandwidth_w(int max_bandwidth); + + // From MessageHandler + virtual void OnMessage(talk_base::Message* pmsg); + + // Handled in derived classes + // Get the SRTP ciphers to use for RTP media + virtual void GetSrtpCiphers(std::vector* ciphers) const = 0; + virtual void OnConnectionMonitorUpdate(SocketMonitor* monitor, + const std::vector& infos) = 0; + + private: + sigslot::signal3 SignalSendPacketPreCrypto; + sigslot::signal3 SignalSendPacketPostCrypto; + sigslot::signal3 SignalRecvPacketPreCrypto; + sigslot::signal3 SignalRecvPacketPostCrypto; + talk_base::CriticalSection signal_send_packet_cs_; + talk_base::CriticalSection signal_recv_packet_cs_; + + talk_base::Thread* worker_thread_; + MediaEngineInterface* media_engine_; + BaseSession* session_; + MediaChannel* media_channel_; + std::vector local_streams_; + std::vector remote_streams_; + + std::string content_name_; + bool rtcp_; + TransportChannel* transport_channel_; + TransportChannel* rtcp_transport_channel_; + SrtpFilter srtp_filter_; + RtcpMuxFilter rtcp_mux_filter_; + SsrcMuxFilter ssrc_filter_; + talk_base::scoped_ptr socket_monitor_; + bool enabled_; + bool writable_; + bool rtp_ready_to_send_; + bool rtcp_ready_to_send_; + bool optimistic_data_send_; + bool was_ever_writable_; + MediaContentDirection local_content_direction_; + MediaContentDirection remote_content_direction_; + std::set muted_streams_; + bool has_received_packet_; + bool dtls_keyed_; + bool secure_required_; +}; + +// VoiceChannel is a specialization that adds support for early media, DTMF, +// and input/output level monitoring. +class VoiceChannel : public BaseChannel { + public: + VoiceChannel(talk_base::Thread* thread, MediaEngineInterface* media_engine, + VoiceMediaChannel* channel, BaseSession* session, + const std::string& content_name, bool rtcp); + ~VoiceChannel(); + bool Init(); + bool SetRenderer(uint32 ssrc, AudioRenderer* renderer); + + // downcasts a MediaChannel + virtual VoiceMediaChannel* media_channel() const { + return static_cast(BaseChannel::media_channel()); + } + + bool SetRingbackTone(const void* buf, int len); + void SetEarlyMedia(bool enable); + // This signal is emitted when we have gone a period of time without + // receiving early media. When received, a UI should start playing its + // own ringing sound + sigslot::signal1 SignalEarlyMediaTimeout; + + bool PlayRingbackTone(uint32 ssrc, bool play, bool loop); + // TODO(ronghuawu): Replace PressDTMF with InsertDtmf. + bool PressDTMF(int digit, bool playout); + // Returns if the telephone-event has been negotiated. + bool CanInsertDtmf(); + // Send and/or play a DTMF |event| according to the |flags|. + // The DTMF out-of-band signal will be used on sending. + // The |ssrc| should be either 0 or a valid send stream ssrc. + // The valid value for the |event| are -2 to 15. + // kDtmfReset(-2) is used to reset the DTMF. + // kDtmfDelay(-1) is used to insert a delay to the end of the DTMF queue. + // 0 to 15 which corresponding to DTMF event 0-9, *, #, A-D. + bool InsertDtmf(uint32 ssrc, int event_code, int duration, int flags); + bool SetOutputScaling(uint32 ssrc, double left, double right); + // Get statistics about the current media session. + bool GetStats(VoiceMediaInfo* stats); + + // Monitoring functions + sigslot::signal2&> + SignalConnectionMonitor; + + void StartMediaMonitor(int cms); + void StopMediaMonitor(); + sigslot::signal2 SignalMediaMonitor; + + void StartAudioMonitor(int cms); + void StopAudioMonitor(); + bool IsAudioMonitorRunning() const; + sigslot::signal2 SignalAudioMonitor; + + void StartTypingMonitor(const TypingMonitorOptions& settings); + void StopTypingMonitor(); + bool IsTypingMonitorRunning() const; + + // Overrides BaseChannel::MuteStream_w. + virtual bool MuteStream_w(uint32 ssrc, bool mute); + + int GetInputLevel_w(); + int GetOutputLevel_w(); + void GetActiveStreams_w(AudioInfo::StreamList* actives); + + // Signal errors from VoiceMediaChannel. Arguments are: + // ssrc(uint32), and error(VoiceMediaChannel::Error). + sigslot::signal3 + SignalMediaError; + + // Configuration and setting. + bool SetChannelOptions(const AudioOptions& options); + + private: + // overrides from BaseChannel + virtual void OnChannelRead(TransportChannel* channel, + const char* data, size_t len, int flags); + virtual void ChangeState(); + virtual const ContentInfo* GetFirstContent(const SessionDescription* sdesc); + virtual bool SetLocalContent_w(const MediaContentDescription* content, + ContentAction action); + virtual bool SetRemoteContent_w(const MediaContentDescription* content, + ContentAction action); + bool SetRingbackTone_w(const void* buf, int len); + bool PlayRingbackTone_w(uint32 ssrc, bool play, bool loop); + void HandleEarlyMediaTimeout(); + bool CanInsertDtmf_w(); + bool InsertDtmf_w(uint32 ssrc, int event, int duration, int flags); + bool SetOutputScaling_w(uint32 ssrc, double left, double right); + bool GetStats_w(VoiceMediaInfo* stats); + + virtual void OnMessage(talk_base::Message* pmsg); + virtual void GetSrtpCiphers(std::vector* ciphers) const; + virtual void OnConnectionMonitorUpdate( + SocketMonitor* monitor, const std::vector& infos); + virtual void OnMediaMonitorUpdate( + VoiceMediaChannel* media_channel, const VoiceMediaInfo& info); + void OnAudioMonitorUpdate(AudioMonitor* monitor, const AudioInfo& info); + void OnVoiceChannelError(uint32 ssrc, VoiceMediaChannel::Error error); + void SendLastMediaError(); + void OnSrtpError(uint32 ssrc, SrtpFilter::Mode mode, SrtpFilter::Error error); + // Configuration and setting. + bool SetChannelOptions_w(const AudioOptions& options); + bool SetRenderer_w(uint32 ssrc, AudioRenderer* renderer); + + static const int kEarlyMediaTimeout = 1000; + bool received_media_; + talk_base::scoped_ptr media_monitor_; + talk_base::scoped_ptr audio_monitor_; + talk_base::scoped_ptr typing_monitor_; +}; + +// VideoChannel is a specialization for video. +class VideoChannel : public BaseChannel { + public: + // Make screen capturer virtual so that it can be overriden in testing. + // E.g. used to test that window events are triggered correctly. + class ScreenCapturerFactory { + public: + virtual VideoCapturer* CreateScreenCapturer(const ScreencastId& window) = 0; + virtual ~ScreenCapturerFactory() {} + }; + + VideoChannel(talk_base::Thread* thread, MediaEngineInterface* media_engine, + VideoMediaChannel* channel, BaseSession* session, + const std::string& content_name, bool rtcp, + VoiceChannel* voice_channel); + ~VideoChannel(); + bool Init(); + + bool SetRenderer(uint32 ssrc, VideoRenderer* renderer); + bool ApplyViewRequest(const ViewRequest& request); + + // TODO(pthatcher): Refactor to use a "capture id" instead of an + // ssrc here as the "key". + VideoCapturer* AddScreencast(uint32 ssrc, const ScreencastId& id); + VideoCapturer* GetScreencastCapturer(uint32 ssrc); + bool SetCapturer(uint32 ssrc, VideoCapturer* capturer); + bool RemoveScreencast(uint32 ssrc); + // True if we've added a screencast. Doesn't matter if the capturer + // has been started or not. + bool IsScreencasting(); + int ScreencastFps(uint32 ssrc); + // Get statistics about the current media session. + bool GetStats(VideoMediaInfo* stats); + + sigslot::signal2&> + SignalConnectionMonitor; + + void StartMediaMonitor(int cms); + void StopMediaMonitor(); + sigslot::signal2 SignalMediaMonitor; + sigslot::signal2 SignalScreencastWindowEvent; + + bool SendIntraFrame(); + bool RequestIntraFrame(); + sigslot::signal3 + SignalMediaError; + + void SetScreenCaptureFactory( + ScreenCapturerFactory* screencapture_factory); + + // Configuration and setting. + bool SetChannelOptions(const VideoOptions& options); + + protected: + // downcasts a MediaChannel + virtual VideoMediaChannel* media_channel() const { + return static_cast(BaseChannel::media_channel()); + } + + private: + typedef std::map ScreencastMap; + + // overrides from BaseChannel + virtual void ChangeState(); + virtual const ContentInfo* GetFirstContent(const SessionDescription* sdesc); + virtual bool SetLocalContent_w(const MediaContentDescription* content, + ContentAction action); + virtual bool SetRemoteContent_w(const MediaContentDescription* content, + ContentAction action); + void SendIntraFrame_w() { + media_channel()->SendIntraFrame(); + } + void RequestIntraFrame_w() { + media_channel()->RequestIntraFrame(); + } + + bool ApplyViewRequest_w(const ViewRequest& request); + void SetRenderer_w(uint32 ssrc, VideoRenderer* renderer); + + VideoCapturer* AddScreencast_w(uint32 ssrc, const ScreencastId& id); + VideoCapturer* GetScreencastCapturer_w(uint32 ssrc); + bool SetCapturer_w(uint32 ssrc, VideoCapturer* capturer); + bool RemoveScreencast_w(uint32 ssrc); + void OnScreencastWindowEvent_s(uint32 ssrc, talk_base::WindowEvent we); + bool IsScreencasting_w() const; + int ScreencastFps_w(uint32 ssrc) const; + void SetScreenCaptureFactory_w( + ScreenCapturerFactory* screencapture_factory); + bool GetStats_w(VideoMediaInfo* stats); + + virtual void OnMessage(talk_base::Message* pmsg); + virtual void GetSrtpCiphers(std::vector* ciphers) const; + virtual void OnConnectionMonitorUpdate( + SocketMonitor* monitor, const std::vector& infos); + virtual void OnMediaMonitorUpdate( + VideoMediaChannel* media_channel, const VideoMediaInfo& info); + virtual void OnScreencastWindowEvent(uint32 ssrc, + talk_base::WindowEvent event); + virtual void OnStateChange(VideoCapturer* capturer, CaptureState ev); + bool GetLocalSsrc(const VideoCapturer* capturer, uint32* ssrc); + + void OnVideoChannelError(uint32 ssrc, VideoMediaChannel::Error error); + void OnSrtpError(uint32 ssrc, SrtpFilter::Mode mode, SrtpFilter::Error error); + // Configuration and setting. + bool SetChannelOptions_w(const VideoOptions& options); + + VoiceChannel* voice_channel_; + VideoRenderer* renderer_; + talk_base::scoped_ptr screencapture_factory_; + ScreencastMap screencast_capturers_; + talk_base::scoped_ptr media_monitor_; + + talk_base::WindowEvent previous_we_; +}; + +// DataChannel is a specialization for data. +class DataChannel : public BaseChannel { + public: + DataChannel(talk_base::Thread* thread, + DataMediaChannel* media_channel, + BaseSession* session, + const std::string& content_name, + bool rtcp); + ~DataChannel(); + bool Init(); + + // downcasts a MediaChannel + virtual DataMediaChannel* media_channel() const { + return static_cast(BaseChannel::media_channel()); + } + + bool SendData(const SendDataParams& params, + const talk_base::Buffer& payload, + SendDataResult* result); + + void StartMediaMonitor(int cms); + void StopMediaMonitor(); + + sigslot::signal2 SignalMediaMonitor; + sigslot::signal2&> + SignalConnectionMonitor; + sigslot::signal3 + SignalMediaError; + sigslot::signal3 + SignalDataReceived; + // Signal for notifying when the channel becomes ready to send data. + // That occurs when the channel is enabled, the transport is writable and + // both local and remote descriptions are set. + // TODO(perkj): Signal this per SSRC stream. + sigslot::signal1 SignalReadyToSendData; + + private: + struct SendDataMessageData : public talk_base::MessageData { + SendDataMessageData(const SendDataParams& params, + const talk_base::Buffer* payload, + SendDataResult* result) + : params(params), + payload(payload), + result(result), + succeeded(false) { + } + + const SendDataParams& params; + const talk_base::Buffer* payload; + SendDataResult* result; + bool succeeded; + }; + + struct DataReceivedMessageData : public talk_base::MessageData { + // We copy the data because the data will become invalid after we + // handle DataMediaChannel::SignalDataReceived but before we fire + // SignalDataReceived. + DataReceivedMessageData( + const ReceiveDataParams& params, const char* data, size_t len) + : params(params), + payload(data, len) { + } + const ReceiveDataParams params; + const talk_base::Buffer payload; + }; + + // overrides from BaseChannel + virtual const ContentInfo* GetFirstContent(const SessionDescription* sdesc); + // If data_channel_type_ is DCT_NONE, set it. Otherwise, check that + // it's the same as what was set previously. Returns false if it's + // set to one type one type and changed to another type later. + bool SetDataChannelType(DataChannelType new_data_channel_type); + // Same as SetDataChannelType, but extracts the type from the + // DataContentDescription. + bool SetDataChannelTypeFromContent(const DataContentDescription* content); + virtual bool SetMaxSendBandwidth_w(int max_bandwidth); + virtual bool SetLocalContent_w(const MediaContentDescription* content, + ContentAction action); + virtual bool SetRemoteContent_w(const MediaContentDescription* content, + ContentAction action); + virtual void ChangeState(); + virtual bool WantsPacket(bool rtcp, talk_base::Buffer* packet); + + virtual void OnMessage(talk_base::Message* pmsg); + virtual void GetSrtpCiphers(std::vector* ciphers) const; + virtual void OnConnectionMonitorUpdate( + SocketMonitor* monitor, const std::vector& infos); + virtual void OnMediaMonitorUpdate( + DataMediaChannel* media_channel, const DataMediaInfo& info); + virtual bool ShouldSetupDtlsSrtp() const; + void OnDataReceived( + const ReceiveDataParams& params, const char* data, size_t len); + void OnDataChannelError(uint32 ssrc, DataMediaChannel::Error error); + void OnSrtpError(uint32 ssrc, SrtpFilter::Mode mode, SrtpFilter::Error error); + + talk_base::scoped_ptr media_monitor_; + // TODO(pthatcher): Make a separate SctpDataChannel and + // RtpDataChannel instead of using this. + DataChannelType data_channel_type_; +}; + +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_CHANNEL_H_ diff --git a/talk/session/media/channel_unittest.cc b/talk/session/media/channel_unittest.cc new file mode 100644 index 000000000..8c0250537 --- /dev/null +++ b/talk/session/media/channel_unittest.cc @@ -0,0 +1,2895 @@ +// libjingle +// Copyright 2009 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/base/fileutils.h" +#include "talk/base/gunit.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/pathutils.h" +#include "talk/base/signalthread.h" +#include "talk/base/ssladapter.h" +#include "talk/base/sslidentity.h" +#include "talk/base/window.h" +#include "talk/media/base/fakemediaengine.h" +#include "talk/media/base/fakertp.h" +#include "talk/media/base/fakevideocapturer.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/rtpdump.h" +#include "talk/media/base/screencastid.h" +#include "talk/media/base/testutils.h" +#include "talk/p2p/base/fakesession.h" +#include "talk/session/media/channel.h" +#include "talk/session/media/mediamessages.h" +#include "talk/session/media/mediarecorder.h" +#include "talk/session/media/mediasessionclient.h" +#include "talk/session/media/typingmonitor.h" + +#define MAYBE_SKIP_TEST(feature) \ + if (!(talk_base::SSLStreamAdapter::feature())) { \ + LOG(LS_INFO) << "Feature disabled... skipping"; \ + return; \ + } + +using cricket::CA_OFFER; +using cricket::CA_PRANSWER; +using cricket::CA_ANSWER; +using cricket::CA_UPDATE; +using cricket::FakeVoiceMediaChannel; +using cricket::kDtmfDelay; +using cricket::kDtmfReset; +using cricket::ScreencastId; +using cricket::StreamParams; +using cricket::TransportChannel; +using talk_base::WindowId; + +static const cricket::AudioCodec kPcmuCodec(0, "PCMU", 64000, 8000, 1, 0); +static const cricket::AudioCodec kPcmaCodec(8, "PCMA", 64000, 8000, 1, 0); +static const cricket::AudioCodec kIsacCodec(103, "ISAC", 40000, 16000, 1, 0); +static const cricket::VideoCodec kH264Codec(97, "H264", 640, 400, 30, 0); +static const cricket::VideoCodec kH264SvcCodec(99, "H264-SVC", 320, 200, 15, 0); +static const cricket::DataCodec kGoogleDataCodec(101, "google-data", 0); +static const uint32 kSsrc1 = 0x1111; +static const uint32 kSsrc2 = 0x2222; +static const uint32 kSsrc3 = 0x3333; +static const char kCName[] = "a@b.com"; + +template +class Traits { + public: + typedef ChannelT Channel; + typedef MediaChannelT MediaChannel; + typedef ContentT Content; + typedef CodecT Codec; + typedef MediaInfoT MediaInfo; +}; + +class FakeScreenCaptureFactory + : public cricket::VideoChannel::ScreenCapturerFactory, + public sigslot::has_slots<> { + public: + FakeScreenCaptureFactory() + : window_capturer_(NULL), + capture_state_(cricket::CS_STOPPED) {} + + virtual cricket::VideoCapturer* CreateScreenCapturer( + const ScreencastId& window) { + if (window_capturer_ != NULL) { + // Class is only designed to handle one fake screencapturer. + ADD_FAILURE(); + return NULL; + } + window_capturer_ = new cricket::FakeVideoCapturer; + window_capturer_->SignalDestroyed.connect( + this, + &FakeScreenCaptureFactory::OnWindowCapturerDestroyed); + window_capturer_->SignalStateChange.connect( + this, + &FakeScreenCaptureFactory::OnStateChange); + return window_capturer_; + } + + cricket::FakeVideoCapturer* window_capturer() { return window_capturer_; } + + cricket::CaptureState capture_state() { return capture_state_; } + + private: + void OnWindowCapturerDestroyed(cricket::FakeVideoCapturer* capturer) { + if (capturer == window_capturer_) { + window_capturer_ = NULL; + } + } + void OnStateChange(cricket::VideoCapturer*, cricket::CaptureState state) { + capture_state_ = state; + } + + cricket::FakeVideoCapturer* window_capturer_; + cricket::CaptureState capture_state_; +}; + +// Controls how long we wait for a session to send messages that we +// expect, in milliseconds. We put it high to avoid flaky tests. +static const int kEventTimeout = 5000; + +class VoiceTraits : public Traits { +}; + +class VideoTraits : public Traits { +}; + +class DataTraits : public Traits { +}; + + +talk_base::StreamInterface* Open(const std::string& path) { + return talk_base::Filesystem::OpenFile( + talk_base::Pathname(path), "wb"); +} + +// Base class for Voice/VideoChannel tests +template +class ChannelTest : public testing::Test, public sigslot::has_slots<> { + public: + enum Flags { RTCP = 0x1, RTCP_MUX = 0x2, SECURE = 0x4, SSRC_MUX = 0x8, + DTLS = 0x10 }; + + ChannelTest(const uint8* rtp_data, int rtp_len, + const uint8* rtcp_data, int rtcp_len) + : session1_(true), + session2_(false), + media_channel1_(NULL), + media_channel2_(NULL), + rtp_packet_(reinterpret_cast(rtp_data), rtp_len), + rtcp_packet_(reinterpret_cast(rtcp_data), rtcp_len), + media_info_callbacks1_(), + media_info_callbacks2_(), + mute_callback_recved_(false), + mute_callback_value_(false), + ssrc_(0), + error_(T::MediaChannel::ERROR_NONE) { + } + + static void SetUpTestCase() { + talk_base::InitializeSSL(); + } + + void CreateChannels(int flags1, int flags2) { + CreateChannels(new typename T::MediaChannel(NULL), + new typename T::MediaChannel(NULL), + flags1, flags2, talk_base::Thread::Current()); + } + void CreateChannels(int flags) { + CreateChannels(new typename T::MediaChannel(NULL), + new typename T::MediaChannel(NULL), + flags, talk_base::Thread::Current()); + } + void CreateChannels(int flags1, int flags2, + talk_base::Thread* thread) { + CreateChannels(new typename T::MediaChannel(NULL), + new typename T::MediaChannel(NULL), + flags1, flags2, thread); + } + void CreateChannels(int flags, + talk_base::Thread* thread) { + CreateChannels(new typename T::MediaChannel(NULL), + new typename T::MediaChannel(NULL), + flags, thread); + } + void CreateChannels( + typename T::MediaChannel* ch1, typename T::MediaChannel* ch2, + int flags1, int flags2, talk_base::Thread* thread) { + media_channel1_ = ch1; + media_channel2_ = ch2; + channel1_.reset(CreateChannel(thread, &media_engine_, ch1, &session1_, + (flags1 & RTCP) != 0)); + channel2_.reset(CreateChannel(thread, &media_engine_, ch2, &session2_, + (flags2 & RTCP) != 0)); + channel1_->SignalMediaMonitor.connect( + this, &ChannelTest::OnMediaMonitor); + channel2_->SignalMediaMonitor.connect( + this, &ChannelTest::OnMediaMonitor); + channel1_->SignalMediaError.connect( + this, &ChannelTest::OnMediaChannelError); + channel2_->SignalMediaError.connect( + this, &ChannelTest::OnMediaChannelError); + channel1_->SignalAutoMuted.connect( + this, &ChannelTest::OnMediaMuted); + CreateContent(flags1, kPcmuCodec, kH264Codec, + &local_media_content1_); + CreateContent(flags2, kPcmuCodec, kH264Codec, + &local_media_content2_); + CopyContent(local_media_content1_, &remote_media_content1_); + CopyContent(local_media_content2_, &remote_media_content2_); + + if (flags1 & DTLS) { + identity1_.reset(talk_base::SSLIdentity::Generate("session1")); + session1_.set_ssl_identity(identity1_.get()); + } + if (flags2 & DTLS) { + identity2_.reset(talk_base::SSLIdentity::Generate("session2")); + session2_.set_ssl_identity(identity2_.get()); + } + + // Add stream information (SSRC) to the local content but not to the remote + // content. This means that we per default know the SSRC of what we send but + // not what we receive. + AddLegacyStreamInContent(kSsrc1, flags1, &local_media_content1_); + AddLegacyStreamInContent(kSsrc2, flags2, &local_media_content2_); + + // If SSRC_MUX is used we also need to know the SSRC of the incoming stream. + if (flags1 & SSRC_MUX) { + AddLegacyStreamInContent(kSsrc1, flags1, &remote_media_content1_); + } + if (flags2 & SSRC_MUX) { + AddLegacyStreamInContent(kSsrc2, flags2, &remote_media_content2_); + } + } + + void CreateChannels( + typename T::MediaChannel* ch1, typename T::MediaChannel* ch2, + int flags, talk_base::Thread* thread) { + media_channel1_ = ch1; + media_channel2_ = ch2; + + channel1_.reset(CreateChannel(thread, &media_engine_, ch1, &session1_, + (flags & RTCP) != 0)); + channel2_.reset(CreateChannel(thread, &media_engine_, ch2, &session1_, + (flags & RTCP) != 0)); + channel1_->SignalMediaMonitor.connect( + this, &ChannelTest::OnMediaMonitor); + channel2_->SignalMediaMonitor.connect( + this, &ChannelTest::OnMediaMonitor); + channel2_->SignalMediaError.connect( + this, &ChannelTest::OnMediaChannelError); + CreateContent(flags, kPcmuCodec, kH264Codec, + &local_media_content1_); + CreateContent(flags, kPcmuCodec, kH264Codec, + &local_media_content2_); + CopyContent(local_media_content1_, &remote_media_content1_); + CopyContent(local_media_content2_, &remote_media_content2_); + // Add stream information (SSRC) to the local content but not to the remote + // content. This means that we per default know the SSRC of what we send but + // not what we receive. + AddLegacyStreamInContent(kSsrc1, flags, &local_media_content1_); + AddLegacyStreamInContent(kSsrc2, flags, &local_media_content2_); + + // If SSRC_MUX is used we also need to know the SSRC of the incoming stream. + if (flags & SSRC_MUX) { + AddLegacyStreamInContent(kSsrc1, flags, &remote_media_content1_); + AddLegacyStreamInContent(kSsrc2, flags, &remote_media_content2_); + } + } + + typename T::Channel* CreateChannel(talk_base::Thread* thread, + cricket::MediaEngineInterface* engine, + typename T::MediaChannel* ch, + cricket::BaseSession* session, + bool rtcp) { + typename T::Channel* channel = new typename T::Channel( + thread, engine, ch, session, cricket::CN_AUDIO, rtcp); + if (!channel->Init()) { + delete channel; + channel = NULL; + } + return channel; + } + + bool SendInitiate() { + bool result = channel1_->SetLocalContent(&local_media_content1_, CA_OFFER); + if (result) { + channel1_->Enable(true); + result = channel2_->SetRemoteContent(&remote_media_content1_, CA_OFFER); + if (result) { + session1_.Connect(&session2_); + + result = channel2_->SetLocalContent(&local_media_content2_, CA_ANSWER); + } + } + return result; + } + + bool SendAccept() { + channel2_->Enable(true); + return channel1_->SetRemoteContent(&remote_media_content2_, CA_ANSWER); + } + + bool SendOffer() { + bool result = channel1_->SetLocalContent(&local_media_content1_, CA_OFFER); + if (result) { + channel1_->Enable(true); + result = channel2_->SetRemoteContent(&remote_media_content1_, CA_OFFER); + } + return result; + } + + bool SendProvisionalAnswer() { + bool result = channel2_->SetLocalContent(&local_media_content2_, + CA_PRANSWER); + if (result) { + channel2_->Enable(true); + result = channel1_->SetRemoteContent(&remote_media_content2_, + CA_PRANSWER); + session1_.Connect(&session2_); + } + return result; + } + + bool SendFinalAnswer() { + bool result = channel2_->SetLocalContent(&local_media_content2_, CA_ANSWER); + if (result) + result = channel1_->SetRemoteContent(&remote_media_content2_, CA_ANSWER); + return result; + } + + bool SendTerminate() { + channel1_.reset(); + channel2_.reset(); + return true; + } + + bool AddStream1(int id) { + return channel1_->AddRecvStream(cricket::StreamParams::CreateLegacy(id)); + } + bool RemoveStream1(int id) { + return channel1_->RemoveRecvStream(id); + } + + cricket::FakeTransport* GetTransport1() { + return session1_.GetTransport(channel1_->content_name()); + } + cricket::FakeTransport* GetTransport2() { + return session2_.GetTransport(channel2_->content_name()); + } + + bool SendRtp1() { + return media_channel1_->SendRtp(rtp_packet_.c_str(), rtp_packet_.size()); + } + bool SendRtp2() { + return media_channel2_->SendRtp(rtp_packet_.c_str(), rtp_packet_.size()); + } + bool SendRtcp1() { + return media_channel1_->SendRtcp(rtcp_packet_.c_str(), rtcp_packet_.size()); + } + bool SendRtcp2() { + return media_channel2_->SendRtcp(rtcp_packet_.c_str(), rtcp_packet_.size()); + } + // Methods to send custom data. + bool SendCustomRtp1(uint32 ssrc, int sequence_number) { + std::string data(CreateRtpData(ssrc, sequence_number)); + return media_channel1_->SendRtp(data.c_str(), data.size()); + } + bool SendCustomRtp2(uint32 ssrc, int sequence_number) { + std::string data(CreateRtpData(ssrc, sequence_number)); + return media_channel2_->SendRtp(data.c_str(), data.size()); + } + bool SendCustomRtcp1(uint32 ssrc) { + std::string data(CreateRtcpData(ssrc)); + return media_channel1_->SendRtcp(data.c_str(), data.size()); + } + bool SendCustomRtcp2(uint32 ssrc) { + std::string data(CreateRtcpData(ssrc)); + return media_channel2_->SendRtcp(data.c_str(), data.size()); + } + bool CheckRtp1() { + return media_channel1_->CheckRtp(rtp_packet_.c_str(), rtp_packet_.size()); + } + bool CheckRtp2() { + return media_channel2_->CheckRtp(rtp_packet_.c_str(), rtp_packet_.size()); + } + bool CheckRtcp1() { + return media_channel1_->CheckRtcp(rtcp_packet_.c_str(), + rtcp_packet_.size()); + } + bool CheckRtcp2() { + return media_channel2_->CheckRtcp(rtcp_packet_.c_str(), + rtcp_packet_.size()); + } + // Methods to check custom data. + bool CheckCustomRtp1(uint32 ssrc, int sequence_number) { + std::string data(CreateRtpData(ssrc, sequence_number)); + return media_channel1_->CheckRtp(data.c_str(), data.size()); + } + bool CheckCustomRtp2(uint32 ssrc, int sequence_number) { + std::string data(CreateRtpData(ssrc, sequence_number)); + return media_channel2_->CheckRtp(data.c_str(), data.size()); + } + bool CheckCustomRtcp1(uint32 ssrc) { + std::string data(CreateRtcpData(ssrc)); + return media_channel1_->CheckRtcp(data.c_str(), data.size()); + } + bool CheckCustomRtcp2(uint32 ssrc) { + std::string data(CreateRtcpData(ssrc)); + return media_channel2_->CheckRtcp(data.c_str(), data.size()); + } + std::string CreateRtpData(uint32 ssrc, int sequence_number) { + std::string data(rtp_packet_); + // Set SSRC in the rtp packet copy. + talk_base::SetBE32(const_cast(data.c_str()) + 8, ssrc); + talk_base::SetBE16(const_cast(data.c_str()) + 2, sequence_number); + return data; + } + std::string CreateRtcpData(uint32 ssrc) { + std::string data(rtcp_packet_); + // Set SSRC in the rtcp packet copy. + talk_base::SetBE32(const_cast(data.c_str()) + 4, ssrc); + return data; + } + + bool CheckNoRtp1() { + return media_channel1_->CheckNoRtp(); + } + bool CheckNoRtp2() { + return media_channel2_->CheckNoRtp(); + } + bool CheckNoRtcp1() { + return media_channel1_->CheckNoRtcp(); + } + bool CheckNoRtcp2() { + return media_channel2_->CheckNoRtcp(); + } + + void CreateContent(int flags, + const cricket::AudioCodec& audio_codec, + const cricket::VideoCodec& video_codec, + typename T::Content* content) { + // overridden in specialized classes + } + void CopyContent(const typename T::Content& source, + typename T::Content* content) { + // overridden in specialized classes + } + + void SetOptimisticDataSend(bool optimistic_data_send) { + channel1_->set_optimistic_data_send(optimistic_data_send); + channel2_->set_optimistic_data_send(optimistic_data_send); + } + + // Creates a cricket::SessionDescription with one MediaContent and one stream. + // kPcmuCodec is used as audio codec and kH264Codec is used as video codec. + cricket::SessionDescription* CreateSessionDescriptionWithStream(uint32 ssrc) { + typename T::Content content; + cricket::SessionDescription* sdesc = new cricket::SessionDescription(); + CreateContent(SECURE, kPcmuCodec, kH264Codec, &content); + AddLegacyStreamInContent(ssrc, 0, &content); + sdesc->AddContent("DUMMY_CONTENT_NAME", + cricket::NS_JINGLE_RTP, content.Copy()); + return sdesc; + } + + class CallThread : public talk_base::SignalThread { + public: + typedef bool (ChannelTest::*Method)(); + CallThread(ChannelTest* obj, Method method, bool* result) + : obj_(obj), + method_(method), + result_(result) { + *result = false; + } + virtual void DoWork() { + bool result = (*obj_.*method_)(); + if (result_) { + *result_ = result; + } + } + private: + ChannelTest* obj_; + Method method_; + bool* result_; + }; + void CallOnThread(typename CallThread::Method method, bool* result) { + CallThread* thread = new CallThread(this, method, result); + thread->Start(); + thread->Release(); + } + + void CallOnThreadAndWaitForDone(typename CallThread::Method method, + bool* result) { + CallThread* thread = new CallThread(this, method, result); + thread->Start(); + thread->Destroy(true); + } + + bool CodecMatches(const typename T::Codec& c1, const typename T::Codec& c2) { + return false; // overridden in specialized classes + } + + void OnMediaMonitor(typename T::Channel* channel, + const typename T::MediaInfo& info) { + if (channel == channel1_.get()) { + media_info_callbacks1_++; + } else if (channel == channel2_.get()) { + media_info_callbacks2_++; + } + } + + void OnMediaChannelError(typename T::Channel* channel, + uint32 ssrc, + typename T::MediaChannel::Error error) { + ssrc_ = ssrc; + error_ = error; + } + + void OnMediaMuted(cricket::BaseChannel* channel, bool muted) { + mute_callback_recved_ = true; + mute_callback_value_ = muted; + } + + void AddLegacyStreamInContent(uint32 ssrc, int flags, + typename T::Content* content) { + // Base implementation. + } + + // Tests that can be used by derived classes. + + // Basic sanity check. + void TestInit() { + CreateChannels(0, 0); + EXPECT_FALSE(channel1_->secure()); + EXPECT_FALSE(media_channel1_->sending()); + EXPECT_FALSE(media_channel1_->playout()); + EXPECT_TRUE(media_channel1_->codecs().empty()); + EXPECT_TRUE(media_channel1_->recv_streams().empty()); + EXPECT_TRUE(media_channel1_->rtp_packets().empty()); + EXPECT_TRUE(media_channel1_->rtcp_packets().empty()); + } + + // Test that SetLocalContent and SetRemoteContent properly configure + // the codecs. + void TestSetContents() { + CreateChannels(0, 0); + typename T::Content content; + CreateContent(0, kPcmuCodec, kH264Codec, &content); + EXPECT_TRUE(channel1_->SetLocalContent(&content, CA_OFFER)); + EXPECT_EQ(0U, media_channel1_->codecs().size()); + EXPECT_TRUE(channel1_->SetRemoteContent(&content, CA_ANSWER)); + ASSERT_EQ(1U, media_channel1_->codecs().size()); + EXPECT_TRUE(CodecMatches(content.codecs()[0], + media_channel1_->codecs()[0])); + } + + // Test that SetLocalContent and SetRemoteContent properly deals + // with an empty offer. + void TestSetContentsNullOffer() { + CreateChannels(0, 0); + typename T::Content content; + EXPECT_TRUE(channel1_->SetLocalContent(&content, CA_OFFER)); + CreateContent(0, kPcmuCodec, kH264Codec, &content); + EXPECT_EQ(0U, media_channel1_->codecs().size()); + EXPECT_TRUE(channel1_->SetRemoteContent(&content, CA_ANSWER)); + ASSERT_EQ(1U, media_channel1_->codecs().size()); + EXPECT_TRUE(CodecMatches(content.codecs()[0], + media_channel1_->codecs()[0])); + } + + // Test that SetLocalContent and SetRemoteContent properly set RTCP + // mux. + void TestSetContentsRtcpMux() { + CreateChannels(RTCP, RTCP); + EXPECT_TRUE(channel1_->rtcp_transport_channel() != NULL); + EXPECT_TRUE(channel2_->rtcp_transport_channel() != NULL); + typename T::Content content; + CreateContent(0, kPcmuCodec, kH264Codec, &content); + // Both sides agree on mux. Should no longer be a separate RTCP channel. + content.set_rtcp_mux(true); + EXPECT_TRUE(channel1_->SetLocalContent(&content, CA_OFFER)); + EXPECT_TRUE(channel1_->SetRemoteContent(&content, CA_ANSWER)); + EXPECT_TRUE(channel1_->rtcp_transport_channel() == NULL); + // Only initiator supports mux. Should still have a separate RTCP channel. + EXPECT_TRUE(channel2_->SetLocalContent(&content, CA_OFFER)); + content.set_rtcp_mux(false); + EXPECT_TRUE(channel2_->SetRemoteContent(&content, CA_ANSWER)); + EXPECT_TRUE(channel2_->rtcp_transport_channel() != NULL); + } + + // Test that SetLocalContent and SetRemoteContent properly set RTCP + // mux when a provisional answer is received. + void TestSetContentsRtcpMuxWithPrAnswer() { + CreateChannels(RTCP, RTCP); + EXPECT_TRUE(channel1_->rtcp_transport_channel() != NULL); + EXPECT_TRUE(channel2_->rtcp_transport_channel() != NULL); + typename T::Content content; + CreateContent(0, kPcmuCodec, kH264Codec, &content); + content.set_rtcp_mux(true); + EXPECT_TRUE(channel1_->SetLocalContent(&content, CA_OFFER)); + EXPECT_TRUE(channel1_->SetRemoteContent(&content, CA_PRANSWER)); + EXPECT_TRUE(channel1_->rtcp_transport_channel() != NULL); + EXPECT_TRUE(channel1_->SetRemoteContent(&content, CA_ANSWER)); + // Both sides agree on mux. Should no longer be a separate RTCP channel. + EXPECT_TRUE(channel1_->rtcp_transport_channel() == NULL); + // Only initiator supports mux. Should still have a separate RTCP channel. + EXPECT_TRUE(channel2_->SetLocalContent(&content, CA_OFFER)); + content.set_rtcp_mux(false); + EXPECT_TRUE(channel2_->SetRemoteContent(&content, CA_PRANSWER)); + EXPECT_TRUE(channel2_->SetRemoteContent(&content, CA_ANSWER)); + EXPECT_TRUE(channel2_->rtcp_transport_channel() != NULL); + } + + // Test that SetLocalContent and SetRemoteContent properly set + // video options to the media channel. + void TestSetContentsVideoOptions() { + CreateChannels(0, 0); + typename T::Content content; + CreateContent(0, kPcmuCodec, kH264Codec, &content); + content.set_buffered_mode_latency(101); + EXPECT_TRUE(channel1_->SetLocalContent(&content, CA_OFFER)); + EXPECT_EQ(0U, media_channel1_->codecs().size()); + cricket::VideoOptions options; + ASSERT_TRUE(media_channel1_->GetOptions(&options)); + int latency = 0; + EXPECT_TRUE(options.buffered_mode_latency.Get(&latency)); + EXPECT_EQ(101, latency); + content.set_buffered_mode_latency(102); + EXPECT_TRUE(channel1_->SetRemoteContent(&content, CA_ANSWER)); + ASSERT_EQ(1U, media_channel1_->codecs().size()); + EXPECT_TRUE(CodecMatches(content.codecs()[0], + media_channel1_->codecs()[0])); + ASSERT_TRUE(media_channel1_->GetOptions(&options)); + EXPECT_TRUE(options.buffered_mode_latency.Get(&latency)); + EXPECT_EQ(102, latency); + } + + // Test that SetRemoteContent properly deals with a content update. + void TestSetRemoteContentUpdate() { + CreateChannels(0, 0); + typename T::Content content; + CreateContent(RTCP | RTCP_MUX | SECURE, + kPcmuCodec, kH264Codec, + &content); + EXPECT_EQ(0U, media_channel1_->codecs().size()); + EXPECT_TRUE(channel1_->SetLocalContent(&content, CA_OFFER)); + EXPECT_TRUE(channel1_->SetRemoteContent(&content, CA_ANSWER)); + ASSERT_EQ(1U, media_channel1_->codecs().size()); + EXPECT_TRUE(CodecMatches(content.codecs()[0], + media_channel1_->codecs()[0])); + // Now update with other codecs. + typename T::Content update_content; + update_content.set_partial(true); + CreateContent(0, kIsacCodec, kH264SvcCodec, + &update_content); + EXPECT_TRUE(channel1_->SetRemoteContent(&update_content, CA_UPDATE)); + ASSERT_EQ(1U, media_channel1_->codecs().size()); + EXPECT_TRUE(CodecMatches(update_content.codecs()[0], + media_channel1_->codecs()[0])); + // Now update without any codecs. This is ignored. + typename T::Content empty_content; + empty_content.set_partial(true); + EXPECT_TRUE(channel1_->SetRemoteContent(&empty_content, CA_UPDATE)); + ASSERT_EQ(1U, media_channel1_->codecs().size()); + EXPECT_TRUE(CodecMatches(update_content.codecs()[0], + media_channel1_->codecs()[0])); + } + + // Test that Add/RemoveStream properly forward to the media channel. + void TestStreams() { + CreateChannels(0, 0); + EXPECT_TRUE(AddStream1(1)); + EXPECT_TRUE(AddStream1(2)); + EXPECT_EQ(2U, media_channel1_->recv_streams().size()); + EXPECT_TRUE(RemoveStream1(2)); + EXPECT_EQ(1U, media_channel1_->recv_streams().size()); + EXPECT_TRUE(RemoveStream1(1)); + EXPECT_EQ(0U, media_channel1_->recv_streams().size()); + } + + // Test that SetLocalContent properly handles adding and removing StreamParams + // to the local content description. + // This test uses the CA_UPDATE action that don't require a full + // MediaContentDescription to do an update. + void TestUpdateStreamsInLocalContent() { + cricket::StreamParams stream1; + stream1.groupid = "group1"; + stream1.id = "stream1"; + stream1.ssrcs.push_back(kSsrc1); + stream1.cname = "stream1_cname"; + + cricket::StreamParams stream2; + stream2.groupid = "group2"; + stream2.id = "stream2"; + stream2.ssrcs.push_back(kSsrc2); + stream2.cname = "stream2_cname"; + + cricket::StreamParams stream3; + stream3.groupid = "group3"; + stream3.id = "stream3"; + stream3.ssrcs.push_back(kSsrc3); + stream3.cname = "stream3_cname"; + + CreateChannels(0, 0); + typename T::Content content1; + CreateContent(0, kPcmuCodec, kH264Codec, &content1); + content1.AddStream(stream1); + EXPECT_EQ(0u, media_channel1_->send_streams().size()); + EXPECT_TRUE(channel1_->SetLocalContent(&content1, CA_OFFER)); + + ASSERT_EQ(1u, media_channel1_->send_streams().size()); + EXPECT_EQ(stream1, media_channel1_->send_streams()[0]); + + // Update the local streams by adding another sending stream. + // Use a partial updated session description. + typename T::Content content2; + content2.AddStream(stream2); + content2.AddStream(stream3); + content2.set_partial(true); + EXPECT_TRUE(channel1_->SetLocalContent(&content2, CA_UPDATE)); + ASSERT_EQ(3u, media_channel1_->send_streams().size()); + EXPECT_EQ(stream1, media_channel1_->send_streams()[0]); + EXPECT_EQ(stream2, media_channel1_->send_streams()[1]); + EXPECT_EQ(stream3, media_channel1_->send_streams()[2]); + + // Update the local streams by removing the first sending stream. + // This is done by removing all SSRCS for this particular stream. + typename T::Content content3; + stream1.ssrcs.clear(); + content3.AddStream(stream1); + content3.set_partial(true); + EXPECT_TRUE(channel1_->SetLocalContent(&content3, CA_UPDATE)); + ASSERT_EQ(2u, media_channel1_->send_streams().size()); + EXPECT_EQ(stream2, media_channel1_->send_streams()[0]); + EXPECT_EQ(stream3, media_channel1_->send_streams()[1]); + + // Update the local streams with a stream that does not change. + // THe update is ignored. + typename T::Content content4; + content4.AddStream(stream2); + content4.set_partial(true); + EXPECT_TRUE(channel1_->SetLocalContent(&content4, CA_UPDATE)); + ASSERT_EQ(2u, media_channel1_->send_streams().size()); + EXPECT_EQ(stream2, media_channel1_->send_streams()[0]); + EXPECT_EQ(stream3, media_channel1_->send_streams()[1]); + } + + // Test that SetRemoteContent properly handles adding and removing + // StreamParams to the remote content description. + // This test uses the CA_UPDATE action that don't require a full + // MediaContentDescription to do an update. + void TestUpdateStreamsInRemoteContent() { + cricket::StreamParams stream1; + stream1.id = "Stream1"; + stream1.groupid = "1"; + stream1.ssrcs.push_back(kSsrc1); + stream1.cname = "stream1_cname"; + + cricket::StreamParams stream2; + stream2.id = "Stream2"; + stream2.groupid = "2"; + stream2.ssrcs.push_back(kSsrc2); + stream2.cname = "stream2_cname"; + + cricket::StreamParams stream3; + stream3.id = "Stream3"; + stream3.groupid = "3"; + stream3.ssrcs.push_back(kSsrc3); + stream3.cname = "stream3_cname"; + + CreateChannels(0, 0); + typename T::Content content1; + CreateContent(0, kPcmuCodec, kH264Codec, &content1); + content1.AddStream(stream1); + EXPECT_EQ(0u, media_channel1_->recv_streams().size()); + EXPECT_TRUE(channel1_->SetRemoteContent(&content1, CA_OFFER)); + + ASSERT_EQ(1u, media_channel1_->codecs().size()); + ASSERT_EQ(1u, media_channel1_->recv_streams().size()); + EXPECT_EQ(stream1, media_channel1_->recv_streams()[0]); + + // Update the remote streams by adding another sending stream. + // Use a partial updated session description. + typename T::Content content2; + content2.AddStream(stream2); + content2.AddStream(stream3); + content2.set_partial(true); + EXPECT_TRUE(channel1_->SetRemoteContent(&content2, CA_UPDATE)); + ASSERT_EQ(3u, media_channel1_->recv_streams().size()); + EXPECT_EQ(stream1, media_channel1_->recv_streams()[0]); + EXPECT_EQ(stream2, media_channel1_->recv_streams()[1]); + EXPECT_EQ(stream3, media_channel1_->recv_streams()[2]); + + // Update the remote streams by removing the first stream. + // This is done by removing all SSRCS for this particular stream. + typename T::Content content3; + stream1.ssrcs.clear(); + content3.AddStream(stream1); + content3.set_partial(true); + EXPECT_TRUE(channel1_->SetRemoteContent(&content3, CA_UPDATE)); + ASSERT_EQ(2u, media_channel1_->recv_streams().size()); + EXPECT_EQ(stream2, media_channel1_->recv_streams()[0]); + EXPECT_EQ(stream3, media_channel1_->recv_streams()[1]); + + // Update the remote streams with a stream that does not change. + // The update is ignored. + typename T::Content content4; + content4.AddStream(stream2); + content4.set_partial(true); + EXPECT_TRUE(channel1_->SetRemoteContent(&content4, CA_UPDATE)); + ASSERT_EQ(2u, media_channel1_->recv_streams().size()); + EXPECT_EQ(stream2, media_channel1_->recv_streams()[0]); + EXPECT_EQ(stream3, media_channel1_->recv_streams()[1]); + } + + // Test that SetLocalContent and SetRemoteContent properly + // handles adding and removing StreamParams when the action is a full + // CA_OFFER / CA_ANSWER. + void TestChangeStreamParamsInContent() { + cricket::StreamParams stream1; + stream1.groupid = "group1"; + stream1.id = "stream1"; + stream1.ssrcs.push_back(kSsrc1); + stream1.cname = "stream1_cname"; + + cricket::StreamParams stream2; + stream2.groupid = "group1"; + stream2.id = "stream2"; + stream2.ssrcs.push_back(kSsrc2); + stream2.cname = "stream2_cname"; + + // Setup a call where channel 1 send |stream1| to channel 2. + CreateChannels(0, 0); + typename T::Content content1; + CreateContent(0, kPcmuCodec, kH264Codec, &content1); + content1.AddStream(stream1); + EXPECT_TRUE(channel1_->SetLocalContent(&content1, CA_OFFER)); + EXPECT_TRUE(channel1_->Enable(true)); + EXPECT_EQ(1u, media_channel1_->send_streams().size()); + + EXPECT_TRUE(channel2_->SetRemoteContent(&content1, CA_OFFER)); + EXPECT_EQ(1u, media_channel2_->recv_streams().size()); + session1_.Connect(&session2_); + + // Channel 2 do not send anything. + typename T::Content content2; + CreateContent(0, kPcmuCodec, kH264Codec, &content2); + EXPECT_TRUE(channel1_->SetRemoteContent(&content2, CA_ANSWER)); + EXPECT_EQ(0u, media_channel1_->recv_streams().size()); + EXPECT_TRUE(channel2_->SetLocalContent(&content2, CA_ANSWER)); + EXPECT_TRUE(channel2_->Enable(true)); + EXPECT_EQ(0u, media_channel2_->send_streams().size()); + + EXPECT_TRUE(SendCustomRtp1(kSsrc1, 0)); + EXPECT_TRUE(CheckCustomRtp2(kSsrc1, 0)); + + // Let channel 2 update the content by sending |stream2| and enable SRTP. + typename T::Content content3; + CreateContent(SECURE, kPcmuCodec, kH264Codec, &content3); + content3.AddStream(stream2); + EXPECT_TRUE(channel2_->SetLocalContent(&content3, CA_OFFER)); + ASSERT_EQ(1u, media_channel2_->send_streams().size()); + EXPECT_EQ(stream2, media_channel2_->send_streams()[0]); + + EXPECT_TRUE(channel1_->SetRemoteContent(&content3, CA_OFFER)); + ASSERT_EQ(1u, media_channel1_->recv_streams().size()); + EXPECT_EQ(stream2, media_channel1_->recv_streams()[0]); + + // Channel 1 replies but stop sending stream1. + typename T::Content content4; + CreateContent(SECURE, kPcmuCodec, kH264Codec, &content4); + EXPECT_TRUE(channel1_->SetLocalContent(&content4, CA_ANSWER)); + EXPECT_EQ(0u, media_channel1_->send_streams().size()); + + EXPECT_TRUE(channel2_->SetRemoteContent(&content4, CA_ANSWER)); + EXPECT_EQ(0u, media_channel2_->recv_streams().size()); + + EXPECT_TRUE(channel1_->secure()); + EXPECT_TRUE(channel2_->secure()); + EXPECT_TRUE(SendCustomRtp2(kSsrc2, 0)); + EXPECT_TRUE(CheckCustomRtp1(kSsrc2, 0)); + } + + // Test that we only start playout and sending at the right times. + void TestPlayoutAndSendingStates() { + CreateChannels(0, 0); + EXPECT_FALSE(media_channel1_->playout()); + EXPECT_FALSE(media_channel1_->sending()); + EXPECT_FALSE(media_channel2_->playout()); + EXPECT_FALSE(media_channel2_->sending()); + EXPECT_TRUE(channel1_->Enable(true)); + EXPECT_FALSE(media_channel1_->playout()); + EXPECT_FALSE(media_channel1_->sending()); + EXPECT_TRUE(channel1_->SetLocalContent(&local_media_content1_, CA_OFFER)); + EXPECT_TRUE(media_channel1_->playout()); + EXPECT_FALSE(media_channel1_->sending()); + EXPECT_TRUE(channel2_->SetRemoteContent(&local_media_content1_, CA_OFFER)); + EXPECT_FALSE(media_channel2_->playout()); + EXPECT_FALSE(media_channel2_->sending()); + EXPECT_TRUE(channel2_->SetLocalContent(&local_media_content2_, CA_ANSWER)); + EXPECT_FALSE(media_channel2_->playout()); + EXPECT_FALSE(media_channel2_->sending()); + session1_.Connect(&session2_); + EXPECT_TRUE(media_channel1_->playout()); + EXPECT_FALSE(media_channel1_->sending()); + EXPECT_FALSE(media_channel2_->playout()); + EXPECT_FALSE(media_channel2_->sending()); + EXPECT_TRUE(channel2_->Enable(true)); + EXPECT_TRUE(media_channel2_->playout()); + EXPECT_TRUE(media_channel2_->sending()); + EXPECT_TRUE(channel1_->SetRemoteContent(&local_media_content2_, CA_ANSWER)); + EXPECT_TRUE(media_channel1_->playout()); + EXPECT_TRUE(media_channel1_->sending()); + } + + void TestMuteStream() { + CreateChannels(0, 0); + // Test that we can Mute the default channel even though the sending SSRC is + // unknown. + EXPECT_FALSE(media_channel1_->IsStreamMuted(0)); + EXPECT_TRUE(channel1_->MuteStream(0, true)); + EXPECT_TRUE(media_channel1_->IsStreamMuted(0)); + EXPECT_TRUE(channel1_->MuteStream(0, false)); + EXPECT_FALSE(media_channel1_->IsStreamMuted(0)); + + // Test that we can not mute an unknown SSRC. + EXPECT_FALSE(channel1_->MuteStream(kSsrc1, true)); + + SendInitiate(); + // After the local session description has been set, we can mute a stream + // with its SSRC. + EXPECT_TRUE(channel1_->MuteStream(kSsrc1, true)); + EXPECT_TRUE(media_channel1_->IsStreamMuted(kSsrc1)); + EXPECT_TRUE(channel1_->MuteStream(kSsrc1, false)); + EXPECT_FALSE(media_channel1_->IsStreamMuted(kSsrc1)); + } + + // Test that changing the MediaContentDirection in the local and remote + // session description start playout and sending at the right time. + void TestMediaContentDirection() { + CreateChannels(0, 0); + typename T::Content content1; + CreateContent(0, kPcmuCodec, kH264Codec, &content1); + typename T::Content content2; + CreateContent(0, kPcmuCodec, kH264Codec, &content2); + // Set |content2| to be InActive. + content2.set_direction(cricket::MD_INACTIVE); + + EXPECT_TRUE(channel1_->Enable(true)); + EXPECT_TRUE(channel2_->Enable(true)); + EXPECT_FALSE(media_channel1_->playout()); + EXPECT_FALSE(media_channel1_->sending()); + EXPECT_FALSE(media_channel2_->playout()); + EXPECT_FALSE(media_channel2_->sending()); + + EXPECT_TRUE(channel1_->SetLocalContent(&content1, CA_OFFER)); + EXPECT_TRUE(channel2_->SetRemoteContent(&content1, CA_OFFER)); + EXPECT_TRUE(channel2_->SetLocalContent(&content2, CA_PRANSWER)); + EXPECT_TRUE(channel1_->SetRemoteContent(&content2, CA_PRANSWER)); + session1_.Connect(&session2_); + + EXPECT_TRUE(media_channel1_->playout()); + EXPECT_FALSE(media_channel1_->sending()); // remote InActive + EXPECT_FALSE(media_channel2_->playout()); // local InActive + EXPECT_FALSE(media_channel2_->sending()); // local InActive + + // Update |content2| to be RecvOnly. + content2.set_direction(cricket::MD_RECVONLY); + EXPECT_TRUE(channel2_->SetLocalContent(&content2, CA_PRANSWER)); + EXPECT_TRUE(channel1_->SetRemoteContent(&content2, CA_PRANSWER)); + + EXPECT_TRUE(media_channel1_->playout()); + EXPECT_TRUE(media_channel1_->sending()); + EXPECT_TRUE(media_channel2_->playout()); // local RecvOnly + EXPECT_FALSE(media_channel2_->sending()); // local RecvOnly + + // Update |content2| to be SendRecv. + content2.set_direction(cricket::MD_SENDRECV); + EXPECT_TRUE(channel2_->SetLocalContent(&content2, CA_ANSWER)); + EXPECT_TRUE(channel1_->SetRemoteContent(&content2, CA_ANSWER)); + + EXPECT_TRUE(media_channel1_->playout()); + EXPECT_TRUE(media_channel1_->sending()); + EXPECT_TRUE(media_channel2_->playout()); + EXPECT_TRUE(media_channel2_->sending()); + } + + // Test setting up a call. + void TestCallSetup() { + CreateChannels(0, 0); + EXPECT_FALSE(channel1_->secure()); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(media_channel1_->playout()); + EXPECT_FALSE(media_channel1_->sending()); + EXPECT_TRUE(SendAccept()); + EXPECT_FALSE(channel1_->secure()); + EXPECT_TRUE(media_channel1_->sending()); + EXPECT_EQ(1U, media_channel1_->codecs().size()); + EXPECT_TRUE(media_channel2_->playout()); + EXPECT_TRUE(media_channel2_->sending()); + EXPECT_EQ(1U, media_channel2_->codecs().size()); + } + + // Test that we don't crash if packets are sent during call teardown + // when RTCP mux is enabled. This is a regression test against a specific + // race condition that would only occur when a RTCP packet was sent during + // teardown of a channel on which RTCP mux was enabled. + void TestCallTeardownRtcpMux() { + class LastWordMediaChannel : public T::MediaChannel { + public: + LastWordMediaChannel() : T::MediaChannel(NULL) {} + ~LastWordMediaChannel() { + T::MediaChannel::SendRtp(kPcmuFrame, sizeof(kPcmuFrame)); + T::MediaChannel::SendRtcp(kRtcpReport, sizeof(kRtcpReport)); + } + }; + CreateChannels(new LastWordMediaChannel(), new LastWordMediaChannel(), + RTCP | RTCP_MUX, RTCP | RTCP_MUX, + talk_base::Thread::Current()); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_TRUE(SendTerminate()); + } + + // Send voice RTP data to the other side and ensure it gets there. + void SendRtpToRtp() { + CreateChannels(0, 0); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(1U, GetTransport1()->channels().size()); + EXPECT_EQ(1U, GetTransport2()->channels().size()); + EXPECT_TRUE(SendRtp1()); + EXPECT_TRUE(SendRtp2()); + EXPECT_TRUE(CheckRtp1()); + EXPECT_TRUE(CheckRtp2()); + EXPECT_TRUE(CheckNoRtp1()); + EXPECT_TRUE(CheckNoRtp2()); + } + + // Check that RTCP is not transmitted if both sides don't support RTCP. + void SendNoRtcpToNoRtcp() { + CreateChannels(0, 0); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(1U, GetTransport1()->channels().size()); + EXPECT_EQ(1U, GetTransport2()->channels().size()); + EXPECT_FALSE(SendRtcp1()); + EXPECT_FALSE(SendRtcp2()); + EXPECT_TRUE(CheckNoRtcp1()); + EXPECT_TRUE(CheckNoRtcp2()); + } + + // Check that RTCP is not transmitted if the callee doesn't support RTCP. + void SendNoRtcpToRtcp() { + CreateChannels(0, RTCP); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(1U, GetTransport1()->channels().size()); + EXPECT_EQ(2U, GetTransport2()->channels().size()); + EXPECT_FALSE(SendRtcp1()); + EXPECT_FALSE(SendRtcp2()); + EXPECT_TRUE(CheckNoRtcp1()); + EXPECT_TRUE(CheckNoRtcp2()); + } + + // Check that RTCP is not transmitted if the caller doesn't support RTCP. + void SendRtcpToNoRtcp() { + CreateChannels(RTCP, 0); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(2U, GetTransport1()->channels().size()); + EXPECT_EQ(1U, GetTransport2()->channels().size()); + EXPECT_FALSE(SendRtcp1()); + EXPECT_FALSE(SendRtcp2()); + EXPECT_TRUE(CheckNoRtcp1()); + EXPECT_TRUE(CheckNoRtcp2()); + } + + // Check that RTCP is transmitted if both sides support RTCP. + void SendRtcpToRtcp() { + CreateChannels(RTCP, RTCP); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(2U, GetTransport1()->channels().size()); + EXPECT_EQ(2U, GetTransport2()->channels().size()); + EXPECT_TRUE(SendRtcp1()); + EXPECT_TRUE(SendRtcp2()); + EXPECT_TRUE(CheckRtcp1()); + EXPECT_TRUE(CheckRtcp2()); + EXPECT_TRUE(CheckNoRtcp1()); + EXPECT_TRUE(CheckNoRtcp2()); + } + + // Check that RTCP is transmitted if only the initiator supports mux. + void SendRtcpMuxToRtcp() { + CreateChannels(RTCP | RTCP_MUX, RTCP); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(2U, GetTransport1()->channels().size()); + EXPECT_EQ(2U, GetTransport2()->channels().size()); + EXPECT_TRUE(SendRtcp1()); + EXPECT_TRUE(SendRtcp2()); + EXPECT_TRUE(CheckRtcp1()); + EXPECT_TRUE(CheckRtcp2()); + EXPECT_TRUE(CheckNoRtcp1()); + EXPECT_TRUE(CheckNoRtcp2()); + } + + // Check that RTP and RTCP are transmitted ok when both sides support mux. + void SendRtcpMuxToRtcpMux() { + CreateChannels(RTCP | RTCP_MUX, RTCP | RTCP_MUX); + EXPECT_TRUE(SendInitiate()); + EXPECT_EQ(2U, GetTransport1()->channels().size()); + EXPECT_EQ(1U, GetTransport2()->channels().size()); + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(1U, GetTransport1()->channels().size()); + EXPECT_TRUE(SendRtp1()); + EXPECT_TRUE(SendRtp2()); + EXPECT_TRUE(SendRtcp1()); + EXPECT_TRUE(SendRtcp2()); + EXPECT_TRUE(CheckRtp1()); + EXPECT_TRUE(CheckRtp2()); + EXPECT_TRUE(CheckNoRtp1()); + EXPECT_TRUE(CheckNoRtp2()); + EXPECT_TRUE(CheckRtcp1()); + EXPECT_TRUE(CheckRtcp2()); + EXPECT_TRUE(CheckNoRtcp1()); + EXPECT_TRUE(CheckNoRtcp2()); + } + + // Check that RTCP data sent by the initiator before the accept is not muxed. + void SendEarlyRtcpMuxToRtcp() { + CreateChannels(RTCP | RTCP_MUX, RTCP); + EXPECT_TRUE(SendInitiate()); + EXPECT_EQ(2U, GetTransport1()->channels().size()); + EXPECT_EQ(2U, GetTransport2()->channels().size()); + + // RTCP can be sent before the call is accepted, if the transport is ready. + // It should not be muxed though, as the remote side doesn't support mux. + EXPECT_TRUE(SendRtcp1()); + EXPECT_TRUE(CheckNoRtp2()); + EXPECT_TRUE(CheckRtcp2()); + + // Send RTCP packet from callee and verify that it is received. + EXPECT_TRUE(SendRtcp2()); + EXPECT_TRUE(CheckNoRtp1()); + EXPECT_TRUE(CheckRtcp1()); + + // Complete call setup and ensure everything is still OK. + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(2U, GetTransport1()->channels().size()); + EXPECT_TRUE(SendRtcp1()); + EXPECT_TRUE(CheckRtcp2()); + EXPECT_TRUE(SendRtcp2()); + EXPECT_TRUE(CheckRtcp1()); + } + + + // Check that RTCP data is not muxed until both sides have enabled muxing, + // but that we properly demux before we get the accept message, since there + // is a race between RTP data and the jingle accept. + void SendEarlyRtcpMuxToRtcpMux() { + CreateChannels(RTCP | RTCP_MUX, RTCP | RTCP_MUX); + EXPECT_TRUE(SendInitiate()); + EXPECT_EQ(2U, GetTransport1()->channels().size()); + EXPECT_EQ(1U, GetTransport2()->channels().size()); + + // RTCP can't be sent yet, since the RTCP transport isn't writable, and + // we haven't yet received the accept that says we should mux. + EXPECT_FALSE(SendRtcp1()); + + // Send muxed RTCP packet from callee and verify that it is received. + EXPECT_TRUE(SendRtcp2()); + EXPECT_TRUE(CheckNoRtp1()); + EXPECT_TRUE(CheckRtcp1()); + + // Complete call setup and ensure everything is still OK. + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(1U, GetTransport1()->channels().size()); + EXPECT_TRUE(SendRtcp1()); + EXPECT_TRUE(CheckRtcp2()); + EXPECT_TRUE(SendRtcp2()); + EXPECT_TRUE(CheckRtcp1()); + } + + // Test that we properly send SRTP with RTCP in both directions. + // You can pass in DTLS and/or RTCP_MUX as flags. + void SendSrtpToSrtp(int flags1_in = 0, int flags2_in = 0) { + ASSERT((flags1_in & ~(RTCP_MUX | DTLS)) == 0); + ASSERT((flags2_in & ~(RTCP_MUX | DTLS)) == 0); + + int flags1 = RTCP | SECURE | flags1_in; + int flags2 = RTCP | SECURE | flags2_in; + bool dtls1 = !!(flags1_in & DTLS); + bool dtls2 = !!(flags2_in & DTLS); + CreateChannels(flags1, flags2); + EXPECT_FALSE(channel1_->secure()); + EXPECT_FALSE(channel2_->secure()); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE_WAIT(channel1_->writable(), kEventTimeout); + EXPECT_TRUE_WAIT(channel2_->writable(), kEventTimeout); + EXPECT_TRUE(SendAccept()); + EXPECT_TRUE(channel1_->secure()); + EXPECT_TRUE(channel2_->secure()); + EXPECT_EQ(dtls1 && dtls2, channel1_->secure_dtls()); + EXPECT_EQ(dtls1 && dtls2, channel2_->secure_dtls()); + EXPECT_TRUE(SendRtp1()); + EXPECT_TRUE(SendRtp2()); + EXPECT_TRUE(SendRtcp1()); + EXPECT_TRUE(SendRtcp2()); + EXPECT_TRUE(CheckRtp1()); + EXPECT_TRUE(CheckRtp2()); + EXPECT_TRUE(CheckNoRtp1()); + EXPECT_TRUE(CheckNoRtp2()); + EXPECT_TRUE(CheckRtcp1()); + EXPECT_TRUE(CheckRtcp2()); + EXPECT_TRUE(CheckNoRtcp1()); + EXPECT_TRUE(CheckNoRtcp2()); + } + + // Test that we properly handling SRTP negotiating down to RTP. + void SendSrtpToRtp() { + CreateChannels(RTCP | SECURE, RTCP); + EXPECT_FALSE(channel1_->secure()); + EXPECT_FALSE(channel2_->secure()); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_FALSE(channel1_->secure()); + EXPECT_FALSE(channel2_->secure()); + EXPECT_TRUE(SendRtp1()); + EXPECT_TRUE(SendRtp2()); + EXPECT_TRUE(SendRtcp1()); + EXPECT_TRUE(SendRtcp2()); + EXPECT_TRUE(CheckRtp1()); + EXPECT_TRUE(CheckRtp2()); + EXPECT_TRUE(CheckNoRtp1()); + EXPECT_TRUE(CheckNoRtp2()); + EXPECT_TRUE(CheckRtcp1()); + EXPECT_TRUE(CheckRtcp2()); + EXPECT_TRUE(CheckNoRtcp1()); + EXPECT_TRUE(CheckNoRtcp2()); + } + + // Test that we can send and receive early media when a provisional answer is + // sent and received. The test uses SRTP, RTCP mux and SSRC mux. + void SendEarlyMediaUsingRtcpMuxSrtp() { + int sequence_number1_1 = 0, sequence_number2_2 = 0; + + CreateChannels(SSRC_MUX | RTCP | RTCP_MUX | SECURE, + SSRC_MUX | RTCP | RTCP_MUX | SECURE); + EXPECT_TRUE(SendOffer()); + EXPECT_TRUE(SendProvisionalAnswer()); + EXPECT_TRUE(channel1_->secure()); + EXPECT_TRUE(channel2_->secure()); + EXPECT_EQ(2U, GetTransport1()->channels().size()); + EXPECT_EQ(2U, GetTransport2()->channels().size()); + EXPECT_TRUE(SendCustomRtcp1(kSsrc1)); + EXPECT_TRUE(CheckCustomRtcp2(kSsrc1)); + EXPECT_TRUE(SendCustomRtp1(kSsrc1, ++sequence_number1_1)); + EXPECT_TRUE(CheckCustomRtp2(kSsrc1, sequence_number1_1)); + + // Send packets from callee and verify that it is received. + EXPECT_TRUE(SendCustomRtcp2(kSsrc2)); + EXPECT_TRUE(CheckCustomRtcp1(kSsrc2)); + EXPECT_TRUE(SendCustomRtp2(kSsrc2, ++sequence_number2_2)); + EXPECT_TRUE(CheckCustomRtp1(kSsrc2, sequence_number2_2)); + + // Complete call setup and ensure everything is still OK. + EXPECT_TRUE(SendFinalAnswer()); + EXPECT_EQ(1U, GetTransport1()->channels().size()); + EXPECT_EQ(1U, GetTransport2()->channels().size()); + EXPECT_TRUE(channel1_->secure()); + EXPECT_TRUE(channel2_->secure()); + EXPECT_TRUE(SendCustomRtcp1(kSsrc1)); + EXPECT_TRUE(CheckCustomRtcp2(kSsrc1)); + EXPECT_TRUE(SendCustomRtp1(kSsrc1, ++sequence_number1_1)); + EXPECT_TRUE(CheckCustomRtp2(kSsrc1, sequence_number1_1)); + EXPECT_TRUE(SendCustomRtcp2(kSsrc2)); + EXPECT_TRUE(CheckCustomRtcp1(kSsrc2)); + EXPECT_TRUE(SendCustomRtp2(kSsrc2, ++sequence_number2_2)); + EXPECT_TRUE(CheckCustomRtp1(kSsrc2, sequence_number2_2)); + } + + // Test that we properly send RTP without SRTP from a thread. + void SendRtpToRtpOnThread() { + bool sent_rtp1, sent_rtp2, sent_rtcp1, sent_rtcp2; + CreateChannels(RTCP, RTCP); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + CallOnThread(&ChannelTest::SendRtp1, &sent_rtp1); + CallOnThread(&ChannelTest::SendRtp2, &sent_rtp2); + CallOnThread(&ChannelTest::SendRtcp1, &sent_rtcp1); + CallOnThread(&ChannelTest::SendRtcp2, &sent_rtcp2); + EXPECT_TRUE_WAIT(CheckRtp1(), 1000); + EXPECT_TRUE_WAIT(CheckRtp2(), 1000); + EXPECT_TRUE_WAIT(sent_rtp1, 1000); + EXPECT_TRUE_WAIT(sent_rtp2, 1000); + EXPECT_TRUE(CheckNoRtp1()); + EXPECT_TRUE(CheckNoRtp2()); + EXPECT_TRUE_WAIT(CheckRtcp1(), 1000); + EXPECT_TRUE_WAIT(CheckRtcp2(), 1000); + EXPECT_TRUE_WAIT(sent_rtcp1, 1000); + EXPECT_TRUE_WAIT(sent_rtcp2, 1000); + EXPECT_TRUE(CheckNoRtcp1()); + EXPECT_TRUE(CheckNoRtcp2()); + } + + // Test that we properly send SRTP with RTCP from a thread. + void SendSrtpToSrtpOnThread() { + bool sent_rtp1, sent_rtp2, sent_rtcp1, sent_rtcp2; + CreateChannels(RTCP | SECURE, RTCP | SECURE); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + CallOnThread(&ChannelTest::SendRtp1, &sent_rtp1); + CallOnThread(&ChannelTest::SendRtp2, &sent_rtp2); + CallOnThread(&ChannelTest::SendRtcp1, &sent_rtcp1); + CallOnThread(&ChannelTest::SendRtcp2, &sent_rtcp2); + EXPECT_TRUE_WAIT(CheckRtp1(), 1000); + EXPECT_TRUE_WAIT(CheckRtp2(), 1000); + EXPECT_TRUE_WAIT(sent_rtp1, 1000); + EXPECT_TRUE_WAIT(sent_rtp2, 1000); + EXPECT_TRUE(CheckNoRtp1()); + EXPECT_TRUE(CheckNoRtp2()); + EXPECT_TRUE_WAIT(CheckRtcp1(), 1000); + EXPECT_TRUE_WAIT(CheckRtcp2(), 1000); + EXPECT_TRUE_WAIT(sent_rtcp1, 1000); + EXPECT_TRUE_WAIT(sent_rtcp2, 1000); + EXPECT_TRUE(CheckNoRtcp1()); + EXPECT_TRUE(CheckNoRtcp2()); + } + + // Test that the mediachannel retains its sending state after the transport + // becomes non-writable. + void SendWithWritabilityLoss() { + CreateChannels(0, 0); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(1U, GetTransport1()->channels().size()); + EXPECT_EQ(1U, GetTransport2()->channels().size()); + EXPECT_TRUE(SendRtp1()); + EXPECT_TRUE(SendRtp2()); + EXPECT_TRUE(CheckRtp1()); + EXPECT_TRUE(CheckRtp2()); + EXPECT_TRUE(CheckNoRtp1()); + EXPECT_TRUE(CheckNoRtp2()); + + // Lose writability, with optimistic send + SetOptimisticDataSend(true); + GetTransport1()->SetWritable(false); + EXPECT_TRUE(media_channel1_->sending()); + EXPECT_TRUE(SendRtp1()); + EXPECT_TRUE(SendRtp2()); + EXPECT_TRUE(CheckRtp1()); + EXPECT_TRUE(CheckRtp2()); + EXPECT_TRUE(CheckNoRtp1()); + EXPECT_TRUE(CheckNoRtp2()); + + // Check again with optimistic send off, which should fail. + SetOptimisticDataSend(false); + EXPECT_FALSE(SendRtp1()); + EXPECT_TRUE(SendRtp2()); + EXPECT_TRUE(CheckRtp1()); + EXPECT_TRUE(CheckNoRtp2()); + + // Regain writability + GetTransport1()->SetWritable(true); + EXPECT_TRUE(media_channel1_->sending()); + EXPECT_TRUE(SendRtp1()); + EXPECT_TRUE(SendRtp2()); + EXPECT_TRUE(CheckRtp1()); + EXPECT_TRUE(CheckRtp2()); + EXPECT_TRUE(CheckNoRtp1()); + EXPECT_TRUE(CheckNoRtp2()); + + // Lose writability completely + GetTransport1()->SetDestination(NULL); + EXPECT_TRUE(media_channel1_->sending()); + + // Should fail regardless of optimistic send at this point. + SetOptimisticDataSend(true); + EXPECT_FALSE(SendRtp1()); + EXPECT_TRUE(SendRtp2()); + EXPECT_TRUE(CheckRtp1()); + EXPECT_TRUE(CheckNoRtp2()); + SetOptimisticDataSend(false); + EXPECT_FALSE(SendRtp1()); + EXPECT_TRUE(SendRtp2()); + EXPECT_TRUE(CheckRtp1()); + EXPECT_TRUE(CheckNoRtp2()); + + // Gain writability back + GetTransport1()->SetDestination(GetTransport2()); + EXPECT_TRUE(media_channel1_->sending()); + EXPECT_TRUE(SendRtp1()); + EXPECT_TRUE(SendRtp2()); + EXPECT_TRUE(CheckRtp1()); + EXPECT_TRUE(CheckRtp2()); + EXPECT_TRUE(CheckNoRtp1()); + EXPECT_TRUE(CheckNoRtp2()); + } + + void SendSsrcMuxToSsrcMuxWithRtcpMux() { + int sequence_number1_1 = 0, sequence_number2_2 = 0; + CreateChannels(SSRC_MUX | RTCP | RTCP_MUX, SSRC_MUX | RTCP | RTCP_MUX); + EXPECT_TRUE(SendInitiate()); + EXPECT_EQ(2U, GetTransport1()->channels().size()); + EXPECT_EQ(1U, GetTransport2()->channels().size()); + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(1U, GetTransport1()->channels().size()); + EXPECT_EQ(1U, GetTransport2()->channels().size()); + EXPECT_TRUE(channel1_->ssrc_filter()->IsActive()); + // channel1 - should have media_content2 as remote. i.e. kSsrc2 + EXPECT_TRUE(channel1_->ssrc_filter()->FindStream(kSsrc2)); + EXPECT_TRUE(channel2_->ssrc_filter()->IsActive()); + // channel2 - should have media_content1 as remote. i.e. kSsrc1 + EXPECT_TRUE(channel2_->ssrc_filter()->FindStream(kSsrc1)); + EXPECT_TRUE(SendCustomRtp1(kSsrc1, ++sequence_number1_1)); + EXPECT_TRUE(SendCustomRtp2(kSsrc2, ++sequence_number2_2)); + EXPECT_TRUE(SendCustomRtcp1(kSsrc1)); + EXPECT_TRUE(SendCustomRtcp2(kSsrc2)); + EXPECT_TRUE(CheckCustomRtp1(kSsrc2, sequence_number2_2)); + EXPECT_TRUE(CheckNoRtp1()); + EXPECT_TRUE(CheckCustomRtp2(kSsrc1, sequence_number1_1)); + EXPECT_TRUE(CheckNoRtp2()); + EXPECT_TRUE(CheckCustomRtcp1(kSsrc2)); + EXPECT_TRUE(CheckNoRtcp1()); + EXPECT_TRUE(CheckCustomRtcp2(kSsrc1)); + EXPECT_TRUE(CheckNoRtcp2()); + } + + void SendSsrcMuxToSsrcMux() { + int sequence_number1_1 = 0, sequence_number2_2 = 0; + CreateChannels(SSRC_MUX | RTCP, SSRC_MUX | RTCP); + EXPECT_TRUE(SendInitiate()); + EXPECT_EQ(2U, GetTransport1()->channels().size()); + EXPECT_EQ(2U, GetTransport2()->channels().size()); + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(2U, GetTransport1()->channels().size()); + EXPECT_EQ(2U, GetTransport2()->channels().size()); + EXPECT_TRUE(channel1_->ssrc_filter()->IsActive()); + // channel1 - should have media_content2 as remote. i.e. kSsrc2 + EXPECT_TRUE(channel1_->ssrc_filter()->FindStream(kSsrc2)); + EXPECT_TRUE(channel2_->ssrc_filter()->IsActive()); + // channel2 - should have media_content1 as remote. i.e. kSsrc1 + EXPECT_TRUE(SendCustomRtp1(kSsrc1, ++sequence_number1_1)); + EXPECT_TRUE(SendCustomRtp2(kSsrc2, ++sequence_number2_2)); + EXPECT_TRUE(SendCustomRtcp1(kSsrc1)); + EXPECT_TRUE(SendCustomRtcp2(kSsrc2)); + EXPECT_TRUE(CheckCustomRtp1(kSsrc2, sequence_number2_2)); + EXPECT_FALSE(CheckCustomRtp1(kSsrc1, sequence_number2_2)); + EXPECT_TRUE(CheckCustomRtp2(kSsrc1, sequence_number1_1)); + EXPECT_FALSE(CheckCustomRtp2(kSsrc2, sequence_number1_1)); + EXPECT_TRUE(CheckCustomRtcp1(kSsrc2)); + EXPECT_FALSE(CheckCustomRtcp1(kSsrc1)); + EXPECT_TRUE(CheckCustomRtcp2(kSsrc1)); + EXPECT_FALSE(CheckCustomRtcp2(kSsrc2)); + } + + // Test that the media monitor can be run and gives timely callbacks. + void TestMediaMonitor() { + static const int kTimeout = 500; + CreateChannels(0, 0); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + channel1_->StartMediaMonitor(100); + channel2_->StartMediaMonitor(100); + // Ensure we get callbacks and stop. + EXPECT_TRUE_WAIT(media_info_callbacks1_ > 0, kTimeout); + EXPECT_TRUE_WAIT(media_info_callbacks2_ > 0, kTimeout); + channel1_->StopMediaMonitor(); + channel2_->StopMediaMonitor(); + // Ensure a restart of a stopped monitor works. + channel1_->StartMediaMonitor(100); + EXPECT_TRUE_WAIT(media_info_callbacks1_ > 0, kTimeout); + channel1_->StopMediaMonitor(); + // Ensure stopping a stopped monitor is OK. + channel1_->StopMediaMonitor(); + } + + void TestMediaSinks() { + CreateChannels(0, 0); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_FALSE(channel1_->HasSendSinks(cricket::SINK_POST_CRYPTO)); + EXPECT_FALSE(channel1_->HasRecvSinks(cricket::SINK_POST_CRYPTO)); + EXPECT_FALSE(channel1_->HasSendSinks(cricket::SINK_PRE_CRYPTO)); + EXPECT_FALSE(channel1_->HasRecvSinks(cricket::SINK_PRE_CRYPTO)); + + talk_base::Pathname path; + EXPECT_TRUE(talk_base::Filesystem::GetTemporaryFolder(path, true, NULL)); + path.SetFilename("sink-test.rtpdump"); + talk_base::scoped_ptr sink( + new cricket::RtpDumpSink(Open(path.pathname()))); + sink->set_packet_filter(cricket::PF_ALL); + EXPECT_TRUE(sink->Enable(true)); + channel1_->RegisterSendSink( + sink.get(), &cricket::RtpDumpSink::OnPacket, cricket::SINK_POST_CRYPTO); + EXPECT_TRUE(channel1_->HasSendSinks(cricket::SINK_POST_CRYPTO)); + EXPECT_FALSE(channel1_->HasRecvSinks(cricket::SINK_POST_CRYPTO)); + EXPECT_FALSE(channel1_->HasSendSinks(cricket::SINK_PRE_CRYPTO)); + EXPECT_FALSE(channel1_->HasRecvSinks(cricket::SINK_PRE_CRYPTO)); + + // The first packet is recorded with header + data. + EXPECT_TRUE(SendRtp1()); + // The second packet is recorded with header only. + sink->set_packet_filter(cricket::PF_RTPHEADER); + EXPECT_TRUE(SendRtp1()); + // The third packet is not recorded since sink is disabled. + EXPECT_TRUE(sink->Enable(false)); + EXPECT_TRUE(SendRtp1()); + // The fourth packet is not recorded since sink is unregistered. + EXPECT_TRUE(sink->Enable(true)); + channel1_->UnregisterSendSink(sink.get(), cricket::SINK_POST_CRYPTO); + EXPECT_TRUE(SendRtp1()); + sink.reset(); // This will close the file. + + // Read the recorded file and verify two packets. + talk_base::scoped_ptr stream( + talk_base::Filesystem::OpenFile(path, "rb")); + + cricket::RtpDumpReader reader(stream.get()); + cricket::RtpDumpPacket packet; + EXPECT_EQ(talk_base::SR_SUCCESS, reader.ReadPacket(&packet)); + std::string read_packet(reinterpret_cast(&packet.data[0]), + packet.data.size()); + EXPECT_EQ(rtp_packet_, read_packet); + + EXPECT_EQ(talk_base::SR_SUCCESS, reader.ReadPacket(&packet)); + size_t len = 0; + packet.GetRtpHeaderLen(&len); + EXPECT_EQ(len, packet.data.size()); + EXPECT_EQ(0, memcmp(&packet.data[0], rtp_packet_.c_str(), len)); + + EXPECT_EQ(talk_base::SR_EOS, reader.ReadPacket(&packet)); + + // Delete the file for media recording. + stream.reset(); + EXPECT_TRUE(talk_base::Filesystem::DeleteFile(path)); + } + + void TestSetContentFailure() { + CreateChannels(0, 0); + typename T::Content content; + cricket::SessionDescription* sdesc_loc = new cricket::SessionDescription(); + cricket::SessionDescription* sdesc_rem = new cricket::SessionDescription(); + + // Set up the session description. + CreateContent(0, kPcmuCodec, kH264Codec, &content); + sdesc_loc->AddContent(cricket::CN_AUDIO, cricket::NS_JINGLE_RTP, + new cricket::AudioContentDescription()); + sdesc_loc->AddContent(cricket::CN_VIDEO, cricket::NS_JINGLE_RTP, + new cricket::VideoContentDescription()); + EXPECT_TRUE(session1_.set_local_description(sdesc_loc)); + sdesc_rem->AddContent(cricket::CN_AUDIO, cricket::NS_JINGLE_RTP, + new cricket::AudioContentDescription()); + sdesc_rem->AddContent(cricket::CN_VIDEO, cricket::NS_JINGLE_RTP, + new cricket::VideoContentDescription()); + EXPECT_TRUE(session1_.set_remote_description(sdesc_rem)); + + // Test failures in SetLocalContent. + media_channel1_->set_fail_set_recv_codecs(true); + session1_.SetError(cricket::BaseSession::ERROR_NONE); + session1_.SetState(cricket::Session::STATE_SENTINITIATE); + EXPECT_EQ(cricket::BaseSession::ERROR_CONTENT, session1_.error()); + media_channel1_->set_fail_set_recv_codecs(true); + session1_.SetError(cricket::BaseSession::ERROR_NONE); + session1_.SetState(cricket::Session::STATE_SENTACCEPT); + EXPECT_EQ(cricket::BaseSession::ERROR_CONTENT, session1_.error()); + + // Test failures in SetRemoteContent. + media_channel1_->set_fail_set_send_codecs(true); + session1_.SetError(cricket::BaseSession::ERROR_NONE); + session1_.SetState(cricket::Session::STATE_RECEIVEDINITIATE); + EXPECT_EQ(cricket::BaseSession::ERROR_CONTENT, session1_.error()); + media_channel1_->set_fail_set_send_codecs(true); + session1_.SetError(cricket::BaseSession::ERROR_NONE); + session1_.SetState(cricket::Session::STATE_RECEIVEDACCEPT); + EXPECT_EQ(cricket::BaseSession::ERROR_CONTENT, session1_.error()); + } + + void TestSendTwoOffers() { + CreateChannels(0, 0); + + // Set up the initial session description. + cricket::SessionDescription* sdesc = CreateSessionDescriptionWithStream(1); + EXPECT_TRUE(session1_.set_local_description(sdesc)); + + session1_.SetError(cricket::BaseSession::ERROR_NONE); + session1_.SetState(cricket::Session::STATE_SENTINITIATE); + EXPECT_EQ(cricket::BaseSession::ERROR_NONE, session1_.error()); + EXPECT_TRUE(media_channel1_->HasSendStream(1)); + + // Update the local description and set the state again. + sdesc = CreateSessionDescriptionWithStream(2); + EXPECT_TRUE(session1_.set_local_description(sdesc)); + + session1_.SetState(cricket::Session::STATE_SENTINITIATE); + EXPECT_EQ(cricket::BaseSession::ERROR_NONE, session1_.error()); + EXPECT_FALSE(media_channel1_->HasSendStream(1)); + EXPECT_TRUE(media_channel1_->HasSendStream(2)); + } + + void TestReceiveTwoOffers() { + CreateChannels(0, 0); + + // Set up the initial session description. + cricket::SessionDescription* sdesc = CreateSessionDescriptionWithStream(1); + EXPECT_TRUE(session1_.set_remote_description(sdesc)); + + session1_.SetError(cricket::BaseSession::ERROR_NONE); + session1_.SetState(cricket::Session::STATE_RECEIVEDINITIATE); + EXPECT_EQ(cricket::BaseSession::ERROR_NONE, session1_.error()); + EXPECT_TRUE(media_channel1_->HasRecvStream(1)); + + sdesc = CreateSessionDescriptionWithStream(2); + EXPECT_TRUE(session1_.set_remote_description(sdesc)); + session1_.SetState(cricket::Session::STATE_RECEIVEDINITIATE); + EXPECT_EQ(cricket::BaseSession::ERROR_NONE, session1_.error()); + EXPECT_FALSE(media_channel1_->HasRecvStream(1)); + EXPECT_TRUE(media_channel1_->HasRecvStream(2)); + } + + void TestSendPrAnswer() { + CreateChannels(0, 0); + + // Set up the initial session description. + cricket::SessionDescription* sdesc = CreateSessionDescriptionWithStream(1); + EXPECT_TRUE(session1_.set_remote_description(sdesc)); + + session1_.SetError(cricket::BaseSession::ERROR_NONE); + session1_.SetState(cricket::Session::STATE_RECEIVEDINITIATE); + EXPECT_EQ(cricket::BaseSession::ERROR_NONE, session1_.error()); + EXPECT_TRUE(media_channel1_->HasRecvStream(1)); + + // Send PRANSWER + sdesc = CreateSessionDescriptionWithStream(2); + EXPECT_TRUE(session1_.set_local_description(sdesc)); + + session1_.SetState(cricket::Session::STATE_SENTPRACCEPT); + EXPECT_EQ(cricket::BaseSession::ERROR_NONE, session1_.error()); + EXPECT_TRUE(media_channel1_->HasRecvStream(1)); + EXPECT_TRUE(media_channel1_->HasSendStream(2)); + + // Send ACCEPT + sdesc = CreateSessionDescriptionWithStream(3); + EXPECT_TRUE(session1_.set_local_description(sdesc)); + + session1_.SetState(cricket::Session::STATE_SENTACCEPT); + EXPECT_EQ(cricket::BaseSession::ERROR_NONE, session1_.error()); + EXPECT_TRUE(media_channel1_->HasRecvStream(1)); + EXPECT_FALSE(media_channel1_->HasSendStream(2)); + EXPECT_TRUE(media_channel1_->HasSendStream(3)); + } + + void TestReceivePrAnswer() { + CreateChannels(0, 0); + + // Set up the initial session description. + cricket::SessionDescription* sdesc = CreateSessionDescriptionWithStream(1); + EXPECT_TRUE(session1_.set_local_description(sdesc)); + + session1_.SetError(cricket::BaseSession::ERROR_NONE); + session1_.SetState(cricket::Session::STATE_SENTINITIATE); + EXPECT_EQ(cricket::BaseSession::ERROR_NONE, session1_.error()); + EXPECT_TRUE(media_channel1_->HasSendStream(1)); + + // Receive PRANSWER + sdesc = CreateSessionDescriptionWithStream(2); + EXPECT_TRUE(session1_.set_remote_description(sdesc)); + + session1_.SetState(cricket::Session::STATE_RECEIVEDPRACCEPT); + EXPECT_EQ(cricket::BaseSession::ERROR_NONE, session1_.error()); + EXPECT_TRUE(media_channel1_->HasSendStream(1)); + EXPECT_TRUE(media_channel1_->HasRecvStream(2)); + + // Receive ACCEPT + sdesc = CreateSessionDescriptionWithStream(3); + EXPECT_TRUE(session1_.set_remote_description(sdesc)); + + session1_.SetState(cricket::Session::STATE_RECEIVEDACCEPT); + EXPECT_EQ(cricket::BaseSession::ERROR_NONE, session1_.error()); + EXPECT_TRUE(media_channel1_->HasSendStream(1)); + EXPECT_FALSE(media_channel1_->HasRecvStream(2)); + EXPECT_TRUE(media_channel1_->HasRecvStream(3)); + } + + void TestFlushRtcp() { + bool send_rtcp1; + + CreateChannels(RTCP, RTCP); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(2U, GetTransport1()->channels().size()); + EXPECT_EQ(2U, GetTransport2()->channels().size()); + + // Send RTCP1 from a different thread. + CallOnThreadAndWaitForDone(&ChannelTest::SendRtcp1, &send_rtcp1); + EXPECT_TRUE(send_rtcp1); + // The sending message is only posted. channel2_ should be empty. + EXPECT_TRUE(CheckNoRtcp2()); + + // When channel1_ is deleted, the RTCP packet should be sent out to + // channel2_. + channel1_.reset(); + EXPECT_TRUE(CheckRtcp2()); + } + + void TestChangeStateError() { + CreateChannels(RTCP, RTCP); + EXPECT_TRUE(SendInitiate()); + media_channel2_->set_fail_set_send(true); + EXPECT_TRUE(channel2_->Enable(true)); + EXPECT_EQ(cricket::VoiceMediaChannel::ERROR_REC_DEVICE_OPEN_FAILED, + error_); + } + + void TestSrtpError() { + static const unsigned char kBadPacket[] = { + 0x90, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 + }; + CreateChannels(RTCP | SECURE, RTCP | SECURE); + EXPECT_FALSE(channel1_->secure()); + EXPECT_FALSE(channel2_->secure()); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_TRUE(channel1_->secure()); + EXPECT_TRUE(channel2_->secure()); + channel2_->set_srtp_signal_silent_time(200); + + // Testing failures in sending packets. + EXPECT_FALSE(media_channel2_->SendRtp(kBadPacket, sizeof(kBadPacket))); + // The first failure will trigger an error. + EXPECT_EQ_WAIT(T::MediaChannel::ERROR_REC_SRTP_ERROR, error_, 500); + error_ = T::MediaChannel::ERROR_NONE; + // The next 1 sec failures will not trigger an error. + EXPECT_FALSE(media_channel2_->SendRtp(kBadPacket, sizeof(kBadPacket))); + // Wait for a while to ensure no message comes in. + talk_base::Thread::Current()->ProcessMessages(210); + EXPECT_EQ(T::MediaChannel::ERROR_NONE, error_); + // The error will be triggered again. + EXPECT_FALSE(media_channel2_->SendRtp(kBadPacket, sizeof(kBadPacket))); + EXPECT_EQ_WAIT(T::MediaChannel::ERROR_REC_SRTP_ERROR, error_, 500); + + // Testing failures in receiving packets. + error_ = T::MediaChannel::ERROR_NONE; + cricket::TransportChannel* transport_channel = + channel2_->transport_channel(); + transport_channel->SignalReadPacket( + transport_channel, reinterpret_cast(kBadPacket), + sizeof(kBadPacket), 0); + EXPECT_EQ_WAIT(T::MediaChannel::ERROR_PLAY_SRTP_AUTH_FAILED, error_, 500); + } + + void TestOnReadyToSend() { + CreateChannels(RTCP, RTCP); + TransportChannel* rtp = channel1_->transport_channel(); + TransportChannel* rtcp = channel1_->rtcp_transport_channel(); + EXPECT_FALSE(media_channel1_->ready_to_send()); + rtp->SignalReadyToSend(rtp); + EXPECT_FALSE(media_channel1_->ready_to_send()); + rtcp->SignalReadyToSend(rtcp); + // MediaChannel::OnReadyToSend only be called when both rtp and rtcp + // channel are ready to send. + EXPECT_TRUE(media_channel1_->ready_to_send()); + + // rtp channel becomes not ready to send will be propagated to mediachannel + channel1_->SetReadyToSend(rtp, false); + EXPECT_FALSE(media_channel1_->ready_to_send()); + channel1_->SetReadyToSend(rtp, true); + EXPECT_TRUE(media_channel1_->ready_to_send()); + + // rtcp channel becomes not ready to send will be propagated to mediachannel + channel1_->SetReadyToSend(rtcp, false); + EXPECT_FALSE(media_channel1_->ready_to_send()); + channel1_->SetReadyToSend(rtcp, true); + EXPECT_TRUE(media_channel1_->ready_to_send()); + } + + void TestOnReadyToSendWithRtcpMux() { + CreateChannels(RTCP, RTCP); + typename T::Content content; + CreateContent(0, kPcmuCodec, kH264Codec, &content); + // Both sides agree on mux. Should no longer be a separate RTCP channel. + content.set_rtcp_mux(true); + EXPECT_TRUE(channel1_->SetLocalContent(&content, CA_OFFER)); + EXPECT_TRUE(channel1_->SetRemoteContent(&content, CA_ANSWER)); + EXPECT_TRUE(channel1_->rtcp_transport_channel() == NULL); + TransportChannel* rtp = channel1_->transport_channel(); + EXPECT_FALSE(media_channel1_->ready_to_send()); + // In the case of rtcp mux, the SignalReadyToSend() from rtp channel + // should trigger the MediaChannel's OnReadyToSend. + rtp->SignalReadyToSend(rtp); + EXPECT_TRUE(media_channel1_->ready_to_send()); + channel1_->SetReadyToSend(rtp, false); + EXPECT_FALSE(media_channel1_->ready_to_send()); + } + + protected: + cricket::FakeSession session1_; + cricket::FakeSession session2_; + cricket::FakeMediaEngine media_engine_; + // The media channels are owned by the voice channel objects below. + typename T::MediaChannel* media_channel1_; + typename T::MediaChannel* media_channel2_; + talk_base::scoped_ptr channel1_; + talk_base::scoped_ptr channel2_; + typename T::Content local_media_content1_; + typename T::Content local_media_content2_; + typename T::Content remote_media_content1_; + typename T::Content remote_media_content2_; + talk_base::scoped_ptr identity1_; + talk_base::scoped_ptr identity2_; + // The RTP and RTCP packets to send in the tests. + std::string rtp_packet_; + std::string rtcp_packet_; + int media_info_callbacks1_; + int media_info_callbacks2_; + bool mute_callback_recved_; + bool mute_callback_value_; + + uint32 ssrc_; + typename T::MediaChannel::Error error_; +}; + + +template<> +void ChannelTest::CreateContent( + int flags, + const cricket::AudioCodec& audio_codec, + const cricket::VideoCodec& video_codec, + cricket::AudioContentDescription* audio) { + audio->AddCodec(audio_codec); + audio->set_rtcp_mux((flags & RTCP_MUX) != 0); + if (flags & SECURE) { + audio->AddCrypto(cricket::CryptoParams( + 1, cricket::CS_AES_CM_128_HMAC_SHA1_32, + "inline:" + talk_base::CreateRandomString(40), "")); + } +} + +template<> +void ChannelTest::CopyContent( + const cricket::AudioContentDescription& source, + cricket::AudioContentDescription* audio) { + *audio = source; +} + +template<> +bool ChannelTest::CodecMatches(const cricket::AudioCodec& c1, + const cricket::AudioCodec& c2) { + return c1.name == c2.name && c1.clockrate == c2.clockrate && + c1.bitrate == c2.bitrate && c1.channels == c2.channels; +} + +template<> +void ChannelTest::AddLegacyStreamInContent( + uint32 ssrc, int flags, cricket::AudioContentDescription* audio) { + audio->AddLegacyStream(ssrc); +} + +class VoiceChannelTest + : public ChannelTest { + public: + typedef ChannelTest + Base; + VoiceChannelTest() : Base(kPcmuFrame, sizeof(kPcmuFrame), + kRtcpReport, sizeof(kRtcpReport)) { + } + + void TestSetChannelOptions() { + CreateChannels(0, 0); + + cricket::AudioOptions options1; + options1.echo_cancellation.Set(false); + cricket::AudioOptions options2; + options2.echo_cancellation.Set(true); + + channel1_->SetChannelOptions(options1); + channel2_->SetChannelOptions(options1); + cricket::AudioOptions actual_options; + ASSERT_TRUE(media_channel1_->GetOptions(&actual_options)); + EXPECT_EQ(options1, actual_options); + ASSERT_TRUE(media_channel2_->GetOptions(&actual_options)); + EXPECT_EQ(options1, actual_options); + + channel1_->SetChannelOptions(options2); + channel2_->SetChannelOptions(options2); + ASSERT_TRUE(media_channel1_->GetOptions(&actual_options)); + EXPECT_EQ(options2, actual_options); + ASSERT_TRUE(media_channel2_->GetOptions(&actual_options)); + EXPECT_EQ(options2, actual_options); + } +}; + +// override to add NULL parameter +template<> +cricket::VideoChannel* ChannelTest::CreateChannel( + talk_base::Thread* thread, cricket::MediaEngineInterface* engine, + cricket::FakeVideoMediaChannel* ch, cricket::BaseSession* session, + bool rtcp) { + cricket::VideoChannel* channel = new cricket::VideoChannel( + thread, engine, ch, session, cricket::CN_VIDEO, rtcp, NULL); + if (!channel->Init()) { + delete channel; + channel = NULL; + } + return channel; +} + +// override to add 0 parameter +template<> +bool ChannelTest::AddStream1(int id) { + return channel1_->AddRecvStream(cricket::StreamParams::CreateLegacy(id)); +} + +template<> +void ChannelTest::CreateContent( + int flags, + const cricket::AudioCodec& audio_codec, + const cricket::VideoCodec& video_codec, + cricket::VideoContentDescription* video) { + video->AddCodec(video_codec); + video->set_rtcp_mux((flags & RTCP_MUX) != 0); + if (flags & SECURE) { + video->AddCrypto(cricket::CryptoParams( + 1, cricket::CS_AES_CM_128_HMAC_SHA1_80, + "inline:" + talk_base::CreateRandomString(40), "")); + } +} + +template<> +void ChannelTest::CopyContent( + const cricket::VideoContentDescription& source, + cricket::VideoContentDescription* video) { + *video = source; +} + +template<> +bool ChannelTest::CodecMatches(const cricket::VideoCodec& c1, + const cricket::VideoCodec& c2) { + return c1.name == c2.name && c1.width == c2.width && c1.height == c2.height && + c1.framerate == c2.framerate; +} + +template<> +void ChannelTest::AddLegacyStreamInContent( + uint32 ssrc, int flags, cricket::VideoContentDescription* video) { + video->AddLegacyStream(ssrc); +} + +class VideoChannelTest + : public ChannelTest { + public: + typedef ChannelTest + Base; + VideoChannelTest() : Base(kH264Packet, sizeof(kH264Packet), + kRtcpReport, sizeof(kRtcpReport)) { + } + + void TestSetChannelOptions() { + CreateChannels(0, 0); + + cricket::VideoOptions o1, o2; + o1.video_noise_reduction.Set(true); + + channel1_->SetChannelOptions(o1); + channel2_->SetChannelOptions(o1); + EXPECT_TRUE(media_channel1_->GetOptions(&o2)); + EXPECT_EQ(o1, o2); + EXPECT_TRUE(media_channel2_->GetOptions(&o2)); + EXPECT_EQ(o1, o2); + + o1.video_leaky_bucket.Set(true); + channel1_->SetChannelOptions(o1); + channel2_->SetChannelOptions(o1); + EXPECT_TRUE(media_channel1_->GetOptions(&o2)); + EXPECT_EQ(o1, o2); + EXPECT_TRUE(media_channel2_->GetOptions(&o2)); + EXPECT_EQ(o1, o2); + } +}; + + +// VoiceChannelTest + +TEST_F(VoiceChannelTest, TestInit) { + Base::TestInit(); + EXPECT_FALSE(media_channel1_->IsStreamMuted(0)); + EXPECT_TRUE(media_channel1_->dtmf_info_queue().empty()); +} + +TEST_F(VoiceChannelTest, TestSetContents) { + Base::TestSetContents(); +} + +TEST_F(VoiceChannelTest, TestSetContentsNullOffer) { + Base::TestSetContentsNullOffer(); +} + +TEST_F(VoiceChannelTest, TestSetContentsRtcpMux) { + Base::TestSetContentsRtcpMux(); +} + +TEST_F(VoiceChannelTest, TestSetContentsRtcpMuxWithPrAnswer) { + Base::TestSetContentsRtcpMux(); +} + +TEST_F(VoiceChannelTest, TestSetRemoteContentUpdate) { + Base::TestSetRemoteContentUpdate(); +} + +TEST_F(VoiceChannelTest, TestStreams) { + Base::TestStreams(); +} + +TEST_F(VoiceChannelTest, TestUpdateStreamsInLocalContent) { + Base::TestUpdateStreamsInLocalContent(); +} + +TEST_F(VoiceChannelTest, TestUpdateRemoteStreamsInContent) { + Base::TestUpdateStreamsInRemoteContent(); +} + +TEST_F(VoiceChannelTest, TestChangeStreamParamsInContent) { + Base::TestChangeStreamParamsInContent(); +} + +TEST_F(VoiceChannelTest, TestPlayoutAndSendingStates) { + Base::TestPlayoutAndSendingStates(); +} + +TEST_F(VoiceChannelTest, TestMuteStream) { + Base::TestMuteStream(); +} + +TEST_F(VoiceChannelTest, TestMediaContentDirection) { + Base::TestMediaContentDirection(); +} + +TEST_F(VoiceChannelTest, TestCallSetup) { + Base::TestCallSetup(); +} + +TEST_F(VoiceChannelTest, TestCallTeardownRtcpMux) { + Base::TestCallTeardownRtcpMux(); +} + +TEST_F(VoiceChannelTest, SendRtpToRtp) { + Base::SendRtpToRtp(); +} + +TEST_F(VoiceChannelTest, SendNoRtcpToNoRtcp) { + Base::SendNoRtcpToNoRtcp(); +} + +TEST_F(VoiceChannelTest, SendNoRtcpToRtcp) { + Base::SendNoRtcpToRtcp(); +} + +TEST_F(VoiceChannelTest, SendRtcpToNoRtcp) { + Base::SendRtcpToNoRtcp(); +} + +TEST_F(VoiceChannelTest, SendRtcpToRtcp) { + Base::SendRtcpToRtcp(); +} + +TEST_F(VoiceChannelTest, SendRtcpMuxToRtcp) { + Base::SendRtcpMuxToRtcp(); +} + +TEST_F(VoiceChannelTest, SendRtcpMuxToRtcpMux) { + Base::SendRtcpMuxToRtcpMux(); +} + +TEST_F(VoiceChannelTest, SendEarlyRtcpMuxToRtcp) { + Base::SendEarlyRtcpMuxToRtcp(); +} + +TEST_F(VoiceChannelTest, SendEarlyRtcpMuxToRtcpMux) { + Base::SendEarlyRtcpMuxToRtcpMux(); +} + +TEST_F(VoiceChannelTest, SendSrtpToSrtpRtcpMux) { + Base::SendSrtpToSrtp(RTCP_MUX, RTCP_MUX); +} + +TEST_F(VoiceChannelTest, SendSrtpToRtp) { + Base::SendSrtpToSrtp(); +} + +TEST_F(VoiceChannelTest, SendSrtcpMux) { + Base::SendSrtpToSrtp(RTCP_MUX, RTCP_MUX); +} + +TEST_F(VoiceChannelTest, SendDtlsSrtpToSrtp) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + Base::SendSrtpToSrtp(DTLS, 0); +} + +TEST_F(VoiceChannelTest, SendDtlsSrtpToDtlsSrtp) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + Base::SendSrtpToSrtp(DTLS, DTLS); +} + +TEST_F(VoiceChannelTest, SendDtlsSrtpToDtlsSrtpRtcpMux) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + Base::SendSrtpToSrtp(DTLS | RTCP_MUX, DTLS | RTCP_MUX); +} + +TEST_F(VoiceChannelTest, SendEarlyMediaUsingRtcpMuxSrtp) { + Base::SendEarlyMediaUsingRtcpMuxSrtp(); +} + +TEST_F(VoiceChannelTest, SendRtpToRtpOnThread) { + Base::SendRtpToRtpOnThread(); +} + +TEST_F(VoiceChannelTest, SendSrtpToSrtpOnThread) { + Base::SendSrtpToSrtpOnThread(); +} + +TEST_F(VoiceChannelTest, SendWithWritabilityLoss) { + Base::SendWithWritabilityLoss(); +} + +TEST_F(VoiceChannelTest, TestMediaMonitor) { + Base::TestMediaMonitor(); +} + +// Test that MuteStream properly forwards to the media channel and does +// not signal. +TEST_F(VoiceChannelTest, TestVoiceSpecificMuteStream) { + CreateChannels(0, 0); + EXPECT_FALSE(media_channel1_->IsStreamMuted(0)); + EXPECT_FALSE(mute_callback_recved_); + EXPECT_TRUE(channel1_->MuteStream(0, true)); + EXPECT_TRUE(media_channel1_->IsStreamMuted(0)); + EXPECT_FALSE(mute_callback_recved_); + EXPECT_TRUE(channel1_->MuteStream(0, false)); + EXPECT_FALSE(media_channel1_->IsStreamMuted(0)); + EXPECT_FALSE(mute_callback_recved_); +} + +// Test that keyboard automute works correctly and signals upwards. +TEST_F(VoiceChannelTest, TestKeyboardMute) { + CreateChannels(0, 0); + EXPECT_FALSE(media_channel1_->IsStreamMuted(0)); + EXPECT_EQ(cricket::VoiceMediaChannel::ERROR_NONE, error_); + + cricket::VoiceMediaChannel::Error e = + cricket::VoiceMediaChannel::ERROR_REC_TYPING_NOISE_DETECTED; + + // Typing doesn't mute automatically unless typing monitor has been installed + media_channel1_->TriggerError(0, e); + talk_base::Thread::Current()->ProcessMessages(0); + EXPECT_EQ(e, error_); + EXPECT_FALSE(media_channel1_->IsStreamMuted(0)); + EXPECT_FALSE(mute_callback_recved_); + + cricket::TypingMonitorOptions o = {0}; + o.mute_period = 1500; + channel1_->StartTypingMonitor(o); + media_channel1_->TriggerError(0, e); + talk_base::Thread::Current()->ProcessMessages(0); + EXPECT_TRUE(media_channel1_->IsStreamMuted(0)); + EXPECT_TRUE(mute_callback_recved_); +} + +// Test that PressDTMF properly forwards to the media channel. +TEST_F(VoiceChannelTest, TestDtmf) { + CreateChannels(0, 0); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(0U, media_channel1_->dtmf_info_queue().size()); + + EXPECT_TRUE(channel1_->PressDTMF(1, true)); + EXPECT_TRUE(channel1_->PressDTMF(8, false)); + + ASSERT_EQ(2U, media_channel1_->dtmf_info_queue().size()); + EXPECT_TRUE(CompareDtmfInfo(media_channel1_->dtmf_info_queue()[0], + 0, 1, 160, cricket::DF_PLAY | cricket::DF_SEND)); + EXPECT_TRUE(CompareDtmfInfo(media_channel1_->dtmf_info_queue()[1], + 0, 8, 160, cricket::DF_SEND)); +} + +// Test that InsertDtmf properly forwards to the media channel. +TEST_F(VoiceChannelTest, TestInsertDtmf) { + CreateChannels(0, 0); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_EQ(0U, media_channel1_->dtmf_info_queue().size()); + + EXPECT_TRUE(channel1_->InsertDtmf(-1, kDtmfReset, -1, cricket::DF_SEND)); + EXPECT_TRUE(channel1_->InsertDtmf(0, kDtmfDelay, 90, cricket::DF_PLAY)); + EXPECT_TRUE(channel1_->InsertDtmf(1, 3, 100, cricket::DF_SEND)); + EXPECT_TRUE(channel1_->InsertDtmf(2, 5, 110, cricket::DF_PLAY)); + EXPECT_TRUE(channel1_->InsertDtmf(3, 7, 120, + cricket::DF_PLAY | cricket::DF_SEND)); + + ASSERT_EQ(5U, media_channel1_->dtmf_info_queue().size()); + EXPECT_TRUE(CompareDtmfInfo(media_channel1_->dtmf_info_queue()[0], + -1, kDtmfReset, -1, cricket::DF_SEND)); + EXPECT_TRUE(CompareDtmfInfo(media_channel1_->dtmf_info_queue()[1], + 0, kDtmfDelay, 90, cricket::DF_PLAY)); + EXPECT_TRUE(CompareDtmfInfo(media_channel1_->dtmf_info_queue()[2], + 1, 3, 100, cricket::DF_SEND)); + EXPECT_TRUE(CompareDtmfInfo(media_channel1_->dtmf_info_queue()[3], + 2, 5, 110, cricket::DF_PLAY)); + EXPECT_TRUE(CompareDtmfInfo(media_channel1_->dtmf_info_queue()[4], + 3, 7, 120, cricket::DF_PLAY | cricket::DF_SEND)); +} + +TEST_F(VoiceChannelTest, TestMediaSinks) { + Base::TestMediaSinks(); +} + +TEST_F(VoiceChannelTest, TestSetContentFailure) { + Base::TestSetContentFailure(); +} + +TEST_F(VoiceChannelTest, TestSendTwoOffers) { + Base::TestSendTwoOffers(); +} + +TEST_F(VoiceChannelTest, TestReceiveTwoOffers) { + Base::TestReceiveTwoOffers(); +} + +TEST_F(VoiceChannelTest, TestSendPrAnswer) { + Base::TestSendPrAnswer(); +} + +TEST_F(VoiceChannelTest, TestReceivePrAnswer) { + Base::TestReceivePrAnswer(); +} + +TEST_F(VoiceChannelTest, TestFlushRtcp) { + Base::TestFlushRtcp(); +} + +TEST_F(VoiceChannelTest, TestChangeStateError) { + Base::TestChangeStateError(); +} + +TEST_F(VoiceChannelTest, TestSrtpError) { + Base::TestSrtpError(); +} + +TEST_F(VoiceChannelTest, TestOnReadyToSend) { + Base::TestOnReadyToSend(); +} + +TEST_F(VoiceChannelTest, TestOnReadyToSendWithRtcpMux) { + Base::TestOnReadyToSendWithRtcpMux(); +} + +// Test that we can play a ringback tone properly. +TEST_F(VoiceChannelTest, TestRingbackTone) { + CreateChannels(RTCP, RTCP); + EXPECT_FALSE(media_channel1_->ringback_tone_play()); + EXPECT_TRUE(channel1_->SetRingbackTone("RIFF", 4)); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + // Play ringback tone, no loop. + EXPECT_TRUE(channel1_->PlayRingbackTone(0, true, false)); + EXPECT_EQ(0U, media_channel1_->ringback_tone_ssrc()); + EXPECT_TRUE(media_channel1_->ringback_tone_play()); + EXPECT_FALSE(media_channel1_->ringback_tone_loop()); + // Stop the ringback tone. + EXPECT_TRUE(channel1_->PlayRingbackTone(0, false, false)); + EXPECT_FALSE(media_channel1_->ringback_tone_play()); + // Add a stream. + EXPECT_TRUE(AddStream1(1)); + // Play ringback tone, looping, on the new stream. + EXPECT_TRUE(channel1_->PlayRingbackTone(1, true, true)); + EXPECT_EQ(1U, media_channel1_->ringback_tone_ssrc()); + EXPECT_TRUE(media_channel1_->ringback_tone_play()); + EXPECT_TRUE(media_channel1_->ringback_tone_loop()); + // Stop the ringback tone. + EXPECT_TRUE(channel1_->PlayRingbackTone(1, false, false)); + EXPECT_FALSE(media_channel1_->ringback_tone_play()); +} + +// Test that we can scale the output volume properly for 1:1 calls. +TEST_F(VoiceChannelTest, TestScaleVolume1to1Call) { + CreateChannels(RTCP, RTCP); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + double left, right; + + // Default is (1.0, 1.0). + EXPECT_TRUE(media_channel1_->GetOutputScaling(0, &left, &right)); + EXPECT_DOUBLE_EQ(1.0, left); + EXPECT_DOUBLE_EQ(1.0, right); + // invalid ssrc. + EXPECT_FALSE(media_channel1_->GetOutputScaling(3, &left, &right)); + + // Set scale to (1.5, 0.5). + EXPECT_TRUE(channel1_->SetOutputScaling(0, 1.5, 0.5)); + EXPECT_TRUE(media_channel1_->GetOutputScaling(0, &left, &right)); + EXPECT_DOUBLE_EQ(1.5, left); + EXPECT_DOUBLE_EQ(0.5, right); + + // Set scale to (0, 0). + EXPECT_TRUE(channel1_->SetOutputScaling(0, 0.0, 0.0)); + EXPECT_TRUE(media_channel1_->GetOutputScaling(0, &left, &right)); + EXPECT_DOUBLE_EQ(0.0, left); + EXPECT_DOUBLE_EQ(0.0, right); +} + +// Test that we can scale the output volume properly for multiway calls. +TEST_F(VoiceChannelTest, TestScaleVolumeMultiwayCall) { + CreateChannels(RTCP, RTCP); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + EXPECT_TRUE(AddStream1(1)); + EXPECT_TRUE(AddStream1(2)); + + double left, right; + // Default is (1.0, 1.0). + EXPECT_TRUE(media_channel1_->GetOutputScaling(0, &left, &right)); + EXPECT_DOUBLE_EQ(1.0, left); + EXPECT_DOUBLE_EQ(1.0, right); + EXPECT_TRUE(media_channel1_->GetOutputScaling(1, &left, &right)); + EXPECT_DOUBLE_EQ(1.0, left); + EXPECT_DOUBLE_EQ(1.0, right); + EXPECT_TRUE(media_channel1_->GetOutputScaling(2, &left, &right)); + EXPECT_DOUBLE_EQ(1.0, left); + EXPECT_DOUBLE_EQ(1.0, right); + // invalid ssrc. + EXPECT_FALSE(media_channel1_->GetOutputScaling(3, &left, &right)); + + // Set scale to (1.5, 0.5) for ssrc = 1. + EXPECT_TRUE(channel1_->SetOutputScaling(1, 1.5, 0.5)); + EXPECT_TRUE(media_channel1_->GetOutputScaling(1, &left, &right)); + EXPECT_DOUBLE_EQ(1.5, left); + EXPECT_DOUBLE_EQ(0.5, right); + EXPECT_TRUE(media_channel1_->GetOutputScaling(2, &left, &right)); + EXPECT_DOUBLE_EQ(1.0, left); + EXPECT_DOUBLE_EQ(1.0, right); + EXPECT_TRUE(media_channel1_->GetOutputScaling(0, &left, &right)); + EXPECT_DOUBLE_EQ(1.0, left); + EXPECT_DOUBLE_EQ(1.0, right); + + // Set scale to (0, 0) for all ssrcs. + EXPECT_TRUE(channel1_->SetOutputScaling(0, 0.0, 0.0)); + EXPECT_TRUE(media_channel1_->GetOutputScaling(0, &left, &right)); + EXPECT_DOUBLE_EQ(0.0, left); + EXPECT_DOUBLE_EQ(0.0, right); + EXPECT_TRUE(media_channel1_->GetOutputScaling(1, &left, &right)); + EXPECT_DOUBLE_EQ(0.0, left); + EXPECT_DOUBLE_EQ(0.0, right); + EXPECT_TRUE(media_channel1_->GetOutputScaling(2, &left, &right)); + EXPECT_DOUBLE_EQ(0.0, left); + EXPECT_DOUBLE_EQ(0.0, right); +} + +TEST_F(VoiceChannelTest, SendSsrcMuxToSsrcMux) { + Base::SendSsrcMuxToSsrcMux(); +} + +TEST_F(VoiceChannelTest, SendSsrcMuxToSsrcMuxWithRtcpMux) { + Base::SendSsrcMuxToSsrcMuxWithRtcpMux(); +} + +TEST_F(VoiceChannelTest, TestSetChannelOptions) { + TestSetChannelOptions(); +} + +// VideoChannelTest +TEST_F(VideoChannelTest, TestInit) { + Base::TestInit(); +} + +TEST_F(VideoChannelTest, TestSetContents) { + Base::TestSetContents(); +} + +TEST_F(VideoChannelTest, TestSetContentsNullOffer) { + Base::TestSetContentsNullOffer(); +} + +TEST_F(VideoChannelTest, TestSetContentsRtcpMux) { + Base::TestSetContentsRtcpMux(); +} + +TEST_F(VideoChannelTest, TestSetContentsRtcpMuxWithPrAnswer) { + Base::TestSetContentsRtcpMux(); +} + +TEST_F(VideoChannelTest, TestSetContentsVideoOptions) { + Base::TestSetContentsVideoOptions(); +} + +TEST_F(VideoChannelTest, TestSetRemoteContentUpdate) { + Base::TestSetRemoteContentUpdate(); +} + +TEST_F(VideoChannelTest, TestStreams) { + Base::TestStreams(); +} + +TEST_F(VideoChannelTest, TestScreencastEvents) { + const int kTimeoutMs = 500; + TestInit(); + FakeScreenCaptureFactory* screencapture_factory = + new FakeScreenCaptureFactory(); + channel1_->SetScreenCaptureFactory(screencapture_factory); + cricket::ScreencastEventCatcher catcher; + channel1_->SignalScreencastWindowEvent.connect( + &catcher, + &cricket::ScreencastEventCatcher::OnEvent); + EXPECT_TRUE(channel1_->AddScreencast(0, ScreencastId(WindowId(0))) != NULL); + ASSERT_TRUE(screencapture_factory->window_capturer() != NULL); + EXPECT_EQ_WAIT(cricket::CS_STOPPED, screencapture_factory->capture_state(), + kTimeoutMs); + screencapture_factory->window_capturer()->SignalStateChange( + screencapture_factory->window_capturer(), cricket::CS_PAUSED); + EXPECT_EQ_WAIT(talk_base::WE_MINIMIZE, catcher.event(), kTimeoutMs); + screencapture_factory->window_capturer()->SignalStateChange( + screencapture_factory->window_capturer(), cricket::CS_RUNNING); + EXPECT_EQ_WAIT(talk_base::WE_RESTORE, catcher.event(), kTimeoutMs); + screencapture_factory->window_capturer()->SignalStateChange( + screencapture_factory->window_capturer(), cricket::CS_STOPPED); + EXPECT_EQ_WAIT(talk_base::WE_CLOSE, catcher.event(), kTimeoutMs); + EXPECT_TRUE(channel1_->RemoveScreencast(0)); + ASSERT_TRUE(screencapture_factory->window_capturer() == NULL); +} + +TEST_F(VideoChannelTest, TestUpdateStreamsInLocalContent) { + Base::TestUpdateStreamsInLocalContent(); +} + +TEST_F(VideoChannelTest, TestUpdateRemoteStreamsInContent) { + Base::TestUpdateStreamsInRemoteContent(); +} + +TEST_F(VideoChannelTest, TestChangeStreamParamsInContent) { + Base::TestChangeStreamParamsInContent(); +} + +TEST_F(VideoChannelTest, TestPlayoutAndSendingStates) { + Base::TestPlayoutAndSendingStates(); +} + +TEST_F(VideoChannelTest, TestMuteStream) { + Base::TestMuteStream(); +} + +TEST_F(VideoChannelTest, TestMediaContentDirection) { + Base::TestMediaContentDirection(); +} + +TEST_F(VideoChannelTest, TestCallSetup) { + Base::TestCallSetup(); +} + +TEST_F(VideoChannelTest, TestCallTeardownRtcpMux) { + Base::TestCallTeardownRtcpMux(); +} + +TEST_F(VideoChannelTest, SendRtpToRtp) { + Base::SendRtpToRtp(); +} + +TEST_F(VideoChannelTest, SendNoRtcpToNoRtcp) { + Base::SendNoRtcpToNoRtcp(); +} + +TEST_F(VideoChannelTest, SendNoRtcpToRtcp) { + Base::SendNoRtcpToRtcp(); +} + +TEST_F(VideoChannelTest, SendRtcpToNoRtcp) { + Base::SendRtcpToNoRtcp(); +} + +TEST_F(VideoChannelTest, SendRtcpToRtcp) { + Base::SendRtcpToRtcp(); +} + +TEST_F(VideoChannelTest, SendRtcpMuxToRtcp) { + Base::SendRtcpMuxToRtcp(); +} + +TEST_F(VideoChannelTest, SendRtcpMuxToRtcpMux) { + Base::SendRtcpMuxToRtcpMux(); +} + +TEST_F(VideoChannelTest, SendEarlyRtcpMuxToRtcp) { + Base::SendEarlyRtcpMuxToRtcp(); +} + +TEST_F(VideoChannelTest, SendEarlyRtcpMuxToRtcpMux) { + Base::SendEarlyRtcpMuxToRtcpMux(); +} + +TEST_F(VideoChannelTest, SendSrtpToSrtp) { + Base::SendSrtpToSrtp(); +} + +TEST_F(VideoChannelTest, SendSrtpToRtp) { + Base::SendSrtpToSrtp(); +} + +TEST_F(VideoChannelTest, SendDtlsSrtpToSrtp) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + Base::SendSrtpToSrtp(DTLS, 0); +} + +TEST_F(VideoChannelTest, SendDtlsSrtpToDtlsSrtp) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + Base::SendSrtpToSrtp(DTLS, DTLS); +} + +TEST_F(VideoChannelTest, SendDtlsSrtpToDtlsSrtpRtcpMux) { + MAYBE_SKIP_TEST(HaveDtlsSrtp); + Base::SendSrtpToSrtp(DTLS | RTCP_MUX, DTLS | RTCP_MUX); +} + +TEST_F(VideoChannelTest, SendSrtcpMux) { + Base::SendSrtpToSrtp(RTCP_MUX, RTCP_MUX); +} + +TEST_F(VideoChannelTest, SendEarlyMediaUsingRtcpMuxSrtp) { + Base::SendEarlyMediaUsingRtcpMuxSrtp(); +} + +TEST_F(VideoChannelTest, SendRtpToRtpOnThread) { + Base::SendRtpToRtpOnThread(); +} + +TEST_F(VideoChannelTest, SendSrtpToSrtpOnThread) { + Base::SendSrtpToSrtpOnThread(); +} + +TEST_F(VideoChannelTest, SendWithWritabilityLoss) { + Base::SendWithWritabilityLoss(); +} + +TEST_F(VideoChannelTest, TestMediaMonitor) { + Base::TestMediaMonitor(); +} + +TEST_F(VideoChannelTest, TestMediaSinks) { + Base::TestMediaSinks(); +} + +TEST_F(VideoChannelTest, TestSetContentFailure) { + Base::TestSetContentFailure(); +} + +TEST_F(VideoChannelTest, TestSendTwoOffers) { + Base::TestSendTwoOffers(); +} + +TEST_F(VideoChannelTest, TestReceiveTwoOffers) { + Base::TestReceiveTwoOffers(); +} + +TEST_F(VideoChannelTest, TestSendPrAnswer) { + Base::TestSendPrAnswer(); +} + +TEST_F(VideoChannelTest, TestReceivePrAnswer) { + Base::TestReceivePrAnswer(); +} + +TEST_F(VideoChannelTest, TestFlushRtcp) { + Base::TestFlushRtcp(); +} + +TEST_F(VideoChannelTest, SendSsrcMuxToSsrcMux) { + Base::SendSsrcMuxToSsrcMux(); +} + +TEST_F(VideoChannelTest, SendSsrcMuxToSsrcMuxWithRtcpMux) { + Base::SendSsrcMuxToSsrcMuxWithRtcpMux(); +} + +// TODO(gangji): Add VideoChannelTest.TestChangeStateError. + +TEST_F(VideoChannelTest, TestSrtpError) { + Base::TestSrtpError(); +} + +TEST_F(VideoChannelTest, TestOnReadyToSend) { + Base::TestOnReadyToSend(); +} + +TEST_F(VideoChannelTest, TestOnReadyToSendWithRtcpMux) { + Base::TestOnReadyToSendWithRtcpMux(); +} + +TEST_F(VideoChannelTest, TestApplyViewRequest) { + CreateChannels(0, 0); + cricket::StreamParams stream2; + stream2.id = "stream2"; + stream2.ssrcs.push_back(2222); + local_media_content1_.AddStream(stream2); + + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + + cricket::VideoFormat send_format; + EXPECT_TRUE(media_channel1_->GetSendStreamFormat(kSsrc1, &send_format)); + EXPECT_EQ(640, send_format.width); + EXPECT_EQ(400, send_format.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), send_format.interval); + + cricket::ViewRequest request; + // stream1: 320x200x15; stream2: 0x0x0 + request.static_video_views.push_back(cricket::StaticVideoView( + cricket::StreamSelector(kSsrc1), 320, 200, 15)); + EXPECT_TRUE(channel1_->ApplyViewRequest(request)); + EXPECT_TRUE(media_channel1_->GetSendStreamFormat(kSsrc1, &send_format)); + EXPECT_EQ(320, send_format.width); + EXPECT_EQ(200, send_format.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(15), send_format.interval); + EXPECT_TRUE(media_channel1_->GetSendStreamFormat(2222, &send_format)); + EXPECT_EQ(0, send_format.width); + EXPECT_EQ(0, send_format.height); + + // stream1: 160x100x8; stream2: 0x0x0 + request.static_video_views.clear(); + request.static_video_views.push_back(cricket::StaticVideoView( + cricket::StreamSelector(kSsrc1), 160, 100, 8)); + EXPECT_TRUE(channel1_->ApplyViewRequest(request)); + EXPECT_TRUE(media_channel1_->GetSendStreamFormat(kSsrc1, &send_format)); + EXPECT_EQ(160, send_format.width); + EXPECT_EQ(100, send_format.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(8), send_format.interval); + + // stream1: 0x0x0; stream2: 640x400x30 + request.static_video_views.clear(); + request.static_video_views.push_back(cricket::StaticVideoView( + cricket::StreamSelector("", stream2.id), 640, 400, 30)); + EXPECT_TRUE(channel1_->ApplyViewRequest(request)); + EXPECT_TRUE(media_channel1_->GetSendStreamFormat(kSsrc1, &send_format)); + EXPECT_EQ(0, send_format.width); + EXPECT_EQ(0, send_format.height); + EXPECT_TRUE(media_channel1_->GetSendStreamFormat(2222, &send_format)); + EXPECT_EQ(640, send_format.width); + EXPECT_EQ(400, send_format.height); + EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), send_format.interval); + + // stream1: 0x0x0; stream2: 0x0x0 + request.static_video_views.clear(); + EXPECT_TRUE(channel1_->ApplyViewRequest(request)); + EXPECT_TRUE(media_channel1_->GetSendStreamFormat(kSsrc1, &send_format)); + EXPECT_EQ(0, send_format.width); + EXPECT_EQ(0, send_format.height); +} + +TEST_F(VideoChannelTest, TestSetChannelOptions) { + TestSetChannelOptions(); +} + + +// DataChannelTest + +class DataChannelTest + : public ChannelTest { + public: + typedef ChannelTest + Base; + DataChannelTest() : Base(kDataPacket, sizeof(kDataPacket), + kRtcpReport, sizeof(kRtcpReport)) { + } +}; + +// Override to avoid engine channel parameter. +template<> +cricket::DataChannel* ChannelTest::CreateChannel( + talk_base::Thread* thread, cricket::MediaEngineInterface* engine, + cricket::FakeDataMediaChannel* ch, cricket::BaseSession* session, + bool rtcp) { + cricket::DataChannel* channel = new cricket::DataChannel( + thread, ch, session, cricket::CN_DATA, rtcp); + if (!channel->Init()) { + delete channel; + channel = NULL; + } + return channel; +} + +template<> +void ChannelTest::CreateContent( + int flags, + const cricket::AudioCodec& audio_codec, + const cricket::VideoCodec& video_codec, + cricket::DataContentDescription* data) { + data->AddCodec(kGoogleDataCodec); + data->set_rtcp_mux((flags & RTCP_MUX) != 0); + if (flags & SECURE) { + data->AddCrypto(cricket::CryptoParams( + 1, cricket::CS_AES_CM_128_HMAC_SHA1_32, + "inline:" + talk_base::CreateRandomString(40), "")); + } +} + +template<> +void ChannelTest::CopyContent( + const cricket::DataContentDescription& source, + cricket::DataContentDescription* data) { + *data = source; +} + +template<> +bool ChannelTest::CodecMatches(const cricket::DataCodec& c1, + const cricket::DataCodec& c2) { + return c1.name == c2.name; +} + +template<> +void ChannelTest::AddLegacyStreamInContent( + uint32 ssrc, int flags, cricket::DataContentDescription* data) { + data->AddLegacyStream(ssrc); +} + +TEST_F(DataChannelTest, TestInit) { + Base::TestInit(); + EXPECT_FALSE(media_channel1_->IsStreamMuted(0)); +} + +TEST_F(DataChannelTest, TestSetContents) { + Base::TestSetContents(); +} + +TEST_F(DataChannelTest, TestSetContentsNullOffer) { + Base::TestSetContentsNullOffer(); +} + +TEST_F(DataChannelTest, TestSetContentsRtcpMux) { + Base::TestSetContentsRtcpMux(); +} + +TEST_F(DataChannelTest, TestSetRemoteContentUpdate) { + Base::TestSetRemoteContentUpdate(); +} + +TEST_F(DataChannelTest, TestStreams) { + Base::TestStreams(); +} + +TEST_F(DataChannelTest, TestUpdateStreamsInLocalContent) { + Base::TestUpdateStreamsInLocalContent(); +} + +TEST_F(DataChannelTest, TestUpdateRemoteStreamsInContent) { + Base::TestUpdateStreamsInRemoteContent(); +} + +TEST_F(DataChannelTest, TestChangeStreamParamsInContent) { + Base::TestChangeStreamParamsInContent(); +} + +TEST_F(DataChannelTest, TestPlayoutAndSendingStates) { + Base::TestPlayoutAndSendingStates(); +} + +TEST_F(DataChannelTest, TestMediaContentDirection) { + Base::TestMediaContentDirection(); +} + +TEST_F(DataChannelTest, TestCallSetup) { + Base::TestCallSetup(); +} + +TEST_F(DataChannelTest, TestCallTeardownRtcpMux) { + Base::TestCallTeardownRtcpMux(); +} + +TEST_F(DataChannelTest, TestOnReadyToSend) { + Base::TestOnReadyToSend(); +} + +TEST_F(DataChannelTest, TestOnReadyToSendWithRtcpMux) { + Base::TestOnReadyToSendWithRtcpMux(); +} + +TEST_F(DataChannelTest, SendRtpToRtp) { + Base::SendRtpToRtp(); +} + +TEST_F(DataChannelTest, SendNoRtcpToNoRtcp) { + Base::SendNoRtcpToNoRtcp(); +} + +TEST_F(DataChannelTest, SendNoRtcpToRtcp) { + Base::SendNoRtcpToRtcp(); +} + +TEST_F(DataChannelTest, SendRtcpToNoRtcp) { + Base::SendRtcpToNoRtcp(); +} + +TEST_F(DataChannelTest, SendRtcpToRtcp) { + Base::SendRtcpToRtcp(); +} + +TEST_F(DataChannelTest, SendRtcpMuxToRtcp) { + Base::SendRtcpMuxToRtcp(); +} + +TEST_F(DataChannelTest, SendRtcpMuxToRtcpMux) { + Base::SendRtcpMuxToRtcpMux(); +} + +TEST_F(DataChannelTest, SendEarlyRtcpMuxToRtcp) { + Base::SendEarlyRtcpMuxToRtcp(); +} + +TEST_F(DataChannelTest, SendEarlyRtcpMuxToRtcpMux) { + Base::SendEarlyRtcpMuxToRtcpMux(); +} + +TEST_F(DataChannelTest, SendSrtpToSrtp) { + Base::SendSrtpToSrtp(); +} + +TEST_F(DataChannelTest, SendSrtpToRtp) { + Base::SendSrtpToSrtp(); +} + +TEST_F(DataChannelTest, SendSrtcpMux) { + Base::SendSrtpToSrtp(RTCP_MUX, RTCP_MUX); +} + +TEST_F(DataChannelTest, SendRtpToRtpOnThread) { + Base::SendRtpToRtpOnThread(); +} + +TEST_F(DataChannelTest, SendSrtpToSrtpOnThread) { + Base::SendSrtpToSrtpOnThread(); +} + +TEST_F(DataChannelTest, SendWithWritabilityLoss) { + Base::SendWithWritabilityLoss(); +} + +TEST_F(DataChannelTest, TestMediaMonitor) { + Base::TestMediaMonitor(); +} + +TEST_F(DataChannelTest, TestSendData) { + CreateChannels(0, 0); + EXPECT_TRUE(SendInitiate()); + EXPECT_TRUE(SendAccept()); + + cricket::SendDataParams params; + params.ssrc = 42; + unsigned char data[] = { + 'f', 'o', 'o' + }; + talk_base::Buffer payload(data, 3); + cricket::SendDataResult result; + ASSERT_TRUE(media_channel1_->SendData(params, payload, &result)); + EXPECT_EQ(params.ssrc, + media_channel1_->last_sent_data_params().ssrc); + EXPECT_EQ("foo", media_channel1_->last_sent_data()); +} + +// TODO(pthatcher): TestSetReceiver? diff --git a/talk/session/media/channelmanager.cc b/talk/session/media/channelmanager.cc new file mode 100644 index 000000000..529ceea21 --- /dev/null +++ b/talk/session/media/channelmanager.cc @@ -0,0 +1,931 @@ +/* + * libjingle + * Copyright 2004 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/media/channelmanager.h" + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "talk/base/bind.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/sigslotrepeater.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" +#include "talk/media/base/capturemanager.h" +#include "talk/media/base/hybriddataengine.h" +#include "talk/media/base/rtpdataengine.h" +#include "talk/media/base/videocapturer.h" +#ifdef HAVE_SCTP +#include "talk/media/sctp/sctpdataengine.h" +#endif +#include "talk/session/media/soundclip.h" + +namespace cricket { + +enum { + MSG_VIDEOCAPTURESTATE = 1, +}; + +using talk_base::Bind; + +static const int kNotSetOutputVolume = -1; + +struct CaptureStateParams : public talk_base::MessageData { + CaptureStateParams(cricket::VideoCapturer* c, cricket::CaptureState s) + : capturer(c), + state(s) {} + cricket::VideoCapturer* capturer; + cricket::CaptureState state; +}; + +static DataEngineInterface* ConstructDataEngine() { +#ifdef HAVE_SCTP + return new HybridDataEngine(new RtpDataEngine(), new SctpDataEngine()); +#else + return new RtpDataEngine(); +#endif +} + +#if !defined(DISABLE_MEDIA_ENGINE_FACTORY) +ChannelManager::ChannelManager(talk_base::Thread* worker_thread) { + Construct(MediaEngineFactory::Create(), + ConstructDataEngine(), + cricket::DeviceManagerFactory::Create(), + new CaptureManager(), + worker_thread); +} +#endif + +ChannelManager::ChannelManager(MediaEngineInterface* me, + DataEngineInterface* dme, + DeviceManagerInterface* dm, + CaptureManager* cm, + talk_base::Thread* worker_thread) { + Construct(me, dme, dm, cm, worker_thread); +} + +ChannelManager::ChannelManager(MediaEngineInterface* me, + DeviceManagerInterface* dm, + talk_base::Thread* worker_thread) { + Construct(me, + ConstructDataEngine(), + dm, + new CaptureManager(), + worker_thread); +} + +void ChannelManager::Construct(MediaEngineInterface* me, + DataEngineInterface* dme, + DeviceManagerInterface* dm, + CaptureManager* cm, + talk_base::Thread* worker_thread) { + media_engine_.reset(me); + data_media_engine_.reset(dme); + device_manager_.reset(dm); + capture_manager_.reset(cm); + initialized_ = false; + main_thread_ = talk_base::Thread::Current(); + worker_thread_ = worker_thread; + audio_in_device_ = DeviceManagerInterface::kDefaultDeviceName; + audio_out_device_ = DeviceManagerInterface::kDefaultDeviceName; + audio_options_ = MediaEngineInterface::DEFAULT_AUDIO_OPTIONS; + audio_delay_offset_ = MediaEngineInterface::kDefaultAudioDelayOffset; + audio_output_volume_ = kNotSetOutputVolume; + local_renderer_ = NULL; + capturing_ = false; + monitoring_ = false; + enable_rtx_ = false; + + // Init the device manager immediately, and set up our default video device. + SignalDevicesChange.repeat(device_manager_->SignalDevicesChange); + device_manager_->Init(); + + // Camera is started asynchronously, request callbacks when startup + // completes to be able to forward them to the rendering manager. + media_engine_->SignalVideoCaptureStateChange().connect( + this, &ChannelManager::OnVideoCaptureStateChange); + capture_manager_->SignalCapturerStateChange.connect( + this, &ChannelManager::OnVideoCaptureStateChange); +} + +ChannelManager::~ChannelManager() { + if (initialized_) + Terminate(); +} + +bool ChannelManager::SetVideoRtxEnabled(bool enable) { + // To be safe, this call is only allowed before initialization. Apps like + // Flute only have a singleton ChannelManager and we don't want this flag to + // be toggled between calls or when there's concurrent calls. We expect apps + // to enable this at startup and retain that setting for the lifetime of the + // app. + if (!initialized_) { + enable_rtx_ = enable; + return true; + } else { + LOG(LS_WARNING) << "Cannot toggle rtx after initialization!"; + return false; + } +} + +int ChannelManager::GetCapabilities() { + return media_engine_->GetCapabilities() & device_manager_->GetCapabilities(); +} + +void ChannelManager::GetSupportedAudioCodecs( + std::vector* codecs) const { + codecs->clear(); + + for (std::vector::const_iterator it = + media_engine_->audio_codecs().begin(); + it != media_engine_->audio_codecs().end(); ++it) { + codecs->push_back(*it); + } +} + +void ChannelManager::GetSupportedAudioRtpHeaderExtensions( + RtpHeaderExtensions* ext) const { + *ext = media_engine_->audio_rtp_header_extensions(); +} + +void ChannelManager::GetSupportedVideoCodecs( + std::vector* codecs) const { + codecs->clear(); + + std::vector::const_iterator it; + for (it = media_engine_->video_codecs().begin(); + it != media_engine_->video_codecs().end(); ++it) { + if (!enable_rtx_ && _stricmp(kRtxCodecName, it->name.c_str()) == 0) { + continue; + } + codecs->push_back(*it); + } +} + +void ChannelManager::GetSupportedVideoRtpHeaderExtensions( + RtpHeaderExtensions* ext) const { + *ext = media_engine_->video_rtp_header_extensions(); +} + +void ChannelManager::GetSupportedDataCodecs( + std::vector* codecs) const { + *codecs = data_media_engine_->data_codecs(); +} + +bool ChannelManager::Init() { + ASSERT(!initialized_); + if (initialized_) { + return false; + } + + ASSERT(worker_thread_ != NULL); + if (worker_thread_ && worker_thread_->started()) { + if (media_engine_->Init(worker_thread_)) { + initialized_ = true; + + // Now that we're initialized, apply any stored preferences. A preferred + // device might have been unplugged. In this case, we fallback to the + // default device but keep the user preferences. The preferences are + // changed only when the Javascript FE changes them. + const std::string preferred_audio_in_device = audio_in_device_; + const std::string preferred_audio_out_device = audio_out_device_; + const std::string preferred_camera_device = camera_device_; + Device device; + if (!device_manager_->GetAudioInputDevice(audio_in_device_, &device)) { + LOG(LS_WARNING) << "The preferred microphone '" << audio_in_device_ + << "' is unavailable. Fall back to the default."; + audio_in_device_ = DeviceManagerInterface::kDefaultDeviceName; + } + if (!device_manager_->GetAudioOutputDevice(audio_out_device_, &device)) { + LOG(LS_WARNING) << "The preferred speaker '" << audio_out_device_ + << "' is unavailable. Fall back to the default."; + audio_out_device_ = DeviceManagerInterface::kDefaultDeviceName; + } + if (!device_manager_->GetVideoCaptureDevice(camera_device_, &device)) { + if (!camera_device_.empty()) { + LOG(LS_WARNING) << "The preferred camera '" << camera_device_ + << "' is unavailable. Fall back to the default."; + } + camera_device_ = DeviceManagerInterface::kDefaultDeviceName; + } + + if (!SetAudioOptions(audio_in_device_, audio_out_device_, + audio_options_, audio_delay_offset_)) { + LOG(LS_WARNING) << "Failed to SetAudioOptions with" + << " microphone: " << audio_in_device_ + << " speaker: " << audio_out_device_ + << " options: " << audio_options_ + << " delay: " << audio_delay_offset_; + } + + // If audio_output_volume_ has been set via SetOutputVolume(), set the + // audio output volume of the engine. + if (kNotSetOutputVolume != audio_output_volume_ && + !SetOutputVolume(audio_output_volume_)) { + LOG(LS_WARNING) << "Failed to SetOutputVolume to " + << audio_output_volume_; + } + if (!SetCaptureDevice(camera_device_) && !camera_device_.empty()) { + LOG(LS_WARNING) << "Failed to SetCaptureDevice with camera: " + << camera_device_; + } + + // Restore the user preferences. + audio_in_device_ = preferred_audio_in_device; + audio_out_device_ = preferred_audio_out_device; + camera_device_ = preferred_camera_device; + + // Now apply the default video codec that has been set earlier. + if (default_video_encoder_config_.max_codec.id != 0) { + SetDefaultVideoEncoderConfig(default_video_encoder_config_); + } + // And the local renderer. + if (local_renderer_) { + SetLocalRenderer(local_renderer_); + } + } + } + return initialized_; +} + +void ChannelManager::Terminate() { + ASSERT(initialized_); + if (!initialized_) { + return; + } + worker_thread_->Invoke(Bind(&ChannelManager::Terminate_w, this)); + media_engine_->Terminate(); + initialized_ = false; +} + +void ChannelManager::Terminate_w() { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + // Need to destroy the voice/video channels + while (!video_channels_.empty()) { + DestroyVideoChannel_w(video_channels_.back()); + } + while (!voice_channels_.empty()) { + DestroyVoiceChannel_w(voice_channels_.back()); + } + while (!soundclips_.empty()) { + DestroySoundclip_w(soundclips_.back()); + } + if (!SetCaptureDevice_w(NULL)) { + LOG(LS_WARNING) << "failed to delete video capturer"; + } +} + +VoiceChannel* ChannelManager::CreateVoiceChannel( + BaseSession* session, const std::string& content_name, bool rtcp) { + return worker_thread_->Invoke( + Bind(&ChannelManager::CreateVoiceChannel_w, this, + session, content_name, rtcp)); +} + +VoiceChannel* ChannelManager::CreateVoiceChannel_w( + BaseSession* session, const std::string& content_name, bool rtcp) { + // This is ok to alloc from a thread other than the worker thread + ASSERT(initialized_); + VoiceMediaChannel* media_channel = media_engine_->CreateChannel(); + if (media_channel == NULL) + return NULL; + + VoiceChannel* voice_channel = new VoiceChannel( + worker_thread_, media_engine_.get(), media_channel, + session, content_name, rtcp); + if (!voice_channel->Init()) { + delete voice_channel; + return NULL; + } + voice_channels_.push_back(voice_channel); + return voice_channel; +} + +void ChannelManager::DestroyVoiceChannel(VoiceChannel* voice_channel) { + if (voice_channel) { + worker_thread_->Invoke( + Bind(&ChannelManager::DestroyVoiceChannel_w, this, voice_channel)); + } +} + +void ChannelManager::DestroyVoiceChannel_w(VoiceChannel* voice_channel) { + // Destroy voice channel. + ASSERT(initialized_); + VoiceChannels::iterator it = std::find(voice_channels_.begin(), + voice_channels_.end(), voice_channel); + ASSERT(it != voice_channels_.end()); + if (it == voice_channels_.end()) + return; + + voice_channels_.erase(it); + delete voice_channel; +} + +VideoChannel* ChannelManager::CreateVideoChannel( + BaseSession* session, const std::string& content_name, bool rtcp, + VoiceChannel* voice_channel) { + return worker_thread_->Invoke( + Bind(&ChannelManager::CreateVideoChannel_w, this, session, + content_name, rtcp, voice_channel)); +} + +VideoChannel* ChannelManager::CreateVideoChannel_w( + BaseSession* session, const std::string& content_name, bool rtcp, + VoiceChannel* voice_channel) { + // This is ok to alloc from a thread other than the worker thread + ASSERT(initialized_); + VideoMediaChannel* media_channel = + // voice_channel can be NULL in case of NullVoiceEngine. + media_engine_->CreateVideoChannel(voice_channel ? + voice_channel->media_channel() : NULL); + if (media_channel == NULL) + return NULL; + + VideoChannel* video_channel = new VideoChannel( + worker_thread_, media_engine_.get(), media_channel, + session, content_name, rtcp, voice_channel); + if (!video_channel->Init()) { + delete video_channel; + return NULL; + } + video_channels_.push_back(video_channel); + return video_channel; +} + +void ChannelManager::DestroyVideoChannel(VideoChannel* video_channel) { + if (video_channel) { + worker_thread_->Invoke( + Bind(&ChannelManager::DestroyVideoChannel_w, this, video_channel)); + } +} + +void ChannelManager::DestroyVideoChannel_w(VideoChannel* video_channel) { + // Destroy video channel. + ASSERT(initialized_); + VideoChannels::iterator it = std::find(video_channels_.begin(), + video_channels_.end(), video_channel); + ASSERT(it != video_channels_.end()); + if (it == video_channels_.end()) + return; + + video_channels_.erase(it); + delete video_channel; +} + +DataChannel* ChannelManager::CreateDataChannel( + BaseSession* session, const std::string& content_name, + bool rtcp, DataChannelType channel_type) { + return worker_thread_->Invoke( + Bind(&ChannelManager::CreateDataChannel_w, this, session, content_name, + rtcp, channel_type)); +} + +DataChannel* ChannelManager::CreateDataChannel_w( + BaseSession* session, const std::string& content_name, + bool rtcp, DataChannelType data_channel_type) { + // This is ok to alloc from a thread other than the worker thread. + ASSERT(initialized_); + DataMediaChannel* media_channel = data_media_engine_->CreateChannel( + data_channel_type); + if (!media_channel) { + LOG(LS_WARNING) << "Failed to create data channel of type " + << data_channel_type; + return NULL; + } + + DataChannel* data_channel = new DataChannel( + worker_thread_, media_channel, + session, content_name, rtcp); + if (!data_channel->Init()) { + LOG(LS_WARNING) << "Failed to init data channel."; + delete data_channel; + return NULL; + } + data_channels_.push_back(data_channel); + return data_channel; +} + +void ChannelManager::DestroyDataChannel(DataChannel* data_channel) { + if (data_channel) { + worker_thread_->Invoke( + Bind(&ChannelManager::DestroyDataChannel_w, this, data_channel)); + } +} + +void ChannelManager::DestroyDataChannel_w(DataChannel* data_channel) { + // Destroy data channel. + ASSERT(initialized_); + DataChannels::iterator it = std::find(data_channels_.begin(), + data_channels_.end(), data_channel); + ASSERT(it != data_channels_.end()); + if (it == data_channels_.end()) + return; + + data_channels_.erase(it); + delete data_channel; +} + +Soundclip* ChannelManager::CreateSoundclip() { + return worker_thread_->Invoke( + Bind(&ChannelManager::CreateSoundclip_w, this)); +} + +Soundclip* ChannelManager::CreateSoundclip_w() { + ASSERT(initialized_); + ASSERT(worker_thread_ == talk_base::Thread::Current()); + + SoundclipMedia* soundclip_media = media_engine_->CreateSoundclip(); + if (!soundclip_media) { + return NULL; + } + + Soundclip* soundclip = new Soundclip(worker_thread_, soundclip_media); + soundclips_.push_back(soundclip); + return soundclip; +} + +void ChannelManager::DestroySoundclip(Soundclip* soundclip) { + if (soundclip) { + worker_thread_->Invoke( + Bind(&ChannelManager::DestroySoundclip_w, this, soundclip)); + } +} + +void ChannelManager::DestroySoundclip_w(Soundclip* soundclip) { + // Destroy soundclip. + ASSERT(initialized_); + Soundclips::iterator it = std::find(soundclips_.begin(), + soundclips_.end(), soundclip); + ASSERT(it != soundclips_.end()); + if (it == soundclips_.end()) + return; + + soundclips_.erase(it); + delete soundclip; +} + +bool ChannelManager::GetAudioOptions(std::string* in_name, + std::string* out_name, int* opts) { + if (in_name) + *in_name = audio_in_device_; + if (out_name) + *out_name = audio_out_device_; + if (opts) + *opts = audio_options_; + return true; +} + +bool ChannelManager::SetAudioOptions(const std::string& in_name, + const std::string& out_name, int opts) { + return SetAudioOptions(in_name, out_name, opts, audio_delay_offset_); +} + +bool ChannelManager::SetAudioOptions(const std::string& in_name, + const std::string& out_name, int opts, + int delay_offset) { + // Get device ids from DeviceManager. + Device in_dev, out_dev; + if (!device_manager_->GetAudioInputDevice(in_name, &in_dev)) { + LOG(LS_WARNING) << "Failed to GetAudioInputDevice: " << in_name; + return false; + } + if (!device_manager_->GetAudioOutputDevice(out_name, &out_dev)) { + LOG(LS_WARNING) << "Failed to GetAudioOutputDevice: " << out_name; + return false; + } + + // If we're initialized, pass the settings to the media engine. + bool ret = true; + if (initialized_) { + ret = worker_thread_->Invoke( + Bind(&ChannelManager::SetAudioOptions_w, this, + opts, delay_offset, &in_dev, &out_dev)); + } + + // If all worked well, save the values for use in GetAudioOptions. + if (ret) { + audio_options_ = opts; + audio_in_device_ = in_name; + audio_out_device_ = out_name; + audio_delay_offset_ = delay_offset; + } + return ret; +} + +bool ChannelManager::SetAudioOptions_w(int opts, int delay_offset, + const Device* in_dev, const Device* out_dev) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + ASSERT(initialized_); + + // Set audio options + bool ret = media_engine_->SetAudioOptions(opts); + + if (ret) { + ret = media_engine_->SetAudioDelayOffset(delay_offset); + } + + // Set the audio devices + if (ret) { + ret = media_engine_->SetSoundDevices(in_dev, out_dev); + } + + return ret; +} + +bool ChannelManager::GetOutputVolume(int* level) { + if (!initialized_) { + return false; + } + return worker_thread_->Invoke( + Bind(&MediaEngineInterface::GetOutputVolume, media_engine_.get(), level)); +} + +bool ChannelManager::SetOutputVolume(int level) { + bool ret = level >= 0 && level <= 255; + if (initialized_) { + ret &= worker_thread_->Invoke( + Bind(&MediaEngineInterface::SetOutputVolume, + media_engine_.get(), level)); + } + + if (ret) { + audio_output_volume_ = level; + } + + return ret; +} + +bool ChannelManager::IsSameCapturer(const std::string& capturer_name, + VideoCapturer* capturer) { + if (capturer == NULL) { + return false; + } + Device device; + if (!device_manager_->GetVideoCaptureDevice(capturer_name, &device)) { + return false; + } + return capturer->GetId() == device.id; +} + +bool ChannelManager::GetCaptureDevice(std::string* cam_name) { + if (camera_device_.empty()) { + // Initialize camera_device_ with default. + Device device; + if (!device_manager_->GetVideoCaptureDevice( + DeviceManagerInterface::kDefaultDeviceName, &device)) { + LOG(LS_WARNING) << "Device manager can't find default camera: " << + DeviceManagerInterface::kDefaultDeviceName; + return false; + } + camera_device_ = device.name; + } + *cam_name = camera_device_; + return true; +} + +bool ChannelManager::SetCaptureDevice(const std::string& cam_name) { + Device device; + bool ret = true; + if (!device_manager_->GetVideoCaptureDevice(cam_name, &device)) { + if (!cam_name.empty()) { + LOG(LS_WARNING) << "Device manager can't find camera: " << cam_name; + } + ret = false; + } + + // If we're running, tell the media engine about it. + if (initialized_ && ret) { + ret = worker_thread_->Invoke( + Bind(&ChannelManager::SetCaptureDevice_w, this, &device)); + } + + // If everything worked, retain the name of the selected camera. + if (ret) { + camera_device_ = device.name; + } else if (camera_device_.empty()) { + // When video option setting fails, we still want camera_device_ to be in a + // good state, so we initialize it with default if it's empty. + Device default_device; + if (!device_manager_->GetVideoCaptureDevice( + DeviceManagerInterface::kDefaultDeviceName, &default_device)) { + LOG(LS_WARNING) << "Device manager can't find default camera: " << + DeviceManagerInterface::kDefaultDeviceName; + } + camera_device_ = default_device.name; + } + + return ret; +} + +VideoCapturer* ChannelManager::CreateVideoCapturer() { + Device device; + if (!device_manager_->GetVideoCaptureDevice(camera_device_, &device)) { + if (!camera_device_.empty()) { + LOG(LS_WARNING) << "Device manager can't find camera: " << camera_device_; + } + return NULL; + } + return device_manager_->CreateVideoCapturer(device); +} + +bool ChannelManager::SetCaptureDevice_w(const Device* cam_device) { + ASSERT(worker_thread_ == talk_base::Thread::Current()); + ASSERT(initialized_); + + if (!cam_device) { + video_device_name_.clear(); + return true; + } + video_device_name_ = cam_device->name; + return true; +} + +bool ChannelManager::SetDefaultVideoEncoderConfig(const VideoEncoderConfig& c) { + bool ret = true; + if (initialized_) { + ret = worker_thread_->Invoke( + Bind(&MediaEngineInterface::SetDefaultVideoEncoderConfig, + media_engine_.get(), c)); + } + if (ret) { + default_video_encoder_config_ = c; + } + return ret; +} + +bool ChannelManager::SetLocalMonitor(bool enable) { + bool ret = initialized_ && worker_thread_->Invoke( + Bind(&MediaEngineInterface::SetLocalMonitor, + media_engine_.get(), enable)); + if (ret) { + monitoring_ = enable; + } + return ret; +} + +bool ChannelManager::SetLocalRenderer(VideoRenderer* renderer) { + bool ret = true; + if (initialized_) { + ret = worker_thread_->Invoke( + Bind(&MediaEngineInterface::SetLocalRenderer, + media_engine_.get(), renderer)); + } + if (ret) { + local_renderer_ = renderer; + } + return ret; +} + +bool ChannelManager::SetVideoCapturer(VideoCapturer* capturer) { + bool ret = true; + if (initialized_) { + ret = worker_thread_->Invoke( + Bind(&MediaEngineInterface::SetVideoCapturer, + media_engine_.get(), capturer)); + } + return ret; +} + +bool ChannelManager::SetVideoCapture(bool capture) { + bool ret = initialized_ && worker_thread_->Invoke( + Bind(&MediaEngineInterface::SetVideoCapture, + media_engine_.get(), capture)); + if (ret) { + capturing_ = capture; + } + return ret; +} + +void ChannelManager::SetVoiceLogging(int level, const char* filter) { + if (initialized_) { + worker_thread_->Invoke( + Bind(&MediaEngineInterface::SetVoiceLogging, + media_engine_.get(), level, filter)); + } else { + media_engine_->SetVoiceLogging(level, filter); + } +} + +void ChannelManager::SetVideoLogging(int level, const char* filter) { + if (initialized_) { + worker_thread_->Invoke( + Bind(&MediaEngineInterface::SetVideoLogging, + media_engine_.get(), level, filter)); + } else { + media_engine_->SetVideoLogging(level, filter); + } +} + +// TODO(janahan): For now pass this request through the mediaengine to the +// voice and video engines to do the real work. Once the capturer refactoring +// is done, we will access the capturer using the ssrc (similar to how the +// renderer is accessed today) and register with it directly. +bool ChannelManager::RegisterVideoProcessor(VideoCapturer* capturer, + VideoProcessor* processor) { + return initialized_ && worker_thread_->Invoke( + Bind(&ChannelManager::RegisterVideoProcessor_w, this, + capturer, processor)); +} + +bool ChannelManager::RegisterVideoProcessor_w(VideoCapturer* capturer, + VideoProcessor* processor) { + return capture_manager_->AddVideoProcessor(capturer, processor); +} + +bool ChannelManager::UnregisterVideoProcessor(VideoCapturer* capturer, + VideoProcessor* processor) { + return initialized_ && worker_thread_->Invoke( + Bind(&ChannelManager::UnregisterVideoProcessor_w, this, + capturer, processor)); +} + +bool ChannelManager::UnregisterVideoProcessor_w(VideoCapturer* capturer, + VideoProcessor* processor) { + return capture_manager_->RemoveVideoProcessor(capturer, processor); +} + +bool ChannelManager::RegisterVoiceProcessor( + uint32 ssrc, + VoiceProcessor* processor, + MediaProcessorDirection direction) { + return initialized_ && worker_thread_->Invoke( + Bind(&MediaEngineInterface::RegisterVoiceProcessor, media_engine_.get(), + ssrc, processor, direction)); +} + +bool ChannelManager::UnregisterVoiceProcessor( + uint32 ssrc, + VoiceProcessor* processor, + MediaProcessorDirection direction) { + return initialized_ && worker_thread_->Invoke( + Bind(&MediaEngineInterface::UnregisterVoiceProcessor, + media_engine_.get(), ssrc, processor, direction)); +} + +// The following are done in the new "CaptureManager" style that +// all local video capturers, processors, and managers should move +// to. +// TODO(pthatcher): Add more of the CaptureManager interface. +bool ChannelManager::StartVideoCapture( + VideoCapturer* capturer, const VideoFormat& video_format) { + return initialized_ && worker_thread_->Invoke( + Bind(&CaptureManager::StartVideoCapture, + capture_manager_.get(), capturer, video_format)); +} + +bool ChannelManager::MuteToBlackThenPause( + VideoCapturer* video_capturer, bool muted) { + if (!initialized_) { + return false; + } + worker_thread_->Invoke( + Bind(&VideoCapturer::MuteToBlackThenPause, video_capturer, muted)); + return true; +} + +bool ChannelManager::StopVideoCapture( + VideoCapturer* capturer, const VideoFormat& video_format) { + return initialized_ && worker_thread_->Invoke( + Bind(&CaptureManager::StopVideoCapture, + capture_manager_.get(), capturer, video_format)); +} + +bool ChannelManager::RestartVideoCapture( + VideoCapturer* video_capturer, + const VideoFormat& previous_format, + const VideoFormat& desired_format, + CaptureManager::RestartOptions options) { + return initialized_ && worker_thread_->Invoke( + Bind(&CaptureManager::RestartVideoCapture, capture_manager_.get(), + video_capturer, previous_format, desired_format, options)); +} + +bool ChannelManager::AddVideoRenderer( + VideoCapturer* capturer, VideoRenderer* renderer) { + return initialized_ && worker_thread_->Invoke( + Bind(&CaptureManager::AddVideoRenderer, + capture_manager_.get(), capturer, renderer)); +} + +bool ChannelManager::RemoveVideoRenderer( + VideoCapturer* capturer, VideoRenderer* renderer) { + return initialized_ && worker_thread_->Invoke( + Bind(&CaptureManager::RemoveVideoRenderer, + capture_manager_.get(), capturer, renderer)); +} + +bool ChannelManager::IsScreencastRunning() const { + return initialized_ && worker_thread_->Invoke( + Bind(&ChannelManager::IsScreencastRunning_w, this)); +} + +bool ChannelManager::IsScreencastRunning_w() const { + VideoChannels::const_iterator it = video_channels_.begin(); + for ( ; it != video_channels_.end(); ++it) { + if ((*it) && (*it)->IsScreencasting()) { + return true; + } + } + return false; +} + +void ChannelManager::OnVideoCaptureStateChange(VideoCapturer* capturer, + CaptureState result) { + // TODO(whyuan): Check capturer and signal failure only for camera video, not + // screencast. + capturing_ = result == CS_RUNNING; + main_thread_->Post(this, MSG_VIDEOCAPTURESTATE, + new CaptureStateParams(capturer, result)); +} + +void ChannelManager::OnMessage(talk_base::Message* message) { + switch (message->message_id) { + case MSG_VIDEOCAPTURESTATE: { + CaptureStateParams* data = + static_cast(message->pdata); + SignalVideoCaptureStateChange(data->capturer, data->state); + delete data; + break; + } + } +} + + +static void GetDeviceNames(const std::vector& devs, + std::vector* names) { + names->clear(); + for (size_t i = 0; i < devs.size(); ++i) { + names->push_back(devs[i].name); + } +} + +bool ChannelManager::GetAudioInputDevices(std::vector* names) { + names->clear(); + std::vector devs; + bool ret = device_manager_->GetAudioInputDevices(&devs); + if (ret) + GetDeviceNames(devs, names); + + return ret; +} + +bool ChannelManager::GetAudioOutputDevices(std::vector* names) { + names->clear(); + std::vector devs; + bool ret = device_manager_->GetAudioOutputDevices(&devs); + if (ret) + GetDeviceNames(devs, names); + + return ret; +} + +bool ChannelManager::GetVideoCaptureDevices(std::vector* names) { + names->clear(); + std::vector devs; + bool ret = device_manager_->GetVideoCaptureDevices(&devs); + if (ret) + GetDeviceNames(devs, names); + + return ret; +} + +void ChannelManager::SetVideoCaptureDeviceMaxFormat( + const std::string& usb_id, + const VideoFormat& max_format) { + device_manager_->SetVideoCaptureDeviceMaxFormat(usb_id, max_format); +} + +VideoFormat ChannelManager::GetStartCaptureFormat() { + return worker_thread_->Invoke( + Bind(&MediaEngineInterface::GetStartCaptureFormat, media_engine_.get())); +} + +} // namespace cricket diff --git a/talk/session/media/channelmanager.h b/talk/session/media/channelmanager.h new file mode 100644 index 000000000..0c5773791 --- /dev/null +++ b/talk/session/media/channelmanager.h @@ -0,0 +1,310 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_SESSION_MEDIA_CHANNELMANAGER_H_ +#define TALK_SESSION_MEDIA_CHANNELMANAGER_H_ + +#include +#include + +#include "talk/base/criticalsection.h" +#include "talk/base/sigslotrepeater.h" +#include "talk/base/thread.h" +#include "talk/media/base/capturemanager.h" +#include "talk/media/base/mediaengine.h" +#include "talk/p2p/base/session.h" +#include "talk/session/media/voicechannel.h" + +namespace cricket { + +class Soundclip; +class VideoProcessor; +class VoiceChannel; +class VoiceProcessor; + +// ChannelManager allows the MediaEngine to run on a separate thread, and takes +// care of marshalling calls between threads. It also creates and keeps track of +// voice and video channels; by doing so, it can temporarily pause all the +// channels when a new audio or video device is chosen. The voice and video +// channels are stored in separate vectors, to easily allow operations on just +// voice or just video channels. +// ChannelManager also allows the application to discover what devices it has +// using device manager. +class ChannelManager : public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: +#if !defined(DISABLE_MEDIA_ENGINE_FACTORY) + // Creates the channel manager, and specifies the worker thread to use. + explicit ChannelManager(talk_base::Thread* worker); +#endif + + // For testing purposes. Allows the media engine and data media + // engine and dev manager to be mocks. The ChannelManager takes + // ownership of these objects. + ChannelManager(MediaEngineInterface* me, + DataEngineInterface* dme, + DeviceManagerInterface* dm, + CaptureManager* cm, + talk_base::Thread* worker); + // Same as above, but gives an easier default DataEngine. + ChannelManager(MediaEngineInterface* me, + DeviceManagerInterface* dm, + talk_base::Thread* worker); + ~ChannelManager(); + + // Accessors for the worker thread, allowing it to be set after construction, + // but before Init. set_worker_thread will return false if called after Init. + talk_base::Thread* worker_thread() const { return worker_thread_; } + bool set_worker_thread(talk_base::Thread* thread) { + if (initialized_) return false; + worker_thread_ = thread; + return true; + } + + // Gets capabilities. Can be called prior to starting the media engine. + int GetCapabilities(); + + // Retrieves the list of supported audio & video codec types. + // Can be called before starting the media engine. + void GetSupportedAudioCodecs(std::vector* codecs) const; + void GetSupportedAudioRtpHeaderExtensions(RtpHeaderExtensions* ext) const; + void GetSupportedVideoCodecs(std::vector* codecs) const; + void GetSupportedVideoRtpHeaderExtensions(RtpHeaderExtensions* ext) const; + void GetSupportedDataCodecs(std::vector* codecs) const; + + // Indicates whether the media engine is started. + bool initialized() const { return initialized_; } + // Starts up the media engine. + bool Init(); + // Shuts down the media engine. + void Terminate(); + + // The operations below all occur on the worker thread. + + // Creates a voice channel, to be associated with the specified session. + VoiceChannel* CreateVoiceChannel( + BaseSession* session, const std::string& content_name, bool rtcp); + // Destroys a voice channel created with the Create API. + void DestroyVoiceChannel(VoiceChannel* voice_channel); + // Creates a video channel, synced with the specified voice channel, and + // associated with the specified session. + VideoChannel* CreateVideoChannel( + BaseSession* session, const std::string& content_name, bool rtcp, + VoiceChannel* voice_channel); + // Destroys a video channel created with the Create API. + void DestroyVideoChannel(VideoChannel* video_channel); + DataChannel* CreateDataChannel( + BaseSession* session, const std::string& content_name, + bool rtcp, DataChannelType data_channel_type); + // Destroys a data channel created with the Create API. + void DestroyDataChannel(DataChannel* data_channel); + + // Creates a soundclip. + Soundclip* CreateSoundclip(); + // Destroys a soundclip created with the Create API. + void DestroySoundclip(Soundclip* soundclip); + + // Indicates whether any channels exist. + bool has_channels() const { + return (!voice_channels_.empty() || !video_channels_.empty() || + !soundclips_.empty()); + } + + // Configures the audio and video devices. A null pointer can be passed to + // GetAudioOptions() for any parameter of no interest. + bool GetAudioOptions(std::string* wave_in_device, + std::string* wave_out_device, int* opts); + bool SetAudioOptions(const std::string& wave_in_device, + const std::string& wave_out_device, int opts); + bool GetOutputVolume(int* level); + bool SetOutputVolume(int level); + bool IsSameCapturer(const std::string& capturer_name, + VideoCapturer* capturer); + bool GetCaptureDevice(std::string* cam_device); + // Create capturer based on what has been set in SetCaptureDevice(). + VideoCapturer* CreateVideoCapturer(); + bool SetCaptureDevice(const std::string& cam_device); + bool SetDefaultVideoEncoderConfig(const VideoEncoderConfig& config); + // RTX will be enabled/disabled in engines that support it. The supporting + // engines will start offering an RTX codec. Must be called before Init(). + bool SetVideoRtxEnabled(bool enable); + + // Starts/stops the local microphone and enables polling of the input level. + bool SetLocalMonitor(bool enable); + bool monitoring() const { return monitoring_; } + // Sets the local renderer where to renderer the local camera. + bool SetLocalRenderer(VideoRenderer* renderer); + // Sets the externally provided video capturer. The ssrc is the ssrc of the + // (video) stream for which the video capturer should be set. + bool SetVideoCapturer(VideoCapturer* capturer); + // Starts and stops the local camera and renders it to the local renderer. + bool SetVideoCapture(bool capture); + bool capturing() const { return capturing_; } + + // Configures the logging output of the mediaengine(s). + void SetVoiceLogging(int level, const char* filter); + void SetVideoLogging(int level, const char* filter); + + // The channel manager handles the Tx side for Video processing, + // as well as Tx and Rx side for Voice processing. + // (The Rx Video processing will go throug the simplerenderingmanager, + // to be implemented). + bool RegisterVideoProcessor(VideoCapturer* capturer, + VideoProcessor* processor); + bool UnregisterVideoProcessor(VideoCapturer* capturer, + VideoProcessor* processor); + bool RegisterVoiceProcessor(uint32 ssrc, + VoiceProcessor* processor, + MediaProcessorDirection direction); + bool UnregisterVoiceProcessor(uint32 ssrc, + VoiceProcessor* processor, + MediaProcessorDirection direction); + // The following are done in the new "CaptureManager" style that + // all local video capturers, processors, and managers should move to. + // TODO(pthatcher): Make methods nicer by having start return a handle that + // can be used for stop and restart, rather than needing to pass around + // formats a a pseudo-handle. + bool StartVideoCapture(VideoCapturer* video_capturer, + const VideoFormat& video_format); + // When muting, produce black frames then pause the camera. + // When unmuting, start the camera. Camera starts unmuted. + bool MuteToBlackThenPause(VideoCapturer* video_capturer, bool muted); + bool StopVideoCapture(VideoCapturer* video_capturer, + const VideoFormat& video_format); + bool RestartVideoCapture(VideoCapturer* video_capturer, + const VideoFormat& previous_format, + const VideoFormat& desired_format, + CaptureManager::RestartOptions options); + + bool AddVideoRenderer(VideoCapturer* capturer, VideoRenderer* renderer); + bool RemoveVideoRenderer(VideoCapturer* capturer, VideoRenderer* renderer); + bool IsScreencastRunning() const; + + // The operations below occur on the main thread. + + bool GetAudioInputDevices(std::vector* names); + bool GetAudioOutputDevices(std::vector* names); + bool GetVideoCaptureDevices(std::vector* names); + void SetVideoCaptureDeviceMaxFormat(const std::string& usb_id, + const VideoFormat& max_format); + + sigslot::repeater0<> SignalDevicesChange; + sigslot::signal2 SignalVideoCaptureStateChange; + + // Returns the current selected device. Note: Subtly different from + // GetCaptureDevice(). See member video_device_ for more details. + // This API is mainly a hook used by unittests. + const std::string& video_device_name() const { return video_device_name_; } + + // TODO(hellner): Remove this function once the engine capturer has been + // removed. + VideoFormat GetStartCaptureFormat(); + protected: + // Adds non-transient parameters which can only be changed through the + // options store. + bool SetAudioOptions(const std::string& wave_in_device, + const std::string& wave_out_device, int opts, + int delay_offset); + int audio_delay_offset() const { return audio_delay_offset_; } + + private: + typedef std::vector VoiceChannels; + typedef std::vector VideoChannels; + typedef std::vector DataChannels; + typedef std::vector Soundclips; + + void Construct(MediaEngineInterface* me, + DataEngineInterface* dme, + DeviceManagerInterface* dm, + CaptureManager* cm, + talk_base::Thread* worker_thread); + void Terminate_w(); + VoiceChannel* CreateVoiceChannel_w( + BaseSession* session, const std::string& content_name, bool rtcp); + void DestroyVoiceChannel_w(VoiceChannel* voice_channel); + VideoChannel* CreateVideoChannel_w( + BaseSession* session, const std::string& content_name, bool rtcp, + VoiceChannel* voice_channel); + void DestroyVideoChannel_w(VideoChannel* video_channel); + DataChannel* CreateDataChannel_w( + BaseSession* session, const std::string& content_name, + bool rtcp, DataChannelType data_channel_type); + void DestroyDataChannel_w(DataChannel* data_channel); + Soundclip* CreateSoundclip_w(); + void DestroySoundclip_w(Soundclip* soundclip); + bool SetAudioOptions_w(int opts, int delay_offset, const Device* in_dev, + const Device* out_dev); + bool SetCaptureDevice_w(const Device* cam_device); + void OnVideoCaptureStateChange(VideoCapturer* capturer, + CaptureState result); + bool RegisterVideoProcessor_w(VideoCapturer* capturer, + VideoProcessor* processor); + bool UnregisterVideoProcessor_w(VideoCapturer* capturer, + VideoProcessor* processor); + bool IsScreencastRunning_w() const; + virtual void OnMessage(talk_base::Message *message); + + talk_base::scoped_ptr media_engine_; + talk_base::scoped_ptr data_media_engine_; + talk_base::scoped_ptr device_manager_; + talk_base::scoped_ptr capture_manager_; + bool initialized_; + talk_base::Thread* main_thread_; + talk_base::Thread* worker_thread_; + + VoiceChannels voice_channels_; + VideoChannels video_channels_; + DataChannels data_channels_; + Soundclips soundclips_; + + std::string audio_in_device_; + std::string audio_out_device_; + int audio_options_; + int audio_delay_offset_; + int audio_output_volume_; + std::string camera_device_; + VideoEncoderConfig default_video_encoder_config_; + VideoRenderer* local_renderer_; + bool enable_rtx_; + + bool capturing_; + bool monitoring_; + + talk_base::scoped_ptr video_capturer_; + + // String containing currently set device. Note that this string is subtly + // different from camera_device_. E.g. camera_device_ will list unplugged + // but selected devices while this sting will be empty or contain current + // selected device. + // TODO(hellner): refactor the code such that there is no need to keep two + // strings for video devices that have subtle differences in behavior. + std::string video_device_name_; +}; + +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_CHANNELMANAGER_H_ diff --git a/talk/session/media/channelmanager_unittest.cc b/talk/session/media/channelmanager_unittest.cc new file mode 100644 index 000000000..20db58d11 --- /dev/null +++ b/talk/session/media/channelmanager_unittest.cc @@ -0,0 +1,610 @@ +// libjingle +// Copyright 2008 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/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/thread.h" +#include "talk/media/base/fakecapturemanager.h" +#include "talk/media/base/fakemediaengine.h" +#include "talk/media/base/fakemediaprocessor.h" +#include "talk/media/base/nullvideorenderer.h" +#include "talk/media/devices/fakedevicemanager.h" +#include "talk/media/base/testutils.h" +#include "talk/p2p/base/fakesession.h" +#include "talk/session/media/channelmanager.h" + +namespace cricket { + +static const AudioCodec kAudioCodecs[] = { + AudioCodec(97, "voice", 1, 2, 3, 0), + AudioCodec(110, "CELT", 32000, 48000, 2, 0), + AudioCodec(111, "OPUS", 48000, 32000, 2, 0), +}; + +static const VideoCodec kVideoCodecs[] = { + VideoCodec(99, "H264", 100, 200, 300, 0), + VideoCodec(100, "VP8", 100, 200, 300, 0), + VideoCodec(96, "rtx", 100, 200, 300, 0), +}; + +class ChannelManagerTest : public testing::Test { + protected: + ChannelManagerTest() : fme_(NULL), fdm_(NULL), fcm_(NULL), cm_(NULL) { + } + + virtual void SetUp() { + fme_ = new cricket::FakeMediaEngine(); + fme_->SetAudioCodecs(MAKE_VECTOR(kAudioCodecs)); + fme_->SetVideoCodecs(MAKE_VECTOR(kVideoCodecs)); + fdme_ = new cricket::FakeDataEngine(); + fdm_ = new cricket::FakeDeviceManager(); + fcm_ = new cricket::FakeCaptureManager(); + cm_ = new cricket::ChannelManager( + fme_, fdme_, fdm_, fcm_, talk_base::Thread::Current()); + session_ = new cricket::FakeSession(true); + + std::vector in_device_list, out_device_list, vid_device_list; + in_device_list.push_back("audio-in1"); + in_device_list.push_back("audio-in2"); + out_device_list.push_back("audio-out1"); + out_device_list.push_back("audio-out2"); + vid_device_list.push_back("video-in1"); + vid_device_list.push_back("video-in2"); + fdm_->SetAudioInputDevices(in_device_list); + fdm_->SetAudioOutputDevices(out_device_list); + fdm_->SetVideoCaptureDevices(vid_device_list); + } + + virtual void TearDown() { + delete session_; + delete cm_; + cm_ = NULL; + fdm_ = NULL; + fcm_ = NULL; + fdme_ = NULL; + fme_ = NULL; + } + + talk_base::Thread worker_; + cricket::FakeMediaEngine* fme_; + cricket::FakeDataEngine* fdme_; + cricket::FakeDeviceManager* fdm_; + cricket::FakeCaptureManager* fcm_; + cricket::ChannelManager* cm_; + cricket::FakeSession* session_; +}; + +// Test that we startup/shutdown properly. +TEST_F(ChannelManagerTest, StartupShutdown) { + EXPECT_FALSE(cm_->initialized()); + EXPECT_EQ(talk_base::Thread::Current(), cm_->worker_thread()); + EXPECT_TRUE(cm_->Init()); + EXPECT_TRUE(cm_->initialized()); + cm_->Terminate(); + EXPECT_FALSE(cm_->initialized()); +} + +// Test that we startup/shutdown properly with a worker thread. +TEST_F(ChannelManagerTest, StartupShutdownOnThread) { + worker_.Start(); + EXPECT_FALSE(cm_->initialized()); + EXPECT_EQ(talk_base::Thread::Current(), cm_->worker_thread()); + EXPECT_TRUE(cm_->set_worker_thread(&worker_)); + EXPECT_EQ(&worker_, cm_->worker_thread()); + EXPECT_TRUE(cm_->Init()); + EXPECT_TRUE(cm_->initialized()); + // Setting the worker thread while initialized should fail. + EXPECT_FALSE(cm_->set_worker_thread(talk_base::Thread::Current())); + cm_->Terminate(); + EXPECT_FALSE(cm_->initialized()); +} + +// Test that we fail to startup if we're given an unstarted thread. +TEST_F(ChannelManagerTest, StartupShutdownOnUnstartedThread) { + EXPECT_TRUE(cm_->set_worker_thread(&worker_)); + EXPECT_FALSE(cm_->Init()); + EXPECT_FALSE(cm_->initialized()); +} + +// Test that we can create and destroy a voice and video channel. +TEST_F(ChannelManagerTest, CreateDestroyChannels) { + EXPECT_TRUE(cm_->Init()); + cricket::VoiceChannel* voice_channel = cm_->CreateVoiceChannel( + session_, cricket::CN_AUDIO, false); + EXPECT_TRUE(voice_channel != NULL); + cricket::VideoChannel* video_channel = + cm_->CreateVideoChannel(session_, cricket::CN_VIDEO, + false, voice_channel); + EXPECT_TRUE(video_channel != NULL); + cricket::DataChannel* data_channel = + cm_->CreateDataChannel(session_, cricket::CN_DATA, + false, cricket::DCT_RTP); + EXPECT_TRUE(data_channel != NULL); + cm_->DestroyVideoChannel(video_channel); + cm_->DestroyVoiceChannel(voice_channel); + cm_->DestroyDataChannel(data_channel); + cm_->Terminate(); +} + +// Test that we can create and destroy a voice and video channel with a worker. +TEST_F(ChannelManagerTest, CreateDestroyChannelsOnThread) { + worker_.Start(); + EXPECT_TRUE(cm_->set_worker_thread(&worker_)); + EXPECT_TRUE(cm_->Init()); + cricket::VoiceChannel* voice_channel = cm_->CreateVoiceChannel( + session_, cricket::CN_AUDIO, false); + EXPECT_TRUE(voice_channel != NULL); + cricket::VideoChannel* video_channel = + cm_->CreateVideoChannel(session_, cricket::CN_VIDEO, + false, voice_channel); + EXPECT_TRUE(video_channel != NULL); + cricket::DataChannel* data_channel = + cm_->CreateDataChannel(session_, cricket::CN_DATA, + false, cricket::DCT_RTP); + EXPECT_TRUE(data_channel != NULL); + cm_->DestroyVideoChannel(video_channel); + cm_->DestroyVoiceChannel(voice_channel); + cm_->DestroyDataChannel(data_channel); + cm_->Terminate(); +} + +// Test that we fail to create a voice/video channel if the session is unable +// to create a cricket::TransportChannel +TEST_F(ChannelManagerTest, NoTransportChannelTest) { + EXPECT_TRUE(cm_->Init()); + session_->set_fail_channel_creation(true); + // The test is useless unless the session does not fail creating + // cricket::TransportChannel. + ASSERT_TRUE(session_->CreateChannel( + "audio", "rtp", cricket::ICE_CANDIDATE_COMPONENT_RTP) == NULL); + + cricket::VoiceChannel* voice_channel = cm_->CreateVoiceChannel( + session_, cricket::CN_AUDIO, false); + EXPECT_TRUE(voice_channel == NULL); + cricket::VideoChannel* video_channel = + cm_->CreateVideoChannel(session_, cricket::CN_VIDEO, + false, voice_channel); + EXPECT_TRUE(video_channel == NULL); + cricket::DataChannel* data_channel = + cm_->CreateDataChannel(session_, cricket::CN_DATA, + false, cricket::DCT_RTP); + EXPECT_TRUE(data_channel == NULL); + cm_->Terminate(); +} + +// Test that SetDefaultVideoCodec passes through the right values. +TEST_F(ChannelManagerTest, SetDefaultVideoEncoderConfig) { + cricket::VideoCodec codec(96, "G264", 1280, 720, 60, 0); + cricket::VideoEncoderConfig config(codec, 1, 2); + EXPECT_TRUE(cm_->Init()); + EXPECT_TRUE(cm_->SetDefaultVideoEncoderConfig(config)); + EXPECT_EQ(config, fme_->default_video_encoder_config()); +} + +// Test that SetDefaultVideoCodec passes through the right values. +TEST_F(ChannelManagerTest, SetDefaultVideoCodecBeforeInit) { + cricket::VideoCodec codec(96, "G264", 1280, 720, 60, 0); + cricket::VideoEncoderConfig config(codec, 1, 2); + EXPECT_TRUE(cm_->SetDefaultVideoEncoderConfig(config)); + EXPECT_TRUE(cm_->Init()); + EXPECT_EQ(config, fme_->default_video_encoder_config()); +} + +TEST_F(ChannelManagerTest, SetAudioOptionsBeforeInit) { + // Test that values that we set before Init are applied. + EXPECT_TRUE(cm_->SetAudioOptions("audio-in1", "audio-out1", 0x2)); + EXPECT_TRUE(cm_->Init()); + EXPECT_EQ("audio-in1", fme_->audio_in_device()); + EXPECT_EQ("audio-out1", fme_->audio_out_device()); + EXPECT_EQ(0x2, fme_->audio_options()); + EXPECT_EQ(0, fme_->audio_delay_offset()); + EXPECT_EQ(cricket::MediaEngineInterface::kDefaultAudioDelayOffset, + fme_->audio_delay_offset()); +} + +TEST_F(ChannelManagerTest, GetAudioOptionsBeforeInit) { + std::string audio_in, audio_out; + int opts; + // Test that GetAudioOptions works before Init. + EXPECT_TRUE(cm_->SetAudioOptions("audio-in2", "audio-out2", 0x1)); + EXPECT_TRUE(cm_->GetAudioOptions(&audio_in, &audio_out, &opts)); + EXPECT_EQ("audio-in2", audio_in); + EXPECT_EQ("audio-out2", audio_out); + EXPECT_EQ(0x1, opts); + // Test that options set before Init can be gotten after Init. + EXPECT_TRUE(cm_->SetAudioOptions("audio-in1", "audio-out1", 0x2)); + EXPECT_TRUE(cm_->Init()); + EXPECT_TRUE(cm_->GetAudioOptions(&audio_in, &audio_out, &opts)); + EXPECT_EQ("audio-in1", audio_in); + EXPECT_EQ("audio-out1", audio_out); + EXPECT_EQ(0x2, opts); +} + +TEST_F(ChannelManagerTest, GetAudioOptionsWithNullParameters) { + std::string audio_in, audio_out; + int opts; + EXPECT_TRUE(cm_->SetAudioOptions("audio-in2", "audio-out2", 0x1)); + EXPECT_TRUE(cm_->GetAudioOptions(&audio_in, NULL, NULL)); + EXPECT_EQ("audio-in2", audio_in); + EXPECT_TRUE(cm_->GetAudioOptions(NULL, &audio_out, NULL)); + EXPECT_EQ("audio-out2", audio_out); + EXPECT_TRUE(cm_->GetAudioOptions(NULL, NULL, &opts)); + EXPECT_EQ(0x1, opts); +} + +TEST_F(ChannelManagerTest, SetAudioOptions) { + // Test initial state. + EXPECT_TRUE(cm_->Init()); + EXPECT_EQ(std::string(cricket::DeviceManagerInterface::kDefaultDeviceName), + fme_->audio_in_device()); + EXPECT_EQ(std::string(cricket::DeviceManagerInterface::kDefaultDeviceName), + fme_->audio_out_device()); + EXPECT_EQ(cricket::MediaEngineInterface::DEFAULT_AUDIO_OPTIONS, + fme_->audio_options()); + EXPECT_EQ(cricket::MediaEngineInterface::kDefaultAudioDelayOffset, + fme_->audio_delay_offset()); + // Test setting defaults. + EXPECT_TRUE(cm_->SetAudioOptions("", "", + cricket::MediaEngineInterface::DEFAULT_AUDIO_OPTIONS)); + EXPECT_EQ("", fme_->audio_in_device()); + EXPECT_EQ("", fme_->audio_out_device()); + EXPECT_EQ(cricket::MediaEngineInterface::DEFAULT_AUDIO_OPTIONS, + fme_->audio_options()); + EXPECT_EQ(cricket::MediaEngineInterface::kDefaultAudioDelayOffset, + fme_->audio_delay_offset()); + // Test setting specific values. + EXPECT_TRUE(cm_->SetAudioOptions("audio-in1", "audio-out1", 0x2)); + EXPECT_EQ("audio-in1", fme_->audio_in_device()); + EXPECT_EQ("audio-out1", fme_->audio_out_device()); + EXPECT_EQ(0x2, fme_->audio_options()); + EXPECT_EQ(cricket::MediaEngineInterface::kDefaultAudioDelayOffset, + fme_->audio_delay_offset()); + // Test setting bad values. + EXPECT_FALSE(cm_->SetAudioOptions("audio-in9", "audio-out2", 0x1)); +} + +TEST_F(ChannelManagerTest, GetAudioOptions) { + std::string audio_in, audio_out; + int opts; + // Test initial state. + EXPECT_TRUE(cm_->Init()); + EXPECT_TRUE(cm_->GetAudioOptions(&audio_in, &audio_out, &opts)); + EXPECT_EQ(std::string(cricket::DeviceManagerInterface::kDefaultDeviceName), + audio_in); + EXPECT_EQ(std::string(cricket::DeviceManagerInterface::kDefaultDeviceName), + audio_out); + EXPECT_EQ(cricket::MediaEngineInterface::DEFAULT_AUDIO_OPTIONS, opts); + // Test that we get back specific values that we set. + EXPECT_TRUE(cm_->SetAudioOptions("audio-in1", "audio-out1", 0x2)); + EXPECT_TRUE(cm_->GetAudioOptions(&audio_in, &audio_out, &opts)); + EXPECT_EQ("audio-in1", audio_in); + EXPECT_EQ("audio-out1", audio_out); + EXPECT_EQ(0x2, opts); +} + +TEST_F(ChannelManagerTest, SetCaptureDeviceBeforeInit) { + // Test that values that we set before Init are applied. + EXPECT_TRUE(cm_->SetCaptureDevice("video-in2")); + EXPECT_TRUE(cm_->Init()); + EXPECT_EQ("video-in2", cm_->video_device_name()); +} + +TEST_F(ChannelManagerTest, GetCaptureDeviceBeforeInit) { + std::string video_in; + // Test that GetCaptureDevice works before Init. + EXPECT_TRUE(cm_->SetCaptureDevice("video-in1")); + EXPECT_TRUE(cm_->GetCaptureDevice(&video_in)); + EXPECT_EQ("video-in1", video_in); + // Test that options set before Init can be gotten after Init. + EXPECT_TRUE(cm_->SetCaptureDevice("video-in2")); + EXPECT_TRUE(cm_->Init()); + EXPECT_TRUE(cm_->GetCaptureDevice(&video_in)); + EXPECT_EQ("video-in2", video_in); +} + +TEST_F(ChannelManagerTest, SetCaptureDevice) { + // Test setting defaults. + EXPECT_TRUE(cm_->Init()); + EXPECT_TRUE(cm_->SetCaptureDevice("")); // will use DeviceManager default + EXPECT_EQ("video-in1", cm_->video_device_name()); + // Test setting specific values. + EXPECT_TRUE(cm_->SetCaptureDevice("video-in2")); + EXPECT_EQ("video-in2", cm_->video_device_name()); + // TODO(juberti): Add test for invalid value here. +} + +// Test unplugging and plugging back the preferred devices. When the preferred +// device is unplugged, we fall back to the default device. When the preferred +// device is plugged back, we use it. +TEST_F(ChannelManagerTest, SetAudioOptionsUnplugPlug) { + // Set preferences "audio-in1" and "audio-out1" before init. + EXPECT_TRUE(cm_->SetAudioOptions("audio-in1", "audio-out1", 0x2)); + // Unplug device "audio-in1" and "audio-out1". + std::vector in_device_list, out_device_list; + in_device_list.push_back("audio-in2"); + out_device_list.push_back("audio-out2"); + fdm_->SetAudioInputDevices(in_device_list); + fdm_->SetAudioOutputDevices(out_device_list); + // Init should fall back to default devices. + EXPECT_TRUE(cm_->Init()); + // The media engine should use the default. + EXPECT_EQ("", fme_->audio_in_device()); + EXPECT_EQ("", fme_->audio_out_device()); + // The channel manager keeps the preferences "audio-in1" and "audio-out1". + std::string audio_in, audio_out; + EXPECT_TRUE(cm_->GetAudioOptions(&audio_in, &audio_out, NULL)); + EXPECT_EQ("audio-in1", audio_in); + EXPECT_EQ("audio-out1", audio_out); + cm_->Terminate(); + + // Plug devices "audio-in2" and "audio-out2" back. + in_device_list.push_back("audio-in1"); + out_device_list.push_back("audio-out1"); + fdm_->SetAudioInputDevices(in_device_list); + fdm_->SetAudioOutputDevices(out_device_list); + // Init again. The preferences, "audio-in2" and "audio-out2", are used. + EXPECT_TRUE(cm_->Init()); + EXPECT_EQ("audio-in1", fme_->audio_in_device()); + EXPECT_EQ("audio-out1", fme_->audio_out_device()); + EXPECT_TRUE(cm_->GetAudioOptions(&audio_in, &audio_out, NULL)); + EXPECT_EQ("audio-in1", audio_in); + EXPECT_EQ("audio-out1", audio_out); +} + +// We have one camera. Unplug it, fall back to no camera. +TEST_F(ChannelManagerTest, SetCaptureDeviceUnplugPlugOneCamera) { + // Set preferences "video-in1" before init. + std::vector vid_device_list; + vid_device_list.push_back("video-in1"); + fdm_->SetVideoCaptureDevices(vid_device_list); + EXPECT_TRUE(cm_->SetCaptureDevice("video-in1")); + + // Unplug "video-in1". + vid_device_list.clear(); + fdm_->SetVideoCaptureDevices(vid_device_list); + + // Init should fall back to avatar. + EXPECT_TRUE(cm_->Init()); + // The media engine should use no camera. + EXPECT_EQ("", cm_->video_device_name()); + // The channel manager keeps the user preference "video-in". + std::string video_in; + EXPECT_TRUE(cm_->GetCaptureDevice(&video_in)); + EXPECT_EQ("video-in1", video_in); + cm_->Terminate(); + + // Plug device "video-in1" back. + vid_device_list.push_back("video-in1"); + fdm_->SetVideoCaptureDevices(vid_device_list); + // Init again. The user preferred device, "video-in1", is used. + EXPECT_TRUE(cm_->Init()); + EXPECT_EQ("video-in1", cm_->video_device_name()); + EXPECT_TRUE(cm_->GetCaptureDevice(&video_in)); + EXPECT_EQ("video-in1", video_in); +} + +// We have multiple cameras. Unplug the preferred, fall back to another camera. +TEST_F(ChannelManagerTest, SetCaptureDeviceUnplugPlugTwoDevices) { + // Set video device to "video-in1" before init. + EXPECT_TRUE(cm_->SetCaptureDevice("video-in1")); + // Unplug device "video-in1". + std::vector vid_device_list; + vid_device_list.push_back("video-in2"); + fdm_->SetVideoCaptureDevices(vid_device_list); + // Init should fall back to default device "video-in2". + EXPECT_TRUE(cm_->Init()); + // The media engine should use the default device "video-in2". + EXPECT_EQ("video-in2", cm_->video_device_name()); + // The channel manager keeps the user preference "video-in". + std::string video_in; + EXPECT_TRUE(cm_->GetCaptureDevice(&video_in)); + EXPECT_EQ("video-in1", video_in); + cm_->Terminate(); + + // Plug device "video-in1" back. + vid_device_list.push_back("video-in1"); + fdm_->SetVideoCaptureDevices(vid_device_list); + // Init again. The user preferred device, "video-in1", is used. + EXPECT_TRUE(cm_->Init()); + EXPECT_EQ("video-in1", cm_->video_device_name()); + EXPECT_TRUE(cm_->GetCaptureDevice(&video_in)); + EXPECT_EQ("video-in1", video_in); +} + +TEST_F(ChannelManagerTest, GetCaptureDevice) { + std::string video_in; + // Test setting/getting defaults. + EXPECT_TRUE(cm_->Init()); + EXPECT_TRUE(cm_->SetCaptureDevice("")); + EXPECT_TRUE(cm_->GetCaptureDevice(&video_in)); + EXPECT_EQ("video-in1", video_in); + // Test setting/getting specific values. + EXPECT_TRUE(cm_->SetCaptureDevice("video-in2")); + EXPECT_TRUE(cm_->GetCaptureDevice(&video_in)); + EXPECT_EQ("video-in2", video_in); +} + +TEST_F(ChannelManagerTest, GetSetOutputVolumeBeforeInit) { + int level; + // Before init, SetOutputVolume() remembers the volume but does not change the + // volume of the engine. GetOutputVolume() should fail. + EXPECT_EQ(-1, fme_->output_volume()); + EXPECT_FALSE(cm_->GetOutputVolume(&level)); + EXPECT_FALSE(cm_->SetOutputVolume(-1)); // Invalid volume. + EXPECT_TRUE(cm_->SetOutputVolume(99)); + EXPECT_EQ(-1, fme_->output_volume()); + + // Init() will apply the remembered volume. + EXPECT_TRUE(cm_->Init()); + EXPECT_TRUE(cm_->GetOutputVolume(&level)); + EXPECT_EQ(99, level); + EXPECT_EQ(level, fme_->output_volume()); + + EXPECT_TRUE(cm_->SetOutputVolume(60)); + EXPECT_TRUE(cm_->GetOutputVolume(&level)); + EXPECT_EQ(60, level); + EXPECT_EQ(level, fme_->output_volume()); +} + +TEST_F(ChannelManagerTest, GetSetOutputVolume) { + int level; + EXPECT_TRUE(cm_->Init()); + EXPECT_TRUE(cm_->GetOutputVolume(&level)); + EXPECT_EQ(level, fme_->output_volume()); + + EXPECT_FALSE(cm_->SetOutputVolume(-1)); // Invalid volume. + EXPECT_TRUE(cm_->SetOutputVolume(60)); + EXPECT_EQ(60, fme_->output_volume()); + EXPECT_TRUE(cm_->GetOutputVolume(&level)); + EXPECT_EQ(60, level); +} + +// Test that a value set before Init is applied properly. +TEST_F(ChannelManagerTest, SetLocalRendererBeforeInit) { + cricket::NullVideoRenderer renderer; + EXPECT_TRUE(cm_->SetLocalRenderer(&renderer)); + EXPECT_TRUE(cm_->Init()); + EXPECT_EQ(&renderer, fme_->local_renderer()); +} + +// Test that a value set after init is passed through properly. +TEST_F(ChannelManagerTest, SetLocalRenderer) { + cricket::NullVideoRenderer renderer; + EXPECT_TRUE(cm_->Init()); + EXPECT_TRUE(cm_->SetLocalRenderer(&renderer)); + EXPECT_EQ(&renderer, fme_->local_renderer()); +} + +// Test that logging options set before Init are applied properly, +// and retained even after Init. +TEST_F(ChannelManagerTest, SetLoggingBeforeInit) { + cm_->SetVoiceLogging(talk_base::LS_INFO, "test-voice"); + cm_->SetVideoLogging(talk_base::LS_VERBOSE, "test-video"); + EXPECT_EQ(talk_base::LS_INFO, fme_->voice_loglevel()); + EXPECT_STREQ("test-voice", fme_->voice_logfilter().c_str()); + EXPECT_EQ(talk_base::LS_VERBOSE, fme_->video_loglevel()); + EXPECT_STREQ("test-video", fme_->video_logfilter().c_str()); + EXPECT_TRUE(cm_->Init()); + EXPECT_EQ(talk_base::LS_INFO, fme_->voice_loglevel()); + EXPECT_STREQ("test-voice", fme_->voice_logfilter().c_str()); + EXPECT_EQ(talk_base::LS_VERBOSE, fme_->video_loglevel()); + EXPECT_STREQ("test-video", fme_->video_logfilter().c_str()); +} + +// Test that logging options set after Init are applied properly. +TEST_F(ChannelManagerTest, SetLogging) { + EXPECT_TRUE(cm_->Init()); + cm_->SetVoiceLogging(talk_base::LS_INFO, "test-voice"); + cm_->SetVideoLogging(talk_base::LS_VERBOSE, "test-video"); + EXPECT_EQ(talk_base::LS_INFO, fme_->voice_loglevel()); + EXPECT_STREQ("test-voice", fme_->voice_logfilter().c_str()); + EXPECT_EQ(talk_base::LS_VERBOSE, fme_->video_loglevel()); + EXPECT_STREQ("test-video", fme_->video_logfilter().c_str()); +} + +// Test that SetVideoCapture passes through the right value. +TEST_F(ChannelManagerTest, SetVideoCapture) { + // Should fail until we are initialized. + EXPECT_FALSE(fme_->capture()); + EXPECT_FALSE(cm_->SetVideoCapture(true)); + EXPECT_FALSE(fme_->capture()); + EXPECT_TRUE(cm_->Init()); + EXPECT_FALSE(fme_->capture()); + EXPECT_TRUE(cm_->SetVideoCapture(true)); + EXPECT_TRUE(fme_->capture()); + EXPECT_TRUE(cm_->SetVideoCapture(false)); + EXPECT_FALSE(fme_->capture()); +} + +// Test that the Video/Voice Processors register and unregister +TEST_F(ChannelManagerTest, RegisterProcessors) { + cricket::FakeMediaProcessor fmp; + EXPECT_TRUE(cm_->Init()); + EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_TX)); + EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_RX)); + + EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_TX)); + EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_RX)); + + EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_TX)); + EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_RX)); + + EXPECT_TRUE(cm_->RegisterVoiceProcessor(1, + &fmp, + cricket::MPD_RX)); + EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_TX)); + EXPECT_TRUE(fme_->voice_processor_registered(cricket::MPD_RX)); + + + EXPECT_TRUE(cm_->UnregisterVoiceProcessor(1, + &fmp, + cricket::MPD_RX)); + EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_TX)); + EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_RX)); + + EXPECT_TRUE(cm_->RegisterVoiceProcessor(1, + &fmp, + cricket::MPD_TX)); + EXPECT_TRUE(fme_->voice_processor_registered(cricket::MPD_TX)); + EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_RX)); + + EXPECT_TRUE(cm_->UnregisterVoiceProcessor(1, + &fmp, + cricket::MPD_TX)); + EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_TX)); + EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_RX)); +} + +TEST_F(ChannelManagerTest, SetVideoRtxEnabled) { + std::vector codecs; + const VideoCodec rtx_codec(96, "rtx", 0, 0, 0, 0); + + // By default RTX is disabled. + cm_->GetSupportedVideoCodecs(&codecs); + EXPECT_FALSE(ContainsMatchingCodec(codecs, rtx_codec)); + + // Enable and check. + EXPECT_TRUE(cm_->SetVideoRtxEnabled(true)); + cm_->GetSupportedVideoCodecs(&codecs); + EXPECT_TRUE(ContainsMatchingCodec(codecs, rtx_codec)); + + // Disable and check. + EXPECT_TRUE(cm_->SetVideoRtxEnabled(false)); + cm_->GetSupportedVideoCodecs(&codecs); + EXPECT_FALSE(ContainsMatchingCodec(codecs, rtx_codec)); + + // Cannot toggle rtx after initialization. + EXPECT_TRUE(cm_->Init()); + EXPECT_FALSE(cm_->SetVideoRtxEnabled(true)); + EXPECT_FALSE(cm_->SetVideoRtxEnabled(false)); + + // Can set again after terminate. + cm_->Terminate(); + EXPECT_TRUE(cm_->SetVideoRtxEnabled(true)); + cm_->GetSupportedVideoCodecs(&codecs); + EXPECT_TRUE(ContainsMatchingCodec(codecs, rtx_codec)); +} + +} // namespace cricket diff --git a/talk/session/media/currentspeakermonitor.cc b/talk/session/media/currentspeakermonitor.cc new file mode 100644 index 000000000..1f3e0938f --- /dev/null +++ b/talk/session/media/currentspeakermonitor.cc @@ -0,0 +1,208 @@ +/* + * libjingle + * Copyright 2011 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/media/currentspeakermonitor.h" + +#include "talk/base/logging.h" +#include "talk/session/media/call.h" + +namespace cricket { + +namespace { +const int kMaxAudioLevel = 9; +// To avoid overswitching, we disable switching for a period of time after a +// switch is done. +const int kDefaultMinTimeBetweenSwitches = 1000; +} + +CurrentSpeakerMonitor::CurrentSpeakerMonitor(Call* call, BaseSession* session) + : started_(false), + call_(call), + session_(session), + current_speaker_ssrc_(0), + earliest_permitted_switch_time_(0), + min_time_between_switches_(kDefaultMinTimeBetweenSwitches) { +} + +CurrentSpeakerMonitor::~CurrentSpeakerMonitor() { + Stop(); +} + +void CurrentSpeakerMonitor::Start() { + if (!started_) { + call_->SignalAudioMonitor.connect( + this, &CurrentSpeakerMonitor::OnAudioMonitor); + call_->SignalMediaStreamsUpdate.connect( + this, &CurrentSpeakerMonitor::OnMediaStreamsUpdate); + + started_ = true; + } +} + +void CurrentSpeakerMonitor::Stop() { + if (started_) { + call_->SignalAudioMonitor.disconnect(this); + call_->SignalMediaStreamsUpdate.disconnect(this); + + started_ = false; + ssrc_to_speaking_state_map_.clear(); + current_speaker_ssrc_ = 0; + earliest_permitted_switch_time_ = 0; + } +} + +void CurrentSpeakerMonitor::set_min_time_between_switches( + uint32 min_time_between_switches) { + min_time_between_switches_ = min_time_between_switches; +} + +void CurrentSpeakerMonitor::OnAudioMonitor(Call* call, const AudioInfo& info) { + std::map active_ssrc_to_level_map; + cricket::AudioInfo::StreamList::const_iterator stream_list_it; + for (stream_list_it = info.active_streams.begin(); + stream_list_it != info.active_streams.end(); ++stream_list_it) { + uint32 ssrc = stream_list_it->first; + active_ssrc_to_level_map[ssrc] = stream_list_it->second; + + // It's possible we haven't yet added this source to our map. If so, + // add it now with a "not speaking" state. + if (ssrc_to_speaking_state_map_.find(ssrc) == + ssrc_to_speaking_state_map_.end()) { + ssrc_to_speaking_state_map_[ssrc] = SS_NOT_SPEAKING; + } + } + + int max_level = 0; + uint32 loudest_speaker_ssrc = 0; + + // Update the speaking states of all participants based on the new audio + // level information. Also retain loudest speaker. + std::map::iterator state_it; + for (state_it = ssrc_to_speaking_state_map_.begin(); + state_it != ssrc_to_speaking_state_map_.end(); ++state_it) { + bool is_previous_speaker = current_speaker_ssrc_ == state_it->first; + + // This uses a state machine in order to gradually identify + // members as having started or stopped speaking. Matches the + // algorithm used by the hangouts js code. + + std::map::const_iterator level_it = + active_ssrc_to_level_map.find(state_it->first); + // Note that the stream map only contains streams with non-zero audio + // levels. + int level = (level_it != active_ssrc_to_level_map.end()) ? + level_it->second : 0; + switch (state_it->second) { + case SS_NOT_SPEAKING: + if (level > 0) { + // Reset level because we don't think they're really speaking. + level = 0; + state_it->second = SS_MIGHT_BE_SPEAKING; + } else { + // State unchanged. + } + break; + case SS_MIGHT_BE_SPEAKING: + if (level > 0) { + state_it->second = SS_SPEAKING; + } else { + state_it->second = SS_NOT_SPEAKING; + } + break; + case SS_SPEAKING: + if (level > 0) { + // State unchanged. + } else { + state_it->second = SS_WAS_SPEAKING_RECENTLY1; + if (is_previous_speaker) { + // Assume this is an inter-word silence and assign him the highest + // volume. + level = kMaxAudioLevel; + } + } + break; + case SS_WAS_SPEAKING_RECENTLY1: + if (level > 0) { + state_it->second = SS_SPEAKING; + } else { + state_it->second = SS_WAS_SPEAKING_RECENTLY2; + if (is_previous_speaker) { + // Assume this is an inter-word silence and assign him the highest + // volume. + level = kMaxAudioLevel; + } + } + break; + case SS_WAS_SPEAKING_RECENTLY2: + if (level > 0) { + state_it->second = SS_SPEAKING; + } else { + state_it->second = SS_NOT_SPEAKING; + } + break; + } + + if (level > max_level) { + loudest_speaker_ssrc = state_it->first; + max_level = level; + } else if (level > 0 && level == max_level && is_previous_speaker) { + // Favor continuity of loudest speakers if audio levels are equal. + loudest_speaker_ssrc = state_it->first; + } + } + + // We avoid over-switching by disabling switching for a period of time after + // a switch is done. + uint32 now = talk_base::Time(); + if (earliest_permitted_switch_time_ <= now && + current_speaker_ssrc_ != loudest_speaker_ssrc) { + current_speaker_ssrc_ = loudest_speaker_ssrc; + LOG(LS_INFO) << "Current speaker changed to " << current_speaker_ssrc_; + earliest_permitted_switch_time_ = now + min_time_between_switches_; + SignalUpdate(this, current_speaker_ssrc_); + } +} + +void CurrentSpeakerMonitor::OnMediaStreamsUpdate(Call* call, + Session* session, + const MediaStreams& added, + const MediaStreams& removed) { + if (call == call_ && session == session_) { + // Update the speaking state map based on added and removed streams. + for (std::vector::const_iterator + it = removed.video().begin(); it != removed.video().end(); ++it) { + ssrc_to_speaking_state_map_.erase(it->first_ssrc()); + } + + for (std::vector::const_iterator + it = added.video().begin(); it != added.video().end(); ++it) { + ssrc_to_speaking_state_map_[it->first_ssrc()] = SS_NOT_SPEAKING; + } + } +} + +} // namespace cricket diff --git a/talk/session/media/currentspeakermonitor.h b/talk/session/media/currentspeakermonitor.h new file mode 100644 index 000000000..1781db58c --- /dev/null +++ b/talk/session/media/currentspeakermonitor.h @@ -0,0 +1,100 @@ +/* + * libjingle + * Copyright 2011 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. + */ + +// CurrentSpeakerMonitor monitors the audio levels for a session and determines +// which participant is currently speaking. + +#ifndef TALK_SESSION_MEDIA_CURRENTSPEAKERMONITOR_H_ +#define TALK_SESSION_MEDIA_CURRENTSPEAKERMONITOR_H_ + +#include + +#include "talk/base/basictypes.h" +#include "talk/base/sigslot.h" + +namespace cricket { + +class BaseSession; +class Call; +class Session; +struct AudioInfo; +struct MediaStreams; + +// Note that the call's audio monitor must be started before this is started. +// It's recommended that the audio monitor be started with a 100 ms period. +class CurrentSpeakerMonitor : public sigslot::has_slots<> { + public: + CurrentSpeakerMonitor(Call* call, BaseSession* session); + ~CurrentSpeakerMonitor(); + + BaseSession* session() const { return session_; } + + void Start(); + void Stop(); + + // Used by tests. Note that the actual minimum time between switches + // enforced by the monitor will be the given value plus or minus the + // resolution of the system clock. + void set_min_time_between_switches(uint32 min_time_between_switches); + + // This is fired when the current speaker changes, and provides his audio + // SSRC. This only fires after the audio monitor on the underlying Call has + // been started. + sigslot::signal2 SignalUpdate; + + private: + void OnAudioMonitor(Call* call, const AudioInfo& info); + void OnMediaStreamsUpdate(Call* call, + Session* session, + const MediaStreams& added, + const MediaStreams& removed); + + // These are states that a participant will pass through so that we gradually + // recognize that they have started and stopped speaking. This avoids + // "twitchiness". + enum SpeakingState { + SS_NOT_SPEAKING, + SS_MIGHT_BE_SPEAKING, + SS_SPEAKING, + SS_WAS_SPEAKING_RECENTLY1, + SS_WAS_SPEAKING_RECENTLY2 + }; + + bool started_; + Call* call_; + BaseSession* session_; + std::map ssrc_to_speaking_state_map_; + uint32 current_speaker_ssrc_; + // To prevent overswitching, switching is disabled for some time after a + // switch is made. This gives us the earliest time a switch is permitted. + uint32 earliest_permitted_switch_time_; + uint32 min_time_between_switches_; +}; + +} + +#endif // TALK_SESSION_MEDIA_CURRENTSPEAKERMONITOR_H_ diff --git a/talk/session/media/currentspeakermonitor_unittest.cc b/talk/session/media/currentspeakermonitor_unittest.cc new file mode 100644 index 000000000..1306f8940 --- /dev/null +++ b/talk/session/media/currentspeakermonitor_unittest.cc @@ -0,0 +1,232 @@ +/* + * libjingle + * Copyright 2004 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/base/gunit.h" +#include "talk/base/thread.h" +#include "talk/session/media/call.h" +#include "talk/session/media/currentspeakermonitor.h" + +namespace cricket { + +static const uint32 kSsrc1 = 1001; +static const uint32 kSsrc2 = 1002; +static const uint32 kMinTimeBetweenSwitches = 10; +// Due to limited system clock resolution, the CurrentSpeakerMonitor may +// actually require more or less time between switches than that specified +// in the call to set_min_time_between_switches. To be safe, we sleep for +// 90 ms more than the min time between switches before checking for a switch. +// I am assuming system clocks do not have a coarser resolution than 90 ms. +static const uint32 kSleepTimeBetweenSwitches = 100; + +class MockCall : public Call { + public: + MockCall() : Call(NULL) {} + + void EmitAudioMonitor(const AudioInfo& info) { + SignalAudioMonitor(this, info); + } +}; + +class CurrentSpeakerMonitorTest : public testing::Test, + public sigslot::has_slots<> { + public: + CurrentSpeakerMonitorTest() { + call_ = new MockCall(); + monitor_ = new CurrentSpeakerMonitor(call_, NULL); + // Shrink the minimum time betweeen switches to 10 ms so we don't have to + // slow down our tests. + monitor_->set_min_time_between_switches(kMinTimeBetweenSwitches); + monitor_->SignalUpdate.connect(this, &CurrentSpeakerMonitorTest::OnUpdate); + current_speaker_ = 0; + num_changes_ = 0; + monitor_->Start(); + } + + ~CurrentSpeakerMonitorTest() { + delete monitor_; + delete call_; + } + + protected: + MockCall* call_; + CurrentSpeakerMonitor* monitor_; + int num_changes_; + uint32 current_speaker_; + + void OnUpdate(CurrentSpeakerMonitor* monitor, uint32 current_speaker) { + current_speaker_ = current_speaker; + num_changes_++; + } +}; + +static void InitAudioInfo(AudioInfo* info, int input_level, int output_level) { + info->input_level = input_level; + info->output_level = output_level; +} + +TEST_F(CurrentSpeakerMonitorTest, NoActiveStreams) { + AudioInfo info; + InitAudioInfo(&info, 0, 0); + call_->EmitAudioMonitor(info); + + EXPECT_EQ(current_speaker_, 0U); + EXPECT_EQ(num_changes_, 0); +} + +TEST_F(CurrentSpeakerMonitorTest, MultipleActiveStreams) { + AudioInfo info; + InitAudioInfo(&info, 0, 0); + + info.active_streams.push_back(std::make_pair(kSsrc1, 3)); + info.active_streams.push_back(std::make_pair(kSsrc2, 7)); + call_->EmitAudioMonitor(info); + + // No speaker recognized because the initial sample is treated as possibly + // just noise and disregarded. + EXPECT_EQ(current_speaker_, 0U); + EXPECT_EQ(num_changes_, 0); + + info.active_streams.push_back(std::make_pair(kSsrc1, 3)); + info.active_streams.push_back(std::make_pair(kSsrc2, 7)); + call_->EmitAudioMonitor(info); + + EXPECT_EQ(current_speaker_, kSsrc2); + EXPECT_EQ(num_changes_, 1); +} + +TEST_F(CurrentSpeakerMonitorTest, RapidSpeakerChange) { + AudioInfo info; + InitAudioInfo(&info, 0, 0); + + info.active_streams.push_back(std::make_pair(kSsrc1, 3)); + info.active_streams.push_back(std::make_pair(kSsrc2, 7)); + call_->EmitAudioMonitor(info); + + EXPECT_EQ(current_speaker_, 0U); + EXPECT_EQ(num_changes_, 0); + + info.active_streams.push_back(std::make_pair(kSsrc1, 3)); + info.active_streams.push_back(std::make_pair(kSsrc2, 7)); + call_->EmitAudioMonitor(info); + + EXPECT_EQ(current_speaker_, kSsrc2); + EXPECT_EQ(num_changes_, 1); + + info.active_streams.push_back(std::make_pair(kSsrc1, 9)); + info.active_streams.push_back(std::make_pair(kSsrc2, 1)); + call_->EmitAudioMonitor(info); + + // We expect no speaker change because of the rapid change. + EXPECT_EQ(current_speaker_, kSsrc2); + EXPECT_EQ(num_changes_, 1); +} + +TEST_F(CurrentSpeakerMonitorTest, SpeakerChange) { + AudioInfo info; + InitAudioInfo(&info, 0, 0); + + info.active_streams.push_back(std::make_pair(kSsrc1, 3)); + info.active_streams.push_back(std::make_pair(kSsrc2, 7)); + call_->EmitAudioMonitor(info); + + EXPECT_EQ(current_speaker_, 0U); + EXPECT_EQ(num_changes_, 0); + + info.active_streams.push_back(std::make_pair(kSsrc1, 3)); + info.active_streams.push_back(std::make_pair(kSsrc2, 7)); + call_->EmitAudioMonitor(info); + + EXPECT_EQ(current_speaker_, kSsrc2); + EXPECT_EQ(num_changes_, 1); + + // Wait so the changes don't come so rapidly. + talk_base::Thread::SleepMs(kSleepTimeBetweenSwitches); + + info.active_streams.push_back(std::make_pair(kSsrc1, 9)); + info.active_streams.push_back(std::make_pair(kSsrc2, 1)); + call_->EmitAudioMonitor(info); + + EXPECT_EQ(current_speaker_, kSsrc1); + EXPECT_EQ(num_changes_, 2); +} + +TEST_F(CurrentSpeakerMonitorTest, InterwordSilence) { + AudioInfo info; + InitAudioInfo(&info, 0, 0); + + info.active_streams.push_back(std::make_pair(kSsrc1, 3)); + info.active_streams.push_back(std::make_pair(kSsrc2, 7)); + call_->EmitAudioMonitor(info); + + EXPECT_EQ(current_speaker_, 0U); + EXPECT_EQ(num_changes_, 0); + + info.active_streams.push_back(std::make_pair(kSsrc1, 3)); + info.active_streams.push_back(std::make_pair(kSsrc2, 7)); + call_->EmitAudioMonitor(info); + + EXPECT_EQ(current_speaker_, kSsrc2); + EXPECT_EQ(num_changes_, 1); + + info.active_streams.push_back(std::make_pair(kSsrc1, 3)); + info.active_streams.push_back(std::make_pair(kSsrc2, 7)); + call_->EmitAudioMonitor(info); + + EXPECT_EQ(current_speaker_, kSsrc2); + EXPECT_EQ(num_changes_, 1); + + // Wait so the changes don't come so rapidly. + talk_base::Thread::SleepMs(kSleepTimeBetweenSwitches); + + info.active_streams.push_back(std::make_pair(kSsrc1, 3)); + info.active_streams.push_back(std::make_pair(kSsrc2, 0)); + call_->EmitAudioMonitor(info); + + // Current speaker shouldn't have changed because we treat this as an inter- + // word silence. + EXPECT_EQ(current_speaker_, kSsrc2); + EXPECT_EQ(num_changes_, 1); + + info.active_streams.push_back(std::make_pair(kSsrc1, 3)); + info.active_streams.push_back(std::make_pair(kSsrc2, 0)); + call_->EmitAudioMonitor(info); + + // Current speaker shouldn't have changed because we treat this as an inter- + // word silence. + EXPECT_EQ(current_speaker_, kSsrc2); + EXPECT_EQ(num_changes_, 1); + + info.active_streams.push_back(std::make_pair(kSsrc1, 3)); + info.active_streams.push_back(std::make_pair(kSsrc2, 0)); + call_->EmitAudioMonitor(info); + + // At this point, we should have concluded that SSRC2 stopped speaking. + EXPECT_EQ(current_speaker_, kSsrc1); + EXPECT_EQ(num_changes_, 2); +} + +} // namespace cricket diff --git a/talk/session/media/mediamessages.cc b/talk/session/media/mediamessages.cc new file mode 100644 index 000000000..6b5d03cf9 --- /dev/null +++ b/talk/session/media/mediamessages.cc @@ -0,0 +1,394 @@ +/* + * libjingle + * Copyright 2010 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. + */ + +/* + * Documentation is in mediamessages.h. + */ + +#include "talk/session/media/mediamessages.h" + +#include "talk/base/logging.h" +#include "talk/base/stringencode.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/parsing.h" +#include "talk/session/media/mediasessionclient.h" +#include "talk/xmllite/xmlelement.h" + +namespace cricket { + +namespace { + +// NOTE: There is no check here for duplicate streams, so check before +// adding. +void AddStream(std::vector* streams, const StreamParams& stream) { + streams->push_back(stream); +} + +bool ParseSsrc(const std::string& string, uint32* ssrc) { + return talk_base::FromString(string, ssrc); +} + +bool ParseSsrc(const buzz::XmlElement* element, uint32* ssrc) { + if (element == NULL) { + return false; + } + return ParseSsrc(element->BodyText(), ssrc); +} + +// Builds a element according to the following spec: +// goto/jinglemuc +buzz::XmlElement* CreateViewElem(const std::string& name, + const std::string& type) { + buzz::XmlElement* view_elem = + new buzz::XmlElement(QN_JINGLE_DRAFT_VIEW, true); + view_elem->AddAttr(QN_NAME, name); + view_elem->SetAttr(QN_TYPE, type); + return view_elem; +} + +buzz::XmlElement* CreateVideoViewElem(const std::string& content_name, + const std::string& type) { + return CreateViewElem(content_name, type); +} + +buzz::XmlElement* CreateNoneVideoViewElem(const std::string& content_name) { + return CreateVideoViewElem(content_name, STR_JINGLE_DRAFT_VIEW_TYPE_NONE); +} + +buzz::XmlElement* CreateStaticVideoViewElem(const std::string& content_name, + const StaticVideoView& view) { + buzz::XmlElement* view_elem = + CreateVideoViewElem(content_name, STR_JINGLE_DRAFT_VIEW_TYPE_STATIC); + AddXmlAttr(view_elem, QN_SSRC, view.selector.ssrc); + + buzz::XmlElement* params_elem = new buzz::XmlElement(QN_JINGLE_DRAFT_PARAMS); + AddXmlAttr(params_elem, QN_WIDTH, view.width); + AddXmlAttr(params_elem, QN_HEIGHT, view.height); + AddXmlAttr(params_elem, QN_FRAMERATE, view.framerate); + AddXmlAttr(params_elem, QN_PREFERENCE, view.preference); + view_elem->AddElement(params_elem); + + return view_elem; +} + +} // namespace + +bool MediaStreams::GetAudioStream( + const StreamSelector& selector, StreamParams* stream) { + return GetStream(audio_, selector, stream); +} + +bool MediaStreams::GetVideoStream( + const StreamSelector& selector, StreamParams* stream) { + return GetStream(video_, selector, stream); +} + +bool MediaStreams::GetDataStream( + const StreamSelector& selector, StreamParams* stream) { + return GetStream(data_, selector, stream); +} + +void MediaStreams::CopyFrom(const MediaStreams& streams) { + audio_ = streams.audio_; + video_ = streams.video_; + data_ = streams.data_; +} + +void MediaStreams::AddAudioStream(const StreamParams& stream) { + AddStream(&audio_, stream); +} + +void MediaStreams::AddVideoStream(const StreamParams& stream) { + AddStream(&video_, stream); +} + +void MediaStreams::AddDataStream(const StreamParams& stream) { + AddStream(&data_, stream); +} + +bool MediaStreams::RemoveAudioStream( + const StreamSelector& selector) { + return RemoveStream(&audio_, selector); +} + +bool MediaStreams::RemoveVideoStream( + const StreamSelector& selector) { + return RemoveStream(&video_, selector); +} + +bool MediaStreams::RemoveDataStream( + const StreamSelector& selector) { + return RemoveStream(&data_, selector); +} + +bool IsJingleViewRequest(const buzz::XmlElement* action_elem) { + return action_elem->FirstNamed(QN_JINGLE_DRAFT_VIEW) != NULL; +} + +bool ParseStaticVideoView(const buzz::XmlElement* view_elem, + StaticVideoView* view, + ParseError* error) { + uint32 ssrc; + if (!ParseSsrc(view_elem->Attr(QN_SSRC), &ssrc)) { + return BadParse("Invalid or missing view ssrc.", error); + } + view->selector = StreamSelector(ssrc); + + const buzz::XmlElement* params_elem = + view_elem->FirstNamed(QN_JINGLE_DRAFT_PARAMS); + if (params_elem) { + view->width = GetXmlAttr(params_elem, QN_WIDTH, 0); + view->height = GetXmlAttr(params_elem, QN_HEIGHT, 0); + view->framerate = GetXmlAttr(params_elem, QN_FRAMERATE, 0); + view->preference = GetXmlAttr(params_elem, QN_PREFERENCE, 0); + } else { + return BadParse("Missing view params.", error); + } + + return true; +} + +bool ParseJingleViewRequest(const buzz::XmlElement* action_elem, + ViewRequest* view_request, + ParseError* error) { + for (const buzz::XmlElement* view_elem = + action_elem->FirstNamed(QN_JINGLE_DRAFT_VIEW); + view_elem != NULL; + view_elem = view_elem->NextNamed(QN_JINGLE_DRAFT_VIEW)) { + std::string type = view_elem->Attr(QN_TYPE); + if (STR_JINGLE_DRAFT_VIEW_TYPE_NONE == type) { + view_request->static_video_views.clear(); + return true; + } else if (STR_JINGLE_DRAFT_VIEW_TYPE_STATIC == type) { + StaticVideoView static_video_view(StreamSelector(0), 0, 0, 0); + if (!ParseStaticVideoView(view_elem, &static_video_view, error)) { + return false; + } + view_request->static_video_views.push_back(static_video_view); + } else { + LOG(LS_INFO) << "Ingnoring unknown view type: " << type; + } + } + return true; +} + +bool WriteJingleViewRequest(const std::string& content_name, + const ViewRequest& request, + XmlElements* elems, + WriteError* error) { + if (request.static_video_views.empty()) { + elems->push_back(CreateNoneVideoViewElem(content_name)); + } else { + for (StaticVideoViews::const_iterator view = + request.static_video_views.begin(); + view != request.static_video_views.end(); ++view) { + elems->push_back(CreateStaticVideoViewElem(content_name, *view)); + } + } + return true; +} + +bool ParseSsrcAsLegacyStream(const buzz::XmlElement* desc_elem, + std::vector* streams, + ParseError* error) { + const std::string ssrc_str = desc_elem->Attr(QN_SSRC); + if (!ssrc_str.empty()) { + uint32 ssrc; + if (!ParseSsrc(ssrc_str, &ssrc)) { + return BadParse("Missing or invalid ssrc.", error); + } + + streams->push_back(StreamParams::CreateLegacy(ssrc)); + } + return true; +} + +bool ParseSsrcs(const buzz::XmlElement* parent_elem, + std::vector* ssrcs, + ParseError* error) { + for (const buzz::XmlElement* ssrc_elem = + parent_elem->FirstNamed(QN_JINGLE_DRAFT_SSRC); + ssrc_elem != NULL; + ssrc_elem = ssrc_elem->NextNamed(QN_JINGLE_DRAFT_SSRC)) { + uint32 ssrc; + if (!ParseSsrc(ssrc_elem->BodyText(), &ssrc)) { + return BadParse("Missing or invalid ssrc.", error); + } + + ssrcs->push_back(ssrc); + } + return true; +} + +bool ParseSsrcGroups(const buzz::XmlElement* parent_elem, + std::vector* ssrc_groups, + ParseError* error) { + for (const buzz::XmlElement* group_elem = + parent_elem->FirstNamed(QN_JINGLE_DRAFT_SSRC_GROUP); + group_elem != NULL; + group_elem = group_elem->NextNamed(QN_JINGLE_DRAFT_SSRC_GROUP)) { + std::string semantics = group_elem->Attr(QN_SEMANTICS); + std::vector ssrcs; + if (!ParseSsrcs(group_elem, &ssrcs, error)) { + return false; + } + ssrc_groups->push_back(SsrcGroup(semantics, ssrcs)); + } + return true; +} + +bool ParseJingleStream(const buzz::XmlElement* stream_elem, + std::vector* streams, + ParseError* error) { + StreamParams stream; + // We treat the nick as a stream groupid. + stream.groupid = stream_elem->Attr(QN_NICK); + stream.id = stream_elem->Attr(QN_NAME); + stream.type = stream_elem->Attr(QN_TYPE); + stream.display = stream_elem->Attr(QN_DISPLAY); + stream.cname = stream_elem->Attr(QN_CNAME); + if (!ParseSsrcs(stream_elem, &(stream.ssrcs), error)) { + return false; + } + std::vector ssrc_groups; + if (!ParseSsrcGroups(stream_elem, &(stream.ssrc_groups), error)) { + return false; + } + streams->push_back(stream); + return true; +} + +bool ParseJingleRtpHeaderExtensions(const buzz::XmlElement* parent_elem, + std::vector* hdrexts, + ParseError* error) { + for (const buzz::XmlElement* hdrext_elem = + parent_elem->FirstNamed(QN_JINGLE_RTP_HDREXT); + hdrext_elem != NULL; + hdrext_elem = hdrext_elem->NextNamed(QN_JINGLE_RTP_HDREXT)) { + std::string uri = hdrext_elem->Attr(QN_URI); + int id = GetXmlAttr(hdrext_elem, QN_ID, 0); + if (id <= 0) { + return BadParse("Invalid RTP header extension id.", error); + } + hdrexts->push_back(RtpHeaderExtension(uri, id)); + } + return true; +} + +bool HasJingleStreams(const buzz::XmlElement* desc_elem) { + const buzz::XmlElement* streams_elem = + desc_elem->FirstNamed(QN_JINGLE_DRAFT_STREAMS); + return (streams_elem != NULL); +} + +bool ParseJingleStreams(const buzz::XmlElement* desc_elem, + std::vector* streams, + ParseError* error) { + const buzz::XmlElement* streams_elem = + desc_elem->FirstNamed(QN_JINGLE_DRAFT_STREAMS); + if (streams_elem == NULL) { + return BadParse("Missing streams element.", error); + } + for (const buzz::XmlElement* stream_elem = + streams_elem->FirstNamed(QN_JINGLE_DRAFT_STREAM); + stream_elem != NULL; + stream_elem = stream_elem->NextNamed(QN_JINGLE_DRAFT_STREAM)) { + if (!ParseJingleStream(stream_elem, streams, error)) { + return false; + } + } + return true; +} + +void WriteSsrcs(const std::vector& ssrcs, + buzz::XmlElement* parent_elem) { + for (std::vector::const_iterator ssrc = ssrcs.begin(); + ssrc != ssrcs.end(); ++ssrc) { + buzz::XmlElement* ssrc_elem = + new buzz::XmlElement(QN_JINGLE_DRAFT_SSRC, false); + SetXmlBody(ssrc_elem, *ssrc); + + parent_elem->AddElement(ssrc_elem); + } +} + +void WriteSsrcGroups(const std::vector& groups, + buzz::XmlElement* parent_elem) { + for (std::vector::const_iterator group = groups.begin(); + group != groups.end(); ++group) { + buzz::XmlElement* group_elem = + new buzz::XmlElement(QN_JINGLE_DRAFT_SSRC_GROUP, false); + AddXmlAttrIfNonEmpty(group_elem, QN_SEMANTICS, group->semantics); + WriteSsrcs(group->ssrcs, group_elem); + + parent_elem->AddElement(group_elem); + } +} + +void WriteJingleStream(const StreamParams& stream, + buzz::XmlElement* parent_elem) { + buzz::XmlElement* stream_elem = + new buzz::XmlElement(QN_JINGLE_DRAFT_STREAM, false); + // We treat the nick as a stream groupid. + AddXmlAttrIfNonEmpty(stream_elem, QN_NICK, stream.groupid); + AddXmlAttrIfNonEmpty(stream_elem, QN_NAME, stream.id); + AddXmlAttrIfNonEmpty(stream_elem, QN_TYPE, stream.type); + AddXmlAttrIfNonEmpty(stream_elem, QN_DISPLAY, stream.display); + AddXmlAttrIfNonEmpty(stream_elem, QN_CNAME, stream.cname); + WriteSsrcs(stream.ssrcs, stream_elem); + WriteSsrcGroups(stream.ssrc_groups, stream_elem); + + parent_elem->AddElement(stream_elem); +} + +void WriteJingleStreams(const std::vector& streams, + buzz::XmlElement* parent_elem) { + buzz::XmlElement* streams_elem = + new buzz::XmlElement(QN_JINGLE_DRAFT_STREAMS, true); + for (std::vector::const_iterator stream = streams.begin(); + stream != streams.end(); ++stream) { + WriteJingleStream(*stream, streams_elem); + } + + parent_elem->AddElement(streams_elem); +} + +void WriteJingleRtpHeaderExtensions( + const std::vector& hdrexts, + buzz::XmlElement* parent_elem) { + for (std::vector::const_iterator hdrext = hdrexts.begin(); + hdrext != hdrexts.end(); ++hdrext) { + buzz::XmlElement* hdrext_elem = + new buzz::XmlElement(QN_JINGLE_RTP_HDREXT, false); + AddXmlAttr(hdrext_elem, QN_URI, hdrext->uri); + AddXmlAttr(hdrext_elem, QN_ID, hdrext->id); + parent_elem->AddElement(hdrext_elem); + } +} + + +} // namespace cricket diff --git a/talk/session/media/mediamessages.h b/talk/session/media/mediamessages.h new file mode 100644 index 000000000..dcb48a85a --- /dev/null +++ b/talk/session/media/mediamessages.h @@ -0,0 +1,169 @@ +/* + * libjingle + * Copyright 2010 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. + */ + +/* + * A collection of functions and types for serializing and + * deserializing Jingle session messages related to media. + * Specificially, the and messages. They are not yet + * standardized, but their current documentation can be found at: + * goto/jinglemuc + */ + +#ifndef TALK_SESSION_MEDIA_MEDIAMESSAGES_H_ +#define TALK_SESSION_MEDIA_MEDIAMESSAGES_H_ + +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/media/base/mediachannel.h" // For RtpHeaderExtension +#include "talk/media/base/streamparams.h" +#include "talk/p2p/base/parsing.h" +#include "talk/p2p/base/sessiondescription.h" + +namespace cricket { + +// A collection of audio and video and data streams. Most of the +// methods are merely for convenience. Many of these methods are keyed +// by ssrc, which is the source identifier in the RTP spec +// (http://tools.ietf.org/html/rfc3550). +struct MediaStreams { + public: + MediaStreams() {} + void CopyFrom(const MediaStreams& sources); + + bool empty() const { + return audio_.empty() && video_.empty() && data_.empty(); + } + + std::vector* mutable_audio() { return &audio_; } + std::vector* mutable_video() { return &video_; } + std::vector* mutable_data() { return &data_; } + const std::vector& audio() const { return audio_; } + const std::vector& video() const { return video_; } + const std::vector& data() const { return data_; } + + // Gets a stream, returning true if found. + bool GetAudioStream( + const StreamSelector& selector, StreamParams* stream); + bool GetVideoStream( + const StreamSelector& selector, StreamParams* stream); + bool GetDataStream( + const StreamSelector& selector, StreamParams* stream); + // Adds a stream. + void AddAudioStream(const StreamParams& stream); + void AddVideoStream(const StreamParams& stream); + void AddDataStream(const StreamParams& stream); + // Removes a stream, returning true if found and removed. + bool RemoveAudioStream(const StreamSelector& selector); + bool RemoveVideoStream(const StreamSelector& selector); + bool RemoveDataStream(const StreamSelector& selector); + + private: + std::vector audio_; + std::vector video_; + std::vector data_; + + DISALLOW_COPY_AND_ASSIGN(MediaStreams); +}; + +// In a message, there are a number of views specified. This +// represents one such view. We currently only support "static" +// views. +struct StaticVideoView { + StaticVideoView(const StreamSelector& selector, + int width, int height, int framerate) + : selector(selector), + width(width), + height(height), + framerate(framerate), + preference(0) { + } + + StreamSelector selector; + int width; + int height; + int framerate; + int preference; +}; + +typedef std::vector StaticVideoViews; + +// Represents a whole view request message, which contains many views. +struct ViewRequest { + StaticVideoViews static_video_views; +}; + +// If the parent element (usually ) is a jingle view. +bool IsJingleViewRequest(const buzz::XmlElement* action_elem); + +// Parses a view request from the parent element (usually +// ). If it fails, it returns false and fills an error +// message. +bool ParseJingleViewRequest(const buzz::XmlElement* action_elem, + ViewRequest* view_request, + ParseError* error); + +// Serializes a view request to XML. If it fails, returns false and +// fills in an error message. +bool WriteJingleViewRequest(const std::string& content_name, + const ViewRequest& view, + XmlElements* elems, + WriteError* error); + +// TODO(pthatcher): Get rid of legacy source notify and replace with +// description-info as soon as reflector is capable of sending it. +bool IsSourcesNotify(const buzz::XmlElement* action_elem); + +// If the given elem has . +bool HasJingleStreams(const buzz::XmlElement* desc_elem); + +// Parses streams from a jingle . If it fails, returns +// false and fills an error message. +bool ParseJingleStreams(const buzz::XmlElement* desc_elem, + std::vector* streams, + ParseError* error); + +// Write a element to the parent_elem. +void WriteJingleStreams(const std::vector& streams, + buzz::XmlElement* parent_elem); + +// Parses rtp header extensions from a jingle . If it +// fails, returns false and fills an error message. +bool ParseJingleRtpHeaderExtensions( + const buzz::XmlElement* desc_elem, + std::vector* hdrexts, + ParseError* error); + +// Writes elements to the parent_elem. +void WriteJingleRtpHeaderExtensions( + const std::vector& hdrexts, + buzz::XmlElement* parent_elem); + +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_MEDIAMESSAGES_H_ diff --git a/talk/session/media/mediamessages_unittest.cc b/talk/session/media/mediamessages_unittest.cc new file mode 100644 index 000000000..4c76be31c --- /dev/null +++ b/talk/session/media/mediamessages_unittest.cc @@ -0,0 +1,352 @@ +/* + * libjingle + * Copyright 2004 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/media/mediamessages.h" + +#include +#include + +#include "talk/base/gunit.h" +#include "talk/base/scoped_ptr.h" +#include "talk/p2p/base/constants.h" +#include "talk/session/media/mediasessionclient.h" +#include "talk/xmllite/xmlelement.h" + +// Unit tests for mediamessages.cc. + +namespace cricket { + +namespace { + +static const char kViewVideoNoneXml[] = + ""; + +class MediaMessagesTest : public testing::Test { + public: + // CreateMediaSessionDescription uses a static variable cricket::NS_JINGLE_RTP + // defined in another file and cannot be used to initialize another static + // variable (http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.14) + MediaMessagesTest() + : remote_description_(CreateMediaSessionDescription("audio1", "video1")) { + } + + protected: + static std::string ViewVideoStaticVgaXml(const std::string& ssrc) { + return "" + "" + ""; + } + + static cricket::StreamParams CreateStream(const std::string& nick, + const std::string& name, + uint32 ssrc1, + uint32 ssrc2, + const std::string& semantics, + const std::string& type, + const std::string& display) { + StreamParams stream; + stream.groupid = nick; + stream.id = name; + stream.ssrcs.push_back(ssrc1); + stream.ssrcs.push_back(ssrc2); + stream.ssrc_groups.push_back( + cricket::SsrcGroup(semantics, stream.ssrcs)); + stream.type = type; + stream.display = display; + return stream; + } + + static std::string StreamsXml(const std::string& stream1, + const std::string& stream2) { + return "" + + stream1 + + stream2 + + ""; + } + + + static std::string StreamXml(const std::string& nick, + const std::string& name, + const std::string& ssrc1, + const std::string& ssrc2, + const std::string& semantics, + const std::string& type, + const std::string& display) { + return "" + "" + ssrc1 + "" + "" + ssrc2 + "" + "" + "" + ssrc1 + "" + "" + ssrc2 + "" + "" + ""; + } + + static std::string HeaderExtensionsXml(const std::string& hdrext1, + const std::string& hdrext2) { + return "" + + hdrext1 + + hdrext2 + + ""; + } + + static std::string HeaderExtensionXml(const std::string& uri, + const std::string& id) { + return ""; + } + + static cricket::SessionDescription* CreateMediaSessionDescription( + const std::string& audio_content_name, + const std::string& video_content_name) { + cricket::SessionDescription* desc = new cricket::SessionDescription(); + desc->AddContent(audio_content_name, cricket::NS_JINGLE_RTP, + new cricket::AudioContentDescription()); + desc->AddContent(video_content_name, cricket::NS_JINGLE_RTP, + new cricket::VideoContentDescription()); + return desc; + } + + talk_base::scoped_ptr remote_description_; +}; + +} // anonymous namespace + +// Test serializing/deserializing an empty message. +TEST_F(MediaMessagesTest, ViewNoneToFromXml) { + buzz::XmlElement* expected_view_elem = + buzz::XmlElement::ForStr(kViewVideoNoneXml); + talk_base::scoped_ptr action_elem( + new buzz::XmlElement(QN_JINGLE)); + + EXPECT_FALSE(cricket::IsJingleViewRequest(action_elem.get())); + action_elem->AddElement(expected_view_elem); + EXPECT_TRUE(cricket::IsJingleViewRequest(action_elem.get())); + + cricket::ViewRequest view_request; + cricket::XmlElements actual_view_elems; + cricket::WriteError error; + + ASSERT_TRUE(cricket::WriteJingleViewRequest( + "video1", view_request, &actual_view_elems, &error)); + + ASSERT_EQ(1U, actual_view_elems.size()); + EXPECT_EQ(expected_view_elem->Str(), actual_view_elems[0]->Str()); + + cricket::ParseError parse_error; + EXPECT_TRUE(cricket::IsJingleViewRequest(action_elem.get())); + ASSERT_TRUE(cricket::ParseJingleViewRequest( + action_elem.get(), &view_request, &parse_error)); + EXPECT_EQ(0U, view_request.static_video_views.size()); +} + +// Test serializing/deserializing an a simple vga message. +TEST_F(MediaMessagesTest, ViewVgaToFromXml) { + talk_base::scoped_ptr action_elem( + new buzz::XmlElement(QN_JINGLE)); + buzz::XmlElement* expected_view_elem1 = + buzz::XmlElement::ForStr(ViewVideoStaticVgaXml("1234")); + buzz::XmlElement* expected_view_elem2 = + buzz::XmlElement::ForStr(ViewVideoStaticVgaXml("2468")); + action_elem->AddElement(expected_view_elem1); + action_elem->AddElement(expected_view_elem2); + + cricket::ViewRequest view_request; + cricket::XmlElements actual_view_elems; + cricket::WriteError error; + + view_request.static_video_views.push_back(cricket::StaticVideoView( + cricket::StreamSelector(1234), 640, 480, 30)); + view_request.static_video_views.push_back(cricket::StaticVideoView( + cricket::StreamSelector(2468), 640, 480, 30)); + + ASSERT_TRUE(cricket::WriteJingleViewRequest( + "video1", view_request, &actual_view_elems, &error)); + + ASSERT_EQ(2U, actual_view_elems.size()); + EXPECT_EQ(expected_view_elem1->Str(), actual_view_elems[0]->Str()); + EXPECT_EQ(expected_view_elem2->Str(), actual_view_elems[1]->Str()); + + view_request.static_video_views.clear(); + cricket::ParseError parse_error; + EXPECT_TRUE(cricket::IsJingleViewRequest(action_elem.get())); + ASSERT_TRUE(cricket::ParseJingleViewRequest( + action_elem.get(), &view_request, &parse_error)); + EXPECT_EQ(2U, view_request.static_video_views.size()); + EXPECT_EQ(1234U, view_request.static_video_views[0].selector.ssrc); + EXPECT_EQ(640, view_request.static_video_views[0].width); + EXPECT_EQ(480, view_request.static_video_views[0].height); + EXPECT_EQ(30, view_request.static_video_views[0].framerate); + EXPECT_EQ(2468U, view_request.static_video_views[1].selector.ssrc); +} + +// Test deserializing bad view XML. +TEST_F(MediaMessagesTest, ParseBadViewXml) { + talk_base::scoped_ptr action_elem( + new buzz::XmlElement(QN_JINGLE)); + buzz::XmlElement* view_elem = + buzz::XmlElement::ForStr(ViewVideoStaticVgaXml("not-an-ssrc")); + action_elem->AddElement(view_elem); + + cricket::ViewRequest view_request; + cricket::ParseError parse_error; + ASSERT_FALSE(cricket::ParseJingleViewRequest( + action_elem.get(), &view_request, &parse_error)); +} + + +// Test serializing/deserializing typical streams xml. +TEST_F(MediaMessagesTest, StreamsToFromXml) { + talk_base::scoped_ptr expected_streams_elem( + buzz::XmlElement::ForStr( + StreamsXml( + StreamXml("nick1", "stream1", "101", "102", + "semantics1", "type1", "display1"), + StreamXml("nick2", "stream2", "201", "202", + "semantics2", "type2", "display2")))); + + std::vector expected_streams; + expected_streams.push_back(CreateStream("nick1", "stream1", 101U, 102U, + "semantics1", "type1", "display1")); + expected_streams.push_back(CreateStream("nick2", "stream2", 201U, 202U, + "semantics2", "type2", "display2")); + + talk_base::scoped_ptr actual_desc_elem( + new buzz::XmlElement(QN_JINGLE_RTP_CONTENT)); + cricket::WriteJingleStreams(expected_streams, actual_desc_elem.get()); + + const buzz::XmlElement* actual_streams_elem = + actual_desc_elem->FirstNamed(QN_JINGLE_DRAFT_STREAMS); + ASSERT_TRUE(actual_streams_elem != NULL); + EXPECT_EQ(expected_streams_elem->Str(), actual_streams_elem->Str()); + + talk_base::scoped_ptr expected_desc_elem( + new buzz::XmlElement(QN_JINGLE_RTP_CONTENT)); + expected_desc_elem->AddElement(new buzz::XmlElement( + *expected_streams_elem)); + std::vector actual_streams; + cricket::ParseError parse_error; + + EXPECT_TRUE(cricket::HasJingleStreams(expected_desc_elem.get())); + ASSERT_TRUE(cricket::ParseJingleStreams( + expected_desc_elem.get(), &actual_streams, &parse_error)); + EXPECT_EQ(2U, actual_streams.size()); + EXPECT_EQ(expected_streams[0], actual_streams[0]); + EXPECT_EQ(expected_streams[1], actual_streams[1]); +} + +// Test deserializing bad streams xml. +TEST_F(MediaMessagesTest, StreamsFromBadXml) { + talk_base::scoped_ptr streams_elem( + buzz::XmlElement::ForStr( + StreamsXml( + StreamXml("nick1", "name1", "101", "not-an-ssrc", + "semantics1", "type1", "display1"), + StreamXml("nick2", "name2", "202", "not-an-ssrc", + "semantics2", "type2", "display2")))); + talk_base::scoped_ptr desc_elem( + new buzz::XmlElement(QN_JINGLE_RTP_CONTENT)); + desc_elem->AddElement(new buzz::XmlElement(*streams_elem)); + + std::vector actual_streams; + cricket::ParseError parse_error; + ASSERT_FALSE(cricket::ParseJingleStreams( + desc_elem.get(), &actual_streams, &parse_error)); +} + +// Test serializing/deserializing typical RTP Header Extension xml. +TEST_F(MediaMessagesTest, HeaderExtensionsToFromXml) { + talk_base::scoped_ptr expected_desc_elem( + buzz::XmlElement::ForStr( + HeaderExtensionsXml( + HeaderExtensionXml("abc", "123"), + HeaderExtensionXml("def", "456")))); + + std::vector expected_hdrexts; + expected_hdrexts.push_back(RtpHeaderExtension("abc", 123)); + expected_hdrexts.push_back(RtpHeaderExtension("def", 456)); + + talk_base::scoped_ptr actual_desc_elem( + new buzz::XmlElement(QN_JINGLE_RTP_CONTENT)); + cricket::WriteJingleRtpHeaderExtensions(expected_hdrexts, actual_desc_elem.get()); + + ASSERT_TRUE(actual_desc_elem != NULL); + EXPECT_EQ(expected_desc_elem->Str(), actual_desc_elem->Str()); + + std::vector actual_hdrexts; + cricket::ParseError parse_error; + ASSERT_TRUE(cricket::ParseJingleRtpHeaderExtensions( + expected_desc_elem.get(), &actual_hdrexts, &parse_error)); + EXPECT_EQ(2U, actual_hdrexts.size()); + EXPECT_EQ(expected_hdrexts[0], actual_hdrexts[0]); + EXPECT_EQ(expected_hdrexts[1], actual_hdrexts[1]); +} + +// Test deserializing bad RTP header extension xml. +TEST_F(MediaMessagesTest, HeaderExtensionsFromBadXml) { + std::vector actual_hdrexts; + cricket::ParseError parse_error; + + talk_base::scoped_ptr desc_elem( + buzz::XmlElement::ForStr( + HeaderExtensionsXml( + HeaderExtensionXml("abc", "123"), + HeaderExtensionXml("def", "not-an-id")))); + ASSERT_FALSE(cricket::ParseJingleRtpHeaderExtensions( + desc_elem.get(), &actual_hdrexts, &parse_error)); + + desc_elem.reset( + buzz::XmlElement::ForStr( + HeaderExtensionsXml( + HeaderExtensionXml("abc", "123"), + HeaderExtensionXml("def", "-1")))); + ASSERT_FALSE(cricket::ParseJingleRtpHeaderExtensions( + desc_elem.get(), &actual_hdrexts, &parse_error)); +} + +} // namespace cricket diff --git a/talk/session/media/mediamonitor.cc b/talk/session/media/mediamonitor.cc new file mode 100644 index 000000000..844180eb8 --- /dev/null +++ b/talk/session/media/mediamonitor.cc @@ -0,0 +1,108 @@ +/* + * libjingle + * Copyright 2005 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/base/common.h" +#include "talk/session/media/channelmanager.h" +#include "talk/session/media/mediamonitor.h" + +namespace cricket { + +enum { + MSG_MONITOR_POLL = 1, + MSG_MONITOR_START = 2, + MSG_MONITOR_STOP = 3, + MSG_MONITOR_SIGNAL = 4 +}; + +MediaMonitor::MediaMonitor(talk_base::Thread* worker_thread, + talk_base::Thread* monitor_thread) + : worker_thread_(worker_thread), + monitor_thread_(monitor_thread), monitoring_(false), rate_(0) { +} + +MediaMonitor::~MediaMonitor() { + monitoring_ = false; + monitor_thread_->Clear(this); + worker_thread_->Clear(this); +} + +void MediaMonitor::Start(uint32 milliseconds) { + rate_ = milliseconds; + if (rate_ < 100) + rate_ = 100; + worker_thread_->Post(this, MSG_MONITOR_START); +} + +void MediaMonitor::Stop() { + worker_thread_->Post(this, MSG_MONITOR_STOP); + rate_ = 0; +} + +void MediaMonitor::OnMessage(talk_base::Message* message) { + talk_base::CritScope cs(&crit_); + + switch (message->message_id) { + case MSG_MONITOR_START: + ASSERT(talk_base::Thread::Current() == worker_thread_); + if (!monitoring_) { + monitoring_ = true; + PollMediaChannel(); + } + break; + + case MSG_MONITOR_STOP: + ASSERT(talk_base::Thread::Current() == worker_thread_); + if (monitoring_) { + monitoring_ = false; + worker_thread_->Clear(this); + } + break; + + case MSG_MONITOR_POLL: + ASSERT(talk_base::Thread::Current() == worker_thread_); + PollMediaChannel(); + break; + + case MSG_MONITOR_SIGNAL: + ASSERT(talk_base::Thread::Current() == monitor_thread_); + Update(); + break; + } +} + +void MediaMonitor::PollMediaChannel() { + talk_base::CritScope cs(&crit_); + ASSERT(talk_base::Thread::Current() == worker_thread_); + + GetStats(); + + // Signal the monitoring thread, start another poll timer + monitor_thread_->Post(this, MSG_MONITOR_SIGNAL); + worker_thread_->PostDelayed(rate_, this, MSG_MONITOR_POLL); +} + +} diff --git a/talk/session/media/mediamonitor.h b/talk/session/media/mediamonitor.h new file mode 100644 index 000000000..a9ce88959 --- /dev/null +++ b/talk/session/media/mediamonitor.h @@ -0,0 +1,98 @@ +/* + * libjingle + * Copyright 2005 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. + */ + +// Class to collect statistics from a media channel + +#ifndef TALK_SESSION_MEDIA_MEDIAMONITOR_H_ +#define TALK_SESSION_MEDIA_MEDIAMONITOR_H_ + +#include "talk/base/criticalsection.h" +#include "talk/base/sigslot.h" +#include "talk/base/thread.h" +#include "talk/media/base/mediachannel.h" + +namespace cricket { + +// The base MediaMonitor class, independent of voice and video. +class MediaMonitor : public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + MediaMonitor(talk_base::Thread* worker_thread, + talk_base::Thread* monitor_thread); + ~MediaMonitor(); + + void Start(uint32 milliseconds); + void Stop(); + + protected: + void OnMessage(talk_base::Message *message); + void PollMediaChannel(); + virtual void GetStats() = 0; + virtual void Update() = 0; + + talk_base::CriticalSection crit_; + talk_base::Thread* worker_thread_; + talk_base::Thread* monitor_thread_; + bool monitoring_; + uint32 rate_; +}; + +// Templatized MediaMonitor that can deal with different kinds of media. +template +class MediaMonitorT : public MediaMonitor { + public: + MediaMonitorT(MC* media_channel, talk_base::Thread* worker_thread, + talk_base::Thread* monitor_thread) + : MediaMonitor(worker_thread, monitor_thread), + media_channel_(media_channel) {} + sigslot::signal2 SignalUpdate; + + protected: + // These routines assume the crit_ lock is held by the calling thread. + virtual void GetStats() { + media_info_.Clear(); + media_channel_->GetStats(&media_info_); + } + virtual void Update() { + MI stats(media_info_); + crit_.Leave(); + SignalUpdate(media_channel_, stats); + crit_.Enter(); + } + + private: + MC* media_channel_; + MI media_info_; +}; + +typedef MediaMonitorT VoiceMediaMonitor; +typedef MediaMonitorT VideoMediaMonitor; +typedef MediaMonitorT DataMediaMonitor; + +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_MEDIAMONITOR_H_ diff --git a/talk/session/media/mediarecorder.cc b/talk/session/media/mediarecorder.cc new file mode 100644 index 000000000..0aed63a2c --- /dev/null +++ b/talk/session/media/mediarecorder.cc @@ -0,0 +1,224 @@ +/* + * libjingle + * Copyright 2010 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/media/mediarecorder.h" + +#include + +#include + +#include "talk/base/fileutils.h" +#include "talk/base/logging.h" +#include "talk/base/pathutils.h" +#include "talk/media/base/rtpdump.h" + + +namespace cricket { + +/////////////////////////////////////////////////////////////////////////// +// Implementation of RtpDumpSink. +/////////////////////////////////////////////////////////////////////////// +RtpDumpSink::RtpDumpSink(talk_base::StreamInterface* stream) + : max_size_(INT_MAX), + recording_(false), + packet_filter_(PF_NONE) { + stream_.reset(stream); +} + +RtpDumpSink::~RtpDumpSink() {} + +void RtpDumpSink::SetMaxSize(size_t size) { + talk_base::CritScope cs(&critical_section_); + max_size_ = size; +} + +bool RtpDumpSink::Enable(bool enable) { + talk_base::CritScope cs(&critical_section_); + + recording_ = enable; + + // Create a file and the RTP writer if we have not done yet. + if (recording_ && !writer_) { + if (!stream_) { + return false; + } + writer_.reset(new RtpDumpWriter(stream_.get())); + writer_->set_packet_filter(packet_filter_); + } else if (!recording_ && stream_) { + stream_->Flush(); + } + return true; +} + +void RtpDumpSink::OnPacket(const void* data, size_t size, bool rtcp) { + talk_base::CritScope cs(&critical_section_); + + if (recording_ && writer_) { + size_t current_size; + if (writer_->GetDumpSize(¤t_size) && + current_size + RtpDumpPacket::kHeaderLength + size <= max_size_) { + if (!rtcp) { + writer_->WriteRtpPacket(data, size); + } else { + // TODO(whyuan): Enable recording RTCP. + } + } + } +} + +void RtpDumpSink::set_packet_filter(int filter) { + talk_base::CritScope cs(&critical_section_); + packet_filter_ = filter; + if (writer_) { + writer_->set_packet_filter(packet_filter_); + } +} + +void RtpDumpSink::Flush() { + talk_base::CritScope cs(&critical_section_); + if (stream_) { + stream_->Flush(); + } +} + +/////////////////////////////////////////////////////////////////////////// +// Implementation of MediaRecorder. +/////////////////////////////////////////////////////////////////////////// +MediaRecorder::MediaRecorder() {} + +MediaRecorder::~MediaRecorder() { + talk_base::CritScope cs(&critical_section_); + std::map::iterator itr; + for (itr = sinks_.begin(); itr != sinks_.end(); ++itr) { + delete itr->second; + } +} + +bool MediaRecorder::AddChannel(VoiceChannel* channel, + talk_base::StreamInterface* send_stream, + talk_base::StreamInterface* recv_stream, + int filter) { + return InternalAddChannel(channel, false, send_stream, recv_stream, + filter); +} +bool MediaRecorder::AddChannel(VideoChannel* channel, + talk_base::StreamInterface* send_stream, + talk_base::StreamInterface* recv_stream, + int filter) { + return InternalAddChannel(channel, true, send_stream, recv_stream, + filter); +} + +bool MediaRecorder::InternalAddChannel(BaseChannel* channel, + bool video_channel, + talk_base::StreamInterface* send_stream, + talk_base::StreamInterface* recv_stream, + int filter) { + if (!channel) { + return false; + } + + talk_base::CritScope cs(&critical_section_); + if (sinks_.end() != sinks_.find(channel)) { + return false; // The channel was added already. + } + + SinkPair* sink_pair = new SinkPair; + sink_pair->video_channel = video_channel; + sink_pair->filter = filter; + sink_pair->send_sink.reset(new RtpDumpSink(send_stream)); + sink_pair->send_sink->set_packet_filter(filter); + sink_pair->recv_sink.reset(new RtpDumpSink(recv_stream)); + sink_pair->recv_sink->set_packet_filter(filter); + sinks_[channel] = sink_pair; + + return true; +} + +void MediaRecorder::RemoveChannel(BaseChannel* channel, + SinkType type) { + talk_base::CritScope cs(&critical_section_); + std::map::iterator itr = sinks_.find(channel); + if (sinks_.end() != itr) { + channel->UnregisterSendSink(itr->second->send_sink.get(), type); + channel->UnregisterRecvSink(itr->second->recv_sink.get(), type); + delete itr->second; + sinks_.erase(itr); + } +} + +bool MediaRecorder::EnableChannel( + BaseChannel* channel, bool enable_send, bool enable_recv, + SinkType type) { + talk_base::CritScope cs(&critical_section_); + std::map::iterator itr = sinks_.find(channel); + if (sinks_.end() == itr) { + return false; + } + + SinkPair* sink_pair = itr->second; + RtpDumpSink* sink = sink_pair->send_sink.get(); + sink->Enable(enable_send); + if (enable_send) { + channel->RegisterSendSink(sink, &RtpDumpSink::OnPacket, type); + } else { + channel->UnregisterSendSink(sink, type); + } + + sink = sink_pair->recv_sink.get(); + sink->Enable(enable_recv); + if (enable_recv) { + channel->RegisterRecvSink(sink, &RtpDumpSink::OnPacket, type); + } else { + channel->UnregisterRecvSink(sink, type); + } + + if (sink_pair->video_channel && + (sink_pair->filter & PF_RTPPACKET) == PF_RTPPACKET) { + // Request a full intra frame. + VideoChannel* video_channel = static_cast(channel); + if (enable_send) { + video_channel->SendIntraFrame(); + } + if (enable_recv) { + video_channel->RequestIntraFrame(); + } + } + + return true; +} + +void MediaRecorder::FlushSinks() { + talk_base::CritScope cs(&critical_section_); + std::map::iterator itr; + for (itr = sinks_.begin(); itr != sinks_.end(); ++itr) { + itr->second->send_sink->Flush(); + itr->second->recv_sink->Flush(); + } +} + +} // namespace cricket diff --git a/talk/session/media/mediarecorder.h b/talk/session/media/mediarecorder.h new file mode 100644 index 000000000..df22e984d --- /dev/null +++ b/talk/session/media/mediarecorder.h @@ -0,0 +1,119 @@ +/* + * libjingle + * Copyright 2010 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. + */ + +#ifndef TALK_SESSION_MEDIA_MEDIARECORDER_H_ +#define TALK_SESSION_MEDIA_MEDIARECORDER_H_ + +#include +#include + +#include "talk/base/criticalsection.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" +#include "talk/session/media/channel.h" +#include "talk/session/media/mediasink.h" + +namespace talk_base { +class Pathname; +class FileStream; +} + +namespace cricket { + +class BaseChannel; +class VideoChannel; +class VoiceChannel; +class RtpDumpWriter; + +// RtpDumpSink implements MediaSinkInterface by dumping the RTP/RTCP packets to +// a file. +class RtpDumpSink : public MediaSinkInterface, public sigslot::has_slots<> { + public: + // Takes ownership of stream. + explicit RtpDumpSink(talk_base::StreamInterface* stream); + virtual ~RtpDumpSink(); + + virtual void SetMaxSize(size_t size); + virtual bool Enable(bool enable); + virtual bool IsEnabled() const { return recording_; } + virtual void OnPacket(const void* data, size_t size, bool rtcp); + virtual void set_packet_filter(int filter); + int packet_filter() const { return packet_filter_; } + void Flush(); + + private: + size_t max_size_; + bool recording_; + int packet_filter_; + talk_base::scoped_ptr stream_; + talk_base::scoped_ptr writer_; + talk_base::CriticalSection critical_section_; + + DISALLOW_COPY_AND_ASSIGN(RtpDumpSink); +}; + +class MediaRecorder { + public: + MediaRecorder(); + virtual ~MediaRecorder(); + + bool AddChannel(VoiceChannel* channel, + talk_base::StreamInterface* send_stream, + talk_base::StreamInterface* recv_stream, + int filter); + bool AddChannel(VideoChannel* channel, + talk_base::StreamInterface* send_stream, + talk_base::StreamInterface* recv_stream, + int filter); + void RemoveChannel(BaseChannel* channel, SinkType type); + bool EnableChannel(BaseChannel* channel, bool enable_send, bool enable_recv, + SinkType type); + void FlushSinks(); + + private: + struct SinkPair { + bool video_channel; + int filter; + talk_base::scoped_ptr send_sink; + talk_base::scoped_ptr recv_sink; + }; + + bool InternalAddChannel(BaseChannel* channel, + bool video_channel, + talk_base::StreamInterface* send_stream, + talk_base::StreamInterface* recv_stream, + int filter); + + std::map sinks_; + talk_base::CriticalSection critical_section_; + + DISALLOW_COPY_AND_ASSIGN(MediaRecorder); +}; + +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_MEDIARECORDER_H_ diff --git a/talk/session/media/mediarecorder_unittest.cc b/talk/session/media/mediarecorder_unittest.cc new file mode 100644 index 000000000..5155e6dd3 --- /dev/null +++ b/talk/session/media/mediarecorder_unittest.cc @@ -0,0 +1,358 @@ +// libjingle +// Copyright 2010 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 + +#include "talk/base/bytebuffer.h" +#include "talk/base/fileutils.h" +#include "talk/base/gunit.h" +#include "talk/base/pathutils.h" +#include "talk/base/thread.h" +#include "talk/media/base/fakemediaengine.h" +#include "talk/media/base/rtpdump.h" +#include "talk/media/base/testutils.h" +#include "talk/p2p/base/fakesession.h" +#include "talk/session/media/channel.h" +#include "talk/session/media/mediarecorder.h" + +namespace cricket { + +talk_base::StreamInterface* Open(const std::string& path) { + return talk_base::Filesystem::OpenFile( + talk_base::Pathname(path), "wb"); +} + +///////////////////////////////////////////////////////////////////////// +// Test RtpDumpSink +///////////////////////////////////////////////////////////////////////// +class RtpDumpSinkTest : public testing::Test { + public: + virtual void SetUp() { + EXPECT_TRUE(talk_base::Filesystem::GetTemporaryFolder(path_, true, NULL)); + path_.SetFilename("sink-test.rtpdump"); + sink_.reset(new RtpDumpSink(Open(path_.pathname()))); + + for (int i = 0; i < ARRAY_SIZE(rtp_buf_); ++i) { + RtpTestUtility::kTestRawRtpPackets[i].WriteToByteBuffer( + RtpTestUtility::kDefaultSsrc, &rtp_buf_[i]); + } + } + + virtual void TearDown() { + stream_.reset(); + EXPECT_TRUE(talk_base::Filesystem::DeleteFile(path_)); + } + + protected: + void OnRtpPacket(const RawRtpPacket& raw) { + talk_base::ByteBuffer buf; + raw.WriteToByteBuffer(RtpTestUtility::kDefaultSsrc, &buf); + sink_->OnPacket(buf.Data(), buf.Length(), false); + } + + talk_base::StreamResult ReadPacket(RtpDumpPacket* packet) { + if (!stream_.get()) { + sink_.reset(); // This will close the file. So we can read it. + stream_.reset(talk_base::Filesystem::OpenFile(path_, "rb")); + reader_.reset(new RtpDumpReader(stream_.get())); + } + return reader_->ReadPacket(packet); + } + + talk_base::Pathname path_; + talk_base::scoped_ptr sink_; + talk_base::ByteBuffer rtp_buf_[3]; + talk_base::scoped_ptr stream_; + talk_base::scoped_ptr reader_; +}; + +TEST_F(RtpDumpSinkTest, TestRtpDumpSink) { + // By default, the sink is disabled. The 1st packet is not written. + EXPECT_FALSE(sink_->IsEnabled()); + sink_->set_packet_filter(PF_ALL); + OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[0]); + + // Enable the sink. The 2nd packet is written. + EXPECT_TRUE(sink_->Enable(true)); + EXPECT_TRUE(sink_->IsEnabled()); + EXPECT_TRUE(talk_base::Filesystem::IsFile(path_.pathname())); + OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[1]); + + // Disable the sink. The 3rd packet is not written. + EXPECT_TRUE(sink_->Enable(false)); + EXPECT_FALSE(sink_->IsEnabled()); + OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[2]); + + // Read the recorded file and verify it contains only the 2nd packet. + RtpDumpPacket packet; + EXPECT_EQ(talk_base::SR_SUCCESS, ReadPacket(&packet)); + EXPECT_TRUE(RtpTestUtility::VerifyPacket( + &packet, &RtpTestUtility::kTestRawRtpPackets[1], false)); + EXPECT_EQ(talk_base::SR_EOS, ReadPacket(&packet)); +} + +TEST_F(RtpDumpSinkTest, TestRtpDumpSinkMaxSize) { + EXPECT_TRUE(sink_->Enable(true)); + sink_->set_packet_filter(PF_ALL); + sink_->SetMaxSize(strlen(RtpDumpFileHeader::kFirstLine) + + RtpDumpFileHeader::kHeaderLength + + RtpDumpPacket::kHeaderLength + + RtpTestUtility::kTestRawRtpPackets[0].size()); + OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[0]); + + // Exceed the limit size: the 2nd and 3rd packets are not written. + OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[1]); + OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[2]); + + // Read the recorded file and verify that it contains only the first packet. + RtpDumpPacket packet; + EXPECT_EQ(talk_base::SR_SUCCESS, ReadPacket(&packet)); + EXPECT_TRUE(RtpTestUtility::VerifyPacket( + &packet, &RtpTestUtility::kTestRawRtpPackets[0], false)); + EXPECT_EQ(talk_base::SR_EOS, ReadPacket(&packet)); +} + +TEST_F(RtpDumpSinkTest, TestRtpDumpSinkFilter) { + // The default filter is PF_NONE. + EXPECT_EQ(PF_NONE, sink_->packet_filter()); + + // Set to PF_RTPHEADER before enable. + sink_->set_packet_filter(PF_RTPHEADER); + EXPECT_EQ(PF_RTPHEADER, sink_->packet_filter()); + EXPECT_TRUE(sink_->Enable(true)); + // We dump only the header of the first packet. + OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[0]); + + // Set the filter to PF_RTPPACKET. We dump all the second packet. + sink_->set_packet_filter(PF_RTPPACKET); + EXPECT_EQ(PF_RTPPACKET, sink_->packet_filter()); + OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[1]); + + // Set the filter to PF_NONE. We do not dump the third packet. + sink_->set_packet_filter(PF_NONE); + EXPECT_EQ(PF_NONE, sink_->packet_filter()); + OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[2]); + + // Read the recorded file and verify the header of the first packet and + // the whole packet for the second packet. + RtpDumpPacket packet; + EXPECT_EQ(talk_base::SR_SUCCESS, ReadPacket(&packet)); + EXPECT_TRUE(RtpTestUtility::VerifyPacket( + &packet, &RtpTestUtility::kTestRawRtpPackets[0], true)); + EXPECT_EQ(talk_base::SR_SUCCESS, ReadPacket(&packet)); + EXPECT_TRUE(RtpTestUtility::VerifyPacket( + &packet, &RtpTestUtility::kTestRawRtpPackets[1], false)); + EXPECT_EQ(talk_base::SR_EOS, ReadPacket(&packet)); +} + +///////////////////////////////////////////////////////////////////////// +// Test MediaRecorder +///////////////////////////////////////////////////////////////////////// +void TestMediaRecorder(BaseChannel* channel, + FakeVideoMediaChannel* video_media_channel, + int filter) { + // Create media recorder. + talk_base::scoped_ptr recorder(new MediaRecorder); + // Fail to EnableChannel before AddChannel. + EXPECT_FALSE(recorder->EnableChannel(channel, true, true, SINK_PRE_CRYPTO)); + EXPECT_FALSE(channel->HasSendSinks(SINK_PRE_CRYPTO)); + EXPECT_FALSE(channel->HasRecvSinks(SINK_PRE_CRYPTO)); + EXPECT_FALSE(channel->HasSendSinks(SINK_POST_CRYPTO)); + EXPECT_FALSE(channel->HasRecvSinks(SINK_POST_CRYPTO)); + + // Add the channel to the recorder. + talk_base::Pathname path; + EXPECT_TRUE(talk_base::Filesystem::GetTemporaryFolder(path, true, NULL)); + path.SetFilename("send.rtpdump"); + std::string send_file = path.pathname(); + path.SetFilename("recv.rtpdump"); + std::string recv_file = path.pathname(); + if (video_media_channel) { + EXPECT_TRUE(recorder->AddChannel(static_cast(channel), + Open(send_file), Open(recv_file), filter)); + } else { + EXPECT_TRUE(recorder->AddChannel(static_cast(channel), + Open(send_file), Open(recv_file), filter)); + } + + // Enable recording only the sent media. + EXPECT_TRUE(recorder->EnableChannel(channel, true, false, SINK_PRE_CRYPTO)); + EXPECT_TRUE(channel->HasSendSinks(SINK_PRE_CRYPTO)); + EXPECT_FALSE(channel->HasRecvSinks(SINK_POST_CRYPTO)); + EXPECT_FALSE(channel->HasSendSinks(SINK_POST_CRYPTO)); + EXPECT_FALSE(channel->HasRecvSinks(SINK_POST_CRYPTO)); + if (video_media_channel) { + EXPECT_TRUE_WAIT(video_media_channel->sent_intra_frame(), 100); + } + + // Enable recording only the received meida. + EXPECT_TRUE(recorder->EnableChannel(channel, false, true, SINK_PRE_CRYPTO)); + EXPECT_FALSE(channel->HasSendSinks(SINK_PRE_CRYPTO)); + EXPECT_TRUE(channel->HasRecvSinks(SINK_PRE_CRYPTO)); + if (video_media_channel) { + EXPECT_TRUE(video_media_channel->requested_intra_frame()); + } + + // Enable recording both the sent and the received video. + EXPECT_TRUE(recorder->EnableChannel(channel, true, true, SINK_PRE_CRYPTO)); + EXPECT_TRUE(channel->HasSendSinks(SINK_PRE_CRYPTO)); + EXPECT_TRUE(channel->HasRecvSinks(SINK_PRE_CRYPTO)); + + // Enable recording only headers. + if (video_media_channel) { + video_media_channel->set_sent_intra_frame(false); + video_media_channel->set_requested_intra_frame(false); + } + EXPECT_TRUE(recorder->EnableChannel(channel, true, true, SINK_PRE_CRYPTO)); + EXPECT_TRUE(channel->HasSendSinks(SINK_PRE_CRYPTO)); + EXPECT_TRUE(channel->HasRecvSinks(SINK_PRE_CRYPTO)); + if (video_media_channel) { + if ((filter & PF_RTPPACKET) == PF_RTPPACKET) { + // If record the whole RTP packet, trigers FIR. + EXPECT_TRUE(video_media_channel->requested_intra_frame()); + EXPECT_TRUE(video_media_channel->sent_intra_frame()); + } else { + // If record only the RTP header, does not triger FIR. + EXPECT_FALSE(video_media_channel->requested_intra_frame()); + EXPECT_FALSE(video_media_channel->sent_intra_frame()); + } + } + + // Remove the voice channel from the recorder. + recorder->RemoveChannel(channel, SINK_PRE_CRYPTO); + EXPECT_FALSE(channel->HasSendSinks(SINK_PRE_CRYPTO)); + EXPECT_FALSE(channel->HasRecvSinks(SINK_PRE_CRYPTO)); + + // Delete all files. + recorder.reset(); + EXPECT_TRUE(talk_base::Filesystem::DeleteFile(send_file)); + EXPECT_TRUE(talk_base::Filesystem::DeleteFile(recv_file)); +} + +// Fisrt start recording header and then start recording media. Verify that +// differnt files are created for header and media. +void TestRecordHeaderAndMedia(BaseChannel* channel, + FakeVideoMediaChannel* video_media_channel) { + // Create RTP header recorder. + talk_base::scoped_ptr header_recorder(new MediaRecorder); + + talk_base::Pathname path; + EXPECT_TRUE(talk_base::Filesystem::GetTemporaryFolder(path, true, NULL)); + path.SetFilename("send-header.rtpdump"); + std::string send_header_file = path.pathname(); + path.SetFilename("recv-header.rtpdump"); + std::string recv_header_file = path.pathname(); + if (video_media_channel) { + EXPECT_TRUE(header_recorder->AddChannel( + static_cast(channel), + Open(send_header_file), Open(recv_header_file), PF_RTPHEADER)); + } else { + EXPECT_TRUE(header_recorder->AddChannel( + static_cast(channel), + Open(send_header_file), Open(recv_header_file), PF_RTPHEADER)); + } + + // Enable recording both sent and received. + EXPECT_TRUE( + header_recorder->EnableChannel(channel, true, true, SINK_POST_CRYPTO)); + EXPECT_TRUE(channel->HasSendSinks(SINK_POST_CRYPTO)); + EXPECT_TRUE(channel->HasRecvSinks(SINK_POST_CRYPTO)); + EXPECT_FALSE(channel->HasSendSinks(SINK_PRE_CRYPTO)); + EXPECT_FALSE(channel->HasRecvSinks(SINK_PRE_CRYPTO)); + if (video_media_channel) { + EXPECT_FALSE(video_media_channel->sent_intra_frame()); + EXPECT_FALSE(video_media_channel->requested_intra_frame()); + } + + // Verify that header files are created. + EXPECT_TRUE(talk_base::Filesystem::IsFile(send_header_file)); + EXPECT_TRUE(talk_base::Filesystem::IsFile(recv_header_file)); + + // Create RTP header recorder. + talk_base::scoped_ptr recorder(new MediaRecorder); + path.SetFilename("send.rtpdump"); + std::string send_file = path.pathname(); + path.SetFilename("recv.rtpdump"); + std::string recv_file = path.pathname(); + if (video_media_channel) { + EXPECT_TRUE(recorder->AddChannel( + static_cast(channel), + Open(send_file), Open(recv_file), PF_RTPPACKET)); + } else { + EXPECT_TRUE(recorder->AddChannel( + static_cast(channel), + Open(send_file), Open(recv_file), PF_RTPPACKET)); + } + + // Enable recording both sent and received. + EXPECT_TRUE(recorder->EnableChannel(channel, true, true, SINK_PRE_CRYPTO)); + EXPECT_TRUE(channel->HasSendSinks(SINK_POST_CRYPTO)); + EXPECT_TRUE(channel->HasRecvSinks(SINK_POST_CRYPTO)); + EXPECT_TRUE(channel->HasSendSinks(SINK_PRE_CRYPTO)); + EXPECT_TRUE(channel->HasRecvSinks(SINK_PRE_CRYPTO)); + if (video_media_channel) { + EXPECT_TRUE_WAIT(video_media_channel->sent_intra_frame(), 100); + EXPECT_TRUE(video_media_channel->requested_intra_frame()); + } + + // Verify that media files are created. + EXPECT_TRUE(talk_base::Filesystem::IsFile(send_file)); + EXPECT_TRUE(talk_base::Filesystem::IsFile(recv_file)); + + // Delete all files. + header_recorder.reset(); + recorder.reset(); + EXPECT_TRUE(talk_base::Filesystem::DeleteFile(send_header_file)); + EXPECT_TRUE(talk_base::Filesystem::DeleteFile(recv_header_file)); + EXPECT_TRUE(talk_base::Filesystem::DeleteFile(send_file)); + EXPECT_TRUE(talk_base::Filesystem::DeleteFile(recv_file)); +} + +TEST(MediaRecorderTest, TestMediaRecorderVoiceChannel) { + // Create the voice channel. + FakeSession session(true); + FakeMediaEngine media_engine; + VoiceChannel channel(talk_base::Thread::Current(), &media_engine, + new FakeVoiceMediaChannel(NULL), &session, "", false); + EXPECT_TRUE(channel.Init()); + TestMediaRecorder(&channel, NULL, PF_RTPPACKET); + TestMediaRecorder(&channel, NULL, PF_RTPHEADER); + TestRecordHeaderAndMedia(&channel, NULL); +} + +TEST(MediaRecorderTest, TestMediaRecorderVideoChannel) { + // Create the video channel. + FakeSession session(true); + FakeMediaEngine media_engine; + FakeVideoMediaChannel* media_channel = new FakeVideoMediaChannel(NULL); + VideoChannel channel(talk_base::Thread::Current(), &media_engine, + media_channel, &session, "", false, NULL); + EXPECT_TRUE(channel.Init()); + TestMediaRecorder(&channel, media_channel, PF_RTPPACKET); + TestMediaRecorder(&channel, media_channel, PF_RTPHEADER); + TestRecordHeaderAndMedia(&channel, media_channel); +} + +} // namespace cricket diff --git a/talk/session/media/mediasession.cc b/talk/session/media/mediasession.cc new file mode 100644 index 000000000..3d0041821 --- /dev/null +++ b/talk/session/media/mediasession.cc @@ -0,0 +1,1657 @@ +/* + * libjingle + * Copyright 2004 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/media/mediasession.h" + +#include +#include +#include +#include + +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringutils.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/cryptoparams.h" +#include "talk/p2p/base/constants.h" +#include "talk/session/media/channelmanager.h" +#include "talk/session/media/srtpfilter.h" +#include "talk/xmpp/constants.h" + +namespace { +const char kInline[] = "inline:"; +} + +namespace cricket { + +using talk_base::scoped_ptr; + +// RTP Profile names +// http://www.iana.org/assignments/rtp-parameters/rtp-parameters.xml +// RFC4585 +const char kMediaProtocolAvpf[] = "RTP/AVPF"; +// RFC5124 +const char kMediaProtocolSavpf[] = "RTP/SAVPF"; + +const char kMediaProtocolRtpPrefix[] = "RTP/"; + +const char kMediaProtocolSctp[] = "SCTP"; +const char kMediaProtocolDtlsSctp[] = "DTLS/SCTP"; + +static bool IsMediaContentOfType(const ContentInfo* content, + MediaType media_type) { + if (!IsMediaContent(content)) { + return false; + } + + const MediaContentDescription* mdesc = + static_cast(content->description); + return mdesc && mdesc->type() == media_type; +} + +static bool CreateCryptoParams(int tag, const std::string& cipher, + CryptoParams *out) { + std::string key; + key.reserve(SRTP_MASTER_KEY_BASE64_LEN); + + if (!talk_base::CreateRandomString(SRTP_MASTER_KEY_BASE64_LEN, &key)) { + return false; + } + out->tag = tag; + out->cipher_suite = cipher; + out->key_params = kInline; + out->key_params += key; + return true; +} + +#ifdef HAVE_SRTP +static bool AddCryptoParams(const std::string& cipher_suite, + CryptoParamsVec *out) { + int size = out->size(); + + out->resize(size + 1); + return CreateCryptoParams(size, cipher_suite, &out->at(size)); +} + +void AddMediaCryptos(const CryptoParamsVec& cryptos, + MediaContentDescription* media) { + for (CryptoParamsVec::const_iterator crypto = cryptos.begin(); + crypto != cryptos.end(); ++crypto) { + media->AddCrypto(*crypto); + } +} + +bool CreateMediaCryptos(const std::vector& crypto_suites, + MediaContentDescription* media) { + CryptoParamsVec cryptos; + for (std::vector::const_iterator it = crypto_suites.begin(); + it != crypto_suites.end(); ++it) { + if (!AddCryptoParams(*it, &cryptos)) { + return false; + } + } + AddMediaCryptos(cryptos, media); + return true; +} +#endif + +const CryptoParamsVec* GetCryptos(const MediaContentDescription* media) { + if (!media) { + return NULL; + } + return &media->cryptos(); +} + +bool FindMatchingCrypto(const CryptoParamsVec& cryptos, + const CryptoParams& crypto, + CryptoParams* out) { + for (CryptoParamsVec::const_iterator it = cryptos.begin(); + it != cryptos.end(); ++it) { + if (crypto.Matches(*it)) { + *out = *it; + return true; + } + } + return false; +} + +// For audio, HMAC 32 is prefered because of the low overhead. +void GetSupportedAudioCryptoSuites( + std::vector* crypto_suites) { +#ifdef HAVE_SRTP + crypto_suites->push_back(CS_AES_CM_128_HMAC_SHA1_32); + crypto_suites->push_back(CS_AES_CM_128_HMAC_SHA1_80); +#endif +} + +void GetSupportedVideoCryptoSuites( + std::vector* crypto_suites) { + GetSupportedDefaultCryptoSuites(crypto_suites); +} + +void GetSupportedDataCryptoSuites( + std::vector* crypto_suites) { + GetSupportedDefaultCryptoSuites(crypto_suites); +} + +void GetSupportedDefaultCryptoSuites( + std::vector* crypto_suites) { +#ifdef HAVE_SRTP + crypto_suites->push_back(CS_AES_CM_128_HMAC_SHA1_80); +#endif +} + +// For video support only 80-bit SHA1 HMAC. For audio 32-bit HMAC is +// tolerated unless bundle is enabled because it is low overhead. Pick the +// crypto in the list that is supported. +static bool SelectCrypto(const MediaContentDescription* offer, + bool bundle, + CryptoParams *crypto) { + bool audio = offer->type() == MEDIA_TYPE_AUDIO; + const CryptoParamsVec& cryptos = offer->cryptos(); + + for (CryptoParamsVec::const_iterator i = cryptos.begin(); + i != cryptos.end(); ++i) { + if (CS_AES_CM_128_HMAC_SHA1_80 == i->cipher_suite || + (CS_AES_CM_128_HMAC_SHA1_32 == i->cipher_suite && audio && !bundle)) { + return CreateCryptoParams(i->tag, i->cipher_suite, crypto); + } + } + return false; +} + +static const StreamParams* FindFirstStreamParamsByCname( + const StreamParamsVec& params_vec, + const std::string& cname) { + for (StreamParamsVec::const_iterator it = params_vec.begin(); + it != params_vec.end(); ++it) { + if (cname == it->cname) + return &*it; + } + return NULL; +} + +// Generates a new CNAME or the CNAME of an already existing StreamParams +// if a StreamParams exist for another Stream in streams with sync_label +// sync_label. +static bool GenerateCname(const StreamParamsVec& params_vec, + const MediaSessionOptions::Streams& streams, + const std::string& synch_label, + std::string* cname) { + ASSERT(cname != NULL); + if (!cname) + return false; + + // Check if a CNAME exist for any of the other synched streams. + for (MediaSessionOptions::Streams::const_iterator stream_it = streams.begin(); + stream_it != streams.end() ; ++stream_it) { + if (synch_label != stream_it->sync_label) + continue; + + StreamParams param; + // groupid is empty for StreamParams generated using + // MediaSessionDescriptionFactory. + if (GetStreamByIds(params_vec, "", stream_it->id, + ¶m)) { + *cname = param.cname; + return true; + } + } + // No other stream seems to exist that we should sync with. + // Generate a random string for the RTCP CNAME, as stated in RFC 6222. + // This string is only used for synchronization, and therefore is opaque. + do { + if (!talk_base::CreateRandomString(16, cname)) { + ASSERT(false); + return false; + } + } while (FindFirstStreamParamsByCname(params_vec, *cname)); + + return true; +} + +// Generate random SSRC values that are not already present in |params_vec|. +// Either 2 or 1 ssrcs will be generated based on |include_rtx_stream| being +// true or false. The generated values are added to |ssrcs|. +static void GenerateSsrcs(const StreamParamsVec& params_vec, + bool include_rtx_stream, + std::vector* ssrcs) { + unsigned int num_ssrcs = include_rtx_stream ? 2 : 1; + for (unsigned int i = 0; i < num_ssrcs; i++) { + uint32 candidate; + do { + candidate = talk_base::CreateRandomNonZeroId(); + } while (GetStreamBySsrc(params_vec, candidate, NULL) || + std::count(ssrcs->begin(), ssrcs->end(), candidate) > 0); + ssrcs->push_back(candidate); + } +} + +// Returns false if we exhaust the range of SIDs. +static bool GenerateSctpSid(const StreamParamsVec& params_vec, + uint32* sid) { + if (params_vec.size() > kMaxSctpSid) { + LOG(LS_WARNING) << + "Could not generate an SCTP SID: too many SCTP streams."; + return false; + } + while (true) { + uint32 candidate = talk_base::CreateRandomNonZeroId() % kMaxSctpSid; + if (!GetStreamBySsrc(params_vec, candidate, NULL)) { + *sid = candidate; + return true; + } + } +} + +static bool GenerateSctpSids(const StreamParamsVec& params_vec, + std::vector* sids) { + uint32 sid; + if (!GenerateSctpSid(params_vec, &sid)) { + LOG(LS_WARNING) << "Could not generated an SCTP SID."; + return false; + } + sids->push_back(sid); + return true; +} + +// Finds all StreamParams of all media types and attach them to stream_params. +static void GetCurrentStreamParams(const SessionDescription* sdesc, + StreamParamsVec* stream_params) { + if (!sdesc) + return; + + const ContentInfos& contents = sdesc->contents(); + for (ContentInfos::const_iterator content = contents.begin(); + content != contents.end(); ++content) { + if (!IsMediaContent(&*content)) { + continue; + } + const MediaContentDescription* media = + static_cast( + content->description); + const StreamParamsVec& streams = media->streams(); + for (StreamParamsVec::const_iterator it = streams.begin(); + it != streams.end(); ++it) { + stream_params->push_back(*it); + } + } +} + +template +class UsedIds { + public: + UsedIds(int min_allowed_id, int max_allowed_id) + : min_allowed_id_(min_allowed_id), + max_allowed_id_(max_allowed_id), + next_id_(max_allowed_id) { + } + + // Loops through all Id in |ids| and changes its id if it is + // already in use by another IdStruct. Call this methods with all Id + // in a session description to make sure no duplicate ids exists. + // Note that typename Id must be a type of IdStruct. + template + void FindAndSetIdUsed(std::vector* ids) { + for (typename std::vector::iterator it = ids->begin(); + it != ids->end(); ++it) { + FindAndSetIdUsed(&*it); + } + } + + // Finds and sets an unused id if the |idstruct| id is already in use. + void FindAndSetIdUsed(IdStruct* idstruct) { + const int original_id = idstruct->id; + int new_id = idstruct->id; + + if (original_id > max_allowed_id_ || original_id < min_allowed_id_) { + // If the original id is not in range - this is an id that can't be + // dynamically changed. + return; + } + + if (IsIdUsed(original_id)) { + new_id = FindUnusedId(); + LOG(LS_WARNING) << "Duplicate id found. Reassigning from " << original_id + << " to " << new_id; + idstruct->id = new_id; + } + SetIdUsed(new_id); + } + + private: + // Returns the first unused id in reverse order. + // This hopefully reduce the risk of more collisions. We want to change the + // default ids as little as possible. + int FindUnusedId() { + while (IsIdUsed(next_id_) && next_id_ >= min_allowed_id_) { + --next_id_; + } + ASSERT(next_id_ >= min_allowed_id_); + return next_id_; + } + + bool IsIdUsed(int new_id) { + return id_set_.find(new_id) != id_set_.end(); + } + + void SetIdUsed(int new_id) { + id_set_.insert(new_id); + } + + const int min_allowed_id_; + const int max_allowed_id_; + int next_id_; + std::set id_set_; +}; + +// Helper class used for finding duplicate RTP payload types among audio, video +// and data codecs. When bundle is used the payload types may not collide. +class UsedPayloadTypes : public UsedIds { + public: + UsedPayloadTypes() + : UsedIds(kDynamicPayloadTypeMin, kDynamicPayloadTypeMax) { + } + + + private: + static const int kDynamicPayloadTypeMin = 96; + static const int kDynamicPayloadTypeMax = 127; +}; + +// Helper class used for finding duplicate RTP Header extension ids among +// audio and video extensions. +class UsedRtpHeaderExtensionIds : public UsedIds { + public: + UsedRtpHeaderExtensionIds() + : UsedIds(kLocalIdMin, kLocalIdMax) { + } + + private: + // Min and Max local identifier as specified by RFC5285. + static const int kLocalIdMin = 1; + static const int kLocalIdMax = 255; +}; + +static bool IsSctp(const MediaContentDescription* desc) { + return ((desc->protocol() == kMediaProtocolSctp) || + (desc->protocol() == kMediaProtocolDtlsSctp)); +} + +// Adds a StreamParams for each Stream in Streams with media type +// media_type to content_description. +// |current_params| - All currently known StreamParams of any media type. +template +static bool AddStreamParams( + MediaType media_type, + const MediaSessionOptions::Streams& streams, + StreamParamsVec* current_streams, + MediaContentDescriptionImpl* content_description, + const bool add_legacy_stream) { + const bool include_rtx_stream = + ContainsRtxCodec(content_description->codecs()); + + if (streams.empty() && add_legacy_stream) { + // TODO(perkj): Remove this legacy stream when all apps use StreamParams. + std::vector ssrcs; + if (IsSctp(content_description)) { + GenerateSctpSids(*current_streams, &ssrcs); + } else { + GenerateSsrcs(*current_streams, include_rtx_stream, &ssrcs); + } + if (include_rtx_stream) { + content_description->AddLegacyStream(ssrcs[0], ssrcs[1]); + content_description->set_multistream(true); + } else { + content_description->AddLegacyStream(ssrcs[0]); + } + return true; + } + + MediaSessionOptions::Streams::const_iterator stream_it; + for (stream_it = streams.begin(); + stream_it != streams.end(); ++stream_it) { + if (stream_it->type != media_type) + continue; // Wrong media type. + + StreamParams param; + // groupid is empty for StreamParams generated using + // MediaSessionDescriptionFactory. + if (!GetStreamByIds(*current_streams, "", stream_it->id, + ¶m)) { + // This is a new stream. + // Get a CNAME. Either new or same as one of the other synched streams. + std::string cname; + if (!GenerateCname(*current_streams, streams, stream_it->sync_label, + &cname)) { + return false; + } + + std::vector ssrcs; + if (IsSctp(content_description)) { + GenerateSctpSids(*current_streams, &ssrcs); + } else { + GenerateSsrcs(*current_streams, include_rtx_stream, &ssrcs); + } + StreamParams stream_param; + stream_param.id = stream_it->id; + stream_param.ssrcs.push_back(ssrcs[0]); + if (include_rtx_stream) { + stream_param.AddFidSsrc(ssrcs[0], ssrcs[1]); + content_description->set_multistream(true); + } + stream_param.cname = cname; + stream_param.sync_label = stream_it->sync_label; + content_description->AddStream(stream_param); + + // Store the new StreamParams in current_streams. + // This is necessary so that we can use the CNAME for other media types. + current_streams->push_back(stream_param); + } else { + content_description->AddStream(param); + } + } + return true; +} + +// Updates the transport infos of the |sdesc| according to the given +// |bundle_group|. The transport infos of the content names within the +// |bundle_group| should be updated to use the ufrag and pwd of the first +// content within the |bundle_group|. +static bool UpdateTransportInfoForBundle(const ContentGroup& bundle_group, + SessionDescription* sdesc) { + // The bundle should not be empty. + if (!sdesc || !bundle_group.FirstContentName()) { + return false; + } + + // We should definitely have a transport for the first content. + std::string selected_content_name = *bundle_group.FirstContentName(); + const TransportInfo* selected_transport_info = + sdesc->GetTransportInfoByName(selected_content_name); + if (!selected_transport_info) { + return false; + } + + // Set the other contents to use the same ICE credentials. + const std::string selected_ufrag = + selected_transport_info->description.ice_ufrag; + const std::string selected_pwd = + selected_transport_info->description.ice_pwd; + for (TransportInfos::iterator it = + sdesc->transport_infos().begin(); + it != sdesc->transport_infos().end(); ++it) { + if (bundle_group.HasContentName(it->content_name) && + it->content_name != selected_content_name) { + it->description.ice_ufrag = selected_ufrag; + it->description.ice_pwd = selected_pwd; + } + } + return true; +} + +// Gets the CryptoParamsVec of the given |content_name| from |sdesc|, and +// sets it to |cryptos|. +static bool GetCryptosByName(const SessionDescription* sdesc, + const std::string& content_name, + CryptoParamsVec* cryptos) { + if (!sdesc || !cryptos) { + return false; + } + + const ContentInfo* content = sdesc->GetContentByName(content_name); + if (!IsMediaContent(content) || !content->description) { + return false; + } + + const MediaContentDescription* media_desc = + static_cast(content->description); + *cryptos = media_desc->cryptos(); + return true; +} + +// Predicate function used by the remove_if. +// Returns true if the |crypto|'s cipher_suite is not found in |filter|. +static bool CryptoNotFound(const CryptoParams crypto, + const CryptoParamsVec* filter) { + if (filter == NULL) { + return true; + } + for (CryptoParamsVec::const_iterator it = filter->begin(); + it != filter->end(); ++it) { + if (it->cipher_suite == crypto.cipher_suite) { + return false; + } + } + return true; +} + +// Prunes the |target_cryptos| by removing the crypto params (cipher_suite) +// which are not available in |filter|. +static void PruneCryptos(const CryptoParamsVec& filter, + CryptoParamsVec* target_cryptos) { + if (!target_cryptos) { + return; + } + target_cryptos->erase(std::remove_if(target_cryptos->begin(), + target_cryptos->end(), + bind2nd(ptr_fun(CryptoNotFound), + &filter)), + target_cryptos->end()); +} + +static bool IsRtpContent(SessionDescription* sdesc, + const std::string& content_name) { + bool is_rtp = false; + ContentInfo* content = sdesc->GetContentByName(content_name); + if (IsMediaContent(content)) { + MediaContentDescription* media_desc = + static_cast(content->description); + if (!media_desc) { + return false; + } + is_rtp = media_desc->protocol().empty() || + talk_base::starts_with(media_desc->protocol().data(), + kMediaProtocolRtpPrefix); + } + return is_rtp; +} + +// Updates the crypto parameters of the |sdesc| according to the given +// |bundle_group|. The crypto parameters of all the contents within the +// |bundle_group| should be updated to use the common subset of the +// available cryptos. +static bool UpdateCryptoParamsForBundle(const ContentGroup& bundle_group, + SessionDescription* sdesc) { + // The bundle should not be empty. + if (!sdesc || !bundle_group.FirstContentName()) { + return false; + } + + // Get the common cryptos. + const ContentNames& content_names = bundle_group.content_names(); + CryptoParamsVec common_cryptos; + for (ContentNames::const_iterator it = content_names.begin(); + it != content_names.end(); ++it) { + if (!IsRtpContent(sdesc, *it)) { + continue; + } + if (it == content_names.begin()) { + // Initial the common_cryptos with the first content in the bundle group. + if (!GetCryptosByName(sdesc, *it, &common_cryptos)) { + return false; + } + if (common_cryptos.empty()) { + // If there's no crypto params, we should just return. + return true; + } + } else { + CryptoParamsVec cryptos; + if (!GetCryptosByName(sdesc, *it, &cryptos)) { + return false; + } + PruneCryptos(cryptos, &common_cryptos); + } + } + + if (common_cryptos.empty()) { + return false; + } + + // Update to use the common cryptos. + for (ContentNames::const_iterator it = content_names.begin(); + it != content_names.end(); ++it) { + if (!IsRtpContent(sdesc, *it)) { + continue; + } + ContentInfo* content = sdesc->GetContentByName(*it); + if (IsMediaContent(content)) { + MediaContentDescription* media_desc = + static_cast(content->description); + if (!media_desc) { + return false; + } + media_desc->set_cryptos(common_cryptos); + } + } + return true; +} + +template +static bool ContainsRtxCodec(const std::vector& codecs) { + typename std::vector::const_iterator it; + for (it = codecs.begin(); it != codecs.end(); ++it) { + if (IsRtxCodec(*it)) { + return true; + } + } + return false; +} + +template +static bool IsRtxCodec(const C& codec) { + return stricmp(codec.name.c_str(), kRtxCodecName) == 0; +} + +// Create a media content to be offered in a session-initiate, +// according to the given options.rtcp_mux, options.is_muc, +// options.streams, codecs, secure_transport, crypto, and streams. If we don't +// currently have crypto (in current_cryptos) and it is enabled (in +// secure_policy), crypto is created (according to crypto_suites). If +// add_legacy_stream is true, and current_streams is empty, a legacy +// stream is created. The created content is added to the offer. +template +static bool CreateMediaContentOffer( + const MediaSessionOptions& options, + const std::vector& codecs, + const SecureMediaPolicy& secure_policy, + const CryptoParamsVec* current_cryptos, + const std::vector& crypto_suites, + const RtpHeaderExtensions& rtp_extensions, + bool add_legacy_stream, + StreamParamsVec* current_streams, + MediaContentDescriptionImpl* offer) { + offer->AddCodecs(codecs); + offer->SortCodecs(); + + offer->set_crypto_required(secure_policy == SEC_REQUIRED); + offer->set_rtcp_mux(options.rtcp_mux_enabled); + offer->set_multistream(options.is_muc); + offer->set_rtp_header_extensions(rtp_extensions); + + if (!AddStreamParams( + offer->type(), options.streams, current_streams, + offer, add_legacy_stream)) { + return false; + } + +#ifdef HAVE_SRTP + if (secure_policy != SEC_DISABLED) { + if (current_cryptos) { + AddMediaCryptos(*current_cryptos, offer); + } + if (offer->cryptos().empty()) { + if (!CreateMediaCryptos(crypto_suites, offer)) { + return false; + } + } + } +#endif + + if (offer->crypto_required() && offer->cryptos().empty()) { + return false; + } + return true; +} + +template +static void NegotiateCodecs(const std::vector& local_codecs, + const std::vector& offered_codecs, + std::vector* negotiated_codecs) { + typename std::vector::const_iterator ours; + for (ours = local_codecs.begin(); + ours != local_codecs.end(); ++ours) { + typename std::vector::const_iterator theirs; + for (theirs = offered_codecs.begin(); + theirs != offered_codecs.end(); ++theirs) { + if (ours->Matches(*theirs)) { + C negotiated = *ours; + negotiated.IntersectFeedbackParams(*theirs); + if (IsRtxCodec(negotiated)) { + // Only negotiate RTX if kCodecParamAssociatedPayloadType has been + // set. + std::string apt_value; + if (!theirs->GetParam(kCodecParamAssociatedPayloadType, &apt_value)) { + LOG(LS_WARNING) << "RTX missing associated payload type."; + continue; + } + negotiated.SetParam(kCodecParamAssociatedPayloadType, apt_value); + } + negotiated.id = theirs->id; + negotiated_codecs->push_back(negotiated); + } + } + } +} + +template +static bool FindMatchingCodec(const std::vector& codecs, + const C& codec_to_match, + C* found_codec) { + for (typename std::vector::const_iterator it = codecs.begin(); + it != codecs.end(); ++it) { + if (it->Matches(codec_to_match)) { + if (found_codec != NULL) { + *found_codec= *it; + } + return true; + } + } + return false; +} + +// Adds all codecs from |reference_codecs| to |offered_codecs| that dont' +// already exist in |offered_codecs| and ensure the payload types don't +// collide. +template +static void FindCodecsToOffer( + const std::vector& reference_codecs, + std::vector* offered_codecs, + UsedPayloadTypes* used_pltypes) { + + typedef std::map RtxCodecReferences; + RtxCodecReferences new_rtx_codecs; + + // Find all new RTX codecs. + for (typename std::vector::const_iterator it = reference_codecs.begin(); + it != reference_codecs.end(); ++it) { + if (!FindMatchingCodec(*offered_codecs, *it, NULL) && IsRtxCodec(*it)) { + C rtx_codec = *it; + int referenced_pl_type = + talk_base::FromString( + rtx_codec.params[kCodecParamAssociatedPayloadType]); + new_rtx_codecs.insert(std::pair(referenced_pl_type, + rtx_codec)); + } + } + + // Add all new codecs that are not RTX codecs. + for (typename std::vector::const_iterator it = reference_codecs.begin(); + it != reference_codecs.end(); ++it) { + if (!FindMatchingCodec(*offered_codecs, *it, NULL) && !IsRtxCodec(*it)) { + C codec = *it; + int original_payload_id = codec.id; + used_pltypes->FindAndSetIdUsed(&codec); + offered_codecs->push_back(codec); + + // If this codec is referenced by a new RTX codec, update the reference + // in the RTX codec with the new payload type. + typename RtxCodecReferences::iterator rtx_it = + new_rtx_codecs.find(original_payload_id); + if (rtx_it != new_rtx_codecs.end()) { + C& rtx_codec = rtx_it->second; + rtx_codec.params[kCodecParamAssociatedPayloadType] = + talk_base::ToString(codec.id); + } + } + } + + // Add all new RTX codecs. + for (typename RtxCodecReferences::iterator it = new_rtx_codecs.begin(); + it != new_rtx_codecs.end(); ++it) { + C& rtx_codec = it->second; + used_pltypes->FindAndSetIdUsed(&rtx_codec); + offered_codecs->push_back(rtx_codec); + } +} + + +static bool FindByUri(const RtpHeaderExtensions& extensions, + const RtpHeaderExtension& ext_to_match, + RtpHeaderExtension* found_extension) { + for (RtpHeaderExtensions::const_iterator it = extensions.begin(); + it != extensions.end(); ++it) { + // We assume that all URIs are given in a canonical format. + if (it->uri == ext_to_match.uri) { + if (found_extension != NULL) { + *found_extension= *it; + } + return true; + } + } + return false; +} + +static void FindAndSetRtpHdrExtUsed( + const RtpHeaderExtensions& reference_extensions, + RtpHeaderExtensions* offered_extensions, + UsedRtpHeaderExtensionIds* used_extensions) { + for (RtpHeaderExtensions::const_iterator it = reference_extensions.begin(); + it != reference_extensions.end(); ++it) { + if (!FindByUri(*offered_extensions, *it, NULL)) { + RtpHeaderExtension ext = *it; + used_extensions->FindAndSetIdUsed(&ext); + offered_extensions->push_back(ext); + } + } +} + +static void NegotiateRtpHeaderExtensions( + const RtpHeaderExtensions& local_extensions, + const RtpHeaderExtensions& offered_extensions, + RtpHeaderExtensions* negotiated_extenstions) { + RtpHeaderExtensions::const_iterator ours; + for (ours = local_extensions.begin(); + ours != local_extensions.end(); ++ours) { + RtpHeaderExtension theirs; + if (FindByUri(offered_extensions, *ours, &theirs)) { + // We respond with their RTP header extension id. + negotiated_extenstions->push_back(theirs); + } + } +} + +static void StripCNCodecs(AudioCodecs* audio_codecs) { + AudioCodecs::iterator iter = audio_codecs->begin(); + while (iter != audio_codecs->end()) { + if (stricmp(iter->name.c_str(), kComfortNoiseCodecName) == 0) { + iter = audio_codecs->erase(iter); + } else { + ++iter; + } + } +} + +// Create a media content to be answered in a session-accept, +// according to the given options.rtcp_mux, options.streams, codecs, +// crypto, and streams. If we don't currently have crypto (in +// current_cryptos) and it is enabled (in secure_policy), crypto is +// created (according to crypto_suites). If add_legacy_stream is +// true, and current_streams is empty, a legacy stream is created. +// The codecs, rtcp_mux, and crypto are all negotiated with the offer +// from the incoming session-initiate. If the negotiation fails, this +// method returns false. The created content is added to the offer. +template +static bool CreateMediaContentAnswer( + const MediaContentDescriptionImpl* offer, + const MediaSessionOptions& options, + const std::vector& local_codecs, + const SecureMediaPolicy& sdes_policy, + const CryptoParamsVec* current_cryptos, + const RtpHeaderExtensions& local_rtp_extenstions, + StreamParamsVec* current_streams, + bool add_legacy_stream, + bool bundle_enabled, + MediaContentDescriptionImpl* answer) { + std::vector negotiated_codecs; + NegotiateCodecs(local_codecs, offer->codecs(), &negotiated_codecs); + answer->AddCodecs(negotiated_codecs); + answer->SortCodecs(); + answer->set_protocol(offer->protocol()); + RtpHeaderExtensions negotiated_rtp_extensions; + NegotiateRtpHeaderExtensions(local_rtp_extenstions, + offer->rtp_header_extensions(), + &negotiated_rtp_extensions); + answer->set_rtp_header_extensions(negotiated_rtp_extensions); + + answer->set_rtcp_mux(options.rtcp_mux_enabled && offer->rtcp_mux()); + + if (sdes_policy != SEC_DISABLED) { + CryptoParams crypto; + if (SelectCrypto(offer, bundle_enabled, &crypto)) { + if (current_cryptos) { + FindMatchingCrypto(*current_cryptos, crypto, &crypto); + } + answer->AddCrypto(crypto); + } + } + + if (answer->cryptos().empty() && + (offer->crypto_required() || sdes_policy == SEC_REQUIRED)) { + return false; + } + + if (!AddStreamParams( + answer->type(), options.streams, current_streams, + answer, add_legacy_stream)) { + return false; // Something went seriously wrong. + } + + // Make sure the answer media content direction is per default set as + // described in RFC3264 section 6.1. + switch (offer->direction()) { + case MD_INACTIVE: + answer->set_direction(MD_INACTIVE); + break; + case MD_SENDONLY: + answer->set_direction(MD_RECVONLY); + break; + case MD_RECVONLY: + answer->set_direction(MD_SENDONLY); + break; + case MD_SENDRECV: + answer->set_direction(MD_SENDRECV); + break; + default: + break; + } + + return true; +} + +static bool IsMediaProtocolSupported(MediaType type, + const std::string& protocol) { + // Data channels can have a protocol of SCTP or SCTP/DTLS. + if (type == MEDIA_TYPE_DATA && + (protocol == kMediaProtocolSctp || + protocol == kMediaProtocolDtlsSctp)) { + return true; + } + // Since not all applications serialize and deserialize the media protocol, + // we will have to accept |protocol| to be empty. + return protocol == kMediaProtocolAvpf || protocol == kMediaProtocolSavpf || + protocol.empty(); +} + +static void SetMediaProtocol(bool secure_transport, + MediaContentDescription* desc) { + if (!desc->cryptos().empty() || secure_transport) + desc->set_protocol(kMediaProtocolSavpf); + else + desc->set_protocol(kMediaProtocolAvpf); +} + +void MediaSessionOptions::AddStream(MediaType type, + const std::string& id, + const std::string& sync_label) { + streams.push_back(Stream(type, id, sync_label)); + + if (type == MEDIA_TYPE_VIDEO) + has_video = true; + else if (type == MEDIA_TYPE_AUDIO) + has_audio = true; + // If we haven't already set the data_channel_type, and we add a + // stream, we assume it's an RTP data stream. + else if (type == MEDIA_TYPE_DATA && data_channel_type == DCT_NONE) + data_channel_type = DCT_RTP; +} + +void MediaSessionOptions::RemoveStream(MediaType type, + const std::string& id) { + Streams::iterator stream_it = streams.begin(); + for (; stream_it != streams.end(); ++stream_it) { + if (stream_it->type == type && stream_it->id == id) { + streams.erase(stream_it); + return; + } + } + ASSERT(false); +} + +MediaSessionDescriptionFactory::MediaSessionDescriptionFactory( + const TransportDescriptionFactory* transport_desc_factory) + : secure_(SEC_DISABLED), + add_legacy_(true), + transport_desc_factory_(transport_desc_factory) { +} + +MediaSessionDescriptionFactory::MediaSessionDescriptionFactory( + ChannelManager* channel_manager, + const TransportDescriptionFactory* transport_desc_factory) + : secure_(SEC_DISABLED), + add_legacy_(true), + transport_desc_factory_(transport_desc_factory) { + channel_manager->GetSupportedAudioCodecs(&audio_codecs_); + channel_manager->GetSupportedAudioRtpHeaderExtensions(&audio_rtp_extensions_); + channel_manager->GetSupportedVideoCodecs(&video_codecs_); + channel_manager->GetSupportedVideoRtpHeaderExtensions(&video_rtp_extensions_); + channel_manager->GetSupportedDataCodecs(&data_codecs_); +} + +SessionDescription* MediaSessionDescriptionFactory::CreateOffer( + const MediaSessionOptions& options, + const SessionDescription* current_description) const { + bool secure_transport = (transport_desc_factory_->secure() != SEC_DISABLED); + + scoped_ptr offer(new SessionDescription()); + + StreamParamsVec current_streams; + GetCurrentStreamParams(current_description, ¤t_streams); + + AudioCodecs audio_codecs; + VideoCodecs video_codecs; + DataCodecs data_codecs; + GetCodecsToOffer(current_description, &audio_codecs, &video_codecs, + &data_codecs); + + if (!options.vad_enabled) { + // If application doesn't want CN codecs in offer. + StripCNCodecs(&audio_codecs); + } + + RtpHeaderExtensions audio_rtp_extensions; + RtpHeaderExtensions video_rtp_extensions; + GetRtpHdrExtsToOffer(current_description, &audio_rtp_extensions, + &video_rtp_extensions); + + // Handle m=audio. + if (options.has_audio) { + scoped_ptr audio(new AudioContentDescription()); + std::vector crypto_suites; + GetSupportedAudioCryptoSuites(&crypto_suites); + if (!CreateMediaContentOffer( + options, + audio_codecs, + secure(), + GetCryptos(GetFirstAudioContentDescription(current_description)), + crypto_suites, + audio_rtp_extensions, + add_legacy_, + ¤t_streams, + audio.get())) { + return NULL; + } + + audio->set_lang(lang_); + SetMediaProtocol(secure_transport, audio.get()); + offer->AddContent(CN_AUDIO, NS_JINGLE_RTP, audio.release()); + if (!AddTransportOffer(CN_AUDIO, options.transport_options, + current_description, offer.get())) { + return NULL; + } + } + + // Handle m=video. + if (options.has_video) { + scoped_ptr video(new VideoContentDescription()); + std::vector crypto_suites; + GetSupportedVideoCryptoSuites(&crypto_suites); + if (!CreateMediaContentOffer( + options, + video_codecs, + secure(), + GetCryptos(GetFirstVideoContentDescription(current_description)), + crypto_suites, + video_rtp_extensions, + add_legacy_, + ¤t_streams, + video.get())) { + return NULL; + } + + video->set_bandwidth(options.video_bandwidth); + SetMediaProtocol(secure_transport, video.get()); + offer->AddContent(CN_VIDEO, NS_JINGLE_RTP, video.release()); + if (!AddTransportOffer(CN_VIDEO, options.transport_options, + current_description, offer.get())) { + return NULL; + } + } + + // Handle m=data. + if (options.has_data()) { + scoped_ptr data(new DataContentDescription()); + bool is_sctp = (options.data_channel_type == DCT_SCTP); + + std::vector crypto_suites; + cricket::SecurePolicy sdes_policy = secure(); + if (is_sctp) { + // SDES doesn't make sense for SCTP, so we disable it, and we only + // get SDES crypto suites for RTP-based data channels. + sdes_policy = cricket::SEC_DISABLED; + // Unlike SetMediaProtocol below, we need to set the protocol + // before we call CreateMediaContentOffer. Otherwise, + // CreateMediaContentOffer won't know this is SCTP and will + // generate SSRCs rather than SIDs. + data->set_protocol( + secure_transport ? kMediaProtocolDtlsSctp : kMediaProtocolSctp); + } else { + GetSupportedDataCryptoSuites(&crypto_suites); + } + + if (!CreateMediaContentOffer( + options, + data_codecs, + sdes_policy, + GetCryptos(GetFirstDataContentDescription(current_description)), + crypto_suites, + RtpHeaderExtensions(), + add_legacy_, + ¤t_streams, + data.get())) { + return NULL; + } + + if (is_sctp) { + offer->AddContent(CN_DATA, NS_JINGLE_DRAFT_SCTP, data.release()); + } else { + data->set_bandwidth(options.data_bandwidth); + SetMediaProtocol(secure_transport, data.get()); + offer->AddContent(CN_DATA, NS_JINGLE_RTP, data.release()); + } + if (!AddTransportOffer(CN_DATA, options.transport_options, + current_description, offer.get())) { + return NULL; + } + } + + // Bundle the contents together, if we've been asked to do so, and update any + // parameters that need to be tweaked for BUNDLE. + if (options.bundle_enabled) { + ContentGroup offer_bundle(GROUP_TYPE_BUNDLE); + for (ContentInfos::const_iterator content = offer->contents().begin(); + content != offer->contents().end(); ++content) { + offer_bundle.AddContentName(content->name); + } + offer->AddGroup(offer_bundle); + if (!UpdateTransportInfoForBundle(offer_bundle, offer.get())) { + LOG(LS_ERROR) << "CreateOffer failed to UpdateTransportInfoForBundle."; + return NULL; + } + if (!UpdateCryptoParamsForBundle(offer_bundle, offer.get())) { + LOG(LS_ERROR) << "CreateOffer failed to UpdateCryptoParamsForBundle."; + return NULL; + } + } + + return offer.release(); +} + +SessionDescription* MediaSessionDescriptionFactory::CreateAnswer( + const SessionDescription* offer, const MediaSessionOptions& options, + const SessionDescription* current_description) const { + // The answer contains the intersection of the codecs in the offer with the + // codecs we support, ordered by our local preference. As indicated by + // XEP-0167, we retain the same payload ids from the offer in the answer. + scoped_ptr answer(new SessionDescription()); + + StreamParamsVec current_streams; + GetCurrentStreamParams(current_description, ¤t_streams); + + bool bundle_enabled = + offer->HasGroup(GROUP_TYPE_BUNDLE) && options.bundle_enabled; + + // Handle m=audio. + const ContentInfo* audio_content = GetFirstAudioContent(offer); + if (audio_content) { + scoped_ptr audio_transport( + CreateTransportAnswer(audio_content->name, offer, + options.transport_options, + current_description)); + if (!audio_transport) { + return NULL; + } + + AudioCodecs audio_codecs = audio_codecs_; + if (!options.vad_enabled) { + StripCNCodecs(&audio_codecs); + } + + scoped_ptr audio_answer( + new AudioContentDescription()); + // Do not require or create SDES cryptos if DTLS is used. + cricket::SecurePolicy sdes_policy = + audio_transport->secure() ? cricket::SEC_DISABLED : secure(); + if (!CreateMediaContentAnswer( + static_cast( + audio_content->description), + options, + audio_codecs, + sdes_policy, + GetCryptos(GetFirstAudioContentDescription(current_description)), + audio_rtp_extensions_, + ¤t_streams, + add_legacy_, + bundle_enabled, + audio_answer.get())) { + return NULL; // Fails the session setup. + } + + bool rejected = !options.has_audio || audio_content->rejected || + !IsMediaProtocolSupported(MEDIA_TYPE_AUDIO, + audio_answer->protocol()); + if (!rejected) { + AddTransportAnswer(audio_content->name, *(audio_transport.get()), + answer.get()); + } else { + // RFC 3264 + // The answer MUST contain the same number of m-lines as the offer. + LOG(LS_INFO) << "Audio is not supported in the answer."; + } + + answer->AddContent(audio_content->name, audio_content->type, rejected, + audio_answer.release()); + } else { + LOG(LS_INFO) << "Audio is not available in the offer."; + } + + // Handle m=video. + const ContentInfo* video_content = GetFirstVideoContent(offer); + if (video_content) { + scoped_ptr video_transport( + CreateTransportAnswer(video_content->name, offer, + options.transport_options, + current_description)); + if (!video_transport) { + return NULL; + } + + scoped_ptr video_answer( + new VideoContentDescription()); + // Do not require or create SDES cryptos if DTLS is used. + cricket::SecurePolicy sdes_policy = + video_transport->secure() ? cricket::SEC_DISABLED : secure(); + if (!CreateMediaContentAnswer( + static_cast( + video_content->description), + options, + video_codecs_, + sdes_policy, + GetCryptos(GetFirstVideoContentDescription(current_description)), + video_rtp_extensions_, + ¤t_streams, + add_legacy_, + bundle_enabled, + video_answer.get())) { + return NULL; + } + bool rejected = !options.has_video || video_content->rejected || + !IsMediaProtocolSupported(MEDIA_TYPE_VIDEO, video_answer->protocol()); + if (!rejected) { + if (!AddTransportAnswer(video_content->name, *(video_transport.get()), + answer.get())) { + return NULL; + } + video_answer->set_bandwidth(options.video_bandwidth); + } else { + // RFC 3264 + // The answer MUST contain the same number of m-lines as the offer. + LOG(LS_INFO) << "Video is not supported in the answer."; + } + answer->AddContent(video_content->name, video_content->type, rejected, + video_answer.release()); + } else { + LOG(LS_INFO) << "Video is not available in the offer."; + } + + // Handle m=data. + const ContentInfo* data_content = GetFirstDataContent(offer); + if (data_content) { + scoped_ptr data_transport( + CreateTransportAnswer(data_content->name, offer, + options.transport_options, + current_description)); + if (!data_transport) { + return NULL; + } + scoped_ptr data_answer( + new DataContentDescription()); + // Do not require or create SDES cryptos if DTLS is used. + cricket::SecurePolicy sdes_policy = + data_transport->secure() ? cricket::SEC_DISABLED : secure(); + if (!CreateMediaContentAnswer( + static_cast( + data_content->description), + options, + data_codecs_, + sdes_policy, + GetCryptos(GetFirstDataContentDescription(current_description)), + RtpHeaderExtensions(), + ¤t_streams, + add_legacy_, + bundle_enabled, + data_answer.get())) { + return NULL; // Fails the session setup. + } + + bool rejected = !options.has_data() || data_content->rejected || + !IsMediaProtocolSupported(MEDIA_TYPE_DATA, data_answer->protocol()); + if (!rejected) { + data_answer->set_bandwidth(options.data_bandwidth); + if (!AddTransportAnswer(data_content->name, *(data_transport.get()), + answer.get())) { + return NULL; + } + } else { + // RFC 3264 + // The answer MUST contain the same number of m-lines as the offer. + LOG(LS_INFO) << "Data is not supported in the answer."; + } + answer->AddContent(data_content->name, data_content->type, rejected, + data_answer.release()); + } else { + LOG(LS_INFO) << "Data is not available in the offer."; + } + + // If the offer supports BUNDLE, and we want to use it too, create a BUNDLE + // group in the answer with the appropriate content names. + if (offer->HasGroup(GROUP_TYPE_BUNDLE) && options.bundle_enabled) { + const ContentGroup* offer_bundle = offer->GetGroupByName(GROUP_TYPE_BUNDLE); + ContentGroup answer_bundle(GROUP_TYPE_BUNDLE); + for (ContentInfos::const_iterator content = answer->contents().begin(); + content != answer->contents().end(); ++content) { + if (!content->rejected && offer_bundle->HasContentName(content->name)) { + answer_bundle.AddContentName(content->name); + } + } + if (answer_bundle.FirstContentName()) { + answer->AddGroup(answer_bundle); + + // Share the same ICE credentials and crypto params across all contents, + // as BUNDLE requires. + if (!UpdateTransportInfoForBundle(answer_bundle, answer.get())) { + LOG(LS_ERROR) << "CreateAnswer failed to UpdateTransportInfoForBundle."; + return NULL; + } + + if (!UpdateCryptoParamsForBundle(answer_bundle, answer.get())) { + LOG(LS_ERROR) << "CreateAnswer failed to UpdateCryptoParamsForBundle."; + return NULL; + } + } + } + + return answer.release(); +} + +// Gets the TransportInfo of the given |content_name| from the +// |current_description|. If doesn't exist, returns a new one. +static const TransportDescription* GetTransportDescription( + const std::string& content_name, + const SessionDescription* current_description) { + const TransportDescription* desc = NULL; + if (current_description) { + const TransportInfo* info = + current_description->GetTransportInfoByName(content_name); + if (info) { + desc = &info->description; + } + } + return desc; +} + +void MediaSessionDescriptionFactory::GetCodecsToOffer( + const SessionDescription* current_description, + AudioCodecs* audio_codecs, + VideoCodecs* video_codecs, + DataCodecs* data_codecs) const { + UsedPayloadTypes used_pltypes; + audio_codecs->clear(); + video_codecs->clear(); + data_codecs->clear(); + + + // First - get all codecs from the current description if the media type + // is used. + // Add them to |used_pltypes| so the payloadtype is not reused if a new media + // type is added. + if (current_description) { + const AudioContentDescription* audio = + GetFirstAudioContentDescription(current_description); + if (audio) { + *audio_codecs = audio->codecs(); + used_pltypes.FindAndSetIdUsed(audio_codecs); + } + const VideoContentDescription* video = + GetFirstVideoContentDescription(current_description); + if (video) { + *video_codecs = video->codecs(); + used_pltypes.FindAndSetIdUsed(video_codecs); + } + const DataContentDescription* data = + GetFirstDataContentDescription(current_description); + if (data) { + *data_codecs = data->codecs(); + used_pltypes.FindAndSetIdUsed(data_codecs); + } + } + + // Add our codecs that are not in |current_description|. + FindCodecsToOffer(audio_codecs_, audio_codecs, &used_pltypes); + FindCodecsToOffer(video_codecs_, video_codecs, &used_pltypes); + FindCodecsToOffer(data_codecs_, data_codecs, &used_pltypes); +} + +void MediaSessionDescriptionFactory::GetRtpHdrExtsToOffer( + const SessionDescription* current_description, + RtpHeaderExtensions* audio_extensions, + RtpHeaderExtensions* video_extensions) const { + UsedRtpHeaderExtensionIds used_ids; + audio_extensions->clear(); + video_extensions->clear(); + + // First - get all extensions from the current description if the media type + // is used. + // Add them to |used_ids| so the local ids are not reused if a new media + // type is added. + if (current_description) { + const AudioContentDescription* audio = + GetFirstAudioContentDescription(current_description); + if (audio) { + *audio_extensions = audio->rtp_header_extensions(); + used_ids.FindAndSetIdUsed(audio_extensions); + } + const VideoContentDescription* video = + GetFirstVideoContentDescription(current_description); + if (video) { + *video_extensions = video->rtp_header_extensions(); + used_ids.FindAndSetIdUsed(video_extensions); + } + } + + // Add our default RTP header extensions that are not in + // |current_description|. + FindAndSetRtpHdrExtUsed(audio_rtp_header_extensions(), audio_extensions, + &used_ids); + FindAndSetRtpHdrExtUsed(video_rtp_header_extensions(), video_extensions, + &used_ids); +} + +bool MediaSessionDescriptionFactory::AddTransportOffer( + const std::string& content_name, + const TransportOptions& transport_options, + const SessionDescription* current_desc, + SessionDescription* offer_desc) const { + if (!transport_desc_factory_) + return false; + const TransportDescription* current_tdesc = + GetTransportDescription(content_name, current_desc); + talk_base::scoped_ptr new_tdesc( + transport_desc_factory_->CreateOffer(transport_options, current_tdesc)); + bool ret = (new_tdesc.get() != NULL && + offer_desc->AddTransportInfo(TransportInfo(content_name, *new_tdesc))); + if (!ret) { + LOG(LS_ERROR) + << "Failed to AddTransportOffer, content name=" << content_name; + } + return ret; +} + +TransportDescription* MediaSessionDescriptionFactory::CreateTransportAnswer( + const std::string& content_name, + const SessionDescription* offer_desc, + const TransportOptions& transport_options, + const SessionDescription* current_desc) const { + if (!transport_desc_factory_) + return NULL; + const TransportDescription* offer_tdesc = + GetTransportDescription(content_name, offer_desc); + const TransportDescription* current_tdesc = + GetTransportDescription(content_name, current_desc); + return + transport_desc_factory_->CreateAnswer(offer_tdesc, transport_options, + current_tdesc); +} + +bool MediaSessionDescriptionFactory::AddTransportAnswer( + const std::string& content_name, + const TransportDescription& transport_desc, + SessionDescription* answer_desc) const { + if (!answer_desc->AddTransportInfo(TransportInfo(content_name, + transport_desc))) { + LOG(LS_ERROR) + << "Failed to AddTransportAnswer, content name=" << content_name; + return false; + } + return true; +} + +bool IsMediaContent(const ContentInfo* content) { + return (content && + (content->type == NS_JINGLE_RTP || + content->type == NS_JINGLE_DRAFT_SCTP)); +} + +bool IsAudioContent(const ContentInfo* content) { + return IsMediaContentOfType(content, MEDIA_TYPE_AUDIO); +} + +bool IsVideoContent(const ContentInfo* content) { + return IsMediaContentOfType(content, MEDIA_TYPE_VIDEO); +} + +bool IsDataContent(const ContentInfo* content) { + return IsMediaContentOfType(content, MEDIA_TYPE_DATA); +} + +static const ContentInfo* GetFirstMediaContent(const ContentInfos& contents, + MediaType media_type) { + for (ContentInfos::const_iterator content = contents.begin(); + content != contents.end(); content++) { + if (IsMediaContentOfType(&*content, media_type)) { + return &*content; + } + } + return NULL; +} + +const ContentInfo* GetFirstAudioContent(const ContentInfos& contents) { + return GetFirstMediaContent(contents, MEDIA_TYPE_AUDIO); +} + +const ContentInfo* GetFirstVideoContent(const ContentInfos& contents) { + return GetFirstMediaContent(contents, MEDIA_TYPE_VIDEO); +} + +const ContentInfo* GetFirstDataContent(const ContentInfos& contents) { + return GetFirstMediaContent(contents, MEDIA_TYPE_DATA); +} + +static const ContentInfo* GetFirstMediaContent(const SessionDescription* sdesc, + MediaType media_type) { + if (sdesc == NULL) + return NULL; + + return GetFirstMediaContent(sdesc->contents(), media_type); +} + +const ContentInfo* GetFirstAudioContent(const SessionDescription* sdesc) { + return GetFirstMediaContent(sdesc, MEDIA_TYPE_AUDIO); +} + +const ContentInfo* GetFirstVideoContent(const SessionDescription* sdesc) { + return GetFirstMediaContent(sdesc, MEDIA_TYPE_VIDEO); +} + +const ContentInfo* GetFirstDataContent(const SessionDescription* sdesc) { + return GetFirstMediaContent(sdesc, MEDIA_TYPE_DATA); +} + +const MediaContentDescription* GetFirstMediaContentDescription( + const SessionDescription* sdesc, MediaType media_type) { + const ContentInfo* content = GetFirstMediaContent(sdesc, media_type); + const ContentDescription* description = content ? content->description : NULL; + return static_cast(description); +} + +const AudioContentDescription* GetFirstAudioContentDescription( + const SessionDescription* sdesc) { + return static_cast( + GetFirstMediaContentDescription(sdesc, MEDIA_TYPE_AUDIO)); +} + +const VideoContentDescription* GetFirstVideoContentDescription( + const SessionDescription* sdesc) { + return static_cast( + GetFirstMediaContentDescription(sdesc, MEDIA_TYPE_VIDEO)); +} + +const DataContentDescription* GetFirstDataContentDescription( + const SessionDescription* sdesc) { + return static_cast( + GetFirstMediaContentDescription(sdesc, MEDIA_TYPE_DATA)); +} + +bool GetMediaChannelNameFromComponent( + int component, MediaType media_type, std::string* channel_name) { + if (media_type == MEDIA_TYPE_AUDIO) { + if (component == ICE_CANDIDATE_COMPONENT_RTP) { + *channel_name = GICE_CHANNEL_NAME_RTP; + return true; + } else if (component == ICE_CANDIDATE_COMPONENT_RTCP) { + *channel_name = GICE_CHANNEL_NAME_RTCP; + return true; + } + } else if (media_type == MEDIA_TYPE_VIDEO) { + if (component == ICE_CANDIDATE_COMPONENT_RTP) { + *channel_name = GICE_CHANNEL_NAME_VIDEO_RTP; + return true; + } else if (component == ICE_CANDIDATE_COMPONENT_RTCP) { + *channel_name = GICE_CHANNEL_NAME_VIDEO_RTCP; + return true; + } + } else if (media_type == MEDIA_TYPE_DATA) { + if (component == ICE_CANDIDATE_COMPONENT_RTP) { + *channel_name = GICE_CHANNEL_NAME_DATA_RTP; + return true; + } else if (component == ICE_CANDIDATE_COMPONENT_RTCP) { + *channel_name = GICE_CHANNEL_NAME_DATA_RTCP; + return true; + } + } + + return false; +} + +bool GetMediaComponentFromChannelName( + const std::string& channel_name, int* component) { + if (channel_name == GICE_CHANNEL_NAME_RTP || + channel_name == GICE_CHANNEL_NAME_VIDEO_RTP || + channel_name == GICE_CHANNEL_NAME_DATA_RTP) { + *component = ICE_CANDIDATE_COMPONENT_RTP; + return true; + } else if (channel_name == GICE_CHANNEL_NAME_RTCP || + channel_name == GICE_CHANNEL_NAME_VIDEO_RTCP || + channel_name == GICE_CHANNEL_NAME_DATA_RTP) { + *component = ICE_CANDIDATE_COMPONENT_RTCP; + return true; + } + + return false; +} + +bool GetMediaTypeFromChannelName( + const std::string& channel_name, MediaType* media_type) { + if (channel_name == GICE_CHANNEL_NAME_RTP || + channel_name == GICE_CHANNEL_NAME_RTCP) { + *media_type = MEDIA_TYPE_AUDIO; + return true; + } else if (channel_name == GICE_CHANNEL_NAME_VIDEO_RTP || + channel_name == GICE_CHANNEL_NAME_VIDEO_RTCP) { + *media_type = MEDIA_TYPE_VIDEO; + return true; + } else if (channel_name == GICE_CHANNEL_NAME_DATA_RTP || + channel_name == GICE_CHANNEL_NAME_DATA_RTCP) { + *media_type = MEDIA_TYPE_DATA; + return true; + } + + return false; +} + +} // namespace cricket diff --git a/talk/session/media/mediasession.h b/talk/session/media/mediasession.h new file mode 100644 index 000000000..327480466 --- /dev/null +++ b/talk/session/media/mediasession.h @@ -0,0 +1,497 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +// Types and classes used in media session descriptions. + +#ifndef TALK_SESSION_MEDIA_MEDIASESSION_H_ +#define TALK_SESSION_MEDIA_MEDIASESSION_H_ + +#include +#include +#include + +#include "talk/base/scoped_ptr.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/constants.h" +#include "talk/media/base/cryptoparams.h" +#include "talk/media/base/mediachannel.h" +#include "talk/media/base/mediaengine.h" // For DataChannelType +#include "talk/media/base/streamparams.h" +#include "talk/p2p/base/sessiondescription.h" +#include "talk/p2p/base/transport.h" +#include "talk/p2p/base/transportdescriptionfactory.h" + +namespace cricket { + +class ChannelManager; +typedef std::vector AudioCodecs; +typedef std::vector VideoCodecs; +typedef std::vector DataCodecs; +typedef std::vector CryptoParamsVec; +typedef std::vector RtpHeaderExtensions; + +// TODO(juberti): Replace SecureMediaPolicy with SecurePolicy everywhere. +typedef SecurePolicy SecureMediaPolicy; + +enum MediaType { + MEDIA_TYPE_AUDIO, + MEDIA_TYPE_VIDEO, + MEDIA_TYPE_DATA +}; + +enum MediaContentDirection { + MD_INACTIVE, + MD_SENDONLY, + MD_RECVONLY, + MD_SENDRECV +}; + +// RTC4585 RTP/AVPF +extern const char kMediaProtocolAvpf[]; +// RFC5124 RTP/SAVPF +extern const char kMediaProtocolSavpf[]; + +extern const char kMediaProtocolRtpPrefix[]; + +extern const char kMediaProtocolSctp[]; +extern const char kMediaProtocolDtlsSctp[]; + +// Options to control how session descriptions are generated. +const int kAutoBandwidth = -1; +const int kBufferedModeDisabled = 0; +// TODO(pthatcher): This is imposed by usrsctp lib. I have no idea +// why it is 9. Figure out why, and make it bigger, hopefully up to +// 2^16-1. +const uint32 kMaxSctpSid = 9; + +struct MediaSessionOptions { + MediaSessionOptions() : + has_audio(true), // Audio enabled by default. + has_video(false), + data_channel_type(DCT_NONE), + is_muc(false), + vad_enabled(true), // When disabled, removes all CN codecs from SDP. + rtcp_mux_enabled(true), + bundle_enabled(false), + video_bandwidth(kAutoBandwidth), + data_bandwidth(kDataMaxBandwidth) { + } + + bool has_data() const { return data_channel_type != DCT_NONE; } + + // Add a stream with MediaType type and id. + // All streams with the same sync_label will get the same CNAME. + // All ids must be unique. + void AddStream(MediaType type, + const std::string& id, + const std::string& sync_label); + void RemoveStream(MediaType type, const std::string& id); + + bool has_audio; + bool has_video; + DataChannelType data_channel_type; + bool is_muc; + bool vad_enabled; + bool rtcp_mux_enabled; + bool bundle_enabled; + // bps. -1 == auto. + int video_bandwidth; + int data_bandwidth; + TransportOptions transport_options; + + struct Stream { + Stream(MediaType type, + const std::string& id, + const std::string& sync_label) + : type(type), id(id), sync_label(sync_label) { + } + MediaType type; + std::string id; + std::string sync_label; + }; + + typedef std::vector Streams; + Streams streams; +}; + +// "content" (as used in XEP-0166) descriptions for voice and video. +class MediaContentDescription : public ContentDescription { + public: + MediaContentDescription() + : rtcp_mux_(false), + bandwidth_(kAutoBandwidth), + crypto_required_(false), + rtp_header_extensions_set_(false), + multistream_(false), + conference_mode_(false), + partial_(false), + buffered_mode_latency_(kBufferedModeDisabled), + direction_(MD_SENDRECV) { + } + + virtual MediaType type() const = 0; + virtual bool has_codecs() const = 0; + + // |protocol| is the expected media transport protocol, such as RTP/AVPF, + // RTP/SAVPF or SCTP/DTLS. + std::string protocol() const { return protocol_; } + void set_protocol(const std::string& protocol) { protocol_ = protocol; } + + MediaContentDirection direction() const { return direction_; } + void set_direction(MediaContentDirection direction) { + direction_ = direction; + } + + bool rtcp_mux() const { return rtcp_mux_; } + void set_rtcp_mux(bool mux) { rtcp_mux_ = mux; } + + int bandwidth() const { return bandwidth_; } + void set_bandwidth(int bandwidth) { bandwidth_ = bandwidth; } + + const std::vector& cryptos() const { return cryptos_; } + void AddCrypto(const CryptoParams& params) { + cryptos_.push_back(params); + } + void set_cryptos(const std::vector& cryptos) { + cryptos_ = cryptos; + } + bool crypto_required() const { return crypto_required_; } + void set_crypto_required(bool crypto) { + crypto_required_ = crypto; + } + + const RtpHeaderExtensions& rtp_header_extensions() const { + return rtp_header_extensions_; + } + void set_rtp_header_extensions(const RtpHeaderExtensions& extensions) { + rtp_header_extensions_ = extensions; + rtp_header_extensions_set_ = true; + } + void AddRtpHeaderExtension(const RtpHeaderExtension& ext) { + rtp_header_extensions_.push_back(ext); + rtp_header_extensions_set_ = true; + } + void ClearRtpHeaderExtensions() { + rtp_header_extensions_.clear(); + rtp_header_extensions_set_ = true; + } + // We can't always tell if an empty list of header extensions is + // because the other side doesn't support them, or just isn't hooked up to + // signal them. For now we assume an empty list means no signaling, but + // provide the ClearRtpHeaderExtensions method to allow "no support" to be + // clearly indicated (i.e. when derived from other information). + bool rtp_header_extensions_set() const { + return rtp_header_extensions_set_; + } + // True iff the client supports multiple streams. + void set_multistream(bool multistream) { multistream_ = multistream; } + bool multistream() const { return multistream_; } + const StreamParamsVec& streams() const { + return streams_; + } + // TODO(pthatcher): Remove this by giving mediamessage.cc access + // to MediaContentDescription + StreamParamsVec& mutable_streams() { + return streams_; + } + void AddStream(const StreamParams& stream) { + streams_.push_back(stream); + } + // Legacy streams have an ssrc, but nothing else. + void AddLegacyStream(uint32 ssrc) { + streams_.push_back(StreamParams::CreateLegacy(ssrc)); + } + void AddLegacyStream(uint32 ssrc, uint32 fid_ssrc) { + StreamParams sp = StreamParams::CreateLegacy(ssrc); + sp.AddFidSsrc(ssrc, fid_ssrc); + streams_.push_back(sp); + } + // Sets the CNAME of all StreamParams if it have not been set. + // This can be used to set the CNAME of legacy streams. + void SetCnameIfEmpty(const std::string& cname) { + for (cricket::StreamParamsVec::iterator it = streams_.begin(); + it != streams_.end(); ++it) { + if (it->cname.empty()) + it->cname = cname; + } + } + uint32 first_ssrc() const { + if (streams_.empty()) { + return 0; + } + return streams_[0].first_ssrc(); + } + bool has_ssrcs() const { + if (streams_.empty()) { + return false; + } + return streams_[0].has_ssrcs(); + } + + void set_conference_mode(bool enable) { conference_mode_ = enable; } + bool conference_mode() const { return conference_mode_; } + + void set_partial(bool partial) { partial_ = partial; } + bool partial() const { return partial_; } + + void set_buffered_mode_latency(int latency) { + buffered_mode_latency_ = latency; + } + int buffered_mode_latency() const { return buffered_mode_latency_; } + + protected: + bool rtcp_mux_; + int bandwidth_; + std::string protocol_; + std::vector cryptos_; + bool crypto_required_; + std::vector rtp_header_extensions_; + bool rtp_header_extensions_set_; + bool multistream_; + StreamParamsVec streams_; + bool conference_mode_; + bool partial_; + int buffered_mode_latency_; + MediaContentDirection direction_; +}; + +template +class MediaContentDescriptionImpl : public MediaContentDescription { + public: + struct PreferenceSort { + bool operator()(C a, C b) { return a.preference > b.preference; } + }; + + const std::vector& codecs() const { return codecs_; } + void set_codecs(const std::vector& codecs) { codecs_ = codecs; } + virtual bool has_codecs() const { return !codecs_.empty(); } + bool HasCodec(int id) { + bool found = false; + for (typename std::vector::iterator iter = codecs_.begin(); + iter != codecs_.end(); ++iter) { + if (iter->id == id) { + found = true; + break; + } + } + return found; + } + void AddCodec(const C& codec) { + codecs_.push_back(codec); + } + void AddCodecs(const std::vector& codecs) { + typename std::vector::const_iterator codec; + for (codec = codecs.begin(); codec != codecs.end(); ++codec) { + AddCodec(*codec); + } + } + void SortCodecs() { + std::sort(codecs_.begin(), codecs_.end(), PreferenceSort()); + } + + private: + std::vector codecs_; +}; + +class AudioContentDescription : public MediaContentDescriptionImpl { + public: + AudioContentDescription() : + agc_minus_10db_(false) {} + + virtual ContentDescription* Copy() const { + return new AudioContentDescription(*this); + } + virtual MediaType type() const { return MEDIA_TYPE_AUDIO; } + + const std::string &lang() const { return lang_; } + void set_lang(const std::string &lang) { lang_ = lang; } + + bool agc_minus_10db() const { return agc_minus_10db_; } + void set_agc_minus_10db(bool enable) { + agc_minus_10db_ = enable; + } + + private: + bool agc_minus_10db_; + + private: + std::string lang_; +}; + +class VideoContentDescription : public MediaContentDescriptionImpl { + public: + virtual ContentDescription* Copy() const { + return new VideoContentDescription(*this); + } + virtual MediaType type() const { return MEDIA_TYPE_VIDEO; } +}; + +class DataContentDescription : public MediaContentDescriptionImpl { + public: + virtual ContentDescription* Copy() const { + return new DataContentDescription(*this); + } + virtual MediaType type() const { return MEDIA_TYPE_DATA; } +}; + +// Creates media session descriptions according to the supplied codecs and +// other fields, as well as the supplied per-call options. +// When creating answers, performs the appropriate negotiation +// of the various fields to determine the proper result. +class MediaSessionDescriptionFactory { + public: + // Default ctor; use methods below to set configuration. + // The TransportDescriptionFactory is not owned by MediaSessionDescFactory, + // so it must be kept alive by the user of this class. + explicit MediaSessionDescriptionFactory( + const TransportDescriptionFactory* factory); + // This helper automatically sets up the factory to get its configuration + // from the specified ChannelManager. + MediaSessionDescriptionFactory(ChannelManager* cmanager, + const TransportDescriptionFactory* factory); + + const AudioCodecs& audio_codecs() const { return audio_codecs_; } + void set_audio_codecs(const AudioCodecs& codecs) { audio_codecs_ = codecs; } + void set_audio_rtp_header_extensions(const RtpHeaderExtensions& extensions) { + audio_rtp_extensions_ = extensions; + } + const RtpHeaderExtensions& audio_rtp_header_extensions() const { + return audio_rtp_extensions_; + } + const VideoCodecs& video_codecs() const { return video_codecs_; } + void set_video_codecs(const VideoCodecs& codecs) { video_codecs_ = codecs; } + void set_video_rtp_header_extensions(const RtpHeaderExtensions& extensions) { + video_rtp_extensions_ = extensions; + } + const RtpHeaderExtensions& video_rtp_header_extensions() const { + return video_rtp_extensions_; + } + const DataCodecs& data_codecs() const { return data_codecs_; } + void set_data_codecs(const DataCodecs& codecs) { data_codecs_ = codecs; } + SecurePolicy secure() const { return secure_; } + void set_secure(SecurePolicy s) { secure_ = s; } + // Decides if a StreamParams shall be added to the audio and video media + // content in SessionDescription when CreateOffer and CreateAnswer is called + // even if |options| don't include a Stream. This is needed to support legacy + // applications. |add_legacy_| is true per default. + void set_add_legacy_streams(bool add_legacy) { add_legacy_ = add_legacy; } + + SessionDescription* CreateOffer( + const MediaSessionOptions& options, + const SessionDescription* current_description) const; + SessionDescription* CreateAnswer( + const SessionDescription* offer, + const MediaSessionOptions& options, + const SessionDescription* current_description) const; + + private: + void GetCodecsToOffer(const SessionDescription* current_description, + AudioCodecs* audio_codecs, + VideoCodecs* video_codecs, + DataCodecs* data_codecs) const; + void GetRtpHdrExtsToOffer(const SessionDescription* current_description, + RtpHeaderExtensions* audio_extensions, + RtpHeaderExtensions* video_extensions) const; + bool AddTransportOffer( + const std::string& content_name, + const TransportOptions& transport_options, + const SessionDescription* current_desc, + SessionDescription* offer) const; + + TransportDescription* CreateTransportAnswer( + const std::string& content_name, + const SessionDescription* offer_desc, + const TransportOptions& transport_options, + const SessionDescription* current_desc) const; + + bool AddTransportAnswer( + const std::string& content_name, + const TransportDescription& transport_desc, + SessionDescription* answer_desc) const; + + AudioCodecs audio_codecs_; + RtpHeaderExtensions audio_rtp_extensions_; + VideoCodecs video_codecs_; + RtpHeaderExtensions video_rtp_extensions_; + DataCodecs data_codecs_; + SecurePolicy secure_; + bool add_legacy_; + std::string lang_; + const TransportDescriptionFactory* transport_desc_factory_; +}; + +// Convenience functions. +bool IsMediaContent(const ContentInfo* content); +bool IsAudioContent(const ContentInfo* content); +bool IsVideoContent(const ContentInfo* content); +bool IsDataContent(const ContentInfo* content); +const ContentInfo* GetFirstAudioContent(const ContentInfos& contents); +const ContentInfo* GetFirstVideoContent(const ContentInfos& contents); +const ContentInfo* GetFirstDataContent(const ContentInfos& contents); +const ContentInfo* GetFirstAudioContent(const SessionDescription* sdesc); +const ContentInfo* GetFirstVideoContent(const SessionDescription* sdesc); +const ContentInfo* GetFirstDataContent(const SessionDescription* sdesc); +const AudioContentDescription* GetFirstAudioContentDescription( + const SessionDescription* sdesc); +const VideoContentDescription* GetFirstVideoContentDescription( + const SessionDescription* sdesc); +const DataContentDescription* GetFirstDataContentDescription( + const SessionDescription* sdesc); +bool GetStreamBySsrc( + const SessionDescription* sdesc, MediaType media_type, + uint32 ssrc, StreamParams* stream_out); +bool GetStreamByIds( + const SessionDescription* sdesc, MediaType media_type, + const std::string& groupid, const std::string& id, + StreamParams* stream_out); + +// Functions for translating media candidate names. + +// For converting between media ICE component and G-ICE channel +// names. For example: +// "rtp" <=> 1 +// "rtcp" <=> 2 +// "video_rtp" <=> 1 +// "video_rtcp" <=> 2 +// Will not convert in the general case of arbitrary channel names, +// but is useful for cases where we have candidates for media +// channels. +// returns false if there is no mapping. +bool GetMediaChannelNameFromComponent( + int component, cricket::MediaType media_type, std::string* channel_name); +bool GetMediaComponentFromChannelName( + const std::string& channel_name, int* component); +bool GetMediaTypeFromChannelName( + const std::string& channel_name, cricket::MediaType* media_type); + +void GetSupportedAudioCryptoSuites(std::vector* crypto_suites); +void GetSupportedVideoCryptoSuites(std::vector* crypto_suites); +void GetSupportedDataCryptoSuites(std::vector* crypto_suites); +void GetSupportedDefaultCryptoSuites(std::vector* crypto_suites); +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_MEDIASESSION_H_ diff --git a/talk/session/media/mediasession_unittest.cc b/talk/session/media/mediasession_unittest.cc new file mode 100644 index 000000000..5b0a859f5 --- /dev/null +++ b/talk/session/media/mediasession_unittest.cc @@ -0,0 +1,1905 @@ +/* + * libjingle + * Copyright 2004 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 +#include + +#include "talk/base/gunit.h" +#include "talk/base/fakesslidentity.h" +#include "talk/base/messagedigest.h" +#include "talk/media/base/codec.h" +#include "talk/media/base/testutils.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/transportdescription.h" +#include "talk/p2p/base/transportinfo.h" +#include "talk/session/media/mediasession.h" +#include "talk/session/media/srtpfilter.h" + +#ifdef HAVE_SRTP +#define ASSERT_CRYPTO(cd, s, cs) \ + ASSERT_FALSE(cd->crypto_required()); \ + ASSERT_EQ(s, cd->cryptos().size()); \ + ASSERT_EQ(std::string(cs), cd->cryptos()[0].cipher_suite) +#else +#define ASSERT_CRYPTO(cd, s, cs) \ + ASSERT_FALSE(cd->crypto_required()); \ + ASSERT_EQ(0U, cd->cryptos().size()); +#endif + +typedef std::vector Candidates; + +using cricket::MediaContentDescription; +using cricket::MediaSessionDescriptionFactory; +using cricket::MediaSessionOptions; +using cricket::MediaType; +using cricket::SessionDescription; +using cricket::SsrcGroup; +using cricket::StreamParams; +using cricket::StreamParamsVec; +using cricket::TransportDescription; +using cricket::TransportDescriptionFactory; +using cricket::TransportInfo; +using cricket::ContentInfo; +using cricket::CryptoParamsVec; +using cricket::AudioContentDescription; +using cricket::VideoContentDescription; +using cricket::DataContentDescription; +using cricket::GetFirstAudioContentDescription; +using cricket::GetFirstVideoContentDescription; +using cricket::GetFirstDataContentDescription; +using cricket::kAutoBandwidth; +using cricket::AudioCodec; +using cricket::VideoCodec; +using cricket::DataCodec; +using cricket::NS_JINGLE_RTP; +using cricket::MEDIA_TYPE_AUDIO; +using cricket::MEDIA_TYPE_VIDEO; +using cricket::MEDIA_TYPE_DATA; +using cricket::RtpHeaderExtension; +using cricket::SEC_DISABLED; +using cricket::SEC_ENABLED; +using cricket::SEC_REQUIRED; +using cricket::CS_AES_CM_128_HMAC_SHA1_32; +using cricket::CS_AES_CM_128_HMAC_SHA1_80; + +static const AudioCodec kAudioCodecs1[] = { + AudioCodec(103, "ISAC", 16000, -1, 1, 6), + AudioCodec(102, "iLBC", 8000, 13300, 1, 5), + AudioCodec(0, "PCMU", 8000, 64000, 1, 4), + AudioCodec(8, "PCMA", 8000, 64000, 1, 3), + AudioCodec(117, "red", 8000, 0, 1, 2), + AudioCodec(107, "CN", 48000, 0, 1, 1) +}; + +static const AudioCodec kAudioCodecs2[] = { + AudioCodec(126, "speex", 16000, 22000, 1, 3), + AudioCodec(127, "iLBC", 8000, 13300, 1, 2), + AudioCodec(0, "PCMU", 8000, 64000, 1, 1), +}; + +static const AudioCodec kAudioCodecsAnswer[] = { + AudioCodec(102, "iLBC", 8000, 13300, 1, 2), + AudioCodec(0, "PCMU", 8000, 64000, 1, 1), +}; + +static const VideoCodec kVideoCodecs1[] = { + VideoCodec(96, "H264-SVC", 320, 200, 30, 2), + VideoCodec(97, "H264", 320, 200, 30, 1) +}; + +static const VideoCodec kVideoCodecs2[] = { + VideoCodec(126, "H264", 320, 200, 30, 2), + VideoCodec(127, "H263", 320, 200, 30, 1) +}; + +static const VideoCodec kVideoCodecsAnswer[] = { + VideoCodec(97, "H264", 320, 200, 30, 2) +}; + +static const DataCodec kDataCodecs1[] = { + DataCodec(98, "binary-data", 2), + DataCodec(99, "utf8-text", 1) +}; + +static const DataCodec kDataCodecs2[] = { + DataCodec(126, "binary-data", 2), + DataCodec(127, "utf8-text", 1) +}; + +static const DataCodec kDataCodecsAnswer[] = { + DataCodec(98, "binary-data", 2), + DataCodec(99, "utf8-text", 1) +}; + +static const RtpHeaderExtension kAudioRtpExtension1[] = { + RtpHeaderExtension("urn:ietf:params:rtp-hdrext:ssrc-audio-level", 8), + RtpHeaderExtension("http://google.com/testing/audio_something", 10), +}; + +static const RtpHeaderExtension kAudioRtpExtension2[] = { + RtpHeaderExtension("urn:ietf:params:rtp-hdrext:ssrc-audio-level", 2), + RtpHeaderExtension("http://google.com/testing/audio_something_else", 8), +}; + +static const RtpHeaderExtension kAudioRtpExtensionAnswer[] = { + RtpHeaderExtension("urn:ietf:params:rtp-hdrext:ssrc-audio-level", 8), +}; + +static const RtpHeaderExtension kVideoRtpExtension1[] = { + RtpHeaderExtension("urn:ietf:params:rtp-hdrext:toffset", 14), + RtpHeaderExtension("http://google.com/testing/video_something", 15), +}; + +static const RtpHeaderExtension kVideoRtpExtension2[] = { + RtpHeaderExtension("urn:ietf:params:rtp-hdrext:toffset", 2), + RtpHeaderExtension("http://google.com/testing/video_something_else", 14), +}; + +static const RtpHeaderExtension kVideoRtpExtensionAnswer[] = { + RtpHeaderExtension("urn:ietf:params:rtp-hdrext:toffset", 14), +}; + +static const uint32 kFec1Ssrc[] = {10, 11}; +static const uint32 kFec2Ssrc[] = {20, 21}; +static const uint32 kFec3Ssrc[] = {30, 31}; + +static const char kMediaStream1[] = "stream_1"; +static const char kMediaStream2[] = "stream_2"; +static const char kVideoTrack1[] = "video_1"; +static const char kVideoTrack2[] = "video_2"; +static const char kAudioTrack1[] = "audio_1"; +static const char kAudioTrack2[] = "audio_2"; +static const char kAudioTrack3[] = "audio_3"; +static const char kDataTrack1[] = "data_1"; +static const char kDataTrack2[] = "data_2"; +static const char kDataTrack3[] = "data_3"; + +class MediaSessionDescriptionFactoryTest : public testing::Test { + public: + MediaSessionDescriptionFactoryTest() + : f1_(&tdf1_), f2_(&tdf2_), id1_("id1"), id2_("id2") { + f1_.set_audio_codecs(MAKE_VECTOR(kAudioCodecs1)); + f1_.set_video_codecs(MAKE_VECTOR(kVideoCodecs1)); + f1_.set_data_codecs(MAKE_VECTOR(kDataCodecs1)); + f2_.set_audio_codecs(MAKE_VECTOR(kAudioCodecs2)); + f2_.set_video_codecs(MAKE_VECTOR(kVideoCodecs2)); + f2_.set_data_codecs(MAKE_VECTOR(kDataCodecs2)); + tdf1_.set_identity(&id1_); + tdf2_.set_identity(&id2_); + } + + + bool CompareCryptoParams(const CryptoParamsVec& c1, + const CryptoParamsVec& c2) { + if (c1.size() != c2.size()) + return false; + for (size_t i = 0; i < c1.size(); ++i) + if (c1[i].tag != c2[i].tag || c1[i].cipher_suite != c2[i].cipher_suite || + c1[i].key_params != c2[i].key_params || + c1[i].session_params != c2[i].session_params) + return false; + return true; + } + + void TestTransportInfo(bool offer, const MediaSessionOptions& options, + bool has_current_desc) { + const std::string current_audio_ufrag = "current_audio_ufrag"; + const std::string current_audio_pwd = "current_audio_pwd"; + const std::string current_video_ufrag = "current_video_ufrag"; + const std::string current_video_pwd = "current_video_pwd"; + const std::string current_data_ufrag = "current_data_ufrag"; + const std::string current_data_pwd = "current_data_pwd"; + talk_base::scoped_ptr current_desc; + talk_base::scoped_ptr desc; + if (has_current_desc) { + current_desc.reset(new SessionDescription()); + EXPECT_TRUE(current_desc->AddTransportInfo( + TransportInfo("audio", + TransportDescription("", std::vector(), + current_audio_ufrag, + current_audio_pwd, + cricket::ICEMODE_FULL, + NULL, Candidates())))); + EXPECT_TRUE(current_desc->AddTransportInfo( + TransportInfo("video", + TransportDescription("", std::vector(), + current_video_ufrag, + current_video_pwd, + cricket::ICEMODE_FULL, + NULL, Candidates())))); + EXPECT_TRUE(current_desc->AddTransportInfo( + TransportInfo("data", + TransportDescription("", std::vector(), + current_data_ufrag, + current_data_pwd, + cricket::ICEMODE_FULL, + NULL, Candidates())))); + } + if (offer) { + desc.reset(f1_.CreateOffer(options, current_desc.get())); + } else { + talk_base::scoped_ptr offer; + offer.reset(f1_.CreateOffer(options, NULL)); + desc.reset(f1_.CreateAnswer(offer.get(), options, current_desc.get())); + } + ASSERT_TRUE(desc.get() != NULL); + const TransportInfo* ti_audio = desc->GetTransportInfoByName("audio"); + if (options.has_audio) { + EXPECT_TRUE(ti_audio != NULL); + if (has_current_desc) { + EXPECT_EQ(current_audio_ufrag, ti_audio->description.ice_ufrag); + EXPECT_EQ(current_audio_pwd, ti_audio->description.ice_pwd); + } else { + EXPECT_EQ(static_cast(cricket::ICE_UFRAG_LENGTH), + ti_audio->description.ice_ufrag.size()); + EXPECT_EQ(static_cast(cricket::ICE_PWD_LENGTH), + ti_audio->description.ice_pwd.size()); + } + + } else { + EXPECT_TRUE(ti_audio == NULL); + } + const TransportInfo* ti_video = desc->GetTransportInfoByName("video"); + if (options.has_video) { + EXPECT_TRUE(ti_video != NULL); + if (options.bundle_enabled) { + EXPECT_EQ(ti_audio->description.ice_ufrag, + ti_video->description.ice_ufrag); + EXPECT_EQ(ti_audio->description.ice_pwd, + ti_video->description.ice_pwd); + } else { + if (has_current_desc) { + EXPECT_EQ(current_video_ufrag, ti_video->description.ice_ufrag); + EXPECT_EQ(current_video_pwd, ti_video->description.ice_pwd); + } else { + EXPECT_EQ(static_cast(cricket::ICE_UFRAG_LENGTH), + ti_video->description.ice_ufrag.size()); + EXPECT_EQ(static_cast(cricket::ICE_PWD_LENGTH), + ti_video->description.ice_pwd.size()); + } + } + } else { + EXPECT_TRUE(ti_video == NULL); + } + const TransportInfo* ti_data = desc->GetTransportInfoByName("data"); + if (options.has_data()) { + EXPECT_TRUE(ti_data != NULL); + if (options.bundle_enabled) { + EXPECT_EQ(ti_audio->description.ice_ufrag, + ti_data->description.ice_ufrag); + EXPECT_EQ(ti_audio->description.ice_pwd, + ti_data->description.ice_pwd); + } else { + if (has_current_desc) { + EXPECT_EQ(current_data_ufrag, ti_data->description.ice_ufrag); + EXPECT_EQ(current_data_pwd, ti_data->description.ice_pwd); + } else { + EXPECT_EQ(static_cast(cricket::ICE_UFRAG_LENGTH), + ti_data->description.ice_ufrag.size()); + EXPECT_EQ(static_cast(cricket::ICE_PWD_LENGTH), + ti_data->description.ice_pwd.size()); + } + } + } else { + EXPECT_TRUE(ti_video == NULL); + } + } + + void TestCryptoWithBundle(bool offer) { + f1_.set_secure(SEC_ENABLED); + MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + options.data_channel_type = cricket::DCT_RTP; + talk_base::scoped_ptr ref_desc; + talk_base::scoped_ptr desc; + if (offer) { + options.bundle_enabled = false; + ref_desc.reset(f1_.CreateOffer(options, NULL)); + options.bundle_enabled = true; + desc.reset(f1_.CreateOffer(options, ref_desc.get())); + } else { + options.bundle_enabled = true; + ref_desc.reset(f1_.CreateOffer(options, NULL)); + desc.reset(f1_.CreateAnswer(ref_desc.get(), options, NULL)); + } + ASSERT_TRUE(desc.get() != NULL); + const cricket::MediaContentDescription* audio_media_desc = + static_cast ( + desc.get()->GetContentDescriptionByName("audio")); + ASSERT_TRUE(audio_media_desc != NULL); + const cricket::MediaContentDescription* video_media_desc = + static_cast ( + desc.get()->GetContentDescriptionByName("video")); + ASSERT_TRUE(video_media_desc != NULL); + EXPECT_TRUE(CompareCryptoParams(audio_media_desc->cryptos(), + video_media_desc->cryptos())); + EXPECT_EQ(1u, audio_media_desc->cryptos().size()); + EXPECT_EQ(std::string(CS_AES_CM_128_HMAC_SHA1_80), + audio_media_desc->cryptos()[0].cipher_suite); + + // Verify the selected crypto is one from the reference audio + // media content. + const cricket::MediaContentDescription* ref_audio_media_desc = + static_cast ( + ref_desc.get()->GetContentDescriptionByName("audio")); + bool found = false; + for (size_t i = 0; i < ref_audio_media_desc->cryptos().size(); ++i) { + if (ref_audio_media_desc->cryptos()[i].Matches( + audio_media_desc->cryptos()[0])) { + found = true; + break; + } + } + EXPECT_TRUE(found); + } + + // This test that the audio and video media direction is set to + // |expected_direction_in_answer| in an answer if the offer direction is set + // to |direction_in_offer|. + void TestMediaDirectionInAnswer( + cricket::MediaContentDirection direction_in_offer, + cricket::MediaContentDirection expected_direction_in_answer) { + MediaSessionOptions opts; + opts.has_video = true; + talk_base::scoped_ptr offer( + f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + ContentInfo* ac_offer= offer->GetContentByName("audio"); + ASSERT_TRUE(ac_offer != NULL); + AudioContentDescription* acd_offer = + static_cast(ac_offer->description); + acd_offer->set_direction(direction_in_offer); + ContentInfo* vc_offer= offer->GetContentByName("video"); + ASSERT_TRUE(vc_offer != NULL); + VideoContentDescription* vcd_offer = + static_cast(vc_offer->description); + vcd_offer->set_direction(direction_in_offer); + + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + const AudioContentDescription* acd_answer = + GetFirstAudioContentDescription(answer.get()); + EXPECT_EQ(expected_direction_in_answer, acd_answer->direction()); + const VideoContentDescription* vcd_answer = + GetFirstVideoContentDescription(answer.get()); + EXPECT_EQ(expected_direction_in_answer, vcd_answer->direction()); + } + + bool VerifyNoCNCodecs(const cricket::ContentInfo* content) { + const cricket::ContentDescription* description = content->description; + ASSERT(description != NULL); + const cricket::AudioContentDescription* audio_content_desc = + static_cast (description); + ASSERT(audio_content_desc != NULL); + for (size_t i = 0; i < audio_content_desc->codecs().size(); ++i) { + if (audio_content_desc->codecs()[i].name == "CN") + return false; + } + return true; + } + + protected: + MediaSessionDescriptionFactory f1_; + MediaSessionDescriptionFactory f2_; + TransportDescriptionFactory tdf1_; + TransportDescriptionFactory tdf2_; + talk_base::FakeSSLIdentity id1_; + talk_base::FakeSSLIdentity id2_; +}; + +// Create a typical audio offer, and ensure it matches what we expect. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateAudioOffer) { + f1_.set_secure(SEC_ENABLED); + talk_base::scoped_ptr offer( + f1_.CreateOffer(MediaSessionOptions(), NULL)); + ASSERT_TRUE(offer.get() != NULL); + const ContentInfo* ac = offer->GetContentByName("audio"); + const ContentInfo* vc = offer->GetContentByName("video"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc == NULL); + EXPECT_EQ(std::string(NS_JINGLE_RTP), ac->type); + const AudioContentDescription* acd = + static_cast(ac->description); + EXPECT_EQ(MEDIA_TYPE_AUDIO, acd->type()); + EXPECT_EQ(f1_.audio_codecs(), acd->codecs()); + EXPECT_NE(0U, acd->first_ssrc()); // a random nonzero ssrc + EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // default bandwidth (auto) + EXPECT_TRUE(acd->rtcp_mux()); // rtcp-mux defaults on + ASSERT_CRYPTO(acd, 2U, CS_AES_CM_128_HMAC_SHA1_32); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), acd->protocol()); +} + +// Create a typical video offer, and ensure it matches what we expect. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateVideoOffer) { + MediaSessionOptions opts; + opts.has_video = true; + f1_.set_secure(SEC_ENABLED); + talk_base::scoped_ptr + offer(f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + const ContentInfo* ac = offer->GetContentByName("audio"); + const ContentInfo* vc = offer->GetContentByName("video"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + EXPECT_EQ(std::string(NS_JINGLE_RTP), ac->type); + EXPECT_EQ(std::string(NS_JINGLE_RTP), vc->type); + const AudioContentDescription* acd = + static_cast(ac->description); + const VideoContentDescription* vcd = + static_cast(vc->description); + EXPECT_EQ(MEDIA_TYPE_AUDIO, acd->type()); + EXPECT_EQ(f1_.audio_codecs(), acd->codecs()); + EXPECT_NE(0U, acd->first_ssrc()); // a random nonzero ssrc + EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // default bandwidth (auto) + EXPECT_TRUE(acd->rtcp_mux()); // rtcp-mux defaults on + ASSERT_CRYPTO(acd, 2U, CS_AES_CM_128_HMAC_SHA1_32); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), acd->protocol()); + EXPECT_EQ(MEDIA_TYPE_VIDEO, vcd->type()); + EXPECT_EQ(f1_.video_codecs(), vcd->codecs()); + EXPECT_NE(0U, vcd->first_ssrc()); // a random nonzero ssrc + EXPECT_EQ(kAutoBandwidth, vcd->bandwidth()); // default bandwidth (auto) + EXPECT_TRUE(vcd->rtcp_mux()); // rtcp-mux defaults on + ASSERT_CRYPTO(vcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), vcd->protocol()); +} + +// Test creating an offer with bundle where the Codecs have the same dynamic +// RTP playlod type. The test verifies that the offer don't contain the +// duplicate RTP payload types. +TEST_F(MediaSessionDescriptionFactoryTest, TestBundleOfferWithSameCodecPlType) { + const VideoCodec& offered_video_codec = f2_.video_codecs()[0]; + const AudioCodec& offered_audio_codec = f2_.audio_codecs()[0]; + const DataCodec& offered_data_codec = f2_.data_codecs()[0]; + ASSERT_EQ(offered_video_codec.id, offered_audio_codec.id); + ASSERT_EQ(offered_video_codec.id, offered_data_codec.id); + + MediaSessionOptions opts; + opts.has_audio = true; + opts.has_video = true; + opts.data_channel_type = cricket::DCT_RTP; + opts.bundle_enabled = true; + talk_base::scoped_ptr + offer(f2_.CreateOffer(opts, NULL)); + const VideoContentDescription* vcd = + GetFirstVideoContentDescription(offer.get()); + const AudioContentDescription* acd = + GetFirstAudioContentDescription(offer.get()); + const DataContentDescription* dcd = + GetFirstDataContentDescription(offer.get()); + ASSERT_TRUE(NULL != vcd); + ASSERT_TRUE(NULL != acd); + ASSERT_TRUE(NULL != dcd); + EXPECT_NE(vcd->codecs()[0].id, acd->codecs()[0].id); + EXPECT_NE(vcd->codecs()[0].id, dcd->codecs()[0].id); + EXPECT_NE(acd->codecs()[0].id, dcd->codecs()[0].id); + EXPECT_EQ(vcd->codecs()[0].name, offered_video_codec.name); + EXPECT_EQ(acd->codecs()[0].name, offered_audio_codec.name); + EXPECT_EQ(dcd->codecs()[0].name, offered_data_codec.name); +} + +// Test creating an updated offer with with bundle, audio, video and data +// after an audio only session has been negotiated. +TEST_F(MediaSessionDescriptionFactoryTest, + TestCreateUpdatedVideoOfferWithBundle) { + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + MediaSessionOptions opts; + opts.has_audio = true; + opts.has_video = false; + opts.data_channel_type = cricket::DCT_NONE; + opts.bundle_enabled = true; + talk_base::scoped_ptr offer(f1_.CreateOffer(opts, NULL)); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + + MediaSessionOptions updated_opts; + updated_opts.has_audio = true; + updated_opts.has_video = true; + updated_opts.data_channel_type = cricket::DCT_RTP; + updated_opts.bundle_enabled = true; + talk_base::scoped_ptr updated_offer(f1_.CreateOffer( + updated_opts, answer.get())); + + const AudioContentDescription* acd = + GetFirstAudioContentDescription(updated_offer.get()); + const VideoContentDescription* vcd = + GetFirstVideoContentDescription(updated_offer.get()); + const DataContentDescription* dcd = + GetFirstDataContentDescription(updated_offer.get()); + EXPECT_TRUE(NULL != vcd); + EXPECT_TRUE(NULL != acd); + EXPECT_TRUE(NULL != dcd); + + ASSERT_CRYPTO(acd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), acd->protocol()); + ASSERT_CRYPTO(vcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), vcd->protocol()); + ASSERT_CRYPTO(dcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), dcd->protocol()); +} +// Create a typical data offer, and ensure it matches what we expect. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateDataOffer) { + MediaSessionOptions opts; + opts.data_channel_type = cricket::DCT_RTP; + f1_.set_secure(SEC_ENABLED); + talk_base::scoped_ptr + offer(f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + const ContentInfo* ac = offer->GetContentByName("audio"); + const ContentInfo* dc = offer->GetContentByName("data"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(dc != NULL); + EXPECT_EQ(std::string(NS_JINGLE_RTP), ac->type); + EXPECT_EQ(std::string(NS_JINGLE_RTP), dc->type); + const AudioContentDescription* acd = + static_cast(ac->description); + const DataContentDescription* dcd = + static_cast(dc->description); + EXPECT_EQ(MEDIA_TYPE_AUDIO, acd->type()); + EXPECT_EQ(f1_.audio_codecs(), acd->codecs()); + EXPECT_NE(0U, acd->first_ssrc()); // a random nonzero ssrc + EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // default bandwidth (auto) + EXPECT_TRUE(acd->rtcp_mux()); // rtcp-mux defaults on + ASSERT_CRYPTO(acd, 2U, CS_AES_CM_128_HMAC_SHA1_32); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), acd->protocol()); + EXPECT_EQ(MEDIA_TYPE_DATA, dcd->type()); + EXPECT_EQ(f1_.data_codecs(), dcd->codecs()); + EXPECT_NE(0U, dcd->first_ssrc()); // a random nonzero ssrc + EXPECT_EQ(cricket::kDataMaxBandwidth, + dcd->bandwidth()); // default bandwidth (auto) + EXPECT_TRUE(dcd->rtcp_mux()); // rtcp-mux defaults on + ASSERT_CRYPTO(dcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), dcd->protocol()); +} + +// Create an audio, video offer without legacy StreamParams. +TEST_F(MediaSessionDescriptionFactoryTest, + TestCreateOfferWithoutLegacyStreams) { + MediaSessionOptions opts; + opts.has_video = true; + f1_.set_add_legacy_streams(false); + talk_base::scoped_ptr + offer(f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + const ContentInfo* ac = offer->GetContentByName("audio"); + const ContentInfo* vc = offer->GetContentByName("video"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + const AudioContentDescription* acd = + static_cast(ac->description); + const VideoContentDescription* vcd = + static_cast(vc->description); + + EXPECT_FALSE(vcd->has_ssrcs()); // No StreamParams. + EXPECT_FALSE(acd->has_ssrcs()); // No StreamParams. +} + +// Create a typical audio answer, and ensure it matches what we expect. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateAudioAnswer) { + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + talk_base::scoped_ptr offer( + f1_.CreateOffer(MediaSessionOptions(), NULL)); + ASSERT_TRUE(offer.get() != NULL); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), MediaSessionOptions(), NULL)); + const ContentInfo* ac = answer->GetContentByName("audio"); + const ContentInfo* vc = answer->GetContentByName("video"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc == NULL); + EXPECT_EQ(std::string(NS_JINGLE_RTP), ac->type); + const AudioContentDescription* acd = + static_cast(ac->description); + EXPECT_EQ(MEDIA_TYPE_AUDIO, acd->type()); + EXPECT_EQ(MAKE_VECTOR(kAudioCodecsAnswer), acd->codecs()); + EXPECT_NE(0U, acd->first_ssrc()); // a random nonzero ssrc + EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // negotiated auto bw + EXPECT_TRUE(acd->rtcp_mux()); // negotiated rtcp-mux + ASSERT_CRYPTO(acd, 1U, CS_AES_CM_128_HMAC_SHA1_32); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), acd->protocol()); +} + +// Create a typical video answer, and ensure it matches what we expect. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateVideoAnswer) { + MediaSessionOptions opts; + opts.has_video = true; + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + talk_base::scoped_ptr offer(f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + const ContentInfo* ac = answer->GetContentByName("audio"); + const ContentInfo* vc = answer->GetContentByName("video"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + EXPECT_EQ(std::string(NS_JINGLE_RTP), ac->type); + EXPECT_EQ(std::string(NS_JINGLE_RTP), vc->type); + const AudioContentDescription* acd = + static_cast(ac->description); + const VideoContentDescription* vcd = + static_cast(vc->description); + EXPECT_EQ(MEDIA_TYPE_AUDIO, acd->type()); + EXPECT_EQ(MAKE_VECTOR(kAudioCodecsAnswer), acd->codecs()); + EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // negotiated auto bw + EXPECT_NE(0U, acd->first_ssrc()); // a random nonzero ssrc + EXPECT_TRUE(acd->rtcp_mux()); // negotiated rtcp-mux + ASSERT_CRYPTO(acd, 1U, CS_AES_CM_128_HMAC_SHA1_32); + EXPECT_EQ(MEDIA_TYPE_VIDEO, vcd->type()); + EXPECT_EQ(MAKE_VECTOR(kVideoCodecsAnswer), vcd->codecs()); + EXPECT_NE(0U, vcd->first_ssrc()); // a random nonzero ssrc + EXPECT_TRUE(vcd->rtcp_mux()); // negotiated rtcp-mux + ASSERT_CRYPTO(vcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), vcd->protocol()); +} + +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateDataAnswer) { + MediaSessionOptions opts; + opts.data_channel_type = cricket::DCT_RTP; + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + talk_base::scoped_ptr offer(f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + const ContentInfo* ac = answer->GetContentByName("audio"); + const ContentInfo* vc = answer->GetContentByName("data"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + EXPECT_EQ(std::string(NS_JINGLE_RTP), ac->type); + EXPECT_EQ(std::string(NS_JINGLE_RTP), vc->type); + const AudioContentDescription* acd = + static_cast(ac->description); + const DataContentDescription* vcd = + static_cast(vc->description); + EXPECT_EQ(MEDIA_TYPE_AUDIO, acd->type()); + EXPECT_EQ(MAKE_VECTOR(kAudioCodecsAnswer), acd->codecs()); + EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // negotiated auto bw + EXPECT_NE(0U, acd->first_ssrc()); // a random nonzero ssrc + EXPECT_TRUE(acd->rtcp_mux()); // negotiated rtcp-mux + ASSERT_CRYPTO(acd, 1U, CS_AES_CM_128_HMAC_SHA1_32); + EXPECT_EQ(MEDIA_TYPE_DATA, vcd->type()); + EXPECT_EQ(MAKE_VECTOR(kDataCodecsAnswer), vcd->codecs()); + EXPECT_NE(0U, vcd->first_ssrc()); // a random nonzero ssrc + EXPECT_TRUE(vcd->rtcp_mux()); // negotiated rtcp-mux + ASSERT_CRYPTO(vcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), vcd->protocol()); +} + +// This test that the media direction is set to send/receive in an answer if +// the offer is send receive. +TEST_F(MediaSessionDescriptionFactoryTest, CreateAnswerToSendReceiveOffer) { + TestMediaDirectionInAnswer(cricket::MD_SENDRECV, cricket::MD_SENDRECV); +} + +// This test that the media direction is set to receive only in an answer if +// the offer is send only. +TEST_F(MediaSessionDescriptionFactoryTest, CreateAnswerToSendOnlyOffer) { + TestMediaDirectionInAnswer(cricket::MD_SENDONLY, cricket::MD_RECVONLY); +} + +// This test that the media direction is set to send only in an answer if +// the offer is recv only. +TEST_F(MediaSessionDescriptionFactoryTest, CreateAnswerToRecvOnlyOffer) { + TestMediaDirectionInAnswer(cricket::MD_RECVONLY, cricket::MD_SENDONLY); +} + +// This test that the media direction is set to inactive in an answer if +// the offer is inactive. +TEST_F(MediaSessionDescriptionFactoryTest, CreateAnswerToInactiveOffer) { + TestMediaDirectionInAnswer(cricket::MD_INACTIVE, cricket::MD_INACTIVE); +} + +// Test that a data content with an unknown protocol is rejected in an answer. +TEST_F(MediaSessionDescriptionFactoryTest, + CreateDataAnswerToOfferWithUnknownProtocol) { + MediaSessionOptions opts; + opts.data_channel_type = cricket::DCT_RTP; + opts.has_audio = false; + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + talk_base::scoped_ptr offer(f1_.CreateOffer(opts, NULL)); + ContentInfo* dc_offer= offer->GetContentByName("data"); + ASSERT_TRUE(dc_offer != NULL); + DataContentDescription* dcd_offer = + static_cast(dc_offer->description); + ASSERT_TRUE(dcd_offer != NULL); + std::string protocol = "a weird unknown protocol"; + dcd_offer->set_protocol(protocol); + + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + + const ContentInfo* dc_answer = answer->GetContentByName("data"); + ASSERT_TRUE(dc_answer != NULL); + EXPECT_TRUE(dc_answer->rejected); + const DataContentDescription* dcd_answer = + static_cast(dc_answer->description); + ASSERT_TRUE(dcd_answer != NULL); + EXPECT_EQ(protocol, dcd_answer->protocol()); +} + +// Test that the media protocol is RTP/AVPF if DTLS and SDES are disabled. +TEST_F(MediaSessionDescriptionFactoryTest, AudioOfferAnswerWithCryptoDisabled) { + MediaSessionOptions opts; + f1_.set_secure(SEC_DISABLED); + f2_.set_secure(SEC_DISABLED); + tdf1_.set_secure(SEC_DISABLED); + tdf2_.set_secure(SEC_DISABLED); + + talk_base::scoped_ptr offer(f1_.CreateOffer(opts, NULL)); + const AudioContentDescription* offer_acd = + GetFirstAudioContentDescription(offer.get()); + ASSERT_TRUE(offer_acd != NULL); + EXPECT_EQ(std::string(cricket::kMediaProtocolAvpf), offer_acd->protocol()); + + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + + const ContentInfo* ac_answer = answer->GetContentByName("audio"); + ASSERT_TRUE(ac_answer != NULL); + EXPECT_FALSE(ac_answer->rejected); + + const AudioContentDescription* answer_acd = + GetFirstAudioContentDescription(answer.get()); + ASSERT_TRUE(answer_acd != NULL); + EXPECT_EQ(std::string(cricket::kMediaProtocolAvpf), answer_acd->protocol()); +} + +// Create a video offer and answer and ensure the RTP header extensions +// matches what we expect. +TEST_F(MediaSessionDescriptionFactoryTest, TestOfferAnswerWithRtpExtensions) { + MediaSessionOptions opts; + opts.has_video = true; + + f1_.set_audio_rtp_header_extensions(MAKE_VECTOR(kAudioRtpExtension1)); + f1_.set_video_rtp_header_extensions(MAKE_VECTOR(kVideoRtpExtension1)); + f2_.set_audio_rtp_header_extensions(MAKE_VECTOR(kAudioRtpExtension2)); + f2_.set_video_rtp_header_extensions(MAKE_VECTOR(kVideoRtpExtension2)); + + talk_base::scoped_ptr offer(f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + + EXPECT_EQ(MAKE_VECTOR(kAudioRtpExtension1), + GetFirstAudioContentDescription( + offer.get())->rtp_header_extensions()); + EXPECT_EQ(MAKE_VECTOR(kVideoRtpExtension1), + GetFirstVideoContentDescription( + offer.get())->rtp_header_extensions()); + EXPECT_EQ(MAKE_VECTOR(kAudioRtpExtensionAnswer), + GetFirstAudioContentDescription( + answer.get())->rtp_header_extensions()); + EXPECT_EQ(MAKE_VECTOR(kVideoRtpExtensionAnswer), + GetFirstVideoContentDescription( + answer.get())->rtp_header_extensions()); +} + +// Create an audio, video, data answer without legacy StreamParams. +TEST_F(MediaSessionDescriptionFactoryTest, + TestCreateAnswerWithoutLegacyStreams) { + MediaSessionOptions opts; + opts.has_video = true; + opts.data_channel_type = cricket::DCT_RTP; + f1_.set_add_legacy_streams(false); + f2_.set_add_legacy_streams(false); + talk_base::scoped_ptr offer(f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + const ContentInfo* ac = answer->GetContentByName("audio"); + const ContentInfo* vc = answer->GetContentByName("video"); + const ContentInfo* dc = answer->GetContentByName("data"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + const AudioContentDescription* acd = + static_cast(ac->description); + const VideoContentDescription* vcd = + static_cast(vc->description); + const DataContentDescription* dcd = + static_cast(dc->description); + + EXPECT_FALSE(acd->has_ssrcs()); // No StreamParams. + EXPECT_FALSE(vcd->has_ssrcs()); // No StreamParams. + EXPECT_FALSE(dcd->has_ssrcs()); // No StreamParams. +} + +TEST_F(MediaSessionDescriptionFactoryTest, TestPartial) { + MediaSessionOptions opts; + opts.has_video = true; + opts.data_channel_type = cricket::DCT_RTP; + f1_.set_secure(SEC_ENABLED); + talk_base::scoped_ptr + offer(f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + const ContentInfo* ac = offer->GetContentByName("audio"); + const ContentInfo* vc = offer->GetContentByName("video"); + const ContentInfo* dc = offer->GetContentByName("data"); + AudioContentDescription* acd = const_cast( + static_cast(ac->description)); + VideoContentDescription* vcd = const_cast( + static_cast(vc->description)); + DataContentDescription* dcd = const_cast( + static_cast(dc->description)); + + EXPECT_FALSE(acd->partial()); // default is false. + acd->set_partial(true); + EXPECT_TRUE(acd->partial()); + acd->set_partial(false); + EXPECT_FALSE(acd->partial()); + + EXPECT_FALSE(vcd->partial()); // default is false. + vcd->set_partial(true); + EXPECT_TRUE(vcd->partial()); + vcd->set_partial(false); + EXPECT_FALSE(vcd->partial()); + + EXPECT_FALSE(dcd->partial()); // default is false. + dcd->set_partial(true); + EXPECT_TRUE(dcd->partial()); + dcd->set_partial(false); + EXPECT_FALSE(dcd->partial()); +} + +// Create a typical video answer, and ensure it matches what we expect. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateVideoAnswerRtcpMux) { + MediaSessionOptions offer_opts; + MediaSessionOptions answer_opts; + answer_opts.has_video = true; + offer_opts.has_video = true; + answer_opts.data_channel_type = cricket::DCT_RTP; + offer_opts.data_channel_type = cricket::DCT_RTP; + + talk_base::scoped_ptr offer(NULL); + talk_base::scoped_ptr answer(NULL); + + offer_opts.rtcp_mux_enabled = true; + answer_opts.rtcp_mux_enabled = true; + + offer.reset(f1_.CreateOffer(offer_opts, NULL)); + answer.reset(f2_.CreateAnswer(offer.get(), answer_opts, NULL)); + ASSERT_TRUE(NULL != GetFirstAudioContentDescription(offer.get())); + ASSERT_TRUE(NULL != GetFirstVideoContentDescription(offer.get())); + ASSERT_TRUE(NULL != GetFirstDataContentDescription(offer.get())); + ASSERT_TRUE(NULL != GetFirstAudioContentDescription(answer.get())); + ASSERT_TRUE(NULL != GetFirstVideoContentDescription(answer.get())); + ASSERT_TRUE(NULL != GetFirstDataContentDescription(answer.get())); + EXPECT_TRUE(GetFirstAudioContentDescription(offer.get())->rtcp_mux()); + EXPECT_TRUE(GetFirstVideoContentDescription(offer.get())->rtcp_mux()); + EXPECT_TRUE(GetFirstDataContentDescription(offer.get())->rtcp_mux()); + EXPECT_TRUE(GetFirstAudioContentDescription(answer.get())->rtcp_mux()); + EXPECT_TRUE(GetFirstVideoContentDescription(answer.get())->rtcp_mux()); + EXPECT_TRUE(GetFirstDataContentDescription(answer.get())->rtcp_mux()); + + offer_opts.rtcp_mux_enabled = true; + answer_opts.rtcp_mux_enabled = false; + + offer.reset(f1_.CreateOffer(offer_opts, NULL)); + answer.reset(f2_.CreateAnswer(offer.get(), answer_opts, NULL)); + ASSERT_TRUE(NULL != GetFirstAudioContentDescription(offer.get())); + ASSERT_TRUE(NULL != GetFirstVideoContentDescription(offer.get())); + ASSERT_TRUE(NULL != GetFirstDataContentDescription(offer.get())); + ASSERT_TRUE(NULL != GetFirstAudioContentDescription(answer.get())); + ASSERT_TRUE(NULL != GetFirstVideoContentDescription(answer.get())); + ASSERT_TRUE(NULL != GetFirstDataContentDescription(answer.get())); + EXPECT_TRUE(GetFirstAudioContentDescription(offer.get())->rtcp_mux()); + EXPECT_TRUE(GetFirstVideoContentDescription(offer.get())->rtcp_mux()); + EXPECT_TRUE(GetFirstDataContentDescription(offer.get())->rtcp_mux()); + EXPECT_FALSE(GetFirstAudioContentDescription(answer.get())->rtcp_mux()); + EXPECT_FALSE(GetFirstVideoContentDescription(answer.get())->rtcp_mux()); + EXPECT_FALSE(GetFirstDataContentDescription(answer.get())->rtcp_mux()); + + offer_opts.rtcp_mux_enabled = false; + answer_opts.rtcp_mux_enabled = true; + + offer.reset(f1_.CreateOffer(offer_opts, NULL)); + answer.reset(f2_.CreateAnswer(offer.get(), answer_opts, NULL)); + ASSERT_TRUE(NULL != GetFirstAudioContentDescription(offer.get())); + ASSERT_TRUE(NULL != GetFirstVideoContentDescription(offer.get())); + ASSERT_TRUE(NULL != GetFirstDataContentDescription(offer.get())); + ASSERT_TRUE(NULL != GetFirstAudioContentDescription(answer.get())); + ASSERT_TRUE(NULL != GetFirstVideoContentDescription(answer.get())); + ASSERT_TRUE(NULL != GetFirstDataContentDescription(answer.get())); + EXPECT_FALSE(GetFirstAudioContentDescription(offer.get())->rtcp_mux()); + EXPECT_FALSE(GetFirstVideoContentDescription(offer.get())->rtcp_mux()); + EXPECT_FALSE(GetFirstDataContentDescription(offer.get())->rtcp_mux()); + EXPECT_FALSE(GetFirstAudioContentDescription(answer.get())->rtcp_mux()); + EXPECT_FALSE(GetFirstVideoContentDescription(answer.get())->rtcp_mux()); + EXPECT_FALSE(GetFirstDataContentDescription(answer.get())->rtcp_mux()); + + offer_opts.rtcp_mux_enabled = false; + answer_opts.rtcp_mux_enabled = false; + + offer.reset(f1_.CreateOffer(offer_opts, NULL)); + answer.reset(f2_.CreateAnswer(offer.get(), answer_opts, NULL)); + ASSERT_TRUE(NULL != GetFirstAudioContentDescription(offer.get())); + ASSERT_TRUE(NULL != GetFirstVideoContentDescription(offer.get())); + ASSERT_TRUE(NULL != GetFirstDataContentDescription(offer.get())); + ASSERT_TRUE(NULL != GetFirstAudioContentDescription(answer.get())); + ASSERT_TRUE(NULL != GetFirstVideoContentDescription(answer.get())); + ASSERT_TRUE(NULL != GetFirstDataContentDescription(answer.get())); + EXPECT_FALSE(GetFirstAudioContentDescription(offer.get())->rtcp_mux()); + EXPECT_FALSE(GetFirstVideoContentDescription(offer.get())->rtcp_mux()); + EXPECT_FALSE(GetFirstDataContentDescription(offer.get())->rtcp_mux()); + EXPECT_FALSE(GetFirstAudioContentDescription(answer.get())->rtcp_mux()); + EXPECT_FALSE(GetFirstVideoContentDescription(answer.get())->rtcp_mux()); + EXPECT_FALSE(GetFirstDataContentDescription(answer.get())->rtcp_mux()); +} + +// Create an audio-only answer to a video offer. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateAudioAnswerToVideo) { + MediaSessionOptions opts; + opts.has_video = true; + talk_base::scoped_ptr + offer(f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), MediaSessionOptions(), NULL)); + const ContentInfo* ac = answer->GetContentByName("audio"); + const ContentInfo* vc = answer->GetContentByName("video"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + ASSERT_TRUE(vc->description != NULL); + EXPECT_TRUE(vc->rejected); +} + +// Create an audio-only answer to an offer with data. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateNoDataAnswerToDataOffer) { + MediaSessionOptions opts; + opts.data_channel_type = cricket::DCT_RTP; + talk_base::scoped_ptr + offer(f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), MediaSessionOptions(), NULL)); + const ContentInfo* ac = answer->GetContentByName("audio"); + const ContentInfo* dc = answer->GetContentByName("data"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(dc != NULL); + ASSERT_TRUE(dc->description != NULL); + EXPECT_TRUE(dc->rejected); +} + +// Create an answer that rejects the contents which are rejected in the offer. +TEST_F(MediaSessionDescriptionFactoryTest, + CreateAnswerToOfferWithRejectedMedia) { + MediaSessionOptions opts; + opts.has_video = true; + opts.data_channel_type = cricket::DCT_RTP; + talk_base::scoped_ptr + offer(f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + ContentInfo* ac = offer->GetContentByName("audio"); + ContentInfo* vc = offer->GetContentByName("video"); + ContentInfo* dc = offer->GetContentByName("data"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + ASSERT_TRUE(dc != NULL); + ac->rejected = true; + vc->rejected = true; + dc->rejected = true; + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + ac = answer->GetContentByName("audio"); + vc = answer->GetContentByName("video"); + dc = answer->GetContentByName("data"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + ASSERT_TRUE(dc != NULL); + EXPECT_TRUE(ac->rejected); + EXPECT_TRUE(vc->rejected); + EXPECT_TRUE(dc->rejected); +} + +// Create an audio and video offer with: +// - one video track +// - two audio tracks +// - two data tracks +// and ensure it matches what we expect. Also updates the initial offer by +// adding a new video track and replaces one of the audio tracks. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateMultiStreamVideoOffer) { + MediaSessionOptions opts; + opts.AddStream(MEDIA_TYPE_VIDEO, kVideoTrack1, kMediaStream1); + opts.AddStream(MEDIA_TYPE_AUDIO, kAudioTrack1, kMediaStream1); + opts.AddStream(MEDIA_TYPE_AUDIO, kAudioTrack2, kMediaStream1); + opts.data_channel_type = cricket::DCT_RTP; + opts.AddStream(MEDIA_TYPE_DATA, kDataTrack1, kMediaStream1); + opts.AddStream(MEDIA_TYPE_DATA, kDataTrack2, kMediaStream1); + + f1_.set_secure(SEC_ENABLED); + talk_base::scoped_ptr offer(f1_.CreateOffer(opts, NULL)); + + ASSERT_TRUE(offer.get() != NULL); + const ContentInfo* ac = offer->GetContentByName("audio"); + const ContentInfo* vc = offer->GetContentByName("video"); + const ContentInfo* dc = offer->GetContentByName("data"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + ASSERT_TRUE(dc != NULL); + const AudioContentDescription* acd = + static_cast(ac->description); + const VideoContentDescription* vcd = + static_cast(vc->description); + const DataContentDescription* dcd = + static_cast(dc->description); + EXPECT_EQ(MEDIA_TYPE_AUDIO, acd->type()); + EXPECT_EQ(f1_.audio_codecs(), acd->codecs()); + + const StreamParamsVec& audio_streams = acd->streams(); + ASSERT_EQ(2U, audio_streams.size()); + EXPECT_EQ(audio_streams[0].cname , audio_streams[1].cname); + EXPECT_EQ(kAudioTrack1, audio_streams[0].id); + ASSERT_EQ(1U, audio_streams[0].ssrcs.size()); + EXPECT_NE(0U, audio_streams[0].ssrcs[0]); + EXPECT_EQ(kAudioTrack2, audio_streams[1].id); + ASSERT_EQ(1U, audio_streams[1].ssrcs.size()); + EXPECT_NE(0U, audio_streams[1].ssrcs[0]); + + EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // default bandwidth (auto) + EXPECT_TRUE(acd->rtcp_mux()); // rtcp-mux defaults on + ASSERT_CRYPTO(acd, 2U, CS_AES_CM_128_HMAC_SHA1_32); + + EXPECT_EQ(MEDIA_TYPE_VIDEO, vcd->type()); + EXPECT_EQ(f1_.video_codecs(), vcd->codecs()); + ASSERT_CRYPTO(vcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + + const StreamParamsVec& video_streams = vcd->streams(); + ASSERT_EQ(1U, video_streams.size()); + EXPECT_EQ(video_streams[0].cname, audio_streams[0].cname); + EXPECT_EQ(kVideoTrack1, video_streams[0].id); + EXPECT_EQ(kAutoBandwidth, vcd->bandwidth()); // default bandwidth (auto) + EXPECT_TRUE(vcd->rtcp_mux()); // rtcp-mux defaults on + + EXPECT_EQ(MEDIA_TYPE_DATA, dcd->type()); + EXPECT_EQ(f1_.data_codecs(), dcd->codecs()); + ASSERT_CRYPTO(dcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + + const StreamParamsVec& data_streams = dcd->streams(); + ASSERT_EQ(2U, data_streams.size()); + EXPECT_EQ(data_streams[0].cname , data_streams[1].cname); + EXPECT_EQ(kDataTrack1, data_streams[0].id); + ASSERT_EQ(1U, data_streams[0].ssrcs.size()); + EXPECT_NE(0U, data_streams[0].ssrcs[0]); + EXPECT_EQ(kDataTrack2, data_streams[1].id); + ASSERT_EQ(1U, data_streams[1].ssrcs.size()); + EXPECT_NE(0U, data_streams[1].ssrcs[0]); + + EXPECT_EQ(cricket::kDataMaxBandwidth, + dcd->bandwidth()); // default bandwidth (auto) + EXPECT_TRUE(dcd->rtcp_mux()); // rtcp-mux defaults on + ASSERT_CRYPTO(dcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + + + // Update the offer. Add a new video track that is not synched to the + // other tracks and replace audio track 2 with audio track 3. + opts.AddStream(MEDIA_TYPE_VIDEO, kVideoTrack2, kMediaStream2); + opts.RemoveStream(MEDIA_TYPE_AUDIO, kAudioTrack2); + opts.AddStream(MEDIA_TYPE_AUDIO, kAudioTrack3, kMediaStream1); + opts.RemoveStream(MEDIA_TYPE_DATA, kDataTrack2); + opts.AddStream(MEDIA_TYPE_DATA, kDataTrack3, kMediaStream1); + talk_base::scoped_ptr + updated_offer(f1_.CreateOffer(opts, offer.get())); + + ASSERT_TRUE(updated_offer.get() != NULL); + ac = updated_offer->GetContentByName("audio"); + vc = updated_offer->GetContentByName("video"); + dc = updated_offer->GetContentByName("data"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + ASSERT_TRUE(dc != NULL); + const AudioContentDescription* updated_acd = + static_cast(ac->description); + const VideoContentDescription* updated_vcd = + static_cast(vc->description); + const DataContentDescription* updated_dcd = + static_cast(dc->description); + + EXPECT_EQ(acd->type(), updated_acd->type()); + EXPECT_EQ(acd->codecs(), updated_acd->codecs()); + EXPECT_EQ(vcd->type(), updated_vcd->type()); + EXPECT_EQ(vcd->codecs(), updated_vcd->codecs()); + EXPECT_EQ(dcd->type(), updated_dcd->type()); + EXPECT_EQ(dcd->codecs(), updated_dcd->codecs()); + ASSERT_CRYPTO(updated_acd, 2U, CS_AES_CM_128_HMAC_SHA1_32); + EXPECT_TRUE(CompareCryptoParams(acd->cryptos(), updated_acd->cryptos())); + ASSERT_CRYPTO(updated_vcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + EXPECT_TRUE(CompareCryptoParams(vcd->cryptos(), updated_vcd->cryptos())); + ASSERT_CRYPTO(updated_dcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + EXPECT_TRUE(CompareCryptoParams(dcd->cryptos(), updated_dcd->cryptos())); + + const StreamParamsVec& updated_audio_streams = updated_acd->streams(); + ASSERT_EQ(2U, updated_audio_streams.size()); + EXPECT_EQ(audio_streams[0], updated_audio_streams[0]); + EXPECT_EQ(kAudioTrack3, updated_audio_streams[1].id); // New audio track. + ASSERT_EQ(1U, updated_audio_streams[1].ssrcs.size()); + EXPECT_NE(0U, updated_audio_streams[1].ssrcs[0]); + EXPECT_EQ(updated_audio_streams[0].cname, updated_audio_streams[1].cname); + + const StreamParamsVec& updated_video_streams = updated_vcd->streams(); + ASSERT_EQ(2U, updated_video_streams.size()); + EXPECT_EQ(video_streams[0], updated_video_streams[0]); + EXPECT_EQ(kVideoTrack2, updated_video_streams[1].id); + EXPECT_NE(updated_video_streams[1].cname, updated_video_streams[0].cname); + + const StreamParamsVec& updated_data_streams = updated_dcd->streams(); + ASSERT_EQ(2U, updated_data_streams.size()); + EXPECT_EQ(data_streams[0], updated_data_streams[0]); + EXPECT_EQ(kDataTrack3, updated_data_streams[1].id); // New data track. + ASSERT_EQ(1U, updated_data_streams[1].ssrcs.size()); + EXPECT_NE(0U, updated_data_streams[1].ssrcs[0]); + EXPECT_EQ(updated_data_streams[0].cname, updated_data_streams[1].cname); +} + +// Create an audio and video answer to a standard video offer with: +// - one video track +// - two audio tracks +// - two data tracks +// and ensure it matches what we expect. Also updates the initial answer by +// adding a new video track and removes one of the audio tracks. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateMultiStreamVideoAnswer) { + MediaSessionOptions offer_opts; + offer_opts.has_video = true; + offer_opts.data_channel_type = cricket::DCT_RTP; + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + talk_base::scoped_ptr offer(f1_.CreateOffer(offer_opts, + NULL)); + + MediaSessionOptions opts; + opts.AddStream(MEDIA_TYPE_VIDEO, kVideoTrack1, kMediaStream1); + opts.AddStream(MEDIA_TYPE_AUDIO, kAudioTrack1, kMediaStream1); + opts.AddStream(MEDIA_TYPE_AUDIO, kAudioTrack2, kMediaStream1); + opts.data_channel_type = cricket::DCT_RTP; + opts.AddStream(MEDIA_TYPE_DATA, kDataTrack1, kMediaStream1); + opts.AddStream(MEDIA_TYPE_DATA, kDataTrack2, kMediaStream1); + + talk_base::scoped_ptr + answer(f2_.CreateAnswer(offer.get(), opts, NULL)); + + ASSERT_TRUE(answer.get() != NULL); + const ContentInfo* ac = answer->GetContentByName("audio"); + const ContentInfo* vc = answer->GetContentByName("video"); + const ContentInfo* dc = answer->GetContentByName("data"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + ASSERT_TRUE(dc != NULL); + const AudioContentDescription* acd = + static_cast(ac->description); + const VideoContentDescription* vcd = + static_cast(vc->description); + const DataContentDescription* dcd = + static_cast(dc->description); + ASSERT_CRYPTO(acd, 1U, CS_AES_CM_128_HMAC_SHA1_32); + ASSERT_CRYPTO(vcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + ASSERT_CRYPTO(dcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + + EXPECT_EQ(MEDIA_TYPE_AUDIO, acd->type()); + EXPECT_EQ(MAKE_VECTOR(kAudioCodecsAnswer), acd->codecs()); + + const StreamParamsVec& audio_streams = acd->streams(); + ASSERT_EQ(2U, audio_streams.size()); + EXPECT_TRUE(audio_streams[0].cname == audio_streams[1].cname); + EXPECT_EQ(kAudioTrack1, audio_streams[0].id); + ASSERT_EQ(1U, audio_streams[0].ssrcs.size()); + EXPECT_NE(0U, audio_streams[0].ssrcs[0]); + EXPECT_EQ(kAudioTrack2, audio_streams[1].id); + ASSERT_EQ(1U, audio_streams[1].ssrcs.size()); + EXPECT_NE(0U, audio_streams[1].ssrcs[0]); + + EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // default bandwidth (auto) + EXPECT_TRUE(acd->rtcp_mux()); // rtcp-mux defaults on + + EXPECT_EQ(MEDIA_TYPE_VIDEO, vcd->type()); + EXPECT_EQ(MAKE_VECTOR(kVideoCodecsAnswer), vcd->codecs()); + + const StreamParamsVec& video_streams = vcd->streams(); + ASSERT_EQ(1U, video_streams.size()); + EXPECT_EQ(video_streams[0].cname, audio_streams[0].cname); + EXPECT_EQ(kVideoTrack1, video_streams[0].id); + EXPECT_EQ(kAutoBandwidth, vcd->bandwidth()); // default bandwidth (auto) + EXPECT_TRUE(vcd->rtcp_mux()); // rtcp-mux defaults on + + EXPECT_EQ(MEDIA_TYPE_DATA, dcd->type()); + EXPECT_EQ(MAKE_VECTOR(kDataCodecsAnswer), dcd->codecs()); + + const StreamParamsVec& data_streams = dcd->streams(); + ASSERT_EQ(2U, data_streams.size()); + EXPECT_TRUE(data_streams[0].cname == data_streams[1].cname); + EXPECT_EQ(kDataTrack1, data_streams[0].id); + ASSERT_EQ(1U, data_streams[0].ssrcs.size()); + EXPECT_NE(0U, data_streams[0].ssrcs[0]); + EXPECT_EQ(kDataTrack2, data_streams[1].id); + ASSERT_EQ(1U, data_streams[1].ssrcs.size()); + EXPECT_NE(0U, data_streams[1].ssrcs[0]); + + EXPECT_EQ(cricket::kDataMaxBandwidth, + dcd->bandwidth()); // default bandwidth (auto) + EXPECT_TRUE(dcd->rtcp_mux()); // rtcp-mux defaults on + + // Update the answer. Add a new video track that is not synched to the + // other traacks and remove 1 audio track. + opts.AddStream(MEDIA_TYPE_VIDEO, kVideoTrack2, kMediaStream2); + opts.RemoveStream(MEDIA_TYPE_AUDIO, kAudioTrack2); + opts.RemoveStream(MEDIA_TYPE_DATA, kDataTrack2); + talk_base::scoped_ptr + updated_answer(f2_.CreateAnswer(offer.get(), opts, answer.get())); + + ASSERT_TRUE(updated_answer.get() != NULL); + ac = updated_answer->GetContentByName("audio"); + vc = updated_answer->GetContentByName("video"); + dc = updated_answer->GetContentByName("data"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + ASSERT_TRUE(dc != NULL); + const AudioContentDescription* updated_acd = + static_cast(ac->description); + const VideoContentDescription* updated_vcd = + static_cast(vc->description); + const DataContentDescription* updated_dcd = + static_cast(dc->description); + + ASSERT_CRYPTO(updated_acd, 1U, CS_AES_CM_128_HMAC_SHA1_32); + EXPECT_TRUE(CompareCryptoParams(acd->cryptos(), updated_acd->cryptos())); + ASSERT_CRYPTO(updated_vcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + EXPECT_TRUE(CompareCryptoParams(vcd->cryptos(), updated_vcd->cryptos())); + ASSERT_CRYPTO(updated_dcd, 1U, CS_AES_CM_128_HMAC_SHA1_80); + EXPECT_TRUE(CompareCryptoParams(dcd->cryptos(), updated_dcd->cryptos())); + + EXPECT_EQ(acd->type(), updated_acd->type()); + EXPECT_EQ(acd->codecs(), updated_acd->codecs()); + EXPECT_EQ(vcd->type(), updated_vcd->type()); + EXPECT_EQ(vcd->codecs(), updated_vcd->codecs()); + EXPECT_EQ(dcd->type(), updated_dcd->type()); + EXPECT_EQ(dcd->codecs(), updated_dcd->codecs()); + + const StreamParamsVec& updated_audio_streams = updated_acd->streams(); + ASSERT_EQ(1U, updated_audio_streams.size()); + EXPECT_TRUE(audio_streams[0] == updated_audio_streams[0]); + + const StreamParamsVec& updated_video_streams = updated_vcd->streams(); + ASSERT_EQ(2U, updated_video_streams.size()); + EXPECT_EQ(video_streams[0], updated_video_streams[0]); + EXPECT_EQ(kVideoTrack2, updated_video_streams[1].id); + EXPECT_NE(updated_video_streams[1].cname, updated_video_streams[0].cname); + + const StreamParamsVec& updated_data_streams = updated_dcd->streams(); + ASSERT_EQ(1U, updated_data_streams.size()); + EXPECT_TRUE(data_streams[0] == updated_data_streams[0]); +} + + +// Create an updated offer after creating an answer to the original offer and +// verify that the codecs that were part of the original answer are not changed +// in the updated offer. +TEST_F(MediaSessionDescriptionFactoryTest, + RespondentCreatesOfferAfterCreatingAnswer) { + MediaSessionOptions opts; + opts.has_audio = true; + opts.has_video = true; + + talk_base::scoped_ptr offer(f1_.CreateOffer(opts, NULL)); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + + const AudioContentDescription* acd = + GetFirstAudioContentDescription(answer.get()); + EXPECT_EQ(MAKE_VECTOR(kAudioCodecsAnswer), acd->codecs()); + + const VideoContentDescription* vcd = + GetFirstVideoContentDescription(answer.get()); + EXPECT_EQ(MAKE_VECTOR(kVideoCodecsAnswer), vcd->codecs()); + + talk_base::scoped_ptr updated_offer( + f2_.CreateOffer(opts, answer.get())); + + // The expected audio codecs are the common audio codecs from the first + // offer/answer exchange plus the audio codecs only |f2_| offer, sorted in + // preference order. + const AudioCodec kUpdatedAudioCodecOffer[] = { + kAudioCodecs2[0], + kAudioCodecsAnswer[0], + kAudioCodecsAnswer[1], + }; + + // The expected video codecs are the common video codecs from the first + // offer/answer exchange plus the video codecs only |f2_| offer, sorted in + // preference order. + const VideoCodec kUpdatedVideoCodecOffer[] = { + kVideoCodecsAnswer[0], + kVideoCodecs2[1], + }; + + const AudioContentDescription* updated_acd = + GetFirstAudioContentDescription(updated_offer.get()); + EXPECT_EQ(MAKE_VECTOR(kUpdatedAudioCodecOffer), updated_acd->codecs()); + + const VideoContentDescription* updated_vcd = + GetFirstVideoContentDescription(updated_offer.get()); + EXPECT_EQ(MAKE_VECTOR(kUpdatedVideoCodecOffer), updated_vcd->codecs()); +} + +// Create an updated offer after creating an answer to the original offer and +// verify that the codecs that were part of the original answer are not changed +// in the updated offer. In this test Rtx is enabled. +TEST_F(MediaSessionDescriptionFactoryTest, + RespondentCreatesOfferAfterCreatingAnswerWithRtx) { + MediaSessionOptions opts; + opts.has_video = true; + opts.has_audio = false; + std::vector f1_codecs = MAKE_VECTOR(kVideoCodecs1); + VideoCodec rtx_f1; + rtx_f1.id = 126; + rtx_f1.name = cricket::kRtxCodecName; + + // This creates rtx for H264 with the payload type |f1_| uses. + rtx_f1.params[cricket::kCodecParamAssociatedPayloadType] = + talk_base::ToString(kVideoCodecs1[1].id); + f1_codecs.push_back(rtx_f1); + f1_.set_video_codecs(f1_codecs); + + std::vector f2_codecs = MAKE_VECTOR(kVideoCodecs2); + VideoCodec rtx_f2; + rtx_f2.id = 127; + rtx_f2.name = cricket::kRtxCodecName; + + // This creates rtx for H264 with the payload type |f2_| uses. + rtx_f2.params[cricket::kCodecParamAssociatedPayloadType] = + talk_base::ToString(kVideoCodecs2[0].id); + f2_codecs.push_back(rtx_f2); + f2_.set_video_codecs(f2_codecs); + + talk_base::scoped_ptr offer(f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + + const VideoContentDescription* vcd = + GetFirstVideoContentDescription(answer.get()); + + std::vector expected_codecs = MAKE_VECTOR(kVideoCodecsAnswer); + expected_codecs.push_back(rtx_f1); + + EXPECT_EQ(expected_codecs, vcd->codecs()); + + // Now, make sure we get same result, except for the preference order, + // if |f2_| creates an updated offer even though the default payload types + // are different from |f1_|. + expected_codecs[0].preference = f1_codecs[1].preference; + + talk_base::scoped_ptr updated_offer( + f2_.CreateOffer(opts, answer.get())); + ASSERT_TRUE(updated_offer); + talk_base::scoped_ptr updated_answer( + f1_.CreateAnswer(updated_offer.get(), opts, answer.get())); + + const VideoContentDescription* updated_vcd = + GetFirstVideoContentDescription(updated_answer.get()); + + EXPECT_EQ(expected_codecs, updated_vcd->codecs()); +} + +// Create an updated offer that adds video after creating an audio only answer +// to the original offer. This test verifies that if a video codec and the RTX +// codec have the same default payload type as an audio codec that is already in +// use, the added codecs payload types are changed. +TEST_F(MediaSessionDescriptionFactoryTest, + RespondentCreatesOfferWithVideoAndRtxAfterCreatingAudioAnswer) { + std::vector f1_codecs = MAKE_VECTOR(kVideoCodecs1); + VideoCodec rtx_f1; + rtx_f1.id = 126; + rtx_f1.name = cricket::kRtxCodecName; + + // This creates rtx for H264 with the payload type |f1_| uses. + rtx_f1.params[cricket::kCodecParamAssociatedPayloadType] = + talk_base::ToString(kVideoCodecs1[1].id); + f1_codecs.push_back(rtx_f1); + f1_.set_video_codecs(f1_codecs); + + MediaSessionOptions opts; + opts.has_audio = true; + opts.has_video = false; + + talk_base::scoped_ptr offer(f1_.CreateOffer(opts, NULL)); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + + const AudioContentDescription* acd = + GetFirstAudioContentDescription(answer.get()); + EXPECT_EQ(MAKE_VECTOR(kAudioCodecsAnswer), acd->codecs()); + + // Now - let |f2_| add video with RTX and let the payload type the RTX codec + // reference be the same as an audio codec that was negotiated in the + // first offer/answer exchange. + opts.has_audio = true; + opts.has_video = true; + + std::vector f2_codecs = MAKE_VECTOR(kVideoCodecs2); + int used_pl_type = acd->codecs()[0].id; + f2_codecs[0].id = used_pl_type; // Set the payload type for H264. + VideoCodec rtx_f2; + rtx_f2.id = 127; + rtx_f2.name = cricket::kRtxCodecName; + rtx_f2.params[cricket::kCodecParamAssociatedPayloadType] = + talk_base::ToString(used_pl_type); + f2_codecs.push_back(rtx_f2); + f2_.set_video_codecs(f2_codecs); + + talk_base::scoped_ptr updated_offer( + f2_.CreateOffer(opts, answer.get())); + ASSERT_TRUE(updated_offer); + talk_base::scoped_ptr updated_answer( + f1_.CreateAnswer(updated_offer.get(), opts, answer.get())); + + const AudioContentDescription* updated_acd = + GetFirstAudioContentDescription(answer.get()); + EXPECT_EQ(MAKE_VECTOR(kAudioCodecsAnswer), updated_acd->codecs()); + + const VideoContentDescription* updated_vcd = + GetFirstVideoContentDescription(updated_answer.get()); + + ASSERT_EQ("H264", updated_vcd->codecs()[0].name); + ASSERT_EQ(cricket::kRtxCodecName, updated_vcd->codecs()[1].name); + int new_h264_pl_type = updated_vcd->codecs()[0].id; + EXPECT_NE(used_pl_type, new_h264_pl_type); + VideoCodec rtx = updated_vcd->codecs()[1]; + int pt_referenced_by_rtx = talk_base::FromString( + rtx.params[cricket::kCodecParamAssociatedPayloadType]); + EXPECT_EQ(new_h264_pl_type, pt_referenced_by_rtx); +} + +// Test that RTX is ignored when there is no associated payload type parameter. +TEST_F(MediaSessionDescriptionFactoryTest, RtxWithoutApt) { + MediaSessionOptions opts; + opts.has_video = true; + opts.has_audio = false; + std::vector f1_codecs = MAKE_VECTOR(kVideoCodecs1); + VideoCodec rtx_f1; + rtx_f1.id = 126; + rtx_f1.name = cricket::kRtxCodecName; + + f1_codecs.push_back(rtx_f1); + f1_.set_video_codecs(f1_codecs); + + std::vector f2_codecs = MAKE_VECTOR(kVideoCodecs2); + VideoCodec rtx_f2; + rtx_f2.id = 127; + rtx_f2.name = cricket::kRtxCodecName; + + // This creates rtx for H264 with the payload type |f2_| uses. + rtx_f2.SetParam(cricket::kCodecParamAssociatedPayloadType, + talk_base::ToString(kVideoCodecs2[0].id)); + f2_codecs.push_back(rtx_f2); + f2_.set_video_codecs(f2_codecs); + + talk_base::scoped_ptr offer(f1_.CreateOffer(opts, NULL)); + ASSERT_TRUE(offer.get() != NULL); + // kCodecParamAssociatedPayloadType will always be added to the offer when RTX + // is selected. Manually remove kCodecParamAssociatedPayloadType so that it + // is possible to test that that RTX is dropped when + // kCodecParamAssociatedPayloadType is missing in the offer. + VideoContentDescription* desc = + static_cast( + offer->GetContentDescriptionByName(cricket::CN_VIDEO)); + ASSERT_TRUE(desc != NULL); + std::vector codecs = desc->codecs(); + for (std::vector::iterator iter = codecs.begin(); + iter != codecs.end(); ++iter) { + if (iter->name.find(cricket::kRtxCodecName) == 0) { + iter->params.clear(); + } + } + desc->set_codecs(codecs); + + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + + const VideoContentDescription* vcd = + GetFirstVideoContentDescription(answer.get()); + + for (std::vector::const_iterator iter = vcd->codecs().begin(); + iter != vcd->codecs().end(); ++iter) { + ASSERT_STRNE(iter->name.c_str(), cricket::kRtxCodecName); + } +} + +// Create an updated offer after creating an answer to the original offer and +// verify that the RTP header extensions that were part of the original answer +// are not changed in the updated offer. +TEST_F(MediaSessionDescriptionFactoryTest, + RespondentCreatesOfferAfterCreatingAnswerWithRtpExtensions) { + MediaSessionOptions opts; + opts.has_audio = true; + opts.has_video = true; + + f1_.set_audio_rtp_header_extensions(MAKE_VECTOR(kAudioRtpExtension1)); + f1_.set_video_rtp_header_extensions(MAKE_VECTOR(kVideoRtpExtension1)); + f2_.set_audio_rtp_header_extensions(MAKE_VECTOR(kAudioRtpExtension2)); + f2_.set_video_rtp_header_extensions(MAKE_VECTOR(kVideoRtpExtension2)); + + talk_base::scoped_ptr offer(f1_.CreateOffer(opts, NULL)); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), opts, NULL)); + + EXPECT_EQ(MAKE_VECTOR(kAudioRtpExtensionAnswer), + GetFirstAudioContentDescription( + answer.get())->rtp_header_extensions()); + EXPECT_EQ(MAKE_VECTOR(kVideoRtpExtensionAnswer), + GetFirstVideoContentDescription( + answer.get())->rtp_header_extensions()); + + talk_base::scoped_ptr updated_offer( + f2_.CreateOffer(opts, answer.get())); + + // The expected RTP header extensions in the new offer are the resulting + // extensions from the first offer/answer exchange plus the extensions only + // |f2_| offer. + // Since the default local extension id |f2_| uses has already been used by + // |f1_| for another extensions, it is changed to 255. + const RtpHeaderExtension kUpdatedAudioRtpExtensions[] = { + kAudioRtpExtensionAnswer[0], + RtpHeaderExtension(kAudioRtpExtension2[1].uri, 255), + }; + + // Since the default local extension id |f2_| uses has already been used by + // |f1_| for another extensions, is is changed to 254. + const RtpHeaderExtension kUpdatedVideoRtpExtensions[] = { + kVideoRtpExtensionAnswer[0], + RtpHeaderExtension(kVideoRtpExtension2[1].uri, 254), + }; + + const AudioContentDescription* updated_acd = + GetFirstAudioContentDescription(updated_offer.get()); + EXPECT_EQ(MAKE_VECTOR(kUpdatedAudioRtpExtensions), + updated_acd->rtp_header_extensions()); + + const VideoContentDescription* updated_vcd = + GetFirstVideoContentDescription(updated_offer.get()); + EXPECT_EQ(MAKE_VECTOR(kUpdatedVideoRtpExtensions), + updated_vcd->rtp_header_extensions()); +} + +TEST(MediaSessionDescription, CopySessionDescription) { + SessionDescription source; + cricket::ContentGroup group(cricket::CN_AUDIO); + source.AddGroup(group); + AudioContentDescription* acd(new AudioContentDescription()); + acd->set_codecs(MAKE_VECTOR(kAudioCodecs1)); + acd->AddLegacyStream(1); + source.AddContent(cricket::CN_AUDIO, cricket::NS_JINGLE_RTP, acd); + VideoContentDescription* vcd(new VideoContentDescription()); + vcd->set_codecs(MAKE_VECTOR(kVideoCodecs1)); + vcd->AddLegacyStream(2); + source.AddContent(cricket::CN_VIDEO, cricket::NS_JINGLE_RTP, vcd); + + talk_base::scoped_ptr copy(source.Copy()); + ASSERT_TRUE(copy.get() != NULL); + EXPECT_TRUE(copy->HasGroup(cricket::CN_AUDIO)); + const ContentInfo* ac = copy->GetContentByName("audio"); + const ContentInfo* vc = copy->GetContentByName("video"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + EXPECT_EQ(std::string(NS_JINGLE_RTP), ac->type); + const AudioContentDescription* acd_copy = + static_cast(ac->description); + EXPECT_EQ(acd->codecs(), acd_copy->codecs()); + EXPECT_EQ(1u, acd->first_ssrc()); + + EXPECT_EQ(std::string(NS_JINGLE_RTP), vc->type); + const VideoContentDescription* vcd_copy = + static_cast(vc->description); + EXPECT_EQ(vcd->codecs(), vcd_copy->codecs()); + EXPECT_EQ(2u, vcd->first_ssrc()); +} + +// The below TestTransportInfoXXX tests create different offers/answers, and +// ensure the TransportInfo in the SessionDescription matches what we expect. +TEST_F(MediaSessionDescriptionFactoryTest, TestTransportInfoOfferAudio) { + MediaSessionOptions options; + options.has_audio = true; + TestTransportInfo(true, options, false); +} + +TEST_F(MediaSessionDescriptionFactoryTest, TestTransportInfoOfferAudioCurrent) { + MediaSessionOptions options; + options.has_audio = true; + TestTransportInfo(true, options, true); +} + +TEST_F(MediaSessionDescriptionFactoryTest, TestTransportInfoOfferMultimedia) { + MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + options.data_channel_type = cricket::DCT_RTP; + TestTransportInfo(true, options, false); +} + +TEST_F(MediaSessionDescriptionFactoryTest, + TestTransportInfoOfferMultimediaCurrent) { + MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + options.data_channel_type = cricket::DCT_RTP; + TestTransportInfo(true, options, true); +} + +TEST_F(MediaSessionDescriptionFactoryTest, TestTransportInfoOfferBundle) { + MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + options.data_channel_type = cricket::DCT_RTP; + options.bundle_enabled = true; + TestTransportInfo(true, options, false); +} + +TEST_F(MediaSessionDescriptionFactoryTest, + TestTransportInfoOfferBundleCurrent) { + MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + options.data_channel_type = cricket::DCT_RTP; + options.bundle_enabled = true; + TestTransportInfo(true, options, true); +} + +TEST_F(MediaSessionDescriptionFactoryTest, TestTransportInfoAnswerAudio) { + MediaSessionOptions options; + options.has_audio = true; + TestTransportInfo(false, options, false); +} + +TEST_F(MediaSessionDescriptionFactoryTest, + TestTransportInfoAnswerAudioCurrent) { + MediaSessionOptions options; + options.has_audio = true; + TestTransportInfo(false, options, true); +} + +TEST_F(MediaSessionDescriptionFactoryTest, TestTransportInfoAnswerMultimedia) { + MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + options.data_channel_type = cricket::DCT_RTP; + TestTransportInfo(false, options, false); +} + +TEST_F(MediaSessionDescriptionFactoryTest, + TestTransportInfoAnswerMultimediaCurrent) { + MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + options.data_channel_type = cricket::DCT_RTP; + TestTransportInfo(false, options, true); +} + +TEST_F(MediaSessionDescriptionFactoryTest, TestTransportInfoAnswerBundle) { + MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + options.data_channel_type = cricket::DCT_RTP; + options.bundle_enabled = true; + TestTransportInfo(false, options, false); +} + +TEST_F(MediaSessionDescriptionFactoryTest, + TestTransportInfoAnswerBundleCurrent) { + MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + options.data_channel_type = cricket::DCT_RTP; + options.bundle_enabled = true; + TestTransportInfo(false, options, true); +} + +// Create an offer with bundle enabled and verify the crypto parameters are +// the common set of the available cryptos. +TEST_F(MediaSessionDescriptionFactoryTest, TestCryptoWithOfferBundle) { + TestCryptoWithBundle(true); +} + +// Create an answer with bundle enabled and verify the crypto parameters are +// the common set of the available cryptos. +TEST_F(MediaSessionDescriptionFactoryTest, TestCryptoWithAnswerBundle) { + TestCryptoWithBundle(false); +} + +// Test that we include both SDES and DTLS in the offer, but only include SDES +// in the answer if DTLS isn't negotiated. +TEST_F(MediaSessionDescriptionFactoryTest, TestCryptoDtls) { + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + tdf1_.set_secure(SEC_ENABLED); + tdf2_.set_secure(SEC_DISABLED); + MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + talk_base::scoped_ptr offer, answer; + const cricket::MediaContentDescription* audio_media_desc; + const cricket::MediaContentDescription* video_media_desc; + const cricket::TransportDescription* audio_trans_desc; + const cricket::TransportDescription* video_trans_desc; + + // Generate an offer with SDES and DTLS support. + offer.reset(f1_.CreateOffer(options, NULL)); + ASSERT_TRUE(offer.get() != NULL); + + audio_media_desc = static_cast( + offer->GetContentDescriptionByName("audio")); + ASSERT_TRUE(audio_media_desc != NULL); + video_media_desc = static_cast ( + offer->GetContentDescriptionByName("video")); + ASSERT_TRUE(video_media_desc != NULL); + EXPECT_EQ(2u, audio_media_desc->cryptos().size()); + EXPECT_EQ(1u, video_media_desc->cryptos().size()); + + audio_trans_desc = offer->GetTransportDescriptionByName("audio"); + ASSERT_TRUE(audio_trans_desc != NULL); + video_trans_desc = offer->GetTransportDescriptionByName("video"); + ASSERT_TRUE(video_trans_desc != NULL); + ASSERT_TRUE(audio_trans_desc->identity_fingerprint.get() != NULL); + ASSERT_TRUE(video_trans_desc->identity_fingerprint.get() != NULL); + + // Generate an answer with only SDES support, since tdf2 has crypto disabled. + answer.reset(f2_.CreateAnswer(offer.get(), options, NULL)); + ASSERT_TRUE(answer.get() != NULL); + + audio_media_desc = static_cast ( + answer->GetContentDescriptionByName("audio")); + ASSERT_TRUE(audio_media_desc != NULL); + video_media_desc = static_cast ( + answer->GetContentDescriptionByName("video")); + ASSERT_TRUE(video_media_desc != NULL); + EXPECT_EQ(1u, audio_media_desc->cryptos().size()); + EXPECT_EQ(1u, video_media_desc->cryptos().size()); + + audio_trans_desc = answer->GetTransportDescriptionByName("audio"); + ASSERT_TRUE(audio_trans_desc != NULL); + video_trans_desc = answer->GetTransportDescriptionByName("video"); + ASSERT_TRUE(video_trans_desc != NULL); + ASSERT_TRUE(audio_trans_desc->identity_fingerprint.get() == NULL); + ASSERT_TRUE(video_trans_desc->identity_fingerprint.get() == NULL); + + // Enable DTLS; the answer should now only have DTLS support. + tdf2_.set_secure(SEC_ENABLED); + answer.reset(f2_.CreateAnswer(offer.get(), options, NULL)); + ASSERT_TRUE(answer.get() != NULL); + + audio_media_desc = static_cast ( + answer->GetContentDescriptionByName("audio")); + ASSERT_TRUE(audio_media_desc != NULL); + video_media_desc = static_cast ( + answer->GetContentDescriptionByName("video")); + ASSERT_TRUE(video_media_desc != NULL); + EXPECT_TRUE(audio_media_desc->cryptos().empty()); + EXPECT_TRUE(video_media_desc->cryptos().empty()); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), + audio_media_desc->protocol()); + EXPECT_EQ(std::string(cricket::kMediaProtocolSavpf), + video_media_desc->protocol()); + + audio_trans_desc = answer->GetTransportDescriptionByName("audio"); + ASSERT_TRUE(audio_trans_desc != NULL); + video_trans_desc = answer->GetTransportDescriptionByName("video"); + ASSERT_TRUE(video_trans_desc != NULL); + ASSERT_TRUE(audio_trans_desc->identity_fingerprint.get() != NULL); + ASSERT_TRUE(video_trans_desc->identity_fingerprint.get() != NULL); +} + +// Test that an answer can't be created if cryptos are required but the offer is +// unsecure. +TEST_F(MediaSessionDescriptionFactoryTest, TestSecureAnswerToUnsecureOffer) { + MediaSessionOptions options; + f1_.set_secure(SEC_DISABLED); + tdf1_.set_secure(SEC_DISABLED); + f2_.set_secure(SEC_REQUIRED); + tdf1_.set_secure(SEC_ENABLED); + + talk_base::scoped_ptr offer(f1_.CreateOffer(options, + NULL)); + ASSERT_TRUE(offer.get() != NULL); + talk_base::scoped_ptr answer( + f2_.CreateAnswer(offer.get(), options, NULL)); + EXPECT_TRUE(answer.get() == NULL); +} + +// Test that we accept a DTLS offer without SDES and create an appropriate +// answer. +TEST_F(MediaSessionDescriptionFactoryTest, TestCryptoOfferDtlsButNotSdes) { + f1_.set_secure(SEC_DISABLED); + f2_.set_secure(SEC_ENABLED); + tdf1_.set_secure(SEC_ENABLED); + tdf2_.set_secure(SEC_ENABLED); + MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + options.data_channel_type = cricket::DCT_RTP; + + talk_base::scoped_ptr offer, answer; + + // Generate an offer with DTLS but without SDES. + offer.reset(f1_.CreateOffer(options, NULL)); + ASSERT_TRUE(offer.get() != NULL); + + const AudioContentDescription* audio_offer = + GetFirstAudioContentDescription(offer.get()); + ASSERT_TRUE(audio_offer->cryptos().empty()); + const VideoContentDescription* video_offer = + GetFirstVideoContentDescription(offer.get()); + ASSERT_TRUE(video_offer->cryptos().empty()); + const DataContentDescription* data_offer = + GetFirstDataContentDescription(offer.get()); + ASSERT_TRUE(data_offer->cryptos().empty()); + + const cricket::TransportDescription* audio_offer_trans_desc = + offer->GetTransportDescriptionByName("audio"); + ASSERT_TRUE(audio_offer_trans_desc->identity_fingerprint.get() != NULL); + const cricket::TransportDescription* video_offer_trans_desc = + offer->GetTransportDescriptionByName("video"); + ASSERT_TRUE(video_offer_trans_desc->identity_fingerprint.get() != NULL); + const cricket::TransportDescription* data_offer_trans_desc = + offer->GetTransportDescriptionByName("data"); + ASSERT_TRUE(data_offer_trans_desc->identity_fingerprint.get() != NULL); + + // Generate an answer with DTLS. + answer.reset(f2_.CreateAnswer(offer.get(), options, NULL)); + ASSERT_TRUE(answer.get() != NULL); + + const cricket::TransportDescription* audio_answer_trans_desc = + answer->GetTransportDescriptionByName("audio"); + EXPECT_TRUE(audio_answer_trans_desc->identity_fingerprint.get() != NULL); + const cricket::TransportDescription* video_answer_trans_desc = + answer->GetTransportDescriptionByName("video"); + EXPECT_TRUE(video_answer_trans_desc->identity_fingerprint.get() != NULL); + const cricket::TransportDescription* data_answer_trans_desc = + answer->GetTransportDescriptionByName("data"); + EXPECT_TRUE(data_answer_trans_desc->identity_fingerprint.get() != NULL); +} + +// Verifies if vad_enabled option is set to false, CN codecs are not present in +// offer or answer. +TEST_F(MediaSessionDescriptionFactoryTest, TestVADEnableOption) { + MediaSessionOptions options; + options.has_audio = true; + options.has_video = true; + talk_base::scoped_ptr offer( + f1_.CreateOffer(options, NULL)); + ASSERT_TRUE(offer.get() != NULL); + const ContentInfo* audio_content = offer->GetContentByName("audio"); + EXPECT_FALSE(VerifyNoCNCodecs(audio_content)); + + options.vad_enabled = false; + offer.reset(f1_.CreateOffer(options, NULL)); + ASSERT_TRUE(offer.get() != NULL); + audio_content = offer->GetContentByName("audio"); + EXPECT_TRUE(VerifyNoCNCodecs(audio_content)); + talk_base::scoped_ptr answer( + f1_.CreateAnswer(offer.get(), options, NULL)); + ASSERT_TRUE(answer.get() != NULL); + audio_content = answer->GetContentByName("audio"); + EXPECT_TRUE(VerifyNoCNCodecs(audio_content)); +} diff --git a/talk/session/media/mediasessionclient.cc b/talk/session/media/mediasessionclient.cc new file mode 100644 index 000000000..18407ca73 --- /dev/null +++ b/talk/session/media/mediasessionclient.cc @@ -0,0 +1,1085 @@ +/* + * libjingle + * Copyright 2004 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 + +#include "talk/session/media/mediasessionclient.h" + +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/stringencode.h" +#include "talk/base/stringutils.h" +#include "talk/media/base/cryptoparams.h" +#include "talk/media/base/capturemanager.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/parsing.h" +#include "talk/session/media/mediamessages.h" +#include "talk/session/media/srtpfilter.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlconstants.h" +#include "talk/xmpp/constants.h" + +namespace cricket { + +#if !defined(DISABLE_MEDIA_ENGINE_FACTORY) +MediaSessionClient::MediaSessionClient( + const buzz::Jid& jid, SessionManager *manager) + : jid_(jid), + session_manager_(manager), + focus_call_(NULL), + channel_manager_(new ChannelManager(session_manager_->worker_thread())), + desc_factory_(channel_manager_, + session_manager_->transport_desc_factory()) { + Construct(); +} +#endif + +MediaSessionClient::MediaSessionClient( + const buzz::Jid& jid, SessionManager *manager, + MediaEngineInterface* media_engine, + DataEngineInterface* data_media_engine, + DeviceManagerInterface* device_manager) + : jid_(jid), + session_manager_(manager), + focus_call_(NULL), + channel_manager_(new ChannelManager( + media_engine, data_media_engine, + device_manager, new CaptureManager(), + session_manager_->worker_thread())), + desc_factory_(channel_manager_, + session_manager_->transport_desc_factory()) { + Construct(); +} + +void MediaSessionClient::Construct() { + // Register ourselves as the handler of audio and video sessions. + session_manager_->AddClient(NS_JINGLE_RTP, this); + // Forward device notifications. + SignalDevicesChange.repeat(channel_manager_->SignalDevicesChange); + // Bring up the channel manager. + // In previous versions of ChannelManager, this was done automatically + // in the constructor. + channel_manager_->Init(); +} + +MediaSessionClient::~MediaSessionClient() { + // Destroy all calls + std::map::iterator it; + while (calls_.begin() != calls_.end()) { + std::map::iterator it = calls_.begin(); + DestroyCall((*it).second); + } + + // Delete channel manager. This will wait for the channels to exit + delete channel_manager_; + + // Remove ourselves from the client map. + session_manager_->RemoveClient(NS_JINGLE_RTP); +} + +Call *MediaSessionClient::CreateCall() { + Call *call = new Call(this); + calls_[call->id()] = call; + SignalCallCreate(call); + return call; +} + +void MediaSessionClient::OnSessionCreate(Session *session, + bool received_initiate) { + if (received_initiate) { + session->SignalState.connect(this, &MediaSessionClient::OnSessionState); + } +} + +void MediaSessionClient::OnSessionState(BaseSession* base_session, + BaseSession::State state) { + // MediaSessionClient can only be used with a Session*, so it's + // safe to cast here. + Session* session = static_cast(base_session); + + if (state == Session::STATE_RECEIVEDINITIATE) { + // The creation of the call must happen after the session has + // processed the initiate message because we need the + // remote_description to know what content names to use in the + // call. + + // If our accept would have no codecs, then we must reject this call. + const SessionDescription* offer = session->remote_description(); + const SessionDescription* accept = CreateAnswer(offer, CallOptions()); + const ContentInfo* audio_content = GetFirstAudioContent(accept); + bool audio_rejected = (!audio_content) ? true : audio_content->rejected; + const AudioContentDescription* audio_desc = (!audio_content) ? NULL : + static_cast(audio_content->description); + + // For some reason, we need a call even if we reject. So, either find a + // matching call or create a new one. + // The matching of existing calls is used to support the multi-session mode + // required for p2p handoffs: ie. once a MUC call is established, a new + // session may be established for the same call but is direct between the + // clients. To indicate that this is the case, the initiator of the incoming + // session is set to be the same as the remote name of the MUC for the + // existing session, thus the client can know that this is a new session for + // the existing call, rather than a whole new call. + Call* call = NULL; + if (multisession_enabled_) { + call = FindCallByRemoteName(session->initiator_name()); + } + + if (call == NULL) { + // Could not find a matching call, so create a new one. + call = CreateCall(); + } + + session_map_[session->id()] = call; + call->IncomingSession(session, offer); + + if (audio_rejected || !audio_desc || audio_desc->codecs().size() == 0) { + session->Reject(STR_TERMINATE_INCOMPATIBLE_PARAMETERS); + } + delete accept; + } +} + +void MediaSessionClient::DestroyCall(Call *call) { + // Change focus away, signal destruction + + if (call == focus_call_) + SetFocus(NULL); + SignalCallDestroy(call); + + // Remove it from calls_ map and delete + + std::map::iterator it = calls_.find(call->id()); + if (it != calls_.end()) + calls_.erase(it); + + delete call; +} + +void MediaSessionClient::OnSessionDestroy(Session *session) { + // Find the call this session is in, remove it + SessionMap::iterator it = session_map_.find(session->id()); + ASSERT(it != session_map_.end()); + if (it != session_map_.end()) { + Call *call = (*it).second; + session_map_.erase(it); + call->RemoveSession(session); + } +} + +Call *MediaSessionClient::GetFocus() { + return focus_call_; +} + +void MediaSessionClient::SetFocus(Call *call) { + Call *old_focus_call = focus_call_; + if (focus_call_ != call) { + if (focus_call_ != NULL) + focus_call_->EnableChannels(false); + focus_call_ = call; + if (focus_call_ != NULL) + focus_call_->EnableChannels(true); + SignalFocus(focus_call_, old_focus_call); + } +} + +void MediaSessionClient::JoinCalls(Call *call_to_join, Call *call) { + // Move all sessions from call to call_to_join, delete call. + // If call_to_join has focus, added sessions should have enabled channels. + + if (focus_call_ == call) + SetFocus(NULL); + call_to_join->Join(call, focus_call_ == call_to_join); + DestroyCall(call); +} + +Session *MediaSessionClient::CreateSession(Call *call) { + const std::string& type = NS_JINGLE_RTP; + Session *session = session_manager_->CreateSession(jid().Str(), type); + session_map_[session->id()] = call; + return session; +} + +Call *MediaSessionClient::FindCallByRemoteName(const std::string &remote_name) { + SessionMap::const_iterator call; + for (call = session_map_.begin(); call != session_map_.end(); ++call) { + std::vector sessions = call->second->sessions(); + std::vector::const_iterator session; + for (session = sessions.begin(); session != sessions.end(); ++session) { + if (remote_name == (*session)->remote_name()) { + return call->second; + } + } + } + + return NULL; +} + +// TODO(pthatcher): Move all of the parsing and writing functions into +// mediamessages.cc, with unit tests. +bool ParseGingleAudioCodec(const buzz::XmlElement* element, AudioCodec* out) { + int id = GetXmlAttr(element, QN_ID, -1); + if (id < 0) + return false; + + std::string name = GetXmlAttr(element, QN_NAME, buzz::STR_EMPTY); + int clockrate = GetXmlAttr(element, QN_CLOCKRATE, 0); + int bitrate = GetXmlAttr(element, QN_BITRATE, 0); + int channels = GetXmlAttr(element, QN_CHANNELS, 1); + *out = AudioCodec(id, name, clockrate, bitrate, channels, 0); + return true; +} + +bool ParseGingleVideoCodec(const buzz::XmlElement* element, VideoCodec* out) { + int id = GetXmlAttr(element, QN_ID, -1); + if (id < 0) + return false; + + std::string name = GetXmlAttr(element, QN_NAME, buzz::STR_EMPTY); + int width = GetXmlAttr(element, QN_WIDTH, 0); + int height = GetXmlAttr(element, QN_HEIGHT, 0); + int framerate = GetXmlAttr(element, QN_FRAMERATE, 0); + + *out = VideoCodec(id, name, width, height, framerate, 0); + return true; +} + +// Parses an ssrc string as a legacy stream. If it fails, returns +// false and fills an error message. +bool ParseSsrcAsLegacyStream(const std::string& ssrc_str, + std::vector* streams, + ParseError* error) { + if (!ssrc_str.empty()) { + uint32 ssrc; + if (!talk_base::FromString(ssrc_str, &ssrc)) { + return BadParse("Missing or invalid ssrc.", error); + } + + streams->push_back(StreamParams::CreateLegacy(ssrc)); + } + return true; +} + +void ParseGingleSsrc(const buzz::XmlElement* parent_elem, + const buzz::QName& name, + MediaContentDescription* media) { + const buzz::XmlElement* ssrc_elem = parent_elem->FirstNamed(name); + if (ssrc_elem) { + ParseError error; + ParseSsrcAsLegacyStream( + ssrc_elem->BodyText(), &(media->mutable_streams()), &error); + } +} + +bool ParseCryptoParams(const buzz::XmlElement* element, + CryptoParams* out, + ParseError* error) { + if (!element->HasAttr(QN_CRYPTO_SUITE)) { + return BadParse("crypto: crypto-suite attribute missing ", error); + } else if (!element->HasAttr(QN_CRYPTO_KEY_PARAMS)) { + return BadParse("crypto: key-params attribute missing ", error); + } else if (!element->HasAttr(QN_CRYPTO_TAG)) { + return BadParse("crypto: tag attribute missing ", error); + } + + const std::string& crypto_suite = element->Attr(QN_CRYPTO_SUITE); + const std::string& key_params = element->Attr(QN_CRYPTO_KEY_PARAMS); + const int tag = GetXmlAttr(element, QN_CRYPTO_TAG, 0); + const std::string& session_params = + element->Attr(QN_CRYPTO_SESSION_PARAMS); // Optional. + + *out = CryptoParams(tag, crypto_suite, key_params, session_params); + return true; +} + + +// Parse the first encryption element found with a matching 'usage' +// element. +// is specific to Gingle. In Jingle, is already +// scoped to a content. +// Return false if there was an encryption element and it could not be +// parsed. +bool ParseGingleEncryption(const buzz::XmlElement* desc, + const buzz::QName& usage, + MediaContentDescription* media, + ParseError* error) { + for (const buzz::XmlElement* encryption = desc->FirstNamed(QN_ENCRYPTION); + encryption != NULL; + encryption = encryption->NextNamed(QN_ENCRYPTION)) { + if (encryption->FirstNamed(usage) != NULL) { + media->set_crypto_required( + GetXmlAttr(encryption, QN_ENCRYPTION_REQUIRED, false)); + for (const buzz::XmlElement* crypto = encryption->FirstNamed(QN_CRYPTO); + crypto != NULL; + crypto = crypto->NextNamed(QN_CRYPTO)) { + CryptoParams params; + if (!ParseCryptoParams(crypto, ¶ms, error)) { + return false; + } + media->AddCrypto(params); + } + break; + } + } + return true; +} + +void ParseBandwidth(const buzz::XmlElement* parent_elem, + MediaContentDescription* media) { + const buzz::XmlElement* bw_elem = GetXmlChild(parent_elem, LN_BANDWIDTH); + int bandwidth_kbps = -1; + if (bw_elem && talk_base::FromString(bw_elem->BodyText(), &bandwidth_kbps)) { + if (bandwidth_kbps >= 0) { + media->set_bandwidth(bandwidth_kbps * 1000); + } + } +} + +bool ParseGingleAudioContent(const buzz::XmlElement* content_elem, + ContentDescription** content, + ParseError* error) { + AudioContentDescription* audio = new AudioContentDescription(); + + if (content_elem->FirstElement()) { + for (const buzz::XmlElement* codec_elem = + content_elem->FirstNamed(QN_GINGLE_AUDIO_PAYLOADTYPE); + codec_elem != NULL; + codec_elem = codec_elem->NextNamed(QN_GINGLE_AUDIO_PAYLOADTYPE)) { + AudioCodec codec; + if (ParseGingleAudioCodec(codec_elem, &codec)) { + audio->AddCodec(codec); + } + } + } else { + // For backward compatibility, we can assume the other client is + // an old version of Talk if it has no audio payload types at all. + audio->AddCodec(AudioCodec(103, "ISAC", 16000, -1, 1, 1)); + audio->AddCodec(AudioCodec(0, "PCMU", 8000, 64000, 1, 0)); + } + + ParseGingleSsrc(content_elem, QN_GINGLE_AUDIO_SRCID, audio); + + if (!ParseGingleEncryption(content_elem, QN_GINGLE_AUDIO_CRYPTO_USAGE, + audio, error)) { + return false; + } + + *content = audio; + return true; +} + +bool ParseGingleVideoContent(const buzz::XmlElement* content_elem, + ContentDescription** content, + ParseError* error) { + VideoContentDescription* video = new VideoContentDescription(); + + for (const buzz::XmlElement* codec_elem = + content_elem->FirstNamed(QN_GINGLE_VIDEO_PAYLOADTYPE); + codec_elem != NULL; + codec_elem = codec_elem->NextNamed(QN_GINGLE_VIDEO_PAYLOADTYPE)) { + VideoCodec codec; + if (ParseGingleVideoCodec(codec_elem, &codec)) { + video->AddCodec(codec); + } + } + + ParseGingleSsrc(content_elem, QN_GINGLE_VIDEO_SRCID, video); + ParseBandwidth(content_elem, video); + + if (!ParseGingleEncryption(content_elem, QN_GINGLE_VIDEO_CRYPTO_USAGE, + video, error)) { + return false; + } + + *content = video; + return true; +} + +void ParsePayloadTypeParameters(const buzz::XmlElement* element, + std::map* paramap) { + for (const buzz::XmlElement* param = element->FirstNamed(QN_PARAMETER); + param != NULL; param = param->NextNamed(QN_PARAMETER)) { + std::string name = GetXmlAttr(param, QN_PAYLOADTYPE_PARAMETER_NAME, + buzz::STR_EMPTY); + std::string value = GetXmlAttr(param, QN_PAYLOADTYPE_PARAMETER_VALUE, + buzz::STR_EMPTY); + if (!name.empty() && !value.empty()) { + paramap->insert(make_pair(name, value)); + } + } +} + +int FindWithDefault(const std::map& map, + const std::string& key, const int def) { + std::map::const_iterator iter = map.find(key); + return (iter == map.end()) ? def : atoi(iter->second.c_str()); +} + + +// Parse the first encryption element found. +// Return false if there was an encryption element and it could not be +// parsed. +bool ParseJingleEncryption(const buzz::XmlElement* content_elem, + MediaContentDescription* media, + ParseError* error) { + const buzz::XmlElement* encryption = + content_elem->FirstNamed(QN_ENCRYPTION); + if (encryption == NULL) { + return true; + } + + media->set_crypto_required( + GetXmlAttr(encryption, QN_ENCRYPTION_REQUIRED, false)); + + for (const buzz::XmlElement* crypto = encryption->FirstNamed(QN_CRYPTO); + crypto != NULL; + crypto = crypto->NextNamed(QN_CRYPTO)) { + CryptoParams params; + if (!ParseCryptoParams(crypto, ¶ms, error)) { + return false; + } + media->AddCrypto(params); + } + return true; +} + +bool ParseJingleAudioCodec(const buzz::XmlElement* elem, AudioCodec* codec) { + int id = GetXmlAttr(elem, QN_ID, -1); + if (id < 0) + return false; + + std::string name = GetXmlAttr(elem, QN_NAME, buzz::STR_EMPTY); + int clockrate = GetXmlAttr(elem, QN_CLOCKRATE, 0); + int channels = GetXmlAttr(elem, QN_CHANNELS, 1); + + std::map paramap; + ParsePayloadTypeParameters(elem, ¶map); + int bitrate = FindWithDefault(paramap, PAYLOADTYPE_PARAMETER_BITRATE, 0); + + *codec = AudioCodec(id, name, clockrate, bitrate, channels, 0); + return true; +} + +bool ParseJingleVideoCodec(const buzz::XmlElement* elem, VideoCodec* codec) { + int id = GetXmlAttr(elem, QN_ID, -1); + if (id < 0) + return false; + + std::string name = GetXmlAttr(elem, QN_NAME, buzz::STR_EMPTY); + + std::map paramap; + ParsePayloadTypeParameters(elem, ¶map); + int width = FindWithDefault(paramap, PAYLOADTYPE_PARAMETER_WIDTH, 0); + int height = FindWithDefault(paramap, PAYLOADTYPE_PARAMETER_HEIGHT, 0); + int framerate = FindWithDefault(paramap, PAYLOADTYPE_PARAMETER_FRAMERATE, 0); + + *codec = VideoCodec(id, name, width, height, framerate, 0); + codec->params = paramap; + return true; +} + +bool ParseJingleDataCodec(const buzz::XmlElement* elem, DataCodec* codec) { + int id = GetXmlAttr(elem, QN_ID, -1); + if (id < 0) + return false; + + std::string name = GetXmlAttr(elem, QN_NAME, buzz::STR_EMPTY); + + *codec = DataCodec(id, name, 0); + return true; +} + +bool ParseJingleStreamsOrLegacySsrc(const buzz::XmlElement* desc_elem, + MediaContentDescription* media, + ParseError* error) { + if (HasJingleStreams(desc_elem)) { + if (!ParseJingleStreams(desc_elem, &(media->mutable_streams()), error)) { + return false; + } + } else { + const std::string ssrc_str = desc_elem->Attr(QN_SSRC); + if (!ParseSsrcAsLegacyStream( + ssrc_str, &(media->mutable_streams()), error)) { + return false; + } + } + return true; +} + +bool ParseJingleAudioContent(const buzz::XmlElement* content_elem, + ContentDescription** content, + ParseError* error) { + AudioContentDescription* audio = new AudioContentDescription(); + + for (const buzz::XmlElement* payload_elem = + content_elem->FirstNamed(QN_JINGLE_RTP_PAYLOADTYPE); + payload_elem != NULL; + payload_elem = payload_elem->NextNamed(QN_JINGLE_RTP_PAYLOADTYPE)) { + AudioCodec codec; + if (ParseJingleAudioCodec(payload_elem, &codec)) { + audio->AddCodec(codec); + } + } + + if (!ParseJingleStreamsOrLegacySsrc(content_elem, audio, error)) { + return false; + } + + if (!ParseJingleEncryption(content_elem, audio, error)) { + return false; + } + + audio->set_rtcp_mux(content_elem->FirstNamed(QN_JINGLE_RTCP_MUX) != NULL); + + RtpHeaderExtensions hdrexts; + if (!ParseJingleRtpHeaderExtensions(content_elem, &hdrexts, error)) { + return false; + } + audio->set_rtp_header_extensions(hdrexts); + + *content = audio; + return true; +} + +bool ParseJingleVideoContent(const buzz::XmlElement* content_elem, + ContentDescription** content, + ParseError* error) { + VideoContentDescription* video = new VideoContentDescription(); + + for (const buzz::XmlElement* payload_elem = + content_elem->FirstNamed(QN_JINGLE_RTP_PAYLOADTYPE); + payload_elem != NULL; + payload_elem = payload_elem->NextNamed(QN_JINGLE_RTP_PAYLOADTYPE)) { + VideoCodec codec; + if (ParseJingleVideoCodec(payload_elem, &codec)) { + video->AddCodec(codec); + } + } + + if (!ParseJingleStreamsOrLegacySsrc(content_elem, video, error)) { + return false; + } + ParseBandwidth(content_elem, video); + + if (!ParseJingleEncryption(content_elem, video, error)) { + return false; + } + + video->set_rtcp_mux(content_elem->FirstNamed(QN_JINGLE_RTCP_MUX) != NULL); + + RtpHeaderExtensions hdrexts; + if (!ParseJingleRtpHeaderExtensions(content_elem, &hdrexts, error)) { + return false; + } + video->set_rtp_header_extensions(hdrexts); + + *content = video; + return true; +} + +bool ParseJingleSctpDataContent(const buzz::XmlElement* content_elem, + ContentDescription** content, + ParseError* error) { + DataContentDescription* data = new DataContentDescription(); + data->set_protocol(kMediaProtocolSctp); + + for (const buzz::XmlElement* stream_elem = + content_elem->FirstNamed(QN_JINGLE_DRAFT_SCTP_STREAM); + stream_elem != NULL; + stream_elem = stream_elem->NextNamed(QN_JINGLE_DRAFT_SCTP_STREAM)) { + StreamParams stream; + stream.groupid = stream_elem->Attr(QN_NICK); + stream.id = stream_elem->Attr(QN_NAME); + uint32 sid; + if (!talk_base::FromString(stream_elem->Attr(QN_SID), &sid)) { + return BadParse("Missing or invalid sid.", error); + } + if (sid > kMaxSctpSid) { + return BadParse("SID is greater than max value.", error); + } + + stream.ssrcs.push_back(sid); + data->mutable_streams().push_back(stream); + } + + *content = data; + return true; +} + +bool ParseJingleRtpDataContent(const buzz::XmlElement* content_elem, + ContentDescription** content, + ParseError* error) { + DataContentDescription* data = new DataContentDescription(); + + for (const buzz::XmlElement* payload_elem = + content_elem->FirstNamed(QN_JINGLE_RTP_PAYLOADTYPE); + payload_elem != NULL; + payload_elem = payload_elem->NextNamed(QN_JINGLE_RTP_PAYLOADTYPE)) { + DataCodec codec; + if (ParseJingleDataCodec(payload_elem, &codec)) { + data->AddCodec(codec); + } + } + + if (!ParseJingleStreamsOrLegacySsrc(content_elem, data, error)) { + return false; + } + ParseBandwidth(content_elem, data); + + if (!ParseJingleEncryption(content_elem, data, error)) { + return false; + } + + data->set_rtcp_mux(content_elem->FirstNamed(QN_JINGLE_RTCP_MUX) != NULL); + + *content = data; + return true; +} + +bool MediaSessionClient::ParseContent(SignalingProtocol protocol, + const buzz::XmlElement* content_elem, + ContentDescription** content, + ParseError* error) { + if (protocol == PROTOCOL_GINGLE) { + const std::string& content_type = content_elem->Name().Namespace(); + if (NS_GINGLE_AUDIO == content_type) { + return ParseGingleAudioContent(content_elem, content, error); + } else if (NS_GINGLE_VIDEO == content_type) { + return ParseGingleVideoContent(content_elem, content, error); + } else { + return BadParse("Unknown content type: " + content_type, error); + } + } else { + const std::string& content_type = content_elem->Name().Namespace(); + // We use the XMLNS of the element to determine if + // it's RTP or SCTP. + if (content_type == NS_JINGLE_DRAFT_SCTP) { + return ParseJingleSctpDataContent(content_elem, content, error); + } + + std::string media; + if (!RequireXmlAttr(content_elem, QN_JINGLE_CONTENT_MEDIA, &media, error)) + return false; + + if (media == JINGLE_CONTENT_MEDIA_AUDIO) { + return ParseJingleAudioContent(content_elem, content, error); + } else if (media == JINGLE_CONTENT_MEDIA_VIDEO) { + return ParseJingleVideoContent(content_elem, content, error); + } else if (media == JINGLE_CONTENT_MEDIA_DATA) { + return ParseJingleRtpDataContent(content_elem, content, error); + } else { + return BadParse("Unknown media: " + media, error); + } + } +} + +buzz::XmlElement* CreateGingleAudioCodecElem(const AudioCodec& codec) { + buzz::XmlElement* payload_type = + new buzz::XmlElement(QN_GINGLE_AUDIO_PAYLOADTYPE, true); + AddXmlAttr(payload_type, QN_ID, codec.id); + payload_type->AddAttr(QN_NAME, codec.name); + if (codec.clockrate > 0) + AddXmlAttr(payload_type, QN_CLOCKRATE, codec.clockrate); + if (codec.bitrate > 0) + AddXmlAttr(payload_type, QN_BITRATE, codec.bitrate); + if (codec.channels > 1) + AddXmlAttr(payload_type, QN_CHANNELS, codec.channels); + return payload_type; +} + +buzz::XmlElement* CreateGingleVideoCodecElem(const VideoCodec& codec) { + buzz::XmlElement* payload_type = + new buzz::XmlElement(QN_GINGLE_VIDEO_PAYLOADTYPE, true); + AddXmlAttr(payload_type, QN_ID, codec.id); + payload_type->AddAttr(QN_NAME, codec.name); + AddXmlAttr(payload_type, QN_WIDTH, codec.width); + AddXmlAttr(payload_type, QN_HEIGHT, codec.height); + AddXmlAttr(payload_type, QN_FRAMERATE, codec.framerate); + return payload_type; +} + +buzz::XmlElement* CreateGingleSsrcElem(const buzz::QName& name, uint32 ssrc) { + buzz::XmlElement* elem = new buzz::XmlElement(name, true); + if (ssrc) { + SetXmlBody(elem, ssrc); + } + return elem; +} + +buzz::XmlElement* CreateBandwidthElem(const buzz::QName& name, int bps) { + int kbps = bps / 1000; + buzz::XmlElement* elem = new buzz::XmlElement(name); + elem->AddAttr(buzz::QN_TYPE, "AS"); + SetXmlBody(elem, kbps); + return elem; +} + +// For Jingle, usage_qname is empty. +buzz::XmlElement* CreateJingleEncryptionElem(const CryptoParamsVec& cryptos, + bool required) { + buzz::XmlElement* encryption_elem = new buzz::XmlElement(QN_ENCRYPTION); + + if (required) { + encryption_elem->SetAttr(QN_ENCRYPTION_REQUIRED, "true"); + } + + for (CryptoParamsVec::const_iterator i = cryptos.begin(); + i != cryptos.end(); + ++i) { + buzz::XmlElement* crypto_elem = new buzz::XmlElement(QN_CRYPTO); + + AddXmlAttr(crypto_elem, QN_CRYPTO_TAG, i->tag); + crypto_elem->AddAttr(QN_CRYPTO_SUITE, i->cipher_suite); + crypto_elem->AddAttr(QN_CRYPTO_KEY_PARAMS, i->key_params); + if (!i->session_params.empty()) { + crypto_elem->AddAttr(QN_CRYPTO_SESSION_PARAMS, i->session_params); + } + encryption_elem->AddElement(crypto_elem); + } + return encryption_elem; +} + +buzz::XmlElement* CreateGingleEncryptionElem(const CryptoParamsVec& cryptos, + const buzz::QName& usage_qname, + bool required) { + buzz::XmlElement* encryption_elem = + CreateJingleEncryptionElem(cryptos, required); + + if (required) { + encryption_elem->SetAttr(QN_ENCRYPTION_REQUIRED, "true"); + } + + buzz::XmlElement* usage_elem = new buzz::XmlElement(usage_qname); + encryption_elem->AddElement(usage_elem); + + return encryption_elem; +} + +buzz::XmlElement* CreateGingleAudioContentElem( + const AudioContentDescription* audio, + bool crypto_required) { + buzz::XmlElement* elem = + new buzz::XmlElement(QN_GINGLE_AUDIO_CONTENT, true); + + for (AudioCodecs::const_iterator codec = audio->codecs().begin(); + codec != audio->codecs().end(); ++codec) { + elem->AddElement(CreateGingleAudioCodecElem(*codec)); + } + if (audio->has_ssrcs()) { + elem->AddElement(CreateGingleSsrcElem( + QN_GINGLE_AUDIO_SRCID, audio->first_ssrc())); + } + + const CryptoParamsVec& cryptos = audio->cryptos(); + if (!cryptos.empty()) { + elem->AddElement(CreateGingleEncryptionElem(cryptos, + QN_GINGLE_AUDIO_CRYPTO_USAGE, + crypto_required)); + } + return elem; +} + +buzz::XmlElement* CreateGingleVideoContentElem( + const VideoContentDescription* video, + bool crypto_required) { + buzz::XmlElement* elem = + new buzz::XmlElement(QN_GINGLE_VIDEO_CONTENT, true); + + for (VideoCodecs::const_iterator codec = video->codecs().begin(); + codec != video->codecs().end(); ++codec) { + elem->AddElement(CreateGingleVideoCodecElem(*codec)); + } + if (video->has_ssrcs()) { + elem->AddElement(CreateGingleSsrcElem( + QN_GINGLE_VIDEO_SRCID, video->first_ssrc())); + } + if (video->bandwidth() != kAutoBandwidth) { + elem->AddElement(CreateBandwidthElem(QN_GINGLE_VIDEO_BANDWIDTH, + video->bandwidth())); + } + + const CryptoParamsVec& cryptos = video->cryptos(); + if (!cryptos.empty()) { + elem->AddElement(CreateGingleEncryptionElem(cryptos, + QN_GINGLE_VIDEO_CRYPTO_USAGE, + crypto_required)); + } + + return elem; +} + +template +buzz::XmlElement* CreatePayloadTypeParameterElem( + const std::string& name, T value) { + buzz::XmlElement* elem = new buzz::XmlElement(QN_PARAMETER); + + elem->AddAttr(QN_PAYLOADTYPE_PARAMETER_NAME, name); + AddXmlAttr(elem, QN_PAYLOADTYPE_PARAMETER_VALUE, value); + + return elem; +} + +buzz::XmlElement* CreateJingleAudioCodecElem(const AudioCodec& codec) { + buzz::XmlElement* elem = new buzz::XmlElement(QN_JINGLE_RTP_PAYLOADTYPE); + + AddXmlAttr(elem, QN_ID, codec.id); + elem->AddAttr(QN_NAME, codec.name); + if (codec.clockrate > 0) { + AddXmlAttr(elem, QN_CLOCKRATE, codec.clockrate); + } + if (codec.bitrate > 0) { + elem->AddElement(CreatePayloadTypeParameterElem( + PAYLOADTYPE_PARAMETER_BITRATE, codec.bitrate)); + } + if (codec.channels > 1) { + AddXmlAttr(elem, QN_CHANNELS, codec.channels); + } + + return elem; +} + +buzz::XmlElement* CreateJingleVideoCodecElem(const VideoCodec& codec) { + buzz::XmlElement* elem = new buzz::XmlElement(QN_JINGLE_RTP_PAYLOADTYPE); + + AddXmlAttr(elem, QN_ID, codec.id); + elem->AddAttr(QN_NAME, codec.name); + elem->AddElement(CreatePayloadTypeParameterElem( + PAYLOADTYPE_PARAMETER_WIDTH, codec.width)); + elem->AddElement(CreatePayloadTypeParameterElem( + PAYLOADTYPE_PARAMETER_HEIGHT, codec.height)); + elem->AddElement(CreatePayloadTypeParameterElem( + PAYLOADTYPE_PARAMETER_FRAMERATE, codec.framerate)); + CodecParameterMap::const_iterator param_iter; + for (param_iter = codec.params.begin(); param_iter != codec.params.end(); + ++param_iter) { + elem->AddElement(CreatePayloadTypeParameterElem(param_iter->first, + param_iter->second)); + } + + return elem; +} + +buzz::XmlElement* CreateJingleDataCodecElem(const DataCodec& codec) { + buzz::XmlElement* elem = new buzz::XmlElement(QN_JINGLE_RTP_PAYLOADTYPE); + + AddXmlAttr(elem, QN_ID, codec.id); + elem->AddAttr(QN_NAME, codec.name); + + return elem; +} + +void WriteLegacyJingleSsrc(const MediaContentDescription* media, + buzz::XmlElement* elem) { + if (media->has_ssrcs()) { + AddXmlAttr(elem, QN_SSRC, media->first_ssrc()); + } +} + +void WriteJingleStreamsOrLegacySsrc(const MediaContentDescription* media, + buzz::XmlElement* desc_elem) { + if (!media->multistream()) { + WriteLegacyJingleSsrc(media, desc_elem); + } else { + WriteJingleStreams(media->streams(), desc_elem); + } +} + +buzz::XmlElement* CreateJingleAudioContentElem( + const AudioContentDescription* audio, bool crypto_required) { + buzz::XmlElement* elem = + new buzz::XmlElement(QN_JINGLE_RTP_CONTENT, true); + + elem->SetAttr(QN_JINGLE_CONTENT_MEDIA, JINGLE_CONTENT_MEDIA_AUDIO); + WriteJingleStreamsOrLegacySsrc(audio, elem); + + for (AudioCodecs::const_iterator codec = audio->codecs().begin(); + codec != audio->codecs().end(); ++codec) { + elem->AddElement(CreateJingleAudioCodecElem(*codec)); + } + + const CryptoParamsVec& cryptos = audio->cryptos(); + if (!cryptos.empty()) { + elem->AddElement(CreateJingleEncryptionElem(cryptos, crypto_required)); + } + + if (audio->rtcp_mux()) { + elem->AddElement(new buzz::XmlElement(QN_JINGLE_RTCP_MUX)); + } + + WriteJingleRtpHeaderExtensions(audio->rtp_header_extensions(), elem); + + return elem; +} + +buzz::XmlElement* CreateJingleVideoContentElem( + const VideoContentDescription* video, bool crypto_required) { + buzz::XmlElement* elem = + new buzz::XmlElement(QN_JINGLE_RTP_CONTENT, true); + + elem->SetAttr(QN_JINGLE_CONTENT_MEDIA, JINGLE_CONTENT_MEDIA_VIDEO); + WriteJingleStreamsOrLegacySsrc(video, elem); + + for (VideoCodecs::const_iterator codec = video->codecs().begin(); + codec != video->codecs().end(); ++codec) { + elem->AddElement(CreateJingleVideoCodecElem(*codec)); + } + + const CryptoParamsVec& cryptos = video->cryptos(); + if (!cryptos.empty()) { + elem->AddElement(CreateJingleEncryptionElem(cryptos, crypto_required)); + } + + if (video->rtcp_mux()) { + elem->AddElement(new buzz::XmlElement(QN_JINGLE_RTCP_MUX)); + } + + if (video->bandwidth() != kAutoBandwidth) { + elem->AddElement(CreateBandwidthElem(QN_JINGLE_RTP_BANDWIDTH, + video->bandwidth())); + } + + WriteJingleRtpHeaderExtensions(video->rtp_header_extensions(), elem); + + return elem; +} + +buzz::XmlElement* CreateJingleSctpDataContentElem( + const DataContentDescription* data) { + buzz::XmlElement* content_elem = + new buzz::XmlElement(QN_JINGLE_DRAFT_SCTP_CONTENT, true); + for (std::vector::const_iterator + stream = data->streams().begin(); + stream != data->streams().end(); ++stream) { + buzz::XmlElement* stream_elem = + new buzz::XmlElement(QN_JINGLE_DRAFT_SCTP_STREAM, false); + AddXmlAttrIfNonEmpty(stream_elem, QN_NICK, stream->groupid); + AddXmlAttrIfNonEmpty(stream_elem, QN_NAME, stream->id); + if (!stream->ssrcs.empty()) { + AddXmlAttr(stream_elem, QN_SID, stream->ssrcs[0]); + } + content_elem->AddElement(stream_elem); + } + return content_elem;; +} + +buzz::XmlElement* CreateJingleRtpDataContentElem( + const DataContentDescription* data, bool crypto_required) { + + buzz::XmlElement* elem = + new buzz::XmlElement(QN_JINGLE_RTP_CONTENT, true); + + elem->SetAttr(QN_JINGLE_CONTENT_MEDIA, JINGLE_CONTENT_MEDIA_DATA); + WriteJingleStreamsOrLegacySsrc(data, elem); + + for (DataCodecs::const_iterator codec = data->codecs().begin(); + codec != data->codecs().end(); ++codec) { + elem->AddElement(CreateJingleDataCodecElem(*codec)); + } + + const CryptoParamsVec& cryptos = data->cryptos(); + if (!cryptos.empty()) { + elem->AddElement(CreateJingleEncryptionElem(cryptos, crypto_required)); + } + + if (data->rtcp_mux()) { + elem->AddElement(new buzz::XmlElement(QN_JINGLE_RTCP_MUX)); + } + + if (data->bandwidth() != kAutoBandwidth) { + elem->AddElement(CreateBandwidthElem(QN_JINGLE_RTP_BANDWIDTH, + data->bandwidth())); + } + + return elem; +} + +bool IsSctp(const DataContentDescription* data) { + return (data->protocol() == kMediaProtocolSctp || + data->protocol() == kMediaProtocolDtlsSctp); +} + +buzz::XmlElement* CreateJingleDataContentElem( + const DataContentDescription* data, bool crypto_required) { + if (IsSctp(data)) { + return CreateJingleSctpDataContentElem(data); + } else { + return CreateJingleRtpDataContentElem(data, crypto_required); + } +} + +bool MediaSessionClient::IsWritable(SignalingProtocol protocol, + const ContentDescription* content) { + const MediaContentDescription* media = + static_cast(content); + if (protocol == PROTOCOL_GINGLE && + media->type() == MEDIA_TYPE_DATA) { + return false; + } + return true; +} + +bool MediaSessionClient::WriteContent(SignalingProtocol protocol, + const ContentDescription* content, + buzz::XmlElement** elem, + WriteError* error) { + const MediaContentDescription* media = + static_cast(content); + bool crypto_required = secure() == SEC_REQUIRED; + + if (media->type() == MEDIA_TYPE_AUDIO) { + const AudioContentDescription* audio = + static_cast(media); + if (protocol == PROTOCOL_GINGLE) { + *elem = CreateGingleAudioContentElem(audio, crypto_required); + } else { + *elem = CreateJingleAudioContentElem(audio, crypto_required); + } + } else if (media->type() == MEDIA_TYPE_VIDEO) { + const VideoContentDescription* video = + static_cast(media); + if (protocol == PROTOCOL_GINGLE) { + *elem = CreateGingleVideoContentElem(video, crypto_required); + } else { + *elem = CreateJingleVideoContentElem(video, crypto_required); + } + } else if (media->type() == MEDIA_TYPE_DATA) { + const DataContentDescription* data = + static_cast(media); + if (protocol == PROTOCOL_GINGLE) { + return BadWrite("Data channel not supported with Gingle.", error); + } else { + *elem = CreateJingleDataContentElem(data, crypto_required); + } + } else { + return BadWrite("Unknown content type: " + + talk_base::ToString(media->type()), error); + } + + return true; +} + +} // namespace cricket diff --git a/talk/session/media/mediasessionclient.h b/talk/session/media/mediasessionclient.h new file mode 100644 index 000000000..c76f0e9dc --- /dev/null +++ b/talk/session/media/mediasessionclient.h @@ -0,0 +1,174 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_SESSION_MEDIA_MEDIASESSIONCLIENT_H_ +#define TALK_SESSION_MEDIA_MEDIASESSIONCLIENT_H_ + +#include +#include +#include +#include +#include "talk/base/messagequeue.h" +#include "talk/base/sigslot.h" +#include "talk/base/sigslotrepeater.h" +#include "talk/base/thread.h" +#include "talk/media/base/cryptoparams.h" +#include "talk/p2p/base/session.h" +#include "talk/p2p/base/sessionclient.h" +#include "talk/p2p/base/sessiondescription.h" +#include "talk/p2p/base/sessionmanager.h" +#include "talk/session/media/call.h" +#include "talk/session/media/channelmanager.h" +#include "talk/session/media/mediasession.h" + +namespace cricket { + +class Call; + +class MediaSessionClient : public SessionClient, public sigslot::has_slots<> { + public: +#if !defined(DISABLE_MEDIA_ENGINE_FACTORY) + MediaSessionClient(const buzz::Jid& jid, SessionManager *manager); +#endif + // Alternative constructor, allowing injection of media_engine + // and device_manager. + MediaSessionClient(const buzz::Jid& jid, SessionManager *manager, + MediaEngineInterface* media_engine, + DataEngineInterface* data_media_engine, + DeviceManagerInterface* device_manager); + ~MediaSessionClient(); + + const buzz::Jid &jid() const { return jid_; } + SessionManager* session_manager() const { return session_manager_; } + ChannelManager* channel_manager() const { return channel_manager_; } + + // Return mapping of call ids to Calls. + const std::map& calls() const { return calls_; } + + // The settings below combine with the settings on SessionManager to choose + + // whether SDES-SRTP, DTLS-SRTP, or no security should be used. The possible + // combinations are shown in the following table. Note that where either DTLS + // or SDES is possible, DTLS is preferred. Thus to require either SDES or + // DTLS, but not mandate DTLS, set SDES to require and DTLS to enable. + // + // | SDES:Disable | SDES:Enable | SDES:Require | + // ----------------------------------------------------------------| + // DTLS:Disable | No SRTP | SDES Optional | SDES Mandatory | + // DTLS:Enable | DTLS Optional | DTLS/SDES Opt | DTLS/SDES Mand | + // DTLS:Require | DTLS Mandatory | DTLS Mandatory | DTLS Mandatory | + + // Control use of SDES-SRTP. + SecurePolicy secure() const { return desc_factory_.secure(); } + void set_secure(SecurePolicy s) { desc_factory_.set_secure(s); } + + // Control use of multiple sessions in a call. + void set_multisession_enabled(bool multisession_enabled) { + multisession_enabled_ = multisession_enabled; + } + + int GetCapabilities() { return channel_manager_->GetCapabilities(); } + + Call *CreateCall(); + void DestroyCall(Call *call); + + Call *GetFocus(); + void SetFocus(Call *call); + + void JoinCalls(Call *call_to_join, Call *call); + + bool GetAudioInputDevices(std::vector* names) { + return channel_manager_->GetAudioInputDevices(names); + } + bool GetAudioOutputDevices(std::vector* names) { + return channel_manager_->GetAudioOutputDevices(names); + } + bool GetVideoCaptureDevices(std::vector* names) { + return channel_manager_->GetVideoCaptureDevices(names); + } + + bool SetAudioOptions(const std::string& in_name, const std::string& out_name, + int opts) { + return channel_manager_->SetAudioOptions(in_name, out_name, opts); + } + bool SetOutputVolume(int level) { + return channel_manager_->SetOutputVolume(level); + } + bool SetCaptureDevice(const std::string& cam_device) { + return channel_manager_->SetCaptureDevice(cam_device); + } + + SessionDescription* CreateOffer(const CallOptions& options) { + return desc_factory_.CreateOffer(options, NULL); + } + SessionDescription* CreateAnswer(const SessionDescription* offer, + const CallOptions& options) { + return desc_factory_.CreateAnswer(offer, options, NULL); + } + + sigslot::signal2 SignalFocus; + sigslot::signal1 SignalCallCreate; + sigslot::signal1 SignalCallDestroy; + sigslot::repeater0<> SignalDevicesChange; + + virtual bool ParseContent(SignalingProtocol protocol, + const buzz::XmlElement* elem, + ContentDescription** content, + ParseError* error); + virtual bool IsWritable(SignalingProtocol protocol, + const ContentDescription* content); + virtual bool WriteContent(SignalingProtocol protocol, + const ContentDescription* content, + buzz::XmlElement** elem, + WriteError* error); + + private: + void Construct(); + void OnSessionCreate(Session *session, bool received_initiate); + void OnSessionState(BaseSession *session, BaseSession::State state); + void OnSessionDestroy(Session *session); + Session *CreateSession(Call *call); + Call *FindCallByRemoteName(const std::string &remote_name); + + buzz::Jid jid_; + SessionManager* session_manager_; + Call *focus_call_; + ChannelManager *channel_manager_; + MediaSessionDescriptionFactory desc_factory_; + bool multisession_enabled_; + std::map calls_; + + // Maintain a mapping of session id to call. + typedef std::map SessionMap; + SessionMap session_map_; + + friend class Call; +}; + +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_MEDIASESSIONCLIENT_H_ diff --git a/talk/session/media/mediasessionclient_unittest.cc b/talk/session/media/mediasessionclient_unittest.cc new file mode 100644 index 000000000..f9b747bc3 --- /dev/null +++ b/talk/session/media/mediasessionclient_unittest.cc @@ -0,0 +1,3310 @@ +/* + * libjingle + * Copyright 2004 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 +#include + +#include "talk/base/gunit.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/media/base/fakemediaengine.h" +#include "talk/media/devices/fakedevicemanager.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/client/basicportallocator.h" +#include "talk/session/media/mediasessionclient.h" +#include "talk/xmllite/xmlbuilder.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmllite/xmlprinter.h" +#include "talk/xmpp/constants.h" + +// The codecs that our FakeMediaEngine will support. Order is important, since +// the tests check that our messages have codecs in the correct order. +static const cricket::AudioCodec kAudioCodecs[] = { + cricket::AudioCodec(103, "ISAC", 16000, -1, 1, 18), + cricket::AudioCodec(104, "ISAC", 32000, -1, 1, 17), + cricket::AudioCodec(119, "ISACLC", 16000, 40000, 1, 16), + cricket::AudioCodec(99, "speex", 16000, 22000, 1, 15), + cricket::AudioCodec(97, "IPCMWB", 16000, 80000, 1, 14), + cricket::AudioCodec(9, "G722", 16000, 64000, 1, 13), + cricket::AudioCodec(102, "iLBC", 8000, 13300, 1, 12), + cricket::AudioCodec(98, "speex", 8000, 11000, 1, 11), + cricket::AudioCodec(3, "GSM", 8000, 13000, 1, 10), + cricket::AudioCodec(100, "EG711U", 8000, 64000, 1, 9), + cricket::AudioCodec(101, "EG711A", 8000, 64000, 1, 8), + cricket::AudioCodec(0, "PCMU", 8000, 64000, 1, 7), + cricket::AudioCodec(8, "PCMA", 8000, 64000, 1, 6), + cricket::AudioCodec(126, "CN", 32000, 0, 1, 5), + cricket::AudioCodec(105, "CN", 16000, 0, 1, 4), + cricket::AudioCodec(13, "CN", 8000, 0, 1, 3), + cricket::AudioCodec(117, "red", 8000, 0, 1, 2), + cricket::AudioCodec(106, "telephone-event", 8000, 0, 1, 1) +}; + +static const cricket::VideoCodec kVideoCodecs[] = { + cricket::VideoCodec(96, "H264-SVC", 320, 200, 30, 1) +}; + +static const cricket::DataCodec kDataCodecs[] = { + cricket::DataCodec(127, "google-data", 0) +}; + +const std::string kGingleCryptoOffer = \ + " " \ + " " \ + " " \ + " " \ + " "; + +// Jingle offer does not have any element. +const std::string kJingleCryptoOffer = \ + " " \ + " " \ + " " \ + " "; + + +const std::string kGingleRequiredCryptoOffer = \ + " "\ + " " \ + " " \ + " " \ + " "; + +const std::string kJingleRequiredCryptoOffer = \ + " "\ + " " \ + " " \ + " "; + + +const std::string kGingleUnsupportedCryptoOffer = \ + " " \ + " " \ + " " \ + " " \ + " "; + +const std::string kJingleUnsupportedCryptoOffer = \ + " " \ + " " \ + " " \ + " "; + + +// With unsupported but with required="true" +const std::string kGingleRequiredUnsupportedCryptoOffer = \ + "" \ + " " \ + " " \ + " " \ + " "; + +const std::string kJingleRequiredUnsupportedCryptoOffer = \ + "" \ + " " \ + " " \ + " "; + +const std::string kGingleInitiate( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleInitiate( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +// Initiate string with a different order of supported codecs. +// Should accept the supported ones, but with our desired order. +const std::string kGingleInitiateDifferentPreference( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleInitiateDifferentPreference( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kGingleVideoInitiate( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleVideoInitiate( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleVideoInitiateWithRtpData( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleVideoInitiateWithSctpData( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleVideoInitiateWithBandwidth( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " 42 " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleVideoInitiateWithRtcpMux( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +// Initiate string with a combination of supported and unsupported codecs +// Should accept the supported ones +const std::string kGingleInitiateSomeUnsupported( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleInitiateSomeUnsupported( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kGingleVideoInitiateWithBandwidth( + " " \ + " " \ + " " \ + " " \ + " " \ + " 42 " \ + " " \ + " " \ + " "); + +// Initiate string without any supported codecs. Should send a reject. +const std::string kGingleInitiateNoSupportedAudioCodecs( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleInitiateNoSupportedAudioCodecs( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +// Initiate string without any codecs. Assumes ancient version of Cricket +// and tries a session with ISAC and PCMU +const std::string kGingleInitiateNoAudioCodecs( + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleInitiateNoAudioCodecs( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +// The codecs are supported, but not at the given clockrates. Should send +// a reject. +const std::string kGingleInitiateWrongClockrates( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleInitiateWrongClockrates( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +// The codecs are supported, but not with the given number of channels. +// Should send a reject. +const std::string kGingleInitiateWrongChannels( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleInitiateWrongChannels( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +// Initiate with a dynamic codec not using webrtc default payload id. Should +// accept with provided payload id. +const std::string kGingleInitiateDynamicAudioCodecs( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleInitiateDynamicAudioCodecs( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +// Initiate string with nothing but static codec id's. Should accept. +const std::string kGingleInitiateStaticAudioCodecs( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleInitiateStaticAudioCodecs( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +// Initiate with payload type-less codecs. Should reject. +const std::string kGingleInitiateNoPayloadTypes( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleInitiateNoPayloadTypes( + " " \ + " " \ + " sid='abcdef' initiator='me@domain.com/resource'> " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +// Initiate with unnamed dynamic codces. Should reject. +const std::string kGingleInitiateDynamicWithoutNames( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleInitiateDynamicWithoutNames( + " " \ + " " \ + " sid='abcdef' initiator='me@domain.com/resource'> " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const uint32 kAudioSsrc = 4294967295U; +const uint32 kVideoSsrc = 87654321; +const uint32 kDataSsrc = 1010101; +const uint32 kDataSid = 0; +// Note that this message does not specify a session ID. It must be populated +// before use. +const std::string kGingleAcceptWithSsrcs( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " 4294967295 " \ + " 87654321 " \ + " " \ + " " \ + " "); + +const std::string kJingleAcceptWithSsrcs( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleAcceptWithRtpDataSsrcs( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +const std::string kJingleAcceptWithSctpData( + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " "); + +std::string JingleView(const std::string& ssrc, + const std::string& width, + const std::string& height, + const std::string& framerate) { + // We have some slightly weird whitespace formatting to make the + // actual XML generated match the expected XML here. + return \ + "" + "" + "" + "" + "" + "" + ""; +} + +std::string JingleStreamAdd(const std::string& content_name, + const std::string& nick, + const std::string& name, + const std::string& ssrc) { + return \ + "" + " " + " " + " " + " " + " " + " " + ssrc + "" + " " + " " + " " + " " + " " + ""; +} + +std::string JingleOutboundStreamRemove(const std::string& sid, + const std::string& content_name, + const std::string& name) { + return \ + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; +} + +std::string JingleOutboundStreamAdd(const std::string& sid, + const std::string& content_name, + const std::string& name, + const std::string& ssrc) { + return \ + "" + "" + "" + "" + "" + "" + "" + ssrc + "" + "" + "" + "" + "" + "" + ""; +} + +std::string JingleStreamAddWithoutSsrc(const std::string& content_name, + const std::string& nick, + const std::string& name) { + return \ + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; +} + +std::string JingleStreamRemove(const std::string& content_name, + const std::string& nick, + const std::string& name) { + return \ + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; +} + +// Convenience function to get CallOptions that have audio enabled, +// but not video or data. +static cricket::CallOptions AudioCallOptions() { + cricket::CallOptions options; + options.has_audio = true; + options.has_video = false; + options.data_channel_type = cricket::DCT_NONE; + return options; +} + +// Convenience function to get CallOptions that have audio and video +// enabled, but not data. +static cricket::CallOptions VideoCallOptions() { + cricket::CallOptions options; + options.has_audio = true; + options.has_video = true; + options.data_channel_type = cricket::DCT_NONE; + return options; +} + +buzz::XmlElement* CopyElement(const buzz::XmlElement* elem) { + return new buzz::XmlElement(*elem); +} + +static std::string AddEncryption(std::string stanza, std::string encryption) { + std::string::size_type pos = stanza.find(""); + while (pos != std::string::npos) { + stanza = stanza.insert(pos, encryption); + pos = stanza.find("", pos + encryption.length() + 1); + } + return stanza; +} + +int IntFromJingleCodecParameter(const buzz::XmlElement* parameter, + const std::string& expected_name) { + if (parameter) { + const std::string& actual_name = + parameter->Attr(cricket::QN_PAYLOADTYPE_PARAMETER_NAME); + + EXPECT_EQ(expected_name, actual_name) + << "wrong parameter name. Expected '" + << expected_name << "'. Actually '" + << actual_name << "'."; + + return atoi(parameter->Attr( + cricket::QN_PAYLOADTYPE_PARAMETER_VALUE).c_str()); + } + return 0; +} + +// Parses and extracts payload and codec info from test XML. Since +// that XML will be in various contents (Gingle and Jingle), we need an +// abstract parser with one concrete implementation per XML content. +class MediaSessionTestParser { + public: + virtual buzz::XmlElement* ActionFromStanza(buzz::XmlElement* stanza) = 0; + virtual buzz::XmlElement* ContentFromAction(buzz::XmlElement* action) = 0; + virtual buzz::XmlElement* NextContent(buzz::XmlElement* content) = 0; + virtual buzz::XmlElement* PayloadTypeFromContent( + buzz::XmlElement* content) = 0; + virtual buzz::XmlElement* NextFromPayloadType( + buzz::XmlElement* payload_type) = 0; + virtual cricket::AudioCodec AudioCodecFromPayloadType( + const buzz::XmlElement* payload_type) = 0; + virtual cricket::VideoCodec VideoCodecFromPayloadType( + const buzz::XmlElement* payload_type) = 0; + virtual cricket::DataCodec DataCodecFromPayloadType( + const buzz::XmlElement* payload_type) = 0; + virtual buzz::XmlElement* EncryptionFromContent( + buzz::XmlElement* content) = 0; + virtual buzz::XmlElement* NextFromEncryption( + buzz::XmlElement* encryption) = 0; + virtual const buzz::XmlElement* BandwidthFromContent( + buzz::XmlElement* content) = 0; + virtual const buzz::XmlElement* RtcpMuxFromContent( + buzz::XmlElement* content) = 0; + virtual bool ActionIsTerminate(const buzz::XmlElement* action) = 0; + virtual ~MediaSessionTestParser() {} +}; + +class JingleSessionTestParser : public MediaSessionTestParser { + public: + JingleSessionTestParser() : action_(NULL) {} + + ~JingleSessionTestParser() { + delete action_; + } + + buzz::XmlElement* ActionFromStanza(buzz::XmlElement* stanza) { + return stanza->FirstNamed(cricket::QN_JINGLE); + } + + buzz::XmlElement* ContentFromAction(buzz::XmlElement* action) { + // We need to be able to use multiple contents, but the action + // gets deleted before we can call NextContent, so we need to + // stash away a copy. + action_ = CopyElement(action); + return action_->FirstNamed(cricket::QN_JINGLE_CONTENT); + } + + buzz::XmlElement* NextContent(buzz::XmlElement* content) { + // For some reason, content->NextNamed(cricket::QN_JINGLE_CONTENT) + // doesn't work. + return action_->FirstNamed(cricket::QN_JINGLE_CONTENT) + ->NextNamed(cricket::QN_JINGLE_CONTENT); + } + + buzz::XmlElement* PayloadTypeFromContent(buzz::XmlElement* content) { + buzz::XmlElement* content_desc = + content->FirstNamed(cricket::QN_JINGLE_RTP_CONTENT); + if (!content_desc) + return NULL; + + return content_desc->FirstNamed(cricket::QN_JINGLE_RTP_PAYLOADTYPE); + } + + buzz::XmlElement* NextFromPayloadType(buzz::XmlElement* payload_type) { + return payload_type->NextNamed(cricket::QN_JINGLE_RTP_PAYLOADTYPE); + } + + cricket::AudioCodec AudioCodecFromPayloadType( + const buzz::XmlElement* payload_type) { + int id = 0; + if (payload_type->HasAttr(cricket::QN_ID)) + id = atoi(payload_type->Attr(cricket::QN_ID).c_str()); + + std::string name; + if (payload_type->HasAttr(cricket::QN_NAME)) + name = payload_type->Attr(cricket::QN_NAME); + + int clockrate = 0; + if (payload_type->HasAttr(cricket::QN_CLOCKRATE)) + clockrate = atoi(payload_type->Attr(cricket::QN_CLOCKRATE).c_str()); + + int bitrate = IntFromJingleCodecParameter( + payload_type->FirstNamed(cricket::QN_PARAMETER), "bitrate"); + + int channels = 1; + if (payload_type->HasAttr(cricket::QN_CHANNELS)) + channels = atoi(payload_type->Attr( + cricket::QN_CHANNELS).c_str()); + + return cricket::AudioCodec(id, name, clockrate, bitrate, channels, 0); + } + + cricket::VideoCodec VideoCodecFromPayloadType( + const buzz::XmlElement* payload_type) { + int id = 0; + if (payload_type->HasAttr(cricket::QN_ID)) + id = atoi(payload_type->Attr(cricket::QN_ID).c_str()); + + std::string name; + if (payload_type->HasAttr(cricket::QN_NAME)) + name = payload_type->Attr(cricket::QN_NAME); + + int width = 0; + int height = 0; + int framerate = 0; + const buzz::XmlElement* param = + payload_type->FirstNamed(cricket::QN_PARAMETER); + if (param) { + width = IntFromJingleCodecParameter(param, "width"); + param = param->NextNamed(cricket::QN_PARAMETER); + if (param) { + height = IntFromJingleCodecParameter(param, "height"); + param = param->NextNamed(cricket::QN_PARAMETER); + if (param) { + framerate = IntFromJingleCodecParameter(param, "framerate"); + } + } + } + + return cricket::VideoCodec(id, name, width, height, framerate, 0); + } + + cricket::DataCodec DataCodecFromPayloadType( + const buzz::XmlElement* payload_type) { + int id = 0; + if (payload_type->HasAttr(cricket::QN_ID)) + id = atoi(payload_type->Attr(cricket::QN_ID).c_str()); + + std::string name; + if (payload_type->HasAttr(cricket::QN_NAME)) + name = payload_type->Attr(cricket::QN_NAME); + + return cricket::DataCodec(id, name, 0); + } + + bool ActionIsTerminate(const buzz::XmlElement* action) { + return (action->HasAttr(cricket::QN_ACTION) && + action->Attr(cricket::QN_ACTION) == "session-terminate"); + } + + buzz::XmlElement* EncryptionFromContent(buzz::XmlElement* content) { + buzz::XmlElement* content_desc = + content->FirstNamed(cricket::QN_JINGLE_RTP_CONTENT); + if (!content_desc) + return NULL; + + return content_desc->FirstNamed(cricket::QN_ENCRYPTION); + } + + buzz::XmlElement* NextFromEncryption(buzz::XmlElement* encryption) { + return encryption->NextNamed(cricket::QN_ENCRYPTION); + } + + const buzz::XmlElement* BandwidthFromContent(buzz::XmlElement* content) { + buzz::XmlElement* content_desc = + content->FirstNamed(cricket::QN_JINGLE_RTP_CONTENT); + if (!content_desc) + return NULL; + + return content_desc->FirstNamed(cricket::QN_JINGLE_RTP_BANDWIDTH); + } + + const buzz::XmlElement* RtcpMuxFromContent(buzz::XmlElement* content) { + return content->FirstNamed(cricket::QN_JINGLE_RTCP_MUX); + } + + private: + buzz::XmlElement* action_; +}; + +class GingleSessionTestParser : public MediaSessionTestParser { + public: + GingleSessionTestParser() : found_content_count_(0) {} + + buzz::XmlElement* ActionFromStanza(buzz::XmlElement* stanza) { + return stanza->FirstNamed(cricket::QN_GINGLE_SESSION); + } + + buzz::XmlElement* ContentFromAction(buzz::XmlElement* session) { + buzz::XmlElement* content = + session->FirstNamed(cricket::QN_GINGLE_AUDIO_CONTENT); + if (content == NULL) + content = session->FirstNamed(cricket::QN_GINGLE_VIDEO_CONTENT); + return content; + } + + // Assumes contents are in order of audio, and then video. + buzz::XmlElement* NextContent(buzz::XmlElement* content) { + found_content_count_++; + return content; + } + + buzz::XmlElement* PayloadTypeFromContent(buzz::XmlElement* content) { + if (found_content_count_ > 0) { + return content->FirstNamed(cricket::QN_GINGLE_VIDEO_PAYLOADTYPE); + } else { + return content->FirstNamed(cricket::QN_GINGLE_AUDIO_PAYLOADTYPE); + } + } + + buzz::XmlElement* NextFromPayloadType(buzz::XmlElement* payload_type) { + if (found_content_count_ > 0) { + return payload_type->NextNamed(cricket::QN_GINGLE_VIDEO_PAYLOADTYPE); + } else { + return payload_type->NextNamed(cricket::QN_GINGLE_AUDIO_PAYLOADTYPE); + } + } + + cricket::AudioCodec AudioCodecFromPayloadType( + const buzz::XmlElement* payload_type) { + int id = 0; + if (payload_type->HasAttr(cricket::QN_ID)) + id = atoi(payload_type->Attr(cricket::QN_ID).c_str()); + + std::string name; + if (payload_type->HasAttr(cricket::QN_NAME)) + name = payload_type->Attr(cricket::QN_NAME); + + int clockrate = 0; + if (payload_type->HasAttr(cricket::QN_CLOCKRATE)) + clockrate = atoi(payload_type->Attr(cricket::QN_CLOCKRATE).c_str()); + + int bitrate = 0; + if (payload_type->HasAttr(cricket::QN_BITRATE)) + bitrate = atoi(payload_type->Attr(cricket::QN_BITRATE).c_str()); + + int channels = 1; + if (payload_type->HasAttr(cricket::QN_CHANNELS)) + channels = atoi(payload_type->Attr(cricket::QN_CHANNELS).c_str()); + + return cricket::AudioCodec(id, name, clockrate, bitrate, channels, 0); + } + + cricket::VideoCodec VideoCodecFromPayloadType( + const buzz::XmlElement* payload_type) { + int id = 0; + if (payload_type->HasAttr(cricket::QN_ID)) + id = atoi(payload_type->Attr(cricket::QN_ID).c_str()); + + std::string name; + if (payload_type->HasAttr(cricket::QN_NAME)) + name = payload_type->Attr(cricket::QN_NAME); + + int width = 0; + if (payload_type->HasAttr(cricket::QN_WIDTH)) + width = atoi(payload_type->Attr(cricket::QN_WIDTH).c_str()); + + int height = 0; + if (payload_type->HasAttr(cricket::QN_HEIGHT)) + height = atoi(payload_type->Attr(cricket::QN_HEIGHT).c_str()); + + int framerate = 1; + if (payload_type->HasAttr(cricket::QN_FRAMERATE)) + framerate = atoi(payload_type->Attr(cricket::QN_FRAMERATE).c_str()); + + return cricket::VideoCodec(id, name, width, height, framerate, 0); + } + + cricket::DataCodec DataCodecFromPayloadType( + const buzz::XmlElement* payload_type) { + // Gingle can't do data codecs. + return cricket::DataCodec(0, "", 0); + } + + buzz::XmlElement* EncryptionFromContent( + buzz::XmlElement* content) { + return content->FirstNamed(cricket::QN_ENCRYPTION); + } + + buzz::XmlElement* NextFromEncryption(buzz::XmlElement* encryption) { + return encryption->NextNamed(cricket::QN_ENCRYPTION); + } + + const buzz::XmlElement* BandwidthFromContent(buzz::XmlElement* content) { + return content->FirstNamed(cricket::QN_GINGLE_VIDEO_BANDWIDTH); + } + + const buzz::XmlElement* RtcpMuxFromContent(buzz::XmlElement* content) { + return NULL; + } + + bool ActionIsTerminate(const buzz::XmlElement* session) { + return (session->HasAttr(buzz::QN_TYPE) && + session->Attr(buzz::QN_TYPE) == "terminate"); + } + + int found_content_count_; +}; + +class MediaSessionClientTest : public sigslot::has_slots<> { + public: + explicit MediaSessionClientTest(MediaSessionTestParser* parser, + cricket::SignalingProtocol initial_protocol) { + nm_ = new talk_base::BasicNetworkManager(); + pa_ = new cricket::BasicPortAllocator(nm_); + sm_ = new cricket::SessionManager(pa_, NULL); + fme_ = new cricket::FakeMediaEngine(); + fdme_ = new cricket::FakeDataEngine(); + + std::vector + audio_codecs(kAudioCodecs, kAudioCodecs + ARRAY_SIZE(kAudioCodecs)); + fme_->SetAudioCodecs(audio_codecs); + std::vector + video_codecs(kVideoCodecs, kVideoCodecs + ARRAY_SIZE(kVideoCodecs)); + fme_->SetVideoCodecs(video_codecs); + std::vector + data_codecs(kDataCodecs, kDataCodecs + ARRAY_SIZE(kDataCodecs)); + fdme_->SetDataCodecs(data_codecs); + + client_ = new cricket::MediaSessionClient( + buzz::Jid("user@domain.com/resource"), sm_, + fme_, fdme_, new cricket::FakeDeviceManager()); + client_->session_manager()->SignalOutgoingMessage.connect( + this, &MediaSessionClientTest::OnSendStanza); + client_->session_manager()->SignalSessionCreate.connect( + this, &MediaSessionClientTest::OnSessionCreate); + client_->SignalCallCreate.connect( + this, &MediaSessionClientTest::OnCallCreate); + client_->SignalCallDestroy.connect( + this, &MediaSessionClientTest::OnCallDestroy); + + call_ = NULL; + parser_ = parser; + initial_protocol_ = initial_protocol; + expect_incoming_crypto_ = false; + expect_outgoing_crypto_ = false; + expected_video_bandwidth_ = cricket::kAutoBandwidth; + expected_video_rtcp_mux_ = false; + } + + ~MediaSessionClientTest() { + delete client_; + delete sm_; + delete pa_; + delete nm_; + delete parser_; + } + + buzz::XmlElement* ActionFromStanza(buzz::XmlElement* stanza) { + return parser_->ActionFromStanza(stanza); + } + + buzz::XmlElement* ContentFromAction(buzz::XmlElement* action) { + return parser_->ContentFromAction(action); + } + + buzz::XmlElement* PayloadTypeFromContent(buzz::XmlElement* payload) { + return parser_->PayloadTypeFromContent(payload); + } + + buzz::XmlElement* NextFromPayloadType(buzz::XmlElement* payload_type) { + return parser_->NextFromPayloadType(payload_type); + } + + buzz::XmlElement* EncryptionFromContent(buzz::XmlElement* content) { + return parser_->EncryptionFromContent(content); + } + + buzz::XmlElement* NextFromEncryption(buzz::XmlElement* encryption) { + return parser_->NextFromEncryption(encryption); + } + + cricket::AudioCodec AudioCodecFromPayloadType( + const buzz::XmlElement* payload_type) { + return parser_->AudioCodecFromPayloadType(payload_type); + } + + const cricket::AudioContentDescription* GetFirstAudioContentDescription( + const cricket::SessionDescription* sdesc) { + const cricket::ContentInfo* content = + cricket::GetFirstAudioContent(sdesc); + if (content == NULL) + return NULL; + return static_cast( + content->description); + } + + const cricket::VideoContentDescription* GetFirstVideoContentDescription( + const cricket::SessionDescription* sdesc) { + const cricket::ContentInfo* content = + cricket::GetFirstVideoContent(sdesc); + if (content == NULL) + return NULL; + return static_cast( + content->description); + } + + void CheckCryptoFromGoodIncomingInitiate(const cricket::Session* session) { + ASSERT_TRUE(session != NULL); + const cricket::AudioContentDescription* content = + GetFirstAudioContentDescription(session->remote_description()); + ASSERT_TRUE(content != NULL); + ASSERT_EQ(2U, content->cryptos().size()); + ASSERT_EQ(145, content->cryptos()[0].tag); + ASSERT_EQ("AES_CM_128_HMAC_SHA1_32", content->cryptos()[0].cipher_suite); + ASSERT_EQ("inline:hsWuSQJxx7przmb8HM+ZkeNcG3HezSNID7LmfDa9", + content->cryptos()[0].key_params); + ASSERT_EQ(51, content->cryptos()[1].tag); + ASSERT_EQ("AES_CM_128_HMAC_SHA1_80", content->cryptos()[1].cipher_suite); + ASSERT_EQ("inline:J4lfdUL8W1F7TNJKcbuygaQuA429SJy2e9JctPUy", + content->cryptos()[1].key_params); + } + + void CheckCryptoForGoodOutgoingAccept(const cricket::Session* session) { + const cricket::AudioContentDescription* content = + GetFirstAudioContentDescription(session->local_description()); + ASSERT_EQ(1U, content->cryptos().size()); + ASSERT_EQ(145, content->cryptos()[0].tag); + ASSERT_EQ("AES_CM_128_HMAC_SHA1_32", content->cryptos()[0].cipher_suite); + ASSERT_EQ(47U, content->cryptos()[0].key_params.size()); + } + + void CheckBadCryptoFromIncomingInitiate(const cricket::Session* session) { + const cricket::AudioContentDescription* content = + GetFirstAudioContentDescription(session->remote_description()); + ASSERT_EQ(1U, content->cryptos().size()); + ASSERT_EQ(145, content->cryptos()[0].tag); + ASSERT_EQ("NOT_SUPPORTED", content->cryptos()[0].cipher_suite); + ASSERT_EQ("inline:hsWuSQJxx7przmb8HM+ZkeNcG3HezSNID7LmfDa9", + content->cryptos()[0].key_params); + } + + void CheckNoCryptoForOutgoingAccept(const cricket::Session* session) { + const cricket::AudioContentDescription* content = + GetFirstAudioContentDescription(session->local_description()); + ASSERT_TRUE(content->cryptos().empty()); + } + + void CheckVideoBandwidth(int expected_bandwidth, + const cricket::SessionDescription* sdesc) { + const cricket::VideoContentDescription* video = + GetFirstVideoContentDescription(sdesc); + if (video != NULL) { + ASSERT_EQ(expected_bandwidth, video->bandwidth()); + } + } + + void CheckVideoRtcpMux(bool expected_video_rtcp_mux, + const cricket::SessionDescription* sdesc) { + const cricket::VideoContentDescription* video = + GetFirstVideoContentDescription(sdesc); + if (video != NULL) { + ASSERT_EQ(expected_video_rtcp_mux, video->rtcp_mux()); + } + } + + virtual void CheckRtpDataContent(buzz::XmlElement* content) { + if (initial_protocol_) { + // Gingle can not write out data content. + return; + } + + buzz::XmlElement* e = PayloadTypeFromContent(content); + ASSERT_TRUE(e != NULL); + cricket::DataCodec codec = parser_->DataCodecFromPayloadType(e); + EXPECT_EQ(127, codec.id); + EXPECT_EQ("google-data", codec.name); + + CheckDataRtcpMux(true, call_->sessions()[0]->local_description()); + CheckDataRtcpMux(true, call_->sessions()[0]->remote_description()); + if (expect_outgoing_crypto_) { + content = parser_->NextContent(content); + buzz::XmlElement* encryption = EncryptionFromContent(content); + ASSERT_TRUE(encryption != NULL); + // TODO(pthatcher): Check encryption parameters? + } + } + + virtual void CheckSctpDataContent(buzz::XmlElement* content) { + if (initial_protocol_) { + // Gingle can not write out data content. + return; + } + + buzz::XmlElement* payload_type = PayloadTypeFromContent(content); + ASSERT_TRUE(payload_type == NULL); + buzz::XmlElement* encryption = EncryptionFromContent(content); + ASSERT_TRUE(encryption == NULL); + // TODO(pthatcher): Check for . + } + + void CheckDataRtcpMux(bool expected_data_rtcp_mux, + const cricket::SessionDescription* sdesc) { + const cricket::DataContentDescription* data = + GetFirstDataContentDescription(sdesc); + if (data != NULL) { + ASSERT_EQ(expected_data_rtcp_mux, data->rtcp_mux()); + } + } + + void CheckAudioSsrcForIncomingAccept(const cricket::Session* session) { + const cricket::AudioContentDescription* audio = + GetFirstAudioContentDescription(session->remote_description()); + ASSERT_TRUE(audio != NULL); + ASSERT_EQ(kAudioSsrc, audio->first_ssrc()); + } + + void CheckVideoSsrcForIncomingAccept(const cricket::Session* session) { + const cricket::VideoContentDescription* video = + GetFirstVideoContentDescription(session->remote_description()); + ASSERT_TRUE(video != NULL); + ASSERT_EQ(kVideoSsrc, video->first_ssrc()); + } + + void CheckDataSsrcForIncomingAccept(const cricket::Session* session) { + const cricket::DataContentDescription* data = + GetFirstDataContentDescription(session->remote_description()); + ASSERT_TRUE(data != NULL); + ASSERT_EQ(kDataSsrc, data->first_ssrc()); + } + + void TestGoodIncomingInitiate(const std::string& initiate_string, + const cricket::CallOptions& options, + buzz::XmlElement** element) { + *element = NULL; + + buzz::XmlElement* el = buzz::XmlElement::ForStr(initiate_string); + client_->session_manager()->OnIncomingMessage(el); + ASSERT_TRUE(call_ != NULL); + ASSERT_TRUE(call_->sessions()[0] != NULL); + ASSERT_EQ(cricket::Session::STATE_RECEIVEDINITIATE, + call_->sessions()[0]->state()); + ASSERT_EQ(1U, stanzas_.size()); + ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name()); + ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE)); + ASSERT_EQ(std::string(buzz::STR_RESULT), stanzas_[0]->Attr(buzz::QN_TYPE)); + delete stanzas_[0]; + stanzas_.clear(); + CheckVideoBandwidth(expected_video_bandwidth_, + call_->sessions()[0]->remote_description()); + CheckVideoRtcpMux(expected_video_rtcp_mux_, + call_->sessions()[0]->remote_description()); + if (expect_incoming_crypto_) { + CheckCryptoFromGoodIncomingInitiate(call_->sessions()[0]); + } + + // TODO(pthatcher): Add tests for sending in accept. + call_->AcceptSession(call_->sessions()[0], options); + ASSERT_EQ(cricket::Session::STATE_SENTACCEPT, + call_->sessions()[0]->state()); + ASSERT_EQ(1U, stanzas_.size()); + ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name()); + ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE)); + ASSERT_EQ(std::string(buzz::STR_SET), stanzas_[0]->Attr(buzz::QN_TYPE)); + + buzz::XmlElement* e = ActionFromStanza(stanzas_[0]); + ASSERT_TRUE(e != NULL); + ASSERT_TRUE(ContentFromAction(e) != NULL); + *element = CopyElement(ContentFromAction(e)); + ASSERT_TRUE(*element != NULL); + delete stanzas_[0]; + stanzas_.clear(); + if (expect_outgoing_crypto_) { + CheckCryptoForGoodOutgoingAccept(call_->sessions()[0]); + } + + if (options.data_channel_type == cricket::DCT_RTP) { + CheckDataRtcpMux(true, call_->sessions()[0]->local_description()); + CheckDataRtcpMux(true, call_->sessions()[0]->remote_description()); + // TODO(pthatcher): Check rtcpmux and crypto? + } + + call_->Terminate(); + ASSERT_EQ(cricket::Session::STATE_SENTTERMINATE, + call_->sessions()[0]->state()); + ASSERT_EQ(1U, stanzas_.size()); + ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name()); + ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE)); + ASSERT_EQ(std::string(buzz::STR_SET), stanzas_[0]->Attr(buzz::QN_TYPE)); + e = ActionFromStanza(stanzas_[0]); + ASSERT_TRUE(e != NULL); + ASSERT_TRUE(parser_->ActionIsTerminate(e)); + delete stanzas_[0]; + stanzas_.clear(); + } + + void TestRejectOffer(const std::string &initiate_string, + const cricket::CallOptions& options, + buzz::XmlElement** element) { + *element = NULL; + + buzz::XmlElement* el = buzz::XmlElement::ForStr(initiate_string); + client_->session_manager()->OnIncomingMessage(el); + ASSERT_TRUE(call_ != NULL); + ASSERT_TRUE(call_->sessions()[0] != NULL); + ASSERT_EQ(cricket::Session::STATE_RECEIVEDINITIATE, + call_->sessions()[0]->state()); + ASSERT_EQ(1U, stanzas_.size()); + ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name()); + ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE)); + ASSERT_EQ(std::string(buzz::STR_RESULT), stanzas_[0]->Attr(buzz::QN_TYPE)); + delete stanzas_[0]; + stanzas_.clear(); + + call_->AcceptSession(call_->sessions()[0], options); + ASSERT_EQ(cricket::Session::STATE_SENTACCEPT, + call_->sessions()[0]->state()); + ASSERT_EQ(1U, stanzas_.size()); + ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name()); + ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE)); + ASSERT_EQ(std::string(buzz::STR_SET), stanzas_[0]->Attr(buzz::QN_TYPE)); + + buzz::XmlElement* e = ActionFromStanza(stanzas_[0]); + ASSERT_TRUE(e != NULL); + ASSERT_TRUE(ContentFromAction(e) != NULL); + *element = CopyElement(ContentFromAction(e)); + ASSERT_TRUE(*element != NULL); + delete stanzas_[0]; + stanzas_.clear(); + + buzz::XmlElement* content = *element; + // The NextContent method actually returns the second content. So we + // can't handle the case when audio, video and data are all enabled. But + // since we are testing rejection, it won't be the case. + if (options.has_audio) { + ASSERT_TRUE(content != NULL); + ASSERT_EQ("test audio", content->Attr(buzz::QName("", "name"))); + content = parser_->NextContent(content); + } + + if (options.has_video) { + ASSERT_TRUE(content != NULL); + ASSERT_EQ("test video", content->Attr(buzz::QName("", "name"))); + content = parser_->NextContent(content); + } + + if (options.has_data()) { + ASSERT_TRUE(content != NULL); + ASSERT_EQ("test data", content->Attr(buzz::QName("", "name"))); + content = parser_->NextContent(content); + } + + call_->Terminate(); + ASSERT_EQ(cricket::Session::STATE_SENTTERMINATE, + call_->sessions()[0]->state()); + ASSERT_EQ(1U, stanzas_.size()); + ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name()); + ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE)); + ASSERT_EQ(std::string(buzz::STR_SET), stanzas_[0]->Attr(buzz::QN_TYPE)); + e = ActionFromStanza(stanzas_[0]); + ASSERT_TRUE(e != NULL); + ASSERT_TRUE(parser_->ActionIsTerminate(e)); + delete stanzas_[0]; + stanzas_.clear(); + } + + void TestBadIncomingInitiate(const std::string& initiate_string) { + buzz::XmlElement* el = buzz::XmlElement::ForStr(initiate_string); + client_->session_manager()->OnIncomingMessage(el); + ASSERT_TRUE(call_ != NULL); + ASSERT_TRUE(call_->sessions()[0] != NULL); + ASSERT_EQ(cricket::Session::STATE_SENTREJECT, + call_->sessions()[0]->state()); + ASSERT_EQ(2U, stanzas_.size()); + ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name()); + ASSERT_TRUE(stanzas_[1]->HasAttr(buzz::QN_TYPE)); + ASSERT_EQ(std::string(buzz::STR_RESULT), stanzas_[1]->Attr(buzz::QN_TYPE)); + delete stanzas_[0]; + stanzas_.clear(); + } + + void TestGoodOutgoingInitiate(const cricket::CallOptions& options) { + client_->CreateCall(); + ASSERT_TRUE(call_ != NULL); + call_->InitiateSession(buzz::Jid("me@mydomain.com"), + buzz::Jid("me@mydomain.com"), options); + ASSERT_TRUE(call_->sessions()[0] != NULL); + ASSERT_EQ(cricket::Session::STATE_SENTINITIATE, + call_->sessions()[0]->state()); + ASSERT_EQ(1U, stanzas_.size()); + ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name()); + ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE)); + ASSERT_EQ(std::string(buzz::STR_SET), stanzas_[0]->Attr(buzz::QN_TYPE)); + buzz::XmlElement* action = ActionFromStanza(stanzas_[0]); + ASSERT_TRUE(action != NULL); + buzz::XmlElement* content = ContentFromAction(action); + ASSERT_TRUE(content != NULL); + + buzz::XmlElement* e = PayloadTypeFromContent(content); + ASSERT_TRUE(e != NULL); + cricket::AudioCodec codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(103, codec.id); + ASSERT_EQ("ISAC", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(0, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(104, codec.id); + ASSERT_EQ("ISAC", codec.name); + ASSERT_EQ(32000, codec.clockrate); + ASSERT_EQ(0, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(119, codec.id); + ASSERT_EQ("ISACLC", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(40000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(99, codec.id); + ASSERT_EQ("speex", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(22000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(97, codec.id); + ASSERT_EQ("IPCMWB", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(80000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(9, codec.id); + ASSERT_EQ("G722", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(64000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(102, codec.id); + ASSERT_EQ("iLBC", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(13300, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(98, codec.id); + ASSERT_EQ("speex", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(11000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(3, codec.id); + ASSERT_EQ("GSM", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(13000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(100, codec.id); + ASSERT_EQ("EG711U", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(64000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(101, codec.id); + ASSERT_EQ("EG711A", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(64000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(0, codec.id); + ASSERT_EQ("PCMU", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(64000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(8, codec.id); + ASSERT_EQ("PCMA", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(64000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(126, codec.id); + ASSERT_EQ("CN", codec.name); + ASSERT_EQ(32000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(105, codec.id); + ASSERT_EQ("CN", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(13, codec.id); + ASSERT_EQ("CN", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(117, codec.id); + ASSERT_EQ("red", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(106, codec.id); + ASSERT_EQ("telephone-event", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e == NULL); + + if (expect_outgoing_crypto_) { + buzz::XmlElement* encryption = EncryptionFromContent(content); + ASSERT_TRUE(encryption != NULL); + + if (client_->secure() == cricket::SEC_REQUIRED) { + ASSERT_TRUE(cricket::GetXmlAttr( + encryption, cricket::QN_ENCRYPTION_REQUIRED, false)); + } + + if (content->Name().Namespace() == cricket::NS_GINGLE_AUDIO) { + e = encryption->FirstNamed(cricket::QN_GINGLE_AUDIO_CRYPTO_USAGE); + ASSERT_TRUE(e != NULL); + ASSERT_TRUE( + e->NextNamed(cricket::QN_GINGLE_AUDIO_CRYPTO_USAGE) == NULL); + ASSERT_TRUE( + e->FirstNamed(cricket::QN_GINGLE_VIDEO_CRYPTO_USAGE) == NULL); + } + + e = encryption->FirstNamed(cricket::QN_CRYPTO); + ASSERT_TRUE(e != NULL); + ASSERT_EQ("0", e->Attr(cricket::QN_CRYPTO_TAG)); + ASSERT_EQ("AES_CM_128_HMAC_SHA1_32", e->Attr(cricket::QN_CRYPTO_SUITE)); + std::string key_0 = e->Attr(cricket::QN_CRYPTO_KEY_PARAMS); + ASSERT_EQ(47U, key_0.length()); + ASSERT_EQ("inline:", key_0.substr(0, 7)); + + e = e->NextNamed(cricket::QN_CRYPTO); + ASSERT_TRUE(e != NULL); + ASSERT_EQ("1", e->Attr(cricket::QN_CRYPTO_TAG)); + ASSERT_EQ("AES_CM_128_HMAC_SHA1_80", e->Attr(cricket::QN_CRYPTO_SUITE)); + std::string key_1 = e->Attr(cricket::QN_CRYPTO_KEY_PARAMS); + ASSERT_EQ(47U, key_1.length()); + ASSERT_EQ("inline:", key_1.substr(0, 7)); + ASSERT_NE(key_0, key_1); + + encryption = NextFromEncryption(encryption); + ASSERT_TRUE(encryption == NULL); + } + + if (options.has_video) { + CheckVideoBandwidth(options.video_bandwidth, + call_->sessions()[0]->local_description()); + CheckVideoRtcpMux(expected_video_rtcp_mux_, + call_->sessions()[0]->remote_description()); + content = parser_->NextContent(content); + const buzz::XmlElement* bandwidth = + parser_->BandwidthFromContent(content); + if (options.video_bandwidth == cricket::kAutoBandwidth) { + ASSERT_TRUE(bandwidth == NULL); + } else { + ASSERT_TRUE(bandwidth != NULL); + ASSERT_EQ("AS", bandwidth->Attr(buzz::QName("", "type"))); + ASSERT_EQ(talk_base::ToString(options.video_bandwidth / 1000), + bandwidth->BodyText()); + } + } + + if (options.data_channel_type == cricket::DCT_RTP) { + content = parser_->NextContent(content); + CheckRtpDataContent(content); + } + + if (options.data_channel_type == cricket::DCT_SCTP) { + content = parser_->NextContent(content); + CheckSctpDataContent(content); + } + + delete stanzas_[0]; + stanzas_.clear(); + } + + void TestHasAllSupportedAudioCodecs(buzz::XmlElement* e) { + ASSERT_TRUE(e != NULL); + + e = PayloadTypeFromContent(e); + ASSERT_TRUE(e != NULL); + cricket::AudioCodec codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(103, codec.id); + ASSERT_EQ("ISAC", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(104, codec.id); + ASSERT_EQ("ISAC", codec.name); + ASSERT_EQ(32000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(119, codec.id); + ASSERT_EQ("ISACLC", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(40000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(99, codec.id); + ASSERT_EQ("speex", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(22000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(97, codec.id); + ASSERT_EQ("IPCMWB", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(80000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(9, codec.id); + ASSERT_EQ("G722", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(64000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(102, codec.id); + ASSERT_EQ("iLBC", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(13300, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(98, codec.id); + ASSERT_EQ("speex", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(11000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(3, codec.id); + ASSERT_EQ("GSM", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(13000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(100, codec.id); + ASSERT_EQ("EG711U", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(64000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(101, codec.id); + ASSERT_EQ("EG711A", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(64000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(0, codec.id); + ASSERT_EQ("PCMU", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(64000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(8, codec.id); + ASSERT_EQ("PCMA", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(64000, codec.bitrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(126, codec.id); + ASSERT_EQ("CN", codec.name); + ASSERT_EQ(32000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(105, codec.id); + ASSERT_EQ("CN", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(13, codec.id); + ASSERT_EQ("CN", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(117, codec.id); + ASSERT_EQ("red", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(106, codec.id); + ASSERT_EQ("telephone-event", codec.name); + ASSERT_EQ(8000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e == NULL); + } + + void TestCodecsOfVideoInitiate(buzz::XmlElement* content) { + ASSERT_TRUE(content != NULL); + buzz::XmlElement* payload_type = PayloadTypeFromContent(content); + ASSERT_TRUE(payload_type != NULL); + cricket::AudioCodec codec = AudioCodecFromPayloadType(payload_type); + ASSERT_EQ(103, codec.id); + ASSERT_EQ("ISAC", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + content = parser_->NextContent(content); + ASSERT_TRUE(content != NULL); + payload_type = PayloadTypeFromContent(content); + ASSERT_TRUE(payload_type != NULL); + cricket::VideoCodec vcodec = + parser_->VideoCodecFromPayloadType(payload_type); + ASSERT_EQ(99, vcodec.id); + ASSERT_EQ("H264-SVC", vcodec.name); + ASSERT_EQ(320, vcodec.width); + ASSERT_EQ(200, vcodec.height); + ASSERT_EQ(30, vcodec.framerate); + } + + void TestHasAudioCodecsFromInitiateSomeUnsupported(buzz::XmlElement* e) { + ASSERT_TRUE(e != NULL); + e = PayloadTypeFromContent(e); + ASSERT_TRUE(e != NULL); + + cricket::AudioCodec codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(103, codec.id); + ASSERT_EQ("ISAC", codec.name); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(100, codec.id); + ASSERT_EQ("EG711U", codec.name); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(101, codec.id); + ASSERT_EQ("EG711A", codec.name); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(0, codec.id); + ASSERT_EQ("PCMU", codec.name); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(13, codec.id); + ASSERT_EQ("CN", codec.name); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e == NULL); + } + + void TestHasAudioCodecsFromInitiateDynamicAudioCodecs( + buzz::XmlElement* e) { + ASSERT_TRUE(e != NULL); + e = PayloadTypeFromContent(e); + ASSERT_TRUE(e != NULL); + + cricket::AudioCodec codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(123, codec.id); + ASSERT_EQ(16000, codec.clockrate); + ASSERT_EQ(1, codec.channels); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e == NULL); + } + + void TestHasDefaultAudioCodecs(buzz::XmlElement* e) { + ASSERT_TRUE(e != NULL); + e = PayloadTypeFromContent(e); + ASSERT_TRUE(e != NULL); + + cricket::AudioCodec codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(103, codec.id); + ASSERT_EQ("ISAC", codec.name); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(0, codec.id); + ASSERT_EQ("PCMU", codec.name); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e == NULL); + } + + void TestHasAudioCodecsFromInitiateStaticAudioCodecs( + buzz::XmlElement* e) { + ASSERT_TRUE(e != NULL); + e = PayloadTypeFromContent(e); + ASSERT_TRUE(e != NULL); + + cricket::AudioCodec codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(3, codec.id); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(0, codec.id); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e != NULL); + codec = AudioCodecFromPayloadType(e); + ASSERT_EQ(8, codec.id); + + e = NextFromPayloadType(e); + ASSERT_TRUE(e == NULL); + } + + void TestGingleInitiateWithUnsupportedCrypto( + const std::string &initiate_string, + buzz::XmlElement** element) { + *element = NULL; + + buzz::XmlElement* el = buzz::XmlElement::ForStr(initiate_string); + client_->session_manager()->OnIncomingMessage(el); + + ASSERT_EQ(cricket::Session::STATE_RECEIVEDINITIATE, + call_->sessions()[0]->state()); + delete stanzas_[0]; + stanzas_.clear(); + CheckBadCryptoFromIncomingInitiate(call_->sessions()[0]); + + call_->AcceptSession(call_->sessions()[0], cricket::CallOptions()); + delete stanzas_[0]; + stanzas_.clear(); + CheckNoCryptoForOutgoingAccept(call_->sessions()[0]); + + call_->Terminate(); + ASSERT_EQ(cricket::Session::STATE_SENTTERMINATE, + call_->sessions()[0]->state()); + delete stanzas_[0]; + stanzas_.clear(); + } + + void TestIncomingAcceptWithSsrcs( + const std::string& accept_string, + cricket::CallOptions& options) { + client_->CreateCall(); + ASSERT_TRUE(call_ != NULL); + + call_->InitiateSession(buzz::Jid("me@mydomain.com"), + buzz::Jid("me@mydomain.com"), options); + ASSERT_TRUE(call_->sessions()[0] != NULL); + ASSERT_EQ(cricket::Session::STATE_SENTINITIATE, + call_->sessions()[0]->state()); + ASSERT_EQ(1U, stanzas_.size()); + ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name()); + ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE)); + ASSERT_EQ(std::string(buzz::STR_SET), stanzas_[0]->Attr(buzz::QN_TYPE)); + buzz::XmlElement* action = ActionFromStanza(stanzas_[0]); + ASSERT_TRUE(action != NULL); + buzz::XmlElement* content = ContentFromAction(action); + ASSERT_TRUE(content != NULL); + if (initial_protocol_ == cricket::PROTOCOL_JINGLE) { + buzz::XmlElement* content_desc = + content->FirstNamed(cricket::QN_JINGLE_RTP_CONTENT); + ASSERT_TRUE(content_desc != NULL); + ASSERT_EQ("", content_desc->Attr(cricket::QN_SSRC)); + } + delete stanzas_[0]; + stanzas_.clear(); + + // We need to insert the session ID into the session accept message. + buzz::XmlElement* el = buzz::XmlElement::ForStr(accept_string); + const std::string sid = call_->sessions()[0]->id(); + if (initial_protocol_ == cricket::PROTOCOL_JINGLE) { + buzz::XmlElement* jingle = el->FirstNamed(cricket::QN_JINGLE); + jingle->SetAttr(cricket::QN_SID, sid); + } else { + buzz::XmlElement* session = el->FirstNamed(cricket::QN_GINGLE_SESSION); + session->SetAttr(cricket::QN_ID, sid); + } + + client_->session_manager()->OnIncomingMessage(el); + + ASSERT_EQ(cricket::Session::STATE_RECEIVEDACCEPT, + call_->sessions()[0]->state()); + ASSERT_EQ(1U, stanzas_.size()); + ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name()); + ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE)); + ASSERT_EQ(std::string(buzz::STR_RESULT), stanzas_[0]->Attr(buzz::QN_TYPE)); + delete stanzas_[0]; + stanzas_.clear(); + + CheckAudioSsrcForIncomingAccept(call_->sessions()[0]); + CheckVideoSsrcForIncomingAccept(call_->sessions()[0]); + if (options.data_channel_type == cricket::DCT_RTP) { + CheckDataSsrcForIncomingAccept(call_->sessions()[0]); + } + // TODO(pthatcher): Check kDataSid if DCT_SCTP. + } + + size_t ClearStanzas() { + size_t size = stanzas_.size(); + for (size_t i = 0; i < size; i++) { + delete stanzas_[i]; + } + stanzas_.clear(); + return size; + } + + buzz::XmlElement* SetJingleSid(buzz::XmlElement* stanza) { + buzz::XmlElement* jingle = + stanza->FirstNamed(cricket::QN_JINGLE); + jingle->SetAttr(cricket::QN_SID, call_->sessions()[0]->id()); + return stanza; + } + + void TestSendVideoStreamUpdate() { + cricket::CallOptions options = VideoCallOptions(); + options.is_muc = true; + + client_->CreateCall(); + call_->InitiateSession(buzz::Jid("me@mydomain.com"), + buzz::Jid("me@mydomain.com"), options); + ClearStanzas(); + + cricket::StreamParams stream; + stream.id = "test-stream"; + stream.ssrcs.push_back(1001); + talk_base::scoped_ptr expected_stream_add( + buzz::XmlElement::ForStr( + JingleOutboundStreamAdd( + call_->sessions()[0]->id(), + "video", stream.id, "1001"))); + talk_base::scoped_ptr expected_stream_remove( + buzz::XmlElement::ForStr( + JingleOutboundStreamRemove( + call_->sessions()[0]->id(), + "video", stream.id))); + + call_->SendVideoStreamUpdate(call_->sessions()[0], + call_->CreateVideoStreamUpdate(stream)); + ASSERT_EQ(1U, stanzas_.size()); + EXPECT_EQ(expected_stream_add->Str(), stanzas_[0]->Str()); + ClearStanzas(); + + stream.ssrcs.clear(); + call_->SendVideoStreamUpdate(call_->sessions()[0], + call_->CreateVideoStreamUpdate(stream)); + ASSERT_EQ(1U, stanzas_.size()); + EXPECT_EQ(expected_stream_remove->Str(), stanzas_[0]->Str()); + ClearStanzas(); + } + + void TestStreamsUpdateAndViewRequests() { + cricket::CallOptions options = VideoCallOptions(); + options.is_muc = true; + + client_->CreateCall(); + call_->InitiateSession(buzz::Jid("me@mydomain.com"), + buzz::Jid("me@mydomain.com"), options); + ASSERT_EQ(1U, ClearStanzas()); + ASSERT_EQ(0U, last_streams_added_.audio().size()); + ASSERT_EQ(0U, last_streams_added_.video().size()); + ASSERT_EQ(0U, last_streams_removed_.audio().size()); + ASSERT_EQ(0U, last_streams_removed_.video().size()); + + talk_base::scoped_ptr accept_stanza( + buzz::XmlElement::ForStr(kJingleAcceptWithSsrcs)); + SetJingleSid(accept_stanza.get()); + client_->session_manager()->OnIncomingMessage(accept_stanza.get()); + ASSERT_EQ(cricket::Session::STATE_RECEIVEDACCEPT, + call_->sessions()[0]->state()); + ASSERT_EQ(1U, stanzas_.size()); + ASSERT_EQ(std::string(buzz::STR_RESULT), stanzas_[0]->Attr(buzz::QN_TYPE)); + ClearStanzas(); + // Need to clear the added streams, because they are populated when + // receiving an accept message now. + last_streams_added_.mutable_video()->clear(); + last_streams_added_.mutable_audio()->clear(); + + call_->sessions()[0]->SetState(cricket::Session::STATE_INPROGRESS); + + talk_base::scoped_ptr streams_stanza( + buzz::XmlElement::ForStr( + JingleStreamAdd("video", "Bob", "video1", "ABC"))); + SetJingleSid(streams_stanza.get()); + client_->session_manager()->OnIncomingMessage(streams_stanza.get()); + // First one is ignored because of bad syntax. + ASSERT_EQ(1U, stanzas_.size()); + // TODO(pthatcher): Figure out how to make this an ERROR rather than RESULT. + ASSERT_EQ(std::string(buzz::STR_ERROR), stanzas_[0]->Attr(buzz::QN_TYPE)); + ClearStanzas(); + ASSERT_EQ(0U, last_streams_added_.audio().size()); + ASSERT_EQ(0U, last_streams_added_.video().size()); + ASSERT_EQ(0U, last_streams_removed_.audio().size()); + ASSERT_EQ(0U, last_streams_removed_.video().size()); + + streams_stanza.reset(buzz::XmlElement::ForStr( + JingleStreamAdd("audio", "Bob", "audio1", "1234"))); + SetJingleSid(streams_stanza.get()); + client_->session_manager()->OnIncomingMessage(streams_stanza.get()); + ASSERT_EQ(1U, last_streams_added_.audio().size()); + ASSERT_EQ("Bob", last_streams_added_.audio()[0].groupid); + ASSERT_EQ(1U, last_streams_added_.audio()[0].ssrcs.size()); + ASSERT_EQ(1234U, last_streams_added_.audio()[0].first_ssrc()); + + // Ignores adds without ssrcs. + streams_stanza.reset(buzz::XmlElement::ForStr( + JingleStreamAddWithoutSsrc("audio", "Bob", "audioX"))); + SetJingleSid(streams_stanza.get()); + client_->session_manager()->OnIncomingMessage(streams_stanza.get()); + ASSERT_EQ(1U, last_streams_added_.audio().size()); + ASSERT_EQ(1234U, last_streams_added_.audio()[0].first_ssrc()); + + // Ignores stream updates with unknown content names. (Don't terminate). + streams_stanza.reset(buzz::XmlElement::ForStr( + JingleStreamAddWithoutSsrc("foo", "Bob", "foo"))); + SetJingleSid(streams_stanza.get()); + client_->session_manager()->OnIncomingMessage(streams_stanza.get()); + + streams_stanza.reset(buzz::XmlElement::ForStr( + JingleStreamAdd("audio", "Joe", "audio1", "2468"))); + SetJingleSid(streams_stanza.get()); + client_->session_manager()->OnIncomingMessage(streams_stanza.get()); + ASSERT_EQ(1U, last_streams_added_.audio().size()); + ASSERT_EQ("Joe", last_streams_added_.audio()[0].groupid); + ASSERT_EQ(1U, last_streams_added_.audio()[0].ssrcs.size()); + ASSERT_EQ(2468U, last_streams_added_.audio()[0].first_ssrc()); + + streams_stanza.reset(buzz::XmlElement::ForStr( + JingleStreamAdd("video", "Bob", "video1", "5678"))); + SetJingleSid(streams_stanza.get()); + client_->session_manager()->OnIncomingMessage(streams_stanza.get()); + ASSERT_EQ(1U, last_streams_added_.video().size()); + ASSERT_EQ("Bob", last_streams_added_.video()[0].groupid); + ASSERT_EQ(1U, last_streams_added_.video()[0].ssrcs.size()); + ASSERT_EQ(5678U, last_streams_added_.video()[0].first_ssrc()); + + // We're testing that a "duplicate" is effectively ignored. + last_streams_added_.mutable_video()->clear(); + last_streams_removed_.mutable_video()->clear(); + streams_stanza.reset(buzz::XmlElement::ForStr( + JingleStreamAdd("video", "Bob", "video1", "5678"))); + SetJingleSid(streams_stanza.get()); + client_->session_manager()->OnIncomingMessage(streams_stanza.get()); + ASSERT_EQ(0U, last_streams_added_.video().size()); + ASSERT_EQ(0U, last_streams_removed_.video().size()); + + streams_stanza.reset(buzz::XmlElement::ForStr( + JingleStreamAdd("video", "Bob", "video2", "5679"))); + SetJingleSid(streams_stanza.get()); + client_->session_manager()->OnIncomingMessage(streams_stanza.get()); + ASSERT_EQ(1U, last_streams_added_.video().size()); + ASSERT_EQ("Bob", last_streams_added_.video()[0].groupid); + ASSERT_EQ(1U, last_streams_added_.video()[0].ssrcs.size()); + ASSERT_EQ(5679U, last_streams_added_.video()[0].first_ssrc()); + + cricket::FakeVoiceMediaChannel* voice_channel = fme_->GetVoiceChannel(0); + ASSERT_TRUE(voice_channel != NULL); + ASSERT_TRUE(voice_channel->HasRecvStream(1234U)); + ASSERT_TRUE(voice_channel->HasRecvStream(2468U)); + cricket::FakeVideoMediaChannel* video_channel = fme_->GetVideoChannel(0); + ASSERT_TRUE(video_channel != NULL); + ASSERT_TRUE(video_channel->HasRecvStream(5678U)); + ClearStanzas(); + + cricket::ViewRequest viewRequest; + cricket::StaticVideoView staticVideoView( + cricket::StreamSelector(5678U), 640, 480, 30); + viewRequest.static_video_views.push_back(staticVideoView); + talk_base::scoped_ptr expected_view_elem( + buzz::XmlElement::ForStr(JingleView("5678", "640", "480", "30"))); + SetJingleSid(expected_view_elem.get()); + + ASSERT_TRUE( + call_->SendViewRequest(call_->sessions()[0], viewRequest)); + ASSERT_EQ(1U, stanzas_.size()); + ASSERT_EQ(expected_view_elem->Str(), stanzas_[0]->Str()); + ClearStanzas(); + + streams_stanza.reset(buzz::XmlElement::ForStr( + JingleStreamRemove("audio", "Bob", "audio1"))); + SetJingleSid(streams_stanza.get()); + client_->session_manager()->OnIncomingMessage(streams_stanza.get()); + ASSERT_EQ(1U, last_streams_removed_.audio().size()); + ASSERT_EQ(1U, last_streams_removed_.audio()[0].ssrcs.size()); + EXPECT_EQ(1234U, last_streams_removed_.audio()[0].first_ssrc()); + + streams_stanza.reset(buzz::XmlElement::ForStr( + JingleStreamRemove("video", "Bob", "video1"))); + SetJingleSid(streams_stanza.get()); + client_->session_manager()->OnIncomingMessage(streams_stanza.get()); + ASSERT_EQ(1U, last_streams_removed_.video().size()); + ASSERT_EQ(1U, last_streams_removed_.video()[0].ssrcs.size()); + EXPECT_EQ(5678U, last_streams_removed_.video()[0].first_ssrc()); + + streams_stanza.reset(buzz::XmlElement::ForStr( + JingleStreamRemove("video", "Bob", "video2"))); + SetJingleSid(streams_stanza.get()); + client_->session_manager()->OnIncomingMessage(streams_stanza.get()); + ASSERT_EQ(1U, last_streams_removed_.video().size()); + ASSERT_EQ(1U, last_streams_removed_.video()[0].ssrcs.size()); + EXPECT_EQ(5679U, last_streams_removed_.video()[0].first_ssrc()); + + // Duplicate removal: should be ignored. + last_streams_removed_.mutable_audio()->clear(); + streams_stanza.reset(buzz::XmlElement::ForStr( + JingleStreamRemove("audio", "Bob", "audio1"))); + SetJingleSid(streams_stanza.get()); + client_->session_manager()->OnIncomingMessage(streams_stanza.get()); + ASSERT_EQ(0U, last_streams_removed_.audio().size()); + + // Duplicate removal: should be ignored. + last_streams_removed_.mutable_video()->clear(); + streams_stanza.reset(buzz::XmlElement::ForStr( + JingleStreamRemove("video", "Bob", "video1"))); + SetJingleSid(streams_stanza.get()); + client_->session_manager()->OnIncomingMessage(streams_stanza.get()); + ASSERT_EQ(0U, last_streams_removed_.video().size()); + + voice_channel = fme_->GetVoiceChannel(0); + ASSERT_TRUE(voice_channel != NULL); + ASSERT_FALSE(voice_channel->HasRecvStream(1234U)); + ASSERT_TRUE(voice_channel->HasRecvStream(2468U)); + video_channel = fme_->GetVideoChannel(0); + ASSERT_TRUE(video_channel != NULL); + ASSERT_FALSE(video_channel->HasRecvStream(5678U)); + + // Fails because ssrc is now invalid. + ASSERT_FALSE( + call_->SendViewRequest(call_->sessions()[0], viewRequest)); + + ClearStanzas(); + } + + void MakeSignalingSecure(cricket::SecureMediaPolicy secure) { + client_->set_secure(secure); + } + + void ExpectCrypto(cricket::SecureMediaPolicy secure) { + MakeSignalingSecure(secure); + expect_incoming_crypto_ = true; +#ifdef HAVE_SRTP + expect_outgoing_crypto_ = true; +#endif + } + + void ExpectVideoBandwidth(int bandwidth) { + expected_video_bandwidth_ = bandwidth; + } + + void ExpectVideoRtcpMux(bool rtcp_mux) { + expected_video_rtcp_mux_ = rtcp_mux; + } + + private: + void OnSendStanza(cricket::SessionManager* manager, + const buzz::XmlElement* stanza) { + LOG(LS_INFO) << stanza->Str(); + stanzas_.push_back(new buzz::XmlElement(*stanza)); + } + + void OnSessionCreate(cricket::Session* session, bool initiate) { + session->set_current_protocol(initial_protocol_); + } + + void OnCallCreate(cricket::Call *call) { + call_ = call; + call->SignalMediaStreamsUpdate.connect( + this, &MediaSessionClientTest::OnMediaStreamsUpdate); + } + + void OnCallDestroy(cricket::Call *call) { + call_ = NULL; + } + + void OnMediaStreamsUpdate(cricket::Call *call, + cricket::Session *session, + const cricket::MediaStreams& added, + const cricket::MediaStreams& removed) { + last_streams_added_.CopyFrom(added); + last_streams_removed_.CopyFrom(removed); + } + + talk_base::NetworkManager* nm_; + cricket::PortAllocator* pa_; + cricket::SessionManager* sm_; + cricket::FakeMediaEngine* fme_; + cricket::FakeDataEngine* fdme_; + cricket::MediaSessionClient* client_; + + cricket::Call* call_; + std::vector stanzas_; + MediaSessionTestParser* parser_; + cricket::SignalingProtocol initial_protocol_; + bool expect_incoming_crypto_; + bool expect_outgoing_crypto_; + int expected_video_bandwidth_; + bool expected_video_rtcp_mux_; + cricket::MediaStreams last_streams_added_; + cricket::MediaStreams last_streams_removed_; +}; + +MediaSessionClientTest* GingleTest() { + return new MediaSessionClientTest(new GingleSessionTestParser(), + cricket::PROTOCOL_GINGLE); +} + +MediaSessionClientTest* JingleTest() { + return new MediaSessionClientTest(new JingleSessionTestParser(), + cricket::PROTOCOL_JINGLE); +} + +TEST(MediaSessionTest, JingleGoodVideoInitiate) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->TestGoodIncomingInitiate( + kJingleVideoInitiate, VideoCallOptions(), elem.use()); + test->TestCodecsOfVideoInitiate(elem.get()); +} + +TEST(MediaSessionTest, JingleGoodVideoInitiateWithBandwidth) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->ExpectVideoBandwidth(42000); + test->TestGoodIncomingInitiate( + kJingleVideoInitiateWithBandwidth, VideoCallOptions(), elem.use()); +} + +TEST(MediaSessionTest, JingleGoodVideoInitiateWithRtcpMux) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->ExpectVideoRtcpMux(true); + test->TestGoodIncomingInitiate( + kJingleVideoInitiateWithRtcpMux, VideoCallOptions(), elem.use()); +} + +TEST(MediaSessionTest, JingleGoodVideoInitiateWithRtpData) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + cricket::CallOptions options = VideoCallOptions(); + options.data_channel_type = cricket::DCT_RTP; + test->TestGoodIncomingInitiate( + AddEncryption(kJingleVideoInitiateWithRtpData, kJingleCryptoOffer), + options, + elem.use()); +} + +TEST(MediaSessionTest, JingleGoodVideoInitiateWithSctpData) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + cricket::CallOptions options = VideoCallOptions(); + options.data_channel_type = cricket::DCT_SCTP; + test->TestGoodIncomingInitiate(kJingleVideoInitiateWithSctpData, + options, + elem.use()); +} + +TEST(MediaSessionTest, JingleRejectAudio) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + cricket::CallOptions options = VideoCallOptions(); + options.has_audio = false; + options.data_channel_type = cricket::DCT_RTP; + test->TestRejectOffer(kJingleVideoInitiateWithRtpData, options, elem.use()); +} + +TEST(MediaSessionTest, JingleRejectVideo) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + cricket::CallOptions options = AudioCallOptions(); + options.data_channel_type = cricket::DCT_RTP; + test->TestRejectOffer(kJingleVideoInitiateWithRtpData, options, elem.use()); +} + +TEST(MediaSessionTest, JingleRejectData) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->TestRejectOffer( + kJingleVideoInitiateWithRtpData, VideoCallOptions(), elem.use()); +} + +TEST(MediaSessionTest, JingleRejectVideoAndData) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->TestRejectOffer( + kJingleVideoInitiateWithRtpData, AudioCallOptions(), elem.use()); +} + +TEST(MediaSessionTest, JingleGoodInitiateAllSupportedAudioCodecs) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->TestGoodIncomingInitiate( + kJingleInitiate, AudioCallOptions(), elem.use()); + test->TestHasAllSupportedAudioCodecs(elem.get()); +} + +TEST(MediaSessionTest, JingleGoodInitiateDifferentPreferenceAudioCodecs) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->TestGoodIncomingInitiate( + kJingleInitiateDifferentPreference, AudioCallOptions(), elem.use()); + test->TestHasAllSupportedAudioCodecs(elem.get()); +} + +TEST(MediaSessionTest, JingleGoodInitiateSomeUnsupportedAudioCodecs) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->TestGoodIncomingInitiate( + kJingleInitiateSomeUnsupported, AudioCallOptions(), elem.use()); + test->TestHasAudioCodecsFromInitiateSomeUnsupported(elem.get()); +} + +TEST(MediaSessionTest, JingleGoodInitiateDynamicAudioCodecs) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->TestGoodIncomingInitiate( + kJingleInitiateDynamicAudioCodecs, AudioCallOptions(), elem.use()); + test->TestHasAudioCodecsFromInitiateDynamicAudioCodecs(elem.get()); +} + +TEST(MediaSessionTest, JingleGoodInitiateStaticAudioCodecs) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->TestGoodIncomingInitiate( + kJingleInitiateStaticAudioCodecs, AudioCallOptions(), elem.use()); + test->TestHasAudioCodecsFromInitiateStaticAudioCodecs(elem.get()); +} + +TEST(MediaSessionTest, JingleBadInitiateNoAudioCodecs) { + talk_base::scoped_ptr test(JingleTest()); + test->TestBadIncomingInitiate(kJingleInitiateNoAudioCodecs); +} + +TEST(MediaSessionTest, JingleBadInitiateNoSupportedAudioCodecs) { + talk_base::scoped_ptr test(JingleTest()); + test->TestBadIncomingInitiate(kJingleInitiateNoSupportedAudioCodecs); +} + +TEST(MediaSessionTest, JingleBadInitiateWrongClockrates) { + talk_base::scoped_ptr test(JingleTest()); + test->TestBadIncomingInitiate(kJingleInitiateWrongClockrates); +} + +TEST(MediaSessionTest, JingleBadInitiateWrongChannels) { + talk_base::scoped_ptr test(JingleTest()); + test->TestBadIncomingInitiate(kJingleInitiateWrongChannels); +} + +TEST(MediaSessionTest, JingleBadInitiateNoPayloadTypes) { + talk_base::scoped_ptr test(JingleTest()); + test->TestBadIncomingInitiate(kJingleInitiateNoPayloadTypes); +} + +TEST(MediaSessionTest, JingleBadInitiateDynamicWithoutNames) { + talk_base::scoped_ptr test(JingleTest()); + test->TestBadIncomingInitiate(kJingleInitiateDynamicWithoutNames); +} + +TEST(MediaSessionTest, JingleGoodOutgoingInitiate) { + talk_base::scoped_ptr test(JingleTest()); + test->TestGoodOutgoingInitiate(AudioCallOptions()); +} + +TEST(MediaSessionTest, JingleGoodOutgoingInitiateWithBandwidth) { + talk_base::scoped_ptr test(JingleTest()); + cricket::CallOptions options = VideoCallOptions(); + options.video_bandwidth = 42000; + test->TestGoodOutgoingInitiate(options); +} + +TEST(MediaSessionTest, JingleGoodOutgoingInitiateWithRtcpMux) { + talk_base::scoped_ptr test(JingleTest()); + cricket::CallOptions options = VideoCallOptions(); + options.rtcp_mux_enabled = true; + test->TestGoodOutgoingInitiate(options); +} + +TEST(MediaSessionTest, JingleGoodOutgoingInitiateWithRtpData) { + talk_base::scoped_ptr test(JingleTest()); + cricket::CallOptions options; + options.data_channel_type = cricket::DCT_RTP; + test->ExpectCrypto(cricket::SEC_ENABLED); + test->TestGoodOutgoingInitiate(options); +} + +TEST(MediaSessionTest, JingleGoodOutgoingInitiateWithSctpData) { + talk_base::scoped_ptr test(JingleTest()); + cricket::CallOptions options; + options.data_channel_type = cricket::DCT_SCTP; + test->TestGoodOutgoingInitiate(options); +} + +// Crypto related tests. + +// Offer has crypto but the session is not secured, just ignore it. +TEST(MediaSessionTest, JingleInitiateWithCryptoIsIgnoredWhenNotSecured) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->TestGoodIncomingInitiate( + AddEncryption(kJingleVideoInitiate, kJingleCryptoOffer), + VideoCallOptions(), + elem.use()); +} + +// Offer has crypto required but the session is not secure, fail. +TEST(MediaSessionTest, JingleInitiateWithCryptoRequiredWhenNotSecured) { + talk_base::scoped_ptr test(JingleTest()); + test->TestBadIncomingInitiate(AddEncryption(kJingleVideoInitiate, + kJingleRequiredCryptoOffer)); +} + +// Offer has no crypto but the session is secure required, fail. +TEST(MediaSessionTest, JingleInitiateWithNoCryptoFailsWhenSecureRequired) { + talk_base::scoped_ptr test(JingleTest()); + test->ExpectCrypto(cricket::SEC_REQUIRED); + test->TestBadIncomingInitiate(kJingleInitiate); +} + +// Offer has crypto and session is secure, expect crypto in the answer. +TEST(MediaSessionTest, JingleInitiateWithCryptoWhenSecureEnabled) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->ExpectCrypto(cricket::SEC_ENABLED); + test->TestGoodIncomingInitiate( + AddEncryption(kJingleVideoInitiate, kJingleCryptoOffer), + VideoCallOptions(), + elem.use()); +} + +// Offer has crypto and session is secure required, expect crypto in +// the answer. +TEST(MediaSessionTest, JingleInitiateWithCryptoWhenSecureRequired) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->ExpectCrypto(cricket::SEC_REQUIRED); + test->TestGoodIncomingInitiate( + AddEncryption(kJingleVideoInitiate, kJingleCryptoOffer), + VideoCallOptions(), + elem.use()); +} + +// Offer has unsupported crypto and session is secure, no crypto in +// the answer. +TEST(MediaSessionTest, JingleInitiateWithUnsupportedCrypto) { + talk_base::scoped_ptr test(JingleTest()); + talk_base::scoped_ptr elem; + test->MakeSignalingSecure(cricket::SEC_ENABLED); + test->TestGoodIncomingInitiate( + AddEncryption(kJingleInitiate, kJingleUnsupportedCryptoOffer), + VideoCallOptions(), + elem.use()); +} + +// Offer has unsupported REQUIRED crypto and session is not secure, fail. +TEST(MediaSessionTest, JingleInitiateWithRequiredUnsupportedCrypto) { + talk_base::scoped_ptr test(JingleTest()); + test->TestBadIncomingInitiate( + AddEncryption(kJingleInitiate, kJingleRequiredUnsupportedCryptoOffer)); +} + +// Offer has unsupported REQUIRED crypto and session is secure, fail. +TEST(MediaSessionTest, JingleInitiateWithRequiredUnsupportedCryptoWhenSecure) { + talk_base::scoped_ptr test(JingleTest()); + test->MakeSignalingSecure(cricket::SEC_ENABLED); + test->TestBadIncomingInitiate( + AddEncryption(kJingleInitiate, kJingleRequiredUnsupportedCryptoOffer)); +} + +// Offer has unsupported REQUIRED crypto and session is required secure, fail. +TEST(MediaSessionTest, + JingleInitiateWithRequiredUnsupportedCryptoWhenSecureRequired) { + talk_base::scoped_ptr test(JingleTest()); + test->MakeSignalingSecure(cricket::SEC_REQUIRED); + test->TestBadIncomingInitiate( + AddEncryption(kJingleInitiate, kJingleRequiredUnsupportedCryptoOffer)); +} + + +TEST(MediaSessionTest, JingleGoodOutgoingInitiateWithCrypto) { + talk_base::scoped_ptr test(JingleTest()); + test->ExpectCrypto(cricket::SEC_ENABLED); + test->TestGoodOutgoingInitiate(AudioCallOptions()); +} + +TEST(MediaSessionTest, JingleGoodOutgoingInitiateWithCryptoRequired) { + talk_base::scoped_ptr test(JingleTest()); + test->ExpectCrypto(cricket::SEC_REQUIRED); + test->TestGoodOutgoingInitiate(AudioCallOptions()); +} + +TEST(MediaSessionTest, JingleIncomingAcceptWithSsrcs) { + talk_base::scoped_ptr test(JingleTest()); + cricket::CallOptions options = VideoCallOptions(); + options.is_muc = true; + test->TestIncomingAcceptWithSsrcs(kJingleAcceptWithSsrcs, options); +} + +TEST(MediaSessionTest, JingleIncomingAcceptWithRtpDataSsrcs) { + talk_base::scoped_ptr test(JingleTest()); + cricket::CallOptions options = VideoCallOptions(); + options.is_muc = true; + options.data_channel_type = cricket::DCT_RTP; + test->TestIncomingAcceptWithSsrcs(kJingleAcceptWithRtpDataSsrcs, options); +} + +TEST(MediaSessionTest, JingleIncomingAcceptWithSctpData) { + talk_base::scoped_ptr test(JingleTest()); + cricket::CallOptions options = VideoCallOptions(); + options.is_muc = true; + options.data_channel_type = cricket::DCT_SCTP; + test->TestIncomingAcceptWithSsrcs(kJingleAcceptWithSctpData, options); +} + +TEST(MediaSessionTest, JingleStreamsUpdateAndView) { + talk_base::scoped_ptr test(JingleTest()); + test->TestStreamsUpdateAndViewRequests(); +} + +TEST(MediaSessionTest, JingleSendVideoStreamUpdate) { + talk_base::scoped_ptr test(JingleTest()); + test->TestSendVideoStreamUpdate(); +} + +// Gingle tests + +TEST(MediaSessionTest, GingleGoodVideoInitiate) { + talk_base::scoped_ptr elem; + talk_base::scoped_ptr test(GingleTest()); + test->TestGoodIncomingInitiate( + kGingleVideoInitiate, VideoCallOptions(), elem.use()); + test->TestCodecsOfVideoInitiate(elem.get()); +} + +TEST(MediaSessionTest, GingleGoodVideoInitiateWithBandwidth) { + talk_base::scoped_ptr elem; + talk_base::scoped_ptr test(GingleTest()); + test->ExpectVideoBandwidth(42000); + test->TestGoodIncomingInitiate( + kGingleVideoInitiateWithBandwidth, VideoCallOptions(), elem.use()); +} + +TEST(MediaSessionTest, GingleGoodInitiateAllSupportedAudioCodecs) { + talk_base::scoped_ptr elem; + talk_base::scoped_ptr test(GingleTest()); + test->TestGoodIncomingInitiate( + kGingleInitiate, AudioCallOptions(), elem.use()); + test->TestHasAllSupportedAudioCodecs(elem.get()); +} + +TEST(MediaSessionTest, GingleGoodInitiateAllSupportedAudioCodecsWithCrypto) { + talk_base::scoped_ptr elem; + talk_base::scoped_ptr test(GingleTest()); + test->ExpectCrypto(cricket::SEC_ENABLED); + test->TestGoodIncomingInitiate( + AddEncryption(kGingleInitiate, kGingleCryptoOffer), + AudioCallOptions(), + elem.use()); + test->TestHasAllSupportedAudioCodecs(elem.get()); +} + +TEST(MediaSessionTest, GingleGoodInitiateDifferentPreferenceAudioCodecs) { + talk_base::scoped_ptr elem; + talk_base::scoped_ptr test(GingleTest()); + test->TestGoodIncomingInitiate( + kGingleInitiateDifferentPreference, AudioCallOptions(), elem.use()); + test->TestHasAllSupportedAudioCodecs(elem.get()); +} + +TEST(MediaSessionTest, GingleGoodInitiateSomeUnsupportedAudioCodecs) { + talk_base::scoped_ptr elem; + talk_base::scoped_ptr test(GingleTest()); + test->TestGoodIncomingInitiate( + kGingleInitiateSomeUnsupported, AudioCallOptions(), elem.use()); + test->TestHasAudioCodecsFromInitiateSomeUnsupported(elem.get()); +} + +TEST(MediaSessionTest, GingleGoodInitiateDynamicAudioCodecs) { + talk_base::scoped_ptr elem; + talk_base::scoped_ptr test(GingleTest()); + test->TestGoodIncomingInitiate( + kGingleInitiateDynamicAudioCodecs, AudioCallOptions(), elem.use()); + test->TestHasAudioCodecsFromInitiateDynamicAudioCodecs(elem.get()); +} + +TEST(MediaSessionTest, GingleGoodInitiateStaticAudioCodecs) { + talk_base::scoped_ptr elem; + talk_base::scoped_ptr test(GingleTest()); + test->TestGoodIncomingInitiate( + kGingleInitiateStaticAudioCodecs, AudioCallOptions(), elem.use()); + test->TestHasAudioCodecsFromInitiateStaticAudioCodecs(elem.get()); +} + +TEST(MediaSessionTest, GingleGoodInitiateNoAudioCodecs) { + talk_base::scoped_ptr elem; + talk_base::scoped_ptr test(GingleTest()); + test->TestGoodIncomingInitiate( + kGingleInitiateNoAudioCodecs, AudioCallOptions(), elem.use()); + test->TestHasDefaultAudioCodecs(elem.get()); +} + +TEST(MediaSessionTest, GingleBadInitiateNoSupportedAudioCodecs) { + talk_base::scoped_ptr test(GingleTest()); + test->TestBadIncomingInitiate(kGingleInitiateNoSupportedAudioCodecs); +} + +TEST(MediaSessionTest, GingleBadInitiateWrongClockrates) { + talk_base::scoped_ptr test(GingleTest()); + test->TestBadIncomingInitiate(kGingleInitiateWrongClockrates); +} + +TEST(MediaSessionTest, GingleBadInitiateWrongChannels) { + talk_base::scoped_ptr test(GingleTest()); + test->TestBadIncomingInitiate(kGingleInitiateWrongChannels); +} + + +TEST(MediaSessionTest, GingleBadInitiateNoPayloadTypes) { + talk_base::scoped_ptr test(GingleTest()); + test->TestBadIncomingInitiate(kGingleInitiateNoPayloadTypes); +} + +TEST(MediaSessionTest, GingleBadInitiateDynamicWithoutNames) { + talk_base::scoped_ptr test(GingleTest()); + test->TestBadIncomingInitiate(kGingleInitiateDynamicWithoutNames); +} + +TEST(MediaSessionTest, GingleGoodOutgoingInitiate) { + talk_base::scoped_ptr test(GingleTest()); + test->TestGoodOutgoingInitiate(AudioCallOptions()); +} + +TEST(MediaSessionTest, GingleGoodOutgoingInitiateWithBandwidth) { + talk_base::scoped_ptr test(GingleTest()); + cricket::CallOptions options = VideoCallOptions(); + options.video_bandwidth = 42000; + test->TestGoodOutgoingInitiate(options); +} + +// Crypto related tests. + +// Offer has crypto but the session is not secured, just ignore it. +TEST(MediaSessionTest, GingleInitiateWithCryptoIsIgnoredWhenNotSecured) { + talk_base::scoped_ptr elem; + talk_base::scoped_ptr test(GingleTest()); + test->TestGoodIncomingInitiate( + AddEncryption(kGingleInitiate, kGingleCryptoOffer), + VideoCallOptions(), + elem.use()); +} + +// Offer has crypto required but the session is not secure, fail. +TEST(MediaSessionTest, GingleInitiateWithCryptoRequiredWhenNotSecured) { + talk_base::scoped_ptr test(GingleTest()); + test->TestBadIncomingInitiate(AddEncryption(kGingleInitiate, + kGingleRequiredCryptoOffer)); +} + +// Offer has no crypto but the session is secure required, fail. +TEST(MediaSessionTest, GingleInitiateWithNoCryptoFailsWhenSecureRequired) { + talk_base::scoped_ptr test(GingleTest()); + test->ExpectCrypto(cricket::SEC_REQUIRED); + test->TestBadIncomingInitiate(kGingleInitiate); +} + +// Offer has crypto and session is secure, expect crypto in the answer. +TEST(MediaSessionTest, GingleInitiateWithCryptoWhenSecureEnabled) { + talk_base::scoped_ptr elem; + talk_base::scoped_ptr test(GingleTest()); + test->ExpectCrypto(cricket::SEC_ENABLED); + test->TestGoodIncomingInitiate( + AddEncryption(kGingleInitiate, kGingleCryptoOffer), + VideoCallOptions(), + elem.use()); +} + +// Offer has crypto and session is secure required, expect crypto in +// the answer. +TEST(MediaSessionTest, GingleInitiateWithCryptoWhenSecureRequired) { + talk_base::scoped_ptr elem; + talk_base::scoped_ptr test(GingleTest()); + test->ExpectCrypto(cricket::SEC_REQUIRED); + test->TestGoodIncomingInitiate( + AddEncryption(kGingleInitiate, kGingleCryptoOffer), + VideoCallOptions(), + elem.use()); +} + +// Offer has unsupported crypto and session is secure, no crypto in +// the answer. +TEST(MediaSessionTest, GingleInitiateWithUnsupportedCrypto) { + talk_base::scoped_ptr elem; + talk_base::scoped_ptr test(GingleTest()); + test->MakeSignalingSecure(cricket::SEC_ENABLED); + test->TestGoodIncomingInitiate( + AddEncryption(kGingleInitiate, kGingleUnsupportedCryptoOffer), + VideoCallOptions(), + elem.use()); +} + +// Offer has unsupported REQUIRED crypto and session is not secure, fail. +TEST(MediaSessionTest, GingleInitiateWithRequiredUnsupportedCrypto) { + talk_base::scoped_ptr test(GingleTest()); + test->TestBadIncomingInitiate( + AddEncryption(kGingleInitiate, kGingleRequiredUnsupportedCryptoOffer)); +} + +// Offer has unsupported REQUIRED crypto and session is secure, fail. +TEST(MediaSessionTest, GingleInitiateWithRequiredUnsupportedCryptoWhenSecure) { + talk_base::scoped_ptr test(GingleTest()); + test->MakeSignalingSecure(cricket::SEC_ENABLED); + test->TestBadIncomingInitiate( + AddEncryption(kGingleInitiate, kGingleRequiredUnsupportedCryptoOffer)); +} + +// Offer has unsupported REQUIRED crypto and session is required secure, fail. +TEST(MediaSessionTest, + GingleInitiateWithRequiredUnsupportedCryptoWhenSecureRequired) { + talk_base::scoped_ptr test(GingleTest()); + test->MakeSignalingSecure(cricket::SEC_REQUIRED); + test->TestBadIncomingInitiate( + AddEncryption(kGingleInitiate, kGingleRequiredUnsupportedCryptoOffer)); +} + +TEST(MediaSessionTest, GingleGoodOutgoingInitiateWithCrypto) { + talk_base::scoped_ptr test(GingleTest()); + test->ExpectCrypto(cricket::SEC_ENABLED); + test->TestGoodOutgoingInitiate(AudioCallOptions()); +} + +TEST(MediaSessionTest, GingleGoodOutgoingInitiateWithCryptoRequired) { + talk_base::scoped_ptr test(GingleTest()); + test->ExpectCrypto(cricket::SEC_REQUIRED); + test->TestGoodOutgoingInitiate(AudioCallOptions()); +} + +TEST(MediaSessionTest, GingleIncomingAcceptWithSsrcs) { + talk_base::scoped_ptr test(GingleTest()); + cricket::CallOptions options = VideoCallOptions(); + options.is_muc = true; + test->TestIncomingAcceptWithSsrcs(kGingleAcceptWithSsrcs, options); +} + +TEST(MediaSessionTest, GingleGoodOutgoingInitiateWithRtpData) { + talk_base::scoped_ptr test(GingleTest()); + cricket::CallOptions options; + options.data_channel_type = cricket::DCT_RTP; + test->ExpectCrypto(cricket::SEC_ENABLED); + test->TestGoodOutgoingInitiate(options); +} diff --git a/talk/session/media/mediasink.h b/talk/session/media/mediasink.h new file mode 100644 index 000000000..fb0e06be8 --- /dev/null +++ b/talk/session/media/mediasink.h @@ -0,0 +1,48 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_SESSION_MEDIA_MEDIASINK_H_ +#define TALK_SESSION_MEDIA_MEDIASINK_H_ + +namespace cricket { + +// MediaSinkInterface is a sink to handle RTP and RTCP packets that are sent or +// received by a channel. +class MediaSinkInterface { + public: + virtual ~MediaSinkInterface() {} + + virtual void SetMaxSize(size_t size) = 0; + virtual bool Enable(bool enable) = 0; + virtual bool IsEnabled() const = 0; + virtual void OnPacket(const void* data, size_t size, bool rtcp) = 0; + virtual void set_packet_filter(int filter) = 0; +}; + +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_MEDIASINK_H_ diff --git a/talk/session/media/rtcpmuxfilter.cc b/talk/session/media/rtcpmuxfilter.cc new file mode 100644 index 000000000..7091952fd --- /dev/null +++ b/talk/session/media/rtcpmuxfilter.cc @@ -0,0 +1,132 @@ +/* + * libjingle + * Copyright 2004 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/media/rtcpmuxfilter.h" + +#include "talk/base/logging.h" + +namespace cricket { + +RtcpMuxFilter::RtcpMuxFilter() : state_(ST_INIT), offer_enable_(false) { +} + +bool RtcpMuxFilter::IsActive() const { + return state_ == ST_SENTPRANSWER || + state_ == ST_RECEIVEDPRANSWER || + state_ == ST_ACTIVE; +} + +bool RtcpMuxFilter::SetOffer(bool offer_enable, ContentSource src) { + if (!ExpectOffer(offer_enable, src)) { + LOG(LS_ERROR) << "Invalid state for change of RTCP mux offer"; + return false; + } + + offer_enable_ = offer_enable; + state_ = (src == CS_LOCAL) ? ST_SENTOFFER : ST_RECEIVEDOFFER; + return true; +} + +bool RtcpMuxFilter::SetProvisionalAnswer(bool answer_enable, + ContentSource src) { + if (!ExpectAnswer(src)) { + LOG(LS_ERROR) << "Invalid state for RTCP mux provisional answer"; + return false; + } + + if (offer_enable_) { + if (answer_enable) { + if (src == CS_REMOTE) + state_ = ST_RECEIVEDPRANSWER; + else // CS_LOCAL + state_ = ST_SENTPRANSWER; + } else { + // The provisional answer doesn't want to use RTCP mux. + // Go back to the original state after the offer was set and wait for next + // provisional or final answer. + if (src == CS_REMOTE) + state_ = ST_SENTOFFER; + else // CS_LOCAL + state_ = ST_RECEIVEDOFFER; + } + } else if (answer_enable) { + // If the offer didn't specify RTCP mux, the answer shouldn't either. + LOG(LS_WARNING) << "Invalid parameters in RTCP mux provisional answer"; + return false; + } + + return true; +} + +bool RtcpMuxFilter::SetAnswer(bool answer_enable, ContentSource src) { + if (!ExpectAnswer(src)) { + LOG(LS_ERROR) << "Invalid state for RTCP mux answer"; + return false; + } + + if (offer_enable_ && answer_enable) { + state_ = ST_ACTIVE; + } else if (answer_enable) { + // If the offer didn't specify RTCP mux, the answer shouldn't either. + LOG(LS_WARNING) << "Invalid parameters in RTCP mux answer"; + return false; + } else { + state_ = ST_INIT; + } + return true; +} + +bool RtcpMuxFilter::DemuxRtcp(const char* data, int len) { + // If we're muxing RTP/RTCP, we must inspect each packet delivered and + // determine whether it is RTP or RTCP. We do so by checking the packet type, + // and assuming RTP if type is 0-63 or 96-127. For additional details, see + // http://tools.ietf.org/html/rfc5761. + // Note that if we offer RTCP mux, we may receive muxed RTCP before we + // receive the answer, so we operate in that state too. + if (!offer_enable_ || state_ < ST_SENTOFFER) { + return false; + } + + int type = (len >= 2) ? (static_cast(data[1]) & 0x7F) : 0; + return (type >= 64 && type < 96); +} + +bool RtcpMuxFilter::ExpectOffer(bool offer_enable, ContentSource source) { + return ((state_ == ST_INIT) || + (state_ == ST_ACTIVE && offer_enable == offer_enable_) || + (state_ == ST_SENTOFFER && source == CS_LOCAL) || + (state_ == ST_RECEIVEDOFFER && source == CS_REMOTE)); +} + +bool RtcpMuxFilter::ExpectAnswer(ContentSource source) { + return ((state_ == ST_SENTOFFER && source == CS_REMOTE) || + (state_ == ST_RECEIVEDOFFER && source == CS_LOCAL) || + (state_ == ST_SENTPRANSWER && source == CS_LOCAL) || + (state_ == ST_RECEIVEDPRANSWER && source == CS_REMOTE)); +} + +} // namespace cricket diff --git a/talk/session/media/rtcpmuxfilter.h b/talk/session/media/rtcpmuxfilter.h new file mode 100644 index 000000000..a5bb85e3c --- /dev/null +++ b/talk/session/media/rtcpmuxfilter.h @@ -0,0 +1,86 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_SESSION_MEDIA_RTCPMUXFILTER_H_ +#define TALK_SESSION_MEDIA_RTCPMUXFILTER_H_ + +#include "talk/base/basictypes.h" +#include "talk/p2p/base/sessiondescription.h" + +namespace cricket { + +// RTCP Muxer, as defined in RFC 5761 (http://tools.ietf.org/html/rfc5761) +class RtcpMuxFilter { + public: + RtcpMuxFilter(); + + // Whether the filter is active, i.e. has RTCP mux been properly negotiated. + bool IsActive() const; + + // Specifies whether the offer indicates the use of RTCP mux. + bool SetOffer(bool offer_enable, ContentSource src); + + // Specifies whether the provisional answer indicates the use of RTCP mux. + bool SetProvisionalAnswer(bool answer_enable, ContentSource src); + + // Specifies whether the answer indicates the use of RTCP mux. + bool SetAnswer(bool answer_enable, ContentSource src); + + // Determines whether the specified packet is RTCP. + bool DemuxRtcp(const char* data, int len); + + private: + bool ExpectOffer(bool offer_enable, ContentSource source); + bool ExpectAnswer(ContentSource source); + enum State { + // RTCP mux filter unused. + ST_INIT, + // Offer with RTCP mux enabled received. + // RTCP mux filter is not active. + ST_RECEIVEDOFFER, + // Offer with RTCP mux enabled sent. + // RTCP mux filter can demux incoming packets but is not active. + ST_SENTOFFER, + // RTCP mux filter is active but the sent answer is only provisional. + // When the final answer is set, the state transitions to ST_ACTIVE or + // ST_INIT. + ST_SENTPRANSWER, + // RTCP mux filter is active but the received answer is only provisional. + // When the final answer is set, the state transitions to ST_ACTIVE or + // ST_INIT. + ST_RECEIVEDPRANSWER, + // Offer and answer set, RTCP mux enabled. It is not possible to de-activate + // the filter. + ST_ACTIVE + }; + State state_; + bool offer_enable_; +}; + +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_RTCPMUXFILTER_H_ diff --git a/talk/session/media/rtcpmuxfilter_unittest.cc b/talk/session/media/rtcpmuxfilter_unittest.cc new file mode 100644 index 000000000..ad3349838 --- /dev/null +++ b/talk/session/media/rtcpmuxfilter_unittest.cc @@ -0,0 +1,212 @@ +// libjingle +// Copyright 2011 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/media/rtcpmuxfilter.h" + +#include "talk/base/gunit.h" +#include "talk/media/base/testutils.h" + +TEST(RtcpMuxFilterTest, DemuxRtcpSender) { + cricket::RtcpMuxFilter filter; + const char data[] = { 0, 73, 0, 0 }; + const int len = 4; + + // Init state - refuse to demux + EXPECT_FALSE(filter.DemuxRtcp(data, len)); + // After sent offer, demux should be enabled + filter.SetOffer(true, cricket::CS_LOCAL); + EXPECT_TRUE(filter.DemuxRtcp(data, len)); + // Remote accepted, demux should be enabled + filter.SetAnswer(true, cricket::CS_REMOTE); + EXPECT_TRUE(filter.DemuxRtcp(data, len)); +} + +TEST(RtcpMuxFilterTest, DemuxRtcpReceiver) { + cricket::RtcpMuxFilter filter; + const char data[] = { 0, 73, 0, 0 }; + const int len = 4; + + // Init state - refuse to demux + EXPECT_FALSE(filter.DemuxRtcp(data, len)); + // After received offer, demux should not be enabled + filter.SetOffer(true, cricket::CS_REMOTE); + EXPECT_FALSE(filter.DemuxRtcp(data, len)); + // We accept, demux is now enabled + filter.SetAnswer(true, cricket::CS_LOCAL); + EXPECT_TRUE(filter.DemuxRtcp(data, len)); +} + +TEST(RtcpMuxFilterTest, DemuxRtcpSenderProvisionalAnswer) { + cricket::RtcpMuxFilter filter; + const char data[] = { 0, 73, 0, 0 }; + const int len = 4; + + filter.SetOffer(true, cricket::CS_REMOTE); + // Received provisional answer without mux enabled. + filter.SetProvisionalAnswer(false, cricket::CS_LOCAL); + EXPECT_FALSE(filter.DemuxRtcp(data, len)); + // Received provisional answer with mux enabled. + filter.SetProvisionalAnswer(true, cricket::CS_LOCAL); + EXPECT_TRUE(filter.DemuxRtcp(data, len)); + // Remote accepted, demux should be enabled. + filter.SetAnswer(true, cricket::CS_LOCAL); + EXPECT_TRUE(filter.DemuxRtcp(data, len)); +} + +TEST(RtcpMuxFilterTest, DemuxRtcpReceiverProvisionalAnswer) { + cricket::RtcpMuxFilter filter; + const char data[] = { 0, 73, 0, 0 }; + const int len = 4; + + filter.SetOffer(true, cricket::CS_LOCAL); + // Received provisional answer without mux enabled. + filter.SetProvisionalAnswer(false, cricket::CS_REMOTE); + // After sent offer, demux should be enabled until we have received a + // final answer. + EXPECT_TRUE(filter.DemuxRtcp(data, len)); + // Received provisional answer with mux enabled. + filter.SetProvisionalAnswer(true, cricket::CS_REMOTE); + EXPECT_TRUE(filter.DemuxRtcp(data, len)); + // Remote accepted, demux should be enabled. + filter.SetAnswer(true, cricket::CS_REMOTE); + EXPECT_TRUE(filter.DemuxRtcp(data, len)); +} + +TEST(RtcpMuxFilterTest, IsActiveSender) { + cricket::RtcpMuxFilter filter; + // Init state - not active + EXPECT_FALSE(filter.IsActive()); + // After sent offer, demux should not be active. + filter.SetOffer(true, cricket::CS_LOCAL); + EXPECT_FALSE(filter.IsActive()); + // Remote accepted, filter is now active. + filter.SetAnswer(true, cricket::CS_REMOTE); + EXPECT_TRUE(filter.IsActive()); +} + +// Test that we can receive provisional answer and final answer. +TEST(RtcpMuxFilterTest, ReceivePrAnswer) { + cricket::RtcpMuxFilter filter; + filter.SetOffer(true, cricket::CS_LOCAL); + // Received provisional answer with mux enabled. + EXPECT_TRUE(filter.SetProvisionalAnswer(true, cricket::CS_REMOTE)); + // We are now active since both sender and receiver support mux. + EXPECT_TRUE(filter.IsActive()); + // Received provisional answer with mux disabled. + EXPECT_TRUE(filter.SetProvisionalAnswer(false, cricket::CS_REMOTE)); + // We are now inactive since the receiver doesn't support mux. + EXPECT_FALSE(filter.IsActive()); + // Received final answer with mux enabled. + EXPECT_TRUE(filter.SetAnswer(true, cricket::CS_REMOTE)); + EXPECT_TRUE(filter.IsActive()); +} + +TEST(RtcpMuxFilterTest, IsActiveReceiver) { + cricket::RtcpMuxFilter filter; + // Init state - not active. + EXPECT_FALSE(filter.IsActive()); + // After received offer, demux should not be active + filter.SetOffer(true, cricket::CS_REMOTE); + EXPECT_FALSE(filter.IsActive()); + // We accept, filter is now active + filter.SetAnswer(true, cricket::CS_LOCAL); + EXPECT_TRUE(filter.IsActive()); +} + +// Test that we can send provisional answer and final answer. +TEST(RtcpMuxFilterTest, SendPrAnswer) { + cricket::RtcpMuxFilter filter; + filter.SetOffer(true, cricket::CS_REMOTE); + // Send provisional answer with mux enabled. + EXPECT_TRUE(filter.SetProvisionalAnswer(true, cricket::CS_LOCAL)); + EXPECT_TRUE(filter.IsActive()); + // Received provisional answer with mux disabled. + EXPECT_TRUE(filter.SetProvisionalAnswer(false, cricket::CS_LOCAL)); + EXPECT_FALSE(filter.IsActive()); + // Send final answer with mux enabled. + EXPECT_TRUE(filter.SetAnswer(true, cricket::CS_LOCAL)); + EXPECT_TRUE(filter.IsActive()); +} + +// Test that we can enable the filter in an update. +// We can not disable the filter later since that would mean we need to +// recreate a rtcp transport channel. +TEST(RtcpMuxFilterTest, EnableFilterDuringUpdate) { + cricket::RtcpMuxFilter filter; + EXPECT_FALSE(filter.IsActive()); + EXPECT_TRUE(filter.SetOffer(false, cricket::CS_REMOTE)); + EXPECT_TRUE(filter.SetAnswer(false, cricket::CS_LOCAL)); + EXPECT_FALSE(filter.IsActive()); + + EXPECT_TRUE(filter.SetOffer(true, cricket::CS_REMOTE)); + EXPECT_TRUE(filter.SetAnswer(true, cricket::CS_LOCAL)); + EXPECT_TRUE(filter.IsActive()); + + EXPECT_FALSE(filter.SetOffer(false, cricket::CS_REMOTE)); + EXPECT_FALSE(filter.SetAnswer(false, cricket::CS_LOCAL)); + EXPECT_TRUE(filter.IsActive()); +} + +// Test that SetOffer can be called twice. +TEST(RtcpMuxFilterTest, SetOfferTwice) { + cricket::RtcpMuxFilter filter; + + EXPECT_TRUE(filter.SetOffer(true, cricket::CS_REMOTE)); + EXPECT_TRUE(filter.SetOffer(true, cricket::CS_REMOTE)); + EXPECT_TRUE(filter.SetAnswer(true, cricket::CS_LOCAL)); + EXPECT_TRUE(filter.IsActive()); + + cricket::RtcpMuxFilter filter2; + EXPECT_TRUE(filter2.SetOffer(false, cricket::CS_LOCAL)); + EXPECT_TRUE(filter2.SetOffer(false, cricket::CS_LOCAL)); + EXPECT_TRUE(filter2.SetAnswer(false, cricket::CS_REMOTE)); + EXPECT_FALSE(filter2.IsActive()); +} + +// Test that the filter can be enabled twice. +TEST(RtcpMuxFilterTest, EnableFilterTwiceDuringUpdate) { + cricket::RtcpMuxFilter filter; + + EXPECT_TRUE(filter.SetOffer(true, cricket::CS_REMOTE)); + EXPECT_TRUE(filter.SetAnswer(true, cricket::CS_LOCAL)); + EXPECT_TRUE(filter.IsActive()); + + EXPECT_TRUE(filter.SetOffer(true, cricket::CS_REMOTE)); + EXPECT_TRUE(filter.SetAnswer(true, cricket::CS_LOCAL)); + EXPECT_TRUE(filter.IsActive()); +} + +// Test that the filter can be kept disabled during updates. +TEST(RtcpMuxFilterTest, KeepFilterDisabledDuringUpdate) { + cricket::RtcpMuxFilter filter; + + EXPECT_TRUE(filter.SetOffer(false, cricket::CS_REMOTE)); + EXPECT_TRUE(filter.SetAnswer(false, cricket::CS_LOCAL)); + EXPECT_FALSE(filter.IsActive()); + + EXPECT_TRUE(filter.SetOffer(false, cricket::CS_REMOTE)); + EXPECT_TRUE(filter.SetAnswer(false, cricket::CS_LOCAL)); + EXPECT_FALSE(filter.IsActive()); +} diff --git a/talk/session/media/soundclip.cc b/talk/session/media/soundclip.cc new file mode 100644 index 000000000..44f457cda --- /dev/null +++ b/talk/session/media/soundclip.cc @@ -0,0 +1,82 @@ +/* + * libjingle + * Copyright 2004 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/media/soundclip.h" + +namespace cricket { + +enum { + MSG_PLAYSOUND = 1, +}; + +struct PlaySoundMessageData : talk_base::MessageData { + PlaySoundMessageData(const void *c, + int l, + SoundclipMedia::SoundclipFlags f) + : clip(c), + len(l), + flags(f), + result(false) { + } + + const void *clip; + int len; + SoundclipMedia::SoundclipFlags flags; + bool result; +}; + +Soundclip::Soundclip(talk_base::Thread *thread, SoundclipMedia *soundclip_media) + : worker_thread_(thread), + soundclip_media_(soundclip_media) { +} + +bool Soundclip::PlaySound(const void *clip, + int len, + SoundclipMedia::SoundclipFlags flags) { + PlaySoundMessageData data(clip, len, flags); + worker_thread_->Send(this, MSG_PLAYSOUND, &data); + return data.result; +} + +bool Soundclip::PlaySound_w(const void *clip, + int len, + SoundclipMedia::SoundclipFlags flags) { + return soundclip_media_->PlaySound(static_cast(clip), + len, + flags); +} + +void Soundclip::OnMessage(talk_base::Message *message) { + ASSERT(message->message_id == MSG_PLAYSOUND); + PlaySoundMessageData *data = + static_cast(message->pdata); + data->result = PlaySound_w(data->clip, + data->len, + data->flags); +} + +} // namespace cricket diff --git a/talk/session/media/soundclip.h b/talk/session/media/soundclip.h new file mode 100644 index 000000000..f057d8de3 --- /dev/null +++ b/talk/session/media/soundclip.h @@ -0,0 +1,70 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_SESSION_MEDIA_SOUNDCLIP_H_ +#define TALK_SESSION_MEDIA_SOUNDCLIP_H_ + +#include "talk/base/scoped_ptr.h" +#include "talk/media/base/mediaengine.h" + +namespace talk_base { + +class Thread; + +} + +namespace cricket { + +// Soundclip wraps SoundclipMedia to support marshalling calls to the proper +// thread. +class Soundclip : private talk_base::MessageHandler { + public: + Soundclip(talk_base::Thread* thread, SoundclipMedia* soundclip_media); + + // Plays a sound out to the speakers with the given audio stream. The stream + // must be 16-bit little-endian 16 kHz PCM. If a stream is already playing + // on this Soundclip, it is stopped. If clip is NULL, nothing is played. + // Returns whether it was successful. + bool PlaySound(const void* clip, + int len, + SoundclipMedia::SoundclipFlags flags); + + private: + bool PlaySound_w(const void* clip, + int len, + SoundclipMedia::SoundclipFlags flags); + + // From MessageHandler + virtual void OnMessage(talk_base::Message* message); + + talk_base::Thread* worker_thread_; + talk_base::scoped_ptr soundclip_media_; +}; + +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_SOUNDCLIP_H_ diff --git a/talk/session/media/srtpfilter.cc b/talk/session/media/srtpfilter.cc new file mode 100644 index 000000000..e5104db49 --- /dev/null +++ b/talk/session/media/srtpfilter.cc @@ -0,0 +1,805 @@ +/* + * libjingle + * Copyright 2009 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. + */ + +#undef HAVE_CONFIG_H + +#include "talk/session/media/srtpfilter.h" + +#include +#include + +#include "talk/base/base64.h" +#include "talk/base/logging.h" +#include "talk/base/stringencode.h" +#include "talk/base/timeutils.h" +#include "talk/media/base/rtputils.h" + +// Enable this line to turn on SRTP debugging +// #define SRTP_DEBUG + +#ifdef HAVE_SRTP +#ifdef SRTP_RELATIVE_PATH +#include "srtp.h" // NOLINT +#else +#include "third_party/libsrtp/include/srtp.h" +#endif // SRTP_RELATIVE_PATH +#ifdef _DEBUG +extern "C" debug_module_t mod_srtp; +extern "C" debug_module_t mod_auth; +extern "C" debug_module_t mod_cipher; +extern "C" debug_module_t mod_stat; +extern "C" debug_module_t mod_alloc; +extern "C" debug_module_t mod_aes_icm; +extern "C" debug_module_t mod_aes_hmac; +#endif +#else +// SrtpFilter needs that constant. +#define SRTP_MASTER_KEY_LEN 30 +#endif // HAVE_SRTP + +namespace cricket { + +const char CS_AES_CM_128_HMAC_SHA1_80[] = "AES_CM_128_HMAC_SHA1_80"; +const char CS_AES_CM_128_HMAC_SHA1_32[] = "AES_CM_128_HMAC_SHA1_32"; +const int SRTP_MASTER_KEY_BASE64_LEN = SRTP_MASTER_KEY_LEN * 4 / 3; +const int SRTP_MASTER_KEY_KEY_LEN = 16; +const int SRTP_MASTER_KEY_SALT_LEN = 14; + +#ifndef HAVE_SRTP + +// This helper function is used on systems that don't (yet) have SRTP, +// to log that the functions that require it won't do anything. +namespace { +bool SrtpNotAvailable(const char *func) { + LOG(LS_ERROR) << func << ": SRTP is not available on your system."; + return false; +} +} // anonymous namespace + +#endif // !HAVE_SRTP + +void EnableSrtpDebugging() { +#ifdef HAVE_SRTP +#ifdef _DEBUG + debug_on(mod_srtp); + debug_on(mod_auth); + debug_on(mod_cipher); + debug_on(mod_stat); + debug_on(mod_alloc); + debug_on(mod_aes_icm); + // debug_on(mod_aes_cbc); + // debug_on(mod_hmac); +#endif +#endif // HAVE_SRTP +} + +SrtpFilter::SrtpFilter() + : state_(ST_INIT), + signal_silent_time_in_ms_(0) { +} + +SrtpFilter::~SrtpFilter() { +} + +bool SrtpFilter::IsActive() const { + return state_ >= ST_ACTIVE; +} + +bool SrtpFilter::SetOffer(const std::vector& offer_params, + ContentSource source) { + if (!ExpectOffer(source)) { + LOG(LS_ERROR) << "Wrong state to update SRTP offer"; + return false; + } + return StoreParams(offer_params, source); +} + +bool SrtpFilter::SetAnswer(const std::vector& answer_params, + ContentSource source) { + return DoSetAnswer(answer_params, source, true); +} + +bool SrtpFilter::SetProvisionalAnswer( + const std::vector& answer_params, + ContentSource source) { + return DoSetAnswer(answer_params, source, false); +} + +bool SrtpFilter::SetRtpParams(const std::string& send_cs, + const uint8* send_key, int send_key_len, + const std::string& recv_cs, + const uint8* recv_key, int recv_key_len) { + if (state_ == ST_ACTIVE) { + LOG(LS_ERROR) << "Tried to set SRTP Params when filter already active"; + return false; + } + CreateSrtpSessions(); + if (!send_session_->SetSend(send_cs, send_key, send_key_len)) + return false; + + if (!recv_session_->SetRecv(recv_cs, recv_key, recv_key_len)) + return false; + + state_ = ST_ACTIVE; + + LOG(LS_INFO) << "SRTP activated with negotiated parameters:" + << " send cipher_suite " << send_cs + << " recv cipher_suite " << recv_cs; + + return true; +} + +// This function is provided separately because DTLS-SRTP behaves +// differently in RTP/RTCP mux and non-mux modes. +// +// - In the non-muxed case, RTP and RTCP are keyed with different +// keys (from different DTLS handshakes), and so we need a new +// SrtpSession. +// - In the muxed case, they are keyed with the same keys, so +// this function is not needed +bool SrtpFilter::SetRtcpParams(const std::string& send_cs, + const uint8* send_key, int send_key_len, + const std::string& recv_cs, + const uint8* recv_key, int recv_key_len) { + // This can only be called once, but can be safely called after + // SetRtpParams + if (send_rtcp_session_ || recv_rtcp_session_) { + LOG(LS_ERROR) << "Tried to set SRTCP Params when filter already active"; + return false; + } + + send_rtcp_session_.reset(new SrtpSession()); + SignalSrtpError.repeat(send_rtcp_session_->SignalSrtpError); + send_rtcp_session_->set_signal_silent_time(signal_silent_time_in_ms_); + if (!send_rtcp_session_->SetRecv(send_cs, send_key, send_key_len)) + return false; + + recv_rtcp_session_.reset(new SrtpSession()); + SignalSrtpError.repeat(recv_rtcp_session_->SignalSrtpError); + recv_rtcp_session_->set_signal_silent_time(signal_silent_time_in_ms_); + if (!recv_rtcp_session_->SetRecv(recv_cs, recv_key, recv_key_len)) + return false; + + LOG(LS_INFO) << "SRTCP activated with negotiated parameters:" + << " send cipher_suite " << send_cs + << " recv cipher_suite " << recv_cs; + + return true; +} + +bool SrtpFilter::ProtectRtp(void* p, int in_len, int max_len, int* out_len) { + if (!IsActive()) { + LOG(LS_WARNING) << "Failed to ProtectRtp: SRTP not active"; + return false; + } + return send_session_->ProtectRtp(p, in_len, max_len, out_len); +} + +bool SrtpFilter::ProtectRtcp(void* p, int in_len, int max_len, int* out_len) { + if (!IsActive()) { + LOG(LS_WARNING) << "Failed to ProtectRtcp: SRTP not active"; + return false; + } + if (send_rtcp_session_) { + return send_rtcp_session_->ProtectRtcp(p, in_len, max_len, out_len); + } else { + return send_session_->ProtectRtcp(p, in_len, max_len, out_len); + } +} + +bool SrtpFilter::UnprotectRtp(void* p, int in_len, int* out_len) { + if (!IsActive()) { + LOG(LS_WARNING) << "Failed to UnprotectRtp: SRTP not active"; + return false; + } + return recv_session_->UnprotectRtp(p, in_len, out_len); +} + +bool SrtpFilter::UnprotectRtcp(void* p, int in_len, int* out_len) { + if (!IsActive()) { + LOG(LS_WARNING) << "Failed to UnprotectRtcp: SRTP not active"; + return false; + } + if (recv_rtcp_session_) { + return recv_rtcp_session_->UnprotectRtcp(p, in_len, out_len); + } else { + return recv_session_->UnprotectRtcp(p, in_len, out_len); + } +} + +void SrtpFilter::set_signal_silent_time(uint32 signal_silent_time_in_ms) { + signal_silent_time_in_ms_ = signal_silent_time_in_ms; + if (state_ == ST_ACTIVE) { + send_session_->set_signal_silent_time(signal_silent_time_in_ms); + recv_session_->set_signal_silent_time(signal_silent_time_in_ms); + if (send_rtcp_session_) + send_rtcp_session_->set_signal_silent_time(signal_silent_time_in_ms); + if (recv_rtcp_session_) + recv_rtcp_session_->set_signal_silent_time(signal_silent_time_in_ms); + } +} + +bool SrtpFilter::ExpectOffer(ContentSource source) { + return ((state_ == ST_INIT) || + (state_ == ST_ACTIVE) || + (state_ == ST_SENTOFFER && source == CS_LOCAL) || + (state_ == ST_SENTUPDATEDOFFER && source == CS_LOCAL) || + (state_ == ST_RECEIVEDOFFER && source == CS_REMOTE) || + (state_ == ST_RECEIVEDUPDATEDOFFER && source == CS_REMOTE)); +} + +bool SrtpFilter::StoreParams(const std::vector& params, + ContentSource source) { + offer_params_ = params; + if (state_ == ST_INIT) { + state_ = (source == CS_LOCAL) ? ST_SENTOFFER : ST_RECEIVEDOFFER; + } else { // state >= ST_ACTIVE + state_ = + (source == CS_LOCAL) ? ST_SENTUPDATEDOFFER : ST_RECEIVEDUPDATEDOFFER; + } + return true; +} + +bool SrtpFilter::ExpectAnswer(ContentSource source) { + return ((state_ == ST_SENTOFFER && source == CS_REMOTE) || + (state_ == ST_RECEIVEDOFFER && source == CS_LOCAL) || + (state_ == ST_SENTUPDATEDOFFER && source == CS_REMOTE) || + (state_ == ST_RECEIVEDUPDATEDOFFER && source == CS_LOCAL) || + (state_ == ST_SENTPRANSWER_NO_CRYPTO && source == CS_LOCAL) || + (state_ == ST_SENTPRANSWER && source == CS_LOCAL) || + (state_ == ST_RECEIVEDPRANSWER_NO_CRYPTO && source == CS_REMOTE) || + (state_ == ST_RECEIVEDPRANSWER && source == CS_REMOTE)); +} + +bool SrtpFilter::DoSetAnswer(const std::vector& answer_params, + ContentSource source, + bool final) { + if (!ExpectAnswer(source)) { + LOG(LS_ERROR) << "Invalid state for SRTP answer"; + return false; + } + + // If the answer doesn't requests crypto complete the negotiation of an + // unencrypted session. + // Otherwise, finalize the parameters and apply them. + if (answer_params.empty()) { + if (final) { + return ResetParams(); + } else { + // Need to wait for the final answer to decide if + // we should go to Active state. + state_ = (source == CS_LOCAL) ? ST_SENTPRANSWER_NO_CRYPTO : + ST_RECEIVEDPRANSWER_NO_CRYPTO; + return true; + } + } + CryptoParams selected_params; + if (!NegotiateParams(answer_params, &selected_params)) + return false; + const CryptoParams& send_params = + (source == CS_REMOTE) ? selected_params : answer_params[0]; + const CryptoParams& recv_params = + (source == CS_REMOTE) ? answer_params[0] : selected_params; + if (!ApplyParams(send_params, recv_params)) { + return false; + } + + if (final) { + offer_params_.clear(); + state_ = ST_ACTIVE; + } else { + state_ = + (source == CS_LOCAL) ? ST_SENTPRANSWER : ST_RECEIVEDPRANSWER; + } + return true; +} + +void SrtpFilter::CreateSrtpSessions() { + send_session_.reset(new SrtpSession()); + applied_send_params_ = CryptoParams(); + recv_session_.reset(new SrtpSession()); + applied_recv_params_ = CryptoParams(); + + SignalSrtpError.repeat(send_session_->SignalSrtpError); + SignalSrtpError.repeat(recv_session_->SignalSrtpError); + + send_session_->set_signal_silent_time(signal_silent_time_in_ms_); + recv_session_->set_signal_silent_time(signal_silent_time_in_ms_); +} + +bool SrtpFilter::NegotiateParams(const std::vector& answer_params, + CryptoParams* selected_params) { + // We're processing an accept. We should have exactly one set of params, + // unless the offer didn't mention crypto, in which case we shouldn't be here. + bool ret = (answer_params.size() == 1U && !offer_params_.empty()); + if (ret) { + // We should find a match between the answer params and the offered params. + std::vector::const_iterator it; + for (it = offer_params_.begin(); it != offer_params_.end(); ++it) { + if (answer_params[0].Matches(*it)) { + break; + } + } + + if (it != offer_params_.end()) { + *selected_params = *it; + } else { + ret = false; + } + } + + if (!ret) { + LOG(LS_WARNING) << "Invalid parameters in SRTP answer"; + } + return ret; +} + +bool SrtpFilter::ApplyParams(const CryptoParams& send_params, + const CryptoParams& recv_params) { + // TODO(jiayl): Split this method to apply send and receive CryptoParams + // independently, so that we can skip one method when either send or receive + // CryptoParams is unchanged. + if (applied_send_params_.cipher_suite == send_params.cipher_suite && + applied_send_params_.key_params == send_params.key_params && + applied_recv_params_.cipher_suite == recv_params.cipher_suite && + applied_recv_params_.key_params == recv_params.key_params) { + LOG(LS_INFO) << "Applying the same SRTP parameters again. No-op."; + + // We do not want to reset the ROC if the keys are the same. So just return. + return true; + } + // TODO(juberti): Zero these buffers after use. + bool ret; + uint8 send_key[SRTP_MASTER_KEY_LEN], recv_key[SRTP_MASTER_KEY_LEN]; + ret = (ParseKeyParams(send_params.key_params, send_key, sizeof(send_key)) && + ParseKeyParams(recv_params.key_params, recv_key, sizeof(recv_key))); + if (ret) { + CreateSrtpSessions(); + ret = (send_session_->SetSend(send_params.cipher_suite, + send_key, sizeof(send_key)) && + recv_session_->SetRecv(recv_params.cipher_suite, + recv_key, sizeof(recv_key))); + } + if (ret) { + LOG(LS_INFO) << "SRTP activated with negotiated parameters:" + << " send cipher_suite " << send_params.cipher_suite + << " recv cipher_suite " << recv_params.cipher_suite; + applied_send_params_ = send_params; + applied_recv_params_ = recv_params; + } else { + LOG(LS_WARNING) << "Failed to apply negotiated SRTP parameters"; + } + return ret; +} + +bool SrtpFilter::ResetParams() { + offer_params_.clear(); + state_ = ST_INIT; + LOG(LS_INFO) << "SRTP reset to init state"; + return true; +} + +bool SrtpFilter::ParseKeyParams(const std::string& key_params, + uint8* key, int len) { + // example key_params: "inline:YUJDZGVmZ2hpSktMbW9QUXJzVHVWd3l6MTIzNDU2" + + // Fail if key-method is wrong. + if (key_params.find("inline:") != 0) { + return false; + } + + // Fail if base64 decode fails, or the key is the wrong size. + std::string key_b64(key_params.substr(7)), key_str; + if (!talk_base::Base64::Decode(key_b64, talk_base::Base64::DO_STRICT, + &key_str, NULL) || + static_cast(key_str.size()) != len) { + return false; + } + + memcpy(key, key_str.c_str(), len); + return true; +} + +/////////////////////////////////////////////////////////////////////////////// +// SrtpSession + +#ifdef HAVE_SRTP + +bool SrtpSession::inited_ = false; + +SrtpSession::SrtpSession() + : session_(NULL), + rtp_auth_tag_len_(0), + rtcp_auth_tag_len_(0), + srtp_stat_(new SrtpStat()), + last_send_seq_num_(-1) { + sessions()->push_back(this); + SignalSrtpError.repeat(srtp_stat_->SignalSrtpError); +} + +SrtpSession::~SrtpSession() { + sessions()->erase(std::find(sessions()->begin(), sessions()->end(), this)); + if (session_) { + srtp_dealloc(session_); + } +} + +bool SrtpSession::SetSend(const std::string& cs, const uint8* key, int len) { + return SetKey(ssrc_any_outbound, cs, key, len); +} + +bool SrtpSession::SetRecv(const std::string& cs, const uint8* key, int len) { + return SetKey(ssrc_any_inbound, cs, key, len); +} + +bool SrtpSession::ProtectRtp(void* p, int in_len, int max_len, int* out_len) { + if (!session_) { + LOG(LS_WARNING) << "Failed to protect SRTP packet: no SRTP Session"; + return false; + } + + int need_len = in_len + rtp_auth_tag_len_; // NOLINT + if (max_len < need_len) { + LOG(LS_WARNING) << "Failed to protect SRTP packet: The buffer length " + << max_len << " is less than the needed " << need_len; + return false; + } + + *out_len = in_len; + int err = srtp_protect(session_, p, out_len); + uint32 ssrc; + if (GetRtpSsrc(p, in_len, &ssrc)) { + srtp_stat_->AddProtectRtpResult(ssrc, err); + } + int seq_num; + GetRtpSeqNum(p, in_len, &seq_num); + if (err != err_status_ok) { + LOG(LS_WARNING) << "Failed to protect SRTP packet, seqnum=" + << seq_num << ", err=" << err << ", last seqnum=" + << last_send_seq_num_; + return false; + } + last_send_seq_num_ = seq_num; + return true; +} + +bool SrtpSession::ProtectRtcp(void* p, int in_len, int max_len, int* out_len) { + if (!session_) { + LOG(LS_WARNING) << "Failed to protect SRTCP packet: no SRTP Session"; + return false; + } + + int need_len = in_len + sizeof(uint32) + rtcp_auth_tag_len_; // NOLINT + if (max_len < need_len) { + LOG(LS_WARNING) << "Failed to protect SRTCP packet: The buffer length " + << max_len << " is less than the needed " << need_len; + return false; + } + + *out_len = in_len; + int err = srtp_protect_rtcp(session_, p, out_len); + srtp_stat_->AddProtectRtcpResult(err); + if (err != err_status_ok) { + LOG(LS_WARNING) << "Failed to protect SRTCP packet, err=" << err; + return false; + } + return true; +} + +bool SrtpSession::UnprotectRtp(void* p, int in_len, int* out_len) { + if (!session_) { + LOG(LS_WARNING) << "Failed to unprotect SRTP packet: no SRTP Session"; + return false; + } + + *out_len = in_len; + int err = srtp_unprotect(session_, p, out_len); + uint32 ssrc; + if (GetRtpSsrc(p, in_len, &ssrc)) { + srtp_stat_->AddUnprotectRtpResult(ssrc, err); + } + if (err != err_status_ok) { + LOG(LS_WARNING) << "Failed to unprotect SRTP packet, err=" << err; + return false; + } + return true; +} + +bool SrtpSession::UnprotectRtcp(void* p, int in_len, int* out_len) { + if (!session_) { + LOG(LS_WARNING) << "Failed to unprotect SRTCP packet: no SRTP Session"; + return false; + } + + *out_len = in_len; + int err = srtp_unprotect_rtcp(session_, p, out_len); + srtp_stat_->AddUnprotectRtcpResult(err); + if (err != err_status_ok) { + LOG(LS_WARNING) << "Failed to unprotect SRTCP packet, err=" << err; + return false; + } + return true; +} + +void SrtpSession::set_signal_silent_time(uint32 signal_silent_time_in_ms) { + srtp_stat_->set_signal_silent_time(signal_silent_time_in_ms); +} + +bool SrtpSession::SetKey(int type, const std::string& cs, + const uint8* key, int len) { + if (session_) { + LOG(LS_ERROR) << "Failed to create SRTP session: " + << "SRTP session already created"; + return false; + } + + if (!Init()) { + return false; + } + + srtp_policy_t policy; + memset(&policy, 0, sizeof(policy)); + + if (cs == CS_AES_CM_128_HMAC_SHA1_80) { + crypto_policy_set_aes_cm_128_hmac_sha1_80(&policy.rtp); + crypto_policy_set_aes_cm_128_hmac_sha1_80(&policy.rtcp); + } else if (cs == CS_AES_CM_128_HMAC_SHA1_32) { + crypto_policy_set_aes_cm_128_hmac_sha1_32(&policy.rtp); // rtp is 32, + crypto_policy_set_aes_cm_128_hmac_sha1_80(&policy.rtcp); // rtcp still 80 + } else { + LOG(LS_WARNING) << "Failed to create SRTP session: unsupported" + << " cipher_suite " << cs.c_str(); + return false; + } + + if (!key || len != SRTP_MASTER_KEY_LEN) { + LOG(LS_WARNING) << "Failed to create SRTP session: invalid key"; + return false; + } + + policy.ssrc.type = static_cast(type); + policy.ssrc.value = 0; + policy.key = const_cast(key); + // TODO(astor) parse window size from WSH session-param + policy.window_size = 1024; + policy.allow_repeat_tx = 1; + policy.next = NULL; + + int err = srtp_create(&session_, &policy); + if (err != err_status_ok) { + LOG(LS_ERROR) << "Failed to create SRTP session, err=" << err; + return false; + } + + rtp_auth_tag_len_ = policy.rtp.auth_tag_len; + rtcp_auth_tag_len_ = policy.rtcp.auth_tag_len; + return true; +} + +bool SrtpSession::Init() { + if (!inited_) { + int err; + err = srtp_init(); + if (err != err_status_ok) { + LOG(LS_ERROR) << "Failed to init SRTP, err=" << err; + return false; + } + + err = srtp_install_event_handler(&SrtpSession::HandleEventThunk); + if (err != err_status_ok) { + LOG(LS_ERROR) << "Failed to install SRTP event handler, err=" << err; + return false; + } + + inited_ = true; + } + + return true; +} + +void SrtpSession::HandleEvent(const srtp_event_data_t* ev) { + switch (ev->event) { + case event_ssrc_collision: + LOG(LS_INFO) << "SRTP event: SSRC collision"; + break; + case event_key_soft_limit: + LOG(LS_INFO) << "SRTP event: reached soft key usage limit"; + break; + case event_key_hard_limit: + LOG(LS_INFO) << "SRTP event: reached hard key usage limit"; + break; + case event_packet_index_limit: + LOG(LS_INFO) << "SRTP event: reached hard packet limit (2^48 packets)"; + break; + default: + LOG(LS_INFO) << "SRTP event: unknown " << ev->event; + break; + } +} + +void SrtpSession::HandleEventThunk(srtp_event_data_t* ev) { + for (std::list::iterator it = sessions()->begin(); + it != sessions()->end(); ++it) { + if ((*it)->session_ == ev->session) { + (*it)->HandleEvent(ev); + break; + } + } +} + +std::list* SrtpSession::sessions() { + LIBJINGLE_DEFINE_STATIC_LOCAL(std::list, sessions, ()); + return &sessions; +} + +#else // !HAVE_SRTP + +// On some systems, SRTP is not (yet) available. + +SrtpSession::SrtpSession() { + LOG(WARNING) << "SRTP implementation is missing."; +} + +SrtpSession::~SrtpSession() { +} + +bool SrtpSession::SetSend(const std::string& cs, const uint8* key, int len) { + return SrtpNotAvailable(__FUNCTION__); +} + +bool SrtpSession::SetRecv(const std::string& cs, const uint8* key, int len) { + return SrtpNotAvailable(__FUNCTION__); +} + +bool SrtpSession::ProtectRtp(void* data, int in_len, int max_len, + int* out_len) { + return SrtpNotAvailable(__FUNCTION__); +} + +bool SrtpSession::ProtectRtcp(void* data, int in_len, int max_len, + int* out_len) { + return SrtpNotAvailable(__FUNCTION__); +} + +bool SrtpSession::UnprotectRtp(void* data, int in_len, int* out_len) { + return SrtpNotAvailable(__FUNCTION__); +} + +bool SrtpSession::UnprotectRtcp(void* data, int in_len, int* out_len) { + return SrtpNotAvailable(__FUNCTION__); +} + +void SrtpSession::set_signal_silent_time(uint32 signal_silent_time) { + // Do nothing. +} + +#endif // HAVE_SRTP + +/////////////////////////////////////////////////////////////////////////////// +// SrtpStat + +#ifdef HAVE_SRTP + +SrtpStat::SrtpStat() + : signal_silent_time_(1000) { +} + +void SrtpStat::AddProtectRtpResult(uint32 ssrc, int result) { + FailureKey key; + key.ssrc = ssrc; + key.mode = SrtpFilter::PROTECT; + switch (result) { + case err_status_ok: + key.error = SrtpFilter::ERROR_NONE; + break; + case err_status_auth_fail: + key.error = SrtpFilter::ERROR_AUTH; + break; + default: + key.error = SrtpFilter::ERROR_FAIL; + } + HandleSrtpResult(key); +} + +void SrtpStat::AddUnprotectRtpResult(uint32 ssrc, int result) { + FailureKey key; + key.ssrc = ssrc; + key.mode = SrtpFilter::UNPROTECT; + switch (result) { + case err_status_ok: + key.error = SrtpFilter::ERROR_NONE; + break; + case err_status_auth_fail: + key.error = SrtpFilter::ERROR_AUTH; + break; + case err_status_replay_fail: + case err_status_replay_old: + key.error = SrtpFilter::ERROR_REPLAY; + break; + default: + key.error = SrtpFilter::ERROR_FAIL; + } + HandleSrtpResult(key); +} + +void SrtpStat::AddProtectRtcpResult(int result) { + AddProtectRtpResult(0U, result); +} + +void SrtpStat::AddUnprotectRtcpResult(int result) { + AddUnprotectRtpResult(0U, result); +} + +void SrtpStat::HandleSrtpResult(const SrtpStat::FailureKey& key) { + // Handle some cases where error should be signalled right away. For other + // errors, trigger error for the first time seeing it. After that, silent + // the same error for a certain amount of time (default 1 sec). + if (key.error != SrtpFilter::ERROR_NONE) { + // For errors, signal first time and wait for 1 sec. + FailureStat* stat = &(failures_[key]); + uint32 current_time = talk_base::Time(); + if (stat->last_signal_time == 0 || + talk_base::TimeDiff(current_time, stat->last_signal_time) > + static_cast(signal_silent_time_)) { + SignalSrtpError(key.ssrc, key.mode, key.error); + stat->last_signal_time = current_time; + } + } +} + +#else // !HAVE_SRTP + +// On some systems, SRTP is not (yet) available. + +SrtpStat::SrtpStat() + : signal_silent_time_(1000) { + LOG(WARNING) << "SRTP implementation is missing."; +} + +void SrtpStat::AddProtectRtpResult(uint32 ssrc, int result) { + SrtpNotAvailable(__FUNCTION__); +} + +void SrtpStat::AddUnprotectRtpResult(uint32 ssrc, int result) { + SrtpNotAvailable(__FUNCTION__); +} + +void SrtpStat::AddProtectRtcpResult(int result) { + SrtpNotAvailable(__FUNCTION__); +} + +void SrtpStat::AddUnprotectRtcpResult(int result) { + SrtpNotAvailable(__FUNCTION__); +} + +void SrtpStat::HandleSrtpResult(const SrtpStat::FailureKey& key) { + SrtpNotAvailable(__FUNCTION__); +} + +#endif // HAVE_SRTP + +} // namespace cricket diff --git a/talk/session/media/srtpfilter.h b/talk/session/media/srtpfilter.h new file mode 100644 index 000000000..7d97eff6c --- /dev/null +++ b/talk/session/media/srtpfilter.h @@ -0,0 +1,304 @@ +/* + * libjingle + * Copyright 2009 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. + */ + +#ifndef TALK_SESSION_MEDIA_SRTPFILTER_H_ +#define TALK_SESSION_MEDIA_SRTPFILTER_H_ + +#include +#include +#include +#include + +#include "talk/base/basictypes.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslotrepeater.h" +#include "talk/media/base/cryptoparams.h" +#include "talk/p2p/base/sessiondescription.h" + +// Forward declaration to avoid pulling in libsrtp headers here +struct srtp_event_data_t; +struct srtp_ctx_t; +typedef srtp_ctx_t* srtp_t; +struct srtp_policy_t; + +namespace cricket { + +// Cipher suite to use for SRTP. Typically a 80-bit HMAC will be used, except +// in applications (voice) where the additional bandwidth may be significant. +// A 80-bit HMAC is always used for SRTCP. +// 128-bit AES with 80-bit SHA-1 HMAC. +extern const char CS_AES_CM_128_HMAC_SHA1_80[]; +// 128-bit AES with 32-bit SHA-1 HMAC. +extern const char CS_AES_CM_128_HMAC_SHA1_32[]; +// Key is 128 bits and salt is 112 bits == 30 bytes. B64 bloat => 40 bytes. +extern const int SRTP_MASTER_KEY_BASE64_LEN; + +// Needed for DTLS-SRTP +extern const int SRTP_MASTER_KEY_KEY_LEN; +extern const int SRTP_MASTER_KEY_SALT_LEN; + +class SrtpSession; +class SrtpStat; + +void EnableSrtpDebugging(); + +// Class to transform SRTP to/from RTP. +// Initialize by calling SetSend with the local security params, then call +// SetRecv once the remote security params are received. At that point +// Protect/UnprotectRt(c)p can be called to encrypt/decrypt data. +// TODO: Figure out concurrency policy for SrtpFilter. +class SrtpFilter { + public: + enum Mode { + PROTECT, + UNPROTECT + }; + enum Error { + ERROR_NONE, + ERROR_FAIL, + ERROR_AUTH, + ERROR_REPLAY, + }; + + SrtpFilter(); + ~SrtpFilter(); + + // Whether the filter is active (i.e. crypto has been properly negotiated). + bool IsActive() const; + + // Indicates which crypto algorithms and keys were contained in the offer. + // offer_params should contain a list of available parameters to use, or none, + // if crypto is not desired. This must be called before SetAnswer. + bool SetOffer(const std::vector& offer_params, + ContentSource source); + // Same as SetAnwer. But multiple calls are allowed to SetProvisionalAnswer + // after a call to SetOffer. + bool SetProvisionalAnswer(const std::vector& answer_params, + ContentSource source); + // Indicates which crypto algorithms and keys were contained in the answer. + // answer_params should contain the negotiated parameters, which may be none, + // if crypto was not desired or could not be negotiated (and not required). + // This must be called after SetOffer. If crypto negotiation completes + // successfully, this will advance the filter to the active state. + bool SetAnswer(const std::vector& answer_params, + ContentSource source); + + // Just set up both sets of keys directly. + // Used with DTLS-SRTP. + bool SetRtpParams(const std::string& send_cs, + const uint8* send_key, int send_key_len, + const std::string& recv_cs, + const uint8* recv_key, int recv_key_len); + bool SetRtcpParams(const std::string& send_cs, + const uint8* send_key, int send_key_len, + const std::string& recv_cs, + const uint8* recv_key, int recv_key_len); + + // Encrypts/signs an individual RTP/RTCP packet, in-place. + // If an HMAC is used, this will increase the packet size. + bool ProtectRtp(void* data, int in_len, int max_len, int* out_len); + bool ProtectRtcp(void* data, int in_len, int max_len, int* out_len); + // Decrypts/verifies an invidiual RTP/RTCP packet. + // If an HMAC is used, this will decrease the packet size. + bool UnprotectRtp(void* data, int in_len, int* out_len); + bool UnprotectRtcp(void* data, int in_len, int* out_len); + + // Update the silent threshold (in ms) for signaling errors. + void set_signal_silent_time(uint32 signal_silent_time_in_ms); + + sigslot::repeater3 SignalSrtpError; + + protected: + bool ExpectOffer(ContentSource source); + bool StoreParams(const std::vector& params, + ContentSource source); + bool ExpectAnswer(ContentSource source); + bool DoSetAnswer(const std::vector& answer_params, + ContentSource source, + bool final); + void CreateSrtpSessions(); + bool NegotiateParams(const std::vector& answer_params, + CryptoParams* selected_params); + bool ApplyParams(const CryptoParams& send_params, + const CryptoParams& recv_params); + bool ResetParams(); + static bool ParseKeyParams(const std::string& params, uint8* key, int len); + + private: + enum State { + ST_INIT, // SRTP filter unused. + ST_SENTOFFER, // Offer with SRTP parameters sent. + ST_RECEIVEDOFFER, // Offer with SRTP parameters received. + ST_SENTPRANSWER_NO_CRYPTO, // Sent provisional answer without crypto. + // Received provisional answer without crypto. + ST_RECEIVEDPRANSWER_NO_CRYPTO, + ST_ACTIVE, // Offer and answer set. + // SRTP filter is active but new parameters are offered. + // When the answer is set, the state transitions to ST_ACTIVE or ST_INIT. + ST_SENTUPDATEDOFFER, + // SRTP filter is active but new parameters are received. + // When the answer is set, the state transitions back to ST_ACTIVE. + ST_RECEIVEDUPDATEDOFFER, + // SRTP filter is active but the sent answer is only provisional. + // When the final answer is set, the state transitions to ST_ACTIVE or + // ST_INIT. + ST_SENTPRANSWER, + // SRTP filter is active but the received answer is only provisional. + // When the final answer is set, the state transitions to ST_ACTIVE or + // ST_INIT. + ST_RECEIVEDPRANSWER + }; + State state_; + uint32 signal_silent_time_in_ms_; + std::vector offer_params_; + talk_base::scoped_ptr send_session_; + talk_base::scoped_ptr recv_session_; + talk_base::scoped_ptr send_rtcp_session_; + talk_base::scoped_ptr recv_rtcp_session_; + CryptoParams applied_send_params_; + CryptoParams applied_recv_params_; +}; + +// Class that wraps a libSRTP session. +class SrtpSession { + public: + SrtpSession(); + ~SrtpSession(); + + // Configures the session for sending data using the specified + // cipher-suite and key. Receiving must be done by a separate session. + bool SetSend(const std::string& cs, const uint8* key, int len); + // Configures the session for receiving data using the specified + // cipher-suite and key. Sending must be done by a separate session. + bool SetRecv(const std::string& cs, const uint8* key, int len); + + // Encrypts/signs an individual RTP/RTCP packet, in-place. + // If an HMAC is used, this will increase the packet size. + bool ProtectRtp(void* data, int in_len, int max_len, int* out_len); + bool ProtectRtcp(void* data, int in_len, int max_len, int* out_len); + // Decrypts/verifies an invidiual RTP/RTCP packet. + // If an HMAC is used, this will decrease the packet size. + bool UnprotectRtp(void* data, int in_len, int* out_len); + bool UnprotectRtcp(void* data, int in_len, int* out_len); + + // Update the silent threshold (in ms) for signaling errors. + void set_signal_silent_time(uint32 signal_silent_time_in_ms); + + sigslot::repeater3 + SignalSrtpError; + + private: + bool SetKey(int type, const std::string& cs, const uint8* key, int len); + static bool Init(); + void HandleEvent(const srtp_event_data_t* ev); + static void HandleEventThunk(srtp_event_data_t* ev); + static std::list* sessions(); + + srtp_t session_; + int rtp_auth_tag_len_; + int rtcp_auth_tag_len_; + talk_base::scoped_ptr srtp_stat_; + static bool inited_; + int last_send_seq_num_; + DISALLOW_COPY_AND_ASSIGN(SrtpSession); +}; + +// Class that collects failures of SRTP. +class SrtpStat { + public: + SrtpStat(); + + // Report RTP protection results to the handler. + void AddProtectRtpResult(uint32 ssrc, int result); + // Report RTP unprotection results to the handler. + void AddUnprotectRtpResult(uint32 ssrc, int result); + // Report RTCP protection results to the handler. + void AddProtectRtcpResult(int result); + // Report RTCP unprotection results to the handler. + void AddUnprotectRtcpResult(int result); + + // Get silent time (in ms) for SRTP statistics handler. + uint32 signal_silent_time() const { return signal_silent_time_; } + // Set silent time (in ms) for SRTP statistics handler. + void set_signal_silent_time(uint32 signal_silent_time) { + signal_silent_time_ = signal_silent_time; + } + + // Sigslot for reporting errors. + sigslot::signal3 + SignalSrtpError; + + private: + // For each different ssrc and error, we collect statistics separately. + struct FailureKey { + FailureKey() + : ssrc(0), + mode(SrtpFilter::PROTECT), + error(SrtpFilter::ERROR_NONE) { + } + FailureKey(uint32 in_ssrc, SrtpFilter::Mode in_mode, + SrtpFilter::Error in_error) + : ssrc(in_ssrc), + mode(in_mode), + error(in_error) { + } + bool operator <(const FailureKey& key) const { + return ssrc < key.ssrc || mode < key.mode || error < key.error; + } + uint32 ssrc; + SrtpFilter::Mode mode; + SrtpFilter::Error error; + }; + // For tracing conditions for signaling, currently we only use + // last_signal_time. Wrap this as a struct so that later on, if we need any + // other improvements, it will be easier. + struct FailureStat { + FailureStat() + : last_signal_time(0) { + } + explicit FailureStat(uint32 in_last_signal_time) + : last_signal_time(in_last_signal_time) { + } + void Reset() { + last_signal_time = 0; + } + uint32 last_signal_time; + }; + + // Inspect SRTP result and signal error if needed. + void HandleSrtpResult(const FailureKey& key); + + std::map failures_; + // Threshold in ms to silent the signaling errors. + uint32 signal_silent_time_; + + DISALLOW_COPY_AND_ASSIGN(SrtpStat); +}; + +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_SRTPFILTER_H_ diff --git a/talk/session/media/srtpfilter_unittest.cc b/talk/session/media/srtpfilter_unittest.cc new file mode 100644 index 000000000..1b4aef279 --- /dev/null +++ b/talk/session/media/srtpfilter_unittest.cc @@ -0,0 +1,863 @@ +/* + * libjingle + * Copyright 2004 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/base/byteorder.h" +#include "talk/base/gunit.h" +#include "talk/base/thread.h" +#include "talk/media/base/cryptoparams.h" +#include "talk/media/base/fakertp.h" +#include "talk/p2p/base/sessiondescription.h" +#include "talk/session/media/srtpfilter.h" +#ifdef SRTP_RELATIVE_PATH +#include "crypto/include/err.h" +#else +#include "third_party/libsrtp/crypto/include/err.h" +#endif + +using cricket::CS_AES_CM_128_HMAC_SHA1_80; +using cricket::CS_AES_CM_128_HMAC_SHA1_32; +using cricket::CryptoParams; +using cricket::CS_LOCAL; +using cricket::CS_REMOTE; + +static const uint8 kTestKey1[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234"; +static const uint8 kTestKey2[] = "4321ZYXWVUTSRQPONMLKJIHGFEDCBA"; +static const int kTestKeyLen = 30; +static const std::string kTestKeyParams1 = + "inline:WVNfX19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz"; +static const std::string kTestKeyParams2 = + "inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR"; +static const std::string kTestKeyParams3 = + "inline:1234X19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz"; +static const std::string kTestKeyParams4 = + "inline:4567QCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR"; +static const cricket::CryptoParams kTestCryptoParams1( + 1, "AES_CM_128_HMAC_SHA1_80", kTestKeyParams1, ""); +static const cricket::CryptoParams kTestCryptoParams2( + 1, "AES_CM_128_HMAC_SHA1_80", kTestKeyParams2, ""); + +static int rtp_auth_tag_len(const std::string& cs) { + return (cs == CS_AES_CM_128_HMAC_SHA1_32) ? 4 : 10; +} +static int rtcp_auth_tag_len(const std::string& cs) { + return 10; +} + +class SrtpFilterTest : public testing::Test { + protected: + SrtpFilterTest() + // Need to initialize |sequence_number_|, the value does not matter. + : sequence_number_(1) { + } + static std::vector MakeVector(const CryptoParams& params) { + std::vector vec; + vec.push_back(params); + return vec; + } + void TestSetParams(const std::vector& params1, + const std::vector& params2) { + EXPECT_TRUE(f1_.SetOffer(params1, CS_LOCAL)); + EXPECT_TRUE(f2_.SetOffer(params1, CS_REMOTE)); + EXPECT_TRUE(f2_.SetAnswer(params2, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(params2, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + } + void TestProtectUnprotect(const std::string& cs1, const std::string& cs2) { + char rtp_packet[sizeof(kPcmuFrame) + 10]; + char original_rtp_packet[sizeof(kPcmuFrame)]; + char rtcp_packet[sizeof(kRtcpReport) + 4 + 10]; + int rtp_len = sizeof(kPcmuFrame), rtcp_len = sizeof(kRtcpReport), out_len; + memcpy(rtp_packet, kPcmuFrame, rtp_len); + // In order to be able to run this test function multiple times we can not + // use the same sequence number twice. Increase the sequence number by one. + talk_base::SetBE16(reinterpret_cast(rtp_packet) + 2, + ++sequence_number_); + memcpy(original_rtp_packet, rtp_packet, rtp_len); + memcpy(rtcp_packet, kRtcpReport, rtcp_len); + + EXPECT_TRUE(f1_.ProtectRtp(rtp_packet, rtp_len, + sizeof(rtp_packet), &out_len)); + EXPECT_EQ(out_len, rtp_len + rtp_auth_tag_len(cs1)); + EXPECT_NE(0, memcmp(rtp_packet, original_rtp_packet, rtp_len)); + EXPECT_TRUE(f2_.UnprotectRtp(rtp_packet, out_len, &out_len)); + EXPECT_EQ(rtp_len, out_len); + EXPECT_EQ(0, memcmp(rtp_packet, original_rtp_packet, rtp_len)); + + EXPECT_TRUE(f2_.ProtectRtp(rtp_packet, rtp_len, + sizeof(rtp_packet), &out_len)); + EXPECT_EQ(out_len, rtp_len + rtp_auth_tag_len(cs2)); + EXPECT_NE(0, memcmp(rtp_packet, original_rtp_packet, rtp_len)); + EXPECT_TRUE(f1_.UnprotectRtp(rtp_packet, out_len, &out_len)); + EXPECT_EQ(rtp_len, out_len); + EXPECT_EQ(0, memcmp(rtp_packet, original_rtp_packet, rtp_len)); + + EXPECT_TRUE(f1_.ProtectRtcp(rtcp_packet, rtcp_len, + sizeof(rtcp_packet), &out_len)); + EXPECT_EQ(out_len, rtcp_len + 4 + rtcp_auth_tag_len(cs1)); // NOLINT + EXPECT_NE(0, memcmp(rtcp_packet, kRtcpReport, rtcp_len)); + EXPECT_TRUE(f2_.UnprotectRtcp(rtcp_packet, out_len, &out_len)); + EXPECT_EQ(rtcp_len, out_len); + EXPECT_EQ(0, memcmp(rtcp_packet, kRtcpReport, rtcp_len)); + + EXPECT_TRUE(f2_.ProtectRtcp(rtcp_packet, rtcp_len, + sizeof(rtcp_packet), &out_len)); + EXPECT_EQ(out_len, rtcp_len + 4 + rtcp_auth_tag_len(cs2)); // NOLINT + EXPECT_NE(0, memcmp(rtcp_packet, kRtcpReport, rtcp_len)); + EXPECT_TRUE(f1_.UnprotectRtcp(rtcp_packet, out_len, &out_len)); + EXPECT_EQ(rtcp_len, out_len); + EXPECT_EQ(0, memcmp(rtcp_packet, kRtcpReport, rtcp_len)); + } + cricket::SrtpFilter f1_; + cricket::SrtpFilter f2_; + int sequence_number_; +}; + +// Test that we can set up the session and keys properly. +TEST_F(SrtpFilterTest, TestGoodSetupOneCipherSuite) { + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); +} + +// Test that we can set up things with multiple params. +TEST_F(SrtpFilterTest, TestGoodSetupMultipleCipherSuites) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + offer.push_back(kTestCryptoParams1); + offer[1].tag = 2; + offer[1].cipher_suite = CS_AES_CM_128_HMAC_SHA1_32; + answer[0].tag = 2; + answer[0].cipher_suite = CS_AES_CM_128_HMAC_SHA1_32; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); +} + +// Test that we handle the cases where crypto is not desired. +TEST_F(SrtpFilterTest, TestGoodSetupNoCipherSuites) { + std::vector offer, answer; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we handle the cases where crypto is not desired by the remote side. +TEST_F(SrtpFilterTest, TestGoodSetupNoAnswerCipherSuites) { + std::vector answer; + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail if we call the functions the wrong way. +TEST_F(SrtpFilterTest, TestBadSetup) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_LOCAL)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we can set offer multiple times from the same source. +TEST_F(SrtpFilterTest, TestGoodSetupMultipleOffers) { + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams2), CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams2), CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + + EXPECT_TRUE(f2_.SetOffer(MakeVector(kTestCryptoParams1), CS_REMOTE)); + EXPECT_TRUE(f2_.SetOffer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_TRUE(f2_.SetAnswer(MakeVector(kTestCryptoParams2), CS_LOCAL)); + EXPECT_TRUE(f2_.IsActive()); + EXPECT_TRUE(f2_.SetOffer(MakeVector(kTestCryptoParams1), CS_REMOTE)); + EXPECT_TRUE(f2_.SetOffer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_TRUE(f2_.SetAnswer(MakeVector(kTestCryptoParams2), CS_LOCAL)); +} +// Test that we can't set offer multiple times from different sources. +TEST_F(SrtpFilterTest, TestBadSetupMultipleOffers) { + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_FALSE(f1_.SetOffer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_TRUE(f1_.SetAnswer(MakeVector(kTestCryptoParams1), CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams2), CS_LOCAL)); + EXPECT_FALSE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_REMOTE)); + EXPECT_TRUE(f1_.SetAnswer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + + EXPECT_TRUE(f2_.SetOffer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_FALSE(f2_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_TRUE(f2_.SetAnswer(MakeVector(kTestCryptoParams2), CS_LOCAL)); + EXPECT_TRUE(f2_.IsActive()); + EXPECT_TRUE(f2_.SetOffer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_FALSE(f2_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_TRUE(f2_.SetAnswer(MakeVector(kTestCryptoParams2), CS_LOCAL)); +} + +// Test that we fail if we have params in the answer when none were offered. +TEST_F(SrtpFilterTest, TestNoAnswerCipherSuites) { + std::vector offer; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail if we have too many params in our answer. +TEST_F(SrtpFilterTest, TestMultipleAnswerCipherSuites) { + std::vector answer(MakeVector(kTestCryptoParams2)); + answer.push_back(kTestCryptoParams2); + answer[1].tag = 2; + answer[1].cipher_suite = CS_AES_CM_128_HMAC_SHA1_32; + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail if we don't support the cipher-suite. +TEST_F(SrtpFilterTest, TestInvalidCipherSuite) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + offer[0].cipher_suite = answer[0].cipher_suite = "FOO"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail if we can't agree on a tag. +TEST_F(SrtpFilterTest, TestNoMatchingTag) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].tag = 99; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail if we can't agree on a cipher-suite. +TEST_F(SrtpFilterTest, TestNoMatchingCipherSuite) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].tag = 2; + answer[0].cipher_suite = "FOO"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail keys with bad base64 content. +TEST_F(SrtpFilterTest, TestInvalidKeyData) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].key_params = "inline:!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail keys with the wrong key-method. +TEST_F(SrtpFilterTest, TestWrongKeyMethod) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].key_params = "outline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail keys of the wrong length. +TEST_F(SrtpFilterTest, TestKeyTooShort) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].key_params = "inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtx"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail keys of the wrong length. +TEST_F(SrtpFilterTest, TestKeyTooLong) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].key_params = "inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBRABCD"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail keys with lifetime or MKI set (since we don't support) +TEST_F(SrtpFilterTest, TestUnsupportedOptions) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].key_params = + "inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we can encrypt/decrypt after setting the same CryptoParams again on +// one side. +TEST_F(SrtpFilterTest, TestSettingSameKeyOnOneSide) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + TestSetParams(offer, answer); + + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_80, + CS_AES_CM_128_HMAC_SHA1_80); + + // Re-applying the same keys on one end and it should not reset the ROC. + EXPECT_TRUE(f2_.SetOffer(offer, CS_REMOTE)); + EXPECT_TRUE(f2_.SetAnswer(answer, CS_LOCAL)); + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_80, CS_AES_CM_128_HMAC_SHA1_80); +} + +// Test that we can encrypt/decrypt after negotiating AES_CM_128_HMAC_SHA1_80. +TEST_F(SrtpFilterTest, TestProtect_AES_CM_128_HMAC_SHA1_80) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + offer.push_back(kTestCryptoParams1); + offer[1].tag = 2; + offer[1].cipher_suite = CS_AES_CM_128_HMAC_SHA1_32; + TestSetParams(offer, answer); + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_80, CS_AES_CM_128_HMAC_SHA1_80); +} + +// Test that we can encrypt/decrypt after negotiating AES_CM_128_HMAC_SHA1_32. +TEST_F(SrtpFilterTest, TestProtect_AES_CM_128_HMAC_SHA1_32) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + offer.push_back(kTestCryptoParams1); + offer[1].tag = 2; + offer[1].cipher_suite = CS_AES_CM_128_HMAC_SHA1_32; + answer[0].tag = 2; + answer[0].cipher_suite = CS_AES_CM_128_HMAC_SHA1_32; + TestSetParams(offer, answer); + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_32, CS_AES_CM_128_HMAC_SHA1_32); +} + +// Test that we can change encryption parameters. +TEST_F(SrtpFilterTest, TestChangeParameters) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + + TestSetParams(offer, answer); + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_80, CS_AES_CM_128_HMAC_SHA1_80); + + // Change the key parameters and cipher_suite. + offer[0].key_params = kTestKeyParams3; + offer[0].cipher_suite = CS_AES_CM_128_HMAC_SHA1_32; + answer[0].key_params = kTestKeyParams4; + answer[0].cipher_suite = CS_AES_CM_128_HMAC_SHA1_32; + + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_TRUE(f2_.SetOffer(offer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f1_.IsActive()); + + // Test that the old keys are valid until the negotiation is complete. + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_80, CS_AES_CM_128_HMAC_SHA1_80); + + // Complete the negotiation and test that we can still understand each other. + EXPECT_TRUE(f2_.SetAnswer(answer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_32, CS_AES_CM_128_HMAC_SHA1_32); +} + +// Test that we can send and receive provisional answers with crypto enabled. +// Also test that we can change the crypto. +TEST_F(SrtpFilterTest, TestProvisionalAnswer) { + std::vector offer(MakeVector(kTestCryptoParams1)); + offer.push_back(kTestCryptoParams1); + offer[1].tag = 2; + offer[1].cipher_suite = CS_AES_CM_128_HMAC_SHA1_32; + std::vector answer(MakeVector(kTestCryptoParams2)); + + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_TRUE(f2_.SetOffer(offer, CS_REMOTE)); + EXPECT_TRUE(f2_.SetProvisionalAnswer(answer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetProvisionalAnswer(answer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f2_.IsActive()); + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_80, CS_AES_CM_128_HMAC_SHA1_80); + + answer[0].key_params = kTestKeyParams4; + answer[0].tag = 2; + answer[0].cipher_suite = CS_AES_CM_128_HMAC_SHA1_32; + EXPECT_TRUE(f2_.SetAnswer(answer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f2_.IsActive()); + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_32, CS_AES_CM_128_HMAC_SHA1_32); +} + +// Test that a provisional answer doesn't need to contain a crypto. +TEST_F(SrtpFilterTest, TestProvisionalAnswerWithoutCrypto) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer; + + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_TRUE(f2_.SetOffer(offer, CS_REMOTE)); + EXPECT_TRUE(f2_.SetProvisionalAnswer(answer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetProvisionalAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); + EXPECT_FALSE(f2_.IsActive()); + + answer.push_back(kTestCryptoParams2); + EXPECT_TRUE(f2_.SetAnswer(answer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f2_.IsActive()); + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_80, CS_AES_CM_128_HMAC_SHA1_80); +} + +// Test that we can disable encryption. +TEST_F(SrtpFilterTest, TestDisableEncryption) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + + TestSetParams(offer, answer); + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_80, CS_AES_CM_128_HMAC_SHA1_80); + + offer.clear(); + answer.clear(); + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_TRUE(f2_.SetOffer(offer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f2_.IsActive()); + + // Test that the old keys are valid until the negotiation is complete. + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_80, CS_AES_CM_128_HMAC_SHA1_80); + + // Complete the negotiation. + EXPECT_TRUE(f2_.SetAnswer(answer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + + EXPECT_FALSE(f1_.IsActive()); + EXPECT_FALSE(f2_.IsActive()); +} + +// Test directly setting the params with AES_CM_128_HMAC_SHA1_80 +TEST_F(SrtpFilterTest, TestProtect_SetParamsDirect_AES_CM_128_HMAC_SHA1_80) { + EXPECT_TRUE(f1_.SetRtpParams(CS_AES_CM_128_HMAC_SHA1_80, + kTestKey1, kTestKeyLen, + CS_AES_CM_128_HMAC_SHA1_80, + kTestKey2, kTestKeyLen)); + EXPECT_TRUE(f2_.SetRtpParams(CS_AES_CM_128_HMAC_SHA1_80, + kTestKey2, kTestKeyLen, + CS_AES_CM_128_HMAC_SHA1_80, + kTestKey1, kTestKeyLen)); + EXPECT_TRUE(f1_.SetRtcpParams(CS_AES_CM_128_HMAC_SHA1_80, + kTestKey1, kTestKeyLen, + CS_AES_CM_128_HMAC_SHA1_80, + kTestKey2, kTestKeyLen)); + EXPECT_TRUE(f2_.SetRtcpParams(CS_AES_CM_128_HMAC_SHA1_80, + kTestKey2, kTestKeyLen, + CS_AES_CM_128_HMAC_SHA1_80, + kTestKey1, kTestKeyLen)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f2_.IsActive()); + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_80, CS_AES_CM_128_HMAC_SHA1_80); +} + +// Test directly setting the params with AES_CM_128_HMAC_SHA1_32 +TEST_F(SrtpFilterTest, TestProtect_SetParamsDirect_AES_CM_128_HMAC_SHA1_32) { + EXPECT_TRUE(f1_.SetRtpParams(CS_AES_CM_128_HMAC_SHA1_32, + kTestKey1, kTestKeyLen, + CS_AES_CM_128_HMAC_SHA1_32, + kTestKey2, kTestKeyLen)); + EXPECT_TRUE(f2_.SetRtpParams(CS_AES_CM_128_HMAC_SHA1_32, + kTestKey2, kTestKeyLen, + CS_AES_CM_128_HMAC_SHA1_32, + kTestKey1, kTestKeyLen)); + EXPECT_TRUE(f1_.SetRtcpParams(CS_AES_CM_128_HMAC_SHA1_32, + kTestKey1, kTestKeyLen, + CS_AES_CM_128_HMAC_SHA1_32, + kTestKey2, kTestKeyLen)); + EXPECT_TRUE(f2_.SetRtcpParams(CS_AES_CM_128_HMAC_SHA1_32, + kTestKey2, kTestKeyLen, + CS_AES_CM_128_HMAC_SHA1_32, + kTestKey1, kTestKeyLen)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f2_.IsActive()); + TestProtectUnprotect(CS_AES_CM_128_HMAC_SHA1_32, CS_AES_CM_128_HMAC_SHA1_32); +} + +// Test directly setting the params with bogus keys +TEST_F(SrtpFilterTest, TestSetParamsKeyTooShort) { + EXPECT_FALSE(f1_.SetRtpParams(CS_AES_CM_128_HMAC_SHA1_80, + kTestKey1, kTestKeyLen - 1, + CS_AES_CM_128_HMAC_SHA1_80, + kTestKey1, kTestKeyLen - 1)); + EXPECT_FALSE(f1_.SetRtcpParams(CS_AES_CM_128_HMAC_SHA1_80, + kTestKey1, kTestKeyLen - 1, + CS_AES_CM_128_HMAC_SHA1_80, + kTestKey1, kTestKeyLen - 1)); +} + +class SrtpSessionTest : public testing::Test { + protected: + virtual void SetUp() { + rtp_len_ = sizeof(kPcmuFrame); + rtcp_len_ = sizeof(kRtcpReport); + memcpy(rtp_packet_, kPcmuFrame, rtp_len_); + memcpy(rtcp_packet_, kRtcpReport, rtcp_len_); + } + void TestProtectRtp(const std::string& cs) { + int out_len = 0; + EXPECT_TRUE(s1_.ProtectRtp(rtp_packet_, rtp_len_, + sizeof(rtp_packet_), &out_len)); + EXPECT_EQ(out_len, rtp_len_ + rtp_auth_tag_len(cs)); + EXPECT_NE(0, memcmp(rtp_packet_, kPcmuFrame, rtp_len_)); + rtp_len_ = out_len; + } + void TestProtectRtcp(const std::string& cs) { + int out_len = 0; + EXPECT_TRUE(s1_.ProtectRtcp(rtcp_packet_, rtcp_len_, + sizeof(rtcp_packet_), &out_len)); + EXPECT_EQ(out_len, rtcp_len_ + 4 + rtcp_auth_tag_len(cs)); // NOLINT + EXPECT_NE(0, memcmp(rtcp_packet_, kRtcpReport, rtcp_len_)); + rtcp_len_ = out_len; + } + void TestUnprotectRtp(const std::string& cs) { + int out_len = 0, expected_len = sizeof(kPcmuFrame); + EXPECT_TRUE(s2_.UnprotectRtp(rtp_packet_, rtp_len_, &out_len)); + EXPECT_EQ(expected_len, out_len); + EXPECT_EQ(0, memcmp(rtp_packet_, kPcmuFrame, out_len)); + } + void TestUnprotectRtcp(const std::string& cs) { + int out_len = 0, expected_len = sizeof(kRtcpReport); + EXPECT_TRUE(s2_.UnprotectRtcp(rtcp_packet_, rtcp_len_, &out_len)); + EXPECT_EQ(expected_len, out_len); + EXPECT_EQ(0, memcmp(rtcp_packet_, kRtcpReport, out_len)); + } + cricket::SrtpSession s1_; + cricket::SrtpSession s2_; + char rtp_packet_[sizeof(kPcmuFrame) + 10]; + char rtcp_packet_[sizeof(kRtcpReport) + 4 + 10]; + int rtp_len_; + int rtcp_len_; +}; + +// Test that we can set up the session and keys properly. +TEST_F(SrtpSessionTest, TestGoodSetup) { + EXPECT_TRUE(s1_.SetSend(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, kTestKeyLen)); + EXPECT_TRUE(s2_.SetRecv(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, kTestKeyLen)); +} + +// Test that we can't change the keys once set. +TEST_F(SrtpSessionTest, TestBadSetup) { + EXPECT_TRUE(s1_.SetSend(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, kTestKeyLen)); + EXPECT_TRUE(s2_.SetRecv(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, kTestKeyLen)); + EXPECT_FALSE(s1_.SetSend(CS_AES_CM_128_HMAC_SHA1_80, kTestKey2, kTestKeyLen)); + EXPECT_FALSE(s2_.SetRecv(CS_AES_CM_128_HMAC_SHA1_80, kTestKey2, kTestKeyLen)); +} + +// Test that we fail keys of the wrong length. +TEST_F(SrtpSessionTest, TestKeysTooShort) { + EXPECT_FALSE(s1_.SetSend(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, 1)); + EXPECT_FALSE(s2_.SetRecv(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, 1)); +} + +// Test that we can encrypt and decrypt RTP/RTCP using AES_CM_128_HMAC_SHA1_80. +TEST_F(SrtpSessionTest, TestProtect_AES_CM_128_HMAC_SHA1_80) { + EXPECT_TRUE(s1_.SetSend(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, kTestKeyLen)); + EXPECT_TRUE(s2_.SetRecv(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, kTestKeyLen)); + TestProtectRtp(CS_AES_CM_128_HMAC_SHA1_80); + TestProtectRtcp(CS_AES_CM_128_HMAC_SHA1_80); + TestUnprotectRtp(CS_AES_CM_128_HMAC_SHA1_80); + TestUnprotectRtcp(CS_AES_CM_128_HMAC_SHA1_80); +} + +// Test that we can encrypt and decrypt RTP/RTCP using AES_CM_128_HMAC_SHA1_32. +TEST_F(SrtpSessionTest, TestProtect_AES_CM_128_HMAC_SHA1_32) { + EXPECT_TRUE(s1_.SetSend(CS_AES_CM_128_HMAC_SHA1_32, kTestKey1, kTestKeyLen)); + EXPECT_TRUE(s2_.SetRecv(CS_AES_CM_128_HMAC_SHA1_32, kTestKey1, kTestKeyLen)); + TestProtectRtp(CS_AES_CM_128_HMAC_SHA1_32); + TestProtectRtcp(CS_AES_CM_128_HMAC_SHA1_32); + TestUnprotectRtp(CS_AES_CM_128_HMAC_SHA1_32); + TestUnprotectRtcp(CS_AES_CM_128_HMAC_SHA1_32); +} + +// Test that we fail to unprotect if someone tampers with the RTP/RTCP paylaods. +TEST_F(SrtpSessionTest, TestTamperReject) { + int out_len; + EXPECT_TRUE(s1_.SetSend(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, kTestKeyLen)); + EXPECT_TRUE(s2_.SetRecv(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, kTestKeyLen)); + TestProtectRtp(CS_AES_CM_128_HMAC_SHA1_80); + TestProtectRtcp(CS_AES_CM_128_HMAC_SHA1_80); + rtp_packet_[0] = 0x12; + rtcp_packet_[1] = 0x34; + EXPECT_FALSE(s2_.UnprotectRtp(rtp_packet_, rtp_len_, &out_len)); + EXPECT_FALSE(s2_.UnprotectRtcp(rtcp_packet_, rtcp_len_, &out_len)); +} + +// Test that we fail to unprotect if the payloads are not authenticated. +TEST_F(SrtpSessionTest, TestUnencryptReject) { + int out_len; + EXPECT_TRUE(s1_.SetSend(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, kTestKeyLen)); + EXPECT_TRUE(s2_.SetRecv(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, kTestKeyLen)); + EXPECT_FALSE(s2_.UnprotectRtp(rtp_packet_, rtp_len_, &out_len)); + EXPECT_FALSE(s2_.UnprotectRtcp(rtcp_packet_, rtcp_len_, &out_len)); +} + +// Test that we fail when using buffers that are too small. +TEST_F(SrtpSessionTest, TestBuffersTooSmall) { + int out_len; + EXPECT_TRUE(s1_.SetSend(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, kTestKeyLen)); + EXPECT_FALSE(s1_.ProtectRtp(rtp_packet_, rtp_len_, + sizeof(rtp_packet_) - 10, &out_len)); + EXPECT_FALSE(s1_.ProtectRtcp(rtcp_packet_, rtcp_len_, + sizeof(rtcp_packet_) - 14, &out_len)); +} + +TEST_F(SrtpSessionTest, TestReplay) { + static const uint16 kMaxSeqnum = static_cast(-1); + static const uint16 seqnum_big = 62275; + static const uint16 seqnum_small = 10; + static const uint16 replay_window = 1024; + int out_len; + + EXPECT_TRUE(s1_.SetSend(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, kTestKeyLen)); + EXPECT_TRUE(s2_.SetRecv(CS_AES_CM_128_HMAC_SHA1_80, kTestKey1, kTestKeyLen)); + + // Initial sequence number. + talk_base::SetBE16(reinterpret_cast(rtp_packet_) + 2, seqnum_big); + EXPECT_TRUE(s1_.ProtectRtp(rtp_packet_, rtp_len_, sizeof(rtp_packet_), + &out_len)); + + // Replay within the 1024 window should succeed. + talk_base::SetBE16(reinterpret_cast(rtp_packet_) + 2, + seqnum_big - replay_window + 1); + EXPECT_TRUE(s1_.ProtectRtp(rtp_packet_, rtp_len_, sizeof(rtp_packet_), + &out_len)); + + // Replay out side of the 1024 window should fail. + talk_base::SetBE16(reinterpret_cast(rtp_packet_) + 2, + seqnum_big - replay_window - 1); + EXPECT_FALSE(s1_.ProtectRtp(rtp_packet_, rtp_len_, sizeof(rtp_packet_), + &out_len)); + + // Increment sequence number to a small number. + talk_base::SetBE16(reinterpret_cast(rtp_packet_) + 2, seqnum_small); + EXPECT_TRUE(s1_.ProtectRtp(rtp_packet_, rtp_len_, sizeof(rtp_packet_), + &out_len)); + + // Replay around 0 but out side of the 1024 window should fail. + talk_base::SetBE16(reinterpret_cast(rtp_packet_) + 2, + kMaxSeqnum + seqnum_small - replay_window - 1); + EXPECT_FALSE(s1_.ProtectRtp(rtp_packet_, rtp_len_, sizeof(rtp_packet_), + &out_len)); + + // Replay around 0 but within the 1024 window should succeed. + for (uint16 seqnum = 65000; seqnum < 65003; ++seqnum) { + talk_base::SetBE16(reinterpret_cast(rtp_packet_) + 2, seqnum); + EXPECT_TRUE(s1_.ProtectRtp(rtp_packet_, rtp_len_, sizeof(rtp_packet_), + &out_len)); + } + + // Go back to normal sequence nubmer. + // NOTE: without the fix in libsrtp, this would fail. This is because + // without the fix, the loop above would keep incrementing local sequence + // number in libsrtp, eventually the new sequence number would go out side + // of the window. + talk_base::SetBE16(reinterpret_cast(rtp_packet_) + 2, + seqnum_small + 1); + EXPECT_TRUE(s1_.ProtectRtp(rtp_packet_, rtp_len_, sizeof(rtp_packet_), + &out_len)); +} + +class SrtpStatTest + : public testing::Test, + public sigslot::has_slots<> { + public: + SrtpStatTest() + : ssrc_(0U), + mode_(-1), + error_(cricket::SrtpFilter::ERROR_NONE) { + srtp_stat_.SignalSrtpError.connect(this, &SrtpStatTest::OnSrtpError); + srtp_stat_.set_signal_silent_time(200); + } + + protected: + void OnSrtpError(uint32 ssrc, cricket::SrtpFilter::Mode mode, + cricket::SrtpFilter::Error error) { + ssrc_ = ssrc; + mode_ = mode; + error_ = error; + } + void Reset() { + ssrc_ = 0U; + mode_ = -1; + error_ = cricket::SrtpFilter::ERROR_NONE; + } + + cricket::SrtpStat srtp_stat_; + uint32 ssrc_; + int mode_; + cricket::SrtpFilter::Error error_; + + private: + DISALLOW_COPY_AND_ASSIGN(SrtpStatTest); +}; + +TEST_F(SrtpStatTest, TestProtectRtpError) { + Reset(); + srtp_stat_.AddProtectRtpResult(1, err_status_ok); + EXPECT_EQ(0U, ssrc_); + EXPECT_EQ(-1, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_NONE, error_); + Reset(); + srtp_stat_.AddProtectRtpResult(1, err_status_auth_fail); + EXPECT_EQ(1U, ssrc_); + EXPECT_EQ(cricket::SrtpFilter::PROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_AUTH, error_); + Reset(); + srtp_stat_.AddProtectRtpResult(1, err_status_fail); + EXPECT_EQ(1U, ssrc_); + EXPECT_EQ(cricket::SrtpFilter::PROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_FAIL, error_); + // Within 200ms, the error will not be triggered. + Reset(); + srtp_stat_.AddProtectRtpResult(1, err_status_fail); + EXPECT_EQ(0U, ssrc_); + EXPECT_EQ(-1, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_NONE, error_); + // Now the error will be triggered again. + Reset(); + talk_base::Thread::Current()->SleepMs(210); + srtp_stat_.AddProtectRtpResult(1, err_status_fail); + EXPECT_EQ(1U, ssrc_); + EXPECT_EQ(cricket::SrtpFilter::PROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_FAIL, error_); +} + +TEST_F(SrtpStatTest, TestUnprotectRtpError) { + Reset(); + srtp_stat_.AddUnprotectRtpResult(1, err_status_ok); + EXPECT_EQ(0U, ssrc_); + EXPECT_EQ(-1, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_NONE, error_); + Reset(); + srtp_stat_.AddUnprotectRtpResult(1, err_status_auth_fail); + EXPECT_EQ(1U, ssrc_); + EXPECT_EQ(cricket::SrtpFilter::UNPROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_AUTH, error_); + Reset(); + srtp_stat_.AddUnprotectRtpResult(1, err_status_replay_fail); + EXPECT_EQ(1U, ssrc_); + EXPECT_EQ(cricket::SrtpFilter::UNPROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_REPLAY, error_); + Reset(); + talk_base::Thread::Current()->SleepMs(210); + srtp_stat_.AddUnprotectRtpResult(1, err_status_replay_old); + EXPECT_EQ(1U, ssrc_); + EXPECT_EQ(cricket::SrtpFilter::UNPROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_REPLAY, error_); + Reset(); + srtp_stat_.AddUnprotectRtpResult(1, err_status_fail); + EXPECT_EQ(1U, ssrc_); + EXPECT_EQ(cricket::SrtpFilter::UNPROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_FAIL, error_); + // Within 200ms, the error will not be triggered. + Reset(); + srtp_stat_.AddUnprotectRtpResult(1, err_status_fail); + EXPECT_EQ(0U, ssrc_); + EXPECT_EQ(-1, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_NONE, error_); + // Now the error will be triggered again. + Reset(); + talk_base::Thread::Current()->SleepMs(210); + srtp_stat_.AddUnprotectRtpResult(1, err_status_fail); + EXPECT_EQ(1U, ssrc_); + EXPECT_EQ(cricket::SrtpFilter::UNPROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_FAIL, error_); +} + +TEST_F(SrtpStatTest, TestProtectRtcpError) { + Reset(); + srtp_stat_.AddProtectRtcpResult(err_status_ok); + EXPECT_EQ(-1, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_NONE, error_); + Reset(); + srtp_stat_.AddProtectRtcpResult(err_status_auth_fail); + EXPECT_EQ(cricket::SrtpFilter::PROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_AUTH, error_); + Reset(); + srtp_stat_.AddProtectRtcpResult(err_status_fail); + EXPECT_EQ(cricket::SrtpFilter::PROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_FAIL, error_); + // Within 200ms, the error will not be triggered. + Reset(); + srtp_stat_.AddProtectRtcpResult(err_status_fail); + EXPECT_EQ(-1, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_NONE, error_); + // Now the error will be triggered again. + Reset(); + talk_base::Thread::Current()->SleepMs(210); + srtp_stat_.AddProtectRtcpResult(err_status_fail); + EXPECT_EQ(cricket::SrtpFilter::PROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_FAIL, error_); +} + +TEST_F(SrtpStatTest, TestUnprotectRtcpError) { + Reset(); + srtp_stat_.AddUnprotectRtcpResult(err_status_ok); + EXPECT_EQ(-1, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_NONE, error_); + Reset(); + srtp_stat_.AddUnprotectRtcpResult(err_status_auth_fail); + EXPECT_EQ(cricket::SrtpFilter::UNPROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_AUTH, error_); + Reset(); + srtp_stat_.AddUnprotectRtcpResult(err_status_replay_fail); + EXPECT_EQ(cricket::SrtpFilter::UNPROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_REPLAY, error_); + Reset(); + talk_base::Thread::Current()->SleepMs(210); + srtp_stat_.AddUnprotectRtcpResult(err_status_replay_fail); + EXPECT_EQ(cricket::SrtpFilter::UNPROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_REPLAY, error_); + Reset(); + srtp_stat_.AddUnprotectRtcpResult(err_status_fail); + EXPECT_EQ(cricket::SrtpFilter::UNPROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_FAIL, error_); + // Within 200ms, the error will not be triggered. + Reset(); + srtp_stat_.AddUnprotectRtcpResult(err_status_fail); + EXPECT_EQ(-1, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_NONE, error_); + // Now the error will be triggered again. + Reset(); + talk_base::Thread::Current()->SleepMs(210); + srtp_stat_.AddUnprotectRtcpResult(err_status_fail); + EXPECT_EQ(cricket::SrtpFilter::UNPROTECT, mode_); + EXPECT_EQ(cricket::SrtpFilter::ERROR_FAIL, error_); +} diff --git a/talk/session/media/ssrcmuxfilter.cc b/talk/session/media/ssrcmuxfilter.cc new file mode 100644 index 000000000..638167d18 --- /dev/null +++ b/talk/session/media/ssrcmuxfilter.cc @@ -0,0 +1,93 @@ +/* + * libjingle + * Copyright 2004 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/media/ssrcmuxfilter.h" + +#include + +#include "talk/base/logging.h" +#include "talk/media/base/rtputils.h" + +namespace cricket { + +static const uint32 kSsrc01 = 0x01; + +SsrcMuxFilter::SsrcMuxFilter() { +} + +SsrcMuxFilter::~SsrcMuxFilter() { +} + +bool SsrcMuxFilter::IsActive() const { + return !streams_.empty(); +} + +bool SsrcMuxFilter::DemuxPacket(const char* data, size_t len, bool rtcp) { + uint32 ssrc = 0; + if (!rtcp) { + GetRtpSsrc(data, len, &ssrc); + } else { + int pl_type = 0; + if (!GetRtcpType(data, len, &pl_type)) return false; + if (pl_type == kRtcpTypeSDES) { + // SDES packet parsing not supported. + LOG(LS_INFO) << "SDES packet received for demux."; + return true; + } else { + if (!GetRtcpSsrc(data, len, &ssrc)) return false; + if (ssrc == kSsrc01) { + // SSRC 1 has a special meaning and indicates generic feedback on + // some systems and should never be dropped. If it is forwarded + // incorrectly it will be ignored by lower layers anyway. + return true; + } + } + } + return FindStream(ssrc); +} + +bool SsrcMuxFilter::AddStream(const StreamParams& stream) { + if (GetStreamBySsrc(streams_, stream.first_ssrc(), NULL)) { + LOG(LS_WARNING) << "Stream already added to filter"; + return false; + } + streams_.push_back(stream); + return true; +} + +bool SsrcMuxFilter::RemoveStream(uint32 ssrc) { + return RemoveStreamBySsrc(&streams_, ssrc); +} + +bool SsrcMuxFilter::FindStream(uint32 ssrc) const { + if (ssrc == 0) { + return false; + } + return (GetStreamBySsrc(streams_, ssrc, NULL)); +} + +} // namespace cricket diff --git a/talk/session/media/ssrcmuxfilter.h b/talk/session/media/ssrcmuxfilter.h new file mode 100644 index 000000000..9420f54cc --- /dev/null +++ b/talk/session/media/ssrcmuxfilter.h @@ -0,0 +1,67 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_SESSION_MEDIA_SSRCMUXFILTER_H_ +#define TALK_SESSION_MEDIA_SSRCMUXFILTER_H_ + +#include + +#include "talk/base/basictypes.h" +#include "talk/media/base/streamparams.h" + +namespace cricket { + +// This class maintains list of recv SSRC's destined for cricket::BaseChannel. +// In case of single RTP session and single transport channel, all session +// ( or media) channels share a common transport channel. Hence they all get +// SignalReadPacket when packet received on transport channel. This requires +// cricket::BaseChannel to know all the valid sources, else media channel +// will decode invalid packets. +class SsrcMuxFilter { + public: + SsrcMuxFilter(); + ~SsrcMuxFilter(); + + // Whether the rtp mux is active for a sdp session. + // Returns true if the filter contains a stream. + bool IsActive() const; + // Determines packet belongs to valid cricket::BaseChannel. + bool DemuxPacket(const char* data, size_t len, bool rtcp); + // Adding a valid source to the filter. + bool AddStream(const StreamParams& stream); + // Removes source from the filter. + bool RemoveStream(uint32 ssrc); + // Utility method added for unitest. + bool FindStream(uint32 ssrc) const; + + private: + std::vector streams_; +}; + +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_SSRCMUXFILTER_H_ diff --git a/talk/session/media/ssrcmuxfilter_unittest.cc b/talk/session/media/ssrcmuxfilter_unittest.cc new file mode 100644 index 000000000..85a4dbe50 --- /dev/null +++ b/talk/session/media/ssrcmuxfilter_unittest.cc @@ -0,0 +1,184 @@ +/* + * libjingle + * Copyright 2004 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/base/gunit.h" +#include "talk/session/media/ssrcmuxfilter.h" + +static const int kSsrc1 = 0x1111; +static const int kSsrc2 = 0x2222; +static const int kSsrc3 = 0x3333; + +using cricket::StreamParams; + +// SSRC = 0x1111 +static const unsigned char kRtpPacketSsrc1[] = { + 0x80, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x11, +}; + +// SSRC = 0x2222 +static const unsigned char kRtpPacketSsrc2[] = { + 0x80, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x22, +}; + +// SSRC = 0 +static const unsigned char kRtpPacketInvalidSsrc[] = { + 0x80, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +// invalid size +static const unsigned char kRtpPacketTooSmall[] = { + 0x80, 0x80, 0x00, 0x00, +}; + +// PT = 200 = SR, len = 28, SSRC of sender = 0x0001 +// NTP TS = 0, RTP TS = 0, packet count = 0 +static const unsigned char kRtcpPacketSrSsrc01[] = { + 0x80, 0xC8, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + +// PT = 200 = SR, len = 28, SSRC of sender = 0x2222 +// NTP TS = 0, RTP TS = 0, packet count = 0 +static const unsigned char kRtcpPacketSrSsrc2[] = { + 0x80, 0xC8, 0x00, 0x1B, 0x00, 0x00, 0x22, 0x22, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + +// First packet - SR = PT = 200, len = 0, SSRC of sender = 0x1111 +// NTP TS = 0, RTP TS = 0, packet count = 0 +// second packet - SDES = PT = 202, count = 0, SSRC = 0x1111, cname len = 0 +static const unsigned char kRtcpPacketCompoundSrSdesSsrc1[] = { + 0x80, 0xC8, 0x00, 0x01, 0x00, 0x00, 0x11, 0x11, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x81, 0xCA, 0x00, 0x00, 0x00, 0x00, 0x11, 0x11, 0x01, 0x00, +}; + +// SDES = PT = 202, count = 0, SSRC = 0x2222, cname len = 0 +static const unsigned char kRtcpPacketSdesSsrc2[] = { + 0x81, 0xCA, 0x00, 0x00, 0x00, 0x00, 0x22, 0x22, 0x01, 0x00, +}; + +// Packet has only mandatory fixed RTCP header +static const unsigned char kRtcpPacketFixedHeaderOnly[] = { + 0x80, 0xC8, 0x00, 0x00, +}; + +// Small packet for SSRC demux. +static const unsigned char kRtcpPacketTooSmall[] = { + 0x80, 0xC8, 0x00, 0x00, 0x00, 0x00, +}; + +// PT = 206, FMT = 1, Sender SSRC = 0x1111, Media SSRC = 0x1111 +// No FCI information is needed for PLI. +static const unsigned char kRtcpPacketNonCompoundRtcpPliFeedback[] = { + 0x81, 0xCE, 0x00, 0x0C, 0x00, 0x00, 0x11, 0x11, 0x00, 0x00, 0x11, 0x11, +}; + +TEST(SsrcMuxFilterTest, AddRemoveStreamTest) { + cricket::SsrcMuxFilter ssrc_filter; + EXPECT_FALSE(ssrc_filter.IsActive()); + EXPECT_TRUE(ssrc_filter.AddStream(StreamParams::CreateLegacy(kSsrc1))); + StreamParams stream2; + stream2.ssrcs.push_back(kSsrc2); + stream2.ssrcs.push_back(kSsrc3); + EXPECT_TRUE(ssrc_filter.AddStream(stream2)); + + EXPECT_TRUE(ssrc_filter.IsActive()); + EXPECT_TRUE(ssrc_filter.FindStream(kSsrc1)); + EXPECT_TRUE(ssrc_filter.FindStream(kSsrc2)); + EXPECT_TRUE(ssrc_filter.FindStream(kSsrc3)); + EXPECT_TRUE(ssrc_filter.RemoveStream(kSsrc1)); + EXPECT_FALSE(ssrc_filter.FindStream(kSsrc1)); + EXPECT_TRUE(ssrc_filter.RemoveStream(kSsrc3)); + EXPECT_FALSE(ssrc_filter.RemoveStream(kSsrc2)); // Already removed. + EXPECT_FALSE(ssrc_filter.IsActive()); +} + +TEST(SsrcMuxFilterTest, RtpPacketTest) { + cricket::SsrcMuxFilter ssrc_filter; + EXPECT_TRUE(ssrc_filter.AddStream(StreamParams::CreateLegacy(kSsrc1))); + EXPECT_TRUE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtpPacketSsrc1), + sizeof(kRtpPacketSsrc1), false)); + EXPECT_TRUE(ssrc_filter.AddStream(StreamParams::CreateLegacy(kSsrc2))); + EXPECT_TRUE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtpPacketSsrc2), + sizeof(kRtpPacketSsrc2), false)); + EXPECT_TRUE(ssrc_filter.RemoveStream(kSsrc2)); + EXPECT_FALSE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtpPacketSsrc2), + sizeof(kRtpPacketSsrc2), false)); + EXPECT_FALSE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtpPacketInvalidSsrc), + sizeof(kRtpPacketInvalidSsrc), false)); + EXPECT_FALSE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtpPacketTooSmall), + sizeof(kRtpPacketTooSmall), false)); +} + +TEST(SsrcMuxFilterTest, RtcpPacketTest) { + cricket::SsrcMuxFilter ssrc_filter; + EXPECT_TRUE(ssrc_filter.AddStream(StreamParams::CreateLegacy(kSsrc1))); + EXPECT_TRUE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtcpPacketCompoundSrSdesSsrc1), + sizeof(kRtcpPacketCompoundSrSdesSsrc1), true)); + EXPECT_TRUE(ssrc_filter.AddStream(StreamParams::CreateLegacy(kSsrc2))); + EXPECT_TRUE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtcpPacketSrSsrc2), + sizeof(kRtcpPacketSrSsrc2), true)); + EXPECT_TRUE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtcpPacketSdesSsrc2), + sizeof(kRtcpPacketSdesSsrc2), true)); + EXPECT_TRUE(ssrc_filter.RemoveStream(kSsrc2)); + // RTCP Packets other than SR and RR are demuxed regardless of SSRC. + EXPECT_TRUE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtcpPacketSdesSsrc2), + sizeof(kRtcpPacketSdesSsrc2), true)); + // RTCP Packets with 'special' SSRC 0x01 are demuxed also + EXPECT_TRUE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtcpPacketSrSsrc01), + sizeof(kRtcpPacketSrSsrc01), true)); + EXPECT_FALSE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtcpPacketSrSsrc2), + sizeof(kRtcpPacketSrSsrc2), true)); + EXPECT_FALSE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtcpPacketFixedHeaderOnly), + sizeof(kRtcpPacketFixedHeaderOnly), true)); + EXPECT_FALSE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtcpPacketTooSmall), + sizeof(kRtcpPacketTooSmall), true)); + EXPECT_TRUE(ssrc_filter.DemuxPacket( + reinterpret_cast(kRtcpPacketNonCompoundRtcpPliFeedback), + sizeof(kRtcpPacketNonCompoundRtcpPliFeedback), true)); +} diff --git a/talk/session/media/typewrapping.h.pump b/talk/session/media/typewrapping.h.pump new file mode 100644 index 000000000..3b529277f --- /dev/null +++ b/talk/session/media/typewrapping.h.pump @@ -0,0 +1,297 @@ +// To generate typewrapping.h from typewrapping.h.pump, execute: +// /home/build/google3/third_party/gtest/scripts/pump.py typewrapping.h.pump + +// Copyright 2009 Google Inc. +// Author: tschmelcher@google.com (Tristan Schmelcher) +// +// A template meta-programming framework for customizable rule-based +// type-checking of type wrappers and wrapper functions. +// +// This framework is useful in a scenario where there are a set of types that +// you choose to "wrap" by implementing new preferred types such that the new +// and the old can be converted back and forth in some way, but you already have +// a library of functions that expect the original types. Example: +// +// Type A wraps X +// Type B wraps Y +// Type C wraps Z +// +// And function X Foo(Y, Z) exists. +// +// Since A, B, and C are preferred, you choose to implement a wrapper function +// with this interface: +// +// A Foo2(B, C) +// +// However, this can lead to subtle discrepancies, because if the interface to +// Foo ever changes then Foo2 may become out-of-sync. e.g., Foo might have +// originally returned void, but later is changed to return an error code. If +// the programmer forgets to change Foo2, the code will probably still work, but +// with an implicit cast to void inserted by the compiler, potentially leading +// to run-time errors or errors in usage. +// +// The purpose of this library is to prevent these discrepancies from occurring. +// You use it as follows: +// +// First, declare a new wrapping ruleset: +// +// DECLARE_WRAPPING_RULESET(ruleset_name) +// +// Then declare rules on what types wrap which other types and how to convert +// them: +// +// DECLARE_WRAPPER(ruleset_name, A, X, variable_name, wrapping_code, +// unwrapping_code) +// +// Where wrapping_code and unwrapping_code are expressions giving the code to +// use to wrap and unwrap a variable with the name "variable_name". There are +// also some helper macros to declare common wrapping schemes. +// +// Then implement your wrapped functions like this: +// +// A Foo_Wrapped(B b, C c) { +// return WRAP_CALL2(ruleset_name, A, Foo, B, b, C, c); +// } +// +// WRAP_CALL2 will unwrap b and c (if B and C are wrapped types) and call Foo, +// then wrap the result to type A if different from the return type. More +// importantly, if the types in Foo's interface do not _exactly_ match the +// unwrapped forms of A, B, and C (after typedef-equivalence), then you will get +// a compile-time error for a static_cast from the real function type to the +// expected one (except on Mac where this check is infeasible), and with no icky +// template instantiation errors either! +// +// There are also macros to wrap/unwrap individual values according to whichever +// rule applies to their types: +// +// WRAP(ruleset_name, A, X, value) // Compile-time error if no associated rule. +// +// UNWRAP(ruleset_name, A, value) // Infers X. If A is not a wrapper, no change. +// +// UNWRAP_TYPE(ruleset_name, A) // Evaluates to X. +// +// +// Essentially, the library works by "storing" the DECLARE_WRAPPER calls in +// template specializations. When the wrapper or unwrapper is invoked, the +// normal C++ template system essentially "looks up" the rule for the given +// type(s). +// +// All of the auto-generated code can be inlined to produce zero impact on +// run-time performance and code size (though some compilers may require +// gentle encouragement in order for them to do so). + +#ifndef TALK_SESSION_PHONE_TYPEWRAPPING_H_ +#define TALK_SESSION_PHONE_TYPEWRAPPING_H_ + +#include "talk/base/common.h" + +#ifdef OSX +// XCode's GCC doesn't respect typedef-equivalence when casting function pointer +// types, so we can't enforce that the wrapped function signatures strictly +// match the expected types. Instead we have to forego the nice user-friendly +// static_cast check (because it will spuriously fail) and make the Call() +// function into a member template below. +#define CAST_FUNCTION_(function, ...) \ + function +#else +#define CAST_FUNCTION_(function, ...) \ + static_cast<__VA_ARGS__>(function) +#endif + +// Internal helper macros. +#define SMART_WRAPPER_(wrapper, toType, fromType, from) \ + (wrapper::Wrap(from)) + +#define SMART_UNWRAPPER_(unwrapper, fromType, from) \ + (unwrapper::Unwrap(from)) + +#define SMART_UNWRAPPER_TYPE_(unwrapper, fromType) \ + typename unwrapper::ToType + +$var n = 27 +$range i 0..n + +$for i [[ +$range j 1..i + +// The code that follows wraps calls to $i-argument functions, unwrapping the +// arguments and wrapping the return value as needed. + +// The usual case. +template< + template class Wrapper, + template class Unwrapper, + typename ReturnType$for j [[, + typename ArgType$j]]> +class SmartFunctionWrapper$i { + public: + typedef SMART_UNWRAPPER_TYPE_(Unwrapper, ReturnType) OriginalReturnType; + +$for j [[ + typedef SMART_UNWRAPPER_TYPE_(Unwrapper, ArgType$j) OriginalArgType$j; + +]] + typedef OriginalReturnType (*OriginalFunctionType)($for j , [[ + + OriginalArgType$j]]); + +#ifdef OSX + template + static FORCE_INLINE ReturnType Call(F function +#else + static FORCE_INLINE ReturnType Call(OriginalFunctionType function +#endif + $for j [[, + ArgType$j v$j]]) { + return SMART_WRAPPER_(Wrapper, ReturnType, OriginalReturnType, + (*function)($for j , [[ + + SMART_UNWRAPPER_(Unwrapper, ArgType$j, v$j)]])); + } +}; + +// Special case for functions that return void. (SMART_WRAPPER_ involves +// passing the unwrapped value in a function call, which is not a legal thing to +// do with void, so we need a special case here that doesn't call +// SMART_WRAPPER_()). +template< + template class Wrapper, + template class Unwrapper$for j [[, + typename ArgType$j]]> +class SmartFunctionWrapper$i< + Wrapper, + Unwrapper, + void$for j [[, + ArgType$j]]> { + public: + typedef void OriginalReturnType; + +$for j [[ + typedef SMART_UNWRAPPER_TYPE_(Unwrapper, ArgType$j) OriginalArgType$j; + +]] + typedef OriginalReturnType (*OriginalFunctionType)($for j , [[ + + OriginalArgType$j]]); + +#ifdef OSX + template + static FORCE_INLINE void Call(F function +#else + static FORCE_INLINE void Call(OriginalFunctionType function +#endif + $for j [[, + ArgType$j v$j]]) { + (*function)($for j , [[ + + SMART_UNWRAPPER_(Unwrapper, ArgType$j, v$j)]]); + } +}; + + +]] +// Programmer interface follows. Only macros below here should be used outside +// this file. + +#define DECLARE_WRAPPING_RULESET(ruleSet) \ + namespace ruleSet { \ +\ + /* SmartWrapper is for wrapping values. */ \ + template \ + class SmartWrapper; \ +\ + /* Special case where the types are the same. */ \ + template \ + class SmartWrapper { \ + public: \ + static FORCE_INLINE T1 Wrap(T1 from) { \ + return from; \ + } \ + }; \ +\ + /* Class for unwrapping (i.e., going to the original value). This is done + function-style rather than predicate-style. The default rule is to leave + the type unchanged. */ \ + template \ + class SmartUnwrapper { \ + public: \ + typedef FromType ToType; \ + static FORCE_INLINE ToType Unwrap(FromType from) { \ + return from; \ + } \ + }; \ +\ + } + +// Declares a wrapping rule. +#define DECLARE_WRAPPER(ruleSet, wrappedType, unwrappedType, var, wrapCode, unwrapCode) \ + namespace ruleSet { \ +\ + template<> \ + class SmartWrapper { \ + public: \ + static FORCE_INLINE wrappedType Wrap(unwrappedType var) { \ + return wrapCode; \ + } \ + }; \ +\ + template<> \ + class SmartUnwrapper { \ + public: \ + typedef unwrappedType ToType; \ + static FORCE_INLINE unwrappedType Unwrap(wrappedType var) { \ + return unwrapCode; \ + } \ + }; \ +\ + } + +// Helper macro for declaring a wrapper that wraps/unwraps with reinterpret_cast<>. +#define DECLARE_WRAPPER_BY_REINTERPRET_CAST(ruleSet, wrappedType, unwrappedType) \ + DECLARE_WRAPPER(ruleSet, wrappedType, unwrappedType, FROM, reinterpret_cast(FROM), reinterpret_cast(FROM)) + +// Helper macro for declaring a wrapper that wraps/unwraps implicitly. +#define DECLARE_WRAPPER_BY_IMPLICIT_CAST(ruleSet, wrappedType, unwrappedType) \ + DECLARE_WRAPPER(ruleSet, wrappedType, unwrappedType, FROM, FROM, FROM) + +// Helper macro for declaring that the pointer types for one type wrap the pointer types for another type. +#define DECLARE_POINTER_WRAPPER(ruleSet, wrappedType, unwrappedType) \ + DECLARE_WRAPPER_BY_REINTERPRET_CAST(ruleSet, wrappedType*, unwrappedType*) \ + DECLARE_WRAPPER_BY_REINTERPRET_CAST(ruleSet, const wrappedType*, const unwrappedType*) \ + DECLARE_WRAPPER_BY_REINTERPRET_CAST(ruleSet, wrappedType* const, unwrappedType* const) \ + DECLARE_WRAPPER_BY_REINTERPRET_CAST(ruleSet, const wrappedType* const, const unwrappedType* const) \ + +// Macro to wrap a single value. +#define WRAP(ruleSet, toType, fromType, from) \ + SMART_WRAPPER_(ruleSet::SmartWrapper, toType, fromType, from) + +// Macro to unwrap a single value. +#define UNWRAP(ruleSet, fromType, from) \ + SMART_UNWRAPPER_(ruleSet::SmartUnwrapper, fromType, from) + +// Macro to get the unwrapped form of a type. +#define UNWRAP_TYPE(ruleSet, fromType) \ + SMART_UNWRAPPER_TYPE_(ruleSet::SmartUnwrapper, from) + +// Macros to wrap function calls. + +$for i [[ +$range j 1..i +#define WRAP_CALL$i(ruleSet, toType, function$for j [[, argType$j, arg$j]]) \ + (SmartFunctionWrapper$i< \ + ruleSet::SmartWrapper, \ + ruleSet::SmartUnwrapper, \ + toType$for j [[, \ + argType$j]]>::Call( \ + CAST_FUNCTION_( \ + &function, \ + SmartFunctionWrapper$i< \ + ruleSet::SmartWrapper, \ + ruleSet::SmartUnwrapper, \ + toType$for j [[, \ + argType$j]]>::OriginalFunctionType)$for j [[, \ + arg$j]])) + +]] + +#endif // TALK_SESSION_PHONE_TYPEWRAPPINGHELPERS_H_ diff --git a/talk/session/media/typingmonitor.cc b/talk/session/media/typingmonitor.cc new file mode 100644 index 000000000..3c5d387b8 --- /dev/null +++ b/talk/session/media/typingmonitor.cc @@ -0,0 +1,123 @@ +/* + * libjingle + * Copyright 2004 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/media/typingmonitor.h" + +#include "talk/base/logging.h" +#include "talk/base/thread.h" +#include "talk/session/media/channel.h" + +namespace cricket { + +TypingMonitor::TypingMonitor(VoiceChannel* channel, + talk_base::Thread* worker_thread, + const TypingMonitorOptions& settings) + : channel_(channel), + worker_thread_(worker_thread), + mute_period_(settings.mute_period), + muted_at_(0), + has_pending_unmute_(false) { + channel_->media_channel()->SignalMediaError.connect( + this, &TypingMonitor::OnVoiceChannelError); + channel_->media_channel()->SetTypingDetectionParameters( + settings.time_window, settings.cost_per_typing, + settings.reporting_threshold, settings.penalty_decay, + settings.type_event_delay); +} + +TypingMonitor::~TypingMonitor() { + // Shortcut any pending unmutes. + if (has_pending_unmute_) { + talk_base::MessageList messages; + worker_thread_->Clear(this, 0, &messages); + ASSERT(messages.size() == 1); + channel_->MuteStream(0, false); + SignalMuted(channel_, false); + } +} + +void TypingMonitor::OnVoiceChannelError(uint32 ssrc, + VoiceMediaChannel::Error error) { + if (error == VoiceMediaChannel::ERROR_REC_TYPING_NOISE_DETECTED && + !channel_->IsStreamMuted(0)) { + // Please be careful and cognizant about threading issues when editing this + // code. The MuteStream() call below is a ::Send and is synchronous as well + // as the muted signal that comes from this. This function can be called + // from any thread. + + // TODO(perkj): Refactor TypingMonitor and the MediaChannel to handle + // multiple sending audio streams. SSRC 0 means the default sending audio + // channel. + channel_->MuteStream(0, true); + SignalMuted(channel_, true); + has_pending_unmute_ = true; + muted_at_ = talk_base::Time(); + + worker_thread_->PostDelayed(mute_period_, this, 0); + LOG(LS_INFO) << "Muting for at least " << mute_period_ << "ms."; + } +} + +/** + * If we mute due to detected typing and the user also mutes during our waiting + * period, we don't want to undo their mute. So, clear our callback. Should + * be called on the worker_thread. + */ +void TypingMonitor::OnChannelMuted() { + if (has_pending_unmute_) { + talk_base::MessageList removed; + worker_thread_->Clear(this, 0, &removed); + ASSERT(removed.size() == 1); + has_pending_unmute_ = false; + } +} + +/** + * When the specified mute period has elapsed, unmute, or, if the user kept + * typing after the initial warning fired, wait for the remainder of time to + * elapse since they finished and try to unmute again. Should be called on the + * worker thread. + */ +void TypingMonitor::OnMessage(talk_base::Message* msg) { + if (!channel_->IsStreamMuted(0) || !has_pending_unmute_) return; + int silence_period = channel_->media_channel()->GetTimeSinceLastTyping(); + int expiry_time = mute_period_ - silence_period; + if (silence_period < 0 || expiry_time < 50) { + LOG(LS_INFO) << "Mute timeout hit, last typing " << silence_period + << "ms ago, unmuting after " << talk_base::TimeSince(muted_at_) + << "ms total."; + has_pending_unmute_ = false; + channel_->MuteStream(0, false); + SignalMuted(channel_, false); + } else { + LOG(LS_INFO) << "Mute timeout hit, last typing " << silence_period + << "ms ago, check again in " << expiry_time << "ms."; + talk_base::Thread::Current()->PostDelayed(expiry_time, this, 0); + } +} + +} // namespace cricket diff --git a/talk/session/media/typingmonitor.h b/talk/session/media/typingmonitor.h new file mode 100644 index 000000000..c9b64e79c --- /dev/null +++ b/talk/session/media/typingmonitor.h @@ -0,0 +1,84 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef TALK_SESSION_MEDIA_TYPINGMONITOR_H_ +#define TALK_SESSION_MEDIA_TYPINGMONITOR_H_ + +#include "talk/base/messagehandler.h" +#include "talk/media/base/mediachannel.h" + +namespace talk_base { +class Thread; +} + +namespace cricket { + +class VoiceChannel; +class BaseChannel; + +struct TypingMonitorOptions { + int cost_per_typing; + int mute_period; + int penalty_decay; + int reporting_threshold; + int time_window; + int type_event_delay; + size_t min_participants; +}; + +/** + * An object that observes a channel and listens for typing detection warnings, + * which can be configured to mute audio capture of that channel for some period + * of time. The purpose is to automatically mute someone if they are disturbing + * a conference with loud keystroke audio signals. + */ +class TypingMonitor + : public talk_base::MessageHandler, public sigslot::has_slots<> { + public: + TypingMonitor(VoiceChannel* channel, talk_base::Thread* worker_thread, + const TypingMonitorOptions& params); + ~TypingMonitor(); + + sigslot::signal2 SignalMuted; + + void OnChannelMuted(); + + private: + void OnVoiceChannelError(uint32 ssrc, VoiceMediaChannel::Error error); + void OnMessage(talk_base::Message* msg); + + VoiceChannel* channel_; + talk_base::Thread* worker_thread_; + int mute_period_; + int muted_at_; + bool has_pending_unmute_; +}; + +} // namespace cricket + +#endif // TALK_SESSION_MEDIA_TYPINGMONITOR_H_ + diff --git a/talk/session/media/typingmonitor_unittest.cc b/talk/session/media/typingmonitor_unittest.cc new file mode 100644 index 000000000..eb8c5bc54 --- /dev/null +++ b/talk/session/media/typingmonitor_unittest.cc @@ -0,0 +1,92 @@ +/* + * libjingle + * Copyright 2004 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/base/gunit.h" +#include "talk/media/base/fakemediaengine.h" +#include "talk/p2p/base/fakesession.h" +#include "talk/session/media/channel.h" +#include "talk/session/media/currentspeakermonitor.h" +#include "talk/session/media/typingmonitor.h" + +namespace cricket { + +class TypingMonitorTest : public testing::Test { + protected: + TypingMonitorTest() : session_(true) { + vc_.reset(new VoiceChannel(talk_base::Thread::Current(), &engine_, + engine_.CreateChannel(), &session_, "", false)); + engine_.GetVoiceChannel(0)->set_time_since_last_typing(1000); + + TypingMonitorOptions settings = {10, 20, 30, 40, 50}; + monitor_.reset(new TypingMonitor(vc_.get(), + talk_base::Thread::Current(), + settings)); + } + + void TearDown() { + vc_.reset(); + } + + talk_base::scoped_ptr monitor_; + talk_base::scoped_ptr vc_; + FakeMediaEngine engine_; + FakeSession session_; +}; + +TEST_F(TypingMonitorTest, TestTriggerMute) { + EXPECT_FALSE(vc_->IsStreamMuted(0)); + EXPECT_FALSE(engine_.GetVoiceChannel(0)->IsStreamMuted(0)); + + engine_.GetVoiceChannel(0)->TriggerError(0, VoiceMediaChannel::ERROR_OTHER); + EXPECT_FALSE(vc_->IsStreamMuted(0)); + EXPECT_FALSE(engine_.GetVoiceChannel(0)->IsStreamMuted(0)); + + engine_.GetVoiceChannel(0)->TriggerError( + 0, VoiceMediaChannel::ERROR_REC_TYPING_NOISE_DETECTED); + EXPECT_TRUE(vc_->IsStreamMuted(0)); + EXPECT_TRUE(engine_.GetVoiceChannel(0)->IsStreamMuted(0)); + + EXPECT_TRUE_WAIT(!vc_->IsStreamMuted(0) && + !engine_.GetVoiceChannel(0)->IsStreamMuted(0), 100); +} + +TEST_F(TypingMonitorTest, TestResetMonitor) { + engine_.GetVoiceChannel(0)->set_time_since_last_typing(1000); + EXPECT_FALSE(vc_->IsStreamMuted(0)); + EXPECT_FALSE(engine_.GetVoiceChannel(0)->IsStreamMuted(0)); + + engine_.GetVoiceChannel(0)->TriggerError( + 0, VoiceMediaChannel::ERROR_REC_TYPING_NOISE_DETECTED); + EXPECT_TRUE(vc_->IsStreamMuted(0)); + EXPECT_TRUE(engine_.GetVoiceChannel(0)->IsStreamMuted(0)); + + monitor_.reset(); + EXPECT_FALSE(vc_->IsStreamMuted(0)); + EXPECT_FALSE(engine_.GetVoiceChannel(0)->IsStreamMuted(0)); +} + +} // namespace cricket diff --git a/talk/session/media/voicechannel.h b/talk/session/media/voicechannel.h new file mode 100644 index 000000000..6c1b6afdd --- /dev/null +++ b/talk/session/media/voicechannel.h @@ -0,0 +1,33 @@ +/* + * libjingle + * Copyright 2004 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. + */ + +#ifndef _VOICECHANNEL_H_ +#define _VOICECHANNEL_H_ + +#include "talk/session/media/channel.h" + +#endif // _VOICECHANNEL_H_ diff --git a/talk/session/tunnel/pseudotcpchannel.cc b/talk/session/tunnel/pseudotcpchannel.cc new file mode 100644 index 000000000..8b9a19f0b --- /dev/null +++ b/talk/session/tunnel/pseudotcpchannel.cc @@ -0,0 +1,600 @@ +/* + * 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 +#include "talk/base/basictypes.h" +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringutils.h" +#include "talk/p2p/base/candidate.h" +#include "talk/p2p/base/transportchannel.h" +#include "pseudotcpchannel.h" + +using namespace talk_base; + +namespace cricket { + +extern const talk_base::ConstantLabel SESSION_STATES[]; + +// MSG_WK_* - worker thread messages +// MSG_ST_* - stream thread messages +// MSG_SI_* - signal thread messages + +enum { + MSG_WK_CLOCK = 1, + MSG_WK_PURGE, + MSG_ST_EVENT, + MSG_SI_DESTROYCHANNEL, + MSG_SI_DESTROY, +}; + +struct EventData : public MessageData { + int event, error; + EventData(int ev, int err = 0) : event(ev), error(err) { } +}; + +/////////////////////////////////////////////////////////////////////////////// +// PseudoTcpChannel::InternalStream +/////////////////////////////////////////////////////////////////////////////// + +class PseudoTcpChannel::InternalStream : public StreamInterface { +public: + InternalStream(PseudoTcpChannel* parent); + virtual ~InternalStream(); + + virtual StreamState GetState() const; + virtual StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error); + virtual StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + virtual void Close(); + +private: + // parent_ is accessed and modified exclusively on the event thread, to + // avoid thread contention. This means that the PseudoTcpChannel cannot go + // away until after it receives a Close() from TunnelStream. + PseudoTcpChannel* parent_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// PseudoTcpChannel +// Member object lifetime summaries: +// session_ - passed in constructor, cleared when channel_ goes away. +// channel_ - created in Connect, destroyed when session_ or tcp_ goes away. +// tcp_ - created in Connect, destroyed when channel_ goes away, or connection +// closes. +// worker_thread_ - created when channel_ is created, purged when channel_ is +// destroyed. +// stream_ - created in GetStream, destroyed by owner at arbitrary time. +// this - created in constructor, destroyed when worker_thread_ and stream_ +// are both gone. +/////////////////////////////////////////////////////////////////////////////// + +// +// Signal thread methods +// + +PseudoTcpChannel::PseudoTcpChannel(Thread* stream_thread, Session* session) + : signal_thread_(session->session_manager()->signaling_thread()), + worker_thread_(NULL), + stream_thread_(stream_thread), + session_(session), channel_(NULL), tcp_(NULL), stream_(NULL), + stream_readable_(false), pending_read_event_(false), + ready_to_connect_(false) { + ASSERT(signal_thread_->IsCurrent()); + ASSERT(NULL != session_); +} + +PseudoTcpChannel::~PseudoTcpChannel() { + ASSERT(signal_thread_->IsCurrent()); + ASSERT(worker_thread_ == NULL); + ASSERT(session_ == NULL); + ASSERT(channel_ == NULL); + ASSERT(stream_ == NULL); + ASSERT(tcp_ == NULL); +} + +bool PseudoTcpChannel::Connect(const std::string& content_name, + const std::string& channel_name, + int component) { + ASSERT(signal_thread_->IsCurrent()); + CritScope lock(&cs_); + + if (channel_) + return false; + + ASSERT(session_ != NULL); + worker_thread_ = session_->session_manager()->worker_thread(); + content_name_ = content_name; + channel_ = session_->CreateChannel( + content_name, channel_name, component); + channel_name_ = channel_name; + channel_->SetOption(Socket::OPT_DONTFRAGMENT, 1); + + channel_->SignalDestroyed.connect(this, + &PseudoTcpChannel::OnChannelDestroyed); + channel_->SignalWritableState.connect(this, + &PseudoTcpChannel::OnChannelWritableState); + channel_->SignalReadPacket.connect(this, + &PseudoTcpChannel::OnChannelRead); + channel_->SignalRouteChange.connect(this, + &PseudoTcpChannel::OnChannelConnectionChanged); + + ASSERT(tcp_ == NULL); + tcp_ = new PseudoTcp(this, 0); + if (session_->initiator()) { + // Since we may try several protocols and network adapters that won't work, + // waiting until we get our first writable notification before initiating + // TCP negotiation. + ready_to_connect_ = true; + } + + return true; +} + +StreamInterface* PseudoTcpChannel::GetStream() { + ASSERT(signal_thread_->IsCurrent()); + CritScope lock(&cs_); + ASSERT(NULL != session_); + if (!stream_) + stream_ = new PseudoTcpChannel::InternalStream(this); + //TODO("should we disallow creation of new stream at some point?"); + return stream_; +} + +void PseudoTcpChannel::OnChannelDestroyed(TransportChannel* channel) { + LOG_F(LS_INFO) << "(" << channel->component() << ")"; + ASSERT(signal_thread_->IsCurrent()); + CritScope lock(&cs_); + ASSERT(channel == channel_); + signal_thread_->Clear(this, MSG_SI_DESTROYCHANNEL); + // When MSG_WK_PURGE is received, we know there will be no more messages from + // the worker thread. + worker_thread_->Clear(this, MSG_WK_CLOCK); + worker_thread_->Post(this, MSG_WK_PURGE); + session_ = NULL; + channel_ = NULL; + if ((stream_ != NULL) + && ((tcp_ == NULL) || (tcp_->State() != PseudoTcp::TCP_CLOSED))) + stream_thread_->Post(this, MSG_ST_EVENT, new EventData(SE_CLOSE, 0)); + if (tcp_) { + tcp_->Close(true); + AdjustClock(); + } + SignalChannelClosed(this); +} + +void PseudoTcpChannel::OnSessionTerminate(Session* session) { + // When the session terminates before we even connected + CritScope lock(&cs_); + if (session_ != NULL && channel_ == NULL) { + ASSERT(session == session_); + ASSERT(worker_thread_ == NULL); + ASSERT(tcp_ == NULL); + LOG(LS_INFO) << "Destroying unconnected PseudoTcpChannel"; + session_ = NULL; + if (stream_ != NULL) + stream_thread_->Post(this, MSG_ST_EVENT, new EventData(SE_CLOSE, -1)); + } + + // Even though session_ is being destroyed, we mustn't clear the pointer, + // since we'll need it to tear down channel_. + // + // TODO: Is it always the case that if channel_ != NULL then we'll get + // a channel-destroyed notification? +} + +void PseudoTcpChannel::GetOption(PseudoTcp::Option opt, int* value) { + ASSERT(signal_thread_->IsCurrent()); + CritScope lock(&cs_); + ASSERT(tcp_ != NULL); + tcp_->GetOption(opt, value); +} + +void PseudoTcpChannel::SetOption(PseudoTcp::Option opt, int value) { + ASSERT(signal_thread_->IsCurrent()); + CritScope lock(&cs_); + ASSERT(tcp_ != NULL); + tcp_->SetOption(opt, value); +} + +// +// Stream thread methods +// + +StreamState PseudoTcpChannel::GetState() const { + ASSERT(stream_ != NULL && stream_thread_->IsCurrent()); + CritScope lock(&cs_); + if (!session_) + return SS_CLOSED; + if (!tcp_) + return SS_OPENING; + switch (tcp_->State()) { + case PseudoTcp::TCP_LISTEN: + case PseudoTcp::TCP_SYN_SENT: + case PseudoTcp::TCP_SYN_RECEIVED: + return SS_OPENING; + case PseudoTcp::TCP_ESTABLISHED: + return SS_OPEN; + case PseudoTcp::TCP_CLOSED: + default: + return SS_CLOSED; + } +} + +StreamResult PseudoTcpChannel::Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + ASSERT(stream_ != NULL && stream_thread_->IsCurrent()); + CritScope lock(&cs_); + if (!tcp_) + return SR_BLOCK; + + stream_readable_ = false; + int result = tcp_->Recv(static_cast(buffer), buffer_len); + //LOG_F(LS_VERBOSE) << "Recv returned: " << result; + if (result > 0) { + if (read) + *read = result; + // PseudoTcp doesn't currently support repeated Readable signals. Simulate + // them here. + stream_readable_ = true; + if (!pending_read_event_) { + pending_read_event_ = true; + stream_thread_->Post(this, MSG_ST_EVENT, new EventData(SE_READ), true); + } + return SR_SUCCESS; + } else if (IsBlockingError(tcp_->GetError())) { + return SR_BLOCK; + } else { + if (error) + *error = tcp_->GetError(); + return SR_ERROR; + } + // This spot is never reached. +} + +StreamResult PseudoTcpChannel::Write(const void* data, size_t data_len, + size_t* written, int* error) { + ASSERT(stream_ != NULL && stream_thread_->IsCurrent()); + CritScope lock(&cs_); + if (!tcp_) + return SR_BLOCK; + int result = tcp_->Send(static_cast(data), data_len); + //LOG_F(LS_VERBOSE) << "Send returned: " << result; + if (result > 0) { + if (written) + *written = result; + return SR_SUCCESS; + } else if (IsBlockingError(tcp_->GetError())) { + return SR_BLOCK; + } else { + if (error) + *error = tcp_->GetError(); + return SR_ERROR; + } + // This spot is never reached. +} + +void PseudoTcpChannel::Close() { + ASSERT(stream_ != NULL && stream_thread_->IsCurrent()); + CritScope lock(&cs_); + stream_ = NULL; + // Clear out any pending event notifications + stream_thread_->Clear(this, MSG_ST_EVENT); + if (tcp_) { + tcp_->Close(false); + AdjustClock(); + } else { + CheckDestroy(); + } +} + +// +// Worker thread methods +// + +void PseudoTcpChannel::OnChannelWritableState(TransportChannel* channel) { + LOG_F(LS_VERBOSE) << "[" << channel_name_ << "]"; + ASSERT(worker_thread_->IsCurrent()); + CritScope lock(&cs_); + if (!channel_) { + LOG_F(LS_WARNING) << "NULL channel"; + return; + } + ASSERT(channel == channel_); + if (!tcp_) { + LOG_F(LS_WARNING) << "NULL tcp"; + return; + } + if (!ready_to_connect_ || !channel->writable()) + return; + + ready_to_connect_ = false; + tcp_->Connect(); + AdjustClock(); +} + +void PseudoTcpChannel::OnChannelRead(TransportChannel* channel, + const char* data, size_t size, int flags) { + //LOG_F(LS_VERBOSE) << "(" << size << ")"; + ASSERT(worker_thread_->IsCurrent()); + CritScope lock(&cs_); + if (!channel_) { + LOG_F(LS_WARNING) << "NULL channel"; + return; + } + ASSERT(channel == channel_); + if (!tcp_) { + LOG_F(LS_WARNING) << "NULL tcp"; + return; + } + tcp_->NotifyPacket(data, size); + AdjustClock(); +} + +void PseudoTcpChannel::OnChannelConnectionChanged(TransportChannel* channel, + const Candidate& candidate) { + LOG_F(LS_VERBOSE) << "[" << channel_name_ << "]"; + ASSERT(worker_thread_->IsCurrent()); + CritScope lock(&cs_); + if (!channel_) { + LOG_F(LS_WARNING) << "NULL channel"; + return; + } + ASSERT(channel == channel_); + if (!tcp_) { + LOG_F(LS_WARNING) << "NULL tcp"; + return; + } + + uint16 mtu = 1280; // safe default + int family = candidate.address().family(); + Socket* socket = + worker_thread_->socketserver()->CreateAsyncSocket(family, SOCK_DGRAM); + talk_base::scoped_ptr mtu_socket(socket); + if (socket == NULL) { + LOG_F(LS_WARNING) << "Couldn't create socket while estimating MTU."; + } else { + if (mtu_socket->Connect(candidate.address()) < 0 || + mtu_socket->EstimateMTU(&mtu) < 0) { + LOG_F(LS_WARNING) << "Failed to estimate MTU, error=" + << mtu_socket->GetError(); + } + } + + LOG_F(LS_VERBOSE) << "Using MTU of " << mtu << " bytes"; + tcp_->NotifyMTU(mtu); + AdjustClock(); +} + +void PseudoTcpChannel::OnTcpOpen(PseudoTcp* tcp) { + LOG_F(LS_VERBOSE) << "[" << channel_name_ << "]"; + ASSERT(cs_.CurrentThreadIsOwner()); + ASSERT(worker_thread_->IsCurrent()); + ASSERT(tcp == tcp_); + if (stream_) { + stream_readable_ = true; + pending_read_event_ = true; + stream_thread_->Post(this, MSG_ST_EVENT, + new EventData(SE_OPEN | SE_READ | SE_WRITE)); + } +} + +void PseudoTcpChannel::OnTcpReadable(PseudoTcp* tcp) { + //LOG_F(LS_VERBOSE); + ASSERT(cs_.CurrentThreadIsOwner()); + ASSERT(worker_thread_->IsCurrent()); + ASSERT(tcp == tcp_); + if (stream_) { + stream_readable_ = true; + if (!pending_read_event_) { + pending_read_event_ = true; + stream_thread_->Post(this, MSG_ST_EVENT, new EventData(SE_READ)); + } + } +} + +void PseudoTcpChannel::OnTcpWriteable(PseudoTcp* tcp) { + //LOG_F(LS_VERBOSE); + ASSERT(cs_.CurrentThreadIsOwner()); + ASSERT(worker_thread_->IsCurrent()); + ASSERT(tcp == tcp_); + if (stream_) + stream_thread_->Post(this, MSG_ST_EVENT, new EventData(SE_WRITE)); +} + +void PseudoTcpChannel::OnTcpClosed(PseudoTcp* tcp, uint32 nError) { + LOG_F(LS_VERBOSE) << "[" << channel_name_ << "]"; + ASSERT(cs_.CurrentThreadIsOwner()); + ASSERT(worker_thread_->IsCurrent()); + ASSERT(tcp == tcp_); + if (stream_) + stream_thread_->Post(this, MSG_ST_EVENT, new EventData(SE_CLOSE, nError)); +} + +// +// Multi-thread methods +// + +void PseudoTcpChannel::OnMessage(Message* pmsg) { + if (pmsg->message_id == MSG_WK_CLOCK) { + + ASSERT(worker_thread_->IsCurrent()); + //LOG(LS_INFO) << "PseudoTcpChannel::OnMessage(MSG_WK_CLOCK)"; + CritScope lock(&cs_); + if (tcp_) { + tcp_->NotifyClock(PseudoTcp::Now()); + AdjustClock(false); + } + + } else if (pmsg->message_id == MSG_WK_PURGE) { + + ASSERT(worker_thread_->IsCurrent()); + LOG_F(LS_INFO) << "(MSG_WK_PURGE)"; + // At this point, we know there are no additional worker thread messages. + CritScope lock(&cs_); + ASSERT(NULL == session_); + ASSERT(NULL == channel_); + worker_thread_ = NULL; + CheckDestroy(); + + } else if (pmsg->message_id == MSG_ST_EVENT) { + + ASSERT(stream_thread_->IsCurrent()); + //LOG(LS_INFO) << "PseudoTcpChannel::OnMessage(MSG_ST_EVENT, " + // << data->event << ", " << data->error << ")"; + ASSERT(stream_ != NULL); + EventData* data = static_cast(pmsg->pdata); + if (data->event & SE_READ) { + CritScope lock(&cs_); + pending_read_event_ = false; + } + stream_->SignalEvent(stream_, data->event, data->error); + delete data; + + } else if (pmsg->message_id == MSG_SI_DESTROYCHANNEL) { + + ASSERT(signal_thread_->IsCurrent()); + LOG_F(LS_INFO) << "(MSG_SI_DESTROYCHANNEL)"; + ASSERT(session_ != NULL); + ASSERT(channel_ != NULL); + session_->DestroyChannel(content_name_, channel_->component()); + + } else if (pmsg->message_id == MSG_SI_DESTROY) { + + ASSERT(signal_thread_->IsCurrent()); + LOG_F(LS_INFO) << "(MSG_SI_DESTROY)"; + // The message queue is empty, so it is safe to destroy ourselves. + delete this; + + } else { + ASSERT(false); + } +} + +IPseudoTcpNotify::WriteResult PseudoTcpChannel::TcpWritePacket( + PseudoTcp* tcp, const char* buffer, size_t len) { + ASSERT(cs_.CurrentThreadIsOwner()); + ASSERT(tcp == tcp_); + ASSERT(NULL != channel_); + int sent = channel_->SendPacket(buffer, len); + if (sent > 0) { + //LOG_F(LS_VERBOSE) << "(" << sent << ") Sent"; + return IPseudoTcpNotify::WR_SUCCESS; + } else if (IsBlockingError(channel_->GetError())) { + LOG_F(LS_VERBOSE) << "Blocking"; + return IPseudoTcpNotify::WR_SUCCESS; + } else if (channel_->GetError() == EMSGSIZE) { + LOG_F(LS_ERROR) << "EMSGSIZE"; + return IPseudoTcpNotify::WR_TOO_LARGE; + } else { + PLOG(LS_ERROR, channel_->GetError()) << "PseudoTcpChannel::TcpWritePacket"; + ASSERT(false); + return IPseudoTcpNotify::WR_FAIL; + } +} + +void PseudoTcpChannel::AdjustClock(bool clear) { + ASSERT(cs_.CurrentThreadIsOwner()); + ASSERT(NULL != tcp_); + + long timeout = 0; + if (tcp_->GetNextClock(PseudoTcp::Now(), timeout)) { + ASSERT(NULL != channel_); + // Reset the next clock, by clearing the old and setting a new one. + if (clear) + worker_thread_->Clear(this, MSG_WK_CLOCK); + worker_thread_->PostDelayed(_max(timeout, 0L), this, MSG_WK_CLOCK); + return; + } + + delete tcp_; + tcp_ = NULL; + ready_to_connect_ = false; + + if (channel_) { + // If TCP has failed, no need for channel_ anymore + signal_thread_->Post(this, MSG_SI_DESTROYCHANNEL); + } +} + +void PseudoTcpChannel::CheckDestroy() { + ASSERT(cs_.CurrentThreadIsOwner()); + if ((worker_thread_ != NULL) || (stream_ != NULL)) + return; + signal_thread_->Post(this, MSG_SI_DESTROY); +} + +/////////////////////////////////////////////////////////////////////////////// +// PseudoTcpChannel::InternalStream +/////////////////////////////////////////////////////////////////////////////// + +PseudoTcpChannel::InternalStream::InternalStream(PseudoTcpChannel* parent) + : parent_(parent) { +} + +PseudoTcpChannel::InternalStream::~InternalStream() { + Close(); +} + +StreamState PseudoTcpChannel::InternalStream::GetState() const { + if (!parent_) + return SS_CLOSED; + return parent_->GetState(); +} + +StreamResult PseudoTcpChannel::InternalStream::Read( + void* buffer, size_t buffer_len, size_t* read, int* error) { + if (!parent_) { + if (error) + *error = ENOTCONN; + return SR_ERROR; + } + return parent_->Read(buffer, buffer_len, read, error); +} + +StreamResult PseudoTcpChannel::InternalStream::Write( + const void* data, size_t data_len, size_t* written, int* error) { + if (!parent_) { + if (error) + *error = ENOTCONN; + return SR_ERROR; + } + return parent_->Write(data, data_len, written, error); +} + +void PseudoTcpChannel::InternalStream::Close() { + if (!parent_) + return; + parent_->Close(); + parent_ = NULL; +} + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace cricket diff --git a/talk/session/tunnel/pseudotcpchannel.h b/talk/session/tunnel/pseudotcpchannel.h new file mode 100644 index 000000000..a540699a5 --- /dev/null +++ b/talk/session/tunnel/pseudotcpchannel.h @@ -0,0 +1,140 @@ +/* + * 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. + */ + +#ifndef TALK_SESSION_TUNNEL_PSEUDOTCPCHANNEL_H_ +#define TALK_SESSION_TUNNEL_PSEUDOTCPCHANNEL_H_ + +#include "talk/base/criticalsection.h" +#include "talk/base/messagequeue.h" +#include "talk/base/stream.h" +#include "talk/p2p/base/pseudotcp.h" +#include "talk/p2p/base/session.h" + +namespace talk_base { +class Thread; +} + +namespace cricket { + +class Candidate; +class TransportChannel; + +/////////////////////////////////////////////////////////////////////////////// +// PseudoTcpChannel +// Note: The PseudoTcpChannel must persist until both of: +// 1) The StreamInterface provided via GetStream has been closed. +// This is tracked via non-null stream_. +// 2) The PseudoTcp session has completed. +// This is tracked via non-null worker_thread_. When PseudoTcp is done, +// the TransportChannel is signalled to tear-down. Once the channel is +// torn down, the worker thread is purged. +// These indicators are checked by CheckDestroy, invoked whenever one of them +// changes. +/////////////////////////////////////////////////////////////////////////////// +// PseudoTcpChannel::GetStream +// Note: The stream pointer returned by GetStream is owned by the caller. +// They can close & immediately delete the stream while PseudoTcpChannel still +// has cleanup work to do. They can also close the stream but not delete it +// until long after PseudoTcpChannel has finished. We must cope with both. +/////////////////////////////////////////////////////////////////////////////// + +class PseudoTcpChannel + : public IPseudoTcpNotify, + public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + // Signal thread methods + PseudoTcpChannel(talk_base::Thread* stream_thread, + Session* session); + + bool Connect(const std::string& content_name, + const std::string& channel_name, + int component); + talk_base::StreamInterface* GetStream(); + + sigslot::signal1 SignalChannelClosed; + + // Call this when the Session used to create this channel is being torn + // down, to ensure that things get cleaned up properly. + void OnSessionTerminate(Session* session); + + // See the PseudoTcp class for available options. + void GetOption(PseudoTcp::Option opt, int* value); + void SetOption(PseudoTcp::Option opt, int value); + + private: + class InternalStream; + friend class InternalStream; + + virtual ~PseudoTcpChannel(); + + // Stream thread methods + talk_base::StreamState GetState() const; + talk_base::StreamResult Read(void* buffer, size_t buffer_len, + size_t* read, int* error); + talk_base::StreamResult Write(const void* data, size_t data_len, + size_t* written, int* error); + void Close(); + + // Multi-thread methods + void OnMessage(talk_base::Message* pmsg); + void AdjustClock(bool clear = true); + void CheckDestroy(); + + // Signal thread methods + void OnChannelDestroyed(TransportChannel* channel); + + // Worker thread methods + void OnChannelWritableState(TransportChannel* channel); + void OnChannelRead(TransportChannel* channel, const char* data, size_t size, + int flags); + void OnChannelConnectionChanged(TransportChannel* channel, + const Candidate& candidate); + + virtual void OnTcpOpen(PseudoTcp* ptcp); + virtual void OnTcpReadable(PseudoTcp* ptcp); + virtual void OnTcpWriteable(PseudoTcp* ptcp); + virtual void OnTcpClosed(PseudoTcp* ptcp, uint32 nError); + virtual IPseudoTcpNotify::WriteResult TcpWritePacket(PseudoTcp* tcp, + const char* buffer, + size_t len); + + talk_base::Thread* signal_thread_, * worker_thread_, * stream_thread_; + Session* session_; + TransportChannel* channel_; + std::string content_name_; + std::string channel_name_; + PseudoTcp* tcp_; + InternalStream* stream_; + bool stream_readable_, pending_read_event_; + bool ready_to_connect_; + mutable talk_base::CriticalSection cs_; +}; + +} // namespace cricket + +#endif // TALK_SESSION_TUNNEL_PSEUDOTCPCHANNEL_H_ diff --git a/talk/session/tunnel/securetunnelsessionclient.cc b/talk/session/tunnel/securetunnelsessionclient.cc new file mode 100644 index 000000000..9287d22ab --- /dev/null +++ b/talk/session/tunnel/securetunnelsessionclient.cc @@ -0,0 +1,387 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +// SecureTunnelSessionClient and SecureTunnelSession implementation. + +#include "talk/session/tunnel/securetunnelsessionclient.h" +#include "talk/base/basicdefs.h" +#include "talk/base/basictypes.h" +#include "talk/base/common.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/stringutils.h" +#include "talk/base/sslidentity.h" +#include "talk/base/sslstreamadapter.h" +#include "talk/p2p/base/transportchannel.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/session/tunnel/pseudotcpchannel.h" + +namespace cricket { + +// XML elements and namespaces for XMPP stanzas used in content exchanges. + +const char NS_SECURE_TUNNEL[] = "http://www.google.com/talk/securetunnel"; +const buzz::StaticQName QN_SECURE_TUNNEL_DESCRIPTION = + { NS_SECURE_TUNNEL, "description" }; +const buzz::StaticQName QN_SECURE_TUNNEL_TYPE = + { NS_SECURE_TUNNEL, "type" }; +const buzz::StaticQName QN_SECURE_TUNNEL_CLIENT_CERT = + { NS_SECURE_TUNNEL, "client-cert" }; +const buzz::StaticQName QN_SECURE_TUNNEL_SERVER_CERT = + { NS_SECURE_TUNNEL, "server-cert" }; +const char CN_SECURE_TUNNEL[] = "securetunnel"; + +// SecureTunnelContentDescription + +// TunnelContentDescription is extended to hold string forms of the +// client and server certificate, PEM encoded. + +struct SecureTunnelContentDescription : public ContentDescription { + std::string description; + std::string client_pem_certificate; + std::string server_pem_certificate; + + SecureTunnelContentDescription(const std::string& desc, + const std::string& client_pem_cert, + const std::string& server_pem_cert) + : description(desc), + client_pem_certificate(client_pem_cert), + server_pem_certificate(server_pem_cert) { + } + virtual ContentDescription* Copy() const { + return new SecureTunnelContentDescription(*this); + } +}; + +// SecureTunnelSessionClient + +SecureTunnelSessionClient::SecureTunnelSessionClient( + const buzz::Jid& jid, SessionManager* manager) + : TunnelSessionClient(jid, manager, NS_SECURE_TUNNEL) { +} + +void SecureTunnelSessionClient::SetIdentity(talk_base::SSLIdentity* identity) { + ASSERT(identity_.get() == NULL); + identity_.reset(identity); +} + +bool SecureTunnelSessionClient::GenerateIdentity() { + ASSERT(identity_.get() == NULL); + identity_.reset(talk_base::SSLIdentity::Generate( + // The name on the certificate does not matter: the peer will + // make sure the cert it gets during SSL negotiation matches the + // one it got from XMPP. It would be neat to put something + // recognizable in there such as the JID, except this will show + // in clear during the SSL negotiation and so it could be a + // privacy issue. Specifying an empty string here causes + // it to use a random string. +#ifdef _DEBUG + jid().Str() +#else + "" +#endif + )); + if (identity_.get() == NULL) { + LOG(LS_ERROR) << "Failed to generate SSL identity"; + return false; + } + return true; +} + +talk_base::SSLIdentity& SecureTunnelSessionClient::GetIdentity() const { + ASSERT(identity_.get() != NULL); + return *identity_; +} + +// Parses a certificate from a PEM encoded string. +// Returns NULL on failure. +// The caller is responsible for freeing the returned object. +static talk_base::SSLCertificate* ParseCertificate( + const std::string& pem_cert) { + if (pem_cert.empty()) + return NULL; + return talk_base::SSLCertificate::FromPEMString(pem_cert); +} + +TunnelSession* SecureTunnelSessionClient::MakeTunnelSession( + Session* session, talk_base::Thread* stream_thread, + TunnelSessionRole role) { + return new SecureTunnelSession(this, session, stream_thread, role); +} + +bool FindSecureTunnelContent(const cricket::SessionDescription* sdesc, + std::string* name, + const SecureTunnelContentDescription** content) { + const ContentInfo* cinfo = sdesc->FirstContentByType(NS_SECURE_TUNNEL); + if (cinfo == NULL) + return false; + + *name = cinfo->name; + *content = static_cast( + cinfo->description); + return true; +} + +void SecureTunnelSessionClient::OnIncomingTunnel(const buzz::Jid &jid, + Session *session) { + std::string content_name; + const SecureTunnelContentDescription* content = NULL; + if (!FindSecureTunnelContent(session->remote_description(), + &content_name, &content)) { + ASSERT(false); + } + + // Validate the certificate + talk_base::scoped_ptr peer_cert( + ParseCertificate(content->client_pem_certificate)); + if (peer_cert.get() == NULL) { + LOG(LS_ERROR) + << "Rejecting incoming secure tunnel with invalid cetificate"; + DeclineTunnel(session); + return; + } + // If there were a convenient place we could have cached the + // peer_cert so as not to have to parse it a second time when + // configuring the tunnel. + SignalIncomingTunnel(this, jid, content->description, session); +} + +// The XML representation of a session initiation request (XMPP IQ), +// containing the initiator's SecureTunnelContentDescription, +// looks something like this: +// +// +// +// send:filename +// +// -----BEGIN CERTIFICATE----- +// INITIATOR'S CERTIFICATE IN PERM FORMAT (ASCII GIBBERISH) +// -----END CERTIFICATE----- +// +// +// +// +// + +// The session accept iq, containing the recipient's certificate and +// echoing the initiator's certificate, looks something like this: +// +// +// +// send:FILENAME +// +// -----BEGIN CERTIFICATE----- +// INITIATOR'S CERTIFICATE IN PERM FORMAT (ASCII GIBBERISH) +// -----END CERTIFICATE----- +// +// +// -----BEGIN CERTIFICATE----- +// RECIPIENT'S CERTIFICATE IN PERM FORMAT (ASCII GIBBERISH) +// -----END CERTIFICATE----- +// +// +// +// + + +bool SecureTunnelSessionClient::ParseContent(SignalingProtocol protocol, + const buzz::XmlElement* elem, + ContentDescription** content, + ParseError* error) { + const buzz::XmlElement* type_elem = elem->FirstNamed(QN_SECURE_TUNNEL_TYPE); + + if (type_elem == NULL) + // Missing mandatory XML element. + return false; + + // Here we consider the certificate components to be optional. In + // practice the client certificate is always present, and the server + // certificate is initially missing from the session description + // sent during session initiation. OnAccept() will enforce that we + // have a certificate for our peer. + const buzz::XmlElement* client_cert_elem = + elem->FirstNamed(QN_SECURE_TUNNEL_CLIENT_CERT); + const buzz::XmlElement* server_cert_elem = + elem->FirstNamed(QN_SECURE_TUNNEL_SERVER_CERT); + *content = new SecureTunnelContentDescription( + type_elem->BodyText(), + client_cert_elem ? client_cert_elem->BodyText() : "", + server_cert_elem ? server_cert_elem->BodyText() : ""); + return true; +} + +bool SecureTunnelSessionClient::WriteContent( + SignalingProtocol protocol, const ContentDescription* untyped_content, + buzz::XmlElement** elem, WriteError* error) { + const SecureTunnelContentDescription* content = + static_cast(untyped_content); + + buzz::XmlElement* root = + new buzz::XmlElement(QN_SECURE_TUNNEL_DESCRIPTION, true); + buzz::XmlElement* type_elem = new buzz::XmlElement(QN_SECURE_TUNNEL_TYPE); + type_elem->SetBodyText(content->description); + root->AddElement(type_elem); + if (!content->client_pem_certificate.empty()) { + buzz::XmlElement* client_cert_elem = + new buzz::XmlElement(QN_SECURE_TUNNEL_CLIENT_CERT); + client_cert_elem->SetBodyText(content->client_pem_certificate); + root->AddElement(client_cert_elem); + } + if (!content->server_pem_certificate.empty()) { + buzz::XmlElement* server_cert_elem = + new buzz::XmlElement(QN_SECURE_TUNNEL_SERVER_CERT); + server_cert_elem->SetBodyText(content->server_pem_certificate); + root->AddElement(server_cert_elem); + } + *elem = root; + return true; +} + +SessionDescription* NewSecureTunnelSessionDescription( + const std::string& content_name, ContentDescription* content) { + SessionDescription* sdesc = new SessionDescription(); + sdesc->AddContent(content_name, NS_SECURE_TUNNEL, content); + return sdesc; +} + +SessionDescription* SecureTunnelSessionClient::CreateOffer( + const buzz::Jid &jid, const std::string &description) { + // We are the initiator so we are the client. Put our cert into the + // description. + std::string pem_cert = GetIdentity().certificate().ToPEMString(); + return NewSecureTunnelSessionDescription( + CN_SECURE_TUNNEL, + new SecureTunnelContentDescription(description, pem_cert, "")); +} + +SessionDescription* SecureTunnelSessionClient::CreateAnswer( + const SessionDescription* offer) { + std::string content_name; + const SecureTunnelContentDescription* offer_tunnel = NULL; + if (!FindSecureTunnelContent(offer, &content_name, &offer_tunnel)) + return NULL; + + // We are accepting a session request. We need to add our cert, the + // server cert, into the description. The client cert was validated + // in OnIncomingTunnel(). + ASSERT(!offer_tunnel->client_pem_certificate.empty()); + return NewSecureTunnelSessionDescription( + content_name, + new SecureTunnelContentDescription( + offer_tunnel->description, + offer_tunnel->client_pem_certificate, + GetIdentity().certificate().ToPEMString())); +} + +// SecureTunnelSession + +SecureTunnelSession::SecureTunnelSession( + SecureTunnelSessionClient* client, Session* session, + talk_base::Thread* stream_thread, TunnelSessionRole role) + : TunnelSession(client, session, stream_thread), + role_(role) { +} + +talk_base::StreamInterface* SecureTunnelSession::MakeSecureStream( + talk_base::StreamInterface* stream) { + talk_base::SSLStreamAdapter* ssl_stream = + talk_base::SSLStreamAdapter::Create(stream); + talk_base::SSLIdentity* identity = + static_cast(client_)-> + GetIdentity().GetReference(); + ssl_stream->SetIdentity(identity); + if (role_ == RESPONDER) + ssl_stream->SetServerRole(); + ssl_stream->StartSSLWithPeer(); + + // SSL negotiation will start on the stream as soon as it + // opens. However our SSLStreamAdapter still hasn't been told what + // certificate to allow for our peer. If we are the initiator, we do + // not have the peer's certificate yet: we will obtain it from the + // session accept message which we will receive later (see + // OnAccept()). We won't Connect() the PseudoTcpChannel until we get + // that, so the stream will stay closed until then. Keep a handle + // on the streem so we can configure the peer certificate later. + ssl_stream_reference_.reset(new talk_base::StreamReference(ssl_stream)); + return ssl_stream_reference_->NewReference(); +} + +talk_base::StreamInterface* SecureTunnelSession::GetStream() { + ASSERT(channel_ != NULL); + ASSERT(ssl_stream_reference_.get() == NULL); + return MakeSecureStream(channel_->GetStream()); +} + +void SecureTunnelSession::OnAccept() { + // We have either sent or received a session accept: it's time to + // connect the tunnel. First we must set the peer certificate. + ASSERT(channel_ != NULL); + ASSERT(session_ != NULL); + std::string content_name; + const SecureTunnelContentDescription* remote_tunnel = NULL; + if (!FindSecureTunnelContent(session_->remote_description(), + &content_name, &remote_tunnel)) { + session_->Reject(STR_TERMINATE_INCOMPATIBLE_PARAMETERS); + return; + } + + const std::string& cert_pem = + role_ == INITIATOR ? remote_tunnel->server_pem_certificate : + remote_tunnel->client_pem_certificate; + talk_base::SSLCertificate* peer_cert = + ParseCertificate(cert_pem); + if (peer_cert == NULL) { + ASSERT(role_ == INITIATOR); // when RESPONDER we validated it earlier + LOG(LS_ERROR) + << "Rejecting secure tunnel accept with invalid cetificate"; + session_->Reject(STR_TERMINATE_INCOMPATIBLE_PARAMETERS); + return; + } + ASSERT(ssl_stream_reference_.get() != NULL); + talk_base::SSLStreamAdapter* ssl_stream = + static_cast( + ssl_stream_reference_->GetStream()); + ssl_stream->SetPeerCertificate(peer_cert); // pass ownership of certificate. + // We no longer need our handle to the ssl stream. + ssl_stream_reference_.reset(); + LOG(LS_INFO) << "Connecting tunnel"; + // This will try to connect the PseudoTcpChannel. If and when that + // succeeds, then ssl negotiation will take place, and when that + // succeeds, the tunnel stream will finally open. + VERIFY(channel_->Connect( + content_name, "tcp", ICE_CANDIDATE_COMPONENT_DEFAULT)); +} + +} // namespace cricket diff --git a/talk/session/tunnel/securetunnelsessionclient.h b/talk/session/tunnel/securetunnelsessionclient.h new file mode 100644 index 000000000..5c65b984d --- /dev/null +++ b/talk/session/tunnel/securetunnelsessionclient.h @@ -0,0 +1,165 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +// SecureTunnelSessionClient and SecureTunnelSession. +// SecureTunnelSessionClient extends TunnelSessionClient to exchange +// certificates as part of the session description. +// SecureTunnelSession is a TunnelSession that wraps the underlying +// tunnel stream into an SSLStreamAdapter. + +#ifndef TALK_SESSION_TUNNEL_SECURETUNNELSESSIONCLIENT_H_ +#define TALK_SESSION_TUNNEL_SECURETUNNELSESSIONCLIENT_H_ + +#include + +#include "talk/base/sslidentity.h" +#include "talk/base/sslstreamadapter.h" +#include "talk/session/tunnel/tunnelsessionclient.h" + +namespace cricket { + +class SecureTunnelSession; // below + +// SecureTunnelSessionClient + +// This TunnelSessionClient establishes secure tunnels protected by +// SSL/TLS. The PseudoTcpChannel stream is wrapped with an +// SSLStreamAdapter. An SSLIdentity must be set or generated. +// +// The TunnelContentDescription is extended to include the client and +// server certificates. The initiator acts as the client. The session +// initiate stanza carries a description that contains the client's +// certificate, and the session accept response's description has the +// server certificate added to it. + +class SecureTunnelSessionClient : public TunnelSessionClient { + public: + // The jid is used as the name for sessions for outgoing tunnels. + // manager is the SessionManager to which we register this client + // and its sessions. + SecureTunnelSessionClient(const buzz::Jid& jid, SessionManager* manager); + + // Configures this client to use a preexisting SSLIdentity. + // The client takes ownership of the identity object. + // Use either SetIdentity or GenerateIdentity, and only once. + void SetIdentity(talk_base::SSLIdentity* identity); + + // Generates an identity from nothing. + // Returns true if generation was successful. + // Use either SetIdentity or GenerateIdentity, and only once. + bool GenerateIdentity(); + + // Returns our identity for SSL purposes, as either set by + // SetIdentity() or generated by GenerateIdentity(). Call this + // method only after our identity has been successfully established + // by one of those methods. + talk_base::SSLIdentity& GetIdentity() const; + + // Inherited methods + virtual void OnIncomingTunnel(const buzz::Jid& jid, Session *session); + virtual bool ParseContent(SignalingProtocol protocol, + const buzz::XmlElement* elem, + ContentDescription** content, + ParseError* error); + virtual bool WriteContent(SignalingProtocol protocol, + const ContentDescription* content, + buzz::XmlElement** elem, + WriteError* error); + virtual SessionDescription* CreateOffer( + const buzz::Jid &jid, const std::string &description); + virtual SessionDescription* CreateAnswer( + const SessionDescription* offer); + + protected: + virtual TunnelSession* MakeTunnelSession( + Session* session, talk_base::Thread* stream_thread, + TunnelSessionRole role); + + private: + // Our identity (key and certificate) for SSL purposes. The + // certificate part will be communicated within the session + // description. The identity will be passed to the SSLStreamAdapter + // and used for SSL authentication. + talk_base::scoped_ptr identity_; + + DISALLOW_EVIL_CONSTRUCTORS(SecureTunnelSessionClient); +}; + +// SecureTunnelSession: +// A TunnelSession represents one session for one client. It +// provides the actual tunnel stream and handles state changes. +// A SecureTunnelSession is a TunnelSession that wraps the underlying +// tunnel stream into an SSLStreamAdapter. + +class SecureTunnelSession : public TunnelSession { + public: + // This TunnelSession will tie together the given client and session. + // stream_thread is passed to the PseudoTCPChannel: it's the thread + // designated to interact with the tunnel stream. + // role is either INITIATOR or RESPONDER, depending on who is + // initiating the session. + SecureTunnelSession(SecureTunnelSessionClient* client, Session* session, + talk_base::Thread* stream_thread, + TunnelSessionRole role); + + // Returns the stream that implements the actual P2P tunnel. + // This may be called only once. Caller is responsible for freeing + // the returned object. + virtual talk_base::StreamInterface* GetStream(); + + protected: + // Inherited method: callback on accepting a session. + virtual void OnAccept(); + + // Helper method for GetStream() that Instantiates the + // SSLStreamAdapter to wrap the PseudoTcpChannel's stream, and + // configures it with our identity and role. + talk_base::StreamInterface* MakeSecureStream( + talk_base::StreamInterface* stream); + + // Our role in requesting the tunnel: INITIATOR or + // RESPONDER. Translates to our role in SSL negotiation: + // respectively client or server. Also indicates which slot of the + // SecureTunnelContentDescription our cert goes into: client-cert or + // server-cert respectively. + TunnelSessionRole role_; + + // This is the stream representing the usable tunnel endpoint. It's + // a StreamReference wrapping the SSLStreamAdapter instance, which + // further wraps a PseudoTcpChannel::InternalStream. The + // StreamReference is because in the case of CreateTunnel(), the + // stream endpoint is returned early, but we need to keep a handle + // on it so we can setup the peer certificate when we receive it + // later. + talk_base::scoped_ptr ssl_stream_reference_; + + DISALLOW_EVIL_CONSTRUCTORS(SecureTunnelSession); +}; + +} // namespace cricket + +#endif // TALK_SESSION_TUNNEL_SECURETUNNELSESSIONCLIENT_H_ diff --git a/talk/session/tunnel/tunnelsessionclient.cc b/talk/session/tunnel/tunnelsessionclient.cc new file mode 100644 index 000000000..71d0ce119 --- /dev/null +++ b/talk/session/tunnel/tunnelsessionclient.cc @@ -0,0 +1,432 @@ +/* + * libjingle + * Copyright 2004--2008, 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/base/basicdefs.h" +#include "talk/base/basictypes.h" +#include "talk/base/common.h" +#include "talk/base/helpers.h" +#include "talk/base/logging.h" +#include "talk/base/stringutils.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/transportchannel.h" +#include "talk/xmllite/xmlelement.h" +#include "pseudotcpchannel.h" +#include "tunnelsessionclient.h" + +namespace cricket { + +const char NS_TUNNEL[] = "http://www.google.com/talk/tunnel"; +const buzz::StaticQName QN_TUNNEL_DESCRIPTION = { NS_TUNNEL, "description" }; +const buzz::StaticQName QN_TUNNEL_TYPE = { NS_TUNNEL, "type" }; +const char CN_TUNNEL[] = "tunnel"; + +enum { + MSG_CLOCK = 1, + MSG_DESTROY, + MSG_TERMINATE, + MSG_EVENT, + MSG_CREATE_TUNNEL, +}; + +struct EventData : public talk_base::MessageData { + int event, error; + EventData(int ev, int err = 0) : event(ev), error(err) { } +}; + +struct CreateTunnelData : public talk_base::MessageData { + buzz::Jid jid; + std::string description; + talk_base::Thread* thread; + talk_base::StreamInterface* stream; +}; + +extern const talk_base::ConstantLabel SESSION_STATES[]; + +const talk_base::ConstantLabel SESSION_STATES[] = { + KLABEL(Session::STATE_INIT), + KLABEL(Session::STATE_SENTINITIATE), + KLABEL(Session::STATE_RECEIVEDINITIATE), + KLABEL(Session::STATE_SENTACCEPT), + KLABEL(Session::STATE_RECEIVEDACCEPT), + KLABEL(Session::STATE_SENTMODIFY), + KLABEL(Session::STATE_RECEIVEDMODIFY), + KLABEL(Session::STATE_SENTREJECT), + KLABEL(Session::STATE_RECEIVEDREJECT), + KLABEL(Session::STATE_SENTREDIRECT), + KLABEL(Session::STATE_SENTTERMINATE), + KLABEL(Session::STATE_RECEIVEDTERMINATE), + KLABEL(Session::STATE_INPROGRESS), + KLABEL(Session::STATE_DEINIT), + LASTLABEL +}; + +/////////////////////////////////////////////////////////////////////////////// +// TunnelContentDescription +/////////////////////////////////////////////////////////////////////////////// + +struct TunnelContentDescription : public ContentDescription { + std::string description; + + TunnelContentDescription(const std::string& desc) : description(desc) { } + virtual ContentDescription* Copy() const { + return new TunnelContentDescription(*this); + } +}; + +/////////////////////////////////////////////////////////////////////////////// +// TunnelSessionClientBase +/////////////////////////////////////////////////////////////////////////////// + +TunnelSessionClientBase::TunnelSessionClientBase(const buzz::Jid& jid, + SessionManager* manager, const std::string &ns) + : jid_(jid), session_manager_(manager), namespace_(ns), shutdown_(false) { + session_manager_->AddClient(namespace_, this); +} + +TunnelSessionClientBase::~TunnelSessionClientBase() { + shutdown_ = true; + for (std::vector::iterator it = sessions_.begin(); + it != sessions_.end(); + ++it) { + Session* session = (*it)->ReleaseSession(true); + session_manager_->DestroySession(session); + } + session_manager_->RemoveClient(namespace_); +} + +void TunnelSessionClientBase::OnSessionCreate(Session* session, bool received) { + LOG(LS_INFO) << "TunnelSessionClientBase::OnSessionCreate: received=" + << received; + ASSERT(session_manager_->signaling_thread()->IsCurrent()); + if (received) + sessions_.push_back( + MakeTunnelSession(session, talk_base::Thread::Current(), RESPONDER)); +} + +void TunnelSessionClientBase::OnSessionDestroy(Session* session) { + LOG(LS_INFO) << "TunnelSessionClientBase::OnSessionDestroy"; + ASSERT(session_manager_->signaling_thread()->IsCurrent()); + if (shutdown_) + return; + for (std::vector::iterator it = sessions_.begin(); + it != sessions_.end(); + ++it) { + if ((*it)->HasSession(session)) { + VERIFY((*it)->ReleaseSession(false) == session); + sessions_.erase(it); + return; + } + } +} + +talk_base::StreamInterface* TunnelSessionClientBase::CreateTunnel( + const buzz::Jid& to, const std::string& description) { + // Valid from any thread + CreateTunnelData data; + data.jid = to; + data.description = description; + data.thread = talk_base::Thread::Current(); + data.stream = NULL; + session_manager_->signaling_thread()->Send(this, MSG_CREATE_TUNNEL, &data); + return data.stream; +} + +talk_base::StreamInterface* TunnelSessionClientBase::AcceptTunnel( + Session* session) { + ASSERT(session_manager_->signaling_thread()->IsCurrent()); + TunnelSession* tunnel = NULL; + for (std::vector::iterator it = sessions_.begin(); + it != sessions_.end(); + ++it) { + if ((*it)->HasSession(session)) { + tunnel = *it; + break; + } + } + ASSERT(tunnel != NULL); + + SessionDescription* answer = CreateAnswer(session->remote_description()); + if (answer == NULL) + return NULL; + + session->Accept(answer); + return tunnel->GetStream(); +} + +void TunnelSessionClientBase::DeclineTunnel(Session* session) { + ASSERT(session_manager_->signaling_thread()->IsCurrent()); + session->Reject(STR_TERMINATE_DECLINE); +} + +void TunnelSessionClientBase::OnMessage(talk_base::Message* pmsg) { + if (pmsg->message_id == MSG_CREATE_TUNNEL) { + ASSERT(session_manager_->signaling_thread()->IsCurrent()); + CreateTunnelData* data = static_cast(pmsg->pdata); + SessionDescription* offer = CreateOffer(data->jid, data->description); + if (offer == NULL) { + return; + } + + Session* session = session_manager_->CreateSession(jid_.Str(), namespace_); + TunnelSession* tunnel = MakeTunnelSession(session, data->thread, + INITIATOR); + sessions_.push_back(tunnel); + session->Initiate(data->jid.Str(), offer); + data->stream = tunnel->GetStream(); + } +} + +TunnelSession* TunnelSessionClientBase::MakeTunnelSession( + Session* session, talk_base::Thread* stream_thread, + TunnelSessionRole /*role*/) { + return new TunnelSession(this, session, stream_thread); +} + +/////////////////////////////////////////////////////////////////////////////// +// TunnelSessionClient +/////////////////////////////////////////////////////////////////////////////// + +TunnelSessionClient::TunnelSessionClient(const buzz::Jid& jid, + SessionManager* manager, + const std::string &ns) + : TunnelSessionClientBase(jid, manager, ns) { +} + +TunnelSessionClient::TunnelSessionClient(const buzz::Jid& jid, + SessionManager* manager) + : TunnelSessionClientBase(jid, manager, NS_TUNNEL) { +} + +TunnelSessionClient::~TunnelSessionClient() { +} + + +bool TunnelSessionClient::ParseContent(SignalingProtocol protocol, + const buzz::XmlElement* elem, + ContentDescription** content, + ParseError* error) { + if (const buzz::XmlElement* type_elem = elem->FirstNamed(QN_TUNNEL_TYPE)) { + *content = new TunnelContentDescription(type_elem->BodyText()); + return true; + } + return false; +} + +bool TunnelSessionClient::WriteContent( + SignalingProtocol protocol, + const ContentDescription* untyped_content, + buzz::XmlElement** elem, WriteError* error) { + const TunnelContentDescription* content = + static_cast(untyped_content); + + buzz::XmlElement* root = new buzz::XmlElement(QN_TUNNEL_DESCRIPTION, true); + buzz::XmlElement* type_elem = new buzz::XmlElement(QN_TUNNEL_TYPE); + type_elem->SetBodyText(content->description); + root->AddElement(type_elem); + *elem = root; + return true; +} + +SessionDescription* NewTunnelSessionDescription( + const std::string& content_name, ContentDescription* content) { + SessionDescription* sdesc = new SessionDescription(); + sdesc->AddContent(content_name, NS_TUNNEL, content); + return sdesc; +} + +bool FindTunnelContent(const cricket::SessionDescription* sdesc, + std::string* name, + const TunnelContentDescription** content) { + const ContentInfo* cinfo = sdesc->FirstContentByType(NS_TUNNEL); + if (cinfo == NULL) + return false; + + *name = cinfo->name; + *content = static_cast( + cinfo->description); + return true; +} + +void TunnelSessionClient::OnIncomingTunnel(const buzz::Jid &jid, + Session *session) { + std::string content_name; + const TunnelContentDescription* content = NULL; + if (!FindTunnelContent(session->remote_description(), + &content_name, &content)) { + session->Reject(STR_TERMINATE_INCOMPATIBLE_PARAMETERS); + return; + } + + SignalIncomingTunnel(this, jid, content->description, session); +} + +SessionDescription* TunnelSessionClient::CreateOffer( + const buzz::Jid &jid, const std::string &description) { + SessionDescription* offer = NewTunnelSessionDescription( + CN_TUNNEL, new TunnelContentDescription(description)); + talk_base::scoped_ptr tdesc( + session_manager_->transport_desc_factory()->CreateOffer( + TransportOptions(), NULL)); + if (tdesc.get()) { + offer->AddTransportInfo(TransportInfo(CN_TUNNEL, *tdesc)); + } else { + delete offer; + offer = NULL; + } + return offer; +} + +SessionDescription* TunnelSessionClient::CreateAnswer( + const SessionDescription* offer) { + std::string content_name; + const TunnelContentDescription* offer_tunnel = NULL; + if (!FindTunnelContent(offer, &content_name, &offer_tunnel)) + return NULL; + + SessionDescription* answer = NewTunnelSessionDescription( + content_name, new TunnelContentDescription(offer_tunnel->description)); + const TransportInfo* tinfo = offer->GetTransportInfoByName(content_name); + if (tinfo) { + const TransportDescription* offer_tdesc = &tinfo->description; + ASSERT(offer_tdesc != NULL); + talk_base::scoped_ptr tdesc( + session_manager_->transport_desc_factory()->CreateAnswer( + offer_tdesc, TransportOptions(), NULL)); + if (tdesc.get()) { + answer->AddTransportInfo(TransportInfo(content_name, *tdesc)); + } else { + delete answer; + answer = NULL; + } + } + return answer; +} +/////////////////////////////////////////////////////////////////////////////// +// TunnelSession +/////////////////////////////////////////////////////////////////////////////// + +// +// Signalling thread methods +// + +TunnelSession::TunnelSession(TunnelSessionClientBase* client, Session* session, + talk_base::Thread* stream_thread) + : client_(client), session_(session), channel_(NULL) { + ASSERT(client_ != NULL); + ASSERT(session_ != NULL); + session_->SignalState.connect(this, &TunnelSession::OnSessionState); + channel_ = new PseudoTcpChannel(stream_thread, session_); + channel_->SignalChannelClosed.connect(this, &TunnelSession::OnChannelClosed); +} + +TunnelSession::~TunnelSession() { + ASSERT(client_ != NULL); + ASSERT(session_ == NULL); + ASSERT(channel_ == NULL); +} + +talk_base::StreamInterface* TunnelSession::GetStream() { + ASSERT(channel_ != NULL); + return channel_->GetStream(); +} + +bool TunnelSession::HasSession(Session* session) { + ASSERT(NULL != session_); + return (session_ == session); +} + +Session* TunnelSession::ReleaseSession(bool channel_exists) { + ASSERT(NULL != session_); + ASSERT(NULL != channel_); + Session* session = session_; + session_->SignalState.disconnect(this); + session_ = NULL; + if (channel_exists) + channel_->SignalChannelClosed.disconnect(this); + channel_ = NULL; + delete this; + return session; +} + +void TunnelSession::OnSessionState(BaseSession* session, + BaseSession::State state) { + LOG(LS_INFO) << "TunnelSession::OnSessionState(" + << talk_base::nonnull( + talk_base::FindLabel(state, SESSION_STATES), "Unknown") + << ")"; + ASSERT(session == session_); + + switch (state) { + case Session::STATE_RECEIVEDINITIATE: + OnInitiate(); + break; + case Session::STATE_SENTACCEPT: + case Session::STATE_RECEIVEDACCEPT: + OnAccept(); + break; + case Session::STATE_SENTTERMINATE: + case Session::STATE_RECEIVEDTERMINATE: + OnTerminate(); + break; + case Session::STATE_DEINIT: + // ReleaseSession should have been called before this. + ASSERT(false); + break; + default: + break; + } +} + +void TunnelSession::OnInitiate() { + ASSERT(client_ != NULL); + ASSERT(session_ != NULL); + client_->OnIncomingTunnel(buzz::Jid(session_->remote_name()), session_); +} + +void TunnelSession::OnAccept() { + ASSERT(channel_ != NULL); + const ContentInfo* content = + session_->remote_description()->FirstContentByType(NS_TUNNEL); + ASSERT(content != NULL); + VERIFY(channel_->Connect( + content->name, "tcp", ICE_CANDIDATE_COMPONENT_DEFAULT)); +} + +void TunnelSession::OnTerminate() { + ASSERT(channel_ != NULL); + channel_->OnSessionTerminate(session_); +} + +void TunnelSession::OnChannelClosed(PseudoTcpChannel* channel) { + ASSERT(channel_ == channel); + ASSERT(session_ != NULL); + session_->Terminate(); +} + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace cricket diff --git a/talk/session/tunnel/tunnelsessionclient.h b/talk/session/tunnel/tunnelsessionclient.h new file mode 100644 index 000000000..55ce14a6d --- /dev/null +++ b/talk/session/tunnel/tunnelsessionclient.h @@ -0,0 +1,182 @@ +/* + * libjingle + * Copyright 2004--2008, 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. + */ + +#ifndef __TUNNELSESSIONCLIENT_H__ +#define __TUNNELSESSIONCLIENT_H__ + +#include + +#include "talk/base/criticalsection.h" +#include "talk/base/stream.h" +#include "talk/p2p/base/constants.h" +#include "talk/p2p/base/pseudotcp.h" +#include "talk/p2p/base/session.h" +#include "talk/p2p/base/sessiondescription.h" +#include "talk/p2p/base/sessionmanager.h" +#include "talk/p2p/base/sessionclient.h" +#include "talk/xmllite/qname.h" +#include "talk/xmpp/constants.h" + +namespace cricket { + +class TunnelSession; +class TunnelStream; + +enum TunnelSessionRole { INITIATOR, RESPONDER }; + +/////////////////////////////////////////////////////////////////////////////// +// TunnelSessionClient +/////////////////////////////////////////////////////////////////////////////// + +// Base class is still abstract +class TunnelSessionClientBase + : public SessionClient, public talk_base::MessageHandler { +public: + TunnelSessionClientBase(const buzz::Jid& jid, SessionManager* manager, + const std::string &ns); + virtual ~TunnelSessionClientBase(); + + const buzz::Jid& jid() const { return jid_; } + SessionManager* session_manager() const { return session_manager_; } + + void OnSessionCreate(Session* session, bool received); + void OnSessionDestroy(Session* session); + + // This can be called on any thread. The stream interface is + // thread-safe, but notifications must be registered on the creating + // thread. + talk_base::StreamInterface* CreateTunnel(const buzz::Jid& to, + const std::string& description); + + talk_base::StreamInterface* AcceptTunnel(Session* session); + void DeclineTunnel(Session* session); + + // Invoked on an incoming tunnel + virtual void OnIncomingTunnel(const buzz::Jid &jid, Session *session) = 0; + + // Invoked on an outgoing session request + virtual SessionDescription* CreateOffer( + const buzz::Jid &jid, const std::string &description) = 0; + // Invoked on a session request accept to create + // the local-side session description + virtual SessionDescription* CreateAnswer( + const SessionDescription* offer) = 0; + +protected: + + void OnMessage(talk_base::Message* pmsg); + + // helper method to instantiate TunnelSession. By overriding this, + // subclasses of TunnelSessionClient are able to instantiate + // subclasses of TunnelSession instead. + virtual TunnelSession* MakeTunnelSession(Session* session, + talk_base::Thread* stream_thread, + TunnelSessionRole role); + + buzz::Jid jid_; + SessionManager* session_manager_; + std::vector sessions_; + std::string namespace_; + bool shutdown_; +}; + +class TunnelSessionClient + : public TunnelSessionClientBase, public sigslot::has_slots<> { +public: + TunnelSessionClient(const buzz::Jid& jid, SessionManager* manager); + TunnelSessionClient(const buzz::Jid& jid, SessionManager* manager, + const std::string &ns); + virtual ~TunnelSessionClient(); + + virtual bool ParseContent(SignalingProtocol protocol, + const buzz::XmlElement* elem, + ContentDescription** content, + ParseError* error); + virtual bool WriteContent(SignalingProtocol protocol, + const ContentDescription* content, + buzz::XmlElement** elem, + WriteError* error); + + // Signal arguments are this, initiator, description, session + sigslot::signal4 + SignalIncomingTunnel; + + virtual void OnIncomingTunnel(const buzz::Jid &jid, + Session *session); + virtual SessionDescription* CreateOffer( + const buzz::Jid &jid, const std::string &description); + virtual SessionDescription* CreateAnswer( + const SessionDescription* offer); +}; + +/////////////////////////////////////////////////////////////////////////////// +// TunnelSession +// Note: The lifetime of TunnelSession is complicated. It needs to survive +// until the following three conditions are true: +// 1) TunnelStream has called Close (tracked via non-null stream_) +// 2) PseudoTcp has completed (tracked via non-null tcp_) +// 3) Session has been destroyed (tracked via non-null session_) +// This is accomplished by calling CheckDestroy after these indicators change. +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +// TunnelStream +// Note: Because TunnelStream provides a stream interface, its lifetime is +// controlled by the owner of the stream pointer. As a result, we must support +// both the TunnelSession disappearing before TunnelStream, and vice versa. +/////////////////////////////////////////////////////////////////////////////// + +class PseudoTcpChannel; + +class TunnelSession : public sigslot::has_slots<> { + public: + // Signalling thread methods + TunnelSession(TunnelSessionClientBase* client, Session* session, + talk_base::Thread* stream_thread); + + virtual talk_base::StreamInterface* GetStream(); + bool HasSession(Session* session); + Session* ReleaseSession(bool channel_exists); + + protected: + virtual ~TunnelSession(); + + virtual void OnSessionState(BaseSession* session, BaseSession::State state); + virtual void OnInitiate(); + virtual void OnAccept(); + virtual void OnTerminate(); + virtual void OnChannelClosed(PseudoTcpChannel* channel); + + TunnelSessionClientBase* client_; + Session* session_; + PseudoTcpChannel* channel_; +}; + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace cricket + +#endif // __TUNNELSESSIONCLIENT_H__ diff --git a/talk/session/tunnel/tunnelsessionclient_unittest.cc b/talk/session/tunnel/tunnelsessionclient_unittest.cc new file mode 100644 index 000000000..7370351e6 --- /dev/null +++ b/talk/session/tunnel/tunnelsessionclient_unittest.cc @@ -0,0 +1,226 @@ +/* + * libjingle + * Copyright 2010, 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 +#include "talk/base/gunit.h" +#include "talk/base/messagehandler.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stream.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" +#include "talk/p2p/base/sessionmanager.h" +#include "talk/p2p/base/transport.h" +#include "talk/p2p/client/fakeportallocator.h" +#include "talk/session/tunnel/tunnelsessionclient.h" + +static const int kTimeoutMs = 10000; +static const int kBlockSize = 4096; +static const buzz::Jid kLocalJid("local@localhost"); +static const buzz::Jid kRemoteJid("remote@localhost"); + +// This test fixture creates the necessary plumbing to create and run +// two TunnelSessionClients that talk to each other. +class TunnelSessionClientTest : public testing::Test, + public talk_base::MessageHandler, + public sigslot::has_slots<> { + public: + TunnelSessionClientTest() + : local_pa_(talk_base::Thread::Current(), NULL), + remote_pa_(talk_base::Thread::Current(), NULL), + local_sm_(&local_pa_, talk_base::Thread::Current()), + remote_sm_(&remote_pa_, talk_base::Thread::Current()), + local_client_(kLocalJid, &local_sm_), + remote_client_(kRemoteJid, &remote_sm_), + done_(false) { + local_sm_.SignalRequestSignaling.connect(this, + &TunnelSessionClientTest::OnLocalRequestSignaling); + local_sm_.SignalOutgoingMessage.connect(this, + &TunnelSessionClientTest::OnOutgoingMessage); + remote_sm_.SignalRequestSignaling.connect(this, + &TunnelSessionClientTest::OnRemoteRequestSignaling); + remote_sm_.SignalOutgoingMessage.connect(this, + &TunnelSessionClientTest::OnOutgoingMessage); + remote_client_.SignalIncomingTunnel.connect(this, + &TunnelSessionClientTest::OnIncomingTunnel); + } + + // Transfer the desired amount of data from the local to the remote client. + void TestTransfer(int size) { + // Create some dummy data to send. + send_stream_.ReserveSize(size); + for (int i = 0; i < size; ++i) { + char ch = static_cast(i); + send_stream_.Write(&ch, 1, NULL, NULL); + } + send_stream_.Rewind(); + // Prepare the receive stream. + recv_stream_.ReserveSize(size); + // Create the tunnel and set things in motion. + local_tunnel_.reset(local_client_.CreateTunnel(kRemoteJid, "test")); + local_tunnel_->SignalEvent.connect(this, + &TunnelSessionClientTest::OnStreamEvent); + EXPECT_TRUE_WAIT(done_, kTimeoutMs); + // Make sure we received the right data. + EXPECT_EQ(0, memcmp(send_stream_.GetBuffer(), + recv_stream_.GetBuffer(), size)); + } + + private: + enum { MSG_LSIGNAL, MSG_RSIGNAL }; + + // There's no SessionManager* argument in this callback, so we need 2 of them. + void OnLocalRequestSignaling() { + local_sm_.OnSignalingReady(); + } + void OnRemoteRequestSignaling() { + remote_sm_.OnSignalingReady(); + } + + // Post a message, to avoid problems with directly connecting the callbacks. + void OnOutgoingMessage(cricket::SessionManager* manager, + const buzz::XmlElement* stanza) { + if (manager == &local_sm_) { + talk_base::Thread::Current()->Post(this, MSG_LSIGNAL, + talk_base::WrapMessageData(*stanza)); + } else if (manager == &remote_sm_) { + talk_base::Thread::Current()->Post(this, MSG_RSIGNAL, + talk_base::WrapMessageData(*stanza)); + } + } + + // Need to add a "from=" attribute (normally added by the server) + // Then route the incoming signaling message to the "other" session manager. + virtual void OnMessage(talk_base::Message* message) { + talk_base::TypedMessageData* data = + static_cast*>( + message->pdata); + bool response = data->data().Attr(buzz::QN_TYPE) == buzz::STR_RESULT; + if (message->message_id == MSG_RSIGNAL) { + data->data().AddAttr(buzz::QN_FROM, remote_client_.jid().Str()); + if (!response) { + local_sm_.OnIncomingMessage(&data->data()); + } else { + local_sm_.OnIncomingResponse(NULL, &data->data()); + } + } else if (message->message_id == MSG_LSIGNAL) { + data->data().AddAttr(buzz::QN_FROM, local_client_.jid().Str()); + if (!response) { + remote_sm_.OnIncomingMessage(&data->data()); + } else { + remote_sm_.OnIncomingResponse(NULL, &data->data()); + } + } + delete data; + } + + // Accept the tunnel when it arrives and wire up the stream. + void OnIncomingTunnel(cricket::TunnelSessionClient* client, + buzz::Jid jid, std::string description, + cricket::Session* session) { + remote_tunnel_.reset(remote_client_.AcceptTunnel(session)); + remote_tunnel_->SignalEvent.connect(this, + &TunnelSessionClientTest::OnStreamEvent); + } + + // Send from send_stream_ as long as we're not flow-controlled. + // Read bytes out into recv_stream_ as they arrive. + // End the test when we are notified that the local side has closed the + // tunnel. All data has been read out at this point. + void OnStreamEvent(talk_base::StreamInterface* stream, int events, + int error) { + if (events & talk_base::SE_READ) { + if (stream == remote_tunnel_.get()) { + ReadData(); + } + } + if (events & talk_base::SE_WRITE) { + if (stream == local_tunnel_.get()) { + bool done = false; + WriteData(&done); + if (done) { + local_tunnel_->Close(); + } + } + } + if (events & talk_base::SE_CLOSE) { + if (stream == remote_tunnel_.get()) { + remote_tunnel_->Close(); + done_ = true; + } + } + } + + // Spool from the tunnel into recv_stream. + // Flow() doesn't work here because it won't write if the read blocks. + void ReadData() { + char block[kBlockSize]; + size_t read, position; + talk_base::StreamResult res; + while ((res = remote_tunnel_->Read(block, sizeof(block), &read, NULL)) == + talk_base::SR_SUCCESS) { + recv_stream_.Write(block, read, NULL, NULL); + } + ASSERT(res != talk_base::SR_EOS); + recv_stream_.GetPosition(&position); + LOG(LS_VERBOSE) << "Recv position: " << position; + } + // Spool from send_stream into the tunnel. Back up if we get flow controlled. + void WriteData(bool* done) { + char block[kBlockSize]; + size_t leftover = 0, position; + talk_base::StreamResult res = talk_base::Flow(&send_stream_, + block, sizeof(block), local_tunnel_.get(), &leftover); + if (res == talk_base::SR_BLOCK) { + send_stream_.GetPosition(&position); + send_stream_.SetPosition(position - leftover); + LOG(LS_VERBOSE) << "Send position: " << position - leftover; + *done = false; + } else if (res == talk_base::SR_SUCCESS) { + *done = true; + } else { + ASSERT(false); // shouldn't happen + } + } + + private: + cricket::FakePortAllocator local_pa_; + cricket::FakePortAllocator remote_pa_; + cricket::SessionManager local_sm_; + cricket::SessionManager remote_sm_; + cricket::TunnelSessionClient local_client_; + cricket::TunnelSessionClient remote_client_; + talk_base::scoped_ptr local_tunnel_; + talk_base::scoped_ptr remote_tunnel_; + talk_base::MemoryStream send_stream_; + talk_base::MemoryStream recv_stream_; + bool done_; +}; + +// Test the normal case of sending data from one side to the other. +TEST_F(TunnelSessionClientTest, TestTransfer) { + TestTransfer(1000000); +} diff --git a/talk/site_scons/site_tools/talk_linux.py b/talk/site_scons/site_tools/talk_linux.py new file mode 100644 index 000000000..1ceb94a76 --- /dev/null +++ b/talk/site_scons/site_tools/talk_linux.py @@ -0,0 +1,313 @@ +# Copyright 2010 Google Inc. +# All Rights Reserved. +# Author: tschmelcher@google.com (Tristan Schmelcher) + +"""Tool for helpers used in linux building process.""" + +import os +import SCons.Defaults +import subprocess + + +def _OutputFromShellCommand(command): + process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) + return process.communicate()[0].strip() + + +# This is a pure SCons helper function. +def _InternalBuildDebianPackage(env, debian_files, package_files, + output_dir=None, force_version=None): + """Creates build rules to build a Debian package from the specified sources. + + Args: + env: SCons Environment. + debian_files: Array of the Debian control file sources that should be + copied into the package source tree, e.g., changelog, control, rules, + etc. + package_files: An array of 2-tuples listing the files that should be + copied into the package source tree. + The first element is the path where the file should be placed for the + .install control file to find it, relative to the generated debian + package source directory. + The second element is the file source. + output_dir: An optional directory to place the files in. If omitted, the + current output directory is used. + force_version: Optional. Forces the version of the package to start with + this version string if specified. If the last entry in the changelog + is not for a version that starts with this then a dummy entry is + generated with this version and a ~prerelease suffix (so that the + final version will compare as greater). + + Return: + A list of the targets (if any). + """ + if 0 != subprocess.call(['which', 'dpkg-buildpackage']): + print ('dpkg-buildpackage not installed on this system; ' + 'skipping DEB build stage') + return [] + # Read the control file and changelog file to determine the package name, + # version, and arch that the Debian build tools will use to name the + # generated files. + control_file = None + changelog_file = None + for file in debian_files: + if os.path.basename(file) == 'control': + control_file = env.File(file).srcnode().abspath + elif os.path.basename(file) == 'changelog': + changelog_file = env.File(file).srcnode().abspath + if not control_file: + raise Exception('Need to have a control file') + if not changelog_file: + raise Exception('Need to have a changelog file') + source = _OutputFromShellCommand( + "awk '/^Source:/ { print $2; }' " + control_file) + packages = _OutputFromShellCommand( + "awk '/^Package:/ { print $2; }' " + control_file).split('\n') + version = _OutputFromShellCommand( + "sed -nr '1 { s/.*\\((.*)\\).*/\\1/; p }' " + changelog_file) + arch = _OutputFromShellCommand('dpkg --print-architecture') + add_dummy_changelog_entry = False + if force_version and not version.startswith(force_version): + print ('Warning: no entry in ' + changelog_file + ' for version ' + + force_version + ' (last is ' + version +'). A dummy entry will be ' + + 'generated. Remember to add the real changelog entry before ' + + 'releasing.') + version = force_version + '~prerelease' + add_dummy_changelog_entry = True + source_dir_name = source + '_' + version + '_' + arch + target_file_names = [ source_dir_name + '.changes' ] + for package in packages: + package_file_name = package + '_' + version + '_' + arch + '.deb' + target_file_names.append(package_file_name) + # The targets + if output_dir: + targets = [os.path.join(output_dir, s) for s in target_file_names] + else: + targets = target_file_names + # Path to where we will construct the debian build tree. + deb_build_tree = os.path.join(source_dir_name, 'deb_build_tree') + # First copy the files. + for file in package_files: + env.Command(os.path.join(deb_build_tree, file[0]), file[1], + SCons.Defaults.Copy('$TARGET', '$SOURCE')) + env.Depends(targets, os.path.join(deb_build_tree, file[0])) + # Now copy the Debian metadata sources. We have to do this all at once so + # that we can remove the target directory before copying, because there + # can't be any other stale files there or else dpkg-buildpackage may use + # them and give incorrect build output. + copied_debian_files_paths = [] + for file in debian_files: + copied_debian_files_paths.append(os.path.join(deb_build_tree, 'debian', + os.path.basename(file))) + copy_commands = [ + """dir=$$(dirname $TARGET) && \ + rm -Rf $$dir && \ + mkdir -p $$dir && \ + cp $SOURCES $$dir && \ + chmod -R u+w $$dir""" + ] + if add_dummy_changelog_entry: + copy_commands += [ + """debchange -c $$(dirname $TARGET)/changelog --newversion %s \ + --distribution UNRELEASED \ + 'Developer preview build. (This entry was auto-generated.)'""" % + version + ] + env.Command(copied_debian_files_paths, debian_files, copy_commands) + env.Depends(targets, copied_debian_files_paths) + # Must explicitly specify -a because otherwise cross-builds won't work. + # Must explicitly specify -D because -a disables it. + # Must explicitly specify fakeroot because old dpkg tools don't assume that. + env.Command(targets, None, + """dir=%(dir)s && \ + cd $$dir && \ + dpkg-buildpackage -b -uc -a%(arch)s -D -rfakeroot && \ + cd $$OLDPWD && \ + for file in %(targets)s; do \ + mv $$dir/../$$file $$(dirname $TARGET) || exit 1; \ + done""" % + {'dir':env.Dir(deb_build_tree).path, + 'arch':arch, + 'targets':' '.join(target_file_names)}) + return targets + + +def BuildDebianPackage(env, debian_files, package_files, force_version=None): + """Creates build rules to build a Debian package from the specified sources. + + This is a Hammer-ified version of _InternalBuildDebianPackage that knows to + put the packages in the Hammer staging dir. + + Args: + env: SCons Environment. + debian_files: Array of the Debian control file sources that should be + copied into the package source tree, e.g., changelog, control, rules, + etc. + package_files: An array of 2-tuples listing the files that should be + copied into the package source tree. + The first element is the path where the file should be placed for the + .install control file to find it, relative to the generated debian + package source directory. + The second element is the file source. + force_version: Optional. Forces the version of the package to start with + this version string if specified. If the last entry in the changelog + is not for a version that starts with this then a dummy entry is + generated with this version and a ~prerelease suffix (so that the + final version will compare as greater). + + Return: + A list of the targets (if any). + """ + if not env.Bit('host_linux'): + return [] + return _InternalBuildDebianPackage(env, debian_files, package_files, + output_dir='$STAGING_DIR', force_version=force_version) + + +def _GetPkgConfigCommand(): + """Return the pkg-config command line to use. + + Returns: + A string specifying the pkg-config command line to use. + """ + return os.environ.get('PKG_CONFIG') or 'pkg-config' + + +def _EscapePosixShellArgument(arg): + """Escapes a shell command line argument so that it is interpreted literally. + + Args: + arg: The shell argument to escape. + + Returns: + The escaped string. + """ + return "'%s'" % arg.replace("'", "'\\''") + + +def _HavePackage(package): + """Whether the given pkg-config package name is present on the build system. + + Args: + package: The name of the package. + + Returns: + True if the package is present, else False + """ + return subprocess.call('%s --exists %s' % ( + _GetPkgConfigCommand(), + _EscapePosixShellArgument(package)), shell=True) == 0 + + +def _GetPackageFlags(flag_type, packages): + """Get the flags needed to compile/link against the given package(s). + + Returns the flags that are needed to compile/link against the given pkg-config + package(s). + + Args: + flag_type: The option to pkg-config specifying the type of flags to get. + packages: The list of package names as strings. + + Returns: + The flags of the requested type. + + Raises: + subprocess.CalledProcessError: The pkg-config command failed. + """ + pkg_config = _GetPkgConfigCommand() + command = ' '.join([pkg_config] + + [_EscapePosixShellArgument(arg) for arg in + [flag_type] + packages]) + process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) + output = process.communicate()[0] + if process.returncode != 0: + raise subprocess.CalledProcessError(process.returncode, pkg_config) + return output.strip().split(' ') + + +def GetPackageParams(env, packages): + """Get the params needed to compile/link against the given package(s). + + Returns the params that are needed to compile/link against the given + pkg-config package(s). + + Args: + env: The current SCons environment. + packages: The name of the package, or a list of names. + + Returns: + A dictionary containing the params. + + Raises: + Exception: One or more of the packages is not installed. + """ + if not env.Bit('host_linux'): + return {} + if not SCons.Util.is_List(packages): + packages = [packages] + for package in packages: + if not _HavePackage(package): + raise Exception(('Required package \"%s\" was not found. Please install ' + 'the package that provides the \"%s.pc\" file.') % + (package, package)) + package_ccflags = _GetPackageFlags('--cflags', packages) + package_libs = _GetPackageFlags('--libs', packages) + # Split package_libs into libs, libdirs, and misc. linker flags. (In a perfect + # world we could just leave libdirs in link_flags, but some linkers are + # somehow confused by the different argument order.) + libs = [flag[2:] for flag in package_libs if flag[0:2] == '-l'] + libdirs = [flag[2:] for flag in package_libs if flag[0:2] == '-L'] + link_flags = [flag for flag in package_libs if flag[0:2] not in ['-l', '-L']] + return { + 'ccflags': package_ccflags, + 'libs': libs, + 'libdirs': libdirs, + 'link_flags': link_flags, + 'dependent_target_settings' : { + 'libs': libs[:], + 'libdirs': libdirs[:], + 'link_flags': link_flags[:], + }, + } + + +def EnableFeatureWherePackagePresent(env, bit, cpp_flag, package): + """Enable a feature if a required pkg-config package is present. + + Args: + env: The current SCons environment. + bit: The name of the Bit to enable when the package is present. + cpp_flag: The CPP flag to enable when the package is present. + package: The name of the package. + """ + if not env.Bit('host_linux'): + return + if _HavePackage(package): + env.SetBits(bit) + env.Append(CPPDEFINES=[cpp_flag]) + else: + print ('Warning: Package \"%s\" not found. Feature \"%s\" will not be ' + 'built. To build with this feature, install the package that ' + 'provides the \"%s.pc\" file.') % (package, bit, package) + +def GetGccVersion(env): + if env.Bit('cross_compile'): + gcc_command = env['CXX'] + else: + gcc_command = 'gcc' + version_string = _OutputFromShellCommand( + '%s --version | head -n 1 |' + r'sed "s/.*\([0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/g"' % gcc_command) + return tuple([int(x or '0') for x in version_string.split('.')]) + +def generate(env): + if env.Bit('linux'): + env.AddMethod(EnableFeatureWherePackagePresent) + env.AddMethod(GetPackageParams) + env.AddMethod(BuildDebianPackage) + env.AddMethod(GetGccVersion) + + +def exists(env): + return 1 # Required by scons diff --git a/talk/site_scons/site_tools/talk_noops.py b/talk/site_scons/site_tools/talk_noops.py new file mode 100644 index 000000000..bb8f10622 --- /dev/null +++ b/talk/site_scons/site_tools/talk_noops.py @@ -0,0 +1,20 @@ +# Copyright 2010 Google Inc. +# All Rights Reserved. +# Author: thaloun@google.com (Tim Haloun) + +"""Noop tool that defines builder functions for non-default platforms to + avoid errors when scanning sconsscripts.""" + +import SCons.Builder + + +def generate(env): + """SCons method.""" + if not env.Bit('windows'): + builder = SCons.Builder.Builder( + action='' + ) + env.Append(BUILDERS={'RES': builder, 'Grit': builder}) + +def exists(env): + return 1 diff --git a/talk/site_scons/talk.py b/talk/site_scons/talk.py new file mode 100644 index 000000000..a274d08c5 --- /dev/null +++ b/talk/site_scons/talk.py @@ -0,0 +1,635 @@ +# Copyright 2010 Google Inc. +# All Rights Reserved. +# +# Author: Tim Haloun (thaloun@google.com) +# Daniel Petersson (dape@google.com) +# +import os +import SCons.Util + +class LibraryInfo: + """Records information on the libraries defined in a build configuration. + + Attributes: + lib_targets: Dictionary of library target params for lookups in + ExtendComponent(). + prebuilt_libraries: Set of all prebuilt static libraries. + system_libraries: Set of libraries not found in the above (used to detect + out-of-order build rules). + """ + + # Dictionary of LibraryInfo objects keyed by BUILD_TYPE value. + __library_info = {} + + @staticmethod + def get(env): + """Gets the LibraryInfo object for the current build type. + + Args: + env: The environment object. + + Returns: + The LibraryInfo object. + """ + return LibraryInfo.__library_info.setdefault(env['BUILD_TYPE'], + LibraryInfo()) + + def __init__(self): + self.lib_targets = {} + self.prebuilt_libraries = set() + self.system_libraries = set() + + +def _GetLibParams(env, lib): + """Gets the params for the given library if it is a library target. + + Returns the params that were specified when the given lib target name was + created, or None if no such lib target has been defined. In the None case, it + additionally records the negative result so as to detect out-of-order + dependencies for future targets. + + Args: + env: The environment object. + lib: The library's name as a string. + + Returns: + Its dictionary of params, or None. + """ + info = LibraryInfo.get(env) + if lib in info.lib_targets: + return info.lib_targets[lib] + else: + if lib not in info.prebuilt_libraries and lib not in info.system_libraries: + info.system_libraries.add(lib) + return None + + +def _RecordLibParams(env, lib, params): + """Record the params used for a library target. + + Record the params used for a library target while checking for several error + conditions. + + Args: + env: The environment object. + lib: The library target's name as a string. + params: Its dictionary of params. + + Raises: + Exception: The lib target has already been recorded, or the lib was + previously declared to be prebuilt, or the lib target is being defined + after a reverse library dependency. + """ + info = LibraryInfo.get(env) + if lib in info.lib_targets: + raise Exception('Multiple definitions of ' + lib) + if lib in info.prebuilt_libraries: + raise Exception(lib + ' already declared as a prebuilt library') + if lib in info.system_libraries: + raise Exception(lib + ' cannot be defined after its reverse library ' + 'dependencies') + info.lib_targets[lib] = params + + +def _IsPrebuiltLibrary(env, lib): + """Checks whether or not the given library is a prebuilt static library. + + Returns whether or not the given library name has been declared to be a + prebuilt static library. In the False case, it additionally records the + negative result so as to detect out-of-order dependencies for future targets. + + Args: + env: The environment object. + lib: The library's name as a string. + + Returns: + True or False + """ + info = LibraryInfo.get(env) + if lib in info.prebuilt_libraries: + return True + else: + if lib not in info.lib_targets and lib not in info.system_libraries: + info.system_libraries.add(lib) + return False + + +def _RecordPrebuiltLibrary(env, lib): + """Record that a library is a prebuilt static library. + + Record that the given library name refers to a prebuilt static library while + checking for several error conditions. + + Args: + env: The environment object. + lib: The library's name as a string. + + Raises: + Exception: The lib has already been recorded to be prebuilt, or the lib was + previously declared as a target, or the lib is being declared as + prebuilt after a reverse library dependency. + """ + info = LibraryInfo.get(env) + if lib in info.prebuilt_libraries: + raise Exception('Multiple prebuilt declarations of ' + lib) + if lib in info.lib_targets: + raise Exception(lib + ' already defined as a target') + if lib in info.system_libraries: + raise Exception(lib + ' cannot be declared as prebuilt after its reverse ' + 'library dependencies') + info.prebuilt_libraries.add(lib) + + +def _GenericLibrary(env, static, **kwargs): + """Extends ComponentLibrary to support multiplatform builds + of dynamic or static libraries. + + Args: + env: The environment object. + kwargs: The keyword arguments. + + Returns: + See swtoolkit ComponentLibrary + """ + params = CombineDicts(kwargs, {'COMPONENT_STATIC': static}) + return ExtendComponent(env, 'ComponentLibrary', **params) + + +def DeclarePrebuiltLibraries(env, libraries): + """Informs the build engine about external static libraries. + + Informs the build engine that the given external library name(s) are prebuilt + static libraries, as opposed to shared libraries. + + Args: + env: The environment object. + libraries: The library or libraries that are being declared as prebuilt + static libraries. + """ + if not SCons.Util.is_List(libraries): + libraries = [libraries] + for library in libraries: + _RecordPrebuiltLibrary(env, library) + + +def Library(env, **kwargs): + """Extends ComponentLibrary to support multiplatform builds of static + libraries. + + Args: + env: The current environment. + kwargs: The keyword arguments. + + Returns: + See swtoolkit ComponentLibrary + """ + return _GenericLibrary(env, True, **kwargs) + + +def DynamicLibrary(env, **kwargs): + """Extends ComponentLibrary to support multiplatform builds + of dynmic libraries. + + Args: + env: The environment object. + kwargs: The keyword arguments. + + Returns: + See swtoolkit ComponentLibrary + """ + return _GenericLibrary(env, False, **kwargs) + + +def Object(env, **kwargs): + return ExtendComponent(env, 'ComponentObject', **kwargs) + + +def Unittest(env, **kwargs): + """Extends ComponentTestProgram to support unittest built + for multiple platforms. + + Args: + env: The current environment. + kwargs: The keyword arguments. + + Returns: + See swtoolkit ComponentProgram. + """ + kwargs['name'] = kwargs['name'] + '_unittest' + + common_test_params = { + 'posix_cppdefines': ['GUNIT_NO_GOOGLE3', 'GTEST_HAS_RTTI=0'], + 'libs': ['unittest_main', 'gunit'] + } + if 'explicit_libs' not in kwargs: + common_test_params['win_libs'] = [ + 'advapi32', + 'crypt32', + 'iphlpapi', + 'secur32', + 'shell32', + 'shlwapi', + 'user32', + 'wininet', + 'ws2_32' + ] + common_test_params['lin_libs'] = [ + 'crypto', + 'pthread', + 'ssl', + ] + + params = CombineDicts(kwargs, common_test_params) + return ExtendComponent(env, 'ComponentTestProgram', **params) + + +def App(env, **kwargs): + """Extends ComponentProgram to support executables with platform specific + options. + + Args: + env: The current environment. + kwargs: The keyword arguments. + + Returns: + See swtoolkit ComponentProgram. + """ + if 'explicit_libs' not in kwargs: + common_app_params = { + 'win_libs': [ + 'advapi32', + 'crypt32', + 'iphlpapi', + 'secur32', + 'shell32', + 'shlwapi', + 'user32', + 'wininet', + 'ws2_32' + ]} + params = CombineDicts(kwargs, common_app_params) + else: + params = kwargs + return ExtendComponent(env, 'ComponentProgram', **params) + +def WiX(env, **kwargs): + """ Extends the WiX builder + Args: + env: The current environment. + kwargs: The keyword arguments. + + Returns: + The node produced by the environment's wix builder + """ + return ExtendComponent(env, 'WiX', **kwargs) + +def Repository(env, at, path): + """Maps a directory external to $MAIN_DIR to the given path so that sources + compiled from it end up in the correct place under $OBJ_DIR. NOT required + when only referring to header files. + + Args: + env: The current environment object. + at: The 'mount point' within the current directory. + path: Path to the actual directory. + """ + env.Dir(at).addRepository(env.Dir(path)) + + +def Components(*paths): + """Completes the directory paths with the correct file + names such that the directory/directory.scons name + convention can be used. + + Args: + paths: The paths to complete. If it refers to an existing + file then it is ignored. + + Returns: + The completed lif scons files that are needed to build talk. + """ + files = [] + for path in paths: + if os.path.isfile(path): + files.append(path) + else: + files.append(ExpandSconsPath(path)) + return files + + +def ExpandSconsPath(path): + """Expands a directory path into the path to the + scons file that our build uses. + Ex: magiflute/plugin/common => magicflute/plugin/common/common.scons + + Args: + path: The directory path to expand. + + Returns: + The expanded path. + """ + return '%s/%s.scons' % (path, os.path.basename(path)) + + +def ReadVersion(filename): + """Executes the supplied file and pulls out a version definition from it. """ + defs = {} + execfile(str(filename), defs) + if 'version' not in defs: + return '0.0.0.0' + version = defs['version'] + parts = version.split(',') + build = os.environ.get('GOOGLE_VERSION_BUILDNUMBER') + if build: + parts[-1] = str(build) + return '.'.join(parts) + + +#------------------------------------------------------------------------------- +# Helper methods for translating talk.Foo() declarations in to manipulations of +# environmuent construction variables, including parameter parsing and merging, +# +def PopEntry(dictionary, key): + """Get the value from a dictionary by key. If the key + isn't in the dictionary then None is returned. If it is in + the dictionary the value is fetched and then is it removed + from the dictionary. + + Args: + dictionary: The dictionary. + key: The key to get the value for. + Returns: + The value or None if the key is missing. + """ + value = None + if key in dictionary: + value = dictionary[key] + dictionary.pop(key) + return value + + +def MergeAndFilterByPlatform(env, params): + """Take a dictionary of arguments to lists of values, and, depending on + which platform we are targetting, merge the lists of associated keys. + Merge by combining value lists like so: + {win_foo = [a,b], lin_foo = [c,d], foo = [e], mac_bar = [f], bar = [g] } + becomes {foo = [a,b,e], bar = [g]} on windows, and + {foo = [e], bar = [f,g]} on mac + + Args: + env: The hammer environment which knows which platforms are active + params: The keyword argument dictionary. + Returns: + A new dictionary with the filtered and combined entries of params + """ + platforms = { + 'linux': 'lin_', + 'mac': 'mac_', + 'posix': 'posix_', + 'windows': 'win_', + } + active_prefixes = [ + platforms[x] for x in iter(platforms) if env.Bit(x) + ] + inactive_prefixes = [ + platforms[x] for x in iter(platforms) if not env.Bit(x) + ] + + merged = {} + for arg, values in params.iteritems(): + inactive_platform = False + + key = arg + + for prefix in active_prefixes: + if arg.startswith(prefix): + key = arg[len(prefix):] + + for prefix in inactive_prefixes: + if arg.startswith(prefix): + inactive_platform = True + + if inactive_platform: + continue + + AddToDict(merged, key, values) + + return merged + + +def MergeSettingsFromLibraryDependencies(env, params): + if 'libs' in params: + for lib in params['libs']: + libparams = _GetLibParams(env, lib) + if libparams: + if 'dependent_target_settings' in libparams: + params = CombineDicts( + params, + MergeAndFilterByPlatform( + env, + libparams['dependent_target_settings'])) + return params + + +def ExtendComponent(env, component, **kwargs): + """A wrapper around a scons builder function that preprocesses and post- + processes its inputs and outputs. For example, it merges and filters + certain keyword arguments before appending them to the environments + construction variables. It can build signed targets and 64bit copies + of targets as well. + + Args: + env: The hammer environment with which to build the target + component: The environment's builder function, e.g. ComponentProgram + kwargs: keyword arguments that are either merged, translated, and passed on + to the call to component, or which control execution. + TODO(): Document the fields, such as cppdefines->CPPDEFINES, + prepend_includedirs, include_talk_media_libs, etc. + Returns: + The output node returned by the call to component, or a subsequent signed + dependant node. + """ + env = env.Clone() + + # prune parameters intended for other platforms, then merge + params = MergeAndFilterByPlatform(env, kwargs) + + # get the 'target' field + name = PopEntry(params, 'name') + + # get the 'packages' field and process it if present (only used for Linux). + packages = PopEntry(params, 'packages') + if packages and len(packages): + params = CombineDicts(params, env.GetPackageParams(packages)) + + # save pristine params of lib targets for future reference + if 'ComponentLibrary' == component: + _RecordLibParams(env, name, dict(params)) + + # add any dependent target settings from library dependencies + params = MergeSettingsFromLibraryDependencies(env, params) + + # if this is a signed binary we need to make an unsigned version first + signed = env.Bit('windows') and PopEntry(params, 'signed') + if signed: + name = 'unsigned_' + name + + # potentially exit now + srcs = PopEntry(params, 'srcs') + if not srcs or not hasattr(env, component): + return None + + # apply any explicit dependencies + dependencies = PopEntry(params, 'depends') + if dependencies is not None: + env.Depends(name, dependencies) + + # put the contents of params into the environment + # some entries are renamed then appended, others renamed then prepended + appends = { + 'cppdefines' : 'CPPDEFINES', + 'libdirs' : 'LIBPATH', + 'link_flags' : 'LINKFLAGS', + 'libs' : 'LIBS', + 'FRAMEWORKS' : 'FRAMEWORKS', + } + prepends = {} + if env.Bit('windows'): + # MSVC compile flags have precedence at the beginning ... + prepends['ccflags'] = 'CCFLAGS' + else: + # ... while GCC compile flags have precedence at the end + appends['ccflags'] = 'CCFLAGS' + if PopEntry(params, 'prepend_includedirs'): + prepends['includedirs'] = 'CPPPATH' + else: + appends['includedirs'] = 'CPPPATH' + + for field, var in appends.items(): + values = PopEntry(params, field) + if values is not None: + env.Append(**{var : values}) + for field, var in prepends.items(): + values = PopEntry(params, field) + if values is not None: + env.Prepend(**{var : values}) + + # any other parameters are replaced without renaming + for field, value in params.items(): + env.Replace(**{field : value}) + + if env.Bit('linux') and 'LIBS' in env: + libs = env['LIBS'] + # When using --as-needed + --start/end-group, shared libraries need to come + # after --end-group on the command-line because the pruning decision only + # considers the preceding modules and --start/end-group may cause the + # effective position of early static libraries on the command-line to be + # deferred to the point of --end-group. To effect this, we move shared libs + # into _LIBFLAGS, which has the --end-group as its first entry. SCons does + # not track dependencies on system shared libraries anyway so we lose + # nothing by removing them from LIBS. + static_libs = [lib for lib in libs if + _GetLibParams(env, lib) or _IsPrebuiltLibrary(env, lib)] + shared_libs = ['-l' + lib for lib in libs if not + (_GetLibParams(env, lib) or _IsPrebuiltLibrary(env, lib))] + env.Replace(LIBS=static_libs) + env.Append(_LIBFLAGS=shared_libs) + + # invoke the builder function + builder = getattr(env, component) + + node = builder(name, srcs) + + if env.Bit('mac') and 'ComponentProgram' == component: + # Build .dSYM debug packages. This is useful even for non-stripped + # binaries, as the dsym utility will fetch symbols from all + # statically-linked libraries (the linker doesn't include them in to the + # final binary). + build_dsym = env.Command( + env.Dir('$STAGING_DIR/%s.dSYM' % node[0]), + node, + 'mkdir -p `dirname $TARGET` && dsymutil -o $TARGET $SOURCE') + env.Alias('all_dsym', env.Alias('%s.dSYM' % node[0], build_dsym)) + + if signed: + # Get the name of the built binary, then get the name of the final signed + # version from it. We need the output path since we don't know the file + # extension beforehand. + target = node[0].path.split('_', 1)[1] + # postsignprefix: If defined, postsignprefix is a string that should be + # prepended to the target executable. This is to provide a work around + # for EXEs and DLLs with the same name, which thus have PDBs with the + # same name. Setting postsignprefix allows the EXE and its PDB + # to be renamed and copied in a previous step; then the desired + # name of the EXE (but not PDB) is reconstructed after signing. + postsignprefix = PopEntry(params, 'postsignprefix') + if postsignprefix is not None: + target = postsignprefix + target + signed_node = env.SignedBinary( + source = node, + target = '$STAGING_DIR/' + target, + ) + env.Alias('signed_binaries', signed_node) + return signed_node + + return node + + +def AddToDict(dictionary, key, values, append=True): + """Merge the given key value(s) pair into a dictionary. If it contains an + entry with that key already, then combine by appending or prepending the + values as directed. Otherwise, assign a new keyvalue pair. + """ + if values is None: + return + + if key not in dictionary: + dictionary[key] = values + return + + cur = dictionary[key] + # TODO(dape): Make sure that there are no duplicates + # in the list. I can't use python set for this since + # the nodes that are returned by the SCONS builders + # are not hashable. + # dictionary[key] = list(set(cur).union(set(values))) + if append: + dictionary[key] = cur + values + else: + dictionary[key] = values + cur + + +def CombineDicts(a, b): + """Unions two dictionaries of arrays/dictionaries. + + Unions two dictionaries of arrays/dictionaries by combining the values of keys + shared between them. The original dictionaries should not be used again after + this call. + + Args: + a: First dict. + b: Second dict. + + Returns: + The union of a and b. + """ + c = {} + for key in a: + if key in b: + aval = a[key] + bval = b.pop(key) + if isinstance(aval, dict) and isinstance(bval, dict): + c[key] = CombineDicts(aval, bval) + else: + c[key] = aval + bval + else: + c[key] = a[key] + + for key in b: + c[key] = b[key] + + return c + + +def RenameKey(d, old, new, append=True): + AddToDict(d, new, PopEntry(d, old), append) diff --git a/talk/sound/alsasoundsystem.cc b/talk/sound/alsasoundsystem.cc new file mode 100644 index 000000000..de9e2d67f --- /dev/null +++ b/talk/sound/alsasoundsystem.cc @@ -0,0 +1,761 @@ +/* + * libjingle + * Copyright 2004--2010, 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/sound/alsasoundsystem.h" + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringutils.h" +#include "talk/base/timeutils.h" +#include "talk/base/worker.h" +#include "talk/sound/sounddevicelocator.h" +#include "talk/sound/soundinputstreaminterface.h" +#include "talk/sound/soundoutputstreaminterface.h" + +namespace cricket { + +// Lookup table from the cricket format enum in soundsysteminterface.h to +// ALSA's enums. +static const snd_pcm_format_t kCricketFormatToAlsaFormatTable[] = { + // The order here must match the order in soundsysteminterface.h + SND_PCM_FORMAT_S16_LE, +}; + +// Lookup table for the size of a single sample of a given format. +static const size_t kCricketFormatToSampleSizeTable[] = { + // The order here must match the order in soundsysteminterface.h + sizeof(int16_t), // 2 +}; + +// Minimum latency we allow, in microseconds. This is more or less arbitrary, +// but it has to be at least large enough to be able to buffer data during a +// missed context switch, and the typical Linux scheduling quantum is 10ms. +static const int kMinimumLatencyUsecs = 20 * 1000; + +// The latency we'll use for kNoLatencyRequirements (chosen arbitrarily). +static const int kDefaultLatencyUsecs = kMinimumLatencyUsecs * 2; + +// We translate newlines in ALSA device descriptions to hyphens. +static const char kAlsaDescriptionSearch[] = "\n"; +static const char kAlsaDescriptionReplace[] = " - "; + +class AlsaDeviceLocator : public SoundDeviceLocator { + public: + AlsaDeviceLocator(const std::string &name, + const std::string &device_name) + : SoundDeviceLocator(name, device_name) { + // The ALSA descriptions have newlines in them, which won't show up in + // a drop-down box. Replace them with hyphens. + talk_base::replace_substrs(kAlsaDescriptionSearch, + sizeof(kAlsaDescriptionSearch) - 1, + kAlsaDescriptionReplace, + sizeof(kAlsaDescriptionReplace) - 1, + &name_); + } + + virtual SoundDeviceLocator *Copy() const { + return new AlsaDeviceLocator(*this); + } +}; + +// Functionality that is common to both AlsaInputStream and AlsaOutputStream. +class AlsaStream { + public: + AlsaStream(AlsaSoundSystem *alsa, + snd_pcm_t *handle, + size_t frame_size, + int wait_timeout_ms, + int flags, + int freq) + : alsa_(alsa), + handle_(handle), + frame_size_(frame_size), + wait_timeout_ms_(wait_timeout_ms), + flags_(flags), + freq_(freq) { + } + + ~AlsaStream() { + Close(); + } + + // Waits for the stream to be ready to accept/return more data, and returns + // how much can be written/read, or 0 if we need to Wait() again. + snd_pcm_uframes_t Wait() { + snd_pcm_sframes_t frames; + // Ideally we would not use snd_pcm_wait() and instead hook snd_pcm_poll_* + // into PhysicalSocketServer, but PhysicalSocketServer is nasty enough + // already and the current clients of SoundSystemInterface do not run + // anything else on their worker threads, so snd_pcm_wait() is good enough. + frames = symbol_table()->snd_pcm_avail_update()(handle_); + if (frames < 0) { + LOG(LS_ERROR) << "snd_pcm_avail_update(): " << GetError(frames); + Recover(frames); + return 0; + } else if (frames > 0) { + // Already ready, so no need to wait. + return frames; + } + // Else no space/data available, so must wait. + int ready = symbol_table()->snd_pcm_wait()(handle_, wait_timeout_ms_); + if (ready < 0) { + LOG(LS_ERROR) << "snd_pcm_wait(): " << GetError(ready); + Recover(ready); + return 0; + } else if (ready == 0) { + // Timeout, so nothing can be written/read right now. + // We set the timeout to twice the requested latency, so continuous + // timeouts are indicative of a problem, so log as a warning. + LOG(LS_WARNING) << "Timeout while waiting on stream"; + return 0; + } + // Else ready > 0 (i.e., 1), so it's ready. Get count. + frames = symbol_table()->snd_pcm_avail_update()(handle_); + if (frames < 0) { + LOG(LS_ERROR) << "snd_pcm_avail_update(): " << GetError(frames); + Recover(frames); + return 0; + } else if (frames == 0) { + // wait() said we were ready, so this ought to have been positive. Has + // been observed to happen in practice though. + LOG(LS_WARNING) << "Spurious wake-up"; + } + return frames; + } + + int CurrentDelayUsecs() { + if (!(flags_ & SoundSystemInterface::FLAG_REPORT_LATENCY)) { + return 0; + } + + snd_pcm_sframes_t delay; + int err = symbol_table()->snd_pcm_delay()(handle_, &delay); + if (err != 0) { + LOG(LS_ERROR) << "snd_pcm_delay(): " << GetError(err); + Recover(err); + // We'd rather continue playout/capture with an incorrect delay than stop + // it altogether, so return a valid value. + return 0; + } + // The delay is in frames. Convert to microseconds. + return delay * talk_base::kNumMicrosecsPerSec / freq_; + } + + // Used to recover from certain recoverable errors, principally buffer overrun + // or underrun (identified as EPIPE). Without calling this the stream stays + // in the error state forever. + bool Recover(int error) { + int err; + err = symbol_table()->snd_pcm_recover()( + handle_, + error, + // Silent; i.e., no logging on stderr. + 1); + if (err != 0) { + // Docs say snd_pcm_recover returns the original error if it is not one + // of the recoverable ones, so this log message will probably contain the + // same error twice. + LOG(LS_ERROR) << "Unable to recover from \"" << GetError(error) << "\": " + << GetError(err); + return false; + } + if (error == -EPIPE && // Buffer underrun/overrun. + symbol_table()->snd_pcm_stream()(handle_) == SND_PCM_STREAM_CAPTURE) { + // For capture streams we also have to repeat the explicit start() to get + // data flowing again. + err = symbol_table()->snd_pcm_start()(handle_); + if (err != 0) { + LOG(LS_ERROR) << "snd_pcm_start(): " << GetError(err); + return false; + } + } + return true; + } + + bool Close() { + if (handle_) { + int err; + err = symbol_table()->snd_pcm_drop()(handle_); + if (err != 0) { + LOG(LS_ERROR) << "snd_pcm_drop(): " << GetError(err); + // Continue anyways. + } + err = symbol_table()->snd_pcm_close()(handle_); + if (err != 0) { + LOG(LS_ERROR) << "snd_pcm_close(): " << GetError(err); + // Continue anyways. + } + handle_ = NULL; + } + return true; + } + + AlsaSymbolTable *symbol_table() { + return &alsa_->symbol_table_; + } + + snd_pcm_t *handle() { + return handle_; + } + + const char *GetError(int err) { + return alsa_->GetError(err); + } + + size_t frame_size() { + return frame_size_; + } + + private: + AlsaSoundSystem *alsa_; + snd_pcm_t *handle_; + size_t frame_size_; + int wait_timeout_ms_; + int flags_; + int freq_; + + DISALLOW_COPY_AND_ASSIGN(AlsaStream); +}; + +// Implementation of an input stream. See soundinputstreaminterface.h regarding +// thread-safety. +class AlsaInputStream : + public SoundInputStreamInterface, + private talk_base::Worker { + public: + AlsaInputStream(AlsaSoundSystem *alsa, + snd_pcm_t *handle, + size_t frame_size, + int wait_timeout_ms, + int flags, + int freq) + : stream_(alsa, handle, frame_size, wait_timeout_ms, flags, freq), + buffer_size_(0) { + } + + virtual ~AlsaInputStream() { + bool success = StopReading(); + // We need that to live. + VERIFY(success); + } + + virtual bool StartReading() { + return StartWork(); + } + + virtual bool StopReading() { + return StopWork(); + } + + virtual bool GetVolume(int *volume) { + // TODO: Implement this. + return false; + } + + virtual bool SetVolume(int volume) { + // TODO: Implement this. + return false; + } + + virtual bool Close() { + return StopReading() && stream_.Close(); + } + + virtual int LatencyUsecs() { + return stream_.CurrentDelayUsecs(); + } + + private: + // Inherited from Worker. + virtual void OnStart() { + HaveWork(); + } + + // Inherited from Worker. + virtual void OnHaveWork() { + // Block waiting for data. + snd_pcm_uframes_t avail = stream_.Wait(); + if (avail > 0) { + // Data is available. + size_t size = avail * stream_.frame_size(); + if (size > buffer_size_) { + // Must increase buffer size. + buffer_.reset(new char[size]); + buffer_size_ = size; + } + // Read all the data. + snd_pcm_sframes_t read = stream_.symbol_table()->snd_pcm_readi()( + stream_.handle(), + buffer_.get(), + avail); + if (read < 0) { + LOG(LS_ERROR) << "snd_pcm_readi(): " << GetError(read); + stream_.Recover(read); + } else if (read == 0) { + // Docs say this shouldn't happen. + ASSERT(false); + LOG(LS_ERROR) << "No data?"; + } else { + // Got data. Pass it off to the app. + SignalSamplesRead(buffer_.get(), + read * stream_.frame_size(), + this); + } + } + // Check for more data with no delay, after any pending messages are + // dispatched. + HaveWork(); + } + + // Inherited from Worker. + virtual void OnStop() { + // Nothing to do. + } + + const char *GetError(int err) { + return stream_.GetError(err); + } + + AlsaStream stream_; + talk_base::scoped_array buffer_; + size_t buffer_size_; + + DISALLOW_COPY_AND_ASSIGN(AlsaInputStream); +}; + +// Implementation of an output stream. See soundoutputstreaminterface.h +// regarding thread-safety. +class AlsaOutputStream : + public SoundOutputStreamInterface, + private talk_base::Worker { + public: + AlsaOutputStream(AlsaSoundSystem *alsa, + snd_pcm_t *handle, + size_t frame_size, + int wait_timeout_ms, + int flags, + int freq) + : stream_(alsa, handle, frame_size, wait_timeout_ms, flags, freq) { + } + + virtual ~AlsaOutputStream() { + bool success = DisableBufferMonitoring(); + // We need that to live. + VERIFY(success); + } + + virtual bool EnableBufferMonitoring() { + return StartWork(); + } + + virtual bool DisableBufferMonitoring() { + return StopWork(); + } + + virtual bool WriteSamples(const void *sample_data, + size_t size) { + if (size % stream_.frame_size() != 0) { + // No client of SoundSystemInterface does this, so let's not support it. + // (If we wanted to support it, we'd basically just buffer the fractional + // frame until we get more data.) + ASSERT(false); + LOG(LS_ERROR) << "Writes with fractional frames are not supported"; + return false; + } + snd_pcm_uframes_t frames = size / stream_.frame_size(); + snd_pcm_sframes_t written = stream_.symbol_table()->snd_pcm_writei()( + stream_.handle(), + sample_data, + frames); + if (written < 0) { + LOG(LS_ERROR) << "snd_pcm_writei(): " << GetError(written); + stream_.Recover(written); + return false; + } else if (static_cast(written) < frames) { + // Shouldn't happen. Drop the rest of the data. + LOG(LS_ERROR) << "Stream wrote only " << written << " of " << frames + << " frames!"; + return false; + } + return true; + } + + virtual bool GetVolume(int *volume) { + // TODO: Implement this. + return false; + } + + virtual bool SetVolume(int volume) { + // TODO: Implement this. + return false; + } + + virtual bool Close() { + return DisableBufferMonitoring() && stream_.Close(); + } + + virtual int LatencyUsecs() { + return stream_.CurrentDelayUsecs(); + } + + private: + // Inherited from Worker. + virtual void OnStart() { + HaveWork(); + } + + // Inherited from Worker. + virtual void OnHaveWork() { + snd_pcm_uframes_t avail = stream_.Wait(); + if (avail > 0) { + size_t space = avail * stream_.frame_size(); + SignalBufferSpace(space, this); + } + HaveWork(); + } + + // Inherited from Worker. + virtual void OnStop() { + // Nothing to do. + } + + const char *GetError(int err) { + return stream_.GetError(err); + } + + AlsaStream stream_; + + DISALLOW_COPY_AND_ASSIGN(AlsaOutputStream); +}; + +AlsaSoundSystem::AlsaSoundSystem() : initialized_(false) {} + +AlsaSoundSystem::~AlsaSoundSystem() { + // Not really necessary, because Terminate() doesn't really do anything. + Terminate(); +} + +bool AlsaSoundSystem::Init() { + if (IsInitialized()) { + return true; + } + + // Load libasound. + if (!symbol_table_.Load()) { + // Very odd for a Linux machine to not have a working libasound ... + LOG(LS_ERROR) << "Failed to load symbol table"; + return false; + } + + initialized_ = true; + + return true; +} + +void AlsaSoundSystem::Terminate() { + if (!IsInitialized()) { + return; + } + + initialized_ = false; + + // We do not unload the symbol table because we may need it again soon if + // Init() is called again. +} + +bool AlsaSoundSystem::EnumeratePlaybackDevices( + SoundDeviceLocatorList *devices) { + return EnumerateDevices(devices, false); +} + +bool AlsaSoundSystem::EnumerateCaptureDevices( + SoundDeviceLocatorList *devices) { + return EnumerateDevices(devices, true); +} + +bool AlsaSoundSystem::GetDefaultPlaybackDevice(SoundDeviceLocator **device) { + return GetDefaultDevice(device); +} + +bool AlsaSoundSystem::GetDefaultCaptureDevice(SoundDeviceLocator **device) { + return GetDefaultDevice(device); +} + +SoundOutputStreamInterface *AlsaSoundSystem::OpenPlaybackDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms) { + return OpenDevice( + device, + params, + SND_PCM_STREAM_PLAYBACK, + &AlsaSoundSystem::StartOutputStream); +} + +SoundInputStreamInterface *AlsaSoundSystem::OpenCaptureDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms) { + return OpenDevice( + device, + params, + SND_PCM_STREAM_CAPTURE, + &AlsaSoundSystem::StartInputStream); +} + +const char *AlsaSoundSystem::GetName() const { + return "ALSA"; +} + +bool AlsaSoundSystem::EnumerateDevices( + SoundDeviceLocatorList *devices, + bool capture_not_playback) { + ClearSoundDeviceLocatorList(devices); + + if (!IsInitialized()) { + return false; + } + + const char *type = capture_not_playback ? "Input" : "Output"; + // dmix and dsnoop are only for playback and capture, respectively, but ALSA + // stupidly includes them in both lists. + const char *ignore_prefix = capture_not_playback ? "dmix:" : "dsnoop:"; + // (ALSA lists many more "devices" of questionable interest, but we show them + // just in case the weird devices may actually be desirable for some + // users/systems.) + const char *ignore_default = "default"; + const char *ignore_null = "null"; + const char *ignore_pulse = "pulse"; + // The 'pulse' entry has a habit of mysteriously disappearing when you query + // a second time. Remove it from our list. (GIPS lib did the same thing.) + int err; + + void **hints; + err = symbol_table_.snd_device_name_hint()(-1, // All cards + "pcm", // Only PCM devices + &hints); + if (err != 0) { + LOG(LS_ERROR) << "snd_device_name_hint(): " << GetError(err); + return false; + } + + for (void **list = hints; *list != NULL; ++list) { + char *actual_type = symbol_table_.snd_device_name_get_hint()(*list, "IOID"); + if (actual_type) { // NULL means it's both. + bool wrong_type = (strcmp(actual_type, type) != 0); + free(actual_type); + if (wrong_type) { + // Wrong type of device (i.e., input vs. output). + continue; + } + } + + char *name = symbol_table_.snd_device_name_get_hint()(*list, "NAME"); + if (!name) { + LOG(LS_ERROR) << "Device has no name???"; + // Skip it. + continue; + } + + // Now check if we actually want to show this device. + if (strcmp(name, ignore_default) != 0 && + strcmp(name, ignore_null) != 0 && + strcmp(name, ignore_pulse) != 0 && + !talk_base::starts_with(name, ignore_prefix)) { + + // Yes, we do. + char *desc = symbol_table_.snd_device_name_get_hint()(*list, "DESC"); + if (!desc) { + // Virtual devices don't necessarily have descriptions. Use their names + // instead (not pretty!). + desc = name; + } + + AlsaDeviceLocator *device = new AlsaDeviceLocator(desc, name); + + devices->push_back(device); + + if (desc != name) { + free(desc); + } + } + + free(name); + } + + err = symbol_table_.snd_device_name_free_hint()(hints); + if (err != 0) { + LOG(LS_ERROR) << "snd_device_name_free_hint(): " << GetError(err); + // Continue and return true anyways, since we did get the whole list. + } + + return true; +} + +bool AlsaSoundSystem::GetDefaultDevice(SoundDeviceLocator **device) { + if (!IsInitialized()) { + return false; + } + *device = new AlsaDeviceLocator("Default device", "default"); + return true; +} + +inline size_t AlsaSoundSystem::FrameSize(const OpenParams ¶ms) { + ASSERT(static_cast(params.format) < + ARRAY_SIZE(kCricketFormatToSampleSizeTable)); + return kCricketFormatToSampleSizeTable[params.format] * params.channels; +} + +template +StreamInterface *AlsaSoundSystem::OpenDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms, + snd_pcm_stream_t type, + StreamInterface *(AlsaSoundSystem::*start_fn)( + snd_pcm_t *handle, + size_t frame_size, + int wait_timeout_ms, + int flags, + int freq)) { + + if (!IsInitialized()) { + return NULL; + } + + StreamInterface *stream; + int err; + + const char *dev = static_cast(device)-> + device_name().c_str(); + + snd_pcm_t *handle = NULL; + err = symbol_table_.snd_pcm_open()( + &handle, + dev, + type, + // No flags. + 0); + if (err != 0) { + LOG(LS_ERROR) << "snd_pcm_open(" << dev << "): " << GetError(err); + return NULL; + } + LOG(LS_VERBOSE) << "Opening " << dev; + ASSERT(handle); // If open succeeded, handle ought to be valid + + // Compute requested latency in microseconds. + int latency; + if (params.latency == kNoLatencyRequirements) { + latency = kDefaultLatencyUsecs; + } else { + // kLowLatency is 0, so we treat it the same as a request for zero latency. + // Compute what the user asked for. + latency = talk_base::kNumMicrosecsPerSec * + params.latency / + params.freq / + FrameSize(params); + // And this is what we'll actually use. + latency = talk_base::_max(latency, kMinimumLatencyUsecs); + } + + ASSERT(static_cast(params.format) < + ARRAY_SIZE(kCricketFormatToAlsaFormatTable)); + + err = symbol_table_.snd_pcm_set_params()( + handle, + kCricketFormatToAlsaFormatTable[params.format], + // SoundSystemInterface only supports interleaved audio. + SND_PCM_ACCESS_RW_INTERLEAVED, + params.channels, + params.freq, + 1, // Allow ALSA to resample. + latency); + if (err != 0) { + LOG(LS_ERROR) << "snd_pcm_set_params(): " << GetError(err); + goto fail; + } + + err = symbol_table_.snd_pcm_prepare()(handle); + if (err != 0) { + LOG(LS_ERROR) << "snd_pcm_prepare(): " << GetError(err); + goto fail; + } + + stream = (this->*start_fn)( + handle, + FrameSize(params), + // We set the wait time to twice the requested latency, so that wait + // timeouts should be rare. + 2 * latency / talk_base::kNumMicrosecsPerMillisec, + params.flags, + params.freq); + if (stream) { + return stream; + } + // Else fall through. + + fail: + err = symbol_table_.snd_pcm_close()(handle); + if (err != 0) { + LOG(LS_ERROR) << "snd_pcm_close(): " << GetError(err); + } + return NULL; +} + +SoundOutputStreamInterface *AlsaSoundSystem::StartOutputStream( + snd_pcm_t *handle, + size_t frame_size, + int wait_timeout_ms, + int flags, + int freq) { + // Nothing to do here but instantiate the stream. + return new AlsaOutputStream( + this, handle, frame_size, wait_timeout_ms, flags, freq); +} + +SoundInputStreamInterface *AlsaSoundSystem::StartInputStream( + snd_pcm_t *handle, + size_t frame_size, + int wait_timeout_ms, + int flags, + int freq) { + // Output streams start automatically once enough data has been written, but + // input streams must be started manually or else snd_pcm_wait() will never + // return true. + int err; + err = symbol_table_.snd_pcm_start()(handle); + if (err != 0) { + LOG(LS_ERROR) << "snd_pcm_start(): " << GetError(err); + return NULL; + } + return new AlsaInputStream( + this, handle, frame_size, wait_timeout_ms, flags, freq); +} + +inline const char *AlsaSoundSystem::GetError(int err) { + return symbol_table_.snd_strerror()(err); +} + +} // namespace cricket diff --git a/talk/sound/alsasoundsystem.h b/talk/sound/alsasoundsystem.h new file mode 100644 index 000000000..870f25ee3 --- /dev/null +++ b/talk/sound/alsasoundsystem.h @@ -0,0 +1,120 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_ALSASOUNDSYSTEM_H_ +#define TALK_SOUND_ALSASOUNDSYSTEM_H_ + +#include "talk/base/constructormagic.h" +#include "talk/sound/alsasymboltable.h" +#include "talk/sound/soundsysteminterface.h" + +namespace cricket { + +class AlsaStream; +class AlsaInputStream; +class AlsaOutputStream; + +// Sound system implementation for ALSA, the predominant sound device API on +// Linux (but typically not used directly by applications anymore). +class AlsaSoundSystem : public SoundSystemInterface { + friend class AlsaStream; + friend class AlsaInputStream; + friend class AlsaOutputStream; + public: + static SoundSystemInterface *Create() { + return new AlsaSoundSystem(); + } + + AlsaSoundSystem(); + + virtual ~AlsaSoundSystem(); + + virtual bool Init(); + virtual void Terminate(); + + virtual bool EnumeratePlaybackDevices(SoundDeviceLocatorList *devices); + virtual bool EnumerateCaptureDevices(SoundDeviceLocatorList *devices); + + virtual bool GetDefaultPlaybackDevice(SoundDeviceLocator **device); + virtual bool GetDefaultCaptureDevice(SoundDeviceLocator **device); + + virtual SoundOutputStreamInterface *OpenPlaybackDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms); + virtual SoundInputStreamInterface *OpenCaptureDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms); + + virtual const char *GetName() const; + + private: + bool IsInitialized() { return initialized_; } + + bool EnumerateDevices(SoundDeviceLocatorList *devices, + bool capture_not_playback); + + bool GetDefaultDevice(SoundDeviceLocator **device); + + static size_t FrameSize(const OpenParams ¶ms); + + template + StreamInterface *OpenDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms, + snd_pcm_stream_t type, + StreamInterface *(AlsaSoundSystem::*start_fn)( + snd_pcm_t *handle, + size_t frame_size, + int wait_timeout_ms, + int flags, + int freq)); + + SoundOutputStreamInterface *StartOutputStream( + snd_pcm_t *handle, + size_t frame_size, + int wait_timeout_ms, + int flags, + int freq); + + SoundInputStreamInterface *StartInputStream( + snd_pcm_t *handle, + size_t frame_size, + int wait_timeout_ms, + int flags, + int freq); + + const char *GetError(int err); + + bool initialized_; + AlsaSymbolTable symbol_table_; + + DISALLOW_COPY_AND_ASSIGN(AlsaSoundSystem); +}; + +} // namespace cricket + +#endif // TALK_SOUND_ALSASOUNDSYSTEM_H_ diff --git a/talk/sound/alsasymboltable.cc b/talk/sound/alsasymboltable.cc new file mode 100644 index 000000000..290c7290b --- /dev/null +++ b/talk/sound/alsasymboltable.cc @@ -0,0 +1,37 @@ +/* + * libjingle + * Copyright 2004--2010, 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/sound/alsasymboltable.h" + +namespace cricket { + +#define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME ALSA_SYMBOLS_CLASS_NAME +#define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST ALSA_SYMBOLS_LIST +#define LATE_BINDING_SYMBOL_TABLE_DLL_NAME "libasound.so.2" +#include "talk/base/latebindingsymboltable.cc.def" + +} // namespace cricket diff --git a/talk/sound/alsasymboltable.h b/talk/sound/alsasymboltable.h new file mode 100644 index 000000000..cf7803f37 --- /dev/null +++ b/talk/sound/alsasymboltable.h @@ -0,0 +1,66 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_ALSASYMBOLTABLE_H_ +#define TALK_SOUND_ALSASYMBOLTABLE_H_ + +#include + +#include "talk/base/latebindingsymboltable.h" + +namespace cricket { + +#define ALSA_SYMBOLS_CLASS_NAME AlsaSymbolTable +// The ALSA symbols we need, as an X-Macro list. +// This list must contain precisely every libasound function that is used in +// alsasoundsystem.cc. +#define ALSA_SYMBOLS_LIST \ + X(snd_device_name_free_hint) \ + X(snd_device_name_get_hint) \ + X(snd_device_name_hint) \ + X(snd_pcm_avail_update) \ + X(snd_pcm_close) \ + X(snd_pcm_delay) \ + X(snd_pcm_drop) \ + X(snd_pcm_open) \ + X(snd_pcm_prepare) \ + X(snd_pcm_readi) \ + X(snd_pcm_recover) \ + X(snd_pcm_set_params) \ + X(snd_pcm_start) \ + X(snd_pcm_stream) \ + X(snd_pcm_wait) \ + X(snd_pcm_writei) \ + X(snd_strerror) + +#define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME ALSA_SYMBOLS_CLASS_NAME +#define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST ALSA_SYMBOLS_LIST +#include "talk/base/latebindingsymboltable.h.def" + +} // namespace cricket + +#endif // TALK_SOUND_ALSASYMBOLTABLE_H_ diff --git a/talk/sound/automaticallychosensoundsystem.h b/talk/sound/automaticallychosensoundsystem.h new file mode 100644 index 000000000..026c080e6 --- /dev/null +++ b/talk/sound/automaticallychosensoundsystem.h @@ -0,0 +1,105 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_AUTOMATICALLYCHOSENSOUNDSYSTEM_H_ +#define TALK_SOUND_AUTOMATICALLYCHOSENSOUNDSYSTEM_H_ + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/sound/soundsysteminterface.h" +#include "talk/sound/soundsystemproxy.h" + +namespace cricket { + +// A function type that creates an instance of a sound system implementation. +typedef SoundSystemInterface *(*SoundSystemCreator)(); + +// An AutomaticallyChosenSoundSystem is a sound system proxy that defers to +// an instance of the first sound system implementation in a list that +// successfully initializes. +template +class AutomaticallyChosenSoundSystem : public SoundSystemProxy { + public: + // Chooses and initializes the underlying sound system. + virtual bool Init(); + // Terminates the underlying sound system implementation, but caches it for + // future re-use. + virtual void Terminate(); + + virtual const char *GetName() const; + + private: + talk_base::scoped_ptr sound_systems_[kNumSoundSystems]; +}; + +template +bool AutomaticallyChosenSoundSystem::Init() { + if (wrapped_) { + return true; + } + for (int i = 0; i < kNumSoundSystems; ++i) { + if (!sound_systems_[i].get()) { + sound_systems_[i].reset((*kSoundSystemCreators[i])()); + } + if (sound_systems_[i]->Init()) { + // This is the first sound system in the list to successfully + // initialize, so we're done. + wrapped_ = sound_systems_[i].get(); + break; + } + // Else it failed to initialize, so try the remaining ones. + } + if (!wrapped_) { + LOG(LS_ERROR) << "Failed to find a usable sound system"; + return false; + } + LOG(LS_INFO) << "Selected " << wrapped_->GetName() << " sound system"; + return true; +} + +template +void AutomaticallyChosenSoundSystem::Terminate() { + if (!wrapped_) { + return; + } + wrapped_->Terminate(); + wrapped_ = NULL; + // We do not free the scoped_ptrs because we may be re-init'ed soon. +} + +template +const char *AutomaticallyChosenSoundSystem::GetName() const { + return wrapped_ ? wrapped_->GetName() : "automatic"; +} + +} // namespace cricket + +#endif // TALK_SOUND_AUTOMATICALLYCHOSENSOUNDSYSTEM_H_ diff --git a/talk/sound/automaticallychosensoundsystem_unittest.cc b/talk/sound/automaticallychosensoundsystem_unittest.cc new file mode 100644 index 000000000..a8afeecb4 --- /dev/null +++ b/talk/sound/automaticallychosensoundsystem_unittest.cc @@ -0,0 +1,214 @@ +/* + * libjingle + * Copyright 2004--2010, 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/base/gunit.h" +#include "talk/sound/automaticallychosensoundsystem.h" +#include "talk/sound/nullsoundsystem.h" + +namespace cricket { + +class NeverFailsToFailSoundSystem : public NullSoundSystem { + public: + // Overrides superclass. + virtual bool Init() { + return false; + } + + static SoundSystemInterface *Create() { + return new NeverFailsToFailSoundSystem(); + } +}; + +class InitCheckingSoundSystem1 : public NullSoundSystem { + public: + // Overrides superclass. + virtual bool Init() { + created_ = true; + return true; + } + + static SoundSystemInterface *Create() { + return new InitCheckingSoundSystem1(); + } + + static bool created_; +}; + +bool InitCheckingSoundSystem1::created_ = false; + +class InitCheckingSoundSystem2 : public NullSoundSystem { + public: + // Overrides superclass. + virtual bool Init() { + created_ = true; + return true; + } + + static SoundSystemInterface *Create() { + return new InitCheckingSoundSystem2(); + } + + static bool created_; +}; + +bool InitCheckingSoundSystem2::created_ = false; + +class DeletionCheckingSoundSystem1 : public NeverFailsToFailSoundSystem { + public: + virtual ~DeletionCheckingSoundSystem1() { + deleted_ = true; + } + + static SoundSystemInterface *Create() { + return new DeletionCheckingSoundSystem1(); + } + + static bool deleted_; +}; + +bool DeletionCheckingSoundSystem1::deleted_ = false; + +class DeletionCheckingSoundSystem2 : public NeverFailsToFailSoundSystem { + public: + virtual ~DeletionCheckingSoundSystem2() { + deleted_ = true; + } + + static SoundSystemInterface *Create() { + return new DeletionCheckingSoundSystem2(); + } + + static bool deleted_; +}; + +bool DeletionCheckingSoundSystem2::deleted_ = false; + +class DeletionCheckingSoundSystem3 : public NullSoundSystem { + public: + virtual ~DeletionCheckingSoundSystem3() { + deleted_ = true; + } + + static SoundSystemInterface *Create() { + return new DeletionCheckingSoundSystem3(); + } + + static bool deleted_; +}; + +bool DeletionCheckingSoundSystem3::deleted_ = false; + +extern const SoundSystemCreator kSingleSystemFailingCreators[] = { + &NeverFailsToFailSoundSystem::Create, +}; + +TEST(AutomaticallyChosenSoundSystem, SingleSystemFailing) { + AutomaticallyChosenSoundSystem< + kSingleSystemFailingCreators, + ARRAY_SIZE(kSingleSystemFailingCreators)> sound_system; + EXPECT_FALSE(sound_system.Init()); +} + +extern const SoundSystemCreator kSingleSystemSucceedingCreators[] = { + &NullSoundSystem::Create, +}; + +TEST(AutomaticallyChosenSoundSystem, SingleSystemSucceeding) { + AutomaticallyChosenSoundSystem< + kSingleSystemSucceedingCreators, + ARRAY_SIZE(kSingleSystemSucceedingCreators)> sound_system; + EXPECT_TRUE(sound_system.Init()); +} + +extern const SoundSystemCreator + kFailedFirstSystemResultsInUsingSecondCreators[] = { + &NeverFailsToFailSoundSystem::Create, + &NullSoundSystem::Create, +}; + +TEST(AutomaticallyChosenSoundSystem, FailedFirstSystemResultsInUsingSecond) { + AutomaticallyChosenSoundSystem< + kFailedFirstSystemResultsInUsingSecondCreators, + ARRAY_SIZE(kFailedFirstSystemResultsInUsingSecondCreators)> sound_system; + EXPECT_TRUE(sound_system.Init()); +} + +extern const SoundSystemCreator kEarlierEntriesHavePriorityCreators[] = { + &InitCheckingSoundSystem1::Create, + &InitCheckingSoundSystem2::Create, +}; + +TEST(AutomaticallyChosenSoundSystem, EarlierEntriesHavePriority) { + AutomaticallyChosenSoundSystem< + kEarlierEntriesHavePriorityCreators, + ARRAY_SIZE(kEarlierEntriesHavePriorityCreators)> sound_system; + InitCheckingSoundSystem1::created_ = false; + InitCheckingSoundSystem2::created_ = false; + EXPECT_TRUE(sound_system.Init()); + EXPECT_TRUE(InitCheckingSoundSystem1::created_); + EXPECT_FALSE(InitCheckingSoundSystem2::created_); +} + +extern const SoundSystemCreator kManySoundSystemsCreators[] = { + &NullSoundSystem::Create, + &NullSoundSystem::Create, + &NullSoundSystem::Create, + &NullSoundSystem::Create, + &NullSoundSystem::Create, + &NullSoundSystem::Create, + &NullSoundSystem::Create, +}; + +TEST(AutomaticallyChosenSoundSystem, ManySoundSystems) { + AutomaticallyChosenSoundSystem< + kManySoundSystemsCreators, + ARRAY_SIZE(kManySoundSystemsCreators)> sound_system; + EXPECT_TRUE(sound_system.Init()); +} + +extern const SoundSystemCreator kDeletesAllCreatedSoundSystemsCreators[] = { + &DeletionCheckingSoundSystem1::Create, + &DeletionCheckingSoundSystem2::Create, + &DeletionCheckingSoundSystem3::Create, +}; + +TEST(AutomaticallyChosenSoundSystem, DeletesAllCreatedSoundSystems) { + typedef AutomaticallyChosenSoundSystem< + kDeletesAllCreatedSoundSystemsCreators, + ARRAY_SIZE(kDeletesAllCreatedSoundSystemsCreators)> TestSoundSystem; + TestSoundSystem *sound_system = new TestSoundSystem(); + DeletionCheckingSoundSystem1::deleted_ = false; + DeletionCheckingSoundSystem2::deleted_ = false; + DeletionCheckingSoundSystem3::deleted_ = false; + EXPECT_TRUE(sound_system->Init()); + delete sound_system; + EXPECT_TRUE(DeletionCheckingSoundSystem1::deleted_); + EXPECT_TRUE(DeletionCheckingSoundSystem2::deleted_); + EXPECT_TRUE(DeletionCheckingSoundSystem3::deleted_); +} + +} // namespace cricket diff --git a/talk/sound/linuxsoundsystem.cc b/talk/sound/linuxsoundsystem.cc new file mode 100644 index 000000000..7980a1546 --- /dev/null +++ b/talk/sound/linuxsoundsystem.cc @@ -0,0 +1,42 @@ +/* + * libjingle + * Copyright 2004--2010, 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/sound/linuxsoundsystem.h" + +#include "talk/sound/alsasoundsystem.h" +#include "talk/sound/pulseaudiosoundsystem.h" + +namespace cricket { + +const SoundSystemCreator kLinuxSoundSystemCreators[] = { +#ifdef HAVE_LIBPULSE + &PulseAudioSoundSystem::Create, +#endif + &AlsaSoundSystem::Create, +}; + +} // namespace cricket diff --git a/talk/sound/linuxsoundsystem.h b/talk/sound/linuxsoundsystem.h new file mode 100644 index 000000000..eb48b8837 --- /dev/null +++ b/talk/sound/linuxsoundsystem.h @@ -0,0 +1,58 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_LINUXSOUNDSYSTEM_H_ +#define TALK_SOUND_LINUXSOUNDSYSTEM_H_ + +#include "talk/sound/automaticallychosensoundsystem.h" + +namespace cricket { + +extern const SoundSystemCreator kLinuxSoundSystemCreators[ +#ifdef HAVE_LIBPULSE + 2 +#else + 1 +#endif + ]; + +// The vast majority of Linux systems use ALSA for the device-level sound API, +// but an increasing number are using PulseAudio for the application API and +// only using ALSA internally in PulseAudio itself. But like everything on +// Linux this is user-configurable, so we need to support both and choose the +// right one at run-time. +// PulseAudioSoundSystem is designed to only successfully initialize if +// PulseAudio is installed and running, and if it is running then direct device +// access using ALSA typically won't work, so if PulseAudioSoundSystem +// initializes then we choose that. Otherwise we choose ALSA. +typedef AutomaticallyChosenSoundSystem< + kLinuxSoundSystemCreators, + ARRAY_SIZE(kLinuxSoundSystemCreators)> LinuxSoundSystem; + +} // namespace cricket + +#endif // TALK_SOUND_LINUXSOUNDSYSTEM_H_ diff --git a/talk/sound/nullsoundsystem.cc b/talk/sound/nullsoundsystem.cc new file mode 100644 index 000000000..29200086e --- /dev/null +++ b/talk/sound/nullsoundsystem.cc @@ -0,0 +1,174 @@ +/* + * libjingle + * Copyright 2004--2010, 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/sound/nullsoundsystem.h" + +#include "talk/base/logging.h" +#include "talk/sound/sounddevicelocator.h" +#include "talk/sound/soundinputstreaminterface.h" +#include "talk/sound/soundoutputstreaminterface.h" + +namespace talk_base { + +class Thread; + +} + +namespace cricket { + +// Name used for the single device and the sound system itself. +static const char kNullName[] = "null"; + +class NullSoundDeviceLocator : public SoundDeviceLocator { + public: + NullSoundDeviceLocator() : SoundDeviceLocator(kNullName, kNullName) {} + + virtual SoundDeviceLocator *Copy() const { + return new NullSoundDeviceLocator(); + } +}; + +class NullSoundInputStream : public SoundInputStreamInterface { + public: + virtual bool StartReading() { + return true; + } + + virtual bool StopReading() { + return true; + } + + virtual bool GetVolume(int *volume) { + *volume = SoundSystemInterface::kMinVolume; + return true; + } + + virtual bool SetVolume(int volume) { + return false; + } + + virtual bool Close() { + return true; + } + + virtual int LatencyUsecs() { + return 0; + } +}; + +class NullSoundOutputStream : public SoundOutputStreamInterface { + public: + virtual bool EnableBufferMonitoring() { + return true; + } + + virtual bool DisableBufferMonitoring() { + return true; + } + + virtual bool WriteSamples(const void *sample_data, + size_t size) { + LOG(LS_VERBOSE) << "Got " << size << " bytes of playback samples"; + return true; + } + + virtual bool GetVolume(int *volume) { + *volume = SoundSystemInterface::kMinVolume; + return true; + } + + virtual bool SetVolume(int volume) { + return false; + } + + virtual bool Close() { + return true; + } + + virtual int LatencyUsecs() { + return 0; + } +}; + +NullSoundSystem::~NullSoundSystem() { +} + +bool NullSoundSystem::Init() { + return true; +} + +void NullSoundSystem::Terminate() { + // Nothing to do. +} + +bool NullSoundSystem::EnumeratePlaybackDevices( + SoundSystemInterface::SoundDeviceLocatorList *devices) { + ClearSoundDeviceLocatorList(devices); + SoundDeviceLocator *device; + GetDefaultPlaybackDevice(&device); + devices->push_back(device); + return true; +} + +bool NullSoundSystem::EnumerateCaptureDevices( + SoundSystemInterface::SoundDeviceLocatorList *devices) { + ClearSoundDeviceLocatorList(devices); + SoundDeviceLocator *device; + GetDefaultCaptureDevice(&device); + devices->push_back(device); + return true; +} + +bool NullSoundSystem::GetDefaultPlaybackDevice( + SoundDeviceLocator **device) { + *device = new NullSoundDeviceLocator(); + return true; +} + +bool NullSoundSystem::GetDefaultCaptureDevice( + SoundDeviceLocator **device) { + *device = new NullSoundDeviceLocator(); + return true; +} + +SoundOutputStreamInterface *NullSoundSystem::OpenPlaybackDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms) { + return new NullSoundOutputStream(); +} + +SoundInputStreamInterface *NullSoundSystem::OpenCaptureDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms) { + return new NullSoundInputStream(); +} + +const char *NullSoundSystem::GetName() const { + return kNullName; +} + +} // namespace cricket diff --git a/talk/sound/nullsoundsystem.h b/talk/sound/nullsoundsystem.h new file mode 100644 index 000000000..3edb4f92f --- /dev/null +++ b/talk/sound/nullsoundsystem.h @@ -0,0 +1,70 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_NULLSOUNDSYSTEM_H_ +#define TALK_SOUND_NULLSOUNDSYSTEM_H_ + +#include "talk/sound/soundsysteminterface.h" + +namespace cricket { + +class SoundDeviceLocator; +class SoundInputStreamInterface; +class SoundOutputStreamInterface; + +// A simple reference sound system that drops output samples and generates +// no input samples. +class NullSoundSystem : public SoundSystemInterface { + public: + static SoundSystemInterface *Create() { + return new NullSoundSystem(); + } + + virtual ~NullSoundSystem(); + + virtual bool Init(); + virtual void Terminate(); + + virtual bool EnumeratePlaybackDevices(SoundDeviceLocatorList *devices); + virtual bool EnumerateCaptureDevices(SoundDeviceLocatorList *devices); + + virtual SoundOutputStreamInterface *OpenPlaybackDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms); + virtual SoundInputStreamInterface *OpenCaptureDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms); + + virtual bool GetDefaultPlaybackDevice(SoundDeviceLocator **device); + virtual bool GetDefaultCaptureDevice(SoundDeviceLocator **device); + + virtual const char *GetName() const; +}; + +} // namespace cricket + +#endif // TALK_SOUND_NULLSOUNDSYSTEM_H_ diff --git a/talk/sound/nullsoundsystemfactory.cc b/talk/sound/nullsoundsystemfactory.cc new file mode 100644 index 000000000..089d51f53 --- /dev/null +++ b/talk/sound/nullsoundsystemfactory.cc @@ -0,0 +1,49 @@ +/* + * libjingle + * Copyright 2004--2010, 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/sound/nullsoundsystemfactory.h" + +#include "talk/sound/nullsoundsystem.h" + +namespace cricket { + +NullSoundSystemFactory::NullSoundSystemFactory() { +} + +NullSoundSystemFactory::~NullSoundSystemFactory() { +} + +bool NullSoundSystemFactory::SetupInstance() { + instance_.reset(new NullSoundSystem()); + return true; +} + +void NullSoundSystemFactory::CleanupInstance() { + instance_.reset(); +} + +} // namespace cricket diff --git a/talk/sound/nullsoundsystemfactory.h b/talk/sound/nullsoundsystemfactory.h new file mode 100644 index 000000000..71ae98019 --- /dev/null +++ b/talk/sound/nullsoundsystemfactory.h @@ -0,0 +1,50 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_NULLSOUNDSYSTEMFACTORY_H_ +#define TALK_SOUND_NULLSOUNDSYSTEMFACTORY_H_ + +#include "talk/sound/soundsystemfactory.h" + +namespace cricket { + +// A SoundSystemFactory that always returns a NullSoundSystem. Intended for +// testing. +class NullSoundSystemFactory : public SoundSystemFactory { + public: + NullSoundSystemFactory(); + virtual ~NullSoundSystemFactory(); + + protected: + // Inherited from SoundSystemFactory. + virtual bool SetupInstance(); + virtual void CleanupInstance(); +}; + +} // namespace cricket + +#endif // TALK_SOUND_NULLSOUNDSYSTEMFACTORY_H_ diff --git a/talk/sound/platformsoundsystem.cc b/talk/sound/platformsoundsystem.cc new file mode 100644 index 000000000..9dff9ae6e --- /dev/null +++ b/talk/sound/platformsoundsystem.cc @@ -0,0 +1,48 @@ +/* + * libjingle + * Copyright 2004--2010, 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/sound/platformsoundsystem.h" + +#include "talk/base/common.h" +#ifdef LINUX +#include "talk/sound/linuxsoundsystem.h" +#else +#include "talk/sound/nullsoundsystem.h" +#endif + +namespace cricket { + +SoundSystemInterface *CreatePlatformSoundSystem() { +#ifdef LINUX + return new LinuxSoundSystem(); +#else + ASSERT(false && "Not implemented"); + return new NullSoundSystem(); +#endif +} + +} // namespace cricket diff --git a/talk/sound/platformsoundsystem.h b/talk/sound/platformsoundsystem.h new file mode 100644 index 000000000..1a8d2142a --- /dev/null +++ b/talk/sound/platformsoundsystem.h @@ -0,0 +1,40 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_PLATFORMSOUNDSYSTEM_H_ +#define TALK_SOUND_PLATFORMSOUNDSYSTEM_H_ + +namespace cricket { + +class SoundSystemInterface; + +// Creates the sound system implementation for this platform. +SoundSystemInterface *CreatePlatformSoundSystem(); + +} // namespace cricket + +#endif // TALK_SOUND_PLATFORMSOUNDSYSTEM_H_ diff --git a/talk/sound/platformsoundsystemfactory.cc b/talk/sound/platformsoundsystemfactory.cc new file mode 100644 index 000000000..6c69954e6 --- /dev/null +++ b/talk/sound/platformsoundsystemfactory.cc @@ -0,0 +1,57 @@ +/* + * libjingle + * Copyright 2004--2010, 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/sound/platformsoundsystemfactory.h" + +#include "talk/sound/platformsoundsystem.h" +#include "talk/sound/soundsysteminterface.h" + +namespace cricket { + +PlatformSoundSystemFactory::PlatformSoundSystemFactory() { +} + +PlatformSoundSystemFactory::~PlatformSoundSystemFactory() { +} + +bool PlatformSoundSystemFactory::SetupInstance() { + if (!instance_.get()) { + instance_.reset(CreatePlatformSoundSystem()); + } + if (!instance_->Init()) { + LOG(LS_ERROR) << "Can't initialize platform's sound system"; + return false; + } + return true; +} + +void PlatformSoundSystemFactory::CleanupInstance() { + instance_->Terminate(); + // We do not delete the sound system because we might be re-initialized soon. +} + +} // namespace cricket diff --git a/talk/sound/platformsoundsystemfactory.h b/talk/sound/platformsoundsystemfactory.h new file mode 100644 index 000000000..63ca863f9 --- /dev/null +++ b/talk/sound/platformsoundsystemfactory.h @@ -0,0 +1,52 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_PLATFORMSOUNDSYSTEMFACTORY_H_ +#define TALK_SOUND_PLATFORMSOUNDSYSTEMFACTORY_H_ + +#include "talk/sound/soundsystemfactory.h" + +namespace cricket { + +// A SoundSystemFactory that returns the platform's native sound system +// implementation. +class PlatformSoundSystemFactory : public SoundSystemFactory { + public: + PlatformSoundSystemFactory(); + virtual ~PlatformSoundSystemFactory(); + + protected: + // Inherited from SoundSystemFactory. + virtual bool SetupInstance(); + virtual void CleanupInstance(); +}; + +} // namespace cricket + +#endif // TALK_SOUND_PLATFORMSOUNDSYSTEMFACTORY_H_ + + diff --git a/talk/sound/pulseaudiosoundsystem.cc b/talk/sound/pulseaudiosoundsystem.cc new file mode 100644 index 000000000..7eb690aed --- /dev/null +++ b/talk/sound/pulseaudiosoundsystem.cc @@ -0,0 +1,1559 @@ +/* + * libjingle + * Copyright 2010, 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/sound/pulseaudiosoundsystem.h" + +#ifdef HAVE_LIBPULSE + +#include "talk/base/common.h" +#include "talk/base/fileutils.h" // for GetApplicationName() +#include "talk/base/logging.h" +#include "talk/base/worker.h" +#include "talk/base/timeutils.h" +#include "talk/sound/sounddevicelocator.h" +#include "talk/sound/soundinputstreaminterface.h" +#include "talk/sound/soundoutputstreaminterface.h" + +namespace cricket { + +// First PulseAudio protocol version that supports PA_STREAM_ADJUST_LATENCY. +static const uint32_t kAdjustLatencyProtocolVersion = 13; + +// Lookup table from the cricket format enum in soundsysteminterface.h to +// Pulse's enums. +static const pa_sample_format_t kCricketFormatToPulseFormatTable[] = { + // The order here must match the order in soundsysteminterface.h + PA_SAMPLE_S16LE, +}; + +// Some timing constants for optimal operation. See +// https://tango.0pointer.de/pipermail/pulseaudio-discuss/2008-January/001170.html +// for a good explanation of some of the factors that go into this. + +// Playback. + +// For playback, there is a round-trip delay to fill the server-side playback +// buffer, so setting too low of a latency is a buffer underflow risk. We will +// automatically increase the latency if a buffer underflow does occur, but we +// also enforce a sane minimum at start-up time. Anything lower would be +// virtually guaranteed to underflow at least once, so there's no point in +// allowing lower latencies. +static const int kPlaybackLatencyMinimumMsecs = 20; +// Every time a playback stream underflows, we will reconfigure it with target +// latency that is greater by this amount. +static const int kPlaybackLatencyIncrementMsecs = 20; +// We also need to configure a suitable request size. Too small and we'd burn +// CPU from the overhead of transfering small amounts of data at once. Too large +// and the amount of data remaining in the buffer right before refilling it +// would be a buffer underflow risk. We set it to half of the buffer size. +static const int kPlaybackRequestFactor = 2; + +// Capture. + +// For capture, low latency is not a buffer overflow risk, but it makes us burn +// CPU from the overhead of transfering small amounts of data at once, so we set +// a recommended value that we use for the kLowLatency constant (but if the user +// explicitly requests something lower then we will honour it). +// 1ms takes about 6-7% CPU. 5ms takes about 5%. 10ms takes about 4.x%. +static const int kLowCaptureLatencyMsecs = 10; +// There is a round-trip delay to ack the data to the server, so the +// server-side buffer needs extra space to prevent buffer overflow. 20ms is +// sufficient, but there is no penalty to making it bigger, so we make it huge. +// (750ms is libpulse's default value for the _total_ buffer size in the +// kNoLatencyRequirements case.) +static const int kCaptureBufferExtraMsecs = 750; + +static void FillPlaybackBufferAttr(int latency, + pa_buffer_attr *attr) { + attr->maxlength = latency; + attr->tlength = latency; + attr->minreq = latency / kPlaybackRequestFactor; + attr->prebuf = attr->tlength - attr->minreq; + LOG(LS_VERBOSE) << "Configuring latency = " << attr->tlength << ", minreq = " + << attr->minreq << ", minfill = " << attr->prebuf; +} + +static pa_volume_t CricketVolumeToPulseVolume(int volume) { + // PA's volume space goes from 0% at PA_VOLUME_MUTED (value 0) to 100% at + // PA_VOLUME_NORM (value 0x10000). It can also go beyond 100% up to + // PA_VOLUME_MAX (value UINT32_MAX-1), but using that is probably unwise. + // We just linearly map the 0-255 scale of SoundSystemInterface onto + // PA_VOLUME_MUTED-PA_VOLUME_NORM. If the programmer exceeds kMaxVolume then + // they can access the over-100% features of PA. + return PA_VOLUME_MUTED + (PA_VOLUME_NORM - PA_VOLUME_MUTED) * + volume / SoundSystemInterface::kMaxVolume; +} + +static int PulseVolumeToCricketVolume(pa_volume_t pa_volume) { + return SoundSystemInterface::kMinVolume + + (SoundSystemInterface::kMaxVolume - SoundSystemInterface::kMinVolume) * + pa_volume / PA_VOLUME_NORM; +} + +static pa_volume_t MaxChannelVolume(pa_cvolume *channel_volumes) { + pa_volume_t pa_volume = PA_VOLUME_MUTED; // Minimum possible value. + for (int i = 0; i < channel_volumes->channels; ++i) { + if (pa_volume < channel_volumes->values[i]) { + pa_volume = channel_volumes->values[i]; + } + } + return pa_volume; +} + +class PulseAudioDeviceLocator : public SoundDeviceLocator { + public: + PulseAudioDeviceLocator(const std::string &name, + const std::string &device_name) + : SoundDeviceLocator(name, device_name) { + } + + virtual SoundDeviceLocator *Copy() const { + return new PulseAudioDeviceLocator(*this); + } +}; + +// Functionality that is common to both PulseAudioInputStream and +// PulseAudioOutputStream. +class PulseAudioStream { + public: + PulseAudioStream(PulseAudioSoundSystem *pulse, pa_stream *stream, int flags) + : pulse_(pulse), stream_(stream), flags_(flags) { + } + + ~PulseAudioStream() { + // Close() should have been called during the containing class's destructor. + ASSERT(stream_ == NULL); + } + + // Must be called with the lock held. + bool Close() { + if (!IsClosed()) { + // Unset this here so that we don't get a TERMINATED callback. + symbol_table()->pa_stream_set_state_callback()(stream_, NULL, NULL); + if (symbol_table()->pa_stream_disconnect()(stream_) != 0) { + LOG(LS_ERROR) << "Can't disconnect stream"; + // Continue and return true anyways. + } + symbol_table()->pa_stream_unref()(stream_); + stream_ = NULL; + } + return true; + } + + // Must be called with the lock held. + int LatencyUsecs() { + if (!(flags_ & SoundSystemInterface::FLAG_REPORT_LATENCY)) { + return 0; + } + + pa_usec_t latency; + int negative; + Lock(); + int re = symbol_table()->pa_stream_get_latency()(stream_, &latency, + &negative); + Unlock(); + if (re != 0) { + LOG(LS_ERROR) << "Can't query latency"; + // We'd rather continue playout/capture with an incorrect delay than stop + // it altogether, so return a valid value. + return 0; + } + if (negative) { + // The delay can be negative for monitoring streams if the captured + // samples haven't been played yet. In such a case, "latency" contains the + // magnitude, so we must negate it to get the real value. + return -latency; + } else { + return latency; + } + } + + PulseAudioSoundSystem *pulse() { + return pulse_; + } + + PulseAudioSymbolTable *symbol_table() { + return &pulse()->symbol_table_; + } + + pa_stream *stream() { + ASSERT(stream_ != NULL); + return stream_; + } + + bool IsClosed() { + return stream_ == NULL; + } + + void Lock() { + pulse()->Lock(); + } + + void Unlock() { + pulse()->Unlock(); + } + + private: + PulseAudioSoundSystem *pulse_; + pa_stream *stream_; + int flags_; + + DISALLOW_COPY_AND_ASSIGN(PulseAudioStream); +}; + +// Implementation of an input stream. See soundinputstreaminterface.h regarding +// thread-safety. +class PulseAudioInputStream : + public SoundInputStreamInterface, + private talk_base::Worker { + + struct GetVolumeCallbackData { + PulseAudioInputStream *instance; + pa_cvolume *channel_volumes; + }; + + struct GetSourceChannelCountCallbackData { + PulseAudioInputStream *instance; + uint8_t *channels; + }; + + public: + PulseAudioInputStream(PulseAudioSoundSystem *pulse, + pa_stream *stream, + int flags) + : stream_(pulse, stream, flags), + temp_sample_data_(NULL), + temp_sample_data_size_(0) { + // This callback seems to never be issued, but let's set it anyways. + symbol_table()->pa_stream_set_overflow_callback()(stream, &OverflowCallback, + NULL); + } + + virtual ~PulseAudioInputStream() { + bool success = Close(); + // We need that to live. + VERIFY(success); + } + + virtual bool StartReading() { + return StartWork(); + } + + virtual bool StopReading() { + return StopWork(); + } + + virtual bool GetVolume(int *volume) { + bool ret = false; + + Lock(); + + // Unlike output streams, input streams have no concept of a stream volume, + // only a device volume. So we have to retrieve the volume of the device + // itself. + + pa_cvolume channel_volumes; + + GetVolumeCallbackData data; + data.instance = this; + data.channel_volumes = &channel_volumes; + + pa_operation *op = symbol_table()->pa_context_get_source_info_by_index()( + stream_.pulse()->context_, + symbol_table()->pa_stream_get_device_index()(stream_.stream()), + &GetVolumeCallbackThunk, + &data); + if (!stream_.pulse()->FinishOperation(op)) { + goto done; + } + + if (data.channel_volumes) { + // This pointer was never unset by the callback, so we must have received + // an empty list of infos. This probably never happens, but we code for it + // anyway. + LOG(LS_ERROR) << "Did not receive GetVolumeCallback"; + goto done; + } + + // We now have the volume for each channel. Each channel could have a + // different volume if, e.g., the user went and changed the volumes in the + // PA UI. To get a single volume for SoundSystemInterface we just take the + // maximum. Ideally we'd do so with pa_cvolume_max, but it doesn't exist in + // Hardy, so we do it manually. + pa_volume_t pa_volume; + pa_volume = MaxChannelVolume(&channel_volumes); + // Now map onto the SoundSystemInterface range. + *volume = PulseVolumeToCricketVolume(pa_volume); + + ret = true; + done: + Unlock(); + return ret; + } + + virtual bool SetVolume(int volume) { + bool ret = false; + pa_volume_t pa_volume = CricketVolumeToPulseVolume(volume); + + Lock(); + + // Unlike output streams, input streams have no concept of a stream volume, + // only a device volume. So we have to change the volume of the device + // itself. + + // The device may have a different number of channels than the stream and + // their mapping may be different, so we don't want to use the channel count + // from our sample spec. We could use PA_CHANNELS_MAX to cover our bases, + // and the server allows that even if the device's channel count is lower, + // but some buggy PA clients don't like that (the pavucontrol on Hardy dies + // in an assert if the channel count is different). So instead we look up + // the actual number of channels that the device has. + + uint8_t channels; + + GetSourceChannelCountCallbackData data; + data.instance = this; + data.channels = &channels; + + uint32_t device_index = symbol_table()->pa_stream_get_device_index()( + stream_.stream()); + + pa_operation *op = symbol_table()->pa_context_get_source_info_by_index()( + stream_.pulse()->context_, + device_index, + &GetSourceChannelCountCallbackThunk, + &data); + if (!stream_.pulse()->FinishOperation(op)) { + goto done; + } + + if (data.channels) { + // This pointer was never unset by the callback, so we must have received + // an empty list of infos. This probably never happens, but we code for it + // anyway. + LOG(LS_ERROR) << "Did not receive GetSourceChannelCountCallback"; + goto done; + } + + pa_cvolume channel_volumes; + symbol_table()->pa_cvolume_set()(&channel_volumes, channels, pa_volume); + + op = symbol_table()->pa_context_set_source_volume_by_index()( + stream_.pulse()->context_, + device_index, + &channel_volumes, + // This callback merely logs errors. + &SetVolumeCallback, + NULL); + if (!op) { + LOG(LS_ERROR) << "pa_context_set_source_volume_by_index()"; + goto done; + } + // Don't need to wait for this to complete. + symbol_table()->pa_operation_unref()(op); + + ret = true; + done: + Unlock(); + return ret; + } + + virtual bool Close() { + if (!StopReading()) { + return false; + } + bool ret = true; + if (!stream_.IsClosed()) { + Lock(); + ret = stream_.Close(); + Unlock(); + } + return ret; + } + + virtual int LatencyUsecs() { + return stream_.LatencyUsecs(); + } + + private: + void Lock() { + stream_.Lock(); + } + + void Unlock() { + stream_.Unlock(); + } + + PulseAudioSymbolTable *symbol_table() { + return stream_.symbol_table(); + } + + void EnableReadCallback() { + symbol_table()->pa_stream_set_read_callback()( + stream_.stream(), + &ReadCallbackThunk, + this); + } + + void DisableReadCallback() { + symbol_table()->pa_stream_set_read_callback()( + stream_.stream(), + NULL, + NULL); + } + + static void ReadCallbackThunk(pa_stream *unused1, + size_t unused2, + void *userdata) { + PulseAudioInputStream *instance = + static_cast(userdata); + instance->OnReadCallback(); + } + + void OnReadCallback() { + // We get the data pointer and size now in order to save one Lock/Unlock + // on OnMessage. + if (symbol_table()->pa_stream_peek()(stream_.stream(), + &temp_sample_data_, + &temp_sample_data_size_) != 0) { + LOG(LS_ERROR) << "Can't read data!"; + return; + } + // Since we consume the data asynchronously on a different thread, we have + // to temporarily disable the read callback or else Pulse will call it + // continuously until we consume the data. We re-enable it below. + DisableReadCallback(); + HaveWork(); + } + + // Inherited from Worker. + virtual void OnStart() { + Lock(); + EnableReadCallback(); + Unlock(); + } + + // Inherited from Worker. + virtual void OnHaveWork() { + ASSERT(temp_sample_data_ && temp_sample_data_size_); + SignalSamplesRead(temp_sample_data_, + temp_sample_data_size_, + this); + temp_sample_data_ = NULL; + temp_sample_data_size_ = 0; + + Lock(); + for (;;) { + // Ack the last thing we read. + if (symbol_table()->pa_stream_drop()(stream_.stream()) != 0) { + LOG(LS_ERROR) << "Can't ack read data"; + } + + if (symbol_table()->pa_stream_readable_size()(stream_.stream()) <= 0) { + // Then that was all the data. + break; + } + + // Else more data. + const void *sample_data; + size_t sample_data_size; + if (symbol_table()->pa_stream_peek()(stream_.stream(), + &sample_data, + &sample_data_size) != 0) { + LOG(LS_ERROR) << "Can't read data!"; + break; + } + + // Drop lock for sigslot dispatch, which could take a while. + Unlock(); + SignalSamplesRead(sample_data, sample_data_size, this); + Lock(); + + // Return to top of loop for the ack and the check for more data. + } + EnableReadCallback(); + Unlock(); + } + + // Inherited from Worker. + virtual void OnStop() { + Lock(); + DisableReadCallback(); + Unlock(); + } + + static void OverflowCallback(pa_stream *stream, + void *userdata) { + LOG(LS_WARNING) << "Buffer overflow on capture stream " << stream; + } + + static void GetVolumeCallbackThunk(pa_context *unused, + const pa_source_info *info, + int eol, + void *userdata) { + GetVolumeCallbackData *data = + static_cast(userdata); + data->instance->OnGetVolumeCallback(info, eol, &data->channel_volumes); + } + + void OnGetVolumeCallback(const pa_source_info *info, + int eol, + pa_cvolume **channel_volumes) { + if (eol) { + // List is over. Wake GetVolume(). + stream_.pulse()->Signal(); + return; + } + + if (*channel_volumes) { + **channel_volumes = info->volume; + // Unset the pointer so that we know that we have have already copied the + // volume. + *channel_volumes = NULL; + } else { + // We have received an additional callback after the first one, which + // doesn't make sense for a single source. This probably never happens, + // but we code for it anyway. + LOG(LS_WARNING) << "Ignoring extra GetVolumeCallback"; + } + } + + static void GetSourceChannelCountCallbackThunk(pa_context *unused, + const pa_source_info *info, + int eol, + void *userdata) { + GetSourceChannelCountCallbackData *data = + static_cast(userdata); + data->instance->OnGetSourceChannelCountCallback(info, eol, &data->channels); + } + + void OnGetSourceChannelCountCallback(const pa_source_info *info, + int eol, + uint8_t **channels) { + if (eol) { + // List is over. Wake SetVolume(). + stream_.pulse()->Signal(); + return; + } + + if (*channels) { + **channels = info->channel_map.channels; + // Unset the pointer so that we know that we have have already copied the + // channel count. + *channels = NULL; + } else { + // We have received an additional callback after the first one, which + // doesn't make sense for a single source. This probably never happens, + // but we code for it anyway. + LOG(LS_WARNING) << "Ignoring extra GetSourceChannelCountCallback"; + } + } + + static void SetVolumeCallback(pa_context *unused1, + int success, + void *unused2) { + if (!success) { + LOG(LS_ERROR) << "Failed to change capture volume"; + } + } + + PulseAudioStream stream_; + // Temporary storage for passing data between threads. + const void *temp_sample_data_; + size_t temp_sample_data_size_; + + DISALLOW_COPY_AND_ASSIGN(PulseAudioInputStream); +}; + +// Implementation of an output stream. See soundoutputstreaminterface.h +// regarding thread-safety. +class PulseAudioOutputStream : + public SoundOutputStreamInterface, + private talk_base::Worker { + + struct GetVolumeCallbackData { + PulseAudioOutputStream *instance; + pa_cvolume *channel_volumes; + }; + + public: + PulseAudioOutputStream(PulseAudioSoundSystem *pulse, + pa_stream *stream, + int flags, + int latency) + : stream_(pulse, stream, flags), + configured_latency_(latency), + temp_buffer_space_(0) { + symbol_table()->pa_stream_set_underflow_callback()(stream, + &UnderflowCallbackThunk, + this); + } + + virtual ~PulseAudioOutputStream() { + bool success = Close(); + // We need that to live. + VERIFY(success); + } + + virtual bool EnableBufferMonitoring() { + return StartWork(); + } + + virtual bool DisableBufferMonitoring() { + return StopWork(); + } + + virtual bool WriteSamples(const void *sample_data, + size_t size) { + bool ret = true; + Lock(); + if (symbol_table()->pa_stream_write()(stream_.stream(), + sample_data, + size, + NULL, + 0, + PA_SEEK_RELATIVE) != 0) { + LOG(LS_ERROR) << "Unable to write"; + ret = false; + } + Unlock(); + return ret; + } + + virtual bool GetVolume(int *volume) { + bool ret = false; + + Lock(); + + pa_cvolume channel_volumes; + + GetVolumeCallbackData data; + data.instance = this; + data.channel_volumes = &channel_volumes; + + pa_operation *op = symbol_table()->pa_context_get_sink_input_info()( + stream_.pulse()->context_, + symbol_table()->pa_stream_get_index()(stream_.stream()), + &GetVolumeCallbackThunk, + &data); + if (!stream_.pulse()->FinishOperation(op)) { + goto done; + } + + if (data.channel_volumes) { + // This pointer was never unset by the callback, so we must have received + // an empty list of infos. This probably never happens, but we code for it + // anyway. + LOG(LS_ERROR) << "Did not receive GetVolumeCallback"; + goto done; + } + + // We now have the volume for each channel. Each channel could have a + // different volume if, e.g., the user went and changed the volumes in the + // PA UI. To get a single volume for SoundSystemInterface we just take the + // maximum. Ideally we'd do so with pa_cvolume_max, but it doesn't exist in + // Hardy, so we do it manually. + pa_volume_t pa_volume; + pa_volume = MaxChannelVolume(&channel_volumes); + // Now map onto the SoundSystemInterface range. + *volume = PulseVolumeToCricketVolume(pa_volume); + + ret = true; + done: + Unlock(); + return ret; + } + + virtual bool SetVolume(int volume) { + bool ret = false; + pa_volume_t pa_volume = CricketVolumeToPulseVolume(volume); + + Lock(); + + const pa_sample_spec *spec = symbol_table()->pa_stream_get_sample_spec()( + stream_.stream()); + if (!spec) { + LOG(LS_ERROR) << "pa_stream_get_sample_spec()"; + goto done; + } + + pa_cvolume channel_volumes; + symbol_table()->pa_cvolume_set()(&channel_volumes, spec->channels, + pa_volume); + + pa_operation *op; + op = symbol_table()->pa_context_set_sink_input_volume()( + stream_.pulse()->context_, + symbol_table()->pa_stream_get_index()(stream_.stream()), + &channel_volumes, + // This callback merely logs errors. + &SetVolumeCallback, + NULL); + if (!op) { + LOG(LS_ERROR) << "pa_context_set_sink_input_volume()"; + goto done; + } + // Don't need to wait for this to complete. + symbol_table()->pa_operation_unref()(op); + + ret = true; + done: + Unlock(); + return ret; + } + + virtual bool Close() { + if (!DisableBufferMonitoring()) { + return false; + } + bool ret = true; + if (!stream_.IsClosed()) { + Lock(); + symbol_table()->pa_stream_set_underflow_callback()(stream_.stream(), + NULL, + NULL); + ret = stream_.Close(); + Unlock(); + } + return ret; + } + + virtual int LatencyUsecs() { + return stream_.LatencyUsecs(); + } + +#if 0 + // TODO: Versions 0.9.16 and later of Pulse have a new API for + // zero-copy writes, but Hardy is not new enough to have that so we can't + // rely on it. Perhaps auto-detect if it's present or not and use it if we + // can? + + virtual bool GetWriteBuffer(void **buffer, size_t *size) { + bool ret = true; + Lock(); + if (symbol_table()->pa_stream_begin_write()(stream_.stream(), buffer, size) + != 0) { + LOG(LS_ERROR) << "Can't get write buffer"; + ret = false; + } + Unlock(); + return ret; + } + + // Releases the caller's hold on the write buffer. "written" must be the + // amount of data that was written. + virtual bool ReleaseWriteBuffer(void *buffer, size_t written) { + bool ret = true; + Lock(); + if (written == 0) { + if (symbol_table()->pa_stream_cancel_write()(stream_.stream()) != 0) { + LOG(LS_ERROR) << "Can't cancel write"; + ret = false; + } + } else { + if (symbol_table()->pa_stream_write()(stream_.stream(), + buffer, + written, + NULL, + 0, + PA_SEEK_RELATIVE) != 0) { + LOG(LS_ERROR) << "Unable to write"; + ret = false; + } + } + Unlock(); + return ret; + } +#endif + + private: + void Lock() { + stream_.Lock(); + } + + void Unlock() { + stream_.Unlock(); + } + + PulseAudioSymbolTable *symbol_table() { + return stream_.symbol_table(); + } + + void EnableWriteCallback() { + pa_stream_state_t state = symbol_table()->pa_stream_get_state()( + stream_.stream()); + if (state == PA_STREAM_READY) { + // May already have available space. Must check. + temp_buffer_space_ = symbol_table()->pa_stream_writable_size()( + stream_.stream()); + if (temp_buffer_space_ > 0) { + // Yup, there is already space available, so if we register a write + // callback then it will not receive any event. So dispatch one ourself + // instead. + HaveWork(); + return; + } + } + symbol_table()->pa_stream_set_write_callback()( + stream_.stream(), + &WriteCallbackThunk, + this); + } + + void DisableWriteCallback() { + symbol_table()->pa_stream_set_write_callback()( + stream_.stream(), + NULL, + NULL); + } + + static void WriteCallbackThunk(pa_stream *unused, + size_t buffer_space, + void *userdata) { + PulseAudioOutputStream *instance = + static_cast(userdata); + instance->OnWriteCallback(buffer_space); + } + + void OnWriteCallback(size_t buffer_space) { + temp_buffer_space_ = buffer_space; + // Since we write the data asynchronously on a different thread, we have + // to temporarily disable the write callback or else Pulse will call it + // continuously until we write the data. We re-enable it below. + DisableWriteCallback(); + HaveWork(); + } + + // Inherited from Worker. + virtual void OnStart() { + Lock(); + EnableWriteCallback(); + Unlock(); + } + + // Inherited from Worker. + virtual void OnHaveWork() { + ASSERT(temp_buffer_space_ > 0); + + SignalBufferSpace(temp_buffer_space_, this); + + temp_buffer_space_ = 0; + Lock(); + EnableWriteCallback(); + Unlock(); + } + + // Inherited from Worker. + virtual void OnStop() { + Lock(); + DisableWriteCallback(); + Unlock(); + } + + static void UnderflowCallbackThunk(pa_stream *unused, + void *userdata) { + PulseAudioOutputStream *instance = + static_cast(userdata); + instance->OnUnderflowCallback(); + } + + void OnUnderflowCallback() { + LOG(LS_WARNING) << "Buffer underflow on playback stream " + << stream_.stream(); + + if (configured_latency_ == SoundSystemInterface::kNoLatencyRequirements) { + // We didn't configure a pa_buffer_attr before, so switching to one now + // would be questionable. + return; + } + + // Otherwise reconfigure the stream with a higher target latency. + + const pa_sample_spec *spec = symbol_table()->pa_stream_get_sample_spec()( + stream_.stream()); + if (!spec) { + LOG(LS_ERROR) << "pa_stream_get_sample_spec()"; + return; + } + + size_t bytes_per_sec = symbol_table()->pa_bytes_per_second()(spec); + + int new_latency = configured_latency_ + + bytes_per_sec * kPlaybackLatencyIncrementMsecs / + talk_base::kNumMicrosecsPerSec; + + pa_buffer_attr new_attr = {0}; + FillPlaybackBufferAttr(new_latency, &new_attr); + + pa_operation *op = symbol_table()->pa_stream_set_buffer_attr()( + stream_.stream(), + &new_attr, + // No callback. + NULL, + NULL); + if (!op) { + LOG(LS_ERROR) << "pa_stream_set_buffer_attr()"; + return; + } + // Don't need to wait for this to complete. + symbol_table()->pa_operation_unref()(op); + + // Save the new latency in case we underflow again. + configured_latency_ = new_latency; + } + + static void GetVolumeCallbackThunk(pa_context *unused, + const pa_sink_input_info *info, + int eol, + void *userdata) { + GetVolumeCallbackData *data = + static_cast(userdata); + data->instance->OnGetVolumeCallback(info, eol, &data->channel_volumes); + } + + void OnGetVolumeCallback(const pa_sink_input_info *info, + int eol, + pa_cvolume **channel_volumes) { + if (eol) { + // List is over. Wake GetVolume(). + stream_.pulse()->Signal(); + return; + } + + if (*channel_volumes) { + **channel_volumes = info->volume; + // Unset the pointer so that we know that we have have already copied the + // volume. + *channel_volumes = NULL; + } else { + // We have received an additional callback after the first one, which + // doesn't make sense for a single sink input. This probably never + // happens, but we code for it anyway. + LOG(LS_WARNING) << "Ignoring extra GetVolumeCallback"; + } + } + + static void SetVolumeCallback(pa_context *unused1, + int success, + void *unused2) { + if (!success) { + LOG(LS_ERROR) << "Failed to change playback volume"; + } + } + + PulseAudioStream stream_; + int configured_latency_; + // Temporary storage for passing data between threads. + size_t temp_buffer_space_; + + DISALLOW_COPY_AND_ASSIGN(PulseAudioOutputStream); +}; + +PulseAudioSoundSystem::PulseAudioSoundSystem() + : mainloop_(NULL), context_(NULL) { +} + +PulseAudioSoundSystem::~PulseAudioSoundSystem() { + Terminate(); +} + +bool PulseAudioSoundSystem::Init() { + if (IsInitialized()) { + return true; + } + + // Load libpulse. + if (!symbol_table_.Load()) { + // Most likely the Pulse library and sound server are not installed on + // this system. + LOG(LS_WARNING) << "Failed to load symbol table"; + return false; + } + + // Now create and start the Pulse event thread. + mainloop_ = symbol_table_.pa_threaded_mainloop_new()(); + if (!mainloop_) { + LOG(LS_ERROR) << "Can't create mainloop"; + goto fail0; + } + + if (symbol_table_.pa_threaded_mainloop_start()(mainloop_) != 0) { + LOG(LS_ERROR) << "Can't start mainloop"; + goto fail1; + } + + Lock(); + context_ = CreateNewConnection(); + Unlock(); + + if (!context_) { + goto fail2; + } + + // Otherwise we're now ready! + return true; + + fail2: + symbol_table_.pa_threaded_mainloop_stop()(mainloop_); + fail1: + symbol_table_.pa_threaded_mainloop_free()(mainloop_); + mainloop_ = NULL; + fail0: + return false; +} + +void PulseAudioSoundSystem::Terminate() { + if (!IsInitialized()) { + return; + } + + Lock(); + symbol_table_.pa_context_disconnect()(context_); + symbol_table_.pa_context_unref()(context_); + Unlock(); + context_ = NULL; + symbol_table_.pa_threaded_mainloop_stop()(mainloop_); + symbol_table_.pa_threaded_mainloop_free()(mainloop_); + mainloop_ = NULL; + + // We do not unload the symbol table because we may need it again soon if + // Init() is called again. +} + +bool PulseAudioSoundSystem::EnumeratePlaybackDevices( + SoundDeviceLocatorList *devices) { + return EnumerateDevices( + devices, + symbol_table_.pa_context_get_sink_info_list(), + &EnumeratePlaybackDevicesCallbackThunk); +} + +bool PulseAudioSoundSystem::EnumerateCaptureDevices( + SoundDeviceLocatorList *devices) { + return EnumerateDevices( + devices, + symbol_table_.pa_context_get_source_info_list(), + &EnumerateCaptureDevicesCallbackThunk); +} + +bool PulseAudioSoundSystem::GetDefaultPlaybackDevice( + SoundDeviceLocator **device) { + return GetDefaultDevice<&pa_server_info::default_sink_name>(device); +} + +bool PulseAudioSoundSystem::GetDefaultCaptureDevice( + SoundDeviceLocator **device) { + return GetDefaultDevice<&pa_server_info::default_source_name>(device); +} + +SoundOutputStreamInterface *PulseAudioSoundSystem::OpenPlaybackDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms) { + return OpenDevice( + device, + params, + "Playback", + &PulseAudioSoundSystem::ConnectOutputStream); +} + +SoundInputStreamInterface *PulseAudioSoundSystem::OpenCaptureDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms) { + return OpenDevice( + device, + params, + "Capture", + &PulseAudioSoundSystem::ConnectInputStream); +} + +const char *PulseAudioSoundSystem::GetName() const { + return "PulseAudio"; +} + +inline bool PulseAudioSoundSystem::IsInitialized() { + return mainloop_ != NULL; +} + +struct ConnectToPulseCallbackData { + PulseAudioSoundSystem *instance; + bool connect_done; +}; + +void PulseAudioSoundSystem::ConnectToPulseCallbackThunk( + pa_context *context, void *userdata) { + ConnectToPulseCallbackData *data = + static_cast(userdata); + data->instance->OnConnectToPulseCallback(context, &data->connect_done); +} + +void PulseAudioSoundSystem::OnConnectToPulseCallback( + pa_context *context, bool *connect_done) { + pa_context_state_t state = symbol_table_.pa_context_get_state()(context); + if (state == PA_CONTEXT_READY || + state == PA_CONTEXT_FAILED || + state == PA_CONTEXT_TERMINATED) { + // Connection process has reached a terminal state. Wake ConnectToPulse(). + *connect_done = true; + Signal(); + } +} + +// Must be called with the lock held. +bool PulseAudioSoundSystem::ConnectToPulse(pa_context *context) { + bool ret = true; + ConnectToPulseCallbackData data; + // Have to put this up here to satisfy the compiler. + pa_context_state_t state; + + data.instance = this; + data.connect_done = false; + + symbol_table_.pa_context_set_state_callback()(context, + &ConnectToPulseCallbackThunk, + &data); + + // Connect to PulseAudio sound server. + if (symbol_table_.pa_context_connect()( + context, + NULL, // Default server + PA_CONTEXT_NOAUTOSPAWN, + NULL) != 0) { // No special fork handling needed + LOG(LS_ERROR) << "Can't start connection to PulseAudio sound server"; + ret = false; + goto done; + } + + // Wait for the connection state machine to reach a terminal state. + do { + Wait(); + } while (!data.connect_done); + + // Now check to see what final state we reached. + state = symbol_table_.pa_context_get_state()(context); + + if (state != PA_CONTEXT_READY) { + if (state == PA_CONTEXT_FAILED) { + LOG(LS_ERROR) << "Failed to connect to PulseAudio sound server"; + } else if (state == PA_CONTEXT_TERMINATED) { + LOG(LS_ERROR) << "PulseAudio connection terminated early"; + } else { + // Shouldn't happen, because we only signal on one of those three states. + LOG(LS_ERROR) << "Unknown problem connecting to PulseAudio"; + } + ret = false; + } + + done: + // We unset our callback for safety just in case the state might somehow + // change later, because the pointer to "data" will be invalid after return + // from this function. + symbol_table_.pa_context_set_state_callback()(context, NULL, NULL); + return ret; +} + +// Must be called with the lock held. +pa_context *PulseAudioSoundSystem::CreateNewConnection() { + // Create connection context. + std::string app_name; + // TODO: Pulse etiquette says this name should be localized. Do + // we care? + talk_base::Filesystem::GetApplicationName(&app_name); + pa_context *context = symbol_table_.pa_context_new()( + symbol_table_.pa_threaded_mainloop_get_api()(mainloop_), + app_name.c_str()); + if (!context) { + LOG(LS_ERROR) << "Can't create context"; + goto fail0; + } + + // Now connect. + if (!ConnectToPulse(context)) { + goto fail1; + } + + // Otherwise the connection succeeded and is ready. + return context; + + fail1: + symbol_table_.pa_context_unref()(context); + fail0: + return NULL; +} + +struct EnumerateDevicesCallbackData { + PulseAudioSoundSystem *instance; + SoundSystemInterface::SoundDeviceLocatorList *devices; +}; + +void PulseAudioSoundSystem::EnumeratePlaybackDevicesCallbackThunk( + pa_context *unused, + const pa_sink_info *info, + int eol, + void *userdata) { + EnumerateDevicesCallbackData *data = + static_cast(userdata); + data->instance->OnEnumeratePlaybackDevicesCallback(data->devices, info, eol); +} + +void PulseAudioSoundSystem::EnumerateCaptureDevicesCallbackThunk( + pa_context *unused, + const pa_source_info *info, + int eol, + void *userdata) { + EnumerateDevicesCallbackData *data = + static_cast(userdata); + data->instance->OnEnumerateCaptureDevicesCallback(data->devices, info, eol); +} + +void PulseAudioSoundSystem::OnEnumeratePlaybackDevicesCallback( + SoundDeviceLocatorList *devices, + const pa_sink_info *info, + int eol) { + if (eol) { + // List is over. Wake EnumerateDevices(). + Signal(); + return; + } + + // Else this is the next device. + devices->push_back( + new PulseAudioDeviceLocator(info->description, info->name)); +} + +void PulseAudioSoundSystem::OnEnumerateCaptureDevicesCallback( + SoundDeviceLocatorList *devices, + const pa_source_info *info, + int eol) { + if (eol) { + // List is over. Wake EnumerateDevices(). + Signal(); + return; + } + + if (info->monitor_of_sink != PA_INVALID_INDEX) { + // We don't want to list monitor sources, since they are almost certainly + // not what the user wants for voice conferencing. + return; + } + + // Else this is the next device. + devices->push_back( + new PulseAudioDeviceLocator(info->description, info->name)); +} + +template +bool PulseAudioSoundSystem::EnumerateDevices( + SoundDeviceLocatorList *devices, + pa_operation *(*enumerate_fn)( + pa_context *c, + void (*callback_fn)( + pa_context *c, + const InfoStruct *i, + int eol, + void *userdata), + void *userdata), + void (*callback_fn)( + pa_context *c, + const InfoStruct *i, + int eol, + void *userdata)) { + ClearSoundDeviceLocatorList(devices); + if (!IsInitialized()) { + return false; + } + + EnumerateDevicesCallbackData data; + data.instance = this; + data.devices = devices; + + Lock(); + pa_operation *op = (*enumerate_fn)( + context_, + callback_fn, + &data); + bool ret = FinishOperation(op); + Unlock(); + return ret; +} + +struct GetDefaultDeviceCallbackData { + PulseAudioSoundSystem *instance; + SoundDeviceLocator **device; +}; + +template +void PulseAudioSoundSystem::GetDefaultDeviceCallbackThunk( + pa_context *unused, + const pa_server_info *info, + void *userdata) { + GetDefaultDeviceCallbackData *data = + static_cast(userdata); + data->instance->OnGetDefaultDeviceCallback(info, data->device); +} + +template +void PulseAudioSoundSystem::OnGetDefaultDeviceCallback( + const pa_server_info *info, + SoundDeviceLocator **device) { + if (info) { + const char *dev = info->*field; + if (dev) { + *device = new PulseAudioDeviceLocator("Default device", dev); + } + } + Signal(); +} + +template +bool PulseAudioSoundSystem::GetDefaultDevice(SoundDeviceLocator **device) { + if (!IsInitialized()) { + return false; + } + bool ret; + *device = NULL; + GetDefaultDeviceCallbackData data; + data.instance = this; + data.device = device; + Lock(); + pa_operation *op = symbol_table_.pa_context_get_server_info()( + context_, + &GetDefaultDeviceCallbackThunk, + &data); + ret = FinishOperation(op); + Unlock(); + return ret && (*device != NULL); +} + +void PulseAudioSoundSystem::StreamStateChangedCallbackThunk( + pa_stream *stream, + void *userdata) { + PulseAudioSoundSystem *instance = + static_cast(userdata); + instance->OnStreamStateChangedCallback(stream); +} + +void PulseAudioSoundSystem::OnStreamStateChangedCallback(pa_stream *stream) { + pa_stream_state_t state = symbol_table_.pa_stream_get_state()(stream); + if (state == PA_STREAM_READY) { + LOG(LS_INFO) << "Pulse stream " << stream << " ready"; + } else if (state == PA_STREAM_FAILED || + state == PA_STREAM_TERMINATED || + state == PA_STREAM_UNCONNECTED) { + LOG(LS_ERROR) << "Pulse stream " << stream << " failed to connect: " + << LastError(); + } +} + +template +StreamInterface *PulseAudioSoundSystem::OpenDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms, + const char *stream_name, + StreamInterface *(PulseAudioSoundSystem::*connect_fn)( + pa_stream *stream, + const char *dev, + int flags, + pa_stream_flags_t pa_flags, + int latency, + const pa_sample_spec &spec)) { + if (!IsInitialized()) { + return NULL; + } + + const char *dev = static_cast(device)-> + device_name().c_str(); + + StreamInterface *stream_interface = NULL; + + ASSERT(params.format < ARRAY_SIZE(kCricketFormatToPulseFormatTable)); + + pa_sample_spec spec; + spec.format = kCricketFormatToPulseFormatTable[params.format]; + spec.rate = params.freq; + spec.channels = params.channels; + + int pa_flags = 0; + if (params.flags & FLAG_REPORT_LATENCY) { + pa_flags |= PA_STREAM_INTERPOLATE_TIMING | + PA_STREAM_AUTO_TIMING_UPDATE; + } + + if (params.latency != kNoLatencyRequirements) { + // If configuring a specific latency then we want to specify + // PA_STREAM_ADJUST_LATENCY to make the server adjust parameters + // automatically to reach that target latency. However, that flag doesn't + // exist in Ubuntu 8.04 and many people still use that, so we have to check + // the protocol version of libpulse. + if (symbol_table_.pa_context_get_protocol_version()(context_) >= + kAdjustLatencyProtocolVersion) { + pa_flags |= PA_STREAM_ADJUST_LATENCY; + } + } + + Lock(); + + pa_stream *stream = symbol_table_.pa_stream_new()(context_, stream_name, + &spec, NULL); + if (!stream) { + LOG(LS_ERROR) << "Can't create pa_stream"; + goto done; + } + + // Set a state callback to log errors. + symbol_table_.pa_stream_set_state_callback()(stream, + &StreamStateChangedCallbackThunk, + this); + + stream_interface = (this->*connect_fn)( + stream, + dev, + params.flags, + static_cast(pa_flags), + params.latency, + spec); + if (!stream_interface) { + LOG(LS_ERROR) << "Can't connect stream to " << dev; + symbol_table_.pa_stream_unref()(stream); + } + + done: + Unlock(); + return stream_interface; +} + +// Must be called with the lock held. +SoundOutputStreamInterface *PulseAudioSoundSystem::ConnectOutputStream( + pa_stream *stream, + const char *dev, + int flags, + pa_stream_flags_t pa_flags, + int latency, + const pa_sample_spec &spec) { + pa_buffer_attr attr = {0}; + pa_buffer_attr *pattr = NULL; + if (latency != kNoLatencyRequirements) { + // kLowLatency is 0, so we treat it the same as a request for zero latency. + ssize_t bytes_per_sec = symbol_table_.pa_bytes_per_second()(&spec); + latency = talk_base::_max( + latency, + static_cast( + bytes_per_sec * kPlaybackLatencyMinimumMsecs / + talk_base::kNumMicrosecsPerSec)); + FillPlaybackBufferAttr(latency, &attr); + pattr = &attr; + } + if (symbol_table_.pa_stream_connect_playback()( + stream, + dev, + pattr, + pa_flags, + // Let server choose volume + NULL, + // Not synchronized to any other playout + NULL) != 0) { + return NULL; + } + return new PulseAudioOutputStream(this, stream, flags, latency); +} + +// Must be called with the lock held. +SoundInputStreamInterface *PulseAudioSoundSystem::ConnectInputStream( + pa_stream *stream, + const char *dev, + int flags, + pa_stream_flags_t pa_flags, + int latency, + const pa_sample_spec &spec) { + pa_buffer_attr attr = {0}; + pa_buffer_attr *pattr = NULL; + if (latency != kNoLatencyRequirements) { + size_t bytes_per_sec = symbol_table_.pa_bytes_per_second()(&spec); + if (latency == kLowLatency) { + latency = bytes_per_sec * kLowCaptureLatencyMsecs / + talk_base::kNumMicrosecsPerSec; + } + // Note: fragsize specifies a maximum transfer size, not a minimum, so it is + // not possible to force a high latency setting, only a low one. + attr.fragsize = latency; + attr.maxlength = latency + bytes_per_sec * kCaptureBufferExtraMsecs / + talk_base::kNumMicrosecsPerSec; + LOG(LS_VERBOSE) << "Configuring latency = " << attr.fragsize + << ", maxlength = " << attr.maxlength; + pattr = &attr; + } + if (symbol_table_.pa_stream_connect_record()(stream, + dev, + pattr, + pa_flags) != 0) { + return NULL; + } + return new PulseAudioInputStream(this, stream, flags); +} + +// Must be called with the lock held. +bool PulseAudioSoundSystem::FinishOperation(pa_operation *op) { + if (!op) { + LOG(LS_ERROR) << "Failed to start operation"; + return false; + } + + do { + Wait(); + } while (symbol_table_.pa_operation_get_state()(op) == PA_OPERATION_RUNNING); + + symbol_table_.pa_operation_unref()(op); + + return true; +} + +inline void PulseAudioSoundSystem::Lock() { + symbol_table_.pa_threaded_mainloop_lock()(mainloop_); +} + +inline void PulseAudioSoundSystem::Unlock() { + symbol_table_.pa_threaded_mainloop_unlock()(mainloop_); +} + +// Must be called with the lock held. +inline void PulseAudioSoundSystem::Wait() { + symbol_table_.pa_threaded_mainloop_wait()(mainloop_); +} + +// Must be called with the lock held. +inline void PulseAudioSoundSystem::Signal() { + symbol_table_.pa_threaded_mainloop_signal()(mainloop_, 0); +} + +// Must be called with the lock held. +const char *PulseAudioSoundSystem::LastError() { + return symbol_table_.pa_strerror()(symbol_table_.pa_context_errno()( + context_)); +} + +} // namespace cricket + +#endif // HAVE_LIBPULSE diff --git a/talk/sound/pulseaudiosoundsystem.h b/talk/sound/pulseaudiosoundsystem.h new file mode 100644 index 000000000..8a9fe4928 --- /dev/null +++ b/talk/sound/pulseaudiosoundsystem.h @@ -0,0 +1,194 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_PULSEAUDIOSOUNDSYSTEM_H_ +#define TALK_SOUND_PULSEAUDIOSOUNDSYSTEM_H_ + +#ifdef HAVE_LIBPULSE + +#include "talk/base/constructormagic.h" +#include "talk/sound/pulseaudiosymboltable.h" +#include "talk/sound/soundsysteminterface.h" + +namespace cricket { + +class PulseAudioInputStream; +class PulseAudioOutputStream; +class PulseAudioStream; + +// Sound system implementation for PulseAudio, a cross-platform sound server +// (but commonly used only on Linux, which is the only platform we support +// it on). +// Init(), Terminate(), and the destructor should never be invoked concurrently, +// but all other methods are thread-safe. +class PulseAudioSoundSystem : public SoundSystemInterface { + friend class PulseAudioInputStream; + friend class PulseAudioOutputStream; + friend class PulseAudioStream; + public: + static SoundSystemInterface *Create() { + return new PulseAudioSoundSystem(); + } + + PulseAudioSoundSystem(); + + virtual ~PulseAudioSoundSystem(); + + virtual bool Init(); + virtual void Terminate(); + + virtual bool EnumeratePlaybackDevices(SoundDeviceLocatorList *devices); + virtual bool EnumerateCaptureDevices(SoundDeviceLocatorList *devices); + + virtual bool GetDefaultPlaybackDevice(SoundDeviceLocator **device); + virtual bool GetDefaultCaptureDevice(SoundDeviceLocator **device); + + virtual SoundOutputStreamInterface *OpenPlaybackDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms); + virtual SoundInputStreamInterface *OpenCaptureDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms); + + virtual const char *GetName() const; + + private: + bool IsInitialized(); + + static void ConnectToPulseCallbackThunk(pa_context *context, void *userdata); + + void OnConnectToPulseCallback(pa_context *context, bool *connect_done); + + bool ConnectToPulse(pa_context *context); + + pa_context *CreateNewConnection(); + + template + bool EnumerateDevices(SoundDeviceLocatorList *devices, + pa_operation *(*enumerate_fn)( + pa_context *c, + void (*callback_fn)( + pa_context *c, + const InfoStruct *i, + int eol, + void *userdata), + void *userdata), + void (*callback_fn)( + pa_context *c, + const InfoStruct *i, + int eol, + void *userdata)); + + static void EnumeratePlaybackDevicesCallbackThunk(pa_context *unused, + const pa_sink_info *info, + int eol, + void *userdata); + + static void EnumerateCaptureDevicesCallbackThunk(pa_context *unused, + const pa_source_info *info, + int eol, + void *userdata); + + void OnEnumeratePlaybackDevicesCallback( + SoundDeviceLocatorList *devices, + const pa_sink_info *info, + int eol); + + void OnEnumerateCaptureDevicesCallback( + SoundDeviceLocatorList *devices, + const pa_source_info *info, + int eol); + + template + static void GetDefaultDeviceCallbackThunk( + pa_context *unused, + const pa_server_info *info, + void *userdata); + + template + void OnGetDefaultDeviceCallback( + const pa_server_info *info, + SoundDeviceLocator **device); + + template + bool GetDefaultDevice(SoundDeviceLocator **device); + + static void StreamStateChangedCallbackThunk(pa_stream *stream, + void *userdata); + + void OnStreamStateChangedCallback(pa_stream *stream); + + template + StreamInterface *OpenDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms, + const char *stream_name, + StreamInterface *(PulseAudioSoundSystem::*connect_fn)( + pa_stream *stream, + const char *dev, + int flags, + pa_stream_flags_t pa_flags, + int latency, + const pa_sample_spec &spec)); + + SoundOutputStreamInterface *ConnectOutputStream( + pa_stream *stream, + const char *dev, + int flags, + pa_stream_flags_t pa_flags, + int latency, + const pa_sample_spec &spec); + + SoundInputStreamInterface *ConnectInputStream( + pa_stream *stream, + const char *dev, + int flags, + pa_stream_flags_t pa_flags, + int latency, + const pa_sample_spec &spec); + + bool FinishOperation(pa_operation *op); + + void Lock(); + void Unlock(); + void Wait(); + void Signal(); + + const char *LastError(); + + pa_threaded_mainloop *mainloop_; + pa_context *context_; + PulseAudioSymbolTable symbol_table_; + + DISALLOW_COPY_AND_ASSIGN(PulseAudioSoundSystem); +}; + +} // namespace cricket + +#endif // HAVE_LIBPULSE + +#endif // TALK_SOUND_PULSEAUDIOSOUNDSYSTEM_H_ diff --git a/talk/sound/pulseaudiosymboltable.cc b/talk/sound/pulseaudiosymboltable.cc new file mode 100644 index 000000000..05213ec84 --- /dev/null +++ b/talk/sound/pulseaudiosymboltable.cc @@ -0,0 +1,41 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifdef HAVE_LIBPULSE + +#include "talk/sound/pulseaudiosymboltable.h" + +namespace cricket { + +#define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME PULSE_AUDIO_SYMBOLS_CLASS_NAME +#define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST PULSE_AUDIO_SYMBOLS_LIST +#define LATE_BINDING_SYMBOL_TABLE_DLL_NAME "libpulse.so.0" +#include "talk/base/latebindingsymboltable.cc.def" + +} // namespace cricket + +#endif // HAVE_LIBPULSE diff --git a/talk/sound/pulseaudiosymboltable.h b/talk/sound/pulseaudiosymboltable.h new file mode 100644 index 000000000..ef651578e --- /dev/null +++ b/talk/sound/pulseaudiosymboltable.h @@ -0,0 +1,104 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_PULSEAUDIOSYMBOLTABLE_H_ +#define TALK_SOUND_PULSEAUDIOSYMBOLTABLE_H_ + +#include +#include +#include +#include +#include +#include + +#include "talk/base/latebindingsymboltable.h" + +namespace cricket { + +#define PULSE_AUDIO_SYMBOLS_CLASS_NAME PulseAudioSymbolTable +// The PulseAudio symbols we need, as an X-Macro list. +// This list must contain precisely every libpulse function that is used in +// pulseaudiosoundsystem.cc. +#define PULSE_AUDIO_SYMBOLS_LIST \ + X(pa_bytes_per_second) \ + X(pa_context_connect) \ + X(pa_context_disconnect) \ + X(pa_context_errno) \ + X(pa_context_get_protocol_version) \ + X(pa_context_get_server_info) \ + X(pa_context_get_sink_info_list) \ + X(pa_context_get_sink_input_info) \ + X(pa_context_get_source_info_by_index) \ + X(pa_context_get_source_info_list) \ + X(pa_context_get_state) \ + X(pa_context_new) \ + X(pa_context_set_sink_input_volume) \ + X(pa_context_set_source_volume_by_index) \ + X(pa_context_set_state_callback) \ + X(pa_context_unref) \ + X(pa_cvolume_set) \ + X(pa_operation_get_state) \ + X(pa_operation_unref) \ + X(pa_stream_connect_playback) \ + X(pa_stream_connect_record) \ + X(pa_stream_disconnect) \ + X(pa_stream_drop) \ + X(pa_stream_get_device_index) \ + X(pa_stream_get_index) \ + X(pa_stream_get_latency) \ + X(pa_stream_get_sample_spec) \ + X(pa_stream_get_state) \ + X(pa_stream_new) \ + X(pa_stream_peek) \ + X(pa_stream_readable_size) \ + X(pa_stream_set_buffer_attr) \ + X(pa_stream_set_overflow_callback) \ + X(pa_stream_set_read_callback) \ + X(pa_stream_set_state_callback) \ + X(pa_stream_set_underflow_callback) \ + X(pa_stream_set_write_callback) \ + X(pa_stream_unref) \ + X(pa_stream_writable_size) \ + X(pa_stream_write) \ + X(pa_strerror) \ + X(pa_threaded_mainloop_free) \ + X(pa_threaded_mainloop_get_api) \ + X(pa_threaded_mainloop_lock) \ + X(pa_threaded_mainloop_new) \ + X(pa_threaded_mainloop_signal) \ + X(pa_threaded_mainloop_start) \ + X(pa_threaded_mainloop_stop) \ + X(pa_threaded_mainloop_unlock) \ + X(pa_threaded_mainloop_wait) + +#define LATE_BINDING_SYMBOL_TABLE_CLASS_NAME PULSE_AUDIO_SYMBOLS_CLASS_NAME +#define LATE_BINDING_SYMBOL_TABLE_SYMBOLS_LIST PULSE_AUDIO_SYMBOLS_LIST +#include "talk/base/latebindingsymboltable.h.def" + +} // namespace cricket + +#endif // TALK_SOUND_PULSEAUDIOSYMBOLTABLE_H_ diff --git a/talk/sound/sounddevicelocator.h b/talk/sound/sounddevicelocator.h new file mode 100644 index 000000000..e0a8970d6 --- /dev/null +++ b/talk/sound/sounddevicelocator.h @@ -0,0 +1,71 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_SOUNDDEVICELOCATOR_H_ +#define TALK_SOUND_SOUNDDEVICELOCATOR_H_ + +#include + +#include "talk/base/constructormagic.h" + +namespace cricket { + +// A simple container for holding the name of a device and any additional id +// information needed to locate and open it. Implementations of +// SoundSystemInterface must subclass this to add any id information that they +// need. +class SoundDeviceLocator { + public: + virtual ~SoundDeviceLocator() {} + + // Human-readable name for the device. + const std::string &name() const { return name_; } + + // Name sound system uses to locate this device. + const std::string &device_name() const { return device_name_; } + + // Makes a duplicate of this locator. + virtual SoundDeviceLocator *Copy() const = 0; + + protected: + SoundDeviceLocator(const std::string &name, + const std::string &device_name) + : name_(name), device_name_(device_name) {} + + explicit SoundDeviceLocator(const SoundDeviceLocator &that) + : name_(that.name_), device_name_(that.device_name_) {} + + std::string name_; + std::string device_name_; + + private: + DISALLOW_ASSIGN(SoundDeviceLocator); +}; + +} // namespace cricket + +#endif // TALK_SOUND_SOUNDDEVICELOCATOR_H_ diff --git a/talk/sound/soundinputstreaminterface.h b/talk/sound/soundinputstreaminterface.h new file mode 100644 index 000000000..de831a6a8 --- /dev/null +++ b/talk/sound/soundinputstreaminterface.h @@ -0,0 +1,85 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_SOUNDINPUTSTREAMINTERFACE_H_ +#define TALK_SOUND_SOUNDINPUTSTREAMINTERFACE_H_ + +#include "talk/base/constructormagic.h" +#include "talk/base/sigslot.h" + +namespace cricket { + +// Interface for consuming an input stream from a recording device. +// Semantics and thread-safety of StartReading()/StopReading() are the same as +// for talk_base::Worker. +class SoundInputStreamInterface { + public: + virtual ~SoundInputStreamInterface() {} + + // Starts the reading of samples on the current thread. + virtual bool StartReading() = 0; + // Stops the reading of samples. + virtual bool StopReading() = 0; + + // Retrieves the current input volume for this stream. Nominal range is + // defined by SoundSystemInterface::k(Max|Min)Volume, but values exceeding the + // max may be possible in some implementations. This call retrieves the actual + // volume currently in use by the OS, not a cached value from a previous + // (Get|Set)Volume() call. + virtual bool GetVolume(int *volume) = 0; + + // Changes the input volume for this stream. Nominal range is defined by + // SoundSystemInterface::k(Max|Min)Volume. The effect of exceeding kMaxVolume + // is implementation-defined. + virtual bool SetVolume(int volume) = 0; + + // Closes this stream object. If currently reading then this may only be + // called from the reading thread. + virtual bool Close() = 0; + + // Get the latency of the stream. + virtual int LatencyUsecs() = 0; + + // Notifies the consumer of new data read from the device. + // The first parameter is a pointer to the data read, and is only valid for + // the duration of the call. + // The second parameter is the amount of data read in bytes (i.e., the valid + // length of the memory pointed to). + // The 3rd parameter is the stream that is issuing the callback. + sigslot::signal3 SignalSamplesRead; + + protected: + SoundInputStreamInterface() {} + + private: + DISALLOW_COPY_AND_ASSIGN(SoundInputStreamInterface); +}; + +} // namespace cricket + +#endif // TALK_SOUND_SOUNDOUTPUTSTREAMINTERFACE_H_ diff --git a/talk/sound/soundoutputstreaminterface.h b/talk/sound/soundoutputstreaminterface.h new file mode 100644 index 000000000..d096ba3f5 --- /dev/null +++ b/talk/sound/soundoutputstreaminterface.h @@ -0,0 +1,89 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_SOUNDOUTPUTSTREAMINTERFACE_H_ +#define TALK_SOUND_SOUNDOUTPUTSTREAMINTERFACE_H_ + +#include "talk/base/constructormagic.h" +#include "talk/base/sigslot.h" + +namespace cricket { + +// Interface for outputting a stream to a playback device. +// Semantics and thread-safety of EnableBufferMonitoring()/ +// DisableBufferMonitoring() are the same as for talk_base::Worker. +class SoundOutputStreamInterface { + public: + virtual ~SoundOutputStreamInterface() {} + + // Enables monitoring the available buffer space on the current thread. + virtual bool EnableBufferMonitoring() = 0; + // Disables the monitoring. + virtual bool DisableBufferMonitoring() = 0; + + // Write the given samples to the devices. If currently monitoring then this + // may only be called from the monitoring thread. + virtual bool WriteSamples(const void *sample_data, + size_t size) = 0; + + // Retrieves the current output volume for this stream. Nominal range is + // defined by SoundSystemInterface::k(Max|Min)Volume, but values exceeding the + // max may be possible in some implementations. This call retrieves the actual + // volume currently in use by the OS, not a cached value from a previous + // (Get|Set)Volume() call. + virtual bool GetVolume(int *volume) = 0; + + // Changes the output volume for this stream. Nominal range is defined by + // SoundSystemInterface::k(Max|Min)Volume. The effect of exceeding kMaxVolume + // is implementation-defined. + virtual bool SetVolume(int volume) = 0; + + // Closes this stream object. If currently monitoring then this may only be + // called from the monitoring thread. + virtual bool Close() = 0; + + // Get the latency of the stream. + virtual int LatencyUsecs() = 0; + + // Notifies the producer of the available buffer space for writes. + // It fires continuously as long as the space is greater than zero. + // The first parameter is the amount of buffer space available for data to + // be written (i.e., the maximum amount of data that can be written right now + // with WriteSamples() without blocking). + // The 2nd parameter is the stream that is issuing the callback. + sigslot::signal2 SignalBufferSpace; + + protected: + SoundOutputStreamInterface() {} + + private: + DISALLOW_COPY_AND_ASSIGN(SoundOutputStreamInterface); +}; + +} // namespace cricket + +#endif // TALK_SOUND_SOUNDOUTPUTSTREAMINTERFACE_H_ diff --git a/talk/sound/soundsystemfactory.h b/talk/sound/soundsystemfactory.h new file mode 100644 index 000000000..517220b03 --- /dev/null +++ b/talk/sound/soundsystemfactory.h @@ -0,0 +1,44 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_SOUNDSYSTEMFACTORY_H_ +#define TALK_SOUND_SOUNDSYSTEMFACTORY_H_ + +#include "talk/base/referencecountedsingletonfactory.h" + +namespace cricket { + +class SoundSystemInterface; + +typedef talk_base::ReferenceCountedSingletonFactory + SoundSystemFactory; + +typedef talk_base::rcsf_ptr SoundSystemHandle; + +} // namespace cricket + +#endif // TALK_SOUND_SOUNDSYSTEMFACTORY_H_ diff --git a/talk/sound/soundsysteminterface.cc b/talk/sound/soundsysteminterface.cc new file mode 100644 index 000000000..b43226291 --- /dev/null +++ b/talk/sound/soundsysteminterface.cc @@ -0,0 +1,46 @@ +/* + * libjingle + * Copyright 2004--2010, 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/sound/soundsysteminterface.h" + +#include "talk/sound/sounddevicelocator.h" + +namespace cricket { + +void SoundSystemInterface::ClearSoundDeviceLocatorList( + SoundSystemInterface::SoundDeviceLocatorList *devices) { + for (SoundDeviceLocatorList::iterator i = devices->begin(); + i != devices->end(); + ++i) { + if (*i) { + delete *i; + } + } + devices->clear(); +} + +} // namespace cricket diff --git a/talk/sound/soundsysteminterface.h b/talk/sound/soundsysteminterface.h new file mode 100644 index 000000000..7a059b0d6 --- /dev/null +++ b/talk/sound/soundsysteminterface.h @@ -0,0 +1,129 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_SOUNDSYSTEMINTERFACE_H_ +#define TALK_SOUND_SOUNDSYSTEMINTERFACE_H_ + +#include + +#include "talk/base/constructormagic.h" + +namespace cricket { + +class SoundDeviceLocator; +class SoundInputStreamInterface; +class SoundOutputStreamInterface; + +// Interface for a platform's sound system. +// Implementations must guarantee thread-safety for at least the following use +// cases: +// 1) Concurrent enumeration and opening of devices from different threads. +// 2) Concurrent use of different Sound(Input|Output)StreamInterface +// instances from different threads (but concurrent use of the _same_ one from +// different threads need not be supported). +class SoundSystemInterface { + public: + typedef std::vector SoundDeviceLocatorList; + + enum SampleFormat { + // Only one supported sample format at this time. + // The values here may be used in lookup tables, so they shouldn't change. + FORMAT_S16LE = 0, + }; + + enum Flags { + // Enable reporting the current stream latency in + // Sound(Input|Output)StreamInterface. See those classes for more details. + FLAG_REPORT_LATENCY = (1 << 0), + }; + + struct OpenParams { + // Format for the sound stream. + SampleFormat format; + // Sampling frequency in hertz. + unsigned int freq; + // Number of channels in the PCM stream. + unsigned int channels; + // Misc flags. Should be taken from the Flags enum above. + int flags; + // Desired latency, measured as number of bytes of sample data + int latency; + }; + + // Special values for the "latency" field of OpenParams. + // Use this one to say you don't care what the latency is. The sound system + // will optimize for other things instead. + static const int kNoLatencyRequirements = -1; + // Use this one to say that you want the sound system to pick an appropriate + // small latency value. The sound system may pick the minimum allowed one, or + // a slightly higher one in the event that the true minimum requires an + // undesirable trade-off. + static const int kLowLatency = 0; + + // Max value for the volume parameters for Sound(Input|Output)StreamInterface. + static const int kMaxVolume = 255; + // Min value for the volume parameters for Sound(Input|Output)StreamInterface. + static const int kMinVolume = 0; + + // Helper for clearing a locator list and deleting the entries. + static void ClearSoundDeviceLocatorList(SoundDeviceLocatorList *devices); + + virtual ~SoundSystemInterface() {} + + virtual bool Init() = 0; + virtual void Terminate() = 0; + + // Enumerates the available devices. (Any pre-existing locators in the lists + // are deleted.) + virtual bool EnumeratePlaybackDevices(SoundDeviceLocatorList *devices) = 0; + virtual bool EnumerateCaptureDevices(SoundDeviceLocatorList *devices) = 0; + + // Gets a special locator for the default device. + virtual bool GetDefaultPlaybackDevice(SoundDeviceLocator **device) = 0; + virtual bool GetDefaultCaptureDevice(SoundDeviceLocator **device) = 0; + + // Opens the given device, or returns NULL on error. + virtual SoundOutputStreamInterface *OpenPlaybackDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms) = 0; + virtual SoundInputStreamInterface *OpenCaptureDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms) = 0; + + // A human-readable name for this sound system. + virtual const char *GetName() const = 0; + + protected: + SoundSystemInterface() {} + + private: + DISALLOW_COPY_AND_ASSIGN(SoundSystemInterface); +}; + +} // namespace cricket + +#endif // TALK_SOUND_SOUNDSYSTEMINTERFACE_H_ diff --git a/talk/sound/soundsystemproxy.cc b/talk/sound/soundsystemproxy.cc new file mode 100644 index 000000000..737a6bb42 --- /dev/null +++ b/talk/sound/soundsystemproxy.cc @@ -0,0 +1,64 @@ +/* + * libjingle + * Copyright 2004--2010, 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/sound/soundsystemproxy.h" + +namespace cricket { + +bool SoundSystemProxy::EnumeratePlaybackDevices( + SoundDeviceLocatorList *devices) { + return wrapped_ ? wrapped_->EnumeratePlaybackDevices(devices) : false; +} + +bool SoundSystemProxy::EnumerateCaptureDevices( + SoundDeviceLocatorList *devices) { + return wrapped_ ? wrapped_->EnumerateCaptureDevices(devices) : false; +} + +bool SoundSystemProxy::GetDefaultPlaybackDevice( + SoundDeviceLocator **device) { + return wrapped_ ? wrapped_->GetDefaultPlaybackDevice(device) : false; +} + +bool SoundSystemProxy::GetDefaultCaptureDevice( + SoundDeviceLocator **device) { + return wrapped_ ? wrapped_->GetDefaultCaptureDevice(device) : false; +} + +SoundOutputStreamInterface *SoundSystemProxy::OpenPlaybackDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms) { + return wrapped_ ? wrapped_->OpenPlaybackDevice(device, params) : NULL; +} + +SoundInputStreamInterface *SoundSystemProxy::OpenCaptureDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms) { + return wrapped_ ? wrapped_->OpenCaptureDevice(device, params) : NULL; +} + +} // namespace cricket diff --git a/talk/sound/soundsystemproxy.h b/talk/sound/soundsystemproxy.h new file mode 100644 index 000000000..9ccace808 --- /dev/null +++ b/talk/sound/soundsystemproxy.h @@ -0,0 +1,64 @@ +/* + * libjingle + * Copyright 2004--2010, 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. + */ + +#ifndef TALK_SOUND_SOUNDSYSTEMPROXY_H_ +#define TALK_SOUND_SOUNDSYSTEMPROXY_H_ + +#include "talk/base/basictypes.h" // for NULL +#include "talk/sound/soundsysteminterface.h" + +namespace cricket { + +// A SoundSystemProxy is a sound system that defers to another one. +// Init(), Terminate(), and GetName() are left as pure virtual, so a sub-class +// must define them. +class SoundSystemProxy : public SoundSystemInterface { + public: + SoundSystemProxy() : wrapped_(NULL) {} + + // Each of these methods simply defers to wrapped_ if non-NULL, else fails. + + virtual bool EnumeratePlaybackDevices(SoundDeviceLocatorList *devices); + virtual bool EnumerateCaptureDevices(SoundDeviceLocatorList *devices); + + virtual bool GetDefaultPlaybackDevice(SoundDeviceLocator **device); + virtual bool GetDefaultCaptureDevice(SoundDeviceLocator **device); + + virtual SoundOutputStreamInterface *OpenPlaybackDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms); + virtual SoundInputStreamInterface *OpenCaptureDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms); + + protected: + SoundSystemInterface *wrapped_; +}; + +} // namespace cricket + +#endif // TALK_SOUND_SOUNDSYSTEMPROXY_H_ diff --git a/talk/third_party/libudev/libudev.h b/talk/third_party/libudev/libudev.h new file mode 100644 index 000000000..5bc42df5b --- /dev/null +++ b/talk/third_party/libudev/libudev.h @@ -0,0 +1,175 @@ +/* + * libudev - interface to udev device information + * + * Copyright (C) 2008-2010 Kay Sievers + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + */ + +#ifndef _LIBUDEV_H_ +#define _LIBUDEV_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * udev - library context + * + * reads the udev config and system environment + * allows custom logging + */ +struct udev; +struct udev *udev_ref(struct udev *udev); +void udev_unref(struct udev *udev); +struct udev *udev_new(void); +void udev_set_log_fn(struct udev *udev, + void (*log_fn)(struct udev *udev, + int priority, const char *file, int line, const char *fn, + const char *format, va_list args)); +int udev_get_log_priority(struct udev *udev); +void udev_set_log_priority(struct udev *udev, int priority); +const char *udev_get_sys_path(struct udev *udev); +const char *udev_get_dev_path(struct udev *udev); +void *udev_get_userdata(struct udev *udev); +void udev_set_userdata(struct udev *udev, void *userdata); + +/* + * udev_list + * + * access to libudev generated lists + */ +struct udev_list_entry; +struct udev_list_entry *udev_list_entry_get_next(struct udev_list_entry *list_entry); +struct udev_list_entry *udev_list_entry_get_by_name(struct udev_list_entry *list_entry, const char *name); +const char *udev_list_entry_get_name(struct udev_list_entry *list_entry); +const char *udev_list_entry_get_value(struct udev_list_entry *list_entry); +/** + * udev_list_entry_foreach: + * @list_entry: entry to store the current position + * @first_entry: first entry to start with + * + * Helper to iterate over all entries of a list. + */ +#define udev_list_entry_foreach(list_entry, first_entry) \ + for (list_entry = first_entry; \ + list_entry != NULL; \ + list_entry = udev_list_entry_get_next(list_entry)) + +/* + * udev_device + * + * access to sysfs/kernel devices + */ +struct udev_device; +struct udev_device *udev_device_ref(struct udev_device *udev_device); +void udev_device_unref(struct udev_device *udev_device); +struct udev *udev_device_get_udev(struct udev_device *udev_device); +struct udev_device *udev_device_new_from_syspath(struct udev *udev, const char *syspath); +struct udev_device *udev_device_new_from_devnum(struct udev *udev, char type, dev_t devnum); +struct udev_device *udev_device_new_from_subsystem_sysname(struct udev *udev, const char *subsystem, const char *sysname); +struct udev_device *udev_device_new_from_environment(struct udev *udev); +/* udev_device_get_parent_*() does not take a reference on the returned device, it is automatically unref'd with the parent */ +struct udev_device *udev_device_get_parent(struct udev_device *udev_device); +struct udev_device *udev_device_get_parent_with_subsystem_devtype(struct udev_device *udev_device, + const char *subsystem, const char *devtype); +/* retrieve device properties */ +const char *udev_device_get_devpath(struct udev_device *udev_device); +const char *udev_device_get_subsystem(struct udev_device *udev_device); +const char *udev_device_get_devtype(struct udev_device *udev_device); +const char *udev_device_get_syspath(struct udev_device *udev_device); +const char *udev_device_get_sysname(struct udev_device *udev_device); +const char *udev_device_get_sysnum(struct udev_device *udev_device); +const char *udev_device_get_devnode(struct udev_device *udev_device); +struct udev_list_entry *udev_device_get_devlinks_list_entry(struct udev_device *udev_device); +struct udev_list_entry *udev_device_get_properties_list_entry(struct udev_device *udev_device); +struct udev_list_entry *udev_device_get_tags_list_entry(struct udev_device *udev_device); +const char *udev_device_get_property_value(struct udev_device *udev_device, const char *key); +const char *udev_device_get_driver(struct udev_device *udev_device); +dev_t udev_device_get_devnum(struct udev_device *udev_device); +const char *udev_device_get_action(struct udev_device *udev_device); +unsigned long long int udev_device_get_seqnum(struct udev_device *udev_device); +const char *udev_device_get_sysattr_value(struct udev_device *udev_device, const char *sysattr); + +/* + * udev_monitor + * + * access to kernel uevents and udev events + */ +struct udev_monitor; +struct udev_monitor *udev_monitor_ref(struct udev_monitor *udev_monitor); +void udev_monitor_unref(struct udev_monitor *udev_monitor); +struct udev *udev_monitor_get_udev(struct udev_monitor *udev_monitor); +/* kernel and udev generated events over netlink */ +struct udev_monitor *udev_monitor_new_from_netlink(struct udev *udev, const char *name); +/* custom socket (use netlink and filters instead) */ +struct udev_monitor *udev_monitor_new_from_socket(struct udev *udev, const char *socket_path); +/* bind socket */ +int udev_monitor_enable_receiving(struct udev_monitor *udev_monitor); +int udev_monitor_set_receive_buffer_size(struct udev_monitor *udev_monitor, int size); +int udev_monitor_get_fd(struct udev_monitor *udev_monitor); +struct udev_device *udev_monitor_receive_device(struct udev_monitor *udev_monitor); +/* in-kernel socket filters to select messages that get delivered to a listener */ +int udev_monitor_filter_add_match_subsystem_devtype(struct udev_monitor *udev_monitor, + const char *subsystem, const char *devtype); +int udev_monitor_filter_add_match_tag(struct udev_monitor *udev_monitor, const char *tag); +int udev_monitor_filter_update(struct udev_monitor *udev_monitor); +int udev_monitor_filter_remove(struct udev_monitor *udev_monitor); + +/* + * udev_enumerate + * + * search sysfs for specific devices and provide a sorted list + */ +struct udev_enumerate; +struct udev_enumerate *udev_enumerate_ref(struct udev_enumerate *udev_enumerate); +void udev_enumerate_unref(struct udev_enumerate *udev_enumerate); +struct udev *udev_enumerate_get_udev(struct udev_enumerate *udev_enumerate); +struct udev_enumerate *udev_enumerate_new(struct udev *udev); +/* device properties filter */ +int udev_enumerate_add_match_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem); +int udev_enumerate_add_nomatch_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem); +int udev_enumerate_add_match_sysattr(struct udev_enumerate *udev_enumerate, const char *sysattr, const char *value); +int udev_enumerate_add_nomatch_sysattr(struct udev_enumerate *udev_enumerate, const char *sysattr, const char *value); +int udev_enumerate_add_match_property(struct udev_enumerate *udev_enumerate, const char *property, const char *value); +int udev_enumerate_add_match_sysname(struct udev_enumerate *udev_enumerate, const char *sysname); +int udev_enumerate_add_match_tag(struct udev_enumerate *udev_enumerate, const char *tag); +int udev_enumerate_add_syspath(struct udev_enumerate *udev_enumerate, const char *syspath); +/* run enumeration with active filters */ +int udev_enumerate_scan_devices(struct udev_enumerate *udev_enumerate); +int udev_enumerate_scan_subsystems(struct udev_enumerate *udev_enumerate); +/* return device list */ +struct udev_list_entry *udev_enumerate_get_list_entry(struct udev_enumerate *udev_enumerate); + +/* + * udev_queue + * + * access to the currently running udev events + */ +struct udev_queue; +struct udev_queue *udev_queue_ref(struct udev_queue *udev_queue); +void udev_queue_unref(struct udev_queue *udev_queue); +struct udev *udev_queue_get_udev(struct udev_queue *udev_queue); +struct udev_queue *udev_queue_new(struct udev *udev); +unsigned long long int udev_queue_get_kernel_seqnum(struct udev_queue *udev_queue); +unsigned long long int udev_queue_get_udev_seqnum(struct udev_queue *udev_queue); +int udev_queue_get_udev_is_active(struct udev_queue *udev_queue); +int udev_queue_get_queue_is_empty(struct udev_queue *udev_queue); +int udev_queue_get_seqnum_is_finished(struct udev_queue *udev_queue, unsigned long long int seqnum); +int udev_queue_get_seqnum_sequence_is_finished(struct udev_queue *udev_queue, + unsigned long long int start, unsigned long long int end); +struct udev_list_entry *udev_queue_get_queued_list_entry(struct udev_queue *udev_queue); +struct udev_list_entry *udev_queue_get_failed_list_entry(struct udev_queue *udev_queue); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/talk/xmllite/qname.cc b/talk/xmllite/qname.cc new file mode 100644 index 000000000..0dadb7909 --- /dev/null +++ b/talk/xmllite/qname.cc @@ -0,0 +1,95 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmllite/qname.h" + +namespace buzz { + +QName::QName() { +} + +QName::QName(const QName& qname) + : namespace_(qname.namespace_), + local_part_(qname.local_part_) { +} + +QName::QName(const StaticQName& const_value) + : namespace_(const_value.ns), + local_part_(const_value.local) { +} + +QName::QName(const std::string& ns, const std::string& local) + : namespace_(ns), + local_part_(local) { +} + +QName::QName(const std::string& merged_or_local) { + size_t i = merged_or_local.rfind(':'); + if (i == std::string::npos) { + local_part_ = merged_or_local; + } else { + namespace_ = merged_or_local.substr(0, i); + local_part_ = merged_or_local.substr(i + 1); + } +} + +QName::~QName() { +} + +std::string QName::Merged() const { + if (namespace_[0] == '\0') + return local_part_; + + std::string result; + result.reserve(namespace_.length() + 1 + local_part_.length()); + result += namespace_; + result += ':'; + result += local_part_; + return result; +} + +bool QName::IsEmpty() const { + return namespace_.empty() && local_part_.empty(); +} + +int QName::Compare(const StaticQName& other) const { + int result = local_part_.compare(other.local); + if (result != 0) + return result; + + return namespace_.compare(other.ns); +} + +int QName::Compare(const QName& other) const { + int result = local_part_.compare(other.local_part_); + if (result != 0) + return result; + + return namespace_.compare(other.namespace_); +} + +} // namespace buzz diff --git a/talk/xmllite/qname.h b/talk/xmllite/qname.h new file mode 100644 index 000000000..92e54d030 --- /dev/null +++ b/talk/xmllite/qname.h @@ -0,0 +1,100 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMLLITE_QNAME_H_ +#define TALK_XMLLITE_QNAME_H_ + +#include + +namespace buzz { + +class QName; + +// StaticQName is used to represend constant quailified names. They +// can be initialized statically and don't need intializers code, e.g. +// const StaticQName QN_FOO = { "foo_namespace", "foo" }; +// +// Beside this use case, QName should be used everywhere +// else. StaticQName instances are implicitly converted to QName +// objects. +struct StaticQName { + const char* const ns; + const char* const local; + + bool operator==(const QName& other) const; + bool operator!=(const QName& other) const; +}; + +class QName { + public: + QName(); + QName(const QName& qname); + QName(const StaticQName& const_value); + QName(const std::string& ns, const std::string& local); + explicit QName(const std::string& merged_or_local); + ~QName(); + + const std::string& Namespace() const { return namespace_; } + const std::string& LocalPart() const { return local_part_; } + std::string Merged() const; + bool IsEmpty() const; + + int Compare(const StaticQName& other) const; + int Compare(const QName& other) const; + + bool operator==(const StaticQName& other) const { + return Compare(other) == 0; + } + bool operator==(const QName& other) const { + return Compare(other) == 0; + } + bool operator!=(const StaticQName& other) const { + return Compare(other) != 0; + } + bool operator!=(const QName& other) const { + return Compare(other) != 0; + } + bool operator<(const QName& other) const { + return Compare(other) < 0; + } + + private: + std::string namespace_; + std::string local_part_; +}; + +inline bool StaticQName::operator==(const QName& other) const { + return other.Compare(*this) == 0; +} + +inline bool StaticQName::operator!=(const QName& other) const { + return other.Compare(*this) != 0; +} + +} // namespace buzz + +#endif // TALK_XMLLITE_QNAME_H_ diff --git a/talk/xmllite/qname_unittest.cc b/talk/xmllite/qname_unittest.cc new file mode 100644 index 000000000..976d822f1 --- /dev/null +++ b/talk/xmllite/qname_unittest.cc @@ -0,0 +1,131 @@ +/* + * libjingle + * Copyright 2004, 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 +#include "talk/base/gunit.h" +#include "talk/xmllite/qname.h" + +using buzz::StaticQName; +using buzz::QName; + +TEST(QNameTest, TestTrivial) { + QName name("test"); + EXPECT_EQ(name.LocalPart(), "test"); + EXPECT_EQ(name.Namespace(), ""); +} + +TEST(QNameTest, TestSplit) { + QName name("a:test"); + EXPECT_EQ(name.LocalPart(), "test"); + EXPECT_EQ(name.Namespace(), "a"); + QName name2("a-very:long:namespace:test-this"); + EXPECT_EQ(name2.LocalPart(), "test-this"); + EXPECT_EQ(name2.Namespace(), "a-very:long:namespace"); +} + +TEST(QNameTest, TestMerge) { + QName name("a", "test"); + EXPECT_EQ(name.LocalPart(), "test"); + EXPECT_EQ(name.Namespace(), "a"); + EXPECT_EQ(name.Merged(), "a:test"); + QName name2("a-very:long:namespace", "test-this"); + EXPECT_EQ(name2.LocalPart(), "test-this"); + EXPECT_EQ(name2.Namespace(), "a-very:long:namespace"); + EXPECT_EQ(name2.Merged(), "a-very:long:namespace:test-this"); +} + +TEST(QNameTest, TestAssignment) { + QName name("a", "test"); + // copy constructor + QName namecopy(name); + EXPECT_EQ(namecopy.LocalPart(), "test"); + EXPECT_EQ(namecopy.Namespace(), "a"); + QName nameassigned(""); + nameassigned = name; + EXPECT_EQ(nameassigned.LocalPart(), "test"); + EXPECT_EQ(nameassigned.Namespace(), "a"); +} + +TEST(QNameTest, TestConstAssignment) { + StaticQName name = { "a", "test" }; + QName namecopy(name); + EXPECT_EQ(namecopy.LocalPart(), "test"); + EXPECT_EQ(namecopy.Namespace(), "a"); + QName nameassigned(""); + nameassigned = name; + EXPECT_EQ(nameassigned.LocalPart(), "test"); + EXPECT_EQ(nameassigned.Namespace(), "a"); +} + +TEST(QNameTest, TestEquality) { + QName name("a-very:long:namespace:test-this"); + QName name2("a-very:long:namespace", "test-this"); + QName name3("a-very:long:namespaxe", "test-this"); + EXPECT_TRUE(name == name2); + EXPECT_FALSE(name == name3); +} + +TEST(QNameTest, TestCompare) { + QName name("a"); + QName name2("nsa", "a"); + QName name3("nsa", "b"); + QName name4("nsb", "b"); + + EXPECT_TRUE(name < name2); + EXPECT_FALSE(name2 < name); + + EXPECT_FALSE(name2 < name2); + + EXPECT_TRUE(name2 < name3); + EXPECT_FALSE(name3 < name2); + + EXPECT_TRUE(name3 < name4); + EXPECT_FALSE(name4 < name3); +} + +TEST(QNameTest, TestStaticQName) { + const StaticQName const_name1 = { "namespace", "local-name1" }; + const StaticQName const_name2 = { "namespace", "local-name2" }; + const QName name("namespace", "local-name1"); + const QName name1 = const_name1; + const QName name2 = const_name2; + + EXPECT_TRUE(name == const_name1); + EXPECT_TRUE(const_name1 == name); + EXPECT_FALSE(name != const_name1); + EXPECT_FALSE(const_name1 != name); + + EXPECT_TRUE(name == name1); + EXPECT_TRUE(name1 == name); + EXPECT_FALSE(name != name1); + EXPECT_FALSE(name1 != name); + + EXPECT_FALSE(name == name2); + EXPECT_FALSE(name2 == name); + EXPECT_TRUE(name != name2); + EXPECT_TRUE(name2 != name); +} diff --git a/talk/xmllite/xmlbuilder.cc b/talk/xmllite/xmlbuilder.cc new file mode 100644 index 000000000..486b6d54e --- /dev/null +++ b/talk/xmllite/xmlbuilder.cc @@ -0,0 +1,147 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmllite/xmlbuilder.h" + +#include +#include +#include "talk/base/common.h" +#include "talk/xmllite/xmlconstants.h" +#include "talk/xmllite/xmlelement.h" + +namespace buzz { + +XmlBuilder::XmlBuilder() : + pelCurrent_(NULL), + pelRoot_(NULL), + pvParents_(new std::vector()) { +} + +void +XmlBuilder::Reset() { + pelRoot_.reset(); + pelCurrent_ = NULL; + pvParents_->clear(); +} + +XmlElement * +XmlBuilder::BuildElement(XmlParseContext * pctx, + const char * name, const char ** atts) { + QName tagName(pctx->ResolveQName(name, false)); + if (tagName.IsEmpty()) + return NULL; + + XmlElement * pelNew = new XmlElement(tagName); + + if (!*atts) + return pelNew; + + std::set seenNonlocalAtts; + + while (*atts) { + QName attName(pctx->ResolveQName(*atts, true)); + if (attName.IsEmpty()) { + delete pelNew; + return NULL; + } + + // verify that namespaced names are unique + if (!attName.Namespace().empty()) { + if (seenNonlocalAtts.count(attName)) { + delete pelNew; + return NULL; + } + seenNonlocalAtts.insert(attName); + } + + pelNew->AddAttr(attName, std::string(*(atts + 1))); + atts += 2; + } + + return pelNew; +} + +void +XmlBuilder::StartElement(XmlParseContext * pctx, + const char * name, const char ** atts) { + XmlElement * pelNew = BuildElement(pctx, name, atts); + if (pelNew == NULL) { + pctx->RaiseError(XML_ERROR_SYNTAX); + return; + } + + if (!pelCurrent_) { + pelCurrent_ = pelNew; + pelRoot_.reset(pelNew); + pvParents_->push_back(NULL); + } else { + pelCurrent_->AddElement(pelNew); + pvParents_->push_back(pelCurrent_); + pelCurrent_ = pelNew; + } +} + +void +XmlBuilder::EndElement(XmlParseContext * pctx, const char * name) { + UNUSED(pctx); + UNUSED(name); + pelCurrent_ = pvParents_->back(); + pvParents_->pop_back(); +} + +void +XmlBuilder::CharacterData(XmlParseContext * pctx, + const char * text, int len) { + UNUSED(pctx); + if (pelCurrent_) { + pelCurrent_->AddParsedText(text, len); + } +} + +void +XmlBuilder::Error(XmlParseContext * pctx, XML_Error err) { + UNUSED(pctx); + UNUSED(err); + pelRoot_.reset(NULL); + pelCurrent_ = NULL; + pvParents_->clear(); +} + +XmlElement * +XmlBuilder::CreateElement() { + return pelRoot_.release(); +} + +XmlElement * +XmlBuilder::BuiltElement() { + return pelRoot_.get(); +} + +XmlBuilder::~XmlBuilder() { +} + +} // namespace buzz diff --git a/talk/xmllite/xmlbuilder.h b/talk/xmllite/xmlbuilder.h new file mode 100644 index 000000000..984eee204 --- /dev/null +++ b/talk/xmllite/xmlbuilder.h @@ -0,0 +1,78 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _xmlbuilder_h_ +#define _xmlbuilder_h_ + +#include +#include +#include "talk/base/scoped_ptr.h" +#include "talk/xmllite/xmlparser.h" + +#ifdef EXPAT_RELATIVE_PATH +#include "expat.h" +#else +#include "third_party/expat/v2_0_1/Source/lib/expat.h" +#endif // EXPAT_RELATIVE_PATH + +namespace buzz { + +class XmlElement; +class XmlParseContext; + + +class XmlBuilder : public XmlParseHandler { +public: + XmlBuilder(); + + static XmlElement * BuildElement(XmlParseContext * pctx, + const char * name, const char ** atts); + virtual void StartElement(XmlParseContext * pctx, + const char * name, const char ** atts); + virtual void EndElement(XmlParseContext * pctx, const char * name); + virtual void CharacterData(XmlParseContext * pctx, + const char * text, int len); + virtual void Error(XmlParseContext * pctx, XML_Error); + virtual ~XmlBuilder(); + + void Reset(); + + // Take ownership of the built element; second call returns NULL + XmlElement * CreateElement(); + + // Peek at the built element without taking ownership + XmlElement * BuiltElement(); + +private: + XmlElement * pelCurrent_; + talk_base::scoped_ptr pelRoot_; + talk_base::scoped_ptr > pvParents_; +}; + +} + +#endif diff --git a/talk/xmllite/xmlbuilder_unittest.cc b/talk/xmllite/xmlbuilder_unittest.cc new file mode 100644 index 000000000..9302276d6 --- /dev/null +++ b/talk/xmllite/xmlbuilder_unittest.cc @@ -0,0 +1,194 @@ +/* + * libjingle + * Copyright 2004, 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 +#include +#include +#include "talk/base/common.h" +#include "talk/base/gunit.h" +#include "talk/xmllite/xmlbuilder.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmllite/xmlparser.h" + +using buzz::XmlBuilder; +using buzz::XmlElement; +using buzz::XmlParser; + +TEST(XmlBuilderTest, TestTrivial) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, ""); + EXPECT_EQ("", builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestAttributes1) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, ""); + EXPECT_EQ("", builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestAttributes2) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, ""); + EXPECT_EQ("", + builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestNesting1) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, + ""); + EXPECT_EQ("", + builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestNesting2) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, + "" + "" + ""); + EXPECT_EQ("" + "" + "", builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestQuoting1) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, ""); + EXPECT_EQ("", builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestQuoting2) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, ""); + EXPECT_EQ("", + builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestQuoting3) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, ""); + EXPECT_EQ("", + builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestQuoting4) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, ""); + EXPECT_EQ("", + builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestQuoting5) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, + ""); + EXPECT_EQ("", + builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestText1) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, ">"); + EXPECT_EQ(">", builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestText2) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, "<>&""); + EXPECT_EQ("<>&\"", + builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestText3) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, "so <important>"); + EXPECT_EQ("so <important>", + builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestText4) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, "<important>, yes"); + EXPECT_EQ("<important>, yes", + builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestText5) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, + "importance &<important>&"); + EXPECT_EQ("importance &<important>&", + builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestNamespace1) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, ""); + EXPECT_EQ("", builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestNamespace2) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, ""); + EXPECT_EQ("", + builder.BuiltElement()->Str()); +} + +TEST(XmlBuilderTest, TestNamespace3) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, ""); + EXPECT_TRUE(NULL == builder.BuiltElement()); +} + +TEST(XmlBuilderTest, TestNamespace4) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, ""); + EXPECT_TRUE(NULL == builder.BuiltElement()); +} + +TEST(XmlBuilderTest, TestAttrCollision1) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, ""); + EXPECT_TRUE(NULL == builder.BuiltElement()); +} + +TEST(XmlBuilderTest, TestAttrCollision2) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, + ""); + EXPECT_TRUE(NULL == builder.BuiltElement()); +} + +TEST(XmlBuilderTest, TestAttrCollision3) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, + "" + ""); + EXPECT_TRUE(NULL == builder.BuiltElement()); +} + diff --git a/talk/xmllite/xmlconstants.cc b/talk/xmllite/xmlconstants.cc new file mode 100644 index 000000000..f94d77910 --- /dev/null +++ b/talk/xmllite/xmlconstants.cc @@ -0,0 +1,42 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmllite/xmlconstants.h" + +namespace buzz { + +const char STR_EMPTY[] = ""; +const char NS_XML[] = "http://www.w3.org/XML/1998/namespace"; +const char NS_XMLNS[] = "http://www.w3.org/2000/xmlns/"; +const char STR_XMLNS[] = "xmlns"; +const char STR_XML[] = "xml"; +const char STR_VERSION[] = "version"; +const char STR_ENCODING[] = "encoding"; + +const StaticQName QN_XMLNS = { STR_EMPTY, STR_XMLNS }; + +} // namespace buzz diff --git a/talk/xmllite/xmlconstants.h b/talk/xmllite/xmlconstants.h new file mode 100644 index 000000000..3e5da9816 --- /dev/null +++ b/talk/xmllite/xmlconstants.h @@ -0,0 +1,47 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMLLITE_XMLCONSTANTS_H_ +#define TALK_XMLLITE_XMLCONSTANTS_H_ + +#include "talk/xmllite/qname.h" + +namespace buzz { + +extern const char STR_EMPTY[]; +extern const char NS_XML[]; +extern const char NS_XMLNS[]; +extern const char STR_XMLNS[]; +extern const char STR_XML[]; +extern const char STR_VERSION[]; +extern const char STR_ENCODING[]; + +extern const StaticQName QN_XMLNS; + +} // namespace buzz + +#endif // TALK_XMLLITE_XMLCONSTANTS_H_ diff --git a/talk/xmllite/xmlelement.cc b/talk/xmllite/xmlelement.cc new file mode 100644 index 000000000..176ce5ce3 --- /dev/null +++ b/talk/xmllite/xmlelement.cc @@ -0,0 +1,513 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmllite/xmlelement.h" + +#include +#include +#include +#include + +#include "talk/base/common.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlparser.h" +#include "talk/xmllite/xmlbuilder.h" +#include "talk/xmllite/xmlprinter.h" +#include "talk/xmllite/xmlconstants.h" + +namespace buzz { + +XmlChild::~XmlChild() { +} + +bool XmlText::IsTextImpl() const { + return true; +} + +XmlElement* XmlText::AsElementImpl() const { + return NULL; +} + +XmlText* XmlText::AsTextImpl() const { + return const_cast(this); +} + +void XmlText::SetText(const std::string& text) { + text_ = text; +} + +void XmlText::AddParsedText(const char* buf, int len) { + text_.append(buf, len); +} + +void XmlText::AddText(const std::string& text) { + text_ += text; +} + +XmlText::~XmlText() { +} + +XmlElement::XmlElement(const QName& name) : + name_(name), + first_attr_(NULL), + last_attr_(NULL), + first_child_(NULL), + last_child_(NULL), + cdata_(false) { +} + +XmlElement::XmlElement(const XmlElement& elt) : + XmlChild(), + name_(elt.name_), + first_attr_(NULL), + last_attr_(NULL), + first_child_(NULL), + last_child_(NULL), + cdata_(false) { + + // copy attributes + XmlAttr* attr; + XmlAttr ** plast_attr = &first_attr_; + XmlAttr* newAttr = NULL; + for (attr = elt.first_attr_; attr; attr = attr->NextAttr()) { + newAttr = new XmlAttr(*attr); + *plast_attr = newAttr; + plast_attr = &(newAttr->next_attr_); + } + last_attr_ = newAttr; + + // copy children + XmlChild* pChild; + XmlChild ** ppLast = &first_child_; + XmlChild* newChild = NULL; + + for (pChild = elt.first_child_; pChild; pChild = pChild->NextChild()) { + if (pChild->IsText()) { + newChild = new XmlText(*(pChild->AsText())); + } else { + newChild = new XmlElement(*(pChild->AsElement())); + } + *ppLast = newChild; + ppLast = &(newChild->next_child_); + } + last_child_ = newChild; + + cdata_ = elt.cdata_; +} + +XmlElement::XmlElement(const QName& name, bool useDefaultNs) : + name_(name), + first_attr_(useDefaultNs ? new XmlAttr(QN_XMLNS, name.Namespace()) : NULL), + last_attr_(first_attr_), + first_child_(NULL), + last_child_(NULL), + cdata_(false) { +} + +bool XmlElement::IsTextImpl() const { + return false; +} + +XmlElement* XmlElement::AsElementImpl() const { + return const_cast(this); +} + +XmlText* XmlElement::AsTextImpl() const { + return NULL; +} + +const std::string XmlElement::BodyText() const { + if (first_child_ && first_child_->IsText() && last_child_ == first_child_) { + return first_child_->AsText()->Text(); + } + + return std::string(); +} + +void XmlElement::SetBodyText(const std::string& text) { + if (text.empty()) { + ClearChildren(); + } else if (first_child_ == NULL) { + AddText(text); + } else if (first_child_->IsText() && last_child_ == first_child_) { + first_child_->AsText()->SetText(text); + } else { + ClearChildren(); + AddText(text); + } +} + +const QName XmlElement::FirstElementName() const { + const XmlElement* element = FirstElement(); + if (element == NULL) + return QName(); + return element->Name(); +} + +XmlAttr* XmlElement::FirstAttr() { + return first_attr_; +} + +const std::string XmlElement::Attr(const StaticQName& name) const { + XmlAttr* attr; + for (attr = first_attr_; attr; attr = attr->next_attr_) { + if (attr->name_ == name) + return attr->value_; + } + return std::string(); +} + +const std::string XmlElement::Attr(const QName& name) const { + XmlAttr* attr; + for (attr = first_attr_; attr; attr = attr->next_attr_) { + if (attr->name_ == name) + return attr->value_; + } + return std::string(); +} + +bool XmlElement::HasAttr(const StaticQName& name) const { + XmlAttr* attr; + for (attr = first_attr_; attr; attr = attr->next_attr_) { + if (attr->name_ == name) + return true; + } + return false; +} + +bool XmlElement::HasAttr(const QName& name) const { + XmlAttr* attr; + for (attr = first_attr_; attr; attr = attr->next_attr_) { + if (attr->name_ == name) + return true; + } + return false; +} + +void XmlElement::SetAttr(const QName& name, const std::string& value) { + XmlAttr* attr; + for (attr = first_attr_; attr; attr = attr->next_attr_) { + if (attr->name_ == name) + break; + } + if (!attr) { + attr = new XmlAttr(name, value); + if (last_attr_) + last_attr_->next_attr_ = attr; + else + first_attr_ = attr; + last_attr_ = attr; + return; + } + attr->value_ = value; +} + +void XmlElement::ClearAttr(const QName& name) { + XmlAttr* attr; + XmlAttr* last_attr = NULL; + for (attr = first_attr_; attr; attr = attr->next_attr_) { + if (attr->name_ == name) + break; + last_attr = attr; + } + if (!attr) + return; + if (!last_attr) + first_attr_ = attr->next_attr_; + else + last_attr->next_attr_ = attr->next_attr_; + if (last_attr_ == attr) + last_attr_ = last_attr; + delete attr; +} + +XmlChild* XmlElement::FirstChild() { + return first_child_; +} + +XmlElement* XmlElement::FirstElement() { + XmlChild* pChild; + for (pChild = first_child_; pChild; pChild = pChild->next_child_) { + if (!pChild->IsText()) + return pChild->AsElement(); + } + return NULL; +} + +XmlElement* XmlElement::NextElement() { + XmlChild* pChild; + for (pChild = next_child_; pChild; pChild = pChild->next_child_) { + if (!pChild->IsText()) + return pChild->AsElement(); + } + return NULL; +} + +XmlElement* XmlElement::FirstWithNamespace(const std::string& ns) { + XmlChild* pChild; + for (pChild = first_child_; pChild; pChild = pChild->next_child_) { + if (!pChild->IsText() && pChild->AsElement()->Name().Namespace() == ns) + return pChild->AsElement(); + } + return NULL; +} + +XmlElement * +XmlElement::NextWithNamespace(const std::string& ns) { + XmlChild* pChild; + for (pChild = next_child_; pChild; pChild = pChild->next_child_) { + if (!pChild->IsText() && pChild->AsElement()->Name().Namespace() == ns) + return pChild->AsElement(); + } + return NULL; +} + +XmlElement * +XmlElement::FirstNamed(const QName& name) { + XmlChild* pChild; + for (pChild = first_child_; pChild; pChild = pChild->next_child_) { + if (!pChild->IsText() && pChild->AsElement()->Name() == name) + return pChild->AsElement(); + } + return NULL; +} + +XmlElement * +XmlElement::FirstNamed(const StaticQName& name) { + XmlChild* pChild; + for (pChild = first_child_; pChild; pChild = pChild->next_child_) { + if (!pChild->IsText() && pChild->AsElement()->Name() == name) + return pChild->AsElement(); + } + return NULL; +} + +XmlElement * +XmlElement::NextNamed(const QName& name) { + XmlChild* pChild; + for (pChild = next_child_; pChild; pChild = pChild->next_child_) { + if (!pChild->IsText() && pChild->AsElement()->Name() == name) + return pChild->AsElement(); + } + return NULL; +} + +XmlElement * +XmlElement::NextNamed(const StaticQName& name) { + XmlChild* pChild; + for (pChild = next_child_; pChild; pChild = pChild->next_child_) { + if (!pChild->IsText() && pChild->AsElement()->Name() == name) + return pChild->AsElement(); + } + return NULL; +} + +XmlElement* XmlElement::FindOrAddNamedChild(const QName& name) { + XmlElement* child = FirstNamed(name); + if (!child) { + child = new XmlElement(name); + AddElement(child); + } + + return child; +} + +const std::string XmlElement::TextNamed(const QName& name) const { + XmlChild* pChild; + for (pChild = first_child_; pChild; pChild = pChild->next_child_) { + if (!pChild->IsText() && pChild->AsElement()->Name() == name) + return pChild->AsElement()->BodyText(); + } + return std::string(); +} + +void XmlElement::InsertChildAfter(XmlChild* predecessor, XmlChild* next) { + if (predecessor == NULL) { + next->next_child_ = first_child_; + first_child_ = next; + } + else { + next->next_child_ = predecessor->next_child_; + predecessor->next_child_ = next; + } +} + +void XmlElement::RemoveChildAfter(XmlChild* predecessor) { + XmlChild* next; + + if (predecessor == NULL) { + next = first_child_; + first_child_ = next->next_child_; + } + else { + next = predecessor->next_child_; + predecessor->next_child_ = next->next_child_; + } + + if (last_child_ == next) + last_child_ = predecessor; + + delete next; +} + +void XmlElement::AddAttr(const QName& name, const std::string& value) { + ASSERT(!HasAttr(name)); + + XmlAttr ** pprev = last_attr_ ? &(last_attr_->next_attr_) : &first_attr_; + last_attr_ = (*pprev = new XmlAttr(name, value)); +} + +void XmlElement::AddAttr(const QName& name, const std::string& value, + int depth) { + XmlElement* element = this; + while (depth--) { + element = element->last_child_->AsElement(); + } + element->AddAttr(name, value); +} + +void XmlElement::AddParsedText(const char* cstr, int len) { + if (len == 0) + return; + + if (last_child_ && last_child_->IsText()) { + last_child_->AsText()->AddParsedText(cstr, len); + return; + } + XmlChild ** pprev = last_child_ ? &(last_child_->next_child_) : &first_child_; + last_child_ = *pprev = new XmlText(cstr, len); +} + +void XmlElement::AddCDATAText(const char* buf, int len) { + cdata_ = true; + AddParsedText(buf, len); +} + +void XmlElement::AddText(const std::string& text) { + if (text == STR_EMPTY) + return; + + if (last_child_ && last_child_->IsText()) { + last_child_->AsText()->AddText(text); + return; + } + XmlChild ** pprev = last_child_ ? &(last_child_->next_child_) : &first_child_; + last_child_ = *pprev = new XmlText(text); +} + +void XmlElement::AddText(const std::string& text, int depth) { + // note: the first syntax is ambigious for msvc 6 + // XmlElement* pel(this); + XmlElement* element = this; + while (depth--) { + element = element->last_child_->AsElement(); + } + element->AddText(text); +} + +void XmlElement::AddElement(XmlElement *child) { + if (child == NULL) + return; + + XmlChild ** pprev = last_child_ ? &(last_child_->next_child_) : &first_child_; + *pprev = child; + last_child_ = child; + child->next_child_ = NULL; +} + +void XmlElement::AddElement(XmlElement *child, int depth) { + XmlElement* element = this; + while (depth--) { + element = element->last_child_->AsElement(); + } + element->AddElement(child); +} + +void XmlElement::ClearNamedChildren(const QName& name) { + XmlChild* prev_child = NULL; + XmlChild* next_child; + XmlChild* child; + for (child = FirstChild(); child; child = next_child) { + next_child = child->NextChild(); + if (!child->IsText() && child->AsElement()->Name() == name) + { + RemoveChildAfter(prev_child); + continue; + } + prev_child = child; + } +} + +void XmlElement::ClearAttributes() { + XmlAttr* attr; + for (attr = first_attr_; attr; ) { + XmlAttr* to_delete = attr; + attr = attr->next_attr_; + delete to_delete; + } + first_attr_ = last_attr_ = NULL; +} + +void XmlElement::ClearChildren() { + XmlChild* pchild; + for (pchild = first_child_; pchild; ) { + XmlChild* to_delete = pchild; + pchild = pchild->next_child_; + delete to_delete; + } + first_child_ = last_child_ = NULL; +} + +std::string XmlElement::Str() const { + std::stringstream ss; + XmlPrinter::PrintXml(&ss, this); + return ss.str(); +} + +XmlElement* XmlElement::ForStr(const std::string& str) { + XmlBuilder builder; + XmlParser::ParseXml(&builder, str); + return builder.CreateElement(); +} + +XmlElement::~XmlElement() { + XmlAttr* attr; + for (attr = first_attr_; attr; ) { + XmlAttr* to_delete = attr; + attr = attr->next_attr_; + delete to_delete; + } + + XmlChild* pchild; + for (pchild = first_child_; pchild; ) { + XmlChild* to_delete = pchild; + pchild = pchild->next_child_; + delete to_delete; + } +} + +} // namespace buzz diff --git a/talk/xmllite/xmlelement.h b/talk/xmllite/xmlelement.h new file mode 100644 index 000000000..ffdc333bb --- /dev/null +++ b/talk/xmllite/xmlelement.h @@ -0,0 +1,251 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMLLITE_XMLELEMENT_H_ +#define TALK_XMLLITE_XMLELEMENT_H_ + +#include +#include + +#include "talk/base/scoped_ptr.h" +#include "talk/xmllite/qname.h" + +namespace buzz { + +class XmlChild; +class XmlText; +class XmlElement; +class XmlAttr; + +class XmlChild { + public: + XmlChild* NextChild() { return next_child_; } + const XmlChild* NextChild() const { return next_child_; } + + bool IsText() const { return IsTextImpl(); } + + XmlElement* AsElement() { return AsElementImpl(); } + const XmlElement* AsElement() const { return AsElementImpl(); } + + XmlText* AsText() { return AsTextImpl(); } + const XmlText* AsText() const { return AsTextImpl(); } + + + protected: + XmlChild() : + next_child_(NULL) { + } + + virtual bool IsTextImpl() const = 0; + virtual XmlElement* AsElementImpl() const = 0; + virtual XmlText* AsTextImpl() const = 0; + + + virtual ~XmlChild(); + + private: + friend class XmlElement; + + XmlChild(const XmlChild& noimpl); + + XmlChild* next_child_; +}; + +class XmlText : public XmlChild { + public: + explicit XmlText(const std::string& text) : + XmlChild(), + text_(text) { + } + explicit XmlText(const XmlText& t) : + XmlChild(), + text_(t.text_) { + } + explicit XmlText(const char* cstr, size_t len) : + XmlChild(), + text_(cstr, len) { + } + virtual ~XmlText(); + + const std::string& Text() const { return text_; } + void SetText(const std::string& text); + void AddParsedText(const char* buf, int len); + void AddText(const std::string& text); + + protected: + virtual bool IsTextImpl() const; + virtual XmlElement* AsElementImpl() const; + virtual XmlText* AsTextImpl() const; + + private: + std::string text_; +}; + +class XmlAttr { + public: + XmlAttr* NextAttr() const { return next_attr_; } + const QName& Name() const { return name_; } + const std::string& Value() const { return value_; } + + private: + friend class XmlElement; + + explicit XmlAttr(const QName& name, const std::string& value) : + next_attr_(NULL), + name_(name), + value_(value) { + } + explicit XmlAttr(const XmlAttr& att) : + next_attr_(NULL), + name_(att.name_), + value_(att.value_) { + } + + XmlAttr* next_attr_; + QName name_; + std::string value_; +}; + +class XmlElement : public XmlChild { + public: + explicit XmlElement(const QName& name); + explicit XmlElement(const QName& name, bool useDefaultNs); + explicit XmlElement(const XmlElement& elt); + + virtual ~XmlElement(); + + const QName& Name() const { return name_; } + void SetName(const QName& name) { name_ = name; } + + const std::string BodyText() const; + void SetBodyText(const std::string& text); + + const QName FirstElementName() const; + + XmlAttr* FirstAttr(); + const XmlAttr* FirstAttr() const + { return const_cast(this)->FirstAttr(); } + + // Attr will return an empty string if the attribute isn't there: + // use HasAttr to test presence of an attribute. + const std::string Attr(const StaticQName& name) const; + const std::string Attr(const QName& name) const; + bool HasAttr(const StaticQName& name) const; + bool HasAttr(const QName& name) const; + void SetAttr(const QName& name, const std::string& value); + void ClearAttr(const QName& name); + + XmlChild* FirstChild(); + const XmlChild* FirstChild() const { + return const_cast(this)->FirstChild(); + } + + XmlElement* FirstElement(); + const XmlElement* FirstElement() const { + return const_cast(this)->FirstElement(); + } + + XmlElement* NextElement(); + const XmlElement* NextElement() const { + return const_cast(this)->NextElement(); + } + + XmlElement* FirstWithNamespace(const std::string& ns); + const XmlElement* FirstWithNamespace(const std::string& ns) const { + return const_cast(this)->FirstWithNamespace(ns); + } + + XmlElement* NextWithNamespace(const std::string& ns); + const XmlElement* NextWithNamespace(const std::string& ns) const { + return const_cast(this)->NextWithNamespace(ns); + } + + XmlElement* FirstNamed(const StaticQName& name); + const XmlElement* FirstNamed(const StaticQName& name) const { + return const_cast(this)->FirstNamed(name); + } + + XmlElement* FirstNamed(const QName& name); + const XmlElement* FirstNamed(const QName& name) const { + return const_cast(this)->FirstNamed(name); + } + + XmlElement* NextNamed(const StaticQName& name); + const XmlElement* NextNamed(const StaticQName& name) const { + return const_cast(this)->NextNamed(name); + } + + XmlElement* NextNamed(const QName& name); + const XmlElement* NextNamed(const QName& name) const { + return const_cast(this)->NextNamed(name); + } + + // Finds the first element named 'name'. If that element can't be found then + // adds one and returns it. + XmlElement* FindOrAddNamedChild(const QName& name); + + const std::string TextNamed(const QName& name) const; + + void InsertChildAfter(XmlChild* predecessor, XmlChild* new_child); + void RemoveChildAfter(XmlChild* predecessor); + + void AddParsedText(const char* buf, int len); + // Note: CDATA is not supported by XMPP, therefore using this function will + // generate non-XMPP compatible XML. + void AddCDATAText(const char* buf, int len); + void AddText(const std::string& text); + void AddText(const std::string& text, int depth); + void AddElement(XmlElement* child); + void AddElement(XmlElement* child, int depth); + void AddAttr(const QName& name, const std::string& value); + void AddAttr(const QName& name, const std::string& value, int depth); + void ClearNamedChildren(const QName& name); + void ClearAttributes(); + void ClearChildren(); + + static XmlElement* ForStr(const std::string& str); + std::string Str() const; + + bool IsCDATA() const { return cdata_; } + + protected: + virtual bool IsTextImpl() const; + virtual XmlElement* AsElementImpl() const; + virtual XmlText* AsTextImpl() const; + + private: + QName name_; + XmlAttr* first_attr_; + XmlAttr* last_attr_; + XmlChild* first_child_; + XmlChild* last_child_; + bool cdata_; +}; + +} // namespace buzz + +#endif // TALK_XMLLITE_XMLELEMENT_H_ diff --git a/talk/xmllite/xmlelement_unittest.cc b/talk/xmllite/xmlelement_unittest.cc new file mode 100644 index 000000000..6d488fa75 --- /dev/null +++ b/talk/xmllite/xmlelement_unittest.cc @@ -0,0 +1,271 @@ +/* + * libjingle + * Copyright 2004, 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 +#include +#include +#include "talk/base/common.h" +#include "talk/base/gunit.h" +#include "talk/base/thread.h" +#include "talk/xmllite/xmlelement.h" + +using buzz::QName; +using buzz::XmlAttr; +using buzz::XmlChild; +using buzz::XmlElement; + +std::ostream& operator<<(std::ostream& os, const QName& name) { + os << name.Namespace() << ":" << name.LocalPart(); + return os; +} + +TEST(XmlElementTest, TestConstructors) { + XmlElement elt(QName("google:test", "first")); + EXPECT_EQ("", elt.Str()); + + XmlElement elt2(QName("google:test", "first"), true); + EXPECT_EQ("", elt2.Str()); +} + +TEST(XmlElementTest, TestAdd) { + XmlElement elt(QName("google:test", "root"), true); + elt.AddElement(new XmlElement(QName("google:test", "first"))); + elt.AddElement(new XmlElement(QName("google:test", "nested")), 1); + elt.AddText("nested-value", 2); + elt.AddText("between-", 1); + elt.AddText("value", 1); + elt.AddElement(new XmlElement(QName("google:test", "nested2")), 1); + elt.AddElement(new XmlElement(QName("google:test", "second"))); + elt.AddText("init-value", 1); + elt.AddElement(new XmlElement(QName("google:test", "nested3")), 1); + elt.AddText("trailing-value", 1); + + // make sure it looks ok overall + EXPECT_EQ("" + "nested-valuebetween-value" + "init-valuetrailing-value", + elt.Str()); + + // make sure text was concatenated + XmlChild * pchild = + elt.FirstChild()->AsElement()->FirstChild()->NextChild(); + EXPECT_TRUE(pchild->IsText()); + EXPECT_EQ("between-value", pchild->AsText()->Text()); +} + +TEST(XmlElementTest, TestAttrs) { + XmlElement elt(QName("", "root")); + elt.SetAttr(QName("", "a"), "avalue"); + EXPECT_EQ("", elt.Str()); + + elt.SetAttr(QName("", "b"), "bvalue"); + EXPECT_EQ("", elt.Str()); + + elt.SetAttr(QName("", "a"), "avalue2"); + EXPECT_EQ("", elt.Str()); + + elt.SetAttr(QName("", "b"), "bvalue2"); + EXPECT_EQ("", elt.Str()); + + elt.SetAttr(QName("", "c"), "cvalue"); + EXPECT_EQ("", elt.Str()); + + XmlAttr * patt = elt.FirstAttr(); + EXPECT_EQ(QName("", "a"), patt->Name()); + EXPECT_EQ("avalue2", patt->Value()); + + patt = patt->NextAttr(); + EXPECT_EQ(QName("", "b"), patt->Name()); + EXPECT_EQ("bvalue2", patt->Value()); + + patt = patt->NextAttr(); + EXPECT_EQ(QName("", "c"), patt->Name()); + EXPECT_EQ("cvalue", patt->Value()); + + patt = patt->NextAttr(); + EXPECT_TRUE(NULL == patt); + + EXPECT_TRUE(elt.HasAttr(QName("", "a"))); + EXPECT_TRUE(elt.HasAttr(QName("", "b"))); + EXPECT_TRUE(elt.HasAttr(QName("", "c"))); + EXPECT_FALSE(elt.HasAttr(QName("", "d"))); + + elt.SetAttr(QName("", "d"), "dvalue"); + EXPECT_EQ("", + elt.Str()); + EXPECT_TRUE(elt.HasAttr(QName("", "d"))); + + elt.ClearAttr(QName("", "z")); // not found, no effect + EXPECT_EQ("", + elt.Str()); + + elt.ClearAttr(QName("", "b")); + EXPECT_EQ("", elt.Str()); + + elt.ClearAttr(QName("", "a")); + EXPECT_EQ("", elt.Str()); + + elt.ClearAttr(QName("", "d")); + EXPECT_EQ("", elt.Str()); + + elt.ClearAttr(QName("", "c")); + EXPECT_EQ("", elt.Str()); +} + +TEST(XmlElementTest, TestBodyText) { + XmlElement elt(QName("", "root")); + EXPECT_EQ("", elt.BodyText()); + + elt.AddText("body value text"); + + EXPECT_EQ("body value text", elt.BodyText()); + + elt.ClearChildren(); + elt.AddText("more value "); + elt.AddText("text"); + + EXPECT_EQ("more value text", elt.BodyText()); + + elt.ClearChildren(); + elt.AddText("decoy"); + elt.AddElement(new XmlElement(QName("", "dummy"))); + EXPECT_EQ("", elt.BodyText()); + + elt.SetBodyText("replacement"); + EXPECT_EQ("replacement", elt.BodyText()); + + elt.SetBodyText(""); + EXPECT_TRUE(NULL == elt.FirstChild()); + + elt.SetBodyText("goodbye"); + EXPECT_EQ("goodbye", elt.FirstChild()->AsText()->Text()); + EXPECT_EQ("goodbye", elt.BodyText()); +} + +TEST(XmlElementTest, TestCopyConstructor) { + XmlElement * element = XmlElement::ForStr( + "This is a " + "little little test"); + + XmlElement * pelCopy = new XmlElement(*element); + EXPECT_EQ("This is a " + "little little test", pelCopy->Str()); + delete pelCopy; + + pelCopy = new XmlElement(*(element->FirstChild()->NextChild()->AsElement())); + EXPECT_EQ("" + "little little", pelCopy->Str()); + + XmlAttr * patt = pelCopy->FirstAttr(); + EXPECT_EQ(QName("", "a"), patt->Name()); + EXPECT_EQ("avalue", patt->Value()); + + patt = patt->NextAttr(); + EXPECT_EQ(QName("", "b"), patt->Name()); + EXPECT_EQ("bvalue", patt->Value()); + + patt = patt->NextAttr(); + EXPECT_TRUE(NULL == patt); + delete pelCopy; + delete element; +} + +TEST(XmlElementTest, TestNameSearch) { + XmlElement * element = XmlElement::ForStr( + "" + "George" + "X." + "some text" + "Harrison" + "John" + "Y." + "Lennon" + ""); + EXPECT_TRUE(NULL == + element->FirstNamed(QName("", "firstname"))); + EXPECT_EQ(element->FirstChild(), + element->FirstNamed(QName("test-foo", "firstname"))); + EXPECT_EQ(element->FirstChild()->NextChild(), + element->FirstNamed(QName("test-foo", "middlename"))); + EXPECT_EQ(element->FirstElement()->NextElement(), + element->FirstNamed(QName("test-foo", "middlename"))); + EXPECT_EQ("Harrison", + element->TextNamed(QName("test-foo", "lastname"))); + EXPECT_EQ(element->FirstElement()->NextElement()->NextElement(), + element->FirstNamed(QName("test-foo", "lastname"))); + EXPECT_EQ("John", element->FirstNamed(QName("test-foo", "firstname"))-> + NextNamed(QName("test-foo", "firstname"))->BodyText()); + EXPECT_EQ("Y.", element->FirstNamed(QName("test-foo", "middlename"))-> + NextNamed(QName("test-foo", "middlename"))->BodyText()); + EXPECT_EQ("Lennon", element->FirstNamed(QName("test-foo", "lastname"))-> + NextNamed(QName("test-foo", "lastname"))->BodyText()); + EXPECT_TRUE(NULL == element->FirstNamed(QName("test-foo", "firstname"))-> + NextNamed(QName("test-foo", "firstname"))-> + NextNamed(QName("test-foo", "firstname"))); + + delete element; +} + +class XmlElementCreatorThread : public talk_base::Thread { + public: + XmlElementCreatorThread(int count, buzz::QName qname) : + count_(count), qname_(qname) {} + + virtual void Run() { + std::vector elems; + for (int i = 0; i < count_; i++) { + elems.push_back(new XmlElement(qname_)); + } + for (int i = 0; i < count_; i++) { + delete elems[i]; + } + } + + private: + int count_; + buzz::QName qname_; +}; + +// If XmlElement creation and destruction isn't thread safe, +// this test should crash. +TEST(XmlElementTest, TestMultithread) { + int thread_count = 2; // Was 100, but that's too slow. + int elem_count = 100; // Was 100000, but that's too slow. + buzz::QName qname("foo", "bar"); + + std::vector threads; + for (int i = 0; i < thread_count; i++) { + threads.push_back( + new XmlElementCreatorThread(elem_count, qname)); + threads[i]->Start(); + } + + for (int i = 0; i < thread_count; i++) { + threads[i]->Stop(); + delete threads[i]; + } +} diff --git a/talk/xmllite/xmlnsstack.cc b/talk/xmllite/xmlnsstack.cc new file mode 100644 index 000000000..26e27f809 --- /dev/null +++ b/talk/xmllite/xmlnsstack.cc @@ -0,0 +1,195 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmllite/xmlnsstack.h" + +#include +#include +#include + +#include "talk/xmllite/xmlelement.h" +#include "talk/xmllite/xmlconstants.h" + +namespace buzz { + +XmlnsStack::XmlnsStack() : + pxmlnsStack_(new std::vector), + pxmlnsDepthStack_(new std::vector) { +} + +XmlnsStack::~XmlnsStack() {} + +void XmlnsStack::PushFrame() { + pxmlnsDepthStack_->push_back(pxmlnsStack_->size()); +} + +void XmlnsStack::PopFrame() { + size_t prev_size = pxmlnsDepthStack_->back(); + pxmlnsDepthStack_->pop_back(); + if (prev_size < pxmlnsStack_->size()) { + pxmlnsStack_->erase(pxmlnsStack_->begin() + prev_size, + pxmlnsStack_->end()); + } +} + +std::pair XmlnsStack::NsForPrefix( + const std::string& prefix) { + if (prefix.length() >= 3 && + (prefix[0] == 'x' || prefix[0] == 'X') && + (prefix[1] == 'm' || prefix[1] == 'M') && + (prefix[2] == 'l' || prefix[2] == 'L')) { + if (prefix == "xml") + return std::make_pair(NS_XML, true); + if (prefix == "xmlns") + return std::make_pair(NS_XMLNS, true); + // Other names with xml prefix are illegal. + return std::make_pair(STR_EMPTY, false); + } + + std::vector::iterator pos; + for (pos = pxmlnsStack_->end(); pos > pxmlnsStack_->begin(); ) { + pos -= 2; + if (*pos == prefix) + return std::make_pair(*(pos + 1), true); + } + + if (prefix == STR_EMPTY) + return std::make_pair(STR_EMPTY, true); // default namespace + + return std::make_pair(STR_EMPTY, false); // none found +} + +bool XmlnsStack::PrefixMatchesNs(const std::string& prefix, + const std::string& ns) { + const std::pair match = NsForPrefix(prefix); + return match.second && (match.first == ns); +} + +std::pair XmlnsStack::PrefixForNs(const std::string& ns, + bool isattr) { + if (ns == NS_XML) + return std::make_pair(std::string("xml"), true); + if (ns == NS_XMLNS) + return std::make_pair(std::string("xmlns"), true); + if (isattr ? ns == STR_EMPTY : PrefixMatchesNs(STR_EMPTY, ns)) + return std::make_pair(STR_EMPTY, true); + + std::vector::iterator pos; + for (pos = pxmlnsStack_->end(); pos > pxmlnsStack_->begin(); ) { + pos -= 2; + if (*(pos + 1) == ns && + (!isattr || !pos->empty()) && PrefixMatchesNs(*pos, ns)) + return std::make_pair(*pos, true); + } + + return std::make_pair(STR_EMPTY, false); // none found +} + +std::string XmlnsStack::FormatQName(const QName& name, bool isAttr) { + std::string prefix(PrefixForNs(name.Namespace(), isAttr).first); + if (prefix == STR_EMPTY) + return name.LocalPart(); + else + return prefix + ':' + name.LocalPart(); +} + +void XmlnsStack::AddXmlns(const std::string & prefix, const std::string & ns) { + pxmlnsStack_->push_back(prefix); + pxmlnsStack_->push_back(ns); +} + +void XmlnsStack::RemoveXmlns() { + pxmlnsStack_->pop_back(); + pxmlnsStack_->pop_back(); +} + +static bool IsAsciiLetter(char ch) { + return ((ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z')); +} + +static std::string AsciiLower(const std::string & s) { + std::string result(s); + size_t i; + for (i = 0; i < result.length(); i++) { + if (result[i] >= 'A' && result[i] <= 'Z') + result[i] += 'a' - 'A'; + } + return result; +} + +static std::string SuggestPrefix(const std::string & ns) { + size_t len = ns.length(); + size_t i = ns.find_last_of('.'); + if (i != std::string::npos && len - i <= 4 + 1) + len = i; // chop off ".html" or ".xsd" or ".?{0,4}" + size_t last = len; + while (last > 0) { + last -= 1; + if (IsAsciiLetter(ns[last])) { + size_t first = last; + last += 1; + while (first > 0) { + if (!IsAsciiLetter(ns[first - 1])) + break; + first -= 1; + } + if (last - first > 4) + last = first + 3; + std::string candidate(AsciiLower(ns.substr(first, last - first))); + if (candidate.find("xml") != 0) + return candidate; + break; + } + } + return "ns"; +} + +std::pair XmlnsStack::AddNewPrefix(const std::string& ns, + bool isAttr) { + if (PrefixForNs(ns, isAttr).second) + return std::make_pair(STR_EMPTY, false); + + std::string base(SuggestPrefix(ns)); + std::string result(base); + int i = 2; + while (NsForPrefix(result).second) { + std::stringstream ss; + ss << base; + ss << (i++); + ss >> result; + } + AddXmlns(result, ns); + return std::make_pair(result, true); +} + +void XmlnsStack::Reset() { + pxmlnsStack_->clear(); + pxmlnsDepthStack_->clear(); +} + +} diff --git a/talk/xmllite/xmlnsstack.h b/talk/xmllite/xmlnsstack.h new file mode 100644 index 000000000..f6b4b8189 --- /dev/null +++ b/talk/xmllite/xmlnsstack.h @@ -0,0 +1,62 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMLLITE_XMLNSSTACK_H_ +#define TALK_XMLLITE_XMLNSSTACK_H_ + +#include +#include +#include "talk/base/scoped_ptr.h" +#include "talk/xmllite/qname.h" + +namespace buzz { + +class XmlnsStack { +public: + XmlnsStack(); + ~XmlnsStack(); + + void AddXmlns(const std::string& prefix, const std::string& ns); + void RemoveXmlns(); + void PushFrame(); + void PopFrame(); + void Reset(); + + std::pair NsForPrefix(const std::string& prefix); + bool PrefixMatchesNs(const std::string & prefix, const std::string & ns); + std::pair PrefixForNs(const std::string& ns, bool isAttr); + std::pair AddNewPrefix(const std::string& ns, bool isAttr); + std::string FormatQName(const QName & name, bool isAttr); + +private: + + talk_base::scoped_ptr > pxmlnsStack_; + talk_base::scoped_ptr > pxmlnsDepthStack_; +}; +} + +#endif // TALK_XMLLITE_XMLNSSTACK_H_ diff --git a/talk/xmllite/xmlnsstack_unittest.cc b/talk/xmllite/xmlnsstack_unittest.cc new file mode 100644 index 000000000..20b59721f --- /dev/null +++ b/talk/xmllite/xmlnsstack_unittest.cc @@ -0,0 +1,258 @@ +/* + * libjingle + * Copyright 2004, 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/xmllite/xmlnsstack.h" + +#include +#include +#include + +#include "talk/base/common.h" +#include "talk/base/gunit.h" +#include "talk/xmllite/xmlconstants.h" + +using buzz::NS_XML; +using buzz::NS_XMLNS; +using buzz::QName; +using buzz::XmlnsStack; + +TEST(XmlnsStackTest, TestBuiltin) { + XmlnsStack stack; + + EXPECT_EQ(std::string(NS_XML), stack.NsForPrefix("xml").first); + EXPECT_EQ(std::string(NS_XMLNS), stack.NsForPrefix("xmlns").first); + EXPECT_EQ("", stack.NsForPrefix("").first); + + EXPECT_EQ("xml", stack.PrefixForNs(NS_XML, false).first); + EXPECT_EQ("xmlns", stack.PrefixForNs(NS_XMLNS, false).first); + EXPECT_EQ("", stack.PrefixForNs("", false).first); + EXPECT_EQ("", stack.PrefixForNs("", true).first); +} + +TEST(XmlnsStackTest, TestNsForPrefix) { + XmlnsStack stack; + stack.AddXmlns("pre1", "ns1"); + stack.AddXmlns("pre2", "ns2"); + stack.AddXmlns("pre1", "ns3"); + stack.AddXmlns("", "ns4"); + + EXPECT_EQ("ns3", stack.NsForPrefix("pre1").first); + EXPECT_TRUE(stack.NsForPrefix("pre1").second); + EXPECT_EQ("ns2", stack.NsForPrefix("pre2").first); + EXPECT_EQ("ns4", stack.NsForPrefix("").first); + EXPECT_EQ("", stack.NsForPrefix("pre3").first); + EXPECT_FALSE(stack.NsForPrefix("pre3").second); +} + +TEST(XmlnsStackTest, TestPrefixForNs) { + XmlnsStack stack; + stack.AddXmlns("pre1", "ns1"); + stack.AddXmlns("pre2", "ns2"); + stack.AddXmlns("pre1", "ns3"); + stack.AddXmlns("pre3", "ns2"); + stack.AddXmlns("pre4", "ns4"); + stack.AddXmlns("", "ns4"); + + EXPECT_EQ("", stack.PrefixForNs("ns1", false).first); + EXPECT_FALSE(stack.PrefixForNs("ns1", false).second); + EXPECT_EQ("", stack.PrefixForNs("ns1", true).first); + EXPECT_FALSE(stack.PrefixForNs("ns1", true).second); + EXPECT_EQ("pre3", stack.PrefixForNs("ns2", false).first); + EXPECT_TRUE(stack.PrefixForNs("ns2", false).second); + EXPECT_EQ("pre3", stack.PrefixForNs("ns2", true).first); + EXPECT_TRUE(stack.PrefixForNs("ns2", true).second); + EXPECT_EQ("pre1", stack.PrefixForNs("ns3", false).first); + EXPECT_EQ("pre1", stack.PrefixForNs("ns3", true).first); + EXPECT_EQ("", stack.PrefixForNs("ns4", false).first); + EXPECT_TRUE(stack.PrefixForNs("ns4", false).second); + EXPECT_EQ("pre4", stack.PrefixForNs("ns4", true).first); + EXPECT_EQ("", stack.PrefixForNs("ns5", false).first); + EXPECT_FALSE(stack.PrefixForNs("ns5", false).second); + EXPECT_EQ("", stack.PrefixForNs("ns5", true).first); + EXPECT_EQ("", stack.PrefixForNs("", false).first); + EXPECT_EQ("", stack.PrefixForNs("", true).first); + + stack.AddXmlns("", "ns6"); + EXPECT_EQ("", stack.PrefixForNs("ns6", false).first); + EXPECT_TRUE(stack.PrefixForNs("ns6", false).second); + EXPECT_EQ("", stack.PrefixForNs("ns6", true).first); + EXPECT_FALSE(stack.PrefixForNs("ns6", true).second); +} + +TEST(XmlnsStackTest, TestFrames) { + XmlnsStack stack; + stack.PushFrame(); + stack.AddXmlns("pre1", "ns1"); + stack.AddXmlns("pre2", "ns2"); + + stack.PushFrame(); + stack.AddXmlns("pre1", "ns3"); + stack.AddXmlns("pre3", "ns2"); + stack.AddXmlns("pre4", "ns4"); + + stack.PushFrame(); + stack.PushFrame(); + stack.AddXmlns("", "ns4"); + + // basic test + EXPECT_EQ("ns3", stack.NsForPrefix("pre1").first); + EXPECT_EQ("ns2", stack.NsForPrefix("pre2").first); + EXPECT_EQ("ns2", stack.NsForPrefix("pre3").first); + EXPECT_EQ("ns4", stack.NsForPrefix("pre4").first); + EXPECT_EQ("", stack.NsForPrefix("pre5").first); + EXPECT_FALSE(stack.NsForPrefix("pre5").second); + EXPECT_EQ("ns4", stack.NsForPrefix("").first); + EXPECT_TRUE(stack.NsForPrefix("").second); + + // pop the default xmlns definition + stack.PopFrame(); + EXPECT_EQ("ns3", stack.NsForPrefix("pre1").first); + EXPECT_EQ("ns2", stack.NsForPrefix("pre2").first); + EXPECT_EQ("ns2", stack.NsForPrefix("pre3").first); + EXPECT_EQ("ns4", stack.NsForPrefix("pre4").first); + EXPECT_EQ("", stack.NsForPrefix("pre5").first); + EXPECT_FALSE(stack.NsForPrefix("pre5").second); + EXPECT_EQ("", stack.NsForPrefix("").first); + EXPECT_TRUE(stack.NsForPrefix("").second); + + // pop empty frame (nop) + stack.PopFrame(); + EXPECT_EQ("ns3", stack.NsForPrefix("pre1").first); + EXPECT_EQ("ns2", stack.NsForPrefix("pre2").first); + EXPECT_EQ("ns2", stack.NsForPrefix("pre3").first); + EXPECT_EQ("ns4", stack.NsForPrefix("pre4").first); + EXPECT_EQ("", stack.NsForPrefix("pre5").first); + EXPECT_FALSE(stack.NsForPrefix("pre5").second); + EXPECT_EQ("", stack.NsForPrefix("").first); + EXPECT_TRUE(stack.NsForPrefix("").second); + + // pop frame with three defs + stack.PopFrame(); + EXPECT_EQ("ns1", stack.NsForPrefix("pre1").first); + EXPECT_EQ("ns2", stack.NsForPrefix("pre2").first); + EXPECT_EQ("", stack.NsForPrefix("pre3").first); + EXPECT_FALSE(stack.NsForPrefix("pre3").second); + EXPECT_EQ("", stack.NsForPrefix("pre4").first); + EXPECT_FALSE(stack.NsForPrefix("pre4").second); + EXPECT_EQ("", stack.NsForPrefix("pre5").first); + EXPECT_FALSE(stack.NsForPrefix("pre5").second); + EXPECT_EQ("", stack.NsForPrefix("").first); + EXPECT_TRUE(stack.NsForPrefix("").second); + + // pop frame with last two defs + stack.PopFrame(); + EXPECT_FALSE(stack.NsForPrefix("pre1").second); + EXPECT_FALSE(stack.NsForPrefix("pre2").second); + EXPECT_FALSE(stack.NsForPrefix("pre3").second); + EXPECT_FALSE(stack.NsForPrefix("pre4").second); + EXPECT_FALSE(stack.NsForPrefix("pre5").second); + EXPECT_TRUE(stack.NsForPrefix("").second); + EXPECT_EQ("", stack.NsForPrefix("pre1").first); + EXPECT_EQ("", stack.NsForPrefix("pre2").first); + EXPECT_EQ("", stack.NsForPrefix("pre3").first); + EXPECT_EQ("", stack.NsForPrefix("pre4").first); + EXPECT_EQ("", stack.NsForPrefix("pre5").first); + EXPECT_EQ("", stack.NsForPrefix("").first); +} + +TEST(XmlnsStackTest, TestAddNewPrefix) { + XmlnsStack stack; + + // builtin namespaces cannot be added + EXPECT_FALSE(stack.AddNewPrefix("", true).second); + EXPECT_FALSE(stack.AddNewPrefix("", false).second); + EXPECT_FALSE(stack.AddNewPrefix(NS_XML, true).second); + EXPECT_FALSE(stack.AddNewPrefix(NS_XML, false).second); + EXPECT_FALSE(stack.AddNewPrefix(NS_XMLNS, true).second); + EXPECT_FALSE(stack.AddNewPrefix(NS_XMLNS, false).second); + + // namespaces already added cannot be added again. + EXPECT_EQ("foo", stack.AddNewPrefix("http://a.b.com/foo.htm", true).first); + EXPECT_EQ("bare", stack.AddNewPrefix("http://a.b.com/bare", false).first); + EXPECT_EQ("z", stack.AddNewPrefix("z", false).first); + EXPECT_FALSE(stack.AddNewPrefix("http://a.b.com/foo.htm", true).second); + EXPECT_FALSE(stack.AddNewPrefix("http://a.b.com/bare", true).second); + EXPECT_FALSE(stack.AddNewPrefix("z", true).second); + EXPECT_FALSE(stack.AddNewPrefix("http://a.b.com/foo.htm", false).second); + EXPECT_FALSE(stack.AddNewPrefix("http://a.b.com/bare", false).second); + EXPECT_FALSE(stack.AddNewPrefix("z", false).second); + + // default namespace usable by non-attributes only + stack.AddXmlns("", "http://my/default"); + EXPECT_FALSE(stack.AddNewPrefix("http://my/default", false).second); + EXPECT_EQ("def", stack.AddNewPrefix("http://my/default", true).first); + + // namespace cannot start with 'xml' + EXPECT_EQ("ns", stack.AddNewPrefix("http://a.b.com/xmltest", true).first); + EXPECT_EQ("ns2", stack.AddNewPrefix("xmlagain", false).first); + + // verify added namespaces are still defined + EXPECT_EQ("http://a.b.com/foo.htm", stack.NsForPrefix("foo").first); + EXPECT_TRUE(stack.NsForPrefix("foo").second); + EXPECT_EQ("http://a.b.com/bare", stack.NsForPrefix("bare").first); + EXPECT_TRUE(stack.NsForPrefix("bare").second); + EXPECT_EQ("z", stack.NsForPrefix("z").first); + EXPECT_TRUE(stack.NsForPrefix("z").second); + EXPECT_EQ("http://my/default", stack.NsForPrefix("").first); + EXPECT_TRUE(stack.NsForPrefix("").second); + EXPECT_EQ("http://my/default", stack.NsForPrefix("def").first); + EXPECT_TRUE(stack.NsForPrefix("def").second); + EXPECT_EQ("http://a.b.com/xmltest", stack.NsForPrefix("ns").first); + EXPECT_TRUE(stack.NsForPrefix("ns").second); + EXPECT_EQ("xmlagain", stack.NsForPrefix("ns2").first); + EXPECT_TRUE(stack.NsForPrefix("ns2").second); +} + +TEST(XmlnsStackTest, TestFormatQName) { + XmlnsStack stack; + stack.AddXmlns("pre1", "ns1"); + stack.AddXmlns("pre2", "ns2"); + stack.AddXmlns("pre1", "ns3"); + stack.AddXmlns("", "ns4"); + + EXPECT_EQ("zip", + stack.FormatQName(QName("ns1", "zip"), false)); // no match + EXPECT_EQ("pre2:abracadabra", + stack.FormatQName(QName("ns2", "abracadabra"), false)); + EXPECT_EQ("pre1:a", + stack.FormatQName(QName("ns3", "a"), false)); + EXPECT_EQ("simple", + stack.FormatQName(QName("ns4", "simple"), false)); + EXPECT_EQ("root", + stack.FormatQName(QName("", "root"), false)); // no match + + EXPECT_EQ("zip", + stack.FormatQName(QName("ns1", "zip"), true)); // no match + EXPECT_EQ("pre2:abracadabra", + stack.FormatQName(QName("ns2", "abracadabra"), true)); + EXPECT_EQ("pre1:a", + stack.FormatQName(QName("ns3", "a"), true)); + EXPECT_EQ("simple", + stack.FormatQName(QName("ns4", "simple"), true)); // no match + EXPECT_EQ("root", + stack.FormatQName(QName("", "root"), true)); +} diff --git a/talk/xmllite/xmlparser.cc b/talk/xmllite/xmlparser.cc new file mode 100644 index 000000000..3e4d73348 --- /dev/null +++ b/talk/xmllite/xmlparser.cc @@ -0,0 +1,279 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmllite/xmlparser.h" + +#include +#include + +#include "talk/base/common.h" +#include "talk/xmllite/xmlconstants.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmllite/xmlnsstack.h" +#include "talk/xmllite/xmlnsstack.h" + +namespace buzz { + + +static void +StartElementCallback(void * userData, const char *name, const char **atts) { + (static_cast(userData))->ExpatStartElement(name, atts); +} + +static void +EndElementCallback(void * userData, const char *name) { + (static_cast(userData))->ExpatEndElement(name); +} + +static void +CharacterDataCallback(void * userData, const char *text, int len) { + (static_cast(userData))->ExpatCharacterData(text, len); +} + +static void +XmlDeclCallback(void * userData, const char * ver, const char * enc, int st) { + (static_cast(userData))->ExpatXmlDecl(ver, enc, st); +} + +XmlParser::XmlParser(XmlParseHandler *pxph) : + context_(this), pxph_(pxph), sentError_(false) { + expat_ = XML_ParserCreate(NULL); + XML_SetUserData(expat_, this); + XML_SetElementHandler(expat_, StartElementCallback, EndElementCallback); + XML_SetCharacterDataHandler(expat_, CharacterDataCallback); + XML_SetXmlDeclHandler(expat_, XmlDeclCallback); +} + +void +XmlParser::Reset() { + if (!XML_ParserReset(expat_, NULL)) { + XML_ParserFree(expat_); + expat_ = XML_ParserCreate(NULL); + } + XML_SetUserData(expat_, this); + XML_SetElementHandler(expat_, StartElementCallback, EndElementCallback); + XML_SetCharacterDataHandler(expat_, CharacterDataCallback); + XML_SetXmlDeclHandler(expat_, XmlDeclCallback); + context_.Reset(); + sentError_ = false; +} + +static bool +XmlParser_StartsWithXmlns(const char *name) { + return name[0] == 'x' && + name[1] == 'm' && + name[2] == 'l' && + name[3] == 'n' && + name[4] == 's'; +} + +void +XmlParser::ExpatStartElement(const char *name, const char **atts) { + if (context_.RaisedError() != XML_ERROR_NONE) + return; + const char **att; + context_.StartElement(); + for (att = atts; *att; att += 2) { + if (XmlParser_StartsWithXmlns(*att)) { + if ((*att)[5] == '\0') { + context_.StartNamespace("", *(att + 1)); + } + else if ((*att)[5] == ':') { + if (**(att + 1) == '\0') { + // In XML 1.0 empty namespace illegal with prefix (not in 1.1) + context_.RaiseError(XML_ERROR_SYNTAX); + return; + } + context_.StartNamespace((*att) + 6, *(att + 1)); + } + } + } + context_.SetPosition(XML_GetCurrentLineNumber(expat_), + XML_GetCurrentColumnNumber(expat_), + XML_GetCurrentByteIndex(expat_)); + pxph_->StartElement(&context_, name, atts); +} + +void +XmlParser::ExpatEndElement(const char *name) { + if (context_.RaisedError() != XML_ERROR_NONE) + return; + context_.EndElement(); + context_.SetPosition(XML_GetCurrentLineNumber(expat_), + XML_GetCurrentColumnNumber(expat_), + XML_GetCurrentByteIndex(expat_)); + pxph_->EndElement(&context_, name); +} + +void +XmlParser::ExpatCharacterData(const char *text, int len) { + if (context_.RaisedError() != XML_ERROR_NONE) + return; + context_.SetPosition(XML_GetCurrentLineNumber(expat_), + XML_GetCurrentColumnNumber(expat_), + XML_GetCurrentByteIndex(expat_)); + pxph_->CharacterData(&context_, text, len); +} + +void +XmlParser::ExpatXmlDecl(const char * ver, const char * enc, int standalone) { + if (context_.RaisedError() != XML_ERROR_NONE) + return; + + if (ver && std::string("1.0") != ver) { + context_.RaiseError(XML_ERROR_SYNTAX); + return; + } + + if (standalone == 0) { + context_.RaiseError(XML_ERROR_SYNTAX); + return; + } + + if (enc && !((enc[0] == 'U' || enc[0] == 'u') && + (enc[1] == 'T' || enc[1] == 't') && + (enc[2] == 'F' || enc[2] == 'f') && + enc[3] == '-' && enc[4] =='8')) { + context_.RaiseError(XML_ERROR_INCORRECT_ENCODING); + return; + } + +} + +bool +XmlParser::Parse(const char *data, size_t len, bool isFinal) { + if (sentError_) + return false; + + if (XML_Parse(expat_, data, static_cast(len), isFinal) != + XML_STATUS_OK) { + context_.SetPosition(XML_GetCurrentLineNumber(expat_), + XML_GetCurrentColumnNumber(expat_), + XML_GetCurrentByteIndex(expat_)); + context_.RaiseError(XML_GetErrorCode(expat_)); + } + + if (context_.RaisedError() != XML_ERROR_NONE) { + sentError_ = true; + pxph_->Error(&context_, context_.RaisedError()); + return false; + } + + return true; +} + +XmlParser::~XmlParser() { + XML_ParserFree(expat_); +} + +void +XmlParser::ParseXml(XmlParseHandler *pxph, std::string text) { + XmlParser parser(pxph); + parser.Parse(text.c_str(), text.length(), true); +} + +XmlParser::ParseContext::ParseContext(XmlParser *parser) : + parser_(parser), + xmlnsstack_(), + raised_(XML_ERROR_NONE), + line_number_(0), + column_number_(0), + byte_index_(0) { +} + +void +XmlParser::ParseContext::StartNamespace(const char *prefix, const char *ns) { + xmlnsstack_.AddXmlns(*prefix ? prefix : STR_EMPTY, ns); +} + +void +XmlParser::ParseContext::StartElement() { + xmlnsstack_.PushFrame(); +} + +void +XmlParser::ParseContext::EndElement() { + xmlnsstack_.PopFrame(); +} + +QName +XmlParser::ParseContext::ResolveQName(const char* qname, bool isAttr) { + const char *c; + for (c = qname; *c; ++c) { + if (*c == ':') { + const std::pair result = + xmlnsstack_.NsForPrefix(std::string(qname, c - qname)); + if (!result.second) + return QName(); + return QName(result.first, c + 1); + } + } + if (isAttr) + return QName(STR_EMPTY, qname); + + std::pair result = xmlnsstack_.NsForPrefix(STR_EMPTY); + if (!result.second) + return QName(); + + return QName(result.first, qname); +} + +void +XmlParser::ParseContext::Reset() { + xmlnsstack_.Reset(); + raised_ = XML_ERROR_NONE; +} + +void +XmlParser::ParseContext::SetPosition(int line, int column, + long byte_index) { + line_number_ = line; + column_number_ = column; + byte_index_ = byte_index; +} + +void +XmlParser::ParseContext::GetPosition(unsigned long * line, + unsigned long * column, + unsigned long * byte_index) { + if (line != NULL) { + *line = static_cast(line_number_); + } + + if (column != NULL) { + *column = static_cast(column_number_); + } + + if (byte_index != NULL) { + *byte_index = static_cast(byte_index_); + } +} + +XmlParser::ParseContext::~ParseContext() { +} + +} // namespace buzz diff --git a/talk/xmllite/xmlparser.h b/talk/xmllite/xmlparser.h new file mode 100644 index 000000000..69cde75f7 --- /dev/null +++ b/talk/xmllite/xmlparser.h @@ -0,0 +1,121 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMLLITE_XMLPARSER_H_ +#define TALK_XMLLITE_XMLPARSER_H_ + +#include + +#include "talk/xmllite/xmlnsstack.h" +#ifdef EXPAT_RELATIVE_PATH +#include "expat.h" +#else +#include "third_party/expat/v2_0_1/Source/lib/expat.h" +#endif // EXPAT_RELATIVE_PATH + +struct XML_ParserStruct; +typedef struct XML_ParserStruct* XML_Parser; + +namespace buzz { + +class XmlParseHandler; +class XmlParseContext; +class XmlParser; + +class XmlParseContext { +public: + virtual ~XmlParseContext() {} + virtual QName ResolveQName(const char * qname, bool isAttr) = 0; + virtual void RaiseError(XML_Error err) = 0; + virtual void GetPosition(unsigned long * line, unsigned long * column, + unsigned long * byte_index) = 0; +}; + +class XmlParseHandler { +public: + virtual ~XmlParseHandler() {} + virtual void StartElement(XmlParseContext * pctx, + const char * name, const char ** atts) = 0; + virtual void EndElement(XmlParseContext * pctx, + const char * name) = 0; + virtual void CharacterData(XmlParseContext * pctx, + const char * text, int len) = 0; + virtual void Error(XmlParseContext * pctx, + XML_Error errorCode) = 0; +}; + +class XmlParser { +public: + static void ParseXml(XmlParseHandler * pxph, std::string text); + + explicit XmlParser(XmlParseHandler * pxph); + bool Parse(const char * data, size_t len, bool isFinal); + void Reset(); + virtual ~XmlParser(); + + // expat callbacks + void ExpatStartElement(const char * name, const char ** atts); + void ExpatEndElement(const char * name); + void ExpatCharacterData(const char * text, int len); + void ExpatXmlDecl(const char * ver, const char * enc, int standalone); + +private: + + class ParseContext : public XmlParseContext { + public: + ParseContext(XmlParser * parser); + virtual ~ParseContext(); + virtual QName ResolveQName(const char * qname, bool isAttr); + virtual void RaiseError(XML_Error err) { if (!raised_) raised_ = err; } + virtual void GetPosition(unsigned long * line, unsigned long * column, + unsigned long * byte_index); + XML_Error RaisedError() { return raised_; } + void Reset(); + + void StartElement(); + void EndElement(); + void StartNamespace(const char * prefix, const char * ns); + void SetPosition(int line, int column, long byte_index); + + private: + const XmlParser * parser_; + XmlnsStack xmlnsstack_; + XML_Error raised_; + XML_Size line_number_; + XML_Size column_number_; + XML_Index byte_index_; + }; + + ParseContext context_; + XML_Parser expat_; + XmlParseHandler * pxph_; + bool sentError_; +}; + +} // namespace buzz + +#endif // TALK_XMLLITE_XMLPARSER_H_ diff --git a/talk/xmllite/xmlparser_unittest.cc b/talk/xmllite/xmlparser_unittest.cc new file mode 100644 index 000000000..24947fb9a --- /dev/null +++ b/talk/xmllite/xmlparser_unittest.cc @@ -0,0 +1,302 @@ +/* + * libjingle + * Copyright 2004, 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 +#include +#include +#include "talk/base/common.h" +#include "talk/base/gunit.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlparser.h" + +using buzz::QName; +using buzz::XmlParser; +using buzz::XmlParseContext; +using buzz::XmlParseHandler; + +class XmlParserTestHandler : public XmlParseHandler { + public: + virtual void StartElement(XmlParseContext * pctx, + const char * name, const char ** atts) { + ss_ << "START (" << pctx->ResolveQName(name, false).Merged(); + while (*atts) { + ss_ << ", " << pctx->ResolveQName(*atts, true).Merged() + << "='" << *(atts+1) << "'"; + atts += 2; + } + ss_ << ") "; + } + virtual void EndElement(XmlParseContext * pctx, const char * name) { + UNUSED(pctx); + UNUSED(name); + ss_ << "END "; + } + virtual void CharacterData(XmlParseContext * pctx, + const char * text, int len) { + UNUSED(pctx); + ss_ << "TEXT (" << std::string(text, len) << ") "; + } + virtual void Error(XmlParseContext * pctx, XML_Error code) { + UNUSED(pctx); + ss_ << "ERROR (" << static_cast(code) << ") "; + } + virtual ~XmlParserTestHandler() { + } + + std::string Str() { + return ss_.str(); + } + + std::string StrClear() { + std::string result = ss_.str(); + ss_.str(""); + return result; + } + + private: + std::stringstream ss_; +}; + + +TEST(XmlParserTest, TestTrivial) { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, ""); + EXPECT_EQ("START (testing) END ", handler.Str()); +} + +TEST(XmlParserTest, TestAttributes) { + { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, ""); + EXPECT_EQ("START (testing, a='b') END ", handler.Str()); + } + { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, ""); + EXPECT_EQ("START (testing, e='', long='some text') END ", handler.Str()); + } +} + +TEST(XmlParserTest, TestNesting) { + { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, + ""); + EXPECT_EQ("START (top) START (first) END START (second) START (third) " + "END END END ", handler.Str()); + } + { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, "" + "" + ""); + EXPECT_EQ("START (top) START (fifth) START (deeper) START (and) START " + "(deeper) END END START (sibling) START (leaf) END END END " + "END START (first) END START (second) START (third) END END END ", + handler.Str()); + } +} + +TEST(XmlParserTest, TestXmlDecl) { + { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, ""); + EXPECT_EQ("START (testing) END ", handler.Str()); + } + { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, + ""); + EXPECT_EQ("START (testing) END ", handler.Str()); + } + { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, + "" + ""); + EXPECT_EQ("START (testing) END ", handler.Str()); + } +} + +TEST(XmlParserTest, TestNamespace) { + { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, ""); + EXPECT_EQ("START (my-namespace:top, xmlns='my-namespace', a='b') END ", + handler.Str()); + } + { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, ""); + EXPECT_EQ("START (my-namespace:top, " + "http://www.w3.org/2000/xmlns/:foo='my-namespace', " + "a='b', my-namespace:c='d') END ", handler.Str()); + } + { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, "" + ""); + EXPECT_EQ("START (top) START (my-namespace:nested, xmlns='my-namespace') " + "START (my-namespace:leaf) END END START (sibling) END END ", + handler.Str()); + } +} + +TEST(XmlParserTest, TestIncremental) { + XmlParserTestHandler handler; + XmlParser parser(&handler); + std::string fragment; + + fragment = " garbage "); + EXPECT_EQ("START (top) END ERROR (9) ", handler.Str()); + } + { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, "<-hm->"); + EXPECT_EQ("ERROR (4) ", handler.Str()); + } + { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, "&foobar;"); + EXPECT_EQ("START (hello) ERROR (11) ", handler.Str()); + } + { + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, + ""); + EXPECT_EQ("ERROR (3) ", handler.Str()); + } + { + // XmlParser requires utf-8 + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, + ""); + EXPECT_EQ("ERROR (19) ", handler.Str()); + } + { + // XmlParser requires version 1.0 + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, + ""); + EXPECT_EQ("ERROR (2) ", handler.Str()); + } + { + // XmlParser requires standalone documents + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, + ""); + EXPECT_EQ("ERROR (2) ", handler.Str()); + } + { + // XmlParser doesn't like empty namespace URIs + XmlParserTestHandler handler; + XmlParser::ParseXml(&handler, + ""); + EXPECT_EQ("ERROR (2) ", handler.Str()); + } +} diff --git a/talk/xmllite/xmlprinter.cc b/talk/xmllite/xmlprinter.cc new file mode 100644 index 000000000..1350454b8 --- /dev/null +++ b/talk/xmllite/xmlprinter.cc @@ -0,0 +1,191 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmllite/xmlprinter.h" + +#include +#include +#include + +#include "talk/xmllite/xmlconstants.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmllite/xmlnsstack.h" + +namespace buzz { + +class XmlPrinterImpl { +public: + XmlPrinterImpl(std::ostream* pout, XmlnsStack* ns_stack); + void PrintElement(const XmlElement* element); + void PrintQuotedValue(const std::string& text); + void PrintBodyText(const std::string& text); + void PrintCDATAText(const std::string& text); + +private: + std::ostream *pout_; + XmlnsStack* ns_stack_; +}; + +void XmlPrinter::PrintXml(std::ostream* pout, const XmlElement* element) { + XmlnsStack ns_stack; + PrintXml(pout, element, &ns_stack); +} + +void XmlPrinter::PrintXml(std::ostream* pout, const XmlElement* element, + XmlnsStack* ns_stack) { + XmlPrinterImpl printer(pout, ns_stack); + printer.PrintElement(element); +} + +XmlPrinterImpl::XmlPrinterImpl(std::ostream* pout, XmlnsStack* ns_stack) + : pout_(pout), + ns_stack_(ns_stack) { +} + +void XmlPrinterImpl::PrintElement(const XmlElement* element) { + ns_stack_->PushFrame(); + + // first go through attrs of pel to add xmlns definitions + const XmlAttr* attr; + for (attr = element->FirstAttr(); attr; attr = attr->NextAttr()) { + if (attr->Name() == QN_XMLNS) { + ns_stack_->AddXmlns(STR_EMPTY, attr->Value()); + } else if (attr->Name().Namespace() == NS_XMLNS) { + ns_stack_->AddXmlns(attr->Name().LocalPart(), + attr->Value()); + } + } + + // then go through qnames to make sure needed xmlns definitons are added + std::vector new_ns; + std::pair prefix; + prefix = ns_stack_->AddNewPrefix(element->Name().Namespace(), false); + if (prefix.second) { + new_ns.push_back(prefix.first); + new_ns.push_back(element->Name().Namespace()); + } + + for (attr = element->FirstAttr(); attr; attr = attr->NextAttr()) { + prefix = ns_stack_->AddNewPrefix(attr->Name().Namespace(), true); + if (prefix.second) { + new_ns.push_back(prefix.first); + new_ns.push_back(attr->Name().Namespace()); + } + } + + // print the element name + *pout_ << '<' << ns_stack_->FormatQName(element->Name(), false); + + // and the attributes + for (attr = element->FirstAttr(); attr; attr = attr->NextAttr()) { + *pout_ << ' ' << ns_stack_->FormatQName(attr->Name(), true) << "=\""; + PrintQuotedValue(attr->Value()); + *pout_ << '"'; + } + + // and the extra xmlns declarations + std::vector::iterator i(new_ns.begin()); + while (i < new_ns.end()) { + if (*i == STR_EMPTY) { + *pout_ << " xmlns=\"" << *(i + 1) << '"'; + } else { + *pout_ << " xmlns:" << *i << "=\"" << *(i + 1) << '"'; + } + i += 2; + } + + // now the children + const XmlChild* child = element->FirstChild(); + + if (child == NULL) + *pout_ << "/>"; + else { + *pout_ << '>'; + while (child) { + if (child->IsText()) { + if (element->IsCDATA()) { + PrintCDATAText(child->AsText()->Text()); + } else { + PrintBodyText(child->AsText()->Text()); + } + } else { + PrintElement(child->AsElement()); + } + child = child->NextChild(); + } + *pout_ << "FormatQName(element->Name(), false) << '>'; + } + + ns_stack_->PopFrame(); +} + +void XmlPrinterImpl::PrintQuotedValue(const std::string& text) { + size_t safe = 0; + for (;;) { + size_t unsafe = text.find_first_of("<>&\"", safe); + if (unsafe == std::string::npos) + unsafe = text.length(); + *pout_ << text.substr(safe, unsafe - safe); + if (unsafe == text.length()) + return; + switch (text[unsafe]) { + case '<': *pout_ << "<"; break; + case '>': *pout_ << ">"; break; + case '&': *pout_ << "&"; break; + case '"': *pout_ << """; break; + } + safe = unsafe + 1; + if (safe == text.length()) + return; + } +} + +void XmlPrinterImpl::PrintBodyText(const std::string& text) { + size_t safe = 0; + for (;;) { + size_t unsafe = text.find_first_of("<>&", safe); + if (unsafe == std::string::npos) + unsafe = text.length(); + *pout_ << text.substr(safe, unsafe - safe); + if (unsafe == text.length()) + return; + switch (text[unsafe]) { + case '<': *pout_ << "<"; break; + case '>': *pout_ << ">"; break; + case '&': *pout_ << "&"; break; + } + safe = unsafe + 1; + if (safe == text.length()) + return; + } +} + +void XmlPrinterImpl::PrintCDATAText(const std::string& text) { + *pout_ << ""; +} + +} // namespace buzz diff --git a/talk/xmllite/xmlprinter.h b/talk/xmllite/xmlprinter.h new file mode 100644 index 000000000..90cc255b7 --- /dev/null +++ b/talk/xmllite/xmlprinter.h @@ -0,0 +1,49 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMLLITE_XMLPRINTER_H_ +#define TALK_XMLLITE_XMLPRINTER_H_ + +#include +#include + +namespace buzz { + +class XmlElement; +class XmlnsStack; + +class XmlPrinter { + public: + static void PrintXml(std::ostream* pout, const XmlElement* pelt); + + static void PrintXml(std::ostream* pout, const XmlElement* pelt, + XmlnsStack* ns_stack); +}; + +} // namespace buzz + +#endif // TALK_XMLLITE_XMLPRINTER_H_ diff --git a/talk/xmllite/xmlprinter_unittest.cc b/talk/xmllite/xmlprinter_unittest.cc new file mode 100644 index 000000000..60b0e42bd --- /dev/null +++ b/talk/xmllite/xmlprinter_unittest.cc @@ -0,0 +1,62 @@ +/* + * libjingle + * Copyright 2004, 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/xmllite/xmlprinter.h" + +#include +#include + +#include "talk/base/common.h" +#include "talk/base/gunit.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmllite/xmlnsstack.h" + +using buzz::QName; +using buzz::XmlElement; +using buzz::XmlnsStack; +using buzz::XmlPrinter; + +TEST(XmlPrinterTest, TestBasicPrinting) { + XmlElement elt(QName("google:test", "first")); + std::stringstream ss; + XmlPrinter::PrintXml(&ss, &elt); + EXPECT_EQ("", ss.str()); +} + +TEST(XmlPrinterTest, TestNamespacedPrinting) { + XmlElement elt(QName("google:test", "first")); + elt.AddElement(new XmlElement(QName("nested:test", "second"))); + std::stringstream ss; + + XmlnsStack ns_stack; + ns_stack.AddXmlns("gg", "google:test"); + ns_stack.AddXmlns("", "nested:test"); + + XmlPrinter::PrintXml(&ss, &elt, &ns_stack); + EXPECT_EQ("", ss.str()); +} diff --git a/talk/xmpp/asyncsocket.h b/talk/xmpp/asyncsocket.h new file mode 100644 index 000000000..fb4ef029b --- /dev/null +++ b/talk/xmpp/asyncsocket.h @@ -0,0 +1,87 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _ASYNCSOCKET_H_ +#define _ASYNCSOCKET_H_ + +#include "talk/base/sigslot.h" + +namespace talk_base { + class SocketAddress; +} + +namespace buzz { + +class AsyncSocket { +public: + enum State { + STATE_CLOSED = 0, //!< Socket is not open. + STATE_CLOSING, //!< Socket is closing but can have buffered data + STATE_CONNECTING, //!< In the process of + STATE_OPEN, //!< Socket is connected +#if defined(FEATURE_ENABLE_SSL) + STATE_TLS_CONNECTING, //!< Establishing TLS connection + STATE_TLS_OPEN, //!< TLS connected +#endif + }; + + enum Error { + ERROR_NONE = 0, //!< No error + ERROR_WINSOCK, //!< Winsock error + ERROR_DNS, //!< Couldn't resolve host name + ERROR_WRONGSTATE, //!< Call made while socket is in the wrong state +#if defined(FEATURE_ENABLE_SSL) + ERROR_SSL, //!< Something went wrong with OpenSSL +#endif + }; + + virtual ~AsyncSocket() {} + virtual State state() = 0; + virtual Error error() = 0; + virtual int GetError() = 0; // winsock error code + + virtual bool Connect(const talk_base::SocketAddress& addr) = 0; + virtual bool Read(char * data, size_t len, size_t* len_read) = 0; + virtual bool Write(const char * data, size_t len) = 0; + virtual bool Close() = 0; +#if defined(FEATURE_ENABLE_SSL) + // We allow matching any passed domain. This allows us to avoid + // handling the valuable certificates for logins into proxies. If + // both names are passed as empty, we do not require a match. + virtual bool StartTls(const std::string & domainname) = 0; +#endif + + sigslot::signal0<> SignalConnected; + sigslot::signal0<> SignalSSLConnected; + sigslot::signal0<> SignalClosed; + sigslot::signal0<> SignalRead; + sigslot::signal0<> SignalError; +}; + +} + +#endif diff --git a/talk/xmpp/chatroommodule.h b/talk/xmpp/chatroommodule.h new file mode 100644 index 000000000..47a7106a1 --- /dev/null +++ b/talk/xmpp/chatroommodule.h @@ -0,0 +1,270 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _multiuserchatmodule_h_ +#define _multiuserchatmodule_h_ + +#include "talk/xmpp/module.h" +#include "talk/xmpp/rostermodule.h" + +namespace buzz { + +// forward declarations +class XmppChatroomModule; +class XmppChatroomHandler; +class XmppChatroomMember; +class XmppChatroomMemberEnumerator; + +enum XmppChatroomState { + XMPP_CHATROOM_STATE_NOT_IN_ROOM = 0, + XMPP_CHATROOM_STATE_REQUESTED_ENTER = 1, + XMPP_CHATROOM_STATE_IN_ROOM = 2, + XMPP_CHATROOM_STATE_REQUESTED_EXIT = 3, +}; + +//! Module that encapsulates a chatroom. +class XmppChatroomModule : public XmppModule { +public: + + //! Creates a new XmppChatroomModule + static XmppChatroomModule* Create(); + virtual ~XmppChatroomModule() {} + + //! Sets the chatroom handler (callbacks) for the chatroom + virtual XmppReturnStatus set_chatroom_handler(XmppChatroomHandler* handler) = 0; + + //! Gets the chatroom handler for the module + virtual XmppChatroomHandler* chatroom_handler() = 0; + + //! Sets the jid of the chatroom. + //! Has to be set before entering the chatroom and can't be changed + //! while in the chatroom + virtual XmppReturnStatus set_chatroom_jid(const Jid& chatroom_jid) = 0; + + //! The jid for the chatroom + virtual const Jid& chatroom_jid() const = 0; + + //! Sets the nickname of the member + //! Has to be set before entering the chatroom and can't be changed + //! while in the chatroom + virtual XmppReturnStatus set_nickname(const std::string& nickname) = 0; + + //! The nickname of the member in the chatroom + virtual const std::string& nickname() const = 0; + + //! Returns the jid of the member (this is the chatroom_jid plus the + //! nickname as the resource name) + virtual const Jid member_jid() const = 0; + + //! Requests that the user enter a chatroom + //! The EnterChatroom callback will be called when the request is complete. + //! Password should be empty for a room that doesn't require a password + //! If the room doesn't exist, the server will create an "Instant Room" if the + //! server policy supports this action. + //! There will be different methods for creating/configuring a "Reserved Room" + //! Async callback for this method is ChatroomEnteredStatus + virtual XmppReturnStatus RequestEnterChatroom(const std::string& password, + const std::string& client_version, + const std::string& locale) = 0; + + //! Requests that the user exit a chatroom + //! Async callback for this method is ChatroomExitedStatus + virtual XmppReturnStatus RequestExitChatroom() = 0; + + //! Requests a status change + //! status is the standard XMPP status code + //! extended_status is the extended status when status is XMPP_PRESENCE_XA + virtual XmppReturnStatus RequestConnectionStatusChange( + XmppPresenceConnectionStatus connection_status) = 0; + + //! Returns the number of members in the room + virtual size_t GetChatroomMemberCount() = 0; + + //! Gets an enumerator for the members in the chatroom + //! The caller must delete the enumerator when the caller is finished with it. + //! The caller must also ensure that the lifetime of the enumerator is + //! scoped by the XmppChatRoomModule that created it. + virtual XmppReturnStatus CreateMemberEnumerator(XmppChatroomMemberEnumerator** enumerator) = 0; + + //! Gets the subject of the chatroom + virtual const std::string subject() = 0; + + //! Returns the current state of the user with respect to the chatroom + virtual XmppChatroomState state() = 0; + + virtual XmppReturnStatus SendMessage(const XmlElement& message) = 0; +}; + +//! Class for enumerating participatns +class XmppChatroomMemberEnumerator { +public: + virtual ~XmppChatroomMemberEnumerator() { } + //! Returns the member at the current position + //! Returns null if the enumerator is before the beginning + //! or after the end of the collection + virtual XmppChatroomMember* current() = 0; + + //! Returns whether the enumerator is valid + //! This returns true if the collection has changed + //! since the enumerator was created + virtual bool IsValid() = 0; + + //! Returns whether the enumerator is before the beginning + //! This is the initial state of the enumerator + virtual bool IsBeforeBeginning() = 0; + + //! Returns whether the enumerator is after the end + virtual bool IsAfterEnd() = 0; + + //! Advances the enumerator to the next position + //! Returns false is the enumerator is advanced + //! off the end of the collection + virtual bool Next() = 0; + + //! Advances the enumerator to the previous position + //! Returns false is the enumerator is advanced + //! off the end of the collection + virtual bool Prev() = 0; +}; + + +//! Represents a single member in a chatroom +class XmppChatroomMember { +public: + virtual ~XmppChatroomMember() { } + + //! The jid for the member in the chatroom + virtual const Jid member_jid() const = 0; + + //! The full jid for the member + //! This is only available in non-anonymous rooms. + //! If the room is anonymous, this returns JID_EMPTY + virtual const Jid full_jid() const = 0; + + //! Returns the backing presence for this member + virtual const XmppPresence* presence() const = 0; + + //! The nickname for this member + virtual const std::string name() const = 0; +}; + +//! Status codes for ChatroomEnteredStatus callback +enum XmppChatroomEnteredStatus +{ + //! User successfully entered the room + XMPP_CHATROOM_ENTERED_SUCCESS = 0, + //! The nickname confliced with somebody already in the room + XMPP_CHATROOM_ENTERED_FAILURE_NICKNAME_CONFLICT = 1, + //! A password is required to enter the room + XMPP_CHATROOM_ENTERED_FAILURE_PASSWORD_REQUIRED = 2, + //! The specified password was incorrect + XMPP_CHATROOM_ENTERED_FAILURE_PASSWORD_INCORRECT = 3, + //! The user is not a member of a member-only room + XMPP_CHATROOM_ENTERED_FAILURE_NOT_A_MEMBER = 4, + //! The user cannot enter because the user has been banned + XMPP_CHATROOM_ENTERED_FAILURE_MEMBER_BANNED = 5, + //! The room has the maximum number of users already + XMPP_CHATROOM_ENTERED_FAILURE_MAX_USERS = 6, + //! The room has been locked by an administrator + XMPP_CHATROOM_ENTERED_FAILURE_ROOM_LOCKED = 7, + //! Someone in the room has blocked you + XMPP_CHATROOM_ENTERED_FAILURE_MEMBER_BLOCKED = 8, + //! You have blocked someone in the room + XMPP_CHATROOM_ENTERED_FAILURE_MEMBER_BLOCKING = 9, + //! Client is old. User must upgrade to a more recent version for + // hangouts to work. + XMPP_CHATROOM_ENTERED_FAILURE_OUTDATED_CLIENT = 10, + //! Some other reason + XMPP_CHATROOM_ENTERED_FAILURE_UNSPECIFIED = 2000, +}; + +//! Status codes for ChatroomExitedStatus callback +enum XmppChatroomExitedStatus +{ + //! The user requested to exit and did so + XMPP_CHATROOM_EXITED_REQUESTED = 0, + //! The user was banned from the room + XMPP_CHATROOM_EXITED_BANNED = 1, + //! The user has been kicked out of the room + XMPP_CHATROOM_EXITED_KICKED = 2, + //! The user has been removed from the room because the + //! user is no longer a member of a member-only room + //! or the room has changed to membership-only + XMPP_CHATROOM_EXITED_NOT_A_MEMBER = 3, + //! The system is shutting down + XMPP_CHATROOM_EXITED_SYSTEM_SHUTDOWN = 4, + //! For some other reason + XMPP_CHATROOM_EXITED_UNSPECIFIED = 5, +}; + +//! The XmppChatroomHandler is the interface for callbacks from the +//! the chatroom +class XmppChatroomHandler { +public: + virtual ~XmppChatroomHandler() {} + + //! Indicates the response to RequestEnterChatroom method + //! XMPP_CHATROOM_SUCCESS represents success. + //! Other status codes are for errors + virtual void ChatroomEnteredStatus(XmppChatroomModule* room, + const XmppPresence* presence, + XmppChatroomEnteredStatus status) = 0; + + + //! Indicates that the user has exited the chatroom, either due to + //! a call to RequestExitChatroom or for some other reason. + //! status indicates the reason the user exited + virtual void ChatroomExitedStatus(XmppChatroomModule* room, + XmppChatroomExitedStatus status) = 0; + + //! Indicates a member entered the room. + //! It can be called before ChatroomEnteredStatus. + virtual void MemberEntered(XmppChatroomModule* room, + const XmppChatroomMember* entered_member) = 0; + + //! Indicates that a member exited the room. + virtual void MemberExited(XmppChatroomModule* room, + const XmppChatroomMember* exited_member) = 0; + + //! Indicates that the data for the member has changed + //! (such as the nickname or presence) + virtual void MemberChanged(XmppChatroomModule* room, + const XmppChatroomMember* changed_member) = 0; + + //! Indicates a new message has been received + //! message is the message - + // $TODO - message should be changed + //! to a strongly-typed message class that contains info + //! such as the sender, message bodies, etc., + virtual void MessageReceived(XmppChatroomModule* room, + const XmlElement& message) = 0; +}; + + +} + +#endif diff --git a/talk/xmpp/chatroommodule_unittest.cc b/talk/xmpp/chatroommodule_unittest.cc new file mode 100644 index 000000000..a152f6060 --- /dev/null +++ b/talk/xmpp/chatroommodule_unittest.cc @@ -0,0 +1,297 @@ +/* + * libjingle + * Copyright 2004, 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 +#include +#include +#include "common/common.h" +#include "buzz/xmppengine.h" +#include "buzz/xmlelement.h" +#include "buzz/chatroommodule.h" +#include "buzz/constants.h" +#include "engine/util_unittest.h" +#include "test/unittest.h" +#include "test/unittest-inl.h" + +#define TEST_OK(x) TEST_EQ((x),XMPP_RETURN_OK) +#define TEST_BADARGUMENT(x) TEST_EQ((x),XMPP_RETURN_BADARGUMENT) + +namespace buzz { + +class MultiUserChatModuleTest; + +static void +WriteEnteredStatus(std::ostream& os, XmppChatroomEnteredStatus status) { + switch(status) { + case XMPP_CHATROOM_ENTERED_SUCCESS: + os<<"success"; + break; + case XMPP_CHATROOM_ENTERED_FAILURE_NICKNAME_CONFLICT: + os<<"failure(nickname conflict)"; + break; + case XMPP_CHATROOM_ENTERED_FAILURE_PASSWORD_REQUIRED: + os<<"failure(password required)"; + break; + case XMPP_CHATROOM_ENTERED_FAILURE_PASSWORD_INCORRECT: + os<<"failure(password incorrect)"; + break; + case XMPP_CHATROOM_ENTERED_FAILURE_NOT_A_MEMBER: + os<<"failure(not a member)"; + break; + case XMPP_CHATROOM_ENTERED_FAILURE_MEMBER_BANNED: + os<<"failure(member banned)"; + break; + case XMPP_CHATROOM_ENTERED_FAILURE_MAX_USERS: + os<<"failure(max users)"; + break; + case XMPP_CHATROOM_ENTERED_FAILURE_ROOM_LOCKED: + os<<"failure(room locked)"; + break; + case XMPP_CHATROOM_ENTERED_FAILURE_UNSPECIFIED: + os<<"failure(unspecified)"; + break; + default: + os<<"unknown"; + break; + } +} + +static void +WriteExitedStatus(std::ostream& os, XmppChatroomExitedStatus status) { + switch (status) { + case XMPP_CHATROOM_EXITED_REQUESTED: + os<<"requested"; + break; + case XMPP_CHATROOM_EXITED_BANNED: + os<<"banned"; + break; + case XMPP_CHATROOM_EXITED_KICKED: + os<<"kicked"; + break; + case XMPP_CHATROOM_EXITED_NOT_A_MEMBER: + os<<"not member"; + break; + case XMPP_CHATROOM_EXITED_SYSTEM_SHUTDOWN: + os<<"system shutdown"; + break; + case XMPP_CHATROOM_EXITED_UNSPECIFIED: + os<<"unspecified"; + break; + default: + os<<"unknown"; + break; + } +} + +//! This session handler saves all calls to a string. These are events and +//! data delivered form the engine to application code. +class XmppTestChatroomHandler : public XmppChatroomHandler { +public: + XmppTestChatroomHandler() {} + virtual ~XmppTestChatroomHandler() {} + + void ChatroomEnteredStatus(XmppChatroomModule* room, + XmppChatroomEnteredStatus status) { + UNUSED(room); + ss_ <<"[ChatroomEnteredStatus status: "; + WriteEnteredStatus(ss_, status); + ss_ <<"]"; + } + + + void ChatroomExitedStatus(XmppChatroomModule* room, + XmppChatroomExitedStatus status) { + UNUSED(room); + ss_ <<"[ChatroomExitedStatus status: "; + WriteExitedStatus(ss_, status); + ss_ <<"]"; + } + + void MemberEntered(XmppChatroomModule* room, + const XmppChatroomMember* entered_member) { + UNUSED(room); + ss_ << "[MemberEntered " << entered_member->member_jid().Str() << "]"; + } + + void MemberExited(XmppChatroomModule* room, + const XmppChatroomMember* exited_member) { + UNUSED(room); + ss_ << "[MemberExited " << exited_member->member_jid().Str() << "]"; + } + + void MemberChanged(XmppChatroomModule* room, + const XmppChatroomMember* changed_member) { + UNUSED(room); + ss_ << "[MemberChanged " << changed_member->member_jid().Str() << "]"; + } + + virtual void MessageReceived(XmppChatroomModule* room, const XmlElement& message) { + UNUSED2(room, message); + } + + + std::string Str() { + return ss_.str(); + } + + std::string StrClear() { + std::string result = ss_.str(); + ss_.str(""); + return result; + } + +private: + std::stringstream ss_; +}; + +//! This is the class that holds all of the unit test code for the +//! roster module +class XmppChatroomModuleTest : public UnitTest { +public: + XmppChatroomModuleTest() {} + + void TestEnterExitChatroom() { + std::stringstream dump; + + // Configure the engine + scoped_ptr engine(XmppEngine::Create()); + XmppTestHandler handler(engine.get()); + + // Configure the module and handler + scoped_ptr chatroom(XmppChatroomModule::Create()); + + // Configure the module handler + chatroom->RegisterEngine(engine.get()); + + // Set up callbacks + engine->SetOutputHandler(&handler); + engine->AddStanzaHandler(&handler); + engine->SetSessionHandler(&handler); + + // Set up minimal login info + engine->SetUser(Jid("david@my-server")); + engine->SetPassword("david"); + + // Do the whole login handshake + RunLogin(this, engine.get(), &handler); + TEST_EQ("", handler.OutputActivity()); + + // Get the chatroom and set the handler + XmppTestChatroomHandler chatroom_handler; + chatroom->set_chatroom_handler(static_cast(&chatroom_handler)); + + // try to enter the chatroom + TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_NOT_IN_ROOM); + chatroom->set_nickname("thirdwitch"); + chatroom->set_chatroom_jid(Jid("darkcave@my-server")); + chatroom->RequestEnterChatroom("", XMPP_CONNECTION_STATUS_UNKNOWN, "en"); + TEST_EQ(chatroom_handler.StrClear(), ""); + TEST_EQ(handler.OutputActivity(), + "" + "" + ""); + TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_REQUESTED_ENTER); + + // simulate the server and test the client + std::string input; + input = "" + "" + "" + "" + ""; + TEST_OK(engine->HandleInput(input.c_str(), input.length())); + TEST_EQ(chatroom_handler.StrClear(), ""); + TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_REQUESTED_ENTER); + + input = "" + "" + "" + "" + ""; + TEST_OK(engine->HandleInput(input.c_str(), input.length())); + TEST_EQ(chatroom_handler.StrClear(), ""); + TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_REQUESTED_ENTER); + + input = "" + "" + "" + "" + ""; + TEST_OK(engine->HandleInput(input.c_str(), input.length())); + TEST_EQ(chatroom_handler.StrClear(), + "[ChatroomEnteredStatus status: success]"); + TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_IN_ROOM); + + // simulate somebody else entering the room after we entered + input = "" + "" + "" + "" + ""; + TEST_OK(engine->HandleInput(input.c_str(), input.length())); + TEST_EQ(chatroom_handler.StrClear(), "[MemberEntered darkcave@my-server/fourthwitch]"); + TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_IN_ROOM); + + // simulate somebody else leaving the room after we entered + input = "" + "" + "" + "" + ""; + TEST_OK(engine->HandleInput(input.c_str(), input.length())); + TEST_EQ(chatroom_handler.StrClear(), "[MemberExited darkcave@my-server/secondwitch]"); + TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_IN_ROOM); + + // try to leave the room + chatroom->RequestExitChatroom(); + TEST_EQ(chatroom_handler.StrClear(), ""); + TEST_EQ(handler.OutputActivity(), + ""); + TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_REQUESTED_EXIT); + + // simulate the server and test the client + input = "" + "" + "" + "" + ""; + TEST_OK(engine->HandleInput(input.c_str(), input.length())); + TEST_EQ(chatroom_handler.StrClear(), + "[ChatroomExitedStatus status: requested]"); + TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_NOT_IN_ROOM); + } + +}; + +// A global function that creates the test suite for this set of tests. +TestBase* ChatroomModuleTest_Create() { + TestSuite* suite = new TestSuite("ChatroomModuleTest"); + ADD_TEST(suite, XmppChatroomModuleTest, TestEnterExitChatroom); + return suite; +} + +} diff --git a/talk/xmpp/chatroommoduleimpl.cc b/talk/xmpp/chatroommoduleimpl.cc new file mode 100644 index 000000000..eb046d721 --- /dev/null +++ b/talk/xmpp/chatroommoduleimpl.cc @@ -0,0 +1,755 @@ +/* + * libjingle + * Copyright 2004--2005, 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 +#include +#include +#include +#include +#include +#include "talk/base/common.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/moduleimpl.h" +#include "talk/xmpp/chatroommodule.h" + +namespace buzz { + +// forward declarations +class XmppChatroomImpl; +class XmppChatroomMemberImpl; + +//! Module that encapsulates multiple chatrooms. +//! Each chatroom is represented by an XmppChatroomImpl instance +class XmppChatroomModuleImpl : public XmppChatroomModule, + public XmppModuleImpl, public XmppIqHandler { +public: + IMPLEMENT_XMPPMODULE + + // Creates a chatroom with specified Jid + XmppChatroomModuleImpl(); + ~XmppChatroomModuleImpl(); + + // XmppChatroomModule + virtual XmppReturnStatus set_chatroom_handler(XmppChatroomHandler* handler); + virtual XmppChatroomHandler* chatroom_handler(); + virtual XmppReturnStatus set_chatroom_jid(const Jid& chatroom_jid); + virtual const Jid& chatroom_jid() const; + virtual XmppReturnStatus set_nickname(const std::string& nickname); + virtual const std::string& nickname() const; + virtual const Jid member_jid() const; + virtual XmppReturnStatus RequestEnterChatroom(const std::string& password, + const std::string& client_version, + const std::string& locale); + virtual XmppReturnStatus RequestExitChatroom(); + virtual XmppReturnStatus RequestConnectionStatusChange( + XmppPresenceConnectionStatus connection_status); + virtual size_t GetChatroomMemberCount(); + virtual XmppReturnStatus CreateMemberEnumerator(XmppChatroomMemberEnumerator** enumerator); + virtual const std::string subject(); + virtual XmppChatroomState state() { return chatroom_state_; } + virtual XmppReturnStatus SendMessage(const XmlElement& message); + + // XmppModule + virtual void IqResponse(XmppIqCookie cookie, const XmlElement * pelStanza) {UNUSED2(cookie, pelStanza);} + virtual bool HandleStanza(const XmlElement *); + +private: + friend class XmppChatroomMemberEnumeratorImpl; + + XmppReturnStatus ServerChangeMyPresence(const XmlElement& presence); + XmppReturnStatus ClientChangeMyPresence(XmppChatroomState new_state); + XmppReturnStatus ChangePresence(XmppChatroomState new_state, const XmlElement* presence, bool isServer); + XmppReturnStatus ServerChangedOtherPresence(const XmlElement& presence_element); + XmppChatroomEnteredStatus GetEnterFailureFromXml(const XmlElement* presence); + XmppChatroomExitedStatus GetExitFailureFromXml(const XmlElement* presence); + + bool CheckEnterChatroomStateOk(); + + void FireEnteredStatus(const XmlElement* presence, + XmppChatroomEnteredStatus status); + void FireExitStatus(XmppChatroomExitedStatus status); + void FireMessageReceived(const XmlElement& message); + void FireMemberEntered(const XmppChatroomMember* entered_member); + void FireMemberChanged(const XmppChatroomMember* changed_member); + void FireMemberExited(const XmppChatroomMember* exited_member); + + + typedef std::map JidMemberMap; + + XmppChatroomHandler* chatroom_handler_; + Jid chatroom_jid_; + std::string nickname_; + XmppChatroomState chatroom_state_; + JidMemberMap chatroom_jid_members_; + int chatroom_jid_members_version_; +}; + + +class XmppChatroomMemberImpl : public XmppChatroomMember { +public: + ~XmppChatroomMemberImpl() {} + XmppReturnStatus SetPresence(const XmppPresence* presence); + + // XmppChatroomMember + const Jid member_jid() const; + const Jid full_jid() const; + const std::string name() const; + const XmppPresence* presence() const; + +private: + talk_base::scoped_ptr presence_; +}; + +class XmppChatroomMemberEnumeratorImpl : + public XmppChatroomMemberEnumerator { +public: + XmppChatroomMemberEnumeratorImpl(XmppChatroomModuleImpl::JidMemberMap* chatroom_jid_members, + int* map_version); + + // XmppChatroomMemberEnumerator + virtual XmppChatroomMember* current(); + virtual bool Next(); + virtual bool Prev(); + virtual bool IsValid(); + virtual bool IsBeforeBeginning(); + virtual bool IsAfterEnd(); + +private: + XmppChatroomModuleImpl::JidMemberMap* map_; + int map_version_created_; + int* map_version_; + XmppChatroomModuleImpl::JidMemberMap::iterator iterator_; + bool before_beginning_; +}; + + +// XmppChatroomModuleImpl ------------------------------------------------ +XmppChatroomModule * +XmppChatroomModule::Create() { + return new XmppChatroomModuleImpl(); +} + +XmppChatroomModuleImpl::XmppChatroomModuleImpl() : + chatroom_handler_(NULL), + chatroom_jid_(STR_EMPTY), + chatroom_state_(XMPP_CHATROOM_STATE_NOT_IN_ROOM), + chatroom_jid_members_version_(0) { +} + +XmppChatroomModuleImpl::~XmppChatroomModuleImpl() { + JidMemberMap::iterator iterator = chatroom_jid_members_.begin(); + while (iterator != chatroom_jid_members_.end()) { + delete iterator->second; + iterator++; + } +} + + +bool +XmppChatroomModuleImpl::HandleStanza(const XmlElement* stanza) { + ASSERT(engine() != NULL); + + // we handle stanzas that are for one of our chatrooms + Jid from_jid = Jid(stanza->Attr(QN_FROM)); + // see if it's one of our chatrooms + if (chatroom_jid_ != from_jid.BareJid()) { + return false; // not one of our chatrooms + } else { + // handle presence stanza + if (stanza->Name() == QN_PRESENCE) { + if (from_jid == member_jid()) { + ServerChangeMyPresence(*stanza); + } else { + ServerChangedOtherPresence(*stanza); + } + } else if (stanza->Name() == QN_MESSAGE) { + FireMessageReceived(*stanza); + } + return true; + } +} + + +XmppReturnStatus +XmppChatroomModuleImpl::set_chatroom_handler(XmppChatroomHandler* handler) { + // Calling with NULL removes the handler. + chatroom_handler_ = handler; + return XMPP_RETURN_OK; +} + + +XmppChatroomHandler* +XmppChatroomModuleImpl::chatroom_handler() { + return chatroom_handler_; +} + +XmppReturnStatus +XmppChatroomModuleImpl::set_chatroom_jid(const Jid& chatroom_jid) { + if (chatroom_state_ != XMPP_CHATROOM_STATE_NOT_IN_ROOM) { + return XMPP_RETURN_BADSTATE; // $TODO - this isn't a bad state, it's a bad call, diff error code? + } + if (chatroom_jid != chatroom_jid.BareJid()) { + // chatroom_jid must be a bare jid + return XMPP_RETURN_BADARGUMENT; + } + + chatroom_jid_ = chatroom_jid; + return XMPP_RETURN_OK; +} + +const Jid& +XmppChatroomModuleImpl::chatroom_jid() const { + return chatroom_jid_; +} + + XmppReturnStatus + XmppChatroomModuleImpl::set_nickname(const std::string& nickname) { + if (chatroom_state_ != XMPP_CHATROOM_STATE_NOT_IN_ROOM) { + return XMPP_RETURN_BADSTATE; // $TODO - this isn't a bad state, it's a bad call, diff error code? + } + nickname_ = nickname; + return XMPP_RETURN_OK; + } + + const std::string& + XmppChatroomModuleImpl::nickname() const { + return nickname_; + } + +const Jid +XmppChatroomModuleImpl::member_jid() const { + return Jid(chatroom_jid_.node(), chatroom_jid_.domain(), nickname_); +} + + +bool +XmppChatroomModuleImpl::CheckEnterChatroomStateOk() { + if (chatroom_jid_.IsValid() == false) { + ASSERT(0); + return false; + } + if (nickname_ == STR_EMPTY) { + ASSERT(0); + return false; + } + return true; +} + +std::string GetAttrValueFor(XmppPresenceConnectionStatus connection_status) { + switch (connection_status) { + default: + case XMPP_CONNECTION_STATUS_UNKNOWN: + return ""; + case XMPP_CONNECTION_STATUS_CONNECTING: + return STR_PSTN_CONFERENCE_STATUS_CONNECTING; + case XMPP_CONNECTION_STATUS_CONNECTED: + return STR_PSTN_CONFERENCE_STATUS_CONNECTED; + } +} + +XmppReturnStatus +XmppChatroomModuleImpl::RequestEnterChatroom( + const std::string& password, + const std::string& client_version, + const std::string& locale) { + UNUSED(password); + if (!engine()) + return XMPP_RETURN_BADSTATE; + + if (chatroom_state_ != XMPP_CHATROOM_STATE_NOT_IN_ROOM) + return XMPP_RETURN_BADSTATE; // $TODO - this isn't a bad state, it's a bad call, diff error code? + + if (CheckEnterChatroomStateOk() == false) { + return XMPP_RETURN_BADSTATE; + } + + // entering a chatroom is a presence request to the server + XmlElement element(QN_PRESENCE); + element.AddAttr(QN_TO, member_jid().Str()); + + XmlElement* muc_x = new XmlElement(QN_MUC_X); + element.AddElement(muc_x); + + if (!client_version.empty()) { + XmlElement* client_version_element = new XmlElement(QN_CLIENT_VERSION, + false); + client_version_element->SetBodyText(client_version); + muc_x->AddElement(client_version_element); + } + + if (!locale.empty()) { + XmlElement* locale_element = new XmlElement(QN_LOCALE, false); + + locale_element->SetBodyText(locale); + muc_x->AddElement(locale_element); + } + + XmppReturnStatus status = engine()->SendStanza(&element); + if (status == XMPP_RETURN_OK) { + return ClientChangeMyPresence(XMPP_CHATROOM_STATE_REQUESTED_ENTER); + } + return status; +} + +XmppReturnStatus +XmppChatroomModuleImpl::RequestExitChatroom() { + if (!engine()) + return XMPP_RETURN_BADSTATE; + + // currently, can't leave a room unless you've entered + // no way to cancel a pending enter call - is that bad? + if (chatroom_state_ != XMPP_CHATROOM_STATE_IN_ROOM) + return XMPP_RETURN_BADSTATE; // $TODO - this isn't a bad state, it's a bad call, diff error code? + + // exiting a chatroom is a presence request to the server + XmlElement element(QN_PRESENCE); + element.AddAttr(QN_TO, member_jid().Str()); + element.AddAttr(QN_TYPE, "unavailable"); + XmppReturnStatus status = engine()->SendStanza(&element); + if (status == XMPP_RETURN_OK) { + return ClientChangeMyPresence(XMPP_CHATROOM_STATE_REQUESTED_EXIT); + } + return status; +} + +XmppReturnStatus +XmppChatroomModuleImpl::RequestConnectionStatusChange( + XmppPresenceConnectionStatus connection_status) { + if (!engine()) + return XMPP_RETURN_BADSTATE; + + if (chatroom_state_ != XMPP_CHATROOM_STATE_IN_ROOM) { + // $TODO - this isn't a bad state, it's a bad call, diff error code? + return XMPP_RETURN_BADSTATE; + } + + if (CheckEnterChatroomStateOk() == false) { + return XMPP_RETURN_BADSTATE; + } + + // entering a chatroom is a presence request to the server + XmlElement element(QN_PRESENCE); + element.AddAttr(QN_TO, member_jid().Str()); + element.AddElement(new XmlElement(QN_MUC_X)); + if (connection_status != XMPP_CONNECTION_STATUS_UNKNOWN) { + XmlElement* con_status_element = + new XmlElement(QN_GOOGLE_PSTN_CONFERENCE_STATUS); + con_status_element->AddAttr(QN_STATUS, GetAttrValueFor(connection_status)); + element.AddElement(con_status_element); + } + XmppReturnStatus status = engine()->SendStanza(&element); + + return status; +} + +size_t +XmppChatroomModuleImpl::GetChatroomMemberCount() { + return chatroom_jid_members_.size(); +} + +XmppReturnStatus +XmppChatroomModuleImpl::CreateMemberEnumerator(XmppChatroomMemberEnumerator** enumerator) { + *enumerator = new XmppChatroomMemberEnumeratorImpl(&chatroom_jid_members_, &chatroom_jid_members_version_); + return XMPP_RETURN_OK; +} + +const std::string +XmppChatroomModuleImpl::subject() { + return ""; //NYI +} + +XmppReturnStatus +XmppChatroomModuleImpl::SendMessage(const XmlElement& message) { + XmppReturnStatus xmpp_status = XMPP_RETURN_OK; + + // can only send a message if we're in the room + if (chatroom_state_ != XMPP_CHATROOM_STATE_IN_ROOM) { + return XMPP_RETURN_BADSTATE; // $TODO - this isn't a bad state, it's a bad call, diff error code? + } + + if (message.Name() != QN_MESSAGE) { + IFR(XMPP_RETURN_BADARGUMENT); + } + + const std::string& type = message.Attr(QN_TYPE); + if (type != "groupchat") { + IFR(XMPP_RETURN_BADARGUMENT); + } + + if (message.HasAttr(QN_FROM)) { + IFR(XMPP_RETURN_BADARGUMENT); + } + + if (message.Attr(QN_TO) != chatroom_jid_.Str()) { + IFR(XMPP_RETURN_BADARGUMENT); + } + + IFR(engine()->SendStanza(&message)); + + return xmpp_status; +} + +enum TransitionType { + TRANSITION_TYPE_NONE = 0, + TRANSITION_TYPE_ENTER_SUCCESS = 1, + TRANSITION_TYPE_ENTER_FAILURE = 2, + TRANSITION_TYPE_EXIT_VOLUNTARILY = 3, + TRANSITION_TYPE_EXIT_INVOLUNTARILY = 4, +}; + +struct StateTransitionDescription { + XmppChatroomState old_state; + XmppChatroomState new_state; + bool is_valid_server_transition; + bool is_valid_client_transition; + TransitionType transition_type; +}; + +StateTransitionDescription Transitions[] = { + { XMPP_CHATROOM_STATE_NOT_IN_ROOM, XMPP_CHATROOM_STATE_REQUESTED_ENTER, false, true, TRANSITION_TYPE_NONE, }, + { XMPP_CHATROOM_STATE_NOT_IN_ROOM, XMPP_CHATROOM_STATE_IN_ROOM, false, false, TRANSITION_TYPE_ENTER_SUCCESS, }, + { XMPP_CHATROOM_STATE_NOT_IN_ROOM, XMPP_CHATROOM_STATE_REQUESTED_EXIT, false, false, TRANSITION_TYPE_NONE, }, + { XMPP_CHATROOM_STATE_REQUESTED_ENTER, XMPP_CHATROOM_STATE_NOT_IN_ROOM, true, false, TRANSITION_TYPE_ENTER_FAILURE, }, + { XMPP_CHATROOM_STATE_REQUESTED_ENTER, XMPP_CHATROOM_STATE_IN_ROOM, true, false, TRANSITION_TYPE_ENTER_SUCCESS, }, + { XMPP_CHATROOM_STATE_REQUESTED_ENTER, XMPP_CHATROOM_STATE_REQUESTED_EXIT, false, false, TRANSITION_TYPE_NONE, }, + { XMPP_CHATROOM_STATE_IN_ROOM, XMPP_CHATROOM_STATE_NOT_IN_ROOM, true, false, TRANSITION_TYPE_EXIT_INVOLUNTARILY, }, + { XMPP_CHATROOM_STATE_IN_ROOM, XMPP_CHATROOM_STATE_REQUESTED_ENTER, false, false, TRANSITION_TYPE_NONE, }, + { XMPP_CHATROOM_STATE_IN_ROOM, XMPP_CHATROOM_STATE_REQUESTED_EXIT, false, true, TRANSITION_TYPE_NONE, }, + { XMPP_CHATROOM_STATE_REQUESTED_EXIT, XMPP_CHATROOM_STATE_NOT_IN_ROOM, true, false, TRANSITION_TYPE_EXIT_VOLUNTARILY, }, + { XMPP_CHATROOM_STATE_REQUESTED_EXIT, XMPP_CHATROOM_STATE_REQUESTED_ENTER, false, false, TRANSITION_TYPE_NONE, }, + { XMPP_CHATROOM_STATE_REQUESTED_EXIT, XMPP_CHATROOM_STATE_IN_ROOM, false, false, TRANSITION_TYPE_NONE, }, +}; + + + +void +XmppChatroomModuleImpl::FireEnteredStatus(const XmlElement* presence, + XmppChatroomEnteredStatus status) { + if (chatroom_handler_) { + talk_base::scoped_ptr xmpp_presence(XmppPresence::Create()); + xmpp_presence->set_raw_xml(presence); + chatroom_handler_->ChatroomEnteredStatus(this, xmpp_presence.get(), status); + } +} + +void +XmppChatroomModuleImpl::FireExitStatus(XmppChatroomExitedStatus status) { + if (chatroom_handler_) + chatroom_handler_->ChatroomExitedStatus(this, status); +} + +void +XmppChatroomModuleImpl::FireMessageReceived(const XmlElement& message) { + if (chatroom_handler_) + chatroom_handler_->MessageReceived(this, message); +} + +void +XmppChatroomModuleImpl::FireMemberEntered(const XmppChatroomMember* entered_member) { + if (chatroom_handler_) + chatroom_handler_->MemberEntered(this, entered_member); +} + +void +XmppChatroomModuleImpl::FireMemberChanged( + const XmppChatroomMember* changed_member) { + if (chatroom_handler_) + chatroom_handler_->MemberChanged(this, changed_member); +} + +void +XmppChatroomModuleImpl::FireMemberExited(const XmppChatroomMember* exited_member) { + if (chatroom_handler_) + chatroom_handler_->MemberExited(this, exited_member); +} + + +XmppReturnStatus +XmppChatroomModuleImpl::ServerChangedOtherPresence(const XmlElement& + presence_element) { + XmppReturnStatus xmpp_status = XMPP_RETURN_OK; + talk_base::scoped_ptr presence(XmppPresence::Create()); + IFR(presence->set_raw_xml(&presence_element)); + + JidMemberMap::iterator pos = chatroom_jid_members_.find(presence->jid()); + + if (pos == chatroom_jid_members_.end()) { + if (presence->available() == XMPP_PRESENCE_AVAILABLE) { + XmppChatroomMemberImpl* member = new XmppChatroomMemberImpl(); + member->SetPresence(presence.get()); + chatroom_jid_members_.insert(std::make_pair(member->member_jid(), member)); + chatroom_jid_members_version_++; + FireMemberEntered(member); + } + } else { + XmppChatroomMemberImpl* member = pos->second; + if (presence->available() == XMPP_PRESENCE_AVAILABLE) { + member->SetPresence(presence.get()); + chatroom_jid_members_version_++; + FireMemberChanged(member); + } + else if (presence->available() == XMPP_PRESENCE_UNAVAILABLE) { + chatroom_jid_members_.erase(pos); + chatroom_jid_members_version_++; + FireMemberExited(member); + delete member; + } + } + + return xmpp_status; +} + +XmppReturnStatus +XmppChatroomModuleImpl::ClientChangeMyPresence(XmppChatroomState new_state) { + return ChangePresence(new_state, NULL, false); +} + +XmppReturnStatus +XmppChatroomModuleImpl::ServerChangeMyPresence(const XmlElement& presence) { + XmppChatroomState new_state; + + if (presence.HasAttr(QN_TYPE) == false) { + new_state = XMPP_CHATROOM_STATE_IN_ROOM; + } else { + new_state = XMPP_CHATROOM_STATE_NOT_IN_ROOM; + } + return ChangePresence(new_state, &presence, true); + +} + +XmppReturnStatus +XmppChatroomModuleImpl::ChangePresence(XmppChatroomState new_state, + const XmlElement* presence, + bool isServer) { + UNUSED(presence); + + XmppChatroomState old_state = chatroom_state_; + + // do nothing if state hasn't changed + if (old_state == new_state) + return XMPP_RETURN_OK; + + // find the right transition description + StateTransitionDescription* transition_desc = NULL; + for (int i=0; i < ARRAY_SIZE(Transitions); i++) { + if (Transitions[i].old_state == old_state && + Transitions[i].new_state == new_state) { + transition_desc = &Transitions[i]; + break; + } + } + + if (transition_desc == NULL) { + ASSERT(0); + return XMPP_RETURN_BADSTATE; + } + + // we assert for any invalid transition states, and we'll + if (isServer) { + // $TODO send original stanza back to server and log an error? + // Disable the assert because of b/6133072 + // ASSERT(transition_desc->is_valid_server_transition); + if (!transition_desc->is_valid_server_transition) { + return XMPP_RETURN_BADSTATE; + } + } else { + if (transition_desc->is_valid_client_transition == false) { + ASSERT(0); + return XMPP_RETURN_BADARGUMENT; + } + } + + // set the new state and then fire any notifications to the handler + chatroom_state_ = new_state; + + switch (transition_desc->transition_type) { + case TRANSITION_TYPE_ENTER_SUCCESS: + FireEnteredStatus(presence, XMPP_CHATROOM_ENTERED_SUCCESS); + break; + case TRANSITION_TYPE_ENTER_FAILURE: + FireEnteredStatus(presence, GetEnterFailureFromXml(presence)); + break; + case TRANSITION_TYPE_EXIT_INVOLUNTARILY: + FireExitStatus(GetExitFailureFromXml(presence)); + break; + case TRANSITION_TYPE_EXIT_VOLUNTARILY: + FireExitStatus(XMPP_CHATROOM_EXITED_REQUESTED); + break; + case TRANSITION_TYPE_NONE: + break; + } + + return XMPP_RETURN_OK; +} + +XmppChatroomEnteredStatus +XmppChatroomModuleImpl::GetEnterFailureFromXml(const XmlElement* presence) { + XmppChatroomEnteredStatus status = XMPP_CHATROOM_ENTERED_FAILURE_UNSPECIFIED; + const XmlElement* error = presence->FirstNamed(QN_ERROR); + if (error != NULL && error->HasAttr(QN_CODE)) { + int code = atoi(error->Attr(QN_CODE).c_str()); + switch (code) { + case 401: status = XMPP_CHATROOM_ENTERED_FAILURE_PASSWORD_REQUIRED; break; + case 403: { + status = XMPP_CHATROOM_ENTERED_FAILURE_MEMBER_BANNED; + if (error->FirstNamed(QN_GOOGLE_SESSION_BLOCKED)) { + status = XMPP_CHATROOM_ENTERED_FAILURE_MEMBER_BLOCKED; + } else if (error->FirstNamed(QN_GOOGLE_SESSION_BLOCKING)) { + status = XMPP_CHATROOM_ENTERED_FAILURE_MEMBER_BLOCKING; + } + break; + } + case 405: status = XMPP_CHATROOM_ENTERED_FAILURE_ROOM_LOCKED; break; + case 406: status = XMPP_CHATROOM_ENTERED_FAILURE_OUTDATED_CLIENT; break; + case 407: status = XMPP_CHATROOM_ENTERED_FAILURE_NOT_A_MEMBER; break; + case 409: status = XMPP_CHATROOM_ENTERED_FAILURE_NICKNAME_CONFLICT; break; + // http://xmpp.org/extensions/xep-0045.html#enter-maxusers + case 503: status = XMPP_CHATROOM_ENTERED_FAILURE_MAX_USERS; break; + } + } + return status; +} + +XmppChatroomExitedStatus +XmppChatroomModuleImpl::GetExitFailureFromXml(const XmlElement* presence) { + XmppChatroomExitedStatus status = XMPP_CHATROOM_EXITED_UNSPECIFIED; + const XmlElement* muc_user = presence->FirstNamed(QN_MUC_USER_X); + if (muc_user != NULL) { + const XmlElement* user_status = muc_user->FirstNamed(QN_MUC_USER_STATUS); + if (user_status != NULL && user_status->HasAttr(QN_CODE)) { + int code = atoi(user_status->Attr(QN_CODE).c_str()); + switch (code) { + case 307: status = XMPP_CHATROOM_EXITED_KICKED; break; + case 322: status = XMPP_CHATROOM_EXITED_NOT_A_MEMBER; break; + case 332: status = XMPP_CHATROOM_EXITED_SYSTEM_SHUTDOWN; break; + } + } + } + return status; +} + +XmppReturnStatus +XmppChatroomMemberImpl::SetPresence(const XmppPresence* presence) { + ASSERT(presence != NULL); + + // copy presence + presence_.reset(XmppPresence::Create()); + presence_->set_raw_xml(presence->raw_xml()); + return XMPP_RETURN_OK; +} + +const Jid +XmppChatroomMemberImpl::member_jid() const { + return presence_->jid(); +} + +const Jid +XmppChatroomMemberImpl::full_jid() const { + return Jid(""); +} + +const std::string +XmppChatroomMemberImpl::name() const { + return member_jid().resource(); +} + +const XmppPresence* +XmppChatroomMemberImpl::presence() const { + return presence_.get(); +} + + +// XmppChatroomMemberEnumeratorImpl -------------------------------------- +XmppChatroomMemberEnumeratorImpl::XmppChatroomMemberEnumeratorImpl( + XmppChatroomModuleImpl::JidMemberMap* map, int* map_version) { + map_ = map; + map_version_ = map_version; + map_version_created_ = *map_version_; + iterator_ = map->begin(); + before_beginning_ = true; +} + +XmppChatroomMember* +XmppChatroomMemberEnumeratorImpl::current() { + if (IsValid() == false) { + return NULL; + } else if (IsBeforeBeginning() || IsAfterEnd()) { + return NULL; + } else { + return iterator_->second; + } +} + +bool +XmppChatroomMemberEnumeratorImpl::Prev() { + if (IsValid() == false) { + return false; + } else if (IsBeforeBeginning()) { + return false; + } else if (iterator_ == map_->begin()) { + before_beginning_ = true; + return false; + } else { + iterator_--; + return current() != NULL; + } +} + +bool +XmppChatroomMemberEnumeratorImpl::Next() { + if (IsValid() == false) { + return false; + } else if (IsBeforeBeginning()) { + before_beginning_ = false; + iterator_ = map_->begin(); + return current() != NULL; + } else if (IsAfterEnd()) { + return false; + } else { + iterator_++; + return current() != NULL; + } +} + +bool +XmppChatroomMemberEnumeratorImpl::IsValid() { + return map_version_created_ == *map_version_; +} + +bool +XmppChatroomMemberEnumeratorImpl::IsBeforeBeginning() { + return before_beginning_; +} + +bool +XmppChatroomMemberEnumeratorImpl::IsAfterEnd() { + return (iterator_ == map_->end()); +} + + + +} // namespace buzz diff --git a/talk/xmpp/constants.cc b/talk/xmpp/constants.cc new file mode 100644 index 000000000..193ae2bd9 --- /dev/null +++ b/talk/xmpp/constants.cc @@ -0,0 +1,608 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmpp/constants.h" + +#include + +#include "talk/base/basicdefs.h" +#include "talk/xmllite/xmlconstants.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmllite/qname.h" +#include "talk/xmpp/jid.h" + +namespace buzz { + +// TODO: Remove static objects of complex types, particularly +// Jid and QName. + +const char NS_CLIENT[] = "jabber:client"; +const char NS_SERVER[] = "jabber:server"; +const char NS_STREAM[] = "http://etherx.jabber.org/streams"; +const char NS_XSTREAM[] = "urn:ietf:params:xml:ns:xmpp-streams"; +const char NS_TLS[] = "urn:ietf:params:xml:ns:xmpp-tls"; +const char NS_SASL[] = "urn:ietf:params:xml:ns:xmpp-sasl"; +const char NS_BIND[] = "urn:ietf:params:xml:ns:xmpp-bind"; +const char NS_DIALBACK[] = "jabber:server:dialback"; +const char NS_SESSION[] = "urn:ietf:params:xml:ns:xmpp-session"; +const char NS_STANZA[] = "urn:ietf:params:xml:ns:xmpp-stanzas"; +const char NS_PRIVACY[] = "jabber:iq:privacy"; +const char NS_ROSTER[] = "jabber:iq:roster"; +const char NS_VCARD[] = "vcard-temp"; +const char NS_AVATAR_HASH[] = "google:avatar"; +const char NS_VCARD_UPDATE[] = "vcard-temp:x:update"; +const char STR_CLIENT[] = "client"; +const char STR_SERVER[] = "server"; +const char STR_STREAM[] = "stream"; + +const char STR_GET[] = "get"; +const char STR_SET[] = "set"; +const char STR_RESULT[] = "result"; +const char STR_ERROR[] = "error"; + +const char STR_FORM[] = "form"; +const char STR_SUBMIT[] = "submit"; +const char STR_TEXT_SINGLE[] = "text-single"; +const char STR_LIST_SINGLE[] = "list-single"; +const char STR_LIST_MULTI[] = "list-multi"; +const char STR_HIDDEN[] = "hidden"; +const char STR_FORM_TYPE[] = "FORM_TYPE"; + +const char STR_FROM[] = "from"; +const char STR_TO[] = "to"; +const char STR_BOTH[] = "both"; +const char STR_REMOVE[] = "remove"; +const char STR_TRUE[] = "true"; + +const char STR_TYPE[] = "type"; +const char STR_NAME[] = "name"; +const char STR_ID[] = "id"; +const char STR_JID[] = "jid"; +const char STR_SUBSCRIPTION[] = "subscription"; +const char STR_ASK[] = "ask"; +const char STR_X[] = "x"; +const char STR_GOOGLE_COM[] = "google.com"; +const char STR_GMAIL_COM[] = "gmail.com"; +const char STR_GOOGLEMAIL_COM[] = "googlemail.com"; +const char STR_DEFAULT_DOMAIN[] = "default.talk.google.com"; +const char STR_TALK_GOOGLE_COM[] = "talk.google.com"; +const char STR_TALKX_L_GOOGLE_COM[] = "talkx.l.google.com"; +const char STR_XMPP_GOOGLE_COM[] = "xmpp.google.com"; +const char STR_XMPPX_L_GOOGLE_COM[] = "xmppx.l.google.com"; + +#ifdef FEATURE_ENABLE_VOICEMAIL +const char STR_VOICEMAIL[] = "voicemail"; +const char STR_OUTGOINGVOICEMAIL[] = "outgoingvoicemail"; +#endif + +const char STR_UNAVAILABLE[] = "unavailable"; + +const char NS_PING[] = "urn:xmpp:ping"; +const StaticQName QN_PING = { NS_PING, "ping" }; + +const char NS_MUC_UNIQUE[] = "http://jabber.org/protocol/muc#unique"; +const StaticQName QN_MUC_UNIQUE_QUERY = { NS_MUC_UNIQUE, "unique" }; +const StaticQName QN_HANGOUT_ID = { STR_EMPTY, "hangout-id" }; + +const char STR_GOOGLE_MUC_LOOKUP_JID[] = "lookup.groupchat.google.com"; + +const char STR_MUC_ROOMCONFIG_ROOMNAME[] = "muc#roomconfig_roomname"; +const char STR_MUC_ROOMCONFIG_FEATURES[] = "muc#roomconfig_features"; +const char STR_MUC_ROOM_FEATURE_ENTERPRISE[] = "muc_enterprise"; +const char STR_MUC_ROOMCONFIG[] = "http://jabber.org/protocol/muc#roomconfig"; +const char STR_MUC_ROOM_FEATURE_HANGOUT[] = "muc_es"; +const char STR_MUC_ROOM_FEATURE_HANGOUT_LITE[] = "muc_lite"; +const char STR_MUC_ROOM_FEATURE_BROADCAST[] = "broadcast"; +const char STR_MUC_ROOM_FEATURE_MULTI_USER_VC[] = "muc_muvc"; + +const StaticQName QN_STREAM_STREAM = { NS_STREAM, STR_STREAM }; +const StaticQName QN_STREAM_FEATURES = { NS_STREAM, "features" }; +const StaticQName QN_STREAM_ERROR = { NS_STREAM, "error" }; + +const StaticQName QN_XSTREAM_BAD_FORMAT = { NS_XSTREAM, "bad-format" }; +const StaticQName QN_XSTREAM_BAD_NAMESPACE_PREFIX = + { NS_XSTREAM, "bad-namespace-prefix" }; +const StaticQName QN_XSTREAM_CONFLICT = { NS_XSTREAM, "conflict" }; +const StaticQName QN_XSTREAM_CONNECTION_TIMEOUT = + { NS_XSTREAM, "connection-timeout" }; +const StaticQName QN_XSTREAM_HOST_GONE = { NS_XSTREAM, "host-gone" }; +const StaticQName QN_XSTREAM_HOST_UNKNOWN = { NS_XSTREAM, "host-unknown" }; +const StaticQName QN_XSTREAM_IMPROPER_ADDRESSIING = + { NS_XSTREAM, "improper-addressing" }; +const StaticQName QN_XSTREAM_INTERNAL_SERVER_ERROR = + { NS_XSTREAM, "internal-server-error" }; +const StaticQName QN_XSTREAM_INVALID_FROM = { NS_XSTREAM, "invalid-from" }; +const StaticQName QN_XSTREAM_INVALID_ID = { NS_XSTREAM, "invalid-id" }; +const StaticQName QN_XSTREAM_INVALID_NAMESPACE = + { NS_XSTREAM, "invalid-namespace" }; +const StaticQName QN_XSTREAM_INVALID_XML = { NS_XSTREAM, "invalid-xml" }; +const StaticQName QN_XSTREAM_NOT_AUTHORIZED = { NS_XSTREAM, "not-authorized" }; +const StaticQName QN_XSTREAM_POLICY_VIOLATION = + { NS_XSTREAM, "policy-violation" }; +const StaticQName QN_XSTREAM_REMOTE_CONNECTION_FAILED = + { NS_XSTREAM, "remote-connection-failed" }; +const StaticQName QN_XSTREAM_RESOURCE_CONSTRAINT = + { NS_XSTREAM, "resource-constraint" }; +const StaticQName QN_XSTREAM_RESTRICTED_XML = { NS_XSTREAM, "restricted-xml" }; +const StaticQName QN_XSTREAM_SEE_OTHER_HOST = { NS_XSTREAM, "see-other-host" }; +const StaticQName QN_XSTREAM_SYSTEM_SHUTDOWN = + { NS_XSTREAM, "system-shutdown" }; +const StaticQName QN_XSTREAM_UNDEFINED_CONDITION = + { NS_XSTREAM, "undefined-condition" }; +const StaticQName QN_XSTREAM_UNSUPPORTED_ENCODING = + { NS_XSTREAM, "unsupported-encoding" }; +const StaticQName QN_XSTREAM_UNSUPPORTED_STANZA_TYPE = + { NS_XSTREAM, "unsupported-stanza-type" }; +const StaticQName QN_XSTREAM_UNSUPPORTED_VERSION = + { NS_XSTREAM, "unsupported-version" }; +const StaticQName QN_XSTREAM_XML_NOT_WELL_FORMED = + { NS_XSTREAM, "xml-not-well-formed" }; +const StaticQName QN_XSTREAM_TEXT = { NS_XSTREAM, "text" }; + +const StaticQName QN_TLS_STARTTLS = { NS_TLS, "starttls" }; +const StaticQName QN_TLS_REQUIRED = { NS_TLS, "required" }; +const StaticQName QN_TLS_PROCEED = { NS_TLS, "proceed" }; +const StaticQName QN_TLS_FAILURE = { NS_TLS, "failure" }; + +const StaticQName QN_SASL_MECHANISMS = { NS_SASL, "mechanisms" }; +const StaticQName QN_SASL_MECHANISM = { NS_SASL, "mechanism" }; +const StaticQName QN_SASL_AUTH = { NS_SASL, "auth" }; +const StaticQName QN_SASL_CHALLENGE = { NS_SASL, "challenge" }; +const StaticQName QN_SASL_RESPONSE = { NS_SASL, "response" }; +const StaticQName QN_SASL_ABORT = { NS_SASL, "abort" }; +const StaticQName QN_SASL_SUCCESS = { NS_SASL, "success" }; +const StaticQName QN_SASL_FAILURE = { NS_SASL, "failure" }; +const StaticQName QN_SASL_ABORTED = { NS_SASL, "aborted" }; +const StaticQName QN_SASL_INCORRECT_ENCODING = + { NS_SASL, "incorrect-encoding" }; +const StaticQName QN_SASL_INVALID_AUTHZID = { NS_SASL, "invalid-authzid" }; +const StaticQName QN_SASL_INVALID_MECHANISM = { NS_SASL, "invalid-mechanism" }; +const StaticQName QN_SASL_MECHANISM_TOO_WEAK = + { NS_SASL, "mechanism-too-weak" }; +const StaticQName QN_SASL_NOT_AUTHORIZED = { NS_SASL, "not-authorized" }; +const StaticQName QN_SASL_TEMPORARY_AUTH_FAILURE = + { NS_SASL, "temporary-auth-failure" }; + +// These are non-standard. +const char NS_GOOGLE_AUTH_PROTOCOL[] = + "http://www.google.com/talk/protocol/auth"; +const StaticQName QN_GOOGLE_AUTH_CLIENT_USES_FULL_BIND_RESULT = + { NS_GOOGLE_AUTH_PROTOCOL, "client-uses-full-bind-result" }; +const char NS_GOOGLE_AUTH_OLD[] = "google:auth"; +const StaticQName QN_GOOGLE_ALLOW_NON_GOOGLE_ID_XMPP_LOGIN = + { NS_GOOGLE_AUTH_PROTOCOL, "allow-non-google-login" }; +const StaticQName QN_GOOGLE_AUTH_SERVICE = + { NS_GOOGLE_AUTH_PROTOCOL, "service" }; + +const StaticQName QN_DIALBACK_RESULT = { NS_DIALBACK, "result" }; +const StaticQName QN_DIALBACK_VERIFY = { NS_DIALBACK, "verify" }; + +const StaticQName QN_STANZA_BAD_REQUEST = { NS_STANZA, "bad-request" }; +const StaticQName QN_STANZA_CONFLICT = { NS_STANZA, "conflict" }; +const StaticQName QN_STANZA_FEATURE_NOT_IMPLEMENTED = + { NS_STANZA, "feature-not-implemented" }; +const StaticQName QN_STANZA_FORBIDDEN = { NS_STANZA, "forbidden" }; +const StaticQName QN_STANZA_GONE = { NS_STANZA, "gone" }; +const StaticQName QN_STANZA_INTERNAL_SERVER_ERROR = + { NS_STANZA, "internal-server-error" }; +const StaticQName QN_STANZA_ITEM_NOT_FOUND = { NS_STANZA, "item-not-found" }; +const StaticQName QN_STANZA_JID_MALFORMED = { NS_STANZA, "jid-malformed" }; +const StaticQName QN_STANZA_NOT_ACCEPTABLE = { NS_STANZA, "not-acceptable" }; +const StaticQName QN_STANZA_NOT_ALLOWED = { NS_STANZA, "not-allowed" }; +const StaticQName QN_STANZA_PAYMENT_REQUIRED = + { NS_STANZA, "payment-required" }; +const StaticQName QN_STANZA_RECIPIENT_UNAVAILABLE = + { NS_STANZA, "recipient-unavailable" }; +const StaticQName QN_STANZA_REDIRECT = { NS_STANZA, "redirect" }; +const StaticQName QN_STANZA_REGISTRATION_REQUIRED = + { NS_STANZA, "registration-required" }; +const StaticQName QN_STANZA_REMOTE_SERVER_NOT_FOUND = + { NS_STANZA, "remote-server-not-found" }; +const StaticQName QN_STANZA_REMOTE_SERVER_TIMEOUT = + { NS_STANZA, "remote-server-timeout" }; +const StaticQName QN_STANZA_RESOURCE_CONSTRAINT = + { NS_STANZA, "resource-constraint" }; +const StaticQName QN_STANZA_SERVICE_UNAVAILABLE = + { NS_STANZA, "service-unavailable" }; +const StaticQName QN_STANZA_SUBSCRIPTION_REQUIRED = + { NS_STANZA, "subscription-required" }; +const StaticQName QN_STANZA_UNDEFINED_CONDITION = + { NS_STANZA, "undefined-condition" }; +const StaticQName QN_STANZA_UNEXPECTED_REQUEST = + { NS_STANZA, "unexpected-request" }; +const StaticQName QN_STANZA_TEXT = { NS_STANZA, "text" }; + +const StaticQName QN_BIND_BIND = { NS_BIND, "bind" }; +const StaticQName QN_BIND_RESOURCE = { NS_BIND, "resource" }; +const StaticQName QN_BIND_JID = { NS_BIND, "jid" }; + +const StaticQName QN_MESSAGE = { NS_CLIENT, "message" }; +const StaticQName QN_BODY = { NS_CLIENT, "body" }; +const StaticQName QN_SUBJECT = { NS_CLIENT, "subject" }; +const StaticQName QN_THREAD = { NS_CLIENT, "thread" }; +const StaticQName QN_PRESENCE = { NS_CLIENT, "presence" }; +const StaticQName QN_SHOW = { NS_CLIENT, "show" }; +const StaticQName QN_STATUS = { NS_CLIENT, "status" }; +const StaticQName QN_LANG = { NS_CLIENT, "lang" }; +const StaticQName QN_PRIORITY = { NS_CLIENT, "priority" }; +const StaticQName QN_IQ = { NS_CLIENT, "iq" }; +const StaticQName QN_ERROR = { NS_CLIENT, "error" }; + +const StaticQName QN_SERVER_MESSAGE = { NS_SERVER, "message" }; +const StaticQName QN_SERVER_BODY = { NS_SERVER, "body" }; +const StaticQName QN_SERVER_SUBJECT = { NS_SERVER, "subject" }; +const StaticQName QN_SERVER_THREAD = { NS_SERVER, "thread" }; +const StaticQName QN_SERVER_PRESENCE = { NS_SERVER, "presence" }; +const StaticQName QN_SERVER_SHOW = { NS_SERVER, "show" }; +const StaticQName QN_SERVER_STATUS = { NS_SERVER, "status" }; +const StaticQName QN_SERVER_LANG = { NS_SERVER, "lang" }; +const StaticQName QN_SERVER_PRIORITY = { NS_SERVER, "priority" }; +const StaticQName QN_SERVER_IQ = { NS_SERVER, "iq" }; +const StaticQName QN_SERVER_ERROR = { NS_SERVER, "error" }; + +const StaticQName QN_SESSION_SESSION = { NS_SESSION, "session" }; + +const StaticQName QN_PRIVACY_QUERY = { NS_PRIVACY, "query" }; +const StaticQName QN_PRIVACY_ACTIVE = { NS_PRIVACY, "active" }; +const StaticQName QN_PRIVACY_DEFAULT = { NS_PRIVACY, "default" }; +const StaticQName QN_PRIVACY_LIST = { NS_PRIVACY, "list" }; +const StaticQName QN_PRIVACY_ITEM = { NS_PRIVACY, "item" }; +const StaticQName QN_PRIVACY_IQ = { NS_PRIVACY, "iq" }; +const StaticQName QN_PRIVACY_MESSAGE = { NS_PRIVACY, "message" }; +const StaticQName QN_PRIVACY_PRESENCE_IN = { NS_PRIVACY, "presence-in" }; +const StaticQName QN_PRIVACY_PRESENCE_OUT = { NS_PRIVACY, "presence-out" }; + +const StaticQName QN_ROSTER_QUERY = { NS_ROSTER, "query" }; +const StaticQName QN_ROSTER_ITEM = { NS_ROSTER, "item" }; +const StaticQName QN_ROSTER_GROUP = { NS_ROSTER, "group" }; + +const StaticQName QN_VCARD = { NS_VCARD, "vCard" }; +const StaticQName QN_VCARD_FN = { NS_VCARD, "FN" }; +const StaticQName QN_VCARD_PHOTO = { NS_VCARD, "PHOTO" }; +const StaticQName QN_VCARD_PHOTO_BINVAL = { NS_VCARD, "BINVAL" }; +const StaticQName QN_VCARD_AVATAR_HASH = { NS_AVATAR_HASH, "hash" }; +const StaticQName QN_VCARD_AVATAR_HASH_MODIFIED = + { NS_AVATAR_HASH, "modified" }; + +const StaticQName QN_NAME = { STR_EMPTY, "name" }; +const StaticQName QN_AFFILIATION = { STR_EMPTY, "affiliation" }; +const StaticQName QN_ROLE = { STR_EMPTY, "role" }; + +#if defined(FEATURE_ENABLE_PSTN) +const StaticQName QN_VCARD_TEL = { NS_VCARD, "TEL" }; +const StaticQName QN_VCARD_VOICE = { NS_VCARD, "VOICE" }; +const StaticQName QN_VCARD_HOME = { NS_VCARD, "HOME" }; +const StaticQName QN_VCARD_WORK = { NS_VCARD, "WORK" }; +const StaticQName QN_VCARD_CELL = { NS_VCARD, "CELL" }; +const StaticQName QN_VCARD_NUMBER = { NS_VCARD, "NUMBER" }; +#endif + +const StaticQName QN_XML_LANG = { NS_XML, "lang" }; + +const StaticQName QN_ENCODING = { STR_EMPTY, STR_ENCODING }; +const StaticQName QN_VERSION = { STR_EMPTY, STR_VERSION }; +const StaticQName QN_TO = { STR_EMPTY, "to" }; +const StaticQName QN_FROM = { STR_EMPTY, "from" }; +const StaticQName QN_TYPE = { STR_EMPTY, "type" }; +const StaticQName QN_ID = { STR_EMPTY, "id" }; +const StaticQName QN_CODE = { STR_EMPTY, "code" }; + +const StaticQName QN_VALUE = { STR_EMPTY, "value" }; +const StaticQName QN_ACTION = { STR_EMPTY, "action" }; +const StaticQName QN_ORDER = { STR_EMPTY, "order" }; +const StaticQName QN_MECHANISM = { STR_EMPTY, "mechanism" }; +const StaticQName QN_ASK = { STR_EMPTY, "ask" }; +const StaticQName QN_JID = { STR_EMPTY, "jid" }; +const StaticQName QN_NICK = { STR_EMPTY, "nick" }; +const StaticQName QN_SUBSCRIPTION = { STR_EMPTY, "subscription" }; +const StaticQName QN_TITLE1 = { STR_EMPTY, "title1" }; +const StaticQName QN_TITLE2 = { STR_EMPTY, "title2" }; +const StaticQName QN_SOURCE = { STR_EMPTY, "source" }; +const StaticQName QN_TIME = { STR_EMPTY, "time" }; + +const StaticQName QN_XMLNS_CLIENT = { NS_XMLNS, STR_CLIENT }; +const StaticQName QN_XMLNS_SERVER = { NS_XMLNS, STR_SERVER }; +const StaticQName QN_XMLNS_STREAM = { NS_XMLNS, STR_STREAM }; + + +// Presence +const char STR_SHOW_AWAY[] = "away"; +const char STR_SHOW_CHAT[] = "chat"; +const char STR_SHOW_DND[] = "dnd"; +const char STR_SHOW_XA[] = "xa"; +const char STR_SHOW_OFFLINE[] = "offline"; + +const char NS_GOOGLE_PSTN_CONFERENCE[] = "http://www.google.com/pstn-conference"; +const StaticQName QN_GOOGLE_PSTN_CONFERENCE_STATUS = { NS_GOOGLE_PSTN_CONFERENCE, "status" }; +const StaticQName QN_ATTR_STATUS = { STR_EMPTY, "status" }; + +// Presence connection status +const char STR_PSTN_CONFERENCE_STATUS_CONNECTING[] = "connecting"; +const char STR_PSTN_CONFERENCE_STATUS_CONNECTED[] = "connected"; +const char STR_PSTN_CONFERENCE_STATUS_HANGUP[] = "hangup"; + +// Subscription +const char STR_SUBSCRIBE[] = "subscribe"; +const char STR_SUBSCRIBED[] = "subscribed"; +const char STR_UNSUBSCRIBE[] = "unsubscribe"; +const char STR_UNSUBSCRIBED[] = "unsubscribed"; + +// Google Invite +const char NS_GOOGLE_SUBSCRIBE[] = "google:subscribe"; +const StaticQName QN_INVITATION = { NS_GOOGLE_SUBSCRIBE, "invitation" }; +const StaticQName QN_INVITE_NAME = { NS_GOOGLE_SUBSCRIBE, "name" }; +const StaticQName QN_INVITE_SUBJECT = { NS_GOOGLE_SUBSCRIBE, "subject" }; +const StaticQName QN_INVITE_MESSAGE = { NS_GOOGLE_SUBSCRIBE, "body" }; + +// Kick +const char NS_GOOGLE_MUC_ADMIN[] = "google:muc#admin"; +const StaticQName QN_GOOGLE_MUC_ADMIN_QUERY = { NS_GOOGLE_MUC_ADMIN, "query" }; +const StaticQName QN_GOOGLE_MUC_ADMIN_QUERY_ITEM = + { NS_GOOGLE_MUC_ADMIN, "item" }; +const StaticQName QN_GOOGLE_MUC_ADMIN_QUERY_ITEM_REASON = + { NS_GOOGLE_MUC_ADMIN, "reason" }; + +// PubSub: http://xmpp.org/extensions/xep-0060.html +const char NS_PUBSUB[] = "http://jabber.org/protocol/pubsub"; +const StaticQName QN_PUBSUB = { NS_PUBSUB, "pubsub" }; +const StaticQName QN_PUBSUB_ITEMS = { NS_PUBSUB, "items" }; +const StaticQName QN_PUBSUB_ITEM = { NS_PUBSUB, "item" }; +const StaticQName QN_PUBSUB_PUBLISH = { NS_PUBSUB, "publish" }; +const StaticQName QN_PUBSUB_RETRACT = { NS_PUBSUB, "retract" }; +const StaticQName QN_ATTR_PUBLISHER = { STR_EMPTY, "publisher" }; + +const char NS_PUBSUB_EVENT[] = "http://jabber.org/protocol/pubsub#event"; +const StaticQName QN_NODE = { STR_EMPTY, "node" }; +const StaticQName QN_PUBSUB_EVENT = { NS_PUBSUB_EVENT, "event" }; +const StaticQName QN_PUBSUB_EVENT_ITEMS = { NS_PUBSUB_EVENT, "items" }; +const StaticQName QN_PUBSUB_EVENT_ITEM = { NS_PUBSUB_EVENT, "item" }; +const StaticQName QN_PUBSUB_EVENT_RETRACT = { NS_PUBSUB_EVENT, "retract" }; +const StaticQName QN_NOTIFY = { STR_EMPTY, "notify" }; + +const char NS_PRESENTER[] = "google:presenter"; +const StaticQName QN_PRESENTER_PRESENTER = { NS_PRESENTER, "presenter" }; +const StaticQName QN_PRESENTER_PRESENTATION_ITEM = + { NS_PRESENTER, "presentation-item" }; +const StaticQName QN_PRESENTER_PRESENTATION_TYPE = + { NS_PRESENTER, "presentation-type" }; +const StaticQName QN_PRESENTER_PRESENTATION_ID = + { NS_PRESENTER, "presentation-id" }; + +// JEP 0030 +const StaticQName QN_CATEGORY = { STR_EMPTY, "category" }; +const StaticQName QN_VAR = { STR_EMPTY, "var" }; +const char NS_DISCO_INFO[] = "http://jabber.org/protocol/disco#info"; +const char NS_DISCO_ITEMS[] = "http://jabber.org/protocol/disco#items"; +const StaticQName QN_DISCO_INFO_QUERY = { NS_DISCO_INFO, "query" }; +const StaticQName QN_DISCO_IDENTITY = { NS_DISCO_INFO, "identity" }; +const StaticQName QN_DISCO_FEATURE = { NS_DISCO_INFO, "feature" }; + +const StaticQName QN_DISCO_ITEMS_QUERY = { NS_DISCO_ITEMS, "query" }; +const StaticQName QN_DISCO_ITEM = { NS_DISCO_ITEMS, "item" }; + +// JEP 0020 +const char NS_FEATURE[] = "http://jabber.org/protocol/feature-neg"; +const StaticQName QN_FEATURE_FEATURE = { NS_FEATURE, "feature" }; + +// JEP 0004 +const char NS_XDATA[] = "jabber:x:data"; +const StaticQName QN_XDATA_X = { NS_XDATA, "x" }; +const StaticQName QN_XDATA_INSTRUCTIONS = { NS_XDATA, "instructions" }; +const StaticQName QN_XDATA_TITLE = { NS_XDATA, "title" }; +const StaticQName QN_XDATA_FIELD = { NS_XDATA, "field" }; +const StaticQName QN_XDATA_REPORTED = { NS_XDATA, "reported" }; +const StaticQName QN_XDATA_ITEM = { NS_XDATA, "item" }; +const StaticQName QN_XDATA_DESC = { NS_XDATA, "desc" }; +const StaticQName QN_XDATA_REQUIRED = { NS_XDATA, "required" }; +const StaticQName QN_XDATA_VALUE = { NS_XDATA, "value" }; +const StaticQName QN_XDATA_OPTION = { NS_XDATA, "option" }; + +// JEP 0045 +const char NS_MUC[] = "http://jabber.org/protocol/muc"; +const StaticQName QN_MUC_X = { NS_MUC, "x" }; +const StaticQName QN_MUC_ITEM = { NS_MUC, "item" }; +const StaticQName QN_MUC_AFFILIATION = { NS_MUC, "affiliation" }; +const StaticQName QN_MUC_ROLE = { NS_MUC, "role" }; +const char STR_AFFILIATION_NONE[] = "none"; +const char STR_ROLE_PARTICIPANT[] = "participant"; + +const char NS_GOOGLE_SESSION[] = "http://www.google.com/session"; +const StaticQName QN_GOOGLE_CIRCLE_ID = { STR_EMPTY, "google-circle-id" }; +const StaticQName QN_GOOGLE_USER_ID = { STR_EMPTY, "google-user-id" }; +const StaticQName QN_GOOGLE_SESSION_BLOCKED = { NS_GOOGLE_SESSION, "blocked" }; +const StaticQName QN_GOOGLE_SESSION_BLOCKING = + { NS_GOOGLE_SESSION, "blocking" }; + +const char NS_MUC_OWNER[] = "http://jabber.org/protocol/muc#owner"; +const StaticQName QN_MUC_OWNER_QUERY = { NS_MUC_OWNER, "query" }; + +const char NS_MUC_USER[] = "http://jabber.org/protocol/muc#user"; +const StaticQName QN_MUC_USER_CONTINUE = { NS_MUC_USER, "continue" }; +const StaticQName QN_MUC_USER_X = { NS_MUC_USER, "x" }; +const StaticQName QN_MUC_USER_ITEM = { NS_MUC_USER, "item" }; +const StaticQName QN_MUC_USER_STATUS = { NS_MUC_USER, "status" }; +const StaticQName QN_MUC_USER_REASON = { NS_MUC_USER, "reason" }; +const StaticQName QN_MUC_USER_ABUSE_VIOLATION = { NS_MUC_USER, "abuse-violation" }; + +// JEP 0055 - Jabber Search +const char NS_SEARCH[] = "jabber:iq:search"; +const StaticQName QN_SEARCH_QUERY = { NS_SEARCH, "query" }; +const StaticQName QN_SEARCH_ITEM = { NS_SEARCH, "item" }; +const StaticQName QN_SEARCH_ROOM_NAME = { NS_SEARCH, "room-name" }; +const StaticQName QN_SEARCH_ROOM_DOMAIN = { NS_SEARCH, "room-domain" }; +const StaticQName QN_SEARCH_ROOM_JID = { NS_SEARCH, "room-jid" }; +const StaticQName QN_SEARCH_HANGOUT_ID = { NS_SEARCH, "hangout-id" }; +const StaticQName QN_SEARCH_EXTERNAL_ID = { NS_SEARCH, "external-id" }; + +// JEP 0115 +const char NS_CAPS[] = "http://jabber.org/protocol/caps"; +const StaticQName QN_CAPS_C = { NS_CAPS, "c" }; +const StaticQName QN_VER = { STR_EMPTY, "ver" }; +const StaticQName QN_EXT = { STR_EMPTY, "ext" }; + +// JEP 0153 +const char kNSVCard[] = "vcard-temp:x:update"; +const StaticQName kQnVCardX = { kNSVCard, "x" }; +const StaticQName kQnVCardPhoto = { kNSVCard, "photo" }; + +// JEP 0172 User Nickname +const char NS_NICKNAME[] = "http://jabber.org/protocol/nick"; +const StaticQName QN_NICKNAME = { NS_NICKNAME, "nick" }; + +// JEP 0085 chat state +const char NS_CHATSTATE[] = "http://jabber.org/protocol/chatstates"; +const StaticQName QN_CS_ACTIVE = { NS_CHATSTATE, "active" }; +const StaticQName QN_CS_COMPOSING = { NS_CHATSTATE, "composing" }; +const StaticQName QN_CS_PAUSED = { NS_CHATSTATE, "paused" }; +const StaticQName QN_CS_INACTIVE = { NS_CHATSTATE, "inactive" }; +const StaticQName QN_CS_GONE = { NS_CHATSTATE, "gone" }; + +// JEP 0091 Delayed Delivery +const char kNSDelay[] = "jabber:x:delay"; +const StaticQName kQnDelayX = { kNSDelay, "x" }; +const StaticQName kQnStamp = { STR_EMPTY, "stamp" }; + +// Google time stamping (higher resolution) +const char kNSTimestamp[] = "google:timestamp"; +const StaticQName kQnTime = { kNSTimestamp, "time" }; +const StaticQName kQnMilliseconds = { STR_EMPTY, "ms" }; + +// Jingle Info +const char NS_JINGLE_INFO[] = "google:jingleinfo"; +const StaticQName QN_JINGLE_INFO_QUERY = { NS_JINGLE_INFO, "query" }; +const StaticQName QN_JINGLE_INFO_STUN = { NS_JINGLE_INFO, "stun" }; +const StaticQName QN_JINGLE_INFO_RELAY = { NS_JINGLE_INFO, "relay" }; +const StaticQName QN_JINGLE_INFO_SERVER = { NS_JINGLE_INFO, "server" }; +const StaticQName QN_JINGLE_INFO_TOKEN = { NS_JINGLE_INFO, "token" }; +const StaticQName QN_JINGLE_INFO_HOST = { STR_EMPTY, "host" }; +const StaticQName QN_JINGLE_INFO_TCP = { STR_EMPTY, "tcp" }; +const StaticQName QN_JINGLE_INFO_UDP = { STR_EMPTY, "udp" }; +const StaticQName QN_JINGLE_INFO_TCPSSL = { STR_EMPTY, "tcpssl" }; + +// Call Performance Logging +const char NS_GOOGLE_CALLPERF_STATS[] = "google:call-perf-stats"; +const StaticQName QN_CALLPERF_STATS = + { NS_GOOGLE_CALLPERF_STATS, "callPerfStats" }; +const StaticQName QN_CALLPERF_SESSIONID = { STR_EMPTY, "sessionId" }; +const StaticQName QN_CALLPERF_LOCALUSER = { STR_EMPTY, "localUser" }; +const StaticQName QN_CALLPERF_REMOTEUSER = { STR_EMPTY, "remoteUser" }; +const StaticQName QN_CALLPERF_STARTTIME = { STR_EMPTY, "startTime" }; +const StaticQName QN_CALLPERF_CALL_LENGTH = { STR_EMPTY, "callLength" }; +const StaticQName QN_CALLPERF_CALL_ACCEPTED = { STR_EMPTY, "callAccepted" }; +const StaticQName QN_CALLPERF_CALL_ERROR_CODE = { STR_EMPTY, "callErrorCode" }; +const StaticQName QN_CALLPERF_TERMINATE_CODE = { STR_EMPTY, "terminateCode" }; +const StaticQName QN_CALLPERF_DATAPOINT = + { NS_GOOGLE_CALLPERF_STATS, "dataPoint" }; +const StaticQName QN_CALLPERF_DATAPOINT_TIME = { STR_EMPTY, "timeStamp" }; +const StaticQName QN_CALLPERF_DATAPOINT_FRACTION_LOST = + { STR_EMPTY, "fraction_lost" }; +const StaticQName QN_CALLPERF_DATAPOINT_CUM_LOST = { STR_EMPTY, "cum_lost" }; +const StaticQName QN_CALLPERF_DATAPOINT_EXT_MAX = { STR_EMPTY, "ext_max" }; +const StaticQName QN_CALLPERF_DATAPOINT_JITTER = { STR_EMPTY, "jitter" }; +const StaticQName QN_CALLPERF_DATAPOINT_RTT = { STR_EMPTY, "RTT" }; +const StaticQName QN_CALLPERF_DATAPOINT_BYTES_R = + { STR_EMPTY, "bytesReceived" }; +const StaticQName QN_CALLPERF_DATAPOINT_PACKETS_R = + { STR_EMPTY, "packetsReceived" }; +const StaticQName QN_CALLPERF_DATAPOINT_BYTES_S = { STR_EMPTY, "bytesSent" }; +const StaticQName QN_CALLPERF_DATAPOINT_PACKETS_S = + { STR_EMPTY, "packetsSent" }; +const StaticQName QN_CALLPERF_DATAPOINT_PROCESS_CPU = + { STR_EMPTY, "processCpu" }; +const StaticQName QN_CALLPERF_DATAPOINT_SYSTEM_CPU = { STR_EMPTY, "systemCpu" }; +const StaticQName QN_CALLPERF_DATAPOINT_CPUS = { STR_EMPTY, "cpus" }; +const StaticQName QN_CALLPERF_CONNECTION = + { NS_GOOGLE_CALLPERF_STATS, "connection" }; +const StaticQName QN_CALLPERF_CONNECTION_LOCAL_ADDRESS = + { STR_EMPTY, "localAddress" }; +const StaticQName QN_CALLPERF_CONNECTION_REMOTE_ADDRESS = + { STR_EMPTY, "remoteAddress" }; +const StaticQName QN_CALLPERF_CONNECTION_FLAGS = { STR_EMPTY, "flags" }; +const StaticQName QN_CALLPERF_CONNECTION_RTT = { STR_EMPTY, "rtt" }; +const StaticQName QN_CALLPERF_CONNECTION_TOTAL_BYTES_S = + { STR_EMPTY, "totalBytesSent" }; +const StaticQName QN_CALLPERF_CONNECTION_BYTES_SECOND_S = + { STR_EMPTY, "bytesSecondSent" }; +const StaticQName QN_CALLPERF_CONNECTION_TOTAL_BYTES_R = + { STR_EMPTY, "totalBytesRecv" }; +const StaticQName QN_CALLPERF_CONNECTION_BYTES_SECOND_R = + { STR_EMPTY, "bytesSecondRecv" }; +const StaticQName QN_CALLPERF_CANDIDATE = + { NS_GOOGLE_CALLPERF_STATS, "candidate" }; +const StaticQName QN_CALLPERF_CANDIDATE_ENDPOINT = { STR_EMPTY, "endpoint" }; +const StaticQName QN_CALLPERF_CANDIDATE_PROTOCOL = { STR_EMPTY, "protocol" }; +const StaticQName QN_CALLPERF_CANDIDATE_ADDRESS = { STR_EMPTY, "address" }; +const StaticQName QN_CALLPERF_MEDIA = { NS_GOOGLE_CALLPERF_STATS, "media" }; +const StaticQName QN_CALLPERF_MEDIA_DIRECTION = { STR_EMPTY, "direction" }; +const StaticQName QN_CALLPERF_MEDIA_SSRC = { STR_EMPTY, "SSRC" }; +const StaticQName QN_CALLPERF_MEDIA_ENERGY = { STR_EMPTY, "energy" }; +const StaticQName QN_CALLPERF_MEDIA_FIR = { STR_EMPTY, "fir" }; +const StaticQName QN_CALLPERF_MEDIA_NACK = { STR_EMPTY, "nack" }; +const StaticQName QN_CALLPERF_MEDIA_FPS = { STR_EMPTY, "fps" }; +const StaticQName QN_CALLPERF_MEDIA_FPS_NETWORK = { STR_EMPTY, "fpsNetwork" }; +const StaticQName QN_CALLPERF_MEDIA_FPS_DECODED = { STR_EMPTY, "fpsDecoded" }; +const StaticQName QN_CALLPERF_MEDIA_JITTER_BUFFER_SIZE = + { STR_EMPTY, "jitterBufferSize" }; +const StaticQName QN_CALLPERF_MEDIA_PREFERRED_JITTER_BUFFER_SIZE = + { STR_EMPTY, "preferredJitterBufferSize" }; +const StaticQName QN_CALLPERF_MEDIA_TOTAL_PLAYOUT_DELAY = + { STR_EMPTY, "totalPlayoutDelay" }; + +// Muc invites. +const StaticQName QN_MUC_USER_INVITE = { NS_MUC_USER, "invite" }; + +// Multiway audio/video. +const char NS_GOOGLE_MUC_USER[] = "google:muc#user"; +const StaticQName QN_GOOGLE_MUC_USER_AVAILABLE_MEDIA = + { NS_GOOGLE_MUC_USER, "available-media" }; +const StaticQName QN_GOOGLE_MUC_USER_ENTRY = { NS_GOOGLE_MUC_USER, "entry" }; +const StaticQName QN_GOOGLE_MUC_USER_MEDIA = { NS_GOOGLE_MUC_USER, "media" }; +const StaticQName QN_GOOGLE_MUC_USER_TYPE = { NS_GOOGLE_MUC_USER, "type" }; +const StaticQName QN_GOOGLE_MUC_USER_SRC_ID = { NS_GOOGLE_MUC_USER, "src-id" }; +const StaticQName QN_GOOGLE_MUC_USER_STATUS = { NS_GOOGLE_MUC_USER, "status" }; +const StaticQName QN_CLIENT_VERSION = { NS_GOOGLE_MUC_USER, "client-version" }; +const StaticQName QN_LOCALE = { NS_GOOGLE_MUC_USER, "locale" }; +const StaticQName QN_LABEL = { STR_EMPTY, "label" }; + +const char NS_GOOGLE_MUC_MEDIA[] = "google:muc#media"; +const StaticQName QN_GOOGLE_MUC_AUDIO_MUTE = + { NS_GOOGLE_MUC_MEDIA, "audio-mute" }; +const StaticQName QN_GOOGLE_MUC_VIDEO_MUTE = + { NS_GOOGLE_MUC_MEDIA, "video-mute" }; +const StaticQName QN_GOOGLE_MUC_VIDEO_PAUSE = + { NS_GOOGLE_MUC_MEDIA, "video-pause" }; +const StaticQName QN_GOOGLE_MUC_RECORDING = + { NS_GOOGLE_MUC_MEDIA, "recording" }; +const StaticQName QN_GOOGLE_MUC_MEDIA_BLOCK = { NS_GOOGLE_MUC_MEDIA, "block" }; +const StaticQName QN_STATE_ATTR = { STR_EMPTY, "state" }; + +const char AUTH_MECHANISM_GOOGLE_COOKIE[] = "X-GOOGLE-COOKIE"; +const char AUTH_MECHANISM_GOOGLE_TOKEN[] = "X-GOOGLE-TOKEN"; +const char AUTH_MECHANISM_OAUTH2[] = "X-OAUTH2"; +const char AUTH_MECHANISM_PLAIN[] = "PLAIN"; + +} // namespace buzz diff --git a/talk/xmpp/constants.h b/talk/xmpp/constants.h new file mode 100644 index 000000000..e01a798a7 --- /dev/null +++ b/talk/xmpp/constants.h @@ -0,0 +1,551 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_CONSTANTS_H_ +#define TALK_XMPP_CONSTANTS_H_ + +#include +#include "talk/xmllite/qname.h" +#include "talk/xmpp/jid.h" + +namespace buzz { + +extern const char NS_CLIENT[]; +extern const char NS_SERVER[]; +extern const char NS_STREAM[]; +extern const char NS_XSTREAM[]; +extern const char NS_TLS[]; +extern const char NS_SASL[]; +extern const char NS_BIND[]; +extern const char NS_DIALBACK[]; +extern const char NS_SESSION[]; +extern const char NS_STANZA[]; +extern const char NS_PRIVACY[]; +extern const char NS_ROSTER[]; +extern const char NS_VCARD[]; +extern const char NS_AVATAR_HASH[]; +extern const char NS_VCARD_UPDATE[]; +extern const char STR_CLIENT[]; +extern const char STR_SERVER[]; +extern const char STR_STREAM[]; + +extern const char STR_GET[]; +extern const char STR_SET[]; +extern const char STR_RESULT[]; +extern const char STR_ERROR[]; + +extern const char STR_FORM[]; +extern const char STR_SUBMIT[]; +extern const char STR_TEXT_SINGLE[]; +extern const char STR_LIST_SINGLE[]; +extern const char STR_LIST_MULTI[]; +extern const char STR_HIDDEN[]; +extern const char STR_FORM_TYPE[]; + +extern const char STR_FROM[]; +extern const char STR_TO[]; +extern const char STR_BOTH[]; +extern const char STR_REMOVE[]; +extern const char STR_TRUE[]; + +extern const char STR_TYPE[]; +extern const char STR_NAME[]; +extern const char STR_ID[]; +extern const char STR_JID[]; +extern const char STR_SUBSCRIPTION[]; +extern const char STR_ASK[]; +extern const char STR_X[]; +extern const char STR_GOOGLE_COM[]; +extern const char STR_GMAIL_COM[]; +extern const char STR_GOOGLEMAIL_COM[]; +extern const char STR_DEFAULT_DOMAIN[]; +extern const char STR_TALK_GOOGLE_COM[]; +extern const char STR_TALKX_L_GOOGLE_COM[]; +extern const char STR_XMPP_GOOGLE_COM[]; +extern const char STR_XMPPX_L_GOOGLE_COM[]; + +#ifdef FEATURE_ENABLE_VOICEMAIL +extern const char STR_VOICEMAIL[]; +extern const char STR_OUTGOINGVOICEMAIL[]; +#endif + +extern const char STR_UNAVAILABLE[]; + +extern const char NS_PING[]; +extern const StaticQName QN_PING; + +extern const char NS_MUC_UNIQUE[]; +extern const StaticQName QN_MUC_UNIQUE_QUERY; +extern const StaticQName QN_HANGOUT_ID; + +extern const char STR_GOOGLE_MUC_LOOKUP_JID[]; +extern const char STR_MUC_ROOMCONFIG_ROOMNAME[]; +extern const char STR_MUC_ROOMCONFIG_FEATURES[]; +extern const char STR_MUC_ROOM_FEATURE_ENTERPRISE[]; +extern const char STR_MUC_ROOMCONFIG[]; +extern const char STR_MUC_ROOM_FEATURE_HANGOUT[]; +extern const char STR_MUC_ROOM_FEATURE_HANGOUT_LITE[]; +extern const char STR_MUC_ROOM_FEATURE_BROADCAST[]; +extern const char STR_MUC_ROOM_FEATURE_MULTI_USER_VC[]; + +extern const StaticQName QN_STREAM_STREAM; +extern const StaticQName QN_STREAM_FEATURES; +extern const StaticQName QN_STREAM_ERROR; + +extern const StaticQName QN_XSTREAM_BAD_FORMAT; +extern const StaticQName QN_XSTREAM_BAD_NAMESPACE_PREFIX; +extern const StaticQName QN_XSTREAM_CONFLICT; +extern const StaticQName QN_XSTREAM_CONNECTION_TIMEOUT; +extern const StaticQName QN_XSTREAM_HOST_GONE; +extern const StaticQName QN_XSTREAM_HOST_UNKNOWN; +extern const StaticQName QN_XSTREAM_IMPROPER_ADDRESSIING; +extern const StaticQName QN_XSTREAM_INTERNAL_SERVER_ERROR; +extern const StaticQName QN_XSTREAM_INVALID_FROM; +extern const StaticQName QN_XSTREAM_INVALID_ID; +extern const StaticQName QN_XSTREAM_INVALID_NAMESPACE; +extern const StaticQName QN_XSTREAM_INVALID_XML; +extern const StaticQName QN_XSTREAM_NOT_AUTHORIZED; +extern const StaticQName QN_XSTREAM_POLICY_VIOLATION; +extern const StaticQName QN_XSTREAM_REMOTE_CONNECTION_FAILED; +extern const StaticQName QN_XSTREAM_RESOURCE_CONSTRAINT; +extern const StaticQName QN_XSTREAM_RESTRICTED_XML; +extern const StaticQName QN_XSTREAM_SEE_OTHER_HOST; +extern const StaticQName QN_XSTREAM_SYSTEM_SHUTDOWN; +extern const StaticQName QN_XSTREAM_UNDEFINED_CONDITION; +extern const StaticQName QN_XSTREAM_UNSUPPORTED_ENCODING; +extern const StaticQName QN_XSTREAM_UNSUPPORTED_STANZA_TYPE; +extern const StaticQName QN_XSTREAM_UNSUPPORTED_VERSION; +extern const StaticQName QN_XSTREAM_XML_NOT_WELL_FORMED; +extern const StaticQName QN_XSTREAM_TEXT; + +extern const StaticQName QN_TLS_STARTTLS; +extern const StaticQName QN_TLS_REQUIRED; +extern const StaticQName QN_TLS_PROCEED; +extern const StaticQName QN_TLS_FAILURE; + +extern const StaticQName QN_SASL_MECHANISMS; +extern const StaticQName QN_SASL_MECHANISM; +extern const StaticQName QN_SASL_AUTH; +extern const StaticQName QN_SASL_CHALLENGE; +extern const StaticQName QN_SASL_RESPONSE; +extern const StaticQName QN_SASL_ABORT; +extern const StaticQName QN_SASL_SUCCESS; +extern const StaticQName QN_SASL_FAILURE; +extern const StaticQName QN_SASL_ABORTED; +extern const StaticQName QN_SASL_INCORRECT_ENCODING; +extern const StaticQName QN_SASL_INVALID_AUTHZID; +extern const StaticQName QN_SASL_INVALID_MECHANISM; +extern const StaticQName QN_SASL_MECHANISM_TOO_WEAK; +extern const StaticQName QN_SASL_NOT_AUTHORIZED; +extern const StaticQName QN_SASL_TEMPORARY_AUTH_FAILURE; + +// These are non-standard. +extern const char NS_GOOGLE_AUTH[]; +extern const char NS_GOOGLE_AUTH_PROTOCOL[]; +extern const StaticQName QN_GOOGLE_AUTH_CLIENT_USES_FULL_BIND_RESULT; +extern const StaticQName QN_GOOGLE_ALLOW_NON_GOOGLE_ID_XMPP_LOGIN; +extern const StaticQName QN_GOOGLE_AUTH_SERVICE; + +extern const StaticQName QN_DIALBACK_RESULT; +extern const StaticQName QN_DIALBACK_VERIFY; + +extern const StaticQName QN_STANZA_BAD_REQUEST; +extern const StaticQName QN_STANZA_CONFLICT; +extern const StaticQName QN_STANZA_FEATURE_NOT_IMPLEMENTED; +extern const StaticQName QN_STANZA_FORBIDDEN; +extern const StaticQName QN_STANZA_GONE; +extern const StaticQName QN_STANZA_INTERNAL_SERVER_ERROR; +extern const StaticQName QN_STANZA_ITEM_NOT_FOUND; +extern const StaticQName QN_STANZA_JID_MALFORMED; +extern const StaticQName QN_STANZA_NOT_ACCEPTABLE; +extern const StaticQName QN_STANZA_NOT_ALLOWED; +extern const StaticQName QN_STANZA_PAYMENT_REQUIRED; +extern const StaticQName QN_STANZA_RECIPIENT_UNAVAILABLE; +extern const StaticQName QN_STANZA_REDIRECT; +extern const StaticQName QN_STANZA_REGISTRATION_REQUIRED; +extern const StaticQName QN_STANZA_REMOTE_SERVER_NOT_FOUND; +extern const StaticQName QN_STANZA_REMOTE_SERVER_TIMEOUT; +extern const StaticQName QN_STANZA_RESOURCE_CONSTRAINT; +extern const StaticQName QN_STANZA_SERVICE_UNAVAILABLE; +extern const StaticQName QN_STANZA_SUBSCRIPTION_REQUIRED; +extern const StaticQName QN_STANZA_UNDEFINED_CONDITION; +extern const StaticQName QN_STANZA_UNEXPECTED_REQUEST; +extern const StaticQName QN_STANZA_TEXT; + +extern const StaticQName QN_BIND_BIND; +extern const StaticQName QN_BIND_RESOURCE; +extern const StaticQName QN_BIND_JID; + +extern const StaticQName QN_MESSAGE; +extern const StaticQName QN_BODY; +extern const StaticQName QN_SUBJECT; +extern const StaticQName QN_THREAD; +extern const StaticQName QN_PRESENCE; +extern const StaticQName QN_SHOW; +extern const StaticQName QN_STATUS; +extern const StaticQName QN_LANG; +extern const StaticQName QN_PRIORITY; +extern const StaticQName QN_IQ; +extern const StaticQName QN_ERROR; + +extern const StaticQName QN_SERVER_MESSAGE; +extern const StaticQName QN_SERVER_BODY; +extern const StaticQName QN_SERVER_SUBJECT; +extern const StaticQName QN_SERVER_THREAD; +extern const StaticQName QN_SERVER_PRESENCE; +extern const StaticQName QN_SERVER_SHOW; +extern const StaticQName QN_SERVER_STATUS; +extern const StaticQName QN_SERVER_LANG; +extern const StaticQName QN_SERVER_PRIORITY; +extern const StaticQName QN_SERVER_IQ; +extern const StaticQName QN_SERVER_ERROR; + +extern const StaticQName QN_SESSION_SESSION; + +extern const StaticQName QN_PRIVACY_QUERY; +extern const StaticQName QN_PRIVACY_ACTIVE; +extern const StaticQName QN_PRIVACY_DEFAULT; +extern const StaticQName QN_PRIVACY_LIST; +extern const StaticQName QN_PRIVACY_ITEM; +extern const StaticQName QN_PRIVACY_IQ; +extern const StaticQName QN_PRIVACY_MESSAGE; +extern const StaticQName QN_PRIVACY_PRESENCE_IN; +extern const StaticQName QN_PRIVACY_PRESENCE_OUT; + +extern const StaticQName QN_ROSTER_QUERY; +extern const StaticQName QN_ROSTER_ITEM; +extern const StaticQName QN_ROSTER_GROUP; + +extern const StaticQName QN_VCARD; +extern const StaticQName QN_VCARD_FN; +extern const StaticQName QN_VCARD_PHOTO; +extern const StaticQName QN_VCARD_PHOTO_BINVAL; +extern const StaticQName QN_VCARD_AVATAR_HASH; +extern const StaticQName QN_VCARD_AVATAR_HASH_MODIFIED; + +#if defined(FEATURE_ENABLE_PSTN) +extern const StaticQName QN_VCARD_TEL; +extern const StaticQName QN_VCARD_VOICE; +extern const StaticQName QN_VCARD_HOME; +extern const StaticQName QN_VCARD_WORK; +extern const StaticQName QN_VCARD_CELL; +extern const StaticQName QN_VCARD_NUMBER; +#endif + +#if defined(FEATURE_ENABLE_RICHPROFILES) +extern const StaticQName QN_USER_PROFILE_QUERY; +extern const StaticQName QN_USER_PROFILE_URL; + +extern const StaticQName QN_ATOM_FEED; +extern const StaticQName QN_ATOM_ENTRY; +extern const StaticQName QN_ATOM_TITLE; +extern const StaticQName QN_ATOM_ID; +extern const StaticQName QN_ATOM_MODIFIED; +extern const StaticQName QN_ATOM_IMAGE; +extern const StaticQName QN_ATOM_LINK; +extern const StaticQName QN_ATOM_HREF; +#endif + +extern const StaticQName QN_XML_LANG; + +extern const StaticQName QN_ENCODING; +extern const StaticQName QN_VERSION; +extern const StaticQName QN_TO; +extern const StaticQName QN_FROM; +extern const StaticQName QN_TYPE; +extern const StaticQName QN_ID; +extern const StaticQName QN_CODE; +extern const StaticQName QN_NAME; +extern const StaticQName QN_VALUE; +extern const StaticQName QN_ACTION; +extern const StaticQName QN_ORDER; +extern const StaticQName QN_MECHANISM; +extern const StaticQName QN_ASK; +extern const StaticQName QN_JID; +extern const StaticQName QN_NICK; +extern const StaticQName QN_SUBSCRIPTION; +extern const StaticQName QN_TITLE1; +extern const StaticQName QN_TITLE2; +extern const StaticQName QN_AFFILIATION; +extern const StaticQName QN_ROLE; +extern const StaticQName QN_TIME; + +extern const StaticQName QN_XMLNS_CLIENT; +extern const StaticQName QN_XMLNS_SERVER; +extern const StaticQName QN_XMLNS_STREAM; + +// Presence +extern const char STR_SHOW_AWAY[]; +extern const char STR_SHOW_CHAT[]; +extern const char STR_SHOW_DND[]; +extern const char STR_SHOW_XA[]; +extern const char STR_SHOW_OFFLINE[]; + +extern const char NS_GOOGLE_PSTN_CONFERENCE[]; +extern const StaticQName QN_GOOGLE_PSTN_CONFERENCE_STATUS; +extern const StaticQName QN_ATTR_STATUS; + +// Presence connection status +extern const char STR_PSTN_CONFERENCE_STATUS_CONNECTING[]; +extern const char STR_PSTN_CONFERENCE_STATUS_CONNECTED[]; +extern const char STR_PSTN_CONFERENCE_STATUS_HANGUP[]; + +// Subscription +extern const char STR_SUBSCRIBE[]; +extern const char STR_SUBSCRIBED[]; +extern const char STR_UNSUBSCRIBE[]; +extern const char STR_UNSUBSCRIBED[]; + +// Google Invite +extern const char NS_GOOGLE_SUBSCRIBE[]; +extern const StaticQName QN_INVITATION; +extern const StaticQName QN_INVITE_NAME; +extern const StaticQName QN_INVITE_SUBJECT; +extern const StaticQName QN_INVITE_MESSAGE; + +// Kick +extern const char NS_GOOGLE_MUC_ADMIN[]; +extern const StaticQName QN_GOOGLE_MUC_ADMIN_QUERY; +extern const StaticQName QN_GOOGLE_MUC_ADMIN_QUERY_ITEM; +extern const StaticQName QN_GOOGLE_MUC_ADMIN_QUERY_ITEM_REASON; + +// PubSub: http://xmpp.org/extensions/xep-0060.html +extern const char NS_PUBSUB[]; +extern const StaticQName QN_PUBSUB; +extern const StaticQName QN_PUBSUB_ITEMS; +extern const StaticQName QN_PUBSUB_ITEM; +extern const StaticQName QN_PUBSUB_PUBLISH; +extern const StaticQName QN_PUBSUB_RETRACT; +extern const StaticQName QN_ATTR_PUBLISHER; + +extern const char NS_PUBSUB_EVENT[]; +extern const StaticQName QN_NODE; +extern const StaticQName QN_PUBSUB_EVENT; +extern const StaticQName QN_PUBSUB_EVENT_ITEMS; +extern const StaticQName QN_PUBSUB_EVENT_ITEM; +extern const StaticQName QN_PUBSUB_EVENT_RETRACT; +extern const StaticQName QN_NOTIFY; + +extern const char NS_PRESENTER[]; +extern const StaticQName QN_PRESENTER_PRESENTER; +extern const StaticQName QN_PRESENTER_PRESENTATION_ITEM; +extern const StaticQName QN_PRESENTER_PRESENTATION_TYPE; +extern const StaticQName QN_PRESENTER_PRESENTATION_ID; + +// JEP 0030 +extern const StaticQName QN_CATEGORY; +extern const StaticQName QN_VAR; +extern const char NS_DISCO_INFO[]; +extern const char NS_DISCO_ITEMS[]; + +extern const StaticQName QN_DISCO_INFO_QUERY; +extern const StaticQName QN_DISCO_IDENTITY; +extern const StaticQName QN_DISCO_FEATURE; + +extern const StaticQName QN_DISCO_ITEMS_QUERY; +extern const StaticQName QN_DISCO_ITEM; + +// JEP 0020 +extern const char NS_FEATURE[]; +extern const StaticQName QN_FEATURE_FEATURE; + +// JEP 0004 +extern const char NS_XDATA[]; +extern const StaticQName QN_XDATA_X; +extern const StaticQName QN_XDATA_INSTRUCTIONS; +extern const StaticQName QN_XDATA_TITLE; +extern const StaticQName QN_XDATA_FIELD; +extern const StaticQName QN_XDATA_REPORTED; +extern const StaticQName QN_XDATA_ITEM; +extern const StaticQName QN_XDATA_DESC; +extern const StaticQName QN_XDATA_REQUIRED; +extern const StaticQName QN_XDATA_VALUE; +extern const StaticQName QN_XDATA_OPTION; + +// JEP 0045 +extern const char NS_MUC[]; +extern const StaticQName QN_MUC_X; +extern const StaticQName QN_MUC_ITEM; +extern const StaticQName QN_MUC_AFFILIATION; +extern const StaticQName QN_MUC_ROLE; +extern const StaticQName QN_CLIENT_VERSION; +extern const StaticQName QN_LOCALE; +extern const char STR_AFFILIATION_NONE[]; +extern const char STR_ROLE_PARTICIPANT[]; + +extern const char NS_GOOGLE_SESSION[]; +extern const StaticQName QN_GOOGLE_USER_ID; +extern const StaticQName QN_GOOGLE_CIRCLE_ID; +extern const StaticQName QN_GOOGLE_SESSION_BLOCKED; +extern const StaticQName QN_GOOGLE_SESSION_BLOCKING; + +extern const char NS_MUC_OWNER[]; +extern const StaticQName QN_MUC_OWNER_QUERY; + +extern const char NS_MUC_USER[]; +extern const StaticQName QN_MUC_USER_CONTINUE; +extern const StaticQName QN_MUC_USER_X; +extern const StaticQName QN_MUC_USER_ITEM; +extern const StaticQName QN_MUC_USER_STATUS; +extern const StaticQName QN_MUC_USER_REASON; +extern const StaticQName QN_MUC_USER_ABUSE_VIOLATION; + +// JEP 0055 - Jabber Search +extern const char NS_SEARCH[]; +extern const StaticQName QN_SEARCH_QUERY; +extern const StaticQName QN_SEARCH_ITEM; +extern const StaticQName QN_SEARCH_ROOM_NAME; +extern const StaticQName QN_SEARCH_ROOM_JID; +extern const StaticQName QN_SEARCH_ROOM_DOMAIN; +extern const StaticQName QN_SEARCH_HANGOUT_ID; +extern const StaticQName QN_SEARCH_EXTERNAL_ID; + +// JEP 0115 +extern const char NS_CAPS[]; +extern const StaticQName QN_CAPS_C; +extern const StaticQName QN_VER; +extern const StaticQName QN_EXT; + + +// Avatar - JEP 0153 +extern const char kNSVCard[]; +extern const StaticQName kQnVCardX; +extern const StaticQName kQnVCardPhoto; + +// JEP 0172 User Nickname +extern const char NS_NICKNAME[]; +extern const StaticQName QN_NICKNAME; + +// JEP 0085 chat state +extern const char NS_CHATSTATE[]; +extern const StaticQName QN_CS_ACTIVE; +extern const StaticQName QN_CS_COMPOSING; +extern const StaticQName QN_CS_PAUSED; +extern const StaticQName QN_CS_INACTIVE; +extern const StaticQName QN_CS_GONE; + +// JEP 0091 Delayed Delivery +extern const char kNSDelay[]; +extern const StaticQName kQnDelayX; +extern const StaticQName kQnStamp; + +// Google time stamping (higher resolution) +extern const char kNSTimestamp[]; +extern const StaticQName kQnTime; +extern const StaticQName kQnMilliseconds; + +extern const char NS_JINGLE_INFO[]; +extern const StaticQName QN_JINGLE_INFO_QUERY; +extern const StaticQName QN_JINGLE_INFO_STUN; +extern const StaticQName QN_JINGLE_INFO_RELAY; +extern const StaticQName QN_JINGLE_INFO_SERVER; +extern const StaticQName QN_JINGLE_INFO_TOKEN; +extern const StaticQName QN_JINGLE_INFO_HOST; +extern const StaticQName QN_JINGLE_INFO_TCP; +extern const StaticQName QN_JINGLE_INFO_UDP; +extern const StaticQName QN_JINGLE_INFO_TCPSSL; + +extern const char NS_GOOGLE_CALLPERF_STATS[]; +extern const StaticQName QN_CALLPERF_STATS; +extern const StaticQName QN_CALLPERF_SESSIONID; +extern const StaticQName QN_CALLPERF_LOCALUSER; +extern const StaticQName QN_CALLPERF_REMOTEUSER; +extern const StaticQName QN_CALLPERF_STARTTIME; +extern const StaticQName QN_CALLPERF_CALL_LENGTH; +extern const StaticQName QN_CALLPERF_CALL_ACCEPTED; +extern const StaticQName QN_CALLPERF_CALL_ERROR_CODE; +extern const StaticQName QN_CALLPERF_TERMINATE_CODE; +extern const StaticQName QN_CALLPERF_DATAPOINT; +extern const StaticQName QN_CALLPERF_DATAPOINT_TIME; +extern const StaticQName QN_CALLPERF_DATAPOINT_FRACTION_LOST; +extern const StaticQName QN_CALLPERF_DATAPOINT_CUM_LOST; +extern const StaticQName QN_CALLPERF_DATAPOINT_EXT_MAX; +extern const StaticQName QN_CALLPERF_DATAPOINT_JITTER; +extern const StaticQName QN_CALLPERF_DATAPOINT_RTT; +extern const StaticQName QN_CALLPERF_DATAPOINT_BYTES_R; +extern const StaticQName QN_CALLPERF_DATAPOINT_PACKETS_R; +extern const StaticQName QN_CALLPERF_DATAPOINT_BYTES_S; +extern const StaticQName QN_CALLPERF_DATAPOINT_PACKETS_S; +extern const StaticQName QN_CALLPERF_DATAPOINT_PROCESS_CPU; +extern const StaticQName QN_CALLPERF_DATAPOINT_SYSTEM_CPU; +extern const StaticQName QN_CALLPERF_DATAPOINT_CPUS; +extern const StaticQName QN_CALLPERF_CONNECTION; +extern const StaticQName QN_CALLPERF_CONNECTION_LOCAL_ADDRESS; +extern const StaticQName QN_CALLPERF_CONNECTION_REMOTE_ADDRESS; +extern const StaticQName QN_CALLPERF_CONNECTION_FLAGS; +extern const StaticQName QN_CALLPERF_CONNECTION_RTT; +extern const StaticQName QN_CALLPERF_CONNECTION_TOTAL_BYTES_S; +extern const StaticQName QN_CALLPERF_CONNECTION_BYTES_SECOND_S; +extern const StaticQName QN_CALLPERF_CONNECTION_TOTAL_BYTES_R; +extern const StaticQName QN_CALLPERF_CONNECTION_BYTES_SECOND_R; +extern const StaticQName QN_CALLPERF_CANDIDATE; +extern const StaticQName QN_CALLPERF_CANDIDATE_ENDPOINT; +extern const StaticQName QN_CALLPERF_CANDIDATE_PROTOCOL; +extern const StaticQName QN_CALLPERF_CANDIDATE_ADDRESS; +extern const StaticQName QN_CALLPERF_MEDIA; +extern const StaticQName QN_CALLPERF_MEDIA_DIRECTION; +extern const StaticQName QN_CALLPERF_MEDIA_SSRC; +extern const StaticQName QN_CALLPERF_MEDIA_ENERGY; +extern const StaticQName QN_CALLPERF_MEDIA_FIR; +extern const StaticQName QN_CALLPERF_MEDIA_NACK; +extern const StaticQName QN_CALLPERF_MEDIA_FPS; +extern const StaticQName QN_CALLPERF_MEDIA_FPS_NETWORK; +extern const StaticQName QN_CALLPERF_MEDIA_FPS_DECODED; +extern const StaticQName QN_CALLPERF_MEDIA_JITTER_BUFFER_SIZE; +extern const StaticQName QN_CALLPERF_MEDIA_PREFERRED_JITTER_BUFFER_SIZE; +extern const StaticQName QN_CALLPERF_MEDIA_TOTAL_PLAYOUT_DELAY; + +// Muc invites. +extern const StaticQName QN_MUC_USER_INVITE; + +// Multiway audio/video. +extern const char NS_GOOGLE_MUC_USER[]; +extern const StaticQName QN_GOOGLE_MUC_USER_AVAILABLE_MEDIA; +extern const StaticQName QN_GOOGLE_MUC_USER_ENTRY; +extern const StaticQName QN_GOOGLE_MUC_USER_MEDIA; +extern const StaticQName QN_GOOGLE_MUC_USER_TYPE; +extern const StaticQName QN_GOOGLE_MUC_USER_SRC_ID; +extern const StaticQName QN_GOOGLE_MUC_USER_STATUS; +extern const StaticQName QN_LABEL; + +extern const char NS_GOOGLE_MUC_MEDIA[]; +extern const StaticQName QN_GOOGLE_MUC_AUDIO_MUTE; +extern const StaticQName QN_GOOGLE_MUC_VIDEO_MUTE; +extern const StaticQName QN_GOOGLE_MUC_VIDEO_PAUSE; +extern const StaticQName QN_GOOGLE_MUC_RECORDING; +extern const StaticQName QN_GOOGLE_MUC_MEDIA_BLOCK; +extern const StaticQName QN_STATE_ATTR; + + +extern const char AUTH_MECHANISM_GOOGLE_COOKIE[]; +extern const char AUTH_MECHANISM_GOOGLE_TOKEN[]; +extern const char AUTH_MECHANISM_OAUTH2[]; +extern const char AUTH_MECHANISM_PLAIN[]; + +} // namespace buzz + +#endif // TALK_XMPP_CONSTANTS_H_ diff --git a/talk/xmpp/discoitemsquerytask.cc b/talk/xmpp/discoitemsquerytask.cc new file mode 100644 index 000000000..7cdee2cd1 --- /dev/null +++ b/talk/xmpp/discoitemsquerytask.cc @@ -0,0 +1,79 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/scoped_ptr.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/discoitemsquerytask.h" +#include "talk/xmpp/xmpptask.h" + +namespace buzz { + +DiscoItemsQueryTask::DiscoItemsQueryTask(XmppTaskParentInterface* parent, + const Jid& to, + const std::string& node) + : IqTask(parent, STR_GET, to, MakeRequest(node)) { +} + +XmlElement* DiscoItemsQueryTask::MakeRequest(const std::string& node) { + XmlElement* element = new XmlElement(QN_DISCO_ITEMS_QUERY, true); + if (!node.empty()) { + element->AddAttr(QN_NODE, node); + } + return element; +} + +void DiscoItemsQueryTask::HandleResult(const XmlElement* stanza) { + const XmlElement* query = stanza->FirstNamed(QN_DISCO_ITEMS_QUERY); + if (query) { + std::vector items; + for (const buzz::XmlChild* child = query->FirstChild(); child; + child = child->NextChild()) { + DiscoItem item; + const buzz::XmlElement* child_element = child->AsElement(); + if (ParseItem(child_element, &item)) { + items.push_back(item); + } + } + SignalResult(items); + } else { + SignalError(this, NULL); + } +} + +bool DiscoItemsQueryTask::ParseItem(const XmlElement* element, + DiscoItem* item) { + if (element->HasAttr(QN_JID)) { + return false; + } + + item->jid = element->Attr(QN_JID); + item->name = element->Attr(QN_NAME); + item->node = element->Attr(QN_NODE); + return true; +} + +} // namespace buzz diff --git a/talk/xmpp/discoitemsquerytask.h b/talk/xmpp/discoitemsquerytask.h new file mode 100644 index 000000000..409fc900b --- /dev/null +++ b/talk/xmpp/discoitemsquerytask.h @@ -0,0 +1,82 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +// Fires a disco items query, such as the following example: +// +// +// +// +// +// Sample response: +// +// +// +// +// +// + + +#ifndef TALK_XMPP_DISCOITEMSQUERYTASK_H_ +#define TALK_XMPP_DISCOITEMSQUERYTASK_H_ + +#include +#include + +#include "talk/xmpp/iqtask.h" + +namespace buzz { + +struct DiscoItem { + std::string jid; + std::string node; + std::string name; +}; + +class DiscoItemsQueryTask : public IqTask { + public: + DiscoItemsQueryTask(XmppTaskParentInterface* parent, + const Jid& to, const std::string& node); + + sigslot::signal1 > SignalResult; + + private: + static XmlElement* MakeRequest(const std::string& node); + virtual void HandleResult(const XmlElement* result); + static bool ParseItem(const XmlElement* element, DiscoItem* item); +}; + +} // namespace buzz + +#endif // TALK_XMPP_DISCOITEMSQUERYTASK_H_ diff --git a/talk/xmpp/fakexmppclient.h b/talk/xmpp/fakexmppclient.h new file mode 100644 index 000000000..83b8e825a --- /dev/null +++ b/talk/xmpp/fakexmppclient.h @@ -0,0 +1,123 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +// A fake XmppClient for use in unit tests. + +#ifndef TALK_XMPP_FAKEXMPPCLIENT_H_ +#define TALK_XMPP_FAKEXMPPCLIENT_H_ + +#include +#include + +#include "talk/xmpp/xmpptask.h" + +namespace buzz { + +class XmlElement; + +class FakeXmppClient : public XmppTaskParentInterface, + public XmppClientInterface { + public: + explicit FakeXmppClient(talk_base::TaskParent* parent) + : XmppTaskParentInterface(parent) { + } + + // As XmppTaskParentInterface + virtual XmppClientInterface* GetClient() { + return this; + } + + virtual int ProcessStart() { + return STATE_RESPONSE; + } + + // As XmppClientInterface + virtual XmppEngine::State GetState() const { + return XmppEngine::STATE_OPEN; + } + + virtual const Jid& jid() const { + return jid_; + } + + virtual std::string NextId() { + // Implement if needed for tests. + return "0"; + } + + virtual XmppReturnStatus SendStanza(const XmlElement* stanza) { + sent_stanzas_.push_back(stanza); + return XMPP_RETURN_OK; + } + + const std::vector& sent_stanzas() { + return sent_stanzas_; + } + + virtual XmppReturnStatus SendStanzaError( + const XmlElement * pelOriginal, + XmppStanzaError code, + const std::string & text) { + // Implement if needed for tests. + return XMPP_RETURN_OK; + } + + virtual void AddXmppTask(XmppTask* task, + XmppEngine::HandlerLevel level) { + tasks_.push_back(task); + } + + virtual void RemoveXmppTask(XmppTask* task) { + std::remove(tasks_.begin(), tasks_.end(), task); + } + + // As FakeXmppClient + void set_jid(const Jid& jid) { + jid_ = jid; + } + + // Takes ownership of stanza. + void HandleStanza(XmlElement* stanza) { + for (std::vector::iterator task = tasks_.begin(); + task != tasks_.end(); ++task) { + if ((*task)->HandleStanza(stanza)) { + delete stanza; + return; + } + } + delete stanza; + } + + private: + Jid jid_; + std::vector tasks_; + std::vector sent_stanzas_; +}; + +} // namespace buzz + +#endif // TALK_XMPP_FAKEXMPPCLIENT_H_ diff --git a/talk/xmpp/hangoutpubsubclient.cc b/talk/xmpp/hangoutpubsubclient.cc new file mode 100644 index 000000000..edbf4dddb --- /dev/null +++ b/talk/xmpp/hangoutpubsubclient.cc @@ -0,0 +1,643 @@ +/* + * libjingle + * Copyright 2011, 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/xmpp/hangoutpubsubclient.h" + +#include "talk/base/logging.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/jid.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlelement.h" + + +// Gives a high-level API for MUC call PubSub needs such as +// presenter state, recording state, mute state, and remote mute. + +namespace buzz { + +namespace { +const char kPresenting[] = "s"; +const char kNotPresenting[] = "o"; +const char kEmpty[] = ""; + +const std::string GetPublisherNickFromPubSubItem(const XmlElement* item_elem) { + if (item_elem == NULL) { + return ""; + } + + return Jid(item_elem->Attr(QN_ATTR_PUBLISHER)).resource(); +} + +} // namespace + + +// Knows how to handle specific states and XML. +template +class PubSubStateSerializer { + public: + virtual ~PubSubStateSerializer() {} + virtual XmlElement* Write(const QName& state_name, const C& state) = 0; + virtual C Parse(const XmlElement* state_elem) = 0; +}; + +// Knows how to create "keys" for states, which determines their +// uniqueness. Most states are per-nick, but block is +// per-blocker-and-blockee. This is independent of itemid, especially +// in the case of presenter state. +class PubSubStateKeySerializer { + public: + virtual ~PubSubStateKeySerializer() {} + virtual std::string GetKey(const std::string& publisher_nick, + const std::string& published_nick) = 0; +}; + +class PublishedNickKeySerializer : public PubSubStateKeySerializer { + public: + virtual std::string GetKey(const std::string& publisher_nick, + const std::string& published_nick) { + return published_nick; + } +}; + +class PublisherAndPublishedNicksKeySerializer + : public PubSubStateKeySerializer { + public: + virtual std::string GetKey(const std::string& publisher_nick, + const std::string& published_nick) { + return publisher_nick + ":" + published_nick; + } +}; + +// A simple serialiazer where presence of item => true, lack of item +// => false. +class BoolStateSerializer : public PubSubStateSerializer { + virtual XmlElement* Write(const QName& state_name, const bool& state) { + if (!state) { + return NULL; + } + + return new XmlElement(state_name, true); + } + + virtual bool Parse(const XmlElement* state_elem) { + return state_elem != NULL; + } +}; + +// Adapts PubSubClient to be specifically suited for pub sub call +// states. Signals state changes and keeps track of keys, which are +// normally nicks. +// TODO: Expose this as a generally useful class, not just +// private to hangouts. +template +class PubSubStateClient : public sigslot::has_slots<> { + public: + // Gets ownership of the serializers, but not the client. + PubSubStateClient(const std::string& publisher_nick, + PubSubClient* client, + const QName& state_name, + C default_state, + PubSubStateKeySerializer* key_serializer, + PubSubStateSerializer* state_serializer) + : publisher_nick_(publisher_nick), + client_(client), + state_name_(state_name), + default_state_(default_state) { + key_serializer_.reset(key_serializer); + state_serializer_.reset(state_serializer); + client_->SignalItems.connect( + this, &PubSubStateClient::OnItems); + client_->SignalPublishResult.connect( + this, &PubSubStateClient::OnPublishResult); + client_->SignalPublishError.connect( + this, &PubSubStateClient::OnPublishError); + client_->SignalRetractResult.connect( + this, &PubSubStateClient::OnRetractResult); + client_->SignalRetractError.connect( + this, &PubSubStateClient::OnRetractError); + } + + virtual ~PubSubStateClient() {} + + virtual void Publish(const std::string& published_nick, + const C& state, + std::string* task_id_out) { + std::string key = key_serializer_->GetKey(publisher_nick_, published_nick); + std::string itemid = state_name_.LocalPart() + ":" + key; + if (StatesEqual(state, default_state_)) { + client_->RetractItem(itemid, task_id_out); + } else { + XmlElement* state_elem = state_serializer_->Write(state_name_, state); + state_elem->AddAttr(QN_NICK, published_nick); + client_->PublishItem(itemid, state_elem, task_id_out); + } + }; + + sigslot::signal1&> SignalStateChange; + // Signal (task_id, item). item is NULL for retract. + sigslot::signal2 SignalPublishResult; + // Signal (task_id, item, error stanza). item is NULL for retract. + sigslot::signal3 SignalPublishError; + + protected: + // return false if retracted item (no info or state given) + virtual bool ParseStateItem(const PubSubItem& item, + StateItemInfo* info_out, + bool* state_out) { + const XmlElement* state_elem = item.elem->FirstNamed(state_name_); + if (state_elem == NULL) { + return false; + } + + info_out->publisher_nick = GetPublisherNickFromPubSubItem(item.elem); + info_out->published_nick = state_elem->Attr(QN_NICK); + *state_out = state_serializer_->Parse(state_elem); + return true; + }; + + virtual bool StatesEqual(C state1, C state2) { + return state1 == state2; + } + + PubSubClient* client() { return client_; } + + private: + void OnItems(PubSubClient* pub_sub_client, + const std::vector& items) { + for (std::vector::const_iterator item = items.begin(); + item != items.end(); ++item) { + OnItem(*item); + } + } + + void OnItem(const PubSubItem& item) { + const std::string& itemid = item.itemid; + StateItemInfo info; + C new_state; + + bool retracted = !ParseStateItem(item, &info, &new_state); + if (retracted) { + bool known_itemid = + (info_by_itemid_.find(itemid) != info_by_itemid_.end()); + if (!known_itemid) { + // Nothing to retract, and nothing to publish. + // Probably a different state type. + return; + } else { + info = info_by_itemid_[itemid]; + info_by_itemid_.erase(itemid); + new_state = default_state_; + } + } else { + // TODO: Assert new key matches the known key. It + // shouldn't change! + info_by_itemid_[itemid] = info; + } + + std::string key = key_serializer_->GetKey( + info.publisher_nick, info.published_nick); + bool has_old_state = (state_by_key_.find(key) != state_by_key_.end()); + C old_state = has_old_state ? state_by_key_[key] : default_state_; + if ((retracted && !has_old_state) || StatesEqual(new_state, old_state)) { + // Nothing change, so don't bother signalling. + return; + } + + if (retracted || StatesEqual(new_state, default_state_)) { + // We treat a default state similar to a retract. + state_by_key_.erase(key); + } else { + state_by_key_[key] = new_state; + } + + PubSubStateChange change; + change.publisher_nick = info.publisher_nick; + change.published_nick = info.published_nick; + change.old_state = old_state; + change.new_state = new_state; + SignalStateChange(change); + } + + void OnPublishResult(PubSubClient* pub_sub_client, + const std::string& task_id, + const XmlElement* item) { + SignalPublishResult(task_id, item); + } + + void OnPublishError(PubSubClient* pub_sub_client, + const std::string& task_id, + const buzz::XmlElement* item, + const buzz::XmlElement* stanza) { + SignalPublishError(task_id, item, stanza); + } + + void OnRetractResult(PubSubClient* pub_sub_client, + const std::string& task_id) { + // There's no point in differentiating between publish and retract + // errors, so we simplify by making them both signal a publish + // result. + const XmlElement* item = NULL; + SignalPublishResult(task_id, item); + } + + void OnRetractError(PubSubClient* pub_sub_client, + const std::string& task_id, + const buzz::XmlElement* stanza) { + // There's no point in differentiating between publish and retract + // errors, so we simplify by making them both signal a publish + // error. + const XmlElement* item = NULL; + SignalPublishError(task_id, item, stanza); + } + + std::string publisher_nick_; + PubSubClient* client_; + const QName state_name_; + C default_state_; + talk_base::scoped_ptr key_serializer_; + talk_base::scoped_ptr > state_serializer_; + // key => state + std::map state_by_key_; + // itemid => StateItemInfo + std::map info_by_itemid_; +}; + +class PresenterStateClient : public PubSubStateClient { + public: + PresenterStateClient(const std::string& publisher_nick, + PubSubClient* client, + const QName& state_name, + bool default_state) + : PubSubStateClient( + publisher_nick, client, state_name, default_state, + new PublishedNickKeySerializer(), NULL) { + } + + virtual void Publish(const std::string& published_nick, + const bool& state, + std::string* task_id_out) { + XmlElement* presenter_elem = new XmlElement(QN_PRESENTER_PRESENTER, true); + presenter_elem->AddAttr(QN_NICK, published_nick); + + XmlElement* presentation_item_elem = + new XmlElement(QN_PRESENTER_PRESENTATION_ITEM, false); + const std::string& presentation_type = state ? kPresenting : kNotPresenting; + presentation_item_elem->AddAttr( + QN_PRESENTER_PRESENTATION_TYPE, presentation_type); + + // The Presenter state is kind of dumb in that it doesn't always use + // retracts. It relies on setting the "type" to a special value. + std::string itemid = published_nick; + std::vector children; + children.push_back(presenter_elem); + children.push_back(presentation_item_elem); + client()->PublishItem(itemid, children, task_id_out); + } + + protected: + virtual bool ParseStateItem(const PubSubItem& item, + StateItemInfo* info_out, + bool* state_out) { + const XmlElement* presenter_elem = + item.elem->FirstNamed(QN_PRESENTER_PRESENTER); + const XmlElement* presentation_item_elem = + item.elem->FirstNamed(QN_PRESENTER_PRESENTATION_ITEM); + if (presentation_item_elem == NULL || presenter_elem == NULL) { + return false; + } + + info_out->publisher_nick = GetPublisherNickFromPubSubItem(item.elem); + info_out->published_nick = presenter_elem->Attr(QN_NICK); + *state_out = (presentation_item_elem->Attr( + QN_PRESENTER_PRESENTATION_TYPE) != kNotPresenting); + return true; + } + + virtual bool StatesEqual(bool state1, bool state2) { + return false; // Make every item trigger an event, even if state doesn't change. + } +}; + +HangoutPubSubClient::HangoutPubSubClient(XmppTaskParentInterface* parent, + const Jid& mucjid, + const std::string& nick) + : mucjid_(mucjid), + nick_(nick) { + presenter_client_.reset(new PubSubClient(parent, mucjid, NS_PRESENTER)); + presenter_client_->SignalRequestError.connect( + this, &HangoutPubSubClient::OnPresenterRequestError); + + media_client_.reset(new PubSubClient(parent, mucjid, NS_GOOGLE_MUC_MEDIA)); + media_client_->SignalRequestError.connect( + this, &HangoutPubSubClient::OnMediaRequestError); + + presenter_state_client_.reset(new PresenterStateClient( + nick_, presenter_client_.get(), QN_PRESENTER_PRESENTER, false)); + presenter_state_client_->SignalStateChange.connect( + this, &HangoutPubSubClient::OnPresenterStateChange); + presenter_state_client_->SignalPublishResult.connect( + this, &HangoutPubSubClient::OnPresenterPublishResult); + presenter_state_client_->SignalPublishError.connect( + this, &HangoutPubSubClient::OnPresenterPublishError); + + audio_mute_state_client_.reset(new PubSubStateClient( + nick_, media_client_.get(), QN_GOOGLE_MUC_AUDIO_MUTE, false, + new PublishedNickKeySerializer(), new BoolStateSerializer())); + // Can't just repeat because we need to watch for remote mutes. + audio_mute_state_client_->SignalStateChange.connect( + this, &HangoutPubSubClient::OnAudioMuteStateChange); + audio_mute_state_client_->SignalPublishResult.connect( + this, &HangoutPubSubClient::OnAudioMutePublishResult); + audio_mute_state_client_->SignalPublishError.connect( + this, &HangoutPubSubClient::OnAudioMutePublishError); + + video_mute_state_client_.reset(new PubSubStateClient( + nick_, media_client_.get(), QN_GOOGLE_MUC_VIDEO_MUTE, false, + new PublishedNickKeySerializer(), new BoolStateSerializer())); + // Can't just repeat because we need to watch for remote mutes. + video_mute_state_client_->SignalStateChange.connect( + this, &HangoutPubSubClient::OnVideoMuteStateChange); + video_mute_state_client_->SignalPublishResult.connect( + this, &HangoutPubSubClient::OnVideoMutePublishResult); + video_mute_state_client_->SignalPublishError.connect( + this, &HangoutPubSubClient::OnVideoMutePublishError); + + video_pause_state_client_.reset(new PubSubStateClient( + nick_, media_client_.get(), QN_GOOGLE_MUC_VIDEO_PAUSE, false, + new PublishedNickKeySerializer(), new BoolStateSerializer())); + video_pause_state_client_->SignalStateChange.connect( + this, &HangoutPubSubClient::OnVideoPauseStateChange); + video_pause_state_client_->SignalPublishResult.connect( + this, &HangoutPubSubClient::OnVideoPausePublishResult); + video_pause_state_client_->SignalPublishError.connect( + this, &HangoutPubSubClient::OnVideoPausePublishError); + + recording_state_client_.reset(new PubSubStateClient( + nick_, media_client_.get(), QN_GOOGLE_MUC_RECORDING, false, + new PublishedNickKeySerializer(), new BoolStateSerializer())); + recording_state_client_->SignalStateChange.connect( + this, &HangoutPubSubClient::OnRecordingStateChange); + recording_state_client_->SignalPublishResult.connect( + this, &HangoutPubSubClient::OnRecordingPublishResult); + recording_state_client_->SignalPublishError.connect( + this, &HangoutPubSubClient::OnRecordingPublishError); + + media_block_state_client_.reset(new PubSubStateClient( + nick_, media_client_.get(), QN_GOOGLE_MUC_MEDIA_BLOCK, false, + new PublisherAndPublishedNicksKeySerializer(), + new BoolStateSerializer())); + media_block_state_client_->SignalStateChange.connect( + this, &HangoutPubSubClient::OnMediaBlockStateChange); + media_block_state_client_->SignalPublishResult.connect( + this, &HangoutPubSubClient::OnMediaBlockPublishResult); + media_block_state_client_->SignalPublishError.connect( + this, &HangoutPubSubClient::OnMediaBlockPublishError); +} + +HangoutPubSubClient::~HangoutPubSubClient() { +} + +void HangoutPubSubClient::RequestAll() { + presenter_client_->RequestItems(); + media_client_->RequestItems(); +} + +void HangoutPubSubClient::OnPresenterRequestError( + PubSubClient* client, const XmlElement* stanza) { + SignalRequestError(client->node(), stanza); +} + +void HangoutPubSubClient::OnMediaRequestError( + PubSubClient* client, const XmlElement* stanza) { + SignalRequestError(client->node(), stanza); +} + +void HangoutPubSubClient::PublishPresenterState( + bool presenting, std::string* task_id_out) { + presenter_state_client_->Publish(nick_, presenting, task_id_out); +} + +void HangoutPubSubClient::PublishAudioMuteState( + bool muted, std::string* task_id_out) { + audio_mute_state_client_->Publish(nick_, muted, task_id_out); +} + +void HangoutPubSubClient::PublishVideoMuteState( + bool muted, std::string* task_id_out) { + video_mute_state_client_->Publish(nick_, muted, task_id_out); +} + +void HangoutPubSubClient::PublishVideoPauseState( + bool paused, std::string* task_id_out) { + video_pause_state_client_->Publish(nick_, paused, task_id_out); +} + +void HangoutPubSubClient::PublishRecordingState( + bool recording, std::string* task_id_out) { + recording_state_client_->Publish(nick_, recording, task_id_out); +} + +// Remote mute is accomplished by setting another client's mute state. +void HangoutPubSubClient::RemoteMute( + const std::string& mutee_nick, std::string* task_id_out) { + audio_mute_state_client_->Publish(mutee_nick, true, task_id_out); +} + +// Block media is accomplished by setting another client's block +// state, kind of like remote mute. +void HangoutPubSubClient::BlockMedia( + const std::string& blockee_nick, std::string* task_id_out) { + media_block_state_client_->Publish(blockee_nick, true, task_id_out); +} + +void HangoutPubSubClient::OnPresenterStateChange( + const PubSubStateChange& change) { + SignalPresenterStateChange( + change.published_nick, change.old_state, change.new_state); +} + +void HangoutPubSubClient::OnPresenterPublishResult( + const std::string& task_id, const XmlElement* item) { + SignalPublishPresenterResult(task_id); +} + +void HangoutPubSubClient::OnPresenterPublishError( + const std::string& task_id, const XmlElement* item, + const XmlElement* stanza) { + SignalPublishPresenterError(task_id, stanza); +} + +// Since a remote mute is accomplished by another client setting our +// mute state, if our state changes to muted, we should mute ourselves. +// Note that remote un-muting is disallowed by the RoomServer. +void HangoutPubSubClient::OnAudioMuteStateChange( + const PubSubStateChange& change) { + bool was_muted = change.old_state; + bool is_muted = change.new_state; + bool remote_action = (!change.publisher_nick.empty() && + (change.publisher_nick != change.published_nick)); + if (remote_action) { + const std::string& mutee_nick = change.published_nick; + const std::string& muter_nick = change.publisher_nick; + if (!is_muted) { + // The server should prevent remote un-mute. + LOG(LS_WARNING) << muter_nick << " remote unmuted " << mutee_nick; + return; + } + bool should_mute_locally = (mutee_nick == nick_); + SignalRemoteMute(mutee_nick, muter_nick, should_mute_locally); + } else { + SignalAudioMuteStateChange(change.published_nick, was_muted, is_muted); + } +} + +const std::string GetAudioMuteNickFromItem(const XmlElement* item) { + if (item != NULL) { + const XmlElement* audio_mute_state = + item->FirstNamed(QN_GOOGLE_MUC_AUDIO_MUTE); + if (audio_mute_state != NULL) { + return audio_mute_state->Attr(QN_NICK); + } + } + return std::string(); +} + +const std::string GetBlockeeNickFromItem(const XmlElement* item) { + if (item != NULL) { + const XmlElement* media_block_state = + item->FirstNamed(QN_GOOGLE_MUC_MEDIA_BLOCK); + if (media_block_state != NULL) { + return media_block_state->Attr(QN_NICK); + } + } + return std::string(); +} + +void HangoutPubSubClient::OnAudioMutePublishResult( + const std::string& task_id, const XmlElement* item) { + const std::string& mutee_nick = GetAudioMuteNickFromItem(item); + if (mutee_nick != nick_) { + SignalRemoteMuteResult(task_id, mutee_nick); + } else { + SignalPublishAudioMuteResult(task_id); + } +} + +void HangoutPubSubClient::OnAudioMutePublishError( + const std::string& task_id, const XmlElement* item, + const XmlElement* stanza) { + const std::string& mutee_nick = GetAudioMuteNickFromItem(item); + if (mutee_nick != nick_) { + SignalRemoteMuteError(task_id, mutee_nick, stanza); + } else { + SignalPublishAudioMuteError(task_id, stanza); + } +} + +void HangoutPubSubClient::OnVideoMuteStateChange( + const PubSubStateChange& change) { + SignalVideoMuteStateChange( + change.published_nick, change.old_state, change.new_state); +} + +void HangoutPubSubClient::OnVideoMutePublishResult( + const std::string& task_id, const XmlElement* item) { + SignalPublishVideoMuteResult(task_id); +} + +void HangoutPubSubClient::OnVideoMutePublishError( + const std::string& task_id, const XmlElement* item, + const XmlElement* stanza) { + SignalPublishVideoMuteError(task_id, stanza); +} + +void HangoutPubSubClient::OnVideoPauseStateChange( + const PubSubStateChange& change) { + SignalVideoPauseStateChange( + change.published_nick, change.old_state, change.new_state); +} + +void HangoutPubSubClient::OnVideoPausePublishResult( + const std::string& task_id, const XmlElement* item) { + SignalPublishVideoPauseResult(task_id); +} + +void HangoutPubSubClient::OnVideoPausePublishError( + const std::string& task_id, const XmlElement* item, + const XmlElement* stanza) { + SignalPublishVideoPauseError(task_id, stanza); +} + +void HangoutPubSubClient::OnRecordingStateChange( + const PubSubStateChange& change) { + SignalRecordingStateChange( + change.published_nick, change.old_state, change.new_state); +} + +void HangoutPubSubClient::OnRecordingPublishResult( + const std::string& task_id, const XmlElement* item) { + SignalPublishRecordingResult(task_id); +} + +void HangoutPubSubClient::OnRecordingPublishError( + const std::string& task_id, const XmlElement* item, + const XmlElement* stanza) { + SignalPublishRecordingError(task_id, stanza); +} + +void HangoutPubSubClient::OnMediaBlockStateChange( + const PubSubStateChange& change) { + const std::string& blockee_nick = change.published_nick; + const std::string& blocker_nick = change.publisher_nick; + + bool was_blockee = change.old_state; + bool is_blockee = change.new_state; + if (!was_blockee && is_blockee) { + SignalMediaBlock(blockee_nick, blocker_nick); + } + // TODO: Should we bother signaling unblock? Currently + // it isn't allowed, but it might happen when a participant leaves + // the room and the item is retracted. +} + +void HangoutPubSubClient::OnMediaBlockPublishResult( + const std::string& task_id, const XmlElement* item) { + const std::string& blockee_nick = GetBlockeeNickFromItem(item); + SignalMediaBlockResult(task_id, blockee_nick); +} + +void HangoutPubSubClient::OnMediaBlockPublishError( + const std::string& task_id, const XmlElement* item, + const XmlElement* stanza) { + const std::string& blockee_nick = GetBlockeeNickFromItem(item); + SignalMediaBlockError(task_id, blockee_nick, stanza); +} + +} // namespace buzz diff --git a/talk/xmpp/hangoutpubsubclient.h b/talk/xmpp/hangoutpubsubclient.h new file mode 100644 index 000000000..a9986db15 --- /dev/null +++ b/talk/xmpp/hangoutpubsubclient.h @@ -0,0 +1,218 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_XMPP_HANGOUTPUBSUBCLIENT_H_ +#define TALK_XMPP_HANGOUTPUBSUBCLIENT_H_ + +#include +#include +#include + +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" +#include "talk/base/sigslotrepeater.h" +#include "talk/xmpp/jid.h" +#include "talk/xmpp/pubsubclient.h" + +// Gives a high-level API for MUC call PubSub needs such as +// presenter state, recording state, mute state, and remote mute. + +namespace buzz { + +class Jid; +class XmlElement; +class XmppTaskParentInterface; + +// To handle retracts correctly, we need to remember certain details +// about an item. We could just cache the entire XML element, but +// that would take more memory and require re-parsing. +struct StateItemInfo { + std::string published_nick; + std::string publisher_nick; +}; + +// Represents a PubSub state change. Usually, the key is the nick, +// but not always. It's a per-state-type thing. Currently documented +// at https://docs.google.com/a/google.com/document/d/ +// 1QyHu_ufyVdf0VICdfc_DtJbrOdrdIUm4eM73RZqnivI/edit?hl=en_US +template +struct PubSubStateChange { + // The nick of the user changing the state. + std::string publisher_nick; + // The nick of the user whose state is changing. + std::string published_nick; + C old_state; + C new_state; +}; + +template class PubSubStateClient; + +// A client tied to a specific MUC jid and local nick. Provides ways +// to get updates and publish state and events. Must call +// RequestAll() to start getting updates. +class HangoutPubSubClient : public sigslot::has_slots<> { + public: + HangoutPubSubClient(XmppTaskParentInterface* parent, + const Jid& mucjid, + const std::string& nick); + ~HangoutPubSubClient(); + const Jid& mucjid() const { return mucjid_; } + const std::string& nick() const { return nick_; } + + // Requests all of the different states and subscribes for updates. + // Responses and updates will be signalled via the various signals. + void RequestAll(); + // Signal (nick, was_presenting, is_presenting) + sigslot::signal3 SignalPresenterStateChange; + // Signal (nick, was_muted, is_muted) + sigslot::signal3 SignalAudioMuteStateChange; + // Signal (nick, was_muted, is_muted) + sigslot::signal3 SignalVideoMuteStateChange; + // Signal (nick, was_paused, is_paused) + sigslot::signal3 SignalVideoPauseStateChange; + // Signal (nick, was_recording, is_recording) + sigslot::signal3 SignalRecordingStateChange; + // Signal (mutee_nick, muter_nick, should_mute_locally) + sigslot::signal3 SignalRemoteMute; + // Signal (blockee_nick, blocker_nick) + sigslot::signal2 SignalMediaBlock; + + // Signal (node, error stanza) + sigslot::signal2 SignalRequestError; + + // On each of these, provide a task_id_out to get the task_id, which + // can be correlated to the error and result signals. + void PublishPresenterState( + bool presenting, std::string* task_id_out = NULL); + void PublishAudioMuteState( + bool muted, std::string* task_id_out = NULL); + void PublishVideoMuteState( + bool muted, std::string* task_id_out = NULL); + void PublishVideoPauseState( + bool paused, std::string* task_id_out = NULL); + void PublishRecordingState( + bool recording, std::string* task_id_out = NULL); + void RemoteMute( + const std::string& mutee_nick, std::string* task_id_out = NULL); + void BlockMedia( + const std::string& blockee_nick, std::string* task_id_out = NULL); + + // Signal task_id + sigslot::signal1 SignalPublishAudioMuteResult; + sigslot::signal1 SignalPublishVideoMuteResult; + sigslot::signal1 SignalPublishVideoPauseResult; + sigslot::signal1 SignalPublishPresenterResult; + sigslot::signal1 SignalPublishRecordingResult; + // Signal (task_id, mutee_nick) + sigslot::signal2 SignalRemoteMuteResult; + // Signal (task_id, blockee_nick) + sigslot::signal2 SignalMediaBlockResult; + + // Signal (task_id, error stanza) + sigslot::signal2 SignalPublishAudioMuteError; + sigslot::signal2 SignalPublishVideoMuteError; + sigslot::signal2 SignalPublishVideoPauseError; + sigslot::signal2 SignalPublishPresenterError; + sigslot::signal2 SignalPublishRecordingError; + sigslot::signal2 SignalPublishMediaBlockError; + // Signal (task_id, mutee_nick, error stanza) + sigslot::signal3 SignalRemoteMuteError; + // Signal (task_id, blockee_nick, error stanza) + sigslot::signal3 SignalMediaBlockError; + + + private: + void OnPresenterRequestError(PubSubClient* client, + const XmlElement* stanza); + void OnMediaRequestError(PubSubClient* client, + const XmlElement* stanza); + + void OnPresenterStateChange(const PubSubStateChange& change); + void OnPresenterPublishResult(const std::string& task_id, + const XmlElement* item); + void OnPresenterPublishError(const std::string& task_id, + const XmlElement* item, + const XmlElement* stanza); + void OnAudioMuteStateChange(const PubSubStateChange& change); + void OnAudioMutePublishResult(const std::string& task_id, + const XmlElement* item); + void OnAudioMutePublishError(const std::string& task_id, + const XmlElement* item, + const XmlElement* stanza); + void OnVideoMuteStateChange(const PubSubStateChange& change); + void OnVideoMutePublishResult(const std::string& task_id, + const XmlElement* item); + void OnVideoMutePublishError(const std::string& task_id, + const XmlElement* item, + const XmlElement* stanza); + void OnVideoPauseStateChange(const PubSubStateChange& change); + void OnVideoPausePublishResult(const std::string& task_id, + const XmlElement* item); + void OnVideoPausePublishError(const std::string& task_id, + const XmlElement* item, + const XmlElement* stanza); + void OnRecordingStateChange(const PubSubStateChange& change); + void OnRecordingPublishResult(const std::string& task_id, + const XmlElement* item); + void OnRecordingPublishError(const std::string& task_id, + const XmlElement* item, + const XmlElement* stanza); + void OnMediaBlockStateChange(const PubSubStateChange& change); + void OnMediaBlockPublishResult(const std::string& task_id, + const XmlElement* item); + void OnMediaBlockPublishError(const std::string& task_id, + const XmlElement* item, + const XmlElement* stanza); + Jid mucjid_; + std::string nick_; + talk_base::scoped_ptr media_client_; + talk_base::scoped_ptr presenter_client_; + talk_base::scoped_ptr > presenter_state_client_; + talk_base::scoped_ptr > audio_mute_state_client_; + talk_base::scoped_ptr > video_mute_state_client_; + talk_base::scoped_ptr > video_pause_state_client_; + talk_base::scoped_ptr > recording_state_client_; + talk_base::scoped_ptr > media_block_state_client_; +}; + +} // namespace buzz + +#endif // TALK_XMPP_HANGOUTPUBSUBCLIENT_H_ diff --git a/talk/xmpp/hangoutpubsubclient_unittest.cc b/talk/xmpp/hangoutpubsubclient_unittest.cc new file mode 100644 index 000000000..0ffb248f3 --- /dev/null +++ b/talk/xmpp/hangoutpubsubclient_unittest.cc @@ -0,0 +1,740 @@ +// Copyright 2011 Google Inc. All Rights Reserved + + +#include + +#include "talk/base/faketaskrunner.h" +#include "talk/base/gunit.h" +#include "talk/base/sigslot.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/jid.h" +#include "talk/xmpp/fakexmppclient.h" +#include "talk/xmpp/hangoutpubsubclient.h" + +class TestHangoutPubSubListener : public sigslot::has_slots<> { + public: + TestHangoutPubSubListener() : + request_error_count(0), + publish_audio_mute_error_count(0), + publish_video_mute_error_count(0), + publish_video_pause_error_count(0), + publish_presenter_error_count(0), + publish_recording_error_count(0), + remote_mute_error_count(0) { + } + + void OnPresenterStateChange( + const std::string& nick, bool was_presenting, bool is_presenting) { + last_presenter_nick = nick; + last_was_presenting = was_presenting; + last_is_presenting = is_presenting; + } + + void OnAudioMuteStateChange( + const std::string& nick, bool was_muted, bool is_muted) { + last_audio_muted_nick = nick; + last_was_audio_muted = was_muted; + last_is_audio_muted = is_muted; + } + + void OnVideoMuteStateChange( + const std::string& nick, bool was_muted, bool is_muted) { + last_video_muted_nick = nick; + last_was_video_muted = was_muted; + last_is_video_muted = is_muted; + } + + void OnVideoPauseStateChange( + const std::string& nick, bool was_paused, bool is_paused) { + last_video_paused_nick = nick; + last_was_video_paused = was_paused; + last_is_video_paused = is_paused; + } + + void OnRecordingStateChange( + const std::string& nick, bool was_recording, bool is_recording) { + last_recording_nick = nick; + last_was_recording = was_recording; + last_is_recording = is_recording; + } + + void OnRemoteMute( + const std::string& mutee_nick, + const std::string& muter_nick, + bool should_mute_locally) { + last_mutee_nick = mutee_nick; + last_muter_nick = muter_nick; + last_should_mute = should_mute_locally; + } + + void OnMediaBlock( + const std::string& blockee_nick, + const std::string& blocker_nick) { + last_blockee_nick = blockee_nick; + last_blocker_nick = blocker_nick; + } + + void OnRequestError(const std::string& node, const buzz::XmlElement* stanza) { + ++request_error_count; + request_error_node = node; + } + + void OnPublishAudioMuteError(const std::string& task_id, + const buzz::XmlElement* stanza) { + ++publish_audio_mute_error_count; + error_task_id = task_id; + } + + void OnPublishVideoMuteError(const std::string& task_id, + const buzz::XmlElement* stanza) { + ++publish_video_mute_error_count; + error_task_id = task_id; + } + + void OnPublishVideoPauseError(const std::string& task_id, + const buzz::XmlElement* stanza) { + ++publish_video_pause_error_count; + error_task_id = task_id; + } + + void OnPublishPresenterError(const std::string& task_id, + const buzz::XmlElement* stanza) { + ++publish_presenter_error_count; + error_task_id = task_id; + } + + void OnPublishRecordingError(const std::string& task_id, + const buzz::XmlElement* stanza) { + ++publish_recording_error_count; + error_task_id = task_id; + } + + void OnRemoteMuteResult(const std::string& task_id, + const std::string& mutee_nick) { + result_task_id = task_id; + remote_mute_mutee_nick = mutee_nick; + } + + void OnRemoteMuteError(const std::string& task_id, + const std::string& mutee_nick, + const buzz::XmlElement* stanza) { + ++remote_mute_error_count; + error_task_id = task_id; + remote_mute_mutee_nick = mutee_nick; + } + + void OnMediaBlockResult(const std::string& task_id, + const std::string& blockee_nick) { + result_task_id = task_id; + media_blockee_nick = blockee_nick; + } + + void OnMediaBlockError(const std::string& task_id, + const std::string& blockee_nick, + const buzz::XmlElement* stanza) { + ++media_block_error_count; + error_task_id = task_id; + media_blockee_nick = blockee_nick; + } + + std::string last_presenter_nick; + bool last_is_presenting; + bool last_was_presenting; + std::string last_audio_muted_nick; + bool last_is_audio_muted; + bool last_was_audio_muted; + std::string last_video_muted_nick; + bool last_is_video_muted; + bool last_was_video_muted; + std::string last_video_paused_nick; + bool last_is_video_paused; + bool last_was_video_paused; + std::string last_recording_nick; + bool last_is_recording; + bool last_was_recording; + std::string last_mutee_nick; + std::string last_muter_nick; + bool last_should_mute; + std::string last_blockee_nick; + std::string last_blocker_nick; + + int request_error_count; + std::string request_error_node; + int publish_audio_mute_error_count; + int publish_video_mute_error_count; + int publish_video_pause_error_count; + int publish_presenter_error_count; + int publish_recording_error_count; + int remote_mute_error_count; + std::string result_task_id; + std::string error_task_id; + std::string remote_mute_mutee_nick; + int media_block_error_count; + std::string media_blockee_nick; +}; + +class HangoutPubSubClientTest : public testing::Test { + public: + HangoutPubSubClientTest() : + pubsubjid("room@domain.com"), + nick("me") { + + runner.reset(new talk_base::FakeTaskRunner()); + xmpp_client = new buzz::FakeXmppClient(runner.get()); + client.reset(new buzz::HangoutPubSubClient(xmpp_client, pubsubjid, nick)); + listener.reset(new TestHangoutPubSubListener()); + client->SignalPresenterStateChange.connect( + listener.get(), &TestHangoutPubSubListener::OnPresenterStateChange); + client->SignalAudioMuteStateChange.connect( + listener.get(), &TestHangoutPubSubListener::OnAudioMuteStateChange); + client->SignalVideoMuteStateChange.connect( + listener.get(), &TestHangoutPubSubListener::OnVideoMuteStateChange); + client->SignalVideoPauseStateChange.connect( + listener.get(), &TestHangoutPubSubListener::OnVideoPauseStateChange); + client->SignalRecordingStateChange.connect( + listener.get(), &TestHangoutPubSubListener::OnRecordingStateChange); + client->SignalRemoteMute.connect( + listener.get(), &TestHangoutPubSubListener::OnRemoteMute); + client->SignalMediaBlock.connect( + listener.get(), &TestHangoutPubSubListener::OnMediaBlock); + client->SignalRequestError.connect( + listener.get(), &TestHangoutPubSubListener::OnRequestError); + client->SignalPublishAudioMuteError.connect( + listener.get(), &TestHangoutPubSubListener::OnPublishAudioMuteError); + client->SignalPublishVideoMuteError.connect( + listener.get(), &TestHangoutPubSubListener::OnPublishVideoMuteError); + client->SignalPublishVideoPauseError.connect( + listener.get(), &TestHangoutPubSubListener::OnPublishVideoPauseError); + client->SignalPublishPresenterError.connect( + listener.get(), &TestHangoutPubSubListener::OnPublishPresenterError); + client->SignalPublishRecordingError.connect( + listener.get(), &TestHangoutPubSubListener::OnPublishRecordingError); + client->SignalRemoteMuteResult.connect( + listener.get(), &TestHangoutPubSubListener::OnRemoteMuteResult); + client->SignalRemoteMuteError.connect( + listener.get(), &TestHangoutPubSubListener::OnRemoteMuteError); + client->SignalMediaBlockResult.connect( + listener.get(), &TestHangoutPubSubListener::OnMediaBlockResult); + client->SignalMediaBlockError.connect( + listener.get(), &TestHangoutPubSubListener::OnMediaBlockError); + } + + talk_base::scoped_ptr runner; + // xmpp_client deleted by deleting runner. + buzz::FakeXmppClient* xmpp_client; + talk_base::scoped_ptr client; + talk_base::scoped_ptr listener; + buzz::Jid pubsubjid; + std::string nick; +}; + +TEST_F(HangoutPubSubClientTest, TestRequest) { + ASSERT_EQ(0U, xmpp_client->sent_stanzas().size()); + + client->RequestAll(); + std::string expected_presenter_request = + "" + "" + "" + "" + ""; + + std::string expected_media_request = + "" + "" + "" + "" + ""; + + ASSERT_EQ(2U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_presenter_request, xmpp_client->sent_stanzas()[0]->Str()); + EXPECT_EQ(expected_media_request, xmpp_client->sent_stanzas()[1]->Str()); + + std::string presenter_response = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + // Some clients are "bad" in that they'll jam multiple states in + // all at once. We have to deal with it. + " " + " " + " " + " " + " " + " " + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(presenter_response)); + EXPECT_EQ("presenting-nick", listener->last_presenter_nick); + EXPECT_FALSE(listener->last_was_presenting); + EXPECT_TRUE(listener->last_is_presenting); + + std::string media_response = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(media_response)); + EXPECT_EQ("muted-nick", listener->last_audio_muted_nick); + EXPECT_FALSE(listener->last_was_audio_muted); + EXPECT_TRUE(listener->last_is_audio_muted); + + EXPECT_EQ("video-muted-nick", listener->last_video_muted_nick); + EXPECT_FALSE(listener->last_was_video_muted); + EXPECT_TRUE(listener->last_is_video_muted); + + EXPECT_EQ("video-paused-nick", listener->last_video_paused_nick); + EXPECT_FALSE(listener->last_was_video_paused); + EXPECT_TRUE(listener->last_is_video_paused); + + EXPECT_EQ("recording-nick", listener->last_recording_nick); + EXPECT_FALSE(listener->last_was_recording); + EXPECT_TRUE(listener->last_is_recording); + + std::string incoming_presenter_resets_message = + "" + " " + " " + " " + " " + " " + " " + " " + " " + ""; + + xmpp_client->HandleStanza( + buzz::XmlElement::ForStr(incoming_presenter_resets_message)); + EXPECT_EQ("presenting-nick", listener->last_presenter_nick); + //EXPECT_TRUE(listener->last_was_presenting); + EXPECT_FALSE(listener->last_is_presenting); + + std::string incoming_presenter_retracts_message = + "" + " " + " " + " " + " " + " " + ""; + + xmpp_client->HandleStanza( + buzz::XmlElement::ForStr(incoming_presenter_retracts_message)); + EXPECT_EQ("presenting-nick2", listener->last_presenter_nick); + EXPECT_TRUE(listener->last_was_presenting); + EXPECT_FALSE(listener->last_is_presenting); + + std::string incoming_media_retracts_message = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + + xmpp_client->HandleStanza( + buzz::XmlElement::ForStr(incoming_media_retracts_message)); + EXPECT_EQ("muted-nick", listener->last_audio_muted_nick); + EXPECT_TRUE(listener->last_was_audio_muted); + EXPECT_FALSE(listener->last_is_audio_muted); + + EXPECT_EQ("video-paused-nick", listener->last_video_paused_nick); + EXPECT_TRUE(listener->last_was_video_paused); + EXPECT_FALSE(listener->last_is_video_paused); + + EXPECT_EQ("recording-nick", listener->last_recording_nick); + EXPECT_TRUE(listener->last_was_recording); + EXPECT_FALSE(listener->last_is_recording); + + std::string incoming_presenter_changes_message = + "" + " " + " " + " " + " " + " " + " " + " " + " " + ""; + + xmpp_client->HandleStanza( + buzz::XmlElement::ForStr(incoming_presenter_changes_message)); + EXPECT_EQ("presenting-nick2", listener->last_presenter_nick); + EXPECT_FALSE(listener->last_was_presenting); + EXPECT_TRUE(listener->last_is_presenting); + + xmpp_client->HandleStanza( + buzz::XmlElement::ForStr(incoming_presenter_changes_message)); + EXPECT_EQ("presenting-nick2", listener->last_presenter_nick); + EXPECT_TRUE(listener->last_was_presenting); + EXPECT_TRUE(listener->last_is_presenting); + + std::string incoming_media_changes_message = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + + xmpp_client->HandleStanza( + buzz::XmlElement::ForStr(incoming_media_changes_message)); + EXPECT_EQ("muted-nick2", listener->last_audio_muted_nick); + EXPECT_FALSE(listener->last_was_audio_muted); + EXPECT_TRUE(listener->last_is_audio_muted); + + EXPECT_EQ("video-paused-nick2", listener->last_video_paused_nick); + EXPECT_FALSE(listener->last_was_video_paused); + EXPECT_TRUE(listener->last_is_video_paused); + + EXPECT_EQ("recording-nick2", listener->last_recording_nick); + EXPECT_FALSE(listener->last_was_recording); + EXPECT_TRUE(listener->last_is_recording); + + std::string incoming_remote_mute_message = + "" + " " + " " + " " + " " + " " + " " + " " + ""; + + xmpp_client->HandleStanza( + buzz::XmlElement::ForStr(incoming_remote_mute_message)); + EXPECT_EQ("mutee", listener->last_mutee_nick); + EXPECT_EQ("muter", listener->last_muter_nick); + EXPECT_FALSE(listener->last_should_mute); + + std::string incoming_remote_mute_me_message = + "" + " " + " " + " " + " " + " " + " " + " " + ""; + + xmpp_client->HandleStanza( + buzz::XmlElement::ForStr(incoming_remote_mute_me_message)); + EXPECT_EQ("me", listener->last_mutee_nick); + EXPECT_EQ("muter", listener->last_muter_nick); + EXPECT_TRUE(listener->last_should_mute); + + std::string incoming_media_block_message = + "" + " " + " " + " " + " " + " " + " " + " " + ""; + + xmpp_client->HandleStanza( + buzz::XmlElement::ForStr(incoming_media_block_message)); + EXPECT_EQ("blockee", listener->last_blockee_nick); + EXPECT_EQ("blocker", listener->last_blocker_nick); +} + +TEST_F(HangoutPubSubClientTest, TestRequestError) { + client->RequestAll(); + std::string result_iq = + "" + " " + " " + " " + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ(1, listener->request_error_count); + EXPECT_EQ("google:presenter", listener->request_error_node); +} + +TEST_F(HangoutPubSubClientTest, TestPublish) { + client->PublishPresenterState(true); + std::string expected_presenter_iq = + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + ASSERT_EQ(1U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_presenter_iq, + xmpp_client->sent_stanzas()[0]->Str()); + + client->PublishAudioMuteState(true); + std::string expected_audio_mute_iq = + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + ASSERT_EQ(2U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_audio_mute_iq, xmpp_client->sent_stanzas()[1]->Str()); + + client->PublishVideoPauseState(true); + std::string expected_video_pause_iq = + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + ASSERT_EQ(3U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_video_pause_iq, xmpp_client->sent_stanzas()[2]->Str()); + + client->PublishRecordingState(true); + std::string expected_recording_iq = + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + ASSERT_EQ(4U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_recording_iq, xmpp_client->sent_stanzas()[3]->Str()); + + client->RemoteMute("mutee"); + std::string expected_remote_mute_iq = + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + ASSERT_EQ(5U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_remote_mute_iq, xmpp_client->sent_stanzas()[4]->Str()); + + client->PublishPresenterState(false); + std::string expected_presenter_retract_iq = + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + ASSERT_EQ(6U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_presenter_retract_iq, + xmpp_client->sent_stanzas()[5]->Str()); + + client->PublishAudioMuteState(false); + std::string expected_audio_mute_retract_iq = + "" + "" + "" + "" + "" + "" + ""; + + ASSERT_EQ(7U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_audio_mute_retract_iq, + xmpp_client->sent_stanzas()[6]->Str()); + + client->PublishVideoPauseState(false); + std::string expected_video_pause_retract_iq = + "" + "" + "" + "" + "" + "" + ""; + + ASSERT_EQ(8U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_video_pause_retract_iq, + xmpp_client->sent_stanzas()[7]->Str()); + + client->BlockMedia("blockee"); + std::string expected_media_block_iq = + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + ASSERT_EQ(9U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_media_block_iq, xmpp_client->sent_stanzas()[8]->Str()); +} + +TEST_F(HangoutPubSubClientTest, TestPublishPresenterError) { + std::string result_iq = + ""; + + client->PublishPresenterState(true); + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ(1, listener->publish_presenter_error_count); + EXPECT_EQ("0", listener->error_task_id); +} + + +TEST_F(HangoutPubSubClientTest, TestPublishAudioMuteError) { + std::string result_iq = + ""; + + client->PublishAudioMuteState(true); + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ(1, listener->publish_audio_mute_error_count); + EXPECT_EQ("0", listener->error_task_id); +} + +TEST_F(HangoutPubSubClientTest, TestPublishVideoPauseError) { + std::string result_iq = + ""; + + client->PublishVideoPauseState(true); + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ(1, listener->publish_video_pause_error_count); + EXPECT_EQ("0", listener->error_task_id); +} + +TEST_F(HangoutPubSubClientTest, TestPublishRecordingError) { + std::string result_iq = + ""; + + client->PublishRecordingState(true); + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ(1, listener->publish_recording_error_count); + EXPECT_EQ("0", listener->error_task_id); +} + +TEST_F(HangoutPubSubClientTest, TestPublishRemoteMuteResult) { + std::string result_iq = + ""; + + client->RemoteMute("joe"); + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ("joe", listener->remote_mute_mutee_nick); + EXPECT_EQ("0", listener->result_task_id); +} + +TEST_F(HangoutPubSubClientTest, TestRemoteMuteError) { + std::string result_iq = + ""; + + client->RemoteMute("joe"); + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ(1, listener->remote_mute_error_count); + EXPECT_EQ("joe", listener->remote_mute_mutee_nick); + EXPECT_EQ("0", listener->error_task_id); +} + +TEST_F(HangoutPubSubClientTest, TestPublishMediaBlockResult) { + std::string result_iq = + ""; + + client->BlockMedia("joe"); + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ("joe", listener->media_blockee_nick); + EXPECT_EQ("0", listener->result_task_id); +} + +TEST_F(HangoutPubSubClientTest, TestMediaBlockError) { + std::string result_iq = + ""; + + client->BlockMedia("joe"); + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ(1, listener->remote_mute_error_count); + EXPECT_EQ("joe", listener->media_blockee_nick); + EXPECT_EQ("0", listener->error_task_id); +} diff --git a/talk/xmpp/iqtask.cc b/talk/xmpp/iqtask.cc new file mode 100644 index 000000000..f54f63041 --- /dev/null +++ b/talk/xmpp/iqtask.cc @@ -0,0 +1,86 @@ +/* + * libjingle + * Copyright 2011, 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/xmpp/iqtask.h" + +#include "talk/xmpp/xmppclient.h" +#include "talk/xmpp/constants.h" + +namespace buzz { + +static const int kDefaultIqTimeoutSecs = 15; + +IqTask::IqTask(XmppTaskParentInterface* parent, + const std::string& verb, + const buzz::Jid& to, + buzz::XmlElement* el) + : buzz::XmppTask(parent, buzz::XmppEngine::HL_SINGLE), + to_(to), + stanza_(MakeIq(verb, to_, task_id())) { + stanza_->AddElement(el); + set_timeout_seconds(kDefaultIqTimeoutSecs); +} + +int IqTask::ProcessStart() { + buzz::XmppReturnStatus ret = SendStanza(stanza_.get()); + // TODO: HandleError(NULL) if SendStanza fails? + return (ret == buzz::XMPP_RETURN_OK) ? STATE_RESPONSE : STATE_ERROR; +} + +bool IqTask::HandleStanza(const buzz::XmlElement* stanza) { + if (!MatchResponseIq(stanza, to_, task_id())) + return false; + + if (stanza->Attr(buzz::QN_TYPE) != buzz::STR_RESULT && + stanza->Attr(buzz::QN_TYPE) != buzz::STR_ERROR) { + return false; + } + + QueueStanza(stanza); + return true; +} + +int IqTask::ProcessResponse() { + const buzz::XmlElement* stanza = NextStanza(); + if (stanza == NULL) + return STATE_BLOCKED; + + bool success = (stanza->Attr(buzz::QN_TYPE) == buzz::STR_RESULT); + if (success) { + HandleResult(stanza); + } else { + SignalError(this, stanza->FirstNamed(QN_ERROR)); + } + return STATE_DONE; +} + +int IqTask::OnTimeout() { + SignalError(this, NULL); + return XmppTask::OnTimeout(); +} + +} // namespace buzz diff --git a/talk/xmpp/iqtask.h b/talk/xmpp/iqtask.h new file mode 100644 index 000000000..2228e6f2c --- /dev/null +++ b/talk/xmpp/iqtask.h @@ -0,0 +1,65 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_XMPP_IQTASK_H_ +#define TALK_XMPP_IQTASK_H_ + +#include + +#include "talk/xmpp/xmpptask.h" +#include "talk/xmpp/xmppengine.h" + +namespace buzz { + +class IqTask : public XmppTask { + public: + IqTask(XmppTaskParentInterface* parent, + const std::string& verb, const Jid& to, + XmlElement* el); + virtual ~IqTask() {} + + const XmlElement* stanza() const { return stanza_.get(); } + + sigslot::signal2 SignalError; + + protected: + virtual void HandleResult(const XmlElement* element) = 0; + + private: + virtual int ProcessStart(); + virtual bool HandleStanza(const XmlElement* stanza); + virtual int ProcessResponse(); + virtual int OnTimeout(); + + Jid to_; + talk_base::scoped_ptr stanza_; +}; + +} // namespace buzz + +#endif // TALK_XMPP_IQTASK_H_ diff --git a/talk/xmpp/jid.cc b/talk/xmpp/jid.cc new file mode 100644 index 000000000..66d5cded5 --- /dev/null +++ b/talk/xmpp/jid.cc @@ -0,0 +1,396 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmpp/jid.h" + +#include + +#include +#include + +#include "talk/base/common.h" +#include "talk/base/logging.h" +#include "talk/xmpp/constants.h" + +namespace buzz { + +Jid::Jid() { +} + +Jid::Jid(const std::string& jid_string) { + if (jid_string.empty()) + return; + + // First find the slash and slice off that part + size_t slash = jid_string.find('/'); + resource_name_ = (slash == std::string::npos ? STR_EMPTY : + jid_string.substr(slash + 1)); + + // Now look for the node + size_t at = jid_string.find('@'); + size_t domain_begin; + if (at < slash && at != std::string::npos) { + node_name_ = jid_string.substr(0, at); + domain_begin = at + 1; + } else { + domain_begin = 0; + } + + // Now take what is left as the domain + size_t domain_length = (slash == std::string::npos) ? + (jid_string.length() - domain_begin) : (slash - domain_begin); + domain_name_ = jid_string.substr(domain_begin, domain_length); + + ValidateOrReset(); +} + +Jid::Jid(const std::string& node_name, + const std::string& domain_name, + const std::string& resource_name) + : node_name_(node_name), + domain_name_(domain_name), + resource_name_(resource_name) { + ValidateOrReset(); +} + +void Jid::ValidateOrReset() { + bool valid_node; + bool valid_domain; + bool valid_resource; + + node_name_ = PrepNode(node_name_, &valid_node); + domain_name_ = PrepDomain(domain_name_, &valid_domain); + resource_name_ = PrepResource(resource_name_, &valid_resource); + + if (!valid_node || !valid_domain || !valid_resource) { + node_name_.clear(); + domain_name_.clear(); + resource_name_.clear(); + } +} + +std::string Jid::Str() const { + if (!IsValid()) + return STR_EMPTY; + + std::string ret; + + if (!node_name_.empty()) + ret = node_name_ + "@"; + + ASSERT(domain_name_ != STR_EMPTY); + ret += domain_name_; + + if (!resource_name_.empty()) + ret += "/" + resource_name_; + + return ret; +} + +Jid::~Jid() { +} + +bool Jid::IsEmpty() const { + return (node_name_.empty() && domain_name_.empty() && + resource_name_.empty()); +} + +bool Jid::IsValid() const { + return !domain_name_.empty(); +} + +bool Jid::IsBare() const { + if (IsEmpty()) { + LOG(LS_VERBOSE) << "Warning: Calling IsBare() on the empty jid."; + return true; + } + return IsValid() && resource_name_.empty(); +} + +bool Jid::IsFull() const { + return IsValid() && !resource_name_.empty(); +} + +Jid Jid::BareJid() const { + if (!IsValid()) + return Jid(); + if (!IsFull()) + return *this; + return Jid(node_name_, domain_name_, STR_EMPTY); +} + +bool Jid::BareEquals(const Jid& other) const { + return other.node_name_ == node_name_ && + other.domain_name_ == domain_name_; +} + +void Jid::CopyFrom(const Jid& jid) { + this->node_name_ = jid.node_name_; + this->domain_name_ = jid.domain_name_; + this->resource_name_ = jid.resource_name_; +} + +bool Jid::operator==(const Jid& other) const { + return other.node_name_ == node_name_ && + other.domain_name_ == domain_name_ && + other.resource_name_ == resource_name_; +} + +int Jid::Compare(const Jid& other) const { + int compare_result; + compare_result = node_name_.compare(other.node_name_); + if (0 != compare_result) + return compare_result; + compare_result = domain_name_.compare(other.domain_name_); + if (0 != compare_result) + return compare_result; + compare_result = resource_name_.compare(other.resource_name_); + return compare_result; +} + +// --- JID parsing code: --- + +// Checks and normalizes the node part of a JID. +std::string Jid::PrepNode(const std::string& node, bool* valid) { + *valid = false; + std::string result; + + for (std::string::const_iterator i = node.begin(); i < node.end(); ++i) { + bool char_valid = true; + unsigned char ch = *i; + if (ch <= 0x7F) { + result += PrepNodeAscii(ch, &char_valid); + } + else { + // TODO: implement the correct stringprep protocol for these + result += tolower(ch); + } + if (!char_valid) { + return STR_EMPTY; + } + } + + if (result.length() > 1023) { + return STR_EMPTY; + } + *valid = true; + return result; +} + + +// Returns the appropriate mapping for an ASCII character in a node. +char Jid::PrepNodeAscii(char ch, bool* valid) { + *valid = true; + switch (ch) { + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': + case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N': + case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': + case 'V': case 'W': case 'X': case 'Y': case 'Z': + return (char)(ch + ('a' - 'A')); + + case 0x00: case 0x01: case 0x02: case 0x03: case 0x04: case 0x05: + case 0x06: case 0x07: case 0x08: case 0x09: case 0x0A: case 0x0B: + case 0x0C: case 0x0D: case 0x0E: case 0x0F: case 0x10: case 0x11: + case 0x12: case 0x13: case 0x14: case 0x15: case 0x16: case 0x17: + case ' ': case '&': case '/': case ':': case '<': case '>': case '@': + case '\"': case '\'': + case 0x7F: + *valid = false; + return 0; + + default: + return ch; + } +} + + +// Checks and normalizes the resource part of a JID. +std::string Jid::PrepResource(const std::string& resource, bool* valid) { + *valid = false; + std::string result; + + for (std::string::const_iterator i = resource.begin(); + i < resource.end(); ++i) { + bool char_valid = true; + unsigned char ch = *i; + if (ch <= 0x7F) { + result += PrepResourceAscii(ch, &char_valid); + } + else { + // TODO: implement the correct stringprep protocol for these + result += ch; + } + } + + if (result.length() > 1023) { + return STR_EMPTY; + } + *valid = true; + return result; +} + +// Returns the appropriate mapping for an ASCII character in a resource. +char Jid::PrepResourceAscii(char ch, bool* valid) { + *valid = true; + switch (ch) { + case 0x00: case 0x01: case 0x02: case 0x03: case 0x04: case 0x05: + case 0x06: case 0x07: case 0x08: case 0x09: case 0x0A: case 0x0B: + case 0x0C: case 0x0D: case 0x0E: case 0x0F: case 0x10: case 0x11: + case 0x12: case 0x13: case 0x14: case 0x15: case 0x16: case 0x17: + case 0x7F: + *valid = false; + return 0; + + default: + return ch; + } +} + +// Checks and normalizes the domain part of a JID. +std::string Jid::PrepDomain(const std::string& domain, bool* valid) { + *valid = false; + std::string result; + + // TODO: if the domain contains a ':', then we should parse it + // as an IPv6 address rather than giving an error about illegal domain. + PrepDomain(domain, &result, valid); + if (!*valid) { + return STR_EMPTY; + } + + if (result.length() > 1023) { + return STR_EMPTY; + } + *valid = true; + return result; +} + + +// Checks and normalizes an IDNA domain. +void Jid::PrepDomain(const std::string& domain, std::string* buf, bool* valid) { + *valid = false; + std::string::const_iterator last = domain.begin(); + for (std::string::const_iterator i = domain.begin(); i < domain.end(); ++i) { + bool label_valid = true; + char ch = *i; + switch (ch) { + case 0x002E: +#if 0 // FIX: This isn't UTF-8-aware. + case 0x3002: + case 0xFF0E: + case 0xFF61: +#endif + PrepDomainLabel(last, i, buf, &label_valid); + *buf += '.'; + last = i + 1; + break; + } + if (!label_valid) { + return; + } + } + PrepDomainLabel(last, domain.end(), buf, valid); +} + +// Checks and normalizes a domain label. +void Jid::PrepDomainLabel( + std::string::const_iterator start, std::string::const_iterator end, + std::string* buf, bool* valid) { + *valid = false; + + int start_len = buf->length(); + for (std::string::const_iterator i = start; i < end; ++i) { + bool char_valid = true; + unsigned char ch = *i; + if (ch <= 0x7F) { + *buf += PrepDomainLabelAscii(ch, &char_valid); + } + else { + // TODO: implement ToASCII for these + *buf += ch; + } + if (!char_valid) { + return; + } + } + + int count = buf->length() - start_len; + if (count == 0) { + return; + } + else if (count > 63) { + return; + } + + // Is this check needed? See comment in PrepDomainLabelAscii. + if ((*buf)[start_len] == '-') { + return; + } + if ((*buf)[buf->length() - 1] == '-') { + return; + } + *valid = true; +} + + +// Returns the appropriate mapping for an ASCII character in a domain label. +char Jid::PrepDomainLabelAscii(char ch, bool* valid) { + *valid = true; + // TODO: A literal reading of the spec seems to say that we do + // not need to check for these illegal characters (an "internationalized + // domain label" runs ToASCII with UseSTD3... set to false). But that + // can't be right. We should at least be checking that there are no '/' + // or '@' characters in the domain. Perhaps we should see what others + // do in this case. + + switch (ch) { + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': + case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N': + case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': + case 'V': case 'W': case 'X': case 'Y': case 'Z': + return (char)(ch + ('a' - 'A')); + + case 0x00: case 0x01: case 0x02: case 0x03: case 0x04: case 0x05: + case 0x06: case 0x07: case 0x08: case 0x09: case 0x0A: case 0x0B: + case 0x0C: case 0x0D: case 0x0E: case 0x0F: case 0x10: case 0x11: + case 0x12: case 0x13: case 0x14: case 0x15: case 0x16: case 0x17: + case 0x18: case 0x19: case 0x1A: case 0x1B: case 0x1C: case 0x1D: + case 0x1E: case 0x1F: case 0x20: case 0x21: case 0x22: case 0x23: + case 0x24: case 0x25: case 0x26: case 0x27: case 0x28: case 0x29: + case 0x2A: case 0x2B: case 0x2C: case 0x2E: case 0x2F: case 0x3A: + case 0x3B: case 0x3C: case 0x3D: case 0x3E: case 0x3F: case 0x40: + case 0x5B: case 0x5C: case 0x5D: case 0x5E: case 0x5F: case 0x60: + case 0x7B: case 0x7C: case 0x7D: case 0x7E: case 0x7F: + *valid = false; + return 0; + + default: + return ch; + } +} + +} // namespace buzz diff --git a/talk/xmpp/jid.h b/talk/xmpp/jid.h new file mode 100644 index 000000000..dcfc123f9 --- /dev/null +++ b/talk/xmpp/jid.h @@ -0,0 +1,98 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_JID_H_ +#define TALK_XMPP_JID_H_ + +#include +#include "talk/base/basictypes.h" +#include "talk/xmllite/xmlconstants.h" + +namespace buzz { + +// The Jid class encapsulates and provides parsing help for Jids. A Jid +// consists of three parts: the node, the domain and the resource, e.g.: +// +// node@domain/resource +// +// The node and resource are both optional. A valid jid is defined to have +// a domain. A bare jid is defined to not have a resource and a full jid +// *does* have a resource. +class Jid { +public: + explicit Jid(); + explicit Jid(const std::string& jid_string); + explicit Jid(const std::string& node_name, + const std::string& domain_name, + const std::string& resource_name); + ~Jid(); + + const std::string & node() const { return node_name_; } + const std::string & domain() const { return domain_name_; } + const std::string & resource() const { return resource_name_; } + + std::string Str() const; + Jid BareJid() const; + + bool IsEmpty() const; + bool IsValid() const; + bool IsBare() const; + bool IsFull() const; + + bool BareEquals(const Jid& other) const; + void CopyFrom(const Jid& jid); + bool operator==(const Jid& other) const; + bool operator!=(const Jid& other) const { return !operator==(other); } + + bool operator<(const Jid& other) const { return Compare(other) < 0; }; + bool operator>(const Jid& other) const { return Compare(other) > 0; }; + + int Compare(const Jid & other) const; + +private: + void ValidateOrReset(); + + static std::string PrepNode(const std::string& node, bool* valid); + static char PrepNodeAscii(char ch, bool* valid); + static std::string PrepResource(const std::string& start, bool* valid); + static char PrepResourceAscii(char ch, bool* valid); + static std::string PrepDomain(const std::string& domain, bool* valid); + static void PrepDomain(const std::string& domain, + std::string* buf, bool* valid); + static void PrepDomainLabel( + std::string::const_iterator start, std::string::const_iterator end, + std::string* buf, bool* valid); + static char PrepDomainLabelAscii(char ch, bool *valid); + + std::string node_name_; + std::string domain_name_; + std::string resource_name_; +}; + +} + +#endif // TALK_XMPP_JID_H_ diff --git a/talk/xmpp/jid_unittest.cc b/talk/xmpp/jid_unittest.cc new file mode 100644 index 000000000..b9597da72 --- /dev/null +++ b/talk/xmpp/jid_unittest.cc @@ -0,0 +1,115 @@ +// Copyright 2004 Google Inc. All Rights Reserved + + +#include "talk/base/gunit.h" +#include "talk/xmpp/jid.h" + +using buzz::Jid; + +TEST(JidTest, TestDomain) { + Jid jid("dude"); + EXPECT_EQ("", jid.node()); + EXPECT_EQ("dude", jid.domain()); + EXPECT_EQ("", jid.resource()); + EXPECT_EQ("dude", jid.Str()); + EXPECT_EQ("dude", jid.BareJid().Str()); + EXPECT_TRUE(jid.IsValid()); + EXPECT_TRUE(jid.IsBare()); + EXPECT_FALSE(jid.IsFull()); +} + +TEST(JidTest, TestNodeDomain) { + Jid jid("walter@dude"); + EXPECT_EQ("walter", jid.node()); + EXPECT_EQ("dude", jid.domain()); + EXPECT_EQ("", jid.resource()); + EXPECT_EQ("walter@dude", jid.Str()); + EXPECT_EQ("walter@dude", jid.BareJid().Str()); + EXPECT_TRUE(jid.IsValid()); + EXPECT_TRUE(jid.IsBare()); + EXPECT_FALSE(jid.IsFull()); +} + +TEST(JidTest, TestDomainResource) { + Jid jid("dude/bowlingalley"); + EXPECT_EQ("", jid.node()); + EXPECT_EQ("dude", jid.domain()); + EXPECT_EQ("bowlingalley", jid.resource()); + EXPECT_EQ("dude/bowlingalley", jid.Str()); + EXPECT_EQ("dude", jid.BareJid().Str()); + EXPECT_TRUE(jid.IsValid()); + EXPECT_FALSE(jid.IsBare()); + EXPECT_TRUE(jid.IsFull()); +} + +TEST(JidTest, TestNodeDomainResource) { + Jid jid("walter@dude/bowlingalley"); + EXPECT_EQ("walter", jid.node()); + EXPECT_EQ("dude", jid.domain()); + EXPECT_EQ("bowlingalley", jid.resource()); + EXPECT_EQ("walter@dude/bowlingalley", jid.Str()); + EXPECT_EQ("walter@dude", jid.BareJid().Str()); + EXPECT_TRUE(jid.IsValid()); + EXPECT_FALSE(jid.IsBare()); + EXPECT_TRUE(jid.IsFull()); +} + +TEST(JidTest, TestNode) { + Jid jid("walter@"); + EXPECT_EQ("", jid.node()); + EXPECT_EQ("", jid.domain()); + EXPECT_EQ("", jid.resource()); + EXPECT_EQ("", jid.Str()); + EXPECT_EQ("", jid.BareJid().Str()); + EXPECT_FALSE(jid.IsValid()); + EXPECT_TRUE(jid.IsBare()); + EXPECT_FALSE(jid.IsFull()); +} + +TEST(JidTest, TestResource) { + Jid jid("/bowlingalley"); + EXPECT_EQ("", jid.node()); + EXPECT_EQ("", jid.domain()); + EXPECT_EQ("", jid.resource()); + EXPECT_EQ("", jid.Str()); + EXPECT_EQ("", jid.BareJid().Str()); + EXPECT_FALSE(jid.IsValid()); + EXPECT_TRUE(jid.IsBare()); + EXPECT_FALSE(jid.IsFull()); +} + +TEST(JidTest, TestNodeResource) { + Jid jid("walter@/bowlingalley"); + EXPECT_EQ("", jid.node()); + EXPECT_EQ("", jid.domain()); + EXPECT_EQ("", jid.resource()); + EXPECT_EQ("", jid.Str()); + EXPECT_EQ("", jid.BareJid().Str()); + EXPECT_FALSE(jid.IsValid()); + EXPECT_TRUE(jid.IsBare()); + EXPECT_FALSE(jid.IsFull()); +} + +TEST(JidTest, TestFunky) { + Jid jid("bowling@muchat/walter@dude"); + EXPECT_EQ("bowling", jid.node()); + EXPECT_EQ("muchat", jid.domain()); + EXPECT_EQ("walter@dude", jid.resource()); + EXPECT_EQ("bowling@muchat/walter@dude", jid.Str()); + EXPECT_EQ("bowling@muchat", jid.BareJid().Str()); + EXPECT_TRUE(jid.IsValid()); + EXPECT_FALSE(jid.IsBare()); + EXPECT_TRUE(jid.IsFull()); +} + +TEST(JidTest, TestFunky2) { + Jid jid("muchat/walter@dude"); + EXPECT_EQ("", jid.node()); + EXPECT_EQ("muchat", jid.domain()); + EXPECT_EQ("walter@dude", jid.resource()); + EXPECT_EQ("muchat/walter@dude", jid.Str()); + EXPECT_EQ("muchat", jid.BareJid().Str()); + EXPECT_TRUE(jid.IsValid()); + EXPECT_FALSE(jid.IsBare()); + EXPECT_TRUE(jid.IsFull()); +} diff --git a/talk/xmpp/jingleinfotask.cc b/talk/xmpp/jingleinfotask.cc new file mode 100644 index 000000000..cf3eac289 --- /dev/null +++ b/talk/xmpp/jingleinfotask.cc @@ -0,0 +1,138 @@ +/* + * libjingle + * Copyright 2010, 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/xmpp/jingleinfotask.h" + +#include "talk/base/socketaddress.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/xmppclient.h" +#include "talk/xmpp/xmpptask.h" + +namespace buzz { + +class JingleInfoTask::JingleInfoGetTask : public XmppTask { + public: + explicit JingleInfoGetTask(XmppTaskParentInterface* parent) + : XmppTask(parent, XmppEngine::HL_SINGLE), + done_(false) {} + + virtual int ProcessStart() { + talk_base::scoped_ptr get( + MakeIq(STR_GET, Jid(), task_id())); + get->AddElement(new XmlElement(QN_JINGLE_INFO_QUERY, true)); + if (SendStanza(get.get()) != XMPP_RETURN_OK) { + return STATE_ERROR; + } + return STATE_RESPONSE; + } + virtual int ProcessResponse() { + if (done_) + return STATE_DONE; + return STATE_BLOCKED; + } + + protected: + virtual bool HandleStanza(const XmlElement * stanza) { + if (!MatchResponseIq(stanza, Jid(), task_id())) + return false; + + if (stanza->Attr(QN_TYPE) != STR_RESULT) + return false; + + // Queue the stanza with the parent so these don't get handled out of order + JingleInfoTask* parent = static_cast(GetParent()); + parent->QueueStanza(stanza); + + // Wake ourselves so we can go into the done state + done_ = true; + Wake(); + return true; + } + + bool done_; +}; + + +void JingleInfoTask::RefreshJingleInfoNow() { + JingleInfoGetTask* get_task = new JingleInfoGetTask(this); + get_task->Start(); +} + +bool +JingleInfoTask::HandleStanza(const XmlElement * stanza) { + if (!MatchRequestIq(stanza, "set", QN_JINGLE_INFO_QUERY)) + return false; + + // only respect relay push from the server + Jid from(stanza->Attr(QN_FROM)); + if (!from.IsEmpty() && + !from.BareEquals(GetClient()->jid()) && + from != Jid(GetClient()->jid().domain())) + return false; + + QueueStanza(stanza); + return true; +} + +int +JingleInfoTask::ProcessStart() { + std::vector relay_hosts; + std::vector stun_hosts; + std::string relay_token; + const XmlElement * stanza = NextStanza(); + if (stanza == NULL) + return STATE_BLOCKED; + const XmlElement * query = stanza->FirstNamed(QN_JINGLE_INFO_QUERY); + if (query == NULL) + return STATE_START; + const XmlElement *stun = query->FirstNamed(QN_JINGLE_INFO_STUN); + if (stun) { + for (const XmlElement *server = stun->FirstNamed(QN_JINGLE_INFO_SERVER); + server != NULL; server = server->NextNamed(QN_JINGLE_INFO_SERVER)) { + std::string host = server->Attr(QN_JINGLE_INFO_HOST); + std::string port = server->Attr(QN_JINGLE_INFO_UDP); + if (host != STR_EMPTY && host != STR_EMPTY) { + stun_hosts.push_back(talk_base::SocketAddress(host, atoi(port.c_str()))); + } + } + } + + const XmlElement *relay = query->FirstNamed(QN_JINGLE_INFO_RELAY); + if (relay) { + relay_token = relay->TextNamed(QN_JINGLE_INFO_TOKEN); + for (const XmlElement *server = relay->FirstNamed(QN_JINGLE_INFO_SERVER); + server != NULL; server = server->NextNamed(QN_JINGLE_INFO_SERVER)) { + std::string host = server->Attr(QN_JINGLE_INFO_HOST); + if (host != STR_EMPTY) { + relay_hosts.push_back(host); + } + } + } + SignalJingleInfo(relay_token, relay_hosts, stun_hosts); + return STATE_START; +} +} diff --git a/talk/xmpp/jingleinfotask.h b/talk/xmpp/jingleinfotask.h new file mode 100644 index 000000000..dbc3fb004 --- /dev/null +++ b/talk/xmpp/jingleinfotask.h @@ -0,0 +1,61 @@ +/* + * libjingle + * Copyright 2010, 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. + */ + +#ifndef TALK_EXAMPLES_LOGIN_JINGLEINFOTASK_H_ +#define TALK_EXAMPLES_LOGIN_JINGLEINFOTASK_H_ + +#include + +#include "talk/p2p/client/httpportallocator.h" +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmpptask.h" +#include "talk/base/sigslot.h" + +namespace buzz { + +class JingleInfoTask : public XmppTask { + public: + explicit JingleInfoTask(XmppTaskParentInterface* parent) : + XmppTask(parent, XmppEngine::HL_TYPE) {} + + virtual int ProcessStart(); + void RefreshJingleInfoNow(); + + sigslot::signal3 &, + const std::vector &> + SignalJingleInfo; + + protected: + class JingleInfoGetTask; + friend class JingleInfoGetTask; + + virtual bool HandleStanza(const XmlElement * stanza); +}; +} + +#endif // TALK_EXAMPLES_LOGIN_JINGLEINFOTASK_H_ diff --git a/talk/xmpp/module.h b/talk/xmpp/module.h new file mode 100644 index 000000000..75a190d89 --- /dev/null +++ b/talk/xmpp/module.h @@ -0,0 +1,51 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _module_h_ +#define _module_h_ + +namespace buzz { + +class XmppEngine; +enum XmppReturnStatus; + +//! This is the base class for extension modules. +//! An engine is registered with the module and the module then hooks the +//! appropriate parts of the engine to implement that set of features. It is +//! important to unregister modules before destructing the engine. +class XmppModule { +public: + virtual ~XmppModule() {} + + //! Register the engine with the module. Only one engine can be associated + //! with a module at a time. This method will return an error if there is + //! already an engine registered. + virtual XmppReturnStatus RegisterEngine(XmppEngine* engine) = 0; +}; + +} +#endif diff --git a/talk/xmpp/moduleimpl.cc b/talk/xmpp/moduleimpl.cc new file mode 100644 index 000000000..b23ca2982 --- /dev/null +++ b/talk/xmpp/moduleimpl.cc @@ -0,0 +1,65 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/common.h" +#include "talk/xmpp/moduleimpl.h" + +namespace buzz { + +XmppModuleImpl::XmppModuleImpl() : + engine_(NULL), + stanza_handler_(this) { +} + +XmppModuleImpl::~XmppModuleImpl() +{ + if (engine_ != NULL) { + engine_->RemoveStanzaHandler(&stanza_handler_); + engine_ = NULL; + } +} + +XmppReturnStatus +XmppModuleImpl::RegisterEngine(XmppEngine* engine) +{ + if (NULL == engine || NULL != engine_) + return XMPP_RETURN_BADARGUMENT; + + engine->AddStanzaHandler(&stanza_handler_); + engine_ = engine; + + return XMPP_RETURN_OK; +} + +XmppEngine* +XmppModuleImpl::engine() { + ASSERT(NULL != engine_); + return engine_; +} + +} + diff --git a/talk/xmpp/moduleimpl.h b/talk/xmpp/moduleimpl.h new file mode 100644 index 000000000..085c83a0d --- /dev/null +++ b/talk/xmpp/moduleimpl.h @@ -0,0 +1,93 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _moduleimpl_h_ +#define _moduleimpl_h_ + +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/module.h" + +namespace buzz { + +//! This is the base implementation class for extension modules. +//! An engine is registered with the module and the module then hooks the +//! appropriate parts of the engine to implement that set of features. It is +//! important to unregister modules before destructing the engine. +class XmppModuleImpl { +protected: + XmppModuleImpl(); + virtual ~XmppModuleImpl(); + + //! Register the engine with the module. Only one engine can be associated + //! with a module at a time. This method will return an error if there is + //! already an engine registered. + XmppReturnStatus RegisterEngine(XmppEngine* engine); + + //! Gets the engine that this module is attached to. + XmppEngine* engine(); + + //! Process the given stanza. + //! The module must return true if it has handled the stanza. + //! A false return value causes the stanza to be passed on to + //! the next registered handler. + virtual bool HandleStanza(const XmlElement *) { return false; }; + +private: + + //! The ModuleSessionHelper nested class allows the Module + //! to hook into and get stanzas and events from the engine. + class ModuleStanzaHandler : public XmppStanzaHandler { + friend class XmppModuleImpl; + + ModuleStanzaHandler(XmppModuleImpl* module) : + module_(module) { + } + + bool HandleStanza(const XmlElement* stanza) { + return module_->HandleStanza(stanza); + } + + XmppModuleImpl* module_; + }; + + friend class ModuleStanzaHandler; + + XmppEngine* engine_; + ModuleStanzaHandler stanza_handler_; +}; + + +// This macro will implement the XmppModule interface for a class +// that derives from both XmppModuleImpl and XmppModule +#define IMPLEMENT_XMPPMODULE \ + XmppReturnStatus RegisterEngine(XmppEngine* engine) { \ + return XmppModuleImpl::RegisterEngine(engine); \ + } + +} + +#endif diff --git a/talk/xmpp/mucroomconfigtask.cc b/talk/xmpp/mucroomconfigtask.cc new file mode 100644 index 000000000..272bd44f7 --- /dev/null +++ b/talk/xmpp/mucroomconfigtask.cc @@ -0,0 +1,91 @@ +/* + * libjingle + * Copyright 2011, 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 +#include + +#include "talk/xmpp/mucroomconfigtask.h" + +#include "talk/base/scoped_ptr.h" +#include "talk/xmpp/constants.h" + +namespace buzz { + +MucRoomConfigTask::MucRoomConfigTask( + XmppTaskParentInterface* parent, + const Jid& room_jid, + const std::string& room_name, + const std::vector& room_features) + : IqTask(parent, STR_SET, room_jid, + MakeRequest(room_name, room_features)), + room_jid_(room_jid) { +} + +XmlElement* MucRoomConfigTask::MakeRequest( + const std::string& room_name, + const std::vector& room_features) { + buzz::XmlElement* owner_query = new + buzz::XmlElement(buzz::QN_MUC_OWNER_QUERY, true); + + buzz::XmlElement* x_form = new buzz::XmlElement(buzz::QN_XDATA_X, true); + x_form->SetAttr(buzz::QN_TYPE, buzz::STR_FORM); + + buzz::XmlElement* roomname_field = + new buzz::XmlElement(buzz::QN_XDATA_FIELD, false); + roomname_field->SetAttr(buzz::QN_VAR, buzz::STR_MUC_ROOMCONFIG_ROOMNAME); + roomname_field->SetAttr(buzz::QN_TYPE, buzz::STR_TEXT_SINGLE); + + buzz::XmlElement* roomname_value = + new buzz::XmlElement(buzz::QN_XDATA_VALUE, false); + roomname_value->SetBodyText(room_name); + + roomname_field->AddElement(roomname_value); + x_form->AddElement(roomname_field); + + buzz::XmlElement* features_field = + new buzz::XmlElement(buzz::QN_XDATA_FIELD, false); + features_field->SetAttr(buzz::QN_VAR, buzz::STR_MUC_ROOMCONFIG_FEATURES); + features_field->SetAttr(buzz::QN_TYPE, buzz::STR_LIST_MULTI); + + for (std::vector::const_iterator feature = room_features.begin(); + feature != room_features.end(); ++feature) { + buzz::XmlElement* features_value = + new buzz::XmlElement(buzz::QN_XDATA_VALUE, false); + features_value->SetBodyText(*feature); + features_field->AddElement(features_value); + } + + x_form->AddElement(features_field); + owner_query->AddElement(x_form); + return owner_query; +} + +void MucRoomConfigTask::HandleResult(const XmlElement* element) { + SignalResult(this); +} + +} // namespace buzz diff --git a/talk/xmpp/mucroomconfigtask.h b/talk/xmpp/mucroomconfigtask.h new file mode 100644 index 000000000..ba0dbaa27 --- /dev/null +++ b/talk/xmpp/mucroomconfigtask.h @@ -0,0 +1,64 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_XMPP_MUCROOMCONFIGTASK_H_ +#define TALK_XMPP_MUCROOMCONFIGTASK_H_ + +#include +#include "talk/xmpp/iqtask.h" + +namespace buzz { + +// This task configures the muc room for document sharing and other enterprise +// specific goodies. +class MucRoomConfigTask : public IqTask { + public: + MucRoomConfigTask(XmppTaskParentInterface* parent, + const Jid& room_jid, + const std::string& room_name, + const std::vector& room_features); + + // Room configuration does not return any reasonable error + // values. The First config request configures the room, subseqent + // ones are just ignored by server and server returns empty + // response. + sigslot::signal1 SignalResult; + + const Jid& room_jid() const { return room_jid_; } + + protected: + virtual void HandleResult(const XmlElement* stanza); + + private: + static XmlElement* MakeRequest(const std::string& room_name, + const std::vector& room_features); + Jid room_jid_; +}; + +} // namespace buzz + +#endif // TALK_XMPP_MUCROOMCONFIGTASK_H_ diff --git a/talk/xmpp/mucroomconfigtask_unittest.cc b/talk/xmpp/mucroomconfigtask_unittest.cc new file mode 100644 index 000000000..e0a8acaeb --- /dev/null +++ b/talk/xmpp/mucroomconfigtask_unittest.cc @@ -0,0 +1,144 @@ +/* + * libjingle + * Copyright 2011, 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 +#include + +#include "talk/base/faketaskrunner.h" +#include "talk/base/gunit.h" +#include "talk/base/sigslot.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/fakexmppclient.h" +#include "talk/xmpp/mucroomconfigtask.h" + +class MucRoomConfigListener : public sigslot::has_slots<> { + public: + MucRoomConfigListener() : result_count(0), error_count(0) {} + + void OnResult(buzz::MucRoomConfigTask*) { + ++result_count; + } + + void OnError(buzz::IqTask* task, + const buzz::XmlElement* error) { + ++error_count; + } + + int result_count; + int error_count; +}; + +class MucRoomConfigTaskTest : public testing::Test { + public: + MucRoomConfigTaskTest() : + room_jid("muc-jid-ponies@domain.com"), + room_name("ponies") { + } + + virtual void SetUp() { + runner = new talk_base::FakeTaskRunner(); + xmpp_client = new buzz::FakeXmppClient(runner); + listener = new MucRoomConfigListener(); + } + + virtual void TearDown() { + delete listener; + // delete xmpp_client; Deleted by deleting runner. + delete runner; + } + + talk_base::FakeTaskRunner* runner; + buzz::FakeXmppClient* xmpp_client; + MucRoomConfigListener* listener; + buzz::Jid room_jid; + std::string room_name; +}; + +TEST_F(MucRoomConfigTaskTest, TestConfigEnterprise) { + ASSERT_EQ(0U, xmpp_client->sent_stanzas().size()); + + std::vector room_features; + room_features.push_back("feature1"); + room_features.push_back("feature2"); + buzz::MucRoomConfigTask* task = new buzz::MucRoomConfigTask( + xmpp_client, room_jid, "ponies", room_features); + EXPECT_EQ(room_jid, task->room_jid()); + + task->SignalResult.connect(listener, &MucRoomConfigListener::OnResult); + task->Start(); + + std::string expected_iq = + "" + "" + "" + "" + "ponies" + "" + "" + "feature1" + "feature2" + "" + "" + "" + ""; + + ASSERT_EQ(1U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_iq, xmpp_client->sent_stanzas()[0]->Str()); + + EXPECT_EQ(0, listener->result_count); + EXPECT_EQ(0, listener->error_count); + + std::string response_iq = + "" + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(response_iq)); + + EXPECT_EQ(1, listener->result_count); + EXPECT_EQ(0, listener->error_count); +} + +TEST_F(MucRoomConfigTaskTest, TestError) { + std::vector room_features; + buzz::MucRoomConfigTask* task = new buzz::MucRoomConfigTask( + xmpp_client, room_jid, "ponies", room_features); + task->SignalError.connect(listener, &MucRoomConfigListener::OnError); + task->Start(); + + std::string error_iq = + "" + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(error_iq)); + + EXPECT_EQ(0, listener->result_count); + EXPECT_EQ(1, listener->error_count); +} diff --git a/talk/xmpp/mucroomdiscoverytask.cc b/talk/xmpp/mucroomdiscoverytask.cc new file mode 100644 index 000000000..e0770fdd7 --- /dev/null +++ b/talk/xmpp/mucroomdiscoverytask.cc @@ -0,0 +1,75 @@ +/* + * libjingle + * Copyright 2012, 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/xmpp/mucroomdiscoverytask.h" + +#include "talk/xmpp/constants.h" + +namespace buzz { + +MucRoomDiscoveryTask::MucRoomDiscoveryTask( + XmppTaskParentInterface* parent, + const Jid& room_jid) + : IqTask(parent, STR_GET, room_jid, + new buzz::XmlElement(buzz::QN_DISCO_INFO_QUERY)) { +} + +void MucRoomDiscoveryTask::HandleResult(const XmlElement* stanza) { + const XmlElement* query = stanza->FirstNamed(QN_DISCO_INFO_QUERY); + if (query == NULL) { + SignalError(this, NULL); + return; + } + + std::set features; + std::map extended_info; + const XmlElement* identity = query->FirstNamed(QN_DISCO_IDENTITY); + if (identity == NULL || !identity->HasAttr(QN_NAME)) { + SignalResult(this, false, "", features, extended_info); + return; + } + + const std::string name(identity->Attr(QN_NAME)); + + for (const XmlElement* feature = query->FirstNamed(QN_DISCO_FEATURE); + feature != NULL; feature = feature->NextNamed(QN_DISCO_FEATURE)) { + features.insert(feature->Attr(QN_VAR)); + } + + const XmlElement* data_x = query->FirstNamed(QN_XDATA_X); + if (data_x != NULL) { + for (const XmlElement* field = data_x->FirstNamed(QN_XDATA_FIELD); + field != NULL; field = field->NextNamed(QN_XDATA_FIELD)) { + const std::string key(field->Attr(QN_VAR)); + extended_info[key] = field->Attr(QN_XDATA_VALUE); + } + } + + SignalResult(this, true, name, features, extended_info); +} + +} // namespace buzz diff --git a/talk/xmpp/mucroomdiscoverytask.h b/talk/xmpp/mucroomdiscoverytask.h new file mode 100644 index 000000000..6e3a21adf --- /dev/null +++ b/talk/xmpp/mucroomdiscoverytask.h @@ -0,0 +1,57 @@ +/* + * libjingle + * Copyright 2012, 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. + */ + +#ifndef TALK_XMPP_MUCROOMDISCOVERYTASK_H_ +#define TALK_XMPP_MUCROOMDISCOVERYTASK_H_ + +#include +#include +#include "talk/xmpp/iqtask.h" + +namespace buzz { + +// This task requests the feature capabilities of the room. It is based on +// XEP-0030, and extended using XEP-0004. +class MucRoomDiscoveryTask : public IqTask { + public: + MucRoomDiscoveryTask(XmppTaskParentInterface* parent, + const Jid& room_jid); + + // Signal (exists, name, features, extended_info) + sigslot::signal5&, + const std::map& > SignalResult; + + protected: + virtual void HandleResult(const XmlElement* stanza); +}; + +} // namespace buzz + +#endif // TALK_XMPP_MUCROOMDISCOVERYTASK_H_ diff --git a/talk/xmpp/mucroomdiscoverytask_unittest.cc b/talk/xmpp/mucroomdiscoverytask_unittest.cc new file mode 100644 index 000000000..b88b6f287 --- /dev/null +++ b/talk/xmpp/mucroomdiscoverytask_unittest.cc @@ -0,0 +1,152 @@ +/* + * libjingle + * Copyright 2011, 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 +#include + +#include "talk/base/faketaskrunner.h" +#include "talk/base/gunit.h" +#include "talk/base/sigslot.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/fakexmppclient.h" +#include "talk/xmpp/mucroomdiscoverytask.h" + +class MucRoomDiscoveryListener : public sigslot::has_slots<> { + public: + MucRoomDiscoveryListener() : error_count(0) {} + + void OnResult(buzz::MucRoomDiscoveryTask* task, + bool exists, + const std::string& name, + const std::set& features, + const std::map& extended_info) { + last_exists = exists; + last_name = name; + last_features = features; + last_extended_info = extended_info; + } + + void OnError(buzz::IqTask* task, + const buzz::XmlElement* error) { + ++error_count; + } + + bool last_exists; + std::string last_name; + std::set last_features; + std::map last_extended_info; + int error_count; +}; + +class MucRoomDiscoveryTaskTest : public testing::Test { + public: + MucRoomDiscoveryTaskTest() : + room_jid("muc-jid-ponies@domain.com"), + room_name("ponies") { + } + + virtual void SetUp() { + runner = new talk_base::FakeTaskRunner(); + xmpp_client = new buzz::FakeXmppClient(runner); + listener = new MucRoomDiscoveryListener(); + } + + virtual void TearDown() { + delete listener; + // delete xmpp_client; Deleted by deleting runner. + delete runner; + } + + talk_base::FakeTaskRunner* runner; + buzz::FakeXmppClient* xmpp_client; + MucRoomDiscoveryListener* listener; + buzz::Jid room_jid; + std::string room_name; +}; + +TEST_F(MucRoomDiscoveryTaskTest, TestDiscovery) { + ASSERT_EQ(0U, xmpp_client->sent_stanzas().size()); + + buzz::MucRoomDiscoveryTask* task = new buzz::MucRoomDiscoveryTask( + xmpp_client, room_jid); + task->SignalResult.connect(listener, &MucRoomDiscoveryListener::OnResult); + task->Start(); + + std::string expected_iq = + "" + "" + ""; + + ASSERT_EQ(1U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_iq, xmpp_client->sent_stanzas()[0]->Str()); + + EXPECT_EQ("", listener->last_name); + + std::string response_iq = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(response_iq)); + + EXPECT_EQ(true, listener->last_exists); + EXPECT_EQ(room_name, listener->last_name); + EXPECT_EQ(2U, listener->last_features.size()); + EXPECT_EQ(1U, listener->last_features.count("feature1")); + EXPECT_EQ(2U, listener->last_extended_info.size()); + EXPECT_EQ("value1", listener->last_extended_info["var1"]); + EXPECT_EQ(0, listener->error_count); +} + +TEST_F(MucRoomDiscoveryTaskTest, TestMissingName) { + buzz::MucRoomDiscoveryTask* task = new buzz::MucRoomDiscoveryTask( + xmpp_client, room_jid); + task->SignalError.connect(listener, &MucRoomDiscoveryListener::OnError); + task->Start(); + + std::string error_iq = + "" + " " + " " + " " + ""; + EXPECT_EQ(0, listener->error_count); + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(error_iq)); + EXPECT_EQ(0, listener->error_count); +} diff --git a/talk/xmpp/mucroomlookuptask.cc b/talk/xmpp/mucroomlookuptask.cc new file mode 100644 index 000000000..b78e5dde7 --- /dev/null +++ b/talk/xmpp/mucroomlookuptask.cc @@ -0,0 +1,176 @@ +/* + * libjingle + * Copyright 2011, 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/xmpp/mucroomlookuptask.h" + +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/xmpp/constants.h" + + +namespace buzz { + +MucRoomLookupTask* +MucRoomLookupTask::CreateLookupTaskForRoomName(XmppTaskParentInterface* parent, + const Jid& lookup_server_jid, + const std::string& room_name, + const std::string& room_domain) { + return new MucRoomLookupTask(parent, lookup_server_jid, + MakeNameQuery(room_name, room_domain)); +} + +MucRoomLookupTask* +MucRoomLookupTask::CreateLookupTaskForRoomJid(XmppTaskParentInterface* parent, + const Jid& lookup_server_jid, + const Jid& room_jid) { + return new MucRoomLookupTask(parent, lookup_server_jid, + MakeJidQuery(room_jid)); +} + +MucRoomLookupTask* +MucRoomLookupTask::CreateLookupTaskForHangoutId(XmppTaskParentInterface* parent, + const Jid& lookup_server_jid, + const std::string& hangout_id) { + return new MucRoomLookupTask(parent, lookup_server_jid, + MakeHangoutIdQuery(hangout_id)); +} + +MucRoomLookupTask* +MucRoomLookupTask::CreateLookupTaskForExternalId( + XmppTaskParentInterface* parent, + const Jid& lookup_server_jid, + const std::string& external_id, + const std::string& type) { + return new MucRoomLookupTask(parent, lookup_server_jid, + MakeExternalIdQuery(external_id, type)); +} + +MucRoomLookupTask::MucRoomLookupTask(XmppTaskParentInterface* parent, + const Jid& lookup_server_jid, + XmlElement* query) + : IqTask(parent, STR_SET, lookup_server_jid, query) { +} + +XmlElement* MucRoomLookupTask::MakeNameQuery( + const std::string& room_name, const std::string& room_domain) { + XmlElement* name_elem = new XmlElement(QN_SEARCH_ROOM_NAME, false); + name_elem->SetBodyText(room_name); + + XmlElement* domain_elem = new XmlElement(QN_SEARCH_ROOM_DOMAIN, false); + domain_elem->SetBodyText(room_domain); + + XmlElement* query = new XmlElement(QN_SEARCH_QUERY, true); + query->AddElement(name_elem); + query->AddElement(domain_elem); + return query; +} + +XmlElement* MucRoomLookupTask::MakeJidQuery(const Jid& room_jid) { + XmlElement* jid_elem = new XmlElement(QN_SEARCH_ROOM_JID); + jid_elem->SetBodyText(room_jid.Str()); + + XmlElement* query = new XmlElement(QN_SEARCH_QUERY); + query->AddElement(jid_elem); + return query; +} + +XmlElement* MucRoomLookupTask::MakeExternalIdQuery( + const std::string& external_id, const std::string& type) { + XmlElement* external_id_elem = new XmlElement(QN_SEARCH_EXTERNAL_ID); + external_id_elem->SetAttr(QN_TYPE, type); + external_id_elem->SetBodyText(external_id); + + XmlElement* query = new XmlElement(QN_SEARCH_QUERY); + query->AddElement(external_id_elem); + return query; +} + +// Construct a stanza to lookup the muc jid for a given hangout id. eg: +// +// +// 0b48ad092c893a53b7bfc87422caf38e93978798e +// +XmlElement* MucRoomLookupTask::MakeHangoutIdQuery( + const std::string& hangout_id) { + XmlElement* hangout_id_elem = new XmlElement(QN_SEARCH_HANGOUT_ID, false); + hangout_id_elem->SetBodyText(hangout_id); + + XmlElement* query = new XmlElement(QN_SEARCH_QUERY, true); + query->AddElement(hangout_id_elem); + return query; +} + +// Handle a response like the following: +// +// +// +// 0b48ad092c893a53b7bfc87422caf38e93978798e +// hangout.google.com +// +// +void MucRoomLookupTask::HandleResult(const XmlElement* stanza) { + const XmlElement* query_elem = stanza->FirstNamed(QN_SEARCH_QUERY); + if (query_elem == NULL) { + SignalError(this, stanza); + return; + } + + const XmlElement* item_elem = query_elem->FirstNamed(QN_SEARCH_ITEM); + if (item_elem == NULL) { + SignalError(this, stanza); + return; + } + + MucRoomInfo room; + room.jid = Jid(item_elem->Attr(buzz::QN_JID)); + if (!room.jid.IsValid()) { + SignalError(this, stanza); + return; + } + + const XmlElement* room_name_elem = + item_elem->FirstNamed(QN_SEARCH_ROOM_NAME); + if (room_name_elem != NULL) { + room.name = room_name_elem->BodyText(); + } + + const XmlElement* room_domain_elem = + item_elem->FirstNamed(QN_SEARCH_ROOM_DOMAIN); + if (room_domain_elem != NULL) { + room.domain = room_domain_elem->BodyText(); + } + + const XmlElement* hangout_id_elem = + item_elem->FirstNamed(QN_SEARCH_HANGOUT_ID); + if (hangout_id_elem != NULL) { + room.hangout_id = hangout_id_elem->BodyText(); + } + + SignalResult(this, room); +} + +} // namespace buzz diff --git a/talk/xmpp/mucroomlookuptask.h b/talk/xmpp/mucroomlookuptask.h new file mode 100644 index 000000000..60001ff9e --- /dev/null +++ b/talk/xmpp/mucroomlookuptask.h @@ -0,0 +1,88 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_XMPP_MUCROOMLOOKUPTASK_H_ +#define TALK_XMPP_MUCROOMLOOKUPTASK_H_ + +#include +#include "talk/xmpp/iqtask.h" + +namespace buzz { + +struct MucRoomInfo { + Jid jid; + std::string name; + std::string domain; + std::string hangout_id; + + std::string full_name() const { + return name + "@" + domain; + } +}; + +class MucRoomLookupTask : public IqTask { + public: + static MucRoomLookupTask* + CreateLookupTaskForRoomName(XmppTaskParentInterface* parent, + const Jid& lookup_server_jid, + const std::string& room_name, + const std::string& room_domain); + static MucRoomLookupTask* + CreateLookupTaskForRoomJid(XmppTaskParentInterface* parent, + const Jid& lookup_server_jid, + const Jid& room_jid); + static MucRoomLookupTask* + CreateLookupTaskForHangoutId(XmppTaskParentInterface* parent, + const Jid& lookup_server_jid, + const std::string& hangout_id); + static MucRoomLookupTask* + CreateLookupTaskForExternalId(XmppTaskParentInterface* parent, + const Jid& lookup_server_jid, + const std::string& external_id, + const std::string& type); + + sigslot::signal2 SignalResult; + + protected: + virtual void HandleResult(const XmlElement* element); + + private: + MucRoomLookupTask(XmppTaskParentInterface* parent, + const Jid& lookup_server_jid, + XmlElement* query); + static XmlElement* MakeNameQuery(const std::string& room_name, + const std::string& room_domain); + static XmlElement* MakeJidQuery(const Jid& room_jid); + static XmlElement* MakeHangoutIdQuery(const std::string& hangout_id); + static XmlElement* MakeExternalIdQuery(const std::string& external_id, + const std::string& type); +}; + +} // namespace buzz + +#endif // TALK_XMPP_MUCROOMLOOKUPTASK_H_ diff --git a/talk/xmpp/mucroomlookuptask_unittest.cc b/talk/xmpp/mucroomlookuptask_unittest.cc new file mode 100644 index 000000000..a662d537d --- /dev/null +++ b/talk/xmpp/mucroomlookuptask_unittest.cc @@ -0,0 +1,204 @@ +/* + * libjingle + * Copyright 2011, 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 +#include + +#include "talk/base/faketaskrunner.h" +#include "talk/base/gunit.h" +#include "talk/base/sigslot.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/fakexmppclient.h" +#include "talk/xmpp/mucroomlookuptask.h" + +class MucRoomLookupListener : public sigslot::has_slots<> { + public: + MucRoomLookupListener() : error_count(0) {} + + void OnResult(buzz::MucRoomLookupTask* task, + const buzz::MucRoomInfo& room) { + last_room = room; + } + + void OnError(buzz::IqTask* task, + const buzz::XmlElement* error) { + ++error_count; + } + + buzz::MucRoomInfo last_room; + int error_count; +}; + +class MucRoomLookupTaskTest : public testing::Test { + public: + MucRoomLookupTaskTest() : + lookup_server_jid("lookup@domain.com"), + room_jid("muc-jid-ponies@domain.com"), + room_name("ponies"), + room_domain("domain.com"), + room_full_name("ponies@domain.com"), + hangout_id("some_hangout_id") { + } + + virtual void SetUp() { + runner = new talk_base::FakeTaskRunner(); + xmpp_client = new buzz::FakeXmppClient(runner); + listener = new MucRoomLookupListener(); + } + + virtual void TearDown() { + delete listener; + // delete xmpp_client; Deleted by deleting runner. + delete runner; + } + + talk_base::FakeTaskRunner* runner; + buzz::FakeXmppClient* xmpp_client; + MucRoomLookupListener* listener; + buzz::Jid lookup_server_jid; + buzz::Jid room_jid; + std::string room_name; + std::string room_domain; + std::string room_full_name; + std::string hangout_id; +}; + +TEST_F(MucRoomLookupTaskTest, TestLookupName) { + ASSERT_EQ(0U, xmpp_client->sent_stanzas().size()); + + buzz::MucRoomLookupTask* task = + buzz::MucRoomLookupTask::CreateLookupTaskForRoomName( + xmpp_client, lookup_server_jid, room_name, room_domain); + task->SignalResult.connect(listener, &MucRoomLookupListener::OnResult); + task->Start(); + + std::string expected_iq = + "" + "" + "ponies" + "domain.com" + "" + ""; + + ASSERT_EQ(1U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_iq, xmpp_client->sent_stanzas()[0]->Str()); + + EXPECT_EQ("", listener->last_room.name); + + std::string response_iq = + "" + " " + " " + " ponies" + " domain.com" + " " + " " + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(response_iq)); + + EXPECT_EQ(room_name, listener->last_room.name); + EXPECT_EQ(room_domain, listener->last_room.domain); + EXPECT_EQ(room_jid, listener->last_room.jid); + EXPECT_EQ(room_full_name, listener->last_room.full_name()); + EXPECT_EQ(0, listener->error_count); +} + +TEST_F(MucRoomLookupTaskTest, TestLookupHangoutId) { + ASSERT_EQ(0U, xmpp_client->sent_stanzas().size()); + + buzz::MucRoomLookupTask* task = buzz::MucRoomLookupTask::CreateLookupTaskForHangoutId( + xmpp_client, lookup_server_jid, hangout_id); + task->SignalResult.connect(listener, &MucRoomLookupListener::OnResult); + task->Start(); + + std::string expected_iq = + "" + "" + "some_hangout_id" + "" + ""; + + ASSERT_EQ(1U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_iq, xmpp_client->sent_stanzas()[0]->Str()); + + EXPECT_EQ("", listener->last_room.name); + + std::string response_iq = + "" + " " + " " + " some_hangout_id" + " domain.com" + " " + " " + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(response_iq)); + + EXPECT_EQ(hangout_id, listener->last_room.name); + EXPECT_EQ(room_domain, listener->last_room.domain); + EXPECT_EQ(room_jid, listener->last_room.jid); + EXPECT_EQ(0, listener->error_count); +} + +TEST_F(MucRoomLookupTaskTest, TestError) { + buzz::MucRoomLookupTask* task = buzz::MucRoomLookupTask::CreateLookupTaskForRoomName( + xmpp_client, lookup_server_jid, room_name, room_domain); + task->SignalError.connect(listener, &MucRoomLookupListener::OnError); + task->Start(); + + std::string error_iq = + "" + ""; + + EXPECT_EQ(0, listener->error_count); + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(error_iq)); + EXPECT_EQ(1, listener->error_count); +} + +TEST_F(MucRoomLookupTaskTest, TestBadJid) { + buzz::MucRoomLookupTask* task = buzz::MucRoomLookupTask::CreateLookupTaskForRoomName( + xmpp_client, lookup_server_jid, room_name, room_domain); + task->SignalError.connect(listener, &MucRoomLookupListener::OnError); + task->Start(); + + std::string response_iq = + "" + " " + " " + " " + ""; + + EXPECT_EQ(0, listener->error_count); + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(response_iq)); + EXPECT_EQ(1, listener->error_count); +} diff --git a/talk/xmpp/mucroomuniquehangoutidtask.cc b/talk/xmpp/mucroomuniquehangoutidtask.cc new file mode 100644 index 000000000..78a8edf27 --- /dev/null +++ b/talk/xmpp/mucroomuniquehangoutidtask.cc @@ -0,0 +1,44 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + + +#include "talk/xmpp/mucroomuniquehangoutidtask.h" + +#include "talk/xmpp/constants.h" + +namespace buzz { + +MucRoomUniqueHangoutIdTask::MucRoomUniqueHangoutIdTask(XmppTaskParentInterface* parent, + const Jid& lookup_server_jid) + : IqTask(parent, STR_GET, lookup_server_jid, MakeUniqueRequestXml()) { +} + +// Construct a stanza to request a unique room id. eg: +// +// +XmlElement* MucRoomUniqueHangoutIdTask::MakeUniqueRequestXml() { + XmlElement* xml = new XmlElement(QN_MUC_UNIQUE_QUERY, false); + xml->SetAttr(QN_HANGOUT_ID, STR_TRUE); + return xml; +} + +// Handle a response like the following: +// +// +// muvc-private-chat-guid@groupchat.google.com +// +void MucRoomUniqueHangoutIdTask::HandleResult(const XmlElement* stanza) { + + const XmlElement* unique_elem = stanza->FirstNamed(QN_MUC_UNIQUE_QUERY); + if (unique_elem == NULL || + !unique_elem->HasAttr(QN_HANGOUT_ID)) { + SignalError(this, stanza); + return; + } + + std::string hangout_id = unique_elem->Attr(QN_HANGOUT_ID); + + SignalResult(this, hangout_id); +} + +} // namespace buzz diff --git a/talk/xmpp/mucroomuniquehangoutidtask.h b/talk/xmpp/mucroomuniquehangoutidtask.h new file mode 100644 index 000000000..d222bacdd --- /dev/null +++ b/talk/xmpp/mucroomuniquehangoutidtask.h @@ -0,0 +1,31 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + + +#ifndef TALK_XMPP_MUCROOMUNIQUEHANGOUTIDTASK_H_ +#define TALK_XMPP_MUCROOMUNIQUEHANGOUTIDTASK_H_ + +#include "talk/xmpp/iqtask.h" + +namespace buzz { + +// Task to request a unique hangout id to be used when starting a hangout. +// The protocol is described in https://docs.google.com/a/google.com/ +// document/d/1EFLT6rCYPDVdqQXSQliXwqB3iUkpZJ9B_MNFeOZgN7g/edit +class MucRoomUniqueHangoutIdTask : public buzz::IqTask { + public: + MucRoomUniqueHangoutIdTask(buzz::XmppTaskParentInterface* parent, + const Jid& lookup_server_jid); + // signal(task, hangout_id) + sigslot::signal2 SignalResult; + + protected: + virtual void HandleResult(const buzz::XmlElement* stanza); + + private: + static buzz::XmlElement* MakeUniqueRequestXml(); + +}; + +} // namespace buzz + +#endif // TALK_XMPP_MUCROOMUNIQUEHANGOUTIDTASK_H_ diff --git a/talk/xmpp/mucroomuniquehangoutidtask_unittest.cc b/talk/xmpp/mucroomuniquehangoutidtask_unittest.cc new file mode 100644 index 000000000..128bab373 --- /dev/null +++ b/talk/xmpp/mucroomuniquehangoutidtask_unittest.cc @@ -0,0 +1,116 @@ +/* + * libjingle + * Copyright 2011, 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 +#include + +#include "talk/base/faketaskrunner.h" +#include "talk/base/gunit.h" +#include "talk/base/sigslot.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/fakexmppclient.h" +#include "talk/xmpp/mucroomuniquehangoutidtask.h" + +class MucRoomUniqueHangoutIdListener : public sigslot::has_slots<> { + public: + MucRoomUniqueHangoutIdListener() : error_count(0) {} + + void OnResult(buzz::MucRoomUniqueHangoutIdTask* task, + const std::string& hangout_id) { + last_hangout_id = hangout_id; + } + + void OnError(buzz::IqTask* task, + const buzz::XmlElement* error) { + ++error_count; + } + + std::string last_hangout_id; + int error_count; +}; + +class MucRoomUniqueHangoutIdTaskTest : public testing::Test { + public: + MucRoomUniqueHangoutIdTaskTest() : + lookup_server_jid("lookup@domain.com"), + hangout_id("some_hangout_id") { + } + + virtual void SetUp() { + runner = new talk_base::FakeTaskRunner(); + xmpp_client = new buzz::FakeXmppClient(runner); + listener = new MucRoomUniqueHangoutIdListener(); + } + + virtual void TearDown() { + delete listener; + // delete xmpp_client; Deleted by deleting runner. + delete runner; + } + + talk_base::FakeTaskRunner* runner; + buzz::FakeXmppClient* xmpp_client; + MucRoomUniqueHangoutIdListener* listener; + buzz::Jid lookup_server_jid; + std::string hangout_id; +}; + +TEST_F(MucRoomUniqueHangoutIdTaskTest, Test) { + ASSERT_EQ(0U, xmpp_client->sent_stanzas().size()); + + buzz::MucRoomUniqueHangoutIdTask* task = new buzz::MucRoomUniqueHangoutIdTask( + xmpp_client, lookup_server_jid); + task->SignalResult.connect(listener, &MucRoomUniqueHangoutIdListener::OnResult); + task->Start(); + + std::string expected_iq = + "" + "" + ""; + + ASSERT_EQ(1U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_iq, xmpp_client->sent_stanzas()[0]->Str()); + + EXPECT_EQ("", listener->last_hangout_id); + + std::string response_iq = + "" + "" + "muvc-private-chat-00001234-5678-9abc-def0-123456789abc" + "" + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(response_iq)); + + EXPECT_EQ(hangout_id, listener->last_hangout_id); + EXPECT_EQ(0, listener->error_count); +} + diff --git a/talk/xmpp/pingtask.cc b/talk/xmpp/pingtask.cc new file mode 100644 index 000000000..233062f7b --- /dev/null +++ b/talk/xmpp/pingtask.cc @@ -0,0 +1,85 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + + +#include "talk/xmpp/pingtask.h" + +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/xmpp/constants.h" + +namespace buzz { + +PingTask::PingTask(buzz::XmppTaskParentInterface* parent, + talk_base::MessageQueue* message_queue, + uint32 ping_period_millis, + uint32 ping_timeout_millis) + : buzz::XmppTask(parent, buzz::XmppEngine::HL_SINGLE), + message_queue_(message_queue), + ping_period_millis_(ping_period_millis), + ping_timeout_millis_(ping_timeout_millis), + next_ping_time_(0), + ping_response_deadline_(0) { + ASSERT(ping_period_millis >= ping_timeout_millis); +} + +bool PingTask::HandleStanza(const buzz::XmlElement* stanza) { + if (!MatchResponseIq(stanza, Jid(STR_EMPTY), task_id())) { + return false; + } + + if (stanza->Attr(buzz::QN_TYPE) != buzz::STR_RESULT && + stanza->Attr(buzz::QN_TYPE) != buzz::STR_ERROR) { + return false; + } + + QueueStanza(stanza); + return true; +} + +// This task runs indefinitely and remains in either the start or blocked +// states. +int PingTask::ProcessStart() { + if (ping_period_millis_ < ping_timeout_millis_) { + LOG(LS_ERROR) << "ping_period_millis should be >= ping_timeout_millis"; + return STATE_ERROR; + } + const buzz::XmlElement* stanza = NextStanza(); + if (stanza != NULL) { + // Received a ping response of some sort (don't care what it is). + ping_response_deadline_ = 0; + } + + uint32 now = talk_base::Time(); + + // If the ping timed out, signal. + if (ping_response_deadline_ != 0 && now >= ping_response_deadline_) { + SignalTimeout(); + return STATE_ERROR; + } + + // Send a ping if it's time. + if (now >= next_ping_time_) { + talk_base::scoped_ptr stanza( + MakeIq(buzz::STR_GET, Jid(STR_EMPTY), task_id())); + stanza->AddElement(new buzz::XmlElement(QN_PING)); + SendStanza(stanza.get()); + + ping_response_deadline_ = now + ping_timeout_millis_; + next_ping_time_ = now + ping_period_millis_; + + // Wake ourselves up when it's time to send another ping or when the ping + // times out (so we can fire a signal). + message_queue_->PostDelayed(ping_timeout_millis_, this); + message_queue_->PostDelayed(ping_period_millis_, this); + } + + return STATE_BLOCKED; +} + +void PingTask::OnMessage(talk_base::Message* msg) { + // Get the task manager to run this task so we can send a ping or signal or + // process a ping response. + Wake(); +} + +} // namespace buzz diff --git a/talk/xmpp/pingtask.h b/talk/xmpp/pingtask.h new file mode 100644 index 000000000..83752412d --- /dev/null +++ b/talk/xmpp/pingtask.h @@ -0,0 +1,71 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_XMPP_PINGTASK_H_ +#define TALK_XMPP_PINGTASK_H_ + +#include "talk/base/messagehandler.h" +#include "talk/base/messagequeue.h" +#include "talk/xmpp/xmpptask.h" + +namespace buzz { + +// Task to periodically send pings to the server to ensure that the network +// connection is valid, implementing XEP-0199. +// +// This is especially useful on cellular networks because: +// 1. It keeps the connections alive through the cellular network's NATs or +// proxies. +// 2. It detects when the server has crashed or any other case in which the +// connection has broken without a fin or reset packet being sent to us. +class PingTask : public buzz::XmppTask, private talk_base::MessageHandler { + public: + PingTask(buzz::XmppTaskParentInterface* parent, + talk_base::MessageQueue* message_queue, uint32 ping_period_millis, + uint32 ping_timeout_millis); + + virtual bool HandleStanza(const buzz::XmlElement* stanza); + virtual int ProcessStart(); + + // Raised if there is no response to a ping within ping_timeout_millis. + // The task is automatically aborted after a timeout. + sigslot::signal0<> SignalTimeout; + + private: + // Implementation of MessageHandler. + virtual void OnMessage(talk_base::Message* msg); + + talk_base::MessageQueue* message_queue_; + uint32 ping_period_millis_; + uint32 ping_timeout_millis_; + uint32 next_ping_time_; + uint32 ping_response_deadline_; // 0 if the response has been received +}; + +} // namespace buzz + +#endif // TALK_XMPP_PINGTASK_H_ diff --git a/talk/xmpp/pingtask_unittest.cc b/talk/xmpp/pingtask_unittest.cc new file mode 100644 index 000000000..477847dde --- /dev/null +++ b/talk/xmpp/pingtask_unittest.cc @@ -0,0 +1,118 @@ +/* + * libjingle + * Copyright 2011, 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 +#include + +#include "talk/base/faketaskrunner.h" +#include "talk/base/gunit.h" +#include "talk/base/sigslot.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/fakexmppclient.h" +#include "talk/xmpp/pingtask.h" + +class PingTaskTest; + +class PingXmppClient : public buzz::FakeXmppClient { + public: + PingXmppClient(talk_base::TaskParent* parent, PingTaskTest* tst) : + FakeXmppClient(parent), test(tst) { + } + + buzz::XmppReturnStatus SendStanza(const buzz::XmlElement* stanza); + + private: + PingTaskTest* test; +}; + +class PingTaskTest : public testing::Test, public sigslot::has_slots<> { + public: + PingTaskTest() : respond_to_pings(true), timed_out(false) { + } + + virtual void SetUp() { + runner = new talk_base::FakeTaskRunner(); + xmpp_client = new PingXmppClient(runner, this); + } + + virtual void TearDown() { + // delete xmpp_client; Deleted by deleting runner. + delete runner; + } + + void ConnectTimeoutSignal(buzz::PingTask* task) { + task->SignalTimeout.connect(this, &PingTaskTest::OnPingTimeout); + } + + void OnPingTimeout() { + timed_out = true; + } + + talk_base::FakeTaskRunner* runner; + PingXmppClient* xmpp_client; + bool respond_to_pings; + bool timed_out; +}; + +buzz::XmppReturnStatus PingXmppClient::SendStanza( + const buzz::XmlElement* stanza) { + buzz::XmppReturnStatus result = FakeXmppClient::SendStanza(stanza); + if (test->respond_to_pings && (stanza->FirstNamed(buzz::QN_PING) != NULL)) { + std::string ping_response = + ""; + HandleStanza(buzz::XmlElement::ForStr(ping_response)); + } + return result; +} + +TEST_F(PingTaskTest, TestSuccess) { + uint32 ping_period_millis = 100; + buzz::PingTask* task = new buzz::PingTask(xmpp_client, + talk_base::Thread::Current(), + ping_period_millis, ping_period_millis / 10); + ConnectTimeoutSignal(task); + task->Start(); + unsigned int expected_ping_count = 5U; + EXPECT_EQ_WAIT(xmpp_client->sent_stanzas().size(), expected_ping_count, + ping_period_millis * (expected_ping_count + 1)); + EXPECT_FALSE(task->IsDone()); + EXPECT_FALSE(timed_out); +} + +TEST_F(PingTaskTest, TestTimeout) { + respond_to_pings = false; + uint32 ping_timeout_millis = 200; + buzz::PingTask* task = new buzz::PingTask(xmpp_client, + talk_base::Thread::Current(), + ping_timeout_millis * 10, ping_timeout_millis); + ConnectTimeoutSignal(task); + task->Start(); + WAIT(false, ping_timeout_millis / 2); + EXPECT_FALSE(timed_out); + EXPECT_TRUE_WAIT(timed_out, ping_timeout_millis * 2); +} diff --git a/talk/xmpp/plainsaslhandler.h b/talk/xmpp/plainsaslhandler.h new file mode 100644 index 000000000..e7d44b9d5 --- /dev/null +++ b/talk/xmpp/plainsaslhandler.h @@ -0,0 +1,80 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _PLAINSASLHANDLER_H_ +#define _PLAINSASLHANDLER_H_ + +#include "talk/xmpp/saslhandler.h" +#include + +namespace buzz { + +class PlainSaslHandler : public SaslHandler { +public: + PlainSaslHandler(const Jid & jid, const talk_base::CryptString & password, + bool allow_plain) : jid_(jid), password_(password), + allow_plain_(allow_plain) {} + + virtual ~PlainSaslHandler() {} + + // Should pick the best method according to this handler + // returns the empty string if none are suitable + virtual std::string ChooseBestSaslMechanism(const std::vector & mechanisms, bool encrypted) { + + if (!encrypted && !allow_plain_) { + return ""; + } + + std::vector::const_iterator it = std::find(mechanisms.begin(), mechanisms.end(), "PLAIN"); + if (it == mechanisms.end()) { + return ""; + } + else { + return "PLAIN"; + } + } + + // Creates a SaslMechanism for the given mechanism name (you own it + // once you get it). If not handled, return NULL. + virtual SaslMechanism * CreateSaslMechanism(const std::string & mechanism) { + if (mechanism == "PLAIN") { + return new SaslPlainMechanism(jid_, password_); + } + return NULL; + } + +private: + Jid jid_; + talk_base::CryptString password_; + bool allow_plain_; +}; + + +} + +#endif + diff --git a/talk/xmpp/presenceouttask.cc b/talk/xmpp/presenceouttask.cc new file mode 100644 index 000000000..cebd740de --- /dev/null +++ b/talk/xmpp/presenceouttask.cc @@ -0,0 +1,157 @@ +/* + * libjingle + * Copyright 2004--2005, 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 +#include +#include "talk/base/stringencode.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/presenceouttask.h" +#include "talk/xmpp/xmppclient.h" + +namespace buzz { + +XmppReturnStatus +PresenceOutTask::Send(const PresenceStatus & s) { + if (GetState() != STATE_INIT && GetState() != STATE_START) + return XMPP_RETURN_BADSTATE; + + XmlElement * presence = TranslateStatus(s); + QueueStanza(presence); + delete presence; + return XMPP_RETURN_OK; +} + +XmppReturnStatus +PresenceOutTask::SendDirected(const Jid & j, const PresenceStatus & s) { + if (GetState() != STATE_INIT && GetState() != STATE_START) + return XMPP_RETURN_BADSTATE; + + XmlElement * presence = TranslateStatus(s); + presence->AddAttr(QN_TO, j.Str()); + QueueStanza(presence); + delete presence; + return XMPP_RETURN_OK; +} + +XmppReturnStatus PresenceOutTask::SendProbe(const Jid & jid) { + if (GetState() != STATE_INIT && GetState() != STATE_START) + return XMPP_RETURN_BADSTATE; + + XmlElement * presence = new XmlElement(QN_PRESENCE); + presence->AddAttr(QN_TO, jid.Str()); + presence->AddAttr(QN_TYPE, "probe"); + + QueueStanza(presence); + delete presence; + return XMPP_RETURN_OK; +} + +int +PresenceOutTask::ProcessStart() { + const XmlElement * stanza = NextStanza(); + if (stanza == NULL) + return STATE_BLOCKED; + + if (SendStanza(stanza) != XMPP_RETURN_OK) + return STATE_ERROR; + + return STATE_START; +} + +XmlElement * +PresenceOutTask::TranslateStatus(const PresenceStatus & s) { + XmlElement * result = new XmlElement(QN_PRESENCE); + if (!s.available()) { + result->AddAttr(QN_TYPE, STR_UNAVAILABLE); + } + else { + if (s.show() != PresenceStatus::SHOW_ONLINE && + s.show() != PresenceStatus::SHOW_OFFLINE) { + result->AddElement(new XmlElement(QN_SHOW)); + switch (s.show()) { + default: + result->AddText(STR_SHOW_AWAY, 1); + break; + case PresenceStatus::SHOW_XA: + result->AddText(STR_SHOW_XA, 1); + break; + case PresenceStatus::SHOW_DND: + result->AddText(STR_SHOW_DND, 1); + break; + case PresenceStatus::SHOW_CHAT: + result->AddText(STR_SHOW_CHAT, 1); + break; + } + } + + result->AddElement(new XmlElement(QN_STATUS)); + result->AddText(s.status(), 1); + + if (!s.nick().empty()) { + result->AddElement(new XmlElement(QN_NICKNAME)); + result->AddText(s.nick(), 1); + } + + std::string pri; + talk_base::ToString(s.priority(), &pri); + + result->AddElement(new XmlElement(QN_PRIORITY)); + result->AddText(pri, 1); + + if (s.know_capabilities()) { + result->AddElement(new XmlElement(QN_CAPS_C, true)); + result->AddAttr(QN_NODE, s.caps_node(), 1); + result->AddAttr(QN_VER, s.version(), 1); + + std::string caps; + caps.append(s.voice_capability() ? "voice-v1" : ""); + caps.append(s.pmuc_capability() ? " pmuc-v1" : ""); + caps.append(s.video_capability() ? " video-v1" : ""); + caps.append(s.camera_capability() ? " camera-v1" : ""); + + result->AddAttr(QN_EXT, caps, 1); + } + + // Put the delay mark on the presence according to JEP-0091 + { + result->AddElement(new XmlElement(kQnDelayX, true)); + + // This here is why we *love* the C runtime + time_t current_time_seconds; + time(¤t_time_seconds); + struct tm* current_time = gmtime(¤t_time_seconds); + char output[256]; + strftime(output, ARRAY_SIZE(output), "%Y%m%dT%H:%M:%S", current_time); + result->AddAttr(kQnStamp, output, 1); + } + } + + return result; +} + + +} diff --git a/talk/xmpp/presenceouttask.h b/talk/xmpp/presenceouttask.h new file mode 100644 index 000000000..cea2b564c --- /dev/null +++ b/talk/xmpp/presenceouttask.h @@ -0,0 +1,54 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _PRESENCEOUTTASK_H_ +#define _PRESENCEOUTTASK_H_ + +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmpptask.h" +#include "talk/xmpp/presencestatus.h" + +namespace buzz { + +class PresenceOutTask : public XmppTask { +public: + explicit PresenceOutTask(XmppTaskParentInterface* parent) + : XmppTask(parent) {} + virtual ~PresenceOutTask() {} + + XmppReturnStatus Send(const PresenceStatus & s); + XmppReturnStatus SendDirected(const Jid & j, const PresenceStatus & s); + XmppReturnStatus SendProbe(const Jid& jid); + + virtual int ProcessStart(); +private: + XmlElement * TranslateStatus(const PresenceStatus & s); +}; + +} + +#endif diff --git a/talk/xmpp/presencereceivetask.cc b/talk/xmpp/presencereceivetask.cc new file mode 100644 index 000000000..80121dde3 --- /dev/null +++ b/talk/xmpp/presencereceivetask.cc @@ -0,0 +1,158 @@ +/* + * libjingle + * Copyright 2004--2012, 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/xmpp/presencereceivetask.h" + +#include "talk/base/stringencode.h" +#include "talk/xmpp/constants.h" + +namespace buzz { + +static bool IsUtf8FirstByte(int c) { + return (((c)&0x80)==0) || // is single byte + ((unsigned char)((c)-0xc0)<0x3e); // or is lead byte +} + +PresenceReceiveTask::PresenceReceiveTask(XmppTaskParentInterface* parent) + : XmppTask(parent, XmppEngine::HL_TYPE) { +} + +PresenceReceiveTask::~PresenceReceiveTask() { + Stop(); +} + +int PresenceReceiveTask::ProcessStart() { + const XmlElement * stanza = NextStanza(); + if (stanza == NULL) { + return STATE_BLOCKED; + } + + Jid from(stanza->Attr(QN_FROM)); + HandlePresence(from, stanza); + + return STATE_START; +} + +bool PresenceReceiveTask::HandleStanza(const XmlElement * stanza) { + // Verify that this is a presence stanze + if (stanza->Name() != QN_PRESENCE) { + return false; // not sure if this ever happens. + } + + // Queue it up + QueueStanza(stanza); + + return true; +} + +void PresenceReceiveTask::HandlePresence(const Jid& from, + const XmlElement* stanza) { + if (stanza->Attr(QN_TYPE) == STR_ERROR) { + return; + } + + PresenceStatus status; + DecodeStatus(from, stanza, &status); + PresenceUpdate(status); +} + +void PresenceReceiveTask::DecodeStatus(const Jid& from, + const XmlElement* stanza, + PresenceStatus* presence_status) { + presence_status->set_jid(from); + if (stanza->Attr(QN_TYPE) == STR_UNAVAILABLE) { + presence_status->set_available(false); + } else { + presence_status->set_available(true); + const XmlElement * status_elem = stanza->FirstNamed(QN_STATUS); + if (status_elem != NULL) { + presence_status->set_status(status_elem->BodyText()); + + // Truncate status messages longer than 300 bytes + if (presence_status->status().length() > 300) { + size_t len = 300; + + // Be careful not to split legal utf-8 chars in half + while (!IsUtf8FirstByte(presence_status->status()[len]) && len > 0) { + len -= 1; + } + std::string truncated(presence_status->status(), 0, len); + presence_status->set_status(truncated); + } + } + + const XmlElement * priority = stanza->FirstNamed(QN_PRIORITY); + if (priority != NULL) { + int pri; + if (talk_base::FromString(priority->BodyText(), &pri)) { + presence_status->set_priority(pri); + } + } + + const XmlElement * show = stanza->FirstNamed(QN_SHOW); + if (show == NULL || show->FirstChild() == NULL) { + presence_status->set_show(PresenceStatus::SHOW_ONLINE); + } else if (show->BodyText() == "away") { + presence_status->set_show(PresenceStatus::SHOW_AWAY); + } else if (show->BodyText() == "xa") { + presence_status->set_show(PresenceStatus::SHOW_XA); + } else if (show->BodyText() == "dnd") { + presence_status->set_show(PresenceStatus::SHOW_DND); + } else if (show->BodyText() == "chat") { + presence_status->set_show(PresenceStatus::SHOW_CHAT); + } else { + presence_status->set_show(PresenceStatus::SHOW_ONLINE); + } + + const XmlElement * caps = stanza->FirstNamed(QN_CAPS_C); + if (caps != NULL) { + std::string node = caps->Attr(QN_NODE); + std::string ver = caps->Attr(QN_VER); + std::string exts = caps->Attr(QN_EXT); + + presence_status->set_know_capabilities(true); + presence_status->set_caps_node(node); + presence_status->set_version(ver); + } + + const XmlElement* delay = stanza->FirstNamed(kQnDelayX); + if (delay != NULL) { + // Ideally we would parse this according to the Psuedo ISO-8601 rules + // that are laid out in JEP-0082: + // http://www.jabber.org/jeps/jep-0082.html + std::string stamp = delay->Attr(kQnStamp); + presence_status->set_sent_time(stamp); + } + + const XmlElement* nick = stanza->FirstNamed(QN_NICKNAME); + if (nick) { + presence_status->set_nick(nick->BodyText()); + } + } +} + +} // namespace buzz diff --git a/talk/xmpp/presencereceivetask.h b/talk/xmpp/presencereceivetask.h new file mode 100644 index 000000000..2bd6494a0 --- /dev/null +++ b/talk/xmpp/presencereceivetask.h @@ -0,0 +1,73 @@ +/* + * libjingle + * Copyright 2004--2012, 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. + */ + +#ifndef THIRD_PARTY_LIBJINGLE_FILES_TALK_XMPP_PRESENCERECEIVETASK_H_ +#define THIRD_PARTY_LIBJINGLE_FILES_TALK_XMPP_PRESENCERECEIVETASK_H_ + +#include "talk/base/sigslot.h" + +#include "talk/xmpp/presencestatus.h" +#include "talk/xmpp/xmpptask.h" + +namespace buzz { + +// A task to receive presence status callbacks from the XMPP server. +class PresenceReceiveTask : public XmppTask { + public: + // Arguments: + // parent a reference to task interface associated withe the XMPP client. + explicit PresenceReceiveTask(XmppTaskParentInterface* parent); + + // Shuts down the thread associated with this task. + virtual ~PresenceReceiveTask(); + + // Starts pulling queued status messages and dispatching them to the + // PresenceUpdate() callback. + virtual int ProcessStart(); + + // Slot for presence message callbacks + sigslot::signal1 PresenceUpdate; + + protected: + // Called by the XMPP engine when presence stanzas are received from the + // server. + virtual bool HandleStanza(const XmlElement * stanza); + + private: + // Handles presence stanzas by converting the data to PresenceStatus + // objects and passing those along to the SignalStatusUpadate() callback. + void HandlePresence(const Jid& from, const XmlElement * stanza); + + // Extracts presence information for the presence stanza sent form the + // server. + static void DecodeStatus(const Jid& from, const XmlElement * stanza, + PresenceStatus* status); +}; + +} // namespace buzz + +#endif // THIRD_PARTY_LIBJINGLE_FILES_TALK_XMPP_PRESENCERECEIVETASK_H_ diff --git a/talk/xmpp/presencestatus.cc b/talk/xmpp/presencestatus.cc new file mode 100644 index 000000000..c75b70546 --- /dev/null +++ b/talk/xmpp/presencestatus.cc @@ -0,0 +1,62 @@ +/* + * libjingle + * Copyright 2004--2012, 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/xmpp/presencestatus.h" + +namespace buzz { +PresenceStatus::PresenceStatus() + : pri_(0), + show_(SHOW_NONE), + available_(false), + e_code_(0), + feedback_probation_(false), + know_capabilities_(false), + voice_capability_(false), + pmuc_capability_(false), + video_capability_(false), + camera_capability_(false) { +} + +void PresenceStatus::UpdateWith(const PresenceStatus& new_value) { + if (!new_value.know_capabilities()) { + bool k = know_capabilities(); + bool p = voice_capability(); + std::string node = caps_node(); + std::string v = version(); + + *this = new_value; + + set_know_capabilities(k); + set_caps_node(node); + set_voice_capability(p); + set_version(v); + } else { + *this = new_value; + } +} + +} // namespace buzz diff --git a/talk/xmpp/presencestatus.h b/talk/xmpp/presencestatus.h new file mode 100644 index 000000000..5cf6b6136 --- /dev/null +++ b/talk/xmpp/presencestatus.h @@ -0,0 +1,205 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef THIRD_PARTY_LIBJINGLE_FILES_TALK_XMPP_PRESENCESTATUS_H_ +#define THIRD_PARTY_LIBJINGLE_FILES_TALK_XMPP_PRESENCESTATUS_H_ + +#include "talk/xmpp/jid.h" +#include "talk/xmpp/constants.h" + +namespace buzz { + +class PresenceStatus { +public: + PresenceStatus(); + ~PresenceStatus() {} + + // These are arranged in "priority order", i.e., if we see + // two statuses at the same priority but with different Shows, + // we will show the one with the highest show in the following + // order. + enum Show { + SHOW_NONE = 0, + SHOW_OFFLINE = 1, + SHOW_XA = 2, + SHOW_AWAY = 3, + SHOW_DND = 4, + SHOW_ONLINE = 5, + SHOW_CHAT = 6, + }; + + const Jid& jid() const { return jid_; } + int priority() const { return pri_; } + Show show() const { return show_; } + const std::string& status() const { return status_; } + const std::string& nick() const { return nick_; } + bool available() const { return available_ ; } + int error_code() const { return e_code_; } + const std::string& error_string() const { return e_str_; } + bool know_capabilities() const { return know_capabilities_; } + bool voice_capability() const { return voice_capability_; } + bool pmuc_capability() const { return pmuc_capability_; } + bool video_capability() const { return video_capability_; } + bool camera_capability() const { return camera_capability_; } + const std::string& caps_node() const { return caps_node_; } + const std::string& version() const { return version_; } + bool feedback_probation() const { return feedback_probation_; } + const std::string& sent_time() const { return sent_time_; } + + void set_jid(const Jid& jid) { jid_ = jid; } + void set_priority(int pri) { pri_ = pri; } + void set_show(Show show) { show_ = show; } + void set_status(const std::string& status) { status_ = status; } + void set_nick(const std::string& nick) { nick_ = nick; } + void set_available(bool a) { available_ = a; } + void set_error(int e_code, const std::string e_str) + { e_code_ = e_code; e_str_ = e_str; } + void set_know_capabilities(bool f) { know_capabilities_ = f; } + void set_voice_capability(bool f) { voice_capability_ = f; } + void set_pmuc_capability(bool f) { pmuc_capability_ = f; } + void set_video_capability(bool f) { video_capability_ = f; } + void set_camera_capability(bool f) { camera_capability_ = f; } + void set_caps_node(const std::string& f) { caps_node_ = f; } + void set_version(const std::string& v) { version_ = v; } + void set_feedback_probation(bool f) { feedback_probation_ = f; } + void set_sent_time(const std::string& time) { sent_time_ = time; } + + void UpdateWith(const PresenceStatus& new_value); + + bool HasQuietStatus() const { + if (status_.empty()) + return false; + return !(QuietStatus().empty()); + } + + // Knowledge of other clients' silly automatic status strings - + // Don't show these. + std::string QuietStatus() const { + if (jid_.resource().find("Psi") != std::string::npos) { + if (status_ == "Online" || + status_.find("Auto Status") != std::string::npos) + return STR_EMPTY; + } + if (jid_.resource().find("Gaim") != std::string::npos) { + if (status_ == "Sorry, I ran out for a bit!") + return STR_EMPTY; + } + return TrimStatus(status_); + } + + std::string ExplicitStatus() const { + std::string result = QuietStatus(); + if (result.empty()) { + result = ShowStatus(); + } + return result; + } + + std::string ShowStatus() const { + std::string result; + if (!available()) { + result = "Offline"; + } + else { + switch (show()) { + case SHOW_AWAY: + case SHOW_XA: + result = "Idle"; + break; + case SHOW_DND: + result = "Busy"; + break; + case SHOW_CHAT: + result = "Chatty"; + break; + default: + result = "Available"; + break; + } + } + return result; + } + + static std::string TrimStatus(const std::string& st) { + std::string s(st); + int j = 0; + bool collapsing = true; + for (unsigned int i = 0; i < s.length(); i+= 1) { + if (s[i] <= ' ' && s[i] >= 0) { + if (collapsing) { + continue; + } + else { + s[j] = ' '; + j += 1; + collapsing = true; + } + } + else { + s[j] = s[i]; + j += 1; + collapsing = false; + } + } + if (collapsing && j > 0) { + j -= 1; + } + s.erase(j, s.length()); + return s; + } + +private: + Jid jid_; + int pri_; + Show show_; + std::string status_; + std::string nick_; + bool available_; + int e_code_; + std::string e_str_; + bool feedback_probation_; + + // capabilities (valid only if know_capabilities_ + bool know_capabilities_; + bool voice_capability_; + bool pmuc_capability_; + bool video_capability_; + bool camera_capability_; + std::string caps_node_; + std::string version_; + + std::string sent_time_; // from the jabber:x:delay element +}; + +class MucPresenceStatus : public PresenceStatus { +}; + +} // namespace buzz + + +#endif // THIRD_PARTY_LIBJINGLE_FILES_TALK_XMPP_PRESENCESTATUS_H_ + diff --git a/talk/xmpp/prexmppauth.h b/talk/xmpp/prexmppauth.h new file mode 100644 index 000000000..3bc5ca681 --- /dev/null +++ b/talk/xmpp/prexmppauth.h @@ -0,0 +1,88 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_PREXMPPAUTH_H_ +#define TALK_XMPP_PREXMPPAUTH_H_ + +#include "talk/base/cryptstring.h" +#include "talk/base/sigslot.h" +#include "talk/xmpp/saslhandler.h" + +namespace talk_base { + class SocketAddress; +} + +namespace buzz { + +class Jid; +class SaslMechanism; + +class CaptchaChallenge { + public: + CaptchaChallenge() : captcha_needed_(false) {} + CaptchaChallenge(const std::string& token, const std::string& url) + : captcha_needed_(true), captcha_token_(token), captcha_image_url_(url) { + } + + bool captcha_needed() const { return captcha_needed_; } + const std::string& captcha_token() const { return captcha_token_; } + + // This url is relative to the gaia server. Once we have better tools + // for cracking URLs, we should probably make this a full URL + const std::string& captcha_image_url() const { return captcha_image_url_; } + + private: + bool captcha_needed_; + std::string captcha_token_; + std::string captcha_image_url_; +}; + +class PreXmppAuth : public SaslHandler { +public: + virtual ~PreXmppAuth() {} + + virtual void StartPreXmppAuth( + const Jid& jid, + const talk_base::SocketAddress& server, + const talk_base::CryptString& pass, + const std::string& auth_mechanism, + const std::string& auth_token) = 0; + + sigslot::signal0<> SignalAuthDone; + + virtual bool IsAuthDone() const = 0; + virtual bool IsAuthorized() const = 0; + virtual bool HadError() const = 0; + virtual int GetError() const = 0; + virtual CaptchaChallenge GetCaptchaChallenge() const = 0; + virtual std::string GetAuthMechanism() const = 0; + virtual std::string GetAuthToken() const = 0; +}; + +} + +#endif // TALK_XMPP_PREXMPPAUTH_H_ diff --git a/talk/xmpp/pubsub_task.cc b/talk/xmpp/pubsub_task.cc new file mode 100644 index 000000000..91e2c729b --- /dev/null +++ b/talk/xmpp/pubsub_task.cc @@ -0,0 +1,217 @@ +/* + * 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/xmpp/pubsub_task.h" + +#include +#include + +#include "talk/base/common.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/xmppengine.h" + +namespace buzz { + +PubsubTask::PubsubTask(XmppTaskParentInterface* parent, + const buzz::Jid& pubsub_node_jid) + : buzz::XmppTask(parent, buzz::XmppEngine::HL_SENDER), + pubsub_node_jid_(pubsub_node_jid) { +} + +PubsubTask::~PubsubTask() { +} + +// Checks for pubsub publish events as well as responses to get IQs. +bool PubsubTask::HandleStanza(const buzz::XmlElement* stanza) { + const buzz::QName& stanza_name(stanza->Name()); + if (stanza_name == buzz::QN_MESSAGE) { + if (MatchStanzaFrom(stanza, pubsub_node_jid_)) { + const buzz::XmlElement* pubsub_event_item = + stanza->FirstNamed(QN_PUBSUB_EVENT); + if (pubsub_event_item != NULL) { + QueueStanza(pubsub_event_item); + return true; + } + } + } else if (stanza_name == buzz::QN_IQ) { + if (MatchResponseIq(stanza, pubsub_node_jid_, task_id())) { + const buzz::XmlElement* pubsub_item = stanza->FirstNamed(QN_PUBSUB); + if (pubsub_item != NULL) { + QueueStanza(pubsub_item); + return true; + } + } + } + return false; +} + +int PubsubTask::ProcessResponse() { + const buzz::XmlElement* stanza = NextStanza(); + if (stanza == NULL) { + return STATE_BLOCKED; + } + + if (stanza->Attr(buzz::QN_TYPE) == buzz::STR_ERROR) { + OnPubsubError(stanza->FirstNamed(buzz::QN_ERROR)); + return STATE_RESPONSE; + } + + const buzz::QName& stanza_name(stanza->Name()); + if (stanza_name == QN_PUBSUB_EVENT) { + HandlePubsubEventMessage(stanza); + } else if (stanza_name == QN_PUBSUB) { + HandlePubsubIqGetResponse(stanza); + } + + return STATE_RESPONSE; +} + +// Registers a function pointer to be called when the value of the pubsub +// node changes. +// Note that this does not actually change the XMPP pubsub +// subscription. All publish events are always received by everyone in the +// MUC. This function just controls whether the handle function will get +// called when the event is received. +bool PubsubTask::SubscribeToNode(const std::string& pubsub_node, + NodeHandler handler) { + subscribed_nodes_[pubsub_node] = handler; + talk_base::scoped_ptr get_iq_request( + MakeIq(buzz::STR_GET, pubsub_node_jid_, task_id())); + if (!get_iq_request) { + return false; + } + buzz::XmlElement* pubsub_element = new buzz::XmlElement(QN_PUBSUB, true); + buzz::XmlElement* items_element = new buzz::XmlElement(QN_PUBSUB_ITEMS, true); + + items_element->AddAttr(buzz::QN_NODE, pubsub_node); + pubsub_element->AddElement(items_element); + get_iq_request->AddElement(pubsub_element); + + if (SendStanza(get_iq_request.get()) != buzz::XMPP_RETURN_OK) { + return false; + } + + return true; +} + +void PubsubTask::UnsubscribeFromNode(const std::string& pubsub_node) { + subscribed_nodes_.erase(pubsub_node); +} + +void PubsubTask::OnPubsubError(const buzz::XmlElement* error_stanza) { +} + +// Checks for a pubsub event message like the following: +// +// +// +// +// +// +// +// +// +// +// +// It also checks for retraction event messages like the following: +// +// +// +// +// +// +// +// +void PubsubTask::HandlePubsubEventMessage( + const buzz::XmlElement* pubsub_event) { + ASSERT(pubsub_event->Name() == QN_PUBSUB_EVENT); + for (const buzz::XmlChild* child = pubsub_event->FirstChild(); + child != NULL; + child = child->NextChild()) { + const buzz::XmlElement* child_element = child->AsElement(); + const buzz::QName& child_name(child_element->Name()); + if (child_name == QN_PUBSUB_EVENT_ITEMS) { + HandlePubsubItems(child_element); + } + } +} + +// Checks for a response to an pubsub IQ get like the following: +// +// +// +// +// +// +// +// +// +// +void PubsubTask::HandlePubsubIqGetResponse( + const buzz::XmlElement* pubsub_iq_response) { + ASSERT(pubsub_iq_response->Name() == QN_PUBSUB); + for (const buzz::XmlChild* child = pubsub_iq_response->FirstChild(); + child != NULL; + child = child->NextChild()) { + const buzz::XmlElement* child_element = child->AsElement(); + const buzz::QName& child_name(child_element->Name()); + if (child_name == QN_PUBSUB_ITEMS) { + HandlePubsubItems(child_element); + } + } +} + +// Calls registered handlers in response to pubsub event or response to +// IQ pubsub get. +// 'items' is the child of a pubsub#event:event node or pubsub:pubsub node. +void PubsubTask::HandlePubsubItems(const buzz::XmlElement* items) { + ASSERT(items->HasAttr(QN_NODE)); + const std::string& node_name(items->Attr(QN_NODE)); + NodeSubscriptions::iterator iter = subscribed_nodes_.find(node_name); + if (iter != subscribed_nodes_.end()) { + NodeHandler handler = iter->second; + const buzz::XmlElement* item = items->FirstElement(); + while (item != NULL) { + const buzz::QName& item_name(item->Name()); + if (item_name != QN_PUBSUB_EVENT_ITEM && + item_name != QN_PUBSUB_EVENT_RETRACT && + item_name != QN_PUBSUB_ITEM) { + continue; + } + + (this->*handler)(item); + item = item->NextElement(); + } + return; + } +} + +} diff --git a/talk/xmpp/pubsub_task.h b/talk/xmpp/pubsub_task.h new file mode 100644 index 000000000..45a74624a --- /dev/null +++ b/talk/xmpp/pubsub_task.h @@ -0,0 +1,75 @@ +/* + * libjingle + * Copyright 2004--2011, 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. + */ + +#ifndef TALK_XMPP_PUBSUB_TASK_H_ +#define TALK_XMPP_PUBSUB_TASK_H_ + +#include +#include +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/jid.h" +#include "talk/xmpp/xmpptask.h" + +namespace buzz { + +// Base class to help write pubsub tasks. +// In ProcessStart call SubscribeNode with namespaces of interest along with +// NodeHandlers. +// When pubsub notifications arrive and matches the namespace, the NodeHandlers +// will be called back. +class PubsubTask : public buzz::XmppTask { + public: + virtual ~PubsubTask(); + + protected: + typedef void (PubsubTask::*NodeHandler)(const buzz::XmlElement* node); + + PubsubTask(XmppTaskParentInterface* parent, const buzz::Jid& pubsub_node_jid); + + virtual bool HandleStanza(const buzz::XmlElement* stanza); + virtual int ProcessResponse(); + + bool SubscribeToNode(const std::string& pubsub_node, NodeHandler handler); + void UnsubscribeFromNode(const std::string& pubsub_node); + + // Called when there is an error. Derived class can do what it needs to. + virtual void OnPubsubError(const buzz::XmlElement* error_stanza); + + private: + typedef std::map NodeSubscriptions; + + void HandlePubsubIqGetResponse(const buzz::XmlElement* pubsub_iq_response); + void HandlePubsubEventMessage(const buzz::XmlElement* pubsub_event_message); + void HandlePubsubItems(const buzz::XmlElement* items); + + buzz::Jid pubsub_node_jid_; + NodeSubscriptions subscribed_nodes_; +}; + +} // namespace buzz + +#endif // TALK_XMPP_PUBSUB_TASK_H_ diff --git a/talk/xmpp/pubsubclient.cc b/talk/xmpp/pubsubclient.cc new file mode 100644 index 000000000..8d6d4c414 --- /dev/null +++ b/talk/xmpp/pubsubclient.cc @@ -0,0 +1,137 @@ +/* + * libjingle + * Copyright 2011, 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/xmpp/pubsubclient.h" + +#include +#include + +#include "talk/xmpp/constants.h" +#include "talk/xmpp/jid.h" +#include "talk/xmpp/pubsubtasks.h" + +namespace buzz { + +void PubSubClient::RequestItems() { + PubSubRequestTask* request_task = + new PubSubRequestTask(parent_, pubsubjid_, node_); + request_task->SignalResult.connect(this, &PubSubClient::OnRequestResult); + request_task->SignalError.connect(this, &PubSubClient::OnRequestError); + + PubSubReceiveTask* receive_task = + new PubSubReceiveTask(parent_, pubsubjid_, node_); + receive_task->SignalUpdate.connect(this, &PubSubClient::OnReceiveUpdate); + + receive_task->Start(); + request_task->Start(); +} + +void PubSubClient::PublishItem( + const std::string& itemid, XmlElement* payload, std::string* task_id_out) { + std::vector children; + children.push_back(payload); + PublishItem(itemid, children, task_id_out); +} + +void PubSubClient::PublishItem( + const std::string& itemid, const std::vector& children, + std::string* task_id_out) { + PubSubPublishTask* publish_task = + new PubSubPublishTask(parent_, pubsubjid_, node_, itemid, children); + publish_task->SignalError.connect(this, &PubSubClient::OnPublishError); + publish_task->SignalResult.connect(this, &PubSubClient::OnPublishResult); + publish_task->Start(); + if (task_id_out) { + *task_id_out = publish_task->task_id(); + } +} + +void PubSubClient::RetractItem( + const std::string& itemid, std::string* task_id_out) { + PubSubRetractTask* retract_task = + new PubSubRetractTask(parent_, pubsubjid_, node_, itemid); + retract_task->SignalError.connect(this, &PubSubClient::OnRetractError); + retract_task->SignalResult.connect(this, &PubSubClient::OnRetractResult); + retract_task->Start(); + if (task_id_out) { + *task_id_out = retract_task->task_id(); + } +} + +void PubSubClient::OnRequestResult(PubSubRequestTask* task, + const std::vector& items) { + SignalItems(this, items); +} + +void PubSubClient::OnRequestError(IqTask* task, + const XmlElement* stanza) { + SignalRequestError(this, stanza); +} + +void PubSubClient::OnReceiveUpdate(PubSubReceiveTask* task, + const std::vector& items) { + SignalItems(this, items); +} + +const XmlElement* GetItemFromStanza(const XmlElement* stanza) { + if (stanza != NULL) { + const XmlElement* pubsub = stanza->FirstNamed(QN_PUBSUB); + if (pubsub != NULL) { + const XmlElement* publish = pubsub->FirstNamed(QN_PUBSUB_PUBLISH); + if (publish != NULL) { + return publish->FirstNamed(QN_PUBSUB_ITEM); + } + } + } + return NULL; +} + +void PubSubClient::OnPublishResult(PubSubPublishTask* task) { + const XmlElement* item = GetItemFromStanza(task->stanza()); + SignalPublishResult(this, task->task_id(), item); +} + +void PubSubClient::OnPublishError(IqTask* task, + const XmlElement* error_stanza) { + PubSubPublishTask* publish_task = + static_cast(task); + const XmlElement* item = GetItemFromStanza(publish_task->stanza()); + SignalPublishError(this, publish_task->task_id(), item, error_stanza); +} + +void PubSubClient::OnRetractResult(PubSubRetractTask* task) { + SignalRetractResult(this, task->task_id()); +} + +void PubSubClient::OnRetractError(IqTask* task, + const XmlElement* stanza) { + PubSubRetractTask* retract_task = + static_cast(task); + SignalRetractError(this, retract_task->task_id(), stanza); +} + +} // namespace buzz diff --git a/talk/xmpp/pubsubclient.h b/talk/xmpp/pubsubclient.h new file mode 100644 index 000000000..099765a2d --- /dev/null +++ b/talk/xmpp/pubsubclient.h @@ -0,0 +1,125 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_XMPP_PUBSUBCLIENT_H_ +#define TALK_XMPP_PUBSUBCLIENT_H_ + +#include +#include + +#include "talk/base/sigslot.h" +#include "talk/base/sigslotrepeater.h" +#include "talk/base/task.h" +#include "talk/xmpp/jid.h" +#include "talk/xmpp/pubsubtasks.h" + +// Easy to use clients built on top of the tasks for XEP-0060 +// (http://xmpp.org/extensions/xep-0060.html). + +namespace buzz { + +class Jid; +class XmlElement; +class XmppTaskParentInterface; + +// An easy-to-use pubsub client that handles the three tasks of +// getting, publishing, and listening for updates. Tied to a specific +// pubsub jid and node. All you have to do is RequestItems, listen +// for SignalItems and PublishItems. +class PubSubClient : public sigslot::has_slots<> { + public: + PubSubClient(XmppTaskParentInterface* parent, + const Jid& pubsubjid, + const std::string& node) + : parent_(parent), + pubsubjid_(pubsubjid), + node_(node) {} + + const std::string& node() const { return node_; } + + // Requests the , which will be returned via + // SignalItems, or SignalRequestError if there is a failure. Should + // auto-subscribe. + void RequestItems(); + // Fired when either are returned or when + // are received. + sigslot::signal2&> SignalItems; + // Signal (this, error stanza) + sigslot::signal2 SignalRequestError; + // Signal (this, task_id, item, error stanza) + sigslot::signal4 SignalPublishError; + // Signal (this, task_id, item) + sigslot::signal3 SignalPublishResult; + // Signal (this, task_id, error stanza) + sigslot::signal3 SignalRetractError; + // Signal (this, task_id) + sigslot::signal2 SignalRetractResult; + + // Publish an item. Takes ownership of payload. + void PublishItem(const std::string& itemid, + XmlElement* payload, + std::string* task_id_out); + // Publish an item. Takes ownership of children. + void PublishItem(const std::string& itemid, + const std::vector& children, + std::string* task_id_out); + // Retract (delete) an item. + void RetractItem(const std::string& itemid, + std::string* task_id_out); + + private: + void OnRequestError(IqTask* task, + const XmlElement* stanza); + void OnRequestResult(PubSubRequestTask* task, + const std::vector& items); + void OnReceiveUpdate(PubSubReceiveTask* task, + const std::vector& items); + void OnPublishResult(PubSubPublishTask* task); + void OnPublishError(IqTask* task, + const XmlElement* stanza); + void OnRetractResult(PubSubRetractTask* task); + void OnRetractError(IqTask* task, + const XmlElement* stanza); + + XmppTaskParentInterface* parent_; + Jid pubsubjid_; + std::string node_; +}; + +} // namespace buzz + +#endif // TALK_XMPP_PUBSUBCLIENT_H_ diff --git a/talk/xmpp/pubsubclient_unittest.cc b/talk/xmpp/pubsubclient_unittest.cc new file mode 100644 index 000000000..2e4c51142 --- /dev/null +++ b/talk/xmpp/pubsubclient_unittest.cc @@ -0,0 +1,271 @@ +// Copyright 2011 Google Inc. All Rights Reserved + + +#include + +#include "talk/base/faketaskrunner.h" +#include "talk/base/gunit.h" +#include "talk/base/sigslot.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/fakexmppclient.h" +#include "talk/xmpp/jid.h" +#include "talk/xmpp/pubsubclient.h" + +struct HandledPubSubItem { + std::string itemid; + std::string payload; +}; + +class TestPubSubItemsListener : public sigslot::has_slots<> { + public: + TestPubSubItemsListener() : error_count(0) {} + + void OnItems(buzz::PubSubClient*, + const std::vector& items) { + for (std::vector::const_iterator item = items.begin(); + item != items.end(); ++item) { + HandledPubSubItem handled_item; + handled_item.itemid = item->itemid; + if (item->elem->FirstElement() != NULL) { + handled_item.payload = item->elem->FirstElement()->Str(); + } + this->items.push_back(handled_item); + } + } + + void OnRequestError(buzz::PubSubClient* client, + const buzz::XmlElement* stanza) { + error_count++; + } + + void OnPublishResult(buzz::PubSubClient* client, + const std::string& task_id, + const buzz::XmlElement* item) { + result_task_id = task_id; + } + + void OnPublishError(buzz::PubSubClient* client, + const std::string& task_id, + const buzz::XmlElement* item, + const buzz::XmlElement* stanza) { + error_count++; + error_task_id = task_id; + } + + void OnRetractResult(buzz::PubSubClient* client, + const std::string& task_id) { + result_task_id = task_id; + } + + void OnRetractError(buzz::PubSubClient* client, + const std::string& task_id, + const buzz::XmlElement* stanza) { + error_count++; + error_task_id = task_id; + } + + std::vector items; + int error_count; + std::string error_task_id; + std::string result_task_id; +}; + +class PubSubClientTest : public testing::Test { + public: + PubSubClientTest() : + pubsubjid("room@domain.com"), + node("topic"), + itemid("key") { + runner.reset(new talk_base::FakeTaskRunner()); + xmpp_client = new buzz::FakeXmppClient(runner.get()); + client.reset(new buzz::PubSubClient(xmpp_client, pubsubjid, node)); + listener.reset(new TestPubSubItemsListener()); + client->SignalItems.connect( + listener.get(), &TestPubSubItemsListener::OnItems); + client->SignalRequestError.connect( + listener.get(), &TestPubSubItemsListener::OnRequestError); + client->SignalPublishResult.connect( + listener.get(), &TestPubSubItemsListener::OnPublishResult); + client->SignalPublishError.connect( + listener.get(), &TestPubSubItemsListener::OnPublishError); + client->SignalRetractResult.connect( + listener.get(), &TestPubSubItemsListener::OnRetractResult); + client->SignalRetractError.connect( + listener.get(), &TestPubSubItemsListener::OnRetractError); + } + + talk_base::scoped_ptr runner; + // xmpp_client deleted by deleting runner. + buzz::FakeXmppClient* xmpp_client; + talk_base::scoped_ptr client; + talk_base::scoped_ptr listener; + buzz::Jid pubsubjid; + std::string node; + std::string itemid; +}; + +TEST_F(PubSubClientTest, TestRequest) { + client->RequestItems(); + + std::string expected_iq = + "" + "" + "" + "" + ""; + + ASSERT_EQ(1U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_iq, xmpp_client->sent_stanzas()[0]->Str()); + + std::string result_iq = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + ASSERT_EQ(2U, listener->items.size()); + EXPECT_EQ("key0", listener->items[0].itemid); + EXPECT_EQ("", + listener->items[0].payload); + EXPECT_EQ("key1", listener->items[1].itemid); + EXPECT_EQ("", + listener->items[1].payload); + + std::string items_message = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(items_message)); + ASSERT_EQ(4U, listener->items.size()); + EXPECT_EQ("key0", listener->items[2].itemid); + EXPECT_EQ("", + listener->items[2].payload); + EXPECT_EQ("key1", listener->items[3].itemid); + EXPECT_EQ("", + listener->items[3].payload); +} + +TEST_F(PubSubClientTest, TestRequestError) { + std::string result_iq = + "" + " " + " " + " " + ""; + + client->RequestItems(); + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ(1, listener->error_count); +} + +TEST_F(PubSubClientTest, TestPublish) { + buzz::XmlElement* payload = + new buzz::XmlElement(buzz::QName(buzz::NS_PUBSUB, "value")); + + std::string task_id; + client->PublishItem(itemid, payload, &task_id); + + std::string expected_iq = + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + ASSERT_EQ(1U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_iq, xmpp_client->sent_stanzas()[0]->Str()); + + std::string result_iq = + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ(task_id, listener->result_task_id); +} + +TEST_F(PubSubClientTest, TestPublishError) { + buzz::XmlElement* payload = + new buzz::XmlElement(buzz::QName(buzz::NS_PUBSUB, "value")); + + std::string task_id; + client->PublishItem(itemid, payload, &task_id); + + std::string result_iq = + "" + " " + " " + " " + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ(1, listener->error_count); + EXPECT_EQ(task_id, listener->error_task_id); +} + +TEST_F(PubSubClientTest, TestRetract) { + std::string task_id; + client->RetractItem(itemid, &task_id); + + std::string expected_iq = + "" + "" + "" + "" + "" + "" + ""; + + ASSERT_EQ(1U, xmpp_client->sent_stanzas().size()); + EXPECT_EQ(expected_iq, xmpp_client->sent_stanzas()[0]->Str()); + + std::string result_iq = + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ(task_id, listener->result_task_id); +} + +TEST_F(PubSubClientTest, TestRetractError) { + std::string task_id; + client->RetractItem(itemid, &task_id); + + std::string result_iq = + "" + " " + " " + " " + ""; + + xmpp_client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + EXPECT_EQ(1, listener->error_count); + EXPECT_EQ(task_id, listener->error_task_id); +} diff --git a/talk/xmpp/pubsubtasks.cc b/talk/xmpp/pubsubtasks.cc new file mode 100644 index 000000000..bbefbe55c --- /dev/null +++ b/talk/xmpp/pubsubtasks.cc @@ -0,0 +1,214 @@ +/* + * libjingle + * Copyright 2011, 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/xmpp/pubsubtasks.h" + +#include +#include + +#include "talk/xmpp/constants.h" +#include "talk/xmpp/receivetask.h" + +// An implementation of the tasks for XEP-0060 +// (http://xmpp.org/extensions/xep-0060.html). + +namespace buzz { + +namespace { + +bool IsPubSubEventItemsElem(const XmlElement* stanza, + const std::string& expected_node) { + if (stanza->Name() != QN_MESSAGE) { + return false; + } + + const XmlElement* event_elem = stanza->FirstNamed(QN_PUBSUB_EVENT); + if (event_elem == NULL) { + return false; + } + + const XmlElement* items_elem = event_elem->FirstNamed(QN_PUBSUB_EVENT_ITEMS); + if (items_elem == NULL) { + return false; + } + + const std::string& actual_node = items_elem->Attr(QN_NODE); + return (actual_node == expected_node); +} + + +// Creates +XmlElement* CreatePubSubItemsElem(const std::string& node) { + XmlElement* items_elem = new XmlElement(QN_PUBSUB_ITEMS, false); + items_elem->AddAttr(QN_NODE, node); + XmlElement* pubsub_elem = new XmlElement(QN_PUBSUB, false); + pubsub_elem->AddElement(items_elem); + return pubsub_elem; +} + +// Creates payload... +// Takes ownership of payload. +XmlElement* CreatePubSubPublishItemElem( + const std::string& node, + const std::string& itemid, + const std::vector& children) { + XmlElement* pubsub_elem = new XmlElement(QN_PUBSUB, true); + XmlElement* publish_elem = new XmlElement(QN_PUBSUB_PUBLISH, false); + publish_elem->AddAttr(QN_NODE, node); + XmlElement* item_elem = new XmlElement(QN_PUBSUB_ITEM, false); + item_elem->AddAttr(QN_ID, itemid); + for (std::vector::const_iterator child = children.begin(); + child != children.end(); ++child) { + item_elem->AddElement(*child); + } + publish_elem->AddElement(item_elem); + pubsub_elem->AddElement(publish_elem); + return pubsub_elem; +} + +// Creates payload... +// Takes ownership of payload. +XmlElement* CreatePubSubRetractItemElem(const std::string& node, + const std::string& itemid) { + XmlElement* pubsub_elem = new XmlElement(QN_PUBSUB, true); + XmlElement* retract_elem = new XmlElement(QN_PUBSUB_RETRACT, false); + retract_elem->AddAttr(QN_NODE, node); + retract_elem->AddAttr(QN_NOTIFY, "true"); + XmlElement* item_elem = new XmlElement(QN_PUBSUB_ITEM, false); + item_elem->AddAttr(QN_ID, itemid); + retract_elem->AddElement(item_elem); + pubsub_elem->AddElement(retract_elem); + return pubsub_elem; +} + +void ParseItem(const XmlElement* item_elem, + std::vector* items) { + PubSubItem item; + item.itemid = item_elem->Attr(QN_ID); + item.elem = item_elem; + items->push_back(item); +} + +// Right now, s are treated the same as items with empty +// payloads. We may want to change it in the future, but right now +// it's sufficient for our needs. +void ParseRetract(const XmlElement* retract_elem, + std::vector* items) { + ParseItem(retract_elem, items); +} + +void ParseEventItemsElem(const XmlElement* stanza, + std::vector* items) { + const XmlElement* event_elem = stanza->FirstNamed(QN_PUBSUB_EVENT); + if (event_elem != NULL) { + const XmlElement* items_elem = + event_elem->FirstNamed(QN_PUBSUB_EVENT_ITEMS); + if (items_elem != NULL) { + for (const XmlElement* item_elem = + items_elem->FirstNamed(QN_PUBSUB_EVENT_ITEM); + item_elem != NULL; + item_elem = item_elem->NextNamed(QN_PUBSUB_EVENT_ITEM)) { + ParseItem(item_elem, items); + } + for (const XmlElement* retract_elem = + items_elem->FirstNamed(QN_PUBSUB_EVENT_RETRACT); + retract_elem != NULL; + retract_elem = retract_elem->NextNamed(QN_PUBSUB_EVENT_RETRACT)) { + ParseRetract(retract_elem, items); + } + } + } +} + +void ParsePubSubItemsElem(const XmlElement* stanza, + std::vector* items) { + const XmlElement* pubsub_elem = stanza->FirstNamed(QN_PUBSUB); + if (pubsub_elem != NULL) { + const XmlElement* items_elem = pubsub_elem->FirstNamed(QN_PUBSUB_ITEMS); + if (items_elem != NULL) { + for (const XmlElement* item_elem = items_elem->FirstNamed(QN_PUBSUB_ITEM); + item_elem != NULL; + item_elem = item_elem->NextNamed(QN_PUBSUB_ITEM)) { + ParseItem(item_elem, items); + } + } + } +} + +} // namespace + +PubSubRequestTask::PubSubRequestTask(XmppTaskParentInterface* parent, + const Jid& pubsubjid, + const std::string& node) + : IqTask(parent, STR_GET, pubsubjid, CreatePubSubItemsElem(node)) { +} + +void PubSubRequestTask::HandleResult(const XmlElement* stanza) { + std::vector items; + ParsePubSubItemsElem(stanza, &items); + SignalResult(this, items); +} + +bool PubSubReceiveTask::WantsStanza(const XmlElement* stanza) { + return MatchStanzaFrom(stanza, pubsubjid_) && + IsPubSubEventItemsElem(stanza, node_); +} + +void PubSubReceiveTask::ReceiveStanza(const XmlElement* stanza) { + std::vector items; + ParseEventItemsElem(stanza, &items); + SignalUpdate(this, items); +} + +PubSubPublishTask::PubSubPublishTask(XmppTaskParentInterface* parent, + const Jid& pubsubjid, + const std::string& node, + const std::string& itemid, + const std::vector& children) + : IqTask(parent, STR_SET, pubsubjid, + CreatePubSubPublishItemElem(node, itemid, children)), + itemid_(itemid) { +} + +void PubSubPublishTask::HandleResult(const XmlElement* stanza) { + SignalResult(this); +} + +PubSubRetractTask::PubSubRetractTask(XmppTaskParentInterface* parent, + const Jid& pubsubjid, + const std::string& node, + const std::string& itemid) + : IqTask(parent, STR_SET, pubsubjid, + CreatePubSubRetractItemElem(node, itemid)), + itemid_(itemid) { +} + +void PubSubRetractTask::HandleResult(const XmlElement* stanza) { + SignalResult(this); +} + +} // namespace buzz diff --git a/talk/xmpp/pubsubtasks.h b/talk/xmpp/pubsubtasks.h new file mode 100644 index 000000000..f0a158178 --- /dev/null +++ b/talk/xmpp/pubsubtasks.h @@ -0,0 +1,130 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_XMPP_PUBSUBTASKS_H_ +#define TALK_XMPP_PUBSUBTASKS_H_ + +#include + +#include "talk/base/sigslot.h" +#include "talk/xmpp/iqtask.h" +#include "talk/xmpp/receivetask.h" + +namespace buzz { + +// A PubSub itemid + payload. Useful for signaling items. +struct PubSubItem { + std::string itemid; + // The entire , owned by the stanza handler. To keep a + // reference after handling, make a copy. + const XmlElement* elem; +}; + +// An IqTask which gets a for a particular jid and +// node, parses the items in the response and signals the items. +class PubSubRequestTask : public IqTask { + public: + PubSubRequestTask(XmppTaskParentInterface* parent, + const Jid& pubsubjid, + const std::string& node); + + sigslot::signal2&> SignalResult; + // SignalError inherited by IqTask. + private: + virtual void HandleResult(const XmlElement* stanza); +}; + +// A ReceiveTask which listens for of a particular +// pubsub JID and node and then signals them items. +class PubSubReceiveTask : public ReceiveTask { + public: + PubSubReceiveTask(XmppTaskParentInterface* parent, + const Jid& pubsubjid, + const std::string& node) + : ReceiveTask(parent), + pubsubjid_(pubsubjid), + node_(node) { + } + + sigslot::signal2&> SignalUpdate; + + protected: + virtual bool WantsStanza(const XmlElement* stanza); + virtual void ReceiveStanza(const XmlElement* stanza); + + private: + Jid pubsubjid_; + std::string node_; +}; + +// An IqTask which publishes a to a particular +// pubsub jid and node. +class PubSubPublishTask : public IqTask { + public: + // Takes ownership of children + PubSubPublishTask(XmppTaskParentInterface* parent, + const Jid& pubsubjid, + const std::string& node, + const std::string& itemid, + const std::vector& children); + + const std::string& itemid() const { return itemid_; } + + sigslot::signal1 SignalResult; + + private: + // SignalError inherited by IqTask. + virtual void HandleResult(const XmlElement* stanza); + + std::string itemid_; +}; + +// An IqTask which publishes a to a particular +// pubsub jid and node. +class PubSubRetractTask : public IqTask { + public: + PubSubRetractTask(XmppTaskParentInterface* parent, + const Jid& pubsubjid, + const std::string& node, + const std::string& itemid); + + const std::string& itemid() const { return itemid_; } + + sigslot::signal1 SignalResult; + + private: + // SignalError inherited by IqTask. + virtual void HandleResult(const XmlElement* stanza); + + std::string itemid_; +}; + +} // namespace buzz + +#endif // TALK_XMPP_PUBSUBTASKS_H_ diff --git a/talk/xmpp/pubsubtasks_unittest.cc b/talk/xmpp/pubsubtasks_unittest.cc new file mode 100644 index 000000000..67fc30641 --- /dev/null +++ b/talk/xmpp/pubsubtasks_unittest.cc @@ -0,0 +1,273 @@ +// Copyright 2011 Google Inc. All Rights Reserved + + +#include + +#include "talk/base/faketaskrunner.h" +#include "talk/base/gunit.h" +#include "talk/base/sigslot.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/iqtask.h" +#include "talk/xmpp/fakexmppclient.h" +#include "talk/xmpp/jid.h" +#include "talk/xmpp/pubsubtasks.h" + +struct HandledPubSubItem { + std::string itemid; + std::string payload; +}; + +class TestPubSubTasksListener : public sigslot::has_slots<> { + public: + TestPubSubTasksListener() : result_count(0), error_count(0) {} + + void OnReceiveUpdate(buzz::PubSubReceiveTask* task, + const std::vector& items) { + OnItems(items); + } + + void OnRequestResult(buzz::PubSubRequestTask* task, + const std::vector& items) { + OnItems(items); + } + + void OnItems(const std::vector& items) { + for (std::vector::const_iterator item = items.begin(); + item != items.end(); ++item) { + HandledPubSubItem handled_item; + handled_item.itemid = item->itemid; + if (item->elem->FirstElement() != NULL) { + handled_item.payload = item->elem->FirstElement()->Str(); + } + this->items.push_back(handled_item); + } + } + + void OnPublishResult(buzz::PubSubPublishTask* task) { + ++result_count; + } + + void OnRetractResult(buzz::PubSubRetractTask* task) { + ++result_count; + } + + void OnError(buzz::IqTask* task, const buzz::XmlElement* stanza) { + ++error_count; + } + + std::vector items; + int result_count; + int error_count; +}; + +class PubSubTasksTest : public testing::Test { + public: + PubSubTasksTest() : + pubsubjid("room@domain.com"), + node("topic"), + itemid("key") { + runner.reset(new talk_base::FakeTaskRunner()); + client = new buzz::FakeXmppClient(runner.get()); + listener.reset(new TestPubSubTasksListener()); + } + + talk_base::scoped_ptr runner; + // Client deleted by deleting runner. + buzz::FakeXmppClient* client; + talk_base::scoped_ptr listener; + buzz::Jid pubsubjid; + std::string node; + std::string itemid; +}; + +TEST_F(PubSubTasksTest, TestRequest) { + buzz::PubSubRequestTask* task = + new buzz::PubSubRequestTask(client, pubsubjid, node); + task->SignalResult.connect( + listener.get(), &TestPubSubTasksListener::OnRequestResult); + task->Start(); + + std::string expected_iq = + "" + "" + "" + "" + ""; + + ASSERT_EQ(1U, client->sent_stanzas().size()); + EXPECT_EQ(expected_iq, client->sent_stanzas()[0]->Str()); + + std::string result_iq = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + + client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + + ASSERT_EQ(2U, listener->items.size()); + EXPECT_EQ("key0", listener->items[0].itemid); + EXPECT_EQ("", + listener->items[0].payload); + EXPECT_EQ("key1", listener->items[1].itemid); + EXPECT_EQ("", + listener->items[1].payload); +} + +TEST_F(PubSubTasksTest, TestRequestError) { + std::string result_iq = + "" + " " + " " + " " + ""; + + buzz::PubSubRequestTask* task = + new buzz::PubSubRequestTask(client, pubsubjid, node); + task->SignalResult.connect( + listener.get(), &TestPubSubTasksListener::OnRequestResult); + task->SignalError.connect( + listener.get(), &TestPubSubTasksListener::OnError); + task->Start(); + client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + + EXPECT_EQ(0, listener->result_count); + EXPECT_EQ(1, listener->error_count); +} + +TEST_F(PubSubTasksTest, TestReceive) { + std::string items_message = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + + buzz::PubSubReceiveTask* task = + new buzz::PubSubReceiveTask(client, pubsubjid, node); + task->SignalUpdate.connect( + listener.get(), &TestPubSubTasksListener::OnReceiveUpdate); + task->Start(); + client->HandleStanza(buzz::XmlElement::ForStr(items_message)); + + ASSERT_EQ(2U, listener->items.size()); + EXPECT_EQ("key0", listener->items[0].itemid); + EXPECT_EQ( + "", + listener->items[0].payload); + EXPECT_EQ("key1", listener->items[1].itemid); + EXPECT_EQ( + "", + listener->items[1].payload); +} + +TEST_F(PubSubTasksTest, TestPublish) { + buzz::XmlElement* payload = + new buzz::XmlElement(buzz::QName(buzz::NS_PUBSUB, "value")); + std::string expected_iq = + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + std::vector children; + children.push_back(payload); + buzz::PubSubPublishTask* task = + new buzz::PubSubPublishTask(client, pubsubjid, node, itemid, children); + task->SignalResult.connect( + listener.get(), &TestPubSubTasksListener::OnPublishResult); + task->Start(); + + ASSERT_EQ(1U, client->sent_stanzas().size()); + EXPECT_EQ(expected_iq, client->sent_stanzas()[0]->Str()); + + std::string result_iq = + ""; + + client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + + EXPECT_EQ(1, listener->result_count); + EXPECT_EQ(0, listener->error_count); +} + +TEST_F(PubSubTasksTest, TestPublishError) { + buzz::XmlElement* payload = + new buzz::XmlElement(buzz::QName(buzz::NS_PUBSUB, "value")); + + std::vector children; + children.push_back(payload); + buzz::PubSubPublishTask* task = + new buzz::PubSubPublishTask(client, pubsubjid, node, itemid, children); + task->SignalResult.connect( + listener.get(), &TestPubSubTasksListener::OnPublishResult); + task->SignalError.connect( + listener.get(), &TestPubSubTasksListener::OnError); + task->Start(); + + std::string result_iq = + "" + " " + " " + " " + ""; + + client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + + EXPECT_EQ(0, listener->result_count); + EXPECT_EQ(1, listener->error_count); +} + +TEST_F(PubSubTasksTest, TestRetract) { + buzz::PubSubRetractTask* task = + new buzz::PubSubRetractTask(client, pubsubjid, node, itemid); + task->SignalResult.connect( + listener.get(), &TestPubSubTasksListener::OnRetractResult); + task->SignalError.connect( + listener.get(), &TestPubSubTasksListener::OnError); + task->Start(); + + std::string expected_iq = + "" + "" + "" + "" + "" + "" + ""; + + ASSERT_EQ(1U, client->sent_stanzas().size()); + EXPECT_EQ(expected_iq, client->sent_stanzas()[0]->Str()); + + std::string result_iq = + ""; + + client->HandleStanza(buzz::XmlElement::ForStr(result_iq)); + + EXPECT_EQ(1, listener->result_count); + EXPECT_EQ(0, listener->error_count); +} diff --git a/talk/xmpp/receivetask.cc b/talk/xmpp/receivetask.cc new file mode 100644 index 000000000..53fac7e9d --- /dev/null +++ b/talk/xmpp/receivetask.cc @@ -0,0 +1,51 @@ +/* + * libjingle + * Copyright 2011, 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/xmpp/receivetask.h" +#include "talk/xmpp/constants.h" + +namespace buzz { + +bool ReceiveTask::HandleStanza(const XmlElement* stanza) { + if (WantsStanza(stanza)) { + QueueStanza(stanza); + return true; + } + + return false; +} + +int ReceiveTask::ProcessStart() { + const XmlElement* stanza = NextStanza(); + if (stanza == NULL) + return STATE_BLOCKED; + + ReceiveStanza(stanza); + return STATE_START; +} + +} // namespace buzz diff --git a/talk/xmpp/receivetask.h b/talk/xmpp/receivetask.h new file mode 100644 index 000000000..b18e0f0bb --- /dev/null +++ b/talk/xmpp/receivetask.h @@ -0,0 +1,58 @@ +/* + * libjingle + * Copyright 2011, 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. + */ + +#ifndef TALK_XMPP_RECEIVETASK_H_ +#define TALK_XMPP_RECEIVETASK_H_ + +#include "talk/xmpp/xmpptask.h" + +namespace buzz { + +// A base class for receiving stanzas. Override WantsStanza to +// indicate that a stanza should be received and ReceiveStanza to +// process it. Once started, ReceiveStanza will be called for all +// stanzas that return true when passed to WantsStanza. This saves +// you from having to remember how to setup the queueing and the task +// states, etc. +class ReceiveTask : public XmppTask { + public: + explicit ReceiveTask(XmppTaskParentInterface* parent) : + XmppTask(parent, XmppEngine::HL_TYPE) {} + virtual int ProcessStart(); + + protected: + virtual bool HandleStanza(const XmlElement* stanza); + + // Return true if the stanza should be received. + virtual bool WantsStanza(const XmlElement* stanza) = 0; + // Process the received stanza. + virtual void ReceiveStanza(const XmlElement* stanza) = 0; +}; + +} // namespace buzz + +#endif // TALK_XMPP_RECEIVETASK_H_ diff --git a/talk/xmpp/rostermodule.h b/talk/xmpp/rostermodule.h new file mode 100644 index 000000000..eafd59544 --- /dev/null +++ b/talk/xmpp/rostermodule.h @@ -0,0 +1,343 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _rostermodule_h_ +#define _rostermodule_h_ + +#include "talk/xmpp/module.h" + +namespace buzz { + +class XmppRosterModule; + +// The main way you initialize and use the module would be like this: +// XmppRosterModule *roster_module = XmppRosterModule::Create(); +// roster_module->RegisterEngine(engine); +// roster_module->BroadcastPresence(); +// roster_module->RequestRosterUpdate(); + +//! This enum captures the valid values for the show attribute in a presence +//! stanza +enum XmppPresenceShow +{ + XMPP_PRESENCE_CHAT = 0, + XMPP_PRESENCE_DEFAULT = 1, + XMPP_PRESENCE_AWAY = 2, + XMPP_PRESENCE_XA = 3, + XMPP_PRESENCE_DND = 4, +}; + +//! These are the valid subscription states in a roster contact. This +//! represents the combination of the subscription and ask attributes +enum XmppSubscriptionState +{ + XMPP_SUBSCRIPTION_NONE = 0, + XMPP_SUBSCRIPTION_NONE_ASKED = 1, + XMPP_SUBSCRIPTION_TO = 2, + XMPP_SUBSCRIPTION_FROM = 3, + XMPP_SUBSCRIPTION_FROM_ASKED = 4, + XMPP_SUBSCRIPTION_BOTH = 5, +}; + +//! These represent the valid types of presence stanzas for managing +//! subscriptions +enum XmppSubscriptionRequestType +{ + XMPP_REQUEST_SUBSCRIBE = 0, + XMPP_REQUEST_UNSUBSCRIBE = 1, + XMPP_REQUEST_SUBSCRIBED = 2, + XMPP_REQUEST_UNSUBSCRIBED = 3, +}; + +enum XmppPresenceAvailable { + XMPP_PRESENCE_UNAVAILABLE = 0, + XMPP_PRESENCE_AVAILABLE = 1, + XMPP_PRESENCE_ERROR = 2, +}; + +enum XmppPresenceConnectionStatus { + XMPP_CONNECTION_STATUS_UNKNOWN = 0, + XMPP_CONNECTION_STATUS_CONNECTING = 1, + XMPP_CONNECTION_STATUS_CONNECTED = 2, + XMPP_CONNECTION_STATUS_HANGUP = 3, +}; + +//! Presence Information +//! This class stores both presence information for outgoing presence and is +//! returned by methods in XmppRosterModule to represent recieved incoming +//! presence information. When this class is writeable (non-const) then each +//! update to any property will set the inner xml. Setting the raw_xml will +//! rederive all of the other properties. +class XmppPresence { +public: + virtual ~XmppPresence() {} + + //! Create a new Presence + //! This is typically only used when sending a directed presence + static XmppPresence* Create(); + + //! The Jid of for the presence information. + //! Typically this will be a full Jid with resource specified. + virtual const Jid jid() const = 0; + + //! Is the contact available? + virtual XmppPresenceAvailable available() const = 0; + + //! Sets if the user is available or not + virtual XmppReturnStatus set_available(XmppPresenceAvailable available) = 0; + + //! The show value of the presence info + virtual XmppPresenceShow presence_show() const = 0; + + //! Set the presence show value + virtual XmppReturnStatus set_presence_show(XmppPresenceShow show) = 0; + + //! The Priority of the presence info + virtual int priority() const = 0; + + //! Set the priority of the presence + virtual XmppReturnStatus set_priority(int priority) = 0; + + //! The plain text status of the presence info. + //! If there are multiple status because of language, this will either be a + //! status that is not tagged for language or the first available + virtual const std::string status() const = 0; + + //! Sets the status for the presence info. + //! If there is more than one status present already then this will remove + //! them all and replace it with one status element we no specified language + virtual XmppReturnStatus set_status(const std::string& status) = 0; + + //! The connection status + virtual XmppPresenceConnectionStatus connection_status() const = 0; + + //! The focus obfuscated GAIA id + virtual const std::string google_user_id() const = 0; + + //! The nickname in the presence + virtual const std::string nickname() const = 0; + + //! The raw xml of the presence update + virtual const XmlElement* raw_xml() const = 0; + + //! Sets the raw presence stanza for the presence update + //! This will cause all other data items in this structure to be rederived + virtual XmppReturnStatus set_raw_xml(const XmlElement * xml) = 0; +}; + +//! A contact as given by the server +class XmppRosterContact { +public: + virtual ~XmppRosterContact() {} + + //! Create a new roster contact + //! This is typically only used when doing a roster update/add + static XmppRosterContact* Create(); + + //! The jid for the contact. + //! Typically this will be a bare Jid. + virtual const Jid jid() const = 0; + + //! Sets the jid for the roster contact update + virtual XmppReturnStatus set_jid(const Jid& jid) = 0; + + //! The name (nickname) stored for this contact + virtual const std::string name() const = 0; + + //! Sets the name + virtual XmppReturnStatus set_name(const std::string& name) = 0; + + //! The Presence subscription state stored on the server for this contact + //! This is never settable and will be ignored when generating a roster + //! add/update request + virtual XmppSubscriptionState subscription_state() const = 0; + + //! The number of Groups applied to this contact + virtual size_t GetGroupCount() const = 0; + + //! Gets a Group applied to the contact based on index. + //! range + virtual const std::string GetGroup(size_t index) const = 0; + + //! Adds a group to this contact. + //! This will return a bad argument error if the group is already there. + virtual XmppReturnStatus AddGroup(const std::string& group) = 0; + + //! Removes a group from the contact. + //! This will return an error if the group cannot be found in the group list. + virtual XmppReturnStatus RemoveGroup(const std::string& group) = 0; + + //! The raw xml for this roster contact + virtual const XmlElement* raw_xml() const = 0; + + //! Sets the raw presence stanza for the contact update/add + //! This will cause all other data items in this structure to be rederived + virtual XmppReturnStatus set_raw_xml(const XmlElement * xml) = 0; +}; + +//! The XmppRosterHandler is an interface for callbacks from the module +class XmppRosterHandler { +public: + virtual ~XmppRosterHandler() {} + + //! A request for a subscription has come in. + //! Typically, the UI will ask the user if it is okay to let the requester + //! get presence notifications for the user. The response is send back + //! by calling ApproveSubscriber or CancelSubscriber. + virtual void SubscriptionRequest(XmppRosterModule* roster, + const Jid& requesting_jid, + XmppSubscriptionRequestType type, + const XmlElement* raw_xml) = 0; + + //! Some type of presence error has occured + virtual void SubscriptionError(XmppRosterModule* roster, + const Jid& from, + const XmlElement* raw_xml) = 0; + + virtual void RosterError(XmppRosterModule* roster, + const XmlElement* raw_xml) = 0; + + //! New presence information has come in + //! The user is notified with the presence object directly. This info is also + //! added to the store accessable from the engine. + virtual void IncomingPresenceChanged(XmppRosterModule* roster, + const XmppPresence* presence) = 0; + + //! A contact has changed + //! This indicates that the data for a contact may have changed. No + //! contacts have been added or removed. + virtual void ContactChanged(XmppRosterModule* roster, + const XmppRosterContact* old_contact, + size_t index) = 0; + + //! A set of contacts have been added + //! These contacts may have been added in response to the original roster + //! request or due to a "roster push" from the server. + virtual void ContactsAdded(XmppRosterModule* roster, + size_t index, size_t number) = 0; + + //! A contact has been removed + //! This contact has been removed form the list. + virtual void ContactRemoved(XmppRosterModule* roster, + const XmppRosterContact* removed_contact, + size_t index) = 0; + +}; + +//! An XmppModule for handle roster and presence functionality +class XmppRosterModule : public XmppModule { +public: + //! Creates a new XmppRosterModule + static XmppRosterModule * Create(); + virtual ~XmppRosterModule() {} + + //! Sets the roster handler (callbacks) for the module + virtual XmppReturnStatus set_roster_handler(XmppRosterHandler * handler) = 0; + + //! Gets the roster handler for the module + virtual XmppRosterHandler* roster_handler() = 0; + + // USER PRESENCE STATE ------------------------------------------------------- + + //! Gets the aggregate outgoing presence + //! This object is non-const and be edited directly. No update is sent + //! to the server until a Broadcast is sent + virtual XmppPresence* outgoing_presence() = 0; + + //! Broadcasts that the user is available. + //! Nothing with respect to presence is sent until this is called. + virtual XmppReturnStatus BroadcastPresence() = 0; + + //! Sends a directed presence to a Jid + //! Note that the client doesn't store where directed presence notifications + //! have been sent. The server can keep the appropriate state + virtual XmppReturnStatus SendDirectedPresence(const XmppPresence* presence, + const Jid& to_jid) = 0; + + // INCOMING PRESENCE STATUS -------------------------------------------------- + + //! Returns the number of incoming presence data recorded + virtual size_t GetIncomingPresenceCount() = 0; + + //! Returns an incoming presence datum based on index + virtual const XmppPresence* GetIncomingPresence(size_t index) = 0; + + //! Gets the number of presence data for a bare Jid + //! There may be a datum per resource + virtual size_t GetIncomingPresenceForJidCount(const Jid& jid) = 0; + + //! Returns a single presence data for a Jid based on index + virtual const XmppPresence* GetIncomingPresenceForJid(const Jid& jid, + size_t index) = 0; + + // ROSTER MANAGEMENT --------------------------------------------------------- + + //! Requests an update of the roster from the server + //! This must be called to initialize the client side cache of the roster + //! After this is sent the server should keep this module apprised of any + //! changes. + virtual XmppReturnStatus RequestRosterUpdate() = 0; + + //! Returns the number of contacts in the roster + virtual size_t GetRosterContactCount() = 0; + + //! Returns a contact by index + virtual const XmppRosterContact* GetRosterContact(size_t index) = 0; + + //! Finds a contact by Jid + virtual const XmppRosterContact* FindRosterContact(const Jid& jid) = 0; + + //! Send a request to the server to add a contact + //! Note that the contact won't show up in the roster until the server can + //! respond. This happens async when the socket is being serviced + virtual XmppReturnStatus RequestRosterChange( + const XmppRosterContact* contact) = 0; + + //! Request that the server remove a contact + //! The jabber protocol specifies that the server should also cancel any + //! subscriptions when this is done. Like adding, this contact won't be + //! removed until the server responds. + virtual XmppReturnStatus RequestRosterRemove(const Jid& jid) = 0; + + // SUBSCRIPTION MANAGEMENT --------------------------------------------------- + + //! Request a subscription to presence notifications form a Jid + virtual XmppReturnStatus RequestSubscription(const Jid& jid) = 0; + + //! Cancel a subscription to presence notifications from a Jid + virtual XmppReturnStatus CancelSubscription(const Jid& jid) = 0; + + //! Approve a request to deliver presence notifications to a jid + virtual XmppReturnStatus ApproveSubscriber(const Jid& jid) = 0; + + //! Deny or cancel presence notification deliver to a jid + virtual XmppReturnStatus CancelSubscriber(const Jid& jid) = 0; +}; + +} + +#endif diff --git a/talk/xmpp/rostermodule_unittest.cc b/talk/xmpp/rostermodule_unittest.cc new file mode 100644 index 000000000..9273eb5d2 --- /dev/null +++ b/talk/xmpp/rostermodule_unittest.cc @@ -0,0 +1,849 @@ +/* + * libjingle + * Copyright 2004, 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 +#include +#include + +#include "talk/base/gunit.h" +#include "talk/base/scoped_ptr.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/rostermodule.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/util_unittest.h" + +#define TEST_OK(x) EXPECT_EQ((x),XMPP_RETURN_OK) +#define TEST_BADARGUMENT(x) EXPECT_EQ((x),XMPP_RETURN_BADARGUMENT) + + +namespace buzz { + +class RosterModuleTest; + +static void +WriteString(std::ostream& os, const std::string& str) { + os<jid().Str()); + os<<" available:"<available(); + os<<" presence_show:"; + WritePresenceShow(os, presence->presence_show()); + os<<" priority:"<priority(); + os<<" status:"; + WriteString(os, presence->status()); + os<<"]"<raw_xml()->Str(); +} + +static void +WriteContact(std::ostream& os, const XmppRosterContact* contact) { + if (contact == NULL) { + os<<"NULL"; + return; + } + + os<<"[Contact jid:"; + WriteString(os, contact->jid().Str()); + os<<" name:"; + WriteString(os, contact->name()); + os<<" subscription_state:"; + WriteSubscriptionState(os, contact->subscription_state()); + os<<" groups:["; + for(size_t i=0; i < contact->GetGroupCount(); ++i) { + os<<(i==0?"":", "); + WriteString(os, contact->GetGroup(i)); + } + os<<"]]"<raw_xml()->Str(); +} + +//! This session handler saves all calls to a string. These are events and +//! data delivered form the engine to application code. +class XmppTestRosterHandler : public XmppRosterHandler { +public: + XmppTestRosterHandler() {} + virtual ~XmppTestRosterHandler() {} + + virtual void SubscriptionRequest(XmppRosterModule*, + const Jid& requesting_jid, + XmppSubscriptionRequestType type, + const XmlElement* raw_xml) { + ss_<<"[SubscriptionRequest Jid:" << requesting_jid.Str()<<" type:"; + WriteSubscriptionRequestType(ss_, type); + ss_<<"]"<Str(); + } + + //! Some type of presence error has occured + virtual void SubscriptionError(XmppRosterModule*, + const Jid& from, + const XmlElement* raw_xml) { + ss_<<"[SubscriptionError from:"<Str(); + } + + virtual void RosterError(XmppRosterModule*, + const XmlElement* raw_xml) { + ss_<<"[RosterError]"<Str(); + } + + //! New presence information has come in + //! The user is notified with the presence object directly. This info is also + //! added to the store accessable from the engine. + virtual void IncomingPresenceChanged(XmppRosterModule*, + const XmppPresence* presence) { + ss_<<"[IncomingPresenceChanged presence:"; + WritePresence(ss_, presence); + ss_<<"]"; + } + + //! A contact has changed + //! This indicates that the data for a contact may have changed. No + //! contacts have been added or removed. + virtual void ContactChanged(XmppRosterModule* roster, + const XmppRosterContact* old_contact, + size_t index) { + ss_<<"[ContactChanged old_contact:"; + WriteContact(ss_, old_contact); + ss_<<" index:"<GetRosterContact(index)); + ss_<<"]"; + } + + //! A set of contacts have been added + //! These contacts may have been added in response to the original roster + //! request or due to a "roster push" from the server. + virtual void ContactsAdded(XmppRosterModule* roster, + size_t index, size_t number) { + ss_<<"[ContactsAdded index:"<GetRosterContact(index+i)); + } + ss_<<"]"; + } + + //! A contact has been removed + //! This contact has been removed form the list. + virtual void ContactRemoved(XmppRosterModule*, + const XmppRosterContact* removed_contact, + size_t index) { + ss_<<"[ContactRemoved old_contact:"; + WriteContact(ss_, removed_contact); + ss_<<" index:"<AddAttr(QN_STATUS, STR_PSTN_CONFERENCE_STATUS_CONNECTING); + XmlElement presence_xml(QN_PRESENCE); + presence_xml.AddElement(status); + talk_base::scoped_ptr presence(XmppPresence::Create()); + presence->set_raw_xml(&presence_xml); + EXPECT_EQ(presence->connection_status(), XMPP_CONNECTION_STATUS_CONNECTING); +} + +TEST_F(RosterModuleTest, TestOutgoingPresence) { + std::stringstream dump; + + talk_base::scoped_ptr engine(XmppEngine::Create()); + XmppTestHandler handler(engine.get()); + XmppTestRosterHandler roster_handler; + + talk_base::scoped_ptr roster(XmppRosterModule::Create()); + roster->set_roster_handler(&roster_handler); + + // Configure the roster module + roster->RegisterEngine(engine.get()); + + // Set up callbacks + engine->SetOutputHandler(&handler); + engine->AddStanzaHandler(&handler); + engine->SetSessionHandler(&handler); + + // Set up minimal login info + engine->SetUser(Jid("david@my-server")); + // engine->SetPassword("david"); + + // Do the whole login handshake + RunLogin(this, engine.get(), &handler); + EXPECT_EQ("", handler.OutputActivity()); + + // Set some presence and broadcast it + TEST_OK(roster->outgoing_presence()-> + set_available(XMPP_PRESENCE_AVAILABLE)); + TEST_OK(roster->outgoing_presence()->set_priority(-37)); + TEST_OK(roster->outgoing_presence()->set_presence_show(XMPP_PRESENCE_DND)); + TEST_OK(roster->outgoing_presence()-> + set_status("I'm off to the races!<>&")); + TEST_OK(roster->BroadcastPresence()); + + EXPECT_EQ(roster_handler.StrClear(), ""); + EXPECT_EQ(handler.OutputActivity(), + "" + "-37" + "dnd" + "I'm off to the races!<>&" + ""); + EXPECT_EQ(handler.SessionActivity(), ""); + + // Try some more + TEST_OK(roster->outgoing_presence()-> + set_available(XMPP_PRESENCE_UNAVAILABLE)); + TEST_OK(roster->outgoing_presence()->set_priority(0)); + TEST_OK(roster->outgoing_presence()->set_presence_show(XMPP_PRESENCE_XA)); + TEST_OK(roster->outgoing_presence()->set_status("Gone fishin'")); + TEST_OK(roster->BroadcastPresence()); + + EXPECT_EQ(roster_handler.StrClear(), ""); + EXPECT_EQ(handler.OutputActivity(), + "" + "xa" + "Gone fishin'" + ""); + EXPECT_EQ(handler.SessionActivity(), ""); + + // Okay -- we are back on + TEST_OK(roster->outgoing_presence()-> + set_available(XMPP_PRESENCE_AVAILABLE)); + TEST_BADARGUMENT(roster->outgoing_presence()->set_priority(128)); + TEST_OK(roster->outgoing_presence()-> + set_presence_show(XMPP_PRESENCE_DEFAULT)); + TEST_OK(roster->outgoing_presence()->set_status("Cookin' wit gas")); + TEST_OK(roster->BroadcastPresence()); + + EXPECT_EQ(roster_handler.StrClear(), ""); + EXPECT_EQ(handler.OutputActivity(), + "" + "Cookin' wit gas" + ""); + EXPECT_EQ(handler.SessionActivity(), ""); + + // Set it via XML + XmlElement presence_input(QN_PRESENCE); + presence_input.AddAttr(QN_TYPE, "unavailable"); + presence_input.AddElement(new XmlElement(QN_PRIORITY)); + presence_input.AddText("42", 1); + presence_input.AddElement(new XmlElement(QN_STATUS)); + presence_input.AddAttr(QN_XML_LANG, "es", 1); + presence_input.AddText("Hola Amigos!", 1); + presence_input.AddElement(new XmlElement(QN_STATUS)); + presence_input.AddText("Hey there, friend!", 1); + TEST_OK(roster->outgoing_presence()->set_raw_xml(&presence_input)); + TEST_OK(roster->BroadcastPresence()); + + WritePresence(dump, roster->outgoing_presence()); + EXPECT_EQ(dump.str(), + "[Presence jid: available:0 presence_show:[default] " + "priority:42 status:Hey there, friend!]" + "" + "42" + "Hola Amigos!" + "Hey there, friend!" + ""); + dump.str(""); + EXPECT_EQ(roster_handler.StrClear(), ""); + EXPECT_EQ(handler.OutputActivity(), + "" + "42" + "Hola Amigos!" + "Hey there, friend!" + ""); + EXPECT_EQ(handler.SessionActivity(), ""); + + // Construct a directed presence + talk_base::scoped_ptr directed_presence(XmppPresence::Create()); + TEST_OK(directed_presence->set_available(XMPP_PRESENCE_AVAILABLE)); + TEST_OK(directed_presence->set_priority(120)); + TEST_OK(directed_presence->set_status("*very* available")); + TEST_OK(roster->SendDirectedPresence(directed_presence.get(), + Jid("myhoney@honey.net"))); + + EXPECT_EQ(roster_handler.StrClear(), ""); + EXPECT_EQ(handler.OutputActivity(), + "" + "120" + "*very* available" + ""); + EXPECT_EQ(handler.SessionActivity(), ""); +} + +TEST_F(RosterModuleTest, TestIncomingPresence) { + talk_base::scoped_ptr engine(XmppEngine::Create()); + XmppTestHandler handler(engine.get()); + XmppTestRosterHandler roster_handler; + + talk_base::scoped_ptr roster(XmppRosterModule::Create()); + roster->set_roster_handler(&roster_handler); + + // Configure the roster module + roster->RegisterEngine(engine.get()); + + // Set up callbacks + engine->SetOutputHandler(&handler); + engine->AddStanzaHandler(&handler); + engine->SetSessionHandler(&handler); + + // Set up minimal login info + engine->SetUser(Jid("david@my-server")); + // engine->SetPassword("david"); + + // Do the whole login handshake + RunLogin(this, engine.get(), &handler); + EXPECT_EQ("", handler.OutputActivity()); + + // Load up with a bunch of data + std::string input; + input = "" + "" + "-10" + "xa" + "Off bowling" + "" + "" + "20" + "Looking for toes..." + "" + "" + "10" + "Throwing rocks" + ""; + TEST_OK(engine->HandleInput(input.c_str(), input.length())); + + EXPECT_EQ(roster_handler.StrClear(), + "[IncomingPresenceChanged " + "presence:[Presence jid:maude@example.net/studio available:1 " + "presence_show:[default] priority:0 status:]" + "]" + "[IncomingPresenceChanged " + "presence:[Presence jid:walter@example.net/home available:1 " + "presence_show:xa priority:-10 status:Off bowling]" + "" + "-10" + "xa" + "Off bowling" + "]" + "[IncomingPresenceChanged " + "presence:[Presence jid:walter@example.net/alley available:1 " + "presence_show:[default] " + "priority:20 status:Looking for toes...]" + "" + "20" + "Looking for toes..." + "]" + "[IncomingPresenceChanged " + "presence:[Presence jid:donny@example.net/alley available:1 " + "presence_show:[default] priority:10 status:Throwing rocks]" + "" + "10" + "Throwing rocks" + "]"); + EXPECT_EQ(handler.OutputActivity(), ""); + handler.SessionActivity(); // Ignore the session output + + // Now look at the data structure we've built + EXPECT_EQ(roster->GetIncomingPresenceCount(), static_cast(4)); + EXPECT_EQ(roster->GetIncomingPresenceForJidCount(Jid("maude@example.net")), + static_cast(1)); + EXPECT_EQ(roster->GetIncomingPresenceForJidCount(Jid("walter@example.net")), + static_cast(2)); + + const XmppPresence * presence; + presence = roster->GetIncomingPresenceForJid(Jid("walter@example.net"), 1); + + std::stringstream dump; + WritePresence(dump, presence); + EXPECT_EQ(dump.str(), + "[Presence jid:walter@example.net/alley available:1 " + "presence_show:[default] priority:20 status:Looking for toes...]" + "" + "20" + "Looking for toes..." + ""); + dump.str(""); + + // Maude took off... + input = "" + "Stealing my rug back" + "-10" + ""; + TEST_OK(engine->HandleInput(input.c_str(), input.length())); + + EXPECT_EQ(roster_handler.StrClear(), + "[IncomingPresenceChanged " + "presence:[Presence jid:maude@example.net/studio available:0 " + "presence_show:[default] priority:-10 " + "status:Stealing my rug back]" + "" + "Stealing my rug back" + "-10" + "]"); + EXPECT_EQ(handler.OutputActivity(), ""); + handler.SessionActivity(); // Ignore the session output +} + +TEST_F(RosterModuleTest, TestPresenceSubscription) { + talk_base::scoped_ptr engine(XmppEngine::Create()); + XmppTestHandler handler(engine.get()); + XmppTestRosterHandler roster_handler; + + talk_base::scoped_ptr roster(XmppRosterModule::Create()); + roster->set_roster_handler(&roster_handler); + + // Configure the roster module + roster->RegisterEngine(engine.get()); + + // Set up callbacks + engine->SetOutputHandler(&handler); + engine->AddStanzaHandler(&handler); + engine->SetSessionHandler(&handler); + + // Set up minimal login info + engine->SetUser(Jid("david@my-server")); + // engine->SetPassword("david"); + + // Do the whole login handshake + RunLogin(this, engine.get(), &handler); + EXPECT_EQ("", handler.OutputActivity()); + + // Test incoming requests + std::string input; + input = + "" + "" + "" + ""; + TEST_OK(engine->HandleInput(input.c_str(), input.length())); + + EXPECT_EQ(roster_handler.StrClear(), + "[SubscriptionRequest Jid:maude@example.net type:subscribe]" + "" + "[SubscriptionRequest Jid:maude@example.net type:unsubscribe]" + "" + "[SubscriptionRequest Jid:maude@example.net type:subscribed]" + "" + "[SubscriptionRequest Jid:maude@example.net type:unsubscribe]" + ""); + EXPECT_EQ(handler.OutputActivity(), ""); + handler.SessionActivity(); // Ignore the session output + + TEST_OK(roster->RequestSubscription(Jid("maude@example.net"))); + TEST_OK(roster->CancelSubscription(Jid("maude@example.net"))); + TEST_OK(roster->ApproveSubscriber(Jid("maude@example.net"))); + TEST_OK(roster->CancelSubscriber(Jid("maude@example.net"))); + + EXPECT_EQ(roster_handler.StrClear(), ""); + EXPECT_EQ(handler.OutputActivity(), + "" + "" + "" + ""); + EXPECT_EQ(handler.SessionActivity(), ""); +} + +TEST_F(RosterModuleTest, TestRosterReceive) { + talk_base::scoped_ptr engine(XmppEngine::Create()); + XmppTestHandler handler(engine.get()); + XmppTestRosterHandler roster_handler; + + talk_base::scoped_ptr roster(XmppRosterModule::Create()); + roster->set_roster_handler(&roster_handler); + + // Configure the roster module + roster->RegisterEngine(engine.get()); + + // Set up callbacks + engine->SetOutputHandler(&handler); + engine->AddStanzaHandler(&handler); + engine->SetSessionHandler(&handler); + + // Set up minimal login info + engine->SetUser(Jid("david@my-server")); + // engine->SetPassword("david"); + + // Do the whole login handshake + RunLogin(this, engine.get(), &handler); + EXPECT_EQ("", handler.OutputActivity()); + + // Request a roster update + TEST_OK(roster->RequestRosterUpdate()); + + EXPECT_EQ(roster_handler.StrClear(),""); + EXPECT_EQ(handler.OutputActivity(), + "" + "" + ""); + EXPECT_EQ(handler.SessionActivity(), ""); + + // Prime the roster with a starting set + std::string input = + "" + "" + "" + "Business Partners" + "" + "" + "Friends" + "Bowling Team" + "Bowling League" + "" + "" + "Friends" + "Bowling Team" + "Bowling League" + "" + "" + "Business Partners" + "" + "" + "Bowling League" + "" + "" + ""; + + TEST_OK(engine->HandleInput(input.c_str(), input.length())); + + EXPECT_EQ(roster_handler.StrClear(), + "[ContactsAdded index:0 number:5 " + "0:[Contact jid:maude@example.net name:Maude Lebowski " + "subscription_state:none_asked " + "groups:[Business Partners]]" + "" + "Business Partners" + " " + "1:[Contact jid:walter@example.net name:Walter Sobchak " + "subscription_state:both " + "groups:[Friends, Bowling Team, Bowling League]]" + "" + "Friends" + "Bowling Team" + "Bowling League" + " " + "2:[Contact jid:donny@example.net name:Donny " + "subscription_state:both " + "groups:[Friends, Bowling Team, Bowling League]]" + "" + "Friends" + "Bowling Team" + "Bowling League" + " " + "3:[Contact jid:jeffrey@example.net name:The Big Lebowski " + "subscription_state:to " + "groups:[Business Partners]]" + "" + "Business Partners" + " " + "4:[Contact jid:jesus@example.net name:Jesus Quintana " + "subscription_state:from groups:[Bowling League]]" + "" + "Bowling League" + "]"); + EXPECT_EQ(handler.OutputActivity(), ""); + EXPECT_EQ(handler.SessionActivity(), ""); + + // Request that someone be added + talk_base::scoped_ptr contact(XmppRosterContact::Create()); + TEST_OK(contact->set_jid(Jid("brandt@example.net"))); + TEST_OK(contact->set_name("Brandt")); + TEST_OK(contact->AddGroup("Business Partners")); + TEST_OK(contact->AddGroup("Watchers")); + TEST_OK(contact->AddGroup("Friends")); + TEST_OK(contact->RemoveGroup("Friends")); // Maybe not... + TEST_OK(roster->RequestRosterChange(contact.get())); + + EXPECT_EQ(roster_handler.StrClear(), ""); + EXPECT_EQ(handler.OutputActivity(), + "" + "" + "" + "Business Partners" + "Watchers" + "" + "" + ""); + EXPECT_EQ(handler.SessionActivity(), ""); + + // Get the push from the server + input = + "" + "" + "" + "" + "Business Partners" + "Watchers" + "" + "" + ""; + TEST_OK(engine->HandleInput(input.c_str(), input.length())); + + EXPECT_EQ(roster_handler.StrClear(), + "[ContactsAdded index:5 number:1 " + "5:[Contact jid:brandt@example.net name:Brandt " + "subscription_state:none " + "groups:[Business Partners, Watchers]]" + "" + "Business Partners" + "Watchers" + "]"); + EXPECT_EQ(handler.OutputActivity(), + ""); + EXPECT_EQ(handler.SessionActivity(), ""); + + // Get a contact update + input = + "" + "" + "" + "Friends" + "Bowling Team" + "Bowling League" + "Not wrong, just an..." + "" + "" + ""; + + TEST_OK(engine->HandleInput(input.c_str(), input.length())); + + EXPECT_EQ(roster_handler.StrClear(), + "[ContactChanged " + "old_contact:[Contact jid:walter@example.net name:Walter Sobchak " + "subscription_state:both " + "groups:[Friends, Bowling Team, Bowling League]]" + "" + "Friends" + "Bowling Team" + "Bowling League" + " " + "index:1 " + "new_contact:[Contact jid:walter@example.net name:Walter Sobchak " + "subscription_state:both " + "groups:[Friends, Bowling Team, Bowling League, " + "Not wrong, just an...]]" + "" + "Friends" + "Bowling Team" + "Bowling League" + "Not wrong, just an..." + "]"); + EXPECT_EQ(handler.OutputActivity(), + ""); + EXPECT_EQ(handler.SessionActivity(), ""); + + // Remove a contact + TEST_OK(roster->RequestRosterRemove(Jid("jesus@example.net"))); + + EXPECT_EQ(roster_handler.StrClear(), ""); + EXPECT_EQ(handler.OutputActivity(), + "" + "" + ""); + EXPECT_EQ(handler.SessionActivity(), ""); + + // Response from the server + input = + "" + "" + "" + "" + "" + "" + ""; + TEST_OK(engine->HandleInput(input.c_str(), input.length())); + + EXPECT_EQ(roster_handler.StrClear(), + "[ContactRemoved " + "old_contact:[Contact jid:jesus@example.net name:Jesus Quintana " + "subscription_state:from groups:[Bowling League]]" + "" + "Bowling League" + " index:4]"); + EXPECT_EQ(handler.OutputActivity(), + ""); + EXPECT_EQ(handler.SessionActivity(), ""); +} + +} diff --git a/talk/xmpp/rostermoduleimpl.cc b/talk/xmpp/rostermoduleimpl.cc new file mode 100644 index 000000000..242288032 --- /dev/null +++ b/talk/xmpp/rostermoduleimpl.cc @@ -0,0 +1,1080 @@ +/* + * libjingle + * Copyright 2004--2005, 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 +#include +#include +#include +#include +#include +#include "talk/base/common.h" +#include "talk/base/stringencode.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/rostermoduleimpl.h" + +namespace buzz { + +// enum prase and persist helpers ---------------------------------------------- +static bool +StringToPresenceShow(const std::string& input, XmppPresenceShow* show) { + // If this becomes a perf issue we can use a hash or a map here + if (STR_SHOW_AWAY == input) + *show = XMPP_PRESENCE_AWAY; + else if (STR_SHOW_DND == input) + *show = XMPP_PRESENCE_DND; + else if (STR_SHOW_XA == input) + *show = XMPP_PRESENCE_XA; + else if (STR_SHOW_CHAT == input) + *show = XMPP_PRESENCE_CHAT; + else if (STR_EMPTY == input) + *show = XMPP_PRESENCE_DEFAULT; + else + return false; + + return true; +} + +static bool +PresenceShowToString(XmppPresenceShow show, const char** output) { + switch(show) { + case XMPP_PRESENCE_AWAY: + *output = STR_SHOW_AWAY; + return true; + case XMPP_PRESENCE_CHAT: + *output = STR_SHOW_CHAT; + return true; + case XMPP_PRESENCE_XA: + *output = STR_SHOW_XA; + return true; + case XMPP_PRESENCE_DND: + *output = STR_SHOW_DND; + return true; + case XMPP_PRESENCE_DEFAULT: + *output = STR_EMPTY; + return true; + } + + *output = STR_EMPTY; + return false; +} + +static bool +StringToSubscriptionState(const std::string& subscription, + const std::string& ask, + XmppSubscriptionState* state) +{ + if (ask == "subscribe") + { + if (subscription == "none") { + *state = XMPP_SUBSCRIPTION_NONE_ASKED; + return true; + } + if (subscription == "from") { + *state = XMPP_SUBSCRIPTION_FROM_ASKED; + return true; + } + } else if (ask == STR_EMPTY) + { + if (subscription == "none") { + *state = XMPP_SUBSCRIPTION_NONE; + return true; + } + if (subscription == "from") { + *state = XMPP_SUBSCRIPTION_FROM; + return true; + } + if (subscription == "to") { + *state = XMPP_SUBSCRIPTION_TO; + return true; + } + if (subscription == "both") { + *state = XMPP_SUBSCRIPTION_BOTH; + return true; + } + } + + return false; +} + +static bool +StringToSubscriptionRequestType(const std::string& string, + XmppSubscriptionRequestType* type) +{ + if (string == "subscribe") + *type = XMPP_REQUEST_SUBSCRIBE; + else if (string == "unsubscribe") + *type = XMPP_REQUEST_UNSUBSCRIBE; + else if (string == "subscribed") + *type = XMPP_REQUEST_SUBSCRIBED; + else if (string == "unsubscribed") + *type = XMPP_REQUEST_UNSUBSCRIBED; + else + return false; + return true; +} + +// XmppPresenceImpl class ------------------------------------------------------ +XmppPresence* +XmppPresence::Create() { + return new XmppPresenceImpl(); +} + +XmppPresenceImpl::XmppPresenceImpl() { +} + +const Jid +XmppPresenceImpl::jid() const { + if (!raw_xml_) + return Jid(); + + return Jid(raw_xml_->Attr(QN_FROM)); +} + +XmppPresenceAvailable +XmppPresenceImpl::available() const { + if (!raw_xml_) + return XMPP_PRESENCE_UNAVAILABLE; + + if (raw_xml_->Attr(QN_TYPE) == "unavailable") + return XMPP_PRESENCE_UNAVAILABLE; + else if (raw_xml_->Attr(QN_TYPE) == "error") + return XMPP_PRESENCE_ERROR; + else + return XMPP_PRESENCE_AVAILABLE; +} + +XmppReturnStatus +XmppPresenceImpl::set_available(XmppPresenceAvailable available) { + if (!raw_xml_) + CreateRawXmlSkeleton(); + + if (available == XMPP_PRESENCE_AVAILABLE) + raw_xml_->ClearAttr(QN_TYPE); + else if (available == XMPP_PRESENCE_UNAVAILABLE) + raw_xml_->SetAttr(QN_TYPE, "unavailable"); + else if (available == XMPP_PRESENCE_ERROR) + raw_xml_->SetAttr(QN_TYPE, "error"); + return XMPP_RETURN_OK; +} + +XmppPresenceShow +XmppPresenceImpl::presence_show() const { + if (!raw_xml_) + return XMPP_PRESENCE_DEFAULT; + + XmppPresenceShow show = XMPP_PRESENCE_DEFAULT; + StringToPresenceShow(raw_xml_->TextNamed(QN_SHOW), &show); + return show; +} + +XmppReturnStatus +XmppPresenceImpl::set_presence_show(XmppPresenceShow show) { + if (!raw_xml_) + CreateRawXmlSkeleton(); + + const char* show_string; + + if(!PresenceShowToString(show, &show_string)) + return XMPP_RETURN_BADARGUMENT; + + raw_xml_->ClearNamedChildren(QN_SHOW); + + if (show!=XMPP_PRESENCE_DEFAULT) { + raw_xml_->AddElement(new XmlElement(QN_SHOW)); + raw_xml_->AddText(show_string, 1); + } + + return XMPP_RETURN_OK; +} + +int +XmppPresenceImpl::priority() const { + if (!raw_xml_) + return 0; + + int raw_priority = 0; + if (!talk_base::FromString(raw_xml_->TextNamed(QN_PRIORITY), &raw_priority)) + raw_priority = 0; + if (raw_priority < -128) + raw_priority = -128; + if (raw_priority > 127) + raw_priority = 127; + + return raw_priority; +} + +XmppReturnStatus +XmppPresenceImpl::set_priority(int priority) { + if (!raw_xml_) + CreateRawXmlSkeleton(); + + if (priority < -128 || priority > 127) + return XMPP_RETURN_BADARGUMENT; + + raw_xml_->ClearNamedChildren(QN_PRIORITY); + if (0 != priority) { + std::string priority_string; + if (talk_base::ToString(priority, &priority_string)) { + raw_xml_->AddElement(new XmlElement(QN_PRIORITY)); + raw_xml_->AddText(priority_string, 1); + } + } + + return XMPP_RETURN_OK; +} + +const std::string +XmppPresenceImpl::status() const { + if (!raw_xml_) + return STR_EMPTY; + + XmlElement* status_element; + XmlElement* element; + + // Search for a status element with no xml:lang attribute on it. if we can't + // find that then just return the first status element in the stanza. + for (status_element = element = raw_xml_->FirstNamed(QN_STATUS); + element; + element = element->NextNamed(QN_STATUS)) { + if (!element->HasAttr(QN_XML_LANG)) { + status_element = element; + break; + } + } + + if (status_element) { + return status_element->BodyText(); + } + + return STR_EMPTY; +} + +XmppReturnStatus +XmppPresenceImpl::set_status(const std::string& status) { + if (!raw_xml_) + CreateRawXmlSkeleton(); + + raw_xml_->ClearNamedChildren(QN_STATUS); + + if (status != STR_EMPTY) { + raw_xml_->AddElement(new XmlElement(QN_STATUS)); + raw_xml_->AddText(status, 1); + } + + return XMPP_RETURN_OK; +} + +XmppPresenceConnectionStatus +XmppPresenceImpl::connection_status() const { + if (!raw_xml_) + return XMPP_CONNECTION_STATUS_UNKNOWN; + + XmlElement* con = raw_xml_->FirstNamed(QN_GOOGLE_PSTN_CONFERENCE_STATUS); + if (con) { + std::string status = con->Attr(QN_ATTR_STATUS); + if (status == STR_PSTN_CONFERENCE_STATUS_CONNECTING) + return XMPP_CONNECTION_STATUS_CONNECTING; + else if (status == STR_PSTN_CONFERENCE_STATUS_CONNECTED) + return XMPP_CONNECTION_STATUS_CONNECTED; + else if (status == STR_PSTN_CONFERENCE_STATUS_HANGUP) + return XMPP_CONNECTION_STATUS_HANGUP; + } + + return XMPP_CONNECTION_STATUS_CONNECTED; +} + +const std::string +XmppPresenceImpl::google_user_id() const { + if (!raw_xml_) + return std::string(); + + XmlElement* muc_user_x = raw_xml_->FirstNamed(QN_MUC_USER_X); + if (muc_user_x) { + XmlElement* muc_user_item = muc_user_x->FirstNamed(QN_MUC_USER_ITEM); + if (muc_user_item) { + return muc_user_item->Attr(QN_GOOGLE_USER_ID); + } + } + + return std::string(); +} + +const std::string +XmppPresenceImpl::nickname() const { + if (!raw_xml_) + return std::string(); + + XmlElement* nickname = raw_xml_->FirstNamed(QN_NICKNAME); + if (nickname) { + return nickname->BodyText(); + } + + return std::string(); +} + +const XmlElement* +XmppPresenceImpl::raw_xml() const { + if (!raw_xml_) + const_cast(this)->CreateRawXmlSkeleton(); + return raw_xml_.get(); +} + +XmppReturnStatus +XmppPresenceImpl::set_raw_xml(const XmlElement * xml) { + if (!xml || + xml->Name() != QN_PRESENCE) + return XMPP_RETURN_BADARGUMENT; + + raw_xml_.reset(new XmlElement(*xml)); + + return XMPP_RETURN_OK; +} + +void +XmppPresenceImpl::CreateRawXmlSkeleton() { + raw_xml_.reset(new XmlElement(QN_PRESENCE)); +} + +// XmppRosterContactImpl ------------------------------------------------------- +XmppRosterContact* +XmppRosterContact::Create() { + return new XmppRosterContactImpl(); +} + +XmppRosterContactImpl::XmppRosterContactImpl() { + ResetGroupCache(); +} + +void +XmppRosterContactImpl::SetXmlFromWire(const XmlElement* xml) { + ResetGroupCache(); + if (xml) + raw_xml_.reset(new XmlElement(*xml)); + else + raw_xml_.reset(NULL); +} + +void +XmppRosterContactImpl::ResetGroupCache() { + group_count_ = -1; + group_index_returned_ = -1; + group_returned_ = NULL; +} + +const Jid +XmppRosterContactImpl::jid() const { + return Jid(raw_xml_->Attr(QN_JID)); +} + +XmppReturnStatus +XmppRosterContactImpl::set_jid(const Jid& jid) +{ + if (!raw_xml_) + CreateRawXmlSkeleton(); + + if (!jid.IsValid()) + return XMPP_RETURN_BADARGUMENT; + + raw_xml_->SetAttr(QN_JID, jid.Str()); + + return XMPP_RETURN_OK; +} + +const std::string +XmppRosterContactImpl::name() const { + return raw_xml_->Attr(QN_NAME); +} + +XmppReturnStatus +XmppRosterContactImpl::set_name(const std::string& name) { + if (!raw_xml_) + CreateRawXmlSkeleton(); + + if (name == STR_EMPTY) + raw_xml_->ClearAttr(QN_NAME); + else + raw_xml_->SetAttr(QN_NAME, name); + + return XMPP_RETURN_OK; +} + +XmppSubscriptionState +XmppRosterContactImpl::subscription_state() const { + if (!raw_xml_) + return XMPP_SUBSCRIPTION_NONE; + + XmppSubscriptionState state = XMPP_SUBSCRIPTION_NONE; + + if (StringToSubscriptionState(raw_xml_->Attr(QN_SUBSCRIPTION), + raw_xml_->Attr(QN_ASK), + &state)) + return state; + + return XMPP_SUBSCRIPTION_NONE; +} + +size_t +XmppRosterContactImpl::GetGroupCount() const { + if (!raw_xml_) + return 0; + + if (-1 == group_count_) { + XmlElement *group_element = raw_xml_->FirstNamed(QN_ROSTER_GROUP); + int group_count = 0; + while(group_element) { + group_count++; + group_element = group_element->NextNamed(QN_ROSTER_GROUP); + } + + ASSERT(group_count > 0); // protect the cast + XmppRosterContactImpl * me = const_cast(this); + me->group_count_ = group_count; + } + + return group_count_; +} + +const std::string +XmppRosterContactImpl::GetGroup(size_t index) const { + if (index >= GetGroupCount()) + return STR_EMPTY; + + // We cache the last group index and element that we returned. This way + // going through the groups in order is order n and not n^2. This could be + // enhanced if necessary by starting at the cached value if the index asked + // is after the cached one. + if (group_index_returned_ >= 0 && + index == static_cast(group_index_returned_) + 1) + { + XmppRosterContactImpl * me = const_cast(this); + me->group_returned_ = group_returned_->NextNamed(QN_ROSTER_GROUP); + ASSERT(group_returned_ != NULL); + me->group_index_returned_++; + } else if (group_index_returned_ < 0 || + static_cast(group_index_returned_) != index) { + XmlElement * group_element = raw_xml_->FirstNamed(QN_ROSTER_GROUP); + size_t group_index = 0; + while(group_index < index) { + ASSERT(group_element != NULL); + group_index++; + group_element = group_element->NextNamed(QN_ROSTER_GROUP); + } + + XmppRosterContactImpl * me = const_cast(this); + me->group_index_returned_ = static_cast(group_index); + me->group_returned_ = group_element; + } + + return group_returned_->BodyText(); +} + +XmppReturnStatus +XmppRosterContactImpl::AddGroup(const std::string& group) { + if (group == STR_EMPTY) + return XMPP_RETURN_BADARGUMENT; + + if (!raw_xml_) + CreateRawXmlSkeleton(); + + if (FindGroup(group, NULL, NULL)) + return XMPP_RETURN_OK; + + raw_xml_->AddElement(new XmlElement(QN_ROSTER_GROUP)); + raw_xml_->AddText(group, 1); + ++group_count_; + + return XMPP_RETURN_OK; +} + +XmppReturnStatus +XmppRosterContactImpl::RemoveGroup(const std::string& group) { + if (group == STR_EMPTY) + return XMPP_RETURN_BADARGUMENT; + + if (!raw_xml_) + return XMPP_RETURN_OK; + + XmlChild * child_before; + if (FindGroup(group, NULL, &child_before)) { + raw_xml_->RemoveChildAfter(child_before); + ResetGroupCache(); + } + return XMPP_RETURN_OK; +} + +bool +XmppRosterContactImpl::FindGroup(const std::string& group, + XmlElement** element, + XmlChild** child_before) { + XmlChild * prev_child = NULL; + XmlChild * next_child; + XmlChild * child; + for (child = raw_xml_->FirstChild(); child; child = next_child) { + next_child = child->NextChild(); + if (!child->IsText() && + child->AsElement()->Name() == QN_ROSTER_GROUP && + child->AsElement()->BodyText() == group) { + if (element) + *element = child->AsElement(); + if (child_before) + *child_before = prev_child; + return true; + } + prev_child = child; + } + + return false; +} + +const XmlElement* +XmppRosterContactImpl::raw_xml() const { + if (!raw_xml_) + const_cast(this)->CreateRawXmlSkeleton(); + return raw_xml_.get(); +} + +XmppReturnStatus +XmppRosterContactImpl::set_raw_xml(const XmlElement* xml) { + if (!xml || + xml->Name() != QN_ROSTER_ITEM || + xml->HasAttr(QN_SUBSCRIPTION) || + xml->HasAttr(QN_ASK)) + return XMPP_RETURN_BADARGUMENT; + + ResetGroupCache(); + + raw_xml_.reset(new XmlElement(*xml)); + + return XMPP_RETURN_OK; +} + +void +XmppRosterContactImpl::CreateRawXmlSkeleton() { + raw_xml_.reset(new XmlElement(QN_ROSTER_ITEM)); +} + +// XmppRosterModuleImpl -------------------------------------------------------- +XmppRosterModule * +XmppRosterModule::Create() { + return new XmppRosterModuleImpl(); +} + +XmppRosterModuleImpl::XmppRosterModuleImpl() : + roster_handler_(NULL), + incoming_presence_map_(new JidPresenceVectorMap()), + incoming_presence_vector_(new PresenceVector()), + contacts_(new ContactVector()) { + +} + +XmppRosterModuleImpl::~XmppRosterModuleImpl() { + DeleteIncomingPresence(); + DeleteContacts(); +} + +XmppReturnStatus +XmppRosterModuleImpl::set_roster_handler(XmppRosterHandler * handler) { + roster_handler_ = handler; + return XMPP_RETURN_OK; +} + +XmppRosterHandler* +XmppRosterModuleImpl::roster_handler() { + return roster_handler_; +} + +XmppPresence* +XmppRosterModuleImpl::outgoing_presence() { + return &outgoing_presence_; +} + +XmppReturnStatus +XmppRosterModuleImpl::BroadcastPresence() { + // Scrub the outgoing presence + const XmlElement* element = outgoing_presence_.raw_xml(); + + ASSERT(!element->HasAttr(QN_TO) && + !element->HasAttr(QN_FROM) && + (element->Attr(QN_TYPE) == STR_EMPTY || + element->Attr(QN_TYPE) == "unavailable")); + + if (!engine()) + return XMPP_RETURN_BADSTATE; + + return engine()->SendStanza(element); +} + +XmppReturnStatus +XmppRosterModuleImpl::SendDirectedPresence(const XmppPresence* presence, + const Jid& to_jid) { + if (!presence) + return XMPP_RETURN_BADARGUMENT; + + if (!engine()) + return XMPP_RETURN_BADSTATE; + + XmlElement element(*(presence->raw_xml())); + + if (element.Name() != QN_PRESENCE || + element.HasAttr(QN_TO) || + element.HasAttr(QN_FROM)) + return XMPP_RETURN_BADARGUMENT; + + if (element.HasAttr(QN_TYPE)) { + if (element.Attr(QN_TYPE) != STR_EMPTY && + element.Attr(QN_TYPE) != "unavailable") { + return XMPP_RETURN_BADARGUMENT; + } + } + + element.SetAttr(QN_TO, to_jid.Str()); + + return engine()->SendStanza(&element); +} + +size_t +XmppRosterModuleImpl::GetIncomingPresenceCount() { + return incoming_presence_vector_->size(); +} + +const XmppPresence* +XmppRosterModuleImpl::GetIncomingPresence(size_t index) { + if (index >= incoming_presence_vector_->size()) + return NULL; + return (*incoming_presence_vector_)[index]; +} + +size_t +XmppRosterModuleImpl::GetIncomingPresenceForJidCount(const Jid& jid) +{ + // find the vector in the map + JidPresenceVectorMap::iterator pos; + pos = incoming_presence_map_->find(jid); + if (pos == incoming_presence_map_->end()) + return 0; + + ASSERT(pos->second != NULL); + + return pos->second->size(); +} + +const XmppPresence* +XmppRosterModuleImpl::GetIncomingPresenceForJid(const Jid& jid, + size_t index) { + JidPresenceVectorMap::iterator pos; + pos = incoming_presence_map_->find(jid); + if (pos == incoming_presence_map_->end()) + return NULL; + + ASSERT(pos->second != NULL); + + if (index >= pos->second->size()) + return NULL; + + return (*pos->second)[index]; +} + +XmppReturnStatus +XmppRosterModuleImpl::RequestRosterUpdate() { + if (!engine()) + return XMPP_RETURN_BADSTATE; + + XmlElement roster_get(QN_IQ); + roster_get.AddAttr(QN_TYPE, "get"); + roster_get.AddAttr(QN_ID, engine()->NextId()); + roster_get.AddElement(new XmlElement(QN_ROSTER_QUERY, true)); + return engine()->SendIq(&roster_get, this, NULL); +} + +size_t +XmppRosterModuleImpl::GetRosterContactCount() { + return contacts_->size(); +} + +const XmppRosterContact* +XmppRosterModuleImpl::GetRosterContact(size_t index) { + if (index >= contacts_->size()) + return NULL; + return (*contacts_)[index]; +} + +class RosterPredicate { +public: + explicit RosterPredicate(const Jid& jid) : jid_(jid) { + } + + bool operator() (XmppRosterContactImpl *& contact) { + return contact->jid() == jid_; + } + +private: + Jid jid_; +}; + +const XmppRosterContact* +XmppRosterModuleImpl::FindRosterContact(const Jid& jid) { + ContactVector::iterator pos; + + pos = std::find_if(contacts_->begin(), + contacts_->end(), + RosterPredicate(jid)); + if (pos == contacts_->end()) + return NULL; + + return *pos; +} + +XmppReturnStatus +XmppRosterModuleImpl::RequestRosterChange( + const XmppRosterContact* contact) { + if (!contact) + return XMPP_RETURN_BADARGUMENT; + + Jid jid = contact->jid(); + + if (!jid.IsValid()) + return XMPP_RETURN_BADARGUMENT; + + if (!engine()) + return XMPP_RETURN_BADSTATE; + + const XmlElement* contact_xml = contact->raw_xml(); + if (contact_xml->Name() != QN_ROSTER_ITEM || + contact_xml->HasAttr(QN_SUBSCRIPTION) || + contact_xml->HasAttr(QN_ASK)) + return XMPP_RETURN_BADARGUMENT; + + XmlElement roster_add(QN_IQ); + roster_add.AddAttr(QN_TYPE, "set"); + roster_add.AddAttr(QN_ID, engine()->NextId()); + roster_add.AddElement(new XmlElement(QN_ROSTER_QUERY, true)); + roster_add.AddElement(new XmlElement(*contact_xml), 1); + + return engine()->SendIq(&roster_add, this, NULL); +} + +XmppReturnStatus +XmppRosterModuleImpl::RequestRosterRemove(const Jid& jid) { + if (!jid.IsValid()) + return XMPP_RETURN_BADARGUMENT; + + if (!engine()) + return XMPP_RETURN_BADSTATE; + + XmlElement roster_add(QN_IQ); + roster_add.AddAttr(QN_TYPE, "set"); + roster_add.AddAttr(QN_ID, engine()->NextId()); + roster_add.AddElement(new XmlElement(QN_ROSTER_QUERY, true)); + roster_add.AddAttr(QN_JID, jid.Str(), 1); + roster_add.AddAttr(QN_SUBSCRIPTION, "remove", 1); + + return engine()->SendIq(&roster_add, this, NULL); +} + +XmppReturnStatus +XmppRosterModuleImpl::RequestSubscription(const Jid& jid) { + return SendSubscriptionRequest(jid, "subscribe"); +} + +XmppReturnStatus +XmppRosterModuleImpl::CancelSubscription(const Jid& jid) { + return SendSubscriptionRequest(jid, "unsubscribe"); +} + +XmppReturnStatus +XmppRosterModuleImpl::ApproveSubscriber(const Jid& jid) { + return SendSubscriptionRequest(jid, "subscribed"); +} + +XmppReturnStatus +XmppRosterModuleImpl::CancelSubscriber(const Jid& jid) { + return SendSubscriptionRequest(jid, "unsubscribed"); +} + +void +XmppRosterModuleImpl::IqResponse(XmppIqCookie, const XmlElement * stanza) { + // The only real Iq response that we expect to recieve are initial roster + // population + if (stanza->Attr(QN_TYPE) == "error") + { + if (roster_handler_) + roster_handler_->RosterError(this, stanza); + + return; + } + + ASSERT(stanza->Attr(QN_TYPE) == "result"); + + InternalRosterItems(stanza); +} + +bool +XmppRosterModuleImpl::HandleStanza(const XmlElement * stanza) +{ + ASSERT(engine() != NULL); + + // There are two types of stanzas that we care about: presence and roster push + // Iqs + if (stanza->Name() == QN_PRESENCE) { + const std::string& jid_string = stanza->Attr(QN_FROM); + Jid jid(jid_string); + + if (!jid.IsValid()) + return false; // if the Jid isn't valid, don't process + + const std::string& type = stanza->Attr(QN_TYPE); + XmppSubscriptionRequestType request_type; + if (StringToSubscriptionRequestType(type, &request_type)) + InternalSubscriptionRequest(jid, stanza, request_type); + else if (type == "unavailable" || type == STR_EMPTY) + InternalIncomingPresence(jid, stanza); + else if (type == "error") + InternalIncomingPresenceError(jid, stanza); + else + return false; + + return true; + } else if (stanza->Name() == QN_IQ) { + const XmlElement * roster_query = stanza->FirstNamed(QN_ROSTER_QUERY); + if (!roster_query || stanza->Attr(QN_TYPE) != "set") + return false; + + InternalRosterItems(stanza); + + // respond to the IQ + XmlElement result(QN_IQ); + result.AddAttr(QN_TYPE, "result"); + result.AddAttr(QN_TO, stanza->Attr(QN_FROM)); + result.AddAttr(QN_ID, stanza->Attr(QN_ID)); + + engine()->SendStanza(&result); + return true; + } + + return false; +} + +void +XmppRosterModuleImpl::DeleteIncomingPresence() { + // Clear out the vector of all presence notifications + { + PresenceVector::iterator pos; + for (pos = incoming_presence_vector_->begin(); + pos < incoming_presence_vector_->end(); + ++pos) { + XmppPresenceImpl * presence = *pos; + *pos = NULL; + delete presence; + } + incoming_presence_vector_->clear(); + } + + // Clear out all of the small presence vectors per Jid + { + JidPresenceVectorMap::iterator pos; + for (pos = incoming_presence_map_->begin(); + pos != incoming_presence_map_->end(); + ++pos) { + PresenceVector* presence_vector = pos->second; + pos->second = NULL; + delete presence_vector; + } + incoming_presence_map_->clear(); + } +} + +void +XmppRosterModuleImpl::DeleteContacts() { + ContactVector::iterator pos; + for (pos = contacts_->begin(); + pos < contacts_->end(); + ++pos) { + XmppRosterContact* contact = *pos; + *pos = NULL; + delete contact; + } + contacts_->clear(); +} + +XmppReturnStatus +XmppRosterModuleImpl::SendSubscriptionRequest(const Jid& jid, + const std::string& type) { + if (!jid.IsValid()) + return XMPP_RETURN_BADARGUMENT; + + if (!engine()) + return XMPP_RETURN_BADSTATE; + + XmlElement presence_request(QN_PRESENCE); + presence_request.AddAttr(QN_TO, jid.Str()); + presence_request.AddAttr(QN_TYPE, type); + + return engine()->SendStanza(&presence_request); +} + + +void +XmppRosterModuleImpl::InternalSubscriptionRequest(const Jid& jid, + const XmlElement* stanza, + XmppSubscriptionRequestType + request_type) { + if (roster_handler_) + roster_handler_->SubscriptionRequest(this, jid, request_type, stanza); +} + +class PresencePredicate { +public: + explicit PresencePredicate(const Jid& jid) : jid_(jid) { + } + + bool operator() (XmppPresenceImpl *& contact) { + return contact->jid() == jid_; + } + +private: + Jid jid_; +}; + +void +XmppRosterModuleImpl::InternalIncomingPresence(const Jid& jid, + const XmlElement* stanza) { + bool added = false; + Jid bare_jid = jid.BareJid(); + + // First add the presence to the map + JidPresenceVectorMap::iterator pos; + pos = incoming_presence_map_->find(jid.BareJid()); + if (pos == incoming_presence_map_->end()) { + // Insert a new entry into the map. Get the position of this new entry + pos = (incoming_presence_map_->insert( + std::make_pair(bare_jid, new PresenceVector()))).first; + } + + PresenceVector * presence_vector = pos->second; + ASSERT(presence_vector != NULL); + + // Try to find this jid in the bare jid bucket + PresenceVector::iterator presence_pos; + XmppPresenceImpl* presence; + presence_pos = std::find_if(presence_vector->begin(), + presence_vector->end(), + PresencePredicate(jid)); + + // Update/add it to the bucket + if (presence_pos == presence_vector->end()) { + presence = new XmppPresenceImpl(); + if (XMPP_RETURN_OK == presence->set_raw_xml(stanza)) { + added = true; + presence_vector->push_back(presence); + } else { + delete presence; + presence = NULL; + } + } else { + presence = *presence_pos; + presence->set_raw_xml(stanza); + } + + // now add to the comprehensive vector + if (added) + incoming_presence_vector_->push_back(presence); + + // Call back to the user with the changed presence information + if (roster_handler_) + roster_handler_->IncomingPresenceChanged(this, presence); +} + + +void +XmppRosterModuleImpl::InternalIncomingPresenceError(const Jid& jid, + const XmlElement* stanza) { + if (roster_handler_) + roster_handler_->SubscriptionError(this, jid, stanza); +} + +void +XmppRosterModuleImpl::InternalRosterItems(const XmlElement* stanza) { + const XmlElement* result_data = stanza->FirstNamed(QN_ROSTER_QUERY); + if (!result_data) + return; // unknown stuff in result! + + bool all_new = contacts_->empty(); + + for (const XmlElement* roster_item = result_data->FirstNamed(QN_ROSTER_ITEM); + roster_item; + roster_item = roster_item->NextNamed(QN_ROSTER_ITEM)) + { + const std::string& jid_string = roster_item->Attr(QN_JID); + Jid jid(jid_string); + if (!jid.IsValid()) + continue; + + // This algorithm is N^2 on the number of incoming contacts after the + // initial load. There is no way to do this faster without allowing + // duplicates, introducing more data structures or write a custom data + // structure. We'll see if this becomes a perf problem and fix it if it + // does. + ContactVector::iterator pos = contacts_->end(); + + if (!all_new) { + pos = std::find_if(contacts_->begin(), + contacts_->end(), + RosterPredicate(jid)); + } + + if (pos != contacts_->end()) { // Update/remove a current contact + if (roster_item->Attr(QN_SUBSCRIPTION) == "remove") { + XmppRosterContact* contact = *pos; + contacts_->erase(pos); + if (roster_handler_) + roster_handler_->ContactRemoved(this, contact, + std::distance(contacts_->begin(), pos)); + delete contact; + } else { + XmppRosterContact* old_contact = *pos; + *pos = new XmppRosterContactImpl(); + (*pos)->SetXmlFromWire(roster_item); + if (roster_handler_) + roster_handler_->ContactChanged(this, old_contact, + std::distance(contacts_->begin(), pos)); + delete old_contact; + } + } else { // Add a new contact + XmppRosterContactImpl* contact = new XmppRosterContactImpl(); + contact->SetXmlFromWire(roster_item); + contacts_->push_back(contact); + if (roster_handler_ && !all_new) + roster_handler_->ContactsAdded(this, contacts_->size() - 1, 1); + } + } + + // Send a consolidated update if all contacts are new + if (roster_handler_ && all_new) + roster_handler_->ContactsAdded(this, 0, contacts_->size()); +} + +} diff --git a/talk/xmpp/rostermoduleimpl.h b/talk/xmpp/rostermoduleimpl.h new file mode 100644 index 000000000..df6b70f13 --- /dev/null +++ b/talk/xmpp/rostermoduleimpl.h @@ -0,0 +1,302 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _rostermoduleimpl_h_ +#define _rostermoduleimpl_h_ + +#include "talk/xmpp/moduleimpl.h" +#include "talk/xmpp/rostermodule.h" + +namespace buzz { + +//! Presence Information +//! This class stores both presence information for outgoing presence and is +//! returned by methods in XmppRosterModule to represent received incoming +//! presence information. When this class is writeable (non-const) then each +//! update to any property will set the inner xml. Setting the raw_xml will +//! rederive all of the other properties. +class XmppPresenceImpl : public XmppPresence { +public: + virtual ~XmppPresenceImpl() {} + + //! The from Jid of for the presence information. + //! Typically this will be a full Jid with resource specified. For outgoing + //! presence this should remain JID_NULL and will be scrubbed from the + //! stanza when being sent. + virtual const Jid jid() const; + + //! Is the contact available? + virtual XmppPresenceAvailable available() const; + + //! Sets if the user is available or not + virtual XmppReturnStatus set_available(XmppPresenceAvailable available); + + //! The show value of the presence info + virtual XmppPresenceShow presence_show() const; + + //! Set the presence show value + virtual XmppReturnStatus set_presence_show(XmppPresenceShow show); + + //! The Priority of the presence info + virtual int priority() const; + + //! Set the priority of the presence + virtual XmppReturnStatus set_priority(int priority); + + //! The plain text status of the presence info. + //! If there are multiple status because of language, this will either be a + //! status that is not tagged for language or the first available + virtual const std::string status() const; + + //! Sets the status for the presence info. + //! If there is more than one status present already then this will remove + //! them all and replace it with one status element we no specified language + virtual XmppReturnStatus set_status(const std::string& status); + + //! The connection status + virtual XmppPresenceConnectionStatus connection_status() const; + + //! The focus obfuscated GAIA id + virtual const std::string google_user_id() const; + + //! The nickname in the presence + virtual const std::string nickname() const; + + //! The raw xml of the presence update + virtual const XmlElement* raw_xml() const; + + //! Sets the raw presence stanza for the presence update + //! This will cause all other data items in this structure to be rederived + virtual XmppReturnStatus set_raw_xml(const XmlElement * xml); + +private: + XmppPresenceImpl(); + + friend class XmppPresence; + friend class XmppRosterModuleImpl; + + void CreateRawXmlSkeleton(); + + // Store everything in the XML element. If this becomes a perf issue we can + // cache the data. + talk_base::scoped_ptr raw_xml_; +}; + +//! A contact as given by the server +class XmppRosterContactImpl : public XmppRosterContact { +public: + virtual ~XmppRosterContactImpl() {} + + //! The jid for the contact. + //! Typically this will be a bare Jid. + virtual const Jid jid() const; + + //! Sets the jid for the roster contact update + virtual XmppReturnStatus set_jid(const Jid& jid); + + //! The name (nickname) stored for this contact + virtual const std::string name() const; + + //! Sets the name + virtual XmppReturnStatus set_name(const std::string& name); + + //! The Presence subscription state stored on the server for this contact + //! This is never settable and will be ignored when generating a roster + //! add/update request + virtual XmppSubscriptionState subscription_state() const; + + //! The number of Groups applied to this contact + virtual size_t GetGroupCount() const; + + //! Gets a Group applied to the contact based on index. + virtual const std::string GetGroup(size_t index) const; + + //! Adds a group to this contact. + //! This will return a no error if the group is already present. + virtual XmppReturnStatus AddGroup(const std::string& group); + + //! Removes a group from the contact. + //! This will return no error if the group isn't there + virtual XmppReturnStatus RemoveGroup(const std::string& group); + + //! The raw xml for this roster contact + virtual const XmlElement* raw_xml() const; + + //! Sets the raw presence stanza for the presence update + //! This will cause all other data items in this structure to be rederived + virtual XmppReturnStatus set_raw_xml(const XmlElement * xml); + +private: + XmppRosterContactImpl(); + + void CreateRawXmlSkeleton(); + void SetXmlFromWire(const XmlElement * xml); + void ResetGroupCache(); + + bool FindGroup(const std::string& group, + XmlElement** element, + XmlChild** child_before); + + + friend class XmppRosterContact; + friend class XmppRosterModuleImpl; + + int group_count_; + int group_index_returned_; + XmlElement * group_returned_; + talk_base::scoped_ptr raw_xml_; +}; + +//! An XmppModule for handle roster and presence functionality +class XmppRosterModuleImpl : public XmppModuleImpl, + public XmppRosterModule, public XmppIqHandler { +public: + virtual ~XmppRosterModuleImpl(); + + IMPLEMENT_XMPPMODULE + + //! Sets the roster handler (callbacks) for the module + virtual XmppReturnStatus set_roster_handler(XmppRosterHandler * handler); + + //! Gets the roster handler for the module + virtual XmppRosterHandler* roster_handler(); + + // USER PRESENCE STATE ------------------------------------------------------- + + //! Gets the aggregate outgoing presence + //! This object is non-const and be edited directly. No update is sent + //! to the server until a Broadcast is sent + virtual XmppPresence* outgoing_presence(); + + //! Broadcasts that the user is available. + //! Nothing with respect to presence is sent until this is called. + virtual XmppReturnStatus BroadcastPresence(); + + //! Sends a directed presence to a Jid + //! Note that the client doesn't store where directed presence notifications + //! have been sent. The server can keep the appropriate state + virtual XmppReturnStatus SendDirectedPresence(const XmppPresence* presence, + const Jid& to_jid); + + // INCOMING PRESENCE STATUS -------------------------------------------------- + + //! Returns the number of incoming presence data recorded + virtual size_t GetIncomingPresenceCount(); + + //! Returns an incoming presence datum based on index + virtual const XmppPresence* GetIncomingPresence(size_t index); + + //! Gets the number of presence data for a bare Jid + //! There may be a datum per resource + virtual size_t GetIncomingPresenceForJidCount(const Jid& jid); + + //! Returns a single presence data for a Jid based on index + virtual const XmppPresence* GetIncomingPresenceForJid(const Jid& jid, + size_t index); + + // ROSTER MANAGEMENT --------------------------------------------------------- + + //! Requests an update of the roster from the server + //! This must be called to initialize the client side cache of the roster + //! After this is sent the server should keep this module apprised of any + //! changes. + virtual XmppReturnStatus RequestRosterUpdate(); + + //! Returns the number of contacts in the roster + virtual size_t GetRosterContactCount(); + + //! Returns a contact by index + virtual const XmppRosterContact* GetRosterContact(size_t index); + + //! Finds a contact by Jid + virtual const XmppRosterContact* FindRosterContact(const Jid& jid); + + //! Send a request to the server to add a contact + //! Note that the contact won't show up in the roster until the server can + //! respond. This happens async when the socket is being serviced + virtual XmppReturnStatus RequestRosterChange( + const XmppRosterContact* contact); + + //! Request that the server remove a contact + //! The jabber protocol specifies that the server should also cancel any + //! subscriptions when this is done. Like adding, this contact won't be + //! removed until the server responds. + virtual XmppReturnStatus RequestRosterRemove(const Jid& jid); + + // SUBSCRIPTION MANAGEMENT --------------------------------------------------- + + //! Request a subscription to presence notifications form a Jid + virtual XmppReturnStatus RequestSubscription(const Jid& jid); + + //! Cancel a subscription to presence notifications from a Jid + virtual XmppReturnStatus CancelSubscription(const Jid& jid); + + //! Approve a request to deliver presence notifications to a jid + virtual XmppReturnStatus ApproveSubscriber(const Jid& jid); + + //! Deny or cancel presence notification deliver to a jid + virtual XmppReturnStatus CancelSubscriber(const Jid& jid); + + // XmppIqHandler IMPLEMENTATION ---------------------------------------------- + virtual void IqResponse(XmppIqCookie cookie, const XmlElement * stanza); + +protected: + // XmppModuleImpl OVERRIDES -------------------------------------------------- + virtual bool HandleStanza(const XmlElement *); + + // PRIVATE DATA -------------------------------------------------------------- +private: + friend class XmppRosterModule; + XmppRosterModuleImpl(); + + // Helper functions + void DeleteIncomingPresence(); + void DeleteContacts(); + XmppReturnStatus SendSubscriptionRequest(const Jid& jid, + const std::string& type); + void InternalSubscriptionRequest(const Jid& jid, const XmlElement* stanza, + XmppSubscriptionRequestType request_type); + void InternalIncomingPresence(const Jid& jid, const XmlElement* stanza); + void InternalIncomingPresenceError(const Jid& jid, const XmlElement* stanza); + void InternalRosterItems(const XmlElement* stanza); + + // Member data + XmppPresenceImpl outgoing_presence_; + XmppRosterHandler* roster_handler_; + + typedef std::vector PresenceVector; + typedef std::map JidPresenceVectorMap; + talk_base::scoped_ptr incoming_presence_map_; + talk_base::scoped_ptr incoming_presence_vector_; + + typedef std::vector ContactVector; + talk_base::scoped_ptr contacts_; +}; + +} + +#endif diff --git a/talk/xmpp/saslcookiemechanism.h b/talk/xmpp/saslcookiemechanism.h new file mode 100644 index 000000000..ded9e9716 --- /dev/null +++ b/talk/xmpp/saslcookiemechanism.h @@ -0,0 +1,86 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_SASLCOOKIEMECHANISM_H_ +#define TALK_XMPP_SASLCOOKIEMECHANISM_H_ + +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/saslmechanism.h" +#include "talk/xmpp/constants.h" + +namespace buzz { + +class SaslCookieMechanism : public SaslMechanism { + +public: + SaslCookieMechanism(const std::string & mechanism, + const std::string & username, + const std::string & cookie, + const std::string & token_service) + : mechanism_(mechanism), + username_(username), + cookie_(cookie), + token_service_(token_service) {} + + SaslCookieMechanism(const std::string & mechanism, + const std::string & username, + const std::string & cookie) + : mechanism_(mechanism), + username_(username), + cookie_(cookie), + token_service_("") {} + + virtual std::string GetMechanismName() { return mechanism_; } + + virtual XmlElement * StartSaslAuth() { + // send initial request + XmlElement * el = new XmlElement(QN_SASL_AUTH, true); + el->AddAttr(QN_MECHANISM, mechanism_); + if (!token_service_.empty()) { + el->AddAttr(QN_GOOGLE_AUTH_SERVICE, token_service_); + } + + std::string credential; + credential.append("\0", 1); + credential.append(username_); + credential.append("\0", 1); + credential.append(cookie_); + el->AddText(Base64Encode(credential)); + return el; + } + +private: + std::string mechanism_; + std::string username_; + std::string cookie_; + std::string token_service_; +}; + +} + +#endif // TALK_XMPP_SASLCOOKIEMECHANISM_H_ diff --git a/talk/xmpp/saslhandler.h b/talk/xmpp/saslhandler.h new file mode 100644 index 000000000..bead8aadc --- /dev/null +++ b/talk/xmpp/saslhandler.h @@ -0,0 +1,59 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _SASLHANDLER_H_ +#define _SASLHANDLER_H_ + +#include +#include + +namespace buzz { + +class XmlElement; +class SaslMechanism; + +// Creates mechanisms to deal with a given mechanism +class SaslHandler { + +public: + + // Intended to be subclassed + virtual ~SaslHandler() {} + + // Should pick the best method according to this handler + // returns the empty string if none are suitable + virtual std::string ChooseBestSaslMechanism(const std::vector & mechanisms, bool encrypted) = 0; + + // Creates a SaslMechanism for the given mechanism name (you own it + // once you get it). + // If not handled, return NULL. + virtual SaslMechanism * CreateSaslMechanism(const std::string & mechanism) = 0; +}; + +} + +#endif diff --git a/talk/xmpp/saslmechanism.cc b/talk/xmpp/saslmechanism.cc new file mode 100644 index 000000000..2645ac04e --- /dev/null +++ b/talk/xmpp/saslmechanism.cc @@ -0,0 +1,72 @@ +/* + * libjingle + * Copyright 2004--2005, 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/base/base64.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/saslmechanism.h" + +using talk_base::Base64; + +namespace buzz { + +XmlElement * +SaslMechanism::StartSaslAuth() { + return new XmlElement(QN_SASL_AUTH, true); +} + +XmlElement * +SaslMechanism::HandleSaslChallenge(const XmlElement * challenge) { + return new XmlElement(QN_SASL_ABORT, true); +} + +void +SaslMechanism::HandleSaslSuccess(const XmlElement * success) { +} + +void +SaslMechanism::HandleSaslFailure(const XmlElement * failure) { +} + +std::string +SaslMechanism::Base64Encode(const std::string & plain) { + return Base64::Encode(plain); +} + +std::string +SaslMechanism::Base64Decode(const std::string & encoded) { + return Base64::Decode(encoded, Base64::DO_LAX); +} + +std::string +SaslMechanism::Base64EncodeFromArray(const char * plain, size_t length) { + std::string result; + Base64::EncodeFromArray(plain, length, &result); + return result; +} + +} diff --git a/talk/xmpp/saslmechanism.h b/talk/xmpp/saslmechanism.h new file mode 100644 index 000000000..f2e5adce4 --- /dev/null +++ b/talk/xmpp/saslmechanism.h @@ -0,0 +1,74 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _SASLMECHANISM_H_ +#define _SASLMECHANISM_H_ + +#include + +namespace buzz { + +class XmlElement; + + +// Defines a mechnanism to do SASL authentication. +// Subclass instances should have a self-contained way to present +// credentials. +class SaslMechanism { + +public: + + // Intended to be subclassed + virtual ~SaslMechanism() {} + + // Should return the name of the SASL mechanism, e.g., "PLAIN" + virtual std::string GetMechanismName() = 0; + + // Should generate the initial "auth" request. Default is just . + virtual XmlElement * StartSaslAuth(); + + // Should respond to a SASL "" request. Default is + // to abort (for mechanisms that do not do challenge-response) + virtual XmlElement * HandleSaslChallenge(const XmlElement * challenge); + + // Notification of a SASL "". Sometimes information + // is passed on success. + virtual void HandleSaslSuccess(const XmlElement * success); + + // Notification of a SASL "". Sometimes information + // for the user is passed on failure. + virtual void HandleSaslFailure(const XmlElement * failure); + +protected: + static std::string Base64Encode(const std::string & plain); + static std::string Base64Decode(const std::string & encoded); + static std::string Base64EncodeFromArray(const char * plain, size_t length); +}; + +} + +#endif diff --git a/talk/xmpp/saslplainmechanism.h b/talk/xmpp/saslplainmechanism.h new file mode 100644 index 000000000..f0793b402 --- /dev/null +++ b/talk/xmpp/saslplainmechanism.h @@ -0,0 +1,65 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_SASLPLAINMECHANISM_H_ +#define TALK_XMPP_SASLPLAINMECHANISM_H_ + +#include "talk/base/cryptstring.h" +#include "talk/xmpp/saslmechanism.h" + +namespace buzz { + +class SaslPlainMechanism : public SaslMechanism { + +public: + SaslPlainMechanism(const buzz::Jid user_jid, const talk_base::CryptString & password) : + user_jid_(user_jid), password_(password) {} + + virtual std::string GetMechanismName() { return "PLAIN"; } + + virtual XmlElement * StartSaslAuth() { + // send initial request + XmlElement * el = new XmlElement(QN_SASL_AUTH, true); + el->AddAttr(QN_MECHANISM, "PLAIN"); + + talk_base::FormatCryptString credential; + credential.Append("\0", 1); + credential.Append(user_jid_.node()); + credential.Append("\0", 1); + credential.Append(&password_); + el->AddText(Base64EncodeFromArray(credential.GetData(), credential.GetLength())); + return el; + } + +private: + Jid user_jid_; + talk_base::CryptString password_; +}; + +} + +#endif // TALK_XMPP_SASLPLAINMECHANISM_H_ diff --git a/talk/xmpp/util_unittest.cc b/talk/xmpp/util_unittest.cc new file mode 100644 index 000000000..3d13007e1 --- /dev/null +++ b/talk/xmpp/util_unittest.cc @@ -0,0 +1,102 @@ +// Copyright 2004 Google, Inc. All Rights Reserved. +// Author: Joe Beda + +#include +#include +#include +#include "talk/base/gunit.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/util_unittest.h" + +namespace buzz { + +void XmppTestHandler::WriteOutput(const char * bytes, size_t len) { + output_ << std::string(bytes, len); +} + +void XmppTestHandler::StartTls(const std::string & cname) { + output_ << "[START-TLS " << cname << "]"; +} + +void XmppTestHandler::CloseConnection() { + output_ << "[CLOSED]"; +} + +void XmppTestHandler::OnStateChange(int state) { + switch (static_cast(state)) { + case XmppEngine::STATE_START: + session_ << "[START]"; + break; + case XmppEngine::STATE_OPENING: + session_ << "[OPENING]"; + break; + case XmppEngine::STATE_OPEN: + session_ << "[OPEN]"; + break; + case XmppEngine::STATE_CLOSED: + session_ << "[CLOSED]"; + switch (engine_->GetError(NULL)) { + case XmppEngine::ERROR_NONE: + // do nothing + break; + case XmppEngine::ERROR_XML: + session_ << "[ERROR-XML]"; + break; + case XmppEngine::ERROR_STREAM: + session_ << "[ERROR-STREAM]"; + break; + case XmppEngine::ERROR_VERSION: + session_ << "[ERROR-VERSION]"; + break; + case XmppEngine::ERROR_UNAUTHORIZED: + session_ << "[ERROR-UNAUTHORIZED]"; + break; + case XmppEngine::ERROR_TLS: + session_ << "[ERROR-TLS]"; + break; + case XmppEngine::ERROR_AUTH: + session_ << "[ERROR-AUTH]"; + break; + case XmppEngine::ERROR_BIND: + session_ << "[ERROR-BIND]"; + break; + case XmppEngine::ERROR_CONNECTION_CLOSED: + session_ << "[ERROR-CONNECTION-CLOSED]"; + break; + case XmppEngine::ERROR_DOCUMENT_CLOSED: + session_ << "[ERROR-DOCUMENT-CLOSED]"; + break; + default: + break; + } + break; + default: + break; + } +} + +bool XmppTestHandler::HandleStanza(const XmlElement * stanza) { + stanza_ << stanza->Str(); + return true; +} + +std::string XmppTestHandler::OutputActivity() { + std::string result = output_.str(); + output_.str(""); + return result; +} + +std::string XmppTestHandler::SessionActivity() { + std::string result = session_.str(); + session_.str(""); + return result; +} + +std::string XmppTestHandler::StanzaActivity() { + std::string result = stanza_.str(); + stanza_.str(""); + return result; +} + +} // namespace buzz diff --git a/talk/xmpp/util_unittest.h b/talk/xmpp/util_unittest.h new file mode 100644 index 000000000..bb0656c12 --- /dev/null +++ b/talk/xmpp/util_unittest.h @@ -0,0 +1,75 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_UTIL_UNITTEST_H_ +#define TALK_XMPP_UTIL_UNITTEST_H_ + +#include +#include +#include "talk/xmpp/xmppengine.h" + +namespace buzz { + +// This class captures callbacks from engine. +class XmppTestHandler : public XmppOutputHandler, public XmppSessionHandler, + public XmppStanzaHandler { + public: + explicit XmppTestHandler(XmppEngine* engine) : engine_(engine) {} + virtual ~XmppTestHandler() {} + + void SetEngine(XmppEngine* engine); + + // Output handler + virtual void WriteOutput(const char * bytes, size_t len); + virtual void StartTls(const std::string & cname); + virtual void CloseConnection(); + + // Session handler + virtual void OnStateChange(int state); + + // Stanza handler + virtual bool HandleStanza(const XmlElement* stanza); + + std::string OutputActivity(); + std::string SessionActivity(); + std::string StanzaActivity(); + + private: + XmppEngine* engine_; + std::stringstream output_; + std::stringstream session_; + std::stringstream stanza_; +}; + +} // namespace buzz + +inline std::ostream& operator<<(std::ostream& os, const buzz::Jid& jid) { + os << jid.Str(); + return os; +} + +#endif // TALK_XMPP_UTIL_UNITTEST_H_ diff --git a/talk/xmpp/xmppauth.cc b/talk/xmpp/xmppauth.cc new file mode 100644 index 000000000..efda96741 --- /dev/null +++ b/talk/xmpp/xmppauth.cc @@ -0,0 +1,105 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmpp/xmppauth.h" + +#include + +#include "talk/xmpp/constants.h" +#include "talk/xmpp/saslcookiemechanism.h" +#include "talk/xmpp/saslplainmechanism.h" + +XmppAuth::XmppAuth() : done_(false) { +} + +XmppAuth::~XmppAuth() { +} + +void XmppAuth::StartPreXmppAuth(const buzz::Jid& jid, + const talk_base::SocketAddress& server, + const talk_base::CryptString& pass, + const std::string& auth_mechanism, + const std::string& auth_token) { + jid_ = jid; + passwd_ = pass; + auth_mechanism_ = auth_mechanism; + auth_token_ = auth_token; + done_ = true; + + SignalAuthDone(); +} + +static bool contains(const std::vector& strings, + const std::string& string) { + return std::find(strings.begin(), strings.end(), string) != strings.end(); +} + +std::string XmppAuth::ChooseBestSaslMechanism( + const std::vector& mechanisms, + bool encrypted) { + // First try Oauth2. + if (GetAuthMechanism() == buzz::AUTH_MECHANISM_OAUTH2 && + contains(mechanisms, buzz::AUTH_MECHANISM_OAUTH2)) { + return buzz::AUTH_MECHANISM_OAUTH2; + } + + // A token is the weakest auth - 15s, service-limited, so prefer it. + if (GetAuthMechanism() == buzz::AUTH_MECHANISM_GOOGLE_TOKEN && + contains(mechanisms, buzz::AUTH_MECHANISM_GOOGLE_TOKEN)) { + return buzz::AUTH_MECHANISM_GOOGLE_TOKEN; + } + + // A cookie is the next weakest - 14 days. + if (GetAuthMechanism() == buzz::AUTH_MECHANISM_GOOGLE_COOKIE && + contains(mechanisms, buzz::AUTH_MECHANISM_GOOGLE_COOKIE)) { + return buzz::AUTH_MECHANISM_GOOGLE_COOKIE; + } + + // As a last resort, use plain authentication. + if (contains(mechanisms, buzz::AUTH_MECHANISM_PLAIN)) { + return buzz::AUTH_MECHANISM_PLAIN; + } + + // No good mechanism found + return ""; +} + +buzz::SaslMechanism* XmppAuth::CreateSaslMechanism( + const std::string& mechanism) { + if (mechanism == buzz::AUTH_MECHANISM_OAUTH2) { + return new buzz::SaslCookieMechanism( + mechanism, jid_.Str(), auth_token_, "oauth2"); + } else if (mechanism == buzz::AUTH_MECHANISM_GOOGLE_TOKEN) { + return new buzz::SaslCookieMechanism(mechanism, jid_.Str(), auth_token_); + // } else if (mechanism == buzz::AUTH_MECHANISM_GOOGLE_COOKIE) { + // return new buzz::SaslCookieMechanism(mechanism, jid.Str(), sid_); + } else if (mechanism == buzz::AUTH_MECHANISM_PLAIN) { + return new buzz::SaslPlainMechanism(jid_, passwd_); + } else { + return NULL; + } +} diff --git a/talk/xmpp/xmppauth.h b/talk/xmpp/xmppauth.h new file mode 100644 index 000000000..5dd696382 --- /dev/null +++ b/talk/xmpp/xmppauth.h @@ -0,0 +1,78 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_XMPPAUTH_H_ +#define TALK_XMPP_XMPPAUTH_H_ + +#include + +#include "talk/base/cryptstring.h" +#include "talk/base/sigslot.h" +#include "talk/xmpp/jid.h" +#include "talk/xmpp/saslhandler.h" +#include "talk/xmpp/prexmppauth.h" + +class XmppAuth: public buzz::PreXmppAuth { +public: + XmppAuth(); + virtual ~XmppAuth(); + + // TODO: Just have one "secret" that is either pass or + // token? + virtual void StartPreXmppAuth(const buzz::Jid& jid, + const talk_base::SocketAddress& server, + const talk_base::CryptString& pass, + const std::string& auth_mechanism, + const std::string& auth_token); + + virtual bool IsAuthDone() const { return done_; } + virtual bool IsAuthorized() const { return true; } + virtual bool HadError() const { return false; } + virtual int GetError() const { return 0; } + virtual buzz::CaptchaChallenge GetCaptchaChallenge() const { + return buzz::CaptchaChallenge(); + } + virtual std::string GetAuthMechanism() const { return auth_mechanism_; } + virtual std::string GetAuthToken() const { return auth_token_; } + + virtual std::string ChooseBestSaslMechanism( + const std::vector& mechanisms, + bool encrypted); + + virtual buzz::SaslMechanism * CreateSaslMechanism( + const std::string& mechanism); + +private: + buzz::Jid jid_; + talk_base::CryptString passwd_; + std::string auth_mechanism_; + std::string auth_token_; + bool done_; +}; + +#endif // TALK_XMPP_XMPPAUTH_H_ + diff --git a/talk/xmpp/xmppclient.cc b/talk/xmpp/xmppclient.cc new file mode 100644 index 000000000..f7d7cf21b --- /dev/null +++ b/talk/xmpp/xmppclient.cc @@ -0,0 +1,440 @@ +/* + * libjingle + * Copyright 2004--2005, 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 "xmppclient.h" +#include "xmpptask.h" +#include "talk/base/logging.h" +#include "talk/base/sigslot.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/stringutils.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/saslplainmechanism.h" +#include "talk/xmpp/prexmppauth.h" +#include "talk/xmpp/plainsaslhandler.h" + +namespace buzz { + +class XmppClient::Private : + public sigslot::has_slots<>, + public XmppSessionHandler, + public XmppOutputHandler { +public: + + explicit Private(XmppClient* client) : + client_(client), + socket_(NULL), + engine_(NULL), + proxy_port_(0), + pre_engine_error_(XmppEngine::ERROR_NONE), + pre_engine_subcode_(0), + signal_closed_(false), + allow_plain_(false) {} + + virtual ~Private() { + // We need to disconnect from socket_ before engine_ is destructed (by + // the auto-generated destructor code). + ResetSocket(); + } + + // the owner + XmppClient* const client_; + + // the two main objects + talk_base::scoped_ptr socket_; + talk_base::scoped_ptr engine_; + talk_base::scoped_ptr pre_auth_; + talk_base::CryptString pass_; + std::string auth_mechanism_; + std::string auth_token_; + talk_base::SocketAddress server_; + std::string proxy_host_; + int proxy_port_; + XmppEngine::Error pre_engine_error_; + int pre_engine_subcode_; + CaptchaChallenge captcha_challenge_; + bool signal_closed_; + bool allow_plain_; + + void ResetSocket() { + if (socket_) { + socket_->SignalConnected.disconnect(this); + socket_->SignalRead.disconnect(this); + socket_->SignalClosed.disconnect(this); + socket_.reset(NULL); + } + } + + // implementations of interfaces + void OnStateChange(int state); + void WriteOutput(const char* bytes, size_t len); + void StartTls(const std::string& domainname); + void CloseConnection(); + + // slots for socket signals + void OnSocketConnected(); + void OnSocketRead(); + void OnSocketClosed(); +}; + +bool IsTestServer(const std::string& server_name, + const std::string& test_server_domain) { + return (!test_server_domain.empty() && + talk_base::ends_with(server_name.c_str(), + test_server_domain.c_str())); +} + +XmppReturnStatus XmppClient::Connect( + const XmppClientSettings& settings, + const std::string& lang, AsyncSocket* socket, PreXmppAuth* pre_auth) { + if (socket == NULL) + return XMPP_RETURN_BADARGUMENT; + if (d_->socket_) + return XMPP_RETURN_BADSTATE; + + d_->socket_.reset(socket); + + d_->socket_->SignalConnected.connect(d_.get(), &Private::OnSocketConnected); + d_->socket_->SignalRead.connect(d_.get(), &Private::OnSocketRead); + d_->socket_->SignalClosed.connect(d_.get(), &Private::OnSocketClosed); + + d_->engine_.reset(XmppEngine::Create()); + d_->engine_->SetSessionHandler(d_.get()); + d_->engine_->SetOutputHandler(d_.get()); + if (!settings.resource().empty()) { + d_->engine_->SetRequestedResource(settings.resource()); + } + d_->engine_->SetTls(settings.use_tls()); + + // The talk.google.com server returns a certificate with common-name: + // CN="gmail.com" for @gmail.com accounts, + // CN="googlemail.com" for @googlemail.com accounts, + // CN="talk.google.com" for other accounts (such as @example.com), + // so we tweak the tls server setting for those other accounts to match the + // returned certificate CN of "talk.google.com". + // For other servers, we leave the strings empty, which causes the jid's + // domain to be used. We do the same for gmail.com and googlemail.com as the + // returned CN matches the account domain in those cases. + std::string server_name = settings.server().HostAsURIString(); + if (server_name == buzz::STR_TALK_GOOGLE_COM || + server_name == buzz::STR_TALKX_L_GOOGLE_COM || + server_name == buzz::STR_XMPP_GOOGLE_COM || + server_name == buzz::STR_XMPPX_L_GOOGLE_COM || + IsTestServer(server_name, settings.test_server_domain())) { + if (settings.host() != STR_GMAIL_COM && + settings.host() != STR_GOOGLEMAIL_COM) { + d_->engine_->SetTlsServer("", STR_TALK_GOOGLE_COM); + } + } + + // Set language + d_->engine_->SetLanguage(lang); + + d_->engine_->SetUser(buzz::Jid(settings.user(), settings.host(), STR_EMPTY)); + + d_->pass_ = settings.pass(); + d_->auth_mechanism_ = settings.auth_mechanism(); + d_->auth_token_ = settings.auth_token(); + d_->server_ = settings.server(); + d_->proxy_host_ = settings.proxy_host(); + d_->proxy_port_ = settings.proxy_port(); + d_->allow_plain_ = settings.allow_plain(); + d_->pre_auth_.reset(pre_auth); + + return XMPP_RETURN_OK; +} + +XmppEngine::State XmppClient::GetState() const { + if (!d_->engine_) + return XmppEngine::STATE_NONE; + return d_->engine_->GetState(); +} + +XmppEngine::Error XmppClient::GetError(int* subcode) { + if (subcode) { + *subcode = 0; + } + if (!d_->engine_) + return XmppEngine::ERROR_NONE; + if (d_->pre_engine_error_ != XmppEngine::ERROR_NONE) { + if (subcode) { + *subcode = d_->pre_engine_subcode_; + } + return d_->pre_engine_error_; + } + return d_->engine_->GetError(subcode); +} + +const XmlElement* XmppClient::GetStreamError() { + if (!d_->engine_) { + return NULL; + } + return d_->engine_->GetStreamError(); +} + +CaptchaChallenge XmppClient::GetCaptchaChallenge() { + if (!d_->engine_) + return CaptchaChallenge(); + return d_->captcha_challenge_; +} + +std::string XmppClient::GetAuthMechanism() { + if (!d_->engine_) + return ""; + return d_->auth_mechanism_; +} + +std::string XmppClient::GetAuthToken() { + if (!d_->engine_) + return ""; + return d_->auth_token_; +} + +int XmppClient::ProcessStart() { + // Should not happen, but was observed in crash reports + if (!d_->socket_) { + LOG(LS_ERROR) << "socket_ already reset"; + return STATE_DONE; + } + + if (d_->pre_auth_) { + d_->pre_auth_->SignalAuthDone.connect(this, &XmppClient::OnAuthDone); + d_->pre_auth_->StartPreXmppAuth( + d_->engine_->GetUser(), d_->server_, d_->pass_, + d_->auth_mechanism_, d_->auth_token_); + d_->pass_.Clear(); // done with this; + return STATE_PRE_XMPP_LOGIN; + } + else { + d_->engine_->SetSaslHandler(new PlainSaslHandler( + d_->engine_->GetUser(), d_->pass_, d_->allow_plain_)); + d_->pass_.Clear(); // done with this; + return STATE_START_XMPP_LOGIN; + } +} + +void XmppClient::OnAuthDone() { + Wake(); +} + +int XmppClient::ProcessTokenLogin() { + // Should not happen, but was observed in crash reports + if (!d_->socket_) { + LOG(LS_ERROR) << "socket_ already reset"; + return STATE_DONE; + } + + // Don't know how this could happen, but crash reports show it as NULL + if (!d_->pre_auth_) { + d_->pre_engine_error_ = XmppEngine::ERROR_AUTH; + EnsureClosed(); + return STATE_ERROR; + } + + // Wait until pre authentication is done is done + if (!d_->pre_auth_->IsAuthDone()) + return STATE_BLOCKED; + + if (!d_->pre_auth_->IsAuthorized()) { + // maybe split out a case when gaia is down? + if (d_->pre_auth_->HadError()) { + d_->pre_engine_error_ = XmppEngine::ERROR_AUTH; + d_->pre_engine_subcode_ = d_->pre_auth_->GetError(); + } + else { + d_->pre_engine_error_ = XmppEngine::ERROR_UNAUTHORIZED; + d_->pre_engine_subcode_ = 0; + d_->captcha_challenge_ = d_->pre_auth_->GetCaptchaChallenge(); + } + d_->pre_auth_.reset(NULL); // done with this + EnsureClosed(); + return STATE_ERROR; + } + + // Save auth token as a result + + d_->auth_mechanism_ = d_->pre_auth_->GetAuthMechanism(); + d_->auth_token_ = d_->pre_auth_->GetAuthToken(); + + // transfer ownership of pre_auth_ to engine + d_->engine_->SetSaslHandler(d_->pre_auth_.release()); + return STATE_START_XMPP_LOGIN; +} + +int XmppClient::ProcessStartXmppLogin() { + // Should not happen, but was observed in crash reports + if (!d_->socket_) { + LOG(LS_ERROR) << "socket_ already reset"; + return STATE_DONE; + } + + // Done with pre-connect tasks - connect! + if (!d_->socket_->Connect(d_->server_)) { + EnsureClosed(); + return STATE_ERROR; + } + + return STATE_RESPONSE; +} + +int XmppClient::ProcessResponse() { + // Hang around while we are connected. + if (!delivering_signal_ && + (!d_->engine_ || d_->engine_->GetState() == XmppEngine::STATE_CLOSED)) + return STATE_DONE; + return STATE_BLOCKED; +} + +XmppReturnStatus XmppClient::Disconnect() { + if (!d_->socket_) + return XMPP_RETURN_BADSTATE; + Abort(); + d_->engine_->Disconnect(); + d_->ResetSocket(); + return XMPP_RETURN_OK; +} + +XmppClient::XmppClient(TaskParent* parent) + : XmppTaskParentInterface(parent), + delivering_signal_(false), + valid_(false) { + d_.reset(new Private(this)); + valid_ = true; +} + +XmppClient::~XmppClient() { + valid_ = false; +} + +const Jid& XmppClient::jid() const { + return d_->engine_->FullJid(); +} + + +std::string XmppClient::NextId() { + return d_->engine_->NextId(); +} + +XmppReturnStatus XmppClient::SendStanza(const XmlElement* stanza) { + return d_->engine_->SendStanza(stanza); +} + +XmppReturnStatus XmppClient::SendStanzaError( + const XmlElement* old_stanza, XmppStanzaError xse, + const std::string& message) { + return d_->engine_->SendStanzaError(old_stanza, xse, message); +} + +XmppReturnStatus XmppClient::SendRaw(const std::string& text) { + return d_->engine_->SendRaw(text); +} + +XmppEngine* XmppClient::engine() { + return d_->engine_.get(); +} + +void XmppClient::Private::OnSocketConnected() { + engine_->Connect(); +} + +void XmppClient::Private::OnSocketRead() { + char bytes[4096]; + size_t bytes_read; + for (;;) { + // Should not happen, but was observed in crash reports + if (!socket_) { + LOG(LS_ERROR) << "socket_ already reset"; + return; + } + + if (!socket_->Read(bytes, sizeof(bytes), &bytes_read)) { + // TODO: deal with error information + return; + } + + if (bytes_read == 0) + return; + +//#ifdef _DEBUG + client_->SignalLogInput(bytes, bytes_read); +//#endif + + engine_->HandleInput(bytes, bytes_read); + } +} + +void XmppClient::Private::OnSocketClosed() { + int code = socket_->GetError(); + engine_->ConnectionClosed(code); +} + +void XmppClient::Private::OnStateChange(int state) { + if (state == XmppEngine::STATE_CLOSED) { + client_->EnsureClosed(); + } + else { + client_->SignalStateChange((XmppEngine::State)state); + } + client_->Wake(); +} + +void XmppClient::Private::WriteOutput(const char* bytes, size_t len) { +//#ifdef _DEBUG + client_->SignalLogOutput(bytes, len); +//#endif + + socket_->Write(bytes, len); + // TODO: deal with error information +} + +void XmppClient::Private::StartTls(const std::string& domain) { +#if defined(FEATURE_ENABLE_SSL) + socket_->StartTls(domain); +#endif +} + +void XmppClient::Private::CloseConnection() { + socket_->Close(); +} + +void XmppClient::AddXmppTask(XmppTask* task, XmppEngine::HandlerLevel level) { + d_->engine_->AddStanzaHandler(task, level); +} + +void XmppClient::RemoveXmppTask(XmppTask* task) { + d_->engine_->RemoveStanzaHandler(task); +} + +void XmppClient::EnsureClosed() { + if (!d_->signal_closed_) { + d_->signal_closed_ = true; + delivering_signal_ = true; + SignalStateChange(XmppEngine::STATE_CLOSED); + delivering_signal_ = false; + } +} + +} // namespace buzz diff --git a/talk/xmpp/xmppclient.h b/talk/xmpp/xmppclient.h new file mode 100644 index 000000000..c8dd91edf --- /dev/null +++ b/talk/xmpp/xmppclient.h @@ -0,0 +1,165 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_XMPPCLIENT_H_ +#define TALK_XMPP_XMPPCLIENT_H_ + +#include +#include "talk/base/basicdefs.h" +#include "talk/base/sigslot.h" +#include "talk/base/task.h" +#include "talk/xmpp/asyncsocket.h" +#include "talk/xmpp/xmppclientsettings.h" +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmpptask.h" + +namespace buzz { + +class PreXmppAuth; +class CaptchaChallenge; + +// Just some non-colliding number. Could have picked "1". +#define XMPP_CLIENT_TASK_CODE 0x366c1e47 + +///////////////////////////////////////////////////////////////////// +// +// XMPPCLIENT +// +///////////////////////////////////////////////////////////////////// +// +// See Task first. XmppClient is a parent task for XmppTasks. +// +// XmppClient is a task which is designed to be the parent task for +// all tasks that depend on a single Xmpp connection. If you want to, +// for example, listen for subscription requests forever, then your +// listener should be a task that is a child of the XmppClient that owns +// the connection you are using. XmppClient has all the utility methods +// that basically drill through to XmppEngine. +// +// XmppClient is just a wrapper for XmppEngine, and if I were writing it +// all over again, I would make XmppClient == XmppEngine. Why? +// XmppEngine needs tasks too, for example it has an XmppLoginTask which +// should just be the same kind of Task instead of an XmppEngine specific +// thing. It would help do certain things like GAIA auth cleaner. +// +///////////////////////////////////////////////////////////////////// + +class XmppClient : public XmppTaskParentInterface, + public XmppClientInterface, + public sigslot::has_slots<> +{ +public: + explicit XmppClient(talk_base::TaskParent * parent); + virtual ~XmppClient(); + + XmppReturnStatus Connect(const XmppClientSettings & settings, + const std::string & lang, + AsyncSocket * socket, + PreXmppAuth * preauth); + + virtual int ProcessStart(); + virtual int ProcessResponse(); + XmppReturnStatus Disconnect(); + + sigslot::signal1 SignalStateChange; + XmppEngine::Error GetError(int *subcode); + + // When there is a stanza, return the stanza + // so that they can be handled. + const XmlElement *GetStreamError(); + + // When there is an authentication error, we may have captcha info + // that the user can use to unlock their account + CaptchaChallenge GetCaptchaChallenge(); + + // When authentication is successful, this returns the service token + // (if we used GAIA authentication) + std::string GetAuthMechanism(); + std::string GetAuthToken(); + + XmppReturnStatus SendRaw(const std::string & text); + + XmppEngine* engine(); + + sigslot::signal2 SignalLogInput; + sigslot::signal2 SignalLogOutput; + + // As XmppTaskParentIntreface + virtual XmppClientInterface* GetClient() { return this; } + + // As XmppClientInterface + virtual XmppEngine::State GetState() const; + virtual const Jid& jid() const; + virtual std::string NextId(); + virtual XmppReturnStatus SendStanza(const XmlElement *stanza); + virtual XmppReturnStatus SendStanzaError(const XmlElement * pelOriginal, + XmppStanzaError code, + const std::string & text); + virtual void AddXmppTask(XmppTask *, XmppEngine::HandlerLevel); + virtual void RemoveXmppTask(XmppTask *); + + private: + friend class XmppTask; + + void OnAuthDone(); + + // Internal state management + enum { + STATE_PRE_XMPP_LOGIN = STATE_NEXT, + STATE_START_XMPP_LOGIN = STATE_NEXT + 1, + }; + int Process(int state) { + switch (state) { + case STATE_PRE_XMPP_LOGIN: return ProcessTokenLogin(); + case STATE_START_XMPP_LOGIN: return ProcessStartXmppLogin(); + default: return Task::Process(state); + } + } + + std::string GetStateName(int state) const { + switch (state) { + case STATE_PRE_XMPP_LOGIN: return "PRE_XMPP_LOGIN"; + case STATE_START_XMPP_LOGIN: return "START_XMPP_LOGIN"; + default: return Task::GetStateName(state); + } + } + + int ProcessTokenLogin(); + int ProcessStartXmppLogin(); + void EnsureClosed(); + + class Private; + friend class Private; + talk_base::scoped_ptr d_; + + bool delivering_signal_; + bool valid_; +}; + +} + +#endif // TALK_XMPP_XMPPCLIENT_H_ diff --git a/talk/xmpp/xmppclientsettings.h b/talk/xmpp/xmppclientsettings.h new file mode 100644 index 000000000..8851f180c --- /dev/null +++ b/talk/xmpp/xmppclientsettings.h @@ -0,0 +1,128 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_XMPPCLIENTSETTINGS_H_ +#define TALK_XMPP_XMPPCLIENTSETTINGS_H_ + +#include "talk/p2p/base/port.h" +#include "talk/base/cryptstring.h" +#include "talk/xmpp/xmppengine.h" + +namespace buzz { + +class XmppUserSettings { + public: + XmppUserSettings() + : use_tls_(buzz::TLS_DISABLED), + allow_plain_(false) { + } + + void set_user(const std::string& user) { user_ = user; } + void set_host(const std::string& host) { host_ = host; } + void set_pass(const talk_base::CryptString& pass) { pass_ = pass; } + void set_auth_token(const std::string& mechanism, + const std::string& token) { + auth_mechanism_ = mechanism; + auth_token_ = token; + } + void set_resource(const std::string& resource) { resource_ = resource; } + void set_use_tls(const TlsOptions use_tls) { use_tls_ = use_tls; } + void set_allow_plain(bool f) { allow_plain_ = f; } + void set_test_server_domain(const std::string& test_server_domain) { + test_server_domain_ = test_server_domain; + } + void set_token_service(const std::string& token_service) { + token_service_ = token_service; + } + + const std::string& user() const { return user_; } + const std::string& host() const { return host_; } + const talk_base::CryptString& pass() const { return pass_; } + const std::string& auth_mechanism() const { return auth_mechanism_; } + const std::string& auth_token() const { return auth_token_; } + const std::string& resource() const { return resource_; } + TlsOptions use_tls() const { return use_tls_; } + bool allow_plain() const { return allow_plain_; } + const std::string& test_server_domain() const { return test_server_domain_; } + const std::string& token_service() const { return token_service_; } + + private: + std::string user_; + std::string host_; + talk_base::CryptString pass_; + std::string auth_mechanism_; + std::string auth_token_; + std::string resource_; + TlsOptions use_tls_; + bool allow_plain_; + std::string test_server_domain_; + std::string token_service_; +}; + +class XmppClientSettings : public XmppUserSettings { + public: + XmppClientSettings() + : protocol_(cricket::PROTO_TCP), + proxy_(talk_base::PROXY_NONE), + proxy_port_(80), + use_proxy_auth_(false) { + } + + void set_server(const talk_base::SocketAddress& server) { + server_ = server; + } + void set_protocol(cricket::ProtocolType protocol) { protocol_ = protocol; } + void set_proxy(talk_base::ProxyType f) { proxy_ = f; } + void set_proxy_host(const std::string& host) { proxy_host_ = host; } + void set_proxy_port(int port) { proxy_port_ = port; }; + void set_use_proxy_auth(bool f) { use_proxy_auth_ = f; } + void set_proxy_user(const std::string& user) { proxy_user_ = user; } + void set_proxy_pass(const talk_base::CryptString& pass) { proxy_pass_ = pass; } + + const talk_base::SocketAddress& server() const { return server_; } + cricket::ProtocolType protocol() const { return protocol_; } + talk_base::ProxyType proxy() const { return proxy_; } + const std::string& proxy_host() const { return proxy_host_; } + int proxy_port() const { return proxy_port_; } + bool use_proxy_auth() const { return use_proxy_auth_; } + const std::string& proxy_user() const { return proxy_user_; } + const talk_base::CryptString& proxy_pass() const { return proxy_pass_; } + + private: + talk_base::SocketAddress server_; + cricket::ProtocolType protocol_; + talk_base::ProxyType proxy_; + std::string proxy_host_; + int proxy_port_; + bool use_proxy_auth_; + std::string proxy_user_; + talk_base::CryptString proxy_pass_; +}; + +} + +#endif // TALK_XMPP_XMPPCLIENT_H_ diff --git a/talk/xmpp/xmppengine.h b/talk/xmpp/xmppengine.h new file mode 100644 index 000000000..e1b35a372 --- /dev/null +++ b/talk/xmpp/xmppengine.h @@ -0,0 +1,349 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _xmppengine_h_ +#define _xmppengine_h_ + +// also part of the API +#include "talk/xmpp/jid.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlelement.h" + + +namespace buzz { + +class XmppEngine; +class SaslHandler; +typedef void * XmppIqCookie; + +//! XMPP stanza error codes. +//! Used in XmppEngine.SendStanzaError(). +enum XmppStanzaError { + XSE_BAD_REQUEST, + XSE_CONFLICT, + XSE_FEATURE_NOT_IMPLEMENTED, + XSE_FORBIDDEN, + XSE_GONE, + XSE_INTERNAL_SERVER_ERROR, + XSE_ITEM_NOT_FOUND, + XSE_JID_MALFORMED, + XSE_NOT_ACCEPTABLE, + XSE_NOT_ALLOWED, + XSE_PAYMENT_REQUIRED, + XSE_RECIPIENT_UNAVAILABLE, + XSE_REDIRECT, + XSE_REGISTRATION_REQUIRED, + XSE_SERVER_NOT_FOUND, + XSE_SERVER_TIMEOUT, + XSE_RESOURCE_CONSTRAINT, + XSE_SERVICE_UNAVAILABLE, + XSE_SUBSCRIPTION_REQUIRED, + XSE_UNDEFINED_CONDITION, + XSE_UNEXPECTED_REQUEST, +}; + +// XmppReturnStatus +// This is used by API functions to synchronously return status. +enum XmppReturnStatus { + XMPP_RETURN_OK, + XMPP_RETURN_BADARGUMENT, + XMPP_RETURN_BADSTATE, + XMPP_RETURN_PENDING, + XMPP_RETURN_UNEXPECTED, + XMPP_RETURN_NOTYETIMPLEMENTED, +}; + +// TlsOptions +// This is used by API to identify TLS setting. +enum TlsOptions { + TLS_DISABLED, + TLS_ENABLED, + TLS_REQUIRED +}; + +//! Callback for socket output for an XmppEngine connection. +//! Register via XmppEngine.SetOutputHandler. An XmppEngine +//! can call back to this handler while it is processing +//! Connect, SendStanza, SendIq, Disconnect, or HandleInput. +class XmppOutputHandler { +public: + virtual ~XmppOutputHandler() {} + + //! Deliver the specified bytes to the XMPP socket. + virtual void WriteOutput(const char * bytes, size_t len) = 0; + + //! Initiate TLS encryption on the socket. + //! The implementation must verify that the SSL + //! certificate matches the given domainname. + virtual void StartTls(const std::string & domainname) = 0; + + //! Called when engine wants the connecton closed. + virtual void CloseConnection() = 0; +}; + +//! Callback to deliver engine state change notifications +//! to the object managing the engine. +class XmppSessionHandler { +public: + virtual ~XmppSessionHandler() {} + //! Called when engine changes state. Argument is new state. + virtual void OnStateChange(int state) = 0; +}; + +//! Callback to deliver stanzas to an Xmpp application module. +//! Register via XmppEngine.SetDefaultSessionHandler or via +//! XmppEngine.AddSessionHAndler. +class XmppStanzaHandler { +public: + virtual ~XmppStanzaHandler() {} + //! Process the given stanza. + //! The handler must return true if it has handled the stanza. + //! A false return value causes the stanza to be passed on to + //! the next registered handler. + virtual bool HandleStanza(const XmlElement * stanza) = 0; +}; + +//! Callback to deliver iq responses (results and errors). +//! Register while sending an iq via XmppEngine.SendIq. +//! Iq responses are routed to matching XmppIqHandlers in preference +//! to sending to any registered SessionHandlers. +class XmppIqHandler { +public: + virtual ~XmppIqHandler() {} + //! Called to handle the iq response. + //! The response may be either a result or an error, and will have + //! an 'id' that matches the request and a 'from' that matches the + //! 'to' of the request. Called no more than once; once this is + //! called, the handler is automatically unregistered. + virtual void IqResponse(XmppIqCookie cookie, const XmlElement * pelStanza) = 0; +}; + +//! The XMPP connection engine. +//! This engine implements the client side of the 'core' XMPP protocol. +//! To use it, register an XmppOutputHandler to handle socket output +//! and pass socket input to HandleInput. Then application code can +//! set up the connection with a user, password, and other settings, +//! and then call Connect() to initiate the connection. +//! An application can listen for events and receive stanzas by +//! registering an XmppStanzaHandler via AddStanzaHandler(). +class XmppEngine { +public: + static XmppEngine * Create(); + virtual ~XmppEngine() {} + + //! Error codes. See GetError(). + enum Error { + ERROR_NONE = 0, //!< No error + ERROR_XML, //!< Malformed XML or encoding error + ERROR_STREAM, //!< XMPP stream error - see GetStreamError() + ERROR_VERSION, //!< XMPP version error + ERROR_UNAUTHORIZED, //!< User is not authorized (rejected credentials) + ERROR_TLS, //!< TLS could not be negotiated + ERROR_AUTH, //!< Authentication could not be negotiated + ERROR_BIND, //!< Resource or session binding could not be negotiated + ERROR_CONNECTION_CLOSED,//!< Connection closed by output handler. + ERROR_DOCUMENT_CLOSED, //!< Closed by + ERROR_SOCKET, //!< Socket error + ERROR_NETWORK_TIMEOUT, //!< Some sort of timeout (eg., we never got the roster) + ERROR_MISSING_USERNAME //!< User has a Google Account but no nickname + }; + + //! States. See GetState(). + enum State { + STATE_NONE = 0, //!< Nonexistent state + STATE_START, //!< Initial state. + STATE_OPENING, //!< Exchanging stream headers, authenticating and so on. + STATE_OPEN, //!< Authenticated and bound. + STATE_CLOSED, //!< Session closed, possibly due to error. + }; + + // SOCKET INPUT AND OUTPUT ------------------------------------------------ + + //! Registers the handler for socket output + virtual XmppReturnStatus SetOutputHandler(XmppOutputHandler *pxoh) = 0; + + //! Provides socket input to the engine + virtual XmppReturnStatus HandleInput(const char * bytes, size_t len) = 0; + + //! Advises the engine that the socket has closed + virtual XmppReturnStatus ConnectionClosed(int subcode) = 0; + + // SESSION SETUP --------------------------------------------------------- + + //! Indicates the (bare) JID for the user to use. + virtual XmppReturnStatus SetUser(const Jid & jid)= 0; + + //! Get the login (bare) JID. + virtual const Jid & GetUser() = 0; + + //! Provides different methods for credentials for login. + //! Takes ownership of this object; deletes when login is done + virtual XmppReturnStatus SetSaslHandler(SaslHandler * h) = 0; + + //! Sets whether TLS will be used within the connection (default true). + virtual XmppReturnStatus SetTls(TlsOptions useTls) = 0; + + //! Sets an alternate domain from which we allows TLS certificates. + //! This is for use in the case where a we want to allow a proxy to + //! serve up its own certificate rather than one owned by the underlying + //! domain. + virtual XmppReturnStatus SetTlsServer(const std::string & proxy_hostname, + const std::string & proxy_domain) = 0; + + //! Gets whether TLS will be used within the connection. + virtual TlsOptions GetTls() = 0; + + //! Sets the request resource name, if any (optional). + //! Note that the resource name may be overridden by the server; after + //! binding, the actual resource name is available as part of FullJid(). + virtual XmppReturnStatus SetRequestedResource(const std::string& resource) = 0; + + //! Gets the request resource name. + virtual const std::string & GetRequestedResource() = 0; + + //! Sets language + virtual void SetLanguage(const std::string & lang) = 0; + + // SESSION MANAGEMENT --------------------------------------------------- + + //! Set callback for state changes. + virtual XmppReturnStatus SetSessionHandler(XmppSessionHandler* handler) = 0; + + //! Initiates the XMPP connection. + //! After supplying connection settings, call this once to initiate, + //! (optionally) encrypt, authenticate, and bind the connection. + virtual XmppReturnStatus Connect() = 0; + + //! The current engine state. + virtual State GetState() = 0; + + //! Returns true if the connection is encrypted (under TLS) + virtual bool IsEncrypted() = 0; + + //! The error code. + //! Consult this after XmppOutputHandler.OnClose(). + virtual Error GetError(int *subcode) = 0; + + //! The stream:error stanza, when the error is XmppEngine::ERROR_STREAM. + //! Notice the stanza returned is owned by the XmppEngine and + //! is deleted when the engine is destroyed. + virtual const XmlElement * GetStreamError() = 0; + + //! Closes down the connection. + //! Sends CloseConnection to output, and disconnects and registered + //! session handlers. After Disconnect completes, it is guaranteed + //! that no further callbacks will be made. + virtual XmppReturnStatus Disconnect() = 0; + + // APPLICATION USE ------------------------------------------------------- + + enum HandlerLevel { + HL_NONE = 0, + HL_PEEK, //!< Sees messages before all other processing; cannot abort + HL_SINGLE, //!< Watches for a single message, e.g., by id and sender + HL_SENDER, //!< Watches for a type of message from a specific sender + HL_TYPE, //!< Watches a type of message, e.g., all groupchat msgs + HL_ALL, //!< Watches all messages - gets last shot + HL_COUNT, //!< Count of handler levels + }; + + //! Adds a listener for session events. + //! Stanza delivery is chained to session handlers; the first to + //! return 'true' is the last to get each stanza. + virtual XmppReturnStatus AddStanzaHandler(XmppStanzaHandler* handler, HandlerLevel level = HL_PEEK) = 0; + + //! Removes a listener for session events. + virtual XmppReturnStatus RemoveStanzaHandler(XmppStanzaHandler* handler) = 0; + + //! Sends a stanza to the server. + virtual XmppReturnStatus SendStanza(const XmlElement * pelStanza) = 0; + + //! Sends raw text to the server + virtual XmppReturnStatus SendRaw(const std::string & text) = 0; + + //! Sends an iq to the server, and registers a callback for the result. + //! Returns the cookie passed to the result handler. + virtual XmppReturnStatus SendIq(const XmlElement* pelStanza, + XmppIqHandler* iq_handler, + XmppIqCookie* cookie) = 0; + + //! Unregisters an iq callback handler given its cookie. + //! No callback will come to this handler after it's unregistered. + virtual XmppReturnStatus RemoveIqHandler(XmppIqCookie cookie, + XmppIqHandler** iq_handler) = 0; + + + //! Forms and sends an error in response to the given stanza. + //! Swaps to and from, sets type to "error", and adds error information + //! based on the passed code. Text is optional and may be STR_EMPTY. + virtual XmppReturnStatus SendStanzaError(const XmlElement * pelOriginal, + XmppStanzaError code, + const std::string & text) = 0; + + //! The fullly bound JID. + //! This JID is only valid after binding has succeeded. If the value + //! is JID_NULL, the binding has not succeeded. + virtual const Jid & FullJid() = 0; + + //! The next unused iq id for this connection. + //! Call this when building iq stanzas, to ensure that each iq + //! gets its own unique id. + virtual std::string NextId() = 0; + +}; + +} + + +// Move these to a better location + +#define XMPP_FAILED(x) \ + ( (x) == buzz::XMPP_RETURN_OK ? false : true) \ + + +#define XMPP_SUCCEEDED(x) \ + ( (x) == buzz::XMPP_RETURN_OK ? true : false) \ + +#define IFR(x) \ + do { \ + xmpp_status = (x); \ + if (XMPP_FAILED(xmpp_status)) { \ + return xmpp_status; \ + } \ + } while (false) \ + + +#define IFC(x) \ + do { \ + xmpp_status = (x); \ + if (XMPP_FAILED(xmpp_status)) { \ + goto Cleanup; \ + } \ + } while (false) \ + + +#endif diff --git a/talk/xmpp/xmppengine_unittest.cc b/talk/xmpp/xmppengine_unittest.cc new file mode 100644 index 000000000..46b79c6d3 --- /dev/null +++ b/talk/xmpp/xmppengine_unittest.cc @@ -0,0 +1,318 @@ +// Copyright 2004 Google Inc. All Rights Reserved +// Author: David Bau + +#include +#include +#include +#include "talk/base/common.h" +#include "talk/base/gunit.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/util_unittest.h" +#include "talk/xmpp/saslplainmechanism.h" +#include "talk/xmpp/plainsaslhandler.h" +#include "talk/xmpp/xmppengine.h" + +using buzz::Jid; +using buzz::QName; +using buzz::XmlElement; +using buzz::XmppEngine; +using buzz::XmppIqCookie; +using buzz::XmppIqHandler; +using buzz::XmppTestHandler; +using buzz::QN_ID; +using buzz::QN_IQ; +using buzz::QN_TYPE; +using buzz::QN_ROSTER_QUERY; +using buzz::XMPP_RETURN_OK; +using buzz::XMPP_RETURN_BADARGUMENT; + +// XmppEngineTestIqHandler +// This class grabs the response to an IQ stanza and stores it in a string. +class XmppEngineTestIqHandler : public XmppIqHandler { + public: + virtual void IqResponse(XmppIqCookie, const XmlElement * stanza) { + ss_ << stanza->Str(); + } + + std::string IqResponseActivity() { + std::string result = ss_.str(); + ss_.str(""); + return result; + } + + private: + std::stringstream ss_; +}; + +class XmppEngineTest : public testing::Test { + public: + XmppEngine* engine() { return engine_.get(); } + XmppTestHandler* handler() { return handler_.get(); } + virtual void SetUp() { + engine_.reset(XmppEngine::Create()); + handler_.reset(new XmppTestHandler(engine_.get())); + + Jid jid("david@my-server"); + talk_base::InsecureCryptStringImpl pass; + pass.password() = "david"; + engine_->SetSessionHandler(handler_.get()); + engine_->SetOutputHandler(handler_.get()); + engine_->AddStanzaHandler(handler_.get()); + engine_->SetUser(jid); + engine_->SetSaslHandler( + new buzz::PlainSaslHandler(jid, talk_base::CryptString(pass), true)); + } + virtual void TearDown() { + handler_.reset(); + engine_.reset(); + } + void RunLogin(); + + private: + talk_base::scoped_ptr engine_; + talk_base::scoped_ptr handler_; +}; + +void XmppEngineTest::RunLogin() { + // Connect + EXPECT_EQ(XmppEngine::STATE_START, engine()->GetState()); + engine()->Connect(); + EXPECT_EQ(XmppEngine::STATE_OPENING, engine()->GetState()); + + EXPECT_EQ("[OPENING]", handler_->SessionActivity()); + + EXPECT_EQ("\r\n", handler_->OutputActivity()); + + std::string input = + ""; + engine()->HandleInput(input.c_str(), input.length()); + + input = + "" + "" + "" + "" + "" + "DIGEST-MD5" + "PLAIN" + "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("", + handler_->OutputActivity()); + + EXPECT_EQ("", handler_->SessionActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + + input = ""; + engine()->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("[START-TLS my-server]" + "\r\n", handler_->OutputActivity()); + + EXPECT_EQ("", handler_->SessionActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + + input = ""; + engine()->HandleInput(input.c_str(), input.length()); + + input = + "" + "" + "DIGEST-MD5" + "PLAIN" + "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("AGRhdmlkAGRhdmlk", + handler_->OutputActivity()); + + EXPECT_EQ("", handler_->SessionActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + + input = ""; + engine()->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("\r\n", handler_->OutputActivity()); + + EXPECT_EQ("", handler_->SessionActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + + input = ""; + engine()->HandleInput(input.c_str(), input.length()); + + input = "" + "" + "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("" + "", + handler_->OutputActivity()); + + EXPECT_EQ("", handler_->SessionActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + + input = "" + "" + "david@my-server/test"; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("" + "", + handler_->OutputActivity()); + + EXPECT_EQ("", handler_->SessionActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + + input = ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[OPEN]", handler_->SessionActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + EXPECT_EQ(Jid("david@my-server/test"), engine()->FullJid()); +} + +// TestSuccessfulLogin() +// This function simply tests to see if a login works. This includes +// encryption and authentication +TEST_F(XmppEngineTest, TestSuccessfulLoginAndDisconnect) { + RunLogin(); + engine()->Disconnect(); + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppEngineTest, TestSuccessfulLoginAndConnectionClosed) { + RunLogin(); + engine()->ConnectionClosed(0); + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-CONNECTION-CLOSED]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + + +// TestNotXmpp() +// This tests the error case when connecting to a non XMPP service +TEST_F(XmppEngineTest, TestNotXmpp) { + // Connect + engine()->Connect(); + EXPECT_EQ("\r\n", handler()->OutputActivity()); + + // Send garbage response (courtesy of apache) + std::string input = ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[OPENING][CLOSED][ERROR-XML]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +// TestPassthrough() +// This tests that arbitrary stanzas can be passed to the server through +// the engine. +TEST_F(XmppEngineTest, TestPassthrough) { + // Queue up an app stanza + XmlElement application_stanza(QName("test", "app-stanza")); + application_stanza.AddText("this-is-a-test"); + engine()->SendStanza(&application_stanza); + + // Do the whole login handshake + RunLogin(); + + EXPECT_EQ("this-is-a-test" + "", handler()->OutputActivity()); + + // do another stanza + XmlElement roster_get(QN_IQ); + roster_get.AddAttr(QN_TYPE, "get"); + roster_get.AddAttr(QN_ID, engine()->NextId()); + roster_get.AddElement(new XmlElement(QN_ROSTER_QUERY, true)); + engine()->SendStanza(&roster_get); + EXPECT_EQ("" + "", handler()->OutputActivity()); + + // now say the server ends the stream + engine()->HandleInput("", 16); + EXPECT_EQ("[CLOSED][ERROR-DOCUMENT-CLOSED]", handler()->SessionActivity()); + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +// TestIqCallback() +// This tests the routing of Iq stanzas and responses. +TEST_F(XmppEngineTest, TestIqCallback) { + XmppEngineTestIqHandler iq_response; + XmppIqCookie cookie; + + // Do the whole login handshake + RunLogin(); + + // Build an iq request + XmlElement roster_get(QN_IQ); + roster_get.AddAttr(QN_TYPE, "get"); + roster_get.AddAttr(QN_ID, engine()->NextId()); + roster_get.AddElement(new XmlElement(QN_ROSTER_QUERY, true)); + engine()->SendIq(&roster_get, &iq_response, &cookie); + EXPECT_EQ("" + "", handler()->OutputActivity()); + EXPECT_EQ("", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); + EXPECT_EQ("", iq_response.IqResponseActivity()); + + // now say the server responds to the iq + std::string input = "" + "foo" + ""; + engine()->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("", handler()->OutputActivity()); + EXPECT_EQ("", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); + EXPECT_EQ("" + "foo" + "", iq_response.IqResponseActivity()); + + EXPECT_EQ(XMPP_RETURN_BADARGUMENT, engine()->RemoveIqHandler(cookie, NULL)); + + // Do it again with another id to test cancel + roster_get.SetAttr(QN_ID, engine()->NextId()); + engine()->SendIq(&roster_get, &iq_response, &cookie); + EXPECT_EQ("" + "", handler()->OutputActivity()); + EXPECT_EQ("", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); + EXPECT_EQ("", iq_response.IqResponseActivity()); + + // cancel the handler this time + EXPECT_EQ(XMPP_RETURN_OK, engine()->RemoveIqHandler(cookie, NULL)); + + // now say the server responds to the iq: the iq handler should not get it. + input = "bar" + ""; + engine()->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("" + "bar" + "", handler()->StanzaActivity()); + EXPECT_EQ("", iq_response.IqResponseActivity()); + EXPECT_EQ("", handler()->OutputActivity()); + EXPECT_EQ("", handler()->SessionActivity()); +} diff --git a/talk/xmpp/xmppengineimpl.cc b/talk/xmpp/xmppengineimpl.cc new file mode 100644 index 000000000..8bcea029a --- /dev/null +++ b/talk/xmpp/xmppengineimpl.cc @@ -0,0 +1,464 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmpp/xmppengineimpl.h" + +#include +#include +#include + +#include "talk/base/common.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmllite/xmlprinter.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/saslhandler.h" +#include "talk/xmpp/xmpplogintask.h" + +namespace buzz { + +XmppEngine* XmppEngine::Create() { + return new XmppEngineImpl(); +} + + +XmppEngineImpl::XmppEngineImpl() + : stanza_parse_handler_(this), + stanza_parser_(&stanza_parse_handler_), + engine_entered_(0), + password_(), + requested_resource_(STR_EMPTY), + tls_option_(buzz::TLS_REQUIRED), + login_task_(new XmppLoginTask(this)), + next_id_(0), + state_(STATE_START), + encrypted_(false), + error_code_(ERROR_NONE), + subcode_(0), + stream_error_(NULL), + raised_reset_(false), + output_handler_(NULL), + session_handler_(NULL), + iq_entries_(new IqEntryVector()), + sasl_handler_(NULL), + output_(new std::stringstream()) { + for (int i = 0; i < HL_COUNT; i+= 1) { + stanza_handlers_[i].reset(new StanzaHandlerVector()); + } + + // Add XMPP namespaces to XML namespaces stack. + xmlns_stack_.AddXmlns("stream", "http://etherx.jabber.org/streams"); + xmlns_stack_.AddXmlns("", "jabber:client"); +} + +XmppEngineImpl::~XmppEngineImpl() { + DeleteIqCookies(); +} + +XmppReturnStatus XmppEngineImpl::SetOutputHandler( + XmppOutputHandler* output_handler) { + if (state_ != STATE_START) + return XMPP_RETURN_BADSTATE; + + output_handler_ = output_handler; + + return XMPP_RETURN_OK; +} + +XmppReturnStatus XmppEngineImpl::SetSessionHandler( + XmppSessionHandler* session_handler) { + if (state_ != STATE_START) + return XMPP_RETURN_BADSTATE; + + session_handler_ = session_handler; + + return XMPP_RETURN_OK; +} + +XmppReturnStatus XmppEngineImpl::HandleInput( + const char* bytes, size_t len) { + if (state_ < STATE_OPENING || state_ > STATE_OPEN) + return XMPP_RETURN_BADSTATE; + + EnterExit ee(this); + + // TODO: The return value of the xml parser is not checked. + stanza_parser_.Parse(bytes, len, false); + + return XMPP_RETURN_OK; +} + +XmppReturnStatus XmppEngineImpl::ConnectionClosed(int subcode) { + if (state_ != STATE_CLOSED) { + EnterExit ee(this); + // If told that connection closed and not already closed, + // then connection was unpexectedly dropped. + if (subcode) { + SignalError(ERROR_SOCKET, subcode); + } else { + SignalError(ERROR_CONNECTION_CLOSED, 0); // no subcode + } + } + return XMPP_RETURN_OK; +} + +XmppReturnStatus XmppEngineImpl::SetTls(TlsOptions use_tls) { + if (state_ != STATE_START) + return XMPP_RETURN_BADSTATE; + tls_option_ = use_tls; + return XMPP_RETURN_OK; +} + +XmppReturnStatus XmppEngineImpl::SetTlsServer( + const std::string& tls_server_hostname, + const std::string& tls_server_domain) { + if (state_ != STATE_START) + return XMPP_RETURN_BADSTATE; + + tls_server_hostname_ = tls_server_hostname; + tls_server_domain_= tls_server_domain; + + return XMPP_RETURN_OK; +} + +TlsOptions XmppEngineImpl::GetTls() { + return tls_option_; +} + +XmppReturnStatus XmppEngineImpl::SetUser(const Jid& jid) { + if (state_ != STATE_START) + return XMPP_RETURN_BADSTATE; + + user_jid_ = jid; + + return XMPP_RETURN_OK; +} + +const Jid& XmppEngineImpl::GetUser() { + return user_jid_; +} + +XmppReturnStatus XmppEngineImpl::SetSaslHandler(SaslHandler* sasl_handler) { + if (state_ != STATE_START) + return XMPP_RETURN_BADSTATE; + + sasl_handler_.reset(sasl_handler); + return XMPP_RETURN_OK; +} + +XmppReturnStatus XmppEngineImpl::SetRequestedResource( + const std::string& resource) { + if (state_ != STATE_START) + return XMPP_RETURN_BADSTATE; + + requested_resource_ = resource; + + return XMPP_RETURN_OK; +} + +const std::string& XmppEngineImpl::GetRequestedResource() { + return requested_resource_; +} + +XmppReturnStatus XmppEngineImpl::AddStanzaHandler( + XmppStanzaHandler* stanza_handler, + XmppEngine::HandlerLevel level) { + if (state_ == STATE_CLOSED) + return XMPP_RETURN_BADSTATE; + + stanza_handlers_[level]->push_back(stanza_handler); + + return XMPP_RETURN_OK; +} + +XmppReturnStatus XmppEngineImpl::RemoveStanzaHandler( + XmppStanzaHandler* stanza_handler) { + bool found = false; + + for (int level = 0; level < HL_COUNT; level += 1) { + StanzaHandlerVector::iterator new_end = + std::remove(stanza_handlers_[level]->begin(), + stanza_handlers_[level]->end(), + stanza_handler); + + if (new_end != stanza_handlers_[level]->end()) { + stanza_handlers_[level]->erase(new_end, stanza_handlers_[level]->end()); + found = true; + } + } + + if (!found) + return XMPP_RETURN_BADARGUMENT; + + return XMPP_RETURN_OK; +} + +XmppReturnStatus XmppEngineImpl::Connect() { + if (state_ != STATE_START) + return XMPP_RETURN_BADSTATE; + + EnterExit ee(this); + + // get the login task started + state_ = STATE_OPENING; + if (login_task_) { + login_task_->IncomingStanza(NULL, false); + if (login_task_->IsDone()) + login_task_.reset(); + } + + return XMPP_RETURN_OK; +} + +XmppReturnStatus XmppEngineImpl::SendStanza(const XmlElement* element) { + if (state_ == STATE_CLOSED) + return XMPP_RETURN_BADSTATE; + + EnterExit ee(this); + + if (login_task_) { + // still handshaking - then outbound stanzas are queued + login_task_->OutgoingStanza(element); + } else { + // handshake done - send straight through + InternalSendStanza(element); + } + + return XMPP_RETURN_OK; +} + +XmppReturnStatus XmppEngineImpl::SendRaw(const std::string& text) { + if (state_ == STATE_CLOSED || login_task_) + return XMPP_RETURN_BADSTATE; + + EnterExit ee(this); + + (*output_) << text; + + return XMPP_RETURN_OK; +} + +std::string XmppEngineImpl::NextId() { + std::stringstream ss; + ss << next_id_++; + return ss.str(); +} + +XmppReturnStatus XmppEngineImpl::Disconnect() { + if (state_ != STATE_CLOSED) { + EnterExit ee(this); + if (state_ == STATE_OPEN) + *output_ << ""; + state_ = STATE_CLOSED; + } + + return XMPP_RETURN_OK; +} + +void XmppEngineImpl::IncomingStart(const XmlElement* start) { + if (HasError() || raised_reset_) + return; + + if (login_task_) { + // start-stream should go to login task + login_task_->IncomingStanza(start, true); + if (login_task_->IsDone()) + login_task_.reset(); + } + else { + // if not logging in, it's an error to see a start + SignalError(ERROR_XML, 0); + } +} + +void XmppEngineImpl::IncomingStanza(const XmlElement* stanza) { + if (HasError() || raised_reset_) + return; + + if (stanza->Name() == QN_STREAM_ERROR) { + // Explicit XMPP stream error + SignalStreamError(stanza); + } else if (login_task_) { + // Handle login handshake + login_task_->IncomingStanza(stanza, false); + if (login_task_->IsDone()) + login_task_.reset(); + } else if (HandleIqResponse(stanza)) { + // iq is handled by above call + } else { + // give every "peek" handler a shot at all stanzas + for (size_t i = 0; i < stanza_handlers_[HL_PEEK]->size(); i += 1) { + (*stanza_handlers_[HL_PEEK])[i]->HandleStanza(stanza); + } + + // give other handlers a shot in precedence order, stopping after handled + for (int level = HL_SINGLE; level <= HL_ALL; level += 1) { + for (size_t i = 0; i < stanza_handlers_[level]->size(); i += 1) { + if ((*stanza_handlers_[level])[i]->HandleStanza(stanza)) + return; + } + } + + // If nobody wants to handle a stanza then send back an error. + // Only do this for IQ stanzas as messages should probably just be dropped + // and presence stanzas should certainly be dropped. + std::string type = stanza->Attr(QN_TYPE); + if (stanza->Name() == QN_IQ && + !(type == "error" || type == "result")) { + SendStanzaError(stanza, XSE_FEATURE_NOT_IMPLEMENTED, STR_EMPTY); + } + } +} + +void XmppEngineImpl::IncomingEnd(bool isError) { + if (HasError() || raised_reset_) + return; + + SignalError(isError ? ERROR_XML : ERROR_DOCUMENT_CLOSED, 0); +} + +void XmppEngineImpl::InternalSendStart(const std::string& to) { + std::string hostname = tls_server_hostname_; + if (hostname.empty()) + hostname = to; + + // If not language is specified, the spec says use * + std::string lang = lang_; + if (lang.length() == 0) + lang = "*"; + + // send stream-beginning + // note, we put a \r\n at tne end fo the first line to cause non-XMPP + // line-oriented servers (e.g., Apache) to reveal themselves more quickly. + *output_ << "\r\n"; +} + +void XmppEngineImpl::InternalSendStanza(const XmlElement* element) { + // It should really never be necessary to set a FROM attribute on a stanza. + // It is implied by the bind on the stream and if you get it wrong + // (by flipping from/to on a message?) the server will close the stream. + ASSERT(!element->HasAttr(QN_FROM)); + + XmlPrinter::PrintXml(output_.get(), element, &xmlns_stack_); +} + +std::string XmppEngineImpl::ChooseBestSaslMechanism( + const std::vector& mechanisms, bool encrypted) { + return sasl_handler_->ChooseBestSaslMechanism(mechanisms, encrypted); +} + +SaslMechanism* XmppEngineImpl::GetSaslMechanism(const std::string& name) { + return sasl_handler_->CreateSaslMechanism(name); +} + +void XmppEngineImpl::SignalBound(const Jid& fullJid) { + if (state_ == STATE_OPENING) { + bound_jid_ = fullJid; + state_ = STATE_OPEN; + } +} + +void XmppEngineImpl::SignalStreamError(const XmlElement* stream_error) { + if (state_ != STATE_CLOSED) { + stream_error_.reset(new XmlElement(*stream_error)); + SignalError(ERROR_STREAM, 0); + } +} + +void XmppEngineImpl::SignalError(Error error_code, int sub_code) { + if (state_ != STATE_CLOSED) { + error_code_ = error_code; + subcode_ = sub_code; + state_ = STATE_CLOSED; + } +} + +bool XmppEngineImpl::HasError() { + return error_code_ != ERROR_NONE; +} + +void XmppEngineImpl::StartTls(const std::string& domain) { + if (output_handler_) { + // As substitute for the real (login jid's) domain, we permit + // verifying a tls_server_domain_ instead, if one was passed. + // This allows us to avoid running a proxy that needs to handle + // valuable certificates. + output_handler_->StartTls( + tls_server_domain_.empty() ? domain : tls_server_domain_); + encrypted_ = true; + } +} + +XmppEngineImpl::EnterExit::EnterExit(XmppEngineImpl* engine) + : engine_(engine), + state_(engine->state_), + error_(engine->error_code_) { + engine->engine_entered_ += 1; +} + +XmppEngineImpl::EnterExit::~EnterExit() { + XmppEngineImpl* engine = engine_; + + engine->engine_entered_ -= 1; + + bool closing = (engine->state_ != state_ && + engine->state_ == STATE_CLOSED); + bool flushing = closing || (engine->engine_entered_ == 0); + + if (engine->output_handler_ && flushing) { + std::string output = engine->output_->str(); + if (output.length() > 0) + engine->output_handler_->WriteOutput(output.c_str(), output.length()); + engine->output_->str(""); + + if (closing) { + engine->output_handler_->CloseConnection(); + engine->output_handler_ = 0; + } + } + + if (engine->engine_entered_) + return; + + if (engine->raised_reset_) { + engine->stanza_parser_.Reset(); + engine->raised_reset_ = false; + } + + if (engine->session_handler_) { + if (engine->state_ != state_) + engine->session_handler_->OnStateChange(engine->state_); + // Note: Handling of OnStateChange(CLOSED) should allow for the + // deletion of the engine, so no members should be accessed + // after this line. + } +} + +} // namespace buzz diff --git a/talk/xmpp/xmppengineimpl.h b/talk/xmpp/xmppengineimpl.h new file mode 100644 index 000000000..e292e75d1 --- /dev/null +++ b/talk/xmpp/xmppengineimpl.h @@ -0,0 +1,284 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_XMPPENGINEIMPL_H_ +#define TALK_XMPP_XMPPENGINEIMPL_H_ + +#include +#include +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmppstanzaparser.h" + +namespace buzz { + +class XmppLoginTask; +class XmppEngine; +class XmppIqEntry; +class SaslHandler; +class SaslMechanism; + +//! The XMPP connection engine. +//! This engine implements the client side of the 'core' XMPP protocol. +//! To use it, register an XmppOutputHandler to handle socket output +//! and pass socket input to HandleInput. Then application code can +//! set up the connection with a user, password, and other settings, +//! and then call Connect() to initiate the connection. +//! An application can listen for events and receive stanzas by +//! registering an XmppStanzaHandler via AddStanzaHandler(). +class XmppEngineImpl : public XmppEngine { + public: + XmppEngineImpl(); + virtual ~XmppEngineImpl(); + + // SOCKET INPUT AND OUTPUT ------------------------------------------------ + + //! Registers the handler for socket output + virtual XmppReturnStatus SetOutputHandler(XmppOutputHandler *pxoh); + + //! Provides socket input to the engine + virtual XmppReturnStatus HandleInput(const char* bytes, size_t len); + + //! Advises the engine that the socket has closed + virtual XmppReturnStatus ConnectionClosed(int subcode); + + // SESSION SETUP --------------------------------------------------------- + + //! Indicates the (bare) JID for the user to use. + virtual XmppReturnStatus SetUser(const Jid& jid); + + //! Get the login (bare) JID. + virtual const Jid& GetUser(); + + //! Indicates the autentication to use. Takes ownership of the object. + virtual XmppReturnStatus SetSaslHandler(SaslHandler* sasl_handler); + + //! Sets whether TLS will be used within the connection (default true). + virtual XmppReturnStatus SetTls(TlsOptions use_tls); + + //! Sets an alternate domain from which we allows TLS certificates. + //! This is for use in the case where a we want to allow a proxy to + //! serve up its own certificate rather than one owned by the underlying + //! domain. + virtual XmppReturnStatus SetTlsServer(const std::string& proxy_hostname, + const std::string& proxy_domain); + + //! Gets whether TLS will be used within the connection. + virtual TlsOptions GetTls(); + + //! Sets the request resource name, if any (optional). + //! Note that the resource name may be overridden by the server; after + //! binding, the actual resource name is available as part of FullJid(). + virtual XmppReturnStatus SetRequestedResource(const std::string& resource); + + //! Gets the request resource name. + virtual const std::string& GetRequestedResource(); + + //! Sets language + virtual void SetLanguage(const std::string& lang) { + lang_ = lang; + } + + // SESSION MANAGEMENT --------------------------------------------------- + + //! Set callback for state changes. + virtual XmppReturnStatus SetSessionHandler(XmppSessionHandler* handler); + + //! Initiates the XMPP connection. + //! After supplying connection settings, call this once to initiate, + //! (optionally) encrypt, authenticate, and bind the connection. + virtual XmppReturnStatus Connect(); + + //! The current engine state. + virtual State GetState() { return state_; } + + //! Returns true if the connection is encrypted (under TLS) + virtual bool IsEncrypted() { return encrypted_; } + + //! The error code. + //! Consult this after XmppOutputHandler.OnClose(). + virtual Error GetError(int *subcode) { + if (subcode) { + *subcode = subcode_; + } + return error_code_; + } + + //! The stream:error stanza, when the error is XmppEngine::ERROR_STREAM. + //! Notice the stanza returned is owned by the XmppEngine and + //! is deleted when the engine is destroyed. + virtual const XmlElement* GetStreamError() { return stream_error_.get(); } + + //! Closes down the connection. + //! Sends CloseConnection to output, and disconnects and registered + //! session handlers. After Disconnect completes, it is guaranteed + //! that no further callbacks will be made. + virtual XmppReturnStatus Disconnect(); + + // APPLICATION USE ------------------------------------------------------- + + //! Adds a listener for session events. + //! Stanza delivery is chained to session handlers; the first to + //! return 'true' is the last to get each stanza. + virtual XmppReturnStatus AddStanzaHandler(XmppStanzaHandler* handler, + XmppEngine::HandlerLevel level); + + //! Removes a listener for session events. + virtual XmppReturnStatus RemoveStanzaHandler(XmppStanzaHandler* handler); + + //! Sends a stanza to the server. + virtual XmppReturnStatus SendStanza(const XmlElement* stanza); + + //! Sends raw text to the server + virtual XmppReturnStatus SendRaw(const std::string& text); + + //! Sends an iq to the server, and registers a callback for the result. + //! Returns the cookie passed to the result handler. + virtual XmppReturnStatus SendIq(const XmlElement* stanza, + XmppIqHandler* iq_handler, + XmppIqCookie* cookie); + + //! Unregisters an iq callback handler given its cookie. + //! No callback will come to this handler after it's unregistered. + virtual XmppReturnStatus RemoveIqHandler(XmppIqCookie cookie, + XmppIqHandler** iq_handler); + + //! Forms and sends an error in response to the given stanza. + //! Swaps to and from, sets type to "error", and adds error information + //! based on the passed code. Text is optional and may be STR_EMPTY. + virtual XmppReturnStatus SendStanzaError(const XmlElement* pelOriginal, + XmppStanzaError code, + const std::string& text); + + //! The fullly bound JID. + //! This JID is only valid after binding has succeeded. If the value + //! is JID_NULL, the binding has not succeeded. + virtual const Jid& FullJid() { return bound_jid_; } + + //! The next unused iq id for this connection. + //! Call this when building iq stanzas, to ensure that each iq + //! gets its own unique id. + virtual std::string NextId(); + + private: + friend class XmppLoginTask; + friend class XmppIqEntry; + + void IncomingStanza(const XmlElement *stanza); + void IncomingStart(const XmlElement *stanza); + void IncomingEnd(bool isError); + + void InternalSendStart(const std::string& domainName); + void InternalSendStanza(const XmlElement* stanza); + std::string ChooseBestSaslMechanism( + const std::vector& mechanisms, bool encrypted); + SaslMechanism* GetSaslMechanism(const std::string& name); + void SignalBound(const Jid& fullJid); + void SignalStreamError(const XmlElement* streamError); + void SignalError(Error errorCode, int subCode); + bool HasError(); + void DeleteIqCookies(); + bool HandleIqResponse(const XmlElement* element); + void StartTls(const std::string& domain); + void RaiseReset() { raised_reset_ = true; } + + class StanzaParseHandler : public XmppStanzaParseHandler { + public: + StanzaParseHandler(XmppEngineImpl* outer) : outer_(outer) {} + virtual ~StanzaParseHandler() {} + + virtual void StartStream(const XmlElement* stream) { + outer_->IncomingStart(stream); + } + virtual void Stanza(const XmlElement* stanza) { + outer_->IncomingStanza(stanza); + } + virtual void EndStream() { + outer_->IncomingEnd(false); + } + virtual void XmlError() { + outer_->IncomingEnd(true); + } + + private: + XmppEngineImpl* const outer_; + }; + + class EnterExit { + public: + EnterExit(XmppEngineImpl* engine); + ~EnterExit(); + private: + XmppEngineImpl* engine_; + State state_; + Error error_; + + }; + + friend class StanzaParseHandler; + friend class EnterExit; + + StanzaParseHandler stanza_parse_handler_; + XmppStanzaParser stanza_parser_; + + // state + int engine_entered_; + Jid user_jid_; + std::string password_; + std::string requested_resource_; + TlsOptions tls_option_; + std::string tls_server_hostname_; + std::string tls_server_domain_; + talk_base::scoped_ptr login_task_; + std::string lang_; + + int next_id_; + Jid bound_jid_; + State state_; + bool encrypted_; + Error error_code_; + int subcode_; + talk_base::scoped_ptr stream_error_; + bool raised_reset_; + XmppOutputHandler* output_handler_; + XmppSessionHandler* session_handler_; + + XmlnsStack xmlns_stack_; + + typedef std::vector StanzaHandlerVector; + talk_base::scoped_ptr stanza_handlers_[HL_COUNT]; + + typedef std::vector IqEntryVector; + talk_base::scoped_ptr iq_entries_; + + talk_base::scoped_ptr sasl_handler_; + + talk_base::scoped_ptr output_; +}; + +} // namespace buzz + +#endif // TALK_XMPP_XMPPENGINEIMPL_H_ diff --git a/talk/xmpp/xmppengineimpl_iq.cc b/talk/xmpp/xmppengineimpl_iq.cc new file mode 100644 index 000000000..5834b90d4 --- /dev/null +++ b/talk/xmpp/xmppengineimpl_iq.cc @@ -0,0 +1,277 @@ +/* + * libjingle + * Copyright 2004--2005, 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 +#include +#include "talk/base/common.h" +#include "talk/xmpp/xmppengineimpl.h" +#include "talk/xmpp/constants.h" + +namespace buzz { + +class XmppIqEntry { + XmppIqEntry(const std::string & id, const std::string & to, + XmppEngine * pxce, XmppIqHandler * iq_handler) : + id_(id), + to_(to), + engine_(pxce), + iq_handler_(iq_handler) { + } + +private: + friend class XmppEngineImpl; + + const std::string id_; + const std::string to_; + XmppEngine * const engine_; + XmppIqHandler * const iq_handler_; +}; + + +XmppReturnStatus +XmppEngineImpl::SendIq(const XmlElement * element, XmppIqHandler * iq_handler, + XmppIqCookie* cookie) { + if (state_ == STATE_CLOSED) + return XMPP_RETURN_BADSTATE; + if (NULL == iq_handler) + return XMPP_RETURN_BADARGUMENT; + if (!element || element->Name() != QN_IQ) + return XMPP_RETURN_BADARGUMENT; + + const std::string& type = element->Attr(QN_TYPE); + if (type != "get" && type != "set") + return XMPP_RETURN_BADARGUMENT; + + if (!element->HasAttr(QN_ID)) + return XMPP_RETURN_BADARGUMENT; + const std::string& id = element->Attr(QN_ID); + + XmppIqEntry * iq_entry = new XmppIqEntry(id, + element->Attr(QN_TO), + this, iq_handler); + iq_entries_->push_back(iq_entry); + SendStanza(element); + + if (cookie) + *cookie = iq_entry; + + return XMPP_RETURN_OK; +} + + +XmppReturnStatus +XmppEngineImpl::RemoveIqHandler(XmppIqCookie cookie, + XmppIqHandler ** iq_handler) { + + std::vector >::iterator pos; + + pos = std::find(iq_entries_->begin(), + iq_entries_->end(), + reinterpret_cast(cookie)); + + if (pos == iq_entries_->end()) + return XMPP_RETURN_BADARGUMENT; + + XmppIqEntry* entry = *pos; + iq_entries_->erase(pos); + if (iq_handler) + *iq_handler = entry->iq_handler_; + delete entry; + + return XMPP_RETURN_OK; +} + +void +XmppEngineImpl::DeleteIqCookies() { + for (size_t i = 0; i < iq_entries_->size(); i += 1) { + XmppIqEntry * iq_entry_ = (*iq_entries_)[i]; + (*iq_entries_)[i] = NULL; + delete iq_entry_; + } + iq_entries_->clear(); +} + +static void +AecImpl(XmlElement * error_element, const QName & name, + const char * type, const char * code) { + error_element->AddElement(new XmlElement(QN_ERROR)); + error_element->AddAttr(QN_CODE, code, 1); + error_element->AddAttr(QN_TYPE, type, 1); + error_element->AddElement(new XmlElement(name, true), 1); +} + + +static void +AddErrorCode(XmlElement * error_element, XmppStanzaError code) { + switch (code) { + case XSE_BAD_REQUEST: + AecImpl(error_element, QN_STANZA_BAD_REQUEST, "modify", "400"); + break; + case XSE_CONFLICT: + AecImpl(error_element, QN_STANZA_CONFLICT, "cancel", "409"); + break; + case XSE_FEATURE_NOT_IMPLEMENTED: + AecImpl(error_element, QN_STANZA_FEATURE_NOT_IMPLEMENTED, + "cancel", "501"); + break; + case XSE_FORBIDDEN: + AecImpl(error_element, QN_STANZA_FORBIDDEN, "auth", "403"); + break; + case XSE_GONE: + AecImpl(error_element, QN_STANZA_GONE, "modify", "302"); + break; + case XSE_INTERNAL_SERVER_ERROR: + AecImpl(error_element, QN_STANZA_INTERNAL_SERVER_ERROR, "wait", "500"); + break; + case XSE_ITEM_NOT_FOUND: + AecImpl(error_element, QN_STANZA_ITEM_NOT_FOUND, "cancel", "404"); + break; + case XSE_JID_MALFORMED: + AecImpl(error_element, QN_STANZA_JID_MALFORMED, "modify", "400"); + break; + case XSE_NOT_ACCEPTABLE: + AecImpl(error_element, QN_STANZA_NOT_ACCEPTABLE, "cancel", "406"); + break; + case XSE_NOT_ALLOWED: + AecImpl(error_element, QN_STANZA_NOT_ALLOWED, "cancel", "405"); + break; + case XSE_PAYMENT_REQUIRED: + AecImpl(error_element, QN_STANZA_PAYMENT_REQUIRED, "auth", "402"); + break; + case XSE_RECIPIENT_UNAVAILABLE: + AecImpl(error_element, QN_STANZA_RECIPIENT_UNAVAILABLE, "wait", "404"); + break; + case XSE_REDIRECT: + AecImpl(error_element, QN_STANZA_REDIRECT, "modify", "302"); + break; + case XSE_REGISTRATION_REQUIRED: + AecImpl(error_element, QN_STANZA_REGISTRATION_REQUIRED, "auth", "407"); + break; + case XSE_SERVER_NOT_FOUND: + AecImpl(error_element, QN_STANZA_REMOTE_SERVER_NOT_FOUND, + "cancel", "404"); + break; + case XSE_SERVER_TIMEOUT: + AecImpl(error_element, QN_STANZA_REMOTE_SERVER_TIMEOUT, "wait", "502"); + break; + case XSE_RESOURCE_CONSTRAINT: + AecImpl(error_element, QN_STANZA_RESOURCE_CONSTRAINT, "wait", "500"); + break; + case XSE_SERVICE_UNAVAILABLE: + AecImpl(error_element, QN_STANZA_SERVICE_UNAVAILABLE, "cancel", "503"); + break; + case XSE_SUBSCRIPTION_REQUIRED: + AecImpl(error_element, QN_STANZA_SUBSCRIPTION_REQUIRED, "auth", "407"); + break; + case XSE_UNDEFINED_CONDITION: + AecImpl(error_element, QN_STANZA_UNDEFINED_CONDITION, "wait", "500"); + break; + case XSE_UNEXPECTED_REQUEST: + AecImpl(error_element, QN_STANZA_UNEXPECTED_REQUEST, "wait", "400"); + break; + } +} + + +XmppReturnStatus +XmppEngineImpl::SendStanzaError(const XmlElement * element_original, + XmppStanzaError code, + const std::string & text) { + + if (state_ == STATE_CLOSED) + return XMPP_RETURN_BADSTATE; + + XmlElement error_element(element_original->Name()); + error_element.AddAttr(QN_TYPE, "error"); + + // copy attrs, copy 'from' to 'to' and strip 'from' + for (const XmlAttr * attribute = element_original->FirstAttr(); + attribute; attribute = attribute->NextAttr()) { + QName name = attribute->Name(); + if (name == QN_TO) + continue; // no need to put a from attr. Server will stamp stanza + else if (name == QN_FROM) + name = QN_TO; + else if (name == QN_TYPE) + continue; + error_element.AddAttr(name, attribute->Value()); + } + + // copy children + for (const XmlChild * child = element_original->FirstChild(); + child; + child = child->NextChild()) { + if (child->IsText()) { + error_element.AddText(child->AsText()->Text()); + } else { + error_element.AddElement(new XmlElement(*(child->AsElement()))); + } + } + + // add error information + AddErrorCode(&error_element, code); + if (text != STR_EMPTY) { + XmlElement * text_element = new XmlElement(QN_STANZA_TEXT, true); + text_element->AddText(text); + error_element.AddElement(text_element); + } + + SendStanza(&error_element); + + return XMPP_RETURN_OK; +} + + +bool +XmppEngineImpl::HandleIqResponse(const XmlElement * element) { + if (iq_entries_->empty()) + return false; + if (element->Name() != QN_IQ) + return false; + std::string type = element->Attr(QN_TYPE); + if (type != "result" && type != "error") + return false; + if (!element->HasAttr(QN_ID)) + return false; + std::string id = element->Attr(QN_ID); + std::string from = element->Attr(QN_FROM); + + for (std::vector::iterator it = iq_entries_->begin(); + it != iq_entries_->end(); it += 1) { + XmppIqEntry * iq_entry = *it; + if (iq_entry->id_ == id && iq_entry->to_ == from) { + iq_entries_->erase(it); + iq_entry->iq_handler_->IqResponse(iq_entry, element); + delete iq_entry; + return true; + } + } + + return false; +} + +} diff --git a/talk/xmpp/xmpplogintask.cc b/talk/xmpp/xmpplogintask.cc new file mode 100644 index 000000000..eec943bec --- /dev/null +++ b/talk/xmpp/xmpplogintask.cc @@ -0,0 +1,397 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmpp/xmpplogintask.h" + +#include +#include + +#include "talk/base/base64.h" +#include "talk/base/common.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/jid.h" +#include "talk/xmpp/saslmechanism.h" +#include "talk/xmpp/xmppengineimpl.h" + +using talk_base::ConstantLabel; + +namespace buzz { + +#ifdef _DEBUG +const ConstantLabel XmppLoginTask::LOGINTASK_STATES[] = { + KLABEL(LOGINSTATE_INIT), + KLABEL(LOGINSTATE_STREAMSTART_SENT), + KLABEL(LOGINSTATE_STARTED_XMPP), + KLABEL(LOGINSTATE_TLS_INIT), + KLABEL(LOGINSTATE_AUTH_INIT), + KLABEL(LOGINSTATE_BIND_INIT), + KLABEL(LOGINSTATE_TLS_REQUESTED), + KLABEL(LOGINSTATE_SASL_RUNNING), + KLABEL(LOGINSTATE_BIND_REQUESTED), + KLABEL(LOGINSTATE_SESSION_REQUESTED), + KLABEL(LOGINSTATE_DONE), + LASTLABEL +}; +#endif // _DEBUG +XmppLoginTask::XmppLoginTask(XmppEngineImpl * pctx) : + pctx_(pctx), + authNeeded_(true), + allowNonGoogleLogin_(true), + state_(LOGINSTATE_INIT), + pelStanza_(NULL), + isStart_(false), + iqId_(STR_EMPTY), + pelFeatures_(NULL), + fullJid_(STR_EMPTY), + streamId_(STR_EMPTY), + pvecQueuedStanzas_(new std::vector()), + sasl_mech_(NULL) { +} + +XmppLoginTask::~XmppLoginTask() { + for (size_t i = 0; i < pvecQueuedStanzas_->size(); i += 1) + delete (*pvecQueuedStanzas_)[i]; +} + +void +XmppLoginTask::IncomingStanza(const XmlElement *element, bool isStart) { + pelStanza_ = element; + isStart_ = isStart; + Advance(); + pelStanza_ = NULL; + isStart_ = false; +} + +const XmlElement * +XmppLoginTask::NextStanza() { + const XmlElement * result = pelStanza_; + pelStanza_ = NULL; + return result; +} + +bool +XmppLoginTask::Advance() { + + for (;;) { + + const XmlElement * element = NULL; + +#if _DEBUG + LOG(LS_VERBOSE) << "XmppLoginTask::Advance - " + << talk_base::ErrorName(state_, LOGINTASK_STATES); +#endif // _DEBUG + + switch (state_) { + + case LOGINSTATE_INIT: { + pctx_->RaiseReset(); + pelFeatures_.reset(NULL); + + // The proper domain to verify against is the real underlying + // domain - i.e., the domain that owns the JID. Our XmppEngineImpl + // also allows matching against a proxy domain instead, if it is told + // to do so - see the implementation of XmppEngineImpl::StartTls and + // XmppEngine::SetTlsServerDomain to see how you can use that feature + pctx_->InternalSendStart(pctx_->user_jid_.domain()); + state_ = LOGINSTATE_STREAMSTART_SENT; + break; + } + + case LOGINSTATE_STREAMSTART_SENT: { + if (NULL == (element = NextStanza())) + return true; + + if (!isStart_ || !HandleStartStream(element)) + return Failure(XmppEngine::ERROR_VERSION); + + state_ = LOGINSTATE_STARTED_XMPP; + return true; + } + + case LOGINSTATE_STARTED_XMPP: { + if (NULL == (element = NextStanza())) + return true; + + if (!HandleFeatures(element)) + return Failure(XmppEngine::ERROR_VERSION); + + bool tls_present = (GetFeature(QN_TLS_STARTTLS) != NULL); + // Error if TLS required but not present. + if (pctx_->tls_option_ == buzz::TLS_REQUIRED && !tls_present) { + return Failure(XmppEngine::ERROR_TLS); + } + // Use TLS if required or enabled, and also available + if ((pctx_->tls_option_ == buzz::TLS_REQUIRED || + pctx_->tls_option_ == buzz::TLS_ENABLED) && tls_present) { + state_ = LOGINSTATE_TLS_INIT; + continue; + } + + if (authNeeded_) { + state_ = LOGINSTATE_AUTH_INIT; + continue; + } + + state_ = LOGINSTATE_BIND_INIT; + continue; + } + + case LOGINSTATE_TLS_INIT: { + const XmlElement * pelTls = GetFeature(QN_TLS_STARTTLS); + if (!pelTls) + return Failure(XmppEngine::ERROR_TLS); + + XmlElement el(QN_TLS_STARTTLS, true); + pctx_->InternalSendStanza(&el); + state_ = LOGINSTATE_TLS_REQUESTED; + continue; + } + + case LOGINSTATE_TLS_REQUESTED: { + if (NULL == (element = NextStanza())) + return true; + if (element->Name() != QN_TLS_PROCEED) + return Failure(XmppEngine::ERROR_TLS); + + // The proper domain to verify against is the real underlying + // domain - i.e., the domain that owns the JID. Our XmppEngineImpl + // also allows matching against a proxy domain instead, if it is told + // to do so - see the implementation of XmppEngineImpl::StartTls and + // XmppEngine::SetTlsServerDomain to see how you can use that feature + pctx_->StartTls(pctx_->user_jid_.domain()); + pctx_->tls_option_ = buzz::TLS_ENABLED; + state_ = LOGINSTATE_INIT; + continue; + } + + case LOGINSTATE_AUTH_INIT: { + const XmlElement * pelSaslAuth = GetFeature(QN_SASL_MECHANISMS); + if (!pelSaslAuth) { + return Failure(XmppEngine::ERROR_AUTH); + } + + // Collect together the SASL auth mechanisms presented by the server + std::vector mechanisms; + for (const XmlElement * pelMech = + pelSaslAuth->FirstNamed(QN_SASL_MECHANISM); + pelMech; + pelMech = pelMech->NextNamed(QN_SASL_MECHANISM)) { + + mechanisms.push_back(pelMech->BodyText()); + } + + // Given all the mechanisms, choose the best + std::string choice(pctx_->ChooseBestSaslMechanism(mechanisms, pctx_->IsEncrypted())); + if (choice.empty()) { + return Failure(XmppEngine::ERROR_AUTH); + } + + // No recognized auth mechanism - that's an error + sasl_mech_.reset(pctx_->GetSaslMechanism(choice)); + if (!sasl_mech_) { + return Failure(XmppEngine::ERROR_AUTH); + } + + // OK, let's start it. + XmlElement * auth = sasl_mech_->StartSaslAuth(); + if (auth == NULL) { + return Failure(XmppEngine::ERROR_AUTH); + } + if (allowNonGoogleLogin_) { + // Setting the following two attributes is required to support + // non-google ids. + + // Allow login with non-google id accounts. + auth->SetAttr(QN_GOOGLE_ALLOW_NON_GOOGLE_ID_XMPP_LOGIN, "true"); + + // Allow login with either the non-google id or the friendly email. + auth->SetAttr(QN_GOOGLE_AUTH_CLIENT_USES_FULL_BIND_RESULT, "true"); + } + + pctx_->InternalSendStanza(auth); + delete auth; + state_ = LOGINSTATE_SASL_RUNNING; + continue; + } + + case LOGINSTATE_SASL_RUNNING: { + if (NULL == (element = NextStanza())) + return true; + if (element->Name().Namespace() != NS_SASL) + return Failure(XmppEngine::ERROR_AUTH); + if (element->Name() == QN_SASL_CHALLENGE) { + XmlElement * response = sasl_mech_->HandleSaslChallenge(element); + if (response == NULL) { + return Failure(XmppEngine::ERROR_AUTH); + } + pctx_->InternalSendStanza(response); + delete response; + state_ = LOGINSTATE_SASL_RUNNING; + continue; + } + if (element->Name() != QN_SASL_SUCCESS) { + return Failure(XmppEngine::ERROR_UNAUTHORIZED); + } + + // Authenticated! + authNeeded_ = false; + state_ = LOGINSTATE_INIT; + continue; + } + + case LOGINSTATE_BIND_INIT: { + const XmlElement * pelBindFeature = GetFeature(QN_BIND_BIND); + const XmlElement * pelSessionFeature = GetFeature(QN_SESSION_SESSION); + if (!pelBindFeature || !pelSessionFeature) + return Failure(XmppEngine::ERROR_BIND); + + XmlElement iq(QN_IQ); + iq.AddAttr(QN_TYPE, "set"); + + iqId_ = pctx_->NextId(); + iq.AddAttr(QN_ID, iqId_); + iq.AddElement(new XmlElement(QN_BIND_BIND, true)); + + if (pctx_->requested_resource_ != STR_EMPTY) { + iq.AddElement(new XmlElement(QN_BIND_RESOURCE), 1); + iq.AddText(pctx_->requested_resource_, 2); + } + pctx_->InternalSendStanza(&iq); + state_ = LOGINSTATE_BIND_REQUESTED; + continue; + } + + case LOGINSTATE_BIND_REQUESTED: { + if (NULL == (element = NextStanza())) + return true; + + if (element->Name() != QN_IQ || element->Attr(QN_ID) != iqId_ || + element->Attr(QN_TYPE) == "get" || element->Attr(QN_TYPE) == "set") + return true; + + if (element->Attr(QN_TYPE) != "result" || element->FirstElement() == NULL || + element->FirstElement()->Name() != QN_BIND_BIND) + return Failure(XmppEngine::ERROR_BIND); + + fullJid_ = Jid(element->FirstElement()->TextNamed(QN_BIND_JID)); + if (!fullJid_.IsFull()) { + return Failure(XmppEngine::ERROR_BIND); + } + + // now request session + XmlElement iq(QN_IQ); + iq.AddAttr(QN_TYPE, "set"); + + iqId_ = pctx_->NextId(); + iq.AddAttr(QN_ID, iqId_); + iq.AddElement(new XmlElement(QN_SESSION_SESSION, true)); + pctx_->InternalSendStanza(&iq); + + state_ = LOGINSTATE_SESSION_REQUESTED; + continue; + } + + case LOGINSTATE_SESSION_REQUESTED: { + if (NULL == (element = NextStanza())) + return true; + if (element->Name() != QN_IQ || element->Attr(QN_ID) != iqId_ || + element->Attr(QN_TYPE) == "get" || element->Attr(QN_TYPE) == "set") + return false; + + if (element->Attr(QN_TYPE) != "result") + return Failure(XmppEngine::ERROR_BIND); + + pctx_->SignalBound(fullJid_); + FlushQueuedStanzas(); + state_ = LOGINSTATE_DONE; + return true; + } + + case LOGINSTATE_DONE: + return false; + } + } +} + +bool +XmppLoginTask::HandleStartStream(const XmlElement *element) { + + if (element->Name() != QN_STREAM_STREAM) + return false; + + if (element->Attr(QN_XMLNS) != "jabber:client") + return false; + + if (element->Attr(QN_VERSION) != "1.0") + return false; + + if (!element->HasAttr(QN_ID)) + return false; + + streamId_ = element->Attr(QN_ID); + + return true; +} + +bool +XmppLoginTask::HandleFeatures(const XmlElement *element) { + if (element->Name() != QN_STREAM_FEATURES) + return false; + + pelFeatures_.reset(new XmlElement(*element)); + return true; +} + +const XmlElement * +XmppLoginTask::GetFeature(const QName & name) { + return pelFeatures_->FirstNamed(name); +} + +bool +XmppLoginTask::Failure(XmppEngine::Error reason) { + state_ = LOGINSTATE_DONE; + pctx_->SignalError(reason, 0); + return false; +} + +void +XmppLoginTask::OutgoingStanza(const XmlElement * element) { + XmlElement * pelCopy = new XmlElement(*element); + pvecQueuedStanzas_->push_back(pelCopy); +} + +void +XmppLoginTask::FlushQueuedStanzas() { + for (size_t i = 0; i < pvecQueuedStanzas_->size(); i += 1) { + pctx_->InternalSendStanza((*pvecQueuedStanzas_)[i]); + delete (*pvecQueuedStanzas_)[i]; + } + pvecQueuedStanzas_->clear(); +} + +} diff --git a/talk/xmpp/xmpplogintask.h b/talk/xmpp/xmpplogintask.h new file mode 100644 index 000000000..9b3f5aec7 --- /dev/null +++ b/talk/xmpp/xmpplogintask.h @@ -0,0 +1,104 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_LOGINTASK_H_ +#define TALK_XMPP_LOGINTASK_H_ + +#include +#include + +#include "talk/base/logging.h" +#include "talk/base/scoped_ptr.h" +#include "talk/xmpp/jid.h" +#include "talk/xmpp/xmppengine.h" + +namespace buzz { + +class XmlElement; +class XmppEngineImpl; +class SaslMechanism; + + +// TODO: Rename to LoginTask. +class XmppLoginTask { + +public: + XmppLoginTask(XmppEngineImpl *pctx); + ~XmppLoginTask(); + + bool IsDone() + { return state_ == LOGINSTATE_DONE; } + void IncomingStanza(const XmlElement * element, bool isStart); + void OutgoingStanza(const XmlElement *element); + void set_allow_non_google_login(bool b) + { allowNonGoogleLogin_ = b; } + +private: + enum LoginTaskState { + LOGINSTATE_INIT = 0, + LOGINSTATE_STREAMSTART_SENT, + LOGINSTATE_STARTED_XMPP, + LOGINSTATE_TLS_INIT, + LOGINSTATE_AUTH_INIT, + LOGINSTATE_BIND_INIT, + LOGINSTATE_TLS_REQUESTED, + LOGINSTATE_SASL_RUNNING, + LOGINSTATE_BIND_REQUESTED, + LOGINSTATE_SESSION_REQUESTED, + LOGINSTATE_DONE, + }; + + const XmlElement * NextStanza(); + bool Advance(); + bool HandleStartStream(const XmlElement * element); + bool HandleFeatures(const XmlElement * element); + const XmlElement * GetFeature(const QName & name); + bool Failure(XmppEngine::Error reason); + void FlushQueuedStanzas(); + + XmppEngineImpl * pctx_; + bool authNeeded_; + bool allowNonGoogleLogin_; + LoginTaskState state_; + const XmlElement * pelStanza_; + bool isStart_; + std::string iqId_; + talk_base::scoped_ptr pelFeatures_; + Jid fullJid_; + std::string streamId_; + talk_base::scoped_ptr > pvecQueuedStanzas_; + + talk_base::scoped_ptr sasl_mech_; + +#ifdef _DEBUG + static const talk_base::ConstantLabel LOGINTASK_STATES[]; +#endif // _DEBUG +}; + +} + +#endif // TALK_XMPP_LOGINTASK_H_ diff --git a/talk/xmpp/xmpplogintask_unittest.cc b/talk/xmpp/xmpplogintask_unittest.cc new file mode 100644 index 000000000..51af81a03 --- /dev/null +++ b/talk/xmpp/xmpplogintask_unittest.cc @@ -0,0 +1,614 @@ +// Copyright 2004 Google Inc. All Rights Reserved + + +#include +#include +#include +#include "talk/base/common.h" +#include "talk/base/cryptstring.h" +#include "talk/base/gunit.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/util_unittest.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/saslplainmechanism.h" +#include "talk/xmpp/plainsaslhandler.h" +#include "talk/xmpp/xmppengine.h" + +using buzz::Jid; +using buzz::QName; +using buzz::XmlElement; +using buzz::XmppEngine; +using buzz::XmppTestHandler; + +enum XlttStage { + XLTT_STAGE_CONNECT = 0, + XLTT_STAGE_STREAMSTART, + XLTT_STAGE_TLS_FEATURES, + XLTT_STAGE_TLS_PROCEED, + XLTT_STAGE_ENCRYPTED_START, + XLTT_STAGE_AUTH_FEATURES, + XLTT_STAGE_AUTH_SUCCESS, + XLTT_STAGE_AUTHENTICATED_START, + XLTT_STAGE_BIND_FEATURES, + XLTT_STAGE_BIND_SUCCESS, + XLTT_STAGE_SESSION_SUCCESS, +}; + +class XmppLoginTaskTest : public testing::Test { + public: + XmppEngine* engine() { return engine_.get(); } + XmppTestHandler* handler() { return handler_.get(); } + virtual void SetUp() { + engine_.reset(XmppEngine::Create()); + handler_.reset(new XmppTestHandler(engine_.get())); + + Jid jid("david@my-server"); + talk_base::InsecureCryptStringImpl pass; + pass.password() = "david"; + engine_->SetSessionHandler(handler_.get()); + engine_->SetOutputHandler(handler_.get()); + engine_->AddStanzaHandler(handler_.get()); + engine_->SetUser(jid); + engine_->SetSaslHandler( + new buzz::PlainSaslHandler(jid, talk_base::CryptString(pass), true)); + } + virtual void TearDown() { + handler_.reset(); + engine_.reset(); + } + void RunPartialLogin(XlttStage startstage, XlttStage endstage); + void SetTlsOptions(buzz::TlsOptions option); + + private: + talk_base::scoped_ptr engine_; + talk_base::scoped_ptr handler_; +}; + +void XmppLoginTaskTest::SetTlsOptions(buzz::TlsOptions option) { + engine_->SetTls(option); +} +void XmppLoginTaskTest::RunPartialLogin(XlttStage startstage, + XlttStage endstage) { + std::string input; + + switch (startstage) { + case XLTT_STAGE_CONNECT: { + engine_->Connect(); + XmlElement appStanza(QName("test", "app-stanza")); + appStanza.AddText("this-is-a-test"); + engine_->SendStanza(&appStanza); + + EXPECT_EQ("\r\n", handler_->OutputActivity()); + EXPECT_EQ("[OPENING]", handler_->SessionActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + if (endstage == XLTT_STAGE_CONNECT) + return; + } + + case XLTT_STAGE_STREAMSTART: { + input = ""; + engine_->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("", handler_->StanzaActivity()); + EXPECT_EQ("", handler_->SessionActivity()); + EXPECT_EQ("", handler_->OutputActivity()); + if (endstage == XLTT_STAGE_STREAMSTART) + return; + } + + case XLTT_STAGE_TLS_FEATURES: { + input = "" + "" + ""; + engine_->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("", + handler_->OutputActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + EXPECT_EQ("", handler_->SessionActivity()); + if (endstage == XLTT_STAGE_TLS_FEATURES) + return; + } + + case XLTT_STAGE_TLS_PROCEED: { + input = std::string(""); + engine_->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("[START-TLS my-server]" + "\r\n", handler_->OutputActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + EXPECT_EQ("", handler_->SessionActivity()); + if (endstage == XLTT_STAGE_TLS_PROCEED) + return; + } + + case XLTT_STAGE_ENCRYPTED_START: { + input = std::string(""); + engine_->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("", handler_->StanzaActivity()); + EXPECT_EQ("", handler_->SessionActivity()); + EXPECT_EQ("", handler_->OutputActivity()); + if (endstage == XLTT_STAGE_ENCRYPTED_START) + return; + } + + case XLTT_STAGE_AUTH_FEATURES: { + input = "" + "" + "DIGEST-MD5" + "PLAIN" + "" + ""; + engine_->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("AGRhdmlkAGRhdmlk", + handler_->OutputActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + EXPECT_EQ("", handler_->SessionActivity()); + if (endstage == XLTT_STAGE_AUTH_FEATURES) + return; + } + + case XLTT_STAGE_AUTH_SUCCESS: { + input = ""; + engine_->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("\r\n", handler_->OutputActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + EXPECT_EQ("", handler_->SessionActivity()); + if (endstage == XLTT_STAGE_AUTH_SUCCESS) + return; + } + + case XLTT_STAGE_AUTHENTICATED_START: { + input = std::string(""); + engine_->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("", handler_->StanzaActivity()); + EXPECT_EQ("", handler_->SessionActivity()); + EXPECT_EQ("", handler_->OutputActivity()); + if (endstage == XLTT_STAGE_AUTHENTICATED_START) + return; + } + + case XLTT_STAGE_BIND_FEATURES: { + input = "" + "" + "" + ""; + engine_->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("" + "", + handler_->OutputActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + EXPECT_EQ("", handler_->SessionActivity()); + if (endstage == XLTT_STAGE_BIND_FEATURES) + return; + } + + case XLTT_STAGE_BIND_SUCCESS: { + input = "" + "" + "david@my-server/test"; + engine_->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("" + "", + handler_->OutputActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + EXPECT_EQ("", handler_->SessionActivity()); + if (endstage == XLTT_STAGE_BIND_SUCCESS) + return; + } + + case XLTT_STAGE_SESSION_SUCCESS: { + input = ""; + engine_->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("this-is-a-test" + "", handler_->OutputActivity()); + EXPECT_EQ("[OPEN]", handler_->SessionActivity()); + EXPECT_EQ("", handler_->StanzaActivity()); + if (endstage == XLTT_STAGE_SESSION_SUCCESS) + return; + } + } +} + +TEST_F(XmppLoginTaskTest, TestUtf8Good) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_CONNECT); + + std::string input = "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("", handler()->OutputActivity()); + EXPECT_EQ("", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestNonUtf8Bad) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_CONNECT); + + std::string input = "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-XML]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestNoFeatures) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_STREAMSTART); + + std::string input = ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-VERSION]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestTlsRequiredNotPresent) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_STREAMSTART); + + std::string input = "" + "" + "DIGEST-MD5" + "PLAIN" + "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-TLS]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestTlsRequeiredAndPresent) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_STREAMSTART); + + std::string input = "" + "" + "" + "" + "" + "X-GOOGLE-TOKEN" + "PLAIN" + "X-OAUTH2" + "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("", + handler()->OutputActivity()); + EXPECT_EQ("", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestTlsEnabledNotPresent) { + SetTlsOptions(buzz::TLS_ENABLED); + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_STREAMSTART); + + std::string input = "" + "" + "DIGEST-MD5" + "PLAIN" + "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("AGRhdmlkAGRhdmlk", handler()->OutputActivity()); + EXPECT_EQ("", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestTlsEnabledAndPresent) { + SetTlsOptions(buzz::TLS_ENABLED); + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_STREAMSTART); + + std::string input = "" + "" + "X-GOOGLE-TOKEN" + "PLAIN" + "X-OAUTH2" + "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("AGRhdmlkAGRhdmlk", handler()->OutputActivity()); + EXPECT_EQ("", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestTlsDisabledNotPresent) { + SetTlsOptions(buzz::TLS_DISABLED); + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_STREAMSTART); + + std::string input = "" + "" + "DIGEST-MD5" + "PLAIN" + "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("AGRhdmlkAGRhdmlk", handler()->OutputActivity()); + EXPECT_EQ("", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestTlsDisabledAndPresent) { + SetTlsOptions(buzz::TLS_DISABLED); + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_STREAMSTART); + + std::string input = "" + "" + "X-GOOGLE-TOKEN" + "PLAIN" + "X-OAUTH2" + "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("AGRhdmlkAGRhdmlk", handler()->OutputActivity()); + EXPECT_EQ("", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestTlsFailure) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_TLS_FEATURES); + + std::string input = ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-TLS]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestTlsBadStream) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_TLS_PROCEED); + + std::string input = ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-VERSION]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestMissingSaslPlain) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_ENCRYPTED_START); + + std::string input = "" + "" + "DIGEST-MD5" + "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-AUTH]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestWrongPassword) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_AUTH_FEATURES); + + std::string input = ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-UNAUTHORIZED]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestAuthBadStream) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_AUTH_SUCCESS); + + std::string input = ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-VERSION]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestMissingBindFeature) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_AUTHENTICATED_START); + + std::string input = "" + "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-BIND]", handler()->SessionActivity()); +} + +TEST_F(XmppLoginTaskTest, TestMissingSessionFeature) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_AUTHENTICATED_START); + + std::string input = "" + "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-BIND]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +/* TODO: Handle this case properly inside XmppLoginTask. +TEST_F(XmppLoginTaskTest, TestBindFailure1) { + // check wrong JID + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_BIND_FEATURES); + + std::string input = "" + "" + "davey@my-server/test"; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-BIND]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} +*/ + +TEST_F(XmppLoginTaskTest, TestBindFailure2) { + // check missing JID + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_BIND_FEATURES); + + std::string input = "" + ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-BIND]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestBindFailure3) { + // check plain failure + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_BIND_FEATURES); + + std::string input = ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-BIND]", handler()->SessionActivity()); + EXPECT_EQ("", handler()->StanzaActivity()); +} + +TEST_F(XmppLoginTaskTest, TestBindFailure4) { + // check wrong id to ignore + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_BIND_FEATURES); + + std::string input = ""; + engine()->HandleInput(input.c_str(), input.length()); + + // continue after an ignored iq + RunPartialLogin(XLTT_STAGE_BIND_SUCCESS, XLTT_STAGE_SESSION_SUCCESS); +} + +TEST_F(XmppLoginTaskTest, TestSessionFailurePlain1) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_BIND_SUCCESS); + + std::string input = ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-BIND]", handler()->SessionActivity()); +} + +TEST_F(XmppLoginTaskTest, TestSessionFailurePlain2) { + RunPartialLogin(XLTT_STAGE_CONNECT, XLTT_STAGE_BIND_SUCCESS); + + // check reverse iq to ignore + // TODO: consider queueing or passing through? + std::string input = ""; + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("", handler()->OutputActivity()); + EXPECT_EQ("", handler()->SessionActivity()); + + // continue after an ignored iq + RunPartialLogin(XLTT_STAGE_SESSION_SUCCESS, XLTT_STAGE_SESSION_SUCCESS); +} + +TEST_F(XmppLoginTaskTest, TestBadXml) { + int errorKind = 0; + for (XlttStage stage = XLTT_STAGE_CONNECT; + stage <= XLTT_STAGE_SESSION_SUCCESS; + stage = static_cast(stage + 1)) { + RunPartialLogin(XLTT_STAGE_CONNECT, stage); + + std::string input; + switch (errorKind++ % 5) { + case 0: input = "&syntax;"; break; + case 1: input = ""; break; + case 2: input = ""; break; + case 3: input = "<>"; break; + case 4: input = ""; break; + } + + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-XML]", handler()->SessionActivity()); + + TearDown(); + SetUp(); + } +} + +TEST_F(XmppLoginTaskTest, TestStreamError) { + for (XlttStage stage = XLTT_STAGE_CONNECT; + stage <= XLTT_STAGE_SESSION_SUCCESS; + stage = static_cast(stage + 1)) { + switch (stage) { + case XLTT_STAGE_CONNECT: + case XLTT_STAGE_TLS_PROCEED: + case XLTT_STAGE_AUTH_SUCCESS: + continue; + default: + break; + } + + RunPartialLogin(XLTT_STAGE_CONNECT, stage); + + std::string input = "" + "" + "" + "Some special application diagnostic information!" + "" + "" + ""; + + engine()->HandleInput(input.c_str(), input.length()); + + EXPECT_EQ("[CLOSED]", handler()->OutputActivity()); + EXPECT_EQ("[CLOSED][ERROR-STREAM]", handler()->SessionActivity()); + + EXPECT_EQ("" + "" + "" + "Some special application diagnostic information!" + "" + "" + "", engine()->GetStreamError()->Str()); + + TearDown(); + SetUp(); + } +} + diff --git a/talk/xmpp/xmpppump.cc b/talk/xmpp/xmpppump.cc new file mode 100644 index 000000000..57329861a --- /dev/null +++ b/talk/xmpp/xmpppump.cc @@ -0,0 +1,84 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmpp/xmpppump.h" + +#include "talk/xmpp/xmppauth.h" + +namespace buzz { + +XmppPump::XmppPump(XmppPumpNotify * notify) { + state_ = buzz::XmppEngine::STATE_NONE; + notify_ = notify; + client_ = new buzz::XmppClient(this); // NOTE: deleted by TaskRunner +} + +void XmppPump::DoLogin(const buzz::XmppClientSettings & xcs, + buzz::AsyncSocket* socket, + buzz::PreXmppAuth* auth) { + OnStateChange(buzz::XmppEngine::STATE_START); + if (!AllChildrenDone()) { + client_->SignalStateChange.connect(this, &XmppPump::OnStateChange); + client_->Connect(xcs, "", socket, auth); + client_->Start(); + } +} + +void XmppPump::DoDisconnect() { + if (!AllChildrenDone()) + client_->Disconnect(); + OnStateChange(buzz::XmppEngine::STATE_CLOSED); +} + +void XmppPump::OnStateChange(buzz::XmppEngine::State state) { + if (state_ == state) + return; + state_ = state; + if (notify_ != NULL) + notify_->OnStateChange(state); +} + +void XmppPump::WakeTasks() { + talk_base::Thread::Current()->Post(this); +} + +int64 XmppPump::CurrentTime() { + return (int64)talk_base::Time(); +} + +void XmppPump::OnMessage(talk_base::Message *pmsg) { + RunTasks(); +} + +buzz::XmppReturnStatus XmppPump::SendStanza(const buzz::XmlElement *stanza) { + if (!AllChildrenDone()) + return client_->SendStanza(stanza); + return buzz::XMPP_RETURN_BADSTATE; +} + +} // namespace buzz + diff --git a/talk/xmpp/xmpppump.h b/talk/xmpp/xmpppump.h new file mode 100644 index 000000000..7a374cc79 --- /dev/null +++ b/talk/xmpp/xmpppump.h @@ -0,0 +1,79 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_XMPPPUMP_H_ +#define TALK_XMPP_XMPPPUMP_H_ + +#include "talk/base/messagequeue.h" +#include "talk/base/taskrunner.h" +#include "talk/base/thread.h" +#include "talk/base/timeutils.h" +#include "talk/xmpp/xmppclient.h" +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmpptask.h" + +namespace buzz { + +// Simple xmpp pump + +class XmppPumpNotify { +public: + virtual ~XmppPumpNotify() {} + virtual void OnStateChange(buzz::XmppEngine::State state) = 0; +}; + +class XmppPump : public talk_base::MessageHandler, public talk_base::TaskRunner { +public: + XmppPump(buzz::XmppPumpNotify * notify = NULL); + + buzz::XmppClient *client() { return client_; } + + void DoLogin(const buzz::XmppClientSettings & xcs, + buzz::AsyncSocket* socket, + buzz::PreXmppAuth* auth); + void DoDisconnect(); + + void OnStateChange(buzz::XmppEngine::State state); + + void WakeTasks(); + + int64 CurrentTime(); + + void OnMessage(talk_base::Message *pmsg); + + buzz::XmppReturnStatus SendStanza(const buzz::XmlElement *stanza); + +private: + buzz::XmppClient *client_; + buzz::XmppEngine::State state_; + buzz::XmppPumpNotify *notify_; +}; + +} // namespace buzz + +#endif // TALK_XMPP_XMPPPUMP_H_ + diff --git a/talk/xmpp/xmppsocket.cc b/talk/xmpp/xmppsocket.cc new file mode 100644 index 000000000..31d1b69ee --- /dev/null +++ b/talk/xmpp/xmppsocket.cc @@ -0,0 +1,262 @@ +/* + * libjingle + * Copyright 2004--2005, 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 "xmppsocket.h" + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "talk/base/basicdefs.h" +#include "talk/base/logging.h" +#include "talk/base/thread.h" +#ifdef FEATURE_ENABLE_SSL +#include "talk/base/ssladapter.h" +#endif + +#ifdef USE_SSLSTREAM +#include "talk/base/socketstream.h" +#ifdef FEATURE_ENABLE_SSL +#include "talk/base/sslstreamadapter.h" +#endif // FEATURE_ENABLE_SSL +#endif // USE_SSLSTREAM + +namespace buzz { + +XmppSocket::XmppSocket(buzz::TlsOptions tls) : cricket_socket_(NULL), + tls_(tls) { + state_ = buzz::AsyncSocket::STATE_CLOSED; +} + +void XmppSocket::CreateCricketSocket(int family) { + talk_base::Thread* pth = talk_base::Thread::Current(); + if (family == AF_UNSPEC) { + family = AF_INET; + } + talk_base::AsyncSocket* socket = + pth->socketserver()->CreateAsyncSocket(family, SOCK_STREAM); +#ifndef USE_SSLSTREAM +#ifdef FEATURE_ENABLE_SSL + if (tls_ != buzz::TLS_DISABLED) { + socket = talk_base::SSLAdapter::Create(socket); + } +#endif // FEATURE_ENABLE_SSL + cricket_socket_ = socket; + cricket_socket_->SignalReadEvent.connect(this, &XmppSocket::OnReadEvent); + cricket_socket_->SignalWriteEvent.connect(this, &XmppSocket::OnWriteEvent); + cricket_socket_->SignalConnectEvent.connect(this, + &XmppSocket::OnConnectEvent); + cricket_socket_->SignalCloseEvent.connect(this, &XmppSocket::OnCloseEvent); +#else // USE_SSLSTREAM + cricket_socket_ = socket; + stream_ = new talk_base::SocketStream(cricket_socket_); +#ifdef FEATURE_ENABLE_SSL + if (tls_ != buzz::TLS_DISABLED) + stream_ = talk_base::SSLStreamAdapter::Create(stream_); +#endif // FEATURE_ENABLE_SSL + stream_->SignalEvent.connect(this, &XmppSocket::OnEvent); +#endif // USE_SSLSTREAM +} + +XmppSocket::~XmppSocket() { + Close(); +#ifndef USE_SSLSTREAM + delete cricket_socket_; +#else // USE_SSLSTREAM + delete stream_; +#endif // USE_SSLSTREAM +} + +#ifndef USE_SSLSTREAM +void XmppSocket::OnReadEvent(talk_base::AsyncSocket * socket) { + SignalRead(); +} + +void XmppSocket::OnWriteEvent(talk_base::AsyncSocket * socket) { + // Write bytes if there are any + while (buffer_.Length() != 0) { + int written = cricket_socket_->Send(buffer_.Data(), buffer_.Length()); + if (written > 0) { + buffer_.Consume(written); + continue; + } + if (!cricket_socket_->IsBlocking()) + LOG(LS_ERROR) << "Send error: " << cricket_socket_->GetError(); + return; + } +} + +void XmppSocket::OnConnectEvent(talk_base::AsyncSocket * socket) { +#if defined(FEATURE_ENABLE_SSL) + if (state_ == buzz::AsyncSocket::STATE_TLS_CONNECTING) { + state_ = buzz::AsyncSocket::STATE_TLS_OPEN; + SignalSSLConnected(); + OnWriteEvent(cricket_socket_); + return; + } +#endif // !defined(FEATURE_ENABLE_SSL) + state_ = buzz::AsyncSocket::STATE_OPEN; + SignalConnected(); +} + +void XmppSocket::OnCloseEvent(talk_base::AsyncSocket * socket, int error) { + SignalCloseEvent(error); +} + +#else // USE_SSLSTREAM + +void XmppSocket::OnEvent(talk_base::StreamInterface* stream, + int events, int err) { + if ((events & talk_base::SE_OPEN)) { +#if defined(FEATURE_ENABLE_SSL) + if (state_ == buzz::AsyncSocket::STATE_TLS_CONNECTING) { + state_ = buzz::AsyncSocket::STATE_TLS_OPEN; + SignalSSLConnected(); + events |= talk_base::SE_WRITE; + } else +#endif + { + state_ = buzz::AsyncSocket::STATE_OPEN; + SignalConnected(); + } + } + if ((events & talk_base::SE_READ)) + SignalRead(); + if ((events & talk_base::SE_WRITE)) { + // Write bytes if there are any + while (buffer_.Length() != 0) { + talk_base::StreamResult result; + size_t written; + int error; + result = stream_->Write(buffer_.Data(), buffer_.Length(), + &written, &error); + if (result == talk_base::SR_ERROR) { + LOG(LS_ERROR) << "Send error: " << error; + return; + } + if (result == talk_base::SR_BLOCK) + return; + ASSERT(result == talk_base::SR_SUCCESS); + ASSERT(written > 0); + buffer_.Shift(written); + } + } + if ((events & talk_base::SE_CLOSE)) + SignalCloseEvent(err); +} +#endif // USE_SSLSTREAM + +buzz::AsyncSocket::State XmppSocket::state() { + return state_; +} + +buzz::AsyncSocket::Error XmppSocket::error() { + return buzz::AsyncSocket::ERROR_NONE; +} + +int XmppSocket::GetError() { + return 0; +} + +bool XmppSocket::Connect(const talk_base::SocketAddress& addr) { + if (cricket_socket_ == NULL) { + CreateCricketSocket(addr.family()); + } + if (cricket_socket_->Connect(addr) < 0) { + return cricket_socket_->IsBlocking(); + } + return true; +} + +bool XmppSocket::Read(char * data, size_t len, size_t* len_read) { +#ifndef USE_SSLSTREAM + int read = cricket_socket_->Recv(data, len); + if (read > 0) { + *len_read = (size_t)read; + return true; + } +#else // USE_SSLSTREAM + talk_base::StreamResult result = stream_->Read(data, len, len_read, NULL); + if (result == talk_base::SR_SUCCESS) + return true; +#endif // USE_SSLSTREAM + return false; +} + +bool XmppSocket::Write(const char * data, size_t len) { + buffer_.WriteBytes(data, len); +#ifndef USE_SSLSTREAM + OnWriteEvent(cricket_socket_); +#else // USE_SSLSTREAM + OnEvent(stream_, talk_base::SE_WRITE, 0); +#endif // USE_SSLSTREAM + return true; +} + +bool XmppSocket::Close() { + if (state_ != buzz::AsyncSocket::STATE_OPEN) + return false; +#ifndef USE_SSLSTREAM + if (cricket_socket_->Close() == 0) { + state_ = buzz::AsyncSocket::STATE_CLOSED; + SignalClosed(); + return true; + } + return false; +#else // USE_SSLSTREAM + state_ = buzz::AsyncSocket::STATE_CLOSED; + stream_->Close(); + SignalClosed(); + return true; +#endif // USE_SSLSTREAM +} + +bool XmppSocket::StartTls(const std::string & domainname) { +#if defined(FEATURE_ENABLE_SSL) + if (tls_ == buzz::TLS_DISABLED) + return false; +#ifndef USE_SSLSTREAM + talk_base::SSLAdapter* ssl_adapter = + static_cast(cricket_socket_); + if (ssl_adapter->StartSSL(domainname.c_str(), false) != 0) + return false; +#else // USE_SSLSTREAM + talk_base::SSLStreamAdapter* ssl_stream = + static_cast(stream_); + if (ssl_stream->StartSSLWithServer(domainname.c_str()) != 0) + return false; +#endif // USE_SSLSTREAM + state_ = buzz::AsyncSocket::STATE_TLS_CONNECTING; + return true; +#else // !defined(FEATURE_ENABLE_SSL) + return false; +#endif // !defined(FEATURE_ENABLE_SSL) +} + +} // namespace buzz + diff --git a/talk/xmpp/xmppsocket.h b/talk/xmpp/xmppsocket.h new file mode 100644 index 000000000..f89333f6b --- /dev/null +++ b/talk/xmpp/xmppsocket.h @@ -0,0 +1,89 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_XMPPSOCKET_H_ +#define TALK_XMPP_XMPPSOCKET_H_ + +#include "talk/base/asyncsocket.h" +#include "talk/base/bytebuffer.h" +#include "talk/base/sigslot.h" +#include "talk/xmpp/asyncsocket.h" +#include "talk/xmpp/xmppengine.h" + +// The below define selects the SSLStreamAdapter implementation for +// SSL, as opposed to the SSLAdapter socket adapter. +// #define USE_SSLSTREAM + +namespace talk_base { + class StreamInterface; + class SocketAddress; +}; +extern talk_base::AsyncSocket* cricket_socket_; + +namespace buzz { + +class XmppSocket : public buzz::AsyncSocket, public sigslot::has_slots<> { +public: + XmppSocket(buzz::TlsOptions tls); + ~XmppSocket(); + + virtual buzz::AsyncSocket::State state(); + virtual buzz::AsyncSocket::Error error(); + virtual int GetError(); + + virtual bool Connect(const talk_base::SocketAddress& addr); + virtual bool Read(char * data, size_t len, size_t* len_read); + virtual bool Write(const char * data, size_t len); + virtual bool Close(); + virtual bool StartTls(const std::string & domainname); + + sigslot::signal1 SignalCloseEvent; + +private: + void CreateCricketSocket(int family); +#ifndef USE_SSLSTREAM + void OnReadEvent(talk_base::AsyncSocket * socket); + void OnWriteEvent(talk_base::AsyncSocket * socket); + void OnConnectEvent(talk_base::AsyncSocket * socket); + void OnCloseEvent(talk_base::AsyncSocket * socket, int error); +#else // USE_SSLSTREAM + void OnEvent(talk_base::StreamInterface* stream, int events, int err); +#endif // USE_SSLSTREAM + + talk_base::AsyncSocket * cricket_socket_; +#ifdef USE_SSLSTREAM + talk_base::StreamInterface *stream_; +#endif // USE_SSLSTREAM + buzz::AsyncSocket::State state_; + talk_base::ByteBuffer buffer_; + buzz::TlsOptions tls_; +}; + +} // namespace buzz + +#endif // TALK_XMPP_XMPPSOCKET_H_ + diff --git a/talk/xmpp/xmppstanzaparser.cc b/talk/xmpp/xmppstanzaparser.cc new file mode 100644 index 000000000..6c3ef5fee --- /dev/null +++ b/talk/xmpp/xmppstanzaparser.cc @@ -0,0 +1,106 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmpp/xmppstanzaparser.h" + +#include "talk/xmllite/xmlelement.h" +#include "talk/base/common.h" +#include "talk/xmpp/constants.h" +#ifdef EXPAT_RELATIVE_PATH +#include "expat.h" +#else +#include "third_party/expat/v2_0_1/Source/lib/expat.h" +#endif + +namespace buzz { + +XmppStanzaParser::XmppStanzaParser(XmppStanzaParseHandler *psph) : + psph_(psph), + innerHandler_(this), + parser_(&innerHandler_), + depth_(0), + builder_() { +} + +void +XmppStanzaParser::Reset() { + parser_.Reset(); + depth_ = 0; + builder_.Reset(); +} + +void +XmppStanzaParser::IncomingStartElement( + XmlParseContext * pctx, const char * name, const char ** atts) { + if (depth_++ == 0) { + XmlElement * pelStream = XmlBuilder::BuildElement(pctx, name, atts); + if (pelStream == NULL) { + pctx->RaiseError(XML_ERROR_SYNTAX); + return; + } + psph_->StartStream(pelStream); + delete pelStream; + return; + } + + builder_.StartElement(pctx, name, atts); +} + +void +XmppStanzaParser::IncomingCharacterData( + XmlParseContext * pctx, const char * text, int len) { + if (depth_ > 1) { + builder_.CharacterData(pctx, text, len); + } +} + +void +XmppStanzaParser::IncomingEndElement( + XmlParseContext * pctx, const char * name) { + if (--depth_ == 0) { + psph_->EndStream(); + return; + } + + builder_.EndElement(pctx, name); + + if (depth_ == 1) { + XmlElement *element = builder_.CreateElement(); + psph_->Stanza(element); + delete element; + } +} + +void +XmppStanzaParser::IncomingError( + XmlParseContext * pctx, XML_Error errCode) { + UNUSED(pctx); + UNUSED(errCode); + psph_->XmlError(); +} + +} diff --git a/talk/xmpp/xmppstanzaparser.h b/talk/xmpp/xmppstanzaparser.h new file mode 100644 index 000000000..c6f8b08ad --- /dev/null +++ b/talk/xmpp/xmppstanzaparser.h @@ -0,0 +1,97 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef _xmppstanzaparser_h_ +#define _xmppstanzaparser_h_ + +#include "talk/xmllite/xmlparser.h" +#include "talk/xmllite/xmlbuilder.h" + + +namespace buzz { + +class XmlElement; + +class XmppStanzaParseHandler { +public: + virtual ~XmppStanzaParseHandler() {} + virtual void StartStream(const XmlElement * pelStream) = 0; + virtual void Stanza(const XmlElement * pelStanza) = 0; + virtual void EndStream() = 0; + virtual void XmlError() = 0; +}; + +class XmppStanzaParser { +public: + XmppStanzaParser(XmppStanzaParseHandler *psph); + bool Parse(const char * data, size_t len, bool isFinal) + { return parser_.Parse(data, len, isFinal); } + void Reset(); + +private: + class ParseHandler : public XmlParseHandler { + public: + ParseHandler(XmppStanzaParser * outer) : outer_(outer) {} + virtual void StartElement(XmlParseContext * pctx, + const char * name, const char ** atts) + { outer_->IncomingStartElement(pctx, name, atts); } + virtual void EndElement(XmlParseContext * pctx, + const char * name) + { outer_->IncomingEndElement(pctx, name); } + virtual void CharacterData(XmlParseContext * pctx, + const char * text, int len) + { outer_->IncomingCharacterData(pctx, text, len); } + virtual void Error(XmlParseContext * pctx, + XML_Error errCode) + { outer_->IncomingError(pctx, errCode); } + private: + XmppStanzaParser * const outer_; + }; + + friend class ParseHandler; + + void IncomingStartElement(XmlParseContext * pctx, + const char * name, const char ** atts); + void IncomingEndElement(XmlParseContext * pctx, + const char * name); + void IncomingCharacterData(XmlParseContext * pctx, + const char * text, int len); + void IncomingError(XmlParseContext * pctx, + XML_Error errCode); + + XmppStanzaParseHandler * psph_; + ParseHandler innerHandler_; + XmlParser parser_; + int depth_; + XmlBuilder builder_; + + }; + + +} + +#endif diff --git a/talk/xmpp/xmppstanzaparser_unittest.cc b/talk/xmpp/xmppstanzaparser_unittest.cc new file mode 100644 index 000000000..06faf8765 --- /dev/null +++ b/talk/xmpp/xmppstanzaparser_unittest.cc @@ -0,0 +1,168 @@ +// Copyright 2004 Google Inc. All Rights Reserved + + +#include +#include +#include +#include "talk/base/common.h" +#include "talk/base/gunit.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/xmppstanzaparser.h" + +using buzz::QName; +using buzz::XmlElement; +using buzz::XmppStanzaParser; +using buzz::XmppStanzaParseHandler; + +class XmppStanzaParserTestHandler : public XmppStanzaParseHandler { + public: + virtual void StartStream(const XmlElement * element) { + ss_ << "START" << element->Str(); + } + virtual void Stanza(const XmlElement * element) { + ss_ << "STANZA" << element->Str(); + } + virtual void EndStream() { + ss_ << "END"; + } + virtual void XmlError() { + ss_ << "ERROR"; + } + + std::string Str() { + return ss_.str(); + } + + std::string StrClear() { + std::string result = ss_.str(); + ss_.str(""); + return result; + } + + private: + std::stringstream ss_; +}; + + +TEST(XmppStanzaParserTest, TestTrivial) { + XmppStanzaParserTestHandler handler; + XmppStanzaParser parser(&handler); + std::string fragment; + + fragment = ""; + parser.Parse(fragment.c_str(), fragment.length(), false); + EXPECT_EQ("STARTEND", handler.StrClear()); +} + +TEST(XmppStanzaParserTest, TestStanzaAtATime) { + XmppStanzaParserTestHandler handler; + XmppStanzaParser parser(&handler); + std::string fragment; + + fragment = ""; + parser.Parse(fragment.c_str(), fragment.length(), false); + EXPECT_EQ("START", handler.StrClear()); + + fragment = "hello"; + parser.Parse(fragment.c_str(), fragment.length(), false); + EXPECT_EQ("STANZA" + "hello", handler.StrClear()); + + fragment = " SOME TEXT TO IGNORE "; + parser.Parse(fragment.c_str(), fragment.length(), false); + EXPECT_EQ("", handler.StrClear()); + + fragment = ""; + parser.Parse(fragment.c_str(), fragment.length(), false); + EXPECT_EQ("STANZA" + "", handler.StrClear()); + + fragment = ""; + parser.Parse(fragment.c_str(), fragment.length(), false); + EXPECT_EQ("END", handler.StrClear()); +} + +TEST(XmppStanzaParserTest, TestFragmentedStanzas) { + XmppStanzaParserTestHandler handler; + XmppStanzaParser parser(&handler); + std::string fragment; + + fragment = "", handler.StrClear()); + + fragment = "lo IGNORE ME " + "" + "helloSTANZA", handler.StrClear()); + + fragment = "ream:stream>"; + parser.Parse(fragment.c_str(), fragment.length(), false); + EXPECT_EQ("END", handler.StrClear()); +} + +TEST(XmppStanzaParserTest, TestReset) { + XmppStanzaParserTestHandler handler; + XmppStanzaParser parser(&handler); + std::string fragment; + + fragment = "", handler.StrClear()); + parser.Reset(); + + fragment = ""; + parser.Parse(fragment.c_str(), fragment.length(), false); + EXPECT_EQ("START", handler.StrClear()); + + fragment = "hello"; + parser.Parse(fragment.c_str(), fragment.length(), false); + EXPECT_EQ("STANZA" + "hello", handler.StrClear()); +} + +TEST(XmppStanzaParserTest, TestError) { + XmppStanzaParserTestHandler handler; + XmppStanzaParser parser(&handler); + std::string fragment; + + fragment = "<-foobar/>"; + parser.Parse(fragment.c_str(), fragment.length(), false); + EXPECT_EQ("ERROR", handler.StrClear()); + + parser.Reset(); + fragment = ""; + parser.Parse(fragment.c_str(), fragment.length(), false); + EXPECT_EQ("ERROR", handler.StrClear()); + parser.Reset(); + + fragment = "ns:stream='str'>hel"; + parser.Parse(fragment.c_str(), fragment.length(), false); + EXPECT_EQ("ERROR", handler.StrClear()); + parser.Reset(); + + fragment = "" + ""; + parser.Parse(fragment.c_str(), fragment.length(), false); + EXPECT_EQ("STARTSTANZA" + "ERROR", handler.StrClear()); +} diff --git a/talk/xmpp/xmpptask.cc b/talk/xmpp/xmpptask.cc new file mode 100644 index 000000000..046f7a104 --- /dev/null +++ b/talk/xmpp/xmpptask.cc @@ -0,0 +1,175 @@ +/* + * 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/xmpp/xmpptask.h" +#include "talk/xmpp/xmppclient.h" +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/constants.h" + +namespace buzz { + +XmppClientInterface::XmppClientInterface() { +} + +XmppClientInterface::~XmppClientInterface() { +} + +XmppTask::XmppTask(XmppTaskParentInterface* parent, + XmppEngine::HandlerLevel level) + : XmppTaskBase(parent), stopped_(false) { +#ifdef _DEBUG + debug_force_timeout_ = false; +#endif + + id_ = GetClient()->NextId(); + GetClient()->AddXmppTask(this, level); + GetClient()->SignalDisconnected.connect(this, &XmppTask::OnDisconnect); +} + +XmppTask::~XmppTask() { + StopImpl(); +} + +void XmppTask::StopImpl() { + while (NextStanza() != NULL) {} + if (!stopped_) { + GetClient()->RemoveXmppTask(this); + GetClient()->SignalDisconnected.disconnect(this); + stopped_ = true; + } +} + +XmppReturnStatus XmppTask::SendStanza(const XmlElement* stanza) { + if (stopped_) + return XMPP_RETURN_BADSTATE; + return GetClient()->SendStanza(stanza); +} + +XmppReturnStatus XmppTask::SendStanzaError(const XmlElement* element_original, + XmppStanzaError code, + const std::string& text) { + if (stopped_) + return XMPP_RETURN_BADSTATE; + return GetClient()->SendStanzaError(element_original, code, text); +} + +void XmppTask::Stop() { + StopImpl(); + Task::Stop(); +} + +void XmppTask::OnDisconnect() { + Error(); +} + +void XmppTask::QueueStanza(const XmlElement* stanza) { +#ifdef _DEBUG + if (debug_force_timeout_) + return; +#endif + + stanza_queue_.push_back(new XmlElement(*stanza)); + Wake(); +} + +const XmlElement* XmppTask::NextStanza() { + XmlElement* result = NULL; + if (!stanza_queue_.empty()) { + result = stanza_queue_.front(); + stanza_queue_.pop_front(); + } + next_stanza_.reset(result); + return result; +} + +XmlElement* XmppTask::MakeIq(const std::string& type, + const buzz::Jid& to, + const std::string& id) { + XmlElement* result = new XmlElement(QN_IQ); + if (!type.empty()) + result->AddAttr(QN_TYPE, type); + if (!to.IsEmpty()) + result->AddAttr(QN_TO, to.Str()); + if (!id.empty()) + result->AddAttr(QN_ID, id); + return result; +} + +XmlElement* XmppTask::MakeIqResult(const XmlElement * query) { + XmlElement* result = new XmlElement(QN_IQ); + result->AddAttr(QN_TYPE, STR_RESULT); + if (query->HasAttr(QN_FROM)) { + result->AddAttr(QN_TO, query->Attr(QN_FROM)); + } + result->AddAttr(QN_ID, query->Attr(QN_ID)); + return result; +} + +bool XmppTask::MatchResponseIq(const XmlElement* stanza, + const Jid& to, + const std::string& id) { + if (stanza->Name() != QN_IQ) + return false; + + if (stanza->Attr(QN_ID) != id) + return false; + + return MatchStanzaFrom(stanza, to); +} + +bool XmppTask::MatchStanzaFrom(const XmlElement* stanza, + const Jid& to) { + Jid from(stanza->Attr(QN_FROM)); + if (from == to) + return true; + + // We address the server as "", check if we are doing so here. + if (!to.IsEmpty()) + return false; + + // It is legal for the server to identify itself with "domain" or + // "myself@domain" + Jid me = GetClient()->jid(); + return (from == Jid(me.domain())) || (from == me.BareJid()); +} + +bool XmppTask::MatchRequestIq(const XmlElement* stanza, + const std::string& type, + const QName& qn) { + if (stanza->Name() != QN_IQ) + return false; + + if (stanza->Attr(QN_TYPE) != type) + return false; + + if (stanza->FirstNamed(qn) == NULL) + return false; + + return true; +} + +} diff --git a/talk/xmpp/xmpptask.h b/talk/xmpp/xmpptask.h new file mode 100644 index 000000000..6a88f98f1 --- /dev/null +++ b/talk/xmpp/xmpptask.h @@ -0,0 +1,189 @@ +/* + * 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. + */ + +#ifndef TALK_XMPP_XMPPTASK_H_ +#define TALK_XMPP_XMPPTASK_H_ + +#include +#include +#include "talk/base/sigslot.h" +#include "talk/base/task.h" +#include "talk/base/taskparent.h" +#include "talk/xmpp/xmppengine.h" + +namespace buzz { + +///////////////////////////////////////////////////////////////////// +// +// XMPPTASK +// +///////////////////////////////////////////////////////////////////// +// +// See Task and XmppClient first. +// +// XmppTask is a task that is designed to go underneath XmppClient and be +// useful there. It has a way of finding its XmppClient parent so you +// can have it nested arbitrarily deep under an XmppClient and it can +// still find the XMPP services. +// +// Tasks register themselves to listen to particular kinds of stanzas +// that are sent out by the client. Rather than processing stanzas +// right away, they should decide if they own the sent stanza, +// and if so, queue it and Wake() the task, or if a stanza does not belong +// to you, return false right away so the next XmppTask can take a crack. +// This technique (synchronous recognize, but asynchronous processing) +// allows you to have arbitrary logic for recognizing stanzas yet still, +// for example, disconnect a client while processing a stanza - +// without reentrancy problems. +// +///////////////////////////////////////////////////////////////////// + +class XmppTask; + +// XmppClientInterface is an abstract interface for sending and +// handling stanzas. It can be implemented for unit tests or +// different network environments. It will usually be implemented by +// XmppClient. +class XmppClientInterface { + public: + XmppClientInterface(); + virtual ~XmppClientInterface(); + + virtual XmppEngine::State GetState() const = 0; + virtual const Jid& jid() const = 0; + virtual std::string NextId() = 0; + virtual XmppReturnStatus SendStanza(const XmlElement* stanza) = 0; + virtual XmppReturnStatus SendStanzaError(const XmlElement* original_stanza, + XmppStanzaError error_code, + const std::string& message) = 0; + virtual void AddXmppTask(XmppTask* task, XmppEngine::HandlerLevel level) = 0; + virtual void RemoveXmppTask(XmppTask* task) = 0; + sigslot::signal0<> SignalDisconnected; + + DISALLOW_EVIL_CONSTRUCTORS(XmppClientInterface); +}; + +// XmppTaskParentInterface is the interface require for any parent of +// an XmppTask. It needs, for example, a way to get an +// XmppClientInterface. + +// We really ought to inherit from a TaskParentInterface, but we tried +// that and it's way too complicated to change +// Task/TaskParent/TaskRunner. For now, this works. +class XmppTaskParentInterface : public talk_base::Task { + public: + explicit XmppTaskParentInterface(talk_base::TaskParent* parent) + : Task(parent) { + } + virtual ~XmppTaskParentInterface() {} + + virtual XmppClientInterface* GetClient() = 0; + + DISALLOW_EVIL_CONSTRUCTORS(XmppTaskParentInterface); +}; + +class XmppTaskBase : public XmppTaskParentInterface { + public: + explicit XmppTaskBase(XmppTaskParentInterface* parent) + : XmppTaskParentInterface(parent), + parent_(parent) { + } + virtual ~XmppTaskBase() {} + + virtual XmppClientInterface* GetClient() { + return parent_->GetClient(); + } + + protected: + XmppTaskParentInterface* parent_; + + DISALLOW_EVIL_CONSTRUCTORS(XmppTaskBase); +}; + +class XmppTask : public XmppTaskBase, + public XmppStanzaHandler, + public sigslot::has_slots<> +{ + public: + XmppTask(XmppTaskParentInterface* parent, + XmppEngine::HandlerLevel level = XmppEngine::HL_NONE); + virtual ~XmppTask(); + + std::string task_id() const { return id_; } + void set_task_id(std::string id) { id_ = id; } + +#ifdef _DEBUG + void set_debug_force_timeout(const bool f) { debug_force_timeout_ = f; } +#endif + + virtual bool HandleStanza(const XmlElement* stanza) { return false; } + + protected: + XmppReturnStatus SendStanza(const XmlElement* stanza); + XmppReturnStatus SetResult(const std::string& code); + XmppReturnStatus SendStanzaError(const XmlElement* element_original, + XmppStanzaError code, + const std::string& text); + + virtual void Stop(); + virtual void OnDisconnect(); + + virtual void QueueStanza(const XmlElement* stanza); + const XmlElement* NextStanza(); + + bool MatchStanzaFrom(const XmlElement* stanza, const Jid& match_jid); + + bool MatchResponseIq(const XmlElement* stanza, const Jid& to, + const std::string& task_id); + + static bool MatchRequestIq(const XmlElement* stanza, const std::string& type, + const QName& qn); + static XmlElement *MakeIqResult(const XmlElement* query); + static XmlElement *MakeIq(const std::string& type, + const Jid& to, const std::string& task_id); + + // Returns true if the task is under the specified rate limit and updates the + // rate limit accordingly + bool VerifyTaskRateLimit(const std::string task_name, int max_count, + int per_x_seconds); + +private: + void StopImpl(); + + bool stopped_; + std::deque stanza_queue_; + talk_base::scoped_ptr next_stanza_; + std::string id_; + +#ifdef _DEBUG + bool debug_force_timeout_; +#endif +}; + +} // namespace buzz + +#endif // TALK_XMPP_XMPPTASK_H_ diff --git a/talk/xmpp/xmppthread.cc b/talk/xmpp/xmppthread.cc new file mode 100644 index 000000000..43dded866 --- /dev/null +++ b/talk/xmpp/xmppthread.cc @@ -0,0 +1,85 @@ +/* + * libjingle + * Copyright 2004--2005, 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/xmpp/xmppthread.h" + +#include "talk/xmpp/xmppauth.h" +#include "talk/xmpp/xmppclientsettings.h" + +namespace buzz { +namespace { + +const uint32 MSG_LOGIN = 1; +const uint32 MSG_DISCONNECT = 2; + +struct LoginData: public talk_base::MessageData { + LoginData(const buzz::XmppClientSettings& s) : xcs(s) {} + virtual ~LoginData() {} + + buzz::XmppClientSettings xcs; +}; + +} // namespace + +XmppThread::XmppThread() { + pump_ = new buzz::XmppPump(this); +} + +XmppThread::~XmppThread() { + delete pump_; +} + +void XmppThread::ProcessMessages(int cms) { + talk_base::Thread::ProcessMessages(cms); +} + +void XmppThread::Login(const buzz::XmppClientSettings& xcs) { + Post(this, MSG_LOGIN, new LoginData(xcs)); +} + +void XmppThread::Disconnect() { + Post(this, MSG_DISCONNECT); +} + +void XmppThread::OnStateChange(buzz::XmppEngine::State state) { +} + +void XmppThread::OnMessage(talk_base::Message* pmsg) { + if (pmsg->message_id == MSG_LOGIN) { + ASSERT(pmsg->pdata != NULL); + LoginData* data = reinterpret_cast(pmsg->pdata); + pump_->DoLogin(data->xcs, new XmppSocket(buzz::TLS_DISABLED), + new XmppAuth()); + delete data; + } else if (pmsg->message_id == MSG_DISCONNECT) { + pump_->DoDisconnect(); + } else { + ASSERT(false); + } +} + +} // namespace buzz diff --git a/talk/xmpp/xmppthread.h b/talk/xmpp/xmppthread.h new file mode 100644 index 000000000..62a5ce695 --- /dev/null +++ b/talk/xmpp/xmppthread.h @@ -0,0 +1,62 @@ +/* + * libjingle + * Copyright 2004--2005, 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. + */ + +#ifndef TALK_XMPP_XMPPTHREAD_H_ +#define TALK_XMPP_XMPPTHREAD_H_ + +#include "talk/base/thread.h" +#include "talk/xmpp/xmppclientsettings.h" +#include "talk/xmpp/xmppengine.h" +#include "talk/xmpp/xmpppump.h" +#include "talk/xmpp/xmppsocket.h" + +namespace buzz { + +class XmppThread: + public talk_base::Thread, buzz::XmppPumpNotify, talk_base::MessageHandler { +public: + XmppThread(); + ~XmppThread(); + + buzz::XmppClient* client() { return pump_->client(); } + + void ProcessMessages(int cms); + + void Login(const buzz::XmppClientSettings & xcs); + void Disconnect(); + +private: + buzz::XmppPump* pump_; + + void OnStateChange(buzz::XmppEngine::State state); + void OnMessage(talk_base::Message* pmsg); +}; + +} // namespace buzz + +#endif // TALK_XMPP_XMPPTHREAD_H_ +

      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 literal 0 HcmV?d00001 diff --git a/talk/media/testdata/video.rtpdump b/talk/media/testdata/video.rtpdump new file mode 100644 index 0000000000000000000000000000000000000000..7be863e50133bdc81560043335b0ab9d8790a0b9 GIT binary patch literal 134998 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�`#6E{H}y^@A_^%}n&XpD-) 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%0Apy6587$ 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%0D(>(lA^ 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

    3. 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%0A^tggmA4#Pd(6z<;Cxz9 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

      8f4Ne}+$c57o5@ciJRw@MpZQ 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 z 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

    4. 5Upyu3cQJ3MgV*jK@eUKfqttZ+N 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

      EbQ5v+#i@h94= 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

      13s`1RMia@0$Vt&{!h6ZUHHAay+a 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⚾^-yx!U3iNuz}#OC!jJOP@xQ|4l$9v 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∓YN+qH3~4Kjs%W2f}uVnqT2utOKG^a}L{g&iSj+K^1)| zHG@IR%q+;f#1zd>RwE3NZ?w3I^Ury-u!lNxN%9%@E5S2om)l= zJ;i&X%mp7%V2~Ye=+vYI_Q=vv9)&N@EI|>w16Te1h}ViE6up+y(OH7Jx!CN$_X6AE zD)7lpE~c%-gf!K6u*&8e`VWO}kk}^@BNbmPre>BhAH%NMs8n8;TdCa^mb&PaV^MF3 zZeePFak?rT)(4qREMRW{492(<*y9L1_No2;AR8{a(Rpb`Pfp;kR^ZRzZq}OpMy)=e z;^jB67iS^dgLediF4{nm3x6;AzoG*|FXS8%;OX?Ju+eBZ664zMTVqr)52t?usgIFU zy`h~C<2m0;J~;Nj-`iq0=J%<1Gn}xTXqM^4R#} zX7PFQpjH{1r?&g8XdG?9H($bBjL#I`pBl_gX^c%B!`VLUT2q35BgUw(pV&F6WqM1p z@%Al=);sabd$&r&h7xfhISYJQqsV3dB-(c?z2!O{5*&)Xsi+Ks$)!T=9B!1bkxSVL zBlcur`*`gtuvc{P>%?Pl$63MGi|?^qV3?}YQtYbu+W1fjgt) zwaMNtx^!#+qvE^(o4g3$JlDRKp3FShgT(>cShFkjnV=l(mwIi6;Iz&Sudv&qA#*T` z-_5eGDcdjgWw*(NLqvi<9`yUv0e1*4dIzkHMjgFV^iN>_xD5t5u-4>LX$|Vt2ET#R zNVVc>^(FTwP4wUjHW1$5r9PbXt^HdVO7qaxs&KABvHb=3FKK;^^O8pW#{9_$?9SAi z{tq$;{u%y+?fWkEwfoKZ*Z$e@uLJa6qEjQMa-Z-1B>xxYYd+llr^%zEhx2z&)I?qC ztL_udW5zrF8!DA+`7h)ee+fTRKVjyRU5xTq!5I`RQCwvT;75M0pO`+FE#`UA%&4+A znkbCJ9N(0#VG5&5?{Tqp#A4L#l0QFP*d&n~a7}=Pgvb#@&VaolUh7D7kVA+(g4znX zL_2o0S>R0C0v=-M+8VHp-3Jrnu)TzIXd%TcUl2 zdd(C$bymcd?Q&bR+C|AXRz~2;$8flX{o?(FbMGyrQi6ZPUa|iOUj7p1_ez;ZhU18i z&3e0-m<_wh?pF2}8Q4ssy+zDpEfTLKZlo4K7f4LF2vy?3hETx{6Zl)rzqb_o2bUH5 zC-5h<%}`2Zuj*oUyUnLccpcrlYp9CPV~Z`7ZLK9(Xl=4bobgPJP4^NXJr>0HlLoTv zh6R7yz#oUdM87l?ZkLC=ekNE3^&WDKX0Op`aO=^k!~Vg|Y$DS`>m2+w;DhV6h5)_I zpjf*;xJy5eYFeQW9Q)q54}8iV!s}M0Tbu>@WqjQm;Wx^k-EnI1hxDI3kvm_o@6ZhU zvE&c`U_1yM694^x$2^Y>WH!nDn*DHJPr!wl_&WG%?4#(t-LK>x-W5dF_zUhy>uED` zk8AG*Z)+b!r-=2wQ2rKtsh$c>v3vHka@PG`V|x<25Cta!D`-8>v;uV#c4Ac7%y-22 z+la<>WQ_)d!ycD;cSbq+t3%sV_Z53K9;tH+mv2^n*{49 zxixGPy^fT?AvQ6tr-&T((&0Ionu>zYmhs=5E%Z%VB$P zc)%PuozD&hp)!7>2m6u!2Kyx3Vr@OT3ndY?C$f|UAzs#-OML}(2so3=i2ula$aBO@ z5*Q>07tAq%(fQ1Xz?Y8MTeIC|nIbkLF7`^?7Q54{&oq&zFR|OxyTa}IP6ozDm^tjLLegmZlZT$fEwrN})5{?MVSL=UJA z{z{YCk?yg!DPSoD#;BDDwPC7J%-#~Qq&P`VLR<$T?%-%3MFyAT?$!FjUK@=nyU!lb z2K;Tj(uqZs^=<6v7w zYzRjm{Lw2GyQX)zcT0zs2r`&?Lk|l4O>;fU!5oRYS(Ht)Lu4oYDN6#*Tr_Qp1s?% zqDJMb{P)Vc_Iv3O{&k_YRY%wKnsBlDaP|}|zw;>ioi<7vy*{>m>oXhCKd!MvO{M`&=JqtKNmoR}C|pO~sG@FldhEV* zzuzwrp@27W29w;ysrc~mU^FBTg@gKlH=r~BsBZU%cug<9n%Z)f8ndW1<8yZgk|tS_ zHlgp3wr1KA%%p?O*e((?wkfSahe94CQ}vR!1R`e^zX0|b9v7w%F(Jyj3&64H2;M-Hk1e%UrNutS zQdE}8;RG^k!|c+SDR=~f*&%6rZYai|@V(n|cn|WAfq*(R{8T@M3Oq>&+eXOv-|d`hC$k?I(~W zvXS%lx0!?P7X3c=r24P;6S3nzoqxsSzkL3J^-boG`?~f_ASF+PrzgH-P6Y*W?JMs~ z{4Tc7XVZvtPCI9PpBDBnjtxYNO6Ki=zYWg%G&4vj<_Pp{X6gfV1E=0>aGT5)tJCUM z`~0{%AS$BzfR9ZJ2DwYY9EUqt+L7In+8%9BaYrY2M8iqCND?1QTzPJo>vrs8UjjRt z5Wc%7yEWB`rx^~1m7Vquso(BSu(vS5&KgmJP7*tc-dw6x@D{L_1Mf;CRTH<#$;)Q1I6v<&S~o@Y-1_`z0Q%z@cdQe?18@KQcf z$b-S9nCmSf))R_g;()u@-mW6QneWdvW;1`Zk-T~y@%*ps%i#K54c=n>@!TBhf$TD~ z$hYaK3){z*6tE}ox5n!5b{oUkKH+Vl&F@s>TRPg}(24lcj_v3bOQ1=ZKz%inn4$lp+7 zWV@^OjrTV;(43afx#x-d&ZPzZ&f|5@Q;jUJvVTHkV6j!+mX$Z^-+!b&(oEnaCwv)}LyOX2QNOCvVk!a_|=LH6bQ*1>| z?2L9!4CH$!w(Z}R=)YqqHN20n&Fv7Zq68RDfa%0w!LKL4`vmbMn2m9p=!t969r$W` za9M#pG~NohBg!vEv8|LDsWSFOmwV;0QX3kCV5ZC{Wj6-ci}53#6W9fFao!8l!dxMa z_m8ujkI9J($7dFwUA}Al<>pOPqi@PC)|#TZ+JDev*%)mg{#vCpP)D}-TU~8mkzvva zb-!QOmr>)rA`LRpeZB%7^|kPGuO$l*vs**{u(4hI6>|eAu$N*(S)6|iWVb5qVVAZo zpaa{9jrE-493i{hM;t2fVhim+5qni2*H5z?0`Tb&I z-R;DM>pV#}!gc1Uh}w*(#ehH24>@K(YHqimP=E4&l)=|O^-lgh-u}flg43^s3 z{%TB-UkU6rV(%N#fo*2@S&ug83<;Yi@D}54I4dx>Lk3ssFs#U3;fUC}EA5UXxUX75 zjnqUc8UuqP$z9P%YJ{znBhhZQQjR1BbKAzc_P0-T-PfNOzH210dm5}Vv9pt{z2Fo3 zIgA|@wsd<^V3LR^B@Xc=ac5t$8y;741a}O~6$`a4v(zay%F#7k$86EX!=8e5MkzLH zE&R|DxG+`jDy_sWgGsuaJ}-P{Hgv}G>hawAGINbP&%D~1Mc%Sf)Lzm{m}6fWECZVY zhv*Im#b!r#M!6^Vd*$`$N#(I{pPGxFR3D69QJ#i3`e^u|W})rf8B9y>dwlu?-bNw#c*OsFihjN1{HX|qd^F+;Z*S7e*VMVza!Mzh5xlrv8$U3RU8?YTNU;Oa^KBnEtt$p+vQoxU-*lw1=Lu zm{*1o0p{qzV((x^Tsko)r2_nwWAn&Ku#qE%iX9mC#?+MGs|hmRGwbKh zw>$h)W?vKIv+ZTxwu~E~MiXSUJ=p{D!Tjy1gW2DurlN<{ z2eQv9FJ$kKC$oD}2XY7D6rR|bB5 z7yH*QcZaP?OVFi~fta-CjI1!!U3NS(|5D2b|!=*NBR{7HY$eL{WLre+xDqyKN) zb_r+y?)qKkBlCp)oqEAOXPmY_*3UTw{GA1VXQ}mpyR^VxjBjxFqjkpmlYJ=jpck08 zp^^McZwWkmF;QNcDPiYFg#vJ8WIy>=swpPb1hzo{P^Y+o9Qx%7#>dJ8`}A@oq3$u%mpRf5M@N zDYt{Y-dvBoZ5n>f47g*KGR7QxS72dZJa7VFC5p2SHMcTizSYL9!LT7k32iqPI}MV% zxH~rcS7~!HF?$;2jCsuUU5#>TOS+Amcs6rJ%nduUQ1mKt`s{IEF^Ay*sL>x)IlohS zaV8wyKbak$$=#cp%;pp;i$-%kD?KzB!XP`9QiTLKvxbh1Og}37QQ_E;{Z!^_cY)SR zv@(eA9f%KhPvDPkQD?w}R?yCFqiuAdU{VH53w?(V9E8s6NE5YwM z5Ol)PCl-uxx7pa_SK)(ej2iqfR~@Zyu@A}Ein`)6S+xHy;_nORg#MZLiu|^S{|fd` z{QsYGkN@Li@!iCv)49y!_EY*f_l)|j`%R2Js^2m8gw4A^|L-{~%Op!ZRncEk1DDuG zcD;5hGv7r_Op+f`(<;goGvBd^{HjLSK=8*TIQxkTHn7ENqb|MG?~!}5J#ufhmuhJ* z+l9K-UQwIz`^{}mzuCtY1Q02h8w!y-#o(2g@#it*U~21Rd$RTR7OC}KVeh~i8|J{8IN>_(^Kx>%Qn<{q z=7v!mk~6>&SO7;r$|uzc@=IfKFx8aXk?IV7tqGO&*~}n|?d7**W`nou$zsu)W{T~) z%zrU;*=CRVBj!%#=$kn^qc;sb-=)4Zvv1uJ!nv`lZHde<{j_*OL>>Hm(gC&F{QYxPC**V7^}($Yas*y}jU1^xr!CE%Y_o z_-lec(PLw`P9rtGjq&eVZM1}762+wkyGkpzmWqC2x-1ybd-%Fqce4ftVTuUsRfEUP zF%AW1q1J2EyTT%E8Fk?wJ+XD+T>4||)tBxG`J`YT2!Hs0#M-}Nv%q&1=)=-mIeTB` zwEGQuGv}~-KTzxYLHJu@y|*(XW;OGHb79u6Aa2-5e7=FdI#n_lSL|9%m*dOUQ1dTx z%QG9WfmQ6Qt)t#vPp_wuJa~%}V-Vgr`7ru>-Eln{9HzSQh1;S5W2c7)HFjr5wO#H& zdYh;d!kDC@aPg4$N|%DLA=s-K(bL-a zWWV&v%zddK`@9t8Z0R{TMl<1F^`5MiG`QRTD>$Lg0tfX$v(B^uyMV=(OPCh0Eb6G$ zoeCD|{Zm5;QFF%rQS4B|EltM553K1jaLIJZnLpj@MeNeeR1BDf1Es@ItTtf zwLjPCYG_}@G2#Co{D~RQbD3`~s(`Qyo9&s*Ip#voIdn`eV(%Pw~_$z^H4;HD(u0}DZ45jZ9dYa49E11F+K3M!7o8w+1KetOB;JnH=x~a!Ns200f3y=D zn3NJ>B2_pODR7t?%MwRU?oN%&5ED-CmPT>~pNuV(sM+BA2kBC=(LraU2%IT+0ptfO z_?=eSYnUD=r`9LxeQ3B)r(5aK0fSqx#;?;FnZ)a(LfI5;lWW1>4a`d3j85q-Y^q|G z4i@1Y=9|T&3>4sxh%aos56;@Ay;nJnj_*iPEAmw?+;6j9r^9D@V(eR5&Kb* z>Z6*~Ew|9it@m1D{Ka`kT-&PyS7qpNl^T^n8~0HgF<+xrO-)nqc3=jZYyA#m7;HAM z4{tNt@|C=Q1uPO5y5z;2TIw@hOa>ico9}nv@3eDD|IQW4lUTn0Vf+_IkoGC#J?Er? zRYX_tIn<+$Qj7UcsC~rRA15+*Tl3Rbu+f)kHo=K>O4IAmJ=y522XDnfpP4yJ7+2(t zLEY2`zNj+whSxHZ%ioUSGAa;=6x3GncMvK#8w4&CI?+xOkaQN4;8 zlyQHAkN@5u!S@P`ipQLby}Gau4EB>#kISh@lB7UNrGgYVoJj70?(>byA!T!XK z`$kfu`^Kcv+$gc*E@I1_#DRn4!SpW7rOZslJOpPAy>xU4sI{#@8|gY2-6h0_LKkQ; zyPBBM1&8b=3@XqYT8HMr2AK8N8lodF7%X#{ubRV72N45aXV+Ls&(kG;m<>^b#$Y(4 z?|`eaBOK85$db(&W8(Sg-;6&nX^%cNef!95`RB(U$nKGqpiiyxmukNZPJ#E;@LR|O z;9u87HJUg~QfV6|OVr-!0a5Q0IWPF*F3t6(x~XIh!3o?F_c69;Tm4=*FJO$ia*Z!` z50+6YFET6qIZGOEd+<2sLRk} zW#a-tu{bMZ-tkAHp&7>scUCi}qA5V;B4 zvVs_c5*Q?Zrf!3k8QM3L+;RIza%?ZLVHOM);zDxpLD(YnVX2v{gnuRY1@MvShoMW5 zS)Z;>Z`P;*+6{UuxLi#Ctqk32wy*1(umRMM*#|V+jJIwxg?g(wk6pCv8DWCtDzubF zJR>t0Y}E#XL2XAcsQ06x(I4#8M#F^W5iyMC_0+-1+Y%26(lZf0*LdR%{E6P}x6U)#v)*?p zvX5Bn!E7O3VJ*>NOB=Vkx1d3B1B{jbwo~>#uNB^Y5sK*x)ur(CyMrE7R)@5SKvDy2 zR86*O;iiVt5G+Q!`Jv5nRqQj=WVf3DTC(9_Pu@t?yz<7^^^s3e!F z;&-ans@+O{mwNC=en#JrEu{u|t|M%oz*7N(q8B`vr5{XR4h&MSi+d*VW9oNtzmC{V zd=4CLC)SfCg?jY}J&&!~zQp$I4rP0GnA~_fyP9{1Lyd8}!q5QK+Hk$P z$tl*CqeZilnc{WWI&h{n!CUB~PF?4)gPQsjs$;^I5L2-in6t>=VVnwnpcZ{DeFC10 zm`TEi{$JwnTkB8Gllmd=y!xa48vf>b{{HjQ>)ioz>|g8ujarHDAwrQ46rvKbgeE37$w|C>+rv5|KW4lCgf1Fpr0=M7_lt+8=?ld-rRsL@20}fE#g>x6JJw_|7~;| zh;bXSfi39P^jlm4*Z&AZ@3Z6U2R_l(|CS*yRDlM zip#@xtx;@Wj=s_cGW()}K0O@L72aI5JVYczbAD zHyWwzm}X}q*^hE^l+CB2DSvEgYTv}3+-<4bv$wI6=m~W%yN0*r=1RJd=rS1A8L(=%$mj8@^u` z^BgtcrwYH@!QSXfu^EM&1Dm0j!leLz+)HcOXW1;OVC=kXRH~c`Z8cn<6=>*D(*S=} zq7F*zIHa~Xo3)MXWu%^A@Lpn1HWGWSH&)`)kNfAf3-*`#Dd(*E9sP>`JN#X+zp>vo zU-Vv3zVUukv)HS8o0^BaHQpMt##@XU8j1jp;G%&(rW2RIy+F?eJ&Kt3p_jk~xzwkk zhsZ=_Ilo)6zMA}SIVx*K#?VZ^_HZ`3=-6T7)Gjfu5B z8TGf`z1r<)+P>o4r`;ygAdJ>42-Ul3K%R;YHx~3mzm@1ej>IpqRak-o~1XoFn)k@}V=`Qup_*mH;YPQZ$zo8mank)zSHf@+);KSh#?3&1jg%1}tF}9U| z@4bodC62I*g`NvcC1Gnr>|j{yjc94NM!C_sl{ow%^ZN9!(reiPw*Xzga$n?7tHkUE z`pxsq+1Q6F=W^>Rc7NUwT#t{qBGcvUNk14op$BfImUNR~ceB2c?O?!#4?CsU8*-%Nfyc`)_nLMrVlUj3PUG;`R# z!`PQu49DVfDg_IOuc<$h>kF-7vN)7s(ShW@xq^2_Eex)kL4Tf~4earA#e6vQO5lfF zJmPa}=_LxDP!V-Ofi-#x*ueGRQ1rJ%k98BX2iQI|r0N9zuz^h)xro+gkEC}I@d|#+ zE);S`z|=0dF9L&tgEb=h7;q4V)xmHev(p)M#@NI(Y!CA_JCnP@odT;dX2DrPoY-d4 zof7qC9>-?~951lR5q-ZnufZ4B_;(tQ__Or|)>7j#N6~eAty<~tOs$@|d}2woP-`IP zU2ZMM_AKCc>0sC5R%4@|rVcyIKF=rBd)y(T$6jPy<l~`em0wu_v-tz zPbfzx4<}xkd@!L;b&YPlZP&26XL{%3xoG0?=r_qk-bkLDJ}`D*`mx<_Odd;ql6yAw zVE)m>iZG{&v@VAK^xWyN>yb5)NgX#(VuwrkOm}4PESWYg_#wYUQ?LoK5SJX;WDT>MS ziKWx4+z=iYo)ernViLHNOqX;26gfiS;9?(@E3c1#?s8`XyD^3BtK*)8?;WcQHOBaZ zQLdY&k5ou4uj{tG>!nP}NannWOMn_9Q9grBZBQlJLx& zWFi@iC&z;^X*{3?9gN1kH_-_sH!Ot zJT}ugy3$>$Eg`Zib0*;?FJ#(~SzWtAU*m=PlVRHIgwxZ3p5bD5u33|17HNu&hm%|6 zt#D%2WFE;J%{>bKJ{o67spzez&!EZv``{`4ZYt(?Ml(bSj&f?s8hdo#V>_OiekhTh zb|$9x92_|~^-SWCsr`w^CQ+;ai+4{RPQ8_Lr2md?Ru}u%7{lRqau0#Ot>6zE2>xK1 z!T0Mz52;?M_BMgP%`yJA_?0TN1I*#Az{Ut$zL@uUIh@yx^mMnu?CqjfU4zXOHjej_ z_iU59k@tEf^9MJiuO*}3O3jw-qU4$QHDag@*uPTma+LXm|2=JIDW#}q!djv65SoQwD?*gxvgTAyW` z2HTSi+4N1>^wOw?rDe;v z_&&0f0E6`51a~X7Go=3(7JN2wA+~UlJtE(0_bJPnzuw?=rN_L7&AW1UrrhYRWRJaB zug;8`hx|y(c{eG&wx`|^YLXGwCCEz?t1Sq z&Ggx-{7r((qBp@kXrcC8Mt=i6qs%h$!{zLF5cn(cH^BSrQisqgYT*44TGrS#q0U2` zw~knTF)^-C#J(Ke(5_6CLyT{)Lu|;+xB~^ok7kIhfz1ZL~LP8;MUjLZJ^F z@$+$n4HUKzf2;}nSmKD8iDKfyORo{UrBd!E>NDg^)#Sax_K6y_uzv;rOAah*fiWLV z(mg$G3EXAmOaSfzivzy+*eNL3{qxmx1@^$3!v^z$!kxH+Rh}!H7{koFPlWKZVlJ!b z1@96ZL*bLLiDbRfZPvVGy+1FtKB|&Oqj}2Q=y&SjaD}FZlgeH{r=0ZGCdX|r(e9=d zFPo8e<##6JTs9%2R#+4*mly9@uSENLwel%yb5o_-_1=G&|4D2k`f_u~iZ5gDLOOax zzbh*%ZsdvINDBg=?|-LqXsSK=;JyQ+2d54uUdp{H{UL9s9-6v$d?x?E#2@kplTYTK zOdiNiu+f$pxHr$dhIn8nTF}IO*uEHlM6-QLZ?Fx%K^xog;8}T_^i6OF8Up%t;Bh^1 zk*IGjqK3p=J^ptk_OF1yUV6$+aAT+~3w{DI(>iS5+Bi?X(Yel^<&f7q-I-cy93^m! z%b5QYv3;rgpXOs!@#xuRPPwP_FZ?f*v*1rKQ~%Wlp3fWvS^r_*=&WIOrJT7J`sviN z;7W1OePuRUXmQd-Vy1Wrn|9ZWS`YogN{!qVTMh>Koi-MHKR=`3%Ei8wnF#or;@$vz zac_jr35*KATPpG{@$;gO2^SFji9QH*Xu&5V4uq3n6l&4UdJ~EX85U_-az0DeOiUI)LezyR*}w(EUbrFa(UorLMQ&wU{B(?;E}QC-4{|#{$_oD@FZQ>aFHAek(H{0)O49t@*9VZh9aC)NZ!+8@~s*3%#$Q1-G zuuJ&GV&)T;GW&8X^QH^gNK}N+-AK)mUIlTi_&qAYA2^I_N}J+s&cZ$mZaNsm7V@#c zU!0c^8<6jc{0Q3@^VCbpdr_1m*R3b+i}^g4>>v2sVl=zW^y8ZK7NOoi zg;yu9^t*I7nwP3)oA9=&n#8J^#i<4RmP&K>R;k4^*BF=OuVl~r6WTW+a}rb=esn)F z&qlAy_l7fa817KC$dElW+{|!FO@xQE>D+*NVCp{Uf&4wGyYqXJe*T{1eYuB{cjq5U z9-2Iqd@`q``ooQisIQ`BX=moWBW%JTWt6AyVXwc*9iXLjtcxS!_lHM zu}?j4owh7i*S?kQhs&7CTWPKqy*95~?}aC`#S`|A%@p7dt}S`;YJCOq*iFnIV*%|A z@EMzk3B{hUHHH2Ib+-R7evkD#NB(irJEeZ;pOVjrda{^`{Cf;Yt9c3vhc@gN%CX- zDP!Td9PG^Yky(# z24e9JV&6vW#M)@H(LYJQerml`JiR=%bm~^QYtMA@v6;W7n1^6j-;dU5_n7$!_=}=Y zvFHjap{}^0qol(~m7|x`iEL87bMmg_J^8y+`}2WxTmHAHM<*XnJ~8=N^7-tYN}e7@ zGySttmpt4NDxTYPx|IsKhrBh{0fTao&9Xg84|+ss2?;#|-apZAs$!-CjTiX#^=uDV zV=rXJeJ=YQmoeYIfSpe40}FdH-SJ#Hwt?64-eFUO-2fNlDeRTBu7dT{>ei>}OB4Sw zE90()dsjxzb&dHMtkHA!C+0`q=gRxuN%b$@S^63O$U!b-KC^1=Uu8fRWYa%oymJ<)Gre?R{687Kp;8 z2PeLVV%t}Oodoy%{>(YMqW^~dyBPa5qJJZLa9f-v(T`&`zXd#YX_4ia{zadA$=((G zt-zExzUQlf7iw82((=}%mN#;G&hRzQ@?&gzzIctN`d*+0P9O(XAp5NDa2ynnJw-}} z6M`einUE*Klir!cS?4R`ulCo}QQy};a8E1WhFLkEGsa&C9vv_C9rYnJ=ROSk!QXm~;D-<7pO*GVjy8@yUnTc> zDO`>&yVdOT2Ed=-`k_Xb>n4NhW1F#<@nF6j&XtJ&@Li(cX)sfS)*qTf-1lY7gQ7D> ze!0k+4@+W>v%oI#`qN$1Xy9vUwS3J+@!rMuN#vtT&AH4T&$e5vdfvAN@K=ugBX7kA zm(ee7Fvxz>XY9|+#zlit~qc^kL|ial}jw zJ5T6UFmFfA*u~iP`QnO?c5@org7v8+_A!yyA|+6~j3eQyQ}DE~ zg>VM_cicded?|68SEgP~UVoas8Gnctr`TVwj$i}G{>*#PT0G`WYgf4o)LY@5HRCU4 zqh+;N?2w>ud8>O>`pTdzeb>}|%KbABNb_fYm-;FHllrYQ3I|V1x&CswG^$Ll&s9-n z7u~;IOtz$DIXZxPzY6ZAlaskgDV=kr+w(zUdg_t!XR;3|hA-*;_(t$&lvzzirCGt= zhkm^;+YRzc8{j7j{|o-GA@yL3*$>{+_3ldc;@k)`5fyaj zdV2bctu^dO>m)}Jn61U$Y$7Kqr@ye4Tyhn7rT&6GYYVd!mH4M>K8H35{m5eKyUU!6 zDJD|RvxDIs?}Ylk_r7}4J1un4n9me>$tPA3Ic)`X5Aq25{(}F&u0Sv%w1x}0DYazk zS+UP`L`@E!7`|7;0^(i}wMKaK*sN;7iKP#XPOPZs#dW|6>apTbV-`6tIj*=8r-7MlbE;fK;rDwPudCR1p6^8euLg*-hWGb z$CH#bb_=$uPb&^KDAeu}*UXfScJ6Q7RkgQeSJjL$dSLp+(a+hlvOh?`QyrwA27gM_ z@a;Nd1FD`(QD4Fz>P|bUITz-7yQsaiG4~Wx1pew`9=_nCiX3F2b)z|(temZO zbQl)EXB7EKBlSPHR;1^w)P>9$s}x=1Qn)gOSd^N$PF}zPFBjuB{3kZI3fqM0 zV~jm;0#60oM+_%yndse$m`&i1Jj7j3JrDayJj>_BiNBw?#;0KW;u-65cLOt~qRt}J z?7=qp7Fc8^lSA(}?!5`@#eP`uVX%jyAKY#Xn-f+_Ne6}8qhQytao9YtrRAM{#$I)g z0iN_}Vb@gbAwD*AWBkQ_H}oRxW28i0q-E`#o;ULbo?4I6p%#!ggIDaO@X?9rAiL6T z*LJ|5YIn;ruLiTyzjCIG3_PrbW~td?)|zaHF)Gsa>0;~;y22)`iZp*stJs)F4{oE; zpPN$m&gA5Md+tuzGwD?6o;y+>O#Pr7@egRvhR2kT!;{)M|EzHqrHNB$Ci4DRpEwVt zPX_O6Z}~^n&-@RSWJ5v9Vp`zHq7N)yDRZzOCT9)neBg$w^v*MzzLWr!A(I&Sp9?3bI=FR(lg#sI6Y} z#rm`8k*lXYGzf~s|K$DY_5dAMec%j;T(7?=hN@mui203zjc2r zJ?PydMbX}gBe`#OflF-w)@ZTdz!zdS{He$#sN;!RipWt#UlvY-@VU$ya|CBGo;`_k z4Df{i6+8;C7u)lV@$(n)cd@=);22;BV=S`AEROp`e1Ja(*Jvxb=z7KZ$3@P82ma_$ z7VySg4D}A-iyH+lxi-U#>CB8}CRE9@z=!aqvAY{1QcnZE1nbI&lmn$^S##Fs>kYYYZ#jds#5j(L+tci zhJN9_)XO%TokqKfhe~&uUFohgdbeg{rj8jHR5mjk%yP6O=bO8NX?1!klEaz((w#HY zQaIyD{ag?2IhuT7=8oj#NSRsdGR)FW4!zOrG>#5(|%&h43VuBqDIM1$Rr&w#yq=^0p96x-U z@Lggae<>0K7`OD3`|LYBOEAe|P1peq(EWmGF!2bPUow5&`Pq{CuFL{4dpY-0< zPNPHgljCQu@`bL0;0(eAi)|paWU$As6Y#eYyH|-{759kH{wUy2*uE-vlZfXy>qXvi z5qnkOcLTOA_S?e#73>`t1;?=;#xGNYiG45i%k_FCxlG(+i}8l8*UztU?8i(@j5)5@ zcu|iQysSF@?M^EJ{!+fA+CK4~8{54Ye_&AH3M}QcNoz`<0*TWa_{)H+7+V5gvE2iU zk&?4>YLpJN&=Oz6i7^?8?}cp)!D0JMW1={}Skn9_*|A<>ZnC;EBk4W*H5U9Ov(w~c zI?Pt9HQkK9=Vq%ST>=|uReFiFj_H(1WqQhxa{Kbh12Ydv?zAgKGj59EUg@Dd&nMrS zxo6`3$@?bm$vsSdyC-q)^zX)wPrWqyME)?UW2Y0}`5!QU{s*+(pO-Sx)5`t%Fh$;z z%4W@Eg1n+DVgmsALAN)i4HMfeLt}9$+@^P5mUiB@1eeXNztq^zWG@vHP05gy%M>y-1l@nrA^<)HUR?f+rxJ-Fk#(=*S{ zu;Krks#YIU=fJIPY3Tk4rzhu&`Q zti2xRwfD^I*xutaJ7+!K9s5h{@A=;>l=lyZ|Yy0oHr+&?A z9HW1snC+Iyg_J+2hNr%aYAHAPGr0iqf!?cf4&5*M9=KlI2Uo$A^4=`(P#s3Gsd5k1 zZ{QL<$6w}qO`nZDPpa#%7aR;T|BKErzE`*dSM?(tqWy&p1as!^>$*2!51(;M^^It` zyymROd5Z*Axo$1r*c7(vN50j*FU%$PdAwhL-oBsm=k$U7X7slE9qXz0=K34dY4E-d zSd8uqgF5>6V0pB%Qh2lSuI-V}U3Lcj2``7P@z4DkuEXKrjDN0t9=xIA$9~Nt?5%5x zZo{H^!eVug|>|Z9I7WVX^!qDSrC+51G^ctoV%i$`7_b z%76Ia7w}&G{l*Wrf4%Xi_y0@&PswS%+WKwldk@~*c=)ig_KS!A(|UCOBe;QYl-}6N zvm0xsGzNb;SDm!nisQhphPRyanUrx*bQOzHwhzCHLN+Q(!?Kmsn)zJOu+nuU)<)N~ zkNS(|&ATgC>|@CxS6olozn!p~=zErp{;t6vJHY4{s27W_AoZ2=!3fs^-!J^R{rKO@ z@mZHz3H7?y{QndD{UcLCf0g{o{YCX>rJp6AJD;V$??{)vjXA+9$~TGIge&Zysh?zi z)O>B`iuE3)^MSqA+_8EcvSp^O42IyV=w~Q?R1f4jKGtZUFn9Tl+KaK3&ut%EgX@mT zXXtU%@mI$gv-IZmz`X1(^B_8!3lUD4b-`ZupvyjxOu8HXMz~QbREt(v#eQWz*kG=X z9UeCv0VY>OA${T6?SfbVYvpk+bdIcZ5}apMK8$^zCTX z&!HlJ%^i%!y>Wl6oP!V9;lGpq()mU7wEQ^QhLgI*dF*}|edxT+p7pzE&Yxnl;IsQb zb$<8tUljlL!5>)DPi6`e@7!A}zrDWk@afa#|M~UbUH|PzzhC&?_It$-9{i~Ad)t3q z{QIpxFaF`yKji-#+VP)l-7miX=;v!cdhqM~pFjA$!mp`2fAH}A^|$UnC^=gjrF*8I zJ8P3a+f{lWFM*xI>2#U+&mOx!0sbb~MlpgO|A-Ctu#-j|*7#rP`emPaFSSEG-^)zX zdFKcmM%lkz*uR%i#O#TO*egc-$INdlwnZE!={!iY<~Y~mHnyUjYfl^o{%*Mq=yFPr z_*?8B$#UPn^ZzpWrTx?7Q|qJXJ?pFF&+Pw)PyS!VXTXkTEwVU3eib{I#RR7RD%&J1 z=*aE~lft<$Mqd|vwSq;s^Rkybul}3jQ+R$Emsb5><1GgBS- z((H4Pk8fum-pwjMuPSc1>@x2S|4;lWK9+t@W0Qr^Zt}8~FdyWtLM0F9C9dTZ@|nVn za3=e8-~5^9z>+X0Ol8>O@4?|i`%(0U^SV#2V!z?NUZPi0-gdTY_yeQ%114&E@voD? zgg+jPgS}C2G|YLu;bpYLuOs|x%Eqj9Mqiy@?cb={O z_}OPGUw-G8>%a5#w>JL!cYe6~)pve*?|*puzpVek>%X<}dr#ukCl9}KkGMSlgY@s5 z|5W*R)<^fh*!X<=R~x_B{$c*3?LXQ0AGiLd^sCMHE%BtkyR}(Js%4brW~^d$5uW24 zR!j1dcQ`r+=CxyClDwDR$L5H2yV~yDfa5n@9j>brlkcME4F1s5#rGQ9hpy2rcEg^6 z_i~i!Q1Hh#*l$JGqwaLLOwPjR-3Gse{fF4TODQ|lcx*6zdG(TY?QaqrwJ{rb8r!(P zyd(PG#kCLqA^uDEcQ=2U|IPGArH`YJxL^Oy{j=y_m-nEK zL^S12hm-EO&$gRjl#0tpc{su{gtxt0Q8SgWGu{Oruc5Bqnkj5bWfRR8q#!*19cR_~ zFqp4=^la|8-~Ywp7w`W2mA`!R*BhU{^;_#7J{!9CckliB?x*+P-1zFjXKR13^9nqshsjsd~qqy3gc3J4Xi5DQ!bPt(m!^9%{_P=vxg)&lyc!6Ke~9_}+fBt8SsM zaRH6X)A14N$=bh(dPUR~bVDWQ8ts`1Dk8& z%B<%h%&VTG{d(|Wz%_FOd{2fyVguv*>+^l`zv{UtZyJd5;BO<{fIpQl(&8;zn|8_C zEOD%o&0}fPw(N?_ffrbHIabN3+QkxI(+;uA)w1o-d0^`!-G0w1R|AIzvt6!)HryH~ zs)pn@Xk&()VA2~mwr`|75)GG!qCwulZIruE3yok+gxt*$6A)BeP*@lKhzDmYRX2XC zvQc@jx)gu7Isf$Iw`Sk{WOM%0A3UDE^TUzp`17kXpMLUB_x>UMRr%A(7^w?CxU`)~8Vwf$xBv-`hf$Jyi3;{9S_3x3uj zv$jW>(L0(R3kJ8Qoe6PHQLgT(!r9|q#z#??=yZmZe~@cn|ER%8OB~+=M!T_*t#F^= z)}xPk5c{{6{C7w6uj1hl{ILV15BqSPx|%d#r85Wq;HnOQzn(PfCBvlw*VoSLqJ3WUFy=AbV+0B~$%Q$!PgYtXuSu+gC zE~zG?{6h5^_0Hr|v14HA8}?5%L-KFk8=7T7FADrI532+In#oQ0H`sBwEylho{y^iE z`=`}Wy-^rd+^ATB*ET+1?@hMxI=`E8Ed0Oer;t-qJ6v)LPCk9VoKhA1u~D>C zDdL&<8rx{=@SOOl@K^a-e|CyQ?PRqM&)M)_gh}yR#bb3#Zq?yAj}8OXv}m%1znq(c z2|E%EdF(#)$a$%Ti&2R~YSXZ!W_zz9x3V z*Wf?8$uUrWwQiHo3V-s!UF0E}MY<{-SGdb3qJ!Q+lxolr1cQUb6!5dyTB-W7>dwj^ z;ikZ^z2WAVBk6|2Z)zf3$6oTv^Wvn&?d3mYKKJjVzb(HXyyZd#cif=jJdQqQAJQ%F z7}vQaRt|fOYfA3|8)|Z)Z{)ez&lCp8cf}dt9?EhL>MN$F0e{=Xe#C&Xf4o0oD66k% z?gI|0v3-Vz!0*Rvfj#9r=p|uaYrdU&;!VRxxB{mK4N-muw$0SCscWmR!p|2+pL>8F zIlUCPYSa?rR%UqSfKWaxmcSkdEjI90GB_)l=V+&yKMQM41^WoLgfoYR8kj7AH8AMb z5BT$n=A`hdQ!tqF=T!c89Ngu+u`oyFXT%)}smg{K1_$uLePOTH7xcQlAsM?lC?G^+ zZDCKeA=^t6YxB16~5AMGM-}$5E@8ADw{ZF=jr}WO&>u9^}v`;b{ z4rkdv8vjPHT%Gd9QGFfQM8R4Z1An@T`;lRVJUY4Tpfcs{h-SS%j{ZyeZ<0TA zekc9h`Y?Idc{l!^`ym?mcJdN^S2Qij3sgfHhTB!MjbIzx>8Oqkmal=uj(E~>ynIkB6)FYtVBrs&XYeQ7fyJs- z#MYT3ziVUD#NU_Q)ARaT7<3GezlOc4c@!4GpbisYVUAs!qb_kDlPH7Ea5&@)21D*p zFzDV6V2EWHbbCS+HN&1V7&NB~6|l{6{3OYz&(gc;hsnbBCzaWUpS(V|{qfd9<&${6 z{83@9_+D=A#2b57cA^uwlYQ|`@qZ})QT)vLo# zQIq#CqVKtX!S2hysQzB@7uAo8PvbY8r_tNqv-oqcagzERwGn1Wgd_2lv4w`iqI%!g zxKKSApG#bK9bQ8Ah|PqDpq_8dAL9?fCBFDNn8@-Y@Q2UM;y%OwlizjPz@Euo6$q?7w0R_Ht%kIaDQR@ zqhvn)B$})IU~5kJ^FJystiCsWr}M4;)qzcRoua#UVr#(q&uP*5@1lR z|6%jLF-QAP0lMDde~$mC{Ikj(=RmR(9h%er88$DVqZ%AQA7imH;f}EZxt|?~+S$gw z4!HA~uazzkd>8T!at@<^W$;I=V|*I@+(YaSW4|!!G&|!LqjO0Myp|mO-%iad5py-G zw+i>~g4q#gYGvxla!ot2hu7)-o=BMz>|9RDn;QKclyJliazMq+TuqMs|_{(r7yK3GyJ!xu! zJx*UZ?XNib*e(^HV^A?4u^u@He%72U{>yxAQhCezZt~1}H+iSU{KJ`OfNlQ%fCh_pS_Vj#5j`ksrL^d>Q|f zzkpA@fu_OfZOfVgf@=903SK#a)f~R*1-X?pz*fDo9cqMHMW;fZPfVM6% z9-LA7<=CY@dYRG;Z?6)SqT}3ErKb)ywa0_n8u(*|6wQ=F=pLM8*OK~gyHJvDVs~qA zI!cYNgBf*kHsRe9yImwFxm}ynQ5+?|$*xxZ{v7&qJN$h5@BCl0o9Of8Q~UksP51SX z>Q?l)9HRqt9{mn*XE-hffAsC}YuGT=@=X8NZLN^&k~h`jefU+pzse0B_3y-?GWJh? zmza!MJN`ZQ26+aYDt>+w{$2GM`N>*+8vIe`QN4wDLA7kdr7(W9iFs%3Rl9)Q#IKW! zh?jfA{J!ANygz;xd5Ou7!M5~An4K|vg_~h3DnOmdw7Z%VEoR&@AFQ}fu^^AKeVP5! zqgUFhero?H{lxk>{eku4^hd=HDi1f@Ee{5J-S3u-?=4sTqED60uTpb~M(DT<6ZZ*s z`g+Liqk7fKhL^_pRLnNE?uZm?mmpW2^% zvWd0uac9cZZ^9GISL~<$vJ-_QX5Auc(_SCgRd3bc4?lfA9dt7et9%(X6WLVqn9J!| z4<(G?Ayu#6-TJMKAI9Hx+-Spl7(F5P*rsZAz(`O!Jmm$RqvL4 zy&)W%m{*#5=dhc~J-{EYYw%pw`;-3_?o{)s=hIht{hRy@^+j&7i~M?UEot*Rn1Wu6 z*VxJf2En0;|A+yV3oGtZ?Zx784bI@x^nLrI^dsvN_G|wr{ms(TO)v-Rz2rv$IB#s7j>1(M$zjeEOqn_q9gHPDXTG&b%qVS%}=O!zJ0^%c--y5TtrQ{ zCmajo?TQ<|jWELp(Vh6?Hx}L?|JeSx0tN$e5odnwgFE1_cV*^&jy?rG7(U*y&Fg4g zo$yA|G4PjTFXa@RtR>s6PTD=x=&!P;;f$FTI?cS0@ORqZpDuVan|xk1o{O~X2RO=i zGyatM?5WALGj}BX)nh<-%j{-4fgfilG4^i{@!y^Z?eP%4KD<&>qsZ{bHNFY{nA0Ja zukmNFu><6wm*8XlMzj{(P2TjLrQaASHDa3u$Nr>l+pEKVptP#V>)@8Lf*}Tl}*6b4fHaOFUPv1n|tC)?L51S`mq4@NQ z5wiMr#uHQzS@*$(__%kJ&+($+qUwEVE(9Bwy|*miP+ic}5>?Al9Y;TtST*i;+klj3(_1ie#vqgbhy<#%iT z)+?4P*glgBm#s1tbrc)CGx$5Oma?~q9u>1##Oa#HVkX1kM!V_hHK+3- zoBP1)U@(Xcys|mqtUPn>l|S?s${z&_n;*S?=dBN(-F@=@_VT0mtBb4eEiLxF)3bs$ zvo)eVEt(i3o7e3|>czQq41MODJ;siXsmh|giXMiv2H+V>$Ant1@TVB?WPs*Nc?4a+ zbHr?e@SJbcH>0m>YJSXo)O-)PG4+a0m;6LMc5>re%zm6;pV(RU!X7G{{s-*82KI;y zQ{ywU=gNQi?BOp_bJl)f^=!3^20jw6)z39Q$87k&irx%Iq7VI_rcL&<^mUuN&wt4A zf)Vs{q|b@XQ!FJesPKdh$~f`Hr()AgT&G$G*c4Y(c1JeI#A>p8#Qtz(__z9d;ZwX8 zX70#~E^7us`DrbORnDQDH^ZLt>TJG)exYnIb@UU_5u+=2Ntzwx%lbJ@!Wi>o*gl(| z&(9<7(yU71y-Y4eL-KmA2EKja}x;}a5 zC&{e!gU$KHPbTj4J?preL*<%2Et))@e07-ZDuY_EyiF_{j#fA>ppCtV#6Nl4rsSZ z8^t?L4ze%WL;Uv|T6&j*p-LZmrlvnYFH5;cn|i+V4~T!@hx8Hm4J0Gf7%zk27ttO4 zAb78`<$OQ3O7rESGadzA#P0J|J~MIN)t4~&E-`(psfmC$w6fTZ1s3^Qbr$?v-TuiA z316!DfzulPOf5th;Rt)0iPs$O6>vgsn)zH|&v0DCA&~u3U0e9WzJfov6yixl(iT0d znJLxPRR=Kj8sRRhwU`-VzE`~w)pZp6-6G$j?nx#PmMcL8{e!zG=QTSGaWhlMQUPPT zVbV!w_Kv%r*mhQXE}5DOJ{T;L^VV_`a$ecJn$I=1kNI}ZrVE3zg9pJM7S7C`viX!P z=x4)b`W*dUYjDnIUo^WX(IG+6P(N_w(FH?u-6h_XlkY-CMVr}Sow_LOj!9lA%) zV|`O=$#P{=ry)+{-0!$6*6JNMn2;M2`_Vb&aWEW?ZU+Nz53P@V zF*Q8%)$G9BS91fiU(F26{9ue{8xv*?IC|A8bb?Z2e~PG?u@2A_6>XqgMH+~$~VN- zqlZPV!2A$9Btr#D3+?6uxo@6(ZA7a1kkIs-!4}o`Kmi*V?uPy7jk$1q`7sla6 zOTYIDb-ms3mC%o$l;26-avrGe8F+ymtOWP`g%E|#paIUY;wbzn*irsqdKWyJK3XlG zsLg)px`Pqr2w;vQ9y9jN*u#u}rMy5kkUBPYPql1xF|l)PvTr6gQ6EkHTjjmtNGqpS z&Z4}h8N8jQu5d#8!^NjC^GL*Zpoh3r`wpJ;IMejnr~|4lC>uzR4cnLXKx7Be z_Q2+48066kDo48Wl*qa@g5&piK7XAhYf7U(J`EmPnH=DPGzd?9@2=E8q4_K%fz?!xe?R9Q~W7>^Zj~M3~-a5mGw~Cv84;d zw(taM_(RJ8y$#J7#if;Wz@n-|=Y@9>-$0$DE$oU%y~$*m9NEPls?V!_@YlYV+H^tq zD@zx~#C~Xfr03!C_0!iNQi5whZ4Z?s)bHX3{|q}#j@RNsbR{GFa@ZMblL>YY-#Z+R zG7&k(Ub}%O1No7kj^(C=yDx>k>`Z-`8yEn8w|;PIy8Che2L9I?qy`NJpJPw<5AGhZ zoT-^`|4}y}{yQuDF@tdm&dG^%)SpgoQ_I@r4#oO@`n)yV3449i`?}#Hz-!|gD2_Yl zo=6XKov5ebd-uh=&?$T=dNF<>+JUCVZeorT$*_mdCfB%y-N4RZ^VD+_Po)PtH~{{X z8~33Hd@|X~dsz;u$s_+k!bch|m-9ZoDSs;LN9nsUZXo^>AEnM;QEfwg57j$N9ohI@ z<(|y0nY=>ro;Xk9_~DyPy-#(yZ2p~So8LqH0`M%Gs#+g)foApP_>45mD_*wd&9ffe zbL`175wB7|!1l5C4E$-w_&>*=_E4zaZt5@OUc#k*)=lMI%tD!1l4@|^!&!+#=GCO7 z5nE^vnYlIisCLK}tjV;^!XNV+-1OP{6|i|uIkLAF+jJvc8^0_YXmSnk2ktU`BiTTL zOZ@N&=GhwJmQY*kP#lNh-VtltK(mM1MQ}JIFJWH0JJ^EwW9&Pp}8cmj{i_quVFm)N}lq zo)>=P8rO*Y!`MIJ?_T<2`0IKh2PfKPtOtawo7OpIWnhe0>G& zxO41&Jd>U_T-polDPe)Q2ZAH)E4ik9?uz@cd(0z+$$8~-Yp0j`btgQlHZ<;ADwo*> zdy0B9_f2{xXsI-pkH&iq{$7e-pz}ubmz)T1Fq^L3=Z2rf_2{71BHq5?(IoVc@k6R* zUrSGV2a_{lU(y(^#}9)b^8KC|ov^sw?~Pj7yP=&|)P-yD9~dKVV9xg&IfQ0?rC*(0 zbKxYb$7Fq2VbJhku)&w%zh1;viDRXF$Z)OXBe8+X4eGot@u_R|PW~;p&A14rw$coL zM>|PRmrruero?~fiPUmm<@v_;3A4f<@0I7^Fz|B}UuGN$aE)fs&1lsl%nbr`3v|#D zO0ikwt{HYRn^=JdT}@Z5i7K5;6Ag4KtMl8F{$P|{Uev8Ublq$}A@1Yf3Wt6L99Keq zCi4#Ei|{(wV?wVV6{4guYK=Oh{@`|agDJSs>w$1D(5{x$p(ni`wfO8RR(x9~kp4%lr$?uU zUGRs?2jXkd-EfKRa}$0cS}l)9Ie*;m2zub>Hj#Z`r+Clx8k%wtlRJPx;fCk&r5UeJ zc2@OeasHmqwt+u!0oeCL-gMsRWTH(37S-#eA54U0=Fp9;tn&tiKh2Ej&*U8Zd(~Lz zKUdCqr`R>By)JB?2Y=_a*IAebXTmAkeJpMhpfb>0- zg9PA_ohxPHgG`GKtyi{*VYg2(yLuz)3h1+WeGz=Dc+efC@;erffxQWL%HVGXPc&C% zpCW#B;0L2~V?P@kodA1NrUo-HJn?02c<_t<(e@v<&9uMMy@qZi8bQQ>vUjyO5d5JL zB%L5#e}g~eALP8JO#S63eYac!BbIoqBR$5>>W*@s_912XGyWGYAb)3aU*e?(dMC%= z85}@o<4n+mAH2Y3oL2%67-0W`-SHt}i5?;i?pF3;b`bwDCx`E4_Qo9o=k!i^uG8z| z+MY@dQpfF$7Q=EBL<|0U*w4JoonYD@jrxeYo3!^2`_yb|DD16`IY#F|yF9*l~#O74MVpX#(rATZ+|W`xGnkZ(`LD&8%itU*HFkQ^mKk;A$;yj+J_=9WdcQhPt7fZ1LH=?af!#Q3$u; zod&k@U+{{gtppUXG*c(N1@OkVH$fp8bfVYephkdba@v1 z%|_!m2Qi#^ zH2C9dW`xK+!5{TA{4aeE)%}hHZOOPdL_DUPw;OKWKIR(nyVyP1Kh1d1XTwis`avzk zOwu?#0{#w0$KtcZc|1qo&1~WfUx;30H{ua-U($Bzj4Nm6I;wuyL;jKN({uoojVL z<-Do~YR{{%rX_nEpgewf`je68FC-^uHU zo7zCmLFbfO?!NFKkEe-)wO^d45hQq zQo4>Uq_&f?F_tYe^g4+7<%_l7MEXq9SZ~B0o+t0U5I09%;q4f1K6k`;Gyw+3J#3+u ztB!gj55dlZzW0Vo;BRc=XOqK|Kc5-`e?wzG%MB0xw0G>*kFU+#`fm4%+23YrfLY9E z{I7}qz+aX_5R=0*H~lyERwv!%aWH{>r0RsW8b0Ds<+RsU?Jk@C8n~0~>qbA4IZ1q* zv>neXa{_;d!sFDi*=YnePQkku>)_jFqs!5xo0t;YL$sn9v~JTU>F1NVjSbYy+X3?8 zZvOtba{$g(XEYYxiPp;b=x%u?7zrlXA34C3;0?OSTw8V>Jg;GZw|eejt}XZx4yh~4 zj~Y9pK8S3M@>f%H!Dct&gQU%hwu0_|lXsZaWKh>R0joH{g#ed3M47Z+`gWWUO zIbvmLNC&Smn{y~S6P%-NuHFZ=A+V=7Ks^rCTBPBd)fBluOe~G=t#Th^4i6GI$Ijr9AvBCXmMl{ zwb`w97)nw;nRS+#%;7Wi8iR)561tlWFkUkJWn5tTKI{d!!~_!i;g~${3x>TB_Q&Q> z4DP$n)qe-e@eDTfce}nj5axcG8y)-E_~69PCkG~eJ~o&m{~h?EXSD4nEz@n!I+uI5 z)U&|u8O+J15&!Y^pkhK|Ic$>bADW&Qy_1}wbe#AQ?1^hY#R)A8>q7FXbES%wG4`)+ z1H0hKOY>Q=-vxueW7=bYA3DYE&QtX6F2Xz5<%2);)KIHQI?K1%OVeubM~#o%qu*%t z$_96{TkI$?U=M!qpnEXB6n4kc;hlKVUyNs%&YvXuyA2>iQ14g#ROW>U(hY zvHnln?e6b)mwMjoF86=0{psx}_jzt?>}L~0;BRRB=VL=-KhF&feBPV8^^=y#o6oxL z_HOl3H!=7VcLmHD?746Ws6QM3%jYfpo#UO1xnrp`4Zxr2^Qz|D%WU-_=QP*3V-x?& z5qC>-8p=JckmsFC&!BM+{^*$;A%8xXzRK>1W_sMag+FZI{-`MhJ(X()e_ixAn41&6 zO%G3dOm>lXs4jRMj@V)BUtcmsrZpVoqRD8O4XNE>M=aZi9)2jjWz}(2Q>n-Ds>Nl; za9QegKx6Mz@5^d_%9UDp&tL~`*D2mNcrn^RChh~rvVFvPCf*~~Q@r>5$o8q%LmWp= zhOR#xpQFh^e;+%)523AejQ9y#`kc1_{^+A9_f_ph^=IWQ!d}MRmpxWIVl?3J^|F6A z+0!r|MjrEP2{mJEU&fiw?4ar4lF!Nx${!ou)FdgP#m1=?(EqH? z5?OQUP>IR{zImSg!Q6pTSdZxPM7%=|5wxQzRA$`D{9VuzTxI%FI6Q+M6#KN9{kXYx z!)|-0)9(IWcd7q-YX&W#MGK2l8L$6}r5MG)FEVzpU@mK6AKMaO=C`i_WX;0=j|E?b(#?t>F($ zHc)GYD`Vz-=_^t@KTH2}FLv+>Dw2Esm%$$siTSHh`ik|?Dq%EgT-{Zc9fd@fP5?oq?H!tP<`h~szYNS{`7$MU`6&|IT7t@$8gzB>L?UCX}aIqjs&e0>u- zH7CK|5jNoNW#{~B@Eobx!-s7kC(gJF)PGFQo7q0$PdV?`d58JAU*j(Ze~SOu!F@aO zrHdbdIWXv{hF3e_Pq}Q(9}9z792ld4T1__5!;&TzI&A2`Fq`0W+vU3y93{r@jZ+GWe4n6u$-CQgEhN z&u|a)s9pz0&&fw$rM^PFff(T=xjyru^a0FnLFEM49?fnl$KVRY=9qbQgGZjrv;d`* zeo^r*HTGB0nBIfVITbUs3r{g`D$a}gC|S-So2PnFKFu;`NI_4vea=5Qqq_n^z}3YaPj%)Fa-BfJ{61PxIG{`b75E`w{#p|`Wec+8oO zXI<(q<%KZ!u)jF`liW1;8yyFO2Gjf zVgKN#nBFZp7Pd+I+UW0)ub%;jvVHKQOkAe8535j%%jz+pe59U}|Hk&=Pu1%==^f^G z-52i+cEX^ig6BPy-IJNZ&ZaVVirJeHHB1Nlv@;NW7--ir?&0k@j=pjJMz~dW-&@ z@`9h+zFivmQErwTBuCymlGFKObYu|!d-KOvr>}hf#@)^bT^2TrI1YPf@({x#5YLGi zkoq&v=ZN|6vsT|`AJwBi=0$YsIP9~ilb|Je!r9CHeT^toJq+!;yaLBmKKF2R6yBSO z|H{Y7gZIXJ=^?g+m*UsTbl{jRX^FbwMzx?R-TK`2Nv}h7z(Zy74!?)FapsNLc;6NE zhSVR!oIip_PCwB{m)}mG5@tGgJBu+@hEJVQw+^ zS#BctMJ|`&kNV$d{bOxEYMQ<9-PV;MrmgbDu_jVO+>+W(NcH=;@7tMs8fa-g|9T_rhKngE^EiLcNP8IYw%0qnBsp`%O&46xvt?Jm^vT!ibv(a!d^36`UdYj+yJo0q{Aud zB<%GyvvZ0g;Z>P=%ICNcMufkbovQ7LAg5&}fO+`+aB6o`UEjgIbugrEP`*U|b`Jit zdNPXhcwaTXsfkZzt7VI^%jPu{Q`Ru3`~&-nx`2FeJExod$E$(sJC2nU{bE#1O6uyC zJ?es5O)x0?hvranUpBA=28jpjbCsxWg+nf!7UqC+ z9`oot4iN#ICzjn$ABWmBG3_b;Qg9{gg!??<&G_@)9siCF_HO}JOyAqR8@)g7pBwnB ze{$fb{gZ>A-=6IJNz2Ts_fIZ$KIt!MK9X(>K9_t0{Na1il>&cyj#|tBxkTp{eNA!; z#dStQLyyL1qv0p~flG1f8{<82LN5ka=^=>s(h{F{kB5iRDLSHEGx*?R%$x5<=irt2 zKny<=?z67ZzWBEGaF`sh+vFXl?+cHq$vv1%u%ks9EytX_?C|WP3)zPSm5T z)Z?3DG@GNV>^Q}infxQGv6#LZIfvmotJcRc^Sg$FYH|hgWa`BD5_;~`N{(au;3s3R z8mKcEJ}S7VVZmUo<{RsFis$M&37V-t1$T2l`7ZNtXVw^hWx$_%zXZQhJoY9}L?3TR|6f z{UK>zxp*6Tyqb#}ET2WC>}+t3-Dw>5qn`>c`ZrJro{DC@`9QvRv1|@J%+=stWz-#d zI@_@hsP4?gYeW@qsQ@`X0Cq1G7AQFN4@ZHK769EG!UjM#4{ zc40U8yMX-@{$7kOrrmH4#1U!n(7s1kvWNH7=CxG!meH>)cTx*%LBDSo{VQw^dyhJz zHc)qiE(Z)qSO{$4Vn*}|@h3C8*iq#gs^h8VLQGE{qMRecpET_0cVMS8?w|Mt?3*D^ zlAaCwnD}hZ!(YgHv)8lywuS{`pX#;=+sB_x?xH%+73#`o%17Wa?gMqZVU>ZuEc&Ad z*N9)fgnblW%is?Tk`tk=p-0(1lZV$aC;TY~0ee|pTpBXn)Bw99vGRyHqf*%l^1<5X zs=NAVt+@VpJ-_yd36BRw%Z}aJ-nc9_RDMivH=#$87_Jt&`RGEU4ZVrodpmi%^mh84 z(!1&RO3!$FTifIVuDIXn_4=gbH5 z&K<>m_+Fj6!Fst66w2#$K!qFBXF#mH^nuE_Z*#bjs@a# zA7O^&kk55tBl}+VnO+Jmu_JK@`3GuB(RDaSP2eua>q~RC1HF?&KZEO*Wp6SSE^0|Hvs=D9RqSo@kmu);TnsJEMJ_}qg87m z=7W!S61&F^I&!1a5&aITuEtODZ!-)S?TyUWW;W2+BMbAst zPxcQ*VYq7Nh}G#K;2+3!nb=T`nR`K#8`MtqvlTx|>#yd2&1(qT8UFP5dLOsM!$m8O zY8%y&JH&uo$uJ*nY;Cyt2OFjJhik=+hf*f8(dobjT3#Ye7ui5zFtdS*1rurGn>tL@ zc@jUh-;UoYeJ6RB0}f$%-3rM%!+v`(WD+RM+4R?#tYMo=c$>O3)hYjUNIn&wDzi-> z$cb~{ECzQtOWsm=*IN!(ywx&WBcQexY1LT1O~KOQ*xU#fZ>H!K{0(mo#t7Lqxn(c4 zHq~4*Kdk!8F#bTZ(fsZvR&TT0)j6#6kb`6#KgEc*(bvs*f6~T4(=R^i9$+u$`6|0x z;EB?Mch80Fkqm{w*UD_qDzijBjh@b4@VCq0Zx=n^qs$szR32lF@TYvn;Liqg=I|LaZ!hfCF_-1jdd-^#que{0 zf94*L9qgnMH{e)7iM=4jxESWc!WPO{_fh4zU$jbzMB!gyBxK8sRJ_IP)2jk zKkaq6Q$BW2dxyij?y|PV3wyyjYOTi7l?&i4!yi#9__G7B7=S@q)A^v?Z}ZU#V&*op zfi>T2u&CJ$af-yf#mnIzZ!(+4{RjRu-y!^|4owe)d3AIq@VV$3xJQ}$J}T~Fh4`t0 zhAFZ4ariC==tI2{?I$08mE7?1%^T@He<&VAkBIn_ zowK{q*=UZagNCgkO8=+;5cLI395xgMZDAWZ*wy5^cHEnMgE}+(RH{<=+RWw|e=PnB z{#p43eL=V_r_>|kemTy}gLHf^MB3G>xDR`TpA+Wl_9wH2S^QV;y$RFOfjNhc=7D5C z*xM_u2=gCV7P*Y+_p0YXoN0Q3_+Rz$YJOMwcvc58zP6VC;yZ*zVOuub^bj?R$Mfyf zTQ#45%~^>ouVj^2B2-Fjw-goq66yn1XgOA7D>p7V27g(NCaY1`^AUCv(idTd#G)rc z4e%j$5Y?RIUHjc+z#hWxjYZ=QycTCNp5V+lv+=Ao6HVKb(UgrYjE7zH?w|;_7%e$> zqYQh(-wF)HwJ0y0Bs?wlk5j_xl_(8azEjqJo>_B&*iTras9mL+DVTo@-XEBt{yaG3RevtAs#ir^N& z2c^fM{;cvMX42Sqeok>3Jtg&J#cL+^BDbi;U|BxE^*4-S#el+y@xAJ`Vf)yPxDQVH zD_B3WDwBI0fiI-pB&O%4T8^+}@MkcnJjB$*vDZ9iHjvk=*+TMj?)z*WG~+|dZc2v; z|G@JDkr!b95_C7Td%AXP=8ZJ>rg;x1(^TVY*+Kj548o zN7=4)X_LLRbg6qb$$L5do2mn5`G;n1jBZeLf!*x~iRaJy4HaoDVsGF}90*mvdo6g4 zy4arNVt6#&=^sjO`ZvivE^#f<_ahGN#GYJsUyV_^F|#1u%nI-EUWw0;Wi-=wq&JRI z%w^6M{V8an^VY^BsyHv=^VK=;|4eU#{6IYhGtW-G0j~w@9Z$2IoSe6;J;axIU13i% zk9FM1_L;AZuT`$A-;w(ajL2?Xr{;H&xq>~(-bj63Vn1e?(J~~@-v_SGY0k*(1Eyck z_h$H0pI5y`9TOYWV+8!Le!gsC#-kB8TJK5t%W9ayANrl-EiG7@l1u#s{S9G|xpOd> z;V*j>{+w^%Q1xo@7L|?kePn3xFtO>9;6V2LS&!V4UPveKj zsKbmg^*8~~Zvl0urD)l`7p;K56=yYCb=JZScLVGdLu317@#4I@zPVm1Zm;L_TUN=9 z92e{buIB}mi$G#I2;DFQnxXGRQRpO0_*EEjneoP$vt<@UI?n7sX=h&`@j2X~7HZ7m zy2uA-C)4H2xWAgfy6RpEkFW{nIJu zgX66JSFgbc+Zh+4HV20tr28sgU9*3vF0<`w)FqNH=Szi}2234)w!tCuzVNLa_*Qz% zv?ZB+qc&ad&54U3Kdc>@Rqd*YbjIz8c*=&)>@dsW%to`$6n(W(YAn>E(IOh97C275 z<&JyT_}*p3ec*7_U6}IR@US`-Y!t-oKOTP^!Hx)e|H7jwEUNb#6 zQ-i+X>`Hc1*FbeJM0GHtlgI4+IT(^yJmNvnNA|%CWY$BV-jdDR%IAvPZsr={n7}3y zpHwwwv+w#C*gIzGS7&1CE!3Lf1%M4x2LWHyY&c->>ytfa{H}gaGaJXx+Y5tY2UE;@ z==HwBZwCJOk9|()^qkNf6xbIZhu(*=dtc*Ewl6!frLuj}Dp5_RHPil+|25cCyeM9b zYShy8&^@j^<_=zO#a>O4hi{fs&E{SrwmS(o`M`j3VWZ4`LGZ^lVG>`TFZD2oKB#+) znYjySt;znN+2QSpRqdwi6a zq`CxZ5At{v9A1iA$U53XD$w*&xW4##6Xywc*igggG`>qZD%d^x8)wSs4F<=l^BpDb zquxS(K-_2e$l}*utNT`iKd|B5ke)SpmtsBDZ?bqzILI`d8`6XRj>O;(?8)}+q`w0O z<5$AH%o{N8;$J2Yldi#K`F4D+J|E)Jn(zH4{>1w>Ifubr#z{pNCc#b;6U!be?i2^& z8d%0wvm0*3Te0sY_lnEa<>I~SN@=CKVy#wJ+2?zY$8~F6Usu+wwaSKtZlyHatYS?w z6@6aj;H06SIMibt^@Xd+1bHs_OD4&Cr`!=KB9j3ONCTSw2lKt?L(e*Q!)4AAG2oK% zy{qAxBmCu~LI@{)n^$~&Z3FCqzxCCP*9)umLnlvGUdHZ4z8io&;ZOE1j&u??iBczx z5)@P>K)dESlxMK7QhYp%J}f+p28Ri#@`dVEYRqKnV6aPbz9uHp^)c}mpE3T9y4RVA zd4Krp;DNsW0qoz)Fz#Pq>YNxoIT$iqMbGX^c?5nHbryW+33^cj)F6ewF6`DT-j3vz z@KyAQ^xu9E2!|%R0Na4tQE)4`LELwR{25O5^E@Mq^%OT{Gn0+Xd!L274c>&m_!wL~ z)mu*aa9rS*YYq}ENE3UR*zRllXRs$*C|oM+$RhQPbLR8IS3e3eFc2!dG+SL>32=-MX`YJXE+ex zFV;QP1Rr{)eAfjFg{A6J{%-Yd;Vw8_sjQY(D)%_-HP?P~_L{GzdV>WZ6w2|Vo zrQwT>w9w0E_oeV>Z^qcd*x-+xchZ`Qr|pqwz@qPK4@na>$T^eYj5QZ7+W6jD?z;^B z@U-L}RD!U9g=nL^_F%2J_GE1%|N6#y;Yo3=$ZB(@j%^>a2mTl#F!+nGch3(SHEZ~D zg8@*lI-l&H_$ctKEb(G`h(9jECpgZ|-4<+M2N>+A@zAje_1doZMfB5{-y#1~UU?Z# zlIk*h0vLb6i_E;enCxKc=v;b{I$%5d=6jf*P#x_=nL0RmuyiInoMY+BF4vYy8ricp z%NiDEbJXUy2c6Vit`PyMr*%~vJz_qto8hzLck#WNdvC;_8xA7-%ujICV*`8iIZlB| z_@LB>E>ov8yk@SiIQX)4W*$j6G`3FuQO}cqaELMB6e^}YNG|mv69lR?%k%EW2JRsW z)cbwSX!sn3%YgkSCTZgHAVvj$Chs-*1oahzIk*SBc4qsOqZr#)ixZiHQ~YUaLE>X+ zZ)+DEH9Op@=FziKZ8MRVc}l&Uh!UK1?;UBSE{Q89t&#~Iwk3P5&RXw zUlChaWcG;MII`&V#_AzcqYuX^M|UPblr`&7XbJg&fJuooIXYy7Png^>dWv3(V{67l+MR$2ys z^wkUpS^0x=GybpH@fo*+ZP`)_DZXpJNtY zJ(8OJW5Pl>ln=(obF$aXY@%$RnE^I_+Sp2BzO42r?xFYy?D)a{X-1bl?(Vd|=r0wP zQt(%i{aY$x3yau6Ft}1M*xSSgZmt+xxL#Q=Y*g}ve7asJfWItWbhO*rAs4Yzau2Xa zy=B&#jpv*>FnA|gDBTh6f<Ms5GUTH{OV8_=^pPCKKnq%QPrd?Y67QYc~oQ9||xCr0A6%NMb zl<5R)XJ$7v_p6<3+IxBt{wrGjncp>b?-(`^ZYefNJzi`db6w~=nqI5wy^8N;^Mnhq zSi_&RV9cL|6%%8~_cp+%*a?5^MKeLKj6;2oJ)loJ-Hl$Oe;D55F*IKq(Vx3Qk0*CxH6Am>Kas&H!f%)(T>IJt$`u)~ox>#7M=qwc% zD@&!N%H87KN)3Z-6qGFli^a9ddU3tN4jayTMLW-RN@#KfmNbo$WZIsIXYKiTPLIw) zbO(F4XfJ{{@MnB3e+GZcU=F(n{x+fl7|d%oi97a(gBsz<=w$g4J58^n zGtrF3!KDhCVpV3vD(p-Ef8-UO_VSv!fAWyy(LNvKMK1USxi5u36aP^o(0sYcJ&dof z_XbVP$oww)eZIz@a$oh}m4CWuq<21 zBYrr)!r>8HD1S_3rd*jFN?6D{$<$9Mt`Wc?UYrT&tbW0lKU;V+N7%!+U?*$wp~2sC z|F4>4lXFhL4>RJesD&@OcY+1*XKY|Gvwgn@<-6#M(NCj}ryeV_WEa5WY4_FSW&hj6fBFyqO9s3j9c#aGXJX#3X!lfxcUF(8&IeMl{Av{RO{E zpC8^bv-;vOqZI;|9^457(mp$d?W@nAsV1qN(Bd4+=Vtv3`CsABU@)_H#I@qgNFT;v z#Kaj*=;0iSc4E)@FMK%`{$%~geCd0WE|5g!3xtGZQ&KXS~=Tn(pZq5L1!9Xc-hQke-j`hJW!b)Xr?L;_!60#e zLXFb+1A{;M&B}?$vupJi{akRx$#@8=#Rz+@AngILll3`-HRT%E6za<+*D#ncbvc6z{jQmx%=Cm5yGfU@ zi8-Z{=#HRXLp(wC8RYFEx2O6-{ww~$PVSo%%wQ4&pksC#|0^xRI{vcya~*$Ky)UzU zs=L(j$D{PfE!Cg(p0fU%YBU`5%g@q}hcgBXvW*;M+FvNm$Mc2x%3N`_GH-0*lCg(N z#Dc5ELR7LGRuQtjtAxcWtcKKMlC{Dbd9bO+r1=7UyrKpE=7|3m5^P?)SXzpg@V`qI zewQPAw?@CWR(}!x;#GSs+Tg%Fh&ISU)@|h?;E!CSWbj8WQo~@~A43qUJtw;;$!>hH z7e>oWl?`gfTt1hYi*^cj((k=lMGuO)!8hWU>{?KVXG4T@Iymg^kKh-ulkiA-+CPWp zlk@}5nwhf0?p}6v?E-uMI{eoLe=jHpj&DbOD8-7i&{jEFCJ%;pqn?NQP>swp?JXY; zPrxg^8iH}E&TL--d*_2oFi@M7zryKkV$Mmu{d%nhPOib9@w>_7@SrYIFT?hM zS7LwVy43NEO)<6Q=ROxd$bDvNz}fv~Y9-H~)11I%G#yTO)Y6G3UO>eQyn#TmUi7## zMAr)>9%hCZ%{uDtr{O-+0|$TE=V$Oo?-#pf>}%ccnw|~2m0NUbTKJhRzWnevxeC|e z6P_ZsIZh3|fw|0X`lYj~l@#ZzbHzFEH=iyR6%WcE->a}QUAn0oq;uV2+rIqn-wJ4*A>c z_aYZC_FegE=7)tv{q9%bX^Xdd+~8055wV}Cy$FBVe=qUdy%fEgoFGy#Jaz1@^j@-E z4f^cx=>~t<&&>R8#&fx9^SN3aJ!ZC$IT76ts>2xGi}BMJoD*3Or<~z$)PXAy%$4S_ ze{-WO3E_Bd~w|ae=#)~Fo+#271DWf-35#I z&zg@H>^t$2u*ad#jNe@)29zySEGQqW9K_V0v3rGhL-->nsoOu<~WJ#5=0zUV+DAK9d>-v#;qxVh=7S4er_K zh|4BH>UC&j;)k_g<``O4`=~pM`>fg@{x|-%DrVuojJu-lpa(v0GnhL?zT9Q>4y4x!7vS-Hc#%GN_COHS#Bkm*LP+oM}%<)RY zMYd0Uj(XfBduQ_9tgmKbyc))^kF`1ozq_8RAK`?Zh+z3VJxJ^y`Mhe+8Q-*yJ;fH^ z=3d*)U1RWv{nhM;;WyOmY7NuaIPPn6%w7k+uG_-eqxr0bKkj)rSK7rY9HXVr?i6PG znZ;;~Zn~Yph%@VE_>&D>C=dghbEk^^qvtEqQ?y3Ee>p72_vm}AQ1e&|?P7sEI3zDg z)Ei;9aEk2{?!X^5klbUbbeEiiT*DRySK?*Ge&ip-fN|!J!Jn~#d0`LRM-Ec+zXpFc ze%K}#vB^om9yxLigW%BQ#>z)>L2Jx)NB;-@so`Nf_YL*?P@jSM{OY_W*#l-?WfQoz z4bge;c(|9E><)VH`@;j+z1``aaKG7|vyWMGG7s55>M!2{kz-8mc2_PsH|QZBs9vYv zp?P<4d+9I5r<_9}IW-kDvx$N!PAT{tjfwq|#^5Y?x)`HjOZ=xjBGLv1cjgfHlLIlw zhV8-c!hbLtF8FEuo{8(!r&c{rH8tfLW=_uBTUicjasbsoYWUOdsoy<|cN_S5=g_ms zdfC`N5UBGq|DmUe?fW)4*0TT;d$mw;RG_NJLS&$*uHeGAR9Q34Vmy&8Pcfs0SWB;hV#CL@~)d6kc zPrjG9&tMO`XMsOE!(PEA{>fE$v=qNe*=yB zKJeF>oM6uBx_t!hK66k8f30BfdVC757xmllKnRlA`a})SKMHsH0=+D>zYP8w+=d8! zBlc<-ZD6BYVD$ECw(lHTb;4c)?=ADY)bmU)qu%pUoh8d@pW7YsPlG?@p*r8p2b`<; zaPC9Mc2AhIH5Z5fwKM!> zy$>*_=Xnl|qS|SqSIzzA5Dj}w>Xk19&F+opmd$P?cfy?sX2?Be3bQ5#6!zvehzIjo zEVybrWeA_^P(Sa1y?bz;R_Qma7o1gxd^k~WM0J@3@(=3G$z9n#)mv=xUvd#@F~o!9 zBf=lCANg;ccN&xP#wPB=))j<79*eSjmKDRd!3PVA*g!`$Y1iZ&9ySprIlsK+TVva~ zV!!;YnX#wG#C@8@-R6G9A7wr$^SR0a6;F!;RQqi3&+7G}H{_nc1`?TvppAOV4kGfs zU~o5+=2`rw`pe#gEjjr5RiH&T1zZari0hLFRkOJC*yOotD|Y%e-IH-;&+qHEaD=(|^cnde}Bo z^W~b?9>0M-*?%3dc!fNN9GiYNIu879#{b4UuyrpJ(-K#ZRVltzUMyw*L((hey=WH+ zwQ~Ho;<)VImQ4e5JZJ2ijmng*dEgB529NwbpKq*lnb@_FeV?jrVpHMFUXa#}4<9RJ zHaoiQ2!FH1+3L*3%;sz!?B!>xvtW=|uyUtxhq}z&WYx+CmIDXWgNf?-8?IX_ZdmM0 zAx4BFV|WVmdXvSP&AV5E+r`)9A>*q&^7aHT_`=_}@U?KQsEF+_*M?`}Z$-a>LTP{4 zfu`aq@|&6JF8X(HxhmbZG<)&C%!!@yjwxy+b9pUzl?}nW<6Ufhg0DhODNc>>cM&@X zmtK3Bz~4#J4-f{Gb71?7?trQFam|G}<9DSsQ15-wbFJ5u>oEm=3iT?!wtd(@@oH-Q z6>{hc`1{lBp4(&aC+>?FfG>kV@p|a*fIsqL`f@MD`^Z70Io_aI&*$-{avJ5)8OC@m z<+>U646jrio-AL{{jInWE``&OTtLh2tcfYmyzp8=^thOmWQLa6^Z{qGJR8hn`)2af zn={~UwlGtbEhG-CEKrx3FM>nr(u?F0>-fz=5uaIF#+NQrr=~_N{L!CVEMoKYJa|*y zp?qVhNDfk5itq9WCY1-{d&NJX-d7^#E8%;|Jxu?XJfvXd<17x$$EwK~8)y}ol_KV| ztb}?DsW>#O$8N!YGP5>ze|Tevy2~&-wnt*>FA-dWkXlI8#yp6z4aP5hjXl{v6Zh3{ zM=efRG&PY6-jQ%e@NFNg1zd|T<6FHD5*G#3pTix|ZG11hEwoby-0{lG{>>O|CBylH z%Rv6y3~%rd9Fl$E9?Iq5msBhUzcMvB5m}#@^{O#s1aeYT3T} z>+&@_xTp;%uF>b(p!}T;So}UddNe+J8A9n|!BYc&J<)A*N=#hvoxLmpi_VBUPTi~<`Z;>!3ERuJ` ztLD#Ok3Ps6wU!Ol`>=0$)m`Lw@we1*^oZ{Tf3kgw1NCST^Kn#@_F~s_UU%2MCu6H4 zTLXFY4@%Vjs5`?p1dGhQ*(f_Q>r9Oz66e6g(DY5=s+-S~+8c47?oY*jItG6{o{sjG zUz7!;=BJ$3;1Dck_&XdAGc!bfO^;*I?V-=nj;?PPJ>L#`>)I38h-SzBfQlj$oA8=K zSWRf?okL6I6#Wg=Q{XkACyFLI*i){n!)_9E&XjXt`=lR)Zm^kMl+VM43x65r$Z2af z?Q1`q*}cs6e8UC`oAQa+Bx&kir1nDJn|%Hi?jvvq{$3XTuzOfQJwr`K9G@47IN(?W zCn{%U`*d%DZSiyUUcj^NYonJ`%WYCqdojMA*RSCZ459?d_d3@@YUM_ik32Dgix6}8 zygD?K9``vD-gGcsn67}m>QsKR%GaD3;z1pJaXzaH(x`mV|=S>mh{X13H$lp0wvjJ4$s@S z0EQ-kr{4Iy&r9Fg?fF6G9n3%S)WST$U$6FngWwQv!I^~L(=g%xz@G4faV?DUf!I?n z8ub3?JCpbA^s^n!+*v-rPL^r$6j%;(F4 zXbg$HLZO)o7ZiTi3*{%-QfE_BFzRYgRbQ^MZx4@)8!qUxprgz+s5s{neTW!;J>Q2$ zyU6?ayNP`t;roOhLfktOwP)N<;`_d@6SM#Qk9|=b^Wr&etO*%&$?*4r&k9xrw+22L zy%+EgPmCRA`Yxi=9M@W+=M3J&kGKwwc?rM5Y=k~Exd0rA_`NAO^Knm6n2yY;X*3=bJCHEK@cNF398eP5C5QGbfK5c8}KGh-*-G4FsEPJJl2 z_GmzXKQM<0A1Hb+pLCzJCvBmPe;$6!5q|}~){DaTRfIo`wa!t1I0|?3vo&@^m}Wfz zqwdApfvWIxqRxOn1;*ZBrq+wOV#e_*J-^=Pdo>4~1QE+bzXmjA}+%j1b=J7?_oKCL;Rn}1w{`qZ%D*k=Dc7_0*l}g{E_P!)E|Zn9>r_5 zSM!RxS71%_Vd9u8{2u*hY7(73px|Ux9L-{v7hMf}IGQhGF1ojDz@vk}ELP0n#BR}P z@_h8un5U9^!6^_rvI2MXbOpzhJWI@7$+_Zt1>$DGOU&EYMy!4&=G)Vgh%qR76aUG+ zfl6Li@)WbkA|o`wY7DZ==7$ty28o6U5#ps-LYrS>^UI`UGecQ0EGb zjDtdji9INKGBLL*p0TmZNzTVEn~1yI{)l}G-!GnV&Lw=Gh;0H_|Hik8dz0k(W$H|U zLGfDbgNV9R7=M=d9Qhl6?qTX}=Fn(~^fCj1uh{De4D!0yi5C9_g`l%H8+jFucAUe3 zL2^Fu6Jt)?Al}y>q9*EbUd2qXm!}HfMlB{dGVv~E={PrR-wffhg=ndU)WANvg6z_Z z-0~zo;xaS1Iep%V@u$vvbLwo2zg`?(pe~91U>u8+)W(nN<0X}Tb&NyevW_nl@fXZd z0}CH0@;-cD1{~(7pXt*Og9`$0)VS0!;Eq~X%soW^&%g&7nz(}-d)bdX&|cF@p=dZqk||M|)r z{M!%u_!>AaZ^j%`FhTC$+wpm^h7M2lNpwy>E%@o=;=e-QnQ4LZ^R>6^ac8g`^LUwy z9wGmGHT*n%@3Y)PK$FT5n!?AKDZN~Ig5M3FiM}&^e)PMkB+35{po@zy1b08dU+f3D zD=F~DdqL=22)`zL-%qR;cg6QU#vgOUIIdCyfUg*T0%HPuG1dhBgzppnP|SWrZXs%W z!RO}=Bm7dqr@;U5Lu7p-?~8c|^qFA52m_4*fvC-)QP#_g5A*Lm9`{s4obBZVg8TQg z^>~k89na;MdA{v4*ACF<3h?p%t8l5|F~X;Pof@2(;{NQBSVT@aQkyByYv4|v4d#fw za|%9CAqSKff_V%dDB^I>ABx&!zdr8uXnHdfdd>koKKvg(kehMe_^a%|A$F3Vn(lG~F+)q9!@Fz5c1g{q@wHMK`e+ixoGX`Sv8=@ybjR)5Q zfAlgsSI<#7JcTih<2xf}^iRQw!bcKE#@I23OB3_RUq`cd%0!C*UKhR(UO#hP>kZ~8 zhlsULvB~qK*!tl0OCrWHZ~Aa{b;c z7u=D{Vc-cr2k!W_`>{60U3~L7_H`l;i~Aqq*Tx3^@Lh+^L)?-O+(Ph2W-2%bA_MGs zz+Ru}3Gtpm2SH@6zY0J4*X+;nz926yau0;xFR%h`_;cZ6#n|iRr@fvaxT6=v&Qx%) z^hUs*ya#Vv=n^vTd$lI&{MW4m_Cb1(N4S2DiTNXTjG7(}LU~@F^=8%C2pJcy(*ToAOPff(*o*$$(4ylbp{2(c(_??+vF~%4x48v@ zJD%)y#`vTE631bY5ANfUpO1ave&0oR=gdp)Fp5`~Q52mmFFX0lM0JGvX&7HOfeJr0 zhr#X}cRenX55l$gdm5o=AC$Q|Tm^$dM~%)Luf<%Adq~_=uG06#n3jP}em?HnJ%jHO zp5b*jQBI;4$PCI9|1Y6cNe_fv@AcA4&NyGc7h|m;>P!5?!8opRw}p=LQ?=)qfV_-$ zCA+%RzXE^Ec6s(@?>+p%FQF!eZ^-xIZF-aB_v{D7w$D=(K94+3#9i{do{tm$t%tMy zcuUV^-xtOhjOVd2F2Nq(U-5kwJH(>jNN*_ie?nueS9kQbKtwGP`)qcQ(7orr2|ls* zIeO-zH-J8%$v!@O@Ha~0J$ukc51Ly%$9X@{PauxMUG%BxeZGz!v!9s5D)vNQ74I{P z+=DrzxE=f^U#G+Tc}KwXvEp%ZT{dA*2cNU%b@D%DE)+Je%<`NM!6G&vQj>%W>U>0f zER5Fzi;MmexgmW+dNfO+$Oq}mc+7oWxL0mk%QBbCc{yS)`JXGEaSSdpFD}YpPnbf? z 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

    5. {+!{jftjJD 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

    T@U>08Y0?wqCFvZF? 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�E+|us1VoiH`(#!bhO0JKHyt9-2HRSJ+9+ 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ƚzQYcC1@;PX5LXOhZyr29f8EGpu0)-*T=)9~yn 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%0AFZHkFMdliAUmWhr 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

MyqFx!{;BD`u8-q)vY#YB z_wLGHICtdt{7cgF(L1TpT)E?Paa6jR>y=OE`jnx}lr)qbO3W3nrmpAalXLki9hZvN zI&PPK*Kxb}R_9y!w-RsU-)n!j^j6!g()G3*T~lp6CEC$c*s5<2;dY8UxC$7=matQ0 zf8A@sPpUR~R?Bs4vWkjJbVx#}>9T5)oVvtsD3~!R*KpU0BQW@Y?}5*+!+SK1IPC#{ zy|8$Pw^x?8n>p6$=L*RCR7nwDGkGIv!gB>Y8E9UrlANiPo(i8uXS6-0 zft|8%VNUqCm=hxRdx9?G@0heVtd`3Df6qc_x2^xxpPG{^e&6aIPP^QYV%ZO#Gv z)#}G&cvf9Br}RGaI4XiJwHwM?F=#YNxIG`iJn0Aecf;>k|J^yxer>--9^&sc{+f1+ z-^5R~&KSl<(}=@$@CwozbSkZ^@j&8u=t%NNs4>+Xx{^5UYf7KMK91iGwz>~} zcjH$*ms01dn$kzBcHjoGDm^38PSbf)|MRJ4*S|Si~PR0@Ox93JsNNXpP6^ zD$PbWFaoxdhmFfdDuj)x-@+Y5%#jKHJcxJD!ZQ6RmAzOM z`Pdj2;K=ORWkQTg2P5ucYM?dv6aJV%;xMo=1#Xs{EA`h3XRaIjs zv2R!MY(I*uA<45bDcBk9T3%*ZUbw=Z^n zfNFjas^XKtS|1?|5r^5sq^?#EWruoAJ};kVJFwTxTvRV}1{Ca4>PF09gs7>j6l3|K z<@#d&2V6hUGz5caT0|TcYr)A`1^p5D6wnFW*&WCC{vPVbX6+n%NngWF(=hqg&a+L% z0k+xJ;qe0fN2QdX0PPKeyAk?$VKQDrgGGjS5&S?(#Yy^9xjy^l z$#Wz1n7Wj>1&(A(=v?xu|8nX=;5L5lL9Er+lDJxRHGRc%HQnlcl4$ee=e(_nhrZUt zEl*3bx$1QKOx3aU#wtGjgMV6LBs~&2BAZA zC{m63r-pBoo0tuQ(}@&ii@6T zg7v^C0GO1pdt-n<8n^1vc_<@!NKh+?j^VZ=0BhZa6@jEffKbEv9FoAptfK~u@wBg4Pn3TP>!iV z>j*dPS`^BrieS+-ijAv1F{2DukIsIRP%qaDYZaA0ueJ-1wc`RX$F)E|a**0UH!6f< za|E{~y`hmYOPa1vk*4V5#qr?JkI<$GlZ|nRgQI2m-71sdB{UAcx7n}|T(5SOK9m2Z ze4%v3wB-k7DzGvK8~g$NtB$yWV^8 zR$prp_)A@=I+H$)7`Wd9DFW}}RB2?eGZ=L%E|hQ^+#mRh@$YOXY(gc*uN(^!!g7d} zBDI`{spF1|O=Pze#f~X@$~f@{27$xqapP>nG$=~4WS{J=@+4!rM<+TkadL|;d-yo# zl@jqlovVW*4X+;;H=nVP-yfS1ECT*;r@|DA1DO8kffwM0rmqTb2`yL31!u8T9e`fZ z7oyC7$LoZGhs^!lui~%#ufl%*7;v`>_&da(!LFjC{v-Vd?pMhC4=N^bp5y{XkPDl* z4uP{W@W;q-1N(?S8ucGti(5;Aza4gG1`z~XsR^iw@g2qI31Em!R>4))OM%I0P@SE{ z!<`YjxzL0)m*6tHKi^FlXpCY<+Jm`)W+DGqeVcq+Z5Dt%mOK~LE0`shb1`K%JRqtg zD|I*G!8?S}Qc9Fh^DFt3_%kZAUAQmZC~g-uiCCs-#_h}S44Xt)4^77&zxgmJjl7=~Dg8*;=$+=$~Dt@l!RWx76RW{usB1)PI;i|B#7&L@B!xO0IBA;sREP#oZ7?n*@I%OVkxj z694Qt_HQ-lee19TX=Z+Lx-i3W=|3j(-NSbd-6R~3;6pnaUFA4&k}(-7pNKMue}*%I znPW}D_0mHqBy|OM9k;P8!Qa>BKzXNnMQX-QqyvN3I2XN%E$zkr!(hs^zRW+&Z>6sK zx9UKVQfjb4tLGYVdAd`olj=q2dbZnd{pr~(dK<+y$eH=>QC{H#K6pdo%{_xJpypZzEKJO zUQzA(Gyfy2)zjua@;yWy`8eK+e16Z@n!4|~nQp0UPM<{nJ?Cvmq!Eu&p{P5W9$^ng z{hTiss-1AdWMGP^%1{^P=@84;29qL1shlsomTM9*+Xw!d^p5A00UR3Gv*UA<>$b+3&^93RKf*s)h4{C^X=eA@|KNt`!}uXO zt{?PbWsp7y9?L^;F-SJ^W01*4NN~~S(P43Q&UAJz+}{?MkAywSSKtH#t=vGni0kik z;YV2ag$r6o{JWsu}#`~IWG#(>Y z-mbWlepLA~(H?kXf&Hod3;qZOKRpD4Bo6X#)wc}6-*e-I|Ec}h^U!_bc^G@(eG+@< zL;m%&CR@D^l6SqAlV_?X%54zgQcQHO5D90~vx0 z5EWM&!?lW!U(Ynli2T~=4E|1k#Na0^HtA>KvjS^p%kWvi-&)`g>^YB`z|C7+@Pgdt zRT3+L375l-I{e>U;LoK30e2!b*cy!7Tgdj2x^q3$zPO_=mJ8H8IUnlid1}7WSMAIG zU`9N&D|?*S%Ft;G_!G%bwRy_o?-zX6Zi7ix8nZ` zoxaguEHv}wZ{fAKP+bk>C8&@HTgCOry&Du{VP(6#L9CO{N%v*oPdcgMK2>MM2kL7l z6@Hj};(M;G6na5nlJHPS-vMnZ+^UiL$sy3rz&j|z=NqUON&O8du;^b%>-68{m-++! zk<^O0K)wwJPZjFnxSgB2^9RL)+(F?e_G6d;;-gu( zB%I6aehL0a?){(M!v|Nsj@=v65&tszH#7gUJNQ?b{Oe$^=5%F|xr9vi!Ue;y%!3;m zQb`%JMcslHu8n1`qchdc95@*dqDzfi=*<42WJx*j=PhxH*kY$h_zyb@ovBRz1^#Yw zYv2*HL8ACE>Im#|z^gFujx*6;ncX0?Ia!z`uj1;YI$Q_*2%gbu0lp~cmo4s?az|`c z4~oY!__M`3$~$WU7jxRg7Nc4I78ewg;ngx8`F8@U)JX!S86xS%higM{ON5P*4(>6u zgo?Eu@^cM4O6*pVft8uK02(h(BXbwvZv?*LGq@J>rvFwZ{ynICk$gv?_r>hxpD_4m z+w;W0Sp6EfkX-R&OJ}jhi0lx>Az!V2ID;UuMF3fD@ev{6Vc`AKQJ4>I{&tlJX z_75DM)tkbU$@=k~0shv&qb3f2OVNw|1AQ>(r4uahSBu@WO7I8X9~)wjfBo)Qs>Cj) z2goGZ7XHDI{8@j@cz*IV)p^HH0*ve`Iqg8e+=qB#;`K|FXCSf+kwAhOe64Df{we$ zD3Xdyu*3~KW`D82iQe2A$WKGgEw`62U?I{G=ZENYI1%RR*Z60cQI6zB8AHQEW4ZL= z_tu|b1Ta@U^Ud;afgg+~!yDBuYWYm9j zC=8#F3U>@uViiY$KRQqB&1T7c#X@|Ka@9Qa9)08-#KU~m7Z$9DM}nq8%uR&$Sf}{I z(jnmxPR=H6)= zWWsDM59OqbD4|+qK1+l?#XnRtH@Gd}r+aZPmP_)qHWd0%h)>11MfoIHNYP5i>2cPJgdPMp~Il!ONR?C;=cUBp*!`Ta^zx|k}Ooh_(RCPM| z$LNf($sY}kAKZbXbB8V;K4ZTi;kF3%K_Jt4e8iu;Mg9k_9v1?CGu2YW&_Nm>ZgN^Y zw-Wc3Js|a8{B7tX{z(7vzk85RvG32Hg}3Uf$TR(g_qqMl`^0(TdE!3wJc>Q`JdQo_ zK8m#=AGcrz_*>PvbYs=Ybd%?FYDd6KEM_KK&~pQqda&}Xyi0=FuHy9oxaEnEQj zgQ6{txAt&XrA$3dqo<2DYaP#d{XE^Ac^boBlL;dJL5>mTZ7$~#lCn=rXqnk_G{N}9 zgcCAdIS7s05Q}{Xdf*W4j*kpL{416Rp#ROMbHzNqpOTLnDPPKmCqy5$C%RG5&pF`p zyD=~D7pSotqesB;I>;X+*dzbs_X%Vk1I{yYZ%6Ed>p8xIm|)@-Bx*t#0lNk z(W>Z)(3lzn&i_d4{Gpobri1g+OZ=&oI{R6o0yA}2``w3+gJWud%SS}Ko~)~c7_STLL3 zZtrCes=sJG!6BK3?qNDAILu%&cmw8e&j?J36BXMTng5yD z=52HExKF?%e1ci@P0!`zCG^0@Jf{!?&m|jxjq+$Ib~wYa{V2p1`VuZhQ}R#H{#L;g z(rM^}LQ6i19rq#eiF{YMqu%B2qMmM1FEGF$;$Fvd8N8T)$-;j@49YHqAY+^l{^~w8 zCi&So?TaQEKV&3A67D`|Hwy3hNSJonaL5@NMW@*@`{RpPIK%SjdI|pUWXV}-cii=x zm}7IeCrNn{v7|5QG*HklK@AA>Wu61Vfy`&J^Ie*mIg_~y$-U5%hgvRr4;JnUEN9{_ z6YBMde;xbZO#PS1zwAoj@3iwhGbn>UU{1|J^vxz1l(X?V7a07(ibz4TlE)Pav(&Dl z$HEb#6mIB!We+r0>k@SlJ}ptJ(j&P4bVf)`S~GVJbK~>sHQ+BxT!%c>*Xkh-*T}8o zIDV`#R2XU&sb6R-RdC+$f$uav-m0ji)K?!r@EygEFyBy%)RiiDj@C?iyM@bWb*r8) z&jmhb13_i-v<&{xP2iTI6q6NHc3|^O#B>=fJE)Ym{271BQT6ZeEnKQB5NFBL#p(P~ zRikb=x2o8CM_JcwajKsnrDh8j)ScszVR62Q)f%*V;fH;7|#)He}dyCl;W-q{Bp4tl<+GVV3 zDgoBvJems~88_*RIeVyM;Iww&57AG|;E!(*|26j#)lw+`7|4#m9}oOtLQe39`7`+U zWd1_(uZ{VibCf>obY}*s1%&lV@TX*>0?5{~Gx+O^&-0Ct6fiw-oL|g(;F2~D_?v7_ z;BysStEJbb^>CzGre>x;Xa4S#2xpCE_N;M^J%{=8PPIUij;LB*p)Tjhh;GtVpM9nqx!RR8@s*Z#ZLpZZ{e zLAo8Y=yv0G>Z#clx@+GG-EpplE;|B7c=;~qV0_Y4BC%l zKR@qs{%DK=wnZN-yZm%K3f@69K%Q`n3WQ>TU?3I@hJeFzH#bsj0DnRTe|(OJ3NV8` z;7{qo4^eTDW(l5%D_2==!jp<6k%PB{jv0T(AEtpKx?mCaMo<&N&I8O*pN_c;;vWT_ zAmEQ<45$)i@JHgGj{47LoXq^MhFO`(zenh1rzbXCMM9oJGJhZFo%NCXsM#WU`eGY1 z(_AiATFCF#a<<$Whuv@~Hwns&ebg0TDwv3UOgb9;A>BFB*)_x##6Hx<+&S#uc0toZ z#y!^?FpY|In*$Fr;#9AsdnQZxo9272RlmjDQE$<|Df=Vq6lfnP8g95u-o*_{v-BA3 z+V{}A;MKKIKPYGFZK!sttyZ2m!1FT%Tc)lJKDV(aa~@VcNj$205_{%<<6!>x|CW2n zpGojX=78*5wLSVme;Rsd-3eZDPK185&r`>Z#!#bmCV0|m3Y?1{^Bw~RPo)ln8a6dD z)gCV7f`#A9=p&n|uEU+cFge{YS?d&pesw^^*CJio#M{QxWZ8{(Ri5a z>h9=LoenS#?Pp`GKMKsT3BfNwUmCdcM-q_$d8il_q(0%#4F=~q`Qc&{`Bx}r2XF;k zHi>>}KfD((cTuxA+6cl^BvGlv5*21F=}EgOZyY{{NB_Vd$-$rUFC1{e$|XBDQu~p; z8+fOG;4c&Z7|8F&opB zK605cUo69ZWrQ)19cm2aQ2+68W)%t)=oK<5&TymYKxlc=L+^K6WWswnr=CUpYk}6y zkPQCbD|e(l@MGAN+DNUa)5E=Lngn=Eqc_0a*Z=v!_ZRq61xyM*;O=vsy$g;hkIZ4x zKx4K%Q=biQmooGdaN!WAW_nswe^6!~kEjAAAgQm^71yQjQ~!g%_v%UIOLc|{mY`N9 zEEcNNhETiJ>c8(is(cLmwZ&cq-Z`1s3&GVNxc&H={M@JK)30F{*Un%U7k;HbkKQ&~ zB4^Fx;e*Dm@D5{JbeFy}x?TS{yvH~iJnJ0x9!;J^9&Yq%sUg%1=WA{dJmF5pQheBg zLaksy{Q+0_tP8Bg4P0XxNw^^rN5m9^3ww5|@(X{0J%io)MeR5GH|;m(rnWsY-=ICw z81$s$H1HM%?l?az<33`K@`n>);13$B8T_*)T6`%=y-sNEgq8{|LgKOXxy;E*^j!u=BQ&)X6Ipi@Ob zrv;NqsIS0%#uOR6#n=w~;s4KyqyMO8fj`V(8kmbtFP7jhPwgoJe{yeJ$M#k-F%VHX zOP-;7G3{8yjnGSg!O?IaD1|~i@$U`7`HGLz;Q)NV1BvmG8?kG+qdN!uk^FlZ+l^5f z{Nc7sI)cm4jmgcypH}hI@KwLF@6ws?<)`oC|KW27{#F8iUS^HC6Uwj6a6iwAO%rB2 z@N>=JZ<;a%-3ZxdOhnux+)i?3R;+*_k$xC@{}cW@f5gByxO*7QPeaZt6NzVav;U?2 z!1Ks?QrVh#?0FV{iFnu+eTDps_r;%PHyJE`!rmurlF$4b^%e6bGvNf_T-Vm)zX+q458F3b| zE`ztUQNye-SKx9QtRSP7US(9%(+K{UGfFf4n|hhP3_jXTb#wR!BTDe+L+tZ~5+QFS z#bWOf@ln7cu!qki_XY#N9rnI~K+vUvWlm0bfHr_0!1ZTP{c(N8T%`y*H|#ucXU|Z) z6$GC!<};lnW-pFsgX8!6yyEY;PPhF8O=p-AeSaf8g(fRIhcyo~BePla}Iw`c~k% z`3U%XTy;Bf$#*5$5NdQzMSs(8ajoie0a}+NK7V>|{271fd*71Y7niVY%q_i<+H9-| z!_FP5)+(2hs)btUN3B+B8F=P|>x?~tMyJu+7(YU|(cV?*-0+vUiM+2LrpDFO1Wv3K z`IMbD(`>a#uJwUAa}`}@;y%$@P5)&5M6boqO^5Q-3Fe$~kzkK*QCrws>iTd;`~$}{ z9s&L+ZzM_Z7xn{pc!0rp2-pi|;veuA4B+;Bijx}w{+Rw;f0p1cTg*ZH1OAl0+$c@( zvv%0yI>X|CbW}%TgP4)Gv~J~ znY*-W4025Xs^KbvtTX{DJOyA}@1 zd!m<|-oRfT`rlr1PqnAg3!i~ORHD7n2jg$1`^D+-ET3Qu1pY<>f2G0@vp19=p;F6? zbEZ)969Fomg49HOjk29vXKNl#n?&_gzT&=;dxH1*x%wBd@471AtKF3EmELNOnx%CE zH{eS!(PLnEzJ~+uzsDfKAFo={2PnCv^=;BRadYDL@Vwf*z<<{bSLSKURN@_sNC*T@ z!&UbfFzSkw0<8zwir-Oo0{b-QpYR79l6aPYH_>!{y80l}Zj!z2b>D7xJ-)scwZd9K zE86miUkOm?p`(hhg4rcBv*6^Q&U?qa(cdz!wO90O?Rof;bu_roSre%>LJW_Y9itm` zQr`@n$?c%H>n zA_=np|Q-|^bsc~am5#tfNy#HM2wel{=2Ob5na9ePzBv zpWBWZ%MGf@IZQk$31-77b7f?;^>g^7eIk6)X{1g$C#fUu5yZXy!JY1&z(MzD;CSqm z|8%0+SCi`Go1S_?y>X9FWi?{ejZGV z_=DcQ>!U_HB{bBmxgyN|3WaQ;yA0+t@YfTZdqIVp96)})>!b%?v+dw@EN1%D^6tjqh3mV)X^T76O6aI!R^+;$@OhEND zRhX{Lz^(E$ehR$kXX+FAv6wZFGe&X4%~8w{YXbaQ=YzdEQ@}kRx(31km_(e@z(4N- z^~VRKv-~$Pkl^n#HLZLwOu;a=$m@iasW<%l2k#;)=*}cxM;Ra`WxJ+wcPiF8(V&4tr7Iq@h z=mHZpU)%^K!SSPi#*78``E$Kl^yyvYT!sXZ0WQw;I@`+IM0Ptt)nU zCGu>E*A&Q#>#D9SQNO~UM{;^D7^5A|2Dq98TY0otr2Y+pt#DEx`lf%v33=UtKUeu+ zI)Y+uR@REu={G!jXZ%``)J?14WJ}}(3)f86B6f~Boq;zXJ<*C%OX9Docgg>qd&$4| zp2MAX^nSXhu*7VSJhPs8Z^f>9E~jn>T4Q&qCr&&4(tOLb8*S7rt2x*Z+Y>w#+e7Vl zPKKIr3-{1z3qE#R{rBQ6z9Y#!{sZ{+Jx)WQDSne`cc0QX%u}IL_HpXGeJk8*w@{~@ z!~TZYG5_gUbMSn;$={Hu4+_aKlqb>Rf0@|hn^J2nzp`~3zt;ZNDCP!a@*sR0h?Ak# zSy^PAkn_GllE_RTpT?|kZbXF%*4C^ zv_OEpxIf^Fd4t}VAGdJPa2R@ACH6oX{Vt36$LFE<=!5UdAk6-HD&ujhBl)AYj=neL zwc%;A+O74jbZd~0)4p0K6-*EhWpqNnh`R;cBcTT&vp;h0(ZM^&%%0f@T-$&@QvV_U zV*df99M#m2gE8MUu>-b}Y|5%;;?8#Zl9SC0(E8$9B1`G6b(cvGf(S?^kz@{y9q@9o z%$(1Z8}kT{9gOFZ!WX8&pE5rezR|WSziNBc-72u5)`>N^uGb8|VCq--hw=yPFW$rZ z;5XsAUP{k07x1DxiS1?@&lUD(Er>vC`{jkFY^;sSoW5UQNWz4pJ2e{egT5=f-~ z!c-<+Mcb`*ZDt4jJM&Dt49sC}l5F%h#m|J!#;yeK#vb_} z#Btw|xZyjOJnlb~*au9V@inHB{%Of^(Xx1D*l&4*6=qq`XENTDz0tGRJ?A?SYYZGp z>LVqLzK`_G2k~zPZpOqw#4_yq6%HDlvb_;19Y7AMqy|a6`)A?_&)}W`I^2TxRhya2J~{h+Q^yQl@2bP|3;j*R^ZUgv{@9>BlxuQ@n_ zKP@DG0QWj*t(Vqw?uy?z%v7ZJS_|7?{_H)MeCE5ByyCx>xaq%;toN|#>Z%`;BdCWC z`c-&4|7%QrhmHr!D);knsOAm&k5_?q^SbZ6bIWrt_0az`@gjW3zQ#2hjSTh|@aH-j z+!H$(XiEGRyc%x}Jc>ODK5_2`ZpUunZt0BwaAJ30cYHfIReY#CJ}z2fWefeF=k^1< ztTc0NV863Be8BnzeagN7G`9VVk_DkT$;R-eWIA*&z1lmoZb#LjO^xChtA|dSai{F?*@P+rQ)Hh;R=& z@W*uQ-w^-Aju{Mx9)vSZtD^?AQsBbWGw1Cbwh;L1u70nO{M#KnD$;{=;IFToZ&YH( z44)q8dJt`o`M4G7r~O52!*2L9^+3mzR%w$TDObd~*ds4i?+G`w%lvueB-bdz367{X zDELtE0k%t#F^bGcv|RMTIXWJdyy~N#QV&WE(n;<#bOx_W_k{b>U8xy-od2@RfxnI5 z-yln|(Yki#T^(?xwbQ4~Qav% zlEgo#=<2uhYYR6V`m@ko>z3zMvdI%qE%NqG^o@MsbQ8L0|H*x;&u8N1dTLAjWS}+P zPQ7)0XWI3LRI7aly!s~pj>N{`&u(paj$Iy|rLPyikQS>O#EUSXnvVT_#8VU7<2e&M z5?t^6>fH{G%6~hP*c+{pxCFN=cf4AX*by=MkEjK-$CCN&UFBQVBEkp(4=r!VgiixcP zTRlPt96#lAy%dRm1dCO%aw^}#+*u^}D-iRc2!a15kWV%aQ#nIZgR`x$56)Nc)YQDS z?kX?AUmS_vaAH)gxrcZcaz8_Ta+kcD+asg?L;kG-_vL@%kL*9de*yl;3|h+cAgBSc zW3;j7iJJ-5u{UJ!SA_ga@b~>k{2>McgISorG`P9Th=1|`++sj~N*<(qXG|Ax&4k;PT~w)lEPR%`Nb)b@ zAHm;6T#PKRDm}qu#Lvg#l;(!0%#1G{2!&lGqB?;{m0L+w#ww|*7ypYpg1!~HGv zzQ7+4DCS{L*%x;o1xg>KyIhL>yAvGilm}KhuHSGJ5Ac_Xf373?p)(h#G2sUWjlPT) zjJy*#Tt@JR{M&)Q%>0?eKPC$P3uZ73M`qB3gJsI324wV)H6WP-I`&5J|FYQvT%hz+ zddNT^KKc?aEEM9pBPZu5{f(Josk4kLcNVkD>h_KMxlS-1yK6S^NBB{Cm%|7{@&|uI%U4 zIJH^YA3mfuLLYQ9c<3_o!N_p#Kh}|->d8-^KSX(VK?POU9>#nST$dZrN;=^G*{Sf1 zPDdg)oZav(nqmx+r%GG3!?4|Uyvvhm+_Jr<-#bt$#Vq=%+Dz43+dTW+{Q)IDg4z@R z0KbqfTtBl5t6PsFJFEt8gL5>{h}lb{+l-#LBm%#f@ICiV_(H6q!n11alH05IQiqe{ z#d*#M=W8&I3HES@k-;DFTN7u)G2CK0#|i%A7Vdf`|2|;Ps-vm-c4cK% z+T*WGR-oS?vp&Bc^OO+Mgx7K+J{dJ&t+^%ov$liZCU57+vt3>azwip( z!+UkaKjKlOhgiyhD{uz}KgK|UK@tN|2Z9HvTNZ2K7EW{40)IIyav(T3n7>Hf<*w2w zMG;(SuGEP@L0SZ}V;f0XJ}S6HEMg+eDZk4&%MO(P6!WHBEff9Fcju_Xuth7=M+lRh zO;U{mtu^!xY@nJys_JvP3DepyY(jAVjZnGlHNtV zinsEE^s)3zBM;s$`yz|nwVor@>!_1%E9NuvsDWlN>ZKB{Olzj#KSqsk{LusU1@?ur zgALfV)S=k^@J;s`e*a0|iTEks#rR$7g>!&y*IT1E?aTh=yhDLLh4jJ$altZE{ZZZoj&y^36qU(kP7+YeM zhDN1F28O3c1%{_a1WHpgeKV7jJ$T=Gro~IWrLpP$8L^rE>9N^?#cp}1(w!0MVt>ye z=di_c5n>% zR`5GFb30VRGX>Wf@h$?#`!F5JJW;3;b2RLQXwihb6&~E5(<(F&bm*XJ4qiJZKChuq zIJs<&-e1U-vn60q?ko+1KU@v6PhY6NjphpNDg08~%Lkn>$Jrvg)Ev!@HoxV^Xlta; z;3#$4_)La+t}=w=|2T#sRrOnzOPE@$bEKLHA0v z$~JK;UoWl$0_(&TaBxjaDX~VXmP|1s4TTdXx{rT5S9~wuQ2#Cm)a&9c>|l>LAHw*Z z;8DGcv>8`|&F-T}vr~p?XNte1ZwbqoMKo4l4M$=-qQBbh!FNtOd&^uMt#j%k)y9s% zMeBaJ#ciZcB=J1zds1D4XEn4Zi8f-Kk83tB zLO*z@Q6%J;WXg*UMEQ;%1U_!el7b_g#lg5^`7MXT9B>u-UkBI|Q2)81q!kZqhRxV& zoJpu@wnoKmCHPe`1F>!>UJEpSj7On$%VccZVPa4tME4Axp=$a^=>B1v&D1$-+4WAE zt%HKyA-5>{oms*b;0mg@+6#5qU_}sCKsvn#S7PLGea!-?#3|*=T|djX0`0X&GNY^_ zT%E5GK7+Guwf(tRg{z`sV~8+7$6QHA4$+p0i@*RFtNtT=Qf=pI^jxl|ahQ8#wXsLd z&Mbb9n63Ag`)JupZ|qpQ$P-K)5~j-oBUXG9?V1?DO-~MI`c`-4zDj(iqW;sbfYDs5 zt&En{wbAW$wIUp7aJjiv+zef{bOs11-S z^h)(gpQ|6X*2Kmo8xCZP3d~y>Ga)Tb9I{Tl*Id0?TR`lM+N^~MK7SjYYthMWa45dQFVhm zae-CH<(s)&zR`~xU=;Hu#z206zKAb}gF_iJ)0)G~i7liTB`PD8u^<(2D#A;g<jB5gGcNLVThCshpAkpK>doFrly0AE%-~ErO@6q zQU67~Nq05yXX0)%Oo^5ej_avtQjbAaI>sm95Sdo9jT|Z8fNBDsQP3)Y3I#4KhVmoK z;ruXj6gL9>!bCiaF`=Djm(%4|B~(KNj<(A=4&EW0lMN00;ZVNJ5qoLhV}{&SD!^4) zmhz3*6PIdPU`XZ|{lo$GDEu>xde5v=9D(r9g>E-}zwEDxP1w1l5 zxjL#Cy#{Kv)3`_KGk&4^H9Jwh!9CI+v0#wWa03#uv0K4@h2T%=q4dysHX5r8ELkxr zICUjM(KR>)t6fOVt{KTLapA=UzsaA~GXj@B&DNwGZo9HWMD&w@KY1;jt5?AFvRX*P z!8!#O^*a3A23YS6l^zj4gMX8Q->Z*!$`~FF$JbIfW7h-E-8bR4<{R+nq4Q$2`LDZY zt4>z$_58ZxXK#J=UhnzToN%R?qBfatE8eAEMOw{tXi)4W+XDU5!_GNZ^n?o&(EvA;agQI<{EIv!h69;jlC_MC*YNz#3yJ|hO{%Qen^ueVgxq=;tyUIenyZjX{AO5Oz0iOzJ*S{0cpJMa+cexXG z7oF6v#O$$|L=RL8!pVcBPMm%FoV{>gVC~al}Fs8@}*p>n4e;gXG_d+e1Cn7 zkc*vBrIim2q++fBak2oh4^@m>V02SuZ#K|3^z0| zVi5myu&q)5WvSn2%LK;pQcG4W2`;N$76`121_G-h!SyRJKTAUIS}#+7QJ@!N>=d`D zKZ`r@v0a2BtcWUD+=&Mh9&xi$FRqu@NgEIo*FslDhZFpB<6ZdQVeq~1Qhmg<8r$eL z^A>f_dE)!sc}u<3UnBlK@?4Ky@|>?e;XP8b*SoX&Zt!*D0as@j;fv1uk3GxP=;Xu> z?~b~a)CK#p_iXBtp=hDsIONndVyYVNOY5z_=v#&5Ov zi*&w4!XS1Ou1@mhUJ{w~7V$|v3H%A3sm^kbf|^frfIs&~U!4OHEGLicVRhlA=?u~0 zh92b_c)P4+)&e0tfx*m!&21j$YlBdG<1$vC#7)vC;+my|?}f^)4;Ze{Hz01daNH{* zc#`wLF(5pHZiq#2H^uIQ%>TXz2O*1WfKeIb0GD~ttQ{y0)%^0nlMAb!79Ke zFxTwEFEMk3a@=m_!{M<5e}%wcH>Jq>QEG7>aZl{~(cA7#s@1(6x*LBGeC`xS5dZi- zI{r={@z+c30UehhuAAmp&!nc+&IxnXzHsr1IsUqp_5L04+t6Rzqwa&_EBup1c)&}T zKS@BL1PsbsGPtV;_A(EYJT^ncBlAzxgABay_F-#MGFP33Tk6GzNAMT{JWIi{6TsHD zaZ$D~`d@BOake>D{KoopG@^ehok3X>A@^g#cRYxa}N%(>D$a0ce-bJ@At3^q^gBlbl{HH0VyfE5o%WDzxn zn2)Jr7IqxHar*(pk-0N4*h9%uvhno6ej-omr}YyH;SN5SI8ecrek2rc#~NjPA7d|@ zYxagKP_DQL^V2-NAGliJF=~aFE*GfX^)Kab%~GM#`GtNFdqCZB?^5^NN1?}dL8J&O z!@Yq&@)gJ48T=n;K*$S>DshA}QZBN3!D<{8Lzhjmp~@!!!6=QV9+a-!vt*;_e6eAG}}AsLF`6a zxtH2Q|8BbtcSr3X@7`Xs&$L#5t8*9g=Yb9*uP{Ny%?=wZ<> z^xd9SZo`t3$s3_I>jBm3+zeiJuR%}YQK-$i6xwX<@SJk)c^@Qhhi;=5yaJud)78+T ztUgzDy}HRasoID%Crsh*Cc$4em>y&nkGzYITp$sCahO39C+igen*!E|1~z*&z1oVx zO|2wc;slwW%yD!lCr`|_vV?y2_oxi_QZ;s}GLd$x^mNqYPcQW>PA~NTkSOnnfo}0f z{1tHn;afS5FCjAzzNbb-QW|y~w(j*fRUY24Jg`C~{V%Ei{5~f)JkjK#3%{O4?xokj z-|r`V4UPDRjGBecp|^lo1_mkq-pV34mMyX1;GI60=jPF^A`klO1{z+NOfR)N@vNkbmtrbzr=^5?o zR?+L6rPS!y;LyZG2{kM>Ia=m!rdGx4JhjPHp4F)}{x!+EKuuB)Mw4FT-*PC3BKw$y zh<`;q@F$Ju#)yOE-fUk@565%|`B(EmBLR=36D*VV#dUViO>td#v)&bMyj?j zQlW>aF2-^=^=6|VD-b6eLo)cwHwtj+RwNS~79wv$PeIJpzm-0-$8fvixQaDiGfT9u z5Kr?Y!Y%BB$zNY&9&&YuD2VI3FV&JnIrsC*bKyp{0ggTUWMGfrPXgmnLjFYz{6+dR z{zw)E((rV=B6)cidGJs2^BvgSiY&binYk9OhJ>?vOn44|*1P*jPJOf$g5q!ZYOeHwO&nSL326tz1zS^t6k@Mnmsqm*gtH2DX$UOuJo;0J5Hh5jn~59D3E zbJQFygTf5srZ>8wNlK%7T-~n#b>st{3HSi^$OrHzA^*Zh3h^(4ED`~MvCK!NCPZ%j z^g(`4$M5RM&|78XV|g>G#kCNLnyZ|Hd*c=LQJ@_+m+$pfX05TudmwQzvM+uVzG5dL zm(+Wa+h((OPh#)lrrK8?@V@vC{JqBA$Gd2|b{~7-$KE^17Vph8?l{w}z}=(3EthCd zwtDU+Z+dR0Z+q{gZ~I!Ro4w67r@W`CcX{`w+x*XBS=`@D+&dy7WFt#}{fBpCZ&W0? zm_d_`IO#vgCU^)g>4z(WFjM_f>WsZpC+!R2I}&Y>zwnil3f2DD*1;EB83*0wZg8~e zq<$ym`Ap9IX(s@vm}?uvyto{jBs2 zA5)7XGu?62>~vvtdb)(mjS+P9gy-m74eDSD#dQ}K>%~Ss`m>>!`HdF~pkFlF_)_{C zxVv3&`#n=zrJOU*DJ{l*`Lw&9ZAq(IbljcYv&(3GbSDn;5xO>MxbJ6W|)BdW= z*UpIt(P;o}K;Eyw9$sal-(CsTA09+MpzkB*(D!baNe{dYpF4VB(g*L95&2|9KD^$c zkUkmH*i3b}N!}#FIYL?whW23j5O{RYL$4h;?rP-T?KS$GjZ~epCA24Y+1NN_n$w=c>3!hRv{5T#e`9;P>guv;BX(%Xe|daZBRU*sp=V9K7>~U`w0_^ye%2c<*;syU6F% zqvEgXufShN<|SG8W9H3ZFVlS>vu3iYl);{asD_6`H|Qsb*bIr=uo?NEheW-4%&9kK zJ~lvaaGkhLUdspNZOlWhEdtF+;ID(LnqiY=?tDod*dBRmv;}U&&wKBup$(YX6_fn? znrl~IV5)T8cPZKEIaqzpbE~>F2>n31qlbQn{QR70)1QZ*V&c^5+z;GJUhpi6|U& zi!kRGm#DMY{uX?SVuf;^SqQE;rYg9Qon+2q=i2ka3*CkAom+sp-$H5;W`94R{wqWO z(a$L05E=On{7qBli$B28zzxE69GpMfqdBrC?9LCs{w7Ggm8$KZsO?5QYCY6``Wif2 z=&hM&E4^L&T>4!5T>h)tMeL0&L!Q}-`y75i0eObgoBQ4w$d)?{vlQ_U9DjMC*wy$P zIgZ@64aOX1kX8uZX)f+R`^m*pwlYK*o0&+?1EZl2UkYveZ=8X+maWtSN&uot%hWO` zOZ9@nRHZsoo@wSHZbB_p&VveNf!RYWab&U9&XIbc)(o3rZiZbMJ>b@YB^p&GYpXTQ zm>}}D#{a`8#J>G|covOBy^XImB4~H1g@RbuHzWS{~NCZv5m6VVUg8AxHe6ePG_}FPWFdL+=guQq?tRP@Zb& zs9oUoIh6a0VM~C5VWR@GlxGd~FXABQTXuhgu`zpSL)K!4<$?i8tHD=Rc3b4A2``klyyEFrn4D?R86m_wfi@AIj zeBYs97OL~Hi6#%1$4Wy(TbC1&Vf+gdxNvPe>aZj=Pg$X^RI>4JjYMqutOR$HI-(dxDPeIiYx=+bn z+b$jeL+TK+y@SM2RKm?H9U)I-hp2r?<$?Q{R2{n%+`RI3%tJ^|`aAq;liQ>%@@A=3 zUM2k`KNsH1pNZzxz_j>(cL1P$f4Am9XMHavD3BhyAVgGzrdc$d^*YSOYbqNw)rh zztH^))v=Hg*;M3o(b&eBriZDs^>4t|AafCDB$M8>1`l?kp$SdTcsec+!^K-OrDSy` z9|k3<9CfttZ~Y7LU&e4g#+t|E+Qn2coHJIqE4^7RxX*e5m&C>Kacm0ek8FZJUXX)6 z)lf0>wd-ul^fJzB?;58LdeTjG4fZDNH*cd!>?1wD&H8sNu!!?LyIp^6ZerkcD=h+t zE0-wm@Cz|l9;D9&qZo=($}06A@>nozrkV+Syd$umt#@Llx zM(EijwkWOQMpPF&aNq6$Uh%brqj56zpEt?OGWer*M={;NyQq5kl_*B^QxLqFz0qf=4adVOvGrl)lu2_I1pX?P$8 zzUN+QKl`3KeKno+SBUmT&AIwE?>913+BFNc1;PUOSI^=SP3V}qF{m$QLR~#hG>lEk_v$)j zv#?p%D&?_5&3_By%^BicD~BycM=lNAzZqJrkjO(_lueO=KebpalNbf+K&s&7OvYPe z6?@B^V*d)W+`q;P=H+uDFWS^XSqj@MviOD}K9t&Dg;;L1SPB zdeHI6L`I9aBZTqBcp=gmY>f0|+-+rr(!YpN+UM6Lr zi&d$XVEb(-H{7uKlg<$~+pVHoz|R_F<+7T+2ld+*$`E;oIf6@ZS9q%%q&geRoJm2Q z@6ZlovT+1)Zy@Fo>>U?>RL+Xf$(Oc?g1iJ1-BkEOWa|aoQaz8!Fw&p}2gNeXx939_ z6AtC5$%#Hv9`=<}z@L~c6lirqP~F1sL>;mV*lm+Hi<{()(gt~hyiw%Em+U*~6Z?N? zUWuR7=hSVdV|CNUi>1G{{zkS$|KJnyAJq5MN8GI+jF0}8R#(lUmgAKVzI$5xuHjb- z?<07IHphGVx$!fZ{M6oP=&J5)JXdqEKEu~%jpXyyS+85;SZe` za{ES;d8#@`%R+>#xBYy)J_~z?QDUxMBoP<(G<|`T$j4)kG{&8d_y;!+Y-|^(E&4j{ zuzG^;6S}3n-rZKIcd&~du{91{mlSrk4%V|agNqiT#TYh4S;Xcj%f(WuP+G|^g=cM! zn#_F#U*x6QQoc~jgh%0O{{gsO?Not3_*-uV{x(1n1gaqL9p1of(jTe)a=-dqex^Q? zpQ6(k7{*UF#vv2PldGkJ>QQwIYFx~Ev^b$$oTtTbopu71}pphX_LXY@P z_`rT1(472h{)gP~QQcLyqvHIAN1orB-iS}MkEH%Z-}xi`Mt@S>6X>nF)OfY7&wW}^ zwZ5&iXX~@d7ftU_PrrwT%rBUayz}>4FY5XOeFT3sR~mov{1k{~9+^4NX+YJfl?d~} z_!y^;!?d03=MUhIbYH>|`*4@4%ajy7Oc|v88yR7qJcm>m;21$0ndF7==uLnsMYIzo zKvNd}V%bFbM#qkXInqCtOO^{gqQ-kiniIJR;2eY-;iz>m%?FC1HYrB)z#o^Vz!43Z zBy_Qb9As~^r4evPT&5Rs1zH}wzdhUmWjDO$w_)Z>W^Nmq4f=p84e&>A(&OR2mt-vB zCK}P$wq3+R8-q(X(s9!G3?qZjG7ul2AG*Rx^Ch4{nTq*ap<2$Bs(#@c@a{Khie%x= z+-E`)70iB2P^5Tek+J~V$_EtOf-3l&VkNXTs?7>?5~D?*+OOXc??An;7i!M|?BCTX ztb~8Nk_oG)Yu0D}le~>j=isNLtdftbonk9;&DAs%|qma+BQn+Y1*4G(9C}gY?HY%F9Uiu(^ z`akl&56Jx<)Dln2{&mm2zXaAW-+;gL)_IF~_q?hv*i~_`DZ2LW!4&qqeZO)`^X@g5 zH$19()dcTM`vYPl^w^A-{-5nqTsVp2w!CKK6jJi~F5(fZT5rOM1^6fV~a4hc_FdEy3H|r03gt zjL-IR9vd8JoN64eU5mZ3YPQCyqdfuMufcy6yk{;54b%p^mHnsj4{oDs$(G(K>@_za zHXO#@#t8J7^3-K=vEC*Hb+71GYFSp~cvf7)D}1|r1jN=%hT zO0T8=gf`*G{@Xjb5Bo?x>Mik(as`u#ZPI7lfH9n)sa&PGnwsy7<3^Zxy&IG9I-jYY zRrkmT<$cKYwy8Ui_i3oH<1trB=A(@W%vMTy0em9`F7iF*Oa8g`SnAa~g&z&%cji&y zsC860VjU5VS*Q53<^}Gefz5pV8h=J>7m?LSb?Pd4u{Ij>n;CEl%Men4=apsvlOzW& z^dF^A?eW+8Bb58~8`rBZHomF(wds}jvZI!JHX5Frn{QWLYPwK*vi0!l#@4cmn)NN7 zqZ^M_pICRXy1V&d&5Ne@*el15y77j3X+HPAaL|7V^m%$4u6eH4_jsG#FU2a|i{58C zda1d5uAU7oXcC=cxrx?TG0YBEQOTj>Hw+v4#n?$ge;%3ObagTOI;TM=h48b;j5CI? zRMmLW4?|}ohWvuQNw}777D?sSI)10S)8B4yrB(1czu~^o!tq_4%uX^Rkw<}l1q?=^ zbGt|_R4c_2xcR`jQAkr##W*klpw6ikarw{`$kKS+=05;`+n9mb#}?GUn-TYbJTtZF;JQQ=Ko^oSy;y&(W3csv<6-J>fJeq}^9%^;wB&#bjU$IR(2ew0#y{cETrcVB z24OGwdRw(4;#g%0wgJ-Apr~o9#6o2a6qq-lUOB4Xf-^_ApVgmtBuyi zYhP&_RCJ0_zay1hDp=4-y z&Ek0MV3o-CB+{?@qqj`@d&FDj1NFqZ1~1FsnBVMQxqjnx&B=zVo+mB6m2FK{QPYMW z$_}qTRMXzFt@8Wz=PP>F->$yje81{h(<{%Lz)RmN>lO9Ve(8lLi04W0e$CAW#J}JT za0Vigd+%5Ng&3BoLd9Aw;-R2|t%fRO3Uj#$<``)V*lVNo6tz^D4}K-sCHPKFRWgz9 zO$X0o2I^oqcfnyN21+9_P+FQMWA+5M#Tj6Yj8caKxnJtT&U7{ zViq!$oq{+x#foAlT9HDOnFRb5snz0Abs=0;;3%cWNaHaL&5>8Ag_yMFa9OI&9a0AH zw~g?wexLmk43hb8n=ab$@UvukwK*6fWPctIKRy=3Q6c=&cB{@azo7&72{n zA$MI3HKumw7Z_#@_Chpe2?F-$zD&{f?sCJ_5AGjMW16}MDTs~@&>BcyA zoE|C6GX;LXeVW;$=5pa`Jot+>x%QU( zY<;Bf+DEESH=gqR*l@D;c;ji$wdQ-C{-$T1pMy`aoBqP{Ht-twd+moO684ZE*FLI$ zPRsvh5?;c@Q=nx zW6&pvK#a=)2WbYjlHz6TZD3nshKRQi`N&L!aCxJkj13JlGW&$5>saZ4Jyr-e!SQh5 z?%{0Y#t^;<`h+v+XlptH9CFjpvmyBNhy}nNy#KHj7KKh5bfS<0%!CKrLa9jJAa%$G zxC1J-Xtj23x3(MlV0-AD&{^84?`8JrjLp!T&4bsRi0}7&#OH}%7Zj-Tol>RRso*M{ za;DPu(zTcc*EwF=i@UhafiJqFFtO;mt(LcG9MlUAE4$;1fgbGC^j7x=`)YavJ(V}>yTO0!sl3*3qw;S3gQ{DB zqcv_Ih5p8<(rQExc06m1T7DJ2FC%decItnFwi_6K_)dj^ZT$@@x)Z`)^@zMnIHYbv z2X#CgZGK=bS`FMJeAYCrK&UlnFpp$tc$u_=y}d@WnQbBZjHsDfP3SXP$VTvN#rxJl zL$8@QSsU2zGb}g{tE@y!bu}-y3a`=0PCV_gzNh!=%eX=EJPA%Y(i<6S34idiNR11A z)oc2h@!p4-8@ZQ%uX{hEpZ6>C+&t?&?{=Yz`FUtAASk zD)1UC+n3(w(CQ$yM_>J2@Di?jE(a9fzwD8SeV5R)O4PFC#c;|_H)q4eDMQTH3Zx}k zHoOrrZ3C*(k^7+rQAT4kI8t7qF2Y_7GCv_sg$5{Y#v*jD;-xq^XH3?|Nu#krf;lE) zCE23=261eMJ`4&uv0{uhl^mZyzRzfft`$VQr)8;H@ zwO+#IKm#;I%TPFnk)R@tJ4S#9hywo^A*6*W;`d|^Q!^wcP5Va3Q7)<-@?{M>-`L0L zfeJRNd-O=ZtJxd)p`xw+=$g*@%avW=1NQ{3S6&TXuDDWvwY;mox1z89er0d{&5B<5 z3*2nHSNR0~qh|v`O}aaX9uA*3Y;|H5Jlsm*=h=1KN;2iuzQSxS8h7smY`BBzuZ+Od zuR=%d3wHAl;t4f@|EF2TT{L+pS-~$AI)(!Nu9j6#H$yLOGc@tGSwB#F?RMV*=aBD^ z12qzy!{pe9{Ri!Ks@-n)?}lE~4tpEbhDg1^T+cM=*mc%TR?}tZk@%7OrL#+{m8@wz z7yFEJ=+a@jOSa`m>&5ioIwU2|( z>Ru4ep9>#M_oe5#`x0-z^geZ-c^(3LcN)5Z)62D21KWHPoPP*k>NX}n;1B=I?8)zQ)@E!LjN z#o0O38nc9|ga=nXyi?&VrIuJlO0`?eRXTOdG8>*T&Qw0v#*U@~rWIo1GJ7pITl+#z zlJBS&_`TW|_Ndk_oKafY7U&gU4uVhKa30>Z*KjVcz0!1L&DExBYr2|ltm(nI*>o4a z84uvge5bsx@#o4{!JE}uV1Cuu`k`E;F%20qb~n{<9bHv@r1E#Y0e$MR*bn#{Dvf#Q z(}dxB^p*ZEI@f(Soa@+BBj=j`igVfD>0E$T&jtT!=am11bDTP4AEer?UG#QnI&LC3 zH0$XB92Rq7<`(XybBVq0{KmYKJ}9KGGH}!1C7)-!On zUi$7hH>x@tuhrbFzgqXqed>MgzCx|>qz2y8HP7G=OzMui_28X0^m;l2*Xj z$lGofwb!D#Dd@nY&?(L=UvvOn4SPD2oFawMn3e#4TAAeN!})mJ*|B<@kZiOGVc;?( za0zw-o9F`2x!3U|R6 zWE6wc-Rf89T1BaU*ER!xe^>sE{x*wz;)HdAIf$a=qn80+#~ce;JO&Mp6Kr`O-(-1PQ1H?Yau1&x%eP~_|M{RI4-!>;ye zDEyqXI{ZhiBh&$_o!V>u06o#oxT71{GE|&>&ST~$^9c*aANQ;99``C7NC@W0eT(x6 zIpCl8<6fz6s89Gz)b{)v?G^Lhe8)Vu`s;e|I=$3*zP3B~xbA-7A$E+O)WDm%>RH1R z&lAMHzJ~6aYxO<2d+&KEX98+DC;&@eD>W)M*W~4LIhb!1N`<@%Z!gC^xLBNxK1vF^ zz@CkcbAk-aAjiYKtBeI(YnTObG+koa^>7BfENLz^&q?vc0e{nM+_cDC!YpWfs$Z*K zh0%v$cN>}t@DfTiHb|pTwZ+r%$UfqoL|~BMa2B6tEtAW!jRAJCkm$s7Gu#L^A}|Ge z^wa4XW-KPCDePQx4l~X158$U1=&9=s^m=XuE_p5l&*Jsi2j9mB$lbbX?gvl92eOmD56`t{ z+GYBpb$-pcrVC{knlF}JY6kY2yI1!#19Q#2rMH@Hm)&W)zvfZn%c^&7uWzSSM1N&| zBaT8>6pVh{0F!~dYW-W>1kq|OHqD2E^F^G9zJ!y=B>C@J6Y!TLuGb#;`)%A!!QQ$( z?r!m@+^*8xP;)h?1JFTkqkpjX(A1^%u%*9hMJJOp>w0+*q_ z2kf~&5%ovJz%$rN`_bRw97oJN>_225@b9ztLQC`qs?DhY65iImu)3J{)^9X!U76sI zXzG4I4E#)D9-bdbJjC0d3D<>sZTtqEk2l<7;|25DdBfayyZn#p-%zjI2Y3aX#sB@X z_kwdCujM=F_dP%idfT=TsL%Ri>A6k{72`(n z)9iTkqQ~*TA9A;^RS7;P!$>qhojgNYpf!u5Q57UpGo6_@NfcrrIL5QsbaS~_uEhw6 z@WV_DCeYFD1a_hm$0b;a^h_s(o@b^~i|vK}g-)t3)lKy+bQbzkom2{kUT80(7F#4Y z{!KnFc;!agBbQ4>@E1*yN5khkTFVi?vk%vv2%f6^ss36;SN-*h>&Vyc)ZeYRTYsy( zx8dfRu7<0X4}+cDBcoS%tv$h%(xTe66&*xu>+Z>0a5rru$|0oBGQ7 znx3qE(fFpa-`!s8fpWsX^ikkUPFJTv#cE1uax?)Np3Ag&+_2$lrBtR36-GfRYace8 zi{Xd*rM_EeQEz%5Slty}jn~)QYU-%n?Cb$^U@v+A<@$P#gFaj%>WLlJerAX+S1&?oEk{^VZtoeyE}G_ZG)>Tpi@k2y#Dhn+*f z-$DO=JH?5A3b;b<4ih$G$&W*|4p7ZR^ghRr@zzslD0US$8VWZMfh=nA<;Nypt83%ATPbY})@W8`rrnL}cqf=O|bnHiXWOt<0@?Z)z922qjv zMr~9VX+yzHN|55zvHU{Ckw$V!1~7*`N?_2J>?He>oSAfnwNhBFN09k3J+nTMndU@r zQFbDiY|f@&{1;^=q{>y=@HP_((biMvYc~5;$c~8U5ikmpt&o95(aDB}cXg=Kz9%9}Rn-}1SyhrVh#i5s zc@#WjG0zFTjfd7EApVTE(TVQovZ^I|gOYI#T7J$0H z*1V+dKp*fEP4>{?r{#aEzw|z~?s{%wZ*6B|dCBrNkQcY_sBPc8!@F)%W1Y3B%rj?e zT21y&Pj%Jziz*jvsq-CZK8wBe$DW7i`Qi2L=?&iW+z9k~&%?Pf%F2O5?h3_LFPhPO zoi50z4MoAA1;(JTVZ;iQicKkLnYxnC#uoQ{Z6WbEF*C%)s3B&Ac5tvaW+n?ssOgpw zUvTKC&tqe3;Ln8SzcyAFV~)lBGuRZQ_0~VRq3RG+v0w9RWLlicr0MCHkY~_YMh=~8 zE}`Q{a9GNw;mO2?IisX76Rbl$kIJ=keR)nk;j+T9f zO6-+%vAv8b!rs*{@YrdvYgT)LXt)Hn)b4dpqSm@y(_7zLMex>x`V>##?IvPg@4#ui zz4lgP@9G<+5r%ZDBp~-?45Z)&X-)KwKDaK^%d}b;M;$Zy?ZmHHtUo~EU zuYSJ*`FL4xGw|Jfr{YvY3wlWVfIm%1#9S82AMhR6VYkyqk@KBIy#eO}2z| zq2tHX4_!ubeCNpfoio1E?kOLz2d&x@=tv$VagQR{+wbi2w>$0rJ&xm5o#%BQ;e`&( za}H52w1YHoe|%Ctk@}b1zclhdvWM>f+32d>5t!|n-Vnn^)i0n6HWv9ycjbB)A6(#> zePot5bD!+Dw_AR7L%_eerLlT-tFU@|>nUj5_kj0&%X2H(i`aL)_Lh6qf6BTlv_Ugw zp<2nUQV*ybp))Z|8?KK~#$XR?5?mM4F{>^EXHw8bo>dDmS4>qF5|?9RfjA%TjnP^H zvNTf5qB3#j!q~E1JZ!XKmyz5%)3jLRKB!5t&5s)hTNp3x&Ga#C7rRgUfj_M}*w5b} zti#;9S#E-Ew~fi8tg4c!XyB$O;7>841WnJO`UrKjo~!U0BMO=TJ!sHgb)7dfi^qP5 z;OIf25h|nW)FyE~zFMo=U46&B=jp?i_cQw`_P8E88gdI=r_FdKQte&eNz8qE$)!D{q;YW zzYadFJ`U}-$xrrn&K2{TTv}8|H;i)L+nt{-}IJjr$(Gmyf}2*DSG4>5kK#&siLc6F#k*P(b`(NB_u!(1tHY(?%1~*jGErQhXL8Fi%UVEY8|dR< z&W1zw|EIv06kD3m3Wznwh=a|c;u5O_-*@6#IZcgGrYK{{^a9i4F<>NLHQz&}>A3)p z3*0T0;wF6~*Q&Mh8?<$Ny=Dq3_Sa=i6J(7QM2+KVl@W@ewlfk_ylhlO9>u3nGOcj3 zpop@fXp%1Ll8L3HpuAq$gi2}$7*~gdg>WO!qH~Nq22OSC5+k2207ImJZ2ua|SvaGk zE4`XsjcGu+u@QSnyP`h! zp3@9(osmiZ%NQ&T!=^ga`tbdlfW9#4jgG~(RvcX3Vm0W=0ejKdg^L19a13%lFLv4f zg&DaHjg`0Pg*{yRu;F3p!=}E{`-rQzn{SuiZRxLUuiwG%QxAgY0+zM7QC}}?wzdI( z`{;wXJC9<@a>6-*T&{y0|49c}1138hf;aN}4u1#kThtjO=6R10?AeF>2SW7*$@lj7 z_W*;9&T{HFrf4r!{7m3%1pQv(e)Bv2u=k<9mhWpF{9EyZ@P>MUd-s+7vhEhNcYmzE zRr5Od*4G!fUe~$4(|>=%9qvlY6{d6J1^UdUUDVc1d%Z_DobsG*{;BFxGto9UQFXSd zv*vRBRnJvuK4Txa?vO(<2X!bn3y<~F!V37#Re>d%{)0Bd}f%5`_c!2w1!dPXktG1X|~L+VDY%!~_4VYVZ&K zEdzhpb}y65dPmp@{I1YMyK0I+qygLS*4&-`=P-`5p_rog_{T630 zV&7qEpP7ZMr%U*xzXh8fTSmBd|Edj0_R&A6KMU9O9c;VwM(Ssu7(`>YAHJ0rtFEKY z{~0}|C-wJgE;k>m-oClrv;VsTb-UZv*S2hK^R%~~@Id#a=BK7h)t$|ql^xB;t4=kZ zukNhB1WocQ_!<{#=Q{#@g5XcQXWZqs!_{~#$Ex>H&qjd1l7J2ySj@OP;w88u$g$WD zi_wNlw*E}KW84vL7+3f+s7NC8M$t9a;b$HMZYN-4Vir^a4h;uUk0e=A}5 z8hP?+u>u}KKE)@p3J>O`BuWY|D5}PT>npfwgV3n0=Qn``4ZHzoQNk2!iZC5r$QgEw z5bZ<@F;1M2XeZ(%@aQ`7c$-hO;)MkAJ|~f%iSExlXD*-Or10~d1>9nHF_(l{O`O#t zthM9#8G+f%l0XqN&zZ(yvr~jt8qo>@TWvJyj)$}=$AN=^*Q1mKejoTw=-*8eBeg-` zA|z^ugjdEZXuv;O{iyL_>4T<+WsjO4l|2Fmv6Hx}uelHBab;WJFuzOQu59Fo>8%_* zfH;D`oz53N@2XE~p9QSPXPs8!b9nJOS#(Mu+-v+62Q%ifz(WWz%XPe<`3G7{LIahhC>Bq`b z&?`S*f5CIH{z`3U@MPWeKr8!`bxpr1+_G-5*P((|CX}le!BP%W&=bKfY>GNtoUP!< z$rIjLoX60>w?Hz3ct?ub6;aFOjPm-Q*bnPF2UMD-8TW9 zzwvl`BDnhhR@Yk>#Qly%K^@6kU^TPb!Afq!0ds(FRvX27)dF{X9WuC9F^IP{)sgZ@ zzZU#JkHjE1bW~SvRDw#Q+^npZTh$HnCS_yj-rb|^713K3PU#ngF*;P6p+JPEGS(mm z6}SN(8L*q6U8RC~s7|oq>*h>QB5b@5Z%;rc6mw~5wgW8=Gl@^OXF|OJoLDnjjIzHL z$6C?IG2(&UBq7>@Lccyi3dg+~W{iL)?of58jy+DybAdm34z?I(qB{<~1K=+L_4hdC zn*59Ug6r2X3$Wp@>mv7biO+6-$m{9}@=x?=D}JtjUfXJK5H~B|sm*+Zz8?MNjo9u7 z{%mYm*?YipAouV=^q+wx+{4%GH_RvFHQ#HV@y&_Bu4lH_6PcyI-AG;oNp)jb})hKb;8k=e&MWag3R_%|Ed z#RJaw>=tJimu~q5*^op<)n#n5%d*0w-#AqLTA8RW(3fB*qFkz0irVl{!ox zp^YHN7^RLghGX9Ijq){IuLkSTbkqlh-d=#cjYRkg&cuc?R9CnND?$jjF@3RcpW6{~ zxDz3dw=ogKmi}Z>FeXEbe9x?Ez$H?&UfTl-054{6^k}+EPRv)Hh7@D}v z{Ell=w+q{pHg2b~hr>RG(2hFe5H>2|Tnrz@<8p_%RsLT2UfQ8H@Y9U-n9OWu+HClp z**kFu??xPJM?Bk4A)iAnyC1pUZB*$uFmLPd<73`-XE*BFUH%`Ooz%|XPrGv%lbsdZ zXnhnP(BFq9Xas%a-u)E1d;eSalKSJF`m>A~u&^B}jVaRK@Va@V@2$Otnyxo^wfb6P zFZO$TEAKW#52pD}<>i)+isP-vtB$lBsp)7sU3DHiy_eTrs_0zTS#hc5D)!;7R-SD* z1N@zr9JUgN=(`)q~eGG|JQjOghMP3$D=ENQ#Aq z#3bCT<8Yr6M+`&~Da^rUrZGtygZDx?49FWzwt~sl2rx&O)5N`Hlrjp8HiE-c)uYXl z=3C%Z*~^6a;2TDPA%mS?IA|=;=QFA5Vmj8Cz>YV=#G%-3B42GN4z?kIVCZpa!{lLl z7$Wl=aREGi<{1mPx%L8XfisVr>!gaQ)4q&G@J?MD~Z=Yk97(v%RW^anAI*a_atHpKycTe(fqSst)=)>&> z)L-?qvA^a~{q5R&!5g02_1#r>fz4|zm&-4$yHwuQ+ynfbtvXeIsv4d(wHKiK+~vkn zK4*h;SUqc9XKy+^^fl))*h>;8C>>&(_D>lqLncdicl(q2@#w z_fhC(MS+cqES%^h04d-}fkla}b@;}>!Hu~4;8q@(|As-{Hj(6#P(_Hv&gKdQ>=tRE zv6#;=bHOuQ!sY3?*qlkjEF%T}R4GCdH1MIp7E;EDf)e>yaA2`Hp`<8_g-mz?XPS%Y zG$(^jcNfzO-FehJ7qdSXvpDd{27V#>0ei!hQSc8#qyl<|tHZSs>L~09jnzt(R0C#u z1_Lh-Q4$P@hnqa_wdb&NoH#bYN`_WtG8Z4x1%kIA@{ggYt)O484aV0RLU0H)hw!Kk z;BTA`wJhLoF8pX_;Z;SLKv8%#3?*zeGgZoV^7%ruh+Ba@jAd2{zsz2Vy~`4ArL}@v zWv%7c+Ns7pYDDL$@-S#e~{dX|;?eX@w zH@w%Ko4y{m$J-s^9$XD^4{liZ*^~NGc;fsMnuiyJcN+REA)O)gROHVDb)@e26MLcF zi}GH6q2J*kx+GJes2Bs~15*7>g9azM z%-GhB1S=3fR}%NY(#NEo><=SD!i}Yq?$;dE#f_YykYDL?4mx~EP*P5H7Ge`4oy&4^ zSoD^e94CuT$DZ&cCrrlN4to%B*s4uLcLQG)Uu!&&GYTA+31Fhn)ibypE012{=F|Cs zJUT0oN$22M=#B<1J1}R zE*y2&9DKdmU_&GltxM#D!;lqD)FYKKCW*U4?3AuK;HlKTuA=+oBCOK zB|X6$__VW+X@x7@H0kf^L(J0&|L>mvlzFAHH;A30;H%1CoL_3+Sg&he+WobCfjhPL z8o{Y=>VxjaJ?P@&er~!&VjpI7$Kl6y*n7>n|bKAK_ zA2)V!P*4_oRYm$5J*s&0_)zx(e=%YV;vZ2mm-Bwb3-aOXM;$sCPY>|`*Q zpM}n1cRprviOj5EGBdL&fr$@dKhvJhPPeABGt3w`uEDDdyyVS_01db;8AG{=o1AUnHD~Me(@jLhy?;DD&Z?1}o3O^D1M-h4v>V;v_ z3e$sy%t`oL%xB`QICduX!&9IGJp-RJ5j~a>P`mj?`dS~N4AnxgMN9}5qs;bd2id<+;Jh4>q$smO-O-!4p_fm@*wyOVSD1?&PX zj<1)V$S0wc-he1L6ZqR~ZD78$Pt%v|EA%DnAS>$?LY_7gn|MR9!7zrOJFs5`|Md(Z z9eb9TCgCJ=h!XsC)Ih`ZC47$tKUc&(L_A>ZbLhSuz#h@oAhYs6Y7f%)>zDe>i`;QI z3RDSjf#<$3@5uvumhYIm=7p;3!JlhhyT5qe*>CG!SublJyAM3~8ZXt{Y`R`^t*N{E zdQ(sJjmBG5cN@E^&NUqO9B}v59ks94ft3T!%URzD$0N3Ad(@NiS@SISlXDe*-*=cx z<}Pj{rU+-%2T~Y1cfcRm8?mTqNem?Y7u0{4-4d>I4C(ACOu-R8?!KkzceNd&onn3 zXOSPg0`G$Q1>V&43%m=O=XvM4b8F{1bG>t%IXEf)*_ah0=c9mKI>XJT@&dqMAfGC5 z3#p|pcJ~z04I%n@qks*9jBq$;MF>z;MTaR84iDk*aw-GvB8(A2rY=a4<{&BGq(t4G z#AI1%VAR9{;j_6EJDG`xI$V?%DUOtfK&5xEK2#c|PZkojBrYDj{78KacI>gMg`EWG z2I8bb`wDk3pKK(7n;QwWy79;WNv{yuu@nx?tP&$tA7X^b-aaGtefJra40zCX| zV;R2+z7W_qz$RGGWf8Miy#T#(=7ySLIyw@HGUDj!Kzq(JF#EJSO z_8-n|xLxBbU>5vVyXQG;cUAX0FFh}vpQ|5(gL|X#dR1>zx2L=DZ0%)m>O1SUytpUQ}PAtC-x%l-W$#h(tM$D;p7IQ8JI0C&;SG)z09j7j2nsGE!%(CN^ih-}6xsFP*-bKMh zK5`ZY7J3OLLnn~xTM$^_PYt9}3j^T01k~dQC?a9+S?U=K2ZxuUJY|C`;1 z+C#ANs8l1LE;W3dVp&YU3exb54LxmmO89k!F?27~?urC3!TA-?V;!m=7vKyNig$m^ z)&BQMu=iK|eNtWnf9K2&b{{ko#tK`sNM@vakArs?iIIeZ@4siBs_qKhse5AgdAi(F zRmU1mR-bF=#9ro!+B3n^wI>4|@W?z-d(1rqkF9OqbJq3RZl|~Uda%oLFff50?KFzJ zjpNYKJa1oOt~eK|PWLKx(LG4-w{~$mv=?xa{YJtK46WxlEoboP3-D-x4R!pXcQH^4wf#a%6bZ-85fXAkDuxkOnL+_ALr5#@mYra0z@8 z?4`orZei0R|KjF!I-?<*$qwWo2MkBo2CBL6Eh4w$FlB@uui!4k9CnHnXU2;e)*9?n zKxfbzDkSOYLb^GeAEylw^K?@T*przh;7+Hz3#cR~7OdD=T(X_WMjMm4Fx=pzTZ659 z%%oBKqWdyX4Z|H-3)3P|sbkimWa$g!c_#YUBnO0+7U7*v(ttl;F$%hNqva81G+Zo3 zb7L*!C3?NPkakTk2lX~FPkyW3W^Xv%)ZY3zVu{x9~(y^F{gx_<|7_!)h_ zr}{1Q2aiA>x>y(|wJXrZLx<-#D{sO-LE}oovOL)T&V4YkNGk9X&nz7tveXl zU$;B3y{^sOg9?4Ox6R(}+h=rpyAk_ty1l^cPqih1FKEhb6AtM|(cd}mTw}o#qjA8c z-Ay00j&sM1$NU513pf^%`UABuY7mnDk-0AvXVrKPT9Nz=W2!U}vySOnq!6i1g9b$$ zo2<`f;c&o#ONF0U3cmYEBGk;Fd`$Fe7mI~rA)7BPVe+kfrVu(rg?1sc)Lu$M7mEg; z1y95-Xi^d^<^zY>&};**ib@ZpQ3Qu+0dTDXIsQC&i{-kR-gJV=AYx+ELf@kGX;gYs zIzAUT44_s)CK?VEU);d*2;7Ww<;8lI7_UtgW6>Kf(ieeUIE!Q9;k(o*<#Ix`Y6kBy ztJzIpcjekkm~3~kKg9++41E~%qT%fk#f{azl?KbGeC6Tj=%MlzhZ)0hhKa+l-8Y)l zW{7ZO_@bjDFA?WfL_Vl#L933BG9%bG^|!Nd(7MnHq9Kk zT<4L!3DnroM8Jms0pmB!+C%a0|HR+`{yzQQ3B-o7`c`^uba7{_4sI*v*^&HpH5PH8 z`w#Czfq1XSZ}hJ!^FG;4d=ZDkA>^sg7IBY7X2Pa2GtL^zArFDd##CvV9)(RQq9uqLgvTwwCxVAK1Biu{&?tE>^x8jB=bRrXsF`sw@N^r*n9$GKYeYee@;HA3PyTeAu()fY? z!PrTCXKbUk8+++vdZ+KI)#bVF_Fy;gCa`zIbIM&x{oBbDc3AD|DH;4(#J(=7GjM^r z;2x*3waQ+%o^jW#9`2LD@DtIIodYIS0_qL)Ur_(X%Cn?7D)42@L{x}jr=vTYgf4%y zo+!-Hk?W!2Blw$R&Y@?M8_Ss?CYrO+uUo`tXgR=L5nZe-ql=}bbfE_9k=O_P6%qVd zE9e#a3K|+4;Px%`7r8}#e2m=9-h6W0g1Q2CN!=2}J{(f(lHB0q$p=ml59ZcdJ`0Pzqoed)D?3LUKrAWyK zvv)RBj;FwDxx`w@tYuJeDgxe$_ z`&<&j-%@l{$3ThkrSTuszW*Bo|G?fr9r79Y`$dMrv+&k<$o4pQ{ilpXsO%(iMe=7g zoQpTF`Qe>Oc0fq{BlyGf1M|Y@tLYA0sW}t)(c2MX*&OyAwN7|X+dp})I9EKE0^QX& zgFT3S-MD)%*BF6l?+Dl6cUm3hMWNHZOkX4Tb5GMJos-NZyN~NP-}3$D9qyBLkQ<5b zLmD=4=ZZ6xcyx_X5i4`0RBZt}AA1(Ftt9NN&193!I4;&q0QTlUWoa%u&zMKgv*yur ztl0#k_`4@6Gog%?W@NET>_T>_ML1BnYs~@dA@%`p&|Vx%SweJA?ipc8@-mrOrD!b`_OwycZ!*%fkIT+iBz&ZpT#_wO*CS# zdpkj#jQevc_WPpg8BRDm&W`5doH#n(jb~yUI4GLaL#pj_fWH*?KQCajJ<_@#;=gwSD=n+hueBJ*uq!PJq+RQ_t+W2A^%Z*;eBM?sJe=J z_d@-Tp0mMowHE@NsBy2>TnThlL0hBhCUyXO0yk=|x$V{I!C23t`n}RA?V^2&!)_kc zg}m>gbB4J9FM!+5L*}LRj{T(fL*2ZaAF9lf(v=J`19~heN)q}m$qHr;M!G-EL4U-V zOD8)?ToM=~^UZWFLrCja=)d2 zxqAmN7s4KexBWN-e{_ifezRIa6>CN4=`BTH1s{X2Qn6FyD*_e^>X-QP;LVcXQs66W z02TuU{`|laUw%Dsf{yhBczD3iN+C}mQX2#9`AH#FVZx1=2EB%R@|93Ksy%XCDwsm>JWGeZ{|GeamtqxTYJPvIl&G2&zsN>(QFVSLs! z8*?Fb2J|7Kz#B@k5`_eCYo_UuWD&tYIYe6{*b25K|n@DD`g3sdoo?wADqal=s`A|3b(@9SS&kQG%%EX3d zmJ5!(n~Otc7QmQYH+Aul6fNdq0@lL zsYERrT_t$Rz^`p86w9X>QOM@t8V?nvso3}=UkjV!IFr#igew4$1aJ1xG$?eh;(jHH z;lyVl8lR6FTAFO(b1iTK;om{}(!ifgT#e(&4i^(0Kz@QVJ1~>e&1puSkYvl^X}eQA zYwVOXqf%VsloE{ni9hmoiBm$a9KarfoRBRwR>9}1fX~yj5c?JjixK~_jU~8)p^^hl zs|<3iXUkrl3kJvo^4ZgM0p--F+b(2LxDsJI<%3>K= ztyYL0Nwy!g0Qz)`{3!vOZ6#64L zxilQS=~8VCy+&L`709{%Y%|-R<787gZVpa1m5nE`ibMWPu0JP$_XV=atbK zEUZV&Ybx{>H5YrAHK1+@48+AE7rs}(8gK=_7vg~cb!j*-V15H!0&nnSBXN%?Qx7PW zPlrbbJ`Uf6SOI(sJi)s%Vu+8zz}w)F!b4Dmmlm)%g@ zvzh0tCHdZ8ut(zHN~*+JL6x{GX~aUV#4P2?pyg6%d0Z~;-pmm8 zGB91wgagn}u1!ZT=J#G8;TU`%Gq}+7_p|a@d8_Pz#!du$AriE&rCRj{Heg4|Z*<^K zx~fL8XN@;xEBG(5?^h1H=l+lS2WXzY^ZsJIs{PsOuYMBf1O6UV_SN@Q-UC;#2Yk~D zfep1O&{*DP?$h9nYIJjVkn8o?_i^X;(l_0^^dskI`la=Td8d7XC)lqtIAKa7rk3DY zN!a-i=EI$34yhqAxkUbEk-9Y32Tb^YDfH^_1jc|JKi;Nt&?CXefVXTam%I&sU-B_v z&yQLG?;~@vP`pDb+9SRTd=vvSKJKj1<3te=r44kfrLH_wvR&`c!E2q!6D8~Hh`TF9)Tr1F`0o+ zCc!AdC-|vG3{UVkfK7tIfina8dL%xQGw}H#E_KLX^LHGEv~FSuUkDh-3L)}5xSHCJ}lqhh_!63rKekeHZubfU+&?iFe4;nDIgB`Hl57TeS z6z{(=5X|XM)LZRk?JM)O=Z*CO{#@`?2tFm=TJRZsP<(6y^(N!qGU0Xtzxn=%hf?r@8@suVfJA^@0sSQiu$ASM}Bbj>? zVE$Hs*b3e@)DggO4xM=LP!Q9!8E~Wlj|zO`NziYdgl-AC==hmWg&sqaG*82JpPGW% zH2ggg-=)FL7L z1%Sc8DrzOZ3ne%!-Br{YyPPgF2>y^C0)K^=VP_MaMAAnG{xXd$J`=fLI&!~1@Rw<2 zp}roBE$$bj9~1I>Lu3nK?~OuZB0gVTq9+JLP|1Ji{KCG|Pw|4028FeOow(nGUKsoC zFkV;vW`yeC-)rDsq51qFv={e_{<;o0tbJv_t%c5z=ecvIrYmr~a!X*nr^&h_-4G9H zP23f;o9zPM<)QPO?zf(EFRbV66YB|76W+2Pw2wSq4HEf#5&XSG&vYg@d#HFtDBNR` z0cSwyQKdukbQ!l?UrsGYjk?@Ljq0Lr=PsvKI4jVlU*RK9WGZ-;`0$GT{j9*8V1ODJ)huF6&guxQ7Sc9Ifir$Mx{5{DmC!Iru$StxL zaf>ZrFq8*o;yIrij=1<%{||RhyUYJa|CMfmI_NyqX*o~~9c+)~+~95cSNj9|RzC*4 z^k(XT{zv~BIV0B({h{~&)#R0kF2aBwLdYi_S_!|BgDCdK|ID~u+hgCM?ie4$*XmL2 zB)=a#+J|-*ecic>T#qQ=zhIx+{nS(YXX>5tf&Hj{6h9J%!~h=*nQ5V#pe~RXX!Ar& z(xen+uDk$!fpqBN=E_CV3J(1dWFzR=Im@_Wa~Tg@G3XZfkSY6Cxqlx2D#Wicx0Jk} zI6kYy!C<9|PJ&s2m|1{+9=Iz6gW$1{x0gE0{KY2Xqml0e#*l9!E&``Hcox`Ylt+V_ zomft-kn$*WT+uDgLVbf8CxD#P#f;AZ?*1QV@4+8ed98hag_Dy!>EIAzLb0)Nm7C-u z$!fN`tZJ)SvMj4j-EDT?Q`For#T3hQFx6=!A*7LV(ulw%E?NBY!pX z*YNf1YxzHO|1CCxu)WLqTkgY8@h7^S!@NWEF}*7O*2-L!Q-kusLF`|X)xvgyUJD$7 zw*mcdp1Qp!N`ZrxY?dTCP7Q-M{3d4@?D0+(Mv_!cGe`5o#&9rf@(!ZeIKoc!0rADu z?>ecSaH!9WOXq;*&gOA+v3;}3`#x(_T&`7 z8=V*G%)w4DCn`nwW9A1G4*i|_cBa=TN0(B5@FyzPf6?Cx z-qGIjuWEk{U(3E0{U!S*IqwH(OvLz;3|$zqe+Pdj?axwoVGhY=7R1?a*V$M<>M>2& z`et;&J9DsoJgTEqL5Cu?o6<>T!)Y4N!IgUy%#EgwaYohcG479Xj-^LA+?SIYD;&cv z4kqx`32Z65ktDYr$PndZkE8EB=pIh?g|fGc{I=LFF(vV1H*sF4;{2&zh+wloUQMnn z_&Zos^Y9Xjfde_jfovryhih6v^*8p(ySTqM+>1Tf3r}h{idbM!+09+Xj&KKRU)wn< z|0Pc*5Ai=emgG0vh$+c&OciV5zXW?__-*Ds&DFeSU9iQt)%%+HMYb_8{YDnNjyb^f z_~dU=-&k)mor*f~cGPq#;7{)5J>JVM*qu&CIAx9%M>A~ZPKp0LS^|HiL9jQJ?u~lr z6j5{L3`T=^lfznfbV%={Zg|LRMiH@wnVo&i%+{F)=}Q_p`-M-$TE#P%a1h>epEbN-mM-tdq6hOSZ|l?i_rHk0kL(ZZ z-}yhHpZsY0UMBh9^8QM1_dV?$YTa*yucBJ{p7>pvaL<}Id+=Ii^teG!;W;dd*)~Y z543V-V%+wFU2t3I!@Zcb6JH1}6gDcTy3xI6PML#k+D%Nj6TIyM`||YzEZnUv%J(`sKZF@M%As=t*FxN;<;bgP| z%9O8zPD;?dHu z#D1wEus2W|!1wmkHSfyy5kU_{{Y246*eTGZwMv~iY9|lw5S>k<3N8O~c6_j@j;%Wl zdLuqquvm*mXRNBK|1=m6xch4AH{~TpWt}@ zMEXQwJUv-Rr}To6FbaAiRhUYiD4a-fPu1+Tm|JJZ2sQTg=LaDgZtpAR0%dAbwrlK(2~iC^7~f87h_=uitMSsl3X zV37FGt6)y8-mFKPdr!DmyEQ1I*R~54)(txI!!kome!Llctv9drzOLU!J%`#~PV(YS z;bs(Wr2DavZpc2pyKpjdzW8M3MP|(&$)D0n`Agcx!VCH{`DfAozo$v z57F<%?y={iFOpp!Y@=Y8M`ngV;i}%j=ATU{6il zkf+o!XGOmlHq$Bk;qCg{#gFKb;4wkNd+sldm)u9P=bfvf?k7sn|7{y6HO9ZVFBzAE ze@ng{{5|t~?`@MUnEHDtM!yyOCHuDjzD5N{{I0?uv7d0oKp%?j=tq4*mT(=s=5iMv zDe+%5b(jOxn3}vM>|YBpU<+Hd+L_Po$sILDJ$xz{6zp-Bq&Vsx^-#FTjiDtup-+Sp z=?T=96NMBi%c*n%{7n`nQxk$abtbWmQ=Ey!iQ@Ro*xWdKfF_fZMR|OZpB1c*r@*27 z4(uX0RCaa*e@>JxkvV6OoV=${fnK-lHCLGP73TDPfiS}0)2mzYySvFzBrcpVb>^3k z1=Hw~AIXkJ=}c|#b)8vCSo6#t!!qp&)6S`Uhz8_tbFFNqfVm9gk-FXu?xtYS_L+eW zqLOwRP0}Z4&n7PwA4|>`9?afX{C4)crE}?p*@eVX=~AjC|33BokL*wIb-cssNFOY| z_lf%w{K8|XFO5jlTp~JFzPF#s0{^n83xy_n)!_h{ADn}*Tk8(lM!^&y7&BT?8G(P5 zZFXyQ)M>Hbd(1v~FpXZFfo6|c3;r8;Zaq6tuzf}&KO=s*G1_ZX6*lUb=tJf*MTg^W z_}?q$AN*JJr@U8<|0(`HvfpF3&+8`tUi!IjSbq#Y)ZZ75ga3~Hw*L-(_e1I}AK-V@ z925xu1oXY-{KUEJUVb%31+5(5hUTHIFj2|98;_Mk&W zDjd$>hcnSHyf} zu-#Z2j+v=KQXl89Gs%#8qh$5~I}Dg%LQR92N-|iPUTW~Wa-9KM=pGe3M`lQ_A$J{e z63owy!2vMy&twy&vCQ*}(bOID`RT>kdy}Q&Y}PIovWvy}^aHa|vc2?bN=Ic;_HTIT zhD2_GwGRd666C+n?&^#7Utll)h>T^~8`qABo0YAkPi?-*CHfnaYr z=93-_uWrBM+;8XIB-Lh7yXdCw@FnK9W#d}~*sJi+SVQNk(r3pGIsQShfnIZrKerv7 zoI`AkCId!A`ADXpnhZzkfzmP32Esve&>u1eQ35AViMvY0;p}jc`*N!~ox>S&W^(7F z*|EZ8W;#EUW@Ai}O&u#3#D5D0CzLM-hhXuz^2hjjeDZLjcfLE(c}GX`&^^k}vc(U! zKW87D0`Qn8&&U(&G8i}Fh9qH5Y~^0 zwwv7ReI}RG&OtO@{OfaHq>It%Oou0|u_$eJglT3ce`ad4z1s7O3o}p7-ji@k3+cN{ z_h-%)@5r1imYDB7ligGJh4C-+h<63Ma}^_mZcXL;6E+-79ewk%mMFXM!Pnsvl;^OVALp^{Z{siwpne&*By43 z)2AH{kEj|nHEG%Po9ZtP8ROAJVceQ1O~UCImbzgCHl?CKJ(`mOvwSV5KTRE*D*uQ! zo}bnd`I!_L#177+*&34)EXKZgB6XrD_EOnH$zSlxW5k5RiGleeGks^koeth$VwdYX zsSuZZ$gnp=jK&^DtO#9?54+1Afy~emK*SpTd$~V+i_gV8)6` zX0|O#_4y{4rK_2Yxz76{5lK3jwvGp5oC&jy-8xqB8GUA9H2s6c`;#7=wqhaDo-6(= zdnSKcJ703sCZ6GQ;jfHqf?BgV>M*Dxu~9@0?23G6rZH&MUnlMyoF7SmJMs>}-(ad= z;cqz8hwW>ketamR`X6>>TZDBawY^}!vB$44+2v-!oForvV?$ZBxs592=79V<-)x+S zI<+QG#eQI~=2QGBTL>1_>>QK)>HLSpWFJ@`xNo57ELcPvOWCLYe*DKIlk=+cSJ66y z&$2IfI{3(V6KoUjSttuq>3JU}OcRy)>qPM-NBzOt9o7=3?iF5$s*LV*ciU|CWs4pf zip;<5L7So$A6!qb8|6r)#nBA+0u0h2?65kGUKie;^gxOOU{7_+VGIrBMuRc`xO>bU z@|d>HuV8O5MSmp0bE&;ZpG2@Iy%VW6jB5!ta5MJ$><1=uxIcPllpa%dl(1& zT4q?bb1l_a9nld3?v>W+*AoNs?}z6@@6l*RX|B(2*Bku?@(;0pe1^dR>_|PIWqb}s zwdRdz-~T=TZt4U7bri4IK=?nieZpt3uXsPzVa-TR|yeFi8|mpdd(O66QlQ!CnwJk8x|#pIguI%f5*s$hl(4R4%q68Mj`b% z7{@!jGGn9YIKc6^o_#sx^m#Wi&%1%SKB>EGml`d;x{SHmJy9*1FRR#10&mJf&j4jL zb~-RaDKXyxCmjT4nwaruc+@x=jhJH**%lR0qv&VM1vByOg>y3(=5kX{E!;72s`TCI zrNTlclONRT!cF>TyjQJq=4tBk^t-v@u!K6Za<`=IMO)d{Dogk9Uo-$;Hz@4XtlRe(3C=a=VpWBV$J|8`L8 z-$1{#+-HW-uQ6)9I%ZHCmN9scm;yx%IFRjbhuLi$apV3kSo`NboWdA&5~<%MrB6rP zm*DXDaB?_5yi(U&!6uj;A?8bF(qyFRD3#_Ulf;C_XQ{oM8J-@!<9K4~41T$YkH$|+ z4l_o7Zm_V8oh`fkZ6?Y^!rjAmGj#?(syau1O6Qugn~X!UYl>@XFTRUGu5&I ze)LV&4L;W;s(ou=vfOOlh~~`osBPT9Dt65t&i9!kU=Mz%G0d#(V07G=h!RGWKS2zc zOxaHN}h|OSWdba`KJzM*)+* z?mP72P`)QS;J^Q3{e@_e-M3Hpzt%s-_DMbQ6ZZw@i@}`zVg9Q1p|`~LDzt%6g%bUU zZ=g%M1$_hQTULn1m1tej4`S{&N8X#0nesh+kB#saskt0h_E=iw+bCmtkN5Nxk8wU!9 zsVT#&Djs7?RzjODOlRdx<)_I_mUClpH&L7fqm!A5{6q#DNX>CLeI$Pynv>{Vumfxz zoKJY54!u`ycQH>|=j=emp&XuQ8C<}v#I)t?;$3a_d8=VIqgsuv-)?MmHX5*1%(dX} z2KFvp&z7Q;cPi|Fu~L@ZNu7B^p$-j~J``5R*r%ChlA}9YH(QxFyKu+kZ0T%veD=Fq zsc=~9EF45py-F|hZ_a(z`-?L~FPXVu1Aerwa-e9tQI;<=wnTNt@jQJsV!lEAE_@ff zNFUM6OlB4;g zSey8KE2u{^9RV+EL_e%H*ftn-ezjS}9#-P{W&G7z`=V>i#|y9OpMahB(B5U^!hZ{Y zAJgx6*?&}jIg~v5ZTlYb{0nTEtjKS(9xrv7>!=yrm=iUqZz#1eI3S`mgvtW_DG$4V z4cv#_V@8`v%RT6>?WI1u!$9o-`^QEOzF+0lu#KvQzn?gQQ_rd42>x3AR-+9Y*lP^B zQZW@9CwPi&9QdL~a+JrwU4A4vB4@cjC%1eLObSLviSsgiq-lKWaN;oMSaD41d{WzC zV(nO#zMjT?je3t99+P}}nB4jZ7DLoO@Sdp5Ur+x=bc4QRUB^u3jn1ZAH>yUkSl}JN zA%&y5&)sRdzl2AN}MU4ojF-p%#4P+^{1~Dj^xkNbek1xGpM%k!Ba$p9 zFC`b(_cGhL8wM8rK7AWIqQRdwB$-)^!PJq`VIqsk^bq)K4eGU8Y;+6w>n29n9n!lA z$nEJPO3(Q=^rkC_=enuS91c3OO@57DPK>(|p11Tvr2bqJ)*1uwrn{KNuJfv8TARh(EgvSM=@;Y>5zBV75d=?v(WvuCH`qO7jZ^X5>ogd@-La&Xs27=T0R~Q*%C7Je_{9 z@R)Y0Fry6?Z%LO$+feZuVn*`1)R)4~>9SSpUzj@#yw6&n=kK@yPV`!AFuqaW9qiW1 zs5_HAWrs?z*uWn>H)6kGs`}%Z0cy=H?B}Il2DgtKH>%czQ&B-Zd~ZPfN^cr&HGP^I zvz5Nc5jNno`gK|*aR^*Rdf@xa{j#r`T)7OpIm%x2gWi4vU&d!yWmYTvRq;8lF}u)S z6Z~DJUhM|{C^cS|n;yDhu3$wN5A5X^H|`v<7S zz-t>cCkzmLE;RV-P$lEW&{hdn+eBnD251>@V0uxe;%jHR76j znOm)m_<8Gj=YoCE+~Slu8?kvZCnS87FXlR^$zA7Pk51{D+fL&Uw?BVl*>Ybdz_M9$+uR z_fs$Cf1Pb{`50w$95B!ex3Kd07d@cA6v46UdRr%l~wzKziPN>tBDnd%|rO|YG&4DMxNzxi}H>%K0+aD>t1NR5N zD{-Id&$B}T8&1z^8GovGu-uoIIDV_QnVO7D`O-T;VWY~ghQA}Hl0HL~Sq~qk*&{z= z{s0{6ZRGmB?$I1RG6HWfiSVDoK~S|i`e!SBy6i+=a}8IYH#Kv?$fWZpjAPodfbY}G z8^x}PPiEp;;rCcx9#=mb*PUY(cCd}z6g1;tWK*J~M{FpWp9m|b-Q|^>A?T)+S8k_~tM33TKRS zrAMI`@mzpGx0O zEtMWhK00?P`Q)NI_2lAH9(twUldH#`2>)@9)IQ*OR)Lq#dcRjZfY;rtxp%$ysX_n$ z*I+(DHQ{mBwL7dqYnAa8woH{d)Eueq!GGiRHldR(b6A4D8~FdN_EzB(Sb{ZCEg;7r zkCJ|TeD)F3av!Yi0hi_ECNev*UGfv~2L4q4exLe2uYnJ4G+VHJ2fYK-mKxzyv2#P*QEN%1q}~$E zq-LU-bRwG0%tWZy%E?b=mbnP&wvLm1b-|l@_|zlEFVA1Z zKHAd@CofH0ntLYk@Z8D7BMT2rKQjMB;*PoRCx2G5(_etEwIRRCLR&-Z->3LH0^d}1 zE7@4m9ki|BkA5xly-}N1OI?{AOUzZt94YpXc%lq`*?xAH4uwal*ED4tz$@_`pYa~> zx}VQ(mm#}Iu16E`t6sO=j6JNtH}8#=qs&V52S4Ne-jbT}>+UPo>)~HB?~{Z4fAv7{ zyY8=?LFVReBHmt&UKP95R7?l|2>k6JCtByBM1jwh|JDdcoZJFFvq7yzY#D!-Itv8f7!*h;aD9O1XFcgM5$ct@ydtVV~RFEFH^$oKP=81)By=<{e+g-}B^5$#s_lBGw zm40pfKL2CmzF@(~q5<^7!rj@^`MdN&WEit~#-Ixe+RF<=iHFZDjXyho$IP9xPfxwT z*7S!;Po_`Lp2rS8k$h_IV)B{dV4#|)Qb;Xre8qJ(>Z%MZVx1?(e)vy-UWNyhnq5q(;MK=oz+0*`l z_WNK*YQ~Kc-F}z}i%xp1G?vhcrJ1Z>g_6eR?1ob{+3<9KcJF+dwrPH!etqy6>vJ%f z>C3@PSY>~ns&uaKuyH4we8PgC_9Iu1@&!FxIH!GUzBjpaYH9M){F90A&i*j<^qiNz zxOiV;aqdFm>C%(QOQlQ6CF**$a39#uV6Woy8jFVY0XpX3F9rUHdWX{kVLw~$dbJMv zzW86_w_570EwYnYXUiVB?XAq(t+%OTG69dK2zy7W>608`J6=cF#&*4Wc2)`g_K7be z4&1`r73yzj63b4X93ev!W%M#?Tyh}hO=^h?sQCyE_5JF^ zT20HgO|XZ5RUA#3BO@*pyBEiN%|5cYlzL?DzT|^*OUcaKy@^NW&L@nI$W_8apa&xQFUn`vEZe^dHIK3xB`&dlVgYO2 zZf1EZ@on(LrACc?S!@0;?+iuqRfPWBz|1JUmNKk)aV`yDvjUuCkM{)rS@sMNTh{3f{2S;$3l9u!t{QrWwVWE_QqgLP<38d)d@w#% z`C;Luqlxlqu1l?5a%E!LDs&N0l*A7RuaQ}UAhv%k)B(wVjdq1UnN4+Znx4Ciw-qb} z*ucQgf<2Dh23ht9>I>#cb3Qq%Q(w!52H5lbOduFk&++-0_;tiCI-!-$(iff)j@B|y zOZYL9`RCp5=|6B^$oweyS^A~m-!i`pew+QRTftP*pUvO7uNbd*uV}CMSF|_6Tn!P7=e)d!{0DI?WA5Pvq`%v;S zdGC`YjY?5nR_qR1mbvSLp&Xf$*uNxsFU%n(xbg$pe&&07gD&h}EBmsf|53;1*QB-u zwDEN%JioOl3ybFTW;P%)M{jS+@3e-%UuOXC5BrDzRkJcudwO&9k#EoXm<#E_9Ogm7VK@Z;j~h_-iPm& zKE(?Dl$}+5Pw8)#@%m^N!2iNV5u@>Mz>Z`4$cNsGgid**>rbpG<6@AzKmPq{i5DiT~gvw7~ky+n(iR@weD8uohz5 zmN6H^x0xV_vIQoT7ir-yCQh3R(+kFIBGP;d`~^WKtn@a`rO=IiMwa-PMw+!ITx^n52YTPdzihH=hCJ8VU+vj z{oKnOT^TXlQA5~}aqK7ROT%oIJ(4{f_L2L7Ke2zypB>ycG&bPOfW!Utt=O%u^o_Q_ z$G(AmPqzfS?O}SvUF0lH_@z4ho5X+EKk!#ZAN8ATBlwzo$ZKL-N-Mq83Y{GX)TpJt zag!N&GK=(9?oYuL{ZHYaGgpFFGw=Dr&Hso0wP1;EbSrJC??}uhb>d}g;4il`BO|!m z#-8U2avI@ZOK!>>isVD|0jVE@xjMlghrC?HePTmJL3bt3kXTRdS5tSYT8aClPRK4K z>9@fLQub7AAN&!iJ$v|GPx`=MPxXV_(Q0opGEP3{>0m~DsIqn9YvqIiz8367L8OI2 zUS|`WQL^WZS#!=PnUNMa;7%em^?bRFGLauPUWXyao|G;!Py|asGF#RqXN_{n0msm^vyc0itGm56G z+1<4&e@kxb+%3k9b2sX@&K0s37az+sEq*KY@hqGUY{PH8?^^dorf%~(MwHgTpYG*7 z@E2-E;aRP-WM)pzo=x3eI-4%B?dP7k^XZ4kIWEjTm%hE=W@k{LtAiiA8=dGb_`5^Q zd<-#Pex!sN9fyuirk_6UVPd~->MiXm{uAE?X9it@7J7AE*u;8oFEb;zu=xVE4(!GaS%E$P*75u4vB;GesRBLfM;9%8-t>h+?XNZ-a0P!K z*suEVRif9@zYDHp{uX?skpn9Z!iUZb+XKq&{qP2fEtj#Q@*9~Wl({arZR{z-Cs%p3 z*u8^bBes3@_*-%f>c;zt|5or<10E~s;}G*<^Q0DDr4IO`)=mhF{c-@Q8uu>bS}`~$E1ZL0~UYfNATej z(`Tc~^fkdg{VJM@PZl;L@60c0)6{LhOiXca^s@e|U_rmeU#G8Q&aV}nzOTbm*dW@K zqTl9!CHK{QrFF-`{n`5#?@g~?d_46@=@VuLC;9BngqJT%l^1Ffm4$s(d$5$v6iSP!`O+Lajr{cObEV|M+yjZHiw~xaFs%<#BdOs%-@`0neXh#t4Tp_E zcvgLaKN#dvMHk!0qY~s9sAfwzql4#BRYgK>mv4V6O4*FuhXKQ&G!U<<+|wP2nVIozt(uf6&wG){fhUJ z{_Eh^*_XpB+PloD;d#-e&8kG_T2t^MFj?4IPL z%suJ#aKLIqiSw$Y7bu(+6;IHU0dxCQzN_ls_+s#7VA9aeK1F>a`aj_w1R1g{luro_XU5d?o8c9 zYA@0QX;r*vg-5s39LljhTC=fXzVB;+uka*(R~@!q>ZkRS=BXJrvWi{Pc-&ULHUsA5 zU>6JVK^>yx&Kk3pcYJPpPEj#Saik)jy z+*4{?%nKc0Zlejm+0M)_drI`(=-IH74_@tcWVNfE^}#`_WBF$qi1Wqv(Z^D{a=+t?mGt+8NS{4BOj9p!74-}UB=lg26YWa6ZCdh)DwhShxhM@~}6V*fZ)i+H3;w`E-t3?i$Qv5aUuP!Q!gc-Up$*w zm_0x9*zEU`KP!GebE^2m%#Y^onz~YacKoC0e<$8XGwLtiA9Giu-|N3aU-9|E_q4|f znM|VCKa(g-C;Ia!$H^=QypSPkq#J^5)=^P00ed~vo`s^&yw;q|qBjd)mDsNet|5x?%o;FHPk(m{ z`{3H}TM`%6&`YjIA1B7&HuLkIC-WX~NdJ=iad8UkNDqDyw}hSyUnU( zj{P6LQ*u{%9qM>&Q`^sN9X2VpOWZ2kC$>`JKINzJy>MNZeJ^=1f1kv83V+yRr6SHX z0|v2`QilU_|(*qqoiw~@}@6XpW7^E#(5cOtiFUW*_Dc`Cq zd$wZJyn+TE1#?;v>=lC|r=*>6Z#QqxoHfr*Q=`F8m-Jb!=oS?Q<+<_cX1e`UcvseP z+pJwMvinSGUG_7%A#azlCbv0n5U7)A?+`=yskIcT1`1j)5iQk03 z)}JjrpFKZ&cVd1%n!Y^u;*3|i2siLd<|LB@uoMMbD0m*$N$&Nv;muZAp6TgAzfSG} z-oL8G5uhF%w2M)AEd{`Q2;Hkj3j z88!HR*pv!p?04dS(Gm{UVlR7yX?*_+QCCBp2ZpyzYY+sWf@8ee9zr--+=j_!hfH?_(KjGDjMp zM)mte=Rx&?!Jx#zvagIhL?5?ja*1p*OlRFl<+;Rl)cwS^h04CI;E2ZrZ_6hvW=mm7 zaHySf!Jf{UI%}PsJcqv)3>LMb&k0s=c&9gQ_vJdMdUiO6%|0w%FOz3({&DHG^aq9CrGK6OF!hoD2mOiseCERJGl_?0f1LbX z>F22*&ECx%+&zi2#o3HeNMt6X0b;x1?2*9KLbe86>$BH29JHAH(@?cWXYw%0?1!{w zH2K*Kg`GqlHbC8Q`C1pf47u*xv2UVp-w^C&&)_;Vt#82ov4zvS#;p!pt*!`VFy8w* zK2Lh6fYU(qBF>1-!KMuE&V^CY6|BwDOHDGRKrkKa#_zs+@{9F3ED)xh8T`RUv z#f{`F_*rTz_yM@w;BG}DMfx9N`=q8Vwr@W*7i^!ZVK3XZ<=B!~RJ3gb{~Xl^VIE25 zoY5p!cDA0s37bgFJm{u!Ni7*Bv)Ra#*iXf663Yp;mhD%JN9Av0ObM=5uvL;CiMybm z%$+h$>coqgd81^+_$$TF(@S!7*xklqr{6l_^qIYOkJXK0VY|H@-KmgDzH{~$ zGcVzTUtIJi7w7Jn_(|!l#8PmTKEY45`-@Mdo}7I#ak=yme5;p}FQ9Yxqv$?*!?US7 z3x$-QHeBve(%A3OcjRvc9h_x&8Qgev&z`f3D4fD za%*$@)zlvyA2#LO=pcq+Sm(4U^MX4ZF5Nr){Qb`zyL9KjO@FGUYNP(uJ);` z_r7rAvBJs3<=Mxke^vY-`F#G4^hx^la(= z#6qEvG4g(PI-ky*W8VBL5p_rA^UCvM)c)AHQ0&We=3BA{$SvW9Py;(?bl`uLrUvmI zIS95dwtsT}K)9c{4&8Km4L8FnV9y=+yTNVu589Idw!%N%Piq#qLGmMbR`i0%HQ=8r?4iv_ z9g)YlKIjwEJEq1Q$8=yl?&Iy3dJjDt>6wTvk83*2#ECrypYlF(tth^Oa2=={x}!$g zNH~JOB(>r!b`aiQp4+^h&E(TrEw5=h_qBXBqeps1hcjrv8PwqmnvtPd@CfxNCw#7K zE|TNw^nvwU;OcGw&mnZ^vGg1LE|ZaNzj@g0fzj7vb-Cml?je*y+N~B;twRTZ zQv?3k>qIrKioIP!`B3-H1c~t8!dS2r4xhPrX86vFg`xHvfSVBfI!MCI_M2R@nn zTku2c!Duf1a^b_N*MrO1S4!t5?qB%P_#1^+QqRCaD3mV1G5v1xJEdpRPm(8pk2&D6 zVk%WAETRgw%3zC#y`D{?D7qTV8Damrv3*S%pCg~C)Xcc1^VIqB9n8L=wLuT$)13-@ z?zLR^JJ5mK&aQ`>;MRSEy6`Hxy2RW$@Q1yu)8gD$G?wsVJhve@#C`_wr((A{<(Deh zEnDV)BX`vQx%rd)Qer88^7yH6VKQ)$CGlqy_XNL8-V)u${=5Bf3#+hcYUXh{-V%F< z|3yzmG~Qw#NNxe{q-U!*F4#Z8)IL>r29Mw(#$U`kh~rcG8VYy9K>&BlXCJr%HAZH73tLbyY<9_(Tq0DLX+)RE0j2*cXTM*tywiT7aBu$j zxutu??p!)GWtg>-U_&x%r1N6AMPzoNs0a2;nw-BfRe^59nFG75-4WCp?#R+`K;TFJ*p6 zk7yM+vhV}(iySJY)L{i7=#szt9AC zhO&`jCrx#KK<{^8jySzWAE(>xQuyn11b>|<7}L!}oq;eDgwY`n$w_AGq(M@&p3O8?U;* zrPusw`jznCvM&~1PTf~JJM+-O()f8es4o}3pE*}J$%eA)h~-g_hA+C#`+_|lv3Ib9 z?=7J0Pn|{W?GXGe=enZ#BK5*J2IO)45WITw+Hzv1J4&;i_F7|y_Q@g&UIDHhs~0n2>6pYg1AokNU2S$m{963 zfmH2FlX(eu?-=gA8Y@pmq ztx;;&s(&IlSl7hK(Mli^&@b$sorw986=HzRzhh2=1 z!E79OlcR9=&$ow-KH|O}yNAKI`|2t*`*wSO z%Dv2d+xNX6X|JN+gI$Ef^f7y_-pIWcysN(-{z?CS;h~J1ugtEJT}=LF8*K(O@>l0h zL<8pGLSL%0(3&B)V9rwNS8PiT8?{)wRQLw%V5e2=pBDGCYNckzHIDi|uXCGsQw~J| z_scH!o0?uX{keluTNB#@uS)b?_tBS<*A^Qhhdv(hWF`IM?dDDHPV3*jE5p) zgBhCOFALAgNT%~z61xZY6#VhHIuW=30f+cq2cPS7o84mb9Jb=(e^E#>J8b2D=}6O& z0e9Fxj@&8?qAuLvmIQyP@O=J6;r`;WA{g`@bdKg9EDnbccq8`3^vKM81EUAdv`%)g zQL#JUffhoC_7=Q?&p4mU-Q@R~vxSHCKj%Nv1&6YK{B7^oxu=-#y%D^!f63lL&LF!- z*0B3#EIPve!`^f!T7E5&^fjofM$K8Yt>BNww&ftRYHj4bEz|+yo>mR^Px6mj>8EXE zx5GDWssDYE9YN@dvlFQen37;JM%Q#Pkr;Y|E`Y_fPnFal6@N476 z=yLX%;C}sfm(Du)JL&q&3C44`%N%(=TXZtS2P+IIA1gY(k{5F%-zD|~ccMwS%i`P2t{S`xG|dMJV2&*gwg6W9$hhTE%_XRpA$`;1B)`uPgE7PG+vQSlfyJhzG!= zs%c|aRgaI?UVeR5$A(9!;=fw*CiX+QM{-m8jF-&_2AMGehnX0E5jh8W2H3*)iVtQ+ z30wu?+3cOcUD{pV+4NlCB{F$5eDlip{=@bu{K;IC*uleQFW6((u-HCSs==LL&}p~Y zb123+ZKC2&MX)Ic?ocsoX3Ju%>WXq1wLotn$`tOJo0z|E_E>(&8;KqWk9r(+yJQ}l zxVQg!-R!o!B^fYQ%eOdMh}Tdb8@CN=}KNYop&Qd9P@$!5!eeuA-NA ztJ?Fh22BN&1*k#SIhFL(4q+$j!AIP~+DnYI-|S&OtJuysXKkc@i$;fV515hO9z1To z6#hc{e)M>D$(y6Xb<#ZPp0tWy3mfitGasUQvG}up+{PG`zeDU1d=>I>!GYxNVjKTq zdlBvec2w1X$+5uac zw@15`Sdib#iS4Pxeevrr_wR^V1%Jc~MuX@MlV$GD9qo@$%tg+C#G6*nc?l2xlpnPv!Dv!la@m^GGYlVm`?VEOKLfZw~y?W9zcXd2Ok^ zbfDB)&jy`+s9$aOx4LVCJ>Ef^%*{Gbh}8^5&9%j8aJ5{1!OMhqh7-|+@R;|2KT>!g zKN4X3JUJJw(W(0eM{CZtoM4+190B18l+k1Vi~eMAlhGR-riX_<75%cx!Y=k3lreK$ zuCq&?Iac<5F%t&<;6ZJ&zTvm!hVw_zUv8$>49@`0sM2x97D^l_c?LB%>S=ZON@lvS zje<+^2X-y&HrTgrZd7|bz#sX?%^qwT^zh@^qgwKhN>$t4r8ujygQ1?Eg+GHIl0BI< z{&szn`=uN+#%As8k24Q=$F$I{(j#Y1pQApXX5Xy%#<(vbIV8SKn6J!ctv*i@$5(w-oB` zIeXf<-#zACTpT?~{!zRbjz$+*3%QgXnY#bjXv4YY$>D&0nyKkkg1@a?x3a^K_>Z$GcXK#w4i;MVHNx{_UR7$%y>9P)n#QfMRat;-53O)s=U{2v8#-3;d%hzJ}B#(~yrV^LR47$vf%WF%W zPv(=PUyDW;f2;IZRBi*#g@?+#Xt0^u)dprwg^LPbfxjiL846cbwxg@w9ll&<*})%s zUiNv_ye76(YS<3EHv^@?94cDQ0hD|@yiq;jC9-M3VL&{%jKMhn1&`>jr6L<{)yd3V z!THSX!I?}HgE}aY z`C^5>`|N(bajwN^Cq^VsG25JKeBIRD`ib2A_L#L~fX&*4I&F_^#u3iE!ru`4$;Y%Qb|p07ANGR3E$B$d0e|Emo2+f{ zP6zVU`VGMVF`L+}wro4STk>5k&Q&E|i+Oz2@TCuYng=&(<^E>jEyLem!QXZ6T5qRa z7qW|n+(r0R65sD3Pi|xrN+I=jZMUD%9jD)z zvmuMw341l0H!744kj(2!OphHC|Em0{iUAduMfwo&EStpq@!SSBQ~WNNQ`{`@Cw6Zc ze`>Z(cvVur!@h!{{hwk_at`up!HdG5;E}uoe8b~5wxZi}8!?RJ9x|IG^KccaE+l;u z!JgP*;lW~Al>L+1kMOo){EfKdRx?%UDlB6)wO`q=)R=2$gU^_+>!zPoIFy`tB?pG$ z3b;4SuspgdPTOBOjN(*$i+2mKN0k>IPtDzod}Bjcl&M*5CFG1fQq_}G!j$7lM- z9`%P359UXYKX7EcM7dBigAuA-mHLiowqdp+nSUp_~aHtJ9)JlcPq1zH7&wy%UF@9_$)=Zm476xuHWt z&mEQfUH#AYbag+~*4K2QZlswloa7E@ICp8_5A9LGV2|LBGr&A%TFnX3%iwz4&dxd6 zNi2Ie1%KbP4+f*;1Z(xRL7&mhOh=4AdTn_)li+L_f7A`Cskg9SDcXTvZ#%IiewAxv z3p?v>4$<^MO;5O2Yl3ZVW7K9gMZ)g`e=7Gqh!%jvlJJI=?Q3J6UG!6T>zn**b5&j; zciz8XT%hJM>xP!=dUlr3W7t{e$egX@&*-A5dYY=CsN6^Lju?Z>`~vB55a&yMdO1dn za}{im*g=I2zPDnNRDTWZ?In+|;%}9|S86lDRZ+bTg+GZGRD7v=c+zXzj?UqFINapE z#D6>R!4+|Q_o?k$#-98wu@B}NK(~(Am)$$?15L-XnM3k@db+lLJZ#DC;hn(ybA?r- z4q9h-xI?f}GKS^pqRBx#NWVkn#gZGxIWhSAwDu>MqaO$EWX3(lpX>}2f6GiZowWnG z)?Aa(XxCX4&SnQTs) z*@@bK?brj)RCZy<`-3;*{lA$T3Wm@!{-&|fAFw3Epy=DPOA!23@m?~& z#y*_Nd>b67JzUf4*_TMqBe>T88u(+TjeR6#Zd=fbj*a|`*gtBs3STPxJ;h&<8uChS zOR%SA#+UI&o`AiJYczsA!KKU=$@9P?hp+LCv5&^Tg25`TCt^gklj#8a6D9VOT5@b( zCFZQ8Rzs~E&Ad%MyCb=tcj0@*f?#vSx8r;Fu?s-rKEXM8hr&H|XL^Q$KjE$>9WRF< zu&?wFvLSPoW;j8I6OWC07#UQq)~|P9X5@O@L37&I^o*}(jeuO3qjL<&d8z$j2gyJ1 zx03Uc`+~!4F5uz~z#ZGw%AHzjE!6oOwt*8>*Q4sU%O%S|ca0o`8KW&I;Z(BWoh>F{ zkVz?e#NvN@>|W>4sYA)$r;hXvV*iGpJIVonVhf+`?d*E8rKkBq{czK4Yr2DcQ}EXr zi4O*U_}*^Os3H#QVE1?{KC_{24%G^OqBFEPXqMV8b06qxQuDh8B^Q*bz3Z6%Sm$kJk8g8Mb|Ntwsp2JU z9`ho?6KE#C>5#c3V!>8xAFp+tzsE`Y3(i@8(U^6!rh#5ck_n@uP8X31Dx^x6Lq5y6 z@1g3>0Xmh+_DQWzY+Z~!;qS$@X)sERD7mh1UF0_A*YDvP+{>)6;#q+`)#D(KuVGK) zUSdn^spK@gSJbr?heos|c`Y839vU@g{KrP-j?t*a|B5chGXB7(;9NLc#C>2;{mrrM zC|mpkHgEOw-OFCFgde{K-m4TC@N!GnkTpY7|!{&hDz0RGs(-a-7= z3H}6w*gd_Ad0qLsQ|chfhc*&VgFpHVThIgN{Syq*`>4qs^LlgjD4dt`TZJ=^{{?f* zaYb^Z4juP;Yk6y$gT3e#th~L_C(4qbm2mY&)$dq zkUU26%37yGY66?3$uJ(2<IiQ|)o|qZV82y8kJm;citCrXqg=l$x~ZbgA+IC(`AWPe9DXn;zMl6@YD97F zK^+VK-sI*icq*>gKQPEFoD(=o3kJ>=v%E<*iq>bS?rU5nZE!F*GZ{OkP`jy?o_9^@gJll8Zkl^pZ14B)Vt%(l!qJp(U z*gx=>?P5<+CmN73{`kB@YM$7MJ;W=q{o6?GkFC>wZ|<<)4Ht9`eatTQDs-rQM&M7` zJ{9{>LzLMa!CyJ{a}!+CHSA_D3u<%QiEqEI@b?w^Zr6BUqo%aZ6Wa$DkNS(?uM)hq zQfCp)h}b^1B~#a6euU3*qkoIF1&=ctrpyI5vMi_09&RM{k4}A@1RcPKV?; z^={g<4ckUb2;A8Udv-?jVI1tC=pq!**q9FcGW)|l*=k|7qRd|BH|CnXHfzY8urjWt z;cu7mNA4kdZunqZw24HsDA?|7HMXGrzF9WtxRqEMrlzoi2bk8mRVOoXZi0E=WmmD$ zlWGLhlU*)c1^2Ld#2qjP+#zew?VM{%bv@DB*Z1s^L*j$Q_YOTb(9uu)*YQ+S7x)`I zc&a5S{L+i&xGL_torfS;e&8SHa))==^L74&@q& z{bWX@p3k;{I-hW6n(*zimj><&d0;bnNSVHdyt&#NLmU6JnL;Q0L~ani?jg60c%(L0 z;qo8*UZgLvm;SHp4JCgi58((Gfv?r$|G=KahH#t3_NhZ%P~|4zqCCb5uOl@Vw>>(Z^ZC&wk4P&Ur>0FTfSDlIM#Owf+BN7GbrFSWa?Ek%GTlF061#0 zySyQD%%3ud19fr^)~QL~BS+k4V@pN5Z6})PR9eh!4ttN3#(KHC-)<>GtYiPiLRDIVd}{!QZemz&7=c$9nqvp6fky1Rp&3++gS6a|4}6p6%)E zc&eql{-OGz#b)H5XJ`}7!sN99k6dzbOY?iunAxG$U81Ki{HT3z_uCSsP= z_?=dKbsIjn=~MfM-(=>zE`)2u`?=HKgc`bR!&z@{_xG`zV1q@~#a8%pt|bFm=k6gk z6nojgoD%cia822T1peBY^KB1ghNPB!riE;8gHGMq>2|ZO=Vh&KH*GKv0vV@#ua{N}+)THJZB`!}z{f*PGebwS!Czc^v9X6}(DCn;{nN~iGErV;Z?U$x zWE)-${S8s5X>vNWE*tE5Y%}nV==fY?AjY4`&P3~|JIoA5$AjHR`=06RI`Zsb*C1!$ z*?#W#c6UA5+Sl+<-SB~PjWfriQ8czY>ECvtNg#fgSWsd=IC#Wc%w_#QY`s@=TzQ)2 z*Q;-!2yN_?BU$t;dJkHFB^N>5e}q4IBa5>SA^GPLJuHR`>LD&uSuO zWM)>YF$&FwA|%~O5ei-C14x&;)bIJ91lc`OF#dHCrT}E-b6)$r&x`*>Yo7Se_+R?J zXwh~$qtSCTS~MqqzjBz^5Zs|1p49w$e?Rx&zHWshdd>lV{&BRGug16Cb~H1Np`^CY z`zrj>+k>Y3>EH?bfK)GI_b>KOeGbitp%*CLAG&S#sU?a7bIouD?*$`4DsT{wNQ1qH z{(?8}kAuI?u#Fxi`(Rw@swtu(5@T2a_Cuyf~Td(0m6t6bDPX1?cCp zzvBu!W~g7HvBORe`Ec1va0)k`dv6!BpXx0Sz#hKdaCdnB=&PR&Pf^c34R1~SjTUqU zTiFeQedgzxe8c!_akvs2tvFTuLG@BpPh@ta(h+EH$kU4JtIR3<$?nMqgF!SVee@;+ zOW123K3B=XEzK9v)mW&HvRk^l&;#3KKy0~2XR)W=SDLG)%G7AUA(%G(H*^rO&FF7} z!5iFv%#S)ZeROKsyU>DXxfgU(VG$qZMKFTT9pk{)55^rZi2a*!ri1DFbEp5OgNq}- z9vK|_&BXJG-%fJIpO5`!WN-ldJ^IuvC-+>O}temG{oAV(%vEcbJ7wnRtl$2FKZ!XKv>y$$8QUDz`Ae3;z#67RuDV!V^^ z2Cra4&`qnH@DI9sT`~|91dp&649-2X;hGOZ7XUj7XBE5mT(*yxuZuZfd@;Sy)974X z2%ZE}LCPzT^QOH;kDUd~XbsXis?93$gV;+QA$FgfXPSK5& zt4fo&ocfaNXEb zOzlSY53S0R=$*E(tB>4(*`q_$U=;uDqn>`#XReak#RL97`W%MK)ki+gEIRYDJ>fUl zpQmK|S1ZTCUwbg*%>~OIs@qhXhEaAM3H!aCfa#ck*U9@Am^1zipT>J^dcR5k*VI`| zp38IGq+A55ZvC za~xZ39>Z5Lxr4!AVhh3FC!IeN&pB2lF6OC4=Sy)V@5N;+Tb(Hk)CY>s>%(+hhl)>X zkDO;!m@e^@y^1BijjHTT?}mTfrHY!^KkUEyjpj&C1P%b&$;3HN*&a*&0S=!#Pu7Vu z-@{A&5!x8v_55%k82)V6)SAU}0k`tgPtoQ%2M3|ue%$CWb(fF$BZg~d&kS=O27hP5 zli0vB?qzD($B1z*v#(@t_(kwVxHmc;JqWtkA9=q@uM^v;pWDZMfDNYq*Mt8)q53Pf z>WqC14Brb!+{JK_{S=iZgD$@#VE+rg_ZInue6MDQn)c6N!qh^VJ+-8-i{GWr zqFgsQ^y0vdw0@Q8Vkt{=uc@Z)JDaf?x5#Ur6o> z2F0H;HDpsikv$AF7l%$lVDbrSgq9aOc?YdO)m^Zed9^pbIu!9HkrE zUuQEm@8lgHH81pph+eUUY}@s2dAGebx^V2$#tukp&S)wz``Tes{ev6WU4LvpdXJjY zk6<7FxYzCbzUys&IP|NL(UISbz8L$>_yE3lVB|ML{m*~ZJJ|7OcP1Zx-@V-1c)}bw zJvjDqsD>ikC7K2F8Vvqai@OU~{t_4@%cef4{ulKS^Zk9bzG4qHZeM)azDuUn0~ZVb z3uaXN6NmIBdlA`VMBV?i>1Cfn=l(psk2W-`w7pdIm#@RKaku}t_Q1JYeE=^Wzbl)J z?NhwhjUV2F4eZ7aT#eB5k1qM`;e<2JOmsgwlpW#2fZlF&Ke$ERk5>twt9(OPY3gPO z6Y6R3zN#jt-iG>bywBuEn&(Jr&BBhftkAmTZ>HZvt;Fni!}sXtDee;wFUdv0o^s#c z#UA$Fa1_AQwaNwRooAStXJZnZNY65_aDlpuwB2@jGw@rQ_*13686qq<{ak7ZZk z&`@U}Co}b-4tvb^J*TvbJ>hQ`gYw79W%0#DYBC)4>Oy*TXk5j0mQTi3HL6+l2%U{+ zt+*MzENqacOvZ!xht(eTx1t`^fZ^zK*ZSNYK9x?kT(Cu!!z&Pwx%us0yL*THPP=)!y?$5aLF0ZI{FQn>^c0?c=&uaD>)+{fhkrRTHu4vv_~2dq(SQ5%-XZWeaqs)C z<(|eK9($DD2-qxAm!a0Dqnb+}{gX=@L=qo?4{vN@7hAe zjo2>v_}lObF4||{*PdWT;ZSJyCAshp%)TUe3vkMa|ELRmvVFV#Jkxjih-u+lJVNe0 z>>sslIuZQ=`A6A77r%+UW)BT}Xz-_NlAEA?Z){;`=7SS`7ikyOr3nN6*3g4li#AIy zqiyOr8LL0OneVQ1N4eZ#9#;>Gs}m3r2e5nsvMwUf4mhxPsG+r@Id;W;7-;tG0nt|X z%V)*@ouT6R%NcvNKD0&r_lx0i@HaI2o6+Z^9h~zCNs(GZYJkYXYm+$#|8F6UZDRg+m~R^Xcd|7SKWp958R52Z*aYf zX?F0K_>cM@wN&-^@ORzeLdCUhS3NcDQvioLrjOg~<$^>1Q*Nw$*k}gs>b2FHdPKq> zbLXXvXsfgnzA3#9UzQ!BoL-cMo^pEGwA$_QZu(4)dOCdU69utzyWLZr<{~i}>TCX8 z=RSRs{s`7oI9q0Sm^&K`Z}sJe{%m-B_}3#tU~o{i7~v29JN)n$trNF?{CKf@y@y@o zR2EGv2L?^vD~^-+%2axc76cg$pYMa$OkThd_7aW@_RsL@AG3SoICZYO)yHmEjcPge zuLIwUJ~{SEI^Jj5Lr4ArU-Sez4M)Or!2@d9hx{+lpg$U+i^uGd@b?J(Qwx&*F7~$* zdvc2WT$*L~qvK`v57G1M@rHv5chnzrpQ8(b5^um}+Tfvo+rNcI$6fG3H(oXlUu)t& zQ{#iBL`)?N>S_*tPT!OGO!)K_)^Yy`+rIaP2US@&`${hm`>B7YTtoFe#d=B2FL^93zL|mB{h7wjiubHK578BW zzrkNHSaBR6ShS;$HW2(J`LJxD8#}~#9&w-QGO~rL$;3u05Z{Z|Up>*qO7Mr~fpiZx z%NtCqzNXW}oDteDFxufCz^yWmiSg)lU>Sl(_62yAt<-3!YobKd=H2pd!_VqPCt=E& zcNdc54R7=p2LE(udYC@k$gf9-RErt;_3+^HUp^h__}T5L+dq1^B>W{al8N6Xwxh=n z{;+>kEy3S2u=gChr#O+^*3?+&iwS>5Lk&F2Pvd*=jn367`g(Qz1vR{f*sIfU;9+eu zD}08Y;Yn=YIre%UL?834{{SAyAvT8X@sEX1!bd)JYHZb0>XC!+SE)Cf+V@GftJ>$1 zgV5(cLe2VSz^jA(8})MHa; zN$Pt|9-!f(s`pR6D~;8w#C+#mY4O8Rp-!&ZO6k|5A7pZYkFj2}=b<@*1cR#C==X|e zcA2;WJsST&xR=iMq2N^5;-MXkPrqjP^y=#hd;DB;RR2rza$>$u_3V#%HU3^WH+q>5 z@xR?+mu-2r%dB`*E`h<46A6n+K1}>a3@BVCJ~*ilil;zL8trE^d8NxM?W`sS*GlW? z4>7IJ*(`5`0~M-!2?o6ZGkZDc!;<%@(9!38Mwh3d`od(q2i3lGAHy5_bw(77WQ zd59l62mat~Q-3`T{!W3v6KHMhM{oDE$NVLD`id@a3sIbAkKmEudxdwjI^gd=Hrl4c z5!3egw-fO}`%u{HKMS}I$+`N;{mEJaEFUr8{opRsG18U?2V%q-%o)y0nADi1si7H4 zj`5$#99zPHX#;Cli22TPpUC$r@6g^a&0ZukMPTn=id3wa|krgB^}>$ZzQ@+8e68GO_G?!fP=_MH4q#hcohu6ko< z!2C<_s66kN?Q*$D_1y}rsNfW%l2f5AXP2E2pKP%BsXmByK6PnfkR2h41<_BfM`&ot z{)Ox0A?rnKAO~MepP0E`W{KbxF#q-<7^+a=qQ?e9lsVvs9vi4(K%b>P)ykfL^UhUr z<4(BuY{=ByyQkTwKYupe|I23+{eRv!_WW1<<2`@YK7HZG=a##- zo)$Xk@0!^l`CG#+PJAysJg_C7OB~nW-a}Wvi+HT5Z@_#J_E0=~!+Cy+4aMK`8Xq9; zxf0)TAEIf2HaI(G&Q(s*ufPYpXV|BGl3BNXuw4#@Cxd(Vse{p1?&t6XVpHcIXcA82YMcTe1U;zWO;&JBP1;$5vze(8e%0_=JZh4ig)Q zUEJjns$R`ZAAD}*!74UG&y^6Kz{tu$LbNkBD-*nR$cP;*X$%Fw(@|aHR!a z!ezJ=mmTJTz#sO{eaPI$xIbN)^A_v{WB+6Wmz|Wm6c3gMUcbopeD^H<Be%=@`u2PcJ3(ORJ?}|<|>|v4!V7Yze0_|)~q&t*G+C!r$(=+dbiw$KGhlZ%g}k24LlwkVAmS>3r?{^Q25*9?hpHYutwdZ zm0lbD4pYCD#?=A#5cFwR0a`>SDhIaOjR4KnkT#vUbDOfVvB z5Kn5hLwyW({jwj7J@JRp5hn7(uJ1!ro#>DL3A`D4;H^o10{*xMsIzG1iJZgqI>^Hl zyUfqrt-XBuJx0HX8Z_FdrQQ&J5DScoUfxO9(v|dP#>#B5y#oy9HmpJ^s@OJu*%8l5 zc?nqD<(B%`K%<8*O>$~5jl#F#yTbeEec{{qyTXU~+rnFI7J%!FO~3{YI799*T5KrV z21Df+{?p1;G06OjnjIsbKTl7t-FY6&+OxrI1?<@i#D6-d6`~cAbFxm}x8MaAonlbn zjgwq%VGonCu1pmL6h)7HTkisnSr1^4TMf=H`9>wnwK8uGJm#-}|0?Z=!Rc z{(<4%>Du(e^1blylNtIGFWL8Tj@mjoI(@Q>vVHVFlbu<~JY|9l;qc>W+CKSR z)p)euqm{Ui`ztzv?K>1m&sx^+XmB>b?+SmW<}7ZXxLefO4F;(PQC~3efU&c`i*MD$ zRTuh*M>B&Y{#j4?UNBiMc$Sk7@+{HKma`j~LV6=@WjFFn1DDF>N=5aV2yGWInB*k8 zd=zZy3^sex2^h0xgsOq}^7>)rdp~QUhy`#Yx2hS8dE8k{n=K}jv z;GuAcqP*w!ls|)7`2rg|m+U1^XW349AkPJPz`y8}+!D7yG5IgaQJ$}5@Yg8D$i4`F zXcUm`aG0ZnYe(P9Ja=MW<742W)!^Y01NIX8CH|M%;V%B9ajIG&cH<1(zB6%)*H)wM zLI1Zkyh0p^4P-a=;b1>~&=a2W*jwRwV!_YxqL0x%xLv-&?C}fnotd=}=lFO$f!4Ej zl6DYR9Q1c4*2`teU$favk~x4z((ZYzI#_S0rq53r<0At{uv)^?43TAj#Og% z=vC-JE9 zy$8%Zm8?8`M{-}yd9